@thebassclef/lite 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cli.cjs +51 -7
  2. package/dist/cli.js +51 -7
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/lite/.claude/hooks/session-reflection.d/00-bassclef-hook-connect.sh +63 -0
  7. package/dist/lite/.claude/hooks/session-reflection.d/05-active-bet.sh +113 -0
  8. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +62 -0
  9. package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +78 -0
  10. package/dist/lite/.claude/hooks/session-reflection.d/12-session-lock.sh +63 -0
  11. package/dist/lite/.claude/hooks/session-reflection.d/15-substrate-config-validate.sh +91 -0
  12. package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +98 -0
  13. package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +61 -0
  14. package/dist/lite/.claude/hooks/session-reflection.d/35-post-merge-whereami-self-heal.sh +180 -0
  15. package/dist/lite/.claude/hooks/session-reflection.d/36-whereami-size-check.sh +117 -0
  16. package/dist/lite/.claude/hooks/session-reflection.d/40-gate-compliance.sh +108 -0
  17. package/dist/lite/.claude/hooks/session-reflection.d/45-auto-sync-status.sh +99 -0
  18. package/dist/lite/.claude/hooks/session-reflection.d/50-evolution-issues.sh +44 -0
  19. package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +187 -0
  20. package/dist/lite/.claude/hooks/session-reflection.d/56-roadmap-reconcile-gate.sh +88 -0
  21. package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +212 -0
  22. package/dist/lite/.claude/hooks/session-reflection.d/70-recent-strategic-artifacts.sh +101 -0
  23. package/dist/lite/.claude/hooks/session-reflection.d/75-prior-r8-findings.sh +100 -0
  24. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +87 -0
  25. package/dist/lite/.claude/hooks/session-reflection.d/80-workflow-staleness.sh +141 -0
  26. package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +67 -0
  27. package/dist/lite/.claude/hooks/session-reflection.d/90-release-backlog.sh +140 -0
  28. package/dist/lite/.claude/hooks/session-reflection.d/90-secrets-expiry.sh +104 -0
  29. package/dist/lite/.claude/hooks/session-reflection.d/95-settings-hook-verify.sh +131 -0
  30. package/dist/lite/.claude/hooks/session-reflection.d/95-telemetry-emit.sh +66 -0
  31. package/dist/lite/.claude/hooks/session-reflection.d/97-install-class-dispatch.sh +57 -0
  32. package/dist/lite/.claude/hooks/session-reflection.d/tests/00-bassclef-hook-connect.test.sh +492 -0
  33. package/dist/lite/.claude/hooks/session-reflection.d/tests/05-active-bet.test.sh +336 -0
  34. package/dist/lite/.claude/hooks/session-reflection.d/tests/12-session-lock.test.sh +216 -0
  35. package/dist/lite/.claude/hooks/session-reflection.d/tests/15-substrate-config-validate.test.sh +50 -0
  36. package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +348 -0
  37. package/dist/lite/.claude/hooks/session-reflection.d/tests/36-whereami-size-check.test.sh +189 -0
  38. package/dist/lite/.claude/hooks/session-reflection.d/tests/45-auto-sync-status.test.sh +263 -0
  39. package/dist/lite/.claude/hooks/session-reflection.d/tests/55-orientation-gate.test.sh +595 -0
  40. package/dist/lite/.claude/hooks/session-reflection.d/tests/56-roadmap-reconcile-gate.test.sh +227 -0
  41. package/dist/lite/.claude/hooks/session-reflection.d/tests/75-prior-r8-findings.test.sh +142 -0
  42. package/dist/lite/.claude/hooks/session-reflection.d/tests/90-release-backlog.test.sh +44 -0
  43. package/dist/lite/.claude/hooks/trace-helper.sh +22 -0
  44. package/package.json +1 -1
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env bash
2
+ # tier: standard
3
+ # Tests for .claude/hooks/session-reflection.d/20-artifact-staleness.sh
4
+ #
5
+ # Per bassclef-upstream#1438: cure chronicle-freshness false-positive on
6
+ # cross-day marathon sessions. Marathon sessions land amendments on the
7
+ # prior-day chronicle instead of creating a new same-day file. Current
8
+ # hook (L9) reads filename date only; amendment headers are invisible.
9
+ #
10
+ # Path A cure: extend hook to also scan chronicle content for latest
11
+ # `^## Amendment YYYY-MM-DD` header. LAST_CHRONICLE_DATE becomes
12
+ # MAX(filename-date, latest-amendment-date). TODAY_CHRONICLE check
13
+ # extends to: file has today filename OR contains `## Amendment $TODAY`
14
+ # header.
15
+ #
16
+ # Tier 0 strict-TDD per .claude/rules/testing-tier-config.md.
17
+ # Test list per .claude/rules/test-list-discipline.md.
18
+ # Sufficiency criteria per .claude/rules/test-sufficiency.md.
19
+ #
20
+ # Run: bash .claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh
21
+
22
+ set -u
23
+
24
+ # test-list:
25
+ # Class A — fresh chronicle (should be SILENT):
26
+ # [x] Same-day filename chronicle exists → silent (original behavior preserved)
27
+ # [x] Prior-day filename with today amendment header → silent (Path A cure)
28
+ # [x] Prior-day filename with multiple amendments including today → silent (Path A cure)
29
+ # [x] Same-day filename AND prior chronicle with amendment → silent
30
+ # Class B — stale chronicle (should BLOCK):
31
+ # [x] Prior-day filename + NO amendment headers + >=3 commits → BLOCK (original class preserved)
32
+ # [x] Prior-day filename + amendment for OLD date (not today) + >=3 commits → BLOCK (cure does not over-mask)
33
+ # [x] No chronicle files + >=3 commits → silent (current hook is silent here; do NOT change)
34
+ # Class C — edge cases:
35
+ # [x] Chronicle exists + <3 commits since → silent (threshold behavior preserved)
36
+ # [x] Chronicle dir does not exist → silent (defensive)
37
+ # [x] Amendment header with malformed date (BADDATE) → falls back to filename (defensive)
38
+ # [x] Amendment header with future date → treated as fresh (defensive; no crash)
39
+ # [x] Multiple amendment lines; latest wins by date-sort not file-order
40
+ # [x] Trap: no tmpfiles created in hook; no trap needed. Reason: hook is read-only on filesystem.
41
+ # [x] Override: no SKIP_ARTIFACT_STALENESS override today; test asserts absence. Reason: existing hook has no override; extension does not add one.
42
+
43
+ REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
44
+ HOOK="${REPO_ROOT}/.claude/hooks/session-reflection.d/20-artifact-staleness.sh"
45
+ PASS=0
46
+ FAIL=0
47
+ FAILED_TESTS=()
48
+
49
+ assert() {
50
+ local name="$1"
51
+ local result="$2"
52
+ if [ "$result" = "ok" ]; then
53
+ PASS=$((PASS + 1))
54
+ echo " PASS $name"
55
+ else
56
+ FAIL=$((FAIL + 1))
57
+ FAILED_TESTS+=("$name")
58
+ echo " FAIL $name — $result"
59
+ fi
60
+ }
61
+
62
+ setup_tmpdir() {
63
+ TMPDIR_TEST="$(mktemp -d)"
64
+ mkdir -p "$TMPDIR_TEST/chronicle"
65
+ # Backdate init commit to 2020-01-01 so it never falls inside test
66
+ # `--since` windows (tests use 2026-* dates). Otherwise init counts
67
+ # as a spurious "commit since chronicle" and breaks threshold tests.
68
+ (cd "$TMPDIR_TEST" \
69
+ && git init -q \
70
+ && git config user.email t@t \
71
+ && git config user.name t \
72
+ && touch README.md \
73
+ && git add README.md \
74
+ && GIT_AUTHOR_DATE="2020-01-01 12:00:00" \
75
+ GIT_COMMITTER_DATE="2020-01-01 12:00:00" \
76
+ git commit -q -m init 2>/dev/null) || true
77
+ }
78
+
79
+ teardown_tmpdir() {
80
+ rm -rf "$TMPDIR_TEST"
81
+ }
82
+
83
+ # Add N extra commits backdated so `git log --since=<date>` counts them.
84
+ add_commits_since() {
85
+ local base_date="$1"
86
+ local count="$2"
87
+ local i=0
88
+ while [ "$i" -lt "$count" ]; do
89
+ i=$((i + 1))
90
+ (cd "$TMPDIR_TEST" \
91
+ && echo "content-$i-$RANDOM" > "file-$i.txt" \
92
+ && git add "file-$i.txt" \
93
+ && GIT_AUTHOR_DATE="$base_date 12:00:00" \
94
+ GIT_COMMITTER_DATE="$base_date 12:00:00" \
95
+ git commit -q -m "commit-$i" 2>/dev/null) || true
96
+ done
97
+ }
98
+
99
+ # Invoke hook via source with blocked_banner stubbed. Inject a `date`
100
+ # stub so TODAY is controllable per test.
101
+ run_hook() {
102
+ local today_override="${1:-}"
103
+ if [ -n "$today_override" ]; then
104
+ cat > "$TMPDIR_TEST/date" <<EOF
105
+ #!/usr/bin/env bash
106
+ if [ "\$1" = "+%Y-%m-%d" ]; then
107
+ echo "$today_override"
108
+ exit 0
109
+ fi
110
+ exec /bin/date "\$@"
111
+ EOF
112
+ chmod +x "$TMPDIR_TEST/date"
113
+ PATH_PREFIX="$TMPDIR_TEST:$PATH"
114
+ else
115
+ PATH_PREFIX="$PATH"
116
+ fi
117
+
118
+ (
119
+ cd "$TMPDIR_TEST" || return 1
120
+ export PATH="$PATH_PREFIX"
121
+ bash -c "blocked_banner() { echo \"🛑 BLOCKED: \$1\"; }; source '$HOOK'" 2>&1
122
+ )
123
+ }
124
+
125
+ # =======================================================================
126
+ # Class A — fresh chronicle (should be SILENT)
127
+ # =======================================================================
128
+
129
+ echo ""
130
+ echo "Class A — fresh chronicle (should be SILENT)"
131
+ echo ""
132
+
133
+ # A1: Same-day filename chronicle → silent
134
+ setup_tmpdir
135
+ touch "$TMPDIR_TEST/chronicle/2026-08-30-todays-work.md"
136
+ add_commits_since "2026-08-30" 5
137
+ OUTPUT=$(run_hook "2026-08-30")
138
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
139
+ assert "A1: same-day filename chronicle → silent" "output=$OUTPUT"
140
+ else
141
+ assert "A1: same-day filename chronicle → silent" "ok"
142
+ fi
143
+ teardown_tmpdir
144
+
145
+ # A2: Prior-day filename with today amendment header → silent (Path A)
146
+ setup_tmpdir
147
+ cat > "$TMPDIR_TEST/chronicle/2026-08-29c-marathon.md" <<'EOF'
148
+ # 2026-08-29c chronicle
149
+
150
+ Body.
151
+
152
+ ## Amendment 2026-08-30 — post-close cures landed
153
+
154
+ More work today.
155
+ EOF
156
+ add_commits_since "2026-08-30" 5
157
+ OUTPUT=$(run_hook "2026-08-30")
158
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
159
+ assert "A2: prior-day + today amendment → silent (Path A)" "STILL BLOCKED: $OUTPUT"
160
+ else
161
+ assert "A2: prior-day + today amendment → silent (Path A)" "ok"
162
+ fi
163
+ teardown_tmpdir
164
+
165
+ # A3: Multiple amendments; latest is today → silent (Path A)
166
+ setup_tmpdir
167
+ cat > "$TMPDIR_TEST/chronicle/2026-08-27a-earlier.md" <<'EOF'
168
+ # earlier chronicle
169
+
170
+ ## Amendment 2026-08-28 — day 2
171
+ ## Amendment 2026-08-29 — day 3
172
+ ## Amendment 2026-08-30 — today
173
+ EOF
174
+ add_commits_since "2026-08-30" 10
175
+ OUTPUT=$(run_hook "2026-08-30")
176
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
177
+ assert "A3: multi-amendment latest is today → silent (Path A)" "STILL BLOCKED: $OUTPUT"
178
+ else
179
+ assert "A3: multi-amendment latest is today → silent (Path A)" "ok"
180
+ fi
181
+ teardown_tmpdir
182
+
183
+ # A4: Same-day filename AND prior chronicle with amendment → silent
184
+ setup_tmpdir
185
+ touch "$TMPDIR_TEST/chronicle/2026-08-30-fresh.md"
186
+ cat > "$TMPDIR_TEST/chronicle/2026-08-29c-marathon.md" <<'EOF'
187
+ # 2026-08-29c
188
+ ## Amendment 2026-08-30 — some
189
+ EOF
190
+ add_commits_since "2026-08-30" 5
191
+ OUTPUT=$(run_hook "2026-08-30")
192
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
193
+ assert "A4: same-day + prior with amendment → silent" "output=$OUTPUT"
194
+ else
195
+ assert "A4: same-day + prior with amendment → silent" "ok"
196
+ fi
197
+ teardown_tmpdir
198
+
199
+ # =======================================================================
200
+ # Class B — stale chronicle (should BLOCK)
201
+ # =======================================================================
202
+
203
+ echo ""
204
+ echo "Class B — stale chronicle (should BLOCK)"
205
+ echo ""
206
+
207
+ # B1: Prior-day filename + NO amendment headers + >=3 commits → BLOCK
208
+ setup_tmpdir
209
+ cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
210
+ # old chronicle
211
+
212
+ Just body content. No amendments.
213
+ EOF
214
+ add_commits_since "2026-08-30" 5
215
+ OUTPUT=$(run_hook "2026-08-30")
216
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
217
+ assert "B1: prior-day + no amendments + 5 commits → BLOCK" "ok"
218
+ else
219
+ assert "B1: prior-day + no amendments + 5 commits → BLOCK" "SILENT: $OUTPUT"
220
+ fi
221
+ teardown_tmpdir
222
+
223
+ # B2: Prior-day filename + amendment for OLD date → BLOCK (no over-mask)
224
+ setup_tmpdir
225
+ cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
226
+ # old chronicle
227
+
228
+ ## Amendment 2026-08-26 — day 2 of that session
229
+
230
+ No newer amendments.
231
+ EOF
232
+ add_commits_since "2026-08-30" 5
233
+ OUTPUT=$(run_hook "2026-08-30")
234
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
235
+ assert "B2: prior-day + old amendment + 5 commits → BLOCK (no over-mask)" "ok"
236
+ else
237
+ assert "B2: prior-day + old amendment + 5 commits → BLOCK (no over-mask)" "SILENT: $OUTPUT"
238
+ fi
239
+ teardown_tmpdir
240
+
241
+ # B3: No chronicle files at all → silent (existing behavior; preserve)
242
+ setup_tmpdir
243
+ add_commits_since "2026-08-30" 5
244
+ OUTPUT=$(run_hook "2026-08-30")
245
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
246
+ assert "B3: no chronicles at all → silent (existing behavior)" "FIRED: $OUTPUT"
247
+ else
248
+ assert "B3: no chronicles at all → silent (existing behavior)" "ok"
249
+ fi
250
+ teardown_tmpdir
251
+
252
+ # =======================================================================
253
+ # Class C — edge cases
254
+ # =======================================================================
255
+
256
+ echo ""
257
+ echo "Class C — edge cases"
258
+ echo ""
259
+
260
+ # C1: <3 commits since → silent (threshold)
261
+ setup_tmpdir
262
+ cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
263
+ # old
264
+ EOF
265
+ add_commits_since "2026-08-30" 2
266
+ OUTPUT=$(run_hook "2026-08-30")
267
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
268
+ assert "C1: <3 commits since → silent (threshold)" "FIRED: $OUTPUT"
269
+ else
270
+ assert "C1: <3 commits since → silent (threshold)" "ok"
271
+ fi
272
+ teardown_tmpdir
273
+
274
+ # C2: Chronicle dir missing → silent (defensive)
275
+ setup_tmpdir
276
+ rm -rf "$TMPDIR_TEST/chronicle"
277
+ add_commits_since "2026-08-30" 5
278
+ OUTPUT=$(run_hook "2026-08-30")
279
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
280
+ assert "C2: chronicle dir missing → silent" "FIRED: $OUTPUT"
281
+ else
282
+ assert "C2: chronicle dir missing → silent" "ok"
283
+ fi
284
+ teardown_tmpdir
285
+
286
+ # C3: Malformed amendment date → falls back to filename (BLOCK)
287
+ setup_tmpdir
288
+ cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
289
+ # old
290
+ ## Amendment BADDATE — not a real date
291
+ EOF
292
+ add_commits_since "2026-08-30" 5
293
+ OUTPUT=$(run_hook "2026-08-30")
294
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
295
+ assert "C3: malformed amendment date → falls back to filename (BLOCK)" "ok"
296
+ else
297
+ assert "C3: malformed amendment date → falls back to filename (BLOCK)" "SILENT: $OUTPUT"
298
+ fi
299
+ teardown_tmpdir
300
+
301
+ # C4: Future-dated amendment → silent (defensive; treated as fresh)
302
+ setup_tmpdir
303
+ cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
304
+ # old
305
+ ## Amendment 2027-01-15 — future dated
306
+ EOF
307
+ add_commits_since "2026-08-30" 5
308
+ OUTPUT=$(run_hook "2026-08-30")
309
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
310
+ assert "C4: future-dated amendment → silent (defensive)" "FIRED: $OUTPUT"
311
+ else
312
+ assert "C4: future-dated amendment → silent (defensive)" "ok"
313
+ fi
314
+ teardown_tmpdir
315
+
316
+ # C5: Multiple amendments — latest by DATE wins (not file position)
317
+ setup_tmpdir
318
+ cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
319
+ # old
320
+ ## Amendment 2026-08-30 — today (appears first in file)
321
+ ## Amendment 2026-08-26 — older (appears second in file)
322
+ EOF
323
+ add_commits_since "2026-08-30" 5
324
+ OUTPUT=$(run_hook "2026-08-30")
325
+ if echo "$OUTPUT" | grep -q "chronicle —"; then
326
+ assert "C5: latest amendment by date wins (not file order)" "STILL BLOCKED: $OUTPUT"
327
+ else
328
+ assert "C5: latest amendment by date wins (not file order)" "ok"
329
+ fi
330
+ teardown_tmpdir
331
+
332
+ # =======================================================================
333
+ # Summary
334
+ # =======================================================================
335
+
336
+ echo ""
337
+ echo "----"
338
+ echo "PASS: $PASS"
339
+ echo "FAIL: $FAIL"
340
+ if [ "$FAIL" -gt 0 ]; then
341
+ echo ""
342
+ echo "Failed tests:"
343
+ for t in "${FAILED_TESTS[@]}"; do
344
+ echo " - $t"
345
+ done
346
+ exit 1
347
+ fi
348
+ exit 0
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env bash
2
+ # tier: standard
3
+ # fixtures: mock-whereami-inline
4
+ # Tests for .claude/hooks/session-reflection.d/36-whereami-size-check.sh
5
+ #
6
+ # Bootstrap pair per .claude/rules/bootstrap-pair-discipline.md — this test
7
+ # + the hook fragment ship together. RED-first per Beck — assertions written
8
+ # against the hook interface before any implementation lands.
9
+ #
10
+ # Anchor luminaries — michael-nygard (fail-safe defaults + stability),
11
+ # kent-beck (RED-first Tier 0), michael-feathers (characterization of
12
+ # rotate-whereami.py behavior — hook does not modify script).
13
+ #
14
+ # Run: bash .claude/hooks/session-reflection.d/tests/36-whereami-size-check.test.sh
15
+
16
+ # test-list:
17
+ # [x] Test 1 — Below 200KB threshold — silent (no output)
18
+ # [x] Test 2 — 200-250KB range — INFORM (rotation approaching; no rotate)
19
+ # [x] Test 3 — Above 250KB — auto-rotate via rotate-whereami.py --days 3
20
+ # [x] Test 4 — SKIP_WHEREAMI_SIZE_CHECK=1 — hook returns without action
21
+ # [x] Test 5 — Missing whereami file — hook returns silently
22
+ # [x] Test 6 — Missing rotate-whereami.py script — hook returns silently
23
+ # [x] Test 7 — Uncommitted whereami changes — hook skips rotation (R1 pre-mortem)
24
+ # [x] Test 8 — Fragment sourced under set -e (coordinator invocation) — parent survives
25
+
26
+ set -o pipefail
27
+
28
+ HOOK="$(cd "$(dirname "$0")/../../../.." && pwd)/.claude/hooks/session-reflection.d/36-whereami-size-check.sh"
29
+ ROTATE_SCRIPT="$(cd "$(dirname "$0")/../../../.." && pwd)/scripts/rotate-whereami.py"
30
+
31
+ TMPDIR=$(mktemp -d)
32
+ trap 'rm -rf "$TMPDIR"' EXIT
33
+
34
+ PASS=0
35
+ FAIL=0
36
+
37
+ assert() {
38
+ local name="$1"
39
+ local result="$2"
40
+ if [ "$result" = "ok" ]; then
41
+ PASS=$((PASS + 1))
42
+ echo "PASS: $name"
43
+ else
44
+ FAIL=$((FAIL + 1))
45
+ echo "FAIL: $name — $result"
46
+ fi
47
+ }
48
+
49
+ # Build a whereami of a given approximate size (bytes) with matched recap blocks
50
+ build_whereami_of_size() {
51
+ local file="$1"
52
+ local target_bytes="$2"
53
+ local blocks_needed=$((target_bytes / 1500)) # ~1500 bytes per block
54
+ [ "$blocks_needed" -lt 1 ] && blocks_needed=1
55
+
56
+ cat > "$file" <<'FIXHEAD'
57
+ ---
58
+ project: test
59
+ phase: Construction
60
+ last_updated:
61
+ at: 2026-08-19T00:00:00Z
62
+ FIXHEAD
63
+
64
+ # Add 100 lines of frontmatter padding (small key: value lines) to hit lower sizes
65
+ for i in $(seq 1 20); do
66
+ echo "frontmatter_field_$i: value_$i" >> "$file"
67
+ done
68
+
69
+ # Add recap blocks with unique old dates (all < 2026-08-16 for archivability
70
+ # under --days 3 cutoff on 2026-08-19). Distribute across 7 months to keep
71
+ # keys unique for up to ~196 blocks.
72
+ # Each block ~1400 bytes (matches 1500 divisor for approximate sizing)
73
+ for i in $(seq 1 $blocks_needed); do
74
+ local month=$(( (i - 1) / 28 + 1 ))
75
+ local day=$(( (i - 1) % 28 + 1 ))
76
+ [ $month -gt 7 ] && month=7 # wrap tail to July for large N
77
+ local d
78
+ d="2026-$(printf '%02d' $month)-$(printf '%02d' $day)"
79
+ echo "operator_recap_${d}: |" >> "$file"
80
+ for j in $(seq 1 15); do
81
+ echo " padding line $j for block $i to reach target size — lorem ipsum lorem ipsum lorem ipsum" >> "$file"
82
+ done
83
+ done
84
+
85
+ echo "---" >> "$file"
86
+ }
87
+
88
+ # === Test 1 — Below 200KB threshold — silent ===
89
+ WHEREAMI="$TMPDIR/w1.md"
90
+ build_whereami_of_size "$WHEREAMI" 100000 # 100KB
91
+ SIZE=$(wc -c < "$WHEREAMI" | tr -d ' ')
92
+ STDOUT_OUT=$(WHEREAMI_PATH="$WHEREAMI" ROTATE_SCRIPT="$ROTATE_SCRIPT" bash "$HOOK" 2>&1)
93
+ if [ -z "$STDOUT_OUT" ]; then
94
+ assert "Test 1: below 200KB — silent (size=$SIZE)" "ok"
95
+ else
96
+ assert "Test 1: below 200KB — silent" "size=$SIZE output='$STDOUT_OUT'"
97
+ fi
98
+
99
+ # === Test 2 — 200-250KB range — INFORM approaching ===
100
+ WHEREAMI="$TMPDIR/w2.md"
101
+ build_whereami_of_size "$WHEREAMI" 220000 # 220KB
102
+ SIZE=$(wc -c < "$WHEREAMI" | tr -d ' ')
103
+ STDOUT_OUT=$(WHEREAMI_PATH="$WHEREAMI" ROTATE_SCRIPT="$ROTATE_SCRIPT" bash "$HOOK" 2>&1)
104
+ if echo "$STDOUT_OUT" | grep -qi "approaching\|inform\|whereami" && ! echo "$STDOUT_OUT" | grep -qi "rotated"; then
105
+ assert "Test 2: 200-250KB — INFORM without rotate (size=$SIZE)" "ok"
106
+ else
107
+ assert "Test 2: 200-250KB — INFORM without rotate" "size=$SIZE output='$STDOUT_OUT'"
108
+ fi
109
+
110
+ # === Test 3 — Above 250KB — auto-rotate ===
111
+ WHEREAMI="$TMPDIR/w3.md"
112
+ build_whereami_of_size "$WHEREAMI" 300000 # 300KB
113
+ SIZE_BEFORE=$(wc -c < "$WHEREAMI" | tr -d ' ')
114
+ STDOUT_OUT=$(WHEREAMI_PATH="$WHEREAMI" ROTATE_SCRIPT="$ROTATE_SCRIPT" ARCHIVE_DIR="$TMPDIR/archive3" bash "$HOOK" 2>&1)
115
+ SIZE_AFTER=$(wc -c < "$WHEREAMI" | tr -d ' ')
116
+ if [ "$SIZE_AFTER" -lt "$SIZE_BEFORE" ] && echo "$STDOUT_OUT" | grep -qi "rotated\|archive"; then
117
+ assert "Test 3: >250KB — auto-rotate (before=$SIZE_BEFORE after=$SIZE_AFTER)" "ok"
118
+ else
119
+ assert "Test 3: >250KB — auto-rotate" "before=$SIZE_BEFORE after=$SIZE_AFTER output='$STDOUT_OUT'"
120
+ fi
121
+
122
+ # === Test 4 — SKIP_WHEREAMI_SIZE_CHECK=1 respects opt-out ===
123
+ WHEREAMI="$TMPDIR/w4.md"
124
+ build_whereami_of_size "$WHEREAMI" 300000
125
+ SIZE_BEFORE=$(wc -c < "$WHEREAMI" | tr -d ' ')
126
+ STDOUT_OUT=$(SKIP_WHEREAMI_SIZE_CHECK=1 WHEREAMI_PATH="$WHEREAMI" ROTATE_SCRIPT="$ROTATE_SCRIPT" ARCHIVE_DIR="$TMPDIR/archive4" bash "$HOOK" 2>&1)
127
+ SIZE_AFTER=$(wc -c < "$WHEREAMI" | tr -d ' ')
128
+ if [ "$SIZE_AFTER" = "$SIZE_BEFORE" ] && [ -z "$STDOUT_OUT" ]; then
129
+ assert "Test 4: SKIP_WHEREAMI_SIZE_CHECK=1 — no action" "ok"
130
+ else
131
+ assert "Test 4: SKIP_WHEREAMI_SIZE_CHECK=1 — no action" "size_before=$SIZE_BEFORE size_after=$SIZE_AFTER output='$STDOUT_OUT'"
132
+ fi
133
+
134
+ # === Test 5 — Missing whereami file — silent ===
135
+ STDOUT_OUT=$(WHEREAMI_PATH="$TMPDIR/nonexistent.md" ROTATE_SCRIPT="$ROTATE_SCRIPT" bash "$HOOK" 2>&1)
136
+ if [ -z "$STDOUT_OUT" ]; then
137
+ assert "Test 5: missing whereami file — silent" "ok"
138
+ else
139
+ assert "Test 5: missing whereami file — silent" "output='$STDOUT_OUT'"
140
+ fi
141
+
142
+ # === Test 6 — Missing rotate-whereami.py — silent ===
143
+ WHEREAMI="$TMPDIR/w6.md"
144
+ build_whereami_of_size "$WHEREAMI" 300000
145
+ STDOUT_OUT=$(WHEREAMI_PATH="$WHEREAMI" ROTATE_SCRIPT="$TMPDIR/nonexistent-script.py" bash "$HOOK" 2>&1)
146
+ if [ -z "$STDOUT_OUT" ] || echo "$STDOUT_OUT" | grep -qi "skip\|not found"; then
147
+ assert "Test 6: missing rotate script — silent or skip note" "ok"
148
+ else
149
+ assert "Test 6: missing rotate script — silent or skip note" "output='$STDOUT_OUT'"
150
+ fi
151
+
152
+ # === Test 7 — Uncommitted whereami changes — hook skips rotation ===
153
+ # Setup: whereami is 'modified' relative to git baseline
154
+ WHEREAMI="$TMPDIR/w7.md"
155
+ build_whereami_of_size "$WHEREAMI" 300000
156
+ SIZE_BEFORE=$(wc -c < "$WHEREAMI" | tr -d ' ')
157
+ # Signal via WHEREAMI_MODIFIED=1 that the hook should treat whereami as dirty
158
+ STDOUT_OUT=$(WHEREAMI_PATH="$WHEREAMI" WHEREAMI_MODIFIED=1 ROTATE_SCRIPT="$ROTATE_SCRIPT" ARCHIVE_DIR="$TMPDIR/archive7" bash "$HOOK" 2>&1)
159
+ SIZE_AFTER=$(wc -c < "$WHEREAMI" | tr -d ' ')
160
+ if [ "$SIZE_AFTER" = "$SIZE_BEFORE" ] && echo "$STDOUT_OUT" | grep -qi "modified\|skip"; then
161
+ assert "Test 7: uncommitted whereami — skip rotation + inform" "ok"
162
+ else
163
+ assert "Test 7: uncommitted whereami — skip rotation + inform" "size_before=$SIZE_BEFORE size_after=$SIZE_AFTER output='$STDOUT_OUT'"
164
+ fi
165
+
166
+ # === Test 8 — Fragment sourced under set -e — parent survives ===
167
+ CANARY="$TMPDIR/canary8.txt"
168
+ WHEREAMI="$TMPDIR/w8.md"
169
+ build_whereami_of_size "$WHEREAMI" 100000
170
+ (
171
+ set -e
172
+ export WHEREAMI_PATH="$WHEREAMI"
173
+ export ROTATE_SCRIPT="$ROTATE_SCRIPT"
174
+ # shellcheck disable=SC1090
175
+ source "$HOOK" 2>/dev/null
176
+ echo "CANARY_REACHED" > "$CANARY"
177
+ ) 2>&1
178
+ if [ -f "$CANARY" ] && grep -q "CANARY_REACHED" "$CANARY"; then
179
+ assert "Test 8: sourced under set -e — parent survives" "ok"
180
+ else
181
+ assert "Test 8: sourced under set -e — parent survives" "canary not written; fragment likely used exit"
182
+ fi
183
+
184
+ # === Summary ===
185
+ echo ""
186
+ echo "==========================================="
187
+ echo "Result: $PASS passed, $FAIL failed"
188
+ echo "==========================================="
189
+ [ "$FAIL" -eq 0 ]