mason-context 0.19.0 → 0.19.1
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 +7 -0
- package/dist/mason-audit.js +432 -253
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +430 -253
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +50 -0
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +1123 -941
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +50 -0
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +507 -296
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason-mcp.js
CHANGED
|
@@ -58,874 +58,941 @@ var init_git_read = __esm({
|
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
// src/
|
|
61
|
+
// src/utils/paths.ts
|
|
62
62
|
import path from "path";
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
i++;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (quote2) {
|
|
73
|
-
if (char === quote2) quote2 = "";
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (char === "'" || char === '"') {
|
|
77
|
-
quote2 = char;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (char === ";" || line.slice(i, i + 2) === "&&") {
|
|
81
|
-
parts.push(line.slice(start, i));
|
|
82
|
-
if (char === "&") i++;
|
|
83
|
-
start = i + 1;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
parts.push(line.slice(start));
|
|
87
|
-
return parts;
|
|
88
|
-
}
|
|
89
|
-
function commandClaims(line, lineNo, cwd, shell = false) {
|
|
90
|
-
const claims = [];
|
|
91
|
-
const segments = splitCommands(line);
|
|
92
|
-
for (const segment of segments) {
|
|
93
|
-
const text2 = segment.trim().replace(/^\$\s+/, "");
|
|
94
|
-
if (text2.startsWith("#") || /^(?:echo|printf)\b/.test(text2)) continue;
|
|
95
|
-
const cd = text2.match(/^cd\s+(.+)$/);
|
|
96
|
-
if (cd) {
|
|
97
|
-
const directory = literal(cd[1]);
|
|
98
|
-
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path.posix.join(cwd ?? ".", directory);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
for (const match of text2.matchAll(COMMAND)) {
|
|
102
|
-
const flags = match[2].trim();
|
|
103
|
-
const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
|
|
104
|
-
let directory = cwd;
|
|
105
|
-
let scopeUnknown;
|
|
106
|
-
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
107
|
-
if (selector) {
|
|
108
|
-
const selected = literal(selector[1]);
|
|
109
|
-
directory = selected === null || cwd === null ? null : path.posix.join(cwd ?? ".", selected);
|
|
110
|
-
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
111
|
-
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
112
|
-
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
113
|
-
scopeUnknown = "Package selection or conditional options after the script need review.";
|
|
114
|
-
}
|
|
115
|
-
if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
|
|
116
|
-
scopeUnknown = "The command's working directory or shell control flow could not be resolved.";
|
|
117
|
-
}
|
|
118
|
-
claims.push({
|
|
119
|
-
scriptName: match[3],
|
|
120
|
-
invocation: match[0],
|
|
121
|
-
excerpt: match[0],
|
|
122
|
-
line: lineNo,
|
|
123
|
-
...directory !== void 0 && directory !== null ? { directory } : {},
|
|
124
|
-
...scopeUnknown ? { scopeUnknown } : {}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
if (/\b(?:cd|pushd|popd)\b/.test(text2)) cwd = null;
|
|
128
|
-
}
|
|
129
|
-
return { claims, cwd };
|
|
63
|
+
function normalizeRepoPath(value) {
|
|
64
|
+
const slash = value.replace(/\\/g, "/");
|
|
65
|
+
if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
|
|
66
|
+
if (slash.split("/").includes("..")) return null;
|
|
67
|
+
const normalized = path.posix.normalize(slash).replace(/\/$/, "");
|
|
68
|
+
return normalized === "." ? null : normalized;
|
|
130
69
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"src/audit/commands.ts"() {
|
|
134
|
-
"use strict";
|
|
135
|
-
TOKEN = `(?:"[^"\\n]*"|'[^'\\n]*'|[^\\s\`;&|]+)`;
|
|
136
|
-
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
137
|
-
literal = (value) => {
|
|
138
|
-
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
139
|
-
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// src/audit/tree.ts
|
|
145
|
-
function glyphIndex(line) {
|
|
146
|
-
for (const glyph of GLYPHS) {
|
|
147
|
-
const idx = line.indexOf(glyph);
|
|
148
|
-
if (idx !== -1) return idx;
|
|
149
|
-
}
|
|
150
|
-
return -1;
|
|
70
|
+
function sanitizeRepoPaths(files) {
|
|
71
|
+
return [...new Set(files.map(normalizeRepoPath).filter((p) => p !== null))];
|
|
151
72
|
}
|
|
152
|
-
function
|
|
153
|
-
|
|
73
|
+
function isWithinRoot(root, candidate) {
|
|
74
|
+
const relative = path.relative(root, candidate);
|
|
75
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
154
76
|
}
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
const columns = name.search(/\s{2,}/);
|
|
160
|
-
if (columns !== -1) name = name.slice(0, columns);
|
|
161
|
-
name = name.trim();
|
|
162
|
-
if (!name || /\s/.test(name)) return null;
|
|
163
|
-
return name;
|
|
77
|
+
function anchorMatches(anchor, file) {
|
|
78
|
+
const a = normalizeRepoPath(anchor);
|
|
79
|
+
const f = normalizeRepoPath(file);
|
|
80
|
+
return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
|
|
164
81
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
if (glyphLines < MIN_GLYPH_LINES) return [];
|
|
168
|
-
const claims = [];
|
|
169
|
-
const stack = [];
|
|
170
|
-
let rootPrefix = "";
|
|
171
|
-
let started = false;
|
|
172
|
-
for (let i = 0; i < blockLines.length; i++) {
|
|
173
|
-
const line = blockLines[i];
|
|
174
|
-
const col = glyphIndex(line);
|
|
175
|
-
if (col === -1) {
|
|
176
|
-
if (isSpacerLine(line)) continue;
|
|
177
|
-
if (!started) {
|
|
178
|
-
const candidate = line.trim();
|
|
179
|
-
if (candidate.endsWith("/") && !/\s/.test(candidate)) {
|
|
180
|
-
rootPrefix = candidate.replace(/\/+$/, "");
|
|
181
|
-
claims.push({
|
|
182
|
-
path: rootPrefix,
|
|
183
|
-
line: blockStartLine + i,
|
|
184
|
-
excerpt: candidate
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
return claims;
|
|
190
|
-
}
|
|
191
|
-
started = true;
|
|
192
|
-
const name = entryName(line.slice(col + GLYPHS[0].length));
|
|
193
|
-
if (name === null) return claims;
|
|
194
|
-
while (stack.length > 0 && stack[stack.length - 1].col >= col) {
|
|
195
|
-
stack.pop();
|
|
196
|
-
}
|
|
197
|
-
const isDir = name.endsWith("/");
|
|
198
|
-
const cleanName = name.replace(/\/+$/, "");
|
|
199
|
-
const segments = [
|
|
200
|
-
...rootPrefix ? [rootPrefix] : [],
|
|
201
|
-
...stack.map((s) => s.name),
|
|
202
|
-
cleanName
|
|
203
|
-
];
|
|
204
|
-
claims.push({
|
|
205
|
-
path: segments.join("/"),
|
|
206
|
-
line: blockStartLine + i,
|
|
207
|
-
excerpt: name
|
|
208
|
-
});
|
|
209
|
-
if (isDir) stack.push({ col, name: cleanName });
|
|
210
|
-
}
|
|
211
|
-
return claims;
|
|
82
|
+
function matchingPaths(anchors, files) {
|
|
83
|
+
return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
|
|
212
84
|
}
|
|
213
|
-
var
|
|
214
|
-
|
|
215
|
-
"src/audit/tree.ts"() {
|
|
85
|
+
var init_paths = __esm({
|
|
86
|
+
"src/utils/paths.ts"() {
|
|
216
87
|
"use strict";
|
|
217
|
-
MIN_GLYPH_LINES = 3;
|
|
218
|
-
GLYPHS = ["\u251C\u2500\u2500", "\u2514\u2500\u2500"];
|
|
219
88
|
}
|
|
220
89
|
});
|
|
221
90
|
|
|
222
|
-
// src/audit/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
91
|
+
// src/audit/types.ts
|
|
92
|
+
var types_exports = {};
|
|
93
|
+
__export(types_exports, {
|
|
94
|
+
ALL_CHECKS: () => ALL_CHECKS
|
|
95
|
+
});
|
|
96
|
+
var ALL_CHECKS;
|
|
97
|
+
var init_types = __esm({
|
|
98
|
+
"src/audit/types.ts"() {
|
|
99
|
+
"use strict";
|
|
100
|
+
ALL_CHECKS = [
|
|
101
|
+
"deleted-reference",
|
|
102
|
+
"new-module",
|
|
103
|
+
"stale-count",
|
|
104
|
+
"dead-command",
|
|
105
|
+
"deps-changed",
|
|
106
|
+
"decision-anchor-drift"
|
|
107
|
+
];
|
|
232
108
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return normalizePathToken(trimmed);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// src/utils/files.ts
|
|
112
|
+
import fs from "fs/promises";
|
|
113
|
+
import { constants } from "fs";
|
|
114
|
+
import path2 from "path";
|
|
115
|
+
import fg from "fast-glob";
|
|
116
|
+
function isSensitiveFile(file) {
|
|
117
|
+
return file.split(/[\\/]/).some(
|
|
118
|
+
(part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(part)
|
|
119
|
+
);
|
|
245
120
|
}
|
|
246
|
-
function
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (line.includes(IGNORE_END)) {
|
|
258
|
-
inRegion = false;
|
|
259
|
-
ignored[i] = true;
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
if (inRegion) {
|
|
263
|
-
ignored[i] = true;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
if (ignoreNext) {
|
|
267
|
-
if (line.trim().length === 0) continue;
|
|
268
|
-
ignored[i] = true;
|
|
269
|
-
ignoreNext = false;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (line.includes(IGNORE_LINE)) {
|
|
273
|
-
ignored[i] = true;
|
|
274
|
-
const rest = line.replace(IGNORE_LINE, "").trim();
|
|
275
|
-
if (rest.length === 0) ignoreNext = true;
|
|
121
|
+
async function readBoundedFile(file, maxBytes) {
|
|
122
|
+
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
123
|
+
try {
|
|
124
|
+
const stat = await handle.stat();
|
|
125
|
+
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
126
|
+
const buffer = Buffer.alloc(Math.min(maxBytes + 1, stat.size + 1));
|
|
127
|
+
let bytes = 0;
|
|
128
|
+
while (bytes < buffer.length) {
|
|
129
|
+
const result = await handle.read(buffer, bytes, buffer.length - bytes, null);
|
|
130
|
+
if (result.bytesRead === 0) break;
|
|
131
|
+
bytes += result.bytesRead;
|
|
276
132
|
}
|
|
133
|
+
return bytes === buffer.length ? null : buffer.subarray(0, bytes).toString("utf8");
|
|
134
|
+
} finally {
|
|
135
|
+
await handle.close();
|
|
277
136
|
}
|
|
278
|
-
return ignored;
|
|
279
137
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
if (!
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const parsed = markdown.parse(lines.map((line, i) => ignored[i] ? "" : line).join("\n"), {});
|
|
295
|
-
for (const token of parsed) {
|
|
296
|
-
if (token.type !== "inline" || !token.map) continue;
|
|
297
|
-
let line = token.map[0] + 1;
|
|
298
|
-
for (const child of token.children ?? []) {
|
|
299
|
-
const target = child.type === "link_open" ? child.attrGet("href") : child.type === "image" ? child.attrGet("src") : null;
|
|
300
|
-
if (target && !/^(?:[a-z][a-z0-9+.-]*:|[\/#~])/i.test(target)) {
|
|
301
|
-
let pathname = null;
|
|
302
|
-
try {
|
|
303
|
-
pathname = decodeURIComponent(target.split(/[?#]/)[0]);
|
|
304
|
-
} catch {
|
|
305
|
-
}
|
|
306
|
-
if (pathname && !/[$*?{}<>\\\x00-\x1f]/.test(pathname)) {
|
|
307
|
-
addPath({ path: pathname, line, excerpt: target, relativeTo: "document" });
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (child.type === "softbreak" || child.type === "hardbreak") line++;
|
|
311
|
-
else line += child.content.split("\n").length - 1;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
let shellCwd;
|
|
315
|
-
let inFence = false;
|
|
316
|
-
let fenceInfo = "";
|
|
317
|
-
let fenceMarker = "";
|
|
318
|
-
let blockLines = [];
|
|
319
|
-
let blockStartLine = 0;
|
|
320
|
-
const processBlock = () => {
|
|
321
|
-
for (const claim of extractTreeClaims(blockLines, blockStartLine)) {
|
|
322
|
-
addPath(claim);
|
|
323
|
-
}
|
|
324
|
-
for (let i = 0; i < blockLines.length; i++) {
|
|
325
|
-
const exact = exactTokenPath(blockLines[i]);
|
|
326
|
-
if (exact) {
|
|
327
|
-
addPath({
|
|
328
|
-
path: exact,
|
|
329
|
-
line: blockStartLine + i,
|
|
330
|
-
excerpt: blockLines[i].trim()
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
for (let i = 0; i < lines.length; i++) {
|
|
336
|
-
const line = lines[i];
|
|
337
|
-
const lineNo = i + 1;
|
|
338
|
-
const fenceMatch = line.match(/^\s*(```+|~~~+)(.*)$/);
|
|
339
|
-
if (fenceMatch) {
|
|
340
|
-
if (!inFence) {
|
|
341
|
-
inFence = true;
|
|
342
|
-
fenceMarker = fenceMatch[1][0];
|
|
343
|
-
fenceInfo = fenceMatch[2].trim().toLowerCase();
|
|
344
|
-
blockLines = [];
|
|
345
|
-
shellCwd = void 0;
|
|
346
|
-
blockStartLine = lineNo + 1;
|
|
347
|
-
} else if (fenceMatch[1][0] === fenceMarker) {
|
|
348
|
-
inFence = false;
|
|
349
|
-
processBlock();
|
|
350
|
-
}
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
if (inFence) {
|
|
354
|
-
blockLines.push(ignored[i] ? "" : line);
|
|
355
|
-
if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {
|
|
356
|
-
const scanned = commandClaims(line, lineNo, shellCwd, true);
|
|
357
|
-
shellCwd = scanned.cwd;
|
|
358
|
-
for (const claim of scanned.claims) addCommand(claim);
|
|
138
|
+
async function loadProjectConfig(root) {
|
|
139
|
+
try {
|
|
140
|
+
const canonicalRoot = await fs.realpath(root);
|
|
141
|
+
const configPath2 = await fs.realpath(path2.join(root, ".mason/config.json"));
|
|
142
|
+
if (!isWithinRoot(canonicalRoot, configPath2)) throw new Error("Project configuration resolves outside the repository");
|
|
143
|
+
const raw = await readBoundedFile(configPath2, 64 * 1024);
|
|
144
|
+
if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
|
|
145
|
+
const value = JSON.parse(raw);
|
|
146
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
|
|
147
|
+
const config = {};
|
|
148
|
+
for (const key of ["patterns", "alwaysInclude", "ignore"]) {
|
|
149
|
+
if (value[key] === void 0) continue;
|
|
150
|
+
if (!Array.isArray(value[key]) || !value[key].every((s) => typeof s === "string")) {
|
|
151
|
+
throw new Error(`Configuration ${key} must be an array of strings`);
|
|
359
152
|
}
|
|
360
|
-
|
|
153
|
+
config[key] = value[key];
|
|
361
154
|
}
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
155
|
+
if (value.audit !== void 0) {
|
|
156
|
+
const audit = value.audit;
|
|
157
|
+
if (!audit || typeof audit !== "object" || Array.isArray(audit)) throw new Error("Configuration audit must be an object");
|
|
158
|
+
const patterns = (raw2) => {
|
|
159
|
+
if (!Array.isArray(raw2) || raw2.length > 100 || !raw2.every((s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s))) {
|
|
160
|
+
throw new Error("Audit patterns must be at most 100 repository-relative globs without traversal or negation");
|
|
161
|
+
}
|
|
162
|
+
return raw2;
|
|
163
|
+
};
|
|
164
|
+
config.audit = {};
|
|
165
|
+
if (audit.include !== void 0) config.audit.include = patterns(audit.include);
|
|
166
|
+
if (audit.exclude !== void 0) {
|
|
167
|
+
if (!audit.exclude || typeof audit.exclude !== "object" || Array.isArray(audit.exclude)) throw new Error("audit.exclude must map check names to patterns");
|
|
168
|
+
const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
|
|
169
|
+
config.audit.exclude = {};
|
|
170
|
+
for (const [check, globs] of Object.entries(audit.exclude)) {
|
|
171
|
+
if (!ALL_CHECKS2.includes(check)) throw new Error("Unknown audit check: " + check);
|
|
172
|
+
config.audit.exclude[check] = patterns(globs);
|
|
173
|
+
}
|
|
367
174
|
}
|
|
368
175
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
176
|
+
return config;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error.code === "ENOENT") return {};
|
|
179
|
+
throw new Error(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function createFileAccess(rootDir) {
|
|
183
|
+
const root = path2.resolve(rootDir);
|
|
184
|
+
const canonicalRoot = await fs.realpath(root).catch(() => root);
|
|
185
|
+
const config = await loadProjectConfig(root);
|
|
186
|
+
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
187
|
+
let gitFiles = null;
|
|
188
|
+
try {
|
|
189
|
+
const { stdout } = await execGit(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
|
|
190
|
+
gitFiles = new Set(stdout.split("\0").filter(Boolean));
|
|
191
|
+
} catch {
|
|
192
|
+
let inGit = false;
|
|
193
|
+
try {
|
|
194
|
+
await execGit(["rev-parse", "--git-dir"], { cwd: root });
|
|
195
|
+
inGit = true;
|
|
196
|
+
} catch {
|
|
374
197
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
198
|
+
if (inGit) throw new Error("Cannot enumerate Git files safely");
|
|
199
|
+
}
|
|
200
|
+
async function resolve2(file) {
|
|
201
|
+
const relative = normalizeRepoPath(file);
|
|
202
|
+
if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative)) return null;
|
|
203
|
+
const candidate = path2.join(root, relative);
|
|
204
|
+
try {
|
|
205
|
+
const real = await fs.realpath(candidate);
|
|
206
|
+
if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path2.relative(canonicalRoot, real))) return null;
|
|
207
|
+
const stat = await fs.stat(real);
|
|
208
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
|
|
209
|
+
if (gitFiles && !gitFiles.has(path2.relative(canonicalRoot, real).split(path2.sep).join("/"))) return null;
|
|
210
|
+
return real;
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
384
213
|
}
|
|
385
|
-
|
|
386
|
-
|
|
214
|
+
}
|
|
215
|
+
async function list(patterns = SOURCE_GLOB, options = {}) {
|
|
216
|
+
const found = await fg(patterns, { cwd: root, ignore, followSymbolicLinks: false, ...options });
|
|
217
|
+
const safe = await Promise.all(found.map(async (f) => await resolve2(f) ? f : null));
|
|
218
|
+
return safe.filter((f) => f !== null).sort();
|
|
219
|
+
}
|
|
220
|
+
async function read(file) {
|
|
221
|
+
const relative = normalizeRepoPath(file);
|
|
222
|
+
if (!relative) return null;
|
|
223
|
+
const real = await resolve2(relative);
|
|
224
|
+
if (!real) return null;
|
|
225
|
+
for (const rel of /* @__PURE__ */ new Set([relative, path2.relative(canonicalRoot, real).split(path2.sep).join("/")])) {
|
|
226
|
+
if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const content2 = await readBoundedFile(real, MAX_SOURCE_BYTES);
|
|
230
|
+
return content2 === null ? null : { path: relative, content: content2, totalLines: content2.split("\n").length };
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
387
233
|
}
|
|
388
|
-
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims) addCommand(claim);
|
|
389
234
|
}
|
|
390
|
-
|
|
391
|
-
return {
|
|
392
|
-
paths: [...paths.values()],
|
|
393
|
-
counts,
|
|
394
|
-
commands: [...commands2.values()]
|
|
395
|
-
};
|
|
235
|
+
return { root, config, list, read };
|
|
396
236
|
}
|
|
397
|
-
var
|
|
398
|
-
var
|
|
399
|
-
"src/
|
|
237
|
+
var SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
|
|
238
|
+
var init_files = __esm({
|
|
239
|
+
"src/utils/files.ts"() {
|
|
400
240
|
"use strict";
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
"
|
|
407
|
-
"
|
|
408
|
-
"
|
|
409
|
-
"
|
|
410
|
-
"
|
|
411
|
-
"
|
|
412
|
-
"
|
|
413
|
-
"
|
|
414
|
-
"
|
|
415
|
-
"
|
|
416
|
-
"
|
|
417
|
-
"
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"
|
|
421
|
-
"
|
|
422
|
-
"
|
|
423
|
-
"
|
|
424
|
-
"
|
|
425
|
-
"
|
|
426
|
-
"
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
"CLAUDE.md",
|
|
430
|
-
"AGENTS.md",
|
|
431
|
-
".gitignore",
|
|
432
|
-
".env.example"
|
|
433
|
-
]);
|
|
434
|
-
SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
|
|
435
|
-
markdown = new MarkdownIt("commonmark", { html: false });
|
|
436
|
-
COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
|
|
437
|
-
COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
|
|
438
|
-
IGNORE_LINE = "<!-- mason:ignore -->";
|
|
439
|
-
IGNORE_START = "<!-- mason:ignore-start -->";
|
|
440
|
-
IGNORE_END = "<!-- mason:ignore-end -->";
|
|
241
|
+
init_git_read();
|
|
242
|
+
init_paths();
|
|
243
|
+
SOURCE_EXTENSIONS = ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs", "vue", "svelte", "kt", "kts", "java", "py", "go", "rs", "swift", "rb", "cs", "cpp", "c", "h", "hpp", "dart", "php"];
|
|
244
|
+
SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
|
|
245
|
+
SOURCE_IGNORE = [
|
|
246
|
+
"**/node_modules/**",
|
|
247
|
+
"**/dist/**",
|
|
248
|
+
"**/build/**",
|
|
249
|
+
"**/.gradle/**",
|
|
250
|
+
"**/target/**",
|
|
251
|
+
"**/.git/**",
|
|
252
|
+
"**/.mason/**",
|
|
253
|
+
"**/vendor/**",
|
|
254
|
+
"**/__pycache__/**",
|
|
255
|
+
"**/venv/**",
|
|
256
|
+
"**/.venv/**",
|
|
257
|
+
"**/*.min.*",
|
|
258
|
+
"**/*.map",
|
|
259
|
+
"**/*.lock",
|
|
260
|
+
"**/generated/**",
|
|
261
|
+
"**/*.generated.*",
|
|
262
|
+
"**/R.java",
|
|
263
|
+
"**/BuildConfig.java",
|
|
264
|
+
"**/package-lock.json",
|
|
265
|
+
"**/yarn.lock",
|
|
266
|
+
"**/pnpm-lock.yaml"
|
|
267
|
+
];
|
|
268
|
+
MAX_SOURCE_BYTES = 1024 * 1024;
|
|
441
269
|
}
|
|
442
270
|
});
|
|
443
271
|
|
|
444
|
-
// src/utils/
|
|
445
|
-
import
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
272
|
+
// src/utils/storage.ts
|
|
273
|
+
import fs2 from "fs/promises";
|
|
274
|
+
import path3 from "path";
|
|
275
|
+
import { randomUUID } from "crypto";
|
|
276
|
+
async function storePath(root, relative, createParents = false) {
|
|
277
|
+
const normalized = normalizeRepoPath(relative);
|
|
278
|
+
if (!normalized) throw new Error(`Invalid store path: ${relative}`);
|
|
279
|
+
let current = await fs2.realpath(root);
|
|
280
|
+
const parts = normalized.split("/");
|
|
281
|
+
for (let i = 0; i < parts.length; i++) {
|
|
282
|
+
current = path3.join(current, parts[i]);
|
|
283
|
+
let stat;
|
|
284
|
+
try {
|
|
285
|
+
stat = await fs2.lstat(current);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error.code !== "ENOENT") throw error;
|
|
288
|
+
if (createParents && i < parts.length - 1) {
|
|
289
|
+
try {
|
|
290
|
+
await fs2.mkdir(current);
|
|
291
|
+
} catch (mkdirError) {
|
|
292
|
+
if (mkdirError.code !== "EEXIST") throw mkdirError;
|
|
293
|
+
}
|
|
294
|
+
stat = await fs2.lstat(current);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (stat?.isSymbolicLink()) throw new Error(`Symlink in store path: ${relative}`);
|
|
298
|
+
}
|
|
299
|
+
return current;
|
|
452
300
|
}
|
|
453
|
-
function
|
|
454
|
-
|
|
301
|
+
async function readStoreJson(root, relative) {
|
|
302
|
+
try {
|
|
303
|
+
const file = await storePath(root, relative);
|
|
304
|
+
const raw = await readBoundedFile(file, 10 * 1024 * 1024);
|
|
305
|
+
if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
|
|
306
|
+
const parsed = JSON.parse(raw);
|
|
307
|
+
if (parsed === null) throw new Error("expected a JSON object, received null");
|
|
308
|
+
return parsed;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error.code === "ENOENT") return null;
|
|
311
|
+
throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
312
|
+
}
|
|
455
313
|
}
|
|
456
|
-
function
|
|
457
|
-
const
|
|
458
|
-
|
|
314
|
+
async function writeStoreJson(root, relative, value) {
|
|
315
|
+
const payload = JSON.stringify(value, null, 2) + "\n";
|
|
316
|
+
if (Buffer.byteLength(payload) > 10 * 1024 * 1024) {
|
|
317
|
+
throw new Error(`Mason store ${relative} exceeds 10 MiB`);
|
|
318
|
+
}
|
|
319
|
+
const file = await storePath(root, relative, true);
|
|
320
|
+
const temporary = path3.join(path3.dirname(file), `.${path3.basename(file)}.${randomUUID()}.tmp`);
|
|
321
|
+
try {
|
|
322
|
+
const handle = await fs2.open(temporary, "wx", 384);
|
|
323
|
+
try {
|
|
324
|
+
await handle.writeFile(payload, "utf8");
|
|
325
|
+
await handle.sync();
|
|
326
|
+
} finally {
|
|
327
|
+
await handle.close();
|
|
328
|
+
}
|
|
329
|
+
await fs2.rename(temporary, file);
|
|
330
|
+
} finally {
|
|
331
|
+
await fs2.rm(temporary, { force: true });
|
|
332
|
+
}
|
|
459
333
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
334
|
+
var init_storage = __esm({
|
|
335
|
+
"src/utils/storage.ts"() {
|
|
336
|
+
"use strict";
|
|
337
|
+
init_paths();
|
|
338
|
+
init_files();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// src/audit/inspection.ts
|
|
343
|
+
import fg2 from "fast-glob";
|
|
344
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
345
|
+
import path4 from "path";
|
|
346
|
+
function withRepositoryInspection(root, run) {
|
|
347
|
+
return inspections.run({ root: path4.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
|
|
348
|
+
}
|
|
349
|
+
function inspectionRead(root, key, read) {
|
|
350
|
+
const inspection = inspections.getStore();
|
|
351
|
+
if (!inspection || inspection.root !== path4.resolve(root)) return read();
|
|
352
|
+
if (!inspection.reads.has(key)) {
|
|
353
|
+
const pending = read();
|
|
354
|
+
inspection.reads.set(key, pending);
|
|
355
|
+
void pending.catch(() => {
|
|
356
|
+
if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return inspection.reads.get(key);
|
|
360
|
+
}
|
|
361
|
+
function inspectionGit(args, options) {
|
|
362
|
+
return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
|
|
464
363
|
}
|
|
465
|
-
function
|
|
466
|
-
|
|
364
|
+
async function auditGitPaths(root, kind) {
|
|
365
|
+
const custom = (await loadProjectConfig(root)).audit?.include ?? [];
|
|
366
|
+
const shared = inspections.getStore()?.root === path4.resolve(root) && !custom.length;
|
|
367
|
+
const expanded = fg2.generateTasks(custom).flatMap((task) => task.positive);
|
|
368
|
+
if (expanded.length > 1e3) throw new Error("Audit include patterns expand to more than 1,000 globs");
|
|
369
|
+
const documentSpecs = [...docSpecs, ...expanded.map((pattern) => `:(glob)${pattern}`)];
|
|
370
|
+
const specs = shared ? [...documentSpecs, ...sourceSpecs] : kind === "documents" ? documentSpecs : sourceSpecs;
|
|
371
|
+
const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
|
|
372
|
+
const { stdout } = await inspectionGit(
|
|
373
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
|
|
374
|
+
{ cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
|
|
375
|
+
);
|
|
376
|
+
const paths = [...new Set(stdout.split("\0").filter(Boolean))].filter((file) => kind === "documents" ? custom.length > 0 || /^(readme|agents|claude)\.md$/i.test(path4.posix.basename(file)) : sourceSuffixes.has(file.slice(file.lastIndexOf("."))));
|
|
377
|
+
if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
|
|
378
|
+
return paths.sort();
|
|
467
379
|
}
|
|
468
|
-
var
|
|
469
|
-
|
|
380
|
+
var inspections, sourceSpecs, docSpecs, sourceSuffixes;
|
|
381
|
+
var init_inspection = __esm({
|
|
382
|
+
"src/audit/inspection.ts"() {
|
|
470
383
|
"use strict";
|
|
384
|
+
init_git_read();
|
|
385
|
+
init_files();
|
|
386
|
+
inspections = new AsyncLocalStorage2();
|
|
387
|
+
sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
|
|
388
|
+
docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
|
|
389
|
+
sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
|
|
471
390
|
}
|
|
472
391
|
});
|
|
473
392
|
|
|
474
|
-
// src/
|
|
475
|
-
import
|
|
476
|
-
import
|
|
477
|
-
import
|
|
478
|
-
import
|
|
479
|
-
function
|
|
480
|
-
|
|
481
|
-
(part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(part)
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
async function readBoundedFile(file, maxBytes) {
|
|
485
|
-
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
393
|
+
// src/audit/inputs.ts
|
|
394
|
+
import fs3 from "fs/promises";
|
|
395
|
+
import * as nativeFs from "fs";
|
|
396
|
+
import path5 from "path";
|
|
397
|
+
import fg3 from "fast-glob";
|
|
398
|
+
async function auditInputPath(root, file) {
|
|
399
|
+
if (file === ".") return fs3.realpath(root);
|
|
486
400
|
try {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
while (bytes < buffer.length) {
|
|
492
|
-
const result = await handle.read(buffer, bytes, buffer.length - bytes, null);
|
|
493
|
-
if (result.bytesRead === 0) break;
|
|
494
|
-
bytes += result.bytesRead;
|
|
401
|
+
return await storePath(root, file);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
404
|
+
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
495
405
|
}
|
|
496
|
-
|
|
497
|
-
} finally {
|
|
498
|
-
await handle.close();
|
|
406
|
+
throw error;
|
|
499
407
|
}
|
|
500
408
|
}
|
|
501
|
-
async function
|
|
409
|
+
async function readAuditInput(root, file) {
|
|
502
410
|
try {
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const raw = await readBoundedFile(configPath2, 64 * 1024);
|
|
507
|
-
if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
|
|
508
|
-
const value = JSON.parse(raw);
|
|
509
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
|
|
510
|
-
const config = {};
|
|
511
|
-
for (const key of ["patterns", "alwaysInclude", "ignore"]) {
|
|
512
|
-
if (value[key] === void 0) continue;
|
|
513
|
-
if (!Array.isArray(value[key]) || !value[key].every((s) => typeof s === "string")) {
|
|
514
|
-
throw new Error(`Configuration ${key} must be an array of strings`);
|
|
515
|
-
}
|
|
516
|
-
config[key] = value[key];
|
|
517
|
-
}
|
|
518
|
-
return config;
|
|
411
|
+
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
412
|
+
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
413
|
+
return value;
|
|
519
414
|
} catch (error) {
|
|
520
|
-
if (error.code
|
|
521
|
-
throw
|
|
415
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
|
|
416
|
+
throw error;
|
|
522
417
|
}
|
|
523
418
|
}
|
|
524
|
-
async function
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
419
|
+
async function auditGlob(root, patterns, options) {
|
|
420
|
+
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
421
|
+
const directories = /* @__PURE__ */ new Set();
|
|
422
|
+
const readdir2 = ((directory, opts, callback) => {
|
|
423
|
+
const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
|
|
424
|
+
directories.add(relative);
|
|
425
|
+
if (directories.size > MAX_INPUTS) {
|
|
426
|
+
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
auditInputPath(root, relative).then(() => {
|
|
430
|
+
nativeFs.readdir(directory, opts, callback);
|
|
431
|
+
}, callback);
|
|
432
|
+
});
|
|
433
|
+
const lstat2 = ((file, callback) => {
|
|
434
|
+
const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
|
|
435
|
+
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
436
|
+
});
|
|
437
|
+
const stream = fg3.stream(patterns, {
|
|
438
|
+
cwd: root,
|
|
439
|
+
ignore: options.ignore,
|
|
440
|
+
onlyFiles: false,
|
|
441
|
+
onlyDirectories: options.onlyDirectories,
|
|
442
|
+
objectMode: true,
|
|
443
|
+
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
444
|
+
fs: { readdir: readdir2, lstat: lstat2 },
|
|
445
|
+
concurrency: 16
|
|
446
|
+
});
|
|
447
|
+
const matches = [];
|
|
530
448
|
try {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
449
|
+
for await (const value of stream) {
|
|
450
|
+
const entry = value;
|
|
451
|
+
const file = entry.path;
|
|
452
|
+
if (options.select && !options.select(file)) continue;
|
|
453
|
+
await auditInputPath(root, file);
|
|
454
|
+
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
455
|
+
matches.push(file);
|
|
456
|
+
if (matches.length > MAX_INPUTS) {
|
|
457
|
+
const largest = [...matches.reduce((counts, item) => {
|
|
458
|
+
const dir = item.split("/")[0];
|
|
459
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
460
|
+
return counts;
|
|
461
|
+
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
462
|
+
throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count4]) => `${dir} (${count4})`).join(", ")}. Narrow this check's inputs.`);
|
|
463
|
+
}
|
|
539
464
|
}
|
|
540
|
-
|
|
465
|
+
} finally {
|
|
466
|
+
stream.destroy();
|
|
541
467
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
468
|
+
return matches.sort();
|
|
469
|
+
}
|
|
470
|
+
async function gitSourcePaths(root) {
|
|
471
|
+
return auditGitPaths(root, "sources");
|
|
472
|
+
}
|
|
473
|
+
async function gitIgnoredPaths(root, files) {
|
|
474
|
+
if (!files.length) return /* @__PURE__ */ new Set();
|
|
475
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
476
|
+
for (let offset = 0; offset < files.length; offset += 1e3) {
|
|
546
477
|
try {
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
478
|
+
const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
|
|
479
|
+
cwd: root,
|
|
480
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
481
|
+
timeout: 1e4,
|
|
482
|
+
input: files.slice(offset, offset + 1e3).join("\0") + "\0"
|
|
483
|
+
});
|
|
484
|
+
for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (error.code !== 1) throw error;
|
|
555
487
|
}
|
|
556
488
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
489
|
+
return ignored;
|
|
490
|
+
}
|
|
491
|
+
var MAX_INPUTS;
|
|
492
|
+
var init_inputs = __esm({
|
|
493
|
+
"src/audit/inputs.ts"() {
|
|
494
|
+
"use strict";
|
|
495
|
+
init_files();
|
|
496
|
+
init_storage();
|
|
497
|
+
init_git_read();
|
|
498
|
+
init_inspection();
|
|
499
|
+
MAX_INPUTS = 1e5;
|
|
561
500
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// src/audit/policy.ts
|
|
504
|
+
import fg4 from "fast-glob";
|
|
505
|
+
async function includedCheckPaths(root, paths, exclude) {
|
|
506
|
+
if (!exclude.length) return new Set(paths);
|
|
507
|
+
const included = /* @__PURE__ */ new Set();
|
|
508
|
+
for (let offset = 0; offset < paths.length; offset += 256) {
|
|
509
|
+
for (const file of await auditGlob(root, paths.slice(offset, offset + 256).map((file2) => fg4.escapePath(file2)), {
|
|
510
|
+
ignore: exclude,
|
|
511
|
+
followSymbolicLinks: false,
|
|
512
|
+
label: "Audit check exclusions"
|
|
513
|
+
})) included.add(file);
|
|
514
|
+
}
|
|
515
|
+
return included;
|
|
516
|
+
}
|
|
517
|
+
var init_policy = __esm({
|
|
518
|
+
"src/audit/policy.ts"() {
|
|
519
|
+
"use strict";
|
|
520
|
+
init_inputs();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// src/audit/commands.ts
|
|
525
|
+
import path6 from "path";
|
|
526
|
+
function splitCommands(line) {
|
|
527
|
+
const parts = [];
|
|
528
|
+
let quote2 = "", start = 0;
|
|
529
|
+
for (let i = 0; i < line.length; i++) {
|
|
530
|
+
const char = line[i];
|
|
531
|
+
if (char === "\\") {
|
|
532
|
+
i++;
|
|
533
|
+
continue;
|
|
569
534
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
535
|
+
if (quote2) {
|
|
536
|
+
if (char === quote2) quote2 = "";
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (char === "'" || char === '"') {
|
|
540
|
+
quote2 = char;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (char === ";" || line.slice(i, i + 2) === "&&") {
|
|
544
|
+
parts.push(line.slice(start, i));
|
|
545
|
+
if (char === "&") i++;
|
|
546
|
+
start = i + 1;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
parts.push(line.slice(start));
|
|
550
|
+
return parts;
|
|
551
|
+
}
|
|
552
|
+
function commandClaims(line, lineNo, cwd, shell = false) {
|
|
553
|
+
const claims = [];
|
|
554
|
+
const segments = splitCommands(line);
|
|
555
|
+
for (const segment of segments) {
|
|
556
|
+
const text2 = segment.trim().replace(/^\$\s+/, "");
|
|
557
|
+
if (text2.startsWith("#") || /^(?:echo|printf)\b/.test(text2)) continue;
|
|
558
|
+
const cd = text2.match(/^cd\s+(.+)$/);
|
|
559
|
+
if (cd) {
|
|
560
|
+
const directory = literal(cd[1]);
|
|
561
|
+
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path6.posix.join(cwd ?? ".", directory);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
for (const match of text2.matchAll(COMMAND)) {
|
|
565
|
+
const flags = match[2].trim();
|
|
566
|
+
const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
|
|
567
|
+
let directory = cwd;
|
|
568
|
+
let scopeUnknown;
|
|
569
|
+
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
570
|
+
if (selector) {
|
|
571
|
+
const selected = literal(selector[1]);
|
|
572
|
+
directory = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
|
|
573
|
+
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
574
|
+
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
575
|
+
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
576
|
+
scopeUnknown = "Package selection or conditional options after the script need review.";
|
|
577
|
+
}
|
|
578
|
+
if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
|
|
579
|
+
scopeUnknown = "The command's working directory or shell control flow could not be resolved.";
|
|
580
|
+
}
|
|
581
|
+
claims.push({
|
|
582
|
+
scriptName: match[3],
|
|
583
|
+
invocation: match[0],
|
|
584
|
+
excerpt: match[0],
|
|
585
|
+
line: lineNo,
|
|
586
|
+
...directory !== void 0 && directory !== null ? { directory } : {},
|
|
587
|
+
...scopeUnknown ? { scopeUnknown } : {}
|
|
588
|
+
});
|
|
575
589
|
}
|
|
590
|
+
if (/\b(?:cd|pushd|popd)\b/.test(text2)) cwd = null;
|
|
576
591
|
}
|
|
577
|
-
return {
|
|
592
|
+
return { claims, cwd };
|
|
578
593
|
}
|
|
579
|
-
var
|
|
580
|
-
var
|
|
581
|
-
"src/
|
|
594
|
+
var TOKEN, COMMAND, literal;
|
|
595
|
+
var init_commands = __esm({
|
|
596
|
+
"src/audit/commands.ts"() {
|
|
582
597
|
"use strict";
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
"**/dist/**",
|
|
590
|
-
"**/build/**",
|
|
591
|
-
"**/.gradle/**",
|
|
592
|
-
"**/target/**",
|
|
593
|
-
"**/.git/**",
|
|
594
|
-
"**/.mason/**",
|
|
595
|
-
"**/vendor/**",
|
|
596
|
-
"**/__pycache__/**",
|
|
597
|
-
"**/venv/**",
|
|
598
|
-
"**/.venv/**",
|
|
599
|
-
"**/*.min.*",
|
|
600
|
-
"**/*.map",
|
|
601
|
-
"**/*.lock",
|
|
602
|
-
"**/generated/**",
|
|
603
|
-
"**/*.generated.*",
|
|
604
|
-
"**/R.java",
|
|
605
|
-
"**/BuildConfig.java",
|
|
606
|
-
"**/package-lock.json",
|
|
607
|
-
"**/yarn.lock",
|
|
608
|
-
"**/pnpm-lock.yaml"
|
|
609
|
-
];
|
|
610
|
-
MAX_SOURCE_BYTES = 1024 * 1024;
|
|
598
|
+
TOKEN = `(?:"[^"\\n]*"|'[^'\\n]*'|[^\\s\`;&|]+)`;
|
|
599
|
+
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
600
|
+
literal = (value) => {
|
|
601
|
+
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
602
|
+
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path6.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
603
|
+
};
|
|
611
604
|
}
|
|
612
605
|
});
|
|
613
606
|
|
|
614
|
-
// src/audit/
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
function inspectionRead(root, key, read) {
|
|
621
|
-
const inspection = inspections.getStore();
|
|
622
|
-
if (!inspection || inspection.root !== path4.resolve(root)) return read();
|
|
623
|
-
if (!inspection.reads.has(key)) {
|
|
624
|
-
const pending = read();
|
|
625
|
-
inspection.reads.set(key, pending);
|
|
626
|
-
void pending.catch(() => {
|
|
627
|
-
if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
|
|
628
|
-
});
|
|
607
|
+
// src/audit/tree.ts
|
|
608
|
+
function glyphIndex(line) {
|
|
609
|
+
for (const glyph of GLYPHS) {
|
|
610
|
+
const idx = line.indexOf(glyph);
|
|
611
|
+
if (idx !== -1) return idx;
|
|
629
612
|
}
|
|
630
|
-
return
|
|
613
|
+
return -1;
|
|
631
614
|
}
|
|
632
|
-
function
|
|
633
|
-
return
|
|
615
|
+
function isSpacerLine(line) {
|
|
616
|
+
return /^[\s│|]*$/.test(line);
|
|
634
617
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
);
|
|
643
|
-
|
|
644
|
-
if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
|
|
645
|
-
return paths.sort();
|
|
618
|
+
function entryName(afterGlyph) {
|
|
619
|
+
let name = afterGlyph.replace(/^\s+/, "");
|
|
620
|
+
const hash2 = name.search(/\s+#/);
|
|
621
|
+
if (hash2 !== -1) name = name.slice(0, hash2);
|
|
622
|
+
const columns = name.search(/\s{2,}/);
|
|
623
|
+
if (columns !== -1) name = name.slice(0, columns);
|
|
624
|
+
name = name.trim();
|
|
625
|
+
if (!name || /\s/.test(name)) return null;
|
|
626
|
+
return name;
|
|
646
627
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
628
|
+
function extractTreeClaims(blockLines, blockStartLine) {
|
|
629
|
+
const glyphLines = blockLines.filter((l) => glyphIndex(l) !== -1).length;
|
|
630
|
+
if (glyphLines < MIN_GLYPH_LINES) return [];
|
|
631
|
+
const claims = [];
|
|
632
|
+
const stack = [];
|
|
633
|
+
let rootPrefix = "";
|
|
634
|
+
let started = false;
|
|
635
|
+
for (let i = 0; i < blockLines.length; i++) {
|
|
636
|
+
const line = blockLines[i];
|
|
637
|
+
const col = glyphIndex(line);
|
|
638
|
+
if (col === -1) {
|
|
639
|
+
if (isSpacerLine(line)) continue;
|
|
640
|
+
if (!started) {
|
|
641
|
+
const candidate = line.trim();
|
|
642
|
+
if (candidate.endsWith("/") && !/\s/.test(candidate)) {
|
|
643
|
+
rootPrefix = candidate.replace(/\/+$/, "");
|
|
644
|
+
claims.push({
|
|
645
|
+
path: rootPrefix,
|
|
646
|
+
line: blockStartLine + i,
|
|
647
|
+
excerpt: candidate
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
return claims;
|
|
653
|
+
}
|
|
654
|
+
started = true;
|
|
655
|
+
const name = entryName(line.slice(col + GLYPHS[0].length));
|
|
656
|
+
if (name === null) return claims;
|
|
657
|
+
while (stack.length > 0 && stack[stack.length - 1].col >= col) {
|
|
658
|
+
stack.pop();
|
|
659
|
+
}
|
|
660
|
+
const isDir = name.endsWith("/");
|
|
661
|
+
const cleanName = name.replace(/\/+$/, "");
|
|
662
|
+
const segments = [
|
|
663
|
+
...rootPrefix ? [rootPrefix] : [],
|
|
664
|
+
...stack.map((s) => s.name),
|
|
665
|
+
cleanName
|
|
666
|
+
];
|
|
667
|
+
claims.push({
|
|
668
|
+
path: segments.join("/"),
|
|
669
|
+
line: blockStartLine + i,
|
|
670
|
+
excerpt: name
|
|
671
|
+
});
|
|
672
|
+
if (isDir) stack.push({ col, name: cleanName });
|
|
673
|
+
}
|
|
674
|
+
return claims;
|
|
675
|
+
}
|
|
676
|
+
var MIN_GLYPH_LINES, GLYPHS;
|
|
677
|
+
var init_tree = __esm({
|
|
678
|
+
"src/audit/tree.ts"() {
|
|
650
679
|
"use strict";
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
inspections = new AsyncLocalStorage2();
|
|
654
|
-
sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
|
|
655
|
-
docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
|
|
656
|
-
sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
|
|
680
|
+
MIN_GLYPH_LINES = 3;
|
|
681
|
+
GLYPHS = ["\u251C\u2500\u2500", "\u2514\u2500\u2500"];
|
|
657
682
|
}
|
|
658
683
|
});
|
|
659
684
|
|
|
660
|
-
// src/audit/
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
["log", "-1", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
670
|
-
{ cwd: resolvedRoot }
|
|
671
|
-
);
|
|
672
|
-
const line = stdout.trim().split("\n")[0];
|
|
673
|
-
return line ? parseCommitLine(line) : null;
|
|
674
|
-
} catch {
|
|
675
|
-
return null;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
async function deletingCommitOf(resolvedRoot, relPath) {
|
|
679
|
-
try {
|
|
680
|
-
const { stdout } = await inspectionGit(
|
|
681
|
-
[
|
|
682
|
-
"log",
|
|
683
|
-
"-1",
|
|
684
|
-
"--diff-filter=D",
|
|
685
|
-
`--format=${COMMIT_FORMAT}`,
|
|
686
|
-
"--",
|
|
687
|
-
`:(literal)${relPath}`
|
|
688
|
-
],
|
|
689
|
-
{ cwd: resolvedRoot }
|
|
690
|
-
);
|
|
691
|
-
const line = stdout.trim().split("\n")[0];
|
|
692
|
-
return line ? parseCommitLine(line) : null;
|
|
693
|
-
} catch {
|
|
685
|
+
// src/audit/claims.ts
|
|
686
|
+
import MarkdownIt from "markdown-it";
|
|
687
|
+
function normalizePathToken(token) {
|
|
688
|
+
let t = token.trim();
|
|
689
|
+
if (!t) return null;
|
|
690
|
+
if (/\s/.test(t)) return null;
|
|
691
|
+
if (t.includes("://") || t.includes("\\")) return null;
|
|
692
|
+
if (/[*?[\]{}<>$`]/.test(t)) return null;
|
|
693
|
+
if (t.startsWith("/") || t.startsWith("~") || t.startsWith("./") || t.startsWith("../")) {
|
|
694
694
|
return null;
|
|
695
695
|
}
|
|
696
|
+
t = t.replace(/:\d+(?:-\d+)?$/, "");
|
|
697
|
+
if (t.includes(":")) return null;
|
|
698
|
+
const normalized = t.replace(/\/+$/, "");
|
|
699
|
+
if (!normalized) return null;
|
|
700
|
+
if (normalized.split("/").some((seg) => /^\.+$/.test(seg))) return null;
|
|
701
|
+
if (normalized.includes("/")) return normalized;
|
|
702
|
+
return ROOT_FILE_NAMES.has(normalized) ? normalized : null;
|
|
696
703
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
702
|
-
);
|
|
703
|
-
const line = stdout.trim().split("\n")[0];
|
|
704
|
-
return line ? parseCommitLine(line) : null;
|
|
705
|
-
} catch {
|
|
706
|
-
return null;
|
|
707
|
-
}
|
|
704
|
+
function exactTokenPath(line) {
|
|
705
|
+
const trimmed = line.trim();
|
|
706
|
+
if (!trimmed || /\s/.test(trimmed) || !trimmed.includes("/")) return null;
|
|
707
|
+
return normalizePathToken(trimmed);
|
|
708
708
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
709
|
+
function computeIgnoredLines(lines) {
|
|
710
|
+
const ignored = new Array(lines.length).fill(false);
|
|
711
|
+
let inRegion = false;
|
|
712
|
+
let ignoreNext = false;
|
|
713
|
+
for (let i = 0; i < lines.length; i++) {
|
|
714
|
+
const line = lines[i];
|
|
715
|
+
if (line.includes(IGNORE_START)) {
|
|
716
|
+
inRegion = true;
|
|
717
|
+
ignored[i] = true;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (line.includes(IGNORE_END)) {
|
|
721
|
+
inRegion = false;
|
|
722
|
+
ignored[i] = true;
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (inRegion) {
|
|
726
|
+
ignored[i] = true;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
if (ignoreNext) {
|
|
730
|
+
if (line.trim().length === 0) continue;
|
|
731
|
+
ignored[i] = true;
|
|
732
|
+
ignoreNext = false;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (line.includes(IGNORE_LINE)) {
|
|
736
|
+
ignored[i] = true;
|
|
737
|
+
const rest = line.replace(IGNORE_LINE, "").trim();
|
|
738
|
+
if (rest.length === 0) ignoreNext = true;
|
|
730
739
|
}
|
|
731
|
-
return { commits, total: commits.length };
|
|
732
|
-
} catch {
|
|
733
|
-
return null;
|
|
734
740
|
}
|
|
741
|
+
return ignored;
|
|
735
742
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
stat = await fs2.lstat(current);
|
|
759
|
-
} catch (error) {
|
|
760
|
-
if (error.code !== "ENOENT") throw error;
|
|
761
|
-
if (createParents && i < parts.length - 1) {
|
|
743
|
+
function extractClaims(content2) {
|
|
744
|
+
const lines = content2.split("\n");
|
|
745
|
+
const ignored = computeIgnoredLines(lines);
|
|
746
|
+
const paths = /* @__PURE__ */ new Map();
|
|
747
|
+
const counts = [];
|
|
748
|
+
const commands2 = /* @__PURE__ */ new Map();
|
|
749
|
+
const addPath = (claim) => {
|
|
750
|
+
const key = [claim.relativeTo ?? "scope", claim.path].join(":");
|
|
751
|
+
if (!paths.has(key)) paths.set(key, claim);
|
|
752
|
+
};
|
|
753
|
+
const addCommand = (claim) => {
|
|
754
|
+
const key = JSON.stringify([claim.scriptName, claim.directory, claim.scopeUnknown]);
|
|
755
|
+
if (!commands2.has(key)) commands2.set(key, claim);
|
|
756
|
+
};
|
|
757
|
+
const parsed = markdown.parse(lines.map((line, i) => ignored[i] ? "" : line).join("\n"), {});
|
|
758
|
+
for (const token of parsed) {
|
|
759
|
+
if (token.type !== "inline" || !token.map) continue;
|
|
760
|
+
let line = token.map[0] + 1;
|
|
761
|
+
for (const child of token.children ?? []) {
|
|
762
|
+
const target = child.type === "link_open" ? child.attrGet("href") : child.type === "image" ? child.attrGet("src") : null;
|
|
763
|
+
if (target && !/^(?:[a-z][a-z0-9+.-]*:|[\/#~])/i.test(target)) {
|
|
764
|
+
let pathname = null;
|
|
762
765
|
try {
|
|
763
|
-
|
|
764
|
-
} catch
|
|
765
|
-
|
|
766
|
+
pathname = decodeURIComponent(target.split(/[?#]/)[0]);
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
if (pathname && !/[$*?{}<>\\\x00-\x1f]/.test(pathname)) {
|
|
770
|
+
addPath({ path: pathname, line, excerpt: target, relativeTo: "document" });
|
|
766
771
|
}
|
|
767
|
-
stat = await fs2.lstat(current);
|
|
768
772
|
}
|
|
773
|
+
if (child.type === "softbreak" || child.type === "hardbreak") line++;
|
|
774
|
+
else line += child.content.split("\n").length - 1;
|
|
769
775
|
}
|
|
770
|
-
if (stat?.isSymbolicLink()) throw new Error(`Symlink in store path: ${relative}`);
|
|
771
|
-
}
|
|
772
|
-
return current;
|
|
773
|
-
}
|
|
774
|
-
async function readStoreJson(root, relative) {
|
|
775
|
-
try {
|
|
776
|
-
const file = await storePath(root, relative);
|
|
777
|
-
const raw = await readBoundedFile(file, 10 * 1024 * 1024);
|
|
778
|
-
if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
|
|
779
|
-
const parsed = JSON.parse(raw);
|
|
780
|
-
if (parsed === null) throw new Error("expected a JSON object, received null");
|
|
781
|
-
return parsed;
|
|
782
|
-
} catch (error) {
|
|
783
|
-
if (error.code === "ENOENT") return null;
|
|
784
|
-
throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
async function writeStoreJson(root, relative, value) {
|
|
788
|
-
const payload = JSON.stringify(value, null, 2) + "\n";
|
|
789
|
-
if (Buffer.byteLength(payload) > 10 * 1024 * 1024) {
|
|
790
|
-
throw new Error(`Mason store ${relative} exceeds 10 MiB`);
|
|
791
776
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
777
|
+
let shellCwd;
|
|
778
|
+
let inFence = false;
|
|
779
|
+
let fenceInfo = "";
|
|
780
|
+
let fenceMarker = "";
|
|
781
|
+
let blockLines = [];
|
|
782
|
+
let blockStartLine = 0;
|
|
783
|
+
const processBlock = () => {
|
|
784
|
+
for (const claim of extractTreeClaims(blockLines, blockStartLine)) {
|
|
785
|
+
addPath(claim);
|
|
801
786
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
787
|
+
for (let i = 0; i < blockLines.length; i++) {
|
|
788
|
+
const exact = exactTokenPath(blockLines[i]);
|
|
789
|
+
if (exact) {
|
|
790
|
+
addPath({
|
|
791
|
+
path: exact,
|
|
792
|
+
line: blockStartLine + i,
|
|
793
|
+
excerpt: blockLines[i].trim()
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
for (let i = 0; i < lines.length; i++) {
|
|
799
|
+
const line = lines[i];
|
|
800
|
+
const lineNo = i + 1;
|
|
801
|
+
const fenceMatch = line.match(/^\s*(```+|~~~+)(.*)$/);
|
|
802
|
+
if (fenceMatch) {
|
|
803
|
+
if (!inFence) {
|
|
804
|
+
inFence = true;
|
|
805
|
+
fenceMarker = fenceMatch[1][0];
|
|
806
|
+
fenceInfo = fenceMatch[2].trim().toLowerCase();
|
|
807
|
+
blockLines = [];
|
|
808
|
+
shellCwd = void 0;
|
|
809
|
+
blockStartLine = lineNo + 1;
|
|
810
|
+
} else if (fenceMatch[1][0] === fenceMarker) {
|
|
811
|
+
inFence = false;
|
|
812
|
+
processBlock();
|
|
813
|
+
}
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (inFence) {
|
|
817
|
+
blockLines.push(ignored[i] ? "" : line);
|
|
818
|
+
if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {
|
|
819
|
+
const scanned = commandClaims(line, lineNo, shellCwd, true);
|
|
820
|
+
shellCwd = scanned.cwd;
|
|
821
|
+
for (const claim of scanned.claims) addCommand(claim);
|
|
822
|
+
}
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (ignored[i]) continue;
|
|
826
|
+
for (const m of line.matchAll(/`([^`]+)`/g)) {
|
|
827
|
+
const normalized = normalizePathToken(m[1]);
|
|
828
|
+
if (normalized) {
|
|
829
|
+
addPath({ path: normalized, line: lineNo, excerpt: m[1] });
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
for (const m of line.matchAll(/"([A-Za-z][\w.@-]*(?:\/[\w.@-]+)+\/?)"/g)) {
|
|
833
|
+
const normalized = normalizePathToken(m[1]);
|
|
834
|
+
if (normalized) {
|
|
835
|
+
addPath({ path: normalized, line: lineNo, excerpt: m[1] });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
for (const m of line.matchAll(COUNT_RE)) {
|
|
839
|
+
const rest = line.slice((m.index ?? 0) + m[0].length);
|
|
840
|
+
if (COUNT_DENYLIST_RE.test(rest)) continue;
|
|
841
|
+
counts.push({
|
|
842
|
+
count: Number.parseInt(m[1], 10),
|
|
843
|
+
unit: m[2].toLowerCase(),
|
|
844
|
+
line: lineNo,
|
|
845
|
+
excerpt: m[0]
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
for (const match of line.matchAll(/`([^`]+)`/g)) {
|
|
849
|
+
for (const claim of commandClaims(match[1], lineNo, void 0, true).claims) addCommand(claim);
|
|
850
|
+
}
|
|
851
|
+
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims) addCommand(claim);
|
|
805
852
|
}
|
|
853
|
+
if (inFence) processBlock();
|
|
854
|
+
return {
|
|
855
|
+
paths: [...paths.values()],
|
|
856
|
+
counts,
|
|
857
|
+
commands: [...commands2.values()]
|
|
858
|
+
};
|
|
806
859
|
}
|
|
807
|
-
var
|
|
808
|
-
|
|
860
|
+
var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
|
|
861
|
+
var init_claims = __esm({
|
|
862
|
+
"src/audit/claims.ts"() {
|
|
809
863
|
"use strict";
|
|
810
|
-
|
|
811
|
-
|
|
864
|
+
init_commands();
|
|
865
|
+
init_tree();
|
|
866
|
+
ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
867
|
+
"package.json",
|
|
868
|
+
"package-lock.json",
|
|
869
|
+
"pnpm-workspace.yaml",
|
|
870
|
+
"tsconfig.json",
|
|
871
|
+
"tsup.config.ts",
|
|
872
|
+
"vitest.config.ts",
|
|
873
|
+
"Makefile",
|
|
874
|
+
"Dockerfile",
|
|
875
|
+
"docker-compose.yml",
|
|
876
|
+
"Cargo.toml",
|
|
877
|
+
"go.mod",
|
|
878
|
+
"go.sum",
|
|
879
|
+
"pyproject.toml",
|
|
880
|
+
"requirements.txt",
|
|
881
|
+
"Gemfile",
|
|
882
|
+
"composer.json",
|
|
883
|
+
"settings.gradle.kts",
|
|
884
|
+
"settings.gradle",
|
|
885
|
+
"build.gradle.kts",
|
|
886
|
+
"build.gradle",
|
|
887
|
+
"manifest.json",
|
|
888
|
+
"server.json",
|
|
889
|
+
"README.md",
|
|
890
|
+
"CHANGELOG.md",
|
|
891
|
+
"LICENSE",
|
|
892
|
+
"CLAUDE.md",
|
|
893
|
+
"AGENTS.md",
|
|
894
|
+
".gitignore",
|
|
895
|
+
".env.example"
|
|
896
|
+
]);
|
|
897
|
+
SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
|
|
898
|
+
markdown = new MarkdownIt("commonmark", { html: false });
|
|
899
|
+
COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
|
|
900
|
+
COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
|
|
901
|
+
IGNORE_LINE = "<!-- mason:ignore -->";
|
|
902
|
+
IGNORE_START = "<!-- mason:ignore-start -->";
|
|
903
|
+
IGNORE_END = "<!-- mason:ignore-end -->";
|
|
812
904
|
}
|
|
813
905
|
});
|
|
814
906
|
|
|
815
|
-
// src/audit/
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
907
|
+
// src/audit/git.ts
|
|
908
|
+
function parseCommitLine(line) {
|
|
909
|
+
const parts = line.split(" ");
|
|
910
|
+
if (parts.length < 3 || !parts[0]) return null;
|
|
911
|
+
return { hash: parts[0], date: parts[1], subject: parts.slice(2).join(" ") };
|
|
912
|
+
}
|
|
913
|
+
async function lastCommitOf(resolvedRoot, relPath) {
|
|
822
914
|
try {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
915
|
+
const { stdout } = await inspectionGit(
|
|
916
|
+
["log", "-1", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
917
|
+
{ cwd: resolvedRoot }
|
|
918
|
+
);
|
|
919
|
+
const line = stdout.trim().split("\n")[0];
|
|
920
|
+
return line ? parseCommitLine(line) : null;
|
|
921
|
+
} catch {
|
|
922
|
+
return null;
|
|
829
923
|
}
|
|
830
924
|
}
|
|
831
|
-
async function
|
|
925
|
+
async function deletingCommitOf(resolvedRoot, relPath) {
|
|
832
926
|
try {
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
927
|
+
const { stdout } = await inspectionGit(
|
|
928
|
+
[
|
|
929
|
+
"log",
|
|
930
|
+
"-1",
|
|
931
|
+
"--diff-filter=D",
|
|
932
|
+
`--format=${COMMIT_FORMAT}`,
|
|
933
|
+
"--",
|
|
934
|
+
`:(literal)${relPath}`
|
|
935
|
+
],
|
|
936
|
+
{ cwd: resolvedRoot }
|
|
937
|
+
);
|
|
938
|
+
const line = stdout.trim().split("\n")[0];
|
|
939
|
+
return line ? parseCommitLine(line) : null;
|
|
940
|
+
} catch {
|
|
941
|
+
return null;
|
|
839
942
|
}
|
|
840
943
|
}
|
|
841
|
-
async function
|
|
842
|
-
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
843
|
-
const directories = /* @__PURE__ */ new Set();
|
|
844
|
-
const readdir2 = ((directory, opts, callback) => {
|
|
845
|
-
const relative = path6.relative(root, directory).split(path6.sep).join("/") || ".";
|
|
846
|
-
directories.add(relative);
|
|
847
|
-
if (directories.size > MAX_INPUTS) {
|
|
848
|
-
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
auditInputPath(root, relative).then(() => {
|
|
852
|
-
nativeFs.readdir(directory, opts, callback);
|
|
853
|
-
}, callback);
|
|
854
|
-
});
|
|
855
|
-
const lstat2 = ((file, callback) => {
|
|
856
|
-
const relative = path6.relative(root, file).split(path6.sep).join("/") || ".";
|
|
857
|
-
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
858
|
-
});
|
|
859
|
-
const stream = fg2.stream(patterns, {
|
|
860
|
-
cwd: root,
|
|
861
|
-
ignore: options.ignore,
|
|
862
|
-
onlyFiles: false,
|
|
863
|
-
onlyDirectories: options.onlyDirectories,
|
|
864
|
-
objectMode: true,
|
|
865
|
-
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
866
|
-
fs: { readdir: readdir2, lstat: lstat2 },
|
|
867
|
-
concurrency: 16
|
|
868
|
-
});
|
|
869
|
-
const matches = [];
|
|
944
|
+
async function firstCommitOf(resolvedRoot, relPath) {
|
|
870
945
|
try {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
const largest = [...matches.reduce((counts, item) => {
|
|
880
|
-
const dir = item.split("/")[0];
|
|
881
|
-
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
882
|
-
return counts;
|
|
883
|
-
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
884
|
-
throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count4]) => `${dir} (${count4})`).join(", ")}. Narrow this check's inputs.`);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
} finally {
|
|
888
|
-
stream.destroy();
|
|
946
|
+
const { stdout } = await inspectionGit(
|
|
947
|
+
["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
948
|
+
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
949
|
+
);
|
|
950
|
+
const line = stdout.trim().split("\n")[0];
|
|
951
|
+
return line ? parseCommitLine(line) : null;
|
|
952
|
+
} catch {
|
|
953
|
+
return null;
|
|
889
954
|
}
|
|
890
|
-
return matches.sort();
|
|
891
|
-
}
|
|
892
|
-
async function gitSourcePaths(root) {
|
|
893
|
-
return auditGitPaths(root, "sources");
|
|
894
955
|
}
|
|
895
|
-
async function
|
|
896
|
-
if (!
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
956
|
+
async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
|
|
957
|
+
if (!fromHash || fromHash === "unknown") return null;
|
|
958
|
+
try {
|
|
959
|
+
const { stdout } = await inspectionGit(
|
|
960
|
+
[
|
|
961
|
+
"log",
|
|
962
|
+
`${fromHash}..HEAD`,
|
|
963
|
+
`--format=%x01${COMMIT_FORMAT}`,
|
|
964
|
+
"--name-only",
|
|
965
|
+
"--",
|
|
966
|
+
...pathspecs
|
|
967
|
+
],
|
|
968
|
+
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
969
|
+
);
|
|
970
|
+
const commits = [];
|
|
971
|
+
for (const block of stdout.split("")) {
|
|
972
|
+
if (!block.trim()) continue;
|
|
973
|
+
const lines = block.split("\n").filter((l) => l.trim().length > 0);
|
|
974
|
+
const ref = parseCommitLine(lines[0]);
|
|
975
|
+
if (!ref) continue;
|
|
976
|
+
commits.push({ ...ref, files: lines.slice(1).map((l) => l.trim()) });
|
|
909
977
|
}
|
|
978
|
+
return { commits, total: commits.length };
|
|
979
|
+
} catch {
|
|
980
|
+
return null;
|
|
910
981
|
}
|
|
911
|
-
return ignored;
|
|
912
982
|
}
|
|
913
|
-
var
|
|
914
|
-
var
|
|
915
|
-
"src/audit/
|
|
983
|
+
var COMMIT_FORMAT;
|
|
984
|
+
var init_git = __esm({
|
|
985
|
+
"src/audit/git.ts"() {
|
|
916
986
|
"use strict";
|
|
917
|
-
init_files();
|
|
918
|
-
init_storage();
|
|
919
|
-
init_git_read();
|
|
920
987
|
init_inspection();
|
|
921
|
-
|
|
988
|
+
COMMIT_FORMAT = "%H%x09%cI%x09%s";
|
|
922
989
|
}
|
|
923
990
|
});
|
|
924
991
|
|
|
925
992
|
// src/audit/docs.ts
|
|
926
993
|
import fs4 from "fs/promises";
|
|
927
994
|
import path7 from "path";
|
|
928
|
-
import
|
|
995
|
+
import fg5 from "fast-glob";
|
|
929
996
|
async function dirtyDocs(resolvedRoot, files) {
|
|
930
997
|
if (!files.length) return /* @__PURE__ */ new Set();
|
|
931
998
|
try {
|
|
@@ -977,7 +1044,7 @@ async function discoverDocPaths(root) {
|
|
|
977
1044
|
const paths = [];
|
|
978
1045
|
const unique = [...new Set(candidates)].sort();
|
|
979
1046
|
for (let offset = 0; offset < unique.length; offset += 256) {
|
|
980
|
-
paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) =>
|
|
1047
|
+
paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg5.escapePath(p)), {
|
|
981
1048
|
ignore: excluded,
|
|
982
1049
|
followSymbolicLinks: false,
|
|
983
1050
|
label: "Documentation discovery"
|
|
@@ -1007,6 +1074,7 @@ async function localDocPaths(root) {
|
|
|
1007
1074
|
return paths;
|
|
1008
1075
|
}
|
|
1009
1076
|
function documentScope(file) {
|
|
1077
|
+
if (!/^(readme|agents|claude)\.md$/i.test(path7.posix.basename(file))) return ".";
|
|
1010
1078
|
const parent = path7.posix.dirname(file);
|
|
1011
1079
|
return path7.posix.basename(parent).toLowerCase() === ".claude" ? path7.posix.dirname(parent) : parent;
|
|
1012
1080
|
}
|
|
@@ -1028,7 +1096,7 @@ async function readDocs(resolvedRoot) {
|
|
|
1028
1096
|
path: candidate,
|
|
1029
1097
|
content: content2,
|
|
1030
1098
|
scope: documentScope(candidate),
|
|
1031
|
-
kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : "instructions",
|
|
1099
|
+
kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path7.posix.basename(candidate)) ? "instructions" : "guide",
|
|
1032
1100
|
lineCount: content2.split("\n").length,
|
|
1033
1101
|
lastCommit,
|
|
1034
1102
|
dirty: dirty.has(candidate),
|
|
@@ -1585,22 +1653,6 @@ var init_drift = __esm({
|
|
|
1585
1653
|
}
|
|
1586
1654
|
});
|
|
1587
1655
|
|
|
1588
|
-
// src/audit/types.ts
|
|
1589
|
-
var ALL_CHECKS;
|
|
1590
|
-
var init_types = __esm({
|
|
1591
|
-
"src/audit/types.ts"() {
|
|
1592
|
-
"use strict";
|
|
1593
|
-
ALL_CHECKS = [
|
|
1594
|
-
"deleted-reference",
|
|
1595
|
-
"new-module",
|
|
1596
|
-
"stale-count",
|
|
1597
|
-
"dead-command",
|
|
1598
|
-
"deps-changed",
|
|
1599
|
-
"decision-anchor-drift"
|
|
1600
|
-
];
|
|
1601
|
-
}
|
|
1602
|
-
});
|
|
1603
|
-
|
|
1604
1656
|
// src/audit/checks/deleted-reference.ts
|
|
1605
1657
|
import path12 from "path";
|
|
1606
1658
|
async function checkDeletedReferences(ctx) {
|
|
@@ -1669,7 +1721,7 @@ var init_deleted_reference = __esm({
|
|
|
1669
1721
|
});
|
|
1670
1722
|
|
|
1671
1723
|
// src/audit/checks/new-module.ts
|
|
1672
|
-
import
|
|
1724
|
+
import fg6 from "fast-glob";
|
|
1673
1725
|
import fs8 from "fs/promises";
|
|
1674
1726
|
import path13 from "path";
|
|
1675
1727
|
function escapeRegExp(text2) {
|
|
@@ -1731,7 +1783,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1731
1783
|
const result = /* @__PURE__ */ new Map();
|
|
1732
1784
|
for (const dir of parents) {
|
|
1733
1785
|
const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
|
|
1734
|
-
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${
|
|
1786
|
+
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
|
|
1735
1787
|
ignore,
|
|
1736
1788
|
onlyDirectories: true,
|
|
1737
1789
|
label: "Module directory discovery",
|
|
@@ -1744,7 +1796,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1744
1796
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1745
1797
|
let count4 = 0;
|
|
1746
1798
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1747
|
-
count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) =>
|
|
1799
|
+
count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)), {
|
|
1748
1800
|
ignore,
|
|
1749
1801
|
followSymbolicLinks: false,
|
|
1750
1802
|
label: `Source discovery in ${dir}`
|
|
@@ -2079,6 +2131,87 @@ var init_dead_command = __esm({
|
|
|
2079
2131
|
}
|
|
2080
2132
|
});
|
|
2081
2133
|
|
|
2134
|
+
// src/audit/dependency-claims.ts
|
|
2135
|
+
import path16 from "path";
|
|
2136
|
+
import { parse as parseToml } from "smol-toml";
|
|
2137
|
+
function dependencyText(content2) {
|
|
2138
|
+
return content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, (text2) => text2.replace(/[^\n]/g, " ")).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
|
|
2139
|
+
}
|
|
2140
|
+
function declarations(file, source) {
|
|
2141
|
+
const result = /* @__PURE__ */ new Map();
|
|
2142
|
+
if (!source) return result;
|
|
2143
|
+
if (path16.posix.basename(file) === "package.json") {
|
|
2144
|
+
const json = JSON.parse(source);
|
|
2145
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "engines"]) {
|
|
2146
|
+
for (const [name, value] of Object.entries(json[section] ?? {})) {
|
|
2147
|
+
if (typeof value === "string") result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
} else if (file.endsWith("libs.versions.toml")) {
|
|
2151
|
+
const catalog = parseToml(source);
|
|
2152
|
+
for (const section of ["libraries", "plugins"]) for (const [alias, entry] of Object.entries(catalog[section] ?? {})) {
|
|
2153
|
+
if (typeof entry === "string") {
|
|
2154
|
+
const parts = entry.split(":");
|
|
2155
|
+
if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
|
|
2156
|
+
} else if (entry && typeof entry === "object") {
|
|
2157
|
+
const name = entry.module ?? (entry.group && entry.name ? `${entry.group}:${entry.name}` : entry.id);
|
|
2158
|
+
const version = typeof entry.version === "string" ? entry.version : catalog.versions?.[entry.version?.ref];
|
|
2159
|
+
if (typeof name === "string") result.set(name, typeof version === "string" ? version : "unspecified");
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
} else if (/build\.gradle(?:\.kts)?$/.test(file)) {
|
|
2163
|
+
for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g)) result.set(match[1], match[2]);
|
|
2164
|
+
for (const match of source.matchAll(/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g)) result.set(match[1], match[2]);
|
|
2165
|
+
}
|
|
2166
|
+
return result;
|
|
2167
|
+
}
|
|
2168
|
+
async function matchDependencyChanges(root, from, head, files, content2) {
|
|
2169
|
+
const matches = [];
|
|
2170
|
+
const lines = dependencyText(content2).split("\n");
|
|
2171
|
+
let incomplete = files.length > 64;
|
|
2172
|
+
for (const file of files.slice(0, 64)) {
|
|
2173
|
+
if (!/(?:^|\/)(?:package\.json|libs\.versions\.toml|build\.gradle(?:\.kts)?)$/.test(file)) {
|
|
2174
|
+
incomplete = true;
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
try {
|
|
2178
|
+
const read = async (revision) => {
|
|
2179
|
+
const { stdout: entry } = await inspectionGit(["ls-tree", revision, "--", `:(literal)${file}`], { cwd: root, timeout: 1e4 });
|
|
2180
|
+
if (!entry) return "";
|
|
2181
|
+
if (!entry.startsWith("100644 ") && !entry.startsWith("100755 ")) throw new Error("Unsupported manifest mode");
|
|
2182
|
+
return (await inspectionGit(["show", `${revision}:${file}`], { cwd: root, maxBuffer: 1024 * 1024, timeout: 1e4 })).stdout;
|
|
2183
|
+
};
|
|
2184
|
+
const [before, after] = await Promise.all([read(from), read(head)]);
|
|
2185
|
+
const old = declarations(file, before), next = declarations(file, after);
|
|
2186
|
+
for (const dependency of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
|
|
2187
|
+
if (old.get(dependency) === next.get(dependency)) continue;
|
|
2188
|
+
const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2189
|
+
const mention = new RegExp(`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`, "i");
|
|
2190
|
+
const line = lines.findIndex((text2) => mention.test(text2));
|
|
2191
|
+
if (line < 0) continue;
|
|
2192
|
+
matches.push({
|
|
2193
|
+
dependency,
|
|
2194
|
+
manifest: file,
|
|
2195
|
+
before: old.get(dependency) ?? null,
|
|
2196
|
+
after: next.get(dependency) ?? null,
|
|
2197
|
+
line: line + 1,
|
|
2198
|
+
excerpt: lines[line].trim().slice(0, 500)
|
|
2199
|
+
});
|
|
2200
|
+
if (matches.length >= 100) return { matches, incomplete: true };
|
|
2201
|
+
}
|
|
2202
|
+
} catch {
|
|
2203
|
+
incomplete = true;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return { matches, incomplete };
|
|
2207
|
+
}
|
|
2208
|
+
var init_dependency_claims = __esm({
|
|
2209
|
+
"src/audit/dependency-claims.ts"() {
|
|
2210
|
+
"use strict";
|
|
2211
|
+
init_inspection();
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2082
2215
|
// src/audit/release-metadata.ts
|
|
2083
2216
|
async function git(root, args) {
|
|
2084
2217
|
return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
@@ -2158,13 +2291,6 @@ async function checkDepsChanged(ctx) {
|
|
|
2158
2291
|
});
|
|
2159
2292
|
continue;
|
|
2160
2293
|
}
|
|
2161
|
-
if (doc.dirty) {
|
|
2162
|
-
result.skipped.push({
|
|
2163
|
-
check: "deps-changed",
|
|
2164
|
-
doc: doc.path,
|
|
2165
|
-
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
2166
|
-
});
|
|
2167
|
-
}
|
|
2168
2294
|
let content2 = doc.content;
|
|
2169
2295
|
if (doc.dirty) {
|
|
2170
2296
|
try {
|
|
@@ -2177,7 +2303,6 @@ async function checkDepsChanged(ctx) {
|
|
|
2177
2303
|
continue;
|
|
2178
2304
|
}
|
|
2179
2305
|
}
|
|
2180
|
-
if (!hasDependencyContent(content2)) continue;
|
|
2181
2306
|
const range = await commitsTouchingSince(
|
|
2182
2307
|
ctx.root,
|
|
2183
2308
|
doc.lastCommit.hash,
|
|
@@ -2201,16 +2326,32 @@ async function checkDepsChanged(ctx) {
|
|
|
2201
2326
|
range.commits = relevant;
|
|
2202
2327
|
range.total = relevant.length;
|
|
2203
2328
|
if (range.total === 0) continue;
|
|
2204
|
-
const
|
|
2329
|
+
const relevance = await matchDependencyChanges(
|
|
2330
|
+
ctx.root,
|
|
2331
|
+
doc.lastCommit.hash,
|
|
2332
|
+
ctx.headHash,
|
|
2333
|
+
[...new Set(range.commits.flatMap((commit) => commit.files))],
|
|
2334
|
+
content2
|
|
2335
|
+
);
|
|
2336
|
+
if (!relevance.matches.length && !hasDependencyContent(content2)) continue;
|
|
2337
|
+
const match = relevance.matches[0];
|
|
2338
|
+
if (doc.dirty && match) result.skipped.push({
|
|
2339
|
+
check: "deps-changed",
|
|
2340
|
+
doc: doc.path,
|
|
2341
|
+
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
2342
|
+
});
|
|
2205
2343
|
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
2206
2344
|
type: "deps-changed",
|
|
2207
|
-
|
|
2208
|
-
|
|
2345
|
+
...match ? {} : { resolution: "informational" },
|
|
2346
|
+
message: match ? `${match.dependency} changed from ${match.before ?? "absent"} to ${match.after ?? "removed"} in ${match.manifest}; review ${doc.path}:${match.line}: ${match.excerpt}${relevance.matches.length > 1 ? ` (${relevance.matches.length - 1} more matches in evidence)` : ""}` : `Dependency manifests changed after the document baseline ${doc.lastCommit.hash.slice(0, 7)} (${doc.lastCommit.date.slice(0, 10)}); no specific affected passage was established. Informational only.${relevance.incomplete ? " Some manifest declarations could not be matched." : ""}`,
|
|
2347
|
+
anchor: { doc: doc.path, line: match?.line ?? null, excerpt: match?.excerpt ?? null },
|
|
2209
2348
|
evidence: {
|
|
2210
2349
|
kind: "doc-behind-manifests",
|
|
2211
2350
|
docLastCommit: doc.lastCommit,
|
|
2212
2351
|
manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
|
|
2213
|
-
totalCommits: range.total
|
|
2352
|
+
totalCommits: range.total,
|
|
2353
|
+
matches: relevance.matches,
|
|
2354
|
+
matchingIncomplete: relevance.incomplete
|
|
2214
2355
|
}
|
|
2215
2356
|
});
|
|
2216
2357
|
}
|
|
@@ -2220,6 +2361,7 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
|
2220
2361
|
var init_deps_changed = __esm({
|
|
2221
2362
|
"src/audit/checks/deps-changed.ts"() {
|
|
2222
2363
|
"use strict";
|
|
2364
|
+
init_dependency_claims();
|
|
2223
2365
|
init_docs();
|
|
2224
2366
|
init_release_metadata();
|
|
2225
2367
|
init_git();
|
|
@@ -2244,7 +2386,7 @@ var init_deps_changed = __esm({
|
|
|
2244
2386
|
});
|
|
2245
2387
|
|
|
2246
2388
|
// src/context/lexical.ts
|
|
2247
|
-
import
|
|
2389
|
+
import path17 from "path";
|
|
2248
2390
|
function tokenize(text2) {
|
|
2249
2391
|
return text2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
|
|
2250
2392
|
}
|
|
@@ -2257,7 +2399,7 @@ function tokenSet(text2) {
|
|
|
2257
2399
|
function scoreEntry(taskTokens, entry) {
|
|
2258
2400
|
const nameTokens = tokenSet(entry.name);
|
|
2259
2401
|
const descTokens = tokenSet(entry.description);
|
|
2260
|
-
const fileTokens = tokenSet(entry.files.map((f) =>
|
|
2402
|
+
const fileTokens = tokenSet(entry.files.map((f) => path17.basename(f)).join(" "));
|
|
2261
2403
|
let score = 0;
|
|
2262
2404
|
for (const token of taskTokens) {
|
|
2263
2405
|
if (nameTokens.has(token)) score += 3;
|
|
@@ -2591,7 +2733,7 @@ __export(decisions_exports, {
|
|
|
2591
2733
|
withDecisionWrite: () => withDecisionWrite
|
|
2592
2734
|
});
|
|
2593
2735
|
import fs9 from "fs/promises";
|
|
2594
|
-
import
|
|
2736
|
+
import path18 from "path";
|
|
2595
2737
|
import { createHash } from "crypto";
|
|
2596
2738
|
import { z as z3 } from "zod";
|
|
2597
2739
|
async function loadDecisionStore(rootDir) {
|
|
@@ -2689,7 +2831,7 @@ async function upsertDecision(rootDir, input) {
|
|
|
2689
2831
|
if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
|
|
2690
2832
|
for (const file of files) {
|
|
2691
2833
|
try {
|
|
2692
|
-
await fs9.access(
|
|
2834
|
+
await fs9.access(path18.join(rootDir, file));
|
|
2693
2835
|
} catch {
|
|
2694
2836
|
warnings.push(`anchor file does not exist on disk: ${file}`);
|
|
2695
2837
|
}
|
|
@@ -2814,9 +2956,9 @@ var init_decisions = __esm({
|
|
|
2814
2956
|
});
|
|
2815
2957
|
|
|
2816
2958
|
// src/decisions/drift.ts
|
|
2817
|
-
import
|
|
2959
|
+
import path19 from "path";
|
|
2818
2960
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
2819
|
-
const resolvedRoot =
|
|
2961
|
+
const resolvedRoot = path19.resolve(rootDir);
|
|
2820
2962
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
2821
2963
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
2822
2964
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -3012,7 +3154,9 @@ var init_findings = __esm({
|
|
|
3012
3154
|
kind: z4.literal("doc-behind-manifests"),
|
|
3013
3155
|
docLastCommit: commitSchema,
|
|
3014
3156
|
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
3015
|
-
totalCommits: count
|
|
3157
|
+
totalCommits: count,
|
|
3158
|
+
matches: z4.array(z4.object({ dependency: z4.string(), manifest: z4.string(), before: z4.string().nullable(), after: z4.string().nullable(), line: z4.number().int().positive(), excerpt: z4.string() })).optional(),
|
|
3159
|
+
matchingIncomplete: z4.boolean().optional()
|
|
3016
3160
|
}),
|
|
3017
3161
|
z4.object({
|
|
3018
3162
|
kind: z4.literal("decision-anchor"),
|
|
@@ -3028,7 +3172,7 @@ var init_findings = __esm({
|
|
|
3028
3172
|
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
3029
3173
|
confidence: z4.enum(["certain", "likely"])
|
|
3030
3174
|
});
|
|
3031
|
-
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.
|
|
3175
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.enum(["recheck", "informational"]).optional() });
|
|
3032
3176
|
checkResultSchema = z4.object({
|
|
3033
3177
|
issues: z4.array(issueSchema),
|
|
3034
3178
|
advisories: z4.array(advisorySchema),
|
|
@@ -3091,14 +3235,18 @@ async function readRecord(root, id) {
|
|
|
3091
3235
|
if (raw === null) return null;
|
|
3092
3236
|
const record = recordSchema.parse(raw);
|
|
3093
3237
|
const { digest: stored, ...payload } = record;
|
|
3094
|
-
if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || event.
|
|
3238
|
+
if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || (event.outcome === "dismissed" ? !event.reasonCode || event.finding.type !== "deps-changed" : !event.reviewer || !!event.reasonCode) || event.evidence.fingerprint !== evidenceFingerprint(event.evidence, event.reasonCode))) {
|
|
3095
3239
|
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
3096
3240
|
}
|
|
3097
3241
|
return record;
|
|
3098
3242
|
}
|
|
3099
|
-
function
|
|
3243
|
+
function evidenceFingerprint(evidence, reason2) {
|
|
3244
|
+
return digest([evidence.scope, evidence.tree, ...reason2 === "no-dependency-claims" ? [] : [evidence.lastScopeCommit], evidence.paths]);
|
|
3245
|
+
}
|
|
3246
|
+
function scopes(finding, reason2) {
|
|
3100
3247
|
const files = [finding.anchor.doc], e = finding.evidence;
|
|
3101
3248
|
if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
3249
|
+
if (reason2 === "no-dependency-claims" && e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`];
|
|
3102
3250
|
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
3103
3251
|
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
3104
3252
|
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
@@ -3109,9 +3257,9 @@ function scopes(finding) {
|
|
|
3109
3257
|
return `:(literal)${file}`;
|
|
3110
3258
|
}).sort();
|
|
3111
3259
|
}
|
|
3112
|
-
async function reviewEvidence(root, finding) {
|
|
3260
|
+
async function reviewEvidence(root, finding, reason2) {
|
|
3113
3261
|
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
3114
|
-
const scope = scopes(finding);
|
|
3262
|
+
const scope = scopes(finding, reason2);
|
|
3115
3263
|
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
3116
3264
|
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
3117
3265
|
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
@@ -3137,7 +3285,7 @@ async function reviewEvidence(root, finding) {
|
|
|
3137
3285
|
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
3138
3286
|
}
|
|
3139
3287
|
}
|
|
3140
|
-
const fingerprint =
|
|
3288
|
+
const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason2);
|
|
3141
3289
|
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
3142
3290
|
return {
|
|
3143
3291
|
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
@@ -3184,7 +3332,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
3184
3332
|
const record = await readRecord(root, id);
|
|
3185
3333
|
if (!record) return null;
|
|
3186
3334
|
const event = record.events.at(-1);
|
|
3187
|
-
const current = await reviewEvidence(root, event.finding);
|
|
3335
|
+
const current = await reviewEvidence(root, event.finding, event.reasonCode);
|
|
3188
3336
|
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
3189
3337
|
const base = {
|
|
3190
3338
|
id,
|
|
@@ -3194,7 +3342,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
3194
3342
|
outcome: event.outcome,
|
|
3195
3343
|
reviewedHead: event.evidence.head
|
|
3196
3344
|
};
|
|
3197
|
-
if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding)) !== digest(event.evidence.scope)) return { ...base, status: "reopened", reason: "Relevant document or repository evidence changed after the assessment; prepare a new review." };
|
|
3345
|
+
if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding, event.reasonCode)) !== digest(event.evidence.scope)) return { ...base, status: "reopened", reason: "Relevant document or repository evidence changed after the assessment; prepare a new review." };
|
|
3198
3346
|
return { ...base, status: event.outcome === "deferred" ? "deferred" : "current", reason: event.outcome === "deferred" ? "The review was deferred and remains outstanding." : "An explicit assessment covers the current committed scope. This does not approve an engineering decision or certify correctness." };
|
|
3199
3347
|
} catch (error) {
|
|
3200
3348
|
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -3222,9 +3370,13 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3222
3370
|
decisionId: finding.evidence.decisionId,
|
|
3223
3371
|
hint: "Use review_decision to prepare evidence and record authorized acceptance, reaffirmation or retirement, then verify this original repair baseline. An advisory assessment cannot approve a decision."
|
|
3224
3372
|
};
|
|
3373
|
+
if (request.action === "dismiss") {
|
|
3374
|
+
if (finding.type !== "deps-changed" || finding.evidence.kind !== "doc-behind-manifests") throw new Error("One-step dismissal is only available for dependency advisories.");
|
|
3375
|
+
if (!request.reasonCode || !request.note) throw new Error("Dismissal requires reasonCode and note; it does not require a reviewer or prepared token.");
|
|
3376
|
+
} else if (request.reasonCode) throw new Error("reasonCode is only used with dismiss.");
|
|
3225
3377
|
const prepare = async () => {
|
|
3226
3378
|
const record = await readRecord(root, request.findingId);
|
|
3227
|
-
const evidence = await reviewEvidence(root, finding);
|
|
3379
|
+
const evidence = await reviewEvidence(root, finding, request.reasonCode);
|
|
3228
3380
|
await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
|
|
3229
3381
|
const token = digest({ baseline: baseline.original, finding, evidence, record });
|
|
3230
3382
|
return { record, evidence, token };
|
|
@@ -3253,10 +3405,10 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3253
3405
|
hint: "Inspect the original finding, scoped document and relevant code changes. Record addressed, inapplicable or deferred only when the user or cited project review authorizes that assessment. Supply the actual reviewer, reason in note and reviewToken. Relevant edits must be committed first. Deferral remains outstanding. Review identities are recorded assertions, not authenticated approvals."
|
|
3254
3406
|
};
|
|
3255
3407
|
}
|
|
3256
|
-
if (!request.reviewer || !request.note || !request.reviewToken) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
|
|
3408
|
+
if (request.action !== "dismiss" && (!request.reviewer || !request.note || !request.reviewToken)) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
|
|
3257
3409
|
return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
|
|
3258
3410
|
const state = await prepare();
|
|
3259
|
-
if (state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
|
|
3411
|
+
if (request.action !== "dismiss" && state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
|
|
3260
3412
|
if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
|
|
3261
3413
|
if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
|
|
3262
3414
|
const { dirty, documentDigest, ...evidence } = state.evidence;
|
|
@@ -3264,7 +3416,8 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3264
3416
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3265
3417
|
reviewer: request.reviewer,
|
|
3266
3418
|
note: request.note,
|
|
3267
|
-
outcome: request.action,
|
|
3419
|
+
outcome: request.action === "dismiss" ? "dismissed" : request.action,
|
|
3420
|
+
reasonCode: request.reasonCode,
|
|
3268
3421
|
finding,
|
|
3269
3422
|
evidence
|
|
3270
3423
|
});
|
|
@@ -3275,11 +3428,11 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3275
3428
|
status: "recorded",
|
|
3276
3429
|
recordPath: recordPath(request.findingId),
|
|
3277
3430
|
event,
|
|
3278
|
-
hint: "Review and commit this assessment record through the normal project workflow, then verify the original repair baseline. Relevant changes reopen the assessment; deferral remains outstanding."
|
|
3431
|
+
hint: request.action === "dismiss" ? "Dismissal saved and effective locally. Commit the record to share it. No-dependency-claims dismissals reopen on document content changes; unrelated-manifest-change dismissals reopen on scoped document or manifest changes. This does not approve a decision." : "Review and commit this assessment record through the normal project workflow, then verify the original repair baseline. Relevant changes reopen the assessment; deferral remains outstanding."
|
|
3279
3432
|
};
|
|
3280
3433
|
});
|
|
3281
3434
|
}
|
|
3282
|
-
var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3435
|
+
var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3283
3436
|
var init_advisory_review = __esm({
|
|
3284
3437
|
"src/audit/advisory-review.ts"() {
|
|
3285
3438
|
"use strict";
|
|
@@ -3305,11 +3458,13 @@ var init_advisory_review = __esm({
|
|
|
3305
3458
|
lastScopeCommit: commitSchema2,
|
|
3306
3459
|
paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
|
|
3307
3460
|
});
|
|
3461
|
+
dismissalReasonSchema = z6.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
3308
3462
|
eventSchema2 = z6.object({
|
|
3309
3463
|
at: z6.string().datetime(),
|
|
3310
|
-
reviewer: z6.string().trim().min(1).max(200),
|
|
3464
|
+
reviewer: z6.string().trim().min(1).max(200).optional(),
|
|
3311
3465
|
note: z6.string().trim().min(1).max(2500),
|
|
3312
|
-
outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
|
|
3466
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
3467
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
3313
3468
|
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
3314
3469
|
evidence: evidenceSchema2
|
|
3315
3470
|
});
|
|
@@ -3321,13 +3476,14 @@ var init_advisory_review = __esm({
|
|
|
3321
3476
|
recordPath: z6.string().optional(),
|
|
3322
3477
|
reviewer: z6.string().optional(),
|
|
3323
3478
|
note: z6.string().optional(),
|
|
3324
|
-
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
3479
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
|
|
3325
3480
|
reviewedHead: z6.string().optional()
|
|
3326
3481
|
});
|
|
3327
3482
|
advisoryReviewRequest = z6.object({
|
|
3328
3483
|
baselinePath: z6.string().min(1),
|
|
3329
3484
|
findingId: hashSchema,
|
|
3330
|
-
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
3485
|
+
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
3486
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
3331
3487
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
3332
3488
|
note: eventSchema2.shape.note.optional(),
|
|
3333
3489
|
reviewToken: hashSchema.optional()
|
|
@@ -3338,12 +3494,12 @@ var init_advisory_review = __esm({
|
|
|
3338
3494
|
|
|
3339
3495
|
// src/audit/audit.ts
|
|
3340
3496
|
import fs11 from "fs/promises";
|
|
3341
|
-
import
|
|
3497
|
+
import path20 from "path";
|
|
3342
3498
|
async function computeAudit(rootDir, options = {}) {
|
|
3343
3499
|
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
3344
3500
|
}
|
|
3345
3501
|
async function auditCurrent(rootDir, options) {
|
|
3346
|
-
const resolvedRoot =
|
|
3502
|
+
const resolvedRoot = path20.resolve(rootDir);
|
|
3347
3503
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
3348
3504
|
if (docs.length === 0) return null;
|
|
3349
3505
|
const report = {
|
|
@@ -3377,7 +3533,7 @@ async function auditCurrent(rootDir, options) {
|
|
|
3377
3533
|
}
|
|
3378
3534
|
let decisionsPresent = false;
|
|
3379
3535
|
try {
|
|
3380
|
-
await fs11.access(
|
|
3536
|
+
await fs11.access(path20.join(resolvedRoot, ".mason", "decisions"));
|
|
3381
3537
|
decisionsPresent = true;
|
|
3382
3538
|
} catch {
|
|
3383
3539
|
}
|
|
@@ -3389,13 +3545,21 @@ async function auditCurrent(rootDir, options) {
|
|
|
3389
3545
|
changesSinceDoc,
|
|
3390
3546
|
decisionsPresent
|
|
3391
3547
|
};
|
|
3548
|
+
const policy = (await loadProjectConfig(resolvedRoot)).audit;
|
|
3392
3549
|
const selected = options.checks ?? ALL_CHECKS;
|
|
3393
3550
|
for (const name of ALL_CHECKS) {
|
|
3394
3551
|
if (!selected.includes(name)) continue;
|
|
3395
|
-
const
|
|
3552
|
+
const exclusions = policy?.exclude?.[name] ?? [];
|
|
3553
|
+
const included = await includedCheckPaths(resolvedRoot, docs.map((doc) => doc.path), exclusions);
|
|
3554
|
+
const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
|
|
3555
|
+
if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
|
|
3556
|
+
const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
|
|
3557
|
+
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
|
|
3396
3558
|
report.checksRun.push(name);
|
|
3397
3559
|
report.issues.push(...issues);
|
|
3398
|
-
|
|
3560
|
+
const advisoryPaths = await includedCheckPaths(resolvedRoot, [...new Set(advisories.map((f) => f.anchor.doc))], exclusions);
|
|
3561
|
+
report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
|
|
3562
|
+
for (const f of advisories) if (!advisoryPaths.has(f.anchor.doc)) (report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
|
|
3399
3563
|
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
3400
3564
|
report.skippedChecks.push(...skipped);
|
|
3401
3565
|
}
|
|
@@ -3407,6 +3571,8 @@ async function auditCurrent(rootDir, options) {
|
|
|
3407
3571
|
var init_audit = __esm({
|
|
3408
3572
|
"src/audit/audit.ts"() {
|
|
3409
3573
|
"use strict";
|
|
3574
|
+
init_files();
|
|
3575
|
+
init_policy();
|
|
3410
3576
|
init_profile();
|
|
3411
3577
|
init_drift();
|
|
3412
3578
|
init_snapshot();
|
|
@@ -3432,7 +3598,7 @@ __export(repair_exports, {
|
|
|
3432
3598
|
withRepairSession: () => withRepairSession
|
|
3433
3599
|
});
|
|
3434
3600
|
import fs12 from "fs/promises";
|
|
3435
|
-
import
|
|
3601
|
+
import path21 from "path";
|
|
3436
3602
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3437
3603
|
import { z as z7 } from "zod";
|
|
3438
3604
|
function recheckable(finding) {
|
|
@@ -3453,7 +3619,7 @@ async function docState(root) {
|
|
|
3453
3619
|
docs.push([doc, null]);
|
|
3454
3620
|
}
|
|
3455
3621
|
}
|
|
3456
|
-
return digest(docs);
|
|
3622
|
+
return digest([docs, (await loadProjectConfig(root)).audit]);
|
|
3457
3623
|
}
|
|
3458
3624
|
async function stableAudit(root, checks, options = {}) {
|
|
3459
3625
|
return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
|
|
@@ -3522,8 +3688,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
|
|
|
3522
3688
|
}
|
|
3523
3689
|
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
3524
3690
|
const root = await fs12.realpath(rootDir);
|
|
3525
|
-
const declaredRoot =
|
|
3526
|
-
const relative =
|
|
3691
|
+
const declaredRoot = path21.resolve(rootDir);
|
|
3692
|
+
const relative = path21.isAbsolute(baselinePath) ? path21.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
3527
3693
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
3528
3694
|
const { digest: savedDigest, ...payload } = stored;
|
|
3529
3695
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
@@ -3572,6 +3738,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
3572
3738
|
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
3573
3739
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
3574
3740
|
}
|
|
3741
|
+
if (current.excludedChecks?.some((excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc)) {
|
|
3742
|
+
return { ...base, status: "resolved", reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained." };
|
|
3743
|
+
}
|
|
3575
3744
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
3576
3745
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
3577
3746
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
@@ -3582,6 +3751,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
3582
3751
|
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
3583
3752
|
return { ...base, review, status: "review-required", reason: review.reason };
|
|
3584
3753
|
}
|
|
3754
|
+
if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
|
|
3755
|
+
return { ...base, status: "resolved", reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required." };
|
|
3756
|
+
}
|
|
3585
3757
|
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
3586
3758
|
return {
|
|
3587
3759
|
...base,
|
|
@@ -3610,7 +3782,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
3610
3782
|
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
3611
3783
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
3612
3784
|
for (const f of findings) counts[f.status]++;
|
|
3613
|
-
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f) && !reviewedIds.has(findingId(f)));
|
|
3785
|
+
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f) && f.resolution !== "informational" && !reviewedIds.has(findingId(f)));
|
|
3614
3786
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
3615
3787
|
return {
|
|
3616
3788
|
version: 1,
|
|
@@ -3633,7 +3805,7 @@ function repairExitCode(report) {
|
|
|
3633
3805
|
function formatRepairSummary(report) {
|
|
3634
3806
|
return [
|
|
3635
3807
|
"Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
|
|
3636
|
-
...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason + "\n findingId: " + f.id),
|
|
3808
|
+
...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + (f.current?.message ?? "Historical evidence at " + report.baselineHead.slice(0, 7) + ": " + f.original.message) + "\n " + f.reason + "\n findingId: " + f.id),
|
|
3637
3809
|
...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
|
|
3638
3810
|
...report.diagnostics.map((d) => " [unverified] " + d),
|
|
3639
3811
|
...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
|
|
@@ -3666,13 +3838,14 @@ var init_repair = __esm({
|
|
|
3666
3838
|
docs: z7.array(z7.object({
|
|
3667
3839
|
path: z7.string().refine((value) => normalizeRepoPath(value) === value),
|
|
3668
3840
|
scope: z7.string().optional(),
|
|
3669
|
-
kind: z7.enum(["instructions", "readme"]).optional(),
|
|
3841
|
+
kind: z7.enum(["instructions", "readme", "guide"]).optional(),
|
|
3670
3842
|
lastCommit: commitSchema.nullable(),
|
|
3671
3843
|
dirty: z7.boolean(),
|
|
3672
3844
|
lineCount: count2
|
|
3673
3845
|
})).nonempty(),
|
|
3674
3846
|
decisionsChecked: z7.boolean(),
|
|
3675
3847
|
clean: z7.boolean(),
|
|
3848
|
+
excludedChecks: z7.array(z7.object({ check: checkSchema, doc: z7.string() })).optional(),
|
|
3676
3849
|
issues: z7.array(issueSchema),
|
|
3677
3850
|
advisories: z7.array(advisorySchema),
|
|
3678
3851
|
suppressedAdvisories: z7.array(advisorySchema).optional(),
|
|
@@ -3698,9 +3871,9 @@ var init_repair = __esm({
|
|
|
3698
3871
|
|
|
3699
3872
|
// src/automation/evidence.ts
|
|
3700
3873
|
import fs13 from "fs/promises";
|
|
3701
|
-
import
|
|
3874
|
+
import path22 from "path";
|
|
3702
3875
|
import { createHash as createHash3 } from "crypto";
|
|
3703
|
-
import
|
|
3876
|
+
import fg7 from "fast-glob";
|
|
3704
3877
|
import { z as z8 } from "zod";
|
|
3705
3878
|
async function git3(root, ...args) {
|
|
3706
3879
|
return profilePhase("automation.git", async () => (await execGit(args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
|
|
@@ -3718,6 +3891,11 @@ async function readInputs(root) {
|
|
|
3718
3891
|
}
|
|
3719
3892
|
async function collectInputs(root) {
|
|
3720
3893
|
const docPaths = await discoverDocPaths(root);
|
|
3894
|
+
const auditPolicy = (await loadProjectConfig(root)).audit;
|
|
3895
|
+
const allowed = /* @__PURE__ */ new Map();
|
|
3896
|
+
for (const name of ["deleted-reference", "new-module", "stale-count", "dead-command"]) {
|
|
3897
|
+
allowed.set(name, await includedCheckPaths(root, docPaths, auditPolicy?.exclude?.[name] ?? []));
|
|
3898
|
+
}
|
|
3721
3899
|
const [headText, docStatus, shallowPath, replacements] = await Promise.all([
|
|
3722
3900
|
git3(root, "rev-parse", "HEAD"),
|
|
3723
3901
|
docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
|
|
@@ -3727,7 +3905,7 @@ async function collectInputs(root) {
|
|
|
3727
3905
|
const head = headText.trim();
|
|
3728
3906
|
let shallow = null;
|
|
3729
3907
|
try {
|
|
3730
|
-
shallow = await fs13.readFile(
|
|
3908
|
+
shallow = await fs13.readFile(path22.resolve(root, shallowPath.trim()), "utf8");
|
|
3731
3909
|
} catch (error) {
|
|
3732
3910
|
if (error.code !== "ENOENT") throw error;
|
|
3733
3911
|
}
|
|
@@ -3746,18 +3924,18 @@ async function collectInputs(root) {
|
|
|
3746
3924
|
docContents.push([file, text2]);
|
|
3747
3925
|
const parsed = { path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") };
|
|
3748
3926
|
parsedDocs.push(parsed);
|
|
3749
|
-
for (const claim of parsed.claims.paths) {
|
|
3927
|
+
for (const claim of allowed.get("deleted-reference").has(file) ? parsed.claims.paths : []) {
|
|
3750
3928
|
const scope = pathClaimScope(file, claim);
|
|
3751
3929
|
if (scope?.candidates.some(gitMetadataPath)) continue;
|
|
3752
3930
|
for (const candidate of scope?.candidates ?? []) {
|
|
3753
3931
|
if (optionalMasonPath(candidate)) continue;
|
|
3754
|
-
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(
|
|
3932
|
+
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path22.posix.dirname(candidate))]);
|
|
3755
3933
|
claims.push([candidate, filePresent, parentPresent]);
|
|
3756
3934
|
}
|
|
3757
3935
|
}
|
|
3758
3936
|
}
|
|
3759
|
-
const combinedDocs = moduleDocumentation(parsedDocs);
|
|
3760
|
-
const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
|
|
3937
|
+
const combinedDocs = moduleDocumentation(parsedDocs.filter((doc) => allowed.get("new-module").has(doc.path)));
|
|
3938
|
+
const countClaims = parsedDocs.filter((doc) => allowed.get("stale-count").has(doc.path)).flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
|
|
3761
3939
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
3762
3940
|
const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
3763
3941
|
if (error.code === "ENOENT") return "absent";
|
|
@@ -3766,8 +3944,8 @@ async function collectInputs(root) {
|
|
|
3766
3944
|
const [modules, counts, commands2, decisionFiles] = await Promise.all([
|
|
3767
3945
|
combinedDocs ? moduleCandidates(root, combinedDocs) : [],
|
|
3768
3946
|
Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
|
|
3769
|
-
commandInputs(root, parsedDocs),
|
|
3770
|
-
|
|
3947
|
+
commandInputs(root, parsedDocs.filter((doc) => allowed.get("dead-command").has(doc.path))),
|
|
3948
|
+
fg7(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
|
|
3771
3949
|
]);
|
|
3772
3950
|
const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
|
|
3773
3951
|
await storePath(root, file);
|
|
@@ -3777,7 +3955,7 @@ async function collectInputs(root) {
|
|
|
3777
3955
|
git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
3778
3956
|
git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
3779
3957
|
]) : ["", ""];
|
|
3780
|
-
const common = [
|
|
3958
|
+
const common = [7, engineVersion, head, shallow, replacements, docContents, auditPolicy];
|
|
3781
3959
|
const keys = {
|
|
3782
3960
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
3783
3961
|
"new-module": hash([common, modules]),
|
|
@@ -3824,6 +4002,8 @@ var engineVersion, hash, content, cacheSchema;
|
|
|
3824
4002
|
var init_evidence = __esm({
|
|
3825
4003
|
"src/automation/evidence.ts"() {
|
|
3826
4004
|
"use strict";
|
|
4005
|
+
init_policy();
|
|
4006
|
+
init_files();
|
|
3827
4007
|
init_git_read();
|
|
3828
4008
|
init_profile();
|
|
3829
4009
|
init_docs();
|
|
@@ -3837,7 +4017,7 @@ var init_evidence = __esm({
|
|
|
3837
4017
|
init_dead_command();
|
|
3838
4018
|
init_storage();
|
|
3839
4019
|
init_advisory_review();
|
|
3840
|
-
engineVersion = true ? "0.19.
|
|
4020
|
+
engineVersion = true ? "0.19.1" : "development";
|
|
3841
4021
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3842
4022
|
content = readAuditInput;
|
|
3843
4023
|
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
@@ -3858,7 +4038,7 @@ __export(model_exports, {
|
|
|
3858
4038
|
});
|
|
3859
4039
|
import { z as z9 } from "zod";
|
|
3860
4040
|
import fs14 from "fs/promises";
|
|
3861
|
-
import
|
|
4041
|
+
import path23 from "path";
|
|
3862
4042
|
async function loadSetup(root) {
|
|
3863
4043
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
3864
4044
|
if (raw === null) return null;
|
|
@@ -3867,7 +4047,7 @@ async function loadSetup(root) {
|
|
|
3867
4047
|
async function effectiveSetup(root) {
|
|
3868
4048
|
const local = await loadSetup(root);
|
|
3869
4049
|
if (local) return { root, setup: local };
|
|
3870
|
-
const common = await fs14.realpath(
|
|
4050
|
+
const common = await fs14.realpath(path23.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
|
|
3871
4051
|
const own = await fs14.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
3872
4052
|
if (common === own) return { root, setup: null };
|
|
3873
4053
|
const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
|
|
@@ -3880,7 +4060,7 @@ async function effectiveSetup(root) {
|
|
|
3880
4060
|
if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
|
|
3881
4061
|
throw error;
|
|
3882
4062
|
}
|
|
3883
|
-
const inheritedCommon = await fs14.realpath(
|
|
4063
|
+
const inheritedCommon = await fs14.realpath(path23.resolve(
|
|
3884
4064
|
inheritedRoot,
|
|
3885
4065
|
(await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
|
|
3886
4066
|
));
|
|
@@ -3963,7 +4143,7 @@ var init_launcher = __esm({
|
|
|
3963
4143
|
"use strict";
|
|
3964
4144
|
init_evidence();
|
|
3965
4145
|
init_model();
|
|
3966
|
-
executingVersion = () => true ? "0.19.
|
|
4146
|
+
executingVersion = () => true ? "0.19.1" : "development";
|
|
3967
4147
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3968
4148
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3969
4149
|
}
|
|
@@ -4089,7 +4269,7 @@ __export(files_exports, {
|
|
|
4089
4269
|
readText: () => readText
|
|
4090
4270
|
});
|
|
4091
4271
|
import fs18 from "fs/promises";
|
|
4092
|
-
import
|
|
4272
|
+
import path27 from "path";
|
|
4093
4273
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4094
4274
|
async function readText(root, file) {
|
|
4095
4275
|
try {
|
|
@@ -4122,7 +4302,7 @@ async function applyEdit(root, edit) {
|
|
|
4122
4302
|
return true;
|
|
4123
4303
|
}
|
|
4124
4304
|
const file = await storePath(root, edit.path, true);
|
|
4125
|
-
const temporary =
|
|
4305
|
+
const temporary = path27.join(path27.dirname(file), ".mason-setup-" + randomUUID3() + ".tmp");
|
|
4126
4306
|
try {
|
|
4127
4307
|
const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
|
|
4128
4308
|
const handle = await fs18.open(temporary, "wx", mode);
|
|
@@ -4714,7 +4894,7 @@ var init_runtime = __esm({
|
|
|
4714
4894
|
|
|
4715
4895
|
// src/automation/hook-root.ts
|
|
4716
4896
|
import fs20 from "fs/promises";
|
|
4717
|
-
import
|
|
4897
|
+
import path28 from "path";
|
|
4718
4898
|
var init_hook_root = __esm({
|
|
4719
4899
|
"src/automation/hook-root.ts"() {
|
|
4720
4900
|
"use strict";
|
|
@@ -5124,9 +5304,11 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
5124
5304
|
- For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
|
|
5125
5305
|
- \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
|
|
5126
5306
|
|
|
5127
|
-
- When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings.
|
|
5307
|
+
- When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings. Specific suppressed advisories remain unresolved; editing a doc does not approve it. Informational dependency notices require no review.
|
|
5128
5308
|
- When advisory assessment is authorized, use \`review_advisory(action: "prepare", baselinePath, findingId)\` to inspect original evidence and history. Record \`addressed\`, \`inapplicable\`, or \`deferred\` with the actual reviewer, reason in note, and returned reviewToken only after relevant edits are committed. Commit the assessment and verify the same baseline again. Relevant evidence changes reopen it; deferral stays outstanding. Decision findings use \`review_decision\`; advisory closure never approves a decision.
|
|
5129
5309
|
|
|
5310
|
+
- For a dependency advisory, \`review_advisory(action: "dismiss", baselinePath, findingId, reasonCode, note)\` records a one-step dismissal without reviewer or token. Use \`no-dependency-claims\` for a document with no dependency claims (reopens on document content changes), or \`unrelated-manifest-change\` for reviewed changes that do not affect its guidance (reopens on document or manifest changes). Keep the relevant scope committed; commit the dismissal to share it. This never approves a decision.
|
|
5311
|
+
|
|
5130
5312
|
- When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason-auto status\` / \`mason-auto check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
|
|
5131
5313
|
|
|
5132
5314
|
Inspect source for what the retrieved context does not answer.
|
|
@@ -5557,10 +5739,10 @@ var init_check = __esm({
|
|
|
5557
5739
|
});
|
|
5558
5740
|
|
|
5559
5741
|
// src/review/evidence.ts
|
|
5560
|
-
import
|
|
5742
|
+
import path29 from "path";
|
|
5561
5743
|
import { z as z22 } from "zod";
|
|
5562
5744
|
function relativeArtifact(root, file) {
|
|
5563
|
-
const relative =
|
|
5745
|
+
const relative = path29.isAbsolute(file) ? path29.relative(root, file) : file;
|
|
5564
5746
|
const normalized = normalizeRepoPath(relative);
|
|
5565
5747
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
5566
5748
|
return normalized;
|
|
@@ -5657,7 +5839,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
5657
5839
|
const report = await readStoreJson(root, file);
|
|
5658
5840
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
5659
5841
|
const sourceRoot = check.sourceRoot ?? root;
|
|
5660
|
-
if (!
|
|
5842
|
+
if (!path29.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
|
|
5661
5843
|
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
5662
5844
|
result.outcome = parsed.outcome;
|
|
5663
5845
|
result.counts = parsed.counts;
|
|
@@ -5779,7 +5961,7 @@ var init_evidence2 = __esm({
|
|
|
5779
5961
|
|
|
5780
5962
|
// src/review/review.ts
|
|
5781
5963
|
import fs21 from "fs/promises";
|
|
5782
|
-
import
|
|
5964
|
+
import path30 from "path";
|
|
5783
5965
|
import { execFile as execFile7 } from "child_process";
|
|
5784
5966
|
import { promisify as promisify7 } from "util";
|
|
5785
5967
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
@@ -5808,7 +5990,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
5808
5990
|
return matchingPaths(record.files, changedFiles);
|
|
5809
5991
|
}
|
|
5810
5992
|
async function computeReview(rootDir, base, options = {}) {
|
|
5811
|
-
const resolvedRoot =
|
|
5993
|
+
const resolvedRoot = path30.resolve(rootDir);
|
|
5812
5994
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
5813
5995
|
if (head === "unknown") return null;
|
|
5814
5996
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -5858,7 +6040,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
5858
6040
|
analyzed,
|
|
5859
6041
|
async (relPath) => {
|
|
5860
6042
|
try {
|
|
5861
|
-
await fs21.access(
|
|
6043
|
+
await fs21.access(path30.join(resolvedRoot, relPath));
|
|
5862
6044
|
return true;
|
|
5863
6045
|
} catch {
|
|
5864
6046
|
return false;
|
|
@@ -5993,7 +6175,7 @@ var init_onboarding = __esm({
|
|
|
5993
6175
|
});
|
|
5994
6176
|
|
|
5995
6177
|
// src/impact/references.ts
|
|
5996
|
-
import
|
|
6178
|
+
import path31 from "path";
|
|
5997
6179
|
function templateEnd(content2, start) {
|
|
5998
6180
|
let i = start + 1, interpolation = 0;
|
|
5999
6181
|
while (i < content2.length) {
|
|
@@ -6119,10 +6301,10 @@ function jvmImports(content2) {
|
|
|
6119
6301
|
}
|
|
6120
6302
|
function resolveImport(file, specifier, available) {
|
|
6121
6303
|
if (!specifier.startsWith("./") && !specifier.startsWith("../")) return null;
|
|
6122
|
-
const relative =
|
|
6304
|
+
const relative = path31.posix.normalize(path31.posix.join(path31.posix.dirname(file), specifier));
|
|
6123
6305
|
if (relative.startsWith("../")) return null;
|
|
6124
6306
|
if (available.has(relative)) return relative;
|
|
6125
|
-
const ext =
|
|
6307
|
+
const ext = path31.posix.extname(relative);
|
|
6126
6308
|
const replacements = { ".js": [".ts", ".tsx"], ".mjs": [".mts"], ".cjs": [".cts"] };
|
|
6127
6309
|
const candidates = ext ? (replacements[ext] ?? []).map((e) => relative.slice(0, -ext.length) + e) : jsExtensions.flatMap((e) => [relative + e, relative + "/index" + e]);
|
|
6128
6310
|
const found = candidates.filter((f) => available.has(f));
|
|
@@ -6134,13 +6316,13 @@ function mentionsPath(content2, target) {
|
|
|
6134
6316
|
function createReferenceMatcher(targets2, available, targetContents = /* @__PURE__ */ new Map()) {
|
|
6135
6317
|
const counts = /* @__PURE__ */ new Map();
|
|
6136
6318
|
for (const file of available) {
|
|
6137
|
-
const name =
|
|
6319
|
+
const name = path31.posix.basename(file, path31.posix.extname(file));
|
|
6138
6320
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
6139
6321
|
}
|
|
6140
6322
|
const jvmTargets = new Map([...targetContents].filter(([file]) => /\.(?:kt|kts|java)$/.test(file)).map(([file, content2]) => [file, declaredSymbols(content2)]));
|
|
6141
6323
|
return (source) => {
|
|
6142
6324
|
if (targets2.includes(source.path)) return [];
|
|
6143
|
-
const resolved = new Set(jsExtensions.includes(
|
|
6325
|
+
const resolved = new Set(jsExtensions.includes(path31.posix.extname(source.path)) ? imports(source.content).map((s) => resolveImport(source.path, s, available)).filter(Boolean) : []);
|
|
6144
6326
|
if (/\.(?:kt|kts|java)$/.test(source.path)) {
|
|
6145
6327
|
const imported = jvmImports(source.content);
|
|
6146
6328
|
for (const [target, symbols] of jvmTargets) if (symbols.some((symbol) => imported.some((name) => name === symbol || name.startsWith(symbol + ".")))) resolved.add(target);
|
|
@@ -6156,7 +6338,7 @@ function createReferenceMatcher(targets2, available, targetContents = /* @__PURE
|
|
|
6156
6338
|
add("explicit-path", target);
|
|
6157
6339
|
continue;
|
|
6158
6340
|
}
|
|
6159
|
-
const ext =
|
|
6341
|
+
const ext = path31.posix.extname(target), name = path31.posix.basename(target, ext);
|
|
6160
6342
|
if (!/\.(?:[cm]?[jt]sx?|py|kt|java|go|rs|swift|rb|cs|cpp|c|h|hpp|dart|php)$/.test(ext) || name.startsWith(".") || name.length < 3 || genericNames.has(name.toLowerCase()) || counts.get(name) !== 1) continue;
|
|
6161
6343
|
if (word(name).test(source.content)) {
|
|
6162
6344
|
add("name-candidate", name);
|
|
@@ -6185,11 +6367,11 @@ var impact_exports = {};
|
|
|
6185
6367
|
__export(impact_exports, {
|
|
6186
6368
|
analyzeImpact: () => analyzeImpact
|
|
6187
6369
|
});
|
|
6188
|
-
import
|
|
6370
|
+
import path32 from "path";
|
|
6189
6371
|
import { execFile as execFile8 } from "child_process";
|
|
6190
6372
|
import { promisify as promisify8 } from "util";
|
|
6191
6373
|
async function analyzeImpact(rootDir, targetFiles) {
|
|
6192
|
-
const resolvedRoot =
|
|
6374
|
+
const resolvedRoot = path32.resolve(rootDir);
|
|
6193
6375
|
const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
|
|
6194
6376
|
const [cochange, references, tests] = await Promise.all([
|
|
6195
6377
|
getCochangeFiles(resolvedRoot, resolvedTargets),
|
|
@@ -6303,9 +6485,9 @@ async function getRelatedTests(rootDir, targetFiles) {
|
|
|
6303
6485
|
const testFiles = await (await createFileAccess(rootDir)).list(testPatterns);
|
|
6304
6486
|
const results = [];
|
|
6305
6487
|
for (const target of targetFiles) {
|
|
6306
|
-
const targetBaseName =
|
|
6488
|
+
const targetBaseName = path32.basename(target).replace(/\.[^.]+$/, "");
|
|
6307
6489
|
for (const testFile of testFiles) {
|
|
6308
|
-
const testBaseName =
|
|
6490
|
+
const testBaseName = path32.basename(testFile).replace(/\.[^.]+$/, "");
|
|
6309
6491
|
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
6310
6492
|
if (sourceName === targetBaseName) {
|
|
6311
6493
|
results.push({
|
|
@@ -6333,13 +6515,13 @@ var assemble_exports = {};
|
|
|
6333
6515
|
__export(assemble_exports, {
|
|
6334
6516
|
assembleContext: () => assembleContext
|
|
6335
6517
|
});
|
|
6336
|
-
import
|
|
6518
|
+
import path33 from "path";
|
|
6337
6519
|
import fs22 from "fs/promises";
|
|
6338
6520
|
async function collectImpact(root, candidates) {
|
|
6339
6521
|
const targets2 = /* @__PURE__ */ new Set();
|
|
6340
6522
|
let sourceFiles;
|
|
6341
6523
|
for (const candidate of sanitizeRepoPaths(candidates)) {
|
|
6342
|
-
const stat = await fs22.stat(
|
|
6524
|
+
const stat = await fs22.stat(path33.join(root, candidate)).catch(() => null);
|
|
6343
6525
|
if (stat?.isDirectory()) {
|
|
6344
6526
|
sourceFiles ??= await (await createFileAccess(root)).list();
|
|
6345
6527
|
for (const file of sourceFiles) {
|
|
@@ -6365,7 +6547,7 @@ async function collectImpact(root, candidates) {
|
|
|
6365
6547
|
};
|
|
6366
6548
|
}
|
|
6367
6549
|
async function assembleContext(rootDir, task, files) {
|
|
6368
|
-
const resolvedRoot =
|
|
6550
|
+
const resolvedRoot = path33.resolve(rootDir);
|
|
6369
6551
|
const mapState = await inspectSnapshot(resolvedRoot);
|
|
6370
6552
|
const snapshot = mapState.snapshot;
|
|
6371
6553
|
const store = await loadDecisionStore(resolvedRoot);
|
|
@@ -6908,8 +7090,8 @@ __export(client_exports, {
|
|
|
6908
7090
|
function createConfluenceClient(config, fetchFn = fetch) {
|
|
6909
7091
|
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
6910
7092
|
const auth = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
|
|
6911
|
-
async function call(method,
|
|
6912
|
-
const res = await fetchFn(`${baseUrl}${
|
|
7093
|
+
async function call(method, path43, body) {
|
|
7094
|
+
const res = await fetchFn(`${baseUrl}${path43}`, {
|
|
6913
7095
|
method,
|
|
6914
7096
|
headers: {
|
|
6915
7097
|
Authorization: auth,
|
|
@@ -6921,7 +7103,7 @@ function createConfluenceClient(config, fetchFn = fetch) {
|
|
|
6921
7103
|
if (!res.ok) {
|
|
6922
7104
|
const text2 = await res.text();
|
|
6923
7105
|
throw new Error(
|
|
6924
|
-
`Confluence ${method} ${
|
|
7106
|
+
`Confluence ${method} ${path43} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
|
|
6925
7107
|
);
|
|
6926
7108
|
}
|
|
6927
7109
|
if (res.status === 204) return null;
|
|
@@ -7023,15 +7205,15 @@ __export(config_exports2, {
|
|
|
7023
7205
|
validateProvider: () => validateProvider
|
|
7024
7206
|
});
|
|
7025
7207
|
import fs24 from "fs/promises";
|
|
7026
|
-
import
|
|
7208
|
+
import path34 from "path";
|
|
7027
7209
|
import os3 from "os";
|
|
7028
7210
|
import { execFile as execFile9 } from "child_process";
|
|
7029
7211
|
import { promisify as promisify9 } from "util";
|
|
7030
7212
|
function configDir() {
|
|
7031
|
-
return
|
|
7213
|
+
return path34.join(os3.homedir(), ".mason");
|
|
7032
7214
|
}
|
|
7033
7215
|
function configFile() {
|
|
7034
|
-
return
|
|
7216
|
+
return path34.join(configDir(), "config.json");
|
|
7035
7217
|
}
|
|
7036
7218
|
async function loadConfig() {
|
|
7037
7219
|
try {
|
|
@@ -7204,16 +7386,16 @@ var init_renderer = __esm({
|
|
|
7204
7386
|
|
|
7205
7387
|
// src/confluence/diff.ts
|
|
7206
7388
|
import fs25 from "fs/promises";
|
|
7207
|
-
import
|
|
7389
|
+
import path35 from "path";
|
|
7208
7390
|
import { createHash as createHash6 } from "crypto";
|
|
7209
7391
|
function hashDescription(description) {
|
|
7210
7392
|
return createHash6("sha256").update(description, "utf8").digest("hex");
|
|
7211
7393
|
}
|
|
7212
7394
|
function syncStateDir(rootDir) {
|
|
7213
|
-
return
|
|
7395
|
+
return path35.join(rootDir, ".mason");
|
|
7214
7396
|
}
|
|
7215
7397
|
function syncStatePath(rootDir) {
|
|
7216
|
-
return
|
|
7398
|
+
return path35.join(syncStateDir(rootDir), "confluence-sync.json");
|
|
7217
7399
|
}
|
|
7218
7400
|
async function loadSyncState(rootDir) {
|
|
7219
7401
|
try {
|
|
@@ -7786,7 +7968,7 @@ import { z as z24 } from "zod";
|
|
|
7786
7968
|
init_repair();
|
|
7787
7969
|
init_advisory_review();
|
|
7788
7970
|
import fs26 from "fs/promises";
|
|
7789
|
-
import
|
|
7971
|
+
import path36 from "path";
|
|
7790
7972
|
import { execFile as execFile11 } from "child_process";
|
|
7791
7973
|
import { promisify as promisify11 } from "util";
|
|
7792
7974
|
|
|
@@ -7795,11 +7977,11 @@ init_audit();
|
|
|
7795
7977
|
init_types();
|
|
7796
7978
|
init_repair();
|
|
7797
7979
|
init_findings();
|
|
7798
|
-
import
|
|
7980
|
+
import path25 from "path";
|
|
7799
7981
|
|
|
7800
7982
|
// src/audit/advisory-cli.ts
|
|
7801
7983
|
init_advisory_review();
|
|
7802
|
-
import
|
|
7984
|
+
import path24 from "path";
|
|
7803
7985
|
import { stripVTControlCharacters } from "util";
|
|
7804
7986
|
|
|
7805
7987
|
// src/audit/cli.ts
|
|
@@ -7866,7 +8048,7 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
7866
8048
|
"- new-module: review whether the directory needs documenting; an omission alone does not establish a defect. Preserve scope and describe only what you verified."
|
|
7867
8049
|
);
|
|
7868
8050
|
lines.push(
|
|
7869
|
-
"-
|
|
8051
|
+
"- Specific historical ADVISORIES require a separate authorized assessment using review_advisory with the baselinePath and findingId from verification. Prepare and inspect evidence first; record addressed, inapplicable or deferred with the actual reviewer, note and reviewToken. Dependency notices marked resolution: informational require no review. Dependency advisories may be dismissed with action: dismiss, reasonCode (no-dependency-claims or unrelated-manifest-change), and note, without a reviewer or token. Decision findings use review_decision. Candidates marked resolution: recheck can be rerun to see whether the condition remains; this does not establish semantic approval. Report what remains unknown."
|
|
7870
8052
|
);
|
|
7871
8053
|
lines.push("");
|
|
7872
8054
|
lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
|
|
@@ -7897,10 +8079,10 @@ import { promisify as promisify3 } from "util";
|
|
|
7897
8079
|
|
|
7898
8080
|
// src/analyzers/base.ts
|
|
7899
8081
|
import fs16 from "fs/promises";
|
|
7900
|
-
import
|
|
8082
|
+
import fg8 from "fast-glob";
|
|
7901
8083
|
var BaseAnalyzer = class {
|
|
7902
8084
|
async findFiles(patterns, root) {
|
|
7903
|
-
return
|
|
8085
|
+
return fg8(patterns, {
|
|
7904
8086
|
cwd: root,
|
|
7905
8087
|
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
|
|
7906
8088
|
absolute: true
|
|
@@ -8112,7 +8294,7 @@ async function isGitRepo(dir) {
|
|
|
8112
8294
|
|
|
8113
8295
|
// src/mcp/sampler.ts
|
|
8114
8296
|
init_files();
|
|
8115
|
-
import
|
|
8297
|
+
import path26 from "path";
|
|
8116
8298
|
import { execFile as execFile4 } from "child_process";
|
|
8117
8299
|
import { promisify as promisify4 } from "util";
|
|
8118
8300
|
var exec2 = promisify4(execFile4);
|
|
@@ -8220,8 +8402,8 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
8220
8402
|
const projectConfig = access.config;
|
|
8221
8403
|
for (const filePath of projectConfig.alwaysInclude ?? []) {
|
|
8222
8404
|
if (selected.size >= maxFiles) break;
|
|
8223
|
-
const resolvedPath =
|
|
8224
|
-
if (!resolvedPath.startsWith(
|
|
8405
|
+
const resolvedPath = path26.resolve(rootDir, filePath);
|
|
8406
|
+
if (!resolvedPath.startsWith(path26.resolve(rootDir))) continue;
|
|
8225
8407
|
selected.set(filePath, "always-include (project config)");
|
|
8226
8408
|
}
|
|
8227
8409
|
let configCount = 0;
|
|
@@ -8287,7 +8469,7 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
8287
8469
|
if (!line) continue;
|
|
8288
8470
|
if (line.includes("node_modules") || line.includes("/build/") || line.includes(".gradle") || line.includes("/generated/"))
|
|
8289
8471
|
continue;
|
|
8290
|
-
const ext =
|
|
8472
|
+
const ext = path26.extname(line).slice(1);
|
|
8291
8473
|
if (!SOURCE_EXTENSIONS.includes(ext)) continue;
|
|
8292
8474
|
fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
|
|
8293
8475
|
}
|
|
@@ -8704,7 +8886,7 @@ async function buildContext(dir) {
|
|
|
8704
8886
|
};
|
|
8705
8887
|
}
|
|
8706
8888
|
async function analyzeProject(dir) {
|
|
8707
|
-
const rootDir =
|
|
8889
|
+
const rootDir = path36.resolve(dir);
|
|
8708
8890
|
const context = await buildContext(rootDir);
|
|
8709
8891
|
const results = await runAll(context);
|
|
8710
8892
|
const projectSnapshot = await detectProjectSnapshot(rootDir);
|
|
@@ -8759,7 +8941,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8759
8941
|
const present = [];
|
|
8760
8942
|
for (const file of buildFiles) {
|
|
8761
8943
|
try {
|
|
8762
|
-
await fs26.access(
|
|
8944
|
+
await fs26.access(path36.join(rootDir, file));
|
|
8763
8945
|
present.push(file);
|
|
8764
8946
|
} catch {
|
|
8765
8947
|
}
|
|
@@ -8801,7 +8983,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8801
8983
|
const sourceFiles = await access.list();
|
|
8802
8984
|
const fileCounts = {};
|
|
8803
8985
|
for (const file of sourceFiles) {
|
|
8804
|
-
const ext =
|
|
8986
|
+
const ext = path36.extname(file).slice(1);
|
|
8805
8987
|
fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
|
|
8806
8988
|
}
|
|
8807
8989
|
return {
|
|
@@ -8812,7 +8994,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8812
8994
|
};
|
|
8813
8995
|
}
|
|
8814
8996
|
async function getCodeSamples(dir, count4 = 15) {
|
|
8815
|
-
const rootDir =
|
|
8997
|
+
const rootDir = path36.resolve(dir);
|
|
8816
8998
|
const samples = await sampleFiles(rootDir, count4);
|
|
8817
8999
|
const output = {
|
|
8818
9000
|
note: "These are previews (first ~60 lines). Read the file directly with your own tools to see it in full.",
|
|
@@ -8855,7 +9037,7 @@ async function unmappedContextResponse(rootDir) {
|
|
|
8855
9037
|
});
|
|
8856
9038
|
}
|
|
8857
9039
|
async function getProjectStructure(dir) {
|
|
8858
|
-
const rootDir =
|
|
9040
|
+
const rootDir = path36.resolve(dir);
|
|
8859
9041
|
const allFiles = await (await createFileAccess(rootDir)).list("**/*");
|
|
8860
9042
|
const dirInfo = /* @__PURE__ */ new Map();
|
|
8861
9043
|
for (const file of allFiles) {
|
|
@@ -8867,7 +9049,7 @@ async function getProjectStructure(dir) {
|
|
|
8867
9049
|
}
|
|
8868
9050
|
const info = dirInfo.get(dirPath);
|
|
8869
9051
|
info.fileCount++;
|
|
8870
|
-
const ext =
|
|
9052
|
+
const ext = path36.extname(file).slice(1);
|
|
8871
9053
|
if (ext) {
|
|
8872
9054
|
info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
|
|
8873
9055
|
}
|
|
@@ -8912,7 +9094,7 @@ async function buildChangedFilePreviews(rootDir, changedFiles) {
|
|
|
8912
9094
|
return previews2;
|
|
8913
9095
|
}
|
|
8914
9096
|
async function getSnapshot(dir) {
|
|
8915
|
-
const rootDir =
|
|
9097
|
+
const rootDir = path36.resolve(dir);
|
|
8916
9098
|
const snapshot = await loadSnapshot(rootDir);
|
|
8917
9099
|
if (!snapshot) {
|
|
8918
9100
|
return unmappedContextResponse(rootDir);
|
|
@@ -9013,7 +9195,7 @@ function driftHint(report) {
|
|
|
9013
9195
|
return "Read the changed files under staleFeatures/staleFlows, update those entries (fold unmappedFiles into the right features), and call save_snapshot with only the affected entries \u2014 unchanged entries are preserved. Drop ghostFiles from any entries that reference them, and delete features/flows that no longer exist via save_snapshot's removeFeatures/removeFlows.";
|
|
9014
9196
|
}
|
|
9015
9197
|
async function checkDrift(dir) {
|
|
9016
|
-
const rootDir =
|
|
9198
|
+
const rootDir = path36.resolve(dir);
|
|
9017
9199
|
const report = await computeDrift(rootDir);
|
|
9018
9200
|
if (!report) {
|
|
9019
9201
|
return JSON.stringify({
|
|
@@ -9044,7 +9226,7 @@ async function checkDrift(dir) {
|
|
|
9044
9226
|
return JSON.stringify({ exists: true, ...report, verification, hint });
|
|
9045
9227
|
}
|
|
9046
9228
|
async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_SIZE, files) {
|
|
9047
|
-
const rootDir =
|
|
9229
|
+
const rootDir = path36.resolve(dir);
|
|
9048
9230
|
const scoped = files !== void 0 && files.length > 0;
|
|
9049
9231
|
const scopeFiles = scoped ? sanitizePaths(rootDir, files) : void 0;
|
|
9050
9232
|
const batch = await prepareSnapshotBatch(dir, offset, batchSize, scopeFiles);
|
|
@@ -9091,7 +9273,7 @@ async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_
|
|
|
9091
9273
|
);
|
|
9092
9274
|
}
|
|
9093
9275
|
async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
|
|
9094
|
-
const rootDir =
|
|
9276
|
+
const rootDir = path36.resolve(dir);
|
|
9095
9277
|
for (const feat of Object.values(features)) {
|
|
9096
9278
|
feat.files = sanitizePaths(rootDir, feat.files);
|
|
9097
9279
|
if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
|
|
@@ -9120,7 +9302,7 @@ async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
|
|
|
9120
9302
|
);
|
|
9121
9303
|
}
|
|
9122
9304
|
async function reduceSnapshot(dir) {
|
|
9123
|
-
const rootDir =
|
|
9305
|
+
const rootDir = path36.resolve(dir);
|
|
9124
9306
|
const partials = await loadAllPartials(rootDir);
|
|
9125
9307
|
if (partials.length === 0) {
|
|
9126
9308
|
return JSON.stringify(
|
|
@@ -9182,7 +9364,7 @@ async function reduceSnapshot(dir) {
|
|
|
9182
9364
|
);
|
|
9183
9365
|
}
|
|
9184
9366
|
async function fullAnalysis(dir) {
|
|
9185
|
-
const rootDir =
|
|
9367
|
+
const rootDir = path36.resolve(dir);
|
|
9186
9368
|
const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
|
|
9187
9369
|
analyzeProject(dir),
|
|
9188
9370
|
getProjectStructure(dir),
|
|
@@ -9211,7 +9393,7 @@ function sanitizePaths(rootDir, files) {
|
|
|
9211
9393
|
return sanitizeRepoPaths(files);
|
|
9212
9394
|
}
|
|
9213
9395
|
async function saveSnapshotData(dir, features, flows, removeFeatures = [], removeFlows = []) {
|
|
9214
|
-
const rootDir =
|
|
9396
|
+
const rootDir = path36.resolve(dir);
|
|
9215
9397
|
return updateSnapshot(rootDir, async (previous) => {
|
|
9216
9398
|
const gitHash = await getCurrentGitHash(rootDir);
|
|
9217
9399
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9291,14 +9473,14 @@ async function saveSnapshotData(dir, features, flows, removeFeatures = [], remov
|
|
|
9291
9473
|
});
|
|
9292
9474
|
}
|
|
9293
9475
|
async function getImpact(dir, files) {
|
|
9294
|
-
const rootDir =
|
|
9476
|
+
const rootDir = path36.resolve(dir);
|
|
9295
9477
|
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
9296
9478
|
const result = await analyzeImpact2(rootDir, files);
|
|
9297
9479
|
return JSON.stringify(result, null, 2);
|
|
9298
9480
|
}
|
|
9299
9481
|
var VERIFY_DEFAULT_SAMPLE = 5;
|
|
9300
9482
|
async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
9301
|
-
const rootDir =
|
|
9483
|
+
const rootDir = path36.resolve(dir);
|
|
9302
9484
|
const snapshot = await loadSnapshot(rootDir);
|
|
9303
9485
|
if (!snapshot) {
|
|
9304
9486
|
return JSON.stringify({
|
|
@@ -9351,7 +9533,7 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
|
9351
9533
|
});
|
|
9352
9534
|
}
|
|
9353
9535
|
async function saveVerification(dir, verdicts) {
|
|
9354
|
-
const rootDir =
|
|
9536
|
+
const rootDir = path36.resolve(dir);
|
|
9355
9537
|
return updateSnapshot(rootDir, async (snapshot) => {
|
|
9356
9538
|
if (!snapshot) {
|
|
9357
9539
|
return { snapshot: null, result: JSON.stringify({ exists: false, stamped: [], hint: "No concept map exists. Build it and run verify_snapshot before recording verdicts." }) };
|
|
@@ -9425,16 +9607,16 @@ async function saveVerification(dir, verdicts) {
|
|
|
9425
9607
|
});
|
|
9426
9608
|
}
|
|
9427
9609
|
async function saveDecision(dir, input) {
|
|
9428
|
-
const rootDir =
|
|
9610
|
+
const rootDir = path36.resolve(dir);
|
|
9429
9611
|
const { upsertDecision: upsertDecision2 } = await Promise.resolve().then(() => (init_decisions(), decisions_exports));
|
|
9430
9612
|
const result = await upsertDecision2(rootDir, input);
|
|
9431
9613
|
return JSON.stringify(result);
|
|
9432
9614
|
}
|
|
9433
9615
|
async function reviewDecision2(dir, input) {
|
|
9434
|
-
return JSON.stringify(await reviewDecision(
|
|
9616
|
+
return JSON.stringify(await reviewDecision(path36.resolve(dir), input));
|
|
9435
9617
|
}
|
|
9436
9618
|
async function getContext(dir, task, files) {
|
|
9437
|
-
const rootDir =
|
|
9619
|
+
const rootDir = path36.resolve(dir);
|
|
9438
9620
|
const { assembleContext: assembleContext2 } = await Promise.resolve().then(() => (init_assemble(), assemble_exports));
|
|
9439
9621
|
const bundle = await assembleContext2(rootDir, task, files);
|
|
9440
9622
|
return JSON.stringify(bundle);
|
|
@@ -9483,7 +9665,7 @@ async function masonInit(dir, options = {}) {
|
|
|
9483
9665
|
return JSON.stringify(await setupProject2(dir, options), null, 2);
|
|
9484
9666
|
}
|
|
9485
9667
|
if (options.host) throw new Error("host applies only to mode: setup.");
|
|
9486
|
-
const rootDir =
|
|
9668
|
+
const rootDir = path36.resolve(dir);
|
|
9487
9669
|
const marker = await loadProjectMarker(rootDir);
|
|
9488
9670
|
const { loadSetup: loadSetup2 } = await Promise.resolve().then(() => (init_model(), model_exports));
|
|
9489
9671
|
const setup = await loadSetup2(rootDir);
|
|
@@ -9503,7 +9685,7 @@ async function masonInit(dir, options = {}) {
|
|
|
9503
9685
|
);
|
|
9504
9686
|
}
|
|
9505
9687
|
async function masonCompleteInit(dir, options = {}) {
|
|
9506
|
-
const rootDir =
|
|
9688
|
+
const rootDir = path36.resolve(dir);
|
|
9507
9689
|
const existing = await loadProjectMarker(rootDir);
|
|
9508
9690
|
const features = await loadProjectFeatures(rootDir);
|
|
9509
9691
|
const marker = {
|
|
@@ -9612,7 +9794,7 @@ async function masonSetConfluence(input) {
|
|
|
9612
9794
|
);
|
|
9613
9795
|
}
|
|
9614
9796
|
async function exportToConfluenceTool(dir, overrides) {
|
|
9615
|
-
const rootDir =
|
|
9797
|
+
const rootDir = path36.resolve(dir);
|
|
9616
9798
|
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
|
|
9617
9799
|
const { exportToConfluence: exportToConfluence2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
9618
9800
|
const config = await loadConfig2();
|
|
@@ -9651,7 +9833,7 @@ function createMcpServer() {
|
|
|
9651
9833
|
const server = new McpServer(
|
|
9652
9834
|
{
|
|
9653
9835
|
name: "mason",
|
|
9654
|
-
version: "0.19.
|
|
9836
|
+
version: "0.19.1"
|
|
9655
9837
|
},
|
|
9656
9838
|
{
|
|
9657
9839
|
instructions: (setupNotice ? setupNotice + "\n\n" : "") + 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, stale or unknown map verification needs a fresh review, recorded failed verdicts need correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
|
|
@@ -9697,7 +9879,7 @@ function createMcpServer() {
|
|
|
9697
9879
|
);
|
|
9698
9880
|
server.tool(
|
|
9699
9881
|
"review_advisory",
|
|
9700
|
-
"Prepare an assessment of an original repair advisory and its exact repository scope, then record authorized addressed, inapplicable or deferred outcomes with reviewToken, reviewer and note. Relevant changes reopen reviews; unrelated commits preserve them. Records belong in Git. Decision findings route to review_decision; this tool never approves decisions. Recorded identities are assertions, not authenticated approvals.",
|
|
9882
|
+
"Prepare an assessment of an original repair advisory and its exact repository scope, then record authorized addressed, inapplicable or deferred outcomes with reviewToken, reviewer and note. Relevant changes reopen reviews; unrelated commits preserve them. Records belong in Git. Decision findings route to review_decision; this tool never approves decisions. Recorded identities are assertions, not authenticated approvals. For dependency advisories, action dismiss with reasonCode and note records a one-step dismissal without a token or reviewer. no-dependency-claims reopens only on document content changes; unrelated-manifest-change also tracks scoped manifests.",
|
|
9701
9883
|
{ dir: z24.string().describe("Absolute path to the project root"), ...advisoryReviewRequest.shape },
|
|
9702
9884
|
async ({ dir, ...request }) => ({ content: [{ type: "text", text: await masonReviewAdvisory(dir, request) }] })
|
|
9703
9885
|
);
|
|
@@ -10043,7 +10225,7 @@ async function startMcpServer(onClose) {
|
|
|
10043
10225
|
init_store();
|
|
10044
10226
|
init_storage();
|
|
10045
10227
|
import fs33 from "fs/promises";
|
|
10046
|
-
import
|
|
10228
|
+
import path42 from "path";
|
|
10047
10229
|
import { spawn as spawn3 } from "child_process";
|
|
10048
10230
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
10049
10231
|
import { z as z30 } from "zod";
|
|
@@ -10051,7 +10233,7 @@ import { z as z30 } from "zod";
|
|
|
10051
10233
|
// src/distribution/state.ts
|
|
10052
10234
|
init_storage();
|
|
10053
10235
|
import os5 from "os";
|
|
10054
|
-
import
|
|
10236
|
+
import path38 from "path";
|
|
10055
10237
|
import { fileURLToPath } from "url";
|
|
10056
10238
|
import fs28 from "fs/promises";
|
|
10057
10239
|
import { z as z26 } from "zod";
|
|
@@ -10059,7 +10241,7 @@ import { z as z26 } from "zod";
|
|
|
10059
10241
|
// src/distribution/path.ts
|
|
10060
10242
|
import fs27 from "fs/promises";
|
|
10061
10243
|
import os4 from "os";
|
|
10062
|
-
import
|
|
10244
|
+
import path37 from "path";
|
|
10063
10245
|
import { execFile as execFile12 } from "child_process";
|
|
10064
10246
|
import { promisify as promisify12 } from "util";
|
|
10065
10247
|
import { createHash as createHash7, randomUUID as randomUUID7 } from "crypto";
|
|
@@ -10074,7 +10256,7 @@ async function configurePath(bin, previous, save, options = {}) {
|
|
|
10074
10256
|
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
10075
10257
|
const changes = [...previous];
|
|
10076
10258
|
const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
|
|
10077
|
-
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) =>
|
|
10259
|
+
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path37.posix.isAbsolute(entry) && path37.posix.resolve(entry) === path37.posix.resolve(bin));
|
|
10078
10260
|
const setupHint = "To connect a project, run one of these inside its directory:\n Codex: mason setup --host codex\n Claude Code: mason setup --host claude";
|
|
10079
10261
|
const message2 = (alreadyOnPath ? "" : "Open a new terminal to use mason.\n") + setupHint;
|
|
10080
10262
|
const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
|
|
@@ -10094,15 +10276,15 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
10094
10276
|
}
|
|
10095
10277
|
return { status: "configured", changes, message: message2 };
|
|
10096
10278
|
}
|
|
10097
|
-
const shell =
|
|
10279
|
+
const shell = path37.basename(env.SHELL ?? os4.userInfo().shell ?? "");
|
|
10098
10280
|
const userHome = options.userHome ?? os4.homedir();
|
|
10099
10281
|
let profiles2;
|
|
10100
10282
|
if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
|
|
10101
|
-
if (env.MASON_PROFILE) profiles2 = [
|
|
10102
|
-
else if (shell === "zsh") profiles2 = [
|
|
10103
|
-
else if (shell === "sh") profiles2 = [
|
|
10283
|
+
if (env.MASON_PROFILE) profiles2 = [path37.resolve(env.MASON_PROFILE)];
|
|
10284
|
+
else if (shell === "zsh") profiles2 = [path37.join(env.ZDOTDIR || userHome, ".zshrc")];
|
|
10285
|
+
else if (shell === "sh") profiles2 = [path37.join(userHome, ".profile")];
|
|
10104
10286
|
else {
|
|
10105
|
-
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) =>
|
|
10287
|
+
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path37.join(userHome, name));
|
|
10106
10288
|
let login = candidates[2];
|
|
10107
10289
|
for (const file of candidates) if (await fs27.lstat(file).then(() => true, (error) => {
|
|
10108
10290
|
if (error.code === "ENOENT") return false;
|
|
@@ -10111,7 +10293,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
10111
10293
|
login = file;
|
|
10112
10294
|
break;
|
|
10113
10295
|
}
|
|
10114
|
-
profiles2 = [
|
|
10296
|
+
profiles2 = [path37.join(userHome, ".bashrc"), login];
|
|
10115
10297
|
}
|
|
10116
10298
|
const marker = `# >>> mason PATH ${createHash7("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
10117
10299
|
for (const profile of profiles2) {
|
|
@@ -10119,7 +10301,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
10119
10301
|
if (error.code === "ENOENT") return null;
|
|
10120
10302
|
throw error;
|
|
10121
10303
|
});
|
|
10122
|
-
const file = stat?.isSymbolicLink() ? await fs27.realpath(profile) :
|
|
10304
|
+
const file = stat?.isSymbolicLink() ? await fs27.realpath(profile) : path37.resolve(profile);
|
|
10123
10305
|
const original = await fs27.readFile(file).catch((error) => {
|
|
10124
10306
|
if (error.code === "ENOENT" && !stat) return null;
|
|
10125
10307
|
throw error;
|
|
@@ -10135,7 +10317,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
10135
10317
|
changes.push({ kind: "profile", file, block, created: original === null });
|
|
10136
10318
|
await save(changes);
|
|
10137
10319
|
}
|
|
10138
|
-
await fs27.mkdir(
|
|
10320
|
+
await fs27.mkdir(path37.dirname(file), { recursive: true });
|
|
10139
10321
|
const handle = await fs27.open(file, original === null ? "ax+" : "a+", 384);
|
|
10140
10322
|
try {
|
|
10141
10323
|
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
@@ -10150,7 +10332,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
10150
10332
|
return manual(`Could not configure PATH: ${error instanceof Error ? error.message : String(error)}`);
|
|
10151
10333
|
}
|
|
10152
10334
|
}
|
|
10153
|
-
var normalizeWindowsEntry = (value) =>
|
|
10335
|
+
var normalizeWindowsEntry = (value) => path37.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
10154
10336
|
var hasWindowsEntry = (value, entry, env = process.env) => {
|
|
10155
10337
|
const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
|
|
10156
10338
|
const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
|
|
@@ -10216,9 +10398,9 @@ async function readInstallation(home) {
|
|
|
10216
10398
|
return record;
|
|
10217
10399
|
}
|
|
10218
10400
|
function standaloneLocation() {
|
|
10219
|
-
const bundle =
|
|
10220
|
-
const home =
|
|
10221
|
-
if (
|
|
10401
|
+
const bundle = path38.resolve(path38.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
10402
|
+
const home = path38.dirname(path38.dirname(bundle));
|
|
10403
|
+
if (path38.dirname(bundle) !== path38.join(home, "versions") || !/^[a-f0-9]{24}$/.test(path38.basename(bundle))) {
|
|
10222
10404
|
return null;
|
|
10223
10405
|
}
|
|
10224
10406
|
return { home, bundle };
|
|
@@ -10245,7 +10427,7 @@ function newerStableVersion(candidate, installed) {
|
|
|
10245
10427
|
// src/distribution/bundle.ts
|
|
10246
10428
|
init_storage();
|
|
10247
10429
|
import fs29 from "fs/promises";
|
|
10248
|
-
import
|
|
10430
|
+
import path39 from "path";
|
|
10249
10431
|
import { createHash as createHash8 } from "crypto";
|
|
10250
10432
|
import { z as z27 } from "zod";
|
|
10251
10433
|
var targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
|
@@ -10263,7 +10445,7 @@ async function readBundle(root) {
|
|
|
10263
10445
|
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
10264
10446
|
const windows = manifest.target.startsWith("win32-");
|
|
10265
10447
|
for (const file of Object.keys(manifest.files)) {
|
|
10266
|
-
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") ||
|
|
10448
|
+
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path39.isAbsolute(file)) {
|
|
10267
10449
|
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
10268
10450
|
}
|
|
10269
10451
|
}
|
|
@@ -10289,7 +10471,7 @@ init_storage();
|
|
|
10289
10471
|
import fs30 from "fs/promises";
|
|
10290
10472
|
import { unlinkSync } from "fs";
|
|
10291
10473
|
import os6 from "os";
|
|
10292
|
-
import
|
|
10474
|
+
import path40 from "path";
|
|
10293
10475
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
10294
10476
|
import { z as z28 } from "zod";
|
|
10295
10477
|
var leaseSchema = z28.object({ pid: z28.number().int().positive(), host: z28.string(), bundle: z28.string().regex(/^[a-f0-9]{24}$/) });
|
|
@@ -10297,7 +10479,7 @@ var leaseSchema = z28.object({ pid: z28.number().int().positive(), host: z28.str
|
|
|
10297
10479
|
// src/distribution/install.ts
|
|
10298
10480
|
import fs32 from "fs/promises";
|
|
10299
10481
|
import os8 from "os";
|
|
10300
|
-
import
|
|
10482
|
+
import path41 from "path";
|
|
10301
10483
|
import { execFile as execFile13, spawn as spawn2 } from "child_process";
|
|
10302
10484
|
import { promisify as promisify13 } from "util";
|
|
10303
10485
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -10372,13 +10554,13 @@ async function installLocked2(home, source, bundle, progress, options, alreadySt
|
|
|
10372
10554
|
if (options.stageOnly) assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
|
|
10373
10555
|
if (!previous) for (const file of await fs32.readdir(home)) {
|
|
10374
10556
|
if (file === ".install-lock") continue;
|
|
10375
|
-
const directory =
|
|
10557
|
+
const directory = path41.join(home, file);
|
|
10376
10558
|
const stat = await fs32.lstat(directory);
|
|
10377
10559
|
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs32.readdir(directory)).length) {
|
|
10378
10560
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
10379
10561
|
}
|
|
10380
10562
|
}
|
|
10381
|
-
const selectedBin =
|
|
10563
|
+
const selectedBin = path41.resolve(process.env.MASON_BIN_DIR ?? previous?.bin ?? (process.platform === "win32" ? path41.join(home, "bin") : path41.join(os8.homedir(), ".local/bin")));
|
|
10382
10564
|
await fs32.mkdir(selectedBin, { recursive: true });
|
|
10383
10565
|
const bin = await fs32.realpath(selectedBin);
|
|
10384
10566
|
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
@@ -10396,10 +10578,10 @@ $previousToken = $env:MASON_UNINSTALL_TOKEN
|
|
|
10396
10578
|
$token = [Guid]::NewGuid().ToString()
|
|
10397
10579
|
$env:MASON_UNINSTALL_TOKEN = $token
|
|
10398
10580
|
try {
|
|
10399
|
-
& ${psQuote(
|
|
10581
|
+
& ${psQuote(path41.join(destination, "node.exe"))} ${psQuote(path41.join(destination, "app/dist/mason.js"))} @args
|
|
10400
10582
|
$code = $LASTEXITCODE
|
|
10401
10583
|
if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
|
|
10402
|
-
if ([IO.File]::ReadAllText(${psQuote(
|
|
10584
|
+
if ([IO.File]::ReadAllText(${psQuote(path41.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
|
|
10403
10585
|
Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
|
|
10404
10586
|
Write-Output ${psQuote("Removed standalone installation: " + home)}
|
|
10405
10587
|
}
|
|
@@ -10408,7 +10590,7 @@ try {
|
|
|
10408
10590
|
finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
|
|
10409
10591
|
`
|
|
10410
10592
|
} : { mason: `#!/bin/sh
|
|
10411
|
-
exec ${shQuote(
|
|
10593
|
+
exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destination, "app/dist/mason.js"))} "$@"
|
|
10412
10594
|
` };
|
|
10413
10595
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
10414
10596
|
const file = await storePath(bin, name);
|
|
@@ -10557,8 +10739,8 @@ async function scheduleAutomaticUpdate() {
|
|
|
10557
10739
|
if (status.success && status.data.lastAttempt <= now && now - status.data.lastAttempt < CHECK_INTERVAL_MS) return;
|
|
10558
10740
|
const bundle = await storePath(home, "versions/" + record.current);
|
|
10559
10741
|
const child = spawn3(
|
|
10560
|
-
|
|
10561
|
-
[
|
|
10742
|
+
path42.join(bundle, process.platform === "win32" ? "node.exe" : "node"),
|
|
10743
|
+
[path42.join(bundle, "app/dist/mason.js"), "internal-update"],
|
|
10562
10744
|
{ cwd: home, env: process.env, stdio: "ignore", detached: true, windowsHide: true }
|
|
10563
10745
|
);
|
|
10564
10746
|
child.on("error", () => {
|
|
@@ -10576,8 +10758,8 @@ async function startMcpWithUpdates(start) {
|
|
|
10576
10758
|
const session = await prepareStandaloneMcp(current.home);
|
|
10577
10759
|
if (session.bundle !== current.bundle) {
|
|
10578
10760
|
const child = spawn3(
|
|
10579
|
-
|
|
10580
|
-
[
|
|
10761
|
+
path42.join(session.bundle, process.platform === "win32" ? "node.exe" : "node"),
|
|
10762
|
+
[path42.join(session.bundle, "app/dist/mason-mcp.js"), ...process.argv.slice(2)],
|
|
10581
10763
|
{ stdio: "inherit", windowsHide: true }
|
|
10582
10764
|
);
|
|
10583
10765
|
const interrupt = () => {
|