mason-context 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/mason-audit.js +524 -324
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +1099 -729
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +50 -0
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +1450 -1069
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +50 -0
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +157 -54
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +1208 -784
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason-mcp.js
CHANGED
|
@@ -58,8 +58,471 @@ var init_git_read = __esm({
|
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
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
|
+
|
|
61
524
|
// src/audit/commands.ts
|
|
62
|
-
import
|
|
525
|
+
import path6 from "path";
|
|
63
526
|
function splitCommands(line) {
|
|
64
527
|
const parts = [];
|
|
65
528
|
let quote2 = "", start = 0;
|
|
@@ -95,7 +558,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
95
558
|
const cd = text2.match(/^cd\s+(.+)$/);
|
|
96
559
|
if (cd) {
|
|
97
560
|
const directory = literal(cd[1]);
|
|
98
|
-
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);
|
|
99
562
|
continue;
|
|
100
563
|
}
|
|
101
564
|
for (const match of text2.matchAll(COMMAND)) {
|
|
@@ -106,7 +569,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
106
569
|
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
107
570
|
if (selector) {
|
|
108
571
|
const selected = literal(selector[1]);
|
|
109
|
-
directory = selected === null || cwd === null ? null :
|
|
572
|
+
directory = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
|
|
110
573
|
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
111
574
|
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
112
575
|
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
@@ -136,7 +599,7 @@ var init_commands = __esm({
|
|
|
136
599
|
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
137
600
|
literal = (value) => {
|
|
138
601
|
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
139
|
-
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) ||
|
|
602
|
+
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path6.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
140
603
|
};
|
|
141
604
|
}
|
|
142
605
|
});
|
|
@@ -382,278 +845,62 @@ function extractClaims(content2) {
|
|
|
382
845
|
excerpt: m[0]
|
|
383
846
|
});
|
|
384
847
|
}
|
|
385
|
-
for (const match of line.matchAll(/`([^`]+)`/g)) {
|
|
386
|
-
for (const claim of commandClaims(match[1], lineNo, void 0, true).claims) addCommand(claim);
|
|
387
|
-
}
|
|
388
|
-
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims) addCommand(claim);
|
|
389
|
-
}
|
|
390
|
-
if (inFence) processBlock();
|
|
391
|
-
return {
|
|
392
|
-
paths: [...paths.values()],
|
|
393
|
-
counts,
|
|
394
|
-
commands: [...commands2.values()]
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
|
|
398
|
-
var init_claims = __esm({
|
|
399
|
-
"src/audit/claims.ts"() {
|
|
400
|
-
"use strict";
|
|
401
|
-
init_commands();
|
|
402
|
-
init_tree();
|
|
403
|
-
ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
404
|
-
"package.json",
|
|
405
|
-
"package-lock.json",
|
|
406
|
-
"pnpm-workspace.yaml",
|
|
407
|
-
"tsconfig.json",
|
|
408
|
-
"tsup.config.ts",
|
|
409
|
-
"vitest.config.ts",
|
|
410
|
-
"Makefile",
|
|
411
|
-
"Dockerfile",
|
|
412
|
-
"docker-compose.yml",
|
|
413
|
-
"Cargo.toml",
|
|
414
|
-
"go.mod",
|
|
415
|
-
"go.sum",
|
|
416
|
-
"pyproject.toml",
|
|
417
|
-
"requirements.txt",
|
|
418
|
-
"Gemfile",
|
|
419
|
-
"composer.json",
|
|
420
|
-
"settings.gradle.kts",
|
|
421
|
-
"settings.gradle",
|
|
422
|
-
"build.gradle.kts",
|
|
423
|
-
"build.gradle",
|
|
424
|
-
"manifest.json",
|
|
425
|
-
"server.json",
|
|
426
|
-
"README.md",
|
|
427
|
-
"CHANGELOG.md",
|
|
428
|
-
"LICENSE",
|
|
429
|
-
"CLAUDE.md",
|
|
430
|
-
"AGENTS.md",
|
|
431
|
-
".gitignore",
|
|
432
|
-
".env.example"
|
|
433
|
-
]);
|
|
434
|
-
SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
|
|
435
|
-
markdown = new MarkdownIt("commonmark", { html: false });
|
|
436
|
-
COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
|
|
437
|
-
COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
|
|
438
|
-
IGNORE_LINE = "<!-- mason:ignore -->";
|
|
439
|
-
IGNORE_START = "<!-- mason:ignore-start -->";
|
|
440
|
-
IGNORE_END = "<!-- mason:ignore-end -->";
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// src/utils/paths.ts
|
|
445
|
-
import path2 from "path";
|
|
446
|
-
function normalizeRepoPath(value) {
|
|
447
|
-
const slash = value.replace(/\\/g, "/");
|
|
448
|
-
if (!slash || slash.includes("\0") || path2.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
|
|
449
|
-
if (slash.split("/").includes("..")) return null;
|
|
450
|
-
const normalized = path2.posix.normalize(slash).replace(/\/$/, "");
|
|
451
|
-
return normalized === "." ? null : normalized;
|
|
452
|
-
}
|
|
453
|
-
function sanitizeRepoPaths(files) {
|
|
454
|
-
return [...new Set(files.map(normalizeRepoPath).filter((p) => p !== null))];
|
|
455
|
-
}
|
|
456
|
-
function isWithinRoot(root, candidate) {
|
|
457
|
-
const relative = path2.relative(root, candidate);
|
|
458
|
-
return relative !== ".." && !relative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relative);
|
|
459
|
-
}
|
|
460
|
-
function anchorMatches(anchor, file) {
|
|
461
|
-
const a = normalizeRepoPath(anchor);
|
|
462
|
-
const f = normalizeRepoPath(file);
|
|
463
|
-
return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
|
|
464
|
-
}
|
|
465
|
-
function matchingPaths(anchors, files) {
|
|
466
|
-
return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
|
|
467
|
-
}
|
|
468
|
-
var init_paths = __esm({
|
|
469
|
-
"src/utils/paths.ts"() {
|
|
470
|
-
"use strict";
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// src/utils/files.ts
|
|
475
|
-
import fs from "fs/promises";
|
|
476
|
-
import { constants } from "fs";
|
|
477
|
-
import path3 from "path";
|
|
478
|
-
import fg from "fast-glob";
|
|
479
|
-
function isSensitiveFile(file) {
|
|
480
|
-
return file.split(/[\\/]/).some(
|
|
481
|
-
(part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(part)
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
async function readBoundedFile(file, maxBytes) {
|
|
485
|
-
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
486
|
-
try {
|
|
487
|
-
const stat = await handle.stat();
|
|
488
|
-
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
489
|
-
const buffer = Buffer.alloc(Math.min(maxBytes + 1, stat.size + 1));
|
|
490
|
-
let bytes = 0;
|
|
491
|
-
while (bytes < buffer.length) {
|
|
492
|
-
const result = await handle.read(buffer, bytes, buffer.length - bytes, null);
|
|
493
|
-
if (result.bytesRead === 0) break;
|
|
494
|
-
bytes += result.bytesRead;
|
|
495
|
-
}
|
|
496
|
-
return bytes === buffer.length ? null : buffer.subarray(0, bytes).toString("utf8");
|
|
497
|
-
} finally {
|
|
498
|
-
await handle.close();
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
async function loadProjectConfig(root) {
|
|
502
|
-
try {
|
|
503
|
-
const canonicalRoot = await fs.realpath(root);
|
|
504
|
-
const configPath2 = await fs.realpath(path3.join(root, ".mason/config.json"));
|
|
505
|
-
if (!isWithinRoot(canonicalRoot, configPath2)) throw new Error("Project configuration resolves outside the repository");
|
|
506
|
-
const raw = await readBoundedFile(configPath2, 64 * 1024);
|
|
507
|
-
if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
|
|
508
|
-
const value = JSON.parse(raw);
|
|
509
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
|
|
510
|
-
const config = {};
|
|
511
|
-
for (const key of ["patterns", "alwaysInclude", "ignore"]) {
|
|
512
|
-
if (value[key] === void 0) continue;
|
|
513
|
-
if (!Array.isArray(value[key]) || !value[key].every((s) => typeof s === "string")) {
|
|
514
|
-
throw new Error(`Configuration ${key} must be an array of strings`);
|
|
515
|
-
}
|
|
516
|
-
config[key] = value[key];
|
|
517
|
-
}
|
|
518
|
-
return config;
|
|
519
|
-
} catch (error) {
|
|
520
|
-
if (error.code === "ENOENT") return {};
|
|
521
|
-
throw new Error(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
async function createFileAccess(rootDir) {
|
|
525
|
-
const root = path3.resolve(rootDir);
|
|
526
|
-
const canonicalRoot = await fs.realpath(root).catch(() => root);
|
|
527
|
-
const config = await loadProjectConfig(root);
|
|
528
|
-
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
529
|
-
let gitFiles = null;
|
|
530
|
-
try {
|
|
531
|
-
const { stdout } = await execGit(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
|
|
532
|
-
gitFiles = new Set(stdout.split("\0").filter(Boolean));
|
|
533
|
-
} catch {
|
|
534
|
-
let inGit = false;
|
|
535
|
-
try {
|
|
536
|
-
await execGit(["rev-parse", "--git-dir"], { cwd: root });
|
|
537
|
-
inGit = true;
|
|
538
|
-
} catch {
|
|
539
|
-
}
|
|
540
|
-
if (inGit) throw new Error("Cannot enumerate Git files safely");
|
|
541
|
-
}
|
|
542
|
-
async function resolve2(file) {
|
|
543
|
-
const relative = normalizeRepoPath(file);
|
|
544
|
-
if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative)) return null;
|
|
545
|
-
const candidate = path3.join(root, relative);
|
|
546
|
-
try {
|
|
547
|
-
const real = await fs.realpath(candidate);
|
|
548
|
-
if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path3.relative(canonicalRoot, real))) return null;
|
|
549
|
-
const stat = await fs.stat(real);
|
|
550
|
-
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
|
|
551
|
-
if (gitFiles && !gitFiles.has(path3.relative(canonicalRoot, real).split(path3.sep).join("/"))) return null;
|
|
552
|
-
return real;
|
|
553
|
-
} catch {
|
|
554
|
-
return null;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
async function list(patterns = SOURCE_GLOB, options = {}) {
|
|
558
|
-
const found = await fg(patterns, { cwd: root, ignore, followSymbolicLinks: false, ...options });
|
|
559
|
-
const safe = await Promise.all(found.map(async (f) => await resolve2(f) ? f : null));
|
|
560
|
-
return safe.filter((f) => f !== null).sort();
|
|
561
|
-
}
|
|
562
|
-
async function read(file) {
|
|
563
|
-
const relative = normalizeRepoPath(file);
|
|
564
|
-
if (!relative) return null;
|
|
565
|
-
const real = await resolve2(relative);
|
|
566
|
-
if (!real) return null;
|
|
567
|
-
for (const rel of /* @__PURE__ */ new Set([relative, path3.relative(canonicalRoot, real).split(path3.sep).join("/")])) {
|
|
568
|
-
if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
|
|
569
|
-
}
|
|
570
|
-
try {
|
|
571
|
-
const content2 = await readBoundedFile(real, MAX_SOURCE_BYTES);
|
|
572
|
-
return content2 === null ? null : { path: relative, content: content2, totalLines: content2.split("\n").length };
|
|
573
|
-
} catch {
|
|
574
|
-
return null;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
return { root, config, list, read };
|
|
578
|
-
}
|
|
579
|
-
var SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
|
|
580
|
-
var init_files = __esm({
|
|
581
|
-
"src/utils/files.ts"() {
|
|
582
|
-
"use strict";
|
|
583
|
-
init_git_read();
|
|
584
|
-
init_paths();
|
|
585
|
-
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"];
|
|
586
|
-
SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
|
|
587
|
-
SOURCE_IGNORE = [
|
|
588
|
-
"**/node_modules/**",
|
|
589
|
-
"**/dist/**",
|
|
590
|
-
"**/build/**",
|
|
591
|
-
"**/.gradle/**",
|
|
592
|
-
"**/target/**",
|
|
593
|
-
"**/.git/**",
|
|
594
|
-
"**/.mason/**",
|
|
595
|
-
"**/vendor/**",
|
|
596
|
-
"**/__pycache__/**",
|
|
597
|
-
"**/venv/**",
|
|
598
|
-
"**/.venv/**",
|
|
599
|
-
"**/*.min.*",
|
|
600
|
-
"**/*.map",
|
|
601
|
-
"**/*.lock",
|
|
602
|
-
"**/generated/**",
|
|
603
|
-
"**/*.generated.*",
|
|
604
|
-
"**/R.java",
|
|
605
|
-
"**/BuildConfig.java",
|
|
606
|
-
"**/package-lock.json",
|
|
607
|
-
"**/yarn.lock",
|
|
608
|
-
"**/pnpm-lock.yaml"
|
|
609
|
-
];
|
|
610
|
-
MAX_SOURCE_BYTES = 1024 * 1024;
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
// src/audit/inspection.ts
|
|
615
|
-
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
616
|
-
import path4 from "path";
|
|
617
|
-
function withRepositoryInspection(root, run) {
|
|
618
|
-
return inspections.run({ root: path4.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
|
|
619
|
-
}
|
|
620
|
-
function inspectionRead(root, key, read) {
|
|
621
|
-
const inspection = inspections.getStore();
|
|
622
|
-
if (!inspection || inspection.root !== path4.resolve(root)) return read();
|
|
623
|
-
if (!inspection.reads.has(key)) {
|
|
624
|
-
const pending = read();
|
|
625
|
-
inspection.reads.set(key, pending);
|
|
626
|
-
void pending.catch(() => {
|
|
627
|
-
if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
|
|
628
|
-
});
|
|
848
|
+
for (const match of line.matchAll(/`([^`]+)`/g)) {
|
|
849
|
+
for (const claim of commandClaims(match[1], lineNo, void 0, true).claims) addCommand(claim);
|
|
850
|
+
}
|
|
851
|
+
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims) addCommand(claim);
|
|
629
852
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const shared = inspections.getStore()?.root === path4.resolve(root);
|
|
637
|
-
const specs = shared ? [...docSpecs, ...sourceSpecs] : kind === "documents" ? docSpecs : sourceSpecs;
|
|
638
|
-
const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
|
|
639
|
-
const { stdout } = await inspectionGit(
|
|
640
|
-
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
|
|
641
|
-
{ cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
|
|
642
|
-
);
|
|
643
|
-
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("."))));
|
|
644
|
-
if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
|
|
645
|
-
return paths.sort();
|
|
853
|
+
if (inFence) processBlock();
|
|
854
|
+
return {
|
|
855
|
+
paths: [...paths.values()],
|
|
856
|
+
counts,
|
|
857
|
+
commands: [...commands2.values()]
|
|
858
|
+
};
|
|
646
859
|
}
|
|
647
|
-
var
|
|
648
|
-
var
|
|
649
|
-
"src/audit/
|
|
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"() {
|
|
650
863
|
"use strict";
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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 -->";
|
|
657
904
|
}
|
|
658
905
|
});
|
|
659
906
|
|
|
@@ -720,212 +967,32 @@ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
|
|
|
720
967
|
],
|
|
721
968
|
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
722
969
|
);
|
|
723
|
-
const commits = [];
|
|
724
|
-
for (const block of stdout.split("")) {
|
|
725
|
-
if (!block.trim()) continue;
|
|
726
|
-
const lines = block.split("\n").filter((l) => l.trim().length > 0);
|
|
727
|
-
const ref = parseCommitLine(lines[0]);
|
|
728
|
-
if (!ref) continue;
|
|
729
|
-
commits.push({ ...ref, files: lines.slice(1).map((l) => l.trim()) });
|
|
730
|
-
}
|
|
731
|
-
return { commits, total: commits.length };
|
|
732
|
-
} catch {
|
|
733
|
-
return null;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
var COMMIT_FORMAT;
|
|
737
|
-
var init_git = __esm({
|
|
738
|
-
"src/audit/git.ts"() {
|
|
739
|
-
"use strict";
|
|
740
|
-
init_inspection();
|
|
741
|
-
COMMIT_FORMAT = "%H%x09%cI%x09%s";
|
|
742
|
-
}
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
// src/utils/storage.ts
|
|
746
|
-
import fs2 from "fs/promises";
|
|
747
|
-
import path5 from "path";
|
|
748
|
-
import { randomUUID } from "crypto";
|
|
749
|
-
async function storePath(root, relative, createParents = false) {
|
|
750
|
-
const normalized = normalizeRepoPath(relative);
|
|
751
|
-
if (!normalized) throw new Error(`Invalid store path: ${relative}`);
|
|
752
|
-
let current = await fs2.realpath(root);
|
|
753
|
-
const parts = normalized.split("/");
|
|
754
|
-
for (let i = 0; i < parts.length; i++) {
|
|
755
|
-
current = path5.join(current, parts[i]);
|
|
756
|
-
let stat;
|
|
757
|
-
try {
|
|
758
|
-
stat = await fs2.lstat(current);
|
|
759
|
-
} catch (error) {
|
|
760
|
-
if (error.code !== "ENOENT") throw error;
|
|
761
|
-
if (createParents && i < parts.length - 1) {
|
|
762
|
-
try {
|
|
763
|
-
await fs2.mkdir(current);
|
|
764
|
-
} catch (mkdirError) {
|
|
765
|
-
if (mkdirError.code !== "EEXIST") throw mkdirError;
|
|
766
|
-
}
|
|
767
|
-
stat = await fs2.lstat(current);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
if (stat?.isSymbolicLink()) throw new Error(`Symlink in store path: ${relative}`);
|
|
771
|
-
}
|
|
772
|
-
return current;
|
|
773
|
-
}
|
|
774
|
-
async function readStoreJson(root, relative) {
|
|
775
|
-
try {
|
|
776
|
-
const file = await storePath(root, relative);
|
|
777
|
-
const raw = await readBoundedFile(file, 10 * 1024 * 1024);
|
|
778
|
-
if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
|
|
779
|
-
const parsed = JSON.parse(raw);
|
|
780
|
-
if (parsed === null) throw new Error("expected a JSON object, received null");
|
|
781
|
-
return parsed;
|
|
782
|
-
} catch (error) {
|
|
783
|
-
if (error.code === "ENOENT") return null;
|
|
784
|
-
throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
async function writeStoreJson(root, relative, value) {
|
|
788
|
-
const payload = JSON.stringify(value, null, 2) + "\n";
|
|
789
|
-
if (Buffer.byteLength(payload) > 10 * 1024 * 1024) {
|
|
790
|
-
throw new Error(`Mason store ${relative} exceeds 10 MiB`);
|
|
791
|
-
}
|
|
792
|
-
const file = await storePath(root, relative, true);
|
|
793
|
-
const temporary = path5.join(path5.dirname(file), `.${path5.basename(file)}.${randomUUID()}.tmp`);
|
|
794
|
-
try {
|
|
795
|
-
const handle = await fs2.open(temporary, "wx", 384);
|
|
796
|
-
try {
|
|
797
|
-
await handle.writeFile(payload, "utf8");
|
|
798
|
-
await handle.sync();
|
|
799
|
-
} finally {
|
|
800
|
-
await handle.close();
|
|
801
|
-
}
|
|
802
|
-
await fs2.rename(temporary, file);
|
|
803
|
-
} finally {
|
|
804
|
-
await fs2.rm(temporary, { force: true });
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
var init_storage = __esm({
|
|
808
|
-
"src/utils/storage.ts"() {
|
|
809
|
-
"use strict";
|
|
810
|
-
init_paths();
|
|
811
|
-
init_files();
|
|
812
|
-
}
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
// src/audit/inputs.ts
|
|
816
|
-
import fs3 from "fs/promises";
|
|
817
|
-
import * as nativeFs from "fs";
|
|
818
|
-
import path6 from "path";
|
|
819
|
-
import fg2 from "fast-glob";
|
|
820
|
-
async function auditInputPath(root, file) {
|
|
821
|
-
if (file === ".") return fs3.realpath(root);
|
|
822
|
-
try {
|
|
823
|
-
return await storePath(root, file);
|
|
824
|
-
} catch (error) {
|
|
825
|
-
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
826
|
-
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
827
|
-
}
|
|
828
|
-
throw error;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
async function readAuditInput(root, file) {
|
|
832
|
-
try {
|
|
833
|
-
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
834
|
-
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
835
|
-
return value;
|
|
836
|
-
} catch (error) {
|
|
837
|
-
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
|
|
838
|
-
throw error;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
async function auditGlob(root, patterns, options) {
|
|
842
|
-
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
843
|
-
const directories = /* @__PURE__ */ new Set();
|
|
844
|
-
const readdir2 = ((directory, opts, callback) => {
|
|
845
|
-
const relative = path6.relative(root, directory).split(path6.sep).join("/") || ".";
|
|
846
|
-
directories.add(relative);
|
|
847
|
-
if (directories.size > MAX_INPUTS) {
|
|
848
|
-
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
auditInputPath(root, relative).then(() => {
|
|
852
|
-
nativeFs.readdir(directory, opts, callback);
|
|
853
|
-
}, callback);
|
|
854
|
-
});
|
|
855
|
-
const lstat2 = ((file, callback) => {
|
|
856
|
-
const relative = path6.relative(root, file).split(path6.sep).join("/") || ".";
|
|
857
|
-
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
858
|
-
});
|
|
859
|
-
const stream = fg2.stream(patterns, {
|
|
860
|
-
cwd: root,
|
|
861
|
-
ignore: options.ignore,
|
|
862
|
-
onlyFiles: false,
|
|
863
|
-
onlyDirectories: options.onlyDirectories,
|
|
864
|
-
objectMode: true,
|
|
865
|
-
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
866
|
-
fs: { readdir: readdir2, lstat: lstat2 },
|
|
867
|
-
concurrency: 16
|
|
868
|
-
});
|
|
869
|
-
const matches = [];
|
|
870
|
-
try {
|
|
871
|
-
for await (const value of stream) {
|
|
872
|
-
const entry = value;
|
|
873
|
-
const file = entry.path;
|
|
874
|
-
if (options.select && !options.select(file)) continue;
|
|
875
|
-
await auditInputPath(root, file);
|
|
876
|
-
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
877
|
-
matches.push(file);
|
|
878
|
-
if (matches.length > MAX_INPUTS) {
|
|
879
|
-
const largest = [...matches.reduce((counts, item) => {
|
|
880
|
-
const dir = item.split("/")[0];
|
|
881
|
-
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
882
|
-
return counts;
|
|
883
|
-
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
884
|
-
throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count4]) => `${dir} (${count4})`).join(", ")}. Narrow this check's inputs.`);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
} finally {
|
|
888
|
-
stream.destroy();
|
|
889
|
-
}
|
|
890
|
-
return matches.sort();
|
|
891
|
-
}
|
|
892
|
-
async function gitSourcePaths(root) {
|
|
893
|
-
return auditGitPaths(root, "sources");
|
|
894
|
-
}
|
|
895
|
-
async function gitIgnoredPaths(root, files) {
|
|
896
|
-
if (!files.length) return /* @__PURE__ */ new Set();
|
|
897
|
-
const ignored = /* @__PURE__ */ new Set();
|
|
898
|
-
for (let offset = 0; offset < files.length; offset += 1e3) {
|
|
899
|
-
try {
|
|
900
|
-
const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
|
|
901
|
-
cwd: root,
|
|
902
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
903
|
-
timeout: 1e4,
|
|
904
|
-
input: files.slice(offset, offset + 1e3).join("\0") + "\0"
|
|
905
|
-
});
|
|
906
|
-
for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
|
|
907
|
-
} catch (error) {
|
|
908
|
-
if (error.code !== 1) throw error;
|
|
970
|
+
const commits = [];
|
|
971
|
+
for (const block of stdout.split("")) {
|
|
972
|
+
if (!block.trim()) continue;
|
|
973
|
+
const lines = block.split("\n").filter((l) => l.trim().length > 0);
|
|
974
|
+
const ref = parseCommitLine(lines[0]);
|
|
975
|
+
if (!ref) continue;
|
|
976
|
+
commits.push({ ...ref, files: lines.slice(1).map((l) => l.trim()) });
|
|
909
977
|
}
|
|
978
|
+
return { commits, total: commits.length };
|
|
979
|
+
} catch {
|
|
980
|
+
return null;
|
|
910
981
|
}
|
|
911
|
-
return ignored;
|
|
912
982
|
}
|
|
913
|
-
var
|
|
914
|
-
var
|
|
915
|
-
"src/audit/
|
|
983
|
+
var COMMIT_FORMAT;
|
|
984
|
+
var init_git = __esm({
|
|
985
|
+
"src/audit/git.ts"() {
|
|
916
986
|
"use strict";
|
|
917
|
-
init_files();
|
|
918
|
-
init_storage();
|
|
919
|
-
init_git_read();
|
|
920
987
|
init_inspection();
|
|
921
|
-
|
|
988
|
+
COMMIT_FORMAT = "%H%x09%cI%x09%s";
|
|
922
989
|
}
|
|
923
990
|
});
|
|
924
991
|
|
|
925
992
|
// src/audit/docs.ts
|
|
926
993
|
import fs4 from "fs/promises";
|
|
927
994
|
import path7 from "path";
|
|
928
|
-
import
|
|
995
|
+
import fg5 from "fast-glob";
|
|
929
996
|
async function dirtyDocs(resolvedRoot, files) {
|
|
930
997
|
if (!files.length) return /* @__PURE__ */ new Set();
|
|
931
998
|
try {
|
|
@@ -977,7 +1044,7 @@ async function discoverDocPaths(root) {
|
|
|
977
1044
|
const paths = [];
|
|
978
1045
|
const unique = [...new Set(candidates)].sort();
|
|
979
1046
|
for (let offset = 0; offset < unique.length; offset += 256) {
|
|
980
|
-
paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) =>
|
|
1047
|
+
paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg5.escapePath(p)), {
|
|
981
1048
|
ignore: excluded,
|
|
982
1049
|
followSymbolicLinks: false,
|
|
983
1050
|
label: "Documentation discovery"
|
|
@@ -1007,6 +1074,7 @@ async function localDocPaths(root) {
|
|
|
1007
1074
|
return paths;
|
|
1008
1075
|
}
|
|
1009
1076
|
function documentScope(file) {
|
|
1077
|
+
if (!/^(readme|agents|claude)\.md$/i.test(path7.posix.basename(file))) return ".";
|
|
1010
1078
|
const parent = path7.posix.dirname(file);
|
|
1011
1079
|
return path7.posix.basename(parent).toLowerCase() === ".claude" ? path7.posix.dirname(parent) : parent;
|
|
1012
1080
|
}
|
|
@@ -1028,7 +1096,7 @@ async function readDocs(resolvedRoot) {
|
|
|
1028
1096
|
path: candidate,
|
|
1029
1097
|
content: content2,
|
|
1030
1098
|
scope: documentScope(candidate),
|
|
1031
|
-
kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : "instructions",
|
|
1099
|
+
kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path7.posix.basename(candidate)) ? "instructions" : "guide",
|
|
1032
1100
|
lineCount: content2.split("\n").length,
|
|
1033
1101
|
lastCommit,
|
|
1034
1102
|
dirty: dirty.has(candidate),
|
|
@@ -1585,22 +1653,6 @@ var init_drift = __esm({
|
|
|
1585
1653
|
}
|
|
1586
1654
|
});
|
|
1587
1655
|
|
|
1588
|
-
// src/audit/types.ts
|
|
1589
|
-
var ALL_CHECKS;
|
|
1590
|
-
var init_types = __esm({
|
|
1591
|
-
"src/audit/types.ts"() {
|
|
1592
|
-
"use strict";
|
|
1593
|
-
ALL_CHECKS = [
|
|
1594
|
-
"deleted-reference",
|
|
1595
|
-
"new-module",
|
|
1596
|
-
"stale-count",
|
|
1597
|
-
"dead-command",
|
|
1598
|
-
"deps-changed",
|
|
1599
|
-
"decision-anchor-drift"
|
|
1600
|
-
];
|
|
1601
|
-
}
|
|
1602
|
-
});
|
|
1603
|
-
|
|
1604
1656
|
// src/audit/checks/deleted-reference.ts
|
|
1605
1657
|
import path12 from "path";
|
|
1606
1658
|
async function checkDeletedReferences(ctx) {
|
|
@@ -1669,7 +1721,7 @@ var init_deleted_reference = __esm({
|
|
|
1669
1721
|
});
|
|
1670
1722
|
|
|
1671
1723
|
// src/audit/checks/new-module.ts
|
|
1672
|
-
import
|
|
1724
|
+
import fg6 from "fast-glob";
|
|
1673
1725
|
import fs8 from "fs/promises";
|
|
1674
1726
|
import path13 from "path";
|
|
1675
1727
|
function escapeRegExp(text2) {
|
|
@@ -1731,7 +1783,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1731
1783
|
const result = /* @__PURE__ */ new Map();
|
|
1732
1784
|
for (const dir of parents) {
|
|
1733
1785
|
const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
|
|
1734
|
-
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${
|
|
1786
|
+
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
|
|
1735
1787
|
ignore,
|
|
1736
1788
|
onlyDirectories: true,
|
|
1737
1789
|
label: "Module directory discovery",
|
|
@@ -1744,7 +1796,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1744
1796
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1745
1797
|
let count4 = 0;
|
|
1746
1798
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1747
|
-
count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) =>
|
|
1799
|
+
count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)), {
|
|
1748
1800
|
ignore,
|
|
1749
1801
|
followSymbolicLinks: false,
|
|
1750
1802
|
label: `Source discovery in ${dir}`
|
|
@@ -2079,6 +2131,87 @@ var init_dead_command = __esm({
|
|
|
2079
2131
|
}
|
|
2080
2132
|
});
|
|
2081
2133
|
|
|
2134
|
+
// src/audit/dependency-claims.ts
|
|
2135
|
+
import path16 from "path";
|
|
2136
|
+
import { parse as parseToml } from "smol-toml";
|
|
2137
|
+
function dependencyText(content2) {
|
|
2138
|
+
return content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, (text2) => text2.replace(/[^\n]/g, " ")).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
|
|
2139
|
+
}
|
|
2140
|
+
function declarations(file, source) {
|
|
2141
|
+
const result = /* @__PURE__ */ new Map();
|
|
2142
|
+
if (!source) return result;
|
|
2143
|
+
if (path16.posix.basename(file) === "package.json") {
|
|
2144
|
+
const json = JSON.parse(source);
|
|
2145
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "engines"]) {
|
|
2146
|
+
for (const [name, value] of Object.entries(json[section] ?? {})) {
|
|
2147
|
+
if (typeof value === "string") result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
} else if (file.endsWith("libs.versions.toml")) {
|
|
2151
|
+
const catalog = parseToml(source);
|
|
2152
|
+
for (const section of ["libraries", "plugins"]) for (const [alias, entry] of Object.entries(catalog[section] ?? {})) {
|
|
2153
|
+
if (typeof entry === "string") {
|
|
2154
|
+
const parts = entry.split(":");
|
|
2155
|
+
if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
|
|
2156
|
+
} else if (entry && typeof entry === "object") {
|
|
2157
|
+
const name = entry.module ?? (entry.group && entry.name ? `${entry.group}:${entry.name}` : entry.id);
|
|
2158
|
+
const version = typeof entry.version === "string" ? entry.version : catalog.versions?.[entry.version?.ref];
|
|
2159
|
+
if (typeof name === "string") result.set(name, typeof version === "string" ? version : "unspecified");
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
} else if (/build\.gradle(?:\.kts)?$/.test(file)) {
|
|
2163
|
+
for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g)) result.set(match[1], match[2]);
|
|
2164
|
+
for (const match of source.matchAll(/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g)) result.set(match[1], match[2]);
|
|
2165
|
+
}
|
|
2166
|
+
return result;
|
|
2167
|
+
}
|
|
2168
|
+
async function matchDependencyChanges(root, from, head, files, content2) {
|
|
2169
|
+
const matches = [];
|
|
2170
|
+
const lines = dependencyText(content2).split("\n");
|
|
2171
|
+
let incomplete = files.length > 64;
|
|
2172
|
+
for (const file of files.slice(0, 64)) {
|
|
2173
|
+
if (!/(?:^|\/)(?:package\.json|libs\.versions\.toml|build\.gradle(?:\.kts)?)$/.test(file)) {
|
|
2174
|
+
incomplete = true;
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
try {
|
|
2178
|
+
const read = async (revision) => {
|
|
2179
|
+
const { stdout: entry } = await inspectionGit(["ls-tree", revision, "--", `:(literal)${file}`], { cwd: root, timeout: 1e4 });
|
|
2180
|
+
if (!entry) return "";
|
|
2181
|
+
if (!entry.startsWith("100644 ") && !entry.startsWith("100755 ")) throw new Error("Unsupported manifest mode");
|
|
2182
|
+
return (await inspectionGit(["show", `${revision}:${file}`], { cwd: root, maxBuffer: 1024 * 1024, timeout: 1e4 })).stdout;
|
|
2183
|
+
};
|
|
2184
|
+
const [before, after] = await Promise.all([read(from), read(head)]);
|
|
2185
|
+
const old = declarations(file, before), next = declarations(file, after);
|
|
2186
|
+
for (const dependency of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
|
|
2187
|
+
if (old.get(dependency) === next.get(dependency)) continue;
|
|
2188
|
+
const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2189
|
+
const mention = new RegExp(`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`, "i");
|
|
2190
|
+
const line = lines.findIndex((text2) => mention.test(text2));
|
|
2191
|
+
if (line < 0) continue;
|
|
2192
|
+
matches.push({
|
|
2193
|
+
dependency,
|
|
2194
|
+
manifest: file,
|
|
2195
|
+
before: old.get(dependency) ?? null,
|
|
2196
|
+
after: next.get(dependency) ?? null,
|
|
2197
|
+
line: line + 1,
|
|
2198
|
+
excerpt: lines[line].trim().slice(0, 500)
|
|
2199
|
+
});
|
|
2200
|
+
if (matches.length >= 100) return { matches, incomplete: true };
|
|
2201
|
+
}
|
|
2202
|
+
} catch {
|
|
2203
|
+
incomplete = true;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return { matches, incomplete };
|
|
2207
|
+
}
|
|
2208
|
+
var init_dependency_claims = __esm({
|
|
2209
|
+
"src/audit/dependency-claims.ts"() {
|
|
2210
|
+
"use strict";
|
|
2211
|
+
init_inspection();
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2082
2215
|
// src/audit/release-metadata.ts
|
|
2083
2216
|
async function git(root, args) {
|
|
2084
2217
|
return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
@@ -2158,13 +2291,6 @@ async function checkDepsChanged(ctx) {
|
|
|
2158
2291
|
});
|
|
2159
2292
|
continue;
|
|
2160
2293
|
}
|
|
2161
|
-
if (doc.dirty) {
|
|
2162
|
-
result.skipped.push({
|
|
2163
|
-
check: "deps-changed",
|
|
2164
|
-
doc: doc.path,
|
|
2165
|
-
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
2166
|
-
});
|
|
2167
|
-
}
|
|
2168
2294
|
let content2 = doc.content;
|
|
2169
2295
|
if (doc.dirty) {
|
|
2170
2296
|
try {
|
|
@@ -2177,7 +2303,6 @@ async function checkDepsChanged(ctx) {
|
|
|
2177
2303
|
continue;
|
|
2178
2304
|
}
|
|
2179
2305
|
}
|
|
2180
|
-
if (!hasDependencyContent(content2)) continue;
|
|
2181
2306
|
const range = await commitsTouchingSince(
|
|
2182
2307
|
ctx.root,
|
|
2183
2308
|
doc.lastCommit.hash,
|
|
@@ -2201,16 +2326,32 @@ async function checkDepsChanged(ctx) {
|
|
|
2201
2326
|
range.commits = relevant;
|
|
2202
2327
|
range.total = relevant.length;
|
|
2203
2328
|
if (range.total === 0) continue;
|
|
2204
|
-
const
|
|
2329
|
+
const relevance = await matchDependencyChanges(
|
|
2330
|
+
ctx.root,
|
|
2331
|
+
doc.lastCommit.hash,
|
|
2332
|
+
ctx.headHash,
|
|
2333
|
+
[...new Set(range.commits.flatMap((commit) => commit.files))],
|
|
2334
|
+
content2
|
|
2335
|
+
);
|
|
2336
|
+
if (!relevance.matches.length && !hasDependencyContent(content2)) continue;
|
|
2337
|
+
const match = relevance.matches[0];
|
|
2338
|
+
if (doc.dirty && match) result.skipped.push({
|
|
2339
|
+
check: "deps-changed",
|
|
2340
|
+
doc: doc.path,
|
|
2341
|
+
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
2342
|
+
});
|
|
2205
2343
|
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
2206
2344
|
type: "deps-changed",
|
|
2207
|
-
|
|
2208
|
-
|
|
2345
|
+
...match ? {} : { resolution: "informational" },
|
|
2346
|
+
message: match ? `${match.dependency} changed from ${match.before ?? "absent"} to ${match.after ?? "removed"} in ${match.manifest}; review ${doc.path}:${match.line}: ${match.excerpt}${relevance.matches.length > 1 ? ` (${relevance.matches.length - 1} more matches in evidence)` : ""}` : `Dependency manifests changed after the document baseline ${doc.lastCommit.hash.slice(0, 7)} (${doc.lastCommit.date.slice(0, 10)}); no specific affected passage was established. Informational only.${relevance.incomplete ? " Some manifest declarations could not be matched." : ""}`,
|
|
2347
|
+
anchor: { doc: doc.path, line: match?.line ?? null, excerpt: match?.excerpt ?? null },
|
|
2209
2348
|
evidence: {
|
|
2210
2349
|
kind: "doc-behind-manifests",
|
|
2211
2350
|
docLastCommit: doc.lastCommit,
|
|
2212
2351
|
manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
|
|
2213
|
-
totalCommits: range.total
|
|
2352
|
+
totalCommits: range.total,
|
|
2353
|
+
matches: relevance.matches,
|
|
2354
|
+
matchingIncomplete: relevance.incomplete
|
|
2214
2355
|
}
|
|
2215
2356
|
});
|
|
2216
2357
|
}
|
|
@@ -2220,6 +2361,7 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
|
2220
2361
|
var init_deps_changed = __esm({
|
|
2221
2362
|
"src/audit/checks/deps-changed.ts"() {
|
|
2222
2363
|
"use strict";
|
|
2364
|
+
init_dependency_claims();
|
|
2223
2365
|
init_docs();
|
|
2224
2366
|
init_release_metadata();
|
|
2225
2367
|
init_git();
|
|
@@ -2244,7 +2386,7 @@ var init_deps_changed = __esm({
|
|
|
2244
2386
|
});
|
|
2245
2387
|
|
|
2246
2388
|
// src/context/lexical.ts
|
|
2247
|
-
import
|
|
2389
|
+
import path17 from "path";
|
|
2248
2390
|
function tokenize(text2) {
|
|
2249
2391
|
return text2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
|
|
2250
2392
|
}
|
|
@@ -2257,7 +2399,7 @@ function tokenSet(text2) {
|
|
|
2257
2399
|
function scoreEntry(taskTokens, entry) {
|
|
2258
2400
|
const nameTokens = tokenSet(entry.name);
|
|
2259
2401
|
const descTokens = tokenSet(entry.description);
|
|
2260
|
-
const fileTokens = tokenSet(entry.files.map((f) =>
|
|
2402
|
+
const fileTokens = tokenSet(entry.files.map((f) => path17.basename(f)).join(" "));
|
|
2261
2403
|
let score = 0;
|
|
2262
2404
|
for (const token of taskTokens) {
|
|
2263
2405
|
if (nameTokens.has(token)) score += 3;
|
|
@@ -2591,7 +2733,7 @@ __export(decisions_exports, {
|
|
|
2591
2733
|
withDecisionWrite: () => withDecisionWrite
|
|
2592
2734
|
});
|
|
2593
2735
|
import fs9 from "fs/promises";
|
|
2594
|
-
import
|
|
2736
|
+
import path18 from "path";
|
|
2595
2737
|
import { createHash } from "crypto";
|
|
2596
2738
|
import { z as z3 } from "zod";
|
|
2597
2739
|
async function loadDecisionStore(rootDir) {
|
|
@@ -2689,7 +2831,7 @@ async function upsertDecision(rootDir, input) {
|
|
|
2689
2831
|
if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
|
|
2690
2832
|
for (const file of files) {
|
|
2691
2833
|
try {
|
|
2692
|
-
await fs9.access(
|
|
2834
|
+
await fs9.access(path18.join(rootDir, file));
|
|
2693
2835
|
} catch {
|
|
2694
2836
|
warnings.push(`anchor file does not exist on disk: ${file}`);
|
|
2695
2837
|
}
|
|
@@ -2814,9 +2956,9 @@ var init_decisions = __esm({
|
|
|
2814
2956
|
});
|
|
2815
2957
|
|
|
2816
2958
|
// src/decisions/drift.ts
|
|
2817
|
-
import
|
|
2959
|
+
import path19 from "path";
|
|
2818
2960
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
2819
|
-
const resolvedRoot =
|
|
2961
|
+
const resolvedRoot = path19.resolve(rootDir);
|
|
2820
2962
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
2821
2963
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
2822
2964
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -3012,7 +3154,9 @@ var init_findings = __esm({
|
|
|
3012
3154
|
kind: z4.literal("doc-behind-manifests"),
|
|
3013
3155
|
docLastCommit: commitSchema,
|
|
3014
3156
|
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
3015
|
-
totalCommits: count
|
|
3157
|
+
totalCommits: count,
|
|
3158
|
+
matches: z4.array(z4.object({ dependency: z4.string(), manifest: z4.string(), before: z4.string().nullable(), after: z4.string().nullable(), line: z4.number().int().positive(), excerpt: z4.string() })).optional(),
|
|
3159
|
+
matchingIncomplete: z4.boolean().optional()
|
|
3016
3160
|
}),
|
|
3017
3161
|
z4.object({
|
|
3018
3162
|
kind: z4.literal("decision-anchor"),
|
|
@@ -3028,7 +3172,7 @@ var init_findings = __esm({
|
|
|
3028
3172
|
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
3029
3173
|
confidence: z4.enum(["certain", "likely"])
|
|
3030
3174
|
});
|
|
3031
|
-
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.
|
|
3175
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.enum(["recheck", "informational"]).optional() });
|
|
3032
3176
|
checkResultSchema = z4.object({
|
|
3033
3177
|
issues: z4.array(issueSchema),
|
|
3034
3178
|
advisories: z4.array(advisorySchema),
|
|
@@ -3038,8 +3182,121 @@ var init_findings = __esm({
|
|
|
3038
3182
|
}
|
|
3039
3183
|
});
|
|
3040
3184
|
|
|
3041
|
-
// src/automation/
|
|
3185
|
+
// src/automation/notifications.ts
|
|
3042
3186
|
import { z as z5 } from "zod";
|
|
3187
|
+
function condition(finding) {
|
|
3188
|
+
const source = finding.current ?? finding.original;
|
|
3189
|
+
const e = source.evidence;
|
|
3190
|
+
const id = e.kind === "decision-anchor" ? digest([source.type, source.anchor.doc, e.decisionId]) : finding.id;
|
|
3191
|
+
const result = { level: "confidence" in source ? 2 : 1, facts: [] };
|
|
3192
|
+
switch (e.kind) {
|
|
3193
|
+
case "count-mismatch":
|
|
3194
|
+
result.magnitude = Math.abs(e.actual - e.claimed);
|
|
3195
|
+
break;
|
|
3196
|
+
case "unmentioned-dir":
|
|
3197
|
+
result.magnitude = e.sourceFileCount;
|
|
3198
|
+
break;
|
|
3199
|
+
case "decision-anchor":
|
|
3200
|
+
result.facts = [...e.changedFiles].sort();
|
|
3201
|
+
break;
|
|
3202
|
+
case "doc-behind-manifests":
|
|
3203
|
+
result.facts = (e.matches ?? []).map((m) => JSON.stringify([m.manifest, m.dependency, m.after])).sort();
|
|
3204
|
+
break;
|
|
3205
|
+
}
|
|
3206
|
+
return { id, condition: result };
|
|
3207
|
+
}
|
|
3208
|
+
function notificationConditions(findings) {
|
|
3209
|
+
const active = findings.filter((f) => f.status !== "resolved");
|
|
3210
|
+
const currentIds = new Set(active.filter((f) => f.current).map((f) => condition(f).id));
|
|
3211
|
+
const result = {};
|
|
3212
|
+
for (const f of active) {
|
|
3213
|
+
const value = condition(f);
|
|
3214
|
+
if (!f.current && currentIds.has(value.id)) continue;
|
|
3215
|
+
const previous = result[value.id];
|
|
3216
|
+
result[value.id] = previous ? {
|
|
3217
|
+
level: Math.max(previous.level, value.condition.level),
|
|
3218
|
+
facts: [.../* @__PURE__ */ new Set([...previous.facts, ...value.condition.facts])].sort(),
|
|
3219
|
+
...previous.magnitude !== void 0 || value.condition.magnitude !== void 0 ? { magnitude: Math.max(previous.magnitude ?? 0, value.condition.magnitude ?? 0) } : {}
|
|
3220
|
+
} : value.condition;
|
|
3221
|
+
}
|
|
3222
|
+
return result;
|
|
3223
|
+
}
|
|
3224
|
+
function createNotificationState(findings, head) {
|
|
3225
|
+
return { version: 1, baselineHead: head, baseline: notificationConditions(findings), delivered: {}, cleared: [] };
|
|
3226
|
+
}
|
|
3227
|
+
function worsened(current, previous) {
|
|
3228
|
+
return !previous || current.level > previous.level || (current.magnitude ?? 0) > (previous.magnitude ?? 0) || current.facts.some((fact) => !previous.facts.includes(fact));
|
|
3229
|
+
}
|
|
3230
|
+
function completionFindings(state, findings) {
|
|
3231
|
+
const current = notificationConditions(findings);
|
|
3232
|
+
const cleared = new Set(state.cleared);
|
|
3233
|
+
const resolvedIds = new Set(findings.filter((f) => f.status === "resolved").map((f) => condition(f).id));
|
|
3234
|
+
for (const id of Object.keys(state.baseline)) if (resolvedIds.has(id) && !current[id]) cleared.add(id);
|
|
3235
|
+
state.cleared = [...cleared];
|
|
3236
|
+
const selected = [];
|
|
3237
|
+
const selectedIds = /* @__PURE__ */ new Set();
|
|
3238
|
+
const delivered = { ...state.delivered };
|
|
3239
|
+
for (const finding of findings) {
|
|
3240
|
+
const value = condition(finding);
|
|
3241
|
+
const previous = state.delivered[value.id];
|
|
3242
|
+
value.condition = current[value.id] ?? value.condition;
|
|
3243
|
+
if (finding.status === "unverified" || !finding.current && finding.status !== "resolved") {
|
|
3244
|
+
continue;
|
|
3245
|
+
}
|
|
3246
|
+
if (finding.status === "resolved") {
|
|
3247
|
+
if (!current[value.id]) delete delivered[value.id];
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
3250
|
+
const eligible = cleared.has(value.id) || worsened(value.condition, state.baseline[value.id]);
|
|
3251
|
+
if (!eligible) continue;
|
|
3252
|
+
if (!selectedIds.has(value.id) && worsened(value.condition, previous)) {
|
|
3253
|
+
selectedIds.add(value.id);
|
|
3254
|
+
const source = finding.current;
|
|
3255
|
+
selected.push(source.evidence.kind === "decision-anchor" ? { ...finding, current: { ...source, evidence: { ...source.evidence, changedFiles: value.condition.facts } } } : finding);
|
|
3256
|
+
if (selected.length <= MAX_COMPLETION_FINDINGS) delivered[value.id] = value.condition;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
state.delivered = delivered;
|
|
3260
|
+
return selected;
|
|
3261
|
+
}
|
|
3262
|
+
function completionSummary(findings, reportPath) {
|
|
3263
|
+
if (!findings.length) return null;
|
|
3264
|
+
const safe = (value) => value.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
|
|
3265
|
+
return [
|
|
3266
|
+
`Mason: ${findings.length} new or worsened finding(s) since the retained baseline.`,
|
|
3267
|
+
...findings.slice(0, MAX_COMPLETION_FINDINGS).map((f) => {
|
|
3268
|
+
const source = f.current ?? f.original;
|
|
3269
|
+
const detail = source.evidence.kind === "decision-anchor" ? `Changes to ${source.evidence.changedFiles.join(", ")} may affect decision "${source.evidence.title}". Inspect the diff if relevant; acceptance is not required.` : source.message;
|
|
3270
|
+
return `[${f.status === "unresolved" ? "issue" : "advisory"}] ${safe(source.anchor.doc)}: ${safe(detail)}`;
|
|
3271
|
+
}),
|
|
3272
|
+
...findings.length > MAX_COMPLETION_FINDINGS ? [`${findings.length - MAX_COMPLETION_FINDINGS} more in the report; pending later feedback.`] : [],
|
|
3273
|
+
`Evidence: ${reportPath}. Explicit audit: mason_automation(action: "check").`,
|
|
3274
|
+
"Inspect findings within the authorized work. Concurrent changes may also contribute; this summary does not establish who introduced them."
|
|
3275
|
+
].join("\n");
|
|
3276
|
+
}
|
|
3277
|
+
var conditionSchema, MAX_COMPLETION_FINDINGS, notificationStateSchema;
|
|
3278
|
+
var init_notifications = __esm({
|
|
3279
|
+
"src/automation/notifications.ts"() {
|
|
3280
|
+
"use strict";
|
|
3281
|
+
init_findings();
|
|
3282
|
+
conditionSchema = z5.object({
|
|
3283
|
+
level: z5.number(),
|
|
3284
|
+
facts: z5.array(z5.string()),
|
|
3285
|
+
magnitude: z5.number().optional()
|
|
3286
|
+
});
|
|
3287
|
+
MAX_COMPLETION_FINDINGS = 4;
|
|
3288
|
+
notificationStateSchema = z5.object({
|
|
3289
|
+
version: z5.literal(1),
|
|
3290
|
+
baselineHead: z5.string(),
|
|
3291
|
+
baseline: z5.record(conditionSchema),
|
|
3292
|
+
delivered: z5.record(conditionSchema),
|
|
3293
|
+
cleared: z5.array(z5.string())
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
});
|
|
3297
|
+
|
|
3298
|
+
// src/automation/store.ts
|
|
3299
|
+
import { z as z6 } from "zod";
|
|
3043
3300
|
function parseState(raw) {
|
|
3044
3301
|
try {
|
|
3045
3302
|
return stateSchema.parse(raw);
|
|
@@ -3055,50 +3312,53 @@ var init_store = __esm({
|
|
|
3055
3312
|
"src/automation/store.ts"() {
|
|
3056
3313
|
"use strict";
|
|
3057
3314
|
init_store_lock();
|
|
3058
|
-
|
|
3315
|
+
init_notifications();
|
|
3316
|
+
hostSchema = z6.enum(["claude", "codex"]);
|
|
3059
3317
|
events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
|
|
3060
|
-
stateSchema =
|
|
3061
|
-
version:
|
|
3062
|
-
root:
|
|
3063
|
-
gitDir:
|
|
3064
|
-
branch:
|
|
3065
|
-
baselines:
|
|
3066
|
-
sessions:
|
|
3318
|
+
stateSchema = z6.object({
|
|
3319
|
+
version: z6.literal(1),
|
|
3320
|
+
root: z6.string(),
|
|
3321
|
+
gitDir: z6.string(),
|
|
3322
|
+
branch: z6.string(),
|
|
3323
|
+
baselines: z6.array(z6.object({ path: z6.string(), at: z6.string(), event: z6.string(), fingerprint: z6.string() })).max(128),
|
|
3324
|
+
sessions: z6.record(z6.object({
|
|
3067
3325
|
host: hostSchema,
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
mutationObserved: z5.boolean(),
|
|
3074
|
-
pending: z5.record(z5.string()),
|
|
3075
|
-
coverageGaps: z5.array(z5.string()),
|
|
3076
|
-
events: z5.record(z5.object({ at: z5.string(), count: z5.number().int().positive() }))
|
|
3326
|
+
lastUsed: z6.string(),
|
|
3327
|
+
notifiedDiagnostics: z6.array(z6.string()).default([]),
|
|
3328
|
+
pending: z6.record(z6.string()),
|
|
3329
|
+
coverageGaps: z6.array(z6.string()),
|
|
3330
|
+
events: z6.record(z6.object({ at: z6.string(), count: z6.number().int().positive() }))
|
|
3077
3331
|
})),
|
|
3078
|
-
updatedAt:
|
|
3079
|
-
fingerprint:
|
|
3080
|
-
latest:
|
|
3081
|
-
|
|
3332
|
+
updatedAt: z6.string(),
|
|
3333
|
+
fingerprint: z6.string().nullable(),
|
|
3334
|
+
latest: z6.string().nullable(),
|
|
3335
|
+
notifications: notificationStateSchema.optional(),
|
|
3336
|
+
agentNotifications: notificationStateSchema.optional(),
|
|
3337
|
+
analysis: z6.object({ id: z6.string(), fingerprint: z6.string(), completedAt: z6.number(), path: z6.string() }).optional()
|
|
3082
3338
|
});
|
|
3083
3339
|
}
|
|
3084
3340
|
});
|
|
3085
3341
|
|
|
3086
3342
|
// src/audit/advisory-review.ts
|
|
3087
3343
|
import fs10 from "fs/promises";
|
|
3088
|
-
import { z as
|
|
3344
|
+
import { z as z7 } from "zod";
|
|
3089
3345
|
async function readRecord(root, id) {
|
|
3090
3346
|
const raw = await readStoreJson(root, recordPath(id));
|
|
3091
3347
|
if (raw === null) return null;
|
|
3092
3348
|
const record = recordSchema.parse(raw);
|
|
3093
3349
|
const { digest: stored, ...payload } = record;
|
|
3094
|
-
if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || event.
|
|
3350
|
+
if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || (event.outcome === "dismissed" ? !event.reasonCode || event.finding.type !== "deps-changed" : !event.reviewer || !!event.reasonCode) || event.evidence.fingerprint !== evidenceFingerprint(event.evidence, event.reasonCode))) {
|
|
3095
3351
|
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
3096
3352
|
}
|
|
3097
3353
|
return record;
|
|
3098
3354
|
}
|
|
3099
|
-
function
|
|
3355
|
+
function evidenceFingerprint(evidence, reason2) {
|
|
3356
|
+
return digest([evidence.scope, evidence.tree, ...reason2 === "no-dependency-claims" ? [] : [evidence.lastScopeCommit], evidence.paths]);
|
|
3357
|
+
}
|
|
3358
|
+
function scopes(finding, reason2) {
|
|
3100
3359
|
const files = [finding.anchor.doc], e = finding.evidence;
|
|
3101
3360
|
if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
3361
|
+
if (reason2 === "no-dependency-claims" && e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`];
|
|
3102
3362
|
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
3103
3363
|
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
3104
3364
|
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
@@ -3109,9 +3369,9 @@ function scopes(finding) {
|
|
|
3109
3369
|
return `:(literal)${file}`;
|
|
3110
3370
|
}).sort();
|
|
3111
3371
|
}
|
|
3112
|
-
async function reviewEvidence(root, finding) {
|
|
3372
|
+
async function reviewEvidence(root, finding, reason2) {
|
|
3113
3373
|
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
3114
|
-
const scope = scopes(finding);
|
|
3374
|
+
const scope = scopes(finding, reason2);
|
|
3115
3375
|
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
3116
3376
|
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
3117
3377
|
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
@@ -3137,7 +3397,7 @@ async function reviewEvidence(root, finding) {
|
|
|
3137
3397
|
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
3138
3398
|
}
|
|
3139
3399
|
}
|
|
3140
|
-
const fingerprint =
|
|
3400
|
+
const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason2);
|
|
3141
3401
|
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
3142
3402
|
return {
|
|
3143
3403
|
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
@@ -3184,7 +3444,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
3184
3444
|
const record = await readRecord(root, id);
|
|
3185
3445
|
if (!record) return null;
|
|
3186
3446
|
const event = record.events.at(-1);
|
|
3187
|
-
const current = await reviewEvidence(root, event.finding);
|
|
3447
|
+
const current = await reviewEvidence(root, event.finding, event.reasonCode);
|
|
3188
3448
|
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
3189
3449
|
const base = {
|
|
3190
3450
|
id,
|
|
@@ -3194,7 +3454,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
3194
3454
|
outcome: event.outcome,
|
|
3195
3455
|
reviewedHead: event.evidence.head
|
|
3196
3456
|
};
|
|
3197
|
-
if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding)) !== digest(event.evidence.scope)) return { ...base, status: "reopened", reason: "Relevant document or repository evidence changed after the assessment; prepare a new review." };
|
|
3457
|
+
if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding, event.reasonCode)) !== digest(event.evidence.scope)) return { ...base, status: "reopened", reason: "Relevant document or repository evidence changed after the assessment; prepare a new review." };
|
|
3198
3458
|
return { ...base, status: event.outcome === "deferred" ? "deferred" : "current", reason: event.outcome === "deferred" ? "The review was deferred and remains outstanding." : "An explicit assessment covers the current committed scope. This does not approve an engineering decision or certify correctness." };
|
|
3199
3459
|
} catch (error) {
|
|
3200
3460
|
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -3222,9 +3482,13 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3222
3482
|
decisionId: finding.evidence.decisionId,
|
|
3223
3483
|
hint: "Use review_decision to prepare evidence and record authorized acceptance, reaffirmation or retirement, then verify this original repair baseline. An advisory assessment cannot approve a decision."
|
|
3224
3484
|
};
|
|
3485
|
+
if (request.action === "dismiss") {
|
|
3486
|
+
if (finding.type !== "deps-changed" || finding.evidence.kind !== "doc-behind-manifests") throw new Error("One-step dismissal is only available for dependency advisories.");
|
|
3487
|
+
if (!request.reasonCode || !request.note) throw new Error("Dismissal requires reasonCode and note; it does not require a reviewer or prepared token.");
|
|
3488
|
+
} else if (request.reasonCode) throw new Error("reasonCode is only used with dismiss.");
|
|
3225
3489
|
const prepare = async () => {
|
|
3226
3490
|
const record = await readRecord(root, request.findingId);
|
|
3227
|
-
const evidence = await reviewEvidence(root, finding);
|
|
3491
|
+
const evidence = await reviewEvidence(root, finding, request.reasonCode);
|
|
3228
3492
|
await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
|
|
3229
3493
|
const token = digest({ baseline: baseline.original, finding, evidence, record });
|
|
3230
3494
|
return { record, evidence, token };
|
|
@@ -3253,10 +3517,10 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3253
3517
|
hint: "Inspect the original finding, scoped document and relevant code changes. Record addressed, inapplicable or deferred only when the user or cited project review authorizes that assessment. Supply the actual reviewer, reason in note and reviewToken. Relevant edits must be committed first. Deferral remains outstanding. Review identities are recorded assertions, not authenticated approvals."
|
|
3254
3518
|
};
|
|
3255
3519
|
}
|
|
3256
|
-
if (!request.reviewer || !request.note || !request.reviewToken) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
|
|
3520
|
+
if (request.action !== "dismiss" && (!request.reviewer || !request.note || !request.reviewToken)) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
|
|
3257
3521
|
return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
|
|
3258
3522
|
const state = await prepare();
|
|
3259
|
-
if (state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
|
|
3523
|
+
if (request.action !== "dismiss" && state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
|
|
3260
3524
|
if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
|
|
3261
3525
|
if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
|
|
3262
3526
|
const { dirty, documentDigest, ...evidence } = state.evidence;
|
|
@@ -3264,7 +3528,8 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3264
3528
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3265
3529
|
reviewer: request.reviewer,
|
|
3266
3530
|
note: request.note,
|
|
3267
|
-
outcome: request.action,
|
|
3531
|
+
outcome: request.action === "dismiss" ? "dismissed" : request.action,
|
|
3532
|
+
reasonCode: request.reasonCode,
|
|
3268
3533
|
finding,
|
|
3269
3534
|
evidence
|
|
3270
3535
|
});
|
|
@@ -3275,11 +3540,11 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3275
3540
|
status: "recorded",
|
|
3276
3541
|
recordPath: recordPath(request.findingId),
|
|
3277
3542
|
event,
|
|
3278
|
-
hint: "Review and commit this assessment record through the normal project workflow, then verify the original repair baseline. Relevant changes reopen the assessment; deferral remains outstanding."
|
|
3543
|
+
hint: request.action === "dismiss" ? "Dismissal saved and effective locally. Commit the record to share it. No-dependency-claims dismissals reopen on document content changes; unrelated-manifest-change dismissals reopen on scoped document or manifest changes. This does not approve a decision." : "Review and commit this assessment record through the normal project workflow, then verify the original repair baseline. Relevant changes reopen the assessment; deferral remains outstanding."
|
|
3279
3544
|
};
|
|
3280
3545
|
});
|
|
3281
3546
|
}
|
|
3282
|
-
var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3547
|
+
var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3283
3548
|
var init_advisory_review = __esm({
|
|
3284
3549
|
"src/audit/advisory-review.ts"() {
|
|
3285
3550
|
"use strict";
|
|
@@ -3295,39 +3560,42 @@ var init_advisory_review = __esm({
|
|
|
3295
3560
|
init_decisions();
|
|
3296
3561
|
init_drift2();
|
|
3297
3562
|
git2 = async (root, ...args) => (await (["log", "ls-tree", "ls-files", "merge-base"].includes(args[0]) ? inspectionGit : execGit)(args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
3298
|
-
hashSchema =
|
|
3299
|
-
commitSchema2 =
|
|
3300
|
-
evidenceSchema2 =
|
|
3563
|
+
hashSchema = z7.string().regex(/^[a-f0-9]{64}$/);
|
|
3564
|
+
commitSchema2 = z7.string().regex(/^[a-f0-9]{40,64}$/);
|
|
3565
|
+
evidenceSchema2 = z7.object({
|
|
3301
3566
|
head: commitSchema2,
|
|
3302
3567
|
fingerprint: hashSchema,
|
|
3303
|
-
scope:
|
|
3304
|
-
tree:
|
|
3568
|
+
scope: z7.array(z7.string()).min(1).max(1e3),
|
|
3569
|
+
tree: z7.string().max(2 * 1024 * 1024),
|
|
3305
3570
|
lastScopeCommit: commitSchema2,
|
|
3306
|
-
paths:
|
|
3571
|
+
paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
|
|
3307
3572
|
});
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3573
|
+
dismissalReasonSchema = z7.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
3574
|
+
eventSchema2 = z7.object({
|
|
3575
|
+
at: z7.string().datetime(),
|
|
3576
|
+
reviewer: z7.string().trim().min(1).max(200).optional(),
|
|
3577
|
+
note: z7.string().trim().min(1).max(2500),
|
|
3578
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
3579
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
3313
3580
|
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
3314
3581
|
evidence: evidenceSchema2
|
|
3315
3582
|
});
|
|
3316
|
-
recordSchema =
|
|
3317
|
-
reviewSummarySchema =
|
|
3583
|
+
recordSchema = z7.object({ version: z7.literal(1), id: hashSchema, events: z7.array(eventSchema2).min(1).max(200), digest: hashSchema });
|
|
3584
|
+
reviewSummarySchema = z7.object({
|
|
3318
3585
|
id: hashSchema,
|
|
3319
|
-
status:
|
|
3320
|
-
reason:
|
|
3321
|
-
recordPath:
|
|
3322
|
-
reviewer:
|
|
3323
|
-
note:
|
|
3324
|
-
outcome:
|
|
3325
|
-
reviewedHead:
|
|
3586
|
+
status: z7.enum(["current", "reopened", "deferred", "unverified"]),
|
|
3587
|
+
reason: z7.string(),
|
|
3588
|
+
recordPath: z7.string().optional(),
|
|
3589
|
+
reviewer: z7.string().optional(),
|
|
3590
|
+
note: z7.string().optional(),
|
|
3591
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
|
|
3592
|
+
reviewedHead: z7.string().optional()
|
|
3326
3593
|
});
|
|
3327
|
-
advisoryReviewRequest =
|
|
3328
|
-
baselinePath:
|
|
3594
|
+
advisoryReviewRequest = z7.object({
|
|
3595
|
+
baselinePath: z7.string().min(1),
|
|
3329
3596
|
findingId: hashSchema,
|
|
3330
|
-
action:
|
|
3597
|
+
action: z7.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
3598
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
3331
3599
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
3332
3600
|
note: eventSchema2.shape.note.optional(),
|
|
3333
3601
|
reviewToken: hashSchema.optional()
|
|
@@ -3338,12 +3606,12 @@ var init_advisory_review = __esm({
|
|
|
3338
3606
|
|
|
3339
3607
|
// src/audit/audit.ts
|
|
3340
3608
|
import fs11 from "fs/promises";
|
|
3341
|
-
import
|
|
3609
|
+
import path20 from "path";
|
|
3342
3610
|
async function computeAudit(rootDir, options = {}) {
|
|
3343
3611
|
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
3344
3612
|
}
|
|
3345
3613
|
async function auditCurrent(rootDir, options) {
|
|
3346
|
-
const resolvedRoot =
|
|
3614
|
+
const resolvedRoot = path20.resolve(rootDir);
|
|
3347
3615
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
3348
3616
|
if (docs.length === 0) return null;
|
|
3349
3617
|
const report = {
|
|
@@ -3377,7 +3645,7 @@ async function auditCurrent(rootDir, options) {
|
|
|
3377
3645
|
}
|
|
3378
3646
|
let decisionsPresent = false;
|
|
3379
3647
|
try {
|
|
3380
|
-
await fs11.access(
|
|
3648
|
+
await fs11.access(path20.join(resolvedRoot, ".mason", "decisions"));
|
|
3381
3649
|
decisionsPresent = true;
|
|
3382
3650
|
} catch {
|
|
3383
3651
|
}
|
|
@@ -3389,13 +3657,21 @@ async function auditCurrent(rootDir, options) {
|
|
|
3389
3657
|
changesSinceDoc,
|
|
3390
3658
|
decisionsPresent
|
|
3391
3659
|
};
|
|
3660
|
+
const policy = (await loadProjectConfig(resolvedRoot)).audit;
|
|
3392
3661
|
const selected = options.checks ?? ALL_CHECKS;
|
|
3393
3662
|
for (const name of ALL_CHECKS) {
|
|
3394
3663
|
if (!selected.includes(name)) continue;
|
|
3395
|
-
const
|
|
3664
|
+
const exclusions = policy?.exclude?.[name] ?? [];
|
|
3665
|
+
const included = await includedCheckPaths(resolvedRoot, docs.map((doc) => doc.path), exclusions);
|
|
3666
|
+
const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
|
|
3667
|
+
if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
|
|
3668
|
+
const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
|
|
3669
|
+
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
|
|
3396
3670
|
report.checksRun.push(name);
|
|
3397
3671
|
report.issues.push(...issues);
|
|
3398
|
-
|
|
3672
|
+
const advisoryPaths = await includedCheckPaths(resolvedRoot, [...new Set(advisories.map((f) => f.anchor.doc))], exclusions);
|
|
3673
|
+
report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
|
|
3674
|
+
for (const f of advisories) if (!advisoryPaths.has(f.anchor.doc)) (report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
|
|
3399
3675
|
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
3400
3676
|
report.skippedChecks.push(...skipped);
|
|
3401
3677
|
}
|
|
@@ -3407,6 +3683,8 @@ async function auditCurrent(rootDir, options) {
|
|
|
3407
3683
|
var init_audit = __esm({
|
|
3408
3684
|
"src/audit/audit.ts"() {
|
|
3409
3685
|
"use strict";
|
|
3686
|
+
init_files();
|
|
3687
|
+
init_policy();
|
|
3410
3688
|
init_profile();
|
|
3411
3689
|
init_drift();
|
|
3412
3690
|
init_snapshot();
|
|
@@ -3432,9 +3710,9 @@ __export(repair_exports, {
|
|
|
3432
3710
|
withRepairSession: () => withRepairSession
|
|
3433
3711
|
});
|
|
3434
3712
|
import fs12 from "fs/promises";
|
|
3435
|
-
import
|
|
3713
|
+
import path21 from "path";
|
|
3436
3714
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3437
|
-
import { z as
|
|
3715
|
+
import { z as z8 } from "zod";
|
|
3438
3716
|
function recheckable(finding) {
|
|
3439
3717
|
return !("confidence" in finding) && finding.resolution === "recheck" && ["deleted-reference", "dead-command", "new-module"].includes(finding.type) && ["missing-path", "missing-script", "unmentioned-dir"].includes(finding.evidence.kind);
|
|
3440
3718
|
}
|
|
@@ -3453,7 +3731,7 @@ async function docState(root) {
|
|
|
3453
3731
|
docs.push([doc, null]);
|
|
3454
3732
|
}
|
|
3455
3733
|
}
|
|
3456
|
-
return digest(docs);
|
|
3734
|
+
return digest([docs, (await loadProjectConfig(root)).audit]);
|
|
3457
3735
|
}
|
|
3458
3736
|
async function stableAudit(root, checks, options = {}) {
|
|
3459
3737
|
return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
|
|
@@ -3505,7 +3783,7 @@ async function withRepairSession(root, options, run, validate, cachedAudit) {
|
|
|
3505
3783
|
}
|
|
3506
3784
|
async function prepareOriginal(rootDir, checks, options, session) {
|
|
3507
3785
|
const root = await fs12.realpath(rootDir);
|
|
3508
|
-
const selected =
|
|
3786
|
+
const selected = z8.array(checkSchema).nonempty().parse(checks);
|
|
3509
3787
|
const report = await (session ? session.audit(selected) : stableAudit(root, selected, options));
|
|
3510
3788
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
3511
3789
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
@@ -3522,8 +3800,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
|
|
|
3522
3800
|
}
|
|
3523
3801
|
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
3524
3802
|
const root = await fs12.realpath(rootDir);
|
|
3525
|
-
const declaredRoot =
|
|
3526
|
-
const relative =
|
|
3803
|
+
const declaredRoot = path21.resolve(rootDir);
|
|
3804
|
+
const relative = path21.isAbsolute(baselinePath) ? path21.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
3527
3805
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
3528
3806
|
const { digest: savedDigest, ...payload } = stored;
|
|
3529
3807
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
@@ -3572,6 +3850,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
3572
3850
|
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
3573
3851
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
3574
3852
|
}
|
|
3853
|
+
if (current.excludedChecks?.some((excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc)) {
|
|
3854
|
+
return { ...base, status: "resolved", reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained." };
|
|
3855
|
+
}
|
|
3575
3856
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
3576
3857
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
3577
3858
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
@@ -3582,6 +3863,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
3582
3863
|
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
3583
3864
|
return { ...base, review, status: "review-required", reason: review.reason };
|
|
3584
3865
|
}
|
|
3866
|
+
if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
|
|
3867
|
+
return { ...base, status: "resolved", reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required." };
|
|
3868
|
+
}
|
|
3585
3869
|
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
3586
3870
|
return {
|
|
3587
3871
|
...base,
|
|
@@ -3610,7 +3894,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
3610
3894
|
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
3611
3895
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
3612
3896
|
for (const f of findings) counts[f.status]++;
|
|
3613
|
-
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f) && !reviewedIds.has(findingId(f)));
|
|
3897
|
+
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f) && f.resolution !== "informational" && !reviewedIds.has(findingId(f)));
|
|
3614
3898
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
3615
3899
|
return {
|
|
3616
3900
|
version: 1,
|
|
@@ -3633,7 +3917,7 @@ function repairExitCode(report) {
|
|
|
3633
3917
|
function formatRepairSummary(report) {
|
|
3634
3918
|
return [
|
|
3635
3919
|
"Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
|
|
3636
|
-
...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason + "\n findingId: " + f.id),
|
|
3920
|
+
...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + (f.current?.message ?? "Historical evidence at " + report.baselineHead.slice(0, 7) + ": " + f.original.message) + "\n " + f.reason + "\n findingId: " + f.id),
|
|
3637
3921
|
...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
|
|
3638
3922
|
...report.diagnostics.map((d) => " [unverified] " + d),
|
|
3639
3923
|
...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
|
|
@@ -3656,41 +3940,42 @@ var init_repair = __esm({
|
|
|
3656
3940
|
init_findings();
|
|
3657
3941
|
init_advisory_review();
|
|
3658
3942
|
init_findings();
|
|
3659
|
-
count2 =
|
|
3660
|
-
reportSchema =
|
|
3661
|
-
version:
|
|
3662
|
-
root:
|
|
3663
|
-
gitAvailable:
|
|
3943
|
+
count2 = z8.number().int().nonnegative();
|
|
3944
|
+
reportSchema = z8.object({
|
|
3945
|
+
version: z8.literal(1),
|
|
3946
|
+
root: z8.string(),
|
|
3947
|
+
gitAvailable: z8.literal(true),
|
|
3664
3948
|
headHash: commitSchema.shape.hash,
|
|
3665
|
-
checksRun:
|
|
3666
|
-
docs:
|
|
3667
|
-
path:
|
|
3668
|
-
scope:
|
|
3669
|
-
kind:
|
|
3949
|
+
checksRun: z8.array(checkSchema).nonempty(),
|
|
3950
|
+
docs: z8.array(z8.object({
|
|
3951
|
+
path: z8.string().refine((value) => normalizeRepoPath(value) === value),
|
|
3952
|
+
scope: z8.string().optional(),
|
|
3953
|
+
kind: z8.enum(["instructions", "readme", "guide"]).optional(),
|
|
3670
3954
|
lastCommit: commitSchema.nullable(),
|
|
3671
|
-
dirty:
|
|
3955
|
+
dirty: z8.boolean(),
|
|
3672
3956
|
lineCount: count2
|
|
3673
3957
|
})).nonempty(),
|
|
3674
|
-
decisionsChecked:
|
|
3675
|
-
clean:
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3958
|
+
decisionsChecked: z8.boolean(),
|
|
3959
|
+
clean: z8.boolean(),
|
|
3960
|
+
excludedChecks: z8.array(z8.object({ check: checkSchema, doc: z8.string() })).optional(),
|
|
3961
|
+
issues: z8.array(issueSchema),
|
|
3962
|
+
advisories: z8.array(advisorySchema),
|
|
3963
|
+
suppressedAdvisories: z8.array(advisorySchema).optional(),
|
|
3964
|
+
skippedChecks: z8.array(z8.object({ check: z8.string(), reason: z8.string(), doc: z8.string().optional() }))
|
|
3680
3965
|
});
|
|
3681
|
-
baselineSchema =
|
|
3682
|
-
kind:
|
|
3683
|
-
version:
|
|
3684
|
-
createdAt:
|
|
3966
|
+
baselineSchema = z8.object({
|
|
3967
|
+
kind: z8.literal("mason-audit-repair"),
|
|
3968
|
+
version: z8.literal(1),
|
|
3969
|
+
createdAt: z8.string().datetime(),
|
|
3685
3970
|
report: reportSchema,
|
|
3686
|
-
digest:
|
|
3971
|
+
digest: z8.string().regex(/^[a-f0-9]{64}$/)
|
|
3687
3972
|
});
|
|
3688
|
-
repairFindingSchema =
|
|
3689
|
-
id:
|
|
3690
|
-
original:
|
|
3691
|
-
status:
|
|
3692
|
-
reason:
|
|
3693
|
-
current:
|
|
3973
|
+
repairFindingSchema = z8.object({
|
|
3974
|
+
id: z8.string(),
|
|
3975
|
+
original: z8.union([issueSchema, advisorySchema]),
|
|
3976
|
+
status: z8.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
3977
|
+
reason: z8.string(),
|
|
3978
|
+
current: z8.union([issueSchema, advisorySchema]).optional(),
|
|
3694
3979
|
review: reviewSummarySchema.optional()
|
|
3695
3980
|
});
|
|
3696
3981
|
}
|
|
@@ -3698,10 +3983,10 @@ var init_repair = __esm({
|
|
|
3698
3983
|
|
|
3699
3984
|
// src/automation/evidence.ts
|
|
3700
3985
|
import fs13 from "fs/promises";
|
|
3701
|
-
import
|
|
3986
|
+
import path22 from "path";
|
|
3702
3987
|
import { createHash as createHash3 } from "crypto";
|
|
3703
|
-
import
|
|
3704
|
-
import { z as
|
|
3988
|
+
import fg7 from "fast-glob";
|
|
3989
|
+
import { z as z9 } from "zod";
|
|
3705
3990
|
async function git3(root, ...args) {
|
|
3706
3991
|
return profilePhase("automation.git", async () => (await execGit(args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
|
|
3707
3992
|
}
|
|
@@ -3718,6 +4003,11 @@ async function readInputs(root) {
|
|
|
3718
4003
|
}
|
|
3719
4004
|
async function collectInputs(root) {
|
|
3720
4005
|
const docPaths = await discoverDocPaths(root);
|
|
4006
|
+
const auditPolicy = (await loadProjectConfig(root)).audit;
|
|
4007
|
+
const allowed = /* @__PURE__ */ new Map();
|
|
4008
|
+
for (const name of ["deleted-reference", "new-module", "stale-count", "dead-command"]) {
|
|
4009
|
+
allowed.set(name, await includedCheckPaths(root, docPaths, auditPolicy?.exclude?.[name] ?? []));
|
|
4010
|
+
}
|
|
3721
4011
|
const [headText, docStatus, shallowPath, replacements] = await Promise.all([
|
|
3722
4012
|
git3(root, "rev-parse", "HEAD"),
|
|
3723
4013
|
docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
|
|
@@ -3727,7 +4017,7 @@ async function collectInputs(root) {
|
|
|
3727
4017
|
const head = headText.trim();
|
|
3728
4018
|
let shallow = null;
|
|
3729
4019
|
try {
|
|
3730
|
-
shallow = await fs13.readFile(
|
|
4020
|
+
shallow = await fs13.readFile(path22.resolve(root, shallowPath.trim()), "utf8");
|
|
3731
4021
|
} catch (error) {
|
|
3732
4022
|
if (error.code !== "ENOENT") throw error;
|
|
3733
4023
|
}
|
|
@@ -3746,18 +4036,18 @@ async function collectInputs(root) {
|
|
|
3746
4036
|
docContents.push([file, text2]);
|
|
3747
4037
|
const parsed = { path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") };
|
|
3748
4038
|
parsedDocs.push(parsed);
|
|
3749
|
-
for (const claim of parsed.claims.paths) {
|
|
4039
|
+
for (const claim of allowed.get("deleted-reference").has(file) ? parsed.claims.paths : []) {
|
|
3750
4040
|
const scope = pathClaimScope(file, claim);
|
|
3751
4041
|
if (scope?.candidates.some(gitMetadataPath)) continue;
|
|
3752
4042
|
for (const candidate of scope?.candidates ?? []) {
|
|
3753
4043
|
if (optionalMasonPath(candidate)) continue;
|
|
3754
|
-
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(
|
|
4044
|
+
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path22.posix.dirname(candidate))]);
|
|
3755
4045
|
claims.push([candidate, filePresent, parentPresent]);
|
|
3756
4046
|
}
|
|
3757
4047
|
}
|
|
3758
4048
|
}
|
|
3759
|
-
const combinedDocs = moduleDocumentation(parsedDocs);
|
|
3760
|
-
const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
|
|
4049
|
+
const combinedDocs = moduleDocumentation(parsedDocs.filter((doc) => allowed.get("new-module").has(doc.path)));
|
|
4050
|
+
const countClaims = parsedDocs.filter((doc) => allowed.get("stale-count").has(doc.path)).flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
|
|
3761
4051
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
3762
4052
|
const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
3763
4053
|
if (error.code === "ENOENT") return "absent";
|
|
@@ -3766,8 +4056,8 @@ async function collectInputs(root) {
|
|
|
3766
4056
|
const [modules, counts, commands2, decisionFiles] = await Promise.all([
|
|
3767
4057
|
combinedDocs ? moduleCandidates(root, combinedDocs) : [],
|
|
3768
4058
|
Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
|
|
3769
|
-
commandInputs(root, parsedDocs),
|
|
3770
|
-
|
|
4059
|
+
commandInputs(root, parsedDocs.filter((doc) => allowed.get("dead-command").has(doc.path))),
|
|
4060
|
+
fg7(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
|
|
3771
4061
|
]);
|
|
3772
4062
|
const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
|
|
3773
4063
|
await storePath(root, file);
|
|
@@ -3777,7 +4067,7 @@ async function collectInputs(root) {
|
|
|
3777
4067
|
git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
3778
4068
|
git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
3779
4069
|
]) : ["", ""];
|
|
3780
|
-
const common = [
|
|
4070
|
+
const common = [7, engineVersion, head, shallow, replacements, docContents, auditPolicy];
|
|
3781
4071
|
const keys = {
|
|
3782
4072
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
3783
4073
|
"new-module": hash([common, modules]),
|
|
@@ -3824,6 +4114,8 @@ var engineVersion, hash, content, cacheSchema;
|
|
|
3824
4114
|
var init_evidence = __esm({
|
|
3825
4115
|
"src/automation/evidence.ts"() {
|
|
3826
4116
|
"use strict";
|
|
4117
|
+
init_policy();
|
|
4118
|
+
init_files();
|
|
3827
4119
|
init_git_read();
|
|
3828
4120
|
init_profile();
|
|
3829
4121
|
init_docs();
|
|
@@ -3837,10 +4129,10 @@ var init_evidence = __esm({
|
|
|
3837
4129
|
init_dead_command();
|
|
3838
4130
|
init_storage();
|
|
3839
4131
|
init_advisory_review();
|
|
3840
|
-
engineVersion = true ? "0.19.
|
|
4132
|
+
engineVersion = true ? "0.19.2" : "development";
|
|
3841
4133
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3842
4134
|
content = readAuditInput;
|
|
3843
|
-
cacheSchema =
|
|
4135
|
+
cacheSchema = z9.object({ version: z9.literal(1), entries: z9.record(z9.object({ key: z9.string(), result: checkResultSchema })), digest: z9.string() });
|
|
3844
4136
|
}
|
|
3845
4137
|
});
|
|
3846
4138
|
|
|
@@ -3856,9 +4148,9 @@ __export(model_exports, {
|
|
|
3856
4148
|
setupHostSchema: () => setupHostSchema,
|
|
3857
4149
|
setupSchema: () => setupSchema
|
|
3858
4150
|
});
|
|
3859
|
-
import { z as
|
|
4151
|
+
import { z as z10 } from "zod";
|
|
3860
4152
|
import fs14 from "fs/promises";
|
|
3861
|
-
import
|
|
4153
|
+
import path23 from "path";
|
|
3862
4154
|
async function loadSetup(root) {
|
|
3863
4155
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
3864
4156
|
if (raw === null) return null;
|
|
@@ -3867,7 +4159,7 @@ async function loadSetup(root) {
|
|
|
3867
4159
|
async function effectiveSetup(root) {
|
|
3868
4160
|
const local = await loadSetup(root);
|
|
3869
4161
|
if (local) return { root, setup: local };
|
|
3870
|
-
const common = await fs14.realpath(
|
|
4162
|
+
const common = await fs14.realpath(path23.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
|
|
3871
4163
|
const own = await fs14.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
3872
4164
|
if (common === own) return { root, setup: null };
|
|
3873
4165
|
const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
|
|
@@ -3880,7 +4172,7 @@ async function effectiveSetup(root) {
|
|
|
3880
4172
|
if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
|
|
3881
4173
|
throw error;
|
|
3882
4174
|
}
|
|
3883
|
-
const inheritedCommon = await fs14.realpath(
|
|
4175
|
+
const inheritedCommon = await fs14.realpath(path23.resolve(
|
|
3884
4176
|
inheritedRoot,
|
|
3885
4177
|
(await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
|
|
3886
4178
|
));
|
|
@@ -3903,29 +4195,29 @@ var init_model = __esm({
|
|
|
3903
4195
|
"use strict";
|
|
3904
4196
|
init_evidence();
|
|
3905
4197
|
init_storage();
|
|
3906
|
-
setupHostSchema =
|
|
3907
|
-
configuredVersion:
|
|
3908
|
-
revision:
|
|
3909
|
-
fingerprint:
|
|
3910
|
-
mcpFingerprint:
|
|
3911
|
-
instructions:
|
|
4198
|
+
setupHostSchema = z10.object({
|
|
4199
|
+
configuredVersion: z10.string(),
|
|
4200
|
+
revision: z10.string().uuid(),
|
|
4201
|
+
fingerprint: z10.string(),
|
|
4202
|
+
mcpFingerprint: z10.string(),
|
|
4203
|
+
instructions: z10.array(z10.string())
|
|
3912
4204
|
});
|
|
3913
|
-
setupSchema =
|
|
4205
|
+
setupSchema = z10.object({ version: z10.literal(2), hosts: z10.object({
|
|
3914
4206
|
codex: setupHostSchema.optional(),
|
|
3915
4207
|
claude: setupHostSchema.optional()
|
|
3916
4208
|
}) });
|
|
3917
4209
|
SETUP_PATH = ".mason/local/setup.json";
|
|
3918
4210
|
INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
|
|
3919
|
-
inactiveHostsSchema =
|
|
3920
|
-
receiptSchema =
|
|
3921
|
-
version:
|
|
3922
|
-
host:
|
|
3923
|
-
status:
|
|
3924
|
-
initialReportPath:
|
|
3925
|
-
initialBaselinePaths:
|
|
3926
|
-
root:
|
|
3927
|
-
revision:
|
|
3928
|
-
configuredAt:
|
|
4211
|
+
inactiveHostsSchema = z10.object({ version: z10.literal(1), hosts: z10.array(z10.enum(["claude", "codex"])) });
|
|
4212
|
+
receiptSchema = z10.object({
|
|
4213
|
+
version: z10.literal(1),
|
|
4214
|
+
host: z10.enum(["codex", "claude"]),
|
|
4215
|
+
status: z10.enum(["installing", "configured"]),
|
|
4216
|
+
initialReportPath: z10.string(),
|
|
4217
|
+
initialBaselinePaths: z10.array(z10.string()),
|
|
4218
|
+
root: z10.string(),
|
|
4219
|
+
revision: z10.string().uuid(),
|
|
4220
|
+
configuredAt: z10.string().optional()
|
|
3929
4221
|
});
|
|
3930
4222
|
}
|
|
3931
4223
|
});
|
|
@@ -3963,7 +4255,7 @@ var init_launcher = __esm({
|
|
|
3963
4255
|
"use strict";
|
|
3964
4256
|
init_evidence();
|
|
3965
4257
|
init_model();
|
|
3966
|
-
executingVersion = () => true ? "0.19.
|
|
4258
|
+
executingVersion = () => true ? "0.19.2" : "development";
|
|
3967
4259
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3968
4260
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3969
4261
|
}
|
|
@@ -4089,7 +4381,7 @@ __export(files_exports, {
|
|
|
4089
4381
|
readText: () => readText
|
|
4090
4382
|
});
|
|
4091
4383
|
import fs18 from "fs/promises";
|
|
4092
|
-
import
|
|
4384
|
+
import path27 from "path";
|
|
4093
4385
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4094
4386
|
async function readText(root, file) {
|
|
4095
4387
|
try {
|
|
@@ -4122,7 +4414,7 @@ async function applyEdit(root, edit) {
|
|
|
4122
4414
|
return true;
|
|
4123
4415
|
}
|
|
4124
4416
|
const file = await storePath(root, edit.path, true);
|
|
4125
|
-
const temporary =
|
|
4417
|
+
const temporary = path27.join(path27.dirname(file), ".mason-setup-" + randomUUID3() + ".tmp");
|
|
4126
4418
|
try {
|
|
4127
4419
|
const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
|
|
4128
4420
|
const handle = await fs18.open(temporary, "wx", mode);
|
|
@@ -4160,7 +4452,7 @@ __export(execution_exports, {
|
|
|
4160
4452
|
import os2 from "os";
|
|
4161
4453
|
import fs19 from "fs/promises";
|
|
4162
4454
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
4163
|
-
import { z as
|
|
4455
|
+
import { z as z13 } from "zod";
|
|
4164
4456
|
function parseExecution(raw) {
|
|
4165
4457
|
try {
|
|
4166
4458
|
return executionSchema.parse(raw);
|
|
@@ -4178,7 +4470,7 @@ function automationFailure(error) {
|
|
|
4178
4470
|
codes.add(String(cause.code));
|
|
4179
4471
|
cause = cause.cause;
|
|
4180
4472
|
}
|
|
4181
|
-
const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof
|
|
4473
|
+
const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z13.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
|
|
4182
4474
|
return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
|
|
4183
4475
|
}
|
|
4184
4476
|
function failureMessage(error) {
|
|
@@ -4307,38 +4599,100 @@ var init_execution = __esm({
|
|
|
4307
4599
|
init_store();
|
|
4308
4600
|
init_evidence();
|
|
4309
4601
|
init_storage();
|
|
4310
|
-
failureSchema =
|
|
4311
|
-
code:
|
|
4312
|
-
message:
|
|
4313
|
-
retryable:
|
|
4314
|
-
receiptRecorded:
|
|
4602
|
+
failureSchema = z13.object({
|
|
4603
|
+
code: z13.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
|
|
4604
|
+
message: z13.string(),
|
|
4605
|
+
retryable: z13.boolean(),
|
|
4606
|
+
receiptRecorded: z13.boolean()
|
|
4315
4607
|
});
|
|
4316
|
-
attemptSchema =
|
|
4317
|
-
id:
|
|
4318
|
-
lease:
|
|
4319
|
-
event:
|
|
4320
|
-
startedAt:
|
|
4321
|
-
finishedAt:
|
|
4322
|
-
durationMs:
|
|
4323
|
-
pid:
|
|
4324
|
-
host:
|
|
4325
|
-
status:
|
|
4326
|
-
verificationStatus:
|
|
4327
|
-
reportPath:
|
|
4608
|
+
attemptSchema = z13.object({
|
|
4609
|
+
id: z13.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
4610
|
+
lease: z13.literal(true).optional(),
|
|
4611
|
+
event: z13.string(),
|
|
4612
|
+
startedAt: z13.string(),
|
|
4613
|
+
finishedAt: z13.string().optional(),
|
|
4614
|
+
durationMs: z13.number().nonnegative().optional(),
|
|
4615
|
+
pid: z13.number().int().positive(),
|
|
4616
|
+
host: z13.string(),
|
|
4617
|
+
status: z13.enum(["running", "completed", "failed", "unknown"]),
|
|
4618
|
+
verificationStatus: z13.string().optional(),
|
|
4619
|
+
reportPath: z13.string().optional(),
|
|
4328
4620
|
failure: failureSchema.optional()
|
|
4329
4621
|
});
|
|
4330
|
-
executionSchema =
|
|
4331
|
-
notificationsSchema =
|
|
4332
|
-
signature:
|
|
4622
|
+
executionSchema = z13.object({ version: z13.literal(1), attempts: z13.array(attemptSchema).max(160), discardedAttempts: z13.number().int().nonnegative().default(0) });
|
|
4623
|
+
notificationsSchema = z13.object({ version: z13.literal(1), sessions: z13.record(z13.object({
|
|
4624
|
+
signature: z13.string(),
|
|
4333
4625
|
code: failureSchema.shape.code,
|
|
4334
|
-
message:
|
|
4335
|
-
count:
|
|
4336
|
-
lastAt:
|
|
4626
|
+
message: z13.string(),
|
|
4627
|
+
count: z13.number().int().positive(),
|
|
4628
|
+
lastAt: z13.string()
|
|
4337
4629
|
})) });
|
|
4338
4630
|
leasePath = (directory, id) => `${directory}/executions/${id}.json`;
|
|
4339
4631
|
}
|
|
4340
4632
|
});
|
|
4341
4633
|
|
|
4634
|
+
// src/decisions/pending.ts
|
|
4635
|
+
var pending_exports = {};
|
|
4636
|
+
__export(pending_exports, {
|
|
4637
|
+
pendingKnowledge: () => pendingKnowledge
|
|
4638
|
+
});
|
|
4639
|
+
async function pendingKnowledge(root, now = Date.now()) {
|
|
4640
|
+
try {
|
|
4641
|
+
const [store, status] = await Promise.all([
|
|
4642
|
+
loadDecisionStore(root),
|
|
4643
|
+
execGit(
|
|
4644
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".mason/decisions"],
|
|
4645
|
+
{ cwd: root, timeout: 1e4, maxBuffer: 1024 * 1024 }
|
|
4646
|
+
)
|
|
4647
|
+
]);
|
|
4648
|
+
const changed = /* @__PURE__ */ new Set();
|
|
4649
|
+
const entries = status.stdout.split("\0");
|
|
4650
|
+
for (let i = 0; i < entries.length; i++) {
|
|
4651
|
+
const entry = entries[i];
|
|
4652
|
+
if (!entry) continue;
|
|
4653
|
+
const file = entry.slice(3);
|
|
4654
|
+
if (/^\.mason\/decisions\/[^/]+\.json$/.test(file)) changed.add(file);
|
|
4655
|
+
if (/[RC]/.test(entry.slice(0, 2))) i++;
|
|
4656
|
+
}
|
|
4657
|
+
const records = new Map(store.records.map((record) => [`.mason/decisions/${record.id}.json`, record]));
|
|
4658
|
+
const age = (at) => {
|
|
4659
|
+
const time = at ? Date.parse(at) : NaN;
|
|
4660
|
+
return Number.isFinite(time) ? `${Math.max(0, Math.floor((now - time) / 864e5))} days` : "unknown age";
|
|
4661
|
+
};
|
|
4662
|
+
const safe = (value) => value.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ");
|
|
4663
|
+
const list = (items) => items.slice(0, 5).map(safe).join("; ") + (items.length > 5 ? `; ${items.length - 5} more` : "");
|
|
4664
|
+
const proposals = store.records.filter((record) => record.status === "active" && decisionApproval(record) === "proposed").map((record) => {
|
|
4665
|
+
const history = record.version === 2 ? record.history : [];
|
|
4666
|
+
let lastReview = -1;
|
|
4667
|
+
history.forEach((event, index2) => {
|
|
4668
|
+
if (event.kind === "accepted" || event.kind === "reaffirmed") lastReview = index2;
|
|
4669
|
+
});
|
|
4670
|
+
const since = history.slice(lastReview + 1).find((event) => event.kind === "created" || event.kind === "revised")?.at ?? record.createdAt;
|
|
4671
|
+
return { record, since };
|
|
4672
|
+
}).sort((a, b) => {
|
|
4673
|
+
const timestamp = (value) => Number.isFinite(Date.parse(value)) ? Date.parse(value) : Infinity;
|
|
4674
|
+
return timestamp(a.since) - timestamp(b.since) || a.record.id.localeCompare(b.record.id);
|
|
4675
|
+
});
|
|
4676
|
+
const lines = [];
|
|
4677
|
+
if (changed.size) lines.push(`${changed.size} uncommitted decision record(s): ${list([...changed].sort().map((file) => `${file} (saved-record age: ${age(records.get(file)?.updatedAt)})`))}.`);
|
|
4678
|
+
if (proposals.length) lines.push(`${proposals.length} proposal(s) awaiting review (oldest first): ${list(proposals.map(({ record, since }) => `.mason/decisions/${record.id}.json (${age(since)})`))}.`);
|
|
4679
|
+
if (store.diagnostics.length) lines.push(`${store.diagnostics.length} decision record(s) could not be read; pending knowledge summary is incomplete.`);
|
|
4680
|
+
return lines.length ? `Mason pending knowledge:
|
|
4681
|
+
${lines.join("\n")}
|
|
4682
|
+
Informational only; review and commit through the normal project workflow. No approval is implied.` : null;
|
|
4683
|
+
} catch {
|
|
4684
|
+
return "Mason pending knowledge: unavailable; decision records or Git status could not be read.";
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
var init_pending = __esm({
|
|
4688
|
+
"src/decisions/pending.ts"() {
|
|
4689
|
+
"use strict";
|
|
4690
|
+
init_git_read();
|
|
4691
|
+
init_decisions();
|
|
4692
|
+
init_provenance();
|
|
4693
|
+
}
|
|
4694
|
+
});
|
|
4695
|
+
|
|
4342
4696
|
// src/automation/runtime.ts
|
|
4343
4697
|
var runtime_exports = {};
|
|
4344
4698
|
__export(runtime_exports, {
|
|
@@ -4348,7 +4702,7 @@ __export(runtime_exports, {
|
|
|
4348
4702
|
summarize: () => summarize
|
|
4349
4703
|
});
|
|
4350
4704
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
4351
|
-
import { z as
|
|
4705
|
+
import { z as z14 } from "zod";
|
|
4352
4706
|
async function observeReadOnlyTool(dir, event) {
|
|
4353
4707
|
const ws = await profilePhase("automation.workspace", () => workspace(dir));
|
|
4354
4708
|
await withLock(ws.root, ws.directory, async () => {
|
|
@@ -4570,6 +4924,19 @@ async function automate(dir, event) {
|
|
|
4570
4924
|
const state = await loadState(ws);
|
|
4571
4925
|
if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
|
|
4572
4926
|
const report = structuredClone(result.analysis.report);
|
|
4927
|
+
if (!state.notifications) {
|
|
4928
|
+
let baseline = { findings: report.findings, head: report.head };
|
|
4929
|
+
if (state.latest) {
|
|
4930
|
+
try {
|
|
4931
|
+
const previous = z14.object({ findings: z14.array(repairFindingSchema), head: z14.string() }).safeParse(await readStoreJson(ws.root, state.latest));
|
|
4932
|
+
if (!previous.success) throw new Error("Unsupported saved report");
|
|
4933
|
+
baseline = previous.data;
|
|
4934
|
+
} catch {
|
|
4935
|
+
report.diagnostics.push("The saved notification baseline could not be read; initialized from the current audit. Original repair evidence is retained. Run an explicit check for all outstanding findings.");
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
state.notifications = createNotificationState(baseline.findings, baseline.head);
|
|
4939
|
+
}
|
|
4573
4940
|
if (result.shared) report.checks = { ...report.checks, ran: [], reused: [.../* @__PURE__ */ new Set([...report.checks.ran, ...report.checks.reused])], shared: true };
|
|
4574
4941
|
report.reportPath = ws.directory + "/checks/" + randomUUID5() + ".json";
|
|
4575
4942
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -4582,12 +4949,8 @@ async function automate(dir, event) {
|
|
|
4582
4949
|
if (Object.keys(state.sessions).length >= 128) throw new Error("Too many sessions with unfinished tool calls; original evidence was retained.");
|
|
4583
4950
|
state.sessions[key] = {
|
|
4584
4951
|
host: event.host,
|
|
4585
|
-
|
|
4586
|
-
continued: false,
|
|
4587
|
-
initialIssues: report.findings.filter((f) => f.status === "unresolved").map((f) => f.id),
|
|
4588
|
-
initialDocs: inputs.docs,
|
|
4952
|
+
notifiedDiagnostics: [],
|
|
4589
4953
|
lastUsed: now,
|
|
4590
|
-
mutationObserved: false,
|
|
4591
4954
|
pending: {},
|
|
4592
4955
|
coverageGaps: [],
|
|
4593
4956
|
events: {}
|
|
@@ -4602,7 +4965,6 @@ async function automate(dir, event) {
|
|
|
4602
4965
|
session.pending[event.toolId] = inputs.fingerprint;
|
|
4603
4966
|
}
|
|
4604
4967
|
if (event.event === "after_tool" && event.mutating) {
|
|
4605
|
-
session.mutationObserved = true;
|
|
4606
4968
|
if (!event.toolId || !session.pending[event.toolId]) {
|
|
4607
4969
|
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.";
|
|
4608
4970
|
if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
|
|
@@ -4613,31 +4975,39 @@ async function automate(dir, event) {
|
|
|
4613
4975
|
if (session.coverageGaps.length && report.status !== "unavailable") report.status = "incomplete";
|
|
4614
4976
|
report.capture = report.status !== "unavailable" && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
|
|
4615
4977
|
}
|
|
4616
|
-
const
|
|
4617
|
-
|
|
4618
|
-
const
|
|
4619
|
-
const
|
|
4620
|
-
if (session)
|
|
4621
|
-
session.
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
const
|
|
4978
|
+
const selected = session && event.event === "task_end" ? completionFindings(state.notifications, report.findings) : [];
|
|
4979
|
+
state.agentNotifications ??= { ...structuredClone(state.notifications), delivered: {} };
|
|
4980
|
+
const feedback = session && ["turn_start", "after_tool"].includes(event.event) ? completionFindings(state.agentNotifications, report.findings) : [];
|
|
4981
|
+
const diagnostics = session ? report.diagnostics.filter((d) => !session.notifiedDiagnostics.includes(d)).slice(0, 2) : [];
|
|
4982
|
+
if (session) session.notifiedDiagnostics = [.../* @__PURE__ */ new Set([
|
|
4983
|
+
...session.notifiedDiagnostics.filter((d) => report.diagnostics.includes(d)),
|
|
4984
|
+
...diagnostics
|
|
4985
|
+
])];
|
|
4986
|
+
const message2 = !session ? summarize(report) : [
|
|
4987
|
+
completionSummary(selected, report.reportPath),
|
|
4988
|
+
completionSummary(feedback, report.reportPath),
|
|
4989
|
+
...diagnostics.length ? [`Mason: verification limited. ${diagnostics.slice(0, 2).map(cleanText).join(" ")} Evidence: ${report.reportPath}.`] : []
|
|
4990
|
+
].filter(Boolean).join("\n") || null;
|
|
4991
|
+
const notify = message2 !== null;
|
|
4992
|
+
const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || newSession || event.event === "task_end";
|
|
4625
4993
|
if (!persistReport) report.reportPath = state.latest;
|
|
4626
4994
|
state.updatedAt = now;
|
|
4627
4995
|
state.fingerprint = inputs.fingerprint;
|
|
4628
4996
|
state.latest = report.reportPath;
|
|
4629
4997
|
if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
|
|
4630
4998
|
await writeStoreJson(ws.root, ws.directory + "/state.json", state);
|
|
4631
|
-
return { report, message:
|
|
4999
|
+
return { report, message: message2 };
|
|
4632
5000
|
}));
|
|
4633
5001
|
}));
|
|
4634
5002
|
}
|
|
4635
5003
|
async function automationStatus(dir) {
|
|
5004
|
+
const { pendingKnowledge: pendingKnowledge2 } = await Promise.resolve().then(() => (init_pending(), pending_exports));
|
|
4636
5005
|
const ws = await profilePhase("automation.workspace", () => workspace(dir));
|
|
5006
|
+
const knowledge = await pendingKnowledge2(ws.root);
|
|
4637
5007
|
const execution = await executionStatus(ws.root, ws.directory);
|
|
4638
5008
|
const notifications = await failureNotifications(ws.root, ws.directory);
|
|
4639
5009
|
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
4640
|
-
if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications };
|
|
5010
|
+
if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications, knowledge };
|
|
4641
5011
|
const state = parseState(raw);
|
|
4642
5012
|
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
|
|
4643
5013
|
const inputs = await readInputs(ws.root);
|
|
@@ -4660,6 +5030,8 @@ async function automationStatus(dir) {
|
|
|
4660
5030
|
hosts,
|
|
4661
5031
|
execution,
|
|
4662
5032
|
notifications,
|
|
5033
|
+
knowledge,
|
|
5034
|
+
notificationBaselineHead: state.notifications?.baselineHead ?? null,
|
|
4663
5035
|
note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
|
|
4664
5036
|
};
|
|
4665
5037
|
}
|
|
@@ -4667,6 +5039,7 @@ var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
|
|
|
4667
5039
|
var init_runtime = __esm({
|
|
4668
5040
|
"src/automation/runtime.ts"() {
|
|
4669
5041
|
"use strict";
|
|
5042
|
+
init_notifications();
|
|
4670
5043
|
init_profile();
|
|
4671
5044
|
init_inspection();
|
|
4672
5045
|
init_execution();
|
|
@@ -4678,34 +5051,34 @@ var init_runtime = __esm({
|
|
|
4678
5051
|
SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved findings no longer fail their checks or have a recorded assessment covering current evidence. Repair and review only within the user's task authorization.";
|
|
4679
5052
|
priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
|
|
4680
5053
|
cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
|
|
4681
|
-
analysisSchema =
|
|
4682
|
-
id:
|
|
4683
|
-
fingerprint:
|
|
4684
|
-
completedAt:
|
|
4685
|
-
baselineDigest:
|
|
5054
|
+
analysisSchema = z14.object({
|
|
5055
|
+
id: z14.string(),
|
|
5056
|
+
fingerprint: z14.string(),
|
|
5057
|
+
completedAt: z14.number(),
|
|
5058
|
+
baselineDigest: z14.string(),
|
|
4686
5059
|
currentAudit: reportSchema.nullable().optional(),
|
|
4687
|
-
report:
|
|
4688
|
-
version:
|
|
4689
|
-
status:
|
|
4690
|
-
root:
|
|
4691
|
-
branch:
|
|
4692
|
-
head:
|
|
4693
|
-
baselinePaths:
|
|
4694
|
-
reportPath:
|
|
4695
|
-
findings:
|
|
4696
|
-
diagnostics:
|
|
4697
|
-
checks:
|
|
4698
|
-
ran:
|
|
4699
|
-
reused:
|
|
4700
|
-
skipped:
|
|
4701
|
-
auditReused:
|
|
5060
|
+
report: z14.object({
|
|
5061
|
+
version: z14.literal(1),
|
|
5062
|
+
status: z14.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
|
|
5063
|
+
root: z14.string(),
|
|
5064
|
+
branch: z14.string(),
|
|
5065
|
+
head: z14.string(),
|
|
5066
|
+
baselinePaths: z14.array(z14.string()),
|
|
5067
|
+
reportPath: z14.string(),
|
|
5068
|
+
findings: z14.array(repairFindingSchema),
|
|
5069
|
+
diagnostics: z14.array(z14.string()),
|
|
5070
|
+
checks: z14.object({
|
|
5071
|
+
ran: z14.array(z14.string()),
|
|
5072
|
+
reused: z14.array(z14.string()),
|
|
5073
|
+
skipped: z14.array(z14.object({ check: z14.string(), reason: z14.string(), doc: z14.string().optional() })),
|
|
5074
|
+
auditReused: z14.boolean().optional()
|
|
4702
5075
|
}),
|
|
4703
|
-
counts:
|
|
4704
|
-
capture:
|
|
4705
|
-
scope:
|
|
5076
|
+
counts: z14.object({ resolved: z14.number(), unresolved: z14.number(), "review-required": z14.number(), unverified: z14.number() }),
|
|
5077
|
+
capture: z14.enum(["observed", "unknown"]),
|
|
5078
|
+
scope: z14.string()
|
|
4706
5079
|
})
|
|
4707
5080
|
});
|
|
4708
|
-
sharedSchema =
|
|
5081
|
+
sharedSchema = z14.object({ analysis: analysisSchema, digest: z14.string() });
|
|
4709
5082
|
baselineDigest = async (ws, state) => profilePhase("automation.baselines", async () => hash(await Promise.all(
|
|
4710
5083
|
state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
|
|
4711
5084
|
)));
|
|
@@ -4714,7 +5087,7 @@ var init_runtime = __esm({
|
|
|
4714
5087
|
|
|
4715
5088
|
// src/automation/hook-root.ts
|
|
4716
5089
|
import fs20 from "fs/promises";
|
|
4717
|
-
import
|
|
5090
|
+
import path28 from "path";
|
|
4718
5091
|
var init_hook_root = __esm({
|
|
4719
5092
|
"src/automation/hook-root.ts"() {
|
|
4720
5093
|
"use strict";
|
|
@@ -4723,7 +5096,7 @@ var init_hook_root = __esm({
|
|
|
4723
5096
|
});
|
|
4724
5097
|
|
|
4725
5098
|
// src/automation/adapters.ts
|
|
4726
|
-
import { z as
|
|
5099
|
+
import { z as z15 } from "zod";
|
|
4727
5100
|
function managedHookCommand(host, platform = process.platform) {
|
|
4728
5101
|
const command = `mason --setup-host ${host} auto hook --host ${host}`;
|
|
4729
5102
|
return platform === "win32" ? `cmd.exe /d /s /c "where mason >nul 2>nul & if errorlevel 1 (exit /b 0) else (${command})"` : `if command -v mason >/dev/null 2>&1; then ${command}; fi`;
|
|
@@ -4749,15 +5122,15 @@ var init_adapters = __esm({
|
|
|
4749
5122
|
init_runtime();
|
|
4750
5123
|
init_store();
|
|
4751
5124
|
init_hook_root();
|
|
4752
|
-
inputSchema =
|
|
4753
|
-
cwd:
|
|
4754
|
-
session_id:
|
|
4755
|
-
hook_event_name:
|
|
4756
|
-
tool_name:
|
|
4757
|
-
tool_use_id:
|
|
4758
|
-
tool_input:
|
|
4759
|
-
stop_hook_active:
|
|
4760
|
-
permission_mode:
|
|
5125
|
+
inputSchema = z15.object({
|
|
5126
|
+
cwd: z15.string().min(1),
|
|
5127
|
+
session_id: z15.string().min(1).max(500),
|
|
5128
|
+
hook_event_name: z15.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
|
|
5129
|
+
tool_name: z15.string().optional(),
|
|
5130
|
+
tool_use_id: z15.string().max(500).optional(),
|
|
5131
|
+
tool_input: z15.unknown().optional(),
|
|
5132
|
+
stop_hook_active: z15.boolean().optional(),
|
|
5133
|
+
permission_mode: z15.string().optional()
|
|
4761
5134
|
});
|
|
4762
5135
|
HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
|
|
4763
5136
|
}
|
|
@@ -4773,7 +5146,7 @@ __export(ownership_exports, {
|
|
|
4773
5146
|
ownershipEdit: () => ownershipEdit,
|
|
4774
5147
|
ownershipSchema: () => ownershipSchema
|
|
4775
5148
|
});
|
|
4776
|
-
import { z as
|
|
5149
|
+
import { z as z16 } from "zod";
|
|
4777
5150
|
import { parse } from "smol-toml";
|
|
4778
5151
|
function blockSpan(text2, start, end) {
|
|
4779
5152
|
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
@@ -4824,15 +5197,15 @@ var init_ownership = __esm({
|
|
|
4824
5197
|
["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
|
|
4825
5198
|
["# mason:mcp:start", "# mason:mcp:end"]
|
|
4826
5199
|
];
|
|
4827
|
-
fileSchema =
|
|
4828
|
-
created:
|
|
4829
|
-
initialHash:
|
|
4830
|
-
blocks:
|
|
4831
|
-
mcpHash:
|
|
4832
|
-
hookCommands:
|
|
4833
|
-
hooks:
|
|
5200
|
+
fileSchema = z16.object({
|
|
5201
|
+
created: z16.boolean(),
|
|
5202
|
+
initialHash: z16.string(),
|
|
5203
|
+
blocks: z16.record(z16.string()),
|
|
5204
|
+
mcpHash: z16.string().optional(),
|
|
5205
|
+
hookCommands: z16.array(z16.string()).optional(),
|
|
5206
|
+
hooks: z16.record(z16.array(z16.string())).optional()
|
|
4834
5207
|
});
|
|
4835
|
-
ownershipSchema =
|
|
5208
|
+
ownershipSchema = z16.object({ version: z16.literal(1), files: z16.record(fileSchema) });
|
|
4836
5209
|
}
|
|
4837
5210
|
});
|
|
4838
5211
|
|
|
@@ -4847,7 +5220,7 @@ __export(install_exports, {
|
|
|
4847
5220
|
planAutomationInstall: () => planAutomationInstall,
|
|
4848
5221
|
recordSchema: () => recordSchema2
|
|
4849
5222
|
});
|
|
4850
|
-
import { z as
|
|
5223
|
+
import { z as z17 } from "zod";
|
|
4851
5224
|
async function installationRecord(root) {
|
|
4852
5225
|
return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
4853
5226
|
}
|
|
@@ -4928,9 +5301,9 @@ var init_install = __esm({
|
|
|
4928
5301
|
init_store();
|
|
4929
5302
|
init_files2();
|
|
4930
5303
|
init_ownership();
|
|
4931
|
-
groupSchema =
|
|
4932
|
-
automationConfigSchema =
|
|
4933
|
-
recordSchema2 =
|
|
5304
|
+
groupSchema = z17.object({ hooks: z17.array(z17.object({ type: z17.string(), command: z17.string().optional() }).passthrough()) }).passthrough();
|
|
5305
|
+
automationConfigSchema = z17.object({ hooks: z17.record(z17.array(groupSchema)).optional() }).passthrough();
|
|
5306
|
+
recordSchema2 = z17.object({ version: z17.literal(1), hosts: z17.record(z17.object({ command: z17.string() })) });
|
|
4934
5307
|
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
4935
5308
|
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
4936
5309
|
}
|
|
@@ -4948,7 +5321,7 @@ __export(config_exports, {
|
|
|
4948
5321
|
});
|
|
4949
5322
|
import { isDeepStrictEqual } from "util";
|
|
4950
5323
|
import { parse as parse2, stringify } from "smol-toml";
|
|
4951
|
-
import { z as
|
|
5324
|
+
import { z as z18 } from "zod";
|
|
4952
5325
|
async function instructionEdits(root, host) {
|
|
4953
5326
|
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
4954
5327
|
const existing = found.find((f) => f.text !== null);
|
|
@@ -5059,7 +5432,7 @@ var init_config = __esm({
|
|
|
5059
5432
|
init_install();
|
|
5060
5433
|
init_files2();
|
|
5061
5434
|
init_launcher();
|
|
5062
|
-
object =
|
|
5435
|
+
object = z18.record(z18.unknown());
|
|
5063
5436
|
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
5064
5437
|
TOML_START = "# mason:mcp:start";
|
|
5065
5438
|
TOML_END = "# mason:mcp:end";
|
|
@@ -5069,7 +5442,7 @@ var init_config = __esm({
|
|
|
5069
5442
|
});
|
|
5070
5443
|
|
|
5071
5444
|
// src/mcp/init.ts
|
|
5072
|
-
import { z as
|
|
5445
|
+
import { z as z19 } from "zod";
|
|
5073
5446
|
async function loadProjectMarker(rootDir) {
|
|
5074
5447
|
const raw = await readStoreJson(rootDir, ".mason/local/project.json");
|
|
5075
5448
|
if (raw === null) return null;
|
|
@@ -5077,8 +5450,8 @@ async function loadProjectMarker(rootDir) {
|
|
|
5077
5450
|
return { ...marker, features: await loadProjectFeatures(rootDir) };
|
|
5078
5451
|
}
|
|
5079
5452
|
async function loadProjectFeatures(rootDir) {
|
|
5080
|
-
const config =
|
|
5081
|
-
return
|
|
5453
|
+
const config = z19.record(z19.unknown()).parse(await readStoreJson(rootDir, ".mason/config.json") ?? {});
|
|
5454
|
+
return z19.object({ confluence: z19.boolean().optional() }).passthrough().parse(config.features ?? {});
|
|
5082
5455
|
}
|
|
5083
5456
|
async function saveProjectMarker(rootDir, marker) {
|
|
5084
5457
|
const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
@@ -5090,8 +5463,8 @@ async function projectMarkerEdits(rootDir, marker) {
|
|
|
5090
5463
|
const edits = [{ path: file, before: await readText(rootDir, file), after: JSON.stringify(local, null, 2) + "\n" }];
|
|
5091
5464
|
if (features && Object.keys(features).length) {
|
|
5092
5465
|
const configPath2 = ".mason/config.json", before = await readText(rootDir, configPath2);
|
|
5093
|
-
const config =
|
|
5094
|
-
config.features = { ...
|
|
5466
|
+
const config = z19.record(z19.unknown()).parse(JSON.parse(before ?? "{}"));
|
|
5467
|
+
config.features = { ...z19.record(z19.unknown()).parse(config.features ?? {}), ...features };
|
|
5095
5468
|
edits.push({ path: configPath2, before, after: JSON.stringify(config, null, 2) + "\n" });
|
|
5096
5469
|
}
|
|
5097
5470
|
return edits;
|
|
@@ -5105,10 +5478,10 @@ var init_init = __esm({
|
|
|
5105
5478
|
"use strict";
|
|
5106
5479
|
init_storage();
|
|
5107
5480
|
init_files2();
|
|
5108
|
-
markerSchema =
|
|
5109
|
-
version:
|
|
5110
|
-
initializedAt:
|
|
5111
|
-
features:
|
|
5481
|
+
markerSchema = z19.object({
|
|
5482
|
+
version: z19.literal(1),
|
|
5483
|
+
initializedAt: z19.string(),
|
|
5484
|
+
features: z19.object({ confluence: z19.boolean().optional() }).passthrough().optional()
|
|
5112
5485
|
}).passthrough();
|
|
5113
5486
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
5114
5487
|
## Mason project knowledge
|
|
@@ -5117,16 +5490,19 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
5117
5490
|
|
|
5118
5491
|
- Task, bug, change request, or investigation of why the project works this way \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
|
|
5119
5492
|
- Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
|
|
5120
|
-
- Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\`
|
|
5493
|
+
- Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` as soon as the lesson is supported by evidence; do not wait for implementation, task completion, or merge. Record the established observation now and identify unimplemented remedies as tentative. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
|
|
5121
5494
|
- Before saving, compare the lesson with matching \`get_context\` records and any pending proposal. If new evidence changes the same lesson's assumptions, scope, or recommended action, revise it with \`save_decision(id: existingId)\`, including when it is accepted. Preserve supported rationale and sources, replace obsolete instructions, and add the new evidence. Create a separate record for a genuinely distinct lesson; skip unchanged restatements. Revision preserves history and leaves the accepted version operative until authorized review.
|
|
5122
5495
|
- Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, stale or unknown map verification requires a fresh evidence review, and a recorded failed verdict means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
|
|
5496
|
+
- Asked to review pending project knowledge \u2192 \`mason_automation(action: "status")\` lists pending proposals and uncommitted records. For ordinary tasks, inspect matching proposals through \`get_context\`; do not request acceptance merely because a proposal exists.
|
|
5123
5497
|
- Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
|
|
5124
5498
|
- For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
|
|
5125
5499
|
- \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
|
|
5126
5500
|
|
|
5127
|
-
- When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings.
|
|
5501
|
+
- When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings. Specific suppressed advisories remain unresolved; editing a doc does not approve it. Informational dependency notices require no review.
|
|
5128
5502
|
- When advisory assessment is authorized, use \`review_advisory(action: "prepare", baselinePath, findingId)\` to inspect original evidence and history. Record \`addressed\`, \`inapplicable\`, or \`deferred\` with the actual reviewer, reason in note, and returned reviewToken only after relevant edits are committed. Commit the assessment and verify the same baseline again. Relevant evidence changes reopen it; deferral stays outstanding. Decision findings use \`review_decision\`; advisory closure never approves a decision.
|
|
5129
5503
|
|
|
5504
|
+
- 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.
|
|
5505
|
+
|
|
5130
5506
|
- When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason-auto status\` / \`mason-auto check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
|
|
5131
5507
|
|
|
5132
5508
|
Inspect source for what the retrieved context does not answer.
|
|
@@ -5141,7 +5517,7 @@ ${CLAUDE_MD_SECTION}`;
|
|
|
5141
5517
|
|
|
5142
5518
|
1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
|
|
5143
5519
|
2. Address findings within the user's requested scope. Setup alone authorizes Mason host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
|
|
5144
|
-
3. When the task reveals a real lesson or constraint, compare matching \`get_context\` records first. If its assumptions, scope, or recommended action changed, revise that record using \`save_decision(id: existingId)\`; create a separate record for a genuinely distinct lesson and skip unchanged restatements. Include title, body, category, anchors, and known owner/source/actor information. Preserve supported rationale and sources, replace obsolete instructions, and add new evidence. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
|
|
5520
|
+
3. Capture a supported lesson as soon as it is established; do not wait for implementation, task completion, or merge. Keep unimplemented remedies tentative. When the task reveals a real lesson or constraint, compare matching \`get_context\` records first. If its assumptions, scope, or recommended action changed, revise that record using \`save_decision(id: existingId)\`; create a separate record for a genuinely distinct lesson and skip unchanged restatements. Include title, body, category, anchors, and known owner/source/actor information. Preserve supported rationale and sources, replace obsolete instructions, and add new evidence. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
|
|
5145
5521
|
|
|
5146
5522
|
${ASSISTANT_SETUP}
|
|
5147
5523
|
|
|
@@ -5304,7 +5680,7 @@ var init_types2 = __esm({
|
|
|
5304
5680
|
});
|
|
5305
5681
|
|
|
5306
5682
|
// src/review/evidence/vitest.ts
|
|
5307
|
-
import { z as
|
|
5683
|
+
import { z as z20 } from "zod";
|
|
5308
5684
|
function parseVitest(raw, sourceRoot) {
|
|
5309
5685
|
const report = schema.parse(raw);
|
|
5310
5686
|
const findings = [], diagnostics = [];
|
|
@@ -5353,24 +5729,24 @@ var init_vitest = __esm({
|
|
|
5353
5729
|
"use strict";
|
|
5354
5730
|
init_paths2();
|
|
5355
5731
|
init_types2();
|
|
5356
|
-
count3 =
|
|
5357
|
-
schema =
|
|
5358
|
-
success:
|
|
5732
|
+
count3 = z20.number().int().nonnegative();
|
|
5733
|
+
schema = z20.object({
|
|
5734
|
+
success: z20.boolean(),
|
|
5359
5735
|
numTotalTests: count3,
|
|
5360
5736
|
numPassedTests: count3,
|
|
5361
5737
|
numFailedTests: count3,
|
|
5362
5738
|
numPendingTests: count3,
|
|
5363
5739
|
numTodoTests: count3,
|
|
5364
5740
|
numFailedTestSuites: count3,
|
|
5365
|
-
testResults:
|
|
5366
|
-
name:
|
|
5367
|
-
status:
|
|
5368
|
-
message:
|
|
5369
|
-
assertionResults:
|
|
5370
|
-
fullName:
|
|
5371
|
-
status:
|
|
5372
|
-
failureMessages:
|
|
5373
|
-
location:
|
|
5741
|
+
testResults: z20.array(z20.object({
|
|
5742
|
+
name: z20.string().min(1),
|
|
5743
|
+
status: z20.enum(["passed", "failed"]),
|
|
5744
|
+
message: z20.string().optional(),
|
|
5745
|
+
assertionResults: z20.array(z20.object({
|
|
5746
|
+
fullName: z20.string(),
|
|
5747
|
+
status: z20.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
5748
|
+
failureMessages: z20.array(z20.string()).nullable().optional(),
|
|
5749
|
+
location: z20.object({ line: count3, column: count3 }).nullable().optional()
|
|
5374
5750
|
}))
|
|
5375
5751
|
}))
|
|
5376
5752
|
});
|
|
@@ -5378,7 +5754,7 @@ var init_vitest = __esm({
|
|
|
5378
5754
|
});
|
|
5379
5755
|
|
|
5380
5756
|
// src/review/evidence/sarif.ts
|
|
5381
|
-
import { z as
|
|
5757
|
+
import { z as z21 } from "zod";
|
|
5382
5758
|
function parseSarif(raw, sourceRoot) {
|
|
5383
5759
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
5384
5760
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -5469,29 +5845,29 @@ var init_sarif = __esm({
|
|
|
5469
5845
|
"use strict";
|
|
5470
5846
|
init_paths2();
|
|
5471
5847
|
init_types2();
|
|
5472
|
-
index =
|
|
5473
|
-
artifact =
|
|
5474
|
-
message =
|
|
5475
|
-
level =
|
|
5476
|
-
location =
|
|
5477
|
-
schema2 =
|
|
5478
|
-
version:
|
|
5479
|
-
runs:
|
|
5480
|
-
tool:
|
|
5481
|
-
invocations:
|
|
5482
|
-
versionControlProvenance:
|
|
5483
|
-
originalUriBaseIds:
|
|
5484
|
-
artifacts:
|
|
5485
|
-
results:
|
|
5486
|
-
ruleId:
|
|
5848
|
+
index = z21.number().int().nonnegative();
|
|
5849
|
+
artifact = z21.object({ uri: z21.string().optional(), uriBaseId: z21.string().optional(), index: index.optional() });
|
|
5850
|
+
message = z21.object({ text: z21.string().optional(), markdown: z21.string().optional(), id: z21.string().optional(), arguments: z21.array(z21.string()).optional() });
|
|
5851
|
+
level = z21.enum(["error", "warning", "note", "none"]);
|
|
5852
|
+
location = z21.object({ physicalLocation: z21.object({ artifactLocation: artifact.optional(), region: z21.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
5853
|
+
schema2 = z21.object({
|
|
5854
|
+
version: z21.literal("2.1.0"),
|
|
5855
|
+
runs: z21.array(z21.object({
|
|
5856
|
+
tool: z21.object({ driver: z21.object({ name: z21.string().min(1), rules: z21.array(z21.object({ id: z21.string(), defaultConfiguration: z21.object({ level: level.optional() }).optional(), messageStrings: z21.record(message).optional() })).optional(), globalMessageStrings: z21.record(message).optional() }) }),
|
|
5857
|
+
invocations: z21.array(z21.object({ executionSuccessful: z21.boolean(), commandLine: z21.string().optional(), toolExecutionNotifications: z21.array(z21.object({ level: level.optional(), message })).optional() })).optional(),
|
|
5858
|
+
versionControlProvenance: z21.array(z21.object({ revisionId: z21.string().optional() })).optional(),
|
|
5859
|
+
originalUriBaseIds: z21.record(artifact).optional(),
|
|
5860
|
+
artifacts: z21.array(z21.object({ location: artifact.optional() })).optional(),
|
|
5861
|
+
results: z21.array(z21.object({
|
|
5862
|
+
ruleId: z21.string().optional(),
|
|
5487
5863
|
ruleIndex: index.optional(),
|
|
5488
5864
|
message,
|
|
5489
5865
|
level: level.optional(),
|
|
5490
|
-
kind:
|
|
5491
|
-
baselineState:
|
|
5492
|
-
suppressions:
|
|
5493
|
-
locations:
|
|
5494
|
-
relatedLocations:
|
|
5866
|
+
kind: z21.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
5867
|
+
baselineState: z21.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
5868
|
+
suppressions: z21.array(z21.object({ kind: z21.enum(["inSource", "external"]), status: z21.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
5869
|
+
locations: z21.array(location).optional(),
|
|
5870
|
+
relatedLocations: z21.array(location).optional()
|
|
5495
5871
|
})).optional()
|
|
5496
5872
|
}))
|
|
5497
5873
|
});
|
|
@@ -5499,7 +5875,7 @@ var init_sarif = __esm({
|
|
|
5499
5875
|
});
|
|
5500
5876
|
|
|
5501
5877
|
// src/review/evidence/check.ts
|
|
5502
|
-
import { z as
|
|
5878
|
+
import { z as z22 } from "zod";
|
|
5503
5879
|
function parseCheck(raw, sourceRoot) {
|
|
5504
5880
|
const report = schema3.parse(raw);
|
|
5505
5881
|
const diagnostics = [], findings = [];
|
|
@@ -5539,17 +5915,17 @@ var init_check = __esm({
|
|
|
5539
5915
|
"use strict";
|
|
5540
5916
|
init_paths2();
|
|
5541
5917
|
init_types2();
|
|
5542
|
-
schema3 =
|
|
5543
|
-
version:
|
|
5544
|
-
commit:
|
|
5545
|
-
results:
|
|
5546
|
-
name:
|
|
5547
|
-
status:
|
|
5548
|
-
message:
|
|
5549
|
-
locations:
|
|
5550
|
-
file:
|
|
5551
|
-
line:
|
|
5552
|
-
column:
|
|
5918
|
+
schema3 = z22.object({
|
|
5919
|
+
version: z22.literal(1),
|
|
5920
|
+
commit: z22.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
|
|
5921
|
+
results: z22.array(z22.object({
|
|
5922
|
+
name: z22.string().min(1).max(1e3),
|
|
5923
|
+
status: z22.enum(["passed", "failed", "skipped"]),
|
|
5924
|
+
message: z22.string().max(1e5).optional(),
|
|
5925
|
+
locations: z22.array(z22.object({
|
|
5926
|
+
file: z22.string().min(1).max(4e3),
|
|
5927
|
+
line: z22.number().int().positive().optional(),
|
|
5928
|
+
column: z22.number().int().positive().optional()
|
|
5553
5929
|
})).max(100).default([])
|
|
5554
5930
|
})).max(1e5)
|
|
5555
5931
|
});
|
|
@@ -5557,10 +5933,10 @@ var init_check = __esm({
|
|
|
5557
5933
|
});
|
|
5558
5934
|
|
|
5559
5935
|
// src/review/evidence.ts
|
|
5560
|
-
import
|
|
5561
|
-
import { z as
|
|
5936
|
+
import path29 from "path";
|
|
5937
|
+
import { z as z23 } from "zod";
|
|
5562
5938
|
function relativeArtifact(root, file) {
|
|
5563
|
-
const relative =
|
|
5939
|
+
const relative = path29.isAbsolute(file) ? path29.relative(root, file) : file;
|
|
5564
5940
|
const normalized = normalizeRepoPath(relative);
|
|
5565
5941
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
5566
5942
|
return normalized;
|
|
@@ -5599,7 +5975,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
5599
5975
|
let manifest, raw;
|
|
5600
5976
|
try {
|
|
5601
5977
|
manifest = relativeArtifact(root, supplied);
|
|
5602
|
-
raw =
|
|
5978
|
+
raw = z23.object({ version: z23.literal(1), checks: z23.array(z23.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
5603
5979
|
} catch (error) {
|
|
5604
5980
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
5605
5981
|
continue;
|
|
@@ -5657,7 +6033,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
5657
6033
|
const report = await readStoreJson(root, file);
|
|
5658
6034
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
5659
6035
|
const sourceRoot = check.sourceRoot ?? root;
|
|
5660
|
-
if (!
|
|
6036
|
+
if (!path29.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
|
|
5661
6037
|
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
5662
6038
|
result.outcome = parsed.outcome;
|
|
5663
6039
|
result.counts = parsed.counts;
|
|
@@ -5754,32 +6130,32 @@ var init_evidence2 = __esm({
|
|
|
5754
6130
|
init_sarif();
|
|
5755
6131
|
init_check();
|
|
5756
6132
|
init_types2();
|
|
5757
|
-
checkSchema2 =
|
|
5758
|
-
id:
|
|
5759
|
-
kind:
|
|
5760
|
-
tool:
|
|
5761
|
-
command:
|
|
5762
|
-
commit:
|
|
5763
|
-
workingTreeClean:
|
|
5764
|
-
source:
|
|
5765
|
-
sourceRoot:
|
|
5766
|
-
status:
|
|
5767
|
-
reason:
|
|
5768
|
-
exitCode:
|
|
5769
|
-
environment:
|
|
5770
|
-
toolVersion:
|
|
5771
|
-
runtime:
|
|
5772
|
-
platform:
|
|
5773
|
-
configuration:
|
|
6133
|
+
checkSchema2 = z23.object({
|
|
6134
|
+
id: z23.string().min(1).max(100),
|
|
6135
|
+
kind: z23.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
|
|
6136
|
+
tool: z23.string().min(1).max(200),
|
|
6137
|
+
command: z23.string().min(1).max(2e3),
|
|
6138
|
+
commit: z23.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
6139
|
+
workingTreeClean: z23.boolean().optional(),
|
|
6140
|
+
source: z23.string().max(2e3).optional(),
|
|
6141
|
+
sourceRoot: z23.string().min(1).max(4e3).optional(),
|
|
6142
|
+
status: z23.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
6143
|
+
reason: z23.string().min(1).max(2e3).optional(),
|
|
6144
|
+
exitCode: z23.number().int().nullable().optional(),
|
|
6145
|
+
environment: z23.object({
|
|
6146
|
+
toolVersion: z23.string().min(1).max(200),
|
|
6147
|
+
runtime: z23.string().min(1).max(200).optional(),
|
|
6148
|
+
platform: z23.string().min(1).max(200).optional(),
|
|
6149
|
+
configuration: z23.array(z23.string().min(1).max(4e3)).max(100).optional()
|
|
5774
6150
|
}).optional(),
|
|
5775
|
-
report:
|
|
6151
|
+
report: z23.object({ format: z23.enum(["vitest-json", "sarif", "mason-check-json"]), path: z23.string().min(1).max(4e3) }).optional()
|
|
5776
6152
|
});
|
|
5777
6153
|
}
|
|
5778
6154
|
});
|
|
5779
6155
|
|
|
5780
6156
|
// src/review/review.ts
|
|
5781
6157
|
import fs21 from "fs/promises";
|
|
5782
|
-
import
|
|
6158
|
+
import path30 from "path";
|
|
5783
6159
|
import { execFile as execFile7 } from "child_process";
|
|
5784
6160
|
import { promisify as promisify7 } from "util";
|
|
5785
6161
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
@@ -5808,7 +6184,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
5808
6184
|
return matchingPaths(record.files, changedFiles);
|
|
5809
6185
|
}
|
|
5810
6186
|
async function computeReview(rootDir, base, options = {}) {
|
|
5811
|
-
const resolvedRoot =
|
|
6187
|
+
const resolvedRoot = path30.resolve(rootDir);
|
|
5812
6188
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
5813
6189
|
if (head === "unknown") return null;
|
|
5814
6190
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -5858,7 +6234,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
5858
6234
|
analyzed,
|
|
5859
6235
|
async (relPath) => {
|
|
5860
6236
|
try {
|
|
5861
|
-
await fs21.access(
|
|
6237
|
+
await fs21.access(path30.join(resolvedRoot, relPath));
|
|
5862
6238
|
return true;
|
|
5863
6239
|
} catch {
|
|
5864
6240
|
return false;
|
|
@@ -5993,7 +6369,7 @@ var init_onboarding = __esm({
|
|
|
5993
6369
|
});
|
|
5994
6370
|
|
|
5995
6371
|
// src/impact/references.ts
|
|
5996
|
-
import
|
|
6372
|
+
import path31 from "path";
|
|
5997
6373
|
function templateEnd(content2, start) {
|
|
5998
6374
|
let i = start + 1, interpolation = 0;
|
|
5999
6375
|
while (i < content2.length) {
|
|
@@ -6119,10 +6495,10 @@ function jvmImports(content2) {
|
|
|
6119
6495
|
}
|
|
6120
6496
|
function resolveImport(file, specifier, available) {
|
|
6121
6497
|
if (!specifier.startsWith("./") && !specifier.startsWith("../")) return null;
|
|
6122
|
-
const relative =
|
|
6498
|
+
const relative = path31.posix.normalize(path31.posix.join(path31.posix.dirname(file), specifier));
|
|
6123
6499
|
if (relative.startsWith("../")) return null;
|
|
6124
6500
|
if (available.has(relative)) return relative;
|
|
6125
|
-
const ext =
|
|
6501
|
+
const ext = path31.posix.extname(relative);
|
|
6126
6502
|
const replacements = { ".js": [".ts", ".tsx"], ".mjs": [".mts"], ".cjs": [".cts"] };
|
|
6127
6503
|
const candidates = ext ? (replacements[ext] ?? []).map((e) => relative.slice(0, -ext.length) + e) : jsExtensions.flatMap((e) => [relative + e, relative + "/index" + e]);
|
|
6128
6504
|
const found = candidates.filter((f) => available.has(f));
|
|
@@ -6134,13 +6510,13 @@ function mentionsPath(content2, target) {
|
|
|
6134
6510
|
function createReferenceMatcher(targets2, available, targetContents = /* @__PURE__ */ new Map()) {
|
|
6135
6511
|
const counts = /* @__PURE__ */ new Map();
|
|
6136
6512
|
for (const file of available) {
|
|
6137
|
-
const name =
|
|
6513
|
+
const name = path31.posix.basename(file, path31.posix.extname(file));
|
|
6138
6514
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
6139
6515
|
}
|
|
6140
6516
|
const jvmTargets = new Map([...targetContents].filter(([file]) => /\.(?:kt|kts|java)$/.test(file)).map(([file, content2]) => [file, declaredSymbols(content2)]));
|
|
6141
6517
|
return (source) => {
|
|
6142
6518
|
if (targets2.includes(source.path)) return [];
|
|
6143
|
-
const resolved = new Set(jsExtensions.includes(
|
|
6519
|
+
const resolved = new Set(jsExtensions.includes(path31.posix.extname(source.path)) ? imports(source.content).map((s) => resolveImport(source.path, s, available)).filter(Boolean) : []);
|
|
6144
6520
|
if (/\.(?:kt|kts|java)$/.test(source.path)) {
|
|
6145
6521
|
const imported = jvmImports(source.content);
|
|
6146
6522
|
for (const [target, symbols] of jvmTargets) if (symbols.some((symbol) => imported.some((name) => name === symbol || name.startsWith(symbol + ".")))) resolved.add(target);
|
|
@@ -6156,7 +6532,7 @@ function createReferenceMatcher(targets2, available, targetContents = /* @__PURE
|
|
|
6156
6532
|
add("explicit-path", target);
|
|
6157
6533
|
continue;
|
|
6158
6534
|
}
|
|
6159
|
-
const ext =
|
|
6535
|
+
const ext = path31.posix.extname(target), name = path31.posix.basename(target, ext);
|
|
6160
6536
|
if (!/\.(?:[cm]?[jt]sx?|py|kt|java|go|rs|swift|rb|cs|cpp|c|h|hpp|dart|php)$/.test(ext) || name.startsWith(".") || name.length < 3 || genericNames.has(name.toLowerCase()) || counts.get(name) !== 1) continue;
|
|
6161
6537
|
if (word(name).test(source.content)) {
|
|
6162
6538
|
add("name-candidate", name);
|
|
@@ -6185,11 +6561,11 @@ var impact_exports = {};
|
|
|
6185
6561
|
__export(impact_exports, {
|
|
6186
6562
|
analyzeImpact: () => analyzeImpact
|
|
6187
6563
|
});
|
|
6188
|
-
import
|
|
6564
|
+
import path32 from "path";
|
|
6189
6565
|
import { execFile as execFile8 } from "child_process";
|
|
6190
6566
|
import { promisify as promisify8 } from "util";
|
|
6191
6567
|
async function analyzeImpact(rootDir, targetFiles) {
|
|
6192
|
-
const resolvedRoot =
|
|
6568
|
+
const resolvedRoot = path32.resolve(rootDir);
|
|
6193
6569
|
const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
|
|
6194
6570
|
const [cochange, references, tests] = await Promise.all([
|
|
6195
6571
|
getCochangeFiles(resolvedRoot, resolvedTargets),
|
|
@@ -6303,9 +6679,9 @@ async function getRelatedTests(rootDir, targetFiles) {
|
|
|
6303
6679
|
const testFiles = await (await createFileAccess(rootDir)).list(testPatterns);
|
|
6304
6680
|
const results = [];
|
|
6305
6681
|
for (const target of targetFiles) {
|
|
6306
|
-
const targetBaseName =
|
|
6682
|
+
const targetBaseName = path32.basename(target).replace(/\.[^.]+$/, "");
|
|
6307
6683
|
for (const testFile of testFiles) {
|
|
6308
|
-
const testBaseName =
|
|
6684
|
+
const testBaseName = path32.basename(testFile).replace(/\.[^.]+$/, "");
|
|
6309
6685
|
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
6310
6686
|
if (sourceName === targetBaseName) {
|
|
6311
6687
|
results.push({
|
|
@@ -6333,13 +6709,13 @@ var assemble_exports = {};
|
|
|
6333
6709
|
__export(assemble_exports, {
|
|
6334
6710
|
assembleContext: () => assembleContext
|
|
6335
6711
|
});
|
|
6336
|
-
import
|
|
6712
|
+
import path33 from "path";
|
|
6337
6713
|
import fs22 from "fs/promises";
|
|
6338
6714
|
async function collectImpact(root, candidates) {
|
|
6339
6715
|
const targets2 = /* @__PURE__ */ new Set();
|
|
6340
6716
|
let sourceFiles;
|
|
6341
6717
|
for (const candidate of sanitizeRepoPaths(candidates)) {
|
|
6342
|
-
const stat = await fs22.stat(
|
|
6718
|
+
const stat = await fs22.stat(path33.join(root, candidate)).catch(() => null);
|
|
6343
6719
|
if (stat?.isDirectory()) {
|
|
6344
6720
|
sourceFiles ??= await (await createFileAccess(root)).list();
|
|
6345
6721
|
for (const file of sourceFiles) {
|
|
@@ -6365,7 +6741,7 @@ async function collectImpact(root, candidates) {
|
|
|
6365
6741
|
};
|
|
6366
6742
|
}
|
|
6367
6743
|
async function assembleContext(rootDir, task, files) {
|
|
6368
|
-
const resolvedRoot =
|
|
6744
|
+
const resolvedRoot = path33.resolve(rootDir);
|
|
6369
6745
|
const mapState = await inspectSnapshot(resolvedRoot);
|
|
6370
6746
|
const snapshot = mapState.snapshot;
|
|
6371
6747
|
const store = await loadDecisionStore(resolvedRoot);
|
|
@@ -6562,7 +6938,7 @@ __export(observations_exports, {
|
|
|
6562
6938
|
readObservation: () => readObservation
|
|
6563
6939
|
});
|
|
6564
6940
|
import fs23 from "fs/promises";
|
|
6565
|
-
import { z as
|
|
6941
|
+
import { z as z24 } from "zod";
|
|
6566
6942
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
6567
6943
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
6568
6944
|
}
|
|
@@ -6633,19 +7009,19 @@ var init_observations = __esm({
|
|
|
6633
7009
|
init_storage();
|
|
6634
7010
|
init_model();
|
|
6635
7011
|
init_launcher();
|
|
6636
|
-
observationSchema =
|
|
6637
|
-
version:
|
|
6638
|
-
masonVersion:
|
|
6639
|
-
root:
|
|
6640
|
-
revision:
|
|
6641
|
-
host:
|
|
6642
|
-
contextCalls:
|
|
6643
|
-
lastContextAt:
|
|
6644
|
-
sessions:
|
|
6645
|
-
events:
|
|
6646
|
-
at:
|
|
6647
|
-
verificationStatus:
|
|
6648
|
-
reportPath:
|
|
7012
|
+
observationSchema = z24.object({
|
|
7013
|
+
version: z24.literal(1),
|
|
7014
|
+
masonVersion: z24.string(),
|
|
7015
|
+
root: z24.string(),
|
|
7016
|
+
revision: z24.string(),
|
|
7017
|
+
host: z24.enum(["codex", "claude"]),
|
|
7018
|
+
contextCalls: z24.number().int().nonnegative(),
|
|
7019
|
+
lastContextAt: z24.string().optional(),
|
|
7020
|
+
sessions: z24.record(z24.object({
|
|
7021
|
+
events: z24.array(z24.enum(events)),
|
|
7022
|
+
at: z24.string(),
|
|
7023
|
+
verificationStatus: z24.string().optional(),
|
|
7024
|
+
reportPath: z24.string().optional()
|
|
6649
7025
|
}))
|
|
6650
7026
|
});
|
|
6651
7027
|
}
|
|
@@ -6908,8 +7284,8 @@ __export(client_exports, {
|
|
|
6908
7284
|
function createConfluenceClient(config, fetchFn = fetch) {
|
|
6909
7285
|
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
6910
7286
|
const auth = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
|
|
6911
|
-
async function call(method,
|
|
6912
|
-
const res = await fetchFn(`${baseUrl}${
|
|
7287
|
+
async function call(method, path43, body) {
|
|
7288
|
+
const res = await fetchFn(`${baseUrl}${path43}`, {
|
|
6913
7289
|
method,
|
|
6914
7290
|
headers: {
|
|
6915
7291
|
Authorization: auth,
|
|
@@ -6921,7 +7297,7 @@ function createConfluenceClient(config, fetchFn = fetch) {
|
|
|
6921
7297
|
if (!res.ok) {
|
|
6922
7298
|
const text2 = await res.text();
|
|
6923
7299
|
throw new Error(
|
|
6924
|
-
`Confluence ${method} ${
|
|
7300
|
+
`Confluence ${method} ${path43} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
|
|
6925
7301
|
);
|
|
6926
7302
|
}
|
|
6927
7303
|
if (res.status === 204) return null;
|
|
@@ -7023,15 +7399,15 @@ __export(config_exports2, {
|
|
|
7023
7399
|
validateProvider: () => validateProvider
|
|
7024
7400
|
});
|
|
7025
7401
|
import fs24 from "fs/promises";
|
|
7026
|
-
import
|
|
7402
|
+
import path34 from "path";
|
|
7027
7403
|
import os3 from "os";
|
|
7028
7404
|
import { execFile as execFile9 } from "child_process";
|
|
7029
7405
|
import { promisify as promisify9 } from "util";
|
|
7030
7406
|
function configDir() {
|
|
7031
|
-
return
|
|
7407
|
+
return path34.join(os3.homedir(), ".mason");
|
|
7032
7408
|
}
|
|
7033
7409
|
function configFile() {
|
|
7034
|
-
return
|
|
7410
|
+
return path34.join(configDir(), "config.json");
|
|
7035
7411
|
}
|
|
7036
7412
|
async function loadConfig() {
|
|
7037
7413
|
try {
|
|
@@ -7204,16 +7580,16 @@ var init_renderer = __esm({
|
|
|
7204
7580
|
|
|
7205
7581
|
// src/confluence/diff.ts
|
|
7206
7582
|
import fs25 from "fs/promises";
|
|
7207
|
-
import
|
|
7583
|
+
import path35 from "path";
|
|
7208
7584
|
import { createHash as createHash6 } from "crypto";
|
|
7209
7585
|
function hashDescription(description) {
|
|
7210
7586
|
return createHash6("sha256").update(description, "utf8").digest("hex");
|
|
7211
7587
|
}
|
|
7212
7588
|
function syncStateDir(rootDir) {
|
|
7213
|
-
return
|
|
7589
|
+
return path35.join(rootDir, ".mason");
|
|
7214
7590
|
}
|
|
7215
7591
|
function syncStatePath(rootDir) {
|
|
7216
|
-
return
|
|
7592
|
+
return path35.join(syncStateDir(rootDir), "confluence-sync.json");
|
|
7217
7593
|
}
|
|
7218
7594
|
async function loadSyncState(rootDir) {
|
|
7219
7595
|
try {
|
|
@@ -7780,13 +8156,13 @@ init_advisory_review();
|
|
|
7780
8156
|
init_decisions();
|
|
7781
8157
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7782
8158
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7783
|
-
import { z as
|
|
8159
|
+
import { z as z25 } from "zod";
|
|
7784
8160
|
|
|
7785
8161
|
// src/mcp/tools.ts
|
|
7786
8162
|
init_repair();
|
|
7787
8163
|
init_advisory_review();
|
|
7788
8164
|
import fs26 from "fs/promises";
|
|
7789
|
-
import
|
|
8165
|
+
import path36 from "path";
|
|
7790
8166
|
import { execFile as execFile11 } from "child_process";
|
|
7791
8167
|
import { promisify as promisify11 } from "util";
|
|
7792
8168
|
|
|
@@ -7795,11 +8171,11 @@ init_audit();
|
|
|
7795
8171
|
init_types();
|
|
7796
8172
|
init_repair();
|
|
7797
8173
|
init_findings();
|
|
7798
|
-
import
|
|
8174
|
+
import path25 from "path";
|
|
7799
8175
|
|
|
7800
8176
|
// src/audit/advisory-cli.ts
|
|
7801
8177
|
init_advisory_review();
|
|
7802
|
-
import
|
|
8178
|
+
import path24 from "path";
|
|
7803
8179
|
import { stripVTControlCharacters } from "util";
|
|
7804
8180
|
|
|
7805
8181
|
// src/audit/cli.ts
|
|
@@ -7866,7 +8242,7 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
7866
8242
|
"- new-module: review whether the directory needs documenting; an omission alone does not establish a defect. Preserve scope and describe only what you verified."
|
|
7867
8243
|
);
|
|
7868
8244
|
lines.push(
|
|
7869
|
-
"-
|
|
8245
|
+
"- Specific historical ADVISORIES require a separate authorized assessment using review_advisory with the baselinePath and findingId from verification. Prepare and inspect evidence first; record addressed, inapplicable or deferred with the actual reviewer, note and reviewToken. Dependency notices marked resolution: informational require no review. Dependency advisories may be dismissed with action: dismiss, reasonCode (no-dependency-claims or unrelated-manifest-change), and note, without a reviewer or token. Decision findings use review_decision. Candidates marked resolution: recheck can be rerun to see whether the condition remains; this does not establish semantic approval. Report what remains unknown."
|
|
7870
8246
|
);
|
|
7871
8247
|
lines.push("");
|
|
7872
8248
|
lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
|
|
@@ -7897,10 +8273,10 @@ import { promisify as promisify3 } from "util";
|
|
|
7897
8273
|
|
|
7898
8274
|
// src/analyzers/base.ts
|
|
7899
8275
|
import fs16 from "fs/promises";
|
|
7900
|
-
import
|
|
8276
|
+
import fg8 from "fast-glob";
|
|
7901
8277
|
var BaseAnalyzer = class {
|
|
7902
8278
|
async findFiles(patterns, root) {
|
|
7903
|
-
return
|
|
8279
|
+
return fg8(patterns, {
|
|
7904
8280
|
cwd: root,
|
|
7905
8281
|
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
|
|
7906
8282
|
absolute: true
|
|
@@ -8112,7 +8488,7 @@ async function isGitRepo(dir) {
|
|
|
8112
8488
|
|
|
8113
8489
|
// src/mcp/sampler.ts
|
|
8114
8490
|
init_files();
|
|
8115
|
-
import
|
|
8491
|
+
import path26 from "path";
|
|
8116
8492
|
import { execFile as execFile4 } from "child_process";
|
|
8117
8493
|
import { promisify as promisify4 } from "util";
|
|
8118
8494
|
var exec2 = promisify4(execFile4);
|
|
@@ -8220,8 +8596,8 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
8220
8596
|
const projectConfig = access.config;
|
|
8221
8597
|
for (const filePath of projectConfig.alwaysInclude ?? []) {
|
|
8222
8598
|
if (selected.size >= maxFiles) break;
|
|
8223
|
-
const resolvedPath =
|
|
8224
|
-
if (!resolvedPath.startsWith(
|
|
8599
|
+
const resolvedPath = path26.resolve(rootDir, filePath);
|
|
8600
|
+
if (!resolvedPath.startsWith(path26.resolve(rootDir))) continue;
|
|
8225
8601
|
selected.set(filePath, "always-include (project config)");
|
|
8226
8602
|
}
|
|
8227
8603
|
let configCount = 0;
|
|
@@ -8287,7 +8663,7 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
8287
8663
|
if (!line) continue;
|
|
8288
8664
|
if (line.includes("node_modules") || line.includes("/build/") || line.includes(".gradle") || line.includes("/generated/"))
|
|
8289
8665
|
continue;
|
|
8290
|
-
const ext =
|
|
8666
|
+
const ext = path26.extname(line).slice(1);
|
|
8291
8667
|
if (!SOURCE_EXTENSIONS.includes(ext)) continue;
|
|
8292
8668
|
fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
|
|
8293
8669
|
}
|
|
@@ -8409,14 +8785,14 @@ init_files();
|
|
|
8409
8785
|
import { createHash as createHash4 } from "crypto";
|
|
8410
8786
|
import { execFile as execFile5 } from "child_process";
|
|
8411
8787
|
import { promisify as promisify5 } from "util";
|
|
8412
|
-
import { z as
|
|
8788
|
+
import { z as z11 } from "zod";
|
|
8413
8789
|
var exec3 = promisify5(execFile5);
|
|
8414
|
-
var requestSchema =
|
|
8415
|
-
id:
|
|
8416
|
-
action:
|
|
8417
|
-
reviewer:
|
|
8418
|
-
note:
|
|
8419
|
-
reviewToken:
|
|
8790
|
+
var requestSchema = z11.object({
|
|
8791
|
+
id: z11.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
8792
|
+
action: z11.enum(["prepare", "accept", "reaffirm", "retire"]).default("prepare"),
|
|
8793
|
+
reviewer: z11.string().trim().min(1).max(200).optional(),
|
|
8794
|
+
note: z11.string().trim().min(1).max(1500).optional(),
|
|
8795
|
+
reviewToken: z11.string().regex(/^[a-f0-9]{64}$/).optional()
|
|
8420
8796
|
});
|
|
8421
8797
|
async function reviewState(root, record) {
|
|
8422
8798
|
const [headHash, workingTree, changes] = await Promise.all([
|
|
@@ -8643,16 +9019,16 @@ init_lock();
|
|
|
8643
9019
|
init_storage();
|
|
8644
9020
|
init_snapshot();
|
|
8645
9021
|
import fs17 from "fs/promises";
|
|
8646
|
-
import { z as
|
|
9022
|
+
import { z as z12 } from "zod";
|
|
8647
9023
|
var DIRECTORY = ".mason/partial-snapshots";
|
|
8648
|
-
var partialSchema =
|
|
8649
|
-
batchId:
|
|
8650
|
-
offset:
|
|
8651
|
-
features:
|
|
8652
|
-
flows:
|
|
8653
|
-
savedAt:
|
|
9024
|
+
var partialSchema = z12.object({
|
|
9025
|
+
batchId: z12.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
9026
|
+
offset: z12.number().int().nonnegative(),
|
|
9027
|
+
features: z12.record(featureSchema),
|
|
9028
|
+
flows: z12.record(flowSchema),
|
|
9029
|
+
savedAt: z12.string()
|
|
8654
9030
|
});
|
|
8655
|
-
var scopeSchema2 =
|
|
9031
|
+
var scopeSchema2 = z12.object({ files: z12.array(z12.string()), savedAt: z12.string() });
|
|
8656
9032
|
async function savePartial(rootDir, partial) {
|
|
8657
9033
|
if (!/^[a-zA-Z0-9_-]+$/.test(partial.batchId)) throw new Error(`Invalid batchId: ${partial.batchId}`);
|
|
8658
9034
|
await withSnapshotWrite(rootDir, () => writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial)));
|
|
@@ -8704,7 +9080,7 @@ async function buildContext(dir) {
|
|
|
8704
9080
|
};
|
|
8705
9081
|
}
|
|
8706
9082
|
async function analyzeProject(dir) {
|
|
8707
|
-
const rootDir =
|
|
9083
|
+
const rootDir = path36.resolve(dir);
|
|
8708
9084
|
const context = await buildContext(rootDir);
|
|
8709
9085
|
const results = await runAll(context);
|
|
8710
9086
|
const projectSnapshot = await detectProjectSnapshot(rootDir);
|
|
@@ -8759,7 +9135,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8759
9135
|
const present = [];
|
|
8760
9136
|
for (const file of buildFiles) {
|
|
8761
9137
|
try {
|
|
8762
|
-
await fs26.access(
|
|
9138
|
+
await fs26.access(path36.join(rootDir, file));
|
|
8763
9139
|
present.push(file);
|
|
8764
9140
|
} catch {
|
|
8765
9141
|
}
|
|
@@ -8801,7 +9177,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8801
9177
|
const sourceFiles = await access.list();
|
|
8802
9178
|
const fileCounts = {};
|
|
8803
9179
|
for (const file of sourceFiles) {
|
|
8804
|
-
const ext =
|
|
9180
|
+
const ext = path36.extname(file).slice(1);
|
|
8805
9181
|
fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
|
|
8806
9182
|
}
|
|
8807
9183
|
return {
|
|
@@ -8812,7 +9188,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8812
9188
|
};
|
|
8813
9189
|
}
|
|
8814
9190
|
async function getCodeSamples(dir, count4 = 15) {
|
|
8815
|
-
const rootDir =
|
|
9191
|
+
const rootDir = path36.resolve(dir);
|
|
8816
9192
|
const samples = await sampleFiles(rootDir, count4);
|
|
8817
9193
|
const output = {
|
|
8818
9194
|
note: "These are previews (first ~60 lines). Read the file directly with your own tools to see it in full.",
|
|
@@ -8855,7 +9231,7 @@ async function unmappedContextResponse(rootDir) {
|
|
|
8855
9231
|
});
|
|
8856
9232
|
}
|
|
8857
9233
|
async function getProjectStructure(dir) {
|
|
8858
|
-
const rootDir =
|
|
9234
|
+
const rootDir = path36.resolve(dir);
|
|
8859
9235
|
const allFiles = await (await createFileAccess(rootDir)).list("**/*");
|
|
8860
9236
|
const dirInfo = /* @__PURE__ */ new Map();
|
|
8861
9237
|
for (const file of allFiles) {
|
|
@@ -8867,7 +9243,7 @@ async function getProjectStructure(dir) {
|
|
|
8867
9243
|
}
|
|
8868
9244
|
const info = dirInfo.get(dirPath);
|
|
8869
9245
|
info.fileCount++;
|
|
8870
|
-
const ext =
|
|
9246
|
+
const ext = path36.extname(file).slice(1);
|
|
8871
9247
|
if (ext) {
|
|
8872
9248
|
info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
|
|
8873
9249
|
}
|
|
@@ -8912,7 +9288,7 @@ async function buildChangedFilePreviews(rootDir, changedFiles) {
|
|
|
8912
9288
|
return previews2;
|
|
8913
9289
|
}
|
|
8914
9290
|
async function getSnapshot(dir) {
|
|
8915
|
-
const rootDir =
|
|
9291
|
+
const rootDir = path36.resolve(dir);
|
|
8916
9292
|
const snapshot = await loadSnapshot(rootDir);
|
|
8917
9293
|
if (!snapshot) {
|
|
8918
9294
|
return unmappedContextResponse(rootDir);
|
|
@@ -9013,7 +9389,7 @@ function driftHint(report) {
|
|
|
9013
9389
|
return "Read the changed files under staleFeatures/staleFlows, update those entries (fold unmappedFiles into the right features), and call save_snapshot with only the affected entries \u2014 unchanged entries are preserved. Drop ghostFiles from any entries that reference them, and delete features/flows that no longer exist via save_snapshot's removeFeatures/removeFlows.";
|
|
9014
9390
|
}
|
|
9015
9391
|
async function checkDrift(dir) {
|
|
9016
|
-
const rootDir =
|
|
9392
|
+
const rootDir = path36.resolve(dir);
|
|
9017
9393
|
const report = await computeDrift(rootDir);
|
|
9018
9394
|
if (!report) {
|
|
9019
9395
|
return JSON.stringify({
|
|
@@ -9044,7 +9420,7 @@ async function checkDrift(dir) {
|
|
|
9044
9420
|
return JSON.stringify({ exists: true, ...report, verification, hint });
|
|
9045
9421
|
}
|
|
9046
9422
|
async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_SIZE, files) {
|
|
9047
|
-
const rootDir =
|
|
9423
|
+
const rootDir = path36.resolve(dir);
|
|
9048
9424
|
const scoped = files !== void 0 && files.length > 0;
|
|
9049
9425
|
const scopeFiles = scoped ? sanitizePaths(rootDir, files) : void 0;
|
|
9050
9426
|
const batch = await prepareSnapshotBatch(dir, offset, batchSize, scopeFiles);
|
|
@@ -9091,7 +9467,7 @@ async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_
|
|
|
9091
9467
|
);
|
|
9092
9468
|
}
|
|
9093
9469
|
async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
|
|
9094
|
-
const rootDir =
|
|
9470
|
+
const rootDir = path36.resolve(dir);
|
|
9095
9471
|
for (const feat of Object.values(features)) {
|
|
9096
9472
|
feat.files = sanitizePaths(rootDir, feat.files);
|
|
9097
9473
|
if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
|
|
@@ -9120,7 +9496,7 @@ async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
|
|
|
9120
9496
|
);
|
|
9121
9497
|
}
|
|
9122
9498
|
async function reduceSnapshot(dir) {
|
|
9123
|
-
const rootDir =
|
|
9499
|
+
const rootDir = path36.resolve(dir);
|
|
9124
9500
|
const partials = await loadAllPartials(rootDir);
|
|
9125
9501
|
if (partials.length === 0) {
|
|
9126
9502
|
return JSON.stringify(
|
|
@@ -9182,7 +9558,7 @@ async function reduceSnapshot(dir) {
|
|
|
9182
9558
|
);
|
|
9183
9559
|
}
|
|
9184
9560
|
async function fullAnalysis(dir) {
|
|
9185
|
-
const rootDir =
|
|
9561
|
+
const rootDir = path36.resolve(dir);
|
|
9186
9562
|
const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
|
|
9187
9563
|
analyzeProject(dir),
|
|
9188
9564
|
getProjectStructure(dir),
|
|
@@ -9211,7 +9587,7 @@ function sanitizePaths(rootDir, files) {
|
|
|
9211
9587
|
return sanitizeRepoPaths(files);
|
|
9212
9588
|
}
|
|
9213
9589
|
async function saveSnapshotData(dir, features, flows, removeFeatures = [], removeFlows = []) {
|
|
9214
|
-
const rootDir =
|
|
9590
|
+
const rootDir = path36.resolve(dir);
|
|
9215
9591
|
return updateSnapshot(rootDir, async (previous) => {
|
|
9216
9592
|
const gitHash = await getCurrentGitHash(rootDir);
|
|
9217
9593
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9291,14 +9667,14 @@ async function saveSnapshotData(dir, features, flows, removeFeatures = [], remov
|
|
|
9291
9667
|
});
|
|
9292
9668
|
}
|
|
9293
9669
|
async function getImpact(dir, files) {
|
|
9294
|
-
const rootDir =
|
|
9670
|
+
const rootDir = path36.resolve(dir);
|
|
9295
9671
|
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
9296
9672
|
const result = await analyzeImpact2(rootDir, files);
|
|
9297
9673
|
return JSON.stringify(result, null, 2);
|
|
9298
9674
|
}
|
|
9299
9675
|
var VERIFY_DEFAULT_SAMPLE = 5;
|
|
9300
9676
|
async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
9301
|
-
const rootDir =
|
|
9677
|
+
const rootDir = path36.resolve(dir);
|
|
9302
9678
|
const snapshot = await loadSnapshot(rootDir);
|
|
9303
9679
|
if (!snapshot) {
|
|
9304
9680
|
return JSON.stringify({
|
|
@@ -9351,7 +9727,7 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
|
9351
9727
|
});
|
|
9352
9728
|
}
|
|
9353
9729
|
async function saveVerification(dir, verdicts) {
|
|
9354
|
-
const rootDir =
|
|
9730
|
+
const rootDir = path36.resolve(dir);
|
|
9355
9731
|
return updateSnapshot(rootDir, async (snapshot) => {
|
|
9356
9732
|
if (!snapshot) {
|
|
9357
9733
|
return { snapshot: null, result: JSON.stringify({ exists: false, stamped: [], hint: "No concept map exists. Build it and run verify_snapshot before recording verdicts." }) };
|
|
@@ -9425,16 +9801,16 @@ async function saveVerification(dir, verdicts) {
|
|
|
9425
9801
|
});
|
|
9426
9802
|
}
|
|
9427
9803
|
async function saveDecision(dir, input) {
|
|
9428
|
-
const rootDir =
|
|
9804
|
+
const rootDir = path36.resolve(dir);
|
|
9429
9805
|
const { upsertDecision: upsertDecision2 } = await Promise.resolve().then(() => (init_decisions(), decisions_exports));
|
|
9430
9806
|
const result = await upsertDecision2(rootDir, input);
|
|
9431
9807
|
return JSON.stringify(result);
|
|
9432
9808
|
}
|
|
9433
9809
|
async function reviewDecision2(dir, input) {
|
|
9434
|
-
return JSON.stringify(await reviewDecision(
|
|
9810
|
+
return JSON.stringify(await reviewDecision(path36.resolve(dir), input));
|
|
9435
9811
|
}
|
|
9436
9812
|
async function getContext(dir, task, files) {
|
|
9437
|
-
const rootDir =
|
|
9813
|
+
const rootDir = path36.resolve(dir);
|
|
9438
9814
|
const { assembleContext: assembleContext2 } = await Promise.resolve().then(() => (init_assemble(), assemble_exports));
|
|
9439
9815
|
const bundle = await assembleContext2(rootDir, task, files);
|
|
9440
9816
|
return JSON.stringify(bundle);
|
|
@@ -9483,7 +9859,7 @@ async function masonInit(dir, options = {}) {
|
|
|
9483
9859
|
return JSON.stringify(await setupProject2(dir, options), null, 2);
|
|
9484
9860
|
}
|
|
9485
9861
|
if (options.host) throw new Error("host applies only to mode: setup.");
|
|
9486
|
-
const rootDir =
|
|
9862
|
+
const rootDir = path36.resolve(dir);
|
|
9487
9863
|
const marker = await loadProjectMarker(rootDir);
|
|
9488
9864
|
const { loadSetup: loadSetup2 } = await Promise.resolve().then(() => (init_model(), model_exports));
|
|
9489
9865
|
const setup = await loadSetup2(rootDir);
|
|
@@ -9503,7 +9879,7 @@ async function masonInit(dir, options = {}) {
|
|
|
9503
9879
|
);
|
|
9504
9880
|
}
|
|
9505
9881
|
async function masonCompleteInit(dir, options = {}) {
|
|
9506
|
-
const rootDir =
|
|
9882
|
+
const rootDir = path36.resolve(dir);
|
|
9507
9883
|
const existing = await loadProjectMarker(rootDir);
|
|
9508
9884
|
const features = await loadProjectFeatures(rootDir);
|
|
9509
9885
|
const marker = {
|
|
@@ -9612,7 +9988,7 @@ async function masonSetConfluence(input) {
|
|
|
9612
9988
|
);
|
|
9613
9989
|
}
|
|
9614
9990
|
async function exportToConfluenceTool(dir, overrides) {
|
|
9615
|
-
const rootDir =
|
|
9991
|
+
const rootDir = path36.resolve(dir);
|
|
9616
9992
|
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
|
|
9617
9993
|
const { exportToConfluence: exportToConfluence2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
9618
9994
|
const config = await loadConfig2();
|
|
@@ -9651,21 +10027,21 @@ function createMcpServer() {
|
|
|
9651
10027
|
const server = new McpServer(
|
|
9652
10028
|
{
|
|
9653
10029
|
name: "mason",
|
|
9654
|
-
version: "0.19.
|
|
10030
|
+
version: "0.19.2"
|
|
9655
10031
|
},
|
|
9656
10032
|
{
|
|
9657
|
-
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.'
|
|
10033
|
+
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 as soon as evidence supports them, without waiting for implementation or merge; 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.'
|
|
9658
10034
|
}
|
|
9659
10035
|
);
|
|
9660
10036
|
server.tool(
|
|
9661
10037
|
"mason_init",
|
|
9662
10038
|
"Inspect this project now: returns documentation audit findings, committed-diff review findings, decision/map status, and a quickstart playbook. Quickstart and map modes are read-only and deterministic. Explicit mode: setup configures MCP, instructions and lifecycle hooks to use mason on PATH while retaining original audit evidence; use it only when the user requests setup. Optional host selects codex or claude. Optional base selects the review comparison; evidence imports CI manifests with check outcomes, commit freshness, and links to changed files and accepted decisions. mode: map returns the full Map-Reduce build workflow. Repeat calls refresh findings even after setup.",
|
|
9663
10039
|
{
|
|
9664
|
-
dir:
|
|
9665
|
-
mode:
|
|
9666
|
-
host:
|
|
9667
|
-
base:
|
|
9668
|
-
evidence:
|
|
10040
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10041
|
+
mode: z25.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
|
|
10042
|
+
host: z25.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
|
|
10043
|
+
base: z25.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
|
|
10044
|
+
evidence: z25.array(z25.string()).max(10).optional().describe("Repository-local CI evidence manifests to include in the review. Imports Vitest JSON, SARIF, and native-validator mason-check-json results without executing check commands.")
|
|
9669
10045
|
},
|
|
9670
10046
|
async ({ dir, mode, host, base, evidence }) => {
|
|
9671
10047
|
const result = await masonInit(dir, { mode, host, base, evidence });
|
|
@@ -9674,10 +10050,10 @@ function createMcpServer() {
|
|
|
9674
10050
|
);
|
|
9675
10051
|
server.tool(
|
|
9676
10052
|
"mason_automation",
|
|
9677
|
-
"Inspect installed automation
|
|
10053
|
+
"Inspect installed automation, observed host events, pending decision records and the notification baseline, or check all retained documentation findings across sessions. status is read-only; check saves local baselines and verification reports without editing source, approving advisories or consuming pending completion notices. Returns concise results with a full report path. Works without a map.",
|
|
9678
10054
|
{
|
|
9679
|
-
dir:
|
|
9680
|
-
action:
|
|
10055
|
+
dir: z25.string().describe("Absolute path to the project directory"),
|
|
10056
|
+
action: z25.enum(["status", "check"]).describe("Inspect configuration and receipts, or capture/resume and verify original audit evidence.")
|
|
9681
10057
|
},
|
|
9682
10058
|
async ({ dir, action }) => ({ content: [{ type: "text", text: await masonAutomation(dir, action) }] })
|
|
9683
10059
|
);
|
|
@@ -9685,10 +10061,10 @@ function createMcpServer() {
|
|
|
9685
10061
|
"mason_repair",
|
|
9686
10062
|
"Track documentation repairs against original audit evidence. prepare saves a local baseline and returns a scoped work order; verify reads that baseline and reports resolved, unresolved, review-required, unverified, and new findings. Suppressed advisories remain unresolved. Does not edit documentation or approve decisions. No map required.",
|
|
9687
10063
|
{
|
|
9688
|
-
dir:
|
|
9689
|
-
action:
|
|
9690
|
-
baselinePath:
|
|
9691
|
-
checks:
|
|
10064
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10065
|
+
action: z25.enum(["prepare", "verify"]),
|
|
10066
|
+
baselinePath: z25.string().optional().describe("Original baseline path returned by prepare; required for verify."),
|
|
10067
|
+
checks: z25.array(z25.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"])).min(1).optional().describe("Optional audit check subset for prepare. Verification always uses the original checks.")
|
|
9692
10068
|
},
|
|
9693
10069
|
async ({ dir, action, baselinePath, checks }) => {
|
|
9694
10070
|
const result = await masonRepair(dir, { action, baselinePath, checks });
|
|
@@ -9697,16 +10073,16 @@ function createMcpServer() {
|
|
|
9697
10073
|
);
|
|
9698
10074
|
server.tool(
|
|
9699
10075
|
"review_advisory",
|
|
9700
|
-
"Prepare an assessment of an original repair advisory and its exact repository scope, then record authorized addressed, inapplicable or deferred outcomes with reviewToken, reviewer and note. Relevant changes reopen reviews; unrelated commits preserve them. Records belong in Git. Decision findings route to review_decision; this tool never approves decisions. Recorded identities are assertions, not authenticated approvals.",
|
|
9701
|
-
{ dir:
|
|
10076
|
+
"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.",
|
|
10077
|
+
{ dir: z25.string().describe("Absolute path to the project root"), ...advisoryReviewRequest.shape },
|
|
9702
10078
|
async ({ dir, ...request }) => ({ content: [{ type: "text", text: await masonReviewAdvisory(dir, request) }] })
|
|
9703
10079
|
);
|
|
9704
10080
|
server.tool(
|
|
9705
10081
|
"mason_complete_init",
|
|
9706
10082
|
"Record assistant instruction setup locally in ignored .mason/local/project.json, with feature settings in shared .mason/config.json. Other tools work without this marker. Repeated calls preserve the original setup time and existing settings; pass confluenceConfigured only to change that setting.",
|
|
9707
10083
|
{
|
|
9708
|
-
dir:
|
|
9709
|
-
confluenceConfigured:
|
|
10084
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10085
|
+
confluenceConfigured: z25.boolean().optional().describe("Set the Confluence setup status; omit to preserve the existing value")
|
|
9710
10086
|
},
|
|
9711
10087
|
async ({ dir, confluenceConfigured }) => {
|
|
9712
10088
|
const result = await masonCompleteInit(dir, { confluenceConfigured });
|
|
@@ -9717,11 +10093,11 @@ function createMcpServer() {
|
|
|
9717
10093
|
"mason_set_confluence",
|
|
9718
10094
|
"Configure Confluence credentials. Two-step flow: (1) call without `spaceKey` to validate the credentials and receive a list of available spaces \u2014 relay them to the user. (2) call again with the same `baseUrl`/`email`/`apiToken` plus the chosen `spaceKey` to persist. Credentials are stored in `~/.mason/config.json`. Warn the user that the API token will be visible in chat history before they paste it.",
|
|
9719
10095
|
{
|
|
9720
|
-
baseUrl:
|
|
9721
|
-
email:
|
|
9722
|
-
apiToken:
|
|
9723
|
-
spaceKey:
|
|
9724
|
-
parentPageId:
|
|
10096
|
+
baseUrl: z25.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
|
|
10097
|
+
email: z25.string().describe("User's Atlassian account email"),
|
|
10098
|
+
apiToken: z25.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
|
|
10099
|
+
spaceKey: z25.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
|
|
10100
|
+
parentPageId: z25.string().optional().describe("Optional parent page ID under which Mason's index page is created")
|
|
9725
10101
|
},
|
|
9726
10102
|
async ({ baseUrl, email, apiToken, spaceKey, parentPageId }) => {
|
|
9727
10103
|
const result = await masonSetConfluence({
|
|
@@ -9738,7 +10114,7 @@ function createMcpServer() {
|
|
|
9738
10114
|
"full_analysis",
|
|
9739
10115
|
"One-shot orientation for a project WITHOUT a concept map (get_snapshot returned exists:false). Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source mapping. On a mapped project, prefer get_snapshot \u2014 it is cheaper and answers feature/architecture questions directly.",
|
|
9740
10116
|
{
|
|
9741
|
-
dir:
|
|
10117
|
+
dir: z25.string().describe("Absolute path to the project root directory")
|
|
9742
10118
|
},
|
|
9743
10119
|
async ({ dir }) => {
|
|
9744
10120
|
const result = await fullAnalysis(dir);
|
|
@@ -9751,7 +10127,7 @@ function createMcpServer() {
|
|
|
9751
10127
|
"analyze_project",
|
|
9752
10128
|
"Run git history analysis on a codebase. Returns commit convention patterns, stale directories, and frequently changed files. These are aggregate stats across hundreds of commits that would be expensive to compute manually.",
|
|
9753
10129
|
{
|
|
9754
|
-
dir:
|
|
10130
|
+
dir: z25.string().describe("Absolute path to the project root directory")
|
|
9755
10131
|
},
|
|
9756
10132
|
async ({ dir }) => {
|
|
9757
10133
|
const result = await analyzeProject(dir);
|
|
@@ -9764,8 +10140,8 @@ function createMcpServer() {
|
|
|
9764
10140
|
"get_code_samples",
|
|
9765
10141
|
"Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Read files natively for full content.",
|
|
9766
10142
|
{
|
|
9767
|
-
dir:
|
|
9768
|
-
count:
|
|
10143
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10144
|
+
count: z25.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
|
|
9769
10145
|
},
|
|
9770
10146
|
async ({ dir, count: count4 }) => {
|
|
9771
10147
|
const result = await getCodeSamples(dir, count4);
|
|
@@ -9778,7 +10154,7 @@ function createMcpServer() {
|
|
|
9778
10154
|
"get_snapshot",
|
|
9779
10155
|
"Return the optional feature-to-file architecture map with drift and trust evidence. Map verification is rechecked against sampled source contents; stale or unknown verification requires a fresh review, and recordedVerdict preserves the historical verdict. If no map exists, returns exists:false plus project structure, Git signals, and test pairs. Decision capture, get_context, and get_impact still work. No initialization required.",
|
|
9780
10156
|
{
|
|
9781
|
-
dir:
|
|
10157
|
+
dir: z25.string().describe("Absolute path to the project root directory")
|
|
9782
10158
|
},
|
|
9783
10159
|
async ({ dir }) => {
|
|
9784
10160
|
const result = await getSnapshot(dir);
|
|
@@ -9791,9 +10167,9 @@ function createMcpServer() {
|
|
|
9791
10167
|
"get_context",
|
|
9792
10168
|
"Assemble task context: matching decisions with rationale, approval, owner, sources, last review, and freshness, plus related tests, file impact, and any available map entries. Map verification is rechecked against sampled source contents; stale or unknown verification requires a fresh review, and recordedVerdict preserves the historical verdict. Proposals are suggestions; legacy records are unreviewed; accepted decisions are constraints subject to freshness. No initialization or map required. Pass task and optional files. map.status and diagnostics preserve missing or invalid knowledge. Impact covers up to three unique files, expanding directory anchors.",
|
|
9793
10169
|
{
|
|
9794
|
-
dir:
|
|
9795
|
-
task:
|
|
9796
|
-
files:
|
|
10170
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10171
|
+
task: z25.string().describe("The task, bug, or change request in natural language \u2014 e.g. 'add rate limiting to the API client' or a ticket description"),
|
|
10172
|
+
files: z25.array(z25.string()).optional().describe("Optional file paths already known to be involved (e.g. from a diff or stack trace). Entries containing them are boosted above pure text matches.")
|
|
9797
10173
|
},
|
|
9798
10174
|
async ({ dir, task, files }) => {
|
|
9799
10175
|
const result = await getContext(dir, task, files);
|
|
@@ -9809,10 +10185,10 @@ function createMcpServer() {
|
|
|
9809
10185
|
"generate_snapshot_batch",
|
|
9810
10186
|
"Map step of the concept-map build. Returns one batch of source files (skeletons of every file in the batch plus a few deeper-read bodies for grounding), along with a system prompt instructing you to derive features and flows for ONLY this batch. Call repeatedly with the returned `nextOffset` until it is null, calling `save_partial_snapshot` between each call. Use product-natural feature names so partials merge cleanly in the reduce step.",
|
|
9811
10187
|
{
|
|
9812
|
-
dir:
|
|
9813
|
-
offset:
|
|
9814
|
-
batchSize:
|
|
9815
|
-
files:
|
|
10188
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10189
|
+
offset: z25.number().int().optional().describe("0-indexed file offset to start the batch at. Omit on the first call; pass the `nextOffset` from the previous response for subsequent calls."),
|
|
10190
|
+
batchSize: z25.number().int().optional().describe("Files per batch. Defaults to 50."),
|
|
10191
|
+
files: z25.array(z25.string()).optional().describe("Scope the batch walk to this explicit file list \u2014 e.g. the drift set from mason_check_drift (changedFiles + unmappedFiles). Pass the SAME list on every batch call of one refresh run. Triggers refresh mode: reduce_snapshot will merge the partials into the existing map instead of rebuilding it.")
|
|
9816
10192
|
},
|
|
9817
10193
|
async ({ dir, offset, batchSize, files }) => {
|
|
9818
10194
|
const result = await generateSnapshotBatch(dir, offset, batchSize, files);
|
|
@@ -9825,23 +10201,23 @@ function createMcpServer() {
|
|
|
9825
10201
|
"save_partial_snapshot",
|
|
9826
10202
|
"Persist the partial concept map you derived for one batch. Call this once per batch, with the `batchId` from the `generate_snapshot_batch` response. Partials accumulate in `.mason/partial-snapshots/` and are merged in the reduce step.",
|
|
9827
10203
|
{
|
|
9828
|
-
dir:
|
|
9829
|
-
batchId:
|
|
9830
|
-
offset:
|
|
9831
|
-
features:
|
|
9832
|
-
|
|
9833
|
-
description:
|
|
9834
|
-
files:
|
|
9835
|
-
tests:
|
|
9836
|
-
type:
|
|
10204
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10205
|
+
batchId: z25.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
|
|
10206
|
+
offset: z25.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
|
|
10207
|
+
features: z25.record(
|
|
10208
|
+
z25.object({
|
|
10209
|
+
description: z25.string(),
|
|
10210
|
+
files: z25.array(z25.string()),
|
|
10211
|
+
tests: z25.array(z25.string()).optional(),
|
|
10212
|
+
type: z25.enum(["capability", "infrastructure"]).optional().describe(
|
|
9837
10213
|
'"capability" (user-facing functionality) or "infrastructure" (internal plumbing with no end user \u2014 DI/service wiring, config, logging, adapters). Defaults to "capability".'
|
|
9838
10214
|
)
|
|
9839
10215
|
})
|
|
9840
10216
|
).describe("Partial features for this batch only \u2014 files outside the batch will be added by other partials."),
|
|
9841
|
-
flows:
|
|
9842
|
-
|
|
9843
|
-
description:
|
|
9844
|
-
chain:
|
|
10217
|
+
flows: z25.record(
|
|
10218
|
+
z25.object({
|
|
10219
|
+
description: z25.string(),
|
|
10220
|
+
chain: z25.array(z25.string())
|
|
9845
10221
|
})
|
|
9846
10222
|
).describe("Partial flows whose entire chain is in this batch. Cross-batch flows are reconstructed in reduce.")
|
|
9847
10223
|
},
|
|
@@ -9856,7 +10232,7 @@ function createMcpServer() {
|
|
|
9856
10232
|
"reduce_snapshot",
|
|
9857
10233
|
"Reduce step of the concept-map build. Returns every partial snapshot plus a system prompt asking you to merge them into one coherent project-wide map. Resolve platform variants into single product features, dedupe near-duplicates, and ensure no file is dropped. After producing the unified map, call `save_snapshot` to persist it (this also clears the partials).",
|
|
9858
10234
|
{
|
|
9859
|
-
dir:
|
|
10235
|
+
dir: z25.string().describe("Absolute path to the project root directory")
|
|
9860
10236
|
},
|
|
9861
10237
|
async ({ dir }) => {
|
|
9862
10238
|
const result = await reduceSnapshot(dir);
|
|
@@ -9869,25 +10245,25 @@ function createMcpServer() {
|
|
|
9869
10245
|
"save_snapshot",
|
|
9870
10246
|
"Save a concept-to-files map as a persistent project snapshot. Maps feature names and data flows to the files that implement them. Persists across conversations \u2014 future sessions can call get_snapshot to instantly find relevant files. No API key needed \u2014 you are the LLM generating the map.",
|
|
9871
10247
|
{
|
|
9872
|
-
dir:
|
|
9873
|
-
features:
|
|
9874
|
-
|
|
9875
|
-
description:
|
|
9876
|
-
files:
|
|
9877
|
-
tests:
|
|
9878
|
-
type:
|
|
10248
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10249
|
+
features: z25.record(
|
|
10250
|
+
z25.object({
|
|
10251
|
+
description: z25.string().describe("One-line description of the feature"),
|
|
10252
|
+
files: z25.array(z25.string()).describe("File paths that implement this feature"),
|
|
10253
|
+
tests: z25.array(z25.string()).optional().describe("Test file paths for this feature"),
|
|
10254
|
+
type: z25.enum(["capability", "infrastructure"]).optional().describe(
|
|
9879
10255
|
'Classification: "capability" for user-facing functionality, "infrastructure" for internal plumbing with no end user (DI/service wiring, config, logging, adapters). Capabilities are published to Confluence; infrastructure stays in the AI concept map only. Defaults to "capability".'
|
|
9880
10256
|
)
|
|
9881
10257
|
})
|
|
9882
10258
|
).describe("Map of feature names to their implementing files"),
|
|
9883
|
-
flows:
|
|
9884
|
-
|
|
9885
|
-
description:
|
|
9886
|
-
chain:
|
|
10259
|
+
flows: z25.record(
|
|
10260
|
+
z25.object({
|
|
10261
|
+
description: z25.string().describe("One-line description of the flow"),
|
|
10262
|
+
chain: z25.array(z25.string()).describe("Ordered list of file paths showing data/call flow")
|
|
9887
10263
|
})
|
|
9888
10264
|
).describe("Map of flow names to ordered file chains"),
|
|
9889
|
-
removeFeatures:
|
|
9890
|
-
removeFlows:
|
|
10265
|
+
removeFeatures: z25.array(z25.string()).optional().describe("Feature names to delete from the existing map \u2014 for features that were renamed or no longer exist. Applied before merging; only meaningful on incremental saves."),
|
|
10266
|
+
removeFlows: z25.array(z25.string()).optional().describe("Flow names to delete from the existing map. Applied before merging; only meaningful on incremental saves.")
|
|
9891
10267
|
},
|
|
9892
10268
|
async ({ dir, features, flows, removeFeatures, removeFlows }) => {
|
|
9893
10269
|
const result = await saveSnapshotData(
|
|
@@ -9904,19 +10280,19 @@ function createMcpServer() {
|
|
|
9904
10280
|
);
|
|
9905
10281
|
server.tool(
|
|
9906
10282
|
"save_decision",
|
|
9907
|
-
"Capture or revise a decision proposal
|
|
10283
|
+
"Capture or revise a decision proposal as soon as the lesson is supported by evidence, without waiting for implementation or merge. Keep unimplemented remedies tentative. Include rationale, anchors, optional owner, sources, and a known actor. Compare matching get_context records and pending proposals before saving. When new evidence changes the same lesson's assumptions, scope, or recommended action, pass its existing id, including for accepted records. Preserve supported rationale and sources and replace obsolete instructions. Create a new record for a genuinely distinct lesson; skip unchanged restatements. No setup or map required. Writes a local record and preserves content history. Changes create a pending proposal while the last accepted revision remains operative; unchanged content does not re-verify or refresh it. Use review_decision for authorized acceptance or reaffirmation. A proposal cannot supersede a record with an operative accepted revision; review its replacement and retire the original separately.",
|
|
9908
10284
|
{
|
|
9909
|
-
dir:
|
|
10285
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
9910
10286
|
title: decisionTitleSchema.describe(`Short, specific headline (max ${TITLE_MAX_CHARS} characters). Generates a stable id on creation; revising the title keeps the existing id.`),
|
|
9911
10287
|
body: decisionBodySchema.describe(`The knowledge itself: what was tried/decided, why, and what to avoid. Must contain information NOT derivable by reading the code. Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer; up to ${BODY_MAX_CHARS} is accepted with a warning above the target. Matching context and hooks include the full body. Preserve exceptions; use sources for supporting references.`),
|
|
9912
|
-
category:
|
|
9913
|
-
files:
|
|
9914
|
-
id:
|
|
9915
|
-
supersedes:
|
|
10288
|
+
category: z25.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
10289
|
+
files: z25.array(z25.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
|
|
10290
|
+
id: z25.string().optional().describe("Pass the matching record's existing id when evidence changes that lesson's assumptions, scope, or recommended action, including for accepted records. Changed content becomes a proposal while the accepted version remains operative; unchanged content leaves review and freshness untouched."),
|
|
10291
|
+
supersedes: z25.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
|
|
9916
10292
|
owner: attributionSchema.shape.owner.describe("Responsible person or team, when known. Null clears it. Required for acceptance."),
|
|
9917
10293
|
sources: attributionSchema.shape.sources.describe("Known PR, issue, incident, discussion, or document references. Omit to preserve; [] clears. At least one is required for acceptance."),
|
|
9918
10294
|
actor: attributionSchema.shape.actor.describe("Known person or agent recording this revision. Omit if unknown; do not infer from Git identity."),
|
|
9919
|
-
force:
|
|
10295
|
+
force: z25.boolean().optional().describe("Save even when a near-duplicate was detected")
|
|
9920
10296
|
},
|
|
9921
10297
|
async ({ dir, title, body, category, files, id, supersedes, force, owner, sources, actor }) => {
|
|
9922
10298
|
const result = await saveDecision(dir, {
|
|
@@ -9938,12 +10314,12 @@ function createMcpServer() {
|
|
|
9938
10314
|
"review_decision",
|
|
9939
10315
|
"Prepare a decision review: returns the full record and history, any operative accepted revision, provenance, changes and previews for both sets of anchors, and a reviewToken. Then record accept, reaffirm, or retire with that token, the authorized reviewer, and a reason. Acceptance replaces the operative revision; retirement withdraws the entire record including its proposal. Acceptance requires owner, source, readable Git HEAD, and committed anchor changes. Changed records or code invalidate the token. Identities and approvals are recorded assertions for normal PR review, not authenticated proof.",
|
|
9940
10316
|
{
|
|
9941
|
-
dir:
|
|
9942
|
-
id:
|
|
9943
|
-
action:
|
|
9944
|
-
reviewer:
|
|
9945
|
-
note:
|
|
9946
|
-
reviewToken:
|
|
10317
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10318
|
+
id: z25.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
|
|
10319
|
+
action: z25.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
|
|
10320
|
+
reviewer: z25.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
|
|
10321
|
+
note: z25.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
|
|
10322
|
+
reviewToken: z25.string().regex(/^[a-f0-9]{64}$/).optional().describe("Token from the prepared review; rejects stale record or code revisions")
|
|
9947
10323
|
},
|
|
9948
10324
|
async ({ dir, ...input }) => {
|
|
9949
10325
|
const result = await reviewDecision2(dir, input);
|
|
@@ -9954,7 +10330,7 @@ function createMcpServer() {
|
|
|
9954
10330
|
"mason_check_drift",
|
|
9955
10331
|
"Check how far the concept map has drifted from HEAD. Deterministic (git + filesystem, no LLM). Returns which features/flows are stale and the changed files behind them, new source files not yet mapped, ghost files (mapped but deleted), renames, and a `recommendation`: `up-to-date` (nothing to do), `incremental` (update just the stale entries via save_snapshot), or `full-rebuild` (re-run the Map-Reduce build). Call this before trusting the map in a long session, or periodically to keep the map and any synced wikis fresh.",
|
|
9956
10332
|
{
|
|
9957
|
-
dir:
|
|
10333
|
+
dir: z25.string().describe("Absolute path to the project root directory")
|
|
9958
10334
|
},
|
|
9959
10335
|
async ({ dir }) => {
|
|
9960
10336
|
const result = await checkDrift(dir);
|
|
@@ -9967,8 +10343,8 @@ function createMcpServer() {
|
|
|
9967
10343
|
"verify_snapshot",
|
|
9968
10344
|
"Spot-check the concept map's CORRECTNESS (drift checks freshness; this checks entries were right to begin with). Returns a sample of entries \u2014 always the never-verified and least-recently-verified first \u2014 with skeletons of their claimed files, for you to judge whether the files actually implement what the entry claims. Report verdicts back via save_verification with each entry\u2019s kind and reviewToken. Tokens bind the entry and bounded source evidence; changed evidence requires a fresh review. Run periodically, or after an automated refresh wrote entries no human reviewed.",
|
|
9969
10345
|
{
|
|
9970
|
-
dir:
|
|
9971
|
-
sample:
|
|
10346
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10347
|
+
sample: z25.number().int().optional().describe("Entries to sample (default 5)")
|
|
9972
10348
|
},
|
|
9973
10349
|
async ({ dir, sample }) => {
|
|
9974
10350
|
const result = await verifySnapshot(dir, sample);
|
|
@@ -9979,13 +10355,13 @@ function createMcpServer() {
|
|
|
9979
10355
|
"save_verification",
|
|
9980
10356
|
"Record verify_snapshot verdicts using each entry\u2019s kind and reviewToken. Missing tokens request a fresh review; changed or deleted entries return conflicts without being stamped. Entries judged ok are stamped verifiedAt; failures are flagged verificationFailed with your note and surface in get_context, get_snapshot, and mason_check_drift until corrected. Verdict notes are required for failures.",
|
|
9981
10357
|
{
|
|
9982
|
-
dir:
|
|
9983
|
-
verdicts:
|
|
9984
|
-
|
|
9985
|
-
kind:
|
|
9986
|
-
reviewToken:
|
|
9987
|
-
ok:
|
|
9988
|
-
note:
|
|
10358
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10359
|
+
verdicts: z25.record(
|
|
10360
|
+
z25.object({
|
|
10361
|
+
kind: z25.enum(["feature", "flow"]).optional().describe("Entry kind returned by verify_snapshot; required to record a verdict"),
|
|
10362
|
+
reviewToken: z25.string().optional().describe("Copy the reviewToken from verify_snapshot after inspecting its evidence; required to record a verdict"),
|
|
10363
|
+
ok: z25.boolean(),
|
|
10364
|
+
note: z25.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
|
|
9989
10365
|
})
|
|
9990
10366
|
).describe("Entry name \u2192 verdict, exactly as returned by verify_snapshot")
|
|
9991
10367
|
},
|
|
@@ -9998,8 +10374,8 @@ function createMcpServer() {
|
|
|
9998
10374
|
"get_impact",
|
|
9999
10375
|
"Trace historical co-change partners, related tests, and references with evidence: resolved imports, explicit paths, or uncertain name candidates. Candidates are not proven dependencies. Deterministic and read-only; no initialization or concept map required.",
|
|
10000
10376
|
{
|
|
10001
|
-
dir:
|
|
10002
|
-
files:
|
|
10377
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10378
|
+
files: z25.array(z25.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
|
|
10003
10379
|
},
|
|
10004
10380
|
async ({ dir, files }) => {
|
|
10005
10381
|
const result = await getImpact(dir, files);
|
|
@@ -10012,12 +10388,12 @@ function createMcpServer() {
|
|
|
10012
10388
|
"export_to_confluence",
|
|
10013
10389
|
"Sync the project's concept map to Confluence as product-readable wiki pages: an index page, one page per feature (PM-language descriptions, no file paths), and a changelog page. Mason replaces managed page bodies; manual edits to those bodies are overwritten. Requires `mason_set_confluence` to have been called first.",
|
|
10014
10390
|
{
|
|
10015
|
-
dir:
|
|
10016
|
-
spaceKey:
|
|
10017
|
-
parentPageId:
|
|
10018
|
-
indexPageTitle:
|
|
10019
|
-
changelogPageTitle:
|
|
10020
|
-
featurePagePrefix:
|
|
10391
|
+
dir: z25.string().describe("Absolute path to the project root directory"),
|
|
10392
|
+
spaceKey: z25.string().optional().describe("Override the configured space key"),
|
|
10393
|
+
parentPageId: z25.string().optional().describe("Override the configured parent page ID"),
|
|
10394
|
+
indexPageTitle: z25.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
|
|
10395
|
+
changelogPageTitle: z25.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
|
|
10396
|
+
featurePagePrefix: z25.string().optional().describe("Prefix for each feature page title (default: 'Feature: ')")
|
|
10021
10397
|
},
|
|
10022
10398
|
async ({ dir, spaceKey, parentPageId, indexPageTitle, changelogPageTitle, featurePagePrefix }) => {
|
|
10023
10399
|
const result = await exportToConfluenceTool(dir, {
|
|
@@ -10043,38 +10419,38 @@ async function startMcpServer(onClose) {
|
|
|
10043
10419
|
init_store();
|
|
10044
10420
|
init_storage();
|
|
10045
10421
|
import fs33 from "fs/promises";
|
|
10046
|
-
import
|
|
10422
|
+
import path42 from "path";
|
|
10047
10423
|
import { spawn as spawn3 } from "child_process";
|
|
10048
10424
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
10049
|
-
import { z as
|
|
10425
|
+
import { z as z31 } from "zod";
|
|
10050
10426
|
|
|
10051
10427
|
// src/distribution/state.ts
|
|
10052
10428
|
init_storage();
|
|
10053
10429
|
import os5 from "os";
|
|
10054
|
-
import
|
|
10430
|
+
import path38 from "path";
|
|
10055
10431
|
import { fileURLToPath } from "url";
|
|
10056
10432
|
import fs28 from "fs/promises";
|
|
10057
|
-
import { z as
|
|
10433
|
+
import { z as z27 } from "zod";
|
|
10058
10434
|
|
|
10059
10435
|
// src/distribution/path.ts
|
|
10060
10436
|
import fs27 from "fs/promises";
|
|
10061
10437
|
import os4 from "os";
|
|
10062
|
-
import
|
|
10438
|
+
import path37 from "path";
|
|
10063
10439
|
import { execFile as execFile12 } from "child_process";
|
|
10064
10440
|
import { promisify as promisify12 } from "util";
|
|
10065
10441
|
import { createHash as createHash7, randomUUID as randomUUID7 } from "crypto";
|
|
10066
|
-
import { z as
|
|
10442
|
+
import { z as z26 } from "zod";
|
|
10067
10443
|
var execute2 = promisify12(execFile12);
|
|
10068
|
-
var pathChangeSchema =
|
|
10069
|
-
|
|
10070
|
-
|
|
10444
|
+
var pathChangeSchema = z26.discriminatedUnion("kind", [
|
|
10445
|
+
z26.object({ kind: z26.literal("profile"), file: z26.string(), block: z26.string(), created: z26.boolean() }),
|
|
10446
|
+
z26.object({ kind: z26.literal("windows"), entry: z26.string(), created: z26.boolean() })
|
|
10071
10447
|
]);
|
|
10072
10448
|
var quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
10073
10449
|
async function configurePath(bin, previous, save, options = {}) {
|
|
10074
10450
|
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
10075
10451
|
const changes = [...previous];
|
|
10076
10452
|
const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
|
|
10077
|
-
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) =>
|
|
10453
|
+
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path37.posix.isAbsolute(entry) && path37.posix.resolve(entry) === path37.posix.resolve(bin));
|
|
10078
10454
|
const setupHint = "To connect a project, run one of these inside its directory:\n Codex: mason setup --host codex\n Claude Code: mason setup --host claude";
|
|
10079
10455
|
const message2 = (alreadyOnPath ? "" : "Open a new terminal to use mason.\n") + setupHint;
|
|
10080
10456
|
const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
|
|
@@ -10094,15 +10470,15 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
10094
10470
|
}
|
|
10095
10471
|
return { status: "configured", changes, message: message2 };
|
|
10096
10472
|
}
|
|
10097
|
-
const shell =
|
|
10473
|
+
const shell = path37.basename(env.SHELL ?? os4.userInfo().shell ?? "");
|
|
10098
10474
|
const userHome = options.userHome ?? os4.homedir();
|
|
10099
10475
|
let profiles2;
|
|
10100
10476
|
if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
|
|
10101
|
-
if (env.MASON_PROFILE) profiles2 = [
|
|
10102
|
-
else if (shell === "zsh") profiles2 = [
|
|
10103
|
-
else if (shell === "sh") profiles2 = [
|
|
10477
|
+
if (env.MASON_PROFILE) profiles2 = [path37.resolve(env.MASON_PROFILE)];
|
|
10478
|
+
else if (shell === "zsh") profiles2 = [path37.join(env.ZDOTDIR || userHome, ".zshrc")];
|
|
10479
|
+
else if (shell === "sh") profiles2 = [path37.join(userHome, ".profile")];
|
|
10104
10480
|
else {
|
|
10105
|
-
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) =>
|
|
10481
|
+
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path37.join(userHome, name));
|
|
10106
10482
|
let login = candidates[2];
|
|
10107
10483
|
for (const file of candidates) if (await fs27.lstat(file).then(() => true, (error) => {
|
|
10108
10484
|
if (error.code === "ENOENT") return false;
|
|
@@ -10111,7 +10487,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
10111
10487
|
login = file;
|
|
10112
10488
|
break;
|
|
10113
10489
|
}
|
|
10114
|
-
profiles2 = [
|
|
10490
|
+
profiles2 = [path37.join(userHome, ".bashrc"), login];
|
|
10115
10491
|
}
|
|
10116
10492
|
const marker = `# >>> mason PATH ${createHash7("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
10117
10493
|
for (const profile of profiles2) {
|
|
@@ -10119,7 +10495,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
10119
10495
|
if (error.code === "ENOENT") return null;
|
|
10120
10496
|
throw error;
|
|
10121
10497
|
});
|
|
10122
|
-
const file = stat?.isSymbolicLink() ? await fs27.realpath(profile) :
|
|
10498
|
+
const file = stat?.isSymbolicLink() ? await fs27.realpath(profile) : path37.resolve(profile);
|
|
10123
10499
|
const original = await fs27.readFile(file).catch((error) => {
|
|
10124
10500
|
if (error.code === "ENOENT" && !stat) return null;
|
|
10125
10501
|
throw error;
|
|
@@ -10135,7 +10511,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
10135
10511
|
changes.push({ kind: "profile", file, block, created: original === null });
|
|
10136
10512
|
await save(changes);
|
|
10137
10513
|
}
|
|
10138
|
-
await fs27.mkdir(
|
|
10514
|
+
await fs27.mkdir(path37.dirname(file), { recursive: true });
|
|
10139
10515
|
const handle = await fs27.open(file, original === null ? "ax+" : "a+", 384);
|
|
10140
10516
|
try {
|
|
10141
10517
|
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
@@ -10150,7 +10526,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
10150
10526
|
return manual(`Could not configure PATH: ${error instanceof Error ? error.message : String(error)}`);
|
|
10151
10527
|
}
|
|
10152
10528
|
}
|
|
10153
|
-
var normalizeWindowsEntry = (value) =>
|
|
10529
|
+
var normalizeWindowsEntry = (value) => path37.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
10154
10530
|
var hasWindowsEntry = (value, entry, env = process.env) => {
|
|
10155
10531
|
const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
|
|
10156
10532
|
const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
|
|
@@ -10187,26 +10563,26 @@ try {
|
|
|
10187
10563
|
}
|
|
10188
10564
|
|
|
10189
10565
|
// src/distribution/state.ts
|
|
10190
|
-
var releaseVersion =
|
|
10191
|
-
var bundleId =
|
|
10192
|
-
var updatePolicySchema =
|
|
10193
|
-
enabled:
|
|
10194
|
-
revision:
|
|
10566
|
+
var releaseVersion = z27.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/);
|
|
10567
|
+
var bundleId = z27.string().regex(/^[a-f0-9]{24}$/);
|
|
10568
|
+
var updatePolicySchema = z27.object({
|
|
10569
|
+
enabled: z27.boolean(),
|
|
10570
|
+
revision: z27.string(),
|
|
10195
10571
|
pinnedVersion: releaseVersion.optional(),
|
|
10196
|
-
mirror:
|
|
10572
|
+
mirror: z27.string().optional()
|
|
10197
10573
|
});
|
|
10198
|
-
var recordSchema3 =
|
|
10199
|
-
format:
|
|
10200
|
-
home:
|
|
10201
|
-
bin:
|
|
10574
|
+
var recordSchema3 = z27.object({
|
|
10575
|
+
format: z27.literal(1),
|
|
10576
|
+
home: z27.string(),
|
|
10577
|
+
bin: z27.string(),
|
|
10202
10578
|
current: bundleId,
|
|
10203
10579
|
version: releaseVersion,
|
|
10204
|
-
launchers:
|
|
10205
|
-
previousLaunchers:
|
|
10206
|
-
pathChanges:
|
|
10580
|
+
launchers: z27.record(z27.string()),
|
|
10581
|
+
previousLaunchers: z27.record(z27.string()).optional(),
|
|
10582
|
+
pathChanges: z27.array(pathChangeSchema).optional(),
|
|
10207
10583
|
updates: updatePolicySchema.optional(),
|
|
10208
|
-
previous:
|
|
10209
|
-
pending:
|
|
10584
|
+
previous: z27.object({ id: bundleId, version: releaseVersion }).optional(),
|
|
10585
|
+
pending: z27.object({ id: bundleId, version: releaseVersion, policyRevision: z27.string() }).optional()
|
|
10210
10586
|
});
|
|
10211
10587
|
async function readInstallation(home) {
|
|
10212
10588
|
const raw = await readStoreJson(home, "install.json");
|
|
@@ -10216,9 +10592,9 @@ async function readInstallation(home) {
|
|
|
10216
10592
|
return record;
|
|
10217
10593
|
}
|
|
10218
10594
|
function standaloneLocation() {
|
|
10219
|
-
const bundle =
|
|
10220
|
-
const home =
|
|
10221
|
-
if (
|
|
10595
|
+
const bundle = path38.resolve(path38.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
10596
|
+
const home = path38.dirname(path38.dirname(bundle));
|
|
10597
|
+
if (path38.dirname(bundle) !== path38.join(home, "versions") || !/^[a-f0-9]{24}$/.test(path38.basename(bundle))) {
|
|
10222
10598
|
return null;
|
|
10223
10599
|
}
|
|
10224
10600
|
return { home, bundle };
|
|
@@ -10245,17 +10621,17 @@ function newerStableVersion(candidate, installed) {
|
|
|
10245
10621
|
// src/distribution/bundle.ts
|
|
10246
10622
|
init_storage();
|
|
10247
10623
|
import fs29 from "fs/promises";
|
|
10248
|
-
import
|
|
10624
|
+
import path39 from "path";
|
|
10249
10625
|
import { createHash as createHash8 } from "crypto";
|
|
10250
|
-
import { z as
|
|
10626
|
+
import { z as z28 } from "zod";
|
|
10251
10627
|
var targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
|
10252
|
-
var digest2 =
|
|
10253
|
-
var bundleSchema =
|
|
10254
|
-
format:
|
|
10255
|
-
version:
|
|
10256
|
-
target:
|
|
10257
|
-
nodeVersion:
|
|
10258
|
-
files:
|
|
10628
|
+
var digest2 = z28.string().regex(/^[a-f0-9]{64}$/);
|
|
10629
|
+
var bundleSchema = z28.object({
|
|
10630
|
+
format: z28.literal(1),
|
|
10631
|
+
version: z28.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
10632
|
+
target: z28.enum(targets),
|
|
10633
|
+
nodeVersion: z28.string(),
|
|
10634
|
+
files: z28.record(digest2)
|
|
10259
10635
|
}).strict();
|
|
10260
10636
|
var sha256 = (bytes) => createHash8("sha256").update(bytes).digest("hex");
|
|
10261
10637
|
async function readBundle(root) {
|
|
@@ -10263,7 +10639,7 @@ async function readBundle(root) {
|
|
|
10263
10639
|
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
10264
10640
|
const windows = manifest.target.startsWith("win32-");
|
|
10265
10641
|
for (const file of Object.keys(manifest.files)) {
|
|
10266
|
-
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") ||
|
|
10642
|
+
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path39.isAbsolute(file)) {
|
|
10267
10643
|
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
10268
10644
|
}
|
|
10269
10645
|
}
|
|
@@ -10289,15 +10665,20 @@ init_storage();
|
|
|
10289
10665
|
import fs30 from "fs/promises";
|
|
10290
10666
|
import { unlinkSync } from "fs";
|
|
10291
10667
|
import os6 from "os";
|
|
10292
|
-
import
|
|
10668
|
+
import path40 from "path";
|
|
10293
10669
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
10294
|
-
import { z as
|
|
10295
|
-
|
|
10670
|
+
import { z as z29 } from "zod";
|
|
10671
|
+
|
|
10672
|
+
// src/distribution/trust.ts
|
|
10673
|
+
import tls from "tls";
|
|
10674
|
+
|
|
10675
|
+
// src/distribution/runtime.ts
|
|
10676
|
+
var leaseSchema = z29.object({ pid: z29.number().int().positive(), host: z29.string(), bundle: z29.string().regex(/^[a-f0-9]{24}$/) });
|
|
10296
10677
|
|
|
10297
10678
|
// src/distribution/install.ts
|
|
10298
10679
|
import fs32 from "fs/promises";
|
|
10299
10680
|
import os8 from "os";
|
|
10300
|
-
import
|
|
10681
|
+
import path41 from "path";
|
|
10301
10682
|
import { execFile as execFile13, spawn as spawn2 } from "child_process";
|
|
10302
10683
|
import { promisify as promisify13 } from "util";
|
|
10303
10684
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -10309,8 +10690,8 @@ init_storage();
|
|
|
10309
10690
|
import fs31 from "fs/promises";
|
|
10310
10691
|
import os7 from "os";
|
|
10311
10692
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
10312
|
-
import { z as
|
|
10313
|
-
var leaseSchema2 =
|
|
10693
|
+
import { z as z30 } from "zod";
|
|
10694
|
+
var leaseSchema2 = z30.object({ pid: z30.number().int().positive(), host: z30.string() });
|
|
10314
10695
|
async function hasRunningMcp(home) {
|
|
10315
10696
|
const directory = await storePath(home, "sessions");
|
|
10316
10697
|
const files = await fs31.readdir(directory).catch((error) => {
|
|
@@ -10372,13 +10753,13 @@ async function installLocked2(home, source, bundle, progress, options, alreadySt
|
|
|
10372
10753
|
if (options.stageOnly) assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
|
|
10373
10754
|
if (!previous) for (const file of await fs32.readdir(home)) {
|
|
10374
10755
|
if (file === ".install-lock") continue;
|
|
10375
|
-
const directory =
|
|
10756
|
+
const directory = path41.join(home, file);
|
|
10376
10757
|
const stat = await fs32.lstat(directory);
|
|
10377
10758
|
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs32.readdir(directory)).length) {
|
|
10378
10759
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
10379
10760
|
}
|
|
10380
10761
|
}
|
|
10381
|
-
const selectedBin =
|
|
10762
|
+
const selectedBin = path41.resolve(process.env.MASON_BIN_DIR ?? previous?.bin ?? (process.platform === "win32" ? path41.join(home, "bin") : path41.join(os8.homedir(), ".local/bin")));
|
|
10382
10763
|
await fs32.mkdir(selectedBin, { recursive: true });
|
|
10383
10764
|
const bin = await fs32.realpath(selectedBin);
|
|
10384
10765
|
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
@@ -10396,10 +10777,10 @@ $previousToken = $env:MASON_UNINSTALL_TOKEN
|
|
|
10396
10777
|
$token = [Guid]::NewGuid().ToString()
|
|
10397
10778
|
$env:MASON_UNINSTALL_TOKEN = $token
|
|
10398
10779
|
try {
|
|
10399
|
-
& ${psQuote(
|
|
10780
|
+
& ${psQuote(path41.join(destination, "node.exe"))} ${psQuote(path41.join(destination, "app/dist/mason.js"))} @args
|
|
10400
10781
|
$code = $LASTEXITCODE
|
|
10401
10782
|
if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
|
|
10402
|
-
if ([IO.File]::ReadAllText(${psQuote(
|
|
10783
|
+
if ([IO.File]::ReadAllText(${psQuote(path41.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
|
|
10403
10784
|
Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
|
|
10404
10785
|
Write-Output ${psQuote("Removed standalone installation: " + home)}
|
|
10405
10786
|
}
|
|
@@ -10408,7 +10789,7 @@ try {
|
|
|
10408
10789
|
finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
|
|
10409
10790
|
`
|
|
10410
10791
|
} : { mason: `#!/bin/sh
|
|
10411
|
-
exec ${shQuote(
|
|
10792
|
+
exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destination, "app/dist/mason.js"))} "$@"
|
|
10412
10793
|
` };
|
|
10413
10794
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
10414
10795
|
const file = await storePath(bin, name);
|
|
@@ -10540,14 +10921,14 @@ async function prepareStandaloneMcp(home) {
|
|
|
10540
10921
|
// src/distribution/updates.ts
|
|
10541
10922
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
10542
10923
|
var RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10543
|
-
var releaseListSchema =
|
|
10544
|
-
var updateManifestSchema =
|
|
10545
|
-
format:
|
|
10546
|
-
version:
|
|
10547
|
-
publishedAt:
|
|
10548
|
-
assets:
|
|
10924
|
+
var releaseListSchema = z31.array(z31.object({ tag_name: z31.string(), draft: z31.boolean(), prerelease: z31.boolean(), published_at: z31.string().nullable() }));
|
|
10925
|
+
var updateManifestSchema = z31.object({
|
|
10926
|
+
format: z31.literal(1),
|
|
10927
|
+
version: z31.string().regex(/^\d+\.\d+\.\d+$/),
|
|
10928
|
+
publishedAt: z31.string().datetime(),
|
|
10929
|
+
assets: z31.record(z31.string().regex(/^[a-f0-9]{64}$/))
|
|
10549
10930
|
}).strict();
|
|
10550
|
-
var statusSchema2 =
|
|
10931
|
+
var statusSchema2 = z31.object({ lastAttempt: z31.number(), availableVersion: z31.string().optional(), error: z31.string().optional() });
|
|
10551
10932
|
async function scheduleAutomaticUpdate() {
|
|
10552
10933
|
try {
|
|
10553
10934
|
const { home, record } = await currentInstallation();
|
|
@@ -10557,8 +10938,8 @@ async function scheduleAutomaticUpdate() {
|
|
|
10557
10938
|
if (status.success && status.data.lastAttempt <= now && now - status.data.lastAttempt < CHECK_INTERVAL_MS) return;
|
|
10558
10939
|
const bundle = await storePath(home, "versions/" + record.current);
|
|
10559
10940
|
const child = spawn3(
|
|
10560
|
-
|
|
10561
|
-
[
|
|
10941
|
+
path42.join(bundle, process.platform === "win32" ? "node.exe" : "node"),
|
|
10942
|
+
[path42.join(bundle, "app/dist/mason.js"), "internal-update"],
|
|
10562
10943
|
{ cwd: home, env: process.env, stdio: "ignore", detached: true, windowsHide: true }
|
|
10563
10944
|
);
|
|
10564
10945
|
child.on("error", () => {
|
|
@@ -10576,8 +10957,8 @@ async function startMcpWithUpdates(start) {
|
|
|
10576
10957
|
const session = await prepareStandaloneMcp(current.home);
|
|
10577
10958
|
if (session.bundle !== current.bundle) {
|
|
10578
10959
|
const child = spawn3(
|
|
10579
|
-
|
|
10580
|
-
[
|
|
10960
|
+
path42.join(session.bundle, process.platform === "win32" ? "node.exe" : "node"),
|
|
10961
|
+
[path42.join(session.bundle, "app/dist/mason-mcp.js"), ...process.argv.slice(2)],
|
|
10581
10962
|
{ stdio: "inherit", windowsHide: true }
|
|
10582
10963
|
);
|
|
10583
10964
|
const interrupt = () => {
|