@ngockhoale/ukit 3.4.47 → 3.4.49
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/CHANGELOG.md +15 -0
- package/manifests/documentation.yaml +22 -0
- package/manifests/platform.full.yaml +27 -0
- package/package.json +4 -1
- package/scripts/dev/worktree-preflight.mjs +20 -1
- package/src/core/diffPlan.js +38 -12
- package/src/core/executionContracts.js +79 -18
- package/src/core/metadata.js +4 -0
- package/src/core/runInstallPipeline.js +19 -2
- package/src/core/runtimeConfig.js +1 -1
- package/src/index/playbookRegistry.js +22 -3
- package/src/index/taskRouting.js +2 -2
- package/template_project/.claude/agents/bug-debugger.md +1 -0
- package/template_project/.claude/agents/code-reviewer.md +1 -0
- package/template_project/.claude/agents/feature-implementer.md +1 -0
- package/template_project/.claude/agents/handoff-planner.md +1 -0
- package/template_project/.claude/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.claude/hooks/completion-gate.sh +1 -1
- package/template_project/.claude/hooks/delegation-gate.mjs +343 -0
- package/template_project/.claude/hooks/skill-router.sh +159 -8
- package/template_project/.claude/settings.json +8 -8
- package/template_project/.claude/ukit/index/playbook-registry.mjs +22 -3
- package/template_project/.claude/ukit/index/route-resolver.mjs +79 -17
- package/template_project/.claude/ukit/index/route-task.mjs +115 -37
- package/template_project/.claude/ukit/index/tier-lane.mjs +202 -0
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +3 -1
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +331 -21
- package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +8 -0
- package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
- package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -0
- package/template_project/.claude/ukit/runtime/playbook-execution.mjs +704 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +79 -3
- package/template_project/.omp/agents/bug-debugger.md +1 -0
- package/template_project/.omp/agents/code-reviewer.md +1 -0
- package/template_project/.omp/agents/feature-implementer.md +1 -0
- package/template_project/.omp/agents/handoff-planner.md +1 -0
- package/template_project/.omp/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.omp/hooks/pre/ukit-bridge.js +254 -7
- package/template_project/ukit/storage/config.json +1 -1
- package/scripts/bench/data-foundation.mjs +0 -929
- package/scripts/bench/decision-runtime-baseline.mjs +0 -427
- package/scripts/bench/decision-runtime-variant.mjs +0 -626
- package/scripts/bench/goldTasks.json +0 -38
- package/scripts/bench/memory-ablation.mjs +0 -495
- package/scripts/bench/memory-baseline.mjs +0 -596
- package/scripts/bench/memory-bench.mjs +0 -661
- package/scripts/bench/memory-canary.mjs +0 -331
- package/scripts/bench/memory-corpus.mjs +0 -354
- package/scripts/bench/memory-gate.mjs +0 -389
- package/scripts/bench/memory-metrics.mjs +0 -179
- package/scripts/bench/outline-savings.mjs +0 -107
- package/scripts/bench/parallel-agents.mjs +0 -224
- package/scripts/bench/recorder-overhead.mjs +0 -204
- package/scripts/bench/runGold.mjs +0 -238
- package/scripts/bench/sqlite-spike.mjs +0 -451
- package/scripts/bench/subagent-orchestrator-corpus.mjs +0 -275
- package/scripts/bench/subagent-orchestrator-eval.mjs +0 -565
- package/scripts/bench/v3-ceremony.mjs +0 -220
- package/scripts/perf/audit-perf.mjs +0 -1195
- package/scripts/perf/diff-perf-findings.mjs +0 -136
- package/scripts/perf/perf-findings.json +0 -474
- package/scripts/perf/perf-measure.md +0 -271
|
@@ -1,929 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// data-foundation bench harness (C54 / DF-00)
|
|
3
|
-
//
|
|
4
|
-
// --fixture Regenerate tests/fixtures/observability/corpus.json deterministically (seed=42).
|
|
5
|
-
// --recorder Emit the corpus through createRecorder into a temp segment store.
|
|
6
|
-
// --analytics Rebuild the derived index from retained segments and print summaries.
|
|
7
|
-
// --evaluate Replay the golden corpus (tests/fixtures/observability/golden/cases.json)
|
|
8
|
-
// under both pre-registered policies and print paired scorecards +
|
|
9
|
-
// a fixed-seed variance report (TASK-014).
|
|
10
|
-
// --perturb Inject seeded synthetic mutations into corpus summaries and measure
|
|
11
|
-
// detection through computeFingerprint/detectOpportunities (TASK-002).
|
|
12
|
-
// --anomalies Replay the golden cases, summarize each run, and report anomaly
|
|
13
|
-
// detection counts per case (TASK-015). Degrades honestly to
|
|
14
|
-
// `detector: 'unavailable'` while analytics/anomalies.js
|
|
15
|
-
// (TASK-011) does not exist — never a stub detector.
|
|
16
|
-
//
|
|
17
|
-
// Flags run in argument order; the exit code is the first non-zero code.
|
|
18
|
-
// --evaluate exits 1 by design while the corpus carries expected-negative
|
|
19
|
-
// strata (a fail verdict is the honest scorecard), so a combined invocation
|
|
20
|
-
// ending in --evaluate exits 1.
|
|
21
|
-
//
|
|
22
|
-
// The corpus is a synthetic, seeded fixture: no real user content. Records are
|
|
23
|
-
// envelope-shaped per SPEC §7 (DF-FR01) but intentionally NOT schema-validated —
|
|
24
|
-
// TASK-003 owns validation. Unknown host measurements are marked UNKNOWN, never 0.
|
|
25
|
-
|
|
26
|
-
import fs from 'node:fs';
|
|
27
|
-
import os from 'node:os';
|
|
28
|
-
import path from 'node:path';
|
|
29
|
-
import { fileURLToPath } from 'node:url';
|
|
30
|
-
import { SEMANTIC_REGISTRY } from '../../src/core/observability/schema/registry.js';
|
|
31
|
-
import { PRIVACY_CLASSES } from '../../src/core/observability/schema/constants.js';
|
|
32
|
-
|
|
33
|
-
const SEED = 42;
|
|
34
|
-
const METRIC_VERSION = 'df-m1';
|
|
35
|
-
const SCHEMA_VERSION = 1;
|
|
36
|
-
const BOOT_ID = 'boot-fixture-0001';
|
|
37
|
-
const WRITER_ID = 'writer-fixture-0001';
|
|
38
|
-
const SESSION_ID = 'session-fixture-0001';
|
|
39
|
-
const PROJECT_REF = 'project-fixture-0001';
|
|
40
|
-
const WALL_EPOCH_MS = Date.UTC(2026, 0, 1, 0, 0, 0); // fixed epoch → deterministic wall_time_utc
|
|
41
|
-
|
|
42
|
-
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
43
|
-
const corpusPath = path.join(repoRoot, 'tests/fixtures/observability/corpus.json');
|
|
44
|
-
|
|
45
|
-
// Records may raise privacy above the registry floor but never lower it —
|
|
46
|
-
// validateSemanticRecord (TASK-003) rejects below-floor classes, so the
|
|
47
|
-
// fixture clamps each record's declared class up to the registry floor.
|
|
48
|
-
function clampToRegistryFloor(semanticName, privacyClass) {
|
|
49
|
-
const floor = SEMANTIC_REGISTRY[semanticName]?.privacy_class;
|
|
50
|
-
if (!floor) return privacyClass;
|
|
51
|
-
return PRIVACY_CLASSES.indexOf(privacyClass) < PRIVACY_CLASSES.indexOf(floor) ? floor : privacyClass;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Deterministic PRNG (mulberry32) — used only for token counts; IDs are counters.
|
|
55
|
-
function mulberry32(seed) {
|
|
56
|
-
let a = seed >>> 0;
|
|
57
|
-
return function next() {
|
|
58
|
-
a |= 0;
|
|
59
|
-
a = (a + 0x6d2b79f5) | 0;
|
|
60
|
-
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
61
|
-
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
62
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function makeCtx() {
|
|
67
|
-
const rand = mulberry32(SEED);
|
|
68
|
-
let seq = 0;
|
|
69
|
-
let rec = 0;
|
|
70
|
-
let span = 0;
|
|
71
|
-
let trace = 0;
|
|
72
|
-
let clockMs = 0;
|
|
73
|
-
return {
|
|
74
|
-
nextSeq: () => ++seq,
|
|
75
|
-
recordId: () => `rec-${String(++rec).padStart(4, '0')}`,
|
|
76
|
-
spanId: () => `span-${String(++span).padStart(4, '0')}`,
|
|
77
|
-
traceId: () => `trace-${String(++trace).padStart(4, '0')}`,
|
|
78
|
-
// advance the deterministic clock by `ms` and return { wall_time_utc, monotonic_ns }
|
|
79
|
-
tick: (ms) => {
|
|
80
|
-
clockMs += ms;
|
|
81
|
-
return {
|
|
82
|
-
wall_time_utc: new Date(WALL_EPOCH_MS + clockMs).toISOString(),
|
|
83
|
-
monotonic_ns: clockMs * 1e6,
|
|
84
|
-
};
|
|
85
|
-
},
|
|
86
|
-
tokens: (lo, hi) => lo + Math.floor(rand() * (hi - lo + 1)),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
function record(ctx, { semanticName, traceId, spanId, parentSpanId, executionId, agentId, importance, privacyClass, payload, dtMs }) {
|
|
90
|
-
const t = ctx.tick(dtMs);
|
|
91
|
-
const rec = {
|
|
92
|
-
record_type: 'fact',
|
|
93
|
-
semantic_name: semanticName,
|
|
94
|
-
schema_version: SCHEMA_VERSION,
|
|
95
|
-
record_id: ctx.recordId(),
|
|
96
|
-
trace_id: traceId,
|
|
97
|
-
span_id: spanId,
|
|
98
|
-
parent_span_id: parentSpanId,
|
|
99
|
-
execution_id: executionId,
|
|
100
|
-
session_id: SESSION_ID,
|
|
101
|
-
project_ref: PROJECT_REF,
|
|
102
|
-
boot_id: BOOT_ID,
|
|
103
|
-
writer_id: WRITER_ID,
|
|
104
|
-
sequence: ctx.nextSeq(),
|
|
105
|
-
wall_time_utc: t.wall_time_utc,
|
|
106
|
-
monotonic_ns: t.monotonic_ns,
|
|
107
|
-
importance,
|
|
108
|
-
privacy_class: clampToRegistryFloor(semanticName, privacyClass),
|
|
109
|
-
payload,
|
|
110
|
-
};
|
|
111
|
-
// DF2-FR01 v1.1: agent_id is the optional multi-agent correlation field —
|
|
112
|
-
// emitted only when the scenario declares one.
|
|
113
|
-
if (typeof agentId === 'string' && agentId.length > 0) rec.agent_id = agentId;
|
|
114
|
-
return rec;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function execSpan(ctx, traceId, executionId, privacyClass, agentId) {
|
|
118
|
-
const spanId = ctx.spanId();
|
|
119
|
-
return {
|
|
120
|
-
spanId,
|
|
121
|
-
started: record(ctx, {
|
|
122
|
-
semanticName: 'execution.started',
|
|
123
|
-
traceId,
|
|
124
|
-
spanId,
|
|
125
|
-
parentSpanId: null,
|
|
126
|
-
executionId,
|
|
127
|
-
agentId,
|
|
128
|
-
importance: 'normal',
|
|
129
|
-
privacyClass,
|
|
130
|
-
payload: { operation: 'task', duration_ms: null },
|
|
131
|
-
dtMs: 10,
|
|
132
|
-
}),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function modelAttempt(ctx, { traceId, parentSpanId, executionId, agentId, attemptIndex, status, resource, privacyClass, extra = {} }) {
|
|
137
|
-
const spanId = ctx.spanId();
|
|
138
|
-
const started = record(ctx, {
|
|
139
|
-
semanticName: 'model.started',
|
|
140
|
-
traceId,
|
|
141
|
-
spanId,
|
|
142
|
-
parentSpanId,
|
|
143
|
-
executionId,
|
|
144
|
-
agentId,
|
|
145
|
-
importance: 'normal',
|
|
146
|
-
privacyClass,
|
|
147
|
-
payload: { attempt_index: attemptIndex, ...extra },
|
|
148
|
-
dtMs: 5,
|
|
149
|
-
});
|
|
150
|
-
const completed = record(ctx, {
|
|
151
|
-
semanticName: status === 'ok' ? 'model.completed' : 'model.failed',
|
|
152
|
-
traceId,
|
|
153
|
-
spanId,
|
|
154
|
-
parentSpanId,
|
|
155
|
-
executionId,
|
|
156
|
-
agentId,
|
|
157
|
-
importance: 'normal',
|
|
158
|
-
privacyClass,
|
|
159
|
-
payload: {
|
|
160
|
-
attempt_index: attemptIndex,
|
|
161
|
-
duration_ms: 120 + attemptIndex * 30,
|
|
162
|
-
resource,
|
|
163
|
-
...(status === 'ok' ? {} : { error_code: extra.error_code || 'MODEL_ERROR' }),
|
|
164
|
-
...extra,
|
|
165
|
-
},
|
|
166
|
-
dtMs: 120 + attemptIndex * 30,
|
|
167
|
-
});
|
|
168
|
-
return [started, completed];
|
|
169
|
-
}
|
|
170
|
-
function execEnd(ctx, { traceId, spanId, executionId, agentId, status, privacyClass, extra = {} }) {
|
|
171
|
-
return record(ctx, {
|
|
172
|
-
semanticName: `execution.${status}`,
|
|
173
|
-
traceId,
|
|
174
|
-
spanId,
|
|
175
|
-
parentSpanId: null,
|
|
176
|
-
executionId,
|
|
177
|
-
agentId,
|
|
178
|
-
importance: 'normal',
|
|
179
|
-
privacyClass,
|
|
180
|
-
payload: { duration_ms: 0, ...extra },
|
|
181
|
-
dtMs: 10,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function sumUsage(attempts) {
|
|
186
|
-
const values = attempts
|
|
187
|
-
.map((r) => r.payload.resource)
|
|
188
|
-
.filter((res) => res && typeof res.value === 'number')
|
|
189
|
-
.map((res) => res.value);
|
|
190
|
-
return values.length ? values.reduce((a, b) => a + b, 0) : null;
|
|
191
|
-
}
|
|
192
|
-
function buildTrace(ctx, { scenario, privacyClass, agentId, build }) {
|
|
193
|
-
const traceId = ctx.traceId();
|
|
194
|
-
const executionId = `exec-${traceId}`;
|
|
195
|
-
const exec = execSpan(ctx, traceId, executionId, privacyClass, agentId);
|
|
196
|
-
const { records, attempts, succeeded } = build({ traceId, executionId, execSpanId: exec.spanId });
|
|
197
|
-
const all = [exec.started, ...records];
|
|
198
|
-
return {
|
|
199
|
-
trace_id: traceId,
|
|
200
|
-
scenario,
|
|
201
|
-
privacy_class: privacyClass,
|
|
202
|
-
records: all,
|
|
203
|
-
metrics: {
|
|
204
|
-
metric_version: METRIC_VERSION,
|
|
205
|
-
tokens_to_success: succeeded ? sumUsage(attempts) : null,
|
|
206
|
-
correction_observed: scenario === 'retry',
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function buildCorpus() {
|
|
212
|
-
const ctx = makeCtx();
|
|
213
|
-
const traces = [];
|
|
214
|
-
|
|
215
|
-
// success: single attempt, provider-reported usage.
|
|
216
|
-
traces.push(
|
|
217
|
-
buildTrace(ctx, {
|
|
218
|
-
scenario: 'success',
|
|
219
|
-
privacyClass: 'public',
|
|
220
|
-
build: ({ traceId, executionId, execSpanId }) => {
|
|
221
|
-
const resource = { value: ctx.tokens(800, 1200), source: 'PROVIDER' };
|
|
222
|
-
const attempts = modelAttempt(ctx, {
|
|
223
|
-
traceId, parentSpanId: execSpanId, executionId,
|
|
224
|
-
attemptIndex: 1, status: 'ok', resource, privacyClass: 'public',
|
|
225
|
-
});
|
|
226
|
-
const end = execEnd(ctx, {
|
|
227
|
-
traceId, spanId: execSpanId, executionId, status: 'completed',
|
|
228
|
-
privacyClass: 'public', extra: { outcome: 'success' },
|
|
229
|
-
});
|
|
230
|
-
return { records: [...attempts, end], attempts, succeeded: true };
|
|
231
|
-
},
|
|
232
|
-
}),
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
// retry: user correction observed → second attempt; tokens_to_success sums both.
|
|
236
|
-
traces.push(
|
|
237
|
-
buildTrace(ctx, {
|
|
238
|
-
scenario: 'retry',
|
|
239
|
-
privacyClass: 'public',
|
|
240
|
-
build: ({ traceId, executionId, execSpanId }) => {
|
|
241
|
-
const res1 = { value: ctx.tokens(400, 700), source: 'PROVIDER' };
|
|
242
|
-
const res2 = { value: ctx.tokens(500, 900), source: 'PROVIDER' };
|
|
243
|
-
const a1 = modelAttempt(ctx, {
|
|
244
|
-
traceId, parentSpanId: execSpanId, executionId,
|
|
245
|
-
attemptIndex: 1, status: 'ok', resource: res1, privacyClass: 'public',
|
|
246
|
-
});
|
|
247
|
-
const correction = record(ctx, {
|
|
248
|
-
semanticName: 'context.item.injected',
|
|
249
|
-
traceId, spanId: ctx.spanId(), parentSpanId: execSpanId, executionId,
|
|
250
|
-
importance: 'high', privacyClass: 'public',
|
|
251
|
-
payload: { kind: 'user_correction', correction_observed: true },
|
|
252
|
-
dtMs: 15,
|
|
253
|
-
});
|
|
254
|
-
const a2 = modelAttempt(ctx, {
|
|
255
|
-
traceId, parentSpanId: execSpanId, executionId,
|
|
256
|
-
attemptIndex: 2, status: 'ok', resource: res2, privacyClass: 'public',
|
|
257
|
-
});
|
|
258
|
-
const end = execEnd(ctx, {
|
|
259
|
-
traceId, spanId: execSpanId, executionId, status: 'completed',
|
|
260
|
-
privacyClass: 'public', extra: { outcome: 'success', attempts: 2 },
|
|
261
|
-
});
|
|
262
|
-
return { records: [...a1, correction, ...a2, end], attempts: [...a1, ...a2], succeeded: true };
|
|
263
|
-
},
|
|
264
|
-
}),
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
// failure: host-blind — usage UNKNOWN (never 0), execution failed.
|
|
268
|
-
traces.push(
|
|
269
|
-
buildTrace(ctx, {
|
|
270
|
-
scenario: 'failure',
|
|
271
|
-
privacyClass: 'public',
|
|
272
|
-
build: ({ traceId, executionId, execSpanId }) => {
|
|
273
|
-
const resource = { value: null, source: 'UNKNOWN' };
|
|
274
|
-
const attempts = modelAttempt(ctx, {
|
|
275
|
-
traceId, parentSpanId: execSpanId, executionId,
|
|
276
|
-
attemptIndex: 1, status: 'error', resource, privacyClass: 'public',
|
|
277
|
-
extra: { error_code: 'TOOL_TIMEOUT' },
|
|
278
|
-
});
|
|
279
|
-
const end = execEnd(ctx, {
|
|
280
|
-
traceId, spanId: execSpanId, executionId, status: 'failed',
|
|
281
|
-
privacyClass: 'public', extra: { outcome: 'failure', error_code: 'TOOL_TIMEOUT' },
|
|
282
|
-
});
|
|
283
|
-
return { records: [...attempts, end], attempts, succeeded: false };
|
|
284
|
-
},
|
|
285
|
-
}),
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
// dropped: queue overflow — telemetry.dropped counted, telemetry_complete=false.
|
|
289
|
-
traces.push(
|
|
290
|
-
buildTrace(ctx, {
|
|
291
|
-
scenario: 'dropped',
|
|
292
|
-
privacyClass: 'public',
|
|
293
|
-
build: ({ traceId, executionId, execSpanId }) => {
|
|
294
|
-
const resource = { value: ctx.tokens(300, 600), source: 'PROVIDER' };
|
|
295
|
-
const attempts = modelAttempt(ctx, {
|
|
296
|
-
traceId, parentSpanId: execSpanId, executionId,
|
|
297
|
-
attemptIndex: 1, status: 'ok', resource, privacyClass: 'public',
|
|
298
|
-
});
|
|
299
|
-
const dropped = record(ctx, {
|
|
300
|
-
semanticName: 'telemetry.dropped',
|
|
301
|
-
traceId, spanId: ctx.spanId(), parentSpanId: execSpanId, executionId,
|
|
302
|
-
importance: 'high', privacyClass: 'public',
|
|
303
|
-
payload: { reason_code: 'QUEUE_OVERFLOW', dropped_count: 3, telemetry_complete: false },
|
|
304
|
-
dtMs: 5,
|
|
305
|
-
});
|
|
306
|
-
const end = execEnd(ctx, {
|
|
307
|
-
traceId, spanId: execSpanId, executionId, status: 'completed',
|
|
308
|
-
privacyClass: 'public', extra: { outcome: 'success', telemetry_complete: false },
|
|
309
|
-
});
|
|
310
|
-
return { records: [...attempts, dropped, end], attempts, succeeded: true };
|
|
311
|
-
},
|
|
312
|
-
}),
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
// secret: payload carries a synthetic fixture secret (never a real credential);
|
|
316
|
-
// host-blind usage → UNKNOWN. TASK-004 leak tests scan for the marker string.
|
|
317
|
-
traces.push(
|
|
318
|
-
buildTrace(ctx, {
|
|
319
|
-
scenario: 'secret',
|
|
320
|
-
privacyClass: 'sensitive',
|
|
321
|
-
build: ({ traceId, executionId, execSpanId }) => {
|
|
322
|
-
const resource = { value: null, source: 'UNKNOWN' };
|
|
323
|
-
const attempts = modelAttempt(ctx, {
|
|
324
|
-
traceId, parentSpanId: execSpanId, executionId,
|
|
325
|
-
attemptIndex: 1, status: 'error', resource, privacyClass: 'sensitive',
|
|
326
|
-
extra: {
|
|
327
|
-
error_code: 'AUTH_FAILED',
|
|
328
|
-
error_detail: 'credential rejected: FIXTURE_SECRET_DO_NOT_LEAK_7f3a9c',
|
|
329
|
-
},
|
|
330
|
-
});
|
|
331
|
-
const end = execEnd(ctx, {
|
|
332
|
-
traceId, spanId: execSpanId, executionId, status: 'failed',
|
|
333
|
-
privacyClass: 'sensitive', extra: { outcome: 'failure', error_code: 'AUTH_FAILED' },
|
|
334
|
-
});
|
|
335
|
-
return { records: [...attempts, end], attempts, succeeded: false };
|
|
336
|
-
},
|
|
337
|
-
}),
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
// memory_outcome: retrieval.query → memory.retrieved hits → model →
|
|
341
|
-
// memory.write → execution.completed → outcome.observed — metadata-only
|
|
342
|
-
// payloads (counts, kinds, opaque refs), never content.
|
|
343
|
-
traces.push(
|
|
344
|
-
buildTrace(ctx, {
|
|
345
|
-
scenario: 'memory_outcome',
|
|
346
|
-
privacyClass: 'sensitive',
|
|
347
|
-
build: ({ traceId, executionId, execSpanId }) => {
|
|
348
|
-
const query = record(ctx, {
|
|
349
|
-
semanticName: 'retrieval.query',
|
|
350
|
-
traceId, spanId: ctx.spanId(), parentSpanId: execSpanId, executionId,
|
|
351
|
-
importance: 'normal', privacyClass: 'internal',
|
|
352
|
-
payload: {
|
|
353
|
-
lane: 'v2', scope: 'all', indexed: true,
|
|
354
|
-
candidate_count: ctx.tokens(10, 40), returned_count: 3,
|
|
355
|
-
duration_ms: ctx.tokens(5, 20), freshness: {},
|
|
356
|
-
},
|
|
357
|
-
dtMs: 8,
|
|
358
|
-
});
|
|
359
|
-
const retrieved = [
|
|
360
|
-
{ kind: 'fact' }, { kind: 'decision' }, { kind: 'decision' },
|
|
361
|
-
].map((hit) => record(ctx, {
|
|
362
|
-
semanticName: 'memory.retrieved',
|
|
363
|
-
traceId, spanId: ctx.spanId(), parentSpanId: execSpanId, executionId,
|
|
364
|
-
importance: 'normal', privacyClass: 'sensitive',
|
|
365
|
-
payload: { kind: hit.kind, record_ref: `memref-${ctx.tokens(1000, 9999)}` },
|
|
366
|
-
dtMs: 4,
|
|
367
|
-
}));
|
|
368
|
-
const resource = { value: ctx.tokens(800, 1200), source: 'PROVIDER' };
|
|
369
|
-
const attempts = modelAttempt(ctx, {
|
|
370
|
-
traceId, parentSpanId: execSpanId, executionId,
|
|
371
|
-
attemptIndex: 1, status: 'ok', resource, privacyClass: 'public',
|
|
372
|
-
});
|
|
373
|
-
const write = record(ctx, {
|
|
374
|
-
semanticName: 'memory.write',
|
|
375
|
-
traceId, spanId: ctx.spanId(), parentSpanId: execSpanId, executionId,
|
|
376
|
-
importance: 'normal', privacyClass: 'sensitive',
|
|
377
|
-
payload: {
|
|
378
|
-
op: 'write', record_count: ctx.tokens(1, 5), changed_count: 1,
|
|
379
|
-
kinds: { fact: 1 }, record_refs: [`memref-${ctx.tokens(1000, 9999)}`],
|
|
380
|
-
tombstone_count: 0,
|
|
381
|
-
},
|
|
382
|
-
dtMs: 6,
|
|
383
|
-
});
|
|
384
|
-
const end = execEnd(ctx, {
|
|
385
|
-
traceId, spanId: execSpanId, executionId, status: 'completed',
|
|
386
|
-
privacyClass: 'public', extra: { outcome: 'success' },
|
|
387
|
-
});
|
|
388
|
-
const outcome = record(ctx, {
|
|
389
|
-
semanticName: 'outcome.observed',
|
|
390
|
-
traceId, spanId: ctx.spanId(), parentSpanId: execSpanId, executionId,
|
|
391
|
-
importance: 'high', privacyClass: 'internal',
|
|
392
|
-
payload: {
|
|
393
|
-
kind: 'feedback', verdict: 'accepted', source: 'user',
|
|
394
|
-
evidence_refs: [end.record_id],
|
|
395
|
-
},
|
|
396
|
-
dtMs: 30,
|
|
397
|
-
});
|
|
398
|
-
return {
|
|
399
|
-
records: [query, ...retrieved, ...attempts, write, end, outcome],
|
|
400
|
-
attempts,
|
|
401
|
-
succeeded: true,
|
|
402
|
-
};
|
|
403
|
-
},
|
|
404
|
-
}),
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
// multi_agent: one trace spanning two agents — orchestrator runs the
|
|
408
|
-
// retrieval query and owns the execution span, worker runs the model
|
|
409
|
-
// attempt. agent_id is the v1.1 correlation field; trace_id stays single.
|
|
410
|
-
traces.push(
|
|
411
|
-
buildTrace(ctx, {
|
|
412
|
-
scenario: 'multi_agent',
|
|
413
|
-
privacyClass: 'public',
|
|
414
|
-
agentId: 'agent-orchestrator',
|
|
415
|
-
build: ({ traceId, executionId, execSpanId }) => {
|
|
416
|
-
const query = record(ctx, {
|
|
417
|
-
semanticName: 'retrieval.query',
|
|
418
|
-
traceId, spanId: ctx.spanId(), parentSpanId: execSpanId, executionId,
|
|
419
|
-
agentId: 'agent-orchestrator',
|
|
420
|
-
importance: 'normal', privacyClass: 'internal',
|
|
421
|
-
payload: {
|
|
422
|
-
lane: 'v2', scope: 'all', indexed: true,
|
|
423
|
-
candidate_count: ctx.tokens(10, 40), returned_count: 2,
|
|
424
|
-
duration_ms: ctx.tokens(5, 20), freshness: {},
|
|
425
|
-
},
|
|
426
|
-
dtMs: 8,
|
|
427
|
-
});
|
|
428
|
-
const resource = { value: ctx.tokens(800, 1200), source: 'PROVIDER' };
|
|
429
|
-
const attempts = modelAttempt(ctx, {
|
|
430
|
-
traceId, parentSpanId: execSpanId, executionId,
|
|
431
|
-
agentId: 'agent-worker',
|
|
432
|
-
attemptIndex: 1, status: 'ok', resource, privacyClass: 'public',
|
|
433
|
-
});
|
|
434
|
-
const end = execEnd(ctx, {
|
|
435
|
-
traceId, spanId: execSpanId, executionId, agentId: 'agent-orchestrator',
|
|
436
|
-
status: 'completed', privacyClass: 'public', extra: { outcome: 'success' },
|
|
437
|
-
});
|
|
438
|
-
return { records: [query, ...attempts, end], attempts, succeeded: true };
|
|
439
|
-
},
|
|
440
|
-
}),
|
|
441
|
-
);
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
metric_version: METRIC_VERSION,
|
|
445
|
-
seed: SEED,
|
|
446
|
-
generated_by: 'scripts/bench/data-foundation.mjs --fixture',
|
|
447
|
-
metric_definitions: {
|
|
448
|
-
tokens_to_success: {
|
|
449
|
-
metric_version: METRIC_VERSION,
|
|
450
|
-
unit: 'tokens',
|
|
451
|
-
definition:
|
|
452
|
-
'Sum of numeric resource.value across all model attempts in the episode; null when no attempt reported a numeric value or the episode did not succeed.',
|
|
453
|
-
},
|
|
454
|
-
correction_observed: {
|
|
455
|
-
metric_version: METRIC_VERSION,
|
|
456
|
-
unit: 'boolean',
|
|
457
|
-
definition:
|
|
458
|
-
'True when a user correction was injected mid-episode and the episode required ≥2 model attempts.',
|
|
459
|
-
},
|
|
460
|
-
},
|
|
461
|
-
traces,
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function writeFixture() {
|
|
466
|
-
const corpus = buildCorpus();
|
|
467
|
-
fs.mkdirSync(path.dirname(corpusPath), { recursive: true });
|
|
468
|
-
fs.writeFileSync(corpusPath, `${JSON.stringify(corpus, null, 2)}\n`);
|
|
469
|
-
process.stdout.write(`wrote ${path.relative(repoRoot, corpusPath)} (seed=${SEED}, traces=${corpus.traces.length})\n`);
|
|
470
|
-
return 0;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function percentile(sorted, p) {
|
|
474
|
-
if (sorted.length === 0) return 0;
|
|
475
|
-
const idx = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
|
|
476
|
-
return sorted[Math.max(0, idx)];
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// --recorder: emit the whole corpus through createRecorder into a temp
|
|
480
|
-
// segment root, measure per-emit latency distribution + flush outcome, then
|
|
481
|
-
// read the store back to prove the round-trip. Exit 0 on success, 1 when the
|
|
482
|
-
// pipeline loses or corrupts records.
|
|
483
|
-
async function runRecorderBench() {
|
|
484
|
-
const { createRecorder } = await import('../../src/core/observability/emit/recorder.js');
|
|
485
|
-
const { readSegments } = await import('../../src/core/observability/segments/readSegments.js');
|
|
486
|
-
|
|
487
|
-
const corpus = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
|
|
488
|
-
const inputs = corpus.traces.flatMap((t) => t.records);
|
|
489
|
-
|
|
490
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ukit-bench-rec-'));
|
|
491
|
-
try {
|
|
492
|
-
const recorder = createRecorder({
|
|
493
|
-
root,
|
|
494
|
-
config: { observability: { stage: 'default' } },
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
const latenciesMs = [];
|
|
498
|
-
let accepted = 0;
|
|
499
|
-
let dropped = 0;
|
|
500
|
-
for (const record of inputs) {
|
|
501
|
-
const t0 = process.hrtime.bigint();
|
|
502
|
-
const r = recorder.emit(record);
|
|
503
|
-
latenciesMs.push(Number(process.hrtime.bigint() - t0) / 1e6);
|
|
504
|
-
if (r.status === 'accepted') accepted += 1;
|
|
505
|
-
else dropped += 1;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const flushStart = process.hrtime.bigint();
|
|
509
|
-
const flush = await recorder.flush({ deadlineMs: 60_000 });
|
|
510
|
-
const flushMs = Number(process.hrtime.bigint() - flushStart) / 1e6;
|
|
511
|
-
|
|
512
|
-
let readBack = 0;
|
|
513
|
-
const it = readSegments(root);
|
|
514
|
-
for await (const _record of it) readBack += 1;
|
|
515
|
-
|
|
516
|
-
latenciesMs.sort((a, b) => a - b);
|
|
517
|
-
const summary = {
|
|
518
|
-
metric_version: METRIC_VERSION,
|
|
519
|
-
bench: 'recorder',
|
|
520
|
-
records: inputs.length,
|
|
521
|
-
accepted,
|
|
522
|
-
dropped,
|
|
523
|
-
written: flush.written,
|
|
524
|
-
read_back: readBack,
|
|
525
|
-
flush_status: flush.status,
|
|
526
|
-
flush_ms: Number(flushMs.toFixed(3)),
|
|
527
|
-
emit_latency_ms: {
|
|
528
|
-
p50: Number(percentile(latenciesMs, 50).toFixed(4)),
|
|
529
|
-
p95: Number(percentile(latenciesMs, 95).toFixed(4)),
|
|
530
|
-
p99: Number(percentile(latenciesMs, 99).toFixed(4)),
|
|
531
|
-
max: Number((latenciesMs[latenciesMs.length - 1] || 0).toFixed(4)),
|
|
532
|
-
},
|
|
533
|
-
health: recorder.health(),
|
|
534
|
-
};
|
|
535
|
-
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
536
|
-
const okRun = flush.status === 'ok' && readBack === accepted && dropped === 0;
|
|
537
|
-
return okRun ? 0 : 1;
|
|
538
|
-
} finally {
|
|
539
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// --analytics: emit the corpus through createRecorder into a temp segment
|
|
544
|
-
// root, rebuild the derived index from retained facts, and print the
|
|
545
|
-
// per-trace summaries + coverage denominators. Exit 0 when the rebuild is
|
|
546
|
-
// complete and equivalent to direct summarization of the corpus records.
|
|
547
|
-
async function runAnalyticsBench() {
|
|
548
|
-
const { createRecorder } = await import('../../src/core/observability/emit/recorder.js');
|
|
549
|
-
const { rebuildIndex } = await import('../../src/core/observability/analytics/rebuild.js');
|
|
550
|
-
const { summarizeTrace } = await import('../../src/core/observability/analytics/summary.js');
|
|
551
|
-
|
|
552
|
-
const corpus = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
|
|
553
|
-
const inputs = corpus.traces.flatMap((t) => t.records);
|
|
554
|
-
|
|
555
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ukit-bench-analytics-'));
|
|
556
|
-
try {
|
|
557
|
-
const recorder = createRecorder({
|
|
558
|
-
root,
|
|
559
|
-
config: { observability: { stage: 'default' } },
|
|
560
|
-
});
|
|
561
|
-
for (const record of inputs) recorder.emit(record);
|
|
562
|
-
const flush = await recorder.flush({ deadlineMs: 60_000 });
|
|
563
|
-
|
|
564
|
-
const started = process.hrtime.bigint();
|
|
565
|
-
const index = await rebuildIndex(root);
|
|
566
|
-
const rebuildMs = Number(process.hrtime.bigint() - started) / 1e6;
|
|
567
|
-
|
|
568
|
-
// Equivalence: rebuild-from-retained-facts must reproduce the same
|
|
569
|
-
// summaries as direct analysis of the corpus records (sanitization only
|
|
570
|
-
// rewrites sensitive free-text, never metric fields).
|
|
571
|
-
const direct = corpus.traces
|
|
572
|
-
.map((t) => summarizeTrace(t.records))
|
|
573
|
-
.sort((a, b) => a.trace_id.localeCompare(b.trace_id));
|
|
574
|
-
const equivalent = JSON.stringify(index.summaries) === JSON.stringify(direct);
|
|
575
|
-
|
|
576
|
-
const report = {
|
|
577
|
-
metric_version: METRIC_VERSION,
|
|
578
|
-
bench: 'analytics',
|
|
579
|
-
records: inputs.length,
|
|
580
|
-
flush_status: flush.status,
|
|
581
|
-
rebuild_ms: Number(rebuildMs.toFixed(3)),
|
|
582
|
-
equivalent_to_direct: equivalent,
|
|
583
|
-
coverage: index.coverage,
|
|
584
|
-
summaries: index.summaries,
|
|
585
|
-
};
|
|
586
|
-
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
587
|
-
const okRun = flush.status === 'ok' && index.ok && equivalent
|
|
588
|
-
&& index.summaries.length === corpus.traces.length;
|
|
589
|
-
return okRun ? 0 : 1;
|
|
590
|
-
} finally {
|
|
591
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// --evaluate: replay the golden case set under every pre-registered policy,
|
|
596
|
-
// print each Scorecard plus the paired comparison (fixed-seed bootstrap
|
|
597
|
-
// variance). Exit 0 when no cohort verdict is 'fail'; 1 on fail verdict or
|
|
598
|
-
// an unloadable corpus. Evaluation is offline — no model calls, no writes.
|
|
599
|
-
async function runEvaluateBench() {
|
|
600
|
-
const { evaluateVariant, compareScorecards } = await import(
|
|
601
|
-
'../../src/core/observability/evaluation/scorecard.js'
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
const goldenPath = path.join(repoRoot, 'tests/fixtures/observability/golden/cases.json');
|
|
605
|
-
let corpus;
|
|
606
|
-
try {
|
|
607
|
-
corpus = JSON.parse(fs.readFileSync(goldenPath, 'utf8'));
|
|
608
|
-
} catch (err) {
|
|
609
|
-
process.stderr.write(`--evaluate: cannot load ${goldenPath}: ${err && err.message}\n`);
|
|
610
|
-
return 1;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const policies = Object.keys(
|
|
614
|
-
(corpus.pre_registered && corpus.pre_registered.policies) || {},
|
|
615
|
-
).sort();
|
|
616
|
-
if (policies.length === 0) {
|
|
617
|
-
process.stderr.write('--evaluate: corpus declares no pre_registered.policies\n');
|
|
618
|
-
return 1;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const scorecards = {};
|
|
622
|
-
for (const policyId of policies) {
|
|
623
|
-
scorecards[policyId] = evaluateVariant(corpus, policyId);
|
|
624
|
-
}
|
|
625
|
-
const comparison = policies.length >= 2
|
|
626
|
-
? compareScorecards(scorecards[policies[0]], scorecards[policies[1]], { seed: SEED })
|
|
627
|
-
: null;
|
|
628
|
-
|
|
629
|
-
const report = {
|
|
630
|
-
metric_version: METRIC_VERSION,
|
|
631
|
-
bench: 'evaluate',
|
|
632
|
-
seed: SEED,
|
|
633
|
-
corpus: path.relative(repoRoot, goldenPath),
|
|
634
|
-
policies,
|
|
635
|
-
scorecards,
|
|
636
|
-
comparison,
|
|
637
|
-
};
|
|
638
|
-
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
639
|
-
const failed = Object.values(scorecards).some((sc) => sc.verdict === 'fail');
|
|
640
|
-
return failed ? 1 : 0;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// --perturb: summarize the corpus, inject seeded synthetic mutations via
|
|
644
|
-
// mutateSummaries, then measure detection through the real
|
|
645
|
-
// computeFingerprint/detectOpportunities path. Exit 0 when every injected
|
|
646
|
-
// mutation is detected; 1 when missed[] is non-empty or the corpus is
|
|
647
|
-
// unloadable. Deterministic at SEED; evaluation is offline — no model calls.
|
|
648
|
-
export async function runPerturb({ quiet = false } = {}) {
|
|
649
|
-
const { summarizeTrace } = await import('../../src/core/observability/analytics/summary.js');
|
|
650
|
-
const { mutateSummaries, detectPerturbations } = await import(
|
|
651
|
-
'../../src/core/observability/evaluation/perturbation.js'
|
|
652
|
-
);
|
|
653
|
-
|
|
654
|
-
let corpus;
|
|
655
|
-
try {
|
|
656
|
-
corpus = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
|
|
657
|
-
} catch (err) {
|
|
658
|
-
process.stderr.write(`--perturb: cannot load ${corpusPath}: ${err && err.message}\n`);
|
|
659
|
-
return { code: 1, report: null };
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const baseline = corpus.traces.map((t) => summarizeTrace(t.records));
|
|
663
|
-
const { summaries: mutated, manifest } = mutateSummaries(baseline, { seed: SEED });
|
|
664
|
-
const outcome = detectPerturbations(mutated, baseline, manifest);
|
|
665
|
-
|
|
666
|
-
const report = {
|
|
667
|
-
metric_version: METRIC_VERSION,
|
|
668
|
-
bench: 'perturb',
|
|
669
|
-
seed: SEED,
|
|
670
|
-
corpus: path.relative(repoRoot, corpusPath),
|
|
671
|
-
injected: outcome.injected,
|
|
672
|
-
detected: outcome.detected,
|
|
673
|
-
missed: outcome.missed,
|
|
674
|
-
manifest,
|
|
675
|
-
};
|
|
676
|
-
if (!quiet) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
677
|
-
return { code: outcome.missed.length > 0 ? 1 : 0, report };
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// --anomalies: replay every golden case under each pre-registered policy,
|
|
681
|
-
// summarize each materialized run, and report per-case anomaly detection
|
|
682
|
-
// counts keyed by detector kind. The detector is imported defensively:
|
|
683
|
-
// while src/core/observability/analytics/anomalies.js (TASK-011) does not
|
|
684
|
-
// exist the report degrades to `detector: 'unavailable'` — never a stub.
|
|
685
|
-
// When a detector IS available, per-case declarations in
|
|
686
|
-
// `expect.anomalies.expected_kinds` act as a regression gate: unmet or
|
|
687
|
-
// over-fired kinds exit 1. Evaluation is offline — no model calls.
|
|
688
|
-
async function loadAnomalyDetector() {
|
|
689
|
-
try {
|
|
690
|
-
const mod = await import('../../src/core/observability/analytics/anomalies.js');
|
|
691
|
-
return typeof mod.detectAnomalies === 'function' ? mod.detectAnomalies : null;
|
|
692
|
-
} catch {
|
|
693
|
-
return null;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function anomalyKind(record) {
|
|
698
|
-
const kind = record && record.payload && record.payload.kind !== undefined
|
|
699
|
-
? record.payload.kind
|
|
700
|
-
: record && record.kind;
|
|
701
|
-
return typeof kind === 'string' && kind.length > 0 ? kind : 'unknown';
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
async function runAnomaliesBench() {
|
|
705
|
-
const { replayCase } = await import('../../src/core/observability/evaluation/replay.js');
|
|
706
|
-
const { summarizeTrace } = await import('../../src/core/observability/analytics/summary.js');
|
|
707
|
-
const detectAnomalies = await loadAnomalyDetector();
|
|
708
|
-
|
|
709
|
-
const goldenPath = path.join(repoRoot, 'tests/fixtures/observability/golden/cases.json');
|
|
710
|
-
let corpus;
|
|
711
|
-
try {
|
|
712
|
-
corpus = JSON.parse(fs.readFileSync(goldenPath, 'utf8'));
|
|
713
|
-
} catch (err) {
|
|
714
|
-
process.stderr.write(`--anomalies: cannot load ${goldenPath}: ${err && err.message}\n`);
|
|
715
|
-
return 1;
|
|
716
|
-
}
|
|
717
|
-
const policies = Object.keys(
|
|
718
|
-
(corpus.pre_registered && corpus.pre_registered.policies) || {},
|
|
719
|
-
).sort();
|
|
720
|
-
if (policies.length === 0) {
|
|
721
|
-
process.stderr.write('--anomalies: corpus declares no pre_registered.policies\n');
|
|
722
|
-
return 1;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const runs = [];
|
|
726
|
-
for (const goldenCase of corpus.cases || []) {
|
|
727
|
-
const expectedKinds = goldenCase && goldenCase.expect && goldenCase.expect.anomalies
|
|
728
|
-
&& Array.isArray(goldenCase.expect.anomalies.expected_kinds)
|
|
729
|
-
? goldenCase.expect.anomalies.expected_kinds
|
|
730
|
-
: null;
|
|
731
|
-
const run = { case_id: goldenCase && goldenCase.case_id, expected_kinds: expectedKinds, policies: {} };
|
|
732
|
-
for (const policyId of policies) {
|
|
733
|
-
const replay = replayCase(goldenCase, { policyId });
|
|
734
|
-
if (!replay.ok) {
|
|
735
|
-
run.policies[policyId] = { ok: false, reason: replay.reason };
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
const summary = summarizeTrace(replay.records);
|
|
739
|
-
run.policies[policyId] = {
|
|
740
|
-
ok: true,
|
|
741
|
-
trace_id: summary.trace_id,
|
|
742
|
-
summary,
|
|
743
|
-
record_ids: replay.records.map((r) => r.record_id),
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
runs.push(run);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Detector wiring: one call over ALL materialized summaries (detectors are
|
|
750
|
-
// cohort-relative), then attribution back to cases by trace_id or any
|
|
751
|
-
// evidence_ref that resolves to a materialized record_id.
|
|
752
|
-
const summaries = runs.flatMap((run) => policies
|
|
753
|
-
.map((policyId) => run.policies[policyId])
|
|
754
|
-
.filter((entry) => entry && entry.ok)
|
|
755
|
-
.map((entry) => entry.summary));
|
|
756
|
-
|
|
757
|
-
const byCase = new Map(runs.map((run) => [run.case_id, new Map()]));
|
|
758
|
-
const byTrace = new Map();
|
|
759
|
-
const byRecordId = new Map();
|
|
760
|
-
for (const run of runs) {
|
|
761
|
-
for (const policyId of policies) {
|
|
762
|
-
const entry = run.policies[policyId];
|
|
763
|
-
if (!entry || !entry.ok) continue;
|
|
764
|
-
if (typeof entry.trace_id === 'string') byTrace.set(entry.trace_id, [run.case_id, policyId]);
|
|
765
|
-
for (const recordId of entry.record_ids) byRecordId.set(recordId, [run.case_id, policyId]);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
let detection = null;
|
|
770
|
-
if (detectAnomalies) {
|
|
771
|
-
const anomalies = detectAnomalies(summaries) || [];
|
|
772
|
-
const unattributed = [];
|
|
773
|
-
for (const anomaly of anomalies) {
|
|
774
|
-
const refs = anomaly && anomaly.payload && Array.isArray(anomaly.payload.evidence_refs)
|
|
775
|
-
? anomaly.payload.evidence_refs
|
|
776
|
-
: [];
|
|
777
|
-
const traceRef = anomaly && anomaly.payload && typeof anomaly.payload.trace_ref === 'string'
|
|
778
|
-
? anomaly.payload.trace_ref
|
|
779
|
-
: (anomaly && typeof anomaly.trace_id === 'string' ? anomaly.trace_id : null);
|
|
780
|
-
let target = traceRef ? byTrace.get(traceRef) : null;
|
|
781
|
-
if (!target) {
|
|
782
|
-
for (const ref of refs) {
|
|
783
|
-
target = byRecordId.get(ref);
|
|
784
|
-
if (target) break;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
if (!target) { unattributed.push(anomaly); continue; }
|
|
788
|
-
const [caseId, policyId] = target;
|
|
789
|
-
const policyMap = byCase.get(caseId);
|
|
790
|
-
if (!policyMap.has(policyId)) policyMap.set(policyId, {});
|
|
791
|
-
const counts = policyMap.get(policyId);
|
|
792
|
-
const kind = anomalyKind(anomaly);
|
|
793
|
-
counts[kind] = (counts[kind] || 0) + 1;
|
|
794
|
-
}
|
|
795
|
-
detection = { anomalies, unattributed };
|
|
796
|
-
}
|
|
797
|
-
// Per-case signals — the summary fields the detectors consume (TASK-011):
|
|
798
|
-
// attempt/retry counters, cache posture, drops, critical path. Reported
|
|
799
|
-
// per policy run so a detector regression is attributable to a case.
|
|
800
|
-
function signalsOf(entry) {
|
|
801
|
-
const s = entry && entry.summary;
|
|
802
|
-
if (!s) return null;
|
|
803
|
-
return {
|
|
804
|
-
model_attempts: s.retries.model_attempts,
|
|
805
|
-
retries: s.retries.retries,
|
|
806
|
-
failed_spans: s.retries.failed_spans,
|
|
807
|
-
cache_misses: s.cache.misses,
|
|
808
|
-
cache_hits: s.cache.hits,
|
|
809
|
-
hit_rate: s.cache.hit_rate,
|
|
810
|
-
dropped_events: s.drops.events,
|
|
811
|
-
dropped_count: s.drops.dropped_count,
|
|
812
|
-
critical_path_ms: s.critical_path_ms,
|
|
813
|
-
telemetry_complete: s.telemetry_complete,
|
|
814
|
-
spans: s.coverage.spans,
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const casesReport = {};
|
|
819
|
-
for (const run of runs) {
|
|
820
|
-
const signals = {};
|
|
821
|
-
for (const policyId of policies) {
|
|
822
|
-
signals[policyId] = signalsOf(run.policies[policyId]);
|
|
823
|
-
}
|
|
824
|
-
let anomalies = null;
|
|
825
|
-
if (detection) {
|
|
826
|
-
anomalies = 0;
|
|
827
|
-
for (const counts of byCase.get(run.case_id)?.values() || []) {
|
|
828
|
-
for (const n of Object.values(counts)) anomalies += n;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
casesReport[run.case_id] = { signals, anomalies };
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// Regression gate — only meaningful when a real detector ran. A case that
|
|
835
|
-
// declares expect.anomalies.expected_kinds must see those kinds fire; a
|
|
836
|
-
// case declaring an empty list must stay clean.
|
|
837
|
-
const unmet = [];
|
|
838
|
-
if (detection) {
|
|
839
|
-
for (const run of runs) {
|
|
840
|
-
if (!Array.isArray(run.expected_kinds)) continue;
|
|
841
|
-
for (const [policyId, counts] of byCase.get(run.case_id) || []) {
|
|
842
|
-
for (const kind of run.expected_kinds) {
|
|
843
|
-
if (!counts[kind]) {
|
|
844
|
-
unmet.push({ case_id: run.case_id, policy: policyId, kind, expected: '>=1', got: 0 });
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
for (const kind of Object.keys(counts)) {
|
|
848
|
-
if (!run.expected_kinds.includes(kind)) {
|
|
849
|
-
unmet.push({ case_id: run.case_id, policy: policyId, kind, expected: 'absent', got: counts[kind] });
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
unmet.sort((a, b) => String(a.case_id).localeCompare(String(b.case_id))
|
|
855
|
-
|| a.policy.localeCompare(b.policy) || a.kind.localeCompare(b.kind));
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
const report = {
|
|
859
|
-
metric_version: METRIC_VERSION,
|
|
860
|
-
bench: 'anomalies',
|
|
861
|
-
seed: SEED,
|
|
862
|
-
corpus: path.relative(repoRoot, goldenPath),
|
|
863
|
-
policies,
|
|
864
|
-
detector: detectAnomalies ? 'detectAnomalies' : 'unavailable',
|
|
865
|
-
summaries: summaries.length,
|
|
866
|
-
anomalies_total: detection ? detection.anomalies.length : null,
|
|
867
|
-
unattributed: detection ? detection.unattributed.length : null,
|
|
868
|
-
unmet_expectations: detection ? unmet : null,
|
|
869
|
-
cases: casesReport,
|
|
870
|
-
};
|
|
871
|
-
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
872
|
-
if (!detection) return 0; // degraded — nothing to gate on
|
|
873
|
-
return unmet.length > 0 ? 1 : 0;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
const BENCH_FLAGS = Object.freeze([
|
|
877
|
-
'--fixture',
|
|
878
|
-
'--recorder',
|
|
879
|
-
'--analytics',
|
|
880
|
-
'--evaluate',
|
|
881
|
-
'--perturb',
|
|
882
|
-
'--anomalies',
|
|
883
|
-
]);
|
|
884
|
-
|
|
885
|
-
async function runFlag(flag) {
|
|
886
|
-
switch (flag) {
|
|
887
|
-
case '--fixture': return writeFixture();
|
|
888
|
-
case '--recorder': return runRecorderBench();
|
|
889
|
-
case '--analytics': return runAnalyticsBench();
|
|
890
|
-
case '--evaluate': return runEvaluateBench();
|
|
891
|
-
case '--perturb': return runPerturb().then(({ code }) => code);
|
|
892
|
-
case '--anomalies': return runAnomaliesBench();
|
|
893
|
-
default:
|
|
894
|
-
process.stderr.write(`unknown flag: ${flag}\n`);
|
|
895
|
-
return 2;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// Flags run in argument order; the exit code is the first non-zero result.
|
|
900
|
-
// (PLAN §5 verification chains --recorder --analytics --evaluate.)
|
|
901
|
-
async function main(argv) {
|
|
902
|
-
const flags = argv.slice(2).filter((arg) => arg.startsWith('--'));
|
|
903
|
-
if (flags.length === 0) {
|
|
904
|
-
process.stderr.write(
|
|
905
|
-
`usage: node scripts/bench/data-foundation.mjs ${BENCH_FLAGS.join('|')}\n`,
|
|
906
|
-
);
|
|
907
|
-
return 2;
|
|
908
|
-
}
|
|
909
|
-
let firstFailure = 0;
|
|
910
|
-
for (const flag of flags) {
|
|
911
|
-
try {
|
|
912
|
-
const code = await runFlag(flag);
|
|
913
|
-
if (code !== 0 && firstFailure === 0) firstFailure = code;
|
|
914
|
-
} catch (err) {
|
|
915
|
-
process.stderr.write(`${flag} failed: ${err && err.message}\n`);
|
|
916
|
-
if (firstFailure === 0) firstFailure = 1;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
return firstFailure;
|
|
920
|
-
}
|
|
921
|
-
const invokedAs = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
922
|
-
if (invokedAs === fileURLToPath(import.meta.url)) {
|
|
923
|
-
// Set the code then let the loop drain stdout — process.exit() hard-kills
|
|
924
|
-
// pending pipe writes, which truncates reports past the 64 KiB buffer.
|
|
925
|
-
Promise.resolve(main(process.argv)).then((code) => {
|
|
926
|
-
process.exitCode = code;
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
|