@windyroad/itil 0.61.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +2 -14
  3. package/bin/{wr-itil-check-afk-accept-eligible → wr-itil-render-story-map} +2 -2
  4. package/bin/wr-itil-story-map-edit +51 -0
  5. package/bin/wr-itil-story-map-query +51 -0
  6. package/hooks/itil-no-implement-draft-gate.sh +19 -28
  7. package/lib/story-oversight.sh +154 -60
  8. package/package.json +2 -1
  9. package/scripts/check-rfc-stories-ratified.sh +15 -9
  10. package/scripts/detect-unratified-stories-maps.sh +15 -32
  11. package/scripts/mark-story-oversight-confirmed.sh +80 -31
  12. package/scripts/migrate-story-status-mirror.sh +7 -3
  13. package/scripts/reconcile-stories.sh +26 -5
  14. package/scripts/render-story-map.mjs +1057 -0
  15. package/scripts/render-story-map.sh +15 -0
  16. package/scripts/story-map-edit.mjs +280 -0
  17. package/scripts/story-map-edit.sh +10 -0
  18. package/scripts/story-map-query.mjs +227 -0
  19. package/scripts/story-map-query.sh +68 -0
  20. package/scripts/update-story-references-section.sh +6 -1
  21. package/skills/capture-rfc/SKILL.md +1 -1
  22. package/skills/capture-story/SKILL.md +16 -8
  23. package/skills/capture-story-map/SKILL.md +102 -59
  24. package/skills/list-stories/SKILL.md +16 -10
  25. package/skills/list-story-maps/SKILL.md +1 -1
  26. package/skills/manage-rfc/SKILL.md +1 -1
  27. package/skills/manage-story/SKILL.md +19 -26
  28. package/skills/manage-story-map/SKILL.md +19 -17
  29. package/skills/reconcile-stories/SKILL.md +5 -2
  30. package/skills/work-problems/SKILL.md +2 -2
  31. package/templates/story-map.css +195 -0
  32. package/templates/story-map.html +23 -0
  33. package/scripts/check-afk-accept-eligible.sh +0 -230
@@ -24,9 +24,9 @@ Per ADR-060 amendment 2026-05-10 lines 145-189 + encoding amendment 2026-05-12:
24
24
 
25
25
  | Invariant | What it asserts | Where it fires |
26
26
  |-----------|-----------------|----------------|
