@vantageos/mcp-doctor 0.3.0 → 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",
@@ -3143,13 +3304,22 @@ var d9 = {
3143
3304
  );
3144
3305
  }
3145
3306
  if (!tree.packageJson) {
3146
- throw new Error(
3147
- "D9 could not locate package.json anywhere above the scanned root \u2014 cannot verify SemVer/CHANGELOG"
3148
- );
3307
+ const reason = "no package.json found anywhere above the scanned root \u2014 cannot verify SemVer/CHANGELOG";
3308
+ return {
3309
+ id: this.id,
3310
+ anchor: "package.json",
3311
+ state: "could-not-judge",
3312
+ reason,
3313
+ verdict: `could not judge: ${reason}`
3314
+ };
3149
3315
  }
3150
3316
  const { path: pkgPath, json: pkgJson } = tree.packageJson;
3151
3317
  if (!pkgJson || Object.keys(pkgJson).length === 0) {
3152
- 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
+ );
3153
3323
  }
3154
3324
  const version = pkgJson.version;
3155
3325
  if (typeof version !== "string") {
@@ -3188,6 +3358,7 @@ var d9 = {
3188
3358
  };
3189
3359
  }
3190
3360
  };
3361
+ var d9 = guardUnreadablePackageJson(d9Inner, { authoritative: false });
3191
3362
 
3192
3363
  // src/detectors/d10.ts
3193
3364
  import { existsSync as existsSync9 } from "fs";
@@ -3239,7 +3410,7 @@ function scanSourceDescriptionFr(tree) {
3239
3410
  }
3240
3411
  return { present: false, anchor: null };
3241
3412
  }
3242
- var d10 = {
3413
+ var d10Inner = {
3243
3414
  id: "D10 bilingual",
3244
3415
  rule: "C-1",
3245
3416
  pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
@@ -3310,6 +3481,7 @@ var d10 = {
3310
3481
  };
3311
3482
  }
3312
3483
  };
3484
+ var d10 = guardUnreadablePackageJson(d10Inner, { authoritative: true });
3313
3485
 
3314
3486
  // src/detectors/d11.ts
3315
3487
  var ZOD_ANY_RE = /\bz\s*\.\s*any\s*\(\s*\)/;
@@ -3381,6 +3553,27 @@ var d11 = {
3381
3553
  }
3382
3554
  }
3383
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
+ }
3384
3577
  return {
3385
3578
  id: this.id,
3386
3579
  anchor: "-",
@@ -3460,7 +3653,7 @@ function findHttpSite(tree) {
3460
3653
  }
3461
3654
  return null;
3462
3655
  }
3463
- var d12 = {
3656
+ var d12Inner = {
3464
3657
  id: "D12 stdio-transport",
3465
3658
  run(tree) {
3466
3659
  const stdioControl = d12StdioPositiveControl();
@@ -3511,6 +3704,7 @@ var d12 = {
3511
3704
  };
3512
3705
  }
3513
3706
  };
3707
+ var d12 = guardUnreadablePackageJson(d12Inner, { authoritative: false });
3514
3708
 
3515
3709
  // src/detectors/d15.ts
3516
3710
  var HANDLE_ASSIGN_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[^;]*\.(?:registerTool|tool)\s*\(/;
@@ -4124,55 +4318,59 @@ var dDebugOptIn = {
4124
4318
  );
4125
4319
  }
4126
4320
  }
4127
- const computed = findAll(tree, COMPUTED_LEVEL_RE);
4128
- if (computed.length > 0) {
4129
- const sites = computed.map((h) => `${h.file.rel}:${h.line}`).join(", ");
4130
- throw new Error(
4131
- `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.`
4132
- );
4133
- }
4134
- const debugCalls = findAll(tree, DEBUG_CALL_RE);
4135
- if (debugCalls.length === 0) {
4136
- return {
4137
- id: this.id,
4138
- anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
4139
- state: "absent",
4140
- verdict: "no debug output emitted (n/a for Server Rule 6 debug-half)"
4141
- };
4142
- }
4143
- const callAnchor = `${debugCalls[0].file.rel}:${debugCalls.map((h) => h.line).join(",")}`;
4144
- const debugFileRels = new Set(debugCalls.map((h) => h.file.rel));
4145
- const localGate = firstMatchInFiles2(tree, GATE_RE, debugFileRels);
4146
- if (localGate) {
4147
- return {
4148
- id: this.id,
4149
- anchor: callAnchor,
4150
- state: "present",
4151
- 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)`
4152
- };
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
+ });
4153
4328
  }
4154
- const remoteGate = firstMatchInFiles2(tree, GATE_RE, new Set(tree.files.map((f) => f.rel)));
4155
- if (remoteGate) {
4156
- 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
+ }
4157
4361
  return {
4158
4362
  id: this.id,
4159
4363
  anchor: callAnchor,
4160
- state: "could-not-judge",
4161
- reason,
4162
- 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)`
4163
4366
  };
