@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/CHANGELOG.md +56 -0
- package/README.md +68 -7
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.js +880 -564
- package/package.json +9 -3
package/dist/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/doctor.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as 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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
2543
|
-
const lockText =
|
|
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 =
|
|
2575
|
-
const lockText =
|
|
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
|
|
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,
|
|
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
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
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
|
|
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
|
-
|
|
3147
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
4128
|
-
|
|
4129
|
-
const
|
|
4130
|
-
|
|
4131
|
-
|
|
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
|
|
4155
|
-
|
|
4156
|
-
|
|
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: "
|
|
4161
|
-
|
|
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 {
|
|
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
|
|
4254
|
-
|
|
4255
|
-
const
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
parsed =
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
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
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
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
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
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
|
-
|
|
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:
|
|
4290
|
-
state: "
|
|
4291
|
-
verdict:
|
|
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:
|
|
4300
|
-
state: "
|
|
4301
|
-
verdict:
|
|
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 {
|
|
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
|
|
4415
|
-
|
|
4416
|
-
const
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
parsed =
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
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
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
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
|
-
|
|
4677
|
+
const total = manifests.length + sites.length;
|
|
4678
|
+
if (total === 0) {
|
|
4456
4679
|
return {
|
|
4457
4680
|
id: this.id,
|
|
4458
|
-
anchor:
|
|
4459
|
-
state: "
|
|
4460
|
-
verdict: "
|
|
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:
|
|
4469
|
-
state: "
|
|
4470
|
-
verdict:
|
|
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 {
|
|
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
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
const
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
parsed =
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
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
|
-
|
|
4894
|
+
const total = manifests.length + sites.length;
|
|
4895
|
+
if (total === 0) {
|
|
4628
4896
|
return {
|
|
4629
4897
|
id: this.id,
|
|
4630
|
-
anchor:
|
|
4631
|
-
state: "
|
|
4632
|
-
verdict:
|
|
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
|
-
|
|
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:
|
|
4906
|
+
anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
|
|
4656
4907
|
state: "non-conforming",
|
|
4657
|
-
verdict: `
|
|
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: "
|
|
4677
|
-
verdict: `
|
|
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 {
|
|
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
|
-
|
|
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
|
|
4851
|
-
const
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
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
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
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
|
-
|
|
4900
|
-
|
|
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: "
|
|
4905
|
-
verdict:
|
|
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
|
|
5156
|
+
anchor: manifests[0].split("/").slice(-2).join("/"),
|
|
4911
5157
|
state: "present",
|
|
4912
|
-
verdict: `
|
|
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,
|
|
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
|
-
|
|
5007
|
-
|
|
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,
|
|
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
|
-
|
|
5164
|
-
|
|
5165
|
-
const
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
const
|
|
5179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5185
|
-
|
|
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
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
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
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
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,
|
|
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
|
-
|
|
5292
|
-
|
|
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
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
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
|
-
|
|
5306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
5657
|
+
body = readFileSync10(filePath, "utf8");
|
|
5391
5658
|
} catch {
|
|
5392
|
-
|
|
5393
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
5489
|
-
const
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
)
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
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 (
|
|
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:
|
|
5530
|
-
state: "
|
|
5531
|
-
verdict:
|
|
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
|
|
5539
|
-
state: "
|
|
5540
|
-
verdict: "
|
|
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
|
|
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
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
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
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
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
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
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
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
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
|
|
5959
|
-
|
|
5960
|
-
const
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
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
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
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:
|
|
5992
|
-
state: "
|
|
5993
|
-
verdict:
|
|
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:
|
|
6001
|
-
state: "
|
|
6002
|
-
verdict: "
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
6934
|
+
const manifest = JSON.parse(readFileSync12(join21(here, "..", "package.json"), "utf8"));
|
|
6619
6935
|
return manifest.version ?? null;
|
|
6620
6936
|
} catch {
|
|
6621
6937
|
return null;
|