@vantageos/mcp-doctor 0.2.0 → 0.3.1

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,31 +1,133 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/detectors/d2.ts
4
- var SESSION_RE = /mcp-session-id/i;
5
- var d2 = {
6
- id: "D2 stateless",
3
+ // src/doctor.ts
4
+ import { readFileSync as readFileSync19, realpathSync } from "fs";
5
+ import { dirname as dirname11, join as join21, resolve as resolve2 } from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // src/detectors/c7-artifact-verifiable.ts
9
+ import { existsSync, readFileSync } from "fs";
10
+ import { join } from "path";
11
+ var PIN = "a74a042b53c71a095ab88b3073bd47728eeda814";
12
+ var DOC_FILES = ["README.md", "README.fr.md"];
13
+ var INSTALL_RE = /\b(?:npm\s+(?:install|i)|npx|pnpm\s+add|yarn\s+add)\s+(@?[\w./-]+)/i;
14
+ var PIN_VERSION_RE = /@\d+\.\d+\.\d+/;
15
+ var PROVENANCE_RE = /\b(?:provenance|sha-?256|sha-?512|checksum|gpg\s+signature|sigstore|cosign|--provenance)\b/i;
16
+ var SECOND_CHANNEL_RE = /\bgithub\s+release|releases\/tag|releases\/download\b/i;
17
+ function c7PositiveControls() {
18
+ const installSample = "npm install @vantageos/mcp-doctor";
19
+ const pinSample = "npm install @vantageos/mcp-doctor@1.2.3";
20
+ const bareSample = "npm install @vantageos/mcp-doctor";
21
+ const provenanceSample = "published with npm publish --provenance";
22
+ const channelSample = "also available as a GitHub release";
23
+ return {
24
+ installMatcher: {
25
+ pattern: INSTALL_RE.source,
26
+ sample: installSample,
27
+ matched: INSTALL_RE.test(installSample)
28
+ },
29
+ pinMatcher: {
30
+ pattern: PIN_VERSION_RE.source,
31
+ sample: pinSample,
32
+ matched: PIN_VERSION_RE.test(pinSample),
33
+ rejectsBare: !PIN_VERSION_RE.test(bareSample)
34
+ },
35
+ provenanceMatcher: {
36
+ pattern: PROVENANCE_RE.source,
37
+ sample: provenanceSample,
38
+ matched: PROVENANCE_RE.test(provenanceSample)
39
+ },
40
+ channelMatcher: {
41
+ pattern: SECOND_CHANNEL_RE.source,
42
+ sample: channelSample,
43
+ matched: SECOND_CHANNEL_RE.test(channelSample)
44
+ }
45
+ };
46
+ }
47
+ function readDocText(root) {
48
+ for (const name of DOC_FILES) {
49
+ const path = join(root, name);
50
+ if (!existsSync(path)) continue;
51
+ try {
52
+ return { text: readFileSync(path, "utf8"), file: name };
53
+ } catch {
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ var c7ArtifactVerifiable = {
59
+ id: "C7 artifact-verifiable",
60
+ rule: "C-7",
61
+ pin: PIN,
7
62
  run(tree) {
8
- const hits = [];
9
- for (const file of tree.files) {
10
- for (const line of file.lines) {
11
- if (line.comment) continue;
12
- if (SESSION_RE.test(line.raw)) hits.push({ file: file.rel, line: line.n });
13
- }
63
+ const controls = c7PositiveControls();
64
+ if (!controls.installMatcher.matched) {
65
+ throw new Error(
66
+ `C7 install-matcher control FAILED on \`${controls.installMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
67
+ );
14
68
  }
15
- if (hits.length > 0) {
16
- const anchor = `${hits[0].file}:${hits.map((h) => h.line).join(",")}`;
69
+ if (!controls.pinMatcher.matched || !controls.pinMatcher.rejectsBare) {
70
+ throw new Error(
71
+ `C7 pin-matcher control FAILED: accept(\`${controls.pinMatcher.sample}\`)=${controls.pinMatcher.matched} rejectsBare=${controls.pinMatcher.rejectsBare} \u2014 matcher broken, verdict untrustworthy`
72
+ );
73
+ }
74
+ if (!controls.provenanceMatcher.matched) {
75
+ throw new Error(
76
+ `C7 provenance-matcher control FAILED on \`${controls.provenanceMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
77
+ );
78
+ }
79
+ if (!controls.channelMatcher.matched) {
80
+ throw new Error(
81
+ `C7 channel-matcher control FAILED on \`${controls.channelMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
82
+ );
83
+ }
84
+ const doc = readDocText(tree.root);
85
+ if (!doc) {
17
86
  return {
18
87
  id: this.id,
19
- anchor,
88
+ anchor: tree.root,
20
89
  state: "non-conforming",
21
- verdict: "session-keyed (mcp-session-id header) -> migrate to stateless"
90
+ verdict: "no README.md/README.fr.md found -> a trunk rule governs every published object; no install-command surface to read (C-7)"
91
+ };
92
+ }
93
+ const installMatch = doc.text.match(INSTALL_RE);
94
+ if (!installMatch) {
95
+ return {
96
+ id: this.id,
97
+ anchor: `${doc.file}:1`,
98
+ state: "non-conforming",
99
+ verdict: `no one-command install naming a registry found in ${doc.file} -> add \`npm install <pkg>\`/\`npx <pkg>\` naming the registry (C-7a)`
100
+ };
101
+ }
102
+ if (!PIN_VERSION_RE.test(doc.text)) {
103
+ return {
104
+ id: this.id,
105
+ anchor: `${doc.file}:1`,
106
+ state: "non-conforming",
107
+ verdict: `install command present but no version-pin shape (\`@<version>\`) found in ${doc.file} -> document that an exact version can be pinned and rolled back (C-7b)`
108
+ };
109
+ }
110
+ if (!SECOND_CHANNEL_RE.test(doc.text)) {
111
+ return {
112
+ id: this.id,
113
+ anchor: `${doc.file}:1`,
114
+ state: "non-conforming",
115
+ verdict: `only one obtain channel found in ${doc.file} -> cite a second independent channel (e.g. GitHub release) alongside the registry (C-7d)`
116
+ };
117
+ }
118
+ if (!PROVENANCE_RE.test(doc.text)) {
119
+ return {
120
+ id: this.id,
121
+ anchor: `${doc.file}:1`,
122
+ state: "non-conforming",
123
+ verdict: `no provenance/checksum signal cited in ${doc.file} -> cite npm provenance or a published checksum (C-7c)`
22
124
  };
23
125
  }
24
126
  return {
25
127
  id: this.id,
26
- anchor: "-",
128
+ anchor: `${doc.file}:1`,
27
129
  state: "present",
28
- verdict: "stateless (no mcp-session-id session-keying)"
130
+ verdict: `${doc.file} declares a one-command pinned install, >=2 obtain channels, and a provenance/checksum signal (C-7a/b/c/d complete). Whether the cited signal actually RESOLVES against what the registry serves is the separate gate check at publish time \u2014 this static run asserts the declaration is complete, not that the gate has resolved it.`
29
131
  };
30
132
  }
31
133
  };
@@ -224,6 +326,208 @@ function sdkFirstAnchor(tree) {
224
326
  }
225
327
  return null;
226
328
  }
329
+ var VERB_FIELD_RE = /\bverb\s*:\s*["'`]([A-Za-z][A-Za-z-]*)["'`]/;
330
+ var TABLE_FIELD_RE = /\btable\s*:\s*["'`]([A-Za-z0-9_.-]+)["'`]/;
331
+ var MECHANISM_FIELD_RE = /\b(?:searchMechanism|mechanism)\s*:\s*["'`]([A-Za-z0-9_-]+(?:\([^)]*\))?)["'`]/;
332
+ var AUTH_READ_FIELD_RE = /\bauthRead\s*:\s*["'`]([A-Za-z-]+)["'`]/;
333
+ var AUTH_WRITE_FIELD_RE = /\bauthWrite\s*:\s*["'`]([A-Za-z-]+)["'`]/;
334
+ var AUTH_FOLDED_FIELD_RE = /\bauth\s*:\s*["'`]([A-Za-z-]+)["'`]/;
335
+ var READ_ONLY_HINT_RE = /\breadOnlyHint\s*:\s*(?:true|false)\b/;
336
+ var DESTRUCTIVE_HINT_RE = /\bdestructiveHint\s*:\s*(?:true|false)\b/;
337
+ var IDEMPOTENT_HINT_RE = /\bidempotentHint\s*:\s*(?:true|false)\b/;
338
+ var OPEN_WORLD_HINT_RE = /\bopenWorldHint\s*:\s*(?:true|false)\b/;
339
+ var TOKENS_FIELD_RE = /\btokensEstimeContexte\s*:\s*(\d+)\b/;
340
+ var PAGINATION_CURSOR_RE = /\bpaginationCursor\s*:\s*(?:true|false)\b/;
341
+ var PAGINATION_DEFAULT_RE = /\bpaginationDefault\s*:\s*(\d+)\b/;
342
+ var PAGINATION_MAX_RE = /\bpaginationMax\s*:\s*(\d+)\b/;
343
+ function toolAnnotationFieldControls() {
344
+ const verbSample = 'annotations: { verb: "create", table: "customers" },';
345
+ const tableSample = 'annotations: { verb: "create", table: "customers" },';
346
+ const mechSample = 'annotations: { searchMechanism: "indexed-bm25" },';
347
+ const authPairSample = 'annotations: { authRead: "public", authWrite: "org-admin" },';
348
+ const authFoldedSample = 'annotations: { auth: "org-admin" },';
349
+ const hintsSample = "annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },";
350
+ const tokensSample = "annotations: { tokensEstimeContexte: 120 },";
351
+ const paginationSample = "annotations: { paginationCursor: true, paginationDefault: 20, paginationMax: 100 },";
352
+ const paginationUnboundedSample = "annotations: { paginationMax: Infinity },";
353
+ return {
354
+ verb: {
355
+ pattern: VERB_FIELD_RE.source,
356
+ sample: verbSample,
357
+ matched: VERB_FIELD_RE.test(verbSample)
358
+ },
359
+ table: {
360
+ pattern: TABLE_FIELD_RE.source,
361
+ sample: tableSample,
362
+ matched: TABLE_FIELD_RE.test(tableSample)
363
+ },
364
+ mechanism: {
365
+ pattern: MECHANISM_FIELD_RE.source,
366
+ sample: mechSample,
367
+ matched: MECHANISM_FIELD_RE.test(mechSample)
368
+ },
369
+ authRead: {
370
+ pattern: AUTH_READ_FIELD_RE.source,
371
+ sample: authPairSample,
372
+ matched: AUTH_READ_FIELD_RE.test(authPairSample)
373
+ },
374
+ authWrite: {
375
+ pattern: AUTH_WRITE_FIELD_RE.source,
376
+ sample: authPairSample,
377
+ matched: AUTH_WRITE_FIELD_RE.test(authPairSample)
378
+ },
379
+ authFolded: {
380
+ pattern: AUTH_FOLDED_FIELD_RE.source,
381
+ sample: authFoldedSample,
382
+ matched: AUTH_FOLDED_FIELD_RE.test(authFoldedSample) && !AUTH_READ_FIELD_RE.test(authFoldedSample)
383
+ },
384
+ behavioralHints: {
385
+ pattern: [
386
+ READ_ONLY_HINT_RE.source,
387
+ DESTRUCTIVE_HINT_RE.source,
388
+ IDEMPOTENT_HINT_RE.source,
389
+ OPEN_WORLD_HINT_RE.source
390
+ ].join(" & "),
391
+ sample: hintsSample,
392
+ matched: READ_ONLY_HINT_RE.test(hintsSample) && DESTRUCTIVE_HINT_RE.test(hintsSample) && IDEMPOTENT_HINT_RE.test(hintsSample) && OPEN_WORLD_HINT_RE.test(hintsSample)
393
+ },
394
+ tokensCtx: {
395
+ pattern: TOKENS_FIELD_RE.source,
396
+ sample: tokensSample,
397
+ matched: TOKENS_FIELD_RE.test(tokensSample)
398
+ },
399
+ pagination: {
400
+ pattern: [
401
+ PAGINATION_CURSOR_RE.source,
402
+ PAGINATION_DEFAULT_RE.source,
403
+ PAGINATION_MAX_RE.source
404
+ ].join(" & "),
405
+ sample: paginationSample,
406
+ matched: PAGINATION_CURSOR_RE.test(paginationSample) && PAGINATION_DEFAULT_RE.test(paginationSample) && PAGINATION_MAX_RE.test(paginationSample),
407
+ rejectsUnbounded: !PAGINATION_MAX_RE.test(paginationUnboundedSample)
408
+ }
409
+ };
410
+ }
411
+ var CALL_OPENER_LOOKBACK = 10;
412
+ function callOpenerLine(file, siteLine) {
413
+ for (let n = siteLine; n >= Math.max(1, siteLine - CALL_OPENER_LOOKBACK); n--) {
414
+ const line = file.lines[n - 1];
415
+ if (!line || line.comment) continue;
416
+ if (TOOL_INLINE_LITERAL_RE.test(line.raw) || TOOL_INLINE_IDENT_RE.test(line.raw) || OPENER_RE.test(line.raw)) {
417
+ return line.n;
418
+ }
419
+ }
420
+ return siteLine;
421
+ }
422
+ function ownCallEnd(file, siteLine) {
423
+ const startLine = callOpenerLine(file, siteLine);
424
+ let depth = 0;
425
+ for (const line of file.lines) {
426
+ if (line.n < startLine) continue;
427
+ if (!line.comment) {
428
+ depth += (line.raw.match(/\(/g) ?? []).length - (line.raw.match(/\)/g) ?? []).length;
429
+ }
430
+ if (line.n >= startLine && depth <= 0 && line.n > startLine) return line.n;
431
+ if (line.n === startLine && depth <= 0) {
432
+ return line.n;
433
+ }
434
+ }
435
+ return file.lines.length;
436
+ }
437
+ function findToolAnnotations(tree) {
438
+ const allSites = findRegisteredTools(tree);
439
+ const out = [];
440
+ for (const file of tree.files) {
441
+ const prefix = `${file.rel}:`;
442
+ const sitesInFile = allSites.filter((s) => s.anchor.startsWith(prefix)).map((s) => ({ ...s, line: Number.parseInt(s.anchor.slice(prefix.length), 10) })).sort((a, b) => a.line - b.line).map((s) => ({ ...s, ownEnd: ownCallEnd(file, s.line) }));
443
+ for (let i = 0; i < sitesInFile.length; i++) {
444
+ const site = sitesInFile[i];
445
+ const windowStart = i === 0 ? 0 : sitesInFile[i - 1].ownEnd;
446
+ const windowEnd = site.ownEnd;
447
+ let verb;
448
+ let table;
449
+ let searchMechanism;
450
+ let authRead;
451
+ let authWrite;
452
+ let authFolded = false;
453
+ let readOnlyHintDeclared = false;
454
+ let destructiveHintDeclared = false;
455
+ let idempotentHintDeclared = false;
456
+ let openWorldHintDeclared = false;
457
+ let tokensEstimeContexte;
458
+ let paginationCursorDeclared = false;
459
+ let paginationDefault;
460
+ let paginationMax;
461
+ for (const line of file.lines) {
462
+ if (line.n <= windowStart || line.n > windowEnd) continue;
463
+ if (line.comment) continue;
464
+ const raw = line.raw;
465
+ if (!verb) {
466
+ const m = raw.match(VERB_FIELD_RE);
467
+ if (m) verb = m[1];
468
+ }
469
+ if (!table) {
470
+ const m = raw.match(TABLE_FIELD_RE);
471
+ if (m) table = m[1];
472
+ }
473
+ if (!searchMechanism) {
474
+ const m = raw.match(MECHANISM_FIELD_RE);
475
+ if (m) searchMechanism = m[1];
476
+ }
477
+ if (!authRead) {
478
+ const m = raw.match(AUTH_READ_FIELD_RE);
479
+ if (m) authRead = m[1];
480
+ }
481
+ if (!authWrite) {
482
+ const m = raw.match(AUTH_WRITE_FIELD_RE);
483
+ if (m) authWrite = m[1];
484
+ }
485
+ if (!authFolded && AUTH_FOLDED_FIELD_RE.test(raw) && !/\bauth(?:Read|Write)\s*:/.test(raw)) {
486
+ authFolded = true;
487
+ }
488
+ if (!readOnlyHintDeclared && READ_ONLY_HINT_RE.test(raw)) readOnlyHintDeclared = true;
489
+ if (!destructiveHintDeclared && DESTRUCTIVE_HINT_RE.test(raw))
490
+ destructiveHintDeclared = true;
491
+ if (!idempotentHintDeclared && IDEMPOTENT_HINT_RE.test(raw)) idempotentHintDeclared = true;
492
+ if (!openWorldHintDeclared && OPEN_WORLD_HINT_RE.test(raw)) openWorldHintDeclared = true;
493
+ if (tokensEstimeContexte === void 0) {
494
+ const m = raw.match(TOKENS_FIELD_RE);
495
+ if (m) tokensEstimeContexte = Number.parseInt(m[1], 10);
496
+ }
497
+ if (!paginationCursorDeclared && PAGINATION_CURSOR_RE.test(raw)) {
498
+ paginationCursorDeclared = true;
499
+ }
500
+ if (paginationDefault === void 0) {
501
+ const m = raw.match(PAGINATION_DEFAULT_RE);
502
+ if (m) paginationDefault = Number.parseInt(m[1], 10);
503
+ }
504
+ if (paginationMax === void 0) {
505
+ const m = raw.match(PAGINATION_MAX_RE);
506
+ if (m) paginationMax = Number.parseInt(m[1], 10);
507
+ }
508
+ }
509
+ out.push({
510
+ name: site.name,
511
+ anchor: site.anchor,
512
+ verb,
513
+ table,
514
+ searchMechanism,
515
+ authRead,
516
+ authWrite,
517
+ authFolded,
518
+ readOnlyHintDeclared,
519
+ destructiveHintDeclared,
520
+ idempotentHintDeclared,
521
+ openWorldHintDeclared,
522
+ tokensEstimeContexte,
523
+ paginationCursorDeclared,
524
+ paginationDefault,
525
+ paginationMax
526
+ });
527
+ }
528
+ }
529
+ return out;
530
+ }
227
531
  function countKeywordInCode(tree, re) {
228
532
  let count = 0;
229
533
  let firstFile;
@@ -240,8 +544,2180 @@ function countKeywordInCode(tree, re) {
240
544
  }
241
545
  }
242
546
  }
243
- return { count, firstFile, firstLine };
244
- }
547
+ return { count, firstFile, firstLine };
548
+ }
549
+
550
+ // src/detectors/c8-authority-declared.ts
551
+ var TIER_SET = /* @__PURE__ */ new Set(["public", "org-member", "org-admin", "fleet-internal", "master"]);
552
+ function c8PositiveControls() {
553
+ const controls = toolAnnotationFieldControls();
554
+ return {
555
+ authReadMatcher: controls.authRead,
556
+ authWriteMatcher: controls.authWrite,
557
+ authFoldedMatcher: controls.authFolded,
558
+ setAcceptsPublic: TIER_SET.has("public"),
559
+ setRejectsOutOfSet: !TIER_SET.has("superadmin")
560
+ };
561
+ }
562
+ function isFolded(t) {
563
+ return t.authFolded === true && t.authRead === void 0 && t.authWrite === void 0;
564
+ }
565
+ var c8AuthorityDeclared = {
566
+ id: "C8 authority-declared",
567
+ rule: "C-8",
568
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
569
+ run(tree) {
570
+ const controls = c8PositiveControls();
571
+ if (!controls.authReadMatcher.matched || !controls.authWriteMatcher.matched || !controls.authFoldedMatcher.matched) {
572
+ throw new Error(
573
+ `C8 positive control FAILED: authRead=${controls.authReadMatcher.matched} authWrite=${controls.authWriteMatcher.matched} folded=${controls.authFoldedMatcher.matched} \u2014 matcher broken, verdict untrustworthy`
574
+ );
575
+ }
576
+ if (!controls.setAcceptsPublic || !controls.setRejectsOutOfSet) {
577
+ throw new Error(
578
+ "C8 closed-set control FAILED: TIER_SET must accept a legitimate tier (public) and reject an out-of-set value (superadmin) \u2014 verdict untrustworthy"
579
+ );
580
+ }
581
+ const unresolved = findUnresolvedToolSites(tree);
582
+ if (unresolved.length > 0) {
583
+ const reason = `tool name at ${unresolved.length} registration site(s) is computed/imported/template (${unresolved.join(", ")}) \u2014 declared authority unattributable statically (C-8)`;
584
+ return {
585
+ id: this.id,
586
+ anchor: unresolved[0],
587
+ state: "could-not-judge",
588
+ reason,
589
+ verdict: `could not judge: ${reason}`
590
+ };
591
+ }
592
+ const tools = findToolAnnotations(tree);
593
+ if (tools.length === 0) {
594
+ return {
595
+ id: this.id,
596
+ anchor: "-",
597
+ state: "absent",
598
+ verdict: "no tool registrations found; authority-declared check n/a"
599
+ };
600
+ }
601
+ for (const t of tools) {
602
+ if (isFolded(t)) {
603
+ return {
604
+ id: this.id,
605
+ anchor: t.anchor,
606
+ state: "non-conforming",
607
+ verdict: `tool "${t.name}" declares a single folded "auth" field instead of the (authRead, authWrite) pair \u2014 folding erases the read/write divergence (positive-control OK) -> declare authRead and authWrite separately from the closed 5-tier set (C-8)`
608
+ };
609
+ }
610
+ if (!t.authRead || !t.authWrite) {
611
+ return {
612
+ id: this.id,
613
+ anchor: t.anchor,
614
+ state: "non-conforming",
615
+ verdict: `tool "${t.name}" is missing ${!t.authRead ? "authRead" : "authWrite"} (positive-control OK) -> declare the full (authRead, authWrite) pair from public/org-member/org-admin/fleet-internal/master (C-8)`
616
+ };
617
+ }
618
+ if (!TIER_SET.has(t.authRead)) {
619
+ return {
620
+ id: this.id,
621
+ anchor: t.anchor,
622
+ state: "non-conforming",
623
+ verdict: `tool "${t.name}" declares authRead "${t.authRead}", outside the closed 5-tier set (positive-control OK) -> use public/org-member/org-admin/fleet-internal/master (C-8)`
624
+ };
625
+ }
626
+ if (!TIER_SET.has(t.authWrite)) {
627
+ return {
628
+ id: this.id,
629
+ anchor: t.anchor,
630
+ state: "non-conforming",
631
+ verdict: `tool "${t.name}" declares authWrite "${t.authWrite}", outside the closed 5-tier set (positive-control OK) -> use public/org-member/org-admin/fleet-internal/master (C-8)`
632
+ };
633
+ }
634
+ }
635
+ return {
636
+ id: this.id,
637
+ anchor: tools[0].anchor,
638
+ state: "present",
639
+ verdict: `${tools.length} tool(s) each declare a distinct (authRead, authWrite) pair in the closed 5-tier set (positive-control OK)`
640
+ };
641
+ }
642
+ };
643
+
644
+ // src/detectors/c9-verb-declared.ts
645
+ var VERB_SET = /* @__PURE__ */ new Set([
646
+ "list",
647
+ "get",
648
+ "search",
649
+ "aggregate",
650
+ "create",
651
+ "update",
652
+ "delete",
653
+ "upsert",
654
+ "transition",
655
+ "bulk-write",
656
+ "pure",
657
+ "introspect",
658
+ "external-effect"
659
+ ]);
660
+ function c9PositiveControls() {
661
+ const field = toolAnnotationFieldControls().verb;
662
+ return {
663
+ fieldMatcher: field,
664
+ setAcceptsCreate: VERB_SET.has("create"),
665
+ setRejectsRpc: !VERB_SET.has("rpc")
666
+ };
667
+ }
668
+ var c9VerbDeclared = {
669
+ id: "C9 verb-declared",
670
+ rule: "C-9",
671
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
672
+ run(tree) {
673
+ const controls = c9PositiveControls();
674
+ if (!controls.fieldMatcher.matched) {
675
+ throw new Error(
676
+ `C9 positive control FAILED: pattern /${controls.fieldMatcher.pattern}/ did not match sample \`${controls.fieldMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
677
+ );
678
+ }
679
+ if (!controls.setAcceptsCreate || !controls.setRejectsRpc) {
680
+ throw new Error(
681
+ "C9 closed-set control FAILED: VERB_SET must accept a legitimate verb (create) and reject the banned rpc sentinel \u2014 verdict untrustworthy"
682
+ );
683
+ }
684
+ const unresolved = findUnresolvedToolSites(tree);
685
+ if (unresolved.length > 0) {
686
+ const reason = `tool name at ${unresolved.length} registration site(s) is computed/imported/template (${unresolved.join(", ")}) \u2014 declared verb unattributable statically (C-9)`;
687
+ return {
688
+ id: this.id,
689
+ anchor: unresolved[0],
690
+ state: "could-not-judge",
691
+ reason,
692
+ verdict: `could not judge: ${reason}`
693
+ };
694
+ }
695
+ const tools = findToolAnnotations(tree);
696
+ if (tools.length === 0) {
697
+ return {
698
+ id: this.id,
699
+ anchor: "-",
700
+ state: "absent",
701
+ verdict: "no tool registrations found; verb-declared check n/a"
702
+ };
703
+ }
704
+ for (const t of tools) {
705
+ if (!t.verb) {
706
+ return {
707
+ id: this.id,
708
+ anchor: t.anchor,
709
+ state: "non-conforming",
710
+ verdict: `tool "${t.name}" declares no operation verb (positive-control OK) -> add annotations.verb from the closed CRUD-and-beyond set (C-9)`
711
+ };
712
+ }
713
+ if (t.verb === "rpc") {
714
+ return {
715
+ id: this.id,
716
+ anchor: t.anchor,
717
+ state: "non-conforming",
718
+ verdict: `tool "${t.name}" declares verb "rpc" \u2014 rpc is the absence of a verb and never publishes (positive-control OK) -> declare a real verb (C-9)`
719
+ };
720
+ }
721
+ if (!VERB_SET.has(t.verb)) {
722
+ return {
723
+ id: this.id,
724
+ anchor: t.anchor,
725
+ state: "non-conforming",
726
+ verdict: `tool "${t.name}" declares verb "${t.verb}", outside the closed set (positive-control OK) -> use list/get/search/aggregate/create/update/delete/upsert/transition/bulk-write/pure/introspect/external-effect (C-9)`
727
+ };
728
+ }
729
+ }
730
+ return {
731
+ id: this.id,
732
+ anchor: tools[0].anchor,
733
+ state: "present",
734
+ verdict: `${tools.length} tool(s) each declare a verb in the closed set (positive-control OK)`
735
+ };
736
+ }
737
+ };
738
+
739
+ // src/detectors/c10-mechanism-declared.ts
740
+ var MECHANISM_BASE_SET = /* @__PURE__ */ new Set([
741
+ "indexed-range",
742
+ "indexed-bm25",
743
+ "indexed-vector",
744
+ "indexed-hybrid-rrf"
745
+ ]);
746
+ function baseOf(mechanism) {
747
+ const paren = mechanism.indexOf("(");
748
+ return paren === -1 ? mechanism : mechanism.slice(0, paren);
749
+ }
750
+ function isListOrSearch(t) {
751
+ if (t.verb) return t.verb === "list" || t.verb === "search";
752
+ return /^(list|search)/i.test(t.name);
753
+ }
754
+ function c10PositiveControls() {
755
+ const field = toolAnnotationFieldControls().mechanism;
756
+ return {
757
+ fieldMatcher: field,
758
+ setAcceptsIndexedBm25: MECHANISM_BASE_SET.has("indexed-bm25"),
759
+ setRejectsUnindexedScan: !MECHANISM_BASE_SET.has("unindexed-scan"),
760
+ vectorParamsStripped: baseOf("indexed-vector(cosine, 1536, 20)") === "indexed-vector"
761
+ };
762
+ }
763
+ var c10MechanismDeclared = {
764
+ id: "C10 mechanism-declared",
765
+ rule: "C-10",
766
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
767
+ run(tree) {
768
+ const controls = c10PositiveControls();
769
+ if (!controls.fieldMatcher.matched) {
770
+ throw new Error(
771
+ `C10 positive control FAILED: pattern /${controls.fieldMatcher.pattern}/ did not match sample \`${controls.fieldMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
772
+ );
773
+ }
774
+ if (!controls.setAcceptsIndexedBm25 || !controls.setRejectsUnindexedScan || !controls.vectorParamsStripped) {
775
+ throw new Error(
776
+ "C10 closed-set control FAILED: MECHANISM_BASE_SET must accept indexed-bm25, reject unindexed-scan, and strip indexed-vector(...) params \u2014 verdict untrustworthy"
777
+ );
778
+ }
779
+ const unresolved = findUnresolvedToolSites(tree);
780
+ if (unresolved.length > 0) {
781
+ const reason = `tool name at ${unresolved.length} registration site(s) is computed/imported/template (${unresolved.join(", ")}) \u2014 declared mechanism unattributable statically (C-10)`;
782
+ return {
783
+ id: this.id,
784
+ anchor: unresolved[0],
785
+ state: "could-not-judge",
786
+ reason,
787
+ verdict: `could not judge: ${reason}`
788
+ };
789
+ }
790
+ const applicable = findToolAnnotations(tree).filter(isListOrSearch);
791
+ if (applicable.length === 0) {
792
+ return {
793
+ id: this.id,
794
+ anchor: "-",
795
+ state: "absent",
796
+ verdict: "no list/search tool found; mechanism-declared check n/a"
797
+ };
798
+ }
799
+ for (const t of applicable) {
800
+ if (!t.searchMechanism) {
801
+ return {
802
+ id: this.id,
803
+ anchor: t.anchor,
804
+ state: "non-conforming",
805
+ verdict: `list/search tool "${t.name}" declares no search mechanism (positive-control OK) -> add annotations.searchMechanism from the closed indexing-strategy set (C-10)`
806
+ };
807
+ }
808
+ const base = baseOf(t.searchMechanism);
809
+ if (base === "unindexed-scan") {
810
+ return {
811
+ id: this.id,
812
+ anchor: t.anchor,
813
+ state: "non-conforming",
814
+ verdict: `list/search tool "${t.name}" declares "unindexed-scan" \u2014 refused at publication (positive-control OK) -> declare an indexed mechanism (C-10)`
815
+ };
816
+ }
817
+ if (!MECHANISM_BASE_SET.has(base)) {
818
+ return {
819
+ id: this.id,
820
+ anchor: t.anchor,
821
+ state: "non-conforming",
822
+ verdict: `list/search tool "${t.name}" declares mechanism "${t.searchMechanism}", outside the closed set (positive-control OK) -> use indexed-range/indexed-bm25/indexed-vector/indexed-hybrid-rrf (C-10)`
823
+ };
824
+ }
825
+ }
826
+ return {
827
+ id: this.id,
828
+ anchor: applicable[0].anchor,
829
+ state: "present",
830
+ verdict: `${applicable.length} list/search tool(s) each declare a mechanism in the closed set (positive-control OK)`
831
+ };
832
+ }
833
+ };
834
+
835
+ // src/detectors/c11-pagination.ts
836
+ var PIN2 = "a74a042b53c71a095ab88b3073bd47728eeda814";
837
+ var PAGINABLE_RE = LIST_READ_RE;
838
+ function c11PositiveControls() {
839
+ const field = toolAnnotationFieldControls().pagination;
840
+ return { fieldMatcher: field };
841
+ }
842
+ var c11Pagination = {
843
+ id: "C11 pagination-declared-applied",
844
+ rule: "C-11",
845
+ pin: PIN2,
846
+ run(tree) {
847
+ const controls = c11PositiveControls();
848
+ if (!controls.fieldMatcher.matched || !controls.fieldMatcher.rejectsUnbounded) {
849
+ throw new Error(
850
+ `C11 positive control FAILED: pattern /${controls.fieldMatcher.pattern}/ matched=${controls.fieldMatcher.matched} rejectsUnbounded=${controls.fieldMatcher.rejectsUnbounded} \u2014 matcher broken, verdict untrustworthy`
851
+ );
852
+ }
853
+ const unresolved = findUnresolvedToolSites(tree);
854
+ if (unresolved.length > 0) {
855
+ const reason = `tool name at ${unresolved.length} registration site(s) is computed/imported/template (${unresolved.join(", ")}) \u2014 paginable-shape unattributable statically (C-11)`;
856
+ return {
857
+ id: this.id,
858
+ anchor: unresolved[0],
859
+ state: "could-not-judge",
860
+ reason,
861
+ verdict: `could not judge: ${reason}`
862
+ };
863
+ }
864
+ const tools = findToolAnnotations(tree).filter((t) => PAGINABLE_RE.test(t.name));
865
+ if (tools.length === 0) {
866
+ return {
867
+ id: this.id,
868
+ anchor: "-",
869
+ state: "absent",
870
+ verdict: "no list/read/get/search tool registrations found; pagination declaration check n/a"
871
+ };
872
+ }
873
+ for (const t of tools) {
874
+ const missing = [];
875
+ if (!t.paginationCursorDeclared) missing.push("paginationCursor");
876
+ if (t.paginationDefault === void 0) missing.push("paginationDefault");
877
+ if (t.paginationMax === void 0) missing.push("paginationMax (finite)");
878
+ if (missing.length > 0) {
879
+ return {
880
+ id: this.id,
881
+ anchor: t.anchor,
882
+ state: "non-conforming",
883
+ verdict: `tool "${t.name}" is missing pagination declaration(s): ${missing.join(", ")} (positive-control OK) -> declare annotations.paginationCursor/paginationDefault/paginationMax with a FINITE max \u2014 an unbounded max never publishes (C-11)`
884
+ };
885
+ }
886
+ }
887
+ return {
888
+ id: this.id,
889
+ anchor: tools[0].anchor,
890
+ state: "present",
891
+ verdict: `${tools.length} paginable tool(s) each declare cursor + default + finite max (positive-control OK)`
892
+ };
893
+ },
894
+ /**
895
+ * DYNAMIC — confirms the tool(s) the static shape considers paginable are
896
+ * genuinely SERVED by the running server (a real runtime fact from the
897
+ * tools/list snapshot). Does NOT perform the invalid-cursor -> -32602
898
+ * round trip: the one-shot RuntimeContext snapshot carries no per-tool
899
+ * schema and no `callTool` verb — that half needs `Session.callTool` with a
900
+ * cursor argument (documented gap, types.ts `Session`). The verdict names
901
+ * this explicitly rather than folding it into a silent full "present".
902
+ */
903
+ runDynamic(runtime) {
904
+ if (runtime.launchError) {
905
+ const reason = `server could not be started/introspected \u2014 runtime pagination applicability unverifiable: ${runtime.launchError} (C-11)`;
906
+ return {
907
+ id: this.id,
908
+ anchor: `${runtime.root} (runtime)`,
909
+ state: "could-not-judge",
910
+ reason,
911
+ verdict: `could not judge: ${reason}`
912
+ };
913
+ }
914
+ const paginable = runtime.tools.filter((t) => PAGINABLE_RE.test(t.name));
915
+ if (paginable.length === 0) {
916
+ return {
917
+ id: this.id,
918
+ anchor: `${runtime.root} (runtime tools/list)`,
919
+ state: "absent",
920
+ verdict: "server advertises no list/read/get/search tool at runtime; pagination check n/a"
921
+ };
922
+ }
923
+ const names = paginable.map((t) => t.name).join(", ");
924
+ return {
925
+ id: this.id,
926
+ anchor: `${runtime.root} (runtime tools/list)`,
927
+ state: "present",
928
+ verdict: `runtime confirms paginable tool(s) actually served: ${names}. The invalid-cursor -> -32602 round trip is NOT performed by this snapshot surface (needs Session.callTool with a cursor argument, not yet exposed \u2014 see types.ts Session doc); this dynamic half asserts service reachability, not the full cursor-handling guarantee.`
929
+ };
930
+ }
931
+ };
932
+
933
+ // src/detectors/c12-no-duplicates.ts
934
+ function tripletKey(t) {
935
+ return `${t.verb}::${t.table}::${t.searchMechanism ?? ""}`;
936
+ }
937
+ function c12PositiveControls() {
938
+ const a = { name: "get_customer", anchor: "a", verb: "get", table: "customers" };
939
+ const b = {
940
+ name: "fetch_customer",
941
+ anchor: "b",
942
+ verb: "get",
943
+ table: "customers"
944
+ };
945
+ const c = { name: "get_order", anchor: "c", verb: "get", table: "orders" };
946
+ return {
947
+ fieldMatcher: toolAnnotationFieldControls().table,
948
+ detectsDuplicate: tripletKey(a) === tripletKey(b),
949
+ clearsDistinctTable: tripletKey(a) !== tripletKey(c)
950
+ };
951
+ }
952
+ var c12NoDuplicates = {
953
+ id: "C12 no-duplicates",
954
+ rule: "C-12",
955
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
956
+ run(tree) {
957
+ const controls = c12PositiveControls();
958
+ if (!controls.fieldMatcher.matched) {
959
+ throw new Error(
960
+ `C12 positive control FAILED: pattern /${controls.fieldMatcher.pattern}/ did not match sample \`${controls.fieldMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
961
+ );
962
+ }
963
+ if (!controls.detectsDuplicate || !controls.clearsDistinctTable) {
964
+ throw new Error(
965
+ "C12 dedupe-key control FAILED: must match on an identical (verb, table) pair and differ on a distinct table \u2014 verdict untrustworthy"
966
+ );
967
+ }
968
+ const unresolved = findUnresolvedToolSites(tree);
969
+ if (unresolved.length > 0) {
970
+ const reason = `tool name at ${unresolved.length} registration site(s) is computed/imported/template (${unresolved.join(", ")}) \u2014 triplet unattributable statically (C-12)`;
971
+ return {
972
+ id: this.id,
973
+ anchor: unresolved[0],
974
+ state: "could-not-judge",
975
+ reason,
976
+ verdict: `could not judge: ${reason}`
977
+ };
978
+ }
979
+ const tools = findToolAnnotations(tree);
980
+ if (tools.length === 0) {
981
+ return {
982
+ id: this.id,
983
+ anchor: "-",
984
+ state: "absent",
985
+ verdict: "no tool registrations found; no-duplicates check n/a"
986
+ };
987
+ }
988
+ const considered = tools.filter((t) => t.verb && t.table);
989
+ if (considered.length === 0) {
990
+ const reason = "no tool declares both a verb and a table \u2014 duplicate-triplet check unverifiable tree-wide (C-12; see C-9 for missing-verb conformance separately)";
991
+ return {
992
+ id: this.id,
993
+ anchor: tools[0].anchor,
994
+ state: "could-not-judge",
995
+ reason,
996
+ verdict: `could not judge: ${reason}`
997
+ };
998
+ }
999
+ const groups = /* @__PURE__ */ new Map();
1000
+ for (const t of considered) {
1001
+ const key = tripletKey(t);
1002
+ const list = groups.get(key) ?? [];
1003
+ list.push(t);
1004
+ groups.set(key, list);
1005
+ }
1006
+ for (const [key, members] of groups) {
1007
+ if (members.length >= 2) {
1008
+ const anchors = members.map((m) => `${m.name}@${m.anchor}`).join(", ");
1009
+ return {
1010
+ id: this.id,
1011
+ anchor: members[0].anchor,
1012
+ state: "non-conforming",
1013
+ verdict: `duplicate triplet (verb::table::mechanism = "${key}") on ${members.length} tools (positive-control OK): ${anchors} -> collapse to one tool, express the other as documentation (C-12)`
1014
+ };
1015
+ }
1016
+ }
1017
+ return {
1018
+ id: this.id,
1019
+ anchor: considered[0].anchor,
1020
+ state: "present",
1021
+ verdict: `${considered.length} tool(s) with a declared (verb, table) checked, no duplicate triplet found (positive-control OK)`
1022
+ };
1023
+ }
1024
+ };
1025
+
1026
+ // src/detectors/c13-context-ceiling.ts
1027
+ var CEILING_GLOBAL_RE = /\b(?:CONTEXT_CEILING|contextCeiling)\b[\s\S]{0,80}?\bglobal\s*:\s*(\d+)\b/;
1028
+ var CEILING_PER_TOOL_RE = /\b(?:CONTEXT_CEILING|contextCeiling)\b[\s\S]{0,80}?\bperTool\s*:\s*(\d+)\b/;
1029
+ function findCeiling(tree) {
1030
+ for (const file of tree.files) {
1031
+ for (const line of file.lines) {
1032
+ if (line.comment) continue;
1033
+ if (/\b(?:CONTEXT_CEILING|contextCeiling)\b/.test(line.raw)) {
1034
+ const start = line.n - 1;
1035
+ const windowLines = file.lines.slice(start, start + 6).map((l) => l.raw);
1036
+ const window = windowLines.join("\n");
1037
+ const g = window.match(CEILING_GLOBAL_RE);
1038
+ const p = window.match(CEILING_PER_TOOL_RE);
1039
+ if (g && p) {
1040
+ return {
1041
+ global: Number.parseInt(g[1], 10),
1042
+ perTool: Number.parseInt(p[1], 10),
1043
+ anchor: `${file.rel}:${line.n}`
1044
+ };
1045
+ }
1046
+ }
1047
+ }
1048
+ }
1049
+ return null;
1050
+ }
1051
+ function c13PositiveControls() {
1052
+ const complete = "const CONTEXT_CEILING = { global: 8000, perTool: 800 };";
1053
+ const partial = "const CONTEXT_CEILING = { global: 8000 };";
1054
+ return {
1055
+ tokensMatcher: toolAnnotationFieldControls().tokensCtx,
1056
+ ceilingAcceptsComplete: CEILING_GLOBAL_RE.test(complete) && CEILING_PER_TOOL_RE.test(complete),
1057
+ ceilingRejectsPartial: !(CEILING_GLOBAL_RE.test(partial) && CEILING_PER_TOOL_RE.test(partial))
1058
+ };
1059
+ }
1060
+ var c13ContextCeiling = {
1061
+ id: "C13 context-ceiling",
1062
+ rule: "C-13",
1063
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
1064
+ run(tree) {
1065
+ const controls = c13PositiveControls();
1066
+ if (!controls.tokensMatcher.matched) {
1067
+ throw new Error(
1068
+ `C13 positive control FAILED: pattern /${controls.tokensMatcher.pattern}/ did not match sample \`${controls.tokensMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
1069
+ );
1070
+ }
1071
+ if (!controls.ceilingAcceptsComplete || !controls.ceilingRejectsPartial) {
1072
+ throw new Error(
1073
+ "C13 ceiling control FAILED: must accept a complete (global, perTool) declaration and reject a partial one \u2014 verdict untrustworthy"
1074
+ );
1075
+ }
1076
+ const unresolved = findUnresolvedToolSites(tree);
1077
+ if (unresolved.length > 0) {
1078
+ const reason = `tool name at ${unresolved.length} registration site(s) is computed/imported/template (${unresolved.join(", ")}) \u2014 declared context cost unattributable statically (C-13)`;
1079
+ return {
1080
+ id: this.id,
1081
+ anchor: unresolved[0],
1082
+ state: "could-not-judge",
1083
+ reason,
1084
+ verdict: `could not judge: ${reason}`
1085
+ };
1086
+ }
1087
+ const tools = findToolAnnotations(tree);
1088
+ if (tools.length === 0) {
1089
+ return {
1090
+ id: this.id,
1091
+ anchor: "-",
1092
+ state: "absent",
1093
+ verdict: "no tool registrations found; context-ceiling check n/a"
1094
+ };
1095
+ }
1096
+ const ceiling = findCeiling(tree);
1097
+ if (!ceiling) {
1098
+ return {
1099
+ id: this.id,
1100
+ anchor: tools[0].anchor,
1101
+ state: "non-conforming",
1102
+ verdict: `${tools.length} tool(s) registered but no (global, perTool) context ceiling declared anywhere (positive-control OK) -> declare CONTEXT_CEILING = { global, perTool } (C-13)`
1103
+ };
1104
+ }
1105
+ let sum = 0;
1106
+ for (const t of tools) {
1107
+ if (t.tokensEstimeContexte === void 0) {
1108
+ return {
1109
+ id: this.id,
1110
+ anchor: t.anchor,
1111
+ state: "non-conforming",
1112
+ verdict: `tool "${t.name}" declares no tokensEstimeContexte (positive-control OK, ceiling declared at ${ceiling.anchor}) -> declare tokensEstimeContexte on every tool (C-13)`
1113
+ };
1114
+ }
1115
+ if (t.tokensEstimeContexte > ceiling.perTool) {
1116
+ return {
1117
+ id: this.id,
1118
+ anchor: t.anchor,
1119
+ state: "non-conforming",
1120
+ verdict: `tool "${t.name}" declares tokensEstimeContexte=${t.tokensEstimeContexte}, above the declared per-tool ceiling ${ceiling.perTool} (${ceiling.anchor}) -> stage the schema, publish by tiers (C-13)`
1121
+ };
1122
+ }
1123
+ sum += t.tokensEstimeContexte;
1124
+ }
1125
+ if (sum > ceiling.global) {
1126
+ return {
1127
+ id: this.id,
1128
+ anchor: ceiling.anchor,
1129
+ state: "non-conforming",
1130
+ verdict: `declared per-tool costs sum to ${sum}, above the declared global ceiling ${ceiling.global} (${ceiling.anchor}) -> stage publication by tiers (C-13)`
1131
+ };
1132
+ }
1133
+ return {
1134
+ id: this.id,
1135
+ anchor: ceiling.anchor,
1136
+ state: "present",
1137
+ verdict: `${tools.length} tool(s) each declare tokensEstimeContexte within the declared per-tool ceiling (${ceiling.perTool}); sum ${sum} within global ceiling ${ceiling.global} (positive-control OK)`
1138
+ };
1139
+ }
1140
+ };
1141
+
1142
+ // src/detectors/c14-catalog-registration.ts
1143
+ var PIN3 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1144
+ var REQUIRED_FIELDS = [
1145
+ "name",
1146
+ "version",
1147
+ "description",
1148
+ "description_fr",
1149
+ "repo_url",
1150
+ "npm_url",
1151
+ "keywords",
1152
+ "auth_type",
1153
+ "transport",
1154
+ "bilingual"
1155
+ ];
1156
+ function readCatalogBlock(tree) {
1157
+ if (!tree.packageJson) return null;
1158
+ const raw = tree.packageJson.json.mcpCatalog;
1159
+ if (!raw || typeof raw !== "object") return null;
1160
+ return { fields: raw, anchor: `${tree.packageJson.path}:mcpCatalog` };
1161
+ }
1162
+ function c14PositiveControls() {
1163
+ const complete = {
1164
+ name: "x",
1165
+ version: "1.0.0",
1166
+ description: "en",
1167
+ description_fr: "fr",
1168
+ repo_url: "https://github.com/x/y",
1169
+ npm_url: "https://npmjs.com/package/x",
1170
+ keywords: ["mcp"],
1171
+ auth_type: "public",
1172
+ transport: "stdio",
1173
+ bilingual: true
1174
+ };
1175
+ const partial = { ...complete };
1176
+ partial.repo_url = void 0;
1177
+ const missing = (block) => REQUIRED_FIELDS.find((f) => block[f] === void 0 || block[f] === null);
1178
+ return {
1179
+ acceptsComplete: missing(complete) === void 0,
1180
+ rejectsPartial: missing(partial) !== void 0
1181
+ };
1182
+ }
1183
+ var c14CatalogRegistration = {
1184
+ id: "C14 catalog-registration",
1185
+ rule: "C-14",
1186
+ pin: PIN3,
1187
+ run(tree) {
1188
+ const controls = c14PositiveControls();
1189
+ if (!controls.acceptsComplete || !controls.rejectsPartial) {
1190
+ throw new Error(
1191
+ `C14 positive control FAILED: acceptsComplete=${controls.acceptsComplete} rejectsPartial=${controls.rejectsPartial} \u2014 matcher broken, verdict untrustworthy`
1192
+ );
1193
+ }
1194
+ const block = readCatalogBlock(tree);
1195
+ if (!block) {
1196
+ return {
1197
+ id: this.id,
1198
+ anchor: tree.packageJson?.path ?? tree.root,
1199
+ state: "absent",
1200
+ verdict: "no mcpCatalog block declared in package.json; this run carries no publication-catalog metadata (C-14 n/a)"
1201
+ };
1202
+ }
1203
+ for (const field of REQUIRED_FIELDS) {
1204
+ const v = block.fields[field];
1205
+ if (v === void 0 || v === null) {
1206
+ return {
1207
+ id: this.id,
1208
+ anchor: block.anchor,
1209
+ state: "non-conforming",
1210
+ verdict: `mcpCatalog block at ${block.anchor} is missing required field "${field}" (positive-control OK) -> declare it before publication (C-14)`
1211
+ };
1212
+ }
1213
+ }
1214
+ if (block.fields.bilingual !== true) {
1215
+ return {
1216
+ id: this.id,
1217
+ anchor: block.anchor,
1218
+ state: "non-conforming",
1219
+ verdict: `mcpCatalog.bilingual at ${block.anchor} is not literally true -> declare bilingual: true (C-14)`
1220
+ };
1221
+ }
1222
+ return {
1223
+ id: this.id,
1224
+ anchor: block.anchor,
1225
+ // C-14 is `gate + dynamique` with NO static half: its `vert` is the entry
1226
+ // being trouvable via `search_components` on the LIVE registry. A complete
1227
+ // static declaration is NOT the rule satisfied — only the rule's precondition.
1228
+ // Returning `present` here would let a tally read "satisfied" from what is
1229
+ // only "declared", the same over-claim a gate rule (C-22) correctly abstains
1230
+ // on. So a complete-but-unresolved declaration is could-not-judge, naming the
1231
+ // gate — never `present` from a static read.
1232
+ state: "could-not-judge",
1233
+ reason: `mcpCatalog block at ${block.anchor} declares all ${REQUIRED_FIELDS.length} required fields, but C-14 is gate+dynamique with no static half: satisfaction is the entry RESOLVING via search_components on the live VantageRegistry, which a static run never performs (C-14).`,
1234
+ verdict: `could not judge: mcpCatalog block at ${block.anchor} declares all ${REQUIRED_FIELDS.length} required fields (positive-control OK), but whether it RESOLVES via search_components on the live registry is the gate+dynamique half a static run cannot reach \u2014 declaration complete, resolution unverified (C-14).`
1235
+ };
1236
+ }
1237
+ };
1238
+
1239
+ // src/detectors/c15-annotations-untrusted.ts
1240
+ var AUTO_APPROVE_ACTION_RE = /\b(?:autoApprove|skipConfirm|autoRun|autoExecute)\w*\s*\(/;
1241
+ var HINT_REFERENCE_RE = /\b(?:readOnlyHint|destructiveHint|idempotentHint|openWorldHint)\b/;
1242
+ var TRUST_CHECK_RE = /\b(?:isTrustedServer|trustedSource|isPinnedSource|pinnedSource|isTrusted)\b/;
1243
+ function c15PositiveControls() {
1244
+ const unpinnedSample = "if (readOnlyHint) autoApprove(tool);";
1245
+ const pinnedSample = "if (readOnlyHint && isTrustedServer(server)) autoApprove(tool);";
1246
+ return {
1247
+ hintsMatcher: toolAnnotationFieldControls().behavioralHints,
1248
+ autoApproveDetectsUnpinned: AUTO_APPROVE_ACTION_RE.test(unpinnedSample) && HINT_REFERENCE_RE.test(unpinnedSample) && !TRUST_CHECK_RE.test(unpinnedSample),
1249
+ autoApproveClearsPinned: AUTO_APPROVE_ACTION_RE.test(pinnedSample) && HINT_REFERENCE_RE.test(pinnedSample) && TRUST_CHECK_RE.test(pinnedSample)
1250
+ };
1251
+ }
1252
+ function allHintsDeclared(t) {
1253
+ return Boolean(
1254
+ t.readOnlyHintDeclared && t.destructiveHintDeclared && t.idempotentHintDeclared && t.openWorldHintDeclared
1255
+ );
1256
+ }
1257
+ function missingHints(t) {
1258
+ const missing = [];
1259
+ if (!t.readOnlyHintDeclared) missing.push("readOnlyHint");
1260
+ if (!t.destructiveHintDeclared) missing.push("destructiveHint");
1261
+ if (!t.idempotentHintDeclared) missing.push("idempotentHint");
1262
+ if (!t.openWorldHintDeclared) missing.push("openWorldHint");
1263
+ return missing;
1264
+ }
1265
+ function findUnpinnedAutoApprovals(tree) {
1266
+ const hits = [];
1267
+ for (const file of tree.files) {
1268
+ for (const line of file.lines) {
1269
+ if (line.comment) continue;
1270
+ const raw = line.raw;
1271
+ if (AUTO_APPROVE_ACTION_RE.test(raw) && HINT_REFERENCE_RE.test(raw) && !TRUST_CHECK_RE.test(raw)) {
1272
+ hits.push({ file, line: line.n });
1273
+ }
1274
+ }
1275
+ }
1276
+ return hits;
1277
+ }
1278
+ var c15AnnotationsUntrusted = {
1279
+ id: "C15 annotations-untrusted",
1280
+ rule: "C-15",
1281
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
1282
+ run(tree) {
1283
+ const controls = c15PositiveControls();
1284
+ if (!controls.hintsMatcher.matched) {
1285
+ throw new Error(
1286
+ `C15 positive control FAILED: pattern /${controls.hintsMatcher.pattern}/ did not match sample \`${controls.hintsMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
1287
+ );
1288
+ }
1289
+ if (!controls.autoApproveDetectsUnpinned || !controls.autoApproveClearsPinned) {
1290
+ throw new Error(
1291
+ "C15 auto-approval control FAILED: must flag a hint-gated approval with no trust check, and clear one that carries a trust check \u2014 verdict untrustworthy"
1292
+ );
1293
+ }
1294
+ const unresolved = findUnresolvedToolSites(tree);
1295
+ if (unresolved.length > 0) {
1296
+ const reason = `tool name at ${unresolved.length} registration site(s) is computed/imported/template (${unresolved.join(", ")}) \u2014 declared annotations unattributable statically (C-15)`;
1297
+ return {
1298
+ id: this.id,
1299
+ anchor: unresolved[0],
1300
+ state: "could-not-judge",
1301
+ reason,
1302
+ verdict: `could not judge: ${reason}`
1303
+ };
1304
+ }
1305
+ const unpinned = findUnpinnedAutoApprovals(tree);
1306
+ if (unpinned.length > 0) {
1307
+ const anchor = `${unpinned[0].file.rel}:${unpinned[0].line}`;
1308
+ return {
1309
+ id: this.id,
1310
+ anchor,
1311
+ state: "non-conforming",
1312
+ verdict: `consumer auto-approves an action off a behavioral hint with no trusted-source check at ${anchor} (positive-control OK) -> gate the auto-approval on isTrustedServer/pinned-source in addition to the hint (C-15)`
1313
+ };
1314
+ }
1315
+ const tools = findToolAnnotations(tree);
1316
+ if (tools.length === 0) {
1317
+ return {
1318
+ id: this.id,
1319
+ anchor: "-",
1320
+ state: "absent",
1321
+ verdict: "no tool registrations found; annotations-untrusted check n/a"
1322
+ };
1323
+ }
1324
+ for (const t of tools) {
1325
+ if (!allHintsDeclared(t)) {
1326
+ const missing = missingHints(t).join(", ");
1327
+ return {
1328
+ id: this.id,
1329
+ anchor: t.anchor,
1330
+ state: "non-conforming",
1331
+ verdict: `tool "${t.name}" is missing behavioral annotation(s): ${missing} (positive-control OK) -> declare readOnlyHint/destructiveHint/idempotentHint/openWorldHint (C-15)`
1332
+ };
1333
+ }
1334
+ }
1335
+ return {
1336
+ id: this.id,
1337
+ anchor: tools[0].anchor,
1338
+ state: "present",
1339
+ verdict: `${tools.length} tool(s) each declare all four behavioral annotations, no unpinned auto-approval found (positive-control OK)`
1340
+ };
1341
+ }
1342
+ };
1343
+
1344
+ // src/detectors/c16-untrusted-output.ts
1345
+ var SYSTEM_ROLE_TEMPLATE_INTERP_RE = /role\s*:\s*["'`]system["'`][\s\S]*?(?:content|text)\s*:\s*`[^`]*\$\{/;
1346
+ var INSTRUCTION_FIELD_TEMPLATE_INTERP_RE = /\b(?:instruction|systemPrompt|prompt)\s*:\s*`[^`]*\$\{/;
1347
+ var BOUNDARY_MARKER_RE = /\b(?:sanitize\w*\s*\(|UNTRUSTED_CONTENT|trustBoundary|provenance\s*:\s*["'`]untrusted["'`])/;
1348
+ var WINDOW_RADIUS = 6;
1349
+ function c16PositiveControls() {
1350
+ const rawSystemSample = 'messages.push({ role: "system", content: `Context: ${toolOutput}` });';
1351
+ const staticSystemSample = 'messages.push({ role: "system", content: "You are a helpful assistant." });';
1352
+ const sanitizeSample = "content: sanitizeOutput(toolOutput)";
1353
+ const provenanceSample = 'meta: { provenance: "untrusted" }';
1354
+ return {
1355
+ interpolationMatchesRaw: SYSTEM_ROLE_TEMPLATE_INTERP_RE.test(rawSystemSample),
1356
+ interpolationClearsStatic: !SYSTEM_ROLE_TEMPLATE_INTERP_RE.test(staticSystemSample),
1357
+ markerMatchesSanitize: BOUNDARY_MARKER_RE.test(sanitizeSample),
1358
+ markerMatchesProvenance: BOUNDARY_MARKER_RE.test(provenanceSample)
1359
+ };
1360
+ }
1361
+ function findInterpolationSites(tree) {
1362
+ const hits = [];
1363
+ for (const file of tree.files) {
1364
+ for (const line of file.lines) {
1365
+ if (line.comment) continue;
1366
+ if (SYSTEM_ROLE_TEMPLATE_INTERP_RE.test(line.raw) || INSTRUCTION_FIELD_TEMPLATE_INTERP_RE.test(line.raw)) {
1367
+ hits.push({ file, line: line.n });
1368
+ }
1369
+ }
1370
+ }
1371
+ return hits;
1372
+ }
1373
+ function windowHasMarker(file, siteLine) {
1374
+ for (const line of file.lines) {
1375
+ if (line.n < siteLine - WINDOW_RADIUS || line.n > siteLine + WINDOW_RADIUS) continue;
1376
+ if (line.comment) continue;
1377
+ if (BOUNDARY_MARKER_RE.test(line.raw)) return true;
1378
+ }
1379
+ return false;
1380
+ }
1381
+ var c16UntrustedOutput = {
1382
+ id: "C16 untrusted-output",
1383
+ rule: "C-16",
1384
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
1385
+ run(tree) {
1386
+ const controls = c16PositiveControls();
1387
+ if (!controls.interpolationMatchesRaw || !controls.interpolationClearsStatic || !controls.markerMatchesSanitize || !controls.markerMatchesProvenance) {
1388
+ throw new Error(
1389
+ `C16 positive control FAILED: raw=${controls.interpolationMatchesRaw} staticCleared=${controls.interpolationClearsStatic} sanitizeMarker=${controls.markerMatchesSanitize} provenanceMarker=${controls.markerMatchesProvenance} \u2014 matcher broken, verdict untrustworthy`
1390
+ );
1391
+ }
1392
+ const sites = findInterpolationSites(tree);
1393
+ if (sites.length === 0) {
1394
+ return {
1395
+ id: this.id,
1396
+ anchor: "-",
1397
+ state: "absent",
1398
+ verdict: "no instruction-position template interpolation found; untrusted-output check n/a (C-16)"
1399
+ };
1400
+ }
1401
+ for (const site of sites) {
1402
+ const anchor = `${site.file.rel}:${site.line}`;
1403
+ if (!windowHasMarker(site.file, site.line)) {
1404
+ return {
1405
+ id: this.id,
1406
+ anchor,
1407
+ state: "non-conforming",
1408
+ verdict: `content interpolated into an instruction position at ${anchor} with no trust-boundary marker within ${WINDOW_RADIUS} lines (positive-control OK) -> sanitize the output or mark its provenance before it reaches an instruction field (C-16)`
1409
+ };
1410
+ }
1411
+ }
1412
+ return {
1413
+ id: this.id,
1414
+ anchor: `${sites[0].file.rel}:${sites[0].line}`,
1415
+ state: "present",
1416
+ verdict: `${sites.length} instruction-position interpolation site(s), each carrying a trust-boundary marker in its window (positive-control OK)`
1417
+ };
1418
+ }
1419
+ };
1420
+
1421
+ // src/detectors/c17-supply-chain.ts
1422
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
1423
+ import { join as join2 } from "path";
1424
+ var PIN4 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1425
+ var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
1426
+ var WORKFLOW_DIR = join2(".github", "workflows");
1427
+ var PROVENANCE_RE2 = /--provenance\b|\bprovenance\s*:\s*true\b|id-token:\s*write/i;
1428
+ function c17PositiveControls() {
1429
+ const sample = "npm publish --provenance --access public";
1430
+ const bare = "npm publish --access public";
1431
+ return {
1432
+ provenanceMatcher: {
1433
+ pattern: PROVENANCE_RE2.source,
1434
+ sample,
1435
+ matched: PROVENANCE_RE2.test(sample),
1436
+ rejectsBare: !PROVENANCE_RE2.test(bare)
1437
+ }
1438
+ };
1439
+ }
1440
+ function findLockfile(root) {
1441
+ for (const name of LOCKFILES) {
1442
+ if (existsSync2(join2(root, name))) return name;
1443
+ }
1444
+ return null;
1445
+ }
1446
+ function findProvenanceClaim(tree, root) {
1447
+ const pkg = tree.packageJson;
1448
+ if (pkg) {
1449
+ const publishConfig = pkg.json.publishConfig;
1450
+ if (publishConfig && publishConfig.provenance === true) {
1451
+ return { site: `${pkg.path}:publishConfig.provenance`, text: "provenance: true" };
1452
+ }
1453
+ }
1454
+ const workflowsDir = join2(root, WORKFLOW_DIR);
1455
+ if (!existsSync2(workflowsDir)) return null;
1456
+ let entries = [];
1457
+ try {
1458
+ entries = readdirSync(workflowsDir);
1459
+ } catch {
1460
+ return null;
1461
+ }
1462
+ for (const name of entries) {
1463
+ if (!/\.ya?ml$/.test(name)) continue;
1464
+ const path = join2(workflowsDir, name);
1465
+ let text;
1466
+ try {
1467
+ text = readFileSync2(path, "utf8");
1468
+ } catch {
1469
+ continue;
1470
+ }
1471
+ if (PROVENANCE_RE2.test(text)) {
1472
+ const line = text.split("\n").findIndex((l) => PROVENANCE_RE2.test(l)) + 1;
1473
+ return { site: `${WORKFLOW_DIR}/${name}:${line}`, text };
1474
+ }
1475
+ }
1476
+ return null;
1477
+ }
1478
+ var c17SupplyChain = {
1479
+ id: "C17 supply-chain",
1480
+ rule: "C-17",
1481
+ pin: PIN4,
1482
+ run(tree) {
1483
+ const controls = c17PositiveControls();
1484
+ if (!controls.provenanceMatcher.matched || !controls.provenanceMatcher.rejectsBare) {
1485
+ throw new Error(
1486
+ `C17 provenance-matcher control FAILED: accept(\`${controls.provenanceMatcher.sample}\`)=${controls.provenanceMatcher.matched} rejectsBare=${controls.provenanceMatcher.rejectsBare} \u2014 matcher broken, verdict untrustworthy`
1487
+ );
1488
+ }
1489
+ const lockfile = findLockfile(tree.root);
1490
+ if (!lockfile) {
1491
+ return {
1492
+ id: this.id,
1493
+ anchor: tree.root,
1494
+ state: "non-conforming",
1495
+ verdict: "no committed lockfile (package-lock.json/pnpm-lock.yaml/yarn.lock/bun.lock) found -> dependencies are not pinned; commit a lockfile (C-17a)"
1496
+ };
1497
+ }
1498
+ const claim = findProvenanceClaim(tree, tree.root);
1499
+ if (!claim) {
1500
+ return {
1501
+ id: this.id,
1502
+ anchor: `${lockfile}:1`,
1503
+ state: "non-conforming",
1504
+ verdict: `lockfile ${lockfile} present, but no provenance attestation claim found in package.json publishConfig or ${WORKFLOW_DIR}/*.yml -> add \`npm publish --provenance\` or \`publishConfig.provenance: true\` (C-17b)`
1505
+ };
1506
+ }
1507
+ return {
1508
+ id: this.id,
1509
+ anchor: claim.site,
1510
+ state: "present",
1511
+ verdict: `lockfile ${lockfile} pinned + provenance attestation claim at ${claim.site} (C-17a/b complete). Whether the attestation record actually RESOLVES against the published artifact at the registry is a separate gate check at publish time \u2014 this static run asserts the declaration is complete, not that the gate has resolved it.`
1512
+ };
1513
+ }
1514
+ };
1515
+
1516
+ // src/detectors/c18-structure-manifest.ts
1517
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1518
+ import { join as join3 } from "path";
1519
+ var PIN5 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1520
+ function manifestPath(root) {
1521
+ const extPath = join3(root, "extension.json");
1522
+ if (existsSync3(extPath)) return { kind: "extension", path: extPath };
1523
+ return { kind: "server", path: join3(root, "mcp.json") };
1524
+ }
1525
+ function readJson(path) {
1526
+ try {
1527
+ return JSON.parse(readFileSync3(path, "utf8"));
1528
+ } catch {
1529
+ return null;
1530
+ }
1531
+ }
1532
+ function c18PositiveControls() {
1533
+ const equalCheck = (a, b) => a === b;
1534
+ return {
1535
+ versionParity: {
1536
+ matched: equalCheck("0.1.0", "0.1.0"),
1537
+ rejectsMismatch: !equalCheck("0.1.0", "0.2.0")
1538
+ }
1539
+ };
1540
+ }
1541
+ var c18StructureManifest = {
1542
+ id: "C18 structure-manifest",
1543
+ rule: "C-18",
1544
+ pin: PIN5,
1545
+ run(tree) {
1546
+ const controls = c18PositiveControls();
1547
+ if (!controls.versionParity.matched || !controls.versionParity.rejectsMismatch) {
1548
+ throw new Error(
1549
+ "C18 version-parity control FAILED: equal-strings must match and unequal-strings must be rejected \u2014 verdict untrustworthy"
1550
+ );
1551
+ }
1552
+ if (!tree.packageJson) {
1553
+ const reason = "could not locate package.json anywhere above the scanned root (C-18)";
1554
+ return {
1555
+ id: this.id,
1556
+ anchor: tree.root,
1557
+ state: "could-not-judge",
1558
+ reason,
1559
+ verdict: `could not judge: ${reason}`
1560
+ };
1561
+ }
1562
+ const pkgVersion = tree.packageJson.json.version;
1563
+ if (typeof pkgVersion !== "string") {
1564
+ const reason = `no string "version" field in ${tree.packageJson.path} \u2014 manifest version-parity unverifiable (C-18)`;
1565
+ return {
1566
+ id: this.id,
1567
+ anchor: tree.packageJson.path,
1568
+ state: "could-not-judge",
1569
+ reason,
1570
+ verdict: `could not judge: ${reason}`
1571
+ };
1572
+ }
1573
+ const { kind, path } = manifestPath(tree.root);
1574
+ const manifestName = kind === "extension" ? "extension.json" : "mcp.json";
1575
+ if (!existsSync3(path)) {
1576
+ return {
1577
+ id: this.id,
1578
+ anchor: tree.root,
1579
+ state: "non-conforming",
1580
+ verdict: `no ${manifestName} found at the tree root -> the ${kind === "extension" ? "extension" : "server/App"} manifest is required (C-18)`
1581
+ };
1582
+ }
1583
+ const manifest = readJson(path);
1584
+ if (!manifest) {
1585
+ return {
1586
+ id: this.id,
1587
+ anchor: path,
1588
+ state: "non-conforming",
1589
+ verdict: `${manifestName} present but could not be parsed as JSON -> fix the manifest (C-18)`
1590
+ };
1591
+ }
1592
+ if (typeof manifest.name !== "string" || manifest.name.length === 0) {
1593
+ return {
1594
+ id: this.id,
1595
+ anchor: path,
1596
+ state: "non-conforming",
1597
+ verdict: `${manifestName} is missing a required "name" field (C-18)`
1598
+ };
1599
+ }
1600
+ if (kind === "extension") {
1601
+ const missing = [];
1602
+ if (typeof manifest.type !== "string") missing.push("type");
1603
+ if (typeof manifest.lifecycle !== "string") missing.push("lifecycle");
1604
+ if (missing.length > 0) {
1605
+ return {
1606
+ id: this.id,
1607
+ anchor: path,
1608
+ state: "non-conforming",
1609
+ verdict: `extension.json is missing required field(s): ${missing.join(", ")} -> declare type/lifecycle (C-18)`
1610
+ };
1611
+ }
1612
+ } else {
1613
+ const missing = [];
1614
+ if (!manifest.capabilities || typeof manifest.capabilities !== "object" || Array.isArray(manifest.capabilities)) {
1615
+ missing.push("capabilities");
1616
+ }
1617
+ if (!manifest.transport || typeof manifest.transport !== "object" || Array.isArray(manifest.transport)) {
1618
+ missing.push("transport");
1619
+ }
1620
+ if (missing.length > 0) {
1621
+ return {
1622
+ id: this.id,
1623
+ anchor: path,
1624
+ state: "non-conforming",
1625
+ verdict: `mcp.json is missing required field(s): ${missing.join(", ")} not declared -> declare capabilities/transport (C-18)`
1626
+ };
1627
+ }
1628
+ }
1629
+ if (typeof manifest.version !== "string") {
1630
+ return {
1631
+ id: this.id,
1632
+ anchor: path,
1633
+ state: "non-conforming",
1634
+ verdict: `${manifestName} is missing a required "version" field (C-18)`
1635
+ };
1636
+ }
1637
+ if (manifest.version !== pkgVersion) {
1638
+ return {
1639
+ id: this.id,
1640
+ anchor: path,
1641
+ state: "non-conforming",
1642
+ verdict: `${manifestName} version "${manifest.version}" does not equal package.json version "${pkgVersion}" -> keep the manifest version in sync (C-18)`
1643
+ };
1644
+ }
1645
+ return {
1646
+ id: this.id,
1647
+ anchor: path,
1648
+ state: "present",
1649
+ verdict: `${manifestName} present, required fields declared, version "${manifest.version}" parity with package.json (C-18)`
1650
+ };
1651
+ }
1652
+ };
1653
+
1654
+ // src/scan.ts
1655
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync } from "fs";
1656
+ import { dirname, join as join4, relative } from "path";
1657
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1658
+ "node_modules",
1659
+ "dist",
1660
+ "build",
1661
+ ".git",
1662
+ "tests",
1663
+ "__tests__",
1664
+ "coverage",
1665
+ // Test material, not a target's served surface: deliberately-malformed
1666
+ // servers and foreign copies live here. Scanning them makes the instrument
1667
+ // trip its own fail-closed detectors when pointed at its own tree, and lints
1668
+ // inputs that are meant to be broken. A real target's fixtures are equally
1669
+ // test inputs, not its product surface.
1670
+ "fixtures"
1671
+ ]);
1672
+ function isCommentLine(raw) {
1673
+ const t = raw.trim();
1674
+ if (t === "") return false;
1675
+ return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
1676
+ }
1677
+ function walk(dir, out) {
1678
+ let entries;
1679
+ try {
1680
+ entries = readdirSync2(dir);
1681
+ } catch {
1682
+ return;
1683
+ }
1684
+ for (const name of entries) {
1685
+ const full = join4(dir, name);
1686
+ let st;
1687
+ try {
1688
+ st = statSync(full);
1689
+ } catch {
1690
+ continue;
1691
+ }
1692
+ if (st.isDirectory()) {
1693
+ if (SKIP_DIRS.has(name)) continue;
1694
+ walk(full, out);
1695
+ } else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
1696
+ if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
1697
+ out.push(full);
1698
+ }
1699
+ }
1700
+ }
1701
+ function loadFile(path, root) {
1702
+ const raw = readFileSync4(path, "utf8");
1703
+ const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
1704
+ return { path, rel: relative(root, path) || path, lines };
1705
+ }
1706
+ function findPackageJson(startDir) {
1707
+ let dir = startDir;
1708
+ for (let depth = 0; depth < 8; depth++) {
1709
+ const candidate = join4(dir, "package.json");
1710
+ if (existsSync4(candidate)) {
1711
+ try {
1712
+ return { path: candidate, json: JSON.parse(readFileSync4(candidate, "utf8")) };
1713
+ } catch {
1714
+ return { path: candidate, json: {} };
1715
+ }
1716
+ }
1717
+ const parent = dirname(dir);
1718
+ if (parent === dir) break;
1719
+ dir = parent;
1720
+ }
1721
+ return null;
1722
+ }
1723
+ function loadTree(root) {
1724
+ const st = statSync(root);
1725
+ const baseDir = st.isDirectory() ? root : dirname(root);
1726
+ const filePaths = [];
1727
+ if (st.isDirectory()) walk(root, filePaths);
1728
+ else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
1729
+ const files = filePaths.map((p) => loadFile(p, baseDir));
1730
+ return { root, files, packageJson: findPackageJson(baseDir) };
1731
+ }
1732
+
1733
+ // src/detectors/c19-served-descriptions.ts
1734
+ var PIN6 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1735
+ var TOOL_DESC_INLINE_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(\s*(?:["'`][^"'`]+["'`]|[A-Za-z_$][\w$]*)\s*,\s*["'`]([^"'`]+)["'`]/;
1736
+ var DESCRIPTOR_DESC_RE = /\bdescription\s*:\s*["'`]([^"'`]+)["'`]/;
1737
+ var BARE_STRING_RE = /^\s*["'`]([^"'`]+)["'`]\s*,?\s*$/;
1738
+ var BACKWARD_LOOKBACK = 10;
1739
+ var FORWARD_LOOKAHEAD = 6;
1740
+ function findInlineDescription(tree, siteAnchor, toolName) {
1741
+ const [rel, lineStr] = siteAnchor.split(":");
1742
+ const file = tree.files.find((f) => f.rel === rel);
1743
+ if (!file) return void 0;
1744
+ const siteLine = Number.parseInt(lineStr, 10);
1745
+ const start = Math.max(1, siteLine - BACKWARD_LOOKBACK);
1746
+ const end = Math.min(siteLine + FORWARD_LOOKAHEAD, file.lines.length + 1);
1747
+ for (let n = start; n < end; n++) {
1748
+ const line = file.lines[n - 1];
1749
+ if (!line || line.comment) continue;
1750
+ const inline = line.raw.match(TOOL_DESC_INLINE_RE);
1751
+ if (inline) return inline[1];
1752
+ const desc = line.raw.match(DESCRIPTOR_DESC_RE);
1753
+ if (desc) return desc[1];
1754
+ if (n >= siteLine) {
1755
+ const bare = line.raw.match(BARE_STRING_RE);
1756
+ if (bare && bare[1] !== toolName) return bare[1];
1757
+ }
1758
+ }
1759
+ return void 0;
1760
+ }
1761
+ var TRIGGER_LIST_RE = /use this whenever/i;
1762
+ var TRIGGER_CLOSE_RE = /even if they don'?t say/i;
1763
+ var BILINGUAL_SPLIT_RE = /^(.{10,})\s\/\s(.{10,})$/;
1764
+ function c19PositiveControls() {
1765
+ const inlineSample = 's.tool("list_items", "List items", {}, handler)';
1766
+ const triggerSample = "List items. Use this whenever the user says list, show, or enumerate \u2014 even if they don't say 'list_items' explicitly.";
1767
+ const bilingualSample = "List items, use this whenever asked. / Liste les elements, a utiliser des que demande.";
1768
+ const monoSample = "List items only, no second language here at all.";
1769
+ return {
1770
+ inlineDescAccepts: TOOL_DESC_INLINE_RE.test(inlineSample),
1771
+ triggerListAccepts: TRIGGER_LIST_RE.test(triggerSample),
1772
+ triggerCloseAccepts: TRIGGER_CLOSE_RE.test(triggerSample),
1773
+ bilingualAccepts: BILINGUAL_SPLIT_RE.test(bilingualSample),
1774
+ bilingualRejectsMonolingual: !BILINGUAL_SPLIT_RE.test(monoSample)
1775
+ };
1776
+ }
1777
+ var c19ServedDescriptions = {
1778
+ id: "C19 served-descriptions",
1779
+ rule: "C-19",
1780
+ pin: PIN6,
1781
+ run(tree) {
1782
+ const controls = c19PositiveControls();
1783
+ if (!controls.inlineDescAccepts || !controls.triggerListAccepts || !controls.triggerCloseAccepts || !controls.bilingualAccepts || !controls.bilingualRejectsMonolingual) {
1784
+ throw new Error(
1785
+ `C19 positive control FAILED: ${JSON.stringify(controls)} \u2014 matcher broken, verdict untrustworthy`
1786
+ );
1787
+ }
1788
+ const sites = findRegisteredTools(tree);
1789
+ if (sites.length === 0) {
1790
+ return {
1791
+ id: this.id,
1792
+ anchor: "-",
1793
+ state: "absent",
1794
+ verdict: "no tool registrations found; served-descriptions check n/a (C-19)"
1795
+ };
1796
+ }
1797
+ for (const site of sites) {
1798
+ const desc = findInlineDescription(tree, site.anchor, site.name);
1799
+ if (!desc) {
1800
+ return {
1801
+ id: this.id,
1802
+ anchor: site.anchor,
1803
+ state: "non-conforming",
1804
+ verdict: `tool "${site.name}" at ${site.anchor} declares no inline description in source (positive-control OK) -> declare one; the SERVED bilingual/budget/trigger-phrase measurement is the dynamic half of C-19, run with a live session`
1805
+ };
1806
+ }
1807
+ }
1808
+ return {
1809
+ id: this.id,
1810
+ anchor: sites[0].anchor,
1811
+ state: "present",
1812
+ verdict: `${sites.length} registered tool(s) each declare a source-level description (positive-control OK). Whether the SERVED text is bilingual, under the C-13 per-tool budget, and carries its trigger phrase is the dynamique half of C-19 \u2014 this static run asserts source-level presence only, run with a live session (--mode=dynamic|both) for the served measurement.`
1813
+ };
1814
+ },
1815
+ runDynamic(runtime) {
1816
+ if (runtime.launchError) {
1817
+ const reason = `server could not be launched/introspected: ${runtime.launchError} \u2014 served descriptions unreadable (C-19)`;
1818
+ return {
1819
+ id: this.id,
1820
+ anchor: runtime.root,
1821
+ state: "could-not-judge",
1822
+ reason,
1823
+ verdict: `could not judge: ${reason}`
1824
+ };
1825
+ }
1826
+ if (runtime.tools.length === 0) {
1827
+ return {
1828
+ id: this.id,
1829
+ anchor: runtime.root,
1830
+ state: "absent",
1831
+ verdict: "server serves no tools over tools/list; served-descriptions check n/a (C-19)"
1832
+ };
1833
+ }
1834
+ const ceiling = findCeiling(loadTree(runtime.root));
1835
+ for (const t of runtime.tools) {
1836
+ if (!t.description) {
1837
+ return {
1838
+ id: this.id,
1839
+ anchor: t.name,
1840
+ state: "non-conforming",
1841
+ verdict: `tool "${t.name}" serves NO description over tools/list -> C-19 requires one, in both languages, with its trigger phrase`
1842
+ };
1843
+ }
1844
+ if (ceiling) {
1845
+ const estTokens = Math.ceil(t.description.length / 4);
1846
+ if (estTokens > ceiling.perTool) {
1847
+ return {
1848
+ id: this.id,
1849
+ anchor: t.name,
1850
+ state: "non-conforming",
1851
+ verdict: `tool "${t.name}" serves a description estimated at ${estTokens} tokens, above the declared per-tool ceiling ${ceiling.perTool} (${ceiling.anchor}) -> shorten it (C-19/C-13)`
1852
+ };
1853
+ }
1854
+ }
1855
+ if (!BILINGUAL_SPLIT_RE.test(t.description)) {
1856
+ return {
1857
+ id: this.id,
1858
+ anchor: t.name,
1859
+ state: "non-conforming",
1860
+ verdict: `tool "${t.name}" serves a description with no "<en> / <fr>" bilingual split -> serve both languages (C-19)`
1861
+ };
1862
+ }
1863
+ if (!TRIGGER_LIST_RE.test(t.description) || !TRIGGER_CLOSE_RE.test(t.description)) {
1864
+ return {
1865
+ id: this.id,
1866
+ anchor: t.name,
1867
+ state: "non-conforming",
1868
+ verdict: `tool "${t.name}" serves a description with no trigger-phrase clause ("use this whenever ... even if they don't say") -> add it (C-19)`
1869
+ };
1870
+ }
1871
+ }
1872
+ return {
1873
+ id: this.id,
1874
+ anchor: runtime.tools[0].name,
1875
+ state: "present",
1876
+ verdict: `${runtime.tools.length} served tool description(s) are bilingual, under the declared per-tool budget${ceiling ? ` (${ceiling.perTool})` : " (no ceiling declared)"}, and carry their trigger phrase (C-19). Prose quality beyond this mechanical check remains a human judgment.`
1877
+ };
1878
+ }
1879
+ };
1880
+
1881
+ // src/detectors/c20-locales-derived.ts
1882
+ var I18N_DIR_RE = /^src\/i18n\/([a-z]{2}(?:-[A-Z]{2})?)\.tsx?$/;
1883
+ var I18N_PLUMBING = /* @__PURE__ */ new Set(["index", "types", "locale", "locales"]);
1884
+ function deriveLocales(tree) {
1885
+ const out = [];
1886
+ for (const file of tree.files) {
1887
+ const m = file.rel.match(I18N_DIR_RE);
1888
+ if (m && !I18N_PLUMBING.has(m[1])) {
1889
+ out.push({ code: m[1], anchor: `${file.rel}:1` });
1890
+ }
1891
+ }
1892
+ return out;
1893
+ }
1894
+ var HARDCODED_UNION_RE = /\btype\s+Locale\s*=\s*["'`][a-z]{2}["'`](?:\s*\|\s*["'`][a-z]{2}["'`])+/;
1895
+ var HARDCODED_ARRAY_RE = /\b(?:const|let)\s+(?:SUPPORTED_LOCALES|LOCALES)\s*(?::[^=]+)?=\s*\[\s*["'`][a-z]{2}["'`](?:\s*,\s*["'`][a-z]{2}["'`])+/;
1896
+ var HARDCODED_ENUM_RE = /\benum\s+Locale\s*\{/;
1897
+ function c20PositiveControls() {
1898
+ const unionSample = 'type Locale = "en" | "fr";';
1899
+ const arraySample = 'const SUPPORTED_LOCALES = ["en", "fr"];';
1900
+ const derivedSample = 'const LOCALES = readdirSync(join(__dirname, "i18n")).map((f) => f.replace(/\\.ts$/, ""));';
1901
+ return {
1902
+ unionMatcher: {
1903
+ pattern: HARDCODED_UNION_RE.source,
1904
+ sample: unionSample,
1905
+ matched: HARDCODED_UNION_RE.test(unionSample)
1906
+ },
1907
+ arrayMatcher: {
1908
+ pattern: HARDCODED_ARRAY_RE.source,
1909
+ sample: arraySample,
1910
+ matched: HARDCODED_ARRAY_RE.test(arraySample)
1911
+ },
1912
+ arrayClearsDerivedUsage: !HARDCODED_ARRAY_RE.test(derivedSample)
1913
+ };
1914
+ }
1915
+ function findHardcodedEnumerations(tree) {
1916
+ const hits = [];
1917
+ for (const file of tree.files) {
1918
+ if (I18N_DIR_RE.test(file.rel)) continue;
1919
+ for (const line of file.lines) {
1920
+ if (line.comment) continue;
1921
+ if (HARDCODED_UNION_RE.test(line.raw)) {
1922
+ hits.push({ file: file.rel, line: line.n, kind: "union type" });
1923
+ } else if (HARDCODED_ARRAY_RE.test(line.raw)) {
1924
+ hits.push({ file: file.rel, line: line.n, kind: "literal array" });
1925
+ } else if (HARDCODED_ENUM_RE.test(line.raw)) {
1926
+ hits.push({ file: file.rel, line: line.n, kind: "enum" });
1927
+ }
1928
+ }
1929
+ }
1930
+ return hits;
1931
+ }
1932
+ var c20LocalesDerived = {
1933
+ id: "C20 locales-derived",
1934
+ rule: "C-20",
1935
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
1936
+ run(tree) {
1937
+ const controls = c20PositiveControls();
1938
+ if (!controls.unionMatcher.matched || !controls.arrayMatcher.matched) {
1939
+ throw new Error(
1940
+ `C20 positive control FAILED: union=${controls.unionMatcher.matched} array=${controls.arrayMatcher.matched} \u2014 matcher broken, verdict untrustworthy`
1941
+ );
1942
+ }
1943
+ if (!controls.arrayClearsDerivedUsage) {
1944
+ throw new Error(
1945
+ "C20 array-matcher control FAILED: must NOT flag a genuinely derived (readdirSync-built) locales binding \u2014 verdict untrustworthy"
1946
+ );
1947
+ }
1948
+ const locales = deriveLocales(tree);
1949
+ if (locales.length === 0) {
1950
+ return {
1951
+ id: this.id,
1952
+ anchor: "-",
1953
+ state: "absent",
1954
+ verdict: "no src/i18n/ locale files found; locales-derived check n/a"
1955
+ };
1956
+ }
1957
+ const hits = findHardcodedEnumerations(tree);
1958
+ if (hits.length > 0) {
1959
+ const h = hits[0];
1960
+ return {
1961
+ id: this.id,
1962
+ anchor: `${h.file}:${h.line}`,
1963
+ state: "non-conforming",
1964
+ verdict: `hardcoded locale ${h.kind} at ${h.file}:${h.line}, alongside ${locales.length} locale file(s) derived from src/i18n/ (positive-control OK) -> derive the locale set from src/i18n/, never hand-list it (C-20)`
1965
+ };
1966
+ }
1967
+ return {
1968
+ id: this.id,
1969
+ anchor: locales[0].anchor,
1970
+ state: "present",
1971
+ verdict: `${locales.length} locale(s) derived from src/i18n/ (${locales.map((l) => l.code).join(", ")}), no hardcoded enumeration found (positive-control OK; static half only \u2014 the per-locale served-response check is dynamic and out of scope for this run)`
1972
+ };
1973
+ }
1974
+ };
1975
+
1976
+ // src/detectors/c21-progressive-disclosure.ts
1977
+ var PIN7 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1978
+ var ESTIMATED_BYTES_PER_TOKEN = 4;
1979
+ function c21PositiveControls() {
1980
+ return { ceilingReadable: typeof findCeiling === "function" };
1981
+ }
1982
+ function estimateHandshakeTokens(runtime) {
1983
+ const payload = JSON.stringify(
1984
+ runtime.tools.map((t) => ({ name: t.name, description: t.description ?? "" }))
1985
+ );
1986
+ return Math.ceil(payload.length / ESTIMATED_BYTES_PER_TOKEN);
1987
+ }
1988
+ var c21ProgressiveDisclosure = {
1989
+ id: "C21 progressive-disclosure",
1990
+ rule: "C-21",
1991
+ pin: PIN7,
1992
+ run(_tree) {
1993
+ const controls = c21PositiveControls();
1994
+ if (!controls.ceilingReadable) {
1995
+ throw new Error(
1996
+ "C21 control FAILED: C-13 findCeiling not importable \u2014 verdict untrustworthy"
1997
+ );
1998
+ }
1999
+ return {
2000
+ id: this.id,
2001
+ anchor: "-",
2002
+ state: "absent",
2003
+ verdict: "C-21 measures what actually lands in context at connection (handshake), which a static-tree run cannot read \u2014 connect with a live session (--mode=dynamic|both) to measure it against the C-13 declared ceiling (C-21 n/a on a static-only run)"
2004
+ };
2005
+ },
2006
+ runDynamic(runtime) {
2007
+ if (runtime.launchError) {
2008
+ const reason = `server could not be launched/introspected: ${runtime.launchError} \u2014 connection-load unmeasurable (C-21)`;
2009
+ return {
2010
+ id: this.id,
2011
+ anchor: runtime.root,
2012
+ state: "could-not-judge",
2013
+ reason,
2014
+ verdict: `could not judge: ${reason}`
2015
+ };
2016
+ }
2017
+ const ceiling = findCeiling(loadTree(runtime.root));
2018
+ if (!ceiling) {
2019
+ const reason = "no (global, perTool) context ceiling declared anywhere in the tree (C-13) \u2014 C-21 has nothing to measure against";
2020
+ return {
2021
+ id: this.id,
2022
+ anchor: runtime.root,
2023
+ state: "could-not-judge",
2024
+ reason,
2025
+ verdict: `could not judge: ${reason}`
2026
+ };
2027
+ }
2028
+ const measured = estimateHandshakeTokens(runtime);
2029
+ if (measured > ceiling.global) {
2030
+ return {
2031
+ id: this.id,
2032
+ anchor: ceiling.anchor,
2033
+ state: "non-conforming",
2034
+ verdict: `measured connection load ~${measured} tokens (served tools/list payload) exceeds the declared global ceiling ${ceiling.global} (${ceiling.anchor}) -> stage publication by tiers (C-21, the measured half of C-13)`
2035
+ };
2036
+ }
2037
+ return {
2038
+ id: this.id,
2039
+ anchor: ceiling.anchor,
2040
+ state: "present",
2041
+ verdict: `measured connection load ~${measured} tokens, within the declared global ceiling ${ceiling.global} (${ceiling.anchor}) -> C-13's declaration holds under measurement (C-21)`
2042
+ };
2043
+ }
2044
+ };
2045
+
2046
+ // src/detectors/c22-test-ratio-gate.ts
2047
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
2048
+ import { join as join5 } from "path";
2049
+ var DOC_FILES2 = ["README.md", "README.fr.md", "CHANGELOG.md"];
2050
+ var RATIO_RE = /\b(?:coverage|ratio de tests?|test ratio|couverture)\b[^\n%]{0,40}?(\d{1,3}(?:\.\d+)?\s?%)/i;
2051
+ var IDENTIFIER_RE = /\b(?:run[/#]?\s?\d+|workflow run|gh run|actions\/runs\/\d+|CI#\d+|\b[0-9a-f]{7,40}\b)/i;
2052
+ function c22PositiveControls() {
2053
+ const ratioSample = "Coverage: 92%";
2054
+ const idSample = "Coverage: 92% (CI run https://github.com/x/y/actions/runs/123456)";
2055
+ const bareSample = "Coverage: 92%, measured locally.";
2056
+ return {
2057
+ ratioMatcher: {
2058
+ pattern: RATIO_RE.source,
2059
+ sample: ratioSample,
2060
+ matched: RATIO_RE.test(ratioSample)
2061
+ },
2062
+ identifierMatcher: {
2063
+ pattern: IDENTIFIER_RE.source,
2064
+ sample: idSample,
2065
+ matched: IDENTIFIER_RE.test(idSample)
2066
+ },
2067
+ identifierClearsBareRatio: !IDENTIFIER_RE.test(bareSample)
2068
+ };
2069
+ }
2070
+ function findRatioCitations(root) {
2071
+ const hits = [];
2072
+ for (const name of DOC_FILES2) {
2073
+ const path = join5(root, name);
2074
+ if (!existsSync5(path)) continue;
2075
+ let text;
2076
+ try {
2077
+ text = readFileSync5(path, "utf8");
2078
+ } catch {
2079
+ continue;
2080
+ }
2081
+ const lines = text.split("\n");
2082
+ for (let i = 0; i < lines.length; i++) {
2083
+ const raw = lines[i];
2084
+ if (RATIO_RE.test(raw)) {
2085
+ hits.push({ file: name, line: i + 1, raw, hasIdentifier: IDENTIFIER_RE.test(raw) });
2086
+ }
2087
+ }
2088
+ }
2089
+ return hits;
2090
+ }
2091
+ var c22TestRatioGate = {
2092
+ id: "C22 test-ratio-gate",
2093
+ rule: "C-22",
2094
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
2095
+ run(tree) {
2096
+ const controls = c22PositiveControls();
2097
+ if (!controls.ratioMatcher.matched || !controls.identifierMatcher.matched) {
2098
+ throw new Error(
2099
+ `C22 positive control FAILED: ratio=${controls.ratioMatcher.matched} identifier=${controls.identifierMatcher.matched} \u2014 matcher broken, verdict untrustworthy`
2100
+ );
2101
+ }
2102
+ if (!controls.identifierClearsBareRatio) {
2103
+ throw new Error(
2104
+ "C22 identifier-matcher control FAILED: must NOT flag a bare ratio with no identifier token \u2014 verdict untrustworthy"
2105
+ );
2106
+ }
2107
+ const hits = findRatioCitations(tree.root);
2108
+ if (hits.length === 0) {
2109
+ return {
2110
+ id: this.id,
2111
+ anchor: "-",
2112
+ state: "absent",
2113
+ verdict: "no test/coverage ratio cited in README/CHANGELOG; test-ratio-gate check n/a"
2114
+ };
2115
+ }
2116
+ const bare = hits.find((h) => !h.hasIdentifier);
2117
+ if (bare) {
2118
+ return {
2119
+ id: this.id,
2120
+ anchor: `${bare.file}:${bare.line}`,
2121
+ state: "non-conforming",
2122
+ verdict: `ratio cited at ${bare.file}:${bare.line} ("${bare.raw.trim()}") carries no passage-identifier token (positive-control OK) -> cite the automatic run/commit identifier the ratio was produced by (C-22)`
2123
+ };
2124
+ }
2125
+ const withId = hits[0];
2126
+ const reason = `ratio cited at ${withId.file}:${withId.line} carries an identifier-shaped token, but whether it RESOLVES against a live CI run is the gate half of C-22 \u2014 this static-only run has no CI query surface and cannot confirm resolution`;
2127
+ return {
2128
+ id: this.id,
2129
+ anchor: `${withId.file}:${withId.line}`,
2130
+ state: "could-not-judge",
2131
+ reason,
2132
+ verdict: `could not judge: ${reason}`
2133
+ };
2134
+ }
2135
+ };
2136
+
2137
+ // src/detectors/c25-build-identity.ts
2138
+ var PIN8 = "20136b50";
2139
+ var IDENTITY_KEY_NAMES = [
2140
+ "buildId",
2141
+ "buildID",
2142
+ "buildSha",
2143
+ "buildHash",
2144
+ "commitHash",
2145
+ "commitSha",
2146
+ "gitSha",
2147
+ "gitCommit",
2148
+ "gitRevision"
2149
+ ];
2150
+ var CONSTRUCTOR_RE = /\bnew\s+(?:McpServer|Server)\s*\(/;
2151
+ var IDENTITY_KEY_LINE_RE = new RegExp(
2152
+ `\\b(${IDENTITY_KEY_NAMES.join("|")})\\s*:\\s*(.+?)\\s*,?\\s*$`
2153
+ );
2154
+ var IDENTITY_KEY_BARE_RE = new RegExp(`\\b(${IDENTITY_KEY_NAMES.join("|")})\\s*:\\s*$`);
2155
+ var LITERAL_SAMPLE = 'commitSha: "a1b2c3d",';
2156
+ var DERIVED_SAMPLE = "buildId: process.env.BUILD_SHA,";
2157
+ var FULL_COMMIT_ID = /^[0-9a-f]{40}$/;
2158
+ function isLiteralValue(expr) {
2159
+ const trimmed = expr.trim().replace(/,$/, "").trim();
2160
+ const m = trimmed.match(/^(["'`])([\s\S]*)\1$/);
2161
+ if (!m) return false;
2162
+ if (m[1] === "`" && /\$\{/.test(m[2])) return false;
2163
+ return true;
2164
+ }
2165
+ function c25PositiveControls() {
2166
+ const literalMatch = LITERAL_SAMPLE.match(IDENTITY_KEY_LINE_RE);
2167
+ const derivedMatch = DERIVED_SAMPLE.match(IDENTITY_KEY_LINE_RE);
2168
+ return {
2169
+ literalAccepts: literalMatch ? isLiteralValue(literalMatch[2]) : false,
2170
+ derivedRejectsAsLiteral: derivedMatch ? isLiteralValue(derivedMatch[2]) : false
2171
+ };
2172
+ }
2173
+ function extractConstructorArgs(file, startIdx) {
2174
+ const code = file.lines.filter((l) => !l.comment);
2175
+ const startPos = code.findIndex((l) => l.n === file.lines[startIdx].n);
2176
+ if (startPos < 0) return null;
2177
+ const text = code.slice(startPos).map((l) => l.raw).join("\n");
2178
+ const openIdx = text.indexOf("(");
2179
+ if (openIdx < 0) return null;
2180
+ let depth = 0;
2181
+ let body = "";
2182
+ for (let i = openIdx; i < text.length; i++) {
2183
+ const ch = text[i];
2184
+ if (ch === "(") {
2185
+ depth++;
2186
+ if (depth === 1) continue;
2187
+ } else if (ch === ")") {
2188
+ depth--;
2189
+ if (depth === 0) return body;
2190
+ }
2191
+ if (depth >= 1) body += ch;
2192
+ }
2193
+ return null;
2194
+ }
2195
+ function findConstructorSites(tree) {
2196
+ const sites = [];
2197
+ for (const file of tree.files) {
2198
+ file.lines.forEach((line, idx) => {
2199
+ if (line.comment) return;
2200
+ if (CONSTRUCTOR_RE.test(line.raw)) {
2201
+ sites.push({
2202
+ anchor: `${file.rel}:${line.n}`,
2203
+ args: extractConstructorArgs(file, idx)
2204
+ });
2205
+ }
2206
+ });
2207
+ }
2208
+ return sites;
2209
+ }
2210
+ function classifyArgs(args, anchor) {
2211
+ for (const raw of args.split("\n")) {
2212
+ const bare = raw.match(IDENTITY_KEY_BARE_RE);
2213
+ if (bare) return { kind: "bare-continuation", key: bare[1], anchor };
2214
+ const m = raw.match(IDENTITY_KEY_LINE_RE);
2215
+ if (m) {
2216
+ const [, key, value] = m;
2217
+ if (isLiteralValue(value)) return { kind: "literal", key, value: value.trim(), anchor };
2218
+ return { kind: "derived", key, anchor };
2219
+ }
2220
+ }
2221
+ return { kind: "none" };
2222
+ }
2223
+ var c25BuildIdentity = {
2224
+ id: "C25 build-identity",
2225
+ rule: "C-25",
2226
+ pin: PIN8,
2227
+ run(tree) {
2228
+ const controls = c25PositiveControls();
2229
+ if (!controls.literalAccepts || controls.derivedRejectsAsLiteral) {
2230
+ throw new Error(
2231
+ `C25 literal/derived control FAILED: ${JSON.stringify(controls)} \u2014 matcher broken, verdict untrustworthy`
2232
+ );
2233
+ }
2234
+ const sites = findConstructorSites(tree);
2235
+ if (sites.length === 0) {
2236
+ return {
2237
+ id: this.id,
2238
+ anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
2239
+ state: "absent",
2240
+ verdict: "no `new McpServer(...)`/`new Server(...)` construction found; C-25 n/a"
2241
+ };
2242
+ }
2243
+ for (const site of sites) {
2244
+ if (site.args === null) {
2245
+ const reason = `constructor call at ${site.anchor} could not be delimited (unbalanced/EOF) \u2014 the build-identity field could not be searched over its full argument object, scope unestablished`;
2246
+ return {
2247
+ id: this.id,
2248
+ anchor: site.anchor,
2249
+ state: "could-not-judge",
2250
+ reason,
2251
+ verdict: `could not judge: ${reason}`
2252
+ };
2253
+ }
2254
+ }
2255
+ for (const site of sites) {
2256
+ const finding = classifyArgs(site.args, site.anchor);
2257
+ if (finding.kind === "bare-continuation") {
2258
+ const reason = `identity key "${finding.key}" found at ${finding.anchor} but its value continues past the line (not statically joined) \u2014 verdict unreadable`;
2259
+ return {
2260
+ id: this.id,
2261
+ anchor: finding.anchor,
2262
+ state: "could-not-judge",
2263
+ reason,
2264
+ verdict: `could not judge: ${reason}`
2265
+ };
2266
+ }
2267
+ if (finding.kind === "derived") {
2268
+ return {
2269
+ id: this.id,
2270
+ anchor: finding.anchor,
2271
+ state: "present",
2272
+ verdict: `build-identity field "${finding.key}" at ${finding.anchor} is fed by the build (not a literal) \u2014 served alongside version (C-25). Whether the SERVED value equals the expected commit is the dynamique half, run with a live session.`
2273
+ };
2274
+ }
2275
+ if (finding.kind === "literal") {
2276
+ return {
2277
+ id: this.id,
2278
+ anchor: finding.anchor,
2279
+ state: "non-conforming",
2280
+ verdict: `build-identity field "${finding.key}" at ${finding.anchor} is a hand-written literal (${finding.value}) -> a literal never changes on its own and proves nothing about the tree actually served; derive it from the build (env var injected at build time, or a generated file) (C-25)`
2281
+ };
2282
+ }
2283
+ }
2284
+ return {
2285
+ id: this.id,
2286
+ anchor: sites[0].anchor,
2287
+ state: "non-conforming",
2288
+ verdict: `server construction at ${sites[0].anchor} declares no build-identity field (${IDENTITY_KEY_NAMES.join("/")}) alongside version -> a version is a DECLARED value that only changes if someone remembers to bump it; add a build identity derived from the tree actually built (C-25)`
2289
+ };
2290
+ },
2291
+ runDynamic(runtime) {
2292
+ if (runtime.launchError) {
2293
+ const reason = `server could not be launched/introspected: ${runtime.launchError} \u2014 served build-identity unreadable (C-25)`;
2294
+ return {
2295
+ id: this.id,
2296
+ anchor: runtime.root,
2297
+ state: "could-not-judge",
2298
+ reason,
2299
+ verdict: `could not judge: ${reason}`
2300
+ };
2301
+ }
2302
+ const serverInfo = runtime.serverInfo ?? {};
2303
+ let found = null;
2304
+ for (const key of IDENTITY_KEY_NAMES) {
2305
+ const v = serverInfo[key];
2306
+ if (v !== void 0 && v !== null && String(v).trim() !== "") {
2307
+ found = { key, value: String(v) };
2308
+ break;
2309
+ }
2310
+ }
2311
+ if (!found) {
2312
+ const declaredVersion = typeof serverInfo.version === "string" ? serverInfo.version : "?";
2313
+ return {
2314
+ id: this.id,
2315
+ anchor: runtime.root,
2316
+ state: "non-conforming",
2317
+ verdict: `server serves no build-identity field (${IDENTITY_KEY_NAMES.join("/")}) over initialize's serverInfo \u2014 only version="${declaredVersion}" -> a declared version proves nothing about the tree served; C-25 requires one`
2318
+ };
2319
+ }
2320
+ if (!runtime.expectedCommit) {
2321
+ const reason = `identity "${found.key}"="${found.value}" served, but no expected commit was supplied to compare against (target is not a resolvable git checkout, or HEAD did not resolve) \u2014 the static verdict stands; the served-vs-expected comparison is OWED, not a pass`;
2322
+ return {
2323
+ id: this.id,
2324
+ anchor: runtime.root,
2325
+ state: "could-not-judge",
2326
+ reason,
2327
+ verdict: `could not judge: ${reason}`
2328
+ };
2329
+ }
2330
+ const served = found.value.trim().toLowerCase();
2331
+ const expected = runtime.expectedCommit.trim().toLowerCase();
2332
+ if (!FULL_COMMIT_ID.test(expected)) {
2333
+ const reason = `expected commit "${runtime.expectedCommit}" is not a full 40-hex commit id \u2014 the served-vs-expected comparison reasons about length and is only sound against a full id (see deriveExpectedCommit in src/runtime/launch.ts); refusing to judge rather than comparing against a value outside the contract`;
2334
+ return {
2335
+ id: this.id,
2336
+ anchor: runtime.root,
2337
+ state: "could-not-judge",
2338
+ reason,
2339
+ verdict: `could not judge: ${reason}`
2340
+ };
2341
+ }
2342
+ const ABBREVIATED_IDENTITY_FLOOR = 7;
2343
+ if (served !== expected && served.length < ABBREVIATED_IDENTITY_FLOOR) {
2344
+ const reason = `served build-identity "${found.key}"="${found.value}" is ${served.length} character(s) long \u2014 below the ${ABBREVIATED_IDENTITY_FLOOR}-character floor under which an abbreviated commit cannot be told from a coincidence (one hex character matches one expected commit in sixteen); the served-vs-expected comparison against "${runtime.expectedCommit}" is UNDECIDABLE, not a pass`;
2345
+ return {
2346
+ id: this.id,
2347
+ anchor: runtime.root,
2348
+ state: "could-not-judge",
2349
+ reason,
2350
+ verdict: `could not judge: ${reason}`
2351
+ };
2352
+ }
2353
+ if (served.length > expected.length && served.startsWith(expected)) {
2354
+ const suffix = found.value.trim().slice(expected.length);
2355
+ return {
2356
+ id: this.id,
2357
+ anchor: runtime.root,
2358
+ state: "non-conforming",
2359
+ verdict: `served build-identity "${found.key}"="${found.value}" is the expected commit "${runtime.expectedCommit}" DECORATED with "${suffix}" -> a full commit id carries no suffix, and the suffixes that occur in practice (-dirty, +local) state that the tree served is NOT that commit; the running server is not serving the tree it claims to (C-25)`
2360
+ };
2361
+ }
2362
+ const matches = served === expected || served.length < expected.length && expected.startsWith(served);
2363
+ if (!matches) {
2364
+ return {
2365
+ id: this.id,
2366
+ anchor: runtime.root,
2367
+ state: "non-conforming",
2368
+ verdict: `served build-identity "${found.key}"="${found.value}" DIFFERS from the expected commit "${runtime.expectedCommit}" -> the running server is not serving the tree it claims to (C-25)`
2369
+ };
2370
+ }
2371
+ return {
2372
+ id: this.id,
2373
+ anchor: runtime.root,
2374
+ state: "present",
2375
+ verdict: `served build-identity "${found.key}"="${found.value}" equals the expected commit "${runtime.expectedCommit}" (C-25)`
2376
+ };
2377
+ }
2378
+ };
2379
+
2380
+ // src/detectors/c26-subpackage-lockfile.ts
2381
+ import { spawnSync } from "child_process";
2382
+ import {
2383
+ existsSync as existsSync6,
2384
+ mkdtempSync,
2385
+ readFileSync as readFileSync6,
2386
+ readdirSync as readdirSync3,
2387
+ rmSync,
2388
+ statSync as statSync2,
2389
+ writeFileSync
2390
+ } from "fs";
2391
+ import { tmpdir } from "os";
2392
+ import { join as join6, relative as relative2 } from "path";
2393
+ var PIN9 = "20136b50";
2394
+ var DEPLOY_CONFIG_FILES = [
2395
+ "railway.json",
2396
+ "railway.toml",
2397
+ "Dockerfile",
2398
+ "fly.toml",
2399
+ "Procfile",
2400
+ "vercel.json"
2401
+ ];
2402
+ var LOCKFILES2 = [
2403
+ { name: "package-lock.json", command: "npm", args: ["ci"] },
2404
+ { name: "bun.lock", command: "bun", args: ["install", "--frozen-lockfile"] },
2405
+ { name: "bun.lockb", command: "bun", args: ["install", "--frozen-lockfile"] },
2406
+ { name: "pnpm-lock.yaml", command: "pnpm", args: ["install", "--frozen-lockfile"] },
2407
+ { name: "yarn.lock", command: "yarn", args: ["install", "--frozen-lockfile"] }
2408
+ ];
2409
+ var SKIP_WALK_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage"]);
2410
+ function walkForConfigs(dir, depth, seen) {
2411
+ if (depth > 8) return;
2412
+ let entries;
2413
+ try {
2414
+ entries = readdirSync3(dir);
2415
+ } catch {
2416
+ return;
2417
+ }
2418
+ for (const name of entries) {
2419
+ const full = join6(dir, name);
2420
+ let st;
2421
+ try {
2422
+ st = statSync2(full);
2423
+ } catch {
2424
+ continue;
2425
+ }
2426
+ if (st.isDirectory()) {
2427
+ if (SKIP_WALK_DIRS.has(name)) continue;
2428
+ walkForConfigs(full, depth + 1, seen);
2429
+ } else if (DEPLOY_CONFIG_FILES.includes(name)) {
2430
+ if (!seen.has(dir)) {
2431
+ seen.set(dir, { dir, configFile: name, configPath: full });
2432
+ }
2433
+ }
2434
+ }
2435
+ }
2436
+ function resolveRailwayRootDirectory(configPath) {
2437
+ let text;
2438
+ try {
2439
+ text = readFileSync6(configPath, "utf8");
2440
+ } catch (err) {
2441
+ return {
2442
+ error: `could not read ${configPath}: ${err instanceof Error ? err.message : String(err)}`
2443
+ };
2444
+ }
2445
+ let parsed;
2446
+ try {
2447
+ parsed = JSON.parse(text);
2448
+ } catch (err) {
2449
+ return {
2450
+ error: `could not parse ${configPath} as JSON: ${err instanceof Error ? err.message : String(err)}`
2451
+ };
2452
+ }
2453
+ const build = parsed.build;
2454
+ const rootDirectory = typeof parsed.rootDirectory === "string" && parsed.rootDirectory || build && typeof build.rootDirectory === "string" && build.rootDirectory || null;
2455
+ return rootDirectory;
2456
+ }
2457
+ function c26DeriveScope(root) {
2458
+ const seen = /* @__PURE__ */ new Map();
2459
+ walkForConfigs(root, 0, seen);
2460
+ const sites = [];
2461
+ for (const site of seen.values()) {
2462
+ if (site.configFile === "railway.json") {
2463
+ const resolved = resolveRailwayRootDirectory(site.configPath);
2464
+ if (resolved && typeof resolved === "object" && "error" in resolved) {
2465
+ return { sites: [], unparsable: { path: site.configPath, reason: resolved.error } };
2466
+ }
2467
+ if (typeof resolved === "string" && resolved.trim()) {
2468
+ sites.push({ ...site, dir: join6(site.dir, resolved) });
2469
+ continue;
2470
+ }
2471
+ }
2472
+ if (site.configFile === "vercel.json") {
2473
+ try {
2474
+ JSON.parse(readFileSync6(site.configPath, "utf8"));
2475
+ } catch (err) {
2476
+ return {
2477
+ sites: [],
2478
+ unparsable: {
2479
+ path: site.configPath,
2480
+ reason: `could not parse ${site.configPath} as JSON: ${err instanceof Error ? err.message : String(err)}`
2481
+ }
2482
+ };
2483
+ }
2484
+ }
2485
+ sites.push(site);
2486
+ }
2487
+ return { sites };
2488
+ }
2489
+ function findLockfile2(dir) {
2490
+ for (const l of LOCKFILES2) {
2491
+ if (existsSync6(join6(dir, l.name))) return l;
2492
+ }
2493
+ return null;
2494
+ }
2495
+ var NPM_INVALID_RE = /Invalid: lock file's (@?[^\s@]+(?:\/[^\s@]+)?)@(\S+) does not satisfy \1@(\S+)/;
2496
+ var NPM_MISSING_RE = /Missing: (@?[^\s@]+(?:\/[^\s@]+)?)@(\S+) from lock file/;
2497
+ function parseLenientJson(text) {
2498
+ return JSON.parse(text.replace(/,(\s*[\]}])/g, "$1"));
2499
+ }
2500
+ function nameBunDrift(manifestText, lockText) {
2501
+ let manifest;
2502
+ let lock;
2503
+ try {
2504
+ manifest = JSON.parse(manifestText);
2505
+ lock = parseLenientJson(lockText);
2506
+ } catch {
2507
+ return [];
2508
+ }
2509
+ const manifestDeps = {
2510
+ ...manifest.dependencies,
2511
+ ...manifest.devDependencies
2512
+ };
2513
+ const workspaces = lock.workspaces;
2514
+ const root = workspaces?.[""];
2515
+ const lockDeps = {
2516
+ ...root?.dependencies,
2517
+ ...root?.devDependencies
2518
+ };
2519
+ const drifted = [];
2520
+ for (const [name, range] of Object.entries(manifestDeps)) {
2521
+ const lockedRange = lockDeps[name];
2522
+ if (lockedRange !== void 0 && lockedRange !== range) {
2523
+ drifted.push(`${name} (package.json wants ${range}, bun.lock recorded ${lockedRange})`);
2524
+ }
2525
+ }
2526
+ return drifted;
2527
+ }
2528
+ function extractGenericToken(stderr) {
2529
+ const m = stderr.match(/(@?[\w.-]+(?:\/[\w.-]+)?)@[\w.^~><=|| -]+/);
2530
+ return m ? m[1] : null;
2531
+ }
2532
+ function nameDrift(lockfileName, siteDir, isolatedDir, stderr) {
2533
+ if (lockfileName === "package-lock.json") {
2534
+ const invalid = stderr.match(NPM_INVALID_RE);
2535
+ if (invalid)
2536
+ return `${invalid[1]} (lockfile has ${invalid[2]}, package.json wants ${invalid[3]})`;
2537
+ const missing = stderr.match(NPM_MISSING_RE);
2538
+ if (missing) return `${missing[1]}@${missing[2]} missing from lock file`;
2539
+ }
2540
+ if (lockfileName === "bun.lock" || lockfileName === "bun.lockb") {
2541
+ try {
2542
+ const manifestText = readFileSync6(join6(isolatedDir, "package.json"), "utf8");
2543
+ const lockText = readFileSync6(join6(isolatedDir, lockfileName), "utf8");
2544
+ const drifted = nameBunDrift(manifestText, lockText);
2545
+ if (drifted.length > 0) return drifted.join(", ");
2546
+ } catch {
2547
+ }
2548
+ }
2549
+ const generic = extractGenericToken(stderr);
2550
+ if (generic)
2551
+ return `${generic} (name extracted from raw install output, see verdict for the full line)`;
2552
+ return `dependency name not statically extractable from this manager's output at ${siteDir} \u2014 raw: ${stderr.slice(0, 300).trim()}`;
2553
+ }
2554
+ function gateSite(site) {
2555
+ const pkgPath = join6(site.dir, "package.json");
2556
+ if (!existsSync6(pkgPath)) {
2557
+ return {
2558
+ site,
2559
+ status: "skipped",
2560
+ reason: `no package.json at ${site.dir} \u2014 not an npm-managed deployable subpackage, C-26 n/a for this site`
2561
+ };
2562
+ }
2563
+ const lock = findLockfile2(site.dir);
2564
+ if (!lock) {
2565
+ return {
2566
+ site,
2567
+ status: "analysed",
2568
+ ok: false,
2569
+ detail: `no lockfile (${LOCKFILES2.map((l) => l.name).join("/")}) present at ${site.dir} while its deployment (${site.configFile}) requires one`
2570
+ };
2571
+ }
2572
+ const isolated = mkdtempSync(join6(tmpdir(), "mcp-doctor-c26-"));
2573
+ try {
2574
+ const manifestText = readFileSync6(pkgPath, "utf8");
2575
+ const lockText = readFileSync6(join6(site.dir, lock.name), "utf8");
2576
+ writeFileSync(join6(isolated, "package.json"), manifestText);
2577
+ writeFileSync(join6(isolated, lock.name), lockText);
2578
+ let proc;
2579
+ try {
2580
+ proc = spawnSync(lock.command, lock.args, {
2581
+ cwd: isolated,
2582
+ encoding: "utf8",
2583
+ timeout: 6e4
2584
+ });
2585
+ } catch (err) {
2586
+ return {
2587
+ site,
2588
+ status: "analysed",
2589
+ ok: false,
2590
+ detail: `could not run "${lock.command} ${lock.args.join(" ")}" in isolation: ${err instanceof Error ? err.message : String(err)}`
2591
+ };
2592
+ }
2593
+ if (proc.error) {
2594
+ return {
2595
+ site,
2596
+ status: "analysed",
2597
+ ok: false,
2598
+ detail: `"${lock.command}" is not available to run the frozen install in isolation: ${proc.error.message}`
2599
+ };
2600
+ }
2601
+ if (proc.status !== 0) {
2602
+ const stderr = (proc.stderr ?? "") + (proc.stdout ?? "");
2603
+ const dep = nameDrift(lock.name, site.dir, isolated, stderr);
2604
+ return {
2605
+ site,
2606
+ status: "analysed",
2607
+ ok: false,
2608
+ detail: `frozen install (${lock.command} ${lock.args.join(" ")}) FAILED in isolation at ${site.dir} -> drifted dependency: ${dep}`
2609
+ };
2610
+ }
2611
+ return {
2612
+ site,
2613
+ status: "analysed",
2614
+ ok: true,
2615
+ detail: `frozen install (${lock.command} ${lock.args.join(" ")}) succeeded in isolation for ${site.dir}`
2616
+ };
2617
+ } finally {
2618
+ rmSync(isolated, { recursive: true, force: true });
2619
+ }
2620
+ }
2621
+ function renderCoverage(scope, verdicts) {
2622
+ if (verdicts.length !== scope.length) {
2623
+ throw new Error(
2624
+ `mcp-doctor: C26 coverage inventory covers ${verdicts.length} of ${scope.length} derived sites \u2014 the run is not whole and will not report as if it were.`
2625
+ );
2626
+ }
2627
+ const covered = new Set(verdicts.map((v) => v.site.dir));
2628
+ for (const s of scope) {
2629
+ if (!covered.has(s.dir)) {
2630
+ throw new Error(`mcp-doctor: C26 coverage inventory omits derived site ${s.dir}.`);
2631
+ }
2632
+ }
2633
+ return verdicts.map((v) => {
2634
+ const rel = relative2(process.cwd(), v.site.dir) || v.site.dir;
2635
+ if (v.status === "skipped") return `SKIPPED ${rel} (${v.reason})`;
2636
+ return `ANALYSED ${rel} -> ${v.ok ? "GREEN" : "RED"}: ${v.detail}`;
2637
+ }).join(" | ");
2638
+ }
2639
+ var c26SubpackageLockfile = {
2640
+ id: "C26 subpackage-lockfile",
2641
+ rule: "C-26",
2642
+ pin: PIN9,
2643
+ run(tree) {
2644
+ const scope = c26DeriveScope(tree.root);
2645
+ if (scope.unparsable) {
2646
+ const reason = `deployment configuration ${scope.unparsable.path} could not be parsed: ${scope.unparsable.reason} \u2014 scope unestablished, C-26 cannot derive its targets`;
2647
+ return {
2648
+ id: this.id,
2649
+ anchor: scope.unparsable.path,
2650
+ state: "could-not-judge",
2651
+ reason,
2652
+ verdict: `could not judge: ${reason}`
2653
+ };
2654
+ }
2655
+ if (scope.sites.length === 0) {
2656
+ return {
2657
+ id: this.id,
2658
+ anchor: tree.root,
2659
+ state: "absent",
2660
+ verdict: `no supported deployment configuration (${DEPLOY_CONFIG_FILES.join("/")}) found; C-26 n/a`
2661
+ };
2662
+ }
2663
+ const verdicts = scope.sites.map(gateSite);
2664
+ const inventory = renderCoverage(scope.sites, verdicts);
2665
+ const analysed = verdicts.filter((v) => v.status === "analysed");
2666
+ if (analysed.length === 0) {
2667
+ return {
2668
+ id: this.id,
2669
+ anchor: tree.root,
2670
+ state: "absent",
2671
+ verdict: `deployment configuration(s) found but every targeted site is SKIPPED (none is npm-managed); C-26 n/a. Coverage: ${inventory}`
2672
+ };
2673
+ }
2674
+ const reds = analysed.filter((v) => v.ok === false);
2675
+ if (reds.length > 0) {
2676
+ return {
2677
+ id: this.id,
2678
+ anchor: reds[0].site.dir,
2679
+ state: "non-conforming",
2680
+ verdict: reds.some((r) => r.detail?.includes("no lockfile")) ? `${reds[0].detail} (C-26). Coverage: ${inventory}` : `${reds.map((r) => r.detail).join(" ; ")} (C-26). Coverage: ${inventory}`
2681
+ };
2682
+ }
2683
+ return {
2684
+ id: this.id,
2685
+ anchor: analysed[0].site.dir,
2686
+ state: "present",
2687
+ verdict: `every analysed deployable subpackage's lockfile is consistent, verified by a frozen install run in the SAME isolation the deployment uses (C-26). Coverage: ${inventory}`
2688
+ };
2689
+ }
2690
+ };
2691
+
2692
+ // src/detectors/d2.ts
2693
+ var SESSION_RE = /mcp-session-id/i;
2694
+ var d2 = {
2695
+ id: "D2 stateless",
2696
+ run(tree) {
2697
+ const hits = [];
2698
+ for (const file of tree.files) {
2699
+ for (const line of file.lines) {
2700
+ if (line.comment) continue;
2701
+ if (SESSION_RE.test(line.raw)) hits.push({ file: file.rel, line: line.n });
2702
+ }
2703
+ }
2704
+ if (hits.length > 0) {
2705
+ const anchor = `${hits[0].file}:${hits.map((h) => h.line).join(",")}`;
2706
+ return {
2707
+ id: this.id,
2708
+ anchor,
2709
+ state: "non-conforming",
2710
+ verdict: "session-keyed (mcp-session-id header) -> migrate to stateless"
2711
+ };
2712
+ }
2713
+ return {
2714
+ id: this.id,
2715
+ anchor: "-",
2716
+ state: "present",
2717
+ verdict: "stateless (no mcp-session-id session-keying)"
2718
+ };
2719
+ }
2720
+ };
245
2721
 
246
2722
  // src/detectors/d3.ts
247
2723
  var RESULT_TYPE_RE = /\bresultType\b/;
@@ -487,8 +2963,8 @@ var d7 = {
487
2963
  };
488
2964
 
489
2965
  // src/detectors/d8.ts
490
- import { existsSync, readFileSync, statSync } from "fs";
491
- import { dirname, join, relative } from "path";
2966
+ import { existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
2967
+ import { dirname as dirname2, join as join7, relative as relative3 } from "path";
492
2968
  var POSITIVE_CONTROL2 = 'server.registerTool("list_items", { ... }, handler);';
493
2969
  function d8PositiveControl() {
494
2970
  return {
@@ -498,9 +2974,9 @@ function d8PositiveControl() {
498
2974
  };
499
2975
  }
500
2976
  function resolveBaseDir(tree) {
501
- if (tree.packageJson) return dirname(tree.packageJson.path);
2977
+ if (tree.packageJson) return dirname2(tree.packageJson.path);
502
2978
  try {
503
- return statSync(tree.root).isDirectory() ? tree.root : dirname(tree.root);
2979
+ return statSync3(tree.root).isDirectory() ? tree.root : dirname2(tree.root);
504
2980
  } catch {
505
2981
  return tree.root;
506
2982
  }
@@ -508,9 +2984,9 @@ function resolveBaseDir(tree) {
508
2984
  function findEvalsFile(tree) {
509
2985
  let dir = resolveBaseDir(tree);
510
2986
  for (let depth = 0; depth < 8; depth++) {
511
- const candidate = join(dir, "evals", "evals.json");
512
- if (existsSync(candidate)) return candidate;
513
- const parent = dirname(dir);
2987
+ const candidate = join7(dir, "evals", "evals.json");
2988
+ if (existsSync7(candidate)) return candidate;
2989
+ const parent = dirname2(dir);
514
2990
  if (parent === dir) break;
515
2991
  dir = parent;
516
2992
  }
@@ -518,6 +2994,8 @@ function findEvalsFile(tree) {
518
2994
  }
519
2995
  var d8 = {
520
2996
  id: "D8 eval-suite-coverage",
2997
+ rule: "C-5",
2998
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
521
2999
  run(tree) {
522
3000
  const control = d8PositiveControl();
523
3001
  if (!control.matched) {
@@ -555,10 +3033,10 @@ var d8 = {
555
3033
  };
556
3034
  }
557
3035
  const baseDir = resolveBaseDir(tree);
558
- const displayPath = relative(baseDir, evalsPath) || evalsPath;
3036
+ const displayPath = relative3(baseDir, evalsPath) || evalsPath;
559
3037
  let cases;
560
3038
  try {
561
- const raw = readFileSync(evalsPath, "utf8");
3039
+ const raw = readFileSync7(evalsPath, "utf8");
562
3040
  const parsed = JSON.parse(raw);
563
3041
  if (!Array.isArray(parsed.cases)) {
564
3042
  throw new Error("`cases` is not an array");
@@ -642,8 +3120,8 @@ var d8 = {
642
3120
  };
643
3121
 
644
3122
  // src/detectors/d9.ts
645
- import { existsSync as existsSync2 } from "fs";
646
- import { dirname as dirname2, join as join2 } from "path";
3123
+ import { existsSync as existsSync8 } from "fs";
3124
+ import { dirname as dirname3, join as join8 } from "path";
647
3125
  var SEMVER_RE = /^(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-]+)*))?$/;
648
3126
  var POSITIVE_CONTROL3 = "1.2.3-beta.1+build.7";
649
3127
  function d9PositiveControl() {
@@ -655,6 +3133,8 @@ function d9PositiveControl() {
655
3133
  }
656
3134
  var d9 = {
657
3135
  id: "D9 semver+changelog",
3136
+ rule: "C-3",
3137
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
658
3138
  run(tree) {
659
3139
  const control = d9PositiveControl();
660
3140
  if (!control.matched) {
@@ -663,9 +3143,14 @@ var d9 = {
663
3143
  );
664
3144
  }
665
3145
  if (!tree.packageJson) {
666
- throw new Error(
667
- "D9 could not locate package.json anywhere above the scanned root \u2014 cannot verify SemVer/CHANGELOG"
668
- );
3146
+ const reason = "no package.json found anywhere above the scanned root \u2014 cannot verify SemVer/CHANGELOG";
3147
+ return {
3148
+ id: this.id,
3149
+ anchor: "package.json",
3150
+ state: "could-not-judge",
3151
+ reason,
3152
+ verdict: `could not judge: ${reason}`
3153
+ };
669
3154
  }
670
3155
  const { path: pkgPath, json: pkgJson } = tree.packageJson;
671
3156
  if (!pkgJson || Object.keys(pkgJson).length === 0) {
@@ -680,9 +3165,9 @@ var d9 = {
680
3165
  verdict: `no "version" field in ${pkgPath.split("/").pop()} -> set a valid SemVer version`
681
3166
  };
682
3167
  }
683
- const projectDir = dirname2(pkgPath);
684
- const changelogPath = join2(projectDir, "CHANGELOG.md");
685
- const hasChangelog = existsSync2(changelogPath);
3168
+ const projectDir = dirname3(pkgPath);
3169
+ const changelogPath = join8(projectDir, "CHANGELOG.md");
3170
+ const hasChangelog = existsSync8(changelogPath);
686
3171
  const validSemver = SEMVER_RE.test(version);
687
3172
  if (validSemver && hasChangelog) {
688
3173
  return {
@@ -710,8 +3195,8 @@ var d9 = {
710
3195
  };
711
3196
 
712
3197
  // src/detectors/d10.ts
713
- import { existsSync as existsSync3 } from "fs";
714
- import { dirname as dirname3, join as join3 } from "path";
3198
+ import { existsSync as existsSync9 } from "fs";
3199
+ import { dirname as dirname4, join as join9 } from "path";
715
3200
  var DESCRIPTION_FR_RE = /"description_fr"\s*:\s*"[^"]+"/;
716
3201
  var POSITIVE_CONTROL4 = '"description_fr": "Un serveur MCP conforme.",';
717
3202
  function d10PositiveControl() {
@@ -761,6 +3246,8 @@ function scanSourceDescriptionFr(tree) {
761
3246
  }
762
3247
  var d10 = {
763
3248
  id: "D10 bilingual",
3249
+ rule: "C-1",
3250
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
764
3251
  run(tree) {
765
3252
  const control = d10PositiveControl();
766
3253
  if (!control.matched) {
@@ -784,7 +3271,7 @@ var d10 = {
784
3271
  };
785
3272
  }
786
3273
  const { path: pkgPath, json: pkg } = tree.packageJson;
787
- const root = dirname3(pkgPath);
3274
+ const root = dirname4(pkgPath);
788
3275
  const descriptionFr = typeof pkg.description_fr === "string" ? pkg.description_fr.trim() : "";
789
3276
  const hasDescriptionFrJson = descriptionFr.length > 0;
790
3277
  const sourceScan = scanSourceDescriptionFr(tree);
@@ -798,8 +3285,8 @@ var d10 = {
798
3285
  };
799
3286
  }
800
3287
  const hasDescriptionFr = hasDescriptionFrJson || sourceScan.present;
801
- const readmeFrPath = join3(root, "README.fr.md");
802
- const hasReadmeFr = existsSync3(readmeFrPath);
3288
+ const readmeFrPath = join9(root, "README.fr.md");
3289
+ const hasReadmeFr = existsSync9(readmeFrPath);
803
3290
  const missing = [];
804
3291
  if (!hasDescriptionFr) missing.push("description_fr (package.json or source _meta)");
805
3292
  if (!hasReadmeFr) missing.push("README.fr.md");
@@ -807,8 +3294,8 @@ var d10 = {
807
3294
  return {
808
3295
  id: this.id,
809
3296
  anchor: `${pkgPath}`,
810
- state: "absent",
811
- verdict: `bilingual release missing: ${missing.join(", ")} -> no release without FR description + README.fr.md`
3297
+ state: "non-conforming",
3298
+ verdict: `bilingual release missing: ${missing.join(", ")} -> no release without FR description + README.fr.md (C-1)`
812
3299
  };
813
3300
  }
814
3301
  const files = Array.isArray(pkg.files) ? pkg.files : null;
@@ -857,6 +3344,8 @@ function d11PositiveControls() {
857
3344
  }
858
3345
  var d11 = {
859
3346
  id: "D11 zod-schemas",
3347
+ rule: "C-4",
3348
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
860
3349
  run(tree) {
861
3350
  const controls = d11PositiveControls();
862
3351
  const failed = Object.entries(controls).find(([, c]) => !c.matched);
@@ -908,8 +3397,8 @@ var d11 = {
908
3397
  return {
909
3398
  id: this.id,
910
3399
  anchor: firstToolAnchor ?? "-",
911
- state: "absent",
912
- verdict: `${toolCount} tool registration(s) found, 0 outputSchema in tree (positive-control OK) -> add output Zod schemas (Server Rule 4; tree-wide proxy, not per-tool \u2014 see D11 scope note)`
3400
+ state: "non-conforming",
3401
+ verdict: `${toolCount} tool registration(s) found, 0 outputSchema in tree (positive-control OK) -> add output Zod schemas (C-4; tree-wide proxy, not per-tool \u2014 see D11 scope note)`
913
3402
  };
914
3403
  }
915
3404
  return {
@@ -1178,8 +3667,14 @@ var d15 = {
1178
3667
  };
1179
3668
 
1180
3669
  // src/detectors/d16.ts
3670
+ import { existsSync as existsSync10, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
3671
+ import { dirname as dirname5, join as join10 } from "path";
1181
3672
  var DISCOVER_RE2 = /server\/discover|DiscoverResult|ServerDiscover|handleServerDiscover/;
1182
- var HEALTH_ROUTE_RE = /\b[A-Za-z_$][\w$]*\.(?:get|post|put|patch|delete|all|route|use)\s*\(\s*["'`][^"'`]*\/health/;
3673
+ var HEALTH_ROUTE_FRAMEWORK_RE = /\b[A-Za-z_$][\w$]*\.(?:get|post|put|patch|delete|all|route|use)\s*\(\s*["'`][^"'`]*\/health/;
3674
+ var HEALTH_ROUTE_RAWPATH_RE = /\bpathname\s*===?\s*["'`][^"'`]*\/health/;
3675
+ var HEALTH_ROUTE_RE = new RegExp(
3676
+ `${HEALTH_ROUTE_FRAMEWORK_RE.source}|${HEALTH_ROUTE_RAWPATH_RE.source}`
3677
+ );
1183
3678
  var VERSION_FIELD_RE = /\bversion\s*:/;
1184
3679
  var PKG_SOURCE_RE = /(?:pkg|pkgJson|packageJson|pjson|packageInfo)\.version|process\.env\.npm_package_version|["'`][^"'`]*package\.json["'`]/;
1185
3680
  var PKG_IMPORT_IMPORT_RE = /import\s+(?:\*\s+as\s+)?([A-Za-z_$][\w$]*)\s+from\s+["'`][^"'`]*package\.json["'`]/;
@@ -1196,11 +3691,20 @@ var CONTROLS2 = [
1196
3691
  negative: 'method: "tools/list"'
1197
3692
  },
1198
3693
  {
1199
- key: "health",
3694
+ key: "health-framework",
1200
3695
  re: HEALTH_ROUTE_RE,
1201
3696
  positive: 'app.get("/health", handler)',
1202
3697
  negative: 'app.get("/tools", handler)'
1203
3698
  },
3699
+ {
3700
+ // FORM 2 — the raw-pathname comparison the old matcher was blind to.
3701
+ // Positive is the REAL vantage-registry-mcp:297 shape; negative is the
3702
+ // sibling `/mcp` route on the same server (a near-miss that must NOT match).
3703
+ key: "health-rawpath",
3704
+ re: HEALTH_ROUTE_RE,
3705
+ positive: 'if (req.method === "GET" && url.pathname === "/health") {',
3706
+ negative: 'if (url.pathname === "/mcp") {'
3707
+ },
1204
3708
  {
1205
3709
  key: "pkg-source",
1206
3710
  re: PKG_SOURCE_RE,
@@ -1308,7 +3812,7 @@ var d16 = {
1308
3812
  id: this.id,
1309
3813
  anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
1310
3814
  state: "absent",
1311
- verdict: "no server/discover handler and no /health route (n/a for Server Rule 16)"
3815
+ verdict: "[static read] no server/discover handler and no /health route (n/a for Server Rule 16)"
1312
3816
  };
1313
3817
  }
1314
3818
  const present = surfaces.find((s) => s.kind === "present");
@@ -1317,7 +3821,7 @@ var d16 = {
1317
3821
  id: this.id,
1318
3822
  anchor: present.anchor,
1319
3823
  state: "present",
1320
- verdict: `version exposed pre-auth at ${present.anchor}, sourced from package.json (Server Rule 16)`
3824
+ verdict: `[static read] version exposed pre-auth at ${present.anchor}, sourced from package.json (Server Rule 16)`
1321
3825
  };
1322
3826
  }
1323
3827
  const decisive = surfaces.find(
@@ -1338,7 +3842,7 @@ var d16 = {
1338
3842
  id: this.id,
1339
3843
  anchor: decisive.anchor,
1340
3844
  state: "non-conforming",
1341
- verdict: `${fix[decisive.kind]} at ${decisive.anchor} (Server Rule 16)`
3845
+ verdict: `[static read] ${fix[decisive.kind]} at ${decisive.anchor} (Server Rule 16)`
1342
3846
  };
1343
3847
  }
1344
3848
  const unreadable = surfaces[0];
@@ -1352,7 +3856,7 @@ var d16 = {
1352
3856
  } else {
1353
3857
  detail = "is present but its source is not a resolvable package.json binding in the read window";
1354
3858
  }
1355
- const reason = `surface at ${unreadable.anchor} ${detail} \u2014 verdict unreadable statically (Server Rule 16)`;
3859
+ const reason = `[static read] surface at ${unreadable.anchor} ${detail} \u2014 verdict unreadable statically (Server Rule 16)`;
1356
3860
  return {
1357
3861
  id: this.id,
1358
3862
  anchor: unreadable.anchor,
@@ -1360,12 +3864,97 @@ var d16 = {
1360
3864
  reason,
1361
3865
  verdict: `could not judge: ${reason}`
1362
3866
  };
3867
+ },
3868
+ /**
3869
+ * DYNAMIC — Rule 16 judged from what the RUNNING server SERVES, its true home.
3870
+ * The MCP `initialize` handshake returns `serverInfo.version` BEFORE any auth
3871
+ * token is presented, so the served version is a pre-auth observable by
3872
+ * construction. The served value is the truth: it is compared to the target's
3873
+ * own package.json version (the canonical source Rule 16 requires).
3874
+ *
3875
+ * present — the server serves a version AND it equals package.json
3876
+ * (observable pre-auth, sourced from the real version).
3877
+ * non-conforming — the server serves NO version, OR serves a version that
3878
+ * DRIFTS from package.json (a hardcoded literal is the
3879
+ * classic cause; the served value != the package's truth).
3880
+ * could-not-judge — the server could not be launched/introspected (naming
3881
+ * the failure), or its package.json could not be read to
3882
+ * establish the canonical version to compare against.
3883
+ *
3884
+ * Every dynamic verdict says out loud it was produced by asking the running
3885
+ * server ("[dynamic]"), so it is NEVER mistaken for the static fallback.
3886
+ */
3887
+ runDynamic(runtime) {
3888
+ if (runtime.launchError) {
3889
+ const reason = `[dynamic] server could not be started/introspected \u2014 served version unobservable: ${runtime.launchError} (Server Rule 16)`;
3890
+ return {
3891
+ id: this.id,
3892
+ anchor: `${runtime.root} (runtime)`,
3893
+ state: "could-not-judge",
3894
+ reason,
3895
+ verdict: `could not judge: ${reason}`
3896
+ };
3897
+ }
3898
+ const servedRaw = runtime.serverInfo?.version;
3899
+ const served = typeof servedRaw === "string" ? servedRaw.trim() : void 0;
3900
+ const anchor = `${runtime.root} (runtime initialize)`;
3901
+ if (!served) {
3902
+ return {
3903
+ id: this.id,
3904
+ anchor,
3905
+ state: "non-conforming",
3906
+ verdict: "[dynamic] the running server serves NO version on the pre-auth initialize handshake -> expose a version sourced from package.json (Server Rule 16)"
3907
+ };
3908
+ }
3909
+ const pkgVersion = readPackageVersion(runtime.root);
3910
+ if (!pkgVersion) {
3911
+ const reason = `[dynamic] the server serves version "${served}" but its package.json version could not be read to compare against \u2014 canonical version unestablished (Server Rule 16)`;
3912
+ return {
3913
+ id: this.id,
3914
+ anchor,
3915
+ state: "could-not-judge",
3916
+ reason,
3917
+ verdict: `could not judge: ${reason}`
3918
+ };
3919
+ }
3920
+ if (served === pkgVersion) {
3921
+ return {
3922
+ id: this.id,
3923
+ anchor,
3924
+ state: "present",
3925
+ verdict: `[dynamic] running server serves version "${served}" pre-auth on initialize, matching package.json "${pkgVersion}" (Server Rule 16)`
3926
+ };
3927
+ }
3928
+ return {
3929
+ id: this.id,
3930
+ anchor,
3931
+ state: "non-conforming",
3932
+ verdict: `[dynamic] running server serves version "${served}" but package.json is "${pkgVersion}" -> served value drifts from the package (source it from package.json) (Server Rule 16)`
3933
+ };
1363
3934
  }
1364
3935
  };
3936
+ function readPackageVersion(root) {
3937
+ try {
3938
+ let dir = statSync4(root).isDirectory() ? root : dirname5(root);
3939
+ for (let depth = 0; depth < 8; depth++) {
3940
+ const candidate = join10(dir, "package.json");
3941
+ if (existsSync10(candidate)) {
3942
+ const pkg = JSON.parse(readFileSync8(candidate, "utf8"));
3943
+ return typeof pkg.version === "string" ? pkg.version : null;
3944
+ }
3945
+ const parent = dirname5(dir);
3946
+ if (parent === dir) break;
3947
+ dir = parent;
3948
+ }
3949
+ } catch {
3950
+ return null;
3951
+ }
3952
+ return null;
3953
+ }
1365
3954
 
1366
3955
  // src/detectors/dAppI18n.ts
1367
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1368
- import { dirname as dirname4, join as join4 } from "path";
3956
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
3957
+ import { dirname as dirname6, join as join11 } from "path";
1369
3958
  function flattenKeys(obj, prefix = "") {
1370
3959
  if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
1371
3960
  return prefix ? [prefix] : [];
@@ -1396,9 +3985,9 @@ function dAppI18nPositiveControls() {
1396
3985
  }
1397
3986
  };
1398
3987
  }
1399
- function readJson(path) {
3988
+ function readJson2(path) {
1400
3989
  try {
1401
- return { ok: true, value: JSON.parse(readFileSync2(path, "utf8")) };
3990
+ return { ok: true, value: JSON.parse(readFileSync9(path, "utf8")) };
1402
3991
  } catch {
1403
3992
  return { ok: false };
1404
3993
  }
@@ -1412,12 +4001,12 @@ var dAppI18n = {
1412
4001
  `DAppI18n key-diff control FAILED: detectsGap=${control.detectsGap} detectsComplete=${control.detectsComplete} \u2014 matcher broken, verdict untrustworthy`
1413
4002
  );
1414
4003
  }
1415
- const root = tree.packageJson ? dirname4(tree.packageJson.path) : tree.root;
1416
- const i18nDir = join4(root, "i18n");
1417
- const enPath = join4(i18nDir, "en.json");
1418
- const frPath = join4(i18nDir, "fr.json");
1419
- const hasI18n = existsSync4(i18nDir);
1420
- const hasFr = existsSync4(frPath);
4004
+ const root = tree.packageJson ? dirname6(tree.packageJson.path) : tree.root;
4005
+ const i18nDir = join11(root, "i18n");
4006
+ const enPath = join11(i18nDir, "en.json");
4007
+ const frPath = join11(i18nDir, "fr.json");
4008
+ const hasI18n = existsSync11(i18nDir);
4009
+ const hasFr = existsSync11(frPath);
1421
4010
  if (!hasI18n && !hasFr) {
1422
4011
  return {
1423
4012
  id: this.id,
@@ -1434,7 +4023,7 @@ var dAppI18n = {
1434
4023
  verdict: "i18n/ layer present but i18n/fr.json is missing -> ship the French locale (App Rule 2)"
1435
4024
  };
1436
4025
  }
1437
- const frRead = readJson(frPath);
4026
+ const frRead = readJson2(frPath);
1438
4027
  if (!frRead.ok) {
1439
4028
  const reason = "i18n/fr.json is not readable JSON \u2014 cannot compare its keys against the source locale (App Rule 2)";
1440
4029
  return {
@@ -1445,7 +4034,7 @@ var dAppI18n = {
1445
4034
  verdict: `could not judge: ${reason}`
1446
4035
  };
1447
4036
  }
1448
- if (!existsSync4(enPath)) {
4037
+ if (!existsSync11(enPath)) {
1449
4038
  const reason = "i18n/fr.json exists but no source-locale i18n/en.json to compare its keys against (App Rule 2)";
1450
4039
  return {
1451
4040
  id: this.id,
@@ -1455,7 +4044,7 @@ var dAppI18n = {
1455
4044
  verdict: `could not judge: ${reason}`
1456
4045
  };
1457
4046
  }
1458
- const enRead = readJson(enPath);
4047
+ const enRead = readJson2(enPath);
1459
4048
  if (!enRead.ok) {
1460
4049
  const reason = "i18n/en.json (source locale) is not readable JSON \u2014 cannot derive the reference key set (App Rule 2)";
1461
4050
  return {
@@ -1588,46 +4177,46 @@ var dDebugOptIn = {
1588
4177
  };
1589
4178
 
1590
4179
  // src/detectors/dExt1.ts
1591
- import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "fs";
1592
- import { join as join5 } from "path";
1593
- var IDENTIFIER_RE = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
4180
+ import { readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4181
+ import { join as join12 } from "path";
4182
+ var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
1594
4183
  var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
1595
4184
  var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
1596
4185
  function dExt1PositiveControls() {
1597
4186
  return {
1598
4187
  identifier: {
1599
- pattern: IDENTIFIER_RE.source,
1600
- positive: IDENTIFIER_RE.test(IDENTIFIER_POSITIVE),
1601
- negative: IDENTIFIER_RE.test(IDENTIFIER_NEGATIVE)
4188
+ pattern: IDENTIFIER_RE2.source,
4189
+ positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
4190
+ negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
1602
4191
  }
1603
4192
  };
1604
4193
  }
1605
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4194
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
1606
4195
  function findExtensionManifests(root) {
1607
4196
  const out = [];
1608
4197
  let base;
1609
4198
  try {
1610
- base = statSync2(root).isDirectory() ? root : join5(root, "..");
4199
+ base = statSync5(root).isDirectory() ? root : join12(root, "..");
1611
4200
  } catch {
1612
4201
  return out;
1613
4202
  }
1614
4203
  const walk2 = (dir) => {
1615
4204
  let entries;
1616
4205
  try {
1617
- entries = readdirSync(dir);
4206
+ entries = readdirSync4(dir);
1618
4207
  } catch {
1619
4208
  return;
1620
4209
  }
1621
4210
  for (const name of entries) {
1622
- const full = join5(dir, name);
4211
+ const full = join12(dir, name);
1623
4212
  let st;
1624
4213
  try {
1625
- st = statSync2(full);
4214
+ st = statSync5(full);
1626
4215
  } catch {
1627
4216
  continue;
1628
4217
  }
1629
4218
  if (st.isDirectory()) {
1630
- if (SKIP_DIRS.has(name)) continue;
4219
+ if (SKIP_DIRS2.has(name)) continue;
1631
4220
  walk2(full);
1632
4221
  } else if (name === "extension.json") {
1633
4222
  out.push(full);
@@ -1667,11 +4256,11 @@ var dExt1 = {
1667
4256
  );
1668
4257
  }
1669
4258
  const manifests = findExtensionManifests(tree.root);
1670
- for (const manifestPath of manifests) {
1671
- const shortPath = manifestPath.split("/").slice(-2).join("/");
4259
+ for (const manifestPath2 of manifests) {
4260
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
1672
4261
  let parsed;
1673
4262
  try {
1674
- parsed = JSON.parse(readFileSync3(manifestPath, "utf8"));
4263
+ parsed = JSON.parse(readFileSync10(manifestPath2, "utf8"));
1675
4264
  } catch {
1676
4265
  throw new Error(
1677
4266
  `DExt1 could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable identifier cannot pass (Extension Rule 1). Fail-closed, not a silent present.`
@@ -1683,7 +4272,7 @@ var dExt1 = {
1683
4272
  `DExt1 found a present manifest ${shortPath} with no string "identifier" field (got ${typeof idValue}) \u2014 Extension Rule 1 requires a reverse-DNS identifier. Fail-closed, not a silent present.`
1684
4273
  );
1685
4274
  }
1686
- if (!IDENTIFIER_RE.test(idValue)) {
4275
+ if (!IDENTIFIER_RE2.test(idValue)) {
1687
4276
  return {
1688
4277
  id: this.id,
1689
4278
  anchor: shortPath,
@@ -1699,7 +4288,7 @@ var dExt1 = {
1699
4288
  );
1700
4289
  }
1701
4290
  for (const site of sites) {
1702
- if (!IDENTIFIER_RE.test(site.value)) {
4291
+ if (!IDENTIFIER_RE2.test(site.value)) {
1703
4292
  return {
1704
4293
  id: this.id,
1705
4294
  anchor: site.anchor,
@@ -1727,8 +4316,8 @@ var dExt1 = {
1727
4316
  };
1728
4317
 
1729
4318
  // src/detectors/dExt2.ts
1730
- import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
1731
- import { join as join6 } from "path";
4319
+ import { readFileSync as readFileSync11, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
4320
+ import { join as join13 } from "path";
1732
4321
  var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
1733
4322
  var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
1734
4323
  var TS_GD_KEY_RE = /\bgracefulDegradation:\s*([^\s,}]+)/;
@@ -1743,32 +4332,32 @@ function dExt2PositiveControls() {
1743
4332
  }
1744
4333
  };
1745
4334
  }
1746
- var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4335
+ var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
1747
4336
  function findExtensionManifests2(root) {
1748
4337
  const out = [];
1749
4338
  let base;
1750
4339
  try {
1751
- base = statSync3(root).isDirectory() ? root : join6(root, "..");
4340
+ base = statSync6(root).isDirectory() ? root : join13(root, "..");
1752
4341
  } catch {
1753
4342
  return out;
1754
4343
  }
1755
4344
  const walk2 = (dir) => {
1756
4345
  let entries;
1757
4346
  try {
1758
- entries = readdirSync2(dir);
4347
+ entries = readdirSync5(dir);
1759
4348
  } catch {
1760
4349
  return;
1761
4350
  }
1762
4351
  for (const name of entries) {
1763
- const full = join6(dir, name);
4352
+ const full = join13(dir, name);
1764
4353
  let st;
1765
4354
  try {
1766
- st = statSync3(full);
4355
+ st = statSync6(full);
1767
4356
  } catch {
1768
4357
  continue;
1769
4358
  }
1770
4359
  if (st.isDirectory()) {
1771
- if (SKIP_DIRS2.has(name)) continue;
4360
+ if (SKIP_DIRS3.has(name)) continue;
1772
4361
  walk2(full);
1773
4362
  } else if (name === "extension.json") {
1774
4363
  out.push(full);
@@ -1828,11 +4417,11 @@ var dExt2 = {
1828
4417
  );
1829
4418
  }
1830
4419
  const manifests = findExtensionManifests2(tree.root);
1831
- for (const manifestPath of manifests) {
1832
- const shortPath = manifestPath.split("/").slice(-2).join("/");
4420
+ for (const manifestPath2 of manifests) {
4421
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
1833
4422
  let parsed;
1834
4423
  try {
1835
- parsed = JSON.parse(readFileSync4(manifestPath, "utf8"));
4424
+ parsed = JSON.parse(readFileSync11(manifestPath2, "utf8"));
1836
4425
  } catch {
1837
4426
  throw new Error(
1838
4427
  `DExt2 could not parse ${shortPath} as JSON \u2014 a present manifest silent on graceful degradation cannot pass (Extension Rule 2). Fail-closed, not a silent present.`
@@ -1896,8 +4485,8 @@ var dExt2 = {
1896
4485
  };
1897
4486
 
1898
4487
  // src/detectors/dExt3.ts
1899
- import { readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
1900
- import { join as join7 } from "path";
4488
+ import { readFileSync as readFileSync12, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
4489
+ import { join as join14 } from "path";
1901
4490
  var TYPE_MATCHERS = [
1902
4491
  { type: "middleware", re: /\bmiddleware\b/ },
1903
4492
  { type: "tool-extension", re: /\btool-ext(?:ension)?\b/ },
@@ -1920,32 +4509,32 @@ function dExt3PositiveControls() {
1920
4509
  }
1921
4510
  };
1922
4511
  }
1923
- var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4512
+ var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
1924
4513
  function findExtensionManifests3(root) {
1925
4514
  const out = [];
1926
4515
  let base;
1927
4516
  try {
1928
- base = statSync4(root).isDirectory() ? root : join7(root, "..");
4517
+ base = statSync7(root).isDirectory() ? root : join14(root, "..");
1929
4518
  } catch {
1930
4519
  return out;
1931
4520
  }
1932
4521
  const walk2 = (dir) => {
1933
4522
  let entries;
1934
4523
  try {
1935
- entries = readdirSync3(dir);
4524
+ entries = readdirSync6(dir);
1936
4525
  } catch {
1937
4526
  return;
1938
4527
  }
1939
4528
  for (const name of entries) {
1940
- const full = join7(dir, name);
4529
+ const full = join14(dir, name);
1941
4530
  let st;
1942
4531
  try {
1943
- st = statSync4(full);
4532
+ st = statSync7(full);
1944
4533
  } catch {
1945
4534
  continue;
1946
4535
  }
1947
4536
  if (st.isDirectory()) {
1948
- if (SKIP_DIRS3.has(name)) continue;
4537
+ if (SKIP_DIRS4.has(name)) continue;
1949
4538
  walk2(full);
1950
4539
  } else if (name === "extension.json") {
1951
4540
  out.push(full);
@@ -2013,11 +4602,11 @@ var dExt3 = {
2013
4602
  const declared = /* @__PURE__ */ new Set();
2014
4603
  let firstAnchor2 = null;
2015
4604
  const manifests = findExtensionManifests3(tree.root);
2016
- for (const manifestPath of manifests) {
2017
- const shortPath = manifestPath.split("/").slice(-2).join("/");
4605
+ for (const manifestPath2 of manifests) {
4606
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
2018
4607
  let parsed;
2019
4608
  try {
2020
- parsed = JSON.parse(readFileSync5(manifestPath, "utf8"));
4609
+ parsed = JSON.parse(readFileSync12(manifestPath2, "utf8"));
2021
4610
  } catch {
2022
4611
  throw new Error(
2023
4612
  `DExt3 could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable type cannot pass (Extension Rule 3). Fail-closed, not a silent present.`
@@ -2103,8 +4692,8 @@ var dExt3 = {
2103
4692
  };
2104
4693
 
2105
4694
  // src/detectors/dExt4.ts
2106
- import { readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
2107
- import { join as join8 } from "path";
4695
+ import { readFileSync as readFileSync13, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
4696
+ import { join as join15 } from "path";
2108
4697
  function canonical(name) {
2109
4698
  return name.replace(/[-_]/g, "").toLowerCase();
2110
4699
  }
@@ -2123,32 +4712,32 @@ function dExt4PositiveControls() {
2123
4712
  }
2124
4713
  };
2125
4714
  }
2126
- var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4715
+ var SKIP_DIRS5 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
2127
4716
  function findExtensionManifests4(root) {
2128
4717
  const out = [];
2129
4718
  let base;
2130
4719
  try {
2131
- base = statSync5(root).isDirectory() ? root : join8(root, "..");
4720
+ base = statSync8(root).isDirectory() ? root : join15(root, "..");
2132
4721
  } catch {
2133
4722
  return out;
2134
4723
  }
2135
4724
  const walk2 = (dir) => {
2136
4725
  let entries;
2137
4726
  try {
2138
- entries = readdirSync4(dir);
4727
+ entries = readdirSync7(dir);
2139
4728
  } catch {
2140
4729
  return;
2141
4730
  }
2142
4731
  for (const name of entries) {
2143
- const full = join8(dir, name);
4732
+ const full = join15(dir, name);
2144
4733
  let st;
2145
4734
  try {
2146
- st = statSync5(full);
4735
+ st = statSync8(full);
2147
4736
  } catch {
2148
4737
  continue;
2149
4738
  }
2150
4739
  if (st.isDirectory()) {
2151
- if (SKIP_DIRS4.has(name)) continue;
4740
+ if (SKIP_DIRS5.has(name)) continue;
2152
4741
  walk2(full);
2153
4742
  } else if (name === "extension.json") {
2154
4743
  out.push(full);
@@ -2162,32 +4751,32 @@ function findHooksDirs(root) {
2162
4751
  const out = [];
2163
4752
  let base;
2164
4753
  try {
2165
- base = statSync5(root).isDirectory() ? root : join8(root, "..");
4754
+ base = statSync8(root).isDirectory() ? root : join15(root, "..");
2166
4755
  } catch {
2167
4756
  return out;
2168
4757
  }
2169
4758
  const walk2 = (dir) => {
2170
4759
  let entries;
2171
4760
  try {
2172
- entries = readdirSync4(dir);
4761
+ entries = readdirSync7(dir);
2173
4762
  } catch {
2174
4763
  return;
2175
4764
  }
2176
4765
  for (const name of entries) {
2177
- const full = join8(dir, name);
4766
+ const full = join15(dir, name);
2178
4767
  let st;
2179
4768
  try {
2180
- st = statSync5(full);
4769
+ st = statSync8(full);
2181
4770
  } catch {
2182
4771
  continue;
2183
4772
  }
2184
4773
  if (!st.isDirectory()) continue;
2185
- if (SKIP_DIRS4.has(name)) continue;
4774
+ if (SKIP_DIRS5.has(name)) continue;
2186
4775
  if (name === "hooks") {
2187
4776
  const tokens = /* @__PURE__ */ new Set();
2188
4777
  let files;
2189
4778
  try {
2190
- files = readdirSync4(full);
4779
+ files = readdirSync7(full);
2191
4780
  } catch {
2192
4781
  files = [];
2193
4782
  }
@@ -2276,11 +4865,11 @@ var dExt4 = {
2276
4865
  const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
2277
4866
  const manifests = findExtensionManifests4(tree.root);
2278
4867
  let declaredAnyLifecycle = false;
2279
- for (const manifestPath of manifests) {
2280
- const shortPath = manifestPath.split("/").slice(-2).join("/");
4868
+ for (const manifestPath2 of manifests) {
4869
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
2281
4870
  let parsed;
2282
4871
  try {
2283
- parsed = JSON.parse(readFileSync6(manifestPath, "utf8"));
4872
+ parsed = JSON.parse(readFileSync13(manifestPath2, "utf8"));
2284
4873
  } catch {
2285
4874
  throw new Error(
2286
4875
  `DExt4 could not parse ${shortPath} as JSON \u2014 a present manifest whose lifecycle cannot be read cannot pass (Extension Rule 4). Fail-closed, not a silent present.`
@@ -2338,8 +4927,8 @@ var dExt4 = {
2338
4927
  };
2339
4928
 
2340
4929
  // src/detectors/dExt5.ts
2341
- import { existsSync as existsSync5, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
2342
- import { dirname as dirname5, join as join9 } from "path";
4930
+ import { existsSync as existsSync12, readFileSync as readFileSync14, readdirSync as readdirSync8, statSync as statSync9 } from "fs";
4931
+ import { dirname as dirname7, join as join16 } from "path";
2343
4932
  function nonEmptyString(v) {
2344
4933
  return typeof v === "string" && v.trim().length > 0;
2345
4934
  }
@@ -2353,32 +4942,32 @@ function dExt5PositiveControls() {
2353
4942
  }
2354
4943
  };
2355
4944
  }
2356
- var SKIP_DIRS5 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4945
+ var SKIP_DIRS6 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
2357
4946
  function findExtensionManifests5(root) {
2358
4947
  const out = [];
2359
4948
  let base;
2360
4949
  try {
2361
- base = statSync6(root).isDirectory() ? root : join9(root, "..");
4950
+ base = statSync9(root).isDirectory() ? root : join16(root, "..");
2362
4951
  } catch {
2363
4952
  return out;
2364
4953
  }
2365
4954
  const walk2 = (dir) => {
2366
4955
  let entries;
2367
4956
  try {
2368
- entries = readdirSync5(dir);
4957
+ entries = readdirSync8(dir);
2369
4958
  } catch {
2370
4959
  return;
2371
4960
  }
2372
4961
  for (const name of entries) {
2373
- const full = join9(dir, name);
4962
+ const full = join16(dir, name);
2374
4963
  let st;
2375
4964
  try {
2376
- st = statSync6(full);
4965
+ st = statSync9(full);
2377
4966
  } catch {
2378
4967
  continue;
2379
4968
  }
2380
4969
  if (st.isDirectory()) {
2381
- if (SKIP_DIRS5.has(name)) continue;
4970
+ if (SKIP_DIRS6.has(name)) continue;
2382
4971
  walk2(full);
2383
4972
  } else if (name === "extension.json") {
2384
4973
  out.push(full);
@@ -2409,9 +4998,9 @@ var dExt5 = {
2409
4998
  verdict: "no extension.json manifest found -> declare a bilingual extension with description_fr + README.fr.md (Extension Rule 5)"
2410
4999
  };
2411
5000
  }
2412
- for (const manifestPath of manifests) {
2413
- const shortPath = manifestPath.split("/").slice(-2).join("/");
2414
- const root = dirname5(manifestPath);
5001
+ for (const manifestPath2 of manifests) {
5002
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
5003
+ const root = dirname7(manifestPath2);
2415
5004
  const cannotRead = (reason) => ({
2416
5005
  id: this.id,
2417
5006
  anchor: shortPath,
@@ -2421,7 +5010,7 @@ var dExt5 = {
2421
5010
  });
2422
5011
  let parsed;
2423
5012
  try {
2424
- parsed = JSON.parse(readFileSync7(manifestPath, "utf8"));
5013
+ parsed = JSON.parse(readFileSync14(manifestPath2, "utf8"));
2425
5014
  } catch {
2426
5015
  return cannotRead(
2427
5016
  `${shortPath} does not parse as JSON \u2014 the FR surface of a present manifest cannot be read (Extension Rule 5)`
@@ -2436,7 +5025,7 @@ var dExt5 = {
2436
5025
  if (!nonEmptyString(parsed.description_fr)) {
2437
5026
  missing.push("description_fr (extension.json)");
2438
5027
  }
2439
- if (!existsSync5(join9(root, "README.fr.md"))) {
5028
+ if (!existsSync12(join16(root, "README.fr.md"))) {
2440
5029
  missing.push("README.fr.md");
2441
5030
  }
2442
5031
  if (isToolExtension(parsed.type) && "tools" in parsed) {
@@ -2479,8 +5068,8 @@ var dExt5 = {
2479
5068
  };
2480
5069
 
2481
5070
  // src/detectors/dExt7.ts
2482
- import { existsSync as existsSync6, readFileSync as readFileSync8, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
2483
- import { dirname as dirname6, join as join10 } from "path";
5071
+ import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync9, statSync as statSync10 } from "fs";
5072
+ import { dirname as dirname8, join as join17 } from "path";
2484
5073
  var ABSENCE_FLAG_FIELDS = [
2485
5074
  "extensionAbsent",
2486
5075
  "extension_absent",
@@ -2523,32 +5112,32 @@ function dExt7PositiveControls() {
2523
5112
  negative: isAbsenceCase(ABSENCE_NEGATIVE)
2524
5113
  };
2525
5114
  }
2526
- var SKIP_DIRS6 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5115
+ var SKIP_DIRS7 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
2527
5116
  function findExtensionManifests6(root) {
2528
5117
  const out = [];
2529
5118
  let base;
2530
5119
  try {
2531
- base = statSync7(root).isDirectory() ? root : join10(root, "..");
5120
+ base = statSync10(root).isDirectory() ? root : join17(root, "..");
2532
5121
  } catch {
2533
5122
  return out;
2534
5123
  }
2535
5124
  const walk2 = (dir) => {
2536
5125
  let entries;
2537
5126
  try {
2538
- entries = readdirSync6(dir);
5127
+ entries = readdirSync9(dir);
2539
5128
  } catch {
2540
5129
  return;
2541
5130
  }
2542
5131
  for (const name of entries) {
2543
- const full = join10(dir, name);
5132
+ const full = join17(dir, name);
2544
5133
  let st;
2545
5134
  try {
2546
- st = statSync7(full);
5135
+ st = statSync10(full);
2547
5136
  } catch {
2548
5137
  continue;
2549
5138
  }
2550
5139
  if (st.isDirectory()) {
2551
- if (SKIP_DIRS6.has(name)) continue;
5140
+ if (SKIP_DIRS7.has(name)) continue;
2552
5141
  walk2(full);
2553
5142
  } else if (name === "extension.json") {
2554
5143
  out.push(full);
@@ -2576,12 +5165,12 @@ var dExt7 = {
2576
5165
  verdict: "no extension.json manifest found -> declare an extension with an eval suite (>=3 cases incl. 1 'extension absente') (Extension Rule 7)"
2577
5166
  };
2578
5167
  }
2579
- for (const manifestPath of manifests) {
2580
- const extDir = dirname6(manifestPath);
5168
+ for (const manifestPath2 of manifests) {
5169
+ const extDir = dirname8(manifestPath2);
2581
5170
  const extLabel = extDir.split("/").pop() ?? extDir;
2582
- const evalsPath = join10(extDir, "evals", "evals.json");
5171
+ const evalsPath = join17(extDir, "evals", "evals.json");
2583
5172
  const shortPath = `${extLabel}/evals/evals.json`;
2584
- if (!existsSync6(evalsPath)) {
5173
+ if (!existsSync13(evalsPath)) {
2585
5174
  return {
2586
5175
  id: this.id,
2587
5176
  anchor: `${extLabel}/extension.json`,
@@ -2591,7 +5180,7 @@ var dExt7 = {
2591
5180
  }
2592
5181
  let cases;
2593
5182
  try {
2594
- const raw = readFileSync8(evalsPath, "utf8");
5183
+ const raw = readFileSync15(evalsPath, "utf8");
2595
5184
  const parsed = JSON.parse(raw);
2596
5185
  if (!Array.isArray(parsed.cases)) {
2597
5186
  throw new Error("`cases` is not an array");
@@ -2634,8 +5223,8 @@ var dExt7 = {
2634
5223
  };
2635
5224
 
2636
5225
  // src/detectors/dExt8.ts
2637
- import { existsSync as existsSync7, readFileSync as readFileSync9, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
2638
- import { dirname as dirname7, join as join11 } from "path";
5226
+ import { existsSync as existsSync14, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync11 } from "fs";
5227
+ import { dirname as dirname9, join as join18 } from "path";
2639
5228
  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-]+)*))?$/;
2640
5229
  var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
2641
5230
  function dExt8PositiveControl() {
@@ -2645,32 +5234,32 @@ function dExt8PositiveControl() {
2645
5234
  matched: SEMVER_RE2.test(POSITIVE_CONTROL5)
2646
5235
  };
2647
5236
  }
2648
- var SKIP_DIRS7 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5237
+ var SKIP_DIRS8 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
2649
5238
  function findExtensionManifests7(root) {
2650
5239
  const out = [];
2651
5240
  let base;
2652
5241
  try {
2653
- base = statSync8(root).isDirectory() ? root : join11(root, "..");
5242
+ base = statSync11(root).isDirectory() ? root : join18(root, "..");
2654
5243
  } catch {
2655
5244
  return out;
2656
5245
  }
2657
5246
  const walk2 = (dir) => {
2658
5247
  let entries;
2659
5248
  try {
2660
- entries = readdirSync7(dir);
5249
+ entries = readdirSync10(dir);
2661
5250
  } catch {
2662
5251
  return;
2663
5252
  }
2664
5253
  for (const name of entries) {
2665
- const full = join11(dir, name);
5254
+ const full = join18(dir, name);
2666
5255
  let st;
2667
5256
  try {
2668
- st = statSync8(full);
5257
+ st = statSync11(full);
2669
5258
  } catch {
2670
5259
  continue;
2671
5260
  }
2672
5261
  if (st.isDirectory()) {
2673
- if (SKIP_DIRS7.has(name)) continue;
5262
+ if (SKIP_DIRS8.has(name)) continue;
2674
5263
  walk2(full);
2675
5264
  } else if (name === "extension.json") {
2676
5265
  out.push(full);
@@ -2698,19 +5287,19 @@ var dExt8 = {
2698
5287
  verdict: "no extension.json manifest found -> declare an extension with a valid SemVer version + CHANGELOG.md (Extension Rule 8)"
2699
5288
  };
2700
5289
  }
2701
- for (const manifestPath of manifests) {
2702
- const extDir = dirname7(manifestPath);
5290
+ for (const manifestPath2 of manifests) {
5291
+ const extDir = dirname9(manifestPath2);
2703
5292
  const extLabel = extDir.split("/").pop() ?? extDir;
2704
- const pkgPath = join11(extDir, "package.json");
5293
+ const pkgPath = join18(extDir, "package.json");
2705
5294
  const shortPkgPath = `${extLabel}/package.json`;
2706
- if (!existsSync7(pkgPath)) {
5295
+ if (!existsSync14(pkgPath)) {
2707
5296
  throw new Error(
2708
5297
  `DExt8 could not locate ${shortPkgPath} for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8). Fail-closed, not a silent present.`
2709
5298
  );
2710
5299
  }
2711
5300
  let pkgJson;
2712
5301
  try {
2713
- pkgJson = JSON.parse(readFileSync9(pkgPath, "utf8"));
5302
+ pkgJson = JSON.parse(readFileSync16(pkgPath, "utf8"));
2714
5303
  } catch {
2715
5304
  throw new Error(
2716
5305
  `DExt8 could not parse ${shortPkgPath} as JSON for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8). Fail-closed, not a silent present.`
@@ -2731,8 +5320,8 @@ var dExt8 = {
2731
5320
  };
2732
5321
  }
2733
5322
  const validSemver = SEMVER_RE2.test(version);
2734
- const changelogPath = join11(extDir, "CHANGELOG.md");
2735
- const hasChangelog = existsSync7(changelogPath);
5323
+ const changelogPath = join18(extDir, "CHANGELOG.md");
5324
+ const hasChangelog = existsSync14(changelogPath);
2736
5325
  if (!validSemver) {
2737
5326
  return {
2738
5327
  id: this.id,
@@ -2761,38 +5350,38 @@ var dExt8 = {
2761
5350
  };
2762
5351
 
2763
5352
  // src/detectors/dExt9.ts
2764
- import { readFileSync as readFileSync10, readdirSync as readdirSync8, statSync as statSync9 } from "fs";
2765
- import { join as join12 } from "path";
2766
- var SKIP_DIRS8 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5353
+ import { readFileSync as readFileSync17, readdirSync as readdirSync11, statSync as statSync12 } from "fs";
5354
+ import { join as join19 } from "path";
5355
+ var SKIP_DIRS9 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
2767
5356
  function collectHooksDirSources(root) {
2768
5357
  const out = [];
2769
5358
  let base;
2770
5359
  try {
2771
- base = statSync9(root).isDirectory() ? root : join12(root, "..");
5360
+ base = statSync12(root).isDirectory() ? root : join19(root, "..");
2772
5361
  } catch {
2773
5362
  return out;
2774
5363
  }
2775
5364
  const walk2 = (dir) => {
2776
5365
  let entries;
2777
5366
  try {
2778
- entries = readdirSync8(dir);
5367
+ entries = readdirSync11(dir);
2779
5368
  } catch {
2780
5369
  return;
2781
5370
  }
2782
5371
  for (const name of entries) {
2783
- const full = join12(dir, name);
5372
+ const full = join19(dir, name);
2784
5373
  let st;
2785
5374
  try {
2786
- st = statSync9(full);
5375
+ st = statSync12(full);
2787
5376
  } catch {
2788
5377
  continue;
2789
5378
  }
2790
5379
  if (!st.isDirectory()) continue;
2791
- if (SKIP_DIRS8.has(name)) continue;
5380
+ if (SKIP_DIRS9.has(name)) continue;
2792
5381
  if (name === "hooks") {
2793
5382
  let files;
2794
5383
  try {
2795
- files = readdirSync8(full);
5384
+ files = readdirSync11(full);
2796
5385
  } catch {
2797
5386
  files = [];
2798
5387
  }
@@ -2800,10 +5389,10 @@ function collectHooksDirSources(root) {
2800
5389
  if (!f.endsWith(".ts") || f.endsWith(".d.ts")) continue;
2801
5390
  const stem = f.replace(/\.ts$/, "");
2802
5391
  if (stem === "index") continue;
2803
- const filePath = join12(full, f);
5392
+ const filePath = join19(full, f);
2804
5393
  let body;
2805
5394
  try {
2806
- body = readFileSync10(filePath, "utf8");
5395
+ body = readFileSync17(filePath, "utf8");
2807
5396
  } catch {
2808
5397
  throw new Error(
2809
5398
  `DExt9 could not read hook implementation ${filePath.split("/").slice(-2).join("/")} \u2014 timeout guard undeterminable for hook "${stem}" (Extension Rule 9). Fail-closed, not a silent absent.`
@@ -2910,11 +5499,11 @@ var dExt9 = {
2910
5499
  }
2911
5500
  const manifests = findExtensionManifests4(tree.root);
2912
5501
  let declaredAnyLifecycle = false;
2913
- for (const manifestPath of manifests) {
2914
- const shortPath = manifestPath.split("/").slice(-2).join("/");
5502
+ for (const manifestPath2 of manifests) {
5503
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
2915
5504
  let parsed;
2916
5505
  try {
2917
- parsed = JSON.parse(readFileSync10(manifestPath, "utf8"));
5506
+ parsed = JSON.parse(readFileSync17(manifestPath2, "utf8"));
2918
5507
  } catch {
2919
5508
  throw new Error(
2920
5509
  `DExt9 could not parse ${shortPath} as JSON \u2014 timeout guards undeterminable (Extension Rule 9). Fail-closed, not a silent absent.`
@@ -2992,6 +5581,8 @@ function unscopePackageName(name) {
2992
5581
  }
2993
5582
  var dServer2 = {
2994
5583
  id: "DServer2 naming",
5584
+ rule: "C-2",
5585
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
2995
5586
  run(tree) {
2996
5587
  const controls = dServer2PositiveControls();
2997
5588
  if (!controls.toolSnake.positive || controls.toolSnake.negative) {
@@ -3123,11 +5714,17 @@ var dServer2 = {
3123
5714
  // src/detectors/dServer6.ts
3124
5715
  var LOG_OPENER_RE = /\b(?:console|logger|log)\.(?:log|info|warn|error|debug|trace)\s*\(/;
3125
5716
  var SECRET_RE = /\b(?:api[_-]?key|token|secret|passwd|password|authorization|auth[_-]?token|bearer|access[_-]?key|client[_-]?secret|private[_-]?key)\b/i;
5717
+ var PII_RE = /\b(?:email|e[_-]?mail|ip[_-]?address|full[_-]?name|phone[_-]?number|ssn|social[_-]?security)\b/i;
5718
+ var INTERNAL_RE = /\b(?:hostname|host[_-]?name|absolute[_-]?path|internal[_-]?path|infra[_-]?version|server[_-]?version|__dirname|__filename)\b/;
3126
5719
  var STACK_RE = /\b(?:error|err|e|ex|exception)\.stack\b/;
3127
5720
  var CLIENT_SINK_RE = /\b(?:content|text|message|body)\s*:|(?:\breturn\b|\bthrow\b|JSON\.stringify)/;
3128
5721
  var SPREAD_RE = /\.\.\.[A-Za-z_$]/;
3129
5722
  var SECRET_POSITIVE = "apiKey";
3130
5723
  var SECRET_NEGATIVE = "customerId";
5724
+ var PII_POSITIVE = "email";
5725
+ var PII_NEGATIVE = "customerId";
5726
+ var INTERNAL_POSITIVE = "hostname";
5727
+ var INTERNAL_NEGATIVE = "toolName";
3131
5728
  var STACK_POSITIVE = "error.stack";
3132
5729
  var STACK_NEGATIVE = "error.message";
3133
5730
  function dServer6PositiveControls() {
@@ -3137,6 +5734,16 @@ function dServer6PositiveControls() {
3137
5734
  positive: SECRET_RE.test(SECRET_POSITIVE),
3138
5735
  negative: SECRET_RE.test(SECRET_NEGATIVE)
3139
5736
  },
5737
+ pii: {
5738
+ pattern: PII_RE.source,
5739
+ positive: PII_RE.test(PII_POSITIVE),
5740
+ negative: PII_RE.test(PII_NEGATIVE)
5741
+ },
5742
+ internal: {
5743
+ pattern: INTERNAL_RE.source,
5744
+ positive: INTERNAL_RE.test(INTERNAL_POSITIVE),
5745
+ negative: INTERNAL_RE.test(INTERNAL_NEGATIVE)
5746
+ },
3140
5747
  stack: {
3141
5748
  pattern: STACK_RE.source,
3142
5749
  positive: STACK_RE.test(STACK_POSITIVE),
@@ -3186,8 +5793,11 @@ function collectLogCalls(file) {
3186
5793
  }
3187
5794
  return { calls, unbalanced };
3188
5795
  }
5796
+ var PIN10 = "a74a042b53c71a095ab88b3073bd47728eeda814";
3189
5797
  var dServer6 = {
3190
5798
  id: "DServer6 secret-hygiene",
5799
+ rule: "C-6",
5800
+ pin: PIN10,
3191
5801
  run(tree) {
3192
5802
  const controls = dServer6PositiveControls();
3193
5803
  if (!controls.secret.positive || controls.secret.negative) {
@@ -3195,6 +5805,16 @@ var dServer6 = {
3195
5805
  `DServer6 secret control FAILED: /${controls.secret.pattern}/ accept(\`${SECRET_POSITIVE}\`)=${controls.secret.positive} reject(\`${SECRET_NEGATIVE}\`)=${!controls.secret.negative} \u2014 matcher broken, verdict untrustworthy`
3196
5806
  );
3197
5807
  }
5808
+ if (!controls.pii.positive || controls.pii.negative) {
5809
+ throw new Error(
5810
+ `DServer6 PII control FAILED: /${controls.pii.pattern}/ accept(\`${PII_POSITIVE}\`)=${controls.pii.positive} reject(\`${PII_NEGATIVE}\`)=${!controls.pii.negative} \u2014 matcher broken, verdict untrustworthy`
5811
+ );
5812
+ }
5813
+ if (!controls.internal.positive || controls.internal.negative) {
5814
+ throw new Error(
5815
+ `DServer6 internal-detail control FAILED: /${controls.internal.pattern}/ accept(\`${INTERNAL_POSITIVE}\`)=${controls.internal.positive} reject(\`${INTERNAL_NEGATIVE}\`)=${!controls.internal.negative} \u2014 matcher broken, verdict untrustworthy`
5816
+ );
5817
+ }
3198
5818
  if (!controls.stack.positive || controls.stack.negative) {
3199
5819
  throw new Error(
3200
5820
  `DServer6 stack control FAILED: /${controls.stack.pattern}/ accept(\`${STACK_POSITIVE}\`)=${controls.stack.positive} reject(\`${STACK_NEGATIVE}\`)=${!controls.stack.negative} \u2014 matcher broken, verdict untrustworthy`
@@ -3204,21 +5824,38 @@ var dServer6 = {
3204
5824
  const { calls, unbalanced } = collectLogCalls(file);
3205
5825
  if (unbalanced.length > 0) {
3206
5826
  throw new Error(
3207
- `DServer6 could not read the argument list of ${unbalanced.length} log call(s): ${unbalanced.join(", ")} \u2014 parentheses never balance, secret content unverifiable (Server Rule 6). Fail-closed, not a silent present.`
5827
+ `DServer6 could not read the argument list of ${unbalanced.length} log call(s): ${unbalanced.join(", ")} \u2014 parentheses never balance, secret/PII content unverifiable (C-6). Fail-closed, not a silent present.`
3208
5828
  );
3209
5829
  }
3210
5830
  for (const call of calls) {
3211
5831
  if (SPREAD_RE.test(call.argText)) {
3212
5832
  throw new Error(
3213
- `DServer6 cannot inspect the spread argument of the log call at ${call.anchor} \u2014 a spread hides arbitrary content, secret presence unverifiable (Server Rule 6). Fail-closed, not a silent present.`
5833
+ `DServer6 cannot inspect the spread argument of the log call at ${call.anchor} \u2014 a spread hides arbitrary content, secret/PII presence unverifiable (C-6). Fail-closed, not a silent present.`
3214
5834
  );
3215
5835
  }
3216
- if (SECRET_RE.test(referencedIdentifiers(call.argText))) {
5836
+ const refs = referencedIdentifiers(call.argText);
5837
+ if (SECRET_RE.test(refs)) {
5838
+ return {
5839
+ id: this.id,
5840
+ anchor: call.anchor,
5841
+ state: "non-conforming",
5842
+ verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (C-6)"
5843
+ };
5844
+ }
5845
+ if (PII_RE.test(refs)) {
5846
+ return {
5847
+ id: this.id,
5848
+ anchor: call.anchor,
5849
+ state: "non-conforming",
5850
+ verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
5851
+ };
5852
+ }
5853
+ if (INTERNAL_RE.test(refs)) {
3217
5854
  return {
3218
5855
  id: this.id,
3219
5856
  anchor: call.anchor,
3220
5857
  state: "non-conforming",
3221
- verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (Server Rule 6)"
5858
+ verdict: "Form D \u2014 log call references an internal-infrastructure identifier (positive-control OK) -> never log internal system detail; redact before logging (C-6)"
3222
5859
  };
3223
5860
  }
3224
5861
  }
@@ -3231,7 +5868,7 @@ var dServer6 = {
3231
5868
  id: this.id,
3232
5869
  anchor: `${file.rel}:${line.n}`,
3233
5870
  state: "non-conforming",
3234
- verdict: "Form B \u2014 raw error.stack serialized into a client-facing response (positive-control OK) -> return an McpError message, never the raw stack (Server Rule 6)"
5871
+ verdict: "Form B \u2014 raw error.stack serialized into a client-facing response (positive-control OK) -> return an McpError message, never the raw stack (C-6)"
3235
5872
  };
3236
5873
  }
3237
5874
  }
@@ -3240,7 +5877,7 @@ var dServer6 = {
3240
5877
  id: this.id,
3241
5878
  anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
3242
5879
  state: "present",
3243
- verdict: "no secret referenced in any log call; no raw error.stack exposed to the client (both positive-controls OK)"
5880
+ verdict: "no secret/PII/internal-detail referenced in any log call; no raw error.stack exposed to the client (all positive-controls OK). Logging-sink configuration and explicit-activation-only diagnostic mode are the gate/runtime half, out of static scope (declared)."
3244
5881
  };
3245
5882
  }
3246
5883
  };
@@ -3568,6 +6205,24 @@ var detectors = [
3568
6205
  d12,
3569
6206
  d15,
3570
6207
  d16,
6208
+ c7ArtifactVerifiable,
6209
+ c8AuthorityDeclared,
6210
+ c9VerbDeclared,
6211
+ c10MechanismDeclared,
6212
+ c11Pagination,
6213
+ c12NoDuplicates,
6214
+ c13ContextCeiling,
6215
+ c14CatalogRegistration,
6216
+ c15AnnotationsUntrusted,
6217
+ c16UntrustedOutput,
6218
+ c17SupplyChain,
6219
+ c18StructureManifest,
6220
+ c19ServedDescriptions,
6221
+ c20LocalesDerived,
6222
+ c21ProgressiveDisclosure,
6223
+ c22TestRatioGate,
6224
+ c25BuildIdentity,
6225
+ c26SubpackageLockfile,
3571
6226
  dServer2,
3572
6227
  dServer6,
3573
6228
  dDebugOptIn,
@@ -3585,19 +6240,19 @@ var detectors = [
3585
6240
  ];
3586
6241
 
3587
6242
  // src/runtime/launch.ts
3588
- import { spawnSync } from "child_process";
3589
- import { existsSync as existsSync8, readFileSync as readFileSync11, statSync as statSync10 } from "fs";
3590
- import { dirname as dirname8, isAbsolute, join as join13, resolve } from "path";
6243
+ import { spawnSync as spawnSync2 } from "child_process";
6244
+ import { existsSync as existsSync15, readFileSync as readFileSync18, statSync as statSync13 } from "fs";
6245
+ import { dirname as dirname10, isAbsolute, join as join20, resolve } from "path";
3591
6246
  function findPackageDir(target) {
3592
6247
  let dir;
3593
6248
  try {
3594
- dir = statSync10(target).isDirectory() ? target : dirname8(target);
6249
+ dir = statSync13(target).isDirectory() ? target : dirname10(target);
3595
6250
  } catch {
3596
6251
  return null;
3597
6252
  }
3598
6253
  for (let depth = 0; depth < 8; depth++) {
3599
- if (existsSync8(join13(dir, "package.json"))) return dir;
3600
- const parent = dirname8(dir);
6254
+ if (existsSync15(join20(dir, "package.json"))) return dir;
6255
+ const parent = dirname10(dir);
3601
6256
  if (parent === dir) break;
3602
6257
  dir = parent;
3603
6258
  }
@@ -3621,10 +6276,10 @@ function deriveLaunchPlan(target) {
3621
6276
  }
3622
6277
  let pkg;
3623
6278
  try {
3624
- pkg = JSON.parse(readFileSync11(join13(pkgDir, "package.json"), "utf8"));
6279
+ pkg = JSON.parse(readFileSync18(join20(pkgDir, "package.json"), "utf8"));
3625
6280
  } catch (err) {
3626
6281
  return {
3627
- error: `could not parse ${join13(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
6282
+ error: `could not parse ${join20(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
3628
6283
  };
3629
6284
  }
3630
6285
  const bin = firstBinEntry(pkg.bin);
@@ -3663,6 +6318,7 @@ try {
3663
6318
  done({ launchError: "spawn failed: " + (e && e.message ? e.message : String(e)) });
3664
6319
  }
3665
6320
  let settled = false;
6321
+ let serverInfo;
3666
6322
  const fail = (msg) => { if (settled) return; settled = true; try { child.kill(); } catch {} done({ launchError: msg }); };
3667
6323
  const to = setTimeout(() => fail("timeout: server did not answer initialize+tools/list within the deadline"), 12000);
3668
6324
  child.on("error", (e) => fail("spawn error: " + (e && e.message ? e.message : String(e))));
@@ -3677,13 +6333,19 @@ child.stdout.on("data", (d) => {
3677
6333
  if (!line.trim()) continue;
3678
6334
  let msg; try { msg = JSON.parse(line); } catch { continue; }
3679
6335
  if (msg.id === 1) {
6336
+ // The initialize response's serverInfo is the SAME non-authenticated
6337
+ // discovery surface a client reads version from (S-7); captured here
6338
+ // so C-25's dynamic half can read a served build-identity field
6339
+ // alongside it, additively -- every existing consumer of the
6340
+ // tools-only introspector output is unaffected.
6341
+ serverInfo = msg.result && msg.result.serverInfo;
3680
6342
  send({ jsonrpc: "2.0", method: "notifications/initialized" });
3681
6343
  send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
3682
6344
  } else if (msg.id === 2) {
3683
6345
  if (settled) return; settled = true; clearTimeout(to);
3684
- const tools = (msg.result && Array.isArray(msg.result.tools) ? msg.result.tools : []).map((t) => ({ name: String(t && t.name) }));
6346
+ const tools = (msg.result && Array.isArray(msg.result.tools) ? msg.result.tools : []).map((t) => ({ name: String(t && t.name), description: t && typeof t.description === "string" ? t.description : undefined }));
3685
6347
  try { child.kill(); } catch {}
3686
- done({ tools });
6348
+ done({ tools, serverInfo });
3687
6349
  }
3688
6350
  }
3689
6351
  });
@@ -3695,10 +6357,10 @@ function readEvalCounts(target) {
3695
6357
  if (!pkgDir) return counts;
3696
6358
  let dir = pkgDir;
3697
6359
  for (let depth = 0; depth < 8; depth++) {
3698
- const candidate = join13(dir, "evals", "evals.json");
3699
- if (existsSync8(candidate)) {
6360
+ const candidate = join20(dir, "evals", "evals.json");
6361
+ if (existsSync15(candidate)) {
3700
6362
  try {
3701
- const parsed = JSON.parse(readFileSync11(candidate, "utf8"));
6363
+ const parsed = JSON.parse(readFileSync18(candidate, "utf8"));
3702
6364
  for (const c of parsed.cases ?? []) {
3703
6365
  if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
3704
6366
  }
@@ -3706,7 +6368,7 @@ function readEvalCounts(target) {
3706
6368
  }
3707
6369
  return counts;
3708
6370
  }
3709
- const parent = dirname8(dir);
6371
+ const parent = dirname10(dir);
3710
6372
  if (parent === dir) break;
3711
6373
  dir = parent;
3712
6374
  }
@@ -3714,17 +6376,32 @@ function readEvalCounts(target) {
3714
6376
  }
3715
6377
  function statSyncSafe(p) {
3716
6378
  try {
3717
- return statSync10(p).isDirectory();
6379
+ return statSync13(p).isDirectory();
3718
6380
  } catch {
3719
6381
  return false;
3720
6382
  }
3721
6383
  }
6384
+ function deriveExpectedCommit(cwd) {
6385
+ try {
6386
+ const out = spawnSync2("git", ["rev-parse", "HEAD"], {
6387
+ cwd,
6388
+ encoding: "utf8",
6389
+ timeout: 5e3
6390
+ });
6391
+ if (out.status === 0 && typeof out.stdout === "string") {
6392
+ const sha = out.stdout.trim();
6393
+ if (/^[0-9a-f]{40}$/i.test(sha)) return sha;
6394
+ }
6395
+ } catch {
6396
+ }
6397
+ return void 0;
6398
+ }
3722
6399
  function launchAndIntrospect(target) {
3723
6400
  const plan = deriveLaunchPlan(target);
3724
6401
  if ("error" in plan) {
3725
6402
  return { root: target, tools: [], launchError: plan.error };
3726
6403
  }
3727
- const proc = spawnSync(process.execPath, ["--input-type=module", "-e", INTROSPECTOR], {
6404
+ const proc = spawnSync2(process.execPath, ["--input-type=module", "-e", INTROSPECTOR], {
3728
6405
  env: { ...process.env, MCP_DOCTOR_PLAN: JSON.stringify(plan) },
3729
6406
  encoding: "utf8",
3730
6407
  timeout: 2e4,
@@ -3763,82 +6440,16 @@ function launchAndIntrospect(target) {
3763
6440
  const tools = (parsed.tools ?? []).map((t) => {
3764
6441
  const name = t.name;
3765
6442
  const count = evalCounts.get(name);
3766
- return count === void 0 ? { name } : { name, evalCaseCount: count };
6443
+ const base = t.description ? { name, description: t.description } : { name };
6444
+ return count === void 0 ? base : { ...base, evalCaseCount: count };
3767
6445
  });
3768
- return { root: target, tools };
3769
- }
3770
-
3771
- // src/scan.ts
3772
- import { existsSync as existsSync9, readFileSync as readFileSync12, readdirSync as readdirSync9, statSync as statSync11 } from "fs";
3773
- import { dirname as dirname9, join as join14, relative as relative2 } from "path";
3774
- var SKIP_DIRS9 = /* @__PURE__ */ new Set([
3775
- "node_modules",
3776
- "dist",
3777
- "build",
3778
- ".git",
3779
- "tests",
3780
- "__tests__",
3781
- "coverage"
3782
- ]);
3783
- function isCommentLine(raw) {
3784
- const t = raw.trim();
3785
- if (t === "") return false;
3786
- return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
3787
- }
3788
- function walk(dir, out) {
3789
- let entries;
3790
- try {
3791
- entries = readdirSync9(dir);
3792
- } catch {
3793
- return;
3794
- }
3795
- for (const name of entries) {
3796
- const full = join14(dir, name);
3797
- let st;
3798
- try {
3799
- st = statSync11(full);
3800
- } catch {
3801
- continue;
3802
- }
3803
- if (st.isDirectory()) {
3804
- if (SKIP_DIRS9.has(name)) continue;
3805
- walk(full, out);
3806
- } else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
3807
- if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
3808
- out.push(full);
3809
- }
3810
- }
3811
- }
3812
- function loadFile(path, root) {
3813
- const raw = readFileSync12(path, "utf8");
3814
- const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
3815
- return { path, rel: relative2(root, path) || path, lines };
3816
- }
3817
- function findPackageJson(startDir) {
3818
- let dir = startDir;
3819
- for (let depth = 0; depth < 8; depth++) {
3820
- const candidate = join14(dir, "package.json");
3821
- if (existsSync9(candidate)) {
3822
- try {
3823
- return { path: candidate, json: JSON.parse(readFileSync12(candidate, "utf8")) };
3824
- } catch {
3825
- return { path: candidate, json: {} };
3826
- }
3827
- }
3828
- const parent = dirname9(dir);
3829
- if (parent === dir) break;
3830
- dir = parent;
3831
- }
3832
- return null;
3833
- }
3834
- function loadTree(root) {
3835
- const st = statSync11(root);
3836
- const baseDir = st.isDirectory() ? root : dirname9(root);
3837
- const filePaths = [];
3838
- if (st.isDirectory()) walk(root, filePaths);
3839
- else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
3840
- const files = filePaths.map((p) => loadFile(p, baseDir));
3841
- return { root, files, packageJson: findPackageJson(baseDir) };
6446
+ const expectedCommit = deriveExpectedCommit(plan.cwd);
6447
+ return {
6448
+ root: target,
6449
+ tools,
6450
+ ...parsed.serverInfo ? { serverInfo: parsed.serverInfo } : {},
6451
+ ...expectedCommit ? { expectedCommit } : {}
6452
+ };
3842
6453
  }
3843
6454
 
3844
6455
  // src/types.ts
@@ -3995,12 +6606,45 @@ function computeExitCode(results) {
3995
6606
  if (results.some((r) => r.state === "could-not-judge")) return 2;
3996
6607
  return 0;
3997
6608
  }
6609
+ var USAGE = [
6610
+ "usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both]",
6611
+ "",
6612
+ " --mode=static read the tree only (default)",
6613
+ " --mode=dynamic launch the server and judge what it serves",
6614
+ " --mode=both both, dynamic findings taking precedence",
6615
+ " --version print the version and exit",
6616
+ " --help print this and exit",
6617
+ "",
6618
+ "exit: 0 clean, 1 a violation, 2 an abstention with no violation"
6619
+ ].join("\n");
6620
+ function readVersion() {
6621
+ try {
6622
+ const here = dirname11(fileURLToPath(import.meta.url));
6623
+ const manifest = JSON.parse(readFileSync19(join21(here, "..", "package.json"), "utf8"));
6624
+ return manifest.version ?? null;
6625
+ } catch {
6626
+ return null;
6627
+ }
6628
+ }
3998
6629
  function main() {
3999
6630
  const args = process.argv.slice(2);
6631
+ if (args.includes("--version") || args.includes("-V")) {
6632
+ const version = readVersion();
6633
+ if (!version) {
6634
+ console.error("mcp-doctor: could not read its own version from package.json");
6635
+ process.exit(1);
6636
+ }
6637
+ console.log(version);
6638
+ return;
6639
+ }
6640
+ if (args.includes("--help") || args.includes("-h")) {
6641
+ console.log(USAGE);
6642
+ return;
6643
+ }
4000
6644
  const modeArg = args.find((a) => a.startsWith("--mode="));
4001
6645
  const target = args.find((a) => !a.startsWith("--"));
4002
6646
  if (!target) {
4003
- console.error("usage: tsx src/doctor.ts <source-tree-path> [--mode=static|dynamic|both]");
6647
+ console.error(USAGE);
4004
6648
  process.exit(1);
4005
6649
  }
4006
6650
  const mode = modeArg ? modeArg.slice("--mode=".length) : "static";
@@ -4012,7 +6656,22 @@ function main() {
4012
6656
  console.log(renderTable(target, results));
4013
6657
  process.exitCode = computeExitCode(results);
4014
6658
  }
4015
- if (import.meta.url === `file://${process.argv[1]}`) main();
6659
+ function isDirectInvocation() {
6660
+ const entry = process.argv[1];
6661
+ if (!entry) return false;
6662
+ let selfReal;
6663
+ try {
6664
+ selfReal = realpathSync(fileURLToPath(import.meta.url));
6665
+ } catch {
6666
+ return false;
6667
+ }
6668
+ try {
6669
+ return realpathSync(resolve2(entry)) === selfReal;
6670
+ } catch {
6671
+ return false;
6672
+ }
6673
+ }
6674
+ if (isDirectInvocation()) main();
4016
6675
  export {
4017
6676
  computeExitCode,
4018
6677
  isLiveDetector,