@vantageos/mcp-doctor 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +3 -7
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.js +872 -561
- package/package.json +8 -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",
|
|
@@ -3154,7 +3315,11 @@ var d9 = {
|
|
|
3154
3315
|
}
|
|
3155
3316
|
const { path: pkgPath, json: pkgJson } = tree.packageJson;
|
|
3156
3317
|
if (!pkgJson || Object.keys(pkgJson).length === 0) {
|
|
3157
|
-
|
|
3318
|
+
return couldNotJudge(
|
|
3319
|
+
this.id,
|
|
3320
|
+
pkgPath.split("/").slice(-2).join("/"),
|
|
3321
|
+
`could not parse ${pkgPath} as JSON \u2014 cannot verify SemVer/CHANGELOG`
|
|
3322
|
+
);
|
|
3158
3323
|
}
|
|
3159
3324
|
const version = pkgJson.version;
|
|
3160
3325
|
if (typeof version !== "string") {
|
|
@@ -3193,6 +3358,7 @@ var d9 = {
|
|
|
3193
3358
|
};
|
|
3194
3359
|
}
|
|
3195
3360
|
};
|
|
3361
|
+
var d9 = guardUnreadablePackageJson(d9Inner, { authoritative: false });
|
|
3196
3362
|
|
|
3197
3363
|
// src/detectors/d10.ts
|
|
3198
3364
|
import { existsSync as existsSync9 } from "fs";
|
|
@@ -3244,7 +3410,7 @@ function scanSourceDescriptionFr(tree) {
|
|
|
3244
3410
|
}
|
|
3245
3411
|
return { present: false, anchor: null };
|
|
3246
3412
|
}
|
|
3247
|
-
var
|
|
3413
|
+
var d10Inner = {
|
|
3248
3414
|
id: "D10 bilingual",
|
|
3249
3415
|
rule: "C-1",
|
|
3250
3416
|
pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
|
|
@@ -3315,6 +3481,7 @@ var d10 = {
|
|
|
3315
3481
|
};
|
|
3316
3482
|
}
|
|
3317
3483
|
};
|
|
3484
|
+
var d10 = guardUnreadablePackageJson(d10Inner, { authoritative: true });
|
|
3318
3485
|
|
|
3319
3486
|
// src/detectors/d11.ts
|
|
3320
3487
|
var ZOD_ANY_RE = /\bz\s*\.\s*any\s*\(\s*\)/;
|
|
@@ -3386,6 +3553,27 @@ var d11 = {
|
|
|
3386
3553
|
}
|
|
3387
3554
|
}
|
|
3388
3555
|
if (toolCount === 0) {
|
|
3556
|
+
const located = findRegisteredTools(tree);
|
|
3557
|
+
if (located.length > 0) {
|
|
3558
|
+
toolCount = located.length;
|
|
3559
|
+
firstToolAnchor = located[0].anchor;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
if (toolCount === 0) {
|
|
3563
|
+
const surface = toolSurfaceAnchor(tree);
|
|
3564
|
+
if (surface) {
|
|
3565
|
+
const reason = noToolsLocatedReason(
|
|
3566
|
+
tree,
|
|
3567
|
+
"the outputSchema check cannot be judged without the tool list"
|
|
3568
|
+
);
|
|
3569
|
+
return {
|
|
3570
|
+
id: this.id,
|
|
3571
|
+
anchor: surface,
|
|
3572
|
+
state: "could-not-judge",
|
|
3573
|
+
reason,
|
|
3574
|
+
verdict: `could not judge: ${reason}`
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3389
3577
|
return {
|
|
3390
3578
|
id: this.id,
|
|
3391
3579
|
anchor: "-",
|
|
@@ -3465,7 +3653,7 @@ function findHttpSite(tree) {
|
|
|
3465
3653
|
}
|
|
3466
3654
|
return null;
|
|
3467
3655
|
}
|
|
3468
|
-
var
|
|
3656
|
+
var d12Inner = {
|
|
3469
3657
|
id: "D12 stdio-transport",
|
|
3470
3658
|
run(tree) {
|
|
3471
3659
|
const stdioControl = d12StdioPositiveControl();
|
|
@@ -3516,6 +3704,7 @@ var d12 = {
|
|
|
3516
3704
|
};
|
|
3517
3705
|
}
|
|
3518
3706
|
};
|
|
3707
|
+
var d12 = guardUnreadablePackageJson(d12Inner, { authoritative: false });
|
|
3519
3708
|
|
|
3520
3709
|
// src/detectors/d15.ts
|
|
3521
3710
|
var HANDLE_ASSIGN_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[^;]*\.(?:registerTool|tool)\s*\(/;
|
|
@@ -4129,55 +4318,59 @@ var dDebugOptIn = {
|
|
|
4129
4318
|
);
|
|
4130
4319
|
}
|
|
4131
4320
|
}
|
|
4132
|
-
const
|
|
4133
|
-
|
|
4134
|
-
const
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
const debugCalls = findAll(tree, DEBUG_CALL_RE);
|
|
4140
|
-
if (debugCalls.length === 0) {
|
|
4141
|
-
return {
|
|
4142
|
-
id: this.id,
|
|
4143
|
-
anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
|
|
4144
|
-
state: "absent",
|
|
4145
|
-
verdict: "no debug output emitted (n/a for Server Rule 6 debug-half)"
|
|
4146
|
-
};
|
|
4147
|
-
}
|
|
4148
|
-
const callAnchor = `${debugCalls[0].file.rel}:${debugCalls.map((h) => h.line).join(",")}`;
|
|
4149
|
-
const debugFileRels = new Set(debugCalls.map((h) => h.file.rel));
|
|
4150
|
-
const localGate = firstMatchInFiles2(tree, GATE_RE, debugFileRels);
|
|
4151
|
-
if (localGate) {
|
|
4152
|
-
return {
|
|
4153
|
-
id: this.id,
|
|
4154
|
-
anchor: callAnchor,
|
|
4155
|
-
state: "present",
|
|
4156
|
-
verdict: `debug output at ${callAnchor} gated by an env/config switch in the same file at ${localGate.file.rel}:${localGate.line} (Server Rule 6 debug-half)`
|
|
4157
|
-
};
|
|
4321
|
+
const unreadable = [];
|
|
4322
|
+
for (const h of findAll(tree, COMPUTED_LEVEL_RE)) {
|
|
4323
|
+
const anchor = `${h.file.rel}:${h.line}`;
|
|
4324
|
+
unreadable.push({
|
|
4325
|
+
anchor,
|
|
4326
|
+
reason: `cannot read the level of the computed-level log call at ${anchor} \u2014 a bracket-indexed level hides whether it is debug (Server Rule 6)`
|
|
4327
|
+
});
|
|
4158
4328
|
}
|
|
4159
|
-
const
|
|
4160
|
-
|
|
4161
|
-
|
|
4329
|
+
const decided = (() => {
|
|
4330
|
+
const debugCalls = findAll(tree, DEBUG_CALL_RE);
|
|
4331
|
+
if (debugCalls.length === 0) {
|
|
4332
|
+
return {
|
|
4333
|
+
id: this.id,
|
|
4334
|
+
anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
|
|
4335
|
+
state: "absent",
|
|
4336
|
+
verdict: "no debug output emitted (n/a for Server Rule 6 debug-half)"
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
4339
|
+
const callAnchor = `${debugCalls[0].file.rel}:${debugCalls.map((h) => h.line).join(",")}`;
|
|
4340
|
+
const debugFileRels = new Set(debugCalls.map((h) => h.file.rel));
|
|
4341
|
+
const localGate = firstMatchInFiles2(tree, GATE_RE, debugFileRels);
|
|
4342
|
+
if (localGate) {
|
|
4343
|
+
return {
|
|
4344
|
+
id: this.id,
|
|
4345
|
+
anchor: callAnchor,
|
|
4346
|
+
state: "present",
|
|
4347
|
+
verdict: `debug output at ${callAnchor} gated by an env/config switch in the same file at ${localGate.file.rel}:${localGate.line} (Server Rule 6 debug-half)`
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
const remoteGate = firstMatchInFiles2(tree, GATE_RE, new Set(tree.files.map((f) => f.rel)));
|
|
4351
|
+
if (remoteGate) {
|
|
4352
|
+
const reason = `debug output at ${callAnchor}; an env/config gate exists at ${remoteGate.file.rel}:${remoteGate.line} but in a DIFFERENT file from every debug call \u2014 whether the debug path is opt-in is unreadable statically (Server Rule 6 debug-half)`;
|
|
4353
|
+
return {
|
|
4354
|
+
id: this.id,
|
|
4355
|
+
anchor: callAnchor,
|
|
4356
|
+
state: "could-not-judge",
|
|
4357
|
+
reason,
|
|
4358
|
+
verdict: `could not judge: ${reason}`
|
|
4359
|
+
};
|
|
4360
|
+
}
|
|
4162
4361
|
return {
|
|
4163
4362
|
id: this.id,
|
|
4164
4363
|
anchor: callAnchor,
|
|
4165
|
-
state: "
|
|
4166
|
-
|
|
4167
|
-
verdict: `could not judge: ${reason}`
|
|
4364
|
+
state: "non-conforming",
|
|
4365
|
+
verdict: `debug output at ${callAnchor} with NO env/config gate -> debug is on by default; gate it behind process.env.*DEBUG* or a config switch (Server Rule 6 debug-half)`
|
|
4168
4366
|
};
|
|
4169
|
-
}
|
|
4170
|
-
return
|
|
4171
|
-
id: this.id,
|
|
4172
|
-
anchor: callAnchor,
|
|
4173
|
-
state: "non-conforming",
|
|
4174
|
-
verdict: `debug output at ${callAnchor} with NO env/config gate -> debug is on by default; gate it behind process.env.*DEBUG* or a config switch (Server Rule 6 debug-half)`
|
|
4175
|
-
};
|
|
4367
|
+
})();
|
|
4368
|
+
return settle(this.id, decided, unreadable);
|
|
4176
4369
|
}
|
|
4177
4370
|
};
|
|
4178
4371
|
|
|
4179
4372
|
// src/detectors/dExt1.ts
|
|
4180
|
-
import {
|
|
4373
|
+
import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
4181
4374
|
import { join as join12 } from "path";
|
|
4182
4375
|
var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
|
|
4183
4376
|
var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
|
|
@@ -4255,68 +4448,76 @@ var dExt1 = {
|
|
|
4255
4448
|
`DExt1 identifier control FAILED: /${controls.identifier.pattern}/ accept(\`${IDENTIFIER_POSITIVE}\`)=${controls.identifier.positive} reject(\`${IDENTIFIER_NEGATIVE}\`)=${!controls.identifier.negative} \u2014 matcher broken, verdict untrustworthy`
|
|
4256
4449
|
);
|
|
4257
4450
|
}
|
|
4258
|
-
const
|
|
4259
|
-
|
|
4260
|
-
const
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
parsed =
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4451
|
+
const unreadable = [];
|
|
4452
|
+
const decided = (() => {
|
|
4453
|
+
const manifests = findExtensionManifests(tree.root);
|
|
4454
|
+
for (const manifestPath2 of manifests) {
|
|
4455
|
+
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
4456
|
+
const parsed = readJsonObject(manifestPath2);
|
|
4457
|
+
if (parsed === null) {
|
|
4458
|
+
unreadable.push({
|
|
4459
|
+
anchor: shortPath,
|
|
4460
|
+
reason: `could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable identifier cannot pass (Extension Rule 1)`
|
|
4461
|
+
});
|
|
4462
|
+
continue;
|
|
4463
|
+
}
|
|
4464
|
+
const idValue = parsed.identifier;
|
|
4465
|
+
if (typeof idValue !== "string") {
|
|
4466
|
+
return {
|
|
4467
|
+
id: this.id,
|
|
4468
|
+
anchor: shortPath,
|
|
4469
|
+
state: "non-conforming",
|
|
4470
|
+
verdict: `manifest ${shortPath} has no string "identifier" field (got ${typeof idValue}) -> declare a reverse-DNS identifier <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
|
|
4471
|
+
};
|
|
4472
|
+
}
|
|
4473
|
+
if (!IDENTIFIER_RE2.test(idValue)) {
|
|
4474
|
+
return {
|
|
4475
|
+
id: this.id,
|
|
4476
|
+
anchor: shortPath,
|
|
4477
|
+
state: "non-conforming",
|
|
4478
|
+
verdict: `manifest identifier "${idValue}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
|
|
4479
|
+
};
|
|
4480
|
+
}
|
|
4268
4481
|
}
|
|
4269
|
-
const
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4482
|
+
const { sites, unresolved } = findTsIdentifiers(tree);
|
|
4483
|
+
for (const anchor of unresolved) {
|
|
4484
|
+
unreadable.push({
|
|
4485
|
+
anchor,
|
|
4486
|
+
reason: `cannot read the identifier at ${anchor} \u2014 value is computed/imported/template, reverse-DNS unverifiable (Extension Rule 1)`
|
|
4487
|
+
});
|
|
4274
4488
|
}
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4489
|
+
for (const site of sites) {
|
|
4490
|
+
if (!IDENTIFIER_RE2.test(site.value)) {
|
|
4491
|
+
return {
|
|
4492
|
+
id: this.id,
|
|
4493
|
+
anchor: site.anchor,
|
|
4494
|
+
state: "non-conforming",
|
|
4495
|
+
verdict: `identifier "${site.value}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
|
|
4496
|
+
};
|
|
4497
|
+
}
|
|
4282
4498
|
}
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
if (unresolved.length > 0) {
|
|
4286
|
-
throw new Error(
|
|
4287
|
-
`DExt1 cannot read the identifier at ${unresolved.length} TS site(s): ${unresolved.join(", ")} \u2014 value is computed/imported/template, reverse-DNS unverifiable (Extension Rule 1). Fail-closed, not a silent present.`
|
|
4288
|
-
);
|
|
4289
|
-
}
|
|
4290
|
-
for (const site of sites) {
|
|
4291
|
-
if (!IDENTIFIER_RE2.test(site.value)) {
|
|
4499
|
+
const total = manifests.length + sites.length;
|
|
4500
|
+
if (total === 0) {
|
|
4292
4501
|
return {
|
|
4293
4502
|
id: this.id,
|
|
4294
|
-
anchor:
|
|
4295
|
-
state: "
|
|
4296
|
-
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)"
|
|
4297
4506
|
};
|
|
4298
4507
|
}
|
|
4299
|
-
}
|
|
4300
|
-
const total = manifests.length + sites.length;
|
|
4301
|
-
if (total === 0) {
|
|
4302
4508
|
return {
|
|
4303
4509
|
id: this.id,
|
|
4304
|
-
anchor:
|
|
4305
|
-
state: "
|
|
4306
|
-
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)`
|
|
4307
4513
|
};
|
|
4308
|
-
}
|
|
4309
|
-
return
|
|
4310
|
-
id: this.id,
|
|
4311
|
-
anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
|
|
4312
|
-
state: "present",
|
|
4313
|
-
verdict: `${total} identifier(s) reverse-DNS conforming (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
|
|
4314
|
-
};
|
|
4514
|
+
})();
|
|
4515
|
+
return settle(this.id, decided, unreadable);
|
|
4315
4516
|
}
|
|
4316
4517
|
};
|
|
4317
4518
|
|
|
4318
4519
|
// src/detectors/dExt2.ts
|
|
4319
|
-
import {
|
|
4520
|
+
import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
4320
4521
|
import { join as join13 } from "path";
|
|
4321
4522
|
var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
|
|
4322
4523
|
var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
|
|
@@ -4416,76 +4617,85 @@ var dExt2 = {
|
|
|
4416
4617
|
`DExt2 graceful-degradation control FAILED: accept(\`${GD_POSITIVE}\`)=${controls.gracefulDegradation.acceptsTrue} reject(\`${GD_NEGATIVE}\`)=${controls.gracefulDegradation.rejectsFalse} \u2014 matcher broken, verdict untrustworthy`
|
|
4417
4618
|
);
|
|
4418
4619
|
}
|
|
4419
|
-
const
|
|
4420
|
-
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
parsed =
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
`DExt2 cannot read the gracefulDegradation value at ${site.anchor} (got \`${site.value}\`) \u2014 computed/imported/template, graceful degradation unverifiable (Extension Rule 2). Fail-closed, not a silent present.`
|
|
4450
|
-
);
|
|
4620
|
+
const unreadable = [];
|
|
4621
|
+
const decided = (() => {
|
|
4622
|
+
const manifests = findExtensionManifests2(tree.root);
|
|
4623
|
+
for (const manifestPath2 of manifests) {
|
|
4624
|
+
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
4625
|
+
const parsed = readJsonObject(manifestPath2);
|
|
4626
|
+
if (parsed === null) {
|
|
4627
|
+
unreadable.push({
|
|
4628
|
+
anchor: shortPath,
|
|
4629
|
+
reason: `could not parse ${shortPath} as JSON \u2014 a present manifest silent on graceful degradation cannot pass (Extension Rule 2)`
|
|
4630
|
+
});
|
|
4631
|
+
continue;
|
|
4632
|
+
}
|
|
4633
|
+
if (!("graceful_degradation" in parsed)) {
|
|
4634
|
+
return {
|
|
4635
|
+
id: this.id,
|
|
4636
|
+
anchor: shortPath,
|
|
4637
|
+
state: "non-conforming",
|
|
4638
|
+
verdict: `manifest ${shortPath} has no "graceful_degradation" field -> set "graceful_degradation": true, an extension never breaks the host server (Extension Rule 2)`
|
|
4639
|
+
};
|
|
4640
|
+
}
|
|
4641
|
+
const gdValue = parsed.graceful_degradation;
|
|
4642
|
+
if (gdValue !== true) {
|
|
4643
|
+
return {
|
|
4644
|
+
id: this.id,
|
|
4645
|
+
anchor: shortPath,
|
|
4646
|
+
state: "non-conforming",
|
|
4647
|
+
verdict: `manifest "graceful_degradation" is ${JSON.stringify(gdValue)}, not true (positive-control OK) -> set "graceful_degradation": true, an extension never breaks the host server (Extension Rule 2)`
|
|
4648
|
+
};
|
|
4649
|
+
}
|
|
4451
4650
|
}
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4651
|
+
const sites = findTsFactorySites(tree);
|
|
4652
|
+
for (const site of sites) {
|
|
4653
|
+
if (site.state === "unresolved") {
|
|
4654
|
+
unreadable.push({
|
|
4655
|
+
anchor: site.anchor,
|
|
4656
|
+
reason: `cannot read the gracefulDegradation value at ${site.anchor} (got \`${site.value}\`) \u2014 computed/imported/template, graceful degradation unverifiable (Extension Rule 2)`
|
|
4657
|
+
});
|
|
4658
|
+
continue;
|
|
4659
|
+
}
|
|
4660
|
+
if (site.state === "false") {
|
|
4661
|
+
return {
|
|
4662
|
+
id: this.id,
|
|
4663
|
+
anchor: site.anchor,
|
|
4664
|
+
state: "non-conforming",
|
|
4665
|
+
verdict: "gracefulDegradation is false, not true (positive-control OK) -> an extension never breaks the host server (Extension Rule 2)"
|
|
4666
|
+
};
|
|
4667
|
+
}
|
|
4668
|
+
if (site.state === "absent") {
|
|
4669
|
+
return {
|
|
4670
|
+
id: this.id,
|
|
4671
|
+
anchor: site.anchor,
|
|
4672
|
+
state: "non-conforming",
|
|
4673
|
+
verdict: "defineExtension block has no gracefulDegradation flag (positive-control OK) -> declare gracefulDegradation: true (Extension Rule 2)"
|
|
4674
|
+
};
|
|
4675
|
+
}
|
|
4459
4676
|
}
|
|
4460
|
-
|
|
4677
|
+
const total = manifests.length + sites.length;
|
|
4678
|
+
if (total === 0) {
|
|
4461
4679
|
return {
|
|
4462
4680
|
id: this.id,
|
|
4463
|
-
anchor:
|
|
4464
|
-
state: "
|
|
4465
|
-
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)"
|
|
4466
4684
|
};
|
|
4467
4685
|
}
|
|
4468
|
-
}
|
|
4469
|
-
const total = manifests.length + sites.length;
|
|
4470
|
-
if (total === 0) {
|
|
4471
4686
|
return {
|
|
4472
4687
|
id: this.id,
|
|
4473
|
-
anchor:
|
|
4474
|
-
state: "
|
|
4475
|
-
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)`
|
|
4476
4691
|
};
|
|
4477
|
-
}
|
|
4478
|
-
return
|
|
4479
|
-
id: this.id,
|
|
4480
|
-
anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
|
|
4481
|
-
state: "present",
|
|
4482
|
-
verdict: `${total} graceful-degradation flag(s) true (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
|
|
4483
|
-
};
|
|
4692
|
+
})();
|
|
4693
|
+
return settle(this.id, decided, unreadable);
|
|
4484
4694
|
}
|
|
4485
4695
|
};
|
|
4486
4696
|
|
|
4487
4697
|
// src/detectors/dExt3.ts
|
|
4488
|
-
import {
|
|
4698
|
+
import { readdirSync as readdirSync6, statSync as statSync7 } from "fs";
|
|
4489
4699
|
import { join as join14 } from "path";
|
|
4490
4700
|
var TYPE_MATCHERS = [
|
|
4491
4701
|
{ type: "middleware", re: /\bmiddleware\b/ },
|
|
@@ -4599,100 +4809,118 @@ var dExt3 = {
|
|
|
4599
4809
|
`DExt3 single-type control FAILED: detectMix(\`${TYPE_MIXED}\`)=${controls.type.detectsMix} detectSingle(\`${TYPE_SINGLE}\`)=${controls.type.detectsSingle} \u2014 matcher broken, verdict untrustworthy`
|
|
4600
4810
|
);
|
|
4601
4811
|
}
|
|
4602
|
-
const
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
const
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
parsed =
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4812
|
+
const unreadable = [];
|
|
4813
|
+
const decided = (() => {
|
|
4814
|
+
const declared = /* @__PURE__ */ new Set();
|
|
4815
|
+
let firstAnchor2 = null;
|
|
4816
|
+
const manifests = findExtensionManifests3(tree.root);
|
|
4817
|
+
for (const manifestPath2 of manifests) {
|
|
4818
|
+
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
4819
|
+
const parsed = readJsonObject(manifestPath2);
|
|
4820
|
+
if (parsed === null) {
|
|
4821
|
+
unreadable.push({
|
|
4822
|
+
anchor: shortPath,
|
|
4823
|
+
reason: `could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable type cannot pass (Extension Rule 3)`
|
|
4824
|
+
});
|
|
4825
|
+
continue;
|
|
4826
|
+
}
|
|
4827
|
+
if (!("type" in parsed)) {
|
|
4828
|
+
return {
|
|
4829
|
+
id: this.id,
|
|
4830
|
+
anchor: shortPath,
|
|
4831
|
+
state: "non-conforming",
|
|
4832
|
+
verdict: `manifest ${shortPath} has no "type" field -> declare exactly one type (middleware|tool-extension|transport) (Extension Rule 3)`
|
|
4833
|
+
};
|
|
4834
|
+
}
|
|
4835
|
+
const rawValue = coerceTypeValue(parsed.type);
|
|
4836
|
+
if (rawValue === null) {
|
|
4837
|
+
return {
|
|
4838
|
+
id: this.id,
|
|
4839
|
+
anchor: shortPath,
|
|
4840
|
+
state: "non-conforming",
|
|
4841
|
+
verdict: `manifest ${shortPath} "type" is neither string nor string[] (got ${typeof parsed.type}) -> declare exactly one type (middleware|tool-extension|transport) (Extension Rule 3)`
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
const found = typesIn(rawValue);
|
|
4845
|
+
if (found.length === 0) {
|
|
4846
|
+
return {
|
|
4847
|
+
id: this.id,
|
|
4848
|
+
anchor: shortPath,
|
|
4849
|
+
state: "non-conforming",
|
|
4850
|
+
verdict: `manifest ${shortPath} "type" value ${JSON.stringify(parsed.type)} names no known category (middleware|tool-extension|transport) -> declare exactly one known type (Extension Rule 3)`
|
|
4851
|
+
};
|
|
4852
|
+
}
|
|
4853
|
+
if (found.length > 1) {
|
|
4854
|
+
return {
|
|
4855
|
+
id: this.id,
|
|
4856
|
+
anchor: shortPath,
|
|
4857
|
+
state: "non-conforming",
|
|
4858
|
+
verdict: `manifest "type" ${JSON.stringify(parsed.type)} mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type, split into ${found.length} extensions (Extension Rule 3)`
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
4861
|
+
firstAnchor2 ??= shortPath;
|
|
4862
|
+
declared.add(found[0]);
|
|
4863
|
+
}
|
|
4864
|
+
const sites = findTsTypeSites(tree);
|
|
4865
|
+
for (const site of sites) {
|
|
4866
|
+
if (site.unresolved) {
|
|
4867
|
+
unreadable.push({
|
|
4868
|
+
anchor: site.anchor,
|
|
4869
|
+
reason: `cannot read the type at ${site.anchor}${site.value === void 0 ? " (no `type:` literal in the defineExtension block)" : ` (got \`${site.value}\`)`} \u2014 computed/imported/template or absent, single-type unverifiable (Extension Rule 3)`
|
|
4870
|
+
});
|
|
4871
|
+
continue;
|
|
4872
|
+
}
|
|
4873
|
+
const raw = site.value ?? "";
|
|
4874
|
+
const found = typesIn(raw);
|
|
4875
|
+
if (found.length === 0) {
|
|
4876
|
+
return {
|
|
4877
|
+
id: this.id,
|
|
4878
|
+
anchor: site.anchor,
|
|
4879
|
+
state: "non-conforming",
|
|
4880
|
+
verdict: `defineExtension type: "${raw}" at ${site.anchor} names no known category (middleware|tool-extension|transport) -> declare exactly one known type (Extension Rule 3)`
|
|
4881
|
+
};
|
|
4882
|
+
}
|
|
4883
|
+
if (found.length > 1) {
|
|
4884
|
+
return {
|
|
4885
|
+
id: this.id,
|
|
4886
|
+
anchor: site.anchor,
|
|
4887
|
+
state: "non-conforming",
|
|
4888
|
+
verdict: `type: "${raw}" mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
|
|
4889
|
+
};
|
|
4890
|
+
}
|
|
4891
|
+
firstAnchor2 ??= site.anchor;
|
|
4892
|
+
declared.add(found[0]);
|
|
4631
4893
|
}
|
|
4632
|
-
|
|
4894
|
+
const total = manifests.length + sites.length;
|
|
4895
|
+
if (total === 0) {
|
|
4633
4896
|
return {
|
|
4634
4897
|
id: this.id,
|
|
4635
|
-
anchor:
|
|
4636
|
-
state: "
|
|
4637
|
-
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)"
|
|
4638
4901
|
};
|
|
4639
4902
|
}
|
|
4640
|
-
|
|
4641
|
-
declared.add(found[0]);
|
|
4642
|
-
}
|
|
4643
|
-
const sites = findTsTypeSites(tree);
|
|
4644
|
-
for (const site of sites) {
|
|
4645
|
-
if (site.unresolved) {
|
|
4646
|
-
throw new Error(
|
|
4647
|
-
`DExt3 cannot read the type at ${site.anchor}${site.value === void 0 ? " (no `type:` literal in the defineExtension block)" : ` (got \`${site.value}\`)`} \u2014 computed/imported/template or absent, single-type unverifiable (Extension Rule 3). Fail-closed, not a silent present.`
|
|
4648
|
-
);
|
|
4649
|
-
}
|
|
4650
|
-
const raw = site.value ?? "";
|
|
4651
|
-
const found = typesIn(raw);
|
|
4652
|
-
if (found.length === 0) {
|
|
4653
|
-
throw new Error(
|
|
4654
|
-
`DExt3 found a defineExtension type: "${raw}" at ${site.anchor} naming no known category (middleware|tool-extension|transport) \u2014 type undeterminable (Extension Rule 3). Fail-closed, not a silent present.`
|
|
4655
|
-
);
|
|
4656
|
-
}
|
|
4657
|
-
if (found.length > 1) {
|
|
4903
|
+
if (declared.size > 1) {
|
|
4658
4904
|
return {
|
|
4659
4905
|
id: this.id,
|
|
4660
|
-
anchor:
|
|
4906
|
+
anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
|
|
4661
4907
|
state: "non-conforming",
|
|
4662
|
-
verdict: `
|
|
4908
|
+
verdict: `extension declares ${declared.size} types across its forms: ${[...declared].join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
|
|
4663
4909
|
};
|
|
4664
4910
|
}
|
|
4665
|
-
firstAnchor2 ??= site.anchor;
|
|
4666
|
-
declared.add(found[0]);
|
|
4667
|
-
}
|
|
4668
|
-
const total = manifests.length + sites.length;
|
|
4669
|
-
if (total === 0) {
|
|
4670
|
-
return {
|
|
4671
|
-
id: this.id,
|
|
4672
|
-
anchor: tree.root.split("/").pop() ?? tree.root,
|
|
4673
|
-
state: "absent",
|
|
4674
|
-
verdict: "no extension type declared (no extension.json manifest, no defineExtension `type:` literal) -> declare exactly one type (Extension Rule 3)"
|
|
4675
|
-
};
|
|
4676
|
-
}
|
|
4677
|
-
if (declared.size > 1) {
|
|
4678
4911
|
return {
|
|
4679
4912
|
id: this.id,
|
|
4680
4913
|
anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
|
|
4681
|
-
state: "
|
|
4682
|
-
verdict: `
|
|
4914
|
+
state: "present",
|
|
4915
|
+
verdict: `single type "${[...declared][0]}" declared across ${total} form(s) (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
|
|
4683
4916
|
};
|
|
4684
|
-
}
|
|
4685
|
-
return
|
|
4686
|
-
id: this.id,
|
|
4687
|
-
anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
|
|
4688
|
-
state: "present",
|
|
4689
|
-
verdict: `single type "${[...declared][0]}" declared across ${total} form(s) (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
|
|
4690
|
-
};
|
|
4917
|
+
})();
|
|
4918
|
+
return settle(this.id, decided, unreadable);
|
|
4691
4919
|
}
|
|
4692
4920
|
};
|
|
4693
4921
|
|
|
4694
4922
|
// src/detectors/dExt4.ts
|
|
4695
|
-
import {
|
|
4923
|
+
import { readdirSync as readdirSync7, statSync as statSync8 } from "fs";
|
|
4696
4924
|
import { join as join15 } from "path";
|
|
4697
4925
|
function canonical(name) {
|
|
4698
4926
|
return name.replace(/[-_]/g, "").toLowerCase();
|
|
@@ -4836,7 +5064,7 @@ function findFactorySites(tree) {
|
|
|
4836
5064
|
function manifestHookTokens(raw) {
|
|
4837
5065
|
const set = /* @__PURE__ */ new Set();
|
|
4838
5066
|
if (!Array.isArray(raw)) {
|
|
4839
|
-
|
|
5067
|
+
return null;
|
|
4840
5068
|
}
|
|
4841
5069
|
for (const h of raw) {
|
|
4842
5070
|
if (typeof h === "string") set.add(canonical(h));
|
|
@@ -4852,82 +5080,90 @@ var dExt4 = {
|
|
|
4852
5080
|
`DExt4 lifecycle-hooks control FAILED: unify(${HOOK_SNAKE}/${HOOK_KEBAB}/${HOOK_CAMEL})=${controls.hooks.unifiesSpellings} distinct(${HOOK_SNAKE}!=${HOOK_OTHER})=${controls.hooks.keepsDistinct} \u2014 normaliser broken, verdict untrustworthy`
|
|
4853
5081
|
);
|
|
4854
5082
|
}
|
|
4855
|
-
const
|
|
4856
|
-
const
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
for (const s of sites) for (const t of s.tokens) implemented.add(t);
|
|
4865
|
-
const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
|
|
4866
|
-
const manifests = findExtensionManifests4(tree.root);
|
|
4867
|
-
let declaredAnyLifecycle = false;
|
|
4868
|
-
for (const manifestPath2 of manifests) {
|
|
4869
|
-
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
4870
|
-
let parsed;
|
|
4871
|
-
try {
|
|
4872
|
-
parsed = JSON.parse(readFileSync13(manifestPath2, "utf8"));
|
|
4873
|
-
} catch {
|
|
4874
|
-
throw new Error(
|
|
4875
|
-
`DExt4 could not parse ${shortPath} as JSON \u2014 a present manifest whose lifecycle cannot be read cannot pass (Extension Rule 4). Fail-closed, not a silent present.`
|
|
4876
|
-
);
|
|
4877
|
-
}
|
|
4878
|
-
if (!("lifecycle" in parsed)) continue;
|
|
4879
|
-
let declared;
|
|
4880
|
-
try {
|
|
4881
|
-
declared = manifestHookTokens(parsed.lifecycle);
|
|
4882
|
-
} catch {
|
|
4883
|
-
throw new Error(
|
|
4884
|
-
`DExt4 found a present manifest ${shortPath} whose "lifecycle" is not an array (got ${typeof parsed.lifecycle}) \u2014 the declared hooks are undeterminable (Extension Rule 4). Fail-closed, not a silent present.`
|
|
4885
|
-
);
|
|
4886
|
-
}
|
|
4887
|
-
if (declared.size === 0) continue;
|
|
4888
|
-
declaredAnyLifecycle = true;
|
|
4889
|
-
if (!implSurfacePresent) {
|
|
4890
|
-
throw new Error(
|
|
4891
|
-
`DExt4 found a present manifest ${shortPath} declaring lifecycle hooks while NO implementation surface exists (no hooks/ directory, no factory lifecycle object) \u2014 the parity is undeterminable (Extension Rule 4). Fail-closed, not a silent present.`
|
|
4892
|
-
);
|
|
5083
|
+
const unreadable = [];
|
|
5084
|
+
const decided = (() => {
|
|
5085
|
+
const hooksDirs = findHooksDirs(tree.root);
|
|
5086
|
+
const { sites, unresolved } = findFactorySites(tree);
|
|
5087
|
+
for (const anchor of unresolved) {
|
|
5088
|
+
unreadable.push({
|
|
5089
|
+
anchor,
|
|
5090
|
+
reason: `cannot read the lifecycle implementation at ${anchor} \u2014 value is computed/imported/template, not an object literal, so the implemented hooks are unverifiable (Extension Rule 4)`
|
|
5091
|
+
});
|
|
4893
5092
|
}
|
|
4894
|
-
const
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
5093
|
+
const implemented = /* @__PURE__ */ new Set();
|
|
5094
|
+
for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
|
|
5095
|
+
for (const s of sites) for (const t of s.tokens) implemented.add(t);
|
|
5096
|
+
const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
|
|
5097
|
+
const manifests = findExtensionManifests4(tree.root);
|
|
5098
|
+
let declaredAnyLifecycle = false;
|
|
5099
|
+
for (const manifestPath2 of manifests) {
|
|
5100
|
+
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
5101
|
+
const parsed = readJsonObject(manifestPath2);
|
|
5102
|
+
if (parsed === null) {
|
|
5103
|
+
unreadable.push({
|
|
5104
|
+
anchor: shortPath,
|
|
5105
|
+
reason: `could not parse ${shortPath} as JSON \u2014 a present manifest whose lifecycle cannot be read cannot pass (Extension Rule 4)`
|
|
5106
|
+
});
|
|
5107
|
+
continue;
|
|
5108
|
+
}
|
|
5109
|
+
if (!("lifecycle" in parsed)) continue;
|
|
5110
|
+
const declared = manifestHookTokens(parsed.lifecycle);
|
|
5111
|
+
if (declared === null) {
|
|
5112
|
+
return {
|
|
5113
|
+
id: this.id,
|
|
5114
|
+
anchor: shortPath,
|
|
5115
|
+
state: "non-conforming",
|
|
5116
|
+
verdict: `manifest ${shortPath} "lifecycle" is not an array (got ${typeof parsed.lifecycle}) -> declare the hooks as an array of hook names (Extension Rule 4)`
|
|
5117
|
+
};
|
|
5118
|
+
}
|
|
5119
|
+
if (declared.size === 0) continue;
|
|
5120
|
+
declaredAnyLifecycle = true;
|
|
5121
|
+
if (!implSurfacePresent) {
|
|
5122
|
+
unreadable.push({
|
|
5123
|
+
anchor: shortPath,
|
|
5124
|
+
reason: `present manifest ${shortPath} declares lifecycle hooks while NO implementation surface exists (no hooks/ directory, no factory lifecycle object) \u2014 the parity is undeterminable (Extension Rule 4)`
|
|
5125
|
+
});
|
|
5126
|
+
continue;
|
|
5127
|
+
}
|
|
5128
|
+
const phantom = [...declared].filter((h) => !implemented.has(h));
|
|
5129
|
+
if (phantom.length > 0) {
|
|
5130
|
+
return {
|
|
5131
|
+
id: this.id,
|
|
5132
|
+
anchor: shortPath,
|
|
5133
|
+
state: "non-conforming",
|
|
5134
|
+
verdict: `manifest "lifecycle" declares hook(s) [${phantom.join(", ")}] with no implementation (implemented: [${[...implemented].join(", ") || "none"}], positive-control OK) -> a declared hook without implementation is a phantom hook (Extension Rule 4)`
|
|
5135
|
+
};
|
|
5136
|
+
}
|
|
4902
5137
|
}
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
5138
|
+
if (!declaredAnyLifecycle) {
|
|
5139
|
+
if (manifests.length === 0 && !implSurfacePresent) {
|
|
5140
|
+
return {
|
|
5141
|
+
id: this.id,
|
|
5142
|
+
anchor: tree.root.split("/").pop() ?? tree.root,
|
|
5143
|
+
state: "absent",
|
|
5144
|
+
verdict: "no lifecycle hooks declared (no extension.json manifest `lifecycle` array) -> declare the hooks the extension implements (Extension Rule 4)"
|
|
5145
|
+
};
|
|
5146
|
+
}
|
|
4906
5147
|
return {
|
|
4907
5148
|
id: this.id,
|
|
4908
|
-
anchor: tree.root.split("/").pop() ?? tree.root,
|
|
4909
|
-
state: "
|
|
4910
|
-
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)`
|
|
4911
5152
|
};
|
|
4912
5153
|
}
|
|
4913
5154
|
return {
|
|
4914
5155
|
id: this.id,
|
|
4915
|
-
anchor: manifests
|
|
5156
|
+
anchor: manifests[0].split("/").slice(-2).join("/"),
|
|
4916
5157
|
state: "present",
|
|
4917
|
-
verdict: `
|
|
5158
|
+
verdict: `every declared lifecycle hook is implemented (declared \u2286 implemented: [${[...implemented].join(", ")}], hooksDir=${hooksDirs.length}, factory=${sites.length}, positive-control OK)`
|
|
4918
5159
|
};
|
|
4919
|
-
}
|
|
4920
|
-
return
|
|
4921
|
-
id: this.id,
|
|
4922
|
-
anchor: manifests[0].split("/").slice(-2).join("/"),
|
|
4923
|
-
state: "present",
|
|
4924
|
-
verdict: `every declared lifecycle hook is implemented (declared \u2286 implemented: [${[...implemented].join(", ")}], hooksDir=${hooksDirs.length}, factory=${sites.length}, positive-control OK)`
|
|
4925
|
-
};
|
|
5160
|
+
})();
|
|
5161
|
+
return settle(this.id, decided, unreadable);
|
|
4926
5162
|
}
|
|
4927
5163
|
};
|
|
4928
5164
|
|
|
4929
5165
|
// src/detectors/dExt5.ts
|
|
4930
|
-
import { existsSync as existsSync12,
|
|
5166
|
+
import { existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync9 } from "fs";
|
|
4931
5167
|
import { dirname as dirname7, join as join16 } from "path";
|
|
4932
5168
|
function nonEmptyString(v) {
|
|
4933
5169
|
return typeof v === "string" && v.trim().length > 0;
|
|
@@ -5008,10 +5244,8 @@ var dExt5 = {
|
|
|
5008
5244
|
reason,
|
|
5009
5245
|
verdict: `could not judge: ${reason}`
|
|
5010
5246
|
});
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
parsed = JSON.parse(readFileSync14(manifestPath2, "utf8"));
|
|
5014
|
-
} catch {
|
|
5247
|
+
const parsed = readJsonObject(manifestPath2);
|
|
5248
|
+
if (parsed === null) {
|
|
5015
5249
|
return cannotRead(
|
|
5016
5250
|
`${shortPath} does not parse as JSON \u2014 the FR surface of a present manifest cannot be read (Extension Rule 5)`
|
|
5017
5251
|
);
|
|
@@ -5068,7 +5302,7 @@ var dExt5 = {
|
|
|
5068
5302
|
};
|
|
5069
5303
|
|
|
5070
5304
|
// src/detectors/dExt7.ts
|
|
5071
|
-
import { existsSync as existsSync13,
|
|
5305
|
+
import { existsSync as existsSync13, readdirSync as readdirSync9, statSync as statSync10 } from "fs";
|
|
5072
5306
|
import { dirname as dirname8, join as join17 } from "path";
|
|
5073
5307
|
var ABSENCE_FLAG_FIELDS = [
|
|
5074
5308
|
"extensionAbsent",
|
|
@@ -5165,65 +5399,78 @@ var dExt7 = {
|
|
|
5165
5399
|
verdict: "no extension.json manifest found -> declare an extension with an eval suite (>=3 cases incl. 1 'extension absente') (Extension Rule 7)"
|
|
5166
5400
|
};
|
|
5167
5401
|
}
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
const
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
const
|
|
5184
|
-
|
|
5402
|
+
const unreadable = [];
|
|
5403
|
+
const decided = (() => {
|
|
5404
|
+
for (const manifestPath2 of manifests) {
|
|
5405
|
+
const extDir = dirname8(manifestPath2);
|
|
5406
|
+
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
5407
|
+
const evalsPath = join17(extDir, "evals", "evals.json");
|
|
5408
|
+
const shortPath = `${extLabel}/evals/evals.json`;
|
|
5409
|
+
if (!existsSync13(evalsPath)) {
|
|
5410
|
+
return {
|
|
5411
|
+
id: this.id,
|
|
5412
|
+
anchor: `${extLabel}/extension.json`,
|
|
5413
|
+
state: "non-conforming",
|
|
5414
|
+
verdict: `no evals/evals.json for extension "${extLabel}" -> add an eval suite with >=3 cases incl. 1 'extension absente' (Extension Rule 7)`
|
|
5415
|
+
};
|
|
5416
|
+
}
|
|
5417
|
+
const parsed = readJsonObject(evalsPath);
|
|
5418
|
+
if (parsed === null) {
|
|
5419
|
+
unreadable.push({
|
|
5420
|
+
anchor: shortPath,
|
|
5421
|
+
reason: `could not parse ${shortPath}: not readable as a JSON object \u2014 a present eval surface that cannot be read cannot pass (Extension Rule 7)`
|
|
5422
|
+
});
|
|
5423
|
+
continue;
|
|
5424
|
+
}
|
|
5185
5425
|
if (!Array.isArray(parsed.cases)) {
|
|
5186
|
-
|
|
5426
|
+
return {
|
|
5427
|
+
id: this.id,
|
|
5428
|
+
anchor: shortPath,
|
|
5429
|
+
state: "non-conforming",
|
|
5430
|
+
verdict: `${shortPath} "cases" is not an array (got ${typeof parsed.cases}) -> declare cases as an array of eval cases (Extension Rule 7)`
|
|
5431
|
+
};
|
|
5187
5432
|
}
|
|
5188
|
-
cases = parsed.cases;
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
`DExt7 could not parse ${shortPath}: ${err instanceof Error ? err.message : String(err)} \u2014 a present eval surface that cannot be read cannot pass (Extension Rule 7). Fail-closed, not a silent present.`
|
|
5433
|
+
const cases = parsed.cases;
|
|
5434
|
+
const badCase = cases.findIndex(
|
|
5435
|
+
(c) => c === null || typeof c !== "object" || Array.isArray(c)
|
|
5192
5436
|
);
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5437
|
+
if (badCase !== -1) {
|
|
5438
|
+
const c = cases[badCase];
|
|
5439
|
+
return {
|
|
5440
|
+
id: this.id,
|
|
5441
|
+
anchor: shortPath,
|
|
5442
|
+
state: "non-conforming",
|
|
5443
|
+
verdict: `${shortPath} cases[${badCase}] is not an object (got ${c === null ? "null" : Array.isArray(c) ? "array" : typeof c}) -> every eval case must be an object (Extension Rule 7)`
|
|
5444
|
+
};
|
|
5445
|
+
}
|
|
5446
|
+
const count = cases.length;
|
|
5447
|
+
const hasAbsence = cases.some(isAbsenceCase);
|
|
5448
|
+
const failures = [];
|
|
5449
|
+
if (count < 3) failures.push(`only ${count} case(s) (need >=3)`);
|
|
5450
|
+
if (!hasAbsence) failures.push("no 'extension absente'/degradation case");
|
|
5451
|
+
if (failures.length > 0) {
|
|
5452
|
+
return {
|
|
5453
|
+
id: this.id,
|
|
5454
|
+
anchor: `${shortPath}`,
|
|
5455
|
+
state: "non-conforming",
|
|
5456
|
+
verdict: `eval suite for "${extLabel}": ${failures.join("; ")} (positive-control OK) -> Extension Rule 7 needs >=3 cases incl. 1 'extension absente'`
|
|
5457
|
+
};
|
|
5199
5458
|
}
|
|
5200
|
-
});
|
|
5201
|
-
const count = cases.length;
|
|
5202
|
-
const hasAbsence = cases.some(isAbsenceCase);
|
|
5203
|
-
const failures = [];
|
|
5204
|
-
if (count < 3) failures.push(`only ${count} case(s) (need >=3)`);
|
|
5205
|
-
if (!hasAbsence) failures.push("no 'extension absente'/degradation case");
|
|
5206
|
-
if (failures.length > 0) {
|
|
5207
|
-
return {
|
|
5208
|
-
id: this.id,
|
|
5209
|
-
anchor: `${shortPath}`,
|
|
5210
|
-
state: "non-conforming",
|
|
5211
|
-
verdict: `eval suite for "${extLabel}": ${failures.join("; ")} (positive-control OK) -> Extension Rule 7 needs >=3 cases incl. 1 'extension absente'`
|
|
5212
|
-
};
|
|
5213
5459
|
}
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
};
|
|
5460
|
+
const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
|
|
5461
|
+
return {
|
|
5462
|
+
id: this.id,
|
|
5463
|
+
anchor: `${firstLabel}/evals/evals.json`,
|
|
5464
|
+
state: "present",
|
|
5465
|
+
verdict: `eval suite complete across ${manifests.length} extension(s): >=3 cases incl. an 'extension absente'/degradation case (positive-control OK)`
|
|
5466
|
+
};
|
|
5467
|
+
})();
|
|
5468
|
+
return settle(this.id, decided, unreadable);
|
|
5222
5469
|
}
|
|
5223
5470
|
};
|
|
5224
5471
|
|
|
5225
5472
|
// src/detectors/dExt8.ts
|
|
5226
|
-
import { existsSync as existsSync14,
|
|
5473
|
+
import { existsSync as existsSync14, readdirSync as readdirSync10, statSync as statSync11 } from "fs";
|
|
5227
5474
|
import { dirname as dirname9, join as join18 } from "path";
|
|
5228
5475
|
var SEMVER_RE2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
5229
5476
|
var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
|
|
@@ -5287,73 +5534,88 @@ var dExt8 = {
|
|
|
5287
5534
|
verdict: "no extension.json manifest found -> declare an extension with a valid SemVer version + CHANGELOG.md (Extension Rule 8)"
|
|
5288
5535
|
};
|
|
5289
5536
|
}
|
|
5537
|
+
const unreadable = [];
|
|
5538
|
+
let finding;
|
|
5539
|
+
let judgedConforming = 0;
|
|
5290
5540
|
for (const manifestPath2 of manifests) {
|
|
5291
5541
|
const extDir = dirname9(manifestPath2);
|
|
5292
5542
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
5293
5543
|
const pkgPath = join18(extDir, "package.json");
|
|
5294
5544
|
const shortPkgPath = `${extLabel}/package.json`;
|
|
5295
5545
|
if (!existsSync14(pkgPath)) {
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5546
|
+
unreadable.push({
|
|
5547
|
+
anchor: shortPkgPath,
|
|
5548
|
+
reason: `could not locate ${shortPkgPath} for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8)`
|
|
5549
|
+
});
|
|
5550
|
+
continue;
|
|
5299
5551
|
}
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5552
|
+
const pkgJson = readJsonObject(pkgPath);
|
|
5553
|
+
if (pkgJson === null) {
|
|
5554
|
+
unreadable.push({
|
|
5555
|
+
anchor: shortPkgPath,
|
|
5556
|
+
reason: `could not parse ${shortPkgPath} as JSON for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8)`
|
|
5557
|
+
});
|
|
5558
|
+
continue;
|
|
5307
5559
|
}
|
|
5308
5560
|
const version = pkgJson.version;
|
|
5309
5561
|
if ("version" in pkgJson && typeof version !== "string") {
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5562
|
+
finding ??= {
|
|
5563
|
+
id: this.id,
|
|
5564
|
+
anchor: shortPkgPath,
|
|
5565
|
+
state: "non-conforming",
|
|
5566
|
+
verdict: `"version" in ${shortPkgPath} is not a string (got ${typeof version}) for extension "${extLabel}" -> set a valid SemVer version string (Extension Rule 8)`
|
|
5567
|
+
};
|
|
5568
|
+
continue;
|
|
5313
5569
|
}
|
|
5314
5570
|
if (typeof version !== "string" || version.trim().length === 0) {
|
|
5315
|
-
|
|
5571
|
+
finding ??= {
|
|
5316
5572
|
id: this.id,
|
|
5317
5573
|
anchor: shortPkgPath,
|
|
5318
5574
|
state: "non-conforming",
|
|
5319
5575
|
verdict: `no "version" field in ${shortPkgPath} for extension "${extLabel}" -> set a valid SemVer version (Extension Rule 8)`
|
|
5320
5576
|
};
|
|
5577
|
+
continue;
|
|
5321
5578
|
}
|
|
5322
5579
|
const validSemver = SEMVER_RE2.test(version);
|
|
5323
5580
|
const changelogPath = join18(extDir, "CHANGELOG.md");
|
|
5324
5581
|
const hasChangelog = existsSync14(changelogPath);
|
|
5325
5582
|
if (!validSemver) {
|
|
5326
|
-
|
|
5583
|
+
finding ??= {
|
|
5327
5584
|
id: this.id,
|
|
5328
5585
|
anchor: shortPkgPath,
|
|
5329
5586
|
state: "non-conforming",
|
|
5330
5587
|
verdict: `"version": "${version}" is not valid SemVer for extension "${extLabel}" (positive-control OK) -> use MAJOR.MINOR.PATCH (Extension Rule 8)`
|
|
5331
5588
|
};
|
|
5589
|
+
continue;
|
|
5332
5590
|
}
|
|
5333
5591
|
if (!hasChangelog) {
|
|
5334
|
-
|
|
5592
|
+
finding ??= {
|
|
5335
5593
|
id: this.id,
|
|
5336
5594
|
anchor: shortPkgPath,
|
|
5337
5595
|
state: "absent",
|
|
5338
5596
|
verdict: `no CHANGELOG.md next to ${shortPkgPath} for extension "${extLabel}" (positive-control OK) -> add CHANGELOG.md, update it each release (Extension Rule 8)`
|
|
5339
5597
|
};
|
|
5598
|
+
continue;
|
|
5340
5599
|
}
|
|
5600
|
+
judgedConforming++;
|
|
5341
5601
|
}
|
|
5602
|
+
if (finding) return settle(this.id, finding, unreadable);
|
|
5342
5603
|
const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
|
|
5343
|
-
|
|
5604
|
+
const conforming = {
|
|
5344
5605
|
id: this.id,
|
|
5345
5606
|
anchor: `${firstLabel}/package.json`,
|
|
5346
5607
|
state: "present",
|
|
5347
|
-
verdict: `SemVer valid + CHANGELOG.md present across ${manifests.length} extension(s) (positive-control OK)`
|
|
5608
|
+
verdict: `SemVer valid + CHANGELOG.md present across ${judgedConforming} of ${manifests.length} extension(s) (positive-control OK)`
|
|
5348
5609
|
};
|
|
5610
|
+
return settle(this.id, conforming, unreadable);
|
|
5349
5611
|
}
|
|
5350
5612
|
};
|
|
5351
5613
|
|
|
5352
5614
|
// src/detectors/dExt9.ts
|
|
5353
|
-
import { readFileSync as
|
|
5615
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync11, statSync as statSync12 } from "fs";
|
|
5354
5616
|
import { join as join19 } from "path";
|
|
5355
5617
|
var SKIP_DIRS9 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
5356
|
-
function collectHooksDirSources(root) {
|
|
5618
|
+
function collectHooksDirSources(root, unreadable) {
|
|
5357
5619
|
const out = [];
|
|
5358
5620
|
let base;
|
|
5359
5621
|
try {
|
|
@@ -5392,11 +5654,14 @@ function collectHooksDirSources(root) {
|
|
|
5392
5654
|
const filePath = join19(full, f);
|
|
5393
5655
|
let body;
|
|
5394
5656
|
try {
|
|
5395
|
-
body =
|
|
5657
|
+
body = readFileSync10(filePath, "utf8");
|
|
5396
5658
|
} catch {
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5659
|
+
const shortPath = filePath.split("/").slice(-2).join("/");
|
|
5660
|
+
unreadable.push({
|
|
5661
|
+
anchor: shortPath,
|
|
5662
|
+
reason: `could not read hook implementation ${shortPath} \u2014 timeout guard undeterminable for hook "${stem}" (Extension Rule 9)`
|
|
5663
|
+
});
|
|
5664
|
+
continue;
|
|
5400
5665
|
}
|
|
5401
5666
|
out.push({
|
|
5402
5667
|
token: canonical(stem),
|
|
@@ -5466,13 +5731,11 @@ function dExt9PositiveControls() {
|
|
|
5466
5731
|
negative: hasImplTimeoutGuard(CTRL_NEGATIVE_BODY)
|
|
5467
5732
|
};
|
|
5468
5733
|
}
|
|
5469
|
-
function manifestTimeoutTokens(raw
|
|
5734
|
+
function manifestTimeoutTokens(raw) {
|
|
5470
5735
|
const map = /* @__PURE__ */ new Map();
|
|
5471
5736
|
if (raw === void 0) return map;
|
|
5472
5737
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
5473
|
-
|
|
5474
|
-
`DExt9 found ${shortPath} whose "timeouts" field is present but not an object (got ${Array.isArray(raw) ? "array" : typeof raw}) \u2014 declared timeout guards are undeterminable (Extension Rule 9). Fail-closed, not a silent absent.`
|
|
5475
|
-
);
|
|
5738
|
+
return null;
|
|
5476
5739
|
}
|
|
5477
5740
|
for (const [key, value] of Object.entries(raw)) {
|
|
5478
5741
|
if (typeof value === "number" && value > 0) {
|
|
@@ -5490,67 +5753,83 @@ var dExt9 = {
|
|
|
5490
5753
|
`DExt9 hook-timeout control FAILED: setTimeoutForm=${controls.setTimeoutForm} abortForm=${controls.abortForm} negative=${controls.negative} \u2014 impl-pattern matcher broken, verdict untrustworthy`
|
|
5491
5754
|
);
|
|
5492
5755
|
}
|
|
5493
|
-
const
|
|
5494
|
-
const
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
)
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
)
|
|
5756
|
+
const unreadable = [];
|
|
5757
|
+
const decided = (() => {
|
|
5758
|
+
const hookSources = [
|
|
5759
|
+
...collectHooksDirSources(tree.root, unreadable),
|
|
5760
|
+
...collectFactorySources(tree)
|
|
5761
|
+
];
|
|
5762
|
+
const implByToken = /* @__PURE__ */ new Map();
|
|
5763
|
+
for (const s of hookSources) {
|
|
5764
|
+
const list = implByToken.get(s.token) ?? [];
|
|
5765
|
+
list.push(s);
|
|
5766
|
+
implByToken.set(s.token, list);
|
|
5767
|
+
}
|
|
5768
|
+
const manifests = findExtensionManifests4(tree.root);
|
|
5769
|
+
let declaredAnyLifecycle = false;
|
|
5770
|
+
for (const manifestPath2 of manifests) {
|
|
5771
|
+
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
5772
|
+
const parsed = readJsonObject(manifestPath2);
|
|
5773
|
+
if (parsed === null) {
|
|
5774
|
+
unreadable.push({
|
|
5775
|
+
anchor: shortPath,
|
|
5776
|
+
reason: `could not parse ${shortPath} as JSON \u2014 timeout guards undeterminable (Extension Rule 9)`
|
|
5777
|
+
});
|
|
5778
|
+
continue;
|
|
5779
|
+
}
|
|
5780
|
+
if (!("lifecycle" in parsed)) continue;
|
|
5781
|
+
const declared = manifestHookTokens(parsed.lifecycle);
|
|
5782
|
+
if (declared === null) {
|
|
5783
|
+
return {
|
|
5784
|
+
id: this.id,
|
|
5785
|
+
anchor: shortPath,
|
|
5786
|
+
state: "non-conforming",
|
|
5787
|
+
verdict: `manifest ${shortPath} "lifecycle" is not an array (got ${typeof parsed.lifecycle}) -> declare the hooks as an array of hook names (Extension Rule 9)`
|
|
5788
|
+
};
|
|
5789
|
+
}
|
|
5790
|
+
if (declared.size === 0) continue;
|
|
5791
|
+
declaredAnyLifecycle = true;
|
|
5792
|
+
const timeouts = manifestTimeoutTokens(parsed.timeouts);
|
|
5793
|
+
if (timeouts === null) {
|
|
5794
|
+
return {
|
|
5795
|
+
id: this.id,
|
|
5796
|
+
anchor: shortPath,
|
|
5797
|
+
state: "non-conforming",
|
|
5798
|
+
verdict: `${shortPath} "timeouts" is present but not an object (got ${Array.isArray(parsed.timeouts) ? "array" : typeof parsed.timeouts}) -> declare timeouts as a { <hook>: <positive ms> } object (Extension Rule 9)`
|
|
5799
|
+
};
|
|
5800
|
+
}
|
|
5801
|
+
const noTimeout = [];
|
|
5802
|
+
for (const hook of declared) {
|
|
5803
|
+
const hasManifestTimeout = timeouts.has(hook);
|
|
5804
|
+
const implSources = implByToken.get(hook) ?? [];
|
|
5805
|
+
const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
|
|
5806
|
+
if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
|
|
5807
|
+
}
|
|
5808
|
+
if (noTimeout.length > 0) {
|
|
5809
|
+
return {
|
|
5810
|
+
id: this.id,
|
|
5811
|
+
anchor: shortPath,
|
|
5812
|
+
state: "non-conforming",
|
|
5813
|
+
verdict: `hook(s) [${noTimeout.join(", ")}] declare NO timeout guard in either form (manifest "timeouts.<hook>" positive number, or impl-side setTimeout/AbortController cutoff) -> a hook exceeding its timeout must be skipped, not a server blocker (Extension Rule 9)`
|
|
5814
|
+
};
|
|
5815
|
+
}
|
|
5520
5816
|
}
|
|
5521
|
-
if (
|
|
5522
|
-
declaredAnyLifecycle = true;
|
|
5523
|
-
const timeouts = manifestTimeoutTokens(parsed.timeouts, shortPath);
|
|
5524
|
-
const noTimeout = [];
|
|
5525
|
-
for (const hook of declared) {
|
|
5526
|
-
const hasManifestTimeout = timeouts.has(hook);
|
|
5527
|
-
const implSources = implByToken.get(hook) ?? [];
|
|
5528
|
-
const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
|
|
5529
|
-
if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
|
|
5530
|
-
}
|
|
5531
|
-
if (noTimeout.length > 0) {
|
|
5817
|
+
if (!declaredAnyLifecycle) {
|
|
5532
5818
|
return {
|
|
5533
5819
|
id: this.id,
|
|
5534
|
-
anchor:
|
|
5535
|
-
state: "
|
|
5536
|
-
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)"
|
|
5537
5823
|
};
|
|
5538
5824
|
}
|
|
5539
|
-
}
|
|
5540
|
-
if (!declaredAnyLifecycle) {
|
|
5541
5825
|
return {
|
|
5542
5826
|
id: this.id,
|
|
5543
|
-
anchor: manifests
|
|
5544
|
-
state: "
|
|
5545
|
-
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)"
|
|
5546
5830
|
};
|
|
5547
|
-
}
|
|
5548
|
-
return
|
|
5549
|
-
id: this.id,
|
|
5550
|
-
anchor: manifests[0].split("/").slice(-2).join("/"),
|
|
5551
|
-
state: "present",
|
|
5552
|
-
verdict: "every declared lifecycle hook has a timeout guard (manifest timeouts.<hook>, or impl-side cutoff), positive-control OK (Extension Rule 9)"
|
|
5553
|
-
};
|
|
5831
|
+
})();
|
|
5832
|
+
return settle(this.id, decided, unreadable);
|
|
5554
5833
|
}
|
|
5555
5834
|
};
|
|
5556
5835
|
|
|
@@ -5579,7 +5858,7 @@ function unscopePackageName(name) {
|
|
|
5579
5858
|
const m = name.match(/^@[^/]+\/(.+)$/);
|
|
5580
5859
|
return m ? m[1] : name;
|
|
5581
5860
|
}
|
|
5582
|
-
var
|
|
5861
|
+
var dServer2Inner = {
|
|
5583
5862
|
id: "DServer2 naming",
|
|
5584
5863
|
rule: "C-2",
|
|
5585
5864
|
pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
|
|
@@ -5607,6 +5886,22 @@ var dServer2 = {
|
|
|
5607
5886
|
};
|
|
5608
5887
|
}
|
|
5609
5888
|
const tools = findRegisteredTools(tree);
|
|
5889
|
+
if (tools.length === 0) {
|
|
5890
|
+
const surface = toolSurfaceAnchor(tree);
|
|
5891
|
+
if (surface) {
|
|
5892
|
+
const reason = noToolsLocatedReason(
|
|
5893
|
+
tree,
|
|
5894
|
+
"snake_case tool naming is unverifiable (Server Rule 2)"
|
|
5895
|
+
);
|
|
5896
|
+
return {
|
|
5897
|
+
id: this.id,
|
|
5898
|
+
anchor: surface,
|
|
5899
|
+
state: "could-not-judge",
|
|
5900
|
+
reason,
|
|
5901
|
+
verdict: `could not judge: ${reason}`
|
|
5902
|
+
};
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5610
5905
|
for (const t of tools) {
|
|
5611
5906
|
if (!TOOL_SNAKE_RE.test(t.name)) {
|
|
5612
5907
|
return {
|
|
@@ -5710,6 +6005,9 @@ var dServer2 = {
|
|
|
5710
6005
|
};
|
|
5711
6006
|
}
|
|
5712
6007
|
};
|
|
6008
|
+
var dServer2 = guardUnreadablePackageJson(dServer2Inner, {
|
|
6009
|
+
authoritative: false
|
|
6010
|
+
});
|
|
5713
6011
|
|
|
5714
6012
|
// src/detectors/dServer6.ts
|
|
5715
6013
|
var LOG_OPENER_RE = /\b(?:console|logger|log)\.(?:log|info|warn|error|debug|trace)\s*\(/;
|
|
@@ -5820,65 +6118,72 @@ var dServer6 = {
|
|
|
5820
6118
|
`DServer6 stack control FAILED: /${controls.stack.pattern}/ accept(\`${STACK_POSITIVE}\`)=${controls.stack.positive} reject(\`${STACK_NEGATIVE}\`)=${!controls.stack.negative} \u2014 matcher broken, verdict untrustworthy`
|
|
5821
6119
|
);
|
|
5822
6120
|
}
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
throw new Error(
|
|
5833
|
-
`DServer6 cannot inspect the spread argument of the log call at ${call.anchor} \u2014 a spread hides arbitrary content, secret/PII presence unverifiable (C-6). Fail-closed, not a silent present.`
|
|
5834
|
-
);
|
|
5835
|
-
}
|
|
5836
|
-
const refs = referencedIdentifiers(call.argText);
|
|
5837
|
-
if (SECRET_RE.test(refs)) {
|
|
5838
|
-
return {
|
|
5839
|
-
id: this.id,
|
|
5840
|
-
anchor: call.anchor,
|
|
5841
|
-
state: "non-conforming",
|
|
5842
|
-
verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (C-6)"
|
|
5843
|
-
};
|
|
5844
|
-
}
|
|
5845
|
-
if (PII_RE.test(refs)) {
|
|
5846
|
-
return {
|
|
5847
|
-
id: this.id,
|
|
5848
|
-
anchor: call.anchor,
|
|
5849
|
-
state: "non-conforming",
|
|
5850
|
-
verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
|
|
5851
|
-
};
|
|
6121
|
+
const unreadable = [];
|
|
6122
|
+
const decided = (() => {
|
|
6123
|
+
for (const file of tree.files) {
|
|
6124
|
+
const { calls, unbalanced } = collectLogCalls(file);
|
|
6125
|
+
for (const anchor of unbalanced) {
|
|
6126
|
+
unreadable.push({
|
|
6127
|
+
anchor,
|
|
6128
|
+
reason: `could not read the argument list of the log call at ${anchor} \u2014 parentheses never balance, secret/PII content unverifiable (C-6)`
|
|
6129
|
+
});
|
|
5852
6130
|
}
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
6131
|
+
for (const call of calls) {
|
|
6132
|
+
if (SPREAD_RE.test(call.argText)) {
|
|
6133
|
+
unreadable.push({
|
|
6134
|
+
anchor: call.anchor,
|
|
6135
|
+
reason: `cannot inspect the spread argument of the log call at ${call.anchor} \u2014 a spread hides arbitrary content, secret/PII presence unverifiable (C-6)`
|
|
6136
|
+
});
|
|
6137
|
+
continue;
|
|
6138
|
+
}
|
|
6139
|
+
const refs = referencedIdentifiers(call.argText);
|
|
6140
|
+
if (SECRET_RE.test(refs)) {
|
|
6141
|
+
return {
|
|
6142
|
+
id: this.id,
|
|
6143
|
+
anchor: call.anchor,
|
|
6144
|
+
state: "non-conforming",
|
|
6145
|
+
verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (C-6)"
|
|
6146
|
+
};
|
|
6147
|
+
}
|
|
6148
|
+
if (PII_RE.test(refs)) {
|
|
6149
|
+
return {
|
|
6150
|
+
id: this.id,
|
|
6151
|
+
anchor: call.anchor,
|
|
6152
|
+
state: "non-conforming",
|
|
6153
|
+
verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
|
|
6154
|
+
};
|
|
6155
|
+
}
|
|
6156
|
+
if (INTERNAL_RE.test(refs)) {
|
|
6157
|
+
return {
|
|
6158
|
+
id: this.id,
|
|
6159
|
+
anchor: call.anchor,
|
|
6160
|
+
state: "non-conforming",
|
|
6161
|
+
verdict: "Form D \u2014 log call references an internal-infrastructure identifier (positive-control OK) -> never log internal system detail; redact before logging (C-6)"
|
|
6162
|
+
};
|
|
6163
|
+
}
|
|
5860
6164
|
}
|
|
5861
6165
|
}
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
}
|
|
6166
|
+
for (const file of tree.files) {
|
|
6167
|
+
for (const line of file.lines) {
|
|
6168
|
+
if (line.comment) continue;
|
|
6169
|
+
if (STACK_RE.test(line.raw) && CLIENT_SINK_RE.test(line.raw)) {
|
|
6170
|
+
return {
|
|
6171
|
+
id: this.id,
|
|
6172
|
+
anchor: `${file.rel}:${line.n}`,
|
|
6173
|
+
state: "non-conforming",
|
|
6174
|
+
verdict: "Form B \u2014 raw error.stack serialized into a client-facing response (positive-control OK) -> return an McpError message, never the raw stack (C-6)"
|
|
6175
|
+
};
|
|
6176
|
+
}
|
|
5873
6177
|
}
|
|
5874
6178
|
}
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
};
|
|
6179
|
+
return {
|
|
6180
|
+
id: this.id,
|
|
6181
|
+
anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
|
|
6182
|
+
state: "present",
|
|
6183
|
+
verdict: "no secret/PII/internal-detail referenced in any log call; no raw error.stack exposed to the client (all positive-controls OK). Logging-sink configuration and explicit-activation-only diagnostic mode are the gate/runtime half, out of static scope (declared)."
|
|
6184
|
+
};
|
|
6185
|
+
})();
|
|
6186
|
+
return settle(this.id, decided, unreadable);
|
|
5882
6187
|
}
|
|
5883
6188
|
};
|
|
5884
6189
|
|
|
@@ -5960,59 +6265,65 @@ var dServer8 = {
|
|
|
5960
6265
|
`DServer8 text control FAILED: /${controls.text.pattern}/ accept(\`${TEXT_POSITIVE}\`)=${controls.text.positive} reject(\`${TEXT_NEGATIVE}\`)=${!controls.text.negative} \u2014 matcher broken, verdict untrustworthy`
|
|
5961
6266
|
);
|
|
5962
6267
|
}
|
|
5963
|
-
const
|
|
5964
|
-
|
|
5965
|
-
const
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
6268
|
+
const unreadable = [];
|
|
6269
|
+
const decided = (() => {
|
|
6270
|
+
const blocks = [];
|
|
6271
|
+
for (const file of tree.files) {
|
|
6272
|
+
const { blocks: fileBlocks, unbalanced } = collectContentArrays(file);
|
|
6273
|
+
for (const anchor of unbalanced) {
|
|
6274
|
+
unreadable.push({
|
|
6275
|
+
anchor,
|
|
6276
|
+
reason: `could not read the content[] array at ${anchor} \u2014 brackets never balance, a ui:// resource emission could hide inside it (Server Rule 8)`
|
|
6277
|
+
});
|
|
6278
|
+
}
|
|
6279
|
+
blocks.push(...fileBlocks);
|
|
6280
|
+
}
|
|
6281
|
+
for (const file of tree.files) {
|
|
6282
|
+
const fileBlocks = blocks.filter((b) => b.file === file);
|
|
6283
|
+
for (const line of file.lines) {
|
|
6284
|
+
if (line.comment) continue;
|
|
6285
|
+
if (!KIT_RE.test(line.raw)) continue;
|
|
6286
|
+
const inside = fileBlocks.some((b) => line.n >= b.startLine && line.n <= b.endLine);
|
|
6287
|
+
if (!inside) {
|
|
6288
|
+
unreadable.push({
|
|
6289
|
+
anchor: `${file.rel}:${line.n}`,
|
|
6290
|
+
reason: `renderUiResource(\u2026) ui:// emission at ${file.rel}:${line.n} sits outside any content[] array \u2014 cannot associate it with a sibling text fallback (Server Rule 8)`
|
|
6291
|
+
});
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
5970
6294
|
}
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
6295
|
+
let sawEmission = false;
|
|
6296
|
+
for (const block of blocks) {
|
|
6297
|
+
const inlineEmission = INLINE_RESOURCE_RE.test(block.text) && UI_URI_RE.test(block.text);
|
|
6298
|
+
const kitEmission = KIT_RE.test(block.text);
|
|
6299
|
+
if (!inlineEmission && !kitEmission) continue;
|
|
6300
|
+
sawEmission = true;
|
|
6301
|
+
const hasText = TEXT_RE.test(block.text) || MARKDOWN_RE.test(block.text);
|
|
6302
|
+
if (!hasText) {
|
|
6303
|
+
return {
|
|
6304
|
+
id: this.id,
|
|
6305
|
+
anchor: block.anchor,
|
|
6306
|
+
state: "non-conforming",
|
|
6307
|
+
verdict: `ui:// resource emitted in content[] with no text fallback beside ui:// in the same array (positive-control OK) -> add a sibling { type: "text" } / { type: "markdown" } entry (Server Rule 8)`
|
|
6308
|
+
};
|
|
5983
6309
|
}
|
|
5984
6310
|
}
|
|
5985
|
-
|
|
5986
|
-
let sawEmission = false;
|
|
5987
|
-
for (const block of blocks) {
|
|
5988
|
-
const inlineEmission = INLINE_RESOURCE_RE.test(block.text) && UI_URI_RE.test(block.text);
|
|
5989
|
-
const kitEmission = KIT_RE.test(block.text);
|
|
5990
|
-
if (!inlineEmission && !kitEmission) continue;
|
|
5991
|
-
sawEmission = true;
|
|
5992
|
-
const hasText = TEXT_RE.test(block.text) || MARKDOWN_RE.test(block.text);
|
|
5993
|
-
if (!hasText) {
|
|
6311
|
+
if (!sawEmission) {
|
|
5994
6312
|
return {
|
|
5995
6313
|
id: this.id,
|
|
5996
|
-
anchor:
|
|
5997
|
-
state: "
|
|
5998
|
-
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)"
|
|
5999
6317
|
};
|
|
6000
6318
|
}
|
|
6001
|
-
}
|
|
6002
|
-
if (!sawEmission) {
|
|
6003
6319
|
return {
|
|
6004
6320
|
id: this.id,
|
|
6005
|
-
anchor:
|
|
6006
|
-
state: "
|
|
6007
|
-
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)"
|
|
6008
6324
|
};
|
|
6009
|
-
}
|
|
6010
|
-
return
|
|
6011
|
-
id: this.id,
|
|
6012
|
-
anchor: blocks.length > 0 ? blocks[0].anchor : tree.root,
|
|
6013
|
-
state: "present",
|
|
6014
|
-
verdict: "every ui:// resource emission has a sibling text/markdown fallback in the same content[] (positive-control OK, Server Rule 8)"
|
|
6015
|
-
};
|
|
6325
|
+
})();
|
|
6326
|
+
return settle(this.id, decided, unreadable);
|
|
6016
6327
|
}
|
|
6017
6328
|
};
|
|
6018
6329
|
|
|
@@ -6241,7 +6552,7 @@ var detectors = [
|
|
|
6241
6552
|
|
|
6242
6553
|
// src/runtime/launch.ts
|
|
6243
6554
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6244
|
-
import { existsSync as existsSync15, readFileSync as
|
|
6555
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync13 } from "fs";
|
|
6245
6556
|
import { dirname as dirname10, isAbsolute, join as join20, resolve } from "path";
|
|
6246
6557
|
function findPackageDir(target) {
|
|
6247
6558
|
let dir;
|
|
@@ -6276,7 +6587,7 @@ function deriveLaunchPlan(target) {
|
|
|
6276
6587
|
}
|
|
6277
6588
|
let pkg;
|
|
6278
6589
|
try {
|
|
6279
|
-
pkg = JSON.parse(
|
|
6590
|
+
pkg = JSON.parse(readFileSync11(join20(pkgDir, "package.json"), "utf8"));
|
|
6280
6591
|
} catch (err) {
|
|
6281
6592
|
return {
|
|
6282
6593
|
error: `could not parse ${join20(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -6360,7 +6671,7 @@ function readEvalCounts(target) {
|
|
|
6360
6671
|
const candidate = join20(dir, "evals", "evals.json");
|
|
6361
6672
|
if (existsSync15(candidate)) {
|
|
6362
6673
|
try {
|
|
6363
|
-
const parsed = JSON.parse(
|
|
6674
|
+
const parsed = JSON.parse(readFileSync11(candidate, "utf8"));
|
|
6364
6675
|
for (const c of parsed.cases ?? []) {
|
|
6365
6676
|
if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
|
|
6366
6677
|
}
|
|
@@ -6620,7 +6931,7 @@ var USAGE = [
|
|
|
6620
6931
|
function readVersion() {
|
|
6621
6932
|
try {
|
|
6622
6933
|
const here = dirname11(fileURLToPath(import.meta.url));
|
|
6623
|
-
const manifest = JSON.parse(
|
|
6934
|
+
const manifest = JSON.parse(readFileSync12(join21(here, "..", "package.json"), "utf8"));
|
|
6624
6935
|
return manifest.version ?? null;
|
|
6625
6936
|
} catch {
|
|
6626
6937
|
return null;
|