4164
- }
4165
- return {
4166
- id: this.id,
4167
- anchor: callAnchor,
4168
- state: "non-conforming",
4169
- 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)`
4170
- };
4367
+ })();
4368
+ return settle(this.id, decided, unreadable);
4171
4369
  }
4172
4370
  };
4173
4371
 
4174
4372
  // src/detectors/dExt1.ts
4175
- import { readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4373
+ import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4176
4374
  import { join as join12 } from "path";
4177
4375
  var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
4178
4376
  var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
@@ -4250,68 +4448,76 @@ var dExt1 = {
4250
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`
4251
4449
  );
4252
4450
  }
4253
- const manifests = findExtensionManifests(tree.root);
4254
- for (const manifestPath2 of manifests) {
4255
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4256
- let parsed;
4257
- try {
4258
- parsed = JSON.parse(readFileSync10(manifestPath2, "utf8"));
4259
- } catch {
4260
- throw new Error(
4261
- `DExt1 could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable identifier cannot pass (Extension Rule 1). Fail-closed, not a silent present.`
4262
- );
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
+ }
4263
4481
  }
4264
- const idValue = parsed.identifier;
4265
- if (typeof idValue !== "string") {
4266
- throw new Error(
4267
- `DExt1 found a present manifest ${shortPath} with no string "identifier" field (got ${typeof idValue}) \u2014 Extension Rule 1 requires a reverse-DNS identifier. Fail-closed, not a silent present.`
4268
- );
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
+ });
4269
4488
  }
4270
- if (!IDENTIFIER_RE2.test(idValue)) {
4271
- return {
4272
- id: this.id,
4273
- anchor: shortPath,
4274
- state: "non-conforming",
4275
- verdict: `manifest identifier "${idValue}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
4276
- };
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
+ }
4277
4498
  }
4278
- }
4279
- const { sites, unresolved } = findTsIdentifiers(tree);
4280
- if (unresolved.length > 0) {
4281
- throw new Error(
4282
- `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.`
4283
- );
4284
- }
4285
- for (const site of sites) {
4286
- if (!IDENTIFIER_RE2.test(site.value)) {
4499
+ const total = manifests.length + sites.length;
4500
+ if (total === 0) {
4287
4501
  return {
4288
4502
  id: this.id,
4289
- anchor: site.anchor,
4290
- state: "non-conforming",
4291
- 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)"
4292
4506
  };
4293
4507
  }
4294
- }
4295
- const total = manifests.length + sites.length;
4296
- if (total === 0) {
4297
4508
  return {
4298
4509
  id: this.id,
4299
- anchor: tree.root.split("/").pop() ?? tree.root,
4300
- state: "absent",
4301
- 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)`
4302
4513
  };
4303
- }
4304
- return {
4305
- id: this.id,
4306
- anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
4307
- state: "present",
4308
- verdict: `${total} identifier(s) reverse-DNS conforming (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4309
- };
4514
+ })();
4515
+ return settle(this.id, decided, unreadable);
4310
4516
  }
4311
4517
  };
4312
4518
 
4313
4519
  // src/detectors/dExt2.ts
4314
- import { readFileSync as readFileSync11, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
4520
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
4315
4521
  import { join as join13 } from "path";
4316
4522
  var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
4317
4523
  var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
@@ -4411,76 +4617,85 @@ var dExt2 = {
4411
4617
  `DExt2 graceful-degradation control FAILED: accept(\`${GD_POSITIVE}\`)=${controls.gracefulDegradation.acceptsTrue} reject(\`${GD_NEGATIVE}\`)=${controls.gracefulDegradation.rejectsFalse} \u2014 matcher broken, verdict untrustworthy`
4412
4618
  );
4413
4619
  }
