@vantageos/mcp-doctor 0.3.1 → 0.3.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/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 readFileSync19, realpathSync } from "fs";
4
+ import { readFileSync as readFileSync12, realpathSync } from "fs";
5
5
  import { dirname as dirname11, join as join21, resolve as resolve2 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
@@ -230,8 +230,46 @@ function findRegisteredTools(tree) {
230
230
  }
231
231
  }
232
232
  }
233
+ for (const file of tree.files) {
234
+ for (let i = 0; i < file.lines.length; i++) {
235
+ const line = file.lines[i];
236
+ if (line.comment) continue;
237
+ const m = line.raw.match(DESCRIPTOR_KEY_RE);
238
+ if (!m) continue;
239
+ const indent = m[1];
240
+ const hasSchema = objectHasKey(file.lines, i, indent);
241
+ if (hasSchema) push(m[2], `${file.rel}:${line.n}`);
242
+ }
243
+ }
233
244
  return out;
234
245
  }
246
+ var DESCRIPTOR_KEY_RE = new RegExp(`^(\\s*)name:\\s*["'\`](${NAME_LITERAL})["'\`]\\s*,?\\s*$`);
247
+ var INPUT_SCHEMA_KEY_RE = /^(\s*)inputSchema\s*:/;
248
+ function objectHasKey(lines, at, indent) {
249
+ for (const step of [1, -1]) {
250
+ for (let j = at + step; j >= 0 && j < lines.length; j += step) {
251
+ const l = lines[j];
252
+ if (l.comment || l.raw.trim() === "") continue;
253
+ const lead = l.raw.match(/^(\s*)/)[1];
254
+ if (lead.length < indent.length) break;
255
+ if (lead === indent && INPUT_SCHEMA_KEY_RE.test(l.raw)) return true;
256
+ }
257
+ }
258
+ return false;
259
+ }
260
+ var TOOL_SURFACE_RE = /\bListToolsRequestSchema\b|["'`]tools\/list["'`]|\binputSchema\s*:|\bToolDefinition\b/;
261
+ function toolSurfaceAnchor(tree) {
262
+ for (const file of tree.files) {
263
+ for (const line of file.lines) {
264
+ if (line.comment) continue;
265
+ if (TOOL_SURFACE_RE.test(line.raw)) return `${file.rel}:${line.n}`;
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+ function noToolsLocatedReason(tree, what) {
271
+ return `the tree serves MCP tools (tool-surface marker found) but no registered tool could be located: searched ${tree.files.length} .ts/.tsx file(s) under ${tree.root} (all directories except node_modules/dist/build/tests/fixtures) for registerTool/.tool( calls and declarative { name, inputSchema } descriptors \u2014 ${what}`;
272
+ }
235
273
  var REGISTRATION_CALL_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(/;
236
274
  function findUnresolvedToolSites(tree) {
237
275
  const bindings = resolveNameBindings(tree);
@@ -1139,6 +1177,59 @@ var c13ContextCeiling = {
1139
1177
  }
1140
1178
  };
1141
1179
 
1180
+ // src/detectors/couldNotJudge.ts
1181
+ import { readFileSync as readFileSync2 } from "fs";
1182
+ function couldNotJudge(id, anchor, reason) {
1183
+ return {
1184
+ id,
1185
+ anchor,
1186
+ state: "could-not-judge",
1187
+ reason,
1188
+ verdict: `could not judge: ${reason}`
1189
+ };
1190
+ }
1191
+ function settle(id, decided, unreadable) {
1192
+ if (unreadable.length === 0) return decided;
1193
+ const named = unreadable.map((u) => `${u.anchor}: ${u.reason}`).join("; ");
1194
+ if (decided.state === "non-conforming") {
1195
+ return { ...decided, verdict: `${decided.verdict} [also could not read: ${named}]` };
1196
+ }
1197
+ const judgedRest = decided.state === "could-not-judge" ? ` ${decided.reason ?? decided.verdict}` : ` The readable remainder judged ${decided.state}: ${decided.verdict}`;
1198
+ return couldNotJudge(
1199
+ id,
1200
+ unreadable[0].anchor,
1201
+ `${unreadable.length} input(s) present but unreadable \u2014 ${named}.${judgedRest}`
1202
+ );
1203
+ }
1204
+ function readJsonObject(path) {
1205
+ let value;
1206
+ try {
1207
+ value = JSON.parse(readFileSync2(path, "utf8"));
1208
+ } catch {
1209
+ return null;
1210
+ }
1211
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
1212
+ return value;
1213
+ }
1214
+ function guardUnreadablePackageJson(inner, opts) {
1215
+ return {
1216
+ ...inner,
1217
+ run(tree) {
1218
+ const decided = inner.run(tree);
1219
+ const pkg = tree.packageJson;
1220
+ if (!pkg?.unreadable) return decided;
1221
+ const u = {
1222
+ anchor: pkg.path,
1223
+ reason: `package.json is present but could not be parsed (${pkg.unreadable})`
1224
+ };
1225
+ if (opts.authoritative && decided.state !== "could-not-judge") {
1226
+ return couldNotJudge(inner.id, u.anchor, `${u.anchor}: ${u.reason}`);
1227
+ }
1228
+ return settle(inner.id, decided, [u]);
1229
+ }
1230
+ };
1231
+ }
1232
+
1142
1233
  // src/detectors/c14-catalog-registration.ts
1143
1234
  var PIN3 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1144
1235
  var REQUIRED_FIELDS = [
@@ -1180,7 +1271,7 @@ function c14PositiveControls() {
1180
1271
  rejectsPartial: missing(partial) !== void 0
1181
1272
  };
1182
1273
  }
1183
- var c14CatalogRegistration = {
1274
+ var c14CatalogRegistrationInner = {
1184
1275
  id: "C14 catalog-registration",
1185
1276
  rule: "C-14",
1186
1277
  pin: PIN3,
@@ -1235,6 +1326,10 @@ var c14CatalogRegistration = {
1235
1326
  };
1236
1327
  }
1237
1328
  };
1329
+ var c14CatalogRegistration = guardUnreadablePackageJson(
1330
+ c14CatalogRegistrationInner,
1331
+ { authoritative: true }
1332
+ );
1238
1333
 
1239
1334
  // src/detectors/c15-annotations-untrusted.ts
1240
1335
  var AUTO_APPROVE_ACTION_RE = /\b(?:autoApprove|skipConfirm|autoRun|autoExecute)\w*\s*\(/;
@@ -1419,7 +1514,7 @@ var c16UntrustedOutput = {
1419
1514
  };
1420
1515
 
1421
1516
  // src/detectors/c17-supply-chain.ts
1422
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
1517
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
1423
1518
  import { join as join2 } from "path";
1424
1519
  var PIN4 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1425
1520
  var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
@@ -1464,7 +1559,7 @@ function findProvenanceClaim(tree, root) {
1464
1559
  const path = join2(workflowsDir, name);
1465
1560
  let text;
1466
1561
  try {
1467
- text = readFileSync2(path, "utf8");
1562
+ text = readFileSync3(path, "utf8");
1468
1563
  } catch {
1469
1564
  continue;
1470
1565
  }
@@ -1475,7 +1570,7 @@ function findProvenanceClaim(tree, root) {
1475
1570
  }
1476
1571
  return null;
1477
1572
  }
1478
- var c17SupplyChain = {
1573
+ var c17SupplyChainInner = {
1479
1574
  id: "C17 supply-chain",
1480
1575
  rule: "C-17",
1481
1576
  pin: PIN4,
@@ -1512,9 +1607,12 @@ var c17SupplyChain = {
1512
1607
  };
1513
1608
  }
1514
1609
  };
1610
+ var c17SupplyChain = guardUnreadablePackageJson(c17SupplyChainInner, {
1611
+ authoritative: true
1612
+ });
1515
1613
 
1516
1614
  // src/detectors/c18-structure-manifest.ts
1517
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1615
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
1518
1616
  import { join as join3 } from "path";
1519
1617
  var PIN5 = "a74a042b53c71a095ab88b3073bd47728eeda814";
1520
1618
  function manifestPath(root) {
@@ -1524,7 +1622,7 @@ function manifestPath(root) {
1524
1622
  }
1525
1623
  function readJson(path) {
1526
1624
  try {
1527
- return JSON.parse(readFileSync3(path, "utf8"));
1625
+ return JSON.parse(readFileSync4(path, "utf8"));
1528
1626
  } catch {
1529
1627
  return null;
1530
1628
  }
@@ -1538,7 +1636,7 @@ function c18PositiveControls() {
1538
1636
  }
1539
1637
  };
1540
1638
  }
1541
- var c18StructureManifest = {
1639
+ var c18StructureManifestInner = {
1542
1640
  id: "C18 structure-manifest",
1543
1641
  rule: "C-18",
1544
1642
  pin: PIN5,
@@ -1650,9 +1748,13 @@ var c18StructureManifest = {
1650
1748
  };
1651
1749
  }
1652
1750
  };
1751
+ var c18StructureManifest = guardUnreadablePackageJson(
1752
+ c18StructureManifestInner,
1753
+ { authoritative: false }
1754
+ );
1653
1755
 
1654
1756
  // src/scan.ts
1655
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync } from "fs";
1757
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync } from "fs";
1656
1758
  import { dirname, join as join4, relative } from "path";
1657
1759
  var SKIP_DIRS = /* @__PURE__ */ new Set([
1658
1760
  "node_modules",
@@ -1699,7 +1801,7 @@ function walk(dir, out) {
1699
1801
  }
1700
1802
  }
1701
1803
  function loadFile(path, root) {
1702
- const raw = readFileSync4(path, "utf8");
1804
+ const raw = readFileSync5(path, "utf8");
1703
1805
  const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
1704
1806
  return { path, rel: relative(root, path) || path, lines };
1705
1807
  }
@@ -1709,9 +1811,13 @@ function findPackageJson(startDir) {
1709
1811
  const candidate = join4(dir, "package.json");
1710
1812
  if (existsSync4(candidate)) {
1711
1813
  try {
1712
- return { path: candidate, json: JSON.parse(readFileSync4(candidate, "utf8")) };
1713
- } catch {
1714
- return { path: candidate, json: {} };
1814
+ const parsed = JSON.parse(readFileSync5(candidate, "utf8"));
1815
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1816
+ return { path: candidate, json: {}, unreadable: "not a JSON object" };
1817
+ }
1818
+ return { path: candidate, json: parsed };
1819
+ } catch (err) {
1820
+ return { path: candidate, json: {}, unreadable: err.message };
1715
1821
  }
1716
1822
  }
1717
1823
  const parent = dirname(dir);
@@ -1787,6 +1893,20 @@ var c19ServedDescriptions = {
1787
1893
  }
1788
1894
  const sites = findRegisteredTools(tree);
1789
1895
  if (sites.length === 0) {
1896
+ const surface = toolSurfaceAnchor(tree);
1897
+ if (surface) {
1898
+ const reason = noToolsLocatedReason(
1899
+ tree,
1900
+ "served descriptions cannot be judged without the tool list (C-19)"
1901
+ );
1902
+ return {
1903
+ id: this.id,
1904
+ anchor: surface,
1905
+ state: "could-not-judge",
1906
+ reason,
1907
+ verdict: `could not judge: ${reason}`
1908
+ };
1909
+ }
1790
1910
  return {
1791
1911
  id: this.id,
1792
1912
  anchor: "-",
@@ -2044,7 +2164,7 @@ var c21ProgressiveDisclosure = {
2044
2164
  };
2045
2165
 
2046
2166
  // src/detectors/c22-test-ratio-gate.ts
2047
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
2167
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
2048
2168
  import { join as join5 } from "path";
2049
2169
  var DOC_FILES2 = ["README.md", "README.fr.md", "CHANGELOG.md"];
2050
2170
  var RATIO_RE = /\b(?:coverage|ratio de tests?|test ratio|couverture)\b[^\n%]{0,40}?(\d{1,3}(?:\.\d+)?\s?%)/i;
@@ -2074,7 +2194,7 @@ function findRatioCitations(root) {
2074
2194
  if (!existsSync5(path)) continue;
2075
2195
  let text;
2076
2196
  try {
2077
- text = readFileSync5(path, "utf8");
2197
+ text = readFileSync6(path, "utf8");
2078
2198
  } catch {
2079
2199
  continue;
2080
2200
  }
@@ -2382,7 +2502,7 @@ import { spawnSync } from "child_process";
2382
2502
  import {
2383
2503
  existsSync as existsSync6,
2384
2504
  mkdtempSync,
2385
- readFileSync as readFileSync6,
2505
+ readFileSync as readFileSync7,
2386
2506
  readdirSync as readdirSync3,
2387
2507
  rmSync,
2388
2508
  statSync as statSync2,
@@ -2436,7 +2556,7 @@ function walkForConfigs(dir, depth, seen) {
2436
2556
  function resolveRailwayRootDirectory(configPath) {
2437
2557
  let text;
2438
2558
  try {
2439
- text = readFileSync6(configPath, "utf8");
2559
+ text = readFileSync7(configPath, "utf8");
2440
2560
  } catch (err) {
2441
2561
  return {
2442
2562
  error: `could not read ${configPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -2471,7 +2591,7 @@ function c26DeriveScope(root) {
2471
2591
  }
2472
2592
  if (site.configFile === "vercel.json") {
2473
2593
  try {
2474
- JSON.parse(readFileSync6(site.configPath, "utf8"));
2594
+ JSON.parse(readFileSync7(site.configPath, "utf8"));
2475
2595
  } catch (err) {
2476
2596
  return {
2477
2597
  sites: [],
@@ -2539,8 +2659,8 @@ function nameDrift(lockfileName, siteDir, isolatedDir, stderr) {
2539
2659
  }
2540
2660
  if (lockfileName === "bun.lock" || lockfileName === "bun.lockb") {
2541
2661
  try {
2542
- const manifestText = readFileSync6(join6(isolatedDir, "package.json"), "utf8");
2543
- const lockText = readFileSync6(join6(isolatedDir, lockfileName), "utf8");
2662
+ const manifestText = readFileSync7(join6(isolatedDir, "package.json"), "utf8");
2663
+ const lockText = readFileSync7(join6(isolatedDir, lockfileName), "utf8");
2544
2664
  const drifted = nameBunDrift(manifestText, lockText);
2545
2665
  if (drifted.length > 0) return drifted.join(", ");
2546
2666
  } catch {
@@ -2571,8 +2691,8 @@ function gateSite(site) {
2571
2691
  }
2572
2692
  const isolated = mkdtempSync(join6(tmpdir(), "mcp-doctor-c26-"));
2573
2693
  try {
2574
- const manifestText = readFileSync6(pkgPath, "utf8");
2575
- const lockText = readFileSync6(join6(site.dir, lock.name), "utf8");
2694
+ const manifestText = readFileSync7(pkgPath, "utf8");
2695
+ const lockText = readFileSync7(join6(site.dir, lock.name), "utf8");
2576
2696
  writeFileSync(join6(isolated, "package.json"), manifestText);
2577
2697
  writeFileSync(join6(isolated, lock.name), lockText);
2578
2698
  let proc;
@@ -2749,6 +2869,21 @@ var d3 = {
2749
2869
  verdict: `could not judge: ${reason}`
2750
2870
  };
2751
2871
  }
2872
+ const surface = toolSurfaceAnchor(tree);
2873
+ if (surface) {
2874
+ const located = findRegisteredTools(tree);
2875
+ const reason = located.length > 0 ? `${located.length} tool(s) located but no hand-rolled list/read result builder found \u2014 result shaping and any resultType are delegated to wrappers/the transport and not statically readable in this tree` : noToolsLocatedReason(
2876
+ tree,
2877
+ "result shaping and resultType are not statically readable without the located tools"
2878
+ );
2879
+ return {
2880
+ id: this.id,
2881
+ anchor: surface,
2882
+ state: "could-not-judge",
2883
+ reason,
2884
+ verdict: `could not judge: ${reason}`
2885
+ };
2886
+ }
2752
2887
  return {
2753
2888
  id: this.id,
2754
2889
  anchor: "-",
@@ -2793,6 +2928,21 @@ var d4 = {
2793
2928
  verdict: `could not judge: ${reason}`
2794
2929
  };
2795
2930
  }
2931
+ const surface = toolSurfaceAnchor(tree);
2932
+ if (surface) {
2933
+ const located = findRegisteredTools(tree);
2934
+ const reason = located.length > 0 ? `${located.length} tool(s) located but no hand-rolled list/read result builder found \u2014 result shaping and any ttlMs/cacheScope are delegated to wrappers/the transport and not statically readable in this tree` : noToolsLocatedReason(
2935
+ tree,
2936
+ "result shaping and any ttlMs/cacheScope are not statically readable without the located tools"
2937
+ );
2938
+ return {
2939
+ id: this.id,
2940
+ anchor: surface,
2941
+ state: "could-not-judge",
2942
+ reason,
2943
+ verdict: `could not judge: ${reason}`
2944
+ };
2945
+ }
2796
2946
  return {
2797
2947
  id: this.id,
2798
2948
  anchor: "-",
@@ -2922,7 +3072,7 @@ function depListed(pkgJson) {
2922
3072
  }
2923
3073
  return false;
2924
3074
  }
2925
- var d7 = {
3075
+ var d7Inner = {
2926
3076
  id: "D7 protocol-kit",
2927
3077
  run(tree) {
2928
3078
  const listed = depListed(tree.packageJson?.json);
@@ -2961,9 +3111,10 @@ var d7 = {
2961
3111
  };
2962
3112
  }
2963
3113
  };
3114
+ var d7 = guardUnreadablePackageJson(d7Inner, { authoritative: true });
2964
3115
 
2965
3116
  // src/detectors/d8.ts
2966
- import { existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
3117
+ import { existsSync as existsSync7, statSync as statSync3 } from "fs";
2967
3118
  import { dirname as dirname2, join as join7, relative as relative3 } from "path";
2968
3119
  var POSITIVE_CONTROL2 = 'server.registerTool("list_items", { ... }, handler);';
2969
3120
  function d8PositiveControl() {
@@ -3016,6 +3167,20 @@ var d8 = {
3016
3167
  verdict: `could not judge: ${reason}`
3017
3168
  };
3018
3169
  }
3170
+ const surface = toolSurfaceAnchor(tree);
3171
+ if (surface) {
3172
+ const reason = noToolsLocatedReason(
3173
+ tree,
3174
+ "eval coverage cannot be judged without the tool list"
3175
+ );
3176
+ return {
3177
+ id: this.id,
3178
+ anchor: surface,
3179
+ state: "could-not-judge",
3180
+ reason,
3181
+ verdict: `could not judge: ${reason}`
3182
+ };
3183
+ }
3019
3184
  return {
3020
3185
  id: this.id,
3021
3186
  anchor: "-",
@@ -3034,19 +3199,15 @@ var d8 = {
3034
3199
  }
3035
3200
  const baseDir = resolveBaseDir(tree);
3036
3201
  const displayPath = relative3(baseDir, evalsPath) || evalsPath;
3037
- let cases;
3038
- try {
3039
- const raw = readFileSync7(evalsPath, "utf8");
3040
- const parsed = JSON.parse(raw);
3041
- if (!Array.isArray(parsed.cases)) {
3042
- throw new Error("`cases` is not an array");
3043
- }
3044
- cases = parsed.cases;
3045
- } catch (err) {
3046
- throw new Error(
3047
- `D8 could not parse ${displayPath}: ${err instanceof Error ? err.message : String(err)} \u2014 coverage verdict untrustworthy, fix the eval file`
3202
+ const parsed = readJsonObject(evalsPath);
3203
+ if (parsed === null || !Array.isArray(parsed.cases)) {
3204
+ return couldNotJudge(
3205
+ this.id,
3206
+ displayPath,
3207
+ `could not parse ${displayPath}: ${parsed === null ? "not readable as a JSON object" : "`cases` is not an array"} \u2014 coverage verdict untrustworthy, fix the eval file`
3048
3208
  );
3049
3209
  }
3210
+ const cases = parsed.cases;
3050
3211
  const counts = /* @__PURE__ */ new Map();
3051
3212
  for (const t of tools) counts.set(t.name, 0);
3052
3213
  for (const c of cases) {
@@ -3131,7 +3292,7 @@ function d9PositiveControl() {
3131
3292
  matched: SEMVER_RE.test(POSITIVE_CONTROL3)
3132
3293
  };
3133
3294
  }
3134
- var d9 = {
3295
+ var d9Inner = {
3135
3296
  id: "D9 semver+changelog",
3136
3297
  rule: "C-3",
3137
3298
  pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
@@ -3154,7 +3315,11 @@ var d9 = {
3154
3315
  }
3155
3316
  const { path: pkgPath, json: pkgJson } = tree.packageJson;
3156
3317
  if (!pkgJson || Object.keys(pkgJson).length === 0) {
3157
- throw new Error(`D9 could not parse ${pkgPath} as JSON \u2014 cannot verify SemVer/CHANGELOG`);
3318
+ return couldNotJudge(
3319
+ this.id,
3320
+ pkgPath.split("/").slice(-2).join("/"),
3321
+ `could not parse ${pkgPath} as JSON \u2014 cannot verify SemVer/CHANGELOG`
3322
+ );
3158
3323
  }
3159
3324
  const version = pkgJson.version;
3160
3325
  if (typeof version !== "string") {
@@ -3193,6 +3358,7 @@ var d9 = {
3193
3358
  };
3194
3359
  }
3195
3360
  };
3361
+ var d9 = guardUnreadablePackageJson(d9Inner, { authoritative: false });
3196
3362
 
3197
3363
  // src/detectors/d10.ts
3198
3364
  import { existsSync as existsSync9 } from "fs";
@@ -3244,7 +3410,7 @@ function scanSourceDescriptionFr(tree) {
3244
3410
  }
3245
3411
  return { present: false, anchor: null };
3246
3412
  }
3247
- var d10 = {
3413
+ var d10Inner = {
3248
3414
  id: "D10 bilingual",
3249
3415
  rule: "C-1",
3250
3416
  pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
@@ -3315,6 +3481,7 @@ var d10 = {
3315
3481
  };
3316
3482
  }
3317
3483
  };
3484
+ var d10 = guardUnreadablePackageJson(d10Inner, { authoritative: true });
3318
3485
 
3319
3486
  // src/detectors/d11.ts
3320
3487
  var ZOD_ANY_RE = /\bz\s*\.\s*any\s*\(\s*\)/;
@@ -3386,6 +3553,27 @@ var d11 = {
3386
3553
  }
3387
3554
  }
3388
3555
  if (toolCount === 0) {
3556
+ const located = findRegisteredTools(tree);
3557
+ if (located.length > 0) {
3558
+ toolCount = located.length;
3559
+ firstToolAnchor = located[0].anchor;
3560
+ }
3561
+ }
3562
+ if (toolCount === 0) {
3563
+ const surface = toolSurfaceAnchor(tree);
3564
+ if (surface) {
3565
+ const reason = noToolsLocatedReason(
3566
+ tree,
3567
+ "the outputSchema check cannot be judged without the tool list"
3568
+ );
3569
+ return {
3570
+ id: this.id,
3571
+ anchor: surface,
3572
+ state: "could-not-judge",
3573
+ reason,
3574
+ verdict: `could not judge: ${reason}`
3575
+ };
3576
+ }
3389
3577
  return {
3390
3578
  id: this.id,
3391
3579
  anchor: "-",
@@ -3465,7 +3653,7 @@ function findHttpSite(tree) {
3465
3653
  }
3466
3654
  return null;
3467
3655
  }
3468
- var d12 = {
3656
+ var d12Inner = {
3469
3657
  id: "D12 stdio-transport",
3470
3658
  run(tree) {
3471
3659
  const stdioControl = d12StdioPositiveControl();
@@ -3516,6 +3704,7 @@ var d12 = {
3516
3704
  };
3517
3705
  }
3518
3706
  };
3707
+ var d12 = guardUnreadablePackageJson(d12Inner, { authoritative: false });
3519
3708
 
3520
3709
  // src/detectors/d15.ts
3521
3710
  var HANDLE_ASSIGN_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[^;]*\.(?:registerTool|tool)\s*\(/;
@@ -4129,55 +4318,59 @@ var dDebugOptIn = {
4129
4318
  );
4130
4319
  }
4131
4320
  }
4132
- const computed = findAll(tree, COMPUTED_LEVEL_RE);
4133
- if (computed.length > 0) {
4134
- const sites = computed.map((h) => `${h.file.rel}:${h.line}`).join(", ");
4135
- throw new Error(
4136
- `DDebugOptIn cannot read the level of ${computed.length} computed-level log call(s): ${sites} \u2014 a bracket-indexed level hides whether it is debug (Server Rule 6). Fail-closed, not a silent present.`
4137
- );
4138
- }
4139
- const debugCalls = findAll(tree, DEBUG_CALL_RE);
4140
- if (debugCalls.length === 0) {
4141
- return {
4142
- id: this.id,
4143
- anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
4144
- state: "absent",
4145
- verdict: "no debug output emitted (n/a for Server Rule 6 debug-half)"
4146
- };
4147
- }
4148
- const callAnchor = `${debugCalls[0].file.rel}:${debugCalls.map((h) => h.line).join(",")}`;
4149
- const debugFileRels = new Set(debugCalls.map((h) => h.file.rel));
4150
- const localGate = firstMatchInFiles2(tree, GATE_RE, debugFileRels);
4151
- if (localGate) {
4152
- return {
4153
- id: this.id,
4154
- anchor: callAnchor,
4155
- state: "present",
4156
- verdict: `debug output at ${callAnchor} gated by an env/config switch in the same file at ${localGate.file.rel}:${localGate.line} (Server Rule 6 debug-half)`
4157
- };
4321
+ const unreadable = [];
4322
+ for (const h of findAll(tree, COMPUTED_LEVEL_RE)) {
4323
+ const anchor = `${h.file.rel}:${h.line}`;
4324
+ unreadable.push({
4325
+ anchor,
4326
+ reason: `cannot read the level of the computed-level log call at ${anchor} \u2014 a bracket-indexed level hides whether it is debug (Server Rule 6)`
4327
+ });
4158
4328
  }
4159
- const remoteGate = firstMatchInFiles2(tree, GATE_RE, new Set(tree.files.map((f) => f.rel)));
4160
- if (remoteGate) {
4161
- const reason = `debug output at ${callAnchor}; an env/config gate exists at ${remoteGate.file.rel}:${remoteGate.line} but in a DIFFERENT file from every debug call \u2014 whether the debug path is opt-in is unreadable statically (Server Rule 6 debug-half)`;
4329
+ const decided = (() => {
4330
+ const debugCalls = findAll(tree, DEBUG_CALL_RE);
4331
+ if (debugCalls.length === 0) {
4332
+ return {
4333
+ id: this.id,
4334
+ anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
4335
+ state: "absent",
4336
+ verdict: "no debug output emitted (n/a for Server Rule 6 debug-half)"
4337
+ };
4338
+ }
4339
+ const callAnchor = `${debugCalls[0].file.rel}:${debugCalls.map((h) => h.line).join(",")}`;
4340
+ const debugFileRels = new Set(debugCalls.map((h) => h.file.rel));
4341
+ const localGate = firstMatchInFiles2(tree, GATE_RE, debugFileRels);
4342
+ if (localGate) {
4343
+ return {
4344
+ id: this.id,
4345
+ anchor: callAnchor,
4346
+ state: "present",
4347
+ verdict: `debug output at ${callAnchor} gated by an env/config switch in the same file at ${localGate.file.rel}:${localGate.line} (Server Rule 6 debug-half)`
4348
+ };
4349
+ }
4350
+ const remoteGate = firstMatchInFiles2(tree, GATE_RE, new Set(tree.files.map((f) => f.rel)));
4351
+ if (remoteGate) {
4352
+ const reason = `debug output at ${callAnchor}; an env/config gate exists at ${remoteGate.file.rel}:${remoteGate.line} but in a DIFFERENT file from every debug call \u2014 whether the debug path is opt-in is unreadable statically (Server Rule 6 debug-half)`;
4353
+ return {
4354
+ id: this.id,
4355
+ anchor: callAnchor,
4356
+ state: "could-not-judge",
4357
+ reason,
4358
+ verdict: `could not judge: ${reason}`
4359
+ };
4360
+ }
4162
4361
  return {
4163
4362
  id: this.id,
4164
4363
  anchor: callAnchor,
4165
- state: "could-not-judge",
4166
- reason,
4167
- verdict: `could not judge: ${reason}`
4364
+ state: "non-conforming",
4365
+ verdict: `debug output at ${callAnchor} with NO env/config gate -> debug is on by default; gate it behind process.env.*DEBUG* or a config switch (Server Rule 6 debug-half)`
4168
4366
  };
4169
- }
4170
- return {
4171
- id: this.id,
4172
- anchor: callAnchor,
4173
- state: "non-conforming",
4174
- verdict: `debug output at ${callAnchor} with NO env/config gate -> debug is on by default; gate it behind process.env.*DEBUG* or a config switch (Server Rule 6 debug-half)`
4175
- };
4367
+ })();
4368
+ return settle(this.id, decided, unreadable);
4176
4369
  }
4177
4370
  };
4178
4371
 
4179
4372
  // src/detectors/dExt1.ts
4180
- import { readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4373
+ import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4181
4374
  import { join as join12 } from "path";
4182
4375
  var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
4183
4376
  var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
@@ -4255,68 +4448,76 @@ var dExt1 = {
4255
4448
  `DExt1 identifier control FAILED: /${controls.identifier.pattern}/ accept(\`${IDENTIFIER_POSITIVE}\`)=${controls.identifier.positive} reject(\`${IDENTIFIER_NEGATIVE}\`)=${!controls.identifier.negative} \u2014 matcher broken, verdict untrustworthy`
4256
4449
  );
4257
4450
  }
4258
- const manifests = findExtensionManifests(tree.root);
4259
- for (const manifestPath2 of manifests) {
4260
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4261
- let parsed;
4262
- try {
4263
- parsed = JSON.parse(readFileSync10(manifestPath2, "utf8"));
4264
- } catch {
4265
- throw new Error(
4266
- `DExt1 could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable identifier cannot pass (Extension Rule 1). Fail-closed, not a silent present.`
4267
- );
4451
+ const unreadable = [];
4452
+ const decided = (() => {
4453
+ const manifests = findExtensionManifests(tree.root);
4454
+ for (const manifestPath2 of manifests) {
4455
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
4456
+ const parsed = readJsonObject(manifestPath2);
4457
+ if (parsed === null) {
4458
+ unreadable.push({
4459
+ anchor: shortPath,
4460
+ reason: `could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable identifier cannot pass (Extension Rule 1)`
4461
+ });
4462
+ continue;
4463
+ }
4464
+ const idValue = parsed.identifier;
4465
+ if (typeof idValue !== "string") {
4466
+ return {
4467
+ id: this.id,
4468
+ anchor: shortPath,
4469
+ state: "non-conforming",
4470
+ verdict: `manifest ${shortPath} has no string "identifier" field (got ${typeof idValue}) -> declare a reverse-DNS identifier <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
4471
+ };
4472
+ }
4473
+ if (!IDENTIFIER_RE2.test(idValue)) {
4474
+ return {
4475
+ id: this.id,
4476
+ anchor: shortPath,
4477
+ state: "non-conforming",
4478
+ verdict: `manifest identifier "${idValue}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
4479
+ };
4480
+ }
4268
4481
  }
4269
- const idValue = parsed.identifier;
4270
- if (typeof idValue !== "string") {
4271
- throw new Error(
4272
- `DExt1 found a present manifest ${shortPath} with no string "identifier" field (got ${typeof idValue}) \u2014 Extension Rule 1 requires a reverse-DNS identifier. Fail-closed, not a silent present.`
4273
- );
4482
+ const { sites, unresolved } = findTsIdentifiers(tree);
4483
+ for (const anchor of unresolved) {
4484
+ unreadable.push({
4485
+ anchor,
4486
+ reason: `cannot read the identifier at ${anchor} \u2014 value is computed/imported/template, reverse-DNS unverifiable (Extension Rule 1)`
4487
+ });
4274
4488
  }
4275
- if (!IDENTIFIER_RE2.test(idValue)) {
4276
- return {
4277
- id: this.id,
4278
- anchor: shortPath,
4279
- state: "non-conforming",
4280
- verdict: `manifest identifier "${idValue}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
4281
- };
4489
+ for (const site of sites) {
4490
+ if (!IDENTIFIER_RE2.test(site.value)) {
4491
+ return {
4492
+ id: this.id,
4493
+ anchor: site.anchor,
4494
+ state: "non-conforming",
4495
+ verdict: `identifier "${site.value}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
4496
+ };
4497
+ }
4282
4498
  }
4283
- }
4284
- const { sites, unresolved } = findTsIdentifiers(tree);
4285
- if (unresolved.length > 0) {
4286
- throw new Error(
4287
- `DExt1 cannot read the identifier at ${unresolved.length} TS site(s): ${unresolved.join(", ")} \u2014 value is computed/imported/template, reverse-DNS unverifiable (Extension Rule 1). Fail-closed, not a silent present.`
4288
- );
4289
- }
4290
- for (const site of sites) {
4291
- if (!IDENTIFIER_RE2.test(site.value)) {
4499
+ const total = manifests.length + sites.length;
4500
+ if (total === 0) {
4292
4501
  return {
4293
4502
  id: this.id,
4294
- anchor: site.anchor,
4295
- state: "non-conforming",
4296
- verdict: `identifier "${site.value}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
4503
+ anchor: tree.root.split("/").pop() ?? tree.root,
4504
+ state: "absent",
4505
+ verdict: "no extension identifier found (no extension.json manifest, no TS `identifier:` literal) -> declare a reverse-DNS identifier (Extension Rule 1)"
4297
4506
  };
4298
4507
  }
4299
- }
4300
- const total = manifests.length + sites.length;
4301
- if (total === 0) {
4302
4508
  return {
4303
4509
  id: this.id,
4304
- anchor: tree.root.split("/").pop() ?? tree.root,
4305
- state: "absent",
4306
- verdict: "no extension identifier found (no extension.json manifest, no TS `identifier:` literal) -> declare a reverse-DNS identifier (Extension Rule 1)"
4510
+ anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
4511
+ state: "present",
4512
+ verdict: `${total} identifier(s) reverse-DNS conforming (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4307
4513
  };
4308
- }
4309
- return {
4310
- id: this.id,
4311
- anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
4312
- state: "present",
4313
- verdict: `${total} identifier(s) reverse-DNS conforming (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4314
- };
4514
+ })();
4515
+ return settle(this.id, decided, unreadable);
4315
4516
  }
4316
4517
  };
4317
4518
 
4318
4519
  // src/detectors/dExt2.ts
4319
- import { readFileSync as readFileSync11, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
4520
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
4320
4521
  import { join as join13 } from "path";
4321
4522
  var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
4322
4523
  var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
@@ -4416,76 +4617,85 @@ var dExt2 = {
4416
4617
  `DExt2 graceful-degradation control FAILED: accept(\`${GD_POSITIVE}\`)=${controls.gracefulDegradation.acceptsTrue} reject(\`${GD_NEGATIVE}\`)=${controls.gracefulDegradation.rejectsFalse} \u2014 matcher broken, verdict untrustworthy`
4417
4618
  );
4418
4619
  }
4419
- const manifests = findExtensionManifests2(tree.root);
4420
- for (const manifestPath2 of manifests) {
4421
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4422
- let parsed;
4423
- try {
4424
- parsed = JSON.parse(readFileSync11(manifestPath2, "utf8"));
4425
- } catch {
4426
- throw new Error(
4427
- `DExt2 could not parse ${shortPath} as JSON \u2014 a present manifest silent on graceful degradation cannot pass (Extension Rule 2). Fail-closed, not a silent present.`
4428
- );
4429
- }
4430
- if (!("graceful_degradation" in parsed)) {
4431
- throw new Error(
4432
- `DExt2 found a present manifest ${shortPath} with no "graceful_degradation" field \u2014 Extension Rule 2 requires it to be true. Fail-closed, not a silent present.`
4433
- );
4434
- }
4435
- const gdValue = parsed.graceful_degradation;
4436
- if (gdValue !== true) {
4437
- return {
4438
- id: this.id,
4439
- anchor: shortPath,
4440
- state: "non-conforming",
4441
- verdict: `manifest "graceful_degradation" is ${JSON.stringify(gdValue)}, not true (positive-control OK) -> set "graceful_degradation": true, an extension never breaks the host server (Extension Rule 2)`
4442
- };
4443
- }
4444
- }
4445
- const sites = findTsFactorySites(tree);
4446
- for (const site of sites) {
4447
- if (site.state === "unresolved") {
4448
- throw new Error(
4449
- `DExt2 cannot read the gracefulDegradation value at ${site.anchor} (got \`${site.value}\`) \u2014 computed/imported/template, graceful degradation unverifiable (Extension Rule 2). Fail-closed, not a silent present.`
4450
- );
4620
+ const unreadable = [];
4621
+ const decided = (() => {
4622
+ const manifests = findExtensionManifests2(tree.root);
4623
+ for (const manifestPath2 of manifests) {
4624
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
4625
+ const parsed = readJsonObject(manifestPath2);
4626
+ if (parsed === null) {
4627
+ unreadable.push({
4628
+ anchor: shortPath,
4629
+ reason: `could not parse ${shortPath} as JSON \u2014 a present manifest silent on graceful degradation cannot pass (Extension Rule 2)`
4630
+ });
4631
+ continue;
4632
+ }
4633
+ if (!("graceful_degradation" in parsed)) {
4634
+ return {
4635
+ id: this.id,
4636
+ anchor: shortPath,
4637
+ state: "non-conforming",
4638
+ verdict: `manifest ${shortPath} has no "graceful_degradation" field -> set "graceful_degradation": true, an extension never breaks the host server (Extension Rule 2)`
4639
+ };
4640
+ }
4641
+ const gdValue = parsed.graceful_degradation;
4642
+ if (gdValue !== true) {
4643
+ return {
4644
+ id: this.id,
4645
+ anchor: shortPath,
4646
+ state: "non-conforming",
4647
+ verdict: `manifest "graceful_degradation" is ${JSON.stringify(gdValue)}, not true (positive-control OK) -> set "graceful_degradation": true, an extension never breaks the host server (Extension Rule 2)`
4648
+ };
4649
+ }
4451
4650
  }
4452
- if (site.state === "false") {
4453
- return {
4454
- id: this.id,
4455
- anchor: site.anchor,
4456
- state: "non-conforming",
4457
- verdict: "gracefulDegradation is false, not true (positive-control OK) -> an extension never breaks the host server (Extension Rule 2)"
4458
- };
4651
+ const sites = findTsFactorySites(tree);
4652
+ for (const site of sites) {
4653
+ if (site.state === "unresolved") {
4654
+ unreadable.push({
4655
+ anchor: site.anchor,
4656
+ reason: `cannot read the gracefulDegradation value at ${site.anchor} (got \`${site.value}\`) \u2014 computed/imported/template, graceful degradation unverifiable (Extension Rule 2)`
4657
+ });
4658
+ continue;
4659
+ }
4660
+ if (site.state === "false") {
4661
+ return {
4662
+ id: this.id,
4663
+ anchor: site.anchor,
4664
+ state: "non-conforming",
4665
+ verdict: "gracefulDegradation is false, not true (positive-control OK) -> an extension never breaks the host server (Extension Rule 2)"
4666
+ };
4667
+ }
4668
+ if (site.state === "absent") {
4669
+ return {
4670
+ id: this.id,
4671
+ anchor: site.anchor,
4672
+ state: "non-conforming",
4673
+ verdict: "defineExtension block has no gracefulDegradation flag (positive-control OK) -> declare gracefulDegradation: true (Extension Rule 2)"
4674
+ };
4675
+ }
4459
4676
  }
4460
- if (site.state === "absent") {
4677
+ const total = manifests.length + sites.length;
4678
+ if (total === 0) {
4461
4679
  return {
4462
4680
  id: this.id,
4463
- anchor: site.anchor,
4464
- state: "non-conforming",
4465
- verdict: "defineExtension block has no gracefulDegradation flag (positive-control OK) -> declare gracefulDegradation: true (Extension Rule 2)"
4681
+ anchor: tree.root.split("/").pop() ?? tree.root,
4682
+ state: "absent",
4683
+ verdict: "no extension graceful-degradation flag found (no extension.json manifest, no defineExtension factory) -> declare graceful_degradation: true (Extension Rule 2)"
4466
4684
  };
4467
4685
  }
4468
- }
4469
- const total = manifests.length + sites.length;
4470
- if (total === 0) {
4471
4686
  return {
4472
4687
  id: this.id,
4473
- anchor: tree.root.split("/").pop() ?? tree.root,
4474
- state: "absent",
4475
- verdict: "no extension graceful-degradation flag found (no extension.json manifest, no defineExtension factory) -> declare graceful_degradation: true (Extension Rule 2)"
4688
+ anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
4689
+ state: "present",
4690
+ verdict: `${total} graceful-degradation flag(s) true (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4476
4691
  };
4477
- }
4478
- return {
4479
- id: this.id,
4480
- anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
4481
- state: "present",
4482
- verdict: `${total} graceful-degradation flag(s) true (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4483
- };
4692
+ })();
4693
+ return settle(this.id, decided, unreadable);
4484
4694
  }
4485
4695
  };
4486
4696
 
4487
4697
  // src/detectors/dExt3.ts
4488
- import { readFileSync as readFileSync12, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
4698
+ import { readdirSync as readdirSync6, statSync as statSync7 } from "fs";
4489
4699
  import { join as join14 } from "path";
4490
4700
  var TYPE_MATCHERS = [
4491
4701
  { type: "middleware", re: /\bmiddleware\b/ },
@@ -4599,100 +4809,118 @@ var dExt3 = {
4599
4809
  `DExt3 single-type control FAILED: detectMix(\`${TYPE_MIXED}\`)=${controls.type.detectsMix} detectSingle(\`${TYPE_SINGLE}\`)=${controls.type.detectsSingle} \u2014 matcher broken, verdict untrustworthy`
4600
4810
  );
4601
4811
  }
4602
- const declared = /* @__PURE__ */ new Set();
4603
- let firstAnchor2 = null;
4604
- const manifests = findExtensionManifests3(tree.root);
4605
- for (const manifestPath2 of manifests) {
4606
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4607
- let parsed;
4608
- try {
4609
- parsed = JSON.parse(readFileSync12(manifestPath2, "utf8"));
4610
- } catch {
4611
- throw new Error(
4612
- `DExt3 could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable type cannot pass (Extension Rule 3). Fail-closed, not a silent present.`
4613
- );
4614
- }
4615
- if (!("type" in parsed)) {
4616
- throw new Error(
4617
- `DExt3 found a present manifest ${shortPath} with no "type" field \u2014 Extension Rule 3 requires exactly one declared type. Fail-closed, not a silent present.`
4618
- );
4619
- }
4620
- const rawValue = coerceTypeValue(parsed.type);
4621
- if (rawValue === null) {
4622
- throw new Error(
4623
- `DExt3 found a present manifest ${shortPath} whose "type" is neither string nor string[] (got ${typeof parsed.type}) \u2014 type undeterminable (Extension Rule 3). Fail-closed, not a silent present.`
4624
- );
4625
- }
4626
- const found = typesIn(rawValue);
4627
- if (found.length === 0) {
4628
- throw new Error(
4629
- `DExt3 found a present manifest ${shortPath} whose "type" value ${JSON.stringify(parsed.type)} names no known category (middleware|tool-extension|transport) \u2014 type undeterminable (Extension Rule 3). Fail-closed, not a silent present.`
4630
- );
4812
+ const unreadable = [];
4813
+ const decided = (() => {
4814
+ const declared = /* @__PURE__ */ new Set();
4815
+ let firstAnchor2 = null;
4816
+ const manifests = findExtensionManifests3(tree.root);
4817
+ for (const manifestPath2 of manifests) {
4818
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
4819
+ const parsed = readJsonObject(manifestPath2);
4820
+ if (parsed === null) {
4821
+ unreadable.push({
4822
+ anchor: shortPath,
4823
+ reason: `could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable type cannot pass (Extension Rule 3)`
4824
+ });
4825
+ continue;
4826
+ }
4827
+ if (!("type" in parsed)) {
4828
+ return {
4829
+ id: this.id,
4830
+ anchor: shortPath,
4831
+ state: "non-conforming",
4832
+ verdict: `manifest ${shortPath} has no "type" field -> declare exactly one type (middleware|tool-extension|transport) (Extension Rule 3)`
4833
+ };
4834
+ }
4835
+ const rawValue = coerceTypeValue(parsed.type);
4836
+ if (rawValue === null) {
4837
+ return {
4838
+ id: this.id,
4839
+ anchor: shortPath,
4840
+ state: "non-conforming",
4841
+ verdict: `manifest ${shortPath} "type" is neither string nor string[] (got ${typeof parsed.type}) -> declare exactly one type (middleware|tool-extension|transport) (Extension Rule 3)`
4842
+ };
4843
+ }
4844
+ const found = typesIn(rawValue);
4845
+ if (found.length === 0) {
4846
+ return {
4847
+ id: this.id,
4848
+ anchor: shortPath,
4849
+ state: "non-conforming",
4850
+ verdict: `manifest ${shortPath} "type" value ${JSON.stringify(parsed.type)} names no known category (middleware|tool-extension|transport) -> declare exactly one known type (Extension Rule 3)`
4851
+ };
4852
+ }
4853
+ if (found.length > 1) {
4854
+ return {
4855
+ id: this.id,
4856
+ anchor: shortPath,
4857
+ state: "non-conforming",
4858
+ verdict: `manifest "type" ${JSON.stringify(parsed.type)} mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type, split into ${found.length} extensions (Extension Rule 3)`
4859
+ };
4860
+ }
4861
+ firstAnchor2 ??= shortPath;
4862
+ declared.add(found[0]);
4863
+ }
4864
+ const sites = findTsTypeSites(tree);
4865
+ for (const site of sites) {
4866
+ if (site.unresolved) {
4867
+ unreadable.push({
4868
+ anchor: site.anchor,
4869
+ reason: `cannot read the type at ${site.anchor}${site.value === void 0 ? " (no `type:` literal in the defineExtension block)" : ` (got \`${site.value}\`)`} \u2014 computed/imported/template or absent, single-type unverifiable (Extension Rule 3)`
4870
+ });
4871
+ continue;
4872
+ }
4873
+ const raw = site.value ?? "";
4874
+ const found = typesIn(raw);
4875
+ if (found.length === 0) {
4876
+ return {
4877
+ id: this.id,
4878
+ anchor: site.anchor,
4879
+ state: "non-conforming",
4880
+ verdict: `defineExtension type: "${raw}" at ${site.anchor} names no known category (middleware|tool-extension|transport) -> declare exactly one known type (Extension Rule 3)`
4881
+ };
4882
+ }
4883
+ if (found.length > 1) {
4884
+ return {
4885
+ id: this.id,
4886
+ anchor: site.anchor,
4887
+ state: "non-conforming",
4888
+ verdict: `type: "${raw}" mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
4889
+ };
4890
+ }
4891
+ firstAnchor2 ??= site.anchor;
4892
+ declared.add(found[0]);
4631
4893
  }
4632
- if (found.length > 1) {
4894
+ const total = manifests.length + sites.length;
4895
+ if (total === 0) {
4633
4896
  return {
4634
4897
  id: this.id,
4635
- anchor: shortPath,
4636
- state: "non-conforming",
4637
- verdict: `manifest "type" ${JSON.stringify(parsed.type)} mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type, split into ${found.length} extensions (Extension Rule 3)`
4898
+ anchor: tree.root.split("/").pop() ?? tree.root,
4899
+ state: "absent",
4900
+ verdict: "no extension type declared (no extension.json manifest, no defineExtension `type:` literal) -> declare exactly one type (Extension Rule 3)"
4638
4901
  };
4639
4902
  }
4640
- firstAnchor2 ??= shortPath;
4641
- declared.add(found[0]);
4642
- }
4643
- const sites = findTsTypeSites(tree);
4644
- for (const site of sites) {
4645
- if (site.unresolved) {
4646
- throw new Error(
4647
- `DExt3 cannot read the type at ${site.anchor}${site.value === void 0 ? " (no `type:` literal in the defineExtension block)" : ` (got \`${site.value}\`)`} \u2014 computed/imported/template or absent, single-type unverifiable (Extension Rule 3). Fail-closed, not a silent present.`
4648
- );
4649
- }
4650
- const raw = site.value ?? "";
4651
- const found = typesIn(raw);
4652
- if (found.length === 0) {
4653
- throw new Error(
4654
- `DExt3 found a defineExtension type: "${raw}" at ${site.anchor} naming no known category (middleware|tool-extension|transport) \u2014 type undeterminable (Extension Rule 3). Fail-closed, not a silent present.`
4655
- );
4656
- }
4657
- if (found.length > 1) {
4903
+ if (declared.size > 1) {
4658
4904
  return {
4659
4905
  id: this.id,
4660
- anchor: site.anchor,
4906
+ anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
4661
4907
  state: "non-conforming",
4662
- verdict: `type: "${raw}" mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
4908
+ verdict: `extension declares ${declared.size} types across its forms: ${[...declared].join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
4663
4909
  };
4664
4910
  }
4665
- firstAnchor2 ??= site.anchor;
4666
- declared.add(found[0]);
4667
- }
4668
- const total = manifests.length + sites.length;
4669
- if (total === 0) {
4670
- return {
4671
- id: this.id,
4672
- anchor: tree.root.split("/").pop() ?? tree.root,
4673
- state: "absent",
4674
- verdict: "no extension type declared (no extension.json manifest, no defineExtension `type:` literal) -> declare exactly one type (Extension Rule 3)"
4675
- };
4676
- }
4677
- if (declared.size > 1) {
4678
4911
  return {
4679
4912
  id: this.id,
4680
4913
  anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
4681
- state: "non-conforming",
4682
- verdict: `extension declares ${declared.size} types across its forms: ${[...declared].join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
4914
+ state: "present",
4915
+ verdict: `single type "${[...declared][0]}" declared across ${total} form(s) (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4683
4916
  };
4684
- }
4685
- return {
4686
- id: this.id,
4687
- anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
4688
- state: "present",
4689
- verdict: `single type "${[...declared][0]}" declared across ${total} form(s) (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4690
- };
4917
+ })();
4918
+ return settle(this.id, decided, unreadable);
4691
4919
  }
4692
4920
  };
4693
4921
 
4694
4922
  // src/detectors/dExt4.ts
4695
- import { readFileSync as readFileSync13, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
4923
+ import { readdirSync as readdirSync7, statSync as statSync8 } from "fs";
4696
4924
  import { join as join15 } from "path";
4697
4925
  function canonical(name) {
4698
4926
  return name.replace(/[-_]/g, "").toLowerCase();
@@ -4836,7 +5064,7 @@ function findFactorySites(tree) {
4836
5064
  function manifestHookTokens(raw) {
4837
5065
  const set = /* @__PURE__ */ new Set();
4838
5066
  if (!Array.isArray(raw)) {
4839
- throw new Error("not-array");
5067
+ return null;
4840
5068
  }
4841
5069
  for (const h of raw) {
4842
5070
  if (typeof h === "string") set.add(canonical(h));
@@ -4852,82 +5080,90 @@ var dExt4 = {
4852
5080
  `DExt4 lifecycle-hooks control FAILED: unify(${HOOK_SNAKE}/${HOOK_KEBAB}/${HOOK_CAMEL})=${controls.hooks.unifiesSpellings} distinct(${HOOK_SNAKE}!=${HOOK_OTHER})=${controls.hooks.keepsDistinct} \u2014 normaliser broken, verdict untrustworthy`
4853
5081
  );
4854
5082
  }
4855
- const hooksDirs = findHooksDirs(tree.root);
4856
- const { sites, unresolved } = findFactorySites(tree);
4857
- if (unresolved.length > 0) {
4858
- throw new Error(
4859
- `DExt4 cannot read the lifecycle implementation at ${unresolved.length} TS site(s): ${unresolved.join(", ")} \u2014 value is computed/imported/template, not an object literal, so the implemented hooks are unverifiable (Extension Rule 4). Fail-closed, not a silent present.`
4860
- );
4861
- }
4862
- const implemented = /* @__PURE__ */ new Set();
4863
- for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
4864
- for (const s of sites) for (const t of s.tokens) implemented.add(t);
4865
- const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
4866
- const manifests = findExtensionManifests4(tree.root);
4867
- let declaredAnyLifecycle = false;
4868
- for (const manifestPath2 of manifests) {
4869
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4870
- let parsed;
4871
- try {
4872
- parsed = JSON.parse(readFileSync13(manifestPath2, "utf8"));
4873
- } catch {
4874
- throw new Error(
4875
- `DExt4 could not parse ${shortPath} as JSON \u2014 a present manifest whose lifecycle cannot be read cannot pass (Extension Rule 4). Fail-closed, not a silent present.`
4876
- );
4877
- }
4878
- if (!("lifecycle" in parsed)) continue;
4879
- let declared;
4880
- try {
4881
- declared = manifestHookTokens(parsed.lifecycle);
4882
- } catch {
4883
- throw new Error(
4884
- `DExt4 found a present manifest ${shortPath} whose "lifecycle" is not an array (got ${typeof parsed.lifecycle}) \u2014 the declared hooks are undeterminable (Extension Rule 4). Fail-closed, not a silent present.`
4885
- );
4886
- }
4887
- if (declared.size === 0) continue;
4888
- declaredAnyLifecycle = true;
4889
- if (!implSurfacePresent) {
4890
- throw new Error(
4891
- `DExt4 found a present manifest ${shortPath} declaring lifecycle hooks while NO implementation surface exists (no hooks/ directory, no factory lifecycle object) \u2014 the parity is undeterminable (Extension Rule 4). Fail-closed, not a silent present.`
4892
- );
5083
+ const unreadable = [];
5084
+ const decided = (() => {
5085
+ const hooksDirs = findHooksDirs(tree.root);
5086
+ const { sites, unresolved } = findFactorySites(tree);
5087
+ for (const anchor of unresolved) {
5088
+ unreadable.push({
5089
+ anchor,
5090
+ reason: `cannot read the lifecycle implementation at ${anchor} \u2014 value is computed/imported/template, not an object literal, so the implemented hooks are unverifiable (Extension Rule 4)`
5091
+ });
4893
5092
  }
4894
- const phantom = [...declared].filter((h) => !implemented.has(h));
4895
- if (phantom.length > 0) {
4896
- return {
4897
- id: this.id,
4898
- anchor: shortPath,
4899
- state: "non-conforming",
4900
- verdict: `manifest "lifecycle" declares hook(s) [${phantom.join(", ")}] with no implementation (implemented: [${[...implemented].join(", ") || "none"}], positive-control OK) -> a declared hook without implementation is a phantom hook (Extension Rule 4)`
4901
- };
5093
+ const implemented = /* @__PURE__ */ new Set();
5094
+ for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
5095
+ for (const s of sites) for (const t of s.tokens) implemented.add(t);
5096
+ const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
5097
+ const manifests = findExtensionManifests4(tree.root);
5098
+ let declaredAnyLifecycle = false;
5099
+ for (const manifestPath2 of manifests) {
5100
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
5101
+ const parsed = readJsonObject(manifestPath2);
5102
+ if (parsed === null) {
5103
+ unreadable.push({
5104
+ anchor: shortPath,
5105
+ reason: `could not parse ${shortPath} as JSON \u2014 a present manifest whose lifecycle cannot be read cannot pass (Extension Rule 4)`
5106
+ });
5107
+ continue;
5108
+ }
5109
+ if (!("lifecycle" in parsed)) continue;
5110
+ const declared = manifestHookTokens(parsed.lifecycle);
5111
+ if (declared === null) {
5112
+ return {
5113
+ id: this.id,
5114
+ anchor: shortPath,
5115
+ state: "non-conforming",
5116
+ verdict: `manifest ${shortPath} "lifecycle" is not an array (got ${typeof parsed.lifecycle}) -> declare the hooks as an array of hook names (Extension Rule 4)`
5117
+ };
5118
+ }
5119
+ if (declared.size === 0) continue;
5120
+ declaredAnyLifecycle = true;
5121
+ if (!implSurfacePresent) {
5122
+ unreadable.push({
5123
+ anchor: shortPath,
5124
+ reason: `present manifest ${shortPath} declares lifecycle hooks while NO implementation surface exists (no hooks/ directory, no factory lifecycle object) \u2014 the parity is undeterminable (Extension Rule 4)`
5125
+ });
5126
+ continue;
5127
+ }
5128
+ const phantom = [...declared].filter((h) => !implemented.has(h));
5129
+ if (phantom.length > 0) {
5130
+ return {
5131
+ id: this.id,
5132
+ anchor: shortPath,
5133
+ state: "non-conforming",
5134
+ verdict: `manifest "lifecycle" declares hook(s) [${phantom.join(", ")}] with no implementation (implemented: [${[...implemented].join(", ") || "none"}], positive-control OK) -> a declared hook without implementation is a phantom hook (Extension Rule 4)`
5135
+ };
5136
+ }
4902
5137
  }
4903
- }
4904
- if (!declaredAnyLifecycle) {
4905
- if (manifests.length === 0 && !implSurfacePresent) {
5138
+ if (!declaredAnyLifecycle) {
5139
+ if (manifests.length === 0 && !implSurfacePresent) {
5140
+ return {
5141
+ id: this.id,
5142
+ anchor: tree.root.split("/").pop() ?? tree.root,
5143
+ state: "absent",
5144
+ verdict: "no lifecycle hooks declared (no extension.json manifest `lifecycle` array) -> declare the hooks the extension implements (Extension Rule 4)"
5145
+ };
5146
+ }
4906
5147
  return {
4907
5148
  id: this.id,
4908
- anchor: tree.root.split("/").pop() ?? tree.root,
4909
- state: "absent",
4910
- verdict: "no lifecycle hooks declared (no extension.json manifest `lifecycle` array) -> declare the hooks the extension implements (Extension Rule 4)"
5149
+ anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
5150
+ state: "present",
5151
+ verdict: `no manifest lifecycle hooks declared, nothing to reconcile (implemented: [${[...implemented].join(", ") || "none"}], positive-control OK)`
4911
5152
  };
4912
5153
  }
4913
5154
  return {
4914
5155
  id: this.id,
4915
- anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
5156
+ anchor: manifests[0].split("/").slice(-2).join("/"),
4916
5157
  state: "present",
4917
- verdict: `no manifest lifecycle hooks declared, nothing to reconcile (implemented: [${[...implemented].join(", ") || "none"}], positive-control OK)`
5158
+ verdict: `every declared lifecycle hook is implemented (declared \u2286 implemented: [${[...implemented].join(", ")}], hooksDir=${hooksDirs.length}, factory=${sites.length}, positive-control OK)`
4918
5159
  };
4919
- }
4920
- return {
4921
- id: this.id,
4922
- anchor: manifests[0].split("/").slice(-2).join("/"),
4923
- state: "present",
4924
- verdict: `every declared lifecycle hook is implemented (declared \u2286 implemented: [${[...implemented].join(", ")}], hooksDir=${hooksDirs.length}, factory=${sites.length}, positive-control OK)`
4925
- };
5160
+ })();
5161
+ return settle(this.id, decided, unreadable);
4926
5162
  }
4927
5163
  };
4928
5164
 
4929
5165
  // src/detectors/dExt5.ts
4930
- import { existsSync as existsSync12, readFileSync as readFileSync14, readdirSync as readdirSync8, statSync as statSync9 } from "fs";
5166
+ import { existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync9 } from "fs";
4931
5167
  import { dirname as dirname7, join as join16 } from "path";
4932
5168
  function nonEmptyString(v) {
4933
5169
  return typeof v === "string" && v.trim().length > 0;
@@ -5008,10 +5244,8 @@ var dExt5 = {
5008
5244
  reason,
5009
5245
  verdict: `could not judge: ${reason}`
5010
5246
  });
5011
- let parsed;
5012
- try {
5013
- parsed = JSON.parse(readFileSync14(manifestPath2, "utf8"));
5014
- } catch {
5247
+ const parsed = readJsonObject(manifestPath2);
5248
+ if (parsed === null) {
5015
5249
  return cannotRead(
5016
5250
  `${shortPath} does not parse as JSON \u2014 the FR surface of a present manifest cannot be read (Extension Rule 5)`
5017
5251
  );
@@ -5068,7 +5302,7 @@ var dExt5 = {
5068
5302
  };
5069
5303
 
5070
5304
  // src/detectors/dExt7.ts
5071
- import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync9, statSync as statSync10 } from "fs";
5305
+ import { existsSync as existsSync13, readdirSync as readdirSync9, statSync as statSync10 } from "fs";
5072
5306
  import { dirname as dirname8, join as join17 } from "path";
5073
5307
  var ABSENCE_FLAG_FIELDS = [
5074
5308
  "extensionAbsent",
@@ -5165,65 +5399,78 @@ var dExt7 = {
5165
5399
  verdict: "no extension.json manifest found -> declare an extension with an eval suite (>=3 cases incl. 1 'extension absente') (Extension Rule 7)"
5166
5400
  };
5167
5401
  }
5168
- for (const manifestPath2 of manifests) {
5169
- const extDir = dirname8(manifestPath2);
5170
- const extLabel = extDir.split("/").pop() ?? extDir;
5171
- const evalsPath = join17(extDir, "evals", "evals.json");
5172
- const shortPath = `${extLabel}/evals/evals.json`;
5173
- if (!existsSync13(evalsPath)) {
5174
- return {
5175
- id: this.id,
5176
- anchor: `${extLabel}/extension.json`,
5177
- state: "non-conforming",
5178
- verdict: `no evals/evals.json for extension "${extLabel}" -> add an eval suite with >=3 cases incl. 1 'extension absente' (Extension Rule 7)`
5179
- };
5180
- }
5181
- let cases;
5182
- try {
5183
- const raw = readFileSync15(evalsPath, "utf8");
5184
- const parsed = JSON.parse(raw);
5402
+ const unreadable = [];
5403
+ const decided = (() => {
5404
+ for (const manifestPath2 of manifests) {
5405
+ const extDir = dirname8(manifestPath2);
5406
+ const extLabel = extDir.split("/").pop() ?? extDir;
5407
+ const evalsPath = join17(extDir, "evals", "evals.json");
5408
+ const shortPath = `${extLabel}/evals/evals.json`;
5409
+ if (!existsSync13(evalsPath)) {
5410
+ return {
5411
+ id: this.id,
5412
+ anchor: `${extLabel}/extension.json`,
5413
+ state: "non-conforming",
5414
+ verdict: `no evals/evals.json for extension "${extLabel}" -> add an eval suite with >=3 cases incl. 1 'extension absente' (Extension Rule 7)`
5415
+ };
5416
+ }
5417
+ const parsed = readJsonObject(evalsPath);
5418
+ if (parsed === null) {
5419
+ unreadable.push({
5420
+ anchor: shortPath,
5421
+ reason: `could not parse ${shortPath}: not readable as a JSON object \u2014 a present eval surface that cannot be read cannot pass (Extension Rule 7)`
5422
+ });
5423
+ continue;
5424
+ }
5185
5425
  if (!Array.isArray(parsed.cases)) {
5186
- throw new Error("`cases` is not an array");
5426
+ return {
5427
+ id: this.id,
5428
+ anchor: shortPath,
5429
+ state: "non-conforming",
5430
+ verdict: `${shortPath} "cases" is not an array (got ${typeof parsed.cases}) -> declare cases as an array of eval cases (Extension Rule 7)`
5431
+ };
5187
5432
  }
5188
- cases = parsed.cases;
5189
- } catch (err) {
5190
- throw new Error(
5191
- `DExt7 could not parse ${shortPath}: ${err instanceof Error ? err.message : String(err)} \u2014 a present eval surface that cannot be read cannot pass (Extension Rule 7). Fail-closed, not a silent present.`
5433
+ const cases = parsed.cases;
5434
+ const badCase = cases.findIndex(
5435
+ (c) => c === null || typeof c !== "object" || Array.isArray(c)
5192
5436
  );
5193
- }
5194
- cases.forEach((c, i) => {
5195
- if (c === null || typeof c !== "object" || Array.isArray(c)) {
5196
- throw new Error(
5197
- `DExt7 found ${shortPath} cases[${i}] is not an object (got ${c === null ? "null" : Array.isArray(c) ? "array" : typeof c}) \u2014 case shape undeterminable, absence-case presence cannot be decided (Extension Rule 7). Fail-closed, not a silent present.`
5198
- );
5437
+ if (badCase !== -1) {
5438
+ const c = cases[badCase];
5439
+ return {
5440
+ id: this.id,
5441
+ anchor: shortPath,
5442
+ state: "non-conforming",
5443
+ verdict: `${shortPath} cases[${badCase}] is not an object (got ${c === null ? "null" : Array.isArray(c) ? "array" : typeof c}) -> every eval case must be an object (Extension Rule 7)`
5444
+ };
5445
+ }
5446
+ const count = cases.length;
5447
+ const hasAbsence = cases.some(isAbsenceCase);
5448
+ const failures = [];
5449
+ if (count < 3) failures.push(`only ${count} case(s) (need >=3)`);
5450
+ if (!hasAbsence) failures.push("no 'extension absente'/degradation case");
5451
+ if (failures.length > 0) {
5452
+ return {
5453
+ id: this.id,
5454
+ anchor: `${shortPath}`,
5455
+ state: "non-conforming",
5456
+ verdict: `eval suite for "${extLabel}": ${failures.join("; ")} (positive-control OK) -> Extension Rule 7 needs >=3 cases incl. 1 'extension absente'`
5457
+ };
5199
5458
  }
5200
- });
5201
- const count = cases.length;
5202
- const hasAbsence = cases.some(isAbsenceCase);
5203
- const failures = [];
5204
- if (count < 3) failures.push(`only ${count} case(s) (need >=3)`);
5205
- if (!hasAbsence) failures.push("no 'extension absente'/degradation case");
5206
- if (failures.length > 0) {
5207
- return {
5208
- id: this.id,
5209
- anchor: `${shortPath}`,
5210
- state: "non-conforming",
5211
- verdict: `eval suite for "${extLabel}": ${failures.join("; ")} (positive-control OK) -> Extension Rule 7 needs >=3 cases incl. 1 'extension absente'`
5212
- };
5213
5459
  }
5214
- }
5215
- const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
5216
- return {
5217
- id: this.id,
5218
- anchor: `${firstLabel}/evals/evals.json`,
5219
- state: "present",
5220
- verdict: `eval suite complete across ${manifests.length} extension(s): >=3 cases incl. an 'extension absente'/degradation case (positive-control OK)`
5221
- };
5460
+ const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
5461
+ return {
5462
+ id: this.id,
5463
+ anchor: `${firstLabel}/evals/evals.json`,
5464
+ state: "present",
5465
+ verdict: `eval suite complete across ${manifests.length} extension(s): >=3 cases incl. an 'extension absente'/degradation case (positive-control OK)`
5466
+ };
5467
+ })();
5468
+ return settle(this.id, decided, unreadable);
5222
5469
  }
5223
5470
  };
5224
5471
 
5225
5472
  // src/detectors/dExt8.ts
5226
- import { existsSync as existsSync14, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync11 } from "fs";
5473
+ import { existsSync as existsSync14, readdirSync as readdirSync10, statSync as statSync11 } from "fs";
5227
5474
  import { dirname as dirname9, join as join18 } from "path";
5228
5475
  var SEMVER_RE2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
5229
5476
  var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
@@ -5287,73 +5534,88 @@ var dExt8 = {
5287
5534
  verdict: "no extension.json manifest found -> declare an extension with a valid SemVer version + CHANGELOG.md (Extension Rule 8)"
5288
5535
  };
5289
5536
  }
5537
+ const unreadable = [];
5538
+ let finding;
5539
+ let judgedConforming = 0;
5290
5540
  for (const manifestPath2 of manifests) {
5291
5541
  const extDir = dirname9(manifestPath2);
5292
5542
  const extLabel = extDir.split("/").pop() ?? extDir;
5293
5543
  const pkgPath = join18(extDir, "package.json");
5294
5544
  const shortPkgPath = `${extLabel}/package.json`;
5295
5545
  if (!existsSync14(pkgPath)) {
5296
- throw new Error(
5297
- `DExt8 could not locate ${shortPkgPath} for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8). Fail-closed, not a silent present.`
5298
- );
5546
+ unreadable.push({
5547
+ anchor: shortPkgPath,
5548
+ reason: `could not locate ${shortPkgPath} for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8)`
5549
+ });
5550
+ continue;
5299
5551
  }
5300
- let pkgJson;
5301
- try {
5302
- pkgJson = JSON.parse(readFileSync16(pkgPath, "utf8"));
5303
- } catch {
5304
- throw new Error(
5305
- `DExt8 could not parse ${shortPkgPath} as JSON for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8). Fail-closed, not a silent present.`
5306
- );
5552
+ const pkgJson = readJsonObject(pkgPath);
5553
+ if (pkgJson === null) {
5554
+ unreadable.push({
5555
+ anchor: shortPkgPath,
5556
+ reason: `could not parse ${shortPkgPath} as JSON for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8)`
5557
+ });
5558
+ continue;
5307
5559
  }
5308
5560
  const version = pkgJson.version;
5309
5561
  if ("version" in pkgJson && typeof version !== "string") {
5310
- throw new Error(
5311
- `DExt8 found ${shortPkgPath} whose "version" is not a string (got ${typeof version}) for extension "${extLabel}" \u2014 cannot verify SemVer (Extension Rule 8). Fail-closed, not a silent present.`
5312
- );
5562
+ finding ??= {
5563
+ id: this.id,
5564
+ anchor: shortPkgPath,
5565
+ state: "non-conforming",
5566
+ verdict: `"version" in ${shortPkgPath} is not a string (got ${typeof version}) for extension "${extLabel}" -> set a valid SemVer version string (Extension Rule 8)`
5567
+ };
5568
+ continue;
5313
5569
  }
5314
5570
  if (typeof version !== "string" || version.trim().length === 0) {
5315
- return {
5571
+ finding ??= {
5316
5572
  id: this.id,
5317
5573
  anchor: shortPkgPath,
5318
5574
  state: "non-conforming",
5319
5575
  verdict: `no "version" field in ${shortPkgPath} for extension "${extLabel}" -> set a valid SemVer version (Extension Rule 8)`
5320
5576
  };
5577
+ continue;
5321
5578
  }
5322
5579
  const validSemver = SEMVER_RE2.test(version);
5323
5580
  const changelogPath = join18(extDir, "CHANGELOG.md");
5324
5581
  const hasChangelog = existsSync14(changelogPath);
5325
5582
  if (!validSemver) {
5326
- return {
5583
+ finding ??= {
5327
5584
  id: this.id,
5328
5585
  anchor: shortPkgPath,
5329
5586
  state: "non-conforming",
5330
5587
  verdict: `"version": "${version}" is not valid SemVer for extension "${extLabel}" (positive-control OK) -> use MAJOR.MINOR.PATCH (Extension Rule 8)`
5331
5588
  };
5589
+ continue;
5332
5590
  }
5333
5591
  if (!hasChangelog) {
5334
- return {
5592
+ finding ??= {
5335
5593
  id: this.id,
5336
5594
  anchor: shortPkgPath,
5337
5595
  state: "absent",
5338
5596
  verdict: `no CHANGELOG.md next to ${shortPkgPath} for extension "${extLabel}" (positive-control OK) -> add CHANGELOG.md, update it each release (Extension Rule 8)`
5339
5597
  };
5598
+ continue;
5340
5599
  }
5600
+ judgedConforming++;
5341
5601
  }
5602
+ if (finding) return settle(this.id, finding, unreadable);
5342
5603
  const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
5343
- return {
5604
+ const conforming = {
5344
5605
  id: this.id,
5345
5606
  anchor: `${firstLabel}/package.json`,
5346
5607
  state: "present",
5347
- verdict: `SemVer valid + CHANGELOG.md present across ${manifests.length} extension(s) (positive-control OK)`
5608
+ verdict: `SemVer valid + CHANGELOG.md present across ${judgedConforming} of ${manifests.length} extension(s) (positive-control OK)`
5348
5609
  };
5610
+ return settle(this.id, conforming, unreadable);
5349
5611
  }
5350
5612
  };
5351
5613
 
5352
5614
  // src/detectors/dExt9.ts
5353
- import { readFileSync as readFileSync17, readdirSync as readdirSync11, statSync as statSync12 } from "fs";
5615
+ import { readFileSync as readFileSync10, readdirSync as readdirSync11, statSync as statSync12 } from "fs";
5354
5616
  import { join as join19 } from "path";
5355
5617
  var SKIP_DIRS9 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5356
- function collectHooksDirSources(root) {
5618
+ function collectHooksDirSources(root, unreadable) {
5357
5619
  const out = [];
5358
5620
  let base;
5359
5621
  try {
@@ -5392,11 +5654,14 @@ function collectHooksDirSources(root) {
5392
5654
  const filePath = join19(full, f);
5393
5655
  let body;
5394
5656
  try {
5395
- body = readFileSync17(filePath, "utf8");
5657
+ body = readFileSync10(filePath, "utf8");
5396
5658
  } catch {
5397
- throw new Error(
5398
- `DExt9 could not read hook implementation ${filePath.split("/").slice(-2).join("/")} \u2014 timeout guard undeterminable for hook "${stem}" (Extension Rule 9). Fail-closed, not a silent absent.`
5399
- );
5659
+ const shortPath = filePath.split("/").slice(-2).join("/");
5660
+ unreadable.push({
5661
+ anchor: shortPath,
5662
+ reason: `could not read hook implementation ${shortPath} \u2014 timeout guard undeterminable for hook "${stem}" (Extension Rule 9)`
5663
+ });
5664
+ continue;
5400
5665
  }
5401
5666
  out.push({
5402
5667
  token: canonical(stem),
@@ -5466,13 +5731,11 @@ function dExt9PositiveControls() {
5466
5731
  negative: hasImplTimeoutGuard(CTRL_NEGATIVE_BODY)
5467
5732
  };
5468
5733
  }
5469
- function manifestTimeoutTokens(raw, shortPath) {
5734
+ function manifestTimeoutTokens(raw) {
5470
5735
  const map = /* @__PURE__ */ new Map();
5471
5736
  if (raw === void 0) return map;
5472
5737
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
5473
- throw new Error(
5474
- `DExt9 found ${shortPath} whose "timeouts" field is present but not an object (got ${Array.isArray(raw) ? "array" : typeof raw}) \u2014 declared timeout guards are undeterminable (Extension Rule 9). Fail-closed, not a silent absent.`
5475
- );
5738
+ return null;
5476
5739
  }
5477
5740
  for (const [key, value] of Object.entries(raw)) {
5478
5741
  if (typeof value === "number" && value > 0) {
@@ -5490,67 +5753,83 @@ var dExt9 = {
5490
5753
  `DExt9 hook-timeout control FAILED: setTimeoutForm=${controls.setTimeoutForm} abortForm=${controls.abortForm} negative=${controls.negative} \u2014 impl-pattern matcher broken, verdict untrustworthy`
5491
5754
  );
5492
5755
  }
5493
- const hookSources = [...collectHooksDirSources(tree.root), ...collectFactorySources(tree)];
5494
- const implByToken = /* @__PURE__ */ new Map();
5495
- for (const s of hookSources) {
5496
- const list = implByToken.get(s.token) ?? [];
5497
- list.push(s);
5498
- implByToken.set(s.token, list);
5499
- }
5500
- const manifests = findExtensionManifests4(tree.root);
5501
- let declaredAnyLifecycle = false;
5502
- for (const manifestPath2 of manifests) {
5503
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
5504
- let parsed;
5505
- try {
5506
- parsed = JSON.parse(readFileSync17(manifestPath2, "utf8"));
5507
- } catch {
5508
- throw new Error(
5509
- `DExt9 could not parse ${shortPath} as JSON \u2014 timeout guards undeterminable (Extension Rule 9). Fail-closed, not a silent absent.`
5510
- );
5511
- }
5512
- if (!("lifecycle" in parsed)) continue;
5513
- let declared;
5514
- try {
5515
- declared = manifestHookTokens(parsed.lifecycle);
5516
- } catch {
5517
- throw new Error(
5518
- `DExt9 found a present manifest ${shortPath} whose "lifecycle" is not an array (got ${typeof parsed.lifecycle}) \u2014 declared hooks are undeterminable (Extension Rule 9). Fail-closed, not a silent absent.`
5519
- );
5756
+ const unreadable = [];
5757
+ const decided = (() => {
5758
+ const hookSources = [
5759
+ ...collectHooksDirSources(tree.root, unreadable),
5760
+ ...collectFactorySources(tree)
5761
+ ];
5762
+ const implByToken = /* @__PURE__ */ new Map();
5763
+ for (const s of hookSources) {
5764
+ const list = implByToken.get(s.token) ?? [];
5765
+ list.push(s);
5766
+ implByToken.set(s.token, list);
5767
+ }
5768
+ const manifests = findExtensionManifests4(tree.root);
5769
+ let declaredAnyLifecycle = false;
5770
+ for (const manifestPath2 of manifests) {
5771
+ const shortPath = manifestPath2.split("/").slice(-2).join("/");
5772
+ const parsed = readJsonObject(manifestPath2);
5773
+ if (parsed === null) {
5774
+ unreadable.push({
5775
+ anchor: shortPath,
5776
+ reason: `could not parse ${shortPath} as JSON \u2014 timeout guards undeterminable (Extension Rule 9)`
5777
+ });
5778
+ continue;
5779
+ }
5780
+ if (!("lifecycle" in parsed)) continue;
5781
+ const declared = manifestHookTokens(parsed.lifecycle);
5782
+ if (declared === null) {
5783
+ return {
5784
+ id: this.id,
5785
+ anchor: shortPath,
5786
+ state: "non-conforming",
5787
+ verdict: `manifest ${shortPath} "lifecycle" is not an array (got ${typeof parsed.lifecycle}) -> declare the hooks as an array of hook names (Extension Rule 9)`
5788
+ };
5789
+ }
5790
+ if (declared.size === 0) continue;
5791
+ declaredAnyLifecycle = true;
5792
+ const timeouts = manifestTimeoutTokens(parsed.timeouts);
5793
+ if (timeouts === null) {
5794
+ return {
5795
+ id: this.id,
5796
+ anchor: shortPath,
5797
+ state: "non-conforming",
5798
+ verdict: `${shortPath} "timeouts" is present but not an object (got ${Array.isArray(parsed.timeouts) ? "array" : typeof parsed.timeouts}) -> declare timeouts as a { <hook>: <positive ms> } object (Extension Rule 9)`
5799
+ };
5800
+ }
5801
+ const noTimeout = [];
5802
+ for (const hook of declared) {
5803
+ const hasManifestTimeout = timeouts.has(hook);
5804
+ const implSources = implByToken.get(hook) ?? [];
5805
+ const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
5806
+ if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
5807
+ }
5808
+ if (noTimeout.length > 0) {
5809
+ return {
5810
+ id: this.id,
5811
+ anchor: shortPath,
5812
+ state: "non-conforming",
5813
+ verdict: `hook(s) [${noTimeout.join(", ")}] declare NO timeout guard in either form (manifest "timeouts.<hook>" positive number, or impl-side setTimeout/AbortController cutoff) -> a hook exceeding its timeout must be skipped, not a server blocker (Extension Rule 9)`
5814
+ };
5815
+ }
5520
5816
  }
5521
- if (declared.size === 0) continue;
5522
- declaredAnyLifecycle = true;
5523
- const timeouts = manifestTimeoutTokens(parsed.timeouts, shortPath);
5524
- const noTimeout = [];
5525
- for (const hook of declared) {
5526
- const hasManifestTimeout = timeouts.has(hook);
5527
- const implSources = implByToken.get(hook) ?? [];
5528
- const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
5529
- if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
5530
- }
5531
- if (noTimeout.length > 0) {
5817
+ if (!declaredAnyLifecycle) {
5532
5818
  return {
5533
5819
  id: this.id,
5534
- anchor: shortPath,
5535
- state: "non-conforming",
5536
- verdict: `hook(s) [${noTimeout.join(", ")}] declare NO timeout guard in either form (manifest "timeouts.<hook>" positive number, or impl-side setTimeout/AbortController cutoff) -> a hook exceeding its timeout must be skipped, not a server blocker (Extension Rule 9)`
5820
+ anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
5821
+ state: "absent",
5822
+ verdict: "no lifecycle hooks declared, nothing to guard with a timeout (Extension Rule 9)"
5537
5823
  };
5538
5824
  }
5539
- }
5540
- if (!declaredAnyLifecycle) {
5541
5825
  return {
5542
5826
  id: this.id,
5543
- anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
5544
- state: "absent",
5545
- verdict: "no lifecycle hooks declared, nothing to guard with a timeout (Extension Rule 9)"
5827
+ anchor: manifests[0].split("/").slice(-2).join("/"),
5828
+ state: "present",
5829
+ verdict: "every declared lifecycle hook has a timeout guard (manifest timeouts.<hook>, or impl-side cutoff), positive-control OK (Extension Rule 9)"
5546
5830
  };
5547
- }
5548
- return {
5549
- id: this.id,
5550
- anchor: manifests[0].split("/").slice(-2).join("/"),
5551
- state: "present",
5552
- verdict: "every declared lifecycle hook has a timeout guard (manifest timeouts.<hook>, or impl-side cutoff), positive-control OK (Extension Rule 9)"
5553
- };
5831
+ })();
5832
+ return settle(this.id, decided, unreadable);
5554
5833
  }
5555
5834
  };
5556
5835
 
@@ -5579,7 +5858,7 @@ function unscopePackageName(name) {
5579
5858
  const m = name.match(/^@[^/]+\/(.+)$/);
5580
5859
  return m ? m[1] : name;
5581
5860
  }
5582
- var dServer2 = {
5861
+ var dServer2Inner = {
5583
5862
  id: "DServer2 naming",
5584
5863
  rule: "C-2",
5585
5864
  pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
@@ -5607,6 +5886,22 @@ var dServer2 = {
5607
5886
  };
5608
5887
  }
5609
5888
  const tools = findRegisteredTools(tree);
5889
+ if (tools.length === 0) {
5890
+ const surface = toolSurfaceAnchor(tree);
5891
+ if (surface) {
5892
+ const reason = noToolsLocatedReason(
5893
+ tree,
5894
+ "snake_case tool naming is unverifiable (Server Rule 2)"
5895
+ );
5896
+ return {
5897
+ id: this.id,
5898
+ anchor: surface,
5899
+ state: "could-not-judge",
5900
+ reason,
5901
+ verdict: `could not judge: ${reason}`
5902
+ };
5903
+ }
5904
+ }
5610
5905
  for (const t of tools) {
5611
5906
  if (!TOOL_SNAKE_RE.test(t.name)) {
5612
5907
  return {
@@ -5710,6 +6005,9 @@ var dServer2 = {
5710
6005
  };
5711
6006
  }
5712
6007
  };
6008
+ var dServer2 = guardUnreadablePackageJson(dServer2Inner, {
6009
+ authoritative: false
6010
+ });
5713
6011
 
5714
6012
  // src/detectors/dServer6.ts
5715
6013
  var LOG_OPENER_RE = /\b(?:console|logger|log)\.(?:log|info|warn|error|debug|trace)\s*\(/;
@@ -5820,65 +6118,72 @@ var dServer6 = {
5820
6118
  `DServer6 stack control FAILED: /${controls.stack.pattern}/ accept(\`${STACK_POSITIVE}\`)=${controls.stack.positive} reject(\`${STACK_NEGATIVE}\`)=${!controls.stack.negative} \u2014 matcher broken, verdict untrustworthy`
5821
6119
  );
5822
6120
  }
5823
- for (const file of tree.files) {
5824
- const { calls, unbalanced } = collectLogCalls(file);
5825
- if (unbalanced.length > 0) {
5826
- throw new Error(
5827
- `DServer6 could not read the argument list of ${unbalanced.length} log call(s): ${unbalanced.join(", ")} \u2014 parentheses never balance, secret/PII content unverifiable (C-6). Fail-closed, not a silent present.`
5828
- );
5829
- }
5830
- for (const call of calls) {
5831
- if (SPREAD_RE.test(call.argText)) {
5832
- throw new Error(
5833
- `DServer6 cannot inspect the spread argument of the log call at ${call.anchor} \u2014 a spread hides arbitrary content, secret/PII presence unverifiable (C-6). Fail-closed, not a silent present.`
5834
- );
5835
- }
5836
- const refs = referencedIdentifiers(call.argText);
5837
- if (SECRET_RE.test(refs)) {
5838
- return {
5839
- id: this.id,
5840
- anchor: call.anchor,
5841
- state: "non-conforming",
5842
- verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (C-6)"
5843
- };
5844
- }
5845
- if (PII_RE.test(refs)) {
5846
- return {
5847
- id: this.id,
5848
- anchor: call.anchor,
5849
- state: "non-conforming",
5850
- verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
5851
- };
6121
+ const unreadable = [];
6122
+ const decided = (() => {
6123
+ for (const file of tree.files) {
6124
+ const { calls, unbalanced } = collectLogCalls(file);
6125
+ for (const anchor of unbalanced) {
6126
+ unreadable.push({
6127
+ anchor,
6128
+ reason: `could not read the argument list of the log call at ${anchor} \u2014 parentheses never balance, secret/PII content unverifiable (C-6)`
6129
+ });
5852
6130
  }
5853
- if (INTERNAL_RE.test(refs)) {
5854
- return {
5855
- id: this.id,
5856
- anchor: call.anchor,
5857
- state: "non-conforming",
5858
- verdict: "Form D \u2014 log call references an internal-infrastructure identifier (positive-control OK) -> never log internal system detail; redact before logging (C-6)"
5859
- };
6131
+ for (const call of calls) {
6132
+ if (SPREAD_RE.test(call.argText)) {
6133
+ unreadable.push({
6134
+ anchor: call.anchor,
6135
+ reason: `cannot inspect the spread argument of the log call at ${call.anchor} \u2014 a spread hides arbitrary content, secret/PII presence unverifiable (C-6)`
6136
+ });
6137
+ continue;
6138
+ }
6139
+ const refs = referencedIdentifiers(call.argText);
6140
+ if (SECRET_RE.test(refs)) {
6141
+ return {
6142
+ id: this.id,
6143
+ anchor: call.anchor,
6144
+ state: "non-conforming",
6145
+ verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (C-6)"
6146
+ };
6147
+ }
6148
+ if (PII_RE.test(refs)) {
6149
+ return {
6150
+ id: this.id,
6151
+ anchor: call.anchor,
6152
+ state: "non-conforming",
6153
+ verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
6154
+ };
6155
+ }
6156
+ if (INTERNAL_RE.test(refs)) {
6157
+ return {
6158
+ id: this.id,
6159
+ anchor: call.anchor,
6160
+ state: "non-conforming",
6161
+ verdict: "Form D \u2014 log call references an internal-infrastructure identifier (positive-control OK) -> never log internal system detail; redact before logging (C-6)"
6162
+ };
6163
+ }
5860
6164
  }
5861
6165
  }
5862
- }
5863
- for (const file of tree.files) {
5864
- for (const line of file.lines) {
5865
- if (line.comment) continue;
5866
- if (STACK_RE.test(line.raw) && CLIENT_SINK_RE.test(line.raw)) {
5867
- return {
5868
- id: this.id,
5869
- anchor: `${file.rel}:${line.n}`,
5870
- state: "non-conforming",
5871
- verdict: "Form B \u2014 raw error.stack serialized into a client-facing response (positive-control OK) -> return an McpError message, never the raw stack (C-6)"
5872
- };
6166
+ for (const file of tree.files) {
6167
+ for (const line of file.lines) {
6168
+ if (line.comment) continue;
6169
+ if (STACK_RE.test(line.raw) && CLIENT_SINK_RE.test(line.raw)) {
6170
+ return {
6171
+ id: this.id,
6172
+ anchor: `${file.rel}:${line.n}`,
6173
+ state: "non-conforming",
6174
+ verdict: "Form B \u2014 raw error.stack serialized into a client-facing response (positive-control OK) -> return an McpError message, never the raw stack (C-6)"
6175
+ };
6176
+ }
5873
6177
  }
5874
6178
  }
5875
- }
5876
- return {
5877
- id: this.id,
5878
- anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
5879
- state: "present",
5880
- verdict: "no secret/PII/internal-detail referenced in any log call; no raw error.stack exposed to the client (all positive-controls OK). Logging-sink configuration and explicit-activation-only diagnostic mode are the gate/runtime half, out of static scope (declared)."
5881
- };
6179
+ return {
6180
+ id: this.id,
6181
+ anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
6182
+ state: "present",
6183
+ verdict: "no secret/PII/internal-detail referenced in any log call; no raw error.stack exposed to the client (all positive-controls OK). Logging-sink configuration and explicit-activation-only diagnostic mode are the gate/runtime half, out of static scope (declared)."
6184
+ };
6185
+ })();
6186
+ return settle(this.id, decided, unreadable);
5882
6187
  }
5883
6188
  };
5884
6189
 
@@ -5960,59 +6265,65 @@ var dServer8 = {
5960
6265
  `DServer8 text control FAILED: /${controls.text.pattern}/ accept(\`${TEXT_POSITIVE}\`)=${controls.text.positive} reject(\`${TEXT_NEGATIVE}\`)=${!controls.text.negative} \u2014 matcher broken, verdict untrustworthy`
5961
6266
  );
5962
6267
  }
5963
- const blocks = [];
5964
- for (const file of tree.files) {
5965
- const { blocks: fileBlocks, unbalanced } = collectContentArrays(file);
5966
- if (unbalanced.length > 0) {
5967
- throw new Error(
5968
- `DServer8 could not read the content[] array at ${unbalanced.join(", ")} \u2014 brackets never balance, a ui:// resource emission could hide inside it (Server Rule 8). Fail-closed, not a silent present.`
5969
- );
6268
+ const unreadable = [];
6269
+ const decided = (() => {
6270
+ const blocks = [];
6271
+ for (const file of tree.files) {
6272
+ const { blocks: fileBlocks, unbalanced } = collectContentArrays(file);
6273
+ for (const anchor of unbalanced) {
6274
+ unreadable.push({
6275
+ anchor,
6276
+ reason: `could not read the content[] array at ${anchor} \u2014 brackets never balance, a ui:// resource emission could hide inside it (Server Rule 8)`
6277
+ });
6278
+ }
6279
+ blocks.push(...fileBlocks);
6280
+ }
6281
+ for (const file of tree.files) {
6282
+ const fileBlocks = blocks.filter((b) => b.file === file);
6283
+ for (const line of file.lines) {
6284
+ if (line.comment) continue;
6285
+ if (!KIT_RE.test(line.raw)) continue;
6286
+ const inside = fileBlocks.some((b) => line.n >= b.startLine && line.n <= b.endLine);
6287
+ if (!inside) {
6288
+ unreadable.push({
6289
+ anchor: `${file.rel}:${line.n}`,
6290
+ reason: `renderUiResource(\u2026) ui:// emission at ${file.rel}:${line.n} sits outside any content[] array \u2014 cannot associate it with a sibling text fallback (Server Rule 8)`
6291
+ });
6292
+ }
6293
+ }
5970
6294
  }
5971
- blocks.push(...fileBlocks);
5972
- }
5973
- for (const file of tree.files) {
5974
- const fileBlocks = blocks.filter((b) => b.file === file);
5975
- for (const line of file.lines) {
5976
- if (line.comment) continue;
5977
- if (!KIT_RE.test(line.raw)) continue;
5978
- const inside = fileBlocks.some((b) => line.n >= b.startLine && line.n <= b.endLine);
5979
- if (!inside) {
5980
- throw new Error(
5981
- `DServer8 found a renderUiResource(\u2026) ui:// emission at ${file.rel}:${line.n} outside any content[] array \u2014 cannot associate it with a sibling text fallback (Server Rule 8). Fail-closed, not a silent present.`
5982
- );
6295
+ let sawEmission = false;
6296
+ for (const block of blocks) {
6297
+ const inlineEmission = INLINE_RESOURCE_RE.test(block.text) && UI_URI_RE.test(block.text);
6298
+ const kitEmission = KIT_RE.test(block.text);
6299
+ if (!inlineEmission && !kitEmission) continue;
6300
+ sawEmission = true;
6301
+ const hasText = TEXT_RE.test(block.text) || MARKDOWN_RE.test(block.text);
6302
+ if (!hasText) {
6303
+ return {
6304
+ id: this.id,
6305
+ anchor: block.anchor,
6306
+ state: "non-conforming",
6307
+ verdict: `ui:// resource emitted in content[] with no text fallback beside ui:// in the same array (positive-control OK) -> add a sibling { type: "text" } / { type: "markdown" } entry (Server Rule 8)`
6308
+ };
5983
6309
  }
5984
6310
  }
5985
- }
5986
- let sawEmission = false;
5987
- for (const block of blocks) {
5988
- const inlineEmission = INLINE_RESOURCE_RE.test(block.text) && UI_URI_RE.test(block.text);
5989
- const kitEmission = KIT_RE.test(block.text);
5990
- if (!inlineEmission && !kitEmission) continue;
5991
- sawEmission = true;
5992
- const hasText = TEXT_RE.test(block.text) || MARKDOWN_RE.test(block.text);
5993
- if (!hasText) {
6311
+ if (!sawEmission) {
5994
6312
  return {
5995
6313
  id: this.id,
5996
- anchor: block.anchor,
5997
- state: "non-conforming",
5998
- verdict: `ui:// resource emitted in content[] with no text fallback beside ui:// in the same array (positive-control OK) -> add a sibling { type: "text" } / { type: "markdown" } entry (Server Rule 8)`
6314
+ anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
6315
+ state: "absent",
6316
+ verdict: "no ui:// resource emission in any content[] array (n/a for Server Rule 8)"
5999
6317
  };
6000
6318
  }
6001
- }
6002
- if (!sawEmission) {
6003
6319
  return {
6004
6320
  id: this.id,
6005
- anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
6006
- state: "absent",
6007
- verdict: "no ui:// resource emission in any content[] array (n/a for Server Rule 8)"
6321
+ anchor: blocks.length > 0 ? blocks[0].anchor : tree.root,
6322
+ state: "present",
6323
+ verdict: "every ui:// resource emission has a sibling text/markdown fallback in the same content[] (positive-control OK, Server Rule 8)"
6008
6324
  };
6009
- }
6010
- return {
6011
- id: this.id,
6012
- anchor: blocks.length > 0 ? blocks[0].anchor : tree.root,
6013
- state: "present",
6014
- verdict: "every ui:// resource emission has a sibling text/markdown fallback in the same content[] (positive-control OK, Server Rule 8)"
6015
- };
6325
+ })();
6326
+ return settle(this.id, decided, unreadable);
6016
6327
  }
6017
6328
  };
6018
6329
 
@@ -6241,7 +6552,7 @@ var detectors = [
6241
6552
 
6242
6553
  // src/runtime/launch.ts
6243
6554
  import { spawnSync as spawnSync2 } from "child_process";
6244
- import { existsSync as existsSync15, readFileSync as readFileSync18, statSync as statSync13 } from "fs";
6555
+ import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync13 } from "fs";
6245
6556
  import { dirname as dirname10, isAbsolute, join as join20, resolve } from "path";
6246
6557
  function findPackageDir(target) {
6247
6558
  let dir;
@@ -6276,7 +6587,7 @@ function deriveLaunchPlan(target) {
6276
6587
  }
6277
6588
  let pkg;
6278
6589
  try {
6279
- pkg = JSON.parse(readFileSync18(join20(pkgDir, "package.json"), "utf8"));
6590
+ pkg = JSON.parse(readFileSync11(join20(pkgDir, "package.json"), "utf8"));
6280
6591
  } catch (err) {
6281
6592
  return {
6282
6593
  error: `could not parse ${join20(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
@@ -6360,7 +6671,7 @@ function readEvalCounts(target) {
6360
6671
  const candidate = join20(dir, "evals", "evals.json");
6361
6672
  if (existsSync15(candidate)) {
6362
6673
  try {
6363
- const parsed = JSON.parse(readFileSync18(candidate, "utf8"));
6674
+ const parsed = JSON.parse(readFileSync11(candidate, "utf8"));
6364
6675
  for (const c of parsed.cases ?? []) {
6365
6676
  if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
6366
6677
  }
@@ -6620,7 +6931,7 @@ var USAGE = [
6620
6931
  function readVersion() {
6621
6932
  try {
6622
6933
  const here = dirname11(fileURLToPath(import.meta.url));
6623
- const manifest = JSON.parse(readFileSync19(join21(here, "..", "package.json"), "utf8"));
6934
+ const manifest = JSON.parse(readFileSync12(join21(here, "..", "package.json"), "utf8"));
6624
6935
  return manifest.version ?? null;
6625
6936
  } catch {
6626
6937
  return null;