Skip to content

Four instruments that were installed and not working, and the obligations to notice - #32

Merged
MendixMau merged 10 commits into
masterfrom
claude/mxcli-project-setup-clhy8g
Sep 9, 2026
Merged

Four instruments that were installed and not working, and the obligations to notice#32
MendixMau merged 10 commits into
masterfrom
claude/mxcli-project-setup-clhy8g

Conversation

@MendixMau

@MendixMau MendixMau commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Direct lane (changes to skills/, bug-logs/, bin/, project-bin/, project-tests/, pipelines)

Four commits, one finding, found four times in two days. Each is an instrument or a pass that was installed, routed, believed in, and silently doing nothing — while mx check, mxcli lint, the MDL suite and the e2e journeys were all green. Three of the four were found by a human looking at the app, not by the harness.

The commits are independent and any can be dropped. If time is short, read commit 2.

# What was not working What it cost, measured Found by
1 obligations.tsv did not reach the design stages, so a Stage-3 pass owed no mark a build plan routing four user tasks by role XPath where the assignee is data on the record — every stage of every project into every approver's inbox a human asking whether the workflow skills were being used
2 nothing read the built stylesheet, and the design check's header asserted it was unnecessary 55 correctly-ported design tokens reaching nothing; two build phases shipped in the framework's default blue a human opening a screenshot
3 check-page-shell.sh could not resolve its own input, and could not bound a page body it had never passed once on this project shape; 9 page titles built with a CSS class instead of a heading render mode the same screenshot
4 snapshot-mpr.sh did not gitignore its own output 2,089 copies of the client model committed, and then an exec that refused to run because the snapshotter's output looked like uncommitted model changes mxcli exec blocking itself

Commits 3 and 4 were found while building on top of commit 2's fix, which is the honest reason they are in the same PR: once you start opening the instruments, they keep being empty.


Commit 2 — check-design-reaches-app.sh: did any of the design system ARRIVE in the built app?

What changed and why (one paragraph):

The gap this closes is a sentence in its own sibling. project-bin/check-design-portability.sh's header opened by asserting "a design system's tokens port into Mendix perfectly — colours, radii and spacing variables survive the SCSS port intact. Its rules may not." The first half is false, and it was the sentence that made everyone comfortable. Tokens survive the port into a file; nothing about landing in the file makes the framework read them. On a real run, 55 tokens sampled from the customer's own screenshots were ported correctly, into the right file, at the stage that owed them — and the app rendered every screen in Atlas's default blue for two more build phases. New instrument project-bin/check-design-reaches-app.sh reads the BUILT stylesheet, which is the one artifact nothing else in this repo reads, and reports four passes with denominators: framework knobs bound to a design token · tokens arrived · classes arrived · dead SCSS bridges, plus a WARN for classes the model asks for that the built sheet never defines. New obligation row design-reaches-app (mdl / project / from-stage 3, denominator yes, degradable) makes someone owe the answer, because the check was never the hard part — nobody was owed an answer, so every gate it passed through was green-by-absence. check-design-portability.sh's falsified premise is corrected in place, with what it cost and a pointer to the new sibling; a header that states a wrong assumption confidently is worse than no header. ui-preflight-pages.md gains hard rule B0 at the consumer's seat — before the first page of the project, prove the design system reached the app — because the ordering is the finding.

Why it does not check for a known default colour. The tempting check is "is --brand-primary still #264ae5". That is a cache of one framework's one version — the defect CLAUDE.md's authoring rule 5 forbids — and it goes stale on the next Atlas release. So the question is inverted and then needs no table at all: is the winning definition of each knob the framework declares in its own customization file pointed at a design-system token? A framework enumerates its own knobs; that file is what it is for. Zero bound is not a style opinion, it is a wire nobody connected.

It requires a build and exits 2 without one. "Did it reach the app" cannot be answered from source, and answering it from source is exactly how this shipped.

Field evidence — which real project this ran against, and what it measured there (CLAUDE.md → "Shipping an instrument", rule 4):

A MOC/PSSR app replacement (requirements-driven, single module, Mendix 11.14, Atlas 3 with $use-css-variables: true), 2026-09-09.

Three independent failures, none visible to anything in this toolkit:

  1. The bridge targeted a dead layer. The port wrote $brand-primary: var(--brand). This Atlas reads CSS custom properties and never reads that SCSS variable, so the assignment compiled to nothing. Measured in deployment/web/theme.compiled.css: --brand present as the sampled red · --brand-primary untouched at the Atlas default · var(--brand) appearing 0 times.
  2. The component classes were never ported at all. 20 defined in the design system, 0 present in the built stylesheet. ds-muted sat on 12 live widgets and resolved to the body colour, so every caption on every page rendered at full ink.
  3. Even what it did set lost the cascade. A project theme module's :root is emitted before theme/web/custom-variables.scss in the compiled sheet, so a knob override written in the module could never win. The mapping has to live in the framework's own customization file.

