@ngockhoale/ukit 3.0.7 → 3.0.9
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 +18 -1
- package/manifests/documentation.yaml +11 -0
- package/package.json +1 -1
- package/scripts/audit/decision-coverage.mjs +29 -2
- package/scripts/bench/data-foundation.mjs +52 -3
- package/scripts/bench/decision-runtime-baseline.mjs +427 -0
- package/scripts/bench/decision-runtime-metrics.mjs +67 -0
- package/scripts/bench/decision-runtime-variant.mjs +626 -0
- package/scripts/bench/memory-ablation.mjs +495 -0
- package/scripts/bench/memory-baseline.mjs +596 -0
- package/scripts/bench/memory-bench.mjs +661 -0
- package/scripts/bench/memory-canary.mjs +321 -0
- package/scripts/bench/memory-corpus.mjs +354 -0
- package/scripts/bench/memory-gate.mjs +389 -0
- package/scripts/bench/memory-metrics.mjs +179 -0
- package/scripts/bench/parallel-agents.mjs +33 -11
- package/scripts/bench/recorder-overhead.mjs +204 -0
- package/scripts/bench/sqlite-spike.mjs +451 -0
- package/scripts/measure-decision-gateway.mjs +306 -0
- package/scripts/perf/audit-perf.mjs +35 -17
- package/src/bug/triageBug.js +4 -3
- package/src/cli/commands/memory.js +357 -63
- package/src/context/detectProjectContext.js +11 -1
- package/src/core/agentRuntime/adapters.js +254 -0
- package/src/core/agentRuntime/artifacts.js +192 -0
- package/src/core/agentRuntime/completionGate.js +176 -0
- package/src/core/agentRuntime/context.js +149 -0
- package/src/core/agentRuntime/contract.js +247 -0
- package/src/core/agentRuntime/diagnostics.js +244 -0
- package/src/core/agentRuntime/evaluation.js +163 -0
- package/src/core/agentRuntime/eventStore.js +404 -0
- package/src/core/agentRuntime/liveness.js +60 -0
- package/src/core/agentRuntime/planCompiler.js +322 -0
- package/src/core/agentRuntime/promotion.js +53 -0
- package/src/core/agentRuntime/qualityComparison.js +112 -0
- package/src/core/agentRuntime/recovery.js +266 -0
- package/src/core/agentRuntime/resourcePolicy.js +78 -0
- package/src/core/agentRuntime/runtimeSupport.js +237 -0
- package/src/core/agentRuntime/supervisor.js +565 -0
- package/src/core/agentRuntime/vmEngine.js +621 -0
- package/src/core/codeintel/analogy.js +3 -2
- package/src/core/experiments/dynamicWorkflow.js +17 -2
- package/src/core/fileOps.js +21 -3
- package/src/core/memory/deltaOverlays.js +75 -30
- package/src/core/memory/learningCandidates.js +93 -48
- package/src/core/memory/memoryFlags.js +83 -0
- package/src/core/memory/memoryFreshness.js +190 -0
- package/src/core/memory/memoryHit.js +144 -0
- package/src/core/memory/migrate.js +69 -189
- package/src/core/memory/migrateMapping.js +232 -0
- package/src/core/memory/mutateMemory.js +323 -0
- package/src/core/memory/policy.js +96 -0
- package/src/core/memory/projectIdentity.js +266 -0
- package/src/core/memory/recordIndex.js +178 -0
- package/src/core/memory/recordStore.js +133 -20
- package/src/core/memory/records.js +144 -6
- package/src/core/memory/retrieval.js +259 -125
- package/src/core/memory/store.js +16 -5
- package/src/core/memory/storeBackup.js +226 -0
- package/src/core/memory/storeV2.js +63 -26
- package/src/core/memory/storeV2Loader.js +30 -12
- package/src/core/memory/userMemory.js +38 -20
- package/src/core/memory/writeClassification.js +161 -0
- package/src/core/memory/writeGuard.js +129 -0
- package/src/core/observability/adapters/hookTelemetryAdapter.js +90 -0
- package/src/core/observability/analytics/cohorts.js +148 -0
- package/src/core/observability/analytics/storeDigest.js +163 -0
- package/src/core/observability/evaluation/experimentPlan.js +95 -0
- package/src/core/observability/evaluation/findings.js +99 -0
- package/src/core/observability/evaluation/optimizationKnowledge.js +10 -1
- package/src/core/observability/evaluation/perturbation.js +273 -0
- package/src/core/observability/evaluation/replay.js +7 -1
- package/src/core/observability/evaluation/scorecard.js +23 -3
- package/src/core/observability/rollout.js +11 -7
- package/src/core/observability/schema/compatibility.js +135 -0
- package/src/core/observability/schema/registry.js +99 -0
- package/src/core/observability/schema/validate.js +7 -0
- package/src/core/observability/support/import.js +53 -9
- package/src/core/observability/support/paths.js +13 -3
- package/src/core/observability/support/projector.js +148 -12
- package/src/core/output/index.js +12 -2
- package/src/core/runtimeConfig.js +83 -0
- package/src/core/runtimePaths.js +3 -0
- package/src/core/sensitiveValueScanner.js +40 -0
- package/src/core/token/index.js +40 -3
- package/src/decision/client.js +37 -13
- package/src/decision/protocol.js +1 -1
- package/src/decision/registry.js +5 -3
- package/src/decision/runtimeDecide.js +242 -0
- package/src/decision/runtimeFilter.js +150 -0
- package/src/decision/runtimeScheduler.js +239 -0
- package/src/index/buildIndex.js +13 -12
- package/src/index/queryIndex.js +35 -14
- package/src/index/relatedTests.js +50 -8
- package/src/index/resolveContext.js +9 -4
- package/src/manifest/selectItems.js +7 -3
- package/src/render/instructionRenderer.js +17 -5
- package/template_project/.claude/ukit/index/lib/index-core.mjs +94 -39
- package/template_project/.claude/ukit/index/route-task.mjs +121 -19
- package/template_project/.claude/ukit/index/unic-decision.mjs +28 -13
- package/template_project/.claude/ukit/runtime/memory-flags.mjs +51 -0
- package/template_project/.claude/ukit/runtime/memory-freshness.mjs +155 -0
- package/template_project/.claude/ukit/runtime/memory-policy.mjs +286 -0
- package/template_project/.claude/ukit/runtime/output-compression.mjs +3 -0
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +145 -14
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// TASK-004 — G3-FR06 decision-gateway cost/latency measurement probe.
|
|
3
|
+
//
|
|
4
|
+
// Probes the configured unic-decision gateway through the real client path
|
|
5
|
+
// (createDecisionClient + resolveGatewayBaseUrl/ApiKey env indirection —
|
|
6
|
+
// NO new credentials) and writes evidence to
|
|
7
|
+
// docs/AI_HANDOFF/inventory/g3-gateway-measurement.{json,md}
|
|
8
|
+
//
|
|
9
|
+
// Rules (PLAN §5 / TASK-004):
|
|
10
|
+
// - cost is provider-reported usage when present, else literal 'unknown';
|
|
11
|
+
// chars/4 estimates are forbidden.
|
|
12
|
+
// - unreachable gateway → availability:'unavailable' + errorClass + zero
|
|
13
|
+
// samples — a recorded outcome, NOT a probe failure.
|
|
14
|
+
// - partial samples → samples:<n> + partial:true; never padded.
|
|
15
|
+
//
|
|
16
|
+
// Usage: node scripts/measure-decision-gateway.mjs [--samples N] [--out dir]
|
|
17
|
+
// Exit codes: 0 = artifact written (any availability), 2 = artifact write failed.
|
|
18
|
+
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { performance } from 'node:perf_hooks';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
import { createDecisionClient } from '../src/decision/client.js';
|
|
26
|
+
import {
|
|
27
|
+
resolveGatewayApiKey,
|
|
28
|
+
resolveGatewayBaseUrl,
|
|
29
|
+
} from '../src/core/gatewayProbe.js';
|
|
30
|
+
|
|
31
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
+
const DEFAULT_SAMPLES = 20;
|
|
33
|
+
const DEFAULT_OUT_DIR = path.join(REPO_ROOT, 'docs/AI_HANDOFF/inventory');
|
|
34
|
+
const PROBE_BATCH_ID = 'g3-gateway-measurement';
|
|
35
|
+
const PROBE_QUESTIONS = [
|
|
36
|
+
{
|
|
37
|
+
decisionKey: 'probe.intent',
|
|
38
|
+
kind: 'choice',
|
|
39
|
+
instruction: 'Pick the intent kind.',
|
|
40
|
+
candidates: ['fix', 'feature', 'docs'],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// ---------- pure helpers (unit-tested) ----------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Nearest-rank percentile over a numeric sample array.
|
|
48
|
+
* @param {number[]} samples latencies in ms (need not be sorted)
|
|
49
|
+
* @param {number} p percentile in (0, 100]
|
|
50
|
+
* @returns {number|null} null for empty input — never fabricated
|
|
51
|
+
*/
|
|
52
|
+
export function percentile(samples, p) {
|
|
53
|
+
const clean = (samples ?? []).filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
|
|
54
|
+
if (clean.length === 0) return null;
|
|
55
|
+
const rank = Math.ceil((p / 100) * clean.length);
|
|
56
|
+
return clean[Math.min(Math.max(rank, 1), clean.length) - 1];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract provider-reported cost/usage from a raw chat-completions JSON body.
|
|
61
|
+
* Returns the provider's `usage` object verbatim when present, else 'unknown'.
|
|
62
|
+
* Never estimates.
|
|
63
|
+
*/
|
|
64
|
+
export function extractCost(parsedBody) {
|
|
65
|
+
const usage = parsedBody?.usage;
|
|
66
|
+
if (usage && typeof usage === 'object' && Object.keys(usage).length > 0) {
|
|
67
|
+
return usage;
|
|
68
|
+
}
|
|
69
|
+
return 'unknown';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the machine-readable measurement artifact.
|
|
74
|
+
* @param {{availability:'ok'|'unavailable', samples:number[], failures:number,
|
|
75
|
+
* attempted:number, checkpoint:string|null, cost:object|'unknown',
|
|
76
|
+
* errorClass?:string|null, endpointSource?:string|null}} probe
|
|
77
|
+
*/
|
|
78
|
+
export function buildMeasurementArtifact(probe, { gitSha, measuredAt } = {}) {
|
|
79
|
+
const latencies = probe?.samples ?? [];
|
|
80
|
+
const artifact = {
|
|
81
|
+
schema: 'g3-gateway-measurement/1',
|
|
82
|
+
availability: probe?.availability === 'ok' ? 'ok' : 'unavailable',
|
|
83
|
+
samples: latencies.length,
|
|
84
|
+
attempted: probe?.attempted ?? latencies.length,
|
|
85
|
+
failures: probe?.failures ?? 0,
|
|
86
|
+
p50Ms: percentile(latencies, 50),
|
|
87
|
+
p95Ms: percentile(latencies, 95),
|
|
88
|
+
minMs: latencies.length ? Math.min(...latencies) : null,
|
|
89
|
+
maxMs: latencies.length ? Math.max(...latencies) : null,
|
|
90
|
+
checkpoint: probe?.checkpoint ?? null,
|
|
91
|
+
cost: probe?.cost ?? 'unknown',
|
|
92
|
+
measuredAt: measuredAt ?? new Date().toISOString(),
|
|
93
|
+
gitSha: gitSha ?? null,
|
|
94
|
+
};
|
|
95
|
+
if (probe?.errorClass) artifact.errorClass = probe.errorClass;
|
|
96
|
+
if (probe?.endpointSource) artifact.endpointSource = probe.endpointSource;
|
|
97
|
+
// Honest sample count: anything below the attempted size is partial, never padded.
|
|
98
|
+
if (artifact.attempted > 0 && artifact.samples < artifact.attempted) {
|
|
99
|
+
artifact.partial = true;
|
|
100
|
+
}
|
|
101
|
+
return artifact;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Render the markdown evidence summary from an artifact object. */
|
|
105
|
+
export function renderMeasurementMarkdown(artifact) {
|
|
106
|
+
const lines = [
|
|
107
|
+
'# G3 Gateway Measurement — unic-decision',
|
|
108
|
+
'',
|
|
109
|
+
`- Measured at: ${artifact.measuredAt}`,
|
|
110
|
+
`- Git SHA: ${artifact.gitSha ?? 'unknown'}`,
|
|
111
|
+
`- Availability: **${artifact.availability}**`,
|
|
112
|
+
`- Checkpoint: ${artifact.checkpoint ?? 'unresolved'}`,
|
|
113
|
+
`- Endpoint source: ${artifact.endpointSource ?? 'none'}`,
|
|
114
|
+
`- Samples: ${artifact.samples} ok / ${artifact.attempted} attempted` +
|
|
115
|
+
(artifact.partial ? ' (**partial**)' : ''),
|
|
116
|
+
`- p50 latency: ${artifact.p50Ms ?? 'n/a'} ms`,
|
|
117
|
+
`- p95 latency: ${artifact.p95Ms ?? 'n/a'} ms`,
|
|
118
|
+
`- min/max: ${artifact.minMs ?? 'n/a'} / ${artifact.maxMs ?? 'n/a'} ms`,
|
|
119
|
+
`- Cost (provider-reported): ${artifact.cost === 'unknown' ? 'unknown' : JSON.stringify(artifact.cost)}`,
|
|
120
|
+
];
|
|
121
|
+
if (artifact.errorClass) lines.push(`- Error class: ${artifact.errorClass}`);
|
|
122
|
+
const interpretation =
|
|
123
|
+
artifact.availability === 'ok'
|
|
124
|
+
? `Gateway reachable; ${artifact.samples} successful decision-batch ` +
|
|
125
|
+
`probes measured over the real client path (createDecisionClient → ` +
|
|
126
|
+
`resolveGatewayBaseUrl/ApiKey). Owner classification: local Lava/JEV ` +
|
|
127
|
+
`deployment, near-zero marginal cost [owner-cited context, not measured].`
|
|
128
|
+
: artifact.errorClass === 'endpoint-rejected'
|
|
129
|
+
? 'Endpoint resolved and answered HTTP, but every decision-batch probe ' +
|
|
130
|
+
'was rejected (400/404/422 → `endpoint-rejected`) — the gateway is ' +
|
|
131
|
+
'reachable yet does not accept the C13 tools-encoded batch on this ' +
|
|
132
|
+
'checkpoint. No latency could be sampled; fallback path absorbs this.'
|
|
133
|
+
: 'Gateway was NOT reachable (or no endpoint configured) during this ' +
|
|
134
|
+
'probe. This is a recorded outcome — the scheduler fallback path ' +
|
|
135
|
+
'absorbs it; re-run the probe when the gateway is expected to be up.';
|
|
136
|
+
lines.push(
|
|
137
|
+
'',
|
|
138
|
+
'## Interpretation',
|
|
139
|
+
'',
|
|
140
|
+
interpretation,
|
|
141
|
+
'',
|
|
142
|
+
'## Explicit unknowns',
|
|
143
|
+
'',
|
|
144
|
+
artifact.cost === 'unknown'
|
|
145
|
+
? '- `cost`: the provider response carried no `usage` block → recorded `unknown` (estimates forbidden).'
|
|
146
|
+
: '- `cost`: recorded verbatim from the provider `usage` block.',
|
|
147
|
+
artifact.checkpoint === null
|
|
148
|
+
? '- `checkpoint`: unresolved (no successful request).'
|
|
149
|
+
: null,
|
|
150
|
+
'',
|
|
151
|
+
);
|
|
152
|
+
return lines.filter((l) => l !== null).join('\n');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function writeMeasurementArtifacts(artifact, outDir) {
|
|
156
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
157
|
+
const jsonPath = path.join(outDir, 'g3-gateway-measurement.json');
|
|
158
|
+
const mdPath = path.join(outDir, 'g3-gateway-measurement.md');
|
|
159
|
+
fs.writeFileSync(jsonPath, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
|
|
160
|
+
fs.writeFileSync(mdPath, renderMeasurementMarkdown(artifact), 'utf8');
|
|
161
|
+
return { jsonPath, mdPath };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------- live probe ----------
|
|
165
|
+
|
|
166
|
+
function gitSha() {
|
|
167
|
+
try {
|
|
168
|
+
return execSync('git rev-parse HEAD', { cwd: REPO_ROOT, encoding: 'utf8' }).trim();
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseArgs(argv) {
|
|
175
|
+
const opts = { samples: DEFAULT_SAMPLES, outDir: DEFAULT_OUT_DIR };
|
|
176
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
177
|
+
if (argv[i] === '--samples') {
|
|
178
|
+
i += 1;
|
|
179
|
+
opts.samples = Number(argv[i]);
|
|
180
|
+
} else if (argv[i] === '--out') {
|
|
181
|
+
i += 1;
|
|
182
|
+
opts.outDir = path.resolve(argv[i]);
|
|
183
|
+
} else if (argv[i] === '--help' || argv[i] === '-h') {
|
|
184
|
+
return { help: true };
|
|
185
|
+
} else {
|
|
186
|
+
throw new Error(`Unknown argument: ${argv[i]}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return opts;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Wrap fetch to capture the last successful response's provider `usage`. */
|
|
193
|
+
function captureCostTransport(baseTransport, sink) {
|
|
194
|
+
return async (url, init) => {
|
|
195
|
+
const res = await baseTransport(url, init);
|
|
196
|
+
if (res?.ok !== false && typeof res?.clone === 'function') {
|
|
197
|
+
try {
|
|
198
|
+
const body = await res.clone().json();
|
|
199
|
+
const cost = extractCost(body);
|
|
200
|
+
if (cost !== 'unknown') sink.cost = cost;
|
|
201
|
+
} catch {
|
|
202
|
+
// clone/parse failure must never break the probe
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return res;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runProbe({ samples }) {
|
|
210
|
+
const projectRoot = REPO_ROOT;
|
|
211
|
+
const env = process.env;
|
|
212
|
+
const base = await resolveGatewayBaseUrl({ projectRoot, env });
|
|
213
|
+
const key = await resolveGatewayApiKey({ projectRoot, env });
|
|
214
|
+
const endpointSource = base?.source ?? null;
|
|
215
|
+
|
|
216
|
+
if (!base?.baseUrl) {
|
|
217
|
+
return {
|
|
218
|
+
availability: 'unavailable',
|
|
219
|
+
errorClass: 'endpoint-unconfigured',
|
|
220
|
+
endpointSource: null,
|
|
221
|
+
samples: [],
|
|
222
|
+
failures: 0,
|
|
223
|
+
attempted: 0,
|
|
224
|
+
checkpoint: null,
|
|
225
|
+
cost: 'unknown',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const sink = { cost: 'unknown' };
|
|
230
|
+
const client = createDecisionClient({
|
|
231
|
+
config: {},
|
|
232
|
+
transport: captureCostTransport(globalThis.fetch.bind(globalThis), sink),
|
|
233
|
+
projectRoot,
|
|
234
|
+
env,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const health = await client.healthProbe();
|
|
238
|
+
const latencies = [];
|
|
239
|
+
let failures = 0;
|
|
240
|
+
let lastErrorClass = null;
|
|
241
|
+
let checkpoint = health?.checkpoint ?? null;
|
|
242
|
+
|
|
243
|
+
if (health.status === 'ok' || health.status === 'unavailable' || health.status === 'timeout') {
|
|
244
|
+
for (let i = 0; i < samples; i += 1) {
|
|
245
|
+
const started = performance.now();
|
|
246
|
+
const result = await client.requestBatch({
|
|
247
|
+
batchId: `${PROBE_BATCH_ID}-${i}`,
|
|
248
|
+
boundary: 'measurement',
|
|
249
|
+
statePacket: { stateVersion: 1, taskClass: 'measurement', probeIndex: i },
|
|
250
|
+
questions: PROBE_QUESTIONS,
|
|
251
|
+
deadlineMs: 10_000,
|
|
252
|
+
});
|
|
253
|
+
const elapsed = performance.now() - started;
|
|
254
|
+
if (result.status === 'ok' || result.status === 'accepted' || result.answers?.length > 0) {
|
|
255
|
+
latencies.push(Math.round(elapsed * 100) / 100);
|
|
256
|
+
checkpoint = checkpoint ?? result.checkpoint ?? null;
|
|
257
|
+
} else {
|
|
258
|
+
failures += 1;
|
|
259
|
+
lastErrorClass = result.fallbackCode ?? result.status;
|
|
260
|
+
checkpoint = checkpoint ?? result.checkpoint ?? null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const available = latencies.length > 0;
|
|
266
|
+
return {
|
|
267
|
+
availability: available ? 'ok' : 'unavailable',
|
|
268
|
+
errorClass: available
|
|
269
|
+
? null
|
|
270
|
+
: (lastErrorClass ?? (health.status === 'ok' ? null : health.status)),
|
|
271
|
+
endpointSource: base.source ?? (key?.source ? `key:${key.source}` : null),
|
|
272
|
+
samples: latencies,
|
|
273
|
+
failures,
|
|
274
|
+
attempted: samples,
|
|
275
|
+
checkpoint,
|
|
276
|
+
cost: sink.cost,
|
|
277
|
+
healthStatus: health.status,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
282
|
+
if (isMain) {
|
|
283
|
+
try {
|
|
284
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
285
|
+
if (opts.help) {
|
|
286
|
+
console.log('Usage: node scripts/measure-decision-gateway.mjs [--samples N] [--out dir]');
|
|
287
|
+
process.exit(0);
|
|
288
|
+
}
|
|
289
|
+
if (!Number.isInteger(opts.samples) || opts.samples < 1) {
|
|
290
|
+
throw new Error('--samples must be a positive integer');
|
|
291
|
+
}
|
|
292
|
+
const probe = await runProbe(opts);
|
|
293
|
+
const artifact = buildMeasurementArtifact(probe, {
|
|
294
|
+
gitSha: gitSha(),
|
|
295
|
+
measuredAt: new Date().toISOString(),
|
|
296
|
+
});
|
|
297
|
+
const { jsonPath, mdPath } = writeMeasurementArtifacts(artifact, opts.outDir);
|
|
298
|
+
console.log(`availability=${artifact.availability} samples=${artifact.samples} p50=${artifact.p50Ms} p95=${artifact.p95Ms} cost=${artifact.cost === 'unknown' ? 'unknown' : 'provider'}`);
|
|
299
|
+
console.log(`wrote ${jsonPath}`);
|
|
300
|
+
console.log(`wrote ${mdPath}`);
|
|
301
|
+
process.exit(0);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.error(`measure-decision-gateway: ${err?.message ?? err}`);
|
|
304
|
+
process.exit(2);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -85,16 +85,26 @@ function parseArgs(argv) {
|
|
|
85
85
|
sinceDays: 14,
|
|
86
86
|
benchIterations: 10,
|
|
87
87
|
};
|
|
88
|
+
// A value-taking flag as the last argv entry (or followed by another flag)
|
|
89
|
+
// would reach path.resolve(undefined) → TypeError; fail with usage instead
|
|
90
|
+
// (same convention as scripts/audit/decision-coverage.mjs).
|
|
91
|
+
const valueFor = (flag, i) => {
|
|
92
|
+
const next = argv[i + 1];
|
|
93
|
+
if (next === undefined || next.startsWith('--')) {
|
|
94
|
+
console.error(`[audit-perf] missing value for ${flag} — expected: node scripts/perf/audit-perf.mjs ${flag} <value>`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
return next;
|
|
98
|
+
};
|
|
88
99
|
for (let i = 0; i < argv.length; i += 1) {
|
|
89
100
|
const arg = argv[i];
|
|
90
|
-
const next = argv[i + 1];
|
|
91
101
|
switch (arg) {
|
|
92
|
-
case '--root': options.root = path.resolve(
|
|
93
|
-
case '--telemetry-dir': options.telemetryDir = path.resolve(
|
|
94
|
-
case '--settings': options.settings = path.resolve(
|
|
95
|
-
case '--out': options.out = path.resolve(
|
|
96
|
-
case '--since': options.sinceDays = Number.parseInt(
|
|
97
|
-
case '--bench-iterations': options.benchIterations = Number.parseInt(
|
|
102
|
+
case '--root': options.root = path.resolve(valueFor(arg, i)); i += 1; break;
|
|
103
|
+
case '--telemetry-dir': options.telemetryDir = path.resolve(valueFor(arg, i)); i += 1; break;
|
|
104
|
+
case '--settings': options.settings = path.resolve(valueFor(arg, i)); i += 1; break;
|
|
105
|
+
case '--out': options.out = path.resolve(valueFor(arg, i)); i += 1; break;
|
|
106
|
+
case '--since': options.sinceDays = Number.parseInt(valueFor(arg, i), 10); i += 1; break;
|
|
107
|
+
case '--bench-iterations': options.benchIterations = Number.parseInt(valueFor(arg, i), 10); i += 1; break;
|
|
98
108
|
default: break;
|
|
99
109
|
}
|
|
100
110
|
}
|
|
@@ -393,11 +403,16 @@ function loadSettings(root, override) {
|
|
|
393
403
|
path.join(root, '.claude', 'settings.json'),
|
|
394
404
|
];
|
|
395
405
|
for (const candidate of candidates) {
|
|
396
|
-
if (fs.existsSync(candidate))
|
|
397
|
-
|
|
406
|
+
if (!fs.existsSync(candidate)) continue;
|
|
407
|
+
try {
|
|
408
|
+
return { file: candidate, settings: JSON.parse(fs.readFileSync(candidate, 'utf8')), error: null };
|
|
409
|
+
} catch (error) {
|
|
410
|
+
// Corrupt settings.json (e.g. unresolved merge markers) must not crash
|
|
411
|
+
// the audit — report the failure and continue with no hook groups.
|
|
412
|
+
return { file: candidate, settings: null, error: `settings.json parse failed: ${error.message}` };
|
|
398
413
|
}
|
|
399
414
|
}
|
|
400
|
-
return { file: null, settings: null };
|
|
415
|
+
return { file: null, settings: null, error: null };
|
|
401
416
|
}
|
|
402
417
|
|
|
403
418
|
function describeGroups(settings) {
|
|
@@ -935,7 +950,7 @@ function renderMeasureDoc(context) {
|
|
|
935
950
|
const {
|
|
936
951
|
generatedAt, root, telemetry, telemetryDetail, perHook, records, events,
|
|
937
952
|
multiplicity, nested, groups, floor, telemetryFinish, hookBench, routerBench,
|
|
938
|
-
additions, findings, settingsFile, iterations, hooksResolvedFrom,
|
|
953
|
+
additions, findings, settingsFile, settingsError, iterations, hooksResolvedFrom,
|
|
939
954
|
telemetryRows, telemetryFiles, appendBench, split,
|
|
940
955
|
} = context;
|
|
941
956
|
const lines = [];
|
|
@@ -958,7 +973,7 @@ function renderMeasureDoc(context) {
|
|
|
958
973
|
lines.push('node --test tests/handoff/c33/perfFindings.test.js');
|
|
959
974
|
lines.push('');
|
|
960
975
|
lines.push(`- root: \`${root}\``);
|
|
961
|
-
lines.push(`- settings.json: \`${settingsFile || 'NOT FOUND'}
|
|
976
|
+
lines.push(`- settings.json: \`${settingsFile || 'NOT FOUND'}\`${settingsError ? ` — **PARSE FAILED** (${settingsError}); hook-group findings degrade to 'settings.json not readable'` : ''}`);
|
|
962
977
|
lines.push(`- hooks resolved from: \`${hooksResolvedFrom || 'NOT FOUND'}\``);
|
|
963
978
|
lines.push(`- telemetry: ${telemetry.available ? `\`${telemetry.dir}\` — ${telemetry.rows.length} rows in ${telemetry.files} files${telemetry.malformed ? ` (${telemetry.malformed} malformed lines skipped)` : ''}` : 'NOT PRESENT — every telemetry-sourced finding is `evidence: "unavailable"`'}`);
|
|
964
979
|
lines.push(`- snapshot provenance: generatedAt \`${generatedAt}\`, telemetryRows: ${telemetryRows}, telemetryFiles: ${telemetryFiles} — hooks append continuously, so a report is a point-in-time view, not a stable dataset`);
|
|
@@ -1100,7 +1115,7 @@ async function main() {
|
|
|
1100
1115
|
const measureFile = path.join(path.dirname(outFile), 'perf-measure.md');
|
|
1101
1116
|
|
|
1102
1117
|
const telemetry = loadTelemetry(telemetryDir);
|
|
1103
|
-
const { file: settingsFile, settings } = loadSettings(root, options.settings);
|
|
1118
|
+
const { file: settingsFile, settings, error: settingsError } = loadSettings(root, options.settings);
|
|
1104
1119
|
const groups = describeGroups(settings);
|
|
1105
1120
|
|
|
1106
1121
|
const perHook = perHookStats(telemetry.rows);
|
|
@@ -1137,7 +1152,7 @@ async function main() {
|
|
|
1137
1152
|
const context = {
|
|
1138
1153
|
telemetry, telemetryDetail, groups, floor, telemetryFinish, hookBench, routerBench,
|
|
1139
1154
|
root,
|
|
1140
|
-
perHook, records, events, multiplicity, nested, additions, settingsFile, split,
|
|
1155
|
+
perHook, records, events, multiplicity, nested, additions, settingsFile, settingsError, split,
|
|
1141
1156
|
iterations: options.benchIterations,
|
|
1142
1157
|
sinceDays: options.sinceDays,
|
|
1143
1158
|
telemetryRows, telemetryFiles, appendBench,
|
|
@@ -1154,7 +1169,7 @@ async function main() {
|
|
|
1154
1169
|
telemetry: telemetry.available
|
|
1155
1170
|
? { dir: telemetryDir, rows: telemetryRows, files: telemetryFiles }
|
|
1156
1171
|
: { dir: telemetryDir, available: false },
|
|
1157
|
-
settings: settingsFile,
|
|
1172
|
+
settings: settingsFile ? { file: settingsFile, error: settingsError } : null,
|
|
1158
1173
|
hooksDir: hooksResolvedFrom,
|
|
1159
1174
|
benchmarkIterations: options.benchIterations,
|
|
1160
1175
|
gitLogWindowDays: options.sinceDays,
|
|
@@ -1169,9 +1184,12 @@ async function main() {
|
|
|
1169
1184
|
process.stdout.write(
|
|
1170
1185
|
`${findings.length} findings → ${path.relative(root, outFile)}`
|
|
1171
1186
|
+ ` (generatedAt ${generatedAt}, telemetry ${telemetry.available ? `${telemetryRows} rows / ${telemetryFiles} files` : 'absent'},`
|
|
1172
|
-
+ ` settings ${settingsFile ? 'found' : 'missing'},`
|
|
1187
|
+
+ ` settings ${settingsError ? 'unparseable' : settingsFile ? 'found' : 'missing'},`
|
|
1173
1188
|
+ ` ${groups.length} groups, ${hookBench.size} hooks benchmarked)\n`,
|
|
1174
1189
|
);
|
|
1175
1190
|
}
|
|
1176
1191
|
|
|
1177
|
-
|
|
1192
|
+
main().catch((error) => {
|
|
1193
|
+
console.error(`[audit-perf] ${error && error.message ? error.message : error}`);
|
|
1194
|
+
process.exit(1);
|
|
1195
|
+
});
|
package/src/bug/triageBug.js
CHANGED
|
@@ -49,9 +49,10 @@ export async function triageBug({ rootDir = process.cwd(), signature } = {}) {
|
|
|
49
49
|
|
|
50
50
|
const recommendedTest = await buildTestCommand(rootDir, recommendedTestFile);
|
|
51
51
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const topAnalogs = topResult ? relatedArtifacts?.analogsMap.get(topResult.filePath) : null;
|
|
53
|
+
const analogFiles = (Array.isArray(topAnalogs) ? topAnalogs : [])
|
|
54
|
+
.filter((analog) => analog && typeof analog.filePath === 'string')
|
|
55
|
+
.slice(0, 2);
|
|
55
56
|
|
|
56
57
|
return {
|
|
57
58
|
signature,
|