4414
- const manifests = findExtensionManifests2(tree.root);
4415
- for (const manifestPath2 of manifests) {
4416
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4417
- let parsed;
4418
- try {
4419
- parsed = JSON.parse(readFileSync11(manifestPath2, "utf8"));
4420
- } catch {
4421
- throw new Error(
4422
- `DExt2 could not parse ${shortPath} as JSON \u2014 a present manifest silent on graceful degradation cannot pass (Extension Rule 2). Fail-closed, not a silent present.`
4423
- );
4424
- }
4425
- if (!("graceful_degradation" in parsed)) {
4426
- throw new Error(
4427
- `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.`
4428
- );
4429
- }
4430
- const gdValue = parsed.graceful_degradation;
4431
- if (gdValue !== true) {
4432
- return {
4433
- id: this.id,
4434
- anchor: shortPath,
4435
- state: "non-conforming",
4436
- 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)`
4437
- };
4438
- }
4439
- }
4440
- const sites = findTsFactorySites(tree);
4441
- for (const site of sites) {
4442
- if (site.state === "unresolved") {
4443
- throw new Error(
4444
- `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.`
4445
- );
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
+ }
4446
4650
  }
4447
- if (site.state === "false") {
4448
- return {
4449
- id: this.id,
4450
- anchor: site.anchor,
4451
- state: "non-conforming",
4452
- verdict: "gracefulDegradation is false, not true (positive-control OK) -> an extension never breaks the host server (Extension Rule 2)"
4453
- };
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
+ }
4454
4676
  }
4455
- if (site.state === "absent") {
4677
+ const total = manifests.length + sites.length;
4678
+ if (total === 0) {
4456
4679
  return {
4457
4680
  id: this.id,
4458
- anchor: site.anchor,
4459
- state: "non-conforming",
4460
- 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)"
4461
4684
  };
4462
4685
  }
4463
- }
4464
- const total = manifests.length + sites.length;
4465
- if (total === 0) {
4466
4686
  return {
4467
4687
  id: this.id,
4468
- anchor: tree.root.split("/").pop() ?? tree.root,
4469
- state: "absent",
4470
- 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)`
4471
4691
  };
4472
- }
4473
- return {
4474
- id: this.id,
4475
- anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
4476
- state: "present",
4477
- verdict: `${total} graceful-degradation flag(s) true (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4478
- };
4692
+ })();
4693
+ return settle(this.id, decided, unreadable);
4479
4694
  }
4480
4695
  };
4481
4696
 
4482
4697
  // src/detectors/dExt3.ts
4483
- import { readFileSync as readFileSync12, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
4698
+ import { readdirSync as readdirSync6, statSync as statSync7 } from "fs";
4484
4699
  import { join as join14 } from "path";
4485
4700
  var TYPE_MATCHERS = [
4486
4701
  { type: "middleware", re: /\bmiddleware\b/ },
@@ -4594,100 +4809,118 @@ var dExt3 = {
4594
4809
  `DExt3 single-type control FAILED: detectMix(\`${TYPE_MIXED}\`)=${controls.type.detectsMix} detectSingle(\`${TYPE_SINGLE}\`)=${controls.type.detectsSingle} \u2014 matcher broken, verdict untrustworthy`
4595
4810
  );
4596
4811
  }
4597
- const declared = /* @__PURE__ */ new Set();
4598
- let firstAnchor2 = null;
4599
- const manifests = findExtensionManifests3(tree.root);
4600
- for (const manifestPath2 of manifests) {
4601
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4602
- let parsed;
4603
- try {
4604
- parsed = JSON.parse(readFileSync12(manifestPath2, "utf8"));
4605
- } catch {
4606
- throw new Error(
4607
- `DExt3 could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable type cannot pass (Extension Rule 3). Fail-closed, not a silent present.`
4608
- );
4609
- }
4610
- if (!("type" in parsed)) {
4611
- throw new Error(
4612
- `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.`
4613
- );
4614
- }
4615
- const rawValue = coerceTypeValue(parsed.type);
4616
- if (rawValue === null) {
4617
- throw new Error(
4618
- `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.`
4619
- );
4620
- }
4621
- const found = typesIn(rawValue);
4622
- if (found.length === 0) {
4623
- throw new Error(
4624
- `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.`
4625
- );
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]);
4626
4893
  }
4627
- if (found.length > 1) {
4894
+ const total = manifests.length + sites.length;
4895
+ if (total === 0) {
4628
4896
  return {
4629
4897
  id: this.id,
4630
- anchor: shortPath,
4631
- state: "non-conforming",
4632
- 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)"
4633
4901
  };
4634
4902
  }
4635
- firstAnchor2 ??= shortPath;
4636
- declared.add(found[0]);
4637
- }
4638
- const sites = findTsTypeSites(tree);
4639
- for (const site of sites) {
4640
- if (site.unresolved) {
4641
- throw new Error(
4642
- `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.`
4643
- );
4644
- }
4645
- const raw = site.value ?? "";
4646
- const found = typesIn(raw);
4647
- if (found.length === 0) {
4648
- throw new Error(
4649
- `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.`
4650
- );
4651
- }
4652
- if (found.length > 1) {
4903
+ if (declared.size > 1) {
4653
4904
  return {
4654
4905
  id: this.id,
4655
- anchor: site.anchor,
4906
+ anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
4656
4907
  state: "non-conforming",
4657
- 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)`
4658
4909
  };
4659
4910
  }
4660
- firstAnchor2 ??= site.anchor;
4661
- declared.add(found[0]);
4662
- }
4663
- const total = manifests.length + sites.length;
4664
- if (total === 0) {
4665
- return {
4666
- id: this.id,
4667
- anchor: tree.root.split("/").pop() ?? tree.root,
4668
- state: "absent",
4669
- verdict: "no extension type declared (no extension.json manifest, no defineExtension `type:` literal) -> declare exactly one type (Extension Rule 3)"
4670
- };
4671
- }
4672
- if (declared.size > 1) {
4673
4911
  return {
4674
4912
  id: this.id,
4675
4913
  anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
4676
- state: "non-conforming",
4677
- 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)`
4678
4916
  };
4679
- }
4680
- return {
4681
- id: this.id,
4682
- anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
4683
- state: "present",
4684
- verdict: `single type "${[...declared][0]}" declared across ${total} form(s) (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
4685
- };
4917
+ })();
4918
+ return settle(this.id, decided, unreadable);
4686
4919
  }
4687
4920
  };
4688
4921
 
4689
4922
  // src/detectors/dExt4.ts
4690
- import { readFileSync as readFileSync13, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
4923
+ import { readdirSync as readdirSync7, statSync as statSync8 } from "fs";
4691
4924
  import { join as join15 } from "path";
4692
4925
  function canonical(name) {
4693
4926
  return name.replace(/[-_]/g, "").toLowerCase();
@@ -4831,7 +5064,7 @@ function findFactorySites(tree) {
4831
5064
  function manifestHookTokens(raw) {
4832
5065
  const set = /* @__PURE__ */ new Set();
4833
5066
  if (!Array.isArray(raw)) {
4834
- throw new Error("not-array");
5067
+ return null;
4835
5068
  }
4836
5069
  for (const h of raw) {
4837
5070
  if (typeof h === "string") set.add(canonical(h));
@@ -4847,82 +5080,90 @@ var dExt4 = {
4847
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`
4848
5081
  );
4849
5082
  }
