@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/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 readFileSync17, realpathSync } from "fs";
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 statSync2 } from "fs";
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 statSync2(tree.root).isDirectory() ? tree.root : join2(tree.root, "..");
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 = statSync2(full);
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)) out.push({ path: full, rel, name });
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
- if (configs.length === 0) {
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 (!configs.some((c) => SINGLEFILE_RE.test(stripComments(readText(c.path) ?? "")))) {
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 statSync3 } from "fs";
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
- for (const f of findDiskFiles(tree, pred)) {
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
- for (const name of readdirSync3(wf).sort()) {
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 (!statSync3(full).isFile()) continue;
2637
- } catch {
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 absentResult(
2857
+ return settle(
2790
2858
  this.id,
2791
- "-",
2792
- "no UI code, no HTML and no ui:// resource in the tree (positive control OK) \u2014 A-3 n/a"
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
- if (uiRels.size === 0 && htmlShells.length === 0 && inlineScripts.length === 0) {
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 redResult(
2923
+ return settle(
2844
2924
  this.id,
2845
- reds[0].split(" ")[0],
2846
- `${reds.join("; ")} (A-3, positive-control OK)`
2925
+ redResult(this.id, reds[0].split(" ")[0], `${reds.join("; ")} (A-3, positive-control OK)`),
2926
+ unreadHtml
2847
2927
  );
2848
2928
  }
2849
- return presentResult(
2929
+ return settle(
2850
2930
  this.id,
2851
- app.anchor,
2852
- `${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)`
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 absentResult(
2962
+ return settle(
2879
2963
  this.id,
2880
- "-",
2881
- "not an App (no UI code, no HTML, no ui:// resource; positive control OK) \u2014 A-4 n/a"
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 noUiCode = app.uiFiles.length === 0 && findDiskFiles(tree, (name) => /\.html?$/i.test(name)).length === 0;
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 testRun = diskText(tree, (name) => /\.(?:test|spec)\.[jt]sx?$/.test(name)).find(
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 statSync4 } from "fs";
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 = statSync4(full);
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 absentResult(
3129
+ return settle(
3024
3130
  this.id,
3025
- "-",
3026
- "not an App (no UI code, no HTML, no ui:// resource; positive control OK) \u2014 A-5 n/a"
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
- for (const f of findDiskFiles(tree, (n) => n === "mcp.json" || n === "mcp-server.json")) {
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
- for (const f of findDiskFiles(tree, (n) => n === "mcp.json" || n === "mcp-server.json")) {
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
- return findDiskFiles(tree, (n) => n === "SKILL.md").map((f) => {
3550
- const text = readText(f.path) ?? "";
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
- return {
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
- for (const sf of readSkillFiles(tree)) {
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
- id: this.id,
3618
- anchor: reds[0].anchor,
3619
- state: "non-conforming",
3620
- 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)`
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 couldNotJudge(
3764
+ return settle(
3624
3765
  this.id,
3625
- surface.anchor,
3626
- "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)"
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 doc = readDocText(tree.root);
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
- for (const f of findDiskFiles(tree, (n) => SERVER_MANIFEST_NAMES.has(n))) {
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 claim = findProvenanceClaim(tree, tree.root);
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({ name, path, role: classifyRootJson(json), json });
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
- return JSON.parse(readFileSync7(path, "utf8"));
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
- manifest = readJson(path);
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 controls = c22PositiveControls();
6421
- if (!controls.ratioMatcher.matched || !controls.identifierMatcher.matched) {
6422
- throw new Error(
6423
- `C22 positive control FAILED: ratio=${controls.ratioMatcher.matched} identifier=${controls.identifierMatcher.matched} \u2014 matcher broken, verdict untrustworthy`
6424
- );
6425
- }
6426
- if (!controls.identifierClearsBareRatio) {
6427
- throw new Error(
6428
- "C22 identifier-matcher control FAILED: must NOT flag a bare ratio with no identifier token \u2014 verdict untrustworthy"
6429
- );
6430
- }
6431
- const hits = findRatioCitations(tree.root);
6432
- if (hits.length === 0) {
6433
- return {
6434
- id: this.id,
6435
- anchor: "-",
6436
- state: "absent",
6437
- verdict: "no test/coverage ratio cited in README/CHANGELOG; test-ratio-gate check n/a"
6438
- };
6439
- }
6440
- const bare = hits.find((h) => !h.hasIdentifier);
6441
- if (bare) {
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: `${bare.file}:${bare.line}`,
6445
- state: "non-conforming",
6446
- 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)`
6688
+ anchor: `${withId.file}:${withId.line}`,
6689
+ state: "could-not-judge",
6690
+ reason,
6691
+ verdict: `could not judge: ${reason}`
6447
6692
  };
6448
- }
6449
- const withId = hits[0];
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 docs = diskText(tree, (_name, rel) => DISPATCH_PATH_RE.test(rel)).filter(
6609
- (d) => INSTRUMENT_RE.test(d.text) && ACTION_RE.test(d.text)
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 absentResult(
6852
+ return settle(
6613
6853
  this.id,
6614
- "-",
6615
- "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)"
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 statSync5,
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 = statSync5(full);
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 = statSync5(full);
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
- const manifestText = readFileSync9(pkgPath, "utf8");
7189
- const lockText = readFileSync9(join9(site.dir, lock.name), "utf8");
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
- docs.push({
7437
- where: `${siteRel}/Dockerfile`,
7438
- segments: dockerSegments(readText(dockerfile) ?? "")
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
- docs.push({
7444
- where: `${siteRel}/railway.json`,
7445
- segments: railwayBuildCommand(readText(railway) ?? "")
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 = [...surface.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 statSync6 } from "fs";
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
- hasLicenseFile = false;
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 (statSync6(join11(folder, "bin")).isDirectory()) {
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, (n) => n === "mcp.json" || n === "mcp-server.json")) {
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
- id: this.id,
8564
- anchor: "-",
8565
- state: "absent",
8566
- 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"
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 statSync7 } from "fs";
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 statSync7(tree.root).isDirectory() ? tree.root : dirname5(tree.root);
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 statSync8 } from "fs";
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 = statSync8(root).isDirectory() ? root : dirname8(root);
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 statSync9 } from "fs";
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 = statSync9(root).isDirectory() ? root : join17(root, "..");
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 = statSync9(full);
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 statSync10 } from "fs";
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 = statSync10(root).isDirectory() ? root : join19(root, "..");
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 = statSync10(full);
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 hooksDirs = findHooksDirs(tree);
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 statSync11 } from "fs";
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 = statSync11(root).isDirectory() ? root : join23(root, "..");
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 = statSync11(full);
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
- try {
12559
- const j = JSON.parse(readFileSync14(m, "utf8"));
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 statSync12 } from "fs";
12993
- import { dirname as dirname14, join as join24, relative as relative10, sep as sep6 } from "path";
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 statSync12(p).isDirectory();
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
- id: this.id,
13033
- anchor: "-",
13034
- state: "absent",
13035
- verdict: "no extension.json in the tree: the object is not an extension; E-11 n/a"
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) => relative10(tree.root, p).split(sep6).join("/") || ".";
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, (n) => n === "mcp.json" || n === "mcp-server.json")) {
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 couldNotJudge(
13555
+ return settle(
13186
13556
  this.id,
13187
- r.caps[0].anchor,
13188
- `${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)`
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
- if (!documented) {
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 readFileSync15, readdirSync as readdirSync12, statSync as statSync13 } from "fs";
15436
- import { dirname as dirname15, join as join25, relative as relative11 } from "path";
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 = statSync13(p);
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")) out.push(p);
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 (!statSync13(root).isDirectory()) dir = dirname15(root);
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 = readFileSync15(path, "utf8");
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: relative11(backendRoot, path),
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 = relative11(backendRoot, path).replace(/\.ts$/, "").split("\\").join("/");
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 doors = backendRoot ? readDoors(backendRoot) : void 0;
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 readFileSync16, statSync as statSync14 } from "fs";
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 = statSync14(target).isDirectory() ? target : dirname16(target);
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
- pkg = JSON.parse(readFileSync16(join26(pkgDir, "package.json"), "utf8"));
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(readFileSync16(candidate, "utf8"));
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 statSync14(p).isDirectory();
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(readFileSync17(join27(here, "..", "package.json"), "utf8"));
17628
+ const manifest = JSON.parse(readFileSync16(join27(here, "..", "package.json"), "utf8"));
17202
17629
  return manifest.version ?? null;
17203
17630
  } catch {
17204
17631
  return null;