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.
Files changed (176) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +73 -0
  3. package/README.md +105 -2
  4. package/dist/BUILD.json +6 -0
  5. package/dist/api/apiAuth.js +43 -0
  6. package/dist/api/server.js +172 -0
  7. package/dist/buildIdentity.js +132 -0
  8. package/dist/cli-extractors.js +46 -0
  9. package/dist/cli.js +491 -0
  10. package/dist/config.js +65 -0
  11. package/dist/context/composer.js +320 -0
  12. package/dist/context/readHonesty.js +478 -0
  13. package/dist/context/rowStore.js +238 -0
  14. package/dist/conversation/formatter.js +81 -0
  15. package/dist/conversation/parser.js +189 -0
  16. package/dist/conversation/types.js +2 -0
  17. package/dist/conversation/writer.js +42 -0
  18. package/dist/db/client.js +147 -0
  19. package/dist/db/indexSalvage.js +81 -0
  20. package/dist/db/migrations.js +642 -0
  21. package/dist/db/readCache.js +109 -0
  22. package/dist/db/schema.js +5 -0
  23. package/dist/db/statements.js +89 -0
  24. package/dist/digests/digestCache.js +142 -0
  25. package/dist/digests/digests.js +597 -0
  26. package/dist/digests/worker.js +348 -0
  27. package/dist/embeddings/caching.js +337 -0
  28. package/dist/embeddings/coalescing.js +177 -0
  29. package/dist/embeddings/interactive.js +103 -0
  30. package/dist/embeddings/local.js +68 -0
  31. package/dist/embeddings/openai.js +105 -0
  32. package/dist/embeddings/rebuild.js +166 -0
  33. package/dist/embeddings/simdScan.js +258 -0
  34. package/dist/embeddings/space.js +21 -0
  35. package/dist/embeddings/types.js +14 -0
  36. package/dist/embeddings/vectorIndex.js +675 -0
  37. package/dist/embeddings/vertex.js +224 -0
  38. package/dist/eval/artifacts.js +42 -0
  39. package/dist/eval/cacheReport.js +139 -0
  40. package/dist/eval/comparison.js +182 -0
  41. package/dist/eval/context700-candidate.js +145 -0
  42. package/dist/eval/evalCache.js +525 -0
  43. package/dist/eval/hashEmbedding.js +35 -0
  44. package/dist/eval/longmemeval.js +56 -0
  45. package/dist/eval/metrics.js +181 -0
  46. package/dist/eval/renorbenchBaseline.js +486 -0
  47. package/dist/eval/renorbenchDocuments.js +134 -0
  48. package/dist/eval/renorbenchProduct.js +182 -0
  49. package/dist/eval/runLedger.js +279 -0
  50. package/dist/eval/runManifest.js +479 -0
  51. package/dist/eval/scoring.js +197 -0
  52. package/dist/evidence/attach.js +267 -0
  53. package/dist/evidence/background.js +147 -0
  54. package/dist/evidence/evidenceCache.js +182 -0
  55. package/dist/evidence/gate.js +37 -0
  56. package/dist/evidence/keyPool.js +85 -0
  57. package/dist/evidence/ollamaChat.js +84 -0
  58. package/dist/evidence/optin.js +165 -0
  59. package/dist/evidence/options.js +130 -0
  60. package/dist/evidence/secondPass.js +456 -0
  61. package/dist/extraction/classifyCapture.js +34 -0
  62. package/dist/extraction/decision-model.js +174 -0
  63. package/dist/extraction/eventExtractor.js +113 -0
  64. package/dist/extraction/extractionClient.js +122 -0
  65. package/dist/extraction/factExtractor.js +46 -0
  66. package/dist/extraction/factsWritePath.js +179 -0
  67. package/dist/extraction/preferenceExtractor.js +153 -0
  68. package/dist/extraction/secretGuard.js +176 -0
  69. package/dist/extraction/secretScanner.js +665 -0
  70. package/dist/extraction/typedFacts.js +224 -0
  71. package/dist/extraction/writePathAdjudication.js +110 -0
  72. package/dist/importing/apply.js +644 -0
  73. package/dist/importing/bundle-write.js +80 -0
  74. package/dist/importing/cli.js +407 -0
  75. package/dist/importing/existing.js +49 -0
  76. package/dist/importing/facts.js +123 -0
  77. package/dist/importing/forgetImported.js +79 -0
  78. package/dist/importing/hosted.js +467 -0
  79. package/dist/importing/index.js +29 -0
  80. package/dist/importing/journalLog.js +96 -0
  81. package/dist/importing/markdown.js +98 -0
  82. package/dist/importing/plan.js +206 -0
  83. package/dist/importing/secretScreen.js +113 -0
  84. package/dist/importing/serialize.js +188 -0
  85. package/dist/importing/source.js +463 -0
  86. package/dist/importing/types.js +6 -0
  87. package/dist/indexer/chunker.js +319 -0
  88. package/dist/indexer/dailyEntries.js +63 -0
  89. package/dist/indexer/hasher.js +10 -0
  90. package/dist/indexer/laneRuns.js +48 -0
  91. package/dist/indexer/o200k.js +481 -0
  92. package/dist/indexer/o200k.json +8 -0
  93. package/dist/indexer/outsideRoot.js +77 -0
  94. package/dist/indexer/parser.js +167 -0
  95. package/dist/indexer/pipeline.js +1168 -0
  96. package/dist/indexer/staleness.js +201 -0
  97. package/dist/indexer/tokenizer.js +77 -0
  98. package/dist/indexer/walker.js +184 -0
  99. package/dist/mcp/hive.js +88 -0
  100. package/dist/mcp/requestErrors.js +47 -0
  101. package/dist/mcp/server.js +396 -0
  102. package/dist/memory/agentLease.js +43 -0
  103. package/dist/memory/confinedWrite.js +315 -0
  104. package/dist/memory/controls.js +73 -0
  105. package/dist/memory/dailyNote.js +265 -0
  106. package/dist/memory/durableAppend.js +57 -0
  107. package/dist/memory/factConflicts.js +203 -0
  108. package/dist/memory/factTerms.js +321 -0
  109. package/dist/memory/hiveNameHeader.cjs +10 -0
  110. package/dist/memory/inventory.js +55 -0
  111. package/dist/memory/invisibleText.js +20 -0
  112. package/dist/memory/lifecycle.js +1756 -0
  113. package/dist/memory/limits.js +13 -0
  114. package/dist/memory/requests.js +183 -0
  115. package/dist/memory/sourceName.js +200 -0
  116. package/dist/memory/whatsNew.js +158 -0
  117. package/dist/memory/writer.js +175 -0
  118. package/dist/memory/writerConflicts.js +85 -0
  119. package/dist/memorymap/memoryMap.js +1568 -0
  120. package/dist/memorymap/replacedJournal.js +58 -0
  121. package/dist/memorymap/replacements.js +405 -0
  122. package/dist/mvp/activity.js +86 -0
  123. package/dist/mvp/agentMemory.js +1744 -0
  124. package/dist/mvp/auth.js +531 -0
  125. package/dist/mvp/cli.js +73 -0
  126. package/dist/mvp/client-store.js +462 -0
  127. package/dist/mvp/compileCache.js +249 -0
  128. package/dist/mvp/compileSeed.js +27 -0
  129. package/dist/mvp/config.js +288 -0
  130. package/dist/mvp/connectionStatus.js +80 -0
  131. package/dist/mvp/contextRelevance.js +413 -0
  132. package/dist/mvp/engine.js +1065 -0
  133. package/dist/mvp/exportAll.js +359 -0
  134. package/dist/mvp/factLanePack.js +77 -0
  135. package/dist/mvp/factMarkers.js +49 -0
  136. package/dist/mvp/factsLane.js +203 -0
  137. package/dist/mvp/hiddenSpans.js +585 -0
  138. package/dist/mvp/history.js +413 -0
  139. package/dist/mvp/hitDates.js +31 -0
  140. package/dist/mvp/hitFloor.js +36 -0
  141. package/dist/mvp/identity.js +18 -0
  142. package/dist/mvp/idle.js +113 -0
  143. package/dist/mvp/memoryDir.js +98 -0
  144. package/dist/mvp/posthogMcp.js +210 -0
  145. package/dist/mvp/readScope.js +104 -0
  146. package/dist/mvp/readUsefulness.js +222 -0
  147. package/dist/mvp/sanitize.js +48 -0
  148. package/dist/mvp/schemaRejections.js +91 -0
  149. package/dist/mvp/scope.js +127 -0
  150. package/dist/mvp/server.js +2089 -0
  151. package/dist/mvp/serverCard.js +29 -0
  152. package/dist/mvp/storedSize.js +139 -0
  153. package/dist/mvp/telemetry.js +441 -0
  154. package/dist/retrieval/adviceLines.js +195 -0
  155. package/dist/retrieval/adviceQuestion.js +44 -0
  156. package/dist/retrieval/conversationShape.js +44 -0
  157. package/dist/retrieval/dailyEntryTime.js +55 -0
  158. package/dist/retrieval/entityLookup.js +113 -0
  159. package/dist/retrieval/factsFirst.js +188 -0
  160. package/dist/retrieval/keyword.js +84 -0
  161. package/dist/retrieval/ledgerEventTime.js +51 -0
  162. package/dist/retrieval/ledgerLiveness.js +83 -0
  163. package/dist/retrieval/ledgerPresentation.js +161 -0
  164. package/dist/retrieval/noteIdentity.js +57 -0
  165. package/dist/retrieval/orchestrator.js +1086 -0
  166. package/dist/retrieval/queryHelpers.js +142 -0
  167. package/dist/retrieval/questionSpan.js +475 -0
  168. package/dist/retrieval/readerEvents.js +690 -0
  169. package/dist/retrieval/readerLayout.js +503 -0
  170. package/dist/retrieval/relativeTimeHints.js +113 -0
  171. package/dist/retrieval/semantic.js +67 -0
  172. package/dist/router/routerCache.js +106 -0
  173. package/dist/router/sessionRouter.js +726 -0
  174. package/dist/toolRefusal.js +120 -0
  175. package/dist/watcher/watcher.js +75 -0
  176. package/package.json +120 -4
