@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,427 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// TASK-004 — DR-01b decision-runtime baseline runner (SPEC §5 FR-006, §8, §10).
|
|
3
|
-
//
|
|
4
|
-
// Replays the versioned synthetic corpus (docs/AI_HANDOFF/benchmark/corpus.yaml)
|
|
5
|
-
// through the TASK-003 metric module and writes a schema-valid JSON report:
|
|
6
|
-
// {version, env{sha,package,node,os,arch,fs}, runs,
|
|
7
|
-
// scenarios[{id,verdict,wallMs,wakes,eligible,quality,tokens}],
|
|
8
|
-
// summary{externalWakeRate,qualityScore,p50,p95}}
|
|
9
|
-
//
|
|
10
|
-
// The baseline replays deterministic inline commands only — no model/agent
|
|
11
|
-
// wake is ever made — so `wakes` is measured as 0 and `tokens` is 'UNKNOWN'
|
|
12
|
-
// (no provider data source exists; never 0 or an estimate, metric-spec §5).
|
|
13
|
-
//
|
|
14
|
-
// Exit codes: 0 = ran to completion (verdicts are data), 2 = malformed
|
|
15
|
-
// corpus/args or harness crash. Per-step timeout default 30s.
|
|
16
|
-
|
|
17
|
-
import { spawn, execSync } from 'node:child_process';
|
|
18
|
-
import fs from 'node:fs';
|
|
19
|
-
import os from 'node:os';
|
|
20
|
-
import path from 'node:path';
|
|
21
|
-
import { fileURLToPath } from 'node:url';
|
|
22
|
-
import yaml from 'yaml';
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
UNKNOWN,
|
|
26
|
-
computeExternalWakeRate,
|
|
27
|
-
computeQualityScore,
|
|
28
|
-
summarizeRuns,
|
|
29
|
-
} from './decision-runtime-metrics.mjs';
|
|
30
|
-
|
|
31
|
-
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
32
|
-
const DEFAULT_RUNS = 9;
|
|
33
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
34
|
-
const ORACLE_TYPES = ['exit-code', 'output-match', 'file-exists'];
|
|
35
|
-
|
|
36
|
-
const USAGE = `Usage: node scripts/bench/decision-runtime-baseline.mjs --corpus <file.yaml> --out <file.json> [options]
|
|
37
|
-
|
|
38
|
-
Replays the DR-01b synthetic corpus and writes a schema-valid JSON report
|
|
39
|
-
(SPEC §5 FR-006). Verdicts are data: a failing scenario does not fail the run.
|
|
40
|
-
|
|
41
|
-
Options:
|
|
42
|
-
--corpus <file.yaml> Corpus path (required)
|
|
43
|
-
--out <file.json> Report output path (required)
|
|
44
|
-
--runs <n> Repetitions per scenario (default ${DEFAULT_RUNS})
|
|
45
|
-
--timeout <ms> Per-step timeout (default ${DEFAULT_TIMEOUT_MS})
|
|
46
|
-
--help Show this help
|
|
47
|
-
|
|
48
|
-
Exit codes: 0 = completed, 2 = malformed corpus/args or crash.`;
|
|
49
|
-
|
|
50
|
-
// ---------- args ----------
|
|
51
|
-
|
|
52
|
-
function parseArgs(argv) {
|
|
53
|
-
const opts = { runs: DEFAULT_RUNS, timeoutMs: DEFAULT_TIMEOUT_MS };
|
|
54
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
55
|
-
const arg = argv[i];
|
|
56
|
-
if (arg === '--help' || arg === '-h') return { help: true };
|
|
57
|
-
const takeValue = () => {
|
|
58
|
-
i += 1;
|
|
59
|
-
if (i >= argv.length) throw new Error(`Missing value for ${arg}`);
|
|
60
|
-
return argv[i];
|
|
61
|
-
};
|
|
62
|
-
if (arg === '--corpus') opts.corpus = takeValue();
|
|
63
|
-
else if (arg === '--out') opts.out = takeValue();
|
|
64
|
-
else if (arg === '--runs') opts.runs = Number(takeValue());
|
|
65
|
-
else if (arg === '--timeout') opts.timeoutMs = Number(takeValue());
|
|
66
|
-
else throw new Error(`Unknown argument: ${arg}`);
|
|
67
|
-
}
|
|
68
|
-
return opts;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---------- corpus loading + validation ----------
|
|
72
|
-
|
|
73
|
-
function isNonEmptyString(value) {
|
|
74
|
-
return typeof value === 'string' && value.trim() !== '';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function isNonNegativeInt(value) {
|
|
78
|
-
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Validate a parsed corpus doc; returns problem strings (empty = valid). */
|
|
82
|
-
function validateCorpus(doc) {
|
|
83
|
-
const problems = [];
|
|
84
|
-
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
85
|
-
return ['corpus is not an object'];
|
|
86
|
-
}
|
|
87
|
-
if (doc.version !== 1) problems.push('missing or invalid `version` (must be 1)');
|
|
88
|
-
const scenarios = doc.scenarios;
|
|
89
|
-
if (!Array.isArray(scenarios) || scenarios.length === 0) {
|
|
90
|
-
problems.push('`scenarios` must be a non-empty array');
|
|
91
|
-
return problems;
|
|
92
|
-
}
|
|
93
|
-
const seen = new Set();
|
|
94
|
-
scenarios.forEach((sc, idx) => {
|
|
95
|
-
const where = `scenarios[${idx}]`;
|
|
96
|
-
if (!sc || typeof sc !== 'object') {
|
|
97
|
-
problems.push(`${where}: not an object`);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const label = isNonEmptyString(sc.id) ? `scenario ${sc.id}` : where;
|
|
101
|
-
if (!isNonEmptyString(sc.id)) problems.push(`${where}: missing required field \`id\``);
|
|
102
|
-
else if (seen.has(sc.id)) problems.push(`duplicate scenario id: ${sc.id}`);
|
|
103
|
-
else seen.add(sc.id);
|
|
104
|
-
if (!isNonEmptyString(sc.class)) problems.push(`${label}: missing required field \`class\``);
|
|
105
|
-
if (!isNonEmptyString(sc.description)) problems.push(`${label}: missing required field \`description\``);
|
|
106
|
-
if (!Array.isArray(sc.steps) || sc.steps.length === 0) {
|
|
107
|
-
problems.push(`${label}: \`steps\` must be a non-empty array`);
|
|
108
|
-
} else {
|
|
109
|
-
sc.steps.forEach((step, sIdx) => {
|
|
110
|
-
if (!step || typeof step !== 'object' || !isNonEmptyString(step.run)) {
|
|
111
|
-
problems.push(`${label} steps[${sIdx}]: missing required field \`run\``);
|
|
112
|
-
} else if (step.expectExit !== undefined && !Number.isInteger(step.expectExit)) {
|
|
113
|
-
problems.push(`${label} steps[${sIdx}]: \`expectExit\` must be an integer`);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
const oracle = sc.oracle;
|
|
118
|
-
if (!oracle || typeof oracle !== 'object' || !ORACLE_TYPES.includes(oracle.type)) {
|
|
119
|
-
problems.push(`${label}: \`oracle.type\` must be one of ${ORACLE_TYPES.join('|')}`);
|
|
120
|
-
} else if (oracle.type === 'exit-code' && !Number.isInteger(oracle.expect)) {
|
|
121
|
-
problems.push(`${label}: exit-code oracle requires integer \`expect\``);
|
|
122
|
-
} else if (oracle.type === 'output-match' && !isNonEmptyString(oracle.pattern)) {
|
|
123
|
-
problems.push(`${label}: output-match oracle requires \`pattern\``);
|
|
124
|
-
} else if (oracle.type === 'file-exists' && !isNonEmptyString(oracle.path)) {
|
|
125
|
-
problems.push(`${label}: file-exists oracle requires \`path\``);
|
|
126
|
-
}
|
|
127
|
-
if (!isNonNegativeInt(sc.eligibleTransitions)) {
|
|
128
|
-
problems.push(`${label}: missing required field \`eligibleTransitions\` (non-negative integer)`);
|
|
129
|
-
}
|
|
130
|
-
if (!isNonNegativeInt(sc.expectedExternalWakes)) {
|
|
131
|
-
problems.push(`${label}: missing required field \`expectedExternalWakes\` (non-negative integer)`);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
return problems;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function loadCorpus(corpusPath) {
|
|
138
|
-
if (!fs.existsSync(corpusPath)) {
|
|
139
|
-
throw new Error(`corpus not found: ${corpusPath}`);
|
|
140
|
-
}
|
|
141
|
-
let doc;
|
|
142
|
-
try {
|
|
143
|
-
doc = yaml.parse(fs.readFileSync(corpusPath, 'utf8'));
|
|
144
|
-
} catch (error) {
|
|
145
|
-
throw new Error(`corpus YAML parse error: ${error.message}`);
|
|
146
|
-
}
|
|
147
|
-
const problems = validateCorpus(doc);
|
|
148
|
-
if (problems.length > 0) {
|
|
149
|
-
throw new Error(`malformed corpus:\n - ${problems.join('\n - ')}`);
|
|
150
|
-
}
|
|
151
|
-
return doc;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ---------- step execution ----------
|
|
155
|
-
|
|
156
|
-
const SHELL = process.platform === 'win32'
|
|
157
|
-
? { cmd: 'cmd.exe', args: ['/d', '/s', '/c'] }
|
|
158
|
-
: { cmd: '/bin/sh', args: ['-c'] };
|
|
159
|
-
|
|
160
|
-
/** Run one inline step command; captures output for output-match oracles. */
|
|
161
|
-
function runStep(run, { cwd, timeoutMs }) {
|
|
162
|
-
return new Promise((resolve) => {
|
|
163
|
-
const started = Date.now();
|
|
164
|
-
let timedOut = false;
|
|
165
|
-
let spawnError = null;
|
|
166
|
-
let output = '';
|
|
167
|
-
let child;
|
|
168
|
-
try {
|
|
169
|
-
// Bounded child: `timeout` arms the spawn-level deadline (SIGTERM) and
|
|
170
|
-
// the watchdog timer below escalates to SIGKILL — a wedged step can
|
|
171
|
-
// never hang the runner.
|
|
172
|
-
child = spawn(SHELL.cmd, [...SHELL.args, run], {
|
|
173
|
-
cwd,
|
|
174
|
-
env: process.env,
|
|
175
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
176
|
-
timeout: timeoutMs,
|
|
177
|
-
});
|
|
178
|
-
} catch (error) {
|
|
179
|
-
resolve({ exitCode: -1, durationMs: Date.now() - started, timedOut: false, error: String(error?.message ?? error), output: '' });
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
// Watchdog: escalates the spawn timeout's SIGTERM to SIGKILL so a step
|
|
183
|
-
// that ignores termination still dies at the deadline.
|
|
184
|
-
const timer = setTimeout(() => {
|
|
185
|
-
timedOut = true;
|
|
186
|
-
child.kill('SIGKILL');
|
|
187
|
-
}, timeoutMs);
|
|
188
|
-
child.stdout.on('data', (chunk) => { output += chunk; });
|
|
189
|
-
child.stderr.on('data', (chunk) => { output += chunk; });
|
|
190
|
-
child.on('error', (error) => { spawnError = error; });
|
|
191
|
-
child.on('close', (code) => {
|
|
192
|
-
clearTimeout(timer);
|
|
193
|
-
resolve({
|
|
194
|
-
exitCode: timedOut || code == null ? -1 : code,
|
|
195
|
-
durationMs: Date.now() - started,
|
|
196
|
-
timedOut,
|
|
197
|
-
error: spawnError ? String(spawnError.message ?? spawnError) : null,
|
|
198
|
-
output,
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ---------- oracle ----------
|
|
205
|
-
|
|
206
|
-
function checkOracle(oracle, { lastExit, output, cwd }) {
|
|
207
|
-
switch (oracle.type) {
|
|
208
|
-
case 'exit-code':
|
|
209
|
-
return lastExit === oracle.expect;
|
|
210
|
-
case 'output-match':
|
|
211
|
-
return output.includes(oracle.pattern);
|
|
212
|
-
case 'file-exists':
|
|
213
|
-
return fs.existsSync(path.resolve(cwd, oracle.path));
|
|
214
|
-
default:
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ---------- scenario replay ----------
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Replay one scenario `runs` times. Each run executes steps in order and stops
|
|
223
|
-
* at the first step whose exit code differs from `expectExit` (default 0) or
|
|
224
|
-
* that times out — a crash mid-operation means later declared-eligible
|
|
225
|
-
* transitions are never reached (coverage gap, metric-spec §1).
|
|
226
|
-
*/
|
|
227
|
-
async function replayScenario(scenario, { runs, timeoutMs, workDir }) {
|
|
228
|
-
const durations = [];
|
|
229
|
-
let oraclePasses = 0;
|
|
230
|
-
let forbiddenFailures = 0;
|
|
231
|
-
let reachedTransitions = 0;
|
|
232
|
-
let stepMismatches = 0;
|
|
233
|
-
let stepTimeouts = 0;
|
|
234
|
-
|
|
235
|
-
for (let r = 0; r < runs; r += 1) {
|
|
236
|
-
const runDir = path.join(workDir, `run-${r}`);
|
|
237
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
238
|
-
const started = Date.now();
|
|
239
|
-
let lastExit = -1;
|
|
240
|
-
let output = '';
|
|
241
|
-
let stepsOk = true;
|
|
242
|
-
let reached = 0;
|
|
243
|
-
|
|
244
|
-
for (const step of scenario.steps) {
|
|
245
|
-
const expected = step.expectExit ?? 0;
|
|
246
|
-
const result = await runStep(step.run, { cwd: runDir, timeoutMs });
|
|
247
|
-
lastExit = result.exitCode;
|
|
248
|
-
output += result.output;
|
|
249
|
-
if (result.timedOut) stepTimeouts += 1;
|
|
250
|
-
if (result.exitCode !== expected || result.timedOut || result.error) {
|
|
251
|
-
stepsOk = false;
|
|
252
|
-
stepMismatches += 1;
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
reached += 1;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
durations.push(Date.now() - started);
|
|
259
|
-
reachedTransitions += Math.min(reached, scenario.eligibleTransitions);
|
|
260
|
-
|
|
261
|
-
const oraclePass = checkOracle(scenario.oracle, { lastExit, output, cwd: runDir });
|
|
262
|
-
if (oraclePass) oraclePasses += 1;
|
|
263
|
-
// Forbidden failure (metric-spec §2): every step completed as declared
|
|
264
|
-
// (exit-code success) while the oracle rejects the run — false completion.
|
|
265
|
-
if (stepsOk && !oraclePass) forbiddenFailures += 1;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const quality = computeQualityScore({
|
|
269
|
-
satisfied: oraclePasses,
|
|
270
|
-
eligible: runs,
|
|
271
|
-
forbiddenFailures,
|
|
272
|
-
});
|
|
273
|
-
const { p50, p95 } = summarizeRuns(durations);
|
|
274
|
-
const coverageGap = Math.max(0, scenario.eligibleTransitions * runs - reachedTransitions);
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
entry: {
|
|
278
|
-
id: scenario.id,
|
|
279
|
-
class: scenario.class,
|
|
280
|
-
verdict: quality.verdict,
|
|
281
|
-
wallMs: { n: runs, p50, p95 },
|
|
282
|
-
wakes: 0,
|
|
283
|
-
eligible: reachedTransitions,
|
|
284
|
-
coverageGap,
|
|
285
|
-
quality: {
|
|
286
|
-
score: quality.score,
|
|
287
|
-
satisfied: oraclePasses,
|
|
288
|
-
eligible: runs,
|
|
289
|
-
forbiddenFailures,
|
|
290
|
-
},
|
|
291
|
-
tokens: UNKNOWN,
|
|
292
|
-
stepMismatches,
|
|
293
|
-
stepTimeouts,
|
|
294
|
-
},
|
|
295
|
-
durations,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ---------- env ----------
|
|
300
|
-
|
|
301
|
-
function detectFsType(dir) {
|
|
302
|
-
try {
|
|
303
|
-
if (typeof fs.statfsSync === 'function') {
|
|
304
|
-
const stats = fs.statfsSync(dir);
|
|
305
|
-
if (stats && typeof stats.type === 'number') {
|
|
306
|
-
return `0x${stats.type.toString(16)}`;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
} catch { /* fall through */ }
|
|
310
|
-
return 'local';
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function collectEnv() {
|
|
314
|
-
const env = {
|
|
315
|
-
sha: 'unknown',
|
|
316
|
-
package: 'unknown',
|
|
317
|
-
node: process.version,
|
|
318
|
-
os: process.platform,
|
|
319
|
-
arch: process.arch,
|
|
320
|
-
fs: detectFsType(REPO_ROOT),
|
|
321
|
-
};
|
|
322
|
-
try {
|
|
323
|
-
env.sha = execSync('git rev-parse HEAD', { cwd: REPO_ROOT, encoding: 'utf8', timeout: 10_000 }).trim();
|
|
324
|
-
} catch { /* leave 'unknown' */ }
|
|
325
|
-
try {
|
|
326
|
-
env.package = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')).version ?? 'unknown';
|
|
327
|
-
} catch { /* leave 'unknown' */ }
|
|
328
|
-
return env;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// ---------- main ----------
|
|
332
|
-
|
|
333
|
-
async function main() {
|
|
334
|
-
let opts;
|
|
335
|
-
try {
|
|
336
|
-
opts = parseArgs(process.argv.slice(2));
|
|
337
|
-
} catch (error) {
|
|
338
|
-
console.error(`[decision-runtime-baseline] ${error.message}\n\n${USAGE}`);
|
|
339
|
-
process.exit(2);
|
|
340
|
-
}
|
|
341
|
-
if (opts.help) {
|
|
342
|
-
console.log(USAGE);
|
|
343
|
-
process.exit(0);
|
|
344
|
-
}
|
|
345
|
-
if (!opts.corpus || !opts.out) {
|
|
346
|
-
console.error(`[decision-runtime-baseline] --corpus and --out are required.\n\n${USAGE}`);
|
|
347
|
-
process.exit(2);
|
|
348
|
-
}
|
|
349
|
-
if (!Number.isInteger(opts.runs) || opts.runs < 1) {
|
|
350
|
-
console.error('[decision-runtime-baseline] --runs must be a positive integer.');
|
|
351
|
-
process.exit(2);
|
|
352
|
-
}
|
|
353
|
-
if (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs < 1) {
|
|
354
|
-
console.error('[decision-runtime-baseline] --timeout must be a positive number of ms.');
|
|
355
|
-
process.exit(2);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
let corpus;
|
|
359
|
-
try {
|
|
360
|
-
corpus = loadCorpus(path.resolve(opts.corpus));
|
|
361
|
-
} catch (error) {
|
|
362
|
-
console.error(`[decision-runtime-baseline] ${error.message}`);
|
|
363
|
-
process.exit(2);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ukit-dr-baseline-'));
|
|
367
|
-
const scenarios = [];
|
|
368
|
-
const allDurations = [];
|
|
369
|
-
for (const scenario of corpus.scenarios) {
|
|
370
|
-
const workDir = path.join(tmpRoot, scenario.id);
|
|
371
|
-
fs.mkdirSync(workDir, { recursive: true });
|
|
372
|
-
const { entry, durations } = await replayScenario(scenario, {
|
|
373
|
-
runs: opts.runs,
|
|
374
|
-
timeoutMs: opts.timeoutMs,
|
|
375
|
-
workDir,
|
|
376
|
-
});
|
|
377
|
-
scenarios.push(entry);
|
|
378
|
-
allDurations.push(...durations);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const totals = scenarios.reduce((acc, sc) => ({
|
|
382
|
-
wakes: acc.wakes + sc.wakes,
|
|
383
|
-
eligible: acc.eligible + sc.eligible,
|
|
384
|
-
satisfied: acc.satisfied + sc.quality.satisfied,
|
|
385
|
-
qualityEligible: acc.qualityEligible + sc.quality.eligible,
|
|
386
|
-
forbiddenFailures: acc.forbiddenFailures + sc.quality.forbiddenFailures,
|
|
387
|
-
}), { wakes: 0, eligible: 0, satisfied: 0, qualityEligible: 0, forbiddenFailures: 0 });
|
|
388
|
-
|
|
389
|
-
const wallSummary = summarizeRuns(allDurations);
|
|
390
|
-
const summaryQuality = computeQualityScore({
|
|
391
|
-
satisfied: totals.satisfied,
|
|
392
|
-
eligible: totals.qualityEligible,
|
|
393
|
-
forbiddenFailures: totals.forbiddenFailures,
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
const report = {
|
|
397
|
-
version: 1,
|
|
398
|
-
env: collectEnv(),
|
|
399
|
-
corpus: { path: path.resolve(opts.corpus), version: corpus.version, scenarios: corpus.scenarios.length },
|
|
400
|
-
runs: opts.runs,
|
|
401
|
-
scenarios,
|
|
402
|
-
summary: {
|
|
403
|
-
externalWakeRate: computeExternalWakeRate({
|
|
404
|
-
externalWakes: totals.wakes,
|
|
405
|
-
eligibleTransitions: totals.eligible,
|
|
406
|
-
}),
|
|
407
|
-
qualityScore: summaryQuality.score,
|
|
408
|
-
qualityVerdict: summaryQuality.verdict,
|
|
409
|
-
p50: wallSummary.p50,
|
|
410
|
-
p95: wallSummary.p95,
|
|
411
|
-
},
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
const outPath = path.resolve(opts.out);
|
|
415
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
416
|
-
fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
417
|
-
|
|
418
|
-
console.log(`[decision-runtime-baseline] report written: ${outPath}`);
|
|
419
|
-
console.log(`[decision-runtime-baseline] scenarios=${scenarios.length} runs=${opts.runs} `
|
|
420
|
-
+ `externalWakeRate=${report.summary.externalWakeRate} quality=${report.summary.qualityScore}`);
|
|
421
|
-
process.exit(0);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
main().catch((error) => {
|
|
425
|
-
console.error(`[decision-runtime-baseline] harness crash: ${error?.stack ?? error}`);
|
|
426
|
-
process.exit(2);
|
|
427
|
-
});
|