4850
- const hooksDirs = findHooksDirs(tree.root);
4851
- const { sites, unresolved } = findFactorySites(tree);
4852
- if (unresolved.length > 0) {
4853
- throw new Error(
4854
- `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.`
4855
- );
4856
- }
4857
- const implemented = /* @__PURE__ */ new Set();
4858
- for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
4859
- for (const s of sites) for (const t of s.tokens) implemented.add(t);
4860
- const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
4861
- const manifests = findExtensionManifests4(tree.root);
4862
- let declaredAnyLifecycle = false;
4863
- for (const manifestPath2 of manifests) {
4864
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
4865
- let parsed;
4866
- try {
4867
- parsed = JSON.parse(readFileSync13(manifestPath2, "utf8"));
4868
- } catch {
4869
- throw new Error(
4870
- `DExt4 could not parse ${shortPath} as JSON \u2014 a present manifest whose lifecycle cannot be read cannot pass (Extension Rule 4). Fail-closed, not a silent present.`
4871
- );
4872
- }
4873
- if (!("lifecycle" in parsed)) continue;
4874
- let declared;
4875
- try {
4876
- declared = manifestHookTokens(parsed.lifecycle);
4877
- } catch {
4878
- throw new Error(
4879
- `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.`
4880
- );
4881
- }
4882
- if (declared.size === 0) continue;
4883
- declaredAnyLifecycle = true;
4884
- if (!implSurfacePresent) {
4885
- throw new Error(
4886
- `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.`
4887
- );
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
+ });
4888
5092
  }
4889
- const phantom = [...declared].filter((h) => !implemented.has(h));
4890
- if (phantom.length > 0) {
4891
- return {
4892
- id: this.id,
4893
- anchor: shortPath,
4894
- state: "non-conforming",
4895
- 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)`
4896
- };
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
+ }
4897
5137
  }
4898
- }
4899
- if (!declaredAnyLifecycle) {
4900
- 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
+ }
4901
5147
  return {
4902
5148
  id: this.id,
4903
- anchor: tree.root.split("/").pop() ?? tree.root,
4904
- state: "absent",
4905
- 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)`
4906
5152
  };
4907
5153
  }
4908
5154
  return {
4909
5155
  id: this.id,
4910
- anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
5156
+ anchor: manifests[0].split("/").slice(-2).join("/"),
4911
5157
  state: "present",
4912
- 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)`
4913
5159
  };
4914
- }
4915
- return {
4916
- id: this.id,
4917
- anchor: manifests[0].split("/").slice(-2).join("/"),
4918
- state: "present",
4919
- verdict: `every declared lifecycle hook is implemented (declared \u2286 implemented: [${[...implemented].join(", ")}], hooksDir=${hooksDirs.length}, factory=${sites.length}, positive-control OK)`
4920
- };
5160
+ })();
5161
+ return settle(this.id, decided, unreadable);
4921
5162
  }
4922
5163
  };
4923
5164
 
4924
5165
  // src/detectors/dExt5.ts
4925
- 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";
4926
5167
  import { dirname as dirname7, join as join16 } from "path";
4927
5168
  function nonEmptyString(v) {
4928
5169
  return typeof v === "string" && v.trim().length > 0;
@@ -5003,10 +5244,8 @@ var dExt5 = {
5003
5244
  reason,
5004
5245
  verdict: `could not judge: ${reason}`
5005
5246
  });
5006
- let parsed;
5007
- try {
5008
- parsed = JSON.parse(readFileSync14(manifestPath2, "utf8"));
5009
- } catch {
5247
+ const parsed = readJsonObject(manifestPath2);
5248
+ if (parsed === null) {
5010
5249
  return cannotRead(
5011
5250
  `${shortPath} does not parse as JSON \u2014 the FR surface of a present manifest cannot be read (Extension Rule 5)`
5012
5251
  );
@@ -5063,7 +5302,7 @@ var dExt5 = {
5063
5302
  };
5064
5303
 
5065
5304
  // src/detectors/dExt7.ts
5066
- 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";
5067
5306
  import { dirname as dirname8, join as join17 } from "path";
5068
5307
  var ABSENCE_FLAG_FIELDS = [
5069
5308
  "extensionAbsent",
@@ -5160,65 +5399,78 @@ var dExt7 = {
5160
5399
  verdict: "no extension.json manifest found -> declare an extension with an eval suite (>=3 cases incl. 1 'extension absente') (Extension Rule 7)"
5161
5400
  };
5162
5401
  }
5163
- for (const manifestPath2 of manifests) {
5164
- const extDir = dirname8(manifestPath2);
5165
- const extLabel = extDir.split("/").pop() ?? extDir;
5166
- const evalsPath = join17(extDir, "evals", "evals.json");
5167
- const shortPath = `${extLabel}/evals/evals.json`;
5168
- if (!existsSync13(evalsPath)) {
5169
- return {
5170
- id: this.id,
5171
- anchor: `${extLabel}/extension.json`,
5172
- state: "non-conforming",
5173
- verdict: `no evals/evals.json for extension "${extLabel}" -> add an eval suite with >=3 cases incl. 1 'extension absente' (Extension Rule 7)`
5174
- };
5175
- }
5176
- let cases;
5177
- try {
5178
- const raw = readFileSync15(evalsPath, "utf8");
5179
- 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
+ }
5180
5425
  if (!Array.isArray(parsed.cases)) {
5181
- 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
+ };
5182
5432
  }
5183
- cases = parsed.cases;
5184
- } catch (err) {
5185
- throw new Error(
5186
- `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)
5187
5436
  );
