@windyroad/retrospective 0.16.0 → 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.
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # @problem P099 — docs/briefing/<topic>.md (Tier 3 of ADR-040) accumulates
4
+ # without rotation. ADR-040 names a 2-5 KB / topic ceiling but the budget
5
+ # was informational only. P099 promotes Tier 3 to advisory enforcement: a
6
+ # read-only diagnostic script surfaces topic files over the configured
7
+ # ceiling so run-retro Step 3 can route them through the rotation
8
+ # AskUserQuestion (interactive) or defer to the retro summary (AFK).
9
+ #
10
+ # Contract: `check-briefing-budgets.sh [<briefing-dir>]` is a diagnose-only
11
+ # advisory script. It walks `<briefing-dir>/<topic>.md` files (default
12
+ # `docs/briefing`), measures byte size per file, and reports each topic
13
+ # file whose size is at or above the threshold (default 5120 bytes,
14
+ # overridable via BRIEFING_TIER3_MAX_BYTES env var).
15
+ #
16
+ # Exit codes:
17
+ # 0 = always (advisory only — overflow is signal, not failure)
18
+ # 2 = parse error (briefing dir missing or unreadable)
19
+ #
20
+ # Output format on overflow (one line per file, terse machine-readable
21
+ # per ADR-038 progressive-disclosure budget):
22
+ # OVER <basename> bytes=<N> threshold=<N>
23
+ #
24
+ # Output is empty (no lines) when no topic files exceed the threshold.
25
+ # README.md is excluded from the scan — it is Tier 2, not Tier 3.
26
+ #
27
+ # The script is read-only — it does NOT mutate any briefing file.
28
+ # Rotation candidates are surfaced to the user via run-retro Step 3
29
+ # (AskUserQuestion interactive path or retro-summary AFK fallback).
30
+ #
31
+ # @jtbd JTBD-006 (Progress the Backlog While I'm Away — AFK-safe advisory)
32
+ # @jtbd JTBD-001 (Enforce Governance Without Slowing Down — read-only,
33
+ # no interactive friction on the happy path; cost amortised across
34
+ # every subsequent topic-file load)
35
+ # @jtbd JTBD-101 (Extend the Suite with Clear Patterns — reusable
36
+ # advisory-script + bats + ADR-amendment shape for accumulator surfaces)
37
+ #
38
+ # Cross-reference:
39
+ # P099: docs/problems/099-briefing-md-grows-unbounded-via-run-retro-appends-violating-progressive-disclosure.open.md
40
+ # ADR-040 — Session-start briefing surface (Tier 3 budget; this script
41
+ # promotes Tier 3 from informational to advisory enforcement)
42
+ # ADR-038 — Progressive disclosure (per-row byte budget on diff output)
43
+ # ADR-013 Rule 1 / Rule 6 — interactive AskUserQuestion path / AFK fallback
44
+ # ADR-005 — Plugin testing strategy (script-level bats governance)
45
+
46
+ setup() {
47
+ SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
48
+ SCRIPT="$SCRIPTS_DIR/check-briefing-budgets.sh"
49
+ FIXTURE_DIR="$(mktemp -d)"
50
+ }
51
+
52
+ teardown() {
53
+ rm -rf "$FIXTURE_DIR"
54
+ }
55
+
56
+ # Helper: write a markdown file with N bytes of body content. The header
57
+ # adds a small constant overhead; body fills to roughly the requested
58
+ # size so the total file size approximates the requested target.
59
+ write_briefing_entry() {
60
+ local path="$1"
61
+ local target_bytes="$2"
62
+ : > "$path"
63
+ printf '# Topic\n\n' >> "$path"
64
+ local header_size=$(wc -c < "$path" | tr -d ' ')
65
+ local body_target=$(( target_bytes - header_size ))
66
+ if [ "$body_target" -gt 0 ]; then
67
+ # Repeated 80-byte line keeps things readable
68
+ local line="- entry text padded out to a known length for byte-budget testing. "
69
+ local line_size=${#line}
70
+ line+=$'\n'
71
+ local line_count=$(( (body_target + line_size) / (line_size + 1) ))
72
+ local i=0
73
+ while [ "$i" -lt "$line_count" ]; do
74
+ printf '%s' "$line" >> "$path"
75
+ i=$(( i + 1 ))
76
+ done
77
+ fi
78
+ }
79
+
80
+ # ── Existence + executable ──────────────────────────────────────────────────
81
+
82
+ @test "check-briefing-budgets: script exists" {
83
+ [ -f "$SCRIPT" ]
84
+ }
85
+
86
+ @test "check-briefing-budgets: script is executable" {
87
+ [ -x "$SCRIPT" ]
88
+ }
89
+
90
+ # ── Default-threshold behaviour (5120 bytes per ADR-040 Tier 3 ceiling) ─────
91
+
92
+ @test "check-briefing-budgets: empty briefing dir produces no output and exits 0" {
93
+ mkdir -p "$FIXTURE_DIR/briefing"
94
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
95
+ [ "$status" -eq 0 ]
96
+ [ -z "$output" ]
97
+ }
98
+
99
+ @test "check-briefing-budgets: all files under threshold produces no output" {
100
+ mkdir -p "$FIXTURE_DIR/briefing"
101
+ write_briefing_entry "$FIXTURE_DIR/briefing/small-topic.md" 1024
102
+ write_briefing_entry "$FIXTURE_DIR/briefing/medium-topic.md" 3000
103
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
104
+ [ "$status" -eq 0 ]
105
+ [ -z "$output" ]
106
+ }
107
+
108
+ @test "check-briefing-budgets: file over threshold emits OVER line with bytes + threshold" {
109
+ mkdir -p "$FIXTURE_DIR/briefing"
110
+ write_briefing_entry "$FIXTURE_DIR/briefing/bloated-topic.md" 10000
111
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
112
+ [ "$status" -eq 0 ]
113
+ echo "$output" | grep -E "^OVER bloated-topic.md bytes=[0-9]+ threshold=5120$"
114
+ }
115
+
116
+ @test "check-briefing-budgets: file exactly at threshold emits OVER (>= boundary)" {
117
+ mkdir -p "$FIXTURE_DIR/briefing"
118
+ # Create a file exactly 5120 bytes
119
+ printf '%.0s.' $(seq 1 5120) > "$FIXTURE_DIR/briefing/edge.md"
120
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
121
+ [ "$status" -eq 0 ]
122
+ echo "$output" | grep -E "^OVER edge.md bytes=5120 threshold=5120$"
123
+ }
124
+
125
+ @test "check-briefing-budgets: only over-threshold files appear in output" {
126
+ mkdir -p "$FIXTURE_DIR/briefing"
127
+ write_briefing_entry "$FIXTURE_DIR/briefing/under.md" 2000
128
+ write_briefing_entry "$FIXTURE_DIR/briefing/over-one.md" 8000
129
+ write_briefing_entry "$FIXTURE_DIR/briefing/over-two.md" 12000
130
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
131
+ [ "$status" -eq 0 ]
132
+ # Both over-files present
133
+ echo "$output" | grep -E "^OVER over-one.md bytes=[0-9]+ threshold=5120$"
134
+ echo "$output" | grep -E "^OVER over-two.md bytes=[0-9]+ threshold=5120$"
135
+ # Under-file absent
136
+ ! echo "$output" | grep -q "under.md"
137
+ }
138
+
139
+ @test "check-briefing-budgets: README.md is excluded from the scan (Tier 2 not Tier 3)" {
140
+ mkdir -p "$FIXTURE_DIR/briefing"
141
+ # Bloated README that would otherwise trip the threshold
142
+ write_briefing_entry "$FIXTURE_DIR/briefing/README.md" 20000
143
+ write_briefing_entry "$FIXTURE_DIR/briefing/topic.md" 3000
144
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
145
+ [ "$status" -eq 0 ]
146
+ [ -z "$output" ]
147
+ }
148
+
149
+ # ── Configurable threshold via env var ──────────────────────────────────────
150
+
151
+ @test "check-briefing-budgets: BRIEFING_TIER3_MAX_BYTES env var overrides default" {
152
+ mkdir -p "$FIXTURE_DIR/briefing"
153
+ write_briefing_entry "$FIXTURE_DIR/briefing/topic.md" 3000
154
+ # Default 5120: under threshold, no output. With env var set to 2000:
155
+ # over threshold, expect OVER line.
156
+ BRIEFING_TIER3_MAX_BYTES=2000 run "$SCRIPT" "$FIXTURE_DIR/briefing"
157
+ [ "$status" -eq 0 ]
158
+ echo "$output" | grep -E "^OVER topic.md bytes=[0-9]+ threshold=2000$"
159
+ }
160
+
161
+ @test "check-briefing-budgets: env var threshold of 0 emits every file (sanity)" {
162
+ mkdir -p "$FIXTURE_DIR/briefing"
163
+ write_briefing_entry "$FIXTURE_DIR/briefing/a.md" 100
164
+ write_briefing_entry "$FIXTURE_DIR/briefing/b.md" 200
165
+ BRIEFING_TIER3_MAX_BYTES=0 run "$SCRIPT" "$FIXTURE_DIR/briefing"
166
+ [ "$status" -eq 0 ]
167
+ echo "$output" | grep -q "OVER a.md "
168
+ echo "$output" | grep -q "OVER b.md "
169
+ }
170
+
171
+ # ── Argument and error handling ─────────────────────────────────────────────
172
+
173
+ @test "check-briefing-budgets: defaults to docs/briefing when no arg provided" {
174
+ cd "$FIXTURE_DIR"
175
+ mkdir -p docs/briefing
176
+ write_briefing_entry "docs/briefing/big.md" 10000
177
+ run "$SCRIPT"
178
+ [ "$status" -eq 0 ]
179
+ echo "$output" | grep -E "^OVER big.md bytes=[0-9]+ threshold=5120$"
180
+ }
181
+
182
+ @test "check-briefing-budgets: missing briefing dir exits 2 with parse error on stderr" {
183
+ run "$SCRIPT" "$FIXTURE_DIR/does-not-exist"
184
+ [ "$status" -eq 2 ]
185
+ echo "$output" | grep -iE "not found|missing|does not exist"
186
+ }
187
+
188
+ @test "check-briefing-budgets: ignores non-markdown files in the briefing dir" {
189
+ mkdir -p "$FIXTURE_DIR/briefing"
190
+ # Bloated non-markdown (e.g. a stray log) should not trip the scan
191
+ printf '%.0s.' $(seq 1 20000) > "$FIXTURE_DIR/briefing/stray.txt"
192
+ write_briefing_entry "$FIXTURE_DIR/briefing/topic.md" 1000
193
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
194
+ [ "$status" -eq 0 ]
195
+ [ -z "$output" ]
196
+ }
197
+
198
+ # ── Output stability ────────────────────────────────────────────────────────
199
+
200
+ @test "check-briefing-budgets: output is sorted by filename for stable diffs" {
201
+ mkdir -p "$FIXTURE_DIR/briefing"
202
+ write_briefing_entry "$FIXTURE_DIR/briefing/zebra.md" 8000
203
+ write_briefing_entry "$FIXTURE_DIR/briefing/alpha.md" 8000
204
+ write_briefing_entry "$FIXTURE_DIR/briefing/middle.md" 8000
205
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
206
+ [ "$status" -eq 0 ]
207
+ # First line is alpha, last is zebra
208
+ first_line=$(echo "$output" | head -1)
209
+ last_line=$(echo "$output" | tail -1)
210
+ echo "$first_line" | grep -q "alpha.md"
211
+ echo "$last_line" | grep -q "zebra.md"
212
+ }
213
+
214
+ # ── MUST_SPLIT signal (P145) ────────────────────────────────────────────────
215
+ #
216
+ # Files at ratio >= 2.0x the threshold also emit a MUST_SPLIT line that
217
+ # names the same basename and a `reason=` code. This promotes ADR-040's
218
+ # Tier 3 reassessment trigger ("≥ 3 topic files exceed 2× the configured
219
+ # ceiling for ≥ 2 consecutive retro cycles") from policy-revisit-time to
220
+ # per-cycle enforcement on the same threshold. The MUST_SPLIT line is
221
+ # the "no defer" signal: run-retro Step 3 Tier 3 silent-agent rotation
222
+ # is forced to pick split-by-subtopic / split-by-date for these files.
223
+ #
224
+ # Output format on >= 2x ratio (one line per file, in addition to the
225
+ # existing OVER line):
226
+ # MUST_SPLIT <basename> reason=<code>
227
+ #
228
+ # @problem P145
229
+
230
+ @test "check-briefing-budgets: file at exactly 2x threshold emits MUST_SPLIT" {
231
+ mkdir -p "$FIXTURE_DIR/briefing"
232
+ # Exactly 10240 bytes = 2.0x of 5120 default threshold
233
+ printf '%.0s.' $(seq 1 10240) > "$FIXTURE_DIR/briefing/exactly-2x.md"
234
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
235
+ [ "$status" -eq 0 ]
236
+ echo "$output" | grep -E "^MUST_SPLIT exactly-2x.md reason=ratio-exceeds-2x$"
237
+ }
238
+
239
+ @test "check-briefing-budgets: file just under 2x does NOT emit MUST_SPLIT" {
240
+ mkdir -p "$FIXTURE_DIR/briefing"
241
+ # 10239 bytes = 1.9998x of 5120 — under the 2.0x trigger
242
+ printf '%.0s.' $(seq 1 10239) > "$FIXTURE_DIR/briefing/just-under-2x.md"
243
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
244
+ [ "$status" -eq 0 ]
245
+ # OVER line still fires (>= threshold)
246
+ echo "$output" | grep -E "^OVER just-under-2x.md bytes=10239 threshold=5120"
247
+ # MUST_SPLIT does NOT fire (< 2x ratio)
248
+ ! echo "$output" | grep -q "MUST_SPLIT"
249
+ }
250
+
251
+ @test "check-briefing-budgets: file well over 2x emits both OVER and MUST_SPLIT" {
252
+ mkdir -p "$FIXTURE_DIR/briefing"
253
+ # 4.0x ceiling — mirrors today's afk-subprocess.md state
254
+ write_briefing_entry "$FIXTURE_DIR/briefing/very-bloated.md" 20480
255
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
256
+ [ "$status" -eq 0 ]
257
+ echo "$output" | grep -E "^OVER very-bloated.md bytes=[0-9]+ threshold=5120"
258
+ echo "$output" | grep -E "^MUST_SPLIT very-bloated.md reason=ratio-exceeds-2x$"
259
+ }
260
+
261
+ @test "check-briefing-budgets: file under threshold emits NEITHER OVER nor MUST_SPLIT" {
262
+ mkdir -p "$FIXTURE_DIR/briefing"
263
+ write_briefing_entry "$FIXTURE_DIR/briefing/small.md" 4096
264
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
265
+ [ "$status" -eq 0 ]
266
+ ! echo "$output" | grep -q "small.md"
267
+ ! echo "$output" | grep -q "MUST_SPLIT"
268
+ }
269
+
270
+ @test "check-briefing-budgets: BRIEFING_TIER3_MAX_BYTES env override flows through to MUST_SPLIT" {
271
+ mkdir -p "$FIXTURE_DIR/briefing"
272
+ # 4096 bytes is 2.0x of 2048 — should trigger MUST_SPLIT under the override
273
+ write_briefing_entry "$FIXTURE_DIR/briefing/topic.md" 4096
274
+ BRIEFING_TIER3_MAX_BYTES=2048 run "$SCRIPT" "$FIXTURE_DIR/briefing"
275
+ [ "$status" -eq 0 ]
276
+ echo "$output" | grep -E "^OVER topic.md bytes=[0-9]+ threshold=2048"
277
+ echo "$output" | grep -E "^MUST_SPLIT topic.md reason=ratio-exceeds-2x$"
278
+ }
279
+
280
+ @test "check-briefing-budgets: mixed OVER + MUST_SPLIT output is sorted deterministically" {
281
+ mkdir -p "$FIXTURE_DIR/briefing"
282
+ # Three OVER files; two of them also MUST_SPLIT. Output must be
283
+ # deterministic so retro summary diffs stay stable across cycles.
284
+ # Contract: OVER block (sorted by basename) followed by MUST_SPLIT
285
+ # block (sorted by basename).
286
+ write_briefing_entry "$FIXTURE_DIR/briefing/zebra-over-only.md" 6000
287
+ write_briefing_entry "$FIXTURE_DIR/briefing/alpha-must-split.md" 12000
288
+ write_briefing_entry "$FIXTURE_DIR/briefing/middle-must-split.md" 15000
289
+ run "$SCRIPT" "$FIXTURE_DIR/briefing"
290
+ [ "$status" -eq 0 ]
291
+ # Three OVER lines — alpha first, middle next, zebra last
292
+ over_lines=$(echo "$output" | grep "^OVER ")
293
+ [ "$(echo "$over_lines" | wc -l | tr -d ' ')" = "3" ]
294
+ echo "$over_lines" | sed -n '1p' | grep -q "alpha-must-split.md"
295
+ echo "$over_lines" | sed -n '2p' | grep -q "middle-must-split.md"
296
+ echo "$over_lines" | sed -n '3p' | grep -q "zebra-over-only.md"
297
+ # Two MUST_SPLIT lines — alpha first, middle second; zebra-over-only NOT present
298
+ must_lines=$(echo "$output" | grep "^MUST_SPLIT ")
299
+ [ "$(echo "$must_lines" | wc -l | tr -d ' ')" = "2" ]
300
+ echo "$must_lines" | sed -n '1p' | grep -q "alpha-must-split.md"
301
+ echo "$must_lines" | sed -n '2p' | grep -q "middle-must-split.md"
302
+ ! echo "$must_lines" | grep -q "zebra-over-only.md"
303
+ # All OVER lines come before any MUST_SPLIT line (block ordering)
304
+ first_must_line_no=$(echo "$output" | grep -n "^MUST_SPLIT " | head -1 | cut -d: -f1)
305
+ last_over_line_no=$(echo "$output" | grep -n "^OVER " | tail -1 | cut -d: -f1)
306
+ [ "$last_over_line_no" -lt "$first_must_line_no" ]
307
+ }
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Behavioural fixture for the internal-ID-leak advisory detector — per
4
+ # ADR-055 (Plugin-published artefacts use namespace-prefixed permalinks).
5
+ #
6
+ # Contract: `check-internal-id-leaks.sh [<root-dir>]` is a diagnose-only
7
+ # advisory script. It walks shipped-artefact surfaces under
8
+ # `<root-dir>/packages/*/` (default `<root-dir>` is `.`) and reports
9
+ # bare internal-ID tokens that lack the `WR-` namespace prefix.
10
+ #
11
+ # Surfaces scanned:
12
+ # - packages/<plugin>/skills/<skill>/SKILL.md
13
+ # - packages/<plugin>/agents/*.md
14
+ # - packages/<plugin>/hooks/*.sh
15
+ # - packages/<plugin>/CHANGELOG.md
16
+ #
17
+ # Bare tokens flagged (regex):
18
+ # ADR-NNN (3+ digits)
19
+ # JTBD-NNN (3+ digits)
20
+ # P-NNN or PNNN (3 digits — problem ticket form)
21
+ #
22
+ # Tokens that DO NOT trigger:
23
+ # WR-ADR-NNN, WR-JTBD-NNN, WR-P-NNN, WR-PNNN (namespace-prefixed)
24
+ # docstring annotation lines beginning with `# @adr` / `# @jtbd` /
25
+ # `# @problem` (maintainer-facing, never expanded into adopter context)
26
+ # REFERENCE.md sibling files (lazy-loaded, maintainer-facing per ADR-054)
27
+ #
28
+ # Exit codes:
29
+ # 0 = always (advisory only — drift is signal, not failure)
30
+ # 2 = parse error (root dir missing or unreadable)
31
+ #
32
+ # Output format on drift (terse machine-readable per ADR-038):
33
+ # OVER <plugin>/<file> bare_count=<N>
34
+ #
35
+ # Followed by a final summary line:
36
+ # TOTAL packages=<N> with_leaks=<M> drift_instances=<K>
37
+ #
38
+ # Output is empty (no lines) when no shipped artefact carries bare tokens.
39
+ # Silent-on-pass per ADR-045 hook injection budget discipline.
40
+ #
41
+ # Read-only — does NOT mutate any artefact. Per ADR-052, this fixture is
42
+ # BEHAVIOURAL — it asserts script output on temp-fixture trees, NOT
43
+ # script source content. No greps of check-internal-id-leaks.sh source.
44
+ #
45
+ # @problem P137 (Plugin-published artefacts reference internal IDs that
46
+ # adopter projects can't resolve)
47
+ # @adr ADR-055 (Plugin-published artefacts use namespace-prefixed
48
+ # permalinks — strategy + advisory detector)
49
+ # @adr ADR-038 (Progressive disclosure — terse machine-readable signal)
50
+ # @adr ADR-045 (Hook injection budget — silent-on-pass)
51
+ # @adr ADR-052 (Behavioural-tests-default — fixture pattern)
52
+ # @adr ADR-005 (Plugin testing strategy)
53
+ # @jtbd JTBD-302 (Trust That the README Describes the Plugin I Just
54
+ # Installed — semantic correctness axis of adopter-facing content)
55
+
56
+ setup() {
57
+ SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
58
+ SCRIPT="$SCRIPTS_DIR/check-internal-id-leaks.sh"
59
+ FIXTURE_ROOT="$(mktemp -d)"
60
+ }
61
+
62
+ teardown() {
63
+ rm -rf "$FIXTURE_ROOT"
64
+ }
65
+
66
+ # Helper: write a SKILL.md with given body content under fixture plugin.
67
+ # Uses %b to interpret \n in the body argument as a real newline so test
68
+ # fixtures can compose multi-line content inline.
69
+ write_skill() {
70
+ local plugin="$1"
71
+ local skill="$2"
72
+ local body="$3"
73
+ local skill_dir="$FIXTURE_ROOT/packages/$plugin/skills/$skill"
74
+ mkdir -p "$skill_dir"
75
+ printf '%b\n' "$body" > "$skill_dir/SKILL.md"
76
+ }
77
+
78
+ # Helper: write an agent file with given body.
79
+ write_agent() {
80
+ local plugin="$1"
81
+ local agent="$2"
82
+ local body="$3"
83
+ local agent_dir="$FIXTURE_ROOT/packages/$plugin/agents"
84
+ mkdir -p "$agent_dir"
85
+ printf '%b\n' "$body" > "$agent_dir/$agent.md"
86
+ }
87
+
88
+ # Helper: write a hook script with given body.
89
+ write_hook() {
90
+ local plugin="$1"
91
+ local hook="$2"
92
+ local body="$3"
93
+ local hook_dir="$FIXTURE_ROOT/packages/$plugin/hooks"
94
+ mkdir -p "$hook_dir"
95
+ printf '%b\n' "$body" > "$hook_dir/$hook.sh"
96
+ }
97
+
98
+ # Helper: write a CHANGELOG.md with given body.
99
+ write_changelog() {
100
+ local plugin="$1"
101
+ local body="$2"
102
+ local plugin_dir="$FIXTURE_ROOT/packages/$plugin"
103
+ mkdir -p "$plugin_dir"
104
+ printf '%b\n' "$body" > "$plugin_dir/CHANGELOG.md"
105
+ }
106
+
107
+ # ── Existence + executable ──────────────────────────────────────────────────
108
+
109
+ @test "check-internal-id-leaks: script exists" {
110
+ [ -f "$SCRIPT" ]
111
+ }
112
+
113
+ @test "check-internal-id-leaks: script is executable" {
114
+ [ -x "$SCRIPT" ]
115
+ }
116
+
117
+ # ── Empty / clean trees ─────────────────────────────────────────────────────
118
+
119
+ @test "check-internal-id-leaks: empty tree produces no output and exits 0" {
120
+ mkdir -p "$FIXTURE_ROOT/packages"
121
+ run "$SCRIPT" "$FIXTURE_ROOT"
122
+ [ "$status" -eq 0 ]
123
+ [ -z "$output" ]
124
+ }
125
+
126
+ @test "check-internal-id-leaks: clean SKILL.md (no IDs at all) produces no output" {
127
+ write_skill "alpha" "clean" "# Skill\n\nThis skill has no references at all."
128
+ run "$SCRIPT" "$FIXTURE_ROOT"
129
+ [ "$status" -eq 0 ]
130
+ [ -z "$output" ]
131
+ }
132
+
133
+ @test "check-internal-id-leaks: SKILL.md with WR-prefixed refs only produces no output" {
134
+ write_skill "alpha" "wr-only" "# Skill\n\nPer WR-ADR-014 and WR-JTBD-101 and WR-P137."
135
+ run "$SCRIPT" "$FIXTURE_ROOT"
136
+ [ "$status" -eq 0 ]
137
+ [ -z "$output" ]
138
+ }
139
+
140
+ # ── Bare-ID detection across surfaces ───────────────────────────────────────
141
+
142
+ @test "check-internal-id-leaks: bare ADR-NNN in SKILL.md is flagged" {
143
+ write_skill "alpha" "leaky" "# Skill\n\nPer ADR-014 the workflow is..."
144
+ run "$SCRIPT" "$FIXTURE_ROOT"
145
+ [ "$status" -eq 0 ]
146
+ echo "$output" | grep -E "^OVER alpha/skills/leaky/SKILL.md bare_count=[0-9]+"
147
+ }
148
+
149
+ @test "check-internal-id-leaks: bare JTBD-NNN in SKILL.md is flagged" {
150
+ write_skill "alpha" "leaky" "# Skill\n\nServes JTBD-101 and JTBD-302."
151
+ run "$SCRIPT" "$FIXTURE_ROOT"
152
+ [ "$status" -eq 0 ]
153
+ echo "$output" | grep -E "^OVER alpha/skills/leaky/SKILL.md bare_count=[0-9]+"
154
+ }
155
+
156
+ @test "check-internal-id-leaks: bare P-NNN in SKILL.md is flagged" {
157
+ write_skill "alpha" "leaky" "# Skill\n\nCloses P137 and P078."
158
+ run "$SCRIPT" "$FIXTURE_ROOT"
159
+ [ "$status" -eq 0 ]
160
+ echo "$output" | grep -E "^OVER alpha/skills/leaky/SKILL.md bare_count=[0-9]+"
161
+ }
162
+
163
+ @test "check-internal-id-leaks: bare IDs in agent file are flagged" {
164
+ write_agent "beta" "specialist" "# Agent\n\nPer ADR-013 Rule 6 escalate."
165
+ run "$SCRIPT" "$FIXTURE_ROOT"
166
+ [ "$status" -eq 0 ]
167
+ echo "$output" | grep -E "^OVER beta/agents/specialist.md bare_count=[0-9]+"
168
+ }
169
+
170
+ @test "check-internal-id-leaks: bare IDs in hook script body are flagged" {
171
+ write_hook "gamma" "guard" "#!/usr/bin/env bash\n# This deny message points at ADR-014 and P137 from prose."
172
+ run "$SCRIPT" "$FIXTURE_ROOT"
173
+ [ "$status" -eq 0 ]
174
+ echo "$output" | grep -E "^OVER gamma/hooks/guard.sh bare_count=[0-9]+"
175
+ }
176
+
177
+ @test "check-internal-id-leaks: bare IDs in CHANGELOG.md are flagged" {
178
+ write_changelog "delta" "## 0.1.0\n\n- Per ADR-014 + P081 the new behaviour ships."
179
+ run "$SCRIPT" "$FIXTURE_ROOT"
180
+ [ "$status" -eq 0 ]
181
+ echo "$output" | grep -E "^OVER delta/CHANGELOG.md bare_count=[0-9]+"
182
+ }
183
+
184
+ # ── Exclusions ──────────────────────────────────────────────────────────────
185
+
186
+ @test "check-internal-id-leaks: docstring @adr annotations on hook lines are NOT flagged" {
187
+ write_hook "alpha" "annotated" "#!/usr/bin/env bash\n# @adr ADR-014 (commit discipline)\n# @jtbd JTBD-101\n# @problem P137"
188
+ run "$SCRIPT" "$FIXTURE_ROOT"
189
+ [ "$status" -eq 0 ]
190
+ [ -z "$output" ]
191
+ }
192
+
193
+ @test "check-internal-id-leaks: REFERENCE.md sibling files are NOT scanned" {
194
+ local skill_dir="$FIXTURE_ROOT/packages/alpha/skills/with-ref"
195
+ mkdir -p "$skill_dir"
196
+ printf '# Skill\nClean body.\n' > "$skill_dir/SKILL.md"
197
+ printf '# Reference\nADR-014 is fine here.\n' > "$skill_dir/REFERENCE.md"
198
+ run "$SCRIPT" "$FIXTURE_ROOT"
199
+ [ "$status" -eq 0 ]
200
+ [ -z "$output" ]
201
+ }
202
+
203
+ # ── Counting + summary ──────────────────────────────────────────────────────
204
+
205
+ @test "check-internal-id-leaks: bare_count matches number of bare tokens in file" {
206
+ write_skill "alpha" "trio" "# Skill\n\nADR-014 and JTBD-101 and P137 — three bare tokens."
207
+ run "$SCRIPT" "$FIXTURE_ROOT"
208
+ [ "$status" -eq 0 ]
209
+ echo "$output" | grep -E "^OVER alpha/skills/trio/SKILL.md bare_count=3$"
210
+ }
211
+
212
+ @test "check-internal-id-leaks: TOTAL summary line emitted on any drift" {
213
+ write_skill "alpha" "leaky" "Per ADR-014 the workflow is."
214
+ run "$SCRIPT" "$FIXTURE_ROOT"
215
+ [ "$status" -eq 0 ]
216
+ echo "$output" | grep -E "^TOTAL packages=1 with_leaks=1 drift_instances=1$"
217
+ }
218
+
219
+ @test "check-internal-id-leaks: TOTAL summary aggregates across files + packages" {
220
+ write_skill "alpha" "leaky" "Per ADR-014."
221
+ write_skill "alpha" "another" "JTBD-101."
222
+ write_agent "beta" "specialist" "P137."
223
+ run "$SCRIPT" "$FIXTURE_ROOT"
224
+ [ "$status" -eq 0 ]
225
+ echo "$output" | grep -E "^TOTAL packages=2 with_leaks=3 drift_instances=3$"
226
+ }
227
+
228
+ @test "check-internal-id-leaks: no TOTAL line emitted when output is empty" {
229
+ write_skill "alpha" "clean" "No bare references here."
230
+ run "$SCRIPT" "$FIXTURE_ROOT"
231
+ [ "$status" -eq 0 ]
232
+ [ -z "$output" ]
233
+ }
234
+
235
+ # ── Determinism ─────────────────────────────────────────────────────────────
236
+
237
+ @test "check-internal-id-leaks: OVER lines are sorted by package/file identifier" {
238
+ write_skill "zeta" "z-skill" "ADR-014."
239
+ write_skill "alpha" "a-skill" "ADR-014."
240
+ write_skill "mu" "m-skill" "ADR-014."
241
+ run "$SCRIPT" "$FIXTURE_ROOT"
242
+ [ "$status" -eq 0 ]
243
+ local first
244
+ first=$(echo "$output" | grep '^OVER' | head -1)
245
+ echo "$first" | grep -q "alpha/skills/a-skill/SKILL.md"
246
+ local last
247
+ last=$(echo "$output" | grep '^OVER' | tail -1)
248
+ echo "$last" | grep -q "zeta/skills/z-skill/SKILL.md"
249
+ }
250
+
251
+ # ── Pre-check error path ────────────────────────────────────────────────────
252
+
253
+ @test "check-internal-id-leaks: missing root dir exits 2" {
254
+ run "$SCRIPT" "/nonexistent/path/$$"
255
+ [ "$status" -eq 2 ]
256
+ }
257
+
258
+ # ── Boundary tokens that must NOT match ─────────────────────────────────────
259
+
260
+ @test "check-internal-id-leaks: WR-prefixed token mid-sentence does not flag" {
261
+ write_skill "alpha" "wr-mid" "Per WR-ADR-014 — clean."
262
+ run "$SCRIPT" "$FIXTURE_ROOT"
263
+ [ "$status" -eq 0 ]
264
+ [ -z "$output" ]
265
+ }
266
+
267
+ @test "check-internal-id-leaks: WR-prefixed JTBD does not flag" {
268
+ write_skill "alpha" "wr-jtbd" "Serves WR-JTBD-302."
269
+ run "$SCRIPT" "$FIXTURE_ROOT"
270
+ [ "$status" -eq 0 ]
271
+ [ -z "$output" ]
272
+ }
273
+
274
+ @test "check-internal-id-leaks: standalone P3 (less than 3 digits) does not flag" {
275
+ write_skill "alpha" "edge" "Phase P3 of the rollout."
276
+ run "$SCRIPT" "$FIXTURE_ROOT"
277
+ [ "$status" -eq 0 ]
278
+ [ -z "$output" ]
279
+ }
280
+
281
+ @test "check-internal-id-leaks: lowercase adr-014 does not flag (case-sensitive)" {
282
+ write_skill "alpha" "lower" "in adr-014 prose context."
283
+ run "$SCRIPT" "$FIXTURE_ROOT"
284
+ [ "$status" -eq 0 ]
285
+ [ -z "$output" ]
286
+ }