What was green the whole time: mx check 0 errors · mxcli lint 0 errors · 7 of 7 MDL tests, 16 assertions · two e2e journeys, 3/3 and 4/4 · check-design-portability.sh nothing to say. Every one of those reads the model or the authored stylesheet, and both defects live between the authored stylesheet and the built one. It was found by a human opening a screenshot and asking why the UI was poor.

Measured before and after, same project, same instrument:

Pass Before After
Framework knobs bound to a design token 0 of 35 29 of 35
Design-system tokens present in the built sheet 43 of 43 43 of 43
Design-system component classes present 0 of 20 28 of 28
var(--brand) occurrences in the built sheet 0 present
exit code 1 0

The 6 knobs still unbound are Atlas defaults this design system deliberately does not claim. Unclaimed is not unbound, and inventing an opinion to make a number go green would be worse than the number.

It found a real, new, unreported defect on its first field run. The WARN pass reported 17 of 23 class names carried by live widgets that no stylesheet defines — aggregate-banner, banner-label, banner-value, card-name, card-row, card-who, chip, chip-label and nine more, plus ds-help. Class names typed from memory instead of read out of ds.css, on a project that had passed every other check. Each is now on the record with its denominator in that project's docs/design-reaches-app.md, promoted into ds.css or removed at its own build row.

The obligation was proven to go red both ways, not asserted. Against the real project: PASS with the numbers · PENDING with the artifact moved away (0 of 1 discharged; NOT DONE: project — owner: mdl-agent) · FAULT on a mark carrying no denominator (a pass that cannot say what it covered has not covered it).

  • Golden input captured, not hand-written. tests/wave2/fixtures/design-reaches-app/ reproduces the structure of a real mxbuild 11.14.0 theme.compiled.css, and CAPTURE.md states exactly what was changed from the capture and why: the colour literals only, because the real palette is a customer's and this repository is public. No line reflowed, no block reordered, no selector altered, no structure simplified. It also lists the counts measured on the real build. Every structural fact the parser depends on is verbatim, because each is a fact nobody would have imagined — :root { alone on its line; a bare :root and a scoped :root.theme-neutral both declaring --brand-primary, with the scoped one appearing later in the file; shades derived at runtime via color-mix(in srgb, var(--brand-primary), …), which is why binding the one knob suffices and why a hardcoded-default check is the wrong check; comment lines inside the :root block.
  • The instrument's own first-draft bug is pinned as a fixture case, in both directions. It read "last occurrence in the file" and reported a correctly bound knob as unbound, because the real sheet's last --brand-primary sits inside :root.theme-neutral, a theme the app is not wearing. built-scoped-only.css pins that a theme-scoped binding is not a binding; the fixed case pins that the bare-:root binding is. An instrument that miscounts its own denominator teaches people to ignore it.
  • tests/wave2/test-design-reaches-app.sh — 23 assertions, all passing. It pins the state that shipped as a failure (if that fixture ever passes, the instrument has stopped instrumenting); pins "tokens arrived" apart from "the design system arrived", because that is precisely the distinction the old header collapsed; pins no build → exit 2, never 0; and pins the dead-SCSS-bridge check as firing only where the theme runs on custom properties — the same line is the correct bridge where it does not, and reporting it there is how a check earns being switched off.
  • Both layouts: every path resolves relative to the project root or is passed as an argument, the same contract as check-design-portability.sh; CUSTOM_VARS is overridable for a project that renamed the framework file. Both platforms: POSIX shell, no Node, no mac-only tools.
  • A producer for every consumer: the artifact this reads is deployment/web/theme.compiled.css, produced by the mandatory build step, and the instrument says so and exits 2 when it is absent.
  • Routed baseline for architect,mdl,gate,review at stages 3 and 5, and added to MXTK_PROJECT_BIN so it installs into projects. Trigger-phrased per authoring rule 1: "after the FIRST build that follows any design-system port, and before any page is built on it."

Honest limits.

  • check-design-reaches-app.sh and check-design-portability.sh remain two scripts, deliberately. They read different artifacts and answer different questions — can the rules match the DOM versus did any of it arrive — and merging them would give the merged thing a build dependency the linting half does not need.
  • The knob-bound number is a report, not a gate, above zero. Only 0 of N fails. I have one project's data on what a healthy ratio looks like (29 of 35), which is not enough to set a threshold, and a threshold guessed from one project is how a check starts crying wolf.
  • Framework-specific in one place, and it is named. The $use-css-variables probe is an Atlas fact. It is confined to pass 4, skipped when the flag is absent, and the flag's absence is reported as unknown rather than assumed either way.

Commit 3 — check-page-shell.sh could not find its input, and could not bound the page body

It is routed baseline for mdl/gate/review, installed into every project, and it had no fixture at all — which is why both defects below sat in it.

1. It could not resolve its own input. The wireframe path was hardcoded design/wireframes/<PageName>.html. Wireframes drawn from a legacy system are named after that system's screens (moc-project-detail.html) while the Mendix pages are named in Mendix convention (MOCProject_Detail). Measured on the same project: 15 wireframes, 8 built pages, "no wireframe" reported for every page, exit 2. It had never passed once.