5188
- }
5189
- cases.forEach((c, i) => {
5190
- if (c === null || typeof c !== "object" || Array.isArray(c)) {
5191
- throw new Error(
5192
- `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.`
5193
- );
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
+ };
5194
5458
  }
5195
- });
5196
- const count = cases.length;
5197
- const hasAbsence = cases.some(isAbsenceCase);
5198
- const failures = [];
5199
- if (count < 3) failures.push(`only ${count} case(s) (need >=3)`);
5200
- if (!hasAbsence) failures.push("no 'extension absente'/degradation case");
5201
- if (failures.length > 0) {
5202
- return {
5203
- id: this.id,
5204
- anchor: `${shortPath}`,
5205
- state: "non-conforming",
5206
- verdict: `eval suite for "${extLabel}": ${failures.join("; ")} (positive-control OK) -> Extension Rule 7 needs >=3 cases incl. 1 'extension absente'`
5207
- };
5208
5459
  }
5209
- }
5210
- const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
5211
- return {
5212
- id: this.id,
5213
- anchor: `${firstLabel}/evals/evals.json`,
5214
- state: "present",
5215
- verdict: `eval suite complete across ${manifests.length} extension(s): >=3 cases incl. an 'extension absente'/degradation case (positive-control OK)`
5216
- };
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);
5217
5469
  }
5218
5470
  };
5219
5471
 
5220
5472
  // src/detectors/dExt8.ts
5221
- 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";
5222
5474
  import { dirname as dirname9, join as join18 } from "path";
5223
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-]+)*))?$/;
5224
5476
  var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
@@ -5282,73 +5534,88 @@ var dExt8 = {
5282
5534
  verdict: "no extension.json manifest found -> declare an extension with a valid SemVer version + CHANGELOG.md (Extension Rule 8)"
5283
5535
  };
5284
5536
  }
