Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are.

## Unreleased

- fix(graph-sweep, app-facts): **the catalog guard checks the refs table, not the `build_mode` label; mxcli v0.24+ labels a complete catalog `source` and both scripts refused every one** ([#259](https://github.com/MendixMau/mxcli-project-toolkit/issues/259)). Field report: four module reviews recorded the sweep as "Instrument FAULT" on a fresh catalog with 5,368 refs rows. Now only an empty or kind-less refs table FAULTs, naming the mode and the per-kind edge counts; a model with no microflow-to-microflow calls (reached only from pages and buttons) still sweeps, and graph-sweep warns when no orphan-read kind is present. `manifest.json` records the real mode. `tests/wave2/test-catalog-edge-guard.sh` (20 checks, both layouts) fails 16 on master's scripts, including the verbatim FAULT line. Synthetic catalogs; a field run on a v0.24 project is still owed. — AlvinnYeo
- fix(conformance-check, review-module, verify-module, coverage-ledger): **per-BRD ledgers (`<Module>/coverage-ledger/<BRD>.md`) are all measured, one report per BRD, review/verify read every report of the run and accept a directory-only ledger, and an index file is named as one instead of "0 measurable rows"** ([#260](https://github.com/MendixMau/mxcli-project-toolkit/issues/260)). Field report: every module with an index ended in `FAULT: 0 measurable rows extracted from 1 ledger(s).`, while `LEDGER_FILE` on one per-BRD file measured 128 rows. `LEDGER_FILE` (file or directory) is in the usage text. **Upgrade notes:** (a) a `LEDGER_FILE` that does not exist now FAULTs instead of silently falling back to another ledger, and `verify-module.sh` reads `LEDGER_FILE` and passes it on to conformance through the environment, so a stale export now FAULTs the conformance rung; (b) baseline pointers of per-BRD rows are re-keyed `<BRD>:<pointer>`, so a project that had both a filled index and a per-BRD directory sees its old rows go unmatched and the new ones re-seeded as "newly baselined" on the first run after upgrade, with no regressions reported: run conformance once, then re-check. Per-BRD reports reach `docs/report.json` via #310, whose `report-normalize.js` filter is extended there (it reads only `report-<date>.tsv` today). Fixtures: `test-conformance-per-brd-ledger.sh` (19, both layouts), `test-review-conformance-per-brd.sh` (6, both layouts), `test-verify-ledger-hint.sh` (4), all failing on master. A field run on a per-BRD project is still owed. — AlvinnYeo
- fix(verify-module): **a stack rung exit 4 from `test-stack-up.sh` (port held by another process) is an instrument FAULT, not a FINDING** — follow-up to [#313](https://github.com/MendixMau/mxcli-project-toolkit/pull/313), whose header says to record it as blocked, not as a test failure; review of #313 found verify-module grading it as findings. Scoped to the stack rung (exit 4 is NOT APPLICABLE from conformance-check). `tests/wave2/test-verify-stack-blocked.sh` (6) fails 2 on master. — AlvinnYeo (port-holder report), review of #313
- learn(testing-shape, test-result-audit): **three test-strength rules, each with a denominator: refusals probed on the server, every undeclared 4xx a failure, one clean-database final run** ([#172](https://github.com/MendixMau/mxcli-project-toolkit/issues/172)). A field comparison of two builds against one frozen spec found the toolkit build checked permission refusals only in the client (a hidden menu item), logged unexpected 400/422 responses as information, and reached 20/20 by stitching three runs. `testing-shape.md` §7 gains three report lines (`N of N restricted pairs probed server-side`, `undeclared 4xx/5xx: 0 of M requests — each fails its step`, `one clean-DB run <id>: N of N scenarios incl. timers, not stitched`); `test-result-audit.md` gains check 2e, which carries the reasons and the field evidence and asks for those numbers. Both baseline skills stay inside the 80,000-word baseline budget (79,997): the reasons live in the on-demand audit skill, and two redundant sentences in `testing-shape.md` §4's verdict paragraphs were folded (the empty-set corollary is stated in the Trace row, `harness-architecture.md` and `report-schema.md`). The backing inbox note is trimmed to its unpromoted "Keep" items. — MendixMau
- learn(learned-detection-gaps, module-review): **two UI defects every model gate passes: a lazy-loaded combo box filter hides its preset, and a theme that pins row heights by position lets the next row cover an inserted widget** ([#299](https://github.com/MendixMau/mxcli-project-toolkit/issues/299)). Both passed check, mxbuild, lint and the scripted checks on a 127-module production app and were found only in the running app. Two register rows with the first oracle and the pre-build check (pluggable properties that change *when* data loads; grep the theme for fixed `height` on positional row classes before an insert), and two LOOK lines in `module-review.md` §4c with denominators (`N of N filters show their preset on first load`, `N of N widgets inserted into an existing container fully visible`). — jopterhorst
- fix(app-restart, test-stack-up): **a port held by another process before `run --local` is named and reported as "instrument blocked" (exit 4), never killed** ([#262](https://github.com/MendixMau/mxcli-project-toolkit/issues/262)). Studio Pro had started its own runtime on the app/admin ports a restart needed; `mxcli run --local` then failed with "Authentication failed" (the admin API answering was Studio Pro's), the next journey ran against Studio Pro's runtime, and four more runs were NOT RUN. `app-restart.sh` now checks the app port and the admin port (record's `adminPort`, else app + 10) before stopping anything — a holder outside the recorded process tree exits 4 with its name, pid and parent — and again after the stop, where anything still holding a port is by definition not ours. `test-stack-up.sh` runs the same check wherever it would tell you to start `run --local` (`--no-docker`, no Docker daemon, `--check` with a leftover record), on the record's ports, `RUN_LOCAL_APP_PORT`/`RUN_LOCAL_ADMIN_PORT`, or mxcli's defaults 8080/8090. Holder lookup: lsof, then ss, then netstat + tasklist on Windows; unnamed holders still block when nothing of ours is running. Fixtures: `test-app-restart.sh` +2 cases (14 assertions, all fail on the old script), `test-stack-up-local.sh` T7 (4 of 6 fail on the old script). — AlvinnYeo
Expand Down
7 changes: 4 additions & 3 deletions agents/review-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,10 @@ current — that block is the single source of truth and gets corrected independ
Both are read-only. Neither touches the `.mpr`. Both refuse to report a clean sweep from a stale
or empty catalog rather than silently passing.

**Stale-catalog self-heal:** if `graph-sweep.sh` faults because `catalog_meta.build_mode` is not
`full`, you may run `./mxcli -p <mpr> -c 'REFRESH CATALOG FULL'` yourself and retry once — this
rebuilds the catalog database, it does not write to `.mpr` content, so it's outside what the
**Stale-catalog self-heal:** if `graph-sweep.sh` faults because the catalog is stale, its refs table
is empty, or its refs carry no `RefKind` (its `build_mode` label is reported, not judged — mxcli v0.24+ writes
`source` for a complete catalog), you may run `./mxcli -p <mpr> -c 'REFRESH CATALOG FULL'`
yourself and retry once — this rebuilds the catalog database, it does not write to `.mpr` content, so it's outside what the
project's write-approval rule protects. If the retry still faults, report it; don't loop.

## Four hard rules — STOP, not suggestions
Expand Down
26 changes: 18 additions & 8 deletions project-bin/app-facts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,13 @@ fi

q() { sqlite3 -noheader "$DB" "$1"; }
BUILD_MODE="$(q "SELECT Value FROM catalog_meta WHERE Key='build_mode';")"
if [ "$BUILD_MODE" != "full" ]; then
echo "FAULT: catalog build_mode is '$BUILD_MODE' after graph-report; refs/activities are not trustworthy." >&2
exit 2
fi
# The build_mode LABEL is not checked (fixed 2026-10-13, field report 2026-10-11): mxcli v0.24.0
# labels a complete catalog 'source', and the old `!= full` test refused it on every run. What
# this script needs is checked instead, below: the tables by name, non-empty refs/activities,
# and at least one refs row that carries a RefKind (the dependency facts group by it). The label
# is recorded in manifest.json as found, so a reader can still see it. Zero 'call' edges is NOT a
# fault: an early module reached only from pages and buttons has none, and its call graph is
# legitimately empty — requiring them (the first cut of this fix) refused such apps.
MISSING=""
for t in modules_data entities_data microflows_data activities_data pages_data scheduled_events_data refs catalog_meta projects; do
[ "$(q "SELECT count(*) FROM sqlite_master WHERE name='$t';")" = "1" ] || MISSING="$MISSING $t"
Expand All @@ -264,10 +267,17 @@ N_MODULES=$(q "SELECT count(*) FROM modules_data;")
N_REFS=$(q "SELECT count(*) FROM refs;")
N_ACTS=$(q "SELECT count(*) FROM activities_data;")
if [ "${N_MODULES:-0}" -eq 0 ] || [ "${N_REFS:-0}" -eq 0 ] || [ "${N_ACTS:-0}" -eq 0 ]; then
echo "FAULT: catalog is vacuous (modules=$N_MODULES refs=$N_REFS activities=$N_ACTS). An empty graph is not a clean app." >&2
echo "FAULT: catalog is vacuous (modules=$N_MODULES refs=$N_REFS activities=$N_ACTS, build_mode '${BUILD_MODE:-unset}'). An empty graph is not a clean app." >&2
exit 2
fi
KINDS="$(q "SELECT k || '=' || n FROM (SELECT COALESCE(NULLIF(RefKind,''),'(none)') k, count(*) n FROM refs GROUP BY k ORDER BY n DESC LIMIT 8);" | paste -sd' ' -)"
N_KINDED=$(q "SELECT count(*) FROM refs WHERE COALESCE(RefKind,'') <> '';")
if [ "${N_KINDED:-0}" -eq 0 ]; then
echo "FAULT: catalog refs carry no RefKind (build_mode '${BUILD_MODE:-unset}', $N_REFS refs by kind: ${KINDS:-none})." >&2
echo " The dependency and call-graph facts are read by kind. Refusing to write facts without one." >&2
exit 2
fi
log "catalog ok: modules=$N_MODULES refs=$N_REFS activities=$N_ACTS (${T1}s)"
log "catalog ok: build_mode=${BUILD_MODE:-unset} modules=$N_MODULES refs=$N_REFS (${KINDS:-none}) activities=$N_ACTS (${T1}s)"

# ---------------------------------------------------------------------------------------------
# Step 2 — inventory + dependencies, straight from the catalog through the REAL module column.
Expand Down Expand Up @@ -546,7 +556,7 @@ fi
FACTS_DIR="$FACTS_DIR" MX_VERSION="$MX_VERSION" MPR_BASE="$MPR_BASE" DB="$DB" \
T1="$T1" T2="$T2" T3="$T3" TALL="$(( $(date +%s) - START_ALL ))" \
LOOP_STATUS="$LOOP_STATUS" LOOP_REASON="$LOOP_REASON" N_CAND="$N_CAND" N_DESC="$N_DESC" N_FAILED="$N_FAILED" N_RULES="$N_RULES" \
N_MODULES="$N_MODULES" N_REFS="$N_REFS" N_ACTS="$N_ACTS" WORKERS="$WORKERS" MAX_LOOPS="$MAX_LOOPS" "$PY" - <<'PY'
BUILD_MODE="$BUILD_MODE" N_MODULES="$N_MODULES" N_REFS="$N_REFS" N_ACTS="$N_ACTS" WORKERS="$WORKERS" MAX_LOOPS="$MAX_LOOPS" "$PY" - <<'PY'
import json, os, datetime, hashlib
F = os.environ["FACTS_DIR"]
def sha(p):
Expand All @@ -563,7 +573,7 @@ man = {
"generated": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
"mpr": os.environ["MPR_BASE"], "mpr_sha256_16": sha(mp) if os.path.exists(mp) else "",
"mxcli": os.environ["MX_VERSION"],
"catalog": {"modules": int(os.environ["N_MODULES"]), "refs": int(os.environ["N_REFS"]), "activities": int(os.environ["N_ACTS"]), "build_mode": "full"},
"catalog": {"modules": int(os.environ["N_MODULES"]), "refs": int(os.environ["N_REFS"]), "activities": int(os.environ["N_ACTS"]), "build_mode": os.environ.get("BUILD_MODE", "")},
"timings_s": {"catalog_and_graph_report": int(os.environ["T1"]), "inventory_and_dependencies": int(os.environ["T2"]), "loops": int(os.environ["T3"]), "total": int(os.environ["TALL"])},
"options": {"workers": int(os.environ["WORKERS"]), "max_loops": int(os.environ["MAX_LOOPS"])},
"sections": {
Expand Down
Loading
Loading