@vantageos/mcp-doctor 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/README.md +31 -2
- package/dist/doctor.d.ts +70 -1
- package/dist/doctor.js +1689 -479
- package/package.json +6 -4
package/dist/doctor.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/doctor.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync16, realpathSync } from "fs";
|
|
5
5
|
import { dirname as dirname17, join as join27, resolve as resolve3 } from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/scan.ts
|
|
9
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
9
|
+
import { constants, accessSync, existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
10
10
|
import { basename, dirname, join, relative, sep } from "path";
|
|
11
11
|
var SKIP_DIR_INFO = {
|
|
12
12
|
node_modules: { reason: "vendored dependencies", authored: false },
|
|
@@ -46,11 +46,12 @@ function isCommentLine(raw) {
|
|
|
46
46
|
}
|
|
47
47
|
var UNSCANNED_RE = /\.(?:m?js|cjs|jsx|mts|cts)$/;
|
|
48
48
|
var NOT_PRODUCT_NAME_RE = /\.d\.[mc]?ts$|\.(?:test|spec)\.[mc]?[jt]sx?$|\.config\.[mc]?[jt]s$|\.min\./;
|
|
49
|
-
function walk(dir, out, onSkip, unscanned = []) {
|
|
49
|
+
function walk(dir, out, onSkip, unscanned = [], seen = { files: 0 }) {
|
|
50
50
|
let entries;
|
|
51
51
|
try {
|
|
52
52
|
entries = readdirSync(dir);
|
|
53
|
-
} catch {
|
|
53
|
+
} catch (err) {
|
|
54
|
+
onSkip(dir, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
57
|
for (const name of entries) {
|
|
@@ -58,7 +59,8 @@ function walk(dir, out, onSkip, unscanned = []) {
|
|
|
58
59
|
let st;
|
|
59
60
|
try {
|
|
60
61
|
st = statSync(full);
|
|
61
|
-
} catch {
|
|
62
|
+
} catch (err) {
|
|
63
|
+
onSkip(full, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
62
64
|
continue;
|
|
63
65
|
}
|
|
64
66
|
if (st.isDirectory()) {
|
|
@@ -66,8 +68,21 @@ function walk(dir, out, onSkip, unscanned = []) {
|
|
|
66
68
|
onSkip(full);
|
|
67
69
|
continue;
|
|
68
70
|
}
|
|
69
|
-
walk(full, out, onSkip, unscanned);
|
|
70
|
-
|
|
71
|
+
walk(full, out, onSkip, unscanned, seen);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!st.isFile()) {
|
|
75
|
+
onSkip(full, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
accessSync(full, constants.R_OK);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
onSkip(full, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
seen.files++;
|
|
85
|
+
if (UNSCANNED_RE.test(name)) {
|
|
71
86
|
if (!NOT_PRODUCT_NAME_RE.test(name)) unscanned.push(full);
|
|
72
87
|
} else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
|
|
73
88
|
if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
|
|
@@ -75,6 +90,22 @@ function walk(dir, out, onSkip, unscanned = []) {
|
|
|
75
90
|
}
|
|
76
91
|
}
|
|
77
92
|
}
|
|
93
|
+
function fsErrorCode(err) {
|
|
94
|
+
const code = err?.code;
|
|
95
|
+
return typeof code === "string" ? code : String(err);
|
|
96
|
+
}
|
|
97
|
+
var SubjectUnreadableError = class extends Error {
|
|
98
|
+
target;
|
|
99
|
+
form;
|
|
100
|
+
constructor(target, form) {
|
|
101
|
+
super(
|
|
102
|
+
`mcp-doctor: could not judge ${target} \u2014 ${form}. Nothing was read, so no verdict is given (exit 2).`
|
|
103
|
+
);
|
|
104
|
+
this.name = "SubjectUnreadableError";
|
|
105
|
+
this.target = target;
|
|
106
|
+
this.form = form;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
78
109
|
function loadFile(path, root) {
|
|
79
110
|
const raw = readFileSync(path, "utf8");
|
|
80
111
|
const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
|
|
@@ -103,6 +134,9 @@ function findPackageJson(startDir) {
|
|
|
103
134
|
const candidate = join(dir, "package.json");
|
|
104
135
|
if (existsSync(candidate)) {
|
|
105
136
|
try {
|
|
137
|
+
if (!statSync(candidate).isFile()) {
|
|
138
|
+
return { path: candidate, json: {}, unreadable: "not a regular file" };
|
|
139
|
+
}
|
|
106
140
|
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
107
141
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
108
142
|
return { path: candidate, json: {}, unreadable: "not a JSON object" };
|
|
@@ -119,16 +153,67 @@ function findPackageJson(startDir) {
|
|
|
119
153
|
return null;
|
|
120
154
|
}
|
|
121
155
|
function loadTree(root, backendRoot) {
|
|
122
|
-
|
|
156
|
+
let st;
|
|
157
|
+
try {
|
|
158
|
+
st = statSync(root);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const code = fsErrorCode(err);
|
|
161
|
+
throw new SubjectUnreadableError(
|
|
162
|
+
root,
|
|
163
|
+
code === "ENOENT" ? "the path does not exist" : `the path cannot be read (${code})`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
123
166
|
const baseDir = st.isDirectory() ? root : dirname(root);
|
|
124
167
|
const filePaths = [];
|
|
125
168
|
const tree = { root, files: [], packageJson: null, skippedDirs: [] };
|
|
126
169
|
if (backendRoot !== void 0) tree.backendRoot = backendRoot;
|
|
127
170
|
const unscanned = [];
|
|
128
|
-
if (st.isDirectory())
|
|
129
|
-
|
|
171
|
+
if (st.isDirectory()) {
|
|
172
|
+
let entries;
|
|
173
|
+
try {
|
|
174
|
+
entries = readdirSync(root);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
throw new SubjectUnreadableError(
|
|
177
|
+
root,
|
|
178
|
+
`the directory cannot be listed (${fsErrorCode(err)})`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (entries.length === 0) {
|
|
182
|
+
throw new SubjectUnreadableError(root, "the directory is empty (0 entries)");
|
|
183
|
+
}
|
|
184
|
+
const seen = { files: 0 };
|
|
185
|
+
walk(root, filePaths, (d, reason) => recordSkip(tree, d, root, reason), unscanned, seen);
|
|
186
|
+
for (const p of filePaths) {
|
|
187
|
+
try {
|
|
188
|
+
tree.files.push(loadFile(p, baseDir));
|
|
189
|
+
} catch (err) {
|
|
190
|
+
recordSkip(tree, p, root, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
191
|
+
seen.files--;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (seen.files === 0) {
|
|
195
|
+
const passed = tree.skippedDirs.map((d) => `${d.path} (${d.reason})`).join(", ");
|
|
196
|
+
throw new SubjectUnreadableError(
|
|
197
|
+
root,
|
|
198
|
+
`the walk reached no readable file (${entries.length} entr${entries.length === 1 ? "y" : "ies"}, none a readable file${passed ? `; passed over: ${passed}` : ""})`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
} else if (st.isFile()) {
|
|
202
|
+
if (!root.endsWith(".ts") && !root.endsWith(".tsx")) {
|
|
203
|
+
throw new SubjectUnreadableError(
|
|
204
|
+
root,
|
|
205
|
+
"a file the scan does not read: a target is a source directory or a single .ts/.tsx file"
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
tree.files.push(loadFile(root, baseDir));
|
|
210
|
+
} catch (err) {
|
|
211
|
+
throw new SubjectUnreadableError(root, `the file cannot be read (${fsErrorCode(err)})`);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
throw new SubjectUnreadableError(root, "neither a directory nor a regular file");
|
|
215
|
+
}
|
|
130
216
|
tree.unscannedSource = unscanned.map((p) => relative(baseDir, p).split(sep).join("/"));
|
|
131
|
-
tree.files = filePaths.map((p) => loadFile(p, baseDir));
|
|
132
217
|
tree.packageJson = findPackageJson(baseDir);
|
|
133
218
|
return tree;
|
|
134
219
|
}
|
|
@@ -423,7 +508,7 @@ function countInCode(file, line, ch) {
|
|
|
423
508
|
}
|
|
424
509
|
|
|
425
510
|
// src/detectors/couldNotJudge.ts
|
|
426
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
511
|
+
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
427
512
|
function couldNotJudge(id, anchor, reason, owedVerdict) {
|
|
428
513
|
return {
|
|
429
514
|
id,
|
|
@@ -451,6 +536,7 @@ function settle(id, decided, unreadable) {
|
|
|
451
536
|
function readJsonObject(path) {
|
|
452
537
|
let value;
|
|
453
538
|
try {
|
|
539
|
+
if (!statSync2(path).isFile()) return null;
|
|
454
540
|
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
455
541
|
} catch {
|
|
456
542
|
return null;
|
|
@@ -477,8 +563,194 @@ function guardUnreadablePackageJson(inner, opts) {
|
|
|
477
563
|
};
|
|
478
564
|
}
|
|
479
565
|
|
|
566
|
+
// src/detectors/siteScan.ts
|
|
567
|
+
import { constants as constants2, accessSync as accessSync2, existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
568
|
+
import { join as join2, relative as relative2, sep as sep2 } from "path";
|
|
569
|
+
function codeSites(tree, re) {
|
|
570
|
+
const out = [];
|
|
571
|
+
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
|
|
572
|
+
for (const file of tree.files) {
|
|
573
|
+
for (const line of file.lines) {
|
|
574
|
+
if (line.comment) continue;
|
|
575
|
+
for (const m of line.raw.matchAll(g)) {
|
|
576
|
+
const col = m.index ?? 0;
|
|
577
|
+
if (isCodeAt(file, line.n, col)) {
|
|
578
|
+
out.push({ file, line, col, match: m, anchor: `${file.rel}:${line.n}` });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
var uncommentedCache = /* @__PURE__ */ new WeakMap();
|
|
586
|
+
function uncommentedLines(file) {
|
|
587
|
+
const hit = uncommentedCache.get(file);
|
|
588
|
+
if (hit) return hit;
|
|
589
|
+
const rows = file.lines.map((l) => {
|
|
590
|
+
let s = "";
|
|
591
|
+
for (let c = 0; c < l.raw.length; c++) {
|
|
592
|
+
s += l.comment || isCommentAt(file, l.n, c) ? " " : l.raw[c];
|
|
593
|
+
}
|
|
594
|
+
return s;
|
|
595
|
+
});
|
|
596
|
+
uncommentedCache.set(file, rows);
|
|
597
|
+
return rows;
|
|
598
|
+
}
|
|
599
|
+
function balancedBlock(file, line, col, open, close) {
|
|
600
|
+
const rows = uncommentedLines(file);
|
|
601
|
+
let depth = 0;
|
|
602
|
+
let text = "";
|
|
603
|
+
for (let n = line; n <= file.lines.length; n++) {
|
|
604
|
+
const raw = rows[n - 1];
|
|
605
|
+
const from = n === line ? col : 0;
|
|
606
|
+
for (let c = from; c < raw.length; c++) {
|
|
607
|
+
const ch = raw[c];
|
|
608
|
+
if (ch === open && isCodeAt(file, n, c)) depth++;
|
|
609
|
+
else if (ch === close && isCodeAt(file, n, c)) depth--;
|
|
610
|
+
text += ch;
|
|
611
|
+
if (depth === 0 && ch === close) {
|
|
612
|
+
return { startLine: line, endLine: n, text };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
text += "\n";
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
function nextOpener(file, line, col, openers = "{[(", maxLines = 3) {
|
|
620
|
+
const rows = uncommentedLines(file);
|
|
621
|
+
for (let n = line; n <= Math.min(file.lines.length, line + maxLines); n++) {
|
|
622
|
+
const raw = rows[n - 1];
|
|
623
|
+
for (let c = n === line ? col : 0; c < raw.length; c++) {
|
|
624
|
+
if (openers.includes(raw[c]) && isCodeAt(file, n, c)) return { line: n, col: c, ch: raw[c] };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
function isRecord(v) {
|
|
630
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
631
|
+
}
|
|
632
|
+
function baseOf(tree) {
|
|
633
|
+
try {
|
|
634
|
+
return statSync3(tree.root).isDirectory() ? tree.root : join2(tree.root, "..");
|
|
635
|
+
} catch {
|
|
636
|
+
return tree.root;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function findDiskFiles(tree, pred) {
|
|
640
|
+
const out = [];
|
|
641
|
+
const base = baseOf(tree);
|
|
642
|
+
const walk2 = (dir) => {
|
|
643
|
+
let entries;
|
|
644
|
+
try {
|
|
645
|
+
entries = readdirSync2(dir);
|
|
646
|
+
} catch (err) {
|
|
647
|
+
recordSkip(tree, dir, base, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
for (const name of entries.sort()) {
|
|
651
|
+
const full = join2(dir, name);
|
|
652
|
+
let st;
|
|
653
|
+
try {
|
|
654
|
+
st = statSync3(full);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
recordSkip(tree, full, base, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (st.isDirectory()) {
|
|
660
|
+
if (SKIP_DIRS.has(name)) {
|
|
661
|
+
recordSkip(tree, full, base);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
walk2(full);
|
|
665
|
+
} else {
|
|
666
|
+
const rel = relative2(base, full).split(sep2).join("/");
|
|
667
|
+
if (!pred(name, rel)) continue;
|
|
668
|
+
if (!st.isFile()) {
|
|
669
|
+
recordSkip(tree, full, base, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
accessSync2(full, constants2.R_OK);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
recordSkip(tree, full, base, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
out.push({ path: full, rel, name });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
walk2(base);
|
|
683
|
+
return out;
|
|
684
|
+
}
|
|
685
|
+
function readText(path) {
|
|
686
|
+
try {
|
|
687
|
+
if (!statSync3(path).isFile()) return null;
|
|
688
|
+
return readFileSync3(path, "utf8");
|
|
689
|
+
} catch {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function readJsonRecord(path) {
|
|
694
|
+
const text = readText(path);
|
|
695
|
+
if (text === null) return null;
|
|
696
|
+
try {
|
|
697
|
+
const v = JSON.parse(text);
|
|
698
|
+
return isRecord(v) ? v : null;
|
|
699
|
+
} catch {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function proseWordCount(markdown) {
|
|
704
|
+
const noFences = markdown.replace(/^(```|~~~)[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, " ");
|
|
705
|
+
const noInline = noFences.replace(/`[^`\n]*`/g, " ");
|
|
706
|
+
return (noInline.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) ?? []).length;
|
|
707
|
+
}
|
|
708
|
+
function lineHits(tree, re) {
|
|
709
|
+
const out = [];
|
|
710
|
+
for (const file of tree.files) {
|
|
711
|
+
const rows = uncommentedLines(file);
|
|
712
|
+
rows.forEach((text, i) => {
|
|
713
|
+
if (re.test(text)) {
|
|
714
|
+
out.push({ file, line: i + 1, anchor: `${file.rel}:${i + 1}`, text });
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
function enclosingBlock(file, line, col, levels = 0) {
|
|
721
|
+
const rows = uncommentedLines(file);
|
|
722
|
+
let curLine = line;
|
|
723
|
+
let curCol = col;
|
|
724
|
+
let result = null;
|
|
725
|
+
for (let level = 0; level <= levels; level++) {
|
|
726
|
+
let depth = 0;
|
|
727
|
+
let found = null;
|
|
728
|
+
outer: for (let n = curLine; n >= 1; n--) {
|
|
729
|
+
const raw = rows[n - 1];
|
|
730
|
+
for (let c = (n === curLine ? curCol : raw.length) - 1; c >= 0; c--) {
|
|
731
|
+
if (!isCodeAt(file, n, c)) continue;
|
|
732
|
+
if (raw[c] === "}") depth++;
|
|
733
|
+
else if (raw[c] === "{") {
|
|
734
|
+
if (depth === 0) {
|
|
735
|
+
found = { l: n, c };
|
|
736
|
+
break outer;
|
|
737
|
+
}
|
|
738
|
+
depth--;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (!found) return result;
|
|
743
|
+
const b = balancedBlock(file, found.l, found.c, "{", "}");
|
|
744
|
+
if (!b) return null;
|
|
745
|
+
result = { ...b, openLine: found.l, openCol: found.c };
|
|
746
|
+
curLine = found.l;
|
|
747
|
+
curCol = found.c;
|
|
748
|
+
}
|
|
749
|
+
return result;
|
|
750
|
+
}
|
|
751
|
+
|
|
480
752
|
// src/detectors/standardPin.ts
|
|
481
|
-
var STANDARD_PIN = "
|
|
753
|
+
var STANDARD_PIN = "6d463a1ecc5256cd59dd1afe5aa5e335accacc90";
|
|
482
754
|
|
|
483
755
|
// src/detectors/dLocalIdentity.ts
|
|
484
756
|
var CLOUD_IDENTITY_PACKAGE = "@vantageos/cloud-identity";
|
|
@@ -601,21 +873,155 @@ function literalHoldsIdentity(m) {
|
|
|
601
873
|
const arg = unfold(m[2]);
|
|
602
874
|
return m[1].split(",").map((e) => unfold(e.trim())).some((e) => e !== "" && !isLiteral(e) && !isLiteral(arg) && comparesIdentity(e, arg));
|
|
603
875
|
}
|
|
604
|
-
function
|
|
876
|
+
function scopesOf(ctx) {
|
|
877
|
+
const out = [];
|
|
878
|
+
for (let level = 0; level < 6; level++) {
|
|
879
|
+
const b = enclosingBlock(ctx.file, ctx.line.n, ctx.idx, level);
|
|
880
|
+
if (!b || out.length > 0 && b.text === out[out.length - 1]) break;
|
|
881
|
+
out.push(b.text);
|
|
882
|
+
}
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
var escRe = (x) => x.replace(/[$]/g, "\\$&");
|
|
886
|
+
function exprUntilSemicolon(text, from) {
|
|
887
|
+
let depth = 0;
|
|
888
|
+
for (let i = from; i < text.length; i++) {
|
|
889
|
+
const c = text[i];
|
|
890
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
891
|
+
for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
895
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
896
|
+
if (depth === 0) return text.slice(from, i).trim();
|
|
897
|
+
depth--;
|
|
898
|
+
} else if (c === ";" && depth === 0) return text.slice(from, i).trim();
|
|
899
|
+
}
|
|
900
|
+
return text.slice(from).trim();
|
|
901
|
+
}
|
|
902
|
+
function bindingOf(name, scopes) {
|
|
903
|
+
const n = escRe(name);
|
|
904
|
+
for (const text of scopes) {
|
|
905
|
+
const decl = new RegExp(String.raw`\b(?:const|let|var)\s+${n}\s*(?::[^=;]+)?=\s*`).exec(text);
|
|
906
|
+
if (decl) return { kind: "value", expr: exprUntilSemicolon(text, decl.index + decl[0].length) };
|
|
907
|
+
const loop = new RegExp(String.raw`\bfor\s*\(\s*(?:const|let|var)\s+${n}\s+of\s+`).exec(text);
|
|
908
|
+
if (loop) {
|
|
909
|
+
const inner = exprUntilSemicolon(text, loop.index + loop[0].length);
|
|
910
|
+
return { kind: "element", expr: inner };
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
function functionBody(file, name) {
|
|
916
|
+
const text = uncommentedLines(file).join("\n");
|
|
917
|
+
const decl = new RegExp(String.raw`\bfunction\s+${escRe(name)}\s*\(`).exec(text);
|
|
918
|
+
if (!decl) return null;
|
|
919
|
+
const params = callArgs(text, decl.index + decl[0].length);
|
|
920
|
+
if (!params) return null;
|
|
921
|
+
return blockFrom(text, params.end);
|
|
922
|
+
}
|
|
923
|
+
function isLocalListing(e, ctx, scopes, depth) {
|
|
924
|
+
if (depth > 4) return false;
|
|
925
|
+
const x = e.trim().replace(/^await\s+/, "");
|
|
926
|
+
if (/^(?:[\w$]+\s*\.\s*)*(?:readdirSync|readdir)\s*\(/.test(x)) return true;
|
|
927
|
+
const call = x.match(/^([A-Za-z_$][\w$]*)\s*\(/);
|
|
928
|
+
if (call) {
|
|
929
|
+
const body = functionBody(ctx.file, call[1]);
|
|
930
|
+
return body !== null && /\b(?:readdirSync|readdir)\s*\(/.test(body);
|
|
931
|
+
}
|
|
932
|
+
if (/^[A-Za-z_$][\w$]*$/.test(x)) {
|
|
933
|
+
const b = bindingOf(x, scopes);
|
|
934
|
+
return b !== null && b.kind === "value" && isLocalListing(b.expr, ctx, scopes, depth + 1);
|
|
935
|
+
}
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
function templateParts(e) {
|
|
939
|
+
if (!/^`[\s\S]*`$/.test(e)) return null;
|
|
940
|
+
const parts = [];
|
|
941
|
+
for (let i = 1; i < e.length - 1; i++) {
|
|
942
|
+
if (e[i] === "\\") {
|
|
943
|
+
i++;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (e[i] === "$" && e[i + 1] === "{") {
|
|
947
|
+
let depth = 0;
|
|
948
|
+
let j = i + 1;
|
|
949
|
+
for (; j < e.length; j++) {
|
|
950
|
+
if (e[j] === "{") depth++;
|
|
951
|
+
else if (e[j] === "}" && --depth === 0) break;
|
|
952
|
+
}
|
|
953
|
+
parts.push(e.slice(i + 2, j));
|
|
954
|
+
i = j;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return parts;
|
|
958
|
+
}
|
|
959
|
+
function isLocalContent(e, ctx, scopes, depth) {
|
|
960
|
+
if (depth > 4) return false;
|
|
961
|
+
const x = e.trim().replace(/^await\s+/, "");
|
|
962
|
+
if (isStaticInput(x, ctx.statics)) return true;
|
|
963
|
+
const fsRead = x.match(/^(?:[\w$]+\s*\.\s*)*(?:readFileSync|readFile)\s*\(/);
|
|
964
|
+
if (fsRead) {
|
|
965
|
+
const a = callArgs(x, fsRead[0].length);
|
|
966
|
+
return a !== null && a.end === x.length;
|
|
967
|
+
}
|
|
968
|
+
const wrapped = x.match(/^(?:new\s+TextEncoder\s*\(\s*\)\s*\.\s*encode|Buffer\s*\.\s*from)\s*\(/);
|
|
969
|
+
if (wrapped) {
|
|
970
|
+
const a = callArgs(x, wrapped[0].length);
|
|
971
|
+
return a !== null && a.end === x.length && isLocalContent(a.args[0], ctx, scopes, depth + 1);
|
|
972
|
+
}
|
|
973
|
+
const tpl = templateParts(x);
|
|
974
|
+
if (tpl) return tpl.every((p) => isLocalContent(p, ctx, scopes, depth + 1));
|
|
975
|
+
const ch = x.match(/^(?:[\w$]+\s*\.\s*)?createHash\s*\(/);
|
|
976
|
+
if (ch) {
|
|
977
|
+
const c = callArgs(x, ch[0].length);
|
|
978
|
+
const up = c ? x.slice(c.end).match(/^\s*\.\s*update\s*\(/) : null;
|
|
979
|
+
const d = c && up ? callArgs(x, c.end + up[0].length) : null;
|
|
980
|
+
return d !== null && isLocalContent(d.args[0], ctx, scopes, depth + 1);
|
|
981
|
+
}
|
|
982
|
+
const sd = x.match(/^(?:[\w$]+\s*\.\s*)*subtle\s*\.\s*digest\s*\(/);
|
|
983
|
+
if (sd) {
|
|
984
|
+
const c = callArgs(x, sd[0].length);
|
|
985
|
+
return c !== null && c.args.length >= 2 && isLocalContent(c.args[1], ctx, scopes, depth + 1);
|
|
986
|
+
}
|
|
987
|
+
if (/^[A-Za-z_$][\w$]*$/.test(x)) {
|
|
988
|
+
const b = bindingOf(x, scopes);
|
|
989
|
+
if (!b) return false;
|
|
990
|
+
return b.kind === "element" ? isLocalListing(b.expr, ctx, scopes, depth + 1) : isLocalContent(b.expr, ctx, scopes, depth + 1);
|
|
991
|
+
}
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
function digestsNonCredentialInput(m, ctx) {
|
|
605
995
|
if (!ctx) return false;
|
|
606
996
|
const text = textFrom(ctx);
|
|
607
997
|
const open = text.indexOf("(");
|
|
608
998
|
if (open < 0) return false;
|
|
609
999
|
const call = callArgs(text, open + 1);
|
|
610
1000
|
if (!call) return false;
|
|
1001
|
+
const scopes = scopesOf(ctx);
|
|
1002
|
+
const local = (e) => isLocalContent(e, ctx, scopes, 0);
|
|
611
1003
|
if (/^subtle/.test(m[0])) {
|
|
612
|
-
return m[1] === "digest" && call.args.length >= 2 &&
|
|
1004
|
+
return m[1] === "digest" && call.args.length >= 2 && local(call.args[1]);
|
|
613
1005
|
}
|
|
614
1006
|
if (!/^createHash/.test(m[0])) return false;
|
|
615
1007
|
const update = text.slice(call.end).match(/^\s*\.\s*update\s*\(/);
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
1008
|
+
if (update) {
|
|
1009
|
+
const data = callArgs(text, call.end + update[0].length);
|
|
1010
|
+
return data !== null && local(data.args[0]);
|
|
1011
|
+
}
|
|
1012
|
+
const held = ctx.line.raw.slice(0, ctx.idx).match(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*$/);
|
|
1013
|
+
if (!held || scopes.length === 0) return false;
|
|
1014
|
+
const v = escRe(held[1]);
|
|
1015
|
+
const scope = scopes[0];
|
|
1016
|
+
const updates = [...scope.matchAll(new RegExp(String.raw`\b${v}\s*\.\s*update\s*\(`, "g"))];
|
|
1017
|
+
if (updates.length === 0) return false;
|
|
1018
|
+
const digests = [...scope.matchAll(new RegExp(String.raw`\b${v}\s*\.\s*digest\s*\(`, "g"))];
|
|
1019
|
+
const uses = [...scope.matchAll(new RegExp(String.raw`(?<![\w$.])${v}(?![\w$])`, "g"))];
|
|
1020
|
+
if (uses.length !== 1 + updates.length + digests.length) return false;
|
|
1021
|
+
return updates.every((u) => {
|
|
1022
|
+
const a = callArgs(scope, (u.index ?? 0) + u[0].length);
|
|
1023
|
+
return a !== null && local(a.args[0]);
|
|
1024
|
+
});
|
|
619
1025
|
}
|
|
620
1026
|
var RENAMEABLE = [
|
|
621
1027
|
"createHash",
|
|
@@ -856,10 +1262,11 @@ var MATCHERS = [
|
|
|
856
1262
|
// Any digest of a presented credential, whatever the algorithm.
|
|
857
1263
|
id: "local-digest",
|
|
858
1264
|
category: "validates-credential",
|
|
859
|
-
// A digest over static content (a literal, or a constant made of literals)
|
|
860
|
-
//
|
|
1265
|
+
// A digest over static content (a literal, or a constant made of literals) or over local
|
|
1266
|
+
// content (file bytes, a listing, a digest of them: a content fingerprint of the tree) is not a
|
|
1267
|
+
// digest of a credential. HMAC and sign/verify always take a key: a site.
|
|
861
1268
|
re: /\bcreateHash\s*\(|\bcreateHmac\s*\(|\bsubtle\s*\.\s*(digest|sign|verify)\s*\(/,
|
|
862
|
-
accept: (m, ctx) => !
|
|
1269
|
+
accept: (m, ctx) => !digestsNonCredentialInput(m, ctx),
|
|
863
1270
|
sample: 'const h = await crypto.subtle.digest("SHA-256", data);'
|
|
864
1271
|
},
|
|
865
1272
|
// ── Minting / signing: shown, not counted (category "mints-token") ──────────
|
|
@@ -1188,178 +1595,6 @@ function makeLocalIdentity(opts = {}) {
|
|
|
1188
1595
|
}
|
|
1189
1596
|
var localIdentity = makeLocalIdentity();
|
|
1190
1597
|
|
|
1191
|
-
// src/detectors/siteScan.ts
|
|
1192
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1193
|
-
import { join as join2, relative as relative2, sep as sep2 } from "path";
|
|
1194
|
-
function codeSites(tree, re) {
|
|
1195
|
-
const out = [];
|
|
1196
|
-
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
|
|
1197
|
-
for (const file of tree.files) {
|
|
1198
|
-
for (const line of file.lines) {
|
|
1199
|
-
if (line.comment) continue;
|
|
1200
|
-
for (const m of line.raw.matchAll(g)) {
|
|
1201
|
-
const col = m.index ?? 0;
|
|
1202
|
-
if (isCodeAt(file, line.n, col)) {
|
|
1203
|
-
out.push({ file, line, col, match: m, anchor: `${file.rel}:${line.n}` });
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
return out;
|
|
1209
|
-
}
|
|
1210
|
-
var uncommentedCache = /* @__PURE__ */ new WeakMap();
|
|
1211
|
-
function uncommentedLines(file) {
|
|
1212
|
-
const hit = uncommentedCache.get(file);
|
|
1213
|
-
if (hit) return hit;
|
|
1214
|
-
const rows = file.lines.map((l) => {
|
|
1215
|
-
let s = "";
|
|
1216
|
-
for (let c = 0; c < l.raw.length; c++) {
|
|
1217
|
-
s += l.comment || isCommentAt(file, l.n, c) ? " " : l.raw[c];
|
|
1218
|
-
}
|
|
1219
|
-
return s;
|
|
1220
|
-
});
|
|
1221
|
-
uncommentedCache.set(file, rows);
|
|
1222
|
-
return rows;
|
|
1223
|
-
}
|
|
1224
|
-
function balancedBlock(file, line, col, open, close) {
|
|
1225
|
-
const rows = uncommentedLines(file);
|
|
1226
|
-
let depth = 0;
|
|
1227
|
-
let text = "";
|
|
1228
|
-
for (let n = line; n <= file.lines.length; n++) {
|
|
1229
|
-
const raw = rows[n - 1];
|
|
1230
|
-
const from = n === line ? col : 0;
|
|
1231
|
-
for (let c = from; c < raw.length; c++) {
|
|
1232
|
-
const ch = raw[c];
|
|
1233
|
-
if (ch === open && isCodeAt(file, n, c)) depth++;
|
|
1234
|
-
else if (ch === close && isCodeAt(file, n, c)) depth--;
|
|
1235
|
-
text += ch;
|
|
1236
|
-
if (depth === 0 && ch === close) {
|
|
1237
|
-
return { startLine: line, endLine: n, text };
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
text += "\n";
|
|
1241
|
-
}
|
|
1242
|
-
return null;
|
|
1243
|
-
}
|
|
1244
|
-
function nextOpener(file, line, col, openers = "{[(", maxLines = 3) {
|
|
1245
|
-
const rows = uncommentedLines(file);
|
|
1246
|
-
for (let n = line; n <= Math.min(file.lines.length, line + maxLines); n++) {
|
|
1247
|
-
const raw = rows[n - 1];
|
|
1248
|
-
for (let c = n === line ? col : 0; c < raw.length; c++) {
|
|
1249
|
-
if (openers.includes(raw[c]) && isCodeAt(file, n, c)) return { line: n, col: c, ch: raw[c] };
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
return null;
|
|
1253
|
-
}
|
|
1254
|
-
function isRecord(v) {
|
|
1255
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1256
|
-
}
|
|
1257
|
-
function baseOf(tree) {
|
|
1258
|
-
try {
|
|
1259
|
-
return statSync2(tree.root).isDirectory() ? tree.root : join2(tree.root, "..");
|
|
1260
|
-
} catch {
|
|
1261
|
-
return tree.root;
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
function findDiskFiles(tree, pred) {
|
|
1265
|
-
const out = [];
|
|
1266
|
-
const base = baseOf(tree);
|
|
1267
|
-
const walk2 = (dir) => {
|
|
1268
|
-
let entries;
|
|
1269
|
-
try {
|
|
1270
|
-
entries = readdirSync2(dir);
|
|
1271
|
-
} catch {
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
for (const name of entries.sort()) {
|
|
1275
|
-
const full = join2(dir, name);
|
|
1276
|
-
let st;
|
|
1277
|
-
try {
|
|
1278
|
-
st = statSync2(full);
|
|
1279
|
-
} catch {
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
if (st.isDirectory()) {
|
|
1283
|
-
if (SKIP_DIRS.has(name)) {
|
|
1284
|
-
recordSkip(tree, full, base);
|
|
1285
|
-
continue;
|
|
1286
|
-
}
|
|
1287
|
-
walk2(full);
|
|
1288
|
-
} else {
|
|
1289
|
-
const rel = relative2(base, full).split(sep2).join("/");
|
|
1290
|
-
if (pred(name, rel)) out.push({ path: full, rel, name });
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
};
|
|
1294
|
-
walk2(base);
|
|
1295
|
-
return out;
|
|
1296
|
-
}
|
|
1297
|
-
function readText(path) {
|
|
1298
|
-
try {
|
|
1299
|
-
return readFileSync3(path, "utf8");
|
|
1300
|
-
} catch {
|
|
1301
|
-
return null;
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
function readJsonRecord(path) {
|
|
1305
|
-
const text = readText(path);
|
|
1306
|
-
if (text === null) return null;
|
|
1307
|
-
try {
|
|
1308
|
-
const v = JSON.parse(text);
|
|
1309
|
-
return isRecord(v) ? v : null;
|
|
1310
|
-
} catch {
|
|
1311
|
-
return null;
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
function proseWordCount(markdown) {
|
|
1315
|
-
const noFences = markdown.replace(/^(```|~~~)[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, " ");
|
|
1316
|
-
const noInline = noFences.replace(/`[^`\n]*`/g, " ");
|
|
1317
|
-
return (noInline.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) ?? []).length;
|
|
1318
|
-
}
|
|
1319
|
-
function lineHits(tree, re) {
|
|
1320
|
-
const out = [];
|
|
1321
|
-
for (const file of tree.files) {
|
|
1322
|
-
const rows = uncommentedLines(file);
|
|
1323
|
-
rows.forEach((text, i) => {
|
|
1324
|
-
if (re.test(text)) {
|
|
1325
|
-
out.push({ file, line: i + 1, anchor: `${file.rel}:${i + 1}`, text });
|
|
1326
|
-
}
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
return out;
|
|
1330
|
-
}
|
|
1331
|
-
function enclosingBlock(file, line, col, levels = 0) {
|
|
1332
|
-
const rows = uncommentedLines(file);
|
|
1333
|
-
let curLine = line;
|
|
1334
|
-
let curCol = col;
|
|
1335
|
-
let result = null;
|
|
1336
|
-
for (let level = 0; level <= levels; level++) {
|
|
1337
|
-
let depth = 0;
|
|
1338
|
-
let found = null;
|
|
1339
|
-
outer: for (let n = curLine; n >= 1; n--) {
|
|
1340
|
-
const raw = rows[n - 1];
|
|
1341
|
-
for (let c = (n === curLine ? curCol : raw.length) - 1; c >= 0; c--) {
|
|
1342
|
-
if (!isCodeAt(file, n, c)) continue;
|
|
1343
|
-
if (raw[c] === "}") depth++;
|
|
1344
|
-
else if (raw[c] === "{") {
|
|
1345
|
-
if (depth === 0) {
|
|
1346
|
-
found = { l: n, c };
|
|
1347
|
-
break outer;
|
|
1348
|
-
}
|
|
1349
|
-
depth--;
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
if (!found) return result;
|
|
1354
|
-
const b = balancedBlock(file, found.l, found.c, "{", "}");
|
|
1355
|
-
if (!b) return null;
|
|
1356
|
-
result = { ...b, openLine: found.l, openCol: found.c };
|
|
1357
|
-
curLine = found.l;
|
|
1358
|
-
curCol = found.c;
|
|
1359
|
-
}
|
|
1360
|
-
return result;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
1598
|
// src/detectors/a2-declared-domains.ts
|
|
1364
1599
|
function stripComments(text) {
|
|
1365
1600
|
return text.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:"'`])\/\/.*$/gm, "$1");
|
|
@@ -1518,7 +1753,7 @@ var a2DeclaredDomains = {
|
|
|
1518
1753
|
};
|
|
1519
1754
|
|
|
1520
1755
|
// src/detectors/v080Kit.ts
|
|
1521
|
-
import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as
|
|
1756
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
1522
1757
|
import { dirname as dirname2, join as join3 } from "path";
|
|
1523
1758
|
|
|
1524
1759
|
// src/detectors/toolContext.ts
|
|
@@ -1936,47 +2171,75 @@ function findUnresolvedToolSites(tree) {
|
|
|
1936
2171
|
const kept = out.filter((a) => !wrappers.internal.has(a));
|
|
1937
2172
|
return [...kept, ...wrappers.unresolved];
|
|
1938
2173
|
}
|
|
1939
|
-
function findListReadBuilders(tree) {
|
|
1940
|
-
const
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2174
|
+
function findListReadBuilders(tree) {
|
|
2175
|
+
const bindings = resolveNameBindings(tree);
|
|
2176
|
+
const extents = [];
|
|
2177
|
+
const endOf = (startLine, startCol, text) => {
|
|
2178
|
+
const lines = text.split("\n");
|
|
2179
|
+
const last = lines.length - 1;
|
|
2180
|
+
return [startLine + last, last === 0 ? startCol + text.length - 1 : lines[last].length - 1];
|
|
2181
|
+
};
|
|
2182
|
+
for (const site of codeSites(tree, /(?:\.tool|\bregisterTool)\s*\(/g)) {
|
|
2183
|
+
const open = site.col + site.match[0].lastIndexOf("(");
|
|
2184
|
+
const b = balancedBlock(site.file, site.line.n, open, "(", ")");
|
|
2185
|
+
if (!b) continue;
|
|
2186
|
+
const first = firstArgument(b.text.slice(1, -1));
|
|
2187
|
+
const lit = first.match(/^["'`]([^"'`]+)["'`]$/);
|
|
2188
|
+
const ident = first.match(/^([A-Za-z_$][\w$]*)(?:\.name)?$/);
|
|
2189
|
+
const name = lit ? lit[1] : ident ? bindings.get(ident[1]) ?? null : null;
|
|
2190
|
+
extents.push({
|
|
2191
|
+
file: site.file,
|
|
2192
|
+
name,
|
|
2193
|
+
nameLine: site.line.n,
|
|
2194
|
+
from: [site.line.n, open],
|
|
2195
|
+
to: endOf(site.line.n, open, b.text)
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
for (const site of codeSites(tree, new RegExp(DESCRIPTOR_NAME_RE.source, "g"))) {
|
|
2199
|
+
const b = enclosingBlock(site.file, site.line.n, site.col);
|
|
2200
|
+
if (!b) continue;
|
|
2201
|
+
extents.push({
|
|
2202
|
+
file: site.file,
|
|
2203
|
+
name: site.match[1],
|
|
2204
|
+
nameLine: site.line.n,
|
|
2205
|
+
from: [b.openLine, b.openCol],
|
|
2206
|
+
to: endOf(b.openLine, b.openCol, b.text)
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
const before = (a, b) => a[0] < b[0] || a[0] === b[0] && a[1] <= b[1];
|
|
2210
|
+
const out = [];
|
|
2211
|
+
for (const site of codeSites(tree, new RegExp(CONTENT_RE.source, "g"))) {
|
|
2212
|
+
const at = [site.line.n, site.col];
|
|
2213
|
+
let owner = null;
|
|
2214
|
+
for (const e of extents) {
|
|
2215
|
+
if (e.file !== site.file || !before(e.from, at) || !before(at, e.to)) continue;
|
|
2216
|
+
if (!owner || before(owner.from, e.from)) owner = e;
|
|
2217
|
+
}
|
|
2218
|
+
if (owner?.name && LIST_READ_RE.test(owner.name)) {
|
|
2219
|
+
out.push({
|
|
2220
|
+
file: site.file,
|
|
2221
|
+
toolName: owner.name,
|
|
2222
|
+
toolNameLine: owner.nameLine,
|
|
2223
|
+
contentLine: site.line.n
|
|
2224
|
+
});
|
|
1976
2225
|
}
|
|
1977
2226
|
}
|
|
1978
2227
|
return out;
|
|
1979
2228
|
}
|
|
2229
|
+
function firstArgument(inner) {
|
|
2230
|
+
let depth = 0;
|
|
2231
|
+
for (let i = 0; i < inner.length; i++) {
|
|
2232
|
+
const c = inner[i];
|
|
2233
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
2234
|
+
for (i++; i < inner.length && inner[i] !== c; i++) if (inner[i] === "\\") i++;
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
2238
|
+
else if (c === ")" || c === "]" || c === "}") depth--;
|
|
2239
|
+
else if (c === "," && depth === 0) return inner.slice(0, i).trim();
|
|
2240
|
+
}
|
|
2241
|
+
return inner.trim();
|
|
2242
|
+
}
|
|
1980
2243
|
var SDK_FIRST_RE = /\bregisterTool\s*\(|\.tool\s*\(|new\s+McpServer\s*\(/;
|
|
1981
2244
|
function sdkFirstAnchor(tree) {
|
|
1982
2245
|
for (const file of tree.files) {
|
|
@@ -2381,16 +2644,29 @@ function governingWorkflows(tree) {
|
|
|
2381
2644
|
const wf = join3(dir, ".github", "workflows");
|
|
2382
2645
|
if (existsSync3(wf)) {
|
|
2383
2646
|
const out = [];
|
|
2384
|
-
|
|
2647
|
+
const base = baseOf(tree);
|
|
2648
|
+
let names;
|
|
2649
|
+
try {
|
|
2650
|
+
names = readdirSync3(wf).sort();
|
|
2651
|
+
} catch (err) {
|
|
2652
|
+
recordSkip(tree, wf, base, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
2653
|
+
return out;
|
|
2654
|
+
}
|
|
2655
|
+
for (const name of names) {
|
|
2385
2656
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
2386
2657
|
const full = join3(wf, name);
|
|
2387
2658
|
try {
|
|
2388
|
-
if (!
|
|
2389
|
-
|
|
2659
|
+
if (!statSync4(full).isFile()) {
|
|
2660
|
+
recordSkip(tree, full, base, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
recordSkip(tree, full, base, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
2390
2665
|
continue;
|
|
2391
2666
|
}
|
|
2392
2667
|
const text = readText(full);
|
|
2393
2668
|
if (text !== null) out.push({ rel: `.github/workflows/${name}`, text });
|
|
2669
|
+
else recordSkip(tree, full, base, "could not be read \u2014 not read");
|
|
2394
2670
|
}
|
|
2395
2671
|
return out;
|
|
2396
2672
|
}
|
|
@@ -2699,7 +2975,7 @@ var a4WcagGate = {
|
|
|
2699
2975
|
};
|
|
2700
2976
|
|
|
2701
2977
|
// src/detectors/a5-performance-budgets.ts
|
|
2702
|
-
import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as
|
|
2978
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
2703
2979
|
import { join as join4, relative as relative3, sep as sep3 } from "path";
|
|
2704
2980
|
import { gzipSync } from "zlib";
|
|
2705
2981
|
var BUDGET_BYTES = 250 * 1024;
|
|
@@ -2735,7 +3011,7 @@ function collect(base) {
|
|
|
2735
3011
|
const candidate = BUNDLE_EXT_RE.test(name) && UI_PATH_RE.test(rel);
|
|
2736
3012
|
let st;
|
|
2737
3013
|
try {
|
|
2738
|
-
st =
|
|
3014
|
+
st = statSync5(full);
|
|
2739
3015
|
} catch {
|
|
2740
3016
|
if (candidate) unmeasured.push(`${rel} (cannot be read: dangling link or permission)`);
|
|
2741
3017
|
continue;
|
|
@@ -2743,6 +3019,10 @@ function collect(base) {
|
|
|
2743
3019
|
if (st.isDirectory()) {
|
|
2744
3020
|
if (!WALK_SKIP.has(name)) walk2(full, depth + 1);
|
|
2745
3021
|
} else if (candidate) {
|
|
3022
|
+
if (!st.isFile()) {
|
|
3023
|
+
unmeasured.push(`${rel} (not a regular file, not read)`);
|
|
3024
|
+
continue;
|
|
3025
|
+
}
|
|
2746
3026
|
if (st.size > MAX_MEASURE_BYTES) {
|
|
2747
3027
|
unmeasured.push(`${rel} (${(st.size / 1048576).toFixed(0)} MB, too large to measure)`);
|
|
2748
3028
|
continue;
|
|
@@ -3996,7 +4276,7 @@ var a12FileWrite = {
|
|
|
3996
4276
|
};
|
|
3997
4277
|
|
|
3998
4278
|
// src/detectors/c7-artifact-verifiable.ts
|
|
3999
|
-
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
4279
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync6 } from "fs";
|
|
4000
4280
|
import { join as join5 } from "path";
|
|
4001
4281
|
var PIN = STANDARD_PIN;
|
|
4002
4282
|
var DOC_FILES = ["README.md", "README.fr.md"];
|
|
@@ -4034,13 +4314,18 @@ function c7PositiveControls() {
|
|
|
4034
4314
|
}
|
|
4035
4315
|
};
|
|
4036
4316
|
}
|
|
4037
|
-
function readDocText(root) {
|
|
4317
|
+
function readDocText(root, unreadable) {
|
|
4038
4318
|
for (const name of DOC_FILES) {
|
|
4039
4319
|
const path = join5(root, name);
|
|
4040
4320
|
if (!existsSync4(path)) continue;
|
|
4041
4321
|
try {
|
|
4322
|
+
if (!statSync6(path).isFile()) {
|
|
4323
|
+
unreadable.push({ anchor: name, reason: `${name} is not a regular file \u2014 not read` });
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4042
4326
|
return { text: readFileSync5(path, "utf8"), file: name };
|
|
4043
|
-
} catch {
|
|
4327
|
+
} catch (err) {
|
|
4328
|
+
unreadable.push({ anchor: name, reason: `${name} could not be read (${fsErrorCode(err)})` });
|
|
4044
4329
|
}
|
|
4045
4330
|
}
|
|
4046
4331
|
return null;
|
|
@@ -4071,7 +4356,15 @@ var c7ArtifactVerifiable = {
|
|
|
4071
4356
|
`C7 channel-matcher control FAILED on \`${controls.channelMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
|
|
4072
4357
|
);
|
|
4073
4358
|
}
|
|
4074
|
-
const
|
|
4359
|
+
const unreadable = [];
|
|
4360
|
+
const doc = readDocText(tree.root, unreadable);
|
|
4361
|
+
if (unreadable.length > 0) {
|
|
4362
|
+
return couldNotJudge(
|
|
4363
|
+
this.id,
|
|
4364
|
+
unreadable[0].anchor,
|
|
4365
|
+
`${unreadable.map((u) => u.reason).join("; ")} \u2014 the install, pin, channel and provenance propositions are unjudged (C-7)`
|
|
4366
|
+
);
|
|
4367
|
+
}
|
|
4075
4368
|
if (!doc) {
|
|
4076
4369
|
return {
|
|
4077
4370
|
id: this.id,
|
|
@@ -4224,6 +4517,7 @@ var VERB_SET = /* @__PURE__ */ new Set([
|
|
|
4224
4517
|
"get",
|
|
4225
4518
|
"search",
|
|
4226
4519
|
"aggregate",
|
|
4520
|
+
"prepare",
|
|
4227
4521
|
"create",
|
|
4228
4522
|
"update",
|
|
4229
4523
|
"delete",
|
|
@@ -4239,6 +4533,7 @@ function c9PositiveControls() {
|
|
|
4239
4533
|
return {
|
|
4240
4534
|
fieldMatcher: field,
|
|
4241
4535
|
setAcceptsCreate: VERB_SET.has("create"),
|
|
4536
|
+
setAcceptsPrepare: VERB_SET.has("prepare"),
|
|
4242
4537
|
setRejectsRpc: !VERB_SET.has("rpc")
|
|
4243
4538
|
};
|
|
4244
4539
|
}
|
|
@@ -4253,9 +4548,9 @@ var c9VerbDeclared = {
|
|
|
4253
4548
|
`C9 positive control FAILED: pattern /${controls.fieldMatcher.pattern}/ did not match sample \`${controls.fieldMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
|
|
4254
4549
|
);
|
|
4255
4550
|
}
|
|
4256
|
-
if (!controls.setAcceptsCreate || !controls.setRejectsRpc) {
|
|
4551
|
+
if (!controls.setAcceptsCreate || !controls.setAcceptsPrepare || !controls.setRejectsRpc) {
|
|
4257
4552
|
throw new Error(
|
|
4258
|
-
"C9 closed-set control FAILED: VERB_SET must accept
|
|
4553
|
+
"C9 closed-set control FAILED: VERB_SET must accept the legitimate verbs create and prepare and reject the banned rpc sentinel \u2014 verdict untrustworthy"
|
|
4259
4554
|
);
|
|
4260
4555
|
}
|
|
4261
4556
|
const unresolved = findUnresolvedToolSites(tree);
|
|
@@ -4302,7 +4597,7 @@ var c9VerbDeclared = {
|
|
|
4302
4597
|
id: this.id,
|
|
4303
4598
|
anchor: t.anchor,
|
|
4304
4599
|
state: "non-conforming",
|
|
4305
|
-
verdict: `tool "${t.name}" declares verb "${t.verb}", outside the closed set (positive-control OK) -> use
|
|
4600
|
+
verdict: `tool "${t.name}" declares verb "${t.verb}", outside the closed set (positive-control OK) -> use one of ${[...VERB_SET].join("/")} (C-9)`
|
|
4306
4601
|
};
|
|
4307
4602
|
}
|
|
4308
4603
|
}
|
|
@@ -4475,17 +4770,14 @@ var c11Pagination = {
|
|
|
4475
4770
|
};
|
|
4476
4771
|
},
|
|
4477
4772
|
/**
|
|
4478
|
-
* DYNAMIC —
|
|
4479
|
-
*
|
|
4480
|
-
*
|
|
4481
|
-
*
|
|
4482
|
-
* schema and no `callTool` verb — that half needs `Session.callTool` with a
|
|
4483
|
-
* cursor argument (documented gap, types.ts `Session`). The verdict names
|
|
4484
|
-
* this explicitly rather than folding it into a silent full "present".
|
|
4773
|
+
* DYNAMIC — the invalid-cursor round trip, on each surface where a cursor arrives (C-11 (a) and
|
|
4774
|
+
* (b)). Red on any probe answered in the wrong form; could-not-judge on a probe that got no
|
|
4775
|
+
* answer, was not sent, or recorded an outcome this code cannot read; present only when every
|
|
4776
|
+
* probe answered in its surface's form.
|
|
4485
4777
|
*/
|
|
4486
4778
|
runDynamic(runtime) {
|
|
4487
4779
|
if (runtime.launchError) {
|
|
4488
|
-
const reason = `server could not be started/introspected \u2014
|
|
4780
|
+
const reason = `server could not be started/introspected \u2014 the invalid-cursor round trip could not run: ${runtime.launchError} (C-11)`;
|
|
4489
4781
|
return {
|
|
4490
4782
|
id: this.id,
|
|
4491
4783
|
anchor: `${runtime.root} (runtime)`,
|
|
@@ -4494,24 +4786,117 @@ var c11Pagination = {
|
|
|
4494
4786
|
verdict: `could not judge: ${reason}`
|
|
4495
4787
|
};
|
|
4496
4788
|
}
|
|
4497
|
-
const paginable = runtime.tools.filter((t) => PAGINABLE_RE.test(t.name));
|
|
4789
|
+
const paginable = runtime.tools.filter((t) => PAGINABLE_RE.test(t.name) || t.cursorArgument);
|
|
4498
4790
|
if (paginable.length === 0) {
|
|
4499
4791
|
return {
|
|
4500
4792
|
id: this.id,
|
|
4501
4793
|
anchor: `${runtime.root} (runtime tools/list)`,
|
|
4502
4794
|
state: "absent",
|
|
4503
|
-
verdict: "server advertises no list/read/get/search tool at runtime; pagination check n/a"
|
|
4795
|
+
verdict: "server advertises no list/read/get/search tool and no tool taking a cursor at runtime; pagination check n/a"
|
|
4796
|
+
};
|
|
4797
|
+
}
|
|
4798
|
+
const anchor = `${runtime.root} (runtime invalid-cursor probes)`;
|
|
4799
|
+
const probes = runtime.cursorProbes;
|
|
4800
|
+
if (!probes || probes.length === 0) {
|
|
4801
|
+
const reason = "no invalid-cursor round trip was recorded for this run: the -32602 / invalid_cursor answers are unread (C-11)";
|
|
4802
|
+
return {
|
|
4803
|
+
id: this.id,
|
|
4804
|
+
anchor,
|
|
4805
|
+
state: "could-not-judge",
|
|
4806
|
+
reason,
|
|
4807
|
+
verdict: `could not judge: ${reason}`
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4810
|
+
const red = [];
|
|
4811
|
+
const unread = [];
|
|
4812
|
+
const ok = [];
|
|
4813
|
+
for (const p of probes) {
|
|
4814
|
+
const j = judgeCursorProbe(p);
|
|
4815
|
+
if (j.kind === "red") red.push(j.text);
|
|
4816
|
+
else if (j.kind === "unread") unread.push(j.text);
|
|
4817
|
+
else ok.push(j.text);
|
|
4818
|
+
}
|
|
4819
|
+
if (red.length > 0) {
|
|
4820
|
+
return {
|
|
4821
|
+
id: this.id,
|
|
4822
|
+
anchor,
|
|
4823
|
+
state: "non-conforming",
|
|
4824
|
+
verdict: `${red.join("; ")} -> an invalid cursor on a protocol list method is -32602; passed as a tool argument it is a tool result with isError: true and structuredContent.error.code "invalid_cursor" (SEP-1303) (C-11)`
|
|
4825
|
+
};
|
|
4826
|
+
}
|
|
4827
|
+
if (unread.length > 0) {
|
|
4828
|
+
const reason = `${unread.join("; ")} (C-11)`;
|
|
4829
|
+
return {
|
|
4830
|
+
id: this.id,
|
|
4831
|
+
anchor,
|
|
4832
|
+
state: "could-not-judge",
|
|
4833
|
+
reason,
|
|
4834
|
+
verdict: `could not judge: ${reason}`
|
|
4504
4835
|
};
|
|
4505
4836
|
}
|
|
4506
|
-
const
|
|
4837
|
+
const toolProbes = probes.filter((p) => p.surface === "tool-argument").length;
|
|
4507
4838
|
return {
|
|
4508
4839
|
id: this.id,
|
|
4509
|
-
anchor
|
|
4840
|
+
anchor,
|
|
4510
4841
|
state: "present",
|
|
4511
|
-
verdict:
|
|
4842
|
+
verdict: `${ok.join("; ")}${toolProbes === 0 ? "; no served tool takes a cursor argument, so C-11 (b) has no object on this server" : ""} (C-11)`
|
|
4512
4843
|
};
|
|
4513
4844
|
}
|
|
4514
4845
|
};
|
|
4846
|
+
function judgeCursorProbe(p) {
|
|
4847
|
+
const where = p.surface === "list-method" ? `${p.target} (protocol list method)` : `tool "${p.target}" (cursor argument)`;
|
|
4848
|
+
const o = p.outcome;
|
|
4849
|
+
switch (o.kind) {
|
|
4850
|
+
case "no-answer":
|
|
4851
|
+
return { kind: "unread", text: `${where}: no answer to the invalid cursor (${o.reason})` };
|
|
4852
|
+
case "not-sent":
|
|
4853
|
+
return { kind: "unread", text: `${where}: probe not sent (${o.reason})` };
|
|
4854
|
+
case "error":
|
|
4855
|
+
if (p.surface === "list-method") {
|
|
4856
|
+
return o.code === -32602 ? { kind: "ok", text: `${where}: -32602 on an invalid cursor` } : {
|
|
4857
|
+
kind: "red",
|
|
4858
|
+
text: `${where}: answered an invalid cursor with error ${String(o.code)}, not -32602`
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
4861
|
+
return {
|
|
4862
|
+
kind: "red",
|
|
4863
|
+
text: `${where}: answered an invalid cursor argument with a protocol error ${String(o.code)} \u2014 a tool input error is a tool result, never a protocol error`
|
|
4864
|
+
};
|
|
4865
|
+
case "result":
|
|
4866
|
+
if (p.surface === "list-method") {
|
|
4867
|
+
return {
|
|
4868
|
+
kind: "red",
|
|
4869
|
+
text: `${where}: answered an invalid cursor with a result (a silent page), not -32602`
|
|
4870
|
+
};
|
|
4871
|
+
}
|
|
4872
|
+
if (!o.isError) {
|
|
4873
|
+
return {
|
|
4874
|
+
kind: "red",
|
|
4875
|
+
text: `${where}: answered an invalid cursor argument with a silent page (no isError)`
|
|
4876
|
+
};
|
|
4877
|
+
}
|
|
4878
|
+
if (o.errorCode === void 0) {
|
|
4879
|
+
return {
|
|
4880
|
+
kind: "red",
|
|
4881
|
+
text: `${where}: isError: true with no structuredContent.error.code (no stable machine code)`
|
|
4882
|
+
};
|
|
4883
|
+
}
|
|
4884
|
+
return o.errorCode === "invalid_cursor" ? {
|
|
4885
|
+
kind: "ok",
|
|
4886
|
+
text: `${where}: isError: true, structuredContent.error.code "invalid_cursor"`
|
|
4887
|
+
} : {
|
|
4888
|
+
kind: "red",
|
|
4889
|
+
text: `${where}: isError: true with structuredContent.error.code ${JSON.stringify(o.errorCode)}, not "invalid_cursor"`
|
|
4890
|
+
};
|
|
4891
|
+
default: {
|
|
4892
|
+
const unknown = o;
|
|
4893
|
+
return {
|
|
4894
|
+
kind: "unread",
|
|
4895
|
+
text: `${where}: an outcome this code cannot read (${JSON.stringify(unknown.kind)})`
|
|
4896
|
+
};
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4515
4900
|
|
|
4516
4901
|
// src/detectors/c12-no-duplicates.ts
|
|
4517
4902
|
function tripletKey(t) {
|
|
@@ -4607,37 +4992,162 @@ var c12NoDuplicates = {
|
|
|
4607
4992
|
};
|
|
4608
4993
|
|
|
4609
4994
|
// src/detectors/c13-context-ceiling.ts
|
|
4610
|
-
var
|
|
4611
|
-
var
|
|
4612
|
-
|
|
4995
|
+
var SERVER_MANIFEST_NAMES = /* @__PURE__ */ new Set(["mcp.json", "mcp-server.json"]);
|
|
4996
|
+
var NAME_RE = /\b(?:CONTEXT_CEILING|contextCeiling)\b/g;
|
|
4997
|
+
var NUM = String.raw`(\d[\d_]*)`;
|
|
4998
|
+
var halfRe = (key) => new RegExp(String.raw`(?:^|[{,\s])["'\x60]?${key}["'\x60]?\s*:\s*([^,}\s][^,}]*)`);
|
|
4999
|
+
function readObjectText(text) {
|
|
5000
|
+
const halves = {};
|
|
5001
|
+
for (const key of ["global", "perTool"]) {
|
|
5002
|
+
const m = text.match(halfRe(key));
|
|
5003
|
+
if (!m) halves[key] = "missing";
|
|
5004
|
+
else {
|
|
5005
|
+
const v = m[1].trim().replace(/\s+as\s+const$/, "");
|
|
5006
|
+
const n = v.match(new RegExp(`^${NUM}$`));
|
|
5007
|
+
halves[key] = n ? Number.parseInt(n[1].replace(/_/g, ""), 10) : v;
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
const odd = Object.entries(halves).filter(([, v]) => typeof v === "string" && v !== "missing");
|
|
5011
|
+
if (odd.length > 0) {
|
|
5012
|
+
return { kind: "unreadable", why: odd.map(([k, v]) => `${k}: ${v}`).join(", ") };
|
|
5013
|
+
}
|
|
5014
|
+
const missing = Object.entries(halves).filter(([, v]) => v === "missing").map(([k]) => k);
|
|
5015
|
+
if (missing.length === 2 && /\.\.\./.test(text)) {
|
|
5016
|
+
return { kind: "unreadable", why: `a spread with no literal halves (${text.slice(0, 60)})` };
|
|
5017
|
+
}
|
|
5018
|
+
if (missing.length > 0) return { kind: "partial", missing };
|
|
5019
|
+
return { kind: "pair", global: halves.global, perTool: halves.perTool };
|
|
5020
|
+
}
|
|
5021
|
+
function braceBlock(text) {
|
|
5022
|
+
let depth = 0;
|
|
5023
|
+
for (let i = 0; i < text.length; i++) {
|
|
5024
|
+
if (text[i] === "{") depth++;
|
|
5025
|
+
else if (text[i] === "}" && --depth === 0) return text.slice(0, i + 1);
|
|
5026
|
+
}
|
|
5027
|
+
return null;
|
|
5028
|
+
}
|
|
5029
|
+
function sameFileConstObject(file, name) {
|
|
5030
|
+
const text = uncommentedLines(file).map(codeOnly).join("\n");
|
|
5031
|
+
const m = new RegExp(
|
|
5032
|
+
String.raw`\bconst\s+${name.replace(/\$/g, "\\$")}\s*(?::[^=]+)?=\s*\{`
|
|
5033
|
+
).exec(text);
|
|
5034
|
+
if (!m) return null;
|
|
5035
|
+
return braceBlock(text.slice(m.index + m[0].length - 1));
|
|
5036
|
+
}
|
|
5037
|
+
function ceilingCensus(tree) {
|
|
5038
|
+
const census = { readable: [], partial: [], unreadable: [] };
|
|
5039
|
+
for (const f of findDiskFiles(tree, (n) => SERVER_MANIFEST_NAMES.has(n))) {
|
|
5040
|
+
const text = readText(f.path);
|
|
5041
|
+
if (text === null) {
|
|
5042
|
+
census.unreadable.push({ anchor: f.rel, reason: `${f.rel} is not readable as text` });
|
|
5043
|
+
continue;
|
|
5044
|
+
}
|
|
5045
|
+
let json;
|
|
5046
|
+
try {
|
|
5047
|
+
json = JSON.parse(text);
|
|
5048
|
+
} catch (e) {
|
|
5049
|
+
census.unreadable.push({
|
|
5050
|
+
anchor: f.rel,
|
|
5051
|
+
reason: `${f.rel} does not parse as JSON (${e instanceof Error ? e.message : String(e)}): a contextCeiling it may declare is unread`
|
|
5052
|
+
});
|
|
5053
|
+
continue;
|
|
5054
|
+
}
|
|
5055
|
+
if (!isRecord(json) || !("contextCeiling" in json)) continue;
|
|
5056
|
+
const anchor = `${f.rel}:${lineOf(text, /"contextCeiling"/)}`;
|
|
5057
|
+
const c = json.contextCeiling;
|
|
5058
|
+
if (!isRecord(c)) {
|
|
5059
|
+
census.partial.push({
|
|
5060
|
+
anchor,
|
|
5061
|
+
form: "manifest",
|
|
5062
|
+
missing: ["global", "perTool (contextCeiling is not an object)"]
|
|
5063
|
+
});
|
|
5064
|
+
continue;
|
|
5065
|
+
}
|
|
5066
|
+
const ok = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
5067
|
+
const missing = [ok(c.global) ? "" : "global", ok(c.perTool) ? "" : "perTool"].filter(Boolean);
|
|
5068
|
+
if (missing.length > 0) census.partial.push({ anchor, form: "manifest", missing });
|
|
5069
|
+
else
|
|
5070
|
+
census.readable.push({
|
|
5071
|
+
global: c.global,
|
|
5072
|
+
perTool: c.perTool,
|
|
5073
|
+
anchor,
|
|
5074
|
+
form: "manifest"
|
|
5075
|
+
});
|
|
5076
|
+
}
|
|
4613
5077
|
for (const file of tree.files) {
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
const
|
|
4618
|
-
const
|
|
4619
|
-
const
|
|
4620
|
-
const
|
|
4621
|
-
const
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
5078
|
+
const rows = uncommentedLines(file).map(codeOnly);
|
|
5079
|
+
rows.forEach((row, i) => {
|
|
5080
|
+
for (const hit of row.matchAll(NAME_RE)) {
|
|
5081
|
+
const at = hit.index ?? 0;
|
|
5082
|
+
const anchor = `${file.rel}:${i + 1}`;
|
|
5083
|
+
const before = row.slice(0, at);
|
|
5084
|
+
const window = [row.slice(at + hit[0].length), ...rows.slice(i + 1, i + 9)].join("\n");
|
|
5085
|
+
const rest = window.replace(/^["'\x60]/, "");
|
|
5086
|
+
let value = null;
|
|
5087
|
+
if (/\b(?:const|let|var)\s+$/.test(before)) {
|
|
5088
|
+
const m = rest.match(/^\s*(?::[^=]+?)?=(?![=>])\s*/);
|
|
5089
|
+
if (m) value = rest.slice(m[0].length);
|
|
5090
|
+
} else {
|
|
5091
|
+
const m = rest.match(/^\s*\??:(?!:)\s*/) ?? rest.match(/^\s*=(?![=>])\s*/);
|
|
5092
|
+
if (m) value = rest.slice(m[0].length);
|
|
5093
|
+
}
|
|
5094
|
+
if (value === null) continue;
|
|
5095
|
+
if (/^[A-Z][a-z]\w*\s*(?:[;,)|>=]|$)/.test(value)) continue;
|
|
5096
|
+
let obj = null;
|
|
5097
|
+
if (value.startsWith("{")) obj = braceBlock(value);
|
|
5098
|
+
else {
|
|
5099
|
+
const id = value.match(/^([A-Za-z_$][\w$]*)\s*(?:[;,)}\n]|as\b|$)/);
|
|
5100
|
+
if (id) {
|
|
5101
|
+
obj = sameFileConstObject(file, id[1]);
|
|
5102
|
+
if (obj === null) {
|
|
5103
|
+
census.unreadable.push({
|
|
5104
|
+
anchor,
|
|
5105
|
+
reason: `${anchor} declares ${hit[0]} as \`${id[1]}\`, which is not a same-file const object literal`
|
|
5106
|
+
});
|
|
5107
|
+
continue;
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
if (obj === null) {
|
|
5112
|
+
census.unreadable.push({
|
|
5113
|
+
anchor,
|
|
5114
|
+
reason: `${anchor} declares ${hit[0]} as a computed value (\`${value.split("\n")[0].trim().slice(0, 50)}\`): its numbers are not readable statically`
|
|
5115
|
+
});
|
|
5116
|
+
continue;
|
|
4628
5117
|
}
|
|
5118
|
+
const r = readObjectText(obj);
|
|
5119
|
+
if (r.kind === "partial" && r.missing.length === 2 && /;/.test(obj)) continue;
|
|
5120
|
+
if (r.kind === "pair")
|
|
5121
|
+
census.readable.push({ global: r.global, perTool: r.perTool, anchor, form: "source" });
|
|
5122
|
+
else if (r.kind === "partial")
|
|
5123
|
+
census.partial.push({ anchor, form: "source", missing: r.missing });
|
|
5124
|
+
else
|
|
5125
|
+
census.unreadable.push({
|
|
5126
|
+
anchor,
|
|
5127
|
+
reason: `${anchor} declares ${hit[0]} with ${r.why}: not numeric literals`
|
|
5128
|
+
});
|
|
4629
5129
|
}
|
|
4630
|
-
}
|
|
5130
|
+
});
|
|
4631
5131
|
}
|
|
4632
|
-
return
|
|
5132
|
+
return census;
|
|
5133
|
+
}
|
|
5134
|
+
function agreed(census) {
|
|
5135
|
+
const [first, ...rest] = census.readable;
|
|
5136
|
+
if (!first) return null;
|
|
5137
|
+
return rest.every((c) => c.global === first.global && c.perTool === first.perTool) ? first : null;
|
|
5138
|
+
}
|
|
5139
|
+
function findCeiling(tree) {
|
|
5140
|
+
const c = agreed(ceilingCensus(tree));
|
|
5141
|
+
return c ? { global: c.global, perTool: c.perTool, anchor: c.anchor } : null;
|
|
4633
5142
|
}
|
|
4634
5143
|
function c13PositiveControls() {
|
|
4635
|
-
const complete = "
|
|
4636
|
-
const
|
|
5144
|
+
const complete = readObjectText("{ global: 8000, perTool: 800 }");
|
|
5145
|
+
const quoted = readObjectText('{ "global": 1200, "perTool": 700 }');
|
|
5146
|
+
const partial = readObjectText("{ global: 8000 }");
|
|
4637
5147
|
return {
|
|
4638
5148
|
tokensMatcher: toolAnnotationFieldControls().tokensCtx,
|
|
4639
|
-
ceilingAcceptsComplete:
|
|
4640
|
-
ceilingRejectsPartial:
|
|
5149
|
+
ceilingAcceptsComplete: complete.kind === "pair" && quoted.kind === "pair",
|
|
5150
|
+
ceilingRejectsPartial: partial.kind === "partial"
|
|
4641
5151
|
};
|
|
4642
5152
|
}
|
|
4643
5153
|
var c13ContextCeiling = {
|
|
@@ -4653,7 +5163,7 @@ var c13ContextCeiling = {
|
|
|
4653
5163
|
}
|
|
4654
5164
|
if (!controls.ceilingAcceptsComplete || !controls.ceilingRejectsPartial) {
|
|
4655
5165
|
throw new Error(
|
|
4656
|
-
"C13 ceiling control FAILED: must accept a complete (global, perTool) declaration and reject a partial one \u2014 verdict untrustworthy"
|
|
5166
|
+
"C13 ceiling control FAILED: must accept a complete (global, perTool) declaration, bare or quoted keys, and reject a partial one \u2014 verdict untrustworthy"
|
|
4657
5167
|
);
|
|
4658
5168
|
}
|
|
4659
5169
|
const unresolved = findUnresolvedToolSites(tree);
|
|
@@ -4678,13 +5188,44 @@ var c13ContextCeiling = {
|
|
|
4678
5188
|
}
|
|
4679
5189
|
const opaque = opaqueAnnotationsAbstain(this.id, "C-13", tools);
|
|
4680
5190
|
if (opaque) return opaque;
|
|
4681
|
-
const
|
|
4682
|
-
|
|
5191
|
+
const census = ceilingCensus(tree);
|
|
5192
|
+
const unreadNote = census.unreadable.length > 0 ? `; also seen and not read: ${census.unreadable.map((u) => u.reason).join("; ")}` : "";
|
|
5193
|
+
if (census.readable.length === 0) {
|
|
5194
|
+
if (census.partial.length > 0) {
|
|
5195
|
+
const p = census.partial[0];
|
|
5196
|
+
return {
|
|
5197
|
+
id: this.id,
|
|
5198
|
+
anchor: p.anchor,
|
|
5199
|
+
state: "non-conforming",
|
|
5200
|
+
verdict: `${census.partial.map((x) => `${x.anchor} declares a context ceiling without ${x.missing.join(" and ")}`).join("; ")} (positive-control OK) -> declare both a global and a per-tool ceiling, as positive numbers (C-13)${unreadNote}`
|
|
5201
|
+
};
|
|
5202
|
+
}
|
|
5203
|
+
if (census.unreadable.length > 0) {
|
|
5204
|
+
const reason = `the context ceiling is declared in a form this run cannot read: ${census.unreadable.map((u) => u.reason).join("; ")} (C-13)`;
|
|
5205
|
+
return {
|
|
5206
|
+
id: this.id,
|
|
5207
|
+
anchor: census.unreadable[0].anchor,
|
|
5208
|
+
state: "could-not-judge",
|
|
5209
|
+
reason,
|
|
5210
|
+
verdict: `could not judge: ${reason}`
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
4683
5213
|
return {
|
|
4684
5214
|
id: this.id,
|
|
4685
5215
|
anchor: tools[0].anchor,
|
|
4686
5216
|
state: "non-conforming",
|
|
4687
|
-
verdict: `${tools.length} tool(s) registered but no (global, perTool) context ceiling declared anywhere (positive-control OK) -> declare
|
|
5217
|
+
verdict: `${tools.length} tool(s) registered but no (global, perTool) context ceiling declared anywhere \u2014 read: the server manifest's contextCeiling (mcp.json / mcp-server.json) and a CONTEXT_CEILING / contextCeiling const or field in source (positive-control OK) -> declare contextCeiling: { global, perTool } in the server manifest (C-13)`
|
|
5218
|
+
};
|
|
5219
|
+
}
|
|
5220
|
+
const ceiling = agreed(census);
|
|
5221
|
+
if (!ceiling) {
|
|
5222
|
+
const reason = `${census.readable.length} declared context ceilings disagree (${census.readable.map((c) => `${c.anchor} global ${c.global} perTool ${c.perTool}`).join("; ")}): which one governs is unknown (C-13)`;
|
|
5223
|
+
return {
|
|
5224
|
+
id: this.id,
|
|
5225
|
+
anchor: census.readable[0].anchor,
|
|
5226
|
+
state: "could-not-judge",
|
|
5227
|
+
reason,
|
|
5228
|
+
verdict: `could not judge: ${reason}`
|
|
4688
5229
|
};
|
|
4689
5230
|
}
|
|
4690
5231
|
let sum = 0;
|
|
@@ -4719,7 +5260,7 @@ var c13ContextCeiling = {
|
|
|
4719
5260
|
id: this.id,
|
|
4720
5261
|
anchor: ceiling.anchor,
|
|
4721
5262
|
state: "present",
|
|
4722
|
-
verdict: `${tools.length} tool(s) each declare tokensEstimeContexte within the declared per-tool ceiling (${ceiling.perTool}); sum ${sum} within global ceiling ${ceiling.global} (positive-control OK)`
|
|
5263
|
+
verdict: `${tools.length} tool(s) each declare tokensEstimeContexte within the declared per-tool ceiling (${ceiling.perTool}); sum ${sum} within global ceiling ${ceiling.global}, declared at ${ceiling.anchor} (positive-control OK)${unreadNote}`
|
|
4723
5264
|
};
|
|
4724
5265
|
}
|
|
4725
5266
|
};
|
|
@@ -4826,7 +5367,7 @@ var c14CatalogRegistration = guardUnreadablePackageJson(
|
|
|
4826
5367
|
);
|
|
4827
5368
|
|
|
4828
5369
|
// src/detectors/c15-annotations-untrusted.ts
|
|
4829
|
-
var READ_VERBS = /* @__PURE__ */ new Set(["list", "get", "search", "aggregate"]);
|
|
5370
|
+
var READ_VERBS = /* @__PURE__ */ new Set(["list", "get", "search", "aggregate", "prepare"]);
|
|
4830
5371
|
var WRITE_VERBS = /* @__PURE__ */ new Set([
|
|
4831
5372
|
"create",
|
|
4832
5373
|
"update",
|
|
@@ -5086,7 +5627,7 @@ var c16UntrustedOutput = {
|
|
|
5086
5627
|
};
|
|
5087
5628
|
|
|
5088
5629
|
// src/detectors/c17-supply-chain.ts
|
|
5089
|
-
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync5 } from "fs";
|
|
5630
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
5090
5631
|
import { dirname as dirname3, join as join6, relative as relative4, resolve } from "path";
|
|
5091
5632
|
var PIN4 = STANDARD_PIN;
|
|
5092
5633
|
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
@@ -5110,8 +5651,8 @@ var RECORD_LINE_RE = /^[\s>*_\-+]*Provenance\s*:\s*(.*)$/i;
|
|
|
5110
5651
|
var SHASUM_HEX_RE = /\bshasum\b[^\n]*?\b([0-9a-f]{40})\b/i;
|
|
5111
5652
|
function judgeProvenanceRecord(line, name, version) {
|
|
5112
5653
|
const body = line.match(RECORD_LINE_RE)?.[1] ?? "";
|
|
5113
|
-
const
|
|
5114
|
-
const named = new RegExp(`(?:^|[\\s(])${
|
|
5654
|
+
const esc3 = (x) => x.replace(/[.+*?^$()[\]{}|\\/]/g, "\\$&");
|
|
5655
|
+
const named = new RegExp(`(?:^|[\\s(])${esc3(name)}@v?${esc3(version)}(?![\\w.-]*\\d)`);
|
|
5115
5656
|
if (!named.test(body)) {
|
|
5116
5657
|
return { ok: false, why: `it does not name ${name}@${version}, the artifact being published` };
|
|
5117
5658
|
}
|
|
@@ -5120,15 +5661,26 @@ function judgeProvenanceRecord(line, name, version) {
|
|
|
5120
5661
|
}
|
|
5121
5662
|
return { ok: true, why: "" };
|
|
5122
5663
|
}
|
|
5123
|
-
function findProvenanceRecord(root, name, version) {
|
|
5664
|
+
function findProvenanceRecord(root, name, version, unreadable) {
|
|
5124
5665
|
let firstBad = null;
|
|
5125
5666
|
for (const recordFile of PROVENANCE_RECORD_FILES) {
|
|
5126
5667
|
const path = join6(root, recordFile);
|
|
5127
5668
|
if (!existsSync5(path)) continue;
|
|
5128
5669
|
let text;
|
|
5129
5670
|
try {
|
|
5671
|
+
if (!statSync7(path).isFile()) {
|
|
5672
|
+
unreadable.push({
|
|
5673
|
+
anchor: recordFile,
|
|
5674
|
+
reason: `${recordFile} is not a regular file \u2014 not read`
|
|
5675
|
+
});
|
|
5676
|
+
continue;
|
|
5677
|
+
}
|
|
5130
5678
|
text = readFileSync6(path, "utf8");
|
|
5131
|
-
} catch {
|
|
5679
|
+
} catch (err) {
|
|
5680
|
+
unreadable.push({
|
|
5681
|
+
anchor: recordFile,
|
|
5682
|
+
reason: `${recordFile} could not be read (${fsErrorCode(err)})`
|
|
5683
|
+
});
|
|
5132
5684
|
continue;
|
|
5133
5685
|
}
|
|
5134
5686
|
const lines = text.split("\n");
|
|
@@ -5181,7 +5733,7 @@ function findLockfile(root) {
|
|
|
5181
5733
|
}
|
|
5182
5734
|
return null;
|
|
5183
5735
|
}
|
|
5184
|
-
function findProvenanceClaim(tree, root) {
|
|
5736
|
+
function findProvenanceClaim(tree, root, unreadable) {
|
|
5185
5737
|
const pkg = tree.packageJson;
|
|
5186
5738
|
if (pkg) {
|
|
5187
5739
|
const publishConfig = pkg.json.publishConfig;
|
|
@@ -5195,16 +5747,29 @@ function findProvenanceClaim(tree, root) {
|
|
|
5195
5747
|
let entries = [];
|
|
5196
5748
|
try {
|
|
5197
5749
|
entries = readdirSync5(workflowsDir).sort();
|
|
5198
|
-
} catch {
|
|
5750
|
+
} catch (err) {
|
|
5751
|
+
unreadable.push({
|
|
5752
|
+
anchor: relative4(root, workflowsDir) || WORKFLOW_DIR,
|
|
5753
|
+
reason: `${relative4(root, workflowsDir) || WORKFLOW_DIR} cannot be listed (${fsErrorCode(err)})`
|
|
5754
|
+
});
|
|
5199
5755
|
continue;
|
|
5200
5756
|
}
|
|
5201
5757
|
for (const name of entries) {
|
|
5202
5758
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
5203
5759
|
const path = join6(workflowsDir, name);
|
|
5204
5760
|
let text;
|
|
5761
|
+
const shown = `${relative4(root, workflowsDir) || WORKFLOW_DIR}/${name}`;
|
|
5205
5762
|
try {
|
|
5763
|
+
if (!statSync7(path).isFile()) {
|
|
5764
|
+
unreadable.push({ anchor: shown, reason: `${shown} is not a regular file \u2014 not read` });
|
|
5765
|
+
continue;
|
|
5766
|
+
}
|
|
5206
5767
|
text = readFileSync6(path, "utf8");
|
|
5207
|
-
} catch {
|
|
5768
|
+
} catch (err) {
|
|
5769
|
+
unreadable.push({
|
|
5770
|
+
anchor: shown,
|
|
5771
|
+
reason: `${shown} could not be read (${fsErrorCode(err)})`
|
|
5772
|
+
});
|
|
5208
5773
|
continue;
|
|
5209
5774
|
}
|
|
5210
5775
|
const hit = workflowStatements(text).find(([, l]) => PROVENANCE_RE2.test(l));
|
|
@@ -5253,10 +5818,11 @@ var c17SupplyChainInner = {
|
|
|
5253
5818
|
verdict: "no committed lockfile (package-lock.json/pnpm-lock.yaml/yarn.lock/bun.lock) found -> dependencies are not pinned; commit a lockfile (C-17a)"
|
|
5254
5819
|
};
|
|
5255
5820
|
}
|
|
5256
|
-
const
|
|
5821
|
+
const unreadable = [];
|
|
5822
|
+
const claim = findProvenanceClaim(tree, tree.root, unreadable);
|
|
5257
5823
|
const pkgVersion = typeof tree.packageJson?.json.version === "string" ? tree.packageJson.json.version : void 0;
|
|
5258
5824
|
const pkgName = typeof tree.packageJson?.json.name === "string" ? tree.packageJson.json.name : void 0;
|
|
5259
|
-
const record = claim ? null : findProvenanceRecord(tree.root, pkgName, pkgVersion);
|
|
5825
|
+
const record = claim ? null : findProvenanceRecord(tree.root, pkgName, pkgVersion, unreadable);
|
|
5260
5826
|
if (!claim && record?.ok) {
|
|
5261
5827
|
return {
|
|
5262
5828
|
id: this.id,
|
|
@@ -5273,6 +5839,13 @@ var c17SupplyChainInner = {
|
|
|
5273
5839
|
verdict: `lockfile ${lockfile} present and a \`Provenance:\` line at ${record.site}, but ${record.why} -> a record covers only the version it names and must carry the reproduced 40-hex shasum (C-17, non-CI path; proposed evidence form)`
|
|
5274
5840
|
};
|
|
5275
5841
|
}
|
|
5842
|
+
if (!claim && !record && unreadable.length > 0) {
|
|
5843
|
+
return couldNotJudge(
|
|
5844
|
+
this.id,
|
|
5845
|
+
unreadable[0].anchor,
|
|
5846
|
+
`no provenance claim in the readable places, but ${unreadable.length} candidate(s) were present and unread \u2014 ${unreadable.map((u) => u.reason).join("; ")} (C-17b)`
|
|
5847
|
+
);
|
|
5848
|
+
}
|
|
5276
5849
|
if (!claim) {
|
|
5277
5850
|
return {
|
|
5278
5851
|
id: this.id,
|
|
@@ -5294,7 +5867,7 @@ var c17SupplyChain = guardUnreadablePackageJson(c17SupplyChainInner, {
|
|
|
5294
5867
|
});
|
|
5295
5868
|
|
|
5296
5869
|
// src/detectors/c18-structure-manifest.ts
|
|
5297
|
-
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
5870
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, statSync as statSync8 } from "fs";
|
|
5298
5871
|
import { join as join7 } from "path";
|
|
5299
5872
|
var PIN5 = STANDARD_PIN;
|
|
5300
5873
|
function manifestPath(root) {
|
|
@@ -5302,7 +5875,7 @@ function manifestPath(root) {
|
|
|
5302
5875
|
if (existsSync6(extPath)) return { kind: "extension", path: extPath };
|
|
5303
5876
|
return { kind: "server", path: join7(root, "mcp.json") };
|
|
5304
5877
|
}
|
|
5305
|
-
var
|
|
5878
|
+
var SERVER_MANIFEST_NAMES2 = ["mcp.json", "mcp-server.json"];
|
|
5306
5879
|
function isObject(v) {
|
|
5307
5880
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5308
5881
|
}
|
|
@@ -5316,19 +5889,34 @@ function classifyRootJson(json) {
|
|
|
5316
5889
|
}
|
|
5317
5890
|
function rootCandidates(root) {
|
|
5318
5891
|
const out = [];
|
|
5319
|
-
for (const name of
|
|
5892
|
+
for (const name of SERVER_MANIFEST_NAMES2) {
|
|
5320
5893
|
const path = join7(root, name);
|
|
5321
5894
|
if (!existsSync6(path)) continue;
|
|
5322
|
-
const json = readJson(path);
|
|
5323
|
-
out.push({
|
|
5895
|
+
const { json, readError } = readJson(path);
|
|
5896
|
+
out.push({
|
|
5897
|
+
name,
|
|
5898
|
+
path,
|
|
5899
|
+
role: classifyRootJson(json),
|
|
5900
|
+
json,
|
|
5901
|
+
...readError ? { readError } : {}
|
|
5902
|
+
});
|
|
5324
5903
|
}
|
|
5325
5904
|
return out;
|
|
5326
5905
|
}
|
|
5327
5906
|
function readJson(path) {
|
|
5907
|
+
let text;
|
|
5908
|
+
try {
|
|
5909
|
+
if (!statSync8(path).isFile()) {
|
|
5910
|
+
return { json: null, readError: "not a regular file \u2014 not read" };
|
|
5911
|
+
}
|
|
5912
|
+
text = readFileSync7(path, "utf8");
|
|
5913
|
+
} catch (err) {
|
|
5914
|
+
return { json: null, readError: `could not be read (${fsErrorCode(err)})` };
|
|
5915
|
+
}
|
|
5328
5916
|
try {
|
|
5329
|
-
return JSON.parse(
|
|
5917
|
+
return { json: JSON.parse(text) };
|
|
5330
5918
|
} catch {
|
|
5331
|
-
return null;
|
|
5919
|
+
return { json: null };
|
|
5332
5920
|
}
|
|
5333
5921
|
}
|
|
5334
5922
|
function c18PositiveControls() {
|
|
@@ -5387,6 +5975,14 @@ var c18StructureManifestInner = {
|
|
|
5387
5975
|
let manifestName = kind === "extension" ? "extension.json" : "mcp.json";
|
|
5388
5976
|
if (kind === "server") {
|
|
5389
5977
|
const cands = rootCandidates(tree.root);
|
|
5978
|
+
const unread = cands.find((c) => c.readError !== void 0);
|
|
5979
|
+
if (unread) {
|
|
5980
|
+
return couldNotJudge(
|
|
5981
|
+
this.id,
|
|
5982
|
+
unread.path,
|
|
5983
|
+
`${unread.name} is present but ${unread.readError} \u2014 the server manifest cannot be judged (C-18)`
|
|
5984
|
+
);
|
|
5985
|
+
}
|
|
5390
5986
|
const bad = cands.find((c) => c.role === "unparseable");
|
|
5391
5987
|
if (bad) {
|
|
5392
5988
|
return {
|
|
@@ -5427,7 +6023,15 @@ var c18StructureManifestInner = {
|
|
|
5427
6023
|
verdict: "no extension.json found at the tree root -> the extension manifest is required (C-18)"
|
|
5428
6024
|
};
|
|
5429
6025
|
}
|
|
5430
|
-
|
|
6026
|
+
const read = readJson(path);
|
|
6027
|
+
if (read.readError !== void 0) {
|
|
6028
|
+
return couldNotJudge(
|
|
6029
|
+
this.id,
|
|
6030
|
+
path,
|
|
6031
|
+
`${manifestName} is present but ${read.readError} \u2014 the manifest cannot be judged (C-18)`
|
|
6032
|
+
);
|
|
6033
|
+
}
|
|
6034
|
+
manifest = read.json;
|
|
5431
6035
|
if (!manifest) {
|
|
5432
6036
|
return {
|
|
5433
6037
|
id: this.id,
|
|
@@ -5872,7 +6476,7 @@ var c21ProgressiveDisclosure = {
|
|
|
5872
6476
|
};
|
|
5873
6477
|
|
|
5874
6478
|
// src/detectors/c22-test-ratio-gate.ts
|
|
5875
|
-
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
6479
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, statSync as statSync9 } from "fs";
|
|
5876
6480
|
import { join as join8 } from "path";
|
|
5877
6481
|
var DOC_FILES2 = ["README.md", "README.fr.md", "CHANGELOG.md"];
|
|
5878
6482
|
var RATIO_RE = /\b(?:coverage|ratio de tests?|test ratio|couverture)\b[^\n%]{0,40}?(\d{1,3}(?:\.\d+)?\s?%)/i;
|
|
@@ -5895,15 +6499,20 @@ function c22PositiveControls() {
|
|
|
5895
6499
|
identifierClearsBareRatio: !IDENTIFIER_RE.test(bareSample)
|
|
5896
6500
|
};
|
|
5897
6501
|
}
|
|
5898
|
-
function findRatioCitations(root) {
|
|
6502
|
+
function findRatioCitations(root, unreadable) {
|
|
5899
6503
|
const hits = [];
|
|
5900
6504
|
for (const name of DOC_FILES2) {
|
|
5901
6505
|
const path = join8(root, name);
|
|
5902
6506
|
if (!existsSync7(path)) continue;
|
|
5903
6507
|
let text;
|
|
5904
6508
|
try {
|
|
6509
|
+
if (!statSync9(path).isFile()) {
|
|
6510
|
+
unreadable.push({ anchor: name, reason: `${name} is not a regular file \u2014 not read` });
|
|
6511
|
+
continue;
|
|
6512
|
+
}
|
|
5905
6513
|
text = readFileSync8(path, "utf8");
|
|
5906
|
-
} catch {
|
|
6514
|
+
} catch (err) {
|
|
6515
|
+
unreadable.push({ anchor: name, reason: `${name} could not be read (${fsErrorCode(err)})` });
|
|
5907
6516
|
continue;
|
|
5908
6517
|
}
|
|
5909
6518
|
const lines = text.split("\n");
|
|
@@ -5921,44 +6530,48 @@ var c22TestRatioGate = {
|
|
|
5921
6530
|
rule: "C-22",
|
|
5922
6531
|
pin: STANDARD_PIN,
|
|
5923
6532
|
run(tree) {
|
|
5924
|
-
const
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
6533
|
+
const unreadable = [];
|
|
6534
|
+
const decided = (() => {
|
|
6535
|
+
const controls = c22PositiveControls();
|
|
6536
|
+
if (!controls.ratioMatcher.matched || !controls.identifierMatcher.matched) {
|
|
6537
|
+
throw new Error(
|
|
6538
|
+
`C22 positive control FAILED: ratio=${controls.ratioMatcher.matched} identifier=${controls.identifierMatcher.matched} \u2014 matcher broken, verdict untrustworthy`
|
|
6539
|
+
);
|
|
6540
|
+
}
|
|
6541
|
+
if (!controls.identifierClearsBareRatio) {
|
|
6542
|
+
throw new Error(
|
|
6543
|
+
"C22 identifier-matcher control FAILED: must NOT flag a bare ratio with no identifier token \u2014 verdict untrustworthy"
|
|
6544
|
+
);
|
|
6545
|
+
}
|
|
6546
|
+
const hits = findRatioCitations(tree.root, unreadable);
|
|
6547
|
+
if (hits.length === 0) {
|
|
6548
|
+
return {
|
|
6549
|
+
id: this.id,
|
|
6550
|
+
anchor: "-",
|
|
6551
|
+
state: "absent",
|
|
6552
|
+
verdict: "no test/coverage ratio cited in README/CHANGELOG; test-ratio-gate check n/a"
|
|
6553
|
+
};
|
|
6554
|
+
}
|
|
6555
|
+
const bare = hits.find((h) => !h.hasIdentifier);
|
|
6556
|
+
if (bare) {
|
|
6557
|
+
return {
|
|
6558
|
+
id: this.id,
|
|
6559
|
+
anchor: `${bare.file}:${bare.line}`,
|
|
6560
|
+
state: "non-conforming",
|
|
6561
|
+
verdict: `ratio cited at ${bare.file}:${bare.line} ("${bare.raw.trim()}") carries no passage-identifier token (positive-control OK) -> cite the automatic run/commit identifier the ratio was produced by (C-22)`
|
|
6562
|
+
};
|
|
6563
|
+
}
|
|
6564
|
+
const withId = hits[0];
|
|
6565
|
+
const reason = `ratio cited at ${withId.file}:${withId.line} carries an identifier-shaped token, but whether it RESOLVES against a live CI run is the gate half of C-22 \u2014 this static-only run has no CI query surface and cannot confirm resolution`;
|
|
5946
6566
|
return {
|
|
5947
6567
|
id: this.id,
|
|
5948
|
-
anchor: `${
|
|
5949
|
-
state: "
|
|
5950
|
-
|
|
6568
|
+
anchor: `${withId.file}:${withId.line}`,
|
|
6569
|
+
state: "could-not-judge",
|
|
6570
|
+
reason,
|
|
6571
|
+
verdict: `could not judge: ${reason}`
|
|
5951
6572
|
};
|
|
5952
|
-
}
|
|
5953
|
-
|
|
5954
|
-
const reason = `ratio cited at ${withId.file}:${withId.line} carries an identifier-shaped token, but whether it RESOLVES against a live CI run is the gate half of C-22 \u2014 this static-only run has no CI query surface and cannot confirm resolution`;
|
|
5955
|
-
return {
|
|
5956
|
-
id: this.id,
|
|
5957
|
-
anchor: `${withId.file}:${withId.line}`,
|
|
5958
|
-
state: "could-not-judge",
|
|
5959
|
-
reason,
|
|
5960
|
-
verdict: `could not judge: ${reason}`
|
|
5961
|
-
};
|
|
6573
|
+
})();
|
|
6574
|
+
return settle(this.id, decided, unreadable);
|
|
5962
6575
|
}
|
|
5963
6576
|
};
|
|
5964
6577
|
|
|
@@ -6453,7 +7066,7 @@ import {
|
|
|
6453
7066
|
readFileSync as readFileSync9,
|
|
6454
7067
|
readdirSync as readdirSync6,
|
|
6455
7068
|
rmSync,
|
|
6456
|
-
statSync as
|
|
7069
|
+
statSync as statSync10,
|
|
6457
7070
|
writeFileSync
|
|
6458
7071
|
} from "fs";
|
|
6459
7072
|
import { tmpdir } from "os";
|
|
@@ -6479,15 +7092,17 @@ function walkForConfigs(dir, depth, seen, onSkip) {
|
|
|
6479
7092
|
let entries;
|
|
6480
7093
|
try {
|
|
6481
7094
|
entries = readdirSync6(dir);
|
|
6482
|
-
} catch {
|
|
7095
|
+
} catch (err) {
|
|
7096
|
+
onSkip(dir, depth, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
6483
7097
|
return;
|
|
6484
7098
|
}
|
|
6485
7099
|
for (const name of entries) {
|
|
6486
7100
|
const full = join9(dir, name);
|
|
6487
7101
|
let st;
|
|
6488
7102
|
try {
|
|
6489
|
-
st =
|
|
6490
|
-
} catch {
|
|
7103
|
+
st = statSync10(full);
|
|
7104
|
+
} catch (err) {
|
|
7105
|
+
onSkip(full, depth + 1, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
6491
7106
|
continue;
|
|
6492
7107
|
}
|
|
6493
7108
|
if (st.isDirectory()) {
|
|
@@ -6497,6 +7112,10 @@ function walkForConfigs(dir, depth, seen, onSkip) {
|
|
|
6497
7112
|
}
|
|
6498
7113
|
walkForConfigs(full, depth + 1, seen, onSkip);
|
|
6499
7114
|
} else if (DEPLOY_CONFIG_FILES.includes(name)) {
|
|
7115
|
+
if (!st.isFile()) {
|
|
7116
|
+
onSkip(full, depth + 1, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
7117
|
+
continue;
|
|
7118
|
+
}
|
|
6500
7119
|
if (!seen.has(dir)) {
|
|
6501
7120
|
seen.set(dir, { dir, configFile: name, configPath: full });
|
|
6502
7121
|
}
|
|
@@ -6516,7 +7135,7 @@ function probeForCandidate(dir, depth) {
|
|
|
6516
7135
|
const full = join9(dir, name);
|
|
6517
7136
|
let st;
|
|
6518
7137
|
try {
|
|
6519
|
-
st =
|
|
7138
|
+
st = statSync10(full);
|
|
6520
7139
|
} catch {
|
|
6521
7140
|
continue;
|
|
6522
7141
|
}
|
|
@@ -6558,12 +7177,12 @@ function resolveRailwayRootDirectory(configPath) {
|
|
|
6558
7177
|
function c26DeriveScope(root, tree) {
|
|
6559
7178
|
const seen = /* @__PURE__ */ new Map();
|
|
6560
7179
|
const skipped = [];
|
|
6561
|
-
const onSkip = (skippedDir, depth) => {
|
|
6562
|
-
const entry = tree ? recordSkip(tree, skippedDir, root) : {
|
|
7180
|
+
const onSkip = (skippedDir, depth, reason) => {
|
|
7181
|
+
const entry = tree ? recordSkip(tree, skippedDir, root, reason) : {
|
|
6563
7182
|
path: relative5(root, skippedDir).split(sep4).join("/"),
|
|
6564
|
-
reason: SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
7183
|
+
reason: reason ?? SKIP_DIR_INFO[basename2(skippedDir)]?.reason ?? "skipped"
|
|
6565
7184
|
};
|
|
6566
|
-
if (SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
7185
|
+
if (reason === void 0 && SKIP_DIR_INFO[basename2(skippedDir)]?.authored) {
|
|
6567
7186
|
const hit = probeForCandidate(skippedDir, depth);
|
|
6568
7187
|
if (hit) entry.candidate = relative5(root, hit).split(sep4).join("/");
|
|
6569
7188
|
}
|
|
@@ -6689,8 +7308,22 @@ function gateSite(site) {
|
|
|
6689
7308
|
}
|
|
6690
7309
|
const isolated = mkdtempSync(join9(tmpdir(), "mcp-doctor-c26-"));
|
|
6691
7310
|
try {
|
|
6692
|
-
|
|
6693
|
-
|
|
7311
|
+
let manifestText;
|
|
7312
|
+
let lockText;
|
|
7313
|
+
const lockPath = join9(site.dir, lock.name);
|
|
7314
|
+
try {
|
|
7315
|
+
for (const p of [pkgPath, lockPath]) {
|
|
7316
|
+
if (!statSync10(p).isFile()) throw new Error(`${p} is not a regular file`);
|
|
7317
|
+
}
|
|
7318
|
+
manifestText = readFileSync9(pkgPath, "utf8");
|
|
7319
|
+
lockText = readFileSync9(lockPath, "utf8");
|
|
7320
|
+
} catch (err) {
|
|
7321
|
+
return {
|
|
7322
|
+
site,
|
|
7323
|
+
status: "unreadable",
|
|
7324
|
+
reason: `${site.dir}: package.json or ${lock.name} is present but cannot be read (${err instanceof Error ? err.message : String(err)}) \u2014 the frozen install was not run`
|
|
7325
|
+
};
|
|
7326
|
+
}
|
|
6694
7327
|
writeFileSync(join9(isolated, "package.json"), manifestText);
|
|
6695
7328
|
writeFileSync(join9(isolated, lock.name), lockText);
|
|
6696
7329
|
let proc;
|
|
@@ -6751,6 +7384,7 @@ function renderCoverage(scope, verdicts) {
|
|
|
6751
7384
|
return verdicts.map((v) => {
|
|
6752
7385
|
const rel = relative5(process.cwd(), v.site.dir) || v.site.dir;
|
|
6753
7386
|
if (v.status === "skipped") return `SKIPPED ${rel} (${v.reason})`;
|
|
7387
|
+
if (v.status === "unreadable") return `UNREAD ${rel} (${v.reason})`;
|
|
6754
7388
|
return `ANALYSED ${rel} -> ${v.ok ? "GREEN" : "RED"}: ${v.detail}`;
|
|
6755
7389
|
}).join(" | ");
|
|
6756
7390
|
}
|
|
@@ -6793,6 +7427,17 @@ var c26SubpackageLockfile = {
|
|
|
6793
7427
|
const verdicts = scope.sites.map(gateSite);
|
|
6794
7428
|
const inventory = renderCoverage(scope.sites, verdicts);
|
|
6795
7429
|
const analysed = verdicts.filter((v) => v.status === "analysed");
|
|
7430
|
+
const unread = verdicts.filter((v) => v.status === "unreadable");
|
|
7431
|
+
if (unread.length > 0 && !analysed.some((v) => v.ok === false)) {
|
|
7432
|
+
const reason = `${unread.length} deployable site(s) could not be read \u2014 ${unread.map((v) => v.reason).join("; ")}. Coverage: ${inventory}`;
|
|
7433
|
+
return {
|
|
7434
|
+
id: this.id,
|
|
7435
|
+
anchor: unread[0].site.dir,
|
|
7436
|
+
state: "could-not-judge",
|
|
7437
|
+
reason,
|
|
7438
|
+
verdict: `could not judge: ${reason}`
|
|
7439
|
+
};
|
|
7440
|
+
}
|
|
6796
7441
|
if (analysed.length === 0) {
|
|
6797
7442
|
if (hidden.length > 0) return unjudgedHidden("no analysable site; ");
|
|
6798
7443
|
return {
|
|
@@ -6932,22 +7577,21 @@ var c27SubpackageTypes = {
|
|
|
6932
7577
|
const workflows = governingWorkflows(tree);
|
|
6933
7578
|
const reds = [];
|
|
6934
7579
|
const oks = [];
|
|
7580
|
+
const unreadDocs = [];
|
|
6935
7581
|
for (const site of tsSites) {
|
|
6936
7582
|
const siteRel = relative6(base, site.dir) || ".";
|
|
6937
7583
|
const docs = [];
|
|
6938
7584
|
const dockerfile = join10(site.dir, "Dockerfile");
|
|
6939
7585
|
if (existsSync9(dockerfile)) {
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
});
|
|
7586
|
+
const text = readText(dockerfile);
|
|
7587
|
+
if (text === null) unreadDocs.push(`${siteRel}/Dockerfile`);
|
|
7588
|
+
else docs.push({ where: `${siteRel}/Dockerfile`, segments: dockerSegments(text) });
|
|
6944
7589
|
}
|
|
6945
7590
|
const railway = join10(site.dir, "railway.json");
|
|
6946
7591
|
if (existsSync9(railway)) {
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
});
|
|
7592
|
+
const text = readText(railway);
|
|
7593
|
+
if (text === null) unreadDocs.push(`${siteRel}/railway.json`);
|
|
7594
|
+
else docs.push({ where: `${siteRel}/railway.json`, segments: railwayBuildCommand(text) });
|
|
6951
7595
|
}
|
|
6952
7596
|
for (const w of workflows) {
|
|
6953
7597
|
if (siteRel === "." || w.text.includes(siteRel)) {
|
|
@@ -6960,6 +7604,7 @@ var c27SubpackageTypes = {
|
|
|
6960
7604
|
oks.push(`${ok.d.where} (${siteRel})`);
|
|
6961
7605
|
continue;
|
|
6962
7606
|
}
|
|
7607
|
+
if (unreadDocs.some((u) => u.startsWith(`${siteRel}/`))) continue;
|
|
6963
7608
|
const unfrozen = verdicts.find((v) => v.phase === "unfrozen");
|
|
6964
7609
|
const early = verdicts.find((v) => v.phase === "before-install");
|
|
6965
7610
|
const why = unfrozen ? `a type check follows a non-frozen install at ${unfrozen.d.where} (the tree is not the lockfile's: use a frozen install)` : early ? `a type check at ${early.d.where} runs before any install (it checks the workspace's tree, not the isolation's)` : "no type check runs in the deployment isolation (no tsc / typecheck step after a frozen install)";
|
|
@@ -6972,6 +7617,13 @@ var c27SubpackageTypes = {
|
|
|
6972
7617
|
`${reds.length} TypeScript deployable(s): ${reds.join("; ")} -> run the sub-package type check inside the isolation the deployment installs, after the frozen install (C-27)`
|
|
6973
7618
|
);
|
|
6974
7619
|
}
|
|
7620
|
+
if (unreadDocs.length > 0) {
|
|
7621
|
+
return couldNotJudge(
|
|
7622
|
+
this.id,
|
|
7623
|
+
`${unreadDocs[0]}:1`,
|
|
7624
|
+
`${unreadDocs.map((u) => `\`${u}\``).join(", ")} is present but cannot be read (permission, or not a regular file): whether the deployment type-checks in its isolation is unread \u2014 never "no type check" (C-27)`
|
|
7625
|
+
);
|
|
7626
|
+
}
|
|
6975
7627
|
return couldNotJudge(
|
|
6976
7628
|
this.id,
|
|
6977
7629
|
oks[0],
|
|
@@ -7201,7 +7853,7 @@ var c30SkillsOrgBound = {
|
|
|
7201
7853
|
};
|
|
7202
7854
|
|
|
7203
7855
|
// src/detectors/c31-plugin-installable.ts
|
|
7204
|
-
import { existsSync as existsSync10, readdirSync as readdirSync8, statSync as
|
|
7856
|
+
import { existsSync as existsSync10, readdirSync as readdirSync8, statSync as statSync11 } from "fs";
|
|
7205
7857
|
import { basename as basename3, dirname as dirname4, join as join11, relative as relative8, sep as sep5 } from "path";
|
|
7206
7858
|
var MIN_README_WORDS = 40;
|
|
7207
7859
|
var KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -7402,12 +8054,17 @@ function judgeFolder(tree, folder, manifestPath2, out) {
|
|
|
7402
8054
|
}
|
|
7403
8055
|
const hasLicenseField = typeof manifest.license === "string" && manifest.license.trim() !== "";
|
|
7404
8056
|
let hasLicenseFile = false;
|
|
8057
|
+
let folderListed = true;
|
|
7405
8058
|
try {
|
|
7406
8059
|
hasLicenseFile = readdirSync8(folder).some((n) => /^LICEN[CS]E/i.test(n));
|
|
7407
|
-
} catch {
|
|
7408
|
-
|
|
8060
|
+
} catch (err) {
|
|
8061
|
+
folderListed = false;
|
|
8062
|
+
out.unreadable.push({
|
|
8063
|
+
anchor: rel(folder),
|
|
8064
|
+
reason: `${rel(folder)} cannot be listed (${fsErrorCode(err)}) \u2014 whether it carries a LICENSE file is unread`
|
|
8065
|
+
});
|
|
7409
8066
|
}
|
|
7410
|
-
if (!hasLicenseField && !hasLicenseFile) {
|
|
8067
|
+
if (!hasLicenseField && !hasLicenseFile && folderListed) {
|
|
7411
8068
|
out.reds.push({
|
|
7412
8069
|
anchor: a,
|
|
7413
8070
|
text: "no licence: plugin.json has no `license` and the folder has no LICENSE file"
|
|
@@ -7415,7 +8072,12 @@ function judgeFolder(tree, folder, manifestPath2, out) {
|
|
|
7415
8072
|
}
|
|
7416
8073
|
const readmePath = join11(folder, "README.md");
|
|
7417
8074
|
const readme = readText(readmePath);
|
|
7418
|
-
if (readme === null) {
|
|
8075
|
+
if (readme === null && existsSync10(readmePath)) {
|
|
8076
|
+
out.unreadable.push({
|
|
8077
|
+
anchor: rel(readmePath),
|
|
8078
|
+
reason: `${rel(readmePath)} is present but cannot be read as text (permission, or not a regular file) \u2014 its length and credential content are unread`
|
|
8079
|
+
});
|
|
8080
|
+
} else if (readme === null) {
|
|
7419
8081
|
out.reds.push({ anchor: rel(folder), text: "no README.md at the plugin folder root" });
|
|
7420
8082
|
} else {
|
|
7421
8083
|
const n = proseWordCount(readme);
|
|
@@ -7475,13 +8137,20 @@ function judgeFolder(tree, folder, manifestPath2, out) {
|
|
|
7475
8137
|
judgeServers(servers, mcpAnchor, out);
|
|
7476
8138
|
}
|
|
7477
8139
|
try {
|
|
7478
|
-
if (
|
|
8140
|
+
if (statSync11(join11(folder, "bin")).isDirectory()) {
|
|
7479
8141
|
out.reds.push({
|
|
7480
8142
|
anchor: rel(join11(folder, "bin")),
|
|
7481
8143
|
text: "a top-level bin/ directory (a plugin ships no bin/ at its top level)"
|
|
7482
8144
|
});
|
|
7483
8145
|
}
|
|
7484
|
-
} catch {
|
|
8146
|
+
} catch (err) {
|
|
8147
|
+
const code = fsErrorCode(err);
|
|
8148
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
8149
|
+
out.unreadable.push({
|
|
8150
|
+
anchor: rel(join11(folder, "bin")),
|
|
8151
|
+
reason: `${rel(join11(folder, "bin"))} could not be probed (${code}) \u2014 whether the plugin ships a top-level bin/ is unread`
|
|
8152
|
+
});
|
|
8153
|
+
}
|
|
7485
8154
|
}
|
|
7486
8155
|
const jsons = [[a, manifest]];
|
|
7487
8156
|
if (servers) jsons.push([mcpAnchor, servers]);
|
|
@@ -8261,7 +8930,7 @@ var d7Inner = {
|
|
|
8261
8930
|
var d7 = guardUnreadablePackageJson(d7Inner, { authoritative: true });
|
|
8262
8931
|
|
|
8263
8932
|
// src/detectors/d8.ts
|
|
8264
|
-
import { existsSync as existsSync11, statSync as
|
|
8933
|
+
import { existsSync as existsSync11, statSync as statSync12 } from "fs";
|
|
8265
8934
|
import { dirname as dirname5, join as join12, relative as relative9 } from "path";
|
|
8266
8935
|
var POSITIVE_CONTROL2 = 'server.registerTool("list_items", { ... }, handler);';
|
|
8267
8936
|
function d8PositiveControl() {
|
|
@@ -8274,7 +8943,7 @@ function d8PositiveControl() {
|
|
|
8274
8943
|
function resolveBaseDir(tree) {
|
|
8275
8944
|
if (tree.packageJson) return dirname5(tree.packageJson.path);
|
|
8276
8945
|
try {
|
|
8277
|
-
return
|
|
8946
|
+
return statSync12(tree.root).isDirectory() ? tree.root : dirname5(tree.root);
|
|
8278
8947
|
} catch {
|
|
8279
8948
|
return tree.root;
|
|
8280
8949
|
}
|
|
@@ -8408,6 +9077,13 @@ var d8 = {
|
|
|
8408
9077
|
verdict: "server advertises no tools at runtime -> n/a for eval coverage"
|
|
8409
9078
|
};
|
|
8410
9079
|
}
|
|
9080
|
+
if (runtime.evalsUnreadable) {
|
|
9081
|
+
return couldNotJudge(
|
|
9082
|
+
this.id,
|
|
9083
|
+
`${runtime.root} (runtime tools/list)`,
|
|
9084
|
+
`the eval file is present but unread \u2014 ${runtime.evalsUnreadable}; per-tool coverage is unknown, never zero (Server Rule 5)`
|
|
9085
|
+
);
|
|
9086
|
+
}
|
|
8411
9087
|
const summary = runtime.tools.map((t) => `${t.name}=${t.evalCaseCount ?? 0}`).join(" ");
|
|
8412
9088
|
const under = runtime.tools.filter((t) => (t.evalCaseCount ?? 0) < 3);
|
|
8413
9089
|
if (under.length > 0) {
|
|
@@ -9033,7 +9709,7 @@ var d15 = {
|
|
|
9033
9709
|
};
|
|
9034
9710
|
|
|
9035
9711
|
// src/detectors/d16.ts
|
|
9036
|
-
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as
|
|
9712
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync13 } from "fs";
|
|
9037
9713
|
import { dirname as dirname8, join as join15 } from "path";
|
|
9038
9714
|
var DISCOVER_RE2 = /server\/discover|DiscoverResult|ServerDiscover|handleServerDiscover/;
|
|
9039
9715
|
var HEALTH_ROUTE_FRAMEWORK_RE = /\b[A-Za-z_$][\w$]*\.(?:get|post|put|patch|delete|all|route|use)\s*\(\s*["'`][^"'`]*\/health/;
|
|
@@ -9355,10 +10031,11 @@ var d16 = {
|
|
|
9355
10031
|
};
|
|
9356
10032
|
function readPackageVersion(root) {
|
|
9357
10033
|
try {
|
|
9358
|
-
let dir =
|
|
10034
|
+
let dir = statSync13(root).isDirectory() ? root : dirname8(root);
|
|
9359
10035
|
for (let depth = 0; depth < 8; depth++) {
|
|
9360
10036
|
const candidate = join15(dir, "package.json");
|
|
9361
10037
|
if (existsSync14(candidate)) {
|
|
10038
|
+
if (!statSync13(candidate).isFile()) return null;
|
|
9362
10039
|
const pkg = JSON.parse(readFileSync10(candidate, "utf8"));
|
|
9363
10040
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
9364
10041
|
}
|
|
@@ -9373,7 +10050,7 @@ function readPackageVersion(root) {
|
|
|
9373
10050
|
}
|
|
9374
10051
|
|
|
9375
10052
|
// src/detectors/dAppI18n.ts
|
|
9376
|
-
import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
|
|
10053
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync14 } from "fs";
|
|
9377
10054
|
import { dirname as dirname9, join as join16 } from "path";
|
|
9378
10055
|
function flattenKeys(obj, prefix = "") {
|
|
9379
10056
|
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
@@ -9407,6 +10084,7 @@ function dAppI18nPositiveControls() {
|
|
|
9407
10084
|
}
|
|
9408
10085
|
function readJson2(path) {
|
|
9409
10086
|
try {
|
|
10087
|
+
if (!statSync14(path).isFile()) return { ok: false };
|
|
9410
10088
|
return { ok: true, value: JSON.parse(readFileSync11(path, "utf8")) };
|
|
9411
10089
|
} catch {
|
|
9412
10090
|
return { ok: false };
|
|
@@ -9605,14 +10283,14 @@ var dDebugOptIn = {
|
|
|
9605
10283
|
};
|
|
9606
10284
|
|
|
9607
10285
|
// src/detectors/extManifests.ts
|
|
9608
|
-
import { readdirSync as readdirSync9, statSync as
|
|
10286
|
+
import { readdirSync as readdirSync9, statSync as statSync15 } from "fs";
|
|
9609
10287
|
import { join as join17 } from "path";
|
|
9610
10288
|
function findExtensionManifests(tree) {
|
|
9611
10289
|
const out = [];
|
|
9612
10290
|
const root = tree.root;
|
|
9613
10291
|
let base;
|
|
9614
10292
|
try {
|
|
9615
|
-
base =
|
|
10293
|
+
base = statSync15(root).isDirectory() ? root : join17(root, "..");
|
|
9616
10294
|
} catch {
|
|
9617
10295
|
return out;
|
|
9618
10296
|
}
|
|
@@ -9620,15 +10298,17 @@ function findExtensionManifests(tree) {
|
|
|
9620
10298
|
let entries;
|
|
9621
10299
|
try {
|
|
9622
10300
|
entries = readdirSync9(dir);
|
|
9623
|
-
} catch {
|
|
10301
|
+
} catch (err) {
|
|
10302
|
+
recordSkip(tree, dir, base, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
9624
10303
|
return;
|
|
9625
10304
|
}
|
|
9626
10305
|
for (const name of entries) {
|
|
9627
10306
|
const full = join17(dir, name);
|
|
9628
10307
|
let st;
|
|
9629
10308
|
try {
|
|
9630
|
-
st =
|
|
9631
|
-
} catch {
|
|
10309
|
+
st = statSync15(full);
|
|
10310
|
+
} catch (err) {
|
|
10311
|
+
recordSkip(tree, full, base, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
9632
10312
|
continue;
|
|
9633
10313
|
}
|
|
9634
10314
|
if (st.isDirectory()) {
|
|
@@ -9638,6 +10318,10 @@ function findExtensionManifests(tree) {
|
|
|
9638
10318
|
}
|
|
9639
10319
|
walk2(full);
|
|
9640
10320
|
} else if (name === "extension.json") {
|
|
10321
|
+
if (!st.isFile()) {
|
|
10322
|
+
recordSkip(tree, full, base, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
10323
|
+
continue;
|
|
10324
|
+
}
|
|
9641
10325
|
out.push(full);
|
|
9642
10326
|
}
|
|
9643
10327
|
}
|
|
@@ -10254,8 +10938,8 @@ var dExt3 = {
|
|
|
10254
10938
|
};
|
|
10255
10939
|
|
|
10256
10940
|
// src/detectors/dExt4.ts
|
|
10257
|
-
import { readdirSync as readdirSync10, statSync as
|
|
10258
|
-
import { join as join19 } from "path";
|
|
10941
|
+
import { readdirSync as readdirSync10, statSync as statSync16 } from "fs";
|
|
10942
|
+
import { join as join19, relative as relative10 } from "path";
|
|
10259
10943
|
function canonical(name) {
|
|
10260
10944
|
return name.replace(/[-_]/g, "").toLowerCase();
|
|
10261
10945
|
}
|
|
@@ -10274,28 +10958,39 @@ function dExt4PositiveControls() {
|
|
|
10274
10958
|
}
|
|
10275
10959
|
};
|
|
10276
10960
|
}
|
|
10277
|
-
function findHooksDirs(tree) {
|
|
10961
|
+
function findHooksDirs(tree, unreadable, gaps) {
|
|
10278
10962
|
const root = tree.root;
|
|
10279
10963
|
const out = [];
|
|
10280
10964
|
let base;
|
|
10281
10965
|
try {
|
|
10282
|
-
base =
|
|
10966
|
+
base = statSync16(root).isDirectory() ? root : join19(root, "..");
|
|
10283
10967
|
} catch {
|
|
10284
10968
|
return out;
|
|
10285
10969
|
}
|
|
10970
|
+
const noteUnread = (path, what, err) => {
|
|
10971
|
+
gaps.count++;
|
|
10972
|
+
const anchor = relative10(base, path) || ".";
|
|
10973
|
+
const reason = `${anchor} ${what} (${fsErrorCode(err)}) \u2014 a hooks/ directory under it may exist unseen, so the implemented hooks are unverifiable (Extension Rule 4)`;
|
|
10974
|
+
if (!unreadable.some((u) => u.anchor === anchor && u.reason === reason)) {
|
|
10975
|
+
unreadable.push({ anchor, reason });
|
|
10976
|
+
}
|
|
10977
|
+
};
|
|
10286
10978
|
const walk2 = (dir) => {
|
|
10287
10979
|
let entries;
|
|
10288
10980
|
try {
|
|
10289
10981
|
entries = readdirSync10(dir);
|
|
10290
|
-
} catch {
|
|
10982
|
+
} catch (err) {
|
|
10983
|
+
noteUnread(dir, "could not be listed", err);
|
|
10291
10984
|
return;
|
|
10292
10985
|
}
|
|
10293
10986
|
for (const name of entries) {
|
|
10294
10987
|
const full = join19(dir, name);
|
|
10295
10988
|
let st;
|
|
10296
10989
|
try {
|
|
10297
|
-
st =
|
|
10298
|
-
} catch {
|
|
10990
|
+
st = statSync16(full);
|
|
10991
|
+
} catch (err) {
|
|
10992
|
+
if (fsErrorCode(err) !== "ENOENT" && fsErrorCode(err) !== "ELOOP")
|
|
10993
|
+
noteUnread(full, "could not be read", err);
|
|
10299
10994
|
continue;
|
|
10300
10995
|
}
|
|
10301
10996
|
if (!st.isDirectory()) continue;
|
|
@@ -10308,7 +11003,8 @@ function findHooksDirs(tree) {
|
|
|
10308
11003
|
let files;
|
|
10309
11004
|
try {
|
|
10310
11005
|
files = readdirSync10(full);
|
|
10311
|
-
} catch {
|
|
11006
|
+
} catch (err) {
|
|
11007
|
+
noteUnread(full, "could not be listed", err);
|
|
10312
11008
|
files = [];
|
|
10313
11009
|
}
|
|
10314
11010
|
for (const f of files) {
|
|
@@ -10386,7 +11082,8 @@ var dExt4 = {
|
|
|
10386
11082
|
}
|
|
10387
11083
|
const unreadable = [];
|
|
10388
11084
|
const decided = (() => {
|
|
10389
|
-
const
|
|
11085
|
+
const hooksDirGaps = { count: 0 };
|
|
11086
|
+
const hooksDirs = findHooksDirs(tree, unreadable, hooksDirGaps);
|
|
10390
11087
|
const { sites, unresolved } = findFactorySites(tree);
|
|
10391
11088
|
for (const anchor of unresolved) {
|
|
10392
11089
|
unreadable.push({
|
|
@@ -10430,6 +11127,7 @@ var dExt4 = {
|
|
|
10430
11127
|
continue;
|
|
10431
11128
|
}
|
|
10432
11129
|
const phantom = [...declared].filter((h) => !implemented.has(h));
|
|
11130
|
+
if (phantom.length > 0 && hooksDirGaps.count > 0) continue;
|
|
10433
11131
|
if (phantom.length > 0) {
|
|
10434
11132
|
return {
|
|
10435
11133
|
id: this.id,
|
|
@@ -10817,30 +11515,41 @@ var dExt8 = {
|
|
|
10817
11515
|
};
|
|
10818
11516
|
|
|
10819
11517
|
// src/detectors/dExt9.ts
|
|
10820
|
-
import { readFileSync as readFileSync13, readdirSync as readdirSync11, statSync as
|
|
10821
|
-
import { join as join23 } from "path";
|
|
10822
|
-
function collectHooksDirSources(tree, unreadable) {
|
|
11518
|
+
import { readFileSync as readFileSync13, readdirSync as readdirSync11, statSync as statSync17 } from "fs";
|
|
11519
|
+
import { join as join23, relative as relative11 } from "path";
|
|
11520
|
+
function collectHooksDirSources(tree, unreadable, gaps) {
|
|
10823
11521
|
const root = tree.root;
|
|
10824
11522
|
const out = [];
|
|
10825
11523
|
let base;
|
|
10826
11524
|
try {
|
|
10827
|
-
base =
|
|
11525
|
+
base = statSync17(root).isDirectory() ? root : join23(root, "..");
|
|
10828
11526
|
} catch {
|
|
10829
11527
|
return out;
|
|
10830
11528
|
}
|
|
11529
|
+
const noteUnread = (path, what, err) => {
|
|
11530
|
+
gaps.count++;
|
|
11531
|
+
const anchor = relative11(base, path) || ".";
|
|
11532
|
+
const reason = `${anchor} ${what} (${fsErrorCode(err)}) \u2014 a hooks/ directory under it may exist unseen, so the timeout guards are undeterminable (Extension Rule 9)`;
|
|
11533
|
+
if (!unreadable.some((u) => u.anchor === anchor && u.reason === reason)) {
|
|
11534
|
+
unreadable.push({ anchor, reason });
|
|
11535
|
+
}
|
|
11536
|
+
};
|
|
10831
11537
|
const walk2 = (dir) => {
|
|
10832
11538
|
let entries;
|
|
10833
11539
|
try {
|
|
10834
11540
|
entries = readdirSync11(dir);
|
|
10835
|
-
} catch {
|
|
11541
|
+
} catch (err) {
|
|
11542
|
+
noteUnread(dir, "could not be listed", err);
|
|
10836
11543
|
return;
|
|
10837
11544
|
}
|
|
10838
11545
|
for (const name of entries) {
|
|
10839
11546
|
const full = join23(dir, name);
|
|
10840
11547
|
let st;
|
|
10841
11548
|
try {
|
|
10842
|
-
st =
|
|
10843
|
-
} catch {
|
|
11549
|
+
st = statSync17(full);
|
|
11550
|
+
} catch (err) {
|
|
11551
|
+
if (fsErrorCode(err) !== "ENOENT" && fsErrorCode(err) !== "ELOOP")
|
|
11552
|
+
noteUnread(full, "could not be read", err);
|
|
10844
11553
|
continue;
|
|
10845
11554
|
}
|
|
10846
11555
|
if (!st.isDirectory()) continue;
|
|
@@ -10852,7 +11561,8 @@ function collectHooksDirSources(tree, unreadable) {
|
|
|
10852
11561
|
let files;
|
|
10853
11562
|
try {
|
|
10854
11563
|
files = readdirSync11(full);
|
|
10855
|
-
} catch {
|
|
11564
|
+
} catch (err) {
|
|
11565
|
+
noteUnread(full, "could not be listed", err);
|
|
10856
11566
|
files = [];
|
|
10857
11567
|
}
|
|
10858
11568
|
for (const f of files) {
|
|
@@ -10862,6 +11572,7 @@ function collectHooksDirSources(tree, unreadable) {
|
|
|
10862
11572
|
const filePath = join23(full, f);
|
|
10863
11573
|
let body;
|
|
10864
11574
|
try {
|
|
11575
|
+
if (!statSync17(filePath).isFile()) throw new Error("not a regular file");
|
|
10865
11576
|
body = readFileSync13(filePath, "utf8");
|
|
10866
11577
|
} catch {
|
|
10867
11578
|
const shortPath = filePath.split("/").slice(-2).join("/");
|
|
@@ -10965,8 +11676,9 @@ var dExt9 = {
|
|
|
10965
11676
|
}
|
|
10966
11677
|
const unreadable = [];
|
|
10967
11678
|
const decided = (() => {
|
|
11679
|
+
const hooksDirGaps = { count: 0 };
|
|
10968
11680
|
const hookSources = [
|
|
10969
|
-
...collectHooksDirSources(tree, unreadable),
|
|
11681
|
+
...collectHooksDirSources(tree, unreadable, hooksDirGaps),
|
|
10970
11682
|
...collectFactorySources(tree)
|
|
10971
11683
|
];
|
|
10972
11684
|
const implByToken = /* @__PURE__ */ new Map();
|
|
@@ -11015,6 +11727,7 @@ var dExt9 = {
|
|
|
11015
11727
|
const hasImplTimeout = implSources.some((s) => hasImplTimeoutGuard(s.body));
|
|
11016
11728
|
if (!hasManifestTimeout && !hasImplTimeout) noTimeout.push(hook);
|
|
11017
11729
|
}
|
|
11730
|
+
if (noTimeout.length > 0 && hooksDirGaps.count > 0) continue;
|
|
11018
11731
|
if (noTimeout.length > 0) {
|
|
11019
11732
|
return {
|
|
11020
11733
|
id: this.id,
|
|
@@ -12028,7 +12741,6 @@ var dServer14 = {
|
|
|
12028
12741
|
};
|
|
12029
12742
|
|
|
12030
12743
|
// src/detectors/e5-extension-isolation.ts
|
|
12031
|
-
import { readFileSync as readFileSync14 } from "fs";
|
|
12032
12744
|
var GLOBAL_RES = [
|
|
12033
12745
|
/\b(?:globalThis|global|window|self)\s*(?:\.\s*[\w$]+|\[[^\]]+\])\s*=(?!=)/,
|
|
12034
12746
|
/Object\.(?:assign|defineProperty|defineProperties|setPrototypeOf)\(\s*(?:globalThis|global|window|self)\b/,
|
|
@@ -12059,11 +12771,8 @@ var e5ExtensionIsolation = {
|
|
|
12059
12771
|
const manifests = findExtensionManifests(tree);
|
|
12060
12772
|
const own = /* @__PURE__ */ new Set();
|
|
12061
12773
|
for (const m of manifests) {
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
if (typeof j.identifier === "string") own.add(j.identifier);
|
|
12065
|
-
} catch {
|
|
12066
|
-
}
|
|
12774
|
+
const j = readJsonObject(m);
|
|
12775
|
+
if (j && typeof j.identifier === "string") own.add(j.identifier);
|
|
12067
12776
|
}
|
|
12068
12777
|
const defined = tree.files.some(
|
|
12069
12778
|
(f) => isProductSource(f.rel) && uncommentedLines(f).some((r) => /\bdefineExtension\s*\(/.test(r))
|
|
@@ -12493,8 +13202,8 @@ var e10MrtrOnly = {
|
|
|
12493
13202
|
};
|
|
12494
13203
|
|
|
12495
13204
|
// src/detectors/e11-surfaces-derived.ts
|
|
12496
|
-
import { existsSync as existsSync20, statSync as
|
|
12497
|
-
import { dirname as dirname14, join as join24, relative as
|
|
13205
|
+
import { existsSync as existsSync20, statSync as statSync18 } from "fs";
|
|
13206
|
+
import { dirname as dirname14, join as join24, relative as relative12, sep as sep6 } from "path";
|
|
12498
13207
|
var ALL = ["chat", "cowork", "claude-code"];
|
|
12499
13208
|
function excludedSurfaces(c) {
|
|
12500
13209
|
const cause = /* @__PURE__ */ new Map();
|
|
@@ -12510,10 +13219,17 @@ function excludedSurfaces(c) {
|
|
|
12510
13219
|
if (c.localMcp) exclude("chat", "a local MCP command excludes chat");
|
|
12511
13220
|
return cause;
|
|
12512
13221
|
}
|
|
12513
|
-
function isDir(p) {
|
|
13222
|
+
function isDir(p, anchor, unreadable) {
|
|
12514
13223
|
try {
|
|
12515
|
-
return
|
|
12516
|
-
} catch {
|
|
13224
|
+
return statSync18(p).isDirectory();
|
|
13225
|
+
} catch (err) {
|
|
13226
|
+
const code = fsErrorCode(err);
|
|
13227
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
13228
|
+
unreadable.push({
|
|
13229
|
+
anchor,
|
|
13230
|
+
reason: `${anchor} could not be probed (${code}) \u2014 whether this component exists is unread`
|
|
13231
|
+
});
|
|
13232
|
+
}
|
|
12517
13233
|
return false;
|
|
12518
13234
|
}
|
|
12519
13235
|
}
|
|
@@ -12552,7 +13268,7 @@ var e11SurfacesDerived = {
|
|
|
12552
13268
|
continue;
|
|
12553
13269
|
}
|
|
12554
13270
|
const folder = dirname14(m.path);
|
|
12555
|
-
const rel = (p) =>
|
|
13271
|
+
const rel = (p) => relative12(tree.root, p).split(sep6).join("/") || ".";
|
|
12556
13272
|
if (!Array.isArray(j.surfaces)) {
|
|
12557
13273
|
reds.push({
|
|
12558
13274
|
anchor: m.rel,
|
|
@@ -12588,9 +13304,9 @@ var e11SurfacesDerived = {
|
|
|
12588
13304
|
}
|
|
12589
13305
|
}
|
|
12590
13306
|
const cause = excludedSurfaces({
|
|
12591
|
-
bin: isDir(join24(folder, "bin")),
|
|
12592
|
-
hooks: isDir(join24(folder, "hooks")),
|
|
12593
|
-
agents: isDir(join24(folder, "agents")),
|
|
13307
|
+
bin: isDir(join24(folder, "bin"), rel(join24(folder, "bin")), unreadable),
|
|
13308
|
+
hooks: isDir(join24(folder, "hooks"), rel(join24(folder, "hooks")), unreadable),
|
|
13309
|
+
agents: isDir(join24(folder, "agents"), rel(join24(folder, "agents")), unreadable),
|
|
12594
13310
|
localMcp
|
|
12595
13311
|
});
|
|
12596
13312
|
for (const s of declared) {
|
|
@@ -14355,6 +15071,7 @@ var TRANSPORT_RE2 = /\bStreamableHTTPServerTransport\b|\bWebStandardStreamableHT
|
|
|
14355
15071
|
var ORIGIN_READ_RE = /headers?\.get\(\s*["'`]origin["'`]|headers?\[\s*["'`]origin["'`]\s*\]|headers?\.origin\b|\.header\(\s*["'`]origin["'`]\s*\)/i;
|
|
14356
15072
|
var PROTECT_FLAG_RE = /enableDnsRebindingProtection\s*:\s*true/;
|
|
14357
15073
|
var ALLOWED_ORIGINS_RE = /\ballowedOrigins\s*:\s*/g;
|
|
15074
|
+
var TYPE_ANNOTATION_RE = /^(?:readonly\s+)?(?:string\s*\[\s*\]|(?:Readonly)?Array\s*<\s*string\s*>)\s*(?:\|\s*undefined\s*)?[;,]?\s*$/;
|
|
14358
15075
|
var DISABLED_RE = /enableDnsRebindingProtection\s*:\s*false/;
|
|
14359
15076
|
var REFUSE_RE = /\b403\b|Forbidden/;
|
|
14360
15077
|
var WIDE_BIND_RE = /\.listen\s*\([^)]*["'`]0\.0\.0\.0["'`]|\bhost(?:name)?\s*:\s*["'`]0\.0\.0\.0["'`]/;
|
|
@@ -14462,12 +15179,14 @@ var s22OriginValidation = {
|
|
|
14462
15179
|
);
|
|
14463
15180
|
}
|
|
14464
15181
|
};
|
|
14465
|
-
function resolveConstLiteral(tree, name) {
|
|
15182
|
+
function resolveConstLiteral(tree, name, file) {
|
|
14466
15183
|
const re = new RegExp(
|
|
14467
15184
|
`\\b(?:const|let|var)\\s+${name.replace(/[$]/g, "\\$&")}\\b[^=\\n]*=\\s*`,
|
|
14468
15185
|
"g"
|
|
14469
15186
|
);
|
|
14470
|
-
|
|
15187
|
+
const sites = codeSites(tree, re);
|
|
15188
|
+
const ordered = file ? [...sites.filter((x) => x.file === file), ...sites.filter((x) => x.file !== file)] : sites;
|
|
15189
|
+
for (const site of ordered) {
|
|
14471
15190
|
const after = site.col + site.match[0].length;
|
|
14472
15191
|
const ch = site.line.raw[after];
|
|
14473
15192
|
if (ch === "[") {
|
|
@@ -14483,10 +15202,11 @@ function classifyAllowedOrigins(tree) {
|
|
|
14483
15202
|
const out = { sites: 0, first: "", ok: [], reds: [], unresolved: [] };
|
|
14484
15203
|
for (const site of codeSites(tree, ALLOWED_ORIGINS_RE)) {
|
|
14485
15204
|
if (!isProductSource(site.file.rel)) continue;
|
|
14486
|
-
out.sites++;
|
|
14487
|
-
out.first ||= site.anchor;
|
|
14488
15205
|
const after = site.col + site.match[0].length;
|
|
14489
15206
|
const rest = site.line.raw.slice(after);
|
|
15207
|
+
if (TYPE_ANNOTATION_RE.test(rest)) continue;
|
|
15208
|
+
out.sites++;
|
|
15209
|
+
out.first ||= site.anchor;
|
|
14490
15210
|
let value;
|
|
14491
15211
|
if (rest.startsWith("[")) {
|
|
14492
15212
|
const b = balancedBlock(site.file, site.line.n, after, "[", "]");
|
|
@@ -14494,7 +15214,7 @@ function classifyAllowedOrigins(tree) {
|
|
|
14494
15214
|
} else {
|
|
14495
15215
|
value = (rest.match(/^(?:"[^"]*"|'[^']*'|`[^`]*`|[A-Za-z_$][\w$.]*)/) ?? [rest.trim()])[0];
|
|
14496
15216
|
}
|
|
14497
|
-
const verdict = classifyOriginValue(tree, value);
|
|
15217
|
+
const verdict = classifyOriginValue(tree, value, site.file);
|
|
14498
15218
|
if (verdict.kind === "wildcard") {
|
|
14499
15219
|
out.reds.push({
|
|
14500
15220
|
anchor: site.anchor,
|
|
@@ -14513,31 +15233,42 @@ function classifyAllowedOrigins(tree) {
|
|
|
14513
15233
|
}
|
|
14514
15234
|
return out;
|
|
14515
15235
|
}
|
|
14516
|
-
function
|
|
14517
|
-
const
|
|
14518
|
-
const
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
for (const el of
|
|
14526
|
-
const
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
continue;
|
|
14530
|
-
}
|
|
14531
|
-
if (lit[2].includes("*")) return { kind: "wildcard", detail: lit[0] };
|
|
15236
|
+
function leavesOf(tree, v, file, depth) {
|
|
15237
|
+
const out = { strings: [], unresolved: "" };
|
|
15238
|
+
const t = v.trim();
|
|
15239
|
+
const str = t.match(/^(["'`])(.*)\1$/s);
|
|
15240
|
+
if (str) {
|
|
15241
|
+
out.strings.push(str[0]);
|
|
15242
|
+
return out;
|
|
15243
|
+
}
|
|
15244
|
+
if (t.startsWith("[")) {
|
|
15245
|
+
for (const el of splitTopLevel(t.slice(1, -1))) {
|
|
15246
|
+
const inner = leavesOf(tree, el.startsWith("...") ? el.slice(3) : el, file, depth);
|
|
15247
|
+
out.strings.push(...inner.strings);
|
|
15248
|
+
out.unresolved ||= inner.unresolved;
|
|
14532
15249
|
}
|
|
14533
|
-
return
|
|
15250
|
+
return out;
|
|
14534
15251
|
}
|
|
14535
|
-
if (/^[A-Za-z_$][\w$]*$/.test(
|
|
14536
|
-
const lit = resolveConstLiteral(tree,
|
|
14537
|
-
if (lit === null)
|
|
14538
|
-
|
|
15252
|
+
if (/^[A-Za-z_$][\w$]*$/.test(t) && depth < 3) {
|
|
15253
|
+
const lit = resolveConstLiteral(tree, t, file);
|
|
15254
|
+
if (lit === null) {
|
|
15255
|
+
out.unresolved = t;
|
|
15256
|
+
return out;
|
|
15257
|
+
}
|
|
15258
|
+
return leavesOf(tree, lit, file, depth + 1);
|
|
14539
15259
|
}
|
|
14540
|
-
|
|
15260
|
+
out.unresolved = t;
|
|
15261
|
+
return out;
|
|
15262
|
+
}
|
|
15263
|
+
function classifyOriginValue(tree, value, file) {
|
|
15264
|
+
const v = value.trim();
|
|
15265
|
+
const leaves = leavesOf(tree, v, file, 0);
|
|
15266
|
+
const wild = leaves.strings.find((x) => x.slice(1, -1).includes("*"));
|
|
15267
|
+
if (wild) return { kind: "wildcard", detail: wild };
|
|
15268
|
+
if (leaves.unresolved) return { kind: "unresolved", detail: leaves.unresolved };
|
|
15269
|
+
if (leaves.strings.length === 0)
|
|
15270
|
+
return { kind: "empty", detail: v.startsWith("[") ? v : `${v} (an empty list)` };
|
|
15271
|
+
return { kind: "ok", detail: v };
|
|
14541
15272
|
}
|
|
14542
15273
|
|
|
14543
15274
|
// src/detectors/s23-protocol-version-header.ts
|
|
@@ -14921,8 +15652,8 @@ var s27ElicitationFloor = {
|
|
|
14921
15652
|
};
|
|
14922
15653
|
|
|
14923
15654
|
// src/detectors/s28-claim-forwarded.ts
|
|
14924
|
-
import { existsSync as existsSync21, readFileSync as
|
|
14925
|
-
import { dirname as dirname15, join as join25, relative as
|
|
15655
|
+
import { existsSync as existsSync21, readFileSync as readFileSync14, readdirSync as readdirSync12, statSync as statSync19 } from "fs";
|
|
15656
|
+
import { dirname as dirname15, join as join25, relative as relative13 } from "path";
|
|
14926
15657
|
var PIN11 = STANDARD_PIN;
|
|
14927
15658
|
var CLAIM_TOKEN = /\bverified(?:Org|Person)\b/;
|
|
14928
15659
|
var ANY_CLAIM = /\bverified(?:Actor|Org|Person)\b/g;
|
|
@@ -14980,11 +15711,12 @@ function splitTop(m, from, to) {
|
|
|
14980
15711
|
if (start < to && m.slice(start, to).trim() !== "") parts.push([start, to]);
|
|
14981
15712
|
return parts;
|
|
14982
15713
|
}
|
|
14983
|
-
function listTs(dir, out = []) {
|
|
15714
|
+
function listTs(dir, unread, out = []) {
|
|
14984
15715
|
let names;
|
|
14985
15716
|
try {
|
|
14986
15717
|
names = readdirSync12(dir);
|
|
14987
|
-
} catch {
|
|
15718
|
+
} catch (err) {
|
|
15719
|
+
unread.push(`${dir} could not be listed (${fsErrorCode(err)})`);
|
|
14988
15720
|
return out;
|
|
14989
15721
|
}
|
|
14990
15722
|
for (const n of names) {
|
|
@@ -14992,19 +15724,23 @@ function listTs(dir, out = []) {
|
|
|
14992
15724
|
const p = join25(dir, n);
|
|
14993
15725
|
let st;
|
|
14994
15726
|
try {
|
|
14995
|
-
st =
|
|
14996
|
-
} catch {
|
|
15727
|
+
st = statSync19(p);
|
|
15728
|
+
} catch (err) {
|
|
15729
|
+
unread.push(`${p} could not be read (${fsErrorCode(err)})`);
|
|
14997
15730
|
continue;
|
|
14998
15731
|
}
|
|
14999
|
-
if (st.isDirectory()) listTs(p, out);
|
|
15000
|
-
else if (n.endsWith(".ts") && !n.endsWith(".test.ts") && !n.endsWith(".d.ts"))
|
|
15732
|
+
if (st.isDirectory()) listTs(p, unread, out);
|
|
15733
|
+
else if (n.endsWith(".ts") && !n.endsWith(".test.ts") && !n.endsWith(".d.ts")) {
|
|
15734
|
+
if (!st.isFile()) unread.push(`${p} is not a regular file \u2014 not read`);
|
|
15735
|
+
else out.push(p);
|
|
15736
|
+
}
|
|
15001
15737
|
}
|
|
15002
15738
|
return out;
|
|
15003
15739
|
}
|
|
15004
15740
|
function discoverBackend(root) {
|
|
15005
15741
|
let dir = root;
|
|
15006
15742
|
try {
|
|
15007
|
-
if (!
|
|
15743
|
+
if (!statSync19(root).isDirectory()) dir = dirname15(root);
|
|
15008
15744
|
} catch {
|
|
15009
15745
|
return void 0;
|
|
15010
15746
|
}
|
|
@@ -15017,22 +15753,23 @@ function discoverBackend(root) {
|
|
|
15017
15753
|
}
|
|
15018
15754
|
return void 0;
|
|
15019
15755
|
}
|
|
15020
|
-
function readDoors(backendRoot) {
|
|
15756
|
+
function readDoors(backendRoot, unread) {
|
|
15021
15757
|
const doors = /* @__PURE__ */ new Map();
|
|
15022
|
-
for (const path of listTs(backendRoot)) {
|
|
15758
|
+
for (const path of listTs(backendRoot, unread)) {
|
|
15023
15759
|
let text;
|
|
15024
15760
|
try {
|
|
15025
|
-
text =
|
|
15026
|
-
} catch {
|
|
15761
|
+
text = readFileSync14(path, "utf8");
|
|
15762
|
+
} catch (err) {
|
|
15763
|
+
unread.push(`${path} could not be read (${fsErrorCode(err)})`);
|
|
15027
15764
|
continue;
|
|
15028
15765
|
}
|
|
15029
15766
|
const file = {
|
|
15030
15767
|
path,
|
|
15031
|
-
rel:
|
|
15768
|
+
rel: relative13(backendRoot, path),
|
|
15032
15769
|
lines: text.split("\n").map((raw, i) => ({ n: i + 1, raw, comment: false }))
|
|
15033
15770
|
};
|
|
15034
15771
|
const m = masked(file);
|
|
15035
|
-
const module =
|
|
15772
|
+
const module = relative13(backendRoot, path).replace(/\.ts$/, "").split("\\").join("/");
|
|
15036
15773
|
const re = /\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:query|mutation|action)\s*\(/g;
|
|
15037
15774
|
for (let hit = re.exec(m); hit; hit = re.exec(m)) {
|
|
15038
15775
|
const open = hit.index + hit[0].length - 1;
|
|
@@ -15086,8 +15823,8 @@ function readDoors(backendRoot) {
|
|
|
15086
15823
|
return doors;
|
|
15087
15824
|
}
|
|
15088
15825
|
function definitionOf(name, files) {
|
|
15089
|
-
const
|
|
15090
|
-
const re = new RegExp(`(?:\\b(?:const|let|var)\\s+${
|
|
15826
|
+
const esc3 = name.replace(/\$/g, "\\$");
|
|
15827
|
+
const re = new RegExp(`(?:\\b(?:const|let|var)\\s+${esc3}\\b|\\bfunction\\s+${esc3}\\b)`);
|
|
15091
15828
|
for (const f of files) {
|
|
15092
15829
|
const hit = re.exec(f.m);
|
|
15093
15830
|
if (!hit) continue;
|
|
@@ -15145,7 +15882,9 @@ function scanS28(tree) {
|
|
|
15145
15882
|
const scan = { violate: [], conform: [], cnj: [], calls: 0, backend: void 0 };
|
|
15146
15883
|
const backendRoot = tree.backendRoot ?? discoverBackend(tree.root);
|
|
15147
15884
|
scan.backend = backendRoot;
|
|
15148
|
-
const
|
|
15885
|
+
const unread = [];
|
|
15886
|
+
const doors = backendRoot ? readDoors(backendRoot, unread) : void 0;
|
|
15887
|
+
const unreadNote = unread.length > 0 ? `; backend source NOT read: ${unread.join("; ")}` : "";
|
|
15149
15888
|
const files = tree.files.map((f) => ({ f, text: rawText(f), m: masked(f) }));
|
|
15150
15889
|
const actorDoors = /* @__PURE__ */ new Set();
|
|
15151
15890
|
for (const { text } of files) {
|
|
@@ -15265,7 +16004,7 @@ function scanS28(tree) {
|
|
|
15265
16004
|
if (!info) {
|
|
15266
16005
|
scan.cnj.push(
|
|
15267
16006
|
site(
|
|
15268
|
-
`forwards no verified claim and the door ${door} is not defined in the supplied backend (${backendRoot})`
|
|
16007
|
+
`forwards no verified claim and the door ${door} is not defined in the supplied backend (${backendRoot})${unreadNote}`
|
|
15269
16008
|
)
|
|
15270
16009
|
);
|
|
15271
16010
|
continue;
|
|
@@ -15654,6 +16393,329 @@ var s34AccessRecheck = {
|
|
|
15654
16393
|
}
|
|
15655
16394
|
};
|
|
15656
16395
|
|
|
16396
|
+
// src/detectors/s35-scope-in-read.ts
|
|
16397
|
+
var CALL_RE2 = /\b([A-Za-z_$][\w$]*)\s*\.\s*(query|action)\s*\(/g;
|
|
16398
|
+
var NARROW_WORD = /\b(?:org|orgId|orgSlug|organizationId|organisationId|organization|organisation|tenantId|namespace|recipient|recipientId|owner|ownerId|createdBy|assignedTo|oauthCtx|fromAllowList)\b/;
|
|
16399
|
+
var CARRY_WORD = /\b(?:orgId|organizationId|organisationId|tenantId|verifiedOrg|verifiedPerson|verifiedActor|oauthCtx)\b/;
|
|
16400
|
+
var PAGINATION_WORD = /\b(?:nextCursor|truncated|hasMore|total)\b/;
|
|
16401
|
+
function masked2(file) {
|
|
16402
|
+
return file.lines.map((line) => {
|
|
16403
|
+
let row = "";
|
|
16404
|
+
for (let c = 0; c < line.raw.length; c++) {
|
|
16405
|
+
const ch = line.raw[c];
|
|
16406
|
+
row += line.comment ? " " : ch === " " || isCodeAt(file, line.n, c) ? ch : " ";
|
|
16407
|
+
}
|
|
16408
|
+
return row;
|
|
16409
|
+
}).join("\n");
|
|
16410
|
+
}
|
|
16411
|
+
var CLOSE = { "(": ")", "[": "]", "{": "}" };
|
|
16412
|
+
function matchClose2(m, open) {
|
|
16413
|
+
const stack = [];
|
|
16414
|
+
for (let i = open; i < m.length; i++) {
|
|
16415
|
+
const ch = m[i];
|
|
16416
|
+
if (CLOSE[ch] !== void 0) stack.push(CLOSE[ch]);
|
|
16417
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
16418
|
+
if (stack.pop() !== ch) return -1;
|
|
16419
|
+
if (stack.length === 0) return i;
|
|
16420
|
+
}
|
|
16421
|
+
}
|
|
16422
|
+
return -1;
|
|
16423
|
+
}
|
|
16424
|
+
function splitTop2(m, from, to) {
|
|
16425
|
+
const out = [];
|
|
16426
|
+
let depth = 0;
|
|
16427
|
+
let start = from;
|
|
16428
|
+
for (let i = from; i < to; i++) {
|
|
16429
|
+
const ch = m[i];
|
|
16430
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
16431
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
16432
|
+
else if (ch === "," && depth === 0) {
|
|
16433
|
+
out.push([start, i]);
|
|
16434
|
+
start = i + 1;
|
|
16435
|
+
}
|
|
16436
|
+
}
|
|
16437
|
+
if (m.slice(start, to).trim() !== "") out.push([start, to]);
|
|
16438
|
+
return out;
|
|
16439
|
+
}
|
|
16440
|
+
function blockEnd(m, at) {
|
|
16441
|
+
let depth = 0;
|
|
16442
|
+
for (let i = at - 1; i >= 0; i--) {
|
|
16443
|
+
const ch = m[i];
|
|
16444
|
+
if (ch === "}") depth++;
|
|
16445
|
+
else if (ch === "{") {
|
|
16446
|
+
if (depth === 0) {
|
|
16447
|
+
const close = matchClose2(m, i);
|
|
16448
|
+
return close < 0 ? m.length : close;
|
|
16449
|
+
}
|
|
16450
|
+
depth--;
|
|
16451
|
+
}
|
|
16452
|
+
}
|
|
16453
|
+
return m.length;
|
|
16454
|
+
}
|
|
16455
|
+
var lineAt2 = (m, offset) => m.slice(0, offset).split("\n").length;
|
|
16456
|
+
var esc2 = (s) => s.replace(/\$/g, "\\$");
|
|
16457
|
+
function boundNames(m, callStart) {
|
|
16458
|
+
const lineStart = m.lastIndexOf("\n", callStart - 1) + 1;
|
|
16459
|
+
const before = m.slice(lineStart, callStart);
|
|
16460
|
+
const simple = before.match(
|
|
16461
|
+
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:await\s+)?$/
|
|
16462
|
+
);
|
|
16463
|
+
if (simple) return [simple[1]];
|
|
16464
|
+
const destr = before.match(/\b(?:const|let|var)\s+\{([^}]*)\}\s*=\s*(?:await\s+)?$/);
|
|
16465
|
+
if (destr) {
|
|
16466
|
+
return destr[1].split(",").map((p) => p.trim()).filter((p) => p !== "" && !p.startsWith("...")).map((p) => (p.includes(":") ? p.split(":")[1] : p).replace(/=.*$/, "").trim()).filter((p) => /^[A-Za-z_$][\w$]*$/.test(p));
|
|
16467
|
+
}
|
|
16468
|
+
return [];
|
|
16469
|
+
}
|
|
16470
|
+
function inlineNarrowing(m, start, close, end) {
|
|
16471
|
+
let pos = m.slice(0, start).replace(/\bawait\s+$/, "").length;
|
|
16472
|
+
let grouped = 0;
|
|
16473
|
+
for (; ; ) {
|
|
16474
|
+
let depth = 0;
|
|
16475
|
+
let open2 = -1;
|
|
16476
|
+
for (let i = pos - 1; i >= 0; i--) {
|
|
16477
|
+
const ch = m[i];
|
|
16478
|
+
if (ch === ")" || ch === "]" || ch === "}") depth++;
|
|
16479
|
+
else if (ch === "(" || ch === "[" || ch === "{") {
|
|
16480
|
+
if (depth === 0) {
|
|
16481
|
+
open2 = ch === "(" ? i : -2;
|
|
16482
|
+
break;
|
|
16483
|
+
}
|
|
16484
|
+
depth--;
|
|
16485
|
+
} else if (ch === ";" && depth === 0) break;
|
|
16486
|
+
}
|
|
16487
|
+
if (open2 < 0) break;
|
|
16488
|
+
const ident = /([A-Za-z_$][\w$]*)\s*$/.exec(m.slice(0, open2));
|
|
16489
|
+
if (ident) {
|
|
16490
|
+
if (ident[1] === "scopeFilterList" || ident[1] === "scopeFilterGet") {
|
|
16491
|
+
return {
|
|
16492
|
+
narrowing: { offset: open2 - ident[0].trimEnd().length, form: ident[1] }
|
|
16493
|
+
};
|
|
16494
|
+
}
|
|
16495
|
+
break;
|
|
16496
|
+
}
|
|
16497
|
+
if (!/^\s*(?:await\s+)?$/.test(m.slice(open2 + 1, start))) break;
|
|
16498
|
+
grouped++;
|
|
16499
|
+
pos = m.slice(0, open2).replace(/\bawait\s+$/, "").length;
|
|
16500
|
+
}
|
|
16501
|
+
let p = close + 1;
|
|
16502
|
+
for (let g = 0; g < grouped; g++) {
|
|
16503
|
+
const rest = m.slice(p);
|
|
16504
|
+
const skip = /^\s*\)/.exec(rest);
|
|
16505
|
+
if (!skip) return null;
|
|
16506
|
+
p += skip[0].length;
|
|
16507
|
+
}
|
|
16508
|
+
const chain = /^\s*(?:\??\.\s*(?!filter\b|then\b)[A-Za-z_$][\w$]*\s*)*\??\.\s*(filter|then)\s*\(/.exec(
|
|
16509
|
+
m.slice(p, end)
|
|
16510
|
+
);
|
|
16511
|
+
if (!chain) return null;
|
|
16512
|
+
const open = p + chain[0].length - 1;
|
|
16513
|
+
const cbClose = matchClose2(m, open);
|
|
16514
|
+
if (cbClose < 0) return { unreadable: `a .${chain[1]}( whose closing paren is not found` };
|
|
16515
|
+
const argSpans = splitTop2(m, open + 1, cbClose);
|
|
16516
|
+
const cbSpan = argSpans[0];
|
|
16517
|
+
const cb = cbSpan ? m.slice(cbSpan[0], cbSpan[1]) : "";
|
|
16518
|
+
if (chain[1] === "filter") {
|
|
16519
|
+
return NARROW_WORD.test(cb) ? { narrowing: { offset: p + chain[0].indexOf("."), form: ".filter" } } : null;
|
|
16520
|
+
}
|
|
16521
|
+
const head = /^\s*(?:async\s+)?(?:\(\s*([A-Za-z_$][\w$]*)\s*(?::[^)]*)?\)|([A-Za-z_$][\w$]*))\s*=>\s*/.exec(
|
|
16522
|
+
cb
|
|
16523
|
+
) ?? /^\s*(?:async\s+)?function\s*[\w$]*\s*\(\s*([A-Za-z_$][\w$]*)[^)]*\)\s*(?=\{)/.exec(cb);
|
|
16524
|
+
const param = head ? head[1] ?? head[2] : void 0;
|
|
16525
|
+
if (!head || param === void 0) {
|
|
16526
|
+
return {
|
|
16527
|
+
unreadable: `a .then callback (\`${cb.trim().slice(0, 40)}\`) whose body the scan does not read`
|
|
16528
|
+
};
|
|
16529
|
+
}
|
|
16530
|
+
const bodyFrom = (cbSpan?.[0] ?? 0) + head[0].length;
|
|
16531
|
+
const bodyTo = cbSpan?.[1] ?? bodyFrom;
|
|
16532
|
+
const found = narrowingsOver(m, [param], bodyFrom, bodyTo);
|
|
16533
|
+
return found.length > 0 ? { narrowing: found[0] } : null;
|
|
16534
|
+
}
|
|
16535
|
+
function rebound(m, names, from, to) {
|
|
16536
|
+
const out = [];
|
|
16537
|
+
const region = m.slice(from, to);
|
|
16538
|
+
for (const name of names) {
|
|
16539
|
+
for (const hit of region.matchAll(
|
|
16540
|
+
new RegExp(
|
|
16541
|
+
`\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*(?::[^=]+)?=\\s*${esc2(name)}\\s*(?:;|$)`,
|
|
16542
|
+
"gm"
|
|
16543
|
+
)
|
|
16544
|
+
)) {
|
|
16545
|
+
out.push(hit[1]);
|
|
16546
|
+
}
|
|
16547
|
+
}
|
|
16548
|
+
return out;
|
|
16549
|
+
}
|
|
16550
|
+
function narrowingsOver(m, names, from, to) {
|
|
16551
|
+
const out = [];
|
|
16552
|
+
const region = m.slice(from, to);
|
|
16553
|
+
for (const name of names) {
|
|
16554
|
+
const n = esc2(name);
|
|
16555
|
+
for (const hit of region.matchAll(/\bscopeFilter(List|Get)\s*\(/g)) {
|
|
16556
|
+
const open = from + (hit.index ?? 0) + hit[0].length - 1;
|
|
16557
|
+
const close = matchClose2(m, open);
|
|
16558
|
+
const args = m.slice(open + 1, close < 0 ? to : close);
|
|
16559
|
+
if (new RegExp(`\\b${n}\\b`).test(args)) {
|
|
16560
|
+
out.push({ offset: from + (hit.index ?? 0), form: `scopeFilter${hit[1]}` });
|
|
16561
|
+
}
|
|
16562
|
+
}
|
|
16563
|
+
for (const hit of region.matchAll(
|
|
16564
|
+
new RegExp(`\\b${n}(?:\\s*\\??\\.\\s*[A-Za-z_$][\\w$]*)*\\s*\\??\\.\\s*filter\\s*\\(`, "g")
|
|
16565
|
+
)) {
|
|
16566
|
+
const open = from + (hit.index ?? 0) + hit[0].length - 1;
|
|
16567
|
+
const close = matchClose2(m, open);
|
|
16568
|
+
const cb = m.slice(open + 1, close < 0 ? to : close);
|
|
16569
|
+
if (NARROW_WORD.test(cb)) out.push({ offset: from + (hit.index ?? 0), form: ".filter" });
|
|
16570
|
+
}
|
|
16571
|
+
for (const hit of region.matchAll(
|
|
16572
|
+
new RegExp(
|
|
16573
|
+
`\\bfor\\s*\\(\\s*(?:const|let|var)\\s+[A-Za-z_$][\\w$]*\\s+of\\s+${n}(?:\\s*\\??\\.\\s*[A-Za-z_$][\\w$]*)*\\s*\\)\\s*\\{`,
|
|
16574
|
+
"g"
|
|
16575
|
+
)
|
|
16576
|
+
)) {
|
|
16577
|
+
const open = from + (hit.index ?? 0) + hit[0].length - 1;
|
|
16578
|
+
const close = matchClose2(m, open);
|
|
16579
|
+
const body = m.slice(open + 1, close < 0 ? to : close);
|
|
16580
|
+
for (const cond of body.matchAll(/\bif\s*\(/g)) {
|
|
16581
|
+
const co = open + 1 + (cond.index ?? 0) + cond[0].length - 1;
|
|
16582
|
+
const cc = matchClose2(m, co);
|
|
16583
|
+
if (cc < 0) continue;
|
|
16584
|
+
const after = m.slice(cc + 1, cc + 40);
|
|
16585
|
+
if (NARROW_WORD.test(m.slice(co, cc)) && /^\s*(?:\{\s*)?continue\b/.test(after)) {
|
|
16586
|
+
out.push({ offset: from + (hit.index ?? 0), form: "a skipping branch in a loop" });
|
|
16587
|
+
break;
|
|
16588
|
+
}
|
|
16589
|
+
}
|
|
16590
|
+
}
|
|
16591
|
+
}
|
|
16592
|
+
return out.sort((a, b) => a.offset - b.offset);
|
|
16593
|
+
}
|
|
16594
|
+
function scanS35(tree) {
|
|
16595
|
+
const res = { calls: 0, red: [], cnj: [], ok: [] };
|
|
16596
|
+
for (const file of tree.files) {
|
|
16597
|
+
if (!isProductSource(file.rel)) continue;
|
|
16598
|
+
const m = masked2(file);
|
|
16599
|
+
const raw = file.lines.map((l) => l.raw).join("\n");
|
|
16600
|
+
for (const hit of m.matchAll(CALL_RE2)) {
|
|
16601
|
+
if (!/convex|client/i.test(hit[1])) continue;
|
|
16602
|
+
const start = hit.index ?? 0;
|
|
16603
|
+
const open = start + hit[0].length - 1;
|
|
16604
|
+
const close = matchClose2(m, open);
|
|
16605
|
+
if (close < 0) continue;
|
|
16606
|
+
res.calls++;
|
|
16607
|
+
const anchor = `${file.rel}:${lineAt2(m, start)}`;
|
|
16608
|
+
const parts = splitTop2(m, open + 1, close);
|
|
16609
|
+
const first = parts[0] ? raw.slice(parts[0][0], parts[0][1]) : "";
|
|
16610
|
+
const cast = String.raw`(?:\s+as\s+[\w$.<>[\] ]+)?\s*$`;
|
|
16611
|
+
const lit = new RegExp(String.raw`^\s*["'\x60]([\w/]+:[\w]+)["'\x60]${cast}`).exec(first);
|
|
16612
|
+
const ref = new RegExp(String.raw`^\s*(?:api|internal)\.([\w]+)\.([\w]+)${cast}`).exec(first);
|
|
16613
|
+
const door = lit ? lit[1] : ref ? `${ref[1]}:${ref[2]}` : void 0;
|
|
16614
|
+
if (door === void 0) {
|
|
16615
|
+
res.cnj.push({
|
|
16616
|
+
anchor,
|
|
16617
|
+
note: `${anchor} names its door by an expression (\`${first.trim().slice(0, 40)}\`): which door, and so which arguments it accepts, is unknown`
|
|
16618
|
+
});
|
|
16619
|
+
continue;
|
|
16620
|
+
}
|
|
16621
|
+
const argText = parts.slice(1).map(([a, b]) => m.slice(a, b)).join(",");
|
|
16622
|
+
const carried = CARRY_WORD.test(argText);
|
|
16623
|
+
const names = boundNames(m, start);
|
|
16624
|
+
const end = blockEnd(m, start);
|
|
16625
|
+
const inline = inlineNarrowing(m, start, close, end);
|
|
16626
|
+
const narrowings = [
|
|
16627
|
+
...inline && "narrowing" in inline ? [inline.narrowing] : [],
|
|
16628
|
+
...narrowingsOver(m, [...names, ...rebound(m, names, close + 1, end)], close + 1, end)
|
|
16629
|
+
].sort((a, b) => a.offset - b.offset);
|
|
16630
|
+
if (narrowings.length === 0 && inline && "unreadable" in inline && !carried) {
|
|
16631
|
+
res.cnj.push({
|
|
16632
|
+
anchor,
|
|
16633
|
+
note: `${anchor} ${door} is followed by ${inline.unreadable}: whether it narrows the result is unread`
|
|
16634
|
+
});
|
|
16635
|
+
continue;
|
|
16636
|
+
}
|
|
16637
|
+
if (narrowings.length === 0 || carried) {
|
|
16638
|
+
res.ok.push({
|
|
16639
|
+
anchor,
|
|
16640
|
+
note: names.length === 0 && !inline ? `${anchor} ${door}: result not bound, nothing narrows it` : `${anchor} ${door}`
|
|
16641
|
+
});
|
|
16642
|
+
continue;
|
|
16643
|
+
}
|
|
16644
|
+
const spread = /\.\.\.\s*[A-Za-z_$]/.test(argText);
|
|
16645
|
+
const n = narrowings[0];
|
|
16646
|
+
const at = `${file.rel}:${lineAt2(m, n.offset)}`;
|
|
16647
|
+
if (spread) {
|
|
16648
|
+
res.cnj.push({
|
|
16649
|
+
anchor,
|
|
16650
|
+
note: `${anchor} ${door} spreads its arguments from a value the scan does not read, and ${at} narrows the result (${n.form}): whether the read carried the predicate is unread`
|
|
16651
|
+
});
|
|
16652
|
+
continue;
|
|
16653
|
+
}
|
|
16654
|
+
const paged = PAGINATION_WORD.test(m.slice(n.offset, end));
|
|
16655
|
+
res.red.push({
|
|
16656
|
+
anchor: at,
|
|
16657
|
+
note: `${at} narrows (${n.form}) the rows of ${anchor} ${door}, whose arguments carry no scope predicate${paged ? "; a pagination value (nextCursor/truncated/hasMore/total) is served after the narrowing" : ""}`
|
|
16658
|
+
});
|
|
16659
|
+
}
|
|
16660
|
+
}
|
|
16661
|
+
return res;
|
|
16662
|
+
}
|
|
16663
|
+
function s35Controls() {
|
|
16664
|
+
const red = scanS35Text(
|
|
16665
|
+
'const rows = await client.query("t:list", { limit });\nconst mine = scopeFilterList(rows, oauthCtx);\n'
|
|
16666
|
+
);
|
|
16667
|
+
const green = scanS35Text(
|
|
16668
|
+
'const rows = await client.query("t:list", { limit, orgId: oauthCtx.orgId });\nconst mine = scopeFilterList(rows, oauthCtx);\n'
|
|
16669
|
+
);
|
|
16670
|
+
return { sees: red, spares: !green };
|
|
16671
|
+
}
|
|
16672
|
+
function scanS35Text(text) {
|
|
16673
|
+
const call = /\bclient\.query\(([^)]*)\)/.exec(text);
|
|
16674
|
+
if (!call) return false;
|
|
16675
|
+
const carried = CARRY_WORD.test(call[1].split(",").slice(1).join(","));
|
|
16676
|
+
return !carried && narrowingsOver(text, ["rows"], call.index + call[0].length, text.length).length > 0;
|
|
16677
|
+
}
|
|
16678
|
+
var s35ScopeInRead = {
|
|
16679
|
+
id: "S35 scope-in-read",
|
|
16680
|
+
rule: "S-35",
|
|
16681
|
+
pin: STANDARD_PIN,
|
|
16682
|
+
run(tree) {
|
|
16683
|
+
const ctl = s35Controls();
|
|
16684
|
+
if (!ctl.sees || !ctl.spares) {
|
|
16685
|
+
throw new Error(
|
|
16686
|
+
`S35 positive control FAILED: ${JSON.stringify(ctl)} \u2014 the narrowing matcher cannot tell a filter after an unscoped read from a repeated one, verdict untrustworthy`
|
|
16687
|
+
);
|
|
16688
|
+
}
|
|
16689
|
+
const scan = scanS35(tree);
|
|
16690
|
+
if (scan.calls === 0) {
|
|
16691
|
+
return absentResult(
|
|
16692
|
+
this.id,
|
|
16693
|
+
"-",
|
|
16694
|
+
"no backend call (<client>.query|action) in product source (positive control OK) \u2014 S-35 n/a"
|
|
16695
|
+
);
|
|
16696
|
+
}
|
|
16697
|
+
if (scan.red.length > 0) {
|
|
16698
|
+
return redResult(
|
|
16699
|
+
this.id,
|
|
16700
|
+
scan.red[0].anchor,
|
|
16701
|
+
`${scan.red.map((s) => s.note).join("; ")} -> pass the scope predicate to the door as an argument and forward its pagination unchanged (S-35)`
|
|
16702
|
+
);
|
|
16703
|
+
}
|
|
16704
|
+
if (scan.cnj.length > 0) {
|
|
16705
|
+
return couldNotJudge(
|
|
16706
|
+
this.id,
|
|
16707
|
+
scan.cnj[0].anchor,
|
|
16708
|
+
`${scan.cnj.map((s) => s.note).join("; ")} (S-35)`
|
|
16709
|
+
);
|
|
16710
|
+
}
|
|
16711
|
+
return presentResult(
|
|
16712
|
+
this.id,
|
|
16713
|
+
scan.ok[0].anchor,
|
|
16714
|
+
`${scan.calls} backend call(s) read; no scope narrowing over an unscoped result (positive control OK). Not decided here: a narrowing in a helper the scan cannot find, and the door applying the predicate (R-11, R-55) (S-35)`
|
|
16715
|
+
);
|
|
16716
|
+
}
|
|
16717
|
+
};
|
|
16718
|
+
|
|
15657
16719
|
// src/detectors/index.ts
|
|
15658
16720
|
var registered = [
|
|
15659
16721
|
d2,
|
|
@@ -15752,6 +16814,7 @@ var registered = [
|
|
|
15752
16814
|
s25ResourceDiscipline,
|
|
15753
16815
|
s26ListChangedParity,
|
|
15754
16816
|
s27ElicitationFloor,
|
|
16817
|
+
s35ScopeInRead,
|
|
15755
16818
|
e5ExtensionIsolation,
|
|
15756
16819
|
a3SandboxRespected,
|
|
15757
16820
|
a4WcagGate,
|
|
@@ -15778,6 +16841,7 @@ var READS_CODE = /* @__PURE__ */ new Set([
|
|
|
15778
16841
|
"S-25",
|
|
15779
16842
|
"S-26",
|
|
15780
16843
|
"S-27",
|
|
16844
|
+
"S-35",
|
|
15781
16845
|
"E-5",
|
|
15782
16846
|
"A-3"
|
|
15783
16847
|
]);
|
|
@@ -15787,12 +16851,12 @@ var detectors = registered.map(
|
|
|
15787
16851
|
|
|
15788
16852
|
// src/runtime/launch.ts
|
|
15789
16853
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
15790
|
-
import { existsSync as existsSync22, readFileSync as
|
|
16854
|
+
import { existsSync as existsSync22, readFileSync as readFileSync15, statSync as statSync20 } from "fs";
|
|
15791
16855
|
import { dirname as dirname16, isAbsolute, join as join26, resolve as resolve2 } from "path";
|
|
15792
16856
|
function findPackageDir(target) {
|
|
15793
16857
|
let dir;
|
|
15794
16858
|
try {
|
|
15795
|
-
dir =
|
|
16859
|
+
dir = statSync20(target).isDirectory() ? target : dirname16(target);
|
|
15796
16860
|
} catch {
|
|
15797
16861
|
return null;
|
|
15798
16862
|
}
|
|
@@ -15822,7 +16886,10 @@ function deriveLaunchPlan(target) {
|
|
|
15822
16886
|
}
|
|
15823
16887
|
let pkg;
|
|
15824
16888
|
try {
|
|
15825
|
-
|
|
16889
|
+
if (!statSync20(join26(pkgDir, "package.json")).isFile()) {
|
|
16890
|
+
return { error: `${join26(pkgDir, "package.json")} is not a regular file \u2014 not read` };
|
|
16891
|
+
}
|
|
16892
|
+
pkg = JSON.parse(readFileSync15(join26(pkgDir, "package.json"), "utf8"));
|
|
15826
16893
|
} catch (err) {
|
|
15827
16894
|
return {
|
|
15828
16895
|
error: `could not parse ${join26(pkgDir, "package.json")}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -15856,6 +16923,7 @@ function deriveLaunchPlan(target) {
|
|
|
15856
16923
|
var INTROSPECTOR = String.raw`
|
|
15857
16924
|
import { spawn } from "node:child_process";
|
|
15858
16925
|
const plan = JSON.parse(process.env.MCP_DOCTOR_PLAN);
|
|
16926
|
+
const INVALID_CURSOR = "mcp-doctor:invalid-cursor:%%%";
|
|
15859
16927
|
const done = (o) => { process.stdout.write(JSON.stringify(o)); process.exit(0); };
|
|
15860
16928
|
let child;
|
|
15861
16929
|
try {
|
|
@@ -15865,11 +16933,35 @@ try {
|
|
|
15865
16933
|
}
|
|
15866
16934
|
let settled = false;
|
|
15867
16935
|
let serverInfo;
|
|
16936
|
+
let capabilities = {};
|
|
16937
|
+
let tools;
|
|
16938
|
+
const probes = [];
|
|
16939
|
+
const pending = new Map();
|
|
16940
|
+
let nextId = 10;
|
|
16941
|
+
let probeTimer;
|
|
15868
16942
|
const fail = (msg) => { if (settled) return; settled = true; try { child.kill(); } catch {} done({ launchError: msg }); };
|
|
15869
|
-
const
|
|
16943
|
+
const finish = (why) => {
|
|
16944
|
+
if (settled) return; settled = true; clearTimeout(to); clearTimeout(probeTimer);
|
|
16945
|
+
for (const idx of pending.values()) probes[idx].outcome = { kind: "no-answer", reason: why };
|
|
16946
|
+
try { child.kill(); } catch {}
|
|
16947
|
+
done({ tools, serverInfo, cursorProbes: probes });
|
|
16948
|
+
};
|
|
16949
|
+
const to = setTimeout(() => {
|
|
16950
|
+
if (tools) finish("no answer before the introspection deadline");
|
|
16951
|
+
else fail("timeout: server did not answer initialize+tools/list within the deadline");
|
|
16952
|
+
}, 12000);
|
|
15870
16953
|
child.on("error", (e) => fail("spawn error: " + (e && e.message ? e.message : String(e))));
|
|
15871
|
-
child.on("exit", (code) =>
|
|
16954
|
+
child.on("exit", (code) => {
|
|
16955
|
+
if (tools) finish("the server exited (code " + code + ") before answering");
|
|
16956
|
+
else fail("server exited (code " + code + ") before completing the MCP handshake");
|
|
16957
|
+
});
|
|
15872
16958
|
const send = (o) => { try { child.stdin.write(JSON.stringify(o) + "\n"); } catch (e) { fail("write failed: " + e.message); } };
|
|
16959
|
+
const probe = (target, surface, method, params) => {
|
|
16960
|
+
const id = nextId++;
|
|
16961
|
+
pending.set(id, probes.length);
|
|
16962
|
+
probes.push({ target, surface });
|
|
16963
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
16964
|
+
};
|
|
15873
16965
|
let buf = "";
|
|
15874
16966
|
child.stdout.on("data", (d) => {
|
|
15875
16967
|
buf += d.toString();
|
|
@@ -15878,6 +16970,8 @@ child.stdout.on("data", (d) => {
|
|
|
15878
16970
|
const line = buf.slice(0, i); buf = buf.slice(i + 1);
|
|
15879
16971
|
if (!line.trim()) continue;
|
|
15880
16972
|
let msg; try { msg = JSON.parse(line); } catch { continue; }
|
|
16973
|
+
// A request or notification FROM the server carries a method: it is not an answer.
|
|
16974
|
+
if (msg.method !== undefined) continue;
|
|
15881
16975
|
if (msg.id === 1) {
|
|
15882
16976
|
// The initialize response's serverInfo is the SAME non-authenticated
|
|
15883
16977
|
// discovery surface a client reads version from (S-7); captured here
|
|
@@ -15885,13 +16979,46 @@ child.stdout.on("data", (d) => {
|
|
|
15885
16979
|
// alongside it, additively -- every existing consumer of the
|
|
15886
16980
|
// tools-only introspector output is unaffected.
|
|
15887
16981
|
serverInfo = msg.result && msg.result.serverInfo;
|
|
16982
|
+
capabilities = (msg.result && msg.result.capabilities) || {};
|
|
15888
16983
|
send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
15889
16984
|
send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
15890
16985
|
} else if (msg.id === 2) {
|
|
15891
|
-
if (settled) return;
|
|
15892
|
-
const
|
|
15893
|
-
|
|
15894
|
-
|
|
16986
|
+
if (settled) return;
|
|
16987
|
+
const served = msg.result && Array.isArray(msg.result.tools) ? msg.result.tools : [];
|
|
16988
|
+
const schemaOf = (t) => (t && t.inputSchema && typeof t.inputSchema === "object" ? t.inputSchema : {});
|
|
16989
|
+
const takesCursor = (t) => {
|
|
16990
|
+
const props = schemaOf(t).properties;
|
|
16991
|
+
return Boolean(props && typeof props === "object" && Object.prototype.hasOwnProperty.call(props, "cursor"));
|
|
16992
|
+
};
|
|
16993
|
+
tools = served.map((t) => ({ name: String(t && t.name), description: t && typeof t.description === "string" ? t.description : undefined, cursorArgument: takesCursor(t) }));
|
|
16994
|
+
const lists = ["tools/list"];
|
|
16995
|
+
if (capabilities.resources) lists.push("resources/list", "resources/templates/list");
|
|
16996
|
+
if (capabilities.prompts) lists.push("prompts/list");
|
|
16997
|
+
for (const m of lists) probe(m, "list-method", m, { cursor: INVALID_CURSOR });
|
|
16998
|
+
for (const t of served) {
|
|
16999
|
+
if (!takesCursor(t)) continue;
|
|
17000
|
+
const name = String(t && t.name);
|
|
17001
|
+
const required = Array.isArray(schemaOf(t).required) ? schemaOf(t).required.filter((r) => r !== "cursor") : [];
|
|
17002
|
+
if (required.length > 0) {
|
|
17003
|
+
probes.push({ target: name, surface: "tool-argument", outcome: { kind: "not-sent", reason: "the tool requires " + required.join(", ") + " beside the cursor; the probe never invents a value for them" } });
|
|
17004
|
+
continue;
|
|
17005
|
+
}
|
|
17006
|
+
probe(name, "tool-argument", "tools/call", { name, arguments: { cursor: INVALID_CURSOR } });
|
|
17007
|
+
}
|
|
17008
|
+
if (pending.size === 0) { finish("no probe pending"); return; }
|
|
17009
|
+
probeTimer = setTimeout(() => finish("no answer within the 5 s probe deadline"), 5000);
|
|
17010
|
+
} else if (pending.has(msg.id)) {
|
|
17011
|
+
const idx = pending.get(msg.id); pending.delete(msg.id);
|
|
17012
|
+
if (msg.error && typeof msg.error === "object") {
|
|
17013
|
+
probes[idx].outcome = { kind: "error", code: msg.error.code, message: typeof msg.error.message === "string" ? msg.error.message : undefined };
|
|
17014
|
+
} else if (msg.result && typeof msg.result === "object") {
|
|
17015
|
+
const sc = msg.result.structuredContent;
|
|
17016
|
+
const err = sc && typeof sc === "object" ? sc.error : undefined;
|
|
17017
|
+
probes[idx].outcome = { kind: "result", isError: msg.result.isError === true, errorCode: err && typeof err === "object" ? err.code : undefined };
|
|
17018
|
+
} else {
|
|
17019
|
+
probes[idx].outcome = { kind: "no-answer", reason: "an answer with neither result nor error" };
|
|
17020
|
+
}
|
|
17021
|
+
if (pending.size === 0) finish("no probe pending");
|
|
15895
17022
|
}
|
|
15896
17023
|
}
|
|
15897
17024
|
});
|
|
@@ -15900,29 +17027,41 @@ send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "
|
|
|
15900
17027
|
function readEvalCounts(target) {
|
|
15901
17028
|
const counts = /* @__PURE__ */ new Map();
|
|
15902
17029
|
const pkgDir = findPackageDir(target) ?? (statSyncSafe(target) ? target : null);
|
|
15903
|
-
if (!pkgDir) return counts;
|
|
17030
|
+
if (!pkgDir) return { counts };
|
|
15904
17031
|
let dir = pkgDir;
|
|
15905
17032
|
for (let depth = 0; depth < 8; depth++) {
|
|
15906
17033
|
const candidate = join26(dir, "evals", "evals.json");
|
|
15907
17034
|
if (existsSync22(candidate)) {
|
|
17035
|
+
let text;
|
|
17036
|
+
try {
|
|
17037
|
+
if (!statSync20(candidate).isFile()) {
|
|
17038
|
+
return { counts, unreadable: `${candidate} is not a regular file \u2014 not read` };
|
|
17039
|
+
}
|
|
17040
|
+
text = readFileSync15(candidate, "utf8");
|
|
17041
|
+
} catch (err) {
|
|
17042
|
+
return {
|
|
17043
|
+
counts,
|
|
17044
|
+
unreadable: `${candidate} could not be read (${fsErrorCode(err)})`
|
|
17045
|
+
};
|
|
17046
|
+
}
|
|
15908
17047
|
try {
|
|
15909
|
-
const parsed = JSON.parse(
|
|
17048
|
+
const parsed = JSON.parse(text);
|
|
15910
17049
|
for (const c of parsed.cases ?? []) {
|
|
15911
17050
|
if (c && typeof c.tool === "string") counts.set(c.tool, (counts.get(c.tool) ?? 0) + 1);
|
|
15912
17051
|
}
|
|
15913
17052
|
} catch {
|
|
15914
17053
|
}
|
|
15915
|
-
return counts;
|
|
17054
|
+
return { counts };
|
|
15916
17055
|
}
|
|
15917
17056
|
const parent = dirname16(dir);
|
|
15918
17057
|
if (parent === dir) break;
|
|
15919
17058
|
dir = parent;
|
|
15920
17059
|
}
|
|
15921
|
-
return counts;
|
|
17060
|
+
return { counts };
|
|
15922
17061
|
}
|
|
15923
17062
|
function statSyncSafe(p) {
|
|
15924
17063
|
try {
|
|
15925
|
-
return
|
|
17064
|
+
return statSync20(p).isDirectory();
|
|
15926
17065
|
} catch {
|
|
15927
17066
|
return false;
|
|
15928
17067
|
}
|
|
@@ -15942,6 +17081,54 @@ function deriveExpectedCommit(cwd) {
|
|
|
15942
17081
|
}
|
|
15943
17082
|
return void 0;
|
|
15944
17083
|
}
|
|
17084
|
+
function readCursorProbe(raw) {
|
|
17085
|
+
const r = raw ?? {};
|
|
17086
|
+
const target = typeof r.target === "string" ? r.target : "<unnamed>";
|
|
17087
|
+
const surface = r.surface === "tool-argument" ? "tool-argument" : "list-method";
|
|
17088
|
+
const o = r.outcome ?? {};
|
|
17089
|
+
const unreadable = (what) => ({
|
|
17090
|
+
target,
|
|
17091
|
+
surface,
|
|
17092
|
+
outcome: { kind: "no-answer", reason: `unreadable probe record (${what})` }
|
|
17093
|
+
});
|
|
17094
|
+
if (r.surface !== "tool-argument" && r.surface !== "list-method") {
|
|
17095
|
+
return unreadable(`surface ${JSON.stringify(r.surface)}`);
|
|
17096
|
+
}
|
|
17097
|
+
switch (o.kind) {
|
|
17098
|
+
case "error":
|
|
17099
|
+
return {
|
|
17100
|
+
target,
|
|
17101
|
+
surface,
|
|
17102
|
+
outcome: {
|
|
17103
|
+
kind: "error",
|
|
17104
|
+
code: o.code,
|
|
17105
|
+
...typeof o.message === "string" ? { message: o.message } : {}
|
|
17106
|
+
}
|
|
17107
|
+
};
|
|
17108
|
+
case "result":
|
|
17109
|
+
return {
|
|
17110
|
+
target,
|
|
17111
|
+
surface,
|
|
17112
|
+
outcome: {
|
|
17113
|
+
kind: "result",
|
|
17114
|
+
isError: o.isError === true,
|
|
17115
|
+
...o.errorCode !== void 0 ? { errorCode: o.errorCode } : {}
|
|
17116
|
+
}
|
|
17117
|
+
};
|
|
17118
|
+
case "no-answer":
|
|
17119
|
+
case "not-sent":
|
|
17120
|
+
return {
|
|
17121
|
+
target,
|
|
17122
|
+
surface,
|
|
17123
|
+
outcome: {
|
|
17124
|
+
kind: o.kind,
|
|
17125
|
+
reason: typeof o.reason === "string" ? o.reason : "no reason given"
|
|
17126
|
+
}
|
|
17127
|
+
};
|
|
17128
|
+
default:
|
|
17129
|
+
return unreadable(`outcome kind ${JSON.stringify(o.kind)}`);
|
|
17130
|
+
}
|
|
17131
|
+
}
|
|
15945
17132
|
function launchAndIntrospect(target) {
|
|
15946
17133
|
const plan = deriveLaunchPlan(target);
|
|
15947
17134
|
if ("error" in plan) {
|
|
@@ -15982,19 +17169,22 @@ function launchAndIntrospect(target) {
|
|
|
15982
17169
|
if (parsed.launchError) {
|
|
15983
17170
|
return { root: target, tools: [], launchError: parsed.launchError };
|
|
15984
17171
|
}
|
|
15985
|
-
const evalCounts = readEvalCounts(target);
|
|
17172
|
+
const { counts: evalCounts, unreadable: evalsUnreadable } = readEvalCounts(target);
|
|
15986
17173
|
const tools = (parsed.tools ?? []).map((t) => {
|
|
15987
17174
|
const name = t.name;
|
|
15988
17175
|
const count = evalCounts.get(name);
|
|
15989
17176
|
const base = t.description ? { name, description: t.description } : { name };
|
|
15990
|
-
|
|
17177
|
+
const withCursor = t.cursorArgument === true ? { ...base, cursorArgument: true } : base;
|
|
17178
|
+
return count === void 0 ? withCursor : { ...withCursor, evalCaseCount: count };
|
|
15991
17179
|
});
|
|
15992
17180
|
const expectedCommit = deriveExpectedCommit(plan.cwd);
|
|
15993
17181
|
return {
|
|
15994
17182
|
root: target,
|
|
15995
17183
|
tools,
|
|
15996
17184
|
...parsed.serverInfo ? { serverInfo: parsed.serverInfo } : {},
|
|
15997
|
-
...expectedCommit ? { expectedCommit } : {}
|
|
17185
|
+
...expectedCommit ? { expectedCommit } : {},
|
|
17186
|
+
...evalsUnreadable ? { evalsUnreadable } : {},
|
|
17187
|
+
...Array.isArray(parsed.cursorProbes) ? { cursorProbes: parsed.cursorProbes.map(readCursorProbe) } : {}
|
|
15998
17188
|
};
|
|
15999
17189
|
}
|
|
16000
17190
|
|
|
@@ -16246,12 +17436,13 @@ var USAGE = [
|
|
|
16246
17436
|
" --version print the version and exit",
|
|
16247
17437
|
" --help print this and exit",
|
|
16248
17438
|
"",
|
|
16249
|
-
"exit: 0 clean, 1 a violation, 2 an abstention with no violation"
|
|
17439
|
+
"exit: 0 clean, 1 a violation, 2 an abstention with no violation (including a target",
|
|
17440
|
+
" that names nothing readable: missing, empty, or not a source tree)"
|
|
16250
17441
|
].join("\n");
|
|
16251
17442
|
function readVersion() {
|
|
16252
17443
|
try {
|
|
16253
17444
|
const here = dirname17(fileURLToPath2(import.meta.url));
|
|
16254
|
-
const manifest = JSON.parse(
|
|
17445
|
+
const manifest = JSON.parse(readFileSync16(join27(here, "..", "package.json"), "utf8"));
|
|
16255
17446
|
return manifest.version ?? null;
|
|
16256
17447
|
} catch {
|
|
16257
17448
|
return null;
|
|
@@ -16297,10 +17488,28 @@ function main() {
|
|
|
16297
17488
|
detectorList = detectors.map((d) => d === localIdentity ? ratchet : d);
|
|
16298
17489
|
}
|
|
16299
17490
|
const backend = backendArg ? backendArg.slice("--backend=".length) : void 0;
|
|
16300
|
-
|
|
16301
|
-
|
|
16302
|
-
|
|
16303
|
-
|
|
17491
|
+
let report;
|
|
17492
|
+
try {
|
|
17493
|
+
report = runDoctorReport(target, detectorList, {
|
|
17494
|
+
mode,
|
|
17495
|
+
...backend !== void 0 ? { backend } : {}
|
|
17496
|
+
});
|
|
17497
|
+
} catch (err) {
|
|
17498
|
+
if (!(err instanceof SubjectUnreadableError)) throw err;
|
|
17499
|
+
if (args.includes("--json")) {
|
|
17500
|
+
console.log(
|
|
17501
|
+
JSON.stringify(
|
|
17502
|
+
{ surface: target, exitCode: 2, subjectUnreadable: err.form, results: [], skipped: [] },
|
|
17503
|
+
null,
|
|
17504
|
+
2
|
|
17505
|
+
)
|
|
17506
|
+
);
|
|
17507
|
+
}
|
|
17508
|
+
console.error(err.message);
|
|
17509
|
+
process.exitCode = 2;
|
|
17510
|
+
return;
|
|
17511
|
+
}
|
|
17512
|
+
const { results, skipped } = report;
|
|
16304
17513
|
console.log(
|
|
16305
17514
|
args.includes("--json") ? renderJson(target, results, skipped) : renderTable(target, results, skipped)
|
|
16306
17515
|
);
|
|
@@ -16323,6 +17532,7 @@ function isDirectInvocation() {
|
|
|
16323
17532
|
}
|
|
16324
17533
|
if (isDirectInvocation()) main();
|
|
16325
17534
|
export {
|
|
17535
|
+
SubjectUnreadableError,
|
|
16326
17536
|
computeExitCode,
|
|
16327
17537
|
countOwedHumanVerdicts,
|
|
16328
17538
|
isLiveDetector,
|