27
- | **I3** trace-to-problem | Every map traces to ≥ 1 problem (ADR-060 line 187) | Hard-block at `/wr-itil:capture-story-map` + re-validated at every transition |
28
- | **I4** trace-to-JTBD | Every map traces to ≥ 1 JTBD (ADR-060 line 188) | Hard-block at `/wr-itil:capture-story-map` + re-validated at every transition |
29
- | **I5** no-WSJF-leak | Maps MUST NOT carry WSJF (ADR-060 line 189) | Behavioural test: argument grammar + frontmatter carry no WSJF |
27
+ | **I3** trace-to-problem | Every map traces to ≥ 1 problem (ADR-060, the story-map schema's `problems:` field) | Hard-block at `/wr-itil:capture-story-map` + re-validated at every transition |
28
+ | **I4** trace-to-JTBD | Every map traces to ≥ 1 JTBD (ADR-060, the story-map schema's `jtbd:` field) | Hard-block at `/wr-itil:capture-story-map` + re-validated at every transition |
29
+ | **I5** no-WSJF-leak | Maps MUST NOT carry WSJF (ADR-060, the no-WSJF-on-maps invariant) | Behavioural test: argument grammar + frontmatter carry no WSJF |
30
30
 
31
31
  **Bootstrap-exemption marker** per ADR-060 line 339 + ADR-053 Bootstrapping precedent: STORY-MAP-001 ships with the `<!-- bootstrap-exempt -->` marker during retrofit; non-bootstrap captures with the marker fail.
32
32
 
@@ -38,7 +38,7 @@ Per ADR-060 amendment 2026-05-10 lines 145-189 + encoding amendment 2026-05-12:
38
38
  /wr-itil:manage-story-map <STORY-MAP-NNN> in-progress # Manual transition
39
39
  /wr-itil:manage-story-map <STORY-MAP-NNN> completed # Transition in-progress → completed
40
40
  /wr-itil:manage-story-map <STORY-MAP-NNN> archived # Close without completion
41
- /wr-itil:manage-story-map <STORY-MAP-NNN> ratify # ADR-090: confirm the map, then its stories, one at a time
41
+ /wr-itil:manage-story-map <STORY-MAP-NNN> ratify # ADR-103: confirm the map approves every story on it
42
42
  /wr-itil:manage-story-map review # Re-validate all maps + refresh README
43
43
  ```
44
44
 
@@ -76,7 +76,7 @@ If `$story_map_id == "review"`, branch to Step 8 (review flow). Otherwise resolv
76
76
 
77
77
  ### 2. Read story-map HTML
78
78
 
79
- Parse `<meta>` block (problems, rfcs, jtbd, adrs, status, reported, decision-makers). Read backbone structure (`<section class="backbone">` `.rib-header` → `.rib` → `<a class="slice">` data-* attributes).
79
+ Parse `<meta>` block (problems, rfcs, jtbd, status, reported, decision-makers) — no `adrs`, and `problems`/`rfcs` are DERIVED, so they carry content the island does not. Read backbone structure from the grid's `<th class="act" scope="col">` columns and `<th class="slice" scope="row">` rows.
80
80
 
81
81
  ### 3-6. (Update flow — bare `<STORY-MAP-NNN>`)
82
82
 
@@ -101,7 +101,7 @@ For any transition `<from> → <to>`:
101
101
  4. **Reverse-trace refresh** — for each problem + JTBD in `<meta>` block:
102
102
 
103
103
  ```bash
104
- for pid_token in $(grep -oE '<meta name="problems" content="[^"]*"' "$map_file" | grep -oE 'P[0-9]{3}'); do
104
+ for pid_token in $(grep -oE '<meta name="problems" content="[^"]*"' "$map_file" | grep -oE 'P[0-9]{3,}'); do
105
105
  pid_num="${pid_token#P}"
106
106
  problem_file=$(ls docs/problems/${pid_num}-*.md docs/problems/*/${pid_num}-*.md 2>/dev/null | head -1)
107
107
  [ -z "$problem_file" ] && continue
@@ -114,13 +114,13 @@ done
114
114
 
115
115
  Per architect amend finding 2 on Slice 7: story-map HTML files do NOT carry an auto-maintained markdown reverse-trace section themselves (the `<a class="slice">` data-attribute traces are authored manually during backbone design). No reverse-trace refresh on the map itself; reverse-trace only flows OUT to problem + JTBD parents.
116
116
 
117
- ### 7.5. Ratification flow (`ratify`) — ADR-090 / STORY-022
117
+ ### 7.5. Ratification flow (`ratify`) — ADR-090 / ADR-103 / STORY-022
118
118
 
119
- `ratify` is **orthogonal to the status lifecycle** — a map can be ratified at any status. It confirms human oversight of the map + its stories after they are authored or edited. Ratification is **drift-invalidated** (ADR-009 lineage, NOT ADR-066 write-once): any later content edit silently re-opens it (the `oversight-hash` fingerprint stops matching). This is the STORY-022 surface.
119
+ `ratify` is **orthogonal to the status lifecycle** — a map can be ratified at any status. It confirms human oversight of **the map**, and under ADR-103 that approves every story on it, including stories added later; stories are never ratified individually. Ratification is **drift-invalidated** (ADR-009 lineage, NOT ADR-066 write-once), but only a **substance** edit re-opens it: the map's own substance as ADR-090 defines it — its journey, its identity, and what it traces to. `oversight_map_substance_keys()` is the authoritative field list; this page deliberately does not restate it. Release rows, the cards in them, story-body edits and template restyling all sit outside the fingerprint basis and change nothing. This is the STORY-022 surface.
120
120
 
121
121
  **Born-confirmed discipline (P348).** Before any marker write, `export CLAUDE_SESSION_ID` from the transcript path — the marker shim silently no-ops on an empty SID. Every `confirmed` marker MUST be backed by a same-turn human confirm event; never write `confirmed` without the `AskUserQuestion` below (a hollow marker is the P348 bug).
122
122
 
123
- **Map first, then stories one at a time (STORY-022 UX):**
123
+ **Ratify the map; its stories follow (STORY-022 UX, amended by ADR-103):**
124
124
 
125
125
  1. **Ratify the map.** Present the map's path/URL + a self-contained briefing of what it is — the JTBD it serves, its backbone activities, its release slices — briefing the substance BEFORE any ID (P350; the user may be on a device with no repo access). Then `AskUserQuestion` with exactly two options:
126
126
  - **Ratify** — the map is correct as-is.
@@ -128,17 +128,17 @@ Per architect amend finding 2 on Slice 7: story-map HTML files do NOT carry an a
128
128
 
129
129
  On **Ratify**: run `wr-itil-mark-story-oversight-confirmed <map-file>` (writes `confirmed` + the fingerprint). The map is now ratified.
130
130
 
131
- 2. **Ratify each story, one at a time.** ONLY after the map is ratified, walk the map's `data-story-id` references in order. For each story that is not already ratified (test with `wr-itil-detect-unratified-stories-maps` or the `is_story_map_ratified` lib helper): brief its `## User value` + `## Acceptance criteria` (substance before ID), then the SAME two-option `AskUserQuestion` (**Ratify** / type-something). On **Ratify**: `wr-itil-mark-story-oversight-confirmed <story-file>`. On free-text: apply the correction as a story edit and re-present (the edit re-opens that story only).
131
+ 2. **Do NOT ratify the stories at all (ADR-103).** Ratifying the map approves every story on it, so there is nothing left to do: no per-story marker, no per-story `AskUserQuestion`, and no oversight field written onto a story. `wr-itil-mark-story-oversight-confirmed` refuses a story path outright. Test a story's approval with `story_is_approved`, which derives it from the story's `story-maps:` field.
132
132
 
133
133
  3. **AFK / non-interactive (ADR-013 Rule 6).** When `AskUserQuestion` is unavailable, do NOT auto-ratify — that would forge a hollow marker (P348). Leave the artefacts unratified; they surface in the `/wr-itil:work-problems` Step 2.4 drain for the next interactive session.
134
134
 
135
- 4. **Single commit** — stage the map + every newly-ratified story + the README refresh; commit per ADR-014.
135
+ 4. **Single commit** — stage the map + the README refresh; commit per ADR-014. No story file changes: ratifying a map approves its stories without touching them.
136
136
 
137
- **Why map-first:** an RFC may reference only ratified stories (`wr-itil-check-rfc-stories-ratified`), and a story is only meaningful inside its ratified map — ratifying stories under an unratified map would invert the dependency STORY-022 encodes.
137
+ **Why the map:** an RFC may reference only approved stories (`wr-itil-check-rfc-stories-ratified`), and a story is only meaningful inside its map — which is why ADR-103 makes the map the single approval surface rather than ratifying at both tiers.
138
138
 
139
- **Reuse offers ratified stories only (STORY-024).** When decomposing a fix, existing map stories the fix touches are offered for **reuse** — referenced by an additional slice card / `data-story-id`, NOT duplicated as a new file; the reused story's `rfcs:` reverse-trace picks up the new RFC. Only **ratified** stories (test with `is_story_map_ratified`) are offered for reuse; an unratified story must be ratified (§ 7.5) before an RFC can reference it.
139
+ **Reuse offers approved stories only (STORY-024).** When decomposing a fix, existing map stories the fix touches are offered for **reuse** — referenced by an additional slice card / `data-story-id`, NOT duplicated as a new file; the reused story's `rfcs:` reverse-trace picks up the new RFC. Only **approved** stories (test with `story_is_approved`, which honours ADR-103 map inheritance) are offered for reuse; an unapproved story needs its map ratified (§ 7) before an RFC can reference it.
140
140
 
141
- **Re-slicing re-opens ratification (STORY-025).** Grouping stories into ordered release slices (Release 1 walking skeleton → Release 2 …) is authored as `data-status` / slice-card edits on the map; deferred phases stay first-class cards (visible, competing for priority), never buried. Because slicing edits the map's content, it drifts the `oversight-hash` and silently re-opens the map's ratification 7.5) no explicit marker reset needed; the fingerprint handles it.
141
+ **Re-slicing does NOT re-open ratification (STORY-025, amended by ADR-103).** Grouping stories into ordered release rows (Release 1 walking skeleton → Release 2 …) is authored as release-row and card edits on the map. **Composing a row asks the two questions in `/wr-itil:capture-story-map` — do these stories NEED to ship together, and SHOULD they — and re-slicing is where the second one usually gets skipped: rows drawn once are rarely re-examined against the cost of delay they now impose.** deferred phases stay first-class cards (visible, competing for priority), never buried. Rows and cards sit OUTSIDE the fingerprint basis, so re-slicing is silent — scheduling is not what the human approved. What DOES re-open a map's approval: a change to the map's own substance — see `oversight_map_substance_keys()`. *(This sentence enumerated five of the seven keys until 2026-08-08, omitting `storyMapId` and `secondaryPersona`. It is the under-enumeration half of that day's drift: it told a maintainer mid-re-slice that a secondary-persona edit was safe when it re-opens approval.)* The fingerprint handles it either way; no explicit marker reset is needed.
142
142
 
143
143
  ### 8. List flow (`list`)
144
144
 
@@ -170,9 +170,11 @@ Story-map ID + new status + invariants verified + parent artefacts touched + tra
170
170
 
171
171
  ## Related
172
172
 
173
- - **ADR-060** — Problem-RFC-Story framework; Phase 2 amendment 2026-05-10 lines 145-189 + encoding amendment 2026-05-12 lines 381-435.
174
- - **ADR-060 line 145** — I5 no-WSJF-on-maps invariant.
175
- - **ADR-060 line 339 + ADR-053** — bootstrap-exemption marker contract.
173
+ - **ADR-060** — Problem-RFC-Story framework; Phase 2 amendment 2026-05-10 (story-map frontmatter schema + the I3/I4/I5 invariants) + encoding amendment 2026-05-12.
174
+ - **ADR-060**, the no-WSJF-on-maps invariant — I5.
175
+ - **ADR-060**, the ordered `stories:` array on an RFC, **+ ADR-053** — bootstrap-exemption marker contract.
176
+
177
+ (Line-number citations were removed here: they are positional, and ADR-060 has been amended enough times that every one of them pointed at the wrong line.)
176
178
  - **`docs/STYLE-GUIDE.md`** — HTML style rules manage-story-map enforces.
177
179
  - **P170** — driver problem ticket.
178
180
  - **JTBD-008** — primary anchor.
@@ -10,10 +10,12 @@ Sibling to `/wr-itil:reconcile-readme` (P118 / ADR-014) and `/wr-itil:reconcile-
10
10
 
11
11
  **Diagnose-only mechanic** — wraps `packages/itil/scripts/reconcile-stories.sh` (resolved via `wr-itil-reconcile-stories` `$PATH` shim per ADR-049). The script reads `docs/stories/<state>/STORY-NNN-*.md` files across all 5 lifecycle subdirs (draft, accepted, in-progress, done, archived), parses `docs/stories/README.md`'s Story Rankings + Done tables, and reports each disagreement. Exit codes: `0` clean, `1` drift detected (structured stdout), `2` parse error.
12
12
 
13
- **Reverse-trace pass** — when `docs/problems/`, `docs/rfcs/`, and `docs/jtbd/` exist on disk (the default project layout), the reconciler ALSO checks the auto-maintained `## Stories` section on each parent artefact against the story frontmatter's `problems:` / `rfcs:` / `jtbd:` claims. Three drift kinds per parent tier (mirrors the RFC-tier reverse-trace contract):
13
+ **Reverse-trace pass** — when the parent directories exist, the reconciler checks auto-maintained `## Stories` sections on problem, JTBD and legacy RFC files. For an ADR-103 row-backed RFC, it checks the map row instead: the RFC must resolve and the row must contain the story card.
14
14
  - `MISSING_REVERSE_TRACE STORY-NNN in <PARENT-ID> ## Stories` — story claims parent but parent's `## Stories` table doesn't list the story
15
15
  - `STALE_REVERSE_TRACE STORY-NNN in <PARENT-ID> ## Stories` — parent lists the story but story no longer claims the parent
16
16
  - `STATUS_MISMATCH STORY-NNN in <PARENT-ID> ## Stories claims=<X> actual=<Y>` — parent's row claims one lifecycle status; story's filesystem subdir is a different state
17
+ - `UNRESOLVED_RFC_TRACE STORY-NNN claims=RFC-NNN` — neither a legacy RFC file nor a story-map release row exists
18
+ - `MISSING_REVERSE_TRACE STORY-NNN in RFC-NNN release row` — the release row exists but does not contain the story card
17
19
 
18
20
  ## When to invoke
19
21
 
@@ -30,7 +32,7 @@ Sibling to `/wr-itil:reconcile-readme` (P118 / ADR-014) and `/wr-itil:reconcile-
30
32
  ### 1. Run the diagnose script
31
33
 
32
34
  ```bash
33
- wr-itil-reconcile-stories docs/stories docs/problems docs/rfcs docs/jtbd > /tmp/wr-itil-stories-drift-$$.txt
35
+ wr-itil-reconcile-stories docs/stories docs/problems docs/rfcs docs/jtbd docs/story-maps > /tmp/wr-itil-stories-drift-$$.txt
34
36
  reconcile_exit=$?
35
37
  ```
36
38
 
@@ -47,6 +49,7 @@ Read `/tmp/wr-itil-stories-drift-$$.txt` line by line. Each line is one of:
47
49
  - `MISSING_REVERSE_TRACE STORY-NNN in <PARENT-ID> ## Stories` — parent's `## Stories` section needs the story added; call `update-<parent-kind>-references-section.sh <parent-file> "Stories"` to refresh.
48
50
  - `STALE_REVERSE_TRACE STORY-NNN in <PARENT-ID> ## Stories` — parent's `## Stories` section needs the story removed; same helper call (idempotent, lazy-empty discipline removes when no traces remain).
49
51
  - `STATUS_MISMATCH STORY-NNN in <PARENT-ID> ## Stories claims=<X> actual=<Y>` — same helper call refreshes the status column.
52
+ - `UNRESOLVED_RFC_TRACE` / row `MISSING_REVERSE_TRACE` — repair the map's release row or card through `/wr-itil:manage-story-map`; do not create an empty RFC file.
50
53
 
51
54
  ### 3. Apply edits
52
55
 
@@ -375,11 +375,11 @@ Before the orchestrator emits the final `ALL_DONE` sentinel for the AFK loop, it
375
375
 
376
376
  **Gate (0) × Step 0e `/goal` anchor (ADR-094).** Under an active goal, the `ALL_DONE` emit does not by itself end the session — the Step 0e external evaluator reads the printed gate (0) table + sentinel and independently confirms the condition holds; a premature emit just triggers a "keep working" turn with the evaluator's reason as guidance. The anchor is one-directional: a cleared goal (or a loop that was never anchored) does NOT relax this gate — gate (0) fires unconditionally either way.
377
377
 
378
- **Gate (a) — Outstanding-questions surface + oversight-unconfirmed drain (P348 amendment 2026-06-02).** Two sub-surfaces, both fire in this gate:
378
+ **Gate (a) — Outstanding-questions surface + oversight-unconfirmed drain (ADR-110 / P348).** Two sub-surfaces, both fire in this gate:
379
379
 
380
380
  1. *Outstanding-questions surface.* Read `.afk-run-state/outstanding-questions.jsonl`. If non-empty, invoke Step 2.5b's surfacing routine to present the accumulated queue (via `AskUserQuestion`-when-available-else-table per ADR-013 Rule 1 / Rule 6). On completion, truncate the queue file. If the queue is empty, this sub-surface returns immediately. The surfacing routine is the existing Step 2.5b — Step 2.4 does NOT re-implement; it sequences.
381
381
 
382
- 2. *Oversight-unconfirmed drain.* Run `wr-architect-detect-unoversighted`, `wr-jtbd-detect-unoversighted`, and `wr-itil-detect-unratified-stories-maps` (all ADR-049 PATH shims, all always exit 0; output is the list of unoversighted/unratified artefact paths). The story/map detector (**ADR-090**) is **drift-aware**: it lists story maps + stories that are never-ratified, explicitly `unconfirmed`, OR **drift-reopened** — a `confirmed` marker whose `oversight-hash` fingerprint no longer matches the edited content. Re-ratify each via `/wr-itil:manage-story` / `/wr-itil:manage-story-map` (or `wr-itil-mark-story-oversight-confirmed`); the nudge + 2-option `Drain now` / `Defer` surface below applies to this list identically. If either lists ≥ 1 artefact whose frontmatter carries `human-oversight: unconfirmed` (the AFK-explicit-deferred state, distinct from the implicit-absent state pre-existing ADR/JTBD files carry), surface a one-line nudge: *"N iter-deferred decision(s)/job(s) carry `human-oversight: unconfirmed`. Run `/wr-architect:review-decisions` and `/wr-jtbd:confirm-jobs-and-personas` to drain."* If `AskUserQuestion` is available (`/wr-itil:work-problems` was invoked interactively before the AFK loop started), surface a 2-option choice — `Drain now` (invokes the appropriate drain skill before `ALL_DONE`) / `Defer to next session` (proceeds to gate (b) with the nudge in the final summary). If `AskUserQuestion` is unavailable, the nudge prints in the final summary table and gate (b) proceeds. The drain is NOT a halt — `unconfirmed` markers are explicit-by-design AFK signals (the iter wrote them KNOWING the user would need to confirm), and the drain is the documented path. Detector difference matters: ADRs/JTBDs that pre-date the ADR-066/ADR-068 marker contract carry NO `human-oversight:` line at all; they fall through to the existing review-decisions/confirm-jobs-and-personas backlog drain (no new surfacing here). The new surfacing fires ONLY on the explicit `unconfirmed` value — the AFK-iter-deferred class P348 introduces.
382
+ 2. *Oversight-unconfirmed drain.* Run `wr-architect-detect-unoversighted`, `wr-jtbd-detect-unoversighted`, and `wr-itil-detect-unratified-stories-maps` (all ADR-049 PATH shims, all always exit 0; output is the list of unoversighted/unratified artefact paths). The story/map detector (**ADR-090** / **ADR-103**) is **drift-aware**: it lists story MAPS that are never-ratified, explicitly `unconfirmed`, or **drift-reopened** — a `confirmed` marker whose `oversight-hash` no longer matches the map's substance — plus the stories those maps leave unapproved. Re-ratify via `/wr-itil:manage-story-map` (or `wr-itil-mark-story-oversight-confirmed <map-file>`) — never per-story: a story carries no marker and the command refuses one; the nudge + 2-option `Drain now` / `Defer` surface below applies to this list identically. If either lists ≥ 1 artefact whose frontmatter carries `human-oversight: unconfirmed` (the AFK-explicit-deferred state, distinct from the implicit-absent state pre-existing ADR/JTBD files carry), surface a one-line nudge: *"N iter-deferred decision(s)/job(s) carry `human-oversight: unconfirmed`. Run `/wr-architect:review-decisions` and `/wr-jtbd:confirm-jobs-and-personas` to drain."* If `AskUserQuestion` is available (`/wr-itil:work-problems` was invoked interactively before the AFK loop started), surface a 2-option choice — `Drain now` (invokes the appropriate drain skill before `ALL_DONE`) / `Defer to next session` (proceeds to gate (b) with the nudge in the final summary). If `AskUserQuestion` is unavailable, the nudge prints in the final summary table and gate (b) proceeds. The drain is NOT a halt — `unconfirmed` markers are explicit-by-design AFK signals (the iter wrote them KNOWING the user would need to confirm), and the drain is the documented path. Detector difference matters: ADRs/JTBDs that pre-date the ADR-066/ADR-068 marker contract carry NO `human-oversight:` line at all; they fall through to the existing review-decisions/confirm-jobs-and-personas backlog drain (no new surfacing here). The new surfacing fires ONLY on the explicit `unconfirmed` value — the AFK-iter-deferred class P348 introduces.
383
383
 
384
384
  **Gate (b) — Session-level retro.** Invoke `/wr-retrospective:run-retro` via the Skill tool. This is the **orchestrator-main-turn session-level retro**, distinct from the per-iter retro fired inside each iter subprocess (per P086 / Step 5 retro-on-exit clause). The session-level retro covers cross-iter patterns, friction observations, framework-improvement candidates, and the AFK loop's overall trajectory — surface visible only after multiple iters have completed. Retro commits its own work per ADR-014; any tickets retro creates ride retro's own commit, and the orchestrator picks them up on the *next* invocation of `/wr-itil:work-problems` rather than re-entering the loop here.
385
385
 
@@ -0,0 +1,195 @@
1
+ /* Story-map presentation. One copy, shared by every map.
2
+ Generated into a repo beside the maps by wr-itil-render-story-map;
3
+ edit the copy shipped in @windyroad/itil, not this one. */
4
+ :root {
5
+ color-scheme: light dark;
6
+ --bg: #ffffff; --fg: #1a1a1a; --muted: #565656; --line: #767676;
7
+ --card: #f6f6f6; --card-line: #767676;
8
+ --live-fg: #0b5d1e; --live-bg: #e4f4e8; --live-line: #0b5d1e;
9
+ --next-fg: #0b3a66; --next-bg: #e2eefb; --next-line: #0b3a66;
10
+ --later-fg: #6b3a00; --later-bg: #f6ead6; --later-line: #6b3a00;
11
+ --defect-fg: #8c1d18; --defect-bg: #fdecea; --defect-line: #8c1d18;
12
+ --focus: #0b3a66;
13
+ }
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --bg: #16181c; --fg: #f2f3f5; --muted: #b6bac2; --line: #8a919c;
17
+ --card: #1f2228; --card-line: #8a919c;
18
+ --live-fg: #7fe0a0; --live-bg: #123020; --live-line: #7fe0a0;
19
+ --next-fg: #97c6f5; --next-bg: #10263c; --next-line: #97c6f5;
20
+ --later-fg: #f0c58a; --later-bg: #33260f; --later-line: #f0c58a;
21
+ --defect-fg: #ffb4ab; --defect-bg: #3b1512; --defect-line: #ffb4ab;
22
+ --focus: #97c6f5;
23
+ }
24
+ }
25
+ :root[data-theme="light"] {
26
+ color-scheme: light;
27
+ --bg: #ffffff; --fg: #1a1a1a; --muted: #565656; --line: #767676;
28
+ --card: #f6f6f6; --card-line: #767676;
29
+ --live-fg: #0b5d1e; --live-bg: #e4f4e8; --live-line: #0b5d1e;
30
+ --next-fg: #0b3a66; --next-bg: #e2eefb; --next-line: #0b3a66;
31
+ --later-fg: #6b3a00; --later-bg: #f6ead6; --later-line: #6b3a00;
32
+ --defect-fg: #8c1d18; --defect-bg: #fdecea; --defect-line: #8c1d18;
33
+ --focus: #0b3a66;
34
+ }
35
+ :root[data-theme="dark"] {
36
+ color-scheme: dark;
37
+ --bg: #16181c; --fg: #f2f3f5; --muted: #b6bac2; --line: #8a919c;
38
+ --card: #1f2228; --card-line: #8a919c;
39
+ --live-fg: #7fe0a0; --live-bg: #123020; --live-line: #7fe0a0;
40
+ --next-fg: #97c6f5; --next-bg: #10263c; --next-line: #97c6f5;
41
+ --later-fg: #f0c58a; --later-bg: #33260f; --later-line: #f0c58a;
42
+ --defect-fg: #ffb4ab; --defect-bg: #3b1512; --defect-line: #ffb4ab;
43
+ --focus: #97c6f5;
44
+ }
45
+ * { box-sizing: border-box; }
46
+ body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg);
47
+ max-width: 1500px; margin: 0 auto; padding: 1.5rem 1rem 3rem; line-height: 1.5; }
48
+ h1 { font-size: 1.5rem; margin: 0 0 .5rem; }
49
+ p { max-width: 74ch; }
50
+ a { color: var(--focus); }
51
+ /* Required on every interactive element, not just links: the theme toggle
52
+ (if a host adds one) and the scroll region both take focus. */
53
+ /* :focus first as a fallback for engines without :focus-visible. The two rules
54
+ are deliberately IDENTICAL — the fallback exists so the only focusable
55
+ element on the page is never left without an indicator on older WebKit, not
56
+ to give keyboard and pointer focus different treatment. Do not "complete"
57
+ the pair by weakening the :focus rule; that would break 2.4.7 on those
58
+ engines.
59
+ No border-radius here — it clips the content of the overflow:auto
60
+ scroll region for as long as it holds focus. */
61
+ a:focus, button:focus, [tabindex]:focus { outline: 3px solid var(--focus); outline-offset: 2px; }
62
+ a:focus-visible,
63
+ button:focus-visible,
64
+ [tabindex]:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
65
+ .badge { display: inline-flex; align-items: center; gap: .3rem; font-size: .75rem; font-weight: 700;
66
+ padding: .1rem .45rem; border-radius: 999px; border: 1px solid; white-space: nowrap; }
67
+ /* Release state is carried by the badge's text label and by a glyph in real
68
+ markup (see .b-glyph). The glyph is aria-hidden — it is a second VISUAL
69
+ channel so colour is never the sole one, and assistive tech already has
70
+ the label. It must not be generated content: in forced-colors mode the
71
+ badge backgrounds all collapse to Canvas, and the glyph becomes the only
72
+ thing distinguishing the three states. */
73
+ .b-live { color: var(--live-fg); background: var(--live-bg); border-color: var(--live-line); }
74
+ .b-next { color: var(--next-fg); background: var(--next-bg); border-color: var(--next-line); }
75
+ .b-later { color: var(--later-fg); background: var(--later-bg); border-color: var(--later-line); }
76
+ /* A row with no RFC identity, or whose stories close no problem. Deliberately
77
+ red rather than the amber `.b-later` used for scheduled-later work: this is
78
+ not "later", it is wrong, and it needs an edit before the map is ratified.
79
+ 7.97:1 light, 9.50:1 dark; the glyph carries it as a second channel. */
80
+ .b-defect { color: var(--defect-fg); background: var(--defect-bg); border-color: var(--defect-line); }
81
+
82
+ .scroll { overflow-x: auto; padding-bottom: .5rem; }
83
+ table.map { border-collapse: collapse; width: 100%; min-width: 940px; }
84
+ table.map caption { text-align: left; color: var(--muted); font-size: .85rem; margin-bottom: .5rem; }
85
+ /* Activity headers across the top are the backbone: the user journey. */
86
+ thead th.act { text-align: left; vertical-align: bottom; padding: .5rem .6rem; font-size: .9rem;
87
+ border-bottom: 3px solid var(--fg); width: 18%; }
88
+ thead th.act .jtbd { display: block; font-weight: 400; color: var(--muted); font-size: .75rem; margin-top: .2rem; }
89
+ thead td.corner { border-bottom: 3px solid var(--fg); }
90
+ /* Each row is one release slice. A row read left to right is everything
91
+ that ships together. */
92
+ th.slice { text-align: left; vertical-align: top; padding: .7rem .6rem; width: 10%; min-width: 96px;
93
+ border-bottom: 2px solid var(--fg); position: sticky; left: 0; background: var(--bg); }
94
+ th.slice .s-name { font-size: .95rem; display: block; margin-top: .35rem; margin-bottom: .35rem; }
95
+ th.slice .s-note { font-weight: 400; color: var(--muted); font-size: .75rem; display: block; margin-top: .35rem; }
96
+ td.cell { vertical-align: top; padding: .5rem; border-bottom: 2px solid var(--fg); border-left: 1px solid var(--line); }
97
+ /* An empty cell is meaningful — it says this activity ships nothing in this
98
+ release — so the hatch is drawn in the 3:1 border tone, not a tint. */
99
+ td.cell.empty { background: repeating-linear-gradient(45deg, transparent, transparent 7px, var(--line) 7px, var(--line) 8px); }
100
+ .task { border: 1px solid var(--card-line); border-radius: 6px; background: var(--card); padding: .5rem .55rem; }
101
+ .task .t-title { font-size: .85rem; }
102
+ .task .t-value { display: block; font-size: .75rem; color: var(--muted); margin-top: .35rem; }
103
+ /* Three clauses, three lines. Run together they are a wall of text at card
104
+ width; the shape that makes the statement scannable is the shape it was
105
+ written in. */
106
+ .task .v-line { display: block; margin-top: .2rem; }
107
+ .task .v-line:first-child { margin-top: 0; }
108
+ /* The connectives carry no information once you know the pattern, so they
109
+ recede and let the three payloads read as a list. The italic IS the channel —
110
+ there is deliberately no opacity. At .75 over --card this composited to
111
+ #7e7e7e for 3.76:1, under the 4.5:1 that 12px text needs; the muted token
112
+ alone is 6.79:1. Receding must not mean unreadable. */
113
+ .task .v-lead { font-style: italic; }
114
+ /* Whose journey this is, is the thing you scan a map for — so the line carries
115
+ the body colour rather than the muted value tone. Structural prominence, not
116
+ fabricated emphasis: bold here belongs to whatever the STORY marked. */
117
+ .task .v-asa { color: var(--fg); }
118
+ /* Author emphasis, rendered as written. */
119
+ .task .t-value strong { font-weight: 700; color: var(--fg); }
120
+ /* A card's own lifecycle state. Subordinate to the row badge by four axes at
121
+ once — no pill, no fill, smaller and lighter, on its own line, and only the
122
+ three states worth colouring get colour. Twenty-four cards would otherwise
123
+ put twenty-four coloured markers against five row badges.
124
+
125
+ No `forced-color-adjust: none` here, unlike `.badge`: that opts out because
126
+ its state lives in its fill; this marker's lives in its text, so letting the
127
+ UA force the colours is correct. */
128
+ .task .t-status {
129
+ display: block;
130
+ font-size: .7rem;
131
+ font-weight: 600;
132
+ margin-bottom: .15rem;
133
+ letter-spacing: .01em;
134
+ }
135
+ .task .ts-glyph { font-weight: 700; margin-right: .25rem; }
136
+
137
+ /* Coloured: the states worth picking out of a wall of cards. Border-only —
138
+ a fill on a data-bearing element is prohibited, and the row badge's own
139
+ fill is 1.14:1 anyway, so its loudness was never coming from the fill. */
140
+ .task .ts-done { color: var(--live-fg); }
141
+ .task .ts-prog { color: var(--later-fg); }
142
+ /* `--muted`, NOT `--line`. Read literally, "archived = light gray" lands on
143
+ --line, which is 4.20:1 on --card in light mode — under the 4.5:1 that .7rem
144
+ text needs, and passing in dark at 5.02:1, so it fails in exactly one theme.
145
+ Same shape as the 3.76:1 .v-lead miss. */
146
+ .task .ts-arch { color: var(--muted); }
147
+
148
+ /* Achromatic: the common states, kept as texture so the coloured ones read. */
149
+ .task .ts-acc,
150
+ .task .ts-draft { color: var(--muted); font-weight: 400; }
151
+
152
+ .task .t-ref { display: block; font-size: .75rem; color: var(--muted); margin-top: .3rem; }
153
+ .slice .s-problems { display: block; font-size: .7rem; color: var(--muted); margin-top: .2rem; }
154
+ .traces { margin-top: 1.5rem; border-top: 1px solid var(--line); padding-top: .75rem;
155
+ font-size: .8rem; color: var(--muted); }
156
+ .traces .tr-group { display: inline-block; margin-right: 1.25rem; }
157
+ /* The caption is the table's accessible name — a screen-reader user gets it,
158
+ and the dimensions in it, before entering the grid. On screen it repeated
159
+ the h1 immediately above, so it is hidden rather than deleted. */
160
+ table.map > caption { position: absolute; width: 1px; height: 1px; padding: 0;
161
+ margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
162
+
163
+ .vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
164
+ overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
165
+ .tasks { list-style: none; margin: 0; padding: 0; }
166
+ .tasks li + li { margin-top: .5rem; }
167
+ .b-glyph { font-weight: 900; }
168
+ .genfrom { color: var(--muted); font-size: .8rem; margin-top: 1.5rem; }
169
+ @media (forced-colors: active) {
170
+ /* background-image is not part of the forced-colors UA cascade, so the
171
+ hatch would paint literal #767676 over the user's chosen Canvas in
172
+ Chromium and vanish entirely in Firefox. Restate it in system colours. */
173
+ td.cell.empty { background-image: repeating-linear-gradient(45deg, Canvas, Canvas 7px, GrayText 7px, GrayText 8px); }
174
+ .badge { forced-color-adjust: none; border: 1px solid CanvasText; }
175
+ }
176
+ /* What the reader is being asked. Sits between the title and the grid, so it
177
+ is read before any scrolling and cannot be missed by not scrolling back. */
178
+ .orient { max-width: 62ch; margin: .5rem 0 1rem; font-size: .95rem; line-height: 1.5; }
179
+ .orient + .orient { color: var(--muted); }
180
+
181
+ /* The column header is the journey step a card belongs to, and rows run taller
182
+ than a screen — so without this it scrolls away inside the first row and
183
+ never comes back, leaving horizontal scroll position as the only clue to
184
+ which activity you are looking at. The row header outranks it at the corner
185
+ intersection: it is pinned on the axis that moves further. */
186
+ thead th.act { position: sticky; top: 0; z-index: 1; background: var(--bg); }
187
+ th.slice { z-index: 2; }
188
+
189
+ /* Sticky row headers are kept at narrow widths: this is exactly where the
190
+ table scrolls furthest, so losing the orientation anchor costs most. The
191
+ column is narrowed and its note dropped rather than unpinned. */
192
+ @media (max-width: 640px) {
193
+ th.slice { min-width: 72px; padding: .5rem .35rem; font-size: .85rem; }
194
+ th.slice .s-note { display: none; }
195
+ }
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{{TITLE_FULL}}</title>
7
+ {{META}}
8
+ <link rel="stylesheet" href="../story-map.css">
9
+ </head>
10
+ <body>
11
+ <main>
12
+ <h1 id="story-map-title">{{TITLE}}</h1>
13
+ {{ORIENT}} <div id="story-map">{{GRID}}</div>
14
+ {{TRACE}}
15
+ </main>
16
+
17
+
18
+ <script id="story-map-data" type="application/json">
19
+ {{DATA}}
20
+ </script>
21
+ {{STATUS}}
22
+ </body>
23
+ </html>
@@ -1,230 +0,0 @@
1
- #!/usr/bin/env bash
2
- # check-afk-accept-eligible.sh — ADR-101 AFK pure-decomposition carve-out.
3
- #
4
- # Decides whether an AFK loop may transition a story `draft -> accepted` with a
5
- # machine-written `human-oversight: confirmed` marker, and implement it, WITHOUT
6
- # a fresh human ratification. Opt-in and fail-closed: absent the project opt-in
7
- # or the story's own declaration, the answer is always "not eligible".
8
- #
9
- # Eligible IFF the project has opted in AND the story declares
10
- # `afk-accept: pure-decomposition` AND both conditions hold:
11
- #
12
- # (a) PARENT SUBSTANCE CONFIRMED — every `adrs:` / `jtbd:` entry resolves and
13
- # carries `human-oversight: confirmed` (persona too); every `rfcs:` entry
14
- # resolves and every ADR in THAT RFC's `adrs:` is confirmed; every
15
- # `problems:` entry resolves; every `story-maps:` entry satisfies the
16
- # ADR-101 map leg (fully ratified, OR confirmed with this story's own card
17
- # excluded from the hash — ADR-095 compels that card, so it cannot be the
18
- # thing that disqualifies the story). Map LIFECYCLE status is out of scope.
19
- # The RFC tier holds no independent decisions per ADR-070 and so has no
20
- # oversight marker of its own; condition (a) proxies through its `adrs:`.
21
- #
22
- # (b) NO NEW SUBSTANCE, ESTABLISHED POSITIVELY — a whitelist, not a novelty
23
- # blacklist, because "introduces no new design choice" is a negative
24
- # existential an agent cannot discharge. `## Decomposition basis` must
25
- # carry exactly one entry per acceptance criterion, each naming the
26
- # artefact whose already-confirmed clause that criterion decomposes, and
27
- # every cited ID must be a member of the set (a) PROVED ratified —
28
- # frontmatter membership is not ratification. Zero criteria is FAIL-CLOSED.
29
- # A secondary blacklist rejects open-decision markers outside fenced or
30
- # backticked spans.
31
- #
32
- # The failure direction is deliberate: conservatively hold a genuine
33
- # decomposition rather than silently accept an embedded decision.
34
- #
35
- # Usage: check-afk-accept-eligible.sh <story-file> [maps-root] [rfcs-root]
36
- # [jtbd-root] [problems-root] [decisions-root]
37
- # Exit: 0 = eligible; 1 = not eligible (reasons on stderr); 2 = usage / file error.
38
- #
39
- # @adr ADR-101 (AFK pure-decomposition carve-out) ADR-090 (drift-invalidated
40
- # oversight) ADR-095 ADR-096 ADR-060 ADR-070 ADR-098 (config shape)
41
- # ADR-013 (Rule 6) ADR-052 (behavioural bats)
42
- # @problem P456 (AFK iter cannot land a fix) P465 (accepted gate unenforced)
43
- # @jtbd JTBD-006 (Progress the Backlog While I'm Away)
44
- # @jtbd JTBD-003 (Compose Only the Guardrails I Need)
45
- set -uo pipefail
46
-
47
- # Adopter-safe: source the shared lazy-fingerprint lib RELATIVE TO THIS SCRIPT
48
- # (P317), never repo-relative.
49
- LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
50
- echo "check-afk-accept-eligible: cannot locate lib dir" >&2; exit 2; }
51
- # shellcheck source=/dev/null
52
- source "$LIB/story-oversight.sh"
53
-
54
- story="${1:-}"
55
- maps_root="${2:-docs/story-maps}"
56
- rfcs_root="${3:-docs/rfcs}"
57
- jtbd_root="${4:-docs/jtbd}"
58
- problems_root="${5:-docs/problems}"
59
- decisions_root="${6:-docs/decisions}"
60
-
61
- if [ -z "$story" ]; then
62
- echo "check-afk-accept-eligible: usage: check-afk-accept-eligible.sh <story-file> [maps-root] [rfcs-root] [jtbd-root] [problems-root] [decisions-root]" >&2
63
- exit 2
64
- fi
65
- [ -f "$story" ] || { echo "check-afk-accept-eligible: file not found: $story" >&2; exit 2; }
66
-
67
- fail() { echo "check-afk-accept-eligible: NOT ELIGIBLE — $1" >&2; exit 1; }
68
-
69
- # --- Project opt-in (ADR-101 split; config shape mirrors ADR-098) ------------
70
- # Resolution FAILS OPEN to the built-in default (this never errors); because the
71
- # built-in default is `false`, the POLICY outcome of every degraded path — no
72
- # jq, no file, unreadable, unparseable, non-boolean — is not-eligible.
73
- # Env trumps both files: a loosening override for bats and one-off use, NOT a
74
- # project-configuration surface.
75
- afk_accept_enabled() {
76
- local v
77
- case "${WR_ITIL_AFK_ACCEPT:-}" in
78
- 1|true) return 0 ;;
79
- 0|false) return 1 ;;
80
- esac
81
- command -v jq >/dev/null 2>&1 || return 1
82
- for cfg in ".claude/itil.config.json" "$HOME/.claude/itil.config.json"; do
83
- [ -r "$cfg" ] || continue
84
- # Per-KEY fallback per ADR-098: a project file that simply omits the key
85
- # must not strand the machine layer. Only a file that actually sets it
86
- # decides the answer.
87
- jq -e 'has("afk_accept_pure_decomposition")' "$cfg" >/dev/null 2>&1 || continue
88
- # Strict: only the literal JSON `true` enables. The string form, 1, yes and
89
- # null do not — a typo must never open a permission-loosening carve-out.
90
- v="$(jq -r 'if .afk_accept_pure_decomposition == true then "true" else "false" end' "$cfg" 2>/dev/null || echo false)"
91
- [ "$v" = "true" ] && return 0
92
- return 1
93
- done
94
- return 1
95
- }
96
-
97
- afk_accept_enabled || fail "the project has not opted in to the ADR-101 carve-out (set \"afk_accept_pure_decomposition\": true in .claude/itil.config.json)"
98
-
99
- # --- Declaration ------------------------------------------------------------
100
- oversight_declares_pure_decomposition "$story" \
101
- || fail "$story does not declare \`afk-accept: pure-decomposition\` — the carve-out is opt-in per story, never a default"
102
-
103
- story_id="$(basename "$story" | grep -oE 'STORY-[0-9]+' | head -1)"
104
- [ -n "$story_id" ] || fail "cannot resolve a STORY-NNN id from $story"
105
-
106
- # Read one frontmatter list field (inline `[A, B]` or block `- A` form).
107
- trace_ids() {
108
- local field="$1" line ids
109
- line="$(awk -v f="^${field}:" '$0 ~ f {print; exit}' "$story")"
110
- ids="$(printf '%s' "$line" | grep -oE '(ADR|JTBD|RFC|STORY-MAP|P)-?[0-9]+' || true)"
111
- if [ -z "$ids" ]; then
112
- ids="$(awk -v f="^${field}:" '$0 ~ f {g=1;next} g&&/^[[:space:]]*-/{print} g&&/^[^[:space:]-]/{exit}' "$story" \
113
- | grep -oE '(ADR|JTBD|RFC|STORY-MAP|P)-?[0-9]+' || true)"
114
- fi
115
- printf '%s' "$ids"
116
- }
117
-
118
- # The set condition (a) PROVES ratified. Condition (b) intersects against this,
119
- # so a basis entry can never cite an artefact (a) never verified (B4).
120
- RATIFIED_SET=""
121
- add_ratified() { RATIFIED_SET="$RATIFIED_SET $1"; }
122
-
123
- # (a) ADR parents — write-once oversight per ADR-066.
124
- for id in $(trace_ids adrs); do
125
- n="${id#ADR-}"
126
- f="$(ls "$decisions_root"/"$n"-*.md 2>/dev/null | head -1)"
127
- [ -n "$f" ] || fail "$id does not resolve under $decisions_root — a new decision is new substance"
128
- oversight_is_confirmed "$f" || fail "$id is not \`human-oversight: confirmed\` — condition (a) requires every parent decision be human-confirmed"
129
- add_ratified "$id"
130
- done
131
-
132
- # (a) JTBD parents + their personas — ADR-068 oversight sibling.
133
- for id in $(trace_ids jtbd); do
134
- n="${id#JTBD-}"
135
- f="$(ls "$jtbd_root"/*/"JTBD-$n"-*.md 2>/dev/null | head -1)"
136
- [ -n "$f" ] || fail "$id does not resolve under $jtbd_root — a new job is new substance"
137
- oversight_is_confirmed "$f" || fail "$id is not \`human-oversight: confirmed\`"
138
- p="$(dirname "$f")/persona.md"
139
- if [ -f "$p" ]; then
140
- oversight_is_confirmed "$p" || fail "the persona for $id ($p) is not \`human-oversight: confirmed\` — a new or unconfirmed persona is new substance"
141
- fi
142
- add_ratified "$id"
143
- done
144
-
145
- # (a) RFC parents — no oversight tier of their own (ADR-070); proxy via `adrs:`.
146
- for id in $(trace_ids rfcs); do
147
- n="${id#RFC-}"
148
- f="$(ls "$rfcs_root"/"RFC-$n"-*.md 2>/dev/null | head -1)"
149
- [ -n "$f" ] || fail "$id does not resolve under $rfcs_root"
150
- rfc_adrs="$(awk '/^adrs:/{print; exit}' "$f" | grep -oE 'ADR-[0-9]+' || true)"
151
- for a in $rfc_adrs; do
152
- af="$(ls "$decisions_root"/"${a#ADR-}"-*.md 2>/dev/null | head -1)"
153
- [ -n "$af" ] || fail "$id depends on $a which does not resolve under $decisions_root"
154
- oversight_is_confirmed "$af" || fail "$id depends on $a which is not \`human-oversight: confirmed\`"
155
- add_ratified "$a"
156
- done
157
- add_ratified "$id"
158
- done
159
-
160
- # (a) Problem parents — resolve only; problems carry no oversight tier.
161
- for id in $(trace_ids problems); do
162
- n="${id#P}"
163
- f="$(ls "$problems_root"/"$n"-*.md "$problems_root"/*/"$n"-*.md 2>/dev/null | head -1)"
164
- [ -n "$f" ] || fail "$id does not resolve under $problems_root"
165
- add_ratified "$id"
166
- done
167
-
168
- # (a) Story-map parents — the ADR-101 map leg.
169
- for id in $(trace_ids story-maps); do
170
- n="${id#STORY-MAP-}"
171
- f="$(ls "$maps_root"/*/"STORY-MAP-$n"-*.html 2>/dev/null | head -1)"
172
- [ -n "$f" ] || fail "$id does not resolve under $maps_root"
173
- oversight_map_leg_ok "$f" "$story_id" \
174
- || fail "$id fails the ADR-101 map leg — it is not human-confirmed, or it has drifted for a reason other than adding ${story_id}'s own card. Ratify the map."
175
- add_ratified "$id"
176
- done
177
-
178
- # --- (b) NO NEW SUBSTANCE, established positively ---------------------------
179
- # Acceptance criteria: prefix-anchored section (headings carry trailing
180
- # qualifiers like `(accepted-gate, INVEST Testable)`), terminated by the next
181
- # `##`. Tick-insensitive so implementation progress never flips the check, and
182
- # the criterion literal is aligned with oversight_content_hash's normaliser so
183
- # every counted line is a normalised line.
184
- section_of() {
185
- awk -v h="$1" '
186
- $0 ~ h {inside=1; next}
187
- inside && /^##[[:space:]]/ {exit}
188
- inside {print}
189
- ' "$story"
190
- }
191
-
192
- # NO `\b` IN THESE ANCHORS, and do not re-add it. Two independent reasons:
193
- # (1) awk escape-processes a `-v` assignment value, so `\b` arrives as a literal
194
- # BACKSPACE (0x08) — the pattern then never matches a real heading, which
195
- # silently makes this whole check inert. It fails CLOSED, so there is no
196
- # permission hole, but the carve-out can never return eligible for anyone.
197
- # (2) POSIX ERE has no `\b` at all, so it is equally wrong in the commit-locus
198
- # copy of this count in itil-no-implement-draft-gate.sh, where the failure
199
- # would be silent in a different way. Those two loci MUST stay aligned.
200
- # Prefix matching is the documented intent anyway (ADR-101): corpus headings
201
- # carry trailing qualifiers like `(accepted-gate, INVEST Testable)`.
202
- criteria_count="$(section_of '^##[[:space:]]+Acceptance criteria' | grep -cE '^- \[[ xX]\]' || true)"
203
- [ "${criteria_count:-0}" -gt 0 ] \
204
- || fail "no top-level \`- [ ]\` criterion lines found in the acceptance-criteria section — condition (b) is fail-closed on zero, since an absent section is indistinguishable from absent criteria"
205
-
206
- basis="$(section_of '^##[[:space:]]+Decomposition basis')"
207
- [ -n "$(printf '%s' "$basis" | tr -d '[:space:]')" ] \
208
- || fail "\`## Decomposition basis\` is missing or empty — condition (b) requires each criterion to name the already-confirmed clause it decomposes"
209
-
210
- basis_count="$(printf '%s\n' "$basis" | grep -cE '^- ' || true)"
211
- [ "${basis_count:-0}" -eq "$criteria_count" ] \
212
- || fail "\`## Decomposition basis\` has $basis_count entries but there are $criteria_count acceptance criteria — every criterion must name the confirmed clause it decomposes"
213
-
214
- # Every cited ID must be in the set condition (a) proved ratified (B4).
215
- for cited in $(printf '%s' "$basis" | grep -oE '(ADR|JTBD|RFC|STORY-MAP)-[0-9]+' | sort -u); do
216
- case " $RATIFIED_SET " in
217
- *" $cited "*) ;;
218
- *) fail "\`## Decomposition basis\` cites $cited, which condition (a) never verified as ratified — cite only traced, confirmed parents" ;;
219
- esac
220
- done
221
-
222
- # Secondary blacklist. Fenced and backticked spans are stripped first so a story
223
- # may DESCRIBE the blocked vocabulary (this one does) without tripping on it.
224
- prose="$(awk '/^```/{f=!f;next} !f' "$story" | sed -E 's/`[^`]*`//g')"
225
- if printf '%s' "$prose" | grep -qiE '\[Unratified Dependency\]|\bTBD\b|to be decided|open question|decision needed'; then
226
- fail "an open-decision marker appears outside a fenced or backticked span — the story is not pure decomposition"
227
- fi
228
-
229
- echo "check-afk-accept-eligible: ELIGIBLE — $story_id is pure decomposition of confirmed substance ($criteria_count criteria, all basis citations ratified)" >&2
230
- exit 0