@@ -0,0 +1,486 @@
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.DEFAULT_BASE_REF = exports.RENORBENCH_ARMS = exports.DOCUMENTS_ARM = exports.PRODUCT_V2_ARM = exports.PRODUCT_ARM = exports.CHUNK_ARM = exports.DOCUMENTS_BASELINE_RELATIVE_PATH = exports.PRODUCT_V2_BASELINE_RELATIVE_PATH = exports.PRODUCT_BASELINE_RELATIVE_PATH = exports.BASELINE_RELATIVE_PATH = exports.BASELINE_BENCHMARK = exports.BASELINE_SCHEMA = void 0;
7
+ exports.armFor = armFor;
8
+ exports.loadRunDir = loadRunDir;
9
+ exports.buildBaseline = buildBaseline;
10
+ exports.readBaseline = readBaseline;
11
+ exports.parseBaseline = parseBaseline;
12
+ exports.checkAgainstBaseline = checkAgainstBaseline;
13
+ exports.readMergeBaseState = readMergeBaseState;
14
+ exports.compareToFloor = compareToFloor;
15
+ exports.recordsLoweringOf = recordsLoweringOf;
16
+ exports.compareToOwnScore = compareToOwnScore;
17
+ exports.checkWithMergeBase = checkWithMergeBase;
18
+ exports.planRecord = planRecord;
19
+ /**
20
+ * The committed RenorBench baseline and the no-regression check the agent gate runs against it.
21
+ *
22
+ * The baseline is a compact record of one hash-embedder run (manifest invariants + scored metrics),
23
+ * small enough that a PR that moves it shows exactly which numbers moved. The verdict comes from the
24
+ * repo's own comparator (`compareRuns`, tolerance 0), never from a second opinion about what
25
+ * "worse" means. Latency is not recorded: it is noise and is not a gate metric.
26
+ *
27
+ * This file is part of the frozen evaluator (EVALUATOR_SOURCES): the gate cannot loosen its own exam
28
+ * without moving the evaluator hash, which the check refuses until the baseline is deliberately refreshed.
29
+ *
30
+ * The baseline file is an ordinary tracked file, so no number in it is trusted: `check` re-scores the run and
31
+ * requires the committed summary to EQUAL it, on every path, before anything else. The merge-base (origin/main)
32
+ * baseline is read only to authorise a LOWER baseline: it must record a deliberate lowering that names the exact
33
+ * merge-base baseline it lowers (`lowering.fromBaselineSha256`) and exactly the metrics that fell, written by
34
+ * `record --accept-lower`.
35
+ *
36
+ * There are four arms, told apart by the `benchmark` id of a run and of its baseline file: the chunk arm (the
37
+ * retrieval hits at top-K 10), the product arm (the reply of the shipped `search_memory` tool), the product arm on the
38
+ * v2 suite (a larger vault, with questions that need session expansion, the newest-value rules and the verdict) and the
39
+ * product arm on the documents suite (handbook-style notes: what the expansion delivers from a document). Each has
40
+ * its own baseline file, tracked metrics, scorer and identity fields (RENORBENCH_ARMS); every rule below applies to all of
41
+ * them, with the arm taken from the data, so a check can never judge a run by another arm's metrics.
42
+ */
43
+ const node_child_process_1 = require("node:child_process");
44
+ const node_fs_1 = __importDefault(require("node:fs"));
45
+ const node_path_1 = __importDefault(require("node:path"));
46
+ const comparison_1 = require("./comparison");
47
+ const scoring_1 = require("./scoring");
48
+ const renorbenchDocuments_1 = require("./renorbenchDocuments");
49
+ const renorbenchProduct_1 = require("./renorbenchProduct");
50
+ const artifacts_1 = require("./artifacts");
51
+ exports.BASELINE_SCHEMA = 1;
52
+ /** The chunk arm's benchmark id: what `benchmark` reads in its runs and in its baseline. */
53
+ exports.BASELINE_BENCHMARK = 'renorbench-v1';
54
+ /** The chunk arm's baseline, relative to the engine root. */
55
+ exports.BASELINE_RELATIVE_PATH = 'docs/evals/baselines/renorbench-v1-hash.json';
56
+ /** The product arm's baseline, relative to the engine root. */
57
+ exports.PRODUCT_BASELINE_RELATIVE_PATH = 'docs/evals/baselines/renorbench-v1-product-hash.json';
58
+ /** The product arm's baseline on the v2 suite, relative to the engine root. */
59
+ exports.PRODUCT_V2_BASELINE_RELATIVE_PATH = 'docs/evals/baselines/renorbench-v2-product-hash.json';
60
+ /** The product arm's baseline on the documents suite, relative to the engine root. */
61
+ exports.DOCUMENTS_BASELINE_RELATIVE_PATH = 'docs/evals/baselines/renorbench-docs-product-hash.json';
62
+ exports.CHUNK_ARM = {
63
+ benchmark: exports.BASELINE_BENCHMARK,
64
+ name: 'chunk',
65
+ baselineRelativePath: exports.BASELINE_RELATIVE_PATH,
66
+ metrics: comparison_1.QUALITY_METRICS,
67
+ score: (cases, results) => (0, scoring_1.scoreRetrieval)(cases, results),
68
+ annotationKeys: ['toolPath'],
69
+ identityFields: [],
70
+ runCommand: 'npm run eval:renor:hash',
71
+ };
72
+ exports.PRODUCT_ARM = {
73
+ benchmark: renorbenchProduct_1.PRODUCT_BENCHMARK,
74
+ name: 'product',
75
+ baselineRelativePath: exports.PRODUCT_BASELINE_RELATIVE_PATH,
76
+ metrics: renorbenchProduct_1.PRODUCT_QUALITY_METRICS,
77
+ score: (cases, results) => (0, renorbenchProduct_1.scoreProductArm)(cases, results),
78
+ annotationKeys: ['productPath'],
79
+ identityFields: ['entryPoint', 'productMode'],
80
+ runCommand: 'npm run eval:renor:product:hash',
81
+ };
82
+ exports.PRODUCT_V2_ARM = {
83
+ benchmark: renorbenchProduct_1.PRODUCT_V2_BENCHMARK,
84
+ name: 'product-v2',
85
+ baselineRelativePath: exports.PRODUCT_V2_BASELINE_RELATIVE_PATH,
86
+ metrics: renorbenchProduct_1.PRODUCT_V2_QUALITY_METRICS,
87
+ score: (cases, results) => (0, renorbenchProduct_1.scoreProductArm)(cases, results),
88
+ annotationKeys: ['productPath'],
89
+ identityFields: ['entryPoint', 'productMode'],
90
+ runCommand: 'npm run eval:renor:product:v2:hash',
91
+ };
92
+ /**
93
+ * The documents suite: handbook-style notes with unique headings, which the other suites have none of. It is scored by
94
+ * whether the probed section reaches the reply, in `search_memory` and in `get_context` at 800 tokens, never by quality: the
95
+ * run is the hash embedder's and reports as infrastructure. `documentsPath` (what a reply costs) is annotated, not scored.
96
+ */
97
+ exports.DOCUMENTS_ARM = {
98
+ benchmark: renorbenchDocuments_1.DOCUMENTS_BENCHMARK,
99
+ name: 'documents',
100
+ baselineRelativePath: exports.DOCUMENTS_BASELINE_RELATIVE_PATH,
101
+ metrics: renorbenchDocuments_1.DOCUMENTS_QUALITY_METRICS,
102
+ score: (cases, results) => (0, renorbenchDocuments_1.scoreDocumentsArm)(cases, results),
103
+ annotationKeys: ['productPath', 'documentsPath'],
104
+ identityFields: ['entryPoint', 'productMode'],
105
+ runCommand: 'npm run eval:renor:product:docs:hash',
106
+ };
107
+ exports.RENORBENCH_ARMS = [exports.CHUNK_ARM, exports.PRODUCT_ARM, exports.PRODUCT_V2_ARM, exports.DOCUMENTS_ARM];
108
+ /** The arm a run or baseline belongs to, by its `benchmark` id. An id that names no arm is refused, never guessed. */
109
+ function armFor(benchmark) {
110
+ const arm = exports.RENORBENCH_ARMS.find(candidate => candidate.benchmark === benchmark);
111
+ if (!arm)
112
+ throw new Error(`benchmark ${String(benchmark)} is not a RenorBench arm (expected ${exports.RENORBENCH_ARMS.map(a => a.benchmark).join(' or ')})`);
113
+ return arm;
114
+ }
115
+ /** The command that re-records the baseline of `arm`, as the messages below quote it. */
116
+ function recordCommand(arm) {
117
+ return `npm run eval:renorbench:baseline -- ${arm.name === 'chunk' ? '' : `--arm ${arm.name} `}record <run dir> --reason "<why>"`;
118
+ }
119
+ function refreshHint(arm) {
120
+ const changed = arm.name === 'chunk' ? 'the evaluator, chunking or fixtures' : 'the evaluator, chunking, fixtures or how the arm is run';
121
+ return `If you changed ${changed} on purpose, refresh the baseline with \`${recordCommand(arm)}\` and say why in the PR body.`;
122
+ }
123
+ function parseJsonl(bytes) {
124
+ return bytes.toString().trim().split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
125
+ }
126
+ /**
127
+ * Load a run directory and re-score it from its raw results: a supplied summary is never trusted. The arm that
128
+ * scores it is the one its manifest names. The runner's own annotation (`toolPath`, `productPath`) next to the
129
+ * scored summary is not part of it.
130
+ */
131
+ function loadRunDir(dir) {
132
+ const manifest = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, 'manifest.json'), 'utf8'));
133
+ const arm = armFor(manifest.benchmark);
134
+ const casesBytes = node_fs_1.default.readFileSync(node_path_1.default.join(dir, 'cases.jsonl'));
135
+ if (!manifest.caseSnapshotSha256 || (0, artifacts_1.sha256)(casesBytes) !== manifest.caseSnapshotSha256) {
136
+ throw new Error(`cases.jsonl does not match the manifest hash: ${dir}`);
137
+ }
138
+ const resultsBytes = node_fs_1.default.readFileSync(node_path_1.default.join(dir, 'results.jsonl'));
139
+ if (manifest.resultsSha256 && (0, artifacts_1.sha256)(resultsBytes) !== manifest.resultsSha256) {
140
+ throw new Error(`results.jsonl does not match the manifest hash: ${dir}`);
141
+ }
142
+ const summary = arm.score(parseJsonl(casesBytes), parseJsonl(resultsBytes));
143
+ const supplied = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, 'summary.json'), 'utf8'));
144
+ for (const key of arm.annotationKeys)
145
+ delete supplied[key];
146
+ if (JSON.stringify(summary) !== JSON.stringify(supplied)) {
147
+ throw new Error(`summary.json does not match the re-scored results (tampered or stale): ${dir}`);
148
+ }
149
+ return { manifest, summary };
150
+ }
151
+ function withoutLatency(map) {
152
+ return Object.fromEntries(Object.entries(map).filter(([key]) => !/LatencyMs$/.test(key)));
153
+ }
154
+ function buildBaseline(input) {
155
+ const { manifest, summary } = input.run;
156
+ const arm = armFor(manifest.benchmark);
157
+ return {
158
+ schema: exports.BASELINE_SCHEMA,
159
+ benchmark: arm.benchmark,
160
+ reason: input.reason,
161
+ recordedAt: input.recordedAt,
162
+ gitSha: manifest.gitSha ?? null,
163
+ manifest: {
164
+ benchmark: manifest.benchmark,
165
+ casesSha256: manifest.casesSha256,
166
+ fixtureSha256: manifest.fixtureSha256,
167
+ topK: manifest.topK,
168
+ provider: manifest.provider,
169
+ qualityEligible: manifest.qualityEligible,
170
+ evaluatorSha256: manifest.evaluatorSha256,
171
+ chunking: manifest.chunking,
172
+ gitDirty: manifest.gitDirty,
173
+ ...Object.fromEntries(arm.identityFields.filter(key => manifest[key] !== undefined).map(key => [key, manifest[key]])),
174
+ },
175
+ summary: {
176
+ overall: withoutLatency(summary.overall),
177
+ byCategory: Object.fromEntries(Object.entries(summary.byCategory ?? {}).map(([name, map]) => [name, withoutLatency(map)])),
178
+ },
179
+ ...(input.lowering ? { lowering: input.lowering } : {}),
180
+ };
181
+ }
182
+ /**
183
+ * Read the committed baseline. Throws an Error whose message names what is wrong with it. With `arm`, a file that is
184
+ * another arm's baseline is refused, so a path that holds the wrong one can never judge a run.
185
+ */
186
+ function readBaseline(file, arm) {
187
+ let text;
188
+ try {
189
+ text = node_fs_1.default.readFileSync(file, 'utf8');
190
+ }
191
+ catch (err) {
192
+ throw new Error(`baseline ${file} is missing or unreadable (${err.code ?? 'read error'})`);
193
+ }
194
+ return parseBaseline(text, file, arm);
195
+ }
196
+ function parseBaseline(text, file, expected) {
197
+ let parsed;
198
+ try {
199
+ parsed = JSON.parse(text);
200
+ }
201
+ catch {
202
+ throw new Error(`baseline ${file} is not valid JSON`);
203
+ }
204
+ if (parsed.schema !== exports.BASELINE_SCHEMA)
205
+ throw new Error(`baseline ${file} has schema ${String(parsed.schema)}, expected ${exports.BASELINE_SCHEMA}`);
206
+ const arm = exports.RENORBENCH_ARMS.find(candidate => candidate.benchmark === parsed.benchmark);
207
+ if (!arm || (expected && arm !== expected)) {
208
+ throw new Error(`baseline ${file} is for benchmark ${String(parsed.benchmark)}, expected ${(expected ?? exports.CHUNK_ARM).benchmark}`);
209
+ }
210
+ if (!parsed.manifest || !parsed.summary?.overall)
211
+ throw new Error(`baseline ${file} has no manifest or no summary.overall`);
212
+ const missing = arm.metrics.map(metric => metric.key)
213
+ .filter(key => typeof parsed.summary.overall[key] !== 'number');
214
+ if (missing.length > 0)
215
+ throw new Error(`baseline ${file} lacks tracked metric(s): ${missing.join(', ')}`);
216
+ if (parsed.lowering !== undefined) {
217
+ const l = parsed.lowering;
218
+ if (!l || typeof l.reason !== 'string' || l.reason.trim().length < 12 || typeof l.fromBaselineSha256 !== 'string' || l.fromBaselineSha256.length === 0
219
+ || !Array.isArray(l.movedMetrics) || !l.movedMetrics.every((m) => typeof m === 'string')) {
220
+ throw new Error(`baseline ${file} has a malformed "lowering" (needs a reason of at least 12 characters, fromBaselineSha256 and movedMetrics, a list of metric names)`);
221
+ }
222
+ }
223
+ return parsed;
224
+ }
225
+ function movesOf(result, tracked) {
226
+ const metrics = [];
227
+ const regressed = [];
228
+ const improved = [];
229
+ for (const { key, higherBetter } of tracked) {
230
+ const move = { metric: key, ...result.quality[key] };
231
+ metrics.push(move);
232
+ if (move.baseline !== null && move.candidate === null) {
233
+ regressed.push(move); // a tracked metric that vanished is not "no change"
234
+ }
235
+ else if (move.delta !== null && move.delta !== 0) {
236
+ const better = higherBetter === move.delta > 0;
237
+ // More empty answers on abstention cases is not a quality gain: it can fail the gate, never praise it.
238
+ if (better && key === 'abstentionEmptyRate')
239
+ continue;
240
+ (better ? improved : regressed).push(move);
241
+ }
242
+ }
243
+ return { metrics, regressed, improved };
244
+ }
245
+ function incomparable(problem) {
246
+ return { ok: false, verdict: 'INCOMPARABLE', problem, regressed: [], improved: [], categoryRegressions: [], metrics: [] };
247
+ }
248
+ /** The no-regression verdict: any tracked quality metric that drops fails, equal and better pass. */
249
+ function checkAgainstBaseline(baseline, candidate) {
250
+ const arm = armFor(baseline.benchmark);
251
+ let result;
252
+ try {
253
+ result = (0, comparison_1.compareRuns)(baseline, candidate, { regressionGate: true, maxQualityRegression: 0, metrics: arm.metrics });
254
+ }
255
+ catch (err) {
256
+ return incomparable(`${err.message} ${refreshHint(arm)}`);
257
+ }
258
+ if (result.verdict === 'DESCRIPTIVE_ONLY') {
259
+ return incomparable(`the run is not comparable with the baseline: ${result.reasons.join(' ')} ${refreshHint(arm)}`);
260
+ }
261
+ // The same numbers from another configuration (a different tool call, an armed product flag, another verdict bar)
262
+ // measure something else: the run is judged only against a baseline made the way it was made.
263
+ const moved = arm.identityFields.filter(key => canonicalJson(baseline.manifest[key]) !== canonicalJson(candidate.manifest[key]));
264
+ if (moved.length > 0) {
265
+ const show = (manifest, key) => canonicalJson(manifest[key]).slice(0, 200);
266
+ return incomparable(`the run was not made the way the baseline was (${moved.map(key => `${key}: ${show(baseline.manifest, key)} (baseline) vs ${show(candidate.manifest, key)} (this run)`).join('; ')}). ${refreshHint(arm)}`);
267
+ }
268
+ const { metrics, regressed, improved } = movesOf(result, arm.metrics);
269
+ const failed = regressed.length > 0 || result.categoryRegressions.length > 0;
270
+ return {
271
+ ok: !failed,
272
+ verdict: failed ? 'REGRESSED' : improved.length > 0 ? 'IMPROVED' : 'EQUAL',
273
+ problem: null,
274
+ regressed,
275
+ improved,
276
+ categoryRegressions: result.categoryRegressions,
277
+ metrics,
278
+ };
279
+ }
280
+ exports.DEFAULT_BASE_REF = 'origin/main';
281
+ function git(cwd, args) {
282
+ return (0, node_child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
283
+ }
284
+ /**
285
+ * Read the baseline file as it was at the merge-base of HEAD and `baseRef`. Fails CLOSED: when git
286
+ * cannot say what the merge-base had, the floor is unknown, and an unknown floor is not a pass. The one
287
+ * exception is a baseline file that does not exist at the merge-base (the PR that introduces it).
288
+ */
289
+ function readMergeBaseState(baselineFile, baseRef = exports.DEFAULT_BASE_REF, arm) {
290
+ const dir = node_fs_1.default.existsSync(node_path_1.default.dirname(node_path_1.default.resolve(baselineFile))) ? node_fs_1.default.realpathSync(node_path_1.default.dirname(node_path_1.default.resolve(baselineFile))) : node_path_1.default.dirname(node_path_1.default.resolve(baselineFile));
291
+ const unavailable = (why) => ({
292
+ kind: 'unavailable',
293
+ problem: `cannot read the merge-base baseline (${why}); the floor is unknown, so the check fails closed. `
294
+ + `Fetch ${baseRef} (git fetch origin main; a shallow clone also needs git fetch --unshallow) and rerun, from a git checkout.`,
295
+ });
296
+ let cwd;
297
+ let mergeBase;
298
+ let spec;
299
+ try {
300
+ // Run from the repository root: ls-tree and show would otherwise read a path relative to the cwd.
301
+ cwd = git(dir, ['rev-parse', '--show-toplevel']);
302
+ mergeBase = git(cwd, ['merge-base', 'HEAD', baseRef]);
303
+ spec = node_path_1.default.relative(node_fs_1.default.realpathSync(cwd), node_path_1.default.join(dir, node_path_1.default.basename(baselineFile))).split(node_path_1.default.sep).join('/');
304
+ }
305
+ catch (err) {
306
+ const stderr = String(err.stderr ?? '').trim().split('\n')[0];
307
+ return unavailable(`git merge-base HEAD ${baseRef} failed${stderr ? `: ${stderr}` : ''}`);
308
+ }
309
+ let text;
310
+ try {
311
+ if (git(cwd, ['ls-tree', mergeBase, '--', spec]) === '')
312
+ return { kind: 'new' };
313
+ text = (0, node_child_process_1.execFileSync)('git', ['show', `${mergeBase}:${spec}`], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 });
314
+ }
315
+ catch (err) {
316
+ return unavailable(`git show ${mergeBase.slice(0, 8)}:${spec} failed: ${err.message.split('\n')[0]}`);
317
+ }
318
+ try {
319
+ return { kind: 'file', baseline: parseBaseline(text, `${baseRef} merge-base ${mergeBase.slice(0, 8)}:${spec}`, arm), sha256: (0, artifacts_1.sha256)(Buffer.from(text)) };
320
+ }
321
+ catch (err) {
322
+ return unavailable(err.message);
323
+ }
324
+ }
325
+ /**
326
+ * Hold a run to a baseline's NUMBERS, whatever else moved. Unlike `checkAgainstBaseline` it tolerates a
327
+ * changed evaluator, chunking or provider: those legitimately move the identity, but a lower score under
328
+ * a new identity is still a lower score. Only different benchmark data (cases / fixture) makes the two
329
+ * incomparable, and then the new baseline's identity is the reviewer's to read.
330
+ */
331
+ function compareToFloor(floor, candidate) {
332
+ const arm = armFor(floor.benchmark);
333
+ let result;
334
+ try {
335
+ result = (0, comparison_1.compareRuns)(floor, candidate, { regressionGate: true, allowEvaluatorChange: true, maxQualityRegression: 0, metrics: arm.metrics });
336
+ }
337
+ catch (err) {
338
+ return { comparable: false, note: err.message, regressed: [], categoryRegressions: [] };
339
+ }
340
+ return { comparable: true, note: null, regressed: movesOf(result, arm.metrics).regressed, categoryRegressions: result.categoryRegressions };
341
+ }
342
+ /** Does the committed baseline record a lowering of exactly this merge-base baseline? */
343
+ function recordsLoweringOf(baseline, mergeBase) {
344
+ return baseline.lowering?.fromBaselineSha256 === mergeBase.sha256;
345
+ }
346
+ function canonicalJson(value) {
347
+ if (Array.isArray(value))
348
+ return `[${value.map(canonicalJson).join(',')}]`;
349
+ if (value && typeof value === 'object') {
350
+ const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
351
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(',')}}`;
352
+ }
353
+ return JSON.stringify(value) ?? 'null';
354
+ }
355
+ function flattenSummary(summary) {
356
+ const flat = new Map();
357
+ for (const [key, value] of Object.entries(summary.overall))
358
+ flat.set(`overall.${key}`, value);
359
+ for (const [name, map] of Object.entries(summary.byCategory ?? {})) {
360
+ for (const [key, value] of Object.entries(map))
361
+ flat.set(`${name}.${key}`, value);
362
+ }
363
+ return flat;
364
+ }
365
+ /**
366
+ * Is the committed baseline's summary exactly what this tree scores? The check re-scores the live run
367
+ * from its raw results, so this is the one statement that binds the committed numbers to a measurement:
368
+ * a hand-edited number cannot equal it. A real lowering ships the worse code and is re-recorded, so its
369
+ * numbers match by construction.
370
+ */
371
+ function compareToOwnScore(baseline, run) {
372
+ const own = flattenSummary(buildBaseline({ run, reason: 'own score', recordedAt: '' }).summary);
373
+ const committed = flattenSummary(baseline.summary);
374
+ const differing = [];
375
+ const detail = [];
376
+ for (const key of new Set([...own.keys(), ...committed.keys()])) {
377
+ if (canonicalJson(own.get(key)) === canonicalJson(committed.get(key)))
378
+ continue;
379
+ differing.push(key);
380
+ detail.push(`${key} ${canonicalJson(committed.get(key))} (baseline) vs ${canonicalJson(own.get(key))} (this tree)`);
381
+ }
382
+ return { same: differing.length === 0, differing, detail };
383
+ }
384
+ /** At most a handful of names in a one-line message: the full lists stay in `regressed[]`. */
385
+ function briefList(names, max = 8) {
386
+ return names.length <= max ? names.join(', ') : `${names.slice(0, max).join(', ')}, ... (+${names.length - max} more)`;
387
+ }
388
+ function notOwnScore(check, own, arm) {
389
+ const shown = own.detail.slice(0, 4).join('; ');
390
+ const better = check.improved.length > 0
391
+ ? ` The tree scores better than the baseline on ${briefList(check.improved.map(m => m.metric))}: that is progress, so re-record to ratchet the floor up.`
392
+ : '';
393
+ return {
394
+ ...check,
395
+ ok: false,
396
+ verdict: 'INCOMPARABLE',
397
+ problem: `the committed baseline is not this tree's own score (${own.differing.length} number(s) differ: ${shown}${own.detail.length > 4 ? '; ...' : ''}).${better} `
398
+ + 'The baseline must equal what this tree measures, number for number: re-record it from a clean committed tree '
399
+ + `(\`${recordCommand(arm)}\`); a hand-edited number is refused.`,
400
+ };
401
+ }
402
+ /**
403
+ * The no-regression verdict the gate uses. Two rules, in this order:
404
+ *
405
+ * 1. The committed baseline must EQUAL this tree's own re-scored run, every stored number, on every path
406
+ * (new file, floor present, incomparable data, lowered or not). It is checked first and unconditionally,
407
+ * so a hand-edited number can never become the floor, whatever else the PR changed. Better code is
408
+ * ratcheted by re-recording in the same PR; worse code ships its lowering the same way.
409
+ * 2. The merge-base floor exists only to authorise a LOWER baseline: when the tree scores below the baseline
410
+ * the merge-base has, the file must record a deliberate lowering of that exact baseline, naming exactly the
411
+ * metrics that fell. The verdict is then LOWERED and never promotable. No merge-base, no pass (the file
412
+ * being new on the merge-base is the one case that needs no floor).
413
+ */
414
+ function checkWithMergeBase(baseline, candidate, mergeBase) {
415
+ const arm = armFor(baseline.benchmark);
416
+ const own = compareToOwnScore(baseline, candidate);
417
+ const check = checkAgainstBaseline(baseline, candidate);
418
+ if (!own.same)
419
+ return check.ok ? notOwnScore(check, own, arm) : check;
420
+ if (!check.ok)
421
+ return check;
422
+ if (mergeBase.kind === 'unavailable')
423
+ return incomparable(mergeBase.problem);
424
+ if (mergeBase.kind === 'new')
425
+ return { ...check, mergeBase: { state: 'new', sha256: null, comparable: true, note: 'the baseline file is new on the merge-base' } };
426
+ const floor = compareToFloor(mergeBase.baseline, candidate);
427
+ const info = { state: 'file', sha256: mergeBase.sha256, comparable: floor.comparable, note: floor.note };
428
+ const names = [...new Set([...floor.regressed.map(m => m.metric), ...floor.categoryRegressions])];
429
+ if (names.length === 0)
430
+ return { ...check, mergeBase: info };
431
+ const refuse = (why) => ({
432
+ ...check,
433
+ ok: false,
434
+ verdict: 'REGRESSED',
435
+ problem: `the run is below the baseline the merge-base has (${briefList(names)}): ${why} `
436
+ + `Fix the regression, or record it with \`${recordCommand(arm).replace('<run dir>', '<dir>')} --accept-lower\` and defend it in the PR body.`,
437
+ regressed: floor.regressed,
438
+ categoryRegressions: floor.categoryRegressions,
439
+ mergeBase: info,
440
+ });
441
+ if (!baseline.lowering)
442
+ return refuse('this baseline does not record a deliberate lowering of it.');
443
+ if (!recordsLoweringOf(baseline, mergeBase))
444
+ return refuse('the baseline records a lowering, but of a different baseline than the merge-base has (stale).');
445
+ const recorded = [...new Set(baseline.lowering.movedMetrics)].sort();
446
+ const actual = [...names].sort();
447
+ if (canonicalJson(recorded) !== canonicalJson(actual)) {
448
+ return refuse(`the recorded lowering names [${briefList(recorded)}] but the metrics actually below the floor are [${briefList(actual)}]; a lowering excuses exactly what it names.`);
449
+ }
450
+ return { ...check, ok: true, verdict: 'LOWERED', lowering: { reason: baseline.lowering.reason, moved: floor.regressed }, categoryRegressions: floor.categoryRegressions, mergeBase: info };
451
+ }
452
+ /**
453
+ * What `record` writes, or why it refuses. Numbers are judged against the merge-base's baseline and the
454
+ * file as it stands, across an evaluator or chunking change: only different benchmark data is incomparable.
455
+ * `lowering.movedMetrics` is derived from the merge-base floor alone, because that is what `check` compares it with.
456
+ */
457
+ function planRecord(input) {
458
+ const { run, previous, mergeBase, acceptLower } = input;
459
+ if (mergeBase.kind === 'unavailable')
460
+ throw new Error(mergeBase.problem);
461
+ const reason = input.reason.trim();
462
+ const arm = armFor(run.manifest.benchmark);
463
+ if (run.manifest.provider?.kind !== 'hash')
464
+ throw new Error(`the baseline is the hash-embedder run: re-run with \`${arm.runCommand}\``);
465
+ if (run.manifest.gitDirty !== false)
466
+ throw new Error('the run came from a dirty tree: commit first, then record, so the baseline names the code that produced it');
467
+ if (reason.length < 12)
468
+ throw new Error('--reason must say why (at least 12 characters)');
469
+ const againstMergeBase = mergeBase.kind === 'file' ? compareToFloor(mergeBase.baseline, run) : null;
470
+ const againstFile = previous ? compareToFloor(previous, run) : null;
471
+ const regressed = [...(againstMergeBase?.regressed ?? []), ...(againstFile?.regressed ?? [])];
472
+ const categoryRegressions = [...(againstMergeBase?.categoryRegressions ?? []), ...(againstFile?.categoryRegressions ?? [])];
473
+ const lowered = regressed.length > 0 || categoryRegressions.length > 0;
474
+ if (lowered && !acceptLower) {
475
+ const err = new Error('the new run is LOWER than the baseline (merge-base or committed): fix the regression, or pass --accept-lower and say why in the PR');
476
+ err.plan = { regressed, categoryRegressions };
477
+ throw err;
478
+ }
479
+ const floorMoved = againstMergeBase
480
+ ? [...new Set([...againstMergeBase.regressed.map(m => m.metric), ...againstMergeBase.categoryRegressions])]
481
+ : [];
482
+ const lowering = mergeBase.kind === 'file' && floorMoved.length > 0
483
+ ? { reason, fromBaselineSha256: mergeBase.sha256, movedMetrics: floorMoved }
484
+ : undefined;
485
+ return { baseline: buildBaseline({ run, reason, recordedAt: input.recordedAt, lowering }), lowered, regressed, categoryRegressions };
486
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DOCUMENTS_QUALITY_METRICS = exports.DOCUMENTS_DIRECT_CATEGORY = exports.DOCUMENTS_CONTEXT_MAX_TOKENS = exports.DOCUMENTS_BENCHMARK = void 0;
4
+ exports.scoreDocumentsArm = scoreDocumentsArm;
5
+ exports.documentsReplyTokens = documentsReplyTokens;
6
+ const metrics_1 = require("./metrics");
7
+ const scoring_1 = require("./scoring");
8
+ /**
9
+ * The documents arm of RenorBench: a vault of handbook-style notes (scripts/eval/generate-renorbench-docs.ts), read through
10
+ * the shipped `search_memory` tool and through `get_context` at 800 tokens, in process, and scored on one thing: whether the
11
+ * probed section of a note (the one with the case's needle) is in the reply an agent receives.
12
+ *
13
+ * Why it exists. RenorBench v1 and v2 have no document in them: v2's notes are chats (a turn is a section, and the turns of a
14
+ * chat share their headings) and short logs, so a change to what the session expansion may add from a DOCUMENT (the budget,
15
+ * the number of notes, the size of a statement, the chunker's cut or its heading rule) moved no gated number. A flat 100-token
16
+ * cap on the expansion's statements removed 186 of the 309 answer-bearing statements it delivered on a vault of documents and
17
+ * passed every gate. This arm is made of documents so that such a change turns the gate red.
18
+ *
19
+ * Nothing here needs a model, a network or a quality claim: the gold is the case's own needle, the embedder is the free hash
20
+ * stand-in (`qualityEligible: false`), and the numbers say what the MECHANISM delivers on this fixture, never how well Renor
21
+ * remembers. They are reported as infrastructure.
22
+ *
23
+ * The categories are the size band of the probed section: `adjacent-le100`, `adjacent-101-200` and `adjacent-201-400` hold
24
+ * the cases that name a neighbour of the section (its title, or the section before it), which the section reaches only when
25
+ * the session expansion adds it; `direct` holds the control, the opening of the section itself, which retrieval finds on its
26
+ * own. The 101-200 band is the cap's cliff: a statement limit below 200 takes every section of it out of reach.
27
+ *
28
+ * Reply sizes are annotated (`documentsPath.replyTokens`), not scored: a tripwire for delivery has no business failing a
29
+ * change that merely costs more or less, and the other arms already hold the reply's size.
30
+ */
31
+ exports.DOCUMENTS_BENCHMARK = 'renorbench-docs-product';
32
+ /** What `get_context` is asked for: its own default budget, the one an agent gets when it does not ask for more. */
33
+ exports.DOCUMENTS_CONTEXT_MAX_TOKENS = 800;
34
+ /** The category of the control: a question that names the probed section's own opening. */
35
+ exports.DOCUMENTS_DIRECT_CATEGORY = 'direct';
36
+ /**
37
+ * What the gate holds the arm to, overall and per category (any one falling fails it). All three only go up. A metric a
38
+ * category has no case for reads null and is not compared there (`directDelivered` of an adjacent band, say). The control
39
+ * is recorded at 1: a direct hit is the statement's own chunk, and no limit on the expansion may take it away.
40
+ */
41
+ exports.DOCUMENTS_QUALITY_METRICS = [
42
+ { key: 'adjacentDelivered', higherBetter: true },
43
+ { key: 'directDelivered', higherBetter: true },
44
+ { key: 'contextDelivered', higherBetter: true },
45
+ ];
46
+ function round(value) {
47
+ return Math.round(value * 10000) / 10000;
48
+ }
49
+ function mean(values) {
50
+ return values.length === 0 ? null : round(values.reduce((sum, value) => sum + value, 0) / values.length);
51
+ }
52
+ function percentile95(values) {
53
+ if (values.length === 0)
54
+ return null;
55
+ const sorted = [...values].sort((a, b) => a - b);
56
+ return sorted[Math.max(0, Math.min(sorted.length - 1, Math.ceil(0.95 * sorted.length) - 1))];
57
+ }
58
+ function emptyAggregate() {
59
+ return { cases: 0, adjacent: 0, adjacentDelivered: 0, direct: 0, directDelivered: 0, contextDelivered: 0 };
60
+ }
61
+ /** The probed section is in the reply: a hit of the case's note that holds the needle. */
62
+ function delivered(test, hits) {
63
+ return (0, metrics_1.hitAtK)(hits, test.relevant, hits.length) === 1;
64
+ }
65
+ function addCase(agg, test, result) {
66
+ agg.cases += 1;
67
+ const found = delivered(test, result.hits);
68
+ if (test.category === exports.DOCUMENTS_DIRECT_CATEGORY) {
69
+ agg.direct += 1;
70
+ if (found)
71
+ agg.directDelivered += 1;
72
+ }
73
+ else {
74
+ agg.adjacent += 1;
75
+ if (found)
76
+ agg.adjacentDelivered += 1;
77
+ }
78
+ if (delivered(test, result.context.hits))
79
+ agg.contextDelivered += 1;
80
+ }
81
+ function summarize(agg) {
82
+ return {
83
+ cases: agg.cases,
84
+ adjacentCases: agg.adjacent,
85
+ directCases: agg.direct,
86
+ adjacentDelivered: agg.adjacent ? round(agg.adjacentDelivered / agg.adjacent) : null,
87
+ directDelivered: agg.direct ? round(agg.directDelivered / agg.direct) : null,
88
+ contextDelivered: agg.cases ? round(agg.contextDelivered / agg.cases) : null,
89
+ };
90
+ }
91
+ function validateResults(cases, results) {
92
+ // The cases, the ids and the hits are validated by the chunk scorer's own rules, so that every arm agrees on what a
93
+ // well-formed run is.
94
+ (0, scoring_1.scoreRetrieval)(cases, results.map(result => ({ id: result.id, hits: result.hits, latencyMs: result.latencyMs, contextTokens: result.replyTokens })));
95
+ for (const test of cases) {
96
+ if (test.shouldAbstain || test.relevant.length === 0)
97
+ throw new Error(`documents case ${test.id} has no gold: every case probes one section`);
98
+ }
99
+ for (const result of results) {
100
+ const context = result.context;
101
+ if (!context || !Array.isArray(context.hits) || typeof context.replyTokens !== 'number')
102
+ throw new Error(`Missing get_context read for result: ${result.id}`);
103
+ if (typeof result.replyTokens !== 'number')
104
+ throw new Error(`Missing replyTokens for result: ${result.id}`);
105
+ }
106
+ }
107
+ function scoreDocumentsArm(cases, results) {
108
+ validateResults(cases, results);
109
+ const byId = new Map(results.map(result => [result.id, result]));
110
+ const overall = emptyAggregate();
111
+ const categories = new Map();
112
+ for (const test of cases) {
113
+ const result = byId.get(test.id); // validateResults has already refused a missing id
114
+ addCase(overall, test, result);
115
+ const category = categories.get(test.category) ?? emptyAggregate();
116
+ addCase(category, test, result);
117
+ categories.set(test.category, category);
118
+ }
119
+ return {
120
+ overall: summarize(overall),
121
+ byCategory: Object.fromEntries([...categories.entries()].map(([name, agg]) => [name, summarize(agg)])),
122
+ };
123
+ }
124
+ /** What a reply costs, per tool: recorded in summary.json for the reader, never part of the score. */
125
+ function documentsReplyTokens(results) {
126
+ const search = results.map(result => result.replyTokens);
127
+ const context = results.map(result => result.context.replyTokens);
128
+ return {
129
+ searchMean: mean(search),
130
+ searchP95: percentile95(search),
131
+ contextMean: mean(context),
132
+ contextP95: percentile95(context),
133
+ };
134
+ }