Now an optional two-column design/wireframes/PAGE-MAP.tsv. Absent map, behaviour is unchanged. A page the map does not name is still a miss and is reported naming the map, so the map cannot hide a page by omission. A row pointing at a wireframe that does not exist is reported too, because a stale row is how a rename turns into a page nobody checks.

2. It could not bound the page body. "To the closing brace at column 0" reads the rest of the file for a page whose body is on one line. A nav-shell script declaring five such stubs reported "page MOCProject_Overview declares 5 H1 titles; its wireframe draws one". Every number was the count for the whole file.

That is the worse kind of false positive: it fires on correct code, it cites a real rule, and the fix it asks for would have deleted four working pages' titles.

Two line-based repairs were tried and each broke the other case. A "started on any brace" scan exits on a balanced one-line construct, so a multi-line declaration carrying Params: { $X: Mod.Entity } ended the body before it opened and a page with a correct RenderMode: H1 was reported as having none. A "started only when depth > 0" scan never starts on a one-line body, where the braces balance within the line. Only the paren state separates the two, so the extractor now counts characters and tracks parens.

What it found once it could run: 9 title widgets across 4 page scripts using Class: 'h1' instead of RenderMode: H1. That is the same defect the screenshots showed — Class: 'h1' leaves a DYNAMICTEXT an inline <span> that Atlas merely sizes, so the page title and its caption rendered on one line, the caption starting mid-word against the heading. All nine retrofitted on the project; both journeys green after.

  • tests/wave2/test-page-shell-body.sh — 18 assertions, all passing. Both extractor cases are pinned in one file, because that is the pair that has to hold at once. It also pins that the rules still FIRE on a real Class: 'h1' title and on two H1s in one body — a fixture that only proved the false positives were gone would have let a dead check through. And it pins the "inspected nothing" exit 2 as not a pass.
  • Field-run before and after: 8 violations across 22 scripts, of which 3 were false and 5 real → 5 pages inspected, 0 violations, 4 pages carrying an explicit per-line opt-out with a reason.

Commit 4 — snapshot-mpr.sh gitignores its own output

It writes a full copy of the .mpr and every mprcontents unit before each exec and keeps the five newest. Nothing ignored it.

So a project commits the client's whole model several times over — and then mxcli exec refuses to run, because exec.sh reads the pruned snapshot as uncommitted model changes. The snapshotter's output blocking the snapshotter's own guard. Measured: 2,089 snapshot files tracked, and an exec that could not proceed until they were untracked by hand.

Written in the snapshotter rather than in init-project.sh deliberately: there it also reaches every project that already exists, on its next exec, instead of only the ones scaffolded after today. A producer guarantees its own output is ignored. Guarded on the project actually being a git repo, appends only when the line is absent, and says on stderr when it adds it — a silent .gitignore edit is its own surprise.


Commit 1 — workflow-count: the first obligation below stage 5

One row in obligations.tsv, and the row's own novelty is the finding: it is the first obligation below stage 5. Every existing row — look, sweep, journeys, coherence, skeleton — is from-stage 5, so this table, the only mechanism in the repo for "a judgement pass owes a mark", did not reach the design stages at all. workflow-structure-rules.md §12 is a ten-row count with a denominator on every line, and it is the only place in the pipeline where a workflow's design is checked against the requirement it came from — and it fired only if someone happened to run it. The table's own header already names this disease: "PROSE HAS NO EXIT CODE. A skill that is never invoked leaves no mark, and a missing mark reads as a clean one." The new row is workflow-count, architect, project scope, denominator yes, from-stage 3, artifact architecture/workflow-count.md; obligation-check.sh ranks from-stage generically, so no code change was needed. It is deliberately not an optin row: an artifact a project must think of in order to owe it is an artifact nobody owes, which is the disease the table was built for — and a workflow-free project discharges it in one line, "0 workflows, nothing to count", exactly how §12's own rows 2/3/4/9 are written. The second, cheap half: agents/architect-agent.md cited workflow-structure-rules.md once and inlined nothing, against this repo's own authoring rule 2 ("a skill only works when its text is in the acting agent's context — a citation is not a read"). The agent that owns Stage 3 now carries the ten rows, the artifact it writes, and the two that actually bite — row 5 (targeting mechanism named with the sentence it came from quoted, plus §6's warning that a role XPath over a nominated assignee delivers the task to everyone holding that role) and row 10 (N constructs in the diagram, N rows in the plan). Added outside the ROUTING:BEGIN/END markers so render-routing.sh --check stays in sync.

