diff --git a/CHANGELOG.md b/CHANGELOG.md index e60da790..da88a47a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m - fix(brd-report, gate-check): **Stage 2 no longer trusts a self-declared "Clean" on requirements-driven BRDs** — `bin/brd-report.sh` now runs brd-validation.md's mechanical subset over every non-code-extracted BRD (complete use cases, entity references that resolve, one owner per entity and business-rule id, rule ids — read from `{id}` or a leading token of a string rule (`BR001: …`), tracked open questions; each with its denominator; reserved names as warnings) and Stage 2 FAILs a Clean report it contradicts (`--waive artifact/brd-validation --reason`) (#622) — a requirements-driven field run (Path B, 9 hand-written BRDs) - fix(gate-check, interview-protocol): **Stage 3/4 can pass an unattended run, and nothing ships on it** (#624): under `Interview mode: auto` (alias `unattended`, as `bin/interview-mode.sh` resolves it) a Stage 3/4 Decisions row recorded `ASSUMED` with `consentBy: auto-mode` and a `consentAt` satisfies the ✋ gate; in every other mode the gate is `CONFIRMED`-only as before (its FAIL line now says when an auto-mode row is the reason), and Stage 7 cutover stays `CONFIRMED`-only. Every run, any mode, prints `N ✋ decision(s) assumed by auto-mode — confirm before release: ` (Stage 0/3/4/7 rows still stamped auto-mode, plus a triage.md signed by auto mode), and Stage 6 FAILs while one remains — a stage-6 waiver does not excuse it. A person confirms or overturns each: any status, `consentBy` naming them. Protected-rule change ("✋ gates never resolve to ASSUMED"), maintainer decision 2026-11-05; rule in `interview-protocol.md` §"What `auto` obliges you to do", one-line pointers in the runbook, ba/architect stubs, `agents/README.md`, `architecture-blueprint.md`, `checkpoint-template.md`, `CONVERSION-RUNBOOK.md` (baseline 64,891 → 65,053 words). New fixture `tests/wave2/test-auto-mode-hand-gates.sh`. Field run: a copy of a real unattended Stage-4 purchasing-app project that had waived Stages 3/4 to get past the gate — waivers removed, 3/4 FAIL → PASS, 11 auto-mode ✋ decisions listed, Stage 6 FAIL with test surfaces present, PASS once confirmed. — Mijn Roostercommissie field run (#624) +- new(gate-check, interaction-model): **Stage 3 now fails on an interaction model that leaves out a user task or a wireframe's page parameter, and asks about BRD actors with no home** ([#291](https://github.com/MendixMau/mxcli-project-toolkit/issues/291), part 2). Once `design/interaction-model.md` exists, `bin/lib/interaction-model-check.sh` (called by `gate-check.sh`) lists one FAIL per BRD approval/review/sign-off/user-task use case not named on any line (row or bullet) under an Inbox heading, and per `design/wireframes/*.html` without a `Page parameter:` line (`Module.Entity`, a trailing `(note)` allowed, or `none — `) in its `wf-contract` block; any of them fails Stage 3. A BRD actor with no "Role homes" row is MANUAL (confirm it is a user role, or add a `none — ` row), never FAIL. Findings are waived by hand in the register (`Waived artifact interaction-model/: reason`); a project adopted above stage 3 reports ADOPTED. A new `artifact-manifest.tsv` row reports the artifact PENDING when it is absent, which does not block. `checkpoint-design.md` asks "who lands where, and how does each role find its work" next to the layout question; `brd-to-build-plan.md` Step 7 reads wire points from the artifact. Fixture `tests/wave2/test-interaction-model-gate.sh` (synthetic trees only). Brought up to date with master 2026-11-03; run against six Stage-3 worker outputs, which found two defects, both fixed: a MANUAL role finding no longer hides the "no Stage-3 CONFIRMED decision" FAIL (it was reported MANUAL, exit 2), and a role cell with a trailing note in parentheses (`Owner (signed-in …)`) now names the role. After review: a "Homes and inboxes" heading with a `Home` column reads as the Role homes table. Fixture cases T13–T18. A field run on a client project is still owed — field retro (#291) - fix(gate-check): **Stage 0 no longer passes a `_pending_` sign-off (#620)**: the triage `Confirmed by:` check rejected only a `[`, so `_pending_`, `TBD`, `n/a`, `—` or `?` signed a human gate off. A whole-value "not yet" (markdown emphasis stripped, case ignored) now FAILs and says to ask the user; "confirmed in chat, pending legal review" still passes — Mijn Roostercommissie field run - fix(obligations): **`design-reaches-app` is owed from Stage 5, not 3 (#627)**: its only producer (check-design-reaches-app.sh against the BUILT stylesheet, ui-preflight-pages.md B0) runs at the first build, so every Stage 3/4 summary carried a PENDING nothing honest could clear. The routing row stays at 3,5 — Mijn Roostercommissie field run - fix(gate-check, render-routing): **the obligations reverse check stops printing in consumer projects (#623)**: "give it an obligation row in obligations.tsv" is a message to the toolkit maintainer and appeared in every project's gate output from Stage 2 on. It now runs in `render-routing.sh --check` (CI) and in gate-check only against the toolkit itself or under `MXTK_AUTHORING=1` — Mijn Roostercommissie field run diff --git a/bin/gate-check.sh b/bin/gate-check.sh index 5ebfa8b4..61bb4230 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -1641,10 +1641,21 @@ check_stage_3() { fi done fi + # The interaction-model facts (#291), computed once before the stage loop and printed under + # "Interaction model (stage 3 facts)". PENDING (no artifact yet — the Artifacts block reports + # that) and ADOPTED/WAIVED leave this verdict alone; a finding is something examined and wrong. + # A FAIL returns here; a MANUAL only replaces a PASS, so it never hides the register FAIL below. + if [ "${MXTK_IM_STATUS:-}" = "FAIL" ]; then + local im_also="" + if ! reg_unavailable && ! has_confirmed_decision 3 && ! { interview_mode_is_auto && has_auto_assumed_decision 3; }; then im_also="; also: no Stage-3 CONFIRMED decision"; fi + echo "FAIL|interaction model: ${MXTK_IM_NOTE} — each finding is listed under 'Interaction model (stage 3 facts)' (skills/interaction-model.md)${im_also}"; return + fi if reg_unavailable; then reg_unavailable_note; return; fi if has_confirmed_decision 3; then + if [ "${MXTK_IM_STATUS:-}" = "MANUAL" ]; then echo "MANUAL|interaction model: ${MXTK_IM_NOTE}$wf_note"; return; fi echo "PASS|fit-gap, blueprint render, design system, wireframes present and a Stage-3 CONFIRMED decision is in $REGISTER$wf_note" elif interview_mode_is_auto && has_auto_assumed_decision 3; then + if [ "${MXTK_IM_STATUS:-}" = "MANUAL" ]; then echo "MANUAL|interaction model: ${MXTK_IM_NOTE}$wf_note"; return; fi echo "PASS|fit-gap, blueprint render, design system, wireframes present and a Stage-3 decision is ASSUMED by auto-mode in $REGISTER (Interview mode: auto) — a human confirms it before release$wf_note" else echo "FAIL|artifacts exist but ${REGISTER:-PROJECT.md} has no Stage-3 CONFIRMED decision — ✋ gate: artifacts without an interview don't pass$(auto_assumed_hint 3)" @@ -3316,6 +3327,26 @@ else printf 'Artifacts: FAULT — bin/lib/artifact-check.sh missing from %s; no artifact was checked\n' "$TOOLKIT_DIR" fi +# --------------------------------------------------------------------------- +# Interaction model — the Stage-3 facts over design/interaction-model.md (#291). +# +# The artifact check above only notices the file is absent. Once it exists, three facts are +# read from it: every BRD role has a home row, every BRD approval/user-task use case has an +# inbox row (or a stated reason), every wireframe carries its `Page parameter:` line. Each +# finding prints here; check_stage_3() turns any un-waived one into a Stage-3 FAIL. Judgement +# (is that the right home?) stays in skills/interaction-model.md. Waivers and adoption: see the +# header of bin/lib/interaction-model-check.sh. +MXTK_IM_STATUS=""; MXTK_IM_NOTE="" +if [ -r "$TOOLKIT_DIR/bin/lib/interaction-model-check.sh" ] && [ -r "$TOOLKIT_DIR/bin/lib/artifact-check.sh" ]; then + # shellcheck source=lib/interaction-model-check.sh + . "$TOOLKIT_DIR/bin/lib/interaction-model-check.sh" + mxtk_interaction_model_check "$PROJECT_DIR" "$REGISTER" "$ANALYSIS_BASE" +else + MXTK_IM_STATUS="MANUAL" + MXTK_IM_NOTE="bin/lib/interaction-model-check.sh missing from $TOOLKIT_DIR — cannot evaluate, which is not a pass" + printf 'Interaction model (stage 3 facts): MANUAL — %s\n' "$MXTK_IM_NOTE" +fi + # --------------------------------------------------------------------------- # Source sufficiency — was the source actually characterised, or did Stage 0 # pass on nobody having looked? diff --git a/bin/lib/artifact-manifest.tsv b/bin/lib/artifact-manifest.tsv index d4ab8b25..7d820ebb 100644 --- a/bin/lib/artifact-manifest.tsv +++ b/bin/lib/artifact-manifest.tsv @@ -89,6 +89,7 @@ blueprint-html 3 architecture/blueprint.html skills/architecture-blueprint.md St design-system 3 design/design-system.html skills/design-artifacts.md:79 (annotated showcase linking ds.css) bin/gate-check.sh:913 (check_stage_3), bin/gate-check.sh:1135 (build-ready) migration,requirements,existing-app-change gate ds-css 3 design/ds.css skills/design-artifacts.md:75 (the physical token/component source) skills/design-artifacts.md:68 (two-files rule — every wireframe links it), bin/gate-check.sh:1135 (build-ready accepts either file) migration,requirements,existing-app-change report wireframes 3 design/wireframes/*.html skills/design-artifacts.md Step 3, one per screen bin/gate-check.sh:923 (check_stage_3), bin/gate-check.sh:1129 (build-ready), skills/ui-preflight-pages.md (mdl-agent pre-flight input) migration,requirements,existing-app-change gate +interaction-model 3 design/interaction-model.md skills/interaction-model.md (Stage 3, after the role model is CONFIRMED and before the first wireframe — design-artifacts.md Step 2b) skills/design-artifacts.md Step 3 (every wireframe's wf-contract block is copied from its row), skills/brd-to-build-plan.md Step 7 Navigation Wiring (home per role, wire points, page parameters), bin/lib/interaction-model-check.sh (the Stage-3 facts check_stage_3 fails on) migration,requirements,existing-app-change report module-definition 3 architecture/modules/*/definition.md skills/architecture-blueprint.md Step 1 (:60 — the directory shape, one doc per module) THIS ROW IS ITS FIRST MECHANICAL CONSUMER — as of 2026-08-22 nothing executable read definition.md (improvement-plan P7 residue, the newest instance of the pattern this table exists to stop); prose consumers: skills/architecture-blueprint.md:43, skills/coverage-ledger.md:27 migration,requirements,existing-app-change report build-plan 4 architecture/build-plan.md skills/brd-to-build-plan.md bin/gate-check.sh:955 (check_stage_4), project-bin/build-plan-status.sh (progress board), project-bin/coverage-preflight.sh:238 (the claims join) migration,requirements,existing-app-change gate build-plan-html 4 architecture/progress-report.html project-bin/build-plan-status.sh --html (skills/brd-to-build-plan.md:47 — derived view, never hand-edited; refreshed by exec.sh and full gate-check.sh runs via --refresh) conversion-runbook.md §2 Stage-4 Surface row, parsed at bin/gate-check.sh:1284 migration,requirements,existing-app-change report diff --git a/bin/lib/interaction-model-check.sh b/bin/lib/interaction-model-check.sh new file mode 100755 index 00000000..d6b0a10b --- /dev/null +++ b/bin/lib/interaction-model-check.sh @@ -0,0 +1,374 @@ +#!/usr/bin/env bash +# interaction-model-check.sh — the Stage-3 FACTS over design/interaction-model.md (#291, part 2). +# +# WHY THIS EXISTS. Three apps built with this toolkit passed every gate while one of them had +# 8 pages rooted on a parameterless "current record" retrieve, one home page for 6 roles and 3 +# process stages in the menu. Every check after Stage 3 compares the build with the wireframe, +# and the wireframes were drawn as one role looking at one record. skills/interaction-model.md +# now owns the Stage-3 artifact that says who lands where and what object each page is about; +# this file only notices when that artifact leaves a role, a user task or a wireframe out. +# +# WHAT IT IS NOT: a judge. Whether a home is the RIGHT home, whether an inbox groups its rows +# well, whether a page should take a parameter at all — those verdicts stay in the skill +# (skills-over-scripts.md). This reads three facts, and only once the artifact exists: +# (a) roles every actor in the BRDs' top-level `actors` (the one role list a bin/ script +# already reads — bin/brd-report.sh) has a row in the artifact's "Role homes" +# table (columns `Role` | `Home page`, interaction-model.md Step 1) with a +# Home page cell — a page, or `none — ` for an actor that is not a +# user role (system actor, merged into another role). BRD actors are NOT the +# CONFIRMED role model (external systems, roles merged at Stage 3), so a missing +# row is MANUAL — "confirm it is a user role" — never FAIL (review of #332); +# (b) user tasks every BRD use case whose title STARTS with approve / review / sign-off (after +# an optional id prefix such as "UC001:"), whose title contains "user task", or +# whose `mdlRefs` names a user task — brd-generation.md has no dedicated +# user-task key, and the looser /approv/ flagged "View approval history" — is +# mentioned, by its id or its full title, on any line (table row or bullet) +# under a heading that names "Inbox" (interaction-model.md Step 2), as its +# inbox entry or as a line stating why it needs none; +# (c) wireframes every design/wireframes/*.html carries a `Page parameter:` line inside its +# `wf-contract` block (design-artifacts.md Step 3; the block ends at its own +# closing , nested divs counted) whose value — tags stripped, so +# `Page parameter:` and `Sales.Order` read — is +# `Module.Entity` or `none — ` (em dash; en dash, hyphen and `—` +# accepted as the same separator). +# A project that writes the interaction model mid-build gets one FAIL per legacy wireframe on +# the next gate run. That is intended: the FAIL output names the remedy (add the line, or waive +# that wireframe). +# The artifact being ABSENT is not this file's business: bin/lib/artifact-manifest.tsv's +# `interaction-model` row reports it PENDING out loud through the artifact check. +# +# PRODUCER OF WHAT IT READS (CLAUDE.md "Shipping an instrument", rule 5): design/interaction-model.md +# is produced by skills/interaction-model.md, which design-artifacts.md Step 2b makes mandatory +# before the first wireframe; the wf-contract block by design-artifacts.md Step 3; BRDs by any +# Stage-2 route, found through bin/lib/discover-brds.sh. +# +# WAIVERS live in the decision register, in the artifact check's own spelling (artifact-check.sh +# header — written by hand, no new flag): +# Waived artifact interaction-model: decided in the client's own UX workshop (all of it) +# Waived artifact interaction-model/SysAdmin: technical role, never signs in (one finding) +# The unit after the slash is the role name, the use-case id as `/`, or the +# wireframe file name — the same unit the FAIL line prints. Case-insensitive. +# A project adopted ABOVE stage 3 (`Adopted at stage: 4` or later) reports ADOPTED and owes +# nothing here, same rule as artifact-check.sh and obligation-check.sh. +# +# Sourced by bin/gate-check.sh after the artifact check; check_stage_3() reads MXTK_IM_STATUS. +# Also runnable directly: bin/lib/interaction-model-check.sh [register] +# +# Sets: MXTK_IM_STATUS PASS | FAIL | PENDING | WAIVED | ADOPTED | MANUAL +# MXTK_IM_NOTE one-line summary with denominators +# MXTK_IM_N_FAIL count of un-waived findings + +set -uo pipefail + +_IM_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +. "$_IM_LIB_DIR/discover-brds.sh" +# Reuse the artifact check's register readers (_art_field, _art_adopted_stage, _art_rank) rather +# than a fourth copy of the "Label: value" parser. Its direct-run guard keeps it inert here. +type _art_field >/dev/null 2>&1 || . "$_IM_LIB_DIR/artifact-check.sh" + +# _im_waivers — every "Waived artifact interaction-model[/unit]: reason" line, once, +# as "unitreason" (unit empty for the whole-artifact line, lowercased). One awk pass so a +# project with fifty findings does not fork fifty times (the Git Bash cost obligation-check.sh +# documents). +_im_waivers() { + [ -f "$1" ] || return 0 + awk ' + //) c = 0; next } + { + l = $0; sub(/\r$/, "", l); gsub(/^[ \t>*_-]+/, "", l); gsub(/\*/, "", l) + i = index(l, ":"); if (i == 0) next + key = tolower(substr(l, 1, i - 1)); gsub(/^[ \t]+|[ \t]+$/, "", key) + val = substr(l, i + 1); gsub(/^[ \t]+|[ \t]+$/, "", val) + if (val == "") next + p = "waived artifact interaction-model" + if (key == p) { print "\t" val; next } + if (index(key, p "/") == 1) print substr(key, length(p) + 2) "\t" val + }' "$1" +} + +# _im_waived — reason on stdout and rc 0 when the unit (or the whole artifact) is waived. +_im_waived() { + local want line u + want="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" + [ -n "$_IM_WAIVERS" ] || return 1 + while IFS= read -r line; do + u="${line%%$'\t'*}" + if [ "$u" = "$want" ]; then printf '%s\n' "${line#*$'\t'}"; return 0; fi + done < — one awk pass over the artifact: +# HOMErolehome rows of the table under a heading naming "homes" ("Role homes", +# "Homes and inboxes") +# INBOXline every non-blank line (table row, bullet, prose) under any heading +# naming "Inbox" — the skill's own Step 2 writes inboxes as bullets. +# "Homes and inboxes" is read both ways; a "Role homes" heading is +# never an inbox, even when it also names "inbox". +# Cells are trimmed and stripped of backticks and bold. CRLF files read like LF ones. The Role / +# Home page (or Home) columns are found by header name, so a reordered table still reads. +_im_tables() { + awk ' + function clean(s) { gsub(/`/, "", s); gsub(/\*\*/, "", s); gsub(/^[ \t]+|[ \t]+$/, "", s); return s } + { sub(/\r$/, "") } + /^#+[ \t]/ { + h = tolower($0) + sec = "" + if (h ~ /homes/) sec = "home" + if (h ~ /inbox/ && h !~ /role homes/) sec = (sec == "home" ? "both" : "inbox") + hdr = 0; next + } + sec == "" { next } + sec != "home" && /[^ \t]/ { printf "INBOX\t%s\n", clean($0) } + sec == "inbox" { next } + !/^[ \t]*\|/ { hdr = 0; next } + { + line = $0 + n = split(line, cell, "|") + if (hdr == 0) { + hdr = 1; rc = 0; hc = 0 + for (i = 2; i < n; i++) { + c = tolower(clean(cell[i])) + if (c == "role") rc = i + if (c == "home page" || c == "home") hc = i + } + next + } + if (line ~ /^[ \t]*\|[ \t:|-]+\|?[ \t]*$/) next + if (rc == 0) next + # "Owner (signed-in User who owns dashboards)" names the role Owner: a trailing note in + # parentheses is dropped before the match (Stage-3 worker output, 2026-11-03). + role = clean(cell[rc]); sub(/[ \t]*\([^()]*\)[ \t]*$/, "", role) + printf "HOME\t%s\t%s\n", role, (hc ? clean(cell[hc]) : "") + }' "$1" +} + +# _im_wireframes ... — "fileSTATEvalue" per wireframe, one awk pass over all: +# NOBLOCK no element whose class names wf-contract +# NOLINE a wf-contract block, but no "Page parameter:" inside it +# VALUE the text after "Page parameter:" up to the next block-level tag, with inline tags +# (b, strong, i, em, code, span, a) and comments removed and entities decoded +# The block ends at the that closes the wf-contract div — nested
s are counted, +# so a contract with an inner row div is read whole (review of #332). +_im_wireframes() { + awk ' + function flush( low, s, t, rest, blk, depth, od, cd, p, v, e, off) { + if (fname == "") return + gsub(/\r/, "", buf) + low = tolower(buf) + s = match(low, /class=[^>]*wf-contract/) + if (s == 0) { printf "%s\tNOBLOCK\t\n", fname; return } + # back up to the opening "<" of this tag, then step past its ">" + t = s; while (t > 1 && substr(buf, t, 1) != "<") t-- + rest = substr(buf, t); off = index(rest, ">"); if (off == 0) off = length(rest) + blk = ""; rest = substr(rest, off + 1); depth = 1 + while (depth > 0 && rest != "") { + od = index(tolower(rest), " 0 && od < cd) { depth++; blk = blk substr(rest, 1, od + 3); rest = substr(rest, od + 4); continue } + depth-- + if (depth == 0) { blk = blk substr(rest, 1, cd - 1); break } + blk = blk substr(rest, 1, cd + 4); rest = substr(rest, cd + 5) + } + # comments out, inline tags out, every other tag becomes a line break (\001) + while ((e = index(blk, "") + blk = substr(blk, 1, e - 1) (p ? substr(v, p + 3) : "") + } + gsub(/<\/?([bBiIaA]|[sS][tT][rR][oO][nN][gG]|[eE][mM]|[cC][oO][dD][eE]|[sS][pP][aA][nN])([ \t][^>]*)?>/, "", blk) + gsub(/<[^>]*>/, "\001", blk) + p = index(blk, "Page parameter:") + if (p == 0) { printf "%s\tNOLINE\t\n", fname; return } + v = substr(blk, p + length("Page parameter:")) + e = index(v, "\001"); if (e > 0) v = substr(v, 1, e - 1) + gsub(/—|—|—|—/, "\342\200\224", v) + gsub(/–|–|–|–/, "\342\200\223", v) + gsub(/ /, " ", v) + gsub(/[ \t]+/, " ", v); gsub(/^ | $/, "", v) + printf "%s\tVALUE\t%s\n", fname, v + } + FNR == 1 { flush(); fname = FILENAME; buf = "" } + { buf = buf " " $0 } + END { flush() }' "$@" +} + +# mxtk_interaction_model_check [register] [analysis-base] +mxtk_interaction_model_check() { + local root="$1" reg="${2:-$1/PROJECT.md}" base="${3:-$1}" + MXTK_IM_STATUS="PASS"; MXTK_IM_NOTE=""; MXTK_IM_N_FAIL=0 + local im="" b + for b in "$root" "$base"; do + [ -s "$b/design/interaction-model.md" ] && { im="$b/design/interaction-model.md"; base="$b"; break; } + done + if [ -z "$im" ]; then + MXTK_IM_STATUS="PENDING" + MXTK_IM_NOTE="design/interaction-model.md absent — nothing to check; the Artifacts block says whether it is owed yet (skills/interaction-model.md)" + printf 'Interaction model (stage 3 facts): PENDING — %s\n' "$MXTK_IM_NOTE" + return 0 + fi + + local adopted + adopted="$(_art_adopted_stage "$reg" 2>/dev/null || true)" + if [ -n "$adopted" ] && [ "$(_art_rank 3)" -lt "$(_art_rank "$adopted")" ]; then + MXTK_IM_STATUS="ADOPTED" + MXTK_IM_NOTE="project joined at stage $adopted, above stage 3 — the interaction model is not checked here" + printf 'Interaction model (stage 3 facts): ADOPTED — %s\n' "$MXTK_IM_NOTE" + return 0 + fi + + _IM_WAIVERS="$(_im_waivers "$reg")" + local r + if r="$(_im_waived "")"; then + MXTK_IM_STATUS="WAIVED"; MXTK_IM_NOTE="$r (register: Waived artifact interaction-model)" + printf 'Interaction model (stage 3 facts): WAIVED — %s\n' "$MXTK_IM_NOTE" + return 0 + fi + + local lines="" n_fail=0 n_waived=0 manual="" + local tables; tables="$(_im_tables "$im")" + local rel="${im#"$root"/}" + + # ── (a) and (b): roles and user tasks, from the BRDs ────────────────────────────────────── + local brds roles="" tasks="" n_roles=0 n_roles_ok=0 n_roles_open=0 n_tasks=0 n_tasks_ok=0 + brds="$(discover_brds "$root")" + if [ -n "$brds" ]; then + if ! command -v jq >/dev/null 2>&1; then + manual="jq not installed — roles and user tasks could not be read from the BRDs; cannot evaluate, which is not a pass" + else + local f files=() + while IFS= read -r f; do [ -n "$f" ] && files+=("$f"); done </dev/null | awk 'NF && !seen[tolower($0)]++')"; then + manual="a BRD did not parse as JSON — roles could not be read; cannot evaluate, which is not a pass" + fi + if ! tasks="$(jq -r '. as $b | (.useCases // [])[] + | select( + ((.title // "") | tostring + | test("^\\s*([A-Za-z]*[0-9][A-Za-z0-9._-]*\\s*[:.)-]?\\s+)?(approves?|reviews?|sign[- ]offs?)\\b"; "i") + or test("user ?task"; "i")) + or ([(.mdlRefs // [])[]? | tostring | select(test("user ?task"; "i"))] | length > 0)) + | [($b.id // (input_filename | split("/") | last | sub("\\.brd\\.json$"; ""))), (.id // ""), (.title // "")] + | @tsv' "${files[@]}" 2>/dev/null)"; then + manual="a BRD did not parse as JSON — user tasks could not be read; cannot evaluate, which is not a pass" + fi + fi + fi + + local role home row found + if [ -z "$manual" ]; then + while IFS= read -r role; do + [ -n "$role" ] || continue + n_roles=$((n_roles+1)) + home="$(printf '%s\n' "$tables" | awk -F'\t' -v r="$(printf '%s' "$role" | tr '[:upper:]' '[:lower:]')" \ + '$1 == "HOME" && tolower($2) == r { print $3; exit }')" + case "$home" in ""|"—"|"-"|"–"|none|None|NONE) found="" ;; *) found=1 ;; esac + if [ -n "$found" ]; then n_roles_ok=$((n_roles_ok+1)) + elif r="$(_im_waived "$role")"; then + n_waived=$((n_waived+1)) + lines="$lines WAIVED role $role — $r"$'\n' + else + n_roles_open=$((n_roles_open+1)) + lines="$lines MANUAL role $role — BRD actor with no row in the \"Role homes\" table of $rel: confirm it is a user role and give it a home, or add a row \`| $role | none — |\` (system actor / merged into ) (interaction-model.md Step 1)"$'\n' + fi + done <\"" + fi ;; + esac + if [ -z "$why" ]; then n_wf_ok=$((n_wf_ok+1)) + elif r="$(_im_waived "$name")"; then + n_waived=$((n_waived+1)) + lines="$lines WAIVED wireframe $name — $r"$'\n' + else + n_fail=$((n_fail+1)) + lines="$lines FAIL wireframe $name — $why (design-artifacts.md Step 3)"$'\n' + n_wf_fail=$((n_wf_fail+1)) + [ "$state" = VALUE ] && n_wf_bad=$((n_wf_bad+1)) + fi + done <.html: \" — wireframes drawn before the interaction model existed are expected to land here"$'\n' + fi + + local counts + if [ -n "$manual" ]; then + counts="roles/user tasks: NOT CHECKED ($manual); $n_wf_ok of $n_wf wireframes carry a valid Page parameter line" + else + counts="$n_roles_ok of $n_roles BRD actors have a Role homes row, $n_tasks_ok of $n_tasks BRD approval/user-task use cases are named under an Inbox heading, $n_wf_ok of $n_wf wireframes carry a valid Page parameter line" + fi + [ "$n_waived" -gt 0 ] && counts="$counts, $n_waived waived" + MXTK_IM_N_FAIL="$n_fail" + if [ "$n_fail" -gt 0 ]; then + MXTK_IM_STATUS="FAIL"; MXTK_IM_NOTE="$n_fail finding(s) in $rel — $counts" + elif [ -n "$manual" ]; then + MXTK_IM_STATUS="MANUAL"; MXTK_IM_NOTE="$counts" + elif [ "$n_roles_open" -gt 0 ]; then + MXTK_IM_STATUS="MANUAL"; MXTK_IM_NOTE="$n_roles_open BRD actor(s) with no Role homes row — confirm each is a user role or add a 'none — ' row; $counts" + else + MXTK_IM_NOTE="$counts" + fi + printf 'Interaction model (stage 3 facts): %s — %s\n' "$MXTK_IM_STATUS" "$MXTK_IM_NOTE" + [ -n "$lines" ] && printf '%s' "$lines" + return 0 +} + +# Direct invocation: run the check against a project dir. +if [ "${BASH_SOURCE[0]:-$0}" = "${0}" ]; then + mxtk_interaction_model_check "${1:-$(pwd)}" "${2:-}" +fi diff --git a/skills/brd-to-build-plan.md b/skills/brd-to-build-plan.md index bb746dd6..8f080a81 100644 --- a/skills/brd-to-build-plan.md +++ b/skills/brd-to-build-plan.md @@ -701,6 +701,8 @@ Before any `GRANT` script, decide and document: Every page generated in the build plan must be wired to a navigation item before the module is considered done. Leaving pages unwired is a silent incompleteness — they exist in the MPR but are unreachable in a running app. +Read the wire points from `design/interaction-model.md` (`interaction-model.md`): each role's home page, each page's nav entry and page parameter. A page that takes a parameter is opened from its callers, never from the menu. + **Required for every module:** | Page type | Where to wire | diff --git a/skills/checkpoints/checkpoint-design.md b/skills/checkpoints/checkpoint-design.md index 992ce81f..8510418d 100644 --- a/skills/checkpoints/checkpoint-design.md +++ b/skills/checkpoints/checkpoint-design.md @@ -55,6 +55,15 @@ user says to proceed. Then run the predefined questions to record the direction. **Record as:** `PROJECT.md` → `## Decisions` → `Atlas layout:` +### Q1b — Who lands where + +**When to ask:** Always, with Q1. Layout is not navigation. + +> "Who lands where, and how does each role find its work?" Propose a home and an inbox per role +> from `design/interaction-model.md` (`interaction-model.md`). + +**Record as:** `PROJECT.md` → `## Decisions` → `Role homes:` + --- ### Q2 — Responsive / platform target @@ -103,6 +112,7 @@ answer to this question. ``` PROJECT.md → ## Decisions: Atlas layout: [chosen layout] + Role homes: [role → home page, inbox] Platform target: [web / responsive / native / both] Design assets: [Figma URL / brand doc link / 'none' / description] Design direction: [brand / Atlas defaults / — ui-ux-pro-max @ + source rows] diff --git a/skills/design-artifacts.md b/skills/design-artifacts.md index 112bc819..09db7680 100644 --- a/skills/design-artifacts.md +++ b/skills/design-artifacts.md @@ -248,8 +248,8 @@ Each wireframe is assembled from Step 1's components — never restyled from scr
``` -The value is the qualified entity (`Module.Entity`) or `none — `, in exactly that form -(a later gate parses the line). `none` is a statement, not a blank: it says the page is a list, a +The value is the qualified entity (`Module.Entity`, a trailing `(note)` allowed) or `none — `, +in exactly that form (a later gate parses the line). `none` is a statement, not a blank: it says the page is a list, a home or a singleton. The `wf-` prefix makes the block annotation, so `page-fidelity.js` does not score it as page content and `assemble-prototype.js` hides it behind **Show bindings**. Draw the screen as the role named there, with that role's menu. diff --git a/tests/wave2/test-interaction-model-gate.sh b/tests/wave2/test-interaction-model-gate.sh new file mode 100755 index 00000000..0139d4c9 --- /dev/null +++ b/tests/wave2/test-interaction-model-gate.sh @@ -0,0 +1,343 @@ +#!/usr/bin/env bash +# Fixture for issue #291 part 2: the Stage-3 interaction-model facts. bin/gate-check.sh

3 +# (via bin/lib/interaction-model-check.sh) FAILs, once design/interaction-model.md exists, on +# (b) a BRD approval/review/sign-off/user-task use case not named on any line under an Inbox +# heading (table row or bullet), +# (c) a design/wireframes/*.html without a valid `Page parameter:` line in its wf-contract block; +# reports MANUAL (never FAIL) for (a) a BRD actor with no "Role homes" row — BRD actors are not +# the confirmed role model; a `none — ` row covers one; +# reports the artifact PENDING (artifact-manifest.tsv row `interaction-model`) when it is absent; +# honours "Waived artifact interaction-model[/]" register lines; and reports ADOPTED for a +# project adopted above stage 3. +# +# tests/wave2/test-interaction-model-gate.sh /path/to/gate-check.sh +# +# Positive control: run it against the base branch's gate-check.sh too — every FAIL case and the +# PENDING/ADOPTED/WAIVED lines are absent there, so those assertions fail. +# +# T9–T12 are the independent review's probe trees for #332, reproduced verbatim. +# +# Input shapes are copied from the skills, not invented: the Role homes table and the per-page +# table from skills/interaction-model.md Steps 1 and 3, the wf-contract block from +# skills/design-artifacts.md Step 3, the BRD keys from skills/brd-generation.md. Synthetic trees +# only — no field run is claimed here. +set -uo pipefail +GATE="${1:?usage: test-interaction-model-gate.sh /path/to/gate-check.sh}" +WORK="$(mktemp -d /tmp/imgate.XXXXXX)" +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; } +bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; } +export MXTK_NO_FETCH=1 + +ROLE_HOMES='## Role homes + +| Role | Home page | Comes to do | +|---|---|---| +| Manager | `Order_Overview` | Find an order and follow it | +| Reviewer | `Review_Inbox` | Work through the reviews waiting for them | +' +INBOXES='## Inboxes and object hubs + +| User task | Role | Inbox page | Grouping | Row opens | +|---|---|---|---|---| +| UC002 Approve order | Reviewer | `Review_Inbox` | by due date | `Order_Review` with its `Sales.Order` | +' +PAGES='## Per-page table + +| Page | How reached | Page parameter / context object | Data view source | Nav entry | Role(s) | +|---|---|---|---|---|---| +| `Order_Overview` | menu; home for Manager | none — list page | grid: database `Sales.Order` | Orders | Manager | +| `Order_Detail` | row of `Order_Overview` | `Sales.Order` | page parameter | — | Manager, Reviewer | +' + +wf() { # wf + if [ "$2" = "NOBLOCK" ]; then + printf '\n

Screen

\n\n' > "$1" + else + printf '\n
\n%s\n

Reached from: menu

\n

Drawn as role: Manager

\n
\n

Screen

\n\n' "$2" > "$1" + fi +} + +# Full Stage-3 PASS scaffold (as test-wireframe-count.sh builds it) plus a BRD with two actors +# and one approval use case, and a passing interaction model. $2 = "nested" puts everything +# under analysis/app/ (the two-tree layout gate-check's ANALYSIS_BASE serves). +mkproj() { + local d="$WORK/$1" b + b="$d"; [ "${2:-}" = "nested" ] && b="$d/analysis/app" + mkdir -p "$b/architecture" "$b/design/wireframes" "$b/knowledge-base/brd" + cat > "$b/knowledge-base/brd/F001-orders.brd.json" <<'JSON' +{ "id": "F001", "title": "Orders", "modules": ["Sales"], + "actors": ["Manager", "Reviewer"], + "useCases": [ + { "id": "UC001", "title": "View order list", "actors": ["Manager"] }, + { "id": "UC002", "title": "Approve order", "actors": ["Reviewer"] } + ], + "openQuestions": [] } +JSON + cat > "$d/PROJECT.md" <<'MD' +# PROJECT.md — fixture +Toolkit commit: none +Entry mode: requirements-driven + +## Decisions + +| Stage | Decision | Status | Notes | +|---|---|---|---| +| 3 | Module structure: one module | CONFIRMED 2026-10-14 | matches the BRD | + +## Open questions + +| # | Question | Raised at | Status | +|---|---|---|---| +MD + printf '# fit-gap\nrow\n' > "$b/architecture/fit-gap.md" + printf '# bp\nx\n' > "$b/architecture/blueprint.md" + sleep 1 + printf '\n' > "$b/architecture/blueprint.html" + printf '\n' > "$b/design/design-system.html" + wf "$b/design/wireframes/Order_Overview.html" '

Page parameter: none — list page

' + wf "$b/design/wireframes/Order_Detail.html" '

Page parameter: Sales.Order

' + printf '# Interaction model\n\n%s\n%s\n%s' "$ROLE_HOMES" "$INBOXES" "$PAGES" > "$b/design/interaction-model.md" + echo "$d" +} +base_of() { if [ -d "$1/analysis/app" ]; then echo "$1/analysis/app"; else echo "$1"; fi; } + +run() { "$GATE" --no-html "$1" 3 2>&1; } +stage3() { printf '%s\n' "$1" | grep '^Stage 3 ' | head -1; } +imline() { printf '%s\n' "$1" | grep '^Interaction model (stage 3 facts):' | head -1; } + +echo "== T1: complete interaction model — Stage 3 PASS, facts PASS with denominators ==" +P="$(mkproj t1)"; OUT="$(run "$P")"; RC=$? +case "$(stage3 "$OUT")" in *": PASS"*) ok "stage 3 passes" ;; *) bad "stage 3 not PASS: $(stage3 "$OUT")" ;; esac +[ "$RC" -eq 0 ] && ok "exit 0" || bad "exit $RC on a passing project" +case "$(imline "$OUT")" in + *"PASS"*"2 of 2 BRD actors"*"1 of 1 BRD approval"*"2 of 2 wireframes"*) ok "facts PASS, every denominator stated" ;; + *) bad "facts line wrong: $(imline "$OUT")" ;; +esac +case "$OUT" in *"Artifact interaction-model"*"PRESENT"*) ok "artifact check sees it PRESENT" ;; *) bad "no PRESENT artifact row" ;; esac + +echo "== T2: artifact absent — PENDING out loud, Stage 3 verdict untouched ==" +P="$(mkproj t2)"; rm "$(base_of "$P")/design/interaction-model.md"; OUT="$(run "$P")" +case "$OUT" in *"Artifact interaction-model"*"PENDING — owed at stage 3"*) ok "manifest row reports it owed and absent" ;; + *) bad "no loud PENDING artifact row: $(printf '%s\n' "$OUT" | grep 'Artifact interaction-model')" ;; esac +case "$(imline "$OUT")" in *PENDING*) ok "facts PENDING, not PASS" ;; *) bad "facts line: $(imline "$OUT")" ;; esac +case "$(stage3 "$OUT")" in *": PASS"*) ok "absence alone does not fail stage 3" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac + +echo "== T3a: a BRD actor with no row in Role homes — MANUAL, not FAIL ==" +P="$(mkproj t3a)"; B="$(base_of "$P")" +grep -v '^| Reviewer |' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +OUT="$(run "$P")"; RC=$? +case "$(stage3 "$OUT")" in *": MANUAL"*"interaction model"*) ok "stage 3 MANUAL" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac +case "$OUT" in *"MANUAL role Reviewer"*"confirm it is a user role"*"none — "*) ok "finding names the actor and the remedy" ;; *) bad "no role finding" ;; esac +case "$OUT" in *"FAIL role"*) bad "an actor without a home is still a FAIL" ;; *) ok "no role FAIL" ;; esac +[ "$RC" -eq 2 ] && ok "exit 2 (manual)" || bad "exit $RC" + +echo "== T3b: a role row with an empty Home page cell — MANUAL ==" +P="$(mkproj t3b)"; B="$(base_of "$P")" +sed 's/^| Reviewer | `Review_Inbox` |/| Reviewer | |/' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$OUT" in *"MANUAL role Reviewer"*) ok "empty home cell is no home" ;; *) bad "empty home accepted: $(imline "$OUT")" ;; esac + +echo "== T3c: a 'none — ' row covers a non-user actor — PASS ==" +P="$(mkproj t3c)"; B="$(base_of "$P")" +sed 's/^| Reviewer | `Review_Inbox` |/| Reviewer | none — merged into Manager |/' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$(stage3 "$OUT")" in *": PASS"*) ok "none-row discharges the actor" ;; *) bad "stage 3: $(stage3 "$OUT") / $(imline "$OUT")" ;; esac + +echo "== T4a: an approval use case with no inbox row — FAIL ==" +P="$(mkproj t4a)"; B="$(base_of "$P")" +grep -v '^| UC002' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$(stage3 "$OUT")" in *": FAIL"*) ok "stage 3 FAILs" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac +case "$OUT" in *"FAIL task F001/UC002"*) ok "finding names BRD/use case" ;; *) bad "no task finding" ;; esac +case "$OUT" in *"task F001/UC001"*) bad "a non-approval use case was demanded an inbox" ;; *) ok "non-approval use case not demanded" ;; esac + +echo "== T4b: a stated reason instead of an inbox — PASS ==" +P="$(mkproj t4b)"; B="$(base_of "$P")" +sed 's/^| UC002 Approve order .*/| UC002 | Reviewer | — | — | none — approved inline on Order_Detail |/' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$(stage3 "$OUT")" in *": PASS"*) ok "reason row discharges the task" ;; *) bad "stage 3: $(stage3 "$OUT") / $(imline "$OUT")" ;; esac + +echo "== T5a: a wireframe with no wf-contract block — FAIL ==" +P="$(mkproj t5a)"; wf "$(base_of "$P")/design/wireframes/Review_Inbox.html" NOBLOCK +OUT="$(run "$P")" +case "$OUT" in *"FAIL wireframe Review_Inbox.html — no wf-contract block"*) ok "missing block named" ;; *) bad "$(imline "$OUT")" ;; esac +case "$(stage3 "$OUT")" in *": FAIL"*) ok "stage 3 FAILs" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac +case "$OUT" in *"remedy: add

Page parameter:"*"Waived artifact interaction-model/.html"*) ok "remedy line printed once" ;; *) bad "no remedy line" ;; esac + +echo "== T5b: a wf-contract block with no Page parameter line — FAIL ==" +P="$(mkproj t5b)"; wf "$(base_of "$P")/design/wireframes/Review_Inbox.html" '

Parameter: Sales.Order

' +OUT="$(run "$P")" +case "$OUT" in *"FAIL wireframe Review_Inbox.html — its wf-contract block has no"*) ok "missing line named" ;; *) bad "$(imline "$OUT")" ;; esac + +echo "== T5c: a Page parameter outside the contract block does not count — FAIL ==" +P="$(mkproj t5c)" +printf '\n

Reached from: menu

\n

Page parameter: Sales.Order

\n\n' \ + > "$(base_of "$P")/design/wireframes/Review_Inbox.html" +OUT="$(run "$P")" +case "$OUT" in *"FAIL wireframe Review_Inbox.html"*) ok "line outside the block rejected" ;; *) bad "$(imline "$OUT")" ;; esac + +echo "== T5d: values that are neither Module.Entity nor 'none — ' — FAIL each ==" +P="$(mkproj t5d)"; B="$(base_of "$P")" +wf "$B/design/wireframes/A.html" '

Page parameter: the order

' +wf "$B/design/wireframes/B.html" '

Page parameter: none

' +wf "$B/design/wireframes/C.html" '

Page parameter: Order

' +OUT="$(run "$P")" +n="$(printf '%s\n' "$OUT" | grep -c '^ FAIL wireframe [ABC]\.html')" +[ "$n" = 3 ] && ok "3 of 3 malformed values FAIL" || bad "$n of 3 malformed values FAIL: $(imline "$OUT")" + +echo "== T5e: none — is the same character — PASS ==" +P="$(mkproj t5e)"; wf "$(base_of "$P")/design/wireframes/Review_Inbox.html" '

Page parameter: none — home

' +OUT="$(run "$P")" +case "$(stage3 "$OUT")" in *": PASS"*) ok "entity em dash accepted" ;; *) bad "$(imline "$OUT")" ;; esac + +echo "== T6a: findings waived in the register — WAIVED lines, stage 3 PASS ==" +P="$(mkproj t6a)"; B="$(base_of "$P")" +grep -v '^| Reviewer |' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +wf "$B/design/wireframes/Legacy.html" NOBLOCK +printf '\nWaived artifact interaction-model/reviewer: technical role in this fixture, never signs in\nWaived artifact interaction-model/legacy.html: drawn before the interaction model\n' >> "$P/PROJECT.md" +OUT="$(run "$P")" +case "$OUT" in *"WAIVED role Reviewer — technical role"*) ok "waiver read, case-insensitive, reason printed" ;; *) bad "no WAIVED line: $(imline "$OUT")" ;; esac +case "$OUT" in *"WAIVED wireframe Legacy.html — drawn before"*) ok "per-wireframe waiver" ;; *) bad "no wireframe WAIVED line" ;; esac +case "$(stage3 "$OUT")" in *": PASS"*) ok "stage 3 passes with the finding waived" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac + +echo "== T6b: the whole artifact waived — facts WAIVED ==" +P="$(mkproj t6b)"; wf "$(base_of "$P")/design/wireframes/Review_Inbox.html" NOBLOCK +printf '\nWaived artifact interaction-model: decided in the client UX workshop\n' >> "$P/PROJECT.md" +OUT="$(run "$P")" +case "$(imline "$OUT")" in *"WAIVED — decided in the client UX workshop"*) ok "whole-artifact waiver" ;; *) bad "$(imline "$OUT")" ;; esac +case "$(stage3 "$OUT")" in *": PASS"*) ok "stage 3 passes" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac + +echo "== T7: adopted above stage 3 — ADOPTED, no FAIL ==" +P="$(mkproj t7)"; B="$(base_of "$P")" +grep -v '^| Reviewer |' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +printf '\nAdopted at stage: 5 — joined mid-build\n' >> "$P/PROJECT.md" +OUT="$(run "$P")"; RC=$? +case "$(imline "$OUT")" in *ADOPTED*) ok "facts ADOPTED" ;; *) bad "$(imline "$OUT")" ;; esac +case "$(stage3 "$OUT")" in *": FAIL"*) bad "adopted project failed stage 3: $(stage3 "$OUT")" ;; *) ok "stage 3 not FAIL" ;; esac +[ "$RC" -eq 0 ] && ok "exit 0" || bad "exit $RC" + +echo "== T8: nested analysis// layout — same facts, same FAIL ==" +P="$(mkproj t8 nested)"; B="$(base_of "$P")" +grep -v '^| Reviewer |' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$OUT" in *"MANUAL role Reviewer"*) ok "nested layout read" ;; *) bad "nested layout not read: $(imline "$OUT")" ;; esac + +echo "== T9: inbox written as bullets (the skill's own Step 2 form) — covered ==" +P="$(mkproj t9)"; B="$(base_of "$P")" +sed 's/^| UC002 Approve order .*/- **UC002 Approve order** → Reviewer, `Review_Inbox`, grouped by due date, row opens `Order_Review` with its order./' \ + "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +grep -q '^- \*\*UC002' "$B/design/interaction-model.md" && ok "fixture: inbox entry is a bullet now" || bad "fixture setup: bullet not written" +OUT="$(run "$P")" +case "$(stage3 "$OUT")" in *": PASS"*) ok "bullet line under the Inbox heading covers the task" ;; *) bad "stage 3: $(stage3 "$OUT") / $(imline "$OUT")" ;; esac + +echo "== T10: task selection — approval-ish titles that are not user tasks are not demanded ==" +P="$(mkproj t10)"; B="$(base_of "$P")" +cat > "$B/knowledge-base/brd/F002-admin.brd.json" <<'JSON' +{ "id": "F002", "title": "Admin", "actors": ["Manager"], + "useCases": [ + { "id": "UC010", "title": "View approval history" }, + { "id": "UC011", "title": "Configure approval thresholds" }, + { "id": "UC012", "title": "Disapprove nothing" }, + { "id": "UC013", "title": "UC013: Review supplier" }, + { "id": "UC014", "title": "Sign off release" }, + { "id": "UC015", "title": "Escalate stuck case", "mdlRefs": ["Sales.WF_Case/UserTask_Escalate"] } + ], + "openQuestions": [] } +JSON +OUT="$(run "$P")" +for u in UC010 UC011 UC012; do + case "$OUT" in *"task F002/$u"*) bad "$u demanded an inbox (false positive)" ;; *) ok "$u not demanded" ;; esac +done +for u in UC013 UC014 UC015; do + case "$OUT" in *"FAIL task F002/$u"*) ok "$u demanded (id-prefixed review / sign off / mdlRefs user task)" ;; *) bad "$u missed" ;; esac +done + +echo "== T11: wireframe markup variants from the review probe — all valid ==" +P="$(mkproj t11)"; W="$(base_of "$P")/design/wireframes" +printf '

Page parameter: Sales.Order

\n' > "$W/bold.html" +printf "

Page parameter: Sales.Order

\n" > "$W/cls.html" +printf '

Page parameter: Sales.Order

\n' > "$W/code.html" +printf '
\r\n

Page parameter: none \342\200\224 home

\r\n
\r\n' > "$W/crlf.html" +printf '

Page parameter: none — list page

\n' > "$W/ent.html" +printf '

Page parameter: none - list page

\n' > "$W/hyph.html" +printf '

Page parameter: none \342\200\223 list page

\n' > "$W/endash.html" +printf '
\n

Page parameter:\n Sales.Order

\n' > "$W/multi.html" +printf '
Reached from: menu

Page parameter: Sales.Order

\n' > "$W/nested.html" +OUT="$(run "$P")" +case "$(imline "$OUT")" in *"11 of 11 wireframes"*) ok "11 of 11 wireframes valid" ;; *) bad "$(imline "$OUT"); $(printf '%s\n' "$OUT" | grep 'FAIL wireframe')" ;; esac +case "$(stage3 "$OUT")" in *": PASS"*) ok "stage 3 passes" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac + +echo "== T12: a CRLF interaction model reads like an LF one ==" +P="$(mkproj t12)"; B="$(base_of "$P")" +sed 's/$/\r/' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$(stage3 "$OUT")" in *": PASS"*) ok "CRLF artifact passes" ;; *) bad "stage 3: $(stage3 "$OUT") / $(imline "$OUT")" ;; esac + +echo "== T13: a MANUAL role finding does not hide a missing Stage-3 CONFIRMED decision ==" +P="$(mkproj t13)"; B="$(base_of "$P")" +grep -v '^| Reviewer |' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +sed 's/CONFIRMED 2026-10-14/ASSUMED 2026-10-14/' "$P/PROJECT.md" > "$P/x" && mv "$P/x" "$P/PROJECT.md" +OUT="$(run "$P")"; RC=$? +case "$(stage3 "$OUT")" in *": FAIL"*"CONFIRMED"*) ok "stage 3 FAIL on the register, not softened to MANUAL" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac +[ "$RC" -eq 1 ] && ok "exit 1" || bad "exit $RC" + +echo "== T14: a role cell with a trailing note in parentheses names the role ==" +P="$(mkproj t14)"; B="$(base_of "$P")" +sed 's/^| Reviewer |/| Reviewer (signed-in, sees only own reviews) |/' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$(imline "$OUT")" in *"2 of 2 BRD actors"*) ok "Reviewer (note) covers Reviewer" ;; *) bad "$(imline "$OUT")" ;; esac +case "$(stage3 "$OUT")" in *": PASS"*) ok "stage 3 passes" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac + +echo "== T15: Module.Entity with a trailing (note) is valid; prose around the entity is not ==" +P="$(mkproj t15)"; W="$(base_of "$P")/design/wireframes" +wf "$W/note.html" '

Page parameter: Sales.Order (new, Customer set)

' +wf "$W/space.html" '

Page parameter: Sales Order

' +wf "$W/or.html" '

Page parameter: Sales.Order or none

' +OUT="$(run "$P")" +case "$OUT" in *"FAIL wireframe note.html"*) bad "trailing (note) rejected" ;; *) ok "Sales.Order (note) accepted" ;; esac +case "$OUT" in *"FAIL wireframe space.html"*) ok "'Sales Order' rejected" ;; *) bad "'Sales Order' accepted" ;; esac +case "$OUT" in *"FAIL wireframe or.html"*) ok "'Sales.Order or none' rejected" ;; *) bad "'Sales.Order or none' accepted" ;; esac +case "$(imline "$OUT")" in *"3 of 5 wireframes"*) ok "3 of 5 wireframes valid" ;; *) bad "$(imline "$OUT")" ;; esac + +echo "== T16: a malformed value gets the write-it-bare remedy; a missing line keeps the add remedy ==" +P="$(mkproj t16)"; wf "$(base_of "$P")/design/wireframes/Bad.html" '

Page parameter: the order

' +OUT="$(run "$P")" +case "$OUT" in *"remedy: "*"malformed"*"bare Module.Entity"*"(note)"*"none — "*) ok "malformed remedy says write the bare value" ;; *) bad "no malformed remedy: $(printf '%s\n' "$OUT" | grep 'remedy:')" ;; esac +case "$OUT" in *"remedy: add

Page parameter:"*) bad "add-the-line remedy printed for a line that exists" ;; *) ok "add remedy not printed for a malformed-only project" ;; esac +P="$(mkproj t16b)"; wf "$(base_of "$P")/design/wireframes/NoLine.html" '

Parameter: Sales.Order

' +OUT="$(run "$P")" +case "$OUT" in *"remedy: add

Page parameter:"*) ok "missing line keeps the add remedy" ;; *) bad "add remedy lost" ;; esac +case "$OUT" in *"malformed"*) bad "malformed remedy printed for a missing line" ;; *) ok "no malformed remedy for a missing line" ;; esac + +echo "== T17: an interaction-model FAIL also names a missing Stage-3 CONFIRMED decision ==" +P="$(mkproj t17)"; B="$(base_of "$P")" +grep -v '^| UC002' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +sed 's/CONFIRMED 2026-10-14/ASSUMED 2026-10-14/' "$P/PROJECT.md" > "$P/x" && mv "$P/x" "$P/PROJECT.md" +OUT="$(run "$P")" +case "$(stage3 "$OUT")" in *": FAIL"*"interaction model"*"also: no Stage-3 CONFIRMED decision"*) ok "both problems in the verdict" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac +P="$(mkproj t17b)"; B="$(base_of "$P")" +grep -v '^| UC002' "$B/design/interaction-model.md" > "$B/x" && mv "$B/x" "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$(stage3 "$OUT")" in *"also: no Stage-3"*) bad "suffix printed with a CONFIRMED decision" ;; *": FAIL"*) ok "no suffix when the decision is there" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac + +echo "== T18: a 'Homes and inboxes' heading with a Home column reads as homes and inboxes ==" +P="$(mkproj t18)"; B="$(base_of "$P")" +printf '# Interaction model\n\n## Homes and inboxes\n\n| Role | Home | Inbox |\n|---|---|---|\n| Manager | `Order_Overview` | none |\n| Reviewer | `Review_Inbox` | UC002 Approve order |\n\n%s' "$PAGES" > "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$(imline "$OUT")" in *"2 of 2 BRD actors"*"1 of 1 BRD approval"*) ok "homes and inbox both read from the combined table" ;; *) bad "$(imline "$OUT")" ;; esac +case "$(stage3 "$OUT")" in *": PASS"*) ok "stage 3 passes" ;; *) bad "stage 3: $(stage3 "$OUT")" ;; esac +P="$(mkproj t18b)"; B="$(base_of "$P")" +printf '# Interaction model\n\n## Role homes\n\n| Role | Home | Comes to do |\n|---|---|---|\n| Manager | `Order_Overview` | x |\n| Reviewer | `Review_Inbox` | UC002 Approve order |\n\n%s' "$PAGES" > "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$OUT" in *"FAIL task F001/UC002"*) ok "a homes-only heading is still not an inbox" ;; *) bad "Role homes table counted as an inbox: $(imline "$OUT")" ;; esac + +P="$(mkproj t18c)"; B="$(base_of "$P")" +printf '# Interaction model\n\n## Role homes and inbox notes\n\n| Role | Home | Comes to do |\n|---|---|---|\n| Manager | `Order_Overview` | x |\n| Reviewer | `Review_Inbox` | UC002 Approve order |\n\n%s' "$PAGES" > "$B/design/interaction-model.md" +OUT="$(run "$P")" +case "$OUT" in *"FAIL task F001/UC002"*) ok "a 'Role homes ... inbox' heading is still not an inbox" ;; *) bad "Role homes + inbox heading counted as an inbox: $(imline "$OUT")" ;; esac + +rm -rf "$WORK" +echo "" +echo "test-interaction-model-gate: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ]