@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.
- package/LICENSE +83 -0
- package/README.md +46 -0
- package/dist/args.d.ts +18 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +92 -0
- package/dist/args.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +142 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/adopt-cmd.d.ts +2 -0
- package/dist/commands/adopt-cmd.d.ts.map +1 -0
- package/dist/commands/adopt-cmd.js +61 -0
- package/dist/commands/adopt-cmd.js.map +1 -0
- package/dist/commands/answer-cmd.d.ts +2 -0
- package/dist/commands/answer-cmd.d.ts.map +1 -0
- package/dist/commands/answer-cmd.js +150 -0
- package/dist/commands/answer-cmd.js.map +1 -0
- package/dist/commands/bench-cmd.d.ts +4 -0
- package/dist/commands/bench-cmd.d.ts.map +1 -0
- package/dist/commands/bench-cmd.js +1542 -0
- package/dist/commands/bench-cmd.js.map +1 -0
- package/dist/commands/config-cmd.d.ts +7 -0
- package/dist/commands/config-cmd.d.ts.map +1 -0
- package/dist/commands/config-cmd.js +80 -0
- package/dist/commands/config-cmd.js.map +1 -0
- package/dist/commands/context-cmd.d.ts +2 -0
- package/dist/commands/context-cmd.d.ts.map +1 -0
- package/dist/commands/context-cmd.js +130 -0
- package/dist/commands/context-cmd.js.map +1 -0
- package/dist/commands/doctor-cmd.d.ts +2 -0
- package/dist/commands/doctor-cmd.d.ts.map +1 -0
- package/dist/commands/doctor-cmd.js +198 -0
- package/dist/commands/doctor-cmd.js.map +1 -0
- package/dist/commands/dream-cmd.d.ts +26 -0
- package/dist/commands/dream-cmd.d.ts.map +1 -0
- package/dist/commands/dream-cmd.js +1167 -0
- package/dist/commands/dream-cmd.js.map +1 -0
- package/dist/commands/dream-model-status.d.ts +5 -0
- package/dist/commands/dream-model-status.d.ts.map +1 -0
- package/dist/commands/dream-model-status.js +33 -0
- package/dist/commands/dream-model-status.js.map +1 -0
- package/dist/commands/dream-notifications.d.ts +24 -0
- package/dist/commands/dream-notifications.d.ts.map +1 -0
- package/dist/commands/dream-notifications.js +168 -0
- package/dist/commands/dream-notifications.js.map +1 -0
- package/dist/commands/dream-schedule.d.ts +55 -0
- package/dist/commands/dream-schedule.d.ts.map +1 -0
- package/dist/commands/dream-schedule.js +160 -0
- package/dist/commands/dream-schedule.js.map +1 -0
- package/dist/commands/folder-cmd.d.ts +2 -0
- package/dist/commands/folder-cmd.d.ts.map +1 -0
- package/dist/commands/folder-cmd.js +84 -0
- package/dist/commands/folder-cmd.js.map +1 -0
- package/dist/commands/graph-cmd.d.ts +2 -0
- package/dist/commands/graph-cmd.d.ts.map +1 -0
- package/dist/commands/graph-cmd.js +126 -0
- package/dist/commands/graph-cmd.js.map +1 -0
- package/dist/commands/inbox-cmd.d.ts +2 -0
- package/dist/commands/inbox-cmd.d.ts.map +1 -0
- package/dist/commands/inbox-cmd.js +69 -0
- package/dist/commands/inbox-cmd.js.map +1 -0
- package/dist/commands/index-cmd.d.ts +2 -0
- package/dist/commands/index-cmd.d.ts.map +1 -0
- package/dist/commands/index-cmd.js +102 -0
- package/dist/commands/index-cmd.js.map +1 -0
- package/dist/commands/ingest-cmd.d.ts +2 -0
- package/dist/commands/ingest-cmd.d.ts.map +1 -0
- package/dist/commands/ingest-cmd.js +167 -0
- package/dist/commands/ingest-cmd.js.map +1 -0
- package/dist/commands/init-cmd.d.ts +292 -0
- package/dist/commands/init-cmd.d.ts.map +1 -0
- package/dist/commands/init-cmd.js +476 -0
- package/dist/commands/init-cmd.js.map +1 -0
- package/dist/commands/init-prompts.d.ts +51 -0
- package/dist/commands/init-prompts.d.ts.map +1 -0
- package/dist/commands/init-prompts.js +192 -0
- package/dist/commands/init-prompts.js.map +1 -0
- package/dist/commands/list-cmd.d.ts +2 -0
- package/dist/commands/list-cmd.d.ts.map +1 -0
- package/dist/commands/list-cmd.js +77 -0
- package/dist/commands/list-cmd.js.map +1 -0
- package/dist/commands/maintenance-policy-output.d.ts +4 -0
- package/dist/commands/maintenance-policy-output.d.ts.map +1 -0
- package/dist/commands/maintenance-policy-output.js +72 -0
- package/dist/commands/maintenance-policy-output.js.map +1 -0
- package/dist/commands/mutate-cmd.d.ts +5 -0
- package/dist/commands/mutate-cmd.d.ts.map +1 -0
- package/dist/commands/mutate-cmd.js +224 -0
- package/dist/commands/mutate-cmd.js.map +1 -0
- package/dist/commands/plan-cmd.d.ts +9 -0
- package/dist/commands/plan-cmd.d.ts.map +1 -0
- package/dist/commands/plan-cmd.js +411 -0
- package/dist/commands/plan-cmd.js.map +1 -0
- package/dist/commands/read-cmd.d.ts +2 -0
- package/dist/commands/read-cmd.d.ts.map +1 -0
- package/dist/commands/read-cmd.js +89 -0
- package/dist/commands/read-cmd.js.map +1 -0
- package/dist/commands/recall-cmd.d.ts +2 -0
- package/dist/commands/recall-cmd.d.ts.map +1 -0
- package/dist/commands/recall-cmd.js +176 -0
- package/dist/commands/recall-cmd.js.map +1 -0
- package/dist/commands/redeploy-cmd.d.ts +43 -0
- package/dist/commands/redeploy-cmd.d.ts.map +1 -0
- package/dist/commands/redeploy-cmd.js +270 -0
- package/dist/commands/redeploy-cmd.js.map +1 -0
- package/dist/commands/rules-cmd.d.ts +2 -0
- package/dist/commands/rules-cmd.d.ts.map +1 -0
- package/dist/commands/rules-cmd.js +72 -0
- package/dist/commands/rules-cmd.js.map +1 -0
- package/dist/commands/serve-cmd.d.ts +23 -0
- package/dist/commands/serve-cmd.d.ts.map +1 -0
- package/dist/commands/serve-cmd.js +415 -0
- package/dist/commands/serve-cmd.js.map +1 -0
- package/dist/commands/timeline-cmd.d.ts +2 -0
- package/dist/commands/timeline-cmd.d.ts.map +1 -0
- package/dist/commands/timeline-cmd.js +67 -0
- package/dist/commands/timeline-cmd.js.map +1 -0
- package/dist/commands/write-cmd.d.ts +49 -0
- package/dist/commands/write-cmd.d.ts.map +1 -0
- package/dist/commands/write-cmd.js +269 -0
- package/dist/commands/write-cmd.js.map +1 -0
- package/dist/ops-handle.d.ts +67 -0
- package/dist/ops-handle.d.ts.map +1 -0
- package/dist/ops-handle.js +196 -0
- package/dist/ops-handle.js.map +1 -0
- package/dist/output.d.ts +28 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +87 -0
- package/dist/output.js.map +1 -0
- package/dist/serve/http.d.ts +19 -0
- package/dist/serve/http.d.ts.map +1 -0
- package/dist/serve/http.js +135 -0
- package/dist/serve/http.js.map +1 -0
- package/dist/serve/mcp.d.ts +21 -0
- package/dist/serve/mcp.d.ts.map +1 -0
- package/dist/serve/mcp.js +79 -0
- package/dist/serve/mcp.js.map +1 -0
- package/dist/serve/socket.d.ts +15 -0
- package/dist/serve/socket.d.ts.map +1 -0
- package/dist/serve/socket.js +311 -0
- package/dist/serve/socket.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +15 -0
- package/dist/version.js.map +1 -0
- 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
|