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-auto.js
CHANGED
|
@@ -96,6 +96,26 @@ var init_git_read = __esm({
|
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
// src/audit/types.ts
|
|
100
|
+
var types_exports = {};
|
|
101
|
+
__export(types_exports, {
|
|
102
|
+
ALL_CHECKS: () => ALL_CHECKS
|
|
103
|
+
});
|
|
104
|
+
var ALL_CHECKS;
|
|
105
|
+
var init_types = __esm({
|
|
106
|
+
"src/audit/types.ts"() {
|
|
107
|
+
"use strict";
|
|
108
|
+
ALL_CHECKS = [
|
|
109
|
+
"deleted-reference",
|
|
110
|
+
"new-module",
|
|
111
|
+
"stale-count",
|
|
112
|
+
"dead-command",
|
|
113
|
+
"deps-changed",
|
|
114
|
+
"decision-anchor-drift"
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
99
119
|
// src/utils/files.ts
|
|
100
120
|
import fs from "fs/promises";
|
|
101
121
|
import { constants } from "fs";
|
|
@@ -140,6 +160,27 @@ async function loadProjectConfig(root) {
|
|
|
140
160
|
}
|
|
141
161
|
config[key] = value[key];
|
|
142
162
|
}
|
|
163
|
+
if (value.audit !== void 0) {
|
|
164
|
+
const audit = value.audit;
|
|
165
|
+
if (!audit || typeof audit !== "object" || Array.isArray(audit)) throw new Error("Configuration audit must be an object");
|
|
166
|
+
const patterns = (raw2) => {
|
|
167
|
+
if (!Array.isArray(raw2) || raw2.length > 100 || !raw2.every((s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s))) {
|
|
168
|
+
throw new Error("Audit patterns must be at most 100 repository-relative globs without traversal or negation");
|
|
169
|
+
}
|
|
170
|
+
return raw2;
|
|
171
|
+
};
|
|
172
|
+
config.audit = {};
|
|
173
|
+
if (audit.include !== void 0) config.audit.include = patterns(audit.include);
|
|
174
|
+
if (audit.exclude !== void 0) {
|
|
175
|
+
if (!audit.exclude || typeof audit.exclude !== "object" || Array.isArray(audit.exclude)) throw new Error("audit.exclude must map check names to patterns");
|
|
176
|
+
const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
|
|
177
|
+
config.audit.exclude = {};
|
|
178
|
+
for (const [check, globs] of Object.entries(audit.exclude)) {
|
|
179
|
+
if (!ALL_CHECKS2.includes(check)) throw new Error("Unknown audit check: " + check);
|
|
180
|
+
config.audit.exclude[check] = patterns(globs);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
143
184
|
return config;
|
|
144
185
|
} catch (error) {
|
|
145
186
|
if (error.code === "ENOENT") return {};
|
|
@@ -439,8 +480,190 @@ var init_store = __esm({
|
|
|
439
480
|
}
|
|
440
481
|
});
|
|
441
482
|
|
|
442
|
-
// src/audit/
|
|
483
|
+
// src/audit/inspection.ts
|
|
484
|
+
import fg2 from "fast-glob";
|
|
485
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
443
486
|
import path4 from "path";
|
|
487
|
+
function withRepositoryInspection(root, run) {
|
|
488
|
+
return inspections.run({ root: path4.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
|
|
489
|
+
}
|
|
490
|
+
function inspectionRead(root, key, read) {
|
|
491
|
+
const inspection = inspections.getStore();
|
|
492
|
+
if (!inspection || inspection.root !== path4.resolve(root)) return read();
|
|
493
|
+
if (!inspection.reads.has(key)) {
|
|
494
|
+
const pending = read();
|
|
495
|
+
inspection.reads.set(key, pending);
|
|
496
|
+
void pending.catch(() => {
|
|
497
|
+
if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return inspection.reads.get(key);
|
|
501
|
+
}
|
|
502
|
+
function inspectionGit(args, options) {
|
|
503
|
+
return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
|
|
504
|
+
}
|
|
505
|
+
async function auditGitPaths(root, kind) {
|
|
506
|
+
const custom = (await loadProjectConfig(root)).audit?.include ?? [];
|
|
507
|
+
const shared = inspections.getStore()?.root === path4.resolve(root) && !custom.length;
|
|
508
|
+
const expanded = fg2.generateTasks(custom).flatMap((task) => task.positive);
|
|
509
|
+
if (expanded.length > 1e3) throw new Error("Audit include patterns expand to more than 1,000 globs");
|
|
510
|
+
const documentSpecs = [...docSpecs, ...expanded.map((pattern) => `:(glob)${pattern}`)];
|
|
511
|
+
const specs = shared ? [...documentSpecs, ...sourceSpecs] : kind === "documents" ? documentSpecs : sourceSpecs;
|
|
512
|
+
const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
|
|
513
|
+
const { stdout } = await inspectionGit(
|
|
514
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
|
|
515
|
+
{ cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
|
|
516
|
+
);
|
|
517
|
+
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("."))));
|
|
518
|
+
if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
|
|
519
|
+
return paths.sort();
|
|
520
|
+
}
|
|
521
|
+
var inspections, sourceSpecs, docSpecs, sourceSuffixes;
|
|
522
|
+
var init_inspection = __esm({
|
|
523
|
+
"src/audit/inspection.ts"() {
|
|
524
|
+
"use strict";
|
|
525
|
+
init_git_read();
|
|
526
|
+
init_files();
|
|
527
|
+
inspections = new AsyncLocalStorage2();
|
|
528
|
+
sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
|
|
529
|
+
docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
|
|
530
|
+
sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// src/audit/inputs.ts
|
|
535
|
+
import fs4 from "fs/promises";
|
|
536
|
+
import * as nativeFs from "fs";
|
|
537
|
+
import path5 from "path";
|
|
538
|
+
import fg3 from "fast-glob";
|
|
539
|
+
async function auditInputPath(root, file) {
|
|
540
|
+
if (file === ".") return fs4.realpath(root);
|
|
541
|
+
try {
|
|
542
|
+
return await storePath(root, file);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
545
|
+
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
546
|
+
}
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function readAuditInput(root, file) {
|
|
551
|
+
try {
|
|
552
|
+
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
553
|
+
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
554
|
+
return value;
|
|
555
|
+
} catch (error) {
|
|
556
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async function auditGlob(root, patterns, options) {
|
|
561
|
+
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
562
|
+
const directories = /* @__PURE__ */ new Set();
|
|
563
|
+
const readdir2 = ((directory, opts, callback) => {
|
|
564
|
+
const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
|
|
565
|
+
directories.add(relative);
|
|
566
|
+
if (directories.size > MAX_INPUTS) {
|
|
567
|
+
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
auditInputPath(root, relative).then(() => {
|
|
571
|
+
nativeFs.readdir(directory, opts, callback);
|
|
572
|
+
}, callback);
|
|
573
|
+
});
|
|
574
|
+
const lstat2 = ((file, callback) => {
|
|
575
|
+
const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
|
|
576
|
+
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
577
|
+
});
|
|
578
|
+
const stream = fg3.stream(patterns, {
|
|
579
|
+
cwd: root,
|
|
580
|
+
ignore: options.ignore,
|
|
581
|
+
onlyFiles: false,
|
|
582
|
+
onlyDirectories: options.onlyDirectories,
|
|
583
|
+
objectMode: true,
|
|
584
|
+
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
585
|
+
fs: { readdir: readdir2, lstat: lstat2 },
|
|
586
|
+
concurrency: 16
|
|
587
|
+
});
|
|
588
|
+
const matches = [];
|
|
589
|
+
try {
|
|
590
|
+
for await (const value of stream) {
|
|
591
|
+
const entry = value;
|
|
592
|
+
const file = entry.path;
|
|
593
|
+
if (options.select && !options.select(file)) continue;
|
|
594
|
+
await auditInputPath(root, file);
|
|
595
|
+
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
596
|
+
matches.push(file);
|
|
597
|
+
if (matches.length > MAX_INPUTS) {
|
|
598
|
+
const largest = [...matches.reduce((counts, item) => {
|
|
599
|
+
const dir = item.split("/")[0];
|
|
600
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
601
|
+
return counts;
|
|
602
|
+
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
603
|
+
throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count4]) => `${dir} (${count4})`).join(", ")}. Narrow this check's inputs.`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} finally {
|
|
607
|
+
stream.destroy();
|
|
608
|
+
}
|
|
609
|
+
return matches.sort();
|
|
610
|
+
}
|
|
611
|
+
async function gitSourcePaths(root) {
|
|
612
|
+
return auditGitPaths(root, "sources");
|
|
613
|
+
}
|
|
614
|
+
async function gitIgnoredPaths(root, files) {
|
|
615
|
+
if (!files.length) return /* @__PURE__ */ new Set();
|
|
616
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
617
|
+
for (let offset = 0; offset < files.length; offset += 1e3) {
|
|
618
|
+
try {
|
|
619
|
+
const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
|
|
620
|
+
cwd: root,
|
|
621
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
622
|
+
timeout: 1e4,
|
|
623
|
+
input: files.slice(offset, offset + 1e3).join("\0") + "\0"
|
|
624
|
+
});
|
|
625
|
+
for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (error.code !== 1) throw error;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return ignored;
|
|
631
|
+
}
|
|
632
|
+
var MAX_INPUTS;
|
|
633
|
+
var init_inputs = __esm({
|
|
634
|
+
"src/audit/inputs.ts"() {
|
|
635
|
+
"use strict";
|
|
636
|
+
init_files();
|
|
637
|
+
init_storage();
|
|
638
|
+
init_git_read();
|
|
639
|
+
init_inspection();
|
|
640
|
+
MAX_INPUTS = 1e5;
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// src/audit/policy.ts
|
|
645
|
+
import fg4 from "fast-glob";
|
|
646
|
+
async function includedCheckPaths(root, paths, exclude) {
|
|
647
|
+
if (!exclude.length) return new Set(paths);
|
|
648
|
+
const included = /* @__PURE__ */ new Set();
|
|
649
|
+
for (let offset = 0; offset < paths.length; offset += 256) {
|
|
650
|
+
for (const file of await auditGlob(root, paths.slice(offset, offset + 256).map((file2) => fg4.escapePath(file2)), {
|
|
651
|
+
ignore: exclude,
|
|
652
|
+
followSymbolicLinks: false,
|
|
653
|
+
label: "Audit check exclusions"
|
|
654
|
+
})) included.add(file);
|
|
655
|
+
}
|
|
656
|
+
return included;
|
|
657
|
+
}
|
|
658
|
+
var init_policy = __esm({
|
|
659
|
+
"src/audit/policy.ts"() {
|
|
660
|
+
"use strict";
|
|
661
|
+
init_inputs();
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// src/audit/commands.ts
|
|
666
|
+
import path6 from "path";
|
|
444
667
|
function splitCommands(line) {
|
|
445
668
|
const parts = [];
|
|
446
669
|
let quote = "", start = 0;
|
|
@@ -476,7 +699,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
476
699
|
const cd = text2.match(/^cd\s+(.+)$/);
|
|
477
700
|
if (cd) {
|
|
478
701
|
const directory = literal(cd[1]);
|
|
479
|
-
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 : path6.posix.join(cwd ?? ".", directory);
|
|
480
703
|
continue;
|
|
481
704
|
}
|
|
482
705
|
for (const match of text2.matchAll(COMMAND)) {
|
|
@@ -487,7 +710,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
487
710
|
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
488
711
|
if (selector) {
|
|
489
712
|
const selected = literal(selector[1]);
|
|
490
|
-
directory = selected === null || cwd === null ? null :
|
|
713
|
+
directory = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
|
|
491
714
|
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
492
715
|
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
493
716
|
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
@@ -517,7 +740,7 @@ var init_commands = __esm({
|
|
|
517
740
|
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
518
741
|
literal = (value) => {
|
|
519
742
|
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
520
|
-
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) ||
|
|
743
|
+
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path6.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
521
744
|
};
|
|
522
745
|
}
|
|
523
746
|
});
|
|
@@ -822,52 +1045,6 @@ var init_claims = __esm({
|
|
|
822
1045
|
}
|
|
823
1046
|
});
|
|
824
1047
|
|
|
825
|
-
// src/audit/inspection.ts
|
|
826
|
-
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
827
|
-
import path5 from "path";
|
|
828
|
-
function withRepositoryInspection(root, run) {
|
|
829
|
-
return inspections.run({ root: path5.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
|
|
830
|
-
}
|
|
831
|
-
function inspectionRead(root, key, read) {
|
|
832
|
-
const inspection = inspections.getStore();
|
|
833
|
-
if (!inspection || inspection.root !== path5.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 === path5.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(path5.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 fs4 from "fs/promises";
|
|
958
|
-
import * as nativeFs from "fs";
|
|
959
|
-
import path6 from "path";
|
|
960
|
-
import fg2 from "fast-glob";
|
|
961
|
-
async function auditInputPath(root, file) {
|
|
962
|
-
if (file === ".") return fs4.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 = path6.relative(root, directory).split(path6.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 = path6.relative(root, file).split(path6.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, count4]) => `${dir} (${count4})`).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 fs5 from "fs/promises";
|
|
1068
1135
|
import path7 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(path7.posix.basename(file))) return ".";
|
|
1151
1219
|
const parent = path7.posix.dirname(file);
|
|
1152
1220
|
return path7.posix.basename(parent).toLowerCase() === ".claude" ? path7.posix.dirname(parent) : parent;
|
|
1153
1221
|
}
|
|
@@ -1169,7 +1237,7 @@ async function readDocs(resolvedRoot) {
|
|
|
1169
1237
|
path: candidate,
|
|
1170
1238
|
content: content2,
|
|
1171
1239
|
scope: documentScope(candidate),
|
|
1172
|
-
kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : "instructions",
|
|
1240
|
+
kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path7.posix.basename(candidate)) ? "instructions" : "guide",
|
|
1173
1241
|
lineCount: content2.split("\n").length,
|
|
1174
1242
|
lastCommit,
|
|
1175
1243
|
dirty: dirty.has(candidate),
|
|
@@ -1438,22 +1506,6 @@ var init_drift = __esm({
|
|
|
1438
1506
|
}
|
|
1439
1507
|
});
|
|
1440
1508
|
|
|
1441
|
-
// src/audit/types.ts
|
|
1442
|
-
var ALL_CHECKS;
|
|
1443
|
-
var init_types = __esm({
|
|
1444
|
-
"src/audit/types.ts"() {
|
|
1445
|
-
"use strict";
|
|
1446
|
-
ALL_CHECKS = [
|
|
1447
|
-
"deleted-reference",
|
|
1448
|
-
"new-module",
|
|
1449
|
-
"stale-count",
|
|
1450
|
-
"dead-command",
|
|
1451
|
-
"deps-changed",
|
|
1452
|
-
"decision-anchor-drift"
|
|
1453
|
-
];
|
|
1454
|
-
}
|
|
1455
|
-
});
|
|
1456
|
-
|
|
1457
1509
|
// src/audit/checks/deleted-reference.ts
|
|
1458
1510
|
import path12 from "path";
|
|
1459
1511
|
async function checkDeletedReferences(ctx) {
|
|
@@ -1522,7 +1574,7 @@ var init_deleted_reference = __esm({
|
|
|
1522
1574
|
});
|
|
1523
1575
|
|
|
1524
1576
|
// src/audit/checks/new-module.ts
|
|
1525
|
-
import
|
|
1577
|
+
import fg6 from "fast-glob";
|
|
1526
1578
|
import fs8 from "fs/promises";
|
|
1527
1579
|
import path13 from "path";
|
|
1528
1580
|
function escapeRegExp(text2) {
|
|
@@ -1584,7 +1636,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1584
1636
|
const result = /* @__PURE__ */ new Map();
|
|
1585
1637
|
for (const dir of parents) {
|
|
1586
1638
|
const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
|
|
1587
|
-
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${
|
|
1639
|
+
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
|
|
1588
1640
|
ignore,
|
|
1589
1641
|
onlyDirectories: true,
|
|
1590
1642
|
label: "Module directory discovery",
|
|
@@ -1597,7 +1649,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1597
1649
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1598
1650
|
let count4 = 0;
|
|
1599
1651
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1600
|
-
count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) =>
|
|
1652
|
+
count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)), {
|
|
1601
1653
|
ignore,
|
|
1602
1654
|
followSymbolicLinks: false,
|
|
1603
1655
|
label: `Source discovery in ${dir}`
|
|
@@ -1932,6 +1984,87 @@ var init_dead_command = __esm({
|
|
|
1932
1984
|
}
|
|
1933
1985
|
});
|
|
1934
1986
|
|
|
1987
|
+
// src/audit/dependency-claims.ts
|
|
1988
|
+
import path16 from "path";
|
|
1989
|
+
import { parse as parseToml } from "smol-toml";
|
|
1990
|
+
function dependencyText(content2) {
|
|
1991
|
+
return content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, (text2) => text2.replace(/[^\n]/g, " ")).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
|
|
1992
|
+
}
|
|
1993
|
+
function declarations(file, source) {
|
|
1994
|
+
const result = /* @__PURE__ */ new Map();
|
|
1995
|
+
if (!source) return result;
|
|
1996
|
+
if (path16.posix.basename(file) === "package.json") {
|
|
1997
|
+
const json2 = JSON.parse(source);
|
|
1998
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "engines"]) {
|
|
1999
|
+
for (const [name, value] of Object.entries(json2[section] ?? {})) {
|
|
2000
|
+
if (typeof value === "string") result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
} else if (file.endsWith("libs.versions.toml")) {
|
|
2004
|
+
const catalog = parseToml(source);
|
|
2005
|
+
for (const section of ["libraries", "plugins"]) for (const [alias, entry] of Object.entries(catalog[section] ?? {})) {
|
|
2006
|
+
if (typeof entry === "string") {
|
|
2007
|
+
const parts = entry.split(":");
|
|
2008
|
+
if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
|
|
2009
|
+
} else if (entry && typeof entry === "object") {
|
|
2010
|
+
const name = entry.module ?? (entry.group && entry.name ? `${entry.group}:${entry.name}` : entry.id);
|
|
2011
|
+
const version = typeof entry.version === "string" ? entry.version : catalog.versions?.[entry.version?.ref];
|
|
2012
|
+
if (typeof name === "string") result.set(name, typeof version === "string" ? version : "unspecified");
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
} else if (/build\.gradle(?:\.kts)?$/.test(file)) {
|
|
2016
|
+
for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g)) result.set(match[1], match[2]);
|
|
2017
|
+
for (const match of source.matchAll(/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g)) result.set(match[1], match[2]);
|
|
2018
|
+
}
|
|
2019
|
+
return result;
|
|
2020
|
+
}
|
|
2021
|
+
async function matchDependencyChanges(root, from, head, files, content2) {
|
|
2022
|
+
const matches = [];
|
|
2023
|
+
const lines = dependencyText(content2).split("\n");
|
|
2024
|
+
let incomplete = files.length > 64;
|
|
2025
|
+
for (const file of files.slice(0, 64)) {
|
|
2026
|
+
if (!/(?:^|\/)(?:package\.json|libs\.versions\.toml|build\.gradle(?:\.kts)?)$/.test(file)) {
|
|
2027
|
+
incomplete = true;
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
try {
|
|
2031
|
+
const read = async (revision) => {
|
|
2032
|
+
const { stdout: entry } = await inspectionGit(["ls-tree", revision, "--", `:(literal)${file}`], { cwd: root, timeout: 1e4 });
|
|
2033
|
+
if (!entry) return "";
|
|
2034
|
+
if (!entry.startsWith("100644 ") && !entry.startsWith("100755 ")) throw new Error("Unsupported manifest mode");
|
|
2035
|
+
return (await inspectionGit(["show", `${revision}:${file}`], { cwd: root, maxBuffer: 1024 * 1024, timeout: 1e4 })).stdout;
|
|
2036
|
+
};
|
|
2037
|
+
const [before, after] = await Promise.all([read(from), read(head)]);
|
|
2038
|
+
const old = declarations(file, before), next = declarations(file, after);
|
|
2039
|
+
for (const dependency of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
|
|
2040
|
+
if (old.get(dependency) === next.get(dependency)) continue;
|
|
2041
|
+
const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2042
|
+
const mention = new RegExp(`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`, "i");
|
|
2043
|
+
const line = lines.findIndex((text2) => mention.test(text2));
|
|
2044
|
+
if (line < 0) continue;
|
|
2045
|
+
matches.push({
|
|
2046
|
+
dependency,
|
|
2047
|
+
manifest: file,
|
|
2048
|
+
before: old.get(dependency) ?? null,
|
|
2049
|
+
after: next.get(dependency) ?? null,
|
|
2050
|
+
line: line + 1,
|
|
2051
|
+
excerpt: lines[line].trim().slice(0, 500)
|
|
2052
|
+
});
|
|
2053
|
+
if (matches.length >= 100) return { matches, incomplete: true };
|
|
2054
|
+
}
|
|
2055
|
+
} catch {
|
|
2056
|
+
incomplete = true;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
return { matches, incomplete };
|
|
2060
|
+
}
|
|
2061
|
+
var init_dependency_claims = __esm({
|
|
2062
|
+
"src/audit/dependency-claims.ts"() {
|
|
2063
|
+
"use strict";
|
|
2064
|
+
init_inspection();
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
|
|
1935
2068
|
// src/audit/release-metadata.ts
|
|
1936
2069
|
async function git(root, args) {
|
|
1937
2070
|
return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
@@ -2011,13 +2144,6 @@ async function checkDepsChanged(ctx) {
|
|
|
2011
2144
|
});
|
|
2012
2145
|
continue;
|
|
2013
2146
|
}
|
|
2014
|
-
if (doc.dirty) {
|
|
2015
|
-
result.skipped.push({
|
|
2016
|
-
check: "deps-changed",
|
|
2017
|
-
doc: doc.path,
|
|
2018
|
-
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
2019
|
-
});
|
|
2020
|
-
}
|
|
2021
2147
|
let content2 = doc.content;
|
|
2022
2148
|
if (doc.dirty) {
|
|
2023
2149
|
try {
|
|
@@ -2030,7 +2156,6 @@ async function checkDepsChanged(ctx) {
|
|
|
2030
2156
|
continue;
|
|
2031
2157
|
}
|
|
2032
2158
|
}
|
|
2033
|
-
if (!hasDependencyContent(content2)) continue;
|
|
2034
2159
|
const range = await commitsTouchingSince(
|
|
2035
2160
|
ctx.root,
|
|
2036
2161
|
doc.lastCommit.hash,
|
|
@@ -2054,16 +2179,32 @@ async function checkDepsChanged(ctx) {
|
|
|
2054
2179
|
range.commits = relevant;
|
|
2055
2180
|
range.total = relevant.length;
|
|
2056
2181
|
if (range.total === 0) continue;
|
|
2057
|
-
const
|
|
2182
|
+
const relevance = await matchDependencyChanges(
|
|
2183
|
+
ctx.root,
|
|
2184
|
+
doc.lastCommit.hash,
|
|
2185
|
+
ctx.headHash,
|
|
2186
|
+
[...new Set(range.commits.flatMap((commit) => commit.files))],
|
|
2187
|
+
content2
|
|
2188
|
+
);
|
|
2189
|
+
if (!relevance.matches.length && !hasDependencyContent(content2)) continue;
|
|
2190
|
+
const match = relevance.matches[0];
|
|
2191
|
+
if (doc.dirty && match) result.skipped.push({
|
|
2192
|
+
check: "deps-changed",
|
|
2193
|
+
doc: doc.path,
|
|
2194
|
+
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
2195
|
+
});
|
|
2058
2196
|
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
2059
2197
|
type: "deps-changed",
|
|
2060
|
-
|
|
2061
|
-
|
|
2198
|
+
...match ? {} : { resolution: "informational" },
|
|
2199
|
+
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." : ""}`,
|
|
2200
|
+
anchor: { doc: doc.path, line: match?.line ?? null, excerpt: match?.excerpt ?? null },
|
|
2062
2201
|
evidence: {
|
|
2063
2202
|
kind: "doc-behind-manifests",
|
|
2064
2203
|
docLastCommit: doc.lastCommit,
|
|
2065
2204
|
manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
|
|
2066
|
-
totalCommits: range.total
|
|
2205
|
+
totalCommits: range.total,
|
|
2206
|
+
matches: relevance.matches,
|
|
2207
|
+
matchingIncomplete: relevance.incomplete
|
|
2067
2208
|
}
|
|
2068
2209
|
});
|
|
2069
2210
|
}
|
|
@@ -2073,6 +2214,7 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
|
2073
2214
|
var init_deps_changed = __esm({
|
|
2074
2215
|
"src/audit/checks/deps-changed.ts"() {
|
|
2075
2216
|
"use strict";
|
|
2217
|
+
init_dependency_claims();
|
|
2076
2218
|
init_docs();
|
|
2077
2219
|
init_release_metadata();
|
|
2078
2220
|
init_git();
|
|
@@ -2097,7 +2239,7 @@ var init_deps_changed = __esm({
|
|
|
2097
2239
|
});
|
|
2098
2240
|
|
|
2099
2241
|
// src/context/lexical.ts
|
|
2100
|
-
import
|
|
2242
|
+
import path17 from "path";
|
|
2101
2243
|
var init_lexical = __esm({
|
|
2102
2244
|
"src/context/lexical.ts"() {
|
|
2103
2245
|
"use strict";
|
|
@@ -2281,7 +2423,7 @@ var init_provenance = __esm({
|
|
|
2281
2423
|
|
|
2282
2424
|
// src/decisions/decisions.ts
|
|
2283
2425
|
import fs9 from "fs/promises";
|
|
2284
|
-
import
|
|
2426
|
+
import path18 from "path";
|
|
2285
2427
|
import { createHash } from "crypto";
|
|
2286
2428
|
import { z as z4 } from "zod";
|
|
2287
2429
|
async function loadDecisionStore(rootDir) {
|
|
@@ -2332,9 +2474,9 @@ var init_decisions = __esm({
|
|
|
2332
2474
|
});
|
|
2333
2475
|
|
|
2334
2476
|
// src/decisions/drift.ts
|
|
2335
|
-
import
|
|
2477
|
+
import path19 from "path";
|
|
2336
2478
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
2337
|
-
const resolvedRoot =
|
|
2479
|
+
const resolvedRoot = path19.resolve(rootDir);
|
|
2338
2480
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
2339
2481
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
2340
2482
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -2530,7 +2672,9 @@ var init_findings = __esm({
|
|
|
2530
2672
|
kind: z5.literal("doc-behind-manifests"),
|
|
2531
2673
|
docLastCommit: commitSchema,
|
|
2532
2674
|
manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
|
|
2533
|
-
totalCommits: count
|
|
2675
|
+
totalCommits: count,
|
|
2676
|
+
matches: z5.array(z5.object({ dependency: z5.string(), manifest: z5.string(), before: z5.string().nullable(), after: z5.string().nullable(), line: z5.number().int().positive(), excerpt: z5.string() })).optional(),
|
|
2677
|
+
matchingIncomplete: z5.boolean().optional()
|
|
2534
2678
|
}),
|
|
2535
2679
|
z5.object({
|
|
2536
2680
|
kind: z5.literal("decision-anchor"),
|
|
@@ -2546,7 +2690,7 @@ var init_findings = __esm({
|
|
|
2546
2690
|
type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2547
2691
|
confidence: z5.enum(["certain", "likely"])
|
|
2548
2692
|
});
|
|
2549
|
-
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.
|
|
2693
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.enum(["recheck", "informational"]).optional() });
|
|
2550
2694
|
checkResultSchema = z5.object({
|
|
2551
2695
|
issues: z5.array(issueSchema),
|
|
2552
2696
|
advisories: z5.array(advisorySchema),
|
|
@@ -2564,14 +2708,18 @@ async function readRecord(root, id) {
|
|
|
2564
2708
|
if (raw === null) return null;
|
|
2565
2709
|
const record = recordSchema.parse(raw);
|
|
2566
2710
|
const { digest: stored, ...payload } = record;
|
|
2567
|
-
if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || event.
|
|
2711
|
+
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))) {
|
|
2568
2712
|
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2569
2713
|
}
|
|
2570
2714
|
return record;
|
|
2571
2715
|
}
|
|
2572
|
-
function
|
|
2716
|
+
function evidenceFingerprint(evidence, reason2) {
|
|
2717
|
+
return digest([evidence.scope, evidence.tree, ...reason2 === "no-dependency-claims" ? [] : [evidence.lastScopeCommit], evidence.paths]);
|
|
2718
|
+
}
|
|
2719
|
+
function scopes(finding, reason2) {
|
|
2573
2720
|
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2574
2721
|
if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
2722
|
+
if (reason2 === "no-dependency-claims" && e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`];
|
|
2575
2723
|
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
2576
2724
|
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
2577
2725
|
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
@@ -2582,9 +2730,9 @@ function scopes(finding) {
|
|
|
2582
2730
|
return `:(literal)${file}`;
|
|
2583
2731
|
}).sort();
|
|
2584
2732
|
}
|
|
2585
|
-
async function reviewEvidence(root, finding) {
|
|
2733
|
+
async function reviewEvidence(root, finding, reason2) {
|
|
2586
2734
|
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
2587
|
-
const scope = scopes(finding);
|
|
2735
|
+
const scope = scopes(finding, reason2);
|
|
2588
2736
|
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2589
2737
|
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2590
2738
|
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
@@ -2610,7 +2758,7 @@ async function reviewEvidence(root, finding) {
|
|
|
2610
2758
|
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2611
2759
|
}
|
|
2612
2760
|
}
|
|
2613
|
-
const fingerprint =
|
|
2761
|
+
const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason2);
|
|
2614
2762
|
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
2615
2763
|
return {
|
|
2616
2764
|
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
@@ -2657,7 +2805,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2657
2805
|
const record = await readRecord(root, id);
|
|
2658
2806
|
if (!record) return null;
|
|
2659
2807
|
const event = record.events.at(-1);
|
|
2660
|
-
const current = await reviewEvidence(root, event.finding);
|
|
2808
|
+
const current = await reviewEvidence(root, event.finding, event.reasonCode);
|
|
2661
2809
|
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
2662
2810
|
const base = {
|
|
2663
2811
|
id,
|
|
@@ -2667,7 +2815,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2667
2815
|
outcome: event.outcome,
|
|
2668
2816
|
reviewedHead: event.evidence.head
|
|
2669
2817
|
};
|
|
2670
|
-
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." };
|
|
2818
|
+
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." };
|
|
2671
2819
|
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." };
|
|
2672
2820
|
} catch (error) {
|
|
2673
2821
|
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -2684,7 +2832,7 @@ async function advisoryReviewInventory(root) {
|
|
|
2684
2832
|
if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
|
|
2685
2833
|
return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
|
|
2686
2834
|
}
|
|
2687
|
-
var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2835
|
+
var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2688
2836
|
var init_advisory_review = __esm({
|
|
2689
2837
|
"src/audit/advisory-review.ts"() {
|
|
2690
2838
|
"use strict";
|
|
@@ -2710,11 +2858,13 @@ var init_advisory_review = __esm({
|
|
|
2710
2858
|
lastScopeCommit: commitSchema2,
|
|
2711
2859
|
paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
|
|
2712
2860
|
});
|
|
2861
|
+
dismissalReasonSchema = z6.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
2713
2862
|
eventSchema2 = z6.object({
|
|
2714
2863
|
at: z6.string().datetime(),
|
|
2715
|
-
reviewer: z6.string().trim().min(1).max(200),
|
|
2864
|
+
reviewer: z6.string().trim().min(1).max(200).optional(),
|
|
2716
2865
|
note: z6.string().trim().min(1).max(2500),
|
|
2717
|
-
outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
|
|
2866
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
2867
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
2718
2868
|
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2719
2869
|
evidence: evidenceSchema2
|
|
2720
2870
|
});
|
|
@@ -2726,13 +2876,14 @@ var init_advisory_review = __esm({
|
|
|
2726
2876
|
recordPath: z6.string().optional(),
|
|
2727
2877
|
reviewer: z6.string().optional(),
|
|
2728
2878
|
note: z6.string().optional(),
|
|
2729
|
-
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
2879
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
|
|
2730
2880
|
reviewedHead: z6.string().optional()
|
|
2731
2881
|
});
|
|
2732
2882
|
advisoryReviewRequest = z6.object({
|
|
2733
2883
|
baselinePath: z6.string().min(1),
|
|
2734
2884
|
findingId: hashSchema,
|
|
2735
|
-
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
2885
|
+
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
2886
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
2736
2887
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2737
2888
|
note: eventSchema2.shape.note.optional(),
|
|
2738
2889
|
reviewToken: hashSchema.optional()
|
|
@@ -2743,12 +2894,12 @@ var init_advisory_review = __esm({
|
|
|
2743
2894
|
|
|
2744
2895
|
// src/audit/audit.ts
|
|
2745
2896
|
import fs11 from "fs/promises";
|
|
2746
|
-
import
|
|
2897
|
+
import path20 from "path";
|
|
2747
2898
|
async function computeAudit(rootDir, options = {}) {
|
|
2748
2899
|
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
2749
2900
|
}
|
|
2750
2901
|
async function auditCurrent(rootDir, options) {
|
|
2751
|
-
const resolvedRoot =
|
|
2902
|
+
const resolvedRoot = path20.resolve(rootDir);
|
|
2752
2903
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2753
2904
|
if (docs.length === 0) return null;
|
|
2754
2905
|
const report = {
|
|
@@ -2782,7 +2933,7 @@ async function auditCurrent(rootDir, options) {
|
|
|
2782
2933
|
}
|
|
2783
2934
|
let decisionsPresent = false;
|
|
2784
2935
|
try {
|
|
2785
|
-
await fs11.access(
|
|
2936
|
+
await fs11.access(path20.join(resolvedRoot, ".mason", "decisions"));
|
|
2786
2937
|
decisionsPresent = true;
|
|
2787
2938
|
} catch {
|
|
2788
2939
|
}
|
|
@@ -2794,13 +2945,21 @@ async function auditCurrent(rootDir, options) {
|
|
|
2794
2945
|
changesSinceDoc,
|
|
2795
2946
|
decisionsPresent
|
|
2796
2947
|
};
|
|
2948
|
+
const policy = (await loadProjectConfig(resolvedRoot)).audit;
|
|
2797
2949
|
const selected = options.checks ?? ALL_CHECKS;
|
|
2798
2950
|
for (const name of ALL_CHECKS) {
|
|
2799
2951
|
if (!selected.includes(name)) continue;
|
|
2800
|
-
const
|
|
2952
|
+
const exclusions = policy?.exclude?.[name] ?? [];
|
|
2953
|
+
const included = await includedCheckPaths(resolvedRoot, docs.map((doc) => doc.path), exclusions);
|
|
2954
|
+
const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
|
|
2955
|
+
if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
|
|
2956
|
+
const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
|
|
2957
|
+
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
|
|
2801
2958
|
report.checksRun.push(name);
|
|
2802
2959
|
report.issues.push(...issues);
|
|
2803
|
-
|
|
2960
|
+
const advisoryPaths = await includedCheckPaths(resolvedRoot, [...new Set(advisories.map((f) => f.anchor.doc))], exclusions);
|
|
2961
|
+
report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
|
|
2962
|
+
for (const f of advisories) if (!advisoryPaths.has(f.anchor.doc)) (report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
|
|
2804
2963
|
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
2805
2964
|
report.skippedChecks.push(...skipped);
|
|
2806
2965
|
}
|
|
@@ -2812,6 +2971,8 @@ async function auditCurrent(rootDir, options) {
|
|
|
2812
2971
|
var init_audit = __esm({
|
|
2813
2972
|
"src/audit/audit.ts"() {
|
|
2814
2973
|
"use strict";
|
|
2974
|
+
init_files();
|
|
2975
|
+
init_policy();
|
|
2815
2976
|
init_profile();
|
|
2816
2977
|
init_drift();
|
|
2817
2978
|
init_snapshot();
|
|
@@ -2824,7 +2985,7 @@ var init_audit = __esm({
|
|
|
2824
2985
|
|
|
2825
2986
|
// src/audit/repair.ts
|
|
2826
2987
|
import fs12 from "fs/promises";
|
|
2827
|
-
import
|
|
2988
|
+
import path21 from "path";
|
|
2828
2989
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2829
2990
|
import { z as z7 } from "zod";
|
|
2830
2991
|
function recheckable(finding) {
|
|
@@ -2845,7 +3006,7 @@ async function docState(root) {
|
|
|
2845
3006
|
docs.push([doc, null]);
|
|
2846
3007
|
}
|
|
2847
3008
|
}
|
|
2848
|
-
return digest(docs);
|
|
3009
|
+
return digest([docs, (await loadProjectConfig(root)).audit]);
|
|
2849
3010
|
}
|
|
2850
3011
|
async function stableAudit(root, checks, options = {}) {
|
|
2851
3012
|
return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
|
|
@@ -2911,8 +3072,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
|
|
|
2911
3072
|
}
|
|
2912
3073
|
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
2913
3074
|
const root = await fs12.realpath(rootDir);
|
|
2914
|
-
const declaredRoot =
|
|
2915
|
-
const relative =
|
|
3075
|
+
const declaredRoot = path21.resolve(rootDir);
|
|
3076
|
+
const relative = path21.isAbsolute(baselinePath) ? path21.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2916
3077
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
2917
3078
|
const { digest: savedDigest, ...payload } = stored;
|
|
2918
3079
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
@@ -2958,6 +3119,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2958
3119
|
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
2959
3120
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
2960
3121
|
}
|
|
3122
|
+
if (current.excludedChecks?.some((excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc)) {
|
|
3123
|
+
return { ...base, status: "resolved", reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained." };
|
|
3124
|
+
}
|
|
2961
3125
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
2962
3126
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
2963
3127
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
@@ -2968,6 +3132,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2968
3132
|
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
2969
3133
|
return { ...base, review, status: "review-required", reason: review.reason };
|
|
2970
3134
|
}
|
|
3135
|
+
if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
|
|
3136
|
+
return { ...base, status: "resolved", reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required." };
|
|
3137
|
+
}
|
|
2971
3138
|
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
2972
3139
|
return {
|
|
2973
3140
|
...base,
|
|
@@ -2996,7 +3163,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2996
3163
|
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
2997
3164
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
2998
3165
|
for (const f of findings) counts[f.status]++;
|
|
2999
|
-
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)));
|
|
3166
|
+
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)));
|
|
3000
3167
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
3001
3168
|
return {
|
|
3002
3169
|
version: 1,
|
|
@@ -3039,13 +3206,14 @@ var init_repair = __esm({
|
|
|
3039
3206
|
docs: z7.array(z7.object({
|
|
3040
3207
|
path: z7.string().refine((value) => normalizeRepoPath(value) === value),
|
|
3041
3208
|
scope: z7.string().optional(),
|
|
3042
|
-
kind: z7.enum(["instructions", "readme"]).optional(),
|
|
3209
|
+
kind: z7.enum(["instructions", "readme", "guide"]).optional(),
|
|
3043
3210
|
lastCommit: commitSchema.nullable(),
|
|
3044
3211
|
dirty: z7.boolean(),
|
|
3045
3212
|
lineCount: count2
|
|
3046
3213
|
})).nonempty(),
|
|
3047
3214
|
decisionsChecked: z7.boolean(),
|
|
3048
3215
|
clean: z7.boolean(),
|
|
3216
|
+
excludedChecks: z7.array(z7.object({ check: checkSchema, doc: z7.string() })).optional(),
|
|
3049
3217
|
issues: z7.array(issueSchema),
|
|
3050
3218
|
advisories: z7.array(advisorySchema),
|
|
3051
3219
|
suppressedAdvisories: z7.array(advisorySchema).optional(),
|
|
@@ -3071,9 +3239,9 @@ var init_repair = __esm({
|
|
|
3071
3239
|
|
|
3072
3240
|
// src/automation/evidence.ts
|
|
3073
3241
|
import fs13 from "fs/promises";
|
|
3074
|
-
import
|
|
3242
|
+
import path22 from "path";
|
|
3075
3243
|
import { createHash as createHash3 } from "crypto";
|
|
3076
|
-
import
|
|
3244
|
+
import fg7 from "fast-glob";
|
|
3077
3245
|
import { z as z8 } from "zod";
|
|
3078
3246
|
async function git3(root, ...args) {
|
|
3079
3247
|
return profilePhase("automation.git", async () => (await execGit(args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
|
|
@@ -3091,6 +3259,11 @@ async function readInputs(root) {
|
|
|
3091
3259
|
}
|
|
3092
3260
|
async function collectInputs(root) {
|
|
3093
3261
|
const docPaths = await discoverDocPaths(root);
|
|
3262
|
+
const auditPolicy = (await loadProjectConfig(root)).audit;
|
|
3263
|
+
const allowed = /* @__PURE__ */ new Map();
|
|
3264
|
+
for (const name of ["deleted-reference", "new-module", "stale-count", "dead-command"]) {
|
|
3265
|
+
allowed.set(name, await includedCheckPaths(root, docPaths, auditPolicy?.exclude?.[name] ?? []));
|
|
3266
|
+
}
|
|
3094
3267
|
const [headText, docStatus, shallowPath, replacements] = await Promise.all([
|
|
3095
3268
|
git3(root, "rev-parse", "HEAD"),
|
|
3096
3269
|
docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
|
|
@@ -3100,7 +3273,7 @@ async function collectInputs(root) {
|
|
|
3100
3273
|
const head = headText.trim();
|
|
3101
3274
|
let shallow = null;
|
|
3102
3275
|
try {
|
|
3103
|
-
shallow = await fs13.readFile(
|
|
3276
|
+
shallow = await fs13.readFile(path22.resolve(root, shallowPath.trim()), "utf8");
|
|
3104
3277
|
} catch (error) {
|
|
3105
3278
|
if (error.code !== "ENOENT") throw error;
|
|
3106
3279
|
}
|
|
@@ -3119,18 +3292,18 @@ async function collectInputs(root) {
|
|
|
3119
3292
|
docContents.push([file, text2]);
|
|
3120
3293
|
const parsed = { path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") };
|
|
3121
3294
|
parsedDocs.push(parsed);
|
|
3122
|
-
for (const claim of parsed.claims.paths) {
|
|
3295
|
+
for (const claim of allowed.get("deleted-reference").has(file) ? parsed.claims.paths : []) {
|
|
3123
3296
|
const scope = pathClaimScope(file, claim);
|
|
3124
3297
|
if (scope?.candidates.some(gitMetadataPath)) continue;
|
|
3125
3298
|
for (const candidate of scope?.candidates ?? []) {
|
|
3126
3299
|
if (optionalMasonPath(candidate)) continue;
|
|
3127
|
-
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(
|
|
3300
|
+
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path22.posix.dirname(candidate))]);
|
|
3128
3301
|
claims.push([candidate, filePresent, parentPresent]);
|
|
3129
3302
|
}
|
|
3130
3303
|
}
|
|
3131
3304
|
}
|
|
3132
|
-
const combinedDocs = moduleDocumentation(parsedDocs);
|
|
3133
|
-
const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
|
|
3305
|
+
const combinedDocs = moduleDocumentation(parsedDocs.filter((doc) => allowed.get("new-module").has(doc.path)));
|
|
3306
|
+
const countClaims = parsedDocs.filter((doc) => allowed.get("stale-count").has(doc.path)).flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
|
|
3134
3307
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
3135
3308
|
const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
3136
3309
|
if (error.code === "ENOENT") return "absent";
|
|
@@ -3139,8 +3312,8 @@ async function collectInputs(root) {
|
|
|
3139
3312
|
const [modules, counts, commands2, decisionFiles] = await Promise.all([
|
|
3140
3313
|
combinedDocs ? moduleCandidates(root, combinedDocs) : [],
|
|
3141
3314
|
Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
|
|
3142
|
-
commandInputs(root, parsedDocs),
|
|
3143
|
-
|
|
3315
|
+
commandInputs(root, parsedDocs.filter((doc) => allowed.get("dead-command").has(doc.path))),
|
|
3316
|
+
fg7(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
|
|
3144
3317
|
]);
|
|
3145
3318
|
const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
|
|
3146
3319
|
await storePath(root, file);
|
|
@@ -3150,7 +3323,7 @@ async function collectInputs(root) {
|
|
|
3150
3323
|
git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
3151
3324
|
git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
3152
3325
|
]) : ["", ""];
|
|
3153
|
-
const common = [
|
|
3326
|
+
const common = [7, engineVersion, head, shallow, replacements, docContents, auditPolicy];
|
|
3154
3327
|
const keys = {
|
|
3155
3328
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
3156
3329
|
"new-module": hash([common, modules]),
|
|
@@ -3197,6 +3370,8 @@ var engineVersion, hash, content, cacheSchema;
|
|
|
3197
3370
|
var init_evidence = __esm({
|
|
3198
3371
|
"src/automation/evidence.ts"() {
|
|
3199
3372
|
"use strict";
|
|
3373
|
+
init_policy();
|
|
3374
|
+
init_files();
|
|
3200
3375
|
init_git_read();
|
|
3201
3376
|
init_profile();
|
|
3202
3377
|
init_docs();
|
|
@@ -3210,7 +3385,7 @@ var init_evidence = __esm({
|
|
|
3210
3385
|
init_dead_command();
|
|
3211
3386
|
init_storage();
|
|
3212
3387
|
init_advisory_review();
|
|
3213
|
-
engineVersion = true ? "0.19.
|
|
3388
|
+
engineVersion = true ? "0.19.1" : "development";
|
|
3214
3389
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3215
3390
|
content = readAuditInput;
|
|
3216
3391
|
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
@@ -3768,9 +3943,9 @@ var init_runtime = __esm({
|
|
|
3768
3943
|
|
|
3769
3944
|
// src/automation/hook-root.ts
|
|
3770
3945
|
import fs15 from "fs/promises";
|
|
3771
|
-
import
|
|
3946
|
+
import path23 from "path";
|
|
3772
3947
|
async function repositoryRoot(dir) {
|
|
3773
|
-
if (!
|
|
3948
|
+
if (!path23.isAbsolute(dir)) throw new Error("Hook input working directory must be absolute.");
|
|
3774
3949
|
try {
|
|
3775
3950
|
return await fs15.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
|
|
3776
3951
|
} catch (error) {
|
|
@@ -3802,7 +3977,7 @@ var init_hook_root = __esm({
|
|
|
3802
3977
|
// src/setup/model.ts
|
|
3803
3978
|
import { z as z11 } from "zod";
|
|
3804
3979
|
import fs16 from "fs/promises";
|
|
3805
|
-
import
|
|
3980
|
+
import path24 from "path";
|
|
3806
3981
|
async function loadSetup(root) {
|
|
3807
3982
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
3808
3983
|
if (raw === null) return null;
|
|
@@ -3811,7 +3986,7 @@ async function loadSetup(root) {
|
|
|
3811
3986
|
async function effectiveSetup(root) {
|
|
3812
3987
|
const local = await loadSetup(root);
|
|
3813
3988
|
if (local) return { root, setup: local };
|
|
3814
|
-
const common = await fs16.realpath(
|
|
3989
|
+
const common = await fs16.realpath(path24.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
|
|
3815
3990
|
const own = await fs16.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
3816
3991
|
if (common === own) return { root, setup: null };
|
|
3817
3992
|
const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
|
|
@@ -3824,7 +3999,7 @@ async function effectiveSetup(root) {
|
|
|
3824
3999
|
if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
|
|
3825
4000
|
throw error;
|
|
3826
4001
|
}
|
|
3827
|
-
const inheritedCommon = await fs16.realpath(
|
|
4002
|
+
const inheritedCommon = await fs16.realpath(path24.resolve(
|
|
3828
4003
|
inheritedRoot,
|
|
3829
4004
|
(await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
|
|
3830
4005
|
));
|
|
@@ -3930,7 +4105,7 @@ var init_launcher = __esm({
|
|
|
3930
4105
|
"use strict";
|
|
3931
4106
|
init_evidence();
|
|
3932
4107
|
init_model();
|
|
3933
|
-
executingVersion = () => true ? "0.19.
|
|
4108
|
+
executingVersion = () => true ? "0.19.1" : "development";
|
|
3934
4109
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3935
4110
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3936
4111
|
}
|
|
@@ -4146,7 +4321,7 @@ __export(files_exports, {
|
|
|
4146
4321
|
readText: () => readText
|
|
4147
4322
|
});
|
|
4148
4323
|
import fs19 from "fs/promises";
|
|
4149
|
-
import
|
|
4324
|
+
import path25 from "path";
|
|
4150
4325
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
4151
4326
|
async function readText(root, file) {
|
|
4152
4327
|
try {
|
|
@@ -4179,7 +4354,7 @@ async function applyEdit(root, edit) {
|
|
|
4179
4354
|
return true;
|
|
4180
4355
|
}
|
|
4181
4356
|
const file = await storePath(root, edit.path, true);
|
|
4182
|
-
const temporary =
|
|
4357
|
+
const temporary = path25.join(path25.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
4183
4358
|
try {
|
|
4184
4359
|
const mode = await fs19.stat(file).then((s) => s.mode & 511, () => 384);
|
|
4185
4360
|
const handle = await fs19.open(temporary, "wx", mode);
|
|
@@ -4304,9 +4479,11 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
4304
4479
|
- For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
|
|
4305
4480
|
- \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
|
|
4306
4481
|
|
|
4307
|
-
- When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings.
|
|
4482
|
+
- When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings. Specific suppressed advisories remain unresolved; editing a doc does not approve it. Informational dependency notices require no review.
|
|
4308
4483
|
- When advisory assessment is authorized, use \`review_advisory(action: "prepare", baselinePath, findingId)\` to inspect original evidence and history. Record \`addressed\`, \`inapplicable\`, or \`deferred\` with the actual reviewer, reason in note, and returned reviewToken only after relevant edits are committed. Commit the assessment and verify the same baseline again. Relevant evidence changes reopen it; deferral stays outstanding. Decision findings use \`review_decision\`; advisory closure never approves a decision.
|
|
4309
4484
|
|
|
4485
|
+
- For a dependency advisory, \`review_advisory(action: "dismiss", baselinePath, findingId, reasonCode, note)\` records a one-step dismissal without reviewer or token. Use \`no-dependency-claims\` for a document with no dependency claims (reopens on document content changes), or \`unrelated-manifest-change\` for reviewed changes that do not affect its guidance (reopens on document or manifest changes). Keep the relevant scope committed; commit the dismissal to share it. This never approves a decision.
|
|
4486
|
+
|
|
4310
4487
|
- When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason-auto status\` / \`mason-auto check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
|
|
4311
4488
|
|
|
4312
4489
|
Inspect source for what the retrieved context does not answer.
|
|
@@ -4602,9 +4779,9 @@ __export(teardown_exports, {
|
|
|
4602
4779
|
});
|
|
4603
4780
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
4604
4781
|
import fs20 from "fs/promises";
|
|
4605
|
-
import
|
|
4782
|
+
import path26 from "path";
|
|
4606
4783
|
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
4607
|
-
import
|
|
4784
|
+
import fg8 from "fast-glob";
|
|
4608
4785
|
import { z as z18 } from "zod";
|
|
4609
4786
|
async function planTeardown(root, selected) {
|
|
4610
4787
|
const edits = [], diagnostics = [];
|
|
@@ -4625,7 +4802,7 @@ async function planTeardown(root, selected) {
|
|
|
4625
4802
|
const setupBefore = await read(SETUP_PATH);
|
|
4626
4803
|
const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
|
|
4627
4804
|
const linked = (await workspace(root)).gitDir !== await fs20.realpath(
|
|
4628
|
-
|
|
4805
|
+
path26.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim())
|
|
4629
4806
|
);
|
|
4630
4807
|
const inactiveBefore = linked && !setup ? await read(INACTIVE_HOSTS_PATH) : null;
|
|
4631
4808
|
const inactive = inactiveBefore === null ? { version: 1, hosts: [] } : inactiveHostsSchema.parse(JSON.parse(inactiveBefore));
|
|
@@ -4748,7 +4925,7 @@ async function planTeardown(root, selected) {
|
|
|
4748
4925
|
if (diagnostics.length) edits.splice(instructionEditStart);
|
|
4749
4926
|
if (!diagnostics.length) {
|
|
4750
4927
|
const cleanup = [];
|
|
4751
|
-
const receiptFiles = await
|
|
4928
|
+
const receiptFiles = await fg8(
|
|
4752
4929
|
selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
|
|
4753
4930
|
{ cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
|
|
4754
4931
|
);
|
|
@@ -5222,10 +5399,10 @@ var init_check = __esm({
|
|
|
5222
5399
|
});
|
|
5223
5400
|
|
|
5224
5401
|
// src/review/evidence.ts
|
|
5225
|
-
import
|
|
5402
|
+
import path27 from "path";
|
|
5226
5403
|
import { z as z22 } from "zod";
|
|
5227
5404
|
function relativeArtifact(root, file) {
|
|
5228
|
-
const relative =
|
|
5405
|
+
const relative = path27.isAbsolute(file) ? path27.relative(root, file) : file;
|
|
5229
5406
|
const normalized = normalizeRepoPath(relative);
|
|
5230
5407
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
5231
5408
|
return normalized;
|
|
@@ -5322,7 +5499,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
5322
5499
|
const report = await readStoreJson(root, file);
|
|
5323
5500
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
5324
5501
|
const sourceRoot = check.sourceRoot ?? root;
|
|
5325
|
-
if (!
|
|
5502
|
+
if (!path27.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
|
|
5326
5503
|
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
5327
5504
|
result.outcome = parsed.outcome;
|
|
5328
5505
|
result.counts = parsed.counts;
|
|
@@ -5444,7 +5621,7 @@ var init_evidence2 = __esm({
|
|
|
5444
5621
|
|
|
5445
5622
|
// src/review/review.ts
|
|
5446
5623
|
import fs21 from "fs/promises";
|
|
5447
|
-
import
|
|
5624
|
+
import path28 from "path";
|
|
5448
5625
|
import { execFile as execFile4 } from "child_process";
|
|
5449
5626
|
import { promisify as promisify4 } from "util";
|
|
5450
5627
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
@@ -5473,7 +5650,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
5473
5650
|
return matchingPaths(record.files, changedFiles);
|
|
5474
5651
|
}
|
|
5475
5652
|
async function computeReview(rootDir, base, options = {}) {
|
|
5476
|
-
const resolvedRoot =
|
|
5653
|
+
const resolvedRoot = path28.resolve(rootDir);
|
|
5477
5654
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
5478
5655
|
if (head === "unknown") return null;
|
|
5479
5656
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -5523,7 +5700,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
5523
5700
|
analyzed,
|
|
5524
5701
|
async (relPath) => {
|
|
5525
5702
|
try {
|
|
5526
|
-
await fs21.access(
|
|
5703
|
+
await fs21.access(path28.join(resolvedRoot, relPath));
|
|
5527
5704
|
return true;
|
|
5528
5705
|
} catch {
|
|
5529
5706
|
return false;
|