Field evidence: a no-code app replacement (requirements-driven, single module, three workflow definitions), 2026-09-08.

  • The defect this would have caught, and what it would have cost. The agent reached workflow-structure-rules.md with a construct question ("can MDL write a parallel split?"), took §11, filled §12 row 10 only, and never ran rows 1–9. Its build plan then specified "targeting XPath on all four tasks ([%UserRole_X%])" — for four user tasks whose assignees are data on the record. §6 is explicit that this is "not a near miss, it is a different application", and this project had deliberately collapsed five approver populations into one Approver role, so a role XPath would have put every stage of every project in every approver's inbox. It was caught at Stage 4, with the plan written, by a human asking whether the workflow skills were actually being used — not by any gate. Corrected to §6's preferred shape (one shared targeting microflow plus four thin wrappers, fallback chain nominee → eligibility → backstop with LOG WARNING), which also satisfies §12 row 6 structurally.
  • Retroactively running the count produced a real artifact with real numbers: 9 of 9 paths ending exactly once · 0 of 0 boundary events (the source has no reminders and no escalation, confirmed across all 44 legacy definitions) · 4 of 4 targeting mechanisms with their source sentence quoted · 1 of 1 multi-user task with decision method consensus and completion timing all participants completed, both sourced to business rules · 2 of 2 expressions using only $WorkflowContext. Four of the ten rows are legitimately 0 of 0 and say so.
  • Proven to go red both ways: PASS with the numbers · PENDING with the artifact moved away · FAULT with a mark carrying no denominator.
  • A postscript that arrived after this commit, and is worth the maintainer's eye. Running §12 properly on the next phase found that one of its own rows carried a wrong citation: the claim "submit is refused until all 8 experts are named" cited a business rule that actually reads "Approve and Reject are available only to the named assignee of the current stage". No rule refuses submit on the experts at all. That is exactly the defect this row exists to catch — a claim carrying a denominator and a reference, where the reference does not support the claim — and it survived because a citation nobody opens reads as evidence. It was caught by reading the rule out of the requirements before scripting, which is the only thing that catches it. Corrected on the project, not here; noted because it is the argument for the row.
  • Honest limit on the second half. The obligation row reaches every project immediately, because obligations.tsv is read from the toolkit at gate-check time rather than copied into projects. The inlined §12 reaches new projects and projects whose architect stub is still an untouched stub — sync-project.sh correctly keeps a completed agent file, so an existing project with a finished architect agent gets the mechanical catch but not the inline text. That is the accepted install contract, not a defect, but it is worth knowing which half travels.
  • Why the skill text itself is untouched. §12 row 5 was already correct, explicit, and carried its own denominator. The failure was delivery, not content, and process/preflight-skill-baseline-2026-08-31.md measured exactly that trap: the "failing" skill scored zero violations when actually read, and a blind rewrite would have churned proven text.
  • A broader exposure, deliberately not fixed here. §12 is a symptom, not the disease: at least three other Stage-3 passes have the same shape — modularize-domain.md's five boundary criteria, architecture-blueprint.md Step 4's Buy/Build verdicts, and design-artifacts.md Step 3b's scope crosscheck. On that project two happened to be run properly and §12 happened not to be; that difference was luck, not process. Commits 2, 3 and 4 are what happened to three more of them over the next two days.

Repo-wide checks

  • No client data anywhere in the diff — bin/check-no-client-data.sh clean over 462 tracked files; the project appears only as "a MOC/PSSR app replacement", and the fixture's colour literals are placeholders, stated as such in CAPTURE.md.
  • bin/check-scripts.sh72/72 shell scripts and 17/17 Node instruments parse.
  • bin/render-routing.sh --check — surfaces in sync, every skill routed or exempted; baseline 73,342 words against an 80,000 budget.
  • bin/check-portability.sh — clean over 113 files. POSIX shell and awk throughout; no Node, no mac-only tooling, no new platform assumption.
  • CHANGELOG.md line per change, crediting the source project.
  • New bug entries headed ## BUG-DRAFT-<slug>: — n/a, no bug entries.
  • Only the two new fixtures were run, per this repo's "Testing this toolkit" rule. Both are self-contained on temp directories they create, so neither can mix another session's in-flight breakage into its result. 41 assertions, 0 failures.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M

workflow-structure-rules.md section 12 is a ten-row count with a denominator on
every line, and it is the only place in the pipeline where a workflow's design
is checked against the requirement it came from. Nothing required it to run.

The row that fixes it is one line in obligations.tsv, and it is the first
obligation below stage 5. That is the real finding: every existing row - look,
sweep, journeys, coherence, skeleton - is from-stage 5, so this table, the only
mechanism here for 'a judgement pass owes a mark', did not reach the design
stages at all. obligation-check.sh ranks from-stage generically, so no code
change was needed.

Measured on a requirements-driven app replacement. The agent read section 11
because it had a construct question, filled section 12 row 10, and never ran
rows 1-9. Row 5 - user tasks whose targeting mechanism is named, with the
sentence it came from quoted - would have caught a build plan specifying a role
XPath for four user tasks whose assignees are data on the record. Section 6
calls that 'not a near miss, it is a different application': one Approver role
covered five populations, so every stage of every project would have landed in
every approver's inbox. It was found at Stage 4 by a human asking whether the
workflow skills were being used, with the plan already written.

Deliberately not an optin row. An artifact a project must think of in order to
owe it is an artifact nobody owes, which is the disease this table exists for. A
workflow-free project discharges it in one line - '0 workflows, nothing to
count' - exactly how section 12's own rows 2, 3, 4 and 9 are written.

