@tenphi/akno 0.1.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 (147) hide show
  1. package/LICENSE +83 -0
  2. package/README.md +46 -0
  3. package/dist/args.d.ts +18 -0
  4. package/dist/args.d.ts.map +1 -0
  5. package/dist/args.js +92 -0
  6. package/dist/args.js.map +1 -0
  7. package/dist/bin.d.ts +3 -0
  8. package/dist/bin.d.ts.map +1 -0
  9. package/dist/bin.js +142 -0
  10. package/dist/bin.js.map +1 -0
  11. package/dist/commands/adopt-cmd.d.ts +2 -0
  12. package/dist/commands/adopt-cmd.d.ts.map +1 -0
  13. package/dist/commands/adopt-cmd.js +61 -0
  14. package/dist/commands/adopt-cmd.js.map +1 -0
  15. package/dist/commands/answer-cmd.d.ts +2 -0
  16. package/dist/commands/answer-cmd.d.ts.map +1 -0
  17. package/dist/commands/answer-cmd.js +150 -0
  18. package/dist/commands/answer-cmd.js.map +1 -0
  19. package/dist/commands/bench-cmd.d.ts +4 -0
  20. package/dist/commands/bench-cmd.d.ts.map +1 -0
  21. package/dist/commands/bench-cmd.js +1542 -0
  22. package/dist/commands/bench-cmd.js.map +1 -0
  23. package/dist/commands/config-cmd.d.ts +7 -0
  24. package/dist/commands/config-cmd.d.ts.map +1 -0
  25. package/dist/commands/config-cmd.js +80 -0
  26. package/dist/commands/config-cmd.js.map +1 -0
  27. package/dist/commands/context-cmd.d.ts +2 -0
  28. package/dist/commands/context-cmd.d.ts.map +1 -0
  29. package/dist/commands/context-cmd.js +130 -0
  30. package/dist/commands/context-cmd.js.map +1 -0
  31. package/dist/commands/doctor-cmd.d.ts +2 -0
  32. package/dist/commands/doctor-cmd.d.ts.map +1 -0
  33. package/dist/commands/doctor-cmd.js +198 -0
  34. package/dist/commands/doctor-cmd.js.map +1 -0
  35. package/dist/commands/dream-cmd.d.ts +26 -0
  36. package/dist/commands/dream-cmd.d.ts.map +1 -0
  37. package/dist/commands/dream-cmd.js +1167 -0
  38. package/dist/commands/dream-cmd.js.map +1 -0
  39. package/dist/commands/dream-model-status.d.ts +5 -0
  40. package/dist/commands/dream-model-status.d.ts.map +1 -0
  41. package/dist/commands/dream-model-status.js +33 -0
  42. package/dist/commands/dream-model-status.js.map +1 -0
  43. package/dist/commands/dream-notifications.d.ts +24 -0
  44. package/dist/commands/dream-notifications.d.ts.map +1 -0
  45. package/dist/commands/dream-notifications.js +168 -0
  46. package/dist/commands/dream-notifications.js.map +1 -0
  47. package/dist/commands/dream-schedule.d.ts +55 -0
  48. package/dist/commands/dream-schedule.d.ts.map +1 -0
  49. package/dist/commands/dream-schedule.js +160 -0
  50. package/dist/commands/dream-schedule.js.map +1 -0
  51. package/dist/commands/folder-cmd.d.ts +2 -0
  52. package/dist/commands/folder-cmd.d.ts.map +1 -0
  53. package/dist/commands/folder-cmd.js +84 -0
  54. package/dist/commands/folder-cmd.js.map +1 -0
  55. package/dist/commands/graph-cmd.d.ts +2 -0
  56. package/dist/commands/graph-cmd.d.ts.map +1 -0
  57. package/dist/commands/graph-cmd.js +126 -0
  58. package/dist/commands/graph-cmd.js.map +1 -0
  59. package/dist/commands/inbox-cmd.d.ts +2 -0
  60. package/dist/commands/inbox-cmd.d.ts.map +1 -0
  61. package/dist/commands/inbox-cmd.js +69 -0
  62. package/dist/commands/inbox-cmd.js.map +1 -0
  63. package/dist/commands/index-cmd.d.ts +2 -0
  64. package/dist/commands/index-cmd.d.ts.map +1 -0
  65. package/dist/commands/index-cmd.js +102 -0
  66. package/dist/commands/index-cmd.js.map +1 -0
  67. package/dist/commands/ingest-cmd.d.ts +2 -0
  68. package/dist/commands/ingest-cmd.d.ts.map +1 -0
  69. package/dist/commands/ingest-cmd.js +167 -0
  70. package/dist/commands/ingest-cmd.js.map +1 -0
  71. package/dist/commands/init-cmd.d.ts +292 -0
  72. package/dist/commands/init-cmd.d.ts.map +1 -0
  73. package/dist/commands/init-cmd.js +476 -0
  74. package/dist/commands/init-cmd.js.map +1 -0
  75. package/dist/commands/init-prompts.d.ts +51 -0
  76. package/dist/commands/init-prompts.d.ts.map +1 -0
  77. package/dist/commands/init-prompts.js +192 -0
  78. package/dist/commands/init-prompts.js.map +1 -0
  79. package/dist/commands/list-cmd.d.ts +2 -0
  80. package/dist/commands/list-cmd.d.ts.map +1 -0
  81. package/dist/commands/list-cmd.js +77 -0
  82. package/dist/commands/list-cmd.js.map +1 -0
  83. package/dist/commands/maintenance-policy-output.d.ts +4 -0
  84. package/dist/commands/maintenance-policy-output.d.ts.map +1 -0
  85. package/dist/commands/maintenance-policy-output.js +72 -0
  86. package/dist/commands/maintenance-policy-output.js.map +1 -0
  87. package/dist/commands/mutate-cmd.d.ts +5 -0
  88. package/dist/commands/mutate-cmd.d.ts.map +1 -0
  89. package/dist/commands/mutate-cmd.js +224 -0
  90. package/dist/commands/mutate-cmd.js.map +1 -0
  91. package/dist/commands/plan-cmd.d.ts +9 -0
  92. package/dist/commands/plan-cmd.d.ts.map +1 -0
  93. package/dist/commands/plan-cmd.js +411 -0
  94. package/dist/commands/plan-cmd.js.map +1 -0
  95. package/dist/commands/read-cmd.d.ts +2 -0
  96. package/dist/commands/read-cmd.d.ts.map +1 -0
  97. package/dist/commands/read-cmd.js +89 -0
  98. package/dist/commands/read-cmd.js.map +1 -0
  99. package/dist/commands/recall-cmd.d.ts +2 -0
  100. package/dist/commands/recall-cmd.d.ts.map +1 -0
  101. package/dist/commands/recall-cmd.js +176 -0
  102. package/dist/commands/recall-cmd.js.map +1 -0
  103. package/dist/commands/redeploy-cmd.d.ts +43 -0
  104. package/dist/commands/redeploy-cmd.d.ts.map +1 -0
  105. package/dist/commands/redeploy-cmd.js +270 -0
  106. package/dist/commands/redeploy-cmd.js.map +1 -0
  107. package/dist/commands/rules-cmd.d.ts +2 -0
  108. package/dist/commands/rules-cmd.d.ts.map +1 -0
  109. package/dist/commands/rules-cmd.js +72 -0
  110. package/dist/commands/rules-cmd.js.map +1 -0
  111. package/dist/commands/serve-cmd.d.ts +23 -0
  112. package/dist/commands/serve-cmd.d.ts.map +1 -0
  113. package/dist/commands/serve-cmd.js +415 -0
  114. package/dist/commands/serve-cmd.js.map +1 -0
  115. package/dist/commands/timeline-cmd.d.ts +2 -0
  116. package/dist/commands/timeline-cmd.d.ts.map +1 -0
  117. package/dist/commands/timeline-cmd.js +67 -0
  118. package/dist/commands/timeline-cmd.js.map +1 -0
  119. package/dist/commands/write-cmd.d.ts +49 -0
  120. package/dist/commands/write-cmd.d.ts.map +1 -0
  121. package/dist/commands/write-cmd.js +269 -0
  122. package/dist/commands/write-cmd.js.map +1 -0
  123. package/dist/ops-handle.d.ts +67 -0
  124. package/dist/ops-handle.d.ts.map +1 -0
  125. package/dist/ops-handle.js +196 -0
  126. package/dist/ops-handle.js.map +1 -0
  127. package/dist/output.d.ts +28 -0
  128. package/dist/output.d.ts.map +1 -0
  129. package/dist/output.js +87 -0
  130. package/dist/output.js.map +1 -0
  131. package/dist/serve/http.d.ts +19 -0
  132. package/dist/serve/http.d.ts.map +1 -0
  133. package/dist/serve/http.js +135 -0
  134. package/dist/serve/http.js.map +1 -0
  135. package/dist/serve/mcp.d.ts +21 -0
  136. package/dist/serve/mcp.d.ts.map +1 -0
  137. package/dist/serve/mcp.js +79 -0
  138. package/dist/serve/mcp.js.map +1 -0
  139. package/dist/serve/socket.d.ts +15 -0
  140. package/dist/serve/socket.d.ts.map +1 -0
  141. package/dist/serve/socket.js +311 -0
  142. package/dist/serve/socket.js.map +1 -0
  143. package/dist/version.d.ts +2 -0
  144. package/dist/version.d.ts.map +1 -0
  145. package/dist/version.js +15 -0
  146. package/dist/version.js.map +1 -0
  147. package/package.json +40 -0
