@vantageos/mcp-doctor 0.3.1 → 0.4.0
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 +81 -0
- package/README.md +25 -10
- package/dist/doctor.d.ts +43 -2
- package/dist/doctor.js +2048 -984
- package/package.json +8 -3
package/dist/doctor.js
CHANGED
|
@@ -1,13 +1,998 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/doctor.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { dirname as dirname11, join as
|
|
4
|
+
import { readFileSync as readFileSync12, realpathSync } from "fs";
|
|
5
|
+
import { dirname as dirname11, join as join19, resolve as resolve2 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
|
+
// src/scan.ts
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
10
|
+
import { basename, dirname, join, relative, sep } from "path";
|
|
11
|
+
var SKIP_DIR_INFO = {
|
|
12
|
+
node_modules: { reason: "vendored dependencies", authored: false },
|
|
13
|
+
dist: { reason: "build output", authored: false },
|
|
14
|
+
build: { reason: "build output", authored: false },
|
|
15
|
+
".git": { reason: "version-control metadata", authored: false },
|
|
16
|
+
coverage: { reason: "generated coverage output", authored: false },
|
|
17
|
+
// `authored: true` marks content the target's own authors wrote, as opposed
|
|
18
|
+
// to vendored or generated content. A walk that looks for a site (C-26) may
|
|
19
|
+
// probe an authored directory it skips: a deployable there would otherwise
|
|
20
|
+
// be reported as if it did not exist.
|
|
21
|
+
tests: { reason: "test material", authored: true },
|
|
22
|
+
__tests__: { reason: "test material", authored: true },
|
|
23
|
+
// Test material, not a target's served surface: deliberately-malformed
|
|
24
|
+
// servers and foreign copies live here. Scanning them makes the instrument
|
|
25
|
+
// trip its own fail-closed detectors when pointed at its own tree, and lints
|
|
26
|
+
// inputs that are meant to be broken. A real target's fixtures are equally
|
|
27
|
+
// test inputs, not its product surface.
|
|
28
|
+
fixtures: { reason: "test material (fixtures)", authored: true }
|
|
29
|
+
};
|
|
30
|
+
var SKIP_DIRS = new Set(Object.keys(SKIP_DIR_INFO));
|
|
31
|
+
function recordSkip(tree, dir, root, reason) {
|
|
32
|
+
const path = relative(root, dir).split(sep).join("/");
|
|
33
|
+
const existing = tree.skippedDirs.find((d) => d.path === path);
|
|
34
|
+
if (existing) return existing;
|
|
35
|
+
const entry = {
|
|
36
|
+
path,
|
|
37
|
+
reason: reason ?? SKIP_DIR_INFO[basename(dir)]?.reason ?? "skipped"
|
|
38
|
+
};
|
|
39
|
+
tree.skippedDirs.push(entry);
|
|
40
|
+
return entry;
|
|
41
|
+
}
|
|
42
|
+
function isCommentLine(raw) {
|
|
43
|
+
const t = raw.trim();
|
|
44
|
+
if (t === "") return false;
|
|
45
|
+
return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
|
|
46
|
+
}
|
|
47
|
+
function walk(dir, out, onSkip) {
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = readdirSync(dir);
|
|
51
|
+
} catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const name of entries) {
|
|
55
|
+
const full = join(dir, name);
|
|
56
|
+
let st;
|
|
57
|
+
try {
|
|
58
|
+
st = statSync(full);
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (st.isDirectory()) {
|
|
63
|
+
if (SKIP_DIRS.has(name)) {
|
|
64
|
+
onSkip(full);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
walk(full, out, onSkip);
|
|
68
|
+
} else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
|
|
69
|
+
if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
|
|
70
|
+
out.push(full);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function loadFile(path, root) {
|
|
75
|
+
const raw = readFileSync(path, "utf8");
|
|
76
|
+
const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
|
|
77
|
+
const file = { path, rel: relative(root, path) || path, lines };
|
|
78
|
+
const mask = maskOf(file);
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (line.comment) continue;
|
|
81
|
+
const row = mask[line.n - 1];
|
|
82
|
+
let any = false;
|
|
83
|
+
let allComment = true;
|
|
84
|
+
for (let c = 0; c < line.raw.length; c++) {
|
|
85
|
+
if (line.raw[c] === " " || line.raw[c] === " " || line.raw[c] === "\r") continue;
|
|
86
|
+
any = true;
|
|
87
|
+
if (row[c] !== COM) {
|
|
88
|
+
allComment = false;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (any && allComment) line.comment = true;
|
|
93
|
+
}
|
|
94
|
+
return file;
|
|
95
|
+
}
|
|
96
|
+
function findPackageJson(startDir) {
|
|
97
|
+
let dir = startDir;
|
|
98
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
99
|
+
const candidate = join(dir, "package.json");
|
|
100
|
+
if (existsSync(candidate)) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
103
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
104
|
+
return { path: candidate, json: {}, unreadable: "not a JSON object" };
|
|
105
|
+
}
|
|
106
|
+
return { path: candidate, json: parsed };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return { path: candidate, json: {}, unreadable: err.message };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const parent = dirname(dir);
|
|
112
|
+
if (parent === dir) break;
|
|
113
|
+
dir = parent;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function loadTree(root) {
|
|
118
|
+
const st = statSync(root);
|
|
119
|
+
const baseDir = st.isDirectory() ? root : dirname(root);
|
|
120
|
+
const filePaths = [];
|
|
121
|
+
const tree = { root, files: [], packageJson: null, skippedDirs: [] };
|
|
122
|
+
if (st.isDirectory()) walk(root, filePaths, (d) => recordSkip(tree, d, root));
|
|
123
|
+
else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
|
|
124
|
+
tree.files = filePaths.map((p) => loadFile(p, baseDir));
|
|
125
|
+
tree.packageJson = findPackageJson(baseDir);
|
|
126
|
+
return tree;
|
|
127
|
+
}
|
|
128
|
+
var COM = 1;
|
|
129
|
+
var CODE = 2;
|
|
130
|
+
var maskCache = /* @__PURE__ */ new WeakMap();
|
|
131
|
+
function buildCodeMask(file) {
|
|
132
|
+
const mask = [];
|
|
133
|
+
let mode = "code";
|
|
134
|
+
const tplStack = [];
|
|
135
|
+
let braceDepth = 0;
|
|
136
|
+
let prevSig = "";
|
|
137
|
+
let prevWord = "";
|
|
138
|
+
const regexAfterKeyword = /* @__PURE__ */ new Set([
|
|
139
|
+
"case",
|
|
140
|
+
"default",
|
|
141
|
+
"delete",
|
|
142
|
+
"do",
|
|
143
|
+
"else",
|
|
144
|
+
"extends",
|
|
145
|
+
"in",
|
|
146
|
+
"instanceof",
|
|
147
|
+
"new",
|
|
148
|
+
"of",
|
|
149
|
+
"return",
|
|
150
|
+
"throw",
|
|
151
|
+
"typeof",
|
|
152
|
+
"void",
|
|
153
|
+
"yield",
|
|
154
|
+
"await"
|
|
155
|
+
]);
|
|
156
|
+
const regexAfter = /* @__PURE__ */ new Set([
|
|
157
|
+
"",
|
|
158
|
+
"(",
|
|
159
|
+
",",
|
|
160
|
+
"=",
|
|
161
|
+
":",
|
|
162
|
+
"[",
|
|
163
|
+
"!",
|
|
164
|
+
"&",
|
|
165
|
+
"|",
|
|
166
|
+
"?",
|
|
167
|
+
"{",
|
|
168
|
+
"}",
|
|
169
|
+
";",
|
|
170
|
+
"+",
|
|
171
|
+
"-",
|
|
172
|
+
"*",
|
|
173
|
+
"%",
|
|
174
|
+
"<",
|
|
175
|
+
">",
|
|
176
|
+
"~",
|
|
177
|
+
"^"
|
|
178
|
+
]);
|
|
179
|
+
for (const line of file.lines) {
|
|
180
|
+
const s = line.raw;
|
|
181
|
+
const row = new Uint8Array(s.length);
|
|
182
|
+
let i = 0;
|
|
183
|
+
while (i < s.length) {
|
|
184
|
+
const c = s[i];
|
|
185
|
+
const n = s[i + 1];
|
|
186
|
+
if (mode === "block") {
|
|
187
|
+
row[i] = COM;
|
|
188
|
+
if (c === "*" && n === "/") {
|
|
189
|
+
row[i + 1] = COM;
|
|
190
|
+
i += 2;
|
|
191
|
+
mode = "code";
|
|
192
|
+
} else i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (mode === "sq" || mode === "dq") {
|
|
196
|
+
if (c === "\\") i += 2;
|
|
197
|
+
else if (mode === "sq" && c === "'" || mode === "dq" && c === '"') {
|
|
198
|
+
row[i] = CODE;
|
|
199
|
+
i++;
|
|
200
|
+
mode = "code";
|
|
201
|
+
prevSig = c;
|
|
202
|
+
} else i++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (mode === "tpl") {
|
|
206
|
+
if (c === "\\") i += 2;
|
|
207
|
+
else if (c === "`") {
|
|
208
|
+
row[i] = CODE;
|
|
209
|
+
i++;
|
|
210
|
+
mode = "code";
|
|
211
|
+
prevSig = c;
|
|
212
|
+
} else if (c === "$" && n === "{") {
|
|
213
|
+
row[i] = CODE;
|
|
214
|
+
row[i + 1] = CODE;
|
|
215
|
+
tplStack.push(braceDepth);
|
|
216
|
+
braceDepth++;
|
|
217
|
+
i += 2;
|
|
218
|
+
mode = "code";
|
|
219
|
+
prevSig = "{";
|
|
220
|
+
} else i++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (mode === "re" || mode === "recls") {
|
|
224
|
+
if (c === "\\") i += 2;
|
|
225
|
+
else if (mode === "re" && c === "[") {
|
|
226
|
+
mode = "recls";
|
|
227
|
+
i++;
|
|
228
|
+
} else if (mode === "recls" && c === "]") {
|
|
229
|
+
mode = "re";
|
|
230
|
+
i++;
|
|
231
|
+
} else if (mode === "re" && c === "/") {
|
|
232
|
+
row[i] = CODE;
|
|
233
|
+
i++;
|
|
234
|
+
mode = "code";
|
|
235
|
+
prevSig = "/";
|
|
236
|
+
} else i++;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (c === "/" && n === "/") {
|
|
240
|
+
row.fill(COM, i);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
if (c === "/" && n === "*") {
|
|
244
|
+
mode = "block";
|
|
245
|
+
row[i] = COM;
|
|
246
|
+
row[i + 1] = COM;
|
|
247
|
+
i += 2;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
row[i] = CODE;
|
|
251
|
+
const wordBefore = prevWord;
|
|
252
|
+
if (c !== " " && c !== " ") prevWord = "";
|
|
253
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
254
|
+
let j = i + 1;
|
|
255
|
+
while (j < s.length && /[\w$]/.test(s[j])) j++;
|
|
256
|
+
for (let k = i; k < j; k++) row[k] = CODE;
|
|
257
|
+
prevWord = prevSig === "." ? "" : s.slice(i, j);
|
|
258
|
+
prevSig = s[j - 1];
|
|
259
|
+
i = j;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
263
|
+
mode = c === '"' ? "dq" : c === "'" ? "sq" : "tpl";
|
|
264
|
+
i++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (c === "/" && (regexAfter.has(prevSig) || regexAfterKeyword.has(wordBefore))) {
|
|
268
|
+
mode = "re";
|
|
269
|
+
i++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (c === "{") braceDepth++;
|
|
273
|
+
else if (c === "}") {
|
|
274
|
+
braceDepth--;
|
|
275
|
+
if (tplStack.length > 0 && tplStack[tplStack.length - 1] === braceDepth) {
|
|
276
|
+
tplStack.pop();
|
|
277
|
+
mode = "tpl";
|
|
278
|
+
i++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (c !== " " && c !== " ") prevSig = c;
|
|
283
|
+
i++;
|
|
284
|
+
}
|
|
285
|
+
if (mode === "sq" || mode === "dq" || mode === "re" || mode === "recls") mode = "code";
|
|
286
|
+
mask.push(row);
|
|
287
|
+
}
|
|
288
|
+
return mask;
|
|
289
|
+
}
|
|
290
|
+
function maskOf(file) {
|
|
291
|
+
let mask = maskCache.get(file);
|
|
292
|
+
if (!mask) {
|
|
293
|
+
mask = buildCodeMask(file);
|
|
294
|
+
maskCache.set(file, mask);
|
|
295
|
+
}
|
|
296
|
+
return mask;
|
|
297
|
+
}
|
|
298
|
+
function isCodeAt(file, n, col) {
|
|
299
|
+
const mask = maskOf(file);
|
|
300
|
+
return mask[n - 1]?.[col] === CODE;
|
|
301
|
+
}
|
|
302
|
+
function matchInCode(file, line, re) {
|
|
303
|
+
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
|
|
304
|
+
for (const m of line.raw.matchAll(g)) {
|
|
305
|
+
if (isCodeAt(file, line.n, m.index ?? 0)) return m;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
function testInCode(file, line, re) {
|
|
310
|
+
return matchInCode(file, line, re) !== null;
|
|
311
|
+
}
|
|
312
|
+
function countInCode(file, line, ch) {
|
|
313
|
+
let n = 0;
|
|
314
|
+
for (let c = 0; c < line.raw.length; c++) {
|
|
315
|
+
if (line.raw[c] === ch && isCodeAt(file, line.n, c)) n++;
|
|
316
|
+
}
|
|
317
|
+
return n;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/detectors/couldNotJudge.ts
|
|
321
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
322
|
+
function couldNotJudge(id, anchor, reason) {
|
|
323
|
+
return {
|
|
324
|
+
id,
|
|
325
|
+
anchor,
|
|
326
|
+
state: "could-not-judge",
|
|
327
|
+
reason,
|
|
328
|
+
verdict: `could not judge: ${reason}`
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function settle(id, decided, unreadable) {
|
|
332
|
+
if (unreadable.length === 0) return decided;
|
|
333
|
+
const named = unreadable.map((u) => `${u.anchor}: ${u.reason}`).join("; ");
|
|
334
|
+
if (decided.state === "non-conforming") {
|
|
335
|
+
return { ...decided, verdict: `${decided.verdict} [also could not read: ${named}]` };
|
|
336
|
+
}
|
|
337
|
+
const judgedRest = decided.state === "could-not-judge" ? ` ${decided.reason ?? decided.verdict}` : ` The readable remainder judged ${decided.state}: ${decided.verdict}`;
|
|
338
|
+
return couldNotJudge(
|
|
339
|
+
id,
|
|
340
|
+
unreadable[0].anchor,
|
|
341
|
+
`${unreadable.length} input(s) present but unreadable \u2014 ${named}.${judgedRest}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
function readJsonObject(path) {
|
|
345
|
+
let value;
|
|
346
|
+
try {
|
|
347
|
+
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
function guardUnreadablePackageJson(inner, opts) {
|
|
355
|
+
return {
|
|
356
|
+
...inner,
|
|
357
|
+
run(tree) {
|
|
358
|
+
const decided = inner.run(tree);
|
|
359
|
+
const pkg = tree.packageJson;
|
|
360
|
+
if (!pkg?.unreadable) return decided;
|
|
361
|
+
const u = {
|
|
362
|
+
anchor: pkg.path,
|
|
363
|
+
reason: `package.json is present but could not be parsed (${pkg.unreadable})`
|
|
364
|
+
};
|
|
365
|
+
if (opts.authoritative && decided.state !== "could-not-judge") {
|
|
366
|
+
return couldNotJudge(inner.id, u.anchor, `${u.anchor}: ${u.reason}`);
|
|
367
|
+
}
|
|
368
|
+
return settle(inner.id, decided, [u]);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/detectors/dLocalIdentity.ts
|
|
374
|
+
var CLOUD_IDENTITY_PACKAGE = "@vantageos/cloud-identity";
|
|
375
|
+
var EXEMPT_MARKER = "allow-local-identity:";
|
|
376
|
+
var MIN_JUSTIFICATION = 8;
|
|
377
|
+
var TENANT_NAMES = "orgId|org_id|orgSlug|org_slug|tenantId|tenant_id";
|
|
378
|
+
var TENANT_PROP = new RegExp(`\\.(${TENANT_NAMES})$`, "i");
|
|
379
|
+
var unbracket = (x) => x.replace(/\??\.?\[\s*["'`]([\w$]+)["'`]\s*\]/g, ".$1").replace(/\?/g, "");
|
|
380
|
+
var FOLD_NAMES = "toLowerCase|toUpperCase|toLocaleLowerCase|toLocaleUpperCase|trim|normalize";
|
|
381
|
+
var FOLD_CALL = String.raw`(?:\s*\.\s*(?:${FOLD_NAMES})\s*\([^()]*\))*`;
|
|
382
|
+
var PATH = String.raw`[\w$.?]+(?![\w$])(?<!\.(?:${FOLD_NAMES}))`;
|
|
383
|
+
var OPERAND = String.raw`${PATH}(?:\[\s*["\`][\w$]+["\`]\s*\]\??[\w$.?]*)*${FOLD_CALL}`;
|
|
384
|
+
var NON_OPERAND = /* @__PURE__ */ new Set(["undefined", "null", "true", "false", "NaN"]);
|
|
385
|
+
var isLiteral = (x) => NON_OPERAND.has(x) || /^-?\d/.test(x);
|
|
386
|
+
var unfold = (x) => unbracket(x.replace(new RegExp(`${FOLD_CALL}$`), ""));
|
|
387
|
+
var lastSegment = (x) => x.split(".").pop() ?? "";
|
|
388
|
+
var ACTING_FIELD = /^(createdBy|assignedTo|assignee|from|fromOrchestrator|toOrchestrator|recipient|sender|callerOrchestrator|orchestrator|orchestratorName|actor|actorName|actingAs|claimedBy|callerName|agentName|fromName)$/;
|
|
389
|
+
var ACTING_BARE = /^(createdBy|assignedTo|assignee|fromOrchestrator|toOrchestrator|callerOrchestrator|actingAs|claimedBy)$/;
|
|
390
|
+
var CALLERISH = /^(caller\w*|actor\w*|acting\w*|claimed\w*|\w*Orchestrator|\w*AgentName)$/i;
|
|
391
|
+
var NAME_FIELD = /^(name|agentName|orchestratorName|displayName)$/;
|
|
392
|
+
var isActing = (x) => ACTING_FIELD.test(lastSegment(x)) && (x.includes(".") || ACTING_BARE.test(x)) || x.includes(".") && CALLERISH.test(lastSegment(x));
|
|
393
|
+
var isNameField = (x) => x.includes(".") && NAME_FIELD.test(lastSegment(x));
|
|
394
|
+
var isBareCallerish = (x) => !x.includes(".") && CALLERISH.test(x);
|
|
395
|
+
var comparesTenants = (a, b) => TENANT_PROP.test(a) || TENANT_PROP.test(b);
|
|
396
|
+
var comparesNames = (a, b) => isActing(a) && (isActing(b) || CALLERISH.test(lastSegment(b)) || isNameField(b)) || isActing(b) && (CALLERISH.test(lastSegment(a)) || isNameField(a)) || isBareCallerish(a) && isNameField(b) || isBareCallerish(b) && isNameField(a);
|
|
397
|
+
var comparesIdentity = (a, b) => comparesTenants(a, b) || comparesNames(a, b);
|
|
398
|
+
function operands(x, y) {
|
|
399
|
+
const [a, b] = [unfold(x), unfold(y)];
|
|
400
|
+
return isLiteral(a) || isLiteral(b) ? null : [a, b];
|
|
401
|
+
}
|
|
402
|
+
var CMP = String.raw`(${OPERAND})\s*[!=]==?\s*(${OPERAND})`;
|
|
403
|
+
var OBJECT_IS = String.raw`\bObject\s*\.\s*is\s*\(\s*(${OPERAND})\s*,\s*(${OPERAND})\s*\)`;
|
|
404
|
+
var acceptWith = (pred) => (m) => {
|
|
405
|
+
const ops = operands(m[1], m[2]);
|
|
406
|
+
return ops !== null && pred(ops[0], ops[1]);
|
|
407
|
+
};
|
|
408
|
+
var STRING_LITERAL = String.raw`(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\`[^\`$\\]*\`)`;
|
|
409
|
+
var LITERAL_CONCAT = new RegExp(String.raw`^${STRING_LITERAL}(?:\s*\+\s*${STRING_LITERAL})*$`);
|
|
410
|
+
function staticConstants(file) {
|
|
411
|
+
const out = /* @__PURE__ */ new Set();
|
|
412
|
+
const decl = /^(?:export\s+)?const\s+([\w$]+)\s*(?::[^=]+)?=\s*(.*)$/;
|
|
413
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
414
|
+
const m = file.lines[i].raw.match(decl);
|
|
415
|
+
if (!m) continue;
|
|
416
|
+
let init = m[2];
|
|
417
|
+
for (let j = i + 1; !/;\s*$/.test(init) && j < Math.min(file.lines.length, i + 40); j++) {
|
|
418
|
+
init += `
|
|
419
|
+
${file.lines[j].raw}`;
|
|
420
|
+
}
|
|
421
|
+
const body = init.replace(/;\s*$/, "").trim();
|
|
422
|
+
if (LITERAL_CONCAT.test(body)) out.add(m[1]);
|
|
423
|
+
}
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
function callArgs(text, from) {
|
|
427
|
+
const args = [];
|
|
428
|
+
let depth = 0;
|
|
429
|
+
let start = from;
|
|
430
|
+
for (let i = from; i < text.length; i++) {
|
|
431
|
+
const c = text[i];
|
|
432
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
433
|
+
for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
437
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
438
|
+
if (depth === 0) {
|
|
439
|
+
args.push(text.slice(start, i).trim());
|
|
440
|
+
return { args, end: i + 1 };
|
|
441
|
+
}
|
|
442
|
+
depth--;
|
|
443
|
+
} else if (c === "," && depth === 0) {
|
|
444
|
+
args.push(text.slice(start, i).trim());
|
|
445
|
+
start = i + 1;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
function isStaticInput(expr, statics) {
|
|
451
|
+
const e = expr.trim();
|
|
452
|
+
if (LITERAL_CONCAT.test(e) || statics.has(e)) return true;
|
|
453
|
+
const wrapped = e.match(/^(?:new\s+TextEncoder\s*\(\s*\)\s*\.\s*encode|Buffer\s*\.\s*from)\s*\(/);
|
|
454
|
+
if (!wrapped) return false;
|
|
455
|
+
const inner = callArgs(e, wrapped[0].length);
|
|
456
|
+
return inner !== null && inner.end === e.length && isStaticInput(inner.args[0], statics);
|
|
457
|
+
}
|
|
458
|
+
function textFrom(ctx, extra = 12) {
|
|
459
|
+
const { file, line, idx } = ctx;
|
|
460
|
+
const tail = file.lines.slice(line.n, line.n + extra).map((l) => l.raw);
|
|
461
|
+
return [line.raw.slice(idx), ...tail].join("\n");
|
|
462
|
+
}
|
|
463
|
+
function blockFrom(text, from) {
|
|
464
|
+
const open = text.indexOf("{", from);
|
|
465
|
+
if (open < 0) return null;
|
|
466
|
+
let depth = 0;
|
|
467
|
+
for (let i = open; i < text.length; i++) {
|
|
468
|
+
const c = text[i];
|
|
469
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
470
|
+
for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (c === "{") depth++;
|
|
474
|
+
else if (c === "}" && --depth === 0) return text.slice(open, i + 1);
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
function switchComparesIdentity(m, ctx) {
|
|
479
|
+
if (!ctx) return false;
|
|
480
|
+
const subject = unfold(m[1]);
|
|
481
|
+
const body = blockFrom(textFrom(ctx, 60), m[0].length);
|
|
482
|
+
if (body === null) return false;
|
|
483
|
+
const label = new RegExp(String.raw`\bcase\s+(${OPERAND})\s*:`, "g");
|
|
484
|
+
for (const c of body.matchAll(label)) {
|
|
485
|
+
const ops = operands(subject, c[1]);
|
|
486
|
+
if (ops !== null && comparesIdentity(ops[0], ops[1])) return true;
|
|
487
|
+
}
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
function literalHoldsIdentity(m) {
|
|
491
|
+
const arg = unfold(m[2]);
|
|
492
|
+
return m[1].split(",").map((e) => unfold(e.trim())).some((e) => e !== "" && !isLiteral(e) && !isLiteral(arg) && comparesIdentity(e, arg));
|
|
493
|
+
}
|
|
494
|
+
function digestsStaticInput(m, ctx) {
|
|
495
|
+
if (!ctx) return false;
|
|
496
|
+
const text = textFrom(ctx);
|
|
497
|
+
const open = text.indexOf("(");
|
|
498
|
+
if (open < 0) return false;
|
|
499
|
+
const call = callArgs(text, open + 1);
|
|
500
|
+
if (!call) return false;
|
|
501
|
+
if (/^subtle/.test(m[0])) {
|
|
502
|
+
return m[1] === "digest" && call.args.length >= 2 && isStaticInput(call.args[1], ctx.statics);
|
|
503
|
+
}
|
|
504
|
+
if (!/^createHash/.test(m[0])) return false;
|
|
505
|
+
const update = text.slice(call.end).match(/^\s*\.\s*update\s*\(/);
|
|
506
|
+
if (!update) return false;
|
|
507
|
+
const data = callArgs(text, call.end + update[0].length);
|
|
508
|
+
return data !== null && isStaticInput(data.args[0], ctx.statics);
|
|
509
|
+
}
|
|
510
|
+
var RENAMEABLE = [
|
|
511
|
+
"createHash",
|
|
512
|
+
"createHmac",
|
|
513
|
+
"timingSafeEqual",
|
|
514
|
+
"jwtVerify",
|
|
515
|
+
"jwtDecrypt",
|
|
516
|
+
"createRemoteJWKSet",
|
|
517
|
+
"createLocalJWKSet",
|
|
518
|
+
"decodeJwt",
|
|
519
|
+
"compactVerify",
|
|
520
|
+
"verifyToken"
|
|
521
|
+
];
|
|
522
|
+
function renamedPrimitives(file) {
|
|
523
|
+
const text = file.lines.map((l) => l.raw).join("\n");
|
|
524
|
+
const out = [];
|
|
525
|
+
const named = /import\s+(type\s+)?\{([^}]*)\}\s*from\s*["']([^"']+)["']/g;
|
|
526
|
+
for (const m of text.matchAll(named)) {
|
|
527
|
+
if (m[1] || m[3].startsWith(CLOUD_IDENTITY_PACKAGE)) continue;
|
|
528
|
+
for (const part of m[2].split(",")) {
|
|
529
|
+
const as = part.trim().match(/^([\w$]+)\s+as\s+([\w$]+)$/);
|
|
530
|
+
if (as && RENAMEABLE.includes(as[1])) out.push([as[1], as[2]]);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const destructured = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:await\s+)?(?:require|import)\s*\(\s*["']([^"']+)["']/g;
|
|
534
|
+
for (const m of text.matchAll(destructured)) {
|
|
535
|
+
if (m[2].startsWith(CLOUD_IDENTITY_PACKAGE)) continue;
|
|
536
|
+
for (const part of m[1].split(",")) {
|
|
537
|
+
const as = part.trim().match(/^([\w$]+)\s*:\s*([\w$]+)$/);
|
|
538
|
+
if (as && RENAMEABLE.includes(as[1])) out.push([as[1], as[2]]);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
var escapeRe = (x) => x.replace(/[$]/g, "\\$&");
|
|
544
|
+
function aliasMatchers(file) {
|
|
545
|
+
const out = [];
|
|
546
|
+
for (const [orig, alias] of renamedPrimitives(file)) {
|
|
547
|
+
for (const mt of MATCHERS) {
|
|
548
|
+
if (!mt.re.source.includes(orig)) continue;
|
|
549
|
+
const re = new RegExp(mt.re.source.split(orig).join(escapeRe(alias)), mt.re.flags);
|
|
550
|
+
const { accept } = mt;
|
|
551
|
+
out.push({
|
|
552
|
+
...mt,
|
|
553
|
+
re,
|
|
554
|
+
// The predicate reads the matched text; give it the original callee name.
|
|
555
|
+
accept: accept ? (m, ctx) => {
|
|
556
|
+
const fake = Object.assign(
|
|
557
|
+
[...m].map((x) => x?.replace(alias, orig)),
|
|
558
|
+
{
|
|
559
|
+
index: m.index,
|
|
560
|
+
input: m.input
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
return accept(fake, ctx);
|
|
564
|
+
} : void 0
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
var MATCHERS = [
|
|
571
|
+
{
|
|
572
|
+
id: "local-jwt-verify",
|
|
573
|
+
category: "resolves-caller",
|
|
574
|
+
re: /\b(jwtVerify|jwtDecrypt|createRemoteJWKSet|createLocalJWKSet|decodeJwt|compactVerify|verifyToken)\s*\(/,
|
|
575
|
+
callee: 1,
|
|
576
|
+
sample: "const { payload } = await jwtVerify(token, jwks);"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
id: "local-jwt-verify",
|
|
580
|
+
category: "resolves-caller",
|
|
581
|
+
re: /\b(jwt|jsonwebtoken)\.(verify|decode)\s*\(/,
|
|
582
|
+
sample: "const claims = jwt.verify(token, key);"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
id: "raw-claim-read",
|
|
586
|
+
category: "resolves-caller",
|
|
587
|
+
re: /\b(payload|claims|decoded)\??\.(org_id|org_slug|org_role|orgId|orgSlug|orgRole)\b/,
|
|
588
|
+
sample: "const orgId = payload.org_id as string;"
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
// A populated grant attached locally: a non-empty literal, or a value read
|
|
592
|
+
// off another object (a mapping row). A pass-through copy of the same
|
|
593
|
+
// field (`fromAllowList: ctx.fromAllowList`) grants nothing new.
|
|
594
|
+
id: "local-grant",
|
|
595
|
+
category: "resolves-caller",
|
|
596
|
+
re: /\bfromAllowList\s*:\s*(\[\s*[^\]\s][^\]]*\]|[\w$]+(?:\??\.[\w$]+)+)/,
|
|
597
|
+
accept: (m) => !/\.fromAllowList$/.test(m[1]),
|
|
598
|
+
sample: 'fromAllowList: ["*"],'
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
// A role compared to a value. A presence test (`orgRole !== undefined`,
|
|
602
|
+
// forwarding the claim on) asserts nothing and is not a site.
|
|
603
|
+
id: "org-role-assert",
|
|
604
|
+
category: "asserts-membership",
|
|
605
|
+
re: /\b(orgRole|org_role)\s*[!=]==\s*([^\s);&|?:]+)/,
|
|
606
|
+
accept: (m) => !NON_OPERAND.has(m[2]),
|
|
607
|
+
sample: 'if (orgRole === "org:admin") return true;'
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
id: "org-role-assert",
|
|
611
|
+
category: "asserts-membership",
|
|
612
|
+
re: /[!=]==\s*["'`]org:|\.has\(\s*\{\s*(role|permission)\s*:/,
|
|
613
|
+
sample: 'if (auth.has({ role: "org:admin" })) return true;'
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
// Identifying an agent by NAME: a typed name compared to a roster or an
|
|
617
|
+
// allow-list, by method or by iteration.
|
|
618
|
+
id: "name-allowlist",
|
|
619
|
+
category: "filters-by-scope",
|
|
620
|
+
re: /\b[\w$]*(roster|allow_?list|allowed_?orchestrators)\s*\??\.\s*(includes|indexOf|some|find|findIndex|every|has)\s*\(/i,
|
|
621
|
+
sample: "if (ctx.fromAllowList.includes(from)) return null;"
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "name-allowlist",
|
|
625
|
+
category: "filters-by-scope",
|
|
626
|
+
re: /\b(isIn\w*Allow_?list)\s*\(/i,
|
|
627
|
+
callee: 1,
|
|
628
|
+
sample: "if (!isInAllowList(list, name)) return false;"
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
id: "name-allowlist",
|
|
632
|
+
category: "filters-by-scope",
|
|
633
|
+
re: /\bof\s+[\w$.?]*(roster|allow_?list|allowed_?orchestrators)\s*\)/i,
|
|
634
|
+
sample: "for (const entry of ctx.fromAllowList) {"
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
// A row kept or dropped by comparing its tenant to the caller's: ===, !==, == or !=.
|
|
638
|
+
id: "tenant-row-filter",
|
|
639
|
+
category: "filters-by-scope",
|
|
640
|
+
re: new RegExp(CMP),
|
|
641
|
+
accept: acceptWith(comparesTenants),
|
|
642
|
+
sample: "return row.orgId == ctx.actor.orgSlug;"
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: "tenant-row-filter",
|
|
646
|
+
category: "filters-by-scope",
|
|
647
|
+
re: new RegExp(OBJECT_IS),
|
|
648
|
+
accept: acceptWith(comparesTenants),
|
|
649
|
+
sample: "return Object.is(row.orgId, ctx.actor.orgSlug);"
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
// A tenant passed to a membership test: `callerOrgs.includes(row.orgId)`.
|
|
653
|
+
id: "tenant-row-filter",
|
|
654
|
+
category: "filters-by-scope",
|
|
655
|
+
re: new RegExp(
|
|
656
|
+
String.raw`\.(includes|has|indexOf|some|find|findIndex)\s*\(\s*(${OPERAND})\s*[,)]`
|
|
657
|
+
),
|
|
658
|
+
accept: (m) => TENANT_PROP.test(unfold(m[2])),
|
|
659
|
+
sample: "return callerOrgs.includes(row.orgId);"
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
// Identifying an agent by NAME through a normaliser: either side of a strict
|
|
663
|
+
// comparison is a call to `normalize...Id/Name/Orchestrator/Agent/...`.
|
|
664
|
+
id: "name-compare",
|
|
665
|
+
category: "filters-by-scope",
|
|
666
|
+
re: /\bnormali[sz]e\w*(id|name|orchestrator|agent|label|slug|handle)\w*\s*\([^()]*(?:\([^()]*\)[^()]*)*\)\s*[!=]==|[!=]==\s*normali[sz]e\w*(id|name|orchestrator|agent|label|slug|handle)\w*\s*\(/i,
|
|
667
|
+
sample: "return normalizeOrchestratorId(claimedName) === only;"
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
// A typed or argument name compared straight to an allow-list / roster entry.
|
|
671
|
+
id: "name-compare",
|
|
672
|
+
category: "filters-by-scope",
|
|
673
|
+
re: /(?:\b[\w$.?]*(?:roster|allow_?list|allowed_?orchestrators)\s*\??\.?\[[^\]]*\]\s*[!=]==|[!=]==\s*[\w$.?]*(?:roster|allow_?list|allowed_?orchestrators)\s*\??\.?\[)/i,
|
|
674
|
+
sample: "return claimedName === ctx.fromAllowList[0];"
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
// Identifying an agent by NAME through its acting-name fields: who made a row
|
|
678
|
+
// against who is asking. ===, !==, == and !=, case-folded or not.
|
|
679
|
+
id: "name-compare",
|
|
680
|
+
category: "filters-by-scope",
|
|
681
|
+
re: new RegExp(CMP),
|
|
682
|
+
accept: acceptWith(comparesNames),
|
|
683
|
+
sample: "return row.assignedTo.toLowerCase() !== a.callerOrchestrator.toLowerCase();"
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: "name-compare",
|
|
687
|
+
category: "filters-by-scope",
|
|
688
|
+
re: new RegExp(OBJECT_IS),
|
|
689
|
+
accept: acceptWith(comparesNames),
|
|
690
|
+
sample: "return Object.is(row.createdBy, m.from);"
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
// `switch (row.createdBy) { case caller: }` is the same compare, spelled as a jump table.
|
|
694
|
+
id: "name-compare",
|
|
695
|
+
category: "filters-by-scope",
|
|
696
|
+
re: new RegExp(String.raw`\bswitch\s*\(\s*(${OPERAND})\s*\)`),
|
|
697
|
+
accept: (m, ctx) => switchComparesIdentity(m, ctx),
|
|
698
|
+
sample: "switch (row.createdBy) { case caller: return true; }"
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
// `[caller].includes(row.createdBy)`: a membership test against a literal that holds the caller.
|
|
702
|
+
id: "name-compare",
|
|
703
|
+
category: "filters-by-scope",
|
|
704
|
+
re: new RegExp(
|
|
705
|
+
String.raw`(?<![\w$\])])\[([^\[\]]*)\]\s*\.\s*(?:includes|indexOf)\s*\(\s*(${OPERAND})\s*[,)]`
|
|
706
|
+
),
|
|
707
|
+
accept: (m) => literalHoldsIdentity(m),
|
|
708
|
+
sample: "return [caller].includes(row.createdBy);"
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
id: "name-compare",
|
|
712
|
+
category: "filters-by-scope",
|
|
713
|
+
re: new RegExp(
|
|
714
|
+
String.raw`\bnew\s+Set\s*\(\s*\[([^\[\]]*)\]\s*\)\s*\.\s*has\s*\(\s*(${OPERAND})\s*[,)]`
|
|
715
|
+
),
|
|
716
|
+
accept: (m) => literalHoldsIdentity(m),
|
|
717
|
+
sample: "return new Set([caller]).has(row.assignedTo);"
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
// `a.localeCompare(b) === 0` is an equality test, whichever way round it is written.
|
|
721
|
+
id: "name-compare",
|
|
722
|
+
category: "filters-by-scope",
|
|
723
|
+
re: new RegExp(
|
|
724
|
+
String.raw`(${OPERAND})\s*\.\s*localeCompare\s*\(\s*(${OPERAND})\s*(?:,[^()]*(?:\([^()]*\)[^()]*)*)?\)\s*[!=]==?\s*0(?![\w$.])`
|
|
725
|
+
),
|
|
726
|
+
accept: acceptWith(comparesIdentity),
|
|
727
|
+
sample: "return row.assignedTo.localeCompare(a.callerOrchestrator) === 0;"
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
id: "name-compare",
|
|
731
|
+
category: "filters-by-scope",
|
|
732
|
+
re: new RegExp(
|
|
733
|
+
String.raw`(?<![\w$.])0\s*[!=]==?\s*(${OPERAND})\s*\.\s*localeCompare\s*\(\s*(${OPERAND})\s*[,)]`
|
|
734
|
+
),
|
|
735
|
+
accept: acceptWith(comparesIdentity),
|
|
736
|
+
sample: "return 0 === row.assignedTo.localeCompare(a.callerOrchestrator);"
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
id: "local-secret-compare",
|
|
740
|
+
category: "validates-credential",
|
|
741
|
+
re: /\b(timingSafeEqual)\s*\(/,
|
|
742
|
+
callee: 1,
|
|
743
|
+
sample: "return timingSafeEqual(a, b);"
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
// Any digest of a presented credential, whatever the algorithm.
|
|
747
|
+
id: "local-digest",
|
|
748
|
+
category: "validates-credential",
|
|
749
|
+
// A digest over static content (a literal, or a constant made of literals) is
|
|
750
|
+
// not a digest of a credential. HMAC and sign/verify always take a key: a site.
|
|
751
|
+
re: /\bcreateHash\s*\(|\bcreateHmac\s*\(|\bsubtle\s*\.\s*(digest|sign|verify)\s*\(/,
|
|
752
|
+
accept: (m, ctx) => !digestsStaticInput(m, ctx),
|
|
753
|
+
sample: 'const h = await crypto.subtle.digest("SHA-256", data);'
|
|
754
|
+
}
|
|
755
|
+
];
|
|
756
|
+
function biteSample(mt, text) {
|
|
757
|
+
const m = text.match(mt.re);
|
|
758
|
+
if (m === null) return false;
|
|
759
|
+
if (!mt.accept) return true;
|
|
760
|
+
const line = { n: 1, raw: text, comment: false };
|
|
761
|
+
const file = { path: "sample", rel: "sample", lines: [line] };
|
|
762
|
+
return mt.accept(m, { file, line, idx: m.index ?? 0, statics: /* @__PURE__ */ new Set() });
|
|
763
|
+
}
|
|
764
|
+
function localIdentityPositiveControls() {
|
|
765
|
+
const out = {};
|
|
766
|
+
MATCHERS.forEach((mt, i) => {
|
|
767
|
+
out[`${mt.id}#${i}`] = biteSample(mt, mt.sample);
|
|
768
|
+
});
|
|
769
|
+
const bites = (line) => MATCHERS.some((mt) => biteSample(mt, line));
|
|
770
|
+
out["negative:pass-through-grant"] = !bites("fromAllowList: ctx.fromAllowList,");
|
|
771
|
+
out["negative:delegated-bearer"] = !MATCHERS.some(
|
|
772
|
+
(mt) => mt.re.test("const r = await validateMasterBearer(authHeader, secret);")
|
|
773
|
+
);
|
|
774
|
+
out["negative:role-presence-test"] = !bites(
|
|
775
|
+
"...(ctx.orgRole !== undefined ? { role: ctx.orgRole } : {}),"
|
|
776
|
+
);
|
|
777
|
+
out["negative:undefined-compare"] = !bites("if (row.orgId === undefined) return;");
|
|
778
|
+
out["negative:acting-field-vs-nullish"] = !bites("if (row.assignedTo !== undefined) return;");
|
|
779
|
+
out["negative:acting-field-vs-literal"] = !bites('return t.createdBy === "system";');
|
|
780
|
+
out["negative:unrelated-field-compare"] = !bites("return page.from === page.to;");
|
|
781
|
+
out["negative:switch-on-unrelated-field"] = !bites('switch (row.kind) { case "a": return 1; }');
|
|
782
|
+
out["negative:literal-array-unrelated"] = !bites("return [kind].includes(row.status);");
|
|
783
|
+
out["negative:localecompare-unrelated"] = !bites("return a.name.localeCompare(b.name) === 0;");
|
|
784
|
+
out["negative:object-is-nullish"] = !bites("return Object.is(row.orgId, undefined);");
|
|
785
|
+
return out;
|
|
786
|
+
}
|
|
787
|
+
function cloudIdentityBindings(file) {
|
|
788
|
+
const text = file.lines.map((l) => l.raw).join("\n");
|
|
789
|
+
const names = /* @__PURE__ */ new Set();
|
|
790
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
791
|
+
const pkg = `@vantageos\\/cloud-identity(?:\\/[^"']*)?`;
|
|
792
|
+
const named = new RegExp(`import\\s+(type\\s+)?\\{([^}]*)\\}\\s*from\\s*["']${pkg}["']`, "g");
|
|
793
|
+
for (const m of text.matchAll(named)) {
|
|
794
|
+
if (m[1]) continue;
|
|
795
|
+
for (const part of m[2].split(",")) {
|
|
796
|
+
const p = part.trim();
|
|
797
|
+
if (!p || p.startsWith("type ")) continue;
|
|
798
|
+
const as = p.match(/^[\w$]+\s+as\s+([\w$]+)$/);
|
|
799
|
+
names.add(as ? as[1] : p);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const ns = new RegExp(`import\\s+\\*\\s+as\\s+([\\w$]+)\\s+from\\s*["']${pkg}["']`, "g");
|
|
803
|
+
for (const m of text.matchAll(ns)) namespaces.add(m[1]);
|
|
804
|
+
return { names, namespaces };
|
|
805
|
+
}
|
|
806
|
+
function isDelegated(raw, start, callee, b) {
|
|
807
|
+
const before = raw.slice(0, start);
|
|
808
|
+
const qualified = before.match(/([\w$]+)\s*\.\s*$/);
|
|
809
|
+
if (qualified) return b.namespaces.has(qualified[1]);
|
|
810
|
+
return b.names.has(callee);
|
|
811
|
+
}
|
|
812
|
+
function countDelegatedCalls(file, b) {
|
|
813
|
+
if (b.names.size === 0 && b.namespaces.size === 0) return 0;
|
|
814
|
+
let n = 0;
|
|
815
|
+
for (const line of file.lines) {
|
|
816
|
+
if (line.comment) continue;
|
|
817
|
+
for (const m of line.raw.matchAll(/([\w$]+)\s*\(/g)) {
|
|
818
|
+
const idx = m.index ?? 0;
|
|
819
|
+
if (isCodeAt(file, line.n, idx) && isDelegated(line.raw, idx, m[1], b)) n++;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return n;
|
|
823
|
+
}
|
|
824
|
+
var DECL = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\s*\*?\s*([\w$]+)|(?:const|let|var|class)\s+([\w$]+))/;
|
|
825
|
+
function enclosingDecl(file, n) {
|
|
826
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
827
|
+
const l = file.lines[i];
|
|
828
|
+
if (l.comment) continue;
|
|
829
|
+
const m = l.raw.match(DECL);
|
|
830
|
+
if (m) return { name: m[1] ?? m[2], line: l.n };
|
|
831
|
+
}
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
function justificationIn(line, file) {
|
|
835
|
+
const at = line.raw.indexOf(EXEMPT_MARKER);
|
|
836
|
+
if (at < 0) return null;
|
|
837
|
+
if (!line.comment && isCodeAt(file, line.n, at)) return null;
|
|
838
|
+
if (!line.comment && !/\/\/|\/\*/.test(line.raw.slice(0, at))) return null;
|
|
839
|
+
return line.raw.slice(at + EXEMPT_MARKER.length).replace(/\*\/\s*$/, "").trim();
|
|
840
|
+
}
|
|
841
|
+
function commentBlockAbove(file, n) {
|
|
842
|
+
const out = [];
|
|
843
|
+
for (let i = n - 2; i >= 0; i--) {
|
|
844
|
+
const l = file.lines[i];
|
|
845
|
+
if (!l.comment) break;
|
|
846
|
+
out.push(l);
|
|
847
|
+
}
|
|
848
|
+
return out;
|
|
849
|
+
}
|
|
850
|
+
function findJustification(file, line, decl) {
|
|
851
|
+
const candidates = [line, ...commentBlockAbove(file, line.n)];
|
|
852
|
+
if (decl) candidates.push(...commentBlockAbove(file, decl.line));
|
|
853
|
+
for (const c of candidates) {
|
|
854
|
+
const j = justificationIn(c, file);
|
|
855
|
+
if (j !== null) return { text: j, valid: j.length >= MIN_JUSTIFICATION };
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
function unreadableReason(file) {
|
|
860
|
+
if (file.lines.some((l) => l.raw.includes("\0") || l.raw.includes("\uFFFD"))) {
|
|
861
|
+
return "file is not valid UTF-8 text";
|
|
862
|
+
}
|
|
863
|
+
const pairs = [
|
|
864
|
+
["{", "}"],
|
|
865
|
+
["(", ")"],
|
|
866
|
+
["[", "]"]
|
|
867
|
+
];
|
|
868
|
+
const off = [];
|
|
869
|
+
for (const [open, close] of pairs) {
|
|
870
|
+
let net = 0;
|
|
871
|
+
for (const l of file.lines) net += countInCode(file, l, open) - countInCode(file, l, close);
|
|
872
|
+
if (net !== 0) off.push(`${open}${close} ${net > 0 ? "+" : ""}${net}`);
|
|
873
|
+
}
|
|
874
|
+
return off.length > 0 ? `brackets never balance (${off.join(", ")}) \u2014 the file cannot be read as code` : null;
|
|
875
|
+
}
|
|
876
|
+
function findLocalIdentitySites(tree) {
|
|
877
|
+
const pkg = tree.packageJson;
|
|
878
|
+
const selfExempt = !pkg?.unreadable && pkg?.json.name === CLOUD_IDENTITY_PACKAGE;
|
|
879
|
+
const scan = {
|
|
880
|
+
selfExempt,
|
|
881
|
+
violations: [],
|
|
882
|
+
exempt: [],
|
|
883
|
+
delegatedCalls: 0,
|
|
884
|
+
unreadable: []
|
|
885
|
+
};
|
|
886
|
+
for (const file of tree.files) {
|
|
887
|
+
const bad = unreadableReason(file);
|
|
888
|
+
if (bad) scan.unreadable.push({ anchor: file.rel, reason: bad });
|
|
889
|
+
const bindings = cloudIdentityBindings(file);
|
|
890
|
+
const statics = staticConstants(file);
|
|
891
|
+
const catalog = [...MATCHERS, ...aliasMatchers(file)];
|
|
892
|
+
scan.delegatedCalls += countDelegatedCalls(file, bindings);
|
|
893
|
+
for (const line of file.lines) {
|
|
894
|
+
if (line.comment) continue;
|
|
895
|
+
let hit = null;
|
|
896
|
+
for (const mt of catalog) {
|
|
897
|
+
const g = new RegExp(mt.re.source, `${mt.re.flags.replace("g", "")}g`);
|
|
898
|
+
for (const m of line.raw.matchAll(g)) {
|
|
899
|
+
const idx = m.index ?? 0;
|
|
900
|
+
if (!isCodeAt(file, line.n, idx)) continue;
|
|
901
|
+
if (mt.accept && !mt.accept(m, { file, line, idx, statics })) continue;
|
|
902
|
+
const callee = mt.callee !== void 0 ? m[mt.callee] : void 0;
|
|
903
|
+
if (callee && isDelegated(line.raw, idx, callee, bindings)) continue;
|
|
904
|
+
hit = mt;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
if (hit) break;
|
|
908
|
+
}
|
|
909
|
+
if (!hit) continue;
|
|
910
|
+
const decl = enclosingDecl(file, line.n);
|
|
911
|
+
const site = {
|
|
912
|
+
file: file.rel,
|
|
913
|
+
line: line.n,
|
|
914
|
+
primitive: hit.id,
|
|
915
|
+
category: hit.category,
|
|
916
|
+
enclosing: decl
|
|
917
|
+
};
|
|
918
|
+
const j = findJustification(file, line, decl);
|
|
919
|
+
if (j?.valid) scan.exempt.push({ ...site, justification: j.text });
|
|
920
|
+
else scan.violations.push(site);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return scan;
|
|
924
|
+
}
|
|
925
|
+
function describeSite(s) {
|
|
926
|
+
const where = s.enclosing ? ` in ${s.enclosing.name} (declared :${s.enclosing.line})` : "";
|
|
927
|
+
return `${s.file}:${s.line} [${s.category}/${s.primitive}]${where}`;
|
|
928
|
+
}
|
|
929
|
+
function exemptSuffix(exempt) {
|
|
930
|
+
if (exempt.length === 0) return "";
|
|
931
|
+
const rows = exempt.map((e) => `${describeSite(e)} \u2014 "${e.justification}"`);
|
|
932
|
+
return `; EXEMPT (${exempt.length}): ${rows.join("; ")}`;
|
|
933
|
+
}
|
|
934
|
+
function makeLocalIdentity(opts = {}) {
|
|
935
|
+
const ceiling = opts.ceiling ?? 0;
|
|
936
|
+
if (!Number.isInteger(ceiling) || ceiling < 0) {
|
|
937
|
+
throw new Error(`local-identity ceiling must be a non-negative integer, got ${ceiling}`);
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
id: "DLocalIdentity no-local-identity",
|
|
941
|
+
run(tree) {
|
|
942
|
+
const controls = localIdentityPositiveControls();
|
|
943
|
+
const failed = Object.entries(controls).filter(([, ok]) => !ok);
|
|
944
|
+
if (failed.length > 0) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
`DLocalIdentity positive control FAILED: ${failed.map(([k]) => k).join(", ")} \u2014 matcher broken, verdict untrustworthy`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
const id = this.id;
|
|
950
|
+
const s = findLocalIdentitySites(tree);
|
|
951
|
+
if (s.selfExempt) {
|
|
952
|
+
const n = s.violations.length + s.exempt.length;
|
|
953
|
+
return {
|
|
954
|
+
id,
|
|
955
|
+
anchor: tree.packageJson?.path ?? "-",
|
|
956
|
+
state: "present",
|
|
957
|
+
verdict: `EXEMPT: the target is ${CLOUD_IDENTITY_PACKAGE} itself, the one place identity primitives live (${n} primitive site(s) not counted)`
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const listed = s.violations.map(describeSite).join("; ");
|
|
961
|
+
const delegated = `${s.delegatedCalls} call(s) to ${CLOUD_IDENTITY_PACKAGE}`;
|
|
962
|
+
let decided;
|
|
963
|
+
if (s.violations.length > ceiling) {
|
|
964
|
+
const v = s.violations[0];
|
|
965
|
+
decided = {
|
|
966
|
+
id,
|
|
967
|
+
anchor: `${v.file}:${v.line}`,
|
|
968
|
+
state: "non-conforming",
|
|
969
|
+
verdict: `${s.violations.length} local identity primitive(s), ceiling ${ceiling} (positive-control OK): ${listed}${exemptSuffix(s.exempt)} -> delegate to ${CLOUD_IDENTITY_PACKAGE} (validateMasterBearer, verifyClerkSessionToken, isMasterScope, ...) or justify the site with "// ${EXEMPT_MARKER} <reason>"`
|
|
970
|
+
};
|
|
971
|
+
} else if (s.violations.length > 0) {
|
|
972
|
+
const v = s.violations[0];
|
|
973
|
+
decided = {
|
|
974
|
+
id,
|
|
975
|
+
anchor: `${v.file}:${v.line}`,
|
|
976
|
+
state: "present",
|
|
977
|
+
verdict: `${s.violations.length} local identity primitive(s) at or under the ceiling ${ceiling} \u2014 a ratchet, not a clean bill: ${listed}${exemptSuffix(s.exempt)}`
|
|
978
|
+
};
|
|
979
|
+
} else {
|
|
980
|
+
decided = {
|
|
981
|
+
id,
|
|
982
|
+
anchor: s.exempt[0] ? `${s.exempt[0].file}:${s.exempt[0].line}` : "-",
|
|
983
|
+
state: "present",
|
|
984
|
+
verdict: `0 local identity primitives; ${delegated} (positive-control OK)${exemptSuffix(s.exempt)}`
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
return settle(id, decided, s.unreadable);
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
var localIdentity = makeLocalIdentity();
|
|
992
|
+
|
|
8
993
|
// src/detectors/c7-artifact-verifiable.ts
|
|
9
|
-
import { existsSync, readFileSync } from "fs";
|
|
10
|
-
import { join } from "path";
|
|
994
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
995
|
+
import { join as join2 } from "path";
|
|
11
996
|
var PIN = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
12
997
|
var DOC_FILES = ["README.md", "README.fr.md"];
|
|
13
998
|
var INSTALL_RE = /\b(?:npm\s+(?:install|i)|npx|pnpm\s+add|yarn\s+add)\s+(@?[\w./-]+)/i;
|
|
@@ -46,10 +1031,10 @@ function c7PositiveControls() {
|
|
|
46
1031
|
}
|
|
47
1032
|
function readDocText(root) {
|
|
48
1033
|
for (const name of DOC_FILES) {
|
|
49
|
-
const path =
|
|
50
|
-
if (!
|
|
1034
|
+
const path = join2(root, name);
|
|
1035
|
+
if (!existsSync2(path)) continue;
|
|
51
1036
|
try {
|
|
52
|
-
return { text:
|
|
1037
|
+
return { text: readFileSync3(path, "utf8"), file: name };
|
|
53
1038
|
} catch {
|
|
54
1039
|
}
|
|
55
1040
|
}
|
|
@@ -230,8 +1215,46 @@ function findRegisteredTools(tree) {
|
|
|
230
1215
|
}
|
|
231
1216
|
}
|
|
232
1217
|
}
|
|
1218
|
+
for (const file of tree.files) {
|
|
1219
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
1220
|
+
const line = file.lines[i];
|
|
1221
|
+
if (line.comment) continue;
|
|
1222
|
+
const m = line.raw.match(DESCRIPTOR_KEY_RE);
|
|
1223
|
+
if (!m) continue;
|
|
1224
|
+
const indent = m[1];
|
|
1225
|
+
const hasSchema = objectHasKey(file.lines, i, indent);
|
|
1226
|
+
if (hasSchema) push(m[2], `${file.rel}:${line.n}`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
233
1229
|
return out;
|
|
234
1230
|
}
|
|
1231
|
+
var DESCRIPTOR_KEY_RE = new RegExp(`^(\\s*)name:\\s*["'\`](${NAME_LITERAL})["'\`]\\s*,?\\s*$`);
|
|
1232
|
+
var INPUT_SCHEMA_KEY_RE = /^(\s*)inputSchema\s*:/;
|
|
1233
|
+
function objectHasKey(lines, at, indent) {
|
|
1234
|
+
for (const step of [1, -1]) {
|
|
1235
|
+
for (let j = at + step; j >= 0 && j < lines.length; j += step) {
|
|
1236
|
+
const l = lines[j];
|
|
1237
|
+
if (l.comment || l.raw.trim() === "") continue;
|
|
1238
|
+
const lead = l.raw.match(/^(\s*)/)[1];
|
|
1239
|
+
if (lead.length < indent.length) break;
|
|
1240
|
+
if (lead === indent && INPUT_SCHEMA_KEY_RE.test(l.raw)) return true;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
var TOOL_SURFACE_RE = /\bListToolsRequestSchema\b|["'`]tools\/list["'`]|\binputSchema\s*:|\bToolDefinition\b/;
|
|
1246
|
+
function toolSurfaceAnchor(tree) {
|
|
1247
|
+
for (const file of tree.files) {
|
|
1248
|
+
for (const line of file.lines) {
|
|
1249
|
+
if (line.comment) continue;
|
|
1250
|
+
if (TOOL_SURFACE_RE.test(line.raw)) return `${file.rel}:${line.n}`;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
function noToolsLocatedReason(tree, what) {
|
|
1256
|
+
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}`;
|
|
1257
|
+
}
|
|
235
1258
|
var REGISTRATION_CALL_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(/;
|
|
236
1259
|
function findUnresolvedToolSites(tree) {
|
|
237
1260
|
const bindings = resolveNameBindings(tree);
|
|
@@ -1180,7 +2203,7 @@ function c14PositiveControls() {
|
|
|
1180
2203
|
rejectsPartial: missing(partial) !== void 0
|
|
1181
2204
|
};
|
|
1182
2205
|
}
|
|
1183
|
-
var
|
|
2206
|
+
var c14CatalogRegistrationInner = {
|
|
1184
2207
|
id: "C14 catalog-registration",
|
|
1185
2208
|
rule: "C-14",
|
|
1186
2209
|
pin: PIN3,
|
|
@@ -1235,6 +2258,10 @@ var c14CatalogRegistration = {
|
|
|
1235
2258
|
};
|
|
1236
2259
|
}
|
|
1237
2260
|
};
|
|
2261
|
+
var c14CatalogRegistration = guardUnreadablePackageJson(
|
|
2262
|
+
c14CatalogRegistrationInner,
|
|
2263
|
+
{ authoritative: true }
|
|
2264
|
+
);
|
|
1238
2265
|
|
|
1239
2266
|
// src/detectors/c15-annotations-untrusted.ts
|
|
1240
2267
|
var AUTO_APPROVE_ACTION_RE = /\b(?:autoApprove|skipConfirm|autoRun|autoExecute)\w*\s*\(/;
|
|
@@ -1419,11 +2446,11 @@ var c16UntrustedOutput = {
|
|
|
1419
2446
|
};
|
|
1420
2447
|
|
|
1421
2448
|
// src/detectors/c17-supply-chain.ts
|
|
1422
|
-
import { existsSync as
|
|
1423
|
-
import { join as
|
|
2449
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
2450
|
+
import { join as join3 } from "path";
|
|
1424
2451
|
var PIN4 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
1425
2452
|
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
1426
|
-
var WORKFLOW_DIR =
|
|
2453
|
+
var WORKFLOW_DIR = join3(".github", "workflows");
|
|
1427
2454
|
var PROVENANCE_RE2 = /--provenance\b|\bprovenance\s*:\s*true\b|id-token:\s*write/i;
|
|
1428
2455
|
function c17PositiveControls() {
|
|
1429
2456
|
const sample = "npm publish --provenance --access public";
|
|
@@ -1439,7 +2466,7 @@ function c17PositiveControls() {
|
|
|
1439
2466
|
}
|
|
1440
2467
|
function findLockfile(root) {
|
|
1441
2468
|
for (const name of LOCKFILES) {
|
|
1442
|
-
if (
|
|
2469
|
+
if (existsSync3(join3(root, name))) return name;
|
|
1443
2470
|
}
|
|
1444
2471
|
return null;
|
|
1445
2472
|
}
|
|
@@ -1451,20 +2478,20 @@ function findProvenanceClaim(tree, root) {
|
|
|
1451
2478
|
return { site: `${pkg.path}:publishConfig.provenance`, text: "provenance: true" };
|
|
1452
2479
|
}
|
|
1453
2480
|
}
|
|
1454
|
-
const workflowsDir =
|
|
1455
|
-
if (!
|
|
2481
|
+
const workflowsDir = join3(root, WORKFLOW_DIR);
|
|
2482
|
+
if (!existsSync3(workflowsDir)) return null;
|
|
1456
2483
|
let entries = [];
|
|
1457
2484
|
try {
|
|
1458
|
-
entries =
|
|
2485
|
+
entries = readdirSync2(workflowsDir);
|
|
1459
2486
|
} catch {
|
|
1460
2487
|
return null;
|
|
1461
2488
|
}
|
|
1462
2489
|
for (const name of entries) {
|
|
1463
2490
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
1464
|
-
const path =
|
|
2491
|
+
const path = join3(workflowsDir, name);
|
|
1465
2492
|
let text;
|
|
1466
2493
|
try {
|
|
1467
|
-
text =
|
|
2494
|
+
text = readFileSync4(path, "utf8");
|
|
1468
2495
|
} catch {
|
|
1469
2496
|
continue;
|
|
1470
2497
|
}
|
|
@@ -1475,7 +2502,7 @@ function findProvenanceClaim(tree, root) {
|
|
|
1475
2502
|
}
|
|
1476
2503
|
return null;
|
|
1477
2504
|
}
|
|
1478
|
-
var
|
|
2505
|
+
var c17SupplyChainInner = {
|
|
1479
2506
|
id: "C17 supply-chain",
|
|
1480
2507
|
rule: "C-17",
|
|
1481
2508
|
pin: PIN4,
|
|
@@ -1512,19 +2539,22 @@ var c17SupplyChain = {
|
|
|
1512
2539
|
};
|
|
1513
2540
|
}
|
|
1514
2541
|
};
|
|
2542
|
+
var c17SupplyChain = guardUnreadablePackageJson(c17SupplyChainInner, {
|
|
2543
|
+
authoritative: true
|
|
2544
|
+
});
|
|
1515
2545
|
|
|
1516
2546
|
// src/detectors/c18-structure-manifest.ts
|
|
1517
|
-
import { existsSync as
|
|
1518
|
-
import { join as
|
|
2547
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
2548
|
+
import { join as join4 } from "path";
|
|
1519
2549
|
var PIN5 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
1520
2550
|
function manifestPath(root) {
|
|
1521
|
-
const extPath =
|
|
1522
|
-
if (
|
|
1523
|
-
return { kind: "server", path:
|
|
2551
|
+
const extPath = join4(root, "extension.json");
|
|
2552
|
+
if (existsSync4(extPath)) return { kind: "extension", path: extPath };
|
|
2553
|
+
return { kind: "server", path: join4(root, "mcp.json") };
|
|
1524
2554
|
}
|
|
1525
2555
|
function readJson(path) {
|
|
1526
2556
|
try {
|
|
1527
|
-
return JSON.parse(
|
|
2557
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1528
2558
|
} catch {
|
|
1529
2559
|
return null;
|
|
1530
2560
|
}
|
|
@@ -1538,7 +2568,7 @@ function c18PositiveControls() {
|
|
|
1538
2568
|
}
|
|
1539
2569
|
};
|
|
1540
2570
|
}
|
|
1541
|
-
var
|
|
2571
|
+
var c18StructureManifestInner = {
|
|
1542
2572
|
id: "C18 structure-manifest",
|
|
1543
2573
|
rule: "C-18",
|
|
1544
2574
|
pin: PIN5,
|
|
@@ -1572,7 +2602,7 @@ var c18StructureManifest = {
|
|
|
1572
2602
|
}
|
|
1573
2603
|
const { kind, path } = manifestPath(tree.root);
|
|
1574
2604
|
const manifestName = kind === "extension" ? "extension.json" : "mcp.json";
|
|
1575
|
-
if (!
|
|
2605
|
+
if (!existsSync4(path)) {
|
|
1576
2606
|
return {
|
|
1577
2607
|
id: this.id,
|
|
1578
2608
|
anchor: tree.root,
|
|
@@ -1650,85 +2680,10 @@ var c18StructureManifest = {
|
|
|
1650
2680
|
};
|
|
1651
2681
|
}
|
|
1652
2682
|
};
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1658
|
-
"node_modules",
|
|
1659
|
-
"dist",
|
|
1660
|
-
"build",
|
|
1661
|
-
".git",
|
|
1662
|
-
"tests",
|
|
1663
|
-
"__tests__",
|
|
1664
|
-
"coverage",
|
|
1665
|
-
// Test material, not a target's served surface: deliberately-malformed
|
|
1666
|
-
// servers and foreign copies live here. Scanning them makes the instrument
|
|
1667
|
-
// trip its own fail-closed detectors when pointed at its own tree, and lints
|
|
1668
|
-
// inputs that are meant to be broken. A real target's fixtures are equally
|
|
1669
|
-
// test inputs, not its product surface.
|
|
1670
|
-
"fixtures"
|
|
1671
|
-
]);
|
|
1672
|
-
function isCommentLine(raw) {
|
|
1673
|
-
const t = raw.trim();
|
|
1674
|
-
if (t === "") return false;
|
|
1675
|
-
return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
|
|
1676
|
-
}
|
|
1677
|
-
function walk(dir, out) {
|
|
1678
|
-
let entries;
|
|
1679
|
-
try {
|
|
1680
|
-
entries = readdirSync2(dir);
|
|
1681
|
-
} catch {
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
for (const name of entries) {
|
|
1685
|
-
const full = join4(dir, name);
|
|
1686
|
-
let st;
|
|
1687
|
-
try {
|
|
1688
|
-
st = statSync(full);
|
|
1689
|
-
} catch {
|
|
1690
|
-
continue;
|
|
1691
|
-
}
|
|
1692
|
-
if (st.isDirectory()) {
|
|
1693
|
-
if (SKIP_DIRS.has(name)) continue;
|
|
1694
|
-
walk(full, out);
|
|
1695
|
-
} else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
|
|
1696
|
-
if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
|
|
1697
|
-
out.push(full);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
function loadFile(path, root) {
|
|
1702
|
-
const raw = readFileSync4(path, "utf8");
|
|
1703
|
-
const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
|
|
1704
|
-
return { path, rel: relative(root, path) || path, lines };
|
|
1705
|
-
}
|
|
1706
|
-
function findPackageJson(startDir) {
|
|
1707
|
-
let dir = startDir;
|
|
1708
|
-
for (let depth = 0; depth < 8; depth++) {
|
|
1709
|
-
const candidate = join4(dir, "package.json");
|
|
1710
|
-
if (existsSync4(candidate)) {
|
|
1711
|
-
try {
|
|
1712
|
-
return { path: candidate, json: JSON.parse(readFileSync4(candidate, "utf8")) };
|
|
1713
|
-
} catch {
|
|
1714
|
-
return { path: candidate, json: {} };
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
const parent = dirname(dir);
|
|
1718
|
-
if (parent === dir) break;
|
|
1719
|
-
dir = parent;
|
|
1720
|
-
}
|
|
1721
|
-
return null;
|
|
1722
|
-
}
|
|
1723
|
-
function loadTree(root) {
|
|
1724
|
-
const st = statSync(root);
|
|
1725
|
-
const baseDir = st.isDirectory() ? root : dirname(root);
|
|
1726
|
-
const filePaths = [];
|
|
1727
|
-
if (st.isDirectory()) walk(root, filePaths);
|
|
1728
|
-
else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
|
|
1729
|
-
const files = filePaths.map((p) => loadFile(p, baseDir));
|
|
1730
|
-
return { root, files, packageJson: findPackageJson(baseDir) };
|
|
1731
|
-
}
|
|
2683
|
+
var c18StructureManifest = guardUnreadablePackageJson(
|
|
2684
|
+
c18StructureManifestInner,
|
|
2685
|
+
{ authoritative: false }
|
|
2686
|
+
);
|
|
1732
2687
|
|
|
1733
2688
|
// src/detectors/c19-served-descriptions.ts
|
|
1734
2689
|
var PIN6 = "a74a042b53c71a095ab88b3073bd47728eeda814";
|
|
@@ -1787,6 +2742,20 @@ var c19ServedDescriptions = {
|
|
|
1787
2742
|
}
|
|
1788
2743
|
const sites = findRegisteredTools(tree);
|
|
1789
2744
|
if (sites.length === 0) {
|
|
2745
|
+
const surface = toolSurfaceAnchor(tree);
|
|
2746
|
+
if (surface) {
|
|
2747
|
+
const reason = noToolsLocatedReason(
|
|
2748
|
+
tree,
|
|
2749
|
+
"served descriptions cannot be judged without the tool list (C-19)"
|
|
2750
|
+
);
|
|
2751
|
+
return {
|
|
2752
|
+
id: this.id,
|
|
2753
|
+
anchor: surface,
|
|
2754
|
+
state: "could-not-judge",
|
|
2755
|
+
reason,
|
|
2756
|
+
verdict: `could not judge: ${reason}`
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
1790
2759
|
return {
|
|
1791
2760
|
id: this.id,
|
|
1792
2761
|
anchor: "-",
|
|
@@ -2044,7 +3013,7 @@ var c21ProgressiveDisclosure = {
|
|
|
2044
3013
|
};
|
|
2045
3014
|
|
|
2046
3015
|
// src/detectors/c22-test-ratio-gate.ts
|
|
2047
|
-
import { existsSync as existsSync5, readFileSync as
|
|
3016
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
2048
3017
|
import { join as join5 } from "path";
|
|
2049
3018
|
var DOC_FILES2 = ["README.md", "README.fr.md", "CHANGELOG.md"];
|
|
2050
3019
|
var RATIO_RE = /\b(?:coverage|ratio de tests?|test ratio|couverture)\b[^\n%]{0,40}?(\d{1,3}(?:\.\d+)?\s?%)/i;
|
|
@@ -2074,7 +3043,7 @@ function findRatioCitations(root) {
|
|
|
2074
3043
|
if (!existsSync5(path)) continue;
|
|
2075
3044
|
let text;
|
|
2076
3045
|
try {
|
|
2077
|
-
text =
|
|
3046
|
+
text = readFileSync6(path, "utf8");
|
|
2078
3047
|
} catch {
|
|
2079
3048
|
continue;
|
|
2080
3049
|
}
|
|
@@ -2382,14 +3351,14 @@ import { spawnSync } from "child_process";
|
|
|
2382
3351
|
import {
|
|
2383
3352
|
existsSync as existsSync6,
|
|
2384
3353
|
mkdtempSync,
|
|
2385
|
-
readFileSync as
|
|
3354
|
+
readFileSync as readFileSync7,
|
|
2386
3355
|
readdirSync as readdirSync3,
|
|
2387
3356
|
rmSync,
|
|
2388
3357
|
statSync as statSync2,
|
|
2389
3358
|
writeFileSync
|
|
2390
3359
|
} from "fs";
|
|
2391
3360
|
import { tmpdir } from "os";
|
|
2392
|
-
import { join as join6, relative as relative2 } from "path";
|
|
3361
|
+
import { basename as basename2, join as join6, relative as relative2, sep as sep2 } from "path";
|
|
2393
3362
|
var PIN9 = "20136b50";
|
|
2394
3363
|
var DEPLOY_CONFIG_FILES = [
|
|
2395
3364
|
"railway.json",
|
|
@@ -2406,8 +3375,7 @@ var LOCKFILES2 = [
|
|
|
2406
3375
|
{ name: "pnpm-lock.yaml", command: "pnpm", args: ["install", "--frozen-lockfile"] },
|
|
2407
3376
|
{ name: "yarn.lock", command: "yarn", args: ["install", "--frozen-lockfile"] }
|
|
2408
3377
|
];
|
|
2409
|
-
|
|
2410
|
-
function walkForConfigs(dir, depth, seen) {
|
|
3378
|
+
function walkForConfigs(dir, depth, seen, onSkip) {
|
|
2411
3379
|
if (depth > 8) return;
|
|
2412
3380
|
let entries;
|
|
2413
3381
|
try {
|
|
@@ -2424,8 +3392,11 @@ function walkForConfigs(dir, depth, seen) {
|
|
|
2424
3392
|
continue;
|
|
2425
3393
|
}
|
|
2426
3394
|
if (st.isDirectory()) {
|
|
2427
|
-
if (
|
|
2428
|
-
|
|
3395
|
+
if (SKIP_DIRS.has(name)) {
|
|
3396
|
+
onSkip(full, depth + 1);
|
|
3397
|
+
continue;
|
|
3398
|
+
}
|
|
3399
|
+
walkForConfigs(full, depth + 1, seen, onSkip);
|
|
2429
3400
|
} else if (DEPLOY_CONFIG_FILES.includes(name)) {
|
|
2430
3401
|
if (!seen.has(dir)) {
|
|
2431
3402
|
seen.set(dir, { dir, configFile: name, configPath: full });
|
|
@@ -2433,10 +3404,41 @@ function walkForConfigs(dir, depth, seen) {
|
|
|
2433
3404
|
}
|
|
2434
3405
|
}
|
|
2435
3406
|
}
|
|
3407
|
+
function probeForCandidate(dir, depth) {
|
|
3408
|
+
if (depth > 8) return null;
|
|
3409
|
+
let entries;
|
|
3410
|
+
try {
|
|
3411
|
+
entries = readdirSync3(dir).sort();
|
|
3412
|
+
} catch {
|
|
3413
|
+
return null;
|
|
3414
|
+
}
|
|
3415
|
+
const subdirs = [];
|
|
3416
|
+
for (const name of entries) {
|
|
3417
|
+
const full = join6(dir, name);
|
|
3418
|
+
let st;
|
|
3419
|
+
try {
|
|
3420
|
+
st = statSync2(full);
|
|
3421
|
+
} catch {
|
|
3422
|
+
continue;
|
|
3423
|
+
}
|
|
3424
|
+
if (st.isDirectory()) {
|
|
3425
|
+
const info = SKIP_DIR_INFO[name];
|
|
3426
|
+
if (info && !info.authored) continue;
|
|
3427
|
+
subdirs.push(full);
|
|
3428
|
+
} else if (DEPLOY_CONFIG_FILES.includes(name)) {
|
|
3429
|
+
return full;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
for (const sub of subdirs) {
|
|
3433
|
+
const hit = probeForCandidate(sub, depth + 1);
|
|
3434
|
+
if (hit) return hit;
|
|
3435
|
+
}
|
|
3436
|
+
return null;
|
|
3437
|
+
}
|
|
2436
3438
|
function resolveRailwayRootDirectory(configPath) {
|
|
2437
3439
|
let text;
|
|
2438
3440
|
try {
|
|
2439
|
-
text =
|
|
3441
|
+
text = readFileSync7(configPath, "utf8");
|
|
2440
3442
|
} catch (err) {
|
|
2441
3443
|
return {
|
|
2442
3444
|
error: `could not read ${configPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2454,15 +3456,31 @@ function resolveRailwayRootDirectory(configPath) {
|
|
|
2454
3456
|
const rootDirectory = typeof parsed.rootDirectory === "string" && parsed.rootDirectory || build && typeof build.rootDirectory === "string" && build.rootDirectory || null;
|
|
2455
3457
|
return rootDirectory;
|
|
2456
3458
|
}
|
|
2457
|
-
function c26DeriveScope(root) {
|
|
3459
|
+
function c26DeriveScope(root, tree) {
|
|
2458
3460
|
const seen = /* @__PURE__ */ new Map();
|
|
2459
|
-
|
|
3461
|
+
const skipped = [];
|
|
3462
|
+
const onSkip = (skippedDir, depth) => {
|
|
3463
|
+
const entry = tree ? recordSkip(tree, skippedDir, root) : {
|
|
3464
|
+
path: relative2(root, skippedDir).split(sep2).join("/"),
|
|
3465
|
+
reason: SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
3466
|
+
};
|
|
3467
|
+
if (SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
3468
|
+
const hit = probeForCandidate(skippedDir, depth);
|
|
3469
|
+
if (hit) entry.candidate = relative2(root, hit).split(sep2).join("/");
|
|
3470
|
+
}
|
|
3471
|
+
if (!skipped.some((d) => d.path === entry.path)) skipped.push(entry);
|
|
3472
|
+
};
|
|
3473
|
+
walkForConfigs(root, 0, seen, onSkip);
|
|
2460
3474
|
const sites = [];
|
|
2461
3475
|
for (const site of seen.values()) {
|
|
2462
3476
|
if (site.configFile === "railway.json") {
|
|
2463
3477
|
const resolved = resolveRailwayRootDirectory(site.configPath);
|
|
2464
3478
|
if (resolved && typeof resolved === "object" && "error" in resolved) {
|
|
2465
|
-
return {
|
|
3479
|
+
return {
|
|
3480
|
+
sites: [],
|
|
3481
|
+
skipped,
|
|
3482
|
+
unparsable: { path: site.configPath, reason: resolved.error }
|
|
3483
|
+
};
|
|
2466
3484
|
}
|
|
2467
3485
|
if (typeof resolved === "string" && resolved.trim()) {
|
|
2468
3486
|
sites.push({ ...site, dir: join6(site.dir, resolved) });
|
|
@@ -2471,10 +3489,11 @@ function c26DeriveScope(root) {
|
|
|
2471
3489
|
}
|
|
2472
3490
|
if (site.configFile === "vercel.json") {
|
|
2473
3491
|
try {
|
|
2474
|
-
JSON.parse(
|
|
3492
|
+
JSON.parse(readFileSync7(site.configPath, "utf8"));
|
|
2475
3493
|
} catch (err) {
|
|
2476
3494
|
return {
|
|
2477
3495
|
sites: [],
|
|
3496
|
+
skipped,
|
|
2478
3497
|
unparsable: {
|
|
2479
3498
|
path: site.configPath,
|
|
2480
3499
|
reason: `could not parse ${site.configPath} as JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2484,7 +3503,7 @@ function c26DeriveScope(root) {
|
|
|
2484
3503
|
}
|
|
2485
3504
|
sites.push(site);
|
|
2486
3505
|
}
|
|
2487
|
-
return { sites };
|
|
3506
|
+
return { sites, skipped };
|
|
2488
3507
|
}
|
|
2489
3508
|
function findLockfile2(dir) {
|
|
2490
3509
|
for (const l of LOCKFILES2) {
|
|
@@ -2539,8 +3558,8 @@ function nameDrift(lockfileName, siteDir, isolatedDir, stderr) {
|
|
|
2539
3558
|
}
|
|
2540
3559
|
if (lockfileName === "bun.lock" || lockfileName === "bun.lockb") {
|
|
2541
3560
|
try {
|
|
2542
|
-
const manifestText =
|
|
2543
|
-
const lockText =
|
|
3561
|
+
const manifestText = readFileSync7(join6(isolatedDir, "package.json"), "utf8");
|
|
3562
|
+
const lockText = readFileSync7(join6(isolatedDir, lockfileName), "utf8");
|
|
2544
3563
|
const drifted = nameBunDrift(manifestText, lockText);
|
|
2545
3564
|
if (drifted.length > 0) return drifted.join(", ");
|
|
2546
3565
|
} catch {
|
|
@@ -2571,8 +3590,8 @@ function gateSite(site) {
|
|
|
2571
3590
|
}
|
|
2572
3591
|
const isolated = mkdtempSync(join6(tmpdir(), "mcp-doctor-c26-"));
|
|
2573
3592
|
try {
|
|
2574
|
-
const manifestText =
|
|
2575
|
-
const lockText =
|
|
3593
|
+
const manifestText = readFileSync7(pkgPath, "utf8");
|
|
3594
|
+
const lockText = readFileSync7(join6(site.dir, lock.name), "utf8");
|
|
2576
3595
|
writeFileSync(join6(isolated, "package.json"), manifestText);
|
|
2577
3596
|
writeFileSync(join6(isolated, lock.name), lockText);
|
|
2578
3597
|
let proc;
|
|
@@ -2641,7 +3660,18 @@ var c26SubpackageLockfile = {
|
|
|
2641
3660
|
rule: "C-26",
|
|
2642
3661
|
pin: PIN9,
|
|
2643
3662
|
run(tree) {
|
|
2644
|
-
const scope = c26DeriveScope(tree.root);
|
|
3663
|
+
const scope = c26DeriveScope(tree.root, tree);
|
|
3664
|
+
const hidden = scope.skipped.filter((d) => d.candidate !== void 0);
|
|
3665
|
+
const unjudgedHidden = (prefix) => {
|
|
3666
|
+
const reason = `${prefix}deployment configuration found only under skipped director${hidden.length === 1 ? "y" : "ies"}: ${hidden.map((d) => `${d.path} (${d.reason}) -> ${d.candidate}`).join("; ")} \u2014 C-26 does not descend into skipped directories, so these site(s) were not judged`;
|
|
3667
|
+
return {
|
|
3668
|
+
id: this.id,
|
|
3669
|
+
anchor: join6(tree.root, hidden[0].candidate),
|
|
3670
|
+
state: "could-not-judge",
|
|
3671
|
+
reason,
|
|
3672
|
+
verdict: `could not judge: ${reason}`
|
|
3673
|
+
};
|
|
3674
|
+
};
|
|
2645
3675
|
if (scope.unparsable) {
|
|
2646
3676
|
const reason = `deployment configuration ${scope.unparsable.path} could not be parsed: ${scope.unparsable.reason} \u2014 scope unestablished, C-26 cannot derive its targets`;
|
|
2647
3677
|
return {
|
|
@@ -2653,6 +3683,7 @@ var c26SubpackageLockfile = {
|
|
|
2653
3683
|
};
|
|
2654
3684
|
}
|
|
2655
3685
|
if (scope.sites.length === 0) {
|
|
3686
|
+
if (hidden.length > 0) return unjudgedHidden("");
|
|
2656
3687
|
return {
|
|
2657
3688
|
id: this.id,
|
|
2658
3689
|
anchor: tree.root,
|
|
@@ -2664,6 +3695,7 @@ var c26SubpackageLockfile = {
|
|
|
2664
3695
|
const inventory = renderCoverage(scope.sites, verdicts);
|
|
2665
3696
|
const analysed = verdicts.filter((v) => v.status === "analysed");
|
|
2666
3697
|
if (analysed.length === 0) {
|
|
3698
|
+
if (hidden.length > 0) return unjudgedHidden("no analysable site; ");
|
|
2667
3699
|
return {
|
|
2668
3700
|
id: this.id,
|
|
2669
3701
|
anchor: tree.root,
|
|
@@ -2680,6 +3712,11 @@ var c26SubpackageLockfile = {
|
|
|
2680
3712
|
verdict: reds.some((r) => r.detail?.includes("no lockfile")) ? `${reds[0].detail} (C-26). Coverage: ${inventory}` : `${reds.map((r) => r.detail).join(" ; ")} (C-26). Coverage: ${inventory}`
|
|
2681
3713
|
};
|
|
2682
3714
|
}
|
|
3715
|
+
if (hidden.length > 0) {
|
|
3716
|
+
return unjudgedHidden(
|
|
3717
|
+
`${analysed.length} analysed site(s) are consistent, but coverage is partial; `
|
|
3718
|
+
);
|
|
3719
|
+
}
|
|
2683
3720
|
return {
|
|
2684
3721
|
id: this.id,
|
|
2685
3722
|
anchor: analysed[0].site.dir,
|
|
@@ -2749,6 +3786,21 @@ var d3 = {
|
|
|
2749
3786
|
verdict: `could not judge: ${reason}`
|
|
2750
3787
|
};
|
|
2751
3788
|
}
|
|
3789
|
+
const surface = toolSurfaceAnchor(tree);
|
|
3790
|
+
if (surface) {
|
|
3791
|
+
const located = findRegisteredTools(tree);
|
|
3792
|
+
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(
|
|
3793
|
+
tree,
|
|
3794
|
+
"result shaping and resultType are not statically readable without the located tools"
|
|
3795
|
+
);
|
|
3796
|
+
return {
|
|
3797
|
+
id: this.id,
|
|
3798
|
+
anchor: surface,
|
|
3799
|
+
state: "could-not-judge",
|
|
3800
|
+
reason,
|
|
3801
|
+
verdict: `could not judge: ${reason}`
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
2752
3804
|
return {
|
|
2753
3805
|
id: this.id,
|
|
2754
3806
|
anchor: "-",
|
|
@@ -2793,6 +3845,21 @@ var d4 = {
|
|
|
2793
3845
|
verdict: `could not judge: ${reason}`
|
|
2794
3846
|
};
|
|
2795
3847
|
}
|
|
3848
|
+
const surface = toolSurfaceAnchor(tree);
|
|
3849
|
+
if (surface) {
|
|
3850
|
+
const located = findRegisteredTools(tree);
|
|
3851
|
+
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(
|
|
3852
|
+
tree,
|
|
3853
|
+
"result shaping and any ttlMs/cacheScope are not statically readable without the located tools"
|
|
3854
|
+
);
|
|
3855
|
+
return {
|
|
3856
|
+
id: this.id,
|
|
3857
|
+
anchor: surface,
|
|
3858
|
+
state: "could-not-judge",
|
|
3859
|
+
reason,
|
|
3860
|
+
verdict: `could not judge: ${reason}`
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
2796
3863
|
return {
|
|
2797
3864
|
id: this.id,
|
|
2798
3865
|
anchor: "-",
|
|
@@ -2922,7 +3989,7 @@ function depListed(pkgJson) {
|
|
|
2922
3989
|
}
|
|
2923
3990
|
return false;
|
|
2924
3991
|
}
|
|
2925
|
-
var
|
|
3992
|
+
var d7Inner = {
|
|
2926
3993
|
id: "D7 protocol-kit",
|
|
2927
3994
|
run(tree) {
|
|
2928
3995
|
const listed = depListed(tree.packageJson?.json);
|
|
@@ -2961,9 +4028,10 @@ var d7 = {
|
|
|
2961
4028
|
};
|
|
2962
4029
|
}
|
|
2963
4030
|
};
|
|
4031
|
+
var d7 = guardUnreadablePackageJson(d7Inner, { authoritative: true });
|
|
2964
4032
|
|
|
2965
4033
|
// src/detectors/d8.ts
|
|
2966
|
-
import { existsSync as existsSync7,
|
|
4034
|
+
import { existsSync as existsSync7, statSync as statSync3 } from "fs";
|
|
2967
4035
|
import { dirname as dirname2, join as join7, relative as relative3 } from "path";
|
|
2968
4036
|
var POSITIVE_CONTROL2 = 'server.registerTool("list_items", { ... }, handler);';
|
|
2969
4037
|
function d8PositiveControl() {
|
|
@@ -3016,6 +4084,20 @@ var d8 = {
|
|
|
3016
4084
|
verdict: `could not judge: ${reason}`
|
|
3017
4085
|
};
|
|
3018
4086
|
}
|
|
4087
|
+
const surface = toolSurfaceAnchor(tree);
|
|
4088
|
+
if (surface) {
|
|
4089
|
+
const reason = noToolsLocatedReason(
|
|
4090
|
+
tree,
|
|
4091
|
+
"eval coverage cannot be judged without the tool list"
|
|
4092
|
+
);
|
|
4093
|
+
return {
|
|
4094
|
+
id: this.id,
|
|
4095
|
+
anchor: surface,
|
|
4096
|
+
state: "could-not-judge",
|
|
4097
|
+
reason,
|
|
4098
|
+
verdict: `could not judge: ${reason}`
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
3019
4101
|
return {
|
|
3020
4102
|
id: this.id,
|
|
3021
4103
|
anchor: "-",
|
|
@@ -3034,19 +4116,15 @@ var d8 = {
|
|
|
3034
4116
|
}
|
|
3035
4117
|
const baseDir = resolveBaseDir(tree);
|
|
3036
4118
|
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`
|
|
4119
|
+
const parsed = readJsonObject(evalsPath);
|
|
4120
|
+
if (parsed === null || !Array.isArray(parsed.cases)) {
|
|
4121
|
+
return couldNotJudge(
|
|
4122
|
+
this.id,
|
|
4123
|
+
displayPath,
|
|
4124
|
+
`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
4125
|
);
|
|
3049
4126
|
}
|
|
4127
|
+
const cases = parsed.cases;
|
|
3050
4128
|
const counts = /* @__PURE__ */ new Map();
|
|
3051
4129
|
for (const t of tools) counts.set(t.name, 0);
|
|
3052
4130
|
for (const c of cases) {
|
|
@@ -3131,7 +4209,7 @@ function d9PositiveControl() {
|
|
|
3131
4209
|
matched: SEMVER_RE.test(POSITIVE_CONTROL3)
|
|
3132
4210
|
};
|
|
3133
4211
|
}
|
|
3134
|
-
var
|
|
4212
|
+
var d9Inner = {
|
|
3135
4213
|
id: "D9 semver+changelog",
|
|
3136
4214
|
rule: "C-3",
|
|
3137
4215
|
pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
|
|
@@ -3154,7 +4232,11 @@ var d9 = {
|
|
|
3154
4232
|
}
|
|
3155
4233
|
const { path: pkgPath, json: pkgJson } = tree.packageJson;
|
|
3156
4234
|
if (!pkgJson || Object.keys(pkgJson).length === 0) {
|
|
3157
|
-
|
|
4235
|
+
return couldNotJudge(
|
|
4236
|
+
this.id,
|
|
4237
|
+
pkgPath.split("/").slice(-2).join("/"),
|
|
4238
|
+
`could not parse ${pkgPath} as JSON \u2014 cannot verify SemVer/CHANGELOG`
|
|
4239
|
+
);
|
|
3158
4240
|
}
|
|
3159
4241
|
const version = pkgJson.version;
|
|
3160
4242
|
if (typeof version !== "string") {
|
|
@@ -3193,6 +4275,7 @@ var d9 = {
|
|
|
3193
4275
|
};
|
|
3194
4276
|
}
|
|
3195
4277
|
};
|
|
4278
|
+
var d9 = guardUnreadablePackageJson(d9Inner, { authoritative: false });
|
|
3196
4279
|
|
|
3197
4280
|
// src/detectors/d10.ts
|
|
3198
4281
|
import { existsSync as existsSync9 } from "fs";
|
|
@@ -3244,7 +4327,7 @@ function scanSourceDescriptionFr(tree) {
|
|
|
3244
4327
|
}
|
|
3245
4328
|
return { present: false, anchor: null };
|
|
3246
4329
|
}
|
|
3247
|
-
var
|
|
4330
|
+
var d10Inner = {
|
|
3248
4331
|
id: "D10 bilingual",
|
|
3249
4332
|
rule: "C-1",
|
|
3250
4333
|
pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
|
|
@@ -3315,6 +4398,7 @@ var d10 = {
|
|
|
3315
4398
|
};
|
|
3316
4399
|
}
|
|
3317
4400
|
};
|
|
4401
|
+
var d10 = guardUnreadablePackageJson(d10Inner, { authoritative: true });
|
|
3318
4402
|
|
|
3319
4403
|
// src/detectors/d11.ts
|
|
3320
4404
|
var ZOD_ANY_RE = /\bz\s*\.\s*any\s*\(\s*\)/;
|
|
@@ -3386,6 +4470,27 @@ var d11 = {
|
|
|
3386
4470
|
}
|
|
3387
4471
|
}
|
|
3388
4472
|
if (toolCount === 0) {
|
|
4473
|
+
const located = findRegisteredTools(tree);
|
|
4474
|
+
if (located.length > 0) {
|
|
4475
|
+
toolCount = located.length;
|
|
4476
|
+
firstToolAnchor = located[0].anchor;
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
if (toolCount === 0) {
|
|
4480
|
+
const surface = toolSurfaceAnchor(tree);
|
|
4481
|
+
if (surface) {
|
|
4482
|
+
const reason = noToolsLocatedReason(
|
|
4483
|
+
tree,
|
|
4484
|
+
"the outputSchema check cannot be judged without the tool list"
|
|
4485
|
+
);
|
|
4486
|
+
return {
|
|
4487
|
+
id: this.id,
|
|
4488
|
+
anchor: surface,
|
|
4489
|
+
state: "could-not-judge",
|
|
4490
|
+
reason,
|
|
4491
|
+
verdict: `could not judge: ${reason}`
|
|
4492
|
+
};
|
|
4493
|
+
}
|
|
3389
4494
|
return {
|
|
3390
4495
|
id: this.id,
|
|
3391
4496
|
anchor: "-",
|
|
@@ -3465,7 +4570,7 @@ function findHttpSite(tree) {
|
|
|
3465
4570
|
}
|
|
3466
4571
|
return null;
|
|
3467
4572
|
}
|
|
3468
|
-
var
|
|
4573
|
+
var d12Inner = {
|
|
3469
4574
|
id: "D12 stdio-transport",
|
|
3470
4575
|
run(tree) {
|
|
3471
4576
|
const stdioControl = d12StdioPositiveControl();
|
|
@@ -3516,6 +4621,7 @@ var d12 = {
|
|
|
3516
4621
|
};
|
|
3517
4622
|
}
|
|
3518
4623
|
};
|
|
4624
|
+
var d12 = guardUnreadablePackageJson(d12Inner, { authoritative: false });
|
|
3519
4625
|
|
|
3520
4626
|
// src/detectors/d15.ts
|
|
3521
4627
|
var HANDLE_ASSIGN_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[^;]*\.(?:registerTool|tool)\s*\(/;
|
|
@@ -4129,71 +5235,63 @@ var dDebugOptIn = {
|
|
|
4129
5235
|
);
|
|
4130
5236
|
}
|
|
4131
5237
|
}
|
|
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
|
-
};
|
|
5238
|
+
const unreadable = [];
|
|
5239
|
+
for (const h of findAll(tree, COMPUTED_LEVEL_RE)) {
|
|
5240
|
+
const anchor = `${h.file.rel}:${h.line}`;
|
|
5241
|
+
unreadable.push({
|
|
5242
|
+
anchor,
|
|
5243
|
+
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)`
|
|
5244
|
+
});
|
|
4158
5245
|
}
|
|
4159
|
-
const
|
|
4160
|
-
|
|
4161
|
-
|
|
5246
|
+
const decided = (() => {
|
|
5247
|
+
const debugCalls = findAll(tree, DEBUG_CALL_RE);
|
|
5248
|
+
if (debugCalls.length === 0) {
|
|
5249
|
+
return {
|
|
5250
|
+
id: this.id,
|
|
5251
|
+
anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
|
|
5252
|
+
state: "absent",
|
|
5253
|
+
verdict: "no debug output emitted (n/a for Server Rule 6 debug-half)"
|
|
5254
|
+
};
|
|
5255
|
+
}
|
|
5256
|
+
const callAnchor = `${debugCalls[0].file.rel}:${debugCalls.map((h) => h.line).join(",")}`;
|
|
5257
|
+
const debugFileRels = new Set(debugCalls.map((h) => h.file.rel));
|
|
5258
|
+
const localGate = firstMatchInFiles2(tree, GATE_RE, debugFileRels);
|
|
5259
|
+
if (localGate) {
|
|
5260
|
+
return {
|
|
5261
|
+
id: this.id,
|
|
5262
|
+
anchor: callAnchor,
|
|
5263
|
+
state: "present",
|
|
5264
|
+
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)`
|
|
5265
|
+
};
|
|
5266
|
+
}
|
|
5267
|
+
const remoteGate = firstMatchInFiles2(tree, GATE_RE, new Set(tree.files.map((f) => f.rel)));
|
|
5268
|
+
if (remoteGate) {
|
|
5269
|
+
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)`;
|
|
5270
|
+
return {
|
|
5271
|
+
id: this.id,
|
|
5272
|
+
anchor: callAnchor,
|
|
5273
|
+
state: "could-not-judge",
|
|
5274
|
+
reason,
|
|
5275
|
+
verdict: `could not judge: ${reason}`
|
|
5276
|
+
};
|
|
5277
|
+
}
|
|
4162
5278
|
return {
|
|
4163
5279
|
id: this.id,
|
|
4164
5280
|
anchor: callAnchor,
|
|
4165
|
-
state: "
|
|
4166
|
-
|
|
4167
|
-
verdict: `could not judge: ${reason}`
|
|
5281
|
+
state: "non-conforming",
|
|
5282
|
+
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
5283
|
};
|
|
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
|
-
};
|
|
5284
|
+
})();
|
|
5285
|
+
return settle(this.id, decided, unreadable);
|
|
4176
5286
|
}
|
|
4177
5287
|
};
|
|
4178
5288
|
|
|
4179
|
-
// src/detectors/
|
|
4180
|
-
import {
|
|
5289
|
+
// src/detectors/extManifests.ts
|
|
5290
|
+
import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
4181
5291
|
import { join as join12 } from "path";
|
|
4182
|
-
|
|
4183
|
-
var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
|
|
4184
|
-
var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
|
|
4185
|
-
function dExt1PositiveControls() {
|
|
4186
|
-
return {
|
|
4187
|
-
identifier: {
|
|
4188
|
-
pattern: IDENTIFIER_RE2.source,
|
|
4189
|
-
positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
|
|
4190
|
-
negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
|
|
4191
|
-
}
|
|
4192
|
-
};
|
|
4193
|
-
}
|
|
4194
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
4195
|
-
function findExtensionManifests(root) {
|
|
5292
|
+
function findExtensionManifests(tree) {
|
|
4196
5293
|
const out = [];
|
|
5294
|
+
const root = tree.root;
|
|
4197
5295
|
let base;
|
|
4198
5296
|
try {
|
|
4199
5297
|
base = statSync5(root).isDirectory() ? root : join12(root, "..");
|
|
@@ -4216,7 +5314,10 @@ function findExtensionManifests(root) {
|
|
|
4216
5314
|
continue;
|
|
4217
5315
|
}
|
|
4218
5316
|
if (st.isDirectory()) {
|
|
4219
|
-
if (
|
|
5317
|
+
if (SKIP_DIRS.has(name)) {
|
|
5318
|
+
recordSkip(tree, full, base);
|
|
5319
|
+
continue;
|
|
5320
|
+
}
|
|
4220
5321
|
walk2(full);
|
|
4221
5322
|
} else if (name === "extension.json") {
|
|
4222
5323
|
out.push(full);
|
|
@@ -4226,20 +5327,34 @@ function findExtensionManifests(root) {
|
|
|
4226
5327
|
walk2(base);
|
|
4227
5328
|
return out;
|
|
4228
5329
|
}
|
|
5330
|
+
|
|
5331
|
+
// src/detectors/dExt1.ts
|
|
5332
|
+
var IDENTIFIER_RE2 = /^[a-z0-9]+(\.[a-z0-9-]+)+\/[a-z0-9-]+$/;
|
|
5333
|
+
var IDENTIFIER_POSITIVE = "com.elpi/middleware-redact-pii";
|
|
5334
|
+
var IDENTIFIER_NEGATIVE = "Com.Elpi/Bad_Id";
|
|
5335
|
+
function dExt1PositiveControls() {
|
|
5336
|
+
return {
|
|
5337
|
+
identifier: {
|
|
5338
|
+
pattern: IDENTIFIER_RE2.source,
|
|
5339
|
+
positive: IDENTIFIER_RE2.test(IDENTIFIER_POSITIVE),
|
|
5340
|
+
negative: IDENTIFIER_RE2.test(IDENTIFIER_NEGATIVE)
|
|
5341
|
+
}
|
|
5342
|
+
};
|
|
5343
|
+
}
|
|
4229
5344
|
var TS_IDENT_LITERAL_RE = /\bidentifier:\s*["'`]([^"'`]*)["'`]/;
|
|
4230
|
-
var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`]/;
|
|
5345
|
+
var TS_IDENT_KEY_RE = /\bidentifier:\s*[^\s"'`{]/;
|
|
4231
5346
|
function findTsIdentifiers(tree) {
|
|
4232
5347
|
const sites = [];
|
|
4233
5348
|
const unresolved = [];
|
|
4234
5349
|
for (const file of tree.files) {
|
|
4235
5350
|
for (const line of file.lines) {
|
|
4236
5351
|
if (line.comment) continue;
|
|
4237
|
-
const lit = line
|
|
5352
|
+
const lit = matchInCode(file, line, TS_IDENT_LITERAL_RE);
|
|
4238
5353
|
if (lit) {
|
|
4239
5354
|
sites.push({ value: lit[1], anchor: `${file.rel}:${line.n}` });
|
|
4240
5355
|
continue;
|
|
4241
5356
|
}
|
|
4242
|
-
if (
|
|
5357
|
+
if (testInCode(file, line, TS_IDENT_KEY_RE)) {
|
|
4243
5358
|
unresolved.push(`${file.rel}:${line.n}`);
|
|
4244
5359
|
}
|
|
4245
5360
|
}
|
|
@@ -4255,69 +5370,75 @@ var dExt1 = {
|
|
|
4255
5370
|
`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
5371
|
);
|
|
4257
5372
|
}
|
|
4258
|
-
const
|
|
4259
|
-
|
|
4260
|
-
const
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
parsed =
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
5373
|
+
const unreadable = [];
|
|
5374
|
+
const decided = (() => {
|
|
5375
|
+
const manifests = findExtensionManifests(tree);
|
|
5376
|
+
for (const manifestPath2 of manifests) {
|
|
5377
|
+
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
5378
|
+
const parsed = readJsonObject(manifestPath2);
|
|
5379
|
+
if (parsed === null) {
|
|
5380
|
+
unreadable.push({
|
|
5381
|
+
anchor: shortPath,
|
|
5382
|
+
reason: `could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable identifier cannot pass (Extension Rule 1)`
|
|
5383
|
+
});
|
|
5384
|
+
continue;
|
|
5385
|
+
}
|
|
5386
|
+
const idValue = parsed.identifier;
|
|
5387
|
+
if (typeof idValue !== "string") {
|
|
5388
|
+
return {
|
|
5389
|
+
id: this.id,
|
|
5390
|
+
anchor: shortPath,
|
|
5391
|
+
state: "non-conforming",
|
|
5392
|
+
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)`
|
|
5393
|
+
};
|
|
5394
|
+
}
|
|
5395
|
+
if (!IDENTIFIER_RE2.test(idValue)) {
|
|
5396
|
+
return {
|
|
5397
|
+
id: this.id,
|
|
5398
|
+
anchor: shortPath,
|
|
5399
|
+
state: "non-conforming",
|
|
5400
|
+
verdict: `manifest identifier "${idValue}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
|
|
5401
|
+
};
|
|
5402
|
+
}
|
|
4268
5403
|
}
|
|
4269
|
-
const
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
5404
|
+
const { sites, unresolved } = findTsIdentifiers(tree);
|
|
5405
|
+
for (const anchor of unresolved) {
|
|
5406
|
+
unreadable.push({
|
|
5407
|
+
anchor,
|
|
5408
|
+
reason: `cannot read the identifier at ${anchor} \u2014 value is computed/imported/template, reverse-DNS unverifiable (Extension Rule 1)`
|
|
5409
|
+
});
|
|
4274
5410
|
}
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
5411
|
+
for (const site of sites) {
|
|
5412
|
+
if (!IDENTIFIER_RE2.test(site.value)) {
|
|
5413
|
+
return {
|
|
5414
|
+
id: this.id,
|
|
5415
|
+
anchor: site.anchor,
|
|
5416
|
+
state: "non-conforming",
|
|
5417
|
+
verdict: `identifier "${site.value}" is not reverse-DNS (positive-control OK) -> use <reverse.dns>/<slug> e.g. com.elpi/redact-pii (Extension Rule 1)`
|
|
5418
|
+
};
|
|
5419
|
+
}
|
|
4282
5420
|
}
|
|
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)) {
|
|
5421
|
+
const total = manifests.length + sites.length;
|
|
5422
|
+
if (total === 0) {
|
|
4292
5423
|
return {
|
|
4293
5424
|
id: this.id,
|
|
4294
|
-
anchor:
|
|
4295
|
-
state: "
|
|
4296
|
-
verdict:
|
|
5425
|
+
anchor: tree.root.split("/").pop() ?? tree.root,
|
|
5426
|
+
state: "absent",
|
|
5427
|
+
verdict: "no extension identifier found (no extension.json manifest, no TS `identifier:` literal) -> declare a reverse-DNS identifier (Extension Rule 1)"
|
|
4297
5428
|
};
|
|
4298
5429
|
}
|
|
4299
|
-
}
|
|
4300
|
-
const total = manifests.length + sites.length;
|
|
4301
|
-
if (total === 0) {
|
|
4302
5430
|
return {
|
|
4303
5431
|
id: this.id,
|
|
4304
|
-
anchor:
|
|
4305
|
-
state: "
|
|
4306
|
-
verdict:
|
|
5432
|
+
anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
|
|
5433
|
+
state: "present",
|
|
5434
|
+
verdict: `${total} identifier(s) reverse-DNS conforming (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
|
|
4307
5435
|
};
|
|
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
|
-
};
|
|
5436
|
+
})();
|
|
5437
|
+
return settle(this.id, decided, unreadable);
|
|
4315
5438
|
}
|
|
4316
5439
|
};
|
|
4317
5440
|
|
|
4318
5441
|
// src/detectors/dExt2.ts
|
|
4319
|
-
import { readFileSync as readFileSync11, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
4320
|
-
import { join as join13 } from "path";
|
|
4321
5442
|
var TS_GD_TRUE_RE = /\bgracefulDegradation:\s*true\b/;
|
|
4322
5443
|
var TS_GD_FALSE_RE = /\bgracefulDegradation:\s*false\b/;
|
|
4323
5444
|
var TS_GD_KEY_RE = /\bgracefulDegradation:\s*([^\s,}]+)/;
|
|
@@ -4332,41 +5453,6 @@ function dExt2PositiveControls() {
|
|
|
4332
5453
|
}
|
|
4333
5454
|
};
|
|
4334
5455
|
}
|
|
4335
|
-
var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
4336
|
-
function findExtensionManifests2(root) {
|
|
4337
|
-
const out = [];
|
|
4338
|
-
let base;
|
|
4339
|
-
try {
|
|
4340
|
-
base = statSync6(root).isDirectory() ? root : join13(root, "..");
|
|
4341
|
-
} catch {
|
|
4342
|
-
return out;
|
|
4343
|
-
}
|
|
4344
|
-
const walk2 = (dir) => {
|
|
4345
|
-
let entries;
|
|
4346
|
-
try {
|
|
4347
|
-
entries = readdirSync5(dir);
|
|
4348
|
-
} catch {
|
|
4349
|
-
return;
|
|
4350
|
-
}
|
|
4351
|
-
for (const name of entries) {
|
|
4352
|
-
const full = join13(dir, name);
|
|
4353
|
-
let st;
|
|
4354
|
-
try {
|
|
4355
|
-
st = statSync6(full);
|
|
4356
|
-
} catch {
|
|
4357
|
-
continue;
|
|
4358
|
-
}
|
|
4359
|
-
if (st.isDirectory()) {
|
|
4360
|
-
if (SKIP_DIRS3.has(name)) continue;
|
|
4361
|
-
walk2(full);
|
|
4362
|
-
} else if (name === "extension.json") {
|
|
4363
|
-
out.push(full);
|
|
4364
|
-
}
|
|
4365
|
-
}
|
|
4366
|
-
};
|
|
4367
|
-
walk2(base);
|
|
4368
|
-
return out;
|
|
4369
|
-
}
|
|
4370
5456
|
function findTsFactorySites(tree) {
|
|
4371
5457
|
const sites = [];
|
|
4372
5458
|
for (const file of tree.files) {
|
|
@@ -4374,7 +5460,7 @@ function findTsFactorySites(tree) {
|
|
|
4374
5460
|
for (let i = 0; i < lines.length; i++) {
|
|
4375
5461
|
const line = lines[i];
|
|
4376
5462
|
if (line.comment) continue;
|
|
4377
|
-
if (!
|
|
5463
|
+
if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE)) continue;
|
|
4378
5464
|
const anchor = `${file.rel}:${line.n}`;
|
|
4379
5465
|
let depth = 0;
|
|
4380
5466
|
let started = false;
|
|
@@ -4384,20 +5470,20 @@ function findTsFactorySites(tree) {
|
|
|
4384
5470
|
const bl = lines[j];
|
|
4385
5471
|
if (!bl.comment) {
|
|
4386
5472
|
if (state === "absent") {
|
|
4387
|
-
if (
|
|
5473
|
+
if (testInCode(file, bl, TS_GD_TRUE_RE)) {
|
|
4388
5474
|
state = "true";
|
|
4389
|
-
} else if (
|
|
5475
|
+
} else if (testInCode(file, bl, TS_GD_FALSE_RE)) {
|
|
4390
5476
|
state = "false";
|
|
4391
5477
|
} else {
|
|
4392
|
-
const key = bl
|
|
5478
|
+
const key = matchInCode(file, bl, TS_GD_KEY_RE);
|
|
4393
5479
|
if (key) {
|
|
4394
5480
|
state = "unresolved";
|
|
4395
5481
|
value = key[1];
|
|
4396
5482
|
}
|
|
4397
5483
|
}
|
|
4398
5484
|
}
|
|
4399
|
-
depth += (bl
|
|
4400
|
-
depth -= (bl
|
|
5485
|
+
depth += countInCode(file, bl, "{");
|
|
5486
|
+
depth -= countInCode(file, bl, "}");
|
|
4401
5487
|
if (depth > 0) started = true;
|
|
4402
5488
|
}
|
|
4403
5489
|
if (started && depth <= 0) break;
|
|
@@ -4416,133 +5502,105 @@ var dExt2 = {
|
|
|
4416
5502
|
`DExt2 graceful-degradation control FAILED: accept(\`${GD_POSITIVE}\`)=${controls.gracefulDegradation.acceptsTrue} reject(\`${GD_NEGATIVE}\`)=${controls.gracefulDegradation.rejectsFalse} \u2014 matcher broken, verdict untrustworthy`
|
|
4417
5503
|
);
|
|
4418
5504
|
}
|
|
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
|
-
);
|
|
5505
|
+
const unreadable = [];
|
|
5506
|
+
const decided = (() => {
|
|
5507
|
+
const manifests = findExtensionManifests(tree);
|
|
5508
|
+
for (const manifestPath2 of manifests) {
|
|
5509
|
+
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
5510
|
+
const parsed = readJsonObject(manifestPath2);
|
|
5511
|
+
if (parsed === null) {
|
|
5512
|
+
unreadable.push({
|
|
5513
|
+
anchor: shortPath,
|
|
5514
|
+
reason: `could not parse ${shortPath} as JSON \u2014 a present manifest silent on graceful degradation cannot pass (Extension Rule 2)`
|
|
5515
|
+
});
|
|
5516
|
+
continue;
|
|
5517
|
+
}
|
|
5518
|
+
if (!("graceful_degradation" in parsed)) {
|
|
5519
|
+
return {
|
|
5520
|
+
id: this.id,
|
|
5521
|
+
anchor: shortPath,
|
|
5522
|
+
state: "non-conforming",
|
|
5523
|
+
verdict: `manifest ${shortPath} has no "graceful_degradation" field -> set "graceful_degradation": true, an extension never breaks the host server (Extension Rule 2)`
|
|
5524
|
+
};
|
|
5525
|
+
}
|
|
5526
|
+
const gdValue = parsed.graceful_degradation;
|
|
5527
|
+
if (gdValue !== true) {
|
|
5528
|
+
return {
|
|
5529
|
+
id: this.id,
|
|
5530
|
+
anchor: shortPath,
|
|
5531
|
+
state: "non-conforming",
|
|
5532
|
+
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)`
|
|
5533
|
+
};
|
|
5534
|
+
}
|
|
4451
5535
|
}
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
5536
|
+
const sites = findTsFactorySites(tree);
|
|
5537
|
+
for (const site of sites) {
|
|
5538
|
+
if (site.state === "unresolved") {
|
|
5539
|
+
unreadable.push({
|
|
5540
|
+
anchor: site.anchor,
|
|
5541
|
+
reason: `cannot read the gracefulDegradation value at ${site.anchor} (got \`${site.value}\`) \u2014 computed/imported/template, graceful degradation unverifiable (Extension Rule 2)`
|
|
5542
|
+
});
|
|
5543
|
+
continue;
|
|
5544
|
+
}
|
|
5545
|
+
if (site.state === "false") {
|
|
5546
|
+
return {
|
|
5547
|
+
id: this.id,
|
|
5548
|
+
anchor: site.anchor,
|
|
5549
|
+
state: "non-conforming",
|
|
5550
|
+
verdict: "gracefulDegradation is false, not true (positive-control OK) -> an extension never breaks the host server (Extension Rule 2)"
|
|
5551
|
+
};
|
|
5552
|
+
}
|
|
5553
|
+
if (site.state === "absent") {
|
|
5554
|
+
return {
|
|
5555
|
+
id: this.id,
|
|
5556
|
+
anchor: site.anchor,
|
|
5557
|
+
state: "non-conforming",
|
|
5558
|
+
verdict: "defineExtension block has no gracefulDegradation flag (positive-control OK) -> declare gracefulDegradation: true (Extension Rule 2)"
|
|
5559
|
+
};
|
|
5560
|
+
}
|
|
4459
5561
|
}
|
|
4460
|
-
|
|
5562
|
+
const total = manifests.length + sites.length;
|
|
5563
|
+
if (total === 0) {
|
|
4461
5564
|
return {
|
|
4462
5565
|
id: this.id,
|
|
4463
|
-
anchor:
|
|
4464
|
-
state: "
|
|
4465
|
-
verdict: "
|
|
5566
|
+
anchor: tree.root.split("/").pop() ?? tree.root,
|
|
5567
|
+
state: "absent",
|
|
5568
|
+
verdict: "no extension graceful-degradation flag found (no extension.json manifest, no defineExtension factory) -> declare graceful_degradation: true (Extension Rule 2)"
|
|
4466
5569
|
};
|
|
4467
5570
|
}
|
|
4468
|
-
}
|
|
4469
|
-
const total = manifests.length + sites.length;
|
|
4470
|
-
if (total === 0) {
|
|
4471
5571
|
return {
|
|
4472
5572
|
id: this.id,
|
|
4473
|
-
anchor:
|
|
4474
|
-
state: "
|
|
4475
|
-
verdict:
|
|
4476
|
-
};
|
|
4477
|
-
}
|
|
4478
|
-
return
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
var TYPE_MIXED = "middleware|transport";
|
|
4503
|
-
var TYPE_SINGLE = "middleware";
|
|
4504
|
-
function dExt3PositiveControls() {
|
|
4505
|
-
return {
|
|
4506
|
-
type: {
|
|
4507
|
-
detectsMix: typesIn(TYPE_MIXED).length === 2,
|
|
4508
|
-
detectsSingle: typesIn(TYPE_SINGLE).length === 1
|
|
4509
|
-
}
|
|
4510
|
-
};
|
|
4511
|
-
}
|
|
4512
|
-
var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
4513
|
-
function findExtensionManifests3(root) {
|
|
4514
|
-
const out = [];
|
|
4515
|
-
let base;
|
|
4516
|
-
try {
|
|
4517
|
-
base = statSync7(root).isDirectory() ? root : join14(root, "..");
|
|
4518
|
-
} catch {
|
|
4519
|
-
return out;
|
|
4520
|
-
}
|
|
4521
|
-
const walk2 = (dir) => {
|
|
4522
|
-
let entries;
|
|
4523
|
-
try {
|
|
4524
|
-
entries = readdirSync6(dir);
|
|
4525
|
-
} catch {
|
|
4526
|
-
return;
|
|
4527
|
-
}
|
|
4528
|
-
for (const name of entries) {
|
|
4529
|
-
const full = join14(dir, name);
|
|
4530
|
-
let st;
|
|
4531
|
-
try {
|
|
4532
|
-
st = statSync7(full);
|
|
4533
|
-
} catch {
|
|
4534
|
-
continue;
|
|
4535
|
-
}
|
|
4536
|
-
if (st.isDirectory()) {
|
|
4537
|
-
if (SKIP_DIRS4.has(name)) continue;
|
|
4538
|
-
walk2(full);
|
|
4539
|
-
} else if (name === "extension.json") {
|
|
4540
|
-
out.push(full);
|
|
4541
|
-
}
|
|
5573
|
+
anchor: sites.length > 0 ? sites[0].anchor : manifests[0].split("/").slice(-2).join("/"),
|
|
5574
|
+
state: "present",
|
|
5575
|
+
verdict: `${total} graceful-degradation flag(s) true (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
|
|
5576
|
+
};
|
|
5577
|
+
})();
|
|
5578
|
+
return settle(this.id, decided, unreadable);
|
|
5579
|
+
}
|
|
5580
|
+
};
|
|
5581
|
+
|
|
5582
|
+
// src/detectors/dExt3.ts
|
|
5583
|
+
var TYPE_MATCHERS = [
|
|
5584
|
+
{ type: "middleware", re: /\bmiddleware\b/ },
|
|
5585
|
+
{ type: "tool-extension", re: /\btool-ext(?:ension)?\b/ },
|
|
5586
|
+
{ type: "transport", re: /\btransport\b/ }
|
|
5587
|
+
];
|
|
5588
|
+
function typesIn(value) {
|
|
5589
|
+
const found = [];
|
|
5590
|
+
for (const { type, re } of TYPE_MATCHERS) {
|
|
5591
|
+
if (re.test(value)) found.push(type);
|
|
5592
|
+
}
|
|
5593
|
+
return found;
|
|
5594
|
+
}
|
|
5595
|
+
var TYPE_MIXED = "middleware|transport";
|
|
5596
|
+
var TYPE_SINGLE = "middleware";
|
|
5597
|
+
function dExt3PositiveControls() {
|
|
5598
|
+
return {
|
|
5599
|
+
type: {
|
|
5600
|
+
detectsMix: typesIn(TYPE_MIXED).length === 2,
|
|
5601
|
+
detectsSingle: typesIn(TYPE_SINGLE).length === 1
|
|
4542
5602
|
}
|
|
4543
5603
|
};
|
|
4544
|
-
walk2(base);
|
|
4545
|
-
return out;
|
|
4546
5604
|
}
|
|
4547
5605
|
var DEFINE_EXTENSION_OPEN_RE2 = /\bdefineExtension\s*\(\s*\{/;
|
|
4548
5606
|
var TS_TYPE_LITERAL_RE = /\btype:\s*["'`]([^"'`]*)["'`]/;
|
|
@@ -4554,7 +5612,7 @@ function findTsTypeSites(tree) {
|
|
|
4554
5612
|
for (let i = 0; i < lines.length; i++) {
|
|
4555
5613
|
const line = lines[i];
|
|
4556
5614
|
if (line.comment) continue;
|
|
4557
|
-
if (!
|
|
5615
|
+
if (!testInCode(file, line, DEFINE_EXTENSION_OPEN_RE2)) continue;
|
|
4558
5616
|
const anchor = `${file.rel}:${line.n}`;
|
|
4559
5617
|
let depth = 0;
|
|
4560
5618
|
let started = false;
|
|
@@ -4565,17 +5623,17 @@ function findTsTypeSites(tree) {
|
|
|
4565
5623
|
const bl = lines[j];
|
|
4566
5624
|
if (!bl.comment) {
|
|
4567
5625
|
if (!found) {
|
|
4568
|
-
const lit = bl
|
|
5626
|
+
const lit = matchInCode(file, bl, TS_TYPE_LITERAL_RE);
|
|
4569
5627
|
if (lit) {
|
|
4570
5628
|
value = lit[1];
|
|
4571
5629
|
found = true;
|
|
4572
|
-
} else if (
|
|
5630
|
+
} else if (testInCode(file, bl, TS_TYPE_KEY_RE)) {
|
|
4573
5631
|
unresolved = true;
|
|
4574
5632
|
found = true;
|
|
4575
5633
|
}
|
|
4576
5634
|
}
|
|
4577
|
-
depth += (bl
|
|
4578
|
-
depth -= (bl
|
|
5635
|
+
depth += countInCode(file, bl, "{");
|
|
5636
|
+
depth -= countInCode(file, bl, "}");
|
|
4579
5637
|
if (depth > 0) started = true;
|
|
4580
5638
|
}
|
|
4581
5639
|
if (started && depth <= 0) break;
|
|
@@ -4599,101 +5657,119 @@ var dExt3 = {
|
|
|
4599
5657
|
`DExt3 single-type control FAILED: detectMix(\`${TYPE_MIXED}\`)=${controls.type.detectsMix} detectSingle(\`${TYPE_SINGLE}\`)=${controls.type.detectsSingle} \u2014 matcher broken, verdict untrustworthy`
|
|
4600
5658
|
);
|
|
4601
5659
|
}
|
|
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
|
-
|
|
5660
|
+
const unreadable = [];
|
|
5661
|
+
const decided = (() => {
|
|
5662
|
+
const declared = /* @__PURE__ */ new Set();
|
|
5663
|
+
let firstAnchor2 = null;
|
|
5664
|
+
const manifests = findExtensionManifests(tree);
|
|
5665
|
+
for (const manifestPath2 of manifests) {
|
|
5666
|
+
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
5667
|
+
const parsed = readJsonObject(manifestPath2);
|
|
5668
|
+
if (parsed === null) {
|
|
5669
|
+
unreadable.push({
|
|
5670
|
+
anchor: shortPath,
|
|
5671
|
+
reason: `could not parse ${shortPath} as JSON \u2014 a present manifest with an unreadable type cannot pass (Extension Rule 3)`
|
|
5672
|
+
});
|
|
5673
|
+
continue;
|
|
5674
|
+
}
|
|
5675
|
+
if (!("type" in parsed)) {
|
|
5676
|
+
return {
|
|
5677
|
+
id: this.id,
|
|
5678
|
+
anchor: shortPath,
|
|
5679
|
+
state: "non-conforming",
|
|
5680
|
+
verdict: `manifest ${shortPath} has no "type" field -> declare exactly one type (middleware|tool-extension|transport) (Extension Rule 3)`
|
|
5681
|
+
};
|
|
5682
|
+
}
|
|
5683
|
+
const rawValue = coerceTypeValue(parsed.type);
|
|
5684
|
+
if (rawValue === null) {
|
|
5685
|
+
return {
|
|
5686
|
+
id: this.id,
|
|
5687
|
+
anchor: shortPath,
|
|
5688
|
+
state: "non-conforming",
|
|
5689
|
+
verdict: `manifest ${shortPath} "type" is neither string nor string[] (got ${typeof parsed.type}) -> declare exactly one type (middleware|tool-extension|transport) (Extension Rule 3)`
|
|
5690
|
+
};
|
|
5691
|
+
}
|
|
5692
|
+
const found = typesIn(rawValue);
|
|
5693
|
+
if (found.length === 0) {
|
|
5694
|
+
return {
|
|
5695
|
+
id: this.id,
|
|
5696
|
+
anchor: shortPath,
|
|
5697
|
+
state: "non-conforming",
|
|
5698
|
+
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)`
|
|
5699
|
+
};
|
|
5700
|
+
}
|
|
5701
|
+
if (found.length > 1) {
|
|
5702
|
+
return {
|
|
5703
|
+
id: this.id,
|
|
5704
|
+
anchor: shortPath,
|
|
5705
|
+
state: "non-conforming",
|
|
5706
|
+
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)`
|
|
5707
|
+
};
|
|
5708
|
+
}
|
|
5709
|
+
firstAnchor2 ??= shortPath;
|
|
5710
|
+
declared.add(found[0]);
|
|
5711
|
+
}
|
|
5712
|
+
const sites = findTsTypeSites(tree);
|
|
5713
|
+
for (const site of sites) {
|
|
5714
|
+
if (site.unresolved) {
|
|
5715
|
+
unreadable.push({
|
|
5716
|
+
anchor: site.anchor,
|
|
5717
|
+
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)`
|
|
5718
|
+
});
|
|
5719
|
+
continue;
|
|
5720
|
+
}
|
|
5721
|
+
const raw = site.value ?? "";
|
|
5722
|
+
const found = typesIn(raw);
|
|
5723
|
+
if (found.length === 0) {
|
|
5724
|
+
return {
|
|
5725
|
+
id: this.id,
|
|
5726
|
+
anchor: site.anchor,
|
|
5727
|
+
state: "non-conforming",
|
|
5728
|
+
verdict: `defineExtension type: "${raw}" at ${site.anchor} names no known category (middleware|tool-extension|transport) -> declare exactly one known type (Extension Rule 3)`
|
|
5729
|
+
};
|
|
5730
|
+
}
|
|
5731
|
+
if (found.length > 1) {
|
|
5732
|
+
return {
|
|
5733
|
+
id: this.id,
|
|
5734
|
+
anchor: site.anchor,
|
|
5735
|
+
state: "non-conforming",
|
|
5736
|
+
verdict: `type: "${raw}" mixes ${found.join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
|
|
5737
|
+
};
|
|
5738
|
+
}
|
|
5739
|
+
firstAnchor2 ??= site.anchor;
|
|
5740
|
+
declared.add(found[0]);
|
|
4631
5741
|
}
|
|
4632
|
-
|
|
5742
|
+
const total = manifests.length + sites.length;
|
|
5743
|
+
if (total === 0) {
|
|
4633
5744
|
return {
|
|
4634
5745
|
id: this.id,
|
|
4635
|
-
anchor:
|
|
4636
|
-
state: "
|
|
4637
|
-
verdict:
|
|
5746
|
+
anchor: tree.root.split("/").pop() ?? tree.root,
|
|
5747
|
+
state: "absent",
|
|
5748
|
+
verdict: "no extension type declared (no extension.json manifest, no defineExtension `type:` literal) -> declare exactly one type (Extension Rule 3)"
|
|
4638
5749
|
};
|
|
4639
5750
|
}
|
|
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) {
|
|
5751
|
+
if (declared.size > 1) {
|
|
4658
5752
|
return {
|
|
4659
5753
|
id: this.id,
|
|
4660
|
-
anchor:
|
|
5754
|
+
anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
|
|
4661
5755
|
state: "non-conforming",
|
|
4662
|
-
verdict: `
|
|
5756
|
+
verdict: `extension declares ${declared.size} types across its forms: ${[...declared].join(" + ")} (positive-control OK) -> one extension = one type (Extension Rule 3)`
|
|
4663
5757
|
};
|
|
4664
5758
|
}
|
|
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
5759
|
return {
|
|
4679
5760
|
id: this.id,
|
|
4680
5761
|
anchor: firstAnchor2 ?? tree.root.split("/").pop() ?? tree.root,
|
|
4681
|
-
state: "
|
|
4682
|
-
verdict: `
|
|
5762
|
+
state: "present",
|
|
5763
|
+
verdict: `single type "${[...declared][0]}" declared across ${total} form(s) (manifest=${manifests.length}, ts=${sites.length}, positive-control OK)`
|
|
4683
5764
|
};
|
|
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
|
-
};
|
|
5765
|
+
})();
|
|
5766
|
+
return settle(this.id, decided, unreadable);
|
|
4691
5767
|
}
|
|
4692
5768
|
};
|
|
4693
5769
|
|
|
4694
5770
|
// src/detectors/dExt4.ts
|
|
4695
|
-
import {
|
|
4696
|
-
import { join as
|
|
5771
|
+
import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
5772
|
+
import { join as join13 } from "path";
|
|
4697
5773
|
function canonical(name) {
|
|
4698
5774
|
return name.replace(/[-_]/g, "").toLowerCase();
|
|
4699
5775
|
}
|
|
@@ -4712,71 +5788,40 @@ function dExt4PositiveControls() {
|
|
|
4712
5788
|
}
|
|
4713
5789
|
};
|
|
4714
5790
|
}
|
|
4715
|
-
|
|
4716
|
-
|
|
5791
|
+
function findHooksDirs(tree) {
|
|
5792
|
+
const root = tree.root;
|
|
4717
5793
|
const out = [];
|
|
4718
5794
|
let base;
|
|
4719
5795
|
try {
|
|
4720
|
-
base =
|
|
5796
|
+
base = statSync6(root).isDirectory() ? root : join13(root, "..");
|
|
4721
5797
|
} catch {
|
|
4722
5798
|
return out;
|
|
4723
5799
|
}
|
|
4724
5800
|
const walk2 = (dir) => {
|
|
4725
5801
|
let entries;
|
|
4726
5802
|
try {
|
|
4727
|
-
entries =
|
|
5803
|
+
entries = readdirSync5(dir);
|
|
4728
5804
|
} catch {
|
|
4729
5805
|
return;
|
|
4730
5806
|
}
|
|
4731
5807
|
for (const name of entries) {
|
|
4732
|
-
const full =
|
|
5808
|
+
const full = join13(dir, name);
|
|
4733
5809
|
let st;
|
|
4734
5810
|
try {
|
|
4735
|
-
st =
|
|
5811
|
+
st = statSync6(full);
|
|
4736
5812
|
} catch {
|
|
4737
5813
|
continue;
|
|
4738
5814
|
}
|
|
4739
|
-
if (st.isDirectory())
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
} else if (name === "extension.json") {
|
|
4743
|
-
out.push(full);
|
|
4744
|
-
}
|
|
4745
|
-
}
|
|
4746
|
-
};
|
|
4747
|
-
walk2(base);
|
|
4748
|
-
return out;
|
|
4749
|
-
}
|
|
4750
|
-
function findHooksDirs(root) {
|
|
4751
|
-
const out = [];
|
|
4752
|
-
let base;
|
|
4753
|
-
try {
|
|
4754
|
-
base = statSync8(root).isDirectory() ? root : join15(root, "..");
|
|
4755
|
-
} catch {
|
|
4756
|
-
return out;
|
|
4757
|
-
}
|
|
4758
|
-
const walk2 = (dir) => {
|
|
4759
|
-
let entries;
|
|
4760
|
-
try {
|
|
4761
|
-
entries = readdirSync7(dir);
|
|
4762
|
-
} catch {
|
|
4763
|
-
return;
|
|
4764
|
-
}
|
|
4765
|
-
for (const name of entries) {
|
|
4766
|
-
const full = join15(dir, name);
|
|
4767
|
-
let st;
|
|
4768
|
-
try {
|
|
4769
|
-
st = statSync8(full);
|
|
4770
|
-
} catch {
|
|
5815
|
+
if (!st.isDirectory()) continue;
|
|
5816
|
+
if (SKIP_DIRS.has(name)) {
|
|
5817
|
+
recordSkip(tree, full, base);
|
|
4771
5818
|
continue;
|
|
4772
5819
|
}
|
|
4773
|
-
if (!st.isDirectory()) continue;
|
|
4774
|
-
if (SKIP_DIRS5.has(name)) continue;
|
|
4775
5820
|
if (name === "hooks") {
|
|
4776
5821
|
const tokens = /* @__PURE__ */ new Set();
|
|
4777
5822
|
let files;
|
|
4778
5823
|
try {
|
|
4779
|
-
files =
|
|
5824
|
+
files = readdirSync5(full);
|
|
4780
5825
|
} catch {
|
|
4781
5826
|
files = [];
|
|
4782
5827
|
}
|
|
@@ -4805,7 +5850,7 @@ function findFactorySites(tree) {
|
|
|
4805
5850
|
for (let i = 0; i < lines.length; i++) {
|
|
4806
5851
|
const line = lines[i];
|
|
4807
5852
|
if (line.comment) continue;
|
|
4808
|
-
if (
|
|
5853
|
+
if (testInCode(file, line, LIFECYCLE_OBJ_RE)) {
|
|
4809
5854
|
const anchor = `${file.rel}:${line.n}`;
|
|
4810
5855
|
const tokens = /* @__PURE__ */ new Set();
|
|
4811
5856
|
let depth = 0;
|
|
@@ -4813,20 +5858,19 @@ function findFactorySites(tree) {
|
|
|
4813
5858
|
for (let j = i; j < lines.length; j++) {
|
|
4814
5859
|
const bl = lines[j];
|
|
4815
5860
|
if (bl.comment) continue;
|
|
4816
|
-
const raw = bl.raw;
|
|
4817
5861
|
if (started && depth === 1) {
|
|
4818
|
-
const k =
|
|
5862
|
+
const k = matchInCode(file, bl, OBJECT_KEY_RE);
|
|
4819
5863
|
if (k) tokens.add(canonical(k[1]));
|
|
4820
5864
|
}
|
|
4821
|
-
depth += (
|
|
4822
|
-
depth -= (
|
|
5865
|
+
depth += countInCode(file, bl, "{");
|
|
5866
|
+
depth -= countInCode(file, bl, "}");
|
|
4823
5867
|
if (depth > 0) started = true;
|
|
4824
5868
|
if (started && depth <= 0) break;
|
|
4825
5869
|
}
|
|
4826
5870
|
sites.push({ anchor, tokens });
|
|
4827
5871
|
continue;
|
|
4828
5872
|
}
|
|
4829
|
-
if (
|
|
5873
|
+
if (testInCode(file, line, LIFECYCLE_KEY_RE)) {
|
|
4830
5874
|
unresolved.push(`${file.rel}:${line.n}`);
|
|
4831
5875
|
}
|
|
4832
5876
|
}
|
|
@@ -4836,7 +5880,7 @@ function findFactorySites(tree) {
|
|
|
4836
5880
|
function manifestHookTokens(raw) {
|
|
4837
5881
|
const set = /* @__PURE__ */ new Set();
|
|
4838
5882
|
if (!Array.isArray(raw)) {
|
|
4839
|
-
|
|
5883
|
+
return null;
|
|
4840
5884
|
}
|
|
4841
5885
|
for (const h of raw) {
|
|
4842
5886
|
if (typeof h === "string") set.add(canonical(h));
|
|
@@ -4852,83 +5896,91 @@ var dExt4 = {
|
|
|
4852
5896
|
`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
5897
|
);
|
|
4854
5898
|
}
|
|
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
|
-
);
|
|
5899
|
+
const unreadable = [];
|
|
5900
|
+
const decided = (() => {
|
|
5901
|
+
const hooksDirs = findHooksDirs(tree);
|
|
5902
|
+
const { sites, unresolved } = findFactorySites(tree);
|
|
5903
|
+
for (const anchor of unresolved) {
|
|
5904
|
+
unreadable.push({
|
|
5905
|
+
anchor,
|
|
5906
|
+
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)`
|
|
5907
|
+
});
|
|
4893
5908
|
}
|
|
4894
|
-
const
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
5909
|
+
const implemented = /* @__PURE__ */ new Set();
|
|
5910
|
+
for (const d of hooksDirs) for (const t of d.tokens) implemented.add(t);
|
|
5911
|
+
for (const s of sites) for (const t of s.tokens) implemented.add(t);
|
|
5912
|
+
const implSurfacePresent = hooksDirs.length > 0 || sites.length > 0;
|
|
5913
|
+
const manifests = findExtensionManifests(tree);
|
|
5914
|
+
let declaredAnyLifecycle = false;
|
|
5915
|
+
for (const manifestPath2 of manifests) {
|
|
5916
|
+
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
5917
|
+
const parsed = readJsonObject(manifestPath2);
|
|
5918
|
+
if (parsed === null) {
|
|
5919
|
+
unreadable.push({
|
|
5920
|
+
anchor: shortPath,
|
|
5921
|
+
reason: `could not parse ${shortPath} as JSON \u2014 a present manifest whose lifecycle cannot be read cannot pass (Extension Rule 4)`
|
|
5922
|
+
});
|
|
5923
|
+
continue;
|
|
5924
|
+
}
|
|
5925
|
+
if (!("lifecycle" in parsed)) continue;
|
|
5926
|
+
const declared = manifestHookTokens(parsed.lifecycle);
|
|
5927
|
+
if (declared === null) {
|
|
5928
|
+
return {
|
|
5929
|
+
id: this.id,
|
|
5930
|
+
anchor: shortPath,
|
|
5931
|
+
state: "non-conforming",
|
|
5932
|
+
verdict: `manifest ${shortPath} "lifecycle" is not an array (got ${typeof parsed.lifecycle}) -> declare the hooks as an array of hook names (Extension Rule 4)`
|
|
5933
|
+
};
|
|
5934
|
+
}
|
|
5935
|
+
if (declared.size === 0) continue;
|
|
5936
|
+
declaredAnyLifecycle = true;
|
|
5937
|
+
if (!implSurfacePresent) {
|
|
5938
|
+
unreadable.push({
|
|
5939
|
+
anchor: shortPath,
|
|
5940
|
+
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)`
|
|
5941
|
+
});
|
|
5942
|
+
continue;
|
|
5943
|
+
}
|
|
5944
|
+
const phantom = [...declared].filter((h) => !implemented.has(h));
|
|
5945
|
+
if (phantom.length > 0) {
|
|
5946
|
+
return {
|
|
5947
|
+
id: this.id,
|
|
5948
|
+
anchor: shortPath,
|
|
5949
|
+
state: "non-conforming",
|
|
5950
|
+
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)`
|
|
5951
|
+
};
|
|
5952
|
+
}
|
|
4902
5953
|
}
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
5954
|
+
if (!declaredAnyLifecycle) {
|
|
5955
|
+
if (manifests.length === 0 && !implSurfacePresent) {
|
|
5956
|
+
return {
|
|
5957
|
+
id: this.id,
|
|
5958
|
+
anchor: tree.root.split("/").pop() ?? tree.root,
|
|
5959
|
+
state: "absent",
|
|
5960
|
+
verdict: "no lifecycle hooks declared (no extension.json manifest `lifecycle` array) -> declare the hooks the extension implements (Extension Rule 4)"
|
|
5961
|
+
};
|
|
5962
|
+
}
|
|
4906
5963
|
return {
|
|
4907
5964
|
id: this.id,
|
|
4908
|
-
anchor: tree.root.split("/").pop() ?? tree.root,
|
|
4909
|
-
state: "
|
|
4910
|
-
verdict:
|
|
5965
|
+
anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
|
|
5966
|
+
state: "present",
|
|
5967
|
+
verdict: `no manifest lifecycle hooks declared, nothing to reconcile (implemented: [${[...implemented].join(", ") || "none"}], positive-control OK)`
|
|
4911
5968
|
};
|
|
4912
5969
|
}
|
|
4913
5970
|
return {
|
|
4914
5971
|
id: this.id,
|
|
4915
|
-
anchor: manifests
|
|
5972
|
+
anchor: manifests[0].split("/").slice(-2).join("/"),
|
|
4916
5973
|
state: "present",
|
|
4917
|
-
verdict: `
|
|
5974
|
+
verdict: `every declared lifecycle hook is implemented (declared \u2286 implemented: [${[...implemented].join(", ")}], hooksDir=${hooksDirs.length}, factory=${sites.length}, positive-control OK)`
|
|
4918
5975
|
};
|
|
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
|
-
};
|
|
5976
|
+
})();
|
|
5977
|
+
return settle(this.id, decided, unreadable);
|
|
4926
5978
|
}
|
|
4927
5979
|
};
|
|
4928
5980
|
|
|
4929
5981
|
// src/detectors/dExt5.ts
|
|
4930
|
-
import { existsSync as existsSync12
|
|
4931
|
-
import { dirname as dirname7, join as
|
|
5982
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5983
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
4932
5984
|
function nonEmptyString(v) {
|
|
4933
5985
|
return typeof v === "string" && v.trim().length > 0;
|
|
4934
5986
|
}
|
|
@@ -4942,41 +5994,6 @@ function dExt5PositiveControls() {
|
|
|
4942
5994
|
}
|
|
4943
5995
|
};
|
|
4944
5996
|
}
|
|
4945
|
-
var SKIP_DIRS6 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
4946
|
-
function findExtensionManifests5(root) {
|
|
4947
|
-
const out = [];
|
|
4948
|
-
let base;
|
|
4949
|
-
try {
|
|
4950
|
-
base = statSync9(root).isDirectory() ? root : join16(root, "..");
|
|
4951
|
-
} catch {
|
|
4952
|
-
return out;
|
|
4953
|
-
}
|
|
4954
|
-
const walk2 = (dir) => {
|
|
4955
|
-
let entries;
|
|
4956
|
-
try {
|
|
4957
|
-
entries = readdirSync8(dir);
|
|
4958
|
-
} catch {
|
|
4959
|
-
return;
|
|
4960
|
-
}
|
|
4961
|
-
for (const name of entries) {
|
|
4962
|
-
const full = join16(dir, name);
|
|
4963
|
-
let st;
|
|
4964
|
-
try {
|
|
4965
|
-
st = statSync9(full);
|
|
4966
|
-
} catch {
|
|
4967
|
-
continue;
|
|
4968
|
-
}
|
|
4969
|
-
if (st.isDirectory()) {
|
|
4970
|
-
if (SKIP_DIRS6.has(name)) continue;
|
|
4971
|
-
walk2(full);
|
|
4972
|
-
} else if (name === "extension.json") {
|
|
4973
|
-
out.push(full);
|
|
4974
|
-
}
|
|
4975
|
-
}
|
|
4976
|
-
};
|
|
4977
|
-
walk2(base);
|
|
4978
|
-
return out;
|
|
4979
|
-
}
|
|
4980
5997
|
function isToolExtension(type) {
|
|
4981
5998
|
return typeof type === "string" && type.includes("tool-extension");
|
|
4982
5999
|
}
|
|
@@ -4989,7 +6006,7 @@ var dExt5 = {
|
|
|
4989
6006
|
`DExt5 bilingual control FAILED: accept(\`${FR_POSITIVE}\`)=${controls.descriptionFr.positive} reject(empty)=${!controls.descriptionFr.negative} \u2014 FR-string matcher broken, absence verdict untrustworthy`
|
|
4990
6007
|
);
|
|
4991
6008
|
}
|
|
4992
|
-
const manifests =
|
|
6009
|
+
const manifests = findExtensionManifests(tree);
|
|
4993
6010
|
if (manifests.length === 0) {
|
|
4994
6011
|
return {
|
|
4995
6012
|
id: this.id,
|
|
@@ -5008,10 +6025,8 @@ var dExt5 = {
|
|
|
5008
6025
|
reason,
|
|
5009
6026
|
verdict: `could not judge: ${reason}`
|
|
5010
6027
|
});
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
parsed = JSON.parse(readFileSync14(manifestPath2, "utf8"));
|
|
5014
|
-
} catch {
|
|
6028
|
+
const parsed = readJsonObject(manifestPath2);
|
|
6029
|
+
if (parsed === null) {
|
|
5015
6030
|
return cannotRead(
|
|
5016
6031
|
`${shortPath} does not parse as JSON \u2014 the FR surface of a present manifest cannot be read (Extension Rule 5)`
|
|
5017
6032
|
);
|
|
@@ -5025,7 +6040,7 @@ var dExt5 = {
|
|
|
5025
6040
|
if (!nonEmptyString(parsed.description_fr)) {
|
|
5026
6041
|
missing.push("description_fr (extension.json)");
|
|
5027
6042
|
}
|
|
5028
|
-
if (!existsSync12(
|
|
6043
|
+
if (!existsSync12(join14(root, "README.fr.md"))) {
|
|
5029
6044
|
missing.push("README.fr.md");
|
|
5030
6045
|
}
|
|
5031
6046
|
if (isToolExtension(parsed.type) && "tools" in parsed) {
|
|
@@ -5068,8 +6083,8 @@ var dExt5 = {
|
|
|
5068
6083
|
};
|
|
5069
6084
|
|
|
5070
6085
|
// src/detectors/dExt7.ts
|
|
5071
|
-
import { existsSync as existsSync13
|
|
5072
|
-
import { dirname as dirname8, join as
|
|
6086
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6087
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
5073
6088
|
var ABSENCE_FLAG_FIELDS = [
|
|
5074
6089
|
"extensionAbsent",
|
|
5075
6090
|
"extension_absent",
|
|
@@ -5104,48 +6119,13 @@ var ABSENCE_POSITIVE_LABEL = {
|
|
|
5104
6119
|
op: "callTool"
|
|
5105
6120
|
};
|
|
5106
6121
|
var ABSENCE_NEGATIVE = { id: "ping-1", op: "callTool", tool: "ping" };
|
|
5107
|
-
function dExt7PositiveControls() {
|
|
5108
|
-
return {
|
|
5109
|
-
flag: isAbsenceCase(ABSENCE_POSITIVE_FLAG),
|
|
5110
|
-
token: isAbsenceCase(ABSENCE_POSITIVE_TOKEN),
|
|
5111
|
-
label: isAbsenceCase(ABSENCE_POSITIVE_LABEL),
|
|
5112
|
-
negative: isAbsenceCase(ABSENCE_NEGATIVE)
|
|
5113
|
-
};
|
|
5114
|
-
}
|
|
5115
|
-
var SKIP_DIRS7 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
5116
|
-
function findExtensionManifests6(root) {
|
|
5117
|
-
const out = [];
|
|
5118
|
-
let base;
|
|
5119
|
-
try {
|
|
5120
|
-
base = statSync10(root).isDirectory() ? root : join17(root, "..");
|
|
5121
|
-
} catch {
|
|
5122
|
-
return out;
|
|
5123
|
-
}
|
|
5124
|
-
const walk2 = (dir) => {
|
|
5125
|
-
let entries;
|
|
5126
|
-
try {
|
|
5127
|
-
entries = readdirSync9(dir);
|
|
5128
|
-
} catch {
|
|
5129
|
-
return;
|
|
5130
|
-
}
|
|
5131
|
-
for (const name of entries) {
|
|
5132
|
-
const full = join17(dir, name);
|
|
5133
|
-
let st;
|
|
5134
|
-
try {
|
|
5135
|
-
st = statSync10(full);
|
|
5136
|
-
} catch {
|
|
5137
|
-
continue;
|
|
5138
|
-
}
|
|
5139
|
-
if (st.isDirectory()) {
|
|
5140
|
-
if (SKIP_DIRS7.has(name)) continue;
|
|
5141
|
-
walk2(full);
|
|
5142
|
-
} else if (name === "extension.json") {
|
|
5143
|
-
out.push(full);
|
|
5144
|
-
}
|
|
5145
|
-
}
|
|
6122
|
+
function dExt7PositiveControls() {
|
|
6123
|
+
return {
|
|
6124
|
+
flag: isAbsenceCase(ABSENCE_POSITIVE_FLAG),
|
|
6125
|
+
token: isAbsenceCase(ABSENCE_POSITIVE_TOKEN),
|
|
6126
|
+
label: isAbsenceCase(ABSENCE_POSITIVE_LABEL),
|
|
6127
|
+
negative: isAbsenceCase(ABSENCE_NEGATIVE)
|
|
5146
6128
|
};
|
|
5147
|
-
walk2(base);
|
|
5148
|
-
return out;
|
|
5149
6129
|
}
|
|
5150
6130
|
var dExt7 = {
|
|
5151
6131
|
id: "DExt7 eval-coverage",
|
|
@@ -5156,7 +6136,7 @@ var dExt7 = {
|
|
|
5156
6136
|
`DExt7 absence-matcher control FAILED: flag=${controls.flag} token=${controls.token} label=${controls.label} reject(normal)=${!controls.negative} \u2014 absence matcher broken, eval-coverage verdict untrustworthy`
|
|
5157
6137
|
);
|
|
5158
6138
|
}
|
|
5159
|
-
const manifests =
|
|
6139
|
+
const manifests = findExtensionManifests(tree);
|
|
5160
6140
|
if (manifests.length === 0) {
|
|
5161
6141
|
return {
|
|
5162
6142
|
id: this.id,
|
|
@@ -5165,66 +6145,79 @@ var dExt7 = {
|
|
|
5165
6145
|
verdict: "no extension.json manifest found -> declare an extension with an eval suite (>=3 cases incl. 1 'extension absente') (Extension Rule 7)"
|
|
5166
6146
|
};
|
|
5167
6147
|
}
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
const
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
const
|
|
5184
|
-
|
|
6148
|
+
const unreadable = [];
|
|
6149
|
+
const decided = (() => {
|
|
6150
|
+
for (const manifestPath2 of manifests) {
|
|
6151
|
+
const extDir = dirname8(manifestPath2);
|
|
6152
|
+
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
6153
|
+
const evalsPath = join15(extDir, "evals", "evals.json");
|
|
6154
|
+
const shortPath = `${extLabel}/evals/evals.json`;
|
|
6155
|
+
if (!existsSync13(evalsPath)) {
|
|
6156
|
+
return {
|
|
6157
|
+
id: this.id,
|
|
6158
|
+
anchor: `${extLabel}/extension.json`,
|
|
6159
|
+
state: "non-conforming",
|
|
6160
|
+
verdict: `no evals/evals.json for extension "${extLabel}" -> add an eval suite with >=3 cases incl. 1 'extension absente' (Extension Rule 7)`
|
|
6161
|
+
};
|
|
6162
|
+
}
|
|
6163
|
+
const parsed = readJsonObject(evalsPath);
|
|
6164
|
+
if (parsed === null) {
|
|
6165
|
+
unreadable.push({
|
|
6166
|
+
anchor: shortPath,
|
|
6167
|
+
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)`
|
|
6168
|
+
});
|
|
6169
|
+
continue;
|
|
6170
|
+
}
|
|
5185
6171
|
if (!Array.isArray(parsed.cases)) {
|
|
5186
|
-
|
|
6172
|
+
return {
|
|
6173
|
+
id: this.id,
|
|
6174
|
+
anchor: shortPath,
|
|
6175
|
+
state: "non-conforming",
|
|
6176
|
+
verdict: `${shortPath} "cases" is not an array (got ${typeof parsed.cases}) -> declare cases as an array of eval cases (Extension Rule 7)`
|
|
6177
|
+
};
|
|
5187
6178
|
}
|
|
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.`
|
|
6179
|
+
const cases = parsed.cases;
|
|
6180
|
+
const badCase = cases.findIndex(
|
|
6181
|
+
(c) => c === null || typeof c !== "object" || Array.isArray(c)
|
|
5192
6182
|
);
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
6183
|
+
if (badCase !== -1) {
|
|
6184
|
+
const c = cases[badCase];
|
|
6185
|
+
return {
|
|
6186
|
+
id: this.id,
|
|
6187
|
+
anchor: shortPath,
|
|
6188
|
+
state: "non-conforming",
|
|
6189
|
+
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)`
|
|
6190
|
+
};
|
|
6191
|
+
}
|
|
6192
|
+
const count = cases.length;
|
|
6193
|
+
const hasAbsence = cases.some(isAbsenceCase);
|
|
6194
|
+
const failures = [];
|
|
6195
|
+
if (count < 3) failures.push(`only ${count} case(s) (need >=3)`);
|
|
6196
|
+
if (!hasAbsence) failures.push("no 'extension absente'/degradation case");
|
|
6197
|
+
if (failures.length > 0) {
|
|
6198
|
+
return {
|
|
6199
|
+
id: this.id,
|
|
6200
|
+
anchor: `${shortPath}`,
|
|
6201
|
+
state: "non-conforming",
|
|
6202
|
+
verdict: `eval suite for "${extLabel}": ${failures.join("; ")} (positive-control OK) -> Extension Rule 7 needs >=3 cases incl. 1 'extension absente'`
|
|
6203
|
+
};
|
|
5199
6204
|
}
|
|
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
6205
|
}
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
};
|
|
6206
|
+
const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
|
|
6207
|
+
return {
|
|
6208
|
+
id: this.id,
|
|
6209
|
+
anchor: `${firstLabel}/evals/evals.json`,
|
|
6210
|
+
state: "present",
|
|
6211
|
+
verdict: `eval suite complete across ${manifests.length} extension(s): >=3 cases incl. an 'extension absente'/degradation case (positive-control OK)`
|
|
6212
|
+
};
|
|
6213
|
+
})();
|
|
6214
|
+
return settle(this.id, decided, unreadable);
|
|
5222
6215
|
}
|
|
5223
6216
|
};
|
|
5224
6217
|
|
|
5225
6218
|
// src/detectors/dExt8.ts
|
|
5226
|
-
import { existsSync as existsSync14
|
|
5227
|
-
import { dirname as dirname9, join as
|
|
6219
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6220
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
5228
6221
|
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
6222
|
var POSITIVE_CONTROL5 = "1.2.3-beta.1+build.7";
|
|
5230
6223
|
function dExt8PositiveControl() {
|
|
@@ -5234,41 +6227,6 @@ function dExt8PositiveControl() {
|
|
|
5234
6227
|
matched: SEMVER_RE2.test(POSITIVE_CONTROL5)
|
|
5235
6228
|
};
|
|
5236
6229
|
}
|
|
5237
|
-
var SKIP_DIRS8 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "coverage"]);
|
|
5238
|
-
function findExtensionManifests7(root) {
|
|
5239
|
-
const out = [];
|
|
5240
|
-
let base;
|
|
5241
|
-
try {
|
|
5242
|
-
base = statSync11(root).isDirectory() ? root : join18(root, "..");
|
|
5243
|
-
} catch {
|
|
5244
|
-
return out;
|
|
5245
|
-
}
|
|
5246
|
-
const walk2 = (dir) => {
|
|
5247
|
-
let entries;
|
|
5248
|
-
try {
|
|
5249
|
-
entries = readdirSync10(dir);
|
|
5250
|
-
} catch {
|
|
5251
|
-
return;
|
|
5252
|
-
}
|
|
5253
|
-
for (const name of entries) {
|
|
5254
|
-
const full = join18(dir, name);
|
|
5255
|
-
let st;
|
|
5256
|
-
try {
|
|
5257
|
-
st = statSync11(full);
|
|
5258
|
-
} catch {
|
|
5259
|
-
continue;
|
|
5260
|
-
}
|
|
5261
|
-
if (st.isDirectory()) {
|
|
5262
|
-
if (SKIP_DIRS8.has(name)) continue;
|
|
5263
|
-
walk2(full);
|
|
5264
|
-
} else if (name === "extension.json") {
|
|
5265
|
-
out.push(full);
|
|
5266
|
-
}
|
|
5267
|
-
}
|
|
5268
|
-
};
|
|
5269
|
-
walk2(base);
|
|
5270
|
-
return out;
|
|
5271
|
-
}
|
|
5272
6230
|
var dExt8 = {
|
|
5273
6231
|
id: "DExt8 semver+changelog",
|
|
5274
6232
|
run(tree) {
|
|
@@ -5278,7 +6236,7 @@ var dExt8 = {
|
|
|
5278
6236
|
`DExt8 positive control FAILED: pattern /${control.pattern}/ did not match sample \`${control.sample}\` \u2014 matcher broken, verdict untrustworthy`
|
|
5279
6237
|
);
|
|
5280
6238
|
}
|
|
5281
|
-
const manifests =
|
|
6239
|
+
const manifests = findExtensionManifests(tree);
|
|
5282
6240
|
if (manifests.length === 0) {
|
|
5283
6241
|
return {
|
|
5284
6242
|
id: this.id,
|
|
@@ -5287,101 +6245,119 @@ var dExt8 = {
|
|
|
5287
6245
|
verdict: "no extension.json manifest found -> declare an extension with a valid SemVer version + CHANGELOG.md (Extension Rule 8)"
|
|
5288
6246
|
};
|
|
5289
6247
|
}
|
|
6248
|
+
const unreadable = [];
|
|
6249
|
+
let finding;
|
|
6250
|
+
let judgedConforming = 0;
|
|
5290
6251
|
for (const manifestPath2 of manifests) {
|
|
5291
6252
|
const extDir = dirname9(manifestPath2);
|
|
5292
6253
|
const extLabel = extDir.split("/").pop() ?? extDir;
|
|
5293
|
-
const pkgPath =
|
|
6254
|
+
const pkgPath = join16(extDir, "package.json");
|
|
5294
6255
|
const shortPkgPath = `${extLabel}/package.json`;
|
|
5295
6256
|
if (!existsSync14(pkgPath)) {
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
6257
|
+
unreadable.push({
|
|
6258
|
+
anchor: shortPkgPath,
|
|
6259
|
+
reason: `could not locate ${shortPkgPath} for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8)`
|
|
6260
|
+
});
|
|
6261
|
+
continue;
|
|
5299
6262
|
}
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
6263
|
+
const pkgJson = readJsonObject(pkgPath);
|
|
6264
|
+
if (pkgJson === null) {
|
|
6265
|
+
unreadable.push({
|
|
6266
|
+
anchor: shortPkgPath,
|
|
6267
|
+
reason: `could not parse ${shortPkgPath} as JSON for extension "${extLabel}" \u2014 cannot verify SemVer/CHANGELOG (Extension Rule 8)`
|
|
6268
|
+
});
|
|
6269
|
+
continue;
|
|
5307
6270
|
}
|
|
5308
6271
|
const version = pkgJson.version;
|
|
5309
6272
|
if ("version" in pkgJson && typeof version !== "string") {
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
6273
|
+
finding ??= {
|
|
6274
|
+
id: this.id,
|
|
6275
|
+
anchor: shortPkgPath,
|
|
6276
|
+
state: "non-conforming",
|
|
6277
|
+
verdict: `"version" in ${shortPkgPath} is not a string (got ${typeof version}) for extension "${extLabel}" -> set a valid SemVer version string (Extension Rule 8)`
|
|
6278
|
+
};
|
|
6279
|
+
continue;
|
|
5313
6280
|
}
|
|
5314
6281
|
if (typeof version !== "string" || version.trim().length === 0) {
|
|
5315
|
-
|
|
6282
|
+
finding ??= {
|
|
5316
6283
|
id: this.id,
|
|
5317
6284
|
anchor: shortPkgPath,
|
|
5318
6285
|
state: "non-conforming",
|
|
5319
6286
|
verdict: `no "version" field in ${shortPkgPath} for extension "${extLabel}" -> set a valid SemVer version (Extension Rule 8)`
|
|
5320
6287
|
};
|
|
6288
|
+
continue;
|
|
5321
6289
|
}
|
|
5322
6290
|
const validSemver = SEMVER_RE2.test(version);
|
|
5323
|
-
const changelogPath =
|
|
6291
|
+
const changelogPath = join16(extDir, "CHANGELOG.md");
|
|
5324
6292
|
const hasChangelog = existsSync14(changelogPath);
|
|
5325
6293
|
if (!validSemver) {
|
|
5326
|
-
|
|
6294
|
+
finding ??= {
|
|
5327
6295
|
id: this.id,
|
|
5328
6296
|
anchor: shortPkgPath,
|
|
5329
6297
|
state: "non-conforming",
|
|
5330
6298
|
verdict: `"version": "${version}" is not valid SemVer for extension "${extLabel}" (positive-control OK) -> use MAJOR.MINOR.PATCH (Extension Rule 8)`
|
|
5331
6299
|
};
|
|
6300
|
+
continue;
|
|
5332
6301
|
}
|
|
5333
6302
|
if (!hasChangelog) {
|
|
5334
|
-
|
|
6303
|
+
finding ??= {
|
|
5335
6304
|
id: this.id,
|
|
5336
6305
|
anchor: shortPkgPath,
|
|
5337
6306
|
state: "absent",
|
|
5338
6307
|
verdict: `no CHANGELOG.md next to ${shortPkgPath} for extension "${extLabel}" (positive-control OK) -> add CHANGELOG.md, update it each release (Extension Rule 8)`
|
|
5339
6308
|
};
|
|
6309
|
+
continue;
|
|
5340
6310
|
}
|
|
6311
|
+
judgedConforming++;
|
|
5341
6312
|
}
|
|
6313
|
+
if (finding) return settle(this.id, finding, unreadable);
|
|
5342
6314
|
const firstLabel = manifests[0].split("/").slice(-2, -1)[0] ?? "ext";
|
|
5343
|
-
|
|
6315
|
+
const conforming = {
|
|
5344
6316
|
id: this.id,
|
|
5345
6317
|
anchor: `${firstLabel}/package.json`,
|
|
5346
6318
|
state: "present",
|
|
5347
|
-
verdict: `SemVer valid + CHANGELOG.md present across ${manifests.length} extension(s) (positive-control OK)`
|
|
6319
|
+
verdict: `SemVer valid + CHANGELOG.md present across ${judgedConforming} of ${manifests.length} extension(s) (positive-control OK)`
|
|
5348
6320
|
};
|
|
6321
|
+
return settle(this.id, conforming, unreadable);
|
|
5349
6322
|
}
|
|
5350
6323
|
};
|
|
5351
6324
|
|
|
5352
6325
|
// src/detectors/dExt9.ts
|
|
5353
|
-
import { readFileSync as
|
|
5354
|
-
import { join as
|
|
5355
|
-
|
|
5356
|
-
|
|
6326
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
|
|
6327
|
+
import { join as join17 } from "path";
|
|
6328
|
+
function collectHooksDirSources(tree, unreadable) {
|
|
6329
|
+
const root = tree.root;
|
|
5357
6330
|
const out = [];
|
|
5358
6331
|
let base;
|
|
5359
6332
|
try {
|
|
5360
|
-
base =
|
|
6333
|
+
base = statSync7(root).isDirectory() ? root : join17(root, "..");
|
|
5361
6334
|
} catch {
|
|
5362
6335
|
return out;
|
|
5363
6336
|
}
|
|
5364
6337
|
const walk2 = (dir) => {
|
|
5365
6338
|
let entries;
|
|
5366
6339
|
try {
|
|
5367
|
-
entries =
|
|
6340
|
+
entries = readdirSync6(dir);
|
|
5368
6341
|
} catch {
|
|
5369
6342
|
return;
|
|
5370
6343
|
}
|
|
5371
6344
|
for (const name of entries) {
|
|
5372
|
-
const full =
|
|
6345
|
+
const full = join17(dir, name);
|
|
5373
6346
|
let st;
|
|
5374
6347
|
try {
|
|
5375
|
-
st =
|
|
6348
|
+
st = statSync7(full);
|
|
5376
6349
|
} catch {
|
|
5377
6350
|
continue;
|
|
5378
6351
|
}
|
|
5379
6352
|
if (!st.isDirectory()) continue;
|
|
5380
|
-
if (
|
|
6353
|
+
if (SKIP_DIRS.has(name)) {
|
|
6354
|
+
recordSkip(tree, full, base);
|
|
6355
|
+
continue;
|
|
6356
|
+
}
|
|
5381
6357
|
if (name === "hooks") {
|
|
5382
6358
|
let files;
|
|
5383
6359
|
try {
|
|
5384
|
-
files =
|
|
6360
|
+
files = readdirSync6(full);
|
|
5385
6361
|
} catch {
|
|
5386
6362
|
files = [];
|
|
5387
6363
|
}
|
|
@@ -5389,14 +6365,17 @@ function collectHooksDirSources(root) {
|
|
|
5389
6365
|
if (!f.endsWith(".ts") || f.endsWith(".d.ts")) continue;
|
|
5390
6366
|
const stem = f.replace(/\.ts$/, "");
|
|
5391
6367
|
if (stem === "index") continue;
|
|
5392
|
-
const filePath =
|
|
6368
|
+
const filePath = join17(full, f);
|
|
5393
6369
|
let body;
|
|
5394
6370
|
try {
|
|
5395
|
-
body =
|
|
6371
|
+
body = readFileSync10(filePath, "utf8");
|
|
5396
6372
|
} catch {
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
6373
|
+
const shortPath = filePath.split("/").slice(-2).join("/");
|
|
6374
|
+
unreadable.push({
|
|
6375
|
+
anchor: shortPath,
|
|
6376
|
+
reason: `could not read hook implementation ${shortPath} \u2014 timeout guard undeterminable for hook "${stem}" (Extension Rule 9)`
|
|
6377
|
+
});
|
|
6378
|
+
continue;
|
|
5400
6379
|
}
|
|
5401
6380
|
out.push({
|
|
5402
6381
|
token: canonical(stem),
|
|
@@ -5420,7 +6399,7 @@ function collectFactorySources(tree) {
|
|
|
5420
6399
|
for (let i = 0; i < lines.length; i++) {
|
|
5421
6400
|
const line = lines[i];
|
|
5422
6401
|
if (line.comment) continue;
|
|
5423
|
-
if (!
|
|
6402
|
+
if (!testInCode(file, line, LIFECYCLE_OBJ_RE2)) continue;
|
|
5424
6403
|
let depth = 0;
|
|
5425
6404
|
let started = false;
|
|
5426
6405
|
let curKey = null;
|
|
@@ -5430,7 +6409,7 @@ function collectFactorySources(tree) {
|
|
|
5430
6409
|
if (bl.comment) continue;
|
|
5431
6410
|
const raw = bl.raw;
|
|
5432
6411
|
if (started && depth === 1) {
|
|
5433
|
-
const k =
|
|
6412
|
+
const k = matchInCode(file, bl, OBJECT_KEY_RE2);
|
|
5434
6413
|
if (k) {
|
|
5435
6414
|
curKey = canonical(k[1]);
|
|
5436
6415
|
keyBodies.set(curKey, []);
|
|
@@ -5439,8 +6418,8 @@ function collectFactorySources(tree) {
|
|
|
5439
6418
|
if (curKey && depth >= 1) {
|
|
5440
6419
|
keyBodies.get(curKey)?.push(raw);
|
|
5441
6420
|
}
|
|
5442
|
-
depth += (
|
|
5443
|
-
depth -= (
|
|
6421
|
+
depth += countInCode(file, bl, "{");
|
|
6422
|
+
depth -= countInCode(file, bl, "}");
|
|
5444
6423
|
if (depth > 0) started = true;
|
|
5445
6424
|
if (started && depth <= 0) break;
|
|
5446
6425
|
}
|
|
@@ -5466,13 +6445,11 @@ function dExt9PositiveControls() {
|
|
|
5466
6445
|
negative: hasImplTimeoutGuard(CTRL_NEGATIVE_BODY)
|
|
5467
6446
|
};
|
|
5468
6447
|
}
|
|
5469
|
-
function manifestTimeoutTokens(raw
|
|
6448
|
+
function manifestTimeoutTokens(raw) {
|
|
5470
6449
|
const map = /* @__PURE__ */ new Map();
|
|
5471
6450
|
if (raw === void 0) return map;
|
|
5472
6451
|
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
|
-
);
|
|
6452
|
+
return null;
|
|
5476
6453
|
}
|
|
5477
6454
|
for (const [key, value] of Object.entries(raw)) {
|
|
5478
6455
|
if (typeof value === "number" && value > 0) {
|
|
@@ -5490,67 +6467,83 @@ var dExt9 = {
|
|
|
5490
6467
|
`DExt9 hook-timeout control FAILED: setTimeoutForm=${controls.setTimeoutForm} abortForm=${controls.abortForm} negative=${controls.negative} \u2014 impl-pattern matcher broken, verdict untrustworthy`
|
|
5491
6468
|
);
|
|
5492
6469
|
}
|
|
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
|
-
)
|
|
6470
|
+
const unreadable = [];
|
|
6471
|
+
const decided = (() => {
|
|
6472
|
+
const hookSources = [
|
|
6473
|
+
...collectHooksDirSources(tree, unreadable),
|
|
6474
|
+
...collectFactorySources(tree)
|
|
6475
|
+
];
|
|
6476
|
+
const implByToken = /* @__PURE__ */ new Map();
|
|
6477
|
+
for (const s of hookSources) {
|
|
6478
|
+
const list = implByToken.get(s.token) ?? [];
|
|
6479
|
+
list.push(s);
|
|
6480
|
+
implByToken.set(s.token, list);
|
|
6481
|
+
}
|
|
6482
|
+
const manifests = findExtensionManifests(tree);
|
|
6483
|
+
let declaredAnyLifecycle = false;
|
|
6484
|
+
for (const manifestPath2 of manifests) {
|
|
6485
|
+
const shortPath = manifestPath2.split("/").slice(-2).join("/");
|
|
6486
|
+
const parsed = readJsonObject(manifestPath2);
|
|
6487
|
+
if (parsed === null) {
|
|
6488
|
+
unreadable.push({
|
|
6489
|
+
anchor: shortPath,
|
|
6490
|
+
reason: `could not parse ${shortPath} as JSON \u2014 timeout guards undeterminable (Extension Rule 9)`
|
|
6491
|
+
});
|
|
6492
|
+
continue;
|
|
6493
|
+
}
|
|
6494
|
+
if (!("lifecycle" in parsed)) continue;
|
|
6495
|
+
const declared = manifestHookTokens(parsed.lifecycle);
|
|
6496
|
+
if (declared === null) {
|
|
6497
|
+
return {
|
|
6498
|
+
id: this.id,
|
|
6499
|
+
anchor: shortPath,
|
|
6500
|
+
state: "non-conforming",
|
|
6501
|
+
verdict: `manifest ${shortPath} "lifecycle" is not an array (got ${typeof parsed.lifecycle}) -> declare the hooks as an array of hook names (Extension Rule 9)`
|
|
6502
|
+
};
|
|
6503
|
+
}
|
|
6504
|
+
if (declared.size === 0) continue;
|
|
6505
|
+
declaredAnyLifecycle = true;
|
|
6506
|
+
const timeouts = manifestTimeoutTokens(parsed.timeouts);
|
|
6507
|
+
if (timeouts === null) {
|
|
6508
|
+
return {
|
|
6509
|
+
id: this.id,
|
|
6510
|
+
anchor: shortPath,
|
|
6511
|
+
state: "non-conforming",
|
|
6512
|
+
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)`
|
|
6513
|
+
};
|
|
6514
|
+
}
|
|
6515
|
+
const noTimeout = [];
|
|
6516
|
+
for (const hook of declared) {
|
|
6517
|
+
const hasManifestTimeout = timeouts.has(hook);
|
|
6518
|
+
const implSources = implByToken.get(hook) ?? [];
|
|
6519
|
+
const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
|
|
6520
|
+
if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
|
|
6521
|
+
}
|
|
6522
|
+
if (noTimeout.length > 0) {
|
|
6523
|
+
return {
|
|
6524
|
+
id: this.id,
|
|
6525
|
+
anchor: shortPath,
|
|
6526
|
+
state: "non-conforming",
|
|
6527
|
+
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)`
|
|
6528
|
+
};
|
|
6529
|
+
}
|
|
5520
6530
|
}
|
|
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) {
|
|
6531
|
+
if (!declaredAnyLifecycle) {
|
|
5532
6532
|
return {
|
|
5533
6533
|
id: this.id,
|
|
5534
|
-
anchor:
|
|
5535
|
-
state: "
|
|
5536
|
-
verdict:
|
|
6534
|
+
anchor: manifests.length > 0 ? manifests[0].split("/").slice(-2).join("/") : tree.root.split("/").pop() ?? tree.root,
|
|
6535
|
+
state: "absent",
|
|
6536
|
+
verdict: "no lifecycle hooks declared, nothing to guard with a timeout (Extension Rule 9)"
|
|
5537
6537
|
};
|
|
5538
6538
|
}
|
|
5539
|
-
}
|
|
5540
|
-
if (!declaredAnyLifecycle) {
|
|
5541
6539
|
return {
|
|
5542
6540
|
id: this.id,
|
|
5543
|
-
anchor: manifests
|
|
5544
|
-
state: "
|
|
5545
|
-
verdict: "
|
|
6541
|
+
anchor: manifests[0].split("/").slice(-2).join("/"),
|
|
6542
|
+
state: "present",
|
|
6543
|
+
verdict: "every declared lifecycle hook has a timeout guard (manifest timeouts.<hook>, or impl-side cutoff), positive-control OK (Extension Rule 9)"
|
|
5546
6544
|
};
|
|
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
|
-
};
|
|
6545
|
+
})();
|
|
6546
|
+
return settle(this.id, decided, unreadable);
|
|
5554
6547
|
}
|
|
5555
6548
|
};
|
|
5556
6549
|
|
|
@@ -5579,7 +6572,7 @@ function unscopePackageName(name) {
|
|
|
5579
6572
|
const m = name.match(/^@[^/]+\/(.+)$/);
|
|
5580
6573
|
return m ? m[1] : name;
|
|
5581
6574
|
}
|
|
5582
|
-
var
|
|
6575
|
+
var dServer2Inner = {
|
|
5583
6576
|
id: "DServer2 naming",
|
|
5584
6577
|
rule: "C-2",
|
|
5585
6578
|
pin: "a74a042b53c71a095ab88b3073bd47728eeda814",
|
|
@@ -5607,6 +6600,22 @@ var dServer2 = {
|
|
|
5607
6600
|
};
|
|
5608
6601
|
}
|
|
5609
6602
|
const tools = findRegisteredTools(tree);
|
|
6603
|
+
if (tools.length === 0) {
|
|
6604
|
+
const surface = toolSurfaceAnchor(tree);
|
|
6605
|
+
if (surface) {
|
|
6606
|
+
const reason = noToolsLocatedReason(
|
|
6607
|
+
tree,
|
|
6608
|
+
"snake_case tool naming is unverifiable (Server Rule 2)"
|
|
6609
|
+
);
|
|
6610
|
+
return {
|
|
6611
|
+
id: this.id,
|
|
6612
|
+
anchor: surface,
|
|
6613
|
+
state: "could-not-judge",
|
|
6614
|
+
reason,
|
|
6615
|
+
verdict: `could not judge: ${reason}`
|
|
6616
|
+
};
|
|
6617
|
+
}
|
|
6618
|
+
}
|
|
5610
6619
|
for (const t of tools) {
|
|
5611
6620
|
if (!TOOL_SNAKE_RE.test(t.name)) {
|
|
5612
6621
|
return {
|
|
@@ -5710,6 +6719,9 @@ var dServer2 = {
|
|
|
5710
6719
|
};
|
|
5711
6720
|
}
|
|
5712
6721
|
};
|
|
6722
|
+
var dServer2 = guardUnreadablePackageJson(dServer2Inner, {
|
|
6723
|
+
authoritative: false
|
|
6724
|
+
});
|
|
5713
6725
|
|
|
5714
6726
|
// src/detectors/dServer6.ts
|
|
5715
6727
|
var LOG_OPENER_RE = /\b(?:console|logger|log)\.(?:log|info|warn|error|debug|trace)\s*\(/;
|
|
@@ -5820,65 +6832,72 @@ var dServer6 = {
|
|
|
5820
6832
|
`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
6833
|
);
|
|
5822
6834
|
}
|
|
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
|
-
};
|
|
6835
|
+
const unreadable = [];
|
|
6836
|
+
const decided = (() => {
|
|
6837
|
+
for (const file of tree.files) {
|
|
6838
|
+
const { calls, unbalanced } = collectLogCalls(file);
|
|
6839
|
+
for (const anchor of unbalanced) {
|
|
6840
|
+
unreadable.push({
|
|
6841
|
+
anchor,
|
|
6842
|
+
reason: `could not read the argument list of the log call at ${anchor} \u2014 parentheses never balance, secret/PII content unverifiable (C-6)`
|
|
6843
|
+
});
|
|
5852
6844
|
}
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
6845
|
+
for (const call of calls) {
|
|
6846
|
+
if (SPREAD_RE.test(call.argText)) {
|
|
6847
|
+
unreadable.push({
|
|
6848
|
+
anchor: call.anchor,
|
|
6849
|
+
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)`
|
|
6850
|
+
});
|
|
6851
|
+
continue;
|
|
6852
|
+
}
|
|
6853
|
+
const refs = referencedIdentifiers(call.argText);
|
|
6854
|
+
if (SECRET_RE.test(refs)) {
|
|
6855
|
+
return {
|
|
6856
|
+
id: this.id,
|
|
6857
|
+
anchor: call.anchor,
|
|
6858
|
+
state: "non-conforming",
|
|
6859
|
+
verdict: "Form A \u2014 log call references a secret-like identifier (positive-control OK) -> never log secrets; redact before logging (C-6)"
|
|
6860
|
+
};
|
|
6861
|
+
}
|
|
6862
|
+
if (PII_RE.test(refs)) {
|
|
6863
|
+
return {
|
|
6864
|
+
id: this.id,
|
|
6865
|
+
anchor: call.anchor,
|
|
6866
|
+
state: "non-conforming",
|
|
6867
|
+
verdict: "Form C \u2014 log call references a PII-shaped identifier (positive-control OK) -> never log PII; redact before logging (C-6)"
|
|
6868
|
+
};
|
|
6869
|
+
}
|
|
6870
|
+
if (INTERNAL_RE.test(refs)) {
|
|
6871
|
+
return {
|
|
6872
|
+
id: this.id,
|
|
6873
|
+
anchor: call.anchor,
|
|
6874
|
+
state: "non-conforming",
|
|
6875
|
+
verdict: "Form D \u2014 log call references an internal-infrastructure identifier (positive-control OK) -> never log internal system detail; redact before logging (C-6)"
|
|
6876
|
+
};
|
|
6877
|
+
}
|
|
5860
6878
|
}
|
|
5861
6879
|
}
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
}
|
|
6880
|
+
for (const file of tree.files) {
|
|
6881
|
+
for (const line of file.lines) {
|
|
6882
|
+
if (line.comment) continue;
|
|
6883
|
+
if (STACK_RE.test(line.raw) && CLIENT_SINK_RE.test(line.raw)) {
|
|
6884
|
+
return {
|
|
6885
|
+
id: this.id,
|
|
6886
|
+
anchor: `${file.rel}:${line.n}`,
|
|
6887
|
+
state: "non-conforming",
|
|
6888
|
+
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)"
|
|
6889
|
+
};
|
|
6890
|
+
}
|
|
5873
6891
|
}
|
|
5874
6892
|
}
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
};
|
|
6893
|
+
return {
|
|
6894
|
+
id: this.id,
|
|
6895
|
+
anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
|
|
6896
|
+
state: "present",
|
|
6897
|
+
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)."
|
|
6898
|
+
};
|
|
6899
|
+
})();
|
|
6900
|
+
return settle(this.id, decided, unreadable);
|
|
5882
6901
|
}
|
|
5883
6902
|
};
|
|
5884
6903
|
|
|
@@ -5960,59 +6979,65 @@ var dServer8 = {
|
|
|
5960
6979
|
`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
6980
|
);
|
|
5962
6981
|
}
|
|
5963
|
-
const
|
|
5964
|
-
|
|
5965
|
-
const
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
6982
|
+
const unreadable = [];
|
|
6983
|
+
const decided = (() => {
|
|
6984
|
+
const blocks = [];
|
|
6985
|
+
for (const file of tree.files) {
|
|
6986
|
+
const { blocks: fileBlocks, unbalanced } = collectContentArrays(file);
|
|
6987
|
+
for (const anchor of unbalanced) {
|
|
6988
|
+
unreadable.push({
|
|
6989
|
+
anchor,
|
|
6990
|
+
reason: `could not read the content[] array at ${anchor} \u2014 brackets never balance, a ui:// resource emission could hide inside it (Server Rule 8)`
|
|
6991
|
+
});
|
|
6992
|
+
}
|
|
6993
|
+
blocks.push(...fileBlocks);
|
|
6994
|
+
}
|
|
6995
|
+
for (const file of tree.files) {
|
|
6996
|
+
const fileBlocks = blocks.filter((b) => b.file === file);
|
|
6997
|
+
for (const line of file.lines) {
|
|
6998
|
+
if (line.comment) continue;
|
|
6999
|
+
if (!KIT_RE.test(line.raw)) continue;
|
|
7000
|
+
const inside = fileBlocks.some((b) => line.n >= b.startLine && line.n <= b.endLine);
|
|
7001
|
+
if (!inside) {
|
|
7002
|
+
unreadable.push({
|
|
7003
|
+
anchor: `${file.rel}:${line.n}`,
|
|
7004
|
+
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)`
|
|
7005
|
+
});
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
5970
7008
|
}
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
7009
|
+
let sawEmission = false;
|
|
7010
|
+
for (const block of blocks) {
|
|
7011
|
+
const inlineEmission = INLINE_RESOURCE_RE.test(block.text) && UI_URI_RE.test(block.text);
|
|
7012
|
+
const kitEmission = KIT_RE.test(block.text);
|
|
7013
|
+
if (!inlineEmission && !kitEmission) continue;
|
|
7014
|
+
sawEmission = true;
|
|
7015
|
+
const hasText = TEXT_RE.test(block.text) || MARKDOWN_RE.test(block.text);
|
|
7016
|
+
if (!hasText) {
|
|
7017
|
+
return {
|
|
7018
|
+
id: this.id,
|
|
7019
|
+
anchor: block.anchor,
|
|
7020
|
+
state: "non-conforming",
|
|
7021
|
+
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)`
|
|
7022
|
+
};
|
|
5983
7023
|
}
|
|
5984
7024
|
}
|
|
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) {
|
|
7025
|
+
if (!sawEmission) {
|
|
5994
7026
|
return {
|
|
5995
7027
|
id: this.id,
|
|
5996
|
-
anchor:
|
|
5997
|
-
state: "
|
|
5998
|
-
verdict:
|
|
7028
|
+
anchor: tree.files.length > 0 ? `${tree.files[0].rel}:1` : tree.root,
|
|
7029
|
+
state: "absent",
|
|
7030
|
+
verdict: "no ui:// resource emission in any content[] array (n/a for Server Rule 8)"
|
|
5999
7031
|
};
|
|
6000
7032
|
}
|
|
6001
|
-
}
|
|
6002
|
-
if (!sawEmission) {
|
|
6003
7033
|
return {
|
|
6004
7034
|
id: this.id,
|
|
6005
|
-
anchor:
|
|
6006
|
-
state: "
|
|
6007
|
-
verdict: "
|
|
7035
|
+
anchor: blocks.length > 0 ? blocks[0].anchor : tree.root,
|
|
7036
|
+
state: "present",
|
|
7037
|
+
verdict: "every ui:// resource emission has a sibling text/markdown fallback in the same content[] (positive-control OK, Server Rule 8)"
|
|
6008
7038
|
};
|
|
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
|
-
};
|
|
7039
|
+
})();
|
|
7040
|
+
return settle(this.id, decided, unreadable);
|
|
6016
7041
|
}
|
|
6017
7042
|
};
|
|
6018
7043
|
|
|
@@ -6236,22 +7261,23 @@ var detectors = [
|
|
|
6236
7261
|
dExt5,
|
|
6237
7262
|
dExt7,
|
|
6238
7263
|
dExt8,
|
|
6239
|
-
dExt9
|
|
7264
|
+
dExt9,
|
|
7265
|
+
localIdentity
|
|
6240
7266
|
];
|
|
6241
7267
|
|
|
6242
7268
|
// src/runtime/launch.ts
|
|
6243
7269
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6244
|
-
import { existsSync as existsSync15, readFileSync as
|
|
6245
|
-
import { dirname as dirname10, isAbsolute, join as
|
|
7270
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
|
|
7271
|
+
import { dirname as dirname10, isAbsolute, join as join18, resolve } from "path";
|
|
6246
7272
|
function findPackageDir(target) {
|
|
6247
7273
|
let dir;
|
|
6248
7274
|
try {
|
|
6249
|
-
dir =
|
|
7275
|
+
dir = statSync8(target).isDirectory() ? target : dirname10(target);
|
|
6250
7276
|
} catch {
|
|
6251
7277
|
return null;
|
|
6252
7278
|
}
|
|
6253
7279
|
for (let depth = 0; depth < 8; depth++) {
|
|
6254
|
-
if (existsSync15(
|
|
7280
|
+
if (existsSync15(join18(dir, "package.json"))) return dir;
|
|
6255
7281
|
const parent = dirname10(dir);
|
|
6256
7282
|
if (parent === dir) break;
|
|
6257
7283
|
dir = parent;
|
|
@@ -6276,10 +7302,10 @@ function deriveLaunchPlan(target) {
|
|
|
6276
7302
|
}
|
|
6277
7303
|
let pkg;
|
|
6278
7304
|
try {
|
|
6279
|
-
pkg = JSON.parse(
|
|
7305
|
+
pkg = JSON.parse(readFileSync11(join18(pkgDir, "package.json"), "utf8"));
|
|
6280
7306
|
} catch (err) {
|
|
6281
7307
|
return {
|
|
6282
|
-
error: `could not parse ${
|
|
7308
|
+
error: `could not parse ${join18(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
|
|
6283
7309
|
};
|
|
6284
7310
|
}
|
|
6285
7311
|
const bin = firstBinEntry(pkg.bin);
|
|
@@ -6357,10 +7383,10 @@ function readEvalCounts(target) {
|
|
|
6357
7383
|
if (!pkgDir) return counts;
|
|
6358
7384
|
let dir = pkgDir;
|
|
6359
7385
|
for (let depth = 0; depth < 8; depth++) {
|
|
6360
|
-
const candidate =
|
|
7386
|
+
const candidate = join18(dir, "evals", "evals.json");
|
|
6361
7387
|
if (existsSync15(candidate)) {
|
|
6362
7388
|
try {
|
|
6363
|
-
const parsed = JSON.parse(
|
|
7389
|
+
const parsed = JSON.parse(readFileSync11(candidate, "utf8"));
|
|
6364
7390
|
for (const c of parsed.cases ?? []) {
|
|
6365
7391
|
if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
|
|
6366
7392
|
}
|
|
@@ -6376,7 +7402,7 @@ function readEvalCounts(target) {
|
|
|
6376
7402
|
}
|
|
6377
7403
|
function statSyncSafe(p) {
|
|
6378
7404
|
try {
|
|
6379
|
-
return
|
|
7405
|
+
return statSync8(p).isDirectory();
|
|
6380
7406
|
} catch {
|
|
6381
7407
|
return false;
|
|
6382
7408
|
}
|
|
@@ -6495,6 +7521,9 @@ function reconcile(id, staticR, dynamicR) {
|
|
|
6495
7521
|
};
|
|
6496
7522
|
}
|
|
6497
7523
|
function runDoctor(target, detectorList = detectors, opts = {}) {
|
|
7524
|
+
return runDoctorReport(target, detectorList, opts).results;
|
|
7525
|
+
}
|
|
7526
|
+
function runDoctorReport(target, detectorList = detectors, opts = {}) {
|
|
6498
7527
|
const mode = opts.mode ?? "static";
|
|
6499
7528
|
const tree = loadTree(target);
|
|
6500
7529
|
const needsRuntime = mode !== "static" && detectorList.some((d) => typeof d.runDynamic === "function");
|
|
@@ -6529,7 +7558,7 @@ function runDoctor(target, detectorList = detectors, opts = {}) {
|
|
|
6529
7558
|
results.push(reconcile(d.id, staticR, dynamicR));
|
|
6530
7559
|
}
|
|
6531
7560
|
}
|
|
6532
|
-
return results;
|
|
7561
|
+
return { results, skipped: tree.skippedDirs };
|
|
6533
7562
|
}
|
|
6534
7563
|
async function invokeLiveChecked(d, session) {
|
|
6535
7564
|
if (!session) {
|
|
@@ -6584,14 +7613,32 @@ async function runDoctorAsync(target, opts = {}) {
|
|
|
6584
7613
|
}
|
|
6585
7614
|
return results;
|
|
6586
7615
|
}
|
|
6587
|
-
function
|
|
7616
|
+
function renderSkipped(skipped) {
|
|
7617
|
+
if (skipped.length === 0) {
|
|
7618
|
+
return "skipped directories (0) \u2014 none: no directory was skipped on this run.";
|
|
7619
|
+
}
|
|
7620
|
+
const sorted = [...skipped].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
7621
|
+
const head = `skipped directories (${sorted.length}) \u2014 NOT read on this run; a verdict above says nothing about what lies inside:`;
|
|
7622
|
+
const rows = sorted.map(
|
|
7623
|
+
(d) => `${d.path} | ${d.reason}${d.candidate ? ` | holds a deployment configuration: ${d.candidate}` : ""}`
|
|
7624
|
+
);
|
|
7625
|
+
return [head, "-".repeat(head.length), ...rows].join("\n");
|
|
7626
|
+
}
|
|
7627
|
+
function renderTable(surface, results, skipped) {
|
|
7628
|
+
const body = renderResults(surface, results);
|
|
7629
|
+
const section = skipped === void 0 ? "skipped directories: NOT REPORTED \u2014 the caller did not pass this run's skipped list (use runDoctorReport), so the scope above may be narrower than it reads." : renderSkipped(skipped);
|
|
7630
|
+
return `${body}
|
|
7631
|
+
|
|
7632
|
+
${section}`;
|
|
7633
|
+
}
|
|
7634
|
+
function renderResults(surface, results) {
|
|
6588
7635
|
const cols = { item: 24, anchor: 26, state: 15 };
|
|
6589
7636
|
const header = `${pad("item", cols.item)} | ${pad("file:line", cols.anchor)} | ${pad("state", cols.state)} | verdict`;
|
|
6590
|
-
const
|
|
7637
|
+
const sep3 = "-".repeat(header.length);
|
|
6591
7638
|
const rows = results.map(
|
|
6592
7639
|
(r) => `${pad(r.id, cols.item)} | ${pad(r.anchor, cols.anchor)} | ${pad(r.state, cols.state)} | ${r.verdict}`
|
|
6593
7640
|
);
|
|
6594
|
-
const table = [`surface: ${surface}`, header,
|
|
7641
|
+
const table = [`surface: ${surface}`, header, sep3, ...rows].join("\n");
|
|
6595
7642
|
const unjudged = results.filter((r) => r.state === "could-not-judge");
|
|
6596
7643
|
if (unjudged.length === 0) return table;
|
|
6597
7644
|
const cnjHeader = `could-not-judge (${unjudged.length}) \u2014 the doctor could NOT read these; this is NOT a clean bill:`;
|
|
@@ -6612,6 +7659,9 @@ var USAGE = [
|
|
|
6612
7659
|
" --mode=static read the tree only (default)",
|
|
6613
7660
|
" --mode=dynamic launch the server and judge what it serves",
|
|
6614
7661
|
" --mode=both both, dynamic findings taking precedence",
|
|
7662
|
+
" --local-identity-ceiling=N",
|
|
7663
|
+
" local identity primitives tolerated before DLocalIdentity",
|
|
7664
|
+
" fails (default 0)",
|
|
6615
7665
|
" --version print the version and exit",
|
|
6616
7666
|
" --help print this and exit",
|
|
6617
7667
|
"",
|
|
@@ -6620,7 +7670,7 @@ var USAGE = [
|
|
|
6620
7670
|
function readVersion() {
|
|
6621
7671
|
try {
|
|
6622
7672
|
const here = dirname11(fileURLToPath(import.meta.url));
|
|
6623
|
-
const manifest = JSON.parse(
|
|
7673
|
+
const manifest = JSON.parse(readFileSync12(join19(here, "..", "package.json"), "utf8"));
|
|
6624
7674
|
return manifest.version ?? null;
|
|
6625
7675
|
} catch {
|
|
6626
7676
|
return null;
|
|
@@ -6652,8 +7702,20 @@ function main() {
|
|
|
6652
7702
|
console.error(`unknown --mode "${mode}" \u2014 expected static|dynamic|both`);
|
|
6653
7703
|
process.exit(1);
|
|
6654
7704
|
}
|
|
6655
|
-
const
|
|
6656
|
-
|
|
7705
|
+
const ceilingArg = args.find((a) => a.startsWith("--local-identity-ceiling="));
|
|
7706
|
+
let detectorList = detectors;
|
|
7707
|
+
if (ceilingArg) {
|
|
7708
|
+
const raw = ceilingArg.slice("--local-identity-ceiling=".length);
|
|
7709
|
+
const ceiling = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
|
|
7710
|
+
if (!Number.isInteger(ceiling)) {
|
|
7711
|
+
console.error(`invalid --local-identity-ceiling "${raw}" \u2014 expected a non-negative integer`);
|
|
7712
|
+
process.exit(1);
|
|
7713
|
+
}
|
|
7714
|
+
const ratchet = makeLocalIdentity({ ceiling });
|
|
7715
|
+
detectorList = detectors.map((d) => d === localIdentity ? ratchet : d);
|
|
7716
|
+
}
|
|
7717
|
+
const { results, skipped } = runDoctorReport(target, detectorList, { mode });
|
|
7718
|
+
console.log(renderTable(target, results, skipped));
|
|
6657
7719
|
process.exitCode = computeExitCode(results);
|
|
6658
7720
|
}
|
|
6659
7721
|
function isDirectInvocation() {
|
|
@@ -6675,7 +7737,9 @@ if (isDirectInvocation()) main();
|
|
|
6675
7737
|
export {
|
|
6676
7738
|
computeExitCode,
|
|
6677
7739
|
isLiveDetector,
|
|
7740
|
+
renderSkipped,
|
|
6678
7741
|
renderTable,
|
|
6679
7742
|
runDoctor,
|
|
6680
|
-
runDoctorAsync
|
|
7743
|
+
runDoctorAsync,
|
|
7744
|
+
runDoctorReport
|
|
6681
7745
|
};
|