@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -14
- package/bin/{wr-itil-check-afk-accept-eligible → wr-itil-render-story-map} +2 -2
- package/bin/wr-itil-story-map-edit +51 -0
- package/bin/wr-itil-story-map-query +51 -0
- package/hooks/itil-no-implement-draft-gate.sh +19 -28
- package/lib/story-oversight.sh +154 -60
- package/package.json +2 -1
- package/scripts/check-rfc-stories-ratified.sh +15 -9
- package/scripts/detect-unratified-stories-maps.sh +15 -32
- package/scripts/mark-story-oversight-confirmed.sh +80 -31
- package/scripts/migrate-story-status-mirror.sh +7 -3
- package/scripts/reconcile-stories.sh +26 -5
- package/scripts/render-story-map.mjs +1057 -0
- package/scripts/render-story-map.sh +15 -0
- package/scripts/story-map-edit.mjs +280 -0
- package/scripts/story-map-edit.sh +10 -0
- package/scripts/story-map-query.mjs +227 -0
- package/scripts/story-map-query.sh +68 -0
- package/scripts/update-story-references-section.sh +6 -1
- package/skills/capture-rfc/SKILL.md +1 -1
- package/skills/capture-story/SKILL.md +16 -8
- package/skills/capture-story-map/SKILL.md +102 -59
- package/skills/list-stories/SKILL.md +16 -10
- package/skills/list-story-maps/SKILL.md +1 -1
- package/skills/manage-rfc/SKILL.md +1 -1
- package/skills/manage-story/SKILL.md +19 -26
- package/skills/manage-story-map/SKILL.md +19 -17
- package/skills/reconcile-stories/SKILL.md +5 -2
- package/skills/work-problems/SKILL.md +2 -2
- package/templates/story-map.css +195 -0
- package/templates/story-map.html +23 -0
- 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
|
|
28
|
-
| **I4** trace-to-JTBD | Every map traces to ≥ 1 JTBD (ADR-060
|
|
29
|
-
| **I5** no-WSJF-leak | Maps MUST NOT carry WSJF (ADR-060
|
|
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-
|
|
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,
|
|
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
|
|
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
|
-
**
|
|
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. **
|
|
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 +
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
174
|
-
- **ADR-060
|
|
175
|
-
- **ADR-060
|
|
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
|
|
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 (
|
|
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
|
|
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
|