From 3521058023ed6e332373d32a5198b6a34018e89a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 16:43:34 +0000 Subject: [PATCH 1/8] Obligations reach stage 3: a workflow's design count now owes a mark 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 Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M --- CHANGELOG.md | 2 ++ agents/architect-agent.md | 36 ++++++++++++++++++++++++++++++++++++ bin/lib/obligations.tsv | 22 ++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8eee3e..5937b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Kinds: `new` · `fix` · `learn` (a skill/learning) · `process` (rules, templat Credit the person or project that surfaced the change — the credit line is the thank-you. ## 2026-09-08 +- new(obligations): **`workflow-count` — the first obligation below stage 5, and that gap is the finding.** Every existing row (look, sweep, journeys, coherence, skeleton) is `from-stage 5`, so `obligations.tsv` — the only mechanism here for *"a judgement pass owes a mark"* — did not reach the **design** stages at all, and `workflow-structure-rules.md` §12 is a ten-row count with a denominator per line that fired only if someone happened to run it. Measured on a requirements-driven app replacement: the agent read §11 for a construct question, filled §12 row 10, never ran rows 1–9, and shipped a build plan specifying a **role XPath** for four user tasks whose assignees are data on the record — which §6 calls *"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. Found at Stage 4 by a human asking whether the workflow skills were being used, with the plan already written. New row is `project` scope, `denominator yes`, `from-stage 3`; `obligation-check.sh` ranks `from-stage` generically so **no code change was needed**. Deliberately not an `optin` row: an artifact a project must think of in order to owe is an artifact nobody owes. A workflow-free project discharges it in one line — *"0 workflows, nothing to count"* — the way §12's own rows 2/3/4/9 are written. Proven to go red both ways on a real project: PASS with the numbers, **PENDING** when the artifact is removed, **FAULT** on a mark carrying no denominator — Maurits Visser, from a no-code app replacement project +- fix(agents/architect-agent.md): **§12's ten rows are inlined, not cited.** The stub referenced `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 count, the `workflow-count.md` artifact it writes, and the two rows that actually bite: row 5 (targeting mechanism named **with the sentence it came from quoted** — and §6's warning that a role XPath over a nominated assignee delivers the task to everyone holding the 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 — Maurits Visser, from a no-code app replacement project - fix(bug-logs): re-probed the two workflow blockers on mxcli **v0.21.0** (released 2026-09-06, Mendix 11.13.0) — **both still open**, and BUG-76's diagnosis in this log was wrong. BUG-121 (`PARALLEL SPLIT`): the same script executed through v0.20.0 and v0.21.0 on two throwaway copies produces a **byte-identical** workflow unit (zero diff from `bson dump` after blanking the `$ID` GUIDs), so the runtime behaviour cannot have changed — and the path contents *are* present in the stored BSON on both versions, so the entry's own title ("writes the paths but not their contents") names the symptom, not the mechanism, which is still unidentified. BUG-76 (`DECISION`): not unconditional storage corruption but a **pincer between two spellings** — a bare outcome passes `mxcli check` and makes the model unloadable (`StorageLoadException`, "not a valid EnumerationValueIdentifier"), while a qualified one is rejected by mxcli's own `MDL-WF03` and loads clean via `exec --no-check` with a single `CE0117` for the empty expression; mxcli mandates the spelling the loader rejects and rejects the one it accepts, which implies two independent fixes rather than one. The correction was made on the source project on 2026-09-02 and never promoted here; it is already filed upstream as mendixlabs/mxcli#1031, so it goes into the draft as a comment to post rather than a rewritten body — a topbar-titled portal project · at merge: the three "disputed" `DECISION` notes on master (`mxcli-bugs.md` v0.20.0 clearance table, `learned-workflow-patterns.md` §1 pointer, §8 version table, §21 verdict table) now cite the resolved pincer diagnosis and the v0.21.0 re-probe instead of "shape-dependent, not isolated" — the toolkit merge queue ## 2026-09-07 diff --git a/agents/architect-agent.md b/agents/architect-agent.md index e883412..495bab9 100644 --- a/agents/architect-agent.md +++ b/agents/architect-agent.md @@ -71,10 +71,46 @@ You own architecture and build-plan decisions for {{PROJECT}}. Hard rule: you ne - Query the live model (`query-the-model.md`) before referencing any marketplace module in the build plan — `SHOW ENTITIES IN ` first, always. - **Every build-plan row you author carries a `claims` field** naming the BRD leaves it discharges, written in the same edit as the row — `brd-to-build-plan.md` Step 5b has the format and the incident. New rows only: a plan that predates the convention is an accepted state, so don't retrofit it, don't report it as incomplete, and don't block on it — the most you do is offer. +## If any process in this app has a workflow — run the count, do not merely cite it + +**A citation is not a read.** `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. It fires only if someone runs it, so the rows are +here rather than behind a link. Write the numbers into +**`architecture/workflow-count.md`** — the `workflow-count` obligation +(`bin/lib/obligations.tsv`, from-stage 3) expects a denominator on its first line, and a project +with no workflow discharges it with *"0 workflows, nothing to count"*. + +Run it at Stage 3 against the **drawn** diagram, and again at Stage 5 against the written MDL. + +| # | Check | Bound | +|---|---|---| +| 1 | Paths that end exactly once | N of N paths; 0 activities after a terminal | +| 2 | Boundary events whose type is named **and** whose terminator matches that type | N of N boundary events | +| 3 | Parallel splits with ≥2 paths and 0 *End workflow* / 0 *Jump to* at **any** depth in a branch | N of N splits | +| 4 | Enum-branching activities carrying every value **plus Empty** | N of N decisions + call-microflows-returning-enum + AI agent tasks | +| 5 | User tasks whose targeting mechanism is named, **with the sentence it came from quoted** | N of N user tasks — `ASSUMED: no targeting` is legal, blank is not | +| 6 | User tasks with an error handler for empty targeting, or an expression that provably cannot be empty | N of N user tasks | +| 7 | Multi-user tasks with decision method **and** completion timing stated, sourced to a business rule | N of N multi-user tasks | +| 8 | Expressions referencing only `$WorkflowContext` / `$WorkflowInstance` | N of N expressions | +| 9 | Event sub-processes with one start event, correct family, recurrence in bounds | N of N sub-processes | +| 10 | Constructs checked against §11 and marked *proven* or *hand-add in Studio Pro* | N of N constructs; every hand-add is its own numbered build-plan row | + +**Row 5 is the one that bites, so read §6 before you fill it.** If the assignee is *data on the +record* ("the reviewer named on the request"), a role XPath is not a near miss — it delivers the +task to **everyone** holding that role. Use a targeting microflow returning the nominee as a +one-element list; §6 prefers it over the *On created* handler, which cannot be written from MDL at +all. Row 6 then comes free if you write the resolver as a fallback chain. + +**Row 10 is a denominator, not a formality:** N constructs in the diagram, N rows in the plan. If +those two numbers differ, the plan is not finished — and a construct MDL cannot express is a +numbered `RUN` row, never a footnote and never omitted. + ## Workflow 1. Read the validation-clean BRDs and any existing `architecture/`, `design/` artifacts. 2. Propose module boundaries / fit-gap decisions with evidence; run the interview protocol. 3. Write `.mx-brd.json`, `architecture/` (blueprint, wiring diagrams, fit-gap.md), and once approved, `architecture/build-plan.md` — numbered, dependency-ordered, every new row with its `claims` block. +3b. **If any process has a workflow: `architecture/workflow-count.md`, all ten rows with numbers** — see the section above. Owed at Stage 3, before the build plan names a workflow row. 4. Record every decision in `PROJECT.md`. ## Report back diff --git a/bin/lib/obligations.tsv b/bin/lib/obligations.tsv index 9b4560d..c867531 100644 --- a/bin/lib/obligations.tsv +++ b/bin/lib/obligations.tsv @@ -49,3 +49,25 @@ sweep test module path .claude/loop/sweep//sweep.md yes yes 5 skills/wir journeys test module path .claude/loop/verify//summary.tsv no no 5 skills/journey-proof.md coherence architect cluster path .claude/loop/coherence/last-cluster-pass.tsv no yes 5 skills/process-coherence-pass.md skeleton mdl project path docs/skeleton-proof.md yes no 5 skills/walking-skeleton.md + +# workflow-count is the FIRST obligation below stage 5, and that gap is why it exists. Every row +# above is from-stage 5: look, sweep, journeys, coherence, skeleton. So this table — the only +# mechanism here for "a judgement pass owes a mark" — did not reach the DESIGN stages at all, +# and workflow-structure-rules.md §12 is a ten-row count with a denominator on every line that +# fired only if someone happened to run it. +# +# Measured, 2026-09-08, on a requirements-driven app replacement: the agent read §11 (what MDL +# can write) because it had a construct question, filled §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 that specified a role XPath for four user tasks whose assignees +# are data on the record. §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 by a human asking whether the workflow skills were being +# used, at Stage 4, with the plan already written. No gate in the pipeline compares a targeting +# mechanism against the sentence it came from; §12 row 5 is that check. +# +# A project with no workflow discharges this in one line — "0 workflows, nothing to count" is a +# legal mark and a real statement, and §12's own rows 2/3/4/9 are written the same way ("0 of 0"). +# That is deliberately NOT an optin row: an artifact a project must think of in order to owe is +# an artifact nobody owes, which is the disease this whole table was built for. +workflow-count architect project path architecture/workflow-count.md yes yes 3 skills/workflow-structure-rules.md From caf7b4e2850065fe6fdd31a51fd13a1d965efeae Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 02:45:24 +0000 Subject: [PATCH 2/8] Ask whether the design system reached the app, and owe an answer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M --- CHANGELOG.md | 7 + README.md | 1 + ROUTING.md | 1 + agents/architect-agent.md | 1 + agents/gate-agent.md | 1 + agents/mdl-agent.md | 1 + agents/review-agent.md | 1 + bin/lib/install-manifest.sh | 7 +- bin/lib/obligations.tsv | 29 ++ bin/lib/skill-routing.tsv | 1 + project-bin/check-design-portability.sh | 20 +- project-bin/check-design-reaches-app.sh | 314 ++++++++++++++++++ skills/conversion-runbook.md | 1 + skills/ui-preflight-pages.md | 18 + .../fixtures/design-reaches-app/CAPTURE.md | 40 +++ .../built-broken-classes.css | 54 +++ .../design-reaches-app/built-broken.css | 47 +++ .../design-reaches-app/built-fixed.css | 76 +++++ .../design-reaches-app/built-scoped-only.css | 71 ++++ .../wave2/fixtures/design-reaches-app/ds.css | 35 ++ tests/wave2/test-design-reaches-app.sh | 144 ++++++++ 21 files changed, 866 insertions(+), 4 deletions(-) create mode 100755 project-bin/check-design-reaches-app.sh create mode 100644 tests/wave2/fixtures/design-reaches-app/CAPTURE.md create mode 100644 tests/wave2/fixtures/design-reaches-app/built-broken-classes.css create mode 100644 tests/wave2/fixtures/design-reaches-app/built-broken.css create mode 100644 tests/wave2/fixtures/design-reaches-app/built-fixed.css create mode 100644 tests/wave2/fixtures/design-reaches-app/built-scoped-only.css create mode 100644 tests/wave2/fixtures/design-reaches-app/ds.css create mode 100755 tests/wave2/test-design-reaches-app.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 5937b15..13945fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ moment updating it became a separate chore). One line per change: Kinds: `new` · `fix` · `learn` (a skill/learning) · `process` (rules, templates, CI). Credit the person or project that surfaced the change — the credit line is the thank-you. +## 2026-09-09 +- new(project-bin): **`check-design-reaches-app.sh` — did any of the design system ARRIVE in the built app?** The gap it closes is a sentence in its own sibling: `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"*, which is false and 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. Measured on a MOC/PSSR app replacement, 2026-09-09: 55 tokens sampled from the customer's own screenshots, 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**. Three independent failures, none visible to anything in this toolkit: the bridge assigned **SCSS variables** (`$brand-primary: var(--brand)`) to a theme running `$use-css-variables: true`, so it compiled to nothing (`--brand` present, `--brand-primary` untouched, `var(--brand)` **0 occurrences** in the built sheet); the **20 component classes were never ported at all**, so `ds-muted` on 12 live widgets resolved to the body colour; and 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 — every one of them reads the model or the authored stylesheet, and the defect lives between the authored stylesheet and the built one. **Found by a human opening a screenshot and asking why the UI was poor.** The check deliberately does NOT look for a known default hex (a cache of one framework version — authoring rule 5); it inverts the question to *"is the winning definition of each knob the framework declares pointed at a design-system token"*, which needs no defaults table and survives the next Atlas release. Four passes with denominators (knobs bound · tokens arrived · classes arrived · dead SCSS bridge) plus a WARN for classes the **model** asks for that the built sheet never defines — which found 17 of 23 on the same project, `ds-help` among them, class names typed from memory onto live widgets. Requires a build and exits **2** without one, because "did it reach the app" cannot be answered from source, and answering it from source is how this shipped — Maurits Visser, from a MOC/PSSR app replacement +- new(obligations): **`design-reaches-app` — the second obligation below stage 5**, `mdl`/project/`from-stage 3`, artifact `docs/design-reaches-app.md` with a denominator, degradable. Owed by `mdl` rather than `review` deliberately: the agent that performed the port is the one that must show it landed. The reason the defect above escaped is not that the check was hard — it is that **nobody was owed an answer**, so every gate it passed through was green-by-absence. A project with no build yet discharges it by saying so (*"binding UNPROVEN, no build"*); silence does not — Maurits Visser, from a MOC/PSSR app replacement +- fix(project-bin/check-design-portability.sh): the falsified premise is **corrected in the header 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 — Maurits Visser, from a MOC/PSSR app replacement +- learn(ui-preflight-pages): **hard rule B0 — before the FIRST page of the project, prove the design system reached the app.** `0 of N` on any of the three denominators means every class you are about to write resolves to nothing, and every later page inherits it. Placed at the consumer, in Step 2, because the ordering is the finding: the port happened at the stage that owed it and the pages were built on top of it unproven — Maurits Visser, from a MOC/PSSR app replacement +- process(install-manifest, skill-routing): `check-design-reaches-app.sh` installed into projects and routed **baseline** for architect/mdl/gate/review at stages 3 and 5, trigger-phrased on *"after the first build that follows any design-system port, and before any page is built on it"* — Maurits Visser, from a MOC/PSSR app replacement + ## 2026-09-08 - new(obligations): **`workflow-count` — the first obligation below stage 5, and that gap is the finding.** Every existing row (look, sweep, journeys, coherence, skeleton) is `from-stage 5`, so `obligations.tsv` — the only mechanism here for *"a judgement pass owes a mark"* — did not reach the **design** stages at all, and `workflow-structure-rules.md` §12 is a ten-row count with a denominator per line that fired only if someone happened to run it. Measured on a requirements-driven app replacement: the agent read §11 for a construct question, filled §12 row 10, never ran rows 1–9, and shipped a build plan specifying a **role XPath** for four user tasks whose assignees are data on the record — which §6 calls *"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. Found at Stage 4 by a human asking whether the workflow skills were being used, with the plan already written. New row is `project` scope, `denominator yes`, `from-stage 3`; `obligation-check.sh` ranks `from-stage` generically so **no code change was needed**. Deliberately not an `optin` row: an artifact a project must think of in order to owe is an artifact nobody owes. A workflow-free project discharges it in one line — *"0 workflows, nothing to count"* — the way §12's own rows 2/3/4/9 are written. Proven to go red both ways on a real project: PASS with the numbers, **PENDING** when the artifact is removed, **FAULT** on a mark carrying no denominator — Maurits Visser, from a no-code app replacement project - fix(agents/architect-agent.md): **§12's ten rows are inlined, not cited.** The stub referenced `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 count, the `workflow-count.md` artifact it writes, and the two rows that actually bite: row 5 (targeting mechanism named **with the sentence it came from quoted** — and §6's warning that a role XPath over a nominated assignee delivers the task to everyone holding the 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 — Maurits Visser, from a no-code app replacement project diff --git a/README.md b/README.md index c1dc022..81b0d74 100644 --- a/README.md +++ b/README.md @@ -743,6 +743,7 @@ The "When to use which skill" table above is *situational* — load a skill when | Writing or reviewing any page or snippet — the spacing scale (8/16/24/32/48), section rhythm, and the page-header scaffold every full page starts with; sections at 0px apart and pages with no H1 are the defects it retires | `skills/design-spacing.md` | | After every page-building script, and any time the UI looks wrong — the cheap repeatable look during the build: one page, one screenshot, three questions. Feeds Gate: UI, never replaces it | `skills/ui-loop.md` | | Building or using the in-app design gallery | `skills/learned-stylegallery.md` | +| After the FIRST build that follows any design-system port, and before any page is built on it — reads the BUILT stylesheet and reports how many framework knobs point at a design token, how many tokens arrived, how many component classes arrived, each with its denominator. Measured on a real run: 55 tokens ported correctly into the right file, 0 of 35 knobs bound and 0 of 20 classes present, two build phases shipped in the framework's default blue with mx check, mxcli lint, the MDL suite and two e2e journeys all green | `project-bin/check-design-reaches-app.sh` | | Before exec'ing ANY page script — compares the drafted MDL's shell against the wireframe's: page column, layout/nav shell, one H1. Measured 0/10 pages on a real first build, repaired wholesale 47 scripts later | `project-bin/check-page-shell.sh` | | After drafting and again after exec'ing any page script — scores the page MDL (or `mxcli describe` output on stdin) against its wireframe: headings/actions/content/classes, weighted. The scored companion to check-page-shell's binary gate; 32% median measured without it, 90% first-draft with it. Every run is appended to the project's docs/PAGE-FIDELITY.tsv — first non-stub row per page = first-build score of record vs the ≥80% target (forward-reference stubs score with --stub, exempt) | `project-bin/page-fidelity.js` | | Choosing CLI vs MCP+MDL vs hand-rolled MCP, or any MCP write session — three co-equal write modes, not CLI-only | `skills/learned-mcp-patterns.md` | diff --git a/ROUTING.md b/ROUTING.md index dd48c64..59bc0e1 100644 --- a/ROUTING.md +++ b/ROUTING.md @@ -110,6 +110,7 @@ picks the row up. That is the whole procedure — there is no second list to rem | After every page-building script, and any time the UI looks wrong — the cheap repeatable look during the build: one page, one screenshot, three questions. Feeds Gate: UI, never replaces it | `skills/ui-loop.md` | mdl,review | 5 | baseline | | Building or using the in-app design gallery | `skills/learned-stylegallery.md` | mdl | 5 | baseline | | Before porting ds.css into SCSS, and at the Stage-3 gate — greps the stylesheet for rules that cannot match the HTML Mendix emits (rem against the real root, table/th/td selectors, positional row selectors). mx check, mxcli check and mxcli lint are all blind to CSS | `project-bin/check-design-portability.sh` | architect,mdl,gate | 3,5 | ondemand | +| After the FIRST build that follows any design-system port, and before any page is built on it — reads the BUILT stylesheet and reports how many framework knobs point at a design token, how many tokens arrived, how many component classes arrived, each with its denominator. Measured on a real run: 55 tokens ported correctly into the right file, 0 of 35 knobs bound and 0 of 20 classes present, two build phases shipped in the framework's default blue with mx check, mxcli lint, the MDL suite and two e2e journeys all green | `project-bin/check-design-reaches-app.sh` | architect,mdl,gate,review | 3,5 | baseline | | Before exec'ing ANY page script — compares the drafted MDL's shell against the wireframe's: page column, layout/nav shell, one H1. Measured 0/10 pages on a real first build, repaired wholesale 47 scripts later | `project-bin/check-page-shell.sh` | mdl,gate,review | 5 | baseline | | After drafting and again after exec'ing any page script — scores the page MDL (or `mxcli describe` output on stdin) against its wireframe: headings/actions/content/classes, weighted. The scored companion to check-page-shell's binary gate; 32% median measured without it, 90% first-draft with it. Every run is appended to the project's docs/PAGE-FIDELITY.tsv — first non-stub row per page = first-build score of record vs the ≥80% target (forward-reference stubs score with --stub, exempt) | `project-bin/page-fidelity.js` | mdl,gate,review | 5 | baseline | | Designing the brand and ONE ANNOTATED WIREFRAME PER SCREEN before building pages — the design system alone is half the deliverable | `skills/design-artifacts.md` | architect | 3 | ondemand | diff --git a/agents/architect-agent.md b/agents/architect-agent.md index 495bab9..f11c501 100644 --- a/agents/architect-agent.md +++ b/agents/architect-agent.md @@ -37,6 +37,7 @@ You own architecture and build-plan decisions for {{PROJECT}}. Hard rule: you ne | `skills/checkpoints/checkpoint-template.md` | Any stage transition — the 2+1 format every CAC uses, and the one-register rule (answers land in PROJECT.md, never in a separate state file). The seven CACs themselves are routed per stage in the situational table | | `skills/agent-roles.md` | Setting up or completing a project's dev-process subagents — once, at project start, not "on demand" | | `skills/module-folder-convention.md` | Placing any document in a module — before the first `create`. Feature group, then Pages/Microflows/Services/Resources; the path comes from the brief's folder plan, and the table says which types mxcli can actually place | +| `project-bin/check-design-reaches-app.sh` | After the FIRST build that follows any design-system port, and before any page is built on it — reads the BUILT stylesheet and reports how many framework knobs point at a design token, how many tokens arrived, how many component classes arrived, each with its denominator. Measured on a real run: 55 tokens ported correctly into the right file, 0 of 35 knobs bound and 0 of 20 classes present, two build phases shipped in the framework's default blue with mx check, mxcli lint, the MDL suite and two e2e journeys all green | | `skills/tool-output-is-not-ground-truth.md` | Any time an exit code, a tool's output or a subagent's report is about to become a stated finding — verify before you conclude | | `bin/status.sh` | The first command of every session, and any time someone asks "where are we" or "what next" — one screen: stage, done/overdue, the ONE next action, from the instruments, never from memory | | `skills/retesting-learned-rules.md` | Before obeying any learned-* STOP or workaround that costs a detour — probe the binary you actually have, then stamp the verdict back into the rule | diff --git a/agents/gate-agent.md b/agents/gate-agent.md index 893a523..4488812 100644 --- a/agents/gate-agent.md +++ b/agents/gate-agent.md @@ -32,6 +32,7 @@ own bug log before running anything you have not run here before. | `skills/degrade-to-judgement.md` | Any pass whose input is missing, stale or unresolvable — before recording UNMEASURED, N/A or a silent skip: name what was missing, say what you assessed against instead, still deliver a verdict | | `skills/checkpoints/checkpoint-template.md` | Any stage transition — the 2+1 format every CAC uses, and the one-register rule (answers land in PROJECT.md, never in a separate state file). The seven CACs themselves are routed per stage in the situational table | | `skills/agent-roles.md` | Setting up or completing a project's dev-process subagents — once, at project start, not "on demand" | +| `project-bin/check-design-reaches-app.sh` | After the FIRST build that follows any design-system port, and before any page is built on it — reads the BUILT stylesheet and reports how many framework knobs point at a design token, how many tokens arrived, how many component classes arrived, each with its denominator. Measured on a real run: 55 tokens ported correctly into the right file, 0 of 35 knobs bound and 0 of 20 classes present, two build phases shipped in the framework's default blue with mx check, mxcli lint, the MDL suite and two e2e journeys all green | | `project-bin/check-page-shell.sh` | Before exec'ing ANY page script — compares the drafted MDL's shell against the wireframe's: page column, layout/nav shell, one H1. Measured 0/10 pages on a real first build, repaired wholesale 47 scripts later | | `project-bin/page-fidelity.js` | After drafting and again after exec'ing any page script — scores the page MDL (or `mxcli describe` output on stdin) against its wireframe: headings/actions/content/classes, weighted. The scored companion to check-page-shell's binary gate; 32% median measured without it, 90% first-draft with it. Every run is appended to the project's docs/PAGE-FIDELITY.tsv — first non-stub row per page = first-build score of record vs the ≥80% target (forward-reference stubs score with --stub, exempt) | | `skills/testing-shape.md` | Before calling any module tested — what testing a module means, and the false-green register of confirmed ways a test reports green over a broken feature | diff --git a/agents/mdl-agent.md b/agents/mdl-agent.md index 9cba5fa..22a202a 100644 --- a/agents/mdl-agent.md +++ b/agents/mdl-agent.md @@ -46,6 +46,7 @@ a rule below names an asset (e.g. "the wireframe", "the brief"), it means the pa | `skills/design-spacing.md` | Writing or reviewing any page or snippet — the spacing scale (8/16/24/32/48), section rhythm, and the page-header scaffold every full page starts with; sections at 0px apart and pages with no H1 are the defects it retires | | `skills/ui-loop.md` | After every page-building script, and any time the UI looks wrong — the cheap repeatable look during the build: one page, one screenshot, three questions. Feeds Gate: UI, never replaces it | | `skills/learned-stylegallery.md` | Building or using the in-app design gallery | +| `project-bin/check-design-reaches-app.sh` | After the FIRST build that follows any design-system port, and before any page is built on it — reads the BUILT stylesheet and reports how many framework knobs point at a design token, how many tokens arrived, how many component classes arrived, each with its denominator. Measured on a real run: 55 tokens ported correctly into the right file, 0 of 35 knobs bound and 0 of 20 classes present, two build phases shipped in the framework's default blue with mx check, mxcli lint, the MDL suite and two e2e journeys all green | | `project-bin/check-page-shell.sh` | Before exec'ing ANY page script — compares the drafted MDL's shell against the wireframe's: page column, layout/nav shell, one H1. Measured 0/10 pages on a real first build, repaired wholesale 47 scripts later | | `project-bin/page-fidelity.js` | After drafting and again after exec'ing any page script — scores the page MDL (or `mxcli describe` output on stdin) against its wireframe: headings/actions/content/classes, weighted. The scored companion to check-page-shell's binary gate; 32% median measured without it, 90% first-draft with it. Every run is appended to the project's docs/PAGE-FIDELITY.tsv — first non-stub row per page = first-build score of record vs the ≥80% target (forward-reference stubs score with --stub, exempt) | | `skills/learned-mcp-patterns.md` | Choosing CLI vs MCP+MDL vs hand-rolled MCP, or any MCP write session — three co-equal write modes, not CLI-only | diff --git a/agents/review-agent.md b/agents/review-agent.md index 740a3b5..4e0ae40 100644 --- a/agents/review-agent.md +++ b/agents/review-agent.md @@ -40,6 +40,7 @@ protects: you have no Write or Edit tool, and you never run `mxcli exec`. | `skills/agent-roles.md` | Setting up or completing a project's dev-process subagents — once, at project start, not "on demand" | | `skills/design-spacing.md` | Writing or reviewing any page or snippet — the spacing scale (8/16/24/32/48), section rhythm, and the page-header scaffold every full page starts with; sections at 0px apart and pages with no H1 are the defects it retires | | `skills/ui-loop.md` | After every page-building script, and any time the UI looks wrong — the cheap repeatable look during the build: one page, one screenshot, three questions. Feeds Gate: UI, never replaces it | +| `project-bin/check-design-reaches-app.sh` | After the FIRST build that follows any design-system port, and before any page is built on it — reads the BUILT stylesheet and reports how many framework knobs point at a design token, how many tokens arrived, how many component classes arrived, each with its denominator. Measured on a real run: 55 tokens ported correctly into the right file, 0 of 35 knobs bound and 0 of 20 classes present, two build phases shipped in the framework's default blue with mx check, mxcli lint, the MDL suite and two e2e journeys all green | | `project-bin/check-page-shell.sh` | Before exec'ing ANY page script — compares the drafted MDL's shell against the wireframe's: page column, layout/nav shell, one H1. Measured 0/10 pages on a real first build, repaired wholesale 47 scripts later | | `project-bin/page-fidelity.js` | After drafting and again after exec'ing any page script — scores the page MDL (or `mxcli describe` output on stdin) against its wireframe: headings/actions/content/classes, weighted. The scored companion to check-page-shell's binary gate; 32% median measured without it, 90% first-draft with it. Every run is appended to the project's docs/PAGE-FIDELITY.tsv — first non-stub row per page = first-build score of record vs the ≥80% target (forward-reference stubs score with --stub, exempt) | | `skills/module-review.md` | Reviewing any module before calling it done — the ONE pass: build, gate, prove, LOOK (is it logical, does it look right, does it match our design, over every page not just the tested ones), confirm with the denominator stated | diff --git a/bin/lib/install-manifest.sh b/bin/lib/install-manifest.sh index 079cf2b..bd1ee62 100644 --- a/bin/lib/install-manifest.sh +++ b/bin/lib/install-manifest.sh @@ -78,6 +78,11 @@ MXTK_AGENTS_STAGE_BUILD="mdl-agent.md gate-agent.md test-agent.md review-agent.m # .claude/loop/page-scope.json, and the producer existed nowhere, so the LOOK rung ran # --static-only (reduced evidence) in every project by construction. # +# check-design-reaches-app.sh joined on 2026-09-09, from the field run where the same design +# system was ported correctly and reached nothing: 0 of 35 framework knobs bound, 0 of 20 +# component classes in the built stylesheet, two build phases shipped in the default blue. +# It needs a BUILD to answer, which is exactly why nothing answered it before. +# # check-design-portability.sh joined on 2026-08-26, from a field run that found five real design # -system defects none of which any existing instrument could see: mx check validates the model and # CSS is not in the model; a class name is an opaque string to `mxcli check --references`; and @@ -90,7 +95,7 @@ MXTK_AGENTS_STAGE_BUILD="mdl-agent.md gate-agent.md test-agent.md review-agent.m # point is the across-runs trend — the stakeholder-visibility story that "we are finding bugs # and improving" — and every other stage artifact gets an HTML surface while the register had # none; the trend view lived only as a grep in the skill. -MXTK_PROJECT_BIN="_common.sh snapshot-mpr.sh restore-mpr.sh exec.sh save-sp.sh restart-sp.sh check-sp-health.sh verify-module.sh test-stack-up.sh fixture-manifest.sh check-root-clean.sh lint-gate.sh close-task.sh conformance-check.sh coverage-preflight.sh graph-sweep.sh review-module.sh coherence-cadence.sh build-plan-status.sh done-drift-check.sh page-scope.sh render-improvement-register.sh check-design-portability.sh check-page-shell.sh page-fidelity.js" +MXTK_PROJECT_BIN="_common.sh snapshot-mpr.sh restore-mpr.sh exec.sh save-sp.sh restart-sp.sh check-sp-health.sh verify-module.sh test-stack-up.sh fixture-manifest.sh check-root-clean.sh lint-gate.sh close-task.sh conformance-check.sh coverage-preflight.sh graph-sweep.sh review-module.sh coherence-cadence.sh build-plan-status.sh done-drift-check.sh page-scope.sh render-improvement-register.sh check-design-portability.sh check-design-reaches-app.sh check-page-shell.sh page-fidelity.js" # Files in project-bin/ that are deliberately NOT installed into projects. The reverse check # below flags anything named by NEITHER list, so a new file in project-bin/ has to be either diff --git a/bin/lib/obligations.tsv b/bin/lib/obligations.tsv index c867531..f31d773 100644 --- a/bin/lib/obligations.tsv +++ b/bin/lib/obligations.tsv @@ -71,3 +71,32 @@ skeleton mdl project path docs/skeleton-proof.md yes no 5 skills/walking-skeleto # That is deliberately NOT an optin row: an artifact a project must think of in order to owe is # an artifact nobody owes, which is the disease this whole table was built for. workflow-count architect project path architecture/workflow-count.md yes yes 3 skills/workflow-structure-rules.md + +# design-reaches-app is the SECOND obligation below stage 5, and it is here because the design +# stages owe a mark for something no other pass can see: whether the design system arrived in +# the built app at all. +# +# Measured 2026-09-09, on the same requirements-driven 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 the +# framework's default blue for two more phases of build, because: +# - the bridge assigned SCSS variables while the theme ran $use-css-variables: true, so it +# compiled to nothing at all (--brand present, --brand-primary untouched, 0 references); +# - the 20 component classes were never ported, so `ds-muted` on 12 live widgets resolved to +# the body colour; +# - the theme module's :root is emitted BEFORE the framework's customization file, 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 opens by asserting the premise +# that failed: "tokens port into Mendix perfectly". Found by a human opening a screenshot. +# +# WHY A MARK AND NOT ONLY A SCRIPT: the script existed in one form already, and the reason this +# escaped is not that the check was hard, it is that nobody was owed an answer. Every gate this +# ran through was green-by-absence. The artifact is a verdict with a denominator — "N of M +# framework knobs bound, N of M classes arrived" — and a project with no build yet discharges +# it by SAYING SO ("no build yet, binding UNPROVEN, proven at the first build"), which is why +# this row is degradable. Silence is not. +# +# It is owed by mdl rather than review deliberately: the agent that performed the port is the +# one that must show it landed. A producer for every consumer, in the same hands. +design-reaches-app mdl project path docs/design-reaches-app.md yes yes 3 project-bin/check-design-reaches-app.sh diff --git a/bin/lib/skill-routing.tsv b/bin/lib/skill-routing.tsv index 2690c84..fc70342 100644 --- a/bin/lib/skill-routing.tsv +++ b/bin/lib/skill-routing.tsv @@ -91,6 +91,7 @@ design-spacing skills/design-spacing.md Writing or reviewing any page or snippet ui-loop skills/ui-loop.md After every page-building script, and any time the UI looks wrong — the cheap repeatable look during the build: one page, one screenshot, three questions. Feeds Gate: UI, never replaces it mdl,review 5 baseline design learned-stylegallery skills/learned-stylegallery.md Building or using the in-app design gallery mdl 5 baseline design check-design-portability project-bin/check-design-portability.sh Before porting ds.css into SCSS, and at the Stage-3 gate — greps the stylesheet for rules that cannot match the HTML Mendix emits (rem against the real root, table/th/td selectors, positional row selectors). mx check, mxcli check and mxcli lint are all blind to CSS architect,mdl,gate 3,5 ondemand design +check-design-reaches-app project-bin/check-design-reaches-app.sh After the FIRST build that follows any design-system port, and before any page is built on it — reads the BUILT stylesheet and reports how many framework knobs point at a design token, how many tokens arrived, how many component classes arrived, each with its denominator. Measured on a real run: 55 tokens ported correctly into the right file, 0 of 35 knobs bound and 0 of 20 classes present, two build phases shipped in the framework's default blue with mx check, mxcli lint, the MDL suite and two e2e journeys all green architect,mdl,gate,review 3,5 baseline design check-page-shell project-bin/check-page-shell.sh Before exec'ing ANY page script — compares the drafted MDL's shell against the wireframe's: page column, layout/nav shell, one H1. Measured 0/10 pages on a real first build, repaired wholesale 47 scripts later mdl,gate,review 5 baseline design page-fidelity project-bin/page-fidelity.js After drafting and again after exec'ing any page script — scores the page MDL (or `mxcli describe` output on stdin) against its wireframe: headings/actions/content/classes, weighted. The scored companion to check-page-shell's binary gate; 32% median measured without it, 90% first-draft with it. Every run is appended to the project's docs/PAGE-FIDELITY.tsv — first non-stub row per page = first-build score of record vs the ≥80% target (forward-reference stubs score with --stub, exempt) mdl,gate,review 5 baseline design learned-mcp-patterns skills/learned-mcp-patterns.md Choosing CLI vs MCP+MDL vs hand-rolled MCP, or any MCP write session — three co-equal write modes, not CLI-only mdl 5 baseline build/mdl diff --git a/project-bin/check-design-portability.sh b/project-bin/check-design-portability.sh index 912022b..1bbacb7 100755 --- a/project-bin/check-design-portability.sh +++ b/project-bin/check-design-portability.sh @@ -2,9 +2,23 @@ # check-design-portability.sh — fail the design-system gate when a stylesheet rule cannot # match the HTML Mendix actually emits. # -# WHY THIS EXISTS. A design system's *tokens* port into Mendix perfectly — colours, radii and -# spacing variables survive the SCSS port intact. Its *rules* may not, because they were -# authored against a DOM the app does not have. Measured on a field run (2026-08-26): 10 of a +# WHY THIS EXISTS. A design system's *rules* may not survive the SCSS port, because they were +# authored against a DOM the app does not have. +# +# CORRECTION, 2026-09-09. This header used to open: "A design system's *tokens* port into +# Mendix perfectly — colours, radii and spacing variables survive the SCSS port intact." That +# is FALSE, it was the sentence that made everyone comfortable, and it cost two build phases on +# a real project. Tokens survive the port into a FILE. Nothing about landing in the file makes +# the framework READ them: on that run 55 correctly ported tokens sat in the right file while +# every screen rendered in Atlas's default blue, because the bridge assigned SCSS variables to +# a theme running $use-css-variables: true and compiled to nothing. Separately, the 20 +# component classes were never ported at all — 0 of them existed in the built stylesheet. +# This script could not have seen either: it lints the AUTHORED stylesheet, and both defects +# live between the authored stylesheet and the built one. +# +# That gap is now project-bin/check-design-reaches-app.sh, which reads the BUILT stylesheet and +# reports how many framework knobs point at a design token, with a denominator. Run BOTH: this +# one asks whether the rules can match, that one asks whether any of it arrived. Measured on a field run (2026-08-26): 10 of a # 70-line ds.css styled through child HTML tags Mendix never emits, and every rem in the file # rendered at 62.5% of intent. Nothing warned. `mx check` returned 0 errors, `mxcli check` and # `mxcli lint` were green, the app built, and the pages rendered — small, and grey. diff --git a/project-bin/check-design-reaches-app.sh b/project-bin/check-design-reaches-app.sh new file mode 100755 index 0000000..a466b80 --- /dev/null +++ b/project-bin/check-design-reaches-app.sh @@ -0,0 +1,314 @@ +#!/usr/bin/env bash +# check-design-reaches-app.sh — did any of the design system ARRIVE in the built app? +# +# Sibling of check-design-portability.sh, and its correction. That script asks whether the +# design system's RULES can match the DOM Mendix emits, and its header opens by asserting the +# premise this one exists to test: +# +# "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 surviving into the file makes the framework read +# them, and nothing about the file makes the app wear them. +# +# ── THE FIELD RUN (2026-09-09, a requirements-driven MOC/PSSR app replacement) ────────────── +# +# 55 tokens sampled from the customer's own screenshots were ported into +# themesource//web/main.scss correctly, on time, at the stage that owed them. The app then +# rendered every screen in the framework's default blue for two more phases of build. +# +# Three independent failures, none of them visible to any check in this toolkit: +# +# 1. THE BRIDGE TARGETED A DEAD LAYER. The port wrote SCSS variables: +# +# $brand-primary: var(--brand); +# +# Atlas 3 with `$use-css-variables: true` — the default in Mendix 10/11 — reads CSS custom +# properties and never reads that SCSS variable. The assignment compiled to NOTHING. +# Measured in deployment/web/theme.compiled.css: +# +# --brand: #FC3122 <- ours, present, correct +# --brand-primary: #264ae5 <- the framework default, untouched +# var(--brand) 0 occurrences +# +# 2. THE COMPONENT CLASSES WERE NEVER PORTED AT ALL. 20 classes defined in the design system, +# 0 present in the built stylesheet. `ds-muted` sat on 12 widgets and resolved to the body +# colour, so every caption on every page rendered at full ink. +# +# 3. THE PORT ALSO WON NOTHING IT DID SET, because a project theme module's :root is emitted +# BEFORE theme/web/custom-variables.scss in the compiled sheet. Anything the module said +# about a framework knob lost the cascade to the stock value. +# +# `mx check` returned 0 errors. `mxcli lint` returned 0 errors. The MDL suite was green, both +# e2e journeys passed, and check-design-portability.sh had nothing to say, because every one of +# those looks at the model or at the authored stylesheet and none of them looks at the BUILT one. +# It was found by a human opening a screenshot and asking why the UI was poor. +# +# ── WHAT THIS CHECKS, AND WHY IT NEEDS NO TABLE OF FRAMEWORK DEFAULTS ─────────────────────── +# +# 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 it needs no such table: +# +# For every knob the framework declares in its own customization file, is the winning +# definition in the BUILT stylesheet pointed at a design-system token? +# +# A framework's customization file enumerates its own knobs; that is what it is for. Count how +# many are bound. Zero bound is not a style opinion, it is a wire that was never connected. +# +# Four passes, each with a denominator, because a pass that cannot say what it covered has not +# covered it: +# +# 1 knobs bound N of M framework knobs point at a design-system token. 0 = VIOLATION +# 2 tokens arrived N of M design-system tokens exist in the built sheet. 0 = VIOLATION +# 3 classes arrived N of M design-system classes exist in the built sheet. 0 = VIOLATION +# 4 dead SCSS bridge `$x: var(--y)` while the theme runs on custom properties. any = VIOL +# +# Plus one WARN pass: classes the MODEL asks for that the built sheet does not define. That is +# how `ds-help` was found on the same run — a class name typed from memory, on a live widget, +# styling nothing. +# +# project-bin/check-design-reaches-app.sh # defaults, from the project root +# project-bin/check-design-reaches-app.sh +# +# Exit 0 clean, 1 violations, 2 inspected nothing (NOT a pass). +# +# THIS CHECK REQUIRES A BUILD. That is the point: there is no way to answer "did it reach the +# app" from source alone, which is exactly why nothing answered it. With no built stylesheet it +# exits 2 and says so, and 2 is not green. + +set -uo pipefail + +DESIGN="${1:-}" +BUILT="${2:-}" + +if [ -z "$DESIGN" ]; then + for c in design/ds.css design/design-system.css; do + [ -f "$c" ] && { DESIGN="$c"; break; } + done +fi +if [ -z "$BUILT" ]; then + for c in deployment/web/theme.compiled.css deployment/web/theme.css; do + [ -f "$c" ] && { BUILT="$c"; break; } + done +fi + +# The framework's own customization surface. Atlas has called this file the same thing since +# Atlas 2; a project that renamed it passes it as a third argument via CUSTOM_VARS. +CUSTOM_VARS="${CUSTOM_VARS:-theme/web/custom-variables.scss}" + +if [ -z "$DESIGN" ] || [ ! -f "$DESIGN" ]; then + printf 'check-design-reaches-app: no design system stylesheet found.\n' >&2 + printf ' Searched: design/ds.css, design/design-system.css\n' >&2 + printf ' This is NOT a pass. Name it explicitly, or run from the project root.\n' >&2 + exit 2 +fi +if [ -z "$BUILT" ] || [ ! -f "$BUILT" ]; then + printf 'check-design-reaches-app: no BUILT stylesheet found.\n' >&2 + printf ' Searched: deployment/web/theme.compiled.css, deployment/web/theme.css\n' >&2 + printf ' Run a build first. "Did the design system reach the app" cannot be answered from\n' >&2 + printf ' source, and answering it from source is how this defect shipped.\n' >&2 + printf ' This is NOT a pass.\n' >&2 + exit 2 +fi + +VIOLATIONS=0 +WARNINGS=0 + +report() { printf 'FAIL %s\n %s\n' "$1" "$2"; VIOLATIONS=$((VIOLATIONS + 1)); } +warn() { printf 'WARN %s\n %s\n' "$1" "$2"; WARNINGS=$((WARNINGS + 1)); } + +# ── Pass 0: read the design system ────────────────────────────────────────────────────────── +# Tokens: custom properties declared in a :root block. Values too, because a knob may be bound +# by repeating the literal rather than by var() — still bound, and still correct. +TOKENS="$(awk ' + /:root/ { inroot = 1 } + inroot && /^[[:space:]]*}/ { inroot = 0 } + inroot && match($0, /--[a-z0-9-]+[[:space:]]*:/) { + name = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", name) + rest = substr($0, RSTART + RLENGTH); sub(/;.*$/, "", rest) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", rest) + if (!(name in seen)) { seen[name] = 1; print name "\t" rest } + } +' "$DESIGN")" +TOKEN_COUNT="$(printf '%s\n' "$TOKENS" | grep -c '^--' || true)" + +# Classes: any `.name` appearing at the head of a rule. Compound selectors contribute each. +CLASSES="$(grep -oE '^[^{@}]*\{' "$DESIGN" \ + | grep -oE '\.[A-Za-z_-][A-Za-z0-9_-]*' | sort -u)" +CLASS_COUNT="$(printf '%s\n' "$CLASSES" | grep -c '^\.' || true)" + +if [ "$TOKEN_COUNT" -eq 0 ] && [ "$CLASS_COUNT" -eq 0 ]; then + printf 'check-design-reaches-app: %s declares neither tokens nor classes.\n' "$DESIGN" >&2 + printf ' Nothing to trace. This is NOT a pass.\n' >&2 + exit 2 +fi + +# ── Pass 1: are the framework's knobs bound to our tokens? ────────────────────────────────── +# Declarations in BARE `:root {` blocks of the built sheet, last one winning. Anything scoped +# — `:root.theme-dark`, `:root.theme-neutral`, a class, a media query — is a theme or a state +# the app is not necessarily wearing, and counting it misreads the cascade. +BUILT_ROOT="$(awk ' + /^[[:space:]]*:root[[:space:]]*\{[[:space:]]*$/ { inroot = 1; next } + inroot && /^[[:space:]]*\}/ { inroot = 0; next } + inroot && match($0, /--[a-zA-Z0-9-]+[[:space:]]*:/) { + name = substr($0, RSTART, RLENGTH); sub(/[[:space:]]*:$/, "", name) + rest = substr($0, RSTART + RLENGTH); sub(/;[[:space:]]*$/, "", rest) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", rest) + map[name] = rest + } + END { for (k in map) print k "\t" map[k] } +' "$BUILT")" + +KNOB_TOTAL=0 +KNOB_BOUND=0 +UNBOUND_SAMPLE="" +if [ -f "$CUSTOM_VARS" ]; then + # Uncommented custom-property declarations in the framework's customization file. A `//` + # commented line is a knob the framework OFFERS and the project has not taken; it is not + # unbound, it is unclaimed, and counting it would drown the signal. + KNOBS="$(grep -oE '^[[:space:]]*--[a-z0-9-]+[[:space:]]*:' "$CUSTOM_VARS" \ + | grep -oE '\-\-[a-z0-9-]+' | sort -u)" + for knob in $KNOBS; do + KNOB_TOTAL=$((KNOB_TOTAL + 1)) + # The WINNING definition, read from BARE `:root` blocks only. + # + # A plain "last occurrence in the file" reading is wrong and was measured wrong: the last + # definition of --brand-primary in a real built sheet sits inside `:root.theme-neutral`, + # a THEME the app does not wear, and reading it reported a correctly bound knob as unbound. + # An instrument that miscounts its own denominator teaches people to ignore it. + val="$(printf '%s\n' "$BUILT_ROOT" | awk -F'\t' -v k="$knob" '$1 == k { v = $2 } END { print v }')" + [ -z "$val" ] && continue + bound=0 + # (a) bound by reference + while IFS="$(printf '\t')" read -r tname tval; do + [ -z "$tname" ] && continue + case "$val" in *"var($tname)"*) bound=1; break ;; esac + # (b) bound by repeating the literal. Only for values that look like a value, so an + # empty or inherit-ish token cannot mark every knob bound. + if [ -n "$tval" ] && [ "${#tval}" -ge 3 ]; then + stripped="$(printf '%s' "$val" | tr -d ' ')" + tstripped="$(printf '%s' "$tval" | tr -d ' ')" + if [ "$stripped" = "$tstripped" ]; then bound=1; break; fi + fi + done </dev/null; then + CSS_VARS_MODE="true" +elif grep -rqE '^[[:space:]]*\$use-css-variables[[:space:]]*:[[:space:]]*false' theme/web/ 2>/dev/null; then + CSS_VARS_MODE="false" +fi +if [ "$CSS_VARS_MODE" = "true" ]; then + for f in theme/web/*.scss themesource/*/web/*.scss; do + [ -f "$f" ] || continue + case "$f" in themesource/atlas_*|themesource/atlas_*/*) continue ;; esac + while IFS= read -r hit; do + [ -z "$hit" ] && continue + ln="${hit%%:*}" + report "$f:$ln assigns a framework SCSS variable while the theme runs \$use-css-variables: true" \ + 'That assignment compiles to nothing. Set the matching CSS custom property in the framework customization file instead.' + done </dev/null) +EOF + done +fi + +# ── WARN pass: classes the model asks for that the built sheet does not define ────────────── +MODEL_CLASS_TOTAL=0 +MODEL_CLASS_MISSING=0 +MODEL_MISSING_SAMPLE="" +if [ -d mdlsource ]; then + MODEL_CLASSES="$(grep -rhoE "Class:[[:space:]]*'[^']+'" mdlsource 2>/dev/null \ + | sed "s/.*'\\(.*\\)'/\\1/" | tr ' ' '\n' | grep -E '^[A-Za-z_-][A-Za-z0-9_-]*$' | sort -u)" + for cls in $MODEL_CLASSES; do + MODEL_CLASS_TOTAL=$((MODEL_CLASS_TOTAL + 1)) + if ! grep -qF -- ".$cls" "$BUILT"; then + MODEL_CLASS_MISSING=$((MODEL_CLASS_MISSING + 1)) + if [ "$(printf '%s' "$MODEL_MISSING_SAMPLE" | wc -w)" -lt 8 ]; then + MODEL_MISSING_SAMPLE="$MODEL_MISSING_SAMPLE $cls" + fi + fi + done +fi + +# ── Verdicts ──────────────────────────────────────────────────────────────────────────────── +if [ "$KNOB_TOTAL" -eq 0 ]; then + warn "no framework knobs found in $CUSTOM_VARS" \ + 'Pass 1 could not run. Set CUSTOM_VARS to the framework customization file, or say in the review why this project has none.' +elif [ "$KNOB_BOUND" -eq 0 ]; then + report "0 of $KNOB_TOTAL framework knobs are bound to a design-system token" \ + "The framework is still wearing its own defaults. Unbound:${UNBOUND_SAMPLE:- (all)}" +fi + +if [ "$TOKENS_ARRIVED" -eq 0 ] && [ "$TOKEN_COUNT" -gt 0 ]; then + report "0 of $TOKEN_COUNT design-system tokens exist in $BUILT" \ + 'The port did not reach the build at all. Check that the theme module directory name matches a real module.' +fi + +if [ "$CLASS_COUNT" -gt 0 ] && [ "$CLASSES_ARRIVED" -eq 0 ]; then + report "0 of $CLASS_COUNT design-system classes exist in $BUILT" \ + 'Every class the pages reference resolves to nothing. Port the component rules, not only the tokens.' +elif [ "$CLASS_COUNT" -gt 0 ] && [ "$CLASSES_ARRIVED" -lt "$CLASS_COUNT" ]; then + warn "$CLASSES_ARRIVED of $CLASS_COUNT design-system classes exist in $BUILT" \ + "Absent:${MISSING_CLASS_SAMPLE:- (none sampled)} — dormant until a widget carries one, then silently inert." +fi + +if [ "$MODEL_CLASS_MISSING" -gt 0 ]; then + warn "$MODEL_CLASS_MISSING of $MODEL_CLASS_TOTAL classes the MODEL asks for are not defined in $BUILT" \ + "Sample:${MODEL_MISSING_SAMPLE} — a class on a live widget that styles nothing. Atlas's own class names are legitimate; anything project-shaped here is a name typed from memory." +fi + +printf '\n' +printf 'design system %s\n' "$DESIGN" +printf 'built stylesheet %s\n' "$BUILT" +printf 'knobs bound %s of %s framework knobs point at a design-system token\n' "$KNOB_BOUND" "$KNOB_TOTAL" +printf 'tokens arrived %s of %s\n' "$TOKENS_ARRIVED" "$TOKEN_COUNT" +printf 'classes arrived %s of %s\n' "$CLASSES_ARRIVED" "$CLASS_COUNT" +printf 'model classes %s of %s asked for by the model are undefined\n' "$MODEL_CLASS_MISSING" "$MODEL_CLASS_TOTAL" +printf 'css-vars mode $use-css-variables: %s\n' "$CSS_VARS_MODE" + +if [ "$VIOLATIONS" -gt 0 ]; then + printf '\ncheck-design-reaches-app: %s violation(s).\n' "$VIOLATIONS" + printf 'The design system exists and the app is not wearing it. Fix the BINDING, not the tokens\n' + printf '—-the tokens were never the problem, which is why every other check stayed green.\n' + exit 1 +fi +if [ "$WARNINGS" -gt 0 ]; then + printf '\ncheck-design-reaches-app: clean, %s warning(s) — read them.\n' "$WARNINGS" + exit 0 +fi +printf '\ncheck-design-reaches-app: clean.\n' diff --git a/skills/conversion-runbook.md b/skills/conversion-runbook.md index d665e86..9524d68 100644 --- a/skills/conversion-runbook.md +++ b/skills/conversion-runbook.md @@ -37,6 +37,7 @@ table; a skill missing here is a skill no agent will find. | Writing or reviewing any page or snippet — the spacing scale (8/16/24/32/48), section rhythm, and the page-header scaffold every full page starts with; sections at 0px apart and pages with no H1 are the defects it retires | `skills/design-spacing.md` | | After every page-building script, and any time the UI looks wrong — the cheap repeatable look during the build: one page, one screenshot, three questions. Feeds Gate: UI, never replaces it | `skills/ui-loop.md` | | Building or using the in-app design gallery | `skills/learned-stylegallery.md` | +| After the FIRST build that follows any design-system port, and before any page is built on it — reads the BUILT stylesheet and reports how many framework knobs point at a design token, how many tokens arrived, how many component classes arrived, each with its denominator. Measured on a real run: 55 tokens ported correctly into the right file, 0 of 35 knobs bound and 0 of 20 classes present, two build phases shipped in the framework's default blue with mx check, mxcli lint, the MDL suite and two e2e journeys all green | `project-bin/check-design-reaches-app.sh` | | Before exec'ing ANY page script — compares the drafted MDL's shell against the wireframe's: page column, layout/nav shell, one H1. Measured 0/10 pages on a real first build, repaired wholesale 47 scripts later | `project-bin/check-page-shell.sh` | | After drafting and again after exec'ing any page script — scores the page MDL (or `mxcli describe` output on stdin) against its wireframe: headings/actions/content/classes, weighted. The scored companion to check-page-shell's binary gate; 32% median measured without it, 90% first-draft with it. Every run is appended to the project's docs/PAGE-FIDELITY.tsv — first non-stub row per page = first-build score of record vs the ≥80% target (forward-reference stubs score with --stub, exempt) | `project-bin/page-fidelity.js` | | Choosing CLI vs MCP+MDL vs hand-rolled MCP, or any MCP write session — three co-equal write modes, not CLI-only | `skills/learned-mcp-patterns.md` | diff --git a/skills/ui-preflight-pages.md b/skills/ui-preflight-pages.md index 741c104..6801c8d 100644 --- a/skills/ui-preflight-pages.md +++ b/skills/ui-preflight-pages.md @@ -73,6 +73,24 @@ stale the day the next project is scaffolded. `grep -o '^\.[a-z-]*' design/ds.cs is a fine first pass; then read the rules you plan to lean on, because a selector's shape matters (`.kpi b` styles a `` inside the tile — it is not a class you can put on a widget). +**Hard rule (B0) — before the FIRST page of the project, prove the design system reached the +app.** A stylesheet that shipped is not a stylesheet the app is wearing. Run +`project-bin/check-design-reaches-app.sh` against the built stylesheet and read the three +denominators: framework knobs bound, tokens arrived, classes arrived. `0 of N` on any of them +means every class you are about to write resolves to nothing, and every later page inherits it. + +Measured 2026-09-09: 55 tokens were ported into the right file at the right stage, 0 of 35 +framework knobs were bound, 0 of 20 component classes existed in the built stylesheet, and two +build phases of pages were written on top of it. The pages were structurally correct. `mx +check`, `mxcli lint`, the MDL suite and two e2e journeys were all green, because none of them +reads a stylesheet. It was found by a human opening a screenshot and asking why the UI was +poor. The three failures were a bridge written against the wrong layer (SCSS variables where +the theme runs `$use-css-variables: true`), component classes never ported at all, and a theme +module whose `:root` loses the cascade to the framework's own customization file. + +If there is no build yet, say so in the report block — "binding UNPROVEN, no build" is a legal +answer and silence is not. + **Hard rule (B1):** every `class:` value on a page widget must match a token in the project's design-system file. Do not invent class names, do not use bare-Atlas class names as the only class on a design-system-styled widget, do not write inline styles. diff --git a/tests/wave2/fixtures/design-reaches-app/CAPTURE.md b/tests/wave2/fixtures/design-reaches-app/CAPTURE.md new file mode 100644 index 0000000..77e098f --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/CAPTURE.md @@ -0,0 +1,40 @@ +# Fixture provenance — design-reaches-app + +## Where the compiled stylesheets came from + +`built-*.css` reproduce the STRUCTURE of a real `deployment/web/theme.compiled.css` +written by mxbuild 11.14.0 for a Mendix 11.14 app on Atlas 3 with +`$use-css-variables: true`. Captured 2026-09-09 from the field run that found the +defect this instrument exists for. + +The structural facts the parser depends on are verbatim from that file, because every +one of them is a fact nobody would have imagined: + +| Fact | Why the parser depends on it | +|---|---| +| `:root {` alone on its line, one declaration per indented line | the awk block reader keys on exactly this | +| a BARE `:root` and a SCOPED `:root.theme-neutral` both declaring `--brand-primary` | the scoped one appears LATER in the file. Reading "last occurrence" reports a correctly bound knob as unbound — measured, and it is the bug this fixture's `scoped-only` case pins | +| shades derived at runtime: `--brand-primary-600: color-mix(in srgb, var(--brand-primary), var(--color-contrast) 20%)` | why binding the ONE knob is sufficient, and why a check for a hardcoded default hex is the wrong check | +| the framework's own `:root` emitted BEFORE `theme/web/custom-variables.scss`'s | the cascade order that made a theme-module override lose | +| `/* Brand Colors */` comment lines inside the block | a comment line inside `:root` must not parse as a declaration | + +**What was changed, and nothing else:** the colour LITERALS. The real file carries a +customer's sampled brand palette and this repository is public. Hex values are replaced +with `#AA1122` / `#00CC66` style placeholders. No line was reflowed, no block reordered, +no selector altered, no structure simplified. + +## Counts asserted against the real file at capture time + +| Measured on the real build | Value | +|---|---| +| lines in `deployment/web/theme.compiled.css` | 30,900 | +| design-system tokens defined in `design/ds.css` | 43 | +| design-system tokens present in the built sheet | 43 | +| design-system classes defined | 28 | +| design-system classes present in the built sheet, BEFORE the fix | 0 | +| framework knobs bound to a design token, BEFORE the fix | 0 of 35 | +| framework knobs bound to a design token, AFTER the fix | 29 of 35 | +| occurrences of `var(--brand)` in the built sheet, BEFORE the fix | 0 | + +The 0-of-35 and 0-of-28 rows are the whole reason this instrument exists: both were true +while `mx check`, `mxcli lint`, the MDL suite and two e2e journeys were green. diff --git a/tests/wave2/fixtures/design-reaches-app/built-broken-classes.css b/tests/wave2/fixtures/design-reaches-app/built-broken-classes.css new file mode 100644 index 0000000..173a736 --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/built-broken-classes.css @@ -0,0 +1,54 @@ +.mx-page { + display: flex; +} + +:root { + /* Brand Colors */ + --brand-primary: #264ae5; + --brand-success: #16aa16; + --brand-warning: #cd8501; + --brand-danger: #EA3337; + --brand-default: color-mix(in srgb, var(--brand-primary) 10%, #e7e7e9); + --gray: #B8BABF; + --font-family-base: "Poppins", sans-serif; + --font-size-default: 14px; + --border-radius-default: 4px; +} + +:root { + --brand-primary-600: color-mix(in srgb, var(--brand-primary), var(--color-contrast) 20%); + --brand-primary-700: color-mix(in srgb, var(--brand-primary), var(--color-contrast) 30%); +} + +:root { + --brand: #AA1122; + --brand-hover: #99101F; + --brand-active: #880E1B; + --brand-ink: #770C18; + --brand-wash: #FDEEEE; + --on-brand: #FFFFFF; + --bg: #FAFAFA; + --surface: #FFFFFF; + --line: #E6E6E8; + --ink: #1B1B1F; + --ink-body: #2E2E33; + --ink-soft: #6B6B73; + --ink-faint: #9A9AA2; + --font: "Open Sans", sans-serif; + --fs-body: 15px; + --radius: 6px; + --sp-3: 16px; + --sp-4: 24px; +} + +:root { + --brand-primary: var(--brand); + --font-family-base: var(--font); + --font-size-default: var(--fs-body); + --border-radius-default: var(--radius); +} + +:root.theme-neutral { + --brand-primary: rgb(37, 37, 67); + --sidebar-bg: var(--brand-default-100); +} diff --git a/tests/wave2/fixtures/design-reaches-app/built-broken.css b/tests/wave2/fixtures/design-reaches-app/built-broken.css new file mode 100644 index 0000000..5ebdea6 --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/built-broken.css @@ -0,0 +1,47 @@ +.mx-page { + display: flex; +} + +:root { + /* Brand Colors */ + --brand-primary: #264ae5; + --brand-success: #16aa16; + --brand-warning: #cd8501; + --brand-danger: #EA3337; + --brand-default: color-mix(in srgb, var(--brand-primary) 10%, #e7e7e9); + --gray: #B8BABF; + --font-family-base: "Poppins", sans-serif; + --font-size-default: 14px; + --border-radius-default: 4px; +} + +:root { + --brand-primary-600: color-mix(in srgb, var(--brand-primary), var(--color-contrast) 20%); + --brand-primary-700: color-mix(in srgb, var(--brand-primary), var(--color-contrast) 30%); +} + +:root { + --brand: #AA1122; + --brand-hover: #99101F; + --brand-active: #880E1B; + --brand-ink: #770C18; + --brand-wash: #FDEEEE; + --on-brand: #FFFFFF; + --bg: #FAFAFA; + --surface: #FFFFFF; + --line: #E6E6E8; + --ink: #1B1B1F; + --ink-body: #2E2E33; + --ink-soft: #6B6B73; + --ink-faint: #9A9AA2; + --font: "Open Sans", sans-serif; + --fs-body: 15px; + --radius: 6px; + --sp-3: 16px; + --sp-4: 24px; +} + +:root.theme-neutral { + --brand-primary: rgb(37, 37, 67); + --sidebar-bg: var(--brand-default-100); +} diff --git a/tests/wave2/fixtures/design-reaches-app/built-fixed.css b/tests/wave2/fixtures/design-reaches-app/built-fixed.css new file mode 100644 index 0000000..a623f10 --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/built-fixed.css @@ -0,0 +1,76 @@ +.mx-page { + display: flex; +} + +:root { + /* Brand Colors */ + --brand-primary: #264ae5; + --brand-success: #16aa16; + --brand-warning: #cd8501; + --brand-danger: #EA3337; + --brand-default: color-mix(in srgb, var(--brand-primary) 10%, #e7e7e9); + --gray: #B8BABF; + --font-family-base: "Poppins", sans-serif; + --font-size-default: 14px; + --border-radius-default: 4px; +} + +:root { + --brand-primary-600: color-mix(in srgb, var(--brand-primary), var(--color-contrast) 20%); + --brand-primary-700: color-mix(in srgb, var(--brand-primary), var(--color-contrast) 30%); +} + +:root { + --brand: #AA1122; + --brand-hover: #99101F; + --brand-active: #880E1B; + --brand-ink: #770C18; + --brand-wash: #FDEEEE; + --on-brand: #FFFFFF; + --bg: #FAFAFA; + --surface: #FFFFFF; + --line: #E6E6E8; + --ink: #1B1B1F; + --ink-body: #2E2E33; + --ink-soft: #6B6B73; + --ink-faint: #9A9AA2; + --font: "Open Sans", sans-serif; + --fs-body: 15px; + --radius: 6px; + --sp-3: 16px; + --sp-4: 24px; +} + +.ds-page { + max-width: 1200px; + margin: 0 auto; + padding: var(--sp-4) var(--sp-3); +} +.ds-card { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius); +} +.ds-muted { + color: var(--ink-soft); +} +.ds-btn { + height: 40px; + border-radius: var(--radius); +} +.ds-btn--primary { + background: var(--brand); + color: var(--on-brand); +} + +:root { + --brand-primary: var(--brand); + --font-family-base: var(--font); + --font-size-default: var(--fs-body); + --border-radius-default: var(--radius); +} + +:root.theme-neutral { + --brand-primary: rgb(37, 37, 67); + --sidebar-bg: var(--brand-default-100); +} diff --git a/tests/wave2/fixtures/design-reaches-app/built-scoped-only.css b/tests/wave2/fixtures/design-reaches-app/built-scoped-only.css new file mode 100644 index 0000000..7e253a1 --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/built-scoped-only.css @@ -0,0 +1,71 @@ +.mx-page { + display: flex; +} + +:root { + /* Brand Colors */ + --brand-primary: #264ae5; + --brand-success: #16aa16; + --brand-warning: #cd8501; + --brand-danger: #EA3337; + --brand-default: color-mix(in srgb, var(--brand-primary) 10%, #e7e7e9); + --gray: #B8BABF; + --font-family-base: "Poppins", sans-serif; + --font-size-default: 14px; + --border-radius-default: 4px; +} + +:root { + --brand-primary-600: color-mix(in srgb, var(--brand-primary), var(--color-contrast) 20%); + --brand-primary-700: color-mix(in srgb, var(--brand-primary), var(--color-contrast) 30%); +} + +:root { + --brand: #AA1122; + --brand-hover: #99101F; + --brand-active: #880E1B; + --brand-ink: #770C18; + --brand-wash: #FDEEEE; + --on-brand: #FFFFFF; + --bg: #FAFAFA; + --surface: #FFFFFF; + --line: #E6E6E8; + --ink: #1B1B1F; + --ink-body: #2E2E33; + --ink-soft: #6B6B73; + --ink-faint: #9A9AA2; + --font: "Open Sans", sans-serif; + --fs-body: 15px; + --radius: 6px; + --sp-3: 16px; + --sp-4: 24px; +} + +.ds-page { + max-width: 1200px; + margin: 0 auto; + padding: var(--sp-4) var(--sp-3); +} +.ds-card { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius); +} +.ds-muted { + color: var(--ink-soft); +} +.ds-btn { + height: 40px; + border-radius: var(--radius); +} +.ds-btn--primary { + background: var(--brand); + color: var(--on-brand); +} + +:root.theme-neutral { + --brand-primary: var(--brand); + --font-family-base: var(--font); + --font-size-default: var(--fs-body); + --border-radius-default: var(--radius); +} diff --git a/tests/wave2/fixtures/design-reaches-app/ds.css b/tests/wave2/fixtures/design-reaches-app/ds.css new file mode 100644 index 0000000..db25302 --- /dev/null +++ b/tests/wave2/fixtures/design-reaches-app/ds.css @@ -0,0 +1,35 @@ +/* Synthetic design system for the fixture. Same SHAPE as a real one: a token block + under :root, a dark-theme redefinition that must be ignored, and component classes. */ +:root { + --brand: #AA1122; + --brand-hover: #99101F; + --brand-active: #880E1B; + --brand-ink: #770C18; + --brand-wash: #FDEEEE; + --on-brand: #FFFFFF; + --bg: #FAFAFA; + --surface: #FFFFFF; + --line: #E6E6E8; + --ink: #1B1B1F; + --ink-body: #2E2E33; + --ink-soft: #6B6B73; + --ink-faint: #9A9AA2; + --font: "Open Sans", sans-serif; + --fs-body: 15px; + --radius: 6px; + --sp-3: 16px; + --sp-4: 24px; +} + +@media (prefers-color-scheme: dark) { + :root { + --brand-ink: #FF7A6E; + --bg: #121214; + } +} + +.ds-page { max-width: 1200px; margin: 0 auto; padding: var(--sp-4) var(--sp-3); } +.ds-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); } +.ds-muted { color: var(--ink-soft); } +.ds-btn { height: 40px; border-radius: var(--radius); } +.ds-btn--primary { background: var(--brand); color: var(--on-brand); } diff --git a/tests/wave2/test-design-reaches-app.sh b/tests/wave2/test-design-reaches-app.sh new file mode 100755 index 0000000..ba4f2a7 --- /dev/null +++ b/tests/wave2/test-design-reaches-app.sh @@ -0,0 +1,144 @@ +#!/bin/bash +# test-design-reaches-app.sh — pin check-design-reaches-app.sh against the state that shipped. +# +# The instrument under test answers one question no other check in this toolkit asks: did the +# design system ARRIVE in the built app. It exists because on a real field run (2026-09-09) the +# answer was no for two whole build phases while every other check was green. +# +# What is worth pinning here, and the happy path is the least of it: +# +# 1. THE SHIPPED STATE MUST FAIL. `built-broken.css` is the real thing: 18 tokens present in +# the built sheet, every framework knob still on its own default, not one component class +# ported. If this fixture ever passes, the instrument has stopped instrumenting. +# +# 2. TOKENS ARRIVING IS NOT THE DESIGN SYSTEM ARRIVING. `built-broken-classes.css` binds every +# knob correctly and still ships zero classes. The sibling script +# check-design-portability.sh opens by asserting "tokens port into Mendix perfectly", and +# that sentence is what made everyone comfortable — so the two halves are pinned apart. +# +# 3. A SCOPED BINDING IS NOT A BINDING. `built-scoped-only.css` binds every knob inside +# `:root.theme-neutral`, a theme the app is not wearing. This case exists 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`. See +# fixtures/design-reaches-app/CAPTURE.md. +# +# 4. NO BUILD IS NOT A PASS. The whole defect class lives between the source and the build, so +# an instrument that green-lights a project with no compiled stylesheet is worse than +# absent. Exit 2, and 2 must not be confused with 0. +# +# Usage: bash test-design-reaches-app.sh [path-to-check-design-reaches-app.sh] + +set -u + +TOOLKIT="$(cd "$(dirname "$0")/../.." && pwd)" +SUT="${1:-$TOOLKIT/project-bin/check-design-reaches-app.sh}" +FIX="$TOOLKIT/tests/wave2/fixtures/design-reaches-app" + +PASS=0; FAIL=0 +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; } +bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n %s\n' "$1" "${2:-}"; } +check(){ if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "expected '$3', got '$2'"; fi; } +has() { case "$2" in *"$3"*) ok "$1" ;; *) bad "$1" "missing '$3'" ;; esac; } +hasnt(){ case "$2" in *"$3"*) bad "$1" "unexpected '$3'" ;; *) ok "$1" ;; esac; } + +if [ ! -x "$SUT" ]; then + printf 'test-design-reaches-app: %s is not executable\n' "$SUT" >&2 + exit 2 +fi + +# A project root shaped the way the instrument reads one: a design system, a built stylesheet, +# and the framework's customization file that enumerates its own knobs. +# $1 project dir $2 which built-*.css $3 $use-css-variables value ("" to omit) +project() { + d="$TMP/$1"; rm -rf "$d" + mkdir -p "$d/design" "$d/deployment/web" "$d/theme/web" "$d/themesource/moc/web" + cp "$FIX/ds.css" "$d/design/ds.css" + [ -n "$2" ] && cp "$FIX/$2" "$d/deployment/web/theme.compiled.css" + # The framework customization file: its uncommented :root declarations ARE the knob list. + cat > "$d/theme/web/custom-variables.scss" <<'EOF' +:root { + --brand-primary: #264ae5; + --font-family-base: "Poppins", sans-serif; + --font-size-default: 14px; + --border-radius-default: 4px; +// --link-color: var(--brand-primary); +} +EOF + [ -n "$3" ] && printf '$use-css-variables: %s;\n' "$3" >> "$d/theme/web/custom-variables.scss" + printf '%s' "$d" +} + +run() { ( cd "$1" && bash "$SUT" 2>&1 ); } +code() { ( cd "$1" && bash "$SUT" >/dev/null 2>&1; printf '%s' "$?" ); } + +printf 'test-design-reaches-app: %s\n' "$SUT" + +# ── 1. the state that shipped ─────────────────────────────────────────────────────────────── +d="$(project broken built-broken.css true)" +out="$(run "$d")" +check "the shipped state exits 1" "$(code "$d")" "1" +has "names the unbound knobs with a denominator" "$out" "of 4 framework knobs are bound" +has "reports 0 knobs bound" "$out" "0 of 4 framework knobs are bound" +has "reports the classes never arrived" "$out" "design-system classes exist" +has "states the token denominator" "$out" "tokens arrived 18 of 18" + +# ── 2. tokens arriving is not the design system arriving ──────────────────────────────────── +d="$(project bc built-broken-classes.css true)" +out="$(run "$d")" +check "bound knobs + zero classes still exits 1" "$(code "$d")" "1" +has "says the classes are the failure" "$out" "0 of 5 design-system classes exist" +hasnt "does not blame the knobs" "$out" "0 of 4 framework knobs are bound" + +# ── 3. a scoped binding is not a binding ──────────────────────────────────────────────────── +d="$(project scoped built-scoped-only.css true)" +out="$(run "$d")" +check "a theme-scoped binding exits 1" "$(code "$d")" "1" +has "counts the scoped binding as unbound" "$out" "0 of 4 framework knobs are bound" + +# ── 4. the fixed state ────────────────────────────────────────────────────────────────────── +d="$(project fixed built-fixed.css true)" +out="$(run "$d")" +check "the fixed state exits 0" "$(code "$d")" "0" +has "reports all four knobs bound" "$out" "knobs bound 4 of 4" +has "reports all classes arrived" "$out" "classes arrived 5 of 5" +has "reports all tokens arrived" "$out" "tokens arrived 18 of 18" + +# ── 5. no build is not a pass ─────────────────────────────────────────────────────────────── +d="$(project nobuild "" true)" +check "a project with no built stylesheet exits 2" "$(code "$d")" "2" +out="$(run "$d")" +has "says so out loud" "$out" "no BUILT stylesheet found" +has "refuses to be read as a pass" "$out" "NOT a pass" + +# ── 6. no design system is not a pass either ──────────────────────────────────────────────── +d="$(project nods built-fixed.css true)" +rm -f "$d/design/ds.css" +check "a project with no design system exits 2" "$(code "$d")" "2" + +# ── 7. the dead SCSS bridge, and only where it is dead ────────────────────────────────────── +d="$(project bridge built-fixed.css true)" +printf '$brand-primary: var(--brand);\n' > "$d/themesource/moc/web/main.scss" +out="$(run "$d")" +check "a dead SCSS bridge exits 1" "$(code "$d")" "1" +has "names the file and line" "$out" "main.scss:1" +has "explains it compiles to nothing" "$out" "compiles to nothing" + +# The SAME line is the CORRECT bridge where the theme does not run on custom properties, and +# reporting it there is how a check earns being switched off. +d="$(project bridge-false built-fixed.css false)" +printf '$brand-primary: var(--brand);\n' > "$d/themesource/moc/web/main.scss" +out="$(run "$d")" +hasnt "does not fire when \$use-css-variables is false" "$out" "compiles to nothing" + +# ── 8. the framework's own themesource is not the project's to fix ────────────────────────── +d="$(project vendor built-fixed.css true)" +mkdir -p "$d/themesource/atlas_core/web" +printf '$brand-primary: var(--brand);\n' > "$d/themesource/atlas_core/web/main.scss" +check "a vendored atlas_core file is not reported" "$(code "$d")" "0" + +printf '\n%s passed, %s failed\n' "$PASS" "$FAIL" +[ "$FAIL" -eq 0 ] || exit 1 From f80cdbf7b97625a80e075757b164b330602ec5e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 03:00:51 +0000 Subject: [PATCH 3/8] check-page-shell: it could not find its input, and could not bound the page body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/.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 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 Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M --- CHANGELOG.md | 1 + project-bin/check-page-shell.sh | 119 ++++++++++++++++- tests/wave2/test-page-shell-body.sh | 197 ++++++++++++++++++++++++++++ 3 files changed, 312 insertions(+), 5 deletions(-) create mode 100755 tests/wave2/test-page-shell-body.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 13945fe..58245df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Credit the person or project that surfaced the change — the credit line is the ## 2026-09-09 - new(project-bin): **`check-design-reaches-app.sh` — did any of the design system ARRIVE in the built app?** The gap it closes is a sentence in its own sibling: `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"*, which is false and 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. Measured on a MOC/PSSR app replacement, 2026-09-09: 55 tokens sampled from the customer's own screenshots, 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**. Three independent failures, none visible to anything in this toolkit: the bridge assigned **SCSS variables** (`$brand-primary: var(--brand)`) to a theme running `$use-css-variables: true`, so it compiled to nothing (`--brand` present, `--brand-primary` untouched, `var(--brand)` **0 occurrences** in the built sheet); the **20 component classes were never ported at all**, so `ds-muted` on 12 live widgets resolved to the body colour; and 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 — every one of them reads the model or the authored stylesheet, and the defect lives between the authored stylesheet and the built one. **Found by a human opening a screenshot and asking why the UI was poor.** The check deliberately does NOT look for a known default hex (a cache of one framework version — authoring rule 5); it inverts the question to *"is the winning definition of each knob the framework declares pointed at a design-system token"*, which needs no defaults table and survives the next Atlas release. Four passes with denominators (knobs bound · tokens arrived · classes arrived · dead SCSS bridge) plus a WARN for classes the **model** asks for that the built sheet never defines — which found 17 of 23 on the same project, `ds-help` among them, class names typed from memory onto live widgets. Requires a build and exits **2** without one, because "did it reach the app" cannot be answered from source, and answering it from source is how this shipped — Maurits Visser, from a MOC/PSSR app replacement +- fix(project-bin/check-page-shell.sh): **it had never passed once on a project whose wireframes are named after the SOURCE screens**, and it had no fixture, which is why both defects below sat in it. (1) The wireframe path was hardcoded `design/wireframes/.html`; wireframes drawn from a legacy system are named after that system's screens (`moc-project-detail.html`) while Mendix pages are named in Mendix convention (`MOCProject_Detail`). Measured on a MOC/PSSR app replacement: 15 wireframes, 8 built pages, *"no wireframe"* for **every** page, exit 2 — a check that cannot find its input across a whole class of project is a check that gets switched off. Now an optional two-column `design/wireframes/PAGE-MAP.tsv`; absent map, unchanged behaviour; a page the map does **not** name is still reported **naming the map**, so the map cannot hide a page by omission; and a row pointing at a missing wireframe is reported, because a stale row is how a rename turns into a page nobody checks. (2) The page body was bounded *"to the closing brace at column 0"*, which reads the rest of the **file** for a page whose body is on one line — a nav-shell script with five such stubs reported *"page MOCProject_Overview declares 5 H1 titles; its wireframe draws one"*, and every number was the count for the whole file. **The worse kind of false positive: it fires on correct code, cites a real rule, and the fix it asks for would break four pages.** Two line-based repairs were tried and each broke the other case (a `Params: { $X: Mod.Entity }` brace closing on its own line ends a "started on any brace" scan before the body opens; a "started only when depth > 0" scan never starts on a one-line body), so the extractor now counts **characters** and tracks parens. New fixture `tests/wave2/test-page-shell-body.sh`, **18 assertions**, pinning both cases in one file because that is the pair that must hold at once, plus that the rules still FIRE on a real `Class: 'h1'` title and on two H1s in one body. What it found once it could run: **9 title widgets across 4 page scripts using `Class: 'h1'` instead of `RenderMode: H1`** — which is the same defect the screenshots showed, because `Class: 'h1'` leaves a DYNAMICTEXT an inline `` and the caption rendered on the same line as the heading — Maurits Visser, from a MOC/PSSR app replacement - new(obligations): **`design-reaches-app` — the second obligation below stage 5**, `mdl`/project/`from-stage 3`, artifact `docs/design-reaches-app.md` with a denominator, degradable. Owed by `mdl` rather than `review` deliberately: the agent that performed the port is the one that must show it landed. The reason the defect above escaped is not that the check was hard — it is that **nobody was owed an answer**, so every gate it passed through was green-by-absence. A project with no build yet discharges it by saying so (*"binding UNPROVEN, no build"*); silence does not — Maurits Visser, from a MOC/PSSR app replacement - fix(project-bin/check-design-portability.sh): the falsified premise is **corrected in the header 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 — Maurits Visser, from a MOC/PSSR app replacement - learn(ui-preflight-pages): **hard rule B0 — before the FIRST page of the project, prove the design system reached the app.** `0 of N` on any of the three denominators means every class you are about to write resolves to nothing, and every later page inherits it. Placed at the consumer, in Step 2, because the ordering is the finding: the port happened at the stage that owed it and the pages were built on top of it unproven — Maurits Visser, from a MOC/PSSR app replacement diff --git a/project-bin/check-page-shell.sh b/project-bin/check-page-shell.sh index a5e9feb..3c58dd9 100755 --- a/project-bin/check-page-shell.sh +++ b/project-bin/check-page-shell.sh @@ -79,6 +79,56 @@ if [ ! -d "$WF_DIR" ]; then exit 2 fi +# --- Resolving a page's wireframe ------------------------------------------------------ +# The default is `design/wireframes/.html`, and for one whole project shape that +# is simply wrong: wireframes drawn from a LEGACY system are named after the source's own +# screens (`moc-project-detail.html`, `notification-templates.html`) while the target pages +# are named in Mendix convention (`MOCProject_Detail`, `NotificationTemplate_Overview`). +# +# Measured on a MOC/PSSR app replacement, 2026-09-09: 15 wireframes, 8 built pages, and +# this check reported "no wireframe" for EVERY page and exited 2 — so it had never passed +# once, on a project that had already shipped a phase of pages carrying the exact +# inline-H1 defect it exists to catch. A check that cannot resolve its own input across a +# whole class of project is a check that gets switched off, which is the failure this +# script's header warns about for a different reason. +# +# So: an optional two-column TAB-separated map, `design/wireframes/PAGE-MAP.tsv` — +# +# MOCProject_Detailmoc-project-detail.html +# +# Blank lines and #-comments ignored. 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. And a map row pointing at a wireframe that does not exist is reported +# too: a stale row is how a wireframe rename turns into a page nobody checks. +WF_MAP="${WF_MAP:-$WF_DIR/PAGE-MAP.tsv}" + +wf_for() { + if [ -f "$WF_MAP" ]; then + mapped="$(awk -F'\t' -v p="$1" ' + /^[[:space:]]*#/ { next } + NF < 2 { next } + { gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1); gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2) } + $1 == p { print $2; exit } + ' "$WF_MAP")" + if [ -n "$mapped" ]; then printf '%s/%s' "$WF_DIR" "$mapped"; return; fi + fi + printf '%s/%s.html' "$WF_DIR" "$1" +} + +if [ -f "$WF_MAP" ]; then + MAP_ROWS=0 + while IFS="$(printf '\t')" read -r mpage mwf _rest; do + case "$mpage" in ''|\#*) continue ;; esac + [ -n "${mwf:-}" ] || continue + MAP_ROWS=$((MAP_ROWS + 1)) + if [ ! -f "$WF_DIR/$mwf" ]; then + report "$WF_MAP" "$mpage" "maps to $mwf, which does not exist in $WF_DIR" \ + 'A stale map row is how a wireframe rename turns into a page nobody checks. Fix the row or delete it.' + fi + done < "$WF_MAP" + printf 'check-page-shell: PAGE-MAP.tsv in use - %s row(s)\n\n' "$MAP_ROWS" +fi + for f in $TARGETS; do [ -f "$f" ] || continue SCANNED=$((SCANNED + 1)) @@ -94,16 +144,75 @@ for f in $TARGETS; do page="$(printf '%s' "$txt" | sed -E 's/.*[Pp][Aa][Gg][Ee][[:space:]]+"?[A-Za-z0-9_]+"?\."?([A-Za-z0-9_]+)"?.*/\1/')" [ -n "$page" ] || continue - wf="$WF_DIR/$page.html" + wf="$(wf_for "$page")" if [ ! -f "$wf" ]; then - report "$f:$ln" "page $page has no wireframe at $wf" \ - 'ui-preflight-pages.md Step 1: no wireframe is a STOP. Draw it (design-artifacts.md) or map it; do not build the page against a guess.' + if [ -f "$WF_MAP" ]; then + report "$f:$ln" "page $page has no wireframe: not at $WF_DIR/$page.html and not named in $WF_MAP" \ + 'ui-preflight-pages.md Step 1: no wireframe is a STOP. Add a TAB row to PAGE-MAP.tsv, or draw it (design-artifacts.md); do not build the page against a guess.' + else + report "$f:$ln" "page $page has no wireframe at $wf" \ + 'ui-preflight-pages.md Step 1: no wireframe is a STOP. Draw it (design-artifacts.md), or map it in design/wireframes/PAGE-MAP.tsv when your wireframes are named after the SOURCE screens rather than the target pages; do not build the page against a guess.' + fi continue fi PAGES=$((PAGES + 1)) - # The page body: from this declaration to the closing brace at column 0. - body="$(awk -v start="$ln" 'NR>=start{print} NR>start && /^\}/{exit}' "$f")" + # The page body: from this declaration to the brace that MATCHES the body's opening + # one, counted by depth. + # + # It used to be "to the closing brace at column 0", and that reads the whole rest of the + # file for a page whose body is on ONE line — `{ HEADER hdr { DYNAMICTEXT h1 (...) } }` + # has no column-0 close, so the scan ran on to the NEXT page's. Measured on a MOC/PSSR + # app replacement, 2026-09-09: a nav-shell script declaring five such stubs reported + # "page MOCProject_Overview declares 5 H1 titles; its wireframe draws one", and the + # notification script reported 2 for a page that has exactly 1. Every number was the + # count for the whole FILE. + # + # That is the worse kind of false positive: it fires on correct code, it names a real + # rule, and the fix it asks for (delete four H1s) would have broken four pages. A check + # that cannot bound its own input tells you nothing about the input. + body="$(awk -v start="$ln" ' + # The page body: from the declaration to the brace that MATCHES the body opening + # one, counted per CHARACTER, with parens tracked so a brace inside the + # declaration cannot be mistaken for the body. + # + # It used to be "to the closing brace at column 0", and that reads the rest of the + # file for a page whose body is on ONE line: `{ HEADER hdr { DYNAMICTEXT h1 (...) } }` + # has no column-0 close, so the scan ran on into the NEXT page. Measured on a + # MOC/PSSR app replacement, 2026-09-09 — a nav-shell script declaring five such + # stubs reported "page MOCProject_Overview declares 5 H1 titles; its wireframe draws + # one", and every number was the count for the whole FILE. That is the worse kind of + # false positive: it fires on correct code, it names a real rule, and the fix it asks + # for (delete four H1s) would have broken four pages. + # + # Two line-based repairs were tried first and each broke the other case, which is why + # this counts characters. A per-line "started on any brace" reading exits on a + # BALANCED one-line construct — a multi-line declaration carrying + # `Params: { $X: Mod.Entity }` closes on its own line — and reported a page with a + # correct RenderMode: H1 as having none. A per-line "started only when depth > 0" + # reading never starts on a one-line body, where the braces balance within the line. + # Only the paren state separates the two, so the paren state is tracked. + NR < start { next } + { print $0 } + done { exit } + { + line = $0 + sub(/--.*$/, "", line) # a trailing -- note must not move the depth + n = length(line) + for (i = 1; i <= n; i++) { + c = substr(line, i, 1) + if (!started) { + if (c == "(") pd++ + else if (c == ")") { if (pd > 0) pd-- } + else if (c == "{" && pd == 0) { started = 1; bd = 1 } + } else { + if (c == "{") bd++ + else if (c == "}") { bd--; if (bd <= 0) { done = 1; break } } + } + } + } + done { exit } + ' "$f")" # A popup owns neither the app column nor a page title — the popup chrome supplies # both, and restating the title in the body is its own documented defect diff --git a/tests/wave2/test-page-shell-body.sh b/tests/wave2/test-page-shell-body.sh new file mode 100755 index 0000000..56e0001 --- /dev/null +++ b/tests/wave2/test-page-shell-body.sh @@ -0,0 +1,197 @@ +#!/bin/bash +# test-page-shell-body.sh — pin check-page-shell.sh's two blind spots. +# +# The script had NO fixture at all, which is why both defects below sat in it: it is +# routed baseline for mdl/gate/review, installed into every project, and until +# 2026-09-09 it had never passed once on a project whose wireframes are named after the +# SOURCE screens rather than the target pages. +# +# 1. IT COULD NOT RESOLVE ITS OWN INPUT. The wireframe path was hardcoded as +# `design/wireframes/.html`. Wireframes drawn from a legacy system are named +# after that system's screens (`moc-project-detail.html`) while Mendix pages are named +# in Mendix convention (`MOCProject_Detail`). Measured: 15 wireframes, 8 built pages, +# "no wireframe" reported for every page, exit 2. A check that cannot find its input on +# a whole class of project is a check that gets switched off. +# +# 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, so a nav-shell script with five such +# stubs reported "declares 5 H1 titles; its wireframe draws one" for the first page — +# 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 break four pages. +# Two line-based repairs were tried and each broke the other case, which is why the +# extractor now counts characters and tracks parens. Both cases are pinned here, in the +# same file, because that is the pair that has to hold at once. +# +# Usage: bash test-page-shell-body.sh [path-to-check-page-shell.sh] + +set -u + +TOOLKIT="$(cd "$(dirname "$0")/../.." && pwd)" +SUT="${1:-$TOOLKIT/project-bin/check-page-shell.sh}" + +PASS=0; FAIL=0 +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; } +bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n %s\n' "$1" "${2:-}"; } +check(){ if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "expected '$3', got '$2'"; fi; } +has() { case "$2" in *"$3"*) ok "$1" ;; *) bad "$1" "missing '$3'" ;; esac; } +hasnt(){ case "$2" in *"$3"*) bad "$1" "unexpected '$3'" ;; *) ok "$1" ;; esac; } + +[ -x "$SUT" ] || { printf '%s is not executable\n' "$SUT" >&2; exit 2; } + +# A wireframe with one

