@vantageos/mcp-doctor 0.3.1 → 0.4.0

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/dist/doctor.js CHANGED
@@ -1,13 +1,998 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/doctor.ts
4
- import { readFileSync as readFileSync19, realpathSync } from "fs";
5
- import { dirname as dirname11, join as join21, resolve as resolve2 } from "path";
4
+ import { readFileSync as readFileSync12, realpathSync } from "fs";
5
+ import { dirname as dirname11, join as join19, resolve as resolve2 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
+ // src/scan.ts
9
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
10
+ import { basename, dirname, join, relative, sep } from "path";
11
+ var SKIP_DIR_INFO = {
12
+ node_modules: { reason: "vendored dependencies", authored: false },
13
+ dist: { reason: "build output", authored: false },
14
+ build: { reason: "build output", authored: false },
15
+ ".git": { reason: "version-control metadata", authored: false },
16
+ coverage: { reason: "generated coverage output", authored: false },
17
+ // `authored: true` marks content the target's own authors wrote, as opposed
18
+ // to vendored or generated content. A walk that looks for a site (C-26) may
19
+ // probe an authored directory it skips: a deployable there would otherwise
20
+ // be reported as if it did not exist.
21
+ tests: { reason: "test material", authored: true },
22
+ __tests__: { reason: "test material", authored: true },
23
+ // Test material, not a target's served surface: deliberately-malformed
24
+ // servers and foreign copies live here. Scanning them makes the instrument
25
+ // trip its own fail-closed detectors when pointed at its own tree, and lints
26
+ // inputs that are meant to be broken. A real target's fixtures are equally
27
+ // test inputs, not its product surface.
28
+ fixtures: { reason: "test material (fixtures)", authored: true }
29
+ };
30
+ var SKIP_DIRS = new Set(Object.keys(SKIP_DIR_INFO));
31
+ function recordSkip(tree, dir, root, reason) {
32
+ const path = relative(root, dir).split(sep).join("/");
33
+ const existing = tree.skippedDirs.find((d) => d.path === path);
34
+ if (existing) return existing;
35
+ const entry = {
36
+ path,
37
+ reason: reason ?? SKIP_DIR_INFO[basename(dir)]?.reason ?? "skipped"
38
+ };
39
+ tree.skippedDirs.push(entry);
40
+ return entry;
41
+ }
42
+ function isCommentLine(raw) {
43
+ const t = raw.trim();
44
+ if (t === "") return false;
45
+ return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
46
+ }
47
+ function walk(dir, out, onSkip) {
48
+ let entries;
49
+ try {
50
+ entries = readdirSync(dir);
51
+ } catch {
52
+ return;
53
+ }
54
+ for (const name of entries) {
55
+ const full = join(dir, name);
56
+ let st;
57
+ try {
58
+ st = statSync(full);
59
+ } catch {
60
+ continue;
61
+ }
62
+ if (st.isDirectory()) {
63
+ if (SKIP_DIRS.has(name)) {
64
+ onSkip(full);
65
+ continue;
66
+ }
67
+ walk(full, out, onSkip);
68
+ } else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
69
+ if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
70
+ out.push(full);
71
+ }
72
+ }
73
+ }
74
+ function loadFile(path, root) {
75
+ const raw = readFileSync(path, "utf8");
76
+ const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
77
+ const file = { path, rel: relative(root, path) || path, lines };
78
+ const mask = maskOf(file);
79
+ for (const line of lines) {
80
+ if (line.comment) continue;
81
+ const row = mask[line.n - 1];
82
+ let any = false;
83
+ let allComment = true;
84
+ for (let c = 0; c < line.raw.length; c++) {
85
+ if (line.raw[c] === " " || line.raw[c] === " " || line.raw[c] === "\r") continue;
86
+ any = true;
87
+ if (row[c] !== COM) {
88
+ allComment = false;
89
+ break;
90
+ }
91
+ }
92
+ if (any && allComment) line.comment = true;
93
+ }
94
+ return file;
95
+ }
96
+ function findPackageJson(startDir) {
97
+ let dir = startDir;
98
+ for (let depth = 0; depth < 8; depth++) {
99
+ const candidate = join(dir, "package.json");
100
+ if (existsSync(candidate)) {
101
+ try {
102
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
103
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
104
+ return { path: candidate, json: {}, unreadable: "not a JSON object" };
105
+ }
106
+ return { path: candidate, json: parsed };
107
+ } catch (err) {
108
+ return { path: candidate, json: {}, unreadable: err.message };
109
+ }
110
+ }
111
+ const parent = dirname(dir);
112
+ if (parent === dir) break;
113
+ dir = parent;
114
+ }
115
+ return null;
116
+ }
117
+ function loadTree(root) {
118
+ const st = statSync(root);
119
+ const baseDir = st.isDirectory() ? root : dirname(root);
120
+ const filePaths = [];
121
+ const tree = { root, files: [], packageJson: null, skippedDirs: [] };
122
+ if (st.isDirectory()) walk(root, filePaths, (d) => recordSkip(tree, d, root));
123
+ else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
124
+ tree.files = filePaths.map((p) => loadFile(p, baseDir));
125
+ tree.packageJson = findPackageJson(baseDir);
126
+ return tree;
127
+ }
128
+ var COM = 1;
129
+ var CODE = 2;
130
+ var maskCache = /* @__PURE__ */ new WeakMap();
131
+ function buildCodeMask(file) {
132
+ const mask = [];
133
+ let mode = "code";
134
+ const tplStack = [];
135
+ let braceDepth = 0;
136
+ let prevSig = "";
137
+ let prevWord = "";
138
+ const regexAfterKeyword = /* @__PURE__ */ new Set([
139
+ "case",
140
+ "default",
141
+ "delete",
142
+ "do",
143
+ "else",
144
+ "extends",
145
+ "in",
146
+ "instanceof",
147
+ "new",
148
+ "of",
149
+ "return",
150
+ "throw",
151
+ "typeof",
152
+ "void",
153
+ "yield",
154
+ "await"
155
+ ]);
156
+ const regexAfter = /* @__PURE__ */ new Set([
157
+ "",
158
+ "(",
159
+ ",",
160
+ "=",
161
+ ":",
162
+ "[",
163
+ "!",
164
+ "&",
165
+ "|",
166
+ "?",
167
+ "{",
168
+ "}",
169
+ ";",
170
+ "+",
171
+ "-",
172
+ "*",
173
+ "%",
174
+ "<",
175
+ ">",
176
+ "~",
177
+ "^"
178
+ ]);
179
+ for (const line of file.lines) {
180
+ const s = line.raw;
181
+ const row = new Uint8Array(s.length);
182
+ let i = 0;
183
+ while (i < s.length) {
184
+ const c = s[i];
185
+ const n = s[i + 1];
186
+ if (mode === "block") {
187
+ row[i] = COM;
188
+ if (c === "*" && n === "/") {
189
+ row[i + 1] = COM;
190
+ i += 2;
191
+ mode = "code";
192
+ } else i++;
193
+ continue;
194
+ }
195
+ if (mode === "sq" || mode === "dq") {
196
+ if (c === "\\") i += 2;
197
+ else if (mode === "sq" && c === "'" || mode === "dq" && c === '"') {
198
+ row[i] = CODE;
199
+ i++;
200
+ mode = "code";
201
+ prevSig = c;
202
+ } else i++;
203
+ continue;
204
+ }
205
+ if (mode === "tpl") {
206
+ if (c === "\\") i += 2;
207
+ else if (c === "`") {
208
+ row[i] = CODE;
209
+ i++;
210
+ mode = "code";
211
+ prevSig = c;
212
+ } else if (c === "$" && n === "{") {
213
+ row[i] = CODE;
214
+ row[i + 1] = CODE;
215
+ tplStack.push(braceDepth);
216
+ braceDepth++;
217
+ i += 2;
218
+ mode = "code";
219
+ prevSig = "{";
220
+ } else i++;
221
+ continue;
222
+ }
223
+ if (mode === "re" || mode === "recls") {
224
+ if (c === "\\") i += 2;
225
+ else if (mode === "re" && c === "[") {
226
+ mode = "recls";
227
+ i++;
228
+ } else if (mode === "recls" && c === "]") {
229
+ mode = "re";
230
+ i++;
231
+ } else if (mode === "re" && c === "/") {
232
+ row[i] = CODE;
233
+ i++;
234
+ mode = "code";
235
+ prevSig = "/";
236
+ } else i++;
237
+ continue;
238
+ }
239
+ if (c === "/" && n === "/") {
240
+ row.fill(COM, i);
241
+ break;
242
+ }
243
+ if (c === "/" && n === "*") {
244
+ mode = "block";
245
+ row[i] = COM;
246
+ row[i + 1] = COM;
247
+ i += 2;
248
+ continue;
249
+ }
250
+ row[i] = CODE;
251
+ const wordBefore = prevWord;
252
+ if (c !== " " && c !== " ") prevWord = "";
253
+ if (/[A-Za-z_$]/.test(c)) {
254
+ let j = i + 1;
255
+ while (j < s.length && /[\w$]/.test(s[j])) j++;
256
+ for (let k = i; k < j; k++) row[k] = CODE;
257
+ prevWord = prevSig === "." ? "" : s.slice(i, j);
258
+ prevSig = s[j - 1];
259
+ i = j;
260
+ continue;
261
+ }
262
+ if (c === '"' || c === "'" || c === "`") {
263
+ mode = c === '"' ? "dq" : c === "'" ? "sq" : "tpl";
264
+ i++;
265
+ continue;
266
+ }
267
+ if (c === "/" && (regexAfter.has(prevSig) || regexAfterKeyword.has(wordBefore))) {
268
+ mode = "re";
269
+ i++;
270
+ continue;
271
+ }
272
+ if (c === "{") braceDepth++;
273
+ else if (c === "}") {
274
+ braceDepth--;
275
+ if (tplStack.length > 0 && tplStack[tplStack.length - 1] === braceDepth) {
276
+ tplStack.pop();
277
+ mode = "tpl";
278
+ i++;
279
+ continue;
280
+ }
281
+ }
282
+ if (c !== " " && c !== " ") prevSig = c;
283
+ i++;
284
+ }
285
+ if (mode === "sq" || mode === "dq" || mode === "re" || mode === "recls") mode = "code";
286
+ mask.push(row);
287
+ }
288
+ return mask;
289
+ }
290
+ function maskOf(file) {
291
+ let mask = maskCache.get(file);
292
+ if (!mask) {
293
+ mask = buildCodeMask(file);
294
+ maskCache.set(file, mask);
295
+ }
296
+ return mask;
297
+ }
298
+ function isCodeAt(file, n, col) {
299
+ const mask = maskOf(file);
300
+ return mask[n - 1]?.[col] === CODE;
301
+ }
302
+ function matchInCode(file, line, re) {
303
+ const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
304
+ for (const m of line.raw.matchAll(g)) {
305
+ if (isCodeAt(file, line.n, m.index ?? 0)) return m;
306
+ }
307
+ return null;
308
+ }
309
+ function testInCode(file, line, re) {
310
+ return matchInCode(file, line, re) !== null;
311
+ }
312
+ function countInCode(file, line, ch) {
313
+ let n = 0;
314
+ for (let c = 0; c < line.raw.length; c++) {
315
+ if (line.raw[c] === ch && isCodeAt(file, line.n, c)) n++;
316
+ }
317
+ return n;
318
+ }
319
+
320
+ // src/detectors/couldNotJudge.ts
321
+ import { readFileSync as readFileSync2 } from "fs";
322
+ function couldNotJudge(id, anchor, reason) {
323
+ return {
324
+ id,
325
+ anchor,
326
+ state: "could-not-judge",
327
+ reason,
328
+ verdict: `could not judge: ${reason}`
329
+ };
330
+ }
331
+ function settle(id, decided, unreadable) {
332
+ if (unreadable.length === 0) return decided;
333
+ const named = unreadable.map((u) => `${u.anchor}: ${u.reason}`).join("; ");
334
+ if (decided.state === "non-conforming") {
335
+ return { ...decided, verdict: `${decided.verdict} [also could not read: ${named}]` };
336
+ }
337
+ const judgedRest = decided.state === "could-not-judge" ? ` ${decided.reason ?? decided.verdict}` : ` The readable remainder judged ${decided.state}: ${decided.verdict}`;
338
+ return couldNotJudge(
339
+ id,
340
+ unreadable[0].anchor,
341
+ `${unreadable.length} input(s) present but unreadable \u2014 ${named}.${judgedRest}`
342
+ );
343
+ }
344
+ function readJsonObject(path) {
345
+ let value;
346
+ try {
347
+ value = JSON.parse(readFileSync2(path, "utf8"));
348
+ } catch {
349
+ return null;
350
+ }
351
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
352
+ return value;
353
+ }
354
+ function guardUnreadablePackageJson(inner, opts) {
355
+ return {
356
+ ...inner,
357
+ run(tree) {
358
+ const decided = inner.run(tree);
359
+ const pkg = tree.packageJson;
360
+ if (!pkg?.unreadable) return decided;
361
+ const u = {
362
+ anchor: pkg.path,
363
+ reason: `package.json is present but could not be parsed (${pkg.unreadable})`
364
+ };
365
+ if (opts.authoritative && decided.state !== "could-not-judge") {
366
+ return couldNotJudge(inner.id, u.anchor, `${u.anchor}: ${u.reason}`);
367
+ }
368
+ return settle(inner.id, decided, [u]);
369
+ }
370
+ };
371
+ }
372
+
373
+ // src/detectors/dLocalIdentity.ts
374
+ var CLOUD_IDENTITY_PACKAGE = "@vantageos/cloud-identity";
375
+ var EXEMPT_MARKER = "allow-local-identity:";
376
+ var MIN_JUSTIFICATION = 8;
377
+ var TENANT_NAMES = "orgId|org_id|orgSlug|org_slug|tenantId|tenant_id";
378
+ var TENANT_PROP = new RegExp(`\\.(${TENANT_NAMES})$`, "i");
379
+ var unbracket = (x) => x.replace(/\??\.?\[\s*["'`]([\w$]+)["'`]\s*\]/g, ".$1").replace(/\?/g, "");
380
+ var FOLD_NAMES = "toLowerCase|toUpperCase|toLocaleLowerCase|toLocaleUpperCase|trim|normalize";
381
+ var FOLD_CALL = String.raw`(?:\s*\.\s*(?:${FOLD_NAMES})\s*\([^()]*\))*`;
382
+ var PATH = String.raw`[\w$.?]+(?![\w$])(?<!\.(?:${FOLD_NAMES}))`;
383
+ var OPERAND = String.raw`${PATH}(?:\[\s*["\`][\w$]+["\`]\s*\]\??[\w$.?]*)*${FOLD_CALL}`;
384
+ var NON_OPERAND = /* @__PURE__ */ new Set(["undefined", "null", "true", "false", "NaN"]);
385
+ var isLiteral = (x) => NON_OPERAND.has(x) || /^-?\d/.test(x);
386
+ var unfold = (x) => unbracket(x.replace(new RegExp(`${FOLD_CALL}$`), ""));
387
+ var lastSegment = (x) => x.split(".").pop() ?? "";
388
+ var ACTING_FIELD = /^(createdBy|assignedTo|assignee|from|fromOrchestrator|toOrchestrator|recipient|sender|callerOrchestrator|orchestrator|orchestratorName|actor|actorName|actingAs|claimedBy|callerName|agentName|fromName)$/;
389
+ var ACTING_BARE = /^(createdBy|assignedTo|assignee|fromOrchestrator|toOrchestrator|callerOrchestrator|actingAs|claimedBy)$/;
390
+ var CALLERISH = /^(caller\w*|actor\w*|acting\w*|claimed\w*|\w*Orchestrator|\w*AgentName)$/i;
391
+ var NAME_FIELD = /^(name|agentName|orchestratorName|displayName)$/;
392
+ var isActing = (x) => ACTING_FIELD.test(lastSegment(x)) && (x.includes(".") || ACTING_BARE.test(x)) || x.includes(".") && CALLERISH.test(lastSegment(x));
393
+ var isNameField = (x) => x.includes(".") && NAME_FIELD.test(lastSegment(x));
394
+ var isBareCallerish = (x) => !x.includes(".") && CALLERISH.test(x);
395
+ var comparesTenants = (a, b) => TENANT_PROP.test(a) || TENANT_PROP.test(b);
396
+ var comparesNames = (a, b) => isActing(a) && (isActing(b) || CALLERISH.test(lastSegment(b)) || isNameField(b)) || isActing(b) && (CALLERISH.test(lastSegment(a)) || isNameField(a)) || isBareCallerish(a) && isNameField(b) || isBareCallerish(b) && isNameField(a);
397
+ var comparesIdentity = (a, b) => comparesTenants(a, b) || comparesNames(a, b);
398
+ function operands(x, y) {
399
+ const [a, b] = [unfold(x), unfold(y)];
400
+ return isLiteral(a) || isLiteral(b) ? null : [a, b];
401
+ }
402
+ var CMP = String.raw`(${OPERAND})\s*[!=]==?\s*(${OPERAND})`;
403
+ var OBJECT_IS = String.raw`\bObject\s*\.\s*is\s*\(\s*(${OPERAND})\s*,\s*(${OPERAND})\s*\)`;
404
+ var acceptWith = (pred) => (m) => {
405
+ const ops = operands(m[1], m[2]);
406
+ return ops !== null && pred(ops[0], ops[1]);
407
+ };
408
+ var STRING_LITERAL = String.raw`(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\`[^\`$\\]*\`)`;
409
+ var LITERAL_CONCAT = new RegExp(String.raw`^${STRING_LITERAL}(?:\s*\+\s*${STRING_LITERAL})*$`);
410
+ function staticConstants(file) {
411
+ const out = /* @__PURE__ */ new Set();
412
+ const decl = /^(?:export\s+)?const\s+([\w$]+)\s*(?::[^=]+)?=\s*(.*)$/;
413
+ for (let i = 0; i < file.lines.length; i++) {
414
+ const m = file.lines[i].raw.match(decl);
415
+ if (!m) continue;
416
+ let init = m[2];
417
+ for (let j = i + 1; !/;\s*$/.test(init) && j < Math.min(file.lines.length, i + 40); j++) {
418
+ init += `
419
+ ${file.lines[j].raw}`;
420
+ }
421
+ const body = init.replace(/;\s*$/, "").trim();
422
+ if (LITERAL_CONCAT.test(body)) out.add(m[1]);
423
+ }
424
+ return out;
425
+ }
426
+ function callArgs(text, from) {
427
+ const args = [];
428
+ let depth = 0;
429
+ let start = from;
430
+ for (let i = from; i < text.length; i++) {
431
+ const c = text[i];
432
+ if (c === '"' || c === "'" || c === "`") {
433
+ for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
434
+ continue;
435
+ }
436
+ if (c === "(" || c === "[" || c === "{") depth++;
437
+ else if (c === ")" || c === "]" || c === "}") {
438
+ if (depth === 0) {
439
+ args.push(text.slice(start, i).trim());
440
+ return { args, end: i + 1 };
441
+ }
442
+ depth--;
443
+ } else if (c === "," && depth === 0) {
444
+ args.push(text.slice(start, i).trim());
445
+ start = i + 1;
446
+ }
447
+ }
448
+ return null;
449
+ }
450
+ function isStaticInput(expr, statics) {
451
+ const e = expr.trim();
452
+ if (LITERAL_CONCAT.test(e) || statics.has(e)) return true;
453
+ const wrapped = e.match(/^(?:new\s+TextEncoder\s*\(\s*\)\s*\.\s*encode|Buffer\s*\.\s*from)\s*\(/);
454
+ if (!wrapped) return false;
455
+ const inner = callArgs(e, wrapped[0].length);
456
+ return inner !== null && inner.end === e.length && isStaticInput(inner.args[0], statics);
457
+ }
458
+ function textFrom(ctx, extra = 12) {
459
+ const { file, line, idx } = ctx;
460
+ const tail = file.lines.slice(line.n, line.n + extra).map((l) => l.raw);
461
+ return [line.raw.slice(idx), ...tail].join("\n");
462
+ }
463
+ function blockFrom(text, from) {
464
+ const open = text.indexOf("{", from);
465
+ if (open < 0) return null;
466
+ let depth = 0;
467
+ for (let i = open; i < text.length; i++) {
468
+ const c = text[i];
469
+ if (c === '"' || c === "'" || c === "`") {
470
+ for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
471
+ continue;
472
+ }
473
+ if (c === "{") depth++;
474
+ else if (c === "}" && --depth === 0) return text.slice(open, i + 1);
475
+ }
476
+ return null;
477
+ }
478
+ function switchComparesIdentity(m, ctx) {
479
+ if (!ctx) return false;
480
+ const subject = unfold(m[1]);
481
+ const body = blockFrom(textFrom(ctx, 60), m[0].length);
482
+ if (body === null) return false;
483
+ const label = new RegExp(String.raw`\bcase\s+(${OPERAND})\s*:`, "g");
484
+ for (const c of body.matchAll(label)) {
485
+ const ops = operands(subject, c[1]);
486
+ if (ops !== null && comparesIdentity(ops[0], ops[1])) return true;
487
+ }
488
+ return false;
489
+ }
490
+ function literalHoldsIdentity(m) {
491
+ const arg = unfold(m[2]);
492
+ return m[1].split(",").map((e) => unfold(e.trim())).some((e) => e !== "" && !isLiteral(e) && !isLiteral(arg) && comparesIdentity(e, arg));
493
+ }
494
+ function digestsStaticInput(m, ctx) {
495
+ if (!ctx) return false;
496
+ const text = textFrom(ctx);
497
+ const open = text.indexOf("(");
498
+ if (open < 0) return false;
499
+ const call = callArgs(text, open + 1);
500
+ if (!call) return false;
501
+ if (/^subtle/.test(m[0])) {
502
+ return m[1] === "digest" && call.args.length >= 2 && isStaticInput(call.args[1], ctx.statics);
503
+ }
504
+ if (!/^createHash/.test(m[0])) return false;
505
+ const update = text.slice(call.end).match(/^\s*\.\s*update\s*\(/);
506
+ if (!update) return false;
507
+ const data = callArgs(text, call.end + update[0].length);
508
+ return data !== null && isStaticInput(data.args[0], ctx.statics);
509
+ }
510
+ var RENAMEABLE = [
511
+ "createHash",
512
+ "createHmac",
513
+ "timingSafeEqual",
514
+ "jwtVerify",
515
+ "jwtDecrypt",
516
+ "createRemoteJWKSet",
517
+ "createLocalJWKSet",
518
+ "decodeJwt",
519
+ "compactVerify",
520
+ "verifyToken"
521
+ ];
522
+ function renamedPrimitives(file) {
523
+ const text = file.lines.map((l) => l.raw).join("\n");
524
+ const out = [];
525
+ const named = /import\s+(type\s+)?\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;
526
+ for (const m of text.matchAll(named)) {
527
+ if (m[1] || m[3].startsWith(CLOUD_IDENTITY_PACKAGE)) continue;
528
+ for (const part of m[2].split(",")) {
529
+ const as = part.trim().match(/^([\w$]+)\s+as\s+([\w$]+)$/);
530
+ if (as && RENAMEABLE.includes(as[1])) out.push([as[1], as[2]]);
531
+ }
532
+ }
533
+ const destructured = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:await\s+)?(?:require|import)\s*\(\s*["']([^"']+)["']/g;
534
+ for (const m of text.matchAll(destructured)) {
535
+ if (m[2].startsWith(CLOUD_IDENTITY_PACKAGE)) continue;
536
+ for (const part of m[1].split(",")) {
537
+ const as = part.trim().match(/^([\w$]+)\s*:\s*([\w$]+)$/);
538
+ if (as && RENAMEABLE.includes(as[1])) out.push([as[1], as[2]]);
539
+ }
540
+ }
541
+ return out;
542
+ }
543
+ var escapeRe = (x) => x.replace(/[$]/g, "\\$&");
544
+ function aliasMatchers(file) {
545
+ const out = [];
546
+ for (const [orig, alias] of renamedPrimitives(file)) {
547
+ for (const mt of MATCHERS) {
548
+ if (!mt.re.source.includes(orig)) continue;
549
+ const re = new RegExp(mt.re.source.split(orig).join(escapeRe(alias)), mt.re.flags);
550
+ const { accept } = mt;
551
+ out.push({
552
+ ...mt,
553
+ re,
554
+ // The predicate reads the matched text; give it the original callee name.
555
+ accept: accept ? (m, ctx) => {
556
+ const fake = Object.assign(
557
+ [...m].map((x) => x?.replace(alias, orig)),
558
+ {
559
+ index: m.index,
560
+ input: m.input
561
+ }
562
+ );
563
+ return accept(fake, ctx);
564
+ } : void 0
565
+ });
566
+ }
567
+ }
568
+ return out;
569
+ }
570
+ var MATCHERS = [
571
+ {
572
+ id: "local-jwt-verify",
573
+ category: "resolves-caller",
574
+ re: /\b(jwtVerify|jwtDecrypt|createRemoteJWKSet|createLocalJWKSet|decodeJwt|compactVerify|verifyToken)\s*\(/,
575
+ callee: 1,
576
+ sample: "const { payload } = await jwtVerify(token, jwks);"
577
+ },
578
+ {
579
+ id: "local-jwt-verify",
580
+ category: "resolves-caller",
581
+ re: /\b(jwt|jsonwebtoken)\.(verify|decode)\s*\(/,
582
+ sample: "const claims = jwt.verify(token, key);"
583
+ },
584
+ {
585
+ id: "raw-claim-read",
586
+ category: "resolves-caller",
587
+ re: /\b(payload|claims|decoded)\??\.(org_id|org_slug|org_role|orgId|orgSlug|orgRole)\b/,
588
+ sample: "const orgId = payload.org_id as string;"
589
+ },
590
+ {
591
+ // A populated grant attached locally: a non-empty literal, or a value read
592
+ // off another object (a mapping row). A pass-through copy of the same
593
+ // field (`fromAllowList: ctx.fromAllowList`) grants nothing new.
594
+ id: "local-grant",
595
+ category: "resolves-caller",
596
+ re: /\bfromAllowList\s*:\s*(\[\s*[^\]\s][^\]]*\]|[\w$]+(?:\??\.[\w$]+)+)/,
597
+ accept: (m) => !/\.fromAllowList$/.test(m[1]),
598
+ sample: 'fromAllowList: ["*"],'
599
+ },
600
+ {
601
+ // A role compared to a value. A presence test (`orgRole !== undefined`,
602
+ // forwarding the claim on) asserts nothing and is not a site.
603
+ id: "org-role-assert",
604
+ category: "asserts-membership",
605
+ re: /\b(orgRole|org_role)\s*[!=]==\s*([^\s);&|?:]+)/,
606
+ accept: (m) => !NON_OPERAND.has(m[2]),
607
+ sample: 'if (orgRole === "org:admin") return true;'
608
+ },
609
+ {
610
+ id: "org-role-assert",
611
+ category: "asserts-membership",
612
+ re: /[!=]==\s*["'`]org:|\.has\(\s*\{\s*(role|permission)\s*:/,
613
+ sample: 'if (auth.has({ role: "org:admin" })) return true;'
614
+ },
615
+ {
616
+ // Identifying an agent by NAME: a typed name compared to a roster or an
617
+ // allow-list, by method or by iteration.
618
+ id: "name-allowlist",
619
+ category: "filters-by-scope",
620
+ re: /\b[\w$]*(roster|allow_?list|allowed_?orchestrators)\s*\??\.\s*(includes|indexOf|some|find|findIndex|every|has)\s*\(/i,
621
+ sample: "if (ctx.fromAllowList.includes(from)) return null;"
622
+ },
623
+ {
624
+ id: "name-allowlist",
625
+ category: "filters-by-scope",
626
+ re: /\b(isIn\w*Allow_?list)\s*\(/i,
627
+ callee: 1,
628
+ sample: "if (!isInAllowList(list, name)) return false;"
629
+ },
630
+ {
631
+ id: "name-allowlist",
632
+ category: "filters-by-scope",
633
+ re: /\bof\s+[\w$.?]*(roster|allow_?list|allowed_?orchestrators)\s*\)/i,
634
+ sample: "for (const entry of ctx.fromAllowList) {"
635
+ },
636
+ {
637
+ // A row kept or dropped by comparing its tenant to the caller's: ===, !==, == or !=.
638
+ id: "tenant-row-filter",
639
+ category: "filters-by-scope",
640
+ re: new RegExp(CMP),
641
+ accept: acceptWith(comparesTenants),
642
+ sample: "return row.orgId == ctx.actor.orgSlug;"
643
+ },
644
+ {
645
+ id: "tenant-row-filter",
646
+ category: "filters-by-scope",
647
+ re: new RegExp(OBJECT_IS),
648
+ accept: acceptWith(comparesTenants),
649
+ sample: "return Object.is(row.orgId, ctx.actor.orgSlug);"
650
+ },
651
+ {
652
+ // A tenant passed to a membership test: `callerOrgs.includes(row.orgId)`.
653
+ id: "tenant-row-filter",
654
+ category: "filters-by-scope",
655
+ re: new RegExp(
656
+ String.raw`\.(includes|has|indexOf|some|find|findIndex)\s*\(\s*(${OPERAND})\s*[,)]`
657
+ ),
658
+ accept: (m) => TENANT_PROP.test(unfold(m[2])),
659
+ sample: "return callerOrgs.includes(row.orgId);"
660
+ },
661
+ {
662
+ // Identifying an agent by NAME through a normaliser: either side of a strict
663
+ // comparison is a call to `normalize...Id/Name/Orchestrator/Agent/...`.
664
+ id: "name-compare",
665
+ category: "filters-by-scope",
666
+ re: /\bnormali[sz]e\w*(id|name|orchestrator|agent|label|slug|handle)\w*\s*\([^()]*(?:\([^()]*\)[^()]*)*\)\s*[!=]==|[!=]==\s*normali[sz]e\w*(id|name|orchestrator|agent|label|slug|handle)\w*\s*\(/i,
667
+ sample: "return normalizeOrchestratorId(claimedName) === only;"
668
+ },
669
+ {
670
+ // A typed or argument name compared straight to an allow-list / roster entry.
671
+ id: "name-compare",
672
+ category: "filters-by-scope",
673
+ re: /(?:\b[\w$.?]*(?:roster|allow_?list|allowed_?orchestrators)\s*\??\.?\[[^\]]*\]\s*[!=]==|[!=]==\s*[\w$.?]*(?:roster|allow_?list|allowed_?orchestrators)\s*\??\.?\[)/i,
674
+ sample: "return claimedName === ctx.fromAllowList[0];"
675
+ },
676
+ {
677
+ // Identifying an agent by NAME through its acting-name fields: who made a row
678
+ // against who is asking. ===, !==, == and !=, case-folded or not.
679
+ id: "name-compare",
680
+ category: "filters-by-scope",
681
+ re: new RegExp(CMP),
682
+ accept: acceptWith(comparesNames),
683
+ sample: "return row.assignedTo.toLowerCase() !== a.callerOrchestrator.toLowerCase();"
684
+ },
685
+ {
686
+ id: "name-compare",
687
+ category: "filters-by-scope",
688
+ re: new RegExp(OBJECT_IS),
689
+ accept: acceptWith(comparesNames),
690
+ sample: "return Object.is(row.createdBy, m.from);"
691
+ },
692
+ {
693
+ // `switch (row.createdBy) { case caller: }` is the same compare, spelled as a jump table.
694
+ id: "name-compare",
695
+ category: "filters-by-scope",
696
+ re: new RegExp(String.raw`\bswitch\s*\(\s*(${OPERAND})\s*\)`),
697
+ accept: (m, ctx) => switchComparesIdentity(m, ctx),
698
+ sample: "switch (row.createdBy) { case caller: return true; }"
699
+ },
700
+ {
701
+ // `[caller].includes(row.createdBy)`: a membership test against a literal that holds the caller.
702
+ id: "name-compare",
703
+ category: "filters-by-scope",
704
+ re: new RegExp(
705
+ String.raw`(?<![\w$\])])\[([^\[\]]*)\]\s*\.\s*(?:includes|indexOf)\s*\(\s*(${OPERAND})\s*[,)]`
706
+ ),
707
+ accept: (m) => literalHoldsIdentity(m),
708
+ sample: "return [caller].includes(row.createdBy);"
709
+ },
710
+ {
711
+ id: "name-compare",
712
+ category: "filters-by-scope",
713
+ re: new RegExp(
714
+ String.raw`\bnew\s+Set\s*\(\s*\[([^\[\]]*)\]\s*\)\s*\.\s*has\s*\(\s*(${OPERAND})\s*[,)]`
715
+ ),
716
+ accept: (m) => literalHoldsIdentity(m),
717
+ sample: "return new Set([caller]).has(row.assignedTo);"
718
+ },
719
+ {
720
+ // `a.localeCompare(b) === 0` is an equality test, whichever way round it is written.
721
+ id: "name-compare",
722
+ category: "filters-by-scope",
723
+ re: new RegExp(
724
+ String.raw`(${OPERAND})\s*\.\s*localeCompare\s*\(\s*(${OPERAND})\s*(?:,[^()]*(?:\([^()]*\)[^()]*)*)?\)\s*[!=]==?\s*0(?![\w$.])`
725
+ ),
726
+ accept: acceptWith(comparesIdentity),
727
+ sample: "return row.assignedTo.localeCompare(a.callerOrchestrator) === 0;"
728
+ },
729
+ {
730
+ id: "name-compare",
731
+ category: "filters-by-scope",
732
+ re: new RegExp(
733
+ String.raw`(?<![\w$.])0\s*[!=]==?\s*(${OPERAND})\s*\.\s*localeCompare\s*\(\s*(${OPERAND})\s*[,)]`
734
+ ),
735
+ accept: acceptWith(comparesIdentity),
736
+ sample: "return 0 === row.assignedTo.localeCompare(a.callerOrchestrator);"
737
+ },
738
+ {
739
+ id: "local-secret-compare",
740
+ category: "validates-credential",
741
+ re: /\b(timingSafeEqual)\s*\(/,
742
+ callee: 1,
743
+ sample: "return timingSafeEqual(a, b);"
744
+ },
745
+ {
746
+ // Any digest of a presented credential, whatever the algorithm.
747
+ id: "local-digest",
748
+ category: "validates-credential",
749
+ // A digest over static content (a literal, or a constant made of literals) is
750
+ // not a digest of a credential. HMAC and sign/verify always take a key: a site.
751
+ re: /\bcreateHash\s*\(|\bcreateHmac\s*\(|\bsubtle\s*\.\s*(digest|sign|verify)\s*\(/,
752
+ accept: (m, ctx) => !digestsStaticInput(m, ctx),
753
+ sample: 'const h = await crypto.subtle.digest("SHA-256", data);'
754
+ }
755
+ ];
756
+ function biteSample(mt, text) {
757
+ const m = text.match(mt.re);
758
+ if (m === null) return false;
759
+ if (!mt.accept) return true;
760
+ const line = { n: 1, raw: text, comment: false };
761
+ const file = { path: "sample", rel: "sample", lines: [line] };
762
+ return mt.accept(m, { file, line, idx: m.index ?? 0, statics: /* @__PURE__ */ new Set() });
763
+ }
764
+ function localIdentityPositiveControls() {
765
+ const out = {};
766
+ MATCHERS.forEach((mt, i) => {
767
+ out[`${mt.id}#${i}`] = biteSample(mt, mt.sample);
768
+ });
769
+ const bites = (line) => MATCHERS.some((mt) => biteSample(mt, line));
770
+ out["negative:pass-through-grant"] = !bites("fromAllowList: ctx.fromAllowList,");
771
+ out["negative:delegated-bearer"] = !MATCHERS.some(
772
+ (mt) => mt.re.test("const r = await validateMasterBearer(authHeader, secret);")
773
+ );
774
+ out["negative:role-presence-test"] = !bites(
775
+ "...(ctx.orgRole !== undefined ? { role: ctx.orgRole } : {}),"
776
+ );
777
+ out["negative:undefined-compare"] = !bites("if (row.orgId === undefined) return;");
778
+ out["negative:acting-field-vs-nullish"] = !bites("if (row.assignedTo !== undefined) return;");
779
+ out["negative:acting-field-vs-literal"] = !bites('return t.createdBy === "system";');
780
+ out["negative:unrelated-field-compare"] = !bites("return page.from === page.to;");
781
+ out["negative:switch-on-unrelated-field"] = !bites('switch (row.kind) { case "a": return 1; }');
782
+ out["negative:literal-array-unrelated"] = !bites("return [kind].includes(row.status);");
783
+ out["negative:localecompare-unrelated"] = !bites("return a.name.localeCompare(b.name) === 0;");
784
+ out["negative:object-is-nullish"] = !bites("return Object.is(row.orgId, undefined);");
785
+ return out;
786
+ }
787
+ function cloudIdentityBindings(file) {
788
+ const text = file.lines.map((l) => l.raw).join("\n");
789
+ const names = /* @__PURE__ */ new Set();
790
+ const namespaces = /* @__PURE__ */ new Set();
791
+ const pkg = `@vantageos\\/cloud-identity(?:\\/[^"']*)?`;
792
+ const named = new RegExp(`import\\s+(type\\s+)?\\{([^}]*)\\}\\s*from\\s*["']${pkg}["']`, "g");
793
+ for (const m of text.matchAll(named)) {
794
+ if (m[1]) continue;
795
+ for (const part of m[2].split(",")) {
796
+ const p = part.trim();
797
+ if (!p || p.startsWith("type ")) continue;
798
+ const as = p.match(/^[\w$]+\s+as\s+([\w$]+)$/);
799
+ names.add(as ? as[1] : p);
800
+ }
801
+ }
802
+ const ns = new RegExp(`import\\s+\\*\\s+as\\s+([\\w$]+)\\s+from\\s*["']${pkg}["']`, "g");
803
+ for (const m of text.matchAll(ns)) namespaces.add(m[1]);
804
+ return { names, namespaces };
805
+ }
806
+ function isDelegated(raw, start, callee, b) {
807
+ const before = raw.slice(0, start);
808
+ const qualified = before.match(/([\w$]+)\s*\.\s*$/);
809
+ if (qualified) return b.namespaces.has(qualified[1]);
810
+ return b.names.has(callee);
811
+ }
812
+ function countDelegatedCalls(file, b) {
813
+ if (b.names.size === 0 && b.namespaces.size === 0) return 0;
814
+ let n = 0;
815
+ for (const line of file.lines) {
816
+ if (line.comment) continue;
817
+ for (const m of line.raw.matchAll(/([\w$]+)\s*\(/g)) {
818
+ const idx = m.index ?? 0;
819
+ if (isCodeAt(file, line.n, idx) && isDelegated(line.raw, idx, m[1], b)) n++;
820
+ }
821
+ }
822
+ return n;
823
+ }
824
+ var DECL = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\s*\*?\s*([\w$]+)|(?:const|let|var|class)\s+([\w$]+))/;
825
+ function enclosingDecl(file, n) {
826
+ for (let i = n - 1; i >= 0; i--) {
827
+ const l = file.lines[i];
828
+ if (l.comment) continue;
829
+ const m = l.raw.match(DECL);
830
+ if (m) return { name: m[1] ?? m[2], line: l.n };
831
+ }
832
+ return null;
833
+ }
834
+ function justificationIn(line, file) {
835
+ const at = line.raw.indexOf(EXEMPT_MARKER);
836
+ if (at < 0) return null;
837
+ if (!line.comment && isCodeAt(file, line.n, at)) return null;
838
+ if (!line.comment && !/\/\/|\/\*/.test(line.raw.slice(0, at))) return null;
839
+ return line.raw.slice(at + EXEMPT_MARKER.length).replace(/\*\/\s*$/, "").trim();
840
+ }
841
+ function commentBlockAbove(file, n) {
842
+ const out = [];
843
+ for (let i = n - 2; i >= 0; i--) {
844
+ const l = file.lines[i];
845
+ if (!l.comment) break;
846
+ out.push(l);
847
+ }
848
+ return out;
849
+ }
850
+ function findJustification(file, line, decl) {
851
+ const candidates = [line, ...commentBlockAbove(file, line.n)];
852
+ if (decl) candidates.push(...commentBlockAbove(file, decl.line));
853
+ for (const c of candidates) {
854
+ const j = justificationIn(c, file);
855
+ if (j !== null) return { text: j, valid: j.length >= MIN_JUSTIFICATION };
856
+ }
857
+ return null;
858
+ }
859
+ function unreadableReason(file) {
860
+ if (file.lines.some((l) => l.raw.includes("\0") || l.raw.includes("\uFFFD"))) {
861
+ return "file is not valid UTF-8 text";
862
+ }
863
+ const pairs = [
864
+ ["{", "}"],
865
+ ["(", ")"],
866
+ ["[", "]"]
867
+ ];
868
+ const off = [];
869
+ for (const [open, close] of pairs) {
870
+ let net = 0;
871
+ for (const l of file.lines) net += countInCode(file, l, open) - countInCode(file, l, close);
872
+ if (net !== 0) off.push(`${open}${close} ${net > 0 ? "+" : ""}${net}`);
873
+ }
874
+ return off.length > 0 ? `brackets never balance (${off.join(", ")}) \u2014 the file cannot be read as code` : null;
875
+ }
876
+ function findLocalIdentitySites(tree) {
877
+ const pkg = tree.packageJson;
878
+ const selfExempt = !pkg?.unreadable && pkg?.json.name === CLOUD_IDENTITY_PACKAGE;
879
+ const scan = {
880
+ selfExempt,
881
+ violations: [],
882
+ exempt: [],
883
+ delegatedCalls: 0,
884
+ unreadable: []
885
+ };
886
+ for (const file of tree.files) {
887
+ const bad = unreadableReason(file);
888
+ if (bad) scan.unreadable.push({ anchor: file.rel, reason: bad });
889
+ const bindings = cloudIdentityBindings(file);
890
+ const statics = staticConstants(file);
891
+ const catalog = [...MATCHERS, ...aliasMatchers(file)];
892
+ scan.delegatedCalls += countDelegatedCalls(file, bindings);
893
+ for (const line of file.lines) {
894
+ if (line.comment) continue;
895
+ let hit = null;
896
+ for (const mt of catalog) {
897
+ const g = new RegExp(mt.re.source, `${mt.re.flags.replace("g", "")}g`);
898
+ for (const m of line.raw.matchAll(g)) {
899
+ const idx = m.index ?? 0;
900
+ if (!isCodeAt(file, line.n, idx)) continue;
901
+ if (mt.accept && !mt.accept(m, { file, line, idx, statics })) continue;
902
+ const callee = mt.callee !== void 0 ? m[mt.callee] : void 0;
903
+ if (callee && isDelegated(line.raw, idx, callee, bindings)) continue;
904
+ hit = mt;
905
+ break;
906
+ }
907
+ if (hit) break;
908
+ }
909
+ if (!hit) continue;
910
+ const decl = enclosingDecl(file, line.n);
911
+ const site = {
912
+ file: file.rel,
913
+ line: line.n,
914
+ primitive: hit.id,
915
+ category: hit.category,
916
+ enclosing: decl
917
+ };
918
+ const j = findJustification(file, line, decl);
919
+ if (j?.valid) scan.exempt.push({ ...site, justification: j.text });
920
+ else scan.violations.push(site);
921
+ }
922
+ }
923
+ return scan;
924
+ }
925
+ function describeSite(s) {
926
+ const where = s.enclosing ? ` in ${s.enclosing.name} (declared :${s.enclosing.line})` : "";
927
+ return `${s.file}:${s.line} [${s.category}/${s.primitive}]${where}`;
928
+ }
929
+ function exemptSuffix(exempt) {
930
+ if (exempt.length === 0) return "";
931
+ const rows = exempt.map((e) => `${describeSite(e)} \u2014 "${e.justification}"`);
932
+ return `; EXEMPT (${exempt.length}): ${rows.join("; ")}`;
933
+ }
934
+ function makeLocalIdentity(opts = {}) {
935
+ const ceiling = opts.ceiling ?? 0;
936
+ if (!Number.isInteger(ceiling) || ceiling < 0) {
937
+ throw new Error(`local-identity ceiling must be a non-negative integer, got ${ceiling}`);
938
+ }
939
+ return {
940
+ id: "DLocalIdentity no-local-identity",
941
+ run(tree) {
942
+ const controls = localIdentityPositiveControls();
943
+ const failed = Object.entries(controls).filter(([, ok]) => !ok);
944
+ if (failed.length > 0) {
945
+ throw new Error(
946
+ `DLocalIdentity positive control FAILED: ${failed.map(([k]) => k).join(", ")} \u2014 matcher broken, verdict untrustworthy`
947
+ );
948
+ }
949
+ const id = this.id;
950
+ const s = findLocalIdentitySites(tree);
951
+ if (s.selfExempt) {
952
+ const n = s.violations.length + s.exempt.length;
953
+ return {
954
+ id,
955
+ anchor: tree.packageJson?.path ?? "-",
956
+ state: "present",
957
+ verdict: `EXEMPT: the target is ${CLOUD_IDENTITY_PACKAGE} itself, the one place identity primitives live (${n} primitive site(s) not counted)`
958
+ };
959
+ }
960
+ const listed = s.violations.map(describeSite).join("; ");
961
+ const delegated = `${s.delegatedCalls} call(s) to ${CLOUD_IDENTITY_PACKAGE}`;
962
+ let decided;
963
+ if (s.violations.length > ceiling) {
964
+ const v = s.violations[0];
965
+ decided = {
966
+ id,
967
+ anchor: `${v.file}:${v.line}`,
968
+ state: "non-conforming",
969
+ verdict: `${s.violations.length} local identity primitive(s), ceiling ${ceiling} (positive-control OK): ${listed}${exemptSuffix(s.exempt)} -> delegate to ${CLOUD_IDENTITY_PACKAGE} (validateMasterBearer, verifyClerkSessionToken, isMasterScope, ...) or justify the site with "// ${EXEMPT_MARKER} <reason>"`
970
+ };
971
+ } else if (s.violations.length > 0) {
972
+ const v = s.violations[0];
973
+ decided = {
974
+ id,
975
+ anchor: `${v.file}:${v.line}`,
976
+ state: "present",
977
+ verdict: `${s.violations.length} local identity primitive(s) at or under the ceiling ${ceiling} \u2014 a ratchet, not a clean bill: ${listed}${exemptSuffix(s.exempt)}`
978
+ };
979
+ } else {
980
+ decided = {
981
+ id,
982
+ anchor: s.exempt[0] ? `${s.exempt[0].file}:${s.exempt[0].line}` : "-",
983
+ state: "present",
984
+ verdict: `0 local identity primitives; ${delegated} (positive-control OK)${exemptSuffix(s.exempt)}`
985
+ };
986
+ }
987
+ return settle(id, decided, s.unreadable);
988
+ }
989
+ };
990
+ }
991
+ var localIdentity = makeLocalIdentity();
992
+
8
993
  // src/detectors/c7-artifact-verifiable.ts
9
- import { existsSync, readFileSync } from "fs";
10
- import { join } from "path";
994
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
995
+ import { join as join2 } from "path";
11
996
  var PIN = "a74a042b53c71a095ab88b3073bd47728eeda814";
12
997
  var DOC_FILES = ["README.md", "README.fr.md"];
13
998
  var INSTALL_RE = /\b(?:npm\s+(?:install|i)|npx|pnpm\s+add|yarn\s+add)\s+(@?[\w./-]+)/i;
@@ -46,10 +1031,10 @@ function c7PositiveControls() {
46
1031
  }
47
1032
  function readDocText(root) {
48
1033
  for (const name of DOC_FILES) {
49
- const path = join(root, name);
50
- if (!existsSync(path)) continue;
1034
+ const path = join2(root, name);
1035
+ if (!existsSync2(path)) continue;
51
1036
  try {
52
- return { text: readFileSync(path, "utf8"), file: name };
1037
+ return { text: readFileSync3(path, "utf8"), file: name };
53
1038
  } catch {
54
1039
  }
55
1040
  }
@@ -230,8 +1215,46 @@ function findRegisteredTools(tree) {
230
1215
  }
231
1216
  }
232
1217
  }
1218
+ for (const file of tree.files) {
1219
+ for (let i = 0; i < file.lines.length; i++) {
1220
+ const line = file.lines[i];
1221
+ if (line.comment) continue;
1222
+ const m = line.raw.match(DESCRIPTOR_KEY_RE);
1223
+ if (!m) continue;
1224
+ const indent = m[1];
1225
+ const hasSchema = objectHasKey(file.lines, i, indent);
1226
+ if (hasSchema) push(m[2], `${file.rel}:${line.n}`);
1227
+ }
1228
+ }
233
1229
  return out;
234
1230
  }
1231
+ var DESCRIPTOR_KEY_RE = new RegExp(`^(\\s*)name:\\s*["'\`](${NAME_LITERAL})["'\`]\\s*,?\\s*$`);
1232
+ var INPUT_SCHEMA_KEY_RE = /^(\s*)inputSchema\s*:/;
1233
+ function objectHasKey(lines, at, indent) {
1234
+ for (const step of [1, -1]) {
1235
+ for (let j = at + step; j >= 0 && j < lines.length; j += step) {
1236
+ const l = lines[j];
1237
+ if (l.comment || l.raw.trim() === "") continue;
1238
+ const lead = l.raw.match(/^(\s*)/)[1];
1239
+ if (lead.length < indent.length) break;
1240
+ if (lead === indent && INPUT_SCHEMA_KEY_RE.test(l.raw)) return true;
1241
+ }
1242
+ }
1243
+ return false;
1244
+ }
1245
+ var TOOL_SURFACE_RE = /\bListToolsRequestSchema\b|["'`]tools\/list["'`]|\binputSchema\s*:|\bToolDefinition\b/;
1246
+ function toolSurfaceAnchor(tree) {
1247
+ for (const file of tree.files) {
1248
+ for (const line of file.lines) {
1249
+ if (line.comment) continue;
1250
+ if (TOOL_SURFACE_RE.test(line.raw)) return `${file.rel}:${line.n}`;
1251
+ }
1252
+ }
1253
+ return null;
1254
+ }
1255
+ function noToolsLocatedReason(tree, what) {
1256
+ return `the tree serves MCP tools (tool-surface marker found) but no registered tool could be located: searched ${tree.files.length} .ts/.tsx file(s) under ${tree.root} (all directories except node_modules/dist/build/tests/fixtures) for registerTool/.tool( calls and declarative { name, inputSchema } descriptors \u2014 ${what}`;
1257
+ }
235
1258
  var REGISTRATION_CALL_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(/;
236
1259
  function findUnresolvedToolSites(tree) {
237
1260
  const bindings = resolveNameBindings(tree);
@@ -1180,7 +2203,7 @@ function c14PositiveControls() {
1180
2203
  rejectsPartial: missing(partial) !== void 0
1181
2204
  };
1182
2205
  }
1183
- var c14CatalogRegistration = {
2206
+ var c14CatalogRegistrationInner = {
1184
2207
  id: "C14 catalog-registration",
1185
2208
  rule: "C-14",
1186
2209
  pin: PIN3,
@@ -1235,6 +2258,10 @@ var c14CatalogRegistration = {
1235
2258
  };
1236
2259
  }
1237
2260
  };
2261
+ var c14CatalogRegistration = guardUnreadablePackageJson(
2262
+ c14CatalogRegistrationInner,
2263
+ { authoritative: true }
2264
+ );
1238
2265
 
1239
2266
  // src/detectors/c15-annotations-untrusted.ts
1240
2267
  var AUTO_APPROVE_ACTION_RE = /\b(?:autoApprove|skipConfirm|autoRun|autoExecute)\w*\s*\(/;
@@ -1419,11 +2446,11 @@ var c16UntrustedOutput = {
1419
2446
  };
1420
2447
 
1421
2448
  // src/detectors/c17-supply-chain.ts
1422
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
1423
- import { join as join2 } from "path";
2449
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
2450
+ import { join as join3 } from "path";
1424
2451
  var PIN4 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1425
2452
  var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
1426
- var WORKFLOW_DIR = join2(".github", "workflows");
2453
+ var WORKFLOW_DIR = join3(".github", "workflows");
1427
2454
  var PROVENANCE_RE2 = /--provenance\b|\bprovenance\s*:\s*true\b|id-token:\s*write/i;
1428
2455
  function c17PositiveControls() {
1429
2456
  const sample = "npm publish --provenance --access public";
@@ -1439,7 +2466,7 @@ function c17PositiveControls() {
1439
2466
  }
1440
2467
  function findLockfile(root) {
1441
2468
  for (const name of LOCKFILES) {
1442
- if (existsSync2(join2(root, name))) return name;
2469
+ if (existsSync3(join3(root, name))) return name;
1443
2470
  }
1444
2471
  return null;
1445
2472
  }
@@ -1451,20 +2478,20 @@ function findProvenanceClaim(tree, root) {
1451
2478
  return { site: `${pkg.path}:publishConfig.provenance`, text: "provenance: true" };
1452
2479
  }
1453
2480
  }
1454
- const workflowsDir = join2(root, WORKFLOW_DIR);
1455
- if (!existsSync2(workflowsDir)) return null;
2481
+ const workflowsDir = join3(root, WORKFLOW_DIR);
2482
+ if (!existsSync3(workflowsDir)) return null;
1456
2483
  let entries = [];
1457
2484
  try {
1458
- entries = readdirSync(workflowsDir);
2485
+ entries = readdirSync2(workflowsDir);
1459
2486
  } catch {
1460
2487
  return null;
1461
2488
  }
1462
2489
  for (const name of entries) {
1463
2490
  if (!/\.ya?ml$/.test(name)) continue;
1464
- const path = join2(workflowsDir, name);
2491
+ const path = join3(workflowsDir, name);
1465
2492
  let text;
1466
2493
  try {
1467
- text = readFileSync2(path, "utf8");
2494
+ text = readFileSync4(path, "utf8");
1468
2495
  } catch {
1469
2496
  continue;
1470
2497
  }
@@ -1475,7 +2502,7 @@ function findProvenanceClaim(tree, root) {
1475
2502
  }
1476
2503
  return null;
1477
2504
  }
1478
- var c17SupplyChain = {
2505
+ var c17SupplyChainInner = {
1479
2506
  id: "C17 supply-chain",
1480
2507
  rule: "C-17",
1481
2508
  pin: PIN4,
@@ -1512,19 +2539,22 @@ var c17SupplyChain = {
1512
2539
  };
1513
2540
  }
1514
2541
  };
2542
+ var c17SupplyChain = guardUnreadablePackageJson(c17SupplyChainInner, {
2543
+ authoritative: true
2544
+ });
1515
2545
 
1516
2546
  // src/detectors/c18-structure-manifest.ts
1517
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1518
- import { join as join3 } from "path";
2547
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
2548
+ import { join as join4 } from "path";
1519
2549
  var PIN5 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1520
2550
  function manifestPath(root) {
1521
- const extPath = join3(root, "extension.json");
1522
- if (existsSync3(extPath)) return { kind: "extension", path: extPath };
1523
- return { kind: "server", path: join3(root, "mcp.json") };
2551
+ const extPath = join4(root, "extension.json");
2552
+ if (existsSync4(extPath)) return { kind: "extension", path: extPath };
2553
+ return { kind: "server", path: join4(root, "mcp.json") };
1524
2554
  }
1525
2555
  function readJson(path) {
1526
2556
  try {
1527
- return JSON.parse(readFileSync3(path, "utf8"));
2557
+ return JSON.parse(readFileSync5(path, "utf8"));
1528
2558
  } catch {
1529
2559
  return null;
1530
2560
  }
@@ -1538,7 +2568,7 @@ function c18PositiveControls() {
1538
2568
  }
1539
2569
  };
1540
2570
  }
1541
- var c18StructureManifest = {
2571
+ var c18StructureManifestInner = {
1542
2572
  id: "C18 structure-manifest",
1543
2573
  rule: "C-18",
1544
2574
  pin: PIN5,
@@ -1572,7 +2602,7 @@ var c18StructureManifest = {
1572
2602
  }
1573
2603
  const { kind, path } = manifestPath(tree.root);
1574
2604
  const manifestName = kind === "extension" ? "extension.json" : "mcp.json";
1575
- if (!existsSync3(path)) {
2605
+ if (!existsSync4(path)) {
1576
2606
  return {
1577
2607
  id: this.id,
1578
2608
  anchor: tree.root,
@@ -1650,85 +2680,10 @@ var c18StructureManifest = {
1650
2680
  };
1651
2681
  }
1652
2682
  };
1653
-
1654
- // src/scan.ts
1655
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync } from "fs";
1656
- import { dirname, join as join4, relative } from "path";
1657
- var SKIP_DIRS = /* @__PURE__ */ new Set([
1658
- "node_modules",
1659
- "dist",
1660
- "build",
1661
- ".git",
1662
- "tests",
1663
- "__tests__",
1664
- "coverage",
1665
- // Test material, not a target's served surface: deliberately-malformed
1666
- // servers and foreign copies live here. Scanning them makes the instrument
1667
- // trip its own fail-closed detectors when pointed at its own tree, and lints
1668
- // inputs that are meant to be broken. A real target's fixtures are equally
1669
- // test inputs, not its product surface.
1670
- "fixtures"
1671
- ]);
1672
- function isCommentLine(raw) {
1673
- const t = raw.trim();
1674
- if (t === "") return false;
1675
- return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
1676
- }
1677
- function walk(dir, out) {
1678
- let entries;
1679
- try {
1680
- entries = readdirSync2(dir);
1681
- } catch {
1682
- return;
1683
- }
1684
- for (const name of entries) {
1685
- const full = join4(dir, name);
1686
- let st;
1687
- try {
1688
- st = statSync(full);
1689
- } catch {
1690
- continue;
1691
- }
1692
- if (st.isDirectory()) {
1693
- if (SKIP_DIRS.has(name)) continue;
1694
- walk(full, out);
1695
- } else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
1696
- if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
1697
- out.push(full);
1698
- }
1699
- }
1700
- }
1701
- function loadFile(path, root) {
1702
- const raw = readFileSync4(path, "utf8");
1703
- const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
1704
- return { path, rel: relative(root, path) || path, lines };
1705
- }
1706
- function findPackageJson(startDir) {
1707
- let dir = startDir;
1708
- for (let depth = 0; depth < 8; depth++) {
1709
- const candidate = join4(dir, "package.json");
1710
- if (existsSync4(candidate)) {
1711
- try {
1712
- return { path: candidate, json: JSON.parse(readFileSync4(candidate, "utf8")) };
1713
- } catch {
1714
- return { path: candidate, json: {} };
1715
- }
1716
- }
1717
- const parent = dirname(dir);
1718
- if (parent === dir) break;
1719
- dir = parent;
1720
- }
1721
- return null;
1722
- }
1723
- function loadTree(root) {
1724
- const st = statSync(root);
1725
- const baseDir = st.isDirectory() ? root : dirname(root);
1726
- const filePaths = [];
1727
- if (st.isDirectory()) walk(root, filePaths);
1728
- else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
1729
- const files = filePaths.map((p) => loadFile(p, baseDir));
1730
- return { root, files, packageJson: findPackageJson(baseDir) };
1731
- }
2683
+ var c18StructureManifest = guardUnreadablePackageJson(
2684
+ c18StructureManifestInner,
2685
+ { authoritative: false }
2686
+ );
1732
2687
 
1733
2688
  // src/detectors/c19-served-descriptions.ts
1734
2689
  var PIN6 = "a74a042b53c71a095ab88b3073bd47728eeda814";
@@ -1787,6 +2742,20 @@ var c19ServedDescriptions = {
1787
2742
  }
1788
2743
  const sites = findRegisteredTools(tree);
1789
2744
  if (sites.length === 0) {
2745
+ const surface = toolSurfaceAnchor(tree);
2746
+ if (surface) {
2747
+ const reason = noToolsLocatedReason(
2748
+ tree,
2749
+ "served descriptions cannot be judged without the tool list (C-19)"
2750
+ );
2751
+ return {
2752
+ id: this.id,
2753
+ anchor: surface,
2754
+ state: "could-not-judge",
2755
+ reason,
2756
+ verdict: `could not judge: ${reason}`
2757
+ };
2758
+ }
1790
2759
  return {
1791
2760
  id: this.id,
1792
2761
  anchor: "-",
@@ -2044,7 +3013,7 @@ var c21ProgressiveDisclosure = {
2044
3013
  };
2045
3014
 
2046
3015
  // src/detectors/c22-test-ratio-gate.ts
2047
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
3016
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
2048
3017
  import { join as join5 } from "path";
2049
3018
  var DOC_FILES2 = ["README.md", "README.fr.md", "CHANGELOG.md"];
2050
3019
  var RATIO_RE = /\b(?:coverage|ratio de tests?|test ratio|couverture)\b[^\n%]{0,40}?(\d{1,3}(?:\.\d+)?\s?%)/i;
@@ -2074,7 +3043,7 @@ function findRatioCitations(root) {
2074
3043
  if (!existsSync5(path)) continue;
2075
3044
  let text;
2076
3045
  try {
2077
- text = readFileSync5(path, "utf8");
3046
+ text = readFileSync6(path, "utf8");
2078
3047
  } catch {
2079
3048
  continue;
2080
3049
  }
@@ -2382,14 +3351,14 @@ import { spawnSync } from "child_process";
2382
3351
  import {
2383
3352
  existsSync as existsSync6,
2384
3353
  mkdtempSync,
2385
- readFileSync as readFileSync6,
3354
+ readFileSync as readFileSync7,
2386
3355
  readdirSync as readdirSync3,
2387
3356
  rmSync,
2388
3357
  statSync as statSync2,
2389
3358
  writeFileSync
2390
3359
  } from "fs";
2391
3360
  import { tmpdir } from "os";
2392
- import { join as join6, relative as relative2 } from "path";
3361
+ import { basename as basename2, join as join6, relative as relative2, sep as sep2 } from "path";
2393
3362
  var PIN9 = "20136b50";
2394
3363
  var DEPLOY_CONFIG_FILES = [
2395
3364
  "railway.json",
@@ -2406,8 +3375,7 @@ var LOCKFILES2 = [
2406
3375
  { name: "pnpm-lock.yaml", command: "pnpm", args: ["install", "--frozen-lockfile"] },
2407
3376
  { name: "yarn.lock", command: "yarn", args: ["install", "--frozen-lockfile"] }
2408
3377
  ];
2409
- var SKIP_WALK_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage"]);
2410
- function walkForConfigs(dir, depth, seen) {
3378
+ function walkForConfigs(dir, depth, seen, onSkip) {
2411
3379
  if (depth > 8) return;
2412
3380
  let entries;
2413
3381
  try {
@@ -2424,8 +3392,11 @@ function walkForConfigs(dir, depth, seen) {
2424
3392
  continue;
2425
3393
  }
2426
3394
  if (st.isDirectory()) {
2427
- if (SKIP_WALK_DIRS.has(name)) continue;
2428
- walkForConfigs(full, depth + 1, seen);
3395
+ if (SKIP_DIRS.has(name)) {
3396
+ onSkip(full, depth + 1);
3397
+ continue;
3398
+ }
3399
+ walkForConfigs(full, depth + 1, seen, onSkip);
2429
3400
  } else if (DEPLOY_CONFIG_FILES.includes(name)) {
2430
3401
  if (!seen.has(dir)) {
2431
3402
  seen.set(dir, { dir, configFile: name, configPath: full });
@@ -2433,10 +3404,41 @@ function walkForConfigs(dir, depth, seen) {
2433
3404
  }
2434
3405
  }
2435
3406
  }
3407
+ function probeForCandidate(dir, depth) {
3408
+ if (depth > 8) return null;
3409
+ let entries;
3410
+ try {
3411
+ entries = readdirSync3(dir).sort();
3412
+ } catch {
3413
+ return null;
3414
+ }
3415
+ const subdirs = [];
3416
+ for (const name of entries) {
3417
+ const full = join6(dir, name);
3418
+ let st;
3419
+ try {
3420
+ st = statSync2(full);
3421
+ } catch {
3422
+ continue;
3423
+ }
3424
+ if (st.isDirectory()) {
3425
+ const info = SKIP_DIR_INFO[name];
3426
+ if (info && !info.authored) continue;
3427
+ subdirs.push(full);
3428
+ } else if (DEPLOY_CONFIG_FILES.includes(name)) {
3429
+ return full;
3430
+ }
3431
+ }
3432
+ for (const sub of subdirs) {
3433
+ const hit = probeForCandidate(sub, depth + 1);
3434
+ if (hit) return hit;
3435
+ }
3436
+ return null;
3437
+ }
2436
3438
  function resolveRailwayRootDirectory(configPath) {
2437
3439
  let text;
2438
3440
  try {
2439
- text = readFileSync6(configPath, "utf8");
3441
+ text = readFileSync7(configPath, "utf8");
2440
3442
  } catch (err) {
2441
3443
  return {
2442
3444
  error: `could not read ${configPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -2454,15 +3456,31 @@ function resolveRailwayRootDirectory(configPath) {
2454
3456
  const rootDirectory = typeof parsed.rootDirectory === "string" && parsed.rootDirectory || build && typeof build.rootDirectory === "string" && build.rootDirectory || null;
2455
3457
  return rootDirectory;
2456
3458
  }
2457
- function c26DeriveScope(root) {
3459
+ function c26DeriveScope(root, tree) {
2458
3460
  const seen = /* @__PURE__ */ new Map();
2459
- walkForConfigs(root, 0, seen);
3461
+ const skipped = [];
3462
+ const onSkip = (skippedDir, depth) => {
3463
+ const entry = tree ? recordSkip(tree, skippedDir, root) : {
3464
+ path: relative2(root, skippedDir).split(sep2).join("/"),
3465
+ reason: SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
3466
+ };
3467
+ if (SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
3468
+ const hit = probeForCandidate(skippedDir, depth);
3469
+ if (hit) entry.candidate = relative2(root, hit).split(sep2).join("/");
3470
+ }
3471
+ if (!skipped.some((d) => d.path === entry.path)) skipped.push(entry);
3472
+ };
3473
+ walkForConfigs(root, 0, seen, onSkip);
2460
3474
  const sites = [];
2461
3475
  for (const site of seen.values()) {
2462
3476
  if (site.configFile === "railway.json") {
2463
3477
  const resolved = resolveRailwayRootDirectory(site.configPath);
2464
3478
  if (resolved && typeof resolved === "object" && "error" in resolved) {
2465
- return { sites: [], unparsable: { path: site.configPath, reason: resolved.error } };
3479
+ return {
3480
+ sites: [],
3481
+ skipped,
3482
+ unparsable: { path: site.configPath, reason: resolved.error }
3483
+ };
2466
3484
  }
2467
3485
  if (typeof resolved === "string" && resolved.trim()) {
2468
3486
  sites.push({ ...site, dir: join6(site.dir, resolved) });
@@ -2471,10 +3489,11 @@ function c26DeriveScope(root) {
2471
3489
  }
2472
3490
  if (site.configFile === "vercel.json") {
2473
3491
  try {
2474
- JSON.parse(readFileSync6(site.configPath, "utf8"));
3492
+ JSON.parse(readFileSync7(site.configPath, "utf8"));
2475
3493
  } catch (err) {
2476
3494
  return {
2477
3495
  sites: [],
3496
+ skipped,
2478
3497
  unparsable: {
2479
3498
  path: site.configPath,
2480
3499
  reason: `could not parse ${site.configPath} as JSON: ${err instanceof Error ? err.message : String(err)}`
@@ -2484,7 +3503,7 @@ function c26DeriveScope(root) {
2484
3503
  }
2485
3504
  sites.push(site);
2486
3505
  }
2487
- return { sites };
3506
+ return { sites, skipped };
2488
3507
  }
2489
3508
  function findLockfile2(dir) {
2490
3509
  for (const l of LOCKFILES2) {
@@ -2539,8 +3558,8 @@ function nameDrift(lockfileName, siteDir, isolatedDir, stderr) {
2539
3558
  }
2540
3559
  if (lockfileName === "bun.lock" || lockfileName === "bun.lockb") {
2541
3560
  try {
2542
- const manifestText = readFileSync6(join6(isolatedDir, "package.json"), "utf8");
2543
- const lockText = readFileSync6(join6(isolatedDir, lockfileName), "utf8");
3561
+ const manifestText = readFileSync7(join6(isolatedDir, "package.json"), "utf8");
3562
+ const lockText = readFileSync7(join6(isolatedDir, lockfileName), "utf8");
2544
3563
  const drifted = nameBunDrift(manifestText, lockText);
2545
3564
  if (drifted.length > 0) return drifted.join(", ");
2546
3565
  } catch {
@@ -2571,8 +3590,8 @@ function gateSite(site) {
2571
3590
  }
2572
3591
  const isolated = mkdtempSync(join6(tmpdir(), "mcp-doctor-c26-"));
2573
3592
  try {
2574
- const manifestText = readFileSync6(pkgPath, "utf8");
2575
- const lockText = readFileSync6(join6(site.dir, lock.name), "utf8");
3593
+ const manifestText = readFileSync7(pkgPath, "utf8");
3594
+ const lockText = readFileSync7(join6(site.dir, lock.name), "utf8");
2576
3595
  writeFileSync(join6(isolated, "package.json"), manifestText);
2577
3596
  writeFileSync(join6(isolated, lock.name), lockText);
2578
3597
  let proc;
@@ -2641,7 +3660,18 @@ var c26SubpackageLockfile = {
2641
3660
  rule: "C-26",
2642
3661
  pin: PIN9,
2643
3662
  run(tree) {
2644
- const scope = c26DeriveScope(tree.root);
3663
+ const scope = c26DeriveScope(tree.root, tree);
3664
+ const hidden = scope.skipped.filter((d) => d.candidate !== void 0);
3665
+ const unjudgedHidden = (prefix) => {
3666
+ const reason = `${prefix}deployment configuration found only under skipped director${hidden.length === 1 ? "y" : "ies"}: ${hidden.map((d) => `${d.path} (${d.reason}) -> ${d.candidate}`).join("; ")} \u2014 C-26 does not descend into skipped directories, so these site(s) were not judged`;
3667
+ return {
3668
+ id: this.id,
3669
+ anchor: join6(tree.root, hidden[0].candidate),
3670
+ state: "could-not-judge",
3671
+ reason,
3672
+ verdict: `could not judge: ${reason}`
3673
+ };
3674
+ };
2645
3675
  if (scope.unparsable) {
2646
3676
  const reason = `deployment configuration ${scope.unparsable.path} could not be parsed: ${scope.unparsable.reason} \u2014 scope unestablished, C-26 cannot derive its targets`;
2647
3677
  return {
@@ -2653,6 +3683,7 @@ var c26SubpackageLockfile = {
2653
3683
  };
2654
3684
  }
2655
3685
  if (scope.sites.length === 0) {
3686
+ if (hidden.length > 0) return unjudgedHidden("");
2656
3687
  return {
2657
3688
  id: this.id,
2658
3689
  anchor: tree.root,
@@ -2664,6 +3695,7 @@ var c26SubpackageLockfile = {
2664
3695
  const inventory = renderCoverage(scope.sites, verdicts);
2665
3696
  const analysed = verdicts.filter((v) => v.status === "analysed");
2666
3697
  if (analysed.length === 0) {
3698
+ if (hidden.length > 0) return unjudgedHidden("no analysable site; ");
2667
3699
  return {
2668
3700
  id: this.id,
2669
3701
  anchor: tree.root,
@@ -2680,6 +3712,11 @@ var c26SubpackageLockfile = {
2680
3712
  verdict: reds.some((r) => r.detail?.includes("no lockfile")) ? `${reds[0].detail} (C-26). Coverage: ${inventory}` : `${reds.map((r) => r.detail).join(" ; ")} (C-26). Coverage: ${inventory}`
2681
3713
  };
2682
3714
  }
3715
+ if (hidden.length > 0) {
3716
+ return unjudgedHidden(
3717
+ `${analysed.length} analysed site(s) are consistent, but coverage is partial; `
3718
+ );
3719
+ }
2683
3720
  return {
2684
3721
  id: this.id,
2685
3722
  anchor: analysed[0].site.dir,
@@ -2749,6 +3786,21 @@ var d3 = {
2749
3786
  verdict: `could not judge: ${reason}`
2750
3787
  };
2751
3788
  }
3789
+ const surface = toolSurfaceAnchor(tree);
3790
+ if (surface) {
3791
+ const located = findRegisteredTools(tree);
3792
+ const reason = located.length > 0 ? `${located.length} tool(s) located but no hand-rolled list/read result builder found \u2014 result shaping and any resultType are delegated to wrappers/the transport and not statically readable in this tree` : noToolsLocatedReason(
3793
+ tree,
3794
+ "result shaping and resultType are not statically readable without the located tools"
3795
+ );
3796
+ return {
3797
+ id: this.id,
3798
+ anchor: surface,
3799
+ state: "could-not-judge",
3800
+ reason,
3801
+ verdict: `could not judge: ${reason}`
3802
+ };
3803
+ }
2752
3804
  return {
2753
3805
  id: this.id,
2754
3806
  anchor: "-",
@@ -2793,6 +3845,21 @@ var d4 = {
2793
3845
  verdict: `could not judge: ${reason}`
2794
3846
  };
2795
3847
  }
3848
+ const surface = toolSurfaceAnchor(tree);
3849
+ if (surface) {
3850
+ const located = findRegisteredTools(tree);
3851
+ const reason = located.length > 0 ? `${located.length} tool(s) located but no hand-rolled list/read result builder found \u2014 result shaping and any ttlMs/cacheScope are delegated to wrappers/the transport and not statically readable in this tree` : noToolsLocatedReason(
3852
+ tree,
3853
+ "result shaping and any ttlMs/cacheScope are not statically readable without the located tools"
3854
+ );
3855
+ return {
3856
+ id: this.id,
3857
+ anchor: surface,
3858
+ state: "could-not-judge",
3859
+ reason,
3860
+ verdict: `could not judge: ${reason}`
3861
+ };
3862
+ }
2796
3863
  return {
2797
3864
  id: this.id,
2798
3865
  anchor: "-",
@@ -2922,7 +3989,7 @@ function depListed(pkgJson) {
2922
3989
  }
2923
3990
  return false;
2924
3991
  }
2925
- var d7 = {
3992
+ var d7Inner = {
2926
3993
  id: "D7 protocol-kit",
2927
3994
  run(tree) {
2928
3995
  const listed = depListed(tree.packageJson?.json);
@@ -2961,9 +4028,10 @@ var d7 = {
2961
4028
  };
2962
4029
  }
2963
4030
  };
4031
+ var d7 = guardUnreadablePackageJson(d7Inner, { authoritative: true });
2964
4032
 
2965
4033
  // src/detectors/d8.ts
2966
- import { existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
4034
+ import { existsSync as existsSync7, statSync as statSync3 } from "fs";
2967
4035
  import { dirname as dirname2, join as join7, relative as relative3 } from "path";
2968
4036
  var POSITIVE_CONTROL2 = 'server.registerTool("list_items", { ... }, handler);';
2969
4037
  function d8PositiveControl() {
@@ -3016,6 +4084,20 @@ var d8 = {
3016
4084
  verdict: `could not judge: ${reason}`
3017
4085
  };
3018
4086
  }
4087
+ const surface = toolSurfaceAnchor(tree);
4088
+ if (surface) {
4089
+ const reason = noToolsLocatedReason(
4090
+ tree,
4091
+ "eval coverage cannot be judged without the tool list"
4092
+ );
4093
+ return {
4094
+ id: this.id,
4095
+ anchor: surface,
4096
+ state: "could-not-judge",
4097
+ reason,
4098
+ verdict: `could not judge: ${reason}`
4099
+ };
4100
+ }
3019
4101
  return {
3020
4102
  id: this.id,
3021
4103
  anchor: "-",
@@ -3034,19 +4116,15 @@ var d8 = {
3034
4116
  }
3035
4117
  const baseDir = resolveBaseDir(tree);
3036
4118
  const displayPath = relative3(baseDir, evalsPath) || evalsPath;
3037
- let cases;
3038
- try {
3039
- const raw = readFileSync7(evalsPath, "utf8");
3040
- const parsed = JSON.parse(raw);
3041
- if (!Array.isArray(parsed.cases)) {
3042
- throw new Error("`cases` is not an array");
3043
- }
3044
- cases = parsed.cases;
3045
- } catch (err) {
3046
- throw new Error(
3047
- `D8 could not parse ${displayPath}: ${err instanceof Error ? err.message : String(err)} \u2014 coverage verdict untrustworthy, fix the eval file`
4119
+ const parsed = readJsonObject(evalsPath);
4120
+ if (parsed === null || !Array.isArray(parsed.cases)) {
4121
+ return couldNotJudge(
4122
+ this.id,
4123
+ displayPath,
4124
+ `could not parse ${displayPath}: ${parsed === null ? "not readable as a JSON object" : "`cases` is not an array"} \u2014 coverage verdict untrustworthy, fix the eval file`
3048
4125
  );
3049
4126
  }
4127
+ const cases = parsed.cases;
3050
4128
  const counts = /* @__PURE__ */ new Map();
3051
4129
  for (const t of tools) counts.set(t.name, 0);
3052
4130
  for (const c of cases) {
@@ -3131,7 +4209,7 @@ function d9PositiveControl() {
3131
4209
  matched: SEMVER_RE.test(POSITIVE_CONTROL3)
3132
4210
  };
3133
4211
  }
3134
- var d9 = {
4212
+ var d9Inner = {
3135
4213
  id: "D9 semver+changelog",
3136
4214
  rule: "C-3",
3137
4215
  pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
@@ -3154,7 +4232,11 @@ var d9 = {
3154
4232
  }
3155
4233
  const { path: pkgPath, json: pkgJson } = tree.packageJson;
3156
4234
  if (!pkgJson || Object.keys(pkgJson).length === 0) {
3157
- throw new Error(`D9 could not parse ${pkgPath} as JSON \u2014 cannot verify SemVer/CHANGELOG`);
4235
+ return couldNotJudge(
4236
+ this.id,
4237
+ pkgPath.split("/").slice(-2).join("/"),
4238
+ `could not parse ${pkgPath} as JSON \u2014 cannot verify SemVer/CHANGELOG`
4239
+ );
3158
4240
  }
3159
4241
  const version = pkgJson.version;
3160
4242
  if (typeof version !== "string") {
@@ -3193,6 +4275,7 @@ var d9 = {
3193
4275
  };
3194
4276
  }
3195
4277
  };
4278
+ var d9 = guardUnreadablePackageJson(d9Inner, { authoritative: false });
3196
4279
 
3197
4280
  // src/detectors/d10.ts
3198
4281
  import { existsSync as existsSync9 } from "fs";
@@ -3244,7 +4327,7 @@ function scanSourceDescriptionFr(tree) {
3244
4327
  }
3245
4328
  return { present: false, anchor: null };
3246
4329
  }
3247
- var d10 = {
4330
+ var d10Inner = {
3248
4331
  id: "D10 bilingual",
3249
4332
  rule: "C-1",
3250
4333
  pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
@@ -3315,6 +4398,7 @@ var d10 = {
3315
4398
  };
3316
4399
  }
3317
4400
  };
4401
+ var d10 = guardUnreadablePackageJson(d10Inner, { authoritative: true });
3318
4402
 
3319
4403
  // src/detectors/d11.ts
3320
4404
  var ZOD_ANY_RE = /\bz\s*\.\s*any\s*\(\s*\)/;
@@ -3386,6 +4470,27 @@ var d11 = {
3386
4470
  }
3387
4471
  }
3388
4472
  if (toolCount === 0) {
4473
+ const located = findRegisteredTools(tree);
4474
+ if (located.length > 0) {
4475
+ toolCount = located.length;
4476
+ firstToolAnchor = located[0].anchor;
4477
+ }
4478
+ }
4479
+ if (toolCount === 0) {
4480
+ const surface = toolSurfaceAnchor(tree);
4481
+ if (surface) {
4482
+ const reason = noToolsLocatedReason(
4483
+ tree,
4484
+ "the outputSchema check cannot be judged without the tool list"
4485
+ );
4486
+ return {
4487
+ id: this.id,
4488
+ anchor: surface,
4489
+ state: "could-not-judge",
4490
+ reason,
4491
+ verdict: `could not judge: ${reason}`
4492
+ };
4493
+ }
3389
4494
  return {
3390
4495
  id: this.id,
3391
4496
  anchor: "-",
@@ -3465,7 +4570,7 @@ function findHttpSite(tree) {
3465
4570
  }
3466
4571
  return null;
3467
4572
  }
3468
- var d12 = {
4573
+ var d12Inner = {
3469
4574
  id: "D12 stdio-transport",
3470
4575
  run(tree) {
3471
4576
  const stdioControl = d12StdioPositiveControl();
@@ -3516,6 +4621,7 @@ var d12 = {
3516
4621
  };
3517
4622
  }
3518
4623
  };
4624
+ var d12 = guardUnreadablePackageJson(d12Inner, { authoritative: false });
3519
4625
 
3520
4626
  // src/detectors/d15.ts
3521
4627
  var HANDLE_ASSIGN_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[^;]*\.(?:registerTool|tool)\s*\(/;
@@ -4129,71 +5235,63 @@ var dDebugOptIn = {
4129
5235
  );
4130
5236
  }
4131
5237
  }
4132
- const computed = findAll(tree, COMPUTED_LEVEL_RE);
4133
- if (computed.length > 0) {
4134
- const sites = computed.map((h) => `${h.file.rel}:${h.line}`).join(", ");
4135
- throw new Error(
4136
- `DDebugOptIn cannot read the level of ${computed.length} computed-level log call(s): ${sites} \u2014 a bracket-indexed level hides whether it is debug (Server Rule 6). Fail-closed, not a silent present.`
4137
- );
4138
- }
4139
- const debugCalls = findAll(tree, DEBUG_CALL_RE);
4140
- if (debugCalls.length === 0) {
4141
- return {
4142
- id: this.id,
4143
- anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
4144
- state: "absent",
4145
- verdict: "no debug output emitted (n/a for Server Rule 6 debug-half)"
4146
- };
4147
- }
4148
- const callAnchor = `${debugCalls[0].file.rel}:${debugCalls.map((h) => h.line).join(",")}`;
4149
- const debugFileRels = new Set(debugCalls.map((h) => h.file.rel));
4150
- const localGate = firstMatchInFiles2(tree, GATE_RE, debugFileRels);
4151
- if (localGate) {
4152
- return {
4153
- id: this.id,
4154
- anchor: callAnchor,
4155
- state: "present",
4156
- verdict: `debug output at ${callAnchor} gated by an env/config switch in the same file at ${localGate.file.rel}:${localGate.line} (Server Rule 6 debug-half)`
4157
- };
5238
+ const unreadable = [];
5239
+ for (const h of findAll(tree, COMPUTED_LEVEL_RE)) {
5240
+ const anchor = `${h.file.rel}:${h.line}`;
5241
+ unreadable.push({
5242
+ anchor,
5243
+ reason: `cannot read the level of the computed-level log call at ${anchor} \u2014 a bracket-indexed level hides whether it is debug (Server Rule 6)`
5244
+ });
4158
5245
  }
4159
- const remoteGate = firstMatchInFiles2(tree, GATE_RE, new Set(tree.files.map((f) => f.rel)));
4160
- if (remoteGate) {
4161
- const reason = `debug output at ${callAnchor}; an env/config gate exists at ${remoteGate.file.rel}:${remoteGate.line} but in a DIFFERENT file from every debug call \u2014 whether the debug path is opt-in is unreadable statically (Server Rule 6 debug-half)`;
5246
+ const decided = (() => {
5247
+ const debugCalls = findAll(tree, DEBUG_CALL_RE);
5248
+ if (debugCalls.length === 0) {
5249
+ return {
5250
+ id: this.id,
5251
+ anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
5252
+ state: "absent",
5253
+ verdict: "no debug output emitted (n/a for Server Rule 6 debug-half)"
5254
+ };
5255
+ }
5256
+ const callAnchor = `${debugCalls[0].file.rel}:${debugCalls.map((h) => h.line).join(",")}`;
5257
+ const debugFileRels = new Set(debugCalls.map((h) => h.file.rel));
5258
+ const localGate = firstMatchInFiles2(tree, GATE_RE, debugFileRels);
5259
+ if (localGate) {
5260
+ return {
5261
+ id: this.id,
5262
+ anchor: callAnchor,
5263
+ state: "present",
5264
+ verdict: `debug output at ${callAnchor} gated by an env/config switch in the same file at ${localGate.file.rel}:${localGate.line} (Server Rule 6 debug-half)`
5265
+ };
5266
+ }
5267
+ const remoteGate = firstMatchInFiles2(tree, GATE_RE, new Set(tree.files.map((f) => f.rel)));
5268
+ if (remoteGate) {
5269
+ const reason = `debug output at ${callAnchor}; an env/config gate exists at ${remoteGate.file.rel}:${remoteGate.line} but in a DIFFERENT file from every debug call \u2014 whether the debug path is opt-in is unreadable statically (Server Rule 6 debug-half)`;
5270
+ return {
5271
+ id: this.id,
5272
+ anchor: callAnchor,
5273
+ state: "could-not-judge",
5274
+ reason,
5275
+ verdict: `could not judge: ${reason}`
5276
+ };
5277
+ }
4162
5278
  return {
4163
5279
  id: this.id,
4164
5280
  anchor: callAnchor,
4165
- state: "could-not-judge",
4166
- reason,
4167
- verdict: `could not judge: ${reason}`
5281
+ state: "non-conforming",
5282
+ verdict: `debug output at ${callAnchor} with NO env/config gate -> debug is on by default; gate it behind process.env.*DEBUG* or a config switch (Server Rule 6 debug-half)`
4168
5283
  };
4169
- }
4170
- return {
4171
- id: this.id,
4172
- anchor: callAnchor,
4173
- state: "non-conforming",
4174
- verdict: `debug output at ${callAnchor} with NO env/config gate -> debug is on by default; gate it behind process.env.*DEBUG* or a config switch (Server Rule 6 debug-half)`
4175
- };
5284
+ })();
5285
+ return settle(this.id, decided, unreadable);
4176
5286
  }
4177
5287
  };
4178
5288
 
4179
- // src/detectors/dExt1.ts
4180
- import { readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
5289
+ // src/detectors/extManifests.ts
5290
+ import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4181
5291
  import { join as join12 } from "path";
4182
- var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
4183
- var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
4184
- var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
4185
- function dExt1PositiveControls() {
4186
- return {
4187
- identifier: {
4188
- pattern: IDENTIFIER_RE2.source,
4189
- positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
4190
- negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
4191
- }
4192
- };
4193
- }
4194
- var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4195
- function findExtensionManifests(root) {
5292
+ function findExtensionManifests(tree) {
4196
5293
  const out = [];
5294
+ const root = tree.root;
4197
5295
  let base;
4198
5296
  try {
4199
5297
  base = statSync5(root).isDirectory() ? root : join12(root, "..");
@@ -4216,7 +5314,10 @@ function findExtensionManifests(root) {
4216
5314
  continue;
4217
5315
  }
4218
5316
  if (st.isDirectory()) {
4219
- if (SKIP_DIRS2.has(name)) continue;
5317
+ if (SKIP_DIRS.has(name)) {
5318
+ recordSkip(tree, full, base);
5319
+ continue;
5320
+ }
4220
5321
  walk2(full);
4221
5322
  } else if (name === "extension.json") {
4222
5323
  out.push(full);
@@ -4226,20 +5327,34 @@ function findExtensionManifests(root) {
4226
5327
  walk2(base);
4227
5328
  return out;
4228
5329
  }
5330
+
5331
+ // src/detectors/dExt1.ts
5332
+ var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
5333
+ var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
5334
+ var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
5335
+ function dExt1PositiveControls() {
5336
+ return {
5337
+ identifier: {
5338
+ pattern: IDENTIFIER_RE2.source,
5339
+ positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
5340
+ negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
5341
+ }
5342
+ };
5343
+ }
4229
5344
  var TS_IDENT_LITERAL_RE = /\bidentifier:\s*["'`]([^"'`]*)["'`]/;
4230
- var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`]/;
5345
+ var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`{]/;
4231
5346
  function findTsIdentifiers(tree) {
4232
5347
  const sites = [];
4233
5348
  const unresolved = [];
4234
5349
  for (const file of tree.files) {
4235
5350
  for (const line of file.lines) {
4236
5351
  if (line.comment) continue;
4237
- const lit = line.raw.match(TS_IDENT_LITERAL_RE);
5352
+ const lit = matchInCode(file, line, TS_IDENT_LITERAL_RE);
4238
5353
  if (lit) {
4239
5354
  sites.push({ value: lit[1], anchor: `${file.rel}:${line.n}` });
4240
5355
  continue;
4241
5356
  }
4242
- if (TS_IDENT_KEY_RE.test(line.raw)) {
5357
+ if (testInCode(file, line, TS_IDENT_KEY_RE)) {
4243
5358
  unresolved.push(`${file.rel}:${line.n}`);
4244
5359
  }
4245
5360
  }
@@ -4255,69 +5370,75 @@ var dExt1 = {
4255
5370
  `DExt1 identifier control FAILED: /${controls.identifier.pattern}/ accept(\`${IDENTIFIER_POSITIVE}\`)=${controls.identifier.positive} reject(\`${IDENTIFIER_NEGATIVE}\`)=${!controls.identifier.negative} \u2014 matcher broken, verdict untrustworthy`
4256
5371
  );
4257
5372
  }
4258
- const manifests = findExtensionManifests(tree.root);
4259
- for (const manifestPath2 of manifests) {
4260
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4261
- let parsed;
4262
- try {
4263
- parsed = JSON.parse(readFileSync10(manifestPath2, "utf8"));
4264
- } catch {
4265
- throw new Error(
4266
- `DExt1 could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable identifier cannot pass (Extension Rule 1). Fail-closed, not a silent present.`
4267
- );
5373
+ const unreadable = [];
5374
+ const decided = (() => {
5375
+ const manifests = findExtensionManifests(tree);
5376
+ for (const manifestPath2 of manifests) {
5377
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
5378
+ const parsed = readJsonObject(manifestPath2);
5379
+ if (parsed === null) {
5380
+ unreadable.push({
5381
+ anchor: shortPath,
5382
+ reason: `could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable identifier cannot pass (Extension Rule 1)`
5383
+ });
5384
+ continue;
5385
+ }
5386
+ const idValue = parsed.identifier;
5387
+ if (typeof idValue !== "string") {
5388
+ return {
5389
+ id: this.id,
5390
+ anchor: shortPath,
5391
+ state: "non-conforming",
5392
+ verdict: `manifest ${shortPath} has no string "identifier" field (got ${typeof idValue}) -> declare a reverse-DNS identifier <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
5393
+ };
5394
+ }
5395
+ if (!IDENTIFIER_RE2.test(idValue)) {
5396
+ return {
5397
+ id: this.id,
5398
+ anchor: shortPath,
5399
+ state: "non-conforming",
5400
+ verdict: `manifest identifier "${idValue}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
5401
+ };
5402
+ }
4268
5403
  }
4269
- const idValue = parsed.identifier;
4270
- if (typeof idValue !== "string") {
4271
- throw new Error(
4272
- `DExt1 found a present manifest ${shortPath} with no string "identifier" field (got ${typeof idValue}) \u2014 Extension Rule 1 requires a reverse-DNS identifier. Fail-closed, not a silent present.`
4273
- );
5404
+ const { sites, unresolved } = findTsIdentifiers(tree);
5405
+ for (const anchor of unresolved) {
5406
+ unreadable.push({
5407
+ anchor,
5408
+ reason: `cannot read the identifier at ${anchor} \u2014 value is computed/imported/template, reverse-DNS unverifiable (Extension Rule 1)`
5409
+ });
4274
5410
  }
4275
- if (!IDENTIFIER_RE2.test(idValue)) {
4276
- return {
4277
- id: this.id,
4278
- anchor: shortPath,
4279
- state: "non-conforming",
4280
- verdict: `manifest identifier "${idValue}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
4281
- };
5411
+ for (const site of sites) {
5412
+ if (!IDENTIFIER_RE2.test(site.value)) {
5413
+ return {
5414
+ id: this.id,
5415
+ anchor: site.anchor,
5416
+ state: "non-conforming",
5417
+ verdict: `identifier "${site.value}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
5418
+ };
5419
+ }
4282
5420
  }
4283
- }
4284
- const { sites, unresolved } = findTsIdentifiers(tree);
4285
- if (unresolved.length > 0) {
4286
- throw new Error(
4287
- `DExt1 cannot read the identifier at ${unresolved.length} TS site(s): ${unresolved.join(", ")} \u2014 value is computed/imported/template, reverse-DNS unverifiable (Extension Rule 1). Fail-closed, not a silent present.`
4288
- );
4289
- }
4290
- for (const site of sites) {
4291
- if (!IDENTIFIER_RE2.test(site.value)) {
5421
+ const total = manifests.length + sites.length;
5422
+ if (total === 0) {
4292
5423
  return {
4293
5424
  id: this.id,
4294
- anchor: site.anchor,
4295
- state: "non-conforming",
4296
- verdict: `identifier "${site.value}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
5425
+ anchor: tree.root.split("/").pop() ?? tree.root,
5426
+ state: "absent",
5427
+ verdict: "no extension identifier found (no extension.json manifest, no TS `identifier:` literal) -> declare a reverse-DNS identifier (Extension Rule 1)"
4297
5428
  };
4298
5429
  }
4299
- }
4300
- const total = manifests.length + sites.length;
4301
- if (total === 0) {
4302
5430
  return {
4303
5431
  id: this.id,
4304
- anchor: tree.root.split("/").pop() ?? tree.root,
4305
- state: "absent",
4306
- verdict: "no extension identifier found (no extension.json manifest, no TS `identifier:` literal) -> declare a reverse-DNS identifier (Extension Rule 1)"
5432
+ anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
5433
+ state: "present",
5434
+ verdict: `${total} identifier(s) reverse-DNS conforming (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4307
5435
  };
4308
- }
4309
- return {
4310
- id: this.id,
4311
- anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
4312
- state: "present",
4313
- verdict: `${total} identifier(s) reverse-DNS conforming (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4314
- };
5436
+ })();
5437
+ return settle(this.id, decided, unreadable);
4315
5438
  }
4316
5439
  };
4317
5440
 
4318
5441
  // src/detectors/dExt2.ts
4319
- import { readFileSync as readFileSync11, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
4320
- import { join as join13 } from "path";
4321
5442
  var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
4322
5443
  var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
4323
5444
  var TS_GD_KEY_RE = /\bgracefulDegradation:\s*([^\s,}]+)/;
@@ -4332,41 +5453,6 @@ function dExt2PositiveControls() {
4332
5453
  }
4333
5454
  };
4334
5455
  }
4335
- var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4336
- function findExtensionManifests2(root) {
4337
- const out = [];
4338
- let base;
4339
- try {
4340
- base = statSync6(root).isDirectory() ? root : join13(root, "..");
4341
- } catch {
4342
- return out;
4343
- }
4344
- const walk2 = (dir) => {
4345
- let entries;
4346
- try {
4347
- entries = readdirSync5(dir);
4348
- } catch {
4349
- return;
4350
- }
4351
- for (const name of entries) {
4352
- const full = join13(dir, name);
4353
- let st;
4354
- try {
4355
- st = statSync6(full);
4356
- } catch {
4357
- continue;
4358
- }
4359
- if (st.isDirectory()) {
4360
- if (SKIP_DIRS3.has(name)) continue;
4361
- walk2(full);
4362
- } else if (name === "extension.json") {
4363
- out.push(full);
4364
- }
4365
- }
4366
- };
4367
- walk2(base);
4368
- return out;
4369
- }
4370
5456
  function findTsFactorySites(tree) {
4371
5457
  const sites = [];
4372
5458
  for (const file of tree.files) {
@@ -4374,7 +5460,7 @@ function findTsFactorySites(tree) {
4374
5460
  for (let i = 0; i < lines.length; i++) {
4375
5461
  const line = lines[i];
4376
5462
  if (line.comment) continue;
4377
- if (!DEFINE_EXTENSION_OPEN_RE.test(line.raw)) continue;
5463
+ if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE)) continue;
4378
5464
  const anchor = `${file.rel}:${line.n}`;
4379
5465
  let depth = 0;
4380
5466
  let started = false;
@@ -4384,20 +5470,20 @@ function findTsFactorySites(tree) {
4384
5470
  const bl = lines[j];
4385
5471
  if (!bl.comment) {
4386
5472
  if (state === "absent") {
4387
- if (TS_GD_TRUE_RE.test(bl.raw)) {
5473
+ if (testInCode(file, bl, TS_GD_TRUE_RE)) {
4388
5474
  state = "true";
4389
- } else if (TS_GD_FALSE_RE.test(bl.raw)) {
5475
+ } else if (testInCode(file, bl, TS_GD_FALSE_RE)) {
4390
5476
  state = "false";
4391
5477
  } else {
4392
- const key = bl.raw.match(TS_GD_KEY_RE);
5478
+ const key = matchInCode(file, bl, TS_GD_KEY_RE);
4393
5479
  if (key) {
4394
5480
  state = "unresolved";
4395
5481
  value = key[1];
4396
5482
  }
4397
5483
  }
4398
5484
  }
4399
- depth += (bl.raw.match(/\{/g) ?? []).length;
4400
- depth -= (bl.raw.match(/\}/g) ?? []).length;
5485
+ depth += countInCode(file, bl, "{");
5486
+ depth -= countInCode(file, bl, "}");
4401
5487
  if (depth > 0) started = true;
4402
5488
  }
4403
5489
  if (started && depth <= 0) break;
@@ -4416,133 +5502,105 @@ var dExt2 = {
4416
5502
  `DExt2 graceful-degradation control FAILED: accept(\`${GD_POSITIVE}\`)=${controls.gracefulDegradation.acceptsTrue} reject(\`${GD_NEGATIVE}\`)=${controls.gracefulDegradation.rejectsFalse} \u2014 matcher broken, verdict untrustworthy`
4417
5503
  );
4418
5504
  }
4419
- const manifests = findExtensionManifests2(tree.root);
4420
- for (const manifestPath2 of manifests) {
4421
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4422
- let parsed;
4423
- try {
4424
- parsed = JSON.parse(readFileSync11(manifestPath2, "utf8"));
4425
- } catch {
4426
- throw new Error(
4427
- `DExt2 could not parse ${shortPath} as JSON \u2014 a present manifest silent on graceful degradation cannot pass (Extension Rule 2). Fail-closed, not a silent present.`
4428
- );
4429
- }
4430
- if (!("graceful_degradation" in parsed)) {
4431
- throw new Error(
4432
- `DExt2 found a present manifest ${shortPath} with no "graceful_degradation" field \u2014 Extension Rule 2 requires it to be true. Fail-closed, not a silent present.`
4433
- );
4434
- }
4435
- const gdValue = parsed.graceful_degradation;
4436
- if (gdValue !== true) {
4437
- return {
4438
- id: this.id,
4439
- anchor: shortPath,
4440
- state: "non-conforming",
4441
- verdict: `manifest "graceful_degradation" is ${JSON.stringify(gdValue)}, not true (positive-control OK) -> set "graceful_degradation": true, an extension never breaks the host server (Extension Rule 2)`
4442
- };
4443
- }
4444
- }
4445
- const sites = findTsFactorySites(tree);
4446
- for (const site of sites) {
4447
- if (site.state === "unresolved") {
4448
- throw new Error(
4449
- `DExt2 cannot read the gracefulDegradation value at ${site.anchor} (got \`${site.value}\`) \u2014 computed/imported/template, graceful degradation unverifiable (Extension Rule 2). Fail-closed, not a silent present.`
4450
- );
5505
+ const unreadable = [];
5506
+ const decided = (() => {
5507
+ const manifests = findExtensionManifests(tree);
5508
+ for (const manifestPath2 of manifests) {
5509
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
5510
+ const parsed = readJsonObject(manifestPath2);
5511
+ if (parsed === null) {
5512
+ unreadable.push({
5513
+ anchor: shortPath,
5514
+ reason: `could not parse ${shortPath} as JSON \u2014 a present manifest silent on graceful degradation cannot pass (Extension Rule 2)`
5515
+ });
5516
+ continue;
5517
+ }
5518
+ if (!("graceful_degradation" in parsed)) {
5519
+ return {
5520
+ id: this.id,
5521
+ anchor: shortPath,
5522
+ state: "non-conforming",
5523
+ verdict: `manifest ${shortPath} has no "graceful_degradation" field -> set "graceful_degradation": true, an extension never breaks the host server (Extension Rule 2)`
5524
+ };
5525
+ }
5526
+ const gdValue = parsed.graceful_degradation;
5527
+ if (gdValue !== true) {
5528
+ return {
5529
+ id: this.id,
5530
+ anchor: shortPath,
5531
+ state: "non-conforming",
5532
+ verdict: `manifest "graceful_degradation" is ${JSON.stringify(gdValue)}, not true (positive-control OK) -> set "graceful_degradation": true, an extension never breaks the host server (Extension Rule 2)`
5533
+ };
5534
+ }
4451
5535
  }
4452
- if (site.state === "false") {
4453
- return {
4454
- id: this.id,
4455
- anchor: site.anchor,
4456
- state: "non-conforming",
4457
- verdict: "gracefulDegradation is false, not true (positive-control OK) -> an extension never breaks the host server (Extension Rule 2)"
4458
- };
5536
+ const sites = findTsFactorySites(tree);
5537
+ for (const site of sites) {
5538
+ if (site.state === "unresolved") {
5539
+ unreadable.push({
5540
+ anchor: site.anchor,
5541
+ reason: `cannot read the gracefulDegradation value at ${site.anchor} (got \`${site.value}\`) \u2014 computed/imported/template, graceful degradation unverifiable (Extension Rule 2)`
5542
+ });
5543
+ continue;
5544
+ }
5545
+ if (site.state === "false") {
5546
+ return {
5547
+ id: this.id,
5548
+ anchor: site.anchor,
5549
+ state: "non-conforming",
5550
+ verdict: "gracefulDegradation is false, not true (positive-control OK) -> an extension never breaks the host server (Extension Rule 2)"
5551
+ };
5552
+ }
5553
+ if (site.state === "absent") {
5554
+ return {
5555
+ id: this.id,
5556
+ anchor: site.anchor,
5557
+ state: "non-conforming",
5558
+ verdict: "defineExtension block has no gracefulDegradation flag (positive-control OK) -> declare gracefulDegradation: true (Extension Rule 2)"
5559
+ };
5560
+ }
4459
5561
  }
4460
- if (site.state === "absent") {
5562
+ const total = manifests.length + sites.length;
5563
+ if (total === 0) {
4461
5564
  return {
4462
5565
  id: this.id,
4463
- anchor: site.anchor,
4464
- state: "non-conforming",
4465
- verdict: "defineExtension block has no gracefulDegradation flag (positive-control OK) -> declare gracefulDegradation: true (Extension Rule 2)"
5566
+ anchor: tree.root.split("/").pop() ?? tree.root,
5567
+ state: "absent",
5568
+ verdict: "no extension graceful-degradation flag found (no extension.json manifest, no defineExtension factory) -> declare graceful_degradation: true (Extension Rule 2)"
4466
5569
  };
4467
5570
  }
4468
- }
4469
- const total = manifests.length + sites.length;
4470
- if (total === 0) {
4471
5571
  return {
4472
5572
  id: this.id,
4473
- anchor: tree.root.split("/").pop() ?? tree.root,
4474
- state: "absent",
4475
- verdict: "no extension graceful-degradation flag found (no extension.json manifest, no defineExtension factory) -> declare graceful_degradation: true (Extension Rule 2)"
4476
- };
4477
- }
4478
- return {
4479
- id: this.id,
4480
- anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
4481
- state: "present",
4482
- verdict: `${total} graceful-degradation flag(s) true (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4483
- };
4484
- }
4485
- };
4486
-
4487
- // src/detectors/dExt3.ts
4488
- import { readFileSync as readFileSync12, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
4489
- import { join as join14 } from "path";
4490
- var TYPE_MATCHERS = [
4491
- { type: "middleware", re: /\bmiddleware\b/ },
4492
- { type: "tool-extension", re: /\btool-ext(?:ension)?\b/ },
4493
- { type: "transport", re: /\btransport\b/ }
4494
- ];
4495
- function typesIn(value) {
4496
- const found = [];
4497
- for (const { type, re } of TYPE_MATCHERS) {
4498
- if (re.test(value)) found.push(type);
4499
- }
4500
- return found;
4501
- }
4502
- var TYPE_MIXED = "middleware|transport";
4503
- var TYPE_SINGLE = "middleware";
4504
- function dExt3PositiveControls() {
4505
- return {
4506
- type: {
4507
- detectsMix: typesIn(TYPE_MIXED).length === 2,
4508
- detectsSingle: typesIn(TYPE_SINGLE).length === 1
4509
- }
4510
- };
4511
- }
4512
- var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4513
- function findExtensionManifests3(root) {
4514
- const out = [];
4515
- let base;
4516
- try {
4517
- base = statSync7(root).isDirectory() ? root : join14(root, "..");
4518
- } catch {
4519
- return out;
4520
- }
4521
- const walk2 = (dir) => {
4522
- let entries;
4523
- try {
4524
- entries = readdirSync6(dir);
4525
- } catch {
4526
- return;
4527
- }
4528
- for (const name of entries) {
4529
- const full = join14(dir, name);
4530
- let st;
4531
- try {
4532
- st = statSync7(full);
4533
- } catch {
4534
- continue;
4535
- }
4536
- if (st.isDirectory()) {
4537
- if (SKIP_DIRS4.has(name)) continue;
4538
- walk2(full);
4539
- } else if (name === "extension.json") {
4540
- out.push(full);
4541
- }
5573
+ anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
5574
+ state: "present",
5575
+ verdict: `${total} graceful-degradation flag(s) true (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
5576
+ };
5577
+ })();
5578
+ return settle(this.id, decided, unreadable);
5579
+ }
5580
+ };
5581
+
5582
+ // src/detectors/dExt3.ts
5583
+ var TYPE_MATCHERS = [
5584
+ { type: "middleware", re: /\bmiddleware\b/ },
5585
+ { type: "tool-extension", re: /\btool-ext(?:ension)?\b/ },
5586
+ { type: "transport", re: /\btransport\b/ }
5587
+ ];
5588
+ function typesIn(value) {
5589
+ const found = [];
5590
+ for (const { type, re } of TYPE_MATCHERS) {
5591
+ if (re.test(value)) found.push(type);
5592
+ }
5593
+ return found;
5594
+ }
5595
+ var TYPE_MIXED = "middleware|transport";
5596
+ var TYPE_SINGLE = "middleware";
5597
+ function dExt3PositiveControls() {
5598
+ return {
5599
+ type: {
5600
+ detectsMix: typesIn(TYPE_MIXED).length === 2,
5601
+ detectsSingle: typesIn(TYPE_SINGLE).length === 1
4542
5602
  }
4543
5603
  };
4544
- walk2(base);
4545
- return out;
4546
5604
  }
4547
5605
  var DEFINE_EXTENSION_OPEN_RE2 = /\bdefineExtension\s*\(\s*\{/;
4548
5606
  var TS_TYPE_LITERAL_RE = /\btype:\s*["'`]([^"'`]*)["'`]/;
@@ -4554,7 +5612,7 @@ function findTsTypeSites(tree) {
4554
5612
  for (let i = 0; i < lines.length; i++) {
4555
5613
  const line = lines[i];
4556
5614
  if (line.comment) continue;
4557
- if (!DEFINE_EXTENSION_OPEN_RE2.test(line.raw)) continue;
5615
+ if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE2)) continue;
4558
5616
  const anchor = `${file.rel}:${line.n}`;
4559
5617
  let depth = 0;
4560
5618
  let started = false;
@@ -4565,17 +5623,17 @@ function findTsTypeSites(tree) {
4565
5623
  const bl = lines[j];
4566
5624
  if (!bl.comment) {
4567
5625
  if (!found) {
4568
- const lit = bl.raw.match(TS_TYPE_LITERAL_RE);
5626
+ const lit = matchInCode(file, bl, TS_TYPE_LITERAL_RE);
4569
5627
  if (lit) {
4570
5628
  value = lit[1];
4571
5629
  found = true;
4572
- } else if (TS_TYPE_KEY_RE.test(bl.raw)) {
5630
+ } else if (testInCode(file, bl, TS_TYPE_KEY_RE)) {
4573
5631
  unresolved = true;
4574
5632
  found = true;
4575
5633
  }
4576
5634
  }
4577
- depth += (bl.raw.match(/\{/g) ?? []).length;
4578
- depth -= (bl.raw.match(/\}/g) ?? []).length;
5635
+ depth += countInCode(file, bl, "{");
5636
+ depth -= countInCode(file, bl, "}");
4579
5637
  if (depth > 0) started = true;
4580
5638
  }
4581
5639
  if (started && depth <= 0) break;
@@ -4599,101 +5657,119 @@ var dExt3 = {
4599
5657
  `DExt3 single-type control FAILED: detectMix(\`${TYPE_MIXED}\`)=${controls.type.detectsMix} detectSingle(\`${TYPE_SINGLE}\`)=${controls.type.detectsSingle} \u2014 matcher broken, verdict untrustworthy`
4600
5658
  );
4601
5659
  }
4602
- const declared = /* @__PURE__ */ new Set();
4603
- let firstAnchor2 = null;
4604
- const manifests = findExtensionManifests3(tree.root);
4605
- for (const manifestPath2 of manifests) {
4606
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4607
- let parsed;
4608
- try {
4609
- parsed = JSON.parse(readFileSync12(manifestPath2, "utf8"));
4610
- } catch {
4611
- throw new Error(
4612
- `DExt3 could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable type cannot pass (Extension Rule 3). Fail-closed, not a silent present.`
4613
- );
4614
- }
4615
- if (!("type" in parsed)) {
4616
- throw new Error(
4617
- `DExt3 found a present manifest ${shortPath} with no "type" field \u2014 Extension Rule 3 requires exactly one declared type. Fail-closed, not a silent present.`
4618
- );
4619
- }
4620
- const rawValue = coerceTypeValue(parsed.type);
4621
- if (rawValue === null) {
4622
- throw new Error(
4623
- `DExt3 found a present manifest ${shortPath} whose "type" is neither string nor string[] (got ${typeof parsed.type}) \u2014 type undeterminable (Extension Rule 3). Fail-closed, not a silent present.`
4624
- );
4625
- }
4626
- const found = typesIn(rawValue);
4627
- if (found.length === 0) {
4628
- throw new Error(
4629
- `DExt3 found a present manifest ${shortPath} whose "type" value ${JSON.stringify(parsed.type)} names no known category (middleware|tool-extension|transport) \u2014 type undeterminable (Extension Rule 3). Fail-closed, not a silent present.`
4630
- );
5660
+ const unreadable = [];
5661
+ const decided = (() => {
5662
+ const declared = /* @__PURE__ */ new Set();
5663
+ let firstAnchor2 = null;
5664
+ const manifests = findExtensionManifests(tree);
5665
+ for (const manifestPath2 of manifests) {
5666
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
5667
+ const parsed = readJsonObject(manifestPath2);
5668
+ if (parsed === null) {
5669
+ unreadable.push({
5670
+ anchor: shortPath,
5671
+ reason: `could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable type cannot pass (Extension Rule 3)`
5672
+ });
5673
+ continue;
5674
+ }
5675
+ if (!("type" in parsed)) {
5676
+ return {
5677
+ id: this.id,
5678
+ anchor: shortPath,
5679
+ state: "non-conforming",
5680
+ verdict: `manifest ${shortPath} has no "type" field -> declare exactly one type (middleware|tool-extension|transport) (Extension Rule 3)`
5681
+ };
5682
+ }
5683
+ const rawValue = coerceTypeValue(parsed.type);
5684
+ if (rawValue === null) {
5685
+ return {
5686
+ id: this.id,
5687
+ anchor: shortPath,
5688
+ state: "non-conforming",
5689
+ verdict: `manifest ${shortPath} "type" is neither string nor string[] (got ${typeof parsed.type}) -> declare exactly one type (middleware|tool-extension|transport) (Extension Rule 3)`
5690
+ };
5691
+ }
5692
+ const found = typesIn(rawValue);
5693
+ if (found.length === 0) {
5694
+ return {
5695
+ id: this.id,
5696
+ anchor: shortPath,
5697
+ state: "non-conforming",
5698
+ verdict: `manifest ${shortPath} "type" value ${JSON.stringify(parsed.type)} names no known category (middleware|tool-extension|transport) -> declare exactly one known type (Extension Rule 3)`
5699
+ };
5700
+ }
5701
+ if (found.length > 1) {
5702
+ return {
5703
+ id: this.id,
5704
+ anchor: shortPath,
5705
+ state: "non-conforming",
5706
+ verdict: `manifest "type" ${JSON.stringify(parsed.type)} mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type, split into ${found.length} extensions (Extension Rule 3)`
5707
+ };
5708
+ }
5709
+ firstAnchor2 ??= shortPath;
5710
+ declared.add(found[0]);
5711
+ }
5712
+ const sites = findTsTypeSites(tree);
5713
+ for (const site of sites) {
5714
+ if (site.unresolved) {
5715
+ unreadable.push({
5716
+ anchor: site.anchor,
5717
+ reason: `cannot read the type at ${site.anchor}${site.value === void 0 ? " (no `type:` literal in the defineExtension block)" : ` (got \`${site.value}\`)`} \u2014 computed/imported/template or absent, single-type unverifiable (Extension Rule 3)`
5718
+ });
5719
+ continue;
5720
+ }
5721
+ const raw = site.value ?? "";
5722
+ const found = typesIn(raw);
5723
+ if (found.length === 0) {
5724
+ return {
5725
+ id: this.id,
5726
+ anchor: site.anchor,
5727
+ state: "non-conforming",
5728
+ verdict: `defineExtension type: "${raw}" at ${site.anchor} names no known category (middleware|tool-extension|transport) -> declare exactly one known type (Extension Rule 3)`
5729
+ };
5730
+ }
5731
+ if (found.length > 1) {
5732
+ return {
5733
+ id: this.id,
5734
+ anchor: site.anchor,
5735
+ state: "non-conforming",
5736
+ verdict: `type: "${raw}" mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
5737
+ };
5738
+ }
5739
+ firstAnchor2 ??= site.anchor;
5740
+ declared.add(found[0]);
4631
5741
  }
4632
- if (found.length > 1) {
5742
+ const total = manifests.length + sites.length;
5743
+ if (total === 0) {
4633
5744
  return {
4634
5745
  id: this.id,
4635
- anchor: shortPath,
4636
- state: "non-conforming",
4637
- verdict: `manifest "type" ${JSON.stringify(parsed.type)} mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type, split into ${found.length} extensions (Extension Rule 3)`
5746
+ anchor: tree.root.split("/").pop() ?? tree.root,
5747
+ state: "absent",
5748
+ verdict: "no extension type declared (no extension.json manifest, no defineExtension `type:` literal) -> declare exactly one type (Extension Rule 3)"
4638
5749
  };
4639
5750
  }
4640
- firstAnchor2 ??= shortPath;
4641
- declared.add(found[0]);
4642
- }
4643
- const sites = findTsTypeSites(tree);
4644
- for (const site of sites) {
4645
- if (site.unresolved) {
4646
- throw new Error(
4647
- `DExt3 cannot read the type at ${site.anchor}${site.value === void 0 ? " (no `type:` literal in the defineExtension block)" : ` (got \`${site.value}\`)`} \u2014 computed/imported/template or absent, single-type unverifiable (Extension Rule 3). Fail-closed, not a silent present.`
4648
- );
4649
- }
4650
- const raw = site.value ?? "";
4651
- const found = typesIn(raw);
4652
- if (found.length === 0) {
4653
- throw new Error(
4654
- `DExt3 found a defineExtension type: "${raw}" at ${site.anchor} naming no known category (middleware|tool-extension|transport) \u2014 type undeterminable (Extension Rule 3). Fail-closed, not a silent present.`
4655
- );
4656
- }
4657
- if (found.length > 1) {
5751
+ if (declared.size > 1) {
4658
5752
  return {
4659
5753
  id: this.id,
4660
- anchor: site.anchor,
5754
+ anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
4661
5755
  state: "non-conforming",
4662
- verdict: `type: "${raw}" mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
5756
+ verdict: `extension declares ${declared.size} types across its forms: ${[...declared].join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
4663
5757
  };
4664
5758
  }
4665
- firstAnchor2 ??= site.anchor;
4666
- declared.add(found[0]);
4667
- }
4668
- const total = manifests.length + sites.length;
4669
- if (total === 0) {
4670
- return {
4671
- id: this.id,
4672
- anchor: tree.root.split("/").pop() ?? tree.root,
4673
- state: "absent",
4674
- verdict: "no extension type declared (no extension.json manifest, no defineExtension `type:` literal) -> declare exactly one type (Extension Rule 3)"
4675
- };
4676
- }
4677
- if (declared.size > 1) {
4678
5759
  return {
4679
5760
  id: this.id,
4680
5761
  anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
4681
- state: "non-conforming",
4682
- verdict: `extension declares ${declared.size} types across its forms: ${[...declared].join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
5762
+ state: "present",
5763
+ verdict: `single type "${[...declared][0]}" declared across ${total} form(s) (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4683
5764
  };
4684
- }
4685
- return {
4686
- id: this.id,
4687
- anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
4688
- state: "present",
4689
- verdict: `single type "${[...declared][0]}" declared across ${total} form(s) (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4690
- };
5765
+ })();
5766
+ return settle(this.id, decided, unreadable);
4691
5767
  }
4692
5768
  };
4693
5769
 
4694
5770
  // src/detectors/dExt4.ts
4695
- import { readFileSync as readFileSync13, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
4696
- import { join as join15 } from "path";
5771
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
5772
+ import { join as join13 } from "path";
4697
5773
  function canonical(name) {
4698
5774
  return name.replace(/[-_]/g, "").toLowerCase();
4699
5775
  }
@@ -4712,71 +5788,40 @@ function dExt4PositiveControls() {
4712
5788
  }
4713
5789
  };
4714
5790
  }
4715
- var SKIP_DIRS5 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4716
- function findExtensionManifests4(root) {
5791
+ function findHooksDirs(tree) {
5792
+ const root = tree.root;
4717
5793
  const out = [];
4718
5794
  let base;
4719
5795
  try {
4720
- base = statSync8(root).isDirectory() ? root : join15(root, "..");
5796
+ base = statSync6(root).isDirectory() ? root : join13(root, "..");
4721
5797
  } catch {
4722
5798
  return out;
4723
5799
  }
4724
5800
  const walk2 = (dir) => {
4725
5801
  let entries;
4726
5802
  try {
4727
- entries = readdirSync7(dir);
5803
+ entries = readdirSync5(dir);
4728
5804
  } catch {
4729
5805
  return;
4730
5806
  }
4731
5807
  for (const name of entries) {
4732
- const full = join15(dir, name);
5808
+ const full = join13(dir, name);
4733
5809
  let st;
4734
5810
  try {
4735
- st = statSync8(full);
5811
+ st = statSync6(full);
4736
5812
  } catch {
4737
5813
  continue;
4738
5814
  }
4739
- if (st.isDirectory()) {
4740
- if (SKIP_DIRS5.has(name)) continue;
4741
- walk2(full);
4742
- } else if (name === "extension.json") {
4743
- out.push(full);
4744
- }
4745
- }
4746
- };
4747
- walk2(base);
4748
- return out;
4749
- }
4750
- function findHooksDirs(root) {
4751
- const out = [];
4752
- let base;
4753
- try {
4754
- base = statSync8(root).isDirectory() ? root : join15(root, "..");
4755
- } catch {
4756
- return out;
4757
- }
4758
- const walk2 = (dir) => {
4759
- let entries;
4760
- try {
4761
- entries = readdirSync7(dir);
4762
- } catch {
4763
- return;
4764
- }
4765
- for (const name of entries) {
4766
- const full = join15(dir, name);
4767
- let st;
4768
- try {
4769
- st = statSync8(full);
4770
- } catch {
5815
+ if (!st.isDirectory()) continue;
5816
+ if (SKIP_DIRS.has(name)) {
5817
+ recordSkip(tree, full, base);
4771
5818
  continue;
4772
5819
  }
4773
- if (!st.isDirectory()) continue;
4774
- if (SKIP_DIRS5.has(name)) continue;
4775
5820
  if (name === "hooks") {
4776
5821
  const tokens = /* @__PURE__ */ new Set();
4777
5822
  let files;
4778
5823
  try {
4779
- files = readdirSync7(full);
5824
+ files = readdirSync5(full);
4780
5825
  } catch {
4781
5826
  files = [];
4782
5827
  }
@@ -4805,7 +5850,7 @@ function findFactorySites(tree) {
4805
5850
  for (let i = 0; i < lines.length; i++) {
4806
5851
  const line = lines[i];
4807
5852
  if (line.comment) continue;
4808
- if (LIFECYCLE_OBJ_RE.test(line.raw)) {
5853
+ if (testInCode(file, line, LIFECYCLE_OBJ_RE)) {
4809
5854
  const anchor = `${file.rel}:${line.n}`;
4810
5855
  const tokens = /* @__PURE__ */ new Set();
4811
5856
  let depth = 0;
@@ -4813,20 +5858,19 @@ function findFactorySites(tree) {
4813
5858
  for (let j = i; j < lines.length; j++) {
4814
5859
  const bl = lines[j];
4815
5860
  if (bl.comment) continue;
4816
- const raw = bl.raw;
4817
5861
  if (started && depth === 1) {
4818
- const k = raw.match(OBJECT_KEY_RE);
5862
+ const k = matchInCode(file, bl, OBJECT_KEY_RE);
4819
5863
  if (k) tokens.add(canonical(k[1]));
4820
5864
  }
4821
- depth += (raw.match(/\{/g) ?? []).length;
4822
- depth -= (raw.match(/\}/g) ?? []).length;
5865
+ depth += countInCode(file, bl, "{");
5866
+ depth -= countInCode(file, bl, "}");
4823
5867
  if (depth > 0) started = true;
4824
5868
  if (started && depth <= 0) break;
4825
5869
  }
4826
5870
  sites.push({ anchor, tokens });
4827
5871
  continue;
4828
5872
  }
4829
- if (LIFECYCLE_KEY_RE.test(line.raw)) {
5873
+ if (testInCode(file, line, LIFECYCLE_KEY_RE)) {
4830
5874
  unresolved.push(`${file.rel}:${line.n}`);
4831
5875
  }
4832
5876
  }
@@ -4836,7 +5880,7 @@ function findFactorySites(tree) {
4836
5880
  function manifestHookTokens(raw) {
4837
5881
  const set = /* @__PURE__ */ new Set();
4838
5882
  if (!Array.isArray(raw)) {
4839
- throw new Error("not-array");
5883
+ return null;
4840
5884
  }
4841
5885
  for (const h of raw) {
4842
5886
  if (typeof h === "string") set.add(canonical(h));
@@ -4852,83 +5896,91 @@ var dExt4 = {
4852
5896
  `DExt4 lifecycle-hooks control FAILED: unify(${HOOK_SNAKE}/${HOOK_KEBAB}/${HOOK_CAMEL})=${controls.hooks.unifiesSpellings} distinct(${HOOK_SNAKE}!=${HOOK_OTHER})=${controls.hooks.keepsDistinct} \u2014 normaliser broken, verdict untrustworthy`
4853
5897
  );
4854
5898
  }
4855
- const hooksDirs = findHooksDirs(tree.root);
4856
- const { sites, unresolved } = findFactorySites(tree);
4857
- if (unresolved.length > 0) {
4858
- throw new Error(
4859
- `DExt4 cannot read the lifecycle implementation at ${unresolved.length} TS site(s): ${unresolved.join(", ")} \u2014 value is computed/imported/template, not an object literal, so the implemented hooks are unverifiable (Extension Rule 4). Fail-closed, not a silent present.`
4860
- );
4861
- }
4862
- const implemented = /* @__PURE__ */ new Set();
4863
- for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
4864
- for (const s of sites) for (const t of s.tokens) implemented.add(t);
4865
- const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
4866
- const manifests = findExtensionManifests4(tree.root);
4867
- let declaredAnyLifecycle = false;
4868
- for (const manifestPath2 of manifests) {
4869
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4870
- let parsed;
4871
- try {
4872
- parsed = JSON.parse(readFileSync13(manifestPath2, "utf8"));
4873
- } catch {
4874
- throw new Error(
4875
- `DExt4 could not parse ${shortPath} as JSON \u2014 a present manifest whose lifecycle cannot be read cannot pass (Extension Rule 4). Fail-closed, not a silent present.`
4876
- );
4877
- }
4878
- if (!("lifecycle" in parsed)) continue;
4879
- let declared;
4880
- try {
4881
- declared = manifestHookTokens(parsed.lifecycle);
4882
- } catch {
4883
- throw new Error(
4884
- `DExt4 found a present manifest ${shortPath} whose "lifecycle" is not an array (got ${typeof parsed.lifecycle}) \u2014 the declared hooks are undeterminable (Extension Rule 4). Fail-closed, not a silent present.`
4885
- );
4886
- }
4887
- if (declared.size === 0) continue;
4888
- declaredAnyLifecycle = true;
4889
- if (!implSurfacePresent) {
4890
- throw new Error(
4891
- `DExt4 found a present manifest ${shortPath} declaring lifecycle hooks while NO implementation surface exists (no hooks/ directory, no factory lifecycle object) \u2014 the parity is undeterminable (Extension Rule 4). Fail-closed, not a silent present.`
4892
- );
5899
+ const unreadable = [];
5900
+ const decided = (() => {
5901
+ const hooksDirs = findHooksDirs(tree);
5902
+ const { sites, unresolved } = findFactorySites(tree);
5903
+ for (const anchor of unresolved) {
5904
+ unreadable.push({
5905
+ anchor,
5906
+ reason: `cannot read the lifecycle implementation at ${anchor} \u2014 value is computed/imported/template, not an object literal, so the implemented hooks are unverifiable (Extension Rule 4)`
5907
+ });
4893
5908
  }
4894
- const phantom = [...declared].filter((h) => !implemented.has(h));
4895
- if (phantom.length > 0) {
4896
- return {
4897
- id: this.id,
4898
- anchor: shortPath,
4899
- state: "non-conforming",
4900
- verdict: `manifest "lifecycle" declares hook(s) [${phantom.join(", ")}] with no implementation (implemented: [${[...implemented].join(", ") || "none"}], positive-control OK) -> a declared hook without implementation is a phantom hook (Extension Rule 4)`
4901
- };
5909
+ const implemented = /* @__PURE__ */ new Set();
5910
+ for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
5911
+ for (const s of sites) for (const t of s.tokens) implemented.add(t);
5912
+ const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
5913
+ const manifests = findExtensionManifests(tree);
5914
+ let declaredAnyLifecycle = false;
5915
+ for (const manifestPath2 of manifests) {
5916
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
5917
+ const parsed = readJsonObject(manifestPath2);
5918
+ if (parsed === null) {
5919
+ unreadable.push({
5920
+ anchor: shortPath,
5921
+ reason: `could not parse ${shortPath} as JSON \u2014 a present manifest whose lifecycle cannot be read cannot pass (Extension Rule 4)`
5922
+ });
5923
+ continue;
5924
+ }
5925
+ if (!("lifecycle" in parsed)) continue;
5926
+ const declared = manifestHookTokens(parsed.lifecycle);
5927
+ if (declared === null) {
5928
+ return {
5929
+ id: this.id,
5930
+ anchor: shortPath,
5931
+ state: "non-conforming",
5932
+ verdict: `manifest ${shortPath} "lifecycle" is not an array (got ${typeof parsed.lifecycle}) -> declare the hooks as an array of hook names (Extension Rule 4)`
5933
+ };
5934
+ }
5935
+ if (declared.size === 0) continue;
5936
+ declaredAnyLifecycle = true;
5937
+ if (!implSurfacePresent) {
5938
+ unreadable.push({
5939
+ anchor: shortPath,
5940
+ reason: `present manifest ${shortPath} declares lifecycle hooks while NO implementation surface exists (no hooks/ directory, no factory lifecycle object) \u2014 the parity is undeterminable (Extension Rule 4)`
5941
+ });
5942
+ continue;
5943
+ }
5944
+ const phantom = [...declared].filter((h) => !implemented.has(h));
5945
+ if (phantom.length > 0) {
5946
+ return {
5947
+ id: this.id,
5948
+ anchor: shortPath,
5949
+ state: "non-conforming",
5950
+ verdict: `manifest "lifecycle" declares hook(s) [${phantom.join(", ")}] with no implementation (implemented: [${[...implemented].join(", ") || "none"}], positive-control OK) -> a declared hook without implementation is a phantom hook (Extension Rule 4)`
5951
+ };
5952
+ }
4902
5953
  }
4903
- }
4904
- if (!declaredAnyLifecycle) {
4905
- if (manifests.length === 0 && !implSurfacePresent) {
5954
+ if (!declaredAnyLifecycle) {
5955
+ if (manifests.length === 0 && !implSurfacePresent) {
5956
+ return {
5957
+ id: this.id,
5958
+ anchor: tree.root.split("/").pop() ?? tree.root,
5959
+ state: "absent",
5960
+ verdict: "no lifecycle hooks declared (no extension.json manifest `lifecycle` array) -> declare the hooks the extension implements (Extension Rule 4)"
5961
+ };
5962
+ }
4906
5963
  return {
4907
5964
  id: this.id,
4908
- anchor: tree.root.split("/").pop() ?? tree.root,
4909
- state: "absent",
4910
- verdict: "no lifecycle hooks declared (no extension.json manifest `lifecycle` array) -> declare the hooks the extension implements (Extension Rule 4)"
5965
+ anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
5966
+ state: "present",
5967
+ verdict: `no manifest lifecycle hooks declared, nothing to reconcile (implemented: [${[...implemented].join(", ") || "none"}], positive-control OK)`
4911
5968
  };
4912
5969
  }
4913
5970
  return {
4914
5971
  id: this.id,
4915
- anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
5972
+ anchor: manifests[0].split("/").slice(-2).join("/"),
4916
5973
  state: "present",
4917
- verdict: `no manifest lifecycle hooks declared, nothing to reconcile (implemented: [${[...implemented].join(", ") || "none"}], positive-control OK)`
5974
+ verdict: `every declared lifecycle hook is implemented (declared \u2286 implemented: [${[...implemented].join(", ")}], hooksDir=${hooksDirs.length}, factory=${sites.length}, positive-control OK)`
4918
5975
  };
4919
- }
4920
- return {
4921
- id: this.id,
4922
- anchor: manifests[0].split("/").slice(-2).join("/"),
4923
- state: "present",
4924
- verdict: `every declared lifecycle hook is implemented (declared \u2286 implemented: [${[...implemented].join(", ")}], hooksDir=${hooksDirs.length}, factory=${sites.length}, positive-control OK)`
4925
- };
5976
+ })();
5977
+ return settle(this.id, decided, unreadable);
4926
5978
  }
4927
5979
  };
4928
5980
 
4929
5981
  // src/detectors/dExt5.ts
4930
- import { existsSync as existsSync12, readFileSync as readFileSync14, readdirSync as readdirSync8, statSync as statSync9 } from "fs";
4931
- import { dirname as dirname7, join as join16 } from "path";
5982
+ import { existsSync as existsSync12 } from "fs";
5983
+ import { dirname as dirname7, join as join14 } from "path";
4932
5984
  function nonEmptyString(v) {
4933
5985
  return typeof v === "string" && v.trim().length > 0;
4934
5986
  }
@@ -4942,41 +5994,6 @@ function dExt5PositiveControls() {
4942
5994
  }
4943
5995
  };
4944
5996
  }
4945
- var SKIP_DIRS6 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4946
- function findExtensionManifests5(root) {
4947
- const out = [];
4948
- let base;
4949
- try {
4950
- base = statSync9(root).isDirectory() ? root : join16(root, "..");
4951
- } catch {
4952
- return out;
4953
- }
4954
- const walk2 = (dir) => {
4955
- let entries;
4956
- try {
4957
- entries = readdirSync8(dir);
4958
- } catch {
4959
- return;
4960
- }
4961
- for (const name of entries) {
4962
- const full = join16(dir, name);
4963
- let st;
4964
- try {
4965
- st = statSync9(full);
4966
- } catch {
4967
- continue;
4968
- }
4969
- if (st.isDirectory()) {
4970
- if (SKIP_DIRS6.has(name)) continue;
4971
- walk2(full);
4972
- } else if (name === "extension.json") {
4973
- out.push(full);
4974
- }
4975
- }
4976
- };
4977
- walk2(base);
4978
- return out;
4979
- }
4980
5997
  function isToolExtension(type) {
4981
5998
  return typeof type === "string" && type.includes("tool-extension");
4982
5999
  }
@@ -4989,7 +6006,7 @@ var dExt5 = {
4989
6006
  `DExt5 bilingual control FAILED: accept(\`${FR_POSITIVE}\`)=${controls.descriptionFr.positive} reject(empty)=${!controls.descriptionFr.negative} \u2014 FR-string matcher broken, absence verdict untrustworthy`
4990
6007
  );
4991
6008
  }
4992
- const manifests = findExtensionManifests5(tree.root);
6009
+ const manifests = findExtensionManifests(tree);
4993
6010
  if (manifests.length === 0) {
4994
6011
  return {
4995
6012
  id: this.id,
@@ -5008,10 +6025,8 @@ var dExt5 = {
5008
6025
  reason,
5009
6026
  verdict: `could not judge: ${reason}`
5010
6027
  });
5011
- let parsed;
5012
- try {
5013
- parsed = JSON.parse(readFileSync14(manifestPath2, "utf8"));
5014
- } catch {
6028
+ const parsed = readJsonObject(manifestPath2);
6029
+ if (parsed === null) {
5015
6030
  return cannotRead(
5016
6031
  `${shortPath} does not parse as JSON \u2014 the FR surface of a present manifest cannot be read (Extension Rule 5)`
5017
6032
  );
@@ -5025,7 +6040,7 @@ var dExt5 = {
5025
6040
  if (!nonEmptyString(parsed.description_fr)) {
5026
6041
  missing.push("description_fr (extension.json)");
5027
6042
  }
5028
- if (!existsSync12(join16(root, "README.fr.md"))) {
6043
+ if (!existsSync12(join14(root, "README.fr.md"))) {
5029
6044
  missing.push("README.fr.md");
5030
6045
  }
5031
6046
  if (isToolExtension(parsed.type) && "tools" in parsed) {
@@ -5068,8 +6083,8 @@ var dExt5 = {
5068
6083
  };
5069
6084
 
5070
6085
  // src/detectors/dExt7.ts
5071
- import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync9, statSync as statSync10 } from "fs";
5072
- import { dirname as dirname8, join as join17 } from "path";
6086
+ import { existsSync as existsSync13 } from "fs";
6087
+ import { dirname as dirname8, join as join15 } from "path";
5073
6088
  var ABSENCE_FLAG_FIELDS = [
5074
6089
  "extensionAbsent",
5075
6090
  "extension_absent",
@@ -5104,48 +6119,13 @@ var ABSENCE_POSITIVE_LABEL = {
5104
6119
  op: "callTool"
5105
6120
  };
5106
6121
  var ABSENCE_NEGATIVE = { id: "ping-1", op: "callTool", tool: "ping" };
5107
- function dExt7PositiveControls() {
5108
- return {
5109
- flag: isAbsenceCase(ABSENCE_POSITIVE_FLAG),
5110
- token: isAbsenceCase(ABSENCE_POSITIVE_TOKEN),
5111
- label: isAbsenceCase(ABSENCE_POSITIVE_LABEL),
5112
- negative: isAbsenceCase(ABSENCE_NEGATIVE)
5113
- };
5114
- }
5115
- var SKIP_DIRS7 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5116
- function findExtensionManifests6(root) {
5117
- const out = [];
5118
- let base;
5119
- try {
5120
- base = statSync10(root).isDirectory() ? root : join17(root, "..");
5121
- } catch {
5122
- return out;
5123
- }
5124
- const walk2 = (dir) => {
5125
- let entries;
5126
- try {
5127
- entries = readdirSync9(dir);
5128
- } catch {
5129
- return;
5130
- }
5131
- for (const name of entries) {
5132
- const full = join17(dir, name);
5133
- let st;
5134
- try {
5135
- st = statSync10(full);
5136
- } catch {
5137
- continue;
5138
- }
5139
- if (st.isDirectory()) {
5140
- if (SKIP_DIRS7.has(name)) continue;
5141
- walk2(full);
5142
- } else if (name === "extension.json") {
5143
- out.push(full);
5144
- }
5145
- }
6122
+ function dExt7PositiveControls() {
6123
+ return {
6124
+ flag: isAbsenceCase(ABSENCE_POSITIVE_FLAG),
6125
+ token: isAbsenceCase(ABSENCE_POSITIVE_TOKEN),
6126
+ label: isAbsenceCase(ABSENCE_POSITIVE_LABEL),
6127
+ negative: isAbsenceCase(ABSENCE_NEGATIVE)
5146
6128
  };
5147
- walk2(base);
5148
- return out;
5149
6129
  }
5150
6130
  var dExt7 = {
5151
6131
  id: "DExt7 eval-coverage",
@@ -5156,7 +6136,7 @@ var dExt7 = {
5156
6136
  `DExt7 absence-matcher control FAILED: flag=${controls.flag} token=${controls.token} label=${controls.label} reject(normal)=${!controls.negative} \u2014 absence matcher broken, eval-coverage verdict untrustworthy`
5157
6137
  );
5158
6138
  }
5159
- const manifests = findExtensionManifests6(tree.root);
6139
+ const manifests = findExtensionManifests(tree);
5160
6140
  if (manifests.length === 0) {
5161
6141
  return {
5162
6142
  id: this.id,
@@ -5165,66 +6145,79 @@ var dExt7 = {
5165
6145
  verdict: "no extension.json manifest found -> declare an extension with an eval suite (>=3 cases incl. 1 'extension absente') (Extension Rule 7)"
5166
6146
  };
5167
6147
  }
5168
- for (const manifestPath2 of manifests) {
5169
- const extDir = dirname8(manifestPath2);
5170
- const extLabel = extDir.split("/").pop() ?? extDir;
5171
- const evalsPath = join17(extDir, "evals", "evals.json");
5172
- const shortPath = `${extLabel}/evals/evals.json`;
5173
- if (!existsSync13(evalsPath)) {
5174
- return {
5175
- id: this.id,
5176
- anchor: `${extLabel}/extension.json`,
5177
- state: "non-conforming",
5178
- verdict: `no evals/evals.json for extension "${extLabel}" -> add an eval suite with >=3 cases incl. 1 'extension absente' (Extension Rule 7)`
5179
- };
5180
- }
5181
- let cases;
5182
- try {
5183
- const raw = readFileSync15(evalsPath, "utf8");
5184
- const parsed = JSON.parse(raw);
6148
+ const unreadable = [];
6149
+ const decided = (() => {
6150
+ for (const manifestPath2 of manifests) {
6151
+ const extDir = dirname8(manifestPath2);
6152
+ const extLabel = extDir.split("/").pop() ?? extDir;
6153
+ const evalsPath = join15(extDir, "evals", "evals.json");
6154
+ const shortPath = `${extLabel}/evals/evals.json`;
6155
+ if (!existsSync13(evalsPath)) {
6156
+ return {
6157
+ id: this.id,
6158
+ anchor: `${extLabel}/extension.json`,
6159
+ state: "non-conforming",
6160
+ verdict: `no evals/evals.json for extension "${extLabel}" -> add an eval suite with >=3 cases incl. 1 'extension absente' (Extension Rule 7)`
6161
+ };
6162
+ }
6163
+ const parsed = readJsonObject(evalsPath);
6164
+ if (parsed === null) {
6165
+ unreadable.push({
6166
+ anchor: shortPath,
6167
+ reason: `could not parse ${shortPath}: not readable as a JSON object \u2014 a present eval surface that cannot be read cannot pass (Extension Rule 7)`
6168
+ });
6169
+ continue;
6170
+ }
5185
6171
  if (!Array.isArray(parsed.cases)) {
5186
- throw new Error("`cases` is not an array");
6172
+ return {
6173
+ id: this.id,
6174
+ anchor: shortPath,
6175
+ state: "non-conforming",
6176
+ verdict: `${shortPath} "cases" is not an array (got ${typeof parsed.cases}) -> declare cases as an array of eval cases (Extension Rule 7)`
6177
+ };
5187
6178
  }
5188
- cases = parsed.cases;
5189
- } catch (err) {
5190
- throw new Error(
5191
- `DExt7 could not parse ${shortPath}: ${err instanceof Error ? err.message : String(err)} \u2014 a present eval surface that cannot be read cannot pass (Extension Rule 7). Fail-closed, not a silent present.`
6179
+ const cases = parsed.cases;
6180
+ const badCase = cases.findIndex(
6181
+ (c) => c === null || typeof c !== "object" || Array.isArray(c)
5192
6182
  );
5193
- }
5194
- cases.forEach((c, i) => {
5195
- if (c === null || typeof c !== "object" || Array.isArray(c)) {
5196
- throw new Error(
5197
- `DExt7 found ${shortPath} cases[${i}] is not an object (got ${c === null ? "null" : Array.isArray(c) ? "array" : typeof c}) \u2014 case shape undeterminable, absence-case presence cannot be decided (Extension Rule 7). Fail-closed, not a silent present.`
5198
- );
6183
+ if (badCase !== -1) {
6184
+ const c = cases[badCase];
6185
+ return {
6186
+ id: this.id,
6187
+ anchor: shortPath,
6188
+ state: "non-conforming",
6189
+ verdict: `${shortPath} cases[${badCase}] is not an object (got ${c === null ? "null" : Array.isArray(c) ? "array" : typeof c}) -> every eval case must be an object (Extension Rule 7)`
6190
+ };
6191
+ }
6192
+ const count = cases.length;
6193
+ const hasAbsence = cases.some(isAbsenceCase);
6194
+ const failures = [];
6195
+ if (count < 3) failures.push(`only ${count} case(s) (need >=3)`);
6196
+ if (!hasAbsence) failures.push("no 'extension absente'/degradation case");
6197
+ if (failures.length > 0) {
6198
+ return {
6199
+ id: this.id,
6200
+ anchor: `${shortPath}`,
6201
+ state: "non-conforming",
6202
+ verdict: `eval suite for "${extLabel}": ${failures.join("; ")} (positive-control OK) -> Extension Rule 7 needs >=3 cases incl. 1 'extension absente'`
6203
+ };
5199
6204
  }
5200
- });
5201
- const count = cases.length;
5202
- const hasAbsence = cases.some(isAbsenceCase);
5203
- const failures = [];
5204
- if (count < 3) failures.push(`only ${count} case(s) (need >=3)`);
5205
- if (!hasAbsence) failures.push("no 'extension absente'/degradation case");
5206
- if (failures.length > 0) {
5207
- return {
5208
- id: this.id,
5209
- anchor: `${shortPath}`,
5210
- state: "non-conforming",
5211
- verdict: `eval suite for "${extLabel}": ${failures.join("; ")} (positive-control OK) -> Extension Rule 7 needs >=3 cases incl. 1 'extension absente'`
5212
- };
5213
6205
  }
5214
- }
5215
- const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
5216
- return {
5217
- id: this.id,
5218
- anchor: `${firstLabel}/evals/evals.json`,
5219
- state: "present",
5220
- verdict: `eval suite complete across ${manifests.length} extension(s): >=3 cases incl. an 'extension absente'/degradation case (positive-control OK)`
5221
- };
6206
+ const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
6207
+ return {
6208
+ id: this.id,
6209
+ anchor: `${firstLabel}/evals/evals.json`,
6210
+ state: "present",
6211
+ verdict: `eval suite complete across ${manifests.length} extension(s): >=3 cases incl. an 'extension absente'/degradation case (positive-control OK)`
6212
+ };
6213
+ })();
6214
+ return settle(this.id, decided, unreadable);
5222
6215
  }
5223
6216
  };
5224
6217
 
5225
6218
  // src/detectors/dExt8.ts
5226
- import { existsSync as existsSync14, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync11 } from "fs";
5227
- import { dirname as dirname9, join as join18 } from "path";
6219
+ import { existsSync as existsSync14 } from "fs";
6220
+ import { dirname as dirname9, join as join16 } from "path";
5228
6221
  var SEMVER_RE2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
5229
6222
  var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
5230
6223
  function dExt8PositiveControl() {
@@ -5234,41 +6227,6 @@ function dExt8PositiveControl() {
5234
6227
  matched: SEMVER_RE2.test(POSITIVE_CONTROL5)
5235
6228
  };
5236
6229
  }
5237
- var SKIP_DIRS8 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5238
- function findExtensionManifests7(root) {
5239
- const out = [];
5240
- let base;
5241
- try {
5242
- base = statSync11(root).isDirectory() ? root : join18(root, "..");
5243
- } catch {
5244
- return out;
5245
- }
5246
- const walk2 = (dir) => {
5247
- let entries;
5248
- try {
5249
- entries = readdirSync10(dir);
5250
- } catch {
5251
- return;
5252
- }
5253
- for (const name of entries) {
5254
- const full = join18(dir, name);
5255
- let st;
5256
- try {
5257
- st = statSync11(full);
5258
- } catch {
5259
- continue;
5260
- }
5261
- if (st.isDirectory()) {
5262
- if (SKIP_DIRS8.has(name)) continue;
5263
- walk2(full);
5264
- } else if (name === "extension.json") {
5265
- out.push(full);
5266
- }
5267
- }
5268
- };
5269
- walk2(base);
5270
- return out;
5271
- }
5272
6230
  var dExt8 = {
5273
6231
  id: "DExt8 semver+changelog",
5274
6232
  run(tree) {
@@ -5278,7 +6236,7 @@ var dExt8 = {
5278
6236
  `DExt8 positive control FAILED: pattern /${control.pattern}/ did not match sample \`${control.sample}\` \u2014 matcher broken, verdict untrustworthy`
5279
6237
  );
5280
6238
  }
5281
- const manifests = findExtensionManifests7(tree.root);
6239
+ const manifests = findExtensionManifests(tree);
5282
6240
  if (manifests.length === 0) {
5283
6241
  return {
5284
6242
  id: this.id,
@@ -5287,101 +6245,119 @@ var dExt8 = {
5287
6245
  verdict: "no extension.json manifest found -> declare an extension with a valid SemVer version + CHANGELOG.md (Extension Rule 8)"
5288
6246
  };
5289
6247
  }
6248
+ const unreadable = [];
6249
+ let finding;
6250
+ let judgedConforming = 0;
5290
6251
  for (const manifestPath2 of manifests) {
5291
6252
  const extDir = dirname9(manifestPath2);
5292
6253
  const extLabel = extDir.split("/").pop() ?? extDir;
5293
- const pkgPath = join18(extDir, "package.json");
6254
+ const pkgPath = join16(extDir, "package.json");
5294
6255
  const shortPkgPath = `${extLabel}/package.json`;
5295
6256
  if (!existsSync14(pkgPath)) {
5296
- throw new Error(
5297
- `DExt8 could not locate ${shortPkgPath} for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8). Fail-closed, not a silent present.`
5298
- );
6257
+ unreadable.push({
6258
+ anchor: shortPkgPath,
6259
+ reason: `could not locate ${shortPkgPath} for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8)`
6260
+ });
6261
+ continue;
5299
6262
  }
5300
- let pkgJson;
5301
- try {
5302
- pkgJson = JSON.parse(readFileSync16(pkgPath, "utf8"));
5303
- } catch {
5304
- throw new Error(
5305
- `DExt8 could not parse ${shortPkgPath} as JSON for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8). Fail-closed, not a silent present.`
5306
- );
6263
+ const pkgJson = readJsonObject(pkgPath);
6264
+ if (pkgJson === null) {
6265
+ unreadable.push({
6266
+ anchor: shortPkgPath,
6267
+ reason: `could not parse ${shortPkgPath} as JSON for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8)`
6268
+ });
6269
+ continue;
5307
6270
  }
5308
6271
  const version = pkgJson.version;
5309
6272
  if ("version" in pkgJson && typeof version !== "string") {
5310
- throw new Error(
5311
- `DExt8 found ${shortPkgPath} whose "version" is not a string (got ${typeof version}) for extension "${extLabel}" \u2014 cannot verify SemVer (Extension Rule 8). Fail-closed, not a silent present.`
5312
- );
6273
+ finding ??= {
6274
+ id: this.id,
6275
+ anchor: shortPkgPath,
6276
+ state: "non-conforming",
6277
+ verdict: `"version" in ${shortPkgPath} is not a string (got ${typeof version}) for extension "${extLabel}" -> set a valid SemVer version string (Extension Rule 8)`
6278
+ };
6279
+ continue;
5313
6280
  }
5314
6281
  if (typeof version !== "string" || version.trim().length === 0) {
5315
- return {
6282
+ finding ??= {
5316
6283
  id: this.id,
5317
6284
  anchor: shortPkgPath,
5318
6285
  state: "non-conforming",
5319
6286
  verdict: `no "version" field in ${shortPkgPath} for extension "${extLabel}" -> set a valid SemVer version (Extension Rule 8)`
5320
6287
  };
6288
+ continue;
5321
6289
  }
5322
6290
  const validSemver = SEMVER_RE2.test(version);
5323
- const changelogPath = join18(extDir, "CHANGELOG.md");
6291
+ const changelogPath = join16(extDir, "CHANGELOG.md");
5324
6292
  const hasChangelog = existsSync14(changelogPath);
5325
6293
  if (!validSemver) {
5326
- return {
6294
+ finding ??= {
5327
6295
  id: this.id,
5328
6296
  anchor: shortPkgPath,
5329
6297
  state: "non-conforming",
5330
6298
  verdict: `"version": "${version}" is not valid SemVer for extension "${extLabel}" (positive-control OK) -> use MAJOR.MINOR.PATCH (Extension Rule 8)`
5331
6299
  };
6300
+ continue;
5332
6301
  }
5333
6302
  if (!hasChangelog) {
5334
- return {
6303
+ finding ??= {
5335
6304
  id: this.id,
5336
6305
  anchor: shortPkgPath,
5337
6306
  state: "absent",
5338
6307
  verdict: `no CHANGELOG.md next to ${shortPkgPath} for extension "${extLabel}" (positive-control OK) -> add CHANGELOG.md, update it each release (Extension Rule 8)`
5339
6308
  };
6309
+ continue;
5340
6310
  }
6311
+ judgedConforming++;
5341
6312
  }
6313
+ if (finding) return settle(this.id, finding, unreadable);
5342
6314
  const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
5343
- return {
6315
+ const conforming = {
5344
6316
  id: this.id,
5345
6317
  anchor: `${firstLabel}/package.json`,
5346
6318
  state: "present",
5347
- verdict: `SemVer valid + CHANGELOG.md present across ${manifests.length} extension(s) (positive-control OK)`
6319
+ verdict: `SemVer valid + CHANGELOG.md present across ${judgedConforming} of ${manifests.length} extension(s) (positive-control OK)`
5348
6320
  };
6321
+ return settle(this.id, conforming, unreadable);
5349
6322
  }
5350
6323
  };
5351
6324
 
5352
6325
  // src/detectors/dExt9.ts
5353
- import { readFileSync as readFileSync17, readdirSync as readdirSync11, statSync as statSync12 } from "fs";
5354
- import { join as join19 } from "path";
5355
- var SKIP_DIRS9 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5356
- function collectHooksDirSources(root) {
6326
+ import { readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
6327
+ import { join as join17 } from "path";
6328
+ function collectHooksDirSources(tree, unreadable) {
6329
+ const root = tree.root;
5357
6330
  const out = [];
5358
6331
  let base;
5359
6332
  try {
5360
- base = statSync12(root).isDirectory() ? root : join19(root, "..");
6333
+ base = statSync7(root).isDirectory() ? root : join17(root, "..");
5361
6334
  } catch {
5362
6335
  return out;
5363
6336
  }
5364
6337
  const walk2 = (dir) => {
5365
6338
  let entries;
5366
6339
  try {
5367
- entries = readdirSync11(dir);
6340
+ entries = readdirSync6(dir);
5368
6341
  } catch {
5369
6342
  return;
5370
6343
  }
5371
6344
  for (const name of entries) {
5372
- const full = join19(dir, name);
6345
+ const full = join17(dir, name);
5373
6346
  let st;
5374
6347
  try {
5375
- st = statSync12(full);
6348
+ st = statSync7(full);
5376
6349
  } catch {
5377
6350
  continue;
5378
6351
  }
5379
6352
  if (!st.isDirectory()) continue;
5380
- if (SKIP_DIRS9.has(name)) continue;
6353
+ if (SKIP_DIRS.has(name)) {
6354
+ recordSkip(tree, full, base);
6355
+ continue;
6356
+ }
5381
6357
  if (name === "hooks") {
5382
6358
  let files;
5383
6359
  try {
5384
- files = readdirSync11(full);
6360
+ files = readdirSync6(full);
5385
6361
  } catch {
5386
6362
  files = [];
5387
6363
  }
@@ -5389,14 +6365,17 @@ function collectHooksDirSources(root) {
5389
6365
  if (!f.endsWith(".ts") || f.endsWith(".d.ts")) continue;
5390
6366
  const stem = f.replace(/\.ts$/, "");
5391
6367
  if (stem === "index") continue;
5392
- const filePath = join19(full, f);
6368
+ const filePath = join17(full, f);
5393
6369
  let body;
5394
6370
  try {
5395
- body = readFileSync17(filePath, "utf8");
6371
+ body = readFileSync10(filePath, "utf8");
5396
6372
  } catch {
5397
- throw new Error(
5398
- `DExt9 could not read hook implementation ${filePath.split("/").slice(-2).join("/")} \u2014 timeout guard undeterminable for hook "${stem}" (Extension Rule 9). Fail-closed, not a silent absent.`
5399
- );
6373
+ const shortPath = filePath.split("/").slice(-2).join("/");
6374
+ unreadable.push({
6375
+ anchor: shortPath,
6376
+ reason: `could not read hook implementation ${shortPath} \u2014 timeout guard undeterminable for hook "${stem}" (Extension Rule 9)`
6377
+ });
6378
+ continue;
5400
6379
  }
5401
6380
  out.push({
5402
6381
  token: canonical(stem),
@@ -5420,7 +6399,7 @@ function collectFactorySources(tree) {
5420
6399
  for (let i = 0; i < lines.length; i++) {
5421
6400
  const line = lines[i];
5422
6401
  if (line.comment) continue;
5423
- if (!LIFECYCLE_OBJ_RE2.test(line.raw)) continue;
6402
+ if (!testInCode(file, line, LIFECYCLE_OBJ_RE2)) continue;
5424
6403
  let depth = 0;
5425
6404
  let started = false;
5426
6405
  let curKey = null;
@@ -5430,7 +6409,7 @@ function collectFactorySources(tree) {
5430
6409
  if (bl.comment) continue;
5431
6410
  const raw = bl.raw;
5432
6411
  if (started && depth === 1) {
5433
- const k = raw.match(OBJECT_KEY_RE2);
6412
+ const k = matchInCode(file, bl, OBJECT_KEY_RE2);
5434
6413
  if (k) {
5435
6414
  curKey = canonical(k[1]);
5436
6415
  keyBodies.set(curKey, []);
@@ -5439,8 +6418,8 @@ function collectFactorySources(tree) {
5439
6418
  if (curKey && depth >= 1) {
5440
6419
  keyBodies.get(curKey)?.push(raw);
5441
6420
  }
5442
- depth += (raw.match(/\{/g) ?? []).length;
5443
- depth -= (raw.match(/\}/g) ?? []).length;
6421
+ depth += countInCode(file, bl, "{");
6422
+ depth -= countInCode(file, bl, "}");
5444
6423
  if (depth > 0) started = true;
5445
6424
  if (started && depth <= 0) break;
5446
6425
  }
@@ -5466,13 +6445,11 @@ function dExt9PositiveControls() {
5466
6445
  negative: hasImplTimeoutGuard(CTRL_NEGATIVE_BODY)
5467
6446
  };
5468
6447
  }
5469
- function manifestTimeoutTokens(raw, shortPath) {
6448
+ function manifestTimeoutTokens(raw) {
5470
6449
  const map = /* @__PURE__ */ new Map();
5471
6450
  if (raw === void 0) return map;
5472
6451
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
5473
- throw new Error(
5474
- `DExt9 found ${shortPath} whose "timeouts" field is present but not an object (got ${Array.isArray(raw) ? "array" : typeof raw}) \u2014 declared timeout guards are undeterminable (Extension Rule 9). Fail-closed, not a silent absent.`
5475
- );
6452
+ return null;
5476
6453
  }
5477
6454
  for (const [key, value] of Object.entries(raw)) {
5478
6455
  if (typeof value === "number" && value > 0) {
@@ -5490,67 +6467,83 @@ var dExt9 = {
5490
6467
  `DExt9 hook-timeout control FAILED: setTimeoutForm=${controls.setTimeoutForm} abortForm=${controls.abortForm} negative=${controls.negative} \u2014 impl-pattern matcher broken, verdict untrustworthy`
5491
6468
  );
5492
6469
  }
5493
- const hookSources = [...collectHooksDirSources(tree.root), ...collectFactorySources(tree)];
5494
- const implByToken = /* @__PURE__ */ new Map();
5495
- for (const s of hookSources) {
5496
- const list = implByToken.get(s.token) ?? [];
5497
- list.push(s);
5498
- implByToken.set(s.token, list);
5499
- }
5500
- const manifests = findExtensionManifests4(tree.root);
5501
- let declaredAnyLifecycle = false;
5502
- for (const manifestPath2 of manifests) {
5503
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
5504
- let parsed;
5505
- try {
5506
- parsed = JSON.parse(readFileSync17(manifestPath2, "utf8"));
5507
- } catch {
5508
- throw new Error(
5509
- `DExt9 could not parse ${shortPath} as JSON \u2014 timeout guards undeterminable (Extension Rule 9). Fail-closed, not a silent absent.`
5510
- );
5511
- }
5512
- if (!("lifecycle" in parsed)) continue;
5513
- let declared;
5514
- try {
5515
- declared = manifestHookTokens(parsed.lifecycle);
5516
- } catch {
5517
- throw new Error(
5518
- `DExt9 found a present manifest ${shortPath} whose "lifecycle" is not an array (got ${typeof parsed.lifecycle}) \u2014 declared hooks are undeterminable (Extension Rule 9). Fail-closed, not a silent absent.`
5519
- );
6470
+ const unreadable = [];
6471
+ const decided = (() => {
6472
+ const hookSources = [
6473
+ ...collectHooksDirSources(tree, unreadable),
6474
+ ...collectFactorySources(tree)
6475
+ ];
6476
+ const implByToken = /* @__PURE__ */ new Map();
6477
+ for (const s of hookSources) {
6478
+ const list = implByToken.get(s.token) ?? [];
6479
+ list.push(s);
6480
+ implByToken.set(s.token, list);
6481
+ }
6482
+ const manifests = findExtensionManifests(tree);
6483
+ let declaredAnyLifecycle = false;
6484
+ for (const manifestPath2 of manifests) {
6485
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
6486
+ const parsed = readJsonObject(manifestPath2);
6487
+ if (parsed === null) {
6488
+ unreadable.push({
6489
+ anchor: shortPath,
6490
+ reason: `could not parse ${shortPath} as JSON \u2014 timeout guards undeterminable (Extension Rule 9)`
6491
+ });
6492
+ continue;
6493
+ }
6494
+ if (!("lifecycle" in parsed)) continue;
6495
+ const declared = manifestHookTokens(parsed.lifecycle);
6496
+ if (declared === null) {
6497
+ return {
6498
+ id: this.id,
6499
+ anchor: shortPath,
6500
+ state: "non-conforming",
6501
+ verdict: `manifest ${shortPath} "lifecycle" is not an array (got ${typeof parsed.lifecycle}) -> declare the hooks as an array of hook names (Extension Rule 9)`
6502
+ };
6503
+ }
6504
+ if (declared.size === 0) continue;
6505
+ declaredAnyLifecycle = true;
6506
+ const timeouts = manifestTimeoutTokens(parsed.timeouts);
6507
+ if (timeouts === null) {
6508
+ return {
6509
+ id: this.id,
6510
+ anchor: shortPath,
6511
+ state: "non-conforming",
6512
+ verdict: `${shortPath} "timeouts" is present but not an object (got ${Array.isArray(parsed.timeouts) ? "array" : typeof parsed.timeouts}) -> declare timeouts as a { <hook>: <positive ms> } object (Extension Rule 9)`
6513
+ };
6514
+ }
6515
+ const noTimeout = [];
6516
+ for (const hook of declared) {
6517
+ const hasManifestTimeout = timeouts.has(hook);
6518
+ const implSources = implByToken.get(hook) ?? [];
6519
+ const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
6520
+ if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
6521
+ }
6522
+ if (noTimeout.length > 0) {
6523
+ return {
6524
+ id: this.id,
6525
+ anchor: shortPath,
6526
+ state: "non-conforming",
6527
+ verdict: `hook(s) [${noTimeout.join(", ")}] declare NO timeout guard in either form (manifest "timeouts.<hook>" positive number, or impl-side setTimeout/AbortController cutoff) -> a hook exceeding its timeout must be skipped, not a server blocker (Extension Rule 9)`
6528
+ };
6529
+ }
5520
6530
  }
5521
- if (declared.size === 0) continue;
5522
- declaredAnyLifecycle = true;
5523
- const timeouts = manifestTimeoutTokens(parsed.timeouts, shortPath);
5524
- const noTimeout = [];
5525
- for (const hook of declared) {
5526
- const hasManifestTimeout = timeouts.has(hook);
5527
- const implSources = implByToken.get(hook) ?? [];
5528
- const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
5529
- if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
5530
- }
5531
- if (noTimeout.length > 0) {
6531
+ if (!declaredAnyLifecycle) {
5532
6532
  return {
5533
6533
  id: this.id,
5534
- anchor: shortPath,
5535
- state: "non-conforming",
5536
- verdict: `hook(s) [${noTimeout.join(", ")}] declare NO timeout guard in either form (manifest "timeouts.<hook>" positive number, or impl-side setTimeout/AbortController cutoff) -> a hook exceeding its timeout must be skipped, not a server blocker (Extension Rule 9)`
6534
+ anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
6535
+ state: "absent",
6536
+ verdict: "no lifecycle hooks declared, nothing to guard with a timeout (Extension Rule 9)"
5537
6537
  };
5538
6538
  }
5539
- }
5540
- if (!declaredAnyLifecycle) {
5541
6539
  return {
5542
6540
  id: this.id,
5543
- anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
5544
- state: "absent",
5545
- verdict: "no lifecycle hooks declared, nothing to guard with a timeout (Extension Rule 9)"
6541
+ anchor: manifests[0].split("/").slice(-2).join("/"),
6542
+ state: "present",
6543
+ verdict: "every declared lifecycle hook has a timeout guard (manifest timeouts.<hook>, or impl-side cutoff), positive-control OK (Extension Rule 9)"
5546
6544
  };
5547
- }
5548
- return {
5549
- id: this.id,
5550
- anchor: manifests[0].split("/").slice(-2).join("/"),
5551
- state: "present",
5552
- verdict: "every declared lifecycle hook has a timeout guard (manifest timeouts.<hook>, or impl-side cutoff), positive-control OK (Extension Rule 9)"
5553
- };
6545
+ })();
6546
+ return settle(this.id, decided, unreadable);
5554
6547
  }
5555
6548
  };
5556
6549
 
@@ -5579,7 +6572,7 @@ function unscopePackageName(name) {
5579
6572
  const m = name.match(/^@[^/]+\/(.+)$/);
5580
6573
  return m ? m[1] : name;
5581
6574
  }
5582
- var dServer2 = {
6575
+ var dServer2Inner = {
5583
6576
  id: "DServer2 naming",
5584
6577
  rule: "C-2",
5585
6578
  pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
@@ -5607,6 +6600,22 @@ var dServer2 = {
5607
6600
  };
5608
6601
  }
5609
6602
  const tools = findRegisteredTools(tree);
6603
+ if (tools.length === 0) {
6604
+ const surface = toolSurfaceAnchor(tree);
6605
+ if (surface) {
6606
+ const reason = noToolsLocatedReason(
6607
+ tree,
6608
+ "snake_case tool naming is unverifiable (Server Rule 2)"
6609
+ );
6610
+ return {
6611
+ id: this.id,
6612
+ anchor: surface,
6613
+ state: "could-not-judge",
6614
+ reason,
6615
+ verdict: `could not judge: ${reason}`
6616
+ };
6617
+ }
6618
+ }
5610
6619
  for (const t of tools) {
5611
6620
  if (!TOOL_SNAKE_RE.test(t.name)) {
5612
6621
  return {
@@ -5710,6 +6719,9 @@ var dServer2 = {
5710
6719
  };
5711
6720
  }
5712
6721
  };
6722
+ var dServer2 = guardUnreadablePackageJson(dServer2Inner, {
6723
+ authoritative: false
6724
+ });
5713
6725
 
5714
6726
  // src/detectors/dServer6.ts
5715
6727
  var LOG_OPENER_RE = /\b(?:console|logger|log)\.(?:log|info|warn|error|debug|trace)\s*\(/;
@@ -5820,65 +6832,72 @@ var dServer6 = {
5820
6832
  `DServer6 stack control FAILED: /${controls.stack.pattern}/ accept(\`${STACK_POSITIVE}\`)=${controls.stack.positive} reject(\`${STACK_NEGATIVE}\`)=${!controls.stack.negative} \u2014 matcher broken, verdict untrustworthy`
5821
6833
  );
5822
6834
  }
5823
- for (const file of tree.files) {
5824
- const { calls, unbalanced } = collectLogCalls(file);
5825
- if (unbalanced.length > 0) {
5826
- throw new Error(
5827
- `DServer6 could not read the argument list of ${unbalanced.length} log call(s): ${unbalanced.join(", ")} \u2014 parentheses never balance, secret/PII content unverifiable (C-6). Fail-closed, not a silent present.`
5828
- );
5829
- }
5830
- for (const call of calls) {
5831
- if (SPREAD_RE.test(call.argText)) {
5832
- throw new Error(
5833
- `DServer6 cannot inspect the spread argument of the log call at ${call.anchor} \u2014 a spread hides arbitrary content, secret/PII presence unverifiable (C-6). Fail-closed, not a silent present.`
5834
- );
5835
- }
5836
- const refs = referencedIdentifiers(call.argText);
5837
- if (SECRET_RE.test(refs)) {
5838
- return {
5839
- id: this.id,
5840
- anchor: call.anchor,
5841
- state: "non-conforming",
5842
- verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (C-6)"
5843
- };
5844
- }
5845
- if (PII_RE.test(refs)) {
5846
- return {
5847
- id: this.id,
5848
- anchor: call.anchor,
5849
- state: "non-conforming",
5850
- verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
5851
- };
6835
+ const unreadable = [];
6836
+ const decided = (() => {
6837
+ for (const file of tree.files) {
6838
+ const { calls, unbalanced } = collectLogCalls(file);
6839
+ for (const anchor of unbalanced) {
6840
+ unreadable.push({
6841
+ anchor,
6842
+ reason: `could not read the argument list of the log call at ${anchor} \u2014 parentheses never balance, secret/PII content unverifiable (C-6)`
6843
+ });
5852
6844
  }
5853
- if (INTERNAL_RE.test(refs)) {
5854
- return {
5855
- id: this.id,
5856
- anchor: call.anchor,
5857
- state: "non-conforming",
5858
- verdict: "Form D \u2014 log call references an internal-infrastructure identifier (positive-control OK) -> never log internal system detail; redact before logging (C-6)"
5859
- };
6845
+ for (const call of calls) {
6846
+ if (SPREAD_RE.test(call.argText)) {
6847
+ unreadable.push({
6848
+ anchor: call.anchor,
6849
+ reason: `cannot inspect the spread argument of the log call at ${call.anchor} \u2014 a spread hides arbitrary content, secret/PII presence unverifiable (C-6)`
6850
+ });
6851
+ continue;
6852
+ }
6853
+ const refs = referencedIdentifiers(call.argText);
6854
+ if (SECRET_RE.test(refs)) {
6855
+ return {
6856
+ id: this.id,
6857
+ anchor: call.anchor,
6858
+ state: "non-conforming",
6859
+ verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (C-6)"
6860
+ };
6861
+ }
6862
+ if (PII_RE.test(refs)) {
6863
+ return {
6864
+ id: this.id,
6865
+ anchor: call.anchor,
6866
+ state: "non-conforming",
6867
+ verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
6868
+ };
6869
+ }
6870
+ if (INTERNAL_RE.test(refs)) {
6871
+ return {
6872
+ id: this.id,
6873
+ anchor: call.anchor,
6874
+ state: "non-conforming",
6875
+ verdict: "Form D \u2014 log call references an internal-infrastructure identifier (positive-control OK) -> never log internal system detail; redact before logging (C-6)"
6876
+ };
6877
+ }
5860
6878
  }
5861
6879
  }
5862
- }
5863
- for (const file of tree.files) {
5864
- for (const line of file.lines) {
5865
- if (line.comment) continue;
5866
- if (STACK_RE.test(line.raw) && CLIENT_SINK_RE.test(line.raw)) {
5867
- return {
5868
- id: this.id,
5869
- anchor: `${file.rel}:${line.n}`,
5870
- state: "non-conforming",
5871
- verdict: "Form B \u2014 raw error.stack serialized into a client-facing response (positive-control OK) -> return an McpError message, never the raw stack (C-6)"
5872
- };
6880
+ for (const file of tree.files) {
6881
+ for (const line of file.lines) {
6882
+ if (line.comment) continue;
6883
+ if (STACK_RE.test(line.raw) && CLIENT_SINK_RE.test(line.raw)) {
6884
+ return {
6885
+ id: this.id,
6886
+ anchor: `${file.rel}:${line.n}`,
6887
+ state: "non-conforming",
6888
+ verdict: "Form B \u2014 raw error.stack serialized into a client-facing response (positive-control OK) -> return an McpError message, never the raw stack (C-6)"
6889
+ };
6890
+ }
5873
6891
  }
5874
6892
  }
5875
- }
5876
- return {
5877
- id: this.id,
5878
- anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
5879
- state: "present",
5880
- verdict: "no secret/PII/internal-detail referenced in any log call; no raw error.stack exposed to the client (all positive-controls OK). Logging-sink configuration and explicit-activation-only diagnostic mode are the gate/runtime half, out of static scope (declared)."
5881
- };
6893
+ return {
6894
+ id: this.id,
6895
+ anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
6896
+ state: "present",
6897
+ verdict: "no secret/PII/internal-detail referenced in any log call; no raw error.stack exposed to the client (all positive-controls OK). Logging-sink configuration and explicit-activation-only diagnostic mode are the gate/runtime half, out of static scope (declared)."
6898
+ };
6899
+ })();
6900
+ return settle(this.id, decided, unreadable);
5882
6901
  }
5883
6902
  };
5884
6903
 
@@ -5960,59 +6979,65 @@ var dServer8 = {
5960
6979
  `DServer8 text control FAILED: /${controls.text.pattern}/ accept(\`${TEXT_POSITIVE}\`)=${controls.text.positive} reject(\`${TEXT_NEGATIVE}\`)=${!controls.text.negative} \u2014 matcher broken, verdict untrustworthy`
5961
6980
  );
5962
6981
  }
5963
- const blocks = [];
5964
- for (const file of tree.files) {
5965
- const { blocks: fileBlocks, unbalanced } = collectContentArrays(file);
5966
- if (unbalanced.length > 0) {
5967
- throw new Error(
5968
- `DServer8 could not read the content[] array at ${unbalanced.join(", ")} \u2014 brackets never balance, a ui:// resource emission could hide inside it (Server Rule 8). Fail-closed, not a silent present.`
5969
- );
6982
+ const unreadable = [];
6983
+ const decided = (() => {
6984
+ const blocks = [];
6985
+ for (const file of tree.files) {
6986
+ const { blocks: fileBlocks, unbalanced } = collectContentArrays(file);
6987
+ for (const anchor of unbalanced) {
6988
+ unreadable.push({
6989
+ anchor,
6990
+ reason: `could not read the content[] array at ${anchor} \u2014 brackets never balance, a ui:// resource emission could hide inside it (Server Rule 8)`
6991
+ });
6992
+ }
6993
+ blocks.push(...fileBlocks);
6994
+ }
6995
+ for (const file of tree.files) {
6996
+ const fileBlocks = blocks.filter((b) => b.file === file);
6997
+ for (const line of file.lines) {
6998
+ if (line.comment) continue;
6999
+ if (!KIT_RE.test(line.raw)) continue;
7000
+ const inside = fileBlocks.some((b) => line.n >= b.startLine && line.n <= b.endLine);
7001
+ if (!inside) {
7002
+ unreadable.push({
7003
+ anchor: `${file.rel}:${line.n}`,
7004
+ reason: `renderUiResource(\u2026) ui:// emission at ${file.rel}:${line.n} sits outside any content[] array \u2014 cannot associate it with a sibling text fallback (Server Rule 8)`
7005
+ });
7006
+ }
7007
+ }
5970
7008
  }
5971
- blocks.push(...fileBlocks);
5972
- }
5973
- for (const file of tree.files) {
5974
- const fileBlocks = blocks.filter((b) => b.file === file);
5975
- for (const line of file.lines) {
5976
- if (line.comment) continue;
5977
- if (!KIT_RE.test(line.raw)) continue;
5978
- const inside = fileBlocks.some((b) => line.n >= b.startLine && line.n <= b.endLine);
5979
- if (!inside) {
5980
- throw new Error(
5981
- `DServer8 found a renderUiResource(\u2026) ui:// emission at ${file.rel}:${line.n} outside any content[] array \u2014 cannot associate it with a sibling text fallback (Server Rule 8). Fail-closed, not a silent present.`
5982
- );
7009
+ let sawEmission = false;
7010
+ for (const block of blocks) {
7011
+ const inlineEmission = INLINE_RESOURCE_RE.test(block.text) && UI_URI_RE.test(block.text);
7012
+ const kitEmission = KIT_RE.test(block.text);
7013
+ if (!inlineEmission && !kitEmission) continue;
7014
+ sawEmission = true;
7015
+ const hasText = TEXT_RE.test(block.text) || MARKDOWN_RE.test(block.text);
7016
+ if (!hasText) {
7017
+ return {
7018
+ id: this.id,
7019
+ anchor: block.anchor,
7020
+ state: "non-conforming",
7021
+ verdict: `ui:// resource emitted in content[] with no text fallback beside ui:// in the same array (positive-control OK) -> add a sibling { type: "text" } / { type: "markdown" } entry (Server Rule 8)`
7022
+ };
5983
7023
  }
5984
7024
  }
5985
- }
5986
- let sawEmission = false;
5987
- for (const block of blocks) {
5988
- const inlineEmission = INLINE_RESOURCE_RE.test(block.text) && UI_URI_RE.test(block.text);
5989
- const kitEmission = KIT_RE.test(block.text);
5990
- if (!inlineEmission && !kitEmission) continue;
5991
- sawEmission = true;
5992
- const hasText = TEXT_RE.test(block.text) || MARKDOWN_RE.test(block.text);
5993
- if (!hasText) {
7025
+ if (!sawEmission) {
5994
7026
  return {
5995
7027
  id: this.id,
5996
- anchor: block.anchor,
5997
- state: "non-conforming",
5998
- verdict: `ui:// resource emitted in content[] with no text fallback beside ui:// in the same array (positive-control OK) -> add a sibling { type: "text" } / { type: "markdown" } entry (Server Rule 8)`
7028
+ anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
7029
+ state: "absent",
7030
+ verdict: "no ui:// resource emission in any content[] array (n/a for Server Rule 8)"
5999
7031
  };
6000
7032
  }
6001
- }
6002
- if (!sawEmission) {
6003
7033
  return {
6004
7034
  id: this.id,
6005
- anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
6006
- state: "absent",
6007
- verdict: "no ui:// resource emission in any content[] array (n/a for Server Rule 8)"
7035
+ anchor: blocks.length > 0 ? blocks[0].anchor : tree.root,
7036
+ state: "present",
7037
+ verdict: "every ui:// resource emission has a sibling text/markdown fallback in the same content[] (positive-control OK, Server Rule 8)"
6008
7038
  };
6009
- }
6010
- return {
6011
- id: this.id,
6012
- anchor: blocks.length > 0 ? blocks[0].anchor : tree.root,
6013
- state: "present",
6014
- verdict: "every ui:// resource emission has a sibling text/markdown fallback in the same content[] (positive-control OK, Server Rule 8)"
6015
- };
7039
+ })();
7040
+ return settle(this.id, decided, unreadable);
6016
7041
  }
6017
7042
  };
6018
7043
 
@@ -6236,22 +7261,23 @@ var detectors = [
6236
7261
  dExt5,
6237
7262
  dExt7,
6238
7263
  dExt8,
6239
- dExt9
7264
+ dExt9,
7265
+ localIdentity
6240
7266
  ];
6241
7267
 
6242
7268
  // src/runtime/launch.ts
6243
7269
  import { spawnSync as spawnSync2 } from "child_process";
6244
- import { existsSync as existsSync15, readFileSync as readFileSync18, statSync as statSync13 } from "fs";
6245
- import { dirname as dirname10, isAbsolute, join as join20, resolve } from "path";
7270
+ import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
7271
+ import { dirname as dirname10, isAbsolute, join as join18, resolve } from "path";
6246
7272
  function findPackageDir(target) {
6247
7273
  let dir;
6248
7274
  try {
6249
- dir = statSync13(target).isDirectory() ? target : dirname10(target);
7275
+ dir = statSync8(target).isDirectory() ? target : dirname10(target);
6250
7276
  } catch {
6251
7277
  return null;
6252
7278
  }
6253
7279
  for (let depth = 0; depth < 8; depth++) {
6254
- if (existsSync15(join20(dir, "package.json"))) return dir;
7280
+ if (existsSync15(join18(dir, "package.json"))) return dir;
6255
7281
  const parent = dirname10(dir);
6256
7282
  if (parent === dir) break;
6257
7283
  dir = parent;
@@ -6276,10 +7302,10 @@ function deriveLaunchPlan(target) {
6276
7302
  }
6277
7303
  let pkg;
6278
7304
  try {
6279
- pkg = JSON.parse(readFileSync18(join20(pkgDir, "package.json"), "utf8"));
7305
+ pkg = JSON.parse(readFileSync11(join18(pkgDir, "package.json"), "utf8"));
6280
7306
  } catch (err) {
6281
7307
  return {
6282
- error: `could not parse ${join20(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
7308
+ error: `could not parse ${join18(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
6283
7309
  };
6284
7310
  }
6285
7311
  const bin = firstBinEntry(pkg.bin);
@@ -6357,10 +7383,10 @@ function readEvalCounts(target) {
6357
7383
  if (!pkgDir) return counts;
6358
7384
  let dir = pkgDir;
6359
7385
  for (let depth = 0; depth < 8; depth++) {
6360
- const candidate = join20(dir, "evals", "evals.json");
7386
+ const candidate = join18(dir, "evals", "evals.json");
6361
7387
  if (existsSync15(candidate)) {
6362
7388
  try {
6363
- const parsed = JSON.parse(readFileSync18(candidate, "utf8"));
7389
+ const parsed = JSON.parse(readFileSync11(candidate, "utf8"));
6364
7390
  for (const c of parsed.cases ?? []) {
6365
7391
  if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
6366
7392
  }
@@ -6376,7 +7402,7 @@ function readEvalCounts(target) {
6376
7402
  }
6377
7403
  function statSyncSafe(p) {
6378
7404
  try {
6379
- return statSync13(p).isDirectory();
7405
+ return statSync8(p).isDirectory();
6380
7406
  } catch {
6381
7407
  return false;
6382
7408
  }
@@ -6495,6 +7521,9 @@ function reconcile(id, staticR, dynamicR) {
6495
7521
  };
6496
7522
  }
6497
7523
  function runDoctor(target, detectorList = detectors, opts = {}) {
7524
+ return runDoctorReport(target, detectorList, opts).results;
7525
+ }
7526
+ function runDoctorReport(target, detectorList = detectors, opts = {}) {
6498
7527
  const mode = opts.mode ?? "static";
6499
7528
  const tree = loadTree(target);
6500
7529
  const needsRuntime = mode !== "static" && detectorList.some((d) => typeof d.runDynamic === "function");
@@ -6529,7 +7558,7 @@ function runDoctor(target, detectorList = detectors, opts = {}) {
6529
7558
  results.push(reconcile(d.id, staticR, dynamicR));
6530
7559
  }
6531
7560
  }
6532
- return results;
7561
+ return { results, skipped: tree.skippedDirs };
6533
7562
  }
6534
7563
  async function invokeLiveChecked(d, session) {
6535
7564
  if (!session) {
@@ -6584,14 +7613,32 @@ async function runDoctorAsync(target, opts = {}) {
6584
7613
  }
6585
7614
  return results;
6586
7615
  }
6587
- function renderTable(surface, results) {
7616
+ function renderSkipped(skipped) {
7617
+ if (skipped.length === 0) {
7618
+ return "skipped directories (0) \u2014 none: no directory was skipped on this run.";
7619
+ }
7620
+ const sorted = [...skipped].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
7621
+ const head = `skipped directories (${sorted.length}) \u2014 NOT read on this run; a verdict above says nothing about what lies inside:`;
7622
+ const rows = sorted.map(
7623
+ (d) => `${d.path} | ${d.reason}${d.candidate ? ` | holds a deployment configuration: ${d.candidate}` : ""}`
7624
+ );
7625
+ return [head, "-".repeat(head.length), ...rows].join("\n");
7626
+ }
7627
+ function renderTable(surface, results, skipped) {
7628
+ const body = renderResults(surface, results);
7629
+ const section = skipped === void 0 ? "skipped directories: NOT REPORTED \u2014 the caller did not pass this run's skipped list (use runDoctorReport), so the scope above may be narrower than it reads." : renderSkipped(skipped);
7630
+ return `${body}
7631
+
7632
+ ${section}`;
7633
+ }
7634
+ function renderResults(surface, results) {
6588
7635
  const cols = { item: 24, anchor: 26, state: 15 };
6589
7636
  const header = `${pad("item", cols.item)} | ${pad("file:line", cols.anchor)} | ${pad("state", cols.state)} | verdict`;
6590
- const sep = "-".repeat(header.length);
7637
+ const sep3 = "-".repeat(header.length);
6591
7638
  const rows = results.map(
6592
7639
  (r) => `${pad(r.id, cols.item)} | ${pad(r.anchor, cols.anchor)} | ${pad(r.state, cols.state)} | ${r.verdict}`
6593
7640
  );
6594
- const table = [`surface: ${surface}`, header, sep, ...rows].join("\n");
7641
+ const table = [`surface: ${surface}`, header, sep3, ...rows].join("\n");
6595
7642
  const unjudged = results.filter((r) => r.state === "could-not-judge");
6596
7643
  if (unjudged.length === 0) return table;
6597
7644
  const cnjHeader = `could-not-judge (${unjudged.length}) \u2014 the doctor could NOT read these; this is NOT a clean bill:`;
@@ -6612,6 +7659,9 @@ var USAGE = [
6612
7659
  " --mode=static read the tree only (default)",
6613
7660
  " --mode=dynamic launch the server and judge what it serves",
6614
7661
  " --mode=both both, dynamic findings taking precedence",
7662
+ " --local-identity-ceiling=N",
7663
+ " local identity primitives tolerated before DLocalIdentity",
7664
+ " fails (default 0)",
6615
7665
  " --version print the version and exit",
6616
7666
  " --help print this and exit",
6617
7667
  "",
@@ -6620,7 +7670,7 @@ var USAGE = [
6620
7670
  function readVersion() {
6621
7671
  try {
6622
7672
  const here = dirname11(fileURLToPath(import.meta.url));
6623
- const manifest = JSON.parse(readFileSync19(join21(here, "..", "package.json"), "utf8"));
7673
+ const manifest = JSON.parse(readFileSync12(join19(here, "..", "package.json"), "utf8"));
6624
7674
  return manifest.version ?? null;
6625
7675
  } catch {
6626
7676
  return null;
@@ -6652,8 +7702,20 @@ function main() {
6652
7702
  console.error(`unknown --mode "${mode}" \u2014 expected static|dynamic|both`);
6653
7703
  process.exit(1);
6654
7704
  }
6655
- const results = runDoctor(target, void 0, { mode });
6656
- console.log(renderTable(target, results));
7705
+ const ceilingArg = args.find((a) => a.startsWith("--local-identity-ceiling="));
7706
+ let detectorList = detectors;
7707
+ if (ceilingArg) {
7708
+ const raw = ceilingArg.slice("--local-identity-ceiling=".length);
7709
+ const ceiling = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
7710
+ if (!Number.isInteger(ceiling)) {
7711
+ console.error(`invalid --local-identity-ceiling "${raw}" \u2014 expected a non-negative integer`);
7712
+ process.exit(1);
7713
+ }
7714
+ const ratchet = makeLocalIdentity({ ceiling });
7715
+ detectorList = detectors.map((d) => d === localIdentity ? ratchet : d);
7716
+ }
7717
+ const { results, skipped } = runDoctorReport(target, detectorList, { mode });
7718
+ console.log(renderTable(target, results, skipped));
6657
7719
  process.exitCode = computeExitCode(results);
6658
7720
  }
6659
7721
  function isDirectInvocation() {
@@ -6675,7 +7737,9 @@ if (isDirectInvocation()) main();
6675
7737
  export {
6676
7738
  computeExitCode,
6677
7739
  isLiveDetector,
7740
+ renderSkipped,
6678
7741
  renderTable,
6679
7742
  runDoctor,
6680
- runDoctorAsync
7743
+ runDoctorAsync,
7744
+ runDoctorReport
6681
7745
  };