ruvnet-brain 4.3.20 → 4.3.25
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/README.md +5 -5
- package/bin/install.mjs +383 -78
- package/console/app.js +141 -9
- package/console/index.html +51 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/host-shell-boundary.mjs +43 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/scripts/update-apply.mjs +2 -32
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +319 -27
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +23 -6
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- package/scripts/corpus-seed-publish.mjs +0 -110
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ADR-086 Step 15 — the C3 retrieval-accuracy gate.
|
|
3
|
+
//
|
|
4
|
+
// Dual's C3 definition, verbatim, is the contract this file implements:
|
|
5
|
+
//
|
|
6
|
+
// metric: "Evidence-supporting Hit@5: a question succeeds only when the first five
|
|
7
|
+
// customer-visible results contain correctly attributed source evidence sufficient to answer it.
|
|
8
|
+
// A matching file path without the supporting span fails."
|
|
9
|
+
//
|
|
10
|
+
// threshold: "Require 20 x successes >= 19 x N independently for each repository and each query
|
|
11
|
+
// mode. Errors and timeouts count as failures. No pooled average, rounding, excluded failed
|
|
12
|
+
// queries or post-failure denominator changes. N=0 is NOT MEASURED; only independently verified
|
|
13
|
+
// empty sources can lack a retrieval score."
|
|
14
|
+
//
|
|
15
|
+
// modes_and_partitions: "Run explicit-repository queries and ordinary full-corpus routed queries
|
|
16
|
+
// separately. Gate each shipped store, each repository and each nonempty gist partition; an
|
|
17
|
+
// aggregate score cannot hide one missing gist."
|
|
18
|
+
//
|
|
19
|
+
// ground_truth: "Build labels from exact upstream bytes, not the candidate's passage sidecars...
|
|
20
|
+
// Hold questions and labels out of indexing and tuning."
|
|
21
|
+
//
|
|
22
|
+
// WHAT THIS MEASURES, AND WHAT IT DOES NOT. The measurement runs against the EXTRACTED FINAL
|
|
23
|
+
// ARCHIVE through the customer query path (kb/forge-ask-all.mjs's searchAll — the same function the
|
|
24
|
+
// shipped CLI calls), never against the build directory and never against a private reimplementation
|
|
25
|
+
// of retrieval. The output is a DETACHED report bound to the final archive's digest; it is never
|
|
26
|
+
// inserted into the measured ZIP, because doing that would either change the archive after it was
|
|
27
|
+
// measured or force a second assembly, and ADR-086's invariants allow exactly one.
|
|
28
|
+
//
|
|
29
|
+
// BOUNDED RUNS ARE NOT PASSES. `--stores` / `--sample` / `--modes` exist because Step 14 measured
|
|
30
|
+
// the oracle producer at ~627s and ~10 model calls per source: a full 194-source oracle is ~33.8h of
|
|
31
|
+
// production, and a full benchmark sweep is correspondingly expensive. A bounded run writes
|
|
32
|
+
// `coverage.complete: false` with a `bounded` block naming exactly what was limited, and
|
|
33
|
+
// validateAccuracyReport() — the reader every gate uses — REFUSES a report whose coverage is not
|
|
34
|
+
// complete. A bounded run can therefore be read, reported and compared, but it can never seal a
|
|
35
|
+
// publishable corpus receipt.
|
|
36
|
+
//
|
|
37
|
+
// TIMEOUTS. The threshold text says errors and timeouts count as failures (they are never excluded
|
|
38
|
+
// from the denominator), and the Step 15 proof text additionally names "timeout" as a standalone
|
|
39
|
+
// blocker. Both readings are honoured, strictly: a timeout is counted as a failure in the
|
|
40
|
+
// arithmetic AND any timeout at all forces the run's state to FAIL. Strict is defensible here;
|
|
41
|
+
// loose is not.
|
|
42
|
+
|
|
43
|
+
import crypto from 'node:crypto';
|
|
44
|
+
import fs from 'node:fs';
|
|
45
|
+
import os from 'node:os';
|
|
46
|
+
import path from 'node:path';
|
|
47
|
+
import { fileURLToPath } from 'node:url';
|
|
48
|
+
import { extractZip } from '../../kb/zip-extract.mjs';
|
|
49
|
+
|
|
50
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
const ROOT = path.resolve(HERE, '..', '..');
|
|
52
|
+
|
|
53
|
+
// SCHEMA 2 (2026-09-14): ADR-086:248's denominator is now ENFORCED rather than documented. A schema-1
|
|
54
|
+
// oracle — unpaired questions with no unit inventory — and every report measured against one remain
|
|
55
|
+
// READABLE, but only as DIAGNOSTIC benchmarks: validateAccuracyReport refuses them for candidate
|
|
56
|
+
// acceptance and for publication, however high they score. Raised by Dual, verified in code.
|
|
57
|
+
export const ACCURACY_SCHEMA_VERSION = 2;
|
|
58
|
+
export const ACCURACY_KIND = 'ruvnet-brain-retrieval-accuracy';
|
|
59
|
+
export const ORACLE_SCHEMA_VERSION = 2;
|
|
60
|
+
export const LEGACY_ORACLE_SCHEMA_VERSION = 1;
|
|
61
|
+
export const ORACLE_KIND = 'ruvnet-brain-retrieval-accuracy-oracle';
|
|
62
|
+
// ADR-086:248: "Deterministically stratify and select min(100, U) units ... Each selected unit receives
|
|
63
|
+
// one direct and one meaning-preserving paraphrased question, so N = 2 × min(100, U)."
|
|
64
|
+
export const MAX_SELECTED_UNITS = 100;
|
|
65
|
+
export const QUESTION_FORMS = Object.freeze(['direct', 'paraphrase']);
|
|
66
|
+
export const C3_ACCEPTANCE = 'c3-acceptance';
|
|
67
|
+
export const DIAGNOSTIC = 'diagnostic';
|
|
68
|
+
export const ACCURACY_METRIC = 'evidence-supporting-hit@5';
|
|
69
|
+
export const HIT_AT_K = 5;
|
|
70
|
+
// 20 x successes >= 19 x N. Held as the two integers Dual named so the comparison stays exact
|
|
71
|
+
// integer arithmetic — no ratio, no rounding, no floating point anywhere on the gate path.
|
|
72
|
+
export const THRESHOLD_NUMERATOR = 19;
|
|
73
|
+
export const THRESHOLD_DENOMINATOR = 20;
|
|
74
|
+
export const QUERY_MODES = Object.freeze(['explicit-repository', 'full-corpus']);
|
|
75
|
+
export const DEFAULT_QUERY_TIMEOUT_MS = 120_000;
|
|
76
|
+
export const DEFAULT_ORACLE_FILE = 'data/retrieval-accuracy-oracle.json';
|
|
77
|
+
|
|
78
|
+
const HEX64 = /^[a-f0-9]{64}$/;
|
|
79
|
+
const HEX40 = /^[a-f0-9]{40}$/;
|
|
80
|
+
const HEX_COMMIT = /^[a-f0-9]{7,64}$/i;
|
|
81
|
+
const PARTITION_KINDS = new Set(['repository', 'gist', 'derived']);
|
|
82
|
+
|
|
83
|
+
export function fail(message) {
|
|
84
|
+
throw new Error(`[retrieval-accuracy] ${message}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function sha256Of(buffer) {
|
|
88
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sha256File(file) {
|
|
92
|
+
return sha256Of(fs.readFileSync(file));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function canonical(value) {
|
|
96
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
97
|
+
if (value && typeof value === 'object') {
|
|
98
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
99
|
+
}
|
|
100
|
+
return JSON.stringify(value === undefined ? null : value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Whitespace-normalized comparison text. A span is "present" iff it is a contiguous substring here. */
|
|
104
|
+
export function normalizeText(value) {
|
|
105
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Repository-relative path comparison: forward slashes, no leading ./ or /, case preserved. */
|
|
109
|
+
export function normalizePath(value) {
|
|
110
|
+
return String(value ?? '').split(path.sep).join('/').replace(/^\.?\//, '').trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The gate arithmetic, isolated so a test can prove it never rounds. */
|
|
114
|
+
export function meetsThreshold(successes, n) {
|
|
115
|
+
if (!Number.isSafeInteger(successes) || !Number.isSafeInteger(n) || successes < 0 || n <= 0) return false;
|
|
116
|
+
if (successes > n) return false;
|
|
117
|
+
return THRESHOLD_DENOMINATOR * successes >= THRESHOLD_NUMERATOR * n;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resultPaths(row) {
|
|
121
|
+
return [row?.path, row?.sourcePath, row?.file].filter((value) => typeof value === 'string' && value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resultText(row) {
|
|
125
|
+
return [row?.fullText, row?.text, row?.passage, row?.snippet]
|
|
126
|
+
.filter((value) => typeof value === 'string').join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Evidence-supporting Hit@5, verbatim from C3: within the first five customer-visible results one
|
|
131
|
+
* must be attributed to the labelled source path AND carry the labelled verbatim span in its
|
|
132
|
+
* customer-visible text. "A matching file path without the supporting span fails" is the reason the
|
|
133
|
+
* span check is not optional and is not a similarity score — it is substring containment after
|
|
134
|
+
* whitespace normalization, so a result that merely names the right file scores zero.
|
|
135
|
+
*
|
|
136
|
+
* "CORRECTLY ATTRIBUTED" MEANS THE RIGHT REPOSITORY (2026-09-14, raised by Dual, confirmed in code).
|
|
137
|
+
* The previous version never looked at which store a result came from, and matched paths by suffix in
|
|
138
|
+
* BOTH directions. In full-corpus mode another repository's file carrying the same sentence was
|
|
139
|
+
* credited, and an expected `docs/README.md` was satisfied by ANY bare `README.md` in any directory of
|
|
140
|
+
* any repository. Attribution now requires the result's store to equal the partition's store AND the
|
|
141
|
+
* repository-relative path to be EXACTLY the labelled path. kb/forge-ask-all.mjs searchAll rows carry
|
|
142
|
+
* `repo` in both query modes (measured against the live brain), which defaultSearch maps to `store`.
|
|
143
|
+
*/
|
|
144
|
+
export function scoreEvidenceHit({ results, label, store }) {
|
|
145
|
+
const top = (Array.isArray(results) ? results : []).slice(0, HIT_AT_K);
|
|
146
|
+
const wantPath = normalizePath(label?.sourcePath);
|
|
147
|
+
const wantSpan = normalizeText(label?.span);
|
|
148
|
+
if (!wantPath || !wantSpan) fail(`label ${label?.id || '(unnamed)'} has no source path or no span`);
|
|
149
|
+
if (typeof store !== 'string' || !store) {
|
|
150
|
+
fail(`label ${label?.id || '(unnamed)'} was scored without its partition's store — repository attribution cannot be skipped`);
|
|
151
|
+
}
|
|
152
|
+
let pathOnly = false;
|
|
153
|
+
let wrongRepository = false;
|
|
154
|
+
for (const row of top) {
|
|
155
|
+
if (!resultPaths(row).map(normalizePath).some((candidate) => candidate === wantPath)) continue;
|
|
156
|
+
const spanPresent = normalizeText(resultText(row)).includes(wantSpan);
|
|
157
|
+
if (row?.store !== store) {
|
|
158
|
+
if (spanPresent) wrongRepository = true;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
pathOnly = true;
|
|
162
|
+
if (spanPresent) return { hit: true, reason: 'evidence-supporting' };
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
hit: false,
|
|
166
|
+
reason: pathOnly ? 'path-matched-without-supporting-span'
|
|
167
|
+
: wrongRepository ? 'evidence-from-wrong-repository'
|
|
168
|
+
: 'no-attributed-result',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The oracle contract. Labels must trace to immutable upstream bytes, so every row carries the git
|
|
174
|
+
* blob SHA and the sha256 of the exact unit bytes it was written from — the two identities
|
|
175
|
+
* scripts/oracle/source-units.mjs binds and that a candidate passage sidecar cannot supply.
|
|
176
|
+
*
|
|
177
|
+
* HONEST LIMIT: this is a checkable PROXY for "built from upstream bytes, not from the candidate's
|
|
178
|
+
* sidecars", not a proof of it. Proving the negative needs the upstream snapshot in hand; what this
|
|
179
|
+
* does guarantee is that a label lifted from a passage sidecar cannot satisfy the schema without
|
|
180
|
+
* someone forging a git blob identity, and that any in-place edit of a row breaks the seal below.
|
|
181
|
+
*/
|
|
182
|
+
export function validateAccuracyOracle(oracle) {
|
|
183
|
+
if (!oracle || typeof oracle !== 'object') fail('oracle is missing or not an object');
|
|
184
|
+
if (oracle.kind !== ORACLE_KIND
|
|
185
|
+
|| (oracle.schemaVersion !== ORACLE_SCHEMA_VERSION && oracle.schemaVersion !== LEGACY_ORACLE_SCHEMA_VERSION)) {
|
|
186
|
+
fail('oracle schema version or kind is wrong');
|
|
187
|
+
}
|
|
188
|
+
// A legacy oracle is still validated in full — it must still be sealed and upstream-grounded — but it
|
|
189
|
+
// is classified DIAGNOSTIC and can never satisfy C3, because it carries no unit inventory, no fixed
|
|
190
|
+
// denominator and no direct/paraphrase pairing.
|
|
191
|
+
const compliant = oracle.schemaVersion === ORACLE_SCHEMA_VERSION;
|
|
192
|
+
if (!Array.isArray(oracle.partitions) || !oracle.partitions.length) fail('oracle declares no partitions');
|
|
193
|
+
if (!Array.isArray(oracle.labels) || !oracle.labels.length) fail('oracle carries no labels');
|
|
194
|
+
const partitions = new Map();
|
|
195
|
+
for (const row of oracle.partitions) {
|
|
196
|
+
const id = String(row?.partition || '');
|
|
197
|
+
if (!id) fail('oracle partition row has no partition id');
|
|
198
|
+
if (partitions.has(id)) fail(`oracle declares partition ${id} twice`);
|
|
199
|
+
if (!PARTITION_KINDS.has(row.kind)) fail(`oracle partition ${id} has an unsupported kind`);
|
|
200
|
+
if (typeof row.store !== 'string' || !row.store) fail(`oracle partition ${id} names no shipped store`);
|
|
201
|
+
if (!HEX_COMMIT.test(String(row.sourceCommit || ''))) fail(`oracle partition ${id} has no upstream source commit`);
|
|
202
|
+
const normalized = { partition: id, kind: row.kind, store: row.store, sourceCommit: String(row.sourceCommit).toLowerCase() };
|
|
203
|
+
if (compliant) {
|
|
204
|
+
if (!Number.isSafeInteger(row.U) || row.U < 1) {
|
|
205
|
+
fail(`oracle partition ${id} carries no meaningful-unit count U >= 1 — N=0 is NOT MEASURED; a genuinely empty source belongs in emptySources with independent evidence`);
|
|
206
|
+
}
|
|
207
|
+
const expectedSelected = Math.min(MAX_SELECTED_UNITS, row.U);
|
|
208
|
+
if (row.selectedUnits !== expectedSelected) {
|
|
209
|
+
fail(`oracle partition ${id} selects ${row.selectedUnits} units; ADR-086:248 requires min(100, U=${row.U}) = ${expectedSelected}`);
|
|
210
|
+
}
|
|
211
|
+
if (row.N !== 2 * expectedSelected) {
|
|
212
|
+
fail(`oracle partition ${id} declares N=${row.N}; ADR-086:248 requires N = 2 x min(100, U) = ${2 * expectedSelected}`);
|
|
213
|
+
}
|
|
214
|
+
if (!HEX64.test(String(row.inventorySha256 || '').toLowerCase())) {
|
|
215
|
+
fail(`oracle partition ${id} binds no meaningful-unit inventory digest`);
|
|
216
|
+
}
|
|
217
|
+
if (typeof row.rulesVersion !== 'string' || !row.rulesVersion) fail(`oracle partition ${id} names no enumeration rules version`);
|
|
218
|
+
if (!Array.isArray(row.unproduced)) fail(`oracle partition ${id} does not account for unproduced units`);
|
|
219
|
+
const unproducedIds = new Set();
|
|
220
|
+
const unproduced = row.unproduced.map((entry) => {
|
|
221
|
+
const unitId = String(entry?.unitId || '');
|
|
222
|
+
if (!unitId || typeof entry.reason !== 'string' || !entry.reason) fail(`oracle partition ${id} has an unproduced unit with no id or reason`);
|
|
223
|
+
if (unproducedIds.has(unitId)) fail(`oracle partition ${id} lists unproduced unit ${unitId} twice`);
|
|
224
|
+
unproducedIds.add(unitId);
|
|
225
|
+
return { unitId, reason: entry.reason };
|
|
226
|
+
});
|
|
227
|
+
Object.assign(normalized, {
|
|
228
|
+
U: row.U, selectedUnits: expectedSelected, N: row.N,
|
|
229
|
+
inventorySha256: String(row.inventorySha256).toLowerCase(), rulesVersion: row.rulesVersion, unproduced,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
partitions.set(id, normalized);
|
|
233
|
+
}
|
|
234
|
+
const seen = new Set();
|
|
235
|
+
const labels = [];
|
|
236
|
+
for (const row of oracle.labels) {
|
|
237
|
+
const id = String(row?.id || '');
|
|
238
|
+
if (!id) fail('oracle label has no id');
|
|
239
|
+
if (seen.has(id)) fail(`oracle label id ${id} appears twice`);
|
|
240
|
+
seen.add(id);
|
|
241
|
+
if (!partitions.has(row.partition)) fail(`oracle label ${id} names undeclared partition ${row.partition}`);
|
|
242
|
+
if (typeof row.question !== 'string' || !normalizeText(row.question)) fail(`oracle label ${id} has no question`);
|
|
243
|
+
if (typeof row.span !== 'string' || !normalizeText(row.span)) fail(`oracle label ${id} has no supporting span`);
|
|
244
|
+
if (typeof row.sourcePath !== 'string' || !normalizePath(row.sourcePath)) fail(`oracle label ${id} has no source path`);
|
|
245
|
+
if (!HEX40.test(String(row.blobSha || '').toLowerCase())) {
|
|
246
|
+
fail(`oracle label ${id} carries no upstream git blob identity — labels must trace to upstream bytes, not candidate passage sidecars`);
|
|
247
|
+
}
|
|
248
|
+
if (!HEX64.test(String(row.unitSha256 || '').toLowerCase())) {
|
|
249
|
+
fail(`oracle label ${id} carries no upstream unit byte digest`);
|
|
250
|
+
}
|
|
251
|
+
const normalized = {
|
|
252
|
+
id,
|
|
253
|
+
partition: row.partition,
|
|
254
|
+
question: row.question,
|
|
255
|
+
span: row.span,
|
|
256
|
+
sourcePath: row.sourcePath,
|
|
257
|
+
blobSha: String(row.blobSha).toLowerCase(),
|
|
258
|
+
unitSha256: String(row.unitSha256).toLowerCase(),
|
|
259
|
+
};
|
|
260
|
+
if (compliant) {
|
|
261
|
+
if (typeof row.unit !== 'string' || !row.unit) fail(`oracle label ${id} names no selected source unit`);
|
|
262
|
+
if (!QUESTION_FORMS.includes(row.form)) fail(`oracle label ${id} is neither the direct nor the paraphrased question of its unit`);
|
|
263
|
+
Object.assign(normalized, { unit: row.unit, form: row.form });
|
|
264
|
+
}
|
|
265
|
+
labels.push(normalized);
|
|
266
|
+
}
|
|
267
|
+
if (compliant) {
|
|
268
|
+
// Exactly one direct and one paraphrase per produced unit, sharing one upstream evidence identity;
|
|
269
|
+
// produced + unproduced must account for EVERY selected unit, so a missing, duplicate, extra or
|
|
270
|
+
// substituted slot cannot quietly move the denominator.
|
|
271
|
+
const byUnit = new Map();
|
|
272
|
+
for (const label of labels) {
|
|
273
|
+
const key = `${label.partition}\u0000${label.unit}`;
|
|
274
|
+
if (!byUnit.has(key)) byUnit.set(key, new Map());
|
|
275
|
+
const forms = byUnit.get(key);
|
|
276
|
+
if (forms.has(label.form)) fail(`oracle unit ${label.unit} in partition ${label.partition} has two ${label.form} questions`);
|
|
277
|
+
forms.set(label.form, label);
|
|
278
|
+
}
|
|
279
|
+
const producedByPartition = new Map();
|
|
280
|
+
for (const [key, forms] of byUnit) {
|
|
281
|
+
const [partitionId, unit] = key.split('\u0000');
|
|
282
|
+
const direct = forms.get('direct');
|
|
283
|
+
const paraphrase = forms.get('paraphrase');
|
|
284
|
+
if (!direct || !paraphrase) fail(`oracle unit ${unit} in partition ${partitionId} is missing its ${direct ? 'paraphrase' : 'direct'} question`);
|
|
285
|
+
for (const field of ['span', 'sourcePath', 'blobSha', 'unitSha256']) {
|
|
286
|
+
if (direct[field] !== paraphrase[field]) fail(`oracle unit ${unit} in partition ${partitionId}: direct and paraphrase disagree on ${field}`);
|
|
287
|
+
}
|
|
288
|
+
if (normalizeText(direct.question).toLowerCase() === normalizeText(paraphrase.question).toLowerCase()) {
|
|
289
|
+
fail(`oracle unit ${unit} in partition ${partitionId}: the paraphrase repeats the direct question`);
|
|
290
|
+
}
|
|
291
|
+
if (partitions.get(partitionId).unproduced.some((entry) => entry.unitId === unit)) {
|
|
292
|
+
fail(`oracle unit ${unit} in partition ${partitionId} is listed as both produced and unproduced`);
|
|
293
|
+
}
|
|
294
|
+
producedByPartition.set(partitionId, (producedByPartition.get(partitionId) || 0) + 1);
|
|
295
|
+
}
|
|
296
|
+
for (const partition of partitions.values()) {
|
|
297
|
+
const produced = producedByPartition.get(partition.partition) || 0;
|
|
298
|
+
if (produced + partition.unproduced.length !== partition.selectedUnits) {
|
|
299
|
+
fail(`oracle partition ${partition.partition} accounts for ${produced + partition.unproduced.length} of its ${partition.selectedUnits} selected units — a missing, duplicate or substituted slot would change N`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const empties = [];
|
|
304
|
+
for (const row of oracle.emptySources || []) {
|
|
305
|
+
const store = String(row?.store || '');
|
|
306
|
+
if (!store) fail('oracle emptySources row names no store');
|
|
307
|
+
if (typeof row.evidence !== 'string' || !row.evidence.trim()) {
|
|
308
|
+
fail(`oracle emptySources row ${store} carries no independent emptiness evidence — "zero extracted chunks is not proof of an empty source"`);
|
|
309
|
+
}
|
|
310
|
+
if (!HEX_COMMIT.test(String(row.sourceCommit || ''))) fail(`oracle emptySources row ${store} has no upstream source commit`);
|
|
311
|
+
empties.push({ store, evidence: row.evidence, sourceCommit: String(row.sourceCommit).toLowerCase() });
|
|
312
|
+
}
|
|
313
|
+
// The seal: rows cannot be edited in place without the digests moving, and the digests are what
|
|
314
|
+
// the corpus receipt and runProtectedCorpusSeed ultimately bind.
|
|
315
|
+
const labelsSha256 = sha256Of(canonical(labels));
|
|
316
|
+
const partitionsSha256 = sha256Of(canonical([...partitions.values()]));
|
|
317
|
+
const seal = oracle.seal || {};
|
|
318
|
+
if (seal.labelsSha256 !== labelsSha256 || seal.partitionsSha256 !== partitionsSha256) {
|
|
319
|
+
fail('oracle seal does not match its own partition/label rows');
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
schemaVersion: oracle.schemaVersion,
|
|
323
|
+
classification: compliant ? C3_ACCEPTANCE : DIAGNOSTIC,
|
|
324
|
+
c3Eligible: compliant,
|
|
325
|
+
partitions, labels, empties, labelsSha256, partitionsSha256,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function readAccuracyOracle(file) {
|
|
330
|
+
const resolved = path.resolve(file || '');
|
|
331
|
+
if (!resolved || !fs.existsSync(resolved)) {
|
|
332
|
+
fail(`retrieval-accuracy oracle missing (${resolved || 'no path supplied'}) — ADR-086 Step 14 owns producing it; the C3 gate fails closed without it`);
|
|
333
|
+
}
|
|
334
|
+
const stat = fs.lstatSync(resolved);
|
|
335
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail('retrieval-accuracy oracle is not a trusted regular file');
|
|
336
|
+
let parsed;
|
|
337
|
+
try {
|
|
338
|
+
parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
339
|
+
} catch (error) {
|
|
340
|
+
fail(`retrieval-accuracy oracle unreadable/corrupt (${error.message})`);
|
|
341
|
+
}
|
|
342
|
+
const validated = validateAccuracyOracle(parsed);
|
|
343
|
+
return {
|
|
344
|
+
...validated,
|
|
345
|
+
file: path.relative(ROOT, resolved).split(path.sep).join('/'),
|
|
346
|
+
sha256: sha256File(resolved),
|
|
347
|
+
bytes: stat.size,
|
|
348
|
+
oracleVersion: String(parsed.oracleVersion || ''),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Stores actually shipped in the extracted archive — the partition universe the gate must cover. */
|
|
353
|
+
export function archiveStores(root) {
|
|
354
|
+
return fs.readdirSync(root)
|
|
355
|
+
.filter((name) => name.endsWith('.big.rvf'))
|
|
356
|
+
.map((name) => name.slice(0, -'.big.rvf'.length))
|
|
357
|
+
.sort();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function defaultSearch({ dir, query, repos, timeoutMs }) {
|
|
361
|
+
const module = await import('../../kb/forge-ask-all.mjs');
|
|
362
|
+
let timer = null;
|
|
363
|
+
const timeout = new Promise((resolve) => {
|
|
364
|
+
timer = setTimeout(() => resolve({ __timedOut: true }), timeoutMs);
|
|
365
|
+
});
|
|
366
|
+
try {
|
|
367
|
+
const answered = await Promise.race([
|
|
368
|
+
module.searchAll({ dir, query, k: HIT_AT_K, ...(repos ? { repos } : {}) }),
|
|
369
|
+
timeout,
|
|
370
|
+
]);
|
|
371
|
+
if (answered?.__timedOut) return { timedOut: true, results: [] };
|
|
372
|
+
return {
|
|
373
|
+
timedOut: false,
|
|
374
|
+
results: (answered?.results || []).map((row) => ({
|
|
375
|
+
store: row.repo, path: row.path, text: row.fullText || row.text || '',
|
|
376
|
+
})),
|
|
377
|
+
};
|
|
378
|
+
} finally {
|
|
379
|
+
if (timer) clearTimeout(timer);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Run the benchmark against an already-assembled archive and write the detached report.
|
|
385
|
+
*
|
|
386
|
+
* `search` is injected so the gate's arithmetic, coverage bookkeeping and fail-closed behaviour can
|
|
387
|
+
* be proven in unit tests without a model cache or a half-gigabyte corpus; production passes no
|
|
388
|
+
* override and gets kb/forge-ask-all.mjs's searchAll — the real customer path.
|
|
389
|
+
*/
|
|
390
|
+
export async function runRetrievalAccuracy({
|
|
391
|
+
bundleFile,
|
|
392
|
+
oracleFile = path.join(ROOT, DEFAULT_ORACLE_FILE),
|
|
393
|
+
outFile,
|
|
394
|
+
storeLimit = null,
|
|
395
|
+
sampleLimit = null,
|
|
396
|
+
modes = QUERY_MODES,
|
|
397
|
+
timeoutMs = DEFAULT_QUERY_TIMEOUT_MS,
|
|
398
|
+
search = defaultSearch,
|
|
399
|
+
now = () => new Date().toISOString(),
|
|
400
|
+
} = {}) {
|
|
401
|
+
const bundle = path.resolve(bundleFile || '');
|
|
402
|
+
if (!bundle || !fs.existsSync(bundle) || !fs.statSync(bundle).isFile()) {
|
|
403
|
+
fail(`archive missing (${bundle || 'no path supplied'})`);
|
|
404
|
+
}
|
|
405
|
+
const archive = { file: path.basename(bundle), sha256: sha256File(bundle), bytes: fs.statSync(bundle).size };
|
|
406
|
+
const report = path.resolve(outFile || `${bundle}.accuracy.json`);
|
|
407
|
+
const oracle = readAccuracyOracle(oracleFile);
|
|
408
|
+
const selectedModes = QUERY_MODES.filter((mode) => modes.includes(mode));
|
|
409
|
+
if (!selectedModes.length) fail('no supported query mode selected');
|
|
410
|
+
|
|
411
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'retrieval-accuracy-'));
|
|
412
|
+
try {
|
|
413
|
+
try {
|
|
414
|
+
await extractZip(bundle, tmp);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
fail(`cannot extract archive (${error.message})`);
|
|
417
|
+
}
|
|
418
|
+
// The measurement root is the EXTRACTED FINAL ARCHIVE, never the build directory.
|
|
419
|
+
const manifests = [];
|
|
420
|
+
const findRoot = (dir) => {
|
|
421
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
422
|
+
const file = path.join(dir, entry.name);
|
|
423
|
+
if (entry.isSymbolicLink()) fail(`extracted archive contains a symbolic link: ${file}`);
|
|
424
|
+
if (entry.isDirectory()) findRoot(file);
|
|
425
|
+
else if (entry.isFile() && entry.name === 'ARCHIVE-MANIFEST.json') manifests.push(file);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
findRoot(tmp);
|
|
429
|
+
if (manifests.length !== 1) fail(`archive must contain exactly one ARCHIVE-MANIFEST.json; found ${manifests.length}`);
|
|
430
|
+
const corpusDir = path.dirname(manifests[0]);
|
|
431
|
+
const shipped = archiveStores(corpusDir);
|
|
432
|
+
if (!shipped.length) fail('extracted archive ships no stores to measure');
|
|
433
|
+
|
|
434
|
+
const labelsByPartition = new Map();
|
|
435
|
+
for (const label of oracle.labels) {
|
|
436
|
+
if (!labelsByPartition.has(label.partition)) labelsByPartition.set(label.partition, []);
|
|
437
|
+
labelsByPartition.get(label.partition).push(label);
|
|
438
|
+
}
|
|
439
|
+
const orderedPartitions = [...oracle.partitions.values()].sort((a, b) => a.partition.localeCompare(b.partition));
|
|
440
|
+
const measuredPartitions = storeLimit == null ? orderedPartitions : orderedPartitions.slice(0, storeLimit);
|
|
441
|
+
|
|
442
|
+
const partitions = [];
|
|
443
|
+
let timeouts = 0;
|
|
444
|
+
let errors = 0;
|
|
445
|
+
for (const partition of measuredPartitions) {
|
|
446
|
+
const all = (labelsByPartition.get(partition.partition) || [])
|
|
447
|
+
.slice()
|
|
448
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
449
|
+
const selected = sampleLimit == null ? all : all.slice(0, sampleLimit);
|
|
450
|
+
// THE DENOMINATOR. For a compliant oracle N comes from the unit inventory — 2 x min(100, U) —
|
|
451
|
+
// never from how many labels happened to survive production. Every unproduced unit keeps its two
|
|
452
|
+
// slots and scores them as misses below. A bounded --sample run is incomplete and unacceptable
|
|
453
|
+
// regardless, so it measures only what it sampled.
|
|
454
|
+
const unproducedSlots = oracle.c3Eligible && sampleLimit == null ? partition.unproduced : [];
|
|
455
|
+
for (const mode of selectedModes) {
|
|
456
|
+
const row = {
|
|
457
|
+
partition: partition.partition,
|
|
458
|
+
partitionKind: partition.kind,
|
|
459
|
+
store: partition.store,
|
|
460
|
+
sourceCommit: partition.sourceCommit,
|
|
461
|
+
...(oracle.c3Eligible ? { U: partition.U, N: partition.N } : {}),
|
|
462
|
+
mode,
|
|
463
|
+
n: selected.length + 2 * unproducedSlots.length,
|
|
464
|
+
unproducedQuestions: 2 * unproducedSlots.length,
|
|
465
|
+
successes: 0,
|
|
466
|
+
failures: 0,
|
|
467
|
+
errors: 0,
|
|
468
|
+
timeouts: 0,
|
|
469
|
+
sampled: sampleLimit != null && selected.length < all.length,
|
|
470
|
+
oracleRows: all.length,
|
|
471
|
+
failedLabels: [],
|
|
472
|
+
};
|
|
473
|
+
if (!row.n) {
|
|
474
|
+
// "N=0 is NOT MEASURED; only independently verified empty sources can lack a retrieval score."
|
|
475
|
+
row.state = 'NOT-MEASURED';
|
|
476
|
+
partitions.push(row);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
for (const label of selected) {
|
|
480
|
+
let outcome;
|
|
481
|
+
try {
|
|
482
|
+
const answered = await search({
|
|
483
|
+
dir: corpusDir,
|
|
484
|
+
query: label.question,
|
|
485
|
+
repos: mode === 'explicit-repository' ? [partition.store] : null,
|
|
486
|
+
timeoutMs,
|
|
487
|
+
mode,
|
|
488
|
+
});
|
|
489
|
+
if (answered?.timedOut) {
|
|
490
|
+
row.timeouts += 1;
|
|
491
|
+
timeouts += 1;
|
|
492
|
+
outcome = { hit: false, reason: 'timeout' };
|
|
493
|
+
} else {
|
|
494
|
+
outcome = scoreEvidenceHit({ results: answered?.results, label, store: partition.store });
|
|
495
|
+
}
|
|
496
|
+
} catch (error) {
|
|
497
|
+
row.errors += 1;
|
|
498
|
+
errors += 1;
|
|
499
|
+
outcome = { hit: false, reason: `error: ${error.message}` };
|
|
500
|
+
}
|
|
501
|
+
// No excluded failed queries and no post-failure denominator changes: every selected label
|
|
502
|
+
// lands in exactly one of successes/failures, and n was fixed before the loop began.
|
|
503
|
+
if (outcome.hit) row.successes += 1;
|
|
504
|
+
else {
|
|
505
|
+
row.failures += 1;
|
|
506
|
+
if (row.failedLabels.length < 20) row.failedLabels.push({ id: label.id, reason: outcome.reason });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const slot of unproducedSlots) {
|
|
510
|
+
// Two questions per selected unit, both misses: the unit was selected, so it counts.
|
|
511
|
+
row.failures += 2;
|
|
512
|
+
if (row.failedLabels.length < 20) row.failedLabels.push({ id: `${partition.partition}::${slot.unitId}`, reason: `unproduced: ${slot.reason}` });
|
|
513
|
+
}
|
|
514
|
+
if (row.successes + row.failures !== row.n) {
|
|
515
|
+
fail(`internal: partition ${row.partition} (${mode}) scored ${row.successes + row.failures} outcomes for n=${row.n}`);
|
|
516
|
+
}
|
|
517
|
+
if (oracle.c3Eligible && sampleLimit == null && row.n !== row.N) {
|
|
518
|
+
fail(`internal: partition ${row.partition} (${mode}) measured n=${row.n} but its inventory fixes N=${row.N}`);
|
|
519
|
+
}
|
|
520
|
+
row.state = meetsThreshold(row.successes, row.n) && row.timeouts === 0 ? 'PASS' : 'FAIL';
|
|
521
|
+
partitions.push(row);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const measuredIds = new Set(measuredPartitions.map((row) => row.partition));
|
|
526
|
+
const unmeasuredPartitions = orderedPartitions
|
|
527
|
+
.filter((row) => !measuredIds.has(row.partition))
|
|
528
|
+
.map((row) => row.partition);
|
|
529
|
+
const emptyStores = new Set(oracle.empties.map((row) => row.store));
|
|
530
|
+
const coveredStores = new Set(partitions.filter((row) => row.n > 0).map((row) => row.store));
|
|
531
|
+
const uncoveredArchiveStores = shipped.filter((store) => !coveredStores.has(store) && !emptyStores.has(store));
|
|
532
|
+
const sampledAny = partitions.some((row) => row.sampled);
|
|
533
|
+
const boundedReasons = [];
|
|
534
|
+
if (storeLimit != null) boundedReasons.push(`--stores ${storeLimit}`);
|
|
535
|
+
if (sampleLimit != null) boundedReasons.push(`--sample ${sampleLimit}`);
|
|
536
|
+
if (selectedModes.length !== QUERY_MODES.length) boundedReasons.push(`--modes ${selectedModes.join(',')}`);
|
|
537
|
+
if (unmeasuredPartitions.length) boundedReasons.push(`${unmeasuredPartitions.length} oracle partition(s) not measured`);
|
|
538
|
+
if (uncoveredArchiveStores.length) boundedReasons.push(`${uncoveredArchiveStores.length} shipped store(s) with no oracle coverage`);
|
|
539
|
+
if (sampledAny) boundedReasons.push('at least one partition measured a sample of its oracle rows');
|
|
540
|
+
const complete = boundedReasons.length === 0;
|
|
541
|
+
|
|
542
|
+
const failingPartitions = partitions.filter((row) => row.state !== 'PASS');
|
|
543
|
+
const state = complete && !failingPartitions.length && timeouts === 0 ? 'PASS' : 'FAIL';
|
|
544
|
+
|
|
545
|
+
const payload = {
|
|
546
|
+
schemaVersion: ACCURACY_SCHEMA_VERSION,
|
|
547
|
+
kind: ACCURACY_KIND,
|
|
548
|
+
// A legacy-oracle run is a DIAGNOSTIC benchmark: its number describes that oracle and this
|
|
549
|
+
// evaluator only, and validateAccuracyReport refuses it for acceptance and publication.
|
|
550
|
+
classification: oracle.classification,
|
|
551
|
+
c3Eligible: oracle.c3Eligible,
|
|
552
|
+
createdAt: now(),
|
|
553
|
+
archive,
|
|
554
|
+
oracle: {
|
|
555
|
+
schemaVersion: oracle.schemaVersion,
|
|
556
|
+
file: oracle.file,
|
|
557
|
+
sha256: oracle.sha256,
|
|
558
|
+
bytes: oracle.bytes,
|
|
559
|
+
oracleVersion: oracle.oracleVersion,
|
|
560
|
+
labelsSha256: oracle.labelsSha256,
|
|
561
|
+
partitionsSha256: oracle.partitionsSha256,
|
|
562
|
+
},
|
|
563
|
+
generator: { retrievalAccuracySha256: sha256File(fileURLToPath(import.meta.url)) },
|
|
564
|
+
metric: ACCURACY_METRIC,
|
|
565
|
+
k: HIT_AT_K,
|
|
566
|
+
threshold: { numerator: THRESHOLD_NUMERATOR, denominator: THRESHOLD_DENOMINATOR },
|
|
567
|
+
modes: selectedModes,
|
|
568
|
+
queryTimeoutMs: timeoutMs,
|
|
569
|
+
coverage: {
|
|
570
|
+
complete,
|
|
571
|
+
bounded: complete ? null : { reasons: boundedReasons, storeLimit, sampleLimit, modes: selectedModes },
|
|
572
|
+
archiveStores: shipped,
|
|
573
|
+
oraclePartitions: orderedPartitions.length,
|
|
574
|
+
measuredPartitions: measuredPartitions.length,
|
|
575
|
+
unmeasuredPartitions,
|
|
576
|
+
uncoveredArchiveStores,
|
|
577
|
+
emptySources: oracle.empties,
|
|
578
|
+
},
|
|
579
|
+
totals: {
|
|
580
|
+
n: partitions.reduce((sum, row) => sum + row.n, 0),
|
|
581
|
+
successes: partitions.reduce((sum, row) => sum + row.successes, 0),
|
|
582
|
+
failures: partitions.reduce((sum, row) => sum + row.failures, 0),
|
|
583
|
+
errors,
|
|
584
|
+
timeouts,
|
|
585
|
+
},
|
|
586
|
+
partitions,
|
|
587
|
+
state,
|
|
588
|
+
};
|
|
589
|
+
fs.mkdirSync(path.dirname(report), { recursive: true });
|
|
590
|
+
fs.writeFileSync(report, `${JSON.stringify(payload, null, 2)}\n`);
|
|
591
|
+
return { reportFile: report, report: payload };
|
|
592
|
+
} finally {
|
|
593
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The STRICT C3 reader: schema, metric, threshold, c3Eligible, archive binding, complete coverage and
|
|
599
|
+
* every per-partition PASS, all re-derived here rather than trusted from the report's summary fields.
|
|
600
|
+
*
|
|
601
|
+
* RETAINED DELIBERATELY THOUGH NOTHING IN THE RELEASE PATH CALLS IT SINCE 2026-09-15. C3 measured
|
|
602
|
+
* 59.0% on a real archive and was demoted to a published diagnostic (readDiagnosticAccuracyReport is
|
|
603
|
+
* what candidate acceptance and publication now use). This function is the RE-ARM path: when the
|
|
604
|
+
* retrieval-quality work lands and C3 can be met, restoring the blocking predicate is a one-line
|
|
605
|
+
* change back to this reader rather than a rewrite. Its behaviour stays pinned by
|
|
606
|
+
* tests/unit/corpus-accuracy-gate.test.mjs so it cannot rot while it waits.
|
|
607
|
+
*/
|
|
608
|
+
export function validateAccuracyReport({
|
|
609
|
+
report, archive, expectedOracleSha256 = null, expectedGeneratorSha256 = null,
|
|
610
|
+
} = {}) {
|
|
611
|
+
if (!report || typeof report !== 'object') fail('accuracy report is missing or not an object');
|
|
612
|
+
if (report.schemaVersion !== ACCURACY_SCHEMA_VERSION || report.kind !== ACCURACY_KIND) {
|
|
613
|
+
fail('accuracy report schema version or kind is wrong');
|
|
614
|
+
}
|
|
615
|
+
if (report.metric !== ACCURACY_METRIC || report.k !== HIT_AT_K
|
|
616
|
+
|| report.threshold?.numerator !== THRESHOLD_NUMERATOR || report.threshold?.denominator !== THRESHOLD_DENOMINATOR) {
|
|
617
|
+
fail('accuracy report does not measure the contracted metric, k or threshold');
|
|
618
|
+
}
|
|
619
|
+
if (report.c3Eligible !== true || report.classification !== C3_ACCEPTANCE
|
|
620
|
+
|| report.oracle?.schemaVersion !== ORACLE_SCHEMA_VERSION) {
|
|
621
|
+
fail(`accuracy report is a ${report.classification || 'legacy'} benchmark against an oracle of schema ${report.oracle?.schemaVersion ?? 'unknown'} — only an ADR-086:248-compliant (schema ${ORACLE_SCHEMA_VERSION}) measurement can qualify a corpus for C3, however high it scored`);
|
|
622
|
+
}
|
|
623
|
+
// The BINDING: the report is bound to the exact final-archive bytes it was measured against. An
|
|
624
|
+
// altered archive changes this digest and the report stops applying, which is the whole reason the
|
|
625
|
+
// report is detached and digest-bound rather than packed inside the ZIP.
|
|
626
|
+
if (!archive || report.archive?.sha256 !== archive.sha256 || report.archive?.bytes !== archive.bytes
|
|
627
|
+
|| report.archive?.file !== archive.file) {
|
|
628
|
+
fail('accuracy report is not bound to this exact final archive');
|
|
629
|
+
}
|
|
630
|
+
if (!HEX64.test(String(report.oracle?.sha256 || ''))) fail('accuracy report binds no oracle digest');
|
|
631
|
+
if (expectedOracleSha256 != null && report.oracle.sha256 !== expectedOracleSha256) {
|
|
632
|
+
fail('accuracy report was measured against a different retrieval oracle than the one committed here');
|
|
633
|
+
}
|
|
634
|
+
if (expectedGeneratorSha256 != null && report.generator?.retrievalAccuracySha256 !== expectedGeneratorSha256) {
|
|
635
|
+
fail('accuracy report was produced by a different benchmark generator than the one committed here');
|
|
636
|
+
}
|
|
637
|
+
if (report.coverage?.complete !== true) {
|
|
638
|
+
const reasons = (report.coverage?.bounded?.reasons || []).join('; ') || 'coverage.complete is not true';
|
|
639
|
+
fail(`accuracy report is a BOUNDED measurement, not a corpus-wide pass (${reasons})`);
|
|
640
|
+
}
|
|
641
|
+
if (Array.isArray(report.coverage?.unmeasuredPartitions) && report.coverage.unmeasuredPartitions.length) {
|
|
642
|
+
fail(`accuracy report leaves ${report.coverage.unmeasuredPartitions.length} oracle partition(s) unmeasured`);
|
|
643
|
+
}
|
|
644
|
+
if (Array.isArray(report.coverage?.uncoveredArchiveStores) && report.coverage.uncoveredArchiveStores.length) {
|
|
645
|
+
fail(`accuracy report leaves ${report.coverage.uncoveredArchiveStores.length} shipped store(s) ungated — an aggregate score cannot hide one missing partition`);
|
|
646
|
+
}
|
|
647
|
+
if (!Array.isArray(report.modes) || QUERY_MODES.some((mode) => !report.modes.includes(mode))) {
|
|
648
|
+
fail('accuracy report does not measure both the explicit-repository and full-corpus query modes');
|
|
649
|
+
}
|
|
650
|
+
if (!Array.isArray(report.partitions) || !report.partitions.length) fail('accuracy report measured no partition');
|
|
651
|
+
const seenModes = new Map();
|
|
652
|
+
for (const row of report.partitions) {
|
|
653
|
+
if (typeof row?.partition !== 'string' || !row.partition) fail('accuracy report partition row has no partition id');
|
|
654
|
+
if (!QUERY_MODES.includes(row.mode)) fail(`accuracy report partition ${row.partition} names an unsupported mode`);
|
|
655
|
+
if (!Number.isSafeInteger(row.n) || row.n <= 0) {
|
|
656
|
+
fail(`accuracy report partition ${row.partition} (${row.mode}) is NOT MEASURED (N=0)`);
|
|
657
|
+
}
|
|
658
|
+
if (![row.successes, row.failures, row.errors, row.timeouts].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
659
|
+
fail(`accuracy report partition ${row.partition} (${row.mode}) has malformed counters`);
|
|
660
|
+
}
|
|
661
|
+
if (row.successes + row.failures !== row.n) {
|
|
662
|
+
fail(`accuracy report partition ${row.partition} (${row.mode}) changed its denominator after the fact`);
|
|
663
|
+
}
|
|
664
|
+
if (!Number.isSafeInteger(row.U) || row.U < 1 || row.N !== 2 * Math.min(MAX_SELECTED_UNITS, row.U)) {
|
|
665
|
+
fail(`accuracy report partition ${row.partition} (${row.mode}) carries no ADR-086:248 denominator (U=${row.U}, N=${row.N})`);
|
|
666
|
+
}
|
|
667
|
+
if (row.n !== row.N) {
|
|
668
|
+
fail(`accuracy report partition ${row.partition} (${row.mode}) measured n=${row.n} but its inventory fixes N=${row.N} — a denominator taken from surviving labels`);
|
|
669
|
+
}
|
|
670
|
+
if (row.timeouts > 0) {
|
|
671
|
+
fail(`accuracy report partition ${row.partition} (${row.mode}) recorded ${row.timeouts} timeout(s)`);
|
|
672
|
+
}
|
|
673
|
+
if (row.sampled === true) fail(`accuracy report partition ${row.partition} (${row.mode}) measured only a sample`);
|
|
674
|
+
if (!meetsThreshold(row.successes, row.n)) {
|
|
675
|
+
fail(`accuracy report partition ${row.partition} (${row.mode}) is below threshold: ${row.successes}/${row.n}`);
|
|
676
|
+
}
|
|
677
|
+
if (row.state !== 'PASS') fail(`accuracy report partition ${row.partition} (${row.mode}) is ${row.state}`);
|
|
678
|
+
const key = row.partition;
|
|
679
|
+
if (!seenModes.has(key)) seenModes.set(key, new Set());
|
|
680
|
+
if (seenModes.get(key).has(row.mode)) fail(`accuracy report measures partition ${key} twice in ${row.mode}`);
|
|
681
|
+
seenModes.get(key).add(row.mode);
|
|
682
|
+
}
|
|
683
|
+
for (const [partition, modes] of seenModes) {
|
|
684
|
+
// "Run explicit-repository queries and ordinary full-corpus routed queries separately."
|
|
685
|
+
if (QUERY_MODES.some((mode) => !modes.has(mode))) fail(`accuracy report partition ${partition} is missing a query mode`);
|
|
686
|
+
}
|
|
687
|
+
if (Number.isSafeInteger(report.totals?.timeouts) && report.totals.timeouts > 0) {
|
|
688
|
+
fail(`accuracy report recorded ${report.totals.timeouts} timeout(s)`);
|
|
689
|
+
}
|
|
690
|
+
if (report.state !== 'PASS') fail(`accuracy report state is ${report.state}`);
|
|
691
|
+
return report;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Read a detached accuracy report from disk and validate it against the archive it must bind.
|
|
696
|
+
* Returns the {file, sha256, bytes} identity A6 requires the corpus receipt to carry.
|
|
697
|
+
*/
|
|
698
|
+
/**
|
|
699
|
+
* INTEGRITY-ONLY read of the machine-generated C3 measurement, for the lane where it is published as
|
|
700
|
+
* a DIAGNOSTIC rather than used as the blocking predicate (ADR-086 amendment, 2026-09-15: C3 measured
|
|
701
|
+
* 59.0% on the real archive and no longer blocks; scripts/oracle/repo-recall.mjs does).
|
|
702
|
+
*
|
|
703
|
+
* This still refuses a report that is malformed, or that describes a DIFFERENT archive — a diagnostic
|
|
704
|
+
* that is not bound to the bytes it graded is worse than none, because it looks like evidence. What it
|
|
705
|
+
* deliberately does NOT enforce is the 19/20 threshold, c3Eligible or the C3 classification, so a
|
|
706
|
+
* failing-but-honest measurement can travel with the release and be read by anyone.
|
|
707
|
+
*/
|
|
708
|
+
export function readDiagnosticAccuracyReport({ reportFile, archive, expectedOracleSha256 = null, expectedGeneratorSha256 = null } = {}) {
|
|
709
|
+
const resolved = path.resolve(reportFile || '');
|
|
710
|
+
if (!resolved || !fs.existsSync(resolved)) fail(`diagnostic retrieval-accuracy report missing (${resolved || 'no path supplied'})`);
|
|
711
|
+
const stat = fs.lstatSync(resolved);
|
|
712
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail('diagnostic retrieval-accuracy report is not a trusted regular file');
|
|
713
|
+
let report;
|
|
714
|
+
try { report = JSON.parse(fs.readFileSync(resolved, 'utf8')); }
|
|
715
|
+
catch (error) { fail(`diagnostic retrieval-accuracy report unreadable/corrupt (${error.message})`); }
|
|
716
|
+
if (report?.schemaVersion !== ACCURACY_SCHEMA_VERSION || report?.kind !== ACCURACY_KIND) {
|
|
717
|
+
fail('diagnostic retrieval-accuracy report schema version or kind is wrong');
|
|
718
|
+
}
|
|
719
|
+
if (!archive || report.archive?.sha256 !== archive.sha256 || report.archive?.bytes !== archive.bytes) {
|
|
720
|
+
fail('diagnostic retrieval-accuracy report is not bound to this exact final archive');
|
|
721
|
+
}
|
|
722
|
+
// The oracle and generator bindings are KEPT even though the score no longer blocks. A diagnostic
|
|
723
|
+
// that does not name the instrument it was measured with is not a diagnostic, it is a number; and
|
|
724
|
+
// silently swapping the oracle underneath a published 59.0% would make that figure meaningless.
|
|
725
|
+
if (expectedOracleSha256 != null && report.oracle?.sha256 !== expectedOracleSha256) {
|
|
726
|
+
fail('diagnostic retrieval-accuracy report was measured against a different retrieval oracle than the one committed here');
|
|
727
|
+
}
|
|
728
|
+
if (expectedGeneratorSha256 != null && report.generator?.retrievalAccuracySha256 !== expectedGeneratorSha256) {
|
|
729
|
+
fail('diagnostic retrieval-accuracy report was produced by a different benchmark generator than the one committed here');
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
identity: { file: path.basename(resolved), sha256: sha256File(resolved), bytes: stat.size },
|
|
733
|
+
state: report.state,
|
|
734
|
+
classification: report.classification ?? null,
|
|
735
|
+
c3Eligible: report.c3Eligible === true,
|
|
736
|
+
totals: report.totals ?? null,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export function readAccuracyReport({
|
|
741
|
+
reportFile, archive, expectedOracleSha256 = null, expectedGeneratorSha256 = null,
|
|
742
|
+
} = {}) {
|
|
743
|
+
const resolved = path.resolve(reportFile || '');
|
|
744
|
+
if (!resolved || !fs.existsSync(resolved)) {
|
|
745
|
+
fail(`detached retrieval-accuracy report missing (${resolved || 'no path supplied'})`);
|
|
746
|
+
}
|
|
747
|
+
const stat = fs.lstatSync(resolved);
|
|
748
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail('detached retrieval-accuracy report is not a trusted regular file');
|
|
749
|
+
let parsed;
|
|
750
|
+
try {
|
|
751
|
+
parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
752
|
+
} catch (error) {
|
|
753
|
+
fail(`detached retrieval-accuracy report unreadable/corrupt (${error.message})`);
|
|
754
|
+
}
|
|
755
|
+
const report = validateAccuracyReport({ report: parsed, archive, expectedOracleSha256, expectedGeneratorSha256 });
|
|
756
|
+
return {
|
|
757
|
+
identity: { file: path.basename(resolved), sha256: sha256File(resolved), bytes: stat.size },
|
|
758
|
+
report,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function arg(argv, name, fallback = null) {
|
|
763
|
+
const index = argv.indexOf(name);
|
|
764
|
+
return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function positiveInt(value, name) {
|
|
768
|
+
if (value == null) return null;
|
|
769
|
+
const parsed = Number(value);
|
|
770
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) fail(`${name} must be a positive integer`);
|
|
771
|
+
return parsed;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
775
|
+
const { report } = await runRetrievalAccuracy({
|
|
776
|
+
bundleFile: arg(argv, '--bundle'),
|
|
777
|
+
oracleFile: arg(argv, '--oracle', path.join(ROOT, DEFAULT_ORACLE_FILE)),
|
|
778
|
+
outFile: arg(argv, '--out'),
|
|
779
|
+
storeLimit: positiveInt(arg(argv, '--stores'), '--stores'),
|
|
780
|
+
sampleLimit: positiveInt(arg(argv, '--sample'), '--sample'),
|
|
781
|
+
modes: arg(argv, '--modes') ? String(arg(argv, '--modes')).split(',').map((mode) => mode.trim()) : QUERY_MODES,
|
|
782
|
+
timeoutMs: positiveInt(arg(argv, '--timeout-ms'), '--timeout-ms') || DEFAULT_QUERY_TIMEOUT_MS,
|
|
783
|
+
});
|
|
784
|
+
process.stdout.write(`${JSON.stringify({
|
|
785
|
+
ok: report.state === 'PASS',
|
|
786
|
+
state: report.state,
|
|
787
|
+
complete: report.coverage.complete,
|
|
788
|
+
bounded: report.coverage.bounded,
|
|
789
|
+
totals: report.totals,
|
|
790
|
+
partitions: report.partitions.length,
|
|
791
|
+
}, null, 2)}\n`);
|
|
792
|
+
// A bounded or failing measurement is not an acceptable candidate input; say so with the exit code
|
|
793
|
+
// as well as in the report, so a shell caller that forgets to read the JSON still fails closed.
|
|
794
|
+
return report.state === 'PASS' ? 0 : 1;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// REALPATH BOTH SIDES, or this CLI silently no-ops. argv[1] is whatever the caller typed, symlinks
|
|
798
|
+
// and all, while node resolves a module URL THROUGH symlinks before it reaches import.meta.url — so a
|
|
799
|
+
// symlinked invocation compares a link path against a real path, decides it is not the entry point,
|
|
800
|
+
// runs nothing, and EXITS 0. On macOS every os.tmpdir() path is symlinked (/var/folders -> /private/
|
|
801
|
+
// var/folders), so any caller staging work in a temp directory hits this. Measured 2026-09-14:
|
|
802
|
+
// build-bundle.mjs and corpus-candidate.mjs both no-opped and prepareCorpusCandidate reported SUCCESS
|
|
803
|
+
// with no archive and no receipt on disk. Same defect, same fix as plugin/scripts/hook-input.mjs:518.
|
|
804
|
+
function isMain() {
|
|
805
|
+
try {
|
|
806
|
+
if (!process.argv[1]) return false;
|
|
807
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
808
|
+
} catch {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (isMain()) {
|
|
814
|
+
main().then((code) => { process.exitCode = code; }).catch((error) => {
|
|
815
|
+
console.error(error.message);
|
|
816
|
+
process.exitCode = 1;
|
|
817
|
+
});
|
|
818
|
+
}
|