diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c6faaca..8fa9eb40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased - fix(dispatching, sync-project, obligation-check): **review follow-ups for the 2026-10-14 integration batch.** `dispatching.md` names app-restart exits 4 and 5 beside 3 and 1. `test-test-scratch.sh` drops `ls | grep -q` under pipefail for here-strings; sync step 4d no longer aborts on empty doctor output; `_ob_has_denominator` strips NUL bytes so bash stops warning. — integration 2026-10-14 reviews +- new(gate-check, interaction-model): **Stage 3 now fails on an interaction model that leaves out a role, a user task or a wireframe's page parameter** ([#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 role with no row in "Role homes", per BRD approval/user-task use case with neither an inbox row nor a stated reason, and per `design/wireframes/*.html` without a `Page parameter:` line (`Module.Entity` or `none — `) in its `wf-contract` block; any of them fails Stage 3. 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) — field retro (#291) +- 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` 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) — field retro (#291) - feat(lint): **Lint Batch 4 — SEC014 external role can write state, audit or ownership members; CONV009 reads its `maxObjects` option.** SEC014 ([#268](https://github.com/MendixMau/mxcli-project-toolkit/issues/268)): a partner role with `create, read *, write *` on self-owned entities passed every stock rule, so it could set its own record's status through the client API. Lint showed only CONV006 +4, which was re-baselined, and a manual review found it five days later. The new rule flags an external role (named in `lint-config.yaml` as `external_roles: "A, B"`, or `"none"`) that writes all members, a `Status`/`State`/`Result`/`Stage`/`Phase` enumeration, an `…At`/`…By` attribute, or the association its own XPath uses for ownership. It is a warning in the security tier. A `_rule` finding marks the rule blind when the option is blank or given as a YAML list (mxcli passes a list as `None`), when a role name does not exist, or when the catalog is not FULL. Each message is under 160 characters and names the place to fix it, because lint-gate prints only that much. An **absent** option is not a `_rule` finding, so it does not block every exec (the #229 pattern). Instead `bin/sync-project.sh` prints a NOTE on every sync through new `bin/lib/sec014-notice.sh` until the option is set. It is a NOTE rather than a counted warning, so a pristine scaffold still syncs clean under `--strict` (fixture `tests/wave2/test-sec014-notice.sh`, 14 cases; `test-bug12-sync.sh` 61/61). `"none"` switches a security rule off, so it is set only on the user's answer in chat and recorded in `PROJECT.md`. Right after configuring, re-baseline with `lint-gate.sh --update-baseline --reason "SEC014 configured" --approved-in-chat`, because the 0 → N jump would otherwise be blamed on the next script. CONV009 ([#267](https://github.com/MendixMau/mxcli-project-toolkit/issues/267)): the stock rule hard-codes 15, so the `maxObjects: 20` example in the config `mxcli init` writes did nothing. The toolkit copy reads `maxObjects` and `countLoopBodies`, with the **default kept at 15** (raising it is a maintainer call). It replaces the stock file through `STOCK-HASHES.txt`, which gains conv009 hashes for v0.17.0/0.24.0/0.25.0 and a full v0.26.0 set, captured by `mxcli init` from the official release assets; `test-stock-hashes.sh` T4 passes live on all four. `lint-that-actually-runs.md` now says `options:` reaches only rules that call `get_option()`. 32 cases in `tests/lint-rules/test-batch4-rules.sh`; the harness gained `get_option`, `permissions_for`, `attributes_for`, `user_roles` and `role_mappings`. Field run (official mxcli 0.26.0, two model copies): stock CONV009 stayed at 5 with `maxObjects: 20`, while the toolkit copy gave 5 at 15, 10 at 10 and 6 at 20 with loop bodies counted. SEC014 gave 1 true finding with the model's external role configured, and 7 with every role configured, matched by a SQL replay. Both rules load on 0.17.0 and 0.24.0 too. CONV009's "every flow reports 0 objects" self-check now needs `activities_for()` to find a real object in a zero-count flow, so an early greenfield model with empty microflows is not reported blind. `lint-rules/README.md` and the TXT002 header now give the supported way to turn a toolkit rule off. ISO dates in particular are a toolkit opinion, so a project with a non-ISO display standard sets `rules: TXT002: enabled: false` in `lint-config.yaml` and records that in `PROJECT.md`. It must not delete the file, which sync would re-install. On mxcli 0.26.0, a rule with 1 finding gave 0 once disabled. After #308, there is one opt-out story, always the user's decision (asked in chat, recorded in `PROJECT.md`), and it never deletes a `.star` file. `enabled: false` in `lint-config.yaml` stops a rule reporting; this is preferred. `.claude/lint-rules-disabled.txt` with a `# reason:` stops sync installing a toolkit rule, and refuses stock-seeded rules, a list that now includes conv009. `rules: SEC014: enabled: false` is the same decision as `external_roles: "none"`. While SEC014 is undecided, `project-bin/lint-gate.sh` also prints one uncounted `note: SEC014 not configured …` line on stderr on every run (no exit-code change, no lint row). Both it and the sync NOTE now count `external_roles` only when it sits under the `SEC014` key. `test-sec014-notice.sh` has 24 cases. — MendixMau - fix(lint-gate): **rules that `mxcli init` reverted to stock now stop the lint run as BLIND instead of showing up as a rise on the next script** ([#264](https://github.com/MendixMau/mxcli-project-toolkit/issues/264)). `bin/lint-gate.sh` reads `.claude/.mxtk-lint-receipt` (written by `bin/lib/install-lint-rules.sh`) before linting: a stock-seeded rule (the file names `mxcli init` writes, now marked `stock` in the receipt) whose md5 no longer matches and whose `# mxtk-lint-rule:` header is gone, or any receipt-listed rule that is missing, is named, the remedy `bin/sync-project.sh ` (fallback `--upgrade-lint-rules all`) is printed, and the run exits 1 with `BLIND — lint rules reverted to stock` (exec.sh logs `LINT BLIND`, not `LINT ROSE`); `--update-baseline` is refused over reverted rules, also under the `LINT_RULES_REVERTED_OK=1` look-anyway override. Field cases: CONV010 26 -> 169 blamed on a pages-only exec, and a re-seed that sat five days while the baseline absorbed stock counts. So the remedy always clears it, `bin/lib/install-lint-rules.sh` restores such a stock-seeded rule even when its body matches no known stock hash (a newer mxcli's seed, or a hand edit that also lost the header: it was reported LOCALLY MODIFIED and never written), labelled `header missing — restored from toolkit, your copy kept as .reseeded-` with the newest 3 copies kept; a headerless toolkit-only or configurable rule is still a local edit, never overwritten. The installer also writes each receipt row's kind, prunes rows for rules no longer shipped, and honours `.claude/lint-rules-disabled.txt`: one line per rule with `# reason: …` (`#\s*reason:` in both tools), a user decision recorded in `PROJECT.md`; a line with no reason, or naming a stock-seeded rule, is ignored with a warning (`lint-rules/README.md` had told projects to just delete txt002). Fixture `tests/wave2/test-lint-receipt-revert.sh` builds its receipt with the real installer and uses a genuine v0.25.0 stock body: 55/55 here, 28 fail on master's lint-gate and installer. — AlvinnYeo - learn(learned-mdl-preflight): **a microflow change near a `return` in a dense flow is planned as drop + create after `show access`; `check -p --references` does not run exec's free-room test** ([#263](https://github.com/MendixMau/mxcli-project-toolkit/issues/263)). STOP row 28 and BUG-156: on v0.25.0 `check` printed "no flow change exec would refuse" and exec then refused "there is no free room for the fragment". Upstream issue drafted, not filed. — AlvinnYeo diff --git a/bin/gate-check.sh b/bin/gate-check.sh index 61f648b3..27c7b8aa 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -1159,6 +1159,13 @@ check_stage_3() { return fi done + # 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. + case "${MXTK_IM_STATUS:-}" in + FAIL) echo "FAIL|interaction model: ${MXTK_IM_NOTE} — each finding is listed under 'Interaction model (stage 3 facts)' (skills/interaction-model.md)"; return ;; + MANUAL) echo "MANUAL|interaction model: ${MXTK_IM_NOTE}"; return ;; + esac 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" @@ -2544,6 +2551,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 93c912ab..b16110eb 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/build-plan.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..cf18f556 --- /dev/null +++ b/bin/lib/interaction-model-check.sh @@ -0,0 +1,363 @@ +#!/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 "Role homes" heading +# INBOXline every non-blank line (table row, bullet, prose) under any heading +# naming "Inbox" — the skill's own Step 2 writes inboxes as bullets +# Cells are trimmed and stripped of backticks and bold. CRLF files read like LF ones. The Role / +# Home page 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 ~ /role homes/) sec = "home" + else if (h ~ /inbox/) sec = "inbox" + hdr = 0; next + } + sec == "" { next } + sec == "inbox" { if ($0 ~ /[^ \t]/) printf "INBOX\t%s\n", clean($0); 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") hc = i + } + next + } + if (line ~ /^[ \t]*\|[ \t:|-]+\|?[ \t]*$/) next + if (rc == 0) next + printf "HOME\t%s\t%s\n", clean(cell[rc]), (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)) + 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 2a75cdbe..d8df2b53 100644 --- a/skills/brd-to-build-plan.md +++ b/skills/brd-to-build-plan.md @@ -697,6 +697,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 7e2b0852..457c23e8 100644 --- a/skills/checkpoints/checkpoint-design.md +++ b/skills/checkpoints/checkpoint-design.md @@ -54,6 +54,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 @@ -102,6 +111,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/tests/wave2/test-interaction-model-gate.sh b/tests/wave2/test-interaction-model-gate.sh new file mode 100755 index 00000000..17f04e85 --- /dev/null +++ b/tests/wave2/test-interaction-model-gate.sh @@ -0,0 +1,280 @@ +#!/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 + +rm -rf "$WORK" +echo "" +echo "test-interaction-model-gate: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ]