@windyroad/itil 2.4.2-preview.1239 → 2.4.2-preview.1241
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/lib/story-oversight.sh
CHANGED
|
@@ -204,39 +204,61 @@ is_story_map_ratified() {
|
|
|
204
204
|
#
|
|
205
205
|
# A story naming no map is NOT approved — otherwise dropping the `story-maps:`
|
|
206
206
|
# field would be a way to self-approve.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
# The map ids a story DECLARES, one per line, empty output when it declares
|
|
208
|
+
# none. Both frontmatter forms — inline `story-maps: [A, B]` and a block list of
|
|
209
|
+
# `- A` continuation lines — are read here and nowhere else. A consumer that
|
|
210
|
+
# wants to tell "declares no map" apart from "declares a map that is not
|
|
211
|
+
# ratified" needs the ids, not just the verdict; parsing them again at the call
|
|
212
|
+
# site would start the second enumeration this lib's header warns about, and a
|
|
213
|
+
# copy covering only the inline form would silently file every block-form story
|
|
214
|
+
# under "no map".
|
|
215
|
+
story_declared_maps() {
|
|
216
|
+
local story="$1" line ids
|
|
211
217
|
line="$(awk '/^story-maps:/{print; exit}' "$story")"
|
|
212
218
|
ids="$(printf '%s' "$line" | grep -oE 'STORY-MAP-[0-9]+' || true)"
|
|
213
219
|
[ -n "$ids" ] || ids="$(awk '/^story-maps:/{g=1;next} g&&/^[[:space:]]*-/{print} g&&/^[^[:space:]-]/{exit}' "$story" \
|
|
214
220
|
| grep -oE 'STORY-MAP-[0-9]+' || true)"
|
|
221
|
+
[ -n "$ids" ] && printf '%s\n' $ids
|
|
222
|
+
return 0
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Echo the ONE map file an id resolves to; non-zero when it resolves to zero
|
|
226
|
+
# files or to more than one.
|
|
227
|
+
#
|
|
228
|
+
# Exactly one, never first-match. This USED to be `ls ... | head -1`, which
|
|
229
|
+
# arbitrary-picks when an id resolves in more than one lifecycle directory —
|
|
230
|
+
# silently, and in the permissive direction: the one branch in the approval
|
|
231
|
+
# predicate that could grant an approval nobody gave. Which copy wins is just
|
|
232
|
+
# glob sort order, so whether it lands on the live map or a stale one is luck.
|
|
233
|
+
# An ambiguous id is a corpus defect; deny and let it surface.
|
|
234
|
+
#
|
|
235
|
+
# Known gap, pre-existing and unreachable today (no map sits at the top level of
|
|
236
|
+
# docs/story-maps/): the detector walks BOTH "$MAPS_DIR"/*.html and
|
|
237
|
+
# "$MAPS_DIR"/*/*.html, so a top-level map is visible to it but invisible here,
|
|
238
|
+
# and a story naming one would read unapproved forever.
|
|
239
|
+
#
|
|
240
|
+
# No `shopt nullglob` here on purpose. This function is sourced, so toggling it
|
|
241
|
+
# would mutate the CALLER's shell — and one caller
|
|
242
|
+
# (detect-unratified-stories-maps.sh) sets nullglob once at the top and relies on
|
|
243
|
+
# it for a later glob. The `-e` test does the same job locally: an unmatched glob
|
|
244
|
+
# stays literal, and a literal path does not exist.
|
|
245
|
+
story_map_file() {
|
|
246
|
+
local id="$1" maps_root="${2:-docs/story-maps}" cand n=0 m=""
|
|
247
|
+
for cand in "$maps_root"/*/"${id}"-*.html; do
|
|
248
|
+
[ -e "$cand" ] || continue
|
|
249
|
+
n=$((n + 1)); m="$cand"
|
|
250
|
+
done
|
|
251
|
+
[ "$n" -eq 1 ] || return 1
|
|
252
|
+
printf '%s' "$m"
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
story_is_approved() {
|
|
256
|
+
local story="$1" maps_root="${2:-docs/story-maps}" ids id m found=0
|
|
257
|
+
|
|
258
|
+
ids="$(story_declared_maps "$story")"
|
|
215
259
|
|
|
216
260
|
for id in $ids; do
|
|
217
|
-
|
|
218
|
-
# `ls ... | head -1`, which arbitrary-picks when an id resolves in more than
|
|
219
|
-
# one of them — silently, and in the permissive direction: the one branch in
|
|
220
|
-
# this predicate that could grant an approval nobody gave. Which copy wins is
|
|
221
|
-
# just glob sort order, so whether it lands on the live map or a stale one is
|
|
222
|
-
# luck. An ambiguous id is a corpus defect; deny and let it surface.
|
|
223
|
-
#
|
|
224
|
-
# Known gap, pre-existing and unreachable today (no map sits at the top
|
|
225
|
-
# level of docs/story-maps/): the detector walks BOTH "$MAPS_DIR"/*.html and
|
|
226
|
-
# "$MAPS_DIR"/*/*.html, so a top-level map is visible to it but invisible
|
|
227
|
-
# here, and a story naming one would read unapproved forever.
|
|
228
|
-
#
|
|
229
|
-
# No `shopt nullglob` here on purpose. This function is sourced, so toggling
|
|
230
|
-
# it would mutate the CALLER's shell — and one caller
|
|
231
|
-
# (detect-unratified-stories-maps.sh) sets nullglob once at the top and
|
|
232
|
-
# relies on it for a later glob. The `-e` test does the same job locally: an
|
|
233
|
-
# unmatched glob stays literal, and a literal path does not exist.
|
|
234
|
-
local cand n=0 m=""
|
|
235
|
-
for cand in "$maps_root"/*/"${id}"-*.html; do
|
|
236
|
-
[ -e "$cand" ] || continue
|
|
237
|
-
n=$((n + 1)); m="$cand"
|
|
238
|
-
done
|
|
239
|
-
[ "$n" -eq 1 ] || return 1
|
|
261
|
+
m="$(story_map_file "$id" "$maps_root")" || return 1
|
|
240
262
|
is_story_map_ratified "$m" || return 1
|
|
241
263
|
found=1
|
|
242
264
|
done
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windyroad/itil",
|
|
3
|
-
"version": "2.4.2-preview.
|
|
3
|
+
"version": "2.4.2-preview.1241",
|
|
4
4
|
"description": "ITIL-aligned IT service management for Claude Code and Codex",
|
|
5
5
|
"bin": {
|
|
6
6
|
"windyroad-itil": "./bin/install.mjs"
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"!hooks/test/",
|
|
44
44
|
"!lib/test/",
|
|
45
45
|
"!scripts/test/",
|
|
46
|
-
"!scripts/check-published-internal-ids.sh",
|
|
47
46
|
"!scripts/codex-work-problems.md",
|
|
48
47
|
"!scripts/sync-codex-skills.mjs",
|
|
49
48
|
"!skills/*/test/"
|
|
@@ -37,12 +37,33 @@
|
|
|
37
37
|
# Story's frontmatter claims <PARENT-ID> but parent's ## Stories
|
|
38
38
|
# table does not list STORY-<NNN>. Skill-side refresh contract
|
|
39
39
|
# was missed.
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
40
|
+
#
|
|
41
|
+
# WHICH POPULATION THE RFC LEG COVERS (reconcile-stories-reports-permanent-false-missing-reverse-trace-drift-against-s-ratified-stories-only-rule-problem / a-clean-reconciler-result-means-the-tier-really-is-clean-delivery-story). On the problems
|
|
42
|
+
# and jtbd legs every story that names the parent is checked. On the rfcs leg
|
|
43
|
+
# only APPROVED stories are, because story-maps-and-stories-carry-a-drift-invalidated-human-oversight-marker-architecture-rule as amended by a-release-row-is-the-rfc-and-the-map-is-the-approval-surface-architecture-rule forbids an
|
|
44
|
+
# RFC from referencing a story whose story map is not ratified: for such a
|
|
45
|
+
# story the `## Stories` row is CORRECTLY absent, and demanding it reported
|
|
46
|
+
# correct work as drift — a finding no compliant action could ever clear,
|
|
47
|
+
# since satisfying it meant breaking the rule it existed to protect.
|
|
48
|
+
#
|
|
49
|
+
# Approval reaches a story through its MAP (a-release-row-is-the-rfc-and-the-map-is-the-approval-surface-architecture-rule), so `story_is_approved`
|
|
50
|
+
# is the predicate. A `human-oversight:` field left on a story file is legacy
|
|
51
|
+
# and is deliberately ignored; reading one would revive the second approval
|
|
52
|
+
# surface a-release-row-is-the-rfc-and-the-map-is-the-approval-surface-architecture-rule deleted.
|
|
53
|
+
#
|
|
54
|
+
# The release-row leg below is NOT gated. story-map-membership-and-story-content-completeness-are-enforced-at-capture-architecture-rule compels a story card onto
|
|
55
|
+
# the map at capture and a-release-row-is-the-rfc-and-the-map-is-the-approval-surface-architecture-rule took cards out of the fingerprint basis, so
|
|
56
|
+
# a card's absence from its row is never correct.
|
|
57
|
+
#
|
|
58
|
+
# Every run of the rfcs leg says on STDERR how many pairs it checked and how
|
|
59
|
+
# many it skipped, split by reason, so a reader of an exit-0 clean result can
|
|
60
|
+
# tell "checked and clean" from "skipped because out of scope". Two of the
|
|
61
|
+
# three skip reasons are corpus defects, not correct absences, and are
|
|
62
|
+
# counted apart: a story that names NO map (story-map-membership-and-story-content-completeness-are-enforced-at-capture-architecture-rule), and a story naming a
|
|
63
|
+
# map id that resolves to zero or to several files. Folding either into the
|
|
64
|
+
# correct-absence bucket would be this ticket's own failure in the other
|
|
65
|
+
# direction. STDOUT stays drift-lines-only — /wr-itil:reconcile-stories
|
|
66
|
+
# redirects it to a file and parses it line by line.
|
|
46
67
|
#
|
|
47
68
|
# Read-only — does NOT mutate the README. The /wr-itil:manage-story skill
|
|
48
69
|
# (problem-tickets-strain-as-fixes-decompose-into-multiple-coordinated-changes-need-an-rfc-framework-that-ties-all-changes-back-to-problems-and-unifies-technical-with-user-business-problems-problem Phase 2 Slice 8) applies edits with narrative-aware preservation;
|
|
@@ -73,6 +94,16 @@
|
|
|
73
94
|
set -uo pipefail
|
|
74
95
|
|
|
75
96
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
97
|
+
|
|
98
|
+
# Adopter-safe: source the shared lazy-fingerprint lib RELATIVE TO THIS SCRIPT
|
|
99
|
+
# (capture-problem-capture-rfc-manage-problem-step-2-create-gate-marker-step-sources-repo-relative-packages-itil-hooks-lib-sh-fails-in-adopter-installs-recurring-published-path-class-problem), matching check-rfc-stories-ratified.sh. `story_is_approved` and its
|
|
100
|
+
# two accessors are the ONE definition of story ratification; this script adds
|
|
101
|
+
# no second one.
|
|
102
|
+
LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
|
|
103
|
+
echo "reconcile-stories: cannot locate lib dir" >&2; exit 2; }
|
|
104
|
+
# shellcheck source=../lib/story-oversight.sh
|
|
105
|
+
source "$LIB/story-oversight.sh"
|
|
106
|
+
|
|
76
107
|
STORIES_DIR="${1:-docs/stories}"
|
|
77
108
|
PROBLEMS_DIR="${2:-$(dirname "$STORIES_DIR")/problems}"
|
|
78
109
|
RFCS_DIR="${3:-$(dirname "$STORIES_DIR")/rfcs}"
|
|
@@ -184,6 +215,42 @@ done
|
|
|
184
215
|
|
|
185
216
|
# ── Reverse-trace pass — story frontmatter ↔ parent ## Stories section ─────
|
|
186
217
|
|
|
218
|
+
# RFC-leg population accounting. Counted per (story, markdown-RFC) pair.
|
|
219
|
+
RFC_MD_CHECKED=0
|
|
220
|
+
RFC_MD_SKIP_UNRATIFIED=0
|
|
221
|
+
RFC_MD_SKIP_NO_MAP=0
|
|
222
|
+
RFC_MD_SKIP_UNRESOLVED_MAP=0
|
|
223
|
+
declare -A MAP_RATIFIED_MEMO
|
|
224
|
+
|
|
225
|
+
# Classify a story for the story-maps-and-stories-carry-a-drift-invalidated-human-oversight-marker-architecture-rule RFC-markdown rule. Sets RFC_MD_BUCKET to one
|
|
226
|
+
# of: approved | unratified | no-map | unresolved-map.
|
|
227
|
+
#
|
|
228
|
+
# Sets a global rather than echoing so the memo survives: a $(...) call runs in a
|
|
229
|
+
# subshell and every MAP_RATIFIED_MEMO write inside one is discarded, which would
|
|
230
|
+
# re-hash the same handful of maps once per claim.
|
|
231
|
+
#
|
|
232
|
+
# `story_is_approved` is false for three different reasons and only ONE of them
|
|
233
|
+
# is a correct absence, so this asks the lib's accessors directly instead of
|
|
234
|
+
# taking the verdict alone.
|
|
235
|
+
_rfc_md_bucket() {
|
|
236
|
+
local sf="$1" ids id m verdict
|
|
237
|
+
RFC_MD_BUCKET=approved
|
|
238
|
+
ids="$(story_declared_maps "$sf")"
|
|
239
|
+
if [ -z "$ids" ]; then RFC_MD_BUCKET=no-map; return 0; fi
|
|
240
|
+
for id in $ids; do
|
|
241
|
+
if ! m="$(story_map_file "$id" "$MAPS_DIR")"; then
|
|
242
|
+
RFC_MD_BUCKET=unresolved-map; return 0
|
|
243
|
+
fi
|
|
244
|
+
verdict="${MAP_RATIFIED_MEMO[$m]:-}"
|
|
245
|
+
if [ -z "$verdict" ]; then
|
|
246
|
+
if is_story_map_ratified "$m"; then verdict=yes; else verdict=no; fi
|
|
247
|
+
MAP_RATIFIED_MEMO["$m"]="$verdict"
|
|
248
|
+
fi
|
|
249
|
+
if [ "$verdict" != yes ]; then RFC_MD_BUCKET=unratified; return 0; fi
|
|
250
|
+
done
|
|
251
|
+
return 0
|
|
252
|
+
}
|
|
253
|
+
|
|
187
254
|
reverse_trace_pass() {
|
|
188
255
|
local parent_dir="$1" parent_kind="$2" parent_id_pattern="$3"
|
|
189
256
|
local -a matches=()
|
|
@@ -234,6 +301,20 @@ reverse_trace_pass() {
|
|
|
234
301
|
|
|
235
302
|
[ -z "$pfile" ] && continue
|
|
236
303
|
|
|
304
|
+
# story-maps-and-stories-carry-a-drift-invalidated-human-oversight-marker-architecture-rule / a-release-row-is-the-rfc-and-the-map-is-the-approval-surface-architecture-rule: an RFC references only approved stories, so an
|
|
305
|
+
# unapproved story's absence from this section is correct, not drift.
|
|
306
|
+
# Only the markdown-RFC leg is narrowed — the release-row branch above
|
|
307
|
+
# has already returned.
|
|
308
|
+
if [ "$parent_kind" = rfcs ]; then
|
|
309
|
+
_rfc_md_bucket "$sf"
|
|
310
|
+
case "$RFC_MD_BUCKET" in
|
|
311
|
+
approved) RFC_MD_CHECKED=$((RFC_MD_CHECKED + 1)) ;;
|
|
312
|
+
unratified) RFC_MD_SKIP_UNRATIFIED=$((RFC_MD_SKIP_UNRATIFIED + 1)); continue ;;
|
|
313
|
+
no-map) RFC_MD_SKIP_NO_MAP=$((RFC_MD_SKIP_NO_MAP + 1)); continue ;;
|
|
314
|
+
unresolved-map) RFC_MD_SKIP_UNRESOLVED_MAP=$((RFC_MD_SKIP_UNRESOLVED_MAP + 1)); continue ;;
|
|
315
|
+
esac
|
|
316
|
+
fi
|
|
317
|
+
|
|
237
318
|
# Check parent's ## Stories section contains this story's ID
|
|
238
319
|
if ! awk '/^## Stories/{flag=1; next} /^## /{flag=0} flag{print}' "$pfile" | grep -qF "$sid"; then
|
|
239
320
|
DRIFT_LINES+=("MISSING_REVERSE_TRACE ${sid} in ${pid} ## Stories")
|
|
@@ -247,6 +328,24 @@ reverse_trace_pass "$PROBLEMS_DIR" "problems" "P[0-9]{3}"
|
|
|
247
328
|
reverse_trace_pass "$RFCS_DIR" "rfcs" "RFC-[0-9]{3}"
|
|
248
329
|
reverse_trace_pass "$JTBD_DIR" "jtbd" "JTBD-[0-9]{3}"
|
|
249
330
|
|
|
331
|
+
# ── Say which population the RFC leg checked ────────────────────────────────
|
|
332
|
+
#
|
|
333
|
+
# On stderr, so stdout stays drift-lines-only for the skill's line-by-line
|
|
334
|
+
# parse. Printed whether or not there is drift: a clean result that never says
|
|
335
|
+
# what it covered is the same defect this leg was narrowed to fix. Skip lines
|
|
336
|
+
# are suppressed at zero — the checked-of-total line already carries that.
|
|
337
|
+
|
|
338
|
+
if [ -d "$RFCS_DIR" ]; then
|
|
339
|
+
rfc_md_total=$(( RFC_MD_CHECKED + RFC_MD_SKIP_UNRATIFIED + RFC_MD_SKIP_NO_MAP + RFC_MD_SKIP_UNRESOLVED_MAP ))
|
|
340
|
+
echo "reconcile-stories: RFC ## Stories reverse trace checked ${RFC_MD_CHECKED} of ${rfc_md_total} story/RFC pairs." >&2
|
|
341
|
+
[ "$RFC_MD_SKIP_UNRATIFIED" -gt 0 ] && \
|
|
342
|
+
echo "reconcile-stories: ${RFC_MD_SKIP_UNRATIFIED} skipped because the story's map is not ratified, so the row is correctly absent." >&2
|
|
343
|
+
[ "$RFC_MD_SKIP_NO_MAP" -gt 0 ] && \
|
|
344
|
+
echo "reconcile-stories: ${RFC_MD_SKIP_NO_MAP} skipped because the story names no story map at all; that is a corpus defect, not a correct absence." >&2
|
|
345
|
+
[ "$RFC_MD_SKIP_UNRESOLVED_MAP" -gt 0 ] && \
|
|
346
|
+
echo "reconcile-stories: ${RFC_MD_SKIP_UNRESOLVED_MAP} skipped because the story names a map id that does not resolve to exactly one file; also a corpus defect." >&2
|
|
347
|
+
fi
|
|
348
|
+
|
|
250
349
|
# ── Emit ─────────────────────────────────────────────────────────────────────
|
|
251
350
|
|
|
252
351
|
if [ ${#DRIFT_LINES[@]} -eq 0 ]; then
|
|
@@ -5,12 +5,35 @@
|
|
|
5
5
|
# update-problem-references-section.sh with the lookup table tuned for
|
|
6
6
|
# RFC-on-RFC reverse traces:
|
|
7
7
|
# - ## Story Maps : sources docs/story-maps/*/*.html via data attributes
|
|
8
|
-
# - ## Stories :
|
|
8
|
+
# - ## Stories : sources docs/stories/*/STORY-*.md by each story's `rfcs:`
|
|
9
9
|
#
|
|
10
10
|
# Per problem-rfc-story-framework-with-mandatory-problem-trace-and-unified-problem-ontology-architecture-rule § Phase 2 encoding amendment 2026-05-12 architect finding 4:
|
|
11
|
-
# no per-section-name branching in body; lookup-table-driven dispatch.
|
|
11
|
+
# no per-section-name branching in body; lookup-table-driven dispatch. The
|
|
12
|
+
# approval gate below rides that table rather than a branch, for the same
|
|
13
|
+
# reason.
|
|
12
14
|
#
|
|
13
|
-
#
|
|
15
|
+
# APPROVAL GATE ON `## Stories` (reconcile-stories-reports-permanent-false-missing-reverse-trace-drift-against-s-ratified-stories-only-rule-problem / a-clean-reconciler-result-means-the-tier-really-is-clean-delivery-story). story-maps-and-stories-carry-a-drift-invalidated-human-oversight-marker-architecture-rule as amended by
|
|
16
|
+
# a-release-row-is-the-rfc-and-the-map-is-the-approval-surface-architecture-rule forbids an RFC from referencing a story whose story map is not
|
|
17
|
+
# ratified. This helper regenerates the WHOLE section from a reverse index over
|
|
18
|
+
# every story claiming the RFC, and it is the prescribed repair for a
|
|
19
|
+
# MISSING_REVERSE_TRACE finding — so ungated, repairing one legitimate finding
|
|
20
|
+
# swept every unapproved sibling in with it, writing exactly the reference the
|
|
21
|
+
# rule forbids. The narrowed detector in reconcile-stories.sh would then report
|
|
22
|
+
# clean over it: a loud false positive traded for a silent true negative, which
|
|
23
|
+
# is strictly worse. A withheld story is named on stderr, because a section
|
|
24
|
+
# that silently shrinks is its own kind of unreadable.
|
|
25
|
+
#
|
|
26
|
+
# KNOWN DIVERGENCE, recorded on reconcile-stories-reports-permanent-false-missing-reverse-trace-drift-against-s-ratified-stories-only-rule-problem and deliberately not fixed here. problem-rfc-story-framework-with-mandatory-problem-trace-and-unified-problem-ontology-architecture-rule
|
|
27
|
+
# line 288 and this header used to say `## Stories` is a FORWARD trace
|
|
28
|
+
# projecting the RFC's own `stories:` array. The implementation has always
|
|
29
|
+
# reverse-indexed story frontmatter instead. Projecting `stories:` was
|
|
30
|
+
# considered: a corpus audit found 8 of 23 approved (story → markdown-RFC)
|
|
31
|
+
# claims present in the RFC's `## Stories` but absent from its `stories:`, so
|
|
32
|
+
# the projection would drop those rows and the reconciler would report
|
|
33
|
+
# MISSING_REVERSE_TRACE on them with no mechanical repair — reconcile-stories-reports-permanent-false-missing-reverse-trace-drift-against-s-ratified-stories-only-rule-problem re-created in
|
|
34
|
+
# the other direction. The header now states what the code does.
|
|
35
|
+
#
|
|
36
|
+
# Usage: update-rfc-references-section.sh <rfc-file> <section-name> [<story-maps-dir>]
|
|
14
37
|
#
|
|
15
38
|
# @adr problem-rfc-story-framework-with-mandatory-problem-trace-and-unified-problem-ontology-architecture-rule (Phase 2 encoding amendment 2026-05-12)
|
|
16
39
|
# @problem problem-tickets-strain-as-fixes-decompose-into-multiple-coordinated-changes-need-an-rfc-framework-that-ties-all-changes-back-to-problems-and-unifies-technical-with-user-business-problems-problem (Phase 2 Slice 2b)
|
|
@@ -19,6 +42,14 @@ set -uo pipefail
|
|
|
19
42
|
|
|
20
43
|
RFC_FILE="${1:-}"
|
|
21
44
|
SECTION_NAME="${2:-}"
|
|
45
|
+
MAPS_DIR="${3:-docs/story-maps}"
|
|
46
|
+
|
|
47
|
+
# Adopter-safe: source the shared ratification lib RELATIVE TO THIS SCRIPT
|
|
48
|
+
# (capture-problem-capture-rfc-manage-problem-step-2-create-gate-marker-step-sources-repo-relative-packages-itil-hooks-lib-sh-fails-in-adopter-installs-recurring-published-path-class-problem), matching check-rfc-stories-ratified.sh.
|
|
49
|
+
LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
|
|
50
|
+
echo "ERROR: cannot locate lib dir" >&2; exit 1; }
|
|
51
|
+
# shellcheck source=../lib/story-oversight.sh
|
|
52
|
+
source "$LIB/story-oversight.sh"
|
|
22
53
|
|
|
23
54
|
if [ -z "$RFC_FILE" ]; then
|
|
24
55
|
echo "ERROR: missing rfc-file argument" >&2
|
|
@@ -33,19 +64,26 @@ if [ ! -f "$RFC_FILE" ]; then
|
|
|
33
64
|
exit 1
|
|
34
65
|
fi
|
|
35
66
|
|
|
36
|
-
declare -A SECTION_GLOB SECTION_MODE SECTION_ID_PATTERN
|
|
67
|
+
declare -A SECTION_GLOB SECTION_MODE SECTION_ID_PATTERN SECTION_ADMIT
|
|
68
|
+
|
|
69
|
+
# A story is APPROVED when every story map it names is ratified (a-release-row-is-the-rfc-and-the-map-is-the-approval-surface-architecture-rule). Any
|
|
70
|
+
# `human-oversight:` field left on the story file is legacy and is ignored.
|
|
71
|
+
admit_if_story_approved() { story_is_approved "$1" "$MAPS_DIR"; }
|
|
37
72
|
|
|
38
73
|
SECTION_GLOB["Story Maps"]="docs/story-maps/*/STORY-MAP-*.html"
|
|
39
74
|
SECTION_MODE["Story Maps"]="html-data-attribute-rfc"
|
|
40
75
|
SECTION_ID_PATTERN["Story Maps"]="STORY-MAP-[0-9]+"
|
|
76
|
+
SECTION_ADMIT["Story Maps"]=""
|
|
41
77
|
|
|
42
78
|
SECTION_GLOB["Stories"]="docs/stories/*/STORY-*.md"
|
|
43
79
|
SECTION_MODE["Stories"]="markdown-frontmatter-rfc"
|
|
44
80
|
SECTION_ID_PATTERN["Stories"]="STORY-[0-9]+"
|
|
81
|
+
SECTION_ADMIT["Stories"]="admit_if_story_approved"
|
|
45
82
|
|
|
46
83
|
glob_pattern="${SECTION_GLOB[$SECTION_NAME]:-}"
|
|
47
84
|
extraction_mode="${SECTION_MODE[$SECTION_NAME]:-}"
|
|
48
85
|
id_pattern="${SECTION_ID_PATTERN[$SECTION_NAME]:-}"
|
|
86
|
+
admit_filter="${SECTION_ADMIT[$SECTION_NAME]:-}"
|
|
49
87
|
|
|
50
88
|
if [ -z "$glob_pattern" ]; then
|
|
51
89
|
echo "ERROR: unknown section-name '$SECTION_NAME'. Supported: Story Maps, Stories" >&2
|
|
@@ -60,7 +98,7 @@ if [ -z "$rfc_id" ]; then
|
|
|
60
98
|
exit 1
|
|
61
99
|
fi
|
|
62
100
|
|
|
63
|
-
declare -a matched_ids=() matched_titles=() matched_statuses=()
|
|
101
|
+
declare -a matched_ids=() matched_titles=() matched_statuses=() withheld_ids=()
|
|
64
102
|
|
|
65
103
|
extract_from_html_rfcs_meta() {
|
|
66
104
|
local file="$1"
|
|
@@ -107,6 +145,10 @@ for artefact in $glob_pattern; do
|
|
|
107
145
|
if "$extract_match" "$artefact"; then
|
|
108
146
|
aid=$(extract_id_from_filename "$artefact")
|
|
109
147
|
[ -n "$aid" ] || continue
|
|
148
|
+
if [ -n "$admit_filter" ] && ! "$admit_filter" "$artefact"; then
|
|
149
|
+
withheld_ids+=("$aid")
|
|
150
|
+
continue
|
|
151
|
+
fi
|
|
110
152
|
matched_ids+=("$aid")
|
|
111
153
|
matched_titles+=("$("$extract_title" "$artefact" 2>/dev/null || echo "")")
|
|
112
154
|
matched_statuses+=("$("$extract_status" "$artefact" 2>/dev/null || echo "unknown")")
|
|
@@ -114,6 +156,10 @@ for artefact in $glob_pattern; do
|
|
|
114
156
|
done
|
|
115
157
|
shopt -u nullglob
|
|
116
158
|
|
|
159
|
+
if [ ${#withheld_ids[@]} -gt 0 ]; then
|
|
160
|
+
echo "update-rfc-references-section: withheld from ${rfc_id} ## ${SECTION_NAME}: ${withheld_ids[*]} — each names a story map that is not ratified, and an RFC references only approved stories. To list one, ratify its map; never hand-add the row." >&2
|
|
161
|
+
fi
|
|
162
|
+
|
|
117
163
|
new_section=""
|
|
118
164
|
if [ ${#matched_ids[@]} -gt 0 ]; then
|
|
119
165
|
new_section="## ${SECTION_NAME}"$'\n\n| ID | Title | Status |\n|----|-------|--------|\n'
|
|
@@ -12,11 +12,21 @@ Sibling to `/wr-itil:reconcile-readme` (the "`docs/problems/README.md` drifts fr
|
|
|
12
12
|
|
|
13
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 the "A release row is the RFC, and the map is the approval surface" architecture rule 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
|
-
- `STALE_REVERSE_TRACE STORY-NNN in <PARENT-ID> ## Stories` — parent lists the story but story no longer claims the parent
|
|
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
15
|
- `UNRESOLVED_RFC_TRACE STORY-NNN claims=RFC-NNN` — neither a legacy RFC file nor a story-map release row exists
|
|
18
16
|
- `MISSING_REVERSE_TRACE STORY-NNN in RFC-NNN release row` — the release row exists but does not contain the story card
|
|
19
17
|
|
|
18
|
+
**Which stories the legacy-RFC leg covers** — only APPROVED ones, and a story is approved when every story map it names is ratified (the "A release row is the RFC, and the map is the approval surface" architecture rule). the "Story maps and stories carry a drift-invalidated human-oversight marker" architecture rule forbids an RFC from referencing a story whose map is not ratified, so for such a story the `## Stories` row is *correctly absent* — a `MISSING_REVERSE_TRACE` against it would be a finding no compliant action could clear, since satisfying it means breaking the rule it exists to protect (the "reconcile-stories reports permanent false MISSING_REVERSE_TRACE drift against 's ratified-stories-only rule" problem / the ": A clean reconciler result means the tier really is clean" delivery story). The problem and JTBD legs are not narrowed; no ratification rule governs those pairs. The release-row leg is not narrowed either: a story card is compelled onto its map at capture (the "Story-map membership and story-content completeness are enforced at capture" architecture rule) and cards sit outside the map's fingerprint basis (the "A release row is the RFC, and the map is the approval surface" architecture rule), so a card's absence from its row is never correct.
|
|
19
|
+
|
|
20
|
+
**The run says which population it checked**, on stderr, whether or not there was drift — so a clean result can be read as "checked and clean" rather than "quietly skipped":
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
reconcile-stories: RFC ## Stories reverse trace checked 15 of 30 story/RFC pairs.
|
|
24
|
+
reconcile-stories: 12 skipped because the story's map is not ratified, so the row is correctly absent.
|
|
25
|
+
reconcile-stories: 3 skipped because the story names no story map at all; that is a corpus defect, not a correct absence.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The two corpus-defect counts — a story naming no map, and a story naming a map id that does not resolve to exactly one file — are reported apart from the correct-absence count on purpose. Folding either in would hide a real defect behind an explanation that does not apply to it.
|
|
29
|
+
|
|
20
30
|
## When to invoke
|
|
21
31
|
|
|
22
32
|
- **Drift detected by another skill's preflight** — `/wr-itil:manage-story` or `/wr-itil:work-problems` Step 0 preflight may surface `docs/stories/README.md` drift; this skill is the recovery path.
|
|
@@ -46,9 +56,7 @@ Read `/tmp/wr-itil-stories-drift-$$.txt` line by line. Each line is one of:
|
|
|
46
56
|
- `DRIFT STORY-NNN rankings: claims=<X> actual=<Y>` — Story Rankings row has wrong Status; update the row.
|
|
47
57
|
- `STALE STORY-NNN rankings: actual=<state>` — Story Rankings table is missing a row; add it.
|
|
48
58
|
- `MISMATCH STORY-NNN done: actual=<state>` — Done table has wrong row OR an extra row; remove/adjust.
|
|
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.
|
|
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).
|
|
51
|
-
- `STATUS_MISMATCH STORY-NNN in <PARENT-ID> ## Stories claims=<X> actual=<Y>` — same helper call refreshes the status column.
|
|
59
|
+
- `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. The helper regenerates the whole section and admits only approved stories, so it can never write in a reference the "Story maps and stories carry a drift-invalidated human-oversight marker" architecture rule forbids. When it withholds one it says so on stderr: the remedy is to **ratify that story's MAP** (the "A release row is the RFC, and the map is the approval surface" architecture rule), never to hand-add the row.
|
|
52
60
|
- `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.
|
|
53
61
|
|
|
54
62
|
### 3. Apply edits
|
|
@@ -103,11 +111,14 @@ After commit, report:
|
|
|
103
111
|
- **the "Plugin-bundled scripts invoked from SKILL.md resolve via `bin/` on `$PATH`" architecture rule** — plugin-bundled scripts via `bin/` on `$PATH`. `wr-itil-reconcile-stories` shim follows this grammar.
|
|
104
112
|
- **the "Governance Skills Commit Their Own Completed Work" architecture rule** — single-commit grain. The reconciliation pass is a single coherent action; one commit per pass.
|
|
105
113
|
- **the "Behavioural-tests-default for skill testing" architecture rule** — behavioural-tests default. Bats coverage at `packages/itil/scripts/test/reconcile-stories.bats` (the "Problem tickets strain as fixes decompose into multiple coordinated changes — need an RFC framework that ties all changes back to problems (and unifies technical with user/business problems)" problem Phase 2 Slice 9).
|
|
106
|
-
- **the "Session-start briefing surface — SessionStart hook over tiered directory + indexed README" architecture rule
|
|
114
|
+
- **Exit-code convention** — exit-1 on drift, exit-0 on clean, exit-2 on parse error. Shared unchanged with `reconcile-readme.sh`, `reconcile-rfcs.sh` and `check-rfc-stories-ratified.sh`; no ADR records it, and none should — a convention with one viable shape, already implemented uniformly, is not decision-bearing. (This line cited the "Session-start briefing surface — SessionStart hook over tiered directory + indexed README" architecture rule until 2026-09-19; the "Session-start briefing surface — SessionStart hook over tiered directory + indexed README" architecture rule is the session-start briefing surface and says nothing about exit codes.)
|
|
115
|
+
- **the "Story maps and stories carry a drift-invalidated human-oversight marker" architecture rule** / **the "A release row is the RFC, and the map is the approval surface" architecture rule** / **the "Story-map membership and story-content completeness are enforced at capture" architecture rule** — the rules that set the legacy-RFC leg's population: an RFC references only approved stories; approval reaches a story through its map; a story card is on its map from capture.
|
|
116
|
+
- **the "reconcile-stories reports permanent false MISSING_REVERSE_TRACE drift against 's ratified-stories-only rule" problem** / **the ": A clean reconciler result means the tier really is clean" delivery story** — the narrowing, and the stderr population report that makes a clean result readable.
|
|
107
117
|
- **the "`docs/problems/README.md` drifts from filesystem truth across sessions despite (refresh-on-create) and (refresh-on-transition) both Closed" problem** / `reconcile-readme.sh` — sibling at the problems tier.
|
|
108
118
|
- **the "Problem-RFC-Story framework with mandatory problem-trace and unified problem ontology" architecture rule Phase 1 item 5** / `reconcile-rfcs.sh` — sibling at the RFC tier.
|
|
109
119
|
- **Slice 2a/2b helpers** — `update-problem-references-section.sh`, `update-rfc-references-section.sh`, `update-jtbd-references-section.sh` are the load-bearing reverse-trace refresh helpers this skill invokes; all three accept `"Stories"` as a section-name token per their lookup tables.
|
|
110
120
|
- **the ": Enforce Governance Without Slowing Down" user outcome** — Enforce Governance Without Slowing Down. Drift detection is an automated governance enforcement surface; mechanical repair preserves the spirit while removing manual toil.
|
|
121
|
+
- **the ": Progress the Backlog While I'm Away" user outcome** — Progress the Backlog While I'm Away. The legacy-RFC-leg narrowing (the "reconcile-stories reports permanent false MISSING_REVERSE_TRACE drift against 's ratified-stories-only rule" problem / the ": A clean reconciler result means the tier really is clean" delivery story) exists so an unattended loop can read a clean result as "checked and clean" rather than "quietly skipped", and so no finding demands an action the ratified-stories rule forbids.
|
|
111
122
|
- **the ": Decompose a Fix Into Coordinated Changes" user outcome** — Decompose a Fix Into Coordinated Changes. Story tier reverse-trace integrity is load-bearing for the working-the-problem flow's per-story dispatch (Slice 13 traversal).
|
|
112
123
|
|
|
113
124
|
$ARGUMENTS
|
|
@@ -23,11 +23,21 @@ Sibling to `/wr-itil:reconcile-readme` (the "`docs/problems/README.md` drifts fr
|
|
|
23
23
|
|
|
24
24
|
**Reverse-trace pass** — when the parent directories exist, the reconciler checks auto-maintained `## Stories` sections on problem, JTBD and legacy RFC files. For an the "A release row is the RFC, and the map is the approval surface" architecture rule row-backed RFC, it checks the map row instead: the RFC must resolve and the row must contain the story card.
|
|
25
25
|
- `MISSING_REVERSE_TRACE STORY-NNN in <PARENT-ID> ## Stories` — story claims parent but parent's `## Stories` table doesn't list the story
|
|
26
|
-
- `STALE_REVERSE_TRACE STORY-NNN in <PARENT-ID> ## Stories` — parent lists the story but story no longer claims the parent
|
|
27
|
-
- `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
|
|
28
26
|
- `UNRESOLVED_RFC_TRACE STORY-NNN claims=RFC-NNN` — neither a legacy RFC file nor a story-map release row exists
|
|
29
27
|
- `MISSING_REVERSE_TRACE STORY-NNN in RFC-NNN release row` — the release row exists but does not contain the story card
|
|
30
28
|
|
|
29
|
+
**Which stories the legacy-RFC leg covers** — only APPROVED ones, and a story is approved when every story map it names is ratified (the "A release row is the RFC, and the map is the approval surface" architecture rule). the "Story maps and stories carry a drift-invalidated human-oversight marker" architecture rule forbids an RFC from referencing a story whose map is not ratified, so for such a story the `## Stories` row is *correctly absent* — a `MISSING_REVERSE_TRACE` against it would be a finding no compliant action could clear, since satisfying it means breaking the rule it exists to protect (the "reconcile-stories reports permanent false MISSING_REVERSE_TRACE drift against 's ratified-stories-only rule" problem / the ": A clean reconciler result means the tier really is clean" delivery story). The problem and JTBD legs are not narrowed; no ratification rule governs those pairs. The release-row leg is not narrowed either: a story card is compelled onto its map at capture (the "Story-map membership and story-content completeness are enforced at capture" architecture rule) and cards sit outside the map's fingerprint basis (the "A release row is the RFC, and the map is the approval surface" architecture rule), so a card's absence from its row is never correct.
|
|
30
|
+
|
|
31
|
+
**The run says which population it checked**, on stderr, whether or not there was drift — so a clean result can be read as "checked and clean" rather than "quietly skipped":
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
reconcile-stories: RFC ## Stories reverse trace checked 15 of 30 story/RFC pairs.
|
|
35
|
+
reconcile-stories: 12 skipped because the story's map is not ratified, so the row is correctly absent.
|
|
36
|
+
reconcile-stories: 3 skipped because the story names no story map at all; that is a corpus defect, not a correct absence.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The two corpus-defect counts — a story naming no map, and a story naming a map id that does not resolve to exactly one file — are reported apart from the correct-absence count on purpose. Folding either in would hide a real defect behind an explanation that does not apply to it.
|
|
40
|
+
|
|
31
41
|
## When to invoke
|
|
32
42
|
|
|
33
43
|
- **Drift detected by another skill's preflight** — `/wr-itil:manage-story` or `/wr-itil:work-problems` Step 0 preflight may surface `docs/stories/README.md` drift; this skill is the recovery path.
|
|
@@ -57,9 +67,7 @@ Read `/tmp/wr-itil-stories-drift-$$.txt` line by line. Each line is one of:
|
|
|
57
67
|
- `DRIFT STORY-NNN rankings: claims=<X> actual=<Y>` — Story Rankings row has wrong Status; update the row.
|
|
58
68
|
- `STALE STORY-NNN rankings: actual=<state>` — Story Rankings table is missing a row; add it.
|
|
59
69
|
- `MISMATCH STORY-NNN done: actual=<state>` — Done table has wrong row OR an extra row; remove/adjust.
|
|
60
|
-
- `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.
|
|
61
|
-
- `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).
|
|
62
|
-
- `STATUS_MISMATCH STORY-NNN in <PARENT-ID> ## Stories claims=<X> actual=<Y>` — same helper call refreshes the status column.
|
|
70
|
+
- `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. The helper regenerates the whole section and admits only approved stories, so it can never write in a reference the "Story maps and stories carry a drift-invalidated human-oversight marker" architecture rule forbids. When it withholds one it says so on stderr: the remedy is to **ratify that story's MAP** (the "A release row is the RFC, and the map is the approval surface" architecture rule), never to hand-add the row.
|
|
63
71
|
- `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.
|
|
64
72
|
|
|
65
73
|
### 3. Apply edits
|
|
@@ -114,11 +122,14 @@ After commit, report:
|
|
|
114
122
|
- **the "Plugin-bundled scripts invoked from SKILL.md resolve via `bin/` on `$PATH`" architecture rule** — plugin-bundled scripts via `bin/` on `$PATH`. `<itil-plugin-root>/bin/wr-itil-reconcile-stories` shim follows this grammar.
|
|
115
123
|
- **the "Governance Skills Commit Their Own Completed Work" architecture rule** — single-commit grain. The reconciliation pass is a single coherent action; one commit per pass.
|
|
116
124
|
- **the "Behavioural-tests-default for skill testing" architecture rule** — behavioural-tests default. Bats coverage at `<itil-plugin-root>/scripts/test/reconcile-stories.bats` (the "Problem tickets strain as fixes decompose into multiple coordinated changes — need an RFC framework that ties all changes back to problems (and unifies technical with user/business problems)" problem Phase 2 Slice 9).
|
|
117
|
-
- **the "Session-start briefing surface — SessionStart hook over tiered directory + indexed README" architecture rule
|
|
125
|
+
- **Exit-code convention** — exit-1 on drift, exit-0 on clean, exit-2 on parse error. Shared unchanged with `reconcile-readme.sh`, `reconcile-rfcs.sh` and `check-rfc-stories-ratified.sh`; no ADR records it, and none should — a convention with one viable shape, already implemented uniformly, is not decision-bearing. (This line cited the "Session-start briefing surface — SessionStart hook over tiered directory + indexed README" architecture rule until 2026-09-19; the "Session-start briefing surface — SessionStart hook over tiered directory + indexed README" architecture rule is the session-start briefing surface and says nothing about exit codes.)
|
|
126
|
+
- **the "Story maps and stories carry a drift-invalidated human-oversight marker" architecture rule** / **the "A release row is the RFC, and the map is the approval surface" architecture rule** / **the "Story-map membership and story-content completeness are enforced at capture" architecture rule** — the rules that set the legacy-RFC leg's population: an RFC references only approved stories; approval reaches a story through its map; a story card is on its map from capture.
|
|
127
|
+
- **the "reconcile-stories reports permanent false MISSING_REVERSE_TRACE drift against 's ratified-stories-only rule" problem** / **the ": A clean reconciler result means the tier really is clean" delivery story** — the narrowing, and the stderr population report that makes a clean result readable.
|
|
118
128
|
- **the "`docs/problems/README.md` drifts from filesystem truth across sessions despite (refresh-on-create) and (refresh-on-transition) both Closed" problem** / `reconcile-readme.sh` — sibling at the problems tier.
|
|
119
129
|
- **the "Problem-RFC-Story framework with mandatory problem-trace and unified problem ontology" architecture rule Phase 1 item 5** / `reconcile-rfcs.sh` — sibling at the RFC tier.
|
|
120
130
|
- **Slice 2a/2b helpers** — `update-problem-references-section.sh`, `update-rfc-references-section.sh`, `update-jtbd-references-section.sh` are the load-bearing reverse-trace refresh helpers this skill invokes; all three accept `"Stories"` as a section-name token per their lookup tables.
|
|
121
131
|
- **the ": Enforce Governance Without Slowing Down" user outcome** — Enforce Governance Without Slowing Down. Drift detection is an automated governance enforcement surface; mechanical repair preserves the spirit while removing manual toil.
|
|
132
|
+
- **the ": Progress the Backlog While I'm Away" user outcome** — Progress the Backlog While I'm Away. The legacy-RFC-leg narrowing (the "reconcile-stories reports permanent false MISSING_REVERSE_TRACE drift against 's ratified-stories-only rule" problem / the ": A clean reconciler result means the tier really is clean" delivery story) exists so an unattended loop can read a clean result as "checked and clean" rather than "quietly skipped", and so no finding demands an action the ratified-stories rule forbids.
|
|
122
133
|
- **the ": Decompose a Fix Into Coordinated Changes" user outcome** — Decompose a Fix Into Coordinated Changes. Story tier reverse-trace integrity is load-bearing for the working-the-problem flow's per-story dispatch (Slice 13 traversal).
|
|
123
134
|
|
|
124
135
|
$ARGUMENTS
|