@vantageos/mcp-doctor 0.8.1 → 0.8.2
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 +18 -0
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.js +410 -166
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes to `@vantageos/mcp-doctor` are documented here.
|
|
4
4
|
This project adheres to [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## 0.8.2 — 2026-10-25
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **A detector's own read that fails, or could block, is named instead of being judged empty** (task
|
|
10
|
+
k17dddv66dbzd1fsmz9bwg4ysx8gy6zv). 0.8.1 closed this for the target walk. Each detector's own reads
|
|
11
|
+
still had the same holes:
|
|
12
|
+
- a read failure answered by a bare `return` or `continue`;
|
|
13
|
+
- a `readFileSync` that would block forever on a FIFO.
|
|
14
|
+
|
|
15
|
+
Every site is now classified in `analysis/detector-read-class.md`: 87 rows, of which 43 are declared
|
|
16
|
+
legitimate (with a comment at the site) and 44 are converted. A converted site reports
|
|
17
|
+
`could-not-judge` naming what it did not read, or records a skip. A non-regular file is never read.
|
|
18
|
+
C-26 no longer aborts the run on an unreadable `package.json` or lockfile. D8 (dynamic) no longer
|
|
19
|
+
counts an unreadable `evals.json` as zero coverage.
|
|
20
|
+
|
|
21
|
+
On readable input every verdict is unchanged: four real trees give the same 101 results, the same
|
|
22
|
+
states and the same exit code on 0.8.1 and 0.8.2.
|
|
23
|
+
|
|
6
24
|
## 0.8.1 — 2026-10-13
|
|
7
25
|
|
|
8
26
|
Standard re-pinned to ElPi-Corp `6d463a1ecc5256cd59dd1afe5aa5e335accacc90`: the direct child of
|
package/dist/doctor.d.ts
CHANGED
|
@@ -201,6 +201,12 @@ interface RuntimeContext {
|
|
|
201
201
|
root: string;
|
|
202
202
|
tools: RuntimeTool[];
|
|
203
203
|
launchError?: string;
|
|
204
|
+
/**
|
|
205
|
+
* Set when the target's `evals/evals.json` is present but was NOT READ (permission, or not a
|
|
206
|
+
* regular file). Every `evalCaseCount` is then unset because the file was unread, not because it
|
|
207
|
+
* covers nothing: D8 reads it as `could-not-judge`, never as zero coverage.
|
|
208
|
+
*/
|
|
209
|
+
evalsUnreadable?: string;
|
|
204
210
|
/**
|
|
205
211
|
* The `serverInfo` object the running server returned in its `initialize`
|
|
206
212
|
* response — the same non-authenticated discovery handshake a client reads
|
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,
|
|
@@ -536,6 +536,7 @@ function settle(id, decided, unreadable) {
|
|
|
536
536
|
function readJsonObject(path) {
|
|
537
537
|
let value;
|
|
538
538
|
try {
|
|
539
|
+
if (!statSync2(path).isFile()) return null;
|
|
539
540
|
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
540
541
|
} catch {
|
|
541
542
|
return null;
|
|
@@ -563,7 +564,7 @@ function guardUnreadablePackageJson(inner, opts) {
|
|
|
563
564
|
}
|
|
564
565
|
|
|
565
566
|
// src/detectors/siteScan.ts
|
|
566
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as
|
|
567
|
+
import { constants as constants2, accessSync as accessSync2, existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
567
568
|
import { join as join2, relative as relative2, sep as sep2 } from "path";
|
|
568
569
|
function codeSites(tree, re) {
|
|
569
570
|
const out = [];
|
|
@@ -630,7 +631,7 @@ function isRecord(v) {
|
|
|
630
631
|
}
|
|
631
632
|
function baseOf(tree) {
|
|
632
633
|
try {
|
|
633
|
-
return
|
|
634
|
+
return statSync3(tree.root).isDirectory() ? tree.root : join2(tree.root, "..");
|
|
634
635
|
} catch {
|
|
635
636
|
return tree.root;
|
|
636
637
|
}
|
|
@@ -642,15 +643,17 @@ function findDiskFiles(tree, pred) {
|
|
|
642
643
|
let entries;
|
|
643
644
|
try {
|
|
644
645
|
entries = readdirSync2(dir);
|
|
645
|
-
} catch {
|
|
646
|
+
} catch (err) {
|
|
647
|
+
recordSkip(tree, dir, base, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
646
648
|
return;
|
|
647
649
|
}
|
|
648
650
|
for (const name of entries.sort()) {
|
|
649
651
|
const full = join2(dir, name);
|
|
650
652
|
let st;
|
|
651
653
|
try {
|
|
652
|
-
st =
|
|
653
|
-
} catch {
|
|
654
|
+
st = statSync3(full);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
recordSkip(tree, full, base, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
654
657
|
continue;
|
|
655
658
|
}
|
|
656
659
|
if (st.isDirectory()) {
|
|
@@ -661,7 +664,18 @@ function findDiskFiles(tree, pred) {
|
|
|
661
664
|
walk2(full);
|
|
662
665
|
} else {
|
|
663
666
|
const rel = relative2(base, full).split(sep2).join("/");
|
|
664
|
-
if (pred(name, rel))
|
|
667
|
+
if (!pred(name, rel)) continue;
|
|
668
|
+
if (!st.isFile()) {
|
|
669
|
+
recordSkip(tree, full, base, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
accessSync2(full, constants2.R_OK);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
recordSkip(tree, full, base, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
out.push({ path: full, rel, name });
|
|
665
679
|
}
|
|
666
680
|
}
|
|
667
681
|
};
|
|
@@ -670,6 +684,7 @@ function findDiskFiles(tree, pred) {
|
|
|
670
684
|
}
|
|
671
685
|
function readText(path) {
|
|
672
686
|
try {
|
|
687
|
+
if (!statSync3(path).isFile()) return null;
|
|
673
688
|
return readFileSync3(path, "utf8");
|
|
674
689
|
} catch {
|
|
675
690
|
return null;
|
|
@@ -1738,7 +1753,7 @@ var a2DeclaredDomains = {
|
|
|
1738
1753
|
};
|
|
1739
1754
|
|
|
1740
1755
|
// src/detectors/v080Kit.ts
|
|
1741
|
-
import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as
|
|
1756
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
1742
1757
|
import { dirname as dirname2, join as join3 } from "path";
|
|
1743
1758
|
|
|
1744
1759
|
// src/detectors/toolContext.ts
|
|
@@ -2629,16 +2644,29 @@ function governingWorkflows(tree) {
|
|
|
2629
2644
|
const wf = join3(dir, ".github", "workflows");
|
|
2630
2645
|
if (existsSync3(wf)) {
|
|
2631
2646
|
const out = [];
|
|
2632
|
-
|
|
2647
|
+
const base = baseOf(tree);
|
|
2648
|
+
let names;
|
|
2649
|
+
try {
|
|
2650
|
+
names = readdirSync3(wf).sort();
|
|
2651
|
+
} catch (err) {
|
|
2652
|
+
recordSkip(tree, wf, base, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
2653
|
+
return out;
|
|
2654
|
+
}
|
|
2655
|
+
for (const name of names) {
|
|
2633
2656
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
2634
2657
|
const full = join3(wf, name);
|
|
2635
2658
|
try {
|
|
2636
|
-
if (!
|
|
2637
|
-
|
|
2659
|
+
if (!statSync4(full).isFile()) {
|
|
2660
|
+
recordSkip(tree, full, base, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
recordSkip(tree, full, base, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
2638
2665
|
continue;
|
|
2639
2666
|
}
|
|
2640
2667
|
const text = readText(full);
|
|
2641
2668
|
if (text !== null) out.push({ rel: `.github/workflows/${name}`, text });
|
|
2669
|
+
else recordSkip(tree, full, base, "could not be read \u2014 not read");
|
|
2642
2670
|
}
|
|
2643
2671
|
return out;
|
|
2644
2672
|
}
|
|
@@ -2947,7 +2975,7 @@ var a4WcagGate = {
|
|
|
2947
2975
|
};
|
|
2948
2976
|
|
|
2949
2977
|
// src/detectors/a5-performance-budgets.ts
|
|
2950
|
-
import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as
|
|
2978
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
2951
2979
|
import { join as join4, relative as relative3, sep as sep3 } from "path";
|
|
2952
2980
|
import { gzipSync } from "zlib";
|
|
2953
2981
|
var BUDGET_BYTES = 250 * 1024;
|
|
@@ -2983,7 +3011,7 @@ function collect(base) {
|
|
|
2983
3011
|
const candidate = BUNDLE_EXT_RE.test(name) && UI_PATH_RE.test(rel);
|
|
2984
3012
|
let st;
|
|
2985
3013
|
try {
|
|
2986
|
-
st =
|
|
3014
|
+
st = statSync5(full);
|
|
2987
3015
|
} catch {
|
|
2988
3016
|
if (candidate) unmeasured.push(`${rel} (cannot be read: dangling link or permission)`);
|
|
2989
3017
|
continue;
|
|
@@ -2991,6 +3019,10 @@ function collect(base) {
|
|
|
2991
3019
|
if (st.isDirectory()) {
|
|
2992
3020
|
if (!WALK_SKIP.has(name)) walk2(full, depth + 1);
|
|
2993
3021
|
} else if (candidate) {
|
|
3022
|
+
if (!st.isFile()) {
|
|
3023
|
+
unmeasured.push(`${rel} (not a regular file, not read)`);
|
|
3024
|
+
continue;
|
|
3025
|
+
}
|
|
2994
3026
|
if (st.size > MAX_MEASURE_BYTES) {
|
|
2995
3027
|
unmeasured.push(`${rel} (${(st.size / 1048576).toFixed(0)} MB, too large to measure)`);
|
|
2996
3028
|
continue;
|
|
@@ -4244,7 +4276,7 @@ var a12FileWrite = {
|
|
|
4244
4276
|
};
|
|
4245
4277
|
|
|
4246
4278
|
// src/detectors/c7-artifact-verifiable.ts
|
|
4247
|
-
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
4279
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync6 } from "fs";
|
|
4248
4280
|
import { join as join5 } from "path";
|
|
4249
4281
|
var PIN = STANDARD_PIN;
|
|
4250
4282
|
var DOC_FILES = ["README.md", "README.fr.md"];
|
|
@@ -4282,13 +4314,18 @@ function c7PositiveControls() {
|
|
|
4282
4314
|
}
|
|
4283
4315
|
};
|
|
4284
4316
|
}
|
|
4285
|
-
function readDocText(root) {
|
|
4317
|
+
function readDocText(root, unreadable) {
|
|
4286
4318
|
for (const name of DOC_FILES) {
|
|
4287
4319
|
const path = join5(root, name);
|
|
4288
4320
|
if (!existsSync4(path)) continue;
|
|
4289
4321
|
try {
|
|
4322
|
+
if (!statSync6(path).isFile()) {
|
|
4323
|
+
unreadable.push({ anchor: name, reason: `${name} is not a regular file \u2014 not read` });
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4290
4326
|
return { text: readFileSync5(path, "utf8"), file: name };
|
|
4291
|
-
} catch {
|
|
4327
|
+
} catch (err) {
|
|
4328
|
+
unreadable.push({ anchor: name, reason: `${name} could not be read (${fsErrorCode(err)})` });
|
|
4292
4329
|
}
|
|
4293
4330
|
}
|
|
4294
4331
|
return null;
|
|
@@ -4319,7 +4356,15 @@ var c7ArtifactVerifiable = {
|
|
|
4319
4356
|
`C7 channel-matcher control FAILED on \`${controls.channelMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
|
|
4320
4357
|
);
|
|
4321
4358
|
}
|
|
4322
|
-
const
|
|
4359
|
+
const unreadable = [];
|
|
4360
|
+
const doc = readDocText(tree.root, unreadable);
|
|
4361
|
+
if (unreadable.length > 0) {
|
|
4362
|
+
return couldNotJudge(
|
|
4363
|
+
this.id,
|
|
4364
|
+
unreadable[0].anchor,
|
|
4365
|
+
`${unreadable.map((u) => u.reason).join("; ")} \u2014 the install, pin, channel and provenance propositions are unjudged (C-7)`
|
|
4366
|
+
);
|
|
4367
|
+
}
|
|
4323
4368
|
if (!doc) {
|
|
4324
4369
|
return {
|
|
4325
4370
|
id: this.id,
|
|
@@ -5582,7 +5627,7 @@ var c16UntrustedOutput = {
|
|
|
5582
5627
|
};
|
|
5583
5628
|
|
|
5584
5629
|
// src/detectors/c17-supply-chain.ts
|
|
5585
|
-
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync5 } from "fs";
|
|
5630
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
5586
5631
|
import { dirname as dirname3, join as join6, relative as relative4, resolve } from "path";
|
|
5587
5632
|
var PIN4 = STANDARD_PIN;
|
|
5588
5633
|
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
@@ -5616,15 +5661,26 @@ function judgeProvenanceRecord(line, name, version) {
|
|
|
5616
5661
|
}
|
|
5617
5662
|
return { ok: true, why: "" };
|
|
5618
5663
|
}
|
|
5619
|
-
function findProvenanceRecord(root, name, version) {
|
|
5664
|
+
function findProvenanceRecord(root, name, version, unreadable) {
|
|
5620
5665
|
let firstBad = null;
|
|
5621
5666
|
for (const recordFile of PROVENANCE_RECORD_FILES) {
|
|
5622
5667
|
const path = join6(root, recordFile);
|
|
5623
5668
|
if (!existsSync5(path)) continue;
|
|
5624
5669
|
let text;
|
|
5625
5670
|
try {
|
|
5671
|
+
if (!statSync7(path).isFile()) {
|
|
5672
|
+
unreadable.push({
|
|
5673
|
+
anchor: recordFile,
|
|
5674
|
+
reason: `${recordFile} is not a regular file \u2014 not read`
|
|
5675
|
+
});
|
|
5676
|
+
continue;
|
|
5677
|
+
}
|
|
5626
5678
|
text = readFileSync6(path, "utf8");
|
|
5627
|
-
} catch {
|
|
5679
|
+
} catch (err) {
|
|
5680
|
+
unreadable.push({
|
|
5681
|
+
anchor: recordFile,
|
|
5682
|
+
reason: `${recordFile} could not be read (${fsErrorCode(err)})`
|
|
5683
|
+
});
|
|
5628
5684
|
continue;
|
|
5629
5685
|
}
|
|
5630
5686
|
const lines = text.split("\n");
|
|
@@ -5677,7 +5733,7 @@ function findLockfile(root) {
|
|
|
5677
5733
|
}
|
|
5678
5734
|
return null;
|
|
5679
5735
|
}
|
|
5680
|
-
function findProvenanceClaim(tree, root) {
|
|
5736
|
+
function findProvenanceClaim(tree, root, unreadable) {
|
|
5681
5737
|
const pkg = tree.packageJson;
|
|
5682
5738
|
if (pkg) {
|
|
5683
5739
|
const publishConfig = pkg.json.publishConfig;
|
|
@@ -5691,16 +5747,29 @@ function findProvenanceClaim(tree, root) {
|
|
|
5691
5747
|
let entries = [];
|
|
5692
5748
|
try {
|
|
5693
5749
|
entries = readdirSync5(workflowsDir).sort();
|
|
5694
|
-
} catch {
|
|
5750
|
+
} catch (err) {
|
|
5751
|
+
unreadable.push({
|
|
5752
|
+
anchor: relative4(root, workflowsDir) || WORKFLOW_DIR,
|
|
5753
|
+
reason: `${relative4(root, workflowsDir) || WORKFLOW_DIR} cannot be listed (${fsErrorCode(err)})`
|
|
5754
|
+
});
|
|
5695
5755
|
continue;
|
|
5696
5756
|
}
|
|
5697
5757
|
for (const name of entries) {
|
|
5698
5758
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
5699
5759
|
const path = join6(workflowsDir, name);
|
|
5700
5760
|
let text;
|
|
5761
|
+
const shown = `${relative4(root, workflowsDir) || WORKFLOW_DIR}/${name}`;
|
|
5701
5762
|
try {
|
|
5763
|
+
if (!statSync7(path).isFile()) {
|
|
5764
|
+
unreadable.push({ anchor: shown, reason: `${shown} is not a regular file \u2014 not read` });
|
|
5765
|
+
continue;
|
|
5766
|
+
}
|
|
5702
5767
|
text = readFileSync6(path, "utf8");
|
|
5703
|
-
} catch {
|
|
5768
|
+
} catch (err) {
|
|
5769
|
+
unreadable.push({
|
|
5770
|
+
anchor: shown,
|
|
5771
|
+
reason: `${shown} could not be read (${fsErrorCode(err)})`
|
|
5772
|
+
});
|
|
5704
5773
|
continue;
|
|
5705
5774
|
}
|
|
5706
5775
|
const hit = workflowStatements(text).find(([, l]) => PROVENANCE_RE2.test(l));
|
|
@@ -5749,10 +5818,11 @@ var c17SupplyChainInner = {
|
|
|
5749
5818
|
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
5819
|
};
|
|
5751
5820
|
}
|
|
5752
|
-
const
|
|
5821
|
+
const unreadable = [];
|
|
5822
|
+
const claim = findProvenanceClaim(tree, tree.root, unreadable);
|
|
5753
5823
|
const pkgVersion = typeof tree.packageJson?.json.version === "string" ? tree.packageJson.json.version : void 0;
|
|
5754
5824
|
const pkgName = typeof tree.packageJson?.json.name === "string" ? tree.packageJson.json.name : void 0;
|
|
5755
|
-
const record = claim ? null : findProvenanceRecord(tree.root, pkgName, pkgVersion);
|
|
5825
|
+
const record = claim ? null : findProvenanceRecord(tree.root, pkgName, pkgVersion, unreadable);
|
|
5756
5826
|
if (!claim && record?.ok) {
|
|
5757
5827
|
return {
|
|
5758
5828
|
id: this.id,
|
|
@@ -5769,6 +5839,13 @@ var c17SupplyChainInner = {
|
|
|
5769
5839
|
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
5840
|
};
|
|
5771
5841
|
}
|
|
5842
|
+
if (!claim && !record && unreadable.length > 0) {
|
|
5843
|
+
return couldNotJudge(
|
|
5844
|
+
this.id,
|
|
5845
|
+
unreadable[0].anchor,
|
|
5846
|
+
`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)`
|
|
5847
|
+
);
|
|
5848
|
+
}
|
|
5772
5849
|
if (!claim) {
|
|
5773
5850
|
return {
|
|
5774
5851
|
id: this.id,
|
|
@@ -5790,7 +5867,7 @@ var c17SupplyChain = guardUnreadablePackageJson(c17SupplyChainInner, {
|
|
|
5790
5867
|
});
|
|
5791
5868
|
|
|
5792
5869
|
// src/detectors/c18-structure-manifest.ts
|
|
5793
|
-
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
5870
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, statSync as statSync8 } from "fs";
|
|
5794
5871
|
import { join as join7 } from "path";
|
|
5795
5872
|
var PIN5 = STANDARD_PIN;
|
|
5796
5873
|
function manifestPath(root) {
|
|
@@ -5815,16 +5892,31 @@ function rootCandidates(root) {
|
|
|
5815
5892
|
for (const name of SERVER_MANIFEST_NAMES2) {
|
|
5816
5893
|
const path = join7(root, name);
|
|
5817
5894
|
if (!existsSync6(path)) continue;
|
|
5818
|
-
const json = readJson(path);
|
|
5819
|
-
out.push({
|
|
5895
|
+
const { json, readError } = readJson(path);
|
|
5896
|
+
out.push({
|
|
5897
|
+
name,
|
|
5898
|
+
path,
|
|
5899
|
+
role: classifyRootJson(json),
|
|
5900
|
+
json,
|
|
5901
|
+
...readError ? { readError } : {}
|
|
5902
|
+
});
|
|
5820
5903
|
}
|
|
5821
5904
|
return out;
|
|
5822
5905
|
}
|
|
5823
5906
|
function readJson(path) {
|
|
5907
|
+
let text;
|
|
5824
5908
|
try {
|
|
5825
|
-
|
|
5909
|
+
if (!statSync8(path).isFile()) {
|
|
5910
|
+
return { json: null, readError: "not a regular file \u2014 not read" };
|
|
5911
|
+
}
|
|
5912
|
+
text = readFileSync7(path, "utf8");
|
|
5913
|
+
} catch (err) {
|
|
5914
|
+
return { json: null, readError: `could not be read (${fsErrorCode(err)})` };
|
|
5915
|
+
}
|
|
5916
|
+
try {
|
|
5917
|
+
return { json: JSON.parse(text) };
|
|
5826
5918
|
} catch {
|
|
5827
|
-
return null;
|
|
5919
|
+
return { json: null };
|
|
5828
5920
|
}
|
|
5829
5921
|
}
|
|
5830
5922
|
function c18PositiveControls() {
|
|
@@ -5883,6 +5975,14 @@ var c18StructureManifestInner = {
|
|
|
5883
5975
|
let manifestName = kind === "extension" ? "extension.json" : "mcp.json";
|
|
5884
5976
|
if (kind === "server") {
|
|
5885
5977
|
const cands = rootCandidates(tree.root);
|
|
5978
|
+
const unread = cands.find((c) => c.readError !== void 0);
|
|
5979
|
+
if (unread) {
|
|
5980
|
+
return couldNotJudge(
|
|
5981
|
+
this.id,
|
|
5982
|
+
unread.path,
|
|
5983
|
+
`${unread.name} is present but ${unread.readError} \u2014 the server manifest cannot be judged (C-18)`
|
|
5984
|
+
);
|
|
5985
|
+
}
|
|
5886
5986
|
const bad = cands.find((c) => c.role === "unparseable");
|
|
5887
5987
|
if (bad) {
|
|
5888
5988
|
return {
|
|
@@ -5923,7 +6023,15 @@ var c18StructureManifestInner = {
|
|
|
5923
6023
|
verdict: "no extension.json found at the tree root -> the extension manifest is required (C-18)"
|
|
5924
6024
|
};
|
|
5925
6025
|
}
|
|
5926
|
-
|
|
6026
|
+
const read = readJson(path);
|
|
6027
|
+
if (read.readError !== void 0) {
|
|
6028
|
+
return couldNotJudge(
|
|
6029
|
+
this.id,
|
|
6030
|
+
path,
|
|
6031
|
+
`${manifestName} is present but ${read.readError} \u2014 the manifest cannot be judged (C-18)`
|
|
6032
|
+
);
|
|
6033
|
+
}
|
|
6034
|
+
manifest = read.json;
|
|
5927
6035
|
if (!manifest) {
|
|
5928
6036
|
return {
|
|
5929
6037
|
id: this.id,
|
|
@@ -6368,7 +6476,7 @@ var c21ProgressiveDisclosure = {
|
|
|
6368
6476
|
};
|
|
6369
6477
|
|
|
6370
6478
|
// src/detectors/c22-test-ratio-gate.ts
|
|
6371
|
-
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
6479
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, statSync as statSync9 } from "fs";
|
|
6372
6480
|
import { join as join8 } from "path";
|
|
6373
6481
|
var DOC_FILES2 = ["README.md", "README.fr.md", "CHANGELOG.md"];
|
|
6374
6482
|
var RATIO_RE = /\b(?:coverage|ratio de tests?|test ratio|couverture)\b[^\n%]{0,40}?(\d{1,3}(?:\.\d+)?\s?%)/i;
|
|
@@ -6391,15 +6499,20 @@ function c22PositiveControls() {
|
|
|
6391
6499
|
identifierClearsBareRatio: !IDENTIFIER_RE.test(bareSample)
|
|
6392
6500
|
};
|
|
6393
6501
|
}
|
|
6394
|
-
function findRatioCitations(root) {
|
|
6502
|
+
function findRatioCitations(root, unreadable) {
|
|
6395
6503
|
const hits = [];
|
|
6396
6504
|
for (const name of DOC_FILES2) {
|
|
6397
6505
|
const path = join8(root, name);
|
|
6398
6506
|
if (!existsSync7(path)) continue;
|
|
6399
6507
|
let text;
|
|
6400
6508
|
try {
|
|
6509
|
+
if (!statSync9(path).isFile()) {
|
|
6510
|
+
unreadable.push({ anchor: name, reason: `${name} is not a regular file \u2014 not read` });
|
|
6511
|
+
continue;
|
|
6512
|
+
}
|
|
6401
6513
|
text = readFileSync8(path, "utf8");
|
|
6402
|
-
} catch {
|
|
6514
|
+
} catch (err) {
|
|
6515
|
+
unreadable.push({ anchor: name, reason: `${name} could not be read (${fsErrorCode(err)})` });
|
|
6403
6516
|
continue;
|
|
6404
6517
|
}
|
|
6405
6518
|
const lines = text.split("\n");
|
|
@@ -6417,44 +6530,48 @@ var c22TestRatioGate = {
|
|
|
6417
6530
|
rule: "C-22",
|
|
6418
6531
|
pin: STANDARD_PIN,
|
|
6419
6532
|
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
|
-
|
|
6533
|
+
const unreadable = [];
|
|
6534
|
+
const decided = (() => {
|
|
6535
|
+
const controls = c22PositiveControls();
|
|
6536
|
+
if (!controls.ratioMatcher.matched || !controls.identifierMatcher.matched) {
|
|
6537
|
+
throw new Error(
|
|
6538
|
+
`C22 positive control FAILED: ratio=${controls.ratioMatcher.matched} identifier=${controls.identifierMatcher.matched} \u2014 matcher broken, verdict untrustworthy`
|
|
6539
|
+
);
|
|
6540
|
+
}
|
|
6541
|
+
if (!controls.identifierClearsBareRatio) {
|
|
6542
|
+
throw new Error(
|
|
6543
|
+
"C22 identifier-matcher control FAILED: must NOT flag a bare ratio with no identifier token \u2014 verdict untrustworthy"
|
|
6544
|
+
);
|
|
6545
|
+
}
|
|
6546
|
+
const hits = findRatioCitations(tree.root, unreadable);
|
|
6547
|
+
if (hits.length === 0) {
|
|
6548
|
+
return {
|
|
6549
|
+
id: this.id,
|
|
6550
|
+
anchor: "-",
|
|
6551
|
+
state: "absent",
|
|
6552
|
+
verdict: "no test/coverage ratio cited in README/CHANGELOG; test-ratio-gate check n/a"
|
|
6553
|
+
};
|
|
6554
|
+
}
|
|
6555
|
+
const bare = hits.find((h) => !h.hasIdentifier);
|
|
6556
|
+
if (bare) {
|
|
6557
|
+
return {
|
|
6558
|
+
id: this.id,
|
|
6559
|
+
anchor: `${bare.file}:${bare.line}`,
|
|
6560
|
+
state: "non-conforming",
|
|
6561
|
+
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)`
|
|
6562
|
+
};
|
|
6563
|
+
}
|
|
6564
|
+
const withId = hits[0];
|
|
6565
|
+
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
6566
|
return {
|
|
6443
6567
|
id: this.id,
|
|
6444
|
-
anchor: `${
|
|
6445
|
-
state: "
|
|
6446
|
-
|
|
6568
|
+
anchor: `${withId.file}:${withId.line}`,
|
|
6569
|
+
state: "could-not-judge",
|
|
6570
|
+
reason,
|
|
6571
|
+
verdict: `could not judge: ${reason}`
|
|
6447
6572
|
};
|
|
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
|
-
};
|
|
6573
|
+
})();
|
|
6574
|
+
return settle(this.id, decided, unreadable);
|
|
6458
6575
|
}
|
|
6459
6576
|
};
|
|
6460
6577
|
|
|
@@ -6949,7 +7066,7 @@ import {
|
|
|
6949
7066
|
readFileSync as readFileSync9,
|
|
6950
7067
|
readdirSync as readdirSync6,
|
|
6951
7068
|
rmSync,
|
|
6952
|
-
statSync as
|
|
7069
|
+
statSync as statSync10,
|
|
6953
7070
|
writeFileSync
|
|
6954
7071
|
} from "fs";
|
|
6955
7072
|
import { tmpdir } from "os";
|
|
@@ -6975,15 +7092,17 @@ function walkForConfigs(dir, depth, seen, onSkip) {
|
|
|
6975
7092
|
let entries;
|
|
6976
7093
|
try {
|
|
6977
7094
|
entries = readdirSync6(dir);
|
|
6978
|
-
} catch {
|
|
7095
|
+
} catch (err) {
|
|
7096
|
+
onSkip(dir, depth, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
6979
7097
|
return;
|
|
6980
7098
|
}
|
|
6981
7099
|
for (const name of entries) {
|
|
6982
7100
|
const full = join9(dir, name);
|
|
6983
7101
|
let st;
|
|
6984
7102
|
try {
|
|
6985
|
-
st =
|
|
6986
|
-
} catch {
|
|
7103
|
+
st = statSync10(full);
|
|
7104
|
+
} catch (err) {
|
|
7105
|
+
onSkip(full, depth + 1, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
6987
7106
|
continue;
|
|
6988
7107
|
}
|
|
6989
7108
|
if (st.isDirectory()) {
|
|
@@ -6993,6 +7112,10 @@ function walkForConfigs(dir, depth, seen, onSkip) {
|
|
|
6993
7112
|
}
|
|
6994
7113
|
walkForConfigs(full, depth + 1, seen, onSkip);
|
|
6995
7114
|
} else if (DEPLOY_CONFIG_FILES.includes(name)) {
|
|
7115
|
+
if (!st.isFile()) {
|
|
7116
|
+
onSkip(full, depth + 1, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
7117
|
+
continue;
|
|
7118
|
+
}
|
|
6996
7119
|
if (!seen.has(dir)) {
|
|
6997
7120
|
seen.set(dir, { dir, configFile: name, configPath: full });
|
|
6998
7121
|
}
|
|
@@ -7012,7 +7135,7 @@ function probeForCandidate(dir, depth) {
|
|
|
7012
7135
|
const full = join9(dir, name);
|
|
7013
7136
|
let st;
|
|
7014
7137
|
try {
|
|
7015
|
-
st =
|
|
7138
|
+
st = statSync10(full);
|
|
7016
7139
|
} catch {
|
|
7017
7140
|
continue;
|
|
7018
7141
|
}
|
|
@@ -7054,12 +7177,12 @@ function resolveRailwayRootDirectory(configPath) {
|
|
|
7054
7177
|
function c26DeriveScope(root, tree) {
|
|
7055
7178
|
const seen = /* @__PURE__ */ new Map();
|
|
7056
7179
|
const skipped = [];
|
|
7057
|
-
const onSkip = (skippedDir, depth) => {
|
|
7058
|
-
const entry = tree ? recordSkip(tree, skippedDir, root) : {
|
|
7180
|
+
const onSkip = (skippedDir, depth, reason) => {
|
|
7181
|
+
const entry = tree ? recordSkip(tree, skippedDir, root, reason) : {
|
|
7059
7182
|
path: relative5(root, skippedDir).split(sep4).join("/"),
|
|
7060
|
-
reason: SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
7183
|
+
reason: reason ?? SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
7061
7184
|
};
|
|
7062
|
-
if (SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
7185
|
+
if (reason === void 0 && SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
7063
7186
|
const hit = probeForCandidate(skippedDir, depth);
|
|
7064
7187
|
if (hit) entry.candidate = relative5(root, hit).split(sep4).join("/");
|
|
7065
7188
|
}
|
|
@@ -7185,8 +7308,22 @@ function gateSite(site) {
|
|
|
7185
7308
|
}
|
|
7186
7309
|
const isolated = mkdtempSync(join9(tmpdir(), "mcp-doctor-c26-"));
|
|
7187
7310
|
try {
|
|
7188
|
-
|
|
7189
|
-
|
|
7311
|
+
let manifestText;
|
|
7312
|
+
let lockText;
|
|
7313
|
+
const lockPath = join9(site.dir, lock.name);
|
|
7314
|
+
try {
|
|
7315
|
+
for (const p of [pkgPath, lockPath]) {
|
|
7316
|
+
if (!statSync10(p).isFile()) throw new Error(`${p} is not a regular file`);
|
|
7317
|
+
}
|
|
7318
|
+
manifestText = readFileSync9(pkgPath, "utf8");
|
|
7319
|
+
lockText = readFileSync9(lockPath, "utf8");
|
|
7320
|
+
} catch (err) {
|
|
7321
|
+
return {
|
|
7322
|
+
site,
|
|
7323
|
+
status: "unreadable",
|
|
7324
|
+
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`
|
|
7325
|
+
};
|
|
7326
|
+
}
|
|
7190
7327
|
writeFileSync(join9(isolated, "package.json"), manifestText);
|
|
7191
7328
|
writeFileSync(join9(isolated, lock.name), lockText);
|
|
7192
7329
|
let proc;
|
|
@@ -7247,6 +7384,7 @@ function renderCoverage(scope, verdicts) {
|
|
|
7247
7384
|
return verdicts.map((v) => {
|
|
7248
7385
|
const rel = relative5(process.cwd(), v.site.dir) || v.site.dir;
|
|
7249
7386
|
if (v.status === "skipped") return `SKIPPED ${rel} (${v.reason})`;
|
|
7387
|
+
if (v.status === "unreadable") return `UNREAD ${rel} (${v.reason})`;
|
|
7250
7388
|
return `ANALYSED ${rel} -> ${v.ok ? "GREEN" : "RED"}: ${v.detail}`;
|
|
7251
7389
|
}).join(" | ");
|
|
7252
7390
|
}
|
|
@@ -7289,6 +7427,17 @@ var c26SubpackageLockfile = {
|
|
|
7289
7427
|
const verdicts = scope.sites.map(gateSite);
|
|
7290
7428
|
const inventory = renderCoverage(scope.sites, verdicts);
|
|
7291
7429
|
const analysed = verdicts.filter((v) => v.status === "analysed");
|
|
7430
|
+
const unread = verdicts.filter((v) => v.status === "unreadable");
|
|
7431
|
+
if (unread.length > 0 && !analysed.some((v) => v.ok === false)) {
|
|
7432
|
+
const reason = `${unread.length} deployable site(s) could not be read \u2014 ${unread.map((v) => v.reason).join("; ")}. Coverage: ${inventory}`;
|
|
7433
|
+
return {
|
|
7434
|
+
id: this.id,
|
|
7435
|
+
anchor: unread[0].site.dir,
|
|
7436
|
+
state: "could-not-judge",
|
|
7437
|
+
reason,
|
|
7438
|
+
verdict: `could not judge: ${reason}`
|
|
7439
|
+
};
|
|
7440
|
+
}
|
|
7292
7441
|
if (analysed.length === 0) {
|
|
7293
7442
|
if (hidden.length > 0) return unjudgedHidden("no analysable site; ");
|
|
7294
7443
|
return {
|
|
@@ -7428,22 +7577,21 @@ var c27SubpackageTypes = {
|
|
|
7428
7577
|
const workflows = governingWorkflows(tree);
|
|
7429
7578
|
const reds = [];
|
|
7430
7579
|
const oks = [];
|
|
7580
|
+
const unreadDocs = [];
|
|
7431
7581
|
for (const site of tsSites) {
|
|
7432
7582
|
const siteRel = relative6(base, site.dir) || ".";
|
|
7433
7583
|
const docs = [];
|
|
7434
7584
|
const dockerfile = join10(site.dir, "Dockerfile");
|
|
7435
7585
|
if (existsSync9(dockerfile)) {
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
});
|
|
7586
|
+
const text = readText(dockerfile);
|
|
7587
|
+
if (text === null) unreadDocs.push(`${siteRel}/Dockerfile`);
|
|
7588
|
+
else docs.push({ where: `${siteRel}/Dockerfile`, segments: dockerSegments(text) });
|
|
7440
7589
|
}
|
|
7441
7590
|
const railway = join10(site.dir, "railway.json");
|
|
7442
7591
|
if (existsSync9(railway)) {
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
});
|
|
7592
|
+
const text = readText(railway);
|
|
7593
|
+
if (text === null) unreadDocs.push(`${siteRel}/railway.json`);
|
|
7594
|
+
else docs.push({ where: `${siteRel}/railway.json`, segments: railwayBuildCommand(text) });
|
|
7447
7595
|
}
|
|
7448
7596
|
for (const w of workflows) {
|
|
7449
7597
|
if (siteRel === "." || w.text.includes(siteRel)) {
|
|
@@ -7456,6 +7604,7 @@ var c27SubpackageTypes = {
|
|
|
7456
7604
|
oks.push(`${ok.d.where} (${siteRel})`);
|
|
7457
7605
|
continue;
|
|
7458
7606
|
}
|
|
7607
|
+
if (unreadDocs.some((u) => u.startsWith(`${siteRel}/`))) continue;
|
|
7459
7608
|
const unfrozen = verdicts.find((v) => v.phase === "unfrozen");
|
|
7460
7609
|
const early = verdicts.find((v) => v.phase === "before-install");
|
|
7461
7610
|
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 +7617,13 @@ var c27SubpackageTypes = {
|
|
|
7468
7617
|
`${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
7618
|
);
|
|
7470
7619
|
}
|
|
7620
|
+
if (unreadDocs.length > 0) {
|
|
7621
|
+
return couldNotJudge(
|
|
7622
|
+
this.id,
|
|
7623
|
+
`${unreadDocs[0]}:1`,
|
|
7624
|
+
`${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)`
|
|
7625
|
+
);
|
|
7626
|
+
}
|
|
7471
7627
|
return couldNotJudge(
|
|
7472
7628
|
this.id,
|
|
7473
7629
|
oks[0],
|
|
@@ -7697,7 +7853,7 @@ var c30SkillsOrgBound = {
|
|
|
7697
7853
|
};
|
|
7698
7854
|
|
|
7699
7855
|
// src/detectors/c31-plugin-installable.ts
|
|
7700
|
-
import { existsSync as existsSync10, readdirSync as readdirSync8, statSync as
|
|
7856
|
+
import { existsSync as existsSync10, readdirSync as readdirSync8, statSync as statSync11 } from "fs";
|
|
7701
7857
|
import { basename as basename3, dirname as dirname4, join as join11, relative as relative8, sep as sep5 } from "path";
|
|
7702
7858
|
var MIN_README_WORDS = 40;
|
|
7703
7859
|
var KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -7898,12 +8054,17 @@ function judgeFolder(tree, folder, manifestPath2, out) {
|
|
|
7898
8054
|
}
|
|
7899
8055
|
const hasLicenseField = typeof manifest.license === "string" && manifest.license.trim() !== "";
|
|
7900
8056
|
let hasLicenseFile = false;
|
|
8057
|
+
let folderListed = true;
|
|
7901
8058
|
try {
|
|
7902
8059
|
hasLicenseFile = readdirSync8(folder).some((n) => /^LICEN[CS]E/i.test(n));
|
|
7903
|
-
} catch {
|
|
7904
|
-
|
|
8060
|
+
} catch (err) {
|
|
8061
|
+
folderListed = false;
|
|
8062
|
+
out.unreadable.push({
|
|
8063
|
+
anchor: rel(folder),
|
|
8064
|
+
reason: `${rel(folder)} cannot be listed (${fsErrorCode(err)}) \u2014 whether it carries a LICENSE file is unread`
|
|
8065
|
+
});
|
|
7905
8066
|
}
|
|
7906
|
-
if (!hasLicenseField && !hasLicenseFile) {
|
|
8067
|
+
if (!hasLicenseField && !hasLicenseFile && folderListed) {
|
|
7907
8068
|
out.reds.push({
|
|
7908
8069
|
anchor: a,
|
|
7909
8070
|
text: "no licence: plugin.json has no `license` and the folder has no LICENSE file"
|
|
@@ -7911,7 +8072,12 @@ function judgeFolder(tree, folder, manifestPath2, out) {
|
|
|
7911
8072
|
}
|
|
7912
8073
|
const readmePath = join11(folder, "README.md");
|
|
7913
8074
|
const readme = readText(readmePath);
|
|
7914
|
-
if (readme === null) {
|
|
8075
|
+
if (readme === null && existsSync10(readmePath)) {
|
|
8076
|
+
out.unreadable.push({
|
|
8077
|
+
anchor: rel(readmePath),
|
|
8078
|
+
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`
|
|
8079
|
+
});
|
|
8080
|
+
} else if (readme === null) {
|
|
7915
8081
|
out.reds.push({ anchor: rel(folder), text: "no README.md at the plugin folder root" });
|
|
7916
8082
|
} else {
|
|
7917
8083
|
const n = proseWordCount(readme);
|
|
@@ -7971,13 +8137,20 @@ function judgeFolder(tree, folder, manifestPath2, out) {
|
|
|
7971
8137
|
judgeServers(servers, mcpAnchor, out);
|
|
7972
8138
|
}
|
|
7973
8139
|
try {
|
|
7974
|
-
if (
|
|
8140
|
+
if (statSync11(join11(folder, "bin")).isDirectory()) {
|
|
7975
8141
|
out.reds.push({
|
|
7976
8142
|
anchor: rel(join11(folder, "bin")),
|
|
7977
8143
|
text: "a top-level bin/ directory (a plugin ships no bin/ at its top level)"
|
|
7978
8144
|
});
|
|
7979
8145
|
}
|
|
7980
|
-
} catch {
|
|
8146
|
+
} catch (err) {
|
|
8147
|
+
const code = fsErrorCode(err);
|
|
8148
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
8149
|
+
out.unreadable.push({
|
|
8150
|
+
anchor: rel(join11(folder, "bin")),
|
|
8151
|
+
reason: `${rel(join11(folder, "bin"))} could not be probed (${code}) \u2014 whether the plugin ships a top-level bin/ is unread`
|
|
8152
|
+
});
|
|
8153
|
+
}
|
|
7981
8154
|
}
|
|
7982
8155
|
const jsons = [[a, manifest]];
|
|
7983
8156
|
if (servers) jsons.push([mcpAnchor, servers]);
|
|
@@ -8757,7 +8930,7 @@ var d7Inner = {
|
|
|
8757
8930
|
var d7 = guardUnreadablePackageJson(d7Inner, { authoritative: true });
|
|
8758
8931
|
|
|
8759
8932
|
// src/detectors/d8.ts
|
|
8760
|
-
import { existsSync as existsSync11, statSync as
|
|
8933
|
+
import { existsSync as existsSync11, statSync as statSync12 } from "fs";
|
|
8761
8934
|
import { dirname as dirname5, join as join12, relative as relative9 } from "path";
|
|
8762
8935
|
var POSITIVE_CONTROL2 = 'server.registerTool("list_items", { ... }, handler);';
|
|
8763
8936
|
function d8PositiveControl() {
|
|
@@ -8770,7 +8943,7 @@ function d8PositiveControl() {
|
|
|
8770
8943
|
function resolveBaseDir(tree) {
|
|
8771
8944
|
if (tree.packageJson) return dirname5(tree.packageJson.path);
|
|
8772
8945
|
try {
|
|
8773
|
-
return
|
|
8946
|
+
return statSync12(tree.root).isDirectory() ? tree.root : dirname5(tree.root);
|
|
8774
8947
|
} catch {
|
|
8775
8948
|
return tree.root;
|
|
8776
8949
|
}
|
|
@@ -8904,6 +9077,13 @@ var d8 = {
|
|
|
8904
9077
|
verdict: "server advertises no tools at runtime -> n/a for eval coverage"
|
|
8905
9078
|
};
|
|
8906
9079
|
}
|
|
9080
|
+
if (runtime.evalsUnreadable) {
|
|
9081
|
+
return couldNotJudge(
|
|
9082
|
+
this.id,
|
|
9083
|
+
`${runtime.root} (runtime tools/list)`,
|
|
9084
|
+
`the eval file is present but unread \u2014 ${runtime.evalsUnreadable}; per-tool coverage is unknown, never zero (Server Rule 5)`
|
|
9085
|
+
);
|
|
9086
|
+
}
|
|
8907
9087
|
const summary = runtime.tools.map((t) => `${t.name}=${t.evalCaseCount ?? 0}`).join(" ");
|
|
8908
9088
|
const under = runtime.tools.filter((t) => (t.evalCaseCount ?? 0) < 3);
|
|
8909
9089
|
if (under.length > 0) {
|
|
@@ -9529,7 +9709,7 @@ var d15 = {
|
|
|
9529
9709
|
};
|
|
9530
9710
|
|
|
9531
9711
|
// src/detectors/d16.ts
|
|
9532
|
-
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as
|
|
9712
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync13 } from "fs";
|
|
9533
9713
|
import { dirname as dirname8, join as join15 } from "path";
|
|
9534
9714
|
var DISCOVER_RE2 = /server\/discover|DiscoverResult|ServerDiscover|handleServerDiscover/;
|
|
9535
9715
|
var HEALTH_ROUTE_FRAMEWORK_RE = /\b[A-Za-z_$][\w$]*\.(?:get|post|put|patch|delete|all|route|use)\s*\(\s*["'`][^"'`]*\/health/;
|
|
@@ -9851,10 +10031,11 @@ var d16 = {
|
|
|
9851
10031
|
};
|
|
9852
10032
|
function readPackageVersion(root) {
|
|
9853
10033
|
try {
|
|
9854
|
-
let dir =
|
|
10034
|
+
let dir = statSync13(root).isDirectory() ? root : dirname8(root);
|
|
9855
10035
|
for (let depth = 0; depth < 8; depth++) {
|
|
9856
10036
|
const candidate = join15(dir, "package.json");
|
|
9857
10037
|
if (existsSync14(candidate)) {
|
|
10038
|
+
if (!statSync13(candidate).isFile()) return null;
|
|
9858
10039
|
const pkg = JSON.parse(readFileSync10(candidate, "utf8"));
|
|
9859
10040
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
9860
10041
|
}
|
|
@@ -9869,7 +10050,7 @@ function readPackageVersion(root) {
|
|
|
9869
10050
|
}
|
|
9870
10051
|
|
|
9871
10052
|
// src/detectors/dAppI18n.ts
|
|
9872
|
-
import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
|
|
10053
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync14 } from "fs";
|
|
9873
10054
|
import { dirname as dirname9, join as join16 } from "path";
|
|
9874
10055
|
function flattenKeys(obj, prefix = "") {
|
|
9875
10056
|
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
@@ -9903,6 +10084,7 @@ function dAppI18nPositiveControls() {
|
|
|
9903
10084
|
}
|
|
9904
10085
|
function readJson2(path) {
|
|
9905
10086
|
try {
|
|
10087
|
+
if (!statSync14(path).isFile()) return { ok: false };
|
|
9906
10088
|
return { ok: true, value: JSON.parse(readFileSync11(path, "utf8")) };
|
|
9907
10089
|
} catch {
|
|
9908
10090
|
return { ok: false };
|
|
@@ -10101,14 +10283,14 @@ var dDebugOptIn = {
|
|
|
10101
10283
|
};
|
|
10102
10284
|
|
|
10103
10285
|
// src/detectors/extManifests.ts
|
|
10104
|
-
import { readdirSync as readdirSync9, statSync as
|
|
10286
|
+
import { readdirSync as readdirSync9, statSync as statSync15 } from "fs";
|
|
10105
10287
|
import { join as join17 } from "path";
|
|
10106
10288
|
function findExtensionManifests(tree) {
|
|
10107
10289
|
const out = [];
|
|
10108
10290
|
const root = tree.root;
|
|
10109
10291
|
let base;
|
|
10110
10292
|
try {
|
|
10111
|
-
base =
|
|
10293
|
+
base = statSync15(root).isDirectory() ? root : join17(root, "..");
|
|
10112
10294
|
} catch {
|
|
10113
10295
|
return out;
|
|
10114
10296
|
}
|
|
@@ -10116,15 +10298,17 @@ function findExtensionManifests(tree) {
|
|
|
10116
10298
|
let entries;
|
|
10117
10299
|
try {
|
|
10118
10300
|
entries = readdirSync9(dir);
|
|
10119
|
-
} catch {
|
|
10301
|
+
} catch (err) {
|
|
10302
|
+
recordSkip(tree, dir, base, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
10120
10303
|
return;
|
|
10121
10304
|
}
|
|
10122
10305
|
for (const name of entries) {
|
|
10123
10306
|
const full = join17(dir, name);
|
|
10124
10307
|
let st;
|
|
10125
10308
|
try {
|
|
10126
|
-
st =
|
|
10127
|
-
} catch {
|
|
10309
|
+
st = statSync15(full);
|
|
10310
|
+
} catch (err) {
|
|
10311
|
+
recordSkip(tree, full, base, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
10128
10312
|
continue;
|
|
10129
10313
|
}
|
|
10130
10314
|
if (st.isDirectory()) {
|
|
@@ -10134,6 +10318,10 @@ function findExtensionManifests(tree) {
|
|
|
10134
10318
|
}
|
|
10135
10319
|
walk2(full);
|
|
10136
10320
|
} else if (name === "extension.json") {
|
|
10321
|
+
if (!st.isFile()) {
|
|
10322
|
+
recordSkip(tree, full, base, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
10323
|
+
continue;
|
|
10324
|
+
}
|
|
10137
10325
|
out.push(full);
|
|
10138
10326
|
}
|
|
10139
10327
|
}
|
|
@@ -10750,8 +10938,8 @@ var dExt3 = {
|
|
|
10750
10938
|
};
|
|
10751
10939
|
|
|
10752
10940
|
// src/detectors/dExt4.ts
|
|
10753
|
-
import { readdirSync as readdirSync10, statSync as
|
|
10754
|
-
import { join as join19 } from "path";
|
|
10941
|
+
import { readdirSync as readdirSync10, statSync as statSync16 } from "fs";
|
|
10942
|
+
import { join as join19, relative as relative10 } from "path";
|
|
10755
10943
|
function canonical(name) {
|
|
10756
10944
|
return name.replace(/[-_]/g, "").toLowerCase();
|
|
10757
10945
|
}
|
|
@@ -10770,28 +10958,39 @@ function dExt4PositiveControls() {
|
|
|
10770
10958
|
}
|
|
10771
10959
|
};
|
|
10772
10960
|
}
|
|
10773
|
-
function findHooksDirs(tree) {
|
|
10961
|
+
function findHooksDirs(tree, unreadable, gaps) {
|
|
10774
10962
|
const root = tree.root;
|
|
10775
10963
|
const out = [];
|
|
10776
10964
|
let base;
|
|
10777
10965
|
try {
|
|
10778
|
-
base =
|
|
10966
|
+
base = statSync16(root).isDirectory() ? root : join19(root, "..");
|
|
10779
10967
|
} catch {
|
|
10780
10968
|
return out;
|
|
10781
10969
|
}
|
|
10970
|
+
const noteUnread = (path, what, err) => {
|
|
10971
|
+
gaps.count++;
|
|
10972
|
+
const anchor = relative10(base, path) || ".";
|
|
10973
|
+
const reason = `${anchor} ${what} (${fsErrorCode(err)}) \u2014 a hooks/ directory under it may exist unseen, so the implemented hooks are unverifiable (Extension Rule 4)`;
|
|
10974
|
+
if (!unreadable.some((u) => u.anchor === anchor && u.reason === reason)) {
|
|
10975
|
+
unreadable.push({ anchor, reason });
|
|
10976
|
+
}
|
|
10977
|
+
};
|
|
10782
10978
|
const walk2 = (dir) => {
|
|
10783
10979
|
let entries;
|
|
10784
10980
|
try {
|
|
10785
10981
|
entries = readdirSync10(dir);
|
|
10786
|
-
} catch {
|
|
10982
|
+
} catch (err) {
|
|
10983
|
+
noteUnread(dir, "could not be listed", err);
|
|
10787
10984
|
return;
|
|
10788
10985
|
}
|
|
10789
10986
|
for (const name of entries) {
|
|
10790
10987
|
const full = join19(dir, name);
|
|
10791
10988
|
let st;
|
|
10792
10989
|
try {
|
|
10793
|
-
st =
|
|
10794
|
-
} catch {
|
|
10990
|
+
st = statSync16(full);
|
|
10991
|
+
} catch (err) {
|
|
10992
|
+
if (fsErrorCode(err) !== "ENOENT" && fsErrorCode(err) !== "ELOOP")
|
|
10993
|
+
noteUnread(full, "could not be read", err);
|
|
10795
10994
|
continue;
|
|
10796
10995
|
}
|
|
10797
10996
|
if (!st.isDirectory()) continue;
|
|
@@ -10804,7 +11003,8 @@ function findHooksDirs(tree) {
|
|
|
10804
11003
|
let files;
|
|
10805
11004
|
try {
|
|
10806
11005
|
files = readdirSync10(full);
|
|
10807
|
-
} catch {
|
|
11006
|
+
} catch (err) {
|
|
11007
|
+
noteUnread(full, "could not be listed", err);
|
|
10808
11008
|
files = [];
|
|
10809
11009
|
}
|
|
10810
11010
|
for (const f of files) {
|
|
@@ -10882,7 +11082,8 @@ var dExt4 = {
|
|
|
10882
11082
|
}
|
|
10883
11083
|
const unreadable = [];
|
|
10884
11084
|
const decided = (() => {
|
|
10885
|
-
const
|
|
11085
|
+
const hooksDirGaps = { count: 0 };
|
|
11086
|
+
const hooksDirs = findHooksDirs(tree, unreadable, hooksDirGaps);
|
|
10886
11087
|
const { sites, unresolved } = findFactorySites(tree);
|
|
10887
11088
|
for (const anchor of unresolved) {
|
|
10888
11089
|
unreadable.push({
|
|
@@ -10926,6 +11127,7 @@ var dExt4 = {
|
|
|
10926
11127
|
continue;
|
|
10927
11128
|
}
|
|
10928
11129
|
const phantom = [...declared].filter((h) => !implemented.has(h));
|
|
11130
|
+
if (phantom.length > 0 && hooksDirGaps.count > 0) continue;
|
|
10929
11131
|
if (phantom.length > 0) {
|
|
10930
11132
|
return {
|
|
10931
11133
|
id: this.id,
|
|
@@ -11313,30 +11515,41 @@ var dExt8 = {
|
|
|
11313
11515
|
};
|
|
11314
11516
|
|
|
11315
11517
|
// 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) {
|
|
11518
|
+
import { readFileSync as readFileSync13, readdirSync as readdirSync11, statSync as statSync17 } from "fs";
|
|
11519
|
+
import { join as join23, relative as relative11 } from "path";
|
|
11520
|
+
function collectHooksDirSources(tree, unreadable, gaps) {
|
|
11319
11521
|
const root = tree.root;
|
|
11320
11522
|
const out = [];
|
|
11321
11523
|
let base;
|
|
11322
11524
|
try {
|
|
11323
|
-
base =
|
|
11525
|
+
base = statSync17(root).isDirectory() ? root : join23(root, "..");
|
|
11324
11526
|
} catch {
|
|
11325
11527
|
return out;
|
|
11326
11528
|
}
|
|
11529
|
+
const noteUnread = (path, what, err) => {
|
|
11530
|
+
gaps.count++;
|
|
11531
|
+
const anchor = relative11(base, path) || ".";
|
|
11532
|
+
const reason = `${anchor} ${what} (${fsErrorCode(err)}) \u2014 a hooks/ directory under it may exist unseen, so the timeout guards are undeterminable (Extension Rule 9)`;
|
|
11533
|
+
if (!unreadable.some((u) => u.anchor === anchor && u.reason === reason)) {
|
|
11534
|
+
unreadable.push({ anchor, reason });
|
|
11535
|
+
}
|
|
11536
|
+
};
|
|
11327
11537
|
const walk2 = (dir) => {
|
|
11328
11538
|
let entries;
|
|
11329
11539
|
try {
|
|
11330
11540
|
entries = readdirSync11(dir);
|
|
11331
|
-
} catch {
|
|
11541
|
+
} catch (err) {
|
|
11542
|
+
noteUnread(dir, "could not be listed", err);
|
|
11332
11543
|
return;
|
|
11333
11544
|
}
|
|
11334
11545
|
for (const name of entries) {
|
|
11335
11546
|
const full = join23(dir, name);
|
|
11336
11547
|
let st;
|
|
11337
11548
|
try {
|
|
11338
|
-
st =
|
|
11339
|
-
} catch {
|
|
11549
|
+
st = statSync17(full);
|
|
11550
|
+
} catch (err) {
|
|
11551
|
+
if (fsErrorCode(err) !== "ENOENT" && fsErrorCode(err) !== "ELOOP")
|
|
11552
|
+
noteUnread(full, "could not be read", err);
|
|
11340
11553
|
continue;
|
|
11341
11554
|
}
|
|
11342
11555
|
if (!st.isDirectory()) continue;
|
|
@@ -11348,7 +11561,8 @@ function collectHooksDirSources(tree, unreadable) {
|
|
|
11348
11561
|
let files;
|
|
11349
11562
|
try {
|
|
11350
11563
|
files = readdirSync11(full);
|
|
11351
|
-
} catch {
|
|
11564
|
+
} catch (err) {
|
|
11565
|
+
noteUnread(full, "could not be listed", err);
|
|
11352
11566
|
files = [];
|
|
11353
11567
|
}
|
|
11354
11568
|
for (const f of files) {
|
|
@@ -11358,6 +11572,7 @@ function collectHooksDirSources(tree, unreadable) {
|
|
|
11358
11572
|
const filePath = join23(full, f);
|
|
11359
11573
|
let body;
|
|
11360
11574
|
try {
|
|
11575
|
+
if (!statSync17(filePath).isFile()) throw new Error("not a regular file");
|
|
11361
11576
|
body = readFileSync13(filePath, "utf8");
|
|
11362
11577
|
} catch {
|
|
11363
11578
|
const shortPath = filePath.split("/").slice(-2).join("/");
|
|
@@ -11461,8 +11676,9 @@ var dExt9 = {
|
|
|
11461
11676
|
}
|
|
11462
11677
|
const unreadable = [];
|
|
11463
11678
|
const decided = (() => {
|
|
11679
|
+
const hooksDirGaps = { count: 0 };
|
|
11464
11680
|
const hookSources = [
|
|
11465
|
-
...collectHooksDirSources(tree, unreadable),
|
|
11681
|
+
...collectHooksDirSources(tree, unreadable, hooksDirGaps),
|
|
11466
11682
|
...collectFactorySources(tree)
|
|
11467
11683
|
];
|
|
11468
11684
|
const implByToken = /* @__PURE__ */ new Map();
|
|
@@ -11511,6 +11727,7 @@ var dExt9 = {
|
|
|
11511
11727
|
const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
|
|
11512
11728
|
if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
|
|
11513
11729
|
}
|
|
11730
|
+
if (noTimeout.length > 0 && hooksDirGaps.count > 0) continue;
|
|
11514
11731
|
if (noTimeout.length > 0) {
|
|
11515
11732
|
return {
|
|
11516
11733
|
id: this.id,
|
|
@@ -12524,7 +12741,6 @@ var dServer14 = {
|
|
|
12524
12741
|
};
|
|
12525
12742
|
|
|
12526
12743
|
// src/detectors/e5-extension-isolation.ts
|
|
12527
|
-
import { readFileSync as readFileSync14 } from "fs";
|
|
12528
12744
|
var GLOBAL_RES = [
|
|
12529
12745
|
/\b(?:globalThis|global|window|self)\s*(?:\.\s*[\w$]+|\[[^\]]+\])\s*=(?!=)/,
|
|
12530
12746
|
/Object\.(?:assign|defineProperty|defineProperties|setPrototypeOf)\(\s*(?:globalThis|global|window|self)\b/,
|
|
@@ -12555,11 +12771,8 @@ var e5ExtensionIsolation = {
|
|
|
12555
12771
|
const manifests = findExtensionManifests(tree);
|
|
12556
12772
|
const own = /* @__PURE__ */ new Set();
|
|
12557
12773
|
for (const m of manifests) {
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
if (typeof j.identifier === "string") own.add(j.identifier);
|
|
12561
|
-
} catch {
|
|
12562
|
-
}
|
|
12774
|
+
const j = readJsonObject(m);
|
|
12775
|
+
if (j && typeof j.identifier === "string") own.add(j.identifier);
|
|
12563
12776
|
}
|
|
12564
12777
|
const defined = tree.files.some(
|
|
12565
12778
|
(f) => isProductSource(f.rel) && uncommentedLines(f).some((r) => /\bdefineExtension\s*\(/.test(r))
|
|
@@ -12989,8 +13202,8 @@ var e10MrtrOnly = {
|
|
|
12989
13202
|
};
|
|
12990
13203
|
|
|
12991
13204
|
// src/detectors/e11-surfaces-derived.ts
|
|
12992
|
-
import { existsSync as existsSync20, statSync as
|
|
12993
|
-
import { dirname as dirname14, join as join24, relative as
|
|
13205
|
+
import { existsSync as existsSync20, statSync as statSync18 } from "fs";
|
|
13206
|
+
import { dirname as dirname14, join as join24, relative as relative12, sep as sep6 } from "path";
|
|
12994
13207
|
var ALL = ["chat", "cowork", "claude-code"];
|
|
12995
13208
|
function excludedSurfaces(c) {
|
|
12996
13209
|
const cause = /* @__PURE__ */ new Map();
|
|
@@ -13006,10 +13219,17 @@ function excludedSurfaces(c) {
|
|
|
13006
13219
|
if (c.localMcp) exclude("chat", "a local MCP command excludes chat");
|
|
13007
13220
|
return cause;
|
|
13008
13221
|
}
|
|
13009
|
-
function isDir(p) {
|
|
13222
|
+
function isDir(p, anchor, unreadable) {
|
|
13010
13223
|
try {
|
|
13011
|
-
return
|
|
13012
|
-
} catch {
|
|
13224
|
+
return statSync18(p).isDirectory();
|
|
13225
|
+
} catch (err) {
|
|
13226
|
+
const code = fsErrorCode(err);
|
|
13227
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
13228
|
+
unreadable.push({
|
|
13229
|
+
anchor,
|
|
13230
|
+
reason: `${anchor} could not be probed (${code}) \u2014 whether this component exists is unread`
|
|
13231
|
+
});
|
|
13232
|
+
}
|
|
13013
13233
|
return false;
|
|
13014
13234
|
}
|
|
13015
13235
|
}
|
|
@@ -13048,7 +13268,7 @@ var e11SurfacesDerived = {
|
|
|
13048
13268
|
continue;
|
|
13049
13269
|
}
|
|
13050
13270
|
const folder = dirname14(m.path);
|
|
13051
|
-
const rel = (p) =>
|
|
13271
|
+
const rel = (p) => relative12(tree.root, p).split(sep6).join("/") || ".";
|
|
13052
13272
|
if (!Array.isArray(j.surfaces)) {
|
|
13053
13273
|
reds.push({
|
|
13054
13274
|
anchor: m.rel,
|
|
@@ -13084,9 +13304,9 @@ var e11SurfacesDerived = {
|
|
|
13084
13304
|
}
|
|
13085
13305
|
}
|
|
13086
13306
|
const cause = excludedSurfaces({
|
|
13087
|
-
bin: isDir(join24(folder, "bin")),
|
|
13088
|
-
hooks: isDir(join24(folder, "hooks")),
|
|
13089
|
-
agents: isDir(join24(folder, "agents")),
|
|
13307
|
+
bin: isDir(join24(folder, "bin"), rel(join24(folder, "bin")), unreadable),
|
|
13308
|
+
hooks: isDir(join24(folder, "hooks"), rel(join24(folder, "hooks")), unreadable),
|
|
13309
|
+
agents: isDir(join24(folder, "agents"), rel(join24(folder, "agents")), unreadable),
|
|
13090
13310
|
localMcp
|
|
13091
13311
|
});
|
|
13092
13312
|
for (const s of declared) {
|
|
@@ -15432,8 +15652,8 @@ var s27ElicitationFloor = {
|
|
|
15432
15652
|
};
|
|
15433
15653
|
|
|
15434
15654
|
// src/detectors/s28-claim-forwarded.ts
|
|
15435
|
-
import { existsSync as existsSync21, readFileSync as
|
|
15436
|
-
import { dirname as dirname15, join as join25, relative as
|
|
15655
|
+
import { existsSync as existsSync21, readFileSync as readFileSync14, readdirSync as readdirSync12, statSync as statSync19 } from "fs";
|
|
15656
|
+
import { dirname as dirname15, join as join25, relative as relative13 } from "path";
|
|
15437
15657
|
var PIN11 = STANDARD_PIN;
|
|
15438
15658
|
var CLAIM_TOKEN = /\bverified(?:Org|Person)\b/;
|
|
15439
15659
|
var ANY_CLAIM = /\bverified(?:Actor|Org|Person)\b/g;
|
|
@@ -15491,11 +15711,12 @@ function splitTop(m, from, to) {
|
|
|
15491
15711
|
if (start < to && m.slice(start, to).trim() !== "") parts.push([start, to]);
|
|
15492
15712
|
return parts;
|
|
15493
15713
|
}
|
|
15494
|
-
function listTs(dir, out = []) {
|
|
15714
|
+
function listTs(dir, unread, out = []) {
|
|
15495
15715
|
let names;
|
|
15496
15716
|
try {
|
|
15497
15717
|
names = readdirSync12(dir);
|
|
15498
|
-
} catch {
|
|
15718
|
+
} catch (err) {
|
|
15719
|
+
unread.push(`${dir} could not be listed (${fsErrorCode(err)})`);
|
|
15499
15720
|
return out;
|
|
15500
15721
|
}
|
|
15501
15722
|
for (const n of names) {
|
|
@@ -15503,19 +15724,23 @@ function listTs(dir, out = []) {
|
|
|
15503
15724
|
const p = join25(dir, n);
|
|
15504
15725
|
let st;
|
|
15505
15726
|
try {
|
|
15506
|
-
st =
|
|
15507
|
-
} catch {
|
|
15727
|
+
st = statSync19(p);
|
|
15728
|
+
} catch (err) {
|
|
15729
|
+
unread.push(`${p} could not be read (${fsErrorCode(err)})`);
|
|
15508
15730
|
continue;
|
|
15509
15731
|
}
|
|
15510
|
-
if (st.isDirectory()) listTs(p, out);
|
|
15511
|
-
else if (n.endsWith(".ts") && !n.endsWith(".test.ts") && !n.endsWith(".d.ts"))
|
|
15732
|
+
if (st.isDirectory()) listTs(p, unread, out);
|
|
15733
|
+
else if (n.endsWith(".ts") && !n.endsWith(".test.ts") && !n.endsWith(".d.ts")) {
|
|
15734
|
+
if (!st.isFile()) unread.push(`${p} is not a regular file \u2014 not read`);
|
|
15735
|
+
else out.push(p);
|
|
15736
|
+
}
|
|
15512
15737
|
}
|
|
15513
15738
|
return out;
|
|
15514
15739
|
}
|
|
15515
15740
|
function discoverBackend(root) {
|
|
15516
15741
|
let dir = root;
|
|
15517
15742
|
try {
|
|
15518
|
-
if (!
|
|
15743
|
+
if (!statSync19(root).isDirectory()) dir = dirname15(root);
|
|
15519
15744
|
} catch {
|
|
15520
15745
|
return void 0;
|
|
15521
15746
|
}
|
|
@@ -15528,22 +15753,23 @@ function discoverBackend(root) {
|
|
|
15528
15753
|
}
|
|
15529
15754
|
return void 0;
|
|
15530
15755
|
}
|
|
15531
|
-
function readDoors(backendRoot) {
|
|
15756
|
+
function readDoors(backendRoot, unread) {
|
|
15532
15757
|
const doors = /* @__PURE__ */ new Map();
|
|
15533
|
-
for (const path of listTs(backendRoot)) {
|
|
15758
|
+
for (const path of listTs(backendRoot, unread)) {
|
|
15534
15759
|
let text;
|
|
15535
15760
|
try {
|
|
15536
|
-
text =
|
|
15537
|
-
} catch {
|
|
15761
|
+
text = readFileSync14(path, "utf8");
|
|
15762
|
+
} catch (err) {
|
|
15763
|
+
unread.push(`${path} could not be read (${fsErrorCode(err)})`);
|
|
15538
15764
|
continue;
|
|
15539
15765
|
}
|
|
15540
15766
|
const file = {
|
|
15541
15767
|
path,
|
|
15542
|
-
rel:
|
|
15768
|
+
rel: relative13(backendRoot, path),
|
|
15543
15769
|
lines: text.split("\n").map((raw, i) => ({ n: i + 1, raw, comment: false }))
|
|
15544
15770
|
};
|
|
15545
15771
|
const m = masked(file);
|
|
15546
|
-
const module =
|
|
15772
|
+
const module = relative13(backendRoot, path).replace(/\.ts$/, "").split("\\").join("/");
|
|
15547
15773
|
const re = /\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:query|mutation|action)\s*\(/g;
|
|
15548
15774
|
for (let hit = re.exec(m); hit; hit = re.exec(m)) {
|
|
15549
15775
|
const open = hit.index + hit[0].length - 1;
|
|
@@ -15656,7 +15882,9 @@ function scanS28(tree) {
|
|
|
15656
15882
|
const scan = { violate: [], conform: [], cnj: [], calls: 0, backend: void 0 };
|
|
15657
15883
|
const backendRoot = tree.backendRoot ?? discoverBackend(tree.root);
|
|
15658
15884
|
scan.backend = backendRoot;
|
|
15659
|
-
const
|
|
15885
|
+
const unread = [];
|
|
15886
|
+
const doors = backendRoot ? readDoors(backendRoot, unread) : void 0;
|
|
15887
|
+
const unreadNote = unread.length > 0 ? `; backend source NOT read: ${unread.join("; ")}` : "";
|
|
15660
15888
|
const files = tree.files.map((f) => ({ f, text: rawText(f), m: masked(f) }));
|
|
15661
15889
|
const actorDoors = /* @__PURE__ */ new Set();
|
|
15662
15890
|
for (const { text } of files) {
|
|
@@ -15776,7 +16004,7 @@ function scanS28(tree) {
|
|
|
15776
16004
|
if (!info) {
|
|
15777
16005
|
scan.cnj.push(
|
|
15778
16006
|
site(
|
|
15779
|
-
`forwards no verified claim and the door ${door} is not defined in the supplied backend (${backendRoot})`
|
|
16007
|
+
`forwards no verified claim and the door ${door} is not defined in the supplied backend (${backendRoot})${unreadNote}`
|
|
15780
16008
|
)
|
|
15781
16009
|
);
|
|
15782
16010
|
continue;
|
|
@@ -16623,12 +16851,12 @@ var detectors = registered.map(
|
|
|
16623
16851
|
|
|
16624
16852
|
// src/runtime/launch.ts
|
|
16625
16853
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
16626
|
-
import { existsSync as existsSync22, readFileSync as
|
|
16854
|
+
import { existsSync as existsSync22, readFileSync as readFileSync15, statSync as statSync20 } from "fs";
|
|
16627
16855
|
import { dirname as dirname16, isAbsolute, join as join26, resolve as resolve2 } from "path";
|
|
16628
16856
|
function findPackageDir(target) {
|
|
16629
16857
|
let dir;
|
|
16630
16858
|
try {
|
|
16631
|
-
dir =
|
|
16859
|
+
dir = statSync20(target).isDirectory() ? target : dirname16(target);
|
|
16632
16860
|
} catch {
|
|
16633
16861
|
return null;
|
|
16634
16862
|
}
|
|
@@ -16658,7 +16886,10 @@ function deriveLaunchPlan(target) {
|
|
|
16658
16886
|
}
|
|
16659
16887
|
let pkg;
|
|
16660
16888
|
try {
|
|
16661
|
-
|
|
16889
|
+
if (!statSync20(join26(pkgDir, "package.json")).isFile()) {
|
|
16890
|
+
return { error: `${join26(pkgDir, "package.json")} is not a regular file \u2014 not read` };
|
|
16891
|
+
}
|
|
16892
|
+
pkg = JSON.parse(readFileSync15(join26(pkgDir, "package.json"), "utf8"));
|
|
16662
16893
|
} catch (err) {
|
|
16663
16894
|
return {
|
|
16664
16895
|
error: `could not parse ${join26(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -16796,29 +17027,41 @@ send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "
|
|
|
16796
17027
|
function readEvalCounts(target) {
|
|
16797
17028
|
const counts = /* @__PURE__ */ new Map();
|
|
16798
17029
|
const pkgDir = findPackageDir(target) ?? (statSyncSafe(target) ? target : null);
|
|
16799
|
-
if (!pkgDir) return counts;
|
|
17030
|
+
if (!pkgDir) return { counts };
|
|
16800
17031
|
let dir = pkgDir;
|
|
16801
17032
|
for (let depth = 0; depth < 8; depth++) {
|
|
16802
17033
|
const candidate = join26(dir, "evals", "evals.json");
|
|
16803
17034
|
if (existsSync22(candidate)) {
|
|
17035
|
+
let text;
|
|
17036
|
+
try {
|
|
17037
|
+
if (!statSync20(candidate).isFile()) {
|
|
17038
|
+
return { counts, unreadable: `${candidate} is not a regular file \u2014 not read` };
|
|
17039
|
+
}
|
|
17040
|
+
text = readFileSync15(candidate, "utf8");
|
|
17041
|
+
} catch (err) {
|
|
17042
|
+
return {
|
|
17043
|
+
counts,
|
|
17044
|
+
unreadable: `${candidate} could not be read (${fsErrorCode(err)})`
|
|
17045
|
+
};
|
|
17046
|
+
}
|
|
16804
17047
|
try {
|
|
16805
|
-
const parsed = JSON.parse(
|
|
17048
|
+
const parsed = JSON.parse(text);
|
|
16806
17049
|
for (const c of parsed.cases ?? []) {
|
|
16807
17050
|
if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
|
|
16808
17051
|
}
|
|
16809
17052
|
} catch {
|
|
16810
17053
|
}
|
|
16811
|
-
return counts;
|
|
17054
|
+
return { counts };
|
|
16812
17055
|
}
|
|
16813
17056
|
const parent = dirname16(dir);
|
|
16814
17057
|
if (parent === dir) break;
|
|
16815
17058
|
dir = parent;
|
|
16816
17059
|
}
|
|
16817
|
-
return counts;
|
|
17060
|
+
return { counts };
|
|
16818
17061
|
}
|
|
16819
17062
|
function statSyncSafe(p) {
|
|
16820
17063
|
try {
|
|
16821
|
-
return
|
|
17064
|
+
return statSync20(p).isDirectory();
|
|
16822
17065
|
} catch {
|
|
16823
17066
|
return false;
|
|
16824
17067
|
}
|
|
@@ -16926,7 +17169,7 @@ function launchAndIntrospect(target) {
|
|
|
16926
17169
|
if (parsed.launchError) {
|
|
16927
17170
|
return { root: target, tools: [], launchError: parsed.launchError };
|
|
16928
17171
|
}
|
|
16929
|
-
const evalCounts = readEvalCounts(target);
|
|
17172
|
+
const { counts: evalCounts, unreadable: evalsUnreadable } = readEvalCounts(target);
|
|
16930
17173
|
const tools = (parsed.tools ?? []).map((t) => {
|
|
16931
17174
|
const name = t.name;
|
|
16932
17175
|
const count = evalCounts.get(name);
|
|
@@ -16940,6 +17183,7 @@ function launchAndIntrospect(target) {
|
|
|
16940
17183
|
tools,
|
|
16941
17184
|
...parsed.serverInfo ? { serverInfo: parsed.serverInfo } : {},
|
|
16942
17185
|
...expectedCommit ? { expectedCommit } : {},
|
|
17186
|
+
...evalsUnreadable ? { evalsUnreadable } : {},
|
|
16943
17187
|
...Array.isArray(parsed.cursorProbes) ? { cursorProbes: parsed.cursorProbes.map(readCursorProbe) } : {}
|
|
16944
17188
|
};
|
|
16945
17189
|
}
|
|
@@ -17198,7 +17442,7 @@ var USAGE = [
|
|
|
17198
17442
|
function readVersion() {
|
|
17199
17443
|
try {
|
|
17200
17444
|
const here = dirname17(fileURLToPath2(import.meta.url));
|
|
17201
|
-
const manifest = JSON.parse(
|
|
17445
|
+
const manifest = JSON.parse(readFileSync16(join27(here, "..", "package.json"), "utf8"));
|
|
17202
17446
|
return manifest.version ?? null;
|
|
17203
17447
|
} catch {
|
|
17204
17448
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vantageos/mcp-doctor",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Static MCP conformance checker: audits an MCP server source tree against the MCP 2026-07-28 standard and emits a conformance-diff table (file:line | present | absent | non-conforming | verdict).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/doctor.js",
|