@vantageos/mcp-doctor 0.3.2 → 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
@@ -2,12 +2,997 @@
2
2
 
3
3
  // src/doctor.ts
4
4
  import { readFileSync as readFileSync12, realpathSync } from "fs";
5
- import { dirname as dirname11, join as join21, resolve as resolve2 } from "path";
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
  }
@@ -1177,59 +2162,6 @@ var c13ContextCeiling = {
1177
2162
  }
1178
2163
  };
1179
2164
 
1180
- // src/detectors/couldNotJudge.ts
1181
- import { readFileSync as readFileSync2 } from "fs";
1182
- function couldNotJudge(id, anchor, reason) {
1183
- return {
1184
- id,
1185
- anchor,
1186
- state: "could-not-judge",
1187
- reason,
1188
- verdict: `could not judge: ${reason}`
1189
- };
1190
- }
1191
- function settle(id, decided, unreadable) {
1192
- if (unreadable.length === 0) return decided;
1193
- const named = unreadable.map((u) => `${u.anchor}: ${u.reason}`).join("; ");
1194
- if (decided.state === "non-conforming") {
1195
- return { ...decided, verdict: `${decided.verdict} [also could not read: ${named}]` };
1196
- }
1197
- const judgedRest = decided.state === "could-not-judge" ? ` ${decided.reason ?? decided.verdict}` : ` The readable remainder judged ${decided.state}: ${decided.verdict}`;
1198
- return couldNotJudge(
1199
- id,
1200
- unreadable[0].anchor,
1201
- `${unreadable.length} input(s) present but unreadable \u2014 ${named}.${judgedRest}`
1202
- );
1203
- }
1204
- function readJsonObject(path) {
1205
- let value;
1206
- try {
1207
- value = JSON.parse(readFileSync2(path, "utf8"));
1208
- } catch {
1209
- return null;
1210
- }
1211
- if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
1212
- return value;
1213
- }
1214
- function guardUnreadablePackageJson(inner, opts) {
1215
- return {
1216
- ...inner,
1217
- run(tree) {
1218
- const decided = inner.run(tree);
1219
- const pkg = tree.packageJson;
1220
- if (!pkg?.unreadable) return decided;
1221
- const u = {
1222
- anchor: pkg.path,
1223
- reason: `package.json is present but could not be parsed (${pkg.unreadable})`
1224
- };
1225
- if (opts.authoritative && decided.state !== "could-not-judge") {
1226
- return couldNotJudge(inner.id, u.anchor, `${u.anchor}: ${u.reason}`);
1227
- }
1228
- return settle(inner.id, decided, [u]);
1229
- }
1230
- };
1231
- }
1232
-
1233
2165
  // src/detectors/c14-catalog-registration.ts
1234
2166
  var PIN3 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1235