and no page column, so only the H1 checks can fire. +wireframe() { + mkdir -p "$1/design/wireframes" + cat > "$1/design/wireframes/$2" <<'EOF' + +

A screen

purpose

+ +EOF +} + +proj() { d="$TMP/$1"; rm -rf "$d"; mkdir -p "$d/mdlsource"; printf '%s' "$d"; } +run() { ( cd "$1" && bash "$SUT" mdlsource/pages.mdl 2>&1 ); } +code() { ( cd "$1" && bash "$SUT" mdlsource/pages.mdl >/dev/null 2>&1; printf '%s' "$?" ); } + +printf 'test-page-shell-body: %s\n' "$SUT" + +# ── 1. five one-line bodies in one file: each page's H1 is its own ─────────────────────── +d="$(proj oneline)" +for w in One.html Two.html Three.html Four.html Five.html; do wireframe "$d" "$w"; done +cat > "$d/mdlsource/pages.mdl" <<'EOF' +CREATE OR REPLACE PAGE MOC.One + (Title: 'One', Layout: MOC.App_Default, Folder: 'F') + { HEADER hdr { DYNAMICTEXT h1 (Content: 'One', RenderMode: H1) } } + +CREATE OR REPLACE PAGE MOC.Two + (Title: 'Two', Layout: MOC.App_Default, Folder: 'F') + { HEADER hdr { DYNAMICTEXT h1 (Content: 'Two', RenderMode: H1) } } + +CREATE OR REPLACE PAGE MOC.Three + (Title: 'Three', Layout: MOC.App_Default, Folder: 'F') + { HEADER hdr { DYNAMICTEXT h1 (Content: 'Three', RenderMode: H1) } } + +CREATE OR REPLACE PAGE MOC.Four + (Title: 'Four', Layout: MOC.App_Default, Folder: 'F') + { HEADER hdr { DYNAMICTEXT h1 (Content: 'Four', RenderMode: H1) } } + +CREATE OR REPLACE PAGE MOC.Five + (Title: 'Five', Layout: MOC.App_Default, Folder: 'F') + { HEADER hdr { DYNAMICTEXT h1 (Content: 'Five', RenderMode: H1) } } +EOF +out="$(run "$d")" +check "five correct one-line pages exit 0" "$(code "$d")" "0" +hasnt "does not report 5 H1s on the first page" "$out" "declares 5 H1" +hasnt "does not report any multiple-H1 violation" "$out" "H1 titles; its wireframe draws one" +has "counted all five pages" "$out" "5 page(s)" + +# ── 2. a multi-line declaration whose Params carry a brace ────────────────────────────── +d="$(proj params)" +wireframe "$d" "Edit.html" +cat > "$d/mdlsource/pages.mdl" <<'EOF' +CREATE OR REPLACE PAGE MOC.Edit + ( + Title: 'Edit', + Layout: MOC.App_Default, + Folder: 'F', + Params: { $Template: MOC.Thing } + ) + { + HEADER hdrEdit { + DYNAMICTEXT h1Edit (Content: 'Edit', RenderMode: H1) + } + } +EOF +out="$(run "$d")" +check "a Params brace does not end the body early" "$(code "$d")" "0" +hasnt "finds the H1 that is there" "$out" "has no RenderMode: H1 title block" + +# ── 3. and the rule still FIRES when the H1 really is missing ─────────────────────────── +d="$(proj noh1)" +wireframe "$d" "Bare.html" +cat > "$d/mdlsource/pages.mdl" <<'EOF' +CREATE OR REPLACE PAGE MOC.Bare + (Title: 'Bare', Layout: MOC.App_Default, Folder: 'F') + { HEADER hdr { DYNAMICTEXT h1 (Content: 'Bare', Class: 'h1') } } +EOF +out="$(run "$d")" +check "a Class-only title still violates" "$(code "$d")" "1" +has "names the missing render mode" "$out" "has no RenderMode: H1 title block" + +# ── 4. two H1s in ONE body is still a violation ───────────────────────────────────────── +d="$(proj twoh1)" +wireframe "$d" "Double.html" +cat > "$d/mdlsource/pages.mdl" <<'EOF' +CREATE OR REPLACE PAGE MOC.Double + (Title: 'Double', Layout: MOC.App_Default, Folder: 'F') + { + HEADER hdr { + DYNAMICTEXT h1a (Content: 'One', RenderMode: H1) + DYNAMICTEXT h1b (Content: 'Two', RenderMode: H1) + } + } +EOF +out="$(run "$d")" +check "two H1s in one page still violate" "$(code "$d")" "1" +has "reports the real count" "$out" "declares 2 H1 titles" + +# ── 5. PAGE-MAP.tsv resolves a source-named wireframe ─────────────────────────────────── +d="$(proj map)" +wireframe "$d" "moc-project-detail.html" +printf '# comment\n\nMOCProject_Detail\tmoc-project-detail.html\n' > "$d/design/wireframes/PAGE-MAP.tsv" +cat > "$d/mdlsource/pages.mdl" <<'EOF' +CREATE OR REPLACE PAGE MOC.MOCProject_Detail + (Title: 'Project', Layout: MOC.App_Default, Folder: 'F') + { HEADER hdr { DYNAMICTEXT h1 (Content: 'Project', RenderMode: H1) } } +EOF +out="$(run "$d")" +check "a mapped wireframe resolves" "$(code "$d")" "0" +has "says the map is in use, with a count" "$out" "PAGE-MAP.tsv in use - 1 row(s)" + +# ── 6. the map cannot hide a page by omission ─────────────────────────────────────────── +d="$(proj mapomit)" +wireframe "$d" "moc-project-detail.html" +printf 'MOCProject_Detail\tmoc-project-detail.html\n' > "$d/design/wireframes/PAGE-MAP.tsv" +cat > "$d/mdlsource/pages.mdl" <<'EOF' +CREATE OR REPLACE PAGE MOC.SomethingElse + (Title: 'Else', Layout: MOC.App_Default, Folder: 'F') + { HEADER hdr { DYNAMICTEXT h1 (Content: 'Else', RenderMode: H1) } } +EOF +out="$(run "$d")" +# Exit 2, not 1: the page is reported AND zero pages were inspectable, and the script +# treats "inspected nothing" as its own non-pass. Both are failures; what matters is +# that it is not 0 and that the report names the map. +check "an unmapped page is still a miss" "$(code "$d")" "2" +has "names the map in the report" "$out" "not named in design/wireframes/PAGE-MAP.tsv" + +# ── 7. a stale map row is reported ────────────────────────────────────────────────────── +d="$(proj mapstale)" +wireframe "$d" "moc-project-detail.html" +printf 'MOCProject_Detail\tmoc-project-detail.html\nGone\tdeleted-wireframe.html\n' \ + > "$d/design/wireframes/PAGE-MAP.tsv" +cat > "$d/mdlsource/pages.mdl" <<'EOF' +CREATE OR REPLACE PAGE MOC.MOCProject_Detail + (Title: 'Project', Layout: MOC.App_Default, Folder: 'F') + { HEADER hdr { DYNAMICTEXT h1 (Content: 'Project', RenderMode: H1) } } +EOF +out="$(run "$d")" +check "a stale map row fails the run" "$(code "$d")" "1" +has "names the missing wireframe" "$out" "deleted-wireframe.html, which does not exist" + +# ── 8. the per-line opt-out is honoured, and needs no wireframe ───────────────────────── +d="$(proj optout)" +mkdir -p "$d/design/wireframes" +wireframe "$d" "Other.html" +cat > "$d/mdlsource/pages.mdl" <<'EOF' +CREATE OR REPLACE PAGE MOC.DevOnly -- page-shell-ok: developer-facing diagnostic, not a product screen + (Title: 'Dev', Layout: MOC.App_Default, Folder: 'F') + { HEADER hdr { DYNAMICTEXT h1 (Content: 'Dev', Class: 'h1') } } +EOF +check "an opted-out page is skipped entirely" "$(code "$d")" "2" +out="$(run "$d")" +has "and inspecting nothing is NOT a pass" "$out" "NOT a pass" + +printf '\n%s passed, %s failed\n' "$PASS" "$FAIL" +[ "$FAIL" -eq 0 ] || exit 1 From cde442de00bababf2a0b4ac9f41b9c1abb369f13 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 03:16:00 +0000 Subject: [PATCH 4/8] snapshot-mpr: gitignore its own output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M --- CHANGELOG.md | 1 + project-bin/snapshot-mpr.sh | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58245df..9f18aca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Credit the person or project that surfaced the change — the credit line is the ## 2026-09-09 - new(project-bin): **`check-design-reaches-app.sh` — did any of the design system ARRIVE in the built app?** The gap it closes is a sentence in its own sibling: `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"*, which is false and 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. Measured on a MOC/PSSR app replacement, 2026-09-09: 55 tokens sampled from the customer's own screenshots, 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**. Three independent failures, none visible to anything in this toolkit: the bridge assigned **SCSS variables** (`$brand-primary: var(--brand)`) to a theme running `$use-css-variables: true`, so it compiled to nothing (`--brand` present, `--brand-primary` untouched, `var(--brand)` **0 occurrences** in the built sheet); the **20 component classes were never ported at all**, so `ds-muted` on 12 live widgets resolved to the body colour; and 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 — every one of them reads the model or the authored stylesheet, and the defect lives between the authored stylesheet and the built one. **Found by a human opening a screenshot and asking why the UI was poor.** The check deliberately does NOT look for a known default hex (a cache of one framework version — authoring rule 5); it inverts the question to *"is the winning definition of each knob the framework declares pointed at a design-system token"*, which needs no defaults table and survives the next Atlas release. Four passes with denominators (knobs bound · tokens arrived · classes arrived · dead SCSS bridge) plus a WARN for classes the **model** asks for that the built sheet never defines — which found 17 of 23 on the same project, `ds-help` among them, class names typed from memory onto live widgets. Requires a build and exits **2** without one, because "did it reach the app" cannot be answered from source, and answering it from source is how this shipped — Maurits Visser, from a MOC/PSSR app replacement +- fix(project-bin/snapshot-mpr.sh): **it now gitignores its own output.** It writes a FULL copy of the `.mpr` and every `mprcontents` unit before each exec and keeps the five newest, and 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**. 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 — Maurits Visser, from a MOC/PSSR app replacement - fix(project-bin/check-page-shell.sh): **it had never passed once on a project whose wireframes are named after the SOURCE screens**, and it had no fixture, which is why both defects below sat in it. (1) The wireframe path was hardcoded `design/wireframes/.html`; wireframes drawn from a legacy system are named after that system's screens (`moc-project-detail.html`) while Mendix pages are named in Mendix convention (`MOCProject_Detail`). Measured on a MOC/PSSR app replacement: 15 wireframes, 8 built pages, *"no wireframe"* for **every** page, exit 2 — a check that cannot find its input across a whole class of project is a check that gets switched off. Now an optional two-column `design/wireframes/PAGE-MAP.tsv`; absent map, unchanged behaviour; a page the map does **not** name is still reported **naming the map**, so the map cannot hide a page by omission; and a row pointing at a missing wireframe is reported, because a stale row is how a rename turns into a page nobody checks. (2) The page body was bounded *"to the closing brace at column 0"*, which reads the rest of the **file** for a page whose body is on one line — a nav-shell script with five such stubs reported *"page MOCProject_Overview declares 5 H1 titles; its wireframe draws one"*, and every number was the count for the whole file. **The worse kind of false positive: it fires on correct code, cites a real rule, and the fix it asks for would break four pages.** Two line-based repairs were tried and each broke the other case (a `Params: { $X: Mod.Entity }` brace closing on its own line ends a "started on any brace" scan before the body opens; a "started only when depth > 0" scan never starts on a one-line body), so the extractor now counts **characters** and tracks parens. New fixture `tests/wave2/test-page-shell-body.sh`, **18 assertions**, pinning both cases in one file because that is the pair that must hold at once, plus that the rules still FIRE on a real `Class: 'h1'` title and on two H1s in one body. What it found once it could run: **9 title widgets across 4 page scripts using `Class: 'h1'` instead of `RenderMode: H1`** — which is the same defect the screenshots showed, because `Class: 'h1'` leaves a DYNAMICTEXT an inline `` and the caption rendered on the same line as the heading — Maurits Visser, from a MOC/PSSR app replacement - new(obligations): **`design-reaches-app` — the second obligation below stage 5**, `mdl`/project/`from-stage 3`, artifact `docs/design-reaches-app.md` with a denominator, degradable. Owed by `mdl` rather than `review` deliberately: the agent that performed the port is the one that must show it landed. The reason the defect above escaped is not that the check was hard — it is that **nobody was owed an answer**, so every gate it passed through was green-by-absence. A project with no build yet discharges it by saying so (*"binding UNPROVEN, no build"*); silence does not — Maurits Visser, from a MOC/PSSR app replacement - fix(project-bin/check-design-portability.sh): the falsified premise is **corrected in the header 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 — Maurits Visser, from a MOC/PSSR app replacement diff --git a/project-bin/snapshot-mpr.sh b/project-bin/snapshot-mpr.sh index 09603c1..36113b7 100755 --- a/project-bin/snapshot-mpr.sh +++ b/project-bin/snapshot-mpr.sh @@ -24,6 +24,27 @@ set -euo pipefail MODEL_DIR="$(find_model_dir)" || exit 1 +# A producer guarantees its own output is ignored. This writes a FULL copy of the .mpr and +# every mprcontents unit, five deep — so a project that does not gitignore it commits the +# client's whole model several times over, and then reports thousands of phantom deletions +# the next time the rotation below prunes one. +# +# Measured on a MOC/PSSR app replacement, 2026-09-09: 2,089 snapshot files tracked, and +# `mxcli exec` then REFUSED to run, because exec.sh read the pruned snapshot as uncommitted +# model changes — this script's output blocking this script's own guard. It is written here +# rather than in init-project.sh on purpose: here it also reaches every project that already +# exists, on its next exec, instead of only the ones scaffolded after today. +if [ -d "$PROJECT_ROOT/.git" ] || [ -f "$PROJECT_ROOT/.git" ]; then + GI="$PROJECT_ROOT/.gitignore" + if ! { [ -f "$GI" ] && grep -qE '^/?\.mpr-snapshots/?$' "$GI"; }; then + if [ -f "$GI" ] && [ -s "$GI" ] && [ -n "$(tail -c 1 "$GI")" ]; then printf '\n' >> "$GI"; fi + printf '# Pre-exec model snapshots (project-bin/snapshot-mpr.sh). A full .mpr + mprcontents\n' >> "$GI" + printf '# copy per exec, five kept. Never committed: it is the client model, several times over.\n' >> "$GI" + printf '/.mpr-snapshots/\n' >> "$GI" + printf 'snapshot-mpr: added /.mpr-snapshots/ to .gitignore (it was not ignored).\n' >&2 + fi +fi + mkdir -p "$PROJECT_ROOT/.mpr-snapshots" DEST="$PROJECT_ROOT/.mpr-snapshots/$(date +%Y%m%d-%H%M%S)" mkdir -p "$DEST" From 093da0f811c52c50e60d99c078768c6fc612e667 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 04:10:35 +0000 Subject: [PATCH 5/8] Two workflow signatures mxcli check passes and the native build rejects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M --- CHANGELOG.md | 1 + skills/learned-workflow-patterns.md | 29 +++++++++++++++++++++++++++++ skills/workflow-structure-rules.md | 19 +++++++++++++++++-- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f18aca..7071c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Kinds: `new` · `fix` · `learn` (a skill/learning) · `process` (rules, templat Credit the person or project that surfaced the change — the credit line is the thank-you. ## 2026-09-09 +- learn(workflow-structure-rules, learned-workflow-patterns): **two workflow signatures that `mxcli check` passes clean and the native build rejects**, plus a v0.21.0 re-probe of the four constructs a real approval chain needs. (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`, execs, round-trips through `DESCRIBE`, and 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** — one parameter is the natural thing to write and it is wrong. Neither signature appears anywhere in `mxcli syntax workflow`, whose own targeting example is `TARGETING MICROFLOW HR.GetApprovers` with no signature stated. **The expensive half is the planning consequence: 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 one project's Phase 7 was ordered. Also: with the signatures right, user task + targeting microflow, **multi-user task**, `CALL MICROFLOW` with a quoted `WITH`, and a **backward `JUMP TO`** inside an outcome all build clean on v0.21.0. That result is only worth having because **the same run included the construct known to be broken as a control**: a scripted `DECISION` passed `mxcli check`, printed `Created workflow`, and left the project unopenable (`StorageLoadException` → `BUILD FAILED`), so BUG-76 is open on v0.21.0 and the probe demonstrably distinguishes working from broken. A probe with no known-bad control cannot tell *"it works"* from *"the probe is blind"* — this repo has already published one wrong conclusion for that exact reason (BUG-121) — Maurits Visser, from a MOC/PSSR app replacement - new(project-bin): **`check-design-reaches-app.sh` — did any of the design system ARRIVE in the built app?** The gap it closes is a sentence in its own sibling: `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"*, which is false and 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. Measured on a MOC/PSSR app replacement, 2026-09-09: 55 tokens sampled from the customer's own screenshots, 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**. Three independent failures, none visible to anything in this toolkit: the bridge assigned **SCSS variables** (`$brand-primary: var(--brand)`) to a theme running `$use-css-variables: true`, so it compiled to nothing (`--brand` present, `--brand-primary` untouched, `var(--brand)` **0 occurrences** in the built sheet); the **20 component classes were never ported at all**, so `ds-muted` on 12 live widgets resolved to the body colour; and 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 — every one of them reads the model or the authored stylesheet, and the defect lives between the authored stylesheet and the built one. **Found by a human opening a screenshot and asking why the UI was poor.** The check deliberately does NOT look for a known default hex (a cache of one framework version — authoring rule 5); it inverts the question to *"is the winning definition of each knob the framework declares pointed at a design-system token"*, which needs no defaults table and survives the next Atlas release. Four passes with denominators (knobs bound · tokens arrived · classes arrived · dead SCSS bridge) plus a WARN for classes the **model** asks for that the built sheet never defines — which found 17 of 23 on the same project, `ds-help` among them, class names typed from memory onto live widgets. Requires a build and exits **2** without one, because "did it reach the app" cannot be answered from source, and answering it from source is how this shipped — Maurits Visser, from a MOC/PSSR app replacement - fix(project-bin/snapshot-mpr.sh): **it now gitignores its own output.** It writes a FULL copy of the `.mpr` and every `mprcontents` unit before each exec and keeps the five newest, and 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**. 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 — Maurits Visser, from a MOC/PSSR app replacement - fix(project-bin/check-page-shell.sh): **it had never passed once on a project whose wireframes are named after the SOURCE screens**, and it had no fixture, which is why both defects below sat in it. (1) The wireframe path was hardcoded `design/wireframes/.html`; wireframes drawn from a legacy system are named after that system's screens (`moc-project-detail.html`) while Mendix pages are named in Mendix convention (`MOCProject_Detail`). Measured on a MOC/PSSR app replacement: 15 wireframes, 8 built pages, *"no wireframe"* for **every** page, exit 2 — a check that cannot find its input across a whole class of project is a check that gets switched off. Now an optional two-column `design/wireframes/PAGE-MAP.tsv`; absent map, unchanged behaviour; a page the map does **not** name is still reported **naming the map**, so the map cannot hide a page by omission; and a row pointing at a missing wireframe is reported, because a stale row is how a rename turns into a page nobody checks. (2) The page body was bounded *"to the closing brace at column 0"*, which reads the rest of the **file** for a page whose body is on one line — a nav-shell script with five such stubs reported *"page MOCProject_Overview declares 5 H1 titles; its wireframe draws one"*, and every number was the count for the whole file. **The worse kind of false positive: it fires on correct code, cites a real rule, and the fix it asks for would break four pages.** Two line-based repairs were tried and each broke the other case (a `Params: { $X: Mod.Entity }` brace closing on its own line ends a "started on any brace" scan before the body opens; a "started only when depth > 0" scan never starts on a one-line body), so the extractor now counts **characters** and tracks parens. New fixture `tests/wave2/test-page-shell-body.sh`, **18 assertions**, pinning both cases in one file because that is the pair that must hold at once, plus that the rules still FIRE on a real `Class: 'h1'` title and on two H1s in one body. What it found once it could run: **9 title widgets across 4 page scripts using `Class: 'h1'` instead of `RenderMode: H1`** — which is the same defect the screenshots showed, because `Class: 'h1'` leaves a DYNAMICTEXT an inline `` and the caption rendered on the same line as the heading — Maurits Visser, from a MOC/PSSR app replacement diff --git a/skills/learned-workflow-patterns.md b/skills/learned-workflow-patterns.md index 0d500d0..5ddfd1d 100644 --- a/skills/learned-workflow-patterns.md +++ b/skills/learned-workflow-patterns.md @@ -312,6 +312,35 @@ WAIT FOR TIMER 'addDays([%CurrentDateTime%], 1)'; WAIT FOR NOTIFICATION; ``` +**The two signatures a workflow's own gates check and `mxcli check` does not** (v0.21.0, +2026-09-09). Both were found by running the native build on a probe whose `mxcli check` was +completely clean, and neither is stated anywhere in `mxcli syntax workflow`: + +```sql +-- A USER TASK's page takes the TASK, never the workflow's context entity. +CREATE OR MODIFY PAGE MyModule.WF_Task_ApproveReject + ( Title: 'Task', Layout: MyModule.App_Default, + Params: { $Task: System.WorkflowUserTask } ) -- <- this, not the context entity + { ... } + +-- A TARGETING MICROFLOW takes TWO parameters, in this order. +CREATE OR MODIFY MICROFLOW MyModule.SUB_ResolveAssignee ( + $Workflow: System.Workflow, -- <- easy to omit; omitting it is the failure + $Context: MyModule.Request +) +RETURNS List of System.User AS $Users +BEGIN ... END; +``` + +Write them the natural way instead and `mxcli check --references` passes, `exec` prints +`Created workflow`, `DESCRIBE` round-trips — and the native build says *"The selected page 'X' +should accept a parameter of type 'WorkflowUserTask', but expects parameters of types 'Request' +instead"* and *"should accept parameters of type 'System.Workflow' and 'MyModule.Request'"*. + +**The planning consequence, which is the expensive half:** the task pages are a **prerequisite of +the workflow**, not a later UI row. A plan that builds the workflow at row 41 and its pages at row +45 cannot make row 41 green. + **The `WITH` clause's value must be quoted.** `WITH ("Ctx" = '$WorkflowContext')` works; `WITH (Ctx = $WorkflowContext)` **segfaults the binary** — and the "parameter is not mapped" hint `--references` prints talks you straight into the crashing spelling. BUG-107. diff --git a/skills/workflow-structure-rules.md b/skills/workflow-structure-rules.md index d82358c..e42aa7b 100644 --- a/skills/workflow-structure-rules.md +++ b/skills/workflow-structure-rules.md @@ -287,9 +287,11 @@ specific mxcli build, and "proven" means proven *on or after* the version named `HELP` is not evidence of non-support (`learned-workflow-patterns.md` §7), but a version below a row's stated floor is evidence against it. -| Construct | MDL status — probed on **mxcli v0.20.0 / Mendix 11.14.0, 2026-09-03** | +| Construct | MDL status — probed on **mxcli v0.20.0 / Mendix 11.14.0, 2026-09-03**; the rows Phase-7-shaped builds depend on were re-probed on **v0.21.0, 2026-09-09** and are marked | |---|---| -| user task, outcomes, targeting XPath (**both** the three-segment path form and the `[%UserRole_X%]` token form), targeting microflow, call workflow, notify workflow | **proven** | +| user task, outcomes, targeting XPath (**both** the three-segment path form and the `[%UserRole_X%]` token form), targeting microflow, call workflow, notify workflow | **proven**. Re-probed on **v0.21.0 / Mendix 11.14.0, 2026-09-09** — but see the two SIGNATURE rows immediately below, which are not about whether the construct writes and are how a clean `mxcli check` still yields a project that will not build | +| **a user task's `PAGE`** | **the page must take a `System.WorkflowUserTask` parameter, not the workflow's context entity.** A page parameterised on the context entity passes `mxcli check --references` completely clean, execs, and reads back from `DESCRIBE`; the native build then says *"The selected page 'X' should accept a parameter of type 'WorkflowUserTask', but expects parameters of types 'MOCProject' instead"*. Measured v0.21.0, 2026-09-09, on three tasks at once. The consequence for a plan: **the task pages are a prerequisite of the workflow row, not a later UI row** — build them first, or the workflow row cannot go green | +| **a targeting microflow's signature** | **it must accept TWO parameters — `System.Workflow` AND the context entity — in that order.** One parameter is the natural thing to write and it is wrong: *"should accept parameters of type 'System.Workflow' and 'MOC.MOCProject'. Instead the selected microflow expects 'MOC.MOCProject'"*. Again clean through `mxcli check`, caught only by the native build. Neither this signature nor the page's appears in `mxcli syntax workflow.user-task.targeting`, whose own example is `TARGETING MICROFLOW HR.GetApprovers` with no signature stated. Measured v0.21.0, 2026-09-09 | | **multi-user task** — the activity itself | **proven**. Undocumented in `mxcli syntax workflow`; works anyway | | **`JUMP TO `** inside a user-task outcome | **proven** | | **`WAIT FOR TIMER ''`** and **`WAIT FOR NOTIFICATION;`** | **proven**. Both undocumented; the notification takes **no name** — that is a Studio Pro property | @@ -308,6 +310,19 @@ row's stated floor is evidence against it. | **user-task `onCreatedEvent`** (the *On created* handler — the mechanism §6 names for assignment carried in data) | **not expressible in MDL, and this is grammar-level, not a docs gap.** The parser enumerates its own alternatives: after `PARAMETER` it accepts only `{BEGIN, EXPORT, DUE, OVERVIEW, DESCRIPTION, DISPLAY}`, and after a user task's `PAGE` only `;`. Four spellings probed (`ON CREATED CALL MICROFLOW`, `ON WORKFLOW EVENT`, task-level `ON CREATED`, `ONCREATEDEVENT`), all rejected at parse. **But it IS on the MCP write path** — `mxcli`'s `CreateWorkflow`/`UpdateWorkflow` payload carries `json:"onCreatedEvent"` on the *user-task* struct, beside `taskPage`, `outcomes` and `boundaryEvents`, and without `omitempty`. So it is a **per-task** property, not one workflow-level handler. End-to-end MCP write **not verified** — that needs a live Studio Pro. Treat as: hand-add, or MCP if you have Studio Pro up (`learned-mcp-patterns.md`) | | **AI agent task activity** | **unprobed** — its model rules (companion microflow first, outcomes mirror its return values, Boolean/Enum/Void only) hold whichever tool writes it | +**Re-probed on v0.21.0, 2026-09-09, WITH A KNOWN-BAD CONTROL.** Four constructs a real approval +chain needs — user task with a targeting microflow, multi-user task, `CALL MICROFLOW` with a quoted +`WITH`, and a backward `JUMP TO` inside an outcome — all build clean on v0.21.0 once the two +signature rows above are respected. That result is only worth having because the same run included +the construct known to be broken: a scripted `DECISION` passed `mxcli check`, reported +`Created workflow`, and left the project **unopenable** — +`Mendix.Modeler.Storage.StorageLoadException: One or more invalid values were detected while +loading the project`, then `BUILD FAILED`. So BUG-76 is open on v0.21.0, and the probe can +distinguish a working construct from a broken one. **A probe with no known-bad control cannot tell +"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). + **The one thing to take from this table.** `mxcli check` was **wrong on 5 of the 12 constructs probed**. Three of them passed `mxcli check --references`, passed `exec`, and read back correctly from `DESCRIBE WORKFLOW` — and were still broken under `mx check`, one of them From 4d9432c66cecd4c2ace4a9c54828f9529c6ecd28 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 04:22:46 +0000 Subject: [PATCH 6/8] page-fidelity: the same extraction defect, and one chrome class it could not reach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M --- CHANGELOG.md | 1 + project-bin/page-fidelity.js | 40 +++++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7071c1a..2178b2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Credit the person or project that surfaced the change — the credit line is the - learn(workflow-structure-rules, learned-workflow-patterns): **two workflow signatures that `mxcli check` passes clean and the native build rejects**, plus a v0.21.0 re-probe of the four constructs a real approval chain needs. (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`, execs, round-trips through `DESCRIBE`, and 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** — one parameter is the natural thing to write and it is wrong. Neither signature appears anywhere in `mxcli syntax workflow`, whose own targeting example is `TARGETING MICROFLOW HR.GetApprovers` with no signature stated. **The expensive half is the planning consequence: 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 one project's Phase 7 was ordered. Also: with the signatures right, user task + targeting microflow, **multi-user task**, `CALL MICROFLOW` with a quoted `WITH`, and a **backward `JUMP TO`** inside an outcome all build clean on v0.21.0. That result is only worth having because **the same run included the construct known to be broken as a control**: a scripted `DECISION` passed `mxcli check`, printed `Created workflow`, and left the project unopenable (`StorageLoadException` → `BUILD FAILED`), so BUG-76 is open on v0.21.0 and the probe demonstrably distinguishes working from broken. A probe with no known-bad control cannot tell *"it works"* from *"the probe is blind"* — this repo has already published one wrong conclusion for that exact reason (BUG-121) — Maurits Visser, from a MOC/PSSR app replacement - new(project-bin): **`check-design-reaches-app.sh` — did any of the design system ARRIVE in the built app?** The gap it closes is a sentence in its own sibling: `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"*, which is false and 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. Measured on a MOC/PSSR app replacement, 2026-09-09: 55 tokens sampled from the customer's own screenshots, 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**. Three independent failures, none visible to anything in this toolkit: the bridge assigned **SCSS variables** (`$brand-primary: var(--brand)`) to a theme running `$use-css-variables: true`, so it compiled to nothing (`--brand` present, `--brand-primary` untouched, `var(--brand)` **0 occurrences** in the built sheet); the **20 component classes were never ported at all**, so `ds-muted` on 12 live widgets resolved to the body colour; and 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 — every one of them reads the model or the authored stylesheet, and the defect lives between the authored stylesheet and the built one. **Found by a human opening a screenshot and asking why the UI was poor.** The check deliberately does NOT look for a known default hex (a cache of one framework version — authoring rule 5); it inverts the question to *"is the winning definition of each knob the framework declares pointed at a design-system token"*, which needs no defaults table and survives the next Atlas release. Four passes with denominators (knobs bound · tokens arrived · classes arrived · dead SCSS bridge) plus a WARN for classes the **model** asks for that the built sheet never defines — which found 17 of 23 on the same project, `ds-help` among them, class names typed from memory onto live widgets. Requires a build and exits **2** without one, because "did it reach the app" cannot be answered from source, and answering it from source is how this shipped — Maurits Visser, from a MOC/PSSR app replacement - fix(project-bin/snapshot-mpr.sh): **it now gitignores its own output.** It writes a FULL copy of the `.mpr` and every `mprcontents` unit before each exec and keeps the five newest, and 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**. 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 — Maurits Visser, from a MOC/PSSR app replacement +- fix(project-bin/page-fidelity.js): **the same body-extraction defect as `check-page-shell.sh`, and one annotation-chrome class it could not reach.** (1) The page body was matched by a regex ending at `\n\}` — a closing brace at **column 0** — which finds nothing at all for an indented page. Measured on a MOC/PSSR app replacement, 2026-09-09: two wizard pages whose bodies are 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 the shell check 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 — so 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%**. **Deliberately NOT added: 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 the tool's own *"every `wf-*` div is annotation chrome"* rule then ate. Teaching the tool a name that contradicts its own convention was the wrong half to change; the generator now emits the documented `.wf-wrap > .wf-screen` shape with the annotation as a **sibling**, and renames its page structure to `page-head` / `page-actions`. Both pages then score 100% — over a thin denominator, and the reason is worth knowing: this 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. So the action dimension is unusable here, `0/0` rather than wrong — Maurits Visser, from a MOC/PSSR app replacement - fix(project-bin/check-page-shell.sh): **it had never passed once on a project whose wireframes are named after the SOURCE screens**, and it had no fixture, which is why both defects below sat in it. (1) The wireframe path was hardcoded `design/wireframes/.html`; wireframes drawn from a legacy system are named after that system's screens (`moc-project-detail.html`) while Mendix pages are named in Mendix convention (`MOCProject_Detail`). Measured on a MOC/PSSR app replacement: 15 wireframes, 8 built pages, *"no wireframe"* for **every** page, exit 2 — a check that cannot find its input across a whole class of project is a check that gets switched off. Now an optional two-column `design/wireframes/PAGE-MAP.tsv`; absent map, unchanged behaviour; a page the map does **not** name is still reported **naming the map**, so the map cannot hide a page by omission; and a row pointing at a missing wireframe is reported, because a stale row is how a rename turns into a page nobody checks. (2) The page body was bounded *"to the closing brace at column 0"*, which reads the rest of the **file** for a page whose body is on one line — a nav-shell script with five such stubs reported *"page MOCProject_Overview declares 5 H1 titles; its wireframe draws one"*, and every number was the count for the whole file. **The worse kind of false positive: it fires on correct code, cites a real rule, and the fix it asks for would break four pages.** Two line-based repairs were tried and each broke the other case (a `Params: { $X: Mod.Entity }` brace closing on its own line ends a "started on any brace" scan before the body opens; a "started only when depth > 0" scan never starts on a one-line body), so the extractor now counts **characters** and tracks parens. New fixture `tests/wave2/test-page-shell-body.sh`, **18 assertions**, pinning both cases in one file because that is the pair that must hold at once, plus that the rules still FIRE on a real `Class: 'h1'` title and on two H1s in one body. What it found once it could run: **9 title widgets across 4 page scripts using `Class: 'h1'` instead of `RenderMode: H1`** — which is the same defect the screenshots showed, because `Class: 'h1'` leaves a DYNAMICTEXT an inline `` and the caption rendered on the same line as the heading — Maurits Visser, from a MOC/PSSR app replacement - new(obligations): **`design-reaches-app` — the second obligation below stage 5**, `mdl`/project/`from-stage 3`, artifact `docs/design-reaches-app.md` with a denominator, degradable. Owed by `mdl` rather than `review` deliberately: the agent that performed the port is the one that must show it landed. The reason the defect above escaped is not that the check was hard — it is that **nobody was owed an answer**, so every gate it passed through was green-by-absence. A project with no build yet discharges it by saying so (*"binding UNPROVEN, no build"*); silence does not — Maurits Visser, from a MOC/PSSR app replacement - fix(project-bin/check-design-portability.sh): the falsified premise is **corrected in the header 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 — Maurits Visser, from a MOC/PSSR app replacement diff --git a/project-bin/page-fidelity.js b/project-bin/page-fidelity.js index a6f6ee7..cae1d27 100755 --- a/project-bin/page-fidelity.js +++ b/project-bin/page-fidelity.js @@ -164,6 +164,12 @@ function contentOf(html) { s = dropBalanced(s, /<(div)[^>]*class="[^"]*\banno-wrap\b[^"]*"/i); s = dropBalanced(s, /<(div)[^>]*class="[^"]*\brail-note\b[^"]*"/i); s = dropBalanced(s, /<(div)[^>]*class="[^"]*\bannot\b[^"]*"/i); + // `.anno` — NOT reached by the `annot` pattern above, because \b after "anno" needs a + // non-word character and "t" is one. Measured 2026-09-09 on a MOC/PSSR app + // replacement, whose annotation block is `div.anno`: every page 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%. + s = dropBalanced(s, /<(div)[^>]*class="[^"]*\banno\b[^"]*"/i); s = dropBalanced(s, /<(table)[^>]*class="[^"]*\bbind\b[^"]*"/i); // A "this screen is descoped/annotation-only" banner is chrome, not page copy. s = dropBalanced(s, /<(div)[^>]*class="[^"]*alert[^"]*"(?=[\s\S]{0,400}?DESCOPED)/i); @@ -292,14 +298,42 @@ function pageMdl() { let out = ''; for (const f of MDLS) { const src = f === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(f, 'utf8'); - // Case-insensitive; page name quoted or bare; body up to a closing brace at col 0. const re = new RegExp( - 'CREATE(\\s+OR\\s+(MODIFY|REPLACE))?\\s+PAGE\\s+"?[A-Za-z0-9_]+"?\\."?' + PAGE + '"?\\b[\\s\\S]*?\\n\\}', 'gi'); - for (const m of src.matchAll(re)) out += m[0] + '\n'; + 'CREATE(\\s+OR\\s+(MODIFY|REPLACE))?\\s+PAGE\\s+"?[A-Za-z0-9_]+"?\\."?' + PAGE + '"?\\b', 'gi'); + for (const m of src.matchAll(re)) out += pageBody(src, m.index) + '\n'; } return out; } +// The page body, from its declaration to the brace that MATCHES the body's opening +// one, counted per character with parens tracked. +// +// It used to be a regex ending at '\n\}' — a closing brace at COLUMN 0 — which finds +// nothing at all for an indented page. Measured 2026-09-09 on a MOC/PSSR app +// replacement: two wizard pages whose bodies are wrapped in a LAYOUTGRID (which +// mxcli's own MPR010 advises for any DataView holding inputs) close at two spaces of +// indent, so the scan matched nothing and the tool exited 2 with 'no declaration of +// page "X" found in input' for a page that is plainly there. A project that follows +// that advice could not score any page. +// +// The parens matter because a multi-line declaration carries +// `Params: { $X: Mod.Entity }`, which opens and closes a brace before the body does — +// the identical pair of cases that took two wrong attempts in check-page-shell.sh, +// whose header records them. +function pageBody(src, from) { + let pd = 0, bd = 0, started = false; + for (let i = from; i < src.length; i++) { + const c = src[i]; + if (!started) { + if (c === '(') pd++; + else if (c === ')') { if (pd > 0) pd--; } + else if (c === '{' && pd === 0) { started = true; bd = 1; } + } else if (c === '{') bd++; + else if (c === '}') { bd--; if (bd <= 0) return src.slice(from, i + 1); } + } + return src.slice(from); +} + function score(wf, mdl) { const corpus = norm(mdl); const cls = new Set(); From 9372a4b18ed38d739f1c3ddccfa367db396de0fd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 07:33:18 +0000 Subject: [PATCH 7/8] page-fidelity: structure is not bound data, and null% again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.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

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 Claude-Session: https://claude.ai/code/session_01Jj5uvTmmcBQT8Uu3asm21M --- CHANGELOG.md | 1 + project-bin/page-fidelity.js | 30 ++++-- .../fixtures/page-fidelity-mocks/CAPTURE.md | 40 +++++++ .../page-fidelity-mocks/demo-list.mdl | 19 ++++ .../page-fidelity-mocks/demo-overview.mdl | 37 +++++++ .../page-fidelity-mocks/grouped-overview.html | 46 ++++++++ .../page-fidelity-mocks/mocked-list.html | 22 ++++ tests/wave2/test-page-fidelity-mocks.sh | 102 ++++++++++++++++++ 8 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 tests/wave2/fixtures/page-fidelity-mocks/CAPTURE.md create mode 100644 tests/wave2/fixtures/page-fidelity-mocks/demo-list.mdl create mode 100644 tests/wave2/fixtures/page-fidelity-mocks/demo-overview.mdl create mode 100644 tests/wave2/fixtures/page-fidelity-mocks/grouped-overview.html create mode 100644 tests/wave2/fixtures/page-fidelity-mocks/mocked-list.html create mode 100755 tests/wave2/test-page-fidelity-mocks.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 2178b2d..bd1f82f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Kinds: `new` · `fix` · `learn` (a skill/learning) · `process` (rules, templat Credit the person or project that surfaced the change — the credit line is the thank-you. ## 2026-09-09 +- fix(project-bin/page-fidelity.js): **structure classed as bound data, and the score of record silently going `null%` again.** A wireframe's own ` +
+
+ +

