@windyroad/itil 0.24.0-preview.268 → 0.24.1-preview.270
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/package.json +2 -1
- package/scripts/check-problems-readme-budget.sh +73 -0
- package/scripts/classify-readme-drift.sh +115 -0
- package/scripts/reconcile-readme.sh +208 -0
- package/scripts/test/check-problems-readme-budget.bats +192 -0
- package/scripts/test/classify-readme-drift.bats +262 -0
- package/scripts/test/reconcile-readme.bats +509 -0
- package/scripts/test/release-watch-poll-loop.bats +180 -0
- package/skills/work-problems/SKILL.md +41 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windyroad/itil",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.1-preview.270",
|
|
4
4
|
"description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"windyroad-itil": "./bin/install.mjs"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"agents/",
|
|
25
25
|
"hooks/",
|
|
26
26
|
"skills/",
|
|
27
|
+
"scripts/",
|
|
27
28
|
".claude-plugin/",
|
|
28
29
|
"lib/"
|
|
29
30
|
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/check-problems-readme-budget.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for the docs/problems/README.md "Last
|
|
5
|
+
# reviewed" line (line 3). Sibling to P099's
|
|
6
|
+
# packages/retrospective/scripts/check-briefing-budgets.sh on a
|
|
7
|
+
# different surface — both apply ADR-040 line 92's reusable triplet
|
|
8
|
+
# (read-only diagnostic + behavioural bats + ADR-tier-budget
|
|
9
|
+
# enforcement clause) to an accumulator-doc surface.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# check-problems-readme-budget.sh [<readme-path>]
|
|
13
|
+
#
|
|
14
|
+
# Default <readme-path> is ./docs/problems/README.md.
|
|
15
|
+
# Threshold is read from PROBLEMS_README_LINE3_MAX_BYTES (default 5120
|
|
16
|
+
# — matches ADR-040 Tier 3 Tier 3 envelope; the soft per-fragment cap
|
|
17
|
+
# is 1024 bytes, enforced by the SKILL.md authoring contracts in
|
|
18
|
+
# manage-problem / transition-problem / transition-problems /
|
|
19
|
+
# review-problems / reconcile-readme).
|
|
20
|
+
#
|
|
21
|
+
# Exit codes:
|
|
22
|
+
# 0 = always (advisory only — overflow is signal, not failure)
|
|
23
|
+
# 2 = parse error (README path missing or unreadable)
|
|
24
|
+
#
|
|
25
|
+
# Output format on overflow (one line, terse machine-readable per
|
|
26
|
+
# ADR-038 progressive-disclosure budget):
|
|
27
|
+
# OVER <readme-path> line=3 bytes=<N> threshold=<N>
|
|
28
|
+
#
|
|
29
|
+
# Output is empty (no lines) when line 3 is under the threshold.
|
|
30
|
+
#
|
|
31
|
+
# Read-only — does NOT mutate the README. Truncation is owned by the
|
|
32
|
+
# per-operation refresh contracts in manage-problem Step 5 P094 +
|
|
33
|
+
# Step 7 P062 (and the sibling skills).
|
|
34
|
+
#
|
|
35
|
+
# @problem P134
|
|
36
|
+
# @adr ADR-040 (Session-start briefing surface — Tier 3 budget; line 92's
|
|
37
|
+
# reusable-pattern note explicitly names "problems index" as a
|
|
38
|
+
# candidate surface for this triplet)
|
|
39
|
+
# @adr ADR-038 (Progressive disclosure — per-row terse budget)
|
|
40
|
+
# @adr ADR-013 Rule 6 (non-interactive fail-safe — advisory exit 0)
|
|
41
|
+
# @adr ADR-005 (Plugin testing strategy)
|
|
42
|
+
# @jtbd JTBD-001 / JTBD-006 / JTBD-101
|
|
43
|
+
|
|
44
|
+
set -uo pipefail
|
|
45
|
+
|
|
46
|
+
README_PATH="${1:-docs/problems/README.md}"
|
|
47
|
+
THRESHOLD="${PROBLEMS_README_LINE3_MAX_BYTES:-5120}"
|
|
48
|
+
|
|
49
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
if [ ! -f "$README_PATH" ]; then
|
|
52
|
+
echo "check-problems-readme-budget: README not found: $README_PATH" >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# ── Measure line 3 byte size ────────────────────────────────────────────────
|
|
57
|
+
# `awk 'NR==3'` extracts line 3 verbatim; the trailing newline that awk
|
|
58
|
+
# emits is not part of the line's content. We use printf '%s' to feed
|
|
59
|
+
# the line to wc -c with no added newline so the byte count matches the
|
|
60
|
+
# in-file content of line 3. Empty / missing line 3 yields 0 bytes.
|
|
61
|
+
|
|
62
|
+
line3="$(awk 'NR==3' "$README_PATH")"
|
|
63
|
+
bytes=$(printf '%s' "$line3" | wc -c | tr -d ' ')
|
|
64
|
+
|
|
65
|
+
if [ "$bytes" -ge "$THRESHOLD" ] && [ "$bytes" -gt 0 ]; then
|
|
66
|
+
echo "OVER $README_PATH line=3 bytes=$bytes threshold=$THRESHOLD"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Edge case: threshold of 0 with non-empty line 3 — still report (sanity
|
|
70
|
+
# path exercised in bats). The first guard above requires bytes > 0 so
|
|
71
|
+
# we never emit OVER for an empty/missing line 3.
|
|
72
|
+
|
|
73
|
+
exit 0
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/classify-readme-drift.sh
|
|
3
|
+
#
|
|
4
|
+
# Classify reconcile-readme.sh exit-1 drift output as either
|
|
5
|
+
# inline-refresh-deferrable (covered by uncommitted ticket renames in the
|
|
6
|
+
# working tree — in-flow P094/P062 refresh will land the README correction
|
|
7
|
+
# in the upcoming commit per ADR-014) or halt-route-reconcile (committed
|
|
8
|
+
# cross-session drift — must route to /wr-itil:reconcile-readme).
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# classify-readme-drift.sh <drift-stdout-file> [<problems-dir>]
|
|
12
|
+
#
|
|
13
|
+
# <drift-stdout-file>: path to a file containing the captured stdout of
|
|
14
|
+
# `reconcile-readme.sh` (one structured drift line per row — `DRIFT`,
|
|
15
|
+
# `MISSING`, `STALE`, or `MISMATCH`).
|
|
16
|
+
#
|
|
17
|
+
# <problems-dir>: defaults to ./docs/problems. Used by `git status
|
|
18
|
+
# --porcelain` to scope the staged-rename probe.
|
|
19
|
+
#
|
|
20
|
+
# Output (stdout, single classification line):
|
|
21
|
+
# INLINE_REFRESH covered=<N> — every drift ID is the destination
|
|
22
|
+
# of a staged rename in the working
|
|
23
|
+
# tree; defer to in-flow P094/P062
|
|
24
|
+
# refresh per ADR-014.
|
|
25
|
+
# HALT_ROUTE_RECONCILE uncovered=<N> — at least one drift ID is NOT
|
|
26
|
+
# covered by a working-tree rename;
|
|
27
|
+
# committed cross-session drift OR
|
|
28
|
+
# mixed; route to
|
|
29
|
+
# /wr-itil:reconcile-readme.
|
|
30
|
+
#
|
|
31
|
+
# Exit codes:
|
|
32
|
+
# 0 = INLINE_REFRESH
|
|
33
|
+
# 1 = HALT_ROUTE_RECONCILE
|
|
34
|
+
# 2 = parse error (drift-stdout-file missing or empty)
|
|
35
|
+
#
|
|
36
|
+
# @problem P149 — manage-problem Step 0 reconcile halt-on-drift directive
|
|
37
|
+
# doesn't distinguish uncommitted-rename-rooted drift from
|
|
38
|
+
# committed cross-session drift; this script is the
|
|
39
|
+
# detection mechanism that makes the carve-out behavioural.
|
|
40
|
+
# @adr ADR-014 (single-commit grain — the carve-out preserves it for the
|
|
41
|
+
# in-flow path while keeping cross-session drift safety)
|
|
42
|
+
# @adr ADR-013 Rule 6 (AFK fail-safe — committed drift still routes to
|
|
43
|
+
# /wr-itil:reconcile-readme; only the inline path
|
|
44
|
+
# is added)
|
|
45
|
+
# @adr ADR-038 (progressive disclosure — output is a single terse line)
|
|
46
|
+
# @jtbd JTBD-006 (Progress the Backlog While I'm Away — AFK loop continuity)
|
|
47
|
+
# @jtbd JTBD-001 (Enforce Governance Without Slowing Down — single-commit grain)
|
|
48
|
+
|
|
49
|
+
set -uo pipefail
|
|
50
|
+
|
|
51
|
+
DRIFT_FILE="${1:-}"
|
|
52
|
+
PROBLEMS_DIR="${2:-docs/problems}"
|
|
53
|
+
|
|
54
|
+
if [ -z "$DRIFT_FILE" ]; then
|
|
55
|
+
echo "USAGE: classify-readme-drift.sh <drift-stdout-file> [<problems-dir>]" >&2
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if [ ! -f "$DRIFT_FILE" ]; then
|
|
60
|
+
echo "PARSE_ERROR: drift-stdout-file not found: $DRIFT_FILE" >&2
|
|
61
|
+
exit 2
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# ── Extract drifting IDs from the script's structured output ────────────────
|
|
65
|
+
# Each line is one of:
|
|
66
|
+
# DRIFT P<NNN> wsjf-rankings: ...
|
|
67
|
+
# MISSING P<NNN> wsjf-rankings: ...
|
|
68
|
+
# MISSING P<NNN> verification-queue: ...
|
|
69
|
+
# STALE P<NNN> verification-queue: ...
|
|
70
|
+
# MISMATCH P<NNN> closed: ...
|
|
71
|
+
DRIFT_IDS="$(grep -oE 'P[0-9]{3}' "$DRIFT_FILE" 2>/dev/null | sort -u)"
|
|
72
|
+
|
|
73
|
+
if [ -z "$DRIFT_IDS" ]; then
|
|
74
|
+
echo "PARSE_ERROR: drift-stdout-file empty (no P<NNN> tokens): $DRIFT_FILE" >&2
|
|
75
|
+
exit 2
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# ── Build set of IDs covered by staged renames in the working tree ──────────
|
|
79
|
+
# `git status --porcelain` v1 emits rename lines as:
|
|
80
|
+
# R <old-path> -> <new-path>
|
|
81
|
+
# RM <old-path> -> <new-path> (rename + unstaged modification)
|
|
82
|
+
# We match the destination path's ticket ID — the post-rename status is what
|
|
83
|
+
# the in-flow P094/P062 refresh will reconcile in the upcoming commit.
|
|
84
|
+
RENAMED_IDS=""
|
|
85
|
+
if git rev-parse --git-dir >/dev/null 2>&1; then
|
|
86
|
+
RENAMED_IDS="$(
|
|
87
|
+
git status --porcelain "$PROBLEMS_DIR" 2>/dev/null \
|
|
88
|
+
| awk '/^R/' \
|
|
89
|
+
| sed 's|.*-> ||' \
|
|
90
|
+
| sed "s|^${PROBLEMS_DIR}/||" \
|
|
91
|
+
| grep -oE '^[0-9]{3}' \
|
|
92
|
+
| awk '{ printf "P%s\n", $0 }' \
|
|
93
|
+
| sort -u
|
|
94
|
+
)"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# ── Cross-reference each drift ID against the renamed set ───────────────────
|
|
98
|
+
COVERED=0
|
|
99
|
+
UNCOVERED=0
|
|
100
|
+
while IFS= read -r id; do
|
|
101
|
+
[ -z "$id" ] && continue
|
|
102
|
+
if [ -n "$RENAMED_IDS" ] && printf '%s\n' "$RENAMED_IDS" | grep -qx "$id"; then
|
|
103
|
+
COVERED=$((COVERED + 1))
|
|
104
|
+
else
|
|
105
|
+
UNCOVERED=$((UNCOVERED + 1))
|
|
106
|
+
fi
|
|
107
|
+
done <<< "$DRIFT_IDS"
|
|
108
|
+
|
|
109
|
+
if [ "$UNCOVERED" -gt 0 ]; then
|
|
110
|
+
printf 'HALT_ROUTE_RECONCILE uncovered=%d\n' "$UNCOVERED"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
printf 'INLINE_REFRESH covered=%d\n' "$COVERED"
|
|
115
|
+
exit 0
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/reconcile-readme.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only drift detector for docs/problems/README.md vs filesystem
|
|
5
|
+
# truth. Reads <problems-dir>/<NNN>-*.<status>.md, parses the README's
|
|
6
|
+
# WSJF Rankings + Verification Queue + Closed tables, and reports each
|
|
7
|
+
# disagreement.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# reconcile-readme.sh [<problems-dir>]
|
|
11
|
+
#
|
|
12
|
+
# Default <problems-dir> is ./docs/problems.
|
|
13
|
+
#
|
|
14
|
+
# Exit codes:
|
|
15
|
+
# 0 = clean (README matches filesystem)
|
|
16
|
+
# 1 = drift detected (structured diff to stdout)
|
|
17
|
+
# 2 = parse error (README missing or malformed)
|
|
18
|
+
#
|
|
19
|
+
# Output format on drift (one line per drift entry, ≤ 150 bytes per
|
|
20
|
+
# ADR-038 progressive-disclosure budget):
|
|
21
|
+
# DRIFT <ID> wsjf-rankings: claims=<status> actual=<status>
|
|
22
|
+
# MISSING <ID> wsjf-rankings: actual=<status> file=<basename>
|
|
23
|
+
# STALE <ID> verification-queue: actual=<status>
|
|
24
|
+
# MISMATCH <ID> closed: actual=<status>
|
|
25
|
+
#
|
|
26
|
+
# Read-only — does NOT mutate the README. The /wr-itil:reconcile-readme
|
|
27
|
+
# skill applies edits with narrative-aware preservation; this script's
|
|
28
|
+
# only job is to report ground truth.
|
|
29
|
+
#
|
|
30
|
+
# @problem P118
|
|
31
|
+
# @adr ADR-014 (Reconciliation as preflight robustness layer)
|
|
32
|
+
# @adr ADR-022 (Verification Pending lifecycle excludes from WSJF Rankings)
|
|
33
|
+
# @adr ADR-038 (Progressive disclosure — per-row byte budget)
|
|
34
|
+
|
|
35
|
+
set -uo pipefail
|
|
36
|
+
|
|
37
|
+
PROBLEMS_DIR="${1:-docs/problems}"
|
|
38
|
+
README="${PROBLEMS_DIR}/README.md"
|
|
39
|
+
|
|
40
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
if [ ! -f "$README" ]; then
|
|
43
|
+
echo "PARSE_ERROR: README not found at ${README}" >&2
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if ! grep -q '^## WSJF Rankings' "$README"; then
|
|
48
|
+
echo "PARSE_ERROR: '## WSJF Rankings' header missing in ${README}" >&2
|
|
49
|
+
exit 2
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ── Build filesystem truth: ID → status ─────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
declare -A FS_STATUS
|
|
55
|
+
shopt -s nullglob
|
|
56
|
+
for f in "$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.open.md \
|
|
57
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.known-error.md \
|
|
58
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.verifying.md \
|
|
59
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.closed.md \
|
|
60
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.parked.md; do
|
|
61
|
+
base="$(basename "$f")"
|
|
62
|
+
num="${base%%-*}"
|
|
63
|
+
id="P${num}"
|
|
64
|
+
# `ticket_status` (not bash `status`) — zsh has `$status` as a read-only
|
|
65
|
+
# built-in mapping to `$?`. Defensive rename per P133 even though this
|
|
66
|
+
# script's `#!/usr/bin/env bash` shebang means it never runs under zsh.
|
|
67
|
+
case "$base" in
|
|
68
|
+
*.open.md) ticket_status="open" ;;
|
|
69
|
+
*.known-error.md) ticket_status="known-error" ;;
|
|
70
|
+
*.verifying.md) ticket_status="verifying" ;;
|
|
71
|
+
*.closed.md) ticket_status="closed" ;;
|
|
72
|
+
*.parked.md) ticket_status="parked" ;;
|
|
73
|
+
*) continue ;;
|
|
74
|
+
esac
|
|
75
|
+
FS_STATUS["$id"]="$ticket_status"
|
|
76
|
+
done
|
|
77
|
+
shopt -u nullglob
|
|
78
|
+
|
|
79
|
+
# ── Parse README sections into ID buckets ───────────────────────────────────
|
|
80
|
+
# We use the section-header line numbers to slice the file into ranges.
|
|
81
|
+
|
|
82
|
+
WSJF_START=$(grep -n '^## WSJF Rankings' "$README" | head -1 | cut -d: -f1)
|
|
83
|
+
VQ_START=$(grep -n '^## Verification Queue' "$README" | head -1 | cut -d: -f1)
|
|
84
|
+
CLOSED_START=$(grep -n '^## Closed' "$README" | head -1 | cut -d: -f1)
|
|
85
|
+
PARKED_START=$(grep -n '^## Parked' "$README" | head -1 | cut -d: -f1)
|
|
86
|
+
END_LINE=$(wc -l < "$README")
|
|
87
|
+
|
|
88
|
+
# Sentinel each end with the next section start (or EOF).
|
|
89
|
+
WSJF_END=${VQ_START:-${CLOSED_START:-${PARKED_START:-$END_LINE}}}
|
|
90
|
+
VQ_END=${CLOSED_START:-${PARKED_START:-$END_LINE}}
|
|
91
|
+
CLOSED_END=${PARKED_START:-$END_LINE}
|
|
92
|
+
PARKED_END=$END_LINE
|
|
93
|
+
|
|
94
|
+
# Extract IDs claimed by each section. Only data rows of the form
|
|
95
|
+
# | ... | P<NNN> | ... |
|
|
96
|
+
# count; header + separator rows are skipped naturally because they
|
|
97
|
+
# do not contain a P<NNN> token in the second column.
|
|
98
|
+
|
|
99
|
+
extract_section_ids() {
|
|
100
|
+
local start="$1" end="$2"
|
|
101
|
+
[ -z "$start" ] && return 0
|
|
102
|
+
sed -n "${start},${end}p" "$README" \
|
|
103
|
+
| grep -oE '\| *P[0-9]{3} *\|' \
|
|
104
|
+
| grep -oE 'P[0-9]{3}' \
|
|
105
|
+
| sort -u
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
README_WSJF_IDS="$(extract_section_ids "$WSJF_START" "$WSJF_END")"
|
|
109
|
+
README_VQ_IDS="$(extract_section_ids "$VQ_START" "$VQ_END")"
|
|
110
|
+
README_CLOSED_IDS="$(extract_section_ids "$CLOSED_START" "$CLOSED_END")"
|
|
111
|
+
README_PARKED_IDS="$(extract_section_ids "$PARKED_START" "$PARKED_END")"
|
|
112
|
+
|
|
113
|
+
# ── Diff ─────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
DRIFT_LINES=()
|
|
116
|
+
|
|
117
|
+
# (1) Each ID listed in WSJF Rankings must be .open.md or .known-error.md
|
|
118
|
+
# on disk. .verifying.md → drift (belongs in VQ); .closed.md → drift;
|
|
119
|
+
# .parked.md → drift; missing → drift.
|
|
120
|
+
while read -r id; do
|
|
121
|
+
[ -z "$id" ] && continue
|
|
122
|
+
actual="${FS_STATUS[$id]:-missing}"
|
|
123
|
+
case "$actual" in
|
|
124
|
+
open|known-error)
|
|
125
|
+
: # ok
|
|
126
|
+
;;
|
|
127
|
+
*)
|
|
128
|
+
DRIFT_LINES+=("DRIFT ${id} wsjf-rankings: claims=open actual=${actual}")
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
done <<< "$README_WSJF_IDS"
|
|
132
|
+
|
|
133
|
+
# (2) Each ID listed in Verification Queue must be .verifying.md on disk.
|
|
134
|
+
# .closed.md → STALE (drift class P062 closure didn't refresh);
|
|
135
|
+
# .open.md / .known-error.md → STALE; missing → STALE.
|
|
136
|
+
while read -r id; do
|
|
137
|
+
[ -z "$id" ] && continue
|
|
138
|
+
actual="${FS_STATUS[$id]:-missing}"
|
|
139
|
+
case "$actual" in
|
|
140
|
+
verifying)
|
|
141
|
+
: # ok
|
|
142
|
+
;;
|
|
143
|
+
*)
|
|
144
|
+
DRIFT_LINES+=("STALE ${id} verification-queue: actual=${actual}")
|
|
145
|
+
;;
|
|
146
|
+
esac
|
|
147
|
+
done <<< "$README_VQ_IDS"
|
|
148
|
+
|
|
149
|
+
# (3) Each ID listed in Closed section must be .closed.md on disk.
|
|
150
|
+
while read -r id; do
|
|
151
|
+
[ -z "$id" ] && continue
|
|
152
|
+
actual="${FS_STATUS[$id]:-missing}"
|
|
153
|
+
case "$actual" in
|
|
154
|
+
closed)
|
|
155
|
+
: # ok
|
|
156
|
+
;;
|
|
157
|
+
*)
|
|
158
|
+
DRIFT_LINES+=("MISMATCH ${id} closed: actual=${actual}")
|
|
159
|
+
;;
|
|
160
|
+
esac
|
|
161
|
+
done <<< "$README_CLOSED_IDS"
|
|
162
|
+
|
|
163
|
+
# (4) Each .open.md / .known-error.md file on disk must appear in WSJF
|
|
164
|
+
# Rankings. Build a lookup set for quick membership tests.
|
|
165
|
+
declare -A IN_WSJF
|
|
166
|
+
while read -r id; do
|
|
167
|
+
[ -z "$id" ] && continue
|
|
168
|
+
IN_WSJF["$id"]=1
|
|
169
|
+
done <<< "$README_WSJF_IDS"
|
|
170
|
+
|
|
171
|
+
declare -A IN_VQ
|
|
172
|
+
while read -r id; do
|
|
173
|
+
[ -z "$id" ] && continue
|
|
174
|
+
IN_VQ["$id"]=1
|
|
175
|
+
done <<< "$README_VQ_IDS"
|
|
176
|
+
|
|
177
|
+
for id in "${!FS_STATUS[@]}"; do
|
|
178
|
+
ticket_status="${FS_STATUS[$id]}"
|
|
179
|
+
case "$ticket_status" in
|
|
180
|
+
open|known-error)
|
|
181
|
+
if [ -z "${IN_WSJF[$id]:-}" ]; then
|
|
182
|
+
DRIFT_LINES+=("MISSING ${id} wsjf-rankings: actual=${ticket_status}")
|
|
183
|
+
fi
|
|
184
|
+
;;
|
|
185
|
+
verifying)
|
|
186
|
+
if [ -z "${IN_VQ[$id]:-}" ]; then
|
|
187
|
+
DRIFT_LINES+=("MISSING ${id} verification-queue: actual=${ticket_status}")
|
|
188
|
+
fi
|
|
189
|
+
;;
|
|
190
|
+
# closed and parked: not required to appear in their respective
|
|
191
|
+
# sections (Closed is curated narrative; Parked is exhaustive but
|
|
192
|
+
# an absence is a soft drift not flagged at this layer).
|
|
193
|
+
esac
|
|
194
|
+
done
|
|
195
|
+
|
|
196
|
+
# ── Report ──────────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
if [ ${#DRIFT_LINES[@]} -eq 0 ]; then
|
|
199
|
+
exit 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# Sort for stable output (ID order).
|
|
203
|
+
IFS=$'\n' sorted=($(printf '%s\n' "${DRIFT_LINES[@]}" | sort))
|
|
204
|
+
unset IFS
|
|
205
|
+
for line in "${sorted[@]}"; do
|
|
206
|
+
printf '%s\n' "$line"
|
|
207
|
+
done
|
|
208
|
+
exit 1
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# @problem P134 — docs/problems/README.md line 3 narrative-blob accumulator
|
|
4
|
+
# bloat. Sibling to P099 (briefing tier 3) on a different surface. The
|
|
5
|
+
# "Last reviewed" line accumulates session-summary fragments unbounded;
|
|
6
|
+
# at ~62 KB it breaks the Read tool entirely (25K-token limit on the
|
|
7
|
+
# whole file regardless of offset/limit). P134 promotes the same
|
|
8
|
+
# advisory-triplet pattern P099 documented (advisory script + behavioural
|
|
9
|
+
# bats + ADR-040-tier-budget enforcement clause) to this surface.
|
|
10
|
+
#
|
|
11
|
+
# Contract: `check-problems-readme-budget.sh [<readme-path>]` is a
|
|
12
|
+
# diagnose-only advisory script. It reads `docs/problems/README.md`
|
|
13
|
+
# (or the supplied path), measures the byte size of line 3, and reports
|
|
14
|
+
# overflow when line 3 is at or above the configured threshold (default
|
|
15
|
+
# 5120 bytes per ADR-040 Tier 3 envelope; overridable via the
|
|
16
|
+
# `PROBLEMS_README_LINE3_MAX_BYTES` env var).
|
|
17
|
+
#
|
|
18
|
+
# Exit codes:
|
|
19
|
+
# 0 = always (advisory only — overflow is signal, not failure)
|
|
20
|
+
# 2 = parse error (README path missing or unreadable)
|
|
21
|
+
#
|
|
22
|
+
# Output format on overflow (one line, terse machine-readable per
|
|
23
|
+
# ADR-038 progressive-disclosure budget):
|
|
24
|
+
# OVER <readme-path> line=3 bytes=<N> threshold=<N>
|
|
25
|
+
#
|
|
26
|
+
# Output is empty (no lines) when line 3 is under the threshold.
|
|
27
|
+
#
|
|
28
|
+
# The script is read-only — it does NOT mutate the README. Truncation
|
|
29
|
+
# is owned by the per-operation refresh contracts in `manage-problem`
|
|
30
|
+
# Step 5 P094 + Step 7 P062 (and the sibling skills `transition-problem`,
|
|
31
|
+
# `transition-problems`, `review-problems`, `reconcile-readme`).
|
|
32
|
+
#
|
|
33
|
+
# @jtbd JTBD-001 (Enforce Governance Without Slowing Down — restores
|
|
34
|
+
# Read-tool affordance to the highest-traffic problems-management surface)
|
|
35
|
+
# @jtbd JTBD-006 (Progress the Backlog While I'm Away — every AFK iter's
|
|
36
|
+
# manage-problem call previously paid an awk/grep workaround tax)
|
|
37
|
+
# @jtbd JTBD-101 (Extend the Suite with Clear Patterns — re-applies P099's
|
|
38
|
+
# advisory-script + bats + ADR-tier-budget triplet to a new surface,
|
|
39
|
+
# exactly the reusable shape ADR-040 line 92 documents)
|
|
40
|
+
#
|
|
41
|
+
# Cross-reference:
|
|
42
|
+
# P134: docs/problems/134-docs-problems-readme-md-line-3-narrative-blob-accumulator-bloat-sibling-p099.*.md
|
|
43
|
+
# P099: docs/problems/099-briefing-md-grows-unbounded-via-run-retro-appends-violating-progressive-disclosure.*.md
|
|
44
|
+
# ADR-040 — Session-start briefing surface (Tier 3 budget; line 92's
|
|
45
|
+
# reusable-pattern note explicitly names "problems index" as a
|
|
46
|
+
# candidate surface for this triplet)
|
|
47
|
+
# ADR-038 — Progressive disclosure (per-row terse budget)
|
|
48
|
+
# ADR-013 Rule 6 — non-interactive fail-safe (advisory-only / exit 0)
|
|
49
|
+
# ADR-005 — Plugin testing strategy (script-level bats governance)
|
|
50
|
+
|
|
51
|
+
setup() {
|
|
52
|
+
SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
53
|
+
SCRIPT="$SCRIPTS_DIR/check-problems-readme-budget.sh"
|
|
54
|
+
FIXTURE_DIR="$(mktemp -d)"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
teardown() {
|
|
58
|
+
rm -rf "$FIXTURE_DIR"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Helper: write a problems README fixture with the given line-3 byte size.
|
|
62
|
+
# Lines 1, 2 are short header; line 3 is the synthetic "Last reviewed"
|
|
63
|
+
# blob padded to target_bytes; lines 4+ contain a short stub so the
|
|
64
|
+
# multi-line shape mirrors the production README.
|
|
65
|
+
write_problems_readme() {
|
|
66
|
+
local path="$1"
|
|
67
|
+
local target_bytes="$2"
|
|
68
|
+
: > "$path"
|
|
69
|
+
printf '# Problem Backlog\n\n' >> "$path"
|
|
70
|
+
if [ "$target_bytes" -gt 0 ]; then
|
|
71
|
+
# Build a single line of exactly target_bytes bytes (no trailing
|
|
72
|
+
# newline before the byte budget completes). The newline at the
|
|
73
|
+
# end is part of the file structure but not counted in line 3's
|
|
74
|
+
# byte size — `awk 'NR==3' | wc -c` would include it; the script
|
|
75
|
+
# under test must strip it to match the threshold semantics.
|
|
76
|
+
local prefix='> Last reviewed: '
|
|
77
|
+
local prefix_size=${#prefix}
|
|
78
|
+
local body_target=$(( target_bytes - prefix_size ))
|
|
79
|
+
if [ "$body_target" -gt 0 ]; then
|
|
80
|
+
printf '%s' "$prefix" >> "$path"
|
|
81
|
+
# Append body_target dots — a single contiguous line.
|
|
82
|
+
printf '%.0s.' $(seq 1 "$body_target") >> "$path"
|
|
83
|
+
else
|
|
84
|
+
# target smaller than prefix — emit only the requested bytes
|
|
85
|
+
printf '%.0s.' $(seq 1 "$target_bytes") >> "$path"
|
|
86
|
+
fi
|
|
87
|
+
printf '\n' >> "$path"
|
|
88
|
+
else
|
|
89
|
+
printf '\n' >> "$path"
|
|
90
|
+
fi
|
|
91
|
+
printf '\n## WSJF Rankings\n\n(stub)\n' >> "$path"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ── Existence + executable ──────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
@test "check-problems-readme-budget: script exists" {
|
|
97
|
+
[ -f "$SCRIPT" ]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@test "check-problems-readme-budget: script is executable" {
|
|
101
|
+
[ -x "$SCRIPT" ]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ── Default-threshold behaviour (5120 bytes per ADR-040 Tier 3 envelope) ────
|
|
105
|
+
|
|
106
|
+
@test "check-problems-readme-budget: line 3 well under threshold produces no output and exits 0" {
|
|
107
|
+
write_problems_readme "$FIXTURE_DIR/README.md" 1024
|
|
108
|
+
run "$SCRIPT" "$FIXTURE_DIR/README.md"
|
|
109
|
+
[ "$status" -eq 0 ]
|
|
110
|
+
[ -z "$output" ]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@test "check-problems-readme-budget: line 3 at exactly the default threshold emits OVER (>= boundary)" {
|
|
114
|
+
write_problems_readme "$FIXTURE_DIR/README.md" 5120
|
|
115
|
+
run "$SCRIPT" "$FIXTURE_DIR/README.md"
|
|
116
|
+
[ "$status" -eq 0 ]
|
|
117
|
+
echo "$output" | grep -E "^OVER .*README.md line=3 bytes=5120 threshold=5120$"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@test "check-problems-readme-budget: line 3 well over threshold emits OVER with bytes + threshold" {
|
|
121
|
+
write_problems_readme "$FIXTURE_DIR/README.md" 12000
|
|
122
|
+
run "$SCRIPT" "$FIXTURE_DIR/README.md"
|
|
123
|
+
[ "$status" -eq 0 ]
|
|
124
|
+
echo "$output" | grep -E "^OVER .*README.md line=3 bytes=12000 threshold=5120$"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@test "check-problems-readme-budget: short line 3 (single fragment ~600 bytes) produces no output" {
|
|
128
|
+
write_problems_readme "$FIXTURE_DIR/README.md" 600
|
|
129
|
+
run "$SCRIPT" "$FIXTURE_DIR/README.md"
|
|
130
|
+
[ "$status" -eq 0 ]
|
|
131
|
+
[ -z "$output" ]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# ── Configurable threshold via env var ──────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
@test "check-problems-readme-budget: PROBLEMS_README_LINE3_MAX_BYTES env var overrides default" {
|
|
137
|
+
write_problems_readme "$FIXTURE_DIR/README.md" 3000
|
|
138
|
+
# Default 5120: under threshold, no output. With env var set to 2000:
|
|
139
|
+
# over threshold, expect OVER line.
|
|
140
|
+
PROBLEMS_README_LINE3_MAX_BYTES=2000 run "$SCRIPT" "$FIXTURE_DIR/README.md"
|
|
141
|
+
[ "$status" -eq 0 ]
|
|
142
|
+
echo "$output" | grep -E "^OVER .*README.md line=3 bytes=3000 threshold=2000$"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@test "check-problems-readme-budget: env var threshold of 0 emits OVER for non-empty line 3 (sanity)" {
|
|
146
|
+
write_problems_readme "$FIXTURE_DIR/README.md" 100
|
|
147
|
+
PROBLEMS_README_LINE3_MAX_BYTES=0 run "$SCRIPT" "$FIXTURE_DIR/README.md"
|
|
148
|
+
[ "$status" -eq 0 ]
|
|
149
|
+
echo "$output" | grep -E "^OVER .*README.md line=3 bytes=100 threshold=0$"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# ── Argument and error handling ─────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
@test "check-problems-readme-budget: defaults to docs/problems/README.md when no arg provided" {
|
|
155
|
+
cd "$FIXTURE_DIR"
|
|
156
|
+
mkdir -p docs/problems
|
|
157
|
+
write_problems_readme docs/problems/README.md 8000
|
|
158
|
+
run "$SCRIPT"
|
|
159
|
+
[ "$status" -eq 0 ]
|
|
160
|
+
echo "$output" | grep -E "^OVER docs/problems/README.md line=3 bytes=8000 threshold=5120$"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@test "check-problems-readme-budget: missing README path exits 2 with parse error on stderr" {
|
|
164
|
+
run "$SCRIPT" "$FIXTURE_DIR/does-not-exist/README.md"
|
|
165
|
+
[ "$status" -eq 2 ]
|
|
166
|
+
echo "$output" | grep -iE "not found|missing|does not exist"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@test "check-problems-readme-budget: file with no line 3 (only 2 lines) exits 0 with no output" {
|
|
170
|
+
printf '# Problem Backlog\n\n' > "$FIXTURE_DIR/README.md"
|
|
171
|
+
run "$SCRIPT" "$FIXTURE_DIR/README.md"
|
|
172
|
+
[ "$status" -eq 0 ]
|
|
173
|
+
[ -z "$output" ]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@test "check-problems-readme-budget: file with empty line 3 exits 0 with no output" {
|
|
177
|
+
printf '# Problem Backlog\n\n\n## WSJF Rankings\n\n(stub)\n' > "$FIXTURE_DIR/README.md"
|
|
178
|
+
run "$SCRIPT" "$FIXTURE_DIR/README.md"
|
|
179
|
+
[ "$status" -eq 0 ]
|
|
180
|
+
[ -z "$output" ]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# ── Read-only contract ──────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
@test "check-problems-readme-budget: script does not mutate the README under audit" {
|
|
186
|
+
write_problems_readme "$FIXTURE_DIR/README.md" 12000
|
|
187
|
+
pre_hash=$(shasum -a 256 "$FIXTURE_DIR/README.md" | awk '{print $1}')
|
|
188
|
+
run "$SCRIPT" "$FIXTURE_DIR/README.md"
|
|
189
|
+
[ "$status" -eq 0 ]
|
|
190
|
+
post_hash=$(shasum -a 256 "$FIXTURE_DIR/README.md" | awk '{print $1}')
|
|
191
|
+
[ "$pre_hash" = "$post_hash" ]
|
|
192
|
+
}
|