5537
+ const unreadable = [];
5538
+ let finding;
5539
+ let judgedConforming = 0;
5285
5540
  for (const manifestPath2 of manifests) {
5286
5541
  const extDir = dirname9(manifestPath2);
5287
5542
  const extLabel = extDir.split("/").pop() ?? extDir;
5288
5543
  const pkgPath = join18(extDir, "package.json");
5289
5544
  const shortPkgPath = `${extLabel}/package.json`;
5290
5545
  if (!existsSync14(pkgPath)) {
5291
- throw new Error(
5292
- `DExt8 could not locate ${shortPkgPath} for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8). Fail-closed, not a silent present.`
5293
- );
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;
5294
5551
  }
5295
- let pkgJson;
5296
- try {
5297
- pkgJson = JSON.parse(readFileSync16(pkgPath, "utf8"));
5298
- } catch {
5299
- throw new Error(
5300
- `DExt8 could not parse ${shortPkgPath} as JSON for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8). Fail-closed, not a silent present.`
5301
- );
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;
5302
5559
  }
5303
5560
  const version = pkgJson.version;
5304
5561
  if ("version" in pkgJson && typeof version !== "string") {
5305
- throw new Error(
5306
- `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.`
5307
- );
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;
5308
5569
  }
5309
5570
  if (typeof version !== "string" || version.trim().length === 0) {
5310
- return {
5571
+ finding ??= {
5311
5572
  id: this.id,
5312
5573
  anchor: shortPkgPath,
5313
5574
  state: "non-conforming",
5314
5575
  verdict: `no "version" field in ${shortPkgPath} for extension "${extLabel}" -> set a valid SemVer version (Extension Rule 8)`
5315
5576
  };
5577
+ continue;
5316
5578
  }
5317
5579
  const validSemver = SEMVER_RE2.test(version);
5318
5580
  const changelogPath = join18(extDir, "CHANGELOG.md");
5319
5581
  const hasChangelog = existsSync14(changelogPath);
5320
5582
  if (!validSemver) {
5321
- return {
5583
+ finding ??= {
5322
5584
  id: this.id,
5323
5585
  anchor: shortPkgPath,
5324
5586
  state: "non-conforming",
5325
5587
  verdict: `"version": "${version}" is not valid SemVer for extension "${extLabel}" (positive-control OK) -> use MAJOR.MINOR.PATCH (Extension Rule 8)`
5326
5588
  };
5589
+ continue;
5327
5590
  }
5328
5591
  if (!hasChangelog) {
5329
- return {
5592
+ finding ??= {
5330
5593
  id: this.id,
5331
5594
  anchor: shortPkgPath,
5332
5595
  state: "absent",
5333
5596
  verdict: `no CHANGELOG.md next to ${shortPkgPath} for extension "${extLabel}" (positive-control OK) -> add CHANGELOG.md, update it each release (Extension Rule 8)`
5334
5597
  };
5598
+ continue;
5335
5599
  }
5600
+ judgedConforming++;
5336
5601
  }
5602
+ if (finding) return settle(this.id, finding, unreadable);
5337
5603
  const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
5338
- return {
5604
+ const conforming = {
5339
5605
  id: this.id,
5340
5606
  anchor: `${firstLabel}/package.json`,
5341
5607
  state: "present",
5342
- 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)`
5343
5609
  };
5610
+ return settle(this.id, conforming, unreadable);
5344
5611
  }
5345
5612
  };
5346
5613
 
5347
5614
  // src/detectors/dExt9.ts
5348
- 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";
5349
5616
  import { join as join19 } from "path";
5350
5617
  var SKIP_DIRS9 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
5351
- function collectHooksDirSources(root) {
5618
+ function collectHooksDirSources(root, unreadable) {
5352
5619
  const out = [];
5353
5620
  let base;
5354
5621
  try {
@@ -5387,11 +5654,14 @@ function collectHooksDirSources(root) {
5387
5654
  const filePath = join19(full, f);
5388
5655
  let body;
5389
5656
  try {
5390
- body = readFileSync17(filePath, "utf8");
5657
+ body = readFileSync10(filePath, "utf8");
5391
5658
  } catch {
5392
- throw new Error(
5393
- `DExt9 could not read hook implementation ${filePath.split("/").slice(-2).join("/")} \u2014 timeout guard undeterminable for hook "${stem}" (Extension Rule 9). Fail-closed, not a silent absent.`
5394
- );
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;
5395
5665
  }
5396
5666
  out.push({
5397
5667
  token: canonical(stem),
@@ -5461,13 +5731,11 @@ function dExt9PositiveControls() {
5461
5731
  negative: hasImplTimeoutGuard(CTRL_NEGATIVE_BODY)
5462
5732
  };
5463
5733
  }
5464
- function manifestTimeoutTokens(raw, shortPath) {
5734
+ function manifestTimeoutTokens(raw) {
5465
5735
  const map = /* @__PURE__ */ new Map();
5466
5736
  if (raw === void 0) return map;
5467
5737
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
5468
- throw new Error(
5469
- `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.`
5470
- );
5738
+ return null;
5471
5739
  }
5472
5740
  for (const [key, value] of Object.entries(raw)) {
5473
5741
  if (typeof value === "number" && value > 0) {
@@ -5485,67 +5753,83 @@ var dExt9 = {
5485
5753
  `DExt9 hook-timeout control FAILED: setTimeoutForm=${controls.setTimeoutForm} abortForm=${controls.abortForm} negative=${controls.negative} \u2014 impl-pattern matcher broken, verdict untrustworthy`
5486
5754
  );
5487
5755
  }
5488
- const hookSources = [...collectHooksDirSources(tree.root), ...collectFactorySources(tree)];
5489
- const implByToken = /* @__PURE__ */ new Map();
5490
- for (const s of hookSources) {
5491
- const list = implByToken.get(s.token) ?? [];
5492
- list.push(s);
5493
- implByToken.set(s.token, list);
5494
- }
5495
- const manifests = findExtensionManifests4(tree.root);
5496
- let declaredAnyLifecycle = false;
5497
- for (const manifestPath2 of manifests) {
5498
- const shortPath = manifestPath2.split("/").slice(-2).join("/");
5499
- let parsed;
5500
- try {
5501
- parsed = JSON.parse(readFileSync17(manifestPath2, "utf8"));
5502
- } catch {
5503
- throw new Error(
5504
- `DExt9 could not parse ${shortPath} as JSON \u2014 timeout guards undeterminable (Extension Rule 9). Fail-closed, not a silent absent.`
5505
- );
5506
- }
5507
- if (!("lifecycle" in parsed)) continue;
5508
- let declared;
5509
- try {
5510
- declared = manifestHookTokens(parsed.lifecycle);
5511
- } catch {
5512
- throw new Error(
5513
- `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.`
5514
- );
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
+ }
5515
5816
  }
5516
- if (declared.size === 0) continue;
5517
- declaredAnyLifecycle = true;
5518
- const timeouts = manifestTimeoutTokens(parsed.timeouts, shortPath);
5519
- const noTimeout = [];
5520
- for (const hook of declared) {
5521
- const hasManifestTimeout = timeouts.has(hook);
5522
- const implSources = implByToken.get(hook) ?? [];
5523
- const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
5524
- if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
5525
- }
5526
- if (noTimeout.length > 0) {
5817
+ if (!declaredAnyLifecycle) {
5527
5818
  return {
5528
5819
  id: this.id,
5529
- anchor: shortPath,
5530
- state: "non-conforming",
5531
- 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)"
5532
5823
  };
5533
5824
  }
5534
- }
5535
- if (!declaredAnyLifecycle) {
5536
5825
  return {
5537
5826
  id: this.id,
5538
- anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
5539
- state: "absent",
5540
- 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)"
5541
5830
  };
5542
- }
5543
- return {
5544
- id: this.id,
5545
- anchor: manifests[0].split("/").slice(-2).join("/"),
5546
- state: "present",
5547
- verdict: "every declared lifecycle hook has a timeout guard (manifest timeouts.<hook>, or impl-side cutoff), positive-control OK (Extension Rule 9)"
5548
- };
5831
+ })();
5832
+ return settle(this.id, decided, unreadable);
5549
5833
  }
5550
5834
  };
5551
5835
 
@@ -5574,7 +5858,7 @@ function unscopePackageName(name) {
5574
5858
  const m = name.match(/^@[^/]+\/(.+)$/);
5575
5859
  return m ? m[1] : name;
5576
5860
  }
5577
- var dServer2 = {
5861
+ var dServer2Inner = {
5578
5862
  id: "DServer2 naming",
5579
5863
  rule: "C-2",
5580
5864
  pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
@@ -5602,6 +5886,22 @@ var dServer2 = {
5602
5886
  };
5603
5887
  }
5604
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
+ }
5605
5905
  for (const t of tools) {
5606
5906
  if (!TOOL_SNAKE_RE.test(t.name)) {
5607
5907
  return {
@@ -5705,6 +6005,9 @@ var dServer2 = {
5705
6005
  };
5706
6006
  }
5707
6007
  };
6008
+ var dServer2 = guardUnreadablePackageJson(dServer2Inner, {
6009
+ authoritative: false
6010
+ });
5708
6011
 
5709
6012
  // src/detectors/dServer6.ts
5710
6013
  var LOG_OPENER_RE = /\b(?:console|logger|log)\.(?:log|info|warn|error|debug|trace)\s*\(/;
@@ -5815,65 +6118,72 @@ var dServer6 = {
5815
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`
5816
6119
  );
5817
6120
  }
5818
- for (const file of tree.files) {
5819
- const { calls, unbalanced } = collectLogCalls(file);
5820
- if (unbalanced.length > 0) {
5821
- throw new Error(
5822
- `DServer6 could not read the argument list of ${unbalanced.length} log call(s): ${unbalanced.join(", ")} \u2014 parentheses never balance, secret/PII content unverifiable (C-6). Fail-closed, not a silent present.`
5823
- );
5824
- }
5825
- for (const call of calls) {
5826
- if (SPREAD_RE.test(call.argText)) {
5827
- throw new Error(
5828
- `DServer6 cannot inspect the spread argument of the log call at ${call.anchor} \u2014 a spread hides arbitrary content, secret/PII presence unverifiable (C-6). Fail-closed, not a silent present.`
5829
- );
5830
- }
5831
- const refs = referencedIdentifiers(call.argText);
5832
- if (SECRET_RE.test(refs)) {
5833
- return {
5834
- id: this.id,
5835
- anchor: call.anchor,
5836
- state: "non-conforming",
5837
- verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (C-6)"
5838
- };
5839
- }
5840
- if (PII_RE.test(refs)) {
5841
- return {
5842
- id: this.id,
5843
- anchor: call.anchor,
5844
- state: "non-conforming",
5845
- verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
5846
- };
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
+ });
5847
6130
  }
5848
- if (INTERNAL_RE.test(refs)) {
5849
- return {
5850
- id: this.id,
5851
- anchor: call.anchor,
5852
- state: "non-conforming",
5853
- verdict: "Form D \u2014 log call references an internal-infrastructure identifier (positive-control OK) -> never log internal system detail; redact before logging (C-6)"
5854
- };
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
+ }
5855
6164
  }
5856
6165
  }
