mason-context 0.19.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/mason-audit.js +432 -253
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +430 -253
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +50 -0
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +1123 -941
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +50 -0
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +507 -296
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason-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),
|
|
@@ -2387,7 +2531,7 @@ __export(repair_exports, {
|
|
|
2387
2531
|
withRepairSession: () => withRepairSession
|
|
2388
2532
|
});
|
|
2389
2533
|
import fs10 from "fs/promises";
|
|
2390
|
-
import
|
|
2534
|
+
import path20 from "path";
|
|
2391
2535
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2392
2536
|
import { z as z6 } from "zod";
|
|
2393
2537
|
function recheckable(finding) {
|
|
@@ -2408,7 +2552,7 @@ async function docState(root) {
|
|
|
2408
2552
|
docs.push([doc, null]);
|
|
2409
2553
|
}
|
|
2410
2554
|
}
|
|
2411
|
-
return digest(docs);
|
|
2555
|
+
return digest([docs, (await loadProjectConfig(root)).audit]);
|
|
2412
2556
|
}
|
|
2413
2557
|
async function stableAudit(root, checks, options = {}) {
|
|
2414
2558
|
return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
|
|
@@ -2477,8 +2621,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
|
|
|
2477
2621
|
}
|
|
2478
2622
|
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
2479
2623
|
const root = await fs10.realpath(rootDir);
|
|
2480
|
-
const declaredRoot =
|
|
2481
|
-
const relative =
|
|
2624
|
+
const declaredRoot = path20.resolve(rootDir);
|
|
2625
|
+
const relative = path20.isAbsolute(baselinePath) ? path20.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2482
2626
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
2483
2627
|
const { digest: savedDigest, ...payload } = stored;
|
|
2484
2628
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
@@ -2527,6 +2671,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2527
2671
|
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
2528
2672
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
2529
2673
|
}
|
|
2674
|
+
if (current.excludedChecks?.some((excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc)) {
|
|
2675
|
+
return { ...base, status: "resolved", reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained." };
|
|
2676
|
+
}
|
|
2530
2677
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
2531
2678
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
2532
2679
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
@@ -2537,6 +2684,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2537
2684
|
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
2538
2685
|
return { ...base, review, status: "review-required", reason: review.reason };
|
|
2539
2686
|
}
|
|
2687
|
+
if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
|
|
2688
|
+
return { ...base, status: "resolved", reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required." };
|
|
2689
|
+
}
|
|
2540
2690
|
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
2541
2691
|
return {
|
|
2542
2692
|
...base,
|
|
@@ -2565,7 +2715,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2565
2715
|
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
2566
2716
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
2567
2717
|
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)));
|
|
2718
|
+
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
2719
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
2570
2720
|
return {
|
|
2571
2721
|
version: 1,
|
|
@@ -2588,7 +2738,7 @@ function repairExitCode(report) {
|
|
|
2588
2738
|
function formatRepairSummary(report) {
|
|
2589
2739
|
return [
|
|
2590
2740
|
"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),
|
|
2741
|
+
...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
2742
|
...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
|
|
2593
2743
|
...report.diagnostics.map((d) => " [unverified] " + d),
|
|
2594
2744
|
...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
|
|
@@ -2621,13 +2771,14 @@ var init_repair = __esm({
|
|
|
2621
2771
|
docs: z6.array(z6.object({
|
|
2622
2772
|
path: z6.string().refine((value) => normalizeRepoPath(value) === value),
|
|
2623
2773
|
scope: z6.string().optional(),
|
|
2624
|
-
kind: z6.enum(["instructions", "readme"]).optional(),
|
|
2774
|
+
kind: z6.enum(["instructions", "readme", "guide"]).optional(),
|
|
2625
2775
|
lastCommit: commitSchema.nullable(),
|
|
2626
2776
|
dirty: z6.boolean(),
|
|
2627
2777
|
lineCount: count2
|
|
2628
2778
|
})).nonempty(),
|
|
2629
2779
|
decisionsChecked: z6.boolean(),
|
|
2630
2780
|
clean: z6.boolean(),
|
|
2781
|
+
excludedChecks: z6.array(z6.object({ check: checkSchema, doc: z6.string() })).optional(),
|
|
2631
2782
|
issues: z6.array(issueSchema),
|
|
2632
2783
|
advisories: z6.array(advisorySchema),
|
|
2633
2784
|
suppressedAdvisories: z6.array(advisorySchema).optional(),
|
|
@@ -2659,14 +2810,18 @@ async function readRecord(root, id) {
|
|
|
2659
2810
|
if (raw === null) return null;
|
|
2660
2811
|
const record = recordSchema.parse(raw);
|
|
2661
2812
|
const { digest: stored, ...payload } = record;
|
|
2662
|
-
if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || event.
|
|
2813
|
+
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
2814
|
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2664
2815
|
}
|
|
2665
2816
|
return record;
|
|
2666
2817
|
}
|
|
2667
|
-
function
|
|
2818
|
+
function evidenceFingerprint(evidence, reason) {
|
|
2819
|
+
return digest([evidence.scope, evidence.tree, ...reason === "no-dependency-claims" ? [] : [evidence.lastScopeCommit], evidence.paths]);
|
|
2820
|
+
}
|
|
2821
|
+
function scopes(finding, reason) {
|
|
2668
2822
|
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2669
2823
|
if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
2824
|
+
if (reason === "no-dependency-claims" && e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`];
|
|
2670
2825
|
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
2671
2826
|
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
2672
2827
|
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
@@ -2677,9 +2832,9 @@ function scopes(finding) {
|
|
|
2677
2832
|
return `:(literal)${file}`;
|
|
2678
2833
|
}).sort();
|
|
2679
2834
|
}
|
|
2680
|
-
async function reviewEvidence(root, finding) {
|
|
2835
|
+
async function reviewEvidence(root, finding, reason) {
|
|
2681
2836
|
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
2682
|
-
const scope = scopes(finding);
|
|
2837
|
+
const scope = scopes(finding, reason);
|
|
2683
2838
|
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2684
2839
|
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2685
2840
|
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
@@ -2705,7 +2860,7 @@ async function reviewEvidence(root, finding) {
|
|
|
2705
2860
|
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2706
2861
|
}
|
|
2707
2862
|
}
|
|
2708
|
-
const fingerprint =
|
|
2863
|
+
const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason);
|
|
2709
2864
|
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
2710
2865
|
return {
|
|
2711
2866
|
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
@@ -2752,7 +2907,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2752
2907
|
const record = await readRecord(root, id);
|
|
2753
2908
|
if (!record) return null;
|
|
2754
2909
|
const event = record.events.at(-1);
|
|
2755
|
-
const current = await reviewEvidence(root, event.finding);
|
|
2910
|
+
const current = await reviewEvidence(root, event.finding, event.reasonCode);
|
|
2756
2911
|
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
2757
2912
|
const base = {
|
|
2758
2913
|
id,
|
|
@@ -2762,7 +2917,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2762
2917
|
outcome: event.outcome,
|
|
2763
2918
|
reviewedHead: event.evidence.head
|
|
2764
2919
|
};
|
|
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." };
|
|
2920
|
+
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
2921
|
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
2922
|
} catch (error) {
|
|
2768
2923
|
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -2790,9 +2945,13 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2790
2945
|
decisionId: finding.evidence.decisionId,
|
|
2791
2946
|
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
2947
|
};
|
|
2948
|
+
if (request.action === "dismiss") {
|
|
2949
|
+
if (finding.type !== "deps-changed" || finding.evidence.kind !== "doc-behind-manifests") throw new Error("One-step dismissal is only available for dependency advisories.");
|
|
2950
|
+
if (!request.reasonCode || !request.note) throw new Error("Dismissal requires reasonCode and note; it does not require a reviewer or prepared token.");
|
|
2951
|
+
} else if (request.reasonCode) throw new Error("reasonCode is only used with dismiss.");
|
|
2793
2952
|
const prepare = async () => {
|
|
2794
2953
|
const record = await readRecord(root, request.findingId);
|
|
2795
|
-
const evidence = await reviewEvidence(root, finding);
|
|
2954
|
+
const evidence = await reviewEvidence(root, finding, request.reasonCode);
|
|
2796
2955
|
await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
|
|
2797
2956
|
const token = digest({ baseline: baseline.original, finding, evidence, record });
|
|
2798
2957
|
return { record, evidence, token };
|
|
@@ -2821,10 +2980,10 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2821
2980
|
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
2981
|
};
|
|
2823
2982
|
}
|
|
2824
|
-
if (!request.reviewer || !request.note || !request.reviewToken) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
|
|
2983
|
+
if (request.action !== "dismiss" && (!request.reviewer || !request.note || !request.reviewToken)) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
|
|
2825
2984
|
return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
|
|
2826
2985
|
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.");
|
|
2986
|
+
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
2987
|
if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
|
|
2829
2988
|
if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
|
|
2830
2989
|
const { dirty, documentDigest, ...evidence } = state.evidence;
|
|
@@ -2832,7 +2991,8 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2832
2991
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2833
2992
|
reviewer: request.reviewer,
|
|
2834
2993
|
note: request.note,
|
|
2835
|
-
outcome: request.action,
|
|
2994
|
+
outcome: request.action === "dismiss" ? "dismissed" : request.action,
|
|
2995
|
+
reasonCode: request.reasonCode,
|
|
2836
2996
|
finding,
|
|
2837
2997
|
evidence
|
|
2838
2998
|
});
|
|
@@ -2843,11 +3003,11 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2843
3003
|
status: "recorded",
|
|
2844
3004
|
recordPath: recordPath(request.findingId),
|
|
2845
3005
|
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."
|
|
3006
|
+
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
3007
|
};
|
|
2848
3008
|
});
|
|
2849
3009
|
}
|
|
2850
|
-
var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3010
|
+
var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2851
3011
|
var init_advisory_review = __esm({
|
|
2852
3012
|
"src/audit/advisory-review.ts"() {
|
|
2853
3013
|
"use strict";
|
|
@@ -2873,11 +3033,13 @@ var init_advisory_review = __esm({
|
|
|
2873
3033
|
lastScopeCommit: commitSchema2,
|
|
2874
3034
|
paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
|
|
2875
3035
|
});
|
|
3036
|
+
dismissalReasonSchema = z7.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
2876
3037
|
eventSchema2 = z7.object({
|
|
2877
3038
|
at: z7.string().datetime(),
|
|
2878
|
-
reviewer: z7.string().trim().min(1).max(200),
|
|
3039
|
+
reviewer: z7.string().trim().min(1).max(200).optional(),
|
|
2879
3040
|
note: z7.string().trim().min(1).max(2500),
|
|
2880
|
-
outcome: z7.enum(["addressed", "inapplicable", "deferred"]),
|
|
3041
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
3042
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
2881
3043
|
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2882
3044
|
evidence: evidenceSchema2
|
|
2883
3045
|
});
|
|
@@ -2889,13 +3051,14 @@ var init_advisory_review = __esm({
|
|
|
2889
3051
|
recordPath: z7.string().optional(),
|
|
2890
3052
|
reviewer: z7.string().optional(),
|
|
2891
3053
|
note: z7.string().optional(),
|
|
2892
|
-
outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
3054
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
|
|
2893
3055
|
reviewedHead: z7.string().optional()
|
|
2894
3056
|
});
|
|
2895
3057
|
advisoryReviewRequest = z7.object({
|
|
2896
3058
|
baselinePath: z7.string().min(1),
|
|
2897
3059
|
findingId: hashSchema,
|
|
2898
|
-
action: z7.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
3060
|
+
action: z7.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
3061
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
2899
3062
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2900
3063
|
note: eventSchema2.shape.note.optional(),
|
|
2901
3064
|
reviewToken: hashSchema.optional()
|
|
@@ -2906,12 +3069,12 @@ var init_advisory_review = __esm({
|
|
|
2906
3069
|
|
|
2907
3070
|
// src/audit/audit.ts
|
|
2908
3071
|
import fs12 from "fs/promises";
|
|
2909
|
-
import
|
|
3072
|
+
import path21 from "path";
|
|
2910
3073
|
async function computeAudit(rootDir, options = {}) {
|
|
2911
3074
|
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
2912
3075
|
}
|
|
2913
3076
|
async function auditCurrent(rootDir, options) {
|
|
2914
|
-
const resolvedRoot =
|
|
3077
|
+
const resolvedRoot = path21.resolve(rootDir);
|
|
2915
3078
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2916
3079
|
if (docs.length === 0) return null;
|
|
2917
3080
|
const report = {
|
|
@@ -2945,7 +3108,7 @@ async function auditCurrent(rootDir, options) {
|
|
|
2945
3108
|
}
|
|
2946
3109
|
let decisionsPresent = false;
|
|
2947
3110
|
try {
|
|
2948
|
-
await fs12.access(
|
|
3111
|
+
await fs12.access(path21.join(resolvedRoot, ".mason", "decisions"));
|
|
2949
3112
|
decisionsPresent = true;
|
|
2950
3113
|
} catch {
|
|
2951
3114
|
}
|
|
@@ -2957,13 +3120,21 @@ async function auditCurrent(rootDir, options) {
|
|
|
2957
3120
|
changesSinceDoc,
|
|
2958
3121
|
decisionsPresent
|
|
2959
3122
|
};
|
|
3123
|
+
const policy = (await loadProjectConfig(resolvedRoot)).audit;
|
|
2960
3124
|
const selected = options.checks ?? ALL_CHECKS;
|
|
2961
3125
|
for (const name of ALL_CHECKS) {
|
|
2962
3126
|
if (!selected.includes(name)) continue;
|
|
2963
|
-
const
|
|
3127
|
+
const exclusions = policy?.exclude?.[name] ?? [];
|
|
3128
|
+
const included = await includedCheckPaths(resolvedRoot, docs.map((doc) => doc.path), exclusions);
|
|
3129
|
+
const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
|
|
3130
|
+
if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
|
|
3131
|
+
const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
|
|
3132
|
+
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
|
|
2964
3133
|
report.checksRun.push(name);
|
|
2965
3134
|
report.issues.push(...issues);
|
|
2966
|
-
|
|
3135
|
+
const advisoryPaths = await includedCheckPaths(resolvedRoot, [...new Set(advisories.map((f) => f.anchor.doc))], exclusions);
|
|
3136
|
+
report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
|
|
3137
|
+
for (const f of advisories) if (!advisoryPaths.has(f.anchor.doc)) (report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
|
|
2967
3138
|
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
2968
3139
|
report.skippedChecks.push(...skipped);
|
|
2969
3140
|
}
|
|
@@ -2975,6 +3146,8 @@ async function auditCurrent(rootDir, options) {
|
|
|
2975
3146
|
var init_audit = __esm({
|
|
2976
3147
|
"src/audit/audit.ts"() {
|
|
2977
3148
|
"use strict";
|
|
3149
|
+
init_files();
|
|
3150
|
+
init_policy();
|
|
2978
3151
|
init_profile();
|
|
2979
3152
|
init_drift();
|
|
2980
3153
|
init_snapshot();
|
|
@@ -2990,11 +3163,11 @@ init_audit();
|
|
|
2990
3163
|
init_types();
|
|
2991
3164
|
init_repair();
|
|
2992
3165
|
init_findings();
|
|
2993
|
-
import
|
|
3166
|
+
import path23 from "path";
|
|
2994
3167
|
|
|
2995
3168
|
// src/audit/advisory-cli.ts
|
|
2996
3169
|
init_advisory_review();
|
|
2997
|
-
import
|
|
3170
|
+
import path22 from "path";
|
|
2998
3171
|
import { stripVTControlCharacters } from "util";
|
|
2999
3172
|
var USAGE = `Usage: mason audit review --baseline <path> --finding <id> [options]
|
|
3000
3173
|
|
|
@@ -3002,13 +3175,15 @@ Prepare an advisory assessment against retained evidence (read-only by default).
|
|
|
3002
3175
|
--dir <path> Repository root (default: current directory)
|
|
3003
3176
|
--baseline <path> Original repair baseline
|
|
3004
3177
|
--finding <id> findingId from repair verification or automation
|
|
3005
|
-
--outcome <value> Record addressed, inapplicable, or
|
|
3178
|
+
--outcome <value> Record addressed, inapplicable, deferred, or dismiss
|
|
3179
|
+
--reason <code> Dismiss: no-dependency-claims or unrelated-manifest-change
|
|
3006
3180
|
--reviewer <name> Actual authorized reviewer
|
|
3007
3181
|
--note <reason> Assessment reason
|
|
3008
3182
|
--token <token> reviewToken returned by preparation
|
|
3009
3183
|
--json Full evidence and review history
|
|
3010
3184
|
|
|
3011
|
-
|
|
3185
|
+
Assessments require preparation and committed relevant edits. Dismiss requires only
|
|
3186
|
+
--reason and --note, with a committed document; no reviewer or token is needed. Review records
|
|
3012
3187
|
belong in Git under .mason/reviews/advisories/. Deferral remains outstanding.
|
|
3013
3188
|
Decision findings use review_decision; this command never approves decisions.
|
|
3014
3189
|
Exit 0: prepared/recorded; 2: unavailable, conflict, or decision review required.`;
|
|
@@ -3026,21 +3201,22 @@ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), e
|
|
|
3026
3201
|
json = true;
|
|
3027
3202
|
continue;
|
|
3028
3203
|
}
|
|
3029
|
-
if (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
|
|
3204
|
+
if (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token", "--reason"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
|
|
3030
3205
|
const value = args[++i];
|
|
3031
3206
|
if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
|
|
3032
3207
|
flags[flag] = value;
|
|
3033
3208
|
}
|
|
3034
|
-
if (!flags["--outcome"] && ["--reviewer", "--note", "--token"].some((key) => flags[key])) throw new Error("Use --outcome to record an assessment.");
|
|
3209
|
+
if (!flags["--outcome"] && ["--reviewer", "--note", "--token", "--reason"].some((key) => flags[key])) throw new Error("Use --outcome to record an assessment.");
|
|
3035
3210
|
const input = {
|
|
3036
3211
|
baselinePath: flags["--baseline"],
|
|
3037
3212
|
findingId: flags["--finding"],
|
|
3038
3213
|
action: flags["--outcome"] ?? "prepare",
|
|
3039
3214
|
reviewer: flags["--reviewer"],
|
|
3040
3215
|
note: flags["--note"],
|
|
3041
|
-
reviewToken: flags["--token"]
|
|
3216
|
+
reviewToken: flags["--token"],
|
|
3217
|
+
reasonCode: flags["--reason"]
|
|
3042
3218
|
};
|
|
3043
|
-
const result = await reviewAdvisory(
|
|
3219
|
+
const result = await reviewAdvisory(path22.resolve(flags["--dir"] ?? process.cwd()), input);
|
|
3044
3220
|
if (json) io.out(JSON.stringify(result, null, 2));
|
|
3045
3221
|
else {
|
|
3046
3222
|
const lines = [`Advisory review: ${result.status}.`];
|
|
@@ -3152,8 +3328,8 @@ function issueLine(issue) {
|
|
|
3152
3328
|
function formatAuditSummary(report) {
|
|
3153
3329
|
const lines = [];
|
|
3154
3330
|
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)));
|
|
3331
|
+
const open = report.advisories.filter((finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding)));
|
|
3332
|
+
const suppressed = (report.suppressedAdvisories ?? []).filter((finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding)));
|
|
3157
3333
|
const reviewCount = open.length + suppressed.length;
|
|
3158
3334
|
for (const doc of report.docs) {
|
|
3159
3335
|
const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
|
|
@@ -3171,6 +3347,9 @@ function formatAuditSummary(report) {
|
|
|
3171
3347
|
for (const issue of report.issues) {
|
|
3172
3348
|
if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
|
|
3173
3349
|
}
|
|
3350
|
+
const information = [...report.advisories, ...report.suppressedAdvisories ?? []].filter((f) => f.resolution === "informational");
|
|
3351
|
+
if (information.length) lines.push(`${information.length} informational dependency notices; no review required (see --json).`);
|
|
3352
|
+
if (report.excludedChecks?.length) lines.push(`${report.excludedChecks.length} document/check pairs excluded by project policy (see --json).`);
|
|
3174
3353
|
if (open.length > 0) {
|
|
3175
3354
|
lines.push("Advisories (do not affect the exit code):");
|
|
3176
3355
|
for (const advisory of open) {
|
|
@@ -3224,7 +3403,7 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
3224
3403
|
"- 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
3404
|
);
|
|
3226
3405
|
lines.push(
|
|
3227
|
-
"-
|
|
3406
|
+
"- 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
3407
|
);
|
|
3229
3408
|
lines.push("");
|
|
3230
3409
|
lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
|
|
@@ -3273,7 +3452,7 @@ async function runAuditCli(argv, io = {
|
|
|
3273
3452
|
io.out(USAGE2);
|
|
3274
3453
|
return 0;
|
|
3275
3454
|
}
|
|
3276
|
-
const rootDir =
|
|
3455
|
+
const rootDir = path23.resolve(args.dir);
|
|
3277
3456
|
if (args.baseline || args.prepareRepair) {
|
|
3278
3457
|
try {
|
|
3279
3458
|
if (args.baseline) {
|