@onlooker-community/ecosystem 0.34.0 → 0.43.0

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 (112) hide show
  1. package/.claude/skills/writing-tests/SKILL.md +27 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/release.yml +14 -14
  4. package/.release-please-manifest.json +7 -7
  5. package/AGENTS.md +116 -0
  6. package/CHANGELOG.md +175 -0
  7. package/CLAUDE.md +5 -0
  8. package/docs/lesson-promotion-pipeline.md +6 -6
  9. package/docs/superpowers/plans/2026-08-10-lesson-confirmation.md +1273 -0
  10. package/docs/superpowers/plans/2026-08-11-lesson-judging.md +1232 -0
  11. package/docs/superpowers/plans/2026-08-11-lesson-unconfirm.md +458 -0
  12. package/docs/superpowers/plans/2026-08-12-author-key.md +567 -0
  13. package/docs/superpowers/plans/2026-08-13-approved-pool.md +747 -0
  14. package/docs/superpowers/plans/2026-08-13-librarian-cluster.md +392 -0
  15. package/docs/superpowers/plans/2026-08-14-criterion-scores-consumers.md +1374 -0
  16. package/docs/superpowers/plans/2026-08-14-criterion-scores-schema.md +277 -0
  17. package/docs/superpowers/plans/2026-08-16-cartographer-undocumented-entity.md +741 -0
  18. package/docs/superpowers/specs/2026-08-10-lesson-confirmation-design.md +213 -0
  19. package/docs/superpowers/specs/2026-08-11-lesson-judging-design.md +267 -0
  20. package/docs/superpowers/specs/2026-08-11-lesson-unconfirm-design.md +152 -0
  21. package/docs/superpowers/specs/2026-08-12-author-key-design.md +317 -0
  22. package/docs/superpowers/specs/2026-08-13-approved-pool-design.md +232 -0
  23. package/docs/superpowers/specs/2026-08-14-criterion-scores-design.md +254 -0
  24. package/docs/superpowers/specs/2026-08-16-cartographer-undocumented-entity-design.md +249 -0
  25. package/package.json +2 -2
  26. package/plugins/assayer/.claude-plugin/plugin.json +1 -1
  27. package/plugins/assayer/CHANGELOG.md +7 -0
  28. package/plugins/assayer/scripts/lib/assayer-config.sh +6 -0
  29. package/plugins/cartographer/.claude-plugin/plugin.json +1 -1
  30. package/plugins/cartographer/CHANGELOG.md +54 -0
  31. package/plugins/cartographer/README.md +56 -1
  32. package/plugins/cartographer/config.json +7 -1
  33. package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +4 -4
  34. package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +7 -4
  35. package/plugins/cartographer/scripts/lib/cartographer-collect.sh +8 -1
  36. package/plugins/cartographer/scripts/lib/cartographer-config.sh +26 -0
  37. package/plugins/cartographer/scripts/lib/cartographer-events.sh +90 -0
  38. package/plugins/cartographer/scripts/lib/cartographer-filter.sh +105 -0
  39. package/plugins/cartographer/scripts/lib/cartographer-omission.sh +130 -0
  40. package/plugins/cartographer/scripts/lib/cartographer-resolve.sh +132 -0
  41. package/plugins/cartographer/scripts/run-audit.sh +136 -53
  42. package/plugins/cartographer/skills/cartographer/SKILL.md +33 -9
  43. package/plugins/curator/.claude-plugin/plugin.json +1 -1
  44. package/plugins/curator/CHANGELOG.md +7 -0
  45. package/plugins/curator/scripts/lib/curator-emit.sh +2 -1
  46. package/plugins/historian/.claude-plugin/plugin.json +1 -1
  47. package/plugins/historian/CHANGELOG.md +7 -0
  48. package/plugins/historian/scripts/lib/historian-emit.sh +2 -1
  49. package/plugins/librarian/.claude-plugin/plugin.json +1 -1
  50. package/plugins/librarian/CHANGELOG.md +71 -0
  51. package/plugins/librarian/config.json +30 -1
  52. package/plugins/librarian/docs/adr/002-agent-definitions-are-shared-assets.md +82 -0
  53. package/plugins/librarian/scripts/hooks/librarian-session-end.sh +28 -0
  54. package/plugins/librarian/scripts/hooks/librarian-session-start.sh +42 -12
  55. package/plugins/librarian/scripts/lib/librarian-author-key.sh +279 -0
  56. package/plugins/librarian/scripts/lib/librarian-cli.sh +346 -2
  57. package/plugins/librarian/scripts/lib/librarian-emit.sh +2 -1
  58. package/plugins/librarian/scripts/lib/librarian-lesson-judge.sh +349 -0
  59. package/plugins/librarian/scripts/lib/librarian-lesson-promote.sh +225 -0
  60. package/plugins/librarian/scripts/lib/librarian-lesson-review.sh +312 -0
  61. package/plugins/librarian/scripts/lib/librarian-lesson-rubric.sh +56 -0
  62. package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +82 -13
  63. package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +6 -1
  64. package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +131 -62
  65. package/plugins/librarian/skills/librarian/SKILL.md +98 -1
  66. package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
  67. package/plugins/tribunal/CHANGELOG.md +63 -0
  68. package/plugins/tribunal/agents/tribunal-judge-adversarial.md +34 -0
  69. package/plugins/tribunal/agents/tribunal-judge-security.md +33 -0
  70. package/plugins/tribunal/agents/tribunal-judge-standard.md +12 -0
  71. package/plugins/tribunal/config.json +1 -0
  72. package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +154 -9
  73. package/plugins/tribunal/scripts/lib/tribunal-gate.sh +195 -16
  74. package/plugins/tribunal/skills/tribunal/SKILL.md +5 -3
  75. package/scripts/lib/prompt-rules.sh +6 -1
  76. package/test/bats/archivist-inject.bats +1 -1
  77. package/test/bats/assayer-extract.bats +2 -2
  78. package/test/bats/bursar-session-start.bats +3 -3
  79. package/test/bats/cartographer-config.bats +43 -0
  80. package/test/bats/cartographer-events.bats +267 -0
  81. package/test/bats/cartographer-filter.bats +169 -0
  82. package/test/bats/cartographer-lock.bats +3 -3
  83. package/test/bats/cartographer-omission.bats +202 -0
  84. package/test/bats/cartographer-resolve.bats +287 -0
  85. package/test/bats/cartographer-run-audit.bats +325 -0
  86. package/test/bats/compass-sanitizer.bats +11 -11
  87. package/test/bats/compass-transcript.bats +2 -2
  88. package/test/bats/config.bats +15 -15
  89. package/test/bats/curator-session-start.bats +10 -3
  90. package/test/bats/emit-payload-default.bats +52 -0
  91. package/test/bats/governor-ledger.bats +1 -1
  92. package/test/bats/historian-prompt-submit.bats +1 -1
  93. package/test/bats/inspector-post-write-hook.bats +4 -4
  94. package/test/bats/librarian-author-key.bats +477 -0
  95. package/test/bats/librarian-cli.bats +16 -16
  96. package/test/bats/librarian-lesson-judge.bats +884 -0
  97. package/test/bats/librarian-lesson-promote.bats +552 -0
  98. package/test/bats/librarian-lesson-review.bats +900 -0
  99. package/test/bats/librarian-lesson-transform.bats +17 -3
  100. package/test/bats/librarian-session-end.bats +161 -0
  101. package/test/bats/librarian-session-start.bats +36 -2
  102. package/test/bats/lineage-config.bats +1 -1
  103. package/test/bats/lineage-redact.bats +5 -5
  104. package/test/bats/session-tracker.bats +4 -4
  105. package/test/bats/tribunal-aggregate.bats +354 -1
  106. package/test/bats/tribunal-gate.bats +631 -0
  107. package/test/bats/tribunal-judge-agents.bats +119 -0
  108. package/test/bats/tribunal-jury.bats +1 -1
  109. package/test/bats/turn-tracker.bats +1 -1
  110. package/test/bats/warden-sanitizer.bats +3 -3
  111. package/test/bats/worktree-tracker.bats +2 -2
  112. package/test/node/lesson-validate-agreement.test.mjs +35 -6
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Covers the resolution loop: a finding whose drift is gone stops rendering.
4
+ #
5
+ # The record has carried resolved:false since the plugin shipped and nothing
6
+ # ever flipped it, so fixing the drift a finding reported did not retire the
7
+ # finding — it rendered forever (ecosystem-nhi).
8
+ #
9
+ # Absence of a finding from a run is the evidence used, which is only sound when
10
+ # the run looked everywhere. The guard tests below are the substance of this
11
+ # file: getting resolution wrong in the permissive direction silently hides live
12
+ # findings, which is worse than the stale ones being retired.
13
+
14
+ setup() {
15
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
16
+ setup_test_env
17
+
18
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/cartographer"
19
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
20
+ # shellcheck disable=SC1091
21
+ source "${PLUGIN_ROOT}/scripts/lib/cartographer-resolve.sh"
22
+
23
+ FINDINGS_DIR="${BATS_TEST_TMPDIR}/findings"
24
+ mkdir -p "$FINDINGS_DIR"
25
+
26
+ # A fixed clock. Real timestamps are epoch seconds, and an audit that starts
27
+ # and finishes inside one second would otherwise make "before the run" and
28
+ # "during the run" indistinguishable.
29
+ AUDIT_START=2000
30
+
31
+ # Where the injected emitter records what it was asked to announce. The sweep
32
+ # runs inside a command substitution in production, so a stub that set a
33
+ # variable would lose it to the subshell — a file survives, which is exactly
34
+ # why the real emitter appends to the event log rather than returning.
35
+ ANNOUNCED="${BATS_TEST_TMPDIR}/announced"
36
+ : > "$ANNOUNCED"
37
+ }
38
+
39
+ _announce() { printf '%s\n' "$1" >> "$ANNOUNCED"; }
40
+
41
+ # Deliberately writes to stdout, which would corrupt the count the sweep prints
42
+ # if the call site did not redirect it.
43
+ _noisy_announce() { printf '%s\n' "$1" >> "$ANNOUNCED"; printf 'NOISE'; }
44
+
45
+ _announced() { cat "$ANNOUNCED" 2>/dev/null; }
46
+
47
+ # Seed a finding record in the shape run_emit writes.
48
+ _seed() {
49
+ local hash="$1" last_seen="$2" resolved="${3:-false}"
50
+ jq -n --arg h "$hash" --argjson ls "$last_seen" --argjson r "$resolved" \
51
+ '{finding_hash: $h, type: "undocumented_entity", severity: "warning",
52
+ file_a: "CLAUDE.md", file_b: null, description: "d", suggested_fix: "f",
53
+ first_seen_at: 1000, last_seen_at: $ls, resolved: $r}' \
54
+ > "${FINDINGS_DIR}/${hash}.json"
55
+ }
56
+
57
+ _field() {
58
+ jq -r ".$2" "${FINDINGS_DIR}/$1.json"
59
+ }
60
+
61
+ @test "a finding not observed this run is resolved" {
62
+ _seed stale 1500
63
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0
64
+ [ "$output" = "1" ] || return 1
65
+ [ "$(_field stale resolved)" = "true" ]
66
+ }
67
+
68
+ @test "a finding observed this run is left open" {
69
+ _seed fresh 2500
70
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0
71
+ [ "$output" = "0" ] || return 1
72
+ [ "$(_field fresh resolved)" = "false" ]
73
+ }
74
+
75
+ @test "resolution stamps resolved_at" {
76
+ _seed stale 1500
77
+ cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0 4242 >/dev/null
78
+ [ "$(_field stale resolved_at)" = "4242" ]
79
+ }
80
+
81
+ # Strict <: a record last seen exactly at the cutoff is ambiguous, and the safe
82
+ # reading is that it was observed. A stale finding is visible and correctable;
83
+ # one wrongly retired is silent.
84
+ @test "a finding last seen exactly at the cutoff is left open" {
85
+ _seed boundary "$AUDIT_START"
86
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0
87
+ [ "$output" = "0" ] || return 1
88
+ [ "$(_field boundary resolved)" = "false" ]
89
+ }
90
+
91
+ @test "an already-resolved finding is not counted again" {
92
+ _seed old 1500 true
93
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0
94
+ [ "$output" = "0" ]
95
+ }
96
+
97
+ @test "resolves only the absent findings in a mixed store" {
98
+ _seed gone 1500
99
+ _seed here 2500
100
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0
101
+ [ "$output" = "1" ] || return 1
102
+ [ "$(_field gone resolved)" = "true" ] || return 1
103
+ [ "$(_field here resolved)" = "false" ]
104
+ }
105
+
106
+ # ── The guards ────────────────────────────────────────────────────────────────
107
+
108
+ # A targeted post-write audit evaluates one file, so nearly every stored finding
109
+ # is absent for reasons unrelated to being fixed. Without this it would wipe the
110
+ # store on every edit.
111
+ @test "a targeted audit resolves nothing" {
112
+ _seed stale 1500
113
+ run cartographer_resolve_absent_findings \
114
+ "$FINDINGS_DIR" "$AUDIT_START" "/repo/CLAUDE.md" 0
115
+ [ "$output" = "0" ] || return 1
116
+ [ "$(_field stale resolved)" = "false" ]
117
+ }
118
+
119
+ # A phase that timed out contributes no findings, which looks identical to its
120
+ # findings being gone. A partial run is not evidence of resolution.
121
+ @test "a run with a failed phase resolves nothing" {
122
+ _seed stale 1500
123
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 1
124
+ [ "$output" = "0" ] || return 1
125
+ [ "$(_field stale resolved)" = "false" ]
126
+ }
127
+
128
+ @test "both guards together still resolve nothing" {
129
+ _seed stale 1500
130
+ run cartographer_resolve_absent_findings \
131
+ "$FINDINGS_DIR" "$AUDIT_START" "/repo/CLAUDE.md" 2
132
+ [ "$output" = "0" ]
133
+ }
134
+
135
+ # ── Reopening ─────────────────────────────────────────────────────────────────
136
+
137
+ # The dedup sentinel outlives resolution, so reintroduced drift comes back as a
138
+ # KNOWN finding, not a new one. If refreshing did not clear resolved, the
139
+ # renderer would keep hiding it — live drift, permanently invisible.
140
+ @test "re-observing a resolved finding reopens it" {
141
+ _seed recurring 1500 true
142
+ cartographer_refresh_finding "${FINDINGS_DIR}/recurring.json" 3000
143
+ [ "$(_field recurring resolved)" = "false" ] || return 1
144
+ [ "$(_field recurring last_seen_at)" = "3000" ]
145
+ }
146
+
147
+ @test "reopening clears the resolved_at stamp" {
148
+ _seed recurring 1500
149
+ cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0 4242 >/dev/null
150
+ [ "$(_field recurring resolved_at)" = "4242" ] || return 1
151
+ cartographer_refresh_finding "${FINDINGS_DIR}/recurring.json" 5000
152
+ [ "$(_field recurring resolved_at)" = "null" ]
153
+ }
154
+
155
+ @test "a reopened finding is resolvable again once the drift goes" {
156
+ _seed recurring 1500 true
157
+ cartographer_refresh_finding "${FINDINGS_DIR}/recurring.json" 3000
158
+ # A later audit that starts after 3000 and does not observe it.
159
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" 4000 "" 0
160
+ [ "$output" = "1" ] || return 1
161
+ [ "$(_field recurring resolved)" = "true" ]
162
+ }
163
+
164
+ @test "refresh keeps first_seen_at, so recurrence does not rewrite history" {
165
+ _seed recurring 1500 true
166
+ cartographer_refresh_finding "${FINDINGS_DIR}/recurring.json" 3000
167
+ [ "$(_field recurring first_seen_at)" = "1000" ]
168
+ }
169
+
170
+ @test "refresh rejects missing arguments and absent files" {
171
+ run cartographer_refresh_finding "" 3000
172
+ [ "$status" -ne 0 ] || return 1
173
+ run cartographer_refresh_finding "${FINDINGS_DIR}/nope.json" 3000
174
+ [ "$status" -ne 0 ]
175
+ }
176
+
177
+ # ── Edges ─────────────────────────────────────────────────────────────────────
178
+
179
+ @test "an empty store resolves nothing" {
180
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0
181
+ [ "$output" = "0" ]
182
+ }
183
+
184
+ @test "a missing findings dir is not an error" {
185
+ run cartographer_resolve_absent_findings \
186
+ "${BATS_TEST_TMPDIR}/nope" "$AUDIT_START" "" 0
187
+ [ "$status" -eq 0 ] || return 1
188
+ [ "$output" = "0" ]
189
+ }
190
+
191
+ @test "a record with no last_seen_at is treated as unobserved" {
192
+ jq -n '{finding_hash: "bare", resolved: false}' > "${FINDINGS_DIR}/bare.json"
193
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0
194
+ [ "$output" = "1" ]
195
+ }
196
+
197
+ @test "unparsable json is skipped rather than fatal" {
198
+ printf 'not json at all' > "${FINDINGS_DIR}/broken.json"
199
+ _seed stale 1500
200
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0
201
+ [ "$status" -eq 0 ] || return 1
202
+ [ "$output" = "1" ]
203
+ }
204
+
205
+ @test "missing required arguments are rejected" {
206
+ run cartographer_resolve_absent_findings "" "$AUDIT_START" "" 0
207
+ [ "$status" -ne 0 ] || return 1
208
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "" "" 0
209
+ [ "$status" -ne 0 ]
210
+ }
211
+
212
+ @test "the record keeps its other fields when resolved" {
213
+ _seed stale 1500
214
+ cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0 >/dev/null
215
+ jq -e '.finding_hash == "stale" and .type == "undocumented_entity"
216
+ and .first_seen_at == 1000 and .description == "d"' \
217
+ "${FINDINGS_DIR}/stale.json" >/dev/null
218
+ }
219
+
220
+
221
+ # ── Resolution reaching the bus (ecosystem-w2i) ──────────────────────────────
222
+
223
+ # The guards are the substance here. A targeted or partial run sees too little
224
+ # to treat absence as evidence, so it must announce nothing at all — announcing
225
+ # a resolution the run did not establish tells every consumer to close a finding
226
+ # that is still live.
227
+ @test "the sound-resolution predicate holds only for a full, complete run" {
228
+ cartographer_resolution_is_sound "" 0 || return 1
229
+ ! cartographer_resolution_is_sound "CLAUDE.md" 0 || return 1
230
+ ! cartographer_resolution_is_sound "" 1 || return 1
231
+ ! cartographer_resolution_is_sound "CLAUDE.md" 2
232
+ }
233
+
234
+ @test "a retired finding is announced through the injected emitter" {
235
+ _seed stale 1500
236
+ cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0 "" _announce >/dev/null
237
+ [ "$(_announced)" = "stale" ]
238
+ }
239
+
240
+ @test "every retired finding is announced, not just the first" {
241
+ _seed one 1500
242
+ _seed two 1600
243
+ cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0 "" _announce >/dev/null
244
+ [ "$(_announced | sort | tr '\n' ' ')" = "one two " ]
245
+ }
246
+
247
+ @test "a targeted audit announces nothing" {
248
+ _seed stale 1500
249
+ cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "CLAUDE.md" 0 "" _announce >/dev/null
250
+ [ -z "$(_announced)" ]
251
+ }
252
+
253
+ @test "a partial run announces nothing" {
254
+ _seed stale 1500
255
+ cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 1 "" _announce >/dev/null
256
+ [ -z "$(_announced)" ]
257
+ }
258
+
259
+ @test "a finding still observed this run is not announced" {
260
+ _seed fresh 2500
261
+ cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0 "" _announce >/dev/null
262
+ [ -z "$(_announced)" ]
263
+ }
264
+
265
+ # Delivery is at-least-once but re-announcing a finding that was already retired
266
+ # would report a closure that did not happen this run.
267
+ @test "an already-resolved finding is not announced again" {
268
+ _seed done 1500 true
269
+ cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0 "" _announce >/dev/null
270
+ [ -z "$(_announced)" ]
271
+ }
272
+
273
+ @test "the sweep still works with no emitter injected" {
274
+ _seed stale 1500
275
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0
276
+ [ "$output" = "1" ] || return 1
277
+ [ "$(_field stale resolved)" = "true" ]
278
+ }
279
+
280
+ # The sweep returns its count on stdout and the caller reads it through a
281
+ # command substitution, so anything the emitter prints lands in that number.
282
+ @test "a chatty emitter cannot corrupt the count" {
283
+ _seed one 1500
284
+ _seed two 1600
285
+ run cartographer_resolve_absent_findings "$FINDINGS_DIR" "$AUDIT_START" "" 0 "" _noisy_announce
286
+ [ "$output" = "2" ]
287
+ }
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # End-to-end coverage of the audit orchestrator.
4
+ #
5
+ # Nothing drove run-audit.sh before this file, and that absence is why two
6
+ # separate defects lived in it undetected: a crash on macOS bash 3.2 whenever
7
+ # the exclude list was empty (ecosystem-3xf), and config never being loaded at
8
+ # all, so every top-level setting was silently ignored (ecosystem-88v). Both are
9
+ # invisible to the unit tests, which exercise the libraries the orchestrator
10
+ # calls rather than the orchestrator itself.
11
+ #
12
+ # The audit shells out to `claude` for its three analysis phases. The stub below
13
+ # returns an empty findings array, which is enough: what is under test here is
14
+ # whether the orchestrator resolves and propagates its own configuration, not
15
+ # what a model says about the corpus.
16
+
17
+ setup() {
18
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
19
+ setup_test_env
20
+
21
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/cartographer"
22
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
23
+ AUDIT="${PLUGIN_ROOT}/scripts/run-audit.sh"
24
+
25
+ FIXTURE_REPO="${BATS_TEST_TMPDIR}/repo"
26
+ mkdir -p "${FIXTURE_REPO}/sub" "${FIXTURE_REPO}/.claude"
27
+ # Path-like tokens and a global counterpart so all three LLM analyzers have
28
+ # something to chew on. Without them stale_ref and scope_collision
29
+ # short-circuit before calling the model, and the call counts below could
30
+ # not tell "skipped by the filter" apart from "had nothing to do".
31
+ printf '# Root\nAlways read scripts/lib/config-loader.sh first.\nNever use src/legacy/gone.ts.\n' \
32
+ > "${FIXTURE_REPO}/CLAUDE.md"
33
+ printf '# Sub\nSee plugins/tribunal/README.md for details.\n' > "${FIXTURE_REPO}/sub/CLAUDE.md"
34
+ mkdir -p "${CLAUDE_HOME}"
35
+ printf '# Global\nAlways prefer tabs.\n' > "${CLAUDE_HOME}/CLAUDE.md"
36
+
37
+ export CARTOGRAPHER_DIR="${BATS_TEST_TMPDIR}/cartographer"
38
+ mkdir -p "$CARTOGRAPHER_DIR"
39
+ AUDIT_LOG="${CARTOGRAPHER_DIR}/audit.log"
40
+
41
+ # The stub records each invocation so tests can assert on how many model calls
42
+ # an audit actually made — the only honest evidence that a filter skipped an
43
+ # analyzer rather than merely discarding its output.
44
+ export CLAUDE_CALL_LOG="${BATS_TEST_TMPDIR}/claude-calls"
45
+ : > "$CLAUDE_CALL_LOG"
46
+
47
+ STUB_BIN="${BATS_TEST_TMPDIR}/bin"
48
+ mkdir -p "$STUB_BIN"
49
+ cat > "${STUB_BIN}/claude" <<'STUB'
50
+ #!/usr/bin/env bash
51
+ cat >/dev/null
52
+ echo call >> "$CLAUDE_CALL_LOG"
53
+ printf '[]'
54
+ STUB
55
+ chmod +x "${STUB_BIN}/claude"
56
+ export PATH="${STUB_BIN}:${PATH}"
57
+ }
58
+
59
+ EVENTS_LOG_PATH() { printf '%s' "${ONLOOKER_DIR}/logs/onlooker-events.jsonl"; }
60
+
61
+ # Seed a finding from an earlier run. last_seen_at sits far in the past, so the
62
+ # current audit — whose stub returns no findings — will not observe it.
63
+ _seed_stored_finding() {
64
+ local hash="$1" last_seen="${2:-1000}"
65
+ mkdir -p "${CARTOGRAPHER_DIR}/findings"
66
+ jq -n --arg h "$hash" --argjson ls "$last_seen" \
67
+ '{finding_hash:$h, type:"undocumented_entity", severity:"warning",
68
+ file_a:"CLAUDE.md", file_b:null, description:"d", suggested_fix:"f",
69
+ first_seen_at:500, last_seen_at:$ls, resolved:false}' \
70
+ > "${CARTOGRAPHER_DIR}/findings/${hash}.json"
71
+ }
72
+
73
+ _resolved_event_count() {
74
+ local n
75
+ n=$(grep -c '"event_type":"cartographer.issue.resolved"' "$(EVENTS_LOG_PATH)" 2>/dev/null) || n=0
76
+ printf '%s' "$n"
77
+ }
78
+
79
+ _last_audit_complete() {
80
+ grep '"event_type":"cartographer.audit.complete"' "$(EVENTS_LOG_PATH)" 2>/dev/null | tail -n 1
81
+ }
82
+
83
+ _llm_calls() {
84
+ wc -l < "$CLAUDE_CALL_LOG" | tr -d ' '
85
+ }
86
+
87
+ _settings() {
88
+ printf '%s' "$1" > "${FIXTURE_REPO}/.claude/settings.json"
89
+ }
90
+
91
+ _run_audit() {
92
+ CARTOGRAPHER_REPO_ROOT="$FIXTURE_REPO" bash "$AUDIT"
93
+ }
94
+
95
+ @test "a full audit completes and records the run" {
96
+ run _run_audit
97
+ [ "$status" -eq 0 ] || return 1
98
+ grep -q 'completed successfully' "$AUDIT_LOG" || return 1
99
+ [ -f "${CARTOGRAPHER_DIR}/last_audit_at" ]
100
+ }
101
+
102
+ # ── Config actually reaches the orchestrator ─────────────────────────────────
103
+
104
+ # exclude_paths is the setting that stings: run_discover hands it straight to
105
+ # cartographer_collect_files, so ignoring it means the discovery walk always
106
+ # used the shipped defaults no matter what the user configured, quietly breaking
107
+ # the replace-not-merge contract in the plugin's own ADR-004.
108
+ @test "repo exclude_paths narrows the discovery walk" {
109
+ _settings '{"cartographer": {"exclude_paths": ["sub"]}}'
110
+ _run_audit
111
+ grep -q 'phase=discover files=1' "$AUDIT_LOG"
112
+ }
113
+
114
+ @test "without an exclude override both instruction files are discovered" {
115
+ _run_audit
116
+ grep -q 'phase=discover files=2' "$AUDIT_LOG"
117
+ }
118
+
119
+ # The orchestrator warns when total_timeout_seconds cannot cover three phases.
120
+ # The warning only fires if the CONFIGURED phase timeout was read: at the 60s
121
+ # default the shipped 600s total is comfortably sufficient and nothing is
122
+ # logged, so its presence is proof the accessor saw 999 rather than the default.
123
+ @test "phase_timeout_seconds is read from config, not the accessor default" {
124
+ _settings '{"cartographer": {"phase_timeout_seconds": 999}}'
125
+ _run_audit
126
+ grep -q 'total_timeout_seconds=600 is less than 3× phase_timeout_seconds=999' "$AUDIT_LOG"
127
+ }
128
+
129
+ @test "no warning at the shipped defaults" {
130
+ _run_audit
131
+ run grep -c 'is less than 3×' "$AUDIT_LOG"
132
+ [ "$output" = "0" ]
133
+ }
134
+
135
+ @test "user-level settings reach the orchestrator too" {
136
+ mkdir -p "${CLAUDE_HOME}"
137
+ printf '%s' '{"cartographer": {"exclude_paths": ["sub"]}}' \
138
+ > "${CLAUDE_HOME}/settings.json"
139
+ _run_audit
140
+ grep -q 'phase=discover files=1' "$AUDIT_LOG"
141
+ }
142
+
143
+ # ── The log stays readable ───────────────────────────────────────────────────
144
+
145
+ # run-audit.sh never exported PLUGIN_ROOT, so every analysis sub-shell that
146
+ # sourced cartographer-config.sh resolved config-loader.sh against an empty
147
+ # prefix and died, appending three lines of noise per phase to audit.log on
148
+ # every audit. Nobody read it because the analyzers took their settings as
149
+ # parameters and kept working.
150
+ @test "the audit log carries no config-loader errors" {
151
+ _run_audit
152
+ run grep -cE 'No such file or directory|command not found' "$AUDIT_LOG"
153
+ [ "$output" = "0" ]
154
+ }
155
+
156
+ @test "the audit log carries no unbound variable errors" {
157
+ _settings '{"cartographer": {"exclude_paths": []}}'
158
+ _run_audit
159
+ run grep -c 'unbound variable' "$AUDIT_LOG"
160
+ [ "$output" = "0" ]
161
+ }
162
+
163
+ # An empty exclude list is a legitimate configuration, and expanding a genuinely
164
+ # empty array under `set -u` is an unbound-variable error on bash 3.2 — the
165
+ # macOS system bash, and what #!/usr/bin/env bash resolves to there. This is the
166
+ # ecosystem-3xf crash, which Linux CI could never have seen.
167
+ @test "an empty exclude list does not abort the audit" {
168
+ _settings '{"cartographer": {"exclude_paths": []}}'
169
+ run _run_audit
170
+ [ "$status" -eq 0 ] || return 1
171
+ grep -q 'phase=discover files=2' "$AUDIT_LOG"
172
+ }
173
+
174
+ # ── Targeted audits ──────────────────────────────────────────────────────────
175
+
176
+ @test "a targeted audit examines only the named file" {
177
+ CARTOGRAPHER_TARGET_FILE="${FIXTURE_REPO}/CLAUDE.md" \
178
+ CARTOGRAPHER_REPO_ROOT="$FIXTURE_REPO" bash "$AUDIT"
179
+ grep -q 'phase=discover files=1' "$AUDIT_LOG"
180
+ }
181
+
182
+ # last_audit_at gates the session-start interval check. A targeted run covers
183
+ # one file, so advancing it would let a single edit suppress the next full audit.
184
+ @test "a targeted audit does not advance last_audit_at" {
185
+ CARTOGRAPHER_TARGET_FILE="${FIXTURE_REPO}/CLAUDE.md" \
186
+ CARTOGRAPHER_REPO_ROOT="$FIXTURE_REPO" bash "$AUDIT"
187
+ [ ! -f "${CARTOGRAPHER_DIR}/last_audit_at" ]
188
+ }
189
+
190
+ # ── --type and --scope ───────────────────────────────────────────────────────
191
+ #
192
+ # SKILL.md documented both flags while run-audit.sh read neither, so passing
193
+ # them silently ran a full audit (ecosystem-9og). These assert on the count of
194
+ # model calls, because that is what distinguishes a genuinely skipped analyzer
195
+ # from one that ran and had its findings thrown away.
196
+
197
+ @test "an unfiltered audit calls the model once per LLM analyzer" {
198
+ _run_audit
199
+ [ "$(_llm_calls)" = "3" ]
200
+ }
201
+
202
+ @test "a type filter skips the analyzers that cannot produce it" {
203
+ CARTOGRAPHER_TYPE_FILTER=stale_ref _run_audit
204
+ [ "$(_llm_calls)" = "1" ]
205
+ }
206
+
207
+ # undocumented_entity is a grep, not a model call, so narrowing to it should
208
+ # cost nothing at all.
209
+ @test "narrowing to undocumented_entity makes no model calls" {
210
+ CARTOGRAPHER_TYPE_FILTER=undocumented_entity _run_audit
211
+ [ "$(_llm_calls)" = "0" ]
212
+ }
213
+
214
+ # contradiction and dead_rule share one pass, so dead_rule must still run it —
215
+ # skipping would silently return nothing for a legitimate request.
216
+ @test "dead_rule still runs the contradiction analyzer" {
217
+ CARTOGRAPHER_TYPE_FILTER=dead_rule _run_audit
218
+ [ "$(_llm_calls)" = "1" ] || return 1
219
+ grep -q 'phase=relate starting' "$AUDIT_LOG" || return 1
220
+ run grep -c 'phase=relate skipped by type filter' "$AUDIT_LOG"
221
+ [ "$output" = "0" ]
222
+ }
223
+
224
+ @test "a type filter the relate phase cannot serve says so in the log" {
225
+ CARTOGRAPHER_TYPE_FILTER=scope_collision _run_audit
226
+ grep -q 'phase=relate skipped by type filter' "$AUDIT_LOG"
227
+ }
228
+
229
+ # A typo must not read as a clean repo.
230
+ @test "an unknown type aborts instead of auditing nothing" {
231
+ run env CARTOGRAPHER_TYPE_FILTER=stale_refs CARTOGRAPHER_REPO_ROOT="$FIXTURE_REPO" bash "$AUDIT"
232
+ [ "$status" -ne 0 ] || return 1
233
+ grep -q "unknown type filter 'stale_refs'" "$AUDIT_LOG" || return 1
234
+ [ "$(_llm_calls)" = "0" ]
235
+ }
236
+
237
+ @test "the active filters are recorded in the log" {
238
+ CARTOGRAPHER_TYPE_FILTER=stale_ref CARTOGRAPHER_SCOPE_PATH=sub _run_audit
239
+ grep -q 'filter type=stale_ref' "$AUDIT_LOG" || return 1
240
+ grep -q 'filter scope=sub' "$AUDIT_LOG"
241
+ }
242
+
243
+ @test "a scope narrows the corpus without skipping analyzers" {
244
+ CARTOGRAPHER_SCOPE_PATH=sub _run_audit
245
+ grep -q 'phase=discover files=1' "$AUDIT_LOG" || return 1
246
+ [ "$(_llm_calls)" = "3" ]
247
+ }
248
+
249
+ @test "an absolute scope narrows the same way" {
250
+ CARTOGRAPHER_SCOPE_PATH="${FIXTURE_REPO}/sub" _run_audit
251
+ grep -q 'phase=discover files=1' "$AUDIT_LOG"
252
+ }
253
+
254
+ @test "the two filters compose" {
255
+ CARTOGRAPHER_TYPE_FILTER=stale_ref CARTOGRAPHER_SCOPE_PATH=sub _run_audit
256
+ grep -q 'phase=discover files=1' "$AUDIT_LOG" || return 1
257
+ [ "$(_llm_calls)" = "1" ]
258
+ }
259
+
260
+ @test "a scope matching nothing still completes cleanly" {
261
+ CARTOGRAPHER_SCOPE_PATH=does/not/exist run _run_audit
262
+ grep -q 'phase=discover files=0' "$AUDIT_LOG"
263
+ }
264
+
265
+ @test "the run record captures the trigger" {
266
+ CARTOGRAPHER_TRIGGER="session_start_first_run" _run_audit
267
+ run bash -c "cat '${CARTOGRAPHER_DIR}/runs/'*.json | jq -r '.trigger'"
268
+ [ "$output" = "session_start_first_run" ]
269
+ }
270
+
271
+ # A typeless finding must not vanish. The builder refuses it (ecosystem-ci0),
272
+ # but refusing is only half the fix: if the orchestrator discards that refusal
273
+ # the event is still silently absent, one layer further up. The audit log is
274
+ # where an operator would go looking for it.
275
+ @test "a finding with no type is reported in the audit log" {
276
+ cat > "${STUB_BIN}/claude" <<'STUB'
277
+ #!/usr/bin/env bash
278
+ cat >/dev/null
279
+ echo call >> "$CLAUDE_CALL_LOG"
280
+ printf '[{"severity":"warning","file_a":"CLAUDE.md","excerpt_a":"Always prefer tabs.","description":"typeless"}]'
281
+ STUB
282
+ chmod +x "${STUB_BIN}/claude"
283
+
284
+ run _run_audit
285
+ [ "$status" -eq 0 ] || return 1
286
+ grep -q 'carries no type' "$AUDIT_LOG"
287
+ }
288
+
289
+
290
+ # ── Resolution reaching the bus (ecosystem-w2i) ──────────────────────────────
291
+ #
292
+ # The resolution loop retired findings on disk but nothing announced it, so a
293
+ # consumer reading only the log — counsel, by design — saw every finding ever
294
+ # opened and none ever closed.
295
+
296
+ @test "a full audit announces the findings it retired" {
297
+ _seed_stored_finding gonehash
298
+ _run_audit
299
+ grep '"event_type":"cartographer.issue.resolved"' "$(EVENTS_LOG_PATH)" \
300
+ | jq -e '.payload.finding_hash == "gonehash"' >/dev/null
301
+ }
302
+
303
+ @test "a completed full audit reports how many findings it retired" {
304
+ _seed_stored_finding gonehash
305
+ _run_audit
306
+ _last_audit_complete | jq -e '.payload.resolved_finding_count == 1' >/dev/null
307
+ }
308
+
309
+ # A targeted run sees one file, so nearly every stored finding is "unobserved"
310
+ # for reasons unrelated to being fixed. It must neither retire nor announce.
311
+ @test "a targeted audit announces no resolutions" {
312
+ _seed_stored_finding gonehash
313
+ CARTOGRAPHER_TARGET_FILE="${FIXTURE_REPO}/CLAUDE.md" \
314
+ CARTOGRAPHER_REPO_ROOT="$FIXTURE_REPO" bash "$AUDIT"
315
+ [ "$(_resolved_event_count)" = "0" ]
316
+ }
317
+
318
+ # Reporting zero would read as "swept, retired nothing". The field is absent
319
+ # instead, because this run never swept.
320
+ @test "a targeted audit reports no resolved count at all" {
321
+ _seed_stored_finding gonehash
322
+ CARTOGRAPHER_TARGET_FILE="${FIXTURE_REPO}/CLAUDE.md" \
323
+ CARTOGRAPHER_REPO_ROOT="$FIXTURE_REPO" bash "$AUDIT"
324
+ _last_audit_complete | jq -e '.payload | has("resolved_finding_count") | not' >/dev/null
325
+ }
@@ -24,27 +24,27 @@ setup() {
24
24
  @test "prior_assistant_turn delimiter is stripped" {
25
25
  local out
26
26
  out=$(compass_sanitize "evil <prior_assistant_turn> payload" 240)
27
- [[ "$out" == *"[STRIPPED]"* ]]
28
- [[ "$out" == *"payload"* ]]
27
+ [[ "$out" == *"[STRIPPED]"* ]] || return 1
28
+ [[ "$out" == *"payload"* ]] || return 1
29
29
  [[ "$out" != *"<prior_assistant_turn>"* ]]
30
30
  }
31
31
 
32
32
  @test "all four pair-slot delimiters are stripped" {
33
33
  local out
34
34
  out=$(compass_sanitize "<context_excerpt>x</context_excerpt><tool_input>y</tool_input>" 240)
35
- [[ "$out" != *"<context_excerpt>"* ]]
36
- [[ "$out" != *"</context_excerpt>"* ]]
37
- [[ "$out" != *"<tool_input>"* ]]
35
+ [[ "$out" != *"<context_excerpt>"* ]] || return 1
36
+ [[ "$out" != *"</context_excerpt>"* ]] || return 1
37
+ [[ "$out" != *"<tool_input>"* ]] || return 1
38
38
  [[ "$out" != *"</tool_input>"* ]]
39
39
  }
40
40
 
41
41
  @test "non-evaluator delimiters are also stripped" {
42
42
  local out
43
43
  out=$(compass_sanitize "<<SYS>>x<</SYS>> [INST] y [/INST] <| z" 240)
44
- [[ "$out" != *"<<SYS>>"* ]]
45
- [[ "$out" != *"<</SYS>>"* ]]
46
- [[ "$out" != *"[INST]"* ]]
47
- [[ "$out" != *"[/INST]"* ]]
44
+ [[ "$out" != *"<<SYS>>"* ]] || return 1
45
+ [[ "$out" != *"<</SYS>>"* ]] || return 1
46
+ [[ "$out" != *"[INST]"* ]] || return 1
47
+ [[ "$out" != *"[/INST]"* ]] || return 1
48
48
  [[ "$out" != *"<|"* ]]
49
49
  }
50
50
 
@@ -53,8 +53,8 @@ setup() {
53
53
  input=$(printf 'a\tb\nc\x00d\x01e')
54
54
  local out
55
55
  out=$(compass_sanitize "$input" 240)
56
- [[ "$out" == *"a"* && "$out" == *"b"* && "$out" == *"c"* ]]
57
- [[ "$out" == *"d"* && "$out" == *"e"* ]]
56
+ [[ "$out" == *"a"* && "$out" == *"b"* && "$out" == *"c"* ]] || return 1
57
+ [[ "$out" == *"d"* && "$out" == *"e"* ]] || return 1
58
58
  [ "${out}" = "$(printf 'a\tb\ncde')" ]
59
59
  }
60
60
 
@@ -60,8 +60,8 @@ setup() {
60
60
  EOF
61
61
  local out
62
62
  out=$(compass_read_prior_turn "$t" 800)
63
- [[ "$out" == *"[STRIPPED]"* ]]
64
- [[ "$out" != *"<prior_assistant_turn>"* ]]
63
+ [[ "$out" == *"[STRIPPED]"* ]] || return 1
64
+ [[ "$out" != *"<prior_assistant_turn>"* ]] || return 1
65
65
 
66
66
  # Truncation honors max_chars.
67
67
  local short