5857
- }
5858
- for (const file of tree.files) {
5859
- for (const line of file.lines) {
5860
- if (line.comment) continue;
5861
- if (STACK_RE.test(line.raw) && CLIENT_SINK_RE.test(line.raw)) {
5862
- return {
5863
- id: this.id,
5864
- anchor: `${file.rel}:${line.n}`,
5865
- state: "non-conforming",
5866
- verdict: "Form B \u2014 raw error.stack serialized into a client-facing response (positive-control OK) -> return an McpError message, never the raw stack (C-6)"
5867
- };
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
+ }
5868
6177
  }
5869
6178
  }
5870
- }
5871
- return {
5872
- id: this.id,
5873
- anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
5874
- state: "present",
5875
- verdict: "no secret/PII/internal-detail referenced in any log call; no raw error.stack exposed to the client (all positive-controls OK). Logging-sink configuration and explicit-activation-only diagnostic mode are the gate/runtime half, out of static scope (declared)."
5876
- };
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);
5877
6187
  }
5878
6188
  };
5879
6189
 
@@ -5955,59 +6265,65 @@ var dServer8 = {
5955
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`
5956
6266
  );
5957
6267
  }
5958
- const blocks = [];
5959
- for (const file of tree.files) {
5960
- const { blocks: fileBlocks, unbalanced } = collectContentArrays(file);
5961
- if (unbalanced.length > 0) {
5962
- throw new Error(
5963
- `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.`
5964
- );
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
+ }
5965
6294
  }
5966
- blocks.push(...fileBlocks);
5967
- }
5968
- for (const file of tree.files) {
5969
- const fileBlocks = blocks.filter((b) => b.file === file);
5970
- for (const line of file.lines) {
5971
- if (line.comment) continue;
5972
- if (!KIT_RE.test(line.raw)) continue;
5973
- const inside = fileBlocks.some((b) => line.n >= b.startLine && line.n <= b.endLine);
5974
- if (!inside) {
5975
- throw new Error(
5976
- `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.`
5977
- );
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
+ };
5978
6309
  }
5979
6310
  }
5980
- }
5981
- let sawEmission = false;
5982
- for (const block of blocks) {
5983
- const inlineEmission = INLINE_RESOURCE_RE.test(block.text) && UI_URI_RE.test(block.text);
5984
- const kitEmission = KIT_RE.test(block.text);
5985
- if (!inlineEmission && !kitEmission) continue;
5986
- sawEmission = true;
5987
- const hasText = TEXT_RE.test(block.text) || MARKDOWN_RE.test(block.text);
5988
- if (!hasText) {
6311
+ if (!sawEmission) {
5989
6312
  return {
5990
6313
  id: this.id,
5991
- anchor: block.anchor,
5992
- state: "non-conforming",
5993
- 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)"
5994
6317
  };
5995
6318
  }
5996
- }
5997
- if (!sawEmission) {
5998
6319
  return {
5999
6320
  id: this.id,
6000
- anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
6001
- state: "absent",
6002
- 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)"
6003
6324
  };
6004
- }
6005
- return {
6006
- id: this.id,
6007
- anchor: blocks.length > 0 ? blocks[0].anchor : tree.root,
6008
- state: "present",
6009
- verdict: "every ui:// resource emission has a sibling text/markdown fallback in the same content[] (positive-control OK, Server Rule 8)"
6010
- };
6325
+ })();
6326
+ return settle(this.id, decided, unreadable);
6011
6327
  }
6012
6328
  };
6013
6329
 
@@ -6236,7 +6552,7 @@ var detectors = [
6236
6552
 
6237
6553
  // src/runtime/launch.ts
6238
6554
  import { spawnSync as spawnSync2 } from "child_process";
6239
- 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";
6240
6556
  import { dirname as dirname10, isAbsolute, join as join20, resolve } from "path";
6241
6557
  function findPackageDir(target) {
6242
6558
  let dir;
@@ -6271,7 +6587,7 @@ function deriveLaunchPlan(target) {
6271
6587
  }
6272
6588
  let pkg;
6273
6589
  try {
6274
- pkg = JSON.parse(readFileSync18(join20(pkgDir, "package.json"), "utf8"));
6590
+ pkg = JSON.parse(readFileSync11(join20(pkgDir, "package.json"), "utf8"));
6275
6591
  } catch (err) {
6276
6592
  return {
6277
6593
  error: `could not parse ${join20(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
@@ -6355,7 +6671,7 @@ function readEvalCounts(target) {
6355
6671
  const candidate = join20(dir, "evals", "evals.json");
6356
6672
  if (existsSync15(candidate)) {
6357
6673
  try {
6358
- const parsed = JSON.parse(readFileSync18(candidate, "utf8"));
6674
+ const parsed = JSON.parse(readFileSync11(candidate, "utf8"));
6359
6675
  for (const c of parsed.cases ?? []) {
6360
6676
  if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
6361
6677
  }
@@ -6615,7 +6931,7 @@ var USAGE = [
6615
6931
  function readVersion() {
6616
6932
  try {
6617
6933
  const here = dirname11(fileURLToPath(import.meta.url));
6618
- const manifest = JSON.parse(readFileSync19(join21(here, "..", "package.json"), "utf8"));
6934
+ const manifest = JSON.parse(readFileSync12(join21(here, "..", "package.json"), "utf8"));
6619
6935
  return manifest.version ?? null;
6620
6936
  } catch {
6621
6937
  return null;