From f0295a3d1a4bca7f8a76bd29ec839b646ca96f4a Mon Sep 17 00:00:00 2001 From: MendixMau Date: Fri, 16 Oct 2026 16:41:41 +0800 Subject: [PATCH 1/5] new(process-board): Stage 3 board that pins each question to its process step One swimlane board per business process. Each step sits in its role lane, shows its wireframe and what the system does, and carries a pin for every open question and ASSUMED decision about that moment, so a reviewer can answer a question where it happens instead of in a flat list. - skills/process-board.md: when, model authoring, anchoring, build, verify by looking - project-bin/process-board.js + template: validate, anchor (derived plus judged by fingerprint, global counted), build (self-contained, light and dark, deterministic) - Stage 3 Agent produces and Surface rows, process-board manifest row, swimlanes opt-in discharged by the model, CAC-4 line, routing row, install pair, check_stage_3 FAIL on a missing or stale board - tests/wave2/test-process-board.sh on a synthetic order approval fixture Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + README.md | 1 + ROUTING.md | 1 + agents/architect-agent.md | 1 + agents/review-agent.md | 1 + bin/gate-check.sh | 20 +- bin/lib/artifact-manifest.tsv | 3 +- bin/lib/install-manifest.sh | 6 +- bin/lib/skill-routing.tsv | 1 + project-bin/process-board.js | 484 ++++++++++++++++ project-bin/process-board.template.html | 516 ++++++++++++++++++ skills/checkpoints/checkpoint-design.md | 3 + skills/conversion-runbook.md | 4 +- skills/process-board.md | 151 +++++ tests/wave2/fixtures/process-board/PROJECT.md | 16 + .../fixtures/process-board/design/ds.css | 3 + .../process-board/design/process-model.json | 29 + .../design/question-anchors.json | 7 + .../design/wireframes/Order_Approve.html | 9 + .../design/wireframes/Order_NewEdit.html | 9 + .../design/wireframes/Order_Overview.html | 9 + .../design/wireframes/Settings_Edit.html | 9 + tests/wave2/test-process-board.sh | 166 ++++++ 23 files changed, 1445 insertions(+), 5 deletions(-) create mode 100644 project-bin/process-board.js create mode 100644 project-bin/process-board.template.html create mode 100644 skills/process-board.md create mode 100644 tests/wave2/fixtures/process-board/PROJECT.md create mode 100644 tests/wave2/fixtures/process-board/design/ds.css create mode 100644 tests/wave2/fixtures/process-board/design/process-model.json create mode 100644 tests/wave2/fixtures/process-board/design/question-anchors.json create mode 100644 tests/wave2/fixtures/process-board/design/wireframes/Order_Approve.html create mode 100644 tests/wave2/fixtures/process-board/design/wireframes/Order_NewEdit.html create mode 100644 tests/wave2/fixtures/process-board/design/wireframes/Order_Overview.html create mode 100644 tests/wave2/fixtures/process-board/design/wireframes/Settings_Edit.html create mode 100755 tests/wave2/test-process-board.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aa535dc..d3bff4c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- new(process-board): **Stage 3 process board: one swimlane board per business process, each step on its wireframe, every open question and ASSUMED decision pinned to the step it is about.** `skills/process-board.md` (when, model, anchoring, build, verify by looking) and `project-bin/process-board.js` (`validate`, `anchor`, `build`; Node stdlib, deterministic) with its template. `validate` fails a step page with no wireframe, a use case on no process and not standalone, a decision with one outcome, two steps in one cell. `anchor` places items mechanically (named microflow, listed or named use case, named screen, overlapping source lines) and keeps judged placements by fingerprint, so a reworded question is unplaced again instead of silently wrong; global buckets are allowed and counted. Wired into the Stage 3 Agent produces and Surface rows, a `process-board` manifest row (report), the `swimlanes` opt-in row (the model discharges it), CAC-4, and a check_stage_3 FAIL when the board is older than its model or anchors. `tests/wave2/test-process-board.sh` on a synthetic order approval fixture. Field evidence: a requirements-driven rebuild POC, 2026-10-16, where the reviewer could not tell where the sixty questions fit; the board placed 102 items with 0 unplaced. MendixMau - learn(skills): **five lab findings land as compact rows, each paid for by trimming prose in the same files, so always-loaded words do not grow (baseline 75,225 to 75,204, budget 80,000).** Enum value `New` and `AutoCreatedDate`/`AutoOwner` (detection gaps), `[%CurrentDateTime%]` not `currentDateTime()`, expression return types (Decimal from date diffs and `div`, absolute `secondsBetween`, `formatDecimal` on pages), CE2729 from the default home page and page microflow grants, Atlas selector carriers for list views, page body and timelines, `-small` spacing suffixes do not exist, `display: contents` rows paint nothing, CSS pseudo text is invisible to `innerText` — lab field runs, Refs #276 (TK-020, TK-021, TK-022, TK-024) - fix(design-check): **a design system whose palette lives only in scoped `:root.` blocks no longer reads as "0 of N tokens carry the design's value".** Pass 2b compared every token against a bare `:root` of the built sheet, so a themed-only palette always warned, a false warning that teaches agents to ignore the check. A token the design declares only in scoped blocks is now compared block for block against the same-named block in the built sheet, and a wrong value is still named with its block (knob binding stays bare-only: a knob bound inside a theme the app is not wearing is not bound). Synthetic fixtures `ds-themed.css`, `built-themed.css`, `built-themed-wrong.css`; 5 new cases in `test-design-reaches-app.sh` (50 total) — TK-015 part (b) - fix(tests): **`test-exec-hash-verify.sh` case K passes on macOS.** `$TMPDIR` ends in `/`, so the bare `mktemp` path held `//` and the exact `MXPATCH_BACKUP_DIR` path assert could not match; `WORK` is now `cd`-ed into and read back with `pwd -P`. Test only, no script change. 42 pass / 1 fail before, 43 / 0 after — #384 review diff --git a/README.md b/README.md index 6b5bf96f..219d3770 100644 --- a/README.md +++ b/README.md @@ -595,6 +595,7 @@ Every mxcli project has a `.ai-context/skills/` directory (bundled by `mxcli ini | After every wireframe edit: assembles design/wireframes/*.html into design/prototype.html, one hash-routed page a stakeholder can click through instead of twenty separate files. Generated, never edited (design-artifacts.md Step 3) | `project-bin/assemble-prototype.js` | | Before wireframes pass to the build loop, and with --brd before a BRD is signed off: dead #/route links, orphan screens, controls with no data-bind and no data-cut, BRD routes no screen has, screens no use case walks (design-artifacts.md Step 3c, brd-validation.md check 8) | `project-bin/check-prototype-links.js` | | Designing the brand and ONE ANNOTATED WIREFRAME PER SCREEN before building pages — the design system alone is half the deliverable | `skills/design-artifacts.md` | +| After the wireframes, when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane board per process, each step on its wireframe, every question pinned to a step (project-bin/process-board.js validate, anchor, build) | `skills/process-board.md` | | Stage 3, after the role model is CONFIRMED and BEFORE THE FIRST WIREFRAME — and any time a page shows ONE object, a menu item or home page is added, or a data source retrieves the current/first record: role homes, inboxes, and per page how it is reached and which object it takes as parameter | `skills/interaction-model.md` | | Before a wireframe or a design commits to a WIDGET, when a requirement shows a widget to one role and not another, and when a page script hits a parse error that looks like a syntax mistake: the short list of things MDL cannot write at all, and the four-minute probe that answers it at Stage 3 instead of at build time | `skills/learned-mdl-cannot-express.md` | diff --git a/ROUTING.md b/ROUTING.md index 5b110dd8..a4e2cf95 100644 --- a/ROUTING.md +++ b/ROUTING.md @@ -131,6 +131,7 @@ picks the row up. That is the whole procedure — there is no second list to rem | After every wireframe edit: assembles design/wireframes/*.html into design/prototype.html, one hash-routed page a stakeholder can click through instead of twenty separate files. Generated, never edited (design-artifacts.md Step 3) | `project-bin/assemble-prototype.js` | architect,review | 3 | ondemand | | Before wireframes pass to the build loop, and with --brd before a BRD is signed off: dead #/route links, orphan screens, controls with no data-bind and no data-cut, BRD routes no screen has, screens no use case walks (design-artifacts.md Step 3c, brd-validation.md check 8) | `project-bin/check-prototype-links.js` | architect,review | 3 | ondemand | | 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 | +| After the wireframes, when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane board per process, each step on its wireframe, every question pinned to a step (project-bin/process-board.js validate, anchor, build) | `skills/process-board.md` | architect,review | 3 | ondemand | | Stage 3, after the role model is CONFIRMED and BEFORE THE FIRST WIREFRAME — and any time a page shows ONE object, a menu item or home page is added, or a data source retrieves the current/first record: role homes, inboxes, and per page how it is reached and which object it takes as parameter | `skills/interaction-model.md` | lead,ba,architect | 3 | ondemand | | Before a wireframe or a design commits to a WIDGET, when a requirement shows a widget to one role and not another, and when a page script hits a parse error that looks like a syntax mistake: the short list of things MDL cannot write at all, and the four-minute probe that answers it at Stage 3 instead of at build time | `skills/learned-mdl-cannot-express.md` | architect,mdl | 3,5 | ondemand | | A CSS/SCSS or style change that appears to have done nothing (a colour or class that won't show), a focus ring or outline that paints on every page landing or load, or an app still grey after a design port every instrument called green — the three ways a correct rule paints nothing (matches nothing / matches chrome / loses the cascade), the two reads that tell them apart, and the class that arrived in the stylesheet and is bound to no widget | `skills/learned-css-that-never-applied.md` | mdl?,review,gate | 5,6 | baseline | diff --git a/agents/architect-agent.md b/agents/architect-agent.md index 576772fe..127a89a1 100644 --- a/agents/architect-agent.md +++ b/agents/architect-agent.md @@ -68,6 +68,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/architecture-blueprint.md` | Diagramming target architecture | | `skills/modularize-domain.md` | Deciding module boundaries before "create module" | | `skills/design-artifacts.md` | Designing the brand and ONE ANNOTATED WIREFRAME PER SCREEN before building pages | +| `skills/process-board.md` | After the wireframes, when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane board per process, each step on its wireframe, every question pinned to a step (project-bin/process-board.js validate, anchor, build) | | `skills/interaction-model.md` | Stage 3, after the role model is CONFIRMED and BEFORE THE FIRST WIREFRAME | | `skills/brd-to-build-plan.md` | Turning BRDs plus architecture into a numbered, dependency-ordered build plan | | `skills/coverage-ledger.md` | Building the Stage 4 coverage ledger | diff --git a/agents/review-agent.md b/agents/review-agent.md index f8590697..f4335750 100644 --- a/agents/review-agent.md +++ b/agents/review-agent.md @@ -64,6 +64,7 @@ This table is your whole list. The baseline table in the project's CLAUDE.local. | `skills/app-analysis.md` | Building or refreshing the standing dossier of an EXISTING app before changing it: inventory, module dependency shape, loop risk patterns, each section with a verdict and a fault where nothing was measured (a standing document; nothing else in the toolkit reads it yet) | | `skills/module-dependency-review.md` | Judging module tangles, bidirectional pairs, cohesion and hubs from analysis/app-facts/dependencies.json; stock graph_module_* views split names on the first dot and are not trusted | | `skills/microflow-loop-antipatterns.md` | Reading what loop bodies do (LOOP_TQ, deferred commit, nested loop, REST in loop, transaction control per item, scheduled-event reachability) from described MDL; the catalog holds top-level activities only and cannot see inside a loop | +| `skills/process-board.md` | After the wireframes, when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane board per process, each step on its wireframe, every question pinned to a step (project-bin/process-board.js validate, anchor, build) | | `project-bin/conformance-check.sh` | Running the ledger rung alone | | `project-bin/graph-sweep.sh` | Running the wiring rung alone | | `skills/fixture-seeding.md` | Seed data, test data, demo data or fixtures are needed, or pages come up empty | diff --git a/bin/gate-check.sh b/bin/gate-check.sh index 233dc15e..aa6c68e4 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -1159,6 +1159,24 @@ check_stage_3() { return fi done + # The process board (skills/process-board.md) is optional, but once its model exists the + # render must be current: a reviewer signs off on what the board shows, so a board older than + # its model or its anchors shows the wrong process. Same rule as blueprint.html above. + local pb_model pb_html pb_src + pb_model="$(resolve_artifact_nonempty "design/process-model.json")" + if [ -n "$pb_model" ]; then + pb_html="$(dirname "$pb_model")/process-board.html" + if [ ! -s "$pb_html" ]; then + echo "FAIL|design/process-model.json exists but design/process-board.html does not; render it (process-board.md Step 4: process-board.js build)" + return + fi + for pb_src in "$pb_model" "$(dirname "$pb_model")/question-anchors.json"; do + if [ -f "$pb_src" ] && [ "$pb_html" -ot "$pb_src" ]; then + echo "FAIL|design/process-board.html is older than $(basename "$pb_src"): stale render at a sign-off gate; regenerate (process-board.md Step 4)" + return + fi + done + fi if reg_unavailable; then reg_unavailable_note; return; fi if has_confirmed_decision 3; then echo "PASS|fit-gap, blueprint render, design system, wireframes present and a Stage-3 CONFIRMED decision is in $REGISTER" @@ -1769,7 +1787,7 @@ stage_protocol_paths() { 0) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-scope.md skills/source-triage.md skills/image-transcription.md skills/small-project-tier.md skills/existing-app-change.md skills/decompose-app.md skills/app-analysis.md skills/layering-review.md skills/module-dependency-review.md skills/microflow-loop-antipatterns.md skills/assess-migration.md skills/migration-pipeline.md skills/rehost-app.md skills/migrate-general.md skills/migrate-outsystems.md skills/source-os11.md skills/os-xml-schema.md skills/source-node-express-react.md skills/document-discovery.md skills/extractor-quality-loop.md skills/qa-loop-goal-pattern.md skills/mendix-epics-api.md skills/corpus-extraction-integrity.md skills/gate-check-file-locations.md" ;; 1) echo "skills/interview-protocol.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-extraction.md skills/dispatching.md skills/image-transcription.md skills/small-project-tier.md skills/migration-pipeline.md skills/rehost-app.md skills/source-os11.md skills/os-xml-schema.md skills/source-node-express-react.md skills/document-discovery.md skills/extractor-quality-loop.md skills/kb-generation.md skills/corpus-extraction-integrity.md" ;; 2) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-brd.md skills/checkpoints/checkpoint-architecture.md skills/dispatching.md skills/image-transcription.md skills/small-project-tier.md skills/kb-generation.md skills/brd-generation.md skills/brd-validation.md" ;; - 3) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-design.md skills/dispatching.md skills/small-project-tier.md skills/mendix-best-practices-index.md skills/decompose-app.md skills/mendix-service-seam.md skills/layering-review.md skills/architecture-blueprint.md skills/modularize-domain.md skills/design-artifacts.md skills/interaction-model.md skills/brd-to-build-plan.md skills/workflow-structure-rules.md skills/rest-between-mendix-apps.md skills/learned-mdl-cannot-express.md" ;; + 3) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-design.md skills/dispatching.md skills/small-project-tier.md skills/mendix-best-practices-index.md skills/decompose-app.md skills/mendix-service-seam.md skills/layering-review.md skills/architecture-blueprint.md skills/modularize-domain.md skills/design-artifacts.md skills/process-board.md skills/interaction-model.md skills/brd-to-build-plan.md skills/workflow-structure-rules.md skills/rest-between-mendix-apps.md skills/learned-mdl-cannot-express.md" ;; 4) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-build.md skills/agent-roles.md skills/dispatching.md skills/small-project-tier.md skills/module-brief.md skills/module-folder-convention.md skills/brd-to-build-plan.md skills/coverage-ledger.md skills/workflow-structure-rules.md skills/rest-integration-first-time-right.md skills/rest-between-mendix-apps.md skills/learned-constants-and-secrets.md" ;; 5|build-ready) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/agent-roles.md skills/dispatching.md skills/module-brief.md skills/learned-mdl-preflight.md skills/module-folder-convention.md skills/learned-microflow-patterns.md skills/microflow-preflight.md skills/mendix-best-practices-index.md skills/ui-preflight-pages.md skills/design-spacing.md skills/learned-stylegallery.md skills/ui-loop.md skills/module-review.md skills/testing-shape.md skills/decompose-app.md skills/mendix-service-seam.md skills/microflow-loop-antipatterns.md skills/rehost-app.md skills/iterative-build-loop.md skills/mdl-cookbook-microflows.md skills/build/mdl/oneshot-mdl-method.md skills/learned-page-patterns.md skills/oneshot-page-structure-patterns.md skills/mendix-agents.md skills/mendix-agent-ui.md skills/mendix-agent-setup.md skills/fixture-seeding.md skills/journey-proof.md skills/unhappy-path-testing.md skills/monkey-test.md skills/e2e-locators-that-lie.md skills/report-schema.md skills/harness-architecture.md skills/process-coherence-pass.md skills/lint-that-actually-runs.md skills/improvement-register.md skills/journey-examples.md skills/wiring-sweep.md skills/learned-workflow-patterns.md skills/workflow-structure-rules.md skills/rest-integration-first-time-right.md skills/rest-between-mendix-apps.md skills/bug-submission-checklist.md skills/empty-widget-triage.md skills/learned-sidebar-collapse-icons.md skills/learned-popup-navigation.md skills/learned-datagrid-customcontent-binding.md skills/learned-popup-feedback-pattern.md skills/learned-mdl-cannot-express.md skills/learned-css-that-never-applied.md skills/learned-detection-gaps.md skills/learned-dg2-patterns.md skills/learned-file-upload-widget.md skills/security-is-not-a-later-script.md skills/learned-local-db-confusion.md skills/full-harness-audit.md skills/test-result-audit.md skills/finding-disposition.md skills/preview-over-hub-tunnel.md skills/walking-skeleton.md skills/platform-link.md skills/teamserver-alignment.md skills/learned-constants-and-secrets.md" ;; 6) echo "skills/interview-protocol.md skills/change-intake.md skills/change-intake-detail.md skills/grill-mode.md skills/checkpoints/checkpoint-template.md skills/checkpoints/checkpoint-cutover.md skills/dispatching.md skills/mendix-best-practices-index.md skills/module-review.md skills/testing-shape.md skills/existing-app-assurance.md skills/decompose-app.md skills/app-analysis.md skills/module-dependency-review.md skills/microflow-loop-antipatterns.md skills/rehost-app.md skills/qa-loop-goal-pattern.md skills/mendix-agent-setup.md skills/e2e-harness-base.md skills/learned-db-assertions.md skills/fixture-seeding.md skills/journey-proof.md skills/unhappy-path-testing.md skills/monkey-test.md skills/e2e-locators-that-lie.md skills/learned-skill-ux-audit.md skills/learned-skill-scope-delta.md skills/report-schema.md skills/harness-architecture.md skills/process-coherence-pass.md skills/e2e-evidence-report.md skills/record-demo-video.md skills/share-demo-package.md skills/lint-that-actually-runs.md skills/improvement-register.md skills/journey-examples.md skills/wiring-sweep.md skills/workflow-structure-rules.md skills/rest-between-mendix-apps.md skills/bug-submission-checklist.md skills/empty-widget-triage.md skills/anonymize-client-app-for-demo.md skills/learned-css-that-never-applied.md skills/learned-detection-gaps.md skills/learned-local-db-confusion.md skills/full-harness-audit.md skills/test-result-audit.md skills/finding-disposition.md skills/handoff-to-studio-pro.md skills/preview-over-hub-tunnel.md skills/platform-link.md skills/teamserver-alignment.md skills/learned-constants-and-secrets.md" ;; diff --git a/bin/lib/artifact-manifest.tsv b/bin/lib/artifact-manifest.tsv index 93c912ab..ed77a71d 100644 --- a/bin/lib/artifact-manifest.tsv +++ b/bin/lib/artifact-manifest.tsv @@ -86,6 +86,7 @@ module-design 3 architecture/module-design.html|module-design.html skills/modula fit-gap 3 architecture/fit-gap.md skills/architecture-blueprint.md Step 4 bin/gate-check.sh:912 (check_stage_3), bin/gate-check.sh:939 (blueprint.html staleness input) migration,requirements,existing-app-change gate blueprint-md 3 architecture/blueprint.md skills/architecture-blueprint.md Steps 2-6 — the markdown is the source of truth bin/gate-check.sh:939 (staleness input), skills/architecture-blueprint.md:54 (render source for blueprint.html) migration,requirements,existing-app-change report blueprint-html 3 architecture/blueprint.html skills/architecture-blueprint.md Step 7 (generated render, never hand-edited) bin/gate-check.sh:931-943 (check_stage_3 presence + freshness) migration,requirements,existing-app-change gate +process-board 3 design/process-board.html skills/process-board.md Step 4 (project-bin/process-board.js build; generated from design/process-model.json, never hand-edited) bin/gate-check.sh:1166-1177 (check_stage_3 freshness against design/process-model.json and its anchors), skills/checkpoints/checkpoint-design.md:16 (CAC-4 walks the questions on the board) migration,requirements,existing-app-change report design-system 3 design/design-system.html skills/design-artifacts.md:79 (annotated showcase linking ds.css) bin/gate-check.sh:913 (check_stage_3), bin/gate-check.sh:1135 (build-ready) migration,requirements,existing-app-change gate ds-css 3 design/ds.css skills/design-artifacts.md:75 (the physical token/component source) skills/design-artifacts.md:68 (two-files rule — every wireframe links it), bin/gate-check.sh:1135 (build-ready accepts either file) migration,requirements,existing-app-change report wireframes 3 design/wireframes/*.html skills/design-artifacts.md Step 3, one per screen bin/gate-check.sh:923 (check_stage_3), bin/gate-check.sh:1129 (build-ready), skills/ui-preflight-pages.md (mdl-agent pre-flight input) migration,requirements,existing-app-change gate @@ -109,7 +110,7 @@ verification-html 6 docs/verification/report.html|test-report.html|reports/test- # alternative is the name a real project already used (workflow-design: a field project's # architecture/workflow-definition.md, written unprompted and never tracked by anything). workflow-design 3 architecture/workflow-definition.md|architecture/workflow-design.md|architecture/workflows/*.md skills/architecture-blueprint.md Step 3b (state diagram per process: states, transitions, who moves it, SLA) + skills/learned-workflow-patterns.md for the native-Workflow shape skills/checkpoints/checkpoint-architecture.md Q3 (asks whether the process is Workflow or enum-driven — either way this is where it is drawn), skills/learned-workflow-patterns.md (the build-time reader), bin/lib/closeout.sh (offers it at the Stage-3 open, lists it at the close) migration,requirements,greenfield,existing-app-change optin -swimlanes 3 architecture/swimlanes.md|architecture/swimlane-*.md|architecture/diagrams/swimlane*.md Mermaid per business process, one lane per role from the Stage-3 role model, one section per process; the cross-persona journey diagrams of skills/architecture-blueprint.md Step 3d are the multi-persona case of the same picture skills/checkpoints/checkpoint-architecture.md (the optional-artifacts question), skills/architecture-blueprint.md Step 3d (journey list it feeds), skills/journey-proof.md (the journeys a swimlane names are the ones Stage 5–6 must prove), bin/lib/closeout.sh migration,requirements,greenfield,existing-app-change optin +swimlanes 3 architecture/swimlanes.md|architecture/swimlane-*.md|architecture/diagrams/swimlane*.md|design/process-model.json Mermaid per business process, one lane per role from the Stage-3 role model, one section per process; the cross-persona journey diagrams of skills/architecture-blueprint.md Step 3d are the multi-persona case of the same picture; or the process board model of skills/process-board.md Step 2, which draws the same lanes and adds the screens and questions skills/checkpoints/checkpoint-architecture.md (the optional-artifacts question), skills/architecture-blueprint.md Step 3d (journey list it feeds), skills/journey-proof.md (the journeys a swimlane names are the ones Stage 5–6 must prove), bin/lib/closeout.sh migration,requirements,greenfield,existing-app-change optin sequence-diagrams 3 architecture/sequence-diagrams.md|architecture/sequence-*.md|architecture/diagrams/sequence*.md Mermaid sequenceDiagram per integration or agent call — caller, Mendix microflow, external system, sync/async, timeout, fallback; the agent-wiring diagram of skills/architecture-blueprint.md Step 3c is one instance skills/checkpoints/checkpoint-architecture.md Q3 agent-wiring half, skills/rest-integration-first-time-right.md (build-time reader for the REST ones), skills/mendix-agents.md, bin/lib/closeout.sh migration,requirements,greenfield,existing-app-change optin erd 3 architecture/erd.md|architecture/domain-model.md|architecture/diagrams/erd*.md Mermaid erDiagram of the TARGET domain across modules — entities, associations, ownership per module; the blueprint's layer diagram shows modules, not entities, and .mx-brd.json shows one module at a time skills/checkpoints/checkpoint-architecture.md Q2 (cross-module data strategy — the picture that question is about), skills/modularize-domain.md, bin/lib/closeout.sh migration,requirements,greenfield,existing-app-change optin nfr-sheet 3 architecture/nfr.md|architecture/nfrs.md|architecture/non-functional-requirements.md One table: NFR, target, how measured, where in the model it is enforced (XPath, index, batch size, timeout) — the per-NFR expansion of the list CAC-3's open question writes to project-profile.md skills/checkpoints/checkpoint-architecture.md open question (the list), skills/architecture-blueprint.md Step 5 (the register that must not lose them), bin/lib/closeout.sh migration,requirements,greenfield,existing-app-change optin diff --git a/bin/lib/install-manifest.sh b/bin/lib/install-manifest.sh index f8003662..e815b5cd 100644 --- a/bin/lib/install-manifest.sh +++ b/bin/lib/install-manifest.sh @@ -115,7 +115,11 @@ MXTK_AGENTS_STAGE_BUILD="mdl-agent.md gate-agent.md test-agent.md review-agent.m # may run itself (stops only the pid run --local recorded for THIS project), and `mxcli test` on a # scratch copy under the test-port lock. exec.sh names app-restart.sh in its watch advisory, so # it has to be installed beside it. -MXTK_PROJECT_BIN="app-restart.sh test-scratch.sh assemble-prototype.js prototype-route.js check-prototype-links.js wf-add-path-terminators.py wf-set-call-captions.py mxunit_bson.py mxpatch.py _common.sh app-facts.sh _claims.sh snapshot-mpr.sh restore-mpr.sh exec.sh affected-tests.sh look-ledger.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 lint-trend.sh metrics-ledger.sh metrics-trend.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 model-stamp.sh verify-model.sh install-project-hooks.sh mx-metadata-note.py session-check.sh constants-audit.sh context-pack.sh" +# process-board.js + process-board.template.html joined on 2026-10-16 and install as a PAIR: +# the generator reads the template from its own directory. The project runs it at Stage 3 +# (skills/process-board.md Step 4) after every change to design/process-model.json, the +# anchors, a wireframe or a BRD, and gate-check.sh check_stage_3 fails a stale render. +MXTK_PROJECT_BIN="process-board.js process-board.template.html app-restart.sh test-scratch.sh assemble-prototype.js prototype-route.js check-prototype-links.js wf-add-path-terminators.py wf-set-call-captions.py mxunit_bson.py mxpatch.py _common.sh app-facts.sh _claims.sh snapshot-mpr.sh restore-mpr.sh exec.sh affected-tests.sh look-ledger.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 lint-trend.sh metrics-ledger.sh metrics-trend.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 model-stamp.sh verify-model.sh install-project-hooks.sh mx-metadata-note.py session-check.sh constants-audit.sh context-pack.sh" # 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/skill-routing.tsv b/bin/lib/skill-routing.tsv index c425d8e9..e87d5271 100644 --- a/bin/lib/skill-routing.tsv +++ b/bin/lib/skill-routing.tsv @@ -152,6 +152,7 @@ brd-validation skills/brd-validation.md Validating BRDs against the code and doc architecture-blueprint skills/architecture-blueprint.md Diagramming target architecture — module defs, wiring, fit-gap, marketplace, security, NFRs, integrations architect 3 ondemand architecture modularize-domain skills/modularize-domain.md Deciding module boundaries before "create module" architect 3 ondemand architecture design-artifacts skills/design-artifacts.md Designing the brand and ONE ANNOTATED WIREFRAME PER SCREEN before building pages — the design system alone is half the deliverable architect 3 ondemand design +process-board skills/process-board.md After the wireframes, when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane board per process, each step on its wireframe, every question pinned to a step (project-bin/process-board.js validate, anchor, build) architect,review 3 ondemand design interaction-model skills/interaction-model.md Stage 3, after the role model is CONFIRMED and BEFORE THE FIRST WIREFRAME — and any time a page shows ONE object, a menu item or home page is added, or a data source retrieves the current/first record: role homes, inboxes, and per page how it is reached and which object it takes as parameter lead,ba,architect 3 ondemand design brd-to-build-plan skills/brd-to-build-plan.md Turning BRDs plus architecture into a numbered, dependency-ordered build plan architect 3,4 ondemand requirements coverage-ledger skills/coverage-ledger.md Building the Stage 4 coverage ledger — every requirement either claimed by a build-plan row or catalogued with a reason, never invisible architect,gate 4 ondemand requirements diff --git a/project-bin/process-board.js b/project-bin/process-board.js new file mode 100644 index 00000000..2e16ae8c --- /dev/null +++ b/project-bin/process-board.js @@ -0,0 +1,484 @@ +#!/usr/bin/env node +// process-board.js: draw the Stage-3 process board. One swimlane board per business process, +// each step showing the wireframe it happens on, what the system does between screens, and +// every open question and assumption pinned to the step or screen it is about. +// +// WHY. A reviewer at Stage 3 gets sixty consolidated questions, thirty ASSUMED decisions and +// forty wireframes as three separate lists. A business owner reading "Should a rejected order go +// back to draft?" cannot tell where in the work that happens, so the question is answered blind or +// not at all. The board puts the three side by side: the process across screens, and each +// question stuck to the moment it changes. Field origin: a requirements-driven rebuild POC, +// 2026-10-16, where the reviewer said exactly this about the question list. +// +// SINGLE SOURCE. The board is GENERATED, never hand-edited, like blueprint.html: +// design/process-model.json authored by the architect agent from the BRDs +// (skills/process-board.md Step 2) +// written by `anchor` (derived) plus the agent's judged pass +// (skills/process-board.md Step 3) +// BRDs, wireframes, PROJECT.md, optional consolidated questions file: read-only inputs +// Producers of the inputs: BRDs = Stage 2 (mandatory); wireframes = design-artifacts.md Step 3 +// (Stage 3, mandatory); PROJECT.md = bin/init-project.sh + every checkpoint. +// +// Usage (run from the project root, or pass --project): +// process-board.js validate [--strict] check the model (and anchors, when present) +// process-board.js anchor derive anchors mechanically, keep judged ones, write +// the anchors file, list what is still unanchored +// process-board.js build validate, then write design/process-board.html +// options: --project --model --out --json (anchor: todo as JSON) +// +// Exit: 0 ok; 1 validation errors (or, with --strict, unanchored items); 2 usage/input error. +// No libraries, no network, deterministic output (same input, same bytes). +'use strict'; +const fs = require('fs'); +const path = require('path'); + +const die = msg => { console.error('process-board: ' + msg); process.exit(2); }; + +// ---------- args ---------- +const argv = process.argv.slice(2); +let cmd = null, root = '.', modelArg = null, outArg = null, strict = false, asJson = false; +for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--project') root = argv[++i]; + else if (a === '--model') modelArg = argv[++i]; + else if (a === '--out') outArg = argv[++i]; + else if (a === '--strict') strict = true; + else if (a === '--json') asJson = true; + else if (a === '-h' || a === '--help') { + console.log('usage: process-board.js validate|anchor|build [--project ] [--model ] [--out ] [--strict] [--json]'); + process.exit(0); + } else if (a.startsWith('-')) die('unknown option ' + a); + else if (!cmd) cmd = a; + else die('unexpected argument ' + a); +} +if (!['validate', 'anchor', 'build'].includes(cmd || '')) die('give a command: validate, anchor or build'); +root = path.resolve(root); +const rel = p => path.resolve(root, p); +const modelFile = modelArg ? path.resolve(modelArg) : rel('design/process-model.json'); +if (!fs.existsSync(modelFile)) die('no process model at ' + modelFile + ' (skills/process-board.md Step 2 authors it)'); + +const readJson = (f, what) => { + try { return JSON.parse(fs.readFileSync(f, 'utf8')); } + catch (e) { die('cannot parse ' + (what || f) + ': ' + e.message); } +}; +const byteSort = (a, b) => (a < b ? -1 : a > b ? 1 : 0); +const model = readJson(modelFile, 'process model'); +const designDir = path.dirname(modelFile); + +// ---------- inputs ---------- +function globDir(dir, re) { + try { return fs.readdirSync(dir).filter(f => re.test(f)).sort(byteSort).map(f => path.join(dir, f)); } + catch (e) { return []; } +} +function brdFiles() { + if (Array.isArray(model.brdDirs) && model.brdDirs.length) { + return model.brdDirs.flatMap(d => globDir(rel(d), /\.brd\.json$/)); + } + // Same three shapes as bin/lib/discover-brds.sh and the artifact manifest. + let out = []; + for (const a of globDir(rel('analysis'), /./)) { + out = out.concat(globDir(path.join(a, 'knowledge-base', 'brd'), /\.brd\.json$/)); + } + out = out.concat(globDir(rel('knowledge-base/brd'), /\.brd\.json$/)); + return [...new Set(out)]; +} + +const BRDS = {}; // brdId -> { file, base, doc } +const UC = {}; // "F001:UC001" -> { brd, id, title, actors, text } +const OQ = {}; // "F001:Q01" -> { brd, id, oq, text } +const BRD_PAGES = {}; // short page name -> [brdId] +for (const f of brdFiles()) { + const doc = readJson(f); + const base = path.basename(f); + const id = String(doc.id || (base.match(/^[A-Za-z]+\d+/) || [base])[0]); + BRDS[id] = { file: f, base, doc }; + for (const u of doc.useCases || []) { + UC[id + ':' + u.id] = { brd: id, id: u.id, title: u.title || '', actors: u.actors || [], json: JSON.stringify(u), raw: u }; + } + for (const q of doc.openQuestions || []) { + if (!q || !q.id) continue; + OQ[id + ':' + q.id] = { brd: id, base, id: q.id, raw: q }; + } + for (const p of doc.pages || []) { + if (!p || !p.name) continue; + (BRD_PAGES[p.name] = BRD_PAGES[p.name] || []).push(id); + } +} +const brdByFile = {}; +for (const [id, b] of Object.entries(BRDS)) brdByFile[b.base] = id; + +// Wireframes: page key is data-mx-page ("Module.Page") when present, else the filename stem. +const wfDir = model.wireframes ? rel(model.wireframes) : path.join(designDir, 'wireframes'); +const WF = {}; // pageKey -> { key, short, file, route, html } +const WF_SHORT = {}; // short name -> key +const kebab = name => name.replace(/\.html$/i, '') + .replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') + .replace(/[_\s]+/g, '-').replace(/[^A-Za-z0-9.\/-]/g, '-').replace(/-+/g, '-') + .replace(/^-|-$/g, '').toLowerCase(); +const attr = (tag, name) => { const m = (tag || '').match(new RegExp('\\b' + name + '\\s*=\\s*["\']([^"\']*)["\']', 'i')); return m ? m[1] : ''; }; +for (const f of globDir(wfDir, /\.html$/i)) { + const html = fs.readFileSync(f, 'utf8'); + const bodyTag = (html.match(/]*>/i) || [''])[0]; + const htmlTag = (html.match(/]*>/i) || [''])[0]; + const stem = path.basename(f).replace(/\.html$/i, ''); + const key = attr(bodyTag, 'data-mx-page') || attr(htmlTag, 'data-mx-page') || stem; + const short = key.includes('.') ? key.split('.').pop() : key; + const route = attr(bodyTag, 'data-route') || attr(htmlTag, 'data-route') || kebab(path.basename(f)); + WF[key] = { key, short, file: path.relative(designDir, f).split(path.sep).join('/'), route, html }; + WF_SHORT[short] = key; + WF_SHORT[stem] = WF_SHORT[stem] || key; +} +const resolvePage = p => (!p ? null : WF[p] ? p : WF_SHORT[p] ? WF_SHORT[p] : WF_SHORT[String(p).split('.').pop()] || null); +const shortOf = p => String(p || '').split('.').pop(); + +// ---------- model checks ---------- +const errors = [], warnings = []; +const err = m => errors.push(m), warn = m => warnings.push(m); +const LANE_KINDS = ['role', 'system', 'scheduled', 'external']; +const STEP_KINDS = ['task', 'system', 'decision', 'end']; +const lanes = Array.isArray(model.lanes) ? model.lanes : []; +const processes = Array.isArray(model.processes) ? model.processes : []; +if (!lanes.length) err('model has no lanes'); +if (!processes.length) err('model has no processes'); +const laneIdx = {}; +lanes.forEach((l, i) => { + if (!l.id) return err('lane ' + i + ' has no id'); + if (laneIdx[l.id] !== undefined) err('lane id ' + l.id + ' is used twice'); + laneIdx[l.id] = i; + if (l.kind && !LANE_KINDS.includes(l.kind)) err('lane ' + l.id + ': kind "' + l.kind + '" is not one of ' + LANE_KINDS.join(', ')); +}); +const normUc = r => String(r || '').trim().replace(/\s+/, ':'); +const STEP = {}; // stepId -> { step, proc } +const ucToSteps = {}; // ucRef -> [stepId] +const pageToSteps = {}; // pageKey -> [stepId] +const actionToSteps = {}; +const procSeen = {}; +for (const p of processes) { + if (!p.id) { err('a process has no id'); continue; } + if (procSeen[p.id]) err('process id ' + p.id + ' is used twice'); + procSeen[p.id] = true; + if (!Array.isArray(p.steps) || !p.steps.length) { err(p.id + ': no steps'); continue; } + let col = -1; + const cells = {}; + for (const s of p.steps) { + if (!s.id) { err(p.id + ': a step has no id'); continue; } + if (STEP[s.id]) err('step id ' + s.id + ' is used twice'); + STEP[s.id] = { step: s, proc: p }; + s.kind = s.kind || (s.page ? 'task' : 'system'); + if (!STEP_KINDS.includes(s.kind)) err(s.id + ': kind "' + s.kind + '" is not one of ' + STEP_KINDS.join(', ')); + if (laneIdx[s.lane] === undefined) err(s.id + ': lane "' + s.lane + '" is not declared in lanes'); + col = Number.isInteger(s.col) ? s.col : col + 1; + s._col = col; + const cell = s.lane + '@' + col; + if (cells[cell]) err(s.id + ': shares lane ' + s.lane + ' column ' + col + ' with ' + cells[cell] + ' (give one a different col)'); + cells[cell] = s.id; + if (s.page) { + const k = resolvePage(s.page); + if (k) { s._page = k; (pageToSteps[k] = pageToSteps[k] || []).push(s.id); } + else if (s.wireframeMissing) { s._pageMissing = true; warn(s.id + ': page ' + s.page + ' has no wireframe (' + s.wireframeMissing + ')'); } + else err(s.id + ': page "' + s.page + '" has no wireframe in ' + path.relative(root, wfDir) + ' (no data-mx-page or filename matches; set wireframeMissing with a reason if that is intended)'); + } else if (s.kind === 'task') { + err(s.id + ': a task step needs a page (the screen the person works on); use kind "system" for automated work'); + } + for (const r of s.useCases || []) { + const ref = normUc(r); + if (!UC[ref]) err(s.id + ': use case ' + r + ' is not in any BRD (expected ":")'); + (ucToSteps[ref] = ucToSteps[ref] || []).push(s.id); + } + for (const a of s.actions || []) (actionToSteps[a] = actionToSteps[a] || []).push(s.id); + } +} +// next-links checked after every step id is known (a link may continue in another process) +for (const { step: s } of Object.values(STEP)) { + s._next = (s.next || []).map(n => (typeof n === 'string' ? { to: n } : n)); + for (const n of s._next) if (!STEP[n.to]) err(s.id + ': next "' + n.to + '" is not a step id'); + if (s.kind === 'decision' && s._next.length < 2) err(s.id + ': a decision needs at least two outcomes in next'); +} +const standalone = Array.isArray(model.standalone) ? model.standalone : []; +const standaloneSet = {}; +for (const st of standalone) { + const ref = normUc(st.useCase); + if (!UC[ref]) err('standalone use case ' + st.useCase + ' is not in any BRD'); + if (!st.reason) err('standalone use case ' + st.useCase + ' has no reason'); + standaloneSet[ref] = st.reason || ''; +} +for (const ref of Object.keys(UC).sort(byteSort)) { + const onBoard = !!ucToSteps[ref], alone = ref in standaloneSet; + if (!onBoard && !alone) err('use case ' + ref + ' (' + UC[ref].title + ') is on no process and not listed as standalone'); + if (onBoard && alone) warn('use case ' + ref + ' is on a process and also listed as standalone'); +} +const pagesOffBoard = Object.keys(WF).filter(k => !pageToSteps[k]).sort(byteSort); +if (pagesOffBoard.length) warn(pagesOffBoard.length + ' wireframe(s) on no process step: ' + pagesOffBoard.join(', ')); + +// ---------- pinnable items: questions + ASSUMED decisions ---------- +const questionsFile = model.questions ? rel(model.questions) : null; +const anchorsFile = model.anchors ? rel(model.anchors) : path.join(designDir, 'question-anchors.json'); +const registerFile = rel(model.register || 'PROJECT.md'); +const ITEMS = []; // { id, kind: question|decision, title, text, blocking, assumption, risk, askWho, refs:[oqKey], fingerprint } +const itemById = {}; +const oqToItem = {}; +const clean = s => String(s == null ? '' : s).replace(/\s+/g, ' ').trim(); +const fp = s => clean(s).slice(0, 80); +function addItem(it) { if (itemById[it.id]) return; ITEMS.push(it); itemById[it.id] = it; } +if (questionsFile) { + if (!fs.existsSync(questionsFile)) die('questions file ' + model.questions + ' named in the model does not exist'); + const qj = readJson(questionsFile, 'questions file'); + const list = Array.isArray(qj) ? qj : (qj.questions || []); + for (const q of list) { + const id = q.cid || q.id; + if (!id) continue; + const refs = []; + for (const s of q.sources || []) { + const b = brdByFile[s.brd] || brdByFile[path.basename(String(s.brd || ''))] || (BRDS[s.brd] ? s.brd : null); + if (b && OQ[b + ':' + s.id]) { refs.push(b + ':' + s.id); oqToItem[b + ':' + s.id] = id; } + } + addItem({ id, kind: 'question', title: clean(q.title) || clean(q.question).slice(0, 80), text: clean(q.question), + blocking: q.blocking === true, assumption: clean(q.assumption), risk: clean(q.risk), askWho: clean(q.askWho), + category: clean(q.category), refs, fingerprint: fp(q.question) }); + } +} +for (const key of Object.keys(OQ).sort(byteSort)) { + if (oqToItem[key]) continue; + const q = OQ[key].raw; + if (q.consolidatedId && itemById[q.consolidatedId]) { oqToItem[key] = q.consolidatedId; itemById[q.consolidatedId].refs.push(key); continue; } + addItem({ id: key, kind: 'question', title: clean(q.question).slice(0, 80), text: clean(q.question), blocking: q.blocking === true, + assumption: clean(q.assumption), risk: clean(q.risk), askWho: clean(q.askWho), category: BRDS[OQ[key].brd].doc.title || '', + refs: [key], fingerprint: fp(q.question) }); + oqToItem[key] = key; +} +// ASSUMED decisions: rows of any register table whose header has Decision and Status columns. +if (fs.existsSync(registerFile)) { + const lines = fs.readFileSync(registerFile, 'utf8').split(/\r?\n/); + let hdr = null; const ord = {}; + const cellsOf = l => l.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim()); + for (const l of lines) { + if (!/^\s*\|/.test(l)) { hdr = null; continue; } + const c = cellsOf(l); + if (!hdr) { + const lc = c.map(x => x.toLowerCase()); + const di = lc.findIndex(x => /^decision/.test(x)), si = lc.findIndex(x => /^status/.test(x)); + hdr = (di >= 0 && si >= 0) ? { di, si, st: lc.findIndex(x => /^stage/.test(x)) } : { skip: true }; + continue; + } + if (hdr.skip || /^[-:\s|]+$/.test(l.replace(/\|/g, ''))) continue; + const status = c[hdr.si] || '', text = clean(c[hdr.di] || ''); + if (!/ASSUMED/.test(status) || !text) continue; + const stage = hdr.st >= 0 ? clean(c[hdr.st]) : ''; + const own = text.match(/^([A-Z][A-Z0-9]*-[A-Z]*\d+)\s*:/); + ord[stage] = (ord[stage] || 0) + 1; + const id = own ? own[1] : 'D' + (stage || 'x') + '-' + String(ord[stage]).padStart(2, '0'); + addItem({ id, kind: 'decision', title: text.replace(/^[A-Z][A-Z0-9]*-[A-Z]*\d+\s*:\s*/, '').slice(0, 90), text, blocking: false, + assumption: text, risk: clean((c.slice(-1)[0] || '').match(/risk:(.*)$/i) ? c.slice(-1)[0].match(/risk:(.*)$/i)[1] : ''), + status: clean(status), stage, refs: [], fingerprint: fp(text) }); + } +} + +// ---------- derive anchors (mechanical) ---------- +const tok = (s, t) => new RegExp('(^|[^A-Za-z0-9_-])' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '($|[^A-Za-z0-9_-])').test(s); +// "a/b.ts:10-20,31; c.sql:5" -> [{f:'b.ts' path, a:10, b:20}, ...]. Only cited line numbers count. +function ranges(s) { + const out = []; + for (const m of String(s).matchAll(/([A-Za-z0-9_.\/\[\]-]+\.[A-Za-z]{1,5}):([0-9][0-9,\s-]*)/g)) { + const f = m[1].replace(/^.*?(?=[^\/]+\/[^\/]+$)/, ''); + for (const part of m[2].split(',')) { + const n = part.trim().match(/^(\d+)(?:-(\d+))?$/); + if (n) out.push({ f, a: +n[1], b: +(n[2] || n[1]) }); + } + } + return out; +} +const overlaps = (xs, ys) => xs.some(x => ys.some(y => x.f === y.f && x.a <= y.b && y.a <= x.b)); +const stepOrder = {}; let so = 0; +for (const p of processes) for (const s of p.steps || []) stepOrder[s.id] = so++; +function derive(it) { + const score = {}, basis = {}, pages = new Set(); + const hit = (sid, w, why) => { if (!STEP[sid]) return; if ((score[sid] || 0) < w) { score[sid] = w; basis[sid] = why; } }; + const texts = [it.text, it.assumption, it.title]; + for (const key of it.refs) { + const o = OQ[key]; + const q = o.raw; + texts.push(clean(q.question), clean(q.assumption), clean(q.source), clean(q.evidence)); + // a) use cases that list this open question; c) use cases whose text names it + for (const [ref, u] of Object.entries(UC)) { + if (u.brd !== o.brd) continue; + const listed = (u.raw.openQuestions || []).includes(o.id); + if (listed || tok(u.json, o.id)) for (const sid of ucToSteps[ref] || []) hit(sid, listed ? 2 : 1, key + ' -> ' + ref + ' -> ' + sid); + } + // b) business rules that land in a use case and name this question + for (const br of BRDS[o.brd].doc.businessRules || []) { + const t = JSON.stringify(br); + if (!tok(t, o.id)) continue; + for (const m of String(br.landsIn || '').match(/UC[-A-Za-z0-9]+/g) || []) { + for (const sid of ucToSteps[o.brd + ':' + m] || []) hit(sid, 1, key + ' -> ' + (br.id || 'rule') + ' -> ' + o.brd + ':' + m + ' -> ' + sid); + } + } + // d) BRD pages whose text names it + for (const pg of BRDS[o.brd].doc.pages || []) { + if (tok(JSON.stringify(pg), o.id)) { const k = resolvePage(pg.name); if (k) pages.add(k); } + } + // source citations: the question and a use case cite overlapping lines of the same file + // (migration BRDs cite "path/file.ts:10-40"); the same file alone is too common to count + const qr = ranges([q.source, q.evidence].map(clean).join(' ')); + if (qr.length) for (const [ref, u] of Object.entries(UC)) { + if (u.brd !== o.brd) continue; + if (overlaps(qr, ranges(String(u.raw.source || '')))) for (const sid of ucToSteps[ref] || []) hit(sid, 1, key + ' cites the lines of ' + ref + ' -> ' + sid); + } + // use case ids named inside the question itself + const own = [q.question, q.assumption, q.source, q.evidence].map(clean).join(' '); + for (const ref of Object.keys(UC)) { + if (UC[ref].brd === o.brd && tok(own, UC[ref].id)) for (const sid of ucToSteps[ref] || []) hit(sid, 1, key + ' names ' + ref + ' -> ' + sid); + } + } + const all = texts.filter(Boolean).join(' '); + // e) microflow names and page names in the text (most specific) + for (const a of Object.keys(actionToSteps)) if (tok(all, a)) for (const sid of actionToSteps[a]) hit(sid, 3, 'names ' + a + ' -> ' + sid); + // A one-word page name (Home, Login, Dashboard) is also an ordinary word or a module name, so + // it counts only in its full Module.Page form; Order_Overview-style names count bare. + for (const k of Object.keys(WF)) { + const sh = WF[k].short, distinctive = /_|[a-z][A-Z]/.test(sh); + if ((distinctive && tok(all, sh)) || (k !== sh && tok(all, k))) pages.add(k); + } + // f) "F002 UC005" style references (decisions and consolidated questions use them) + for (const m of all.matchAll(/\b([A-Z]+\d{3})[ :]+(UC[-A-Za-z0-9]+)/g)) { + for (const sid of ucToSteps[m[1] + ':' + m[2]] || []) hit(sid, 2, 'names ' + m[1] + ':' + m[2] + ' -> ' + sid); + } + // g) a decision that names an open question or consolidated question inherits its steps + if (it.kind === 'decision') { + for (const k of Object.keys(OQ)) if (tok(all, OQ[k].id) && OQ[k].id.length > 3) { const ii = itemById[oqToItem[k]]; if (ii && ii._derived) for (const t of ii._derived) if (t.type === 'step') hit(t.step, 1, 'names ' + OQ[k].id + ' -> ' + t.step); } + for (const q of ITEMS) if (q.kind === 'question' && q.id !== it.id && /^[A-Z]+-\d+$/.test(q.id) && tok(all, q.id) && q._derived) for (const t of q._derived) if (t.type === 'step') hit(t.step, 1, 'names ' + q.id + ' -> ' + t.step); + } + for (const k of pages) for (const sid of pageToSteps[k] || []) hit(sid, 2, 'names screen ' + WF[k].short + ' -> ' + sid); + const top = Math.max(0, ...Object.values(score)); + if (top > 0) { + const sids = Object.keys(score).filter(s => score[s] === top).sort((a, b) => stepOrder[a] - stepOrder[b]).slice(0, 3); + return { targets: sids.map(s => ({ type: 'step', step: s })), basis: sids.map(s => basis[s]).join('; ') }; + } + if (pages.size) { + const ks = [...pages].sort(byteSort).slice(0, 3); + return { targets: ks.map(k => ({ type: 'page', page: k })), basis: 'names screen(s) ' + ks.map(shortOf).join(', ') + ' (on no step)' }; + } + return null; +} +for (const it of ITEMS.filter(i => i.kind === 'question')) { const d = derive(it); it._derived = d ? d.targets : null; it._basis = d ? d.basis : ''; } +for (const it of ITEMS.filter(i => i.kind === 'decision')) { const d = derive(it); it._derived = d ? d.targets : null; it._basis = d ? d.basis : ''; } + +// ---------- existing anchors (judged entries survive; derived are recomputed) ---------- +const prev = fs.existsSync(anchorsFile) ? readJson(anchorsFile, 'anchors file') : { anchors: [] }; +const prevById = {}; +for (const a of prev.anchors || []) prevById[a.id] = a; +function targetOk(t) { + if (!t || !t.type) return 'no type'; + if (t.type === 'step') return STEP[t.step] ? '' : 'step ' + t.step + ' does not exist'; + if (t.type === 'page') return resolvePage(t.page) ? '' : 'page ' + t.page + ' has no wireframe'; + if (t.type === 'global') return t.bucket ? '' : 'global target needs a bucket'; + return 'unknown target type ' + t.type; +} +const ANCH = {}; // id -> { targets, anchoredBy, basis } +const staleJudged = []; +for (const it of ITEMS) { + const p = prevById[it.id]; + if (p && p.anchoredBy === 'judged') { + if (p.fingerprint && p.fingerprint !== it.fingerprint) { staleJudged.push(it.id); } + else { + const bad = (p.targets || []).map(targetOk).filter(Boolean); + if (bad.length) err('anchor ' + it.id + ': ' + bad.join('; ')); + else { ANCH[it.id] = { targets: p.targets, anchoredBy: 'judged', basis: p.basis || p.why || '' }; continue; } + } + } + if (it._derived) ANCH[it.id] = { targets: it._derived, anchoredBy: 'derived', basis: it._basis }; +} +for (const id of Object.keys(prevById)) if (!itemById[id]) warn('anchors file names ' + id + ', which is no longer a question or ASSUMED decision'); +for (const id of staleJudged) warn('judged anchor ' + id + ' dropped: the item text changed since it was judged; judge it again'); + +const unanchored = ITEMS.filter(it => !ANCH[it.id]); +const stats = { derived: 0, judged: 0, global: 0, unanchored: unanchored.length, total: ITEMS.length }; +for (const a of Object.values(ANCH)) { + stats[a.anchoredBy]++; + if (a.targets.every(t => t.type === 'global')) stats.global++; +} +const statLine = () => 'anchors: ' + stats.total + ' item(s), ' + stats.derived + ' derived, ' + stats.judged + ' judged, ' + + stats.global + ' global, ' + stats.unanchored + ' unanchored'; + +function report() { + for (const w of warnings) console.log('WARN ' + w); + for (const e of errors) console.log('ERROR ' + e); + const ucs = Object.keys(UC).length; + console.log('model: ' + processes.length + ' process(es), ' + Object.keys(STEP).length + ' step(s), ' + ucs + ' use case(s) (' + + Object.keys(standaloneSet).length + ' standalone), ' + Object.keys(WF).length + ' wireframe(s), ' + errors.length + ' error(s), ' + warnings.length + ' warning(s)'); + console.log(statLine()); +} + +// ---------- commands ---------- +if (cmd === 'validate') { + report(); + if (unanchored.length) console.log('unanchored: ' + unanchored.map(i => i.id).join(', ')); + process.exit(errors.length || (strict && unanchored.length) ? 1 : 0); +} + +if (cmd === 'anchor') { + const out = { + schema: 'process-anchors/1', + note: 'Generated by process-board.js anchor. Edit only judged entries (anchoredBy "judged"); derived entries are recomputed on every run.', + anchors: ITEMS.filter(it => ANCH[it.id]).map(it => ({ + id: it.id, kind: it.kind, anchoredBy: ANCH[it.id].anchoredBy, targets: ANCH[it.id].targets, + basis: ANCH[it.id].basis, fingerprint: it.fingerprint, + })), + }; + fs.mkdirSync(path.dirname(anchorsFile), { recursive: true }); + fs.writeFileSync(anchorsFile, JSON.stringify(out, null, 2) + '\n'); + report(); + console.log('wrote ' + path.relative(root, anchorsFile)); + if (unanchored.length) { + if (asJson) { + console.log(JSON.stringify(unanchored.map(it => ({ id: it.id, kind: it.kind, anchoredBy: 'judged', targets: [], basis: '', fingerprint: it.fingerprint, text: it.text })), null, 2)); + } else { + console.log('to judge (' + unanchored.length + '): add an entry with anchoredBy "judged" for each'); + for (const it of unanchored) console.log(it.id + '\t' + it.kind + '\t' + it.text.slice(0, 160)); + } + } + process.exit(errors.length ? 1 : 0); +} + +// ---------- build ---------- +report(); +if (errors.length) { console.error('process-board: not building, fix the model first'); process.exit(1); } +const outFile = outArg ? path.resolve(outArg) : path.join(designDir, 'process-board.html'); +const protoFile = model.prototype ? rel(model.prototype) : path.join(designDir, 'prototype.html'); +const protoHref = fs.existsSync(protoFile) ? path.relative(path.dirname(outFile), protoFile).split(path.sep).join('/') : ''; +const dsFile = model.ds ? rel(model.ds) : path.join(designDir, 'ds.css'); +const dsCss = fs.existsSync(dsFile) ? fs.readFileSync(dsFile, 'utf8') : ''; + +// strip + + diff --git a/skills/checkpoints/checkpoint-design.md b/skills/checkpoints/checkpoint-design.md index 7e2b0852..55f96833 100644 --- a/skills/checkpoints/checkpoint-design.md +++ b/skills/checkpoints/checkpoint-design.md @@ -13,6 +13,9 @@ Pull from `.mx-brd.json` files and source KB screens: - Key user flows (group screens by journey: auth, main feature, settings, etc.) - Source layout patterns detected (sidebar, top nav, card grid, list-detail, modal forms) - Any screens with no direct Atlas widget equivalent (flag these specifically) +- When `design/process-board.html` exists (`process-board.md`): walk the open questions and ASSUMED + decisions on the board, step by step, instead of as a flat list; a reviewer answers a question + better when they see the screen and the moment it changes ## What's Next diff --git a/skills/conversion-runbook.md b/skills/conversion-runbook.md index 59d7e8c6..aabcee3b 100644 --- a/skills/conversion-runbook.md +++ b/skills/conversion-runbook.md @@ -578,8 +578,8 @@ The biggest gap before this runbook existed. Module boundaries, wiring diagrams | | | |---|---| | **User defines** | ① One Mendix app or several (if flagged at Stage 0). ② **Module boundaries** (agent proposes with `modularize-domain.md` criteria). ③ **Buy vs build vs stub, per fit-gap item** — the confirming step `brd-to-build-plan.md` assumed already happened. ④ **Target security / role model** — not just whether auth existed in the source, but what the target should be. ⑤ **Data volumes, concurrency, NFRs** — these decide indexing, pagination, datagrid-vs-paged-gallery, loop batch sizes. ⑥ **Integration contracts** — real or stub, endpoint, credentials, owner, test environment. ⑦ **Branding inputs** — logo, palette, type, spacing, per `design-artifacts.md`; with no brand, the design direction (Step 0b question D1: ideation-library directions or Atlas defaults). | -| **Agent produces** | `.mx-brd.json`, `architecture/` (module defs, layer diagram, wiring diagram, `fit-gap.md`, `blueprint.html` checkpoint render — plus a workflow diagram and/or agent-wiring diagram in `blueprint.md`/`blueprint.html` when CAC-3's Q3 flags real scope for either, plus cross-persona journey diagrams + journey list — `architecture-blueprint.md` Step 3d — whenever the BRDs carry more than one persona; single-persona skip recorded as a one-line note, never silent), `design/` per `design-artifacts.md`'s full output list: `design/interaction-model.md` (first) + `ds.css` + `design-system.html` + **`wireframes/*.html`, one annotated wireframe per screen** — the design system without the wireframes is half the deliverable and fails the gate. | -| **Surface** | `module-design.html` · `architecture/blueprint.html` (generated render of `blueprint.md` — architecture-blueprint.md Step 7, never hand-edited) · `design-system.html` + `wireframes/*.html` | +| **Agent produces** | `.mx-brd.json`, `architecture/` (module defs, layer diagram, wiring diagram, `fit-gap.md`, `blueprint.html` checkpoint render — plus a workflow diagram and/or agent-wiring diagram in `blueprint.md`/`blueprint.html` when CAC-3's Q3 flags real scope for either, plus cross-persona journey diagrams + journey list — `architecture-blueprint.md` Step 3d — whenever the BRDs carry more than one persona; single-persona skip recorded as a one-line note, never silent), `design/` per `design-artifacts.md`'s full output list: `design/interaction-model.md` (first) + `ds.css` + `design-system.html` + **`wireframes/*.html`, one annotated wireframe per screen** — the design system without the wireframes is half the deliverable and fails the gate. Once the wireframes exist and the BRDs carry more than a handful of open questions: `design/process-model.json` + `design/process-board.html` per `process-board.md` (one swimlane board per process, each step on its wireframe, every open question and ASSUMED decision pinned to a step; generated, never hand-edited). | +| **Surface** | `module-design.html` · `architecture/blueprint.html` (generated render of `blueprint.md` — architecture-blueprint.md Step 7, never hand-edited) · `design-system.html` + `wireframes/*.html` · `design/process-board.html` (process-board.md Step 4, when the model exists) | | **Gate ✋** | Boundaries approved. Marketplace calls made. Interaction model walked with two objects, two roles. Role model, volumes, integrations and branding **each asked and answered**: `CONFIRMED`, or explicitly delegated by the user ("you decide" → `ASSUMED` with risk). Never `ASSUMED` without the question having reached the user. Close-out block pasted before the ✋ decision is asked (§1b rule 7) — this stage has no closing checkpoint, so the gate is where the Stage-4 open (its approach, skills and optional artifacts) is shown. **No architecture/design artifact is produced before its checkpoint ran.** | | **Owner** | `architect-agent` (interviews run by `ba-agent`) | diff --git a/skills/process-board.md b/skills/process-board.md new file mode 100644 index 00000000..7d1ce45d --- /dev/null +++ b/skills/process-board.md @@ -0,0 +1,151 @@ +# Process Board: Where Each Question Fits in the Work +**Applies to:** migration, requirements-driven build and existing-app change, at Stage 3, once the wireframes exist. +**Requires:** Node 18 or later (no packages). Windows: Git Bash, same commands. +**Purpose:** Draw one swimlane board per business process. Each step sits in the lane of the person or system that does it, shows the wireframe it happens on, says what the system does between screens, and carries a pin for every open question and ASSUMED decision about that moment. A reviewer who cannot place "Should a rejected order go back to draft?" in a list of sixty can answer it on the board. +**Upstream:** BRDs (Stage 2, mandatory: `useCases`, `openQuestions`, `pages`); `design/wireframes/*.html` (`design-artifacts.md` Step 3, mandatory at Stage 3); the `PROJECT.md` decisions table (every checkpoint); optionally a consolidated questions file the project already keeps. +**Downstream:** `checkpoints/checkpoint-design.md` (CAC-4 walks the questions on the board); `bin/gate-check.sh` check_stage_3 (fails a board older than its model or anchors); `journey-proof.md` (a process on the board is a journey to prove). +**Companion:** `architecture-blueprint.md` Step 3d (journey diagrams; the board is the screen-level version of the same picture and discharges the optional `swimlanes` artifact); `assemble-prototype` (the board links each step into `design/prototype.html` when it exists). +**Source:** field run on a requirements-driven rebuild POC, 2026-10-16. The reviewer said of the question list: "I really don't understand where in the process they fit." Result there: 9 processes, 73 steps, 102 pinnable items, 48 placed mechanically, 54 by review, 19 of those in a whole-app bucket, 0 unplaced. + +--- + +## When + +Run it at Stage 3 after the wireframes are drawn and before the Stage-3 gate interview, when any of these hold: + +- the BRDs carry more than about ten open questions, or the register more than about ten ASSUMED decisions; +- more than one role touches the same record (an approval, a hand-over, a queue); +- the reviewer is a business owner, not a builder. + +Skip it for a one-screen app or a project whose questions are all closed. Record the skip as one line in `PROJECT.md`. + +## Inputs and outputs + +| File | Who writes it | Notes | +|---|---|---| +| `design/process-model.json` | you, Step 2 | the processes, lanes and steps. The only hand-authored input. | +| `design/question-anchors.json` | `process-board.js anchor` plus you, Step 3 | where each item is pinned. Path is `model.anchors` if set. | +| `design/process-board.html` | `process-board.js build`, Step 4 | generated, never hand-edited, self-contained, light and dark. | + +The script is `project-bin/process-board.js` (installed into a wired project's `bin/`). Run it from the project root or pass `--project`. For the nested layout (`analysis//design/`), pass `--model analysis//design/process-model.json`; outputs go next to the model. + +## Step 1: Find the processes + +Read the BRD use cases and the blueprint journeys. A process is one piece of work with a start and an end that a business person would name: "submit an order and get it approved", "report a fault and follow it up". Usually 4 to 10 for a small app. Every use case must end up on a step or in `standalone` with a reason. Lookups, exports and settings screens that belong to no flow go in `standalone`. + +## Step 2: Write design/process-model.json + +```json +{ + "schema": "process-model/1", + "app": "Order desk", + "questions": "analysis/questions/consolidated.json", + "anchors": "design/question-anchors.json", + "lanes": [ + { "id": "requester", "label": "Requester", "kind": "role" }, + { "id": "approver", "label": "Approver", "kind": "role" }, + { "id": "system", "label": "System", "kind": "system" } + ], + "processes": [ + { "id": "P1", "title": "Order approval: submit, approve or reject, notify", + "summary": "One plain sentence a business owner recognises.", + "steps": [ + { "id": "P1-01", "lane": "requester", "title": "Fill in and submit the order", + "page": "Orders.Order_NewEdit", "useCases": ["F001:UC001"], "actions": ["ACT_Order_Submit"], + "status": "Draft", "next": ["P1-02"] }, + { "id": "P1-02", "lane": "system", "kind": "system", "title": "Check and store the order", + "does": "Validates the amount and sets the status to Submitted.", "next": ["P1-03"] }, + { "id": "P1-04", "lane": "approver", "kind": "decision", "title": "Approve or reject?", + "next": [ { "to": "P1-05", "label": "Approve" }, { "to": "P1-06", "label": "Reject" } ] } + ] } + ], + "standalone": [ { "useCase": "F001:UC005", "reason": "Nightly job, no screen, on no flow." } ] +} +``` + +Rules the validator enforces: + +- **Lanes**: `kind` is `role`, `system`, `scheduled` or `external`. One lane per role from the Stage-3 role model, one for the app, one per scheduled job or outside system that acts on its own. +- **Steps**: `kind` is `task` (a person on a screen; needs `page`), `system` (the app does it, no screen), `decision` (needs two or more `next` outcomes, each with a `label`) or `end`. Default: `task` when `page` is set, else `system`. +- **page** resolves against the wireframes by `data-mx-page` (`Module.Page`), else the short name or the filename. A page with no wireframe is an error; set `"wireframeMissing": ""` when that is a recorded decision, and it becomes a warning. +- **useCases** are `":"`. An unknown one is an error. +- **next** may point into another process (the card shows "continues in"). A step that loops back draws a dashed edge over the top. +- **col** is the column. It counts up by itself; set it only to line steps up (two lanes in the same column) or to leave a gap. Two steps in one lane and column is an error. +- **actions**, **status**, **does** and **events** are what the reviewer reads as "what happens". Use microflow names in `actions`: they are also the strongest anchoring signal. +- **questions** (optional): a consolidated questions file, an array or `{ "questions": [...] }` with `cid`, `title`, `question`, `blocking`, `assumption`, `risk`, `askWho`, `sources: [{ "brd", "id" }]`. Without it, each BRD open question is its own item. +- **register** (optional, default `PROJECT.md`): any table with a Decision and a Status column. Rows whose status says ASSUMED become decision items. + +Write titles as plain verbs a business owner says: "Approve or reject?", not "ACT_Approve". Microflow names go in `actions`, behind the technical details click. + +Run `process-board.js validate` until it reports 0 errors. Read every warning; a wireframe on no step usually means a missing step. + +## Step 3: Anchor every question and assumption + +```bash +node bin/process-board.js anchor # derive, keep judged entries, write the anchors file +node bin/process-board.js anchor --json # the still-unanchored items as skeletons to fill in +``` + +`anchor` places what it can prove (`anchoredBy: "derived"`), strongest signal first: + +1. the question text names a microflow in a step's `actions`; +2. a use case lists the question in its `openQuestions`, or the text names a use case (`F002 UC005`) or a screen (`Order_Overview`, or `Module.Page` for one-word names); +3. a business rule that names the question lands in a use case; the question and a use case cite overlapping source lines; an ASSUMED decision names a question that is already placed. + +Derived entries are recomputed on every run. For each item left over, and for any derived placement that is wrong, add a judged entry: + +```json +{ "id": "F001:Q04", "kind": "question", "anchoredBy": "judged", + "targets": [ { "type": "global", "bucket": "Money" } ], + "basis": "Applies to every amount in the app", + "fingerprint": "" } +``` + +- Targets: `{ "type": "step", "step": "P1-03" }`, `{ "type": "page", "page": "Order_Overview" }` (a screen on no step), or `{ "type": "global", "bucket": "Hosting and operations" }`. +- Pin to the step where the answer changes what happens. Up to three targets. +- Use a global bucket only for items about the whole app or the way of working (hosting, build plan, look and feel, app structure). Global is allowed and counted, never hidden. +- `basis` is one plain sentence; the reviewer sees it as "Placed by review (...)". +- The fingerprint ties the judgment to the item text. When the item is reworded, the judged entry is dropped with a warning and the item is unanchored again, so a stale placement never survives silently. + +Re-run `anchor` until it reports `0 unanchored`. `validate --strict` fails on any unanchored item. + +## Step 4: Build + +```bash +node bin/process-board.js build +``` + +Validates first and refuses on errors. Writes `design/process-board.html`: an index of process cards (a mini map of lanes, pin counts) plus the whole-app bucket, and one board per process. Rebuild after any change to the model, the anchors, a wireframe, a BRD or the register. `gate-check.sh` check_stage_3 fails when the board is older than `process-model.json` or `question-anchors.json`. + +What the reviewer gets: + +- red pin: open question that blocks the build; amber: open question; violet: an assumption to confirm; +- click a step: the wireframe large, a link to the screen full size and into the clickable prototype, what happens next, and each question as Question, We assume, If wrong, Ask; the use cases and microflows sit behind "Technical details"; +- click a pin or a list row: the question card with links to every step it sits on; +- "Blocking only" hides the rest. + +## Step 5: Verify by looking + +A clean validate proves the data, not the picture. Screenshot the index and at least the two busiest boards, then open the PNGs and read them as the reviewer would. + +```bash +CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" # Linux: google-chrome +B="file://$PWD/design/process-board.html" +"$CHROME" --headless=new --hide-scrollbars --window-size=1800,1400 --virtual-time-budget=8000 --screenshot=/tmp/board-index.png "$B#/" +"$CHROME" --headless=new --hide-scrollbars --window-size=3000,1300 --virtual-time-budget=8000 --screenshot=/tmp/board-P1.png "$B#/P1" +``` + +Use `--headless=new`: the old headless mode often paints the wireframe thumbnails blank. Check: + +- every arrow reads as the real order (no line that seems to enter the wrong card); +- decision labels are readable and sit on their own branch; +- thumbnails show the screens; +- the busiest step does not carry ten pins (split the step, or the question is too broad); +- titles make sense to someone who has never seen the BRDs; +- dark mode (`--force-dark-mode`) is readable. + +Fix the model or the anchors, rebuild, look again. Do not open the board for the user unasked; give the path. + +## Gate + +At the Stage-3 interview, walk the board process by process: blocking pins first, then the assumptions. Record each answer in `PROJECT.md` (CONFIRMED, or ASSUMED with the user's delegation), rebuild, and note the board path in the close-out block. diff --git a/tests/wave2/fixtures/process-board/PROJECT.md b/tests/wave2/fixtures/process-board/PROJECT.md new file mode 100644 index 00000000..cffc69fb --- /dev/null +++ b/tests/wave2/fixtures/process-board/PROJECT.md @@ -0,0 +1,16 @@ +# PROJECT.md: synthetic process board fixture +Toolkit commit: none +Entry mode: requirements-driven + +## Decisions + +| Stage | Decision | Status | Notes | +|---|---|---|---| +| 3 | Module structure: one module Orders | CONFIRMED 2026-09-02 | matches the BRD | +| 3 | Rejection reason is required and shown on Order_Approve | ASSUMED | risk: approvers reject without a reason | +| 3 | Hosting: one environment for the demo | ASSUMED | risk: none for the demo | + +## Open questions + +| # | Question | Raised at | Status | +|---|---|---|---| diff --git a/tests/wave2/fixtures/process-board/design/ds.css b/tests/wave2/fixtures/process-board/design/ds.css new file mode 100644 index 00000000..2a14957d --- /dev/null +++ b/tests/wave2/fixtures/process-board/design/ds.css @@ -0,0 +1,3 @@ +.ds-body{font-family:system-ui,sans-serif;margin:0;background:#fff;color:#123} +.page{padding:40px} +.btn{background:#2563eb;color:#fff;border:0;border-radius:6px;padding:10px 20px;font-size:24px} diff --git a/tests/wave2/fixtures/process-board/design/process-model.json b/tests/wave2/fixtures/process-board/design/process-model.json new file mode 100644 index 00000000..83ad5254 --- /dev/null +++ b/tests/wave2/fixtures/process-board/design/process-model.json @@ -0,0 +1,29 @@ +{ + "schema": "process-model/1", + "app": "Order desk", + "lanes": [ + { "id": "requester", "label": "Requester", "kind": "role" }, + { "id": "approver", "label": "Approver", "kind": "role" }, + { "id": "system", "label": "System", "kind": "system" } + ], + "processes": [ + { + "id": "P1", + "title": "Order approval: submit, approve or reject, notify", + "summary": "A requester submits an order, an approver approves or rejects it, and the requester is told.", + "steps": [ + { "id": "P1-01", "lane": "requester", "title": "Fill in and submit the order", "page": "Orders.Order_NewEdit", "useCases": ["F001:UC001"], "actions": ["ACT_Order_Submit"], "status": "Draft", "next": ["P1-02"] }, + { "id": "P1-02", "lane": "system", "kind": "system", "title": "Check and store the order", "does": "Validates the amount and sets the status to Submitted.", "status": "Draft to Submitted", "next": ["P1-03"] }, + { "id": "P1-03", "lane": "approver", "title": "Open the order", "page": "Order_Approve", "useCases": ["F001:UC002"], "next": ["P1-04"] }, + { "id": "P1-04", "lane": "approver", "kind": "decision", "title": "Approve or reject?", "does": "The approver decides.", "next": [ { "to": "P1-05", "label": "Approve" }, { "to": "P1-06", "label": "Reject" } ] }, + { "id": "P1-05", "lane": "system", "kind": "system", "title": "Record the approval", "actions": ["ACT_Order_Approve"], "status": "Approved", "next": ["P1-07"] }, + { "id": "P1-06", "lane": "system", "kind": "system", "title": "Record the rejection", "actions": ["ACT_Order_Reject"], "status": "Rejected", "next": ["P1-07"] }, + { "id": "P1-07", "lane": "system", "kind": "system", "title": "Tell the requester", "useCases": ["F001:UC003"], "actions": ["SUB_Order_Notify"], "next": ["P1-08"] }, + { "id": "P1-08", "lane": "requester", "title": "See the outcome in the order list", "page": "Order_Overview", "useCases": ["F001:UC004"], "next": [] } + ] + } + ], + "standalone": [ + { "useCase": "F001:UC005", "reason": "A nightly job with no screen and no person; it touches no step of the approval process." } + ] +} diff --git a/tests/wave2/fixtures/process-board/design/question-anchors.json b/tests/wave2/fixtures/process-board/design/question-anchors.json new file mode 100644 index 00000000..eb1e01d4 --- /dev/null +++ b/tests/wave2/fixtures/process-board/design/question-anchors.json @@ -0,0 +1,7 @@ +{ + "schema": "process-anchors/1", + "anchors": [ + { "id": "F001:Q04", "kind": "question", "anchoredBy": "judged", "targets": [ { "type": "global", "bucket": "Money" } ], "basis": "Applies to every amount in the app", "fingerprint": "Which currency do amounts use?" }, + { "id": "D3-02", "kind": "decision", "anchoredBy": "judged", "targets": [ { "type": "global", "bucket": "Hosting" } ], "basis": "Not tied to a screen", "fingerprint": "Hosting: one environment for the demo" } + ] +} diff --git a/tests/wave2/fixtures/process-board/design/wireframes/Order_Approve.html b/tests/wave2/fixtures/process-board/design/wireframes/Order_Approve.html new file mode 100644 index 00000000..ca7761a5 --- /dev/null +++ b/tests/wave2/fixtures/process-board/design/wireframes/Order_Approve.html @@ -0,0 +1,9 @@ + + +Order_Approve + +

