@gamaze/hicortex 0.15.3 → 0.16.1

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.
@@ -55,6 +55,29 @@
55
55
  * badly post-shift. Report written to data/eval-recall-sweep/report.md and
56
56
  * printed to stdout.
57
57
  *
58
+ * ## Section 4 — SCOPE dimension (#203 soft project affinity)
59
+ *
60
+ * A THIRD family on a SEPARATE project-labeled corpus (the blend-sweep corpus
61
+ * above has no project labels). Goal: prove #203's soft project affinity drops
62
+ * cross-scope noise (the boat/battery case — a "hardware" query surfacing
63
+ * "marine" memories on a shared token like "battery") below the cap WITHOUT a
64
+ * hard filter, and WITHOUT losing same-scope recall.
65
+ *
66
+ * Corpus: 6 hardware + 6 marine memories. 3 marine memories are
67
+ * CONTAMINATION SEEDS (share a token with the hardware queries); 3 are
68
+ * marine-only fillers (control).
69
+ *
70
+ * Each hardware query runs TWICE on the same static DB:
71
+ * scope OFF — no `project` sent (byte-identical to pre-#203).
72
+ * scope ON — `project: "hardware"` (computeScore adds +projectAffinity 0.15
73
+ * to hardware memories; marine gets 0; no hard filter).
74
+ *
75
+ * Metrics: contamination@5 (marine in top-5 / 5 — LOWER is better),
76
+ * recall@5 (gold hardware memory surfaced), sameScope@5 (hardware in top-5).
77
+ * Scope is orthogonal to the blend, so the blend weight is held at 0 here —
78
+ * `project` is the ONLY variable. Same invariants (noStrengthen, real
79
+ * embedder, uniform metadata, neverCalledEmbed self-check).
80
+ *
58
81
  * Run: npm run eval:recall-sweep (== node dist/eval/recall-sweep.js)
59
82
  */
60
83
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -301,6 +324,81 @@ const SHIFT_PAIRS = [
301
324
  ["deploy", "testing"],
302
325
  ];
303
326
  const WEIGHTS = [0.0, 0.2, 0.4, 0.5, 0.6, 0.8];
327
+ const SCOPE_CORPUS = [
328
+ // --- hardware (6): the same-scope / gold memories ---
329
+ {
330
+ project: "hardware",
331
+ text: "Lithium-ion battery pack drains 3% per day on standby. Traced to a leaky protection-circuit MOSFET that draws 50mA while the device is powered off.",
332
+ },
333
+ {
334
+ project: "hardware",
335
+ text: "5V power supply brown-out: the rail drooped to 4.2V under peak load. The bulk capacitor was undersized; added a second decoupling stage.",
336
+ },
337
+ {
338
+ project: "hardware",
339
+ text: "Charge controller IC never enters constant-voltage mode — the battery overcharges because the CC/CV transition threshold was misconfigured in firmware.",
340
+ },
341
+ {
342
+ project: "hardware",
343
+ text: "ADC battery voltage reading was 40% off: the resistor divider ratio was wrong, so the fuel gauge reported a full pack at 60%. Recomputed and recalibrated.",
344
+ },
345
+ {
346
+ project: "hardware",
347
+ text: "Battery thermal runaway during charging: the NTC thermistor lead was cold-soldered, disabling temperature compensation. Reflowed the joint.",
348
+ },
349
+ {
350
+ project: "hardware",
351
+ text: "Current-sense shunt drifted under load: the 10 milli-ohm resistor heated and its temperature coefficient skewed the reading. Switched to a low-TCR manganin part.",
352
+ },
353
+ // --- marine SEEDS (3): share a token (battery / drain / voltage / charge /
354
+ // temperature) with the hardware queries — a naive keyword/vector search
355
+ // surfaces these for a hardware query. ---
356
+ {
357
+ project: "marine",
358
+ seed: true,
359
+ text: "Boat battery drained overnight at anchor: the bilge pump ran continuously because the float switch stuck closed. Replaced the switch.",
360
+ },
361
+ {
362
+ project: "marine",
363
+ seed: true,
364
+ text: "Marine house-bank voltage drop: the battery isolator had corroded terminals, dropping 0.8V under load. Cleaned and greased the lugs.",
365
+ },
366
+ {
367
+ project: "marine",
368
+ seed: true,
369
+ text: "Trolling-motor battery won't reach full charge: the on-board charger undercharges the deep-cycle bank because the temperature-compensation sense lead is on the wrong terminal.",
370
+ },
371
+ // --- marine FILLERS (3): no token overlap with any hardware query. Control
372
+ // row — these should never surface in either mode, and if they do it is a
373
+ // ranking bug, not a scope issue. ---
374
+ {
375
+ project: "marine",
376
+ text: "Jib furler jammed: the halyard wrapped on the swivel. Fitted a halyard deflector and re-led the furling line.",
377
+ },
378
+ {
379
+ project: "marine",
380
+ text: "Chartplotter took eight minutes to a GPS fix: the antenna was under the fiberglass hard-top. Relocated it to the T-top rail.",
381
+ },
382
+ {
383
+ project: "marine",
384
+ text: "Ablative antifouling wore thin below the waterline after 18 months. Hauled the boat, barrier-coated, and repainted.",
385
+ },
386
+ ];
387
+ /**
388
+ * 5 hardware-scoped queries. Q1-Q4 each have a marine contamination seed
389
+ * (varying strength: Q1 near-identical, Q3 antonym-semantic, Q4 token-exact);
390
+ * Q5 is the CONTROL — no marine memory shares its tokens, so contamination
391
+ * should be ~0 in both modes (proves the metric isn't trivially inflated).
392
+ */
393
+ const SCOPE_QUERIES = [
394
+ { prompt: "battery drain overnight", goldIndices: [0], expect: "strong (M6 near-identical)" },
395
+ { prompt: "power supply voltage drop", goldIndices: [1], expect: "moderate (M7 voltage)" },
396
+ { prompt: "battery overcharging", goldIndices: [2], expect: "weak (M8 antonym)" },
397
+ { prompt: "battery temperature compensation", goldIndices: [4], expect: "strong (M8 token)" },
398
+ { prompt: "current shunt drift", goldIndices: [5], expect: "control (none)" },
399
+ ];
400
+ const SCOPE_K = 5;
401
+ const SCOPE_PROJECT = "hardware";
304
402
  function buildSessions() {
305
403
  const focused = TOPICS.map((t) => ({
306
404
  kind: "focused",
@@ -340,6 +438,32 @@ async function buildCorpusDb(dbPath) {
340
438
  }
341
439
  return { db, idToTopic };
342
440
  }
441
+ /**
442
+ * Build the SCOPE corpus DB (separate from the blend-sweep corpus — the scope
443
+ * corpus carries project labels the sweep corpus does not). Same honesty
444
+ * controls: real embedder, uniform metadata (base_strength=0.5, created_at~now,
445
+ * no links). The ONLY addition vs the sweep corpus is `project`, which is the
446
+ * #203 scope label computeScore reads.
447
+ *
448
+ * Returns `ids` in corpus order so SCOPE_QUERIES goldIndices map to ids.
449
+ */
450
+ async function buildScopeDb(dbPath) {
451
+ const db = (0, db_js_1.initDb)(dbPath);
452
+ const idToProject = new Map();
453
+ const ids = [];
454
+ for (const mem of SCOPE_CORPUS) {
455
+ const vec = await (0, embedder_js_1.embed)(mem.text);
456
+ const id = storage.insertMemory(db, mem.text, vec, {
457
+ sourceAgent: "eval-scope",
458
+ memoryType: "episode",
459
+ baseStrength: 0.5, // uniform — strength is NOT a discriminator here
460
+ project: mem.project, // #203 scope label
461
+ });
462
+ idToProject.set(id, mem.project);
463
+ ids.push(id);
464
+ }
465
+ return { db, idToProject, ids };
466
+ }
343
467
  /** Embed every distinct prompt once; return a cache keyed by prompt text. */
344
468
  async function embedAllPrompts(sessions) {
345
469
  const cache = new Map();
@@ -649,6 +773,147 @@ function renderReport(focused, shifts, records, sessions, meta) {
649
773
  L.push("");
650
774
  return L.join("\n");
651
775
  }
776
+ /**
777
+ * Run the scope sweep: each hardware query is run TWICE on the same static DB —
778
+ * scope OFF (no `project` sent, byte-identical to pre-#203) and scope ON
779
+ * (`project: "hardware"`). The #203 affinity boosts hardware memories; marine
780
+ * memories get nothing. No hard filter anywhere.
781
+ *
782
+ * Scope is ORTHOGONAL to the session-intent blend, so the blend weight is held
783
+ * at 0 (pure prompt embedding) — the ONLY variable is whether `project` is in
784
+ * options. `queryEmbedding` is supplied (pure prompt), so retrieve() must never
785
+ * call the embedFn; the `neverCalledEmbed` self-check enforces that.
786
+ */
787
+ async function runScopeSweep(db, idToProject, ids) {
788
+ // Precompute query embeddings once — pure prompt, no centroid (scope is the
789
+ // only variable; blend held at 0 to isolate it).
790
+ const queryEmb = new Map();
791
+ for (const q of SCOPE_QUERIES) {
792
+ if (!queryEmb.has(q.prompt))
793
+ queryEmb.set(q.prompt, await (0, embedder_js_1.embed)(q.prompt));
794
+ }
795
+ const neverCalledEmbed = async () => {
796
+ throw new Error("recall-sweep (scope): retrieve() called the embedFn — queryEmbedding was not honored. Eval aborted (results would be invalid).");
797
+ };
798
+ const records = [];
799
+ for (let qi = 0; qi < SCOPE_QUERIES.length; qi++) {
800
+ const q = SCOPE_QUERIES[qi];
801
+ const gold = new Set(q.goldIndices.map((i) => ids[i]));
802
+ const promptVec = queryEmb.get(q.prompt);
803
+ for (const mode of ["off", "on"]) {
804
+ const results = await (0, retrieval_js_1.retrieve)(db, neverCalledEmbed, q.prompt, {
805
+ limit: SCOPE_K,
806
+ queryEmbedding: promptVec,
807
+ noStrengthen: true,
808
+ // OFF: omit project entirely (byte-identical to pre-#203).
809
+ // ON: send project — computeScore adds +projectAffinity (0.15) to
810
+ // every hardware memory; marine memories get 0.
811
+ ...(mode === "on" ? { project: SCOPE_PROJECT } : {}),
812
+ });
813
+ const topIds = results.map((r) => r.id);
814
+ const marineCount = topIds.filter((id) => idToProject.get(id) === "marine").length;
815
+ const hardwareCount = topIds.filter((id) => idToProject.get(id) === "hardware").length;
816
+ const goldCount = topIds.filter((id) => gold.has(id)).length;
817
+ records.push({
818
+ queryLabel: `Q${qi + 1}`,
819
+ prompt: q.prompt,
820
+ mode,
821
+ recallAt5: goldCount > 0 ? 1 : 0,
822
+ goldP5: goldCount,
823
+ sameScopeP5: hardwareCount,
824
+ contaminationAt5: marineCount / SCOPE_K,
825
+ topIds,
826
+ });
827
+ }
828
+ }
829
+ return records;
830
+ }
831
+ function meanScope(rs, field) {
832
+ if (rs.length === 0)
833
+ return 0;
834
+ return rs.reduce((s, r) => s + r[field], 0) / rs.length;
835
+ }
836
+ function renderScopeSection(records) {
837
+ const L = [];
838
+ L.push("## 4. Scope contamination (#203 soft project affinity)\n");
839
+ L.push(`Separate synthetic corpus (project-labeled): ${SCOPE_CORPUS.filter((m) => m.project === "hardware").length} hardware + ` +
840
+ `${SCOPE_CORPUS.filter((m) => m.project === "marine").length} marine memories. ` +
841
+ `${SCOPE_CORPUS.filter((m) => m.project === "marine" && m.seed).length} marine memories are CONTAMINATION SEEDS ` +
842
+ `(share a token — battery / drain / voltage / charge / temperature — with the hardware queries); ` +
843
+ `${SCOPE_CORPUS.filter((m) => m.project === "marine" && !m.seed).length} are marine-only fillers (no token overlap — control). ` +
844
+ `Each hardware query runs TWICE: scope OFF (no \`project\` sent — byte-identical to pre-#203) and scope ON (\`project: "${SCOPE_PROJECT}"\`). ` +
845
+ `The #203 affinity adds +projectAffinity (0.15) to hardware memories in computeScore; marine memories get 0. No hard filter anywhere.\n`);
846
+ L.push("_Same invariants as the blend sweep: static DB (`noStrengthen: true`), real bge-small-en-v1.5 embedder, " +
847
+ "uniform metadata (base_strength=0.5, created_at~now, no links). Blend weight held at 0 (pure prompt) — " +
848
+ "scope is orthogonal to session-intent keying. The `neverCalledEmbed` self-check still passes._\n");
849
+ // ---- Per-query table ----
850
+ L.push("### Per-query results (scope OFF vs ON)\n");
851
+ L.push("contamination@5 = marine memories in top-5 / 5 (**LOWER is better**). recall@5 = the gold hardware memory surfaced (binary). " +
852
+ "sameScope@5 = hardware memories in top-5 (incl. non-gold). goldP@5 = gold count in top-5.\n");
853
+ L.push("| # | query | expected | mode | contamination@5 | recall@5 | sameScope@5 | goldP@5 |");
854
+ L.push("|---|---|---|---|---|---|---|---|");
855
+ for (let qi = 0; qi < SCOPE_QUERIES.length; qi++) {
856
+ const off = records.find((r) => r.queryLabel === `Q${qi + 1}` && r.mode === "off");
857
+ const on = records.find((r) => r.queryLabel === `Q${qi + 1}` && r.mode === "on");
858
+ const q = SCOPE_QUERIES[qi];
859
+ L.push(`| Q${qi + 1} | \`${q.prompt}\` | ${q.expect} | OFF | ${off.contaminationAt5.toFixed(2)} | ${off.recallAt5} | ${off.sameScopeP5} | ${off.goldP5} |`);
860
+ L.push(`| | | | **ON** | **${on.contaminationAt5.toFixed(2)}** | **${on.recallAt5}** | **${on.sameScopeP5}** | **${on.goldP5}** |`);
861
+ }
862
+ L.push("");
863
+ // ---- Aggregate ----
864
+ const offRecs = records.filter((r) => r.mode === "off");
865
+ const onRecs = records.filter((r) => r.mode === "on");
866
+ const contamOff = meanScope(offRecs, "contaminationAt5");
867
+ const contamOn = meanScope(onRecs, "contaminationAt5");
868
+ const recallOff = meanScope(offRecs, "recallAt5");
869
+ const recallOn = meanScope(onRecs, "recallAt5");
870
+ const sameOff = meanScope(offRecs, "sameScopeP5");
871
+ const sameOn = meanScope(onRecs, "sameScopeP5");
872
+ L.push("### Aggregate (mean over all 5 queries)\n");
873
+ L.push("| mode | contamination@5 | recall@5 (gold) | sameScope@5 (hardware) |");
874
+ L.push("|---|---|---|---|");
875
+ L.push(`| OFF (no project) | ${contamOff.toFixed(3)} | ${recallOff.toFixed(3)} | ${sameOff.toFixed(2)} |`);
876
+ L.push(`| ON (project=${SCOPE_PROJECT}) | ${contamOn.toFixed(3)} | ${recallOn.toFixed(3)} | ${sameOn.toFixed(2)} |`);
877
+ L.push("");
878
+ // ---- Verdict ----
879
+ const contamDrop = contamOff - contamOn;
880
+ const recallRegress = recallOn < recallOff - 1e-9;
881
+ const contamMaterial = contamDrop > 0.01; // any measurable mean drop = material
882
+ const pass = contamMaterial && !recallRegress;
883
+ L.push("### Scope verdict (PASS criterion: contamination drops materially AND same-scope recall does not regress)\n");
884
+ L.push(`- contamination@5: OFF ${contamOff.toFixed(3)} → ON ${contamOn.toFixed(3)} ` +
885
+ `(${contamDrop >= 0 ? "−" : "+"}${Math.abs(contamDrop).toFixed(3)}, ${pct(Math.abs(contamDrop))} absolute)`);
886
+ L.push(`- recall@5 (gold surfaced): OFF ${recallOff.toFixed(3)} → ON ${recallOn.toFixed(3)} ` +
887
+ `${recallRegress ? "**(REGRESSED — gold hardware memory lost)**" : "(no regression)"}`);
888
+ L.push(`- sameScope@5 (hardware in top-5): OFF ${sameOff.toFixed(2)} → ON ${sameOn.toFixed(2)} ` +
889
+ `(${sameOn >= sameOff ? "+" : ""}${(sameOn - sameOff).toFixed(2)})`);
890
+ let verdict;
891
+ if (pass) {
892
+ const magnitude = contamDrop > 0.15
893
+ ? "strongly"
894
+ : contamDrop > 0.05
895
+ ? "materially"
896
+ : "marginally";
897
+ verdict =
898
+ `**PASS** — soft project affinity ${magnitude} drops cross-scope contamination (−${pct(contamDrop)} absolute) ` +
899
+ `WITHOUT losing same-scope recall. The #203 change is sufficient on this corpus; no hard filter needed.`;
900
+ }
901
+ else if (!contamMaterial) {
902
+ verdict =
903
+ `**FAIL (insufficient)** — contamination did NOT drop materially (−${pct(contamDrop)} absolute). ` +
904
+ `This is a real finding: #203's soft affinity alone is INSUFFICIENT here, and the BM25/FTS-driven cases ` +
905
+ `the architect review predicted for #205 (field-weighted BM25F) likely persist — the marine seed wins on ` +
906
+ `raw token overlap that the additive +0.15 cannot overcome.`;
907
+ }
908
+ else {
909
+ verdict =
910
+ `**FAIL (recall regression)** — contamination dropped (−${pct(contamDrop)} absolute) but a gold hardware ` +
911
+ `memory was LOST when scope engaged (recall@5 ${recallOff.toFixed(3)} → ${recallOn.toFixed(3)}). The ` +
912
+ `affinity is somehow suppressing a same-scope gold result, which should not happen (it is additive only).`;
913
+ }
914
+ L.push(`- ${verdict}\n`);
915
+ return L.join("\n");
916
+ }
652
917
  // ---------------------------------------------------------------------------
653
918
  // main
654
919
  // ---------------------------------------------------------------------------
@@ -665,14 +930,18 @@ async function main() {
665
930
  const tmpDir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `hicortex-recall-sweep-${(0, node_crypto_1.randomUUID)().slice(0, 8)}`);
666
931
  (0, node_fs_1.mkdirSync)(tmpDir, { recursive: true });
667
932
  const dbPath = (0, node_path_1.join)(tmpDir, "sweep.db");
933
+ const scopeDbPath = (0, node_path_1.join)(tmpDir, "scope.db");
668
934
  const reportDir = (0, node_path_1.join)(process.cwd(), "data", "eval-recall-sweep");
669
935
  console.log(`[recall-sweep] temp DB: ${dbPath}`);
936
+ console.log(`[recall-sweep] scope DB: ${scopeDbPath}`);
670
937
  console.log(`[recall-sweep] report dir: ${reportDir}`);
671
938
  console.log(`[recall-sweep] ${sessions.filter((s) => s.kind === "focused").length} focused + ` +
672
939
  `${sessions.filter((s) => s.kind === "shift").length} shift sessions; ` +
673
940
  `${WEIGHTS.length} weights = ${sessions.length * WEIGHTS.length} sweeps x 4 turns = ` +
674
- `${sessions.length * WEIGHTS.length * 4} retrieves`);
941
+ `${sessions.length * WEIGHTS.length * 4} retrieves; ` +
942
+ `+ scope sweep = ${SCOPE_QUERIES.length} queries x 2 modes = ${SCOPE_QUERIES.length * 2} retrieves`);
675
943
  let db = null;
944
+ let scopeDb = null;
676
945
  try {
677
946
  console.log("[recall-sweep] building corpus (embedding 30 memories)...");
678
947
  const t0 = Date.now();
@@ -685,9 +954,20 @@ async function main() {
685
954
  console.log(`[recall-sweep] sweep done in ${Date.now() - t1}ms (${records.length} turn records)`);
686
955
  const focused = summarizeFocused(records, sessions);
687
956
  const shifts = summarizeShift(records);
688
- const report = renderReport(focused, shifts, records, sessions, {
957
+ let report = renderReport(focused, shifts, records, sessions, {
689
958
  memoryCount: CORPUS.length,
690
959
  });
960
+ // ---- SCOPE sweep (#203) ----
961
+ console.log(`[recall-sweep] building scope corpus (embedding ${SCOPE_CORPUS.length} project-labeled memories)...`);
962
+ const t2 = Date.now();
963
+ const scopeBuilt = await buildScopeDb(scopeDbPath);
964
+ scopeDb = scopeBuilt.db;
965
+ console.log(`[recall-sweep] scope corpus ready in ${Date.now() - t2}ms (${scopeBuilt.ids.length} memories)`);
966
+ console.log("[recall-sweep] running scope sweep (OFF vs ON)...");
967
+ const t3 = Date.now();
968
+ const scopeRecords = await runScopeSweep(scopeDb, scopeBuilt.idToProject, scopeBuilt.ids);
969
+ console.log(`[recall-sweep] scope sweep done in ${Date.now() - t3}ms (${scopeRecords.length} turn records)`);
970
+ report += "\n\n" + renderScopeSection(scopeRecords);
691
971
  (0, node_fs_1.mkdirSync)(reportDir, { recursive: true });
692
972
  const reportPath = (0, node_path_1.join)(reportDir, "report.md");
693
973
  (0, node_fs_1.writeFileSync)(reportPath, report, "utf-8");
@@ -701,6 +981,12 @@ async function main() {
701
981
  }
702
982
  catch { /* already closed */ }
703
983
  }
984
+ if (scopeDb) {
985
+ try {
986
+ scopeDb.close();
987
+ }
988
+ catch { /* already closed */ }
989
+ }
704
990
  if ((0, node_fs_1.existsSync)(tmpDir)) {
705
991
  try {
706
992
  (0, node_fs_1.rmSync)(tmpDir, { recursive: true, force: true });
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Real-query relevance + SNIPPET eval — recall QUALITY on real agent prompts.
4
+ *
5
+ * v2 (spec `specs/2026-08-02-relevance-eval.md`) extends the v1 selection-only
6
+ * eval with the SNIPPET layer: v1 asked "did retrieve() surface the right
7
+ * memories?" (judge sees up to 2000 chars). Production shows the agent only a
8
+ * ~100-char one-liner (`recall-index.ts#memoryTitle`), so a memory can be
9
+ * genuinely relevant while its rendered line is useless — v1 scored that as a
10
+ * win. v2 grades BOTH: `full_verdict` (selection quality, judge sees full
11
+ * content) and `line_verdict` (snippet quality, judge sees ONLY the rendered
12
+ * production one-liner — imported from `recall-index.ts`, never reimplemented).
13
+ *
14
+ * v2 additions (spec §4, §5, §6, §6b) layered onto the v1 base (prompt
15
+ * sampling, readonly snapshot handling, embed-once + neverCalledEmbed,
16
+ * lenient JSON parse, distribution/CI reporting):
17
+ * 1. Dual verdict per surfaced memory — TWO separate, blind judge calls.
18
+ * 2. Snippet-length sweep (100/200/300/title+1st-sentence) on a fixed
19
+ * 40-prompt subset (8 per source).
20
+ * 3. Similarity-floor + retrieval-source analysis (near-free — logged, not
21
+ * re-judged).
22
+ * 4. Token-cost estimate (char/4) per K and per snippet-length variant.
23
+ * 5. ~20 rendered ACTUAL production blocks dumped into the report.
24
+ * 6. Redundancy — one set-level judge call per (prompt × mode) over the
25
+ * production 6.
26
+ * 7. Rate limiting + resumability (§6b, MANDATORY): serial calls,
27
+ * `--judge-delay-ms` (default 2000), exponential backoff with jitter on
28
+ * 429/5xx/timeout (5→10→20→40→80s, max 5 retries, respects
29
+ * `Retry-After`), checkpoint-per-call to a `.jsonl` sidecar, `--resume`,
30
+ * progress logging, 10%-error-rate abort, `--max-calls` budget guard
31
+ * (default 900).
32
+ *
33
+ * Prompt corpus (spec §2, owner decision §11.1): EVEN split, 20 prompts per
34
+ * source × 5 sources — Hermes (lenny, raider, nano) + CC (the DevOps
35
+ * `infrastructure` project, the `aironic-marine` project). Saved to
36
+ * `data/prompts.json`, stable/reused verbatim once a valid v2 set exists.
37
+ *
38
+ * Judge: GLM-5.2 via z.ai — the INSTRUMENT only. It never picks candidates;
39
+ * retrieve() (LLM-free) does. A dedicated raw HTTP caller (NOT `LlmClient`) is
40
+ * used here on purpose: `LlmClient.completeReflect` bakes in a
41
+ * nightly-tolerant retry policy (30s/60s/120s, unlimited rate-limit patience)
42
+ * that conflicts with §6b's specific real-time batch policy (5/10/20/40/80s +
43
+ * jitter, 5 retries, a hard call budget). Implemented directly here rather
44
+ * than adding a second retry mode to `llm.ts` (out of scope for this eval,
45
+ * and another agent is concurrently working elsewhere in this repo).
46
+ *
47
+ * Honesty invariants (non-negotiable — mirror recall-sweep.ts + spec §7):
48
+ * - Snapshot opened READONLY via openSnapshot — never initDb.
49
+ * - noStrengthen: true on every retrieve() call.
50
+ * - Real bge-small-en-v1.5 embedder, embed-once + queryEmbedding reuse.
51
+ * - neverCalledEmbed self-check ABORTS the run if retrieve() ignores
52
+ * queryEmbedding (would invalidate every measured number).
53
+ * - GLM-5.2 is the JUDGE only, real prompts, no synthetic queries.
54
+ * - Production renderer (`formatIndexLine`/`memoryTitle`) imported from
55
+ * `recall-index.ts`, never reimplemented.
56
+ * - judge_error batches/units excluded from every denominator, reported
57
+ * separately.
58
+ *
59
+ * Run:
60
+ * npm run eval:relevance -- <snapshot.db> [prompts.json] [report.md] \
61
+ * [--judge-delay-ms=2000] [--max-calls=900] [--resume] \
62
+ * [--verdicts-json=path] [--verdicts-jsonl=path]
63
+ */
64
+ export {};