@vantageos/mcp-doctor 0.3.2 → 0.5.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,999 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/doctor.ts
4
- import { readFileSync as readFileSync12, realpathSync } from "fs";
5
- import { dirname as dirname11, join as join21, resolve as resolve2 } from "path";
4
+ import { readFileSync as readFileSync13, realpathSync } from "fs";
5
+ import { dirname as dirname12, join as join20, 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, backendRoot) {
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 (backendRoot !== void 0) tree.backendRoot = backendRoot;
123
+ if (st.isDirectory()) walk(root, filePaths, (d) => recordSkip(tree, d, root));
124
+ else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
125
+ tree.files = filePaths.map((p) => loadFile(p, baseDir));
126
+ tree.packageJson = findPackageJson(baseDir);
127
+ return tree;
128
+ }
129
+ var COM = 1;
130
+ var CODE = 2;
131
+ var maskCache = /* @__PURE__ */ new WeakMap();
132
+ function buildCodeMask(file) {
133
+ const mask = [];
134
+ let mode = "code";
135
+ const tplStack = [];
136
+ let braceDepth = 0;
137
+ let prevSig = "";
138
+ let prevWord = "";
139
+ const regexAfterKeyword = /* @__PURE__ */ new Set([
140
+ "case",
141
+ "default",
142
+ "delete",
143
+ "do",
144
+ "else",
145
+ "extends",
146
+ "in",
147
+ "instanceof",
148
+ "new",
149
+ "of",
150
+ "return",
151
+ "throw",
152
+ "typeof",
153
+ "void",
154
+ "yield",
155
+ "await"
156
+ ]);
157
+ const regexAfter = /* @__PURE__ */ new Set([
158
+ "",
159
+ "(",
160
+ ",",
161
+ "=",
162
+ ":",
163
+ "[",
164
+ "!",
165
+ "&",
166
+ "|",
167
+ "?",
168
+ "{",
169
+ "}",
170
+ ";",
171
+ "+",
172
+ "-",
173
+ "*",
174
+ "%",
175
+ "<",
176
+ ">",
177
+ "~",
178
+ "^"
179
+ ]);
180
+ for (const line of file.lines) {
181
+ const s = line.raw;
182
+ const row = new Uint8Array(s.length);
183
+ let i = 0;
184
+ while (i < s.length) {
185
+ const c = s[i];
186
+ const n = s[i + 1];
187
+ if (mode === "block") {
188
+ row[i] = COM;
189
+ if (c === "*" && n === "/") {
190
+ row[i + 1] = COM;
191
+ i += 2;
192
+ mode = "code";
193
+ } else i++;
194
+ continue;
195
+ }
196
+ if (mode === "sq" || mode === "dq") {
197
+ if (c === "\\") i += 2;
198
+ else if (mode === "sq" && c === "'" || mode === "dq" && c === '"') {
199
+ row[i] = CODE;
200
+ i++;
201
+ mode = "code";
202
+ prevSig = c;
203
+ } else i++;
204
+ continue;
205
+ }
206
+ if (mode === "tpl") {
207
+ if (c === "\\") i += 2;
208
+ else if (c === "`") {
209
+ row[i] = CODE;
210
+ i++;
211
+ mode = "code";
212
+ prevSig = c;
213
+ } else if (c === "$" && n === "{") {
214
+ row[i] = CODE;
215
+ row[i + 1] = CODE;
216
+ tplStack.push(braceDepth);
217
+ braceDepth++;
218
+ i += 2;
219
+ mode = "code";
220
+ prevSig = "{";
221
+ } else i++;
222
+ continue;
223
+ }
224
+ if (mode === "re" || mode === "recls") {
225
+ if (c === "\\") i += 2;
226
+ else if (mode === "re" && c === "[") {
227
+ mode = "recls";
228
+ i++;
229
+ } else if (mode === "recls" && c === "]") {
230
+ mode = "re";
231
+ i++;
232
+ } else if (mode === "re" && c === "/") {
233
+ row[i] = CODE;
234
+ i++;
235
+ mode = "code";
236
+ prevSig = "/";
237
+ } else i++;
238
+ continue;
239
+ }
240
+ if (c === "/" && n === "/") {
241
+ row.fill(COM, i);
242
+ break;
243
+ }
244
+ if (c === "/" && n === "*") {
245
+ mode = "block";
246
+ row[i] = COM;
247
+ row[i + 1] = COM;
248
+ i += 2;
249
+ continue;
250
+ }
251
+ row[i] = CODE;
252
+ const wordBefore = prevWord;
253
+ if (c !== " " && c !== " ") prevWord = "";
254
+ if (/[A-Za-z_$]/.test(c)) {
255
+ let j = i + 1;
256
+ while (j < s.length && /[\w$]/.test(s[j])) j++;
257
+ for (let k = i; k < j; k++) row[k] = CODE;
258
+ prevWord = prevSig === "." ? "" : s.slice(i, j);
259
+ prevSig = s[j - 1];
260
+ i = j;
261
+ continue;
262
+ }
263
+ if (c === '"' || c === "'" || c === "`") {
264
+ mode = c === '"' ? "dq" : c === "'" ? "sq" : "tpl";
265
+ i++;
266
+ continue;
267
+ }
268
+ if (c === "/" && (regexAfter.has(prevSig) || regexAfterKeyword.has(wordBefore))) {
269
+ mode = "re";
270
+ i++;
271
+ continue;
272
+ }
273
+ if (c === "{") braceDepth++;
274
+ else if (c === "}") {
275
+ braceDepth--;
276
+ if (tplStack.length > 0 && tplStack[tplStack.length - 1] === braceDepth) {
277
+ tplStack.pop();
278
+ mode = "tpl";
279
+ i++;
280
+ continue;
281
+ }
282
+ }
283
+ if (c !== " " && c !== " ") prevSig = c;
284
+ i++;
285
+ }
286
+ if (mode === "sq" || mode === "dq" || mode === "re" || mode === "recls") mode = "code";
287
+ mask.push(row);
288
+ }
289
+ return mask;
290
+ }
291
+ function maskOf(file) {
292
+ let mask = maskCache.get(file);
293
+ if (!mask) {
294
+ mask = buildCodeMask(file);
295
+ maskCache.set(file, mask);
296
+ }
297
+ return mask;
298
+ }
299
+ function isCodeAt(file, n, col) {
300
+ const mask = maskOf(file);
301
+ return mask[n - 1]?.[col] === CODE;
302
+ }
303
+ function matchInCode(file, line, re) {
304
+ const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
305
+ for (const m of line.raw.matchAll(g)) {
306
+ if (isCodeAt(file, line.n, m.index ?? 0)) return m;
307
+ }
308
+ return null;
309
+ }
310
+ function testInCode(file, line, re) {
311
+ return matchInCode(file, line, re) !== null;
312
+ }
313
+ function countInCode(file, line, ch) {
314
+ let n = 0;
315
+ for (let c = 0; c < line.raw.length; c++) {
316
+ if (line.raw[c] === ch && isCodeAt(file, line.n, c)) n++;
317
+ }
318
+ return n;
319
+ }
320
+
321
+ // src/detectors/couldNotJudge.ts
322
+ import { readFileSync as readFileSync2 } from "fs";
323
+ function couldNotJudge(id, anchor, reason) {
324
+ return {
325
+ id,
326
+ anchor,
327
+ state: "could-not-judge",
328
+ reason,
329
+ verdict: `could not judge: ${reason}`
330
+ };
331
+ }
332
+ function settle(id, decided, unreadable) {
333
+ if (unreadable.length === 0) return decided;
334
+ const named = unreadable.map((u) => `${u.anchor}: ${u.reason}`).join("; ");
335
+ if (decided.state === "non-conforming") {
336
+ return { ...decided, verdict: `${decided.verdict} [also could not read: ${named}]` };
337
+ }
338
+ const judgedRest = decided.state === "could-not-judge" ? ` ${decided.reason ?? decided.verdict}` : ` The readable remainder judged ${decided.state}: ${decided.verdict}`;
339
+ return couldNotJudge(
340
+ id,
341
+ unreadable[0].anchor,
342
+ `${unreadable.length} input(s) present but unreadable \u2014 ${named}.${judgedRest}`
343
+ );
344
+ }
345
+ function readJsonObject(path) {
346
+ let value;
347
+ try {
348
+ value = JSON.parse(readFileSync2(path, "utf8"));
349
+ } catch {
350
+ return null;
351
+ }
352
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
353
+ return value;
354
+ }
355
+ function guardUnreadablePackageJson(inner, opts) {
356
+ return {
357
+ ...inner,
358
+ run(tree) {
359
+ const decided = inner.run(tree);
360
+ const pkg = tree.packageJson;
361
+ if (!pkg?.unreadable) return decided;
362
+ const u = {
363
+ anchor: pkg.path,
364
+ reason: `package.json is present but could not be parsed (${pkg.unreadable})`
365
+ };
366
+ if (opts.authoritative && decided.state !== "could-not-judge") {
367
+ return couldNotJudge(inner.id, u.anchor, `${u.anchor}: ${u.reason}`);
368
+ }
369
+ return settle(inner.id, decided, [u]);
370
+ }
371
+ };
372
+ }
373
+
374
+ // src/detectors/dLocalIdentity.ts
375
+ var CLOUD_IDENTITY_PACKAGE = "@vantageos/cloud-identity";
376
+ var EXEMPT_MARKER = "allow-local-identity:";
377
+ var MIN_JUSTIFICATION = 8;
378
+ var TENANT_NAMES = "orgId|org_id|orgSlug|org_slug|tenantId|tenant_id";
379
+ var TENANT_PROP = new RegExp(`\\.(${TENANT_NAMES})$`, "i");
380
+ var unbracket = (x) => x.replace(/\??\.?\[\s*["'`]([\w$]+)["'`]\s*\]/g, ".$1").replace(/\?/g, "");
381
+ var FOLD_NAMES = "toLowerCase|toUpperCase|toLocaleLowerCase|toLocaleUpperCase|trim|normalize";
382
+ var FOLD_CALL = String.raw`(?:\s*\.\s*(?:${FOLD_NAMES})\s*\([^()]*\))*`;
383
+ var PATH = String.raw`[\w$.?]+(?![\w$])(?<!\.(?:${FOLD_NAMES}))`;
384
+ var OPERAND = String.raw`${PATH}(?:\[\s*["\`][\w$]+["\`]\s*\]\??[\w$.?]*)*${FOLD_CALL}`;
385
+ var NON_OPERAND = /* @__PURE__ */ new Set(["undefined", "null", "true", "false", "NaN"]);
386
+ var isLiteral = (x) => NON_OPERAND.has(x) || /^-?\d/.test(x);
387
+ var unfold = (x) => unbracket(x.replace(new RegExp(`${FOLD_CALL}$`), ""));
388
+ var lastSegment = (x) => x.split(".").pop() ?? "";
389
+ var ACTING_FIELD = /^(createdBy|assignedTo|assignee|from|fromOrchestrator|toOrchestrator|recipient|sender|callerOrchestrator|orchestrator|orchestratorName|actor|actorName|actingAs|claimedBy|callerName|agentName|fromName)$/;
390
+ var ACTING_BARE = /^(createdBy|assignedTo|assignee|fromOrchestrator|toOrchestrator|callerOrchestrator|actingAs|claimedBy)$/;
391
+ var CALLERISH = /^(caller\w*|actor\w*|acting\w*|claimed\w*|\w*Orchestrator|\w*AgentName)$/i;
392
+ var NAME_FIELD = /^(name|agentName|orchestratorName|displayName)$/;
393
+ var isActing = (x) => ACTING_FIELD.test(lastSegment(x)) && (x.includes(".") || ACTING_BARE.test(x)) || x.includes(".") && CALLERISH.test(lastSegment(x));
394
+ var isNameField = (x) => x.includes(".") && NAME_FIELD.test(lastSegment(x));
395
+ var isBareCallerish = (x) => !x.includes(".") && CALLERISH.test(x);
396
+ var comparesTenants = (a, b) => TENANT_PROP.test(a) || TENANT_PROP.test(b);
397
+ 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);
398
+ var comparesIdentity = (a, b) => comparesTenants(a, b) || comparesNames(a, b);
399
+ function operands(x, y) {
400
+ const [a, b] = [unfold(x), unfold(y)];
401
+ return isLiteral(a) || isLiteral(b) ? null : [a, b];
402
+ }
403
+ var CMP = String.raw`(${OPERAND})\s*[!=]==?\s*(${OPERAND})`;
404
+ var OBJECT_IS = String.raw`\bObject\s*\.\s*is\s*\(\s*(${OPERAND})\s*,\s*(${OPERAND})\s*\)`;
405
+ var acceptWith = (pred) => (m) => {
406
+ const ops = operands(m[1], m[2]);
407
+ return ops !== null && pred(ops[0], ops[1]);
408
+ };
409
+ var STRING_LITERAL = String.raw`(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\`[^\`$\\]*\`)`;
410
+ var LITERAL_CONCAT = new RegExp(String.raw`^${STRING_LITERAL}(?:\s*\+\s*${STRING_LITERAL})*$`);
411
+ function staticConstants(file) {
412
+ const out = /* @__PURE__ */ new Set();
413
+ const decl = /^(?:export\s+)?const\s+([\w$]+)\s*(?::[^=]+)?=\s*(.*)$/;
414
+ for (let i = 0; i < file.lines.length; i++) {
415
+ const m = file.lines[i].raw.match(decl);
416
+ if (!m) continue;
417
+ let init = m[2];
418
+ for (let j = i + 1; !/;\s*$/.test(init) && j < Math.min(file.lines.length, i + 40); j++) {
419
+ init += `
420
+ ${file.lines[j].raw}`;
421
+ }
422
+ const body = init.replace(/;\s*$/, "").trim();
423
+ if (LITERAL_CONCAT.test(body)) out.add(m[1]);
424
+ }
425
+ return out;
426
+ }
427
+ function callArgs(text, from) {
428
+ const args = [];
429
+ let depth = 0;
430
+ let start = from;
431
+ for (let i = from; i < text.length; i++) {
432
+ const c = text[i];
433
+ if (c === '"' || c === "'" || c === "`") {
434
+ for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
435
+ continue;
436
+ }
437
+ if (c === "(" || c === "[" || c === "{") depth++;
438
+ else if (c === ")" || c === "]" || c === "}") {
439
+ if (depth === 0) {
440
+ args.push(text.slice(start, i).trim());
441
+ return { args, end: i + 1 };
442
+ }
443
+ depth--;
444
+ } else if (c === "," && depth === 0) {
445
+ args.push(text.slice(start, i).trim());
446
+ start = i + 1;
447
+ }
448
+ }
449
+ return null;
450
+ }
451
+ function isStaticInput(expr, statics) {
452
+ const e = expr.trim();
453
+ if (LITERAL_CONCAT.test(e) || statics.has(e)) return true;
454
+ const wrapped = e.match(/^(?:new\s+TextEncoder\s*\(\s*\)\s*\.\s*encode|Buffer\s*\.\s*from)\s*\(/);
455
+ if (!wrapped) return false;
456
+ const inner = callArgs(e, wrapped[0].length);
457
+ return inner !== null && inner.end === e.length && isStaticInput(inner.args[0], statics);
458
+ }
459
+ function textFrom(ctx, extra = 12) {
460
+ const { file, line, idx } = ctx;
461
+ const tail = file.lines.slice(line.n, line.n + extra).map((l) => l.raw);
462
+ return [line.raw.slice(idx), ...tail].join("\n");
463
+ }
464
+ function blockFrom(text, from) {
465
+ const open = text.indexOf("{", from);
466
+ if (open < 0) return null;
467
+ let depth = 0;
468
+ for (let i = open; i < text.length; i++) {
469
+ const c = text[i];
470
+ if (c === '"' || c === "'" || c === "`") {
471
+ for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
472
+ continue;
473
+ }
474
+ if (c === "{") depth++;
475
+ else if (c === "}" && --depth === 0) return text.slice(open, i + 1);
476
+ }
477
+ return null;
478
+ }
479
+ function switchComparesIdentity(m, ctx) {
480
+ if (!ctx) return false;
481
+ const subject = unfold(m[1]);
482
+ const body = blockFrom(textFrom(ctx, 60), m[0].length);
483
+ if (body === null) return false;
484
+ const label = new RegExp(String.raw`\bcase\s+(${OPERAND})\s*:`, "g");
485
+ for (const c of body.matchAll(label)) {
486
+ const ops = operands(subject, c[1]);
487
+ if (ops !== null && comparesIdentity(ops[0], ops[1])) return true;
488
+ }
489
+ return false;
490
+ }
491
+ function literalHoldsIdentity(m) {
492
+ const arg = unfold(m[2]);
493
+ return m[1].split(",").map((e) => unfold(e.trim())).some((e) => e !== "" && !isLiteral(e) && !isLiteral(arg) && comparesIdentity(e, arg));
494
+ }
495
+ function digestsStaticInput(m, ctx) {
496
+ if (!ctx) return false;
497
+ const text = textFrom(ctx);
498
+ const open = text.indexOf("(");
499
+ if (open < 0) return false;
500
+ const call = callArgs(text, open + 1);
501
+ if (!call) return false;
502
+ if (/^subtle/.test(m[0])) {
503
+ return m[1] === "digest" && call.args.length >= 2 && isStaticInput(call.args[1], ctx.statics);
504
+ }
505
+ if (!/^createHash/.test(m[0])) return false;
506
+ const update = text.slice(call.end).match(/^\s*\.\s*update\s*\(/);
507
+ if (!update) return false;
508
+ const data = callArgs(text, call.end + update[0].length);
509
+ return data !== null && isStaticInput(data.args[0], ctx.statics);
510
+ }
511
+ var RENAMEABLE = [
512
+ "createHash",
513
+ "createHmac",
514
+ "timingSafeEqual",
515
+ "jwtVerify",
516
+ "jwtDecrypt",
517
+ "createRemoteJWKSet",
518
+ "createLocalJWKSet",
519
+ "decodeJwt",
520
+ "compactVerify",
521
+ "verifyToken"
522
+ ];
523
+ function renamedPrimitives(file) {
524
+ const text = file.lines.map((l) => l.raw).join("\n");
525
+ const out = [];
526
+ const named = /import\s+(type\s+)?\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;
527
+ for (const m of text.matchAll(named)) {
528
+ if (m[1] || m[3].startsWith(CLOUD_IDENTITY_PACKAGE)) continue;
529
+ for (const part of m[2].split(",")) {
530
+ const as = part.trim().match(/^([\w$]+)\s+as\s+([\w$]+)$/);
531
+ if (as && RENAMEABLE.includes(as[1])) out.push([as[1], as[2]]);
532
+ }
533
+ }
534
+ const destructured = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:await\s+)?(?:require|import)\s*\(\s*["']([^"']+)["']/g;
535
+ for (const m of text.matchAll(destructured)) {
536
+ if (m[2].startsWith(CLOUD_IDENTITY_PACKAGE)) continue;
537
+ for (const part of m[1].split(",")) {
538
+ const as = part.trim().match(/^([\w$]+)\s*:\s*([\w$]+)$/);
539
+ if (as && RENAMEABLE.includes(as[1])) out.push([as[1], as[2]]);
540
+ }
541
+ }
542
+ return out;
543
+ }
544
+ var escapeRe = (x) => x.replace(/[$]/g, "\\$&");
545
+ function aliasMatchers(file) {
546
+ const out = [];
547
+ for (const [orig, alias] of renamedPrimitives(file)) {
548
+ for (const mt of MATCHERS) {
549
+ if (!mt.re.source.includes(orig)) continue;
550
+ const re = new RegExp(mt.re.source.split(orig).join(escapeRe(alias)), mt.re.flags);
551
+ const { accept } = mt;
552
+ out.push({
553
+ ...mt,
554
+ re,
555
+ // The predicate reads the matched text; give it the original callee name.
556
+ accept: accept ? (m, ctx) => {
557
+ const fake = Object.assign(
558
+ [...m].map((x) => x?.replace(alias, orig)),
559
+ {
560
+ index: m.index,
561
+ input: m.input
562
+ }
563
+ );
564
+ return accept(fake, ctx);
565
+ } : void 0
566
+ });
567
+ }
568
+ }
569
+ return out;
570
+ }
571
+ var MATCHERS = [
572
+ {
573
+ id: "local-jwt-verify",
574
+ category: "resolves-caller",
575
+ re: /\b(jwtVerify|jwtDecrypt|createRemoteJWKSet|createLocalJWKSet|decodeJwt|compactVerify|verifyToken)\s*\(/,
576
+ callee: 1,
577
+ sample: "const { payload } = await jwtVerify(token, jwks);"
578
+ },
579
+ {
580
+ id: "local-jwt-verify",
581
+ category: "resolves-caller",
582
+ re: /\b(jwt|jsonwebtoken)\.(verify|decode)\s*\(/,
583
+ sample: "const claims = jwt.verify(token, key);"
584
+ },
585
+ {
586
+ id: "raw-claim-read",
587
+ category: "resolves-caller",
588
+ re: /\b(payload|claims|decoded)\??\.(org_id|org_slug|org_role|orgId|orgSlug|orgRole)\b/,
589
+ sample: "const orgId = payload.org_id as string;"
590
+ },
591
+ {
592
+ // A populated grant attached locally: a non-empty literal, or a value read
593
+ // off another object (a mapping row). A pass-through copy of the same
594
+ // field (`fromAllowList: ctx.fromAllowList`) grants nothing new.
595
+ id: "local-grant",
596
+ category: "resolves-caller",
597
+ re: /\bfromAllowList\s*:\s*(\[\s*[^\]\s][^\]]*\]|[\w$]+(?:\??\.[\w$]+)+)/,
598
+ accept: (m) => !/\.fromAllowList$/.test(m[1]),
599
+ sample: 'fromAllowList: ["*"],'
600
+ },
601
+ {
602
+ // A role compared to a value. A presence test (`orgRole !== undefined`,
603
+ // forwarding the claim on) asserts nothing and is not a site.
604
+ id: "org-role-assert",
605
+ category: "asserts-membership",
606
+ re: /\b(orgRole|org_role)\s*[!=]==\s*([^\s);&|?:]+)/,
607
+ accept: (m) => !NON_OPERAND.has(m[2]),
608
+ sample: 'if (orgRole === "org:admin") return true;'
609
+ },
610
+ {
611
+ id: "org-role-assert",
612
+ category: "asserts-membership",
613
+ re: /[!=]==\s*["'`]org:|\.has\(\s*\{\s*(role|permission)\s*:/,
614
+ sample: 'if (auth.has({ role: "org:admin" })) return true;'
615
+ },
616
+ {
617
+ // Identifying an agent by NAME: a typed name compared to a roster or an
618
+ // allow-list, by method or by iteration.
619
+ id: "name-allowlist",
620
+ category: "filters-by-scope",
621
+ re: /\b[\w$]*(roster|allow_?list|allowed_?orchestrators)\s*\??\.\s*(includes|indexOf|some|find|findIndex|every|has)\s*\(/i,
622
+ sample: "if (ctx.fromAllowList.includes(from)) return null;"
623
+ },
624
+ {
625
+ id: "name-allowlist",
626
+ category: "filters-by-scope",
627
+ re: /\b(isIn\w*Allow_?list)\s*\(/i,
628
+ callee: 1,
629
+ sample: "if (!isInAllowList(list, name)) return false;"
630
+ },
631
+ {
632
+ id: "name-allowlist",
633
+ category: "filters-by-scope",
634
+ re: /\bof\s+[\w$.?]*(roster|allow_?list|allowed_?orchestrators)\s*\)/i,
635
+ sample: "for (const entry of ctx.fromAllowList) {"
636
+ },
637
+ {
638
+ // A row kept or dropped by comparing its tenant to the caller's: ===, !==, == or !=.
639
+ id: "tenant-row-filter",
640
+ category: "filters-by-scope",
641
+ re: new RegExp(CMP),
642
+ accept: acceptWith(comparesTenants),
643
+ sample: "return row.orgId == ctx.actor.orgSlug;"
644
+ },
645
+ {
646
+ id: "tenant-row-filter",
647
+ category: "filters-by-scope",
648
+ re: new RegExp(OBJECT_IS),
649
+ accept: acceptWith(comparesTenants),
650
+ sample: "return Object.is(row.orgId, ctx.actor.orgSlug);"
651
+ },
652
+ {
653
+ // A tenant passed to a membership test: `callerOrgs.includes(row.orgId)`.
654
+ id: "tenant-row-filter",
655
+ category: "filters-by-scope",
656
+ re: new RegExp(
657
+ String.raw`\.(includes|has|indexOf|some|find|findIndex)\s*\(\s*(${OPERAND})\s*[,)]`
658
+ ),
659
+ accept: (m) => TENANT_PROP.test(unfold(m[2])),
660
+ sample: "return callerOrgs.includes(row.orgId);"
661
+ },
662
+ {
663
+ // Identifying an agent by NAME through a normaliser: either side of a strict
664
+ // comparison is a call to `normalize...Id/Name/Orchestrator/Agent/...`.
665
+ id: "name-compare",
666
+ category: "filters-by-scope",
667
+ 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,
668
+ sample: "return normalizeOrchestratorId(claimedName) === only;"
669
+ },
670
+ {
671
+ // A typed or argument name compared straight to an allow-list / roster entry.
672
+ id: "name-compare",
673
+ category: "filters-by-scope",
674
+ re: /(?:\b[\w$.?]*(?:roster|allow_?list|allowed_?orchestrators)\s*\??\.?\[[^\]]*\]\s*[!=]==|[!=]==\s*[\w$.?]*(?:roster|allow_?list|allowed_?orchestrators)\s*\??\.?\[)/i,
675
+ sample: "return claimedName === ctx.fromAllowList[0];"
676
+ },
677
+ {
678
+ // Identifying an agent by NAME through its acting-name fields: who made a row
679
+ // against who is asking. ===, !==, == and !=, case-folded or not.
680
+ id: "name-compare",
681
+ category: "filters-by-scope",
682
+ re: new RegExp(CMP),
683
+ accept: acceptWith(comparesNames),
684
+ sample: "return row.assignedTo.toLowerCase() !== a.callerOrchestrator.toLowerCase();"
685
+ },
686
+ {
687
+ id: "name-compare",
688
+ category: "filters-by-scope",
689
+ re: new RegExp(OBJECT_IS),
690
+ accept: acceptWith(comparesNames),
691
+ sample: "return Object.is(row.createdBy, m.from);"
692
+ },
693
+ {
694
+ // `switch (row.createdBy) { case caller: }` is the same compare, spelled as a jump table.
695
+ id: "name-compare",
696
+ category: "filters-by-scope",
697
+ re: new RegExp(String.raw`\bswitch\s*\(\s*(${OPERAND})\s*\)`),
698
+ accept: (m, ctx) => switchComparesIdentity(m, ctx),
699
+ sample: "switch (row.createdBy) { case caller: return true; }"
700
+ },
701
+ {
702
+ // `[caller].includes(row.createdBy)`: a membership test against a literal that holds the caller.
703
+ id: "name-compare",
704
+ category: "filters-by-scope",
705
+ re: new RegExp(
706
+ String.raw`(?<![\w$\])])\[([^\[\]]*)\]\s*\.\s*(?:includes|indexOf)\s*\(\s*(${OPERAND})\s*[,)]`
707
+ ),
708
+ accept: (m) => literalHoldsIdentity(m),
709
+ sample: "return [caller].includes(row.createdBy);"
710
+ },
711
+ {
712
+ id: "name-compare",
713
+ category: "filters-by-scope",
714
+ re: new RegExp(
715
+ String.raw`\bnew\s+Set\s*\(\s*\[([^\[\]]*)\]\s*\)\s*\.\s*has\s*\(\s*(${OPERAND})\s*[,)]`
716
+ ),
717
+ accept: (m) => literalHoldsIdentity(m),
718
+ sample: "return new Set([caller]).has(row.assignedTo);"
719
+ },
720
+ {
721
+ // `a.localeCompare(b) === 0` is an equality test, whichever way round it is written.
722
+ id: "name-compare",
723
+ category: "filters-by-scope",
724
+ re: new RegExp(
725
+ String.raw`(${OPERAND})\s*\.\s*localeCompare\s*\(\s*(${OPERAND})\s*(?:,[^()]*(?:\([^()]*\)[^()]*)*)?\)\s*[!=]==?\s*0(?![\w$.])`
726
+ ),
727
+ accept: acceptWith(comparesIdentity),
728
+ sample: "return row.assignedTo.localeCompare(a.callerOrchestrator) === 0;"
729
+ },
730
+ {
731
+ id: "name-compare",
732
+ category: "filters-by-scope",
733
+ re: new RegExp(
734
+ String.raw`(?<![\w$.])0\s*[!=]==?\s*(${OPERAND})\s*\.\s*localeCompare\s*\(\s*(${OPERAND})\s*[,)]`
735
+ ),
736
+ accept: acceptWith(comparesIdentity),
737
+ sample: "return 0 === row.assignedTo.localeCompare(a.callerOrchestrator);"
738
+ },
739
+ {
740
+ id: "local-secret-compare",
741
+ category: "validates-credential",
742
+ re: /\b(timingSafeEqual)\s*\(/,
743
+ callee: 1,
744
+ sample: "return timingSafeEqual(a, b);"
745
+ },
746
+ {
747
+ // Any digest of a presented credential, whatever the algorithm.
748
+ id: "local-digest",
749
+ category: "validates-credential",
750
+ // A digest over static content (a literal, or a constant made of literals) is
751
+ // not a digest of a credential. HMAC and sign/verify always take a key: a site.
752
+ re: /\bcreateHash\s*\(|\bcreateHmac\s*\(|\bsubtle\s*\.\s*(digest|sign|verify)\s*\(/,
753
+ accept: (m, ctx) => !digestsStaticInput(m, ctx),
754
+ sample: 'const h = await crypto.subtle.digest("SHA-256", data);'
755
+ }
756
+ ];
757
+ function biteSample(mt, text) {
758
+ const m = text.match(mt.re);
759
+ if (m === null) return false;
760
+ if (!mt.accept) return true;
761
+ const line = { n: 1, raw: text, comment: false };
762
+ const file = { path: "sample", rel: "sample", lines: [line] };
763
+ return mt.accept(m, { file, line, idx: m.index ?? 0, statics: /* @__PURE__ */ new Set() });
764
+ }
765
+ function localIdentityPositiveControls() {
766
+ const out = {};
767
+ MATCHERS.forEach((mt, i) => {
768
+ out[`${mt.id}#${i}`] = biteSample(mt, mt.sample);
769
+ });
770
+ const bites = (line) => MATCHERS.some((mt) => biteSample(mt, line));
771
+ out["negative:pass-through-grant"] = !bites("fromAllowList: ctx.fromAllowList,");
772
+ out["negative:delegated-bearer"] = !MATCHERS.some(
773
+ (mt) => mt.re.test("const r = await validateMasterBearer(authHeader, secret);")
774
+ );
775
+ out["negative:role-presence-test"] = !bites(
776
+ "...(ctx.orgRole !== undefined ? { role: ctx.orgRole } : {}),"
777
+ );
778
+ out["negative:undefined-compare"] = !bites("if (row.orgId === undefined) return;");
779
+ out["negative:acting-field-vs-nullish"] = !bites("if (row.assignedTo !== undefined) return;");
780
+ out["negative:acting-field-vs-literal"] = !bites('return t.createdBy === "system";');
781
+ out["negative:unrelated-field-compare"] = !bites("return page.from === page.to;");
782
+ out["negative:switch-on-unrelated-field"] = !bites('switch (row.kind) { case "a": return 1; }');
783
+ out["negative:literal-array-unrelated"] = !bites("return [kind].includes(row.status);");
784
+ out["negative:localecompare-unrelated"] = !bites("return a.name.localeCompare(b.name) === 0;");
785
+ out["negative:object-is-nullish"] = !bites("return Object.is(row.orgId, undefined);");
786
+ return out;
787
+ }
788
+ function cloudIdentityBindings(file) {
789
+ const text = file.lines.map((l) => l.raw).join("\n");
790
+ const names = /* @__PURE__ */ new Set();
791
+ const namespaces = /* @__PURE__ */ new Set();
792
+ const pkg = `@vantageos\\/cloud-identity(?:\\/[^"']*)?`;
793
+ const named = new RegExp(`import\\s+(type\\s+)?\\{([^}]*)\\}\\s*from\\s*["']${pkg}["']`, "g");
794
+ for (const m of text.matchAll(named)) {
795
+ if (m[1]) continue;
796
+ for (const part of m[2].split(",")) {
797
+ const p = part.trim();
798
+ if (!p || p.startsWith("type ")) continue;
799
+ const as = p.match(/^[\w$]+\s+as\s+([\w$]+)$/);
800
+ names.add(as ? as[1] : p);
801
+ }
802
+ }
803
+ const ns = new RegExp(`import\\s+\\*\\s+as\\s+([\\w$]+)\\s+from\\s*["']${pkg}["']`, "g");
804
+ for (const m of text.matchAll(ns)) namespaces.add(m[1]);
805
+ return { names, namespaces };
806
+ }
807
+ function isDelegated(raw, start, callee, b) {
808
+ const before = raw.slice(0, start);
809
+ const qualified = before.match(/([\w$]+)\s*\.\s*$/);
810
+ if (qualified) return b.namespaces.has(qualified[1]);
811
+ return b.names.has(callee);
812
+ }
813
+ function countDelegatedCalls(file, b) {
814
+ if (b.names.size === 0 && b.namespaces.size === 0) return 0;
815
+ let n = 0;
816
+ for (const line of file.lines) {
817
+ if (line.comment) continue;
818
+ for (const m of line.raw.matchAll(/([\w$]+)\s*\(/g)) {
819
+ const idx = m.index ?? 0;
820
+ if (isCodeAt(file, line.n, idx) && isDelegated(line.raw, idx, m[1], b)) n++;
821
+ }
822
+ }
823
+ return n;
824
+ }
825
+ var DECL = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\s*\*?\s*([\w$]+)|(?:const|let|var|class)\s+([\w$]+))/;
826
+ function enclosingDecl(file, n) {
827
+ for (let i = n - 1; i >= 0; i--) {
828
+ const l = file.lines[i];
829
+ if (l.comment) continue;
830
+ const m = l.raw.match(DECL);
831
+ if (m) return { name: m[1] ?? m[2], line: l.n };
832
+ }
833
+ return null;
834
+ }
835
+ function justificationIn(line, file) {
836
+ const at = line.raw.indexOf(EXEMPT_MARKER);
837
+ if (at < 0) return null;
838
+ if (!line.comment && isCodeAt(file, line.n, at)) return null;
839
+ if (!line.comment && !/\/\/|\/\*/.test(line.raw.slice(0, at))) return null;
840
+ return line.raw.slice(at + EXEMPT_MARKER.length).replace(/\*\/\s*$/, "").trim();
841
+ }
842
+ function commentBlockAbove(file, n) {
843
+ const out = [];
844
+ for (let i = n - 2; i >= 0; i--) {
845
+ const l = file.lines[i];
846
+ if (!l.comment) break;
847
+ out.push(l);
848
+ }
849
+ return out;
850
+ }
851
+ function findJustification(file, line, decl) {
852
+ const candidates = [line, ...commentBlockAbove(file, line.n)];
853
+ if (decl) candidates.push(...commentBlockAbove(file, decl.line));
854
+ for (const c of candidates) {
855
+ const j = justificationIn(c, file);
856
+ if (j !== null) return { text: j, valid: j.length >= MIN_JUSTIFICATION };
857
+ }
858
+ return null;
859
+ }
860
+ function unreadableReason(file) {
861
+ if (file.lines.some((l) => l.raw.includes("\0") || l.raw.includes("\uFFFD"))) {
862
+ return "file is not valid UTF-8 text";
863
+ }
864
+ const pairs = [
865
+ ["{", "}"],
866
+ ["(", ")"],
867
+ ["[", "]"]
868
+ ];
869
+ const off = [];
870
+ for (const [open, close] of pairs) {
871
+ let net = 0;
872
+ for (const l of file.lines) net += countInCode(file, l, open) - countInCode(file, l, close);
873
+ if (net !== 0) off.push(`${open}${close} ${net > 0 ? "+" : ""}${net}`);
874
+ }
875
+ return off.length > 0 ? `brackets never balance (${off.join(", ")}) \u2014 the file cannot be read as code` : null;
876
+ }
877
+ function findLocalIdentitySites(tree) {
878
+ const pkg = tree.packageJson;
879
+ const selfExempt = !pkg?.unreadable && pkg?.json.name === CLOUD_IDENTITY_PACKAGE;
880
+ const scan = {
881
+ selfExempt,
882
+ violations: [],
883
+ exempt: [],
884
+ delegatedCalls: 0,
885
+ unreadable: []
886
+ };
887
+ for (const file of tree.files) {
888
+ const bad = unreadableReason(file);
889
+ if (bad) scan.unreadable.push({ anchor: file.rel, reason: bad });
890
+ const bindings = cloudIdentityBindings(file);
891
+ const statics = staticConstants(file);
892
+ const catalog = [...MATCHERS, ...aliasMatchers(file)];
893
+ scan.delegatedCalls += countDelegatedCalls(file, bindings);
894
+ for (const line of file.lines) {
895
+ if (line.comment) continue;
896
+ let hit = null;
897
+ for (const mt of catalog) {
898
+ const g = new RegExp(mt.re.source, `${mt.re.flags.replace("g", "")}g`);
899
+ for (const m of line.raw.matchAll(g)) {
900
+ const idx = m.index ?? 0;
901
+ if (!isCodeAt(file, line.n, idx)) continue;
902
+ if (mt.accept && !mt.accept(m, { file, line, idx, statics })) continue;
903
+ const callee = mt.callee !== void 0 ? m[mt.callee] : void 0;
904
+ if (callee && isDelegated(line.raw, idx, callee, bindings)) continue;
905
+ hit = mt;
906
+ break;
907
+ }
908
+ if (hit) break;
909
+ }
910
+ if (!hit) continue;
911
+ const decl = enclosingDecl(file, line.n);
912
+ const site = {
913
+ file: file.rel,
914
+ line: line.n,
915
+ primitive: hit.id,
916
+ category: hit.category,
917
+ enclosing: decl
918
+ };
919
+ const j = findJustification(file, line, decl);
920
+ if (j?.valid) scan.exempt.push({ ...site, justification: j.text });
921
+ else scan.violations.push(site);
922
+ }
923
+ }
924
+ return scan;
925
+ }
926
+ function describeSite(s) {
927
+ const where = s.enclosing ? ` in ${s.enclosing.name} (declared :${s.enclosing.line})` : "";
928
+ return `${s.file}:${s.line} [${s.category}/${s.primitive}]${where}`;
929
+ }
930
+ function exemptSuffix(exempt) {
931
+ if (exempt.length === 0) return "";
932
+ const rows = exempt.map((e) => `${describeSite(e)} \u2014 "${e.justification}"`);
933
+ return `; EXEMPT (${exempt.length}): ${rows.join("; ")}`;
934
+ }
935
+ function makeLocalIdentity(opts = {}) {
936
+ const ceiling = opts.ceiling ?? 0;
937
+ if (!Number.isInteger(ceiling) || ceiling < 0) {
938
+ throw new Error(`local-identity ceiling must be a non-negative integer, got ${ceiling}`);
939
+ }
940
+ return {
941
+ id: "DLocalIdentity no-local-identity",
942
+ run(tree) {
943
+ const controls = localIdentityPositiveControls();
944
+ const failed = Object.entries(controls).filter(([, ok]) => !ok);
945
+ if (failed.length > 0) {
946
+ throw new Error(
947
+ `DLocalIdentity positive control FAILED: ${failed.map(([k]) => k).join(", ")} \u2014 matcher broken, verdict untrustworthy`
948
+ );
949
+ }
950
+ const id = this.id;
951
+ const s = findLocalIdentitySites(tree);
952
+ if (s.selfExempt) {
953
+ const n = s.violations.length + s.exempt.length;
954
+ return {
955
+ id,
956
+ anchor: tree.packageJson?.path ?? "-",
957
+ state: "present",
958
+ verdict: `EXEMPT: the target is ${CLOUD_IDENTITY_PACKAGE} itself, the one place identity primitives live (${n} primitive site(s) not counted)`
959
+ };
960
+ }
961
+ const listed = s.violations.map(describeSite).join("; ");
962
+ const delegated = `${s.delegatedCalls} call(s) to ${CLOUD_IDENTITY_PACKAGE}`;
963
+ let decided;
964
+ if (s.violations.length > ceiling) {
965
+ const v = s.violations[0];
966
+ decided = {
967
+ id,
968
+ anchor: `${v.file}:${v.line}`,
969
+ state: "non-conforming",
970
+ 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>"`
971
+ };
972
+ } else if (s.violations.length > 0) {
973
+ const v = s.violations[0];
974
+ decided = {
975
+ id,
976
+ anchor: `${v.file}:${v.line}`,
977
+ state: "present",
978
+ 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)}`
979
+ };
980
+ } else {
981
+ decided = {
982
+ id,
983
+ anchor: s.exempt[0] ? `${s.exempt[0].file}:${s.exempt[0].line}` : "-",
984
+ state: "present",
985
+ verdict: `0 local identity primitives; ${delegated} (positive-control OK)${exemptSuffix(s.exempt)}`
986
+ };
987
+ }
988
+ return settle(id, decided, s.unreadable);
989
+ }
990
+ };
991
+ }
992
+ var localIdentity = makeLocalIdentity();
993
+
8
994
  // src/detectors/c7-artifact-verifiable.ts
9
- import { existsSync, readFileSync } from "fs";
10
- import { join } from "path";
995
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
996
+ import { join as join2 } from "path";
11
997
  var PIN = "a74a042b53c71a095ab88b3073bd47728eeda814";
12
998
  var DOC_FILES = ["README.md", "README.fr.md"];
13
999
  var INSTALL_RE = /\b(?:npm\s+(?:install|i)|npx|pnpm\s+add|yarn\s+add)\s+(@?[\w./-]+)/i;
@@ -46,10 +1032,10 @@ function c7PositiveControls() {
46
1032
  }
47
1033
  function readDocText(root) {
48
1034
  for (const name of DOC_FILES) {
49
- const path = join(root, name);
50
- if (!existsSync(path)) continue;
1035
+ const path = join2(root, name);
1036
+ if (!existsSync2(path)) continue;
51
1037
  try {
52
- return { text: readFileSync(path, "utf8"), file: name };
1038
+ return { text: readFileSync3(path, "utf8"), file: name };
53
1039
  } catch {
54
1040
  }
55
1041
  }
@@ -1177,59 +2163,6 @@ var c13ContextCeiling = {
1177
2163
  }
1178
2164
  };
1179
2165
 
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
2166
  // src/detectors/c14-catalog-registration.ts
1234
2167
  var PIN3 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1235
2168
  var REQUIRED_FIELDS = [
@@ -1514,11 +2447,11 @@ var c16UntrustedOutput = {
1514
2447
  };
1515
2448
 
1516
2449
  // src/detectors/c17-supply-chain.ts
1517
- import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
1518
- import { join as join2 } from "path";
2450
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
2451
+ import { join as join3 } from "path";
1519
2452
  var PIN4 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1520
2453
  var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
1521
- var WORKFLOW_DIR = join2(".github", "workflows");
2454
+ var WORKFLOW_DIR = join3(".github", "workflows");
1522
2455
  var PROVENANCE_RE2 = /--provenance\b|\bprovenance\s*:\s*true\b|id-token:\s*write/i;
1523
2456
  function c17PositiveControls() {
1524
2457
  const sample = "npm publish --provenance --access public";
@@ -1534,7 +2467,7 @@ function c17PositiveControls() {
1534
2467
  }
1535
2468
  function findLockfile(root) {
1536
2469
  for (const name of LOCKFILES) {
1537
- if (existsSync2(join2(root, name))) return name;
2470
+ if (existsSync3(join3(root, name))) return name;
1538
2471
  }
1539
2472
  return null;
1540
2473
  }
@@ -1546,20 +2479,20 @@ function findProvenanceClaim(tree, root) {
1546
2479
  return { site: `${pkg.path}:publishConfig.provenance`, text: "provenance: true" };
1547
2480
  }
1548
2481
  }
1549
- const workflowsDir = join2(root, WORKFLOW_DIR);
1550
- if (!existsSync2(workflowsDir)) return null;
2482
+ const workflowsDir = join3(root, WORKFLOW_DIR);
2483
+ if (!existsSync3(workflowsDir)) return null;
1551
2484
  let entries = [];
1552
2485
  try {
1553
- entries = readdirSync(workflowsDir);
2486
+ entries = readdirSync2(workflowsDir);
1554
2487
  } catch {
1555
2488
  return null;
1556
2489
  }
1557
2490
  for (const name of entries) {
1558
2491
  if (!/\.ya?ml$/.test(name)) continue;
1559
- const path = join2(workflowsDir, name);
2492
+ const path = join3(workflowsDir, name);
1560
2493
  let text;
1561
2494
  try {
1562
- text = readFileSync3(path, "utf8");
2495
+ text = readFileSync4(path, "utf8");
1563
2496
  } catch {
1564
2497
  continue;
1565
2498
  }
@@ -1612,17 +2545,17 @@ var c17SupplyChain = guardUnreadablePackageJson(c17SupplyChainInner, {
1612
2545
  });
1613
2546
 
1614
2547
  // src/detectors/c18-structure-manifest.ts
1615
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
1616
- import { join as join3 } from "path";
2548
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
2549
+ import { join as join4 } from "path";
1617
2550
  var PIN5 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1618
2551
  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") };
2552
+ const extPath = join4(root, "extension.json");
2553
+ if (existsSync4(extPath)) return { kind: "extension", path: extPath };
2554
+ return { kind: "server", path: join4(root, "mcp.json") };
1622
2555
  }
1623
2556
  function readJson(path) {
1624
2557
  try {
1625
- return JSON.parse(readFileSync4(path, "utf8"));
2558
+ return JSON.parse(readFileSync5(path, "utf8"));
1626
2559
  } catch {
1627
2560
  return null;
1628
2561
  }
@@ -1670,7 +2603,7 @@ var c18StructureManifestInner = {
1670
2603
  }
1671
2604
  const { kind, path } = manifestPath(tree.root);
1672
2605
  const manifestName = kind === "extension" ? "extension.json" : "mcp.json";
1673
- if (!existsSync3(path)) {
2606
+ if (!existsSync4(path)) {
1674
2607
  return {
1675
2608
  id: this.id,
1676
2609
  anchor: tree.root,
@@ -1753,89 +2686,6 @@ var c18StructureManifest = guardUnreadablePackageJson(
1753
2686
  { authoritative: false }
1754
2687
  );
1755
2688
 
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
2689
  // src/detectors/c19-served-descriptions.ts
1840
2690
  var PIN6 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1841
2691
  var TOOL_DESC_INLINE_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(\s*(?:["'`][^"'`]+["'`]|[A-Za-z_$][\w$]*)\s*,\s*["'`]([^"'`]+)["'`]/;
@@ -2509,7 +3359,7 @@ import {
2509
3359
  writeFileSync
2510
3360
  } from "fs";
2511
3361
  import { tmpdir } from "os";
2512
- import { join as join6, relative as relative2 } from "path";
3362
+ import { basename as basename2, join as join6, relative as relative2, sep as sep2 } from "path";
2513
3363
  var PIN9 = "20136b50";
2514
3364
  var DEPLOY_CONFIG_FILES = [
2515
3365
  "railway.json",
@@ -2526,8 +3376,7 @@ var LOCKFILES2 = [
2526
3376
  { name: "pnpm-lock.yaml", command: "pnpm", args: ["install", "--frozen-lockfile"] },
2527
3377
  { name: "yarn.lock", command: "yarn", args: ["install", "--frozen-lockfile"] }
2528
3378
  ];
2529
- var SKIP_WALK_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage"]);
2530
- function walkForConfigs(dir, depth, seen) {
3379
+ function walkForConfigs(dir, depth, seen, onSkip) {
2531
3380
  if (depth > 8) return;
2532
3381
  let entries;
2533
3382
  try {
@@ -2544,8 +3393,11 @@ function walkForConfigs(dir, depth, seen) {
2544
3393
  continue;
2545
3394
  }
2546
3395
  if (st.isDirectory()) {
2547
- if (SKIP_WALK_DIRS.has(name)) continue;
2548
- walkForConfigs(full, depth + 1, seen);
3396
+ if (SKIP_DIRS.has(name)) {
3397
+ onSkip(full, depth + 1);
3398
+ continue;
3399
+ }
3400
+ walkForConfigs(full, depth + 1, seen, onSkip);
2549
3401
  } else if (DEPLOY_CONFIG_FILES.includes(name)) {
2550
3402
  if (!seen.has(dir)) {
2551
3403
  seen.set(dir, { dir, configFile: name, configPath: full });
@@ -2553,6 +3405,37 @@ function walkForConfigs(dir, depth, seen) {
2553
3405
  }
2554
3406
  }
2555
3407
  }
3408
+ function probeForCandidate(dir, depth) {
3409
+ if (depth > 8) return null;
3410
+ let entries;
3411
+ try {
3412
+ entries = readdirSync3(dir).sort();
3413
+ } catch {
3414
+ return null;
3415
+ }
3416
+ const subdirs = [];
3417
+ for (const name of entries) {
3418
+ const full = join6(dir, name);
3419
+ let st;
3420
+ try {
3421
+ st = statSync2(full);
3422
+ } catch {
3423
+ continue;
3424
+ }
3425
+ if (st.isDirectory()) {
3426
+ const info = SKIP_DIR_INFO[name];
3427
+ if (info && !info.authored) continue;
3428
+ subdirs.push(full);
3429
+ } else if (DEPLOY_CONFIG_FILES.includes(name)) {
3430
+ return full;
3431
+ }
3432
+ }
3433
+ for (const sub of subdirs) {
3434
+ const hit = probeForCandidate(sub, depth + 1);
3435
+ if (hit) return hit;
3436
+ }
3437
+ return null;
3438
+ }
2556
3439
  function resolveRailwayRootDirectory(configPath) {
2557
3440
  let text;
2558
3441
  try {
@@ -2574,15 +3457,31 @@ function resolveRailwayRootDirectory(configPath) {
2574
3457
  const rootDirectory = typeof parsed.rootDirectory === "string" && parsed.rootDirectory || build && typeof build.rootDirectory === "string" && build.rootDirectory || null;
2575
3458
  return rootDirectory;
2576
3459
  }
2577
- function c26DeriveScope(root) {
3460
+ function c26DeriveScope(root, tree) {
2578
3461
  const seen = /* @__PURE__ */ new Map();
2579
- walkForConfigs(root, 0, seen);
3462
+ const skipped = [];
3463
+ const onSkip = (skippedDir, depth) => {
3464
+ const entry = tree ? recordSkip(tree, skippedDir, root) : {
3465
+ path: relative2(root, skippedDir).split(sep2).join("/"),
3466
+ reason: SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
3467
+ };
3468
+ if (SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
3469
+ const hit = probeForCandidate(skippedDir, depth);
3470
+ if (hit) entry.candidate = relative2(root, hit).split(sep2).join("/");
3471
+ }
3472
+ if (!skipped.some((d) => d.path === entry.path)) skipped.push(entry);
3473
+ };
3474
+ walkForConfigs(root, 0, seen, onSkip);
2580
3475
  const sites = [];
2581
3476
  for (const site of seen.values()) {
2582
3477
  if (site.configFile === "railway.json") {
2583
3478
  const resolved = resolveRailwayRootDirectory(site.configPath);
2584
3479
  if (resolved && typeof resolved === "object" && "error" in resolved) {
2585
- return { sites: [], unparsable: { path: site.configPath, reason: resolved.error } };
3480
+ return {
3481
+ sites: [],
3482
+ skipped,
3483
+ unparsable: { path: site.configPath, reason: resolved.error }
3484
+ };
2586
3485
  }
2587
3486
  if (typeof resolved === "string" && resolved.trim()) {
2588
3487
  sites.push({ ...site, dir: join6(site.dir, resolved) });
@@ -2595,6 +3494,7 @@ function c26DeriveScope(root) {
2595
3494
  } catch (err) {
2596
3495
  return {
2597
3496
  sites: [],
3497
+ skipped,
2598
3498
  unparsable: {
2599
3499
  path: site.configPath,
2600
3500
  reason: `could not parse ${site.configPath} as JSON: ${err instanceof Error ? err.message : String(err)}`
@@ -2604,7 +3504,7 @@ function c26DeriveScope(root) {
2604
3504
  }
2605
3505
  sites.push(site);
2606
3506
  }
2607
- return { sites };
3507
+ return { sites, skipped };
2608
3508
  }
2609
3509
  function findLockfile2(dir) {
2610
3510
  for (const l of LOCKFILES2) {
@@ -2761,7 +3661,18 @@ var c26SubpackageLockfile = {
2761
3661
  rule: "C-26",
2762
3662
  pin: PIN9,
2763
3663
  run(tree) {
2764
- const scope = c26DeriveScope(tree.root);
3664
+ const scope = c26DeriveScope(tree.root, tree);
3665
+ const hidden = scope.skipped.filter((d) => d.candidate !== void 0);
3666
+ const unjudgedHidden = (prefix) => {
3667
+ 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`;
3668
+ return {
3669
+ id: this.id,
3670
+ anchor: join6(tree.root, hidden[0].candidate),
3671
+ state: "could-not-judge",
3672
+ reason,
3673
+ verdict: `could not judge: ${reason}`
3674
+ };
3675
+ };
2765
3676
  if (scope.unparsable) {
2766
3677
  const reason = `deployment configuration ${scope.unparsable.path} could not be parsed: ${scope.unparsable.reason} \u2014 scope unestablished, C-26 cannot derive its targets`;
2767
3678
  return {
@@ -2773,6 +3684,7 @@ var c26SubpackageLockfile = {
2773
3684
  };
2774
3685
  }
2775
3686
  if (scope.sites.length === 0) {
3687
+ if (hidden.length > 0) return unjudgedHidden("");
2776
3688
  return {
2777
3689
  id: this.id,
2778
3690
  anchor: tree.root,
@@ -2784,6 +3696,7 @@ var c26SubpackageLockfile = {
2784
3696
  const inventory = renderCoverage(scope.sites, verdicts);
2785
3697
  const analysed = verdicts.filter((v) => v.status === "analysed");
2786
3698
  if (analysed.length === 0) {
3699
+ if (hidden.length > 0) return unjudgedHidden("no analysable site; ");
2787
3700
  return {
2788
3701
  id: this.id,
2789
3702
  anchor: tree.root,
@@ -2800,6 +3713,11 @@ var c26SubpackageLockfile = {
2800
3713
  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
3714
  };
2802
3715
  }
3716
+ if (hidden.length > 0) {
3717
+ return unjudgedHidden(
3718
+ `${analysed.length} analysed site(s) are consistent, but coverage is partial; `
3719
+ );
3720
+ }
2803
3721
  return {
2804
3722
  id: this.id,
2805
3723
  anchor: analysed[0].site.dir,
@@ -4369,24 +5287,12 @@ var dDebugOptIn = {
4369
5287
  }
4370
5288
  };
4371
5289
 
4372
- // src/detectors/dExt1.ts
5290
+ // src/detectors/extManifests.ts
4373
5291
  import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4374
5292
  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) {
5293
+ function findExtensionManifests(tree) {
4389
5294
  const out = [];
5295
+ const root = tree.root;
4390
5296
  let base;
4391
5297
  try {
4392
5298
  base = statSync5(root).isDirectory() ? root : join12(root, "..");
@@ -4409,7 +5315,10 @@ function findExtensionManifests(root) {
4409
5315
  continue;
4410
5316
  }
4411
5317
  if (st.isDirectory()) {
4412
- if (SKIP_DIRS2.has(name)) continue;
5318
+ if (SKIP_DIRS.has(name)) {
5319
+ recordSkip(tree, full, base);
5320
+ continue;
5321
+ }
4413
5322
  walk2(full);
4414
5323
  } else if (name === "extension.json") {
4415
5324
  out.push(full);
@@ -4419,20 +5328,34 @@ function findExtensionManifests(root) {
4419
5328
  walk2(base);
4420
5329
  return out;
4421
5330
  }
5331
+
5332
+ // src/detectors/dExt1.ts
5333
+ var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
5334
+ var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
5335
+ var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
5336
+ function dExt1PositiveControls() {
5337
+ return {
5338
+ identifier: {
5339
+ pattern: IDENTIFIER_RE2.source,
5340
+ positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
5341
+ negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
5342
+ }
5343
+ };
5344
+ }
4422
5345
  var TS_IDENT_LITERAL_RE = /\bidentifier:\s*["'`]([^"'`]*)["'`]/;
4423
- var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`]/;
5346
+ var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`{]/;
4424
5347
  function findTsIdentifiers(tree) {
4425
5348
  const sites = [];
4426
5349
  const unresolved = [];
4427
5350
  for (const file of tree.files) {
4428
5351
  for (const line of file.lines) {
4429
5352
  if (line.comment) continue;
4430
- const lit = line.raw.match(TS_IDENT_LITERAL_RE);
5353
+ const lit = matchInCode(file, line, TS_IDENT_LITERAL_RE);
4431
5354
  if (lit) {
4432
5355
  sites.push({ value: lit[1], anchor: `${file.rel}:${line.n}` });
4433
5356
  continue;
4434
5357
  }
4435
- if (TS_IDENT_KEY_RE.test(line.raw)) {
5358
+ if (testInCode(file, line, TS_IDENT_KEY_RE)) {
4436
5359
  unresolved.push(`${file.rel}:${line.n}`);
4437
5360
  }
4438
5361
  }
@@ -4450,7 +5373,7 @@ var dExt1 = {
4450
5373
  }
4451
5374
  const unreadable = [];
4452
5375
  const decided = (() => {
4453
- const manifests = findExtensionManifests(tree.root);
5376
+ const manifests = findExtensionManifests(tree);
4454
5377
  for (const manifestPath2 of manifests) {
4455
5378
  const shortPath = manifestPath2.split("/").slice(-2).join("/");
4456
5379
  const parsed = readJsonObject(manifestPath2);
@@ -4517,8 +5440,6 @@ var dExt1 = {
4517
5440
  };
4518
5441
 
4519
5442
  // src/detectors/dExt2.ts
4520
- import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
4521
- import { join as join13 } from "path";
4522
5443
  var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
4523
5444
  var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
4524
5445
  var TS_GD_KEY_RE = /\bgracefulDegradation:\s*([^\s,}]+)/;
@@ -4533,41 +5454,6 @@ function dExt2PositiveControls() {
4533
5454
  }
4534
5455
  };
4535
5456
  }
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
5457
  function findTsFactorySites(tree) {
4572
5458
  const sites = [];
4573
5459
  for (const file of tree.files) {
@@ -4575,7 +5461,7 @@ function findTsFactorySites(tree) {
4575
5461
  for (let i = 0; i < lines.length; i++) {
4576
5462
  const line = lines[i];
4577
5463
  if (line.comment) continue;
4578
- if (!DEFINE_EXTENSION_OPEN_RE.test(line.raw)) continue;
5464
+ if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE)) continue;
4579
5465
  const anchor = `${file.rel}:${line.n}`;
4580
5466
  let depth = 0;
4581
5467
  let started = false;
@@ -4585,20 +5471,20 @@ function findTsFactorySites(tree) {
4585
5471
  const bl = lines[j];
4586
5472
  if (!bl.comment) {
4587
5473
  if (state === "absent") {
4588
- if (TS_GD_TRUE_RE.test(bl.raw)) {
5474
+ if (testInCode(file, bl, TS_GD_TRUE_RE)) {
4589
5475
  state = "true";
4590
- } else if (TS_GD_FALSE_RE.test(bl.raw)) {
5476
+ } else if (testInCode(file, bl, TS_GD_FALSE_RE)) {
4591
5477
  state = "false";
4592
5478
  } else {
4593
- const key = bl.raw.match(TS_GD_KEY_RE);
5479
+ const key = matchInCode(file, bl, TS_GD_KEY_RE);
4594
5480
  if (key) {
4595
5481
  state = "unresolved";
4596
5482
  value = key[1];
4597
5483
  }
4598
5484
  }
4599
5485
  }
4600
- depth += (bl.raw.match(/\{/g) ?? []).length;
4601
- depth -= (bl.raw.match(/\}/g) ?? []).length;
5486
+ depth += countInCode(file, bl, "{");
5487
+ depth -= countInCode(file, bl, "}");
4602
5488
  if (depth > 0) started = true;
4603
5489
  }
4604
5490
  if (started && depth <= 0) break;
@@ -4619,7 +5505,7 @@ var dExt2 = {
4619
5505
  }
4620
5506
  const unreadable = [];
4621
5507
  const decided = (() => {
4622
- const manifests = findExtensionManifests2(tree.root);
5508
+ const manifests = findExtensionManifests(tree);
4623
5509
  for (const manifestPath2 of manifests) {
4624
5510
  const shortPath = manifestPath2.split("/").slice(-2).join("/");
4625
5511
  const parsed = readJsonObject(manifestPath2);
@@ -4695,8 +5581,6 @@ var dExt2 = {
4695
5581
  };
4696
5582
 
4697
5583
  // src/detectors/dExt3.ts
4698
- import { readdirSync as readdirSync6, statSync as statSync7 } from "fs";
4699
- import { join as join14 } from "path";
4700
5584
  var TYPE_MATCHERS = [
4701
5585
  { type: "middleware", re: /\bmiddleware\b/ },
4702
5586
  { type: "tool-extension", re: /\btool-ext(?:ension)?\b/ },
@@ -4719,41 +5603,6 @@ function dExt3PositiveControls() {
4719
5603
  }
4720
5604
  };
4721
5605
  }
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
5606
  var DEFINE_EXTENSION_OPEN_RE2 = /\bdefineExtension\s*\(\s*\{/;
4758
5607
  var TS_TYPE_LITERAL_RE = /\btype:\s*["'`]([^"'`]*)["'`]/;
4759
5608
  var TS_TYPE_KEY_RE = /\btype:\s*[^\s"'`]/;
@@ -4764,7 +5613,7 @@ function findTsTypeSites(tree) {
4764
5613
  for (let i = 0; i < lines.length; i++) {
4765
5614
  const line = lines[i];
4766
5615
  if (line.comment) continue;
4767
- if (!DEFINE_EXTENSION_OPEN_RE2.test(line.raw)) continue;
5616
+ if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE2)) continue;
4768
5617
  const anchor = `${file.rel}:${line.n}`;
4769
5618
  let depth = 0;
4770
5619
  let started = false;
@@ -4775,17 +5624,17 @@ function findTsTypeSites(tree) {
4775
5624
  const bl = lines[j];
4776
5625
  if (!bl.comment) {
4777
5626
  if (!found) {
4778
- const lit = bl.raw.match(TS_TYPE_LITERAL_RE);
5627
+ const lit = matchInCode(file, bl, TS_TYPE_LITERAL_RE);
4779
5628
  if (lit) {
4780
5629
  value = lit[1];
4781
5630
  found = true;
4782
- } else if (TS_TYPE_KEY_RE.test(bl.raw)) {
5631
+ } else if (testInCode(file, bl, TS_TYPE_KEY_RE)) {
4783
5632
  unresolved = true;
4784
5633
  found = true;
4785
5634
  }
4786
5635
  }
4787
- depth += (bl.raw.match(/\{/g) ?? []).length;
4788
- depth -= (bl.raw.match(/\}/g) ?? []).length;
5636
+ depth += countInCode(file, bl, "{");
5637
+ depth -= countInCode(file, bl, "}");
4789
5638
  if (depth > 0) started = true;
4790
5639
  }
4791
5640
  if (started && depth <= 0) break;
@@ -4813,7 +5662,7 @@ var dExt3 = {
4813
5662
  const decided = (() => {
4814
5663
  const declared = /* @__PURE__ */ new Set();
4815
5664
  let firstAnchor2 = null;
4816
- const manifests = findExtensionManifests3(tree.root);
5665
+ const manifests = findExtensionManifests(tree);
4817
5666
  for (const manifestPath2 of manifests) {
4818
5667
  const shortPath = manifestPath2.split("/").slice(-2).join("/");
4819
5668
  const parsed = readJsonObject(manifestPath2);
@@ -4920,8 +5769,8 @@ var dExt3 = {
4920
5769
  };
4921
5770
 
4922
5771
  // src/detectors/dExt4.ts
4923
- import { readdirSync as readdirSync7, statSync as statSync8 } from "fs";
4924
- import { join as join15 } from "path";
5772
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
5773
+ import { join as join13 } from "path";
4925
5774
  function canonical(name) {
4926
5775
  return name.replace(/[-_]/g, "").toLowerCase();
4927
5776
  }
@@ -4940,71 +5789,40 @@ function dExt4PositiveControls() {
4940
5789
  }
4941
5790
  };
4942
5791
  }
4943
- var SKIP_DIRS5 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4944
- function findExtensionManifests4(root) {
5792
+ function findHooksDirs(tree) {
5793
+ const root = tree.root;
4945
5794
  const out = [];
4946
5795
  let base;
4947
5796
  try {
4948
- base = statSync8(root).isDirectory() ? root : join15(root, "..");
5797
+ base = statSync6(root).isDirectory() ? root : join13(root, "..");
4949
5798
  } catch {
4950
5799
  return out;
4951
5800
  }
4952
5801
  const walk2 = (dir) => {
4953
5802
  let entries;
4954
5803
  try {
4955
- entries = readdirSync7(dir);
5804
+ entries = readdirSync5(dir);
4956
5805
  } catch {
4957
5806
  return;
4958
5807
  }
4959
5808
  for (const name of entries) {
4960
- const full = join15(dir, name);
5809
+ const full = join13(dir, name);
4961
5810
  let st;
4962
5811
  try {
4963
- st = statSync8(full);
5812
+ st = statSync6(full);
4964
5813
  } catch {
4965
5814
  continue;
4966
5815
  }
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 {
5816
+ if (!st.isDirectory()) continue;
5817
+ if (SKIP_DIRS.has(name)) {
5818
+ recordSkip(tree, full, base);
4999
5819
  continue;
5000
5820
  }
5001
- if (!st.isDirectory()) continue;
5002
- if (SKIP_DIRS5.has(name)) continue;
5003
5821
  if (name === "hooks") {
5004
5822
  const tokens = /* @__PURE__ */ new Set();
5005
5823
  let files;
5006
5824
  try {
5007
- files = readdirSync7(full);
5825
+ files = readdirSync5(full);
5008
5826
  } catch {
5009
5827
  files = [];
5010
5828
  }
@@ -5033,7 +5851,7 @@ function findFactorySites(tree) {
5033
5851
  for (let i = 0; i < lines.length; i++) {
5034
5852
  const line = lines[i];
5035
5853
  if (line.comment) continue;
5036
- if (LIFECYCLE_OBJ_RE.test(line.raw)) {
5854
+ if (testInCode(file, line, LIFECYCLE_OBJ_RE)) {
5037
5855
  const anchor = `${file.rel}:${line.n}`;
5038
5856
  const tokens = /* @__PURE__ */ new Set();
5039
5857
  let depth = 0;
@@ -5041,20 +5859,19 @@ function findFactorySites(tree) {
5041
5859
  for (let j = i; j < lines.length; j++) {
5042
5860
  const bl = lines[j];
5043
5861
  if (bl.comment) continue;
5044
- const raw = bl.raw;
5045
5862
  if (started && depth === 1) {
5046
- const k = raw.match(OBJECT_KEY_RE);
5863
+ const k = matchInCode(file, bl, OBJECT_KEY_RE);
5047
5864
  if (k) tokens.add(canonical(k[1]));
5048
5865
  }
5049
- depth += (raw.match(/\{/g) ?? []).length;
5050
- depth -= (raw.match(/\}/g) ?? []).length;
5866
+ depth += countInCode(file, bl, "{");
5867
+ depth -= countInCode(file, bl, "}");
5051
5868
  if (depth > 0) started = true;
5052
5869
  if (started && depth <= 0) break;
5053
5870
  }
5054
5871
  sites.push({ anchor, tokens });
5055
5872
  continue;
5056
5873
  }
5057
- if (LIFECYCLE_KEY_RE.test(line.raw)) {
5874
+ if (testInCode(file, line, LIFECYCLE_KEY_RE)) {
5058
5875
  unresolved.push(`${file.rel}:${line.n}`);
5059
5876
  }
5060
5877
  }
@@ -5082,7 +5899,7 @@ var dExt4 = {
5082
5899
  }
5083
5900
  const unreadable = [];
5084
5901
  const decided = (() => {
5085
- const hooksDirs = findHooksDirs(tree.root);
5902
+ const hooksDirs = findHooksDirs(tree);
5086
5903
  const { sites, unresolved } = findFactorySites(tree);
5087
5904
  for (const anchor of unresolved) {
5088
5905
  unreadable.push({
@@ -5094,7 +5911,7 @@ var dExt4 = {
5094
5911
  for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
5095
5912
  for (const s of sites) for (const t of s.tokens) implemented.add(t);
5096
5913
  const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
5097
- const manifests = findExtensionManifests4(tree.root);
5914
+ const manifests = findExtensionManifests(tree);
5098
5915
  let declaredAnyLifecycle = false;
5099
5916
  for (const manifestPath2 of manifests) {
5100
5917
  const shortPath = manifestPath2.split("/").slice(-2).join("/");
@@ -5163,8 +5980,8 @@ var dExt4 = {
5163
5980
  };
5164
5981
 
5165
5982
  // 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";
5983
+ import { existsSync as existsSync12 } from "fs";
5984
+ import { dirname as dirname7, join as join14 } from "path";
5168
5985
  function nonEmptyString(v) {
5169
5986
  return typeof v === "string" && v.trim().length > 0;
5170
5987
  }
@@ -5178,41 +5995,6 @@ function dExt5PositiveControls() {
5178
5995
  }
5179
5996
  };
5180
5997
  }
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
5998
  function isToolExtension(type) {
5217
5999
  return typeof type === "string" && type.includes("tool-extension");
5218
6000
  }
@@ -5225,7 +6007,7 @@ var dExt5 = {
5225
6007
  `DExt5 bilingual control FAILED: accept(\`${FR_POSITIVE}\`)=${controls.descriptionFr.positive} reject(empty)=${!controls.descriptionFr.negative} \u2014 FR-string matcher broken, absence verdict untrustworthy`
5226
6008
  );
5227
6009
  }
5228
- const manifests = findExtensionManifests5(tree.root);
6010
+ const manifests = findExtensionManifests(tree);
5229
6011
  if (manifests.length === 0) {
5230
6012
  return {
5231
6013
  id: this.id,
@@ -5259,7 +6041,7 @@ var dExt5 = {
5259
6041
  if (!nonEmptyString(parsed.description_fr)) {
5260
6042
  missing.push("description_fr (extension.json)");
5261
6043
  }
5262
- if (!existsSync12(join16(root, "README.fr.md"))) {
6044
+ if (!existsSync12(join14(root, "README.fr.md"))) {
5263
6045
  missing.push("README.fr.md");
5264
6046
  }
5265
6047
  if (isToolExtension(parsed.type) && "tools" in parsed) {
@@ -5302,8 +6084,8 @@ var dExt5 = {
5302
6084
  };
5303
6085
 
5304
6086
  // 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";
6087
+ import { existsSync as existsSync13 } from "fs";
6088
+ import { dirname as dirname8, join as join15 } from "path";
5307
6089
  var ABSENCE_FLAG_FIELDS = [
5308
6090
  "extensionAbsent",
5309
6091
  "extension_absent",
@@ -5346,41 +6128,6 @@ function dExt7PositiveControls() {
5346
6128
  negative: isAbsenceCase(ABSENCE_NEGATIVE)
5347
6129
  };
5348
6130
  }
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
6131
  var dExt7 = {
5385
6132
  id: "DExt7 eval-coverage",
5386
6133
  run(tree) {
@@ -5390,7 +6137,7 @@ var dExt7 = {
5390
6137
  `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
6138
  );
5392
6139
  }
5393
- const manifests = findExtensionManifests6(tree.root);
6140
+ const manifests = findExtensionManifests(tree);
5394
6141
  if (manifests.length === 0) {
5395
6142
  return {
5396
6143
  id: this.id,
@@ -5404,7 +6151,7 @@ var dExt7 = {
5404
6151
  for (const manifestPath2 of manifests) {
5405
6152
  const extDir = dirname8(manifestPath2);
5406
6153
  const extLabel = extDir.split("/").pop() ?? extDir;
5407
- const evalsPath = join17(extDir, "evals", "evals.json");
6154
+ const evalsPath = join15(extDir, "evals", "evals.json");
5408
6155
  const shortPath = `${extLabel}/evals/evals.json`;
5409
6156
  if (!existsSync13(evalsPath)) {
5410
6157
  return {
@@ -5470,8 +6217,8 @@ var dExt7 = {
5470
6217
  };
5471
6218
 
5472
6219
  // 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";
6220
+ import { existsSync as existsSync14 } from "fs";
6221
+ import { dirname as dirname9, join as join16 } from "path";
5475
6222
  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
6223
  var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
5477
6224
  function dExt8PositiveControl() {
@@ -5481,41 +6228,6 @@ function dExt8PositiveControl() {
5481
6228
  matched: SEMVER_RE2.test(POSITIVE_CONTROL5)
5482
6229
  };
5483
6230
  }
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
6231
  var dExt8 = {
5520
6232
  id: "DExt8 semver+changelog",
5521
6233
  run(tree) {
@@ -5525,7 +6237,7 @@ var dExt8 = {
5525
6237
  `DExt8 positive control FAILED: pattern /${control.pattern}/ did not match sample \`${control.sample}\` \u2014 matcher broken, verdict untrustworthy`
5526
6238
  );
5527
6239
  }
5528
- const manifests = findExtensionManifests7(tree.root);
6240
+ const manifests = findExtensionManifests(tree);
5529
6241
  if (manifests.length === 0) {
5530
6242
  return {
5531
6243
  id: this.id,
@@ -5540,7 +6252,7 @@ var dExt8 = {
5540
6252
  for (const manifestPath2 of manifests) {
5541
6253
  const extDir = dirname9(manifestPath2);
5542
6254
  const extLabel = extDir.split("/").pop() ?? extDir;
5543
- const pkgPath = join18(extDir, "package.json");
6255
+ const pkgPath = join16(extDir, "package.json");
5544
6256
  const shortPkgPath = `${extLabel}/package.json`;
5545
6257
  if (!existsSync14(pkgPath)) {
5546
6258
  unreadable.push({
@@ -5577,7 +6289,7 @@ var dExt8 = {
5577
6289
  continue;
5578
6290
  }
5579
6291
  const validSemver = SEMVER_RE2.test(version);
5580
- const changelogPath = join18(extDir, "CHANGELOG.md");
6292
+ const changelogPath = join16(extDir, "CHANGELOG.md");
5581
6293
  const hasChangelog = existsSync14(changelogPath);
5582
6294
  if (!validSemver) {
5583
6295
  finding ??= {
@@ -5612,38 +6324,41 @@ var dExt8 = {
5612
6324
  };
5613
6325
 
5614
6326
  // 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) {
6327
+ import { readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
6328
+ import { join as join17 } from "path";
6329
+ function collectHooksDirSources(tree, unreadable) {
6330
+ const root = tree.root;
5619
6331
  const out = [];
5620
6332
  let base;
5621
6333
  try {
5622
- base = statSync12(root).isDirectory() ? root : join19(root, "..");
6334
+ base = statSync7(root).isDirectory() ? root : join17(root, "..");
5623
6335
  } catch {
5624
6336
  return out;
5625
6337
  }
5626
6338
  const walk2 = (dir) => {
5627
6339
  let entries;
5628
6340
  try {
5629
- entries = readdirSync11(dir);
6341
+ entries = readdirSync6(dir);
5630
6342
  } catch {
5631
6343
  return;
5632
6344
  }
5633
6345
  for (const name of entries) {
5634
- const full = join19(dir, name);
6346
+ const full = join17(dir, name);
5635
6347
  let st;
5636
6348
  try {
5637
- st = statSync12(full);
6349
+ st = statSync7(full);
5638
6350
  } catch {
5639
6351
  continue;
5640
6352
  }
5641
6353
  if (!st.isDirectory()) continue;
5642
- if (SKIP_DIRS9.has(name)) continue;
6354
+ if (SKIP_DIRS.has(name)) {
6355
+ recordSkip(tree, full, base);
6356
+ continue;
6357
+ }
5643
6358
  if (name === "hooks") {
5644
6359
  let files;
5645
6360
  try {
5646
- files = readdirSync11(full);
6361
+ files = readdirSync6(full);
5647
6362
  } catch {
5648
6363
  files = [];
5649
6364
  }
@@ -5651,7 +6366,7 @@ function collectHooksDirSources(root, unreadable) {
5651
6366
  if (!f.endsWith(".ts") || f.endsWith(".d.ts")) continue;
5652
6367
  const stem = f.replace(/\.ts$/, "");
5653
6368
  if (stem === "index") continue;
5654
- const filePath = join19(full, f);
6369
+ const filePath = join17(full, f);
5655
6370
  let body;
5656
6371
  try {
5657
6372
  body = readFileSync10(filePath, "utf8");
@@ -5685,7 +6400,7 @@ function collectFactorySources(tree) {
5685
6400
  for (let i = 0; i < lines.length; i++) {
5686
6401
  const line = lines[i];
5687
6402
  if (line.comment) continue;
5688
- if (!LIFECYCLE_OBJ_RE2.test(line.raw)) continue;
6403
+ if (!testInCode(file, line, LIFECYCLE_OBJ_RE2)) continue;
5689
6404
  let depth = 0;
5690
6405
  let started = false;
5691
6406
  let curKey = null;
@@ -5695,7 +6410,7 @@ function collectFactorySources(tree) {
5695
6410
  if (bl.comment) continue;
5696
6411
  const raw = bl.raw;
5697
6412
  if (started && depth === 1) {
5698
- const k = raw.match(OBJECT_KEY_RE2);
6413
+ const k = matchInCode(file, bl, OBJECT_KEY_RE2);
5699
6414
  if (k) {
5700
6415
  curKey = canonical(k[1]);
5701
6416
  keyBodies.set(curKey, []);
@@ -5704,8 +6419,8 @@ function collectFactorySources(tree) {
5704
6419
  if (curKey && depth >= 1) {
5705
6420
  keyBodies.get(curKey)?.push(raw);
5706
6421
  }
5707
- depth += (raw.match(/\{/g) ?? []).length;
5708
- depth -= (raw.match(/\}/g) ?? []).length;
6422
+ depth += countInCode(file, bl, "{");
6423
+ depth -= countInCode(file, bl, "}");
5709
6424
  if (depth > 0) started = true;
5710
6425
  if (started && depth <= 0) break;
5711
6426
  }
@@ -5756,7 +6471,7 @@ var dExt9 = {
5756
6471
  const unreadable = [];
5757
6472
  const decided = (() => {
5758
6473
  const hookSources = [
5759
- ...collectHooksDirSources(tree.root, unreadable),
6474
+ ...collectHooksDirSources(tree, unreadable),
5760
6475
  ...collectFactorySources(tree)
5761
6476
  ];
5762
6477
  const implByToken = /* @__PURE__ */ new Map();
@@ -5765,7 +6480,7 @@ var dExt9 = {
5765
6480
  list.push(s);
5766
6481
  implByToken.set(s.token, list);
5767
6482
  }
5768
- const manifests = findExtensionManifests4(tree.root);
6483
+ const manifests = findExtensionManifests(tree);
5769
6484
  let declaredAnyLifecycle = false;
5770
6485
  for (const manifestPath2 of manifests) {
5771
6486
  const shortPath = manifestPath2.split("/").slice(-2).join("/");
@@ -6501,6 +7216,436 @@ var dServer14 = {
6501
7216
  }
6502
7217
  };
6503
7218
 
7219
+ // src/detectors/s28-claim-forwarded.ts
7220
+ import { existsSync as existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
7221
+ import { dirname as dirname10, join as join18, relative as relative4 } from "path";
7222
+ var PIN11 = "bacc54a53d5f6f9f20e5254908e2b46807902faa";
7223
+ var CLAIM_TOKEN = /\bverified(?:Org|Person)\b/;
7224
+ var ANY_CLAIM = /\bverified(?:Actor|Org|Person)\b/g;
7225
+ var CALL_RE = /\b([A-Za-z_$][\w$]*)\s*\.\s*(query|mutation|action)\s*\(/g;
7226
+ var GUARD_RE = /\b(?:unresolved\w*Denial|requireVerified\w+|requireClaim\w*)\s*\(|verified-org-unresolved/;
7227
+ var FLEET_WIDE = /fleet-wide:\s*\S[^\n]{7,}/;
7228
+ var SCOPE_MARKERS = /\bwithOrgScope\b|\brequireAuthenticatedCaller\b|\bresolveInboxReader\b|\bresolveVerified\w*\b|\brequireScope\b|\bgetUserIdentity\b|\bisMaster\b|\bscope\b/;
7229
+ var ACTOR_SET_RE = /\bconst\s+(VERIFIED_\w*DOORS)\b[^=]*=\s*new\s+Set\s*(?:<[^>]*>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/;
7230
+ function masked(file) {
7231
+ const out = [];
7232
+ for (const line of file.lines) {
7233
+ let row = "";
7234
+ for (let c = 0; c < line.raw.length; c++) {
7235
+ const ch = line.raw[c];
7236
+ row += ch === " " || ch === " " || ch === "\r" || isCodeAt(file, line.n, c) ? ch : " ";
7237
+ }
7238
+ out.push(row);
7239
+ }
7240
+ return out.join("\n");
7241
+ }
7242
+ function rawText(file) {
7243
+ return file.lines.map((l) => l.raw).join("\n");
7244
+ }
7245
+ function lineAt(text, offset) {
7246
+ let n = 1;
7247
+ for (let i = 0; i < offset && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
7248
+ return n;
7249
+ }
7250
+ var OPEN = { "(": ")", "[": "]", "{": "}" };
7251
+ function matchClose(m, open) {
7252
+ const stack = [];
7253
+ for (let i = open; i < m.length; i++) {
7254
+ const ch = m[i];
7255
+ if (OPEN[ch] !== void 0) stack.push(OPEN[ch]);
7256
+ else if (ch === ")" || ch === "]" || ch === "}") {
7257
+ if (stack.pop() !== ch) return -1;
7258
+ if (stack.length === 0) return i;
7259
+ }
7260
+ }
7261
+ return -1;
7262
+ }
7263
+ function splitTop(m, from, to) {
7264
+ const parts = [];
7265
+ let depth = 0;
7266
+ let start = from;
7267
+ for (let i = from; i < to; i++) {
7268
+ const ch = m[i];
7269
+ if (OPEN[ch] !== void 0) depth++;
7270
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
7271
+ else if (ch === "," && depth === 0) {
7272
+ parts.push([start, i]);
7273
+ start = i + 1;
7274
+ }
7275
+ }
7276
+ if (start < to && m.slice(start, to).trim() !== "") parts.push([start, to]);
7277
+ return parts;
7278
+ }
7279
+ function listTs(dir, out = []) {
7280
+ let names;
7281
+ try {
7282
+ names = readdirSync7(dir);
7283
+ } catch {
7284
+ return out;
7285
+ }
7286
+ for (const n of names) {
7287
+ if (n === "node_modules" || n === "_generated" || n === "__tests__") continue;
7288
+ const p = join18(dir, n);
7289
+ let st;
7290
+ try {
7291
+ st = statSync8(p);
7292
+ } catch {
7293
+ continue;
7294
+ }
7295
+ if (st.isDirectory()) listTs(p, out);
7296
+ else if (n.endsWith(".ts") && !n.endsWith(".test.ts") && !n.endsWith(".d.ts")) out.push(p);
7297
+ }
7298
+ return out;
7299
+ }
7300
+ function discoverBackend(root) {
7301
+ let dir = root;
7302
+ try {
7303
+ if (!statSync8(root).isDirectory()) dir = dirname10(root);
7304
+ } catch {
7305
+ return void 0;
7306
+ }
7307
+ for (let up = 0; up < 5; up++) {
7308
+ const cand = join18(dir, "convex");
7309
+ if (existsSync15(join18(cand, "schema.ts"))) return cand;
7310
+ const parent = dirname10(dir);
7311
+ if (parent === dir) break;
7312
+ dir = parent;
7313
+ }
7314
+ return void 0;
7315
+ }
7316
+ function readDoors(backendRoot) {
7317
+ const doors = /* @__PURE__ */ new Map();
7318
+ for (const path of listTs(backendRoot)) {
7319
+ let text;
7320
+ try {
7321
+ text = readFileSync11(path, "utf8");
7322
+ } catch {
7323
+ continue;
7324
+ }
7325
+ const file = {
7326
+ path,
7327
+ rel: relative4(backendRoot, path),
7328
+ lines: text.split("\n").map((raw, i) => ({ n: i + 1, raw, comment: false }))
7329
+ };
7330
+ const m = masked(file);
7331
+ const module = relative4(backendRoot, path).replace(/\.ts$/, "").split("\\").join("/");
7332
+ const re = /\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:query|mutation|action)\s*\(/g;
7333
+ for (let hit = re.exec(m); hit; hit = re.exec(m)) {
7334
+ const open = hit.index + hit[0].length - 1;
7335
+ const close = matchClose(m, open);
7336
+ if (close < 0) continue;
7337
+ let start = hit.index;
7338
+ const lines = text.slice(0, start).split("\n");
7339
+ let k = lines.length - 2;
7340
+ while (k >= 0 && (lines[k].trim() === "" || /^\s*(\/\/|\/\*|\*)/.test(lines[k])) && lines.length - 2 - k < 60)
7341
+ k--;
7342
+ start = lines.slice(0, k + 1).join("\n").length;
7343
+ const region = text.slice(start, close + 1);
7344
+ const mRegion = m.slice(hit.index, close + 1);
7345
+ const claims = [];
7346
+ const am = /\bargs\s*:\s*/.exec(mRegion);
7347
+ if (am) {
7348
+ const at = hit.index + am.index + am[0].length;
7349
+ let argsText = "";
7350
+ if (m[at] === "{") argsText = text.slice(at, matchClose(m, at) + 1);
7351
+ else {
7352
+ const id = /^[A-Za-z_$][\w$]*/.exec(m.slice(at));
7353
+ if (id) {
7354
+ const dm = new RegExp(`\\bconst\\s+${id[0]}\\b[^=]*=\\s*`).exec(m);
7355
+ if (dm) {
7356
+ let p = dm.index + dm[0].length;
7357
+ if (m.slice(p).startsWith("v.object(")) p += "v.object(".length;
7358
+ if (m[p] === "{") argsText = text.slice(p, matchClose(m, p) + 1);
7359
+ }
7360
+ }
7361
+ }
7362
+ for (const c of argsText.match(ANY_CLAIM) ?? []) if (!claims.includes(c)) claims.push(c);
7363
+ }
7364
+ let exit;
7365
+ if (/\brequireServiceAccount\b/.test(mRegion))
7366
+ exit = "service-account-only (requireServiceAccount)";
7367
+ else if (/\brequireResolvedCaller\s*\([^)]*masterOnly[^)]*mcpBoundOnly|\brequireResolvedCaller\s*\([^)]*mcpBoundOnly[^)]*masterOnly/s.test(
7368
+ mRegion
7369
+ ))
7370
+ exit = "service-account-only (requireResolvedCaller masterOnly+mcpBoundOnly)";
7371
+ else {
7372
+ const fw = FLEET_WIDE.exec(region);
7373
+ if (fw) exit = `fleet-wide declaration (${fw[0].slice(11).trim().slice(0, 80)})`;
7374
+ }
7375
+ doors.set(`${module}:${hit[1]}`, {
7376
+ claims,
7377
+ exit,
7378
+ resolvesCaller: SCOPE_MARKERS.test(mRegion)
7379
+ });
7380
+ }
7381
+ }
7382
+ return doors;
7383
+ }
7384
+ function definitionOf(name, files) {
7385
+ const esc = name.replace(/\$/g, "\\$");
7386
+ const re = new RegExp(`(?:\\b(?:const|let|var)\\s+${esc}\\b|\\bfunction\\s+${esc}\\b)`);
7387
+ for (const f of files) {
7388
+ const hit = re.exec(f.m);
7389
+ if (!hit) continue;
7390
+ let depth = 0;
7391
+ for (let i = hit.index + hit[0].length; i < f.m.length; i++) {
7392
+ const ch = f.m[i];
7393
+ if (OPEN[ch] !== void 0) {
7394
+ depth++;
7395
+ } else if (ch === ")" || ch === "]" || ch === "}") {
7396
+ depth--;
7397
+ if (depth === 0 && ch === "}" && /\bfunction\b/.test(hit[0]))
7398
+ return { mask: f.m.slice(hit.index, i + 1) };
7399
+ } else if (ch === ";" && depth === 0 && !/\bfunction\b/.test(hit[0])) {
7400
+ return { mask: f.m.slice(hit.index, i + 1) };
7401
+ }
7402
+ if (depth < 0) break;
7403
+ }
7404
+ return { mask: f.m.slice(hit.index, Math.min(f.m.length, hit.index + 1500)) };
7405
+ }
7406
+ return void 0;
7407
+ }
7408
+ function readHelper(name, files, seen = /* @__PURE__ */ new Set(), depth = 0) {
7409
+ if (seen.has(name) || depth > 3) return { forwards: false, optional: false };
7410
+ seen.add(name);
7411
+ const def = definitionOf(name, files);
7412
+ if (!def) return { forwards: false, optional: false };
7413
+ let forwards = CLAIM_TOKEN.test(def.mask);
7414
+ const optional = /:\s*\{\s*\}|return\s*\{\s*\}|\?\?\s*\{\s*\}/.test(def.mask);
7415
+ if (!forwards) {
7416
+ for (const c of def.mask.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
7417
+ const sub = readHelper(c[1], files, seen, depth + 1);
7418
+ if (sub.forwards) {
7419
+ forwards = true;
7420
+ break;
7421
+ }
7422
+ }
7423
+ }
7424
+ return { forwards, optional };
7425
+ }
7426
+ function toolNameBefore(text, offset) {
7427
+ const head = text.slice(0, offset);
7428
+ const open = Math.max(head.lastIndexOf("defineTool("), head.lastIndexOf("registerTool("));
7429
+ if (open >= 0) {
7430
+ const m = /^\s*"([a-z][a-z0-9_]*)",?\s*$/m.exec(head.slice(open));
7431
+ if (m) return { name: m[1], start: open };
7432
+ const r = /registerTool\(\s*"([a-z][a-z0-9_]*)"/.exec(head.slice(open));
7433
+ if (r) return { name: r[1], start: open };
7434
+ return { name: void 0, start: open };
7435
+ }
7436
+ const lines = head.split("\n");
7437
+ const back = Math.max(0, lines.length - 120);
7438
+ return { name: void 0, start: lines.slice(0, back).join("\n").length };
7439
+ }
7440
+ function scanS28(tree) {
7441
+ const scan = { violate: [], conform: [], cnj: [], calls: 0, backend: void 0 };
7442
+ const backendRoot = tree.backendRoot ?? discoverBackend(tree.root);
7443
+ scan.backend = backendRoot;
7444
+ const doors = backendRoot ? readDoors(backendRoot) : void 0;
7445
+ const files = tree.files.map((f) => ({ f, text: rawText(f), m: masked(f) }));
7446
+ const actorDoors = /* @__PURE__ */ new Set();
7447
+ for (const { text } of files) {
7448
+ const hit = ACTOR_SET_RE.exec(text);
7449
+ if (hit) for (const s of hit[2].matchAll(/["'`]([\w/]+:[\w]+)["'`]/g)) actorDoors.add(s[1]);
7450
+ }
7451
+ for (const { f, text, m } of files) {
7452
+ for (let hit = CALL_RE.exec(m); hit; hit = CALL_RE.exec(m)) {
7453
+ const recv = hit[1];
7454
+ if (!/convex|client/i.test(recv)) continue;
7455
+ const open = hit.index + hit[0].length - 1;
7456
+ const close = matchClose(m, open);
7457
+ if (close < 0) continue;
7458
+ scan.calls++;
7459
+ const line = lineAt(text, hit.index);
7460
+ const tool = toolNameBefore(text, hit.index);
7461
+ const parts = splitTop(m, open + 1, close);
7462
+ let door;
7463
+ if (parts[0]) {
7464
+ const rawFirst = text.slice(parts[0][0], parts[0][1]);
7465
+ const lit = /^\s*(?:\/\/[^\n]*\n\s*)*["'`]([\w/]+:[\w]+)["'`]/.exec(rawFirst);
7466
+ const ref = /^\s*(?:api|internal)\.([\w]+)\.([\w]+)/.exec(rawFirst);
7467
+ if (lit) door = lit[1];
7468
+ else if (ref) door = `${ref[1]}:${ref[2]}`;
7469
+ }
7470
+ const site = (note) => ({
7471
+ file: f.rel,
7472
+ line,
7473
+ door: door ?? "?",
7474
+ tool: tool.name,
7475
+ note
7476
+ });
7477
+ if (door === void 0) {
7478
+ scan.cnj.push(
7479
+ site(
7480
+ "the door is named by an expression the scan cannot resolve (not statically resolvable)"
7481
+ )
7482
+ );
7483
+ continue;
7484
+ }
7485
+ const argText = parts[1] ? m.slice(parts[1][0], parts[1][1]) : "";
7486
+ const region = m.slice(tool.start, hit.index);
7487
+ const regionRaw = text.slice(tool.start, hit.index);
7488
+ const guard = GUARD_RE.test(region) || GUARD_RE.test(regionRaw);
7489
+ let direct = CLAIM_TOKEN.test(argText);
7490
+ let viaHelper;
7491
+ let virtual = argText;
7492
+ const bare = /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(argText);
7493
+ if (bare && !direct) {
7494
+ const v = bare[1].replace(/\$/g, "\\$");
7495
+ const pieces = [];
7496
+ for (const a of region.matchAll(new RegExp(`Object\\.assign\\s*\\(\\s*${v}\\s*,`, "g"))) {
7497
+ const o = a.index + a[0].length - 1;
7498
+ const c = matchClose(region, region.lastIndexOf("(", o));
7499
+ pieces.push(region.slice(o, c < 0 ? o + 400 : c));
7500
+ }
7501
+ for (const a of region.matchAll(
7502
+ new RegExp(`\\b${v}\\.verified(?:Org|Person|Actor)\\b`, "g")
7503
+ ))
7504
+ pieces.push(a[0]);
7505
+ const decl = new RegExp(`\\b(?:const|let)\\s+${v}\\b[^=]*=\\s*\\{`).exec(region);
7506
+ if (decl) {
7507
+ const o = decl.index + decl[0].length - 1;
7508
+ const c = matchClose(region, o);
7509
+ pieces.push(region.slice(o, c < 0 ? o + 600 : c + 1));
7510
+ }
7511
+ virtual = pieces.join("\n");
7512
+ direct = CLAIM_TOKEN.test(virtual);
7513
+ }
7514
+ if (!direct) {
7515
+ for (const sp of virtual.matchAll(/\.\.\.\s*([A-Za-z_$][\w$]*)/g)) {
7516
+ const h = readHelper(sp[1], files);
7517
+ if (h.forwards) {
7518
+ viaHelper = h;
7519
+ break;
7520
+ }
7521
+ }
7522
+ if (!viaHelper) {
7523
+ for (const sp of virtual.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(\s*\)/g)) {
7524
+ const h = readHelper(sp[1], files);
7525
+ if (h.forwards) {
7526
+ viaHelper = h;
7527
+ break;
7528
+ }
7529
+ }
7530
+ }
7531
+ }
7532
+ if (direct) {
7533
+ scan.conform.push(site("forwards the verified claim in the call"));
7534
+ continue;
7535
+ }
7536
+ if (viaHelper) {
7537
+ if (viaHelper.optional && !guard) {
7538
+ scan.violate.push(
7539
+ site(
7540
+ "forwards its claim through an optional helper (it returns {} when the bearer holds none) and no refusal precedes the call in the handler: a bearer with no resolvable organisation is sent claimless"
7541
+ )
7542
+ );
7543
+ } else {
7544
+ scan.conform.push(
7545
+ site(
7546
+ viaHelper.optional ? "forwards the verified claim through a helper and refuses before the call when it holds none" : "forwards the verified claim through a helper"
7547
+ )
7548
+ );
7549
+ }
7550
+ continue;
7551
+ }
7552
+ if (!doors) {
7553
+ scan.cnj.push(
7554
+ site(
7555
+ "forwards no verified claim and no backend source was found above the scanned tree (no convex/ with a schema.ts, and no backend root given): whether the door is an exit cannot be read"
7556
+ )
7557
+ );
7558
+ continue;
7559
+ }
7560
+ const info = doors.get(door);
7561
+ if (!info) {
7562
+ scan.cnj.push(
7563
+ site(
7564
+ `forwards no verified claim and the door ${door} is not defined in the supplied backend (${backendRoot})`
7565
+ )
7566
+ );
7567
+ continue;
7568
+ }
7569
+ if (info.exit) {
7570
+ scan.conform.push(site(`needs no claim: the door is ${info.exit}`));
7571
+ continue;
7572
+ }
7573
+ if (!info.resolvesCaller) {
7574
+ scan.cnj.push(
7575
+ site(
7576
+ `forwards no verified claim and the door ${door} shows no caller resolution in its own source (its scope comes from a helper or a run call that this scan does not follow)`
7577
+ )
7578
+ );
7579
+ continue;
7580
+ }
7581
+ const accepts = info.claims.length > 0 ? `the door accepts ${info.claims.join(", ")} and the tool omits it` : "the door accepts no claim argument, so R-54 must add one before the tool can forward it";
7582
+ const actorNote = actorDoors.has(door) ? "; verifiedActor is added by the wrapped client only for a bearer that resolved an agent, and a bearer that resolved no agent (a seat or person token, an org-level token) is sent claimless" : "";
7583
+ scan.violate.push(site(`forwards no verified claim \u2014 ${accepts}${actorNote}`));
7584
+ }
7585
+ }
7586
+ return scan;
7587
+ }
7588
+ function fmt(s) {
7589
+ return `${s.file}:${s.line} ${s.door} ${s.tool ? `[${s.tool}] ` : ""}(${s.note})`;
7590
+ }
7591
+ var S28_ID = "S-28 claim-forwarded";
7592
+ var s28ClaimForwarded = {
7593
+ id: S28_ID,
7594
+ rule: "S-28",
7595
+ pin: PIN11,
7596
+ run(tree) {
7597
+ const s = scanS28(tree);
7598
+ const breakdown = `[conform=${s.conform.length} non-conforming=${s.violate.length} could-not-judge=${s.cnj.length}]`;
7599
+ const list = (xs) => xs.slice(0, 5).map(fmt).join("; ");
7600
+ if (s.violate.length > 0) {
7601
+ const first = s.violate[0];
7602
+ return {
7603
+ id: S28_ID,
7604
+ anchor: `${first.file}:${first.line}`,
7605
+ state: "non-conforming",
7606
+ verdict: `S-28: ${s.violate.length} backend call(s) VIOLATE (a tool forwards the verified claim of its caller to every backend door it calls, and refuses when it has none). ${breakdown} Violations: ${list(s.violate)}${s.violate.length > 5 ? `; +${s.violate.length - 5} more` : ""}.${s.cnj.length ? ` could-not-judge calls (a verdict is owed): ${list(s.cnj)}.` : ""}`
7607
+ };
7608
+ }
7609
+ if (s.cnj.length > 0) {
7610
+ const reason2 = `S-28: ${s.cnj.length} backend call(s) could not be judged \u2014 ${list(s.cnj)}. This is NOT a clean bill. ${breakdown}`;
7611
+ return {
7612
+ id: S28_ID,
7613
+ anchor: `${s.cnj[0].file}:${s.cnj[0].line}`,
7614
+ state: "could-not-judge",
7615
+ verdict: reason2,
7616
+ reason: reason2
7617
+ };
7618
+ }
7619
+ if (s.conform.length > 0) {
7620
+ return {
7621
+ id: S28_ID,
7622
+ anchor: `${s.conform[0].file}:${s.conform[0].line}`,
7623
+ state: "present",
7624
+ verdict: `S-28: ${s.conform.length} backend call(s) forward the verified claim or call a door that needs none; 0 violations. ${breakdown} Conform: ${list(s.conform)}.`
7625
+ };
7626
+ }
7627
+ const usesBackend = tree.files.some(
7628
+ (f) => f.lines.some((l) => !l.comment && /\bConvexHttpClient\b|from\s+["']convex\//.test(l.raw))
7629
+ );
7630
+ if (!usesBackend) {
7631
+ return {
7632
+ id: S28_ID,
7633
+ anchor: "source:no-backend-client",
7634
+ state: "absent",
7635
+ verdict: `S-28: no Convex client is imported anywhere in the ${tree.files.length} file(s) read; no backend door is called, so there is no claim to forward (rule out of subject).`
7636
+ };
7637
+ }
7638
+ const reason = `S-28: no applicable site \u2014 a Convex client is imported but none of the ${s.calls} <client>.query|mutation|action call(s) read names a door statically. NOT a clean bill.`;
7639
+ return {
7640
+ id: S28_ID,
7641
+ anchor: "source:no-applicable-site",
7642
+ state: "could-not-judge",
7643
+ verdict: reason,
7644
+ reason
7645
+ };
7646
+ }
7647
+ };
7648
+
6504
7649
  // src/detectors/index.ts
6505
7650
  var detectors = [
6506
7651
  d2,
@@ -6534,6 +7679,7 @@ var detectors = [
6534
7679
  c22TestRatioGate,
6535
7680
  c25BuildIdentity,
6536
7681
  c26SubpackageLockfile,
7682
+ s28ClaimForwarded,
6537
7683
  dServer2,
6538
7684
  dServer6,
6539
7685
  dDebugOptIn,
@@ -6547,23 +7693,24 @@ var detectors = [
6547
7693
  dExt5,
6548
7694
  dExt7,
6549
7695
  dExt8,
6550
- dExt9
7696
+ dExt9,
7697
+ localIdentity
6551
7698
  ];
6552
7699
 
6553
7700
  // src/runtime/launch.ts
6554
7701
  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";
7702
+ import { existsSync as existsSync16, readFileSync as readFileSync12, statSync as statSync9 } from "fs";
7703
+ import { dirname as dirname11, isAbsolute, join as join19, resolve } from "path";
6557
7704
  function findPackageDir(target) {
6558
7705
  let dir;
6559
7706
  try {
6560
- dir = statSync13(target).isDirectory() ? target : dirname10(target);
7707
+ dir = statSync9(target).isDirectory() ? target : dirname11(target);
6561
7708
  } catch {
6562
7709
  return null;
6563
7710
  }
6564
7711
  for (let depth = 0; depth < 8; depth++) {
6565
- if (existsSync15(join20(dir, "package.json"))) return dir;
6566
- const parent = dirname10(dir);
7712
+ if (existsSync16(join19(dir, "package.json"))) return dir;
7713
+ const parent = dirname11(dir);
6567
7714
  if (parent === dir) break;
6568
7715
  dir = parent;
6569
7716
  }
@@ -6587,10 +7734,10 @@ function deriveLaunchPlan(target) {
6587
7734
  }
6588
7735
  let pkg;
6589
7736
  try {
6590
- pkg = JSON.parse(readFileSync11(join20(pkgDir, "package.json"), "utf8"));
7737
+ pkg = JSON.parse(readFileSync12(join19(pkgDir, "package.json"), "utf8"));
6591
7738
  } catch (err) {
6592
7739
  return {
6593
- error: `could not parse ${join20(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
7740
+ error: `could not parse ${join19(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
6594
7741
  };
6595
7742
  }
6596
7743
  const bin = firstBinEntry(pkg.bin);
@@ -6668,10 +7815,10 @@ function readEvalCounts(target) {
6668
7815
  if (!pkgDir) return counts;
6669
7816
  let dir = pkgDir;
6670
7817
  for (let depth = 0; depth < 8; depth++) {
6671
- const candidate = join20(dir, "evals", "evals.json");
6672
- if (existsSync15(candidate)) {
7818
+ const candidate = join19(dir, "evals", "evals.json");
7819
+ if (existsSync16(candidate)) {
6673
7820
  try {
6674
- const parsed = JSON.parse(readFileSync11(candidate, "utf8"));
7821
+ const parsed = JSON.parse(readFileSync12(candidate, "utf8"));
6675
7822
  for (const c of parsed.cases ?? []) {
6676
7823
  if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
6677
7824
  }
@@ -6679,7 +7826,7 @@ function readEvalCounts(target) {
6679
7826
  }
6680
7827
  return counts;
6681
7828
  }
6682
- const parent = dirname10(dir);
7829
+ const parent = dirname11(dir);
6683
7830
  if (parent === dir) break;
6684
7831
  dir = parent;
6685
7832
  }
@@ -6687,7 +7834,7 @@ function readEvalCounts(target) {
6687
7834
  }
6688
7835
  function statSyncSafe(p) {
6689
7836
  try {
6690
- return statSync13(p).isDirectory();
7837
+ return statSync9(p).isDirectory();
6691
7838
  } catch {
6692
7839
  return false;
6693
7840
  }
@@ -6806,8 +7953,11 @@ function reconcile(id, staticR, dynamicR) {
6806
7953
  };
6807
7954
  }
6808
7955
  function runDoctor(target, detectorList = detectors, opts = {}) {
7956
+ return runDoctorReport(target, detectorList, opts).results;
7957
+ }
7958
+ function runDoctorReport(target, detectorList = detectors, opts = {}) {
6809
7959
  const mode = opts.mode ?? "static";
6810
- const tree = loadTree(target);
7960
+ const tree = loadTree(target, opts.backend);
6811
7961
  const needsRuntime = mode !== "static" && detectorList.some((d) => typeof d.runDynamic === "function");
6812
7962
  const runtime = needsRuntime ? launchAndIntrospect(target) : null;
6813
7963
  const results = [];
@@ -6840,7 +7990,7 @@ function runDoctor(target, detectorList = detectors, opts = {}) {
6840
7990
  results.push(reconcile(d.id, staticR, dynamicR));
6841
7991
  }
6842
7992
  }
6843
- return results;
7993
+ return { results, skipped: tree.skippedDirs };
6844
7994
  }
6845
7995
  async function invokeLiveChecked(d, session) {
6846
7996
  if (!session) {
@@ -6895,14 +8045,32 @@ async function runDoctorAsync(target, opts = {}) {
6895
8045
  }
6896
8046
  return results;
6897
8047
  }
6898
- function renderTable(surface, results) {
8048
+ function renderSkipped(skipped) {
8049
+ if (skipped.length === 0) {
8050
+ return "skipped directories (0) \u2014 none: no directory was skipped on this run.";
8051
+ }
8052
+ const sorted = [...skipped].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
8053
+ const head = `skipped directories (${sorted.length}) \u2014 NOT read on this run; a verdict above says nothing about what lies inside:`;
8054
+ const rows = sorted.map(
8055
+ (d) => `${d.path} | ${d.reason}${d.candidate ? ` | holds a deployment configuration: ${d.candidate}` : ""}`
8056
+ );
8057
+ return [head, "-".repeat(head.length), ...rows].join("\n");
8058
+ }
8059
+ function renderTable(surface, results, skipped) {
8060
+ const body = renderResults(surface, results);
8061
+ 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);
8062
+ return `${body}
8063
+
8064
+ ${section}`;
8065
+ }
8066
+ function renderResults(surface, results) {
6899
8067
  const cols = { item: 24, anchor: 26, state: 15 };
6900
8068
  const header = `${pad("item", cols.item)} | ${pad("file:line", cols.anchor)} | ${pad("state", cols.state)} | verdict`;
6901
- const sep = "-".repeat(header.length);
8069
+ const sep3 = "-".repeat(header.length);
6902
8070
  const rows = results.map(
6903
8071
  (r) => `${pad(r.id, cols.item)} | ${pad(r.anchor, cols.anchor)} | ${pad(r.state, cols.state)} | ${r.verdict}`
6904
8072
  );
6905
- const table = [`surface: ${surface}`, header, sep, ...rows].join("\n");
8073
+ const table = [`surface: ${surface}`, header, sep3, ...rows].join("\n");
6906
8074
  const unjudged = results.filter((r) => r.state === "could-not-judge");
6907
8075
  if (unjudged.length === 0) return table;
6908
8076
  const cnjHeader = `could-not-judge (${unjudged.length}) \u2014 the doctor could NOT read these; this is NOT a clean bill:`;
@@ -6918,11 +8086,16 @@ function computeExitCode(results) {
6918
8086
  return 0;
6919
8087
  }
6920
8088
  var USAGE = [
6921
- "usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both]",
8089
+ "usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both] [--backend=<convex-dir>]",
6922
8090
  "",
6923
8091
  " --mode=static read the tree only (default)",
6924
8092
  " --mode=dynamic launch the server and judge what it serves",
6925
8093
  " --mode=both both, dynamic findings taking precedence",
8094
+ " --local-identity-ceiling=N",
8095
+ " local identity primitives tolerated before DLocalIdentity",
8096
+ " fails (default 0)",
8097
+ " --backend=DIR the backend source (a convex/ dir) whose door list S-28 reads;",
8098
+ " default: the nearest convex/ above the tree",
6926
8099
  " --version print the version and exit",
6927
8100
  " --help print this and exit",
6928
8101
  "",
@@ -6930,8 +8103,8 @@ var USAGE = [
6930
8103
  ].join("\n");
6931
8104
  function readVersion() {
6932
8105
  try {
6933
- const here = dirname11(fileURLToPath(import.meta.url));
6934
- const manifest = JSON.parse(readFileSync12(join21(here, "..", "package.json"), "utf8"));
8106
+ const here = dirname12(fileURLToPath(import.meta.url));
8107
+ const manifest = JSON.parse(readFileSync13(join20(here, "..", "package.json"), "utf8"));
6935
8108
  return manifest.version ?? null;
6936
8109
  } catch {
6937
8110
  return null;
@@ -6953,6 +8126,7 @@ function main() {
6953
8126
  return;
6954
8127
  }
6955
8128
  const modeArg = args.find((a) => a.startsWith("--mode="));
8129
+ const backendArg = args.find((a) => a.startsWith("--backend="));
6956
8130
  const target = args.find((a) => !a.startsWith("--"));
6957
8131
  if (!target) {
6958
8132
  console.error(USAGE);
@@ -6963,8 +8137,24 @@ function main() {
6963
8137
  console.error(`unknown --mode "${mode}" \u2014 expected static|dynamic|both`);
6964
8138
  process.exit(1);
6965
8139
  }
6966
- const results = runDoctor(target, void 0, { mode });
6967
- console.log(renderTable(target, results));
8140
+ const ceilingArg = args.find((a) => a.startsWith("--local-identity-ceiling="));
8141
+ let detectorList = detectors;
8142
+ if (ceilingArg) {
8143
+ const raw = ceilingArg.slice("--local-identity-ceiling=".length);
8144
+ const ceiling = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
8145
+ if (!Number.isInteger(ceiling)) {
8146
+ console.error(`invalid --local-identity-ceiling "${raw}" \u2014 expected a non-negative integer`);
8147
+ process.exit(1);
8148
+ }
8149
+ const ratchet = makeLocalIdentity({ ceiling });
8150
+ detectorList = detectors.map((d) => d === localIdentity ? ratchet : d);
8151
+ }
8152
+ const backend = backendArg ? backendArg.slice("--backend=".length) : void 0;
8153
+ const { results, skipped } = runDoctorReport(target, detectorList, {
8154
+ mode,
8155
+ ...backend !== void 0 ? { backend } : {}
8156
+ });
8157
+ console.log(renderTable(target, results, skipped));
6968
8158
  process.exitCode = computeExitCode(results);
6969
8159
  }
6970
8160
  function isDirectInvocation() {
@@ -6986,7 +8176,9 @@ if (isDirectInvocation()) main();
6986
8176
  export {
6987
8177
  computeExitCode,
6988
8178
  isLiveDetector,
8179
+ renderSkipped,
6989
8180
  renderTable,
6990
8181
  runDoctor,
6991
- runDoctorAsync
8182
+ runDoctorAsync,
8183
+ runDoctorReport
6992
8184
  };