Proven to go red both ways against a real project rather than asserted: PASS
with the numbers present, PENDING with the artifact removed, FAULT on a mark
that carries no denominator.

Second change, the cheap half. The architect stub cited
workflow-structure-rules.md once and inlined nothing, against this repo's own
authoring rule 2 - a citation is not a read. It now carries the ten rows, the
artifact it writes, and the two that bite: row 5 with section 6's targeting
warning, and row 10's construct denominator. Added outside the ROUTING markers
so render-routing --check stays in sync.

CI guards clean: 71/71 shell and 17/17 node parse, routing in sync,
check-portability clean over 110 files, leak guard clean over 453. The one
fixture referencing either changed file only deletes architect-agent.md to test
the missing-agent path and never greps its content, so it was verified by
inspection rather than run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
The second proof of the same finding as the workflow-count row on this branch:
obligations.tsv did not reach the design stages, so a design pass could be
skipped entirely and every gate stayed green.

WHAT HAPPENED (field run, 2026-09-09, a MOC/PSSR app replacement)

55 design tokens, sampled from the customer's own screenshots, were ported into
the project's theme module correctly and at the stage that owed them. The app
then rendered every screen in Atlas's default blue for two more phases of
build.

Three independent failures, none visible to anything in this toolkit:

  1. The bridge assigned SCSS variables ($brand-primary: var(--brand)) to a
     theme running $use-css-variables: true, so it compiled to nothing.
     Measured in the built sheet: --brand present as the sampled red,
     --brand-primary untouched at the Atlas default, var(--brand) appearing
     0 times.
  2. The 20 component classes were never ported at all. ds-muted sat on 12
     live widgets and resolved to the body colour.
  3. A theme module's :root is emitted BEFORE theme/web/custom-variables.scss,
     so even what it did set lost the cascade.

mx check 0 errors. mxcli lint 0 errors. MDL suite green. Both e2e journeys
green. check-design-portability.sh had nothing to say — and its header opened
by asserting the premise that failed: "a design system's tokens port into
Mendix perfectly". Found by a human opening a screenshot and asking why the UI
was poor.

WHAT THIS ADDS

project-bin/check-design-reaches-app.sh reads the BUILT stylesheet, which is
the one thing nothing else here reads, and reports four passes with
denominators: framework knobs bound to a design token, tokens arrived, classes
arrived, dead SCSS bridges. Plus a WARN for classes the MODEL asks for that the
built sheet never defines.

It deliberately does NOT look for a known default hex. That is a cache of one
framework's one version, which CLAUDE.md's authoring rule 5 forbids and which
goes stale on the next Atlas release. The question is inverted instead: is the
winning definition of each knob the framework declares in its own
customization file pointed at a design-system token? A framework enumerates
its own knobs; that file is what it is for. Zero bound is not a style opinion,
it is a wire nobody connected.

It requires a build and exits 2 without one. "Did it reach the app" cannot be
answered from source, and answering it from source is how this shipped.

The obligation row is why this is not only a script. The check was never hard;
what was missing is that nobody was owed an answer, so every gate it passed
through was green-by-absence. design-reaches-app is mdl/project/from-stage 3
with a denominator, degradable — a project with no build yet discharges it by
saying "binding UNPROVEN, no build", and silence does not. It is owed by mdl
rather than review on purpose: the agent that performed the port shows it
landed.

check-design-portability.sh's falsified premise is corrected in place rather
than quietly deleted, with what it cost and a pointer to the new sibling. A
header that states a wrong assumption confidently is worse than no header.

ui-preflight-pages.md gains hard rule B0 at the consumer's seat: before the
first page of the project, prove the design system reached the app. The
ordering is the finding — the port happened when it was owed and the pages were
built on top of it unproven.

FIELD-PROOF

Ran against the real project, both states. Before: 0 of 35 knobs bound, 0 of 20
classes present. After: 29 of 35 bound, 43 of 43 tokens, 28 of 28 classes, exit
0. On its first field run the WARN pass found 17 of 23 class names carried by
live widgets that no stylesheet defines, ds-help among them — a real, new,
unreported defect on a project that had passed every other check.

The obligation was proven to go red both ways on that project: PASS with the
numbers, PENDING with the artifact moved away, FAULT on a mark carrying no
denominator.

tests/wave2/test-design-reaches-app.sh, 23 assertions, all passing. It pins the
state that shipped as a FAILURE, pins "tokens arrived" apart from "the design
system arrived", pins a theme-scoped binding as NOT a binding in both
directions (the instrument's first draft read "last occurrence in the file" and
reported a correctly bound knob as unbound, because the real Atlas sheet
declares --brand-primary inside :root.theme-neutral after the bare :root), and
pins "no build" at exit 2 rather than 0. Fixture provenance and the counts
measured on the real build are in fixtures/design-reaches-app/CAPTURE.md,
including exactly what was changed from the capture and why: the colour
literals only, because the real palette is a customer's and this repo is
public. No line reflowed, no block reordered, no selector altered.

