renor-engine 0.0.0-stage → 1.0.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/LICENSE +661 -0
- package/NOTICE +73 -0
- package/README.md +105 -2
- package/dist/BUILD.json +6 -0
- package/dist/api/apiAuth.js +43 -0
- package/dist/api/server.js +172 -0
- package/dist/buildIdentity.js +132 -0
- package/dist/cli-extractors.js +46 -0
- package/dist/cli.js +491 -0
- package/dist/config.js +65 -0
- package/dist/context/composer.js +320 -0
- package/dist/context/readHonesty.js +478 -0
- package/dist/context/rowStore.js +238 -0
- package/dist/conversation/formatter.js +81 -0
- package/dist/conversation/parser.js +189 -0
- package/dist/conversation/types.js +2 -0
- package/dist/conversation/writer.js +42 -0
- package/dist/db/client.js +147 -0
- package/dist/db/indexSalvage.js +81 -0
- package/dist/db/migrations.js +642 -0
- package/dist/db/readCache.js +109 -0
- package/dist/db/schema.js +5 -0
- package/dist/db/statements.js +89 -0
- package/dist/digests/digestCache.js +142 -0
- package/dist/digests/digests.js +597 -0
- package/dist/digests/worker.js +348 -0
- package/dist/embeddings/caching.js +337 -0
- package/dist/embeddings/coalescing.js +177 -0
- package/dist/embeddings/interactive.js +103 -0
- package/dist/embeddings/local.js +68 -0
- package/dist/embeddings/openai.js +105 -0
- package/dist/embeddings/rebuild.js +166 -0
- package/dist/embeddings/simdScan.js +258 -0
- package/dist/embeddings/space.js +21 -0
- package/dist/embeddings/types.js +14 -0
- package/dist/embeddings/vectorIndex.js +675 -0
- package/dist/embeddings/vertex.js +224 -0
- package/dist/eval/artifacts.js +42 -0
- package/dist/eval/cacheReport.js +139 -0
- package/dist/eval/comparison.js +182 -0
- package/dist/eval/context700-candidate.js +145 -0
- package/dist/eval/evalCache.js +525 -0
- package/dist/eval/hashEmbedding.js +35 -0
- package/dist/eval/longmemeval.js +56 -0
- package/dist/eval/metrics.js +181 -0
- package/dist/eval/renorbenchBaseline.js +486 -0
- package/dist/eval/renorbenchDocuments.js +134 -0
- package/dist/eval/renorbenchProduct.js +182 -0
- package/dist/eval/runLedger.js +279 -0
- package/dist/eval/runManifest.js +479 -0
- package/dist/eval/scoring.js +197 -0
- package/dist/evidence/attach.js +267 -0
- package/dist/evidence/background.js +147 -0
- package/dist/evidence/evidenceCache.js +182 -0
- package/dist/evidence/gate.js +37 -0
- package/dist/evidence/keyPool.js +85 -0
- package/dist/evidence/ollamaChat.js +84 -0
- package/dist/evidence/optin.js +165 -0
- package/dist/evidence/options.js +130 -0
- package/dist/evidence/secondPass.js +456 -0
- package/dist/extraction/classifyCapture.js +34 -0
- package/dist/extraction/decision-model.js +174 -0
- package/dist/extraction/eventExtractor.js +113 -0
- package/dist/extraction/extractionClient.js +122 -0
- package/dist/extraction/factExtractor.js +46 -0
- package/dist/extraction/factsWritePath.js +179 -0
- package/dist/extraction/preferenceExtractor.js +153 -0
- package/dist/extraction/secretGuard.js +176 -0
- package/dist/extraction/secretScanner.js +665 -0
- package/dist/extraction/typedFacts.js +224 -0
- package/dist/extraction/writePathAdjudication.js +110 -0
- package/dist/importing/apply.js +644 -0
- package/dist/importing/bundle-write.js +80 -0
- package/dist/importing/cli.js +407 -0
- package/dist/importing/existing.js +49 -0
- package/dist/importing/facts.js +123 -0
- package/dist/importing/forgetImported.js +79 -0
- package/dist/importing/hosted.js +467 -0
- package/dist/importing/index.js +29 -0
- package/dist/importing/journalLog.js +96 -0
- package/dist/importing/markdown.js +98 -0
- package/dist/importing/plan.js +206 -0
- package/dist/importing/secretScreen.js +113 -0
- package/dist/importing/serialize.js +188 -0
- package/dist/importing/source.js +463 -0
- package/dist/importing/types.js +6 -0
- package/dist/indexer/chunker.js +319 -0
- package/dist/indexer/dailyEntries.js +63 -0
- package/dist/indexer/hasher.js +10 -0
- package/dist/indexer/laneRuns.js +48 -0
- package/dist/indexer/o200k.js +481 -0
- package/dist/indexer/o200k.json +8 -0
- package/dist/indexer/outsideRoot.js +77 -0
- package/dist/indexer/parser.js +167 -0
- package/dist/indexer/pipeline.js +1168 -0
- package/dist/indexer/staleness.js +201 -0
- package/dist/indexer/tokenizer.js +77 -0
- package/dist/indexer/walker.js +184 -0
- package/dist/mcp/hive.js +88 -0
- package/dist/mcp/requestErrors.js +47 -0
- package/dist/mcp/server.js +396 -0
- package/dist/memory/agentLease.js +43 -0
- package/dist/memory/confinedWrite.js +315 -0
- package/dist/memory/controls.js +73 -0
- package/dist/memory/dailyNote.js +265 -0
- package/dist/memory/durableAppend.js +57 -0
- package/dist/memory/factConflicts.js +203 -0
- package/dist/memory/factTerms.js +321 -0
- package/dist/memory/hiveNameHeader.cjs +10 -0
- package/dist/memory/inventory.js +55 -0
- package/dist/memory/invisibleText.js +20 -0
- package/dist/memory/lifecycle.js +1756 -0
- package/dist/memory/limits.js +13 -0
- package/dist/memory/requests.js +183 -0
- package/dist/memory/sourceName.js +200 -0
- package/dist/memory/whatsNew.js +158 -0
- package/dist/memory/writer.js +175 -0
- package/dist/memory/writerConflicts.js +85 -0
- package/dist/memorymap/memoryMap.js +1568 -0
- package/dist/memorymap/replacedJournal.js +58 -0
- package/dist/memorymap/replacements.js +405 -0
- package/dist/mvp/activity.js +86 -0
- package/dist/mvp/agentMemory.js +1744 -0
- package/dist/mvp/auth.js +531 -0
- package/dist/mvp/cli.js +73 -0
- package/dist/mvp/client-store.js +462 -0
- package/dist/mvp/compileCache.js +249 -0
- package/dist/mvp/compileSeed.js +27 -0
- package/dist/mvp/config.js +288 -0
- package/dist/mvp/connectionStatus.js +80 -0
- package/dist/mvp/contextRelevance.js +413 -0
- package/dist/mvp/engine.js +1065 -0
- package/dist/mvp/exportAll.js +359 -0
- package/dist/mvp/factLanePack.js +77 -0
- package/dist/mvp/factMarkers.js +49 -0
- package/dist/mvp/factsLane.js +203 -0
- package/dist/mvp/hiddenSpans.js +585 -0
- package/dist/mvp/history.js +413 -0
- package/dist/mvp/hitDates.js +31 -0
- package/dist/mvp/hitFloor.js +36 -0
- package/dist/mvp/identity.js +18 -0
- package/dist/mvp/idle.js +113 -0
- package/dist/mvp/memoryDir.js +98 -0
- package/dist/mvp/posthogMcp.js +210 -0
- package/dist/mvp/readScope.js +104 -0
- package/dist/mvp/readUsefulness.js +222 -0
- package/dist/mvp/sanitize.js +48 -0
- package/dist/mvp/schemaRejections.js +91 -0
- package/dist/mvp/scope.js +127 -0
- package/dist/mvp/server.js +2089 -0
- package/dist/mvp/serverCard.js +29 -0
- package/dist/mvp/storedSize.js +139 -0
- package/dist/mvp/telemetry.js +441 -0
- package/dist/retrieval/adviceLines.js +195 -0
- package/dist/retrieval/adviceQuestion.js +44 -0
- package/dist/retrieval/conversationShape.js +44 -0
- package/dist/retrieval/dailyEntryTime.js +55 -0
- package/dist/retrieval/entityLookup.js +113 -0
- package/dist/retrieval/factsFirst.js +188 -0
- package/dist/retrieval/keyword.js +84 -0
- package/dist/retrieval/ledgerEventTime.js +51 -0
- package/dist/retrieval/ledgerLiveness.js +83 -0
- package/dist/retrieval/ledgerPresentation.js +161 -0
- package/dist/retrieval/noteIdentity.js +57 -0
- package/dist/retrieval/orchestrator.js +1086 -0
- package/dist/retrieval/queryHelpers.js +142 -0
- package/dist/retrieval/questionSpan.js +475 -0
- package/dist/retrieval/readerEvents.js +690 -0
- package/dist/retrieval/readerLayout.js +503 -0
- package/dist/retrieval/relativeTimeHints.js +113 -0
- package/dist/retrieval/semantic.js +67 -0
- package/dist/router/routerCache.js +106 -0
- package/dist/router/sessionRouter.js +726 -0
- package/dist/toolRefusal.js +120 -0
- package/dist/watcher/watcher.js +75 -0
- package/package.json +120 -4
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EVALUATOR_SOURCES = exports.GOLD_RUNTIME_FIELD_NAMES = exports.PRODUCT_FEATURE_FLAGS = exports.SIDECAR_FLAGS = exports.RESEARCH_WATERMARK = exports.RESEARCH_RUNTIME_FIELD_ALLOWLIST = exports.PRODUCT_RUNTIME_FIELD_ALLOWLIST = exports.MANIFEST_CONTRACT_VERSION = void 0;
|
|
7
|
+
exports.productFeatureFlagOverrides = productFeatureFlagOverrides;
|
|
8
|
+
exports.researchTruthFields = researchTruthFields;
|
|
9
|
+
exports.validateTruthFields = validateTruthFields;
|
|
10
|
+
exports.assertNoSidecarFlags = assertNoSidecarFlags;
|
|
11
|
+
exports.assertGoldIsolatedRuntimeInputs = assertGoldIsolatedRuntimeInputs;
|
|
12
|
+
exports.assertProductScoreEligibility = assertProductScoreEligibility;
|
|
13
|
+
exports.evaluatorSourceHash = evaluatorSourceHash;
|
|
14
|
+
exports.gitState = gitState;
|
|
15
|
+
exports.datasetSha256File = datasetSha256File;
|
|
16
|
+
exports.packageLockSha256 = packageLockSha256;
|
|
17
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
20
|
+
const node_child_process_1 = require("node:child_process");
|
|
21
|
+
// ─── Product-truth manifest contract (Wave C, Task 6) ─────────────────────────
|
|
22
|
+
//
|
|
23
|
+
// These fields distinguish RESEARCH execution (gold-derived fields may reach
|
|
24
|
+
// runtime; never a Renor product score) from PRODUCT-PUBLIC execution (every
|
|
25
|
+
// answer context comes from a public MCP/API/CLI surface, gold-blind).
|
|
26
|
+
// The contract is additive and versioned: legacy manifests without these
|
|
27
|
+
// fields remain valid research manifests, but can never carry a product label
|
|
28
|
+
// (scorer refuses, fail closed). Contract shape per
|
|
29
|
+
// docs/evals/productization/inventory-harness.md §4.
|
|
30
|
+
/**
|
|
31
|
+
* Bump when the truth-field semantics change. v2 is the first versioned
|
|
32
|
+
* contract (introduces executionKind/watermark/refusal rules).
|
|
33
|
+
*/
|
|
34
|
+
exports.MANIFEST_CONTRACT_VERSION = 2;
|
|
35
|
+
/**
|
|
36
|
+
* ONLY these dataset fields may reach the product runtime in a
|
|
37
|
+
* product-public execution. Corpus rendering inputs (markdown generation)
|
|
38
|
+
* and the question itself are product-observable; everything else
|
|
39
|
+
* (answer, answer_session_ids, question_type, shouldAbstain) is scorer
|
|
40
|
+
* or offline-analysis input only.
|
|
41
|
+
*/
|
|
42
|
+
exports.PRODUCT_RUNTIME_FIELD_ALLOWLIST = [
|
|
43
|
+
'question_id',
|
|
44
|
+
'question',
|
|
45
|
+
'question_date',
|
|
46
|
+
'haystack_sessions',
|
|
47
|
+
'haystack_dates',
|
|
48
|
+
'haystack_session_ids',
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Research manifests may expose more (benchmark categories, abstention
|
|
52
|
+
* labels drive research-only routing). Recorded honestly so a research
|
|
53
|
+
* manifest can never be relabeled product-public.
|
|
54
|
+
*/
|
|
55
|
+
exports.RESEARCH_RUNTIME_FIELD_ALLOWLIST = [
|
|
56
|
+
...exports.PRODUCT_RUNTIME_FIELD_ALLOWLIST,
|
|
57
|
+
'category',
|
|
58
|
+
'shouldAbstain',
|
|
59
|
+
];
|
|
60
|
+
/** Required non-null watermark on every research manifest/report. */
|
|
61
|
+
exports.RESEARCH_WATERMARK = 'RESEARCH-ONLY';
|
|
62
|
+
/** Research sidecar flags: legal only in research runners; refused for product-public execution. */
|
|
63
|
+
exports.SIDECAR_FLAGS = [
|
|
64
|
+
'--facts-file',
|
|
65
|
+
'--chronicle-file',
|
|
66
|
+
'--prefs-file',
|
|
67
|
+
'--temporal-sidecar',
|
|
68
|
+
'--context-compiler',
|
|
69
|
+
'--completion-run',
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* Shipped default-off product feature flags (Wave B components, ADR-0003).
|
|
73
|
+
* The product runner may arm these in the public CLI process ONLY when the
|
|
74
|
+
* operator explicitly sets them in the runner environment — the passthrough
|
|
75
|
+
* never invents a value, and every armed flag is recorded in the manifest's
|
|
76
|
+
* productMode.overrides so a screen arm is self-describing. Each flag's
|
|
77
|
+
* default remains OFF inside product code, except RENOR_SESSION_EXPAND (on at five notes unless set to 0) and
|
|
78
|
+
* RENOR_SESSION_DIVERSITY (searches capped at 2 chunks per note; get_context uncapped by default): see their entries; this list governs EVAL
|
|
79
|
+
* ARMING only.
|
|
80
|
+
*/
|
|
81
|
+
exports.PRODUCT_FEATURE_FLAGS = [
|
|
82
|
+
{ env: 'RENOR_STRUCTURED_CONTEXT', component: 'C0-composer' },
|
|
83
|
+
{ env: 'RENOR_TYPED_FACTS', component: 'C2-facts' },
|
|
84
|
+
{ env: 'RENOR_CHRONICLE', component: 'C3-chronicle' },
|
|
85
|
+
{ env: 'RENOR_PREFS', component: 'C4-prefs' },
|
|
86
|
+
{ env: 'RENOR_QUERY_SCOPED_ROWS', component: 'C5-qscope' },
|
|
87
|
+
// Evidence second-pass gate policy (src/evidence/gate.ts: off|auto|always).
|
|
88
|
+
// It is a shipped default-off product surface (RENOR_SEARCH_EVIDENCE) with a
|
|
89
|
+
// policy knob, and it is forwarded to the product process by the runner — so
|
|
90
|
+
// two arms differing only in this value are genuinely different arms and must
|
|
91
|
+
// not share a provenance record. Measured 2026-09-26: without this entry, the
|
|
92
|
+
// auto and always arms produced byte-identical productMode.overrides.
|
|
93
|
+
{ env: 'RENOR_EVIDENCE_MODE', component: 'evidence-gate' },
|
|
94
|
+
{ env: 'RENOR_EVIDENCE_SESSIONS', component: 'evidence-coverage' },
|
|
95
|
+
// Session-diversity top-K window (retrieval orchestrator, search default 2 chunks per note, get_context default 0, explicit 0 is off).
|
|
96
|
+
// Forwarded so a capped screen arm's manifest is self-describing, like the other flags. An unset value is the
|
|
97
|
+
// product default, so an arm that must run without the cap sets 0.
|
|
98
|
+
{ env: 'RENOR_SESSION_DIVERSITY', component: 'session-diversity' },
|
|
99
|
+
// Session expansion (retrieval orchestrator, default ON at five notes, 0 is off): how many of the window's best
|
|
100
|
+
// notes are expanded, the token budget of what they add, and the longest statement it may add. Each is recorded, so
|
|
101
|
+
// two arms differing only in the budget or the statement limit differ here too. An unset value is the product
|
|
102
|
+
// default, so an arm that must run without the expansion sets 0.
|
|
103
|
+
{ env: 'RENOR_SESSION_EXPAND', component: 'session-expand' },
|
|
104
|
+
{ env: 'RENOR_SESSION_EXPAND_TOKENS', component: 'session-expand-budget' },
|
|
105
|
+
{ env: 'RENOR_SESSION_EXPAND_STATEMENT_TOKENS', component: 'session-expand-statement' },
|
|
106
|
+
{ env: 'RENOR_SESSION_EXPAND_DOCUMENT_STATEMENT_TOKENS', component: 'session-expand-document-statement' },
|
|
107
|
+
// The operator's declaration that an OpenAI-compatible endpoint returns a calibrated provider's vectors (retrieval
|
|
108
|
+
// orchestrator calibratedModelKey, default unset): it arms the ranked expansion on the CLI path the way a hosted
|
|
109
|
+
// workspace has it, so an arm that sets it is a different arm from its control and is recorded as one.
|
|
110
|
+
{ env: 'RENOR_EMBEDDING_SERVED_BY', component: 'embedding-served-by' },
|
|
111
|
+
// Session router (src/router/sessionRouter.ts, default OFF: off|replace|union) and the two models it calls. The
|
|
112
|
+
// models are recorded because two router arms that differ only in a model are different arms.
|
|
113
|
+
{ env: 'RENOR_SESSION_ROUTER', component: 'session-router' },
|
|
114
|
+
{ env: 'RENOR_ROUTER_MODEL', component: 'session-router-model' },
|
|
115
|
+
{ env: 'RENOR_ROUTER_SUMMARY_MODEL', component: 'session-router-summary-model' },
|
|
116
|
+
// Write-time digests (src/digests/digests.ts, default OFF) and the knobs that change what they hold or show.
|
|
117
|
+
{ env: 'RENOR_DIGESTS', component: 'write-time-digests' },
|
|
118
|
+
{ env: 'RENOR_DIGEST_MODEL', component: 'write-time-digests-model' },
|
|
119
|
+
{ env: 'RENOR_DIGEST_REASONING_EFFORT', component: 'write-time-digests-effort' },
|
|
120
|
+
{ env: 'RENOR_DIGEST_TOKENS', component: 'write-time-digests-budget' },
|
|
121
|
+
// P1 strip-excerpt-meta (run-longmemeval-product --strip-excerpt-meta):
|
|
122
|
+
// deterministic strip of the pure-meta header lines inside each excerpt
|
|
123
|
+
// block; content + Session date footers preserved. Default-off; armed only
|
|
124
|
+
// when the operator sets RENOR_STRIP_EXCERPT_META in the runner env.
|
|
125
|
+
{ env: 'RENOR_STRIP_EXCERPT_META', component: 'P1-excerpt-meta' },
|
|
126
|
+
{ env: 'RENOR_EVIDENCE_ENUMERATE', component: 'evidence-enumeration' },
|
|
127
|
+
// Evidence request bounds (src/evidence/options.ts, default unset = no bound).
|
|
128
|
+
// A bounded arm is a different extractor configuration from an unbounded
|
|
129
|
+
// one, so each armed bound is recorded like the other evidence knobs.
|
|
130
|
+
{ env: 'RENOR_EVIDENCE_MAX_TOKENS', component: 'evidence-max-tokens' },
|
|
131
|
+
{ env: 'RENOR_EVIDENCE_REASONING_EFFORT', component: 'evidence-reasoning-effort' },
|
|
132
|
+
];
|
|
133
|
+
/**
|
|
134
|
+
* Resolve the armed subset for a run: the exact flag values that will be
|
|
135
|
+
* passed through to the product process, as `NAME=value` manifest override
|
|
136
|
+
* strings. Deterministic order (declared above); unset flags are omitted so
|
|
137
|
+
* default-off stays default-off and the manifest shows an empty arm list for
|
|
138
|
+
* control runs at the product defaults.
|
|
139
|
+
*/
|
|
140
|
+
function productFeatureFlagOverrides(env) {
|
|
141
|
+
return exports.PRODUCT_FEATURE_FLAGS
|
|
142
|
+
.filter(flag => env[flag.env] !== undefined && env[flag.env] !== '')
|
|
143
|
+
.map(flag => `${flag.env}=${env[flag.env]}`);
|
|
144
|
+
}
|
|
145
|
+
/** Gold-derived dataset fields that must never appear as runtime-visible inputs. */
|
|
146
|
+
exports.GOLD_RUNTIME_FIELD_NAMES = [
|
|
147
|
+
'answer',
|
|
148
|
+
'answer_session_ids',
|
|
149
|
+
'question_type',
|
|
150
|
+
'shouldAbstain',
|
|
151
|
+
];
|
|
152
|
+
/**
|
|
153
|
+
* Research-preset truth fields for the existing runners. Research runs keep
|
|
154
|
+
* working unchanged; their manifests are explicitly watermarked and declare
|
|
155
|
+
* the (wider) research allowlist, so a research manifest can never be
|
|
156
|
+
* relabeled product-public by the scorer.
|
|
157
|
+
*/
|
|
158
|
+
function researchTruthFields(args) {
|
|
159
|
+
return {
|
|
160
|
+
manifestContractVersion: exports.MANIFEST_CONTRACT_VERSION,
|
|
161
|
+
executionKind: 'research',
|
|
162
|
+
publicEntryPoint: null,
|
|
163
|
+
entryPointDetail: null,
|
|
164
|
+
runtimeVisibleFieldAllowlist: [...exports.RESEARCH_RUNTIME_FIELD_ALLOWLIST],
|
|
165
|
+
goldIsolated: false,
|
|
166
|
+
goldIsolationBasis: 'research execution: gold-derived routing is legal and recorded, never product-labeled',
|
|
167
|
+
productMode: null,
|
|
168
|
+
composition: args.composition ?? null,
|
|
169
|
+
coverage: { ...args.coverage, failedRunRecorded: false },
|
|
170
|
+
cache: args.cache ?? null,
|
|
171
|
+
provenance: null,
|
|
172
|
+
models: null,
|
|
173
|
+
watermark: exports.RESEARCH_WATERMARK,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Structural type check without throwing; returns all contract violations. */
|
|
177
|
+
function validateTruthFields(raw) {
|
|
178
|
+
const errors = [];
|
|
179
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
180
|
+
return ['truth fields missing or not an object'];
|
|
181
|
+
const f = raw;
|
|
182
|
+
if (f.manifestContractVersion !== exports.MANIFEST_CONTRACT_VERSION) {
|
|
183
|
+
errors.push(`manifestContractVersion must be ${exports.MANIFEST_CONTRACT_VERSION} (got ${String(f.manifestContractVersion)})`);
|
|
184
|
+
}
|
|
185
|
+
if (f.executionKind !== 'research' && f.executionKind !== 'product-public') {
|
|
186
|
+
errors.push(`executionKind must be "research" or "product-public" (got ${String(f.executionKind)})`);
|
|
187
|
+
}
|
|
188
|
+
if (f.executionKind === 'research') {
|
|
189
|
+
if (f.watermark !== exports.RESEARCH_WATERMARK)
|
|
190
|
+
errors.push(`research manifests must carry watermark "${exports.RESEARCH_WATERMARK}"`);
|
|
191
|
+
if (f.publicEntryPoint != null)
|
|
192
|
+
errors.push('research manifests must have publicEntryPoint null');
|
|
193
|
+
}
|
|
194
|
+
if (f.executionKind === 'product-public') {
|
|
195
|
+
if (f.watermark != null)
|
|
196
|
+
errors.push('product-public manifests must not carry the RESEARCH-ONLY watermark');
|
|
197
|
+
if (f.publicEntryPoint !== 'mcp' && f.publicEntryPoint !== 'api' && f.publicEntryPoint !== 'cli') {
|
|
198
|
+
errors.push(`product-public manifests must record publicEntryPoint mcp|api|cli (got ${String(f.publicEntryPoint)})`);
|
|
199
|
+
}
|
|
200
|
+
if (!f.entryPointDetail || typeof f.entryPointDetail.transport !== 'string' || !f.entryPointDetail.transport
|
|
201
|
+
|| typeof f.entryPointDetail.command !== 'string' || !f.entryPointDetail.command) {
|
|
202
|
+
errors.push('product-public manifests must record entryPointDetail {transport, command}');
|
|
203
|
+
}
|
|
204
|
+
if (f.goldIsolated !== true)
|
|
205
|
+
errors.push('product-public manifests must assert goldIsolated true');
|
|
206
|
+
if (!f.productMode)
|
|
207
|
+
errors.push('product-public manifests must record productMode');
|
|
208
|
+
}
|
|
209
|
+
if (Array.isArray(f.runtimeVisibleFieldAllowlist)) {
|
|
210
|
+
const expected = f.executionKind === 'product-public'
|
|
211
|
+
? [...exports.PRODUCT_RUNTIME_FIELD_ALLOWLIST]
|
|
212
|
+
: [...exports.RESEARCH_RUNTIME_FIELD_ALLOWLIST];
|
|
213
|
+
const actual = [...f.runtimeVisibleFieldAllowlist].sort();
|
|
214
|
+
if (JSON.stringify(actual) !== JSON.stringify([...expected].sort())) {
|
|
215
|
+
errors.push(`runtimeVisibleFieldAllowlist mismatch: got ${JSON.stringify(f.runtimeVisibleFieldAllowlist)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
errors.push('runtimeVisibleFieldAllowlist missing');
|
|
220
|
+
}
|
|
221
|
+
if (!f.coverage || typeof f.coverage !== 'object')
|
|
222
|
+
errors.push('coverage block missing');
|
|
223
|
+
if (f.watermark !== exports.RESEARCH_WATERMARK && f.watermark != null) {
|
|
224
|
+
errors.push(`watermark must be "${exports.RESEARCH_WATERMARK}" or null (got ${String(f.watermark)})`);
|
|
225
|
+
}
|
|
226
|
+
return errors;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Refuse research sidecar flags for product-public execution. Any argv
|
|
230
|
+
* containing a research sidecar flag is rejected — sidecar facts, chronicles,
|
|
231
|
+
* preferences, temporal tables, compiler candidates, and completion runs are
|
|
232
|
+
* research-only context channels.
|
|
233
|
+
*/
|
|
234
|
+
function assertNoSidecarFlags(argv) {
|
|
235
|
+
const found = exports.SIDECAR_FLAGS.filter(flag => argv.includes(flag));
|
|
236
|
+
if (found.length > 0) {
|
|
237
|
+
throw new Error(`Refused for product-public execution: research sidecar flag(s) ${found.join(', ')}. ` +
|
|
238
|
+
'Sidecar context channels are research-only; product mechanisms must land in src/ behind public surfaces.');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Structural gold-isolation marshal check: the exact runtime inputs handed to
|
|
243
|
+
* the public surface (query text, process argv, spawn env) must not contain
|
|
244
|
+
* gold field names or gold field values. Content parity under gold mutation
|
|
245
|
+
* is proven by tests/eval/product-path-contract.test.ts (poisoned fixture).
|
|
246
|
+
*/
|
|
247
|
+
function assertGoldIsolatedRuntimeInputs(args) {
|
|
248
|
+
const marshaled = [
|
|
249
|
+
args.query,
|
|
250
|
+
args.argv.join('\0'),
|
|
251
|
+
Object.entries(args.env).map(([key, value]) => `${key}=${value}`).join('\0'),
|
|
252
|
+
].join('\0');
|
|
253
|
+
for (const field of exports.GOLD_RUNTIME_FIELD_NAMES) {
|
|
254
|
+
// Gold field NAMES must not appear as structured keys; allow ordinary
|
|
255
|
+
// prose coincidences in the query by checking argv/env strictly and the
|
|
256
|
+
// query only for key-like occurrences.
|
|
257
|
+
if (args.argv.join(' ').toLowerCase().includes(field.toLowerCase())
|
|
258
|
+
|| Object.keys(args.env).some(key => key.toLowerCase().includes(field.toLowerCase()))) {
|
|
259
|
+
throw new Error(`Gold field name "${field}" appeared in runtime argv/env — gold fields must never reach the product process`);
|
|
260
|
+
}
|
|
261
|
+
if (field === 'answer' || field === 'shouldAbstain')
|
|
262
|
+
continue; // prose overlap: covered by parity fixture
|
|
263
|
+
if (args.query.includes(field)) {
|
|
264
|
+
throw new Error(`Gold field name "${field}" appeared in the runtime query`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (marshaled.includes('"shouldAbstain"') || marshaled.includes('"question_type"') || marshaled.includes('"answer_session_ids"')) {
|
|
268
|
+
throw new Error('Gold field JSON keys appeared in marshaled runtime inputs');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Scorer fail-closed eligibility for a product label. Collects ALL refusal
|
|
273
|
+
* reasons and throws one combined error. A product score requires:
|
|
274
|
+
* product-public execution, clean provenance, complete coverage (unless an
|
|
275
|
+
* explicit failed-run record), zero promotion-arm cache reads/hits, and
|
|
276
|
+
* complete model echoes.
|
|
277
|
+
*
|
|
278
|
+
* Returns a coverage note when the run is an explicitly recorded failed run.
|
|
279
|
+
*/
|
|
280
|
+
function assertProductScoreEligibility(manifest) {
|
|
281
|
+
const reasons = [];
|
|
282
|
+
const truth = manifest;
|
|
283
|
+
if (manifest.manifestContractVersion === undefined) {
|
|
284
|
+
throw new Error('Refusing product label: manifest has no product-truth contract fields (pre-contract run). ' +
|
|
285
|
+
'Score it with --label research; it is watermarked RESEARCH-ONLY.');
|
|
286
|
+
}
|
|
287
|
+
for (const error of validateTruthFields(manifest))
|
|
288
|
+
reasons.push(error);
|
|
289
|
+
if (truth.executionKind !== 'product-public') {
|
|
290
|
+
reasons.push(`executionKind is "${String(truth.executionKind)}": research runs carry only the ${exports.RESEARCH_WATERMARK} label, never a product label`);
|
|
291
|
+
}
|
|
292
|
+
const provenance = truth.provenance ?? {
|
|
293
|
+
gitSha: manifest.gitSha, gitDirty: manifest.gitDirty, evaluatorSha256: manifest.evaluatorSha256,
|
|
294
|
+
};
|
|
295
|
+
if (!provenance || typeof provenance !== 'object') {
|
|
296
|
+
reasons.push('provenance block missing');
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const gitSha = String(provenance.gitSha ?? '');
|
|
300
|
+
if (!gitSha || gitSha === 'unknown')
|
|
301
|
+
reasons.push('provenance.gitSha missing or unknown');
|
|
302
|
+
if (provenance.gitDirty === true)
|
|
303
|
+
reasons.push('gitDirty: the executed code does not match the recorded SHA');
|
|
304
|
+
if (gitDirtyLike(provenance.gitDirty))
|
|
305
|
+
reasons.push('provenance.gitDirty missing');
|
|
306
|
+
const evaluatorSha = provenance.evaluatorSha256;
|
|
307
|
+
if (typeof evaluatorSha !== 'string' || !/^[0-9a-f]{64}$/.test(evaluatorSha))
|
|
308
|
+
reasons.push('provenance.evaluatorSha256 missing or malformed');
|
|
309
|
+
const datasetSha = provenance.datasetSha256;
|
|
310
|
+
if (typeof datasetSha !== 'string' || !/^[0-9a-f]{64}$/.test(datasetSha))
|
|
311
|
+
reasons.push('provenance.datasetSha256 missing or malformed');
|
|
312
|
+
}
|
|
313
|
+
const coverage = truth.coverage;
|
|
314
|
+
let failedRunRecord = false;
|
|
315
|
+
if (!coverage || typeof coverage !== 'object') {
|
|
316
|
+
reasons.push('coverage block missing');
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
const c = coverage;
|
|
320
|
+
if (!(c.casesTotal > 0))
|
|
321
|
+
reasons.push('coverage.casesTotal must be > 0');
|
|
322
|
+
if (c.casesCompleted !== c.casesTotal) {
|
|
323
|
+
if (c.failedRunRecorded === true)
|
|
324
|
+
failedRunRecord = true;
|
|
325
|
+
else
|
|
326
|
+
reasons.push(`incomplete coverage: casesCompleted ${c.casesCompleted} < casesTotal ${c.casesTotal} (record a failed-run record explicitly to score a failed run)`);
|
|
327
|
+
}
|
|
328
|
+
if ((c.answerErrors ?? 0) > 0) {
|
|
329
|
+
if (c.failedRunRecorded === true)
|
|
330
|
+
failedRunRecord = true;
|
|
331
|
+
else
|
|
332
|
+
reasons.push(`incomplete coverage: ${c.answerErrors} answer error(s) (record a failed-run record explicitly to score a failed run)`);
|
|
333
|
+
}
|
|
334
|
+
if ((c.judgeParseFailures ?? 0) > 0 && c.failedRunRecorded !== true) {
|
|
335
|
+
reasons.push(`incomplete coverage: ${c.judgeParseFailures} judge parse failure(s)`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const cache = truth.cache;
|
|
339
|
+
if (!cache || typeof cache !== 'object') {
|
|
340
|
+
reasons.push('cache evidence missing');
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
const cc = cache;
|
|
344
|
+
if (cc?.arm !== 'promotion' && cc?.arm !== 'iteration') {
|
|
345
|
+
reasons.push(`cache.arm must be "promotion" or "iteration" (got ${String(cc?.arm)})`);
|
|
346
|
+
}
|
|
347
|
+
// Plan Task 8 semantics: "fresh isolated write-through caches and ZERO HITS".
|
|
348
|
+
// Validity requires zero HITS (no replayed responses); read ATTEMPTS against
|
|
349
|
+
// the verified-fresh DB are harmless by construction (nothing to hit).
|
|
350
|
+
if (cc?.arm === 'promotion' && ((cc.hits ?? 0) > 0)) {
|
|
351
|
+
reasons.push(`promotion arm used cache HITS (hits=${cc.hits}, reads=${cc.reads}): authoritative promotions require zero cache hits — replayed responses invalidate the run`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Embedding provider: eval-hash/local are transport-smoke only. An
|
|
355
|
+
// authoritative product score must run on a production embedding provider,
|
|
356
|
+
// and the measured embedding configuration must be recorded in the manifest.
|
|
357
|
+
// Embedding provider: eval-hash/local are transport-smoke only, and even a
|
|
358
|
+
// watermarked smoke run can NEVER carry the product label — the watermark
|
|
359
|
+
// lets smoke runs exist, it never authorizes the authoritative label.
|
|
360
|
+
const embeddingProvider = String(manifest.embeddingProvider ?? '');
|
|
361
|
+
const transportSmoke = manifest.transportSmoke === true;
|
|
362
|
+
if (transportSmoke) {
|
|
363
|
+
reasons.push('transportSmoke: transport-smoke runs (non-production embeddings) can never carry a product label — score with --label research');
|
|
364
|
+
}
|
|
365
|
+
else if (!embeddingProvider) {
|
|
366
|
+
reasons.push('embeddingProvider missing from manifest: the embedding configuration of the measured product instance must be recorded');
|
|
367
|
+
}
|
|
368
|
+
else if (embeddingProvider === 'eval-hash' || embeddingProvider === 'local') {
|
|
369
|
+
reasons.push(`embeddingProvider is "${embeddingProvider}": non-production embeddings can never carry a product label (--transport-smoke labels smoke runs for research, it does not authorize the product label)`);
|
|
370
|
+
}
|
|
371
|
+
const models = truth.models ?? {
|
|
372
|
+
answerRequested: manifest.answerModelRequested,
|
|
373
|
+
answerEchoes: manifest.answerModelEchoes,
|
|
374
|
+
judgeRequested: manifest.judgeModelRequested,
|
|
375
|
+
judgeEchoes: manifest.judgeModelEchoes,
|
|
376
|
+
};
|
|
377
|
+
if (!models || typeof models !== 'object') {
|
|
378
|
+
reasons.push('model echo evidence missing');
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
const m = models;
|
|
382
|
+
if (!m?.answerEchoes || m.answerEchoes.length === 0)
|
|
383
|
+
reasons.push('missing answer model echo(s): provider responses were not recorded');
|
|
384
|
+
if (!m?.judgeEchoes || m.judgeEchoes.length === 0)
|
|
385
|
+
reasons.push('missing judge model echo(s): provider responses were not recorded');
|
|
386
|
+
if (typeof m?.answerRequested !== 'string' || !m.answerRequested)
|
|
387
|
+
reasons.push('missing answer model request id');
|
|
388
|
+
if (typeof m?.judgeRequested !== 'string' || !m.judgeRequested)
|
|
389
|
+
reasons.push('missing judge model request id');
|
|
390
|
+
}
|
|
391
|
+
if (reasons.length > 0) {
|
|
392
|
+
throw new Error(`Refusing product score (${reasons.length} reason(s)):\n- ${reasons.join('\n- ')}`);
|
|
393
|
+
}
|
|
394
|
+
return { failedRunRecord };
|
|
395
|
+
}
|
|
396
|
+
function gitDirtyLike(value) {
|
|
397
|
+
return value === undefined || value === null || typeof value !== 'boolean';
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Files that define the measurement system (evaluator), as opposed to the
|
|
401
|
+
* system under test (product retrieval code). Any change to these files
|
|
402
|
+
* changes the meaning of every score produced afterwards, so their content
|
|
403
|
+
* hash is recorded in every run manifest and must be identical between a
|
|
404
|
+
* baseline and a candidate for a comparison to count as a clean ablation.
|
|
405
|
+
*
|
|
406
|
+
* Product code (src/retrieval, src/indexer, src/embeddings, src/db, src/mcp)
|
|
407
|
+
* is intentionally NOT listed: it is the candidate being measured.
|
|
408
|
+
*/
|
|
409
|
+
exports.EVALUATOR_SOURCES = [
|
|
410
|
+
'src/eval/hashEmbedding.ts',
|
|
411
|
+
'src/eval/evalCache.ts',
|
|
412
|
+
'scripts/eval/run-longmemeval-qa.ts',
|
|
413
|
+
'scripts/eval/score-longmemeval-qa.ts',
|
|
414
|
+
'scripts/eval/run-longmemeval-product.ts',
|
|
415
|
+
'src/eval/metrics.ts',
|
|
416
|
+
'src/eval/scoring.ts',
|
|
417
|
+
'src/eval/comparison.ts',
|
|
418
|
+
'src/eval/renorbenchBaseline.ts',
|
|
419
|
+
'src/eval/renorbenchProduct.ts',
|
|
420
|
+
'src/eval/renorbenchDocuments.ts',
|
|
421
|
+
'src/eval/runManifest.ts',
|
|
422
|
+
'src/eval/artifacts.ts',
|
|
423
|
+
'src/eval/longmemeval.ts',
|
|
424
|
+
'scripts/eval/run-renorbench.ts',
|
|
425
|
+
'scripts/eval/run-renorbench-product.ts',
|
|
426
|
+
'scripts/eval/renorbench-common.ts',
|
|
427
|
+
'scripts/eval/compare-runs.ts',
|
|
428
|
+
'scripts/eval/renorbench-baseline.ts',
|
|
429
|
+
'scripts/eval/score-retrieval.ts',
|
|
430
|
+
'scripts/eval/analyze-failures.ts',
|
|
431
|
+
'scripts/eval/generate-renorbench.ts',
|
|
432
|
+
'scripts/eval/run-longmemeval.ts',
|
|
433
|
+
'scripts/eval/merge-longmemeval-shards.ts',
|
|
434
|
+
'docs/evals/renorbench-v0.schema.json',
|
|
435
|
+
];
|
|
436
|
+
function sha256(data) {
|
|
437
|
+
return node_crypto_1.default.createHash('sha256').update(data).digest('hex');
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Content hash over the evaluator sources. Missing files are a hard error:
|
|
441
|
+
* a silently truncated evaluator hash would weaken the freeze.
|
|
442
|
+
*/
|
|
443
|
+
function evaluatorSourceHash(repoRoot) {
|
|
444
|
+
const parts = [];
|
|
445
|
+
for (const relative of exports.EVALUATOR_SOURCES) {
|
|
446
|
+
const full = node_path_1.default.join(repoRoot, relative);
|
|
447
|
+
if (!node_fs_1.default.existsSync(full)) {
|
|
448
|
+
throw new Error(`Evaluator source missing: ${relative}`);
|
|
449
|
+
}
|
|
450
|
+
parts.push(`${relative}\0${sha256(node_fs_1.default.readFileSync(full))}`);
|
|
451
|
+
}
|
|
452
|
+
return sha256(parts.join('\n'));
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Git provenance for a run. `dirty === true` means the executed code does not
|
|
456
|
+
* match the recorded SHA, which invalidates promotion-grade comparisons.
|
|
457
|
+
*/
|
|
458
|
+
function gitState(repoRoot) {
|
|
459
|
+
try {
|
|
460
|
+
const sha = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' }).trim();
|
|
461
|
+
const status = (0, node_child_process_1.execFileSync)('git', ['status', '--porcelain'], { cwd: repoRoot, encoding: 'utf8' });
|
|
462
|
+
const dirty = status.trim().length > 0;
|
|
463
|
+
return { sha, dirty, statusSha256: dirty ? sha256(status) : null };
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return { sha: 'unknown', dirty: true, statusSha256: null };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function datasetSha256File(filePath) {
|
|
470
|
+
return sha256(node_fs_1.default.readFileSync(filePath));
|
|
471
|
+
}
|
|
472
|
+
function packageLockSha256(repoRoot) {
|
|
473
|
+
try {
|
|
474
|
+
return sha256(node_fs_1.default.readFileSync(node_path_1.default.join(repoRoot, 'package-lock.json')));
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scoreRetrieval = scoreRetrieval;
|
|
4
|
+
const metrics_1 = require("./metrics");
|
|
5
|
+
function emptyAggregate() {
|
|
6
|
+
return {
|
|
7
|
+
count: 0,
|
|
8
|
+
answerable: 0,
|
|
9
|
+
abstention: 0,
|
|
10
|
+
hit1: 0,
|
|
11
|
+
hit5: 0,
|
|
12
|
+
hit10: 0,
|
|
13
|
+
recall1: 0,
|
|
14
|
+
recall5: 0,
|
|
15
|
+
recall10: 0,
|
|
16
|
+
mrr: 0,
|
|
17
|
+
ndcg5: 0,
|
|
18
|
+
ndcg10: 0,
|
|
19
|
+
abstentionEmpty: 0,
|
|
20
|
+
supersessionSensitive: 0,
|
|
21
|
+
contradictionFirst: 0,
|
|
22
|
+
recallAll5: 0,
|
|
23
|
+
recallAll10: 0,
|
|
24
|
+
forbiddenSensitive: 0,
|
|
25
|
+
forbiddenRetrievals: 0,
|
|
26
|
+
latencyMs: [],
|
|
27
|
+
contextTokens: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function normalized(value) {
|
|
31
|
+
return (value ?? '').trim().toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
function validateCase(test, seenCaseIds) {
|
|
34
|
+
if (seenCaseIds.has(test.id)) {
|
|
35
|
+
throw new Error(`Duplicate eval case id: ${test.id}`);
|
|
36
|
+
}
|
|
37
|
+
seenCaseIds.add(test.id);
|
|
38
|
+
const seenGold = new Set();
|
|
39
|
+
for (const target of test.relevant) {
|
|
40
|
+
const key = `${normalized(target.match)}\0${normalized(target.notePath ?? '')}`;
|
|
41
|
+
if (seenGold.has(key)) {
|
|
42
|
+
throw new Error(`Duplicate gold target in case ${test.id}: ${target.match}`);
|
|
43
|
+
}
|
|
44
|
+
seenGold.add(key);
|
|
45
|
+
}
|
|
46
|
+
if (!test.shouldAbstain && test.relevant.length > 0) {
|
|
47
|
+
const hasPositive = test.relevant.some(target => (0, metrics_1.roleOf)(target) === 'positive');
|
|
48
|
+
if (!hasPositive) {
|
|
49
|
+
throw new Error(`Eval case ${test.id} has gold targets but no positive-role target; binary metrics could never credit it.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const target of test.requiredEvidence ?? []) {
|
|
53
|
+
if ((0, metrics_1.roleOf)(target) === 'forbidden') {
|
|
54
|
+
throw new Error(`Eval case ${test.id} marks required evidence as forbidden.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function addCase(agg, test, result) {
|
|
59
|
+
agg.count += 1;
|
|
60
|
+
if (test.forbiddenEvidence && test.forbiddenEvidence.length > 0) {
|
|
61
|
+
agg.forbiddenSensitive += 1;
|
|
62
|
+
if ((0, metrics_1.forbiddenRetrieved)(result.hits, test.forbiddenEvidence, 10))
|
|
63
|
+
agg.forbiddenRetrievals += 1;
|
|
64
|
+
}
|
|
65
|
+
const abstain = Boolean(test.shouldAbstain) || test.relevant.length === 0;
|
|
66
|
+
if (abstain) {
|
|
67
|
+
agg.abstention += 1;
|
|
68
|
+
if (result.hits.length === 0)
|
|
69
|
+
agg.abstentionEmpty += 1;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
agg.answerable += 1;
|
|
73
|
+
agg.hit1 += (0, metrics_1.hitAtK)(result.hits, test.relevant, 1);
|
|
74
|
+
agg.hit5 += (0, metrics_1.hitAtK)(result.hits, test.relevant, 5);
|
|
75
|
+
agg.hit10 += (0, metrics_1.hitAtK)(result.hits, test.relevant, 10);
|
|
76
|
+
agg.recall1 += (0, metrics_1.recallAtK)(result.hits, test.relevant, 1);
|
|
77
|
+
agg.recall5 += (0, metrics_1.recallAtK)(result.hits, test.relevant, 5);
|
|
78
|
+
agg.recall10 += (0, metrics_1.recallAtK)(result.hits, test.relevant, 10);
|
|
79
|
+
agg.mrr += (0, metrics_1.reciprocalRank)(result.hits, test.relevant);
|
|
80
|
+
agg.ndcg5 += (0, metrics_1.ndcgAtK)(result.hits, test.relevant, 5);
|
|
81
|
+
agg.ndcg10 += (0, metrics_1.ndcgAtK)(result.hits, test.relevant, 10);
|
|
82
|
+
const required = test.requiredEvidence
|
|
83
|
+
?? test.relevant.filter(target => (0, metrics_1.roleOf)(target) === 'positive');
|
|
84
|
+
if (required.length > 0) {
|
|
85
|
+
agg.recallAll5 += (0, metrics_1.recallAllAtK)(result.hits, required, 5);
|
|
86
|
+
agg.recallAll10 += (0, metrics_1.recallAllAtK)(result.hits, required, 10);
|
|
87
|
+
}
|
|
88
|
+
if ((0, metrics_1.contradictionFirst)(result.hits, test.relevant))
|
|
89
|
+
agg.contradictionFirst += 1;
|
|
90
|
+
if (test.relevant.some(target => (0, metrics_1.roleOf)(target) === 'positive')
|
|
91
|
+
&& test.relevant.some(target => (0, metrics_1.roleOf)(target) === 'forbidden')) {
|
|
92
|
+
agg.supersessionSensitive += 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (typeof result.latencyMs === 'number')
|
|
96
|
+
agg.latencyMs.push(result.latencyMs);
|
|
97
|
+
if (typeof result.contextTokens === 'number')
|
|
98
|
+
agg.contextTokens.push(result.contextTokens);
|
|
99
|
+
}
|
|
100
|
+
function round(value) {
|
|
101
|
+
return Math.round(value * 10000) / 10000;
|
|
102
|
+
}
|
|
103
|
+
function mean(values) {
|
|
104
|
+
if (values.length === 0)
|
|
105
|
+
return null;
|
|
106
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
107
|
+
}
|
|
108
|
+
function percentile(values, p) {
|
|
109
|
+
if (values.length === 0)
|
|
110
|
+
return null;
|
|
111
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
112
|
+
const index = Math.min(sorted.length - 1, Math.ceil(p * sorted.length) - 1);
|
|
113
|
+
return sorted[Math.max(0, index)];
|
|
114
|
+
}
|
|
115
|
+
function summarize(agg) {
|
|
116
|
+
const a = Math.max(1, agg.answerable);
|
|
117
|
+
const z = Math.max(1, agg.abstention);
|
|
118
|
+
return {
|
|
119
|
+
cases: agg.count,
|
|
120
|
+
answerableCases: agg.answerable,
|
|
121
|
+
abstentionCases: agg.abstention,
|
|
122
|
+
hitAt1: round(agg.hit1 / a),
|
|
123
|
+
hitAt5: round(agg.hit5 / a),
|
|
124
|
+
hitAt10: round(agg.hit10 / a),
|
|
125
|
+
recallAt1: round(agg.recall1 / a),
|
|
126
|
+
recallAt5: round(agg.recall5 / a),
|
|
127
|
+
recallAt10: round(agg.recall10 / a),
|
|
128
|
+
recallAllAt5: round(agg.recallAll5 / a),
|
|
129
|
+
recallAllAt10: round(agg.recallAll10 / a),
|
|
130
|
+
mrr: round(agg.mrr / a),
|
|
131
|
+
ndcgAt5: round(agg.ndcg5 / a),
|
|
132
|
+
ndcgAt10: round(agg.ndcg10 / a),
|
|
133
|
+
abstentionEmptyRate: agg.abstention ? round(agg.abstentionEmpty / z) : null,
|
|
134
|
+
supersessionSensitiveCases: agg.supersessionSensitive,
|
|
135
|
+
contradictionFirstRate: agg.supersessionSensitive
|
|
136
|
+
? round(agg.contradictionFirst / agg.supersessionSensitive)
|
|
137
|
+
: null,
|
|
138
|
+
forbiddenSensitiveCases: agg.forbiddenSensitive,
|
|
139
|
+
forbiddenRetrievalRate: agg.forbiddenSensitive
|
|
140
|
+
? round(agg.forbiddenRetrievals / agg.forbiddenSensitive)
|
|
141
|
+
: null,
|
|
142
|
+
meanLatencyMs: mean(agg.latencyMs) === null ? null : round(mean(agg.latencyMs)),
|
|
143
|
+
p50LatencyMs: percentile(agg.latencyMs, 0.5) === null ? null : round(percentile(agg.latencyMs, 0.5)),
|
|
144
|
+
p95LatencyMs: percentile(agg.latencyMs, 0.95) === null ? null : round(percentile(agg.latencyMs, 0.95)),
|
|
145
|
+
meanContextTokens: mean(agg.contextTokens) === null ? null : round(mean(agg.contextTokens)),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function scoreRetrieval(cases, results) {
|
|
149
|
+
if (!Array.isArray(cases) || cases.length === 0)
|
|
150
|
+
throw new Error('Empty eval dataset');
|
|
151
|
+
const seenCaseIds = new Set();
|
|
152
|
+
for (const test of cases)
|
|
153
|
+
validateCase(test, seenCaseIds);
|
|
154
|
+
const seenResultIds = new Set();
|
|
155
|
+
for (const result of results) {
|
|
156
|
+
if (seenResultIds.has(result.id)) {
|
|
157
|
+
throw new Error(`Duplicate result id: ${result.id}`);
|
|
158
|
+
}
|
|
159
|
+
seenResultIds.add(result.id);
|
|
160
|
+
if (!Array.isArray(result.hits))
|
|
161
|
+
throw new Error(`Invalid hits for result: ${result.id}`);
|
|
162
|
+
// Measurement validity: a nonfinite or negative timing/context sample must
|
|
163
|
+
// fail the run, not silently skew mean/p95 latency or cost (independent review).
|
|
164
|
+
for (const [key, value] of [['latencyMs', result.latencyMs], ['contextTokens', result.contextTokens]]) {
|
|
165
|
+
if (value !== undefined && (!Number.isFinite(value) || value < 0 || (key === 'contextTokens' && !Number.isInteger(value)))) {
|
|
166
|
+
throw new Error(`Invalid ${key} for result: ${result.id}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const hit of result.hits) {
|
|
170
|
+
if ((hit.text !== undefined && typeof hit.text !== 'string')
|
|
171
|
+
|| (hit.notePath !== undefined && typeof hit.notePath !== 'string')
|
|
172
|
+
|| (hit.score !== undefined && !Number.isFinite(hit.score))) {
|
|
173
|
+
throw new Error(`Invalid hit for result: ${result.id}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const byId = new Map(results.map(result => [result.id, result]));
|
|
178
|
+
const overall = emptyAggregate();
|
|
179
|
+
const categories = new Map();
|
|
180
|
+
for (const test of cases) {
|
|
181
|
+
const result = byId.get(test.id);
|
|
182
|
+
if (!result)
|
|
183
|
+
throw new Error(`Missing result for eval case: ${test.id}`);
|
|
184
|
+
addCase(overall, test, result);
|
|
185
|
+
const category = categories.get(test.category) ?? emptyAggregate();
|
|
186
|
+
addCase(category, test, result);
|
|
187
|
+
categories.set(test.category, category);
|
|
188
|
+
}
|
|
189
|
+
const unknownIds = results.filter(result => !seenCaseIds.has(result.id)).map(result => result.id);
|
|
190
|
+
if (unknownIds.length) {
|
|
191
|
+
throw new Error(`Results contain unknown case ids: ${unknownIds.join(', ')}`);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
overall: summarize(overall),
|
|
195
|
+
byCategory: Object.fromEntries([...categories.entries()].map(([name, agg]) => [name, summarize(agg)])),
|
|
196
|
+
};
|
|
197
|
+
}
|