mason-context 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/mason-audit.js +524 -324
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +1099 -729
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +50 -0
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +1450 -1069
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +50 -0
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +157 -54
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +1208 -784
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason-audit.js
CHANGED
|
@@ -85,6 +85,26 @@ var init_paths = __esm({
|
|
|
85
85
|
}
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
// src/audit/types.ts
|
|
89
|
+
var types_exports = {};
|
|
90
|
+
__export(types_exports, {
|
|
91
|
+
ALL_CHECKS: () => ALL_CHECKS
|
|
92
|
+
});
|
|
93
|
+
var ALL_CHECKS;
|
|
94
|
+
var init_types = __esm({
|
|
95
|
+
"src/audit/types.ts"() {
|
|
96
|
+
"use strict";
|
|
97
|
+
ALL_CHECKS = [
|
|
98
|
+
"deleted-reference",
|
|
99
|
+
"new-module",
|
|
100
|
+
"stale-count",
|
|
101
|
+
"dead-command",
|
|
102
|
+
"deps-changed",
|
|
103
|
+
"decision-anchor-drift"
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
88
108
|
// src/utils/files.ts
|
|
89
109
|
import fs from "fs/promises";
|
|
90
110
|
import { constants } from "fs";
|
|
@@ -124,6 +144,27 @@ async function loadProjectConfig(root) {
|
|
|
124
144
|
}
|
|
125
145
|
config[key] = value[key];
|
|
126
146
|
}
|
|
147
|
+
if (value.audit !== void 0) {
|
|
148
|
+
const audit = value.audit;
|
|
149
|
+
if (!audit || typeof audit !== "object" || Array.isArray(audit)) throw new Error("Configuration audit must be an object");
|
|
150
|
+
const patterns = (raw2) => {
|
|
151
|
+
if (!Array.isArray(raw2) || raw2.length > 100 || !raw2.every((s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s))) {
|
|
152
|
+
throw new Error("Audit patterns must be at most 100 repository-relative globs without traversal or negation");
|
|
153
|
+
}
|
|
154
|
+
return raw2;
|
|
155
|
+
};
|
|
156
|
+
config.audit = {};
|
|
157
|
+
if (audit.include !== void 0) config.audit.include = patterns(audit.include);
|
|
158
|
+
if (audit.exclude !== void 0) {
|
|
159
|
+
if (!audit.exclude || typeof audit.exclude !== "object" || Array.isArray(audit.exclude)) throw new Error("audit.exclude must map check names to patterns");
|
|
160
|
+
const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
|
|
161
|
+
config.audit.exclude = {};
|
|
162
|
+
for (const [check, globs] of Object.entries(audit.exclude)) {
|
|
163
|
+
if (!ALL_CHECKS2.includes(check)) throw new Error("Unknown audit check: " + check);
|
|
164
|
+
config.audit.exclude[check] = patterns(globs);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
127
168
|
return config;
|
|
128
169
|
} catch (error) {
|
|
129
170
|
if (error.code === "ENOENT") return {};
|
|
@@ -235,13 +276,192 @@ var init_storage = __esm({
|
|
|
235
276
|
}
|
|
236
277
|
});
|
|
237
278
|
|
|
238
|
-
// src/
|
|
279
|
+
// src/audit/inspection.ts
|
|
280
|
+
import fg2 from "fast-glob";
|
|
281
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
282
|
+
import path4 from "path";
|
|
283
|
+
function inspectionRead(root, key, read) {
|
|
284
|
+
const inspection = inspections.getStore();
|
|
285
|
+
if (!inspection || inspection.root !== path4.resolve(root)) return read();
|
|
286
|
+
if (!inspection.reads.has(key)) {
|
|
287
|
+
const pending = read();
|
|
288
|
+
inspection.reads.set(key, pending);
|
|
289
|
+
void pending.catch(() => {
|
|
290
|
+
if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return inspection.reads.get(key);
|
|
294
|
+
}
|
|
295
|
+
function inspectionGit(args, options) {
|
|
296
|
+
return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
|
|
297
|
+
}
|
|
298
|
+
async function auditGitPaths(root, kind) {
|
|
299
|
+
const custom = (await loadProjectConfig(root)).audit?.include ?? [];
|
|
300
|
+
const shared = inspections.getStore()?.root === path4.resolve(root) && !custom.length;
|
|
301
|
+
const expanded = fg2.generateTasks(custom).flatMap((task) => task.positive);
|
|
302
|
+
if (expanded.length > 1e3) throw new Error("Audit include patterns expand to more than 1,000 globs");
|
|
303
|
+
const documentSpecs = [...docSpecs, ...expanded.map((pattern) => `:(glob)${pattern}`)];
|
|
304
|
+
const specs = shared ? [...documentSpecs, ...sourceSpecs] : kind === "documents" ? documentSpecs : sourceSpecs;
|
|
305
|
+
const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
|
|
306
|
+
const { stdout } = await inspectionGit(
|
|
307
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
|
|
308
|
+
{ cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
|
|
309
|
+
);
|
|
310
|
+
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("."))));
|
|
311
|
+
if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
|
|
312
|
+
return paths.sort();
|
|
313
|
+
}
|
|
314
|
+
var inspections, sourceSpecs, docSpecs, sourceSuffixes;
|
|
315
|
+
var init_inspection = __esm({
|
|
316
|
+
"src/audit/inspection.ts"() {
|
|
317
|
+
"use strict";
|
|
318
|
+
init_git_read();
|
|
319
|
+
init_files();
|
|
320
|
+
inspections = new AsyncLocalStorage2();
|
|
321
|
+
sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
|
|
322
|
+
docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
|
|
323
|
+
sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// src/audit/inputs.ts
|
|
239
328
|
import fs3 from "fs/promises";
|
|
329
|
+
import * as nativeFs from "fs";
|
|
330
|
+
import path5 from "path";
|
|
331
|
+
import fg3 from "fast-glob";
|
|
332
|
+
async function auditInputPath(root, file) {
|
|
333
|
+
if (file === ".") return fs3.realpath(root);
|
|
334
|
+
try {
|
|
335
|
+
return await storePath(root, file);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
338
|
+
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function readAuditInput(root, file) {
|
|
344
|
+
try {
|
|
345
|
+
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
346
|
+
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
347
|
+
return value;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function auditGlob(root, patterns, options) {
|
|
354
|
+
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
355
|
+
const directories = /* @__PURE__ */ new Set();
|
|
356
|
+
const readdir2 = ((directory, opts, callback) => {
|
|
357
|
+
const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
|
|
358
|
+
directories.add(relative);
|
|
359
|
+
if (directories.size > MAX_INPUTS) {
|
|
360
|
+
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
auditInputPath(root, relative).then(() => {
|
|
364
|
+
nativeFs.readdir(directory, opts, callback);
|
|
365
|
+
}, callback);
|
|
366
|
+
});
|
|
367
|
+
const lstat2 = ((file, callback) => {
|
|
368
|
+
const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
|
|
369
|
+
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
370
|
+
});
|
|
371
|
+
const stream = fg3.stream(patterns, {
|
|
372
|
+
cwd: root,
|
|
373
|
+
ignore: options.ignore,
|
|
374
|
+
onlyFiles: false,
|
|
375
|
+
onlyDirectories: options.onlyDirectories,
|
|
376
|
+
objectMode: true,
|
|
377
|
+
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
378
|
+
fs: { readdir: readdir2, lstat: lstat2 },
|
|
379
|
+
concurrency: 16
|
|
380
|
+
});
|
|
381
|
+
const matches = [];
|
|
382
|
+
try {
|
|
383
|
+
for await (const value of stream) {
|
|
384
|
+
const entry = value;
|
|
385
|
+
const file = entry.path;
|
|
386
|
+
if (options.select && !options.select(file)) continue;
|
|
387
|
+
await auditInputPath(root, file);
|
|
388
|
+
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
389
|
+
matches.push(file);
|
|
390
|
+
if (matches.length > MAX_INPUTS) {
|
|
391
|
+
const largest = [...matches.reduce((counts, item) => {
|
|
392
|
+
const dir = item.split("/")[0];
|
|
393
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
394
|
+
return counts;
|
|
395
|
+
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
396
|
+
throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count3]) => `${dir} (${count3})`).join(", ")}. Narrow this check's inputs.`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} finally {
|
|
400
|
+
stream.destroy();
|
|
401
|
+
}
|
|
402
|
+
return matches.sort();
|
|
403
|
+
}
|
|
404
|
+
async function gitSourcePaths(root) {
|
|
405
|
+
return auditGitPaths(root, "sources");
|
|
406
|
+
}
|
|
407
|
+
async function gitIgnoredPaths(root, files) {
|
|
408
|
+
if (!files.length) return /* @__PURE__ */ new Set();
|
|
409
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
410
|
+
for (let offset = 0; offset < files.length; offset += 1e3) {
|
|
411
|
+
try {
|
|
412
|
+
const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
|
|
413
|
+
cwd: root,
|
|
414
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
415
|
+
timeout: 1e4,
|
|
416
|
+
input: files.slice(offset, offset + 1e3).join("\0") + "\0"
|
|
417
|
+
});
|
|
418
|
+
for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (error.code !== 1) throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return ignored;
|
|
424
|
+
}
|
|
425
|
+
var MAX_INPUTS;
|
|
426
|
+
var init_inputs = __esm({
|
|
427
|
+
"src/audit/inputs.ts"() {
|
|
428
|
+
"use strict";
|
|
429
|
+
init_files();
|
|
430
|
+
init_storage();
|
|
431
|
+
init_git_read();
|
|
432
|
+
init_inspection();
|
|
433
|
+
MAX_INPUTS = 1e5;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// src/audit/policy.ts
|
|
438
|
+
import fg4 from "fast-glob";
|
|
439
|
+
async function includedCheckPaths(root, paths, exclude) {
|
|
440
|
+
if (!exclude.length) return new Set(paths);
|
|
441
|
+
const included = /* @__PURE__ */ new Set();
|
|
442
|
+
for (let offset = 0; offset < paths.length; offset += 256) {
|
|
443
|
+
for (const file of await auditGlob(root, paths.slice(offset, offset + 256).map((file2) => fg4.escapePath(file2)), {
|
|
444
|
+
ignore: exclude,
|
|
445
|
+
followSymbolicLinks: false,
|
|
446
|
+
label: "Audit check exclusions"
|
|
447
|
+
})) included.add(file);
|
|
448
|
+
}
|
|
449
|
+
return included;
|
|
450
|
+
}
|
|
451
|
+
var init_policy = __esm({
|
|
452
|
+
"src/audit/policy.ts"() {
|
|
453
|
+
"use strict";
|
|
454
|
+
init_inputs();
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// src/utils/store-lock.ts
|
|
459
|
+
import fs4 from "fs/promises";
|
|
240
460
|
import os from "os";
|
|
241
461
|
async function lockDiagnostic(file) {
|
|
242
462
|
let reason;
|
|
243
463
|
try {
|
|
244
|
-
const owner = JSON.parse(await
|
|
464
|
+
const owner = JSON.parse(await fs4.readFile(file, "utf8"));
|
|
245
465
|
if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
246
466
|
reason = "Lock owner data is incomplete or malformed.";
|
|
247
467
|
} else if (owner.host !== os.hostname()) {
|
|
@@ -257,7 +477,7 @@ async function lockDiagnostic(file) {
|
|
|
257
477
|
} catch (error) {
|
|
258
478
|
reason = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
|
|
259
479
|
}
|
|
260
|
-
if (await
|
|
480
|
+
if (await fs4.lstat(file + ".reclaim").then(() => true, () => false)) {
|
|
261
481
|
reason += " A recovery guard also needs inspection: " + file + ".reclaim.";
|
|
262
482
|
}
|
|
263
483
|
return reason + " Retry if a writer is active. For manual recovery, stop all Mason writers for this checkout, confirm they have exited (including other hosts), then remove only confirmed abandoned lock and lock.reclaim files. See docs/reference.md#snapshot-repairs-and-verification.";
|
|
@@ -268,21 +488,21 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
|
|
|
268
488
|
let handle;
|
|
269
489
|
while (!handle) {
|
|
270
490
|
try {
|
|
271
|
-
handle = await
|
|
491
|
+
handle = await fs4.open(file, "wx", 384);
|
|
272
492
|
try {
|
|
273
493
|
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
274
494
|
} catch (error) {
|
|
275
495
|
await handle.close().catch(() => {
|
|
276
496
|
});
|
|
277
497
|
handle = void 0;
|
|
278
|
-
await
|
|
498
|
+
await fs4.rm(file, { force: true }).catch(() => {
|
|
279
499
|
});
|
|
280
500
|
throw error;
|
|
281
501
|
}
|
|
282
502
|
} catch (error) {
|
|
283
503
|
if (error.code !== "EEXIST") throw error;
|
|
284
504
|
try {
|
|
285
|
-
const owner = JSON.parse(await
|
|
505
|
+
const owner = JSON.parse(await fs4.readFile(file, "utf8"));
|
|
286
506
|
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
287
507
|
try {
|
|
288
508
|
process.kill(owner.pid, 0);
|
|
@@ -291,13 +511,13 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
|
|
|
291
511
|
const reclaim = file + ".reclaim";
|
|
292
512
|
let guard;
|
|
293
513
|
try {
|
|
294
|
-
guard = await
|
|
295
|
-
const current = JSON.parse(await
|
|
296
|
-
if (current.pid === owner.pid && current.host === owner.host) await
|
|
514
|
+
guard = await fs4.open(reclaim, "wx", 384);
|
|
515
|
+
const current = JSON.parse(await fs4.readFile(file, "utf8"));
|
|
516
|
+
if (current.pid === owner.pid && current.host === owner.host) await fs4.unlink(file);
|
|
297
517
|
} finally {
|
|
298
518
|
if (guard) {
|
|
299
519
|
await guard.close();
|
|
300
|
-
await
|
|
520
|
+
await fs4.rm(reclaim, { force: true });
|
|
301
521
|
}
|
|
302
522
|
}
|
|
303
523
|
}
|
|
@@ -313,7 +533,7 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
|
|
|
313
533
|
return await run();
|
|
314
534
|
} finally {
|
|
315
535
|
await handle.close();
|
|
316
|
-
await
|
|
536
|
+
await fs4.unlink(file);
|
|
317
537
|
}
|
|
318
538
|
}
|
|
319
539
|
var init_store_lock = __esm({
|
|
@@ -332,7 +552,7 @@ var init_lock = __esm({
|
|
|
332
552
|
});
|
|
333
553
|
|
|
334
554
|
// src/test-map.ts
|
|
335
|
-
import
|
|
555
|
+
import path6 from "path";
|
|
336
556
|
var init_test_map = __esm({
|
|
337
557
|
"src/test-map.ts"() {
|
|
338
558
|
"use strict";
|
|
@@ -341,7 +561,7 @@ var init_test_map = __esm({
|
|
|
341
561
|
});
|
|
342
562
|
|
|
343
563
|
// src/snapshot/snapshot.ts
|
|
344
|
-
import
|
|
564
|
+
import path7 from "path";
|
|
345
565
|
import { z } from "zod";
|
|
346
566
|
async function getCurrentGitHash(rootDir) {
|
|
347
567
|
try {
|
|
@@ -393,8 +613,8 @@ var init_snapshot = __esm({
|
|
|
393
613
|
});
|
|
394
614
|
|
|
395
615
|
// src/drift/drift.ts
|
|
396
|
-
import
|
|
397
|
-
import
|
|
616
|
+
import fs5 from "fs/promises";
|
|
617
|
+
import path8 from "path";
|
|
398
618
|
function parseChanges(output) {
|
|
399
619
|
const fields = output.split("\0");
|
|
400
620
|
const changes = [];
|
|
@@ -443,7 +663,7 @@ var init_drift = __esm({
|
|
|
443
663
|
});
|
|
444
664
|
|
|
445
665
|
// src/audit/commands.ts
|
|
446
|
-
import
|
|
666
|
+
import path9 from "path";
|
|
447
667
|
function splitCommands(line) {
|
|
448
668
|
const parts = [];
|
|
449
669
|
let quote = "", start = 0;
|
|
@@ -479,7 +699,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
479
699
|
const cd = text2.match(/^cd\s+(.+)$/);
|
|
480
700
|
if (cd) {
|
|
481
701
|
const directory = literal(cd[1]);
|
|
482
|
-
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null :
|
|
702
|
+
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path9.posix.join(cwd ?? ".", directory);
|
|
483
703
|
continue;
|
|
484
704
|
}
|
|
485
705
|
for (const match of text2.matchAll(COMMAND)) {
|
|
@@ -490,7 +710,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
490
710
|
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
491
711
|
if (selector) {
|
|
492
712
|
const selected = literal(selector[1]);
|
|
493
|
-
directory = selected === null || cwd === null ? null :
|
|
713
|
+
directory = selected === null || cwd === null ? null : path9.posix.join(cwd ?? ".", selected);
|
|
494
714
|
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
495
715
|
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
496
716
|
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
@@ -520,7 +740,7 @@ var init_commands = __esm({
|
|
|
520
740
|
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
521
741
|
literal = (value) => {
|
|
522
742
|
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
523
|
-
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) ||
|
|
743
|
+
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path9.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
524
744
|
};
|
|
525
745
|
}
|
|
526
746
|
});
|
|
@@ -825,49 +1045,6 @@ var init_claims = __esm({
|
|
|
825
1045
|
}
|
|
826
1046
|
});
|
|
827
1047
|
|
|
828
|
-
// src/audit/inspection.ts
|
|
829
|
-
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
830
|
-
import path8 from "path";
|
|
831
|
-
function inspectionRead(root, key, read) {
|
|
832
|
-
const inspection = inspections.getStore();
|
|
833
|
-
if (!inspection || inspection.root !== path8.resolve(root)) return read();
|
|
834
|
-
if (!inspection.reads.has(key)) {
|
|
835
|
-
const pending = read();
|
|
836
|
-
inspection.reads.set(key, pending);
|
|
837
|
-
void pending.catch(() => {
|
|
838
|
-
if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
return inspection.reads.get(key);
|
|
842
|
-
}
|
|
843
|
-
function inspectionGit(args, options) {
|
|
844
|
-
return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
|
|
845
|
-
}
|
|
846
|
-
async function auditGitPaths(root, kind) {
|
|
847
|
-
const shared = inspections.getStore()?.root === path8.resolve(root);
|
|
848
|
-
const specs = shared ? [...docSpecs, ...sourceSpecs] : kind === "documents" ? docSpecs : sourceSpecs;
|
|
849
|
-
const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
|
|
850
|
-
const { stdout } = await inspectionGit(
|
|
851
|
-
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
|
|
852
|
-
{ cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
|
|
853
|
-
);
|
|
854
|
-
const paths = [...new Set(stdout.split("\0").filter(Boolean))].filter((file) => kind === "documents" ? /^(readme|agents|claude)\.md$/i.test(path8.posix.basename(file)) : sourceSuffixes.has(file.slice(file.lastIndexOf("."))));
|
|
855
|
-
if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
|
|
856
|
-
return paths.sort();
|
|
857
|
-
}
|
|
858
|
-
var inspections, sourceSpecs, docSpecs, sourceSuffixes;
|
|
859
|
-
var init_inspection = __esm({
|
|
860
|
-
"src/audit/inspection.ts"() {
|
|
861
|
-
"use strict";
|
|
862
|
-
init_git_read();
|
|
863
|
-
init_files();
|
|
864
|
-
inspections = new AsyncLocalStorage2();
|
|
865
|
-
sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
|
|
866
|
-
docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
|
|
867
|
-
sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
|
|
868
|
-
}
|
|
869
|
-
});
|
|
870
|
-
|
|
871
1048
|
// src/audit/git.ts
|
|
872
1049
|
function parseCommitLine(line) {
|
|
873
1050
|
const parts = line.split(" ");
|
|
@@ -953,120 +1130,10 @@ var init_git = __esm({
|
|
|
953
1130
|
}
|
|
954
1131
|
});
|
|
955
1132
|
|
|
956
|
-
// src/audit/inputs.ts
|
|
957
|
-
import fs5 from "fs/promises";
|
|
958
|
-
import * as nativeFs from "fs";
|
|
959
|
-
import path9 from "path";
|
|
960
|
-
import fg2 from "fast-glob";
|
|
961
|
-
async function auditInputPath(root, file) {
|
|
962
|
-
if (file === ".") return fs5.realpath(root);
|
|
963
|
-
try {
|
|
964
|
-
return await storePath(root, file);
|
|
965
|
-
} catch (error) {
|
|
966
|
-
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
967
|
-
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
968
|
-
}
|
|
969
|
-
throw error;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
async function readAuditInput(root, file) {
|
|
973
|
-
try {
|
|
974
|
-
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
975
|
-
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
976
|
-
return value;
|
|
977
|
-
} catch (error) {
|
|
978
|
-
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
|
|
979
|
-
throw error;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
async function auditGlob(root, patterns, options) {
|
|
983
|
-
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
984
|
-
const directories = /* @__PURE__ */ new Set();
|
|
985
|
-
const readdir2 = ((directory, opts, callback) => {
|
|
986
|
-
const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
|
|
987
|
-
directories.add(relative);
|
|
988
|
-
if (directories.size > MAX_INPUTS) {
|
|
989
|
-
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
auditInputPath(root, relative).then(() => {
|
|
993
|
-
nativeFs.readdir(directory, opts, callback);
|
|
994
|
-
}, callback);
|
|
995
|
-
});
|
|
996
|
-
const lstat2 = ((file, callback) => {
|
|
997
|
-
const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
|
|
998
|
-
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
999
|
-
});
|
|
1000
|
-
const stream = fg2.stream(patterns, {
|
|
1001
|
-
cwd: root,
|
|
1002
|
-
ignore: options.ignore,
|
|
1003
|
-
onlyFiles: false,
|
|
1004
|
-
onlyDirectories: options.onlyDirectories,
|
|
1005
|
-
objectMode: true,
|
|
1006
|
-
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
1007
|
-
fs: { readdir: readdir2, lstat: lstat2 },
|
|
1008
|
-
concurrency: 16
|
|
1009
|
-
});
|
|
1010
|
-
const matches = [];
|
|
1011
|
-
try {
|
|
1012
|
-
for await (const value of stream) {
|
|
1013
|
-
const entry = value;
|
|
1014
|
-
const file = entry.path;
|
|
1015
|
-
if (options.select && !options.select(file)) continue;
|
|
1016
|
-
await auditInputPath(root, file);
|
|
1017
|
-
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
1018
|
-
matches.push(file);
|
|
1019
|
-
if (matches.length > MAX_INPUTS) {
|
|
1020
|
-
const largest = [...matches.reduce((counts, item) => {
|
|
1021
|
-
const dir = item.split("/")[0];
|
|
1022
|
-
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
1023
|
-
return counts;
|
|
1024
|
-
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
1025
|
-
throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count3]) => `${dir} (${count3})`).join(", ")}. Narrow this check's inputs.`);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
} finally {
|
|
1029
|
-
stream.destroy();
|
|
1030
|
-
}
|
|
1031
|
-
return matches.sort();
|
|
1032
|
-
}
|
|
1033
|
-
async function gitSourcePaths(root) {
|
|
1034
|
-
return auditGitPaths(root, "sources");
|
|
1035
|
-
}
|
|
1036
|
-
async function gitIgnoredPaths(root, files) {
|
|
1037
|
-
if (!files.length) return /* @__PURE__ */ new Set();
|
|
1038
|
-
const ignored = /* @__PURE__ */ new Set();
|
|
1039
|
-
for (let offset = 0; offset < files.length; offset += 1e3) {
|
|
1040
|
-
try {
|
|
1041
|
-
const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
|
|
1042
|
-
cwd: root,
|
|
1043
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
1044
|
-
timeout: 1e4,
|
|
1045
|
-
input: files.slice(offset, offset + 1e3).join("\0") + "\0"
|
|
1046
|
-
});
|
|
1047
|
-
for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
if (error.code !== 1) throw error;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
return ignored;
|
|
1053
|
-
}
|
|
1054
|
-
var MAX_INPUTS;
|
|
1055
|
-
var init_inputs = __esm({
|
|
1056
|
-
"src/audit/inputs.ts"() {
|
|
1057
|
-
"use strict";
|
|
1058
|
-
init_files();
|
|
1059
|
-
init_storage();
|
|
1060
|
-
init_git_read();
|
|
1061
|
-
init_inspection();
|
|
1062
|
-
MAX_INPUTS = 1e5;
|
|
1063
|
-
}
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
1133
|
// src/audit/docs.ts
|
|
1067
1134
|
import fs6 from "fs/promises";
|
|
1068
1135
|
import path10 from "path";
|
|
1069
|
-
import
|
|
1136
|
+
import fg5 from "fast-glob";
|
|
1070
1137
|
async function dirtyDocs(resolvedRoot, files) {
|
|
1071
1138
|
if (!files.length) return /* @__PURE__ */ new Set();
|
|
1072
1139
|
try {
|
|
@@ -1118,7 +1185,7 @@ async function discoverDocPaths(root) {
|
|
|
1118
1185
|
const paths = [];
|
|
1119
1186
|
const unique = [...new Set(candidates)].sort();
|
|
1120
1187
|
for (let offset = 0; offset < unique.length; offset += 256) {
|
|
1121
|
-
paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) =>
|
|
1188
|
+
paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg5.escapePath(p)), {
|
|
1122
1189
|
ignore: excluded,
|
|
1123
1190
|
followSymbolicLinks: false,
|
|
1124
1191
|
label: "Documentation discovery"
|
|
@@ -1148,6 +1215,7 @@ async function localDocPaths(root) {
|
|
|
1148
1215
|
return paths;
|
|
1149
1216
|
}
|
|
1150
1217
|
function documentScope(file) {
|
|
1218
|
+
if (!/^(readme|agents|claude)\.md$/i.test(path10.posix.basename(file))) return ".";
|
|
1151
1219
|
const parent = path10.posix.dirname(file);
|
|
1152
1220
|
return path10.posix.basename(parent).toLowerCase() === ".claude" ? path10.posix.dirname(parent) : parent;
|
|
1153
1221
|
}
|
|
@@ -1169,7 +1237,7 @@ async function readDocs(resolvedRoot) {
|
|
|
1169
1237
|
path: candidate,
|
|
1170
1238
|
content,
|
|
1171
1239
|
scope: documentScope(candidate),
|
|
1172
|
-
kind: /^readme\.md$/i.test(path10.posix.basename(candidate)) ? "readme" : "instructions",
|
|
1240
|
+
kind: /^readme\.md$/i.test(path10.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path10.posix.basename(candidate)) ? "instructions" : "guide",
|
|
1173
1241
|
lineCount: content.split("\n").length,
|
|
1174
1242
|
lastCommit,
|
|
1175
1243
|
dirty: dirty.has(candidate),
|
|
@@ -1198,22 +1266,6 @@ var init_docs = __esm({
|
|
|
1198
1266
|
}
|
|
1199
1267
|
});
|
|
1200
1268
|
|
|
1201
|
-
// src/audit/types.ts
|
|
1202
|
-
var ALL_CHECKS;
|
|
1203
|
-
var init_types = __esm({
|
|
1204
|
-
"src/audit/types.ts"() {
|
|
1205
|
-
"use strict";
|
|
1206
|
-
ALL_CHECKS = [
|
|
1207
|
-
"deleted-reference",
|
|
1208
|
-
"new-module",
|
|
1209
|
-
"stale-count",
|
|
1210
|
-
"dead-command",
|
|
1211
|
-
"deps-changed",
|
|
1212
|
-
"decision-anchor-drift"
|
|
1213
|
-
];
|
|
1214
|
-
}
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
1269
|
// src/audit/scope.ts
|
|
1218
1270
|
import fs7 from "fs/promises";
|
|
1219
1271
|
import path11 from "path";
|
|
@@ -1324,7 +1376,7 @@ var init_deleted_reference = __esm({
|
|
|
1324
1376
|
});
|
|
1325
1377
|
|
|
1326
1378
|
// src/audit/checks/new-module.ts
|
|
1327
|
-
import
|
|
1379
|
+
import fg6 from "fast-glob";
|
|
1328
1380
|
import fs8 from "fs/promises";
|
|
1329
1381
|
import path13 from "path";
|
|
1330
1382
|
function escapeRegExp(text2) {
|
|
@@ -1386,7 +1438,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1386
1438
|
const result = /* @__PURE__ */ new Map();
|
|
1387
1439
|
for (const dir of parents) {
|
|
1388
1440
|
const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
|
|
1389
|
-
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${
|
|
1441
|
+
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
|
|
1390
1442
|
ignore,
|
|
1391
1443
|
onlyDirectories: true,
|
|
1392
1444
|
label: "Module directory discovery",
|
|
@@ -1399,7 +1451,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1399
1451
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1400
1452
|
let count3 = 0;
|
|
1401
1453
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1402
|
-
count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) =>
|
|
1454
|
+
count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)), {
|
|
1403
1455
|
ignore,
|
|
1404
1456
|
followSymbolicLinks: false,
|
|
1405
1457
|
label: `Source discovery in ${dir}`
|
|
@@ -1734,6 +1786,87 @@ var init_dead_command = __esm({
|
|
|
1734
1786
|
}
|
|
1735
1787
|
});
|
|
1736
1788
|
|
|
1789
|
+
// src/audit/dependency-claims.ts
|
|
1790
|
+
import path16 from "path";
|
|
1791
|
+
import { parse as parseToml } from "smol-toml";
|
|
1792
|
+
function dependencyText(content) {
|
|
1793
|
+
return content.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, (text2) => text2.replace(/[^\n]/g, " ")).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
|
|
1794
|
+
}
|
|
1795
|
+
function declarations(file, source) {
|
|
1796
|
+
const result = /* @__PURE__ */ new Map();
|
|
1797
|
+
if (!source) return result;
|
|
1798
|
+
if (path16.posix.basename(file) === "package.json") {
|
|
1799
|
+
const json = JSON.parse(source);
|
|
1800
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "engines"]) {
|
|
1801
|
+
for (const [name, value] of Object.entries(json[section] ?? {})) {
|
|
1802
|
+
if (typeof value === "string") result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
} else if (file.endsWith("libs.versions.toml")) {
|
|
1806
|
+
const catalog = parseToml(source);
|
|
1807
|
+
for (const section of ["libraries", "plugins"]) for (const [alias, entry] of Object.entries(catalog[section] ?? {})) {
|
|
1808
|
+
if (typeof entry === "string") {
|
|
1809
|
+
const parts = entry.split(":");
|
|
1810
|
+
if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
|
|
1811
|
+
} else if (entry && typeof entry === "object") {
|
|
1812
|
+
const name = entry.module ?? (entry.group && entry.name ? `${entry.group}:${entry.name}` : entry.id);
|
|
1813
|
+
const version = typeof entry.version === "string" ? entry.version : catalog.versions?.[entry.version?.ref];
|
|
1814
|
+
if (typeof name === "string") result.set(name, typeof version === "string" ? version : "unspecified");
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
} else if (/build\.gradle(?:\.kts)?$/.test(file)) {
|
|
1818
|
+
for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g)) result.set(match[1], match[2]);
|
|
1819
|
+
for (const match of source.matchAll(/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g)) result.set(match[1], match[2]);
|
|
1820
|
+
}
|
|
1821
|
+
return result;
|
|
1822
|
+
}
|
|
1823
|
+
async function matchDependencyChanges(root, from, head, files, content) {
|
|
1824
|
+
const matches = [];
|
|
1825
|
+
const lines = dependencyText(content).split("\n");
|
|
1826
|
+
let incomplete = files.length > 64;
|
|
1827
|
+
for (const file of files.slice(0, 64)) {
|
|
1828
|
+
if (!/(?:^|\/)(?:package\.json|libs\.versions\.toml|build\.gradle(?:\.kts)?)$/.test(file)) {
|
|
1829
|
+
incomplete = true;
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
try {
|
|
1833
|
+
const read = async (revision) => {
|
|
1834
|
+
const { stdout: entry } = await inspectionGit(["ls-tree", revision, "--", `:(literal)${file}`], { cwd: root, timeout: 1e4 });
|
|
1835
|
+
if (!entry) return "";
|
|
1836
|
+
if (!entry.startsWith("100644 ") && !entry.startsWith("100755 ")) throw new Error("Unsupported manifest mode");
|
|
1837
|
+
return (await inspectionGit(["show", `${revision}:${file}`], { cwd: root, maxBuffer: 1024 * 1024, timeout: 1e4 })).stdout;
|
|
1838
|
+
};
|
|
1839
|
+
const [before, after] = await Promise.all([read(from), read(head)]);
|
|
1840
|
+
const old = declarations(file, before), next = declarations(file, after);
|
|
1841
|
+
for (const dependency of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
|
|
1842
|
+
if (old.get(dependency) === next.get(dependency)) continue;
|
|
1843
|
+
const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1844
|
+
const mention = new RegExp(`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`, "i");
|
|
1845
|
+
const line = lines.findIndex((text2) => mention.test(text2));
|
|
1846
|
+
if (line < 0) continue;
|
|
1847
|
+
matches.push({
|
|
1848
|
+
dependency,
|
|
1849
|
+
manifest: file,
|
|
1850
|
+
before: old.get(dependency) ?? null,
|
|
1851
|
+
after: next.get(dependency) ?? null,
|
|
1852
|
+
line: line + 1,
|
|
1853
|
+
excerpt: lines[line].trim().slice(0, 500)
|
|
1854
|
+
});
|
|
1855
|
+
if (matches.length >= 100) return { matches, incomplete: true };
|
|
1856
|
+
}
|
|
1857
|
+
} catch {
|
|
1858
|
+
incomplete = true;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
return { matches, incomplete };
|
|
1862
|
+
}
|
|
1863
|
+
var init_dependency_claims = __esm({
|
|
1864
|
+
"src/audit/dependency-claims.ts"() {
|
|
1865
|
+
"use strict";
|
|
1866
|
+
init_inspection();
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1737
1870
|
// src/audit/release-metadata.ts
|
|
1738
1871
|
async function git(root, args) {
|
|
1739
1872
|
return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
@@ -1813,13 +1946,6 @@ async function checkDepsChanged(ctx) {
|
|
|
1813
1946
|
});
|
|
1814
1947
|
continue;
|
|
1815
1948
|
}
|
|
1816
|
-
if (doc.dirty) {
|
|
1817
|
-
result.skipped.push({
|
|
1818
|
-
check: "deps-changed",
|
|
1819
|
-
doc: doc.path,
|
|
1820
|
-
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
1821
|
-
});
|
|
1822
|
-
}
|
|
1823
1949
|
let content = doc.content;
|
|
1824
1950
|
if (doc.dirty) {
|
|
1825
1951
|
try {
|
|
@@ -1832,7 +1958,6 @@ async function checkDepsChanged(ctx) {
|
|
|
1832
1958
|
continue;
|
|
1833
1959
|
}
|
|
1834
1960
|
}
|
|
1835
|
-
if (!hasDependencyContent(content)) continue;
|
|
1836
1961
|
const range = await commitsTouchingSince(
|
|
1837
1962
|
ctx.root,
|
|
1838
1963
|
doc.lastCommit.hash,
|
|
@@ -1856,16 +1981,32 @@ async function checkDepsChanged(ctx) {
|
|
|
1856
1981
|
range.commits = relevant;
|
|
1857
1982
|
range.total = relevant.length;
|
|
1858
1983
|
if (range.total === 0) continue;
|
|
1859
|
-
const
|
|
1984
|
+
const relevance = await matchDependencyChanges(
|
|
1985
|
+
ctx.root,
|
|
1986
|
+
doc.lastCommit.hash,
|
|
1987
|
+
ctx.headHash,
|
|
1988
|
+
[...new Set(range.commits.flatMap((commit) => commit.files))],
|
|
1989
|
+
content
|
|
1990
|
+
);
|
|
1991
|
+
if (!relevance.matches.length && !hasDependencyContent(content)) continue;
|
|
1992
|
+
const match = relevance.matches[0];
|
|
1993
|
+
if (doc.dirty && match) result.skipped.push({
|
|
1994
|
+
check: "deps-changed",
|
|
1995
|
+
doc: doc.path,
|
|
1996
|
+
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
1997
|
+
});
|
|
1860
1998
|
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
1861
1999
|
type: "deps-changed",
|
|
1862
|
-
|
|
1863
|
-
|
|
2000
|
+
...match ? {} : { resolution: "informational" },
|
|
2001
|
+
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." : ""}`,
|
|
2002
|
+
anchor: { doc: doc.path, line: match?.line ?? null, excerpt: match?.excerpt ?? null },
|
|
1864
2003
|
evidence: {
|
|
1865
2004
|
kind: "doc-behind-manifests",
|
|
1866
2005
|
docLastCommit: doc.lastCommit,
|
|
1867
2006
|
manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
|
|
1868
|
-
totalCommits: range.total
|
|
2007
|
+
totalCommits: range.total,
|
|
2008
|
+
matches: relevance.matches,
|
|
2009
|
+
matchingIncomplete: relevance.incomplete
|
|
1869
2010
|
}
|
|
1870
2011
|
});
|
|
1871
2012
|
}
|
|
@@ -1875,6 +2016,7 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
|
1875
2016
|
var init_deps_changed = __esm({
|
|
1876
2017
|
"src/audit/checks/deps-changed.ts"() {
|
|
1877
2018
|
"use strict";
|
|
2019
|
+
init_dependency_claims();
|
|
1878
2020
|
init_docs();
|
|
1879
2021
|
init_release_metadata();
|
|
1880
2022
|
init_git();
|
|
@@ -1899,7 +2041,7 @@ var init_deps_changed = __esm({
|
|
|
1899
2041
|
});
|
|
1900
2042
|
|
|
1901
2043
|
// src/context/lexical.ts
|
|
1902
|
-
import
|
|
2044
|
+
import path17 from "path";
|
|
1903
2045
|
var init_lexical = __esm({
|
|
1904
2046
|
"src/context/lexical.ts"() {
|
|
1905
2047
|
"use strict";
|
|
@@ -2060,7 +2202,7 @@ var init_provenance = __esm({
|
|
|
2060
2202
|
|
|
2061
2203
|
// src/decisions/decisions.ts
|
|
2062
2204
|
import fs9 from "fs/promises";
|
|
2063
|
-
import
|
|
2205
|
+
import path18 from "path";
|
|
2064
2206
|
import { createHash } from "crypto";
|
|
2065
2207
|
import { z as z3 } from "zod";
|
|
2066
2208
|
async function loadDecisionStore(rootDir) {
|
|
@@ -2111,9 +2253,9 @@ var init_decisions = __esm({
|
|
|
2111
2253
|
});
|
|
2112
2254
|
|
|
2113
2255
|
// src/decisions/drift.ts
|
|
2114
|
-
import
|
|
2256
|
+
import path19 from "path";
|
|
2115
2257
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
2116
|
-
const resolvedRoot =
|
|
2258
|
+
const resolvedRoot = path19.resolve(rootDir);
|
|
2117
2259
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
2118
2260
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
2119
2261
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -2309,7 +2451,9 @@ var init_findings = __esm({
|
|
|
2309
2451
|
kind: z4.literal("doc-behind-manifests"),
|
|
2310
2452
|
docLastCommit: commitSchema,
|
|
2311
2453
|
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
2312
|
-
totalCommits: count
|
|
2454
|
+
totalCommits: count,
|
|
2455
|
+
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(),
|
|
2456
|
+
matchingIncomplete: z4.boolean().optional()
|
|
2313
2457
|
}),
|
|
2314
2458
|
z4.object({
|
|
2315
2459
|
kind: z4.literal("decision-anchor"),
|
|
@@ -2325,7 +2469,7 @@ var init_findings = __esm({
|
|
|
2325
2469
|
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2326
2470
|
confidence: z4.enum(["certain", "likely"])
|
|
2327
2471
|
});
|
|
2328
|
-
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.
|
|
2472
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.enum(["recheck", "informational"]).optional() });
|
|
2329
2473
|
checkResultSchema = z4.object({
|
|
2330
2474
|
issues: z4.array(issueSchema),
|
|
2331
2475
|
advisories: z4.array(advisorySchema),
|
|
@@ -2335,8 +2479,30 @@ var init_findings = __esm({
|
|
|
2335
2479
|
}
|
|
2336
2480
|
});
|
|
2337
2481
|
|
|
2338
|
-
// src/automation/
|
|
2482
|
+
// src/automation/notifications.ts
|
|
2339
2483
|
import { z as z5 } from "zod";
|
|
2484
|
+
var conditionSchema, notificationStateSchema;
|
|
2485
|
+
var init_notifications = __esm({
|
|
2486
|
+
"src/automation/notifications.ts"() {
|
|
2487
|
+
"use strict";
|
|
2488
|
+
init_findings();
|
|
2489
|
+
conditionSchema = z5.object({
|
|
2490
|
+
level: z5.number(),
|
|
2491
|
+
facts: z5.array(z5.string()),
|
|
2492
|
+
magnitude: z5.number().optional()
|
|
2493
|
+
});
|
|
2494
|
+
notificationStateSchema = z5.object({
|
|
2495
|
+
version: z5.literal(1),
|
|
2496
|
+
baselineHead: z5.string(),
|
|
2497
|
+
baseline: z5.record(conditionSchema),
|
|
2498
|
+
delivered: z5.record(conditionSchema),
|
|
2499
|
+
cleared: z5.array(z5.string())
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
// src/automation/store.ts
|
|
2505
|
+
import { z as z6 } from "zod";
|
|
2340
2506
|
function withLock(root, directory, run, waitMs = 5e3) {
|
|
2341
2507
|
return withStoreLock(root, directory, run, waitMs, "Automation");
|
|
2342
2508
|
}
|
|
@@ -2345,29 +2511,28 @@ var init_store = __esm({
|
|
|
2345
2511
|
"src/automation/store.ts"() {
|
|
2346
2512
|
"use strict";
|
|
2347
2513
|
init_store_lock();
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2514
|
+
init_notifications();
|
|
2515
|
+
hostSchema = z6.enum(["claude", "codex"]);
|
|
2516
|
+
stateSchema = z6.object({
|
|
2517
|
+
version: z6.literal(1),
|
|
2518
|
+
root: z6.string(),
|
|
2519
|
+
gitDir: z6.string(),
|
|
2520
|
+
branch: z6.string(),
|
|
2521
|
+
baselines: z6.array(z6.object({ path: z6.string(), at: z6.string(), event: z6.string(), fingerprint: z6.string() })).max(128),
|
|
2522
|
+
sessions: z6.record(z6.object({
|
|
2356
2523
|
host: hostSchema,
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
mutationObserved: z5.boolean(),
|
|
2363
|
-
pending: z5.record(z5.string()),
|
|
2364
|
-
coverageGaps: z5.array(z5.string()),
|
|
2365
|
-
events: z5.record(z5.object({ at: z5.string(), count: z5.number().int().positive() }))
|
|
2524
|
+
lastUsed: z6.string(),
|
|
2525
|
+
notifiedDiagnostics: z6.array(z6.string()).default([]),
|
|
2526
|
+
pending: z6.record(z6.string()),
|
|
2527
|
+
coverageGaps: z6.array(z6.string()),
|
|
2528
|
+
events: z6.record(z6.object({ at: z6.string(), count: z6.number().int().positive() }))
|
|
2366
2529
|
})),
|
|
2367
|
-
updatedAt:
|
|
2368
|
-
fingerprint:
|
|
2369
|
-
latest:
|
|
2370
|
-
|
|
2530
|
+
updatedAt: z6.string(),
|
|
2531
|
+
fingerprint: z6.string().nullable(),
|
|
2532
|
+
latest: z6.string().nullable(),
|
|
2533
|
+
notifications: notificationStateSchema.optional(),
|
|
2534
|
+
agentNotifications: notificationStateSchema.optional(),
|
|
2535
|
+
analysis: z6.object({ id: z6.string(), fingerprint: z6.string(), completedAt: z6.number(), path: z6.string() }).optional()
|
|
2371
2536
|
});
|
|
2372
2537
|
}
|
|
2373
2538
|
});
|
|
@@ -2387,9 +2552,9 @@ __export(repair_exports, {
|
|
|
2387
2552
|
withRepairSession: () => withRepairSession
|
|
2388
2553
|
});
|
|
2389
2554
|
import fs10 from "fs/promises";
|
|
2390
|
-
import
|
|
2555
|
+
import path20 from "path";
|
|
2391
2556
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2392
|
-
import { z as
|
|
2557
|
+
import { z as z7 } from "zod";
|
|
2393
2558
|
function recheckable(finding) {
|
|
2394
2559
|
return !("confidence" in finding) && finding.resolution === "recheck" && ["deleted-reference", "dead-command", "new-module"].includes(finding.type) && ["missing-path", "missing-script", "unmentioned-dir"].includes(finding.evidence.kind);
|
|
2395
2560
|
}
|
|
@@ -2408,7 +2573,7 @@ async function docState(root) {
|
|
|
2408
2573
|
docs.push([doc, null]);
|
|
2409
2574
|
}
|
|
2410
2575
|
}
|
|
2411
|
-
return digest(docs);
|
|
2576
|
+
return digest([docs, (await loadProjectConfig(root)).audit]);
|
|
2412
2577
|
}
|
|
2413
2578
|
async function stableAudit(root, checks, options = {}) {
|
|
2414
2579
|
return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
|
|
@@ -2460,7 +2625,7 @@ async function withRepairSession(root, options, run, validate, cachedAudit) {
|
|
|
2460
2625
|
}
|
|
2461
2626
|
async function prepareOriginal(rootDir, checks, options, session) {
|
|
2462
2627
|
const root = await fs10.realpath(rootDir);
|
|
2463
|
-
const selected =
|
|
2628
|
+
const selected = z7.array(checkSchema).nonempty().parse(checks);
|
|
2464
2629
|
const report = await (session ? session.audit(selected) : stableAudit(root, selected, options));
|
|
2465
2630
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
2466
2631
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
@@ -2477,8 +2642,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
|
|
|
2477
2642
|
}
|
|
2478
2643
|
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
2479
2644
|
const root = await fs10.realpath(rootDir);
|
|
2480
|
-
const declaredRoot =
|
|
2481
|
-
const relative =
|
|
2645
|
+
const declaredRoot = path20.resolve(rootDir);
|
|
2646
|
+
const relative = path20.isAbsolute(baselinePath) ? path20.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2482
2647
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
2483
2648
|
const { digest: savedDigest, ...payload } = stored;
|
|
2484
2649
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
@@ -2527,6 +2692,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2527
2692
|
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
2528
2693
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
2529
2694
|
}
|
|
2695
|
+
if (current.excludedChecks?.some((excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc)) {
|
|
2696
|
+
return { ...base, status: "resolved", reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained." };
|
|
2697
|
+
}
|
|
2530
2698
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
2531
2699
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
2532
2700
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
@@ -2537,6 +2705,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2537
2705
|
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
2538
2706
|
return { ...base, review, status: "review-required", reason: review.reason };
|
|
2539
2707
|
}
|
|
2708
|
+
if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
|
|
2709
|
+
return { ...base, status: "resolved", reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required." };
|
|
2710
|
+
}
|
|
2540
2711
|
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
2541
2712
|
return {
|
|
2542
2713
|
...base,
|
|
@@ -2565,7 +2736,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2565
2736
|
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
2566
2737
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
2567
2738
|
for (const f of findings) counts[f.status]++;
|
|
2568
|
-
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)));
|
|
2739
|
+
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)));
|
|
2569
2740
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
2570
2741
|
return {
|
|
2571
2742
|
version: 1,
|
|
@@ -2588,7 +2759,7 @@ function repairExitCode(report) {
|
|
|
2588
2759
|
function formatRepairSummary(report) {
|
|
2589
2760
|
return [
|
|
2590
2761
|
"Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
|
|
2591
|
-
...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason + "\n findingId: " + f.id),
|
|
2762
|
+
...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),
|
|
2592
2763
|
...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
|
|
2593
2764
|
...report.diagnostics.map((d) => " [unverified] " + d),
|
|
2594
2765
|
...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
|
|
@@ -2611,41 +2782,42 @@ var init_repair = __esm({
|
|
|
2611
2782
|
init_findings();
|
|
2612
2783
|
init_advisory_review();
|
|
2613
2784
|
init_findings();
|
|
2614
|
-
count2 =
|
|
2615
|
-
reportSchema =
|
|
2616
|
-
version:
|
|
2617
|
-
root:
|
|
2618
|
-
gitAvailable:
|
|
2785
|
+
count2 = z7.number().int().nonnegative();
|
|
2786
|
+
reportSchema = z7.object({
|
|
2787
|
+
version: z7.literal(1),
|
|
2788
|
+
root: z7.string(),
|
|
2789
|
+
gitAvailable: z7.literal(true),
|
|
2619
2790
|
headHash: commitSchema.shape.hash,
|
|
2620
|
-
checksRun:
|
|
2621
|
-
docs:
|
|
2622
|
-
path:
|
|
2623
|
-
scope:
|
|
2624
|
-
kind:
|
|
2791
|
+
checksRun: z7.array(checkSchema).nonempty(),
|
|
2792
|
+
docs: z7.array(z7.object({
|
|
2793
|
+
path: z7.string().refine((value) => normalizeRepoPath(value) === value),
|
|
2794
|
+
scope: z7.string().optional(),
|
|
2795
|
+
kind: z7.enum(["instructions", "readme", "guide"]).optional(),
|
|
2625
2796
|
lastCommit: commitSchema.nullable(),
|
|
2626
|
-
dirty:
|
|
2797
|
+
dirty: z7.boolean(),
|
|
2627
2798
|
lineCount: count2
|
|
2628
2799
|
})).nonempty(),
|
|
2629
|
-
decisionsChecked:
|
|
2630
|
-
clean:
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2800
|
+
decisionsChecked: z7.boolean(),
|
|
2801
|
+
clean: z7.boolean(),
|
|
2802
|
+
excludedChecks: z7.array(z7.object({ check: checkSchema, doc: z7.string() })).optional(),
|
|
2803
|
+
issues: z7.array(issueSchema),
|
|
2804
|
+
advisories: z7.array(advisorySchema),
|
|
2805
|
+
suppressedAdvisories: z7.array(advisorySchema).optional(),
|
|
2806
|
+
skippedChecks: z7.array(z7.object({ check: z7.string(), reason: z7.string(), doc: z7.string().optional() }))
|
|
2635
2807
|
});
|
|
2636
|
-
baselineSchema =
|
|
2637
|
-
kind:
|
|
2638
|
-
version:
|
|
2639
|
-
createdAt:
|
|
2808
|
+
baselineSchema = z7.object({
|
|
2809
|
+
kind: z7.literal("mason-audit-repair"),
|
|
2810
|
+
version: z7.literal(1),
|
|
2811
|
+
createdAt: z7.string().datetime(),
|
|
2640
2812
|
report: reportSchema,
|
|
2641
|
-
digest:
|
|
2813
|
+
digest: z7.string().regex(/^[a-f0-9]{64}$/)
|
|
2642
2814
|
});
|
|
2643
|
-
repairFindingSchema =
|
|
2644
|
-
id:
|
|
2645
|
-
original:
|
|
2646
|
-
status:
|
|
2647
|
-
reason:
|
|
2648
|
-
current:
|
|
2815
|
+
repairFindingSchema = z7.object({
|
|
2816
|
+
id: z7.string(),
|
|
2817
|
+
original: z7.union([issueSchema, advisorySchema]),
|
|
2818
|
+
status: z7.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
2819
|
+
reason: z7.string(),
|
|
2820
|
+
current: z7.union([issueSchema, advisorySchema]).optional(),
|
|
2649
2821
|
review: reviewSummarySchema.optional()
|
|
2650
2822
|
});
|
|
2651
2823
|
}
|
|
@@ -2653,20 +2825,24 @@ var init_repair = __esm({
|
|
|
2653
2825
|
|
|
2654
2826
|
// src/audit/advisory-review.ts
|
|
2655
2827
|
import fs11 from "fs/promises";
|
|
2656
|
-
import { z as
|
|
2828
|
+
import { z as z8 } from "zod";
|
|
2657
2829
|
async function readRecord(root, id) {
|
|
2658
2830
|
const raw = await readStoreJson(root, recordPath(id));
|
|
2659
2831
|
if (raw === null) return null;
|
|
2660
2832
|
const record = recordSchema.parse(raw);
|
|
2661
2833
|
const { digest: stored, ...payload } = record;
|
|
2662
|
-
if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || event.
|
|
2834
|
+
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))) {
|
|
2663
2835
|
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2664
2836
|
}
|
|
2665
2837
|
return record;
|
|
2666
2838
|
}
|
|
2667
|
-
function
|
|
2839
|
+
function evidenceFingerprint(evidence, reason) {
|
|
2840
|
+
return digest([evidence.scope, evidence.tree, ...reason === "no-dependency-claims" ? [] : [evidence.lastScopeCommit], evidence.paths]);
|
|
2841
|
+
}
|
|
2842
|
+
function scopes(finding, reason) {
|
|
2668
2843
|
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2669
2844
|
if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
2845
|
+
if (reason === "no-dependency-claims" && e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`];
|
|
2670
2846
|
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
2671
2847
|
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
2672
2848
|
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
@@ -2677,9 +2853,9 @@ function scopes(finding) {
|
|
|
2677
2853
|
return `:(literal)${file}`;
|
|
2678
2854
|
}).sort();
|
|
2679
2855
|
}
|
|
2680
|
-
async function reviewEvidence(root, finding) {
|
|
2856
|
+
async function reviewEvidence(root, finding, reason) {
|
|
2681
2857
|
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
2682
|
-
const scope = scopes(finding);
|
|
2858
|
+
const scope = scopes(finding, reason);
|
|
2683
2859
|
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2684
2860
|
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2685
2861
|
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
@@ -2705,7 +2881,7 @@ async function reviewEvidence(root, finding) {
|
|
|
2705
2881
|
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2706
2882
|
}
|
|
2707
2883
|
}
|
|
2708
|
-
const fingerprint =
|
|
2884
|
+
const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason);
|
|
2709
2885
|
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
2710
2886
|
return {
|
|
2711
2887
|
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
@@ -2752,7 +2928,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2752
2928
|
const record = await readRecord(root, id);
|
|
2753
2929
|
if (!record) return null;
|
|
2754
2930
|
const event = record.events.at(-1);
|
|
2755
|
-
const current = await reviewEvidence(root, event.finding);
|
|
2931
|
+
const current = await reviewEvidence(root, event.finding, event.reasonCode);
|
|
2756
2932
|
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
2757
2933
|
const base = {
|
|
2758
2934
|
id,
|
|
@@ -2762,7 +2938,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2762
2938
|
outcome: event.outcome,
|
|
2763
2939
|
reviewedHead: event.evidence.head
|
|
2764
2940
|
};
|
|
2765
|
-
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." };
|
|
2941
|
+
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." };
|
|
2766
2942
|
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." };
|
|
2767
2943
|
} catch (error) {
|
|
2768
2944
|
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -2790,9 +2966,13 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2790
2966
|
decisionId: finding.evidence.decisionId,
|
|
2791
2967
|
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."
|
|
2792
2968
|
};
|
|
2969
|
+
if (request.action === "dismiss") {
|
|
2970
|
+
if (finding.type !== "deps-changed" || finding.evidence.kind !== "doc-behind-manifests") throw new Error("One-step dismissal is only available for dependency advisories.");
|
|
2971
|
+
if (!request.reasonCode || !request.note) throw new Error("Dismissal requires reasonCode and note; it does not require a reviewer or prepared token.");
|
|
2972
|
+
} else if (request.reasonCode) throw new Error("reasonCode is only used with dismiss.");
|
|
2793
2973
|
const prepare = async () => {
|
|
2794
2974
|
const record = await readRecord(root, request.findingId);
|
|
2795
|
-
const evidence = await reviewEvidence(root, finding);
|
|
2975
|
+
const evidence = await reviewEvidence(root, finding, request.reasonCode);
|
|
2796
2976
|
await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
|
|
2797
2977
|
const token = digest({ baseline: baseline.original, finding, evidence, record });
|
|
2798
2978
|
return { record, evidence, token };
|
|
@@ -2821,10 +3001,10 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2821
3001
|
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."
|
|
2822
3002
|
};
|
|
2823
3003
|
}
|
|
2824
|
-
if (!request.reviewer || !request.note || !request.reviewToken) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
|
|
3004
|
+
if (request.action !== "dismiss" && (!request.reviewer || !request.note || !request.reviewToken)) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
|
|
2825
3005
|
return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
|
|
2826
3006
|
const state = await prepare();
|
|
2827
|
-
if (state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
|
|
3007
|
+
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.");
|
|
2828
3008
|
if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
|
|
2829
3009
|
if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
|
|
2830
3010
|
const { dirty, documentDigest, ...evidence } = state.evidence;
|
|
@@ -2832,7 +3012,8 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2832
3012
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2833
3013
|
reviewer: request.reviewer,
|
|
2834
3014
|
note: request.note,
|
|
2835
|
-
outcome: request.action,
|
|
3015
|
+
outcome: request.action === "dismiss" ? "dismissed" : request.action,
|
|
3016
|
+
reasonCode: request.reasonCode,
|
|
2836
3017
|
finding,
|
|
2837
3018
|
evidence
|
|
2838
3019
|
});
|
|
@@ -2843,11 +3024,11 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2843
3024
|
status: "recorded",
|
|
2844
3025
|
recordPath: recordPath(request.findingId),
|
|
2845
3026
|
event,
|
|
2846
|
-
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."
|
|
3027
|
+
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."
|
|
2847
3028
|
};
|
|
2848
3029
|
});
|
|
2849
3030
|
}
|
|
2850
|
-
var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3031
|
+
var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2851
3032
|
var init_advisory_review = __esm({
|
|
2852
3033
|
"src/audit/advisory-review.ts"() {
|
|
2853
3034
|
"use strict";
|
|
@@ -2863,39 +3044,42 @@ var init_advisory_review = __esm({
|
|
|
2863
3044
|
init_decisions();
|
|
2864
3045
|
init_drift2();
|
|
2865
3046
|
git2 = async (root, ...args) => (await (["log", "ls-tree", "ls-files", "merge-base"].includes(args[0]) ? inspectionGit : execGit)(args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
2866
|
-
hashSchema =
|
|
2867
|
-
commitSchema2 =
|
|
2868
|
-
evidenceSchema2 =
|
|
3047
|
+
hashSchema = z8.string().regex(/^[a-f0-9]{64}$/);
|
|
3048
|
+
commitSchema2 = z8.string().regex(/^[a-f0-9]{40,64}$/);
|
|
3049
|
+
evidenceSchema2 = z8.object({
|
|
2869
3050
|
head: commitSchema2,
|
|
2870
3051
|
fingerprint: hashSchema,
|
|
2871
|
-
scope:
|
|
2872
|
-
tree:
|
|
3052
|
+
scope: z8.array(z8.string()).min(1).max(1e3),
|
|
3053
|
+
tree: z8.string().max(2 * 1024 * 1024),
|
|
2873
3054
|
lastScopeCommit: commitSchema2,
|
|
2874
|
-
paths:
|
|
3055
|
+
paths: z8.array(z8.object({ path: z8.string(), exists: z8.boolean() })).max(1e3)
|
|
2875
3056
|
});
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
3057
|
+
dismissalReasonSchema = z8.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
3058
|
+
eventSchema2 = z8.object({
|
|
3059
|
+
at: z8.string().datetime(),
|
|
3060
|
+
reviewer: z8.string().trim().min(1).max(200).optional(),
|
|
3061
|
+
note: z8.string().trim().min(1).max(2500),
|
|
3062
|
+
outcome: z8.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
3063
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
2881
3064
|
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2882
3065
|
evidence: evidenceSchema2
|
|
2883
3066
|
});
|
|
2884
|
-
recordSchema =
|
|
2885
|
-
reviewSummarySchema =
|
|
3067
|
+
recordSchema = z8.object({ version: z8.literal(1), id: hashSchema, events: z8.array(eventSchema2).min(1).max(200), digest: hashSchema });
|
|
3068
|
+
reviewSummarySchema = z8.object({
|
|
2886
3069
|
id: hashSchema,
|
|
2887
|
-
status:
|
|
2888
|
-
reason:
|
|
2889
|
-
recordPath:
|
|
2890
|
-
reviewer:
|
|
2891
|
-
note:
|
|
2892
|
-
outcome:
|
|
2893
|
-
reviewedHead:
|
|
3070
|
+
status: z8.enum(["current", "reopened", "deferred", "unverified"]),
|
|
3071
|
+
reason: z8.string(),
|
|
3072
|
+
recordPath: z8.string().optional(),
|
|
3073
|
+
reviewer: z8.string().optional(),
|
|
3074
|
+
note: z8.string().optional(),
|
|
3075
|
+
outcome: z8.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
|
|
3076
|
+
reviewedHead: z8.string().optional()
|
|
2894
3077
|
});
|
|
2895
|
-
advisoryReviewRequest =
|
|
2896
|
-
baselinePath:
|
|
3078
|
+
advisoryReviewRequest = z8.object({
|
|
3079
|
+
baselinePath: z8.string().min(1),
|
|
2897
3080
|
findingId: hashSchema,
|
|
2898
|
-
action:
|
|
3081
|
+
action: z8.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
3082
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
2899
3083
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2900
3084
|
note: eventSchema2.shape.note.optional(),
|
|
2901
3085
|
reviewToken: hashSchema.optional()
|
|
@@ -2906,12 +3090,12 @@ var init_advisory_review = __esm({
|
|
|
2906
3090
|
|
|
2907
3091
|
// src/audit/audit.ts
|
|
2908
3092
|
import fs12 from "fs/promises";
|
|
2909
|
-
import
|
|
3093
|
+
import path21 from "path";
|
|
2910
3094
|
async function computeAudit(rootDir, options = {}) {
|
|
2911
3095
|
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
2912
3096
|
}
|
|
2913
3097
|
async function auditCurrent(rootDir, options) {
|
|
2914
|
-
const resolvedRoot =
|
|
3098
|
+
const resolvedRoot = path21.resolve(rootDir);
|
|
2915
3099
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2916
3100
|
if (docs.length === 0) return null;
|
|
2917
3101
|
const report = {
|
|
@@ -2945,7 +3129,7 @@ async function auditCurrent(rootDir, options) {
|
|
|
2945
3129
|
}
|
|
2946
3130
|
let decisionsPresent = false;
|
|
2947
3131
|
try {
|
|
2948
|
-
await fs12.access(
|
|
3132
|
+
await fs12.access(path21.join(resolvedRoot, ".mason", "decisions"));
|
|
2949
3133
|
decisionsPresent = true;
|
|
2950
3134
|
} catch {
|
|
2951
3135
|
}
|
|
@@ -2957,13 +3141,21 @@ async function auditCurrent(rootDir, options) {
|
|
|
2957
3141
|
changesSinceDoc,
|
|
2958
3142
|
decisionsPresent
|
|
2959
3143
|
};
|
|
3144
|
+
const policy = (await loadProjectConfig(resolvedRoot)).audit;
|
|
2960
3145
|
const selected = options.checks ?? ALL_CHECKS;
|
|
2961
3146
|
for (const name of ALL_CHECKS) {
|
|
2962
3147
|
if (!selected.includes(name)) continue;
|
|
2963
|
-
const
|
|
3148
|
+
const exclusions = policy?.exclude?.[name] ?? [];
|
|
3149
|
+
const included = await includedCheckPaths(resolvedRoot, docs.map((doc) => doc.path), exclusions);
|
|
3150
|
+
const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
|
|
3151
|
+
if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
|
|
3152
|
+
const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
|
|
3153
|
+
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
|
|
2964
3154
|
report.checksRun.push(name);
|
|
2965
3155
|
report.issues.push(...issues);
|
|
2966
|
-
|
|
3156
|
+
const advisoryPaths = await includedCheckPaths(resolvedRoot, [...new Set(advisories.map((f) => f.anchor.doc))], exclusions);
|
|
3157
|
+
report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
|
|
3158
|
+
for (const f of advisories) if (!advisoryPaths.has(f.anchor.doc)) (report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
|
|
2967
3159
|
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
2968
3160
|
report.skippedChecks.push(...skipped);
|
|
2969
3161
|
}
|
|
@@ -2975,6 +3167,8 @@ async function auditCurrent(rootDir, options) {
|
|
|
2975
3167
|
var init_audit = __esm({
|
|
2976
3168
|
"src/audit/audit.ts"() {
|
|
2977
3169
|
"use strict";
|
|
3170
|
+
init_files();
|
|
3171
|
+
init_policy();
|
|
2978
3172
|
init_profile();
|
|
2979
3173
|
init_drift();
|
|
2980
3174
|
init_snapshot();
|
|
@@ -2990,11 +3184,11 @@ init_audit();
|
|
|
2990
3184
|
init_types();
|
|
2991
3185
|
init_repair();
|
|
2992
3186
|
init_findings();
|
|
2993
|
-
import
|
|
3187
|
+
import path23 from "path";
|
|
2994
3188
|
|
|
2995
3189
|
// src/audit/advisory-cli.ts
|
|
2996
3190
|
init_advisory_review();
|
|
2997
|
-
import
|
|
3191
|
+
import path22 from "path";
|
|
2998
3192
|
import { stripVTControlCharacters } from "util";
|
|
2999
3193
|
var USAGE = `Usage: mason audit review --baseline <path> --finding <id> [options]
|
|
3000
3194
|
|
|
@@ -3002,13 +3196,15 @@ Prepare an advisory assessment against retained evidence (read-only by default).
|
|
|
3002
3196
|
--dir <path> Repository root (default: current directory)
|
|
3003
3197
|
--baseline <path> Original repair baseline
|
|
3004
3198
|
--finding <id> findingId from repair verification or automation
|
|
3005
|
-
--outcome <value> Record addressed, inapplicable, or
|
|
3199
|
+
--outcome <value> Record addressed, inapplicable, deferred, or dismiss
|
|
3200
|
+
--reason <code> Dismiss: no-dependency-claims or unrelated-manifest-change
|
|
3006
3201
|
--reviewer <name> Actual authorized reviewer
|
|
3007
3202
|
--note <reason> Assessment reason
|
|
3008
3203
|
--token <token> reviewToken returned by preparation
|
|
3009
3204
|
--json Full evidence and review history
|
|
3010
3205
|
|
|
3011
|
-
|
|
3206
|
+
Assessments require preparation and committed relevant edits. Dismiss requires only
|
|
3207
|
+
--reason and --note, with a committed document; no reviewer or token is needed. Review records
|
|
3012
3208
|
belong in Git under .mason/reviews/advisories/. Deferral remains outstanding.
|
|
3013
3209
|
Decision findings use review_decision; this command never approves decisions.
|
|
3014
3210
|
Exit 0: prepared/recorded; 2: unavailable, conflict, or decision review required.`;
|
|
@@ -3026,21 +3222,22 @@ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), e
|
|
|
3026
3222
|
json = true;
|
|
3027
3223
|
continue;
|
|
3028
3224
|
}
|
|
3029
|
-
if (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
|
|
3225
|
+
if (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token", "--reason"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
|
|
3030
3226
|
const value = args[++i];
|
|
3031
3227
|
if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
|
|
3032
3228
|
flags[flag] = value;
|
|
3033
3229
|
}
|
|
3034
|
-
if (!flags["--outcome"] && ["--reviewer", "--note", "--token"].some((key) => flags[key])) throw new Error("Use --outcome to record an assessment.");
|
|
3230
|
+
if (!flags["--outcome"] && ["--reviewer", "--note", "--token", "--reason"].some((key) => flags[key])) throw new Error("Use --outcome to record an assessment.");
|
|
3035
3231
|
const input = {
|
|
3036
3232
|
baselinePath: flags["--baseline"],
|
|
3037
3233
|
findingId: flags["--finding"],
|
|
3038
3234
|
action: flags["--outcome"] ?? "prepare",
|
|
3039
3235
|
reviewer: flags["--reviewer"],
|
|
3040
3236
|
note: flags["--note"],
|
|
3041
|
-
reviewToken: flags["--token"]
|
|
3237
|
+
reviewToken: flags["--token"],
|
|
3238
|
+
reasonCode: flags["--reason"]
|
|
3042
3239
|
};
|
|
3043
|
-
const result = await reviewAdvisory(
|
|
3240
|
+
const result = await reviewAdvisory(path22.resolve(flags["--dir"] ?? process.cwd()), input);
|
|
3044
3241
|
if (json) io.out(JSON.stringify(result, null, 2));
|
|
3045
3242
|
else {
|
|
3046
3243
|
const lines = [`Advisory review: ${result.status}.`];
|
|
@@ -3152,8 +3349,8 @@ function issueLine(issue) {
|
|
|
3152
3349
|
function formatAuditSummary(report) {
|
|
3153
3350
|
const lines = [];
|
|
3154
3351
|
const reviewed = new Set(report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
3155
|
-
const open = report.advisories.filter((finding) => !reviewed.has(findingId(finding)));
|
|
3156
|
-
const suppressed = (report.suppressedAdvisories ?? []).filter((finding) => !reviewed.has(findingId(finding)));
|
|
3352
|
+
const open = report.advisories.filter((finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding)));
|
|
3353
|
+
const suppressed = (report.suppressedAdvisories ?? []).filter((finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding)));
|
|
3157
3354
|
const reviewCount = open.length + suppressed.length;
|
|
3158
3355
|
for (const doc of report.docs) {
|
|
3159
3356
|
const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
|
|
@@ -3171,6 +3368,9 @@ function formatAuditSummary(report) {
|
|
|
3171
3368
|
for (const issue of report.issues) {
|
|
3172
3369
|
if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
|
|
3173
3370
|
}
|
|
3371
|
+
const information = [...report.advisories, ...report.suppressedAdvisories ?? []].filter((f) => f.resolution === "informational");
|
|
3372
|
+
if (information.length) lines.push(`${information.length} informational dependency notices; no review required (see --json).`);
|
|
3373
|
+
if (report.excludedChecks?.length) lines.push(`${report.excludedChecks.length} document/check pairs excluded by project policy (see --json).`);
|
|
3174
3374
|
if (open.length > 0) {
|
|
3175
3375
|
lines.push("Advisories (do not affect the exit code):");
|
|
3176
3376
|
for (const advisory of open) {
|
|
@@ -3224,7 +3424,7 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
3224
3424
|
"- new-module: review whether the directory needs documenting; an omission alone does not establish a defect. Preserve scope and describe only what you verified."
|
|
3225
3425
|
);
|
|
3226
3426
|
lines.push(
|
|
3227
|
-
"-
|
|
3427
|
+
"- 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."
|
|
3228
3428
|
);
|
|
3229
3429
|
lines.push("");
|
|
3230
3430
|
lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
|
|
@@ -3273,7 +3473,7 @@ async function runAuditCli(argv, io = {
|
|
|
3273
3473
|
io.out(USAGE2);
|
|
3274
3474
|
return 0;
|
|
3275
3475
|
}
|
|
3276
|
-
const rootDir =
|
|
3476
|
+
const rootDir = path23.resolve(args.dir);
|
|
3277
3477
|
if (args.baseline || args.prepareRepair) {
|
|
3278
3478
|
try {
|
|
3279
3479
|
if (args.baseline) {
|