@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/CHANGELOG.md +43 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.js +2924 -270
- package/package.json +2 -7
package/dist/doctor.js
CHANGED
|
@@ -1,31 +1,133 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 (
|
|
16
|
-
|
|
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: "
|
|
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:
|
|
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
|
|
2977
|
+
if (tree.packageJson) return dirname2(tree.packageJson.path);
|
|
502
2978
|
try {
|
|
503
|
-
return
|
|
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 =
|
|
512
|
-
if (
|
|
513
|
-
const parent =
|
|
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 =
|
|
3036
|
+
const displayPath = relative3(baseDir, evalsPath) || evalsPath;
|
|
559
3037
|
let cases;
|
|
560
3038
|
try {
|
|
561
|
-
const raw =
|
|
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
|
|
646
|
-
import { dirname as
|
|
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 =
|
|
684
|
-
const changelogPath =
|
|
685
|
-
const hasChangelog =
|
|
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
|
|
714
|
-
import { dirname as
|
|
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 =
|
|
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 =
|
|
802
|
-
const hasReadmeFr =
|
|
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: "
|
|
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: "
|
|
912
|
-
verdict: `${toolCount} tool registration(s) found, 0 outputSchema in tree (positive-control OK) -> add output Zod schemas (
|
|
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
|
|
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:
|
|
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
|
|
1368
|
-
import { dirname as
|
|
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
|
|
3983
|
+
function readJson2(path) {
|
|
1400
3984
|
try {
|
|
1401
|
-
return { ok: true, value: JSON.parse(
|
|
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 ?
|
|
1416
|
-
const i18nDir =
|
|
1417
|
-
const enPath =
|
|
1418
|
-
const frPath =
|
|
1419
|
-
const hasI18n =
|
|
1420
|
-
const hasFr =
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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
|
|
1592
|
-
import { join as
|
|
1593
|
-
var
|
|
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:
|
|
1600
|
-
positive:
|
|
1601
|
-
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
|
|
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 =
|
|
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 =
|
|
4201
|
+
entries = readdirSync4(dir);
|
|
1618
4202
|
} catch {
|
|
1619
4203
|
return;
|
|
1620
4204
|
}
|
|
1621
4205
|
for (const name of entries) {
|
|
1622
|
-
const full =
|
|
4206
|
+
const full = join12(dir, name);
|
|
1623
4207
|
let st;
|
|
1624
4208
|
try {
|
|
1625
|
-
st =
|
|
4209
|
+
st = statSync5(full);
|
|
1626
4210
|
} catch {
|
|
1627
4211
|
continue;
|
|
1628
4212
|
}
|
|
1629
4213
|
if (st.isDirectory()) {
|
|
1630
|
-
if (
|
|
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
|
|
1671
|
-
const shortPath =
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
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
|
|
1731
|
-
import { join as
|
|
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
|
|
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 =
|
|
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 =
|
|
4342
|
+
entries = readdirSync5(dir);
|
|
1759
4343
|
} catch {
|
|
1760
4344
|
return;
|
|
1761
4345
|
}
|
|
1762
4346
|
for (const name of entries) {
|
|
1763
|
-
const full =
|
|
4347
|
+
const full = join13(dir, name);
|
|
1764
4348
|
let st;
|
|
1765
4349
|
try {
|
|
1766
|
-
st =
|
|
4350
|
+
st = statSync6(full);
|
|
1767
4351
|
} catch {
|
|
1768
4352
|
continue;
|
|
1769
4353
|
}
|
|
1770
4354
|
if (st.isDirectory()) {
|
|
1771
|
-
if (
|
|
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
|
|
1832
|
-
const shortPath =
|
|
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(
|
|
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
|
|
1900
|
-
import { join as
|
|
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
|
|
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 =
|
|
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 =
|
|
4519
|
+
entries = readdirSync6(dir);
|
|
1936
4520
|
} catch {
|
|
1937
4521
|
return;
|
|
1938
4522
|
}
|
|
1939
4523
|
for (const name of entries) {
|
|
1940
|
-
const full =
|
|
4524
|
+
const full = join14(dir, name);
|
|
1941
4525
|
let st;
|
|
1942
4526
|
try {
|
|
1943
|
-
st =
|
|
4527
|
+
st = statSync7(full);
|
|
1944
4528
|
} catch {
|
|
1945
4529
|
continue;
|
|
1946
4530
|
}
|
|
1947
4531
|
if (st.isDirectory()) {
|
|
1948
|
-
if (
|
|
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
|
|
2017
|
-
const shortPath =
|
|
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(
|
|
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
|
|
2107
|
-
import { join as
|
|
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
|
|
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 =
|
|
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 =
|
|
4722
|
+
entries = readdirSync7(dir);
|
|
2139
4723
|
} catch {
|
|
2140
4724
|
return;
|
|
2141
4725
|
}
|
|
2142
4726
|
for (const name of entries) {
|
|
2143
|
-
const full =
|
|
4727
|
+
const full = join15(dir, name);
|
|
2144
4728
|
let st;
|
|
2145
4729
|
try {
|
|
2146
|
-
st =
|
|
4730
|
+
st = statSync8(full);
|
|
2147
4731
|
} catch {
|
|
2148
4732
|
continue;
|
|
2149
4733
|
}
|
|
2150
4734
|
if (st.isDirectory()) {
|
|
2151
|
-
if (
|
|
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 =
|
|
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 =
|
|
4756
|
+
entries = readdirSync7(dir);
|
|
2173
4757
|
} catch {
|
|
2174
4758
|
return;
|
|
2175
4759
|
}
|
|
2176
4760
|
for (const name of entries) {
|
|
2177
|
-
const full =
|
|
4761
|
+
const full = join15(dir, name);
|
|
2178
4762
|
let st;
|
|
2179
4763
|
try {
|
|
2180
|
-
st =
|
|
4764
|
+
st = statSync8(full);
|
|
2181
4765
|
} catch {
|
|
2182
4766
|
continue;
|
|
2183
4767
|
}
|
|
2184
4768
|
if (!st.isDirectory()) continue;
|
|
2185
|
-
if (
|
|
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 =
|
|
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
|
|
2280
|
-
const shortPath =
|
|
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(
|
|
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
|
|
2342
|
-
import { dirname as
|
|
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
|
|
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 =
|
|
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 =
|
|
4952
|
+
entries = readdirSync8(dir);
|
|
2369
4953
|
} catch {
|
|
2370
4954
|
return;
|
|
2371
4955
|
}
|
|
2372
4956
|
for (const name of entries) {
|
|
2373
|
-
const full =
|
|
4957
|
+
const full = join16(dir, name);
|
|
2374
4958
|
let st;
|
|
2375
4959
|
try {
|
|
2376
|
-
st =
|
|
4960
|
+
st = statSync9(full);
|
|
2377
4961
|
} catch {
|
|
2378
4962
|
continue;
|
|
2379
4963
|
}
|
|
2380
4964
|
if (st.isDirectory()) {
|
|
2381
|
-
if (
|
|
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
|
|
2413
|
-
const shortPath =
|
|
2414
|
-
const root =
|
|
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(
|
|
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 (!
|
|
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
|
|
2483
|
-
import { dirname as
|
|
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
|
|
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 =
|
|
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 =
|
|
5122
|
+
entries = readdirSync9(dir);
|
|
2539
5123
|
} catch {
|
|
2540
5124
|
return;
|
|
2541
5125
|
}
|
|
2542
5126
|
for (const name of entries) {
|
|
2543
|
-
const full =
|
|
5127
|
+
const full = join17(dir, name);
|
|
2544
5128
|
let st;
|
|
2545
5129
|
try {
|
|
2546
|
-
st =
|
|
5130
|
+
st = statSync10(full);
|
|
2547
5131
|
} catch {
|
|
2548
5132
|
continue;
|
|
2549
5133
|
}
|
|
2550
5134
|
if (st.isDirectory()) {
|
|
2551
|
-
if (
|
|
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
|
|
2580
|
-
const extDir =
|
|
5163
|
+
for (const manifestPath2 of manifests) {
|
|
5164
|
+
const extDir = dirname8(manifestPath2);
|
|
2581
5165
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
2582
|
-
const evalsPath =
|
|
5166
|
+
const evalsPath = join17(extDir, "evals", "evals.json");
|
|
2583
5167
|
const shortPath = `${extLabel}/evals/evals.json`;
|
|
2584
|
-
if (!
|
|
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 =
|
|
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
|
|
2638
|
-
import { dirname as
|
|
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
|
|
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 =
|
|
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 =
|
|
5244
|
+
entries = readdirSync10(dir);
|
|
2661
5245
|
} catch {
|
|
2662
5246
|
return;
|
|
2663
5247
|
}
|
|
2664
5248
|
for (const name of entries) {
|
|
2665
|
-
const full =
|
|
5249
|
+
const full = join18(dir, name);
|
|
2666
5250
|
let st;
|
|
2667
5251
|
try {
|
|
2668
|
-
st =
|
|
5252
|
+
st = statSync11(full);
|
|
2669
5253
|
} catch {
|
|
2670
5254
|
continue;
|
|
2671
5255
|
}
|
|
2672
5256
|
if (st.isDirectory()) {
|
|
2673
|
-
if (
|
|
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
|
|
2702
|
-
const extDir =
|
|
5285
|
+
for (const manifestPath2 of manifests) {
|
|
5286
|
+
const extDir = dirname9(manifestPath2);
|
|
2703
5287
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
2704
|
-
const pkgPath =
|
|
5288
|
+
const pkgPath = join18(extDir, "package.json");
|
|
2705
5289
|
const shortPkgPath = `${extLabel}/package.json`;
|
|
2706
|
-
if (!
|
|
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(
|
|
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 =
|
|
2735
|
-
const hasChangelog =
|
|
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
|
|
2765
|
-
import { join as
|
|
2766
|
-
var
|
|
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 =
|
|
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 =
|
|
5362
|
+
entries = readdirSync11(dir);
|
|
2779
5363
|
} catch {
|
|
2780
5364
|
return;
|
|
2781
5365
|
}
|
|
2782
5366
|
for (const name of entries) {
|
|
2783
|
-
const full =
|
|
5367
|
+
const full = join19(dir, name);
|
|
2784
5368
|
let st;
|
|
2785
5369
|
try {
|
|
2786
|
-
st =
|
|
5370
|
+
st = statSync12(full);
|
|
2787
5371
|
} catch {
|
|
2788
5372
|
continue;
|
|
2789
5373
|
}
|
|
2790
5374
|
if (!st.isDirectory()) continue;
|
|
2791
|
-
if (
|
|
5375
|
+
if (SKIP_DIRS9.has(name)) continue;
|
|
2792
5376
|
if (name === "hooks") {
|
|
2793
5377
|
let files;
|
|
2794
5378
|
try {
|
|
2795
|
-
files =
|
|
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 =
|
|
5387
|
+
const filePath = join19(full, f);
|
|
2804
5388
|
let body;
|
|
2805
5389
|
try {
|
|
2806
|
-
body =
|
|
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
|
|
2914
|
-
const shortPath =
|
|
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(
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
3590
|
-
import { dirname as
|
|
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 =
|
|
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 (
|
|
3600
|
-
const parent =
|
|
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(
|
|
6274
|
+
pkg = JSON.parse(readFileSync18(join20(pkgDir, "package.json"), "utf8"));
|
|
3625
6275
|
} catch (err) {
|
|
3626
6276
|
return {
|
|
3627
|
-
error: `could not parse ${
|
|
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 =
|
|
3699
|
-
if (
|
|
6355
|
+
const candidate = join20(dir, "evals", "evals.json");
|
|
6356
|
+
if (existsSync15(candidate)) {
|
|
3700
6357
|
try {
|
|
3701
|
-
const parsed = JSON.parse(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
6438
|
+
const base = t.description ? { name, description: t.description } : { name };
|
|
6439
|
+
return count === void 0 ? base : { ...base, evalCaseCount: count };
|
|
3767
6440
|
});
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|