Page parameter: Orders.Order

+

Approve order

Order details and history.

+ + + diff --git a/tests/wave2/fixtures/process-board/design/wireframes/Order_NewEdit.html b/tests/wave2/fixtures/process-board/design/wireframes/Order_NewEdit.html new file mode 100644 index 00000000..419261ff --- /dev/null +++ b/tests/wave2/fixtures/process-board/design/wireframes/Order_NewEdit.html @@ -0,0 +1,9 @@ + + +Order_NewEdit + +

Page parameter: Orders.Order

+

New order

Item, quantity, amount.

+ + + diff --git a/tests/wave2/fixtures/process-board/design/wireframes/Order_Overview.html b/tests/wave2/fixtures/process-board/design/wireframes/Order_Overview.html new file mode 100644 index 00000000..7ca3e7a6 --- /dev/null +++ b/tests/wave2/fixtures/process-board/design/wireframes/Order_Overview.html @@ -0,0 +1,9 @@ + + +Order_Overview + +

Page parameter: Orders.Order

+

My orders

A list of past orders.

+ + + diff --git a/tests/wave2/fixtures/process-board/design/wireframes/Settings_Edit.html b/tests/wave2/fixtures/process-board/design/wireframes/Settings_Edit.html new file mode 100644 index 00000000..3a037872 --- /dev/null +++ b/tests/wave2/fixtures/process-board/design/wireframes/Settings_Edit.html @@ -0,0 +1,9 @@ + + +Settings_Edit + +

