@vantageos/mcp-doctor 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/doctor.js CHANGED
@@ -1,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) {
@@ -680,9 +3160,9 @@ var d9 = {
680
3160
  verdict: `no "version" field in ${pkgPath.split("/").pop()} -> set a valid SemVer version`
681
3161
  };
682
3162
  }
683
- const projectDir = dirname2(pkgPath);
684
- const changelogPath = join2(projectDir, "CHANGELOG.md");
685
- const hasChangelog = existsSync2(changelogPath);
3163
+ const projectDir = dirname3(pkgPath);
3164
+ const changelogPath = join8(projectDir, "CHANGELOG.md");
3165
+ const hasChangelog = existsSync8(changelogPath);
686
3166
  const validSemver = SEMVER_RE.test(version);
687
3167
  if (validSemver && hasChangelog) {
688
3168
  return {
@@ -710,8 +3190,8 @@ var d9 = {
710
3190
  };
711
3191
 
712
3192
  // src/detectors/d10.ts
713
- import { existsSync as existsSync3 } from "fs";
714
- import { dirname as dirname3, join as join3 } from "path";
3193
+ import { existsSync as existsSync9 } from "fs";
3194
+ import { dirname as dirname4, join as join9 } from "path";
715
3195
  var DESCRIPTION_FR_RE = /"description_fr"\s*:\s*"[^"]+"/;
716
3196
  var POSITIVE_CONTROL4 = '"description_fr": "Un serveur MCP conforme.",';
717
3197
  function d10PositiveControl() {
@@ -761,6 +3241,8 @@ function scanSourceDescriptionFr(tree) {
761
3241
  }
762
3242
  var d10 = {
763
3243
  id: "D10 bilingual",
3244
+ rule: "C-1",
3245
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
764
3246
  run(tree) {
765
3247
  const control = d10PositiveControl();
766
3248
  if (!control.matched) {
@@ -784,7 +3266,7 @@ var d10 = {
784
3266
  };
785
3267
  }
786
3268
  const { path: pkgPath, json: pkg } = tree.packageJson;
787
- const root = dirname3(pkgPath);
3269
+ const root = dirname4(pkgPath);
788
3270
  const descriptionFr = typeof pkg.description_fr === "string" ? pkg.description_fr.trim() : "";
789
3271
  const hasDescriptionFrJson = descriptionFr.length > 0;
790
3272
  const sourceScan = scanSourceDescriptionFr(tree);
@@ -798,8 +3280,8 @@ var d10 = {
798
3280
  };
799
3281
  }
800
3282
  const hasDescriptionFr = hasDescriptionFrJson || sourceScan.present;
801
- const readmeFrPath = join3(root, "README.fr.md");
802
- const hasReadmeFr = existsSync3(readmeFrPath);
3283
+ const readmeFrPath = join9(root, "README.fr.md");
3284
+ const hasReadmeFr = existsSync9(readmeFrPath);
803
3285
  const missing = [];
804
3286
  if (!hasDescriptionFr) missing.push("description_fr (package.json or source _meta)");
805
3287
  if (!hasReadmeFr) missing.push("README.fr.md");
@@ -807,8 +3289,8 @@ var d10 = {
807
3289
  return {
808
3290
  id: this.id,
809
3291
  anchor: `${pkgPath}`,
810
- state: "absent",
811
- verdict: `bilingual release missing: ${missing.join(", ")} -> no release without FR description + README.fr.md`
3292
+ state: "non-conforming",
3293
+ verdict: `bilingual release missing: ${missing.join(", ")} -> no release without FR description + README.fr.md (C-1)`
812
3294
  };
813
3295
  }
814
3296
  const files = Array.isArray(pkg.files) ? pkg.files : null;
@@ -857,6 +3339,8 @@ function d11PositiveControls() {
857
3339
  }
858
3340
  var d11 = {
859
3341
  id: "D11 zod-schemas",
3342
+ rule: "C-4",
3343
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
860
3344
  run(tree) {
861
3345
  const controls = d11PositiveControls();
862
3346
  const failed = Object.entries(controls).find(([, c]) => !c.matched);
@@ -908,8 +3392,8 @@ var d11 = {
908
3392
  return {
909
3393
  id: this.id,
910
3394
  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)`
3395
+ state: "non-conforming",
3396
+ 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
3397
  };
914
3398
  }
915
3399
  return {
@@ -1178,8 +3662,14 @@ var d15 = {
1178
3662
  };
1179
3663
 
1180
3664
  // src/detectors/d16.ts
3665
+ import { existsSync as existsSync10, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
3666
+ import { dirname as dirname5, join as join10 } from "path";
1181
3667
  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/;
3668
+ var HEALTH_ROUTE_FRAMEWORK_RE = /\b[A-Za-z_$][\w$]*\.(?:get|post|put|patch|delete|all|route|use)\s*\(\s*["'`][^"'`]*\/health/;
3669
+ var HEALTH_ROUTE_RAWPATH_RE = /\bpathname\s*===?\s*["'`][^"'`]*\/health/;
3670
+ var HEALTH_ROUTE_RE = new RegExp(
3671
+ `${HEALTH_ROUTE_FRAMEWORK_RE.source}|${HEALTH_ROUTE_RAWPATH_RE.source}`
3672
+ );
1183
3673
  var VERSION_FIELD_RE = /\bversion\s*:/;
1184
3674
  var PKG_SOURCE_RE = /(?:pkg|pkgJson|packageJson|pjson|packageInfo)\.version|process\.env\.npm_package_version|["'`][^"'`]*package\.json["'`]/;
1185
3675
  var PKG_IMPORT_IMPORT_RE = /import\s+(?:\*\s+as\s+)?([A-Za-z_$][\w$]*)\s+from\s+["'`][^"'`]*package\.json["'`]/;
@@ -1196,11 +3686,20 @@ var CONTROLS2 = [
1196
3686
  negative: 'method: "tools/list"'
1197
3687
  },
1198
3688
  {
1199
- key: "health",
3689
+ key: "health-framework",
1200
3690
  re: HEALTH_ROUTE_RE,
1201
3691
  positive: 'app.get("/health", handler)',
1202
3692
  negative: 'app.get("/tools", handler)'
1203
3693
  },
3694
+ {
3695
+ // FORM 2 — the raw-pathname comparison the old matcher was blind to.
3696
+ // Positive is the REAL vantage-registry-mcp:297 shape; negative is the
3697
+ // sibling `/mcp` route on the same server (a near-miss that must NOT match).
3698
+ key: "health-rawpath",
3699
+ re: HEALTH_ROUTE_RE,
3700
+ positive: 'if (req.method === "GET" && url.pathname === "/health") {',
3701
+ negative: 'if (url.pathname === "/mcp") {'
3702
+ },
1204
3703
  {
1205
3704
  key: "pkg-source",
1206
3705
  re: PKG_SOURCE_RE,
@@ -1308,7 +3807,7 @@ var d16 = {
1308
3807
  id: this.id,
1309
3808
  anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
1310
3809
  state: "absent",
1311
- verdict: "no server/discover handler and no /health route (n/a for Server Rule 16)"
3810
+ verdict: "[static read] no server/discover handler and no /health route (n/a for Server Rule 16)"
1312
3811
  };
1313
3812
  }
1314
3813
  const present = surfaces.find((s) => s.kind === "present");
@@ -1317,7 +3816,7 @@ var d16 = {
1317
3816
  id: this.id,
1318
3817
  anchor: present.anchor,
1319
3818
  state: "present",
1320
- verdict: `version exposed pre-auth at ${present.anchor}, sourced from package.json (Server Rule 16)`
3819
+ verdict: `[static read] version exposed pre-auth at ${present.anchor}, sourced from package.json (Server Rule 16)`
1321
3820
  };
1322
3821
  }
1323
3822
  const decisive = surfaces.find(
@@ -1338,7 +3837,7 @@ var d16 = {
1338
3837
  id: this.id,
1339
3838
  anchor: decisive.anchor,
1340
3839
  state: "non-conforming",
1341
- verdict: `${fix[decisive.kind]} at ${decisive.anchor} (Server Rule 16)`
3840
+ verdict: `[static read] ${fix[decisive.kind]} at ${decisive.anchor} (Server Rule 16)`
1342
3841
  };
1343
3842
  }
1344
3843
  const unreadable = surfaces[0];
@@ -1352,7 +3851,7 @@ var d16 = {
1352
3851
  } else {
1353
3852
  detail = "is present but its source is not a resolvable package.json binding in the read window";
1354
3853
  }
1355
- const reason = `surface at ${unreadable.anchor} ${detail} \u2014 verdict unreadable statically (Server Rule 16)`;
3854
+ const reason = `[static read] surface at ${unreadable.anchor} ${detail} \u2014 verdict unreadable statically (Server Rule 16)`;
1356
3855
  return {
1357
3856
  id: this.id,
1358
3857
  anchor: unreadable.anchor,
@@ -1360,12 +3859,97 @@ var d16 = {
1360
3859
  reason,
1361
3860
  verdict: `could not judge: ${reason}`
1362
3861
  };
3862
+ },
3863
+ /**
3864
+ * DYNAMIC — Rule 16 judged from what the RUNNING server SERVES, its true home.
3865
+ * The MCP `initialize` handshake returns `serverInfo.version` BEFORE any auth
3866
+ * token is presented, so the served version is a pre-auth observable by
3867
+ * construction. The served value is the truth: it is compared to the target's
3868
+ * own package.json version (the canonical source Rule 16 requires).
3869
+ *
3870
+ * present — the server serves a version AND it equals package.json
3871
+ * (observable pre-auth, sourced from the real version).
3872
+ * non-conforming — the server serves NO version, OR serves a version that
3873
+ * DRIFTS from package.json (a hardcoded literal is the
3874
+ * classic cause; the served value != the package's truth).
3875
+ * could-not-judge — the server could not be launched/introspected (naming
3876
+ * the failure), or its package.json could not be read to
3877
+ * establish the canonical version to compare against.
3878
+ *
3879
+ * Every dynamic verdict says out loud it was produced by asking the running
3880
+ * server ("[dynamic]"), so it is NEVER mistaken for the static fallback.
3881
+ */
3882
+ runDynamic(runtime) {
3883
+ if (runtime.launchError) {
3884
+ const reason = `[dynamic] server could not be started/introspected \u2014 served version unobservable: ${runtime.launchError} (Server Rule 16)`;
3885
+ return {
3886
+ id: this.id,
3887
+ anchor: `${runtime.root} (runtime)`,
3888
+ state: "could-not-judge",
3889
+ reason,
3890
+ verdict: `could not judge: ${reason}`
3891
+ };
3892
+ }
3893
+ const servedRaw = runtime.serverInfo?.version;
3894
+ const served = typeof servedRaw === "string" ? servedRaw.trim() : void 0;
3895
+ const anchor = `${runtime.root} (runtime initialize)`;
3896
+ if (!served) {
3897
+ return {
3898
+ id: this.id,
3899
+ anchor,
3900
+ state: "non-conforming",
3901
+ verdict: "[dynamic] the running server serves NO version on the pre-auth initialize handshake -> expose a version sourced from package.json (Server Rule 16)"
3902
+ };
3903
+ }
3904
+ const pkgVersion = readPackageVersion(runtime.root);
3905
+ if (!pkgVersion) {
3906
+ 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)`;
3907
+ return {
3908
+ id: this.id,
3909
+ anchor,
3910
+ state: "could-not-judge",
3911
+ reason,
3912
+ verdict: `could not judge: ${reason}`
3913
+ };
3914
+ }
3915
+ if (served === pkgVersion) {
3916
+ return {
3917
+ id: this.id,
3918
+ anchor,
3919
+ state: "present",
3920
+ verdict: `[dynamic] running server serves version "${served}" pre-auth on initialize, matching package.json "${pkgVersion}" (Server Rule 16)`
3921
+ };
3922
+ }
3923
+ return {
3924
+ id: this.id,
3925
+ anchor,
3926
+ state: "non-conforming",
3927
+ 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)`
3928
+ };
1363
3929
  }
1364
3930
  };
3931
+ function readPackageVersion(root) {
3932
+ try {
3933
+ let dir = statSync4(root).isDirectory() ? root : dirname5(root);
3934
+ for (let depth = 0; depth < 8; depth++) {
3935
+ const candidate = join10(dir, "package.json");
3936
+ if (existsSync10(candidate)) {
3937
+ const pkg = JSON.parse(readFileSync8(candidate, "utf8"));
3938
+ return typeof pkg.version === "string" ? pkg.version : null;
3939
+ }
3940
+ const parent = dirname5(dir);
3941
+ if (parent === dir) break;
3942
+ dir = parent;
3943
+ }
3944
+ } catch {
3945
+ return null;
3946
+ }
3947
+ return null;
3948
+ }
1365
3949
 
1366
3950
  // src/detectors/dAppI18n.ts
1367
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1368
- import { dirname as dirname4, join as join4 } from "path";
3951
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
3952
+ import { dirname as dirname6, join as join11 } from "path";
1369
3953
  function flattenKeys(obj, prefix = "") {
1370
3954
  if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
1371
3955
  return prefix ? [prefix] : [];
@@ -1396,9 +3980,9 @@ function dAppI18nPositiveControls() {
1396
3980
  }
1397
3981
  };
1398
3982
  }
1399
- function readJson(path) {
3983
+ function readJson2(path) {
1400
3984
  try {
1401
- return { ok: true, value: JSON.parse(readFileSync2(path, "utf8")) };
3985
+ return { ok: true, value: JSON.parse(readFileSync9(path, "utf8")) };
1402
3986
  } catch {
1403
3987
  return { ok: false };
1404
3988
  }
@@ -1412,12 +3996,12 @@ var dAppI18n = {
1412
3996
  `DAppI18n key-diff control FAILED: detectsGap=${control.detectsGap} detectsComplete=${control.detectsComplete} \u2014 matcher broken, verdict untrustworthy`
1413
3997
  );
1414
3998
  }
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);
3999
+ const root = tree.packageJson ? dirname6(tree.packageJson.path) : tree.root;
4000
+ const i18nDir = join11(root, "i18n");
4001
+ const enPath = join11(i18nDir, "en.json");
4002
+ const frPath = join11(i18nDir, "fr.json");
4003
+ const hasI18n = existsSync11(i18nDir);
4004
+ const hasFr = existsSync11(frPath);
1421
4005
  if (!hasI18n && !hasFr) {
1422
4006
  return {
1423
4007
  id: this.id,
@@ -1434,7 +4018,7 @@ var dAppI18n = {
1434
4018
  verdict: "i18n/ layer present but i18n/fr.json is missing -> ship the French locale (App Rule 2)"
1435
4019
  };
1436
4020
  }
1437
- const frRead = readJson(frPath);
4021
+ const frRead = readJson2(frPath);
1438
4022
  if (!frRead.ok) {
1439
4023
  const reason = "i18n/fr.json is not readable JSON \u2014 cannot compare its keys against the source locale (App Rule 2)";
1440
4024
  return {
@@ -1445,7 +4029,7 @@ var dAppI18n = {
1445
4029
  verdict: `could not judge: ${reason}`
1446
4030
  };
1447
4031
  }
1448
- if (!existsSync4(enPath)) {
4032
+ if (!existsSync11(enPath)) {
1449
4033
  const reason = "i18n/fr.json exists but no source-locale i18n/en.json to compare its keys against (App Rule 2)";
1450
4034
  return {
1451
4035
  id: this.id,
@@ -1455,7 +4039,7 @@ var dAppI18n = {
1455
4039
  verdict: `could not judge: ${reason}`
1456
4040
  };
1457
4041
  }
1458
- const enRead = readJson(enPath);
4042
+ const enRead = readJson2(enPath);
1459
4043
  if (!enRead.ok) {
1460
4044
  const reason = "i18n/en.json (source locale) is not readable JSON \u2014 cannot derive the reference key set (App Rule 2)";
1461
4045
  return {
@@ -1588,46 +4172,46 @@ var dDebugOptIn = {
1588
4172
  };
1589
4173
 
1590
4174
  // 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-]+$/;
4175
+ import { readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4176
+ import { join as join12 } from "path";
4177
+ var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
1594
4178
  var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
1595
4179
  var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
1596
4180
  function dExt1PositiveControls() {
1597
4181
  return {
1598
4182
  identifier: {
1599
- pattern: IDENTIFIER_RE.source,
1600
- positive: IDENTIFIER_RE.test(IDENTIFIER_POSITIVE),
1601
- negative: IDENTIFIER_RE.test(IDENTIFIER_NEGATIVE)
4183
+ pattern: IDENTIFIER_RE2.source,
4184
+ positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
4185
+ negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
1602
4186
  }
1603
4187
  };
1604
4188
  }
1605
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4189
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
1606
4190
  function findExtensionManifests(root) {
1607
4191
  const out = [];
1608
4192
  let base;
1609
4193
  try {
1610
- base = statSync2(root).isDirectory() ? root : join5(root, "..");
4194
+ base = statSync5(root).isDirectory() ? root : join12(root, "..");
1611
4195
  } catch {
1612
4196
  return out;
1613
4197
  }
1614
4198
  const walk2 = (dir) => {
1615
4199
  let entries;
1616
4200
  try {
1617
- entries = readdirSync(dir);
4201
+ entries = readdirSync4(dir);
1618
4202
  } catch {
1619
4203
  return;
1620
4204
  }
1621
4205
  for (const name of entries) {
1622
- const full = join5(dir, name);
4206
+ const full = join12(dir, name);
1623
4207
  let st;
1624
4208
  try {
1625
- st = statSync2(full);
4209
+ st = statSync5(full);
1626
4210
  } catch {
1627
4211
  continue;
1628
4212
  }
1629
4213
  if (st.isDirectory()) {
1630
- if (SKIP_DIRS.has(name)) continue;
4214
+ if (SKIP_DIRS2.has(name)) continue;
1631
4215
  walk2(full);
1632
4216
  } else if (name === "extension.json") {
1633
4217
  out.push(full);
@@ -1667,11 +4251,11 @@ var dExt1 = {
1667
4251
  );
1668
4252
  }
1669
4253
  const manifests = findExtensionManifests(tree.root);
1670
- for (const manifestPath of manifests) {
1671
- const shortPath = manifestPath.split("/").slice(-2).join("/");
4254
+ for (const manifestPath2 of manifests) {
4255
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
1672
4256
  let parsed;
1673
4257
  try {
1674
- parsed = JSON.parse(readFileSync3(manifestPath, "utf8"));
4258
+ parsed = JSON.parse(readFileSync10(manifestPath2, "utf8"));
1675
4259
  } catch {
1676
4260
  throw new Error(
1677
4261
  `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 +4267,7 @@ var dExt1 = {
1683
4267
  `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
4268
  );
1685
4269
  }
1686
- if (!IDENTIFIER_RE.test(idValue)) {
4270
+ if (!IDENTIFIER_RE2.test(idValue)) {
1687
4271
  return {
1688
4272
  id: this.id,
1689
4273
  anchor: shortPath,
@@ -1699,7 +4283,7 @@ var dExt1 = {
1699
4283
  );
1700
4284
  }
1701
4285
  for (const site of sites) {
1702
- if (!IDENTIFIER_RE.test(site.value)) {
4286
+ if (!IDENTIFIER_RE2.test(site.value)) {
1703
4287
  return {
1704
4288
  id: this.id,
1705
4289
  anchor: site.anchor,
@@ -1727,8 +4311,8 @@ var dExt1 = {
1727
4311
  };
1728
4312
 
1729
4313
  // src/detectors/dExt2.ts
1730
- import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
1731
- import { join as join6 } from "path";
4314
+ import { readFileSync as readFileSync11, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
4315
+ import { join as join13 } from "path";
1732
4316
  var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
1733
4317
  var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
1734
4318
  var TS_GD_KEY_RE = /\bgracefulDegradation:\s*([^\s,}]+)/;
@@ -1743,32 +4327,32 @@ function dExt2PositiveControls() {
1743
4327
  }
1744
4328
  };
1745
4329
  }
1746
- var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4330
+ var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
1747
4331
  function findExtensionManifests2(root) {
1748
4332
  const out = [];
1749
4333
  let base;
1750
4334
  try {
1751
- base = statSync3(root).isDirectory() ? root : join6(root, "..");
4335
+ base = statSync6(root).isDirectory() ? root : join13(root, "..");
1752
4336
  } catch {
1753
4337
  return out;
1754
4338
  }
1755
4339
  const walk2 = (dir) => {
1756
4340
  let entries;
1757
4341
  try {
1758
- entries = readdirSync2(dir);
4342
+ entries = readdirSync5(dir);
1759
4343
  } catch {
1760
4344
  return;
1761
4345
  }
1762
4346
  for (const name of entries) {
1763
- const full = join6(dir, name);
4347
+ const full = join13(dir, name);
1764
4348
  let st;
1765
4349
  try {
1766
- st = statSync3(full);
4350
+ st = statSync6(full);
1767
4351
  } catch {
1768
4352
  continue;
1769
4353
  }
1770
4354
  if (st.isDirectory()) {
1771
- if (SKIP_DIRS2.has(name)) continue;
4355
+ if (SKIP_DIRS3.has(name)) continue;
1772
4356
  walk2(full);
1773
4357
  } else if (name === "extension.json") {
1774
4358
  out.push(full);
@@ -1828,11 +4412,11 @@ var dExt2 = {
1828
4412
  );
1829
4413
  }
1830
4414
  const manifests = findExtensionManifests2(tree.root);
1831
- for (const manifestPath of manifests) {
1832
- const shortPath = manifestPath.split("/").slice(-2).join("/");
4415
+ for (const manifestPath2 of manifests) {
4416
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
1833
4417
  let parsed;
1834
4418
  try {
1835
- parsed = JSON.parse(readFileSync4(manifestPath, "utf8"));
4419
+ parsed = JSON.parse(readFileSync11(manifestPath2, "utf8"));
1836
4420
  } catch {
1837
4421
  throw new Error(
1838
4422
  `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 +4480,8 @@ var dExt2 = {
1896
4480
  };
1897
4481
 
1898
4482
  // src/detectors/dExt3.ts
1899
- import { readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
1900
- import { join as join7 } from "path";
4483
+ import { readFileSync as readFileSync12, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
4484
+ import { join as join14 } from "path";
1901
4485
  var TYPE_MATCHERS = [
1902
4486
  { type: "middleware", re: /\bmiddleware\b/ },
1903
4487
  { type: "tool-extension", re: /\btool-ext(?:ension)?\b/ },
@@ -1920,32 +4504,32 @@ function dExt3PositiveControls() {
1920
4504
  }
1921
4505
  };
1922
4506
  }
1923
- var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4507
+ var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
1924
4508
  function findExtensionManifests3(root) {
1925
4509
  const out = [];
1926
4510
  let base;
1927
4511
  try {
1928
- base = statSync4(root).isDirectory() ? root : join7(root, "..");
4512
+ base = statSync7(root).isDirectory() ? root : join14(root, "..");
1929
4513
  } catch {
1930
4514
  return out;
1931
4515
  }
1932
4516
  const walk2 = (dir) => {
1933
4517
  let entries;
1934
4518
  try {
1935
- entries = readdirSync3(dir);
4519
+ entries = readdirSync6(dir);
1936
4520
  } catch {
1937
4521
  return;
1938
4522
  }
1939
4523
  for (const name of entries) {
1940
- const full = join7(dir, name);
4524
+ const full = join14(dir, name);
1941
4525
  let st;
1942
4526
  try {
1943
- st = statSync4(full);
4527
+ st = statSync7(full);
1944
4528
  } catch {
1945
4529
  continue;
1946
4530
  }
1947
4531
  if (st.isDirectory()) {
1948
- if (SKIP_DIRS3.has(name)) continue;
4532
+ if (SKIP_DIRS4.has(name)) continue;
1949
4533
  walk2(full);
1950
4534
  } else if (name === "extension.json") {
1951
4535
  out.push(full);
@@ -2013,11 +4597,11 @@ var dExt3 = {
2013
4597
  const declared = /* @__PURE__ */ new Set();
2014
4598
  let firstAnchor2 = null;
2015
4599
  const manifests = findExtensionManifests3(tree.root);
2016
- for (const manifestPath of manifests) {
2017
- const shortPath = manifestPath.split("/").slice(-2).join("/");
4600
+ for (const manifestPath2 of manifests) {
4601
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
2018
4602
  let parsed;
2019
4603
  try {
2020
- parsed = JSON.parse(readFileSync5(manifestPath, "utf8"));
4604
+ parsed = JSON.parse(readFileSync12(manifestPath2, "utf8"));
2021
4605
  } catch {
2022
4606
  throw new Error(
2023
4607
  `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 +4687,8 @@ var dExt3 = {
2103
4687
  };
2104
4688
 
2105
4689
  // src/detectors/dExt4.ts
2106
- import { readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
2107
- import { join as join8 } from "path";
4690
+ import { readFileSync as readFileSync13, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
4691
+ import { join as join15 } from "path";
2108
4692
  function canonical(name) {
2109
4693
  return name.replace(/[-_]/g, "").toLowerCase();
2110
4694
  }
@@ -2123,32 +4707,32 @@ function dExt4PositiveControls() {
2123
4707
  }
2124
4708
  };
2125
4709
  }
2126
- var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4710
+ var SKIP_DIRS5 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
2127
4711
  function findExtensionManifests4(root) {
2128
4712
  const out = [];
2129
4713
  let base;
2130
4714
  try {
2131
- base = statSync5(root).isDirectory() ? root : join8(root, "..");
4715
+ base = statSync8(root).isDirectory() ? root : join15(root, "..");
2132
4716
  } catch {
2133
4717
  return out;
2134
4718
  }
2135
4719
  const walk2 = (dir) => {
2136
4720
  let entries;
2137
4721
  try {
2138
- entries = readdirSync4(dir);
4722
+ entries = readdirSync7(dir);
2139
4723
  } catch {
2140
4724
  return;
2141
4725
  }
2142
4726
  for (const name of entries) {
2143
- const full = join8(dir, name);
4727
+ const full = join15(dir, name);
2144
4728
  let st;
2145
4729
  try {
2146
- st = statSync5(full);
4730
+ st = statSync8(full);
2147
4731
  } catch {
2148
4732
  continue;
2149
4733
  }
2150
4734
  if (st.isDirectory()) {
2151
- if (SKIP_DIRS4.has(name)) continue;
4735
+ if (SKIP_DIRS5.has(name)) continue;
2152
4736
  walk2(full);
2153
4737
  } else if (name === "extension.json") {
2154
4738
  out.push(full);
@@ -2162,32 +4746,32 @@ function findHooksDirs(root) {
2162
4746
  const out = [];
2163
4747
  let base;
2164
4748
  try {
2165
- base = statSync5(root).isDirectory() ? root : join8(root, "..");
4749
+ base = statSync8(root).isDirectory() ? root : join15(root, "..");
2166
4750
  } catch {
2167
4751
  return out;
2168
4752
  }
2169
4753
  const walk2 = (dir) => {
2170
4754
  let entries;
2171
4755
  try {
2172
- entries = readdirSync4(dir);
4756
+ entries = readdirSync7(dir);
2173
4757
  } catch {
2174
4758
  return;
2175
4759
  }
2176
4760
  for (const name of entries) {
2177
- const full = join8(dir, name);
4761
+ const full = join15(dir, name);
2178
4762
  let st;
2179
4763
  try {
2180
- st = statSync5(full);
4764
+ st = statSync8(full);
2181
4765
  } catch {
2182
4766
  continue;
2183
4767
  }
2184
4768
  if (!st.isDirectory()) continue;
2185
- if (SKIP_DIRS4.has(name)) continue;
4769
+ if (SKIP_DIRS5.has(name)) continue;
2186
4770
  if (name === "hooks") {
2187
4771
  const tokens = /* @__PURE__ */ new Set();
2188
4772
  let files;
2189
4773
  try {
2190
- files = readdirSync4(full);
4774
+ files = readdirSync7(full);
2191
4775
  } catch {
2192
4776
  files = [];
2193
4777
  }
@@ -2276,11 +4860,11 @@ var dExt4 = {
2276
4860
  const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
2277
4861
  const manifests = findExtensionManifests4(tree.root);
2278
4862
  let declaredAnyLifecycle = false;
2279
- for (const manifestPath of manifests) {
2280
- const shortPath = manifestPath.split("/").slice(-2).join("/");
4863
+ for (const manifestPath2 of manifests) {
4864
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
2281
4865
  let parsed;
2282
4866
  try {
2283
- parsed = JSON.parse(readFileSync6(manifestPath, "utf8"));
4867
+ parsed = JSON.parse(readFileSync13(manifestPath2, "utf8"));
2284
4868
  } catch {
2285
4869
  throw new Error(
2286
4870
  `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 +4922,8 @@ var dExt4 = {
2338
4922
  };
2339
4923
 
2340
4924
  // 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";
4925
+ import { existsSync as existsSync12, readFileSync as readFileSync14, readdirSync as readdirSync8, statSync as statSync9 } from "fs";
4926
+ import { dirname as dirname7, join as join16 } from "path";
2343
4927
  function nonEmptyString(v) {
2344
4928
  return typeof v === "string" && v.trim().length > 0;
2345
4929
  }
@@ -2353,32 +4937,32 @@ function dExt5PositiveControls() {
2353
4937
  }
2354
4938
  };
2355
4939
  }
2356
- var SKIP_DIRS5 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
4940
+ var SKIP_DIRS6 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
2357
4941
  function findExtensionManifests5(root) {
2358
4942
  const out = [];
2359
4943
  let base;
2360
4944
  try {
2361
- base = statSync6(root).isDirectory() ? root : join9(root, "..");
4945
+ base = statSync9(root).isDirectory() ? root : join16(root, "..");
2362
4946
  } catch {
2363
4947
  return out;
2364
4948
  }
2365
4949
  const walk2 = (dir) => {
2366
4950
  let entries;
2367
4951
  try {
2368
- entries = readdirSync5(dir);
4952
+ entries = readdirSync8(dir);
2369
4953
  } catch {
2370
4954
  return;
2371
4955
  }
2372
4956
  for (const name of entries) {
2373
- const full = join9(dir, name);
4957
+ const full = join16(dir, name);
2374
4958
  let st;
2375
4959
  try {
2376
- st = statSync6(full);
4960
+ st = statSync9(full);
2377
4961
  } catch {
2378
4962
  continue;
2379
4963
  }
2380
4964
  if (st.isDirectory()) {
2381
- if (SKIP_DIRS5.has(name)) continue;
4965
+ if (SKIP_DIRS6.has(name)) continue;
2382
4966
  walk2(full);
2383
4967
  } else if (name === "extension.json") {
2384
4968
  out.push(full);
@@ -2409,9 +4993,9 @@ var dExt5 = {
2409
4993
  verdict: "no extension.json manifest found -> declare a bilingual extension with description_fr + README.fr.md (Extension Rule 5)"
2410
4994
  };
2411
4995
  }
2412
- for (const manifestPath of manifests) {
2413
- const shortPath = manifestPath.split("/").slice(-2).join("/");
2414
- const root = dirname5(manifestPath);
4996
+ for (const manifestPath2 of manifests) {
4997
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
4998
+ const root = dirname7(manifestPath2);
2415
4999
  const cannotRead = (reason) => ({
2416
5000
  id: this.id,
2417
5001
  anchor: shortPath,
@@ -2421,7 +5005,7 @@ var dExt5 = {
2421
5005
  });
2422
5006
  let parsed;
2423
5007
  try {
2424
- parsed = JSON.parse(readFileSync7(manifestPath, "utf8"));
5008
+ parsed = JSON.parse(readFileSync14(manifestPath2, "utf8"));
2425
5009
  } catch {
2426
5010
  return cannotRead(
2427
5011
  `${shortPath} does not parse as JSON \u2014 the FR surface of a present manifest cannot be read (Extension Rule 5)`
@@ -2436,7 +5020,7 @@ var dExt5 = {
2436
5020
  if (!nonEmptyString(parsed.description_fr)) {
2437
5021
  missing.push("description_fr (extension.json)");
2438
5022
  }
2439
- if (!existsSync5(join9(root, "README.fr.md"))) {
5023
+ if (!existsSync12(join16(root, "README.fr.md"))) {
2440
5024
  missing.push("README.fr.md");
2441
5025
  }
2442
5026
  if (isToolExtension(parsed.type) && "tools" in parsed) {
@@ -2479,8 +5063,8 @@ var dExt5 = {
2479
5063
  };
2480
5064
 
2481
5065
  // 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";
5066
+ import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync9, statSync as statSync10 } from "fs";
5067
+ import { dirname as dirname8, join as join17 } from "path";
2484
5068
  var ABSENCE_FLAG_FIELDS = [
2485
5069
  "extensionAbsent",
2486
5070
  "extension_absent",
@@ -2523,32 +5107,32 @@ function dExt7PositiveControls() {
2523
5107
  negative: isAbsenceCase(ABSENCE_NEGATIVE)
2524
5108
  };
2525
5109
  }
2526
- var SKIP_DIRS6 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5110
+ var SKIP_DIRS7 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
2527
5111
  function findExtensionManifests6(root) {
2528
5112
  const out = [];
2529
5113
  let base;
2530
5114
  try {
2531
- base = statSync7(root).isDirectory() ? root : join10(root, "..");
5115
+ base = statSync10(root).isDirectory() ? root : join17(root, "..");
2532
5116
  } catch {
2533
5117
  return out;
2534
5118
  }
2535
5119
  const walk2 = (dir) => {
2536
5120
  let entries;
2537
5121
  try {
2538
- entries = readdirSync6(dir);
5122
+ entries = readdirSync9(dir);
2539
5123
  } catch {
2540
5124
  return;
2541
5125
  }
2542
5126
  for (const name of entries) {
2543
- const full = join10(dir, name);
5127
+ const full = join17(dir, name);
2544
5128
  let st;
2545
5129
  try {
2546
- st = statSync7(full);
5130
+ st = statSync10(full);
2547
5131
  } catch {
2548
5132
  continue;
2549
5133
  }
2550
5134
  if (st.isDirectory()) {
2551
- if (SKIP_DIRS6.has(name)) continue;
5135
+ if (SKIP_DIRS7.has(name)) continue;
2552
5136
  walk2(full);
2553
5137
  } else if (name === "extension.json") {
2554
5138
  out.push(full);
@@ -2576,12 +5160,12 @@ var dExt7 = {
2576
5160
  verdict: "no extension.json manifest found -> declare an extension with an eval suite (>=3 cases incl. 1 'extension absente') (Extension Rule 7)"
2577
5161
  };
2578
5162
  }
2579
- for (const manifestPath of manifests) {
2580
- const extDir = dirname6(manifestPath);
5163
+ for (const manifestPath2 of manifests) {
5164
+ const extDir = dirname8(manifestPath2);
2581
5165
  const extLabel = extDir.split("/").pop() ?? extDir;
2582
- const evalsPath = join10(extDir, "evals", "evals.json");
5166
+ const evalsPath = join17(extDir, "evals", "evals.json");
2583
5167
  const shortPath = `${extLabel}/evals/evals.json`;
2584
- if (!existsSync6(evalsPath)) {
5168
+ if (!existsSync13(evalsPath)) {
2585
5169
  return {
2586
5170
  id: this.id,
2587
5171
  anchor: `${extLabel}/extension.json`,
@@ -2591,7 +5175,7 @@ var dExt7 = {
2591
5175
  }
2592
5176
  let cases;
2593
5177
  try {
2594
- const raw = readFileSync8(evalsPath, "utf8");
5178
+ const raw = readFileSync15(evalsPath, "utf8");
2595
5179
  const parsed = JSON.parse(raw);
2596
5180
  if (!Array.isArray(parsed.cases)) {
2597
5181
  throw new Error("`cases` is not an array");
@@ -2634,8 +5218,8 @@ var dExt7 = {
2634
5218
  };
2635
5219
 
2636
5220
  // 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";
5221
+ import { existsSync as existsSync14, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync11 } from "fs";
5222
+ import { dirname as dirname9, join as join18 } from "path";
2639
5223
  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
5224
  var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
2641
5225
  function dExt8PositiveControl() {
@@ -2645,32 +5229,32 @@ function dExt8PositiveControl() {
2645
5229
  matched: SEMVER_RE2.test(POSITIVE_CONTROL5)
2646
5230
  };
2647
5231
  }
2648
- var SKIP_DIRS7 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5232
+ var SKIP_DIRS8 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
2649
5233
  function findExtensionManifests7(root) {
2650
5234
  const out = [];
2651
5235
  let base;
2652
5236
  try {
2653
- base = statSync8(root).isDirectory() ? root : join11(root, "..");
5237
+ base = statSync11(root).isDirectory() ? root : join18(root, "..");
2654
5238
  } catch {
2655
5239
  return out;
2656
5240
  }
2657
5241
  const walk2 = (dir) => {
2658
5242
  let entries;
2659
5243
  try {
2660
- entries = readdirSync7(dir);
5244
+ entries = readdirSync10(dir);
2661
5245
  } catch {
2662
5246
  return;
2663
5247
  }
2664
5248
  for (const name of entries) {
2665
- const full = join11(dir, name);
5249
+ const full = join18(dir, name);
2666
5250
  let st;
2667
5251
  try {
2668
- st = statSync8(full);
5252
+ st = statSync11(full);
2669
5253
  } catch {
2670
5254
  continue;
2671
5255
  }
2672
5256
  if (st.isDirectory()) {
2673
- if (SKIP_DIRS7.has(name)) continue;
5257
+ if (SKIP_DIRS8.has(name)) continue;
2674
5258
  walk2(full);
2675
5259
  } else if (name === "extension.json") {
2676
5260
  out.push(full);
@@ -2698,19 +5282,19 @@ var dExt8 = {
2698
5282
  verdict: "no extension.json manifest found -> declare an extension with a valid SemVer version + CHANGELOG.md (Extension Rule 8)"
2699
5283
  };
2700
5284
  }
2701
- for (const manifestPath of manifests) {
2702
- const extDir = dirname7(manifestPath);
5285
+ for (const manifestPath2 of manifests) {
5286
+ const extDir = dirname9(manifestPath2);
2703
5287
  const extLabel = extDir.split("/").pop() ?? extDir;
2704
- const pkgPath = join11(extDir, "package.json");
5288
+ const pkgPath = join18(extDir, "package.json");
2705
5289
  const shortPkgPath = `${extLabel}/package.json`;
2706
- if (!existsSync7(pkgPath)) {
5290
+ if (!existsSync14(pkgPath)) {
2707
5291
  throw new Error(
2708
5292
  `DExt8 could not locate ${shortPkgPath} for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8). Fail-closed, not a silent present.`
2709
5293
  );
2710
5294
  }
2711
5295
  let pkgJson;
2712
5296
  try {
2713
- pkgJson = JSON.parse(readFileSync9(pkgPath, "utf8"));
5297
+ pkgJson = JSON.parse(readFileSync16(pkgPath, "utf8"));
2714
5298
  } catch {
2715
5299
  throw new Error(
2716
5300
  `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 +5315,8 @@ var dExt8 = {
2731
5315
  };
2732
5316
  }
2733
5317
  const validSemver = SEMVER_RE2.test(version);
2734
- const changelogPath = join11(extDir, "CHANGELOG.md");
2735
- const hasChangelog = existsSync7(changelogPath);
5318
+ const changelogPath = join18(extDir, "CHANGELOG.md");
5319
+ const hasChangelog = existsSync14(changelogPath);
2736
5320
  if (!validSemver) {
2737
5321
  return {
2738
5322
  id: this.id,
@@ -2761,38 +5345,38 @@ var dExt8 = {
2761
5345
  };
2762
5346
 
2763
5347
  // 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"]);
5348
+ import { readFileSync as readFileSync17, readdirSync as readdirSync11, statSync as statSync12 } from "fs";
5349
+ import { join as join19 } from "path";
5350
+ var SKIP_DIRS9 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
2767
5351
  function collectHooksDirSources(root) {
2768
5352
  const out = [];
2769
5353
  let base;
2770
5354
  try {
2771
- base = statSync9(root).isDirectory() ? root : join12(root, "..");
5355
+ base = statSync12(root).isDirectory() ? root : join19(root, "..");
2772
5356
  } catch {
2773
5357
  return out;
2774
5358
  }
2775
5359
  const walk2 = (dir) => {
2776
5360
  let entries;
2777
5361
  try {
2778
- entries = readdirSync8(dir);
5362
+ entries = readdirSync11(dir);
2779
5363
  } catch {
2780
5364
  return;
2781
5365
  }
2782
5366
  for (const name of entries) {
2783
- const full = join12(dir, name);
5367
+ const full = join19(dir, name);
2784
5368
  let st;
2785
5369
  try {
2786
- st = statSync9(full);
5370
+ st = statSync12(full);
2787
5371
  } catch {
2788
5372
  continue;
2789
5373
  }
2790
5374
  if (!st.isDirectory()) continue;
2791
- if (SKIP_DIRS8.has(name)) continue;
5375
+ if (SKIP_DIRS9.has(name)) continue;
2792
5376
  if (name === "hooks") {
2793
5377
  let files;
2794
5378
  try {
2795
- files = readdirSync8(full);
5379
+ files = readdirSync11(full);
2796
5380
  } catch {
2797
5381
  files = [];
2798
5382
  }
@@ -2800,10 +5384,10 @@ function collectHooksDirSources(root) {
2800
5384
  if (!f.endsWith(".ts") || f.endsWith(".d.ts")) continue;
2801
5385
  const stem = f.replace(/\.ts$/, "");
2802
5386
  if (stem === "index") continue;
2803
- const filePath = join12(full, f);
5387
+ const filePath = join19(full, f);
2804
5388
  let body;
2805
5389
  try {
2806
- body = readFileSync10(filePath, "utf8");
5390
+ body = readFileSync17(filePath, "utf8");
2807
5391
  } catch {
2808
5392
  throw new Error(
2809
5393
  `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 +5494,11 @@ var dExt9 = {
2910
5494
  }
2911
5495
  const manifests = findExtensionManifests4(tree.root);
2912
5496
  let declaredAnyLifecycle = false;
2913
- for (const manifestPath of manifests) {
2914
- const shortPath = manifestPath.split("/").slice(-2).join("/");
5497
+ for (const manifestPath2 of manifests) {
5498
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
2915
5499
  let parsed;
2916
5500
  try {
2917
- parsed = JSON.parse(readFileSync10(manifestPath, "utf8"));
5501
+ parsed = JSON.parse(readFileSync17(manifestPath2, "utf8"));
2918
5502
  } catch {
2919
5503
  throw new Error(
2920
5504
  `DExt9 could not parse ${shortPath} as JSON \u2014 timeout guards undeterminable (Extension Rule 9). Fail-closed, not a silent absent.`
@@ -2992,6 +5576,8 @@ function unscopePackageName(name) {
2992
5576
  }
2993
5577
  var dServer2 = {
2994
5578
  id: "DServer2 naming",
5579
+ rule: "C-2",
5580
+ pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
2995
5581
  run(tree) {
2996
5582
  const controls = dServer2PositiveControls();
2997
5583
  if (!controls.toolSnake.positive || controls.toolSnake.negative) {
@@ -3123,11 +5709,17 @@ var dServer2 = {
3123
5709
  // src/detectors/dServer6.ts
3124
5710
  var LOG_OPENER_RE = /\b(?:console|logger|log)\.(?:log|info|warn|error|debug|trace)\s*\(/;
3125
5711
  var SECRET_RE = /\b(?:api[_-]?key|token|secret|passwd|password|authorization|auth[_-]?token|bearer|access[_-]?key|client[_-]?secret|private[_-]?key)\b/i;
5712
+ var PII_RE = /\b(?:email|e[_-]?mail|ip[_-]?address|full[_-]?name|phone[_-]?number|ssn|social[_-]?security)\b/i;
5713
+ var INTERNAL_RE = /\b(?:hostname|host[_-]?name|absolute[_-]?path|internal[_-]?path|infra[_-]?version|server[_-]?version|__dirname|__filename)\b/;
3126
5714
  var STACK_RE = /\b(?:error|err|e|ex|exception)\.stack\b/;
3127
5715
  var CLIENT_SINK_RE = /\b(?:content|text|message|body)\s*:|(?:\breturn\b|\bthrow\b|JSON\.stringify)/;
3128
5716
  var SPREAD_RE = /\.\.\.[A-Za-z_$]/;
3129
5717
  var SECRET_POSITIVE = "apiKey";
3130
5718
  var SECRET_NEGATIVE = "customerId";
5719
+ var PII_POSITIVE = "email";
5720
+ var PII_NEGATIVE = "customerId";
5721
+ var INTERNAL_POSITIVE = "hostname";
5722
+ var INTERNAL_NEGATIVE = "toolName";
3131
5723
  var STACK_POSITIVE = "error.stack";
3132
5724
  var STACK_NEGATIVE = "error.message";
3133
5725
  function dServer6PositiveControls() {
@@ -3137,6 +5729,16 @@ function dServer6PositiveControls() {
3137
5729
  positive: SECRET_RE.test(SECRET_POSITIVE),
3138
5730
  negative: SECRET_RE.test(SECRET_NEGATIVE)
3139
5731
  },
5732
+ pii: {
5733
+ pattern: PII_RE.source,
5734
+ positive: PII_RE.test(PII_POSITIVE),
5735
+ negative: PII_RE.test(PII_NEGATIVE)
5736
+ },
5737
+ internal: {
5738
+ pattern: INTERNAL_RE.source,
5739
+ positive: INTERNAL_RE.test(INTERNAL_POSITIVE),
5740
+ negative: INTERNAL_RE.test(INTERNAL_NEGATIVE)
5741
+ },
3140
5742
  stack: {
3141
5743
  pattern: STACK_RE.source,
3142
5744
  positive: STACK_RE.test(STACK_POSITIVE),
@@ -3186,8 +5788,11 @@ function collectLogCalls(file) {
3186
5788
  }
3187
5789
  return { calls, unbalanced };
3188
5790
  }
5791
+ var PIN10 = "a74a042b53c71a095ab88b3073bd47728eeda814";
3189
5792
  var dServer6 = {
3190
5793
  id: "DServer6 secret-hygiene",
5794
+ rule: "C-6",
5795
+ pin: PIN10,
3191
5796
  run(tree) {
3192
5797
  const controls = dServer6PositiveControls();
3193
5798
  if (!controls.secret.positive || controls.secret.negative) {
@@ -3195,6 +5800,16 @@ var dServer6 = {
3195
5800
  `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
5801
  );
3197
5802
  }
5803
+ if (!controls.pii.positive || controls.pii.negative) {
5804
+ throw new Error(
5805
+ `DServer6 PII control FAILED: /${controls.pii.pattern}/ accept(\`${PII_POSITIVE}\`)=${controls.pii.positive} reject(\`${PII_NEGATIVE}\`)=${!controls.pii.negative} \u2014 matcher broken, verdict untrustworthy`
5806
+ );
5807
+ }
5808
+ if (!controls.internal.positive || controls.internal.negative) {
5809
+ throw new Error(
5810
+ `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`
5811
+ );
5812
+ }
3198
5813
  if (!controls.stack.positive || controls.stack.negative) {
3199
5814
  throw new Error(
3200
5815
  `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 +5819,38 @@ var dServer6 = {
3204
5819
  const { calls, unbalanced } = collectLogCalls(file);
3205
5820
  if (unbalanced.length > 0) {
3206
5821
  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.`
5822
+ `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
5823
  );
3209
5824
  }
3210
5825
  for (const call of calls) {
3211
5826
  if (SPREAD_RE.test(call.argText)) {
3212
5827
  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.`
5828
+ `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
5829
  );
3215
5830
  }
3216
- if (SECRET_RE.test(referencedIdentifiers(call.argText))) {
5831
+ const refs = referencedIdentifiers(call.argText);
5832
+ if (SECRET_RE.test(refs)) {
5833
+ return {
5834
+ id: this.id,
5835
+ anchor: call.anchor,
5836
+ state: "non-conforming",
5837
+ verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (C-6)"
5838
+ };
5839
+ }
5840
+ if (PII_RE.test(refs)) {
5841
+ return {
5842
+ id: this.id,
5843
+ anchor: call.anchor,
5844
+ state: "non-conforming",
5845
+ verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
5846
+ };
5847
+ }
5848
+ if (INTERNAL_RE.test(refs)) {
3217
5849
  return {
3218
5850
  id: this.id,
3219
5851
  anchor: call.anchor,
3220
5852
  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)"
5853
+ 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
5854
  };
3223
5855
  }
3224
5856
  }
@@ -3231,7 +5863,7 @@ var dServer6 = {
3231
5863
  id: this.id,
3232
5864
  anchor: `${file.rel}:${line.n}`,
3233
5865
  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)"
5866
+ 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
5867
  };
3236
5868
  }
3237
5869
  }
@@ -3240,7 +5872,7 @@ var dServer6 = {
3240
5872
  id: this.id,
3241
5873
  anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
3242
5874
  state: "present",
3243
- verdict: "no secret referenced in any log call; no raw error.stack exposed to the client (both positive-controls OK)"
5875
+ 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
5876
  };
3245
5877
  }
3246
5878
  };
@@ -3568,6 +6200,24 @@ var detectors = [
3568
6200
  d12,
3569
6201
  d15,
3570
6202
  d16,
6203
+ c7ArtifactVerifiable,
6204
+ c8AuthorityDeclared,
6205
+ c9VerbDeclared,
6206
+ c10MechanismDeclared,
6207
+ c11Pagination,
6208
+ c12NoDuplicates,
6209
+ c13ContextCeiling,
6210
+ c14CatalogRegistration,
6211
+ c15AnnotationsUntrusted,
6212
+ c16UntrustedOutput,
6213
+ c17SupplyChain,
6214
+ c18StructureManifest,
6215
+ c19ServedDescriptions,
6216
+ c20LocalesDerived,
6217
+ c21ProgressiveDisclosure,
6218
+ c22TestRatioGate,
6219
+ c25BuildIdentity,
6220
+ c26SubpackageLockfile,
3571
6221
  dServer2,
3572
6222
  dServer6,
3573
6223
  dDebugOptIn,
@@ -3585,19 +6235,19 @@ var detectors = [
3585
6235
  ];
3586
6236
 
3587
6237
  // 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";
6238
+ import { spawnSync as spawnSync2 } from "child_process";
6239
+ import { existsSync as existsSync15, readFileSync as readFileSync18, statSync as statSync13 } from "fs";
6240
+ import { dirname as dirname10, isAbsolute, join as join20, resolve } from "path";
3591
6241
  function findPackageDir(target) {
3592
6242
  let dir;
3593
6243
  try {
3594
- dir = statSync10(target).isDirectory() ? target : dirname8(target);
6244
+ dir = statSync13(target).isDirectory() ? target : dirname10(target);
3595
6245
  } catch {
3596
6246
  return null;
3597
6247
  }
3598
6248
  for (let depth = 0; depth < 8; depth++) {
3599
- if (existsSync8(join13(dir, "package.json"))) return dir;
3600
- const parent = dirname8(dir);
6249
+ if (existsSync15(join20(dir, "package.json"))) return dir;
6250
+ const parent = dirname10(dir);
3601
6251
  if (parent === dir) break;
3602
6252
  dir = parent;
3603
6253
  }
@@ -3621,10 +6271,10 @@ function deriveLaunchPlan(target) {
3621
6271
  }
3622
6272
  let pkg;
3623
6273
  try {
3624
- pkg = JSON.parse(readFileSync11(join13(pkgDir, "package.json"), "utf8"));
6274
+ pkg = JSON.parse(readFileSync18(join20(pkgDir, "package.json"), "utf8"));
3625
6275
  } catch (err) {
3626
6276
  return {
3627
- error: `could not parse ${join13(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
6277
+ error: `could not parse ${join20(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
3628
6278
  };
3629
6279
  }
3630
6280
  const bin = firstBinEntry(pkg.bin);
@@ -3663,6 +6313,7 @@ try {
3663
6313
  done({ launchError: "spawn failed: " + (e && e.message ? e.message : String(e)) });
3664
6314
  }
3665
6315
  let settled = false;
6316
+ let serverInfo;
3666
6317
  const fail = (msg) => { if (settled) return; settled = true; try { child.kill(); } catch {} done({ launchError: msg }); };
3667
6318
  const to = setTimeout(() => fail("timeout: server did not answer initialize+tools/list within the deadline"), 12000);
3668
6319
  child.on("error", (e) => fail("spawn error: " + (e && e.message ? e.message : String(e))));
@@ -3677,13 +6328,19 @@ child.stdout.on("data", (d) => {
3677
6328
  if (!line.trim()) continue;
3678
6329
  let msg; try { msg = JSON.parse(line); } catch { continue; }
3679
6330
  if (msg.id === 1) {
6331
+ // The initialize response's serverInfo is the SAME non-authenticated
6332
+ // discovery surface a client reads version from (S-7); captured here
6333
+ // so C-25's dynamic half can read a served build-identity field
6334
+ // alongside it, additively -- every existing consumer of the
6335
+ // tools-only introspector output is unaffected.
6336
+ serverInfo = msg.result && msg.result.serverInfo;
3680
6337
  send({ jsonrpc: "2.0", method: "notifications/initialized" });
3681
6338
  send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
3682
6339
  } else if (msg.id === 2) {
3683
6340
  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) }));
6341
+ 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
6342
  try { child.kill(); } catch {}
3686
- done({ tools });
6343
+ done({ tools, serverInfo });
3687
6344
  }
3688
6345
  }
3689
6346
  });
@@ -3695,10 +6352,10 @@ function readEvalCounts(target) {
3695
6352
  if (!pkgDir) return counts;
3696
6353
  let dir = pkgDir;
3697
6354
  for (let depth = 0; depth < 8; depth++) {
3698
- const candidate = join13(dir, "evals", "evals.json");
3699
- if (existsSync8(candidate)) {
6355
+ const candidate = join20(dir, "evals", "evals.json");
6356
+ if (existsSync15(candidate)) {
3700
6357
  try {
3701
- const parsed = JSON.parse(readFileSync11(candidate, "utf8"));
6358
+ const parsed = JSON.parse(readFileSync18(candidate, "utf8"));
3702
6359
  for (const c of parsed.cases ?? []) {
3703
6360
  if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
3704
6361
  }
@@ -3706,7 +6363,7 @@ function readEvalCounts(target) {
3706
6363
  }
3707
6364
  return counts;
3708
6365
  }
3709
- const parent = dirname8(dir);
6366
+ const parent = dirname10(dir);
3710
6367
  if (parent === dir) break;
3711
6368
  dir = parent;
3712
6369
  }
@@ -3714,17 +6371,32 @@ function readEvalCounts(target) {
3714
6371
  }
3715
6372
  function statSyncSafe(p) {
3716
6373
  try {
3717
- return statSync10(p).isDirectory();
6374
+ return statSync13(p).isDirectory();
3718
6375
  } catch {
3719
6376
  return false;
3720
6377
  }
3721
6378
  }
6379
+ function deriveExpectedCommit(cwd) {
6380
+ try {
6381
+ const out = spawnSync2("git", ["rev-parse", "HEAD"], {
6382
+ cwd,
6383
+ encoding: "utf8",
6384
+ timeout: 5e3
6385
+ });
6386
+ if (out.status === 0 && typeof out.stdout === "string") {
6387
+ const sha = out.stdout.trim();
6388
+ if (/^[0-9a-f]{40}$/i.test(sha)) return sha;
6389
+ }
6390
+ } catch {
6391
+ }
6392
+ return void 0;
6393
+ }
3722
6394
  function launchAndIntrospect(target) {
3723
6395
  const plan = deriveLaunchPlan(target);
3724
6396
  if ("error" in plan) {
3725
6397
  return { root: target, tools: [], launchError: plan.error };
3726
6398
  }
3727
- const proc = spawnSync(process.execPath, ["--input-type=module", "-e", INTROSPECTOR], {
6399
+ const proc = spawnSync2(process.execPath, ["--input-type=module", "-e", INTROSPECTOR], {
3728
6400
  env: { ...process.env, MCP_DOCTOR_PLAN: JSON.stringify(plan) },
3729
6401
  encoding: "utf8",
3730
6402
  timeout: 2e4,
@@ -3763,82 +6435,16 @@ function launchAndIntrospect(target) {
3763
6435
  const tools = (parsed.tools ?? []).map((t) => {
3764
6436
  const name = t.name;
3765
6437
  const count = evalCounts.get(name);
3766
- return count === void 0 ? { name } : { name, evalCaseCount: count };
6438
+ const base = t.description ? { name, description: t.description } : { name };
6439
+ return count === void 0 ? base : { ...base, evalCaseCount: count };
3767
6440
  });
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) };
6441
+ const expectedCommit = deriveExpectedCommit(plan.cwd);
6442
+ return {
6443
+ root: target,
6444
+ tools,
6445
+ ...parsed.serverInfo ? { serverInfo: parsed.serverInfo } : {},
6446
+ ...expectedCommit ? { expectedCommit } : {}
6447
+ };
3842
6448
  }
3843
6449
 
3844
6450
  // src/types.ts
@@ -3995,12 +6601,45 @@ function computeExitCode(results) {
3995
6601
  if (results.some((r) => r.state === "could-not-judge")) return 2;
3996
6602
  return 0;
3997
6603
  }
6604
+ var USAGE = [
6605
+ "usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both]",
6606
+ "",
6607
+ " --mode=static read the tree only (default)",
6608
+ " --mode=dynamic launch the server and judge what it serves",
6609
+ " --mode=both both, dynamic findings taking precedence",
6610
+ " --version print the version and exit",
6611
+ " --help print this and exit",
6612
+ "",
6613
+ "exit: 0 clean, 1 a violation, 2 an abstention with no violation"
6614
+ ].join("\n");
6615
+ function readVersion() {
6616
+ try {
6617
+ const here = dirname11(fileURLToPath(import.meta.url));
6618
+ const manifest = JSON.parse(readFileSync19(join21(here, "..", "package.json"), "utf8"));
6619
+ return manifest.version ?? null;
6620
+ } catch {
6621
+ return null;
6622
+ }
6623
+ }
3998
6624
  function main() {
3999
6625
  const args = process.argv.slice(2);
6626
+ if (args.includes("--version") || args.includes("-V")) {
6627
+ const version = readVersion();
6628
+ if (!version) {
6629
+ console.error("mcp-doctor: could not read its own version from package.json");
6630
+ process.exit(1);
6631
+ }
6632
+ console.log(version);
6633
+ return;
6634
+ }
6635
+ if (args.includes("--help") || args.includes("-h")) {
6636
+ console.log(USAGE);
6637
+ return;
6638
+ }
4000
6639
  const modeArg = args.find((a) => a.startsWith("--mode="));
4001
6640
  const target = args.find((a) => !a.startsWith("--"));
4002
6641
  if (!target) {
4003
- console.error("usage: tsx src/doctor.ts <source-tree-path> [--mode=static|dynamic|both]");
6642
+ console.error(USAGE);
4004
6643
  process.exit(1);
4005
6644
  }
4006
6645
  const mode = modeArg ? modeArg.slice("--mode=".length) : "static";
@@ -4012,7 +6651,22 @@ function main() {
4012
6651
  console.log(renderTable(target, results));
4013
6652
  process.exitCode = computeExitCode(results);
4014
6653
  }
4015
- if (import.meta.url === `file://${process.argv[1]}`) main();
6654
+ function isDirectInvocation() {
6655
+ const entry = process.argv[1];
6656
+ if (!entry) return false;
6657
+ let selfReal;
6658
+ try {
6659
+ selfReal = realpathSync(fileURLToPath(import.meta.url));
6660
+ } catch {
6661
+ return false;
6662
+ }
6663
+ try {
6664
+ return realpathSync(resolve2(entry)) === selfReal;
6665
+ } catch {
6666
+ return false;
6667
+ }
6668
+ }
6669
+ if (isDirectInvocation()) main();
4016
6670
  export {
4017
6671
  computeExitCode,
4018
6672
  isLiveDetector,