Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <rules>` and exits 1 while keeping the write (shape, not corruption), `SKIP_LINT=<reason>` 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
Expand Down
10 changes: 8 additions & 2 deletions agents/gate-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
71 changes: 68 additions & 3 deletions project-bin/exec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <script.mdl>
#
Expand Down Expand Up @@ -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=<reason> 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ""
Expand Down
4 changes: 3 additions & 1 deletion skills/agent-roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
1 change: 1 addition & 0 deletions skills/learned-detection-gaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<reason>` 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: <old>' 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 |

Expand Down
Loading