@@ -0,0 +1,1542 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import fsp from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { attachRankingEndToEndEvidence, attachRankingLatencyEvidence, attachRankingReviewEvidence, completeMergeDiscoveryReview, completeRankingReview, createMergeDiscoveryReviewPacket, createRankingReviewPacket, markAnswerBenchPersisted, markAutoRecallAnswerBenchPersisted, markAutoRecallBenchPersisted, markMergeDiscoveryBenchPersisted, markRankingMatrixPersisted, open, refreshRankingMatrixReport, rebaseRankingReviewPacket, runAnswerBench, runAutoRecallAnswerBench, runAutoRecallBench, runEntityResolutionBench, runGraphBench, runMergeDiscoveryBench, runBench, runLlmRankingProbe, runMixedRetrievalBench, runRankingBench, runRankingEndToEnd, runRankingLatencyBench, runRankingMatrix, RANKING_MATRIX_VARIANT_IDS, } from '@tenphi/akno-core';
5
+ import { openOptionsFrom, parse } from "../args.js";
6
+ import { fail, heading, json, line, style } from "../output.js";
7
+ const BENCH_HELP = `akno bench [options]
8
+
9
+ Run performance budgets against the current knowledge base, plus retrieval
10
+ quality assertions against a fixed corpus of invented pages and documents.
11
+ Numbers rot, so these are asserted rather than remembered.
12
+
13
+ Index-path budgets are asserted. Model-path timings are reported but never
14
+ budgeted: a slow local model is not a regression in this code, and a bench that
15
+ fails on somebody's GPU gets ignored within a week.
16
+
17
+ --iterations <n> Samples per measurement (default 12).
18
+ --retrieval-only Run only the invented mixed-retrieval corpus. Does not
19
+ open or query the configured knowledge base.
20
+ ranking --probe Send one invented three-candidate smoke probe to a live
21
+ generative endpoint. This is not the ranking release gate.
22
+ entities Run the invented select-or-abstain release gate for
23
+ contextual entity resolution. Never opens the knowledge base.
24
+ graph Run the frozen, model-free held-out graph release gate.
25
+ Never opens the knowledge base or configured models.
26
+ merge Measure embedding-only and embedding-plus-classifier
27
+ near-purpose discovery against related scopes, templates,
28
+ and similar entities. Development is default; use
29
+ --split test --runs 5 for the frozen held-out gate.
30
+ merge review Export the model-output-free held-out review packet with
31
+ --output. Complete it before any held-out model run.
32
+ answer Run the invented answer corpus through production retrieval,
33
+ generation, and support verification. Development is default;
34
+ use --split test --runs 5 for the frozen held-out gate.
35
+ auto-recall Run invented prompts through the production precision-first
36
+ context profile. Development is default; use --split test
37
+ --runs 5 for the frozen held-out injection gate.
38
+ auto-recall-answer Compare the same invented host-model turns with and without
39
+ production auto-recall evidence. Development is default;
40
+ use --split test --runs 5 for the held-out quality gate.
41
+ ranking --system <s> Run frozen pools with fusion, native, or llm (default
42
+ fusion).
43
+ ranking --matrix Run fusion, optional native, Luna none at 10/20/40, and
44
+ Luna low at 20 candidates with repeated stability checks.
45
+ --variant <id> Restrict a matrix to one repeated LLM variant. This creates
46
+ development evidence and intentionally cannot select a release preset.
47
+ ranking --track end-to-end
48
+ Index the invented corpus, measure candidate-window recall,
49
+ then run reranking and assembly over the same derived index.
50
+ ranking --track latency
51
+ Measure the selected ranker's cold negotiation, warm
52
+ single-flight UX, and warm loaded latency separately.
53
+ ranking review Export the model-output-free corpus review packet with
54
+ --output, or attach a completed packet with --input and
55
+ --matrix-artifact. Combine --input from a prior review with
56
+ --output to carry forward only exact unchanged passes.
57
+ --split <name> development, test, or all (default development). Test is
58
+ held out from prompt tuning and must be selected explicitly.
59
+ --candidates <n> 10, 20, or 40 for a single-system run (default 20).
60
+ --excerpt-chars <n> 400, 800, or 1600 (default 800).
61
+ --concurrency <n> Simultaneous cases, 1..16 (answer caps at 8; single
62
+ ranking defaults 1, matrix 4). Latency remains per case.
63
+ --runs <n> Answer/auto-recall-answer repetitions or repetitions per LLM
64
+ matrix variant, 1..10 (development 1, held-out/matrix 5).
65
+ --skip-native Omit the optional native reference from a matrix.
66
+ --matrix-artifact <path>
67
+ Use its selected configuration and atomically attach
68
+ end-to-end or latency evidence to that matrix artifact.
69
+ --input <path> Completed corpus review packet for a held-out merge run,
70
+ ranking review attachment, or held-out ranking run.
71
+ --output <path> Atomically persist the content-safe result artifact.
72
+ --provider <name> Configured provider. Ranking defaults to openai; merge uses
73
+ the maintenance role; answer and auto-recall-answer use the
74
+ answer role; auto-recall uses reranker.
75
+ --model <id> Generative model. Ranking and merge default to gpt-5.6-luna;
76
+ answer and auto-recall-answer use the answer role;
77
+ auto-recall uses reranker.
78
+ --embedding-provider <name>
79
+ Embedding provider for end-to-end ranking or answer recall.
80
+ --embedding-model <id>
81
+ Embedding model for end-to-end ranking or answer recall.
82
+ --embedding-dimensions <n>
83
+ Stored vector dimensions for that embedding model.
84
+ --reasoning <v> none, low, medium, high, xhigh, or max. Ranking and merge
85
+ default to none; answer inherits its configured role.
86
+ --write Also measure the restart sweep, which needs the write
87
+ handle. Skipped otherwise rather than measured wrongly.
88
+ --json`;
89
+ export async function benchCommand(argv) {
90
+ const { values, positionals } = parse(argv, {
91
+ iterations: { type: 'string' },
92
+ 'retrieval-only': { type: 'boolean', default: false },
93
+ write: { type: 'boolean', default: false },
94
+ probe: { type: 'boolean', default: false },
95
+ matrix: { type: 'boolean', default: false },
96
+ variant: { type: 'string' },
97
+ 'skip-native': { type: 'boolean', default: false },
98
+ track: { type: 'string' },
99
+ 'matrix-artifact': { type: 'string' },
100
+ provider: { type: 'string' },
101
+ model: { type: 'string' },
102
+ 'embedding-provider': { type: 'string' },
103
+ 'embedding-model': { type: 'string' },
104
+ 'embedding-dimensions': { type: 'string' },
105
+ reasoning: { type: 'string' },
106
+ split: { type: 'string' },
107
+ system: { type: 'string' },
108
+ candidates: { type: 'string' },
109
+ 'excerpt-chars': { type: 'string' },
110
+ concurrency: { type: 'string' },
111
+ runs: { type: 'string' },
112
+ output: { type: 'string' },
113
+ input: { type: 'string' },
114
+ });
115
+ if (values.help) {
116
+ line(BENCH_HELP);
117
+ return 0;
118
+ }
119
+ if (values.input && !['ranking', 'merge'].includes(positionals[0] ?? '')) {
120
+ fail('--input is only valid for ranking or merge');
121
+ return 2;
122
+ }
123
+ if (positionals[0] === 'answer') {
124
+ if (positionals.length > 1) {
125
+ fail(`unknown answer bench argument: ${positionals[1]}`);
126
+ return 2;
127
+ }
128
+ if (values['retrieval-only'] ||
129
+ values.write ||
130
+ values.probe ||
131
+ values.matrix ||
132
+ values.variant ||
133
+ values['skip-native'] ||
134
+ values.track ||
135
+ values['matrix-artifact'] ||
136
+ values.system ||
137
+ values.candidates ||
138
+ values['excerpt-chars'] ||
139
+ values.iterations ||
140
+ (values.split && !['development', 'test'].includes(values.split))) {
141
+ fail('answer bench accepts development/test split, runs, provider/model, embedding, reasoning, concurrency, output, and json options');
142
+ return 2;
143
+ }
144
+ const reasoning = values.reasoning ? parseReasoningEffort(values.reasoning) : undefined;
145
+ if (values.reasoning && !reasoning) {
146
+ fail(`invalid reasoning effort: ${values.reasoning}`);
147
+ return 2;
148
+ }
149
+ const split = (values.split ?? 'development');
150
+ const runs = parseBoundedInteger(values.runs ?? (split === 'test' ? '5' : '1'), 1, 10, 'runs');
151
+ const concurrency = parseBoundedInteger(values.concurrency ?? '2', 1, 8, 'concurrency');
152
+ const { loadConfig } = await import('@tenphi/akno-core');
153
+ const config = loadConfig(openOptionsFrom(values));
154
+ const embeddingDimensions = parseBoundedInteger(values['embedding-dimensions'] ?? String(config.models.embedding.dimensions ?? 1_536), 1, 65_536, 'embedding dimensions');
155
+ if (runs === null || concurrency === null || embeddingDimensions === null)
156
+ return 2;
157
+ let report = await runAnswerBench(config, {
158
+ split,
159
+ runs,
160
+ concurrency,
161
+ ...(values['embedding-provider'] ? { embeddingProvider: values['embedding-provider'] } : {}),
162
+ ...(values['embedding-model'] ? { embeddingModel: values['embedding-model'] } : {}),
163
+ embeddingDimensions,
164
+ ...(values.provider ? { provider: values.provider } : {}),
165
+ ...(values.model ? { model: values.model } : {}),
166
+ ...(reasoning ? { reasoningEffort: reasoning } : {}),
167
+ ...(!values.json
168
+ ? {
169
+ onProgress: ({ run, runs: totalRuns, done, total }) => {
170
+ if (done === 1 || done === total)
171
+ line(` answer run ${run}/${totalRuns} ${done}/${total} cases`);
172
+ },
173
+ }
174
+ : {}),
175
+ });
176
+ let artifactPath = null;
177
+ if (values.output) {
178
+ report = markAnswerBenchPersisted(report);
179
+ artifactPath = await writeJsonArtifact(values.output, report);
180
+ }
181
+ if (values.json)
182
+ json(report);
183
+ else
184
+ renderAnswerBench(report, artifactPath);
185
+ return report.passed ? 0 : 1;
186
+ }
187
+ if (positionals[0] === 'auto-recall') {
188
+ if (positionals.length > 1) {
189
+ fail(`unknown auto-recall bench argument: ${positionals[1]}`);
190
+ return 2;
191
+ }
192
+ if (values['retrieval-only'] ||
193
+ values.write ||
194
+ values.probe ||
195
+ values.matrix ||
196
+ values.variant ||
197
+ values['skip-native'] ||
198
+ values.track ||
199
+ values['matrix-artifact'] ||
200
+ values.system ||
201
+ values.candidates ||
202
+ values['excerpt-chars'] ||
203
+ values.iterations ||
204
+ (values.split && !['development', 'test'].includes(values.split))) {
205
+ fail('auto-recall bench accepts development/test split, runs, provider/model, embedding, reasoning, concurrency, output, and json options');
206
+ return 2;
207
+ }
208
+ const reasoning = values.reasoning ? parseReasoningEffort(values.reasoning) : undefined;
209
+ if (values.reasoning && !reasoning) {
210
+ fail(`invalid reasoning effort: ${values.reasoning}`);
211
+ return 2;
212
+ }
213
+ const split = (values.split ?? 'development');
214
+ const runs = parseBoundedInteger(values.runs ?? (split === 'test' ? '5' : '1'), 1, 10, 'runs');
215
+ const concurrency = parseBoundedInteger(values.concurrency ?? '2', 1, 8, 'concurrency');
216
+ const { loadConfig } = await import('@tenphi/akno-core');
217
+ const config = loadConfig(openOptionsFrom(values));
218
+ const embeddingDimensions = parseBoundedInteger(values['embedding-dimensions'] ?? String(config.models.embedding.dimensions ?? 1_536), 1, 65_536, 'embedding dimensions');
219
+ if (runs === null || concurrency === null || embeddingDimensions === null)
220
+ return 2;
221
+ let report = await runAutoRecallBench(config, {
222
+ split,
223
+ runs,
224
+ concurrency,
225
+ ...(values['embedding-provider'] ? { embeddingProvider: values['embedding-provider'] } : {}),
226
+ ...(values['embedding-model'] ? { embeddingModel: values['embedding-model'] } : {}),
227
+ embeddingDimensions,
228
+ ...(values.provider ? { provider: values.provider } : {}),
229
+ ...(values.model ? { model: values.model } : {}),
230
+ ...(reasoning ? { reasoningEffort: reasoning } : {}),
231
+ ...(!values.json
232
+ ? {
233
+ onProgress: ({ run, runs: totalRuns, done, total }) => {
234
+ if (done === 1 || done === total)
235
+ line(` auto-recall run ${run}/${totalRuns} ${done}/${total} cases`);
236
+ },
237
+ }
238
+ : {}),
239
+ });
240
+ let artifactPath = null;
241
+ if (values.output) {
242
+ report = markAutoRecallBenchPersisted(report);
243
+ artifactPath = await writeJsonArtifact(values.output, report);
244
+ }
245
+ if (values.json)
246
+ json(report);
247
+ else
248
+ renderAutoRecallBench(report, artifactPath);
249
+ return report.passed ? 0 : 1;
250
+ }
251
+ if (positionals[0] === 'auto-recall-answer') {
252
+ if (positionals.length > 1) {
253
+ fail(`unknown auto-recall-answer bench argument: ${positionals[1]}`);
254
+ return 2;
255
+ }
256
+ if (values['retrieval-only'] ||
257
+ values.write ||
258
+ values.probe ||
259
+ values.matrix ||
260
+ values.variant ||
261
+ values['skip-native'] ||
262
+ values.track ||
263
+ values['matrix-artifact'] ||
264
+ values.system ||
265
+ values.candidates ||
266
+ values['excerpt-chars'] ||
267
+ values.iterations ||
268
+ (values.split && !['development', 'test'].includes(values.split))) {
269
+ fail('auto-recall-answer bench accepts development/test split, runs, provider/model, embedding, reasoning, concurrency, output, and json options');
270
+ return 2;
271
+ }
272
+ const reasoning = values.reasoning ? parseReasoningEffort(values.reasoning) : undefined;
273
+ if (values.reasoning && !reasoning) {
274
+ fail(`invalid reasoning effort: ${values.reasoning}`);
275
+ return 2;
276
+ }
277
+ const split = (values.split ?? 'development');
278
+ const runs = parseBoundedInteger(values.runs ?? (split === 'test' ? '5' : '1'), 1, 10, 'runs');
279
+ const concurrency = parseBoundedInteger(values.concurrency ?? '2', 1, 8, 'concurrency');
280
+ const { loadConfig } = await import('@tenphi/akno-core');
281
+ const config = loadConfig(openOptionsFrom(values));
282
+ const embeddingDimensions = parseBoundedInteger(values['embedding-dimensions'] ?? String(config.models.embedding.dimensions ?? 1_536), 1, 65_536, 'embedding dimensions');
283
+ if (runs === null || concurrency === null || embeddingDimensions === null)
284
+ return 2;
285
+ let report = await runAutoRecallAnswerBench(config, {
286
+ split,
287
+ runs,
288
+ concurrency,
289
+ ...(values['embedding-provider'] ? { embeddingProvider: values['embedding-provider'] } : {}),
290
+ ...(values['embedding-model'] ? { embeddingModel: values['embedding-model'] } : {}),
291
+ embeddingDimensions,
292
+ ...(values.provider ? { provider: values.provider } : {}),
293
+ ...(values.model ? { model: values.model } : {}),
294
+ ...(reasoning ? { reasoningEffort: reasoning } : {}),
295
+ ...(!values.json
296
+ ? {
297
+ onProgress: ({ run, runs: totalRuns, done, total }) => {
298
+ if (done === 1 || done === total)
299
+ line(` auto-recall-answer run ${run}/${totalRuns} ${done}/${total} cases`);
300
+ },
301
+ }
302
+ : {}),
303
+ });
304
+ let artifactPath = null;
305
+ if (values.output) {
306
+ report = markAutoRecallAnswerBenchPersisted(report);
307
+ artifactPath = await writeJsonArtifact(values.output, report);
308
+ }
309
+ if (values.json)
310
+ json(report);
311
+ else
312
+ renderAutoRecallAnswerBench(report, artifactPath);
313
+ return report.passed ? 0 : 1;
314
+ }
315
+ if (positionals[0] === 'graph') {
316
+ if (positionals.length > 1) {
317
+ fail(`unknown graph bench argument: ${positionals[1]}`);
318
+ return 2;
319
+ }
320
+ if (values['retrieval-only'] ||
321
+ values.write ||
322
+ values.probe ||
323
+ values.matrix ||
324
+ values.variant ||
325
+ values['skip-native'] ||
326
+ values.track ||
327
+ values['matrix-artifact'] ||
328
+ values.provider ||
329
+ values.model ||
330
+ values['embedding-provider'] ||
331
+ values['embedding-model'] ||
332
+ values['embedding-dimensions'] ||
333
+ values.reasoning ||
334
+ values.split ||
335
+ values.system ||
336
+ values.candidates ||
337
+ values['excerpt-chars'] ||
338
+ values.concurrency ||
339
+ values.runs) {
340
+ fail('graph bench is one frozen test split and accepts only --iterations, --output, and --json');
341
+ return 2;
342
+ }
343
+ const report = await runGraphBench({
344
+ ...(values.iterations ? { iterations: Number(values.iterations) } : {}),
345
+ });
346
+ let artifactPath = null;
347
+ if (values.output)
348
+ artifactPath = await writeJsonArtifact(values.output, report);
349
+ if (values.json)
350
+ json(report);
351
+ else
352
+ renderGraphBench(report, artifactPath);
353
+ return report.passed ? 0 : 1;
354
+ }
355
+ if (positionals[0] === 'merge') {
356
+ if (positionals[1] === 'review') {
357
+ if (positionals.length > 2) {
358
+ fail(`unknown merge review argument: ${positionals[2]}`);
359
+ return 2;
360
+ }
361
+ if (values.input ||
362
+ values['retrieval-only'] ||
363
+ values.write ||
364
+ values.probe ||
365
+ values.matrix ||
366
+ values.variant ||
367
+ values['skip-native'] ||
368
+ values.track ||
369
+ values['matrix-artifact'] ||
370
+ values.provider ||
371
+ values.model ||
372
+ values['embedding-provider'] ||
373
+ values['embedding-model'] ||
374
+ values['embedding-dimensions'] ||
375
+ values.reasoning ||
376
+ values.split ||
377
+ values.system ||
378
+ values.candidates ||
379
+ values['excerpt-chars'] ||
380
+ values.concurrency ||
381
+ values.runs ||
382
+ values.iterations) {
383
+ fail('merge review accepts only --output and --json');
384
+ return 2;
385
+ }
386
+ if (!values.output) {
387
+ fail('merge review requires --output');
388
+ return 2;
389
+ }
390
+ const packet = createMergeDiscoveryReviewPacket();
391
+ const artifactPath = await writeJsonArtifact(values.output, packet);
392
+ if (values.json)
393
+ json(packet);
394
+ else {
395
+ heading('Semantic merge discovery corpus review');
396
+ line(` sources ${packet.corpus.sources}`);
397
+ line(` cases ${packet.corpus.cases}`);
398
+ line(` fingerprint ${packet.corpus.fingerprint}`);
399
+ line(` packet ${artifactPath}`);
400
+ line(style.grey('Review it independently before running the held-out model gate.'));
401
+ }
402
+ return 0;
403
+ }
404
+ if (positionals.length > 1) {
405
+ fail(`unknown merge bench argument: ${positionals[1]}`);
406
+ return 2;
407
+ }
408
+ if (values['retrieval-only'] ||
409
+ values.write ||
410
+ values.probe ||
411
+ values.matrix ||
412
+ values.variant ||
413
+ values['skip-native'] ||
414
+ values.track ||
415
+ values['matrix-artifact'] ||
416
+ values['embedding-dimensions'] ||
417
+ values.system ||
418
+ values.candidates ||
419
+ values['excerpt-chars'] ||
420
+ values.concurrency ||
421
+ values.iterations) {
422
+ fail('merge bench accepts development/test split, runs, review input, embedding and classifier model choices, output, and json options');
423
+ return 2;
424
+ }
425
+ if (values.split && !['development', 'test'].includes(values.split)) {
426
+ fail(`invalid merge split: ${values.split}`);
427
+ return 2;
428
+ }
429
+ const split = (values.split ?? 'development');
430
+ const runs = parseBoundedInteger(values.runs ?? (split === 'test' ? '5' : '1'), 1, 10, 'runs');
431
+ if (runs === null)
432
+ return 2;
433
+ if (split === 'test' && !values.input) {
434
+ fail('held-out merge bench requires a completed review packet via --input');
435
+ return 2;
436
+ }
437
+ if (split === 'test' && !values.output) {
438
+ fail('held-out merge bench requires --output so release evidence is persisted');
439
+ return 2;
440
+ }
441
+ if (split === 'development' && values.input) {
442
+ fail('--input is only valid for the held-out merge split');
443
+ return 2;
444
+ }
445
+ const reasoning = values.reasoning ? parseReasoningEffort(values.reasoning) : undefined;
446
+ if (values.reasoning && !reasoning) {
447
+ fail(`invalid reasoning effort: ${values.reasoning}`);
448
+ return 2;
449
+ }
450
+ let review;
451
+ if (values.input) {
452
+ try {
453
+ review = completeMergeDiscoveryReview(await readJsonArtifact(values.input));
454
+ }
455
+ catch (error) {
456
+ fail(error instanceof Error ? error.message : 'invalid merge discovery review packet');
457
+ return 2;
458
+ }
459
+ }
460
+ const { loadConfig } = await import('@tenphi/akno-core');
461
+ const config = loadConfig(openOptionsFrom(values));
462
+ let report = await runMergeDiscoveryBench(config, {
463
+ split,
464
+ runs,
465
+ ...(review ? { review } : {}),
466
+ ...(values['embedding-provider'] ? { embeddingProvider: values['embedding-provider'] } : {}),
467
+ ...(values['embedding-model'] ? { embeddingModel: values['embedding-model'] } : {}),
468
+ ...(values.provider ? { provider: values.provider } : {}),
469
+ ...(values.model ? { model: values.model } : {}),
470
+ ...(reasoning ? { reasoningEffort: reasoning } : {}),
471
+ ...(!values.json
472
+ ? {
473
+ onProgress: ({ run, runs: totalRuns }) => line(` merge classifier run ${run}/${totalRuns}`),
474
+ }
475
+ : {}),
476
+ });
477
+ let artifactPath = null;
478
+ if (values.output) {
479
+ report = markMergeDiscoveryBenchPersisted(report);
480
+ artifactPath = await writeJsonArtifact(values.output, report);
481
+ }
482
+ if (values.json)
483
+ json(report);
484
+ else
485
+ renderMergeDiscoveryBench(report, artifactPath);
486
+ return report.passed ? 0 : 1;
487
+ }
488
+ if (positionals[0] === 'entities') {
489
+ const reasoning = parseReasoningEffort(values.reasoning);
490
+ if (!reasoning) {
491
+ fail(`invalid reasoning effort: ${values.reasoning}`);
492
+ return 2;
493
+ }
494
+ if (positionals.length > 1) {
495
+ fail(`unknown entities bench argument: ${positionals[1]}`);
496
+ return 2;
497
+ }
498
+ const { loadConfig } = await import('@tenphi/akno-core');
499
+ const config = loadConfig(openOptionsFrom(values));
500
+ const report = await runEntityResolutionBench(config, {
501
+ ...(values.provider ? { provider: values.provider } : {}),
502
+ ...(values.model ? { model: values.model } : {}),
503
+ reasoningEffort: reasoning,
504
+ });
505
+ let artifactPath = null;
506
+ if (values.output)
507
+ artifactPath = await writeJsonArtifact(values.output, report);
508
+ if (values.json)
509
+ json(report);
510
+ else
511
+ renderEntityResolution(report, artifactPath);
512
+ return report.passed ? 0 : 1;
513
+ }
514
+ if (positionals[0] === 'ranking') {
515
+ if (positionals[1] === 'review') {
516
+ if (positionals.length > 2) {
517
+ fail(`unknown ranking review argument: ${positionals[2]}`);
518
+ return 2;
519
+ }
520
+ if (values['retrieval-only'] ||
521
+ values.write ||
522
+ values.probe ||
523
+ values.matrix ||
524
+ values.variant ||
525
+ values['skip-native'] ||
526
+ values.track ||
527
+ values.provider ||
528
+ values.model ||
529
+ values['embedding-provider'] ||
530
+ values['embedding-model'] ||
531
+ values['embedding-dimensions'] ||
532
+ values.reasoning ||
533
+ values.split ||
534
+ values.system ||
535
+ values.candidates ||
536
+ values['excerpt-chars'] ||
537
+ values.concurrency ||
538
+ values.runs ||
539
+ values.iterations) {
540
+ fail('ranking review accepts only --output, or --input with --matrix-artifact, plus --json');
541
+ return 2;
542
+ }
543
+ if (values.input) {
544
+ if (values.output && !values['matrix-artifact']) {
545
+ try {
546
+ const packet = rebaseRankingReviewPacket(await readJsonArtifact(values.input));
547
+ const artifactPath = await writeJsonArtifact(values.output, packet);
548
+ if (values.json)
549
+ json(packet);
550
+ else
551
+ renderRankingReviewPacket(packet, artifactPath);
552
+ return 0;
553
+ }
554
+ catch (error) {
555
+ fail(error instanceof Error ? error.message : 'ranking review rebase failed');
556
+ return 2;
557
+ }
558
+ }
559
+ if (!values['matrix-artifact'] || values.output) {
560
+ fail('ranking review --input requires either --output for a corrected packet or --matrix-artifact for attachment');
561
+ return 2;
562
+ }
563
+ try {
564
+ const packet = await readJsonArtifact(values.input);
565
+ const evidence = completeRankingReview(packet);
566
+ let matrix = refreshRankingMatrixReport(await readRankingMatrixArtifact(values['matrix-artifact']));
567
+ matrix = attachRankingReviewEvidence(matrix, evidence);
568
+ const matrixPath = await writeJsonArtifact(values['matrix-artifact'], matrix);
569
+ if (values.json)
570
+ json(evidence);
571
+ else
572
+ renderRankingReviewEvidence(evidence, matrixPath, matrix);
573
+ return 0;
574
+ }
575
+ catch (error) {
576
+ fail(error instanceof Error ? error.message : 'ranking review validation failed');
577
+ return 2;
578
+ }
579
+ }
580
+ if (!values.output || values['matrix-artifact']) {
581
+ fail('exporting a ranking review requires --output and does not accept --matrix-artifact');
582
+ return 2;
583
+ }
584
+ const packet = createRankingReviewPacket();
585
+ const artifactPath = await writeJsonArtifact(values.output, packet);
586
+ if (values.json)
587
+ json(packet);
588
+ else
589
+ renderRankingReviewPacket(packet, artifactPath);
590
+ return 0;
591
+ }
592
+ if (positionals.length > 1) {
593
+ fail(`unknown ranking bench argument: ${positionals[1]}`);
594
+ return 2;
595
+ }
596
+ if (values.input && (values.track || values.probe)) {
597
+ fail('--input unlocks held-out matrix or single-system runs; tracks use a reviewed matrix instead');
598
+ return 2;
599
+ }
600
+ let heldOutReview = null;
601
+ if (values.input) {
602
+ try {
603
+ heldOutReview = completeRankingReview(await readJsonArtifact(values.input));
604
+ }
605
+ catch (error) {
606
+ fail(error instanceof Error ? error.message : 'ranking review validation failed');
607
+ return 2;
608
+ }
609
+ }
610
+ const reasoning = parseReasoningEffort(values.reasoning);
611
+ if (!reasoning) {
612
+ fail(`invalid reasoning effort: ${values.reasoning}`);
613
+ return 2;
614
+ }
615
+ const { loadConfig } = await import('@tenphi/akno-core');
616
+ const config = loadConfig(openOptionsFrom(values));
617
+ if (values.track) {
618
+ if (values.track !== 'end-to-end' && values.track !== 'latency') {
619
+ fail(`invalid ranking track: ${values.track}`);
620
+ return 2;
621
+ }
622
+ if (values.track === 'latency') {
623
+ if (values.matrix ||
624
+ values.variant ||
625
+ values.probe ||
626
+ values.system ||
627
+ values.runs ||
628
+ values['skip-native'] ||
629
+ values['embedding-provider'] ||
630
+ values['embedding-model'] ||
631
+ values['embedding-dimensions'] ||
632
+ values.split ||
633
+ values.candidates ||
634
+ values['excerpt-chars'] ||
635
+ values.provider ||
636
+ values.model ||
637
+ values.reasoning) {
638
+ fail('ranking latency uses the matrix selection and accepts only --matrix-artifact, --concurrency, --output, and --json');
639
+ return 2;
640
+ }
641
+ if (!values['matrix-artifact']) {
642
+ fail('ranking latency requires --matrix-artifact');
643
+ return 2;
644
+ }
645
+ let matrix = refreshRankingMatrixReport(await readRankingMatrixArtifact(values['matrix-artifact']));
646
+ if (!matrix.selection) {
647
+ fail('the matrix artifact has no selected configuration');
648
+ return 2;
649
+ }
650
+ if (matrix.split !== 'development' && !hasIndependentRankingReview(matrix)) {
651
+ fail('held-out ranking tracks require a matrix with an accepted independent review receipt');
652
+ return 2;
653
+ }
654
+ const selected = matrix.variants.find((variant) => variant.id === matrix.selection.variantId);
655
+ if (!selected ||
656
+ !selected.provider ||
657
+ !selected.model ||
658
+ !selected.reasoningEffort ||
659
+ !selected.promptVersion ||
660
+ !selected.schemaVersion) {
661
+ fail('the selected matrix variant has no complete LLM configuration');
662
+ return 2;
663
+ }
664
+ const loadConcurrency = parseBoundedInteger(values.concurrency ?? String(Math.max(2, matrix.concurrency)), 2, 16, 'concurrency');
665
+ if (loadConcurrency === null)
666
+ return 2;
667
+ const report = await runRankingLatencyBench(config, {
668
+ split: matrix.split,
669
+ candidateCount: selected.candidateCount,
670
+ excerptChars: selected.excerptChars,
671
+ loadConcurrency,
672
+ provider: selected.provider,
673
+ model: selected.model,
674
+ reasoningEffort: selected.reasoningEffort,
675
+ ...(!values.json
676
+ ? {
677
+ onProgress: ({ profile, concurrency }) => line(` ${profile} profile concurrency ${concurrency}`),
678
+ }
679
+ : {}),
680
+ });
681
+ let artifactPath = null;
682
+ if (values.output)
683
+ artifactPath = await writeJsonArtifact(values.output, report);
684
+ matrix = attachRankingLatencyEvidence(matrix, report);
685
+ const matrixPath = await writeJsonArtifact(values['matrix-artifact'], matrix);
686
+ if (values.json)
687
+ json(report);
688
+ else
689
+ renderRankingLatency(report, artifactPath, matrixPath, matrix);
690
+ return report.passed ? 0 : 1;
691
+ }
692
+ if (values.matrix ||
693
+ values.variant ||
694
+ values.probe ||
695
+ values.system ||
696
+ values.runs ||
697
+ values['skip-native']) {
698
+ fail('--track cannot be combined with --matrix, --variant, --probe, --system, --runs, or --skip-native');
699
+ return 2;
700
+ }
701
+ let matrix = null;
702
+ if (values['matrix-artifact']) {
703
+ matrix = refreshRankingMatrixReport(await readRankingMatrixArtifact(values['matrix-artifact']));
704
+ if (!matrix.selection) {
705
+ fail('the matrix artifact has no selected configuration');
706
+ return 2;
707
+ }
708
+ if (values.split && values.split !== matrix.split) {
709
+ fail(`--split ${values.split} does not match the matrix split ${matrix.split}`);
710
+ return 2;
711
+ }
712
+ }
713
+ const selection = matrix?.selection ?? null;
714
+ const selectedVariant = selection
715
+ ? (matrix.variants.find((variant) => variant.id === selection.variantId) ?? null)
716
+ : null;
717
+ const split = parseRankingSplit(values.split ?? matrix?.split);
718
+ const candidateCount = parseCandidateCount(values.candidates ?? (selection ? String(selection.candidateCount) : undefined));
719
+ const excerptChars = parseExcerptChars(values['excerpt-chars'] ?? (selectedVariant ? String(selectedVariant.excerptChars) : undefined));
720
+ const concurrency = parseBoundedInteger(values.concurrency, 1, 16, 'concurrency');
721
+ const openAiPreset = selectedVariant?.provider === 'openai';
722
+ const embeddingDimensions = parseBoundedInteger(values['embedding-dimensions'] ??
723
+ (openAiPreset ? '1536' : String(config.models.embedding.dimensions ?? 1024)), 1, 65_536, 'embedding dimensions');
724
+ if (!split ||
725
+ !candidateCount ||
726
+ !excerptChars ||
727
+ concurrency === null ||
728
+ embeddingDimensions === null) {
729
+ return 2;
730
+ }
731
+ if (split !== 'development' && (!matrix || !hasIndependentRankingReview(matrix))) {
732
+ fail('held-out ranking tracks require a reviewed --matrix-artifact');
733
+ return 2;
734
+ }
735
+ if (selection && values.candidates && candidateCount !== selection.candidateCount) {
736
+ fail('--candidates does not match the matrix selection');
737
+ return 2;
738
+ }
739
+ if (selectedVariant && values['excerpt-chars'] && excerptChars !== selectedVariant.excerptChars) {
740
+ fail('--excerpt-chars does not match the matrix selection');
741
+ return 2;
742
+ }
743
+ if (selection && values.reasoning && reasoning !== selection.reasoningEffort) {
744
+ fail('--reasoning does not match the matrix selection');
745
+ return 2;
746
+ }
747
+ if (selectedVariant && values.provider && values.provider !== selectedVariant.provider) {
748
+ fail('--provider does not match the matrix selection');
749
+ return 2;
750
+ }
751
+ if (selectedVariant && values.model && values.model !== selectedVariant.model) {
752
+ fail('--model does not match the matrix selection');
753
+ return 2;
754
+ }
755
+ const report = await runRankingEndToEnd(config, {
756
+ split,
757
+ candidateCount,
758
+ excerptChars,
759
+ ...(values.concurrency ? { concurrency } : {}),
760
+ embeddingProvider: values['embedding-provider'] ??
761
+ (openAiPreset ? 'openai' : (config.models.embedding.provider?.name ?? 'local')),
762
+ embeddingModel: values['embedding-model'] ??
763
+ (openAiPreset ? 'text-embedding-3-small' : (config.models.embedding.id ?? undefined)),
764
+ embeddingDimensions,
765
+ provider: values.provider ?? selectedVariant?.provider ?? 'openai',
766
+ model: values.model ?? selectedVariant?.model ?? 'gpt-5.6-luna',
767
+ reasoningEffort: values.reasoning ? reasoning : (selection?.reasoningEffort ?? reasoning),
768
+ ...(!values.json ? { onProgress: renderRankingEndToEndProgress() } : {}),
769
+ });
770
+ let artifactPath = null;
771
+ if (values.output)
772
+ artifactPath = await writeJsonArtifact(values.output, report);
773
+ let matrixPath = null;
774
+ if (matrix && values['matrix-artifact']) {
775
+ matrix = attachRankingEndToEndEvidence(matrix, report);
776
+ matrixPath = await writeJsonArtifact(values['matrix-artifact'], matrix);
777
+ }
778
+ if (values.json)
779
+ json(report);
780
+ else
781
+ renderRankingEndToEnd(report, artifactPath, matrixPath, matrix);
782
+ return report.passed ? 0 : 1;
783
+ }
784
+ if (values.matrix) {
785
+ if (values.probe ||
786
+ values.system ||
787
+ values.candidates ||
788
+ values.reasoning ||
789
+ values['matrix-artifact']) {
790
+ fail('--matrix cannot be combined with --probe, --system, --candidates, --reasoning, or --matrix-artifact');
791
+ return 2;
792
+ }
793
+ const split = parseRankingSplit(values.split);
794
+ const excerptChars = parseExcerptChars(values['excerpt-chars']);
795
+ const concurrency = parseBoundedInteger(values.concurrency, 1, 16, 'concurrency');
796
+ const runs = parseBoundedInteger(values.runs, 1, 10, 'runs');
797
+ const selectedVariantId = parseRankingMatrixVariant(values.variant);
798
+ if (!split ||
799
+ !excerptChars ||
800
+ concurrency === null ||
801
+ runs === null ||
802
+ (values.variant && !selectedVariantId))
803
+ return 2;
804
+ if (selectedVariantId && split !== 'development') {
805
+ fail('--variant is development-only; use the complete pre-declared matrix for held-out evidence');
806
+ return 2;
807
+ }
808
+ if (split !== 'development' && !heldOutReview) {
809
+ fail('held-out ranking requires --input with an approved independent review packet');
810
+ return 2;
811
+ }
812
+ if (split === 'development' && heldOutReview) {
813
+ fail('--input is only accepted when a ranking run reads the held-out split');
814
+ return 2;
815
+ }
816
+ let report = await runRankingMatrix(config, {
817
+ split,
818
+ excerptChars,
819
+ ...(values.provider ? { provider: values.provider } : {}),
820
+ ...(values.model ? { model: values.model } : {}),
821
+ ...(values.concurrency ? { concurrency } : {}),
822
+ ...(values.runs ? { runs } : {}),
823
+ ...(values['skip-native'] ? { includeNative: false } : {}),
824
+ ...(selectedVariantId ? { variants: [selectedVariantId] } : {}),
825
+ ...(!values.json
826
+ ? {
827
+ onProgress: ({ variant: id, run, runs: total, }) => line(` ${id} run ${run}/${total}`),
828
+ }
829
+ : {}),
830
+ });
831
+ if (heldOutReview)
832
+ report = attachRankingReviewEvidence(report, heldOutReview);
833
+ let artifactPath = null;
834
+ if (values.output) {
835
+ report = markRankingMatrixPersisted(report);
836
+ artifactPath = await writeJsonArtifact(values.output, report);
837
+ }
838
+ if (values.json)
839
+ json(report);
840
+ else
841
+ renderRankingMatrix(report, artifactPath);
842
+ return report.variants
843
+ .filter((entry) => entry.system === 'llm')
844
+ .every((entry) => entry.comparisonEligible)
845
+ ? 0
846
+ : 1;
847
+ }
848
+ if (values.probe) {
849
+ const report = await runLlmRankingProbe(config, {
850
+ ...(values.provider ? { provider: values.provider } : {}),
851
+ ...(values.model ? { model: values.model } : {}),
852
+ reasoningEffort: reasoning,
853
+ });
854
+ if (values.json)
855
+ json(report);
856
+ else {
857
+ heading('LLM ranking — invented live smoke probe');
858
+ line(` provider ${report.provider}`);
859
+ line(` model ${report.model}`);
860
+ line(` reasoning ${report.reasoningEffort}`);
861
+ line(` latency ${Math.round(report.latencyMs)}ms`);
862
+ line(` order ${report.order.join(' → ') || 'none'}`);
863
+ line(` relevance ${report.relevance.join(', ') || 'none'}`);
864
+ line(`\n${report.passed ? style.green('probe passed') : style.red(report.error ?? 'probe failed')}`);
865
+ line(style.grey('This verifies transport, schema, and one safety case; it is not the release benchmark.'));
866
+ }
867
+ return report.passed ? 0 : 1;
868
+ }
869
+ const system = parseRankingSystem(values.system);
870
+ if (!system) {
871
+ fail(`invalid ranking system: ${values.system}`);
872
+ return 2;
873
+ }
874
+ const split = parseRankingSplit(values.split);
875
+ if (!split) {
876
+ fail(`invalid ranking split: ${values.split}`);
877
+ return 2;
878
+ }
879
+ const candidateCount = parseCandidateCount(values.candidates);
880
+ const excerptChars = parseExcerptChars(values['excerpt-chars']);
881
+ const concurrency = parseBoundedInteger(values.concurrency, 1, 16, 'concurrency');
882
+ if (!candidateCount || !excerptChars || concurrency === null)
883
+ return 2;
884
+ if (split !== 'development' && !heldOutReview) {
885
+ fail('held-out ranking requires --input with an approved independent review packet');
886
+ return 2;
887
+ }
888
+ if (split === 'development' && heldOutReview) {
889
+ fail('--input is only accepted when a ranking run reads the held-out split');
890
+ return 2;
891
+ }
892
+ if (values.output ||
893
+ values.variant ||
894
+ values.runs ||
895
+ values['skip-native'] ||
896
+ values['matrix-artifact']) {
897
+ fail('--output, --variant, --runs, --skip-native, and --matrix-artifact require --matrix or --track');
898
+ return 2;
899
+ }
900
+ const report = await runRankingBench(config, {
901
+ system,
902
+ split,
903
+ candidateCount,
904
+ excerptChars,
905
+ ...(values.concurrency ? { concurrency } : {}),
906
+ ...(values.provider ? { provider: values.provider } : {}),
907
+ ...(values.model ? { model: values.model } : {}),
908
+ reasoningEffort: reasoning,
909
+ });
910
+ if (values.json)
911
+ json(report);
912
+ else
913
+ renderRanking(report);
914
+ return report.passed ? 0 : 1;
915
+ }
916
+ if (positionals.length > 0) {
917
+ fail(`unknown bench target: ${positionals[0]}`);
918
+ return 2;
919
+ }
920
+ if (values['retrieval-only']) {
921
+ const retrieval = await runMixedRetrievalBench({
922
+ ...(values.iterations ? { iterations: Number(values.iterations) } : {}),
923
+ });
924
+ if (values.json)
925
+ json({ passed: retrieval.passed, retrieval });
926
+ else {
927
+ renderRetrieval(retrieval);
928
+ line(`\n${retrieval.passed ? style.green('all retrieval assertions met') : style.red('retrieval regression')}`);
929
+ }
930
+ return retrieval.passed ? 0 : 1;
931
+ }
932
+ const openOptions = openOptionsFrom(values);
933
+ const mem = await open({ ...openOptions, writable: values.write });
934
+ // "Lexical only" has to mean the model stack is absent, not that we hoped it
935
+ // would go unused. A second handle over the same index with no models
936
+ // configured is the only honest way to measure the index path alone.
937
+ let lexical = null;
938
+ try {
939
+ lexical = await open({
940
+ ...openOptions,
941
+ writable: false,
942
+ overrides: {
943
+ providers: {},
944
+ models: {
945
+ embedding: { id: null },
946
+ reranker: { id: null, enabled: false },
947
+ derive: { id: null },
948
+ expansion: { id: null },
949
+ },
950
+ },
951
+ });
952
+ }
953
+ catch {
954
+ // Without it the index-path cases report as skipped, with the reason.
955
+ }
956
+ try {
957
+ const report = await runBench(mem, {
958
+ ...(values.iterations ? { iterations: Number(values.iterations) } : {}),
959
+ ...(lexical ? { lexical } : {}),
960
+ ...(values.write ? { writable: mem } : {}),
961
+ });
962
+ if (values.json) {
963
+ json(report);
964
+ return report.passed ? 0 : 1;
965
+ }
966
+ heading(`Bench — ${report.pages} pages, ${report.chunks} chunks`);
967
+ const width = Math.max(...report.results.map((result) => result.name.length));
968
+ for (const result of report.results) {
969
+ if (result.skipped) {
970
+ const label = result.passed ? style.grey('skipped') : style.red('FAIL');
971
+ line(` ${result.name.padEnd(width)} ${label} ${style.grey(result.skipped)}`);
972
+ continue;
973
+ }
974
+ const verdict = result.budgetMs === null
975
+ ? style.cyan('measured')
976
+ : result.passed
977
+ ? style.green('pass')
978
+ : style.red('FAIL');
979
+ const budget = result.budgetMs === null ? 'not budgeted' : `budget ${result.budgetMs}ms`;
980
+ line(` ${result.name.padEnd(width)} ${verdict} ` +
981
+ `${style.grey(`p50 ${result.p50Ms}ms p95 ${result.p95Ms}ms ${budget}`)}`);
982
+ }
983
+ renderRetrieval(report.retrieval);
984
+ for (const note of report.notes)
985
+ line(`\n ${style.grey(note)}`);
986
+ line(`\n${report.passed ? style.green('all asserted budgets met') : style.red('budget regression')}`);
987
+ return report.passed ? 0 : 1;
988
+ }
989
+ finally {
990
+ await lexical?.close();
991
+ await mem.close();
992
+ }
993
+ }
994
+ function parseReasoningEffort(value) {
995
+ const effort = value ?? 'none';
996
+ return effort === 'none' ||
997
+ effort === 'low' ||
998
+ effort === 'medium' ||
999
+ effort === 'high' ||
1000
+ effort === 'xhigh' ||
1001
+ effort === 'max'
1002
+ ? effort
1003
+ : null;
1004
+ }
1005
+ function parseRankingSystem(value) {
1006
+ const system = value ?? 'fusion';
1007
+ return system === 'fusion' || system === 'native' || system === 'llm' ? system : null;
1008
+ }
1009
+ function parseRankingSplit(value) {
1010
+ const split = value ?? 'development';
1011
+ return split === 'development' || split === 'test' || split === 'all' ? split : null;
1012
+ }
1013
+ function parseRankingMatrixVariant(value) {
1014
+ if (value === undefined)
1015
+ return null;
1016
+ if (RANKING_MATRIX_VARIANT_IDS.includes(value)) {
1017
+ return value;
1018
+ }
1019
+ fail(`invalid ranking matrix variant: ${value} (expected ${RANKING_MATRIX_VARIANT_IDS.join(', ')})`);
1020
+ return null;
1021
+ }
1022
+ function parseCandidateCount(value) {
1023
+ const count = Number(value ?? 20);
1024
+ if (count === 10 || count === 20 || count === 40)
1025
+ return count;
1026
+ fail(`invalid candidate count: ${value}`);
1027
+ return null;
1028
+ }
1029
+ function parseExcerptChars(value) {
1030
+ const chars = Number(value ?? 800);
1031
+ if (chars === 400 || chars === 800 || chars === 1600)
1032
+ return chars;
1033
+ fail(`invalid excerpt length: ${value}`);
1034
+ return null;
1035
+ }
1036
+ function parseBoundedInteger(value, minimum, maximum, label) {
1037
+ if (value === undefined)
1038
+ return minimum;
1039
+ const parsed = Number(value);
1040
+ if (Number.isInteger(parsed) && parsed >= minimum && parsed <= maximum)
1041
+ return parsed;
1042
+ fail(`invalid ${label}: ${value} (expected ${minimum}..${maximum})`);
1043
+ return null;
1044
+ }
1045
+ function renderRetrieval(report) {
1046
+ heading(`Mixed retrieval — invented corpus, ${report.corpus.pages} pages, ` +
1047
+ `${report.corpus.orphanDocuments} orphan documents`);
1048
+ const width = Math.max(...report.results.map((result) => result.name.length));
1049
+ for (const result of report.results) {
1050
+ const verdict = result.passed ? style.green('pass') : style.red('FAIL');
1051
+ line(` ${result.name.padEnd(width)} ${verdict} ` +
1052
+ style.grey(`${metricValue(result.value, result.unit)} ` +
1053
+ `(${result.comparison === 'at_least' ? '≥' : '≤'} ${metricValue(result.target, result.unit)}); ` +
1054
+ result.detail));
1055
+ }
1056
+ }
1057
+ function renderRanking(report) {
1058
+ heading(`Ranking ${report.split} corpus — ${report.system}, ${report.corpus.queries} queries, ` +
1059
+ `${report.corpus.judgments} judgments over ${report.corpus.sources} invented sources`);
1060
+ if (report.model) {
1061
+ line(` model ${report.provider}/${report.model}`);
1062
+ if (report.reasoningEffort)
1063
+ line(` reasoning ${report.reasoningEffort}`);
1064
+ }
1065
+ line(` nDCG@10 ${fixed(report.quality.ndcgAt10)}`);
1066
+ line(` delta from fusion ${signed(report.ndcgDeltaFromFusion)}`);
1067
+ line(` MRR@10 ${fixed(report.quality.mrrAt10)}`);
1068
+ line(` success@1 / @3 ${percent(report.quality.successAt1)} / ${percent(report.quality.successAt3)}`);
1069
+ line(` precision@5 ${percent(report.quality.precisionAt5)}`);
1070
+ line(` zero-over-direct ${fixed(report.quality.gradeZeroAboveGradeThree)}`);
1071
+ line(` valid responses ${percent(report.validResponseRate)}`);
1072
+ line(' category nDCG Δ');
1073
+ for (const category of report.byCategory) {
1074
+ line(` ${category.category.padEnd(24)} ${signed(category.ndcgDeltaFromFusion)}`);
1075
+ }
1076
+ if (report.qualification) {
1077
+ line(` direct answers kept ${percent(report.qualification.answerRetention)}`);
1078
+ line(` support retained ${percent(report.qualification.supportRetention)}`);
1079
+ line(` marginal retained ${percent(report.qualification.marginalRetention)}`);
1080
+ line(` irrelevant rejected ${percent(report.qualification.irrelevantRejection)}`);
1081
+ line(` retained precision ${percent(report.qualification.retainedPrecision)}`);
1082
+ line(` injection rejected ${percent(report.qualification.instructionNegativeRejection)}`);
1083
+ }
1084
+ if (report.calibration.basis === 'auto') {
1085
+ line(` native threshold ${report.calibration.threshold ?? 'unavailable'}`);
1086
+ line(` observed score bounds ${report.calibration.lowestAnswerScore ?? 'n/a'} answer / ` +
1087
+ `${report.calibration.lowestSupportScore ?? 'n/a'} support / ` +
1088
+ `${report.calibration.highestIrrelevantScore ?? 'n/a'} irrelevant`);
1089
+ }
1090
+ if (report.p95LatencyMs > 0) {
1091
+ line(` latency p50 / p95 ${Math.round(report.p50LatencyMs)}ms / ${Math.round(report.p95LatencyMs)}ms`);
1092
+ }
1093
+ if (report.execution.requests > 0) {
1094
+ line(` endpoint requests ${report.execution.endpointRequests} ` +
1095
+ `(${report.execution.extraEndpointRequests} beyond ${report.execution.requests} logical calls)`);
1096
+ }
1097
+ const tokenUsage = report.execution.tokenUsage;
1098
+ if (tokenUsage) {
1099
+ line(` provider tokens ${tokenUsage.inputTokens ?? 'n/a'} input / ` +
1100
+ `${tokenUsage.outputTokens ?? 'n/a'} output on ` +
1101
+ `${tokenUsage.reportedQueries}/${report.execution.requests} calls`);
1102
+ if (tokenUsage.cachedInputTokens !== null || tokenUsage.reasoningOutputTokens !== null) {
1103
+ line(` token details ${tokenUsage.cachedInputTokens ?? 'n/a'} cached input / ` +
1104
+ `${tokenUsage.reasoningOutputTokens ?? 'n/a'} reasoning output`);
1105
+ }
1106
+ }
1107
+ for (const failure of report.failures)
1108
+ line(` ${style.red(failure.queryId)} ${failure.error}`);
1109
+ line(`\n${report.passed ? style.green('development gate passed') : style.red('development gate failed')}`);
1110
+ line(style.grey('Single-system results are tuning evidence; review receipts attach only to a matrix.'));
1111
+ }
1112
+ function renderRankingReviewPacket(packet, artifactPath) {
1113
+ heading('Ranking corpus review — independent handoff packet');
1114
+ line(` corpus ${packet.corpus.version}`);
1115
+ line(` fingerprint ${packet.corpus.fingerprint}`);
1116
+ line(` scope ${packet.corpus.sources} sources, ${packet.corpus.queries} queries, ` +
1117
+ `${packet.corpus.judgments} judgments`);
1118
+ line(` packet ${artifactPath}`);
1119
+ line('\n Reviewer workflow');
1120
+ packet.instructions.forEach((instruction, index) => line(` ${index + 1}. ${instruction}`));
1121
+ line(style.grey('The packet contains corpus content and grades, but no prompt, model response, score, or benchmark outcome.'));
1122
+ }
1123
+ function renderRankingReviewEvidence(evidence, matrixPath, matrix) {
1124
+ heading('Ranking corpus review — receipt attached');
1125
+ line(` reviewer ${evidence.reviewerKind}`);
1126
+ line(` reviewed ${evidence.reviewedAt}`);
1127
+ line(` coverage ${evidence.sourceReviews} sources, ${evidence.caseReviews} cases`);
1128
+ line(` corpus ${evidence.corpusFingerprint}`);
1129
+ line(` receipt ${evidence.receiptFingerprint}`);
1130
+ line(` matrix ${matrixPath}`);
1131
+ if (matrix.releaseGate.blockers.length > 0) {
1132
+ line(` release blockers ${matrix.releaseGate.blockers.join(', ')}`);
1133
+ }
1134
+ line(`\n${style.green('independent review receipt accepted')}`);
1135
+ }
1136
+ function hasIndependentRankingReview(matrix) {
1137
+ return matrix.releaseGate.checks.some((check) => check.id === 'independent_review' && check.passed);
1138
+ }
1139
+ function renderEntityResolution(report, artifactPath) {
1140
+ heading(`Entity resolution — ${report.provider}/${report.model}, invented corpus`);
1141
+ line(` reasoning ${report.reasoningEffort}`);
1142
+ line(` valid responses ${percent(report.metrics.validResponseRate)}`);
1143
+ line(` clear-case recall ${percent(report.metrics.clearRecall)}`);
1144
+ line(` selection precision ${percent(report.metrics.selectionPrecision)}`);
1145
+ line(` indistinguishable abstention ${percent(report.metrics.indistinguishableAbstention)}`);
1146
+ line(` adversarial abstention ${percent(report.metrics.adversarialAbstention)}`);
1147
+ line(` expected outcomes ${percent(report.metrics.expectedOutcomeAccuracy)}`);
1148
+ for (const bench of report.cases) {
1149
+ const verdict = bench.passed ? style.green('pass') : style.red('FAIL');
1150
+ line(` ${bench.id.padEnd(34)} ${verdict} ` +
1151
+ style.grey(`expected ${bench.expected ?? 'abstain'}, got ${bench.selected ?? 'abstain'}`));
1152
+ if (bench.error)
1153
+ line(` ${style.red(bench.error)}`);
1154
+ }
1155
+ if (artifactPath)
1156
+ line(` artifact ${artifactPath}`);
1157
+ if (report.blockers.length > 0)
1158
+ line(` blockers ${report.blockers.join(', ')}`);
1159
+ line(`\n${report.passed ? style.green('entity-resolution gate passed') : style.red('entity-resolution gate failed')}`);
1160
+ }
1161
+ function renderAnswerBench(report, artifactPath) {
1162
+ heading(`Grounded answers — ${report.split}, ${report.corpus.cases} cases across ` +
1163
+ `${report.corpus.categories} categories, ${report.stability.requestedRuns} run(s)`);
1164
+ line(` answer model ${report.answerModel.provider}/${report.answerModel.model}`);
1165
+ line(` reasoning ${report.answerModel.reasoningEffort ?? 'provider default'}`);
1166
+ line(` embedding ${report.embedding.provider}/${report.embedding.model}`);
1167
+ line(` execution ${percent(report.metrics.executionRate)}`);
1168
+ line(` expected outcomes ${percent(report.metrics.outcomeAccuracy)}`);
1169
+ line(` expected facts ${percent(report.metrics.expectedFactAccuracy)}`);
1170
+ line(` citation precision ${percent(report.metrics.citationPrecision)} / recall ${percent(report.metrics.citationRecall)}`);
1171
+ line(` retrieval recall ${percent(report.metrics.retrievalRecall)}`);
1172
+ line(` abstention accuracy ${percent(report.metrics.abstentionAccuracy)}`);
1173
+ line(` privacy leak rate ${percent(report.metrics.privacyLeakRate)}`);
1174
+ line(` degraded rate ${percent(report.metrics.degradedRate)}`);
1175
+ line(` verifier failure rate ${percent(report.metrics.verificationFailureRate)}`);
1176
+ line(` case stability ${report.stability.stableCaseRate === null ? 'n/a (one run)' : percent(report.stability.stableCaseRate)}`);
1177
+ line(` minimum run pass rate ${percent(report.stability.minimumRunPassRate)}`);
1178
+ line(` latency p50 / p95 ${Math.round(report.execution.p50LatencyMs)}ms / ` +
1179
+ `${Math.round(report.execution.p95LatencyMs)}ms ` +
1180
+ style.grey(`(gate ≤ ${report.thresholds.p95LatencyMs}ms)`));
1181
+ line(` provider usage ${report.execution.providerTotalTokens} tokens across ` +
1182
+ `${report.execution.usageReportedCalls}/${report.execution.modelCalls} reported calls`);
1183
+ if (report.runs.length > 1) {
1184
+ for (const run of report.runs) {
1185
+ line(` run ${String(run.run).padEnd(2)} ${run.casesPassed}/${run.casesTotal} passed, ` +
1186
+ `p95 ${Math.round(run.p95LatencyMs)}ms`);
1187
+ }
1188
+ }
1189
+ for (const benchCase of report.cases) {
1190
+ const verdict = benchCase.passed ? style.green('pass') : style.red('FAIL');
1191
+ line(` ${benchCase.id.padEnd(36)} ${verdict} ` +
1192
+ style.grey(`${benchCase.status ?? 'not run'}/${benchCase.outcome ?? benchCase.error ?? 'unknown'}`));
1193
+ }
1194
+ if (report.stability.flakyCaseIds.length > 0) {
1195
+ line(` flaky cases ${report.stability.flakyCaseIds.join(', ')}`);
1196
+ }
1197
+ if (artifactPath)
1198
+ line(` artifact ${artifactPath}`);
1199
+ if (report.blockers.length > 0)
1200
+ line(` blockers ${report.blockers.join(', ')}`);
1201
+ line(`\n${report.passed ? style.green(`${report.split} quality gate passed`) : style.red(`${report.split} quality gate failed`)}`);
1202
+ line(report.releaseEligible
1203
+ ? style.green('Stored held-out evidence satisfies every release gate.')
1204
+ : style.grey(`Release remains blocked by: ${report.releaseBlockers.join(', ')}.`));
1205
+ }
1206
+ function renderAutoRecallBench(report, artifactPath) {
1207
+ heading(`Auto-recall — ${report.split}, ${report.corpus.cases} cases across ` +
1208
+ `${report.corpus.categories} categories, ${report.stability.requestedRuns} run(s)`);
1209
+ line(` qualifier ${report.qualifier.provider}/${report.qualifier.model}`);
1210
+ line(` mode / reasoning ${report.qualifier.mode}/${report.qualifier.reasoningEffort ?? 'default'}`);
1211
+ line(` embedding ${report.embedding.provider}/${report.embedding.model}`);
1212
+ line(` execution ${percent(report.metrics.executionRate)}`);
1213
+ line(` activation precision ${percent(report.metrics.activationPrecision)} / recall ${percent(report.metrics.activationRecall)}`);
1214
+ line(` activation accuracy ${percent(report.metrics.activationAccuracy)}`);
1215
+ line(` source precision ${percent(report.metrics.sourcePrecision)} / recall ${percent(report.metrics.sourceRecall)}`);
1216
+ line(` irrelevant injection ${percent(report.metrics.irrelevantInjectionRate)}`);
1217
+ line(` qualification policy ${percent(report.metrics.qualificationAccuracy)}`);
1218
+ line(` exact locators ${percent(report.metrics.locatorAccuracy)}`);
1219
+ line(` evidence isolation ${percent(report.metrics.evidenceIsolation)}`);
1220
+ line(` hard-budget compliance ${percent(report.metrics.budgetCompliance)}`);
1221
+ line(` degraded rate ${percent(report.metrics.degradedRate)}`);
1222
+ line(` qualifier activation ${percent(report.execution.qualificationRate)} ` +
1223
+ style.grey(`(gate ≤ ${percent(report.thresholds.maximumQualificationRate)})`));
1224
+ line(` latency p50 / p95 ${Math.round(report.execution.p50LatencyMs)}ms / ` +
1225
+ `${Math.round(report.execution.p95LatencyMs)}ms ` +
1226
+ style.grey(`(gate ≤ ${report.thresholds.p95LatencyMs}ms)`));
1227
+ line(` provider usage ${report.execution.providerTotalTokens} tokens across ` +
1228
+ `${report.execution.usageReportedCalls}/${report.execution.qualificationCalls} reported calls`);
1229
+ line(` case stability ${report.stability.stableCaseRate === null ? 'n/a (one run)' : percent(report.stability.stableCaseRate)}`);
1230
+ line(` minimum run pass rate ${percent(report.stability.minimumRunPassRate)}`);
1231
+ for (const benchCase of report.cases) {
1232
+ const verdict = benchCase.passed ? style.green('pass') : style.red('FAIL');
1233
+ const outcome = benchCase.activated ? `activated/${benchCase.activationBasis}` : 'empty';
1234
+ line(` ${benchCase.id.padEnd(40)} ${verdict} ` +
1235
+ style.grey(`${outcome}; ${benchCase.selectedCount} selected; q=${benchCase.qualificationRun}`));
1236
+ }
1237
+ if (report.stability.flakyCaseIds.length > 0) {
1238
+ line(` flaky cases ${report.stability.flakyCaseIds.join(', ')}`);
1239
+ }
1240
+ if (artifactPath)
1241
+ line(` artifact ${artifactPath}`);
1242
+ if (report.blockers.length > 0)
1243
+ line(` blockers ${report.blockers.join(', ')}`);
1244
+ line(`\n${report.passed ? style.green(`${report.split} injection gate passed`) : style.red(`${report.split} injection gate failed`)}`);
1245
+ line(report.releaseEligible
1246
+ ? style.green('Stored held-out evidence satisfies every release gate.')
1247
+ : style.grey(`Release remains blocked by: ${report.releaseBlockers.join(', ')}.`));
1248
+ }
1249
+ function renderAutoRecallAnswerBench(report, artifactPath) {
1250
+ heading(`Auto-recall host answers — ${report.split}, ${report.corpus.cases} paired cases across ` +
1251
+ `${report.corpus.categories} categories, ${report.stability.requestedRuns} run(s)`);
1252
+ line(` host model ${report.hostModel.provider}/${report.hostModel.model}`);
1253
+ line(` reasoning ${report.hostModel.reasoningEffort ?? 'provider default'}`);
1254
+ line(` qualifier ${report.qualifier.provider}/${report.qualifier.model}`);
1255
+ line(` embedding ${report.embedding.provider}/${report.embedding.model}`);
1256
+ line(` execution ${percent(report.metrics.executionRate)}`);
1257
+ line(` context activation ${percent(report.metrics.activationAccuracy)}`);
1258
+ line(` evidence fact coverage ${percent(report.metrics.evidenceFactAccuracy)}`);
1259
+ line(` answers with memory ${percent(report.metrics.withMemoryAccuracy)}`);
1260
+ line(` facts with memory ${percent(report.metrics.withMemoryFactAccuracy)}`);
1261
+ line(` abstention with memory ${percent(report.metrics.withMemoryAbstentionAccuracy)}`);
1262
+ line(` abstention without memory ${percent(report.metrics.withoutMemoryAbstentionAccuracy)}`);
1263
+ line(` pairwise improvement ${percent(report.metrics.pairwiseImprovementRate)}`);
1264
+ line(` unsupported claim rate ${percent(report.metrics.unsupportedClaimRate)}`);
1265
+ line(` forbidden-memory leak rate ${percent(report.metrics.forbiddenLeakRate)}`);
1266
+ line(` context latency p50 / p95 ${Math.round(report.execution.p50ContextLatencyMs)}ms / ` +
1267
+ `${Math.round(report.execution.p95ContextLatencyMs)}ms`);
1268
+ line(` on total latency p50 / p95 ${Math.round(report.execution.p50OnTotalLatencyMs)}ms / ` +
1269
+ `${Math.round(report.execution.p95OnTotalLatencyMs)}ms`);
1270
+ line(` incremental p95 ${Math.round(report.execution.p95IncrementalLatencyMs)}ms ` +
1271
+ style.grey(`(gate ≤ ${report.thresholds.incrementalP95LatencyMs}ms)`));
1272
+ line(` host provider usage ${report.execution.hostProviderTotalTokens} tokens across ` +
1273
+ `${report.execution.hostUsageReportedCalls}/${report.execution.hostModelCalls} reported calls`);
1274
+ line(` qualifier provider usage ${report.execution.qualificationProviderTotalTokens} tokens across ` +
1275
+ `${report.execution.qualificationUsageReportedCalls}/${report.execution.qualificationCalls} reported calls`);
1276
+ line(` case stability ${report.stability.stableCaseRate === null ? 'n/a (one run)' : percent(report.stability.stableCaseRate)}`);
1277
+ for (const benchCase of report.cases) {
1278
+ const verdict = benchCase.passed ? style.green('pass') : style.red('FAIL');
1279
+ const memory = benchCase.contextActivated ? `${benchCase.evidenceCount} evidence` : 'empty';
1280
+ const answer = benchCase.withMemory.answered ? 'answered' : 'abstained';
1281
+ line(` ${benchCase.id.padEnd(40)} ${verdict} ${style.grey(`${memory}; on=${answer}`)}`);
1282
+ }
1283
+ if (report.stability.flakyCaseIds.length > 0) {
1284
+ line(` flaky cases ${report.stability.flakyCaseIds.join(', ')}`);
1285
+ }
1286
+ if (artifactPath)
1287
+ line(` artifact ${artifactPath}`);
1288
+ if (report.blockers.length > 0)
1289
+ line(` blockers ${report.blockers.join(', ')}`);
1290
+ line(`\n${report.passed ? style.green(`${report.split} host-answer gate passed`) : style.red(`${report.split} host-answer gate failed`)}`);
1291
+ line(report.releaseEligible
1292
+ ? style.green('Stored held-out evidence satisfies every release gate.')
1293
+ : style.grey(`Release remains blocked by: ${report.releaseBlockers.join(', ')}.`));
1294
+ }
1295
+ function renderGraphBench(report, artifactPath) {
1296
+ heading(`Graph release gate — frozen ${report.split} corpus, ${report.corpus.pages} invented pages, ` +
1297
+ `${report.corpus.cases} cases`);
1298
+ line(` expected outcomes ${percent(report.metrics.expectedOutcomeAccuracy)}`);
1299
+ line(` exact identity ${percent(report.metrics.identityAccuracy)}`);
1300
+ line(` ambiguous abstention ${percent(report.metrics.ambiguousAbstention)}`);
1301
+ line(` traversable provenance ${percent(report.metrics.provenanceAccuracy)}`);
1302
+ line(` bounded path recall ${percent(report.metrics.pathRecall)}`);
1303
+ line(` graph-only false positives ${percent(report.metrics.graphOnlyFalsePositiveRate)}`);
1304
+ line(` maintenance discovery ${percent(report.metrics.maintenanceRecall)}`);
1305
+ line(` graph latency p50 / p95 ${Math.round(report.metrics.p50LatencyMs)}ms / ` +
1306
+ `${Math.round(report.metrics.p95LatencyMs)}ms`);
1307
+ line(` mixed retrieval regression ${report.metrics.mixedRetrievalPassed ? 'none' : style.red('FAIL')}`);
1308
+ for (const bench of report.cases) {
1309
+ const verdict = bench.passed ? style.green('pass') : style.red('FAIL');
1310
+ line(` ${bench.id.padEnd(36)} ${verdict} ${style.grey(bench.detail)}`);
1311
+ }
1312
+ if (artifactPath)
1313
+ line(` artifact ${artifactPath}`);
1314
+ if (report.blockers.length > 0)
1315
+ line(` blockers ${report.blockers.join(', ')}`);
1316
+ line(`\n${report.passed ? style.green('graph release gate passed') : style.red('graph release gate failed')}`);
1317
+ if (!report.corpus.independentlyReviewed) {
1318
+ line(style.grey('The corpus is held out from user data but still awaits independent corpus review.'));
1319
+ }
1320
+ }
1321
+ function renderMergeDiscoveryBench(report, artifactPath) {
1322
+ heading(`Semantic merge discovery — ${report.split}, ${report.corpus.pages} invented pages, ` +
1323
+ `${report.corpus.cases} declared pairs, ${report.stability.requestedRuns} run(s)`);
1324
+ line(` corpus reviewed ${report.corpus.independentlyReviewed ? 'yes' : 'no'}`);
1325
+ line(` embedding ${report.embedding.provider}/${report.embedding.model}`);
1326
+ line(` embedding-only threshold ${report.embeddingOnly.threshold.toFixed(4)}`);
1327
+ line(` embedding-only recall ${percent(report.embeddingOnly.metrics.candidateRecall)}`);
1328
+ line(` embedding-only precision ${percent(report.embeddingOnly.metrics.candidatePrecision)}`);
1329
+ line(` embedding-only margin ${report.embeddingOnly.metrics.scoreMargin.toFixed(4)}`);
1330
+ line(` embedding latency ${Math.round(report.embeddingLatencyMs)}ms`);
1331
+ for (const bench of report.embeddingOnly.cases) {
1332
+ const verdict = bench.passed ? style.green('pass') : style.red('FAIL');
1333
+ line(` ${bench.id.padEnd(32)} ${verdict} ${bench.score.toFixed(4)} ` +
1334
+ style.grey(`${bench.expected}; ${bench.selected ? 'selected' : 'separate'}`));
1335
+ }
1336
+ if (report.classifier) {
1337
+ line(`\n classifier ${report.classifier.provider}/${report.classifier.model}`);
1338
+ line(` classifier reasoning ${report.classifier.reasoningEffort}`);
1339
+ line(` prefilter threshold ${report.classifier.prefilterThreshold.toFixed(4)}`);
1340
+ line(` classifier calls ${report.classifier.calls}`);
1341
+ line(` final candidate recall ${percent(report.classifier.metrics.candidateRecall)}`);
1342
+ line(` final candidate precision ${percent(report.classifier.metrics.candidatePrecision)}`);
1343
+ line(` final false positives ${percent(report.classifier.metrics.falsePositiveRate)}`);
1344
+ line(` related-scope rejection ${percent(report.classifier.metrics.relatedScopeRejection)}`);
1345
+ line(` template rejection ${percent(report.classifier.metrics.templateRejection)}`);
1346
+ line(` entity-collision rejection ${percent(report.classifier.metrics.entityCollisionRejection)}`);
1347
+ for (const bench of report.classifier.cases) {
1348
+ const verdict = bench.passed ? style.green('pass') : style.red('FAIL');
1349
+ line(` ${bench.id.padEnd(32)} ${verdict} ` +
1350
+ style.grey(`${bench.prefiltered ? (bench.outcome ?? 'invalid') : 'prefiltered'}; ${Math.round(bench.latencyMs)}ms`));
1351
+ }
1352
+ }
1353
+ line(` classifier stability ${report.stability.stableCaseRate === null ? 'n/a (one run)' : percent(report.stability.stableCaseRate)}`);
1354
+ line(` passing runs ${report.stability.passingRuns}/${report.stability.requestedRuns}`);
1355
+ if (report.stability.flakyCaseIds.length > 0) {
1356
+ line(` flaky cases ${report.stability.flakyCaseIds.join(', ')}`);
1357
+ }
1358
+ if (artifactPath)
1359
+ line(` artifact ${artifactPath}`);
1360
+ if (report.blockers.length > 0)
1361
+ line(` blockers ${report.blockers.join(', ')}`);
1362
+ line(`\n${report.passed ? style.green(`${report.split} quality gate passed`) : style.red(`${report.split} quality gate failed`)}`);
1363
+ line(report.releaseEligible
1364
+ ? style.green('Stored held-out evidence satisfies every semantic-discovery release gate.')
1365
+ : style.grey(`Release remains blocked by: ${report.releaseBlockers.join(', ')}.`));
1366
+ }
1367
+ function renderRankingMatrix(report, artifactPath) {
1368
+ const targeted = report.targetedVariants !== null;
1369
+ heading(`${targeted ? 'Targeted ranking evidence' : 'Ranking matrix'} — ${report.split}, ` +
1370
+ `${report.requestedRuns} repeated runs, concurrency ${report.concurrency}`);
1371
+ for (const variant of report.variants) {
1372
+ const stability = variant.medianTop3Overlap === null ? 'n/a' : percent(variant.medianTop3Overlap);
1373
+ line(` ${variant.id.padEnd(16)} ${variant.comparisonEligible ? style.green('measured') : style.red('UNAVAILABLE')} ` +
1374
+ `nDCG ${fixed(variant.quality.ndcgAt10)} Δ ${signed(variant.ndcgDeltaFromFusion)} ` +
1375
+ `top3 ${stability} p95 ${Math.round(variant.p95LatencyMs)}ms`);
1376
+ const usage = variant.execution.tokenUsage;
1377
+ if (usage?.reportedQueries) {
1378
+ const averageInput = usage.inputTokens === null ? 'n/a' : String(Math.round(usage.inputTokens / usage.reportedQueries));
1379
+ const averageOutput = usage.outputTokens === null ? 'n/a' : String(Math.round(usage.outputTokens / usage.reportedQueries));
1380
+ line(` ${''.padEnd(16)} ${averageInput}/${averageOutput} input/output tokens per reported query; ` +
1381
+ `${variant.execution.endpointRequests} endpoint requests ` +
1382
+ `(${variant.execution.extraEndpointRequests} extra)`);
1383
+ }
1384
+ }
1385
+ if (report.selection) {
1386
+ line(`\n selected ${report.selection.variantId}: ${report.selection.candidateCount} candidates, ` +
1387
+ `${report.selection.reasoningEffort} reasoning`);
1388
+ line(` ${style.grey(report.selection.rationale)}`);
1389
+ }
1390
+ if (report.reviewEvidence) {
1391
+ line(` reviewed ${report.reviewEvidence.reviewerKind} at ${report.reviewEvidence.reviewedAt}; ` +
1392
+ `${report.reviewEvidence.receiptFingerprint.slice(0, 12)}…`);
1393
+ }
1394
+ if (artifactPath)
1395
+ line(` artifact ${artifactPath}`);
1396
+ if (!targeted && report.releaseGate.blockers.length > 0) {
1397
+ line(` release blockers ${report.releaseGate.blockers.join(', ')}`);
1398
+ }
1399
+ const measurementsComplete = report.variants
1400
+ .filter((variant) => variant.system === 'llm')
1401
+ .every((variant) => variant.comparisonEligible);
1402
+ line(`\n${measurementsComplete ? style.green(`${targeted ? 'targeted' : 'matrix'} measurements complete`) : style.red(`${targeted ? 'targeted' : 'matrix'} measurements incomplete`)}`);
1403
+ if (targeted) {
1404
+ line(style.grey('Targeted evidence measures one contract; it cannot select or release a preset.'));
1405
+ return;
1406
+ }
1407
+ line(report.releaseEligible
1408
+ ? style.green('Stored held-out evidence satisfies every release gate.')
1409
+ : style.grey('The preset remains blocked until every release gate is evidenced.'));
1410
+ }
1411
+ function renderRankingLatency(report, artifactPath, matrixPath, matrix) {
1412
+ heading(`Ranking latency — ${report.split}, ${report.candidateCount} candidates, ` +
1413
+ `${report.reasoningEffort} reasoning`);
1414
+ for (const [label, profile] of [
1415
+ ['interactive', report.interactive],
1416
+ ['loaded', report.loaded],
1417
+ ]) {
1418
+ line(` ${label.padEnd(12)} concurrency ${profile.concurrency}`);
1419
+ line(` cold ${Math.round(profile.cold.p50LatencyMs)}ms; ` +
1420
+ `${profile.cold.endpointRequests} endpoint request(s)`);
1421
+ line(` warm p50/p95/max ${Math.round(profile.warm.p50LatencyMs)} / ` +
1422
+ `${Math.round(profile.warm.p95LatencyMs)} / ${Math.round(profile.warm.maxLatencyMs)}ms; ` +
1423
+ `${percent(profile.warm.validResponseRate)} valid; ` +
1424
+ `${profile.warm.extraEndpointRequests} extra endpoint requests`);
1425
+ }
1426
+ line(` UX gate warm single-flight p95 ≤ ${report.thresholds.interactiveP95LatencyMs}ms`);
1427
+ if (artifactPath)
1428
+ line(` artifact ${artifactPath}`);
1429
+ line(` matrix ${matrixPath}`);
1430
+ if (report.blockers.length > 0)
1431
+ line(` blockers ${report.blockers.join(', ')}`);
1432
+ if (matrix.releaseGate.blockers.length > 0) {
1433
+ line(` release blockers ${matrix.releaseGate.blockers.join(', ')}`);
1434
+ }
1435
+ line(`\n${report.passed ? style.green('latency evidence passed') : style.red('latency evidence failed')}`);
1436
+ }
1437
+ function renderRankingEndToEnd(report, artifactPath, matrixPath, matrix) {
1438
+ heading(`Ranking end-to-end — ${report.split}, ${report.corpus.queries} queries, ` +
1439
+ `${report.candidateCount}-candidate window`);
1440
+ line(` embedding ${report.embedding.provider ?? 'unavailable'}/${report.embedding.model ?? 'unavailable'}` +
1441
+ ` (${report.embedding.dimensions}d)${report.embedding.available ? '' : ' [unavailable]'}`);
1442
+ line(` embedded chunks ${report.embedding.embeddedChunks}/${report.embedding.totalChunks}`);
1443
+ line(` reranker ${report.reranker.provider ?? 'unavailable'}/${report.reranker.model ?? 'unavailable'} ` +
1444
+ `(${report.reranker.reasoningEffort ?? 'none'})${report.reranker.available ? '' : ' [unavailable]'}`);
1445
+ if (report.embedding.available) {
1446
+ line(` retrieval / judged ${report.retrievalPoolCount} / ${report.candidateCount} ` +
1447
+ `(${report.candidateSelectionVersion})`);
1448
+ line(` fusion-pool recall ${percent(report.fusionPool.directAnswerRecall)}`);
1449
+ line(` candidate answer recall ${percent(report.candidateGeneration.directAnswerRecall)}`);
1450
+ line(` ranked answer recall ${percent(report.rankedRecall.directAnswerRecall)}`);
1451
+ line(` ranked success@1 / @3 ${percent(report.rankedRecall.successAt1)} / ${percent(report.rankedRecall.successAt3)}`);
1452
+ line(` ranked MRR@10 ${fixed(report.rankedRecall.mrrAt10)}`);
1453
+ line(` candidate p50 / p95 ${Math.round(report.candidateGeneration.p50LatencyMs)}ms / ` +
1454
+ `${Math.round(report.candidateGeneration.p95LatencyMs)}ms`);
1455
+ line(` ranked p50 / p95 ${Math.round(report.rankedRecall.p50LatencyMs)}ms / ` +
1456
+ `${Math.round(report.rankedRecall.p95LatencyMs)}ms`);
1457
+ line(` rerank fallback rate ${percent(report.rerankFallbackRate)}`);
1458
+ }
1459
+ else {
1460
+ line(' recall not run — a complete embedding index is required');
1461
+ }
1462
+ line(` degraded queries ${report.candidateGeneration.degradedQueries} candidate / ` +
1463
+ `${report.rankedRecall.degradedQueries} ranked`);
1464
+ const candidateMisses = report.queries.filter((query) => query.candidateRank === null);
1465
+ const rankedMisses = report.queries.filter((query) => query.rankedRank === null);
1466
+ if (report.embedding.available && candidateMisses.length > 0) {
1467
+ line(` candidate misses ${candidateMisses
1468
+ .slice(0, 5)
1469
+ .map((query) => query.queryId)
1470
+ .join(', ')}`);
1471
+ }
1472
+ if (report.embedding.available && rankedMisses.length > 0) {
1473
+ line(` ranked misses ${rankedMisses
1474
+ .slice(0, 5)
1475
+ .map((query) => query.queryId)
1476
+ .join(', ')}`);
1477
+ }
1478
+ if (artifactPath)
1479
+ line(` artifact ${artifactPath}`);
1480
+ if (matrixPath)
1481
+ line(` matrix updated ${matrixPath}`);
1482
+ if (matrix && matrix.releaseGate.blockers.length > 0) {
1483
+ line(` release blockers ${matrix.releaseGate.blockers.join(', ')}`);
1484
+ }
1485
+ line(`\n${report.passed ? style.green('end-to-end recall gate passed') : style.red('end-to-end recall gate failed')}`);
1486
+ line(style.grey(rankingEndToEndEvidenceNote(report.split)));
1487
+ }
1488
+ export function rankingEndToEndEvidenceNote(split) {
1489
+ return split === 'test'
1490
+ ? 'Held-out evidence is final; preserve this result instead of rerunning or using it as tuning input.'
1491
+ : 'Development evidence cannot substitute for independent review or a held-out run.';
1492
+ }
1493
+ function renderRankingEndToEndProgress() {
1494
+ return (progress) => {
1495
+ if (progress.done !== 0 && progress.done !== progress.total)
1496
+ return;
1497
+ const label = progress.phase.replaceAll('_', ' ');
1498
+ line(` ${label} ${progress.done}/${progress.total}`);
1499
+ };
1500
+ }
1501
+ async function readRankingMatrixArtifact(target) {
1502
+ const absolute = path.resolve(target);
1503
+ const parsed = JSON.parse(await fsp.readFile(absolute, 'utf8'));
1504
+ if (parsed.kind !== 'ranking_matrix' || !Array.isArray(parsed.variants)) {
1505
+ throw new Error(`${absolute} is not a ranking matrix artifact`);
1506
+ }
1507
+ return parsed;
1508
+ }
1509
+ async function readJsonArtifact(target) {
1510
+ return JSON.parse(await fsp.readFile(path.resolve(target), 'utf8'));
1511
+ }
1512
+ async function writeJsonArtifact(target, report) {
1513
+ const absolute = path.resolve(target);
1514
+ await fsp.mkdir(path.dirname(absolute), { recursive: true });
1515
+ const temporary = `${absolute}.${process.pid}.${randomUUID()}.tmp`;
1516
+ try {
1517
+ await fsp.writeFile(temporary, `${JSON.stringify(report, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
1518
+ await fsp.rename(temporary, absolute);
1519
+ }
1520
+ catch (error) {
1521
+ await fsp.unlink(temporary).catch(() => undefined);
1522
+ throw error;
1523
+ }
1524
+ return absolute;
1525
+ }
1526
+ function metricValue(value, unit) {
1527
+ if (unit === 'ratio')
1528
+ return `${Math.round(value * 100)}%`;
1529
+ if (unit === 'milliseconds')
1530
+ return `${value}ms`;
1531
+ return String(value);
1532
+ }
1533
+ function fixed(value) {
1534
+ return value.toFixed(3);
1535
+ }
1536
+ function signed(value) {
1537
+ return `${value >= 0 ? '+' : ''}${fixed(value)}`;
1538
+ }
1539
+ function percent(value) {
1540
+ return `${Math.round(value * 1000) / 10}%`;
1541
+ }
1542
+ //# sourceMappingURL=bench-cmd.js.map