2167
  var REQUIRED_FIELDS = [
@@ -1514,11 +2446,11 @@ var c16UntrustedOutput = {
1514
2446
  };
1515
2447
 
1516
2448
  // src/detectors/c17-supply-chain.ts
1517
- import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
1518
- 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";
1519
2451
  var PIN4 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1520
2452
  var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
1521
- var WORKFLOW_DIR = join2(".github", "workflows");
2453
+ var WORKFLOW_DIR = join3(".github", "workflows");
1522
2454
  var PROVENANCE_RE2 = /--provenance\b|\bprovenance\s*:\s*true\b|id-token:\s*write/i;
1523
2455
  function c17PositiveControls() {
1524
2456
  const sample = "npm publish --provenance --access public";
@@ -1534,7 +2466,7 @@ function c17PositiveControls() {
1534
2466
  }
1535
2467
  function findLockfile(root) {
1536
2468
  for (const name of LOCKFILES) {
1537
- if (existsSync2(join2(root, name))) return name;
2469
+ if (existsSync3(join3(root, name))) return name;
1538
2470
  }
1539
2471
  return null;
1540
2472
  }
@@ -1546,20 +2478,20 @@ function findProvenanceClaim(tree, root) {
1546
2478
  return { site: `${pkg.path}:publishConfig.provenance`, text: "provenance: true" };
1547
2479
  }
1548
2480
  }
1549
- const workflowsDir = join2(root, WORKFLOW_DIR);
1550
- if (!existsSync2(workflowsDir)) return null;
2481
+ const workflowsDir = join3(root, WORKFLOW_DIR);
2482
+ if (!existsSync3(workflowsDir)) return null;
1551
2483
  let entries = [];
1552
2484
  try {
1553
- entries = readdirSync(workflowsDir);
2485
+ entries = readdirSync2(workflowsDir);
1554
2486
  } catch {
1555
2487
  return null;
1556
2488
  }
1557
2489
  for (const name of entries) {
1558
2490
  if (!/\.ya?ml$/.test(name)) continue;
1559
- const path = join2(workflowsDir, name);
2491
+ const path = join3(workflowsDir, name);
1560
2492
  let text;
1561
2493
  try {
1562
- text = readFileSync3(path, "utf8");
2494
+ text = readFileSync4(path, "utf8");
1563
2495
  } catch {
1564
2496
  continue;
1565
2497
  }
@@ -1612,17 +2544,17 @@ var c17SupplyChain = guardUnreadablePackageJson(c17SupplyChainInner, {
1612
2544
  });
1613
2545
 
1614
2546
  // src/detectors/c18-structure-manifest.ts
1615
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
1616
- import { join as join3 } from "path";
2547
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
2548
+ import { join as join4 } from "path";
1617
2549
  var PIN5 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1618
2550
  function manifestPath(root) {
1619
- const extPath = join3(root, "extension.json");
1620
- if (existsSync3(extPath)) return { kind: "extension", path: extPath };
1621
- 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") };
1622
2554
  }
1623
2555
  function readJson(path) {
1624
2556
  try {
1625
- return JSON.parse(readFileSync4(path, "utf8"));
2557
+ return JSON.parse(readFileSync5(path, "utf8"));
1626
2558
  } catch {
1627
2559
  return null;
1628
2560
  }
@@ -1670,7 +2602,7 @@ var c18StructureManifestInner = {
1670
2602
  }
1671
2603
  const { kind, path } = manifestPath(tree.root);
1672
2604
  const manifestName = kind === "extension" ? "extension.json" : "mcp.json";
1673
- if (!existsSync3(path)) {
2605
+ if (!existsSync4(path)) {
1674
2606
  return {
1675
2607
  id: this.id,
1676
2608
  anchor: tree.root,
@@ -1753,89 +2685,6 @@ var c18StructureManifest = guardUnreadablePackageJson(
1753
2685
  { authoritative: false }
1754
2686
  );
1755
2687
 
1756
- // src/scan.ts
1757
- import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync } from "fs";
1758
- import { dirname, join as join4, relative } from "path";
1759
- var SKIP_DIRS = /* @__PURE__ */ new Set([
1760
- "node_modules",
1761
- "dist",
1762
- "build",
1763
- ".git",
1764
- "tests",
1765
- "__tests__",
1766
- "coverage",
1767
- // Test material, not a target's served surface: deliberately-malformed
1768
- // servers and foreign copies live here. Scanning them makes the instrument
1769
- // trip its own fail-closed detectors when pointed at its own tree, and lints
1770
- // inputs that are meant to be broken. A real target's fixtures are equally
1771
- // test inputs, not its product surface.
1772
- "fixtures"
1773
- ]);
1774
- function isCommentLine(raw) {
1775
- const t = raw.trim();
1776
- if (t === "") return false;
1777
- return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
1778
- }
1779
- function walk(dir, out) {
1780
- let entries;
1781
- try {
1782
- entries = readdirSync2(dir);
1783
- } catch {
1784
- return;
1785
- }
1786
- for (const name of entries) {
1787
- const full = join4(dir, name);
1788
- let st;
1789
- try {
1790
- st = statSync(full);
1791
- } catch {
1792
- continue;
1793
- }
1794
- if (st.isDirectory()) {
1795
- if (SKIP_DIRS.has(name)) continue;
1796
- walk(full, out);
1797
- } else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
1798
- if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
1799
- out.push(full);
1800
- }
1801
- }
1802
- }
1803
- function loadFile(path, root) {
1804
- const raw = readFileSync5(path, "utf8");
1805
- const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
1806
- return { path, rel: relative(root, path) || path, lines };
1807
- }
1808
- function findPackageJson(startDir) {
1809
- let dir = startDir;
1810
- for (let depth = 0; depth < 8; depth++) {
1811
- const candidate = join4(dir, "package.json");
1812
- if (existsSync4(candidate)) {
1813
- try {
1814
- const parsed = JSON.parse(readFileSync5(candidate, "utf8"));
1815
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1816
- return { path: candidate, json: {}, unreadable: "not a JSON object" };
1817
- }
1818
- return { path: candidate, json: parsed };
1819
- } catch (err) {
1820
- return { path: candidate, json: {}, unreadable: err.message };
1821
- }
1822
- }
1823
- const parent = dirname(dir);
1824
- if (parent === dir) break;
1825
- dir = parent;
1826
- }
1827
- return null;
1828
- }
1829
- function loadTree(root) {
1830
- const st = statSync(root);
1831
- const baseDir = st.isDirectory() ? root : dirname(root);
1832
- const filePaths = [];
1833
- if (st.isDirectory()) walk(root, filePaths);
1834
- else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
1835
- const files = filePaths.map((p) => loadFile(p, baseDir));
1836
- return { root, files, packageJson: findPackageJson(baseDir) };
1837
- }
1838
-
1839
2688
  // src/detectors/c19-served-descriptions.ts
1840
2689
  var PIN6 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1841
2690
  var TOOL_DESC_INLINE_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(\s*(?:["'`][^"'`]+["'`]|[A-Za-z_$][\w$]*)\s*,\s*["'`]([^"'`]+)["'`]/;
@@ -2509,7 +3358,7 @@ import {
2509
3358
  writeFileSync
2510
3359
  } from "fs";
2511
3360
  import { tmpdir } from "os";
2512
- 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";
2513
3362
  var PIN9 = "20136b50";
2514
3363
  var DEPLOY_CONFIG_FILES = [
2515
3364
  "railway.json",
@@ -2526,15 +3375,44 @@ var LOCKFILES2 = [
2526
3375
  { name: "pnpm-lock.yaml", command: "pnpm", args: ["install", "--frozen-lockfile"] },
2527
3376
  { name: "yarn.lock", command: "yarn", args: ["install", "--frozen-lockfile"] }
2528
3377
  ];
2529
- var SKIP_WALK_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage"]);
2530
- function walkForConfigs(dir, depth, seen) {
3378
+ function walkForConfigs(dir, depth, seen, onSkip) {
2531
3379
  if (depth > 8) return;
2532
3380
  let entries;
2533
3381
  try {
2534
- entries = readdirSync3(dir);
3382
+ entries = readdirSync3(dir);
3383
+ } catch {
3384
+ return;
3385
+ }
3386
+ for (const name of entries) {
3387
+ const full = join6(dir, name);
3388
+ let st;
3389
+ try {
3390
+ st = statSync2(full);
3391
+ } catch {
3392
+ continue;
3393
+ }
3394
+ if (st.isDirectory()) {
3395
+ if (SKIP_DIRS.has(name)) {
3396
+ onSkip(full, depth + 1);
3397
+ continue;
3398
+ }
3399
+ walkForConfigs(full, depth + 1, seen, onSkip);
3400
+ } else if (DEPLOY_CONFIG_FILES.includes(name)) {
3401
+ if (!seen.has(dir)) {
3402
+ seen.set(dir, { dir, configFile: name, configPath: full });
3403
+ }
3404
+ }
3405
+ }
3406
+ }
3407
+ function probeForCandidate(dir, depth) {
3408
+ if (depth > 8) return null;
3409
+ let entries;
3410
+ try {
3411
+ entries = readdirSync3(dir).sort();
2535
3412
  } catch {
2536
- return;
3413
+ return null;
2537
3414
  }
3415
+ const subdirs = [];
2538
3416
  for (const name of entries) {
2539
3417
  const full = join6(dir, name);
2540
3418
  let st;
@@ -2544,14 +3422,18 @@ function walkForConfigs(dir, depth, seen) {
2544
3422
  continue;
2545
3423
  }
2546
3424
  if (st.isDirectory()) {
2547
- if (SKIP_WALK_DIRS.has(name)) continue;
2548
- walkForConfigs(full, depth + 1, seen);
3425
+ const info = SKIP_DIR_INFO[name];
3426
+ if (info && !info.authored) continue;
3427
+ subdirs.push(full);
2549
3428
  } else if (DEPLOY_CONFIG_FILES.includes(name)) {
2550
- if (!seen.has(dir)) {
2551
- seen.set(dir, { dir, configFile: name, configPath: full });
2552
- }
3429
+ return full;
2553
3430
  }
2554
3431
  }
3432
+ for (const sub of subdirs) {
3433
+ const hit = probeForCandidate(sub, depth + 1);
3434
+ if (hit) return hit;
3435
+ }
3436
+ return null;
2555
3437
  }
2556
3438
  function resolveRailwayRootDirectory(configPath) {
2557
3439
  let text;
@@ -2574,15 +3456,31 @@ function resolveRailwayRootDirectory(configPath) {
2574
3456
  const rootDirectory = typeof parsed.rootDirectory === "string" && parsed.rootDirectory || build && typeof build.rootDirectory === "string" && build.rootDirectory || null;
2575
3457
  return rootDirectory;
2576
3458
  }
2577
- function c26DeriveScope(root) {
3459
+ function c26DeriveScope(root, tree) {
2578
3460
  const seen = /* @__PURE__ */ new Map();
2579
- 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);
2580
3474
  const sites = [];
2581
3475
  for (const site of seen.values()) {
2582
3476
  if (site.configFile === "railway.json") {
2583
3477
  const resolved = resolveRailwayRootDirectory(site.configPath);
2584
3478
  if (resolved && typeof resolved === "object" && "error" in resolved) {
2585
- return { sites: [], unparsable: { path: site.configPath, reason: resolved.error } };
3479
+ return {
3480
+ sites: [],
3481
+ skipped,
3482
+ unparsable: { path: site.configPath, reason: resolved.error }
3483
+ };
2586
3484
  }
2587
3485
  if (typeof resolved === "string" && resolved.trim()) {
2588
3486
  sites.push({ ...site, dir: join6(site.dir, resolved) });
@@ -2595,6 +3493,7 @@ function c26DeriveScope(root) {
2595
3493
  } catch (err) {
2596
3494
  return {
2597
3495
  sites: [],
3496
+ skipped,
2598
3497
  unparsable: {
2599
3498
  path: site.configPath,
2600
3499
  reason: `could not parse ${site.configPath} as JSON: ${err instanceof Error ? err.message : String(err)}`
@@ -2604,7 +3503,7 @@ function c26DeriveScope(root) {
2604
3503
  }
2605
3504
  sites.push(site);
2606
3505
  }
2607
- return { sites };
3506
+ return { sites, skipped };
2608
3507
  }
2609
3508
  function findLockfile2(dir) {
2610
3509
  for (const l of LOCKFILES2) {
@@ -2761,7 +3660,18 @@ var c26SubpackageLockfile = {
2761
3660
  rule: "C-26",
2762
3661
  pin: PIN9,
2763
3662
  run(tree) {
2764
- 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
+ };
2765
3675
  if (scope.unparsable) {
2766
3676
  const reason = `deployment configuration ${scope.unparsable.path} could not be parsed: ${scope.unparsable.reason} \u2014 scope unestablished, C-26 cannot derive its targets`;
2767
3677
  return {
@@ -2773,6 +3683,7 @@ var c26SubpackageLockfile = {
2773
3683
  };
2774
3684
  }
2775
3685
  if (scope.sites.length === 0) {
3686
+ if (hidden.length > 0) return unjudgedHidden("");
2776
3687
  return {
2777
3688
  id: this.id,
2778
3689
  anchor: tree.root,
@@ -2784,6 +3695,7 @@ var c26SubpackageLockfile = {
2784
3695
  const inventory = renderCoverage(scope.sites, verdicts);
2785
3696
  const analysed = verdicts.filter((v) => v.status === "analysed");
2786
3697
  if (analysed.length === 0) {
3698
+ if (hidden.length > 0) return unjudgedHidden("no analysable site; ");
2787
3699
  return {
2788
3700
  id: this.id,
2789
3701
  anchor: tree.root,
@@ -2800,6 +3712,11 @@ var c26SubpackageLockfile = {
2800
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}`
2801
3713
  };
2802
3714
  }
3715
+ if (hidden.length > 0) {
3716
+ return unjudgedHidden(
3717
+ `${analysed.length} analysed site(s) are consistent, but coverage is partial; `
3718
+ );
3719
+ }
2803
3720
  return {
2804
3721
  id: this.id,
2805
3722
  anchor: analysed[0].site.dir,
@@ -4369,24 +5286,12 @@ var dDebugOptIn = {
4369
5286
  }
4370
5287
  };
4371
5288
 
4372
- // src/detectors/dExt1.ts
5289
+ // src/detectors/extManifests.ts
4373
5290
  import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4374
5291
  import { join as join12 } from "path";
4375
- var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
4376
- var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
4377
- var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
4378
- function dExt1PositiveControls() {
4379
- return {
4380
- identifier: {
4381
- pattern: IDENTIFIER_RE2.source,
4382
- positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
4383
- negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
4384
- }
4385
- };
4386
- }
4387
- var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4388
- function findExtensionManifests(root) {
5292
+ function findExtensionManifests(tree) {
4389
5293
  const out = [];
5294
+ const root = tree.root;
4390
5295
  let base;
4391
5296
  try {
4392
5297
  base = statSync5(root).isDirectory() ? root : join12(root, "..");
@@ -4409,7 +5314,10 @@ function findExtensionManifests(root) {
4409
5314
  continue;
4410
5315
  }
4411
5316
  if (st.isDirectory()) {
4412
- if (SKIP_DIRS2.has(name)) continue;
5317
+ if (SKIP_DIRS.has(name)) {
5318
+ recordSkip(tree, full, base);
5319
+ continue;
5320
+ }
4413
5321
  walk2(full);
4414
5322
  } else if (name === "extension.json") {
4415
5323
  out.push(full);
@@ -4419,20 +5327,34 @@ function findExtensionManifests(root) {
4419
5327
  walk2(base);
4420
5328
  return out;
4421
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
+ }
4422
5344
  var TS_IDENT_LITERAL_RE = /\bidentifier:\s*["'`]([^"'`]*)["'`]/;
4423
- var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`]/;
5345
+ var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`{]/;
4424
5346
  function findTsIdentifiers(tree) {
4425
5347
  const sites = [];
4426
5348
  const unresolved = [];
4427
5349
  for (const file of tree.files) {
4428
5350
  for (const line of file.lines) {
4429
5351
  if (line.comment) continue;
4430
- const lit = line.raw.match(TS_IDENT_LITERAL_RE);
5352
+ const lit = matchInCode(file, line, TS_IDENT_LITERAL_RE);
4431
5353
  if (lit) {
4432
5354
  sites.push({ value: lit[1], anchor: `${file.rel}:${line.n}` });
4433
5355
  continue;
4434
5356
  }
4435
- if (TS_IDENT_KEY_RE.test(line.raw)) {
5357
+ if (testInCode(file, line, TS_IDENT_KEY_RE)) {
4436
5358
  unresolved.push(`${file.rel}:${line.n}`);
4437
5359
  }
4438
5360
  }
@@ -4450,7 +5372,7 @@ var dExt1 = {
4450
5372
  }
4451
5373
  const unreadable = [];
4452
5374
  const decided = (() => {
4453
- const manifests = findExtensionManifests(tree.root);
5375
+ const manifests = findExtensionManifests(tree);
4454
5376
  for (const manifestPath2 of manifests) {
4455
5377
  const shortPath = manifestPath2.split("/").slice(-2).join("/");
4456
5378
  const parsed = readJsonObject(manifestPath2);
@@ -4517,8 +5439,6 @@ var dExt1 = {
4517
5439
  };
4518
5440
 
4519
5441
  // src/detectors/dExt2.ts
4520
- import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
4521
- import { join as join13 } from "path";
4522
5442
  var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
4523
5443
  var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
4524
5444
  var TS_GD_KEY_RE = /\bgracefulDegradation:\s*([^\s,}]+)/;
@@ -4533,41 +5453,6 @@ function dExt2PositiveControls() {
4533
5453
  }
4534
5454
  };
4535
5455
  }
4536
- var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4537
- function findExtensionManifests2(root) {
4538
- const out = [];
4539
- let base;
4540
- try {
4541
- base = statSync6(root).isDirectory() ? root : join13(root, "..");
4542
- } catch {
4543
- return out;
4544
- }
4545
- const walk2 = (dir) => {
4546
- let entries;
4547
- try {
4548
- entries = readdirSync5(dir);
4549
- } catch {
4550
- return;
4551
- }
4552
- for (const name of entries) {
4553
- const full = join13(dir, name);
4554
- let st;
4555
- try {
4556
- st = statSync6(full);
4557
- } catch {
4558
- continue;
4559
- }
4560
- if (st.isDirectory()) {
4561
- if (SKIP_DIRS3.has(name)) continue;
4562
- walk2(full);
4563
- } else if (name === "extension.json") {
4564
- out.push(full);
4565
- }
4566
- }
4567
- };
4568
- walk2(base);
4569
- return out;
4570
- }
4571
5456
  function findTsFactorySites(tree) {
4572
5457
  const sites = [];
4573
5458
  for (const file of tree.files) {
@@ -4575,7 +5460,7 @@ function findTsFactorySites(tree) {
4575
5460
  for (let i = 0; i < lines.length; i++) {
4576
5461
  const line = lines[i];
4577
5462
  if (line.comment) continue;
4578
- if (!DEFINE_EXTENSION_OPEN_RE.test(line.raw)) continue;
5463
+ if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE)) continue;
4579
5464
  const anchor = `${file.rel}:${line.n}`;
4580
5465
  let depth = 0;
4581
5466
  let started = false;
@@ -4585,20 +5470,20 @@ function findTsFactorySites(tree) {
4585
5470
  const bl = lines[j];
4586
5471
  if (!bl.comment) {
4587
5472
  if (state === "absent") {
4588
- if (TS_GD_TRUE_RE.test(bl.raw)) {
5473
+ if (testInCode(file, bl, TS_GD_TRUE_RE)) {
4589
5474
  state = "true";
4590
- } else if (TS_GD_FALSE_RE.test(bl.raw)) {
5475
+ } else if (testInCode(file, bl, TS_GD_FALSE_RE)) {
4591
5476
  state = "false";
4592
5477
  } else {
4593
- const key = bl.raw.match(TS_GD_KEY_RE);
5478
+ const key = matchInCode(file, bl, TS_GD_KEY_RE);
4594
5479
  if (key) {
4595
5480
  state = "unresolved";
4596
5481
  value = key[1];
4597
5482
  }
4598
5483
  }
4599
5484
  }
4600
- depth += (bl.raw.match(/\{/g) ?? []).length;
4601
- depth -= (bl.raw.match(/\}/g) ?? []).length;
5485
+ depth += countInCode(file, bl, "{");
5486
+ depth -= countInCode(file, bl, "}");
4602
5487
  if (depth > 0) started = true;
4603
5488
  }
4604
5489
  if (started && depth <= 0) break;
@@ -4619,7 +5504,7 @@ var dExt2 = {
4619
5504
  }
4620
5505
  const unreadable = [];
4621
5506
  const decided = (() => {
4622
- const manifests = findExtensionManifests2(tree.root);
5507
+ const manifests = findExtensionManifests(tree);
4623
5508
  for (const manifestPath2 of manifests) {
4624
5509
  const shortPath = manifestPath2.split("/").slice(-2).join("/");
4625
5510
  const parsed = readJsonObject(manifestPath2);
@@ -4695,8 +5580,6 @@ var dExt2 = {
4695
5580
  };
4696
5581
 
4697
5582
  // src/detectors/dExt3.ts
4698
- import { readdirSync as readdirSync6, statSync as statSync7 } from "fs";
4699
- import { join as join14 } from "path";
4700
5583
  var TYPE_MATCHERS = [
4701
5584
  { type: "middleware", re: /\bmiddleware\b/ },
4702
5585
  { type: "tool-extension", re: /\btool-ext(?:ension)?\b/ },
@@ -4719,41 +5602,6 @@ function dExt3PositiveControls() {
4719
5602
  }
4720
5603
  };
4721
5604
  }
4722
- var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4723
- function findExtensionManifests3(root) {
4724
- const out = [];
4725
- let base;
4726
- try {
4727
- base = statSync7(root).isDirectory() ? root : join14(root, "..");
4728
- } catch {
4729
- return out;
4730
- }
4731
- const walk2 = (dir) => {
4732
- let entries;
4733
- try {
4734
- entries = readdirSync6(dir);
4735
- } catch {
4736
- return;
4737
- }
4738
- for (const name of entries) {
4739
- const full = join14(dir, name);
4740
- let st;
4741
- try {
4742
- st = statSync7(full);
4743
- } catch {
4744
- continue;
4745
- }
4746
- if (st.isDirectory()) {
4747
- if (SKIP_DIRS4.has(name)) continue;
4748
- walk2(full);
4749
- } else if (name === "extension.json") {
4750
- out.push(full);
4751
- }
4752
- }
4753
- };
4754
- walk2(base);
4755
- return out;
4756
- }
4757
5605
  var DEFINE_EXTENSION_OPEN_RE2 = /\bdefineExtension\s*\(\s*\{/;
4758
5606
  var TS_TYPE_LITERAL_RE = /\btype:\s*["'`]([^"'`]*)["'`]/;
4759
5607
  var TS_TYPE_KEY_RE = /\btype:\s*[^\s"'`]/;
@@ -4764,7 +5612,7 @@ function findTsTypeSites(tree) {
4764
5612
  for (let i = 0; i < lines.length; i++) {
4765
5613
  const line = lines[i];
4766
5614
  if (line.comment) continue;
4767
- if (!DEFINE_EXTENSION_OPEN_RE2.test(line.raw)) continue;
5615
+ if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE2)) continue;
4768
5616
  const anchor = `${file.rel}:${line.n}`;
4769
5617
  let depth = 0;
4770
5618
  let started = false;
@@ -4775,17 +5623,17 @@ function findTsTypeSites(tree) {
4775
5623
  const bl = lines[j];
4776
5624
  if (!bl.comment) {
4777
5625
  if (!found) {
4778
- const lit = bl.raw.match(TS_TYPE_LITERAL_RE);
5626
+ const lit = matchInCode(file, bl, TS_TYPE_LITERAL_RE);
4779
5627
  if (lit) {
4780
5628
  value = lit[1];
4781
5629
  found = true;
4782
- } else if (TS_TYPE_KEY_RE.test(bl.raw)) {
5630
+ } else if (testInCode(file, bl, TS_TYPE_KEY_RE)) {
4783
5631
  unresolved = true;
4784
5632
  found = true;
4785
5633
  }
4786
5634
  }
4787
- depth += (bl.raw.match(/\{/g) ?? []).length;
4788
- depth -= (bl.raw.match(/\}/g) ?? []).length;
5635
+ depth += countInCode(file, bl, "{");
5636
+ depth -= countInCode(file, bl, "}");
4789
5637
  if (depth > 0) started = true;
4790
5638
  }
4791
5639
  if (started && depth <= 0) break;
@@ -4813,7 +5661,7 @@ var dExt3 = {
4813
5661
  const decided = (() => {
4814
5662
  const declared = /* @__PURE__ */ new Set();
4815
5663
  let firstAnchor2 = null;
4816
- const manifests = findExtensionManifests3(tree.root);
5664
+ const manifests = findExtensionManifests(tree);
4817
5665
  for (const manifestPath2 of manifests) {
4818
5666
  const shortPath = manifestPath2.split("/").slice(-2).join("/");
4819
5667
  const parsed = readJsonObject(manifestPath2);
@@ -4920,8 +5768,8 @@ var dExt3 = {
4920
5768
  };
4921
5769
 
4922
5770
  // src/detectors/dExt4.ts
4923
- import { readdirSync as readdirSync7, statSync as statSync8 } from "fs";
4924
- import { join as join15 } from "path";
5771
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
5772
+ import { join as join13 } from "path";
4925
5773
  function canonical(name) {
4926
5774
  return name.replace(/[-_]/g, "").toLowerCase();
4927
5775
  }
@@ -4940,71 +5788,40 @@ function dExt4PositiveControls() {
4940
5788
  }
4941
5789
  };
4942
5790
  }
4943
- var SKIP_DIRS5 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4944
- function findExtensionManifests4(root) {
5791
+ function findHooksDirs(tree) {
5792
+ const root = tree.root;
4945
5793
  const out = [];
4946
5794
  let base;
4947
5795
  try {
4948
- base = statSync8(root).isDirectory() ? root : join15(root, "..");
5796
+ base = statSync6(root).isDirectory() ? root : join13(root, "..");
4949
5797
  } catch {
4950
5798
  return out;
4951
5799
  }
4952
5800
  const walk2 = (dir) => {
4953
5801
  let entries;
4954
5802
  try {
4955
- entries = readdirSync7(dir);
5803
+ entries = readdirSync5(dir);
4956
5804
  } catch {
4957
5805
  return;
4958
5806
  }
4959
5807
  for (const name of entries) {
4960
- const full = join15(dir, name);
5808
+ const full = join13(dir, name);
4961
5809
  let st;
4962
5810
  try {
4963
- st = statSync8(full);
5811
+ st = statSync6(full);
4964
5812
  } catch {
4965
5813
  continue;
4966
5814
  }
4967
- if (st.isDirectory()) {
4968
- if (SKIP_DIRS5.has(name)) continue;
4969
- walk2(full);
4970
- } else if (name === "extension.json") {
4971
- out.push(full);
4972
- }
4973
- }
4974
- };
4975
- walk2(base);
4976
- return out;
4977
- }
4978
- function findHooksDirs(root) {
4979
- const out = [];
4980
- let base;
4981
- try {
4982
- base = statSync8(root).isDirectory() ? root : join15(root, "..");
4983
- } catch {
4984
- return out;
4985
- }
4986
- const walk2 = (dir) => {
4987
- let entries;
4988
- try {
4989
- entries = readdirSync7(dir);
4990
- } catch {
4991
- return;
4992
- }
4993
- for (const name of entries) {
4994
- const full = join15(dir, name);
4995
- let st;
4996
- try {
4997
- st = statSync8(full);
4998
- } catch {
5815
+ if (!st.isDirectory()) continue;
5816
+ if (SKIP_DIRS.has(name)) {
5817
+ recordSkip(tree, full, base);
4999
5818
  continue;
5000
5819
  }
5001
- if (!st.isDirectory()) continue;
5002
- if (SKIP_DIRS5.has(name)) continue;
5003
5820
  if (name === "hooks") {
5004
5821
  const tokens = /* @__PURE__ */ new Set();
5005
5822
  let files;
5006
5823
  try {
5007
- files = readdirSync7(full);
5824
+ files = readdirSync5(full);
5008
5825
  } catch {
5009
5826
  files = [];
5010
5827
  }
@@ -5033,7 +5850,7 @@ function findFactorySites(tree) {
5033
5850
  for (let i = 0; i < lines.length; i++) {
5034
5851
  const line = lines[i];
5035
5852
  if (line.comment) continue;
5036
- if (LIFECYCLE_OBJ_RE.test(line.raw)) {
5853
+ if (testInCode(file, line, LIFECYCLE_OBJ_RE)) {
5037
5854
  const anchor = `${file.rel}:${line.n}`;
5038
5855
  const tokens = /* @__PURE__ */ new Set();
5039
5856
  let depth = 0;
@@ -5041,20 +5858,19 @@ function findFactorySites(tree) {
5041
5858
  for (let j = i; j < lines.length; j++) {
5042
5859
  const bl = lines[j];
5043
5860
  if (bl.comment) continue;
5044
- const raw = bl.raw;
5045
5861
  if (started && depth === 1) {
5046
- const k = raw.match(OBJECT_KEY_RE);
5862
+ const k = matchInCode(file, bl, OBJECT_KEY_RE);
5047
5863
  if (k) tokens.add(canonical(k[1]));
5048
5864
  }
5049
- depth += (raw.match(/\{/g) ?? []).length;
5050
- depth -= (raw.match(/\}/g) ?? []).length;
5865
+ depth += countInCode(file, bl, "{");
5866
+ depth -= countInCode(file, bl, "}");
5051
5867
  if (depth > 0) started = true;
5052
5868
  if (started && depth <= 0) break;
5053
5869
  }
5054
5870
  sites.push({ anchor, tokens });
5055
5871
  continue;
5056
5872
  }
5057
- if (LIFECYCLE_KEY_RE.test(line.raw)) {
5873
+ if (testInCode(file, line, LIFECYCLE_KEY_RE)) {
5058
5874
  unresolved.push(`${file.rel}:${line.n}`);
5059
5875
  }
5060
5876
  }
@@ -5082,7 +5898,7 @@ var dExt4 = {
5082
5898
  }
5083
5899
  const unreadable = [];
5084
5900
  const decided = (() => {
5085
- const hooksDirs = findHooksDirs(tree.root);
5901
+ const hooksDirs = findHooksDirs(tree);
5086
5902
  const { sites, unresolved } = findFactorySites(tree);
5087
5903
  for (const anchor of unresolved) {
5088
5904
  unreadable.push({
@@ -5094,7 +5910,7 @@ var dExt4 = {
5094
5910
  for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
5095
5911
  for (const s of sites) for (const t of s.tokens) implemented.add(t);
5096
5912
  const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
5097
- const manifests = findExtensionManifests4(tree.root);
5913
+ const manifests = findExtensionManifests(tree);
5098
5914
  let declaredAnyLifecycle = false;
5099
5915
  for (const manifestPath2 of manifests) {
5100
5916
  const shortPath = manifestPath2.split("/").slice(-2).join("/");
@@ -5163,8 +5979,8 @@ var dExt4 = {
5163
5979
  };
5164
5980
 
5165
5981
  // src/detectors/dExt5.ts
5166
- import { existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync9 } from "fs";
5167
- 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";
5168
5984
  function nonEmptyString(v) {
5169
5985
  return typeof v === "string" && v.trim().length > 0;
5170
5986
  }
@@ -5178,41 +5994,6 @@ function dExt5PositiveControls() {
5178
5994
  }
5179
5995
  };
5180
5996
  }
5181
- var SKIP_DIRS6 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5182
- function findExtensionManifests5(root) {
5183
- const out = [];
5184
- let base;
5185
- try {
5186
- base = statSync9(root).isDirectory() ? root : join16(root, "..");
5187
- } catch {
5188
- return out;
5189
- }
5190
- const walk2 = (dir) => {
5191
- let entries;
5192
- try {
5193
- entries = readdirSync8(dir);
5194
- } catch {
5195
- return;
5196
- }
5197
- for (const name of entries) {
5198
- const full = join16(dir, name);
5199
- let st;
5200
- try {
5201
- st = statSync9(full);
5202
- } catch {
5203
- continue;
5204
- }
5205
- if (st.isDirectory()) {
5206
- if (SKIP_DIRS6.has(name)) continue;
5207
- walk2(full);
5208
- } else if (name === "extension.json") {
5209
- out.push(full);
5210
- }
5211
- }
5212
- };
5213
- walk2(base);
5214
- return out;
5215
- }
5216
5997
  function isToolExtension(type) {
5217
5998
  return typeof type === "string" && type.includes("tool-extension");
5218
5999
  }
@@ -5225,7 +6006,7 @@ var dExt5 = {
5225
6006
  `DExt5 bilingual control FAILED: accept(\`${FR_POSITIVE}\`)=${controls.descriptionFr.positive} reject(empty)=${!controls.descriptionFr.negative} \u2014 FR-string matcher broken, absence verdict untrustworthy`
5226
6007
  );
5227
6008
  }
5228
- const manifests = findExtensionManifests5(tree.root);
6009
+ const manifests = findExtensionManifests(tree);
5229
6010
  if (manifests.length === 0) {
5230
6011
  return {
5231
6012
  id: this.id,
@@ -5259,7 +6040,7 @@ var dExt5 = {
5259
6040
  if (!nonEmptyString(parsed.description_fr)) {
5260
6041
  missing.push("description_fr (extension.json)");
5261
6042
  }
5262
- if (!existsSync12(join16(root, "README.fr.md"))) {
6043
+ if (!existsSync12(join14(root, "README.fr.md"))) {
5263
6044
  missing.push("README.fr.md");
5264
6045
  }
5265
6046
  if (isToolExtension(parsed.type) && "tools" in parsed) {
@@ -5302,8 +6083,8 @@ var dExt5 = {
5302
6083
  };
5303
6084
 
5304
6085
  // src/detectors/dExt7.ts
5305
- import { existsSync as existsSync13, readdirSync as readdirSync9, statSync as statSync10 } from "fs";
5306
- 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";
5307
6088
  var ABSENCE_FLAG_FIELDS = [
5308
6089
  "extensionAbsent",
5309
6090
  "extension_absent",
@@ -5346,41 +6127,6 @@ function dExt7PositiveControls() {
5346
6127
  negative: isAbsenceCase(ABSENCE_NEGATIVE)
5347
6128
  };
5348
6129
  }
5349
- var SKIP_DIRS7 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5350
- function findExtensionManifests6(root) {
5351
- const out = [];
5352
- let base;
5353
- try {
5354
- base = statSync10(root).isDirectory() ? root : join17(root, "..");
5355
- } catch {
5356
- return out;
5357
- }
5358
- const walk2 = (dir) => {
5359
- let entries;
5360
- try {
5361
- entries = readdirSync9(dir);
5362
- } catch {
5363
- return;
5364
- }
5365
- for (const name of entries) {
5366
- const full = join17(dir, name);
5367
- let st;
5368
- try {
5369
- st = statSync10(full);
5370
- } catch {
5371
- continue;
5372
- }
5373
- if (st.isDirectory()) {
5374
- if (SKIP_DIRS7.has(name)) continue;
5375
- walk2(full);
5376
- } else if (name === "extension.json") {
5377
- out.push(full);
5378
- }
5379
- }
5380
- };
5381
- walk2(base);
5382
- return out;
5383
- }
5384
6130
  var dExt7 = {
5385
6131
  id: "DExt7 eval-coverage",
5386
6132
  run(tree) {
@@ -5390,7 +6136,7 @@ var dExt7 = {
5390
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`
5391
6137
  );
5392
6138
  }
5393
- const manifests = findExtensionManifests6(tree.root);
6139
+ const manifests = findExtensionManifests(tree);
5394
6140
  if (manifests.length === 0) {
5395
6141
  return {
5396
6142
  id: this.id,
@@ -5404,7 +6150,7 @@ var dExt7 = {
5404
6150
  for (const manifestPath2 of manifests) {
5405
6151
  const extDir = dirname8(manifestPath2);
5406
6152
  const extLabel = extDir.split("/").pop() ?? extDir;
5407
- const evalsPath = join17(extDir, "evals", "evals.json");
6153
+ const evalsPath = join15(extDir, "evals", "evals.json");
5408
6154
  const shortPath = `${extLabel}/evals/evals.json`;
5409
6155
  if (!existsSync13(evalsPath)) {
5410
6156
  return {
@@ -5470,8 +6216,8 @@ var dExt7 = {
5470
6216
  };
5471
6217
 
5472
6218
  // src/detectors/dExt8.ts
5473
- import { existsSync as existsSync14, readdirSync as readdirSync10, statSync as statSync11 } from "fs";
5474
- 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";
5475
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-]+)*))?$/;
5476
6222
  var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
5477
6223
  function dExt8PositiveControl() {
@@ -5481,41 +6227,6 @@ function dExt8PositiveControl() {
5481
6227
  matched: SEMVER_RE2.test(POSITIVE_CONTROL5)
5482
6228
  };
5483
6229
  }
5484
- var SKIP_DIRS8 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5485
- function findExtensionManifests7(root) {
5486
- const out = [];
5487
- let base;
5488
- try {
5489
- base = statSync11(root).isDirectory() ? root : join18(root, "..");
5490
- } catch {
5491
- return out;
5492
- }
5493
- const walk2 = (dir) => {
5494
- let entries;
5495
- try {
5496
- entries = readdirSync10(dir);
5497
- } catch {
5498
- return;
5499
- }
5500
- for (const name of entries) {
5501
- const full = join18(dir, name);
5502
- let st;
5503
- try {
5504
- st = statSync11(full);
5505
- } catch {
5506
- continue;
5507
- }
5508
- if (st.isDirectory()) {
5509
- if (SKIP_DIRS8.has(name)) continue;
5510
- walk2(full);
5511
- } else if (name === "extension.json") {
5512
- out.push(full);
5513
- }
5514
- }
5515
- };
5516
- walk2(base);
5517
- return out;
5518
- }
5519
6230
  var dExt8 = {
5520
6231
  id: "DExt8 semver+changelog",
5521
6232
  run(tree) {
@@ -5525,7 +6236,7 @@ var dExt8 = {
5525
6236
  `DExt8 positive control FAILED: pattern /${control.pattern}/ did not match sample \`${control.sample}\` \u2014 matcher broken, verdict untrustworthy`
5526
6237
  );
5527
6238
  }
5528
- const manifests = findExtensionManifests7(tree.root);
6239
+ const manifests = findExtensionManifests(tree);
5529
6240
  if (manifests.length === 0) {
5530
6241
  return {
5531
6242
  id: this.id,
@@ -5540,7 +6251,7 @@ var dExt8 = {
5540
6251
  for (const manifestPath2 of manifests) {
5541
6252
  const extDir = dirname9(manifestPath2);
5542
6253
  const extLabel = extDir.split("/").pop() ?? extDir;
5543
- const pkgPath = join18(extDir, "package.json");
6254
+ const pkgPath = join16(extDir, "package.json");
5544
6255
  const shortPkgPath = `${extLabel}/package.json`;
5545
6256
  if (!existsSync14(pkgPath)) {
5546
6257
  unreadable.push({
@@ -5577,7 +6288,7 @@ var dExt8 = {
5577
6288
  continue;
5578
6289
  }
5579
6290
  const validSemver = SEMVER_RE2.test(version);
5580
- const changelogPath = join18(extDir, "CHANGELOG.md");
6291
+ const changelogPath = join16(extDir, "CHANGELOG.md");
5581
6292
  const hasChangelog = existsSync14(changelogPath);
5582
6293
  if (!validSemver) {
5583
6294
  finding ??= {
@@ -5612,38 +6323,41 @@ var dExt8 = {
5612
6323
  };
5613
6324
 
5614
6325
  // src/detectors/dExt9.ts
5615
- import { readFileSync as readFileSync10, readdirSync as readdirSync11, statSync as statSync12 } from "fs";
5616
- import { join as join19 } from "path";
5617
- var SKIP_DIRS9 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5618
- function collectHooksDirSources(root, unreadable) {
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;
5619
6330
  const out = [];
5620
6331
  let base;
5621
6332
  try {
5622
- base = statSync12(root).isDirectory() ? root : join19(root, "..");
6333
+ base = statSync7(root).isDirectory() ? root : join17(root, "..");
5623
6334
  } catch {
5624
6335
  return out;
5625
6336
  }
5626
6337
  const walk2 = (dir) => {
5627
6338
  let entries;
5628
6339
  try {
5629
- entries = readdirSync11(dir);
6340
+ entries = readdirSync6(dir);
5630
6341
  } catch {
5631
6342
  return;
5632
6343
  }
5633
6344
  for (const name of entries) {
5634
- const full = join19(dir, name);
6345
+ const full = join17(dir, name);
5635
6346
  let st;
5636
6347
  try {
5637
- st = statSync12(full);
6348
+ st = statSync7(full);
5638
6349
  } catch {
5639
6350
  continue;
5640
6351
  }
5641
6352
  if (!st.isDirectory()) continue;
5642
- if (SKIP_DIRS9.has(name)) continue;
6353
+ if (SKIP_DIRS.has(name)) {
6354
+ recordSkip(tree, full, base);
6355
+ continue;
6356
+ }
5643
6357
  if (name === "hooks") {
5644
6358
  let files;
5645
6359
  try {
5646
- files = readdirSync11(full);
6360
+ files = readdirSync6(full);
5647
6361
  } catch {
5648
6362
  files = [];
5649
6363
  }
@@ -5651,7 +6365,7 @@ function collectHooksDirSources(root, unreadable) {
5651
6365
  if (!f.endsWith(".ts") || f.endsWith(".d.ts")) continue;
5652
6366
  const stem = f.replace(/\.ts$/, "");
5653
6367
  if (stem === "index") continue;
5654
- const filePath = join19(full, f);
6368
+ const filePath = join17(full, f);
5655
6369
  let body;
5656
6370
  try {
5657
6371
  body = readFileSync10(filePath, "utf8");
@@ -5685,7 +6399,7 @@ function collectFactorySources(tree) {
5685
6399
  for (let i = 0; i < lines.length; i++) {
5686
6400
  const line = lines[i];
5687
6401
  if (line.comment) continue;
5688
- if (!LIFECYCLE_OBJ_RE2.test(line.raw)) continue;
6402
+ if (!testInCode(file, line, LIFECYCLE_OBJ_RE2)) continue;
5689
6403
  let depth = 0;
5690
6404
  let started = false;
5691
6405
  let curKey = null;
@@ -5695,7 +6409,7 @@ function collectFactorySources(tree) {
5695
6409
  if (bl.comment) continue;
5696
6410
  const raw = bl.raw;
5697
6411
  if (started && depth === 1) {
5698
- const k = raw.match(OBJECT_KEY_RE2);
6412
+ const k = matchInCode(file, bl, OBJECT_KEY_RE2);
5699
6413
  if (k) {
5700
6414
  curKey = canonical(k[1]);
5701
6415
  keyBodies.set(curKey, []);
@@ -5704,8 +6418,8 @@ function collectFactorySources(tree) {
5704
6418
  if (curKey && depth >= 1) {
5705
6419
  keyBodies.get(curKey)?.push(raw);
5706
6420
  }
5707
- depth += (raw.match(/\{/g) ?? []).length;
5708
- depth -= (raw.match(/\}/g) ?? []).length;
6421
+ depth += countInCode(file, bl, "{");
6422
+ depth -= countInCode(file, bl, "}");
5709
6423
  if (depth > 0) started = true;
5710
6424
  if (started && depth <= 0) break;
5711
6425
  }
@@ -5756,7 +6470,7 @@ var dExt9 = {
5756
6470
  const unreadable = [];
5757
6471
  const decided = (() => {
5758
6472
  const hookSources = [
5759
- ...collectHooksDirSources(tree.root, unreadable),
6473
+ ...collectHooksDirSources(tree, unreadable),
5760
6474
  ...collectFactorySources(tree)
5761
6475
  ];
5762
6476
  const implByToken = /* @__PURE__ */ new Map();
@@ -5765,7 +6479,7 @@ var dExt9 = {
5765
6479
  list.push(s);
5766
6480
  implByToken.set(s.token, list);
5767
6481
  }
5768
- const manifests = findExtensionManifests4(tree.root);
6482
+ const manifests = findExtensionManifests(tree);
5769
6483
  let declaredAnyLifecycle = false;
5770
6484
  for (const manifestPath2 of manifests) {
5771
6485
  const shortPath = manifestPath2.split("/").slice(-2).join("/");
@@ -6547,22 +7261,23 @@ var detectors = [
6547
7261
  dExt5,
6548
7262
  dExt7,
6549
7263
  dExt8,
6550
- dExt9
7264
+ dExt9,
7265
+ localIdentity
6551
7266
  ];
6552
7267
 
6553
7268
  // src/runtime/launch.ts
6554
7269
  import { spawnSync as spawnSync2 } from "child_process";
6555
- import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync13 } from "fs";
6556
- 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";
6557
7272
  function findPackageDir(target) {
6558
7273
  let dir;
6559
7274
  try {
6560
- dir = statSync13(target).isDirectory() ? target : dirname10(target);
7275
+ dir = statSync8(target).isDirectory() ? target : dirname10(target);
6561
7276
  } catch {
6562
7277
  return null;
6563
7278
  }
6564
7279
  for (let depth = 0; depth < 8; depth++) {
6565
- if (existsSync15(join20(dir, "package.json"))) return dir;
7280
+ if (existsSync15(join18(dir, "package.json"))) return dir;
6566
7281
  const parent = dirname10(dir);
6567
7282
  if (parent === dir) break;
6568
7283
  dir = parent;
@@ -6587,10 +7302,10 @@ function deriveLaunchPlan(target) {
6587
7302
  }
6588
7303
  let pkg;
6589
7304
  try {
6590
- pkg = JSON.parse(readFileSync11(join20(pkgDir, "package.json"), "utf8"));
7305
+ pkg = JSON.parse(readFileSync11(join18(pkgDir, "package.json"), "utf8"));
6591
7306
  } catch (err) {
6592
7307
  return {
6593
- 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)}`
6594
7309
  };
6595
7310
  }
6596
7311
  const bin = firstBinEntry(pkg.bin);
@@ -6668,7 +7383,7 @@ function readEvalCounts(target) {
6668
7383
  if (!pkgDir) return counts;
6669
7384
  let dir = pkgDir;
6670
7385
  for (let depth = 0; depth < 8; depth++) {
6671
- const candidate = join20(dir, "evals", "evals.json");
7386
+ const candidate = join18(dir, "evals", "evals.json");
6672
7387
  if (existsSync15(candidate)) {
6673
7388
  try {
6674
7389
  const parsed = JSON.parse(readFileSync11(candidate, "utf8"));
@@ -6687,7 +7402,7 @@ function readEvalCounts(target) {
6687
7402
  }
6688
7403
  function statSyncSafe(p) {
6689
7404
  try {
6690
- return statSync13(p).isDirectory();
7405
+ return statSync8(p).isDirectory();
6691
7406
  } catch {
6692
7407
  return false;
6693
7408
  }
@@ -6806,6 +7521,9 @@ function reconcile(id, staticR, dynamicR) {
6806
7521
  };
6807
7522
  }
6808
7523
  function runDoctor(target, detectorList = detectors, opts = {}) {
7524
+ return runDoctorReport(target, detectorList, opts).results;
7525
+ }
7526
+ function runDoctorReport(target, detectorList = detectors, opts = {}) {
6809
7527
  const mode = opts.mode ?? "static";
6810
7528
  const tree = loadTree(target);
6811
7529
  const needsRuntime = mode !== "static" && detectorList.some((d) => typeof d.runDynamic === "function");
@@ -6840,7 +7558,7 @@ function runDoctor(target, detectorList = detectors, opts = {}) {
6840
7558
  results.push(reconcile(d.id, staticR, dynamicR));
6841
7559
  }
6842
7560
  }
6843
- return results;
7561
+ return { results, skipped: tree.skippedDirs };
6844
7562
  }
6845
7563
  async function invokeLiveChecked(d, session) {
6846
7564
  if (!session) {
@@ -6895,14 +7613,32 @@ async function runDoctorAsync(target, opts = {}) {
6895
7613
  }
6896
7614
  return results;
6897
7615
  }
6898
- 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) {
6899
7635
  const cols = { item: 24, anchor: 26, state: 15 };
6900
7636
  const header = `${pad("item", cols.item)} | ${pad("file:line", cols.anchor)} | ${pad("state", cols.state)} | verdict`;
6901
- const sep = "-".repeat(header.length);
7637
+ const sep3 = "-".repeat(header.length);
6902
7638
  const rows = results.map(
6903
7639
  (r) => `${pad(r.id, cols.item)} | ${pad(r.anchor, cols.anchor)} | ${pad(r.state, cols.state)} | ${r.verdict}`
6904
7640
  );
6905
- const table = [`surface: ${surface}`, header, sep, ...rows].join("\n");
7641
+ const table = [`surface: ${surface}`, header, sep3, ...rows].join("\n");
6906
7642
  const unjudged = results.filter((r) => r.state === "could-not-judge");
6907
7643
  if (unjudged.length === 0) return table;
6908
7644
  const cnjHeader = `could-not-judge (${unjudged.length}) \u2014 the doctor could NOT read these; this is NOT a clean bill:`;
@@ -6923,6 +7659,9 @@ var USAGE = [
6923
7659
  " --mode=static read the tree only (default)",
6924
7660
  " --mode=dynamic launch the server and judge what it serves",
6925
7661
  " --mode=both both, dynamic findings taking precedence",
7662
+ " --local-identity-ceiling=N",
7663
+ " local identity primitives tolerated before DLocalIdentity",
7664
+ " fails (default 0)",
6926
7665
  " --version print the version and exit",
6927
7666
  " --help print this and exit",
6928
7667
  "",
@@ -6931,7 +7670,7 @@ var USAGE = [
6931
7670
  function readVersion() {
6932
7671
  try {
6933
7672
  const here = dirname11(fileURLToPath(import.meta.url));
6934
- const manifest = JSON.parse(readFileSync12(join21(here, "..", "package.json"), "utf8"));
7673
+ const manifest = JSON.parse(readFileSync12(join19(here, "..", "package.json"), "utf8"));
6935
7674
  return manifest.version ?? null;
6936
7675
  } catch {
6937
7676
  return null;
@@ -6963,8 +7702,20 @@ function main() {
6963
7702
  console.error(`unknown --mode "${mode}" \u2014 expected static|dynamic|both`);
6964
7703
  process.exit(1);
6965
7704
  }
6966
- const results = runDoctor(target, void 0, { mode });
6967
- 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));
6968
7719
  process.exitCode = computeExitCode(results);
6969
7720
  }
6970
7721
  function isDirectInvocation() {
@@ -6986,7 +7737,9 @@ if (isDirectInvocation()) main();
6986
7737
  export {
6987
7738
  computeExitCode,
6988
7739
  isLiveDetector,
7740
+ renderSkipped,
6989
7741
  renderTable,
6990
7742
  runDoctor,
6991
- runDoctorAsync
7743
+ runDoctorAsync,
7744
+ runDoctorReport
6992
7745
  };