mason-context 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/dist/mason-audit.js +449 -251
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +522 -270
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +51 -0
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +1 -0
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +1486 -664
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +51 -0
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +337 -0
- package/dist/mason-runtime.js.map +1 -0
- package/dist/mason.js +1377 -485
- package/dist/mason.js.map +1 -1
- package/package.json +2 -1
package/dist/mason-audit.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
if (!import.meta.url.endsWith("/mason-runtime.js")) await import("./mason-runtime.js").then(m => m.registerRuntime());
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
5
|
var __esm = (fn, res) => function __init() {
|
|
@@ -84,6 +85,26 @@ var init_paths = __esm({
|
|
|
84
85
|
}
|
|
85
86
|
});
|
|
86
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
|
+
|
|
87
108
|
// src/utils/files.ts
|
|
88
109
|
import fs from "fs/promises";
|
|
89
110
|
import { constants } from "fs";
|
|
@@ -123,6 +144,27 @@ async function loadProjectConfig(root) {
|
|
|
123
144
|
}
|
|
124
145
|
config[key] = value[key];
|
|
125
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
|
+
}
|
|
126
168
|
return config;
|
|
127
169
|
} catch (error) {
|
|
128
170
|
if (error.code === "ENOENT") return {};
|
|
@@ -234,13 +276,192 @@ var init_storage = __esm({
|
|
|
234
276
|
}
|
|
235
277
|
});
|
|
236
278
|
|
|
237
|
-
// 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
|
|
238
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";
|
|
239
460
|
import os from "os";
|
|
240
461
|
async function lockDiagnostic(file) {
|
|
241
462
|
let reason;
|
|
242
463
|
try {
|
|
243
|
-
const owner = JSON.parse(await
|
|
464
|
+
const owner = JSON.parse(await fs4.readFile(file, "utf8"));
|
|
244
465
|
if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
245
466
|
reason = "Lock owner data is incomplete or malformed.";
|
|
246
467
|
} else if (owner.host !== os.hostname()) {
|
|
@@ -256,7 +477,7 @@ async function lockDiagnostic(file) {
|
|
|
256
477
|
} catch (error) {
|
|
257
478
|
reason = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
|
|
258
479
|
}
|
|
259
|
-
if (await
|
|
480
|
+
if (await fs4.lstat(file + ".reclaim").then(() => true, () => false)) {
|
|
260
481
|
reason += " A recovery guard also needs inspection: " + file + ".reclaim.";
|
|
261
482
|
}
|
|
262
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.";
|
|
@@ -267,21 +488,21 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
|
|
|
267
488
|
let handle;
|
|
268
489
|
while (!handle) {
|
|
269
490
|
try {
|
|
270
|
-
handle = await
|
|
491
|
+
handle = await fs4.open(file, "wx", 384);
|
|
271
492
|
try {
|
|
272
493
|
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
273
494
|
} catch (error) {
|
|
274
495
|
await handle.close().catch(() => {
|
|
275
496
|
});
|
|
276
497
|
handle = void 0;
|
|
277
|
-
await
|
|
498
|
+
await fs4.rm(file, { force: true }).catch(() => {
|
|
278
499
|
});
|
|
279
500
|
throw error;
|
|
280
501
|
}
|
|
281
502
|
} catch (error) {
|
|
282
503
|
if (error.code !== "EEXIST") throw error;
|
|
283
504
|
try {
|
|
284
|
-
const owner = JSON.parse(await
|
|
505
|
+
const owner = JSON.parse(await fs4.readFile(file, "utf8"));
|
|
285
506
|
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
286
507
|
try {
|
|
287
508
|
process.kill(owner.pid, 0);
|
|
@@ -290,13 +511,13 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
|
|
|
290
511
|
const reclaim = file + ".reclaim";
|
|
291
512
|
let guard;
|
|
292
513
|
try {
|
|
293
|
-
guard = await
|
|
294
|
-
const current = JSON.parse(await
|
|
295
|
-
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);
|
|
296
517
|
} finally {
|
|
297
518
|
if (guard) {
|
|
298
519
|
await guard.close();
|
|
299
|
-
await
|
|
520
|
+
await fs4.rm(reclaim, { force: true });
|
|
300
521
|
}
|
|
301
522
|
}
|
|
302
523
|
}
|
|
@@ -312,7 +533,7 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
|
|
|
312
533
|
return await run();
|
|
313
534
|
} finally {
|
|
314
535
|
await handle.close();
|
|
315
|
-
await
|
|
536
|
+
await fs4.unlink(file);
|
|
316
537
|
}
|
|
317
538
|
}
|
|
318
539
|
var init_store_lock = __esm({
|
|
@@ -331,7 +552,7 @@ var init_lock = __esm({
|
|
|
331
552
|
});
|
|
332
553
|
|
|
333
554
|
// src/test-map.ts
|
|
334
|
-
import
|
|
555
|
+
import path6 from "path";
|
|
335
556
|
var init_test_map = __esm({
|
|
336
557
|
"src/test-map.ts"() {
|
|
337
558
|
"use strict";
|
|
@@ -340,7 +561,7 @@ var init_test_map = __esm({
|
|
|
340
561
|
});
|
|
341
562
|
|
|
342
563
|
// src/snapshot/snapshot.ts
|
|
343
|
-
import
|
|
564
|
+
import path7 from "path";
|
|
344
565
|
import { z } from "zod";
|
|
345
566
|
async function getCurrentGitHash(rootDir) {
|
|
346
567
|
try {
|
|
@@ -392,8 +613,8 @@ var init_snapshot = __esm({
|
|
|
392
613
|
});
|
|
393
614
|
|
|
394
615
|
// src/drift/drift.ts
|
|
395
|
-
import
|
|
396
|
-
import
|
|
616
|
+
import fs5 from "fs/promises";
|
|
617
|
+
import path8 from "path";
|
|
397
618
|
function parseChanges(output) {
|
|
398
619
|
const fields = output.split("\0");
|
|
399
620
|
const changes = [];
|
|
@@ -442,7 +663,7 @@ var init_drift = __esm({
|
|
|
442
663
|
});
|
|
443
664
|
|
|
444
665
|
// src/audit/commands.ts
|
|
445
|
-
import
|
|
666
|
+
import path9 from "path";
|
|
446
667
|
function splitCommands(line) {
|
|
447
668
|
const parts = [];
|
|
448
669
|
let quote = "", start = 0;
|
|
@@ -478,7 +699,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
478
699
|
const cd = text2.match(/^cd\s+(.+)$/);
|
|
479
700
|
if (cd) {
|
|
480
701
|
const directory = literal(cd[1]);
|
|
481
|
-
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);
|
|
482
703
|
continue;
|
|
483
704
|
}
|
|
484
705
|
for (const match of text2.matchAll(COMMAND)) {
|
|
@@ -489,7 +710,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
489
710
|
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
490
711
|
if (selector) {
|
|
491
712
|
const selected = literal(selector[1]);
|
|
492
|
-
directory = selected === null || cwd === null ? null :
|
|
713
|
+
directory = selected === null || cwd === null ? null : path9.posix.join(cwd ?? ".", selected);
|
|
493
714
|
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
494
715
|
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
495
716
|
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
@@ -519,7 +740,7 @@ var init_commands = __esm({
|
|
|
519
740
|
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
520
741
|
literal = (value) => {
|
|
521
742
|
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
522
|
-
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) ||
|
|
743
|
+
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path9.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
523
744
|
};
|
|
524
745
|
}
|
|
525
746
|
});
|
|
@@ -824,49 +1045,6 @@ var init_claims = __esm({
|
|
|
824
1045
|
}
|
|
825
1046
|
});
|
|
826
1047
|
|
|
827
|
-
// src/audit/inspection.ts
|
|
828
|
-
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
829
|
-
import path8 from "path";
|
|
830
|
-
function inspectionRead(root, key, read) {
|
|
831
|
-
const inspection = inspections.getStore();
|
|
832
|
-
if (!inspection || inspection.root !== path8.resolve(root)) return read();
|
|
833
|
-
if (!inspection.reads.has(key)) {
|
|
834
|
-
const pending = read();
|
|
835
|
-
inspection.reads.set(key, pending);
|
|
836
|
-
void pending.catch(() => {
|
|
837
|
-
if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
return inspection.reads.get(key);
|
|
841
|
-
}
|
|
842
|
-
function inspectionGit(args, options) {
|
|
843
|
-
return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
|
|
844
|
-
}
|
|
845
|
-
async function auditGitPaths(root, kind) {
|
|
846
|
-
const shared = inspections.getStore()?.root === path8.resolve(root);
|
|
847
|
-
const specs = shared ? [...docSpecs, ...sourceSpecs] : kind === "documents" ? docSpecs : sourceSpecs;
|
|
848
|
-
const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
|
|
849
|
-
const { stdout } = await inspectionGit(
|
|
850
|
-
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
|
|
851
|
-
{ cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
|
|
852
|
-
);
|
|
853
|
-
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("."))));
|
|
854
|
-
if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
|
|
855
|
-
return paths.sort();
|
|
856
|
-
}
|
|
857
|
-
var inspections, sourceSpecs, docSpecs, sourceSuffixes;
|
|
858
|
-
var init_inspection = __esm({
|
|
859
|
-
"src/audit/inspection.ts"() {
|
|
860
|
-
"use strict";
|
|
861
|
-
init_git_read();
|
|
862
|
-
init_files();
|
|
863
|
-
inspections = new AsyncLocalStorage2();
|
|
864
|
-
sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
|
|
865
|
-
docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
|
|
866
|
-
sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
|
|
867
|
-
}
|
|
868
|
-
});
|
|
869
|
-
|
|
870
1048
|
// src/audit/git.ts
|
|
871
1049
|
function parseCommitLine(line) {
|
|
872
1050
|
const parts = line.split(" ");
|
|
@@ -952,120 +1130,10 @@ var init_git = __esm({
|
|
|
952
1130
|
}
|
|
953
1131
|
});
|
|
954
1132
|
|
|
955
|
-
// src/audit/inputs.ts
|
|
956
|
-
import fs5 from "fs/promises";
|
|
957
|
-
import * as nativeFs from "fs";
|
|
958
|
-
import path9 from "path";
|
|
959
|
-
import fg2 from "fast-glob";
|
|
960
|
-
async function auditInputPath(root, file) {
|
|
961
|
-
if (file === ".") return fs5.realpath(root);
|
|
962
|
-
try {
|
|
963
|
-
return await storePath(root, file);
|
|
964
|
-
} catch (error) {
|
|
965
|
-
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
966
|
-
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
967
|
-
}
|
|
968
|
-
throw error;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
async function readAuditInput(root, file) {
|
|
972
|
-
try {
|
|
973
|
-
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
974
|
-
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
975
|
-
return value;
|
|
976
|
-
} catch (error) {
|
|
977
|
-
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
|
|
978
|
-
throw error;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
async function auditGlob(root, patterns, options) {
|
|
982
|
-
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
983
|
-
const directories = /* @__PURE__ */ new Set();
|
|
984
|
-
const readdir2 = ((directory, opts, callback) => {
|
|
985
|
-
const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
|
|
986
|
-
directories.add(relative);
|
|
987
|
-
if (directories.size > MAX_INPUTS) {
|
|
988
|
-
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
auditInputPath(root, relative).then(() => {
|
|
992
|
-
nativeFs.readdir(directory, opts, callback);
|
|
993
|
-
}, callback);
|
|
994
|
-
});
|
|
995
|
-
const lstat2 = ((file, callback) => {
|
|
996
|
-
const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
|
|
997
|
-
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
998
|
-
});
|
|
999
|
-
const stream = fg2.stream(patterns, {
|
|
1000
|
-
cwd: root,
|
|
1001
|
-
ignore: options.ignore,
|
|
1002
|
-
onlyFiles: false,
|
|
1003
|
-
onlyDirectories: options.onlyDirectories,
|
|
1004
|
-
objectMode: true,
|
|
1005
|
-
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
1006
|
-
fs: { readdir: readdir2, lstat: lstat2 },
|
|
1007
|
-
concurrency: 16
|
|
1008
|
-
});
|
|
1009
|
-
const matches = [];
|
|
1010
|
-
try {
|
|
1011
|
-
for await (const value of stream) {
|
|
1012
|
-
const entry = value;
|
|
1013
|
-
const file = entry.path;
|
|
1014
|
-
if (options.select && !options.select(file)) continue;
|
|
1015
|
-
await auditInputPath(root, file);
|
|
1016
|
-
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
1017
|
-
matches.push(file);
|
|
1018
|
-
if (matches.length > MAX_INPUTS) {
|
|
1019
|
-
const largest = [...matches.reduce((counts, item) => {
|
|
1020
|
-
const dir = item.split("/")[0];
|
|
1021
|
-
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
1022
|
-
return counts;
|
|
1023
|
-
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
1024
|
-
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.`);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
} finally {
|
|
1028
|
-
stream.destroy();
|
|
1029
|
-
}
|
|
1030
|
-
return matches.sort();
|
|
1031
|
-
}
|
|
1032
|
-
async function gitSourcePaths(root) {
|
|
1033
|
-
return auditGitPaths(root, "sources");
|
|
1034
|
-
}
|
|
1035
|
-
async function gitIgnoredPaths(root, files) {
|
|
1036
|
-
if (!files.length) return /* @__PURE__ */ new Set();
|
|
1037
|
-
const ignored = /* @__PURE__ */ new Set();
|
|
1038
|
-
for (let offset = 0; offset < files.length; offset += 1e3) {
|
|
1039
|
-
try {
|
|
1040
|
-
const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
|
|
1041
|
-
cwd: root,
|
|
1042
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
1043
|
-
timeout: 1e4,
|
|
1044
|
-
input: files.slice(offset, offset + 1e3).join("\0") + "\0"
|
|
1045
|
-
});
|
|
1046
|
-
for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
|
|
1047
|
-
} catch (error) {
|
|
1048
|
-
if (error.code !== 1) throw error;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
return ignored;
|
|
1052
|
-
}
|
|
1053
|
-
var MAX_INPUTS;
|
|
1054
|
-
var init_inputs = __esm({
|
|
1055
|
-
"src/audit/inputs.ts"() {
|
|
1056
|
-
"use strict";
|
|
1057
|
-
init_files();
|
|
1058
|
-
init_storage();
|
|
1059
|
-
init_git_read();
|
|
1060
|
-
init_inspection();
|
|
1061
|
-
MAX_INPUTS = 1e5;
|
|
1062
|
-
}
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
1133
|
// src/audit/docs.ts
|
|
1066
1134
|
import fs6 from "fs/promises";
|
|
1067
1135
|
import path10 from "path";
|
|
1068
|
-
import
|
|
1136
|
+
import fg5 from "fast-glob";
|
|
1069
1137
|
async function dirtyDocs(resolvedRoot, files) {
|
|
1070
1138
|
if (!files.length) return /* @__PURE__ */ new Set();
|
|
1071
1139
|
try {
|
|
@@ -1117,7 +1185,7 @@ async function discoverDocPaths(root) {
|
|
|
1117
1185
|
const paths = [];
|
|
1118
1186
|
const unique = [...new Set(candidates)].sort();
|
|
1119
1187
|
for (let offset = 0; offset < unique.length; offset += 256) {
|
|
1120
|
-
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)), {
|
|
1121
1189
|
ignore: excluded,
|
|
1122
1190
|
followSymbolicLinks: false,
|
|
1123
1191
|
label: "Documentation discovery"
|
|
@@ -1147,6 +1215,7 @@ async function localDocPaths(root) {
|
|
|
1147
1215
|
return paths;
|
|
1148
1216
|
}
|
|
1149
1217
|
function documentScope(file) {
|
|
1218
|
+
if (!/^(readme|agents|claude)\.md$/i.test(path10.posix.basename(file))) return ".";
|
|
1150
1219
|
const parent = path10.posix.dirname(file);
|
|
1151
1220
|
return path10.posix.basename(parent).toLowerCase() === ".claude" ? path10.posix.dirname(parent) : parent;
|
|
1152
1221
|
}
|
|
@@ -1168,7 +1237,7 @@ async function readDocs(resolvedRoot) {
|
|
|
1168
1237
|
path: candidate,
|
|
1169
1238
|
content,
|
|
1170
1239
|
scope: documentScope(candidate),
|
|
1171
|
-
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",
|
|
1172
1241
|
lineCount: content.split("\n").length,
|
|
1173
1242
|
lastCommit,
|
|
1174
1243
|
dirty: dirty.has(candidate),
|
|
@@ -1197,22 +1266,6 @@ var init_docs = __esm({
|
|
|
1197
1266
|
}
|
|
1198
1267
|
});
|
|
1199
1268
|
|
|
1200
|
-
// src/audit/types.ts
|
|
1201
|
-
var ALL_CHECKS;
|
|
1202
|
-
var init_types = __esm({
|
|
1203
|
-
"src/audit/types.ts"() {
|
|
1204
|
-
"use strict";
|
|
1205
|
-
ALL_CHECKS = [
|
|
1206
|
-
"deleted-reference",
|
|
1207
|
-
"new-module",
|
|
1208
|
-
"stale-count",
|
|
1209
|
-
"dead-command",
|
|
1210
|
-
"deps-changed",
|
|
1211
|
-
"decision-anchor-drift"
|
|
1212
|
-
];
|
|
1213
|
-
}
|
|
1214
|
-
});
|
|
1215
|
-
|
|
1216
1269
|
// src/audit/scope.ts
|
|
1217
1270
|
import fs7 from "fs/promises";
|
|
1218
1271
|
import path11 from "path";
|
|
@@ -1323,7 +1376,7 @@ var init_deleted_reference = __esm({
|
|
|
1323
1376
|
});
|
|
1324
1377
|
|
|
1325
1378
|
// src/audit/checks/new-module.ts
|
|
1326
|
-
import
|
|
1379
|
+
import fg6 from "fast-glob";
|
|
1327
1380
|
import fs8 from "fs/promises";
|
|
1328
1381
|
import path13 from "path";
|
|
1329
1382
|
function escapeRegExp(text2) {
|
|
@@ -1385,7 +1438,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1385
1438
|
const result = /* @__PURE__ */ new Map();
|
|
1386
1439
|
for (const dir of parents) {
|
|
1387
1440
|
const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
|
|
1388
|
-
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${
|
|
1441
|
+
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
|
|
1389
1442
|
ignore,
|
|
1390
1443
|
onlyDirectories: true,
|
|
1391
1444
|
label: "Module directory discovery",
|
|
@@ -1398,7 +1451,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1398
1451
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1399
1452
|
let count3 = 0;
|
|
1400
1453
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1401
|
-
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)), {
|
|
1402
1455
|
ignore,
|
|
1403
1456
|
followSymbolicLinks: false,
|
|
1404
1457
|
label: `Source discovery in ${dir}`
|
|
@@ -1733,6 +1786,87 @@ var init_dead_command = __esm({
|
|
|
1733
1786
|
}
|
|
1734
1787
|
});
|
|
1735
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
|
+
|
|
1736
1870
|
// src/audit/release-metadata.ts
|
|
1737
1871
|
async function git(root, args) {
|
|
1738
1872
|
return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
@@ -1789,6 +1923,10 @@ var init_release_metadata = __esm({
|
|
|
1789
1923
|
});
|
|
1790
1924
|
|
|
1791
1925
|
// src/audit/checks/deps-changed.ts
|
|
1926
|
+
function hasDependencyContent(content) {
|
|
1927
|
+
const text2 = content.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
|
|
1928
|
+
return /\b(?:dependenc(?:y|ies)|librar(?:y|ies)|frameworks?|tech(?:nology)? stack|prerequisites|requirements)\b/i.test(text2) || /\b(?:package\.json|build\.gradle(?:\.kts)?|settings\.gradle(?:\.kts)?|libs\.versions\.toml|Cargo\.toml|go\.mod|pyproject\.toml|requirements\.txt|Gemfile|composer\.json)\b/i.test(text2) || /\b(?:node(?:\.js)?|npm|pnpm|yarn|bun|deno|python|ruby|rust|go|java|jdk|kotlin|gradle|swift|php|react|vue|angular|compose|ktor|room)\s*(?:version\s*)?[`*:=>~^v\s-]*\d+(?:\.\d+)*\b/i.test(text2) || /\b(?:npm|pnpm|yarn|pip3?|cargo|gem|composer)\s+(?:install|add|require)\b/i.test(text2);
|
|
1929
|
+
}
|
|
1792
1930
|
function manifestPathspecs(doc) {
|
|
1793
1931
|
const scope = documentScope(doc);
|
|
1794
1932
|
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
@@ -1808,12 +1946,17 @@ async function checkDepsChanged(ctx) {
|
|
|
1808
1946
|
});
|
|
1809
1947
|
continue;
|
|
1810
1948
|
}
|
|
1949
|
+
let content = doc.content;
|
|
1811
1950
|
if (doc.dirty) {
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1951
|
+
try {
|
|
1952
|
+
content = (await inspectionGit(
|
|
1953
|
+
["show", `${doc.lastCommit.hash}:${doc.path}`],
|
|
1954
|
+
{ cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
|
|
1955
|
+
)).stdout;
|
|
1956
|
+
} catch {
|
|
1957
|
+
result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1817
1960
|
}
|
|
1818
1961
|
const range = await commitsTouchingSince(
|
|
1819
1962
|
ctx.root,
|
|
@@ -1838,16 +1981,32 @@ async function checkDepsChanged(ctx) {
|
|
|
1838
1981
|
range.commits = relevant;
|
|
1839
1982
|
range.total = relevant.length;
|
|
1840
1983
|
if (range.total === 0) continue;
|
|
1841
|
-
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
|
+
});
|
|
1842
1998
|
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
1843
1999
|
type: "deps-changed",
|
|
1844
|
-
|
|
1845
|
-
|
|
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 },
|
|
1846
2003
|
evidence: {
|
|
1847
2004
|
kind: "doc-behind-manifests",
|
|
1848
2005
|
docLastCommit: doc.lastCommit,
|
|
1849
2006
|
manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
|
|
1850
|
-
totalCommits: range.total
|
|
2007
|
+
totalCommits: range.total,
|
|
2008
|
+
matches: relevance.matches,
|
|
2009
|
+
matchingIncomplete: relevance.incomplete
|
|
1851
2010
|
}
|
|
1852
2011
|
});
|
|
1853
2012
|
}
|
|
@@ -1857,9 +2016,11 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
|
1857
2016
|
var init_deps_changed = __esm({
|
|
1858
2017
|
"src/audit/checks/deps-changed.ts"() {
|
|
1859
2018
|
"use strict";
|
|
2019
|
+
init_dependency_claims();
|
|
1860
2020
|
init_docs();
|
|
1861
2021
|
init_release_metadata();
|
|
1862
2022
|
init_git();
|
|
2023
|
+
init_inspection();
|
|
1863
2024
|
init_checks();
|
|
1864
2025
|
MANIFEST_COMMITS_CAP = 10;
|
|
1865
2026
|
MANIFEST_PATHSPECS = [
|
|
@@ -1880,7 +2041,7 @@ var init_deps_changed = __esm({
|
|
|
1880
2041
|
});
|
|
1881
2042
|
|
|
1882
2043
|
// src/context/lexical.ts
|
|
1883
|
-
import
|
|
2044
|
+
import path17 from "path";
|
|
1884
2045
|
var init_lexical = __esm({
|
|
1885
2046
|
"src/context/lexical.ts"() {
|
|
1886
2047
|
"use strict";
|
|
@@ -2041,7 +2202,7 @@ var init_provenance = __esm({
|
|
|
2041
2202
|
|
|
2042
2203
|
// src/decisions/decisions.ts
|
|
2043
2204
|
import fs9 from "fs/promises";
|
|
2044
|
-
import
|
|
2205
|
+
import path18 from "path";
|
|
2045
2206
|
import { createHash } from "crypto";
|
|
2046
2207
|
import { z as z3 } from "zod";
|
|
2047
2208
|
async function loadDecisionStore(rootDir) {
|
|
@@ -2092,9 +2253,9 @@ var init_decisions = __esm({
|
|
|
2092
2253
|
});
|
|
2093
2254
|
|
|
2094
2255
|
// src/decisions/drift.ts
|
|
2095
|
-
import
|
|
2256
|
+
import path19 from "path";
|
|
2096
2257
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
2097
|
-
const resolvedRoot =
|
|
2258
|
+
const resolvedRoot = path19.resolve(rootDir);
|
|
2098
2259
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
2099
2260
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
2100
2261
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -2290,7 +2451,9 @@ var init_findings = __esm({
|
|
|
2290
2451
|
kind: z4.literal("doc-behind-manifests"),
|
|
2291
2452
|
docLastCommit: commitSchema,
|
|
2292
2453
|
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
2293
|
-
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()
|
|
2294
2457
|
}),
|
|
2295
2458
|
z4.object({
|
|
2296
2459
|
kind: z4.literal("decision-anchor"),
|
|
@@ -2306,7 +2469,7 @@ var init_findings = __esm({
|
|
|
2306
2469
|
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2307
2470
|
confidence: z4.enum(["certain", "likely"])
|
|
2308
2471
|
});
|
|
2309
|
-
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.
|
|
2472
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.enum(["recheck", "informational"]).optional() });
|
|
2310
2473
|
checkResultSchema = z4.object({
|
|
2311
2474
|
issues: z4.array(issueSchema),
|
|
2312
2475
|
advisories: z4.array(advisorySchema),
|
|
@@ -2368,7 +2531,7 @@ __export(repair_exports, {
|
|
|
2368
2531
|
withRepairSession: () => withRepairSession
|
|
2369
2532
|
});
|
|
2370
2533
|
import fs10 from "fs/promises";
|
|
2371
|
-
import
|
|
2534
|
+
import path20 from "path";
|
|
2372
2535
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2373
2536
|
import { z as z6 } from "zod";
|
|
2374
2537
|
function recheckable(finding) {
|
|
@@ -2389,7 +2552,7 @@ async function docState(root) {
|
|
|
2389
2552
|
docs.push([doc, null]);
|
|
2390
2553
|
}
|
|
2391
2554
|
}
|
|
2392
|
-
return digest(docs);
|
|
2555
|
+
return digest([docs, (await loadProjectConfig(root)).audit]);
|
|
2393
2556
|
}
|
|
2394
2557
|
async function stableAudit(root, checks, options = {}) {
|
|
2395
2558
|
return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
|
|
@@ -2458,8 +2621,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
|
|
|
2458
2621
|
}
|
|
2459
2622
|
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
2460
2623
|
const root = await fs10.realpath(rootDir);
|
|
2461
|
-
const declaredRoot =
|
|
2462
|
-
const relative =
|
|
2624
|
+
const declaredRoot = path20.resolve(rootDir);
|
|
2625
|
+
const relative = path20.isAbsolute(baselinePath) ? path20.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2463
2626
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
2464
2627
|
const { digest: savedDigest, ...payload } = stored;
|
|
2465
2628
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
@@ -2508,6 +2671,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2508
2671
|
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
2509
2672
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
2510
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
|
+
}
|
|
2511
2677
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
2512
2678
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
2513
2679
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
@@ -2518,11 +2684,14 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2518
2684
|
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
2519
2685
|
return { ...base, review, status: "review-required", reason: review.reason };
|
|
2520
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
|
+
}
|
|
2521
2690
|
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
2522
2691
|
return {
|
|
2523
2692
|
...base,
|
|
2524
2693
|
status: "review-required",
|
|
2525
|
-
reason: "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
|
|
2694
|
+
reason: (now ? "The current check still reports this advisory. " : finding.type === "deps-changed" ? "The current check no longer reports this condition; the historical advisory remains awaiting assessment. " : "The current check no longer reports the original finding; retained evidence still requires assessment. ") + "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
|
|
2526
2695
|
};
|
|
2527
2696
|
}
|
|
2528
2697
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
@@ -2546,7 +2715,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2546
2715
|
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
2547
2716
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
2548
2717
|
for (const f of findings) counts[f.status]++;
|
|
2549
|
-
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)));
|
|
2550
2719
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
2551
2720
|
return {
|
|
2552
2721
|
version: 1,
|
|
@@ -2569,7 +2738,7 @@ function repairExitCode(report) {
|
|
|
2569
2738
|
function formatRepairSummary(report) {
|
|
2570
2739
|
return [
|
|
2571
2740
|
"Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
|
|
2572
|
-
...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),
|
|
2573
2742
|
...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
|
|
2574
2743
|
...report.diagnostics.map((d) => " [unverified] " + d),
|
|
2575
2744
|
...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
|
|
@@ -2602,13 +2771,14 @@ var init_repair = __esm({
|
|
|
2602
2771
|
docs: z6.array(z6.object({
|
|
2603
2772
|
path: z6.string().refine((value) => normalizeRepoPath(value) === value),
|
|
2604
2773
|
scope: z6.string().optional(),
|
|
2605
|
-
kind: z6.enum(["instructions", "readme"]).optional(),
|
|
2774
|
+
kind: z6.enum(["instructions", "readme", "guide"]).optional(),
|
|
2606
2775
|
lastCommit: commitSchema.nullable(),
|
|
2607
2776
|
dirty: z6.boolean(),
|
|
2608
2777
|
lineCount: count2
|
|
2609
2778
|
})).nonempty(),
|
|
2610
2779
|
decisionsChecked: z6.boolean(),
|
|
2611
2780
|
clean: z6.boolean(),
|
|
2781
|
+
excludedChecks: z6.array(z6.object({ check: checkSchema, doc: z6.string() })).optional(),
|
|
2612
2782
|
issues: z6.array(issueSchema),
|
|
2613
2783
|
advisories: z6.array(advisorySchema),
|
|
2614
2784
|
suppressedAdvisories: z6.array(advisorySchema).optional(),
|
|
@@ -2640,14 +2810,18 @@ async function readRecord(root, id) {
|
|
|
2640
2810
|
if (raw === null) return null;
|
|
2641
2811
|
const record = recordSchema.parse(raw);
|
|
2642
2812
|
const { digest: stored, ...payload } = record;
|
|
2643
|
-
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))) {
|
|
2644
2814
|
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2645
2815
|
}
|
|
2646
2816
|
return record;
|
|
2647
2817
|
}
|
|
2648
|
-
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) {
|
|
2649
2822
|
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2650
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}`];
|
|
2651
2825
|
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
2652
2826
|
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
2653
2827
|
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
@@ -2658,9 +2832,9 @@ function scopes(finding) {
|
|
|
2658
2832
|
return `:(literal)${file}`;
|
|
2659
2833
|
}).sort();
|
|
2660
2834
|
}
|
|
2661
|
-
async function reviewEvidence(root, finding) {
|
|
2835
|
+
async function reviewEvidence(root, finding, reason) {
|
|
2662
2836
|
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
2663
|
-
const scope = scopes(finding);
|
|
2837
|
+
const scope = scopes(finding, reason);
|
|
2664
2838
|
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2665
2839
|
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2666
2840
|
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
@@ -2686,7 +2860,7 @@ async function reviewEvidence(root, finding) {
|
|
|
2686
2860
|
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2687
2861
|
}
|
|
2688
2862
|
}
|
|
2689
|
-
const fingerprint =
|
|
2863
|
+
const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason);
|
|
2690
2864
|
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
2691
2865
|
return {
|
|
2692
2866
|
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
@@ -2733,7 +2907,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2733
2907
|
const record = await readRecord(root, id);
|
|
2734
2908
|
if (!record) return null;
|
|
2735
2909
|
const event = record.events.at(-1);
|
|
2736
|
-
const current = await reviewEvidence(root, event.finding);
|
|
2910
|
+
const current = await reviewEvidence(root, event.finding, event.reasonCode);
|
|
2737
2911
|
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
2738
2912
|
const base = {
|
|
2739
2913
|
id,
|
|
@@ -2743,7 +2917,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2743
2917
|
outcome: event.outcome,
|
|
2744
2918
|
reviewedHead: event.evidence.head
|
|
2745
2919
|
};
|
|
2746
|
-
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." };
|
|
2747
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." };
|
|
2748
2922
|
} catch (error) {
|
|
2749
2923
|
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -2771,9 +2945,13 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2771
2945
|
decisionId: finding.evidence.decisionId,
|
|
2772
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."
|
|
2773
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.");
|
|
2774
2952
|
const prepare = async () => {
|
|
2775
2953
|
const record = await readRecord(root, request.findingId);
|
|
2776
|
-
const evidence = await reviewEvidence(root, finding);
|
|
2954
|
+
const evidence = await reviewEvidence(root, finding, request.reasonCode);
|
|
2777
2955
|
await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
|
|
2778
2956
|
const token = digest({ baseline: baseline.original, finding, evidence, record });
|
|
2779
2957
|
return { record, evidence, token };
|
|
@@ -2802,10 +2980,10 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2802
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."
|
|
2803
2981
|
};
|
|
2804
2982
|
}
|
|
2805
|
-
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.");
|
|
2806
2984
|
return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
|
|
2807
2985
|
const state = await prepare();
|
|
2808
|
-
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.");
|
|
2809
2987
|
if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
|
|
2810
2988
|
if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
|
|
2811
2989
|
const { dirty, documentDigest, ...evidence } = state.evidence;
|
|
@@ -2813,7 +2991,8 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2813
2991
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2814
2992
|
reviewer: request.reviewer,
|
|
2815
2993
|
note: request.note,
|
|
2816
|
-
outcome: request.action,
|
|
2994
|
+
outcome: request.action === "dismiss" ? "dismissed" : request.action,
|
|
2995
|
+
reasonCode: request.reasonCode,
|
|
2817
2996
|
finding,
|
|
2818
2997
|
evidence
|
|
2819
2998
|
});
|
|
@@ -2824,11 +3003,11 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2824
3003
|
status: "recorded",
|
|
2825
3004
|
recordPath: recordPath(request.findingId),
|
|
2826
3005
|
event,
|
|
2827
|
-
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."
|
|
2828
3007
|
};
|
|
2829
3008
|
});
|
|
2830
3009
|
}
|
|
2831
|
-
var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3010
|
+
var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2832
3011
|
var init_advisory_review = __esm({
|
|
2833
3012
|
"src/audit/advisory-review.ts"() {
|
|
2834
3013
|
"use strict";
|
|
@@ -2854,11 +3033,13 @@ var init_advisory_review = __esm({
|
|
|
2854
3033
|
lastScopeCommit: commitSchema2,
|
|
2855
3034
|
paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
|
|
2856
3035
|
});
|
|
3036
|
+
dismissalReasonSchema = z7.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
2857
3037
|
eventSchema2 = z7.object({
|
|
2858
3038
|
at: z7.string().datetime(),
|
|
2859
|
-
reviewer: z7.string().trim().min(1).max(200),
|
|
3039
|
+
reviewer: z7.string().trim().min(1).max(200).optional(),
|
|
2860
3040
|
note: z7.string().trim().min(1).max(2500),
|
|
2861
|
-
outcome: z7.enum(["addressed", "inapplicable", "deferred"]),
|
|
3041
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
3042
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
2862
3043
|
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2863
3044
|
evidence: evidenceSchema2
|
|
2864
3045
|
});
|
|
@@ -2870,13 +3051,14 @@ var init_advisory_review = __esm({
|
|
|
2870
3051
|
recordPath: z7.string().optional(),
|
|
2871
3052
|
reviewer: z7.string().optional(),
|
|
2872
3053
|
note: z7.string().optional(),
|
|
2873
|
-
outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
3054
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
|
|
2874
3055
|
reviewedHead: z7.string().optional()
|
|
2875
3056
|
});
|
|
2876
3057
|
advisoryReviewRequest = z7.object({
|
|
2877
3058
|
baselinePath: z7.string().min(1),
|
|
2878
3059
|
findingId: hashSchema,
|
|
2879
|
-
action: z7.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
3060
|
+
action: z7.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
3061
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
2880
3062
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2881
3063
|
note: eventSchema2.shape.note.optional(),
|
|
2882
3064
|
reviewToken: hashSchema.optional()
|
|
@@ -2887,12 +3069,12 @@ var init_advisory_review = __esm({
|
|
|
2887
3069
|
|
|
2888
3070
|
// src/audit/audit.ts
|
|
2889
3071
|
import fs12 from "fs/promises";
|
|
2890
|
-
import
|
|
3072
|
+
import path21 from "path";
|
|
2891
3073
|
async function computeAudit(rootDir, options = {}) {
|
|
2892
3074
|
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
2893
3075
|
}
|
|
2894
3076
|
async function auditCurrent(rootDir, options) {
|
|
2895
|
-
const resolvedRoot =
|
|
3077
|
+
const resolvedRoot = path21.resolve(rootDir);
|
|
2896
3078
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2897
3079
|
if (docs.length === 0) return null;
|
|
2898
3080
|
const report = {
|
|
@@ -2926,7 +3108,7 @@ async function auditCurrent(rootDir, options) {
|
|
|
2926
3108
|
}
|
|
2927
3109
|
let decisionsPresent = false;
|
|
2928
3110
|
try {
|
|
2929
|
-
await fs12.access(
|
|
3111
|
+
await fs12.access(path21.join(resolvedRoot, ".mason", "decisions"));
|
|
2930
3112
|
decisionsPresent = true;
|
|
2931
3113
|
} catch {
|
|
2932
3114
|
}
|
|
@@ -2938,13 +3120,21 @@ async function auditCurrent(rootDir, options) {
|
|
|
2938
3120
|
changesSinceDoc,
|
|
2939
3121
|
decisionsPresent
|
|
2940
3122
|
};
|
|
3123
|
+
const policy = (await loadProjectConfig(resolvedRoot)).audit;
|
|
2941
3124
|
const selected = options.checks ?? ALL_CHECKS;
|
|
2942
3125
|
for (const name of ALL_CHECKS) {
|
|
2943
3126
|
if (!selected.includes(name)) continue;
|
|
2944
|
-
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));
|
|
2945
3133
|
report.checksRun.push(name);
|
|
2946
3134
|
report.issues.push(...issues);
|
|
2947
|
-
|
|
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 });
|
|
2948
3138
|
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
2949
3139
|
report.skippedChecks.push(...skipped);
|
|
2950
3140
|
}
|
|
@@ -2956,6 +3146,8 @@ async function auditCurrent(rootDir, options) {
|
|
|
2956
3146
|
var init_audit = __esm({
|
|
2957
3147
|
"src/audit/audit.ts"() {
|
|
2958
3148
|
"use strict";
|
|
3149
|
+
init_files();
|
|
3150
|
+
init_policy();
|
|
2959
3151
|
init_profile();
|
|
2960
3152
|
init_drift();
|
|
2961
3153
|
init_snapshot();
|
|
@@ -2971,11 +3163,11 @@ init_audit();
|
|
|
2971
3163
|
init_types();
|
|
2972
3164
|
init_repair();
|
|
2973
3165
|
init_findings();
|
|
2974
|
-
import
|
|
3166
|
+
import path23 from "path";
|
|
2975
3167
|
|
|
2976
3168
|
// src/audit/advisory-cli.ts
|
|
2977
3169
|
init_advisory_review();
|
|
2978
|
-
import
|
|
3170
|
+
import path22 from "path";
|
|
2979
3171
|
import { stripVTControlCharacters } from "util";
|
|
2980
3172
|
var USAGE = `Usage: mason audit review --baseline <path> --finding <id> [options]
|
|
2981
3173
|
|
|
@@ -2983,13 +3175,15 @@ Prepare an advisory assessment against retained evidence (read-only by default).
|
|
|
2983
3175
|
--dir <path> Repository root (default: current directory)
|
|
2984
3176
|
--baseline <path> Original repair baseline
|
|
2985
3177
|
--finding <id> findingId from repair verification or automation
|
|
2986
|
-
--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
|
|
2987
3180
|
--reviewer <name> Actual authorized reviewer
|
|
2988
3181
|
--note <reason> Assessment reason
|
|
2989
3182
|
--token <token> reviewToken returned by preparation
|
|
2990
3183
|
--json Full evidence and review history
|
|
2991
3184
|
|
|
2992
|
-
|
|
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
|
|
2993
3187
|
belong in Git under .mason/reviews/advisories/. Deferral remains outstanding.
|
|
2994
3188
|
Decision findings use review_decision; this command never approves decisions.
|
|
2995
3189
|
Exit 0: prepared/recorded; 2: unavailable, conflict, or decision review required.`;
|
|
@@ -3007,21 +3201,22 @@ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), e
|
|
|
3007
3201
|
json = true;
|
|
3008
3202
|
continue;
|
|
3009
3203
|
}
|
|
3010
|
-
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);
|
|
3011
3205
|
const value = args[++i];
|
|
3012
3206
|
if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
|
|
3013
3207
|
flags[flag] = value;
|
|
3014
3208
|
}
|
|
3015
|
-
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.");
|
|
3016
3210
|
const input = {
|
|
3017
3211
|
baselinePath: flags["--baseline"],
|
|
3018
3212
|
findingId: flags["--finding"],
|
|
3019
3213
|
action: flags["--outcome"] ?? "prepare",
|
|
3020
3214
|
reviewer: flags["--reviewer"],
|
|
3021
3215
|
note: flags["--note"],
|
|
3022
|
-
reviewToken: flags["--token"]
|
|
3216
|
+
reviewToken: flags["--token"],
|
|
3217
|
+
reasonCode: flags["--reason"]
|
|
3023
3218
|
};
|
|
3024
|
-
const result = await reviewAdvisory(
|
|
3219
|
+
const result = await reviewAdvisory(path22.resolve(flags["--dir"] ?? process.cwd()), input);
|
|
3025
3220
|
if (json) io.out(JSON.stringify(result, null, 2));
|
|
3026
3221
|
else {
|
|
3027
3222
|
const lines = [`Advisory review: ${result.status}.`];
|
|
@@ -3133,8 +3328,8 @@ function issueLine(issue) {
|
|
|
3133
3328
|
function formatAuditSummary(report) {
|
|
3134
3329
|
const lines = [];
|
|
3135
3330
|
const reviewed = new Set(report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
3136
|
-
const open = report.advisories.filter((finding) => !reviewed.has(findingId(finding)));
|
|
3137
|
-
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)));
|
|
3138
3333
|
const reviewCount = open.length + suppressed.length;
|
|
3139
3334
|
for (const doc of report.docs) {
|
|
3140
3335
|
const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
|
|
@@ -3152,6 +3347,9 @@ function formatAuditSummary(report) {
|
|
|
3152
3347
|
for (const issue of report.issues) {
|
|
3153
3348
|
if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
|
|
3154
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).`);
|
|
3155
3353
|
if (open.length > 0) {
|
|
3156
3354
|
lines.push("Advisories (do not affect the exit code):");
|
|
3157
3355
|
for (const advisory of open) {
|
|
@@ -3205,7 +3403,7 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
3205
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."
|
|
3206
3404
|
);
|
|
3207
3405
|
lines.push(
|
|
3208
|
-
"-
|
|
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."
|
|
3209
3407
|
);
|
|
3210
3408
|
lines.push("");
|
|
3211
3409
|
lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
|
|
@@ -3254,7 +3452,7 @@ async function runAuditCli(argv, io = {
|
|
|
3254
3452
|
io.out(USAGE2);
|
|
3255
3453
|
return 0;
|
|
3256
3454
|
}
|
|
3257
|
-
const rootDir =
|
|
3455
|
+
const rootDir = path23.resolve(args.dir);
|
|
3258
3456
|
if (args.baseline || args.prepareRepair) {
|
|
3259
3457
|
try {
|
|
3260
3458
|
if (args.baseline) {
|