mason-context 0.19.0 → 0.19.1

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