Page parameter: Orders.Order

+

Settings

App settings.

+ + + diff --git a/tests/wave2/test-process-board.sh b/tests/wave2/test-process-board.sh new file mode 100755 index 00000000..4a1c9abf --- /dev/null +++ b/tests/wave2/test-process-board.sh @@ -0,0 +1,166 @@ +#!/bin/bash +# test-process-board.sh: pin what process-board.js validates, how it anchors questions, what +# `build` writes, and the Stage-3 gate rule that a process board must not be older than its model. +# +# Golden input: tests/wave2/fixtures/process-board/, a synthetic order approval app (one BRD with +# five use cases and four open questions, four wireframes, a PROJECT.md decisions table with two +# ASSUMED rows, a process model and two judged anchors). Its shapes follow the BRD contract +# (useCases, openQuestions, pages) and the wireframe contract (data-mx-page, data-route) the real +# Stage-2 and Stage-3 producers write. Each defect case copies the fixture and changes one thing, +# so a pass means that one defect was seen. +# +# Usage: bash test-process-board.sh [path-to-process-board.js] + +set -u + +TOOLKIT="$(cd "$(dirname "$0")/../.." && pwd)" +SUT="${1:-$TOOLKIT/project-bin/process-board.js}" +SUT="$(cd "$(dirname "$SUT")" && pwd)/$(basename "$SUT")" +GATE="$TOOLKIT/bin/gate-check.sh" +FIX="$TOOLKIT/tests/wave2/fixtures/process-board" +export MXTK_NO_FETCH=1 + +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' in: $(printf '%s' "$2" | head -c 400)" ;; esac; } +hasnt(){ case "$2" in *"$3"*) bad "$1" "unexpected '$3'" ;; *) ok "$1" ;; esac; } + +command -v node >/dev/null 2>&1 || { echo "SKIP: node not on PATH"; exit 0; } + +# fresh copy of the golden fixture +fresh() { rm -rf "$TMP/$1"; cp -R "$FIX" "$TMP/$1"; echo "$TMP/$1"; } +# edit a JSON file in place with a JavaScript expression over `j` +jedit() { node -e 'const fs=require("fs");const f=process.argv[1];const j=JSON.parse(fs.readFileSync(f,"utf8"));'"$2"';fs.writeFileSync(f,JSON.stringify(j,null,2));' "$1"; } +run() { node "$SUT" "$@" 2>&1; } + +echo "== T1: the golden fixture validates clean ==" +P="$(fresh golden)" +OUT="$(run validate --project "$P")"; RC=$? +check "exit 0" "$RC" "0" +has "summary line" "$OUT" "1 process(es), 8 step(s), 5 use case(s) (1 standalone), 4 wireframe(s), 0 error(s), 1 warning(s)" +has "a wireframe on no step is a warning, not an error" "$OUT" "WARN 1 wireframe(s) on no process step: Orders.Settings_Edit" + +echo "== T2: anchor derives four, keeps two judged, leaves none unanchored ==" +OUT="$(run anchor --project "$P")"; RC=$? +check "exit 0" "$RC" "0" +has "stat line" "$OUT" "anchors: 6 item(s), 4 derived, 2 judged, 2 global, 0 unanchored" +A="$(cat "$P/design/question-anchors.json")" +has "use case lists the question: Q01 on the submit step" "$A" '"basis": "F001:Q01 -> F001:UC001 -> P1-01"' +has "cited source lines overlap: Q02 on the approve screen step" "$A" '"basis": "F001:Q02 cites the lines of F001:UC002 -> P1-03"' +has "named screen: Q03 on the order list step" "$A" '"basis": "names screen Order_Overview -> P1-08"' +has "ASSUMED register row becomes a decision item" "$A" '"id": "D3-01"' +has "judged global anchor survives a re-run" "$A" '"bucket": "Money"' +hasnt "CONFIRMED decisions are not pinned" "$A" "Module structure" + +echo "== T3: build writes a self-contained, deterministic board ==" +OUT="$(run build --project "$P")"; RC=$? +check "exit 0" "$RC" "0" +B="$P/design/process-board.html" +[ -s "$B" ] && ok "design/process-board.html written" || bad "design/process-board.html written" "no file" +H="$(cat "$B" 2>/dev/null)" +has "app name in the page" "$H" "Order desk" +has "process title embedded" "$H" "Order approval: submit, approve or reject, notify" +has "wireframe embedded for thumbnails" "$H" "Approve order" +hasnt "wireframe scripts are stripped" "$H" "stripped from thumbnails" +hasnt "no external script" "$H" '