mason-context 0.18.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 +16 -0
- package/README.md +2 -0
- package/dist/mason-audit.js +449 -251
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +522 -270
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +51 -0
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +1 -0
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +1486 -664
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +51 -0
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +337 -0
- package/dist/mason-runtime.js.map +1 -0
- package/dist/mason.js +1377 -485
- package/dist/mason.js.map +1 -1
- package/package.json +2 -1
package/dist/mason-mcp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
if (!import.meta.url.endsWith("/mason-runtime.js")) await import("./mason-runtime.js").then(m => m.registerRuntime());
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
5
|
var __esm = (fn, res) => function __init() {
|
|
@@ -57,23 +58,486 @@ var init_git_read = __esm({
|
|
|
57
58
|
}
|
|
58
59
|
});
|
|
59
60
|
|
|
61
|
+
// src/utils/paths.ts
|
|
62
|
+
import path from "path";
|
|
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;
|
|
69
|
+
}
|
|
70
|
+
function sanitizeRepoPaths(files) {
|
|
71
|
+
return [...new Set(files.map(normalizeRepoPath).filter((p) => p !== null))];
|
|
72
|
+
}
|
|
73
|
+
function isWithinRoot(root, candidate) {
|
|
74
|
+
const relative = path.relative(root, candidate);
|
|
75
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
76
|
+
}
|
|
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}/`));
|
|
81
|
+
}
|
|
82
|
+
function matchingPaths(anchors, files) {
|
|
83
|
+
return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
|
|
84
|
+
}
|
|
85
|
+
var init_paths = __esm({
|
|
86
|
+
"src/utils/paths.ts"() {
|
|
87
|
+
"use strict";
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
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
|
+
];
|
|
108
|
+
}
|
|
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
|
+
);
|
|
120
|
+
}
|
|
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;
|
|
132
|
+
}
|
|
133
|
+
return bytes === buffer.length ? null : buffer.subarray(0, bytes).toString("utf8");
|
|
134
|
+
} finally {
|
|
135
|
+
await handle.close();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
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`);
|
|
152
|
+
}
|
|
153
|
+
config[key] = value[key];
|
|
154
|
+
}
|
|
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
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
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 {
|
|
197
|
+
}
|
|
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;
|
|
213
|
+
}
|
|
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;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { root, config, list, read };
|
|
236
|
+
}
|
|
237
|
+
var SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
|
|
238
|
+
var init_files = __esm({
|
|
239
|
+
"src/utils/files.ts"() {
|
|
240
|
+
"use strict";
|
|
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;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
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;
|
|
300
|
+
}
|
|
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
|
+
}
|
|
313
|
+
}
|
|
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
|
+
}
|
|
333
|
+
}
|
|
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));
|
|
363
|
+
}
|
|
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();
|
|
379
|
+
}
|
|
380
|
+
var inspections, sourceSpecs, docSpecs, sourceSuffixes;
|
|
381
|
+
var init_inspection = __esm({
|
|
382
|
+
"src/audit/inspection.ts"() {
|
|
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));
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
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);
|
|
400
|
+
try {
|
|
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 });
|
|
405
|
+
}
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function readAuditInput(root, file) {
|
|
410
|
+
try {
|
|
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;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
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 = [];
|
|
448
|
+
try {
|
|
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
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} finally {
|
|
466
|
+
stream.destroy();
|
|
467
|
+
}
|
|
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) {
|
|
477
|
+
try {
|
|
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;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
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;
|
|
500
|
+
}
|
|
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
|
+
|
|
60
524
|
// src/audit/commands.ts
|
|
61
|
-
import
|
|
525
|
+
import path6 from "path";
|
|
62
526
|
function splitCommands(line) {
|
|
63
527
|
const parts = [];
|
|
64
|
-
let
|
|
528
|
+
let quote2 = "", start = 0;
|
|
65
529
|
for (let i = 0; i < line.length; i++) {
|
|
66
530
|
const char = line[i];
|
|
67
531
|
if (char === "\\") {
|
|
68
532
|
i++;
|
|
69
533
|
continue;
|
|
70
534
|
}
|
|
71
|
-
if (
|
|
72
|
-
if (char ===
|
|
535
|
+
if (quote2) {
|
|
536
|
+
if (char === quote2) quote2 = "";
|
|
73
537
|
continue;
|
|
74
538
|
}
|
|
75
539
|
if (char === "'" || char === '"') {
|
|
76
|
-
|
|
540
|
+
quote2 = char;
|
|
77
541
|
continue;
|
|
78
542
|
}
|
|
79
543
|
if (char === ";" || line.slice(i, i + 2) === "&&") {
|
|
@@ -94,7 +558,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
94
558
|
const cd = text2.match(/^cd\s+(.+)$/);
|
|
95
559
|
if (cd) {
|
|
96
560
|
const directory = literal(cd[1]);
|
|
97
|
-
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null :
|
|
561
|
+
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path6.posix.join(cwd ?? ".", directory);
|
|
98
562
|
continue;
|
|
99
563
|
}
|
|
100
564
|
for (const match of text2.matchAll(COMMAND)) {
|
|
@@ -105,7 +569,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
105
569
|
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
106
570
|
if (selector) {
|
|
107
571
|
const selected = literal(selector[1]);
|
|
108
|
-
directory = selected === null || cwd === null ? null :
|
|
572
|
+
directory = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
|
|
109
573
|
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
110
574
|
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
111
575
|
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
@@ -135,7 +599,7 @@ var init_commands = __esm({
|
|
|
135
599
|
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
136
600
|
literal = (value) => {
|
|
137
601
|
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
138
|
-
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) ||
|
|
602
|
+
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path6.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
139
603
|
};
|
|
140
604
|
}
|
|
141
605
|
});
|
|
@@ -391,268 +855,52 @@ function extractClaims(content2) {
|
|
|
391
855
|
paths: [...paths.values()],
|
|
392
856
|
counts,
|
|
393
857
|
commands: [...commands2.values()]
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
|
|
397
|
-
var init_claims = __esm({
|
|
398
|
-
"src/audit/claims.ts"() {
|
|
399
|
-
"use strict";
|
|
400
|
-
init_commands();
|
|
401
|
-
init_tree();
|
|
402
|
-
ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
403
|
-
"package.json",
|
|
404
|
-
"package-lock.json",
|
|
405
|
-
"pnpm-workspace.yaml",
|
|
406
|
-
"tsconfig.json",
|
|
407
|
-
"tsup.config.ts",
|
|
408
|
-
"vitest.config.ts",
|
|
409
|
-
"Makefile",
|
|
410
|
-
"Dockerfile",
|
|
411
|
-
"docker-compose.yml",
|
|
412
|
-
"Cargo.toml",
|
|
413
|
-
"go.mod",
|
|
414
|
-
"go.sum",
|
|
415
|
-
"pyproject.toml",
|
|
416
|
-
"requirements.txt",
|
|
417
|
-
"Gemfile",
|
|
418
|
-
"composer.json",
|
|
419
|
-
"settings.gradle.kts",
|
|
420
|
-
"settings.gradle",
|
|
421
|
-
"build.gradle.kts",
|
|
422
|
-
"build.gradle",
|
|
423
|
-
"manifest.json",
|
|
424
|
-
"server.json",
|
|
425
|
-
"README.md",
|
|
426
|
-
"CHANGELOG.md",
|
|
427
|
-
"LICENSE",
|
|
428
|
-
"CLAUDE.md",
|
|
429
|
-
"AGENTS.md",
|
|
430
|
-
".gitignore",
|
|
431
|
-
".env.example"
|
|
432
|
-
]);
|
|
433
|
-
SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
|
|
434
|
-
markdown = new MarkdownIt("commonmark", { html: false });
|
|
435
|
-
COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
|
|
436
|
-
COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
|
|
437
|
-
IGNORE_LINE = "<!-- mason:ignore -->";
|
|
438
|
-
IGNORE_START = "<!-- mason:ignore-start -->";
|
|
439
|
-
IGNORE_END = "<!-- mason:ignore-end -->";
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// src/utils/paths.ts
|
|
444
|
-
import path2 from "path";
|
|
445
|
-
function normalizeRepoPath(value) {
|
|
446
|
-
const slash = value.replace(/\\/g, "/");
|
|
447
|
-
if (!slash || slash.includes("\0") || path2.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
|
|
448
|
-
if (slash.split("/").includes("..")) return null;
|
|
449
|
-
const normalized = path2.posix.normalize(slash).replace(/\/$/, "");
|
|
450
|
-
return normalized === "." ? null : normalized;
|
|
451
|
-
}
|
|
452
|
-
function sanitizeRepoPaths(files) {
|
|
453
|
-
return [...new Set(files.map(normalizeRepoPath).filter((p) => p !== null))];
|
|
454
|
-
}
|
|
455
|
-
function isWithinRoot(root, candidate) {
|
|
456
|
-
const relative = path2.relative(root, candidate);
|
|
457
|
-
return relative !== ".." && !relative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relative);
|
|
458
|
-
}
|
|
459
|
-
function anchorMatches(anchor, file) {
|
|
460
|
-
const a = normalizeRepoPath(anchor);
|
|
461
|
-
const f = normalizeRepoPath(file);
|
|
462
|
-
return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
|
|
463
|
-
}
|
|
464
|
-
function matchingPaths(anchors, files) {
|
|
465
|
-
return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
|
|
466
|
-
}
|
|
467
|
-
var init_paths = __esm({
|
|
468
|
-
"src/utils/paths.ts"() {
|
|
469
|
-
"use strict";
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
// src/utils/files.ts
|
|
474
|
-
import fs from "fs/promises";
|
|
475
|
-
import { constants } from "fs";
|
|
476
|
-
import path3 from "path";
|
|
477
|
-
import fg from "fast-glob";
|
|
478
|
-
function isSensitiveFile(file) {
|
|
479
|
-
return file.split(/[\\/]/).some(
|
|
480
|
-
(part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(part)
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
async function readBoundedFile(file, maxBytes) {
|
|
484
|
-
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
485
|
-
try {
|
|
486
|
-
const stat = await handle.stat();
|
|
487
|
-
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
488
|
-
const buffer = Buffer.alloc(Math.min(maxBytes + 1, stat.size + 1));
|
|
489
|
-
let bytes = 0;
|
|
490
|
-
while (bytes < buffer.length) {
|
|
491
|
-
const result = await handle.read(buffer, bytes, buffer.length - bytes, null);
|
|
492
|
-
if (result.bytesRead === 0) break;
|
|
493
|
-
bytes += result.bytesRead;
|
|
494
|
-
}
|
|
495
|
-
return bytes === buffer.length ? null : buffer.subarray(0, bytes).toString("utf8");
|
|
496
|
-
} finally {
|
|
497
|
-
await handle.close();
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
async function loadProjectConfig(root) {
|
|
501
|
-
try {
|
|
502
|
-
const canonicalRoot = await fs.realpath(root);
|
|
503
|
-
const configPath2 = await fs.realpath(path3.join(root, ".mason/config.json"));
|
|
504
|
-
if (!isWithinRoot(canonicalRoot, configPath2)) throw new Error("Project configuration resolves outside the repository");
|
|
505
|
-
const raw = await readBoundedFile(configPath2, 64 * 1024);
|
|
506
|
-
if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
|
|
507
|
-
const value = JSON.parse(raw);
|
|
508
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
|
|
509
|
-
const config = {};
|
|
510
|
-
for (const key of ["patterns", "alwaysInclude", "ignore"]) {
|
|
511
|
-
if (value[key] === void 0) continue;
|
|
512
|
-
if (!Array.isArray(value[key]) || !value[key].every((s) => typeof s === "string")) {
|
|
513
|
-
throw new Error(`Configuration ${key} must be an array of strings`);
|
|
514
|
-
}
|
|
515
|
-
config[key] = value[key];
|
|
516
|
-
}
|
|
517
|
-
return config;
|
|
518
|
-
} catch (error) {
|
|
519
|
-
if (error.code === "ENOENT") return {};
|
|
520
|
-
throw new Error(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
async function createFileAccess(rootDir) {
|
|
524
|
-
const root = path3.resolve(rootDir);
|
|
525
|
-
const canonicalRoot = await fs.realpath(root).catch(() => root);
|
|
526
|
-
const config = await loadProjectConfig(root);
|
|
527
|
-
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
528
|
-
let gitFiles = null;
|
|
529
|
-
try {
|
|
530
|
-
const { stdout } = await execGit(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
|
|
531
|
-
gitFiles = new Set(stdout.split("\0").filter(Boolean));
|
|
532
|
-
} catch {
|
|
533
|
-
let inGit = false;
|
|
534
|
-
try {
|
|
535
|
-
await execGit(["rev-parse", "--git-dir"], { cwd: root });
|
|
536
|
-
inGit = true;
|
|
537
|
-
} catch {
|
|
538
|
-
}
|
|
539
|
-
if (inGit) throw new Error("Cannot enumerate Git files safely");
|
|
540
|
-
}
|
|
541
|
-
async function resolve2(file) {
|
|
542
|
-
const relative = normalizeRepoPath(file);
|
|
543
|
-
if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative)) return null;
|
|
544
|
-
const candidate = path3.join(root, relative);
|
|
545
|
-
try {
|
|
546
|
-
const real = await fs.realpath(candidate);
|
|
547
|
-
if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path3.relative(canonicalRoot, real))) return null;
|
|
548
|
-
const stat = await fs.stat(real);
|
|
549
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
|
|
550
|
-
if (gitFiles && !gitFiles.has(path3.relative(canonicalRoot, real).split(path3.sep).join("/"))) return null;
|
|
551
|
-
return real;
|
|
552
|
-
} catch {
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
async function list(patterns = SOURCE_GLOB, options = {}) {
|
|
557
|
-
const found = await fg(patterns, { cwd: root, ignore, followSymbolicLinks: false, ...options });
|
|
558
|
-
const safe = await Promise.all(found.map(async (f) => await resolve2(f) ? f : null));
|
|
559
|
-
return safe.filter((f) => f !== null).sort();
|
|
560
|
-
}
|
|
561
|
-
async function read(file) {
|
|
562
|
-
const relative = normalizeRepoPath(file);
|
|
563
|
-
if (!relative) return null;
|
|
564
|
-
const real = await resolve2(relative);
|
|
565
|
-
if (!real) return null;
|
|
566
|
-
for (const rel of /* @__PURE__ */ new Set([relative, path3.relative(canonicalRoot, real).split(path3.sep).join("/")])) {
|
|
567
|
-
if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
|
|
568
|
-
}
|
|
569
|
-
try {
|
|
570
|
-
const content2 = await readBoundedFile(real, MAX_SOURCE_BYTES);
|
|
571
|
-
return content2 === null ? null : { path: relative, content: content2, totalLines: content2.split("\n").length };
|
|
572
|
-
} catch {
|
|
573
|
-
return null;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
return { root, config, list, read };
|
|
577
|
-
}
|
|
578
|
-
var SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
|
|
579
|
-
var init_files = __esm({
|
|
580
|
-
"src/utils/files.ts"() {
|
|
581
|
-
"use strict";
|
|
582
|
-
init_git_read();
|
|
583
|
-
init_paths();
|
|
584
|
-
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"];
|
|
585
|
-
SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
|
|
586
|
-
SOURCE_IGNORE = [
|
|
587
|
-
"**/node_modules/**",
|
|
588
|
-
"**/dist/**",
|
|
589
|
-
"**/build/**",
|
|
590
|
-
"**/.gradle/**",
|
|
591
|
-
"**/target/**",
|
|
592
|
-
"**/.git/**",
|
|
593
|
-
"**/.mason/**",
|
|
594
|
-
"**/vendor/**",
|
|
595
|
-
"**/__pycache__/**",
|
|
596
|
-
"**/venv/**",
|
|
597
|
-
"**/.venv/**",
|
|
598
|
-
"**/*.min.*",
|
|
599
|
-
"**/*.map",
|
|
600
|
-
"**/*.lock",
|
|
601
|
-
"**/generated/**",
|
|
602
|
-
"**/*.generated.*",
|
|
603
|
-
"**/R.java",
|
|
604
|
-
"**/BuildConfig.java",
|
|
605
|
-
"**/package-lock.json",
|
|
606
|
-
"**/yarn.lock",
|
|
607
|
-
"**/pnpm-lock.yaml"
|
|
608
|
-
];
|
|
609
|
-
MAX_SOURCE_BYTES = 1024 * 1024;
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
// src/audit/inspection.ts
|
|
614
|
-
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
615
|
-
import path4 from "path";
|
|
616
|
-
function withRepositoryInspection(root, run) {
|
|
617
|
-
return inspections.run({ root: path4.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
|
|
618
|
-
}
|
|
619
|
-
function inspectionRead(root, key, read) {
|
|
620
|
-
const inspection = inspections.getStore();
|
|
621
|
-
if (!inspection || inspection.root !== path4.resolve(root)) return read();
|
|
622
|
-
if (!inspection.reads.has(key)) {
|
|
623
|
-
const pending = read();
|
|
624
|
-
inspection.reads.set(key, pending);
|
|
625
|
-
void pending.catch(() => {
|
|
626
|
-
if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
return inspection.reads.get(key);
|
|
630
|
-
}
|
|
631
|
-
function inspectionGit(args, options) {
|
|
632
|
-
return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
|
|
633
|
-
}
|
|
634
|
-
async function auditGitPaths(root, kind) {
|
|
635
|
-
const shared = inspections.getStore()?.root === path4.resolve(root);
|
|
636
|
-
const specs = shared ? [...docSpecs, ...sourceSpecs] : kind === "documents" ? docSpecs : sourceSpecs;
|
|
637
|
-
const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
|
|
638
|
-
const { stdout } = await inspectionGit(
|
|
639
|
-
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
|
|
640
|
-
{ cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
|
|
641
|
-
);
|
|
642
|
-
const paths = [...new Set(stdout.split("\0").filter(Boolean))].filter((file) => kind === "documents" ? /^(readme|agents|claude)\.md$/i.test(path4.posix.basename(file)) : sourceSuffixes.has(file.slice(file.lastIndexOf("."))));
|
|
643
|
-
if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
|
|
644
|
-
return paths.sort();
|
|
645
|
-
}
|
|
646
|
-
var inspections, sourceSpecs, docSpecs, sourceSuffixes;
|
|
647
|
-
var init_inspection = __esm({
|
|
648
|
-
"src/audit/inspection.ts"() {
|
|
858
|
+
};
|
|
859
|
+
}
|
|
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"() {
|
|
649
863
|
"use strict";
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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 -->";
|
|
656
904
|
}
|
|
657
905
|
});
|
|
658
906
|
|
|
@@ -741,190 +989,10 @@ var init_git = __esm({
|
|
|
741
989
|
}
|
|
742
990
|
});
|
|
743
991
|
|
|
744
|
-
// src/utils/storage.ts
|
|
745
|
-
import fs2 from "fs/promises";
|
|
746
|
-
import path5 from "path";
|
|
747
|
-
import { randomUUID } from "crypto";
|
|
748
|
-
async function storePath(root, relative, createParents = false) {
|
|
749
|
-
const normalized = normalizeRepoPath(relative);
|
|
750
|
-
if (!normalized) throw new Error(`Invalid store path: ${relative}`);
|
|
751
|
-
let current = await fs2.realpath(root);
|
|
752
|
-
const parts = normalized.split("/");
|
|
753
|
-
for (let i = 0; i < parts.length; i++) {
|
|
754
|
-
current = path5.join(current, parts[i]);
|
|
755
|
-
let stat;
|
|
756
|
-
try {
|
|
757
|
-
stat = await fs2.lstat(current);
|
|
758
|
-
} catch (error) {
|
|
759
|
-
if (error.code !== "ENOENT") throw error;
|
|
760
|
-
if (createParents && i < parts.length - 1) {
|
|
761
|
-
try {
|
|
762
|
-
await fs2.mkdir(current);
|
|
763
|
-
} catch (mkdirError) {
|
|
764
|
-
if (mkdirError.code !== "EEXIST") throw mkdirError;
|
|
765
|
-
}
|
|
766
|
-
stat = await fs2.lstat(current);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
if (stat?.isSymbolicLink()) throw new Error(`Symlink in store path: ${relative}`);
|
|
770
|
-
}
|
|
771
|
-
return current;
|
|
772
|
-
}
|
|
773
|
-
async function readStoreJson(root, relative) {
|
|
774
|
-
try {
|
|
775
|
-
const file = await storePath(root, relative);
|
|
776
|
-
const raw = await readBoundedFile(file, 10 * 1024 * 1024);
|
|
777
|
-
if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
|
|
778
|
-
const parsed = JSON.parse(raw);
|
|
779
|
-
if (parsed === null) throw new Error("expected a JSON object, received null");
|
|
780
|
-
return parsed;
|
|
781
|
-
} catch (error) {
|
|
782
|
-
if (error.code === "ENOENT") return null;
|
|
783
|
-
throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
async function writeStoreJson(root, relative, value) {
|
|
787
|
-
const payload = JSON.stringify(value, null, 2) + "\n";
|
|
788
|
-
if (Buffer.byteLength(payload) > 10 * 1024 * 1024) {
|
|
789
|
-
throw new Error(`Mason store ${relative} exceeds 10 MiB`);
|
|
790
|
-
}
|
|
791
|
-
const file = await storePath(root, relative, true);
|
|
792
|
-
const temporary = path5.join(path5.dirname(file), `.${path5.basename(file)}.${randomUUID()}.tmp`);
|
|
793
|
-
try {
|
|
794
|
-
const handle = await fs2.open(temporary, "wx", 384);
|
|
795
|
-
try {
|
|
796
|
-
await handle.writeFile(payload, "utf8");
|
|
797
|
-
await handle.sync();
|
|
798
|
-
} finally {
|
|
799
|
-
await handle.close();
|
|
800
|
-
}
|
|
801
|
-
await fs2.rename(temporary, file);
|
|
802
|
-
} finally {
|
|
803
|
-
await fs2.rm(temporary, { force: true });
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
var init_storage = __esm({
|
|
807
|
-
"src/utils/storage.ts"() {
|
|
808
|
-
"use strict";
|
|
809
|
-
init_paths();
|
|
810
|
-
init_files();
|
|
811
|
-
}
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
// src/audit/inputs.ts
|
|
815
|
-
import fs3 from "fs/promises";
|
|
816
|
-
import * as nativeFs from "fs";
|
|
817
|
-
import path6 from "path";
|
|
818
|
-
import fg2 from "fast-glob";
|
|
819
|
-
async function auditInputPath(root, file) {
|
|
820
|
-
if (file === ".") return fs3.realpath(root);
|
|
821
|
-
try {
|
|
822
|
-
return await storePath(root, file);
|
|
823
|
-
} catch (error) {
|
|
824
|
-
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
825
|
-
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
826
|
-
}
|
|
827
|
-
throw error;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
async function readAuditInput(root, file) {
|
|
831
|
-
try {
|
|
832
|
-
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
833
|
-
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
834
|
-
return value;
|
|
835
|
-
} catch (error) {
|
|
836
|
-
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
|
|
837
|
-
throw error;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
async function auditGlob(root, patterns, options) {
|
|
841
|
-
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
842
|
-
const directories = /* @__PURE__ */ new Set();
|
|
843
|
-
const readdir2 = ((directory, opts, callback) => {
|
|
844
|
-
const relative = path6.relative(root, directory).split(path6.sep).join("/") || ".";
|
|
845
|
-
directories.add(relative);
|
|
846
|
-
if (directories.size > MAX_INPUTS) {
|
|
847
|
-
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
auditInputPath(root, relative).then(() => {
|
|
851
|
-
nativeFs.readdir(directory, opts, callback);
|
|
852
|
-
}, callback);
|
|
853
|
-
});
|
|
854
|
-
const lstat2 = ((file, callback) => {
|
|
855
|
-
const relative = path6.relative(root, file).split(path6.sep).join("/") || ".";
|
|
856
|
-
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
857
|
-
});
|
|
858
|
-
const stream = fg2.stream(patterns, {
|
|
859
|
-
cwd: root,
|
|
860
|
-
ignore: options.ignore,
|
|
861
|
-
onlyFiles: false,
|
|
862
|
-
onlyDirectories: options.onlyDirectories,
|
|
863
|
-
objectMode: true,
|
|
864
|
-
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
865
|
-
fs: { readdir: readdir2, lstat: lstat2 },
|
|
866
|
-
concurrency: 16
|
|
867
|
-
});
|
|
868
|
-
const matches = [];
|
|
869
|
-
try {
|
|
870
|
-
for await (const value of stream) {
|
|
871
|
-
const entry = value;
|
|
872
|
-
const file = entry.path;
|
|
873
|
-
if (options.select && !options.select(file)) continue;
|
|
874
|
-
await auditInputPath(root, file);
|
|
875
|
-
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
876
|
-
matches.push(file);
|
|
877
|
-
if (matches.length > MAX_INPUTS) {
|
|
878
|
-
const largest = [...matches.reduce((counts, item) => {
|
|
879
|
-
const dir = item.split("/")[0];
|
|
880
|
-
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
881
|
-
return counts;
|
|
882
|
-
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
883
|
-
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.`);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
} finally {
|
|
887
|
-
stream.destroy();
|
|
888
|
-
}
|
|
889
|
-
return matches.sort();
|
|
890
|
-
}
|
|
891
|
-
async function gitSourcePaths(root) {
|
|
892
|
-
return auditGitPaths(root, "sources");
|
|
893
|
-
}
|
|
894
|
-
async function gitIgnoredPaths(root, files) {
|
|
895
|
-
if (!files.length) return /* @__PURE__ */ new Set();
|
|
896
|
-
const ignored = /* @__PURE__ */ new Set();
|
|
897
|
-
for (let offset = 0; offset < files.length; offset += 1e3) {
|
|
898
|
-
try {
|
|
899
|
-
const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
|
|
900
|
-
cwd: root,
|
|
901
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
902
|
-
timeout: 1e4,
|
|
903
|
-
input: files.slice(offset, offset + 1e3).join("\0") + "\0"
|
|
904
|
-
});
|
|
905
|
-
for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
|
|
906
|
-
} catch (error) {
|
|
907
|
-
if (error.code !== 1) throw error;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
return ignored;
|
|
911
|
-
}
|
|
912
|
-
var MAX_INPUTS;
|
|
913
|
-
var init_inputs = __esm({
|
|
914
|
-
"src/audit/inputs.ts"() {
|
|
915
|
-
"use strict";
|
|
916
|
-
init_files();
|
|
917
|
-
init_storage();
|
|
918
|
-
init_git_read();
|
|
919
|
-
init_inspection();
|
|
920
|
-
MAX_INPUTS = 1e5;
|
|
921
|
-
}
|
|
922
|
-
});
|
|
923
|
-
|
|
924
992
|
// src/audit/docs.ts
|
|
925
993
|
import fs4 from "fs/promises";
|
|
926
994
|
import path7 from "path";
|
|
927
|
-
import
|
|
995
|
+
import fg5 from "fast-glob";
|
|
928
996
|
async function dirtyDocs(resolvedRoot, files) {
|
|
929
997
|
if (!files.length) return /* @__PURE__ */ new Set();
|
|
930
998
|
try {
|
|
@@ -976,7 +1044,7 @@ async function discoverDocPaths(root) {
|
|
|
976
1044
|
const paths = [];
|
|
977
1045
|
const unique = [...new Set(candidates)].sort();
|
|
978
1046
|
for (let offset = 0; offset < unique.length; offset += 256) {
|
|
979
|
-
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)), {
|
|
980
1048
|
ignore: excluded,
|
|
981
1049
|
followSymbolicLinks: false,
|
|
982
1050
|
label: "Documentation discovery"
|
|
@@ -1006,6 +1074,7 @@ async function localDocPaths(root) {
|
|
|
1006
1074
|
return paths;
|
|
1007
1075
|
}
|
|
1008
1076
|
function documentScope(file) {
|
|
1077
|
+
if (!/^(readme|agents|claude)\.md$/i.test(path7.posix.basename(file))) return ".";
|
|
1009
1078
|
const parent = path7.posix.dirname(file);
|
|
1010
1079
|
return path7.posix.basename(parent).toLowerCase() === ".claude" ? path7.posix.dirname(parent) : parent;
|
|
1011
1080
|
}
|
|
@@ -1027,7 +1096,7 @@ async function readDocs(resolvedRoot) {
|
|
|
1027
1096
|
path: candidate,
|
|
1028
1097
|
content: content2,
|
|
1029
1098
|
scope: documentScope(candidate),
|
|
1030
|
-
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",
|
|
1031
1100
|
lineCount: content2.split("\n").length,
|
|
1032
1101
|
lastCommit,
|
|
1033
1102
|
dirty: dirty.has(candidate),
|
|
@@ -1569,34 +1638,18 @@ async function computeDrift(rootDir) {
|
|
|
1569
1638
|
report.stale ||= changedMapped.size > 0 || report.unmappedFiles.length > 0 || committedGhosts.length > 0;
|
|
1570
1639
|
if (!report.historyAvailable) report.recommendation = "full-rebuild";
|
|
1571
1640
|
else if (!report.stale) report.recommendation = "up-to-date";
|
|
1572
|
-
else report.recommendation = changedMapped.size >= FULL_REBUILD_MIN_CHANGED_MAPPED_FILES && changedMapped.size / Math.max(1, mappedFiles.size) > FULL_REBUILD_FRACTION ? "full-rebuild" : "incremental";
|
|
1573
|
-
return report;
|
|
1574
|
-
}
|
|
1575
|
-
var FULL_REBUILD_FRACTION, FULL_REBUILD_MIN_CHANGED_MAPPED_FILES;
|
|
1576
|
-
var init_drift = __esm({
|
|
1577
|
-
"src/drift/drift.ts"() {
|
|
1578
|
-
"use strict";
|
|
1579
|
-
init_git_read();
|
|
1580
|
-
init_snapshot();
|
|
1581
|
-
init_paths();
|
|
1582
|
-
FULL_REBUILD_FRACTION = 0.4;
|
|
1583
|
-
FULL_REBUILD_MIN_CHANGED_MAPPED_FILES = 10;
|
|
1584
|
-
}
|
|
1585
|
-
});
|
|
1586
|
-
|
|
1587
|
-
// src/audit/types.ts
|
|
1588
|
-
var ALL_CHECKS;
|
|
1589
|
-
var init_types = __esm({
|
|
1590
|
-
"src/audit/types.ts"() {
|
|
1591
|
-
"use strict";
|
|
1592
|
-
ALL_CHECKS = [
|
|
1593
|
-
"deleted-reference",
|
|
1594
|
-
"new-module",
|
|
1595
|
-
"stale-count",
|
|
1596
|
-
"dead-command",
|
|
1597
|
-
"deps-changed",
|
|
1598
|
-
"decision-anchor-drift"
|
|
1599
|
-
];
|
|
1641
|
+
else report.recommendation = changedMapped.size >= FULL_REBUILD_MIN_CHANGED_MAPPED_FILES && changedMapped.size / Math.max(1, mappedFiles.size) > FULL_REBUILD_FRACTION ? "full-rebuild" : "incremental";
|
|
1642
|
+
return report;
|
|
1643
|
+
}
|
|
1644
|
+
var FULL_REBUILD_FRACTION, FULL_REBUILD_MIN_CHANGED_MAPPED_FILES;
|
|
1645
|
+
var init_drift = __esm({
|
|
1646
|
+
"src/drift/drift.ts"() {
|
|
1647
|
+
"use strict";
|
|
1648
|
+
init_git_read();
|
|
1649
|
+
init_snapshot();
|
|
1650
|
+
init_paths();
|
|
1651
|
+
FULL_REBUILD_FRACTION = 0.4;
|
|
1652
|
+
FULL_REBUILD_MIN_CHANGED_MAPPED_FILES = 10;
|
|
1600
1653
|
}
|
|
1601
1654
|
});
|
|
1602
1655
|
|
|
@@ -1668,7 +1721,7 @@ var init_deleted_reference = __esm({
|
|
|
1668
1721
|
});
|
|
1669
1722
|
|
|
1670
1723
|
// src/audit/checks/new-module.ts
|
|
1671
|
-
import
|
|
1724
|
+
import fg6 from "fast-glob";
|
|
1672
1725
|
import fs8 from "fs/promises";
|
|
1673
1726
|
import path13 from "path";
|
|
1674
1727
|
function escapeRegExp(text2) {
|
|
@@ -1730,7 +1783,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1730
1783
|
const result = /* @__PURE__ */ new Map();
|
|
1731
1784
|
for (const dir of parents) {
|
|
1732
1785
|
const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
|
|
1733
|
-
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${
|
|
1786
|
+
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
|
|
1734
1787
|
ignore,
|
|
1735
1788
|
onlyDirectories: true,
|
|
1736
1789
|
label: "Module directory discovery",
|
|
@@ -1743,7 +1796,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1743
1796
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1744
1797
|
let count4 = 0;
|
|
1745
1798
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1746
|
-
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)), {
|
|
1747
1800
|
ignore,
|
|
1748
1801
|
followSymbolicLinks: false,
|
|
1749
1802
|
label: `Source discovery in ${dir}`
|
|
@@ -2078,6 +2131,87 @@ var init_dead_command = __esm({
|
|
|
2078
2131
|
}
|
|
2079
2132
|
});
|
|
2080
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
|
+
|
|
2081
2215
|
// src/audit/release-metadata.ts
|
|
2082
2216
|
async function git(root, args) {
|
|
2083
2217
|
return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
@@ -2134,6 +2268,10 @@ var init_release_metadata = __esm({
|
|
|
2134
2268
|
});
|
|
2135
2269
|
|
|
2136
2270
|
// src/audit/checks/deps-changed.ts
|
|
2271
|
+
function hasDependencyContent(content2) {
|
|
2272
|
+
const text2 = content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
|
|
2273
|
+
return /\b(?:dependenc(?:y|ies)|librar(?:y|ies)|frameworks?|tech(?:nology)? stack|prerequisites|requirements)\b/i.test(text2) || /\b(?:package\.json|build\.gradle(?:\.kts)?|settings\.gradle(?:\.kts)?|libs\.versions\.toml|Cargo\.toml|go\.mod|pyproject\.toml|requirements\.txt|Gemfile|composer\.json)\b/i.test(text2) || /\b(?:node(?:\.js)?|npm|pnpm|yarn|bun|deno|python|ruby|rust|go|java|jdk|kotlin|gradle|swift|php|react|vue|angular|compose|ktor|room)\s*(?:version\s*)?[`*:=>~^v\s-]*\d+(?:\.\d+)*\b/i.test(text2) || /\b(?:npm|pnpm|yarn|pip3?|cargo|gem|composer)\s+(?:install|add|require)\b/i.test(text2);
|
|
2274
|
+
}
|
|
2137
2275
|
function manifestPathspecs(doc) {
|
|
2138
2276
|
const scope = documentScope(doc);
|
|
2139
2277
|
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
@@ -2153,12 +2291,17 @@ async function checkDepsChanged(ctx) {
|
|
|
2153
2291
|
});
|
|
2154
2292
|
continue;
|
|
2155
2293
|
}
|
|
2294
|
+
let content2 = doc.content;
|
|
2156
2295
|
if (doc.dirty) {
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2296
|
+
try {
|
|
2297
|
+
content2 = (await inspectionGit(
|
|
2298
|
+
["show", `${doc.lastCommit.hash}:${doc.path}`],
|
|
2299
|
+
{ cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
|
|
2300
|
+
)).stdout;
|
|
2301
|
+
} catch {
|
|
2302
|
+
result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2162
2305
|
}
|
|
2163
2306
|
const range = await commitsTouchingSince(
|
|
2164
2307
|
ctx.root,
|
|
@@ -2183,16 +2326,32 @@ async function checkDepsChanged(ctx) {
|
|
|
2183
2326
|
range.commits = relevant;
|
|
2184
2327
|
range.total = relevant.length;
|
|
2185
2328
|
if (range.total === 0) continue;
|
|
2186
|
-
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
|
+
});
|
|
2187
2343
|
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
2188
2344
|
type: "deps-changed",
|
|
2189
|
-
|
|
2190
|
-
|
|
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 },
|
|
2191
2348
|
evidence: {
|
|
2192
2349
|
kind: "doc-behind-manifests",
|
|
2193
2350
|
docLastCommit: doc.lastCommit,
|
|
2194
2351
|
manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
|
|
2195
|
-
totalCommits: range.total
|
|
2352
|
+
totalCommits: range.total,
|
|
2353
|
+
matches: relevance.matches,
|
|
2354
|
+
matchingIncomplete: relevance.incomplete
|
|
2196
2355
|
}
|
|
2197
2356
|
});
|
|
2198
2357
|
}
|
|
@@ -2202,9 +2361,11 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
|
2202
2361
|
var init_deps_changed = __esm({
|
|
2203
2362
|
"src/audit/checks/deps-changed.ts"() {
|
|
2204
2363
|
"use strict";
|
|
2364
|
+
init_dependency_claims();
|
|
2205
2365
|
init_docs();
|
|
2206
2366
|
init_release_metadata();
|
|
2207
2367
|
init_git();
|
|
2368
|
+
init_inspection();
|
|
2208
2369
|
init_checks();
|
|
2209
2370
|
MANIFEST_COMMITS_CAP = 10;
|
|
2210
2371
|
MANIFEST_PATHSPECS = [
|
|
@@ -2225,7 +2386,7 @@ var init_deps_changed = __esm({
|
|
|
2225
2386
|
});
|
|
2226
2387
|
|
|
2227
2388
|
// src/context/lexical.ts
|
|
2228
|
-
import
|
|
2389
|
+
import path17 from "path";
|
|
2229
2390
|
function tokenize(text2) {
|
|
2230
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));
|
|
2231
2392
|
}
|
|
@@ -2238,7 +2399,7 @@ function tokenSet(text2) {
|
|
|
2238
2399
|
function scoreEntry(taskTokens, entry) {
|
|
2239
2400
|
const nameTokens = tokenSet(entry.name);
|
|
2240
2401
|
const descTokens = tokenSet(entry.description);
|
|
2241
|
-
const fileTokens = tokenSet(entry.files.map((f) =>
|
|
2402
|
+
const fileTokens = tokenSet(entry.files.map((f) => path17.basename(f)).join(" "));
|
|
2242
2403
|
let score = 0;
|
|
2243
2404
|
for (const token of taskTokens) {
|
|
2244
2405
|
if (nameTokens.has(token)) score += 3;
|
|
@@ -2572,7 +2733,7 @@ __export(decisions_exports, {
|
|
|
2572
2733
|
withDecisionWrite: () => withDecisionWrite
|
|
2573
2734
|
});
|
|
2574
2735
|
import fs9 from "fs/promises";
|
|
2575
|
-
import
|
|
2736
|
+
import path18 from "path";
|
|
2576
2737
|
import { createHash } from "crypto";
|
|
2577
2738
|
import { z as z3 } from "zod";
|
|
2578
2739
|
async function loadDecisionStore(rootDir) {
|
|
@@ -2670,7 +2831,7 @@ async function upsertDecision(rootDir, input) {
|
|
|
2670
2831
|
if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
|
|
2671
2832
|
for (const file of files) {
|
|
2672
2833
|
try {
|
|
2673
|
-
await fs9.access(
|
|
2834
|
+
await fs9.access(path18.join(rootDir, file));
|
|
2674
2835
|
} catch {
|
|
2675
2836
|
warnings.push(`anchor file does not exist on disk: ${file}`);
|
|
2676
2837
|
}
|
|
@@ -2795,9 +2956,9 @@ var init_decisions = __esm({
|
|
|
2795
2956
|
});
|
|
2796
2957
|
|
|
2797
2958
|
// src/decisions/drift.ts
|
|
2798
|
-
import
|
|
2959
|
+
import path19 from "path";
|
|
2799
2960
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
2800
|
-
const resolvedRoot =
|
|
2961
|
+
const resolvedRoot = path19.resolve(rootDir);
|
|
2801
2962
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
2802
2963
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
2803
2964
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -2993,7 +3154,9 @@ var init_findings = __esm({
|
|
|
2993
3154
|
kind: z4.literal("doc-behind-manifests"),
|
|
2994
3155
|
docLastCommit: commitSchema,
|
|
2995
3156
|
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
2996
|
-
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()
|
|
2997
3160
|
}),
|
|
2998
3161
|
z4.object({
|
|
2999
3162
|
kind: z4.literal("decision-anchor"),
|
|
@@ -3009,7 +3172,7 @@ var init_findings = __esm({
|
|
|
3009
3172
|
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
3010
3173
|
confidence: z4.enum(["certain", "likely"])
|
|
3011
3174
|
});
|
|
3012
|
-
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.
|
|
3175
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.enum(["recheck", "informational"]).optional() });
|
|
3013
3176
|
checkResultSchema = z4.object({
|
|
3014
3177
|
issues: z4.array(issueSchema),
|
|
3015
3178
|
advisories: z4.array(advisorySchema),
|
|
@@ -3072,14 +3235,18 @@ async function readRecord(root, id) {
|
|
|
3072
3235
|
if (raw === null) return null;
|
|
3073
3236
|
const record = recordSchema.parse(raw);
|
|
3074
3237
|
const { digest: stored, ...payload } = record;
|
|
3075
|
-
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))) {
|
|
3076
3239
|
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
3077
3240
|
}
|
|
3078
3241
|
return record;
|
|
3079
3242
|
}
|
|
3080
|
-
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) {
|
|
3081
3247
|
const files = [finding.anchor.doc], e = finding.evidence;
|
|
3082
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}`];
|
|
3083
3250
|
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
3084
3251
|
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
3085
3252
|
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
@@ -3090,9 +3257,9 @@ function scopes(finding) {
|
|
|
3090
3257
|
return `:(literal)${file}`;
|
|
3091
3258
|
}).sort();
|
|
3092
3259
|
}
|
|
3093
|
-
async function reviewEvidence(root, finding) {
|
|
3260
|
+
async function reviewEvidence(root, finding, reason2) {
|
|
3094
3261
|
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
3095
|
-
const scope = scopes(finding);
|
|
3262
|
+
const scope = scopes(finding, reason2);
|
|
3096
3263
|
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
3097
3264
|
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
3098
3265
|
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
@@ -3118,7 +3285,7 @@ async function reviewEvidence(root, finding) {
|
|
|
3118
3285
|
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
3119
3286
|
}
|
|
3120
3287
|
}
|
|
3121
|
-
const fingerprint =
|
|
3288
|
+
const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason2);
|
|
3122
3289
|
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
3123
3290
|
return {
|
|
3124
3291
|
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
@@ -3165,7 +3332,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
3165
3332
|
const record = await readRecord(root, id);
|
|
3166
3333
|
if (!record) return null;
|
|
3167
3334
|
const event = record.events.at(-1);
|
|
3168
|
-
const current = await reviewEvidence(root, event.finding);
|
|
3335
|
+
const current = await reviewEvidence(root, event.finding, event.reasonCode);
|
|
3169
3336
|
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
3170
3337
|
const base = {
|
|
3171
3338
|
id,
|
|
@@ -3175,7 +3342,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
3175
3342
|
outcome: event.outcome,
|
|
3176
3343
|
reviewedHead: event.evidence.head
|
|
3177
3344
|
};
|
|
3178
|
-
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." };
|
|
3179
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." };
|
|
3180
3347
|
} catch (error) {
|
|
3181
3348
|
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -3203,9 +3370,13 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3203
3370
|
decisionId: finding.evidence.decisionId,
|
|
3204
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."
|
|
3205
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.");
|
|
3206
3377
|
const prepare = async () => {
|
|
3207
3378
|
const record = await readRecord(root, request.findingId);
|
|
3208
|
-
const evidence = await reviewEvidence(root, finding);
|
|
3379
|
+
const evidence = await reviewEvidence(root, finding, request.reasonCode);
|
|
3209
3380
|
await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
|
|
3210
3381
|
const token = digest({ baseline: baseline.original, finding, evidence, record });
|
|
3211
3382
|
return { record, evidence, token };
|
|
@@ -3234,10 +3405,10 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3234
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."
|
|
3235
3406
|
};
|
|
3236
3407
|
}
|
|
3237
|
-
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.");
|
|
3238
3409
|
return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
|
|
3239
3410
|
const state = await prepare();
|
|
3240
|
-
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.");
|
|
3241
3412
|
if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
|
|
3242
3413
|
if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
|
|
3243
3414
|
const { dirty, documentDigest, ...evidence } = state.evidence;
|
|
@@ -3245,7 +3416,8 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3245
3416
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3246
3417
|
reviewer: request.reviewer,
|
|
3247
3418
|
note: request.note,
|
|
3248
|
-
outcome: request.action,
|
|
3419
|
+
outcome: request.action === "dismiss" ? "dismissed" : request.action,
|
|
3420
|
+
reasonCode: request.reasonCode,
|
|
3249
3421
|
finding,
|
|
3250
3422
|
evidence
|
|
3251
3423
|
});
|
|
@@ -3256,11 +3428,11 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3256
3428
|
status: "recorded",
|
|
3257
3429
|
recordPath: recordPath(request.findingId),
|
|
3258
3430
|
event,
|
|
3259
|
-
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."
|
|
3260
3432
|
};
|
|
3261
3433
|
});
|
|
3262
3434
|
}
|
|
3263
|
-
var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3435
|
+
var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3264
3436
|
var init_advisory_review = __esm({
|
|
3265
3437
|
"src/audit/advisory-review.ts"() {
|
|
3266
3438
|
"use strict";
|
|
@@ -3286,11 +3458,13 @@ var init_advisory_review = __esm({
|
|
|
3286
3458
|
lastScopeCommit: commitSchema2,
|
|
3287
3459
|
paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
|
|
3288
3460
|
});
|
|
3461
|
+
dismissalReasonSchema = z6.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
3289
3462
|
eventSchema2 = z6.object({
|
|
3290
3463
|
at: z6.string().datetime(),
|
|
3291
|
-
reviewer: z6.string().trim().min(1).max(200),
|
|
3464
|
+
reviewer: z6.string().trim().min(1).max(200).optional(),
|
|
3292
3465
|
note: z6.string().trim().min(1).max(2500),
|
|
3293
|
-
outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
|
|
3466
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
3467
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
3294
3468
|
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
3295
3469
|
evidence: evidenceSchema2
|
|
3296
3470
|
});
|
|
@@ -3302,13 +3476,14 @@ var init_advisory_review = __esm({
|
|
|
3302
3476
|
recordPath: z6.string().optional(),
|
|
3303
3477
|
reviewer: z6.string().optional(),
|
|
3304
3478
|
note: z6.string().optional(),
|
|
3305
|
-
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
3479
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
|
|
3306
3480
|
reviewedHead: z6.string().optional()
|
|
3307
3481
|
});
|
|
3308
3482
|
advisoryReviewRequest = z6.object({
|
|
3309
3483
|
baselinePath: z6.string().min(1),
|
|
3310
3484
|
findingId: hashSchema,
|
|
3311
|
-
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
3485
|
+
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
3486
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
3312
3487
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
3313
3488
|
note: eventSchema2.shape.note.optional(),
|
|
3314
3489
|
reviewToken: hashSchema.optional()
|
|
@@ -3319,12 +3494,12 @@ var init_advisory_review = __esm({
|
|
|
3319
3494
|
|
|
3320
3495
|
// src/audit/audit.ts
|
|
3321
3496
|
import fs11 from "fs/promises";
|
|
3322
|
-
import
|
|
3497
|
+
import path20 from "path";
|
|
3323
3498
|
async function computeAudit(rootDir, options = {}) {
|
|
3324
3499
|
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
3325
3500
|
}
|
|
3326
3501
|
async function auditCurrent(rootDir, options) {
|
|
3327
|
-
const resolvedRoot =
|
|
3502
|
+
const resolvedRoot = path20.resolve(rootDir);
|
|
3328
3503
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
3329
3504
|
if (docs.length === 0) return null;
|
|
3330
3505
|
const report = {
|
|
@@ -3358,7 +3533,7 @@ async function auditCurrent(rootDir, options) {
|
|
|
3358
3533
|
}
|
|
3359
3534
|
let decisionsPresent = false;
|
|
3360
3535
|
try {
|
|
3361
|
-
await fs11.access(
|
|
3536
|
+
await fs11.access(path20.join(resolvedRoot, ".mason", "decisions"));
|
|
3362
3537
|
decisionsPresent = true;
|
|
3363
3538
|
} catch {
|
|
3364
3539
|
}
|
|
@@ -3370,13 +3545,21 @@ async function auditCurrent(rootDir, options) {
|
|
|
3370
3545
|
changesSinceDoc,
|
|
3371
3546
|
decisionsPresent
|
|
3372
3547
|
};
|
|
3548
|
+
const policy = (await loadProjectConfig(resolvedRoot)).audit;
|
|
3373
3549
|
const selected = options.checks ?? ALL_CHECKS;
|
|
3374
3550
|
for (const name of ALL_CHECKS) {
|
|
3375
3551
|
if (!selected.includes(name)) continue;
|
|
3376
|
-
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));
|
|
3377
3558
|
report.checksRun.push(name);
|
|
3378
3559
|
report.issues.push(...issues);
|
|
3379
|
-
|
|
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 });
|
|
3380
3563
|
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
3381
3564
|
report.skippedChecks.push(...skipped);
|
|
3382
3565
|
}
|
|
@@ -3388,6 +3571,8 @@ async function auditCurrent(rootDir, options) {
|
|
|
3388
3571
|
var init_audit = __esm({
|
|
3389
3572
|
"src/audit/audit.ts"() {
|
|
3390
3573
|
"use strict";
|
|
3574
|
+
init_files();
|
|
3575
|
+
init_policy();
|
|
3391
3576
|
init_profile();
|
|
3392
3577
|
init_drift();
|
|
3393
3578
|
init_snapshot();
|
|
@@ -3413,7 +3598,7 @@ __export(repair_exports, {
|
|
|
3413
3598
|
withRepairSession: () => withRepairSession
|
|
3414
3599
|
});
|
|
3415
3600
|
import fs12 from "fs/promises";
|
|
3416
|
-
import
|
|
3601
|
+
import path21 from "path";
|
|
3417
3602
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3418
3603
|
import { z as z7 } from "zod";
|
|
3419
3604
|
function recheckable(finding) {
|
|
@@ -3434,7 +3619,7 @@ async function docState(root) {
|
|
|
3434
3619
|
docs.push([doc, null]);
|
|
3435
3620
|
}
|
|
3436
3621
|
}
|
|
3437
|
-
return digest(docs);
|
|
3622
|
+
return digest([docs, (await loadProjectConfig(root)).audit]);
|
|
3438
3623
|
}
|
|
3439
3624
|
async function stableAudit(root, checks, options = {}) {
|
|
3440
3625
|
return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
|
|
@@ -3503,8 +3688,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
|
|
|
3503
3688
|
}
|
|
3504
3689
|
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
3505
3690
|
const root = await fs12.realpath(rootDir);
|
|
3506
|
-
const declaredRoot =
|
|
3507
|
-
const relative =
|
|
3691
|
+
const declaredRoot = path21.resolve(rootDir);
|
|
3692
|
+
const relative = path21.isAbsolute(baselinePath) ? path21.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
3508
3693
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
3509
3694
|
const { digest: savedDigest, ...payload } = stored;
|
|
3510
3695
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
@@ -3553,6 +3738,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
3553
3738
|
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
3554
3739
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
3555
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
|
+
}
|
|
3556
3744
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
3557
3745
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
3558
3746
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
@@ -3563,11 +3751,14 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
3563
3751
|
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
3564
3752
|
return { ...base, review, status: "review-required", reason: review.reason };
|
|
3565
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
|
+
}
|
|
3566
3757
|
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
3567
3758
|
return {
|
|
3568
3759
|
...base,
|
|
3569
3760
|
status: "review-required",
|
|
3570
|
-
reason: "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
|
|
3761
|
+
reason: (now ? "The current check still reports this advisory. " : finding.type === "deps-changed" ? "The current check no longer reports this condition; the historical advisory remains awaiting assessment. " : "The current check no longer reports the original finding; retained evidence still requires assessment. ") + "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
|
|
3571
3762
|
};
|
|
3572
3763
|
}
|
|
3573
3764
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
@@ -3591,7 +3782,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
3591
3782
|
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
3592
3783
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
3593
3784
|
for (const f of findings) counts[f.status]++;
|
|
3594
|
-
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)));
|
|
3595
3786
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
3596
3787
|
return {
|
|
3597
3788
|
version: 1,
|
|
@@ -3614,7 +3805,7 @@ function repairExitCode(report) {
|
|
|
3614
3805
|
function formatRepairSummary(report) {
|
|
3615
3806
|
return [
|
|
3616
3807
|
"Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
|
|
3617
|
-
...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),
|
|
3618
3809
|
...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
|
|
3619
3810
|
...report.diagnostics.map((d) => " [unverified] " + d),
|
|
3620
3811
|
...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
|
|
@@ -3647,13 +3838,14 @@ var init_repair = __esm({
|
|
|
3647
3838
|
docs: z7.array(z7.object({
|
|
3648
3839
|
path: z7.string().refine((value) => normalizeRepoPath(value) === value),
|
|
3649
3840
|
scope: z7.string().optional(),
|
|
3650
|
-
kind: z7.enum(["instructions", "readme"]).optional(),
|
|
3841
|
+
kind: z7.enum(["instructions", "readme", "guide"]).optional(),
|
|
3651
3842
|
lastCommit: commitSchema.nullable(),
|
|
3652
3843
|
dirty: z7.boolean(),
|
|
3653
3844
|
lineCount: count2
|
|
3654
3845
|
})).nonempty(),
|
|
3655
3846
|
decisionsChecked: z7.boolean(),
|
|
3656
3847
|
clean: z7.boolean(),
|
|
3848
|
+
excludedChecks: z7.array(z7.object({ check: checkSchema, doc: z7.string() })).optional(),
|
|
3657
3849
|
issues: z7.array(issueSchema),
|
|
3658
3850
|
advisories: z7.array(advisorySchema),
|
|
3659
3851
|
suppressedAdvisories: z7.array(advisorySchema).optional(),
|
|
@@ -3679,9 +3871,9 @@ var init_repair = __esm({
|
|
|
3679
3871
|
|
|
3680
3872
|
// src/automation/evidence.ts
|
|
3681
3873
|
import fs13 from "fs/promises";
|
|
3682
|
-
import
|
|
3874
|
+
import path22 from "path";
|
|
3683
3875
|
import { createHash as createHash3 } from "crypto";
|
|
3684
|
-
import
|
|
3876
|
+
import fg7 from "fast-glob";
|
|
3685
3877
|
import { z as z8 } from "zod";
|
|
3686
3878
|
async function git3(root, ...args) {
|
|
3687
3879
|
return profilePhase("automation.git", async () => (await execGit(args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
|
|
@@ -3699,6 +3891,11 @@ async function readInputs(root) {
|
|
|
3699
3891
|
}
|
|
3700
3892
|
async function collectInputs(root) {
|
|
3701
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
|
+
}
|
|
3702
3899
|
const [headText, docStatus, shallowPath, replacements] = await Promise.all([
|
|
3703
3900
|
git3(root, "rev-parse", "HEAD"),
|
|
3704
3901
|
docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
|
|
@@ -3708,7 +3905,7 @@ async function collectInputs(root) {
|
|
|
3708
3905
|
const head = headText.trim();
|
|
3709
3906
|
let shallow = null;
|
|
3710
3907
|
try {
|
|
3711
|
-
shallow = await fs13.readFile(
|
|
3908
|
+
shallow = await fs13.readFile(path22.resolve(root, shallowPath.trim()), "utf8");
|
|
3712
3909
|
} catch (error) {
|
|
3713
3910
|
if (error.code !== "ENOENT") throw error;
|
|
3714
3911
|
}
|
|
@@ -3727,18 +3924,18 @@ async function collectInputs(root) {
|
|
|
3727
3924
|
docContents.push([file, text2]);
|
|
3728
3925
|
const parsed = { path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") };
|
|
3729
3926
|
parsedDocs.push(parsed);
|
|
3730
|
-
for (const claim of parsed.claims.paths) {
|
|
3927
|
+
for (const claim of allowed.get("deleted-reference").has(file) ? parsed.claims.paths : []) {
|
|
3731
3928
|
const scope = pathClaimScope(file, claim);
|
|
3732
3929
|
if (scope?.candidates.some(gitMetadataPath)) continue;
|
|
3733
3930
|
for (const candidate of scope?.candidates ?? []) {
|
|
3734
3931
|
if (optionalMasonPath(candidate)) continue;
|
|
3735
|
-
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(
|
|
3932
|
+
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path22.posix.dirname(candidate))]);
|
|
3736
3933
|
claims.push([candidate, filePresent, parentPresent]);
|
|
3737
3934
|
}
|
|
3738
3935
|
}
|
|
3739
3936
|
}
|
|
3740
|
-
const combinedDocs = moduleDocumentation(parsedDocs);
|
|
3741
|
-
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 })));
|
|
3742
3939
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
3743
3940
|
const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
3744
3941
|
if (error.code === "ENOENT") return "absent";
|
|
@@ -3747,8 +3944,8 @@ async function collectInputs(root) {
|
|
|
3747
3944
|
const [modules, counts, commands2, decisionFiles] = await Promise.all([
|
|
3748
3945
|
combinedDocs ? moduleCandidates(root, combinedDocs) : [],
|
|
3749
3946
|
Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
|
|
3750
|
-
commandInputs(root, parsedDocs),
|
|
3751
|
-
|
|
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 })
|
|
3752
3949
|
]);
|
|
3753
3950
|
const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
|
|
3754
3951
|
await storePath(root, file);
|
|
@@ -3758,7 +3955,7 @@ async function collectInputs(root) {
|
|
|
3758
3955
|
git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
3759
3956
|
git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
3760
3957
|
]) : ["", ""];
|
|
3761
|
-
const common = [
|
|
3958
|
+
const common = [7, engineVersion, head, shallow, replacements, docContents, auditPolicy];
|
|
3762
3959
|
const keys = {
|
|
3763
3960
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
3764
3961
|
"new-module": hash([common, modules]),
|
|
@@ -3805,6 +4002,8 @@ var engineVersion, hash, content, cacheSchema;
|
|
|
3805
4002
|
var init_evidence = __esm({
|
|
3806
4003
|
"src/automation/evidence.ts"() {
|
|
3807
4004
|
"use strict";
|
|
4005
|
+
init_policy();
|
|
4006
|
+
init_files();
|
|
3808
4007
|
init_git_read();
|
|
3809
4008
|
init_profile();
|
|
3810
4009
|
init_docs();
|
|
@@ -3818,7 +4017,7 @@ var init_evidence = __esm({
|
|
|
3818
4017
|
init_dead_command();
|
|
3819
4018
|
init_storage();
|
|
3820
4019
|
init_advisory_review();
|
|
3821
|
-
engineVersion = true ? "0.
|
|
4020
|
+
engineVersion = true ? "0.19.1" : "development";
|
|
3822
4021
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3823
4022
|
content = readAuditInput;
|
|
3824
4023
|
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
@@ -3828,18 +4027,49 @@ var init_evidence = __esm({
|
|
|
3828
4027
|
// src/setup/model.ts
|
|
3829
4028
|
var model_exports = {};
|
|
3830
4029
|
__export(model_exports, {
|
|
4030
|
+
INACTIVE_HOSTS_PATH: () => INACTIVE_HOSTS_PATH,
|
|
3831
4031
|
SETUP_PATH: () => SETUP_PATH,
|
|
4032
|
+
effectiveSetup: () => effectiveSetup,
|
|
4033
|
+
inactiveHostsSchema: () => inactiveHostsSchema,
|
|
3832
4034
|
loadSetup: () => loadSetup,
|
|
3833
4035
|
loadSetupReceipt: () => loadSetupReceipt,
|
|
3834
4036
|
setupHostSchema: () => setupHostSchema,
|
|
3835
4037
|
setupSchema: () => setupSchema
|
|
3836
4038
|
});
|
|
3837
4039
|
import { z as z9 } from "zod";
|
|
4040
|
+
import fs14 from "fs/promises";
|
|
4041
|
+
import path23 from "path";
|
|
3838
4042
|
async function loadSetup(root) {
|
|
3839
4043
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
3840
4044
|
if (raw === null) return null;
|
|
3841
4045
|
return setupSchema.parse(raw);
|
|
3842
4046
|
}
|
|
4047
|
+
async function effectiveSetup(root) {
|
|
4048
|
+
const local = await loadSetup(root);
|
|
4049
|
+
if (local) return { root, setup: local };
|
|
4050
|
+
const common = await fs14.realpath(path23.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
|
|
4051
|
+
const own = await fs14.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
4052
|
+
if (common === own) return { root, setup: null };
|
|
4053
|
+
const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
|
|
4054
|
+
const candidate = primary.find((field) => field.startsWith("worktree "))?.slice(9);
|
|
4055
|
+
if (!candidate || primary.includes("bare")) return { root, setup: null };
|
|
4056
|
+
let inheritedRoot;
|
|
4057
|
+
try {
|
|
4058
|
+
inheritedRoot = await fs14.realpath((await git3(candidate, "rev-parse", "--show-toplevel")).trim());
|
|
4059
|
+
} catch (error) {
|
|
4060
|
+
if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
|
|
4061
|
+
throw error;
|
|
4062
|
+
}
|
|
4063
|
+
const inheritedCommon = await fs14.realpath(path23.resolve(
|
|
4064
|
+
inheritedRoot,
|
|
4065
|
+
(await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
|
|
4066
|
+
));
|
|
4067
|
+
if (inheritedCommon !== common) throw new Error("Linked worktree setup does not belong to the same Git repository.");
|
|
4068
|
+
const setup = await loadSetup(inheritedRoot);
|
|
4069
|
+
const inactive = await readStoreJson(root, INACTIVE_HOSTS_PATH);
|
|
4070
|
+
if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
|
|
4071
|
+
return { root: inheritedRoot, setup };
|
|
4072
|
+
}
|
|
3843
4073
|
async function loadSetupReceipt(root, directory, host) {
|
|
3844
4074
|
const raw = await readStoreJson(root, directory + "/setup-" + host + ".json");
|
|
3845
4075
|
if (raw === null) return null;
|
|
@@ -3847,10 +4077,11 @@ async function loadSetupReceipt(root, directory, host) {
|
|
|
3847
4077
|
if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
|
|
3848
4078
|
return receipt;
|
|
3849
4079
|
}
|
|
3850
|
-
var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
|
|
4080
|
+
var setupHostSchema, setupSchema, SETUP_PATH, INACTIVE_HOSTS_PATH, inactiveHostsSchema, receiptSchema;
|
|
3851
4081
|
var init_model = __esm({
|
|
3852
4082
|
"src/setup/model.ts"() {
|
|
3853
4083
|
"use strict";
|
|
4084
|
+
init_evidence();
|
|
3854
4085
|
init_storage();
|
|
3855
4086
|
setupHostSchema = z9.object({
|
|
3856
4087
|
configuredVersion: z9.string(),
|
|
@@ -3864,6 +4095,8 @@ var init_model = __esm({
|
|
|
3864
4095
|
claude: setupHostSchema.optional()
|
|
3865
4096
|
}) });
|
|
3866
4097
|
SETUP_PATH = ".mason/local/setup.json";
|
|
4098
|
+
INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
|
|
4099
|
+
inactiveHostsSchema = z9.object({ version: z9.literal(1), hosts: z9.array(z9.enum(["claude", "codex"])) });
|
|
3867
4100
|
receiptSchema = z9.object({
|
|
3868
4101
|
version: z9.literal(1),
|
|
3869
4102
|
host: z9.enum(["codex", "claude"]),
|
|
@@ -3880,7 +4113,7 @@ var init_model = __esm({
|
|
|
3880
4113
|
// src/setup/launcher.ts
|
|
3881
4114
|
import { execFile as execFile2 } from "child_process";
|
|
3882
4115
|
import { promisify as promisify2 } from "util";
|
|
3883
|
-
import
|
|
4116
|
+
import fs15 from "fs/promises";
|
|
3884
4117
|
function globalCommand(args, platform = process.platform) {
|
|
3885
4118
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
|
|
3886
4119
|
}
|
|
@@ -3910,7 +4143,7 @@ var init_launcher = __esm({
|
|
|
3910
4143
|
"use strict";
|
|
3911
4144
|
init_evidence();
|
|
3912
4145
|
init_model();
|
|
3913
|
-
executingVersion = () => true ? "0.
|
|
4146
|
+
executingVersion = () => true ? "0.19.1" : "development";
|
|
3914
4147
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3915
4148
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3916
4149
|
}
|
|
@@ -4035,8 +4268,8 @@ __export(files_exports, {
|
|
|
4035
4268
|
managedBlock: () => managedBlock,
|
|
4036
4269
|
readText: () => readText
|
|
4037
4270
|
});
|
|
4038
|
-
import
|
|
4039
|
-
import
|
|
4271
|
+
import fs18 from "fs/promises";
|
|
4272
|
+
import path27 from "path";
|
|
4040
4273
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4041
4274
|
async function readText(root, file) {
|
|
4042
4275
|
try {
|
|
@@ -4065,14 +4298,14 @@ async function applyEdit(root, edit) {
|
|
|
4065
4298
|
if (edit.after === null) {
|
|
4066
4299
|
const file2 = await storePath(root, edit.path);
|
|
4067
4300
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
4068
|
-
await
|
|
4301
|
+
await fs18.unlink(file2);
|
|
4069
4302
|
return true;
|
|
4070
4303
|
}
|
|
4071
4304
|
const file = await storePath(root, edit.path, true);
|
|
4072
|
-
const temporary =
|
|
4305
|
+
const temporary = path27.join(path27.dirname(file), ".mason-setup-" + randomUUID3() + ".tmp");
|
|
4073
4306
|
try {
|
|
4074
|
-
const mode = await
|
|
4075
|
-
const handle = await
|
|
4307
|
+
const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
|
|
4308
|
+
const handle = await fs18.open(temporary, "wx", mode);
|
|
4076
4309
|
try {
|
|
4077
4310
|
await handle.writeFile(edit.after, "utf8");
|
|
4078
4311
|
await handle.sync();
|
|
@@ -4080,10 +4313,10 @@ async function applyEdit(root, edit) {
|
|
|
4080
4313
|
await handle.close();
|
|
4081
4314
|
}
|
|
4082
4315
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
4083
|
-
await
|
|
4316
|
+
await fs18.rename(temporary, file);
|
|
4084
4317
|
return true;
|
|
4085
4318
|
} finally {
|
|
4086
|
-
await
|
|
4319
|
+
await fs18.rm(temporary, { force: true });
|
|
4087
4320
|
}
|
|
4088
4321
|
}
|
|
4089
4322
|
var init_files2 = __esm({
|
|
@@ -4105,7 +4338,7 @@ __export(execution_exports, {
|
|
|
4105
4338
|
recordExecution: () => recordExecution
|
|
4106
4339
|
});
|
|
4107
4340
|
import os2 from "os";
|
|
4108
|
-
import
|
|
4341
|
+
import fs19 from "fs/promises";
|
|
4109
4342
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
4110
4343
|
import { z as z12 } from "zod";
|
|
4111
4344
|
function parseExecution(raw) {
|
|
@@ -4233,7 +4466,7 @@ async function recordExecution(root, directory, event, run) {
|
|
|
4233
4466
|
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
4234
4467
|
}
|
|
4235
4468
|
} finally {
|
|
4236
|
-
await
|
|
4469
|
+
await fs19.rm(await storePath(root, owner), { force: true }).catch(() => {
|
|
4237
4470
|
});
|
|
4238
4471
|
}
|
|
4239
4472
|
}
|
|
@@ -4317,13 +4550,17 @@ async function observeReadOnlyTool(dir, event) {
|
|
|
4317
4550
|
}
|
|
4318
4551
|
function summarize(report) {
|
|
4319
4552
|
const open = report.findings.filter((f) => f.status !== "resolved");
|
|
4553
|
+
const onlyReview = report.status === "incomplete" && !report.diagnostics.length && !report.counts.unresolved && !report.counts.unverified && report.counts["review-required"] > 0;
|
|
4320
4554
|
return [
|
|
4321
|
-
`Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
|
|
4322
|
-
...open.slice(0, 4).map((f) =>
|
|
4555
|
+
onlyReview ? `Mason: ${report.counts["review-required"]} advisor${report.counts["review-required"] === 1 ? "y" : "ies"} awaiting review; no unresolved issues.` : `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
|
|
4556
|
+
...open.slice(0, 4).map((f) => {
|
|
4557
|
+
const detail = f.current ? f.current.message : f.status === "review-required" ? `${f.original.type}: current check no longer reports ${f.original.type === "deps-changed" ? "this condition" : "the original finding"}; historical evidence awaits assessment.` : `Original evidence: ${f.original.message}. ${f.reason}`;
|
|
4558
|
+
return `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(detail)}`;
|
|
4559
|
+
}),
|
|
4323
4560
|
...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
|
|
4324
4561
|
...report.diagnostics.slice(0, 2).map(cleanText),
|
|
4325
4562
|
`Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason-auto check.`,
|
|
4326
|
-
"Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and
|
|
4563
|
+
"Keep original evidence. Address findings relevant to the authorized task; report unrelated findings with a brief scope reason and suggested follow-up. Use review_advisory for authorized assessments; otherwise leave advisories open without approving them."
|
|
4327
4564
|
].join("\n");
|
|
4328
4565
|
}
|
|
4329
4566
|
async function loadState(ws) {
|
|
@@ -4547,7 +4784,7 @@ async function automate(dir, event) {
|
|
|
4547
4784
|
if (event.event === "after_tool" && event.mutating) {
|
|
4548
4785
|
session.mutationObserved = true;
|
|
4549
4786
|
if (!event.toolId || !session.pending[event.toolId]) {
|
|
4550
|
-
const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
|
|
4787
|
+
const gap = "A tool completed without an observed matching pre-tool capture in this checkout; pre-edit coverage is unknown. The tool may have changed checkouts; captures are kept separately for each worktree.";
|
|
4551
4788
|
if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
|
|
4552
4789
|
}
|
|
4553
4790
|
if (event.toolId) delete session.pending[event.toolId];
|
|
@@ -4656,8 +4893,8 @@ var init_runtime = __esm({
|
|
|
4656
4893
|
});
|
|
4657
4894
|
|
|
4658
4895
|
// src/automation/hook-root.ts
|
|
4659
|
-
import
|
|
4660
|
-
import
|
|
4896
|
+
import fs20 from "fs/promises";
|
|
4897
|
+
import path28 from "path";
|
|
4661
4898
|
var init_hook_root = __esm({
|
|
4662
4899
|
"src/automation/hook-root.ts"() {
|
|
4663
4900
|
"use strict";
|
|
@@ -5067,9 +5304,11 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
5067
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.
|
|
5068
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.
|
|
5069
5306
|
|
|
5070
|
-
- 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.
|
|
5071
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.
|
|
5072
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
|
+
|
|
5073
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.
|
|
5074
5313
|
|
|
5075
5314
|
Inspect source for what the retrieved context does not answer.
|
|
@@ -5500,10 +5739,10 @@ var init_check = __esm({
|
|
|
5500
5739
|
});
|
|
5501
5740
|
|
|
5502
5741
|
// src/review/evidence.ts
|
|
5503
|
-
import
|
|
5742
|
+
import path29 from "path";
|
|
5504
5743
|
import { z as z22 } from "zod";
|
|
5505
5744
|
function relativeArtifact(root, file) {
|
|
5506
|
-
const relative =
|
|
5745
|
+
const relative = path29.isAbsolute(file) ? path29.relative(root, file) : file;
|
|
5507
5746
|
const normalized = normalizeRepoPath(relative);
|
|
5508
5747
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
5509
5748
|
return normalized;
|
|
@@ -5600,7 +5839,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
5600
5839
|
const report = await readStoreJson(root, file);
|
|
5601
5840
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
5602
5841
|
const sourceRoot = check.sourceRoot ?? root;
|
|
5603
|
-
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.");
|
|
5604
5843
|
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
5605
5844
|
result.outcome = parsed.outcome;
|
|
5606
5845
|
result.counts = parsed.counts;
|
|
@@ -5721,8 +5960,8 @@ var init_evidence2 = __esm({
|
|
|
5721
5960
|
});
|
|
5722
5961
|
|
|
5723
5962
|
// src/review/review.ts
|
|
5724
|
-
import
|
|
5725
|
-
import
|
|
5963
|
+
import fs21 from "fs/promises";
|
|
5964
|
+
import path30 from "path";
|
|
5726
5965
|
import { execFile as execFile7 } from "child_process";
|
|
5727
5966
|
import { promisify as promisify7 } from "util";
|
|
5728
5967
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
@@ -5751,7 +5990,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
5751
5990
|
return matchingPaths(record.files, changedFiles);
|
|
5752
5991
|
}
|
|
5753
5992
|
async function computeReview(rootDir, base, options = {}) {
|
|
5754
|
-
const resolvedRoot =
|
|
5993
|
+
const resolvedRoot = path30.resolve(rootDir);
|
|
5755
5994
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
5756
5995
|
if (head === "unknown") return null;
|
|
5757
5996
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -5801,7 +6040,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
5801
6040
|
analyzed,
|
|
5802
6041
|
async (relPath) => {
|
|
5803
6042
|
try {
|
|
5804
|
-
await
|
|
6043
|
+
await fs21.access(path30.join(resolvedRoot, relPath));
|
|
5805
6044
|
return true;
|
|
5806
6045
|
} catch {
|
|
5807
6046
|
return false;
|
|
@@ -5936,7 +6175,7 @@ var init_onboarding = __esm({
|
|
|
5936
6175
|
});
|
|
5937
6176
|
|
|
5938
6177
|
// src/impact/references.ts
|
|
5939
|
-
import
|
|
6178
|
+
import path31 from "path";
|
|
5940
6179
|
function templateEnd(content2, start) {
|
|
5941
6180
|
let i = start + 1, interpolation = 0;
|
|
5942
6181
|
while (i < content2.length) {
|
|
@@ -5958,10 +6197,10 @@ function templateEnd(content2, start) {
|
|
|
5958
6197
|
continue;
|
|
5959
6198
|
}
|
|
5960
6199
|
if (c === '"' || c === "'") {
|
|
5961
|
-
const
|
|
6200
|
+
const quote2 = c;
|
|
5962
6201
|
for (i++; i < content2.length; i++) {
|
|
5963
6202
|
if (content2[i] === "\\") i++;
|
|
5964
|
-
else if (content2[i] ===
|
|
6203
|
+
else if (content2[i] === quote2) {
|
|
5965
6204
|
i++;
|
|
5966
6205
|
break;
|
|
5967
6206
|
}
|
|
@@ -6062,10 +6301,10 @@ function jvmImports(content2) {
|
|
|
6062
6301
|
}
|
|
6063
6302
|
function resolveImport(file, specifier, available) {
|
|
6064
6303
|
if (!specifier.startsWith("./") && !specifier.startsWith("../")) return null;
|
|
6065
|
-
const relative =
|
|
6304
|
+
const relative = path31.posix.normalize(path31.posix.join(path31.posix.dirname(file), specifier));
|
|
6066
6305
|
if (relative.startsWith("../")) return null;
|
|
6067
6306
|
if (available.has(relative)) return relative;
|
|
6068
|
-
const ext =
|
|
6307
|
+
const ext = path31.posix.extname(relative);
|
|
6069
6308
|
const replacements = { ".js": [".ts", ".tsx"], ".mjs": [".mts"], ".cjs": [".cts"] };
|
|
6070
6309
|
const candidates = ext ? (replacements[ext] ?? []).map((e) => relative.slice(0, -ext.length) + e) : jsExtensions.flatMap((e) => [relative + e, relative + "/index" + e]);
|
|
6071
6310
|
const found = candidates.filter((f) => available.has(f));
|
|
@@ -6074,23 +6313,23 @@ function resolveImport(file, specifier, available) {
|
|
|
6074
6313
|
function mentionsPath(content2, target) {
|
|
6075
6314
|
return new RegExp(`(?<![\\w./\\\\-])(?:\\./)?${escape(target)}(?![\\w./\\\\-])`).test(content2);
|
|
6076
6315
|
}
|
|
6077
|
-
function createReferenceMatcher(
|
|
6316
|
+
function createReferenceMatcher(targets2, available, targetContents = /* @__PURE__ */ new Map()) {
|
|
6078
6317
|
const counts = /* @__PURE__ */ new Map();
|
|
6079
6318
|
for (const file of available) {
|
|
6080
|
-
const name =
|
|
6319
|
+
const name = path31.posix.basename(file, path31.posix.extname(file));
|
|
6081
6320
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
6082
6321
|
}
|
|
6083
6322
|
const jvmTargets = new Map([...targetContents].filter(([file]) => /\.(?:kt|kts|java)$/.test(file)).map(([file, content2]) => [file, declaredSymbols(content2)]));
|
|
6084
6323
|
return (source) => {
|
|
6085
|
-
if (
|
|
6086
|
-
const resolved = new Set(jsExtensions.includes(
|
|
6324
|
+
if (targets2.includes(source.path)) return [];
|
|
6325
|
+
const resolved = new Set(jsExtensions.includes(path31.posix.extname(source.path)) ? imports(source.content).map((s) => resolveImport(source.path, s, available)).filter(Boolean) : []);
|
|
6087
6326
|
if (/\.(?:kt|kts|java)$/.test(source.path)) {
|
|
6088
6327
|
const imported = jvmImports(source.content);
|
|
6089
6328
|
for (const [target, symbols] of jvmTargets) if (symbols.some((symbol) => imported.some((name) => name === symbol || name.startsWith(symbol + ".")))) resolved.add(target);
|
|
6090
6329
|
}
|
|
6091
6330
|
const groups = /* @__PURE__ */ new Map();
|
|
6092
6331
|
const add = (evidence, match) => groups.set(evidence, [...groups.get(evidence) ?? [], match]);
|
|
6093
|
-
for (const target of
|
|
6332
|
+
for (const target of targets2) {
|
|
6094
6333
|
if (resolved.has(target)) {
|
|
6095
6334
|
add("resolved-import", target);
|
|
6096
6335
|
continue;
|
|
@@ -6099,7 +6338,7 @@ function createReferenceMatcher(targets, available, targetContents = /* @__PURE_
|
|
|
6099
6338
|
add("explicit-path", target);
|
|
6100
6339
|
continue;
|
|
6101
6340
|
}
|
|
6102
|
-
const ext =
|
|
6341
|
+
const ext = path31.posix.extname(target), name = path31.posix.basename(target, ext);
|
|
6103
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;
|
|
6104
6343
|
if (word(name).test(source.content)) {
|
|
6105
6344
|
add("name-candidate", name);
|
|
@@ -6128,11 +6367,11 @@ var impact_exports = {};
|
|
|
6128
6367
|
__export(impact_exports, {
|
|
6129
6368
|
analyzeImpact: () => analyzeImpact
|
|
6130
6369
|
});
|
|
6131
|
-
import
|
|
6370
|
+
import path32 from "path";
|
|
6132
6371
|
import { execFile as execFile8 } from "child_process";
|
|
6133
6372
|
import { promisify as promisify8 } from "util";
|
|
6134
6373
|
async function analyzeImpact(rootDir, targetFiles) {
|
|
6135
|
-
const resolvedRoot =
|
|
6374
|
+
const resolvedRoot = path32.resolve(rootDir);
|
|
6136
6375
|
const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
|
|
6137
6376
|
const [cochange, references, tests] = await Promise.all([
|
|
6138
6377
|
getCochangeFiles(resolvedRoot, resolvedTargets),
|
|
@@ -6146,10 +6385,10 @@ async function analyzeImpact(rootDir, targetFiles) {
|
|
|
6146
6385
|
tests
|
|
6147
6386
|
};
|
|
6148
6387
|
}
|
|
6149
|
-
async function resolveTargetFiles(rootDir,
|
|
6388
|
+
async function resolveTargetFiles(rootDir, targets2) {
|
|
6150
6389
|
const resolved = [];
|
|
6151
6390
|
const access = await createFileAccess(rootDir);
|
|
6152
|
-
for (const target of
|
|
6391
|
+
for (const target of targets2) {
|
|
6153
6392
|
if (target.includes("/")) {
|
|
6154
6393
|
const normalized = normalizeRepoPath(target);
|
|
6155
6394
|
if (normalized) resolved.push(normalized);
|
|
@@ -6246,9 +6485,9 @@ async function getRelatedTests(rootDir, targetFiles) {
|
|
|
6246
6485
|
const testFiles = await (await createFileAccess(rootDir)).list(testPatterns);
|
|
6247
6486
|
const results = [];
|
|
6248
6487
|
for (const target of targetFiles) {
|
|
6249
|
-
const targetBaseName =
|
|
6488
|
+
const targetBaseName = path32.basename(target).replace(/\.[^.]+$/, "");
|
|
6250
6489
|
for (const testFile of testFiles) {
|
|
6251
|
-
const testBaseName =
|
|
6490
|
+
const testBaseName = path32.basename(testFile).replace(/\.[^.]+$/, "");
|
|
6252
6491
|
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
6253
6492
|
if (sourceName === targetBaseName) {
|
|
6254
6493
|
results.push({
|
|
@@ -6276,26 +6515,26 @@ var assemble_exports = {};
|
|
|
6276
6515
|
__export(assemble_exports, {
|
|
6277
6516
|
assembleContext: () => assembleContext
|
|
6278
6517
|
});
|
|
6279
|
-
import
|
|
6280
|
-
import
|
|
6518
|
+
import path33 from "path";
|
|
6519
|
+
import fs22 from "fs/promises";
|
|
6281
6520
|
async function collectImpact(root, candidates) {
|
|
6282
|
-
const
|
|
6521
|
+
const targets2 = /* @__PURE__ */ new Set();
|
|
6283
6522
|
let sourceFiles;
|
|
6284
6523
|
for (const candidate of sanitizeRepoPaths(candidates)) {
|
|
6285
|
-
const stat = await
|
|
6524
|
+
const stat = await fs22.stat(path33.join(root, candidate)).catch(() => null);
|
|
6286
6525
|
if (stat?.isDirectory()) {
|
|
6287
6526
|
sourceFiles ??= await (await createFileAccess(root)).list();
|
|
6288
6527
|
for (const file of sourceFiles) {
|
|
6289
|
-
if (anchorMatches(candidate, file))
|
|
6290
|
-
if (
|
|
6528
|
+
if (anchorMatches(candidate, file)) targets2.add(file);
|
|
6529
|
+
if (targets2.size >= MAX_IMPACT_TARGETS) break;
|
|
6291
6530
|
}
|
|
6292
6531
|
} else {
|
|
6293
|
-
|
|
6532
|
+
targets2.add(candidate);
|
|
6294
6533
|
}
|
|
6295
|
-
if (
|
|
6534
|
+
if (targets2.size >= MAX_IMPACT_TARGETS) break;
|
|
6296
6535
|
}
|
|
6297
|
-
if (!
|
|
6298
|
-
const result = await analyzeImpact(root, [...
|
|
6536
|
+
if (!targets2.size) return { impact: null, relatedTests: [] };
|
|
6537
|
+
const result = await analyzeImpact(root, [...targets2]);
|
|
6299
6538
|
const referenceCandidatesOmitted = result.references.filter((r) => r.evidence === "name-candidate").length;
|
|
6300
6539
|
return {
|
|
6301
6540
|
impact: {
|
|
@@ -6308,7 +6547,7 @@ async function collectImpact(root, candidates) {
|
|
|
6308
6547
|
};
|
|
6309
6548
|
}
|
|
6310
6549
|
async function assembleContext(rootDir, task, files) {
|
|
6311
|
-
const resolvedRoot =
|
|
6550
|
+
const resolvedRoot = path33.resolve(rootDir);
|
|
6312
6551
|
const mapState = await inspectSnapshot(resolvedRoot);
|
|
6313
6552
|
const snapshot = mapState.snapshot;
|
|
6314
6553
|
const store = await loadDecisionStore(resolvedRoot);
|
|
@@ -6504,7 +6743,7 @@ __export(observations_exports, {
|
|
|
6504
6743
|
observeActivation: () => observeActivation,
|
|
6505
6744
|
readObservation: () => readObservation
|
|
6506
6745
|
});
|
|
6507
|
-
import
|
|
6746
|
+
import fs23 from "fs/promises";
|
|
6508
6747
|
import { z as z23 } from "zod";
|
|
6509
6748
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
6510
6749
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
@@ -6521,15 +6760,15 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
6521
6760
|
if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
|
|
6522
6761
|
try {
|
|
6523
6762
|
if (!revision) {
|
|
6524
|
-
if ((await
|
|
6763
|
+
if ((await effectiveSetup(process.env.MASON_SETUP_ROOT)).setup?.hosts[host]) {
|
|
6525
6764
|
return "Mason setup is now present. Restart the assistant to connect this MCP session to the configured integration; activation has not been established by this session.";
|
|
6526
6765
|
}
|
|
6527
6766
|
return inactiveSetupMessage();
|
|
6528
6767
|
}
|
|
6529
6768
|
const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
|
|
6530
|
-
const ws = capturedDirectory ? { root: await
|
|
6531
|
-
if (ws.root !== await
|
|
6532
|
-
const setup = await
|
|
6769
|
+
const ws = capturedDirectory ? { root: await fs23.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
6770
|
+
if (ws.root !== await fs23.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
6771
|
+
const { setup } = await effectiveSetup(ws.root);
|
|
6533
6772
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
6534
6773
|
const directory = ws.directory + "/activation";
|
|
6535
6774
|
await withLock(ws.root, directory, async () => {
|
|
@@ -6603,8 +6842,9 @@ __export(status_exports, {
|
|
|
6603
6842
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
6604
6843
|
async function setupStatus(dir) {
|
|
6605
6844
|
const ws = await workspace(dir);
|
|
6606
|
-
const
|
|
6607
|
-
|
|
6845
|
+
const activation = await effectiveSetup(ws.root);
|
|
6846
|
+
const setup = activation.setup;
|
|
6847
|
+
if (!setup || !Object.keys(setup.hosts).length) {
|
|
6608
6848
|
const partial = await Promise.all(["codex", "claude"].map(async (host) => ({
|
|
6609
6849
|
host,
|
|
6610
6850
|
receipt: await loadSetupReceipt(ws.root, ws.directory, host)
|
|
@@ -6638,7 +6878,7 @@ async function setupStatus(dir) {
|
|
|
6638
6878
|
]));
|
|
6639
6879
|
const hooks = isDeepStrictEqual2(compatibleHooks, expectedHooks) && !config.disabled;
|
|
6640
6880
|
const local = await loadSetupReceipt(ws.root, ws.directory, host);
|
|
6641
|
-
const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
|
|
6881
|
+
const configured = activation.root !== ws.root || local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
|
|
6642
6882
|
const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
|
|
6643
6883
|
const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
|
|
6644
6884
|
const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
|
|
@@ -6670,6 +6910,7 @@ async function setupStatus(dir) {
|
|
|
6670
6910
|
status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
|
|
6671
6911
|
root: ws.root,
|
|
6672
6912
|
hosts,
|
|
6913
|
+
activationRoot: activation.root,
|
|
6673
6914
|
decisionRecords: decisions.records.length,
|
|
6674
6915
|
diagnostics: decisions.diagnostics,
|
|
6675
6916
|
scope: "Activation receipts record observed use after setup, not complete interception, correct repairs, or measured usefulness. Host trust and higher-priority settings may prevent execution. Receipts are local to this worktree and branch."
|
|
@@ -6677,6 +6918,9 @@ async function setupStatus(dir) {
|
|
|
6677
6918
|
}
|
|
6678
6919
|
function summarizeActivation(status) {
|
|
6679
6920
|
const lines = ["Mason setup: " + status.status + "."];
|
|
6921
|
+
if (status.activationRoot && status.activationRoot !== status.root) {
|
|
6922
|
+
lines.push("Activation inherited from " + status.activationRoot + "; evidence belongs to this worktree.");
|
|
6923
|
+
}
|
|
6680
6924
|
for (const [host, state] of Object.entries(status.hosts)) {
|
|
6681
6925
|
lines.push(
|
|
6682
6926
|
`${host}: ${state.status}`,
|
|
@@ -6846,8 +7090,8 @@ __export(client_exports, {
|
|
|
6846
7090
|
function createConfluenceClient(config, fetchFn = fetch) {
|
|
6847
7091
|
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
6848
7092
|
const auth = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
|
|
6849
|
-
async function call(method,
|
|
6850
|
-
const res = await fetchFn(`${baseUrl}${
|
|
7093
|
+
async function call(method, path43, body) {
|
|
7094
|
+
const res = await fetchFn(`${baseUrl}${path43}`, {
|
|
6851
7095
|
method,
|
|
6852
7096
|
headers: {
|
|
6853
7097
|
Authorization: auth,
|
|
@@ -6859,7 +7103,7 @@ function createConfluenceClient(config, fetchFn = fetch) {
|
|
|
6859
7103
|
if (!res.ok) {
|
|
6860
7104
|
const text2 = await res.text();
|
|
6861
7105
|
throw new Error(
|
|
6862
|
-
`Confluence ${method} ${
|
|
7106
|
+
`Confluence ${method} ${path43} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
|
|
6863
7107
|
);
|
|
6864
7108
|
}
|
|
6865
7109
|
if (res.status === 204) return null;
|
|
@@ -6960,28 +7204,28 @@ __export(config_exports2, {
|
|
|
6960
7204
|
saveConfluenceConfig: () => saveConfluenceConfig,
|
|
6961
7205
|
validateProvider: () => validateProvider
|
|
6962
7206
|
});
|
|
6963
|
-
import
|
|
6964
|
-
import
|
|
7207
|
+
import fs24 from "fs/promises";
|
|
7208
|
+
import path34 from "path";
|
|
6965
7209
|
import os3 from "os";
|
|
6966
7210
|
import { execFile as execFile9 } from "child_process";
|
|
6967
7211
|
import { promisify as promisify9 } from "util";
|
|
6968
7212
|
function configDir() {
|
|
6969
|
-
return
|
|
7213
|
+
return path34.join(os3.homedir(), ".mason");
|
|
6970
7214
|
}
|
|
6971
7215
|
function configFile() {
|
|
6972
|
-
return
|
|
7216
|
+
return path34.join(configDir(), "config.json");
|
|
6973
7217
|
}
|
|
6974
7218
|
async function loadConfig() {
|
|
6975
7219
|
try {
|
|
6976
|
-
const raw = await
|
|
7220
|
+
const raw = await fs24.readFile(configFile(), "utf-8");
|
|
6977
7221
|
return JSON.parse(raw);
|
|
6978
7222
|
} catch {
|
|
6979
7223
|
return null;
|
|
6980
7224
|
}
|
|
6981
7225
|
}
|
|
6982
7226
|
async function saveConfig(config) {
|
|
6983
|
-
await
|
|
6984
|
-
await
|
|
7227
|
+
await fs24.mkdir(configDir(), { recursive: true });
|
|
7228
|
+
await fs24.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
|
|
6985
7229
|
}
|
|
6986
7230
|
function getDefaultModel(provider) {
|
|
6987
7231
|
return DEFAULT_MODELS[provider];
|
|
@@ -7141,21 +7385,21 @@ var init_renderer = __esm({
|
|
|
7141
7385
|
});
|
|
7142
7386
|
|
|
7143
7387
|
// src/confluence/diff.ts
|
|
7144
|
-
import
|
|
7145
|
-
import
|
|
7388
|
+
import fs25 from "fs/promises";
|
|
7389
|
+
import path35 from "path";
|
|
7146
7390
|
import { createHash as createHash6 } from "crypto";
|
|
7147
7391
|
function hashDescription(description) {
|
|
7148
7392
|
return createHash6("sha256").update(description, "utf8").digest("hex");
|
|
7149
7393
|
}
|
|
7150
7394
|
function syncStateDir(rootDir) {
|
|
7151
|
-
return
|
|
7395
|
+
return path35.join(rootDir, ".mason");
|
|
7152
7396
|
}
|
|
7153
7397
|
function syncStatePath(rootDir) {
|
|
7154
|
-
return
|
|
7398
|
+
return path35.join(syncStateDir(rootDir), "confluence-sync.json");
|
|
7155
7399
|
}
|
|
7156
7400
|
async function loadSyncState(rootDir) {
|
|
7157
7401
|
try {
|
|
7158
|
-
const raw = await
|
|
7402
|
+
const raw = await fs25.readFile(syncStatePath(rootDir), "utf-8");
|
|
7159
7403
|
const parsed = JSON.parse(raw);
|
|
7160
7404
|
if (parsed.version !== 2) return null;
|
|
7161
7405
|
return parsed;
|
|
@@ -7164,8 +7408,8 @@ async function loadSyncState(rootDir) {
|
|
|
7164
7408
|
}
|
|
7165
7409
|
}
|
|
7166
7410
|
async function saveSyncState(rootDir, state) {
|
|
7167
|
-
await
|
|
7168
|
-
await
|
|
7411
|
+
await fs25.mkdir(syncStateDir(rootDir), { recursive: true });
|
|
7412
|
+
await fs25.writeFile(
|
|
7169
7413
|
syncStatePath(rootDir),
|
|
7170
7414
|
JSON.stringify(state, null, 2),
|
|
7171
7415
|
"utf-8"
|
|
@@ -7723,8 +7967,8 @@ import { z as z24 } from "zod";
|
|
|
7723
7967
|
// src/mcp/tools.ts
|
|
7724
7968
|
init_repair();
|
|
7725
7969
|
init_advisory_review();
|
|
7726
|
-
import
|
|
7727
|
-
import
|
|
7970
|
+
import fs26 from "fs/promises";
|
|
7971
|
+
import path36 from "path";
|
|
7728
7972
|
import { execFile as execFile11 } from "child_process";
|
|
7729
7973
|
import { promisify as promisify11 } from "util";
|
|
7730
7974
|
|
|
@@ -7733,11 +7977,11 @@ init_audit();
|
|
|
7733
7977
|
init_types();
|
|
7734
7978
|
init_repair();
|
|
7735
7979
|
init_findings();
|
|
7736
|
-
import
|
|
7980
|
+
import path25 from "path";
|
|
7737
7981
|
|
|
7738
7982
|
// src/audit/advisory-cli.ts
|
|
7739
7983
|
init_advisory_review();
|
|
7740
|
-
import
|
|
7984
|
+
import path24 from "path";
|
|
7741
7985
|
import { stripVTControlCharacters } from "util";
|
|
7742
7986
|
|
|
7743
7987
|
// src/audit/cli.ts
|
|
@@ -7804,7 +8048,7 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
7804
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."
|
|
7805
8049
|
);
|
|
7806
8050
|
lines.push(
|
|
7807
|
-
"-
|
|
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."
|
|
7808
8052
|
);
|
|
7809
8053
|
lines.push("");
|
|
7810
8054
|
lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
|
|
@@ -7834,18 +8078,18 @@ import { execFile as execFile3 } from "child_process";
|
|
|
7834
8078
|
import { promisify as promisify3 } from "util";
|
|
7835
8079
|
|
|
7836
8080
|
// src/analyzers/base.ts
|
|
7837
|
-
import
|
|
7838
|
-
import
|
|
8081
|
+
import fs16 from "fs/promises";
|
|
8082
|
+
import fg8 from "fast-glob";
|
|
7839
8083
|
var BaseAnalyzer = class {
|
|
7840
8084
|
async findFiles(patterns, root) {
|
|
7841
|
-
return
|
|
8085
|
+
return fg8(patterns, {
|
|
7842
8086
|
cwd: root,
|
|
7843
8087
|
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
|
|
7844
8088
|
absolute: true
|
|
7845
8089
|
});
|
|
7846
8090
|
}
|
|
7847
8091
|
async readFile(filePath) {
|
|
7848
|
-
return
|
|
8092
|
+
return fs16.readFile(filePath, "utf-8");
|
|
7849
8093
|
}
|
|
7850
8094
|
createFinding(partial) {
|
|
7851
8095
|
return {
|
|
@@ -8050,7 +8294,7 @@ async function isGitRepo(dir) {
|
|
|
8050
8294
|
|
|
8051
8295
|
// src/mcp/sampler.ts
|
|
8052
8296
|
init_files();
|
|
8053
|
-
import
|
|
8297
|
+
import path26 from "path";
|
|
8054
8298
|
import { execFile as execFile4 } from "child_process";
|
|
8055
8299
|
import { promisify as promisify4 } from "util";
|
|
8056
8300
|
var exec2 = promisify4(execFile4);
|
|
@@ -8158,8 +8402,8 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
8158
8402
|
const projectConfig = access.config;
|
|
8159
8403
|
for (const filePath of projectConfig.alwaysInclude ?? []) {
|
|
8160
8404
|
if (selected.size >= maxFiles) break;
|
|
8161
|
-
const resolvedPath =
|
|
8162
|
-
if (!resolvedPath.startsWith(
|
|
8405
|
+
const resolvedPath = path26.resolve(rootDir, filePath);
|
|
8406
|
+
if (!resolvedPath.startsWith(path26.resolve(rootDir))) continue;
|
|
8163
8407
|
selected.set(filePath, "always-include (project config)");
|
|
8164
8408
|
}
|
|
8165
8409
|
let configCount = 0;
|
|
@@ -8225,7 +8469,7 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
8225
8469
|
if (!line) continue;
|
|
8226
8470
|
if (line.includes("node_modules") || line.includes("/build/") || line.includes(".gradle") || line.includes("/generated/"))
|
|
8227
8471
|
continue;
|
|
8228
|
-
const ext =
|
|
8472
|
+
const ext = path26.extname(line).slice(1);
|
|
8229
8473
|
if (!SOURCE_EXTENSIONS.includes(ext)) continue;
|
|
8230
8474
|
fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
|
|
8231
8475
|
}
|
|
@@ -8580,7 +8824,7 @@ ${JSON.stringify({ partials }, null, 2)}`;
|
|
|
8580
8824
|
init_lock();
|
|
8581
8825
|
init_storage();
|
|
8582
8826
|
init_snapshot();
|
|
8583
|
-
import
|
|
8827
|
+
import fs17 from "fs/promises";
|
|
8584
8828
|
import { z as z11 } from "zod";
|
|
8585
8829
|
var DIRECTORY = ".mason/partial-snapshots";
|
|
8586
8830
|
var partialSchema = z11.object({
|
|
@@ -8598,7 +8842,7 @@ async function savePartial(rootDir, partial) {
|
|
|
8598
8842
|
async function loadAllPartials(rootDir) {
|
|
8599
8843
|
let entries;
|
|
8600
8844
|
try {
|
|
8601
|
-
entries = await
|
|
8845
|
+
entries = await fs17.readdir(await storePath(rootDir, DIRECTORY));
|
|
8602
8846
|
} catch (error) {
|
|
8603
8847
|
if (error.code === "ENOENT") return [];
|
|
8604
8848
|
throw error;
|
|
@@ -8621,11 +8865,11 @@ async function loadScope(rootDir) {
|
|
|
8621
8865
|
}
|
|
8622
8866
|
async function clearScope(rootDir) {
|
|
8623
8867
|
await withSnapshotWrite(rootDir, async () => {
|
|
8624
|
-
await
|
|
8868
|
+
await fs17.rm(await storePath(rootDir, `${DIRECTORY}/scope.json`), { force: true });
|
|
8625
8869
|
});
|
|
8626
8870
|
}
|
|
8627
8871
|
async function clearAllPartials(rootDir) {
|
|
8628
|
-
await
|
|
8872
|
+
await fs17.rm(await storePath(rootDir, DIRECTORY), { recursive: true, force: true });
|
|
8629
8873
|
}
|
|
8630
8874
|
function batchIdFor(offset) {
|
|
8631
8875
|
return `batch-${String(offset).padStart(6, "0")}`;
|
|
@@ -8642,7 +8886,7 @@ async function buildContext(dir) {
|
|
|
8642
8886
|
};
|
|
8643
8887
|
}
|
|
8644
8888
|
async function analyzeProject(dir) {
|
|
8645
|
-
const rootDir =
|
|
8889
|
+
const rootDir = path36.resolve(dir);
|
|
8646
8890
|
const context = await buildContext(rootDir);
|
|
8647
8891
|
const results = await runAll(context);
|
|
8648
8892
|
const projectSnapshot = await detectProjectSnapshot(rootDir);
|
|
@@ -8697,7 +8941,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8697
8941
|
const present = [];
|
|
8698
8942
|
for (const file of buildFiles) {
|
|
8699
8943
|
try {
|
|
8700
|
-
await
|
|
8944
|
+
await fs26.access(path36.join(rootDir, file));
|
|
8701
8945
|
present.push(file);
|
|
8702
8946
|
} catch {
|
|
8703
8947
|
}
|
|
@@ -8739,7 +8983,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8739
8983
|
const sourceFiles = await access.list();
|
|
8740
8984
|
const fileCounts = {};
|
|
8741
8985
|
for (const file of sourceFiles) {
|
|
8742
|
-
const ext =
|
|
8986
|
+
const ext = path36.extname(file).slice(1);
|
|
8743
8987
|
fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
|
|
8744
8988
|
}
|
|
8745
8989
|
return {
|
|
@@ -8750,7 +8994,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8750
8994
|
};
|
|
8751
8995
|
}
|
|
8752
8996
|
async function getCodeSamples(dir, count4 = 15) {
|
|
8753
|
-
const rootDir =
|
|
8997
|
+
const rootDir = path36.resolve(dir);
|
|
8754
8998
|
const samples = await sampleFiles(rootDir, count4);
|
|
8755
8999
|
const output = {
|
|
8756
9000
|
note: "These are previews (first ~60 lines). Read the file directly with your own tools to see it in full.",
|
|
@@ -8793,7 +9037,7 @@ async function unmappedContextResponse(rootDir) {
|
|
|
8793
9037
|
});
|
|
8794
9038
|
}
|
|
8795
9039
|
async function getProjectStructure(dir) {
|
|
8796
|
-
const rootDir =
|
|
9040
|
+
const rootDir = path36.resolve(dir);
|
|
8797
9041
|
const allFiles = await (await createFileAccess(rootDir)).list("**/*");
|
|
8798
9042
|
const dirInfo = /* @__PURE__ */ new Map();
|
|
8799
9043
|
for (const file of allFiles) {
|
|
@@ -8805,7 +9049,7 @@ async function getProjectStructure(dir) {
|
|
|
8805
9049
|
}
|
|
8806
9050
|
const info = dirInfo.get(dirPath);
|
|
8807
9051
|
info.fileCount++;
|
|
8808
|
-
const ext =
|
|
9052
|
+
const ext = path36.extname(file).slice(1);
|
|
8809
9053
|
if (ext) {
|
|
8810
9054
|
info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
|
|
8811
9055
|
}
|
|
@@ -8850,7 +9094,7 @@ async function buildChangedFilePreviews(rootDir, changedFiles) {
|
|
|
8850
9094
|
return previews2;
|
|
8851
9095
|
}
|
|
8852
9096
|
async function getSnapshot(dir) {
|
|
8853
|
-
const rootDir =
|
|
9097
|
+
const rootDir = path36.resolve(dir);
|
|
8854
9098
|
const snapshot = await loadSnapshot(rootDir);
|
|
8855
9099
|
if (!snapshot) {
|
|
8856
9100
|
return unmappedContextResponse(rootDir);
|
|
@@ -8951,7 +9195,7 @@ function driftHint(report) {
|
|
|
8951
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.";
|
|
8952
9196
|
}
|
|
8953
9197
|
async function checkDrift(dir) {
|
|
8954
|
-
const rootDir =
|
|
9198
|
+
const rootDir = path36.resolve(dir);
|
|
8955
9199
|
const report = await computeDrift(rootDir);
|
|
8956
9200
|
if (!report) {
|
|
8957
9201
|
return JSON.stringify({
|
|
@@ -8982,7 +9226,7 @@ async function checkDrift(dir) {
|
|
|
8982
9226
|
return JSON.stringify({ exists: true, ...report, verification, hint });
|
|
8983
9227
|
}
|
|
8984
9228
|
async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_SIZE, files) {
|
|
8985
|
-
const rootDir =
|
|
9229
|
+
const rootDir = path36.resolve(dir);
|
|
8986
9230
|
const scoped = files !== void 0 && files.length > 0;
|
|
8987
9231
|
const scopeFiles = scoped ? sanitizePaths(rootDir, files) : void 0;
|
|
8988
9232
|
const batch = await prepareSnapshotBatch(dir, offset, batchSize, scopeFiles);
|
|
@@ -9029,7 +9273,7 @@ async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_
|
|
|
9029
9273
|
);
|
|
9030
9274
|
}
|
|
9031
9275
|
async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
|
|
9032
|
-
const rootDir =
|
|
9276
|
+
const rootDir = path36.resolve(dir);
|
|
9033
9277
|
for (const feat of Object.values(features)) {
|
|
9034
9278
|
feat.files = sanitizePaths(rootDir, feat.files);
|
|
9035
9279
|
if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
|
|
@@ -9058,7 +9302,7 @@ async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
|
|
|
9058
9302
|
);
|
|
9059
9303
|
}
|
|
9060
9304
|
async function reduceSnapshot(dir) {
|
|
9061
|
-
const rootDir =
|
|
9305
|
+
const rootDir = path36.resolve(dir);
|
|
9062
9306
|
const partials = await loadAllPartials(rootDir);
|
|
9063
9307
|
if (partials.length === 0) {
|
|
9064
9308
|
return JSON.stringify(
|
|
@@ -9120,7 +9364,7 @@ async function reduceSnapshot(dir) {
|
|
|
9120
9364
|
);
|
|
9121
9365
|
}
|
|
9122
9366
|
async function fullAnalysis(dir) {
|
|
9123
|
-
const rootDir =
|
|
9367
|
+
const rootDir = path36.resolve(dir);
|
|
9124
9368
|
const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
|
|
9125
9369
|
analyzeProject(dir),
|
|
9126
9370
|
getProjectStructure(dir),
|
|
@@ -9149,7 +9393,7 @@ function sanitizePaths(rootDir, files) {
|
|
|
9149
9393
|
return sanitizeRepoPaths(files);
|
|
9150
9394
|
}
|
|
9151
9395
|
async function saveSnapshotData(dir, features, flows, removeFeatures = [], removeFlows = []) {
|
|
9152
|
-
const rootDir =
|
|
9396
|
+
const rootDir = path36.resolve(dir);
|
|
9153
9397
|
return updateSnapshot(rootDir, async (previous) => {
|
|
9154
9398
|
const gitHash = await getCurrentGitHash(rootDir);
|
|
9155
9399
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9229,14 +9473,14 @@ async function saveSnapshotData(dir, features, flows, removeFeatures = [], remov
|
|
|
9229
9473
|
});
|
|
9230
9474
|
}
|
|
9231
9475
|
async function getImpact(dir, files) {
|
|
9232
|
-
const rootDir =
|
|
9476
|
+
const rootDir = path36.resolve(dir);
|
|
9233
9477
|
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
9234
9478
|
const result = await analyzeImpact2(rootDir, files);
|
|
9235
9479
|
return JSON.stringify(result, null, 2);
|
|
9236
9480
|
}
|
|
9237
9481
|
var VERIFY_DEFAULT_SAMPLE = 5;
|
|
9238
9482
|
async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
9239
|
-
const rootDir =
|
|
9483
|
+
const rootDir = path36.resolve(dir);
|
|
9240
9484
|
const snapshot = await loadSnapshot(rootDir);
|
|
9241
9485
|
if (!snapshot) {
|
|
9242
9486
|
return JSON.stringify({
|
|
@@ -9289,7 +9533,7 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
|
9289
9533
|
});
|
|
9290
9534
|
}
|
|
9291
9535
|
async function saveVerification(dir, verdicts) {
|
|
9292
|
-
const rootDir =
|
|
9536
|
+
const rootDir = path36.resolve(dir);
|
|
9293
9537
|
return updateSnapshot(rootDir, async (snapshot) => {
|
|
9294
9538
|
if (!snapshot) {
|
|
9295
9539
|
return { snapshot: null, result: JSON.stringify({ exists: false, stamped: [], hint: "No concept map exists. Build it and run verify_snapshot before recording verdicts." }) };
|
|
@@ -9363,16 +9607,16 @@ async function saveVerification(dir, verdicts) {
|
|
|
9363
9607
|
});
|
|
9364
9608
|
}
|
|
9365
9609
|
async function saveDecision(dir, input) {
|
|
9366
|
-
const rootDir =
|
|
9610
|
+
const rootDir = path36.resolve(dir);
|
|
9367
9611
|
const { upsertDecision: upsertDecision2 } = await Promise.resolve().then(() => (init_decisions(), decisions_exports));
|
|
9368
9612
|
const result = await upsertDecision2(rootDir, input);
|
|
9369
9613
|
return JSON.stringify(result);
|
|
9370
9614
|
}
|
|
9371
9615
|
async function reviewDecision2(dir, input) {
|
|
9372
|
-
return JSON.stringify(await reviewDecision(
|
|
9616
|
+
return JSON.stringify(await reviewDecision(path36.resolve(dir), input));
|
|
9373
9617
|
}
|
|
9374
9618
|
async function getContext(dir, task, files) {
|
|
9375
|
-
const rootDir =
|
|
9619
|
+
const rootDir = path36.resolve(dir);
|
|
9376
9620
|
const { assembleContext: assembleContext2 } = await Promise.resolve().then(() => (init_assemble(), assemble_exports));
|
|
9377
9621
|
const bundle = await assembleContext2(rootDir, task, files);
|
|
9378
9622
|
return JSON.stringify(bundle);
|
|
@@ -9421,7 +9665,7 @@ async function masonInit(dir, options = {}) {
|
|
|
9421
9665
|
return JSON.stringify(await setupProject2(dir, options), null, 2);
|
|
9422
9666
|
}
|
|
9423
9667
|
if (options.host) throw new Error("host applies only to mode: setup.");
|
|
9424
|
-
const rootDir =
|
|
9668
|
+
const rootDir = path36.resolve(dir);
|
|
9425
9669
|
const marker = await loadProjectMarker(rootDir);
|
|
9426
9670
|
const { loadSetup: loadSetup2 } = await Promise.resolve().then(() => (init_model(), model_exports));
|
|
9427
9671
|
const setup = await loadSetup2(rootDir);
|
|
@@ -9441,7 +9685,7 @@ async function masonInit(dir, options = {}) {
|
|
|
9441
9685
|
);
|
|
9442
9686
|
}
|
|
9443
9687
|
async function masonCompleteInit(dir, options = {}) {
|
|
9444
|
-
const rootDir =
|
|
9688
|
+
const rootDir = path36.resolve(dir);
|
|
9445
9689
|
const existing = await loadProjectMarker(rootDir);
|
|
9446
9690
|
const features = await loadProjectFeatures(rootDir);
|
|
9447
9691
|
const marker = {
|
|
@@ -9550,7 +9794,7 @@ async function masonSetConfluence(input) {
|
|
|
9550
9794
|
);
|
|
9551
9795
|
}
|
|
9552
9796
|
async function exportToConfluenceTool(dir, overrides) {
|
|
9553
|
-
const rootDir =
|
|
9797
|
+
const rootDir = path36.resolve(dir);
|
|
9554
9798
|
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
|
|
9555
9799
|
const { exportToConfluence: exportToConfluence2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
9556
9800
|
const config = await loadConfig2();
|
|
@@ -9589,7 +9833,7 @@ function createMcpServer() {
|
|
|
9589
9833
|
const server = new McpServer(
|
|
9590
9834
|
{
|
|
9591
9835
|
name: "mason",
|
|
9592
|
-
version: "0.
|
|
9836
|
+
version: "0.19.1"
|
|
9593
9837
|
},
|
|
9594
9838
|
{
|
|
9595
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.'
|
|
@@ -9635,7 +9879,7 @@ function createMcpServer() {
|
|
|
9635
9879
|
);
|
|
9636
9880
|
server.tool(
|
|
9637
9881
|
"review_advisory",
|
|
9638
|
-
"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.",
|
|
9639
9883
|
{ dir: z24.string().describe("Absolute path to the project root"), ...advisoryReviewRequest.shape },
|
|
9640
9884
|
async ({ dir, ...request }) => ({ content: [{ type: "text", text: await masonReviewAdvisory(dir, request) }] })
|
|
9641
9885
|
);
|
|
@@ -9970,14 +10214,592 @@ function createMcpServer() {
|
|
|
9970
10214
|
);
|
|
9971
10215
|
return server;
|
|
9972
10216
|
}
|
|
9973
|
-
async function startMcpServer() {
|
|
10217
|
+
async function startMcpServer(onClose) {
|
|
9974
10218
|
const server = createMcpServer();
|
|
10219
|
+
if (onClose) server.server.onclose = onClose;
|
|
9975
10220
|
const transport = new StdioServerTransport();
|
|
9976
10221
|
await server.connect(transport);
|
|
9977
10222
|
}
|
|
9978
10223
|
|
|
10224
|
+
// src/distribution/updates.ts
|
|
10225
|
+
init_store();
|
|
10226
|
+
init_storage();
|
|
10227
|
+
import fs33 from "fs/promises";
|
|
10228
|
+
import path42 from "path";
|
|
10229
|
+
import { spawn as spawn3 } from "child_process";
|
|
10230
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
10231
|
+
import { z as z30 } from "zod";
|
|
10232
|
+
|
|
10233
|
+
// src/distribution/state.ts
|
|
10234
|
+
init_storage();
|
|
10235
|
+
import os5 from "os";
|
|
10236
|
+
import path38 from "path";
|
|
10237
|
+
import { fileURLToPath } from "url";
|
|
10238
|
+
import fs28 from "fs/promises";
|
|
10239
|
+
import { z as z26 } from "zod";
|
|
10240
|
+
|
|
10241
|
+
// src/distribution/path.ts
|
|
10242
|
+
import fs27 from "fs/promises";
|
|
10243
|
+
import os4 from "os";
|
|
10244
|
+
import path37 from "path";
|
|
10245
|
+
import { execFile as execFile12 } from "child_process";
|
|
10246
|
+
import { promisify as promisify12 } from "util";
|
|
10247
|
+
import { createHash as createHash7, randomUUID as randomUUID7 } from "crypto";
|
|
10248
|
+
import { z as z25 } from "zod";
|
|
10249
|
+
var execute2 = promisify12(execFile12);
|
|
10250
|
+
var pathChangeSchema = z25.discriminatedUnion("kind", [
|
|
10251
|
+
z25.object({ kind: z25.literal("profile"), file: z25.string(), block: z25.string(), created: z25.boolean() }),
|
|
10252
|
+
z25.object({ kind: z25.literal("windows"), entry: z25.string(), created: z25.boolean() })
|
|
10253
|
+
]);
|
|
10254
|
+
var quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
10255
|
+
async function configurePath(bin, previous, save, options = {}) {
|
|
10256
|
+
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
10257
|
+
const changes = [...previous];
|
|
10258
|
+
const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
|
|
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));
|
|
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";
|
|
10261
|
+
const message2 = (alreadyOnPath ? "" : "Open a new terminal to use mason.\n") + setupHint;
|
|
10262
|
+
const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
|
|
10263
|
+
${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
10264
|
+
`}${setupHint}` });
|
|
10265
|
+
if (env.MASON_NO_MODIFY_PATH === "1") return manual("Automatic PATH setup is disabled (MASON_NO_MODIFY_PATH=1).");
|
|
10266
|
+
if (/[\r\n\0]/.test(bin) || bin.includes(platform === "win32" ? ";" : ":")) return manual("This installation path cannot be represented safely in PATH.");
|
|
10267
|
+
try {
|
|
10268
|
+
if (platform === "win32") {
|
|
10269
|
+
const current = await windowsPath("read");
|
|
10270
|
+
if (!hasWindowsEntry(current.value, bin)) {
|
|
10271
|
+
if (!changes.some((c) => c.kind === "windows" && c.entry === bin)) {
|
|
10272
|
+
changes.push({ kind: "windows", entry: bin, created: current.value === null });
|
|
10273
|
+
await save(changes);
|
|
10274
|
+
}
|
|
10275
|
+
await windowsPath("add", bin, current);
|
|
10276
|
+
}
|
|
10277
|
+
return { status: "configured", changes, message: message2 };
|
|
10278
|
+
}
|
|
10279
|
+
const shell = path37.basename(env.SHELL ?? os4.userInfo().shell ?? "");
|
|
10280
|
+
const userHome = options.userHome ?? os4.homedir();
|
|
10281
|
+
let profiles2;
|
|
10282
|
+
if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
|
|
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")];
|
|
10286
|
+
else {
|
|
10287
|
+
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path37.join(userHome, name));
|
|
10288
|
+
let login = candidates[2];
|
|
10289
|
+
for (const file of candidates) if (await fs27.lstat(file).then(() => true, (error) => {
|
|
10290
|
+
if (error.code === "ENOENT") return false;
|
|
10291
|
+
throw error;
|
|
10292
|
+
})) {
|
|
10293
|
+
login = file;
|
|
10294
|
+
break;
|
|
10295
|
+
}
|
|
10296
|
+
profiles2 = [path37.join(userHome, ".bashrc"), login];
|
|
10297
|
+
}
|
|
10298
|
+
const marker = `# >>> mason PATH ${createHash7("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
10299
|
+
for (const profile of profiles2) {
|
|
10300
|
+
const stat = await fs27.lstat(profile).catch((error) => {
|
|
10301
|
+
if (error.code === "ENOENT") return null;
|
|
10302
|
+
throw error;
|
|
10303
|
+
});
|
|
10304
|
+
const file = stat?.isSymbolicLink() ? await fs27.realpath(profile) : path37.resolve(profile);
|
|
10305
|
+
const original = await fs27.readFile(file).catch((error) => {
|
|
10306
|
+
if (error.code === "ENOENT" && !stat) return null;
|
|
10307
|
+
throw error;
|
|
10308
|
+
});
|
|
10309
|
+
if (original && !Buffer.from(original.toString("utf8")).equals(original)) return manual(`Shell profile is not UTF-8: ${profile}.`);
|
|
10310
|
+
const text2 = original?.toString("utf8") ?? "";
|
|
10311
|
+
const owned = changes.find((c) => c.kind === "profile" && c.file === file);
|
|
10312
|
+
if (owned && text2.includes(owned.block)) continue;
|
|
10313
|
+
if (text2.includes(marker) || owned && text2.includes("# <<< mason PATH")) return manual(`Mason's PATH block was edited; retained ${profile}.`);
|
|
10314
|
+
const newline = text2.includes("\r\n") ? "\r\n" : "\n";
|
|
10315
|
+
const block = owned?.block ?? ("\n" + marker + '\ncase ":${PATH-}:" in\n *:' + quote(bin) + ":*) ;;\n *) export PATH=" + quote(bin) + ':"${PATH-}" ;;\nesac\n# <<< mason PATH <<<\n').replaceAll("\n", newline);
|
|
10316
|
+
if (!owned) {
|
|
10317
|
+
changes.push({ kind: "profile", file, block, created: original === null });
|
|
10318
|
+
await save(changes);
|
|
10319
|
+
}
|
|
10320
|
+
await fs27.mkdir(path37.dirname(file), { recursive: true });
|
|
10321
|
+
const handle = await fs27.open(file, original === null ? "ax+" : "a+", 384);
|
|
10322
|
+
try {
|
|
10323
|
+
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
10324
|
+
await handle.writeFile(block);
|
|
10325
|
+
await handle.sync();
|
|
10326
|
+
} finally {
|
|
10327
|
+
await handle.close();
|
|
10328
|
+
}
|
|
10329
|
+
}
|
|
10330
|
+
return { status: "configured", changes, message: message2 };
|
|
10331
|
+
} catch (error) {
|
|
10332
|
+
return manual(`Could not configure PATH: ${error instanceof Error ? error.message : String(error)}`);
|
|
10333
|
+
}
|
|
10334
|
+
}
|
|
10335
|
+
var normalizeWindowsEntry = (value) => path37.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
10336
|
+
var hasWindowsEntry = (value, entry, env = process.env) => {
|
|
10337
|
+
const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
|
|
10338
|
+
const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
|
|
10339
|
+
return (value ?? "").split(";").some((part) => normalizeWindowsEntry(expand(part)) === normalizeWindowsEntry(entry));
|
|
10340
|
+
};
|
|
10341
|
+
async function windowsPath(action, entry = "", expected, removeEmpty = false) {
|
|
10342
|
+
const payload = Buffer.from(JSON.stringify({ action, entry, expected, removeEmpty })).toString("base64");
|
|
10343
|
+
const script = `$ErrorActionPreference = 'Stop'
|
|
10344
|
+
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
|
|
10345
|
+
$p = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${payload}')) | ConvertFrom-Json
|
|
10346
|
+
$key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
|
|
10347
|
+
try {
|
|
10348
|
+
$value = $key.GetValue('Path', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
|
10349
|
+
$kind = if ($null -eq $value) { $null } else { $key.GetValueKind('Path').ToString() }
|
|
10350
|
+
if ($null -ne $kind -and $kind -notin @('String','ExpandString')) { throw 'User PATH is not a string.' }
|
|
10351
|
+
if ($p.action -eq 'read') { @{ value = $value; kind = $kind } | ConvertTo-Json -Compress; exit 0 }
|
|
10352
|
+
if ($p.expected -and ($value -cne $p.expected.value -or $kind -cne $p.expected.kind)) { throw 'User PATH changed concurrently; retry the operation.' }
|
|
10353
|
+
$parts = if ($null -eq $value -or $value -eq '') { @() } else { @($value.Split(';')) }
|
|
10354
|
+
if ($p.action -eq 'add') { $next = (@($p.entry) + $parts) -join ';' }
|
|
10355
|
+
else { $next = (@($parts | Where-Object { $_ -cne $p.entry })) -join ';' }
|
|
10356
|
+
if ($next -cne $value) {
|
|
10357
|
+
$writeKind = if ($kind) { [Microsoft.Win32.RegistryValueKind]$kind } else { [Microsoft.Win32.RegistryValueKind]::ExpandString }
|
|
10358
|
+
if ($p.action -eq 'remove' -and $p.removeEmpty -and $next -eq '') { $key.DeleteValue('Path', $false) }
|
|
10359
|
+
else { $key.SetValue('Path', $next, $writeKind) }
|
|
10360
|
+
Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public class MasonEnvironment { [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern IntPtr SendMessageTimeout(IntPtr h, uint m, UIntPtr w, string l, uint f, uint t, out UIntPtr r); }'
|
|
10361
|
+
$result = [UIntPtr]::Zero
|
|
10362
|
+
[void][MasonEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 1000, [ref]$result)
|
|
10363
|
+
}
|
|
10364
|
+
@{ value = $next; kind = $kind } | ConvertTo-Json -Compress
|
|
10365
|
+
} finally { $key.Dispose() }
|
|
10366
|
+
`;
|
|
10367
|
+
const { stdout } = await execute2("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024 });
|
|
10368
|
+
return JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
|
|
10369
|
+
}
|
|
10370
|
+
|
|
10371
|
+
// src/distribution/state.ts
|
|
10372
|
+
var releaseVersion = z26.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/);
|
|
10373
|
+
var bundleId = z26.string().regex(/^[a-f0-9]{24}$/);
|
|
10374
|
+
var updatePolicySchema = z26.object({
|
|
10375
|
+
enabled: z26.boolean(),
|
|
10376
|
+
revision: z26.string(),
|
|
10377
|
+
pinnedVersion: releaseVersion.optional(),
|
|
10378
|
+
mirror: z26.string().optional()
|
|
10379
|
+
});
|
|
10380
|
+
var recordSchema3 = z26.object({
|
|
10381
|
+
format: z26.literal(1),
|
|
10382
|
+
home: z26.string(),
|
|
10383
|
+
bin: z26.string(),
|
|
10384
|
+
current: bundleId,
|
|
10385
|
+
version: releaseVersion,
|
|
10386
|
+
launchers: z26.record(z26.string()),
|
|
10387
|
+
previousLaunchers: z26.record(z26.string()).optional(),
|
|
10388
|
+
pathChanges: z26.array(pathChangeSchema).optional(),
|
|
10389
|
+
updates: updatePolicySchema.optional(),
|
|
10390
|
+
previous: z26.object({ id: bundleId, version: releaseVersion }).optional(),
|
|
10391
|
+
pending: z26.object({ id: bundleId, version: releaseVersion, policyRevision: z26.string() }).optional()
|
|
10392
|
+
});
|
|
10393
|
+
async function readInstallation(home) {
|
|
10394
|
+
const raw = await readStoreJson(home, "install.json");
|
|
10395
|
+
if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
10396
|
+
const record = recordSchema3.parse(raw);
|
|
10397
|
+
if (record.home !== await fs28.realpath(home)) throw new Error("Invalid standalone installation receipt.");
|
|
10398
|
+
return record;
|
|
10399
|
+
}
|
|
10400
|
+
function standaloneLocation() {
|
|
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))) {
|
|
10404
|
+
return null;
|
|
10405
|
+
}
|
|
10406
|
+
return { home, bundle };
|
|
10407
|
+
}
|
|
10408
|
+
async function currentInstallation() {
|
|
10409
|
+
const location2 = standaloneLocation();
|
|
10410
|
+
if (!location2) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
10411
|
+
const { home, bundle } = location2;
|
|
10412
|
+
const record = await readInstallation(home);
|
|
10413
|
+
return { home, bundle, record };
|
|
10414
|
+
}
|
|
10415
|
+
function automaticUpdatesAllowed(record, env = process.env) {
|
|
10416
|
+
return record.updates?.enabled === true && !record.updates.pinnedVersion && !record.updates.mirror && !env.CI && env.MASON_NO_AUTO_UPDATE !== "1" && !env.MASON_RELEASE_BASE && !env.MASON_VERSION;
|
|
10417
|
+
}
|
|
10418
|
+
function newerStableVersion(candidate, installed) {
|
|
10419
|
+
if (!/^\d+\.\d+\.\d+$/.test(candidate)) return false;
|
|
10420
|
+
const a = candidate.split(".").map(BigInt), b = installed.split("-")[0].split(".").map(BigInt);
|
|
10421
|
+
for (let i = 0; i < 3; i++) {
|
|
10422
|
+
if (a[i] !== b[i]) return a[i] > b[i];
|
|
10423
|
+
}
|
|
10424
|
+
return installed.includes("-");
|
|
10425
|
+
}
|
|
10426
|
+
|
|
10427
|
+
// src/distribution/bundle.ts
|
|
10428
|
+
init_storage();
|
|
10429
|
+
import fs29 from "fs/promises";
|
|
10430
|
+
import path39 from "path";
|
|
10431
|
+
import { createHash as createHash8 } from "crypto";
|
|
10432
|
+
import { z as z27 } from "zod";
|
|
10433
|
+
var targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
|
10434
|
+
var digest2 = z27.string().regex(/^[a-f0-9]{64}$/);
|
|
10435
|
+
var bundleSchema = z27.object({
|
|
10436
|
+
format: z27.literal(1),
|
|
10437
|
+
version: z27.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
10438
|
+
target: z27.enum(targets),
|
|
10439
|
+
nodeVersion: z27.string(),
|
|
10440
|
+
files: z27.record(digest2)
|
|
10441
|
+
}).strict();
|
|
10442
|
+
var sha256 = (bytes) => createHash8("sha256").update(bytes).digest("hex");
|
|
10443
|
+
async function readBundle(root) {
|
|
10444
|
+
const bytes = await fs29.readFile(await storePath(root, "bundle.json"));
|
|
10445
|
+
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
10446
|
+
const windows = manifest.target.startsWith("win32-");
|
|
10447
|
+
for (const file of Object.keys(manifest.files)) {
|
|
10448
|
+
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path39.isAbsolute(file)) {
|
|
10449
|
+
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
10450
|
+
}
|
|
10451
|
+
}
|
|
10452
|
+
for (const required of [windows ? "node.exe" : "node", "app/package.json", "app/dist/mason.js", "app/dist/mason-auto.js", "app/dist/mason-mcp.js"]) {
|
|
10453
|
+
if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
|
|
10454
|
+
}
|
|
10455
|
+
return { manifest, manifestHash: sha256(bytes) };
|
|
10456
|
+
}
|
|
10457
|
+
async function verifyBundle(root, expectedHash) {
|
|
10458
|
+
const bundle = await readBundle(root);
|
|
10459
|
+
if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
|
|
10460
|
+
if (bundle.manifest.target !== `${process.platform}-${process.arch}`) throw new Error("Mason bundle is for another platform. Install this platform's bundle and rerun setup.");
|
|
10461
|
+
const files = Object.keys(bundle.manifest.files);
|
|
10462
|
+
for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
|
|
10463
|
+
if (sha256(await fs29.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
|
|
10464
|
+
}));
|
|
10465
|
+
return bundle;
|
|
10466
|
+
}
|
|
10467
|
+
|
|
10468
|
+
// src/distribution/runtime.ts
|
|
10469
|
+
init_store();
|
|
10470
|
+
init_storage();
|
|
10471
|
+
import fs30 from "fs/promises";
|
|
10472
|
+
import { unlinkSync } from "fs";
|
|
10473
|
+
import os6 from "os";
|
|
10474
|
+
import path40 from "path";
|
|
10475
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
10476
|
+
import { z as z28 } from "zod";
|
|
10477
|
+
var leaseSchema = z28.object({ pid: z28.number().int().positive(), host: z28.string(), bundle: z28.string().regex(/^[a-f0-9]{24}$/) });
|
|
10478
|
+
|
|
10479
|
+
// src/distribution/install.ts
|
|
10480
|
+
import fs32 from "fs/promises";
|
|
10481
|
+
import os8 from "os";
|
|
10482
|
+
import path41 from "path";
|
|
10483
|
+
import { execFile as execFile13, spawn as spawn2 } from "child_process";
|
|
10484
|
+
import { promisify as promisify13 } from "util";
|
|
10485
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
10486
|
+
init_storage();
|
|
10487
|
+
init_store();
|
|
10488
|
+
|
|
10489
|
+
// src/distribution/sessions.ts
|
|
10490
|
+
init_storage();
|
|
10491
|
+
import fs31 from "fs/promises";
|
|
10492
|
+
import os7 from "os";
|
|
10493
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
10494
|
+
import { z as z29 } from "zod";
|
|
10495
|
+
var leaseSchema2 = z29.object({ pid: z29.number().int().positive(), host: z29.string() });
|
|
10496
|
+
async function hasRunningMcp(home) {
|
|
10497
|
+
const directory = await storePath(home, "sessions");
|
|
10498
|
+
const files = await fs31.readdir(directory).catch((error) => {
|
|
10499
|
+
if (error.code === "ENOENT") return [];
|
|
10500
|
+
throw error;
|
|
10501
|
+
});
|
|
10502
|
+
for (const name of files) {
|
|
10503
|
+
if (!/^[a-f0-9-]+\.json$/.test(name)) return true;
|
|
10504
|
+
const relative = "sessions/" + name;
|
|
10505
|
+
try {
|
|
10506
|
+
const raw = await readStoreJson(home, relative);
|
|
10507
|
+
if (raw === null) continue;
|
|
10508
|
+
const lease = leaseSchema2.parse(raw);
|
|
10509
|
+
if (lease.host !== os7.hostname()) return true;
|
|
10510
|
+
try {
|
|
10511
|
+
process.kill(lease.pid, 0);
|
|
10512
|
+
return true;
|
|
10513
|
+
} catch (error) {
|
|
10514
|
+
if (error.code !== "ESRCH") return true;
|
|
10515
|
+
}
|
|
10516
|
+
await fs31.rm(await storePath(home, relative), { force: true });
|
|
10517
|
+
} catch {
|
|
10518
|
+
return true;
|
|
10519
|
+
}
|
|
10520
|
+
}
|
|
10521
|
+
return false;
|
|
10522
|
+
}
|
|
10523
|
+
async function registerMcp(home) {
|
|
10524
|
+
const name = `sessions/${randomUUID9()}.json`;
|
|
10525
|
+
await writeStoreJson(home, name, { pid: process.pid, host: os7.hostname() });
|
|
10526
|
+
return async () => {
|
|
10527
|
+
await fs31.rm(await storePath(home, name), { force: true }).catch(() => {
|
|
10528
|
+
});
|
|
10529
|
+
};
|
|
10530
|
+
}
|
|
10531
|
+
|
|
10532
|
+
// src/distribution/install.ts
|
|
10533
|
+
var shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
10534
|
+
var psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
10535
|
+
var WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason-launcher.ps1" %*\r\n';
|
|
10536
|
+
var launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
|
|
10537
|
+
async function replace(file, content2, mode = 384) {
|
|
10538
|
+
const temp = file + ".tmp-" + randomUUID10();
|
|
10539
|
+
try {
|
|
10540
|
+
await fs32.writeFile(temp, content2, { mode, flag: "wx" });
|
|
10541
|
+
await fs32.rename(temp, file);
|
|
10542
|
+
} finally {
|
|
10543
|
+
await fs32.rm(temp, { force: true });
|
|
10544
|
+
}
|
|
10545
|
+
}
|
|
10546
|
+
function assertUpdatePolicy(record, version, revision) {
|
|
10547
|
+
if (!record || !automaticUpdatesAllowed(record, { ...process.env, MASON_VERSION: "" }) || record.updates?.revision !== revision || !newerStableVersion(version, record.version)) {
|
|
10548
|
+
throw new Error("Update policy or installed version changed; download was not activated.");
|
|
10549
|
+
}
|
|
10550
|
+
}
|
|
10551
|
+
async function installLocked2(home, source, bundle, progress, options, alreadyStaged = false) {
|
|
10552
|
+
const raw = await readStoreJson(home, "install.json");
|
|
10553
|
+
const previous = raw === null ? null : recordSchema3.parse(raw);
|
|
10554
|
+
if (options.stageOnly) assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
|
|
10555
|
+
if (!previous) for (const file of await fs32.readdir(home)) {
|
|
10556
|
+
if (file === ".install-lock") continue;
|
|
10557
|
+
const directory = path41.join(home, file);
|
|
10558
|
+
const stat = await fs32.lstat(directory);
|
|
10559
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs32.readdir(directory)).length) {
|
|
10560
|
+
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
10561
|
+
}
|
|
10562
|
+
}
|
|
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")));
|
|
10564
|
+
await fs32.mkdir(selectedBin, { recursive: true });
|
|
10565
|
+
const bin = await fs32.realpath(selectedBin);
|
|
10566
|
+
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
10567
|
+
const id = bundle.manifestHash.slice(0, 24);
|
|
10568
|
+
const destination = await storePath(home, "versions/" + id);
|
|
10569
|
+
if (options.stageOnly) {
|
|
10570
|
+
const receipt2 = { ...previous, pending: { id, version: bundle.manifest.version, policyRevision: options.policyRevision } };
|
|
10571
|
+
await writeStoreJson(home, "install.json", receipt2);
|
|
10572
|
+
return { home, bin, version: bundle.manifest.version, previousVersion: previous.version, path: { message: "Update staged for a future MCP launch." } };
|
|
10573
|
+
}
|
|
10574
|
+
const launchers = process.platform === "win32" ? {
|
|
10575
|
+
"mason.cmd": WINDOWS_BATCH_LAUNCHER,
|
|
10576
|
+
"mason-launcher.ps1": `$ErrorActionPreference = 'Stop'
|
|
10577
|
+
$previousToken = $env:MASON_UNINSTALL_TOKEN
|
|
10578
|
+
$token = [Guid]::NewGuid().ToString()
|
|
10579
|
+
$env:MASON_UNINSTALL_TOKEN = $token
|
|
10580
|
+
try {
|
|
10581
|
+
& ${psQuote(path41.join(destination, "node.exe"))} ${psQuote(path41.join(destination, "app/dist/mason.js"))} @args
|
|
10582
|
+
$code = $LASTEXITCODE
|
|
10583
|
+
if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
|
|
10584
|
+
if ([IO.File]::ReadAllText(${psQuote(path41.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
|
|
10585
|
+
Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
|
|
10586
|
+
Write-Output ${psQuote("Removed standalone installation: " + home)}
|
|
10587
|
+
}
|
|
10588
|
+
exit $code
|
|
10589
|
+
} catch { [Console]::Error.WriteLine($_.Exception.Message); exit 2 }
|
|
10590
|
+
finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
|
|
10591
|
+
`
|
|
10592
|
+
} : { mason: `#!/bin/sh
|
|
10593
|
+
exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destination, "app/dist/mason.js"))} "$@"
|
|
10594
|
+
` };
|
|
10595
|
+
for (const [name, content2] of Object.entries(launchers)) {
|
|
10596
|
+
const file = await storePath(bin, name);
|
|
10597
|
+
const old = await fs32.readFile(file, "utf8").catch((error) => {
|
|
10598
|
+
if (error.code === "ENOENT") return null;
|
|
10599
|
+
throw error;
|
|
10600
|
+
});
|
|
10601
|
+
if (old !== null && old !== previous?.launchers[name] && old !== previous?.previousLaunchers?.[name] && old !== content2) throw new Error("Refusing to replace an unrelated or edited launcher: " + file);
|
|
10602
|
+
}
|
|
10603
|
+
const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
|
|
10604
|
+
const checkRetired = async (name, expected) => {
|
|
10605
|
+
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
10606
|
+
const actual = await fs32.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
10607
|
+
if (error.code === "ENOENT") return null;
|
|
10608
|
+
throw error;
|
|
10609
|
+
});
|
|
10610
|
+
if (actual !== null && actual !== expected) throw new Error("Retired launcher was edited; retained: " + name);
|
|
10611
|
+
};
|
|
10612
|
+
for (const [name, expected] of retired) await checkRetired(name, expected);
|
|
10613
|
+
progress?.step(`Installing Mason ${bundle.manifest.version}`);
|
|
10614
|
+
const updates = options.preservePolicy && previous?.updates ? { ...previous.updates } : {
|
|
10615
|
+
...previous?.updates ?? { enabled: !previous && !process.env.CI && process.env.MASON_NO_AUTO_UPDATE !== "1" },
|
|
10616
|
+
revision: randomUUID10(),
|
|
10617
|
+
pinnedVersion: process.env.MASON_VERSION?.replace(/^v/, "") || void 0,
|
|
10618
|
+
mirror: process.env.MASON_RELEASE_BASE || previous?.updates?.mirror
|
|
10619
|
+
};
|
|
10620
|
+
if (options.rollback) {
|
|
10621
|
+
updates.pinnedVersion = bundle.manifest.version;
|
|
10622
|
+
updates.revision = randomUUID10();
|
|
10623
|
+
}
|
|
10624
|
+
const receipt = {
|
|
10625
|
+
format: 1,
|
|
10626
|
+
home,
|
|
10627
|
+
bin,
|
|
10628
|
+
current: id,
|
|
10629
|
+
version: bundle.manifest.version,
|
|
10630
|
+
launchers,
|
|
10631
|
+
pathChanges: previous?.pathChanges ?? [],
|
|
10632
|
+
updates,
|
|
10633
|
+
previous: previous && previous.current !== id ? { id: previous.current, version: previous.version } : previous?.previous
|
|
10634
|
+
};
|
|
10635
|
+
await writeStoreJson(home, "install.json", previous ? { ...previous, previousLaunchers: launchers } : receipt);
|
|
10636
|
+
try {
|
|
10637
|
+
if (!alreadyStaged) await copyBundle(home, source, destination, bundle);
|
|
10638
|
+
for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
10639
|
+
for (const [name, expected] of retired) {
|
|
10640
|
+
await checkRetired(name, expected);
|
|
10641
|
+
await fs32.rm(await storePath(bin, name), { force: true });
|
|
10642
|
+
}
|
|
10643
|
+
await writeStoreJson(home, "install.json", receipt);
|
|
10644
|
+
} catch (error) {
|
|
10645
|
+
if (previous) {
|
|
10646
|
+
for (const [name, content2] of Object.entries(previous.launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
10647
|
+
await writeStoreJson(home, "install.json", previous);
|
|
10648
|
+
}
|
|
10649
|
+
throw error;
|
|
10650
|
+
}
|
|
10651
|
+
if (options.preservePolicy) return { home, bin, version: receipt.version, previousVersion: previous?.version, path: { message: "Installation selected." } };
|
|
10652
|
+
progress?.step("Checking terminal command availability");
|
|
10653
|
+
const pathResult = await configurePath(bin, receipt.pathChanges ?? [], async (changes) => {
|
|
10654
|
+
receipt.pathChanges = changes;
|
|
10655
|
+
await writeStoreJson(home, "install.json", receipt);
|
|
10656
|
+
});
|
|
10657
|
+
return { home, bin, version: bundle.manifest.version, previousVersion: previous?.version, path: pathResult };
|
|
10658
|
+
}
|
|
10659
|
+
async function copyBundle(home, source, destination, bundle) {
|
|
10660
|
+
const exists = await fs32.lstat(destination).then(() => true, (error) => {
|
|
10661
|
+
if (error.code === "ENOENT") return false;
|
|
10662
|
+
throw error;
|
|
10663
|
+
});
|
|
10664
|
+
if (exists) {
|
|
10665
|
+
await verifyBundle(destination, bundle.manifestHash);
|
|
10666
|
+
return;
|
|
10667
|
+
}
|
|
10668
|
+
const stage = await storePath(home, "versions/.install-" + randomUUID10(), true);
|
|
10669
|
+
await fs32.mkdir(stage);
|
|
10670
|
+
try {
|
|
10671
|
+
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
10672
|
+
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
10673
|
+
await fs32.copyFile(from, to);
|
|
10674
|
+
await fs32.chmod(to, (await fs32.stat(from)).mode & 511);
|
|
10675
|
+
}
|
|
10676
|
+
await verifyBundle(stage, bundle.manifestHash);
|
|
10677
|
+
try {
|
|
10678
|
+
await fs32.rename(stage, destination);
|
|
10679
|
+
} catch (error) {
|
|
10680
|
+
try {
|
|
10681
|
+
await verifyBundle(destination, bundle.manifestHash);
|
|
10682
|
+
} catch {
|
|
10683
|
+
throw error;
|
|
10684
|
+
}
|
|
10685
|
+
}
|
|
10686
|
+
} finally {
|
|
10687
|
+
await fs32.rm(stage, { recursive: true, force: true });
|
|
10688
|
+
}
|
|
10689
|
+
}
|
|
10690
|
+
async function prepareStandaloneMcp(home) {
|
|
10691
|
+
const before = await readInstallation(home), candidate = before.pending;
|
|
10692
|
+
let verified, verificationError;
|
|
10693
|
+
if (candidate && automaticUpdatesAllowed(before) && !await hasRunningMcp(home)) {
|
|
10694
|
+
try {
|
|
10695
|
+
verified = await verifyBundle(await storePath(home, "versions/" + candidate.id));
|
|
10696
|
+
if (verified.manifestHash.slice(0, 24) !== candidate.id || verified.manifest.version !== candidate.version) throw new Error("Staged update changed.");
|
|
10697
|
+
} catch (error) {
|
|
10698
|
+
verificationError = error;
|
|
10699
|
+
}
|
|
10700
|
+
}
|
|
10701
|
+
return withLock(home, ".install-lock", async () => {
|
|
10702
|
+
let record = await readInstallation(home);
|
|
10703
|
+
const pending = record.pending;
|
|
10704
|
+
if (pending && candidate?.id === pending.id && (verified || verificationError) && automaticUpdatesAllowed(record) && pending.policyRevision === record.updates?.revision && newerStableVersion(pending.version, record.version) && !await hasRunningMcp(home)) {
|
|
10705
|
+
try {
|
|
10706
|
+
if (verificationError) throw verificationError;
|
|
10707
|
+
const source = await storePath(home, "versions/" + pending.id);
|
|
10708
|
+
await installLocked2(home, source, verified, void 0, { preservePolicy: true }, true);
|
|
10709
|
+
record = await readInstallation(home);
|
|
10710
|
+
} catch (error) {
|
|
10711
|
+
record = await readInstallation(home);
|
|
10712
|
+
delete record.pending;
|
|
10713
|
+
await writeStoreJson(home, "install.json", record);
|
|
10714
|
+
await writeStoreJson(home, "update-status.json", { lastAttempt: Date.now(), error: String(error) });
|
|
10715
|
+
}
|
|
10716
|
+
}
|
|
10717
|
+
const release = await registerMcp(home);
|
|
10718
|
+
return { bundle: await storePath(home, "versions/" + record.current), release };
|
|
10719
|
+
});
|
|
10720
|
+
}
|
|
10721
|
+
|
|
10722
|
+
// src/distribution/updates.ts
|
|
10723
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
10724
|
+
var RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10725
|
+
var releaseListSchema = z30.array(z30.object({ tag_name: z30.string(), draft: z30.boolean(), prerelease: z30.boolean(), published_at: z30.string().nullable() }));
|
|
10726
|
+
var updateManifestSchema = z30.object({
|
|
10727
|
+
format: z30.literal(1),
|
|
10728
|
+
version: z30.string().regex(/^\d+\.\d+\.\d+$/),
|
|
10729
|
+
publishedAt: z30.string().datetime(),
|
|
10730
|
+
assets: z30.record(z30.string().regex(/^[a-f0-9]{64}$/))
|
|
10731
|
+
}).strict();
|
|
10732
|
+
var statusSchema2 = z30.object({ lastAttempt: z30.number(), availableVersion: z30.string().optional(), error: z30.string().optional() });
|
|
10733
|
+
async function scheduleAutomaticUpdate() {
|
|
10734
|
+
try {
|
|
10735
|
+
const { home, record } = await currentInstallation();
|
|
10736
|
+
if (!automaticUpdatesAllowed(record)) return;
|
|
10737
|
+
const status = statusSchema2.safeParse(await readStoreJson(home, "update-status.json"));
|
|
10738
|
+
const now = Date.now();
|
|
10739
|
+
if (status.success && status.data.lastAttempt <= now && now - status.data.lastAttempt < CHECK_INTERVAL_MS) return;
|
|
10740
|
+
const bundle = await storePath(home, "versions/" + record.current);
|
|
10741
|
+
const child = spawn3(
|
|
10742
|
+
path42.join(bundle, process.platform === "win32" ? "node.exe" : "node"),
|
|
10743
|
+
[path42.join(bundle, "app/dist/mason.js"), "internal-update"],
|
|
10744
|
+
{ cwd: home, env: process.env, stdio: "ignore", detached: true, windowsHide: true }
|
|
10745
|
+
);
|
|
10746
|
+
child.on("error", () => {
|
|
10747
|
+
});
|
|
10748
|
+
child.unref();
|
|
10749
|
+
} catch {
|
|
10750
|
+
}
|
|
10751
|
+
}
|
|
10752
|
+
async function startMcpWithUpdates(start) {
|
|
10753
|
+
const current = standaloneLocation();
|
|
10754
|
+
if (!current) {
|
|
10755
|
+
await start();
|
|
10756
|
+
return;
|
|
10757
|
+
}
|
|
10758
|
+
const session = await prepareStandaloneMcp(current.home);
|
|
10759
|
+
if (session.bundle !== current.bundle) {
|
|
10760
|
+
const child = spawn3(
|
|
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)],
|
|
10763
|
+
{ stdio: "inherit", windowsHide: true }
|
|
10764
|
+
);
|
|
10765
|
+
const interrupt = () => {
|
|
10766
|
+
child.kill("SIGINT");
|
|
10767
|
+
}, terminate = () => {
|
|
10768
|
+
child.kill("SIGTERM");
|
|
10769
|
+
};
|
|
10770
|
+
process.on("SIGINT", interrupt);
|
|
10771
|
+
process.on("SIGTERM", terminate);
|
|
10772
|
+
try {
|
|
10773
|
+
process.exitCode = await new Promise((resolve2, reject) => {
|
|
10774
|
+
child.on("error", reject);
|
|
10775
|
+
child.on("exit", (code) => resolve2(code ?? 1));
|
|
10776
|
+
});
|
|
10777
|
+
} finally {
|
|
10778
|
+
process.off("SIGINT", interrupt);
|
|
10779
|
+
process.off("SIGTERM", terminate);
|
|
10780
|
+
await session.release();
|
|
10781
|
+
}
|
|
10782
|
+
return;
|
|
10783
|
+
}
|
|
10784
|
+
const timer = setInterval(() => {
|
|
10785
|
+
void scheduleAutomaticUpdate();
|
|
10786
|
+
}, 60 * 60 * 1e3);
|
|
10787
|
+
timer.unref();
|
|
10788
|
+
const stop = () => {
|
|
10789
|
+
clearInterval(timer);
|
|
10790
|
+
void session?.release();
|
|
10791
|
+
};
|
|
10792
|
+
try {
|
|
10793
|
+
await start(stop);
|
|
10794
|
+
void scheduleAutomaticUpdate();
|
|
10795
|
+
} catch (error) {
|
|
10796
|
+
stop();
|
|
10797
|
+
throw error;
|
|
10798
|
+
}
|
|
10799
|
+
}
|
|
10800
|
+
|
|
9979
10801
|
// bin/mason-mcp.ts
|
|
9980
|
-
startMcpServer
|
|
10802
|
+
startMcpWithUpdates(startMcpServer).catch((err) => {
|
|
9981
10803
|
process.stderr.write(`Mason MCP server error: ${err}
|
|
9982
10804
|
`);
|
|
9983
10805
|
process.exit(1);
|