Demo

Records across all sites

+
+
+ + +
+
SITE-1
+
+ + + +
Record (start – end)DepartmentTrack ATrack B
864 · Environmental system improvementSustainabilityAwaiting assessmentNot started
861 · Reclaimer refurbishmentMaintenanceApprovedIn review
+
12
+
SITE-2
+
+ + +
Record (start – end)DepartmentTrack ATrack B
863 · Environmental management updateSustainabilityAwaiting assessmentNot started
+
12
+
+
+

Annotation

+

Purpose. Every record the user may see, grouped by site, each group paged independently.

+
+
diff --git a/tests/wave2/fixtures/page-fidelity-mocks/mocked-list.html b/tests/wave2/fixtures/page-fidelity-mocks/mocked-list.html new file mode 100644 index 0000000..835a1f9 --- /dev/null +++ b/tests/wave2/fixtures/page-fidelity-mocks/mocked-list.html @@ -0,0 +1,22 @@ +Demo list — Wireframe + +
+
+

Approvals

+
+
864 · Environmental system improvementA. Reviewer
+
861 · Reclaimer refurbishmentB. Approver
+
860 · Screw conveyor replacementC. Director
+
+
+

Annotation

Purpose. The approver's queue.

+
diff --git a/tests/wave2/test-page-fidelity-mocks.sh b/tests/wave2/test-page-fidelity-mocks.sh new file mode 100755 index 0000000..141a185 --- /dev/null +++ b/tests/wave2/test-page-fidelity-mocks.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# test-page-fidelity-mocks.sh — pin the mock-versus-structure rule in page-fidelity.js. +# +# page-fidelity.js is the SCORE OF RECORD: every run appends a row to the project's +# docs/PAGE-FIDELITY.tsv, and the first non-stub row per page is the first-build number a +# ≥80% gate is read off. It had no fixture, which is how the defect below survived. +# +# THE DEFECT. A wireframe's own