@onlooker-community/ecosystem 0.33.0 → 0.34.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.
- package/.agents/skills/beads/SKILL.md +80 -0
- package/.agents/skills/beads/agents/openai.yaml +4 -0
- package/.claude/settings.json +13 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.codex/config.toml +2 -0
- package/.codex/hooks.json +51 -0
- package/.markdownlint.json +3 -0
- package/.release-please-manifest.json +8 -8
- package/AGENTS.md +135 -0
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +56 -1
- package/docs/lesson-promotion-pipeline.md +210 -0
- package/docs/superpowers/plans/2026-08-09-lesson-transform.md +1537 -0
- package/docs/superpowers/specs/2026-08-09-lesson-transform-design.md +261 -0
- package/package.json +3 -2
- package/plugins/archivist/scripts/lib/archivist-config.sh +10 -34
- package/plugins/assayer/.claude-plugin/plugin.json +1 -1
- package/plugins/assayer/CHANGELOG.md +7 -0
- package/plugins/assayer/scripts/lib/assayer-config.sh +39 -62
- package/plugins/cartographer/scripts/lib/cartographer-config.sh +11 -29
- package/plugins/compass/scripts/lib/compass-config.sh +16 -46
- package/plugins/counsel/scripts/lib/counsel-config.sh +15 -46
- package/plugins/curator/.claude-plugin/plugin.json +1 -1
- package/plugins/curator/CHANGELOG.md +7 -0
- package/plugins/curator/scripts/lib/curator-config.sh +19 -44
- package/plugins/echo/scripts/lib/echo-config.sh +30 -59
- package/plugins/governor/scripts/lib/governor-config.sh +11 -41
- package/plugins/historian/scripts/lib/historian-config.sh +9 -33
- package/plugins/inspector/.claude-plugin/plugin.json +1 -1
- package/plugins/inspector/CHANGELOG.md +7 -0
- package/plugins/inspector/scripts/lib/inspector-config.sh +39 -63
- package/plugins/librarian/.claude-plugin/plugin.json +1 -1
- package/plugins/librarian/CHANGELOG.md +7 -0
- package/plugins/librarian/config.json +4 -0
- package/plugins/librarian/schema/PROVENANCE.json +7 -0
- package/plugins/librarian/schema/lesson-applies-to.subschema.json +74 -0
- package/plugins/librarian/schema/lesson-evidence.subschema.json +36 -0
- package/plugins/librarian/scripts/hooks/librarian-session-end.sh +26 -0
- package/plugins/librarian/scripts/lib/librarian-config.sh +10 -34
- package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +135 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +311 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +140 -0
- package/plugins/lineage/.claude-plugin/plugin.json +1 -1
- package/plugins/lineage/CHANGELOG.md +7 -0
- package/plugins/lineage/scripts/lib/lineage-config.sh +17 -53
- package/plugins/scribe/.claude-plugin/plugin.json +1 -1
- package/plugins/scribe/CHANGELOG.md +7 -0
- package/plugins/scribe/scripts/lib/scribe-config.sh +17 -47
- package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
- package/plugins/tribunal/CHANGELOG.md +7 -0
- package/plugins/tribunal/scripts/lib/tribunal-config.sh +25 -63
- package/plugins/warden/scripts/lib/warden-config.sh +17 -54
- package/scripts/lib/config-loader.sh +8 -1
- package/scripts/lint/check-lesson-schema-drift.mjs +36 -0
- package/test/bats/librarian-lesson-transform.bats +609 -0
- package/test/node/lesson-schema-drift.test.mjs +28 -0
- package/test/node/lesson-validate-agreement.test.mjs +154 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Guards the vendored lesson sub-schemas against silent drift.
|
|
3
|
+
//
|
|
4
|
+
// plugins/librarian/schema/*.subschema.json are copied out of the
|
|
5
|
+
// published lesson contract (see PROVENANCE.json) because ajv is
|
|
6
|
+
// unavailable at runtime (ADR-005); the jq rules in
|
|
7
|
+
// librarian-lesson-validate.sh mirror these files by hand. This check
|
|
8
|
+
// only confirms the provenance pin and JSON validity survive edits — it
|
|
9
|
+
// cannot compare against the upstream schema until schema.onlooker.dev
|
|
10
|
+
// publishes lesson schemas.
|
|
11
|
+
//
|
|
12
|
+
// Exit codes:
|
|
13
|
+
// 0 no problems
|
|
14
|
+
// 1 provenance pin changed or a vendored file is missing/invalid
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
const dir = 'plugins/librarian/schema';
|
|
19
|
+
let failures = 0;
|
|
20
|
+
const fail = (m) => {
|
|
21
|
+
console.error(`check-lesson-schema: ${m}`);
|
|
22
|
+
failures++;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const prov = JSON.parse(readFileSync(`${dir}/PROVENANCE.json`, 'utf8'));
|
|
27
|
+
if (prov.schema_version !== 2) fail(`expected schema_version 2, got ${prov.schema_version}`);
|
|
28
|
+
for (const f of ['lesson-evidence.subschema.json', 'lesson-applies-to.subschema.json']) {
|
|
29
|
+
JSON.parse(readFileSync(`${dir}/${f}`, 'utf8'));
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
fail(err.message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (failures > 0) process.exit(1);
|
|
36
|
+
console.log('check-lesson-schema: ok');
|
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
#
|
|
3
|
+
# Pure validation rules for the lesson transform. No I/O, no CLI, no project
|
|
4
|
+
# key — these are the rules every candidate must satisfy before it is written.
|
|
5
|
+
|
|
6
|
+
setup() {
|
|
7
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
8
|
+
setup_test_env
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/librarian"
|
|
10
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
11
|
+
# shellcheck disable=SC1091
|
|
12
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-validate.sh"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_artifact() {
|
|
16
|
+
jq -cn --arg s "$1" --arg d "$2" '{summary: $s, detail: $d}'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "pregate accepts an artifact carrying a dotted version" {
|
|
20
|
+
run librarian_lesson_pregate "$(_artifact "Vitest 4.1.9 breaks" "against Vite 5.4.21")"
|
|
21
|
+
[ "$status" -eq 0 ]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@test "pregate accepts a v-prefixed version" {
|
|
25
|
+
run librarian_lesson_pregate "$(_artifact "broken on v5" "see notes")"
|
|
26
|
+
[ "$status" -eq 0 ]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@test "pregate accepts an x-range" {
|
|
30
|
+
run librarian_lesson_pregate "$(_artifact "fails on 5.x" "see notes")"
|
|
31
|
+
[ "$status" -eq 0 ]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@test "pregate skips an artifact with no version token at all" {
|
|
35
|
+
run librarian_lesson_pregate "$(_artifact "Prefer functional patterns" "User said so during review.")"
|
|
36
|
+
[ "$status" -eq 1 ]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@test "valid_range accepts the four comparator forms and a two-sided range" {
|
|
40
|
+
for r in "<6" "<=6" "=6" ">4" ">=4" ">=4 <6"; do
|
|
41
|
+
run librarian_lesson_valid_range "$r"
|
|
42
|
+
[ "$status" -eq 0 ]
|
|
43
|
+
done
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "valid_range rejects npm-style ranges a model reaches for by default" {
|
|
47
|
+
for r in "^5.4.21" "~5" "5.x" "5.4.21" "" "latest"; do
|
|
48
|
+
run librarian_lesson_valid_range "$r"
|
|
49
|
+
[ "$status" -eq 1 ]
|
|
50
|
+
done
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@test "valid_range rejects unbounded lower bounds that would never expire" {
|
|
54
|
+
for r in ">=0" ">=0.0" ">=0.0.0" ">0"; do
|
|
55
|
+
run librarian_lesson_valid_range "$r"
|
|
56
|
+
[ "$status" -eq 1 ]
|
|
57
|
+
done
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@test "valid_range rejects a string with an embedded newline containing a valid line" {
|
|
61
|
+
# grep's ^/$ anchor to line boundaries, not string boundaries; a naive
|
|
62
|
+
# grep-based check would let a compound string like this slip through
|
|
63
|
+
# because its second line ("<6") is independently valid.
|
|
64
|
+
run librarian_lesson_valid_range "$(printf '>=999\n<6')"
|
|
65
|
+
[ "$status" -eq 1 ]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_candidate() {
|
|
69
|
+
jq -cn --argjson versions "$1" --argjson stack "$2" '{
|
|
70
|
+
claim: "Vitest 4 cannot import vite/module-runner on Vite 5",
|
|
71
|
+
rationale: "vite/module-runner ships in Vite 6; Vitest 4 assumes it exists.",
|
|
72
|
+
evidence: {
|
|
73
|
+
artifact_ids: ["01KZ45MKAM734ZS7JK24D2DK0R"],
|
|
74
|
+
session_ids: ["sess-1"],
|
|
75
|
+
project_key: "6a7678979e31",
|
|
76
|
+
observed_at: "2026-08-03T15:59:48Z",
|
|
77
|
+
resolution: "Pin vitest to 3.x until Vite 6 lands."
|
|
78
|
+
},
|
|
79
|
+
applies_to: {
|
|
80
|
+
stack: $stack,
|
|
81
|
+
scope: {kind: "versioned", versions: $versions},
|
|
82
|
+
file_patterns: [],
|
|
83
|
+
task_kinds: []
|
|
84
|
+
}
|
|
85
|
+
}'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@test "validate_candidate accepts a well-formed versioned candidate" {
|
|
89
|
+
run librarian_lesson_validate_candidate \
|
|
90
|
+
"$(_candidate '{"vite":"<6","vitest":">=4"}' '["vite","vitest"]')"
|
|
91
|
+
[ "$status" -eq 0 ]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@test "validate_candidate rejects a versions key absent from stack" {
|
|
95
|
+
run librarian_lesson_validate_candidate \
|
|
96
|
+
"$(_candidate '{"vite":"<6","vitest":">=4"}' '["vite"]')"
|
|
97
|
+
[ "$status" -eq 1 ]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@test "validate_candidate rejects an invalid range inside an otherwise valid candidate" {
|
|
101
|
+
run librarian_lesson_validate_candidate \
|
|
102
|
+
"$(_candidate '{"vite":"^5.4.21"}' '["vite"]')"
|
|
103
|
+
[ "$status" -eq 1 ]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@test "validate_candidate rejects an empty versions object" {
|
|
107
|
+
run librarian_lesson_validate_candidate "$(_candidate '{}' '["vite"]')"
|
|
108
|
+
[ "$status" -eq 1 ]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@test "validate_candidate rejects a missing resolution" {
|
|
112
|
+
candidate=$(_candidate '{"vite":"<6"}' '["vite"]' | jq -c 'del(.evidence.resolution)')
|
|
113
|
+
run librarian_lesson_validate_candidate "$candidate"
|
|
114
|
+
[ "$status" -eq 1 ]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@test "validate_candidate rejects an empty resolution" {
|
|
118
|
+
candidate=$(_candidate '{"vite":"<6"}' '["vite"]' | jq -c '.evidence.resolution = ""')
|
|
119
|
+
run librarian_lesson_validate_candidate "$candidate"
|
|
120
|
+
[ "$status" -eq 1 ]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@test "validate_candidate rejects version_independent even when well-formed" {
|
|
124
|
+
candidate=$(_candidate '{"vite":"<6"}' '["vite"]' \
|
|
125
|
+
| jq -c '.applies_to.scope = {kind: "version_independent", justification: "git behavior is stable"}')
|
|
126
|
+
run librarian_lesson_validate_candidate "$candidate"
|
|
127
|
+
[ "$status" -eq 1 ]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@test "validate_candidate rejects an empty-string range" {
|
|
131
|
+
run librarian_lesson_validate_candidate \
|
|
132
|
+
"$(_candidate '{"vite":""}' '["vite"]')"
|
|
133
|
+
[ "$status" -eq 1 ]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@test "validate_candidate rejects a versions value with an embedded newline" {
|
|
137
|
+
# A single value like ">=999\n<6" must not be split by a newline-delimited
|
|
138
|
+
# read into two lines that each pass individually.
|
|
139
|
+
local newline_range
|
|
140
|
+
newline_range=$(printf '>=999\n<6')
|
|
141
|
+
run librarian_lesson_validate_candidate \
|
|
142
|
+
"$(_candidate "$(jq -cn --arg v "$newline_range" '{vite: $v}')" '["vite"]')"
|
|
143
|
+
[ "$status" -eq 1 ]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# ----------------------------------------------------------------------------
|
|
147
|
+
# Storage: the lessons/ subtree, proposal writes, the declined ledger, and
|
|
148
|
+
# artifact-keyed idempotency.
|
|
149
|
+
# ----------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
_storage_setup() {
|
|
152
|
+
# shellcheck disable=SC1091
|
|
153
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-project-key.sh"
|
|
154
|
+
# shellcheck disable=SC1091
|
|
155
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-ulid.sh"
|
|
156
|
+
# shellcheck disable=SC1091
|
|
157
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-storage.sh"
|
|
158
|
+
# shellcheck disable=SC1091
|
|
159
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-storage.sh"
|
|
160
|
+
|
|
161
|
+
PROJECT_REPO="${BATS_TEST_TMPDIR}/repo"
|
|
162
|
+
mkdir -p "$PROJECT_REPO"
|
|
163
|
+
git -C "$PROJECT_REPO" init -q
|
|
164
|
+
git -C "$PROJECT_REPO" config user.email t@example.com
|
|
165
|
+
git -C "$PROJECT_REPO" config user.name "Test"
|
|
166
|
+
git -C "$PROJECT_REPO" remote add origin git@github.com:org/lesson-fixture.git
|
|
167
|
+
PROJECT_KEY=$(librarian_project_key "$PROJECT_REPO")
|
|
168
|
+
[ -n "$PROJECT_KEY" ]
|
|
169
|
+
LESSONS_DIR="${ONLOOKER_DIR}/librarian/${PROJECT_KEY}/lessons"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@test "storage_init creates the proposals and approved directories" {
|
|
173
|
+
_storage_setup
|
|
174
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
175
|
+
[ -d "${LESSONS_DIR}/proposals" ]
|
|
176
|
+
[ -d "${LESSONS_DIR}/approved" ]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@test "write_proposal lands a ULID-keyed file carrying its artifact_id" {
|
|
180
|
+
_storage_setup
|
|
181
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
182
|
+
candidate=$(jq -cn '{claim: "c", rationale: "r"}')
|
|
183
|
+
id=$(librarian_lesson_write_proposal "$PROJECT_KEY" "$candidate" "01KZ45MKAM734ZS7JK24D2DK0R")
|
|
184
|
+
[ -n "$id" ]
|
|
185
|
+
[ -f "${LESSONS_DIR}/proposals/${id}.json" ]
|
|
186
|
+
jq -e '.artifact_id == "01KZ45MKAM734ZS7JK24D2DK0R" and .candidate.claim == "c"' \
|
|
187
|
+
"${LESSONS_DIR}/proposals/${id}.json"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@test "append_declined writes one JSONL line per decline" {
|
|
191
|
+
_storage_setup
|
|
192
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
193
|
+
librarian_lesson_append_declined "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R" "no_resolution"
|
|
194
|
+
librarian_lesson_append_declined "$PROJECT_KEY" "01KZEAF9EY4C6TTR0V7YFN9VYJ" "no_versions"
|
|
195
|
+
[ "$(wc -l < "${LESSONS_DIR}/declined.jsonl")" -eq 2 ]
|
|
196
|
+
head -n 1 "${LESSONS_DIR}/declined.jsonl" \
|
|
197
|
+
| jq -e '.artifact_id == "01KZ45MKAM734ZS7JK24D2DK0R" and .reason == "no_resolution"'
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@test "seen reports a fresh artifact as new" {
|
|
201
|
+
_storage_setup
|
|
202
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
203
|
+
run librarian_lesson_seen "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R"
|
|
204
|
+
[ "$status" -eq 1 ]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@test "seen finds an artifact recorded in declined.jsonl" {
|
|
208
|
+
_storage_setup
|
|
209
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
210
|
+
librarian_lesson_append_declined "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R" "no_resolution"
|
|
211
|
+
run librarian_lesson_seen "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R"
|
|
212
|
+
[ "$status" -eq 0 ]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@test "seen finds an artifact already sitting in proposals" {
|
|
216
|
+
_storage_setup
|
|
217
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
218
|
+
librarian_lesson_write_proposal "$PROJECT_KEY" "$(jq -cn '{claim: "c"}')" "01KZ45MKAM734ZS7JK24D2DK0R"
|
|
219
|
+
run librarian_lesson_seen "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R"
|
|
220
|
+
[ "$status" -eq 0 ]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@test "seen finds an artifact already promoted into the approved pool" {
|
|
224
|
+
_storage_setup
|
|
225
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
226
|
+
jq -n '{artifact_id: "01KZ45MKAM734ZS7JK24D2DK0R"}' \
|
|
227
|
+
> "${LESSONS_DIR}/approved/01KZ45MKGQ7QZWMABQ4H12SHSV.json"
|
|
228
|
+
run librarian_lesson_seen "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R"
|
|
229
|
+
[ "$status" -eq 0 ]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@test "seen still finds a declined artifact when declined.jsonl has a truncated trailing line" {
|
|
233
|
+
_storage_setup
|
|
234
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
235
|
+
librarian_lesson_append_declined "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R" "no_resolution"
|
|
236
|
+
# Simulate a process killed mid-append: a trailing line that never closed.
|
|
237
|
+
printf '{"artifact_id":"01KZEAF9EY4C6TTR0V7YFN9VYJ","reason":"trunc' \
|
|
238
|
+
>> "${LESSONS_DIR}/declined.jsonl"
|
|
239
|
+
run librarian_lesson_seen "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R"
|
|
240
|
+
[ "$status" -eq 0 ]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@test "seen does not poison its scan on a valid-JSON non-object ledger line" {
|
|
244
|
+
# fromjson? only guards the *parse* — a line that is valid JSON but not an
|
|
245
|
+
# object (a bare 123) parses cleanly, then errors on `.artifact_id`
|
|
246
|
+
# indexing, which aborts the whole jq invocation unless `objects` filters
|
|
247
|
+
# it out first. That failure mode reads every previously-declined artifact
|
|
248
|
+
# as unseen, so both directions matter here: a real decline must still be
|
|
249
|
+
# found despite the poison line, and a genuinely new artifact must not be
|
|
250
|
+
# swept up as a false match by whatever mechanism tolerates that line.
|
|
251
|
+
_storage_setup
|
|
252
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
253
|
+
librarian_lesson_append_declined "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R" "no_resolution"
|
|
254
|
+
printf '123\n' >> "${LESSONS_DIR}/declined.jsonl"
|
|
255
|
+
|
|
256
|
+
run librarian_lesson_seen "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R"
|
|
257
|
+
[ "$status" -eq 0 ]
|
|
258
|
+
|
|
259
|
+
run librarian_lesson_seen "$PROJECT_KEY" "01KZ45MKGQ7QZWMABQ4H12SHSV"
|
|
260
|
+
[ "$status" -eq 1 ]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@test "seen reports a genuinely absent artifact as new, not merely 'the ledger has entries'" {
|
|
264
|
+
# This is deliberately NOT just "seen returns 1 for an absent id with a
|
|
265
|
+
# truncated line present" — that formulation cannot discriminate a correct
|
|
266
|
+
# implementation from a broken one. Whether the truncated line makes jq
|
|
267
|
+
# error out entirely or jq completes and simply finds no match, the
|
|
268
|
+
# calling code treats both as "not found" and falls through to the same
|
|
269
|
+
# answer either way, so a standalone not-found assertion there passes
|
|
270
|
+
# regardless of which code path produced it.
|
|
271
|
+
#
|
|
272
|
+
# What a not-found assertion CAN catch is a false-positive implementation:
|
|
273
|
+
# one that treats "the ledger is non-empty" or "contains any well-formed
|
|
274
|
+
# entry" as "seen", instead of checking whether an entry actually matches
|
|
275
|
+
# this artifact_id. The ledger below carries two genuine, well-formed,
|
|
276
|
+
# non-matching declines plus the same truncated trailing line, so the only
|
|
277
|
+
# way this test fails is a false match.
|
|
278
|
+
_storage_setup
|
|
279
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
280
|
+
librarian_lesson_append_declined "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R" "no_resolution"
|
|
281
|
+
librarian_lesson_append_declined "$PROJECT_KEY" "01KZEAF9EY4C6TTR0V7YFN9VYJ" "no_versions"
|
|
282
|
+
printf '{"artifact_id":"01KZ45MKS84KPZQNWC02Z8FE0K","reason":"trunc' \
|
|
283
|
+
>> "${LESSONS_DIR}/declined.jsonl"
|
|
284
|
+
run librarian_lesson_seen "$PROJECT_KEY" "01KZ45MKGQ7QZWMABQ4H12SHSV"
|
|
285
|
+
[ "$status" -eq 1 ]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# ----------------------------------------------------------------------------
|
|
289
|
+
# Transform: prompt building, the `claude -p` call, and orchestration. A
|
|
290
|
+
# stubbed `claude` on PATH stands in for the model.
|
|
291
|
+
# ----------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
_transform_setup() {
|
|
294
|
+
_storage_setup
|
|
295
|
+
# shellcheck disable=SC1091
|
|
296
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-validate.sh"
|
|
297
|
+
# shellcheck disable=SC1091
|
|
298
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-config.sh"
|
|
299
|
+
librarian_config_load "$PROJECT_REPO"
|
|
300
|
+
# shellcheck disable=SC1091
|
|
301
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-transform.sh"
|
|
302
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
303
|
+
|
|
304
|
+
STUB_BIN="${BATS_TEST_TMPDIR}/bin"
|
|
305
|
+
mkdir -p "$STUB_BIN"
|
|
306
|
+
cat > "${STUB_BIN}/claude" <<'STUB'
|
|
307
|
+
#!/usr/bin/env bash
|
|
308
|
+
prompt=$(cat)
|
|
309
|
+
# Stub-selector markers are checked before the generic "module-runner"
|
|
310
|
+
# content match: several fixtures embed real vitest/vite prose (which
|
|
311
|
+
# contains "module-runner") alongside their marker, and the marker names
|
|
312
|
+
# the intended stub behavior.
|
|
313
|
+
if [[ "$prompt" == *"no-resolution-stub"* ]]; then
|
|
314
|
+
printf '%s' '{"eligible":false,"reason":"no_resolution"}'
|
|
315
|
+
elif [[ "$prompt" == *"no-versions-stub"* ]]; then
|
|
316
|
+
printf '%s' '{"eligible":false,"reason":"no_versions"}'
|
|
317
|
+
elif [[ "$prompt" == *"npm-range-stub"* ]]; then
|
|
318
|
+
printf '%s' '{"claim":"c","rationale":"r","evidence":{"resolution":"fix"},"applies_to":{"stack":["vite"],"scope":{"kind":"versioned","versions":{"vite":"^5.4.21"}},"file_patterns":[],"task_kinds":[]}}'
|
|
319
|
+
elif [[ "$prompt" == *"module-runner"* ]]; then
|
|
320
|
+
printf '%s' '{"claim":"Vitest 4 cannot import vite/module-runner on Vite 5","rationale":"vite/module-runner ships in Vite 6; Vitest 4 assumes it exists.","evidence":{"resolution":"Pin vitest to 3.x until Vite 6 lands."},"applies_to":{"stack":["vite","vitest"],"scope":{"kind":"versioned","versions":{"vite":"<6","vitest":">=4"}},"file_patterns":[],"task_kinds":[]}}'
|
|
321
|
+
else
|
|
322
|
+
printf '%s' 'not json at all'
|
|
323
|
+
fi
|
|
324
|
+
STUB
|
|
325
|
+
chmod +x "${STUB_BIN}/claude"
|
|
326
|
+
export PATH="${STUB_BIN}:${PATH}"
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
_seed() {
|
|
330
|
+
jq -cn --arg id "$1" --arg s "$2" --arg d "$3" --arg k "$PROJECT_KEY" \
|
|
331
|
+
'{id: $id, kind: "decision", project_key: $k, session_id: "sess-1",
|
|
332
|
+
created_at: "2026-08-03T15:59:48Z", summary: $s, detail: $d}'
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@test "transform_one proposes a candidate for a groundable artifact" {
|
|
336
|
+
_transform_setup
|
|
337
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Vitest 4.1.9 / Vite 5.x mismatch" \
|
|
338
|
+
"Vitest 4.1.9 imports vite/module-runner which is absent in Vite 5.4.21.")
|
|
339
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
340
|
+
[ "$status" -eq 0 ]
|
|
341
|
+
[[ "$output" == proposed:* ]]
|
|
342
|
+
id="${output#proposed:}"
|
|
343
|
+
jq -e '.candidate.applies_to.scope.versions.vite == "<6"
|
|
344
|
+
and .candidate.applies_to.scope.versions.vitest == ">=4"' \
|
|
345
|
+
"${LESSONS_DIR}/proposals/${id}.json"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@test "transform_one declines the real vitest artifact for having no resolution" {
|
|
349
|
+
_transform_setup
|
|
350
|
+
# The artifact that motivated the pipeline. Its session ended on an open
|
|
351
|
+
# question — the fix was never found — so it cannot become a lesson.
|
|
352
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" \
|
|
353
|
+
"no-resolution-stub: Vitest 4.1.9 / Vite 5.x mismatch confirmed as real, blocking bug." \
|
|
354
|
+
"Running pnpm test reproduces failures. Vitest 4.1.9 attempts to import vite/module-runner which does not exist in Vite 5.4.21.")
|
|
355
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
356
|
+
[ "$status" -eq 0 ]
|
|
357
|
+
[ "$output" = "declined:no_resolution" ]
|
|
358
|
+
tail -n 1 "${LESSONS_DIR}/declined.jsonl" | jq -e '.reason == "no_resolution"'
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
@test "transform_one declines when the model cannot infer versions" {
|
|
362
|
+
_transform_setup
|
|
363
|
+
art=$(_seed "01KZ45MKGQ7QZWMABQ4H12SHSV" "no-versions-stub: 5.4 something" "detail 1.2")
|
|
364
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
365
|
+
[ "$output" = "declined:no_versions" ]
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
@test "transform_one declines unparseable model output as transform_invalid" {
|
|
369
|
+
_transform_setup
|
|
370
|
+
art=$(_seed "01KZ45MKME229J0QK0690TREAB" "garbage 1.0" "detail 2.0")
|
|
371
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
372
|
+
[ "$output" = "declined:transform_invalid" ]
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@test "transform_one declines an npm-style range as schema_invalid" {
|
|
376
|
+
_transform_setup
|
|
377
|
+
art=$(_seed "01KZ45MKS84KPZQNWC02Z8FE0K" "npm-range-stub 5.4" "detail 1.0")
|
|
378
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
379
|
+
[ "$output" = "declined:schema_invalid" ]
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@test "transform_one skips a version-free artifact without touching the ledger" {
|
|
383
|
+
_transform_setup
|
|
384
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Prefer functional patterns" "User said so.")
|
|
385
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
386
|
+
[ "$output" = "skipped:pregate" ]
|
|
387
|
+
[ ! -f "${LESSONS_DIR}/declined.jsonl" ]
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
@test "a missing claude CLI is not a verdict and writes nothing" {
|
|
391
|
+
_transform_setup
|
|
392
|
+
rm -f "${STUB_BIN}/claude"
|
|
393
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Vitest 4.1.9 / Vite 5.x mismatch" \
|
|
394
|
+
"Vitest 4.1.9 imports vite/module-runner absent in Vite 5.4.21.")
|
|
395
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
396
|
+
[ "$status" -eq 0 ]
|
|
397
|
+
[ "$output" = "unavailable" ]
|
|
398
|
+
[ ! -f "${LESSONS_DIR}/declined.jsonl" ]
|
|
399
|
+
[ -z "$(ls -A "${LESSONS_DIR}/proposals")" ]
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
@test "an empty model response is not a verdict and writes nothing" {
|
|
403
|
+
_transform_setup
|
|
404
|
+
cat > "${STUB_BIN}/claude" <<'STUB'
|
|
405
|
+
#!/usr/bin/env bash
|
|
406
|
+
cat >/dev/null
|
|
407
|
+
printf ''
|
|
408
|
+
STUB
|
|
409
|
+
chmod +x "${STUB_BIN}/claude"
|
|
410
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Vitest 4.1.9 / Vite 5.x" "module-runner missing in 5.4.21")
|
|
411
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
412
|
+
[ "$output" = "unavailable" ]
|
|
413
|
+
[ ! -f "${LESSONS_DIR}/declined.jsonl" ]
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
@test "an already-declined artifact is not sent to the model a second time" {
|
|
417
|
+
_transform_setup
|
|
418
|
+
librarian_lesson_append_declined "$PROJECT_KEY" "01KZ45MKAM734ZS7JK24D2DK0R" "no_resolution"
|
|
419
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Vitest 4.1.9 / Vite 5.x" "module-runner missing in 5.4.21")
|
|
420
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
421
|
+
[ "$output" = "skipped:seen" ]
|
|
422
|
+
[ "$(wc -l < "${LESSONS_DIR}/declined.jsonl")" -eq 1 ]
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# ----------------------------------------------------------------------------
|
|
426
|
+
# Review round 1 fixes: config-driven timeout, tightened provenance
|
|
427
|
+
# validation, prose-tolerant JSON extraction, and the gaps a wrong
|
|
428
|
+
# implementation could slip through undetected.
|
|
429
|
+
# ----------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
@test "the reason clamp discards a reason slug the model invented" {
|
|
432
|
+
# Deleting the clamp in librarian_lesson_transform_one lets an arbitrary
|
|
433
|
+
# model-supplied reason string enter the permanent ledger unchecked.
|
|
434
|
+
_transform_setup
|
|
435
|
+
cat > "${STUB_BIN}/claude" <<'STUB'
|
|
436
|
+
#!/usr/bin/env bash
|
|
437
|
+
cat >/dev/null
|
|
438
|
+
printf '%s' '{"eligible":false,"reason":"model_made_this_up"}'
|
|
439
|
+
STUB
|
|
440
|
+
chmod +x "${STUB_BIN}/claude"
|
|
441
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Vitest 4.1.9 / Vite 5.x" "module-runner missing in 5.4.21")
|
|
442
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
443
|
+
[ "$status" -eq 0 ]
|
|
444
|
+
[ "$output" = "declined:transform_invalid" ]
|
|
445
|
+
tail -n 1 "${LESSONS_DIR}/declined.jsonl" | jq -e '.reason == "transform_invalid"'
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
@test "transform_one stitches all four provenance fields, not just versions" {
|
|
449
|
+
# Swapping session_id/artifact_id or using the wrong timestamp source
|
|
450
|
+
# passed every other assertion in this file before this test existed.
|
|
451
|
+
#
|
|
452
|
+
# The two bare assertions below are chained with && into one statement
|
|
453
|
+
# rather than left as separate lines: under the macOS system bash (3.2)
|
|
454
|
+
# that `bats` resolves via `#!/usr/bin/env bash`, a bare `[[ ]]` that
|
|
455
|
+
# fails and isn't the test's last statement does not abort the test —
|
|
456
|
+
# only a `&&`/`||` chain's own short-circuited exit status reliably gates
|
|
457
|
+
# regardless of bash version. See ecosystem-75a.
|
|
458
|
+
_transform_setup
|
|
459
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Vitest 4.1.9 / Vite 5.x mismatch" \
|
|
460
|
+
"Vitest 4.1.9 imports vite/module-runner which is absent in Vite 5.4.21.")
|
|
461
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
462
|
+
id="${output#proposed:}"
|
|
463
|
+
[ "$status" -eq 0 ] \
|
|
464
|
+
&& [[ "$output" == proposed:* ]] \
|
|
465
|
+
&& jq -e --arg pk "$PROJECT_KEY" '
|
|
466
|
+
.candidate.evidence.artifact_ids == ["01KZ45MKAM734ZS7JK24D2DK0R"]
|
|
467
|
+
and .candidate.evidence.session_ids == ["sess-1"]
|
|
468
|
+
and .candidate.evidence.project_key == $pk
|
|
469
|
+
and .candidate.evidence.observed_at == "2026-08-03T15:59:48Z"
|
|
470
|
+
' "${LESSONS_DIR}/proposals/${id}.json"
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
@test "build_prompt states there is no version-independent option and forbids npm ranges" {
|
|
474
|
+
# All three checks are chained with && into one statement — see the note
|
|
475
|
+
# on the provenance-stitching test above: a bare, non-last `[[ ]]` does
|
|
476
|
+
# not gate the test under the bash 3.2 that `bats` resolves on this
|
|
477
|
+
# machine. Only the third check was originally last, so deleting the
|
|
478
|
+
# "no version-independent option" paragraph from build_prompt passed
|
|
479
|
+
# this test as first written — the check for it ran, failed, and the
|
|
480
|
+
# failure was silently swallowed.
|
|
481
|
+
_transform_setup
|
|
482
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Vitest 4.1.9 / Vite 5.x" "module-runner missing in 5.4.21")
|
|
483
|
+
prompt=$(librarian_lesson_build_prompt "$art")
|
|
484
|
+
[[ "$prompt" == *"no version-independent option"* ]] \
|
|
485
|
+
&& [[ "$prompt" == *'"^5.4.21"'* ]] \
|
|
486
|
+
&& [[ "$prompt" == *'">=0"'* ]]
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
@test "transform_one declines a provenance-less artifact instead of writing invalid evidence" {
|
|
490
|
+
# archivist-extract.sh writes session_id: null when the hook payload
|
|
491
|
+
# lacked one. That must not silently become evidence.session_ids: [""] in
|
|
492
|
+
# a written proposal — it must fail validation like any other schema
|
|
493
|
+
# violation, not get buried as though it had been judged and approved.
|
|
494
|
+
_transform_setup
|
|
495
|
+
art=$(jq -cn --arg id "01KZ45MKAM734ZS7JK24D2DK0R" --arg k "$PROJECT_KEY" \
|
|
496
|
+
'{id: $id, kind: "decision", project_key: $k, session_id: null,
|
|
497
|
+
created_at: "2026-08-03T15:59:48Z",
|
|
498
|
+
summary: "Vitest 4.1.9 / Vite 5.x mismatch",
|
|
499
|
+
detail: "Vitest 4.1.9 imports vite/module-runner which is absent in Vite 5.4.21."}')
|
|
500
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
501
|
+
[ "$status" -eq 0 ]
|
|
502
|
+
[ "$output" = "declined:schema_invalid" ]
|
|
503
|
+
[ -z "$(ls -A "${LESSONS_DIR}/proposals")" ]
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
@test "transform_one recovers a candidate wrapped in prose instead of declining it as unparseable" {
|
|
507
|
+
_transform_setup
|
|
508
|
+
cat > "${STUB_BIN}/claude" <<'STUB'
|
|
509
|
+
#!/usr/bin/env bash
|
|
510
|
+
cat >/dev/null
|
|
511
|
+
printf '%s' 'Here is the JSON you requested: {"claim":"Vitest 4 cannot import vite/module-runner on Vite 5","rationale":"vite/module-runner ships in Vite 6; Vitest 4 assumes it exists.","evidence":{"resolution":"Pin vitest to 3.x until Vite 6 lands."},"applies_to":{"stack":["vite","vitest"],"scope":{"kind":"versioned","versions":{"vite":"<6","vitest":">=4"}},"file_patterns":[],"task_kinds":[]}} Hope that helps!'
|
|
512
|
+
STUB
|
|
513
|
+
chmod +x "${STUB_BIN}/claude"
|
|
514
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Vitest 4.1.9 / Vite 5.x mismatch" \
|
|
515
|
+
"Vitest 4.1.9 imports vite/module-runner which is absent in Vite 5.4.21.")
|
|
516
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
517
|
+
[ "$status" -eq 0 ]
|
|
518
|
+
[[ "$output" == proposed:* ]]
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
@test "librarian_lesson_call reads timeout_seconds from config instead of hardcoding it" {
|
|
522
|
+
_transform_setup
|
|
523
|
+
mkdir -p "${PROJECT_REPO}/.claude"
|
|
524
|
+
printf '%s\n' '{"librarian":{"lesson_transform":{"timeout_seconds":7}}}' \
|
|
525
|
+
> "${PROJECT_REPO}/.claude/settings.json"
|
|
526
|
+
librarian_config_load "$PROJECT_REPO"
|
|
527
|
+
|
|
528
|
+
TIMEOUT_CAPTURE="${BATS_TEST_TMPDIR}/timeout-seconds-used"
|
|
529
|
+
export TIMEOUT_CAPTURE
|
|
530
|
+
cat > "${STUB_BIN}/timeout" <<'STUB'
|
|
531
|
+
#!/usr/bin/env bash
|
|
532
|
+
printf '%s' "$1" > "${TIMEOUT_CAPTURE}"
|
|
533
|
+
shift
|
|
534
|
+
exec "$@"
|
|
535
|
+
STUB
|
|
536
|
+
chmod +x "${STUB_BIN}/timeout"
|
|
537
|
+
|
|
538
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Vitest 4.1.9 / Vite 5.x mismatch" \
|
|
539
|
+
"Vitest 4.1.9 imports vite/module-runner which is absent in Vite 5.4.21.")
|
|
540
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
541
|
+
[ "$status" -eq 0 ] \
|
|
542
|
+
&& [ "$(cat "$TIMEOUT_CAPTURE")" = "7" ]
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
@test "a pathologically large response terminates quickly instead of hanging on extraction" {
|
|
546
|
+
# _librarian_lesson_extract_json_object is a per-character bash scan —
|
|
547
|
+
# effectively O(n^2) on long input. It runs after the claude call,
|
|
548
|
+
# uncapped, inside a SessionEnd hook that must not stall session end, and
|
|
549
|
+
# nothing bounds the model's response size (claude -p has no such flag).
|
|
550
|
+
# This response has no brace anywhere, so recovery is impossible and the
|
|
551
|
+
# correct outcome is declined:transform_invalid — the test asserts that
|
|
552
|
+
# verdict is reached well inside a 5s budget, not just that it's eventually
|
|
553
|
+
# reached at all. 40k chars is sized to fail loudly (tens of seconds, not
|
|
554
|
+
# a borderline few) if the bound in librarian_lesson_call is ever removed,
|
|
555
|
+
# rather than flake near the threshold.
|
|
556
|
+
_transform_setup
|
|
557
|
+
cat > "${STUB_BIN}/claude" <<'STUB'
|
|
558
|
+
#!/usr/bin/env bash
|
|
559
|
+
cat >/dev/null
|
|
560
|
+
printf '%s' 'not json, just prose. '
|
|
561
|
+
printf '%*s' 40000 '' | tr ' ' 'x'
|
|
562
|
+
STUB
|
|
563
|
+
chmod +x "${STUB_BIN}/claude"
|
|
564
|
+
art=$(_seed "01KZ45MKAM734ZS7JK24D2DK0R" "Vitest 4.1.9 / Vite 5.x mismatch" \
|
|
565
|
+
"Vitest 4.1.9 imports vite/module-runner which is absent in Vite 5.4.21.")
|
|
566
|
+
|
|
567
|
+
start=$(date +%s)
|
|
568
|
+
run librarian_lesson_transform_one "$PROJECT_KEY" "$art"
|
|
569
|
+
end=$(date +%s)
|
|
570
|
+
elapsed=$((end - start))
|
|
571
|
+
|
|
572
|
+
[ "$status" -eq 0 ] \
|
|
573
|
+
&& [ "$output" = "declined:transform_invalid" ] \
|
|
574
|
+
&& [ "$elapsed" -lt 5 ]
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
# ----------------------------------------------------------------------------
|
|
578
|
+
# SessionEnd wiring: the hook runs the lesson stage over the same durability
|
|
579
|
+
# survivors the classifier saw and lands a proposal on disk.
|
|
580
|
+
# ----------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
@test "SessionEnd runs the lesson stage and lands a candidate on disk" {
|
|
583
|
+
_transform_setup
|
|
584
|
+
|
|
585
|
+
HOOK="${PLUGIN_ROOT}/scripts/hooks/librarian-session-end.sh"
|
|
586
|
+
ARCHIVIST_DIR="${ONLOOKER_DIR}/archivist/${PROJECT_KEY}"
|
|
587
|
+
mkdir -p "${ARCHIVIST_DIR}/decisions"
|
|
588
|
+
|
|
589
|
+
created_at=$(relative_iso_days_ago 1)
|
|
590
|
+
# The detail carries "because" so this fixture clears the durability
|
|
591
|
+
# filter's marker-phrase gate (plugins/librarian/scripts/lib/
|
|
592
|
+
# librarian-durability.sh) — a plain version-mismatch summary with no
|
|
593
|
+
# marker phrase gets dropped as filter_marker_missing before it ever
|
|
594
|
+
# reaches $KEPT, regardless of whether the lesson stage runs.
|
|
595
|
+
jq -n --arg id "01KZ45MKAM734ZS7JK24D2DK0R" --arg at "$created_at" --arg k "$PROJECT_KEY" \
|
|
596
|
+
'{id: $id, kind: "decision", project_key: $k, session_id: "sess-1",
|
|
597
|
+
created_at: $at, updated_at: $at,
|
|
598
|
+
summary: "Vitest 4.1.9 / Vite 5.x mismatch, decided to pin",
|
|
599
|
+
detail: "Vitest 4.1.9 imports vite/module-runner which is absent in Vite 5.4.21, because Vite 6 has not shipped yet.",
|
|
600
|
+
files: ["packages/db"]}' \
|
|
601
|
+
> "${ARCHIVIST_DIR}/decisions/01KZ45MKAM734ZS7JK24D2DK0R.json"
|
|
602
|
+
|
|
603
|
+
input=$(jq -cn --arg cwd "$PROJECT_REPO" \
|
|
604
|
+
'{cwd: $cwd, session_id: "sess-1", hook_event_name: "SessionEnd"}')
|
|
605
|
+
run bash -c "printf '%s' '$input' | '$HOOK'"
|
|
606
|
+
|
|
607
|
+
[ "$status" -eq 0 ]
|
|
608
|
+
[ -n "$(ls -A "${LESSONS_DIR}/proposals" 2>/dev/null)" ]
|
|
609
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
|
|
5
|
+
const dir = 'plugins/librarian/schema';
|
|
6
|
+
const read = (f) => JSON.parse(readFileSync(`${dir}/${f}`, 'utf8'));
|
|
7
|
+
|
|
8
|
+
test('provenance pins schema_version 2', () => {
|
|
9
|
+
assert.equal(read('PROVENANCE.json').schema_version, 2);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('applies_to keeps a two-branch scope union', () => {
|
|
13
|
+
const appliesTo = read('lesson-applies-to.subschema.json');
|
|
14
|
+
const branches = appliesTo.properties.scope.oneOf;
|
|
15
|
+
assert.equal(branches.length, 2);
|
|
16
|
+
assert.deepEqual(branches.map((b) => b.properties.kind.const).sort(), ['version_independent', 'versioned']);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('the versioned branch requires at least one version and carries the range pattern', () => {
|
|
20
|
+
const appliesTo = read('lesson-applies-to.subschema.json');
|
|
21
|
+
const versioned = appliesTo.properties.scope.oneOf.find((b) => b.properties.kind.const === 'versioned');
|
|
22
|
+
assert.equal(versioned.properties.versions.minProperties, 1);
|
|
23
|
+
assert.ok(versioned.properties.versions.additionalProperties.pattern);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('evidence requires a resolution', () => {
|
|
27
|
+
assert.ok(read('lesson-evidence.subschema.json').required.includes('resolution'));
|
|
28
|
+
});
|