Both layouts: every path resolves relative to the project root or is passed as
an argument, same contract as check-design-portability.sh. Both platforms:
POSIX shell, no Node, no mac-only tools; check-portability clean over 112 files.

CI guards: check-scripts 72/72 shell and 17/17 Node parse, render-routing
--check in sync (baseline 73,122 words against an 80,000 budget), leak guard
clean over 461 tracked files. Only the one new fixture was run, and it is
self-contained on temp directories it creates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
@MendixMau MendixMau changed the title obligations: reach stage 3 — a workflow's design count now owes a mark obligations: reach stage 3 — a workflow's design count, and whether the design system reached the app Sep 9, 2026
…e page body

It is routed baseline for mdl/gate/review and installed into every project, it
had no fixture, and until today it had never passed once on a project whose
wireframes are named after the source system's screens. Both defects are the
same shape as the design-system one on this branch: an instrument that was
installed and silently not working.

1. IT COULD NOT RESOLVE ITS OWN INPUT.

   The wireframe path was hardcoded design/wireframes/<PageName>.html.
   Wireframes drawn from a legacy system are named after that system's screens
   (moc-project-detail.html) while the Mendix pages are named in Mendix
   convention (MOCProject_Detail). Measured on a MOC/PSSR app replacement: 15
   wireframes, 8 built pages, "no wireframe" reported for every page, exit 2.

   Now an optional two-column design/wireframes/PAGE-MAP.tsv. Absent map,
   behaviour is unchanged. A page the map does NOT name is still a miss and is
   reported naming the map, so the map cannot hide a page by omission. A row
   pointing at a wireframe that does not exist is reported too, because a stale
   row is how a rename turns into a page nobody checks.

2. IT COULD NOT BOUND THE PAGE BODY.

   "To the closing brace at column 0" reads the rest of the FILE for a page
   whose body is on one line. A nav-shell script declaring five such stubs
   reported "page MOCProject_Overview declares 5 H1 titles; its wireframe draws
   one". Every number was the count for the whole file.

   That is the worse kind of false positive: it fires on correct code, it cites
   a real rule, and the fix it asks for would have broken four pages.

   Two line-based repairs were tried and each broke the other case. A "started
   on any brace" scan exits on a balanced one-line construct, so a multi-line
   declaration carrying Params: { $X: Mod.Entity } ended the body before it
   opened and a page with a correct RenderMode: H1 was reported as having none.
   A "started only when depth > 0" scan never starts on a one-line body, where
   the braces balance within the line. Only the paren state separates the two,
   so the extractor counts characters and tracks parens.

FIELD-PROOF

Ran against the real project. Before: 8 violations across 22 scripts, of which
3 were false and 5 real. After: 5 pages inspected, 0 violations, with 4 pages
carrying an explicit per-line opt-out and a reason.

What it found once it could run: 9 title widgets across 4 page scripts using
Class: 'h1' instead of RenderMode: H1. That is the same defect the screenshots
showed — Class: 'h1' leaves a DYNAMICTEXT an inline <span> that Atlas merely
sizes, so the page title and its caption rendered on one line. The CSS rule
added earlier on this branch is a safety net; RenderMode is the fix.

tests/wave2/test-page-shell-body.sh, 18 assertions, all passing. It pins both
extractor cases in one file because that is the pair that has to hold at once,
and it pins that the rules still FIRE on a real Class-only title and on two H1s
in one body — a fixture that only proved the false positives were gone would
have let a dead check through.

CI guards: check-scripts 72/72 shell and 17/17 Node parse, check-portability
clean over 113 files, render-routing --check in sync, leak guard clean.
POSIX shell and awk only; no Node, no platform-specific tooling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
It writes a full copy of the .mpr and every mprcontents unit before each exec
and keeps the five newest. Nothing ignored it, so a project commits the
client's whole model several times over — and then mxcli exec refuses to run,
because exec.sh reads the pruned snapshot as uncommitted model changes. The
snapshotter's output blocking the snapshotter's own guard.

Measured on a MOC/PSSR app replacement, 2026-09-09: 2,089 snapshot files
tracked, and an exec that could not proceed until they were untracked by hand.

Written here rather than in init-project.sh deliberately: here it also reaches
every project that already exists, on its next exec, instead of only the ones
scaffolded after today. A producer guarantees its own output is ignored.

Guarded on the project actually being a git repo, appends only when the line is
absent, and says on stderr when it adds it — a silent .gitignore edit is its own
surprise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
@MendixMau MendixMau changed the title obligations: reach stage 3 — a workflow's design count, and whether the design system reached the app Four instruments that were installed and not working, and the obligations to notice Sep 9, 2026
workflow-structure-rules.md §11's own instruction is "do not trust the date on
this table — check the binary". The table was probed on v0.20.0, this binary is
v0.21.0, and one project's build plan already CLAIMED it had been checked
against v0.21.0 without anyone running it. Running it found two facts that are
in no skill, no bug log, and no mxcli syntax output.

