@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/manifests/documentation.yaml +11 -0
  3. package/package.json +1 -1
  4. package/scripts/audit/decision-coverage.mjs +29 -2
  5. package/scripts/bench/data-foundation.mjs +52 -3
  6. package/scripts/bench/decision-runtime-baseline.mjs +427 -0
  7. package/scripts/bench/decision-runtime-metrics.mjs +67 -0
  8. package/scripts/bench/decision-runtime-variant.mjs +626 -0
  9. package/scripts/bench/memory-ablation.mjs +495 -0
  10. package/scripts/bench/memory-baseline.mjs +596 -0
  11. package/scripts/bench/memory-bench.mjs +661 -0
  12. package/scripts/bench/memory-canary.mjs +321 -0
  13. package/scripts/bench/memory-corpus.mjs +354 -0
  14. package/scripts/bench/memory-gate.mjs +389 -0
  15. package/scripts/bench/memory-metrics.mjs +179 -0
  16. package/scripts/bench/parallel-agents.mjs +33 -11
  17. package/scripts/bench/recorder-overhead.mjs +204 -0
  18. package/scripts/bench/sqlite-spike.mjs +451 -0
  19. package/scripts/measure-decision-gateway.mjs +306 -0
  20. package/scripts/perf/audit-perf.mjs +35 -17
  21. package/src/bug/triageBug.js +4 -3
  22. package/src/cli/commands/memory.js +357 -63
  23. package/src/context/detectProjectContext.js +11 -1
  24. package/src/core/agentRuntime/adapters.js +254 -0
  25. package/src/core/agentRuntime/artifacts.js +192 -0
  26. package/src/core/agentRuntime/completionGate.js +176 -0
  27. package/src/core/agentRuntime/context.js +149 -0
  28. package/src/core/agentRuntime/contract.js +247 -0
  29. package/src/core/agentRuntime/diagnostics.js +244 -0
  30. package/src/core/agentRuntime/evaluation.js +163 -0
  31. package/src/core/agentRuntime/eventStore.js +404 -0
  32. package/src/core/agentRuntime/liveness.js +60 -0
  33. package/src/core/agentRuntime/planCompiler.js +322 -0
  34. package/src/core/agentRuntime/promotion.js +53 -0
  35. package/src/core/agentRuntime/qualityComparison.js +112 -0
  36. package/src/core/agentRuntime/recovery.js +266 -0
  37. package/src/core/agentRuntime/resourcePolicy.js +78 -0
  38. package/src/core/agentRuntime/runtimeSupport.js +237 -0
  39. package/src/core/agentRuntime/supervisor.js +565 -0
  40. package/src/core/agentRuntime/vmEngine.js +621 -0
  41. package/src/core/codeintel/analogy.js +3 -2
  42. package/src/core/experiments/dynamicWorkflow.js +17 -2
  43. package/src/core/fileOps.js +21 -3
  44. package/src/core/memory/deltaOverlays.js +75 -30
  45. package/src/core/memory/learningCandidates.js +93 -48
  46. package/src/core/memory/memoryFlags.js +83 -0
  47. package/src/core/memory/memoryFreshness.js +190 -0
  48. package/src/core/memory/memoryHit.js +144 -0
  49. package/src/core/memory/migrate.js +69 -189
  50. package/src/core/memory/migrateMapping.js +232 -0
  51. package/src/core/memory/mutateMemory.js +323 -0
  52. package/src/core/memory/policy.js +96 -0
  53. package/src/core/memory/projectIdentity.js +266 -0
  54. package/src/core/memory/recordIndex.js +178 -0
  55. package/src/core/memory/recordStore.js +133 -20
  56. package/src/core/memory/records.js +144 -6
  57. package/src/core/memory/retrieval.js +259 -125
  58. package/src/core/memory/store.js +16 -5
  59. package/src/core/memory/storeBackup.js +226 -0
  60. package/src/core/memory/storeV2.js +63 -26
  61. package/src/core/memory/storeV2Loader.js +30 -12
  62. package/src/core/memory/userMemory.js +38 -20
  63. package/src/core/memory/writeClassification.js +161 -0
  64. package/src/core/memory/writeGuard.js +129 -0
  65. package/src/core/observability/adapters/hookTelemetryAdapter.js +90 -0
  66. package/src/core/observability/analytics/cohorts.js +148 -0
  67. package/src/core/observability/analytics/storeDigest.js +163 -0
  68. package/src/core/observability/evaluation/experimentPlan.js +95 -0
  69. package/src/core/observability/evaluation/findings.js +99 -0
  70. package/src/core/observability/evaluation/optimizationKnowledge.js +10 -1
  71. package/src/core/observability/evaluation/perturbation.js +273 -0
  72. package/src/core/observability/evaluation/replay.js +7 -1
  73. package/src/core/observability/evaluation/scorecard.js +23 -3
  74. package/src/core/observability/rollout.js +11 -7
  75. package/src/core/observability/schema/compatibility.js +135 -0
  76. package/src/core/observability/schema/registry.js +99 -0
  77. package/src/core/observability/schema/validate.js +7 -0
  78. package/src/core/observability/support/import.js +53 -9
  79. package/src/core/observability/support/paths.js +13 -3
  80. package/src/core/observability/support/projector.js +148 -12
  81. package/src/core/output/index.js +12 -2
  82. package/src/core/runtimeConfig.js +83 -0
  83. package/src/core/runtimePaths.js +3 -0
  84. package/src/core/sensitiveValueScanner.js +40 -0
  85. package/src/core/token/index.js +40 -3
  86. package/src/decision/client.js +37 -13
  87. package/src/decision/protocol.js +1 -1
  88. package/src/decision/registry.js +5 -3
  89. package/src/decision/runtimeDecide.js +242 -0
  90. package/src/decision/runtimeFilter.js +150 -0
  91. package/src/decision/runtimeScheduler.js +239 -0
  92. package/src/index/buildIndex.js +13 -12
  93. package/src/index/queryIndex.js +35 -14
  94. package/src/index/relatedTests.js +50 -8
  95. package/src/index/resolveContext.js +9 -4
  96. package/src/manifest/selectItems.js +7 -3
  97. package/src/render/instructionRenderer.js +17 -5
  98. package/template_project/.claude/ukit/index/lib/index-core.mjs +94 -39
  99. package/template_project/.claude/ukit/index/route-task.mjs +121 -19
  100. package/template_project/.claude/ukit/index/unic-decision.mjs +28 -13
  101. package/template_project/.claude/ukit/runtime/memory-flags.mjs +51 -0
  102. package/template_project/.claude/ukit/runtime/memory-freshness.mjs +155 -0
  103. package/template_project/.claude/ukit/runtime/memory-policy.mjs +286 -0
  104. package/template_project/.claude/ukit/runtime/output-compression.mjs +3 -0
  105. 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(next); i += 1; break;
93
- case '--telemetry-dir': options.telemetryDir = path.resolve(next); i += 1; break;
94
- case '--settings': options.settings = path.resolve(next); i += 1; break;
95
- case '--out': options.out = path.resolve(next); i += 1; break;
96
- case '--since': options.sinceDays = Number.parseInt(next, 10); i += 1; break;
97
- case '--bench-iterations': options.benchIterations = Number.parseInt(next, 10); i += 1; break;
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
- return { file: candidate, settings: JSON.parse(fs.readFileSync(candidate, 'utf8')) };
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
- await main();
1192
+ main().catch((error) => {
1193
+ console.error(`[audit-perf] ${error && error.message ? error.message : error}`);
1194
+ process.exit(1);
1195
+ });
@@ -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 analogFiles = topResult
53
- ? (relatedArtifacts?.analogsMap.get(topResult.filePath) ?? []).slice(0, 2)
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,