diff --git a/CHANGELOG.md b/CHANGELOG.md index 5592c10d..c1a77322 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-map): **Process map: one swimlane map per business process, each step on its wireframe once screens exist, every open question and ASSUMED decision pinned to the step it is about; drawn twice, a draft at Stage 2 close without screens (pass 1, shown at CAC-3) and the full map at Stage 3 with screens (pass 2, walked at the Stage 3 gate close-out).** `skills/process-map.md` (when, model, anchoring, build, verify by looking) and `project-bin/process-map.js` (`validate`, `anchor`, `build`; Node stdlib, deterministic) with its template. Named "map" because "process" in the progress report means the toolkit's own stages, while this shows the app's business process. With no wireframes, a step may name a screen that is not drawn yet or have none, and a person step stays a person step; once a wireframe exists `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-map` opt-in manifest row (an existing model counts as opting in), the `swimlanes` opt-in row (the model discharges it), CAC-3 and CAC-4, and a check_stage_3 FAIL when the map is older than its model, its anchors or a wireframe. `tests/wave2/test-process-map.sh` on a synthetic order approval fixture with its own committed BRD, covering both passes. Field evidence: a requirements-driven rebuild POC, 2026-10-16, where the reviewer could not tell where the sixty questions fit; the map placed 102 items with 0 unplaced. — MendixMau - process(testing): **unit tests are chosen per flow, not per module: a module brief lists `Unit flows:` (one `- ACT_X: reason` line each) and `affected-tests.sh` runs only the listed flows the change touched.** Follow-up to #417 on the maintainer's policy that unit-testing everything is overkill (logic and integrations only, UI journeys catch the rest). Module done runs the `--module` modules' listed flows only; a list wins for its module even under `Unit tests default: yes`; bullets (`-`, `*`, `+`, `1.`, `1)`) may be separated by blank lines, carry `(note)` or `Module.` forms, or sit inline (`Unit flows: A: money, B: rest`), a `## Unit flows` heading works with or without a colon, a line that cannot be read warns with its text, and a Unit flows block that yields no flow warns; under default yes a test is dropped only when every module it names has a list that excludes it; CRLF briefs are read; `Unit tests: yes` or `Unit modules:` with no list keeps the module-level selection; a flow listed without a reason, with no test, with a different casing than the model, or not in the model (`SHOW MICROFLOWS IN`), and a module the model lacks, warn. Test: wave2 `test-affected-tests.sh` L10 (35 checks). Baseline unchanged at 75,194 words (budget 80,000) — MendixMau - fix(sync-project, doctor): **a project whose `.claude/settings.json` lacks the `look-ledger.sh seen` hook now gets it back on the next sync, and `doctor.sh` warns when it is missing.** In a lab project the look ledger read "0 of 31 pages seen" while 22 of them had screenshots: `install-claude-permissions.sh` (the only writer of the hook) had never run there, `sync-project.sh` only printed a warning in a long report, and `doctor.sh` checked the allow-list but not the two hooks, so `seen.tsv` stayed empty and any gate built on the ledger would have stalled the build. `install-claude-permissions.sh` gains `--hooks-only` (add the SessionStart and PostToolUse hooks, touch no allow, deny or ask entry, prune nothing, write nothing to `settings.local.json`); `sync-project.sh` runs it when the hooks are missing (`--dry-run` says what it would add); `doctor.sh` names the missing hook and the fix. `tests/wave2/test-look-ledger-hook-repair.sh` reproduces the gap, then proves detect, repair, idempotence and that a Read of a screenshot writes a seen row. Review follow-up: sync repairs only a project that installed the profile before (sidecar with a non-empty allow, or `.mxtk/permission-profile`; checked by `--consent-check`), otherwise it WARNs with the one-line `--hooks-only` command; `.mxtk/no-hook-repair` is the documented opt-out (sync and doctor); `--hooks-only` creates no empty allow/deny, leaves no `.bak`, keeps the file's indent and non-ASCII, and leaves a hand-formatted file untouched (exit 3, lines printed). — lab project field run (night 2026-10-16) - new(bug-log): **`docs/bugs.jsonl` per project plus `bin/file-bugs.sh` to file it safely to mendixlabs/mxcli or this repo.** A session that hits or works around a tool bug appends a row (`--add`, next free `PBUG-NN`, project-local so it never collides with the toolkit's BUG-n; the public number is the issue; the one-line instruction sits in learned-detection-gaps.md). The script normalises Unicode (NFKC, zero-width and lookalike letters, names split by a line break), redacts every title and body (home and spaced paths, user and host names, emails, IPv4/IPv6, tokens, key=value, JSON and `--flag value` secrets, hex tokens, AWS keys, any dotted hostname, server names from config files, URLs other than the two repos matched exactly and cut to the repo prefix plus /issues/N or /pull/N, a URL scheme from a fixed list (jdbc, postgres, mysql, mongodb, redis, amqp, ssh, git and the like) stays and loses the host, any other scheme becomes ``, the project's .mpr, module, entity, attribute, page and microflow names (distinctive ones inside identifiers; ordinary-word and short names only as whole words or segments, never inside mxcli verbs, toolkit file names or other words), Authorization and cookie header values, `$LEAKGUARD_DENY` as a regex plus the project's .leakguard-deny), builds the versions line from version-shaped strings only, scans again with the same matcher and refuses a bug that still looks unsafe: a refused bug gets no duplicate search, no URL and no text in `--links`/`--json`. Default is a dry run with likely duplicates; posting needs `--file PBUG-NN` or `--all` and a y/N per bug read from /dev/tty only (no stdin test hook; a pty cannot be told from a human, so the shipped permissions deny `file-bugs.sh` and `file-bugs.py` with `--file` or `--all`, abbreviations are refused, and a human runs posting with `!`), and a deny list must exist and be valid (an invalid pattern counts as missing) or posting, `--links` and `--json` are refused, passes `--repo= --title= --body-file=` to gh, keeps the log's file mode; `--links` caps title and body under the URL limit. `tests/wave2/test-file-bugs.sh` covers each case with a stubbed gh and a pty helper — MendixMau diff --git a/README.md b/README.md index 867a1b4b..95f9a546 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` | +| At Stage 2 close (draft, before the questions are raised) and again after the wireframes (Stage 3 re-render), when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane map per process, each step on its wireframe, every question pinned to a step (project-bin/process-map.js validate, anchor, build) | `skills/process-map.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 a2a3f1de..e07ec03c 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 | +| At Stage 2 close (draft, before the questions are raised) and again after the wireframes (Stage 3 re-render), when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane map per process, each step on its wireframe, every question pinned to a step (project-bin/process-map.js validate, anchor, build) | `skills/process-map.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..a651dda6 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-map.md` | At Stage 2 close (draft, before the questions are raised) and again after the wireframes (Stage 3 re-render), when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane map per process, each step on its wireframe, every question pinned to a step (project-bin/process-map.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 8f884a96..3118046d 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-map.md` | At Stage 2 close (draft, before the questions are raised) and again after the wireframes (Stage 3 re-render), when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane map per process, each step on its wireframe, every question pinned to a step (project-bin/process-map.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 311016ba..87fb892e 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -1159,6 +1159,24 @@ check_stage_3() { return fi done + # The process map (skills/process-map.md) is optional, but once its model exists the + # render must be current: a reviewer signs off on what the map shows, so a map older than + # its model, its anchors or a wireframe (the thumbnails) 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-map.html" + if [ ! -s "$pb_html" ]; then + echo "FAIL|design/process-model.json exists but design/process-map.html does not; render it (process-map.md Step 4: process-map.js build)" + return + fi + for pb_src in "$pb_model" "$(dirname "$pb_model")/question-anchors.json" "$(dirname "$pb_model")"/wireframes/*.html; do + if [ -f "$pb_src" ] && [ "$pb_html" -ot "$pb_src" ]; then + echo "FAIL|design/process-map.html is older than $(basename "$pb_src"): stale render at a sign-off gate; regenerate (process-map.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-map.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/unit-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/unit-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/demo-recording-polish.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..6808cadd 100644 --- a/bin/lib/artifact-manifest.tsv +++ b/bin/lib/artifact-manifest.tsv @@ -109,7 +109,8 @@ 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 map model of skills/process-map.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 +process-map 3 design/process-map.html skills/process-map.md Step 4 (project-bin/process-map.js build; generated from design/process-model.json, never hand-edited) bin/gate-check.sh:1162-1181 (check_stage_3 freshness against design/process-model.json and its anchors), skills/checkpoints/checkpoint-design.md:16 (CAC-4 uses it to walk the questions; pass 2 at the Stage 3 gate close-out) migration,requirements,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 729457e2..56e46064 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 check-staged-secrets.sh" +# process-map.js + process-map.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-map.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-map.js process-map.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 check-staged-secrets.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 a3634c94..0461fcfd 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-map skills/process-map.md At Stage 2 close (draft, before the questions are raised) and again after the wireframes (Stage 3 re-render), when a reviewer cannot tell where the open questions and ASSUMED decisions fit: one swimlane map per process, each step on its wireframe, every question pinned to a step (project-bin/process-map.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-map.js b/project-bin/process-map.js new file mode 100644 index 00000000..9957a6a1 --- /dev/null +++ b/project-bin/process-map.js @@ -0,0 +1,506 @@ +#!/usr/bin/env node +// process-map.js: draw the Stage-3 process map. One swimlane map 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 map 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 map is GENERATED, never hand-edited, like blueprint.html: +// design/process-model.json authored by the architect agent from the BRDs +// (skills/process-map.md Step 2) +// written by `anchor` (derived) plus the agent's judged pass +// (skills/process-map.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-map.js validate [--strict] check the model (and anchors, when present) +// process-map.js anchor derive anchors mechanically, keep judged ones, write +// the anchors file, list what is still unanchored +// process-map.js build validate, then write design/process-map.html +// options: --project --model --out --json (anchor: todo as JSON) +// +// Exit: 0 ok; 1 validation errors (or unanchored items left: always for anchor, with --strict for validate); 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-map: ' + 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-map.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-map.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 : []; +// Pass 1 (Stage 2 close) has no wireframes at all: a step may name a screen that is not drawn yet, +// or no screen, and renders as a labelled card without a thumbnail. Pass 2 is strict again. +const PASS1 = Object.keys(WF).length === 0; +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 }; + // A person step is a task even before it has a screen (pass 1 has no wireframes); only a step + // in a system, scheduled or external lane, or with an explicit kind, is automatic. + const laneKind = (lanes[laneIdx[s.lane]] || {}).kind || 'role'; + s.kind = s.kind || (s.page || laneKind === 'role' ? '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 (PASS1) { s._pageMissing = true; } + 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' && !PASS1) { + 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 onMap = !!ucToSteps[ref], alone = ref in standaloneSet; + if (!onMap && !alone) err('use case ' + ref + ' (' + UC[ref].title + ') is on no process and not listed as standalone'); + if (onMap && alone) warn('use case ' + ref + ' is on a process and also listed as standalone'); +} +// ignorePages: wireframes that are layout sketches, not screens; they need no step. +const ignoreSet = new Set(); +for (const ig of model.ignorePages || []) { + const k = resolvePage(ig); + if (k) ignoreSet.add(k); else warn('ignorePages: "' + ig + '" matches no wireframe'); +} +const pagesOffMap = Object.keys(WF).filter(k => !pageToSteps[k] && !ignoreSet.has(k)).sort(byteSort); +if (pagesOffMap.length) warn(pagesOffMap.length + ' wireframe(s) on no process step: ' + pagesOffMap.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); + // Pass 1 has no wireframes to resolve a screen name against, so a step's own `page` name stands in. + if (PASS1) for (const { step: st } of Object.values(STEP)) { + if (!st.page) continue; + const sh = shortOf(st.page); + if (tok(all, st.page) || (/_|[a-z][A-Z]/.test(sh) && tok(all, sh))) hit(st.id, 2, 'names screen ' + sh + ' -> ' + st.id); + } + 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'; + +// With --json, stdout carries only the JSON; status lines go to stderr. +const say = (...a) => (asJson ? console.error(...a) : console.log(...a)); +function report() { + for (const w of warnings) say('WARN ' + w); + for (const e of errors) say('ERROR ' + e); + const ucs = Object.keys(UC).length; + say('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)'); + say(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-map.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(); + say('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)); + } + } + // exit 1 while items remain (not an error: keep judging until 0 unanchored) + process.exit(errors.length || unanchored.length ? 1 : 0); +} + +// ---------- build ---------- +report(); +if (errors.length) { console.error('process-map: not building, fix the model first'); process.exit(1); } +const outFile = outArg ? path.resolve(outArg) : path.join(designDir, 'process-map.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-architecture.md b/skills/checkpoints/checkpoint-architecture.md index da7d9c2d..fd50030c 100644 --- a/skills/checkpoints/checkpoint-architecture.md +++ b/skills/checkpoints/checkpoint-architecture.md @@ -19,16 +19,16 @@ before any design system), the governing skills, and the optional artifacts on o ## What to Surface Pull from validated `F{NNN}.brd.json` files: -- Total: entities, enumerations, microflows, pages, cross-module associations -- Any entity referenced by 3+ BRDs (candidate for a Common/Shared module) -- Any BRD with 0 cross-module dependencies (can be built independently) +- Entities, enumerations, microflows, pages, cross-module associations +- Entities used by 3+ BRDs (Common module candidates) +- BRDs with no cross-module dependencies (independent) - Resolved open questions that affect architecture (e.g. auth strategy, data ownership) +- Many questions: draft `process-map.md` (pass 1) ## What's Next `modularize-domain.md` maps feature BRDs to Mendix module boundaries — this is NOT a 1:1 mapping. -The decisions made here determine module names, cross-module associations, and the Common module -strategy before `.mx-brd.json` files are written. +These decisions set module names, cross-module associations and the Common module strategy before `.mx-brd.json` files are written. --- diff --git a/skills/checkpoints/checkpoint-design.md b/skills/checkpoints/checkpoint-design.md index 7e2b0852..992ce81f 100644 --- a/skills/checkpoints/checkpoint-design.md +++ b/skills/checkpoints/checkpoint-design.md @@ -8,28 +8,29 @@ ## What to Surface -Pull from `.mx-brd.json` files and source KB screens: -- Total screens/pages in scope +Pull from `.mx-brd.json` and source KB screens: +- Screens/pages in scope - 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) +- Source layout patterns (sidebar, top nav, card grid, list-detail, modal forms) +- Screens with no direct Atlas widget equivalent (flag) +- `process-map.md`, if drawn: walk questions by step; pass 2 at the Stage 3 gate ## What's Next `design-artifacts.md` produces the design system decisions (Atlas layout, spacing tokens, component naming) andted without design decisions produce -generic Atlas output that will need rework. +generic Atlas output needing rework. --- ## Brainstorm First (before any predefined question) -Design direction is the checkpoint users most often want to *talk about*, not pick from a list. +Users most want to *talk about* design direction, not pick from a list. Open with a divergent conversation: show references (the source app's look, the client's brand, 2–3 mood directions described concretely or as quick HTML swatches — with no client brand, take them from the ideation library when the user says yes to it, `design-artifacts.md` Step 0b -question D1) and discuss — what should -this app *feel* like, what must carry over from the brand, what should deliberately change? +question D1) and discuss — what should this app *feel* like, +what carries over from the brand, what changes? If the user says anything like "let's ideate on the design" at any point, this conversation IS the response — do not generate the design system or launch agents until it converges and the user says to proceed. Then run the predefined questions to record the direction. diff --git a/skills/conversion-runbook.md b/skills/conversion-runbook.md index f5800c02..d4f5f27a 100644 --- a/skills/conversion-runbook.md +++ b/skills/conversion-runbook.md @@ -566,7 +566,7 @@ late against the BRD set you already have. | | | |---|---| | **User defines** | Confirms business rules the code implies. Answers `openQuestions` (via SME). Narrative is never invented. | -| **Agent produces** | BRD scaffolds → enrichment from `KB.md` → validation to clean. `F{NNN}.brd.json`. | +| **Agent produces** | BRD scaffolds → enrichment from `KB.md` → validation. `F{NNN}.brd.json`. Many questions: draft `process-map.md` (pass 1). | | **Surface** | `analysis/brd-report.html` — `bin/brd-report.sh `. One renderer for every entry mode: it reads BRDs, not source, so a requirements-driven or greenfield project gets the same surface as a migration, and so does a BRD no extractor produced. Each section carries a `report-schema.md` verdict, so a section that is empty because this kind of BRD has nothing to put there reads differently from one that is empty because something broke. History: `process/baseline-incident-history.md` §R20. | | **Gate** | Every BRD validation-clean; `validation-report.md` has 0 issues; **every `openQuestions` entry raised in chat** — `bin/open-questions.sh --stage 2` reports 0 blocking, which `gate-check.sh` enforces. Close-out block pasted (§1b rule 7) — CAC-3 opens with it. "Chased to closure" was the old wording and it was unenforceable: a question logged with a self-answer read as closed. | | **Owner** | `ba-agent` | @@ -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 and/or agent-wiring diagram 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; a single-persona skip is noted in one line), `design/` per `design-artifacts.md`: `design/interaction-model.md` (first) + `ds.css` + `design-system.html` + **`wireframes/*.html`, one annotated wireframe per screen** — the design system without wireframes is half the deliverable and fails the gate. Many questions: `process-map.md` pass 2. | +| **Surface** | `module-design.html` · `architecture/blueprint.html` (generated from `blueprint.md`, architecture-blueprint.md Step 7, never hand-edited) · `design-system.html` + `wireframes/*.html` · `process-map.html` | | **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-map.md b/skills/process-map.md new file mode 100644 index 00000000..f28550b8 --- /dev/null +++ b/skills/process-map.md @@ -0,0 +1,166 @@ +# Process Map: Where Each Question Fits in the Work +**Name:** this is the app's business process, drawn from its BRDs. It is not the progress report, which shows the toolkit's own stages. +**Applies to:** migration, requirements-driven build and existing-app change. Two passes: a draft at Stage 2 close (processes and question pins, no screens yet), the full map at Stage 3 once the wireframes exist. +**Requires:** Node 18 or later (no packages). Windows: Git Bash, same commands. +**Purpose:** Draw one swimlane map 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 map. +**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-architecture.md` (CAC-3 shows pass 1); `checkpoints/checkpoint-design.md` (CAC-4 uses it; pass 2 is walked at the Stage 3 gate); `bin/gate-check.sh` check_stage_3 (fails a map older than its model, anchors or wireframes); `journey-proof.md` (a process on the map is a journey to prove). +**Companion:** `architecture-blueprint.md` Step 3d (journey diagrams; the map is the screen-level version of the same picture and discharges the optional `swimlanes` artifact); `assemble-prototype` (the map 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 + +Two passes over one model. The hand work (Steps 1 to 3) happens once, at Stage 2 close; Stage 3 only re-runs the script. + +- **Pass 1, Stage 2 close.** Right after the BRDs validate and the open questions are consolidated, before they are raised with the user. Author the model and the anchors, build the map. With no wireframes in `design/wireframes/`, `validate` allows a step to name a screen that is not drawn yet (shown as a label with "No wireframe yet") or to have no `page`; a step in a role lane is still a person step (a solid card), only steps in system lanes or marked `kind: system` are dashed automatic cards. As soon as one wireframe exists the strict rules apply again. This is the moment the reviewer has to answer the questions, so this is when they need to see where each one fits. The draft is also input for Stage 3: the processes and lanes feed module boundaries (`modularize-domain.md`), the journeys (`architecture-blueprint.md` Step 3d) and the screen list (`design-artifacts.md`). +- **Pass 2, Stage 3.** After the wireframes, set `page` on any step that gained a screen, re-run `anchor` for new questions and `build`. No re-authoring. The Stage-3 gate checks this pass (map present and not older than its model, its anchors or any wireframe). + +Do it 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-map.js anchor` plus you, Step 3 | where each item is pinned. Path is `model.anchors` if set. | +| `design/process-map.html` | `process-map.js build`, Step 4 | generated, never hand-edited, self-contained, light and dark. | + +The script is `project-bin/process-map.js` (installed into a wired project's `bin/`). When the project `bin/` has no copy (an older or scratch project), call it by its toolkit path, `node /project-bin/process-map.js`. 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. Keep a process to at most 10 to 12 steps; a longer one reads better split in two. 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`. + +Rule of thumb for dashboards and exports: they are steps inside the process that produces the data, unless they have their own actor and their own trigger. Then they are a process of their own. A screen reached from several flows is a step in the most used flow. + +## Step 2: Write design/process-model.json + +```json +{ + "schema": "process-model/1", + "app": "Order desk", + "questions": "", + "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` once wireframes exist), `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 or the lane is a `role` lane, 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 `":"`. The part before the colon is the BRD's `id` field. When the use case ids carry their own prefix, it is `"F004:UC-F004-01"`. 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 (look under the analysis questions folder; omit the key when the project keeps none), 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. Expect many of them to be build plan and environment decisions; most end in a global bucket. +- **ignorePages** (optional): a list of wireframe pages (`Module.Page`, short name or filename) that are layout sketches, not screens. They need no step and do not trigger the "wireframe on no step" warning. A name that matches no wireframe warns. + +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-map.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-map.js anchor # derive, keep judged entries, write the anchors file +node bin/process-map.js anchor --json # the still-unanchored items as skeletons to fill in (pure JSON on stdout, status lines on stderr) +``` + +`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. +- Standard bucket names for global items, so the whole-app list stays short: `Build plan`, `Hosting and operations`, `Security`, `Data`, `Scope`, `Look and feel`, `App structure`. ASSUMED register rows about build, environment, security, data or scope usually land here. +- 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. + +Judged entries go into the `anchors` array of the anchors file, next to the derived ones. The skeletons from `anchor --json` already carry `anchoredBy: "judged"` and empty `targets`: fill `targets` and `basis` in place and keep `id`, `kind` and `fingerprint` (`text` can go). + +Re-run `anchor` until it reports `0 unanchored`. `anchor` exits 1 while items remain; that is expected, not an error. `validate --strict` fails on any unanchored item. + +## Step 4: Build + +```bash +node bin/process-map.js build +``` + +Validates first and refuses on errors. Writes `design/process-map.html`: an index of process cards (a mini map of lanes, pin counts) plus the whole-app bucket, and one map 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 map is older than `process-model.json`, `question-anchors.json` or the newest wireframe. + +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 maps, 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-map.html" +shot() { perl -e 'alarm 120; exec @ARGV' "$CHROME" --headless=new --hide-scrollbars --virtual-time-budget=8000 "$@"; } # Linux: timeout 120 +shot --window-size=1800,1400 --screenshot=/tmp/map-index.png "$B#/" +shot --window-size=3000,1300 --screenshot=/tmp/map-P1.png "$B#/P1" +``` + +Use `--virtual-time-budget` and wrap the call in a timeout as shown. Never use Chrome's `--timeout` flag: it made the run hang past two minutes. A map with 10 to 12 steps fits 3000 px; for a wider one, split the process or screenshot at 4000 px. + +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 map for the user unasked; give the path. + +## Gate + +At the Stage 3 gate close-out (Stage 3 has no closing checkpoint; pass 1 was shown at CAC-3), walk the map 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 map path in the close-out block. diff --git a/tests/wave2/fixtures/process-map/PROJECT.md b/tests/wave2/fixtures/process-map/PROJECT.md new file mode 100644 index 00000000..1295a5de --- /dev/null +++ b/tests/wave2/fixtures/process-map/PROJECT.md @@ -0,0 +1,16 @@ +# PROJECT.md: synthetic process map 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-map/brd/F001-orders.brd.json b/tests/wave2/fixtures/process-map/brd/F001-orders.brd.json new file mode 100644 index 00000000..7a92b1b8 --- /dev/null +++ b/tests/wave2/fixtures/process-map/brd/F001-orders.brd.json @@ -0,0 +1,23 @@ +{ + "id": "F001", + "title": "Orders and approval", + "useCases": [ + { "id": "UC001", "title": "Submit an order", "actors": ["Requester"], "screens": ["Order_NewEdit"], "microflows": ["ACT_Order_Submit"], "openQuestions": ["Q01"], "source": "src/orders/submit.ts:10-40" }, + { "id": "UC002", "title": "Approve or reject an order", "actors": ["Approver"], "screens": ["Order_Approve"], "microflows": ["ACT_Order_Approve", "ACT_Order_Reject"], "openQuestions": [], "source": "src/orders/approve.ts:5-60" }, + { "id": "UC003", "title": "Notify the requester", "actors": ["System"], "microflows": ["SUB_Order_Notify"], "openQuestions": [], "source": "src/orders/notify.ts:1-30" }, + { "id": "UC004", "title": "Find past orders", "actors": ["Requester"], "screens": ["Order_Overview"], "openQuestions": [], "source": "src/orders/list.ts:1-20" }, + { "id": "UC005", "title": "Nightly cleanup of abandoned drafts", "actors": ["System"], "openQuestions": [], "source": "src/jobs/cleanup.ts:1-15" } + ], + "openQuestions": [ + { "id": "Q01", "question": "Is there a maximum order amount a requester may submit?", "assumption": "No maximum.", "blocking": true, "source": "src/orders/submit.ts:22" }, + { "id": "Q02", "question": "May an approver approve their own order?", "assumption": "No, the approver must be someone else.", "source": "src/orders/approve.ts:30-34" }, + { "id": "Q03", "question": "Should the history list on Order_Overview show cancelled orders?", "assumption": "Hidden by default." }, + { "id": "Q04", "question": "Which currency do amounts use?", "assumption": "One currency for the whole app." } + ], + "businessRules": [], + "pages": [ + { "name": "Order_NewEdit", "route": "order-new-edit" }, + { "name": "Order_Approve", "route": "order-approve" }, + { "name": "Order_Overview", "route": "order-overview" } + ] +} diff --git a/tests/wave2/fixtures/process-map/design/ds.css b/tests/wave2/fixtures/process-map/design/ds.css new file mode 100644 index 00000000..2a14957d --- /dev/null +++ b/tests/wave2/fixtures/process-map/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-map/design/process-model.json b/tests/wave2/fixtures/process-map/design/process-model.json new file mode 100644 index 00000000..1d3ec2d0 --- /dev/null +++ b/tests/wave2/fixtures/process-map/design/process-model.json @@ -0,0 +1,146 @@ +{ + "schema": "process-model/1", + "app": "Order desk", + "brdDirs": [ + "brd" + ], + "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-map/design/question-anchors.json b/tests/wave2/fixtures/process-map/design/question-anchors.json new file mode 100644 index 00000000..eb1e01d4 --- /dev/null +++ b/tests/wave2/fixtures/process-map/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-map/design/wireframes/Order_Approve.html b/tests/wave2/fixtures/process-map/design/wireframes/Order_Approve.html new file mode 100644 index 00000000..ca7761a5 --- /dev/null +++ b/tests/wave2/fixtures/process-map/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-map/design/wireframes/Order_NewEdit.html b/tests/wave2/fixtures/process-map/design/wireframes/Order_NewEdit.html new file mode 100644 index 00000000..419261ff --- /dev/null +++ b/tests/wave2/fixtures/process-map/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-map/design/wireframes/Order_Overview.html b/tests/wave2/fixtures/process-map/design/wireframes/Order_Overview.html new file mode 100644 index 00000000..7ca3e7a6 --- /dev/null +++ b/tests/wave2/fixtures/process-map/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-map/design/wireframes/Settings_Edit.html b/tests/wave2/fixtures/process-map/design/wireframes/Settings_Edit.html new file mode 100644 index 00000000..3a037872 --- /dev/null +++ b/tests/wave2/fixtures/process-map/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-map.sh b/tests/wave2/test-process-map.sh new file mode 100755 index 00000000..1867c20e --- /dev/null +++ b/tests/wave2/test-process-map.sh @@ -0,0 +1,208 @@ +#!/bin/bash +# test-process-map.sh: pin what process-map.js validates, how it anchors questions, what +# `build` writes, and the Stage-3 gate rule that a process map must not be older than its model. +# +# Golden input: tests/wave2/fixtures/process-map/, 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-map.sh [path-to-process-map.js] + +set -u + +TOOLKIT="$(cd "$(dirname "$0")/../.." && pwd)" +SUT="${1:-$TOOLKIT/project-bin/process-map.js}" +SUT="$(cd "$(dirname "$SUT")" && pwd)/$(basename "$SUT")" +GATE="$TOOLKIT/bin/gate-check.sh" +FIX="$TOOLKIT/tests/wave2/fixtures/process-map" +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 map ==" +OUT="$(run build --project "$P")"; RC=$? +check "exit 0" "$RC" "0" +B="$P/design/process-map.html" +[ -s "$B" ] && ok "design/process-map.html written" || bad "design/process-map.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" '