1. A USER TASK's PAGE must take a System.WorkflowUserTask parameter, not the
   workflow's context entity. A context-parameterised page passes
   mxcli check --references completely clean, execs, and round-trips through
   DESCRIBE. The native build then says: "The selected page 'X' should accept a
   parameter of type 'WorkflowUserTask', but expects parameters of types
   'MOCProject' instead."

2. A TARGETING MICROFLOW must accept TWO parameters, System.Workflow and the
   context entity, in that order. One parameter is the natural thing to write:
   "should accept parameters of type 'System.Workflow' and 'MOC.MOCProject'.
   Instead the selected microflow expects 'MOC.MOCProject'."

Neither appears in mxcli syntax workflow.user-task.targeting, whose own example
is TARGETING MICROFLOW HR.GetApprovers with no signature stated at all.

The expensive half is the planning consequence, and it is now written in both
skills: the task pages are a PREREQUISITE of the workflow row, not a later UI
row. A plan that builds the workflow at row 41 and its task pages at row 45
cannot make row 41 green.

WITH A KNOWN-BAD CONTROL, which is the only reason the pass results are worth
anything. With the signatures corrected, four constructs a real approval chain
needs all build clean on v0.21.0: user task with a targeting microflow,
multi-user task, CALL MICROFLOW with a quoted WITH, and a backward JUMP TO
inside an outcome. The same run also scripted a DECISION, which passed
mxcli check, printed "Created workflow", and left the project unopenable —
StorageLoadException, then BUILD FAILED. So BUG-76 is open on v0.21.0 and the
probe demonstrably tells a working construct from a broken one.

A probe with no known-bad control cannot distinguish "it works" from "the probe
is blind". This repo has already published one wrong conclusion for exactly that
reason: BUG-121, where a sequentially-built control only showed that a task
looks like a task.

Run on a throwaway copy of a real 181MB project, because BUG-76's recovery is
DROP WORKFLOW and this spelling leaves the model unopenable. Guards: routing in
sync, leak guard clean over 462 tracked files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
…uld not reach

Third instrument in this family with the same bug.

1. The page body was matched by a regex ending at a closing brace at COLUMN 0,
   which finds nothing for an indented page. Measured on a MOC/PSSR app
   replacement: two wizard pages wrapped in a LAYOUTGRID — which mxcli's own
   MPR010 advises for any DataView holding inputs — close at two spaces of
   indent, so the tool exited 2 with "no declaration of page X found in input"
   for pages plainly there. A project that follows mxcli's advice could not
   score any page.

   Now the same character-counting, paren-tracking extractor check-page-shell.sh
   got, for the same reason: a multi-line declaration's Params: { $X: Mod.Entity }
   opens and closes a brace before the body does.

2. `.anno` is not reached by the existing `annot` pattern, because \b after
   "anno" needs a non-word character and "t" is one. A project whose annotation
   block is div.anno scored its own annotation as missing page content, reporting
   "heading: Annotation" as the first miss on a page whose real H1 is correct.
   Two faithful pages at 9%.

WHAT I DELIBERATELY DID NOT ADD, having first added and then reverted it: a
fourth content-boundary name. The project's wireframe generator emitted wf-wrap
as the screen itself with the annotation inside it, and wf-head for the page
header — which this tool's own "every wf-* div is annotation chrome" rule then
ate, so the page header disappeared and the score went to null%. Teaching the
tool a name that contradicts its own convention was the wrong half to change.
The generator now emits the .wf-wrap > .wf-screen shape this file already
documents, with the annotation as a SIBLING, and renames its page structure to
page-head / page-actions.

Both pages then score 100%, and the honest caveat is that the denominator is
thin for a reason worth knowing: that project inlines its whole ds.css into
every wireframe so they open standalone, which makes every design-system class
look "wireframe-local" and moves the button labels into the not-scored
bound-data set. The action dimension reads 0/0 there — unusable rather than
wrong, and better as 0/0 than as a number nobody can trust.

check-scripts 72/72 shell and 17/17 Node parse; leak guard clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
@MendixMau

Copy link
Copy Markdown
Owner Author

Two more commits landed after the description above was written

The body describes commits 1–4; the PR now carries 6. Rather than rewrite it, here are the two additions. Same theme: an instrument or a table that was trusted and not checked.

Commit 5 — two workflow signatures mxcli check passes and the native build rejects

workflow-structure-rules.md §11 opens with "do not trust the date on this table — check the binary." The table was probed on v0.20.0, the project runs v0.21.0, and the project's own build plan already claimed it had been checked against v0.21.0 while nobody had run it. Running it found two facts that are in no skill, no bug log, and no mxcli syntax output:

  1. A user task's PAGE must take a System.WorkflowUserTask parameter, not the workflow's context entity. Pointing three tasks at the project detail page — the intuitive choice, since that entity is the workflow's context — passed mxcli check --references completely clean, execed, round-tripped through DESCRIBE, and then: "The selected page 'X' should accept a parameter of type 'WorkflowUserTask', but expects parameters of types 'MOCProject' instead."
  2. A targeting microflow must take TWO parameters, System.Workflow AND the context entity, in that order. One parameter is the natural thing to write. mxcli syntax workflow.user-task.targeting's own example is TARGETING MICROFLOW HR.GetApprovers, with no signature stated at all.

