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-auto.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() {
|
|
@@ -95,6 +96,26 @@ var init_git_read = __esm({
|
|
|
95
96
|
}
|
|
96
97
|
});
|
|
97
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
|
+
|
|
98
119
|
// src/utils/files.ts
|
|
99
120
|
import fs from "fs/promises";
|
|
100
121
|
import { constants } from "fs";
|
|
@@ -139,6 +160,27 @@ async function loadProjectConfig(root) {
|
|
|
139
160
|
}
|
|
140
161
|
config[key] = value[key];
|
|
141
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
|
+
}
|
|
142
184
|
return config;
|
|
143
185
|
} catch (error) {
|
|
144
186
|
if (error.code === "ENOENT") return {};
|
|
@@ -438,8 +480,190 @@ var init_store = __esm({
|
|
|
438
480
|
}
|
|
439
481
|
});
|
|
440
482
|
|
|
441
|
-
// src/audit/
|
|
483
|
+
// src/audit/inspection.ts
|
|
484
|
+
import fg2 from "fast-glob";
|
|
485
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
442
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";
|
|
443
667
|
function splitCommands(line) {
|
|
444
668
|
const parts = [];
|
|
445
669
|
let quote = "", start = 0;
|
|
@@ -475,7 +699,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
475
699
|
const cd = text2.match(/^cd\s+(.+)$/);
|
|
476
700
|
if (cd) {
|
|
477
701
|
const directory = literal(cd[1]);
|
|
478
|
-
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);
|
|
479
703
|
continue;
|
|
480
704
|
}
|
|
481
705
|
for (const match of text2.matchAll(COMMAND)) {
|
|
@@ -486,7 +710,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
486
710
|
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
487
711
|
if (selector) {
|
|
488
712
|
const selected = literal(selector[1]);
|
|
489
|
-
directory = selected === null || cwd === null ? null :
|
|
713
|
+
directory = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
|
|
490
714
|
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
491
715
|
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
492
716
|
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
@@ -516,7 +740,7 @@ var init_commands = __esm({
|
|
|
516
740
|
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
517
741
|
literal = (value) => {
|
|
518
742
|
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
519
|
-
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) ||
|
|
743
|
+
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path6.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
520
744
|
};
|
|
521
745
|
}
|
|
522
746
|
});
|
|
@@ -821,52 +1045,6 @@ var init_claims = __esm({
|
|
|
821
1045
|
}
|
|
822
1046
|
});
|
|
823
1047
|
|
|
824
|
-
// src/audit/inspection.ts
|
|
825
|
-
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
826
|
-
import path5 from "path";
|
|
827
|
-
function withRepositoryInspection(root, run) {
|
|
828
|
-
return inspections.run({ root: path5.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
|
|
829
|
-
}
|
|
830
|
-
function inspectionRead(root, key, read) {
|
|
831
|
-
const inspection = inspections.getStore();
|
|
832
|
-
if (!inspection || inspection.root !== path5.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 === path5.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(path5.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 fs4 from "fs/promises";
|
|
957
|
-
import * as nativeFs from "fs";
|
|
958
|
-
import path6 from "path";
|
|
959
|
-
import fg2 from "fast-glob";
|
|
960
|
-
async function auditInputPath(root, file) {
|
|
961
|
-
if (file === ".") return fs4.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 = path6.relative(root, directory).split(path6.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 = path6.relative(root, file).split(path6.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, count4]) => `${dir} (${count4})`).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 fs5 from "fs/promises";
|
|
1067
1135
|
import path7 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(path7.posix.basename(file))) return ".";
|
|
1150
1219
|
const parent = path7.posix.dirname(file);
|
|
1151
1220
|
return path7.posix.basename(parent).toLowerCase() === ".claude" ? path7.posix.dirname(parent) : parent;
|
|
1152
1221
|
}
|
|
@@ -1168,7 +1237,7 @@ async function readDocs(resolvedRoot) {
|
|
|
1168
1237
|
path: candidate,
|
|
1169
1238
|
content: content2,
|
|
1170
1239
|
scope: documentScope(candidate),
|
|
1171
|
-
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",
|
|
1172
1241
|
lineCount: content2.split("\n").length,
|
|
1173
1242
|
lastCommit,
|
|
1174
1243
|
dirty: dirty.has(candidate),
|
|
@@ -1437,22 +1506,6 @@ var init_drift = __esm({
|
|
|
1437
1506
|
}
|
|
1438
1507
|
});
|
|
1439
1508
|
|
|
1440
|
-
// src/audit/types.ts
|
|
1441
|
-
var ALL_CHECKS;
|
|
1442
|
-
var init_types = __esm({
|
|
1443
|
-
"src/audit/types.ts"() {
|
|
1444
|
-
"use strict";
|
|
1445
|
-
ALL_CHECKS = [
|
|
1446
|
-
"deleted-reference",
|
|
1447
|
-
"new-module",
|
|
1448
|
-
"stale-count",
|
|
1449
|
-
"dead-command",
|
|
1450
|
-
"deps-changed",
|
|
1451
|
-
"decision-anchor-drift"
|
|
1452
|
-
];
|
|
1453
|
-
}
|
|
1454
|
-
});
|
|
1455
|
-
|
|
1456
1509
|
// src/audit/checks/deleted-reference.ts
|
|
1457
1510
|
import path12 from "path";
|
|
1458
1511
|
async function checkDeletedReferences(ctx) {
|
|
@@ -1521,7 +1574,7 @@ var init_deleted_reference = __esm({
|
|
|
1521
1574
|
});
|
|
1522
1575
|
|
|
1523
1576
|
// src/audit/checks/new-module.ts
|
|
1524
|
-
import
|
|
1577
|
+
import fg6 from "fast-glob";
|
|
1525
1578
|
import fs8 from "fs/promises";
|
|
1526
1579
|
import path13 from "path";
|
|
1527
1580
|
function escapeRegExp(text2) {
|
|
@@ -1583,7 +1636,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1583
1636
|
const result = /* @__PURE__ */ new Map();
|
|
1584
1637
|
for (const dir of parents) {
|
|
1585
1638
|
const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
|
|
1586
|
-
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${
|
|
1639
|
+
result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
|
|
1587
1640
|
ignore,
|
|
1588
1641
|
onlyDirectories: true,
|
|
1589
1642
|
label: "Module directory discovery",
|
|
@@ -1596,7 +1649,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1596
1649
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1597
1650
|
let count4 = 0;
|
|
1598
1651
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1599
|
-
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)), {
|
|
1600
1653
|
ignore,
|
|
1601
1654
|
followSymbolicLinks: false,
|
|
1602
1655
|
label: `Source discovery in ${dir}`
|
|
@@ -1931,6 +1984,87 @@ var init_dead_command = __esm({
|
|
|
1931
1984
|
}
|
|
1932
1985
|
});
|
|
1933
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
|
+
|
|
1934
2068
|
// src/audit/release-metadata.ts
|
|
1935
2069
|
async function git(root, args) {
|
|
1936
2070
|
return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
@@ -1987,6 +2121,10 @@ var init_release_metadata = __esm({
|
|
|
1987
2121
|
});
|
|
1988
2122
|
|
|
1989
2123
|
// src/audit/checks/deps-changed.ts
|
|
2124
|
+
function hasDependencyContent(content2) {
|
|
2125
|
+
const text2 = content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
|
|
2126
|
+
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);
|
|
2127
|
+
}
|
|
1990
2128
|
function manifestPathspecs(doc) {
|
|
1991
2129
|
const scope = documentScope(doc);
|
|
1992
2130
|
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
@@ -2006,12 +2144,17 @@ async function checkDepsChanged(ctx) {
|
|
|
2006
2144
|
});
|
|
2007
2145
|
continue;
|
|
2008
2146
|
}
|
|
2147
|
+
let content2 = doc.content;
|
|
2009
2148
|
if (doc.dirty) {
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2149
|
+
try {
|
|
2150
|
+
content2 = (await inspectionGit(
|
|
2151
|
+
["show", `${doc.lastCommit.hash}:${doc.path}`],
|
|
2152
|
+
{ cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
|
|
2153
|
+
)).stdout;
|
|
2154
|
+
} catch {
|
|
2155
|
+
result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
2015
2158
|
}
|
|
2016
2159
|
const range = await commitsTouchingSince(
|
|
2017
2160
|
ctx.root,
|
|
@@ -2036,16 +2179,32 @@ async function checkDepsChanged(ctx) {
|
|
|
2036
2179
|
range.commits = relevant;
|
|
2037
2180
|
range.total = relevant.length;
|
|
2038
2181
|
if (range.total === 0) continue;
|
|
2039
|
-
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
|
+
});
|
|
2040
2196
|
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
2041
2197
|
type: "deps-changed",
|
|
2042
|
-
|
|
2043
|
-
|
|
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 },
|
|
2044
2201
|
evidence: {
|
|
2045
2202
|
kind: "doc-behind-manifests",
|
|
2046
2203
|
docLastCommit: doc.lastCommit,
|
|
2047
2204
|
manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
|
|
2048
|
-
totalCommits: range.total
|
|
2205
|
+
totalCommits: range.total,
|
|
2206
|
+
matches: relevance.matches,
|
|
2207
|
+
matchingIncomplete: relevance.incomplete
|
|
2049
2208
|
}
|
|
2050
2209
|
});
|
|
2051
2210
|
}
|
|
@@ -2055,9 +2214,11 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
|
2055
2214
|
var init_deps_changed = __esm({
|
|
2056
2215
|
"src/audit/checks/deps-changed.ts"() {
|
|
2057
2216
|
"use strict";
|
|
2217
|
+
init_dependency_claims();
|
|
2058
2218
|
init_docs();
|
|
2059
2219
|
init_release_metadata();
|
|
2060
2220
|
init_git();
|
|
2221
|
+
init_inspection();
|
|
2061
2222
|
init_checks();
|
|
2062
2223
|
MANIFEST_COMMITS_CAP = 10;
|
|
2063
2224
|
MANIFEST_PATHSPECS = [
|
|
@@ -2078,7 +2239,7 @@ var init_deps_changed = __esm({
|
|
|
2078
2239
|
});
|
|
2079
2240
|
|
|
2080
2241
|
// src/context/lexical.ts
|
|
2081
|
-
import
|
|
2242
|
+
import path17 from "path";
|
|
2082
2243
|
var init_lexical = __esm({
|
|
2083
2244
|
"src/context/lexical.ts"() {
|
|
2084
2245
|
"use strict";
|
|
@@ -2262,7 +2423,7 @@ var init_provenance = __esm({
|
|
|
2262
2423
|
|
|
2263
2424
|
// src/decisions/decisions.ts
|
|
2264
2425
|
import fs9 from "fs/promises";
|
|
2265
|
-
import
|
|
2426
|
+
import path18 from "path";
|
|
2266
2427
|
import { createHash } from "crypto";
|
|
2267
2428
|
import { z as z4 } from "zod";
|
|
2268
2429
|
async function loadDecisionStore(rootDir) {
|
|
@@ -2313,9 +2474,9 @@ var init_decisions = __esm({
|
|
|
2313
2474
|
});
|
|
2314
2475
|
|
|
2315
2476
|
// src/decisions/drift.ts
|
|
2316
|
-
import
|
|
2477
|
+
import path19 from "path";
|
|
2317
2478
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
2318
|
-
const resolvedRoot =
|
|
2479
|
+
const resolvedRoot = path19.resolve(rootDir);
|
|
2319
2480
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
2320
2481
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
2321
2482
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -2511,7 +2672,9 @@ var init_findings = __esm({
|
|
|
2511
2672
|
kind: z5.literal("doc-behind-manifests"),
|
|
2512
2673
|
docLastCommit: commitSchema,
|
|
2513
2674
|
manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
|
|
2514
|
-
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()
|
|
2515
2678
|
}),
|
|
2516
2679
|
z5.object({
|
|
2517
2680
|
kind: z5.literal("decision-anchor"),
|
|
@@ -2527,7 +2690,7 @@ var init_findings = __esm({
|
|
|
2527
2690
|
type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2528
2691
|
confidence: z5.enum(["certain", "likely"])
|
|
2529
2692
|
});
|
|
2530
|
-
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.
|
|
2693
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.enum(["recheck", "informational"]).optional() });
|
|
2531
2694
|
checkResultSchema = z5.object({
|
|
2532
2695
|
issues: z5.array(issueSchema),
|
|
2533
2696
|
advisories: z5.array(advisorySchema),
|
|
@@ -2545,14 +2708,18 @@ async function readRecord(root, id) {
|
|
|
2545
2708
|
if (raw === null) return null;
|
|
2546
2709
|
const record = recordSchema.parse(raw);
|
|
2547
2710
|
const { digest: stored, ...payload } = record;
|
|
2548
|
-
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))) {
|
|
2549
2712
|
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2550
2713
|
}
|
|
2551
2714
|
return record;
|
|
2552
2715
|
}
|
|
2553
|
-
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) {
|
|
2554
2720
|
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2555
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}`];
|
|
2556
2723
|
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
2557
2724
|
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
2558
2725
|
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
@@ -2563,9 +2730,9 @@ function scopes(finding) {
|
|
|
2563
2730
|
return `:(literal)${file}`;
|
|
2564
2731
|
}).sort();
|
|
2565
2732
|
}
|
|
2566
|
-
async function reviewEvidence(root, finding) {
|
|
2733
|
+
async function reviewEvidence(root, finding, reason2) {
|
|
2567
2734
|
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
2568
|
-
const scope = scopes(finding);
|
|
2735
|
+
const scope = scopes(finding, reason2);
|
|
2569
2736
|
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2570
2737
|
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2571
2738
|
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
@@ -2591,7 +2758,7 @@ async function reviewEvidence(root, finding) {
|
|
|
2591
2758
|
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2592
2759
|
}
|
|
2593
2760
|
}
|
|
2594
|
-
const fingerprint =
|
|
2761
|
+
const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason2);
|
|
2595
2762
|
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
2596
2763
|
return {
|
|
2597
2764
|
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
@@ -2638,7 +2805,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2638
2805
|
const record = await readRecord(root, id);
|
|
2639
2806
|
if (!record) return null;
|
|
2640
2807
|
const event = record.events.at(-1);
|
|
2641
|
-
const current = await reviewEvidence(root, event.finding);
|
|
2808
|
+
const current = await reviewEvidence(root, event.finding, event.reasonCode);
|
|
2642
2809
|
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
2643
2810
|
const base = {
|
|
2644
2811
|
id,
|
|
@@ -2648,7 +2815,7 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2648
2815
|
outcome: event.outcome,
|
|
2649
2816
|
reviewedHead: event.evidence.head
|
|
2650
2817
|
};
|
|
2651
|
-
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." };
|
|
2652
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." };
|
|
2653
2820
|
} catch (error) {
|
|
2654
2821
|
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -2665,7 +2832,7 @@ async function advisoryReviewInventory(root) {
|
|
|
2665
2832
|
if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
|
|
2666
2833
|
return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
|
|
2667
2834
|
}
|
|
2668
|
-
var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2835
|
+
var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2669
2836
|
var init_advisory_review = __esm({
|
|
2670
2837
|
"src/audit/advisory-review.ts"() {
|
|
2671
2838
|
"use strict";
|
|
@@ -2691,11 +2858,13 @@ var init_advisory_review = __esm({
|
|
|
2691
2858
|
lastScopeCommit: commitSchema2,
|
|
2692
2859
|
paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
|
|
2693
2860
|
});
|
|
2861
|
+
dismissalReasonSchema = z6.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
2694
2862
|
eventSchema2 = z6.object({
|
|
2695
2863
|
at: z6.string().datetime(),
|
|
2696
|
-
reviewer: z6.string().trim().min(1).max(200),
|
|
2864
|
+
reviewer: z6.string().trim().min(1).max(200).optional(),
|
|
2697
2865
|
note: z6.string().trim().min(1).max(2500),
|
|
2698
|
-
outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
|
|
2866
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
2867
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
2699
2868
|
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2700
2869
|
evidence: evidenceSchema2
|
|
2701
2870
|
});
|
|
@@ -2707,13 +2876,14 @@ var init_advisory_review = __esm({
|
|
|
2707
2876
|
recordPath: z6.string().optional(),
|
|
2708
2877
|
reviewer: z6.string().optional(),
|
|
2709
2878
|
note: z6.string().optional(),
|
|
2710
|
-
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
2879
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
|
|
2711
2880
|
reviewedHead: z6.string().optional()
|
|
2712
2881
|
});
|
|
2713
2882
|
advisoryReviewRequest = z6.object({
|
|
2714
2883
|
baselinePath: z6.string().min(1),
|
|
2715
2884
|
findingId: hashSchema,
|
|
2716
|
-
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
2885
|
+
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
2886
|
+
reasonCode: dismissalReasonSchema.optional(),
|
|
2717
2887
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2718
2888
|
note: eventSchema2.shape.note.optional(),
|
|
2719
2889
|
reviewToken: hashSchema.optional()
|
|
@@ -2724,12 +2894,12 @@ var init_advisory_review = __esm({
|
|
|
2724
2894
|
|
|
2725
2895
|
// src/audit/audit.ts
|
|
2726
2896
|
import fs11 from "fs/promises";
|
|
2727
|
-
import
|
|
2897
|
+
import path20 from "path";
|
|
2728
2898
|
async function computeAudit(rootDir, options = {}) {
|
|
2729
2899
|
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
2730
2900
|
}
|
|
2731
2901
|
async function auditCurrent(rootDir, options) {
|
|
2732
|
-
const resolvedRoot =
|
|
2902
|
+
const resolvedRoot = path20.resolve(rootDir);
|
|
2733
2903
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2734
2904
|
if (docs.length === 0) return null;
|
|
2735
2905
|
const report = {
|
|
@@ -2763,7 +2933,7 @@ async function auditCurrent(rootDir, options) {
|
|
|
2763
2933
|
}
|
|
2764
2934
|
let decisionsPresent = false;
|
|
2765
2935
|
try {
|
|
2766
|
-
await fs11.access(
|
|
2936
|
+
await fs11.access(path20.join(resolvedRoot, ".mason", "decisions"));
|
|
2767
2937
|
decisionsPresent = true;
|
|
2768
2938
|
} catch {
|
|
2769
2939
|
}
|
|
@@ -2775,13 +2945,21 @@ async function auditCurrent(rootDir, options) {
|
|
|
2775
2945
|
changesSinceDoc,
|
|
2776
2946
|
decisionsPresent
|
|
2777
2947
|
};
|
|
2948
|
+
const policy = (await loadProjectConfig(resolvedRoot)).audit;
|
|
2778
2949
|
const selected = options.checks ?? ALL_CHECKS;
|
|
2779
2950
|
for (const name of ALL_CHECKS) {
|
|
2780
2951
|
if (!selected.includes(name)) continue;
|
|
2781
|
-
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));
|
|
2782
2958
|
report.checksRun.push(name);
|
|
2783
2959
|
report.issues.push(...issues);
|
|
2784
|
-
|
|
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 });
|
|
2785
2963
|
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
2786
2964
|
report.skippedChecks.push(...skipped);
|
|
2787
2965
|
}
|
|
@@ -2793,6 +2971,8 @@ async function auditCurrent(rootDir, options) {
|
|
|
2793
2971
|
var init_audit = __esm({
|
|
2794
2972
|
"src/audit/audit.ts"() {
|
|
2795
2973
|
"use strict";
|
|
2974
|
+
init_files();
|
|
2975
|
+
init_policy();
|
|
2796
2976
|
init_profile();
|
|
2797
2977
|
init_drift();
|
|
2798
2978
|
init_snapshot();
|
|
@@ -2805,7 +2985,7 @@ var init_audit = __esm({
|
|
|
2805
2985
|
|
|
2806
2986
|
// src/audit/repair.ts
|
|
2807
2987
|
import fs12 from "fs/promises";
|
|
2808
|
-
import
|
|
2988
|
+
import path21 from "path";
|
|
2809
2989
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2810
2990
|
import { z as z7 } from "zod";
|
|
2811
2991
|
function recheckable(finding) {
|
|
@@ -2826,7 +3006,7 @@ async function docState(root) {
|
|
|
2826
3006
|
docs.push([doc, null]);
|
|
2827
3007
|
}
|
|
2828
3008
|
}
|
|
2829
|
-
return digest(docs);
|
|
3009
|
+
return digest([docs, (await loadProjectConfig(root)).audit]);
|
|
2830
3010
|
}
|
|
2831
3011
|
async function stableAudit(root, checks, options = {}) {
|
|
2832
3012
|
return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
|
|
@@ -2892,8 +3072,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
|
|
|
2892
3072
|
}
|
|
2893
3073
|
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
2894
3074
|
const root = await fs12.realpath(rootDir);
|
|
2895
|
-
const declaredRoot =
|
|
2896
|
-
const relative =
|
|
3075
|
+
const declaredRoot = path21.resolve(rootDir);
|
|
3076
|
+
const relative = path21.isAbsolute(baselinePath) ? path21.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2897
3077
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
2898
3078
|
const { digest: savedDigest, ...payload } = stored;
|
|
2899
3079
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
@@ -2939,6 +3119,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2939
3119
|
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
2940
3120
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
2941
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
|
+
}
|
|
2942
3125
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
2943
3126
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
2944
3127
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
@@ -2949,11 +3132,14 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2949
3132
|
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
2950
3133
|
return { ...base, review, status: "review-required", reason: review.reason };
|
|
2951
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
|
+
}
|
|
2952
3138
|
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
2953
3139
|
return {
|
|
2954
3140
|
...base,
|
|
2955
3141
|
status: "review-required",
|
|
2956
|
-
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."
|
|
3142
|
+
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."
|
|
2957
3143
|
};
|
|
2958
3144
|
}
|
|
2959
3145
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
@@ -2977,7 +3163,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2977
3163
|
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
2978
3164
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
2979
3165
|
for (const f of findings) counts[f.status]++;
|
|
2980
|
-
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)));
|
|
2981
3167
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
2982
3168
|
return {
|
|
2983
3169
|
version: 1,
|
|
@@ -3020,13 +3206,14 @@ var init_repair = __esm({
|
|
|
3020
3206
|
docs: z7.array(z7.object({
|
|
3021
3207
|
path: z7.string().refine((value) => normalizeRepoPath(value) === value),
|
|
3022
3208
|
scope: z7.string().optional(),
|
|
3023
|
-
kind: z7.enum(["instructions", "readme"]).optional(),
|
|
3209
|
+
kind: z7.enum(["instructions", "readme", "guide"]).optional(),
|
|
3024
3210
|
lastCommit: commitSchema.nullable(),
|
|
3025
3211
|
dirty: z7.boolean(),
|
|
3026
3212
|
lineCount: count2
|
|
3027
3213
|
})).nonempty(),
|
|
3028
3214
|
decisionsChecked: z7.boolean(),
|
|
3029
3215
|
clean: z7.boolean(),
|
|
3216
|
+
excludedChecks: z7.array(z7.object({ check: checkSchema, doc: z7.string() })).optional(),
|
|
3030
3217
|
issues: z7.array(issueSchema),
|
|
3031
3218
|
advisories: z7.array(advisorySchema),
|
|
3032
3219
|
suppressedAdvisories: z7.array(advisorySchema).optional(),
|
|
@@ -3052,9 +3239,9 @@ var init_repair = __esm({
|
|
|
3052
3239
|
|
|
3053
3240
|
// src/automation/evidence.ts
|
|
3054
3241
|
import fs13 from "fs/promises";
|
|
3055
|
-
import
|
|
3242
|
+
import path22 from "path";
|
|
3056
3243
|
import { createHash as createHash3 } from "crypto";
|
|
3057
|
-
import
|
|
3244
|
+
import fg7 from "fast-glob";
|
|
3058
3245
|
import { z as z8 } from "zod";
|
|
3059
3246
|
async function git3(root, ...args) {
|
|
3060
3247
|
return profilePhase("automation.git", async () => (await execGit(args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
|
|
@@ -3072,6 +3259,11 @@ async function readInputs(root) {
|
|
|
3072
3259
|
}
|
|
3073
3260
|
async function collectInputs(root) {
|
|
3074
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
|
+
}
|
|
3075
3267
|
const [headText, docStatus, shallowPath, replacements] = await Promise.all([
|
|
3076
3268
|
git3(root, "rev-parse", "HEAD"),
|
|
3077
3269
|
docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
|
|
@@ -3081,7 +3273,7 @@ async function collectInputs(root) {
|
|
|
3081
3273
|
const head = headText.trim();
|
|
3082
3274
|
let shallow = null;
|
|
3083
3275
|
try {
|
|
3084
|
-
shallow = await fs13.readFile(
|
|
3276
|
+
shallow = await fs13.readFile(path22.resolve(root, shallowPath.trim()), "utf8");
|
|
3085
3277
|
} catch (error) {
|
|
3086
3278
|
if (error.code !== "ENOENT") throw error;
|
|
3087
3279
|
}
|
|
@@ -3100,18 +3292,18 @@ async function collectInputs(root) {
|
|
|
3100
3292
|
docContents.push([file, text2]);
|
|
3101
3293
|
const parsed = { path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") };
|
|
3102
3294
|
parsedDocs.push(parsed);
|
|
3103
|
-
for (const claim of parsed.claims.paths) {
|
|
3295
|
+
for (const claim of allowed.get("deleted-reference").has(file) ? parsed.claims.paths : []) {
|
|
3104
3296
|
const scope = pathClaimScope(file, claim);
|
|
3105
3297
|
if (scope?.candidates.some(gitMetadataPath)) continue;
|
|
3106
3298
|
for (const candidate of scope?.candidates ?? []) {
|
|
3107
3299
|
if (optionalMasonPath(candidate)) continue;
|
|
3108
|
-
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(
|
|
3300
|
+
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path22.posix.dirname(candidate))]);
|
|
3109
3301
|
claims.push([candidate, filePresent, parentPresent]);
|
|
3110
3302
|
}
|
|
3111
3303
|
}
|
|
3112
3304
|
}
|
|
3113
|
-
const combinedDocs = moduleDocumentation(parsedDocs);
|
|
3114
|
-
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 })));
|
|
3115
3307
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
3116
3308
|
const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
3117
3309
|
if (error.code === "ENOENT") return "absent";
|
|
@@ -3120,8 +3312,8 @@ async function collectInputs(root) {
|
|
|
3120
3312
|
const [modules, counts, commands2, decisionFiles] = await Promise.all([
|
|
3121
3313
|
combinedDocs ? moduleCandidates(root, combinedDocs) : [],
|
|
3122
3314
|
Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
|
|
3123
|
-
commandInputs(root, parsedDocs),
|
|
3124
|
-
|
|
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 })
|
|
3125
3317
|
]);
|
|
3126
3318
|
const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
|
|
3127
3319
|
await storePath(root, file);
|
|
@@ -3131,7 +3323,7 @@ async function collectInputs(root) {
|
|
|
3131
3323
|
git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
3132
3324
|
git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
3133
3325
|
]) : ["", ""];
|
|
3134
|
-
const common = [
|
|
3326
|
+
const common = [7, engineVersion, head, shallow, replacements, docContents, auditPolicy];
|
|
3135
3327
|
const keys = {
|
|
3136
3328
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
3137
3329
|
"new-module": hash([common, modules]),
|
|
@@ -3178,6 +3370,8 @@ var engineVersion, hash, content, cacheSchema;
|
|
|
3178
3370
|
var init_evidence = __esm({
|
|
3179
3371
|
"src/automation/evidence.ts"() {
|
|
3180
3372
|
"use strict";
|
|
3373
|
+
init_policy();
|
|
3374
|
+
init_files();
|
|
3181
3375
|
init_git_read();
|
|
3182
3376
|
init_profile();
|
|
3183
3377
|
init_docs();
|
|
@@ -3191,7 +3385,7 @@ var init_evidence = __esm({
|
|
|
3191
3385
|
init_dead_command();
|
|
3192
3386
|
init_storage();
|
|
3193
3387
|
init_advisory_review();
|
|
3194
|
-
engineVersion = true ? "0.
|
|
3388
|
+
engineVersion = true ? "0.19.1" : "development";
|
|
3195
3389
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3196
3390
|
content = readAuditInput;
|
|
3197
3391
|
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
@@ -3405,13 +3599,17 @@ async function observeReadOnlyTool(dir, event) {
|
|
|
3405
3599
|
}
|
|
3406
3600
|
function summarize(report) {
|
|
3407
3601
|
const open = report.findings.filter((f) => f.status !== "resolved");
|
|
3602
|
+
const onlyReview = report.status === "incomplete" && !report.diagnostics.length && !report.counts.unresolved && !report.counts.unverified && report.counts["review-required"] > 0;
|
|
3408
3603
|
return [
|
|
3409
|
-
`Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
|
|
3410
|
-
...open.slice(0, 4).map((f) =>
|
|
3604
|
+
onlyReview ? `Mason: ${report.counts["review-required"]} advisor${report.counts["review-required"] === 1 ? "y" : "ies"} awaiting review; no unresolved issues.` : `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
|
|
3605
|
+
...open.slice(0, 4).map((f) => {
|
|
3606
|
+
const detail = f.current ? f.current.message : f.status === "review-required" ? `${f.original.type}: current check no longer reports ${f.original.type === "deps-changed" ? "this condition" : "the original finding"}; historical evidence awaits assessment.` : `Original evidence: ${f.original.message}. ${f.reason}`;
|
|
3607
|
+
return `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(detail)}`;
|
|
3608
|
+
}),
|
|
3411
3609
|
...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
|
|
3412
3610
|
...report.diagnostics.slice(0, 2).map(cleanText),
|
|
3413
3611
|
`Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason-auto check.`,
|
|
3414
|
-
"Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and
|
|
3612
|
+
"Keep original evidence. Address findings relevant to the authorized task; report unrelated findings with a brief scope reason and suggested follow-up. Use review_advisory for authorized assessments; otherwise leave advisories open without approving them."
|
|
3415
3613
|
].join("\n");
|
|
3416
3614
|
}
|
|
3417
3615
|
async function loadState(ws) {
|
|
@@ -3635,7 +3833,7 @@ async function automate(dir, event) {
|
|
|
3635
3833
|
if (event.event === "after_tool" && event.mutating) {
|
|
3636
3834
|
session.mutationObserved = true;
|
|
3637
3835
|
if (!event.toolId || !session.pending[event.toolId]) {
|
|
3638
|
-
const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
|
|
3836
|
+
const gap = "A tool completed without an observed matching pre-tool capture in this checkout; pre-edit coverage is unknown. The tool may have changed checkouts; captures are kept separately for each worktree.";
|
|
3639
3837
|
if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
|
|
3640
3838
|
}
|
|
3641
3839
|
if (event.toolId) delete session.pending[event.toolId];
|
|
@@ -3745,9 +3943,9 @@ var init_runtime = __esm({
|
|
|
3745
3943
|
|
|
3746
3944
|
// src/automation/hook-root.ts
|
|
3747
3945
|
import fs15 from "fs/promises";
|
|
3748
|
-
import
|
|
3946
|
+
import path23 from "path";
|
|
3749
3947
|
async function repositoryRoot(dir) {
|
|
3750
|
-
if (!
|
|
3948
|
+
if (!path23.isAbsolute(dir)) throw new Error("Hook input working directory must be absolute.");
|
|
3751
3949
|
try {
|
|
3752
3950
|
return await fs15.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
|
|
3753
3951
|
} catch (error) {
|
|
@@ -3778,11 +3976,39 @@ var init_hook_root = __esm({
|
|
|
3778
3976
|
|
|
3779
3977
|
// src/setup/model.ts
|
|
3780
3978
|
import { z as z11 } from "zod";
|
|
3979
|
+
import fs16 from "fs/promises";
|
|
3980
|
+
import path24 from "path";
|
|
3781
3981
|
async function loadSetup(root) {
|
|
3782
3982
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
3783
3983
|
if (raw === null) return null;
|
|
3784
3984
|
return setupSchema.parse(raw);
|
|
3785
3985
|
}
|
|
3986
|
+
async function effectiveSetup(root) {
|
|
3987
|
+
const local = await loadSetup(root);
|
|
3988
|
+
if (local) return { root, setup: local };
|
|
3989
|
+
const common = await fs16.realpath(path24.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
|
|
3990
|
+
const own = await fs16.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
3991
|
+
if (common === own) return { root, setup: null };
|
|
3992
|
+
const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
|
|
3993
|
+
const candidate = primary.find((field) => field.startsWith("worktree "))?.slice(9);
|
|
3994
|
+
if (!candidate || primary.includes("bare")) return { root, setup: null };
|
|
3995
|
+
let inheritedRoot;
|
|
3996
|
+
try {
|
|
3997
|
+
inheritedRoot = await fs16.realpath((await git3(candidate, "rev-parse", "--show-toplevel")).trim());
|
|
3998
|
+
} catch (error) {
|
|
3999
|
+
if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
|
|
4000
|
+
throw error;
|
|
4001
|
+
}
|
|
4002
|
+
const inheritedCommon = await fs16.realpath(path24.resolve(
|
|
4003
|
+
inheritedRoot,
|
|
4004
|
+
(await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
|
|
4005
|
+
));
|
|
4006
|
+
if (inheritedCommon !== common) throw new Error("Linked worktree setup does not belong to the same Git repository.");
|
|
4007
|
+
const setup = await loadSetup(inheritedRoot);
|
|
4008
|
+
const inactive = await readStoreJson(root, INACTIVE_HOSTS_PATH);
|
|
4009
|
+
if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
|
|
4010
|
+
return { root: inheritedRoot, setup };
|
|
4011
|
+
}
|
|
3786
4012
|
async function loadSetupReceipt(root, directory, host) {
|
|
3787
4013
|
const raw = await readStoreJson(root, directory + "/setup-" + host + ".json");
|
|
3788
4014
|
if (raw === null) return null;
|
|
@@ -3790,10 +4016,11 @@ async function loadSetupReceipt(root, directory, host) {
|
|
|
3790
4016
|
if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
|
|
3791
4017
|
return receipt;
|
|
3792
4018
|
}
|
|
3793
|
-
var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
|
|
4019
|
+
var setupHostSchema, setupSchema, SETUP_PATH, INACTIVE_HOSTS_PATH, inactiveHostsSchema, receiptSchema;
|
|
3794
4020
|
var init_model = __esm({
|
|
3795
4021
|
"src/setup/model.ts"() {
|
|
3796
4022
|
"use strict";
|
|
4023
|
+
init_evidence();
|
|
3797
4024
|
init_storage();
|
|
3798
4025
|
setupHostSchema = z11.object({
|
|
3799
4026
|
configuredVersion: z11.string(),
|
|
@@ -3807,6 +4034,8 @@ var init_model = __esm({
|
|
|
3807
4034
|
claude: setupHostSchema.optional()
|
|
3808
4035
|
}) });
|
|
3809
4036
|
SETUP_PATH = ".mason/local/setup.json";
|
|
4037
|
+
INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
|
|
4038
|
+
inactiveHostsSchema = z11.object({ version: z11.literal(1), hosts: z11.array(z11.enum(["claude", "codex"])) });
|
|
3810
4039
|
receiptSchema = z11.object({
|
|
3811
4040
|
version: z11.literal(1),
|
|
3812
4041
|
host: z11.enum(["codex", "claude"]),
|
|
@@ -3833,7 +4062,7 @@ __export(launcher_exports, {
|
|
|
3833
4062
|
});
|
|
3834
4063
|
import { execFile as execFile2 } from "child_process";
|
|
3835
4064
|
import { promisify as promisify2 } from "util";
|
|
3836
|
-
import
|
|
4065
|
+
import fs17 from "fs/promises";
|
|
3837
4066
|
function globalCommand(args, platform = process.platform) {
|
|
3838
4067
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
|
|
3839
4068
|
}
|
|
@@ -3853,8 +4082,8 @@ async function installedCommand() {
|
|
|
3853
4082
|
}
|
|
3854
4083
|
}
|
|
3855
4084
|
async function prepareLaunch(host, options = {}) {
|
|
3856
|
-
const root = await
|
|
3857
|
-
const entry = (await
|
|
4085
|
+
const root = await fs17.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
|
|
4086
|
+
const entry = (await effectiveSetup(root)).setup?.hosts[host];
|
|
3858
4087
|
process.env.MASON_SETUP_ROOT = root;
|
|
3859
4088
|
process.env.MASON_SETUP_HOST = host;
|
|
3860
4089
|
delete process.env.MASON_SETUP_REVISION;
|
|
@@ -3876,7 +4105,7 @@ var init_launcher = __esm({
|
|
|
3876
4105
|
"use strict";
|
|
3877
4106
|
init_evidence();
|
|
3878
4107
|
init_model();
|
|
3879
|
-
executingVersion = () => true ? "0.
|
|
4108
|
+
executingVersion = () => true ? "0.19.1" : "development";
|
|
3880
4109
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3881
4110
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3882
4111
|
}
|
|
@@ -3889,7 +4118,7 @@ __export(observations_exports, {
|
|
|
3889
4118
|
observeActivation: () => observeActivation,
|
|
3890
4119
|
readObservation: () => readObservation
|
|
3891
4120
|
});
|
|
3892
|
-
import
|
|
4121
|
+
import fs18 from "fs/promises";
|
|
3893
4122
|
import { z as z12 } from "zod";
|
|
3894
4123
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
3895
4124
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
@@ -3906,15 +4135,15 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
3906
4135
|
if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
|
|
3907
4136
|
try {
|
|
3908
4137
|
if (!revision) {
|
|
3909
|
-
if ((await
|
|
4138
|
+
if ((await effectiveSetup(process.env.MASON_SETUP_ROOT)).setup?.hosts[host]) {
|
|
3910
4139
|
return "Mason setup is now present. Restart the assistant to connect this MCP session to the configured integration; activation has not been established by this session.";
|
|
3911
4140
|
}
|
|
3912
4141
|
return inactiveSetupMessage();
|
|
3913
4142
|
}
|
|
3914
4143
|
const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
|
|
3915
|
-
const ws = capturedDirectory ? { root: await
|
|
3916
|
-
if (ws.root !== await
|
|
3917
|
-
const setup = await
|
|
4144
|
+
const ws = capturedDirectory ? { root: await fs18.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
4145
|
+
if (ws.root !== await fs18.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
4146
|
+
const { setup } = await effectiveSetup(ws.root);
|
|
3918
4147
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
3919
4148
|
const directory = ws.directory + "/activation";
|
|
3920
4149
|
await withLock(ws.root, directory, async () => {
|
|
@@ -4091,8 +4320,8 @@ __export(files_exports, {
|
|
|
4091
4320
|
managedBlock: () => managedBlock,
|
|
4092
4321
|
readText: () => readText
|
|
4093
4322
|
});
|
|
4094
|
-
import
|
|
4095
|
-
import
|
|
4323
|
+
import fs19 from "fs/promises";
|
|
4324
|
+
import path25 from "path";
|
|
4096
4325
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
4097
4326
|
async function readText(root, file) {
|
|
4098
4327
|
try {
|
|
@@ -4121,14 +4350,14 @@ async function applyEdit(root, edit) {
|
|
|
4121
4350
|
if (edit.after === null) {
|
|
4122
4351
|
const file2 = await storePath(root, edit.path);
|
|
4123
4352
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
4124
|
-
await
|
|
4353
|
+
await fs19.unlink(file2);
|
|
4125
4354
|
return true;
|
|
4126
4355
|
}
|
|
4127
4356
|
const file = await storePath(root, edit.path, true);
|
|
4128
|
-
const temporary =
|
|
4357
|
+
const temporary = path25.join(path25.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
4129
4358
|
try {
|
|
4130
|
-
const mode = await
|
|
4131
|
-
const handle = await
|
|
4359
|
+
const mode = await fs19.stat(file).then((s) => s.mode & 511, () => 384);
|
|
4360
|
+
const handle = await fs19.open(temporary, "wx", mode);
|
|
4132
4361
|
try {
|
|
4133
4362
|
await handle.writeFile(edit.after, "utf8");
|
|
4134
4363
|
await handle.sync();
|
|
@@ -4136,10 +4365,10 @@ async function applyEdit(root, edit) {
|
|
|
4136
4365
|
await handle.close();
|
|
4137
4366
|
}
|
|
4138
4367
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
4139
|
-
await
|
|
4368
|
+
await fs19.rename(temporary, file);
|
|
4140
4369
|
return true;
|
|
4141
4370
|
} finally {
|
|
4142
|
-
await
|
|
4371
|
+
await fs19.rm(temporary, { force: true });
|
|
4143
4372
|
}
|
|
4144
4373
|
}
|
|
4145
4374
|
var init_files2 = __esm({
|
|
@@ -4250,9 +4479,11 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
4250
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.
|
|
4251
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.
|
|
4252
4481
|
|
|
4253
|
-
- 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.
|
|
4254
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.
|
|
4255
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
|
+
|
|
4256
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.
|
|
4257
4488
|
|
|
4258
4489
|
Inspect source for what the retrieved context does not answer.
|
|
@@ -4547,8 +4778,10 @@ __export(teardown_exports, {
|
|
|
4547
4778
|
teardownProject: () => teardownProject
|
|
4548
4779
|
});
|
|
4549
4780
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
4781
|
+
import fs20 from "fs/promises";
|
|
4782
|
+
import path26 from "path";
|
|
4550
4783
|
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
4551
|
-
import
|
|
4784
|
+
import fg8 from "fast-glob";
|
|
4552
4785
|
import { z as z18 } from "zod";
|
|
4553
4786
|
async function planTeardown(root, selected) {
|
|
4554
4787
|
const edits = [], diagnostics = [];
|
|
@@ -4568,6 +4801,11 @@ async function planTeardown(root, selected) {
|
|
|
4568
4801
|
const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
|
|
4569
4802
|
const setupBefore = await read(SETUP_PATH);
|
|
4570
4803
|
const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
|
|
4804
|
+
const linked = (await workspace(root)).gitDir !== await fs20.realpath(
|
|
4805
|
+
path26.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim())
|
|
4806
|
+
);
|
|
4807
|
+
const inactiveBefore = linked && !setup ? await read(INACTIVE_HOSTS_PATH) : null;
|
|
4808
|
+
const inactive = inactiveBefore === null ? { version: 1, hosts: [] } : inactiveHostsSchema.parse(JSON.parse(inactiveBefore));
|
|
4571
4809
|
const automationBefore = await read(AUTOMATION_PATH);
|
|
4572
4810
|
const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
|
|
4573
4811
|
const commands2 = (host) => new Set([
|
|
@@ -4687,7 +4925,7 @@ async function planTeardown(root, selected) {
|
|
|
4687
4925
|
if (diagnostics.length) edits.splice(instructionEditStart);
|
|
4688
4926
|
if (!diagnostics.length) {
|
|
4689
4927
|
const cleanup = [];
|
|
4690
|
-
const receiptFiles = await
|
|
4928
|
+
const receiptFiles = await fg8(
|
|
4691
4929
|
selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
|
|
4692
4930
|
{ cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
|
|
4693
4931
|
);
|
|
@@ -4711,7 +4949,16 @@ async function planTeardown(root, selected) {
|
|
|
4711
4949
|
delete ownership.files[configPath(host)];
|
|
4712
4950
|
}
|
|
4713
4951
|
if (!remainingHost) for (const file of DOC_CANDIDATES) delete ownership.files[file];
|
|
4714
|
-
if (setupBefore !== null) cleanup.push({
|
|
4952
|
+
if (setupBefore !== null) cleanup.push({
|
|
4953
|
+
path: SETUP_PATH,
|
|
4954
|
+
before: setupBefore,
|
|
4955
|
+
after: linked || Object.keys(setup.hosts).length ? json(setup) : null
|
|
4956
|
+
});
|
|
4957
|
+
else if (linked) cleanup.push({
|
|
4958
|
+
path: INACTIVE_HOSTS_PATH,
|
|
4959
|
+
before: inactiveBefore,
|
|
4960
|
+
after: json({ version: 1, hosts: [.../* @__PURE__ */ new Set([...inactive.hosts, ...selected])].sort() })
|
|
4961
|
+
});
|
|
4715
4962
|
if (automationBefore !== null) cleanup.push({ path: AUTOMATION_PATH, before: automationBefore, after: Object.keys(automation.hosts).length ? json(automation) : null });
|
|
4716
4963
|
if (ownershipBefore !== null) cleanup.push({ path: OWNERSHIP_PATH, before: ownershipBefore, after: Object.keys(ownership.files).length ? json(ownership) : null });
|
|
4717
4964
|
edits.push(...cleanup);
|
|
@@ -5152,10 +5399,10 @@ var init_check = __esm({
|
|
|
5152
5399
|
});
|
|
5153
5400
|
|
|
5154
5401
|
// src/review/evidence.ts
|
|
5155
|
-
import
|
|
5402
|
+
import path27 from "path";
|
|
5156
5403
|
import { z as z22 } from "zod";
|
|
5157
5404
|
function relativeArtifact(root, file) {
|
|
5158
|
-
const relative =
|
|
5405
|
+
const relative = path27.isAbsolute(file) ? path27.relative(root, file) : file;
|
|
5159
5406
|
const normalized = normalizeRepoPath(relative);
|
|
5160
5407
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
5161
5408
|
return normalized;
|
|
@@ -5252,7 +5499,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
5252
5499
|
const report = await readStoreJson(root, file);
|
|
5253
5500
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
5254
5501
|
const sourceRoot = check.sourceRoot ?? root;
|
|
5255
|
-
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.");
|
|
5256
5503
|
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
5257
5504
|
result.outcome = parsed.outcome;
|
|
5258
5505
|
result.counts = parsed.counts;
|
|
@@ -5373,8 +5620,8 @@ var init_evidence2 = __esm({
|
|
|
5373
5620
|
});
|
|
5374
5621
|
|
|
5375
5622
|
// src/review/review.ts
|
|
5376
|
-
import
|
|
5377
|
-
import
|
|
5623
|
+
import fs21 from "fs/promises";
|
|
5624
|
+
import path28 from "path";
|
|
5378
5625
|
import { execFile as execFile4 } from "child_process";
|
|
5379
5626
|
import { promisify as promisify4 } from "util";
|
|
5380
5627
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
@@ -5403,7 +5650,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
5403
5650
|
return matchingPaths(record.files, changedFiles);
|
|
5404
5651
|
}
|
|
5405
5652
|
async function computeReview(rootDir, base, options = {}) {
|
|
5406
|
-
const resolvedRoot =
|
|
5653
|
+
const resolvedRoot = path28.resolve(rootDir);
|
|
5407
5654
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
5408
5655
|
if (head === "unknown") return null;
|
|
5409
5656
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -5453,7 +5700,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
5453
5700
|
analyzed,
|
|
5454
5701
|
async (relPath) => {
|
|
5455
5702
|
try {
|
|
5456
|
-
await
|
|
5703
|
+
await fs21.access(path28.join(resolvedRoot, relPath));
|
|
5457
5704
|
return true;
|
|
5458
5705
|
} catch {
|
|
5459
5706
|
return false;
|
|
@@ -5596,8 +5843,9 @@ __export(status_exports, {
|
|
|
5596
5843
|
import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
|
|
5597
5844
|
async function setupStatus(dir) {
|
|
5598
5845
|
const ws = await workspace(dir);
|
|
5599
|
-
const
|
|
5600
|
-
|
|
5846
|
+
const activation = await effectiveSetup(ws.root);
|
|
5847
|
+
const setup = activation.setup;
|
|
5848
|
+
if (!setup || !Object.keys(setup.hosts).length) {
|
|
5601
5849
|
const partial = await Promise.all(["codex", "claude"].map(async (host) => ({
|
|
5602
5850
|
host,
|
|
5603
5851
|
receipt: await loadSetupReceipt(ws.root, ws.directory, host)
|
|
@@ -5631,7 +5879,7 @@ async function setupStatus(dir) {
|
|
|
5631
5879
|
]));
|
|
5632
5880
|
const hooks = isDeepStrictEqual3(compatibleHooks, expectedHooks) && !config.disabled;
|
|
5633
5881
|
const local = await loadSetupReceipt(ws.root, ws.directory, host);
|
|
5634
|
-
const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
|
|
5882
|
+
const configured = activation.root !== ws.root || local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
|
|
5635
5883
|
const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
|
|
5636
5884
|
const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
|
|
5637
5885
|
const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
|
|
@@ -5663,6 +5911,7 @@ async function setupStatus(dir) {
|
|
|
5663
5911
|
status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
|
|
5664
5912
|
root: ws.root,
|
|
5665
5913
|
hosts: hosts2,
|
|
5914
|
+
activationRoot: activation.root,
|
|
5666
5915
|
decisionRecords: decisions.records.length,
|
|
5667
5916
|
diagnostics: decisions.diagnostics,
|
|
5668
5917
|
scope: "Activation receipts record observed use after setup, not complete interception, correct repairs, or measured usefulness. Host trust and higher-priority settings may prevent execution. Receipts are local to this worktree and branch."
|
|
@@ -5670,6 +5919,9 @@ async function setupStatus(dir) {
|
|
|
5670
5919
|
}
|
|
5671
5920
|
function summarizeActivation(status) {
|
|
5672
5921
|
const lines = ["Mason setup: " + status.status + "."];
|
|
5922
|
+
if (status.activationRoot && status.activationRoot !== status.root) {
|
|
5923
|
+
lines.push("Activation inherited from " + status.activationRoot + "; evidence belongs to this worktree.");
|
|
5924
|
+
}
|
|
5673
5925
|
for (const [host, state] of Object.entries(status.hosts)) {
|
|
5674
5926
|
lines.push(
|
|
5675
5927
|
`${host}: ${state.status}`,
|