@ngockhoale/ukit 3.4.45 → 3.4.47

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.
@@ -1,322 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * decision-coverage.mjs (TASK-004 / M07, SPEC §5 FR-010)
4
- *
5
- * CI coverage guard: every consequential decision point in the fixed scan set
6
- * must be covered by a DECISION_REGISTRY entry or a committed allowlist entry.
7
- *
8
- * Discovery convention (deterministic, no invented semantics):
9
- * (a) an exported function named /^(derive|resolve|select|choose|decide)[A-Z]/
10
- * whose body returns a value drawn from a >=2-member choice set — a
11
- * Set/Object.freeze([...])/object-literal constant, a switch, or a
12
- * ternary over >=2 string literals;
13
- * (b) any site carrying a `// @decision-point <key>` annotation.
14
- * Point key = the annotation key, else `<basename>:<functionName>`.
15
- *
16
- * Allowlist: docs/pstack/decision-coverage-allowlist.json —
17
- * [{"key":"<pointKey>","reason":"deterministic|delegated|out-of-scope","owner":"<file>"}]
18
- *
19
- * Exit 0 when every discovered point is covered; exit 1 printing each
20
- * uncovered key (and each invalid allowlist entry). Reports only — does not
21
- * yet block promotion.
22
- *
23
- * Usage: node scripts/audit/decision-coverage.mjs
24
- * [--root <dir>] [--allowlist <path>] [--extra-scan <file>...]
25
- */
26
-
27
- import fs from 'node:fs';
28
- import path from 'node:path';
29
- import { fileURLToPath } from 'node:url';
30
-
31
- import { DECISION_REGISTRY } from '../../src/decision/registry.js';
32
-
33
- const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
34
- const DEFAULT_ROOT = path.resolve(SCRIPT_DIR, '..', '..');
35
-
36
- // Fixed scan set (FR-010) — relative to the repo root.
37
- const SCAN_FILES = [
38
- 'src/index/taskRouting.js',
39
- 'src/core/executionContracts.js',
40
- 'template_project/.claude/ukit/index/route-task.mjs',
41
- ];
42
- const SCAN_GLOBS = ['src/decision/'];
43
- const DEFAULT_ALLOWLIST = 'docs/pstack/decision-coverage-allowlist.json';
44
-
45
- const DECISION_NAME_RE =
46
- /^export\s+(?:async\s+)?function\s+((?:derive|resolve|select|choose|decide)[A-Z]\w*)\s*\(/gm;
47
- const ANNOTATION_RE = /@decision-point\s+([A-Za-z0-9._-]+)/g;
48
- const CONST_RE =
49
- /(?:^|[;{}\n])\s*(?:export\s+)?const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*(?:new\s+Set\s*\(|Object\.freeze\s*\()?\s*([\[{])/g;
50
- const ALLOWED_REASONS = new Set(['deterministic', 'delegated', 'out-of-scope']);
51
-
52
- // --- tiny lexical scanner (strings/comments aware) ---------------------------
53
-
54
- function skipString(source, i) {
55
- const quote = source[i];
56
- for (let j = i + 1; j < source.length; j += 1) {
57
- if (source[j] === '\\') { j += 1; continue; }
58
- if (source[j] === quote) return j;
59
- }
60
- return source.length;
61
- }
62
-
63
- function skipLineComment(source, i) {
64
- const nl = source.indexOf('\n', i);
65
- return nl === -1 ? source.length : nl;
66
- }
67
-
68
- function skipBlockComment(source, i) {
69
- const end = source.indexOf('*/', i + 2);
70
- return end === -1 ? source.length : end + 1;
71
- }
72
-
73
- // Index of the bracket closing source[openIdx], or -1.
74
- function findMatching(source, openIdx) {
75
- const open = source[openIdx];
76
- const close = open === '{' ? '}' : open === '[' ? ']' : ')';
77
- let depth = 0;
78
- for (let i = openIdx; i < source.length; i += 1) {
79
- const ch = source[i];
80
- if (ch === "'" || ch === '"' || ch === '`') { i = skipString(source, i); continue; }
81
- if (ch === '/' && source[i + 1] === '/') { i = skipLineComment(source, i); continue; }
82
- if (ch === '/' && source[i + 1] === '*') { i = skipBlockComment(source, i); continue; }
83
- if (ch === open) depth += 1;
84
- else if (ch === close) {
85
- depth -= 1;
86
- if (depth === 0) return i;
87
- }
88
- }
89
- return -1;
90
- }
91
-
92
- // Commas at bracket depth 0 inside (start, end) — member count = commas + 1.
93
- function countTopLevelCommas(source, start, end) {
94
- let depth = 0;
95
- let commas = 0;
96
- let sawContent = false;
97
- for (let i = start; i < end; i += 1) {
98
- const ch = source[i];
99
- if (ch === "'" || ch === '"' || ch === '`') { i = skipString(source, i); sawContent = true; continue; }
100
- if (ch === '/' && source[i + 1] === '/') { i = skipLineComment(source, i); continue; }
101
- if (ch === '/' && source[i + 1] === '*') { i = skipBlockComment(source, i); continue; }
102
- if (ch === '{' || ch === '[' || ch === '(') depth += 1;
103
- else if (ch === '}' || ch === ']' || ch === ')') depth -= 1;
104
- else if (ch === ',' && depth === 0) commas += 1;
105
- else if (!/\s/.test(ch)) sawContent = true;
106
- }
107
- return sawContent ? commas + 1 : 0;
108
- }
109
-
110
- // --- discovery ---------------------------------------------------------------
111
-
112
- // Names of >=2-member choice-set constants declared anywhere in the file.
113
- function collectChoiceSets(source) {
114
- const names = new Set();
115
- CONST_RE.lastIndex = 0;
116
- let match;
117
- while ((match = CONST_RE.exec(source)) !== null) {
118
- const openIdx = match.index + match[0].lastIndexOf(match[2]);
119
- const closeIdx = findMatching(source, openIdx);
120
- if (closeIdx === -1) continue;
121
- if (countTopLevelCommas(source, openIdx + 1, closeIdx) >= 2) {
122
- names.add(match[1]);
123
- }
124
- CONST_RE.lastIndex = openIdx + 1;
125
- }
126
- return names;
127
- }
128
-
129
- // Heuristic (a): the body returns from a >=2-member choice set — a switch, a
130
- // ternary over >=2 string literals, or a reference to a choice-set constant.
131
- function isConsequential(body, choiceSetNames) {
132
- if (/\bswitch\s*\(/.test(body)) return true;
133
- if (/\?\s*['"][^'"]*['"]\s*:\s*['"]/.test(body)) return true;
134
- for (const name of choiceSetNames) {
135
- if (new RegExp(`\\b${name}\\b`).test(body)) return true;
136
- }
137
- return false;
138
- }
139
-
140
- function discoverInSource(source, basename) {
141
- const points = [];
142
- const choiceSets = collectChoiceSets(source);
143
-
144
- DECISION_NAME_RE.lastIndex = 0;
145
- let match;
146
- while ((match = DECISION_NAME_RE.exec(source)) !== null) {
147
- const paramsOpen = source.indexOf('(', match.index + match[0].indexOf('('));
148
- const paramsClose = findMatching(source, paramsOpen);
149
- if (paramsClose === -1) continue;
150
- const bodyOpen = source.indexOf('{', paramsClose);
151
- if (bodyOpen === -1) continue;
152
- const bodyClose = findMatching(source, bodyOpen);
153
- if (bodyClose === -1) continue;
154
- const body = source.slice(bodyOpen, bodyClose + 1);
155
- if (isConsequential(body, choiceSets)) {
156
- points.push(`${basename}:${match[1]}`);
157
- }
158
- DECISION_NAME_RE.lastIndex = bodyClose + 1;
159
- }
160
-
161
- ANNOTATION_RE.lastIndex = 0;
162
- while ((match = ANNOTATION_RE.exec(source)) !== null) {
163
- points.push(match[1]);
164
- }
165
- return points;
166
- }
167
-
168
- // --- CLI ---------------------------------------------------------------------
169
-
170
- function parseArgs(argv) {
171
- const args = { root: DEFAULT_ROOT, allowlist: null, extraScan: [] };
172
- const valueFor = (flag, i) => {
173
- const next = argv[i + 1];
174
- if (next === undefined || next.startsWith('--')) {
175
- console.error(`missing value for ${flag}`);
176
- process.exit(2);
177
- }
178
- return next;
179
- };
180
- for (let i = 0; i < argv.length; i += 1) {
181
- if (argv[i] === '--root') { args.root = path.resolve(valueFor('--root', i)); i += 1; }
182
- else if (argv[i] === '--allowlist') { args.allowlist = path.resolve(valueFor('--allowlist', i)); i += 1; }
183
- else if (argv[i] === '--extra-scan') { args.extraScan.push(path.resolve(valueFor('--extra-scan', i))); i += 1; }
184
- else {
185
- console.error(`unknown argument: ${argv[i]}`);
186
- process.exit(2);
187
- }
188
- }
189
- return args;
190
- }
191
-
192
- function scanFiles(root, extraScan) {
193
- const files = SCAN_FILES.map((f) => path.join(root, f));
194
- for (const dir of SCAN_GLOBS) {
195
- const absDir = path.join(root, dir);
196
- if (!fs.existsSync(absDir)) {
197
- return { error: `scan directory missing: ${dir}` };
198
- }
199
- for (const entry of fs.readdirSync(absDir).sort()) {
200
- if (entry.endsWith('.js')) files.push(path.join(absDir, entry));
201
- }
202
- }
203
- for (const extra of extraScan) {
204
- let stat;
205
- try {
206
- stat = fs.statSync(extra);
207
- } catch (error) {
208
- return { error: `extra-scan path is not a file: ${extra} (${error.code ?? error.message})` };
209
- }
210
- if (!stat.isFile()) {
211
- return { error: `extra-scan path is not a file: ${extra}` };
212
- }
213
- }
214
- files.push(...extraScan);
215
- return { files };
216
- }
217
-
218
- function loadAllowlist(allowlistPath) {
219
- if (!fs.existsSync(allowlistPath)) {
220
- return { entries: [], errors: [`allowlist missing: ${allowlistPath}`] };
221
- }
222
- let parsed;
223
- try {
224
- parsed = JSON.parse(fs.readFileSync(allowlistPath, 'utf8'));
225
- } catch (error) {
226
- return { entries: [], errors: [`allowlist is not valid JSON: ${error.message}`] };
227
- }
228
- if (!Array.isArray(parsed)) {
229
- return { entries: [], errors: ['allowlist must be a JSON array'] };
230
- }
231
- const entries = [];
232
- const errors = [];
233
- parsed.forEach((entry, index) => {
234
- const label = `allowlist[${index}]`;
235
- if (!entry || typeof entry !== 'object') {
236
- errors.push(`${label}: entry must be an object`);
237
- return;
238
- }
239
- if (typeof entry.key !== 'string' || entry.key.length === 0) {
240
- errors.push(`${label}: key must be a non-empty string`);
241
- return;
242
- }
243
- if (!ALLOWED_REASONS.has(entry.reason)) {
244
- errors.push(
245
- `${label} (${entry.key}): reason must be one of: ${[...ALLOWED_REASONS].join(', ')}`,
246
- );
247
- return;
248
- }
249
- if (typeof entry.owner !== 'string' || entry.owner.length === 0) {
250
- errors.push(`${label} (${entry.key}): owner must be a non-empty string`);
251
- return;
252
- }
253
- entries.push(entry);
254
- });
255
- return { entries, errors };
256
- }
257
-
258
- function main() {
259
- const args = parseArgs(process.argv.slice(2));
260
- const allowlistPath = args.allowlist ?? path.join(args.root, DEFAULT_ALLOWLIST);
261
-
262
- const { files, error } = scanFiles(args.root, args.extraScan);
263
- if (error) {
264
- console.error(`decision-coverage: ${error}`);
265
- process.exit(1);
266
- }
267
-
268
- const discovered = [];
269
- for (const file of files) {
270
- if (!fs.existsSync(file)) {
271
- console.error(`decision-coverage: scan file missing: ${path.relative(args.root, file)}`);
272
- process.exit(1);
273
- }
274
- let source;
275
- try {
276
- source = fs.readFileSync(file, 'utf8');
277
- } catch (error) {
278
- const rel = path.relative(args.root, file);
279
- console.error(
280
- `decision-coverage: cannot read ${rel}: ${error.code ?? error.message}`,
281
- );
282
- process.exit(1);
283
- }
284
- discovered.push(...discoverInSource(source, path.basename(file)));
285
- }
286
-
287
- const registryKeys = new Set(DECISION_REGISTRY.map((entry) => entry.decisionKey));
288
- const { entries, errors } = loadAllowlist(allowlistPath);
289
- const allowlisted = new Set(entries.map((entry) => entry.key));
290
-
291
- const uncovered = [...new Set(discovered)]
292
- .filter((key) => !registryKeys.has(key) && !allowlisted.has(key))
293
- .sort();
294
-
295
- for (const err of errors) {
296
- console.log(`INVALID-ALLOWLIST ${err}`);
297
- }
298
- for (const key of uncovered) {
299
- console.log(`UNCOVERED ${key}`);
300
- }
301
-
302
- if (errors.length > 0 || uncovered.length > 0) {
303
- console.log(
304
- `decision-coverage: FAIL — ${uncovered.length} uncovered, ${errors.length} invalid allowlist entries`,
305
- );
306
- process.exit(1);
307
- }
308
- const registryCovered = [...new Set(discovered)].filter((k) => registryKeys.has(k)).length;
309
- console.log(
310
- `decision-coverage: ok — ${new Set(discovered).size} decision points covered ` +
311
- `(registry: ${registryCovered}, allowlisted: ${new Set(discovered).size - registryCovered})`,
312
- );
313
- }
314
-
315
- try {
316
- main();
317
- } catch (error) {
318
- console.error(
319
- `decision-coverage: ${error instanceof Error ? error.message : String(error)}`,
320
- );
321
- process.exit(1);
322
- }
@@ -1,107 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { loadCorpus } from './reader.mjs';
5
- import { computeMetrics } from './metrics.mjs';
6
- import { computeDuplicates } from './duplicates.mjs';
7
- import { renderMarkdown } from './render.mjs';
8
-
9
- const MISSING = [
10
- 'Provider/gateway id is not recorded in transcript usage.',
11
- 'Native request latency is unavailable; timestamp gaps are only approximate.',
12
- 'Retry counts are not recorded as a distinct telemetry field.',
13
- 'Per-sub-agent request-shape cost is unavailable.',
14
- 'Price table is absent; currency cost cannot be calculated.',
15
- 'Codex and omp transcripts are not included by this Claude Code reader.',
16
- 'Observability token categories are unavailable.',
17
- ];
18
-
19
- export async function buildReport(root, opts = {}) {
20
- const corpus = await loadCorpus(root, { since: opts.since ?? null, until: opts.until ?? null });
21
- // Wave-1 reader predates the normalized call.depth field; derive it without
22
- // changing that shared module, while honoring depth when it is already present.
23
- const depthByAgent = new Map(corpus.agents.map((agent) => [`${agent.sessionId}\u0000${agent.agentId}`, agent.depth]));
24
- for (const call of corpus.calls) {
25
- if (call.depth == null) call.depth = call.agentId == null ? 0 : (depthByAgent.get(`${call.sessionId}\u0000${call.agentId}`) ?? 1);
26
- }
27
- const metrics = computeMetrics(corpus);
28
- const duplicates = computeDuplicates(corpus);
29
- const headline = {
30
- subShare: metrics.share.subOfInputSide,
31
- subUncachedShare: metrics.share.subOfUncached,
32
- contextAmplification: duplicates.amplification,
33
- duplicateContextRatio: duplicates.duplicateRatio,
34
- avgCallsPerTask: metrics.perTask.avgCalls,
35
- };
36
- return {
37
- schema: 'subagent-cost/v1',
38
- generatedAt: new Date().toISOString(),
39
- root: path.basename(path.resolve(root)),
40
- window: { since: opts.since ?? null, until: opts.until ?? null },
41
- stats: corpus.stats,
42
- headline,
43
- metrics,
44
- duplicates,
45
- missing: [...MISSING],
46
- };
47
- }
48
-
49
- function parseArgs(args) {
50
- const opts = {};
51
- const allowed = new Set(['--root', '--since', '--until', '--json', '--md']);
52
- for (let i = 0; i < args.length; i += 1) {
53
- const flag = args[i];
54
- if (!allowed.has(flag)) throw new Error(`bad flag: ${flag}`);
55
- const value = args[i + 1];
56
- if (!value || value.startsWith('--')) throw new Error(`missing value for ${flag}`);
57
- if (opts[flag]) throw new Error(`duplicate flag: ${flag}`);
58
- opts[flag] = value;
59
- i += 1;
60
- }
61
- if (!opts['--root']) throw new Error('missing required --root');
62
- for (const flag of ['--since', '--until']) {
63
- if (opts[flag] && !Number.isFinite(Date.parse(opts[flag]))) throw new Error(`invalid ISO date for ${flag}: ${opts[flag]}`);
64
- }
65
- if (opts['--since'] && opts['--until'] && Date.parse(opts['--since']) > Date.parse(opts['--until'])) throw new Error('--since must be <= --until');
66
- return opts;
67
- }
68
-
69
- async function main(args) {
70
- let opts;
71
- try {
72
- opts = parseArgs(args);
73
- } catch (error) {
74
- process.stderr.write(`${error.message}\n`);
75
- return 2;
76
- }
77
- let report;
78
- try {
79
- report = await buildReport(opts['--root'], { since: opts['--since'] ?? null, until: opts['--until'] ?? null });
80
- } catch (error) {
81
- process.stderr.write(`${error.message}\n`);
82
- return 2;
83
- }
84
- const markdown = renderMarkdown(report);
85
- try {
86
- if (opts['--json']) {
87
- fs.mkdirSync(path.dirname(path.resolve(opts['--json'])), { recursive: true });
88
- fs.writeFileSync(opts['--json'], `${JSON.stringify(report, null, 2)}\n`);
89
- }
90
- if (opts['--md']) {
91
- fs.mkdirSync(path.dirname(path.resolve(opts['--md'])), { recursive: true });
92
- fs.writeFileSync(opts['--md'], markdown);
93
- }
94
- } catch (error) {
95
- process.stderr.write(`${error.message}\n`);
96
- return 2;
97
- }
98
- process.stdout.write(`subagent-cost: ${report.metrics.totals.main.calls + report.metrics.totals.sub.calls} calls, ${report.stats.files} files, headline subShare=${report.headline.subShare}\n`);
99
- if (opts['--json']) process.stdout.write(`json: ${path.resolve(opts['--json'])}\n`);
100
- if (opts['--md']) process.stdout.write(`md: ${path.resolve(opts['--md'])}\n`);
101
- return 0;
102
- }
103
-
104
- const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
105
- if (invokedPath === fileURLToPath(import.meta.url)) {
106
- process.exitCode = await main(process.argv.slice(2));
107
- }
@@ -1,91 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { realpathSync } from 'node:fs';
3
- import { pathToFileURL } from 'node:url';
4
-
5
- const HEADLINE_KEYS = ['subShare', 'subUncachedShare', 'contextAmplification', 'duplicateContextRatio', 'avgCallsPerTask'];
6
- const MARKER = /<!--\s*HEADLINE\s+(\{[^\n]*\})\s*-->/;
7
-
8
- function parseArgs(args) {
9
- const options = {};
10
- for (let i = 0; i < args.length; i += 1) {
11
- const flag = args[i];
12
- if (!['--doc', '--report'].includes(flag) || !args[i + 1] || args[i + 1].startsWith('--')) {
13
- throw new Error('usage: check-doc.mjs --doc <md> --report <json>');
14
- }
15
- if (options[flag]) throw new Error(`duplicate flag: ${flag}`);
16
- options[flag] = args[++i];
17
- }
18
- if (!options['--doc'] || !options['--report']) throw new Error('usage: check-doc.mjs --doc <md> --report <json>');
19
- return options;
20
- }
21
-
22
- async function checkDoc(docPath, reportPath) {
23
- let doc;
24
- let reportText;
25
- try {
26
- [doc, reportText] = await Promise.all([readFile(docPath, 'utf8'), readFile(reportPath, 'utf8')]);
27
- } catch (error) {
28
- console.error(`check-doc: ${error.message}`);
29
- return 2;
30
- }
31
-
32
- let report;
33
- try {
34
- report = JSON.parse(reportText);
35
- } catch {
36
- console.error('check-doc: report is not valid JSON');
37
- return 2;
38
- }
39
- const marker = doc.match(MARKER);
40
- if (!marker) {
41
- console.error('check-doc: missing HEADLINE marker');
42
- return 2;
43
- }
44
- let embedded;
45
- try {
46
- embedded = JSON.parse(marker[1]);
47
- } catch {
48
- console.error('check-doc: invalid HEADLINE JSON');
49
- return 2;
50
- }
51
-
52
- const mismatches = [];
53
- const headline = report?.headline;
54
- for (const key of HEADLINE_KEYS) {
55
- const actual = embedded[key];
56
- const expected = headline?.[key];
57
- if (actual === null || expected === null) {
58
- if (actual !== expected) mismatches.push(key);
59
- } else if (typeof actual !== 'number' || !Number.isFinite(actual) || typeof expected !== 'number' || !Number.isFinite(expected) || Math.abs(actual - expected) > 0.001 * Math.max(1, Math.abs(actual), Math.abs(expected))) {
60
- mismatches.push(key);
61
- }
62
- }
63
-
64
- for (let section = 1; section <= 17; section += 1) {
65
- if (!new RegExp(`^## ${section}\\.`, 'm').test(doc)) mismatches.push(`## ${section}.`);
66
- }
67
- for (let question = 1; question <= 16; question += 1) {
68
- if (!new RegExp(`\\bQ${question}\\b`).test(doc)) mismatches.push(`Q${question}`);
69
- }
70
-
71
- if (mismatches.length) {
72
- console.error(`check-doc: mismatch: ${[...new Set(mismatches)].join(', ')}`);
73
- return 1;
74
- }
75
- console.log('check-doc: headline and required markers match');
76
- return 0;
77
- }
78
-
79
- async function main() {
80
- let options;
81
- try {
82
- options = parseArgs(process.argv.slice(2));
83
- } catch (error) {
84
- console.error(`check-doc: ${error.message}`);
85
- process.exitCode = 2;
86
- return;
87
- }
88
- process.exitCode = await checkDoc(options['--doc'], options['--report']);
89
- }
90
-
91
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) await main();