The expensive half is the planning consequence, and it is now written into both skills: the task pages are a prerequisite of the workflow row, not a later UI row. A plan that builds the workflow at row 41 and its task pages at row 45 cannot make row 41 green — which is exactly how that project's Phase 7 was ordered, and it is corrected there.

With a known-bad control, which is the only reason the pass results count. Once the signatures were fixed, four constructs a real approval chain needs all built clean on v0.21.0: user task + targeting microflow, multi-user task, CALL MICROFLOW with a quoted WITH, and a backward JUMP TO inside an outcome. The same run also scripted a DECISION, which passed mxcli check, printed Created workflow, and left the project unopenable (StorageLoadExceptionBUILD FAILED). So BUG-76 is open on v0.21.0 and the probe demonstrably tells a working construct from a broken one.

A probe with no known-bad control cannot distinguish "it works" from "the probe is blind" — and this repo has already published one wrong conclusion for exactly that reason (BUG-121, where a sequentially-built control only showed that a task looks like a task). Run on a throwaway copy, because BUG-76's recovery is DROP WORKFLOW.

Commit 6 — page-fidelity.js: the same body-extraction defect as check-page-shell.sh

Third instrument in that family with the same bug.

  1. The page body was matched by a regex ending at a closing brace at column 0, which finds nothing for an indented page. Measured: two wizard pages wrapped in a LAYOUTGRID — which mxcli's own MPR010 advises for any DataView holding inputs — close at two spaces of indent, so the tool exited 2 with "no declaration of page X found in input" for pages plainly there. A project that follows mxcli's advice could not score any page. Now the same character-counting, paren-tracking extractor.
  2. .anno is not reached by the existing annot pattern, because \b after "anno" needs a non-word character and "t" is one. A project whose annotation block is div.anno scored its own annotation as missing page content, reporting "heading: Annotation" as the first miss on a page whose real H1 is correct. Two faithful pages at 9%.

What I added and then reverted: a fourth content-boundary name. The project's wireframe generator emitted wf-wrap as the screen with the annotation inside it, and wf-head for the page header — which this tool's own "every wf-* div is annotation chrome" rule then ate, sending the score to null%. Teaching the tool a name that contradicts its own convention was the wrong half to change. The generator now emits the .wf-wrap > .wf-screen shape this file already documents, with the annotation as a sibling.

Both pages then score 100%, with a caveat worth keeping: the denominator is thin because that project inlines all of ds.css into every wireframe so they open standalone, which makes every design-system class look wireframe-local and moves the button labels into the not-scored bound-data set. The action dimension reads 0/0 there — unusable rather than wrong, and better as 0/0 than as a number nobody can trust.

Checks on the current head

check-scripts 72/72 shell + 17/17 Node · render-routing --check in sync (baseline 73,342 words) · check-portability clean over 113 files · leak guard clean over 462 tracked files · both new fixtures 41 assertions, 0 failures. CI green.


Generated by Claude Code

`.page-head` used once, subtree 0.513 of the page — the old rule
(`uses === 1 && kept < 0.4`) classed it a bound-data mock and deleted
the subtree, taking the page's only <h1> with it. Every dimension then
reported 0 of 0, which normalizes to `null%`: a clean report over an
empty corpus, on a page that scores 100% once it is not gutted. Same
silent-null failure the file's own header note describes, arriving a
second time through the other conjunct.

The threshold is removed rather than retuned. It was a guess, and any
value for it splits once-used wrappers on a measurement unrelated to
whether their content is bound data. A repeated region is still a mock
at any size.

New fixture, 14 assertions, two captured wireframe shapes, CAPTURE.md
stating that only literal text and class prefixes changed. The first
assertion is that the score is not null, not that it is high. Known-bad
control carrying the old rule: 8 of 14 fail.

Field run: a MOC/PSSR app replacement's grouped project overview,
2026-09-09 — null% before, 100% after, page script unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M
…k, both task-page params, fixtures take relative paths

- snapshot-mpr.sh: the .gitignore append can no longer abort the snapshot
  (it ran under set -e before the snapshot, and exec.sh calls it bare).
- ui-preflight-pages.md B0 names docs/design-reaches-app.md as the mark that
  discharges the design-reaches-app obligation — a consumer with no producer.
- workflow-structure-rules.md / learned-workflow-patterns.md: the task page
  takes $WorkflowUserTask IN ADDITION to $WorkflowContext (§4 rule 1, CE7412);
  the new wording read as task-only and contradicted the same file.
- The three new fixtures absolutise $1 so the documented relative form works.
- check-page-shell.sh / page-fidelity.js: header note that the brace counter
  does not skip quoted strings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
@MendixMau
MendixMau merged commit 43622af into master Sep 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants