@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
@@ -1,25 +1,21 @@
1
1
  #!/usr/bin/env bash
2
2
  # detect-unratified-stories-maps.sh — ADR-090 detector.
3
3
  #
4
- # Token-cheap detection of story maps + stories that are NOT ratified. Mirrors
5
- # wr-architect detect-unoversighted.sh but spans two artefact types and is
6
- # DRIFT-AWARE via the shared lazy-fingerprint helper: an artefact is ratified
7
- # only when it carries a `confirmed` human-oversight marker AND a stored
8
- # oversight-hash matching its current content. This surfaces three cases as the
9
- # drain queue:
4
+ # Token-cheap detection of story maps that are NOT ratified, plus the stories
5
+ # those maps leave unapproved. Mirrors wr-architect detect-unoversighted.sh.
6
+ #
7
+ # A MAP is ratified only when it carries a `confirmed` human-oversight marker AND
8
+ # a stored oversight-hash matching its current substance, so three cases enter
9
+ # the drain queue:
10
10
  # - never ratified (no marker)
11
- # - drift-reopened (marker says confirmed, but content changed → hash mismatch)
11
+ # - drift-reopened (marker says confirmed, but substance changed → mismatch)
12
12
  # - legacy confirmed (confirmed but no fingerprint yet — needs one re-ratify)
13
13
  #
14
- # With `--with-afk-accepted` (ADR-101) it ALSO lists, on stderr, stories whose
15
- # `confirmed` marker was machine-written by the AFK pure-decomposition carve-out
16
- # rather than by a human they are awaiting post-hoc human ratification. The
17
- # listing keys on the UNION of `afk-accept:` and `oversight-basis:`, because the
18
- # latter is excluded from the content hash and could otherwise be stripped to
19
- # hide a machine-accepted story from this drain; `afk-accept:` stays in the hash.
20
- # stdout is untouched by the flag, so the Step 2.4 drain consumer is unaffected.
14
+ # A STORY carries no marker at all (ADR-103). It is listed when any map in its
15
+ # `story-maps:` field is unratified, or when it names no map — and the remedy is
16
+ # always to ratify the map, never the story.
21
17
  #
22
- # Usage: detect-unratified-stories-maps.sh [--with-afk-accepted] [STORIES_DIR=docs/stories] [MAPS_DIR=docs/story-maps]
18
+ # Usage: detect-unratified-stories-maps.sh [STORIES_DIR=docs/stories] [MAPS_DIR=docs/story-maps]
23
19
  # Output: one unratified artefact path per line, sorted. Empty = all ratified.
24
20
  # Always exits 0 (detector, not a gate). Consumed by the work-problems Step 2.4
25
21
  # oversight-unconfirmed drain.
@@ -31,12 +27,6 @@ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
31
27
  # shellcheck source=/dev/null
32
28
  source "$LIB/story-oversight.sh"
33
29
 
34
- WITH_AFK=0
35
- if [ "${1:-}" = "--with-afk-accepted" ]; then
36
- WITH_AFK=1
37
- shift
38
- fi
39
-
40
30
  STORIES_DIR="${1:-docs/stories}"
41
31
  MAPS_DIR="${2:-docs/story-maps}"
42
32
 
@@ -46,7 +36,10 @@ shopt -s nullglob
46
36
  if [ -d "$STORIES_DIR" ]; then
47
37
  for f in "$STORIES_DIR"/*.md "$STORIES_DIR"/*/*.md; do
48
38
  [ "$(basename "$f")" = "README.md" ] && continue
49
- is_story_map_ratified "$f" || echo "$f"
39
+ # ADR-103: a story's approval may be its MAP's. Using the map predicate
40
+ # here would report every story as unratified — stories carry no marker at
41
+ # all — and send the drain to ratify a surface ADR-103 removed.
42
+ story_is_approved "$f" "$MAPS_DIR" || echo "$f"
50
43
  done
51
44
  fi
52
45
  if [ -d "$MAPS_DIR" ]; then
@@ -56,14 +49,4 @@ shopt -s nullglob
56
49
  fi
57
50
  } | sort
58
51
 
59
- # ADR-101 post-hoc drain — stderr only, so the stdout path contract is preserved.
60
- if [ "$WITH_AFK" = "1" ] && [ -d "$STORIES_DIR" ]; then
61
- for f in "$STORIES_DIR"/*.md "$STORIES_DIR"/*/*.md; do
62
- [ "$(basename "$f")" = "README.md" ] && continue
63
- if oversight_declares_pure_decomposition "$f" || oversight_is_pure_decomposition "$f"; then
64
- echo "AFK-ACCEPTED (awaiting post-hoc human ratification): $f" >&2
65
- fi
66
- done
67
- fi
68
-
69
52
  exit 0
@@ -8,19 +8,15 @@
8
8
  # stored fingerprint no longer matches → the artefact reads as drifted /
9
9
  # unratified until re-ratified (ADR-090 drift-invalidation).
10
10
  #
11
- # With `--pure-decomposition` (ADR-101) it additionally records
12
- # `oversight-basis: pure-decomposition` the marker was written by an AFK loop
13
- # under the carve-out, not by a human ratification event. That field is
14
- # markdown-story-only (a map is always human-ratified and never carries a
15
- # decomposition basis) and is EXCLUDED from the content hash, exactly like
16
- # `oversight-hash`; the authored `afk-accept:` declaration stays INSIDE the hash.
17
- # Without the flag the basis line is dropped, so re-ratifying by hand clears a
18
- # stale basis rather than leaving a human-ratified story labelled AFK-accepted.
11
+ # ADR-103 retired the ADR-101 `--pure-decomposition` flag and its
12
+ # `oversight-basis:` record. Nothing here strips a stale one: the only writer
13
+ # that did was the markdown leg, and markdown is a story, which is now refused
14
+ # outright. No artefact in the corpus carries the field.
19
15
  #
20
- # Usage: mark-story-oversight-confirmed.sh [--pure-decomposition] <story-or-map-file>
16
+ # Usage: mark-story-oversight-confirmed.sh <story-or-map-file>
21
17
  # Exit: 0 = ratified; 2 = usage / file error.
22
18
  #
23
- # Authority: ADR-090, ADR-101. Driver: P404 Phase 2, P456. Test: mark-story-oversight-confirmed.bats.
19
+ # Authority: ADR-090, ADR-103. Driver: P404 Phase 2, P456. Test: mark-story-oversight-confirmed.bats.
24
20
  set -euo pipefail
25
21
 
26
22
  # Adopter-safe: source the shared hash lib RELATIVE TO THIS SCRIPT (P317), never
@@ -30,25 +26,85 @@ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
30
26
  # shellcheck source=/dev/null
31
27
  source "$LIB/story-oversight.sh"
32
28
 
33
- basis=""
34
- if [ "${1:-}" = "--pure-decomposition" ]; then
35
- basis="pure-decomposition"
36
- shift
37
- fi
38
-
39
29
  f="${1:-}"
40
30
  if [ -z "$f" ]; then
41
- echo "mark-story-oversight-confirmed: usage: mark-story-oversight-confirmed.sh [--pure-decomposition] <file>" >&2
31
+ echo "mark-story-oversight-confirmed: usage: mark-story-oversight-confirmed.sh <file>" >&2
42
32
  exit 2
43
33
  fi
44
34
  [ -f "$f" ] || { echo "mark-story-oversight-confirmed: file not found: $f" >&2; exit 2; }
45
35
 
36
+ # ADR-103: only a story MAP is ratified. A story carries no oversight marker at
37
+ # all — its approval is its map's. Marking a story would re-create the second
38
+ # approval surface ADR-103 removed, so refuse rather than silently write one.
39
+ # What ADR-103 removes is the STORY-tier marker, and a story is markdown. So the
40
+ # refusal is keyed on the encoding, not on a `docs/stories/` path substring: a
41
+ # caller working outside that tree — a test fixture, an adopter with a different
42
+ # layout — must get the same answer.
43
+ #
44
+ # Deliberately NOT keyed on the ADR-102 data island, tempting as that is. A map
45
+ # predating ADR-102 has no island, and an island test would lock those out of
46
+ # ratification entirely. Marking some unrelated .html file is a user error; a
47
+ # story marker is a governance hole, and only the second one is worth refusing.
48
+ case "$f" in
49
+ *.md|*.markdown)
50
+ echo "mark-story-oversight-confirmed: refusing to mark $f (ADR-103). Only a story MAP is ratified. A story carries no oversight marker; its approval is derived from the maps named in its \`story-maps:\` field, so ratify the map instead." >&2
51
+ exit 2 ;;
52
+ esac
53
+
46
54
  h="$(oversight_content_hash "$f")"
47
55
  tmp="$(mktemp)"
48
56
 
49
57
  case "$f" in
50
58
  *.html)
51
- if grep -qi '<head' "$f"; then
59
+ if grep -qF '<script id="story-map-data"' "$f"; then
60
+ # ADR-102 map: the data island is the source of truth and every <meta> is
61
+ # GENERATED from it. Writing the marker to <meta> alone would be undone by
62
+ # the next render, silently destroying a human ratification — so the
63
+ # marker goes into the island and the <meta> follows on render.
64
+ #
65
+ # TWO passes, and the order matters. Normalising the island can move a
66
+ # trailing comma onto a line the marker filter does NOT exclude (removing
67
+ # a previously-appended key un-commas the key before it), which would
68
+ # change the very bytes being fingerprinted. So: normalise first, hash the
69
+ # normalised form, then write the hash — a marker-line-only edit, which
70
+ # the filter excludes, so the stored value stays valid.
71
+ _island_normalise() {
72
+ python3 - "$1" "$2" <<'PYMARK'
73
+ import json, pathlib, sys
74
+ path, digest = pathlib.Path(sys.argv[1]), sys.argv[2]
75
+ OPEN = '<script id="story-map-data" type="application/json">'
76
+ html = path.read_text()
77
+ start = html.index(OPEN) + len(OPEN)
78
+ end = html.index('</script>', start)
79
+ data = json.loads(html[start:end].replace('\\u003c', '<'))
80
+ # Marker keys FIRST, never appended: appending gives the previously-last key a
81
+ # trailing comma, a punctuation-only change on an unfiltered line.
82
+ MARKERS = ('humanOversight', 'oversightHash', 'oversightDate', 'oversightNote')
83
+ ordered = {'humanOversight': 'confirmed', 'oversightHash': digest}
84
+ for k in ('oversightDate', 'oversightNote'):
85
+ if k in data:
86
+ ordered[k] = data[k]
87
+ ordered.update({k: v for k, v in data.items() if k not in MARKERS})
88
+ island = json.dumps(ordered, indent=2, ensure_ascii=False).replace('<', '\\u003c')
89
+ path.write_text(html[:start] + "\n" + island + "\n " + html[end:])
90
+ PYMARK
91
+ }
92
+ _island_normalise "$f" "pending"
93
+ h="$(oversight_content_hash "$f")"
94
+ _island_normalise "$f" "$h"
95
+ # Re-render so the generated <meta> block matches the island. Prefer the
96
+ # sibling script over $PATH: this runs from the same package, so the
97
+ # renderer is always beside it even when the bin shim is not on PATH.
98
+ _renderer="$(cd "$(dirname "$0")" && pwd)/render-story-map.sh"
99
+ if [ -x "$_renderer" ]; then
100
+ "$_renderer" "$f" >/dev/null 2>&1 || true
101
+ elif command -v wr-itil-render-story-map >/dev/null 2>&1; then
102
+ wr-itil-render-story-map "$f" >/dev/null 2>&1 || true
103
+ fi
104
+ printf 'mark-story-oversight-confirmed: ratified %s (oversight-hash %s…)\n' "$f" "${h:0:12}" >&2
105
+ rm -f "$tmp"
106
+ exit 0
107
+ elif grep -qi '<head' "$f"; then
52
108
  # Insert the two metas after the <head> tag; drop any existing marker metas.
53
109
  awk -v H="$h" '
54
110
  /<meta[^>]*name="(human-oversight|oversight-hash)"/ { next }
@@ -67,19 +123,12 @@ case "$f" in
67
123
  fi
68
124
  ;;
69
125
  *)
70
- # Markdown: rewrite frontmatter drop existing markers, insert both before
71
- # the closing `---`.
72
- awk -v H="$h" -v B="$basis" '
73
- NR==1 && $0=="---" { infm=1; print; next }
74
- infm && /^(human-oversight|oversight-hash|oversight-basis):/ { next }
75
- infm && /^---[[:space:]]*$/ && !done {
76
- print "human-oversight: confirmed"
77
- print "oversight-hash: " H
78
- if (B != "") print "oversight-basis: " B
79
- print; done=1; infm=0; next
80
- }
81
- { print }
82
- ' "$f" > "$tmp"
126
+ # The markdown-frontmatter writer that used to live here was a working
127
+ # writer for the story-level marker ADR-103 removed. It is deleted rather
128
+ # than left unreachable behind the guard above, so that a later relaxation
129
+ # of that guard fails loudly instead of silently restoring story markers.
130
+ echo "mark-story-oversight-confirmed: refusing to mark $f (ADR-103) no recognised story-map encoding (expected a <script id=\"story-map-data\"> island, or a legacy HTML map)." >&2
131
+ rm -f "$tmp"; exit 2
83
132
  ;;
84
133
  esac
85
134
 
@@ -40,7 +40,7 @@
40
40
  # Usage: migrate-story-status-mirror.sh [<stories-dir>...] (default: docs/stories)
41
41
  # Exit: 0 = completed (including "nothing to do"); 2 = usage / missing lib.
42
42
  #
43
- # Authority: ADR-090 (oversight fingerprint), ADR-101 (oversight-basis).
43
+ # Authority: ADR-090 (oversight fingerprint), ADR-103 (stories carry no marker).
44
44
  # Driver: P474. Test: migrate-story-status-mirror.bats.
45
45
 
46
46
  set -euo pipefail
@@ -105,8 +105,12 @@ for d in "${dirs[@]}"; do
105
105
 
106
106
  # Was this artefact ratified-and-valid BEFORE we touch it? Decide now: once
107
107
  # the line is gone the pre-migration hash is unreproducible.
108
+ #
109
+ # ADR-103: a story carries no fingerprint of its own, so this is always 0 on
110
+ # the story tier and the re-point branch below never runs there. The branch
111
+ # is retained for any artefact that does carry one.
108
112
  was_valid=0
109
- if is_story_map_ratified "$f"; then was_valid=1; fi
113
+ if [ -n "$(oversight_stored_hash "$f")" ] && is_story_map_ratified "$f"; then was_valid=1; fi
110
114
 
111
115
  # Drop the FIRST matching mirror line and nothing else — surrounding blank
112
116
  # lines are left exactly as they are. The template puts the mirror in a run
@@ -132,7 +136,7 @@ for d in "${dirs[@]}"; do
132
136
  refingerprinted=$((refingerprinted + 1))
133
137
  printf 'MIGRATE %s — mirror removed, fingerprint re-pointed (ratification preserved)\n' "$f"
134
138
  else
135
- printf 'MIGRATE %s — mirror removed (was not ratified-and-valid; left unratified)\n' "$f"
139
+ printf 'MIGRATE %s — mirror removed (no fingerprint of its own to re-point)\n' "$f"
136
140
  fi
137
141
  # STORY-*.md only. `README.md` documents the template and legitimately
138
142
  # contains a `**Status**: <status>` example; scanning it would report a
@@ -7,13 +7,14 @@
7
7
  # Story Rankings + Done tables, and reports each disagreement.
8
8
  #
9
9
  # Usage:
10
- # reconcile-stories.sh [<stories-dir> [<problems-dir> [<rfcs-dir> [<jtbd-dir>]]]]
10
+ # reconcile-stories.sh [<stories-dir> [<problems-dir> [<rfcs-dir> [<jtbd-dir> [<story-maps-dir>]]]]]
11
11
  #
12
12
  # Defaults:
13
13
  # <stories-dir> = ./docs/stories
14
14
  # <problems-dir> = ./docs/problems (when supplied + on disk; reverse trace)
15
15
  # <rfcs-dir> = ./docs/rfcs (when supplied + on disk; reverse trace)
16
16
  # <jtbd-dir> = ./docs/jtbd (when supplied + on disk; reverse trace)
17
+ # <story-maps-dir> = ./docs/story-maps (row-backed RFC resolution)
17
18
  #
18
19
  # Exit codes:
19
20
  # 0 = clean (README matches filesystem)
@@ -71,10 +72,12 @@
71
72
 
72
73
  set -uo pipefail
73
74
 
75
+ HERE="$(cd "$(dirname "$0")" && pwd)"
74
76
  STORIES_DIR="${1:-docs/stories}"
75
77
  PROBLEMS_DIR="${2:-$(dirname "$STORIES_DIR")/problems}"
76
78
  RFCS_DIR="${3:-$(dirname "$STORIES_DIR")/rfcs}"
77
79
  JTBD_DIR="${4:-$(dirname "$STORIES_DIR")/jtbd}"
80
+ MAPS_DIR="${5:-$(dirname "$STORIES_DIR")/story-maps}"
78
81
  README="${STORIES_DIR}/README.md"
79
82
 
80
83
  # ── Pre-checks ──────────────────────────────────────────────────────────────
@@ -183,6 +186,8 @@ done
183
186
 
184
187
  reverse_trace_pass() {
185
188
  local parent_dir="$1" parent_kind="$2" parent_id_pattern="$3"
189
+ local -a matches=()
190
+ local pfile row_json
186
191
  [ ! -d "$parent_dir" ] && return 0
187
192
 
188
193
  shopt -s nullglob globstar
@@ -198,19 +203,35 @@ reverse_trace_pass() {
198
203
  parent_claims=$(awk -v k="^${parent_kind}:" '$0 ~ k {gsub(/[][]/,""); gsub(/,/," "); for(i=2;i<=NF;i++)print $i; exit}' "$sf")
199
204
  for pid in $parent_claims; do
200
205
  # Resolve parent file under parent_dir
206
+ matches=()
201
207
  case "$parent_kind" in
202
208
  problems)
203
209
  pnum="${pid#P}"
204
- pfile=$(ls "$parent_dir"/${pnum}-*.md "$parent_dir"/*/${pnum}-*.md 2>/dev/null | head -1)
210
+ matches=("$parent_dir"/${pnum}-*.md "$parent_dir"/*/${pnum}-*.md)
205
211
  ;;
206
212
  rfcs)
207
- pfile=$(ls "$parent_dir"/${pid}-*.md 2>/dev/null | head -1)
213
+ matches=("$parent_dir"/${pid}-*.md)
208
214
  ;;
209
215
  jtbd)
210
- pfile=$(ls "$parent_dir"/*/${pid}-*.md 2>/dev/null | head -1)
216
+ matches=("$parent_dir"/*/${pid}-*.md)
211
217
  ;;
212
- *) pfile="" ;;
218
+ *) matches=() ;;
213
219
  esac
220
+ pfile="${matches[0]:-}"
221
+
222
+ # ADR-103: a release row may be the RFC, so no markdown parent exists.
223
+ # In that case the map row itself is the reverse trace and must contain
224
+ # this story. Query the canonical map island rather than scraping HTML.
225
+ if [ "$parent_kind" = rfcs ] && [ -z "$pfile" ]; then
226
+ row_json=$("$HERE/story-map-query.sh" find-rfc "$pid" --maps-dir "$MAPS_DIR" 2>/dev/null || true)
227
+ if [ -z "$row_json" ] || [ "$row_json" = "[]" ]; then
228
+ DRIFT_LINES+=("UNRESOLVED_RFC_TRACE ${sid} claims=${pid}")
229
+ elif ! grep -qF "\"${sid}\"" <<< "$row_json"; then
230
+ DRIFT_LINES+=("MISSING_REVERSE_TRACE ${sid} in ${pid} release row")
231
+ fi
232
+ continue
233
+ fi
234
+
214
235
  [ -z "$pfile" ] && continue
215
236
 
216
237
  # Check parent's ## Stories section contains this story's ID