@vantageos/mcp-doctor 0.8.1 → 0.8.3
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 +37 -0
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.js +663 -236
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/doctor.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync16, realpathSync } from "fs";
|
|
5
5
|
import { dirname as dirname17, join as join27, resolve as resolve3 } from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
@@ -508,7 +508,7 @@ function countInCode(file, line, ch) {
|
|
|
508
508
|
}
|
|
509
509
|
|
|
510
510
|
// src/detectors/couldNotJudge.ts
|
|
511
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
511
|
+
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
512
512
|
function couldNotJudge(id, anchor, reason, owedVerdict) {
|
|
513
513
|
return {
|
|
514
514
|
id,
|
|
@@ -519,6 +519,12 @@ function couldNotJudge(id, anchor, reason, owedVerdict) {
|
|
|
519
519
|
...owedVerdict ? { owedVerdict } : {}
|
|
520
520
|
};
|
|
521
521
|
}
|
|
522
|
+
function refusedAsUnreadable(refused, what) {
|
|
523
|
+
return refused.map((r) => ({
|
|
524
|
+
anchor: r.rel,
|
|
525
|
+
reason: `${r.rel} ${r.reason}: ${what}`
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
522
528
|
function settle(id, decided, unreadable) {
|
|
523
529
|
if (unreadable.length === 0) return decided;
|
|
524
530
|
const named = unreadable.map((u) => `${u.anchor}: ${u.reason}`).join("; ");
|
|
@@ -536,6 +542,7 @@ function settle(id, decided, unreadable) {
|
|
|
536
542
|
function readJsonObject(path) {
|
|
537
543
|
let value;
|
|
538
544
|
try {
|
|
545
|
+
if (!statSync2(path).isFile()) return null;
|
|
539
546
|
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
540
547
|
} catch {
|
|
541
548
|
return null;
|
|
@@ -563,7 +570,7 @@ function guardUnreadablePackageJson(inner, opts) {
|
|
|
563
570
|
}
|
|
564
571
|
|
|
565
572
|
// src/detectors/siteScan.ts
|
|
566
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as
|
|
573
|
+
import { constants as constants2, accessSync as accessSync2, existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
567
574
|
import { join as join2, relative as relative2, sep as sep2 } from "path";
|
|
568
575
|
function codeSites(tree, re) {
|
|
569
576
|
const out = [];
|
|
@@ -630,27 +637,33 @@ function isRecord(v) {
|
|
|
630
637
|
}
|
|
631
638
|
function baseOf(tree) {
|
|
632
639
|
try {
|
|
633
|
-
return
|
|
640
|
+
return statSync3(tree.root).isDirectory() ? tree.root : join2(tree.root, "..");
|
|
634
641
|
} catch {
|
|
635
642
|
return tree.root;
|
|
636
643
|
}
|
|
637
644
|
}
|
|
638
645
|
function findDiskFiles(tree, pred) {
|
|
639
646
|
const out = [];
|
|
647
|
+
const refused = [];
|
|
640
648
|
const base = baseOf(tree);
|
|
641
649
|
const walk2 = (dir) => {
|
|
642
650
|
let entries;
|
|
643
651
|
try {
|
|
644
652
|
entries = readdirSync2(dir);
|
|
645
|
-
} catch {
|
|
653
|
+
} catch (err) {
|
|
654
|
+
recordSkip(tree, dir, base, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
646
655
|
return;
|
|
647
656
|
}
|
|
648
657
|
for (const name of entries.sort()) {
|
|
649
658
|
const full = join2(dir, name);
|
|
650
659
|
let st;
|
|
651
660
|
try {
|
|
652
|
-
st =
|
|
653
|
-
} catch {
|
|
661
|
+
st = statSync3(full);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
const why = `could not be read (${fsErrorCode(err)}) \u2014 not read`;
|
|
664
|
+
recordSkip(tree, full, base, why);
|
|
665
|
+
const relBad = relative2(base, full).split(sep2).join("/");
|
|
666
|
+
if (pred(name, relBad)) refused.push({ rel: relBad, reason: why });
|
|
654
667
|
continue;
|
|
655
668
|
}
|
|
656
669
|
if (st.isDirectory()) {
|
|
@@ -661,15 +674,31 @@ function findDiskFiles(tree, pred) {
|
|
|
661
674
|
walk2(full);
|
|
662
675
|
} else {
|
|
663
676
|
const rel = relative2(base, full).split(sep2).join("/");
|
|
664
|
-
if (pred(name, rel))
|
|
677
|
+
if (!pred(name, rel)) continue;
|
|
678
|
+
if (!st.isFile()) {
|
|
679
|
+
const why = "not a regular file (FIFO, socket or device) \u2014 not read";
|
|
680
|
+
recordSkip(tree, full, base, why);
|
|
681
|
+
refused.push({ rel, reason: why });
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
accessSync2(full, constants2.R_OK);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
const why = `could not be read (${fsErrorCode(err)}) \u2014 not read`;
|
|
688
|
+
recordSkip(tree, full, base, why);
|
|
689
|
+
refused.push({ rel, reason: why });
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
out.push({ path: full, rel, name });
|
|
665
693
|
}
|
|
666
694
|
}
|
|
667
695
|
};
|
|
668
696
|
walk2(base);
|
|
669
|
-
return out;
|
|
697
|
+
return Object.assign(out, { refused });
|
|
670
698
|
}
|
|
671
699
|
function readText(path) {
|
|
672
700
|
try {
|
|
701
|
+
if (!statSync3(path).isFile()) return null;
|
|
673
702
|
return readFileSync3(path, "utf8");
|
|
674
703
|
} catch {
|
|
675
704
|
return null;
|
|
@@ -1695,12 +1724,29 @@ var a2DeclaredDomains = {
|
|
|
1695
1724
|
}
|
|
1696
1725
|
}
|
|
1697
1726
|
const configs = findDiskFiles(tree, (n) => /^vite\.config\.(ts|js|mjs|cjs|mts)$/.test(n));
|
|
1698
|
-
|
|
1727
|
+
let configUnread = false;
|
|
1728
|
+
for (const r of configs.refused) {
|
|
1729
|
+
configUnread = true;
|
|
1730
|
+
unreadable.push({
|
|
1731
|
+
anchor: r.rel,
|
|
1732
|
+
reason: `${r.rel} ${r.reason}: how the UI bundle is built (single file or not) is unread`
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
const texts2 = configs.map((c) => ({ rel: c.rel, text: readText(c.path) }));
|
|
1736
|
+
for (const t of texts2) {
|
|
1737
|
+
if (t.text !== null) continue;
|
|
1738
|
+
configUnread = true;
|
|
1739
|
+
unreadable.push({
|
|
1740
|
+
anchor: t.rel,
|
|
1741
|
+
reason: `${t.rel} could not be read \u2014 how the UI bundle is built (single file or not) is unread`
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
if (configs.length === 0 && configs.refused.length === 0) {
|
|
1699
1745
|
unreadable.push({
|
|
1700
1746
|
anchor: ui[0].anchor,
|
|
1701
1747
|
reason: "no vite.config.* in the tree \u2014 how the UI bundle is built (single file or not) is unread"
|
|
1702
1748
|
});
|
|
1703
|
-
} else if (!
|
|
1749
|
+
} else if (!configUnread && !texts2.some((t) => SINGLEFILE_RE.test(stripComments(t.text ?? "")))) {
|
|
1704
1750
|
reds.push({
|
|
1705
1751
|
anchor: configs[0].rel,
|
|
1706
1752
|
text: `${configs[0].rel} builds the UI without a single-file plugin (vite-plugin-singlefile or an inlining equivalent): the bundle is multi-file`
|
|
@@ -1738,7 +1784,7 @@ var a2DeclaredDomains = {
|
|
|
1738
1784
|
};
|
|
1739
1785
|
|
|
1740
1786
|
// src/detectors/v080Kit.ts
|
|
1741
|
-
import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as
|
|
1787
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
1742
1788
|
import { dirname as dirname2, join as join3 } from "path";
|
|
1743
1789
|
|
|
1744
1790
|
// src/detectors/toolContext.ts
|
|
@@ -2617,11 +2663,14 @@ function productSites(tree, re) {
|
|
|
2617
2663
|
}
|
|
2618
2664
|
function diskText(tree, pred) {
|
|
2619
2665
|
const out = [];
|
|
2620
|
-
|
|
2666
|
+
const found = findDiskFiles(tree, pred);
|
|
2667
|
+
const refused = [...found.refused];
|
|
2668
|
+
for (const f of found) {
|
|
2621
2669
|
const text = readText(f.path);
|
|
2622
2670
|
if (text !== null) out.push({ ...f, text });
|
|
2671
|
+
else refused.push({ rel: f.rel, reason: "could not be read \u2014 not read" });
|
|
2623
2672
|
}
|
|
2624
|
-
return out;
|
|
2673
|
+
return Object.assign(out, { refused });
|
|
2625
2674
|
}
|
|
2626
2675
|
function governingWorkflows(tree) {
|
|
2627
2676
|
let dir = baseOf(tree);
|
|
@@ -2629,16 +2678,29 @@ function governingWorkflows(tree) {
|
|
|
2629
2678
|
const wf = join3(dir, ".github", "workflows");
|
|
2630
2679
|
if (existsSync3(wf)) {
|
|
2631
2680
|
const out = [];
|
|
2632
|
-
|
|
2681
|
+
const base = baseOf(tree);
|
|
2682
|
+
let names;
|
|
2683
|
+
try {
|
|
2684
|
+
names = readdirSync3(wf).sort();
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
recordSkip(tree, wf, base, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
2687
|
+
return out;
|
|
2688
|
+
}
|
|
2689
|
+
for (const name of names) {
|
|
2633
2690
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
2634
2691
|
const full = join3(wf, name);
|
|
2635
2692
|
try {
|
|
2636
|
-
if (!
|
|
2637
|
-
|
|
2693
|
+
if (!statSync4(full).isFile()) {
|
|
2694
|
+
recordSkip(tree, full, base, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
2695
|
+
continue;
|
|
2696
|
+
}
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
recordSkip(tree, full, base, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
2638
2699
|
continue;
|
|
2639
2700
|
}
|
|
2640
2701
|
const text = readText(full);
|
|
2641
2702
|
if (text !== null) out.push({ rel: `.github/workflows/${name}`, text });
|
|
2703
|
+
else recordSkip(tree, full, base, "could not be read \u2014 not read");
|
|
2642
2704
|
}
|
|
2643
2705
|
return out;
|
|
2644
2706
|
}
|
|
@@ -2727,6 +2789,12 @@ function appSurface(tree) {
|
|
|
2727
2789
|
const anchor = uiSites[0]?.anchor ?? (uiFiles[0] ? `${uiFiles[0].rel}:1` : `${html[0].rel}:1`);
|
|
2728
2790
|
return { anchor, uiFiles };
|
|
2729
2791
|
}
|
|
2792
|
+
function unreadHtmlShells(tree) {
|
|
2793
|
+
return refusedAsUnreadable(
|
|
2794
|
+
findDiskFiles(tree, (name) => /\.html?$/i.test(name)).refused,
|
|
2795
|
+
"whether the tree is an App (UI code held in an HTML shell) is unread"
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2730
2798
|
function codeOnly(text) {
|
|
2731
2799
|
return text.replace(/(["'`])((?:\\.|(?!\1)[^\\\n])*)\1(?!\s*:)/g, (_m, q) => `${q}${q}`);
|
|
2732
2800
|
}
|
|
@@ -2786,10 +2854,14 @@ var a3SandboxRespected = {
|
|
|
2786
2854
|
}
|
|
2787
2855
|
const app = appSurface(tree);
|
|
2788
2856
|
if (app === null) {
|
|
2789
|
-
return
|
|
2857
|
+
return settle(
|
|
2790
2858
|
this.id,
|
|
2791
|
-
|
|
2792
|
-
|
|
2859
|
+
absentResult(
|
|
2860
|
+
this.id,
|
|
2861
|
+
"-",
|
|
2862
|
+
"no UI code, no HTML and no ui:// resource in the tree (positive control OK) \u2014 A-3 n/a"
|
|
2863
|
+
),
|
|
2864
|
+
unreadHtmlShells(tree)
|
|
2793
2865
|
);
|
|
2794
2866
|
}
|
|
2795
2867
|
const uiRels = new Set(app.uiFiles.map((f) => f.rel));
|
|
@@ -2803,7 +2875,11 @@ var a3SandboxRespected = {
|
|
|
2803
2875
|
inlineScripts.push({ anchor: `${file.rel}:${line}`, body: m[1] });
|
|
2804
2876
|
}
|
|
2805
2877
|
}
|
|
2806
|
-
|
|
2878
|
+
const unreadShells = refusedAsUnreadable(
|
|
2879
|
+
htmlShells.refused,
|
|
2880
|
+
"its <script> blocks (parent-frame access, direct storage) are unread"
|
|
2881
|
+
);
|
|
2882
|
+
if (uiRels.size === 0 && htmlShells.length === 0 && inlineScripts.length === 0 && unreadShells.length === 0) {
|
|
2807
2883
|
return couldNotJudge(
|
|
2808
2884
|
this.id,
|
|
2809
2885
|
app.anchor,
|
|
@@ -2839,17 +2915,25 @@ var a3SandboxRespected = {
|
|
|
2839
2915
|
}
|
|
2840
2916
|
}
|
|
2841
2917
|
}
|
|
2918
|
+
const unreadHtml = refusedAsUnreadable(
|
|
2919
|
+
htmlFiles.refused,
|
|
2920
|
+
"its <script> blocks (parent-frame access, direct storage) are unread"
|
|
2921
|
+
);
|
|
2842
2922
|
if (reds.length > 0) {
|
|
2843
|
-
return
|
|
2923
|
+
return settle(
|
|
2844
2924
|
this.id,
|
|
2845
|
-
reds[0].split(" ")[0],
|
|
2846
|
-
|
|
2925
|
+
redResult(this.id, reds[0].split(" ")[0], `${reds.join("; ")} (A-3, positive-control OK)`),
|
|
2926
|
+
unreadHtml
|
|
2847
2927
|
);
|
|
2848
2928
|
}
|
|
2849
|
-
return
|
|
2929
|
+
return settle(
|
|
2850
2930
|
this.id,
|
|
2851
|
-
|
|
2852
|
-
|
|
2931
|
+
presentResult(
|
|
2932
|
+
this.id,
|
|
2933
|
+
app.anchor,
|
|
2934
|
+
`${uiRels.size} UI file(s), ${htmlFiles.length} HTML shell(s) and ${inlineScripts.length} inline script(s) in strings: no parent-frame access, no direct storage \u2014 everything is left to the bridge (positive-control OK); the double-iframe CSP is the host proxy's (A-3)`
|
|
2935
|
+
),
|
|
2936
|
+
unreadHtml
|
|
2853
2937
|
);
|
|
2854
2938
|
}
|
|
2855
2939
|
};
|
|
@@ -2875,13 +2959,18 @@ var a4WcagGate = {
|
|
|
2875
2959
|
}
|
|
2876
2960
|
const app = appSurface(tree);
|
|
2877
2961
|
if (app === null) {
|
|
2878
|
-
return
|
|
2962
|
+
return settle(
|
|
2879
2963
|
this.id,
|
|
2880
|
-
|
|
2881
|
-
|
|
2964
|
+
absentResult(
|
|
2965
|
+
this.id,
|
|
2966
|
+
"-",
|
|
2967
|
+
"not an App (no UI code, no HTML, no ui:// resource; positive control OK) \u2014 A-4 n/a"
|
|
2968
|
+
),
|
|
2969
|
+
unreadHtmlShells(tree)
|
|
2882
2970
|
);
|
|
2883
2971
|
}
|
|
2884
|
-
const
|
|
2972
|
+
const htmlShells = findDiskFiles(tree, (name) => /\.html?$/i.test(name));
|
|
2973
|
+
const noUiCode = app.uiFiles.length === 0 && htmlShells.length === 0;
|
|
2885
2974
|
const pkgs = diskText(tree, (name) => name === "package.json");
|
|
2886
2975
|
const unreadable = [];
|
|
2887
2976
|
let dep = null;
|
|
@@ -2906,11 +2995,17 @@ var a4WcagGate = {
|
|
|
2906
2995
|
const workflowRun = governingWorkflows(tree).find(
|
|
2907
2996
|
(w) => /\baxe\b|axe-core/i.test(stripYamlComments(w.text))
|
|
2908
2997
|
);
|
|
2909
|
-
const
|
|
2998
|
+
const testFiles = diskText(tree, (name) => /\.(?:test|spec)\.[jt]sx?$/.test(name));
|
|
2999
|
+
const testRun = testFiles.find(
|
|
2910
3000
|
(f) => /\baxe\b|axe-core|jest-axe|vitest-axe/i.test(stripJsComments(f.text))
|
|
2911
3001
|
);
|
|
3002
|
+
const refusedInputs = [
|
|
3003
|
+
...refusedAsUnreadable(pkgs.refused, "its axe dependency and scripts are unread"),
|
|
3004
|
+
...refusedAsUnreadable(testFiles.refused, "a test file that may run axe is unread"),
|
|
3005
|
+
...refusedAsUnreadable(htmlShells.refused, "an HTML shell of the App is unread")
|
|
3006
|
+
];
|
|
2912
3007
|
const run = scriptRun ?? (workflowRun ? workflowRun.rel : null) ?? (testRun ? testRun.rel : null);
|
|
2913
|
-
if (noUiCode && dep === null && run === null && unreadable.length === 0) {
|
|
3008
|
+
if (noUiCode && dep === null && run === null && unreadable.length === 0 && refusedInputs.length === 0) {
|
|
2914
3009
|
return couldNotJudge(
|
|
2915
3010
|
this.id,
|
|
2916
3011
|
app.anchor,
|
|
@@ -2924,6 +3019,13 @@ var a4WcagGate = {
|
|
|
2924
3019
|
`${unreadable.join(", ")} is present but could not be parsed (not valid JSON): the axe dependency and the scripts it lists are unreadable, so neither "no gate" nor "a gate" can be said${run === null ? "" : ` (a run was found at ${run})`}; owed: a parsable manifest, then an axe gate that CI runs with 0 blocking WCAG AA violations (A-4)`
|
|
2925
3020
|
);
|
|
2926
3021
|
}
|
|
3022
|
+
if (run === null && refusedInputs.length > 0) {
|
|
3023
|
+
return couldNotJudge(
|
|
3024
|
+
this.id,
|
|
3025
|
+
`${refusedInputs[0].anchor}:1`,
|
|
3026
|
+
`${refusedInputs.map((u) => u.reason).join("; ")}: neither "no gate" nor "an axe dependency that nothing runs" can be said over an input that was not read${dep === null ? "" : ` (${dep} is declared)`}; owed: a readable input, then an axe gate that CI runs with 0 blocking WCAG AA violations (A-4)`
|
|
3027
|
+
);
|
|
3028
|
+
}
|
|
2927
3029
|
if (dep === null && run === null) {
|
|
2928
3030
|
return redResult(
|
|
2929
3031
|
this.id,
|
|
@@ -2947,7 +3049,7 @@ var a4WcagGate = {
|
|
|
2947
3049
|
};
|
|
2948
3050
|
|
|
2949
3051
|
// src/detectors/a5-performance-budgets.ts
|
|
2950
|
-
import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as
|
|
3052
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
2951
3053
|
import { join as join4, relative as relative3, sep as sep3 } from "path";
|
|
2952
3054
|
import { gzipSync } from "zlib";
|
|
2953
3055
|
var BUDGET_BYTES = 250 * 1024;
|
|
@@ -2983,7 +3085,7 @@ function collect(base) {
|
|
|
2983
3085
|
const candidate = BUNDLE_EXT_RE.test(name) && UI_PATH_RE.test(rel);
|
|
2984
3086
|
let st;
|
|
2985
3087
|
try {
|
|
2986
|
-
st =
|
|
3088
|
+
st = statSync5(full);
|
|
2987
3089
|
} catch {
|
|
2988
3090
|
if (candidate) unmeasured.push(`${rel} (cannot be read: dangling link or permission)`);
|
|
2989
3091
|
continue;
|
|
@@ -2991,6 +3093,10 @@ function collect(base) {
|
|
|
2991
3093
|
if (st.isDirectory()) {
|
|
2992
3094
|
if (!WALK_SKIP.has(name)) walk2(full, depth + 1);
|
|
2993
3095
|
} else if (candidate) {
|
|
3096
|
+
if (!st.isFile()) {
|
|
3097
|
+
unmeasured.push(`${rel} (not a regular file, not read)`);
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
2994
3100
|
if (st.size > MAX_MEASURE_BYTES) {
|
|
2995
3101
|
unmeasured.push(`${rel} (${(st.size / 1048576).toFixed(0)} MB, too large to measure)`);
|
|
2996
3102
|
continue;
|
|
@@ -3020,10 +3126,14 @@ var a5PerformanceBudgets = {
|
|
|
3020
3126
|
}
|
|
3021
3127
|
const app = appSurface(tree);
|
|
3022
3128
|
if (app === null) {
|
|
3023
|
-
return
|
|
3129
|
+
return settle(
|
|
3024
3130
|
this.id,
|
|
3025
|
-
|
|
3026
|
-
|
|
3131
|
+
absentResult(
|
|
3132
|
+
this.id,
|
|
3133
|
+
"-",
|
|
3134
|
+
"not an App (no UI code, no HTML, no ui:// resource; positive control OK) \u2014 A-5 n/a"
|
|
3135
|
+
),
|
|
3136
|
+
unreadHtmlShells(tree)
|
|
3027
3137
|
);
|
|
3028
3138
|
}
|
|
3029
3139
|
const { bundles, unmeasured } = collect(baseOf(tree));
|
|
@@ -3212,7 +3322,14 @@ function codeBlockAfter(file, line, col, open, close) {
|
|
|
3212
3322
|
function scanCapabilities(tree) {
|
|
3213
3323
|
const caps = [];
|
|
3214
3324
|
const unreadable = [];
|
|
3215
|
-
|
|
3325
|
+
const manifests = findDiskFiles(tree, (n) => n === "mcp.json" || n === "mcp-server.json");
|
|
3326
|
+
for (const r of manifests.refused) {
|
|
3327
|
+
unreadable.push({
|
|
3328
|
+
anchor: r.rel,
|
|
3329
|
+
reason: `${r.rel} ${r.reason}: its declared capabilities are unread`
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
for (const f of manifests) {
|
|
3216
3333
|
const json = readJsonRecord(f.path);
|
|
3217
3334
|
if (!json) {
|
|
3218
3335
|
unreadable.push({
|
|
@@ -3413,7 +3530,14 @@ function collectNamespacedKeys(tree) {
|
|
|
3413
3530
|
for (const site of codeSites(tree, META_BRACKET_RE)) {
|
|
3414
3531
|
acc.keys.push({ key: site.match[2], anchor: site.anchor, context: "_meta", source: "code" });
|
|
3415
3532
|
}
|
|
3416
|
-
|
|
3533
|
+
const manifests = findDiskFiles(tree, (n) => n === "mcp.json" || n === "mcp-server.json");
|
|
3534
|
+
for (const r of manifests.refused) {
|
|
3535
|
+
acc.unreadable.push({
|
|
3536
|
+
anchor: r.rel,
|
|
3537
|
+
reason: `${r.rel} ${r.reason}: its capability keys are unread`
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
for (const f of manifests) {
|
|
3417
3541
|
const json = readJsonRecord(f.path);
|
|
3418
3542
|
if (!json) {
|
|
3419
3543
|
acc.unreadable.push({
|
|
@@ -3546,17 +3670,25 @@ function assertSkillsControl(id) {
|
|
|
3546
3670
|
}
|
|
3547
3671
|
}
|
|
3548
3672
|
function readSkillFiles(tree) {
|
|
3549
|
-
|
|
3550
|
-
|
|
3673
|
+
const found = findDiskFiles(tree, (n) => n === "SKILL.md");
|
|
3674
|
+
const files = [];
|
|
3675
|
+
const refused = [...found.refused];
|
|
3676
|
+
for (const f of found) {
|
|
3677
|
+
const text = readText(f.path);
|
|
3678
|
+
if (text === null) {
|
|
3679
|
+
refused.push({ rel: f.rel, reason: "could not be read \u2014 not read" });
|
|
3680
|
+
continue;
|
|
3681
|
+
}
|
|
3551
3682
|
const parts = f.rel.split("/");
|
|
3552
|
-
|
|
3683
|
+
files.push({
|
|
3553
3684
|
path: f.path,
|
|
3554
3685
|
rel: f.rel,
|
|
3555
3686
|
dir: parts[parts.length - 2] ?? "",
|
|
3556
3687
|
name: frontmatterName(text),
|
|
3557
3688
|
text
|
|
3558
|
-
};
|
|
3559
|
-
}
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
return Object.assign(files, { refused });
|
|
3560
3692
|
}
|
|
3561
3693
|
|
|
3562
3694
|
// src/detectors/a8-ui-skills-disjoint.ts
|
|
@@ -3603,7 +3735,12 @@ var a8UiSkillsDisjoint = {
|
|
|
3603
3735
|
});
|
|
3604
3736
|
}
|
|
3605
3737
|
}
|
|
3606
|
-
|
|
3738
|
+
const skillFiles = readSkillFiles(tree);
|
|
3739
|
+
const unreadSkills = refusedAsUnreadable(
|
|
3740
|
+
skillFiles.refused,
|
|
3741
|
+
"its frontmatter (a ui:// reference) is unread"
|
|
3742
|
+
);
|
|
3743
|
+
for (const sf of skillFiles) {
|
|
3607
3744
|
const fm = sf.text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
3608
3745
|
const hit = fm?.[1].match(/ui:\/\/\S+/);
|
|
3609
3746
|
if (hit)
|
|
@@ -3613,17 +3750,25 @@ var a8UiSkillsDisjoint = {
|
|
|
3613
3750
|
});
|
|
3614
3751
|
}
|
|
3615
3752
|
if (reds.length > 0) {
|
|
3616
|
-
return
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3753
|
+
return settle(
|
|
3754
|
+
this.id,
|
|
3755
|
+
{
|
|
3756
|
+
id: this.id,
|
|
3757
|
+
anchor: reds[0].anchor,
|
|
3758
|
+
state: "non-conforming",
|
|
3759
|
+
verdict: `${reds.map((r) => r.text).join("; ")} -> skills are plain markdown under the Skills extension, UI resources live under ui://, the two sets never overlap (A-8)`
|
|
3760
|
+
},
|
|
3761
|
+
unreadSkills
|
|
3762
|
+
);
|
|
3622
3763
|
}
|
|
3623
|
-
return
|
|
3764
|
+
return settle(
|
|
3624
3765
|
this.id,
|
|
3625
|
-
|
|
3626
|
-
|
|
3766
|
+
couldNotJudge(
|
|
3767
|
+
this.id,
|
|
3768
|
+
surface.anchor,
|
|
3769
|
+
"no source literal mixes the two spaces; owed (dynamic): skills/list and the ui:// resource list, compared \u2014 no manifest uri uses the UI scheme or equals a listed UI resource, no skills/list entry starts with ui:// (A-8)"
|
|
3770
|
+
),
|
|
3771
|
+
unreadSkills
|
|
3627
3772
|
);
|
|
3628
3773
|
}
|
|
3629
3774
|
};
|
|
@@ -4244,7 +4389,7 @@ var a12FileWrite = {
|
|
|
4244
4389
|
};
|
|
4245
4390
|
|
|
4246
4391
|
// src/detectors/c7-artifact-verifiable.ts
|
|
4247
|
-
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
4392
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync6 } from "fs";
|
|
4248
4393
|
import { join as join5 } from "path";
|
|
4249
4394
|
var PIN = STANDARD_PIN;
|
|
4250
4395
|
var DOC_FILES = ["README.md", "README.fr.md"];
|
|
@@ -4282,13 +4427,18 @@ function c7PositiveControls() {
|
|
|
4282
4427
|
}
|
|
4283
4428
|
};
|
|
4284
4429
|
}
|
|
4285
|
-
function readDocText(root) {
|
|
4430
|
+
function readDocText(root, unreadable) {
|
|
4286
4431
|
for (const name of DOC_FILES) {
|
|
4287
4432
|
const path = join5(root, name);
|
|
4288
4433
|
if (!existsSync4(path)) continue;
|
|
4289
4434
|
try {
|
|
4435
|
+
if (!statSync6(path).isFile()) {
|
|
4436
|
+
unreadable.push({ anchor: name, reason: `${name} is not a regular file \u2014 not read` });
|
|
4437
|
+
continue;
|
|
4438
|
+
}
|
|
4290
4439
|
return { text: readFileSync5(path, "utf8"), file: name };
|
|
4291
|
-
} catch {
|
|
4440
|
+
} catch (err) {
|
|
4441
|
+
unreadable.push({ anchor: name, reason: `${name} could not be read (${fsErrorCode(err)})` });
|
|
4292
4442
|
}
|
|
4293
4443
|
}
|
|
4294
4444
|
return null;
|
|
@@ -4319,7 +4469,15 @@ var c7ArtifactVerifiable = {
|
|
|
4319
4469
|
`C7 channel-matcher control FAILED on \`${controls.channelMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
|
|
4320
4470
|
);
|
|
4321
4471
|
}
|
|
4322
|
-
const
|
|
4472
|
+
const unreadable = [];
|
|
4473
|
+
const doc = readDocText(tree.root, unreadable);
|
|
4474
|
+
if (unreadable.length > 0) {
|
|
4475
|
+
return couldNotJudge(
|
|
4476
|
+
this.id,
|
|
4477
|
+
unreadable[0].anchor,
|
|
4478
|
+
`${unreadable.map((u) => u.reason).join("; ")} \u2014 the install, pin, channel and provenance propositions are unjudged (C-7)`
|
|
4479
|
+
);
|
|
4480
|
+
}
|
|
4323
4481
|
if (!doc) {
|
|
4324
4482
|
return {
|
|
4325
4483
|
id: this.id,
|
|
@@ -4991,7 +5149,14 @@ function sameFileConstObject(file, name) {
|
|
|
4991
5149
|
}
|
|
4992
5150
|
function ceilingCensus(tree) {
|
|
4993
5151
|
const census = { readable: [], partial: [], unreadable: [] };
|
|
4994
|
-
|
|
5152
|
+
const manifests = findDiskFiles(tree, (n) => SERVER_MANIFEST_NAMES.has(n));
|
|
5153
|
+
for (const r of manifests.refused) {
|
|
5154
|
+
census.unreadable.push({
|
|
5155
|
+
anchor: r.rel,
|
|
5156
|
+
reason: `${r.rel} ${r.reason}: a contextCeiling it may declare is unread`
|
|
5157
|
+
});
|
|
5158
|
+
}
|
|
5159
|
+
for (const f of manifests) {
|
|
4995
5160
|
const text = readText(f.path);
|
|
4996
5161
|
if (text === null) {
|
|
4997
5162
|
census.unreadable.push({ anchor: f.rel, reason: `${f.rel} is not readable as text` });
|
|
@@ -5582,7 +5747,7 @@ var c16UntrustedOutput = {
|
|
|
5582
5747
|
};
|
|
5583
5748
|
|
|
5584
5749
|
// src/detectors/c17-supply-chain.ts
|
|
5585
|
-
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync5 } from "fs";
|
|
5750
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
5586
5751
|
import { dirname as dirname3, join as join6, relative as relative4, resolve } from "path";
|
|
5587
5752
|
var PIN4 = STANDARD_PIN;
|
|
5588
5753
|
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
@@ -5616,15 +5781,26 @@ function judgeProvenanceRecord(line, name, version) {
|
|
|
5616
5781
|
}
|
|
5617
5782
|
return { ok: true, why: "" };
|
|
5618
5783
|
}
|
|
5619
|
-
function findProvenanceRecord(root, name, version) {
|
|
5784
|
+
function findProvenanceRecord(root, name, version, unreadable) {
|
|
5620
5785
|
let firstBad = null;
|
|
5621
5786
|
for (const recordFile of PROVENANCE_RECORD_FILES) {
|
|
5622
5787
|
const path = join6(root, recordFile);
|
|
5623
5788
|
if (!existsSync5(path)) continue;
|
|
5624
5789
|
let text;
|
|
5625
5790
|
try {
|
|
5791
|
+
if (!statSync7(path).isFile()) {
|
|
5792
|
+
unreadable.push({
|
|
5793
|
+
anchor: recordFile,
|
|
5794
|
+
reason: `${recordFile} is not a regular file \u2014 not read`
|
|
5795
|
+
});
|
|
5796
|
+
continue;
|
|
5797
|
+
}
|
|
5626
5798
|
text = readFileSync6(path, "utf8");
|
|
5627
|
-
} catch {
|
|
5799
|
+
} catch (err) {
|
|
5800
|
+
unreadable.push({
|
|
5801
|
+
anchor: recordFile,
|
|
5802
|
+
reason: `${recordFile} could not be read (${fsErrorCode(err)})`
|
|
5803
|
+
});
|
|
5628
5804
|
continue;
|
|
5629
5805
|
}
|
|
5630
5806
|
const lines = text.split("\n");
|
|
@@ -5677,7 +5853,7 @@ function findLockfile(root) {
|
|
|
5677
5853
|
}
|
|
5678
5854
|
return null;
|
|
5679
5855
|
}
|
|
5680
|
-
function findProvenanceClaim(tree, root) {
|
|
5856
|
+
function findProvenanceClaim(tree, root, unreadable) {
|
|
5681
5857
|
const pkg = tree.packageJson;
|
|
5682
5858
|
if (pkg) {
|
|
5683
5859
|
const publishConfig = pkg.json.publishConfig;
|
|
@@ -5691,16 +5867,29 @@ function findProvenanceClaim(tree, root) {
|
|
|
5691
5867
|
let entries = [];
|
|
5692
5868
|
try {
|
|
5693
5869
|
entries = readdirSync5(workflowsDir).sort();
|
|
5694
|
-
} catch {
|
|
5870
|
+
} catch (err) {
|
|
5871
|
+
unreadable.push({
|
|
5872
|
+
anchor: relative4(root, workflowsDir) || WORKFLOW_DIR,
|
|
5873
|
+
reason: `${relative4(root, workflowsDir) || WORKFLOW_DIR} cannot be listed (${fsErrorCode(err)})`
|
|
5874
|
+
});
|
|
5695
5875
|
continue;
|
|
5696
5876
|
}
|
|
5697
5877
|
for (const name of entries) {
|
|
5698
5878
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
5699
5879
|
const path = join6(workflowsDir, name);
|
|
5700
5880
|
let text;
|
|
5881
|
+
const shown = `${relative4(root, workflowsDir) || WORKFLOW_DIR}/${name}`;
|
|
5701
5882
|
try {
|
|
5883
|
+
if (!statSync7(path).isFile()) {
|
|
5884
|
+
unreadable.push({ anchor: shown, reason: `${shown} is not a regular file \u2014 not read` });
|
|
5885
|
+
continue;
|
|
5886
|
+
}
|
|
5702
5887
|
text = readFileSync6(path, "utf8");
|
|
5703
|
-
} catch {
|
|
5888
|
+
} catch (err) {
|
|
5889
|
+
unreadable.push({
|
|
5890
|
+
anchor: shown,
|
|
5891
|
+
reason: `${shown} could not be read (${fsErrorCode(err)})`
|
|
5892
|
+
});
|
|
5704
5893
|
continue;
|
|
5705
5894
|
}
|
|
5706
5895
|
const hit = workflowStatements(text).find(([, l]) => PROVENANCE_RE2.test(l));
|
|
@@ -5749,10 +5938,11 @@ var c17SupplyChainInner = {
|
|
|
5749
5938
|
verdict: "no committed lockfile (package-lock.json/pnpm-lock.yaml/yarn.lock/bun.lock) found -> dependencies are not pinned; commit a lockfile (C-17a)"
|
|
5750
5939
|
};
|
|
5751
5940
|
}
|
|
5752
|
-
const
|
|
5941
|
+
const unreadable = [];
|
|
5942
|
+
const claim = findProvenanceClaim(tree, tree.root, unreadable);
|
|
5753
5943
|
const pkgVersion = typeof tree.packageJson?.json.version === "string" ? tree.packageJson.json.version : void 0;
|
|
5754
5944
|
const pkgName = typeof tree.packageJson?.json.name === "string" ? tree.packageJson.json.name : void 0;
|
|
5755
|
-
const record = claim ? null : findProvenanceRecord(tree.root, pkgName, pkgVersion);
|
|
5945
|
+
const record = claim ? null : findProvenanceRecord(tree.root, pkgName, pkgVersion, unreadable);
|
|
5756
5946
|
if (!claim && record?.ok) {
|
|
5757
5947
|
return {
|
|
5758
5948
|
id: this.id,
|
|
@@ -5769,6 +5959,13 @@ var c17SupplyChainInner = {
|
|
|
5769
5959
|
verdict: `lockfile ${lockfile} present and a \`Provenance:\` line at ${record.site}, but ${record.why} -> a record covers only the version it names and must carry the reproduced 40-hex shasum (C-17, non-CI path; proposed evidence form)`
|
|
5770
5960
|
};
|
|
5771
5961
|
}
|
|
5962
|
+
if (!claim && !record && unreadable.length > 0) {
|
|
5963
|
+
return couldNotJudge(
|
|
5964
|
+
this.id,
|
|
5965
|
+
unreadable[0].anchor,
|
|
5966
|
+
`no provenance claim in the readable places, but ${unreadable.length} candidate(s) were present and unread \u2014 ${unreadable.map((u) => u.reason).join("; ")} (C-17b)`
|
|
5967
|
+
);
|
|
5968
|
+
}
|
|
5772
5969
|
if (!claim) {
|
|
5773
5970
|
return {
|
|
5774
5971
|
id: this.id,
|
|
@@ -5790,7 +5987,7 @@ var c17SupplyChain = guardUnreadablePackageJson(c17SupplyChainInner, {
|
|
|
5790
5987
|
});
|
|
5791
5988
|
|
|
5792
5989
|
// src/detectors/c18-structure-manifest.ts
|
|
5793
|
-
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
5990
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, statSync as statSync8 } from "fs";
|
|
5794
5991
|
import { join as join7 } from "path";
|
|
5795
5992
|
var PIN5 = STANDARD_PIN;
|
|
5796
5993
|
function manifestPath(root) {
|
|
@@ -5815,16 +6012,31 @@ function rootCandidates(root) {
|
|
|
5815
6012
|
for (const name of SERVER_MANIFEST_NAMES2) {
|
|
5816
6013
|
const path = join7(root, name);
|
|
5817
6014
|
if (!existsSync6(path)) continue;
|
|
5818
|
-
const json = readJson(path);
|
|
5819
|
-
out.push({
|
|
6015
|
+
const { json, readError } = readJson(path);
|
|
6016
|
+
out.push({
|
|
6017
|
+
name,
|
|
6018
|
+
path,
|
|
6019
|
+
role: classifyRootJson(json),
|
|
6020
|
+
json,
|
|
6021
|
+
...readError ? { readError } : {}
|
|
6022
|
+
});
|
|
5820
6023
|
}
|
|
5821
6024
|
return out;
|
|
5822
6025
|
}
|
|
5823
6026
|
function readJson(path) {
|
|
6027
|
+
let text;
|
|
5824
6028
|
try {
|
|
5825
|
-
|
|
6029
|
+
if (!statSync8(path).isFile()) {
|
|
6030
|
+
return { json: null, readError: "not a regular file \u2014 not read" };
|
|
6031
|
+
}
|
|
6032
|
+
text = readFileSync7(path, "utf8");
|
|
6033
|
+
} catch (err) {
|
|
6034
|
+
return { json: null, readError: `could not be read (${fsErrorCode(err)})` };
|
|
6035
|
+
}
|
|
6036
|
+
try {
|
|
6037
|
+
return { json: JSON.parse(text) };
|
|
5826
6038
|
} catch {
|
|
5827
|
-
return null;
|
|
6039
|
+
return { json: null };
|
|
5828
6040
|
}
|
|
5829
6041
|
}
|
|
5830
6042
|
function c18PositiveControls() {
|
|
@@ -5883,6 +6095,14 @@ var c18StructureManifestInner = {
|
|
|
5883
6095
|
let manifestName = kind === "extension" ? "extension.json" : "mcp.json";
|
|
5884
6096
|
if (kind === "server") {
|
|
5885
6097
|
const cands = rootCandidates(tree.root);
|
|
6098
|
+
const unread = cands.find((c) => c.readError !== void 0);
|
|
6099
|
+
if (unread) {
|
|
6100
|
+
return couldNotJudge(
|
|
6101
|
+
this.id,
|
|
6102
|
+
unread.path,
|
|
6103
|
+
`${unread.name} is present but ${unread.readError} \u2014 the server manifest cannot be judged (C-18)`
|
|
6104
|
+
);
|
|
6105
|
+
}
|
|
5886
6106
|
const bad = cands.find((c) => c.role === "unparseable");
|
|
5887
6107
|
if (bad) {
|
|
5888
6108
|
return {
|
|
@@ -5923,7 +6143,15 @@ var c18StructureManifestInner = {
|
|
|
5923
6143
|
verdict: "no extension.json found at the tree root -> the extension manifest is required (C-18)"
|
|
5924
6144
|
};
|
|
5925
6145
|
}
|
|
5926
|
-
|
|
6146
|
+
const read = readJson(path);
|
|
6147
|
+
if (read.readError !== void 0) {
|
|
6148
|
+
return couldNotJudge(
|
|
6149
|
+
this.id,
|
|
6150
|
+
path,
|
|
6151
|
+
`${manifestName} is present but ${read.readError} \u2014 the manifest cannot be judged (C-18)`
|
|
6152
|
+
);
|
|
6153
|
+
}
|
|
6154
|
+
manifest = read.json;
|
|
5927
6155
|
if (!manifest) {
|
|
5928
6156
|
return {
|
|
5929
6157
|
id: this.id,
|
|
@@ -6368,7 +6596,7 @@ var c21ProgressiveDisclosure = {
|
|
|
6368
6596
|
};
|
|
6369
6597
|
|
|
6370
6598
|
// src/detectors/c22-test-ratio-gate.ts
|
|
6371
|
-
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
6599
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, statSync as statSync9 } from "fs";
|
|
6372
6600
|
import { join as join8 } from "path";
|
|
6373
6601
|
var DOC_FILES2 = ["README.md", "README.fr.md", "CHANGELOG.md"];
|
|
6374
6602
|
var RATIO_RE = /\b(?:coverage|ratio de tests?|test ratio|couverture)\b[^\n%]{0,40}?(\d{1,3}(?:\.\d+)?\s?%)/i;
|
|
@@ -6391,15 +6619,20 @@ function c22PositiveControls() {
|
|
|
6391
6619
|
identifierClearsBareRatio: !IDENTIFIER_RE.test(bareSample)
|
|
6392
6620
|
};
|
|
6393
6621
|
}
|
|
6394
|
-
function findRatioCitations(root) {
|
|
6622
|
+
function findRatioCitations(root, unreadable) {
|
|
6395
6623
|
const hits = [];
|
|
6396
6624
|
for (const name of DOC_FILES2) {
|
|
6397
6625
|
const path = join8(root, name);
|
|
6398
6626
|
if (!existsSync7(path)) continue;
|
|
6399
6627
|
let text;
|
|
6400
6628
|
try {
|
|
6629
|
+
if (!statSync9(path).isFile()) {
|
|
6630
|
+
unreadable.push({ anchor: name, reason: `${name} is not a regular file \u2014 not read` });
|
|
6631
|
+
continue;
|
|
6632
|
+
}
|
|
6401
6633
|
text = readFileSync8(path, "utf8");
|
|
6402
|
-
} catch {
|
|
6634
|
+
} catch (err) {
|
|
6635
|
+
unreadable.push({ anchor: name, reason: `${name} could not be read (${fsErrorCode(err)})` });
|
|
6403
6636
|
continue;
|
|
6404
6637
|
}
|
|
6405
6638
|
const lines = text.split("\n");
|
|
@@ -6417,44 +6650,48 @@ var c22TestRatioGate = {
|
|
|
6417
6650
|
rule: "C-22",
|
|
6418
6651
|
pin: STANDARD_PIN,
|
|
6419
6652
|
run(tree) {
|
|
6420
|
-
const
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6653
|
+
const unreadable = [];
|
|
6654
|
+
const decided = (() => {
|
|
6655
|
+
const controls = c22PositiveControls();
|
|
6656
|
+
if (!controls.ratioMatcher.matched || !controls.identifierMatcher.matched) {
|
|
6657
|
+
throw new Error(
|
|
6658
|
+
`C22 positive control FAILED: ratio=${controls.ratioMatcher.matched} identifier=${controls.identifierMatcher.matched} \u2014 matcher broken, verdict untrustworthy`
|
|
6659
|
+
);
|
|
6660
|
+
}
|
|
6661
|
+
if (!controls.identifierClearsBareRatio) {
|
|
6662
|
+
throw new Error(
|
|
6663
|
+
"C22 identifier-matcher control FAILED: must NOT flag a bare ratio with no identifier token \u2014 verdict untrustworthy"
|
|
6664
|
+
);
|
|
6665
|
+
}
|
|
6666
|
+
const hits = findRatioCitations(tree.root, unreadable);
|
|
6667
|
+
if (hits.length === 0) {
|
|
6668
|
+
return {
|
|
6669
|
+
id: this.id,
|
|
6670
|
+
anchor: "-",
|
|
6671
|
+
state: "absent",
|
|
6672
|
+
verdict: "no test/coverage ratio cited in README/CHANGELOG; test-ratio-gate check n/a"
|
|
6673
|
+
};
|
|
6674
|
+
}
|
|
6675
|
+
const bare = hits.find((h) => !h.hasIdentifier);
|
|
6676
|
+
if (bare) {
|
|
6677
|
+
return {
|
|
6678
|
+
id: this.id,
|
|
6679
|
+
anchor: `${bare.file}:${bare.line}`,
|
|
6680
|
+
state: "non-conforming",
|
|
6681
|
+
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)`
|
|
6682
|
+
};
|
|
6683
|
+
}
|
|
6684
|
+
const withId = hits[0];
|
|
6685
|
+
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`;
|
|
6442
6686
|
return {
|
|
6443
6687
|
id: this.id,
|
|
6444
|
-
anchor: `${
|
|
6445
|
-
state: "
|
|
6446
|
-
|
|
6688
|
+
anchor: `${withId.file}:${withId.line}`,
|
|
6689
|
+
state: "could-not-judge",
|
|
6690
|
+
reason,
|
|
6691
|
+
verdict: `could not judge: ${reason}`
|
|
6447
6692
|
};
|
|
6448
|
-
}
|
|
6449
|
-
|
|
6450
|
-
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`;
|
|
6451
|
-
return {
|
|
6452
|
-
id: this.id,
|
|
6453
|
-
anchor: `${withId.file}:${withId.line}`,
|
|
6454
|
-
state: "could-not-judge",
|
|
6455
|
-
reason,
|
|
6456
|
-
verdict: `could not judge: ${reason}`
|
|
6457
|
-
};
|
|
6693
|
+
})();
|
|
6694
|
+
return settle(this.id, decided, unreadable);
|
|
6458
6695
|
}
|
|
6459
6696
|
};
|
|
6460
6697
|
|
|
@@ -6605,14 +6842,21 @@ var c24AcceptanceAtDispatch = {
|
|
|
6605
6842
|
`C24 positive control FAILED: ${JSON.stringify(ctl)} \u2014 the dispatch matcher cannot tell an instrument job from another, verdict untrustworthy`
|
|
6606
6843
|
);
|
|
6607
6844
|
}
|
|
6608
|
-
const
|
|
6609
|
-
|
|
6845
|
+
const candidates = diskText(tree, (_name, rel) => DISPATCH_PATH_RE.test(rel));
|
|
6846
|
+
const docs = candidates.filter((d) => INSTRUMENT_RE.test(d.text) && ACTION_RE.test(d.text));
|
|
6847
|
+
const unreadDocs = refusedAsUnreadable(
|
|
6848
|
+
candidates.refused,
|
|
6849
|
+
"it may be an instrument dispatch document, so it is unread"
|
|
6610
6850
|
);
|
|
6611
6851
|
if (docs.length === 0) {
|
|
6612
|
-
return
|
|
6852
|
+
return settle(
|
|
6613
6853
|
this.id,
|
|
6614
|
-
|
|
6615
|
-
|
|
6854
|
+
absentResult(
|
|
6855
|
+
this.id,
|
|
6856
|
+
"-",
|
|
6857
|
+
"no instrument dispatch document (task / dispatch / brief / issue template naming a detector or conformance instrument and an action on it) in the tree \u2014 C-24 static half n/a; the gate half is the delivery gate's (positive control OK)"
|
|
6858
|
+
),
|
|
6859
|
+
unreadDocs
|
|
6616
6860
|
);
|
|
6617
6861
|
}
|
|
6618
6862
|
const lacking = docs.filter((d) => !hasAcceptanceBlock(d.text));
|
|
@@ -6949,7 +7193,7 @@ import {
|
|
|
6949
7193
|
readFileSync as readFileSync9,
|
|
6950
7194
|
readdirSync as readdirSync6,
|
|
6951
7195
|
rmSync,
|
|
6952
|
-
statSync as
|
|
7196
|
+
statSync as statSync10,
|
|
6953
7197
|
writeFileSync
|
|
6954
7198
|
} from "fs";
|
|
6955
7199
|
import { tmpdir } from "os";
|
|
@@ -6975,15 +7219,17 @@ function walkForConfigs(dir, depth, seen, onSkip) {
|
|
|
6975
7219
|
let entries;
|
|
6976
7220
|
try {
|
|
6977
7221
|
entries = readdirSync6(dir);
|
|
6978
|
-
} catch {
|
|
7222
|
+
} catch (err) {
|
|
7223
|
+
onSkip(dir, depth, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
6979
7224
|
return;
|
|
6980
7225
|
}
|
|
6981
7226
|
for (const name of entries) {
|
|
6982
7227
|
const full = join9(dir, name);
|
|
6983
7228
|
let st;
|
|
6984
7229
|
try {
|
|
6985
|
-
st =
|
|
6986
|
-
} catch {
|
|
7230
|
+
st = statSync10(full);
|
|
7231
|
+
} catch (err) {
|
|
7232
|
+
onSkip(full, depth + 1, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
6987
7233
|
continue;
|
|
6988
7234
|
}
|
|
6989
7235
|
if (st.isDirectory()) {
|
|
@@ -6993,6 +7239,10 @@ function walkForConfigs(dir, depth, seen, onSkip) {
|
|
|
6993
7239
|
}
|
|
6994
7240
|
walkForConfigs(full, depth + 1, seen, onSkip);
|
|
6995
7241
|
} else if (DEPLOY_CONFIG_FILES.includes(name)) {
|
|
7242
|
+
if (!st.isFile()) {
|
|
7243
|
+
onSkip(full, depth + 1, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
7244
|
+
continue;
|
|
7245
|
+
}
|
|
6996
7246
|
if (!seen.has(dir)) {
|
|
6997
7247
|
seen.set(dir, { dir, configFile: name, configPath: full });
|
|
6998
7248
|
}
|
|
@@ -7012,7 +7262,7 @@ function probeForCandidate(dir, depth) {
|
|
|
7012
7262
|
const full = join9(dir, name);
|
|
7013
7263
|
let st;
|
|
7014
7264
|
try {
|
|
7015
|
-
st =
|
|
7265
|
+
st = statSync10(full);
|
|
7016
7266
|
} catch {
|
|
7017
7267
|
continue;
|
|
7018
7268
|
}
|
|
@@ -7054,12 +7304,12 @@ function resolveRailwayRootDirectory(configPath) {
|
|
|
7054
7304
|
function c26DeriveScope(root, tree) {
|
|
7055
7305
|
const seen = /* @__PURE__ */ new Map();
|
|
7056
7306
|
const skipped = [];
|
|
7057
|
-
const onSkip = (skippedDir, depth) => {
|
|
7058
|
-
const entry = tree ? recordSkip(tree, skippedDir, root) : {
|
|
7307
|
+
const onSkip = (skippedDir, depth, reason) => {
|
|
7308
|
+
const entry = tree ? recordSkip(tree, skippedDir, root, reason) : {
|
|
7059
7309
|
path: relative5(root, skippedDir).split(sep4).join("/"),
|
|
7060
|
-
reason: SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
7310
|
+
reason: reason ?? SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
7061
7311
|
};
|
|
7062
|
-
if (SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
7312
|
+
if (reason === void 0 && SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
7063
7313
|
const hit = probeForCandidate(skippedDir, depth);
|
|
7064
7314
|
if (hit) entry.candidate = relative5(root, hit).split(sep4).join("/");
|
|
7065
7315
|
}
|
|
@@ -7185,8 +7435,22 @@ function gateSite(site) {
|
|
|
7185
7435
|
}
|
|
7186
7436
|
const isolated = mkdtempSync(join9(tmpdir(), "mcp-doctor-c26-"));
|
|
7187
7437
|
try {
|
|
7188
|
-
|
|
7189
|
-
|
|
7438
|
+
let manifestText;
|
|
7439
|
+
let lockText;
|
|
7440
|
+
const lockPath = join9(site.dir, lock.name);
|
|
7441
|
+
try {
|
|
7442
|
+
for (const p of [pkgPath, lockPath]) {
|
|
7443
|
+
if (!statSync10(p).isFile()) throw new Error(`${p} is not a regular file`);
|
|
7444
|
+
}
|
|
7445
|
+
manifestText = readFileSync9(pkgPath, "utf8");
|
|
7446
|
+
lockText = readFileSync9(lockPath, "utf8");
|
|
7447
|
+
} catch (err) {
|
|
7448
|
+
return {
|
|
7449
|
+
site,
|
|
7450
|
+
status: "unreadable",
|
|
7451
|
+
reason: `${site.dir}: package.json or ${lock.name} is present but cannot be read (${err instanceof Error ? err.message : String(err)}) \u2014 the frozen install was not run`
|
|
7452
|
+
};
|
|
7453
|
+
}
|
|
7190
7454
|
writeFileSync(join9(isolated, "package.json"), manifestText);
|
|
7191
7455
|
writeFileSync(join9(isolated, lock.name), lockText);
|
|
7192
7456
|
let proc;
|
|
@@ -7247,6 +7511,7 @@ function renderCoverage(scope, verdicts) {
|
|
|
7247
7511
|
return verdicts.map((v) => {
|
|
7248
7512
|
const rel = relative5(process.cwd(), v.site.dir) || v.site.dir;
|
|
7249
7513
|
if (v.status === "skipped") return `SKIPPED ${rel} (${v.reason})`;
|
|
7514
|
+
if (v.status === "unreadable") return `UNREAD ${rel} (${v.reason})`;
|
|
7250
7515
|
return `ANALYSED ${rel} -> ${v.ok ? "GREEN" : "RED"}: ${v.detail}`;
|
|
7251
7516
|
}).join(" | ");
|
|
7252
7517
|
}
|
|
@@ -7289,6 +7554,17 @@ var c26SubpackageLockfile = {
|
|
|
7289
7554
|
const verdicts = scope.sites.map(gateSite);
|
|
7290
7555
|
const inventory = renderCoverage(scope.sites, verdicts);
|
|
7291
7556
|
const analysed = verdicts.filter((v) => v.status === "analysed");
|
|
7557
|
+
const unread = verdicts.filter((v) => v.status === "unreadable");
|
|
7558
|
+
if (unread.length > 0 && !analysed.some((v) => v.ok === false)) {
|
|
7559
|
+
const reason = `${unread.length} deployable site(s) could not be read \u2014 ${unread.map((v) => v.reason).join("; ")}. Coverage: ${inventory}`;
|
|
7560
|
+
return {
|
|
7561
|
+
id: this.id,
|
|
7562
|
+
anchor: unread[0].site.dir,
|
|
7563
|
+
state: "could-not-judge",
|
|
7564
|
+
reason,
|
|
7565
|
+
verdict: `could not judge: ${reason}`
|
|
7566
|
+
};
|
|
7567
|
+
}
|
|
7292
7568
|
if (analysed.length === 0) {
|
|
7293
7569
|
if (hidden.length > 0) return unjudgedHidden("no analysable site; ");
|
|
7294
7570
|
return {
|
|
@@ -7428,22 +7704,21 @@ var c27SubpackageTypes = {
|
|
|
7428
7704
|
const workflows = governingWorkflows(tree);
|
|
7429
7705
|
const reds = [];
|
|
7430
7706
|
const oks = [];
|
|
7707
|
+
const unreadDocs = [];
|
|
7431
7708
|
for (const site of tsSites) {
|
|
7432
7709
|
const siteRel = relative6(base, site.dir) || ".";
|
|
7433
7710
|
const docs = [];
|
|
7434
7711
|
const dockerfile = join10(site.dir, "Dockerfile");
|
|
7435
7712
|
if (existsSync9(dockerfile)) {
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
});
|
|
7713
|
+
const text = readText(dockerfile);
|
|
7714
|
+
if (text === null) unreadDocs.push(`${siteRel}/Dockerfile`);
|
|
7715
|
+
else docs.push({ where: `${siteRel}/Dockerfile`, segments: dockerSegments(text) });
|
|
7440
7716
|
}
|
|
7441
7717
|
const railway = join10(site.dir, "railway.json");
|
|
7442
7718
|
if (existsSync9(railway)) {
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
});
|
|
7719
|
+
const text = readText(railway);
|
|
7720
|
+
if (text === null) unreadDocs.push(`${siteRel}/railway.json`);
|
|
7721
|
+
else docs.push({ where: `${siteRel}/railway.json`, segments: railwayBuildCommand(text) });
|
|
7447
7722
|
}
|
|
7448
7723
|
for (const w of workflows) {
|
|
7449
7724
|
if (siteRel === "." || w.text.includes(siteRel)) {
|
|
@@ -7456,6 +7731,7 @@ var c27SubpackageTypes = {
|
|
|
7456
7731
|
oks.push(`${ok.d.where} (${siteRel})`);
|
|
7457
7732
|
continue;
|
|
7458
7733
|
}
|
|
7734
|
+
if (unreadDocs.some((u) => u.startsWith(`${siteRel}/`))) continue;
|
|
7459
7735
|
const unfrozen = verdicts.find((v) => v.phase === "unfrozen");
|
|
7460
7736
|
const early = verdicts.find((v) => v.phase === "before-install");
|
|
7461
7737
|
const why = unfrozen ? `a type check follows a non-frozen install at ${unfrozen.d.where} (the tree is not the lockfile's: use a frozen install)` : early ? `a type check at ${early.d.where} runs before any install (it checks the workspace's tree, not the isolation's)` : "no type check runs in the deployment isolation (no tsc / typecheck step after a frozen install)";
|
|
@@ -7468,6 +7744,13 @@ var c27SubpackageTypes = {
|
|
|
7468
7744
|
`${reds.length} TypeScript deployable(s): ${reds.join("; ")} -> run the sub-package type check inside the isolation the deployment installs, after the frozen install (C-27)`
|
|
7469
7745
|
);
|
|
7470
7746
|
}
|
|
7747
|
+
if (unreadDocs.length > 0) {
|
|
7748
|
+
return couldNotJudge(
|
|
7749
|
+
this.id,
|
|
7750
|
+
`${unreadDocs[0]}:1`,
|
|
7751
|
+
`${unreadDocs.map((u) => `\`${u}\``).join(", ")} is present but cannot be read (permission, or not a regular file): whether the deployment type-checks in its isolation is unread \u2014 never "no type check" (C-27)`
|
|
7752
|
+
);
|
|
7753
|
+
}
|
|
7471
7754
|
return couldNotJudge(
|
|
7472
7755
|
this.id,
|
|
7473
7756
|
oks[0],
|
|
@@ -7605,7 +7888,10 @@ var c29ServedSkills = {
|
|
|
7605
7888
|
const missing = [...methods].filter(([, a]) => a.length === 0).map(([m]) => m);
|
|
7606
7889
|
const reds = [];
|
|
7607
7890
|
const helpers = helperImports(tree, /skill/i);
|
|
7608
|
-
const unreadable = [
|
|
7891
|
+
const unreadable = [
|
|
7892
|
+
...surface.unreadable,
|
|
7893
|
+
...refusedAsUnreadable(skillFiles.refused, "its name was not compared to its directory")
|
|
7894
|
+
];
|
|
7609
7895
|
if (missing.length > 0) {
|
|
7610
7896
|
if (helpers.length > 0) {
|
|
7611
7897
|
unreadable.push({
|
|
@@ -7697,7 +7983,7 @@ var c30SkillsOrgBound = {
|
|
|
7697
7983
|
};
|
|
7698
7984
|
|
|
7699
7985
|
// src/detectors/c31-plugin-installable.ts
|
|
7700
|
-
import { existsSync as existsSync10, readdirSync as readdirSync8, statSync as
|
|
7986
|
+
import { existsSync as existsSync10, readdirSync as readdirSync8, statSync as statSync11 } from "fs";
|
|
7701
7987
|
import { basename as basename3, dirname as dirname4, join as join11, relative as relative8, sep as sep5 } from "path";
|
|
7702
7988
|
var MIN_README_WORDS = 40;
|
|
7703
7989
|
var KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -7898,12 +8184,17 @@ function judgeFolder(tree, folder, manifestPath2, out) {
|
|
|
7898
8184
|
}
|
|
7899
8185
|
const hasLicenseField = typeof manifest.license === "string" && manifest.license.trim() !== "";
|
|
7900
8186
|
let hasLicenseFile = false;
|
|
8187
|
+
let folderListed = true;
|
|
7901
8188
|
try {
|
|
7902
8189
|
hasLicenseFile = readdirSync8(folder).some((n) => /^LICEN[CS]E/i.test(n));
|
|
7903
|
-
} catch {
|
|
7904
|
-
|
|
8190
|
+
} catch (err) {
|
|
8191
|
+
folderListed = false;
|
|
8192
|
+
out.unreadable.push({
|
|
8193
|
+
anchor: rel(folder),
|
|
8194
|
+
reason: `${rel(folder)} cannot be listed (${fsErrorCode(err)}) \u2014 whether it carries a LICENSE file is unread`
|
|
8195
|
+
});
|
|
7905
8196
|
}
|
|
7906
|
-
if (!hasLicenseField && !hasLicenseFile) {
|
|
8197
|
+
if (!hasLicenseField && !hasLicenseFile && folderListed) {
|
|
7907
8198
|
out.reds.push({
|
|
7908
8199
|
anchor: a,
|
|
7909
8200
|
text: "no licence: plugin.json has no `license` and the folder has no LICENSE file"
|
|
@@ -7911,7 +8202,12 @@ function judgeFolder(tree, folder, manifestPath2, out) {
|
|
|
7911
8202
|
}
|
|
7912
8203
|
const readmePath = join11(folder, "README.md");
|
|
7913
8204
|
const readme = readText(readmePath);
|
|
7914
|
-
if (readme === null) {
|
|
8205
|
+
if (readme === null && existsSync10(readmePath)) {
|
|
8206
|
+
out.unreadable.push({
|
|
8207
|
+
anchor: rel(readmePath),
|
|
8208
|
+
reason: `${rel(readmePath)} is present but cannot be read as text (permission, or not a regular file) \u2014 its length and credential content are unread`
|
|
8209
|
+
});
|
|
8210
|
+
} else if (readme === null) {
|
|
7915
8211
|
out.reds.push({ anchor: rel(folder), text: "no README.md at the plugin folder root" });
|
|
7916
8212
|
} else {
|
|
7917
8213
|
const n = proseWordCount(readme);
|
|
@@ -7971,13 +8267,20 @@ function judgeFolder(tree, folder, manifestPath2, out) {
|
|
|
7971
8267
|
judgeServers(servers, mcpAnchor, out);
|
|
7972
8268
|
}
|
|
7973
8269
|
try {
|
|
7974
|
-
if (
|
|
8270
|
+
if (statSync11(join11(folder, "bin")).isDirectory()) {
|
|
7975
8271
|
out.reds.push({
|
|
7976
8272
|
anchor: rel(join11(folder, "bin")),
|
|
7977
8273
|
text: "a top-level bin/ directory (a plugin ships no bin/ at its top level)"
|
|
7978
8274
|
});
|
|
7979
8275
|
}
|
|
7980
|
-
} catch {
|
|
8276
|
+
} catch (err) {
|
|
8277
|
+
const code = fsErrorCode(err);
|
|
8278
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
8279
|
+
out.unreadable.push({
|
|
8280
|
+
anchor: rel(join11(folder, "bin")),
|
|
8281
|
+
reason: `${rel(join11(folder, "bin"))} could not be probed (${code}) \u2014 whether the plugin ships a top-level bin/ is unread`
|
|
8282
|
+
});
|
|
8283
|
+
}
|
|
7981
8284
|
}
|
|
7982
8285
|
const jsons = [[a, manifest]];
|
|
7983
8286
|
if (servers) jsons.push([mcpAnchor, servers]);
|
|
@@ -8007,14 +8310,18 @@ var c31PluginInstallable = {
|
|
|
8007
8310
|
tree,
|
|
8008
8311
|
(_n, rel) => rel === ".claude-plugin/plugin.json" || rel.endsWith("/.claude-plugin/plugin.json")
|
|
8009
8312
|
);
|
|
8313
|
+
const refusedManifests = refusedAsUnreadable(
|
|
8314
|
+
manifests.refused,
|
|
8315
|
+
"this plugin folder's manifest is unread, so it was not judged"
|
|
8316
|
+
);
|
|
8010
8317
|
if (manifests.length === 0) {
|
|
8011
8318
|
return couldNotJudge(
|
|
8012
8319
|
this.id,
|
|
8013
|
-
tree.root,
|
|
8014
|
-
`no plugin folder could be located under ${tree.root} (a directory holding .claude-plugin/plugin.json) \u2014 whether the object installs as a Claude plugin is unjudged (C-31)`
|
|
8320
|
+
refusedManifests[0]?.anchor ?? tree.root,
|
|
8321
|
+
refusedManifests.length > 0 ? `${refusedManifests.map((u) => u.reason).join("; ")} \u2014 whether the object installs as a Claude plugin is unjudged (C-31)` : `no plugin folder could be located under ${tree.root} (a directory holding .claude-plugin/plugin.json) \u2014 whether the object installs as a Claude plugin is unjudged (C-31)`
|
|
8015
8322
|
);
|
|
8016
8323
|
}
|
|
8017
|
-
const out = { reds: [], unreadable: [] };
|
|
8324
|
+
const out = { reds: [], unreadable: [...refusedManifests] };
|
|
8018
8325
|
for (const m of manifests) judgeFolder(tree, dirname4(dirname4(m.path)), m.path, out);
|
|
8019
8326
|
const anchor = manifests[0].rel;
|
|
8020
8327
|
if (out.reds.length > 0) {
|
|
@@ -8484,9 +8791,10 @@ function sdkV2Range(tree) {
|
|
|
8484
8791
|
}
|
|
8485
8792
|
return null;
|
|
8486
8793
|
}
|
|
8794
|
+
var isManifest = (n) => n === "mcp.json" || n === "mcp-server.json";
|
|
8487
8795
|
function triggers(tree) {
|
|
8488
8796
|
const out = [];
|
|
8489
|
-
for (const f of findDiskFiles(tree,
|
|
8797
|
+
for (const f of findDiskFiles(tree, isManifest)) {
|
|
8490
8798
|
const j = readJsonRecord(f.path);
|
|
8491
8799
|
if (!j || isRecord(j.mcpServers)) continue;
|
|
8492
8800
|
const v = j.minProtocolVersion;
|
|
@@ -8559,12 +8867,19 @@ var d5 = {
|
|
|
8559
8867
|
verdict: `no server/discover handler (grep=0, positive-control OK) although this server targets ${TARGET}: ${why.join("; ")} -> respond to server/discover with supportedVersions and capabilities, and announce the same capability set as initialize (S-4)`
|
|
8560
8868
|
};
|
|
8561
8869
|
}
|
|
8562
|
-
return
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8870
|
+
return settle(
|
|
8871
|
+
this.id,
|
|
8872
|
+
{
|
|
8873
|
+
id: this.id,
|
|
8874
|
+
anchor: "-",
|
|
8875
|
+
state: "absent",
|
|
8876
|
+
verdict: "no server/discover handler (grep=0, positive-control OK); no closed trigger found (no manifest minProtocolVersion >= 2026-07-28, no events capability, no host extension) -> S-4 does not oblige; add via protocol-kit when targeting 2026-07-28"
|
|
8877
|
+
},
|
|
8878
|
+
refusedAsUnreadable(
|
|
8879
|
+
findDiskFiles(tree, isManifest).refused,
|
|
8880
|
+
"its minProtocolVersion (the trigger of S-4) is unread"
|
|
8881
|
+
)
|
|
8882
|
+
);
|
|
8568
8883
|
}
|
|
8569
8884
|
};
|
|
8570
8885
|
|
|
@@ -8757,7 +9072,7 @@ var d7Inner = {
|
|
|
8757
9072
|
var d7 = guardUnreadablePackageJson(d7Inner, { authoritative: true });
|
|
8758
9073
|
|
|
8759
9074
|
// src/detectors/d8.ts
|
|
8760
|
-
import { existsSync as existsSync11, statSync as
|
|
9075
|
+
import { existsSync as existsSync11, statSync as statSync12 } from "fs";
|
|
8761
9076
|
import { dirname as dirname5, join as join12, relative as relative9 } from "path";
|
|
8762
9077
|
var POSITIVE_CONTROL2 = 'server.registerTool("list_items", { ... }, handler);';
|
|
8763
9078
|
function d8PositiveControl() {
|
|
@@ -8770,7 +9085,7 @@ function d8PositiveControl() {
|
|
|
8770
9085
|
function resolveBaseDir(tree) {
|
|
8771
9086
|
if (tree.packageJson) return dirname5(tree.packageJson.path);
|
|
8772
9087
|
try {
|
|
8773
|
-
return
|
|
9088
|
+
return statSync12(tree.root).isDirectory() ? tree.root : dirname5(tree.root);
|
|
8774
9089
|
} catch {
|
|
8775
9090
|
return tree.root;
|
|
8776
9091
|
}
|
|
@@ -8904,6 +9219,13 @@ var d8 = {
|
|
|
8904
9219
|
verdict: "server advertises no tools at runtime -> n/a for eval coverage"
|
|
8905
9220
|
};
|
|
8906
9221
|
}
|
|
9222
|
+
if (runtime.evalsUnreadable) {
|
|
9223
|
+
return couldNotJudge(
|
|
9224
|
+
this.id,
|
|
9225
|
+
`${runtime.root} (runtime tools/list)`,
|
|
9226
|
+
`the eval file is present but unread \u2014 ${runtime.evalsUnreadable}; per-tool coverage is unknown, never zero (Server Rule 5)`
|
|
9227
|
+
);
|
|
9228
|
+
}
|
|
8907
9229
|
const summary = runtime.tools.map((t) => `${t.name}=${t.evalCaseCount ?? 0}`).join(" ");
|
|
8908
9230
|
const under = runtime.tools.filter((t) => (t.evalCaseCount ?? 0) < 3);
|
|
8909
9231
|
if (under.length > 0) {
|
|
@@ -9529,7 +9851,7 @@ var d15 = {
|
|
|
9529
9851
|
};
|
|
9530
9852
|
|
|
9531
9853
|
// src/detectors/d16.ts
|
|
9532
|
-
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as
|
|
9854
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync13 } from "fs";
|
|
9533
9855
|
import { dirname as dirname8, join as join15 } from "path";
|
|
9534
9856
|
var DISCOVER_RE2 = /server\/discover|DiscoverResult|ServerDiscover|handleServerDiscover/;
|
|
9535
9857
|
var HEALTH_ROUTE_FRAMEWORK_RE = /\b[A-Za-z_$][\w$]*\.(?:get|post|put|patch|delete|all|route|use)\s*\(\s*["'`][^"'`]*\/health/;
|
|
@@ -9851,10 +10173,11 @@ var d16 = {
|
|
|
9851
10173
|
};
|
|
9852
10174
|
function readPackageVersion(root) {
|
|
9853
10175
|
try {
|
|
9854
|
-
let dir =
|
|
10176
|
+
let dir = statSync13(root).isDirectory() ? root : dirname8(root);
|
|
9855
10177
|
for (let depth = 0; depth < 8; depth++) {
|
|
9856
10178
|
const candidate = join15(dir, "package.json");
|
|
9857
10179
|
if (existsSync14(candidate)) {
|
|
10180
|
+
if (!statSync13(candidate).isFile()) return null;
|
|
9858
10181
|
const pkg = JSON.parse(readFileSync10(candidate, "utf8"));
|
|
9859
10182
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
9860
10183
|
}
|
|
@@ -9869,7 +10192,7 @@ function readPackageVersion(root) {
|
|
|
9869
10192
|
}
|
|
9870
10193
|
|
|
9871
10194
|
// src/detectors/dAppI18n.ts
|
|
9872
|
-
import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
|
|
10195
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync14 } from "fs";
|
|
9873
10196
|
import { dirname as dirname9, join as join16 } from "path";
|
|
9874
10197
|
function flattenKeys(obj, prefix = "") {
|
|
9875
10198
|
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
@@ -9903,6 +10226,7 @@ function dAppI18nPositiveControls() {
|
|
|
9903
10226
|
}
|
|
9904
10227
|
function readJson2(path) {
|
|
9905
10228
|
try {
|
|
10229
|
+
if (!statSync14(path).isFile()) return { ok: false };
|
|
9906
10230
|
return { ok: true, value: JSON.parse(readFileSync11(path, "utf8")) };
|
|
9907
10231
|
} catch {
|
|
9908
10232
|
return { ok: false };
|
|
@@ -10101,14 +10425,14 @@ var dDebugOptIn = {
|
|
|
10101
10425
|
};
|
|
10102
10426
|
|
|
10103
10427
|
// src/detectors/extManifests.ts
|
|
10104
|
-
import { readdirSync as readdirSync9, statSync as
|
|
10428
|
+
import { readdirSync as readdirSync9, statSync as statSync15 } from "fs";
|
|
10105
10429
|
import { join as join17 } from "path";
|
|
10106
10430
|
function findExtensionManifests(tree) {
|
|
10107
10431
|
const out = [];
|
|
10108
10432
|
const root = tree.root;
|
|
10109
10433
|
let base;
|
|
10110
10434
|
try {
|
|
10111
|
-
base =
|
|
10435
|
+
base = statSync15(root).isDirectory() ? root : join17(root, "..");
|
|
10112
10436
|
} catch {
|
|
10113
10437
|
return out;
|
|
10114
10438
|
}
|
|
@@ -10116,15 +10440,17 @@ function findExtensionManifests(tree) {
|
|
|
10116
10440
|
let entries;
|
|
10117
10441
|
try {
|
|
10118
10442
|
entries = readdirSync9(dir);
|
|
10119
|
-
} catch {
|
|
10443
|
+
} catch (err) {
|
|
10444
|
+
recordSkip(tree, dir, base, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
10120
10445
|
return;
|
|
10121
10446
|
}
|
|
10122
10447
|
for (const name of entries) {
|
|
10123
10448
|
const full = join17(dir, name);
|
|
10124
10449
|
let st;
|
|
10125
10450
|
try {
|
|
10126
|
-
st =
|
|
10127
|
-
} catch {
|
|
10451
|
+
st = statSync15(full);
|
|
10452
|
+
} catch (err) {
|
|
10453
|
+
recordSkip(tree, full, base, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
10128
10454
|
continue;
|
|
10129
10455
|
}
|
|
10130
10456
|
if (st.isDirectory()) {
|
|
@@ -10134,6 +10460,10 @@ function findExtensionManifests(tree) {
|
|
|
10134
10460
|
}
|
|
10135
10461
|
walk2(full);
|
|
10136
10462
|
} else if (name === "extension.json") {
|
|
10463
|
+
if (!st.isFile()) {
|
|
10464
|
+
recordSkip(tree, full, base, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
10465
|
+
continue;
|
|
10466
|
+
}
|
|
10137
10467
|
out.push(full);
|
|
10138
10468
|
}
|
|
10139
10469
|
}
|
|
@@ -10750,8 +11080,8 @@ var dExt3 = {
|
|
|
10750
11080
|
};
|
|
10751
11081
|
|
|
10752
11082
|
// src/detectors/dExt4.ts
|
|
10753
|
-
import { readdirSync as readdirSync10, statSync as
|
|
10754
|
-
import { join as join19 } from "path";
|
|
11083
|
+
import { readdirSync as readdirSync10, statSync as statSync16 } from "fs";
|
|
11084
|
+
import { join as join19, relative as relative10 } from "path";
|
|
10755
11085
|
function canonical(name) {
|
|
10756
11086
|
return name.replace(/[-_]/g, "").toLowerCase();
|
|
10757
11087
|
}
|
|
@@ -10770,28 +11100,39 @@ function dExt4PositiveControls() {
|
|
|
10770
11100
|
}
|
|
10771
11101
|
};
|
|
10772
11102
|
}
|
|
10773
|
-
function findHooksDirs(tree) {
|
|
11103
|
+
function findHooksDirs(tree, unreadable, gaps) {
|
|
10774
11104
|
const root = tree.root;
|
|
10775
11105
|
const out = [];
|
|
10776
11106
|
let base;
|
|
10777
11107
|
try {
|
|
10778
|
-
base =
|
|
11108
|
+
base = statSync16(root).isDirectory() ? root : join19(root, "..");
|
|
10779
11109
|
} catch {
|
|
10780
11110
|
return out;
|
|
10781
11111
|
}
|
|
11112
|
+
const noteUnread = (path, what, err) => {
|
|
11113
|
+
gaps.count++;
|
|
11114
|
+
const anchor = relative10(base, path) || ".";
|
|
11115
|
+
const reason = `${anchor} ${what} (${fsErrorCode(err)}) \u2014 a hooks/ directory under it may exist unseen, so the implemented hooks are unverifiable (Extension Rule 4)`;
|
|
11116
|
+
if (!unreadable.some((u) => u.anchor === anchor && u.reason === reason)) {
|
|
11117
|
+
unreadable.push({ anchor, reason });
|
|
11118
|
+
}
|
|
11119
|
+
};
|
|
10782
11120
|
const walk2 = (dir) => {
|
|
10783
11121
|
let entries;
|
|
10784
11122
|
try {
|
|
10785
11123
|
entries = readdirSync10(dir);
|
|
10786
|
-
} catch {
|
|
11124
|
+
} catch (err) {
|
|
11125
|
+
noteUnread(dir, "could not be listed", err);
|
|
10787
11126
|
return;
|
|
10788
11127
|
}
|
|
10789
11128
|
for (const name of entries) {
|
|
10790
11129
|
const full = join19(dir, name);
|
|
10791
11130
|
let st;
|
|
10792
11131
|
try {
|
|
10793
|
-
st =
|
|
10794
|
-
} catch {
|
|
11132
|
+
st = statSync16(full);
|
|
11133
|
+
} catch (err) {
|
|
11134
|
+
if (fsErrorCode(err) !== "ENOENT" && fsErrorCode(err) !== "ELOOP")
|
|
11135
|
+
noteUnread(full, "could not be read", err);
|
|
10795
11136
|
continue;
|
|
10796
11137
|
}
|
|
10797
11138
|
if (!st.isDirectory()) continue;
|
|
@@ -10804,7 +11145,8 @@ function findHooksDirs(tree) {
|
|
|
10804
11145
|
let files;
|
|
10805
11146
|
try {
|
|
10806
11147
|
files = readdirSync10(full);
|
|
10807
|
-
} catch {
|
|
11148
|
+
} catch (err) {
|
|
11149
|
+
noteUnread(full, "could not be listed", err);
|
|
10808
11150
|
files = [];
|
|
10809
11151
|
}
|
|
10810
11152
|
for (const f of files) {
|
|
@@ -10882,7 +11224,8 @@ var dExt4 = {
|
|
|
10882
11224
|
}
|
|
10883
11225
|
const unreadable = [];
|
|
10884
11226
|
const decided = (() => {
|
|
10885
|
-
const
|
|
11227
|
+
const hooksDirGaps = { count: 0 };
|
|
11228
|
+
const hooksDirs = findHooksDirs(tree, unreadable, hooksDirGaps);
|
|
10886
11229
|
const { sites, unresolved } = findFactorySites(tree);
|
|
10887
11230
|
for (const anchor of unresolved) {
|
|
10888
11231
|
unreadable.push({
|
|
@@ -10926,6 +11269,7 @@ var dExt4 = {
|
|
|
10926
11269
|
continue;
|
|
10927
11270
|
}
|
|
10928
11271
|
const phantom = [...declared].filter((h) => !implemented.has(h));
|
|
11272
|
+
if (phantom.length > 0 && hooksDirGaps.count > 0) continue;
|
|
10929
11273
|
if (phantom.length > 0) {
|
|
10930
11274
|
return {
|
|
10931
11275
|
id: this.id,
|
|
@@ -11313,30 +11657,41 @@ var dExt8 = {
|
|
|
11313
11657
|
};
|
|
11314
11658
|
|
|
11315
11659
|
// src/detectors/dExt9.ts
|
|
11316
|
-
import { readFileSync as readFileSync13, readdirSync as readdirSync11, statSync as
|
|
11317
|
-
import { join as join23 } from "path";
|
|
11318
|
-
function collectHooksDirSources(tree, unreadable) {
|
|
11660
|
+
import { readFileSync as readFileSync13, readdirSync as readdirSync11, statSync as statSync17 } from "fs";
|
|
11661
|
+
import { join as join23, relative as relative11 } from "path";
|
|
11662
|
+
function collectHooksDirSources(tree, unreadable, gaps) {
|
|
11319
11663
|
const root = tree.root;
|
|
11320
11664
|
const out = [];
|
|
11321
11665
|
let base;
|
|
11322
11666
|
try {
|
|
11323
|
-
base =
|
|
11667
|
+
base = statSync17(root).isDirectory() ? root : join23(root, "..");
|
|
11324
11668
|
} catch {
|
|
11325
11669
|
return out;
|
|
11326
11670
|
}
|
|
11671
|
+
const noteUnread = (path, what, err) => {
|
|
11672
|
+
gaps.count++;
|
|
11673
|
+
const anchor = relative11(base, path) || ".";
|
|
11674
|
+
const reason = `${anchor} ${what} (${fsErrorCode(err)}) \u2014 a hooks/ directory under it may exist unseen, so the timeout guards are undeterminable (Extension Rule 9)`;
|
|
11675
|
+
if (!unreadable.some((u) => u.anchor === anchor && u.reason === reason)) {
|
|
11676
|
+
unreadable.push({ anchor, reason });
|
|
11677
|
+
}
|
|
11678
|
+
};
|
|
11327
11679
|
const walk2 = (dir) => {
|
|
11328
11680
|
let entries;
|
|
11329
11681
|
try {
|
|
11330
11682
|
entries = readdirSync11(dir);
|
|
11331
|
-
} catch {
|
|
11683
|
+
} catch (err) {
|
|
11684
|
+
noteUnread(dir, "could not be listed", err);
|
|
11332
11685
|
return;
|
|
11333
11686
|
}
|
|
11334
11687
|
for (const name of entries) {
|
|
11335
11688
|
const full = join23(dir, name);
|
|
11336
11689
|
let st;
|
|
11337
11690
|
try {
|
|
11338
|
-
st =
|
|
11339
|
-
} catch {
|
|
11691
|
+
st = statSync17(full);
|
|
11692
|
+
} catch (err) {
|
|
11693
|
+
if (fsErrorCode(err) !== "ENOENT" && fsErrorCode(err) !== "ELOOP")
|
|
11694
|
+
noteUnread(full, "could not be read", err);
|
|
11340
11695
|
continue;
|
|
11341
11696
|
}
|
|
11342
11697
|
if (!st.isDirectory()) continue;
|
|
@@ -11348,7 +11703,8 @@ function collectHooksDirSources(tree, unreadable) {
|
|
|
11348
11703
|
let files;
|
|
11349
11704
|
try {
|
|
11350
11705
|
files = readdirSync11(full);
|
|
11351
|
-
} catch {
|
|
11706
|
+
} catch (err) {
|
|
11707
|
+
noteUnread(full, "could not be listed", err);
|
|
11352
11708
|
files = [];
|
|
11353
11709
|
}
|
|
11354
11710
|
for (const f of files) {
|
|
@@ -11358,6 +11714,7 @@ function collectHooksDirSources(tree, unreadable) {
|
|
|
11358
11714
|
const filePath = join23(full, f);
|
|
11359
11715
|
let body;
|
|
11360
11716
|
try {
|
|
11717
|
+
if (!statSync17(filePath).isFile()) throw new Error("not a regular file");
|
|
11361
11718
|
body = readFileSync13(filePath, "utf8");
|
|
11362
11719
|
} catch {
|
|
11363
11720
|
const shortPath = filePath.split("/").slice(-2).join("/");
|
|
@@ -11461,8 +11818,9 @@ var dExt9 = {
|
|
|
11461
11818
|
}
|
|
11462
11819
|
const unreadable = [];
|
|
11463
11820
|
const decided = (() => {
|
|
11821
|
+
const hooksDirGaps = { count: 0 };
|
|
11464
11822
|
const hookSources = [
|
|
11465
|
-
...collectHooksDirSources(tree, unreadable),
|
|
11823
|
+
...collectHooksDirSources(tree, unreadable, hooksDirGaps),
|
|
11466
11824
|
...collectFactorySources(tree)
|
|
11467
11825
|
];
|
|
11468
11826
|
const implByToken = /* @__PURE__ */ new Map();
|
|
@@ -11511,6 +11869,7 @@ var dExt9 = {
|
|
|
11511
11869
|
const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
|
|
11512
11870
|
if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
|
|
11513
11871
|
}
|
|
11872
|
+
if (noTimeout.length > 0 && hooksDirGaps.count > 0) continue;
|
|
11514
11873
|
if (noTimeout.length > 0) {
|
|
11515
11874
|
return {
|
|
11516
11875
|
id: this.id,
|
|
@@ -12524,7 +12883,6 @@ var dServer14 = {
|
|
|
12524
12883
|
};
|
|
12525
12884
|
|
|
12526
12885
|
// src/detectors/e5-extension-isolation.ts
|
|
12527
|
-
import { readFileSync as readFileSync14 } from "fs";
|
|
12528
12886
|
var GLOBAL_RES = [
|
|
12529
12887
|
/\b(?:globalThis|global|window|self)\s*(?:\.\s*[\w$]+|\[[^\]]+\])\s*=(?!=)/,
|
|
12530
12888
|
/Object\.(?:assign|defineProperty|defineProperties|setPrototypeOf)\(\s*(?:globalThis|global|window|self)\b/,
|
|
@@ -12555,11 +12913,8 @@ var e5ExtensionIsolation = {
|
|
|
12555
12913
|
const manifests = findExtensionManifests(tree);
|
|
12556
12914
|
const own = /* @__PURE__ */ new Set();
|
|
12557
12915
|
for (const m of manifests) {
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
if (typeof j.identifier === "string") own.add(j.identifier);
|
|
12561
|
-
} catch {
|
|
12562
|
-
}
|
|
12916
|
+
const j = readJsonObject(m);
|
|
12917
|
+
if (j && typeof j.identifier === "string") own.add(j.identifier);
|
|
12563
12918
|
}
|
|
12564
12919
|
const defined = tree.files.some(
|
|
12565
12920
|
(f) => isProductSource(f.rel) && uncommentedLines(f).some((r) => /\bdefineExtension\s*\(/.test(r))
|
|
@@ -12989,8 +13344,8 @@ var e10MrtrOnly = {
|
|
|
12989
13344
|
};
|
|
12990
13345
|
|
|
12991
13346
|
// src/detectors/e11-surfaces-derived.ts
|
|
12992
|
-
import { existsSync as existsSync20, statSync as
|
|
12993
|
-
import { dirname as dirname14, join as join24, relative as
|
|
13347
|
+
import { existsSync as existsSync20, statSync as statSync18 } from "fs";
|
|
13348
|
+
import { dirname as dirname14, join as join24, relative as relative12, sep as sep6 } from "path";
|
|
12994
13349
|
var ALL = ["chat", "cowork", "claude-code"];
|
|
12995
13350
|
function excludedSurfaces(c) {
|
|
12996
13351
|
const cause = /* @__PURE__ */ new Map();
|
|
@@ -13006,10 +13361,17 @@ function excludedSurfaces(c) {
|
|
|
13006
13361
|
if (c.localMcp) exclude("chat", "a local MCP command excludes chat");
|
|
13007
13362
|
return cause;
|
|
13008
13363
|
}
|
|
13009
|
-
function isDir(p) {
|
|
13364
|
+
function isDir(p, anchor, unreadable) {
|
|
13010
13365
|
try {
|
|
13011
|
-
return
|
|
13012
|
-
} catch {
|
|
13366
|
+
return statSync18(p).isDirectory();
|
|
13367
|
+
} catch (err) {
|
|
13368
|
+
const code = fsErrorCode(err);
|
|
13369
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
13370
|
+
unreadable.push({
|
|
13371
|
+
anchor,
|
|
13372
|
+
reason: `${anchor} could not be probed (${code}) \u2014 whether this component exists is unread`
|
|
13373
|
+
});
|
|
13374
|
+
}
|
|
13013
13375
|
return false;
|
|
13014
13376
|
}
|
|
13015
13377
|
}
|
|
@@ -13027,15 +13389,22 @@ var e11SurfacesDerived = {
|
|
|
13027
13389
|
);
|
|
13028
13390
|
}
|
|
13029
13391
|
const manifests = findDiskFiles(tree, (n) => n === "extension.json");
|
|
13392
|
+
const unreadable = refusedAsUnreadable(
|
|
13393
|
+
manifests.refused,
|
|
13394
|
+
"its surfaces are unread"
|
|
13395
|
+
);
|
|
13030
13396
|
if (manifests.length === 0) {
|
|
13031
|
-
return
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13397
|
+
return settle(
|
|
13398
|
+
this.id,
|
|
13399
|
+
{
|
|
13400
|
+
id: this.id,
|
|
13401
|
+
anchor: "-",
|
|
13402
|
+
state: "absent",
|
|
13403
|
+
verdict: "no extension.json in the tree: the object is not an extension; E-11 n/a"
|
|
13404
|
+
},
|
|
13405
|
+
unreadable
|
|
13406
|
+
);
|
|
13037
13407
|
}
|
|
13038
|
-
const unreadable = [];
|
|
13039
13408
|
const reds = [];
|
|
13040
13409
|
let judged = 0;
|
|
13041
13410
|
for (const m of manifests) {
|
|
@@ -13048,7 +13417,7 @@ var e11SurfacesDerived = {
|
|
|
13048
13417
|
continue;
|
|
13049
13418
|
}
|
|
13050
13419
|
const folder = dirname14(m.path);
|
|
13051
|
-
const rel = (p) =>
|
|
13420
|
+
const rel = (p) => relative12(tree.root, p).split(sep6).join("/") || ".";
|
|
13052
13421
|
if (!Array.isArray(j.surfaces)) {
|
|
13053
13422
|
reds.push({
|
|
13054
13423
|
anchor: m.rel,
|
|
@@ -13084,9 +13453,9 @@ var e11SurfacesDerived = {
|
|
|
13084
13453
|
}
|
|
13085
13454
|
}
|
|
13086
13455
|
const cause = excludedSurfaces({
|
|
13087
|
-
bin: isDir(join24(folder, "bin")),
|
|
13088
|
-
hooks: isDir(join24(folder, "hooks")),
|
|
13089
|
-
agents: isDir(join24(folder, "agents")),
|
|
13456
|
+
bin: isDir(join24(folder, "bin"), rel(join24(folder, "bin")), unreadable),
|
|
13457
|
+
hooks: isDir(join24(folder, "hooks"), rel(join24(folder, "hooks")), unreadable),
|
|
13458
|
+
agents: isDir(join24(folder, "agents"), rel(join24(folder, "agents")), unreadable),
|
|
13090
13459
|
localMcp
|
|
13091
13460
|
});
|
|
13092
13461
|
for (const s of declared) {
|
|
@@ -13123,9 +13492,10 @@ var e11SurfacesDerived = {
|
|
|
13123
13492
|
|
|
13124
13493
|
// src/detectors/e12-host-capability-placement.ts
|
|
13125
13494
|
var TARGET2 = "2026-07-28";
|
|
13495
|
+
var isManifest2 = (n) => n === "mcp.json" || n === "mcp-server.json";
|
|
13126
13496
|
function targets2026(tree) {
|
|
13127
13497
|
if (codeSites(tree, /["'`]2026-07-28["'`]/g).length > 0) return true;
|
|
13128
|
-
for (const f of findDiskFiles(tree,
|
|
13498
|
+
for (const f of findDiskFiles(tree, isManifest2)) {
|
|
13129
13499
|
const j = readJsonRecord(f.path);
|
|
13130
13500
|
if (!j || isRecord(j.mcpServers)) continue;
|
|
13131
13501
|
if (typeof j.minProtocolVersion === "string" && j.minProtocolVersion >= TARGET2) return true;
|
|
@@ -13182,10 +13552,17 @@ var e12HostCapabilityPlacement = {
|
|
|
13182
13552
|
);
|
|
13183
13553
|
}
|
|
13184
13554
|
if (targetUnknown) {
|
|
13185
|
-
return
|
|
13555
|
+
return settle(
|
|
13186
13556
|
this.id,
|
|
13187
|
-
|
|
13188
|
-
|
|
13557
|
+
couldNotJudge(
|
|
13558
|
+
this.id,
|
|
13559
|
+
r.caps[0].anchor,
|
|
13560
|
+
`${experimentalOnly.join(", ")} is announced only under capabilities.experimental, and nothing in this tree states that the server targets ${TARGET2} (no "${TARGET2}" literal, no manifest minProtocolVersion) \u2014 the placement rule applies only to such a server (E-12)`
|
|
13561
|
+
),
|
|
13562
|
+
refusedAsUnreadable(
|
|
13563
|
+
findDiskFiles(tree, isManifest2).refused,
|
|
13564
|
+
"its minProtocolVersion (the server's target) is unread"
|
|
13565
|
+
)
|
|
13189
13566
|
);
|
|
13190
13567
|
}
|
|
13191
13568
|
return settle(
|
|
@@ -14339,6 +14716,10 @@ var s16LocalFootprint = {
|
|
|
14339
14716
|
}
|
|
14340
14717
|
const manifests = diskText(tree, (name) => MANIFESTS.has(name));
|
|
14341
14718
|
const documented = manifests.some((m) => documentsFootprint(m.text));
|
|
14719
|
+
const unreadManifests = refusedAsUnreadable(
|
|
14720
|
+
manifests.refused,
|
|
14721
|
+
"it may document the file footprint, so it is unread"
|
|
14722
|
+
);
|
|
14342
14723
|
const sensitive = productHits(tree, SENSITIVE_RE);
|
|
14343
14724
|
const listens = productHits(tree, LISTEN_RE);
|
|
14344
14725
|
const transports = productHits(tree, TRANSPORT_RE);
|
|
@@ -14356,7 +14737,10 @@ var s16LocalFootprint = {
|
|
|
14356
14737
|
);
|
|
14357
14738
|
}
|
|
14358
14739
|
const reds = [];
|
|
14359
|
-
|
|
14740
|
+
let footprintUnjudged = false;
|
|
14741
|
+
if (!documented && unreadManifests.length > 0) {
|
|
14742
|
+
footprintUnjudged = sensitive.length > 0 || homeReaders.length > 0;
|
|
14743
|
+
} else if (!documented) {
|
|
14360
14744
|
for (const h of sensitive) {
|
|
14361
14745
|
reds.push(
|
|
14362
14746
|
`${h.anchor} names a sensitive path outside any declared object (${h.text.trim().slice(0, 60)}): a local server reads nothing outside its object unless the tool requires it and the manifest documents it`
|
|
@@ -14385,6 +14769,13 @@ var s16LocalFootprint = {
|
|
|
14385
14769
|
`${reds.join("; ")} (S-16, positive-control OK)`
|
|
14386
14770
|
);
|
|
14387
14771
|
}
|
|
14772
|
+
if (footprintUnjudged) {
|
|
14773
|
+
return couldNotJudge(
|
|
14774
|
+
this.id,
|
|
14775
|
+
unreadManifests[0].anchor,
|
|
14776
|
+
`${unreadManifests.map((u) => u.reason).join("; ")}; ${sensitive.length} sensitive path(s) and ${homeReaders.length} home-directory read(s) are in the tree, and whether the manifest documents their footprint is unread (S-16)`
|
|
14777
|
+
);
|
|
14778
|
+
}
|
|
14388
14779
|
const stdio = transports.some((t) => /StdioServerTransport/.test(t.text));
|
|
14389
14780
|
const http = transports.some((t) => !/StdioServerTransport/.test(t.text)) || listens.length > 0;
|
|
14390
14781
|
const socketOnly = listens.length > 0 && listens.every((h) => SOCKET_RE.test(h.text.match(LISTEN_RE)?.[1] ?? ""));
|
|
@@ -15432,8 +15823,8 @@ var s27ElicitationFloor = {
|
|
|
15432
15823
|
};
|
|
15433
15824
|
|
|
15434
15825
|
// src/detectors/s28-claim-forwarded.ts
|
|
15435
|
-
import { existsSync as existsSync21, readFileSync as
|
|
15436
|
-
import { dirname as dirname15, join as join25, relative as
|
|
15826
|
+
import { existsSync as existsSync21, readFileSync as readFileSync14, readdirSync as readdirSync12, statSync as statSync19 } from "fs";
|
|
15827
|
+
import { dirname as dirname15, join as join25, relative as relative13 } from "path";
|
|
15437
15828
|
var PIN11 = STANDARD_PIN;
|
|
15438
15829
|
var CLAIM_TOKEN = /\bverified(?:Org|Person)\b/;
|
|
15439
15830
|
var ANY_CLAIM = /\bverified(?:Actor|Org|Person)\b/g;
|
|
@@ -15491,11 +15882,12 @@ function splitTop(m, from, to) {
|
|
|
15491
15882
|
if (start < to && m.slice(start, to).trim() !== "") parts.push([start, to]);
|
|
15492
15883
|
return parts;
|
|
15493
15884
|
}
|
|
15494
|
-
function listTs(dir, out = []) {
|
|
15885
|
+
function listTs(dir, unread, out = []) {
|
|
15495
15886
|
let names;
|
|
15496
15887
|
try {
|
|
15497
15888
|
names = readdirSync12(dir);
|
|
15498
|
-
} catch {
|
|
15889
|
+
} catch (err) {
|
|
15890
|
+
unread.push(`${dir} could not be listed (${fsErrorCode(err)})`);
|
|
15499
15891
|
return out;
|
|
15500
15892
|
}
|
|
15501
15893
|
for (const n of names) {
|
|
@@ -15503,19 +15895,23 @@ function listTs(dir, out = []) {
|
|
|
15503
15895
|
const p = join25(dir, n);
|
|
15504
15896
|
let st;
|
|
15505
15897
|
try {
|
|
15506
|
-
st =
|
|
15507
|
-
} catch {
|
|
15898
|
+
st = statSync19(p);
|
|
15899
|
+
} catch (err) {
|
|
15900
|
+
unread.push(`${p} could not be read (${fsErrorCode(err)})`);
|
|
15508
15901
|
continue;
|
|
15509
15902
|
}
|
|
15510
|
-
if (st.isDirectory()) listTs(p, out);
|
|
15511
|
-
else if (n.endsWith(".ts") && !n.endsWith(".test.ts") && !n.endsWith(".d.ts"))
|
|
15903
|
+
if (st.isDirectory()) listTs(p, unread, out);
|
|
15904
|
+
else if (n.endsWith(".ts") && !n.endsWith(".test.ts") && !n.endsWith(".d.ts")) {
|
|
15905
|
+
if (!st.isFile()) unread.push(`${p} is not a regular file \u2014 not read`);
|
|
15906
|
+
else out.push(p);
|
|
15907
|
+
}
|
|
15512
15908
|
}
|
|
15513
15909
|
return out;
|
|
15514
15910
|
}
|
|
15515
15911
|
function discoverBackend(root) {
|
|
15516
15912
|
let dir = root;
|
|
15517
15913
|
try {
|
|
15518
|
-
if (!
|
|
15914
|
+
if (!statSync19(root).isDirectory()) dir = dirname15(root);
|
|
15519
15915
|
} catch {
|
|
15520
15916
|
return void 0;
|
|
15521
15917
|
}
|
|
@@ -15528,22 +15924,23 @@ function discoverBackend(root) {
|
|
|
15528
15924
|
}
|
|
15529
15925
|
return void 0;
|
|
15530
15926
|
}
|
|
15531
|
-
function readDoors(backendRoot) {
|
|
15927
|
+
function readDoors(backendRoot, unread) {
|
|
15532
15928
|
const doors = /* @__PURE__ */ new Map();
|
|
15533
|
-
for (const path of listTs(backendRoot)) {
|
|
15929
|
+
for (const path of listTs(backendRoot, unread)) {
|
|
15534
15930
|
let text;
|
|
15535
15931
|
try {
|
|
15536
|
-
text =
|
|
15537
|
-
} catch {
|
|
15932
|
+
text = readFileSync14(path, "utf8");
|
|
15933
|
+
} catch (err) {
|
|
15934
|
+
unread.push(`${path} could not be read (${fsErrorCode(err)})`);
|
|
15538
15935
|
continue;
|
|
15539
15936
|
}
|
|
15540
15937
|
const file = {
|
|
15541
15938
|
path,
|
|
15542
|
-
rel:
|
|
15939
|
+
rel: relative13(backendRoot, path),
|
|
15543
15940
|
lines: text.split("\n").map((raw, i) => ({ n: i + 1, raw, comment: false }))
|
|
15544
15941
|
};
|
|
15545
15942
|
const m = masked(file);
|
|
15546
|
-
const module =
|
|
15943
|
+
const module = relative13(backendRoot, path).replace(/\.ts$/, "").split("\\").join("/");
|
|
15547
15944
|
const re = /\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:query|mutation|action)\s*\(/g;
|
|
15548
15945
|
for (let hit = re.exec(m); hit; hit = re.exec(m)) {
|
|
15549
15946
|
const open = hit.index + hit[0].length - 1;
|
|
@@ -15656,7 +16053,9 @@ function scanS28(tree) {
|
|
|
15656
16053
|
const scan = { violate: [], conform: [], cnj: [], calls: 0, backend: void 0 };
|
|
15657
16054
|
const backendRoot = tree.backendRoot ?? discoverBackend(tree.root);
|
|
15658
16055
|
scan.backend = backendRoot;
|
|
15659
|
-
const
|
|
16056
|
+
const unread = [];
|
|
16057
|
+
const doors = backendRoot ? readDoors(backendRoot, unread) : void 0;
|
|
16058
|
+
const unreadNote = unread.length > 0 ? `; backend source NOT read: ${unread.join("; ")}` : "";
|
|
15660
16059
|
const files = tree.files.map((f) => ({ f, text: rawText(f), m: masked(f) }));
|
|
15661
16060
|
const actorDoors = /* @__PURE__ */ new Set();
|
|
15662
16061
|
for (const { text } of files) {
|
|
@@ -15776,7 +16175,7 @@ function scanS28(tree) {
|
|
|
15776
16175
|
if (!info) {
|
|
15777
16176
|
scan.cnj.push(
|
|
15778
16177
|
site(
|
|
15779
|
-
`forwards no verified claim and the door ${door} is not defined in the supplied backend (${backendRoot})`
|
|
16178
|
+
`forwards no verified claim and the door ${door} is not defined in the supplied backend (${backendRoot})${unreadNote}`
|
|
15780
16179
|
)
|
|
15781
16180
|
);
|
|
15782
16181
|
continue;
|
|
@@ -16155,6 +16554,17 @@ var s34AccessRecheck = {
|
|
|
16155
16554
|
);
|
|
16156
16555
|
}
|
|
16157
16556
|
}
|
|
16557
|
+
if (manifests.refused.length > 0) {
|
|
16558
|
+
const named = refusedAsUnreadable(
|
|
16559
|
+
manifests.refused,
|
|
16560
|
+
"its events.accessRecheckSeconds is unread"
|
|
16561
|
+
);
|
|
16562
|
+
return couldNotJudge(
|
|
16563
|
+
this.id,
|
|
16564
|
+
named[0].anchor,
|
|
16565
|
+
`${named.map((u) => u.reason).join("; ")}${seen.length > 0 ? ` (the readable manifests say: ${seen.join("; ")})` : ""}; the recheck window cannot be judged absent or non-positive over a manifest that was not read (S-34)`
|
|
16566
|
+
);
|
|
16567
|
+
}
|
|
16158
16568
|
const where = manifests[0]?.rel ?? surface.declared.anchor;
|
|
16159
16569
|
return {
|
|
16160
16570
|
id: this.id,
|
|
@@ -16623,12 +17033,12 @@ var detectors = registered.map(
|
|
|
16623
17033
|
|
|
16624
17034
|
// src/runtime/launch.ts
|
|
16625
17035
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
16626
|
-
import { existsSync as existsSync22, readFileSync as
|
|
17036
|
+
import { existsSync as existsSync22, readFileSync as readFileSync15, statSync as statSync20 } from "fs";
|
|
16627
17037
|
import { dirname as dirname16, isAbsolute, join as join26, resolve as resolve2 } from "path";
|
|
16628
17038
|
function findPackageDir(target) {
|
|
16629
17039
|
let dir;
|
|
16630
17040
|
try {
|
|
16631
|
-
dir =
|
|
17041
|
+
dir = statSync20(target).isDirectory() ? target : dirname16(target);
|
|
16632
17042
|
} catch {
|
|
16633
17043
|
return null;
|
|
16634
17044
|
}
|
|
@@ -16658,7 +17068,10 @@ function deriveLaunchPlan(target) {
|
|
|
16658
17068
|
}
|
|
16659
17069
|
let pkg;
|
|
16660
17070
|
try {
|
|
16661
|
-
|
|
17071
|
+
if (!statSync20(join26(pkgDir, "package.json")).isFile()) {
|
|
17072
|
+
return { error: `${join26(pkgDir, "package.json")} is not a regular file \u2014 not read` };
|
|
17073
|
+
}
|
|
17074
|
+
pkg = JSON.parse(readFileSync15(join26(pkgDir, "package.json"), "utf8"));
|
|
16662
17075
|
} catch (err) {
|
|
16663
17076
|
return {
|
|
16664
17077
|
error: `could not parse ${join26(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -16738,6 +17151,7 @@ child.stdout.on("data", (d) => {
|
|
|
16738
17151
|
while ((i = buf.indexOf("\n")) >= 0) {
|
|
16739
17152
|
const line = buf.slice(0, i); buf = buf.slice(i + 1);
|
|
16740
17153
|
if (!line.trim()) continue;
|
|
17154
|
+
// (a) Not a filesystem read: an in-memory parse of one stdout line. A non-JSON line is legitimately not a JSON-RPC answer (a server may log to stdout); the answers it never gave surface as no-answer outcomes / the 5 s probe deadline, never as a silent pass.
|
|
16741
17155
|
let msg; try { msg = JSON.parse(line); } catch { continue; }
|
|
16742
17156
|
// A request or notification FROM the server carries a method: it is not an answer.
|
|
16743
17157
|
if (msg.method !== undefined) continue;
|
|
@@ -16796,29 +17210,41 @@ send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "
|
|
|
16796
17210
|
function readEvalCounts(target) {
|
|
16797
17211
|
const counts = /* @__PURE__ */ new Map();
|
|
16798
17212
|
const pkgDir = findPackageDir(target) ?? (statSyncSafe(target) ? target : null);
|
|
16799
|
-
if (!pkgDir) return counts;
|
|
17213
|
+
if (!pkgDir) return { counts };
|
|
16800
17214
|
let dir = pkgDir;
|
|
16801
17215
|
for (let depth = 0; depth < 8; depth++) {
|
|
16802
17216
|
const candidate = join26(dir, "evals", "evals.json");
|
|
16803
17217
|
if (existsSync22(candidate)) {
|
|
17218
|
+
let text;
|
|
17219
|
+
try {
|
|
17220
|
+
if (!statSync20(candidate).isFile()) {
|
|
17221
|
+
return { counts, unreadable: `${candidate} is not a regular file \u2014 not read` };
|
|
17222
|
+
}
|
|
17223
|
+
text = readFileSync15(candidate, "utf8");
|
|
17224
|
+
} catch (err) {
|
|
17225
|
+
return {
|
|
17226
|
+
counts,
|
|
17227
|
+
unreadable: `${candidate} could not be read (${fsErrorCode(err)})`
|
|
17228
|
+
};
|
|
17229
|
+
}
|
|
16804
17230
|
try {
|
|
16805
|
-
const parsed = JSON.parse(
|
|
17231
|
+
const parsed = JSON.parse(text);
|
|
16806
17232
|
for (const c of parsed.cases ?? []) {
|
|
16807
17233
|
if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
|
|
16808
17234
|
}
|
|
16809
17235
|
} catch {
|
|
16810
17236
|
}
|
|
16811
|
-
return counts;
|
|
17237
|
+
return { counts };
|
|
16812
17238
|
}
|
|
16813
17239
|
const parent = dirname16(dir);
|
|
16814
17240
|
if (parent === dir) break;
|
|
16815
17241
|
dir = parent;
|
|
16816
17242
|
}
|
|
16817
|
-
return counts;
|
|
17243
|
+
return { counts };
|
|
16818
17244
|
}
|
|
16819
17245
|
function statSyncSafe(p) {
|
|
16820
17246
|
try {
|
|
16821
|
-
return
|
|
17247
|
+
return statSync20(p).isDirectory();
|
|
16822
17248
|
} catch {
|
|
16823
17249
|
return false;
|
|
16824
17250
|
}
|
|
@@ -16926,7 +17352,7 @@ function launchAndIntrospect(target) {
|
|
|
16926
17352
|
if (parsed.launchError) {
|
|
16927
17353
|
return { root: target, tools: [], launchError: parsed.launchError };
|
|
16928
17354
|
}
|
|
16929
|
-
const evalCounts = readEvalCounts(target);
|
|
17355
|
+
const { counts: evalCounts, unreadable: evalsUnreadable } = readEvalCounts(target);
|
|
16930
17356
|
const tools = (parsed.tools ?? []).map((t) => {
|
|
16931
17357
|
const name = t.name;
|
|
16932
17358
|
const count = evalCounts.get(name);
|
|
@@ -16940,6 +17366,7 @@ function launchAndIntrospect(target) {
|
|
|
16940
17366
|
tools,
|
|
16941
17367
|
...parsed.serverInfo ? { serverInfo: parsed.serverInfo } : {},
|
|
16942
17368
|
...expectedCommit ? { expectedCommit } : {},
|
|
17369
|
+
...evalsUnreadable ? { evalsUnreadable } : {},
|
|
16943
17370
|
...Array.isArray(parsed.cursorProbes) ? { cursorProbes: parsed.cursorProbes.map(readCursorProbe) } : {}
|
|
16944
17371
|
};
|
|
16945
17372
|
}
|
|
@@ -17198,7 +17625,7 @@ var USAGE = [
|
|
|
17198
17625
|
function readVersion() {
|
|
17199
17626
|
try {
|
|
17200
17627
|
const here = dirname17(fileURLToPath2(import.meta.url));
|
|
17201
|
-
const manifest = JSON.parse(
|
|
17628
|
+
const manifest = JSON.parse(readFileSync16(join27(here, "..", "package.json"), "utf8"));
|
|
17202
17629
|
return manifest.version ?? null;
|
|
17203
17630
|
} catch {
|
|
17204
17631
|
return null;
|