@windyroad/retrospective 0.16.0-preview.273 → 0.17.0-preview.277
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/bin/wr-retrospective-check-tarball-shipped-shims +2 -0
- package/package.json +2 -1
- package/scripts/check-ask-hygiene.sh +163 -0
- package/scripts/check-briefing-budgets.sh +121 -0
- package/scripts/check-internal-id-leaks.sh +169 -0
- package/scripts/check-readme-jtbd-currency.sh +207 -0
- package/scripts/check-skill-md-budgets.sh +130 -0
- package/scripts/check-tarball-shipped-shims.sh +227 -0
- package/scripts/check-tickets-deferred-cause.sh +220 -0
- package/scripts/list-plugin-attribution.sh +139 -0
- package/scripts/measure-context-budget.sh +270 -0
- package/scripts/test/check-ask-hygiene.bats +248 -0
- package/scripts/test/check-briefing-budgets.bats +307 -0
- package/scripts/test/check-internal-id-leaks.bats +286 -0
- package/scripts/test/check-readme-jtbd-currency.bats +217 -0
- package/scripts/test/check-skill-md-budgets.bats +336 -0
- package/scripts/test/check-tarball-shipped-shims.bats +284 -0
- package/scripts/test/check-tickets-deferred-cause.bats +330 -0
- package/scripts/test/list-plugin-attribution.bats +149 -0
- package/scripts/test/measure-context-budget.bats +236 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windyroad/retrospective",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0-preview.277",
|
|
4
4
|
"description": "Session retrospectives that update briefings and create problem tickets",
|
|
5
5
|
"bin": {
|
|
6
6
|
"windyroad-retrospective": "./bin/install.mjs"
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"agents/",
|
|
24
24
|
"hooks/",
|
|
25
25
|
"skills/",
|
|
26
|
+
"scripts/",
|
|
26
27
|
".claude-plugin/",
|
|
27
28
|
"lib/"
|
|
28
29
|
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-ask-hygiene.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for the ask-hygiene trail (per Step 2d
|
|
5
|
+
# in run-retro, ADR-044 / P135 Phase 5). Walks docs/retros/*-ask-hygiene.md
|
|
6
|
+
# trail files, extracts the lazy AskUserQuestion count from each, and
|
|
7
|
+
# emits a tabular trend over the last N retros so the user can see
|
|
8
|
+
# whether ADR-044's framework-resolution boundary is taking hold.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# check-ask-hygiene.sh [<retros-dir>]
|
|
12
|
+
#
|
|
13
|
+
# Default <retros-dir> is ./docs/retros.
|
|
14
|
+
# Window is read from ASK_HYGIENE_WINDOW (default 10 — last N retros).
|
|
15
|
+
#
|
|
16
|
+
# Exit codes:
|
|
17
|
+
# 0 = always (advisory only — count is signal, not failure)
|
|
18
|
+
# 2 = parse error (retros dir missing or unreadable)
|
|
19
|
+
#
|
|
20
|
+
# Output format on populated trail (one line per retro, oldest first):
|
|
21
|
+
# RETRO <YYYY-MM-DD> lazy=<N> direction=<N> override=<N> silent=<N> taste=<N> correction=<N>
|
|
22
|
+
#
|
|
23
|
+
# Plus a trailing TREND line summarising first vs last lazy count
|
|
24
|
+
# (when 2+ entries are in the window):
|
|
25
|
+
# TREND lazy_first=<N> lazy_last=<N> delta=<+|-N>
|
|
26
|
+
#
|
|
27
|
+
# Output is empty (no lines) when no retro trail entries are found —
|
|
28
|
+
# this is the expected first-run state, not an error.
|
|
29
|
+
#
|
|
30
|
+
# Trail file shape (per Step 2d contract — written by run-retro):
|
|
31
|
+
# docs/retros/<YYYY-MM-DD>-ask-hygiene.md
|
|
32
|
+
#
|
|
33
|
+
# Each trail file MUST contain a single line of the shape:
|
|
34
|
+
# `Lazy count: <N>` (case-insensitive; allows a leading `**` for bold)
|
|
35
|
+
#
|
|
36
|
+
# And SHOULD contain matching counts for each non-lazy category:
|
|
37
|
+
# `Direction count: <N>`
|
|
38
|
+
# `Override count: <N>`
|
|
39
|
+
# `Silent-framework count: <N>`
|
|
40
|
+
# `Taste count: <N>`
|
|
41
|
+
# `Correction-followup count: <N>`
|
|
42
|
+
#
|
|
43
|
+
# The script tolerates missing non-lazy categories (defaults to 0).
|
|
44
|
+
# The lazy-count line is the only required field; without it the file
|
|
45
|
+
# is skipped silently.
|
|
46
|
+
#
|
|
47
|
+
# Read-only — does NOT mutate any retro file.
|
|
48
|
+
#
|
|
49
|
+
# @problem P135 (Phase 5 measurement)
|
|
50
|
+
# @adr ADR-044 (Decision-Delegation Contract — framework-resolution boundary; lazy-count metric is the regression signal)
|
|
51
|
+
# @adr ADR-040 (Tier 3 advisory-not-fail-closed — declarative-first precedent)
|
|
52
|
+
# @adr ADR-038 (Progressive disclosure — per-row byte budget)
|
|
53
|
+
# @adr ADR-026 (Cost-source grounding — trail entries cite specific tool invocations per retro)
|
|
54
|
+
# @adr ADR-013 Rule 5 (Policy-authorised silent proceed — script proceeds silently per ADR-040 advisory pattern)
|
|
55
|
+
# @adr ADR-005 (Plugin testing strategy)
|
|
56
|
+
# @jtbd JTBD-001 / JTBD-006 / JTBD-201
|
|
57
|
+
|
|
58
|
+
set -uo pipefail
|
|
59
|
+
|
|
60
|
+
RETROS_DIR="${1:-docs/retros}"
|
|
61
|
+
WINDOW="${ASK_HYGIENE_WINDOW:-10}"
|
|
62
|
+
|
|
63
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
if [ ! -d "$RETROS_DIR" ]; then
|
|
66
|
+
echo "check-ask-hygiene: retros dir not found: $RETROS_DIR" >&2
|
|
67
|
+
exit 2
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ── Scan ────────────────────────────────────────────────────────────────────
|
|
71
|
+
# Iterate ask-hygiene trail files at the top level of RETROS_DIR.
|
|
72
|
+
# Glob expansion uses an existence-check loop for cross-shell portability
|
|
73
|
+
# (P124 lesson — bash's `shopt -s nullglob` fails on zsh).
|
|
74
|
+
|
|
75
|
+
trail_files=()
|
|
76
|
+
for path in "$RETROS_DIR"/*-ask-hygiene.md; do
|
|
77
|
+
[ -e "$path" ] || continue
|
|
78
|
+
trail_files+=("$path")
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
if [ "${#trail_files[@]}" -eq 0 ]; then
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Sort by basename (which starts with the YYYY-MM-DD date prefix), oldest first
|
|
86
|
+
IFS=$'\n' sorted_files=($(printf '%s\n' "${trail_files[@]}" | sort))
|
|
87
|
+
unset IFS
|
|
88
|
+
|
|
89
|
+
# Apply the window — keep only the last N entries (most-recent N)
|
|
90
|
+
total="${#sorted_files[@]}"
|
|
91
|
+
if [ "$total" -gt "$WINDOW" ]; then
|
|
92
|
+
start=$(( total - WINDOW ))
|
|
93
|
+
windowed=("${sorted_files[@]:$start}")
|
|
94
|
+
else
|
|
95
|
+
windowed=("${sorted_files[@]}")
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Extract counts per file
|
|
99
|
+
extract_count() {
|
|
100
|
+
local path="$1"
|
|
101
|
+
local label="$2"
|
|
102
|
+
local default="${3:-0}"
|
|
103
|
+
# Match lines like "Lazy count: 5" or "**Lazy count: 5**" (case-insensitive).
|
|
104
|
+
# Markdown bold is an enclosing pair, so leading **<text>: <N>** has the
|
|
105
|
+
# trailing ** AFTER the number, not after the label.
|
|
106
|
+
local match
|
|
107
|
+
match=$(grep -iE "^\*{0,2}$label count:[[:space:]]+[0-9]+" "$path" 2>/dev/null \
|
|
108
|
+
| head -1 \
|
|
109
|
+
| grep -oE '[0-9]+' \
|
|
110
|
+
| head -1)
|
|
111
|
+
if [ -z "$match" ]; then
|
|
112
|
+
echo "$default"
|
|
113
|
+
else
|
|
114
|
+
echo "$match"
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
extract_date() {
|
|
119
|
+
local basename
|
|
120
|
+
basename="$(basename "$1")"
|
|
121
|
+
# Strip the -ask-hygiene.md suffix; what remains is the YYYY-MM-DD prefix
|
|
122
|
+
echo "${basename%-ask-hygiene.md}"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Emit per-retro lines
|
|
126
|
+
declare -a lazy_counts=()
|
|
127
|
+
for path in "${windowed[@]}"; do
|
|
128
|
+
date=$(extract_date "$path")
|
|
129
|
+
# Lazy count is required (no default); if missing, skip the file silently.
|
|
130
|
+
# Inline rather than calling extract_count() because its `local default=...`
|
|
131
|
+
# falls back to "0" on empty (bash `${3:-0}` semantics), which would
|
|
132
|
+
# mask the "no lazy line" case.
|
|
133
|
+
lazy=$(grep -iE "^\*{0,2}Lazy count:[[:space:]]+[0-9]+" "$path" 2>/dev/null \
|
|
134
|
+
| head -1 \
|
|
135
|
+
| grep -oE '[0-9]+' \
|
|
136
|
+
| head -1)
|
|
137
|
+
if [ -z "$lazy" ]; then
|
|
138
|
+
continue
|
|
139
|
+
fi
|
|
140
|
+
# Other categories default to 0 if the trail entry omits them
|
|
141
|
+
direction=$(extract_count "$path" "Direction" "0")
|
|
142
|
+
override=$(extract_count "$path" "Override" "0")
|
|
143
|
+
silent=$(extract_count "$path" "Silent-framework" "0")
|
|
144
|
+
taste=$(extract_count "$path" "Taste" "0")
|
|
145
|
+
correction=$(extract_count "$path" "Correction-followup" "0")
|
|
146
|
+
echo "RETRO $date lazy=$lazy direction=$direction override=$override silent=$silent taste=$taste correction=$correction"
|
|
147
|
+
lazy_counts+=("$lazy")
|
|
148
|
+
done
|
|
149
|
+
|
|
150
|
+
# Emit trend line when 2+ entries are in the window
|
|
151
|
+
if [ "${#lazy_counts[@]}" -ge 2 ]; then
|
|
152
|
+
first="${lazy_counts[0]}"
|
|
153
|
+
last="${lazy_counts[${#lazy_counts[@]}-1]}"
|
|
154
|
+
delta=$(( last - first ))
|
|
155
|
+
if [ "$delta" -ge 0 ]; then
|
|
156
|
+
delta_s="+$delta"
|
|
157
|
+
else
|
|
158
|
+
delta_s="$delta"
|
|
159
|
+
fi
|
|
160
|
+
echo "TREND lazy_first=$first lazy_last=$last delta=$delta_s"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
exit 0
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-briefing-budgets.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for the docs/briefing/ tree (Tier 3 of
|
|
5
|
+
# ADR-040). Walks <briefing-dir>/<topic>.md files, measures byte size
|
|
6
|
+
# per file, and reports each topic file at or above the configured
|
|
7
|
+
# threshold so run-retro Step 3 can route them through the rotation
|
|
8
|
+
# AskUserQuestion (interactive) or defer to the retro summary (AFK).
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# check-briefing-budgets.sh [<briefing-dir>]
|
|
12
|
+
#
|
|
13
|
+
# Default <briefing-dir> is ./docs/briefing.
|
|
14
|
+
# Threshold is read from BRIEFING_TIER3_MAX_BYTES (default 5120 — the
|
|
15
|
+
# upper bound of ADR-040's stated "2-5 KB / topic" Tier 3 envelope).
|
|
16
|
+
#
|
|
17
|
+
# Exit codes:
|
|
18
|
+
# 0 = always (advisory only — overflow is signal, not failure)
|
|
19
|
+
# 2 = parse error (briefing dir missing or unreadable)
|
|
20
|
+
#
|
|
21
|
+
# Output format on overflow (one line per file, terse machine-readable
|
|
22
|
+
# per ADR-038 progressive-disclosure budget):
|
|
23
|
+
# OVER <basename> bytes=<N> threshold=<N>
|
|
24
|
+
#
|
|
25
|
+
# Files at >= 2.0x the threshold also emit a second line that promotes
|
|
26
|
+
# ADR-040's reassessment trigger ("≥ 3 topic files exceed 2× the
|
|
27
|
+
# configured ceiling for ≥ 2 consecutive retro cycles") from
|
|
28
|
+
# policy-revisit-time to per-cycle enforcement on the same threshold:
|
|
29
|
+
# MUST_SPLIT <basename> reason=<code>
|
|
30
|
+
#
|
|
31
|
+
# The MUST_SPLIT line is the "no defer" signal: run-retro Step 3 Tier 3
|
|
32
|
+
# silent-agent rotation is forced to pick split-by-subtopic /
|
|
33
|
+
# split-by-date for these files (the trim-noise / leave-as-is fall-
|
|
34
|
+
# throughs are not eligible). See P145.
|
|
35
|
+
#
|
|
36
|
+
# Output ordering (deterministic for stable retro-summary diffs):
|
|
37
|
+
# 1. All OVER lines, sorted by basename.
|
|
38
|
+
# 2. Then all MUST_SPLIT lines, sorted by basename.
|
|
39
|
+
#
|
|
40
|
+
# Output is empty (no lines) when no topic files exceed the threshold.
|
|
41
|
+
# README.md is excluded from the scan — it is Tier 2, not Tier 3.
|
|
42
|
+
#
|
|
43
|
+
# Read-only — does NOT mutate any briefing file. Rotation is surfaced
|
|
44
|
+
# to the user via run-retro Step 3.
|
|
45
|
+
#
|
|
46
|
+
# @problem P099 (initial OVER advisory)
|
|
47
|
+
# @problem P145 (MUST_SPLIT escalation — closes the defer-recurrence gap)
|
|
48
|
+
# @adr ADR-040 (Session-start briefing surface — Tier 3 budget; this
|
|
49
|
+
# script promotes Tier 3 from informational to advisory enforcement;
|
|
50
|
+
# MUST_SPLIT promotes the 2× reassessment trigger to per-cycle)
|
|
51
|
+
# @adr ADR-038 (Progressive disclosure — per-row byte budget)
|
|
52
|
+
# @adr ADR-013 (Rule 1 / Rule 6 — interactive vs AFK)
|
|
53
|
+
# @adr ADR-044 (Decision-delegation contract — MUST_SPLIT is framework-
|
|
54
|
+
# resolved removal of the do-nothing options when ratio is decisive)
|
|
55
|
+
# @adr ADR-005 (Plugin testing strategy)
|
|
56
|
+
# @jtbd JTBD-001 / JTBD-006 / JTBD-101
|
|
57
|
+
|
|
58
|
+
set -uo pipefail
|
|
59
|
+
|
|
60
|
+
BRIEFING_DIR="${1:-docs/briefing}"
|
|
61
|
+
THRESHOLD="${BRIEFING_TIER3_MAX_BYTES:-5120}"
|
|
62
|
+
|
|
63
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
if [ ! -d "$BRIEFING_DIR" ]; then
|
|
66
|
+
echo "check-briefing-budgets: briefing dir not found: $BRIEFING_DIR" >&2
|
|
67
|
+
exit 2
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ── Scan ────────────────────────────────────────────────────────────────────
|
|
71
|
+
# Iterate markdown files at the top level of BRIEFING_DIR (not recursive).
|
|
72
|
+
# Sort by basename for stable diff output (per bats fixture contract).
|
|
73
|
+
|
|
74
|
+
shopt -s nullglob
|
|
75
|
+
files=("$BRIEFING_DIR"/*.md)
|
|
76
|
+
shopt -u nullglob
|
|
77
|
+
|
|
78
|
+
if [ "${#files[@]}" -eq 0 ]; then
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Build (basename, bytes) pairs sorted by basename.
|
|
83
|
+
declare -a entries=()
|
|
84
|
+
for path in "${files[@]}"; do
|
|
85
|
+
base="$(basename "$path")"
|
|
86
|
+
# README.md is the Tier 2 index, not a Tier 3 topic file.
|
|
87
|
+
if [ "$base" = "README.md" ]; then
|
|
88
|
+
continue
|
|
89
|
+
fi
|
|
90
|
+
bytes=$(wc -c < "$path" | tr -d ' ')
|
|
91
|
+
entries+=("$base $bytes")
|
|
92
|
+
done
|
|
93
|
+
|
|
94
|
+
# Sort entries by basename
|
|
95
|
+
IFS=$'\n' sorted=($(printf '%s\n' "${entries[@]}" | sort))
|
|
96
|
+
unset IFS
|
|
97
|
+
|
|
98
|
+
# Pass 1: emit OVER lines for every file at or above threshold.
|
|
99
|
+
# Track MUST_SPLIT candidates (ratio >= 2.0x) for pass 2.
|
|
100
|
+
declare -a must_split=()
|
|
101
|
+
for entry in "${sorted[@]}"; do
|
|
102
|
+
base="${entry% *}"
|
|
103
|
+
bytes="${entry##* }"
|
|
104
|
+
if [ "$bytes" -ge "$THRESHOLD" ]; then
|
|
105
|
+
echo "OVER $base bytes=$bytes threshold=$THRESHOLD"
|
|
106
|
+
# Integer-arithmetic ratio test: bytes >= 2 * threshold.
|
|
107
|
+
# Avoids float math; exact at the 2.0x boundary per ADR-040
|
|
108
|
+
# reassessment trigger.
|
|
109
|
+
if [ "$bytes" -ge "$(( THRESHOLD * 2 ))" ]; then
|
|
110
|
+
must_split+=("$base")
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
done
|
|
114
|
+
|
|
115
|
+
# Pass 2: emit MUST_SPLIT lines (already sorted by pass-1 traversal order
|
|
116
|
+
# which is basename-sorted).
|
|
117
|
+
for base in "${must_split[@]}"; do
|
|
118
|
+
echo "MUST_SPLIT $base reason=ratio-exceeds-2x"
|
|
119
|
+
done
|
|
120
|
+
|
|
121
|
+
exit 0
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-internal-id-leaks.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for plugin-published internal-ID leaks per
|
|
5
|
+
# ADR-055 (Plugin-published artefacts use namespace-prefixed permalinks).
|
|
6
|
+
#
|
|
7
|
+
# Walks shipped-artefact surfaces under `<root-dir>/packages/<plugin>/`:
|
|
8
|
+
# - skills/<skill>/SKILL.md
|
|
9
|
+
# - agents/*.md
|
|
10
|
+
# - hooks/*.sh
|
|
11
|
+
# - CHANGELOG.md
|
|
12
|
+
#
|
|
13
|
+
# Reports each artefact carrying bare internal-ID tokens that lack the
|
|
14
|
+
# `WR-` namespace prefix. Bare tokens are tokens that resolve correctly
|
|
15
|
+
# only inside the windyroad-claude-plugin source repo's docs/decisions/,
|
|
16
|
+
# docs/jtbd/, and docs/problems/ trees — adopter projects either find
|
|
17
|
+
# nothing (failure mode 1, benign) or resolve to UNRELATED IDs in the
|
|
18
|
+
# adopter's own tree (failure mode 3, dangerous).
|
|
19
|
+
#
|
|
20
|
+
# Usage:
|
|
21
|
+
# check-internal-id-leaks.sh [<root-dir>]
|
|
22
|
+
#
|
|
23
|
+
# Default <root-dir> is `.`.
|
|
24
|
+
#
|
|
25
|
+
# Token forms detected (case-sensitive, word-boundary):
|
|
26
|
+
# ADR-NNN (3+ digits)
|
|
27
|
+
# JTBD-NNN (3+ digits)
|
|
28
|
+
# PNNN (exactly 3 digits — problem ticket form)
|
|
29
|
+
#
|
|
30
|
+
# Tokens that DO NOT trigger:
|
|
31
|
+
# WR-ADR-NNN, WR-JTBD-NNN, WR-PNNN (namespace-prefixed; the strategy)
|
|
32
|
+
# docstring annotation lines beginning with `# @adr` / `# @jtbd` /
|
|
33
|
+
# `# @problem` (maintainer-facing source annotations, never expanded
|
|
34
|
+
# into adopter agent context per ADR-055 §Scope)
|
|
35
|
+
#
|
|
36
|
+
# Files NOT scanned:
|
|
37
|
+
# REFERENCE.md sibling files (lazy-loaded maintainer surface per ADR-054)
|
|
38
|
+
#
|
|
39
|
+
# Exit codes:
|
|
40
|
+
# 0 = always (advisory only — drift is signal, not failure)
|
|
41
|
+
# 2 = parse error (root dir missing or unreadable)
|
|
42
|
+
#
|
|
43
|
+
# Output format on drift (one line per file with leaks, terse machine-
|
|
44
|
+
# readable per ADR-038 progressive-disclosure budget):
|
|
45
|
+
# OVER <plugin>/<relative-path> bare_count=<N>
|
|
46
|
+
#
|
|
47
|
+
# Followed by a final aggregate summary line:
|
|
48
|
+
# TOTAL packages=<N> with_leaks=<M> drift_instances=<K>
|
|
49
|
+
#
|
|
50
|
+
# Output is empty (no lines) when no shipped artefact carries bare
|
|
51
|
+
# tokens — silent-on-pass per ADR-045 hook injection budget discipline.
|
|
52
|
+
#
|
|
53
|
+
# Output ordering (deterministic for stable retro-summary diffs):
|
|
54
|
+
# OVER lines sorted by `<plugin>/<relative-path>` identifier.
|
|
55
|
+
# TOTAL line last.
|
|
56
|
+
#
|
|
57
|
+
# Read-only — does NOT mutate any artefact. Per ADR-052, the bats fixture
|
|
58
|
+
# at scripts/test/check-internal-id-leaks.bats is BEHAVIOURAL — asserts
|
|
59
|
+
# script output on temp-fixture trees, NOT script source content.
|
|
60
|
+
#
|
|
61
|
+
# @problem P137 (Plugin-published artefacts reference internal IDs that
|
|
62
|
+
# adopter projects can't resolve)
|
|
63
|
+
# @adr ADR-055 (Plugin-published artefacts use namespace-prefixed
|
|
64
|
+
# permalinks — strategy + advisory detector)
|
|
65
|
+
# @adr ADR-038 (Progressive disclosure — terse machine-readable signal)
|
|
66
|
+
# @adr ADR-045 (Hook injection budget — silent-on-pass discipline)
|
|
67
|
+
# @adr ADR-052 (Behavioural-tests-default — fixture pattern)
|
|
68
|
+
# @adr ADR-054 (SKILL.md runtime budget — REFERENCE.md exclusion source)
|
|
69
|
+
# @adr ADR-005 (Plugin testing strategy)
|
|
70
|
+
# @jtbd JTBD-302 (Trust That the README Describes the Plugin I Just
|
|
71
|
+
# Installed — semantic correctness axis of adopter-facing content)
|
|
72
|
+
|
|
73
|
+
set -uo pipefail
|
|
74
|
+
|
|
75
|
+
ROOT_DIR="${1:-.}"
|
|
76
|
+
|
|
77
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
if [ ! -d "$ROOT_DIR" ]; then
|
|
80
|
+
echo "check-internal-id-leaks: root dir not found: $ROOT_DIR" >&2
|
|
81
|
+
exit 2
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ── Collect artefact paths ──────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
shopt -s nullglob
|
|
87
|
+
declare -a artefacts=()
|
|
88
|
+
for path in "$ROOT_DIR"/packages/*/skills/*/SKILL.md; do
|
|
89
|
+
artefacts+=("$path")
|
|
90
|
+
done
|
|
91
|
+
for path in "$ROOT_DIR"/packages/*/agents/*.md; do
|
|
92
|
+
artefacts+=("$path")
|
|
93
|
+
done
|
|
94
|
+
for path in "$ROOT_DIR"/packages/*/hooks/*.sh; do
|
|
95
|
+
artefacts+=("$path")
|
|
96
|
+
done
|
|
97
|
+
for path in "$ROOT_DIR"/packages/*/CHANGELOG.md; do
|
|
98
|
+
artefacts+=("$path")
|
|
99
|
+
done
|
|
100
|
+
shopt -u nullglob
|
|
101
|
+
|
|
102
|
+
if [ "${#artefacts[@]}" -eq 0 ]; then
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# ── Count bare tokens per file ──────────────────────────────────────────────
|
|
107
|
+
# Algorithm (perl one-liner per file):
|
|
108
|
+
# 1. Skip lines matching `^\s*#\s*@(adr|jtbd|problem)\b` (docstring annotations).
|
|
109
|
+
# 2. On surviving lines, find all `(?<!WR-)\b(ADR-\d{3,}|JTBD-\d{3,}|P\d{3})\b`
|
|
110
|
+
# matches and increment a counter.
|
|
111
|
+
# 3. Print the counter as a single integer.
|
|
112
|
+
#
|
|
113
|
+
# Negative lookbehind `(?<!WR-)` is safe in perl. Word-boundary `\b` keeps
|
|
114
|
+
# `WR-ADR-014` matched only at position 3 (after `WR-`), which the
|
|
115
|
+
# lookbehind then rejects. Bare `ADR-014` has start-of-string or non-WR-
|
|
116
|
+
# char before, lookbehind passes, match counts.
|
|
117
|
+
#
|
|
118
|
+
# Word-boundary on the `P\d{3}` form requires \b on both ends so that
|
|
119
|
+
# `Phase` (no digit follows) and `P3` (only 1 digit) don't match.
|
|
120
|
+
|
|
121
|
+
declare -a leaks=()
|
|
122
|
+
declare -A package_set=()
|
|
123
|
+
declare -i total_drift=0
|
|
124
|
+
|
|
125
|
+
# Strip ROOT_DIR + trailing slash for relative-path display.
|
|
126
|
+
strip_root() {
|
|
127
|
+
local full="$1"
|
|
128
|
+
local prefix="$ROOT_DIR/packages/"
|
|
129
|
+
echo "${full#"$prefix"}"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for path in "${artefacts[@]}"; do
|
|
133
|
+
count=$(perl -ne '
|
|
134
|
+
next if /^\s*#\s*\@(adr|jtbd|problem)\b/i;
|
|
135
|
+
while (/(?<!WR-)\b(ADR-\d{3,}|JTBD-\d{3,}|P\d{3})\b/g) {
|
|
136
|
+
$n++;
|
|
137
|
+
}
|
|
138
|
+
END { print $n // 0 }
|
|
139
|
+
' "$path")
|
|
140
|
+
|
|
141
|
+
if [ "$count" -gt 0 ]; then
|
|
142
|
+
rel="$(strip_root "$path")"
|
|
143
|
+
leaks+=("$rel $count")
|
|
144
|
+
plugin="${rel%%/*}"
|
|
145
|
+
package_set["$plugin"]=1
|
|
146
|
+
total_drift=$((total_drift + count))
|
|
147
|
+
fi
|
|
148
|
+
done
|
|
149
|
+
|
|
150
|
+
if [ "${#leaks[@]}" -eq 0 ]; then
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# ── Emit OVER lines (sorted) ────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
IFS=$'\n' sorted=($(printf '%s\n' "${leaks[@]}" | sort))
|
|
157
|
+
unset IFS
|
|
158
|
+
|
|
159
|
+
for entry in "${sorted[@]}"; do
|
|
160
|
+
identifier="${entry% *}"
|
|
161
|
+
bare="${entry##* }"
|
|
162
|
+
echo "OVER $identifier bare_count=$bare"
|
|
163
|
+
done
|
|
164
|
+
|
|
165
|
+
# ── Emit TOTAL summary ──────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
echo "TOTAL packages=${#package_set[@]} with_leaks=${#leaks[@]} drift_instances=$total_drift"
|
|
168
|
+
|
|
169
|
+
exit 0
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-readme-jtbd-currency.sh
|
|
3
|
+
#
|
|
4
|
+
# Diagnose-only advisory script for ADR-051 (JTBD-anchored README rule).
|
|
5
|
+
# Walks packages/*/README.md and emits a drift signal per package:
|
|
6
|
+
#
|
|
7
|
+
# - has_jtbd_anchor=<yes|no> — at least one JTBD-\d{3} match in the README
|
|
8
|
+
# - cited_jobs=<count> — count of distinct JTBD IDs cited
|
|
9
|
+
# - known_jobs=<count> — count of cited IDs that resolve to a current
|
|
10
|
+
# docs/jtbd/<persona>/JTBD-NNN-*.md (any status)
|
|
11
|
+
# - drift_hints=<comma-list> — signal vocabulary:
|
|
12
|
+
# missing-jtbd-section (no JTBD-\d{3} at all)
|
|
13
|
+
# stale-jtbd-citation (cited ID has no resolving file)
|
|
14
|
+
# deprecated-jtbd-citation (cited ID resolves only to .deprecated.md / .superseded.md)
|
|
15
|
+
# skill-inventory-drift (a directory under packages/<plugin>/skills/ is not named in README)
|
|
16
|
+
#
|
|
17
|
+
# Plus a trailing TOTAL line summarising the window:
|
|
18
|
+
# TOTAL packages=<N> with_jtbd=<M> drift_instances=<K>
|
|
19
|
+
#
|
|
20
|
+
# Exit code is always 0 — the script is advisory per ADR-013 Rule 6
|
|
21
|
+
# fail-safe / ADR-040 declarative-first / ADR-051 Phase 1.
|
|
22
|
+
# Drift count is emitted as data on stdout; downstream consumers
|
|
23
|
+
# (run-retro Step 2b future wiring, release-pre-flight habit, Phase 2
|
|
24
|
+
# escalation per ADR-051 Phase 2 criterion) decide whether to act.
|
|
25
|
+
#
|
|
26
|
+
# Usage:
|
|
27
|
+
# check-readme-jtbd-currency.sh [<packages-dir>] [<jtbd-dir>]
|
|
28
|
+
#
|
|
29
|
+
# Defaults:
|
|
30
|
+
# <packages-dir> = ./packages
|
|
31
|
+
# <jtbd-dir> = ./docs/jtbd
|
|
32
|
+
#
|
|
33
|
+
# Exit codes:
|
|
34
|
+
# 0 = always (advisory only — count is signal, not failure)
|
|
35
|
+
# 2 = parse error (packages-dir or jtbd-dir missing or unreadable)
|
|
36
|
+
#
|
|
37
|
+
# Output format (one line per package, alphabetical):
|
|
38
|
+
# README package=<name> has_jtbd_anchor=<yes|no> cited_jobs=<N> known_jobs=<M> drift_hints=<csv>
|
|
39
|
+
#
|
|
40
|
+
# @problem P152 (No pressure or nudge for documentation currency — the driver problem)
|
|
41
|
+
# @adr ADR-051 (JTBD-anchored README with declarative drift advisory — this script's normative source)
|
|
42
|
+
# @adr ADR-008 (JTBD directory structure — the resolution target layout)
|
|
43
|
+
# @adr ADR-013 Rule 6 (non-interactive fail-safe — advisory script never blocks AFK)
|
|
44
|
+
# @adr ADR-040 (declarative-first / advisory-then-escalate precedent)
|
|
45
|
+
# @adr ADR-049 (bin/-on-PATH script resolution — paired wr-retrospective-check-readme-jtbd-currency shim)
|
|
46
|
+
# @adr ADR-005 / ADR-037 (Plugin testing strategy — behavioural tests via bats)
|
|
47
|
+
# @jtbd JTBD-302 (Trust That the README Describes the Plugin I Just Installed — primary served job)
|
|
48
|
+
# @jtbd JTBD-007 (Keep Plugins Current Across Projects — currency expansion)
|
|
49
|
+
# @jtbd JTBD-101 (Extend the Suite with New Plugins — clear patterns the detector documents)
|
|
50
|
+
|
|
51
|
+
set -uo pipefail
|
|
52
|
+
|
|
53
|
+
PACKAGES_DIR="${1:-packages}"
|
|
54
|
+
JTBD_DIR="${2:-docs/jtbd}"
|
|
55
|
+
|
|
56
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
if [ ! -d "$PACKAGES_DIR" ]; then
|
|
59
|
+
echo "check-readme-jtbd-currency: packages dir not found: $PACKAGES_DIR" >&2
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if [ ! -d "$JTBD_DIR" ]; then
|
|
64
|
+
echo "check-readme-jtbd-currency: jtbd dir not found: $JTBD_DIR" >&2
|
|
65
|
+
exit 2
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# ── Build the JTBD index ────────────────────────────────────────────────────
|
|
69
|
+
# Map JTBD-NNN -> comma-separated status suffixes (proposed|validated|deprecated|superseded)
|
|
70
|
+
# A JTBD ID may resolve to multiple files (e.g. during a status transition);
|
|
71
|
+
# we record ALL status suffixes per ID for downstream use.
|
|
72
|
+
|
|
73
|
+
declare -A JTBD_STATUS_BY_ID
|
|
74
|
+
declare -A JTBD_RESOLVED
|
|
75
|
+
|
|
76
|
+
for jpath in "$JTBD_DIR"/*/JTBD-*.md; do
|
|
77
|
+
[ -e "$jpath" ] || continue
|
|
78
|
+
base="$(basename "$jpath")"
|
|
79
|
+
if [[ "$base" =~ ^(JTBD-[0-9]{3})-.*\.([a-z]+)\.md$ ]]; then
|
|
80
|
+
id="${BASH_REMATCH[1]}"
|
|
81
|
+
status="${BASH_REMATCH[2]}"
|
|
82
|
+
JTBD_RESOLVED["$id"]=1
|
|
83
|
+
if [ -z "${JTBD_STATUS_BY_ID[$id]:-}" ]; then
|
|
84
|
+
JTBD_STATUS_BY_ID["$id"]="$status"
|
|
85
|
+
else
|
|
86
|
+
JTBD_STATUS_BY_ID["$id"]="${JTBD_STATUS_BY_ID[$id]},$status"
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
# ── Helpers ─────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
append_hint() {
|
|
94
|
+
local current="$1"
|
|
95
|
+
local hint="$2"
|
|
96
|
+
if [ -z "$current" ]; then
|
|
97
|
+
echo "$hint"
|
|
98
|
+
elif [[ ",$current," == *",$hint,"* ]]; then
|
|
99
|
+
echo "$current"
|
|
100
|
+
else
|
|
101
|
+
echo "$current,$hint"
|
|
102
|
+
fi
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
is_deprecated_only() {
|
|
106
|
+
local statuses="$1"
|
|
107
|
+
IFS=',' read -ra arr <<< "$statuses"
|
|
108
|
+
for s in "${arr[@]}"; do
|
|
109
|
+
case "$s" in
|
|
110
|
+
deprecated|superseded) ;;
|
|
111
|
+
*) return 1 ;;
|
|
112
|
+
esac
|
|
113
|
+
done
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# ── Scan packages ───────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
total_packages=0
|
|
120
|
+
total_with_jtbd=0
|
|
121
|
+
total_drift_instances=0
|
|
122
|
+
|
|
123
|
+
package_dirs=()
|
|
124
|
+
for pdir in "$PACKAGES_DIR"/*/; do
|
|
125
|
+
[ -d "$pdir" ] || continue
|
|
126
|
+
package_dirs+=("$pdir")
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
if [ "${#package_dirs[@]}" -eq 0 ]; then
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
IFS=$'\n' sorted_dirs=($(printf '%s\n' "${package_dirs[@]}" | sort))
|
|
134
|
+
unset IFS
|
|
135
|
+
|
|
136
|
+
for pdir in "${sorted_dirs[@]}"; do
|
|
137
|
+
package="$(basename "$pdir")"
|
|
138
|
+
readme="$pdir/README.md"
|
|
139
|
+
|
|
140
|
+
# Skip packages without a README — out of scope
|
|
141
|
+
[ -f "$readme" ] || continue
|
|
142
|
+
|
|
143
|
+
total_packages=$(( total_packages + 1 ))
|
|
144
|
+
|
|
145
|
+
# Extract distinct JTBD-NNN matches from the README
|
|
146
|
+
cited_ids=()
|
|
147
|
+
while IFS= read -r id; do
|
|
148
|
+
[ -z "$id" ] && continue
|
|
149
|
+
cited_ids+=("$id")
|
|
150
|
+
done < <(grep -oE 'JTBD-[0-9]{3}' "$readme" 2>/dev/null | sort -u)
|
|
151
|
+
|
|
152
|
+
cited_count="${#cited_ids[@]}"
|
|
153
|
+
|
|
154
|
+
has_anchor="no"
|
|
155
|
+
if [ "$cited_count" -gt 0 ]; then
|
|
156
|
+
has_anchor="yes"
|
|
157
|
+
total_with_jtbd=$(( total_with_jtbd + 1 ))
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
hints=""
|
|
161
|
+
known_count=0
|
|
162
|
+
|
|
163
|
+
if [ "$cited_count" -eq 0 ]; then
|
|
164
|
+
hints=$(append_hint "$hints" "missing-jtbd-section")
|
|
165
|
+
else
|
|
166
|
+
has_stale=0
|
|
167
|
+
has_deprecated_only=0
|
|
168
|
+
for id in "${cited_ids[@]}"; do
|
|
169
|
+
if [ -n "${JTBD_RESOLVED[$id]:-}" ]; then
|
|
170
|
+
known_count=$(( known_count + 1 ))
|
|
171
|
+
if is_deprecated_only "${JTBD_STATUS_BY_ID[$id]}"; then
|
|
172
|
+
has_deprecated_only=1
|
|
173
|
+
fi
|
|
174
|
+
else
|
|
175
|
+
has_stale=1
|
|
176
|
+
fi
|
|
177
|
+
done
|
|
178
|
+
[ "$has_stale" -eq 1 ] && hints=$(append_hint "$hints" "stale-jtbd-citation")
|
|
179
|
+
[ "$has_deprecated_only" -eq 1 ] && hints=$(append_hint "$hints" "deprecated-jtbd-citation")
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# Soft heuristic: skill inventory drift — every directory under
|
|
183
|
+
# packages/<plugin>/skills/ should be named in the README.
|
|
184
|
+
if [ -d "$pdir/skills" ]; then
|
|
185
|
+
for sdir in "$pdir/skills"/*/; do
|
|
186
|
+
[ -d "$sdir" ] || continue
|
|
187
|
+
skill="$(basename "$sdir")"
|
|
188
|
+
if ! grep -q -F "$skill" "$readme" 2>/dev/null; then
|
|
189
|
+
hints=$(append_hint "$hints" "skill-inventory-drift")
|
|
190
|
+
break
|
|
191
|
+
fi
|
|
192
|
+
done
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# Drift instance: any non-empty hint set
|
|
196
|
+
if [ -n "$hints" ]; then
|
|
197
|
+
total_drift_instances=$(( total_drift_instances + 1 ))
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
echo "README package=$package has_jtbd_anchor=$has_anchor cited_jobs=$cited_count known_jobs=$known_count drift_hints=$hints"
|
|
201
|
+
done
|
|
202
|
+
|
|
203
|
+
if [ "$total_packages" -gt 0 ]; then
|
|
204
|
+
echo "TOTAL packages=$total_packages with_jtbd=$total_with_jtbd drift_instances=$total_drift_instances"
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
exit 0
|