diff --git a/CHANGELOG.md b/CHANGELOG.md index 748c23f..d5b5c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ three commits past it), and a bug report can name a release instead of a sha nob Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased +- fix(project-bin/exec.sh): **the lint ratchet now runs after every clean mxbuild, and its verdict lands in the same BUILD-LOG row.** mxbuild proves the model compiles, not how it is built: on a field project a migration microflow shipped three commits inside loops (`mxcli lint` CONV011) under "✅ applied · mxbuild clean", because lint lived only in the gate-agent definition as an optional step and that agent had been spawned 0 times against 21 `exec.sh` runs. `exec.sh` now calls `bin/lint-gate.sh` after a clean build; a rise vs the committed baseline writes `⚠️ applied, LINT ROSE: ` and exits 1 while keeping the write (shape, not corruption), `SKIP_LINT=` is the only override and is recorded in the row, and a project without `lint-gate.sh` gets `lint not installed` rather than a blank cell. `agents/gate-agent.md` and `agents/agent-roles.md` now read the row instead of treating lint as optional; `learned-detection-gaps.md` gains the register row. Field run: a marketplace guest-group migration, 2026-09-24, ~13 s per run on a 10k-finding project — Maurits Visser - learn(ui-loop, learned-detection-gaps): **the screenshot harness is an instrument, and nobody scores it.** Two harness defects on the PRD benchmark's Arm A produced screenshots that were about to be written up as app defects. (1) The harness loaded each page at 1280 and then called diff --git a/agents/gate-agent.md b/agents/gate-agent.md index 1e7dbfa..733e482 100644 --- a/agents/gate-agent.md +++ b/agents/gate-agent.md @@ -92,8 +92,14 @@ own bug log before running anything you have not run here before. 2. **Compile gate** (if applicable): {{COMPILE_GATE_COMMAND}}. 3. **Coverage checklist** (Gate 3, `iterative-build-loop.md`): walk the module's confirmed business-rule coverage checklist item by item — CE-error-free ≠ done. -4. **Lint** (when the task calls for it): {{LINT_COMMAND}}. Flag *new* violations; don't fail the - gate on pre-existing baseline ones unless the task scope includes them. +4. **Lint** (always): `bin/exec.sh` already ran `bin/lint-gate.sh` after the clean mxbuild and + wrote the verdict into the same BUILD-LOG row (`✅ applied … lint unchanged` or + `⚠️ applied, LINT ROSE: CONV011 (+3) …`). Read that row first; re-run {{LINT_COMMAND}} only to + get the per-document list behind a rise. Name each rule that rose, the documents behind it, + and whether the rise belongs to the script under review or to earlier debt. A rise is a FAIL + for the script even when mxbuild is clean. Never re-baseline to turn a row green — that is + the user's decision, made in chat and recorded in the commit message. If the row says + `lint not installed`, say so as a finding: nothing has checked shape in this project. ## A clean result is not automatically a pass diff --git a/project-bin/exec.sh b/project-bin/exec.sh index 837d254..66d6b1d 100755 --- a/project-bin/exec.sh +++ b/project-bin/exec.sh @@ -2,7 +2,7 @@ # exec.sh — the guard chain around a model write. # # concurrent-writer guard → module-brief advisory → mxcli check → snapshot → baseline → exec -# → mxbuild gate → auto-restore on regression → SP reopen +# → mxbuild gate → auto-restore on regression → lint ratchet → SP reopen # # Usage: ./bin/exec.sh # @@ -281,6 +281,63 @@ if [ -f "$LAST_ERRS" ]; then fi +# ── Lint ratchet ───────────────────────────────────────────────────────────── +# Added 2026-09-24. mxbuild proves the model compiles; it says nothing about how it +# is built. On a field project a migration microflow shipped with three commits +# inside loops (mxcli lint CONV011) under a "✅ applied · mxbuild clean" row, because +# nothing on this path ever ran lint: the gate-agent listed lint as optional and had +# been spawned 0 times against 21 exec.sh runs, and lint-gate.sh had only ever been +# run by hand to seed its baseline. An instrument that lives in an agent definition +# is a suggestion; one that lives here runs. So lint runs HERE, after a clean +# mxbuild, and its verdict lands in the same BUILD-LOG row as the mxbuild verdict. +# +# Semantics: a rise vs the committed baseline exits 1 but does NOT restore the +# snapshot — lint findings are about shape, not corruption, and the write is +# fixable by a follow-up script; a restore here would teach people to skip it. +# SKIP_LINT= is the only override and the reason is written into the row. +# A project without bin/lint-gate.sh gets a "lint not installed" cell, never a +# blank one — a blank cell reads as fine, which is the false-green this exists +# to stop. ~13 s measured on a 10k-finding project. +LINT_RC=0; LINT_DETAIL="lint not-run" +run_lint_gate() { + if [ -n "${SKIP_LINT:-}" ]; then + LINT_DETAIL="lint SKIPPED (SKIP_LINT=$SKIP_LINT)" + echo " ⚠ lint ratchet skipped: SKIP_LINT=$SKIP_LINT"; return + fi + if [ ! -x "$PROJECT_ROOT/bin/lint-gate.sh" ]; then + LINT_RC=2; LINT_DETAIL="lint not installed (bin/lint-gate.sh missing — sync-project.sh installs it)" + echo " ⚠ $LINT_DETAIL"; return + fi + echo "→ Running lint ratchet (bin/lint-gate.sh, read-only)..." + _lo=$(mktemp /tmp/lint-gate-out.XXXXXX) + # exec.sh runs under set -e: a bare failing command here aborts the script with no BUILD-LOG + # row at all (measured on the first wired run in a real project, 2026-09-24). Capture explicitly. + LINT_RC=0 + bash "$PROJECT_ROOT/bin/lint-gate.sh" >"$_lo" 2>&1 || LINT_RC=$? + case "$LINT_RC" in + 0) LINT_DETAIL="lint unchanged vs baseline"; echo " ✓ lint: no rule rose vs baseline" ;; + 1) _rules=$(grep -E '^ +[A-Z]+[0-9]+ +[a-z]+ +[0-9]+ -> [0-9]+ +\(\+[0-9]+\)' "$_lo" \ + | awk '{printf "%s %s ", $1, $6}') + LINT_DETAIL="lint ROSE: ${_rules:-see output}" + echo " ✗ lint: rule(s) rose vs baseline — ${_rules:-see below}" + sed -n '/^FAIL/,$p' "$_lo" | head -60 | sed 's/^/ /' + echo " Fix the script and re-run, or accept the debt on purpose with" + echo " bin/lint-gate.sh --update-baseline and say so in the commit message." ;; + *) LINT_DETAIL="lint could not run (lint-gate exit $LINT_RC)"; echo " ⚠ $LINT_DETAIL" + tail -5 "$_lo" | sed 's/^/ /' ;; + esac + rm -f "$_lo" +} +# One place decides the applied-row wording, so both mxbuild-clean branches agree. +log_applied() { # $1 = mxbuild detail + run_lint_gate + if [ "$LINT_RC" -eq 1 ]; then + log_build "⚠️ applied, LINT ROSE" "$1; $LINT_DETAIL" + else + log_build "✅ applied" "$1; $LINT_DETAIL" + fi +} + # ── Pre-exec syntax gate ───────────────────────────────────────────────────── # `mxcli check --references` is the ONLY gate that can reject a bad script # before it mutates the .mpr. Every gate after this one recovers by snapshot @@ -539,7 +596,7 @@ if [ -x "$MXBUILD" ] && [ -x "$JAVA_EXE" ]; then # "0 error(s) found". Test the parsed count, never the file's existence. GATE_STATE="pass" echo " ✓ mxbuild: 0 errors — model is clean." - [ "$EXEC_STATUS" -eq 0 ] && log_build "✅ applied" "mxbuild clean" + [ "$EXEC_STATUS" -eq 0 ] && log_applied "mxbuild clean" else # ── Delta gate ───────────────────────────────────────────────────────── # A shared model means another workstream can leave it non-building; an @@ -656,7 +713,7 @@ PYEOF # the ordinary clean build lands. It was the only outcome with no log_build # call: PROJECT-A hit 7 consecutive clean execs that produced 0 log rows # (2026-08-06) and patched it locally before the template caught up. - [ "$EXEC_STATUS" -eq 0 ] && log_build "✅ applied" "mxbuild clean (no errors file written)" + [ "$EXEC_STATUS" -eq 0 ] && log_applied "mxbuild clean (no errors file written)" fi rm -f "$ERRORS_FILE" else @@ -764,6 +821,14 @@ if [ "$GATE_STATE" != "pass" ]; then exit 0 fi +if [ "$LINT_RC" -eq 1 ]; then + echo "" + echo "✗ Script applied and mxbuild is clean, but LINT ROSE — this is not a pass." + echo " $LINT_DETAIL" + echo " The write is kept (lint is shape, not corruption). Fix it before calling the task done." + exit 1 +fi + echo "" echo "✓ Script applied to $MPR_BASE." echo "" diff --git a/skills/agent-roles.md b/skills/agent-roles.md index 44a083b..f635310 100644 --- a/skills/agent-roles.md +++ b/skills/agent-roles.md @@ -257,7 +257,9 @@ You verify {{PROJECT}} after changes have already been applied to the `.mpr`. Re ## Gates to run (in order) 1. **Model check**: {{MODEL_CHECK_COMMAND}}. Expect 0 CE errors. 2. **Compile gate** (if applicable): {{COMPILE_GATE_COMMAND}}. -3. Optionally, {{LINT_COMMAND}} for best-practice regressions if the task calls for it. +3. **Lint, always**: read the lint cell `bin/exec.sh` wrote into the BUILD-LOG row (it runs + `bin/lint-gate.sh` after every clean mxbuild); a `LINT ROSE` cell fails the script. Re-run + {{LINT_COMMAND}} only for the per-document list. Never re-baseline to make a row green. ## Known gotchas {{PROJECT_SPECIFIC_GOTCHAS — e.g. stale .mpr.lock files, access-grant drops after CREATE OR REPLACE, stale proxy folders after a module rename}} diff --git a/skills/learned-detection-gaps.md b/skills/learned-detection-gaps.md index 1164ff6..0730376 100644 --- a/skills/learned-detection-gaps.md +++ b/skills/learned-detection-gaps.md @@ -45,6 +45,7 @@ verification rungs form a ladder, and a green result only certifies what that ru | `create import mapping` array-to-child binding | rungs 1–4; `DESCRIBE IMPORT MAPPING` looks correct | live retrieve after a real `import from mapping`: child list empty | Unverified-until-proven-live; severity not yet classified — read BUG-99's hold before blaming mxcli | BUG-99 | | Cross-module `ALTER PAGE ... INSERT` of a DG2 column | rungs 1–3 (`DESCRIBE` *omits* the malformed column) | rung 5: Studio Pro loader `InvalidCastException`; `mx check` also crashes | Forbidden construct; recovery is `create or replace page` | BUG-96 | | Expression in `ContentParams:` inside a customContent column | rungs 1–3 (`DESCRIBE` normalises correct and broken forms to the same text) | mxbuild / Studio Pro error pane: CE1613 | Bind with `Attribute:`, never an expression | `learned-datagrid-customcontent-binding.md` | +| A `commit` (or `create … commit`) **inside a loop** — and every other shape defect `mxcli lint` has a rule for (CONV011, empty container MPR006, empty nanoflow MPR002) | rungs 1–4 **including exec.sh's mxbuild gate**, and `mxcli check` (whose MDL001/MDL067 hints are not lint, however the script header labels them) | `mxcli lint` — but only if something *runs* it | On a 2026-09-24 field project the gate-agent, where lint was an optional step, had been spawned 0 times against 21 `exec.sh` runs and 234 raw `mxcli exec` runs; `lint-gate.sh` had run twice, by hand, to seed a baseline. A migration flow shipped three commits-in-a-loop under "✅ applied · mxbuild clean". Fix: `exec.sh` runs `lint-gate.sh` after every clean mxbuild and writes the lint verdict into the BUILD-LOG row (`LINT ROSE` exits 1, write kept, `SKIP_LINT=` recorded). **An instrument that lives only in an agent definition is a suggestion; count its BUILD-LOG rows before claiming coverage** | a marketplace guest-group migration, 2026-09-24 | | `Title = null` (or any `= null`) as an **XPath retrieve constraint** | rungs 1–3 — `check --references` clean, exec succeeds, and mxcli's **own OQL engine evaluates the query** and returns rows, so even a read-back looks right | native `mx check` / mxbuild: CE0161 | XPath has no `= null`; write `not(Title)` / `Title != ''` for the empty test. Note what makes this one expensive: mxcli's query engine is more permissive than the model loader, so "I ran the query and it worked" is **not** evidence the constraint is legal | PRD benchmark, Arm A (Maurits Visser), 2026-09 | | A page layout migrated with `ALTER PAGES … WHERE LAYOUT = X`, in a project whose page scripts **hardcode** `Layout:` inside `create or replace page` | rungs 1–6 — every rung, including live runtime: the app loads, every page renders, every journey passes. There is no error anywhere | a screenshot **of the nav shell** on every page, or `grep -l 'Layout: ' mdlsource/` after the migration | `create or replace page` rewrites the page **wholesale**, layout included — so re-running any older page script silently reverts that page to the old shell, and the app ships **two navigation shells at once**. Found when 4 of 7 screens had no navigation at all, just a bare hamburger, after a UI fix loop re-ran five page scripts. A layout migration is not done until the `Layout:` literal is fixed **in the source scripts**; the `ALTER` is a one-shot, the scripts are the repeat offender | PRD benchmark, Arm A (Maurits Visser), 2026-09 |