mason-context 0.16.3 → 0.17.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 +26 -0
- package/README.md +1 -1
- package/dist/mason-audit.js +1728 -770
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +1434 -822
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +3549 -2865
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +80 -19
- package/dist/mason-review.js.map +1 -1
- package/dist/mason.js +1543 -946
- package/dist/mason.js.map +1 -1
- package/package.json +5 -1
package/dist/mason.js
CHANGED
|
@@ -346,6 +346,89 @@ var init_store = __esm({
|
|
|
346
346
|
}
|
|
347
347
|
});
|
|
348
348
|
|
|
349
|
+
// src/audit/commands.ts
|
|
350
|
+
import path4 from "path";
|
|
351
|
+
function splitCommands(line) {
|
|
352
|
+
const parts = [];
|
|
353
|
+
let quote2 = "", start = 0;
|
|
354
|
+
for (let i = 0; i < line.length; i++) {
|
|
355
|
+
const char = line[i];
|
|
356
|
+
if (char === "\\") {
|
|
357
|
+
i++;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (quote2) {
|
|
361
|
+
if (char === quote2) quote2 = "";
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (char === "'" || char === '"') {
|
|
365
|
+
quote2 = char;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (char === ";" || line.slice(i, i + 2) === "&&") {
|
|
369
|
+
parts.push(line.slice(start, i));
|
|
370
|
+
if (char === "&") i++;
|
|
371
|
+
start = i + 1;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
parts.push(line.slice(start));
|
|
375
|
+
return parts;
|
|
376
|
+
}
|
|
377
|
+
function commandClaims(line, lineNo, cwd, shell = false) {
|
|
378
|
+
const claims = [];
|
|
379
|
+
const segments = splitCommands(line);
|
|
380
|
+
for (const segment of segments) {
|
|
381
|
+
const text2 = segment.trim().replace(/^\$\s+/, "");
|
|
382
|
+
if (text2.startsWith("#") || /^(?:echo|printf)\b/.test(text2)) continue;
|
|
383
|
+
const cd = text2.match(/^cd\s+(.+)$/);
|
|
384
|
+
if (cd) {
|
|
385
|
+
const directory = literal(cd[1]);
|
|
386
|
+
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path4.posix.join(cwd ?? ".", directory);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
for (const match of text2.matchAll(COMMAND)) {
|
|
390
|
+
const flags = match[2].trim();
|
|
391
|
+
const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
|
|
392
|
+
let directory = cwd;
|
|
393
|
+
let scopeUnknown;
|
|
394
|
+
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
395
|
+
if (selector) {
|
|
396
|
+
const selected = literal(selector[1]);
|
|
397
|
+
directory = selected === null || cwd === null ? null : path4.posix.join(cwd ?? ".", selected);
|
|
398
|
+
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
399
|
+
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
400
|
+
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
401
|
+
scopeUnknown = "Package selection or conditional options after the script need review.";
|
|
402
|
+
}
|
|
403
|
+
if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
|
|
404
|
+
scopeUnknown = "The command's working directory or shell control flow could not be resolved.";
|
|
405
|
+
}
|
|
406
|
+
claims.push({
|
|
407
|
+
scriptName: match[3],
|
|
408
|
+
invocation: match[0],
|
|
409
|
+
excerpt: match[0],
|
|
410
|
+
line: lineNo,
|
|
411
|
+
...directory !== void 0 && directory !== null ? { directory } : {},
|
|
412
|
+
...scopeUnknown ? { scopeUnknown } : {}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (/\b(?:cd|pushd|popd)\b/.test(text2)) cwd = null;
|
|
416
|
+
}
|
|
417
|
+
return { claims, cwd };
|
|
418
|
+
}
|
|
419
|
+
var TOKEN, COMMAND, literal;
|
|
420
|
+
var init_commands = __esm({
|
|
421
|
+
"src/audit/commands.ts"() {
|
|
422
|
+
"use strict";
|
|
423
|
+
TOKEN = `(?:"[^"\\n]*"|'[^'\\n]*'|[^\\s\`;&|]+)`;
|
|
424
|
+
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
425
|
+
literal = (value) => {
|
|
426
|
+
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
427
|
+
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path4.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
349
432
|
// src/audit/tree.ts
|
|
350
433
|
function glyphIndex(line) {
|
|
351
434
|
for (const glyph of GLYPHS) {
|
|
@@ -425,6 +508,7 @@ var init_tree = __esm({
|
|
|
425
508
|
});
|
|
426
509
|
|
|
427
510
|
// src/audit/claims.ts
|
|
511
|
+
import MarkdownIt from "markdown-it";
|
|
428
512
|
function normalizePathToken(token) {
|
|
429
513
|
let t = token.trim();
|
|
430
514
|
if (!t) return null;
|
|
@@ -488,11 +572,34 @@ function extractClaims(content2) {
|
|
|
488
572
|
const counts = [];
|
|
489
573
|
const commands = /* @__PURE__ */ new Map();
|
|
490
574
|
const addPath = (claim) => {
|
|
491
|
-
|
|
575
|
+
const key = [claim.relativeTo ?? "scope", claim.path].join(":");
|
|
576
|
+
if (!paths.has(key)) paths.set(key, claim);
|
|
492
577
|
};
|
|
493
578
|
const addCommand = (claim) => {
|
|
494
|
-
|
|
579
|
+
const key = JSON.stringify([claim.scriptName, claim.directory, claim.scopeUnknown]);
|
|
580
|
+
if (!commands.has(key)) commands.set(key, claim);
|
|
495
581
|
};
|
|
582
|
+
const parsed = markdown.parse(lines.map((line, i) => ignored[i] ? "" : line).join("\n"), {});
|
|
583
|
+
for (const token of parsed) {
|
|
584
|
+
if (token.type !== "inline" || !token.map) continue;
|
|
585
|
+
let line = token.map[0] + 1;
|
|
586
|
+
for (const child of token.children ?? []) {
|
|
587
|
+
const target = child.type === "link_open" ? child.attrGet("href") : child.type === "image" ? child.attrGet("src") : null;
|
|
588
|
+
if (target && !/^(?:[a-z][a-z0-9+.-]*:|[\/#~])/i.test(target)) {
|
|
589
|
+
let pathname = null;
|
|
590
|
+
try {
|
|
591
|
+
pathname = decodeURIComponent(target.split(/[?#]/)[0]);
|
|
592
|
+
} catch {
|
|
593
|
+
}
|
|
594
|
+
if (pathname && !/[$*?{}<>\\\x00-\x1f]/.test(pathname)) {
|
|
595
|
+
addPath({ path: pathname, line, excerpt: target, relativeTo: "document" });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (child.type === "softbreak" || child.type === "hardbreak") line++;
|
|
599
|
+
else line += child.content.split("\n").length - 1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
let shellCwd;
|
|
496
603
|
let inFence = false;
|
|
497
604
|
let fenceInfo = "";
|
|
498
605
|
let fenceMarker = "";
|
|
@@ -523,6 +630,7 @@ function extractClaims(content2) {
|
|
|
523
630
|
fenceMarker = fenceMatch[1][0];
|
|
524
631
|
fenceInfo = fenceMatch[2].trim().toLowerCase();
|
|
525
632
|
blockLines = [];
|
|
633
|
+
shellCwd = void 0;
|
|
526
634
|
blockStartLine = lineNo + 1;
|
|
527
635
|
} else if (fenceMatch[1][0] === fenceMarker) {
|
|
528
636
|
inFence = false;
|
|
@@ -533,14 +641,9 @@ function extractClaims(content2) {
|
|
|
533
641
|
if (inFence) {
|
|
534
642
|
blockLines.push(ignored[i] ? "" : line);
|
|
535
643
|
if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
invocation: m[0],
|
|
540
|
-
line: lineNo,
|
|
541
|
-
excerpt: m[0]
|
|
542
|
-
});
|
|
543
|
-
}
|
|
644
|
+
const scanned = commandClaims(line, lineNo, shellCwd, true);
|
|
645
|
+
shellCwd = scanned.cwd;
|
|
646
|
+
for (const claim of scanned.claims) addCommand(claim);
|
|
544
647
|
}
|
|
545
648
|
continue;
|
|
546
649
|
}
|
|
@@ -567,14 +670,10 @@ function extractClaims(content2) {
|
|
|
567
670
|
excerpt: m[0]
|
|
568
671
|
});
|
|
569
672
|
}
|
|
570
|
-
for (const
|
|
571
|
-
addCommand(
|
|
572
|
-
scriptName: m[2],
|
|
573
|
-
invocation: m[0],
|
|
574
|
-
line: lineNo,
|
|
575
|
-
excerpt: m[0]
|
|
576
|
-
});
|
|
673
|
+
for (const match of line.matchAll(/`([^`]+)`/g)) {
|
|
674
|
+
for (const claim of commandClaims(match[1], lineNo, void 0, true).claims) addCommand(claim);
|
|
577
675
|
}
|
|
676
|
+
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims) addCommand(claim);
|
|
578
677
|
}
|
|
579
678
|
if (inFence) processBlock();
|
|
580
679
|
return {
|
|
@@ -583,10 +682,11 @@ function extractClaims(content2) {
|
|
|
583
682
|
commands: [...commands.values()]
|
|
584
683
|
};
|
|
585
684
|
}
|
|
586
|
-
var ROOT_FILE_NAMES, SHELL_FENCE_INFOS,
|
|
685
|
+
var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
|
|
587
686
|
var init_claims = __esm({
|
|
588
687
|
"src/audit/claims.ts"() {
|
|
589
688
|
"use strict";
|
|
689
|
+
init_commands();
|
|
590
690
|
init_tree();
|
|
591
691
|
ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
592
692
|
"package.json",
|
|
@@ -620,7 +720,7 @@ var init_claims = __esm({
|
|
|
620
720
|
".env.example"
|
|
621
721
|
]);
|
|
622
722
|
SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
|
|
623
|
-
|
|
723
|
+
markdown = new MarkdownIt("commonmark", { html: false });
|
|
624
724
|
COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
|
|
625
725
|
COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
|
|
626
726
|
IGNORE_LINE = "<!-- mason:ignore -->";
|
|
@@ -641,7 +741,7 @@ async function lastCommitOf(resolvedRoot, relPath) {
|
|
|
641
741
|
try {
|
|
642
742
|
const { stdout } = await exec2(
|
|
643
743
|
"git",
|
|
644
|
-
["log", "-1", `--format=${COMMIT_FORMAT}`, "--", relPath],
|
|
744
|
+
["log", "-1", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
645
745
|
{ cwd: resolvedRoot }
|
|
646
746
|
);
|
|
647
747
|
const line = stdout.trim().split("\n")[0];
|
|
@@ -660,7 +760,7 @@ async function deletingCommitOf(resolvedRoot, relPath) {
|
|
|
660
760
|
"--diff-filter=D",
|
|
661
761
|
`--format=${COMMIT_FORMAT}`,
|
|
662
762
|
"--",
|
|
663
|
-
relPath
|
|
763
|
+
`:(literal)${relPath}`
|
|
664
764
|
],
|
|
665
765
|
{ cwd: resolvedRoot }
|
|
666
766
|
);
|
|
@@ -674,7 +774,7 @@ async function firstCommitOf(resolvedRoot, relPath) {
|
|
|
674
774
|
try {
|
|
675
775
|
const { stdout } = await exec2(
|
|
676
776
|
"git",
|
|
677
|
-
["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", relPath],
|
|
777
|
+
["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
678
778
|
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
679
779
|
);
|
|
680
780
|
const line = stdout.trim().split("\n")[0];
|
|
@@ -720,42 +820,242 @@ var init_git = __esm({
|
|
|
720
820
|
}
|
|
721
821
|
});
|
|
722
822
|
|
|
723
|
-
// src/audit/
|
|
823
|
+
// src/audit/inputs.ts
|
|
724
824
|
import fs4 from "fs/promises";
|
|
725
|
-
import
|
|
825
|
+
import * as nativeFs from "fs";
|
|
826
|
+
import path5 from "path";
|
|
726
827
|
import { execFile as execFile3 } from "child_process";
|
|
727
828
|
import { promisify as promisify3 } from "util";
|
|
829
|
+
import fg2 from "fast-glob";
|
|
830
|
+
async function auditInputPath(root, file) {
|
|
831
|
+
if (file === ".") return fs4.realpath(root);
|
|
832
|
+
try {
|
|
833
|
+
return await storePath(root, file);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
836
|
+
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
837
|
+
}
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async function readAuditInput(root, file) {
|
|
842
|
+
try {
|
|
843
|
+
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
844
|
+
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
845
|
+
return value;
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async function auditGlob(root, patterns, options) {
|
|
852
|
+
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
853
|
+
const directories = /* @__PURE__ */ new Set();
|
|
854
|
+
const readdir2 = ((directory, opts, callback) => {
|
|
855
|
+
const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
|
|
856
|
+
directories.add(relative);
|
|
857
|
+
if (directories.size > MAX_INPUTS) {
|
|
858
|
+
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
auditInputPath(root, relative).then(() => {
|
|
862
|
+
nativeFs.readdir(directory, opts, callback);
|
|
863
|
+
}, callback);
|
|
864
|
+
});
|
|
865
|
+
const lstat2 = ((file, callback) => {
|
|
866
|
+
const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
|
|
867
|
+
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
868
|
+
});
|
|
869
|
+
const stream = fg2.stream(patterns, {
|
|
870
|
+
cwd: root,
|
|
871
|
+
ignore: options.ignore,
|
|
872
|
+
onlyFiles: false,
|
|
873
|
+
onlyDirectories: options.onlyDirectories,
|
|
874
|
+
objectMode: true,
|
|
875
|
+
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
876
|
+
fs: { readdir: readdir2, lstat: lstat2 },
|
|
877
|
+
concurrency: 16
|
|
878
|
+
});
|
|
879
|
+
const matches = [];
|
|
880
|
+
try {
|
|
881
|
+
for await (const value of stream) {
|
|
882
|
+
const entry = value;
|
|
883
|
+
const file = entry.path;
|
|
884
|
+
if (options.select && !options.select(file)) continue;
|
|
885
|
+
await auditInputPath(root, file);
|
|
886
|
+
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
887
|
+
matches.push(file);
|
|
888
|
+
if (matches.length > MAX_INPUTS) {
|
|
889
|
+
const largest = [...matches.reduce((counts, item) => {
|
|
890
|
+
const dir = item.split("/")[0];
|
|
891
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
892
|
+
return counts;
|
|
893
|
+
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
894
|
+
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.`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
} finally {
|
|
898
|
+
stream.destroy();
|
|
899
|
+
}
|
|
900
|
+
return matches.sort();
|
|
901
|
+
}
|
|
902
|
+
async function gitSourcePaths(root) {
|
|
903
|
+
const { stdout } = await execute(
|
|
904
|
+
"git",
|
|
905
|
+
[
|
|
906
|
+
"ls-files",
|
|
907
|
+
"-z",
|
|
908
|
+
"--cached",
|
|
909
|
+
"--others",
|
|
910
|
+
"--exclude-standard",
|
|
911
|
+
"--",
|
|
912
|
+
...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
|
|
913
|
+
],
|
|
914
|
+
{ cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
|
|
915
|
+
);
|
|
916
|
+
return [...new Set(stdout.split("\0").filter(Boolean))].sort();
|
|
917
|
+
}
|
|
918
|
+
async function gitIgnoredPaths(root, files) {
|
|
919
|
+
if (!files.length) return /* @__PURE__ */ new Set();
|
|
920
|
+
const operation = execute(
|
|
921
|
+
"git",
|
|
922
|
+
["check-ignore", "-z", "--stdin"],
|
|
923
|
+
{ cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
|
|
924
|
+
);
|
|
925
|
+
operation.child.stdin.on("error", () => {
|
|
926
|
+
});
|
|
927
|
+
operation.child.stdin.end(files.join("\0") + "\0");
|
|
928
|
+
try {
|
|
929
|
+
return new Set((await operation).stdout.split("\0").filter(Boolean));
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (error.code === 1) return /* @__PURE__ */ new Set();
|
|
932
|
+
throw error;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
var execute, MAX_INPUTS;
|
|
936
|
+
var init_inputs = __esm({
|
|
937
|
+
"src/audit/inputs.ts"() {
|
|
938
|
+
"use strict";
|
|
939
|
+
init_files();
|
|
940
|
+
init_storage();
|
|
941
|
+
execute = promisify3(execFile3);
|
|
942
|
+
MAX_INPUTS = 1e5;
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// src/audit/docs.ts
|
|
947
|
+
import fs5 from "fs/promises";
|
|
948
|
+
import path6 from "path";
|
|
949
|
+
import { execFile as execFile4 } from "child_process";
|
|
950
|
+
import { promisify as promisify4 } from "util";
|
|
951
|
+
import fg3 from "fast-glob";
|
|
728
952
|
async function isDirty(resolvedRoot, relPath) {
|
|
729
953
|
try {
|
|
730
954
|
const { stdout } = await exec3(
|
|
731
955
|
"git",
|
|
732
|
-
["status", "--porcelain", "--", relPath],
|
|
956
|
+
["status", "--porcelain", "--", `:(literal)${relPath}`],
|
|
733
957
|
{ cwd: resolvedRoot }
|
|
734
958
|
);
|
|
735
959
|
return stdout.trim().length > 0;
|
|
736
960
|
} catch {
|
|
737
|
-
return
|
|
961
|
+
return true;
|
|
738
962
|
}
|
|
739
963
|
}
|
|
740
|
-
async function
|
|
741
|
-
|
|
742
|
-
|
|
964
|
+
async function discoverDocPaths(root) {
|
|
965
|
+
let candidates = [];
|
|
966
|
+
try {
|
|
967
|
+
const { stdout } = await exec3(
|
|
968
|
+
"git",
|
|
969
|
+
[
|
|
970
|
+
"ls-files",
|
|
971
|
+
"-z",
|
|
972
|
+
"--cached",
|
|
973
|
+
"--others",
|
|
974
|
+
"--exclude-standard",
|
|
975
|
+
"--",
|
|
976
|
+
":(icase,glob)**/readme.md",
|
|
977
|
+
":(icase,glob)**/agents.md",
|
|
978
|
+
":(icase,glob)**/claude.md"
|
|
979
|
+
],
|
|
980
|
+
{ cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
|
|
981
|
+
);
|
|
982
|
+
candidates = stdout.split("\0").filter(Boolean);
|
|
983
|
+
} catch (error) {
|
|
743
984
|
try {
|
|
744
|
-
|
|
985
|
+
await exec3("git", ["rev-parse", "--git-dir"], { cwd: root });
|
|
745
986
|
} catch {
|
|
746
|
-
return
|
|
987
|
+
return localDocPaths(root);
|
|
988
|
+
}
|
|
989
|
+
throw error;
|
|
990
|
+
}
|
|
991
|
+
candidates.push(...await localDocPaths(root));
|
|
992
|
+
const config = await loadProjectConfig(root);
|
|
993
|
+
const excluded = [
|
|
994
|
+
"**/node_modules/**",
|
|
995
|
+
"**/vendor/**",
|
|
996
|
+
"**/dist/**",
|
|
997
|
+
"**/build/**",
|
|
998
|
+
"**/target/**",
|
|
999
|
+
"**/.git/**",
|
|
1000
|
+
"**/.venv/**",
|
|
1001
|
+
"**/venv/**",
|
|
1002
|
+
".mason/**",
|
|
1003
|
+
...config.ignore ?? []
|
|
1004
|
+
];
|
|
1005
|
+
const paths = [];
|
|
1006
|
+
const unique = [...new Set(candidates)].sort();
|
|
1007
|
+
for (let offset = 0; offset < unique.length; offset += 256) {
|
|
1008
|
+
paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg3.escapePath(p)), {
|
|
1009
|
+
ignore: excluded,
|
|
1010
|
+
followSymbolicLinks: false,
|
|
1011
|
+
label: "Documentation discovery"
|
|
1012
|
+
}));
|
|
1013
|
+
if (paths.length > 1e4) throw new Error("Documentation discovery exceeds 10,000 relevant files; narrow .mason/config.json ignore patterns.");
|
|
1014
|
+
}
|
|
1015
|
+
const priority2 = (file) => {
|
|
1016
|
+
const index2 = DOC_CANDIDATES.findIndex((p) => p.toLowerCase() === file.toLowerCase());
|
|
1017
|
+
return index2 < 0 ? DOC_CANDIDATES.length : index2;
|
|
1018
|
+
};
|
|
1019
|
+
return paths.sort((a, b) => priority2(a) - priority2(b) || a.localeCompare(b));
|
|
1020
|
+
}
|
|
1021
|
+
async function localDocPaths(root) {
|
|
1022
|
+
const paths = [];
|
|
1023
|
+
for (const dir of [".", ".claude"]) {
|
|
1024
|
+
let entries;
|
|
1025
|
+
try {
|
|
1026
|
+
entries = await fs5.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
if (error.code === "ENOENT") continue;
|
|
1029
|
+
throw error;
|
|
747
1030
|
}
|
|
1031
|
+
for (const entry of entries) if (/^(?:agents|claude|readme)\.md$/i.test(entry.name)) {
|
|
1032
|
+
paths.push(path6.posix.join(dir, entry.name));
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return paths;
|
|
1036
|
+
}
|
|
1037
|
+
function documentScope(file) {
|
|
1038
|
+
const parent = path6.posix.dirname(file);
|
|
1039
|
+
return path6.posix.basename(parent).toLowerCase() === ".claude" ? path6.posix.dirname(parent) : parent;
|
|
1040
|
+
}
|
|
1041
|
+
async function discoverDocs(resolvedRoot) {
|
|
1042
|
+
const docs = [];
|
|
1043
|
+
for (const candidate of await discoverDocPaths(resolvedRoot)) {
|
|
1044
|
+
const content2 = await readAuditInput(resolvedRoot, candidate);
|
|
1045
|
+
if (content2 === null) throw new Error("Document disappeared during discovery: " + candidate);
|
|
748
1046
|
const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
|
|
749
|
-
|
|
1047
|
+
docs.push({
|
|
750
1048
|
path: candidate,
|
|
751
1049
|
content: content2,
|
|
1050
|
+
scope: documentScope(candidate),
|
|
1051
|
+
kind: /^readme\.md$/i.test(path6.posix.basename(candidate)) ? "readme" : "instructions",
|
|
752
1052
|
lineCount: content2.split("\n").length,
|
|
753
1053
|
lastCommit,
|
|
754
1054
|
dirty,
|
|
755
1055
|
claims: extractClaims(content2)
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
return docs
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
return docs;
|
|
759
1059
|
}
|
|
760
1060
|
var exec3, DOC_CANDIDATES;
|
|
761
1061
|
var init_docs = __esm({
|
|
@@ -763,7 +1063,9 @@ var init_docs = __esm({
|
|
|
763
1063
|
"use strict";
|
|
764
1064
|
init_claims();
|
|
765
1065
|
init_git();
|
|
766
|
-
|
|
1066
|
+
init_inputs();
|
|
1067
|
+
init_files();
|
|
1068
|
+
exec3 = promisify4(execFile4);
|
|
767
1069
|
DOC_CANDIDATES = [
|
|
768
1070
|
"AGENTS.md",
|
|
769
1071
|
"CLAUDE.md",
|
|
@@ -772,10 +1074,52 @@ var init_docs = __esm({
|
|
|
772
1074
|
}
|
|
773
1075
|
});
|
|
774
1076
|
|
|
1077
|
+
// src/audit/scope.ts
|
|
1078
|
+
import fs6 from "fs/promises";
|
|
1079
|
+
import path7 from "path";
|
|
1080
|
+
function scopedPath(directory, value) {
|
|
1081
|
+
if (path7.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
|
|
1082
|
+
const resolved = path7.posix.normalize(path7.posix.join(directory, value));
|
|
1083
|
+
return resolved === ".." || resolved.startsWith("../") ? null : resolved;
|
|
1084
|
+
}
|
|
1085
|
+
function pathClaimScope(doc, claim) {
|
|
1086
|
+
const directory = claim.relativeTo === "document" ? path7.posix.dirname(doc) : documentScope(doc);
|
|
1087
|
+
const local = scopedPath(directory, claim.path);
|
|
1088
|
+
if (!local) return null;
|
|
1089
|
+
const candidates = claim.relativeTo === "document" || directory === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
|
|
1090
|
+
return {
|
|
1091
|
+
basis: claim.relativeTo === "document" ? "document-link" : directory === "." ? "repository" : "document",
|
|
1092
|
+
directory,
|
|
1093
|
+
candidates
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
async function pathExists(root, file) {
|
|
1097
|
+
try {
|
|
1098
|
+
await fs6.access(await auditInputPath(root, file));
|
|
1099
|
+
return true;
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return false;
|
|
1102
|
+
throw error;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function gitMetadataPath(file) {
|
|
1106
|
+
return file.split("/").includes(".git");
|
|
1107
|
+
}
|
|
1108
|
+
function optionalMasonPath(file) {
|
|
1109
|
+
return file.split("/").includes(".mason");
|
|
1110
|
+
}
|
|
1111
|
+
var init_scope = __esm({
|
|
1112
|
+
"src/audit/scope.ts"() {
|
|
1113
|
+
"use strict";
|
|
1114
|
+
init_inputs();
|
|
1115
|
+
init_docs();
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
775
1119
|
// src/test-map.ts
|
|
776
|
-
import
|
|
1120
|
+
import path8 from "path";
|
|
777
1121
|
async function buildTestMap(dir) {
|
|
778
|
-
const rootDir =
|
|
1122
|
+
const rootDir = path8.resolve(dir);
|
|
779
1123
|
const access = await createFileAccess(rootDir);
|
|
780
1124
|
const testPatterns = [
|
|
781
1125
|
"**/*.test.*",
|
|
@@ -796,7 +1140,7 @@ async function buildTestMap(dir) {
|
|
|
796
1140
|
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
797
1141
|
for (const file of sourceFiles) {
|
|
798
1142
|
if (testFiles.includes(file)) continue;
|
|
799
|
-
const baseName =
|
|
1143
|
+
const baseName = path8.basename(file).replace(/\.[^.]+$/, "");
|
|
800
1144
|
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
801
1145
|
existing.push(file);
|
|
802
1146
|
sourceByBaseName.set(baseName, existing);
|
|
@@ -804,7 +1148,7 @@ async function buildTestMap(dir) {
|
|
|
804
1148
|
const paired = [];
|
|
805
1149
|
const unmatched = [];
|
|
806
1150
|
for (const testFile of testFiles) {
|
|
807
|
-
const testBaseName =
|
|
1151
|
+
const testBaseName = path8.basename(testFile).replace(/\.[^.]+$/, "");
|
|
808
1152
|
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
809
1153
|
if (!sourceName) {
|
|
810
1154
|
unmatched.push(testFile);
|
|
@@ -812,10 +1156,10 @@ async function buildTestMap(dir) {
|
|
|
812
1156
|
}
|
|
813
1157
|
const candidates = sourceByBaseName.get(sourceName);
|
|
814
1158
|
if (candidates && candidates.length > 0) {
|
|
815
|
-
const testDir =
|
|
1159
|
+
const testDir = path8.dirname(testFile);
|
|
816
1160
|
const bestMatch = candidates.reduce((best, candidate) => {
|
|
817
|
-
const candidateDir =
|
|
818
|
-
const bestDir =
|
|
1161
|
+
const candidateDir = path8.dirname(candidate);
|
|
1162
|
+
const bestDir = path8.dirname(best);
|
|
819
1163
|
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
820
1164
|
const bestOverlap = commonSegments(testDir, bestDir);
|
|
821
1165
|
return candidateOverlap > bestOverlap ? candidate : best;
|
|
@@ -834,12 +1178,12 @@ async function buildTestMap(dir) {
|
|
|
834
1178
|
function commonSegments(pathA, pathB) {
|
|
835
1179
|
const segsA = pathA.split("/");
|
|
836
1180
|
const segsB = pathB.split("/");
|
|
837
|
-
let
|
|
1181
|
+
let count4 = 0;
|
|
838
1182
|
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
839
|
-
if (segsA[i] === segsB[i])
|
|
1183
|
+
if (segsA[i] === segsB[i]) count4++;
|
|
840
1184
|
else break;
|
|
841
1185
|
}
|
|
842
|
-
return
|
|
1186
|
+
return count4;
|
|
843
1187
|
}
|
|
844
1188
|
var init_test_map = __esm({
|
|
845
1189
|
"src/test-map.ts"() {
|
|
@@ -849,9 +1193,9 @@ var init_test_map = __esm({
|
|
|
849
1193
|
});
|
|
850
1194
|
|
|
851
1195
|
// src/snapshot/snapshot.ts
|
|
852
|
-
import
|
|
853
|
-
import { execFile as
|
|
854
|
-
import { promisify as
|
|
1196
|
+
import path9 from "path";
|
|
1197
|
+
import { execFile as execFile5 } from "child_process";
|
|
1198
|
+
import { promisify as promisify5 } from "util";
|
|
855
1199
|
import { z as z2 } from "zod";
|
|
856
1200
|
async function inspectSnapshot(rootDir) {
|
|
857
1201
|
try {
|
|
@@ -884,7 +1228,7 @@ var init_snapshot = __esm({
|
|
|
884
1228
|
init_storage();
|
|
885
1229
|
init_paths();
|
|
886
1230
|
init_test_map();
|
|
887
|
-
exec4 =
|
|
1231
|
+
exec4 = promisify5(execFile5);
|
|
888
1232
|
repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
889
1233
|
verificationFields = {
|
|
890
1234
|
refreshedHash: z2.string().optional(),
|
|
@@ -913,10 +1257,10 @@ var init_snapshot = __esm({
|
|
|
913
1257
|
});
|
|
914
1258
|
|
|
915
1259
|
// src/drift/drift.ts
|
|
916
|
-
import
|
|
917
|
-
import
|
|
918
|
-
import { execFile as
|
|
919
|
-
import { promisify as
|
|
1260
|
+
import fs7 from "fs/promises";
|
|
1261
|
+
import path10 from "path";
|
|
1262
|
+
import { execFile as execFile6 } from "child_process";
|
|
1263
|
+
import { promisify as promisify6 } from "util";
|
|
920
1264
|
function parseChanges(output) {
|
|
921
1265
|
const fields = output.split("\0");
|
|
922
1266
|
const changes = [];
|
|
@@ -939,273 +1283,119 @@ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
|
939
1283
|
try {
|
|
940
1284
|
const { stdout } = await exec5("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
941
1285
|
return parseChanges(stdout);
|
|
942
|
-
} catch {
|
|
943
|
-
return null;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
async function getWorkingTree(resolvedRoot) {
|
|
947
|
-
try {
|
|
948
|
-
const [diff, untracked] = await Promise.all([
|
|
949
|
-
exec5("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
|
|
950
|
-
exec5("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
|
|
951
|
-
]);
|
|
952
|
-
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
953
|
-
return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
|
|
954
|
-
} catch {
|
|
955
|
-
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
var exec5;
|
|
959
|
-
var init_drift = __esm({
|
|
960
|
-
"src/drift/drift.ts"() {
|
|
961
|
-
"use strict";
|
|
962
|
-
init_snapshot();
|
|
963
|
-
init_paths();
|
|
964
|
-
exec5 = promisify5(execFile5);
|
|
965
|
-
}
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
// src/audit/types.ts
|
|
969
|
-
var ALL_CHECKS;
|
|
970
|
-
var init_types = __esm({
|
|
971
|
-
"src/audit/types.ts"() {
|
|
972
|
-
"use strict";
|
|
973
|
-
ALL_CHECKS = [
|
|
974
|
-
"deleted-reference",
|
|
975
|
-
"new-module",
|
|
976
|
-
"stale-count",
|
|
977
|
-
"dead-command",
|
|
978
|
-
"deps-changed",
|
|
979
|
-
"decision-anchor-drift"
|
|
980
|
-
];
|
|
981
|
-
}
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
// src/audit/checks/deleted-reference.ts
|
|
985
|
-
import fs6 from "fs/promises";
|
|
986
|
-
import path8 from "path";
|
|
987
|
-
async function exists(absPath) {
|
|
988
|
-
try {
|
|
989
|
-
await fs6.access(absPath);
|
|
990
|
-
return true;
|
|
991
|
-
} catch {
|
|
992
|
-
return false;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
async function checkDeletedReferences(ctx) {
|
|
996
|
-
const result = emptyResult();
|
|
997
|
-
for (const doc of ctx.docs) {
|
|
998
|
-
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
999
|
-
const renames = /* @__PURE__ */ new Map();
|
|
1000
|
-
for (const change of changes ?? []) {
|
|
1001
|
-
if (change.status === "renamed" && change.previousPath) {
|
|
1002
|
-
renames.set(change.previousPath, change.path);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
for (const claim of doc.claims.paths) {
|
|
1006
|
-
if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
|
|
1007
|
-
continue;
|
|
1008
|
-
}
|
|
1009
|
-
if (await exists(path8.join(ctx.root, claim.path))) continue;
|
|
1010
|
-
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
1011
|
-
const renamedTo = renames.get(claim.path) ?? null;
|
|
1012
|
-
if (renamedTo) {
|
|
1013
|
-
result.issues.push({
|
|
1014
|
-
type: "deleted-reference",
|
|
1015
|
-
message: `\`${claim.path}\` was renamed to \`${renamedTo}\``,
|
|
1016
|
-
anchor,
|
|
1017
|
-
confidence: "certain",
|
|
1018
|
-
evidence: {
|
|
1019
|
-
kind: "missing-path",
|
|
1020
|
-
claimed: claim.path,
|
|
1021
|
-
renamedTo,
|
|
1022
|
-
deletedInCommit: null,
|
|
1023
|
-
everTracked: true,
|
|
1024
|
-
parentDirExists: true
|
|
1025
|
-
}
|
|
1026
|
-
});
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
const tracked = await lastCommitOf(ctx.root, claim.path);
|
|
1030
|
-
if (tracked) {
|
|
1031
|
-
const deleted = await deletingCommitOf(ctx.root, claim.path);
|
|
1032
|
-
const detail = deleted ? ` \u2013 deleted in ${deleted.hash.slice(0, 7)} "${deleted.subject}" (${deleted.date.slice(0, 10)})` : "";
|
|
1033
|
-
result.issues.push({
|
|
1034
|
-
type: "deleted-reference",
|
|
1035
|
-
message: `\`${claim.path}\` no longer exists${detail}`,
|
|
1036
|
-
anchor,
|
|
1037
|
-
confidence: "certain",
|
|
1038
|
-
evidence: {
|
|
1039
|
-
kind: "missing-path",
|
|
1040
|
-
claimed: claim.path,
|
|
1041
|
-
renamedTo: null,
|
|
1042
|
-
deletedInCommit: deleted,
|
|
1043
|
-
everTracked: true,
|
|
1044
|
-
parentDirExists: await exists(
|
|
1045
|
-
path8.join(ctx.root, path8.dirname(claim.path))
|
|
1046
|
-
)
|
|
1047
|
-
}
|
|
1048
|
-
});
|
|
1049
|
-
continue;
|
|
1050
|
-
}
|
|
1051
|
-
const parentDirExists = await exists(
|
|
1052
|
-
path8.join(ctx.root, path8.dirname(claim.path))
|
|
1053
|
-
);
|
|
1054
|
-
if (!parentDirExists) continue;
|
|
1055
|
-
const issue = {
|
|
1056
|
-
type: "deleted-reference",
|
|
1057
|
-
message: `\`${claim.path}\` does not exist (never tracked in git \u2013 possible typo or invented path)`,
|
|
1058
|
-
anchor,
|
|
1059
|
-
confidence: "likely",
|
|
1060
|
-
evidence: {
|
|
1061
|
-
kind: "missing-path",
|
|
1062
|
-
claimed: claim.path,
|
|
1063
|
-
renamedTo: null,
|
|
1064
|
-
deletedInCommit: null,
|
|
1065
|
-
everTracked: false,
|
|
1066
|
-
parentDirExists: true
|
|
1067
|
-
}
|
|
1068
|
-
};
|
|
1069
|
-
result.issues.push(issue);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
return result;
|
|
1073
|
-
}
|
|
1074
|
-
var init_deleted_reference = __esm({
|
|
1075
|
-
"src/audit/checks/deleted-reference.ts"() {
|
|
1076
|
-
"use strict";
|
|
1077
|
-
init_git();
|
|
1078
|
-
init_checks();
|
|
1079
|
-
}
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
// src/audit/inputs.ts
|
|
1083
|
-
import fs7 from "fs/promises";
|
|
1084
|
-
import * as nativeFs from "fs";
|
|
1085
|
-
import path9 from "path";
|
|
1086
|
-
import { execFile as execFile6 } from "child_process";
|
|
1087
|
-
import { promisify as promisify6 } from "util";
|
|
1088
|
-
import fg2 from "fast-glob";
|
|
1089
|
-
async function auditInputPath(root, file) {
|
|
1090
|
-
if (file === ".") return fs7.realpath(root);
|
|
1091
|
-
try {
|
|
1092
|
-
return await storePath(root, file);
|
|
1093
|
-
} catch (error) {
|
|
1094
|
-
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
1095
|
-
throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
|
|
1096
|
-
}
|
|
1097
|
-
throw error;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
async function readAuditInput(root, file) {
|
|
1101
|
-
try {
|
|
1102
|
-
const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
|
|
1103
|
-
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
1104
|
-
return value;
|
|
1105
|
-
} catch (error) {
|
|
1106
|
-
if (error.code === "ENOENT") return null;
|
|
1107
|
-
throw error;
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
async function auditGlob(root, patterns, options) {
|
|
1111
|
-
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
1112
|
-
const directories = /* @__PURE__ */ new Set();
|
|
1113
|
-
const readdir2 = ((directory, opts, callback) => {
|
|
1114
|
-
const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
|
|
1115
|
-
directories.add(relative);
|
|
1116
|
-
if (directories.size > MAX_INPUTS) {
|
|
1117
|
-
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
1118
|
-
return;
|
|
1119
|
-
}
|
|
1120
|
-
auditInputPath(root, relative).then(() => {
|
|
1121
|
-
nativeFs.readdir(directory, opts, callback);
|
|
1122
|
-
}, callback);
|
|
1123
|
-
});
|
|
1124
|
-
const lstat2 = ((file, callback) => {
|
|
1125
|
-
const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
|
|
1126
|
-
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
1127
|
-
});
|
|
1128
|
-
const stream = fg2.stream(patterns, {
|
|
1129
|
-
cwd: root,
|
|
1130
|
-
ignore: options.ignore,
|
|
1131
|
-
onlyFiles: false,
|
|
1132
|
-
onlyDirectories: options.onlyDirectories,
|
|
1133
|
-
objectMode: true,
|
|
1134
|
-
followSymbolicLinks: options.followSymbolicLinks ?? true,
|
|
1135
|
-
fs: { readdir: readdir2, lstat: lstat2 },
|
|
1136
|
-
concurrency: 16
|
|
1137
|
-
});
|
|
1138
|
-
const matches = [];
|
|
1139
|
-
try {
|
|
1140
|
-
for await (const value of stream) {
|
|
1141
|
-
const entry = value;
|
|
1142
|
-
const file = entry.path;
|
|
1143
|
-
if (options.select && !options.select(file)) continue;
|
|
1144
|
-
await auditInputPath(root, file);
|
|
1145
|
-
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
1146
|
-
matches.push(file);
|
|
1147
|
-
if (matches.length > MAX_INPUTS) {
|
|
1148
|
-
const largest = [...matches.reduce((counts, item) => {
|
|
1149
|
-
const dir = item.split("/")[0];
|
|
1150
|
-
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
1151
|
-
return counts;
|
|
1152
|
-
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
1153
|
-
throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count3]) => `${dir} (${count3})`).join(", ")}. Narrow this check's inputs.`);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
} finally {
|
|
1157
|
-
stream.destroy();
|
|
1158
|
-
}
|
|
1159
|
-
return matches.sort();
|
|
1160
|
-
}
|
|
1161
|
-
async function gitSourcePaths(root) {
|
|
1162
|
-
const { stdout } = await execute(
|
|
1163
|
-
"git",
|
|
1164
|
-
[
|
|
1165
|
-
"ls-files",
|
|
1166
|
-
"-z",
|
|
1167
|
-
"--cached",
|
|
1168
|
-
"--others",
|
|
1169
|
-
"--exclude-standard",
|
|
1170
|
-
"--",
|
|
1171
|
-
...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
|
|
1172
|
-
],
|
|
1173
|
-
{ cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
|
|
1174
|
-
);
|
|
1175
|
-
return [...new Set(stdout.split("\0").filter(Boolean))].sort();
|
|
1286
|
+
} catch {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1176
1289
|
}
|
|
1177
|
-
async function
|
|
1178
|
-
if (!files.length) return /* @__PURE__ */ new Set();
|
|
1179
|
-
const operation = execute(
|
|
1180
|
-
"git",
|
|
1181
|
-
["check-ignore", "-z", "--stdin"],
|
|
1182
|
-
{ cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
|
|
1183
|
-
);
|
|
1184
|
-
operation.child.stdin.on("error", () => {
|
|
1185
|
-
});
|
|
1186
|
-
operation.child.stdin.end(files.join("\0") + "\0");
|
|
1290
|
+
async function getWorkingTree(resolvedRoot) {
|
|
1187
1291
|
try {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1292
|
+
const [diff, untracked] = await Promise.all([
|
|
1293
|
+
exec5("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
|
|
1294
|
+
exec5("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
|
|
1295
|
+
]);
|
|
1296
|
+
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
1297
|
+
return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
|
|
1298
|
+
} catch {
|
|
1299
|
+
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
1192
1300
|
}
|
|
1193
1301
|
}
|
|
1194
|
-
var
|
|
1195
|
-
var
|
|
1196
|
-
"src/
|
|
1302
|
+
var exec5;
|
|
1303
|
+
var init_drift = __esm({
|
|
1304
|
+
"src/drift/drift.ts"() {
|
|
1197
1305
|
"use strict";
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1306
|
+
init_snapshot();
|
|
1307
|
+
init_paths();
|
|
1308
|
+
exec5 = promisify6(execFile6);
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
// src/audit/types.ts
|
|
1313
|
+
var ALL_CHECKS;
|
|
1314
|
+
var init_types = __esm({
|
|
1315
|
+
"src/audit/types.ts"() {
|
|
1316
|
+
"use strict";
|
|
1317
|
+
ALL_CHECKS = [
|
|
1318
|
+
"deleted-reference",
|
|
1319
|
+
"new-module",
|
|
1320
|
+
"stale-count",
|
|
1321
|
+
"dead-command",
|
|
1322
|
+
"deps-changed",
|
|
1323
|
+
"decision-anchor-drift"
|
|
1324
|
+
];
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
// src/audit/checks/deleted-reference.ts
|
|
1329
|
+
import path11 from "path";
|
|
1330
|
+
async function checkDeletedReferences(ctx) {
|
|
1331
|
+
const result = emptyResult();
|
|
1332
|
+
for (const doc of ctx.docs) {
|
|
1333
|
+
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
1334
|
+
const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
|
|
1335
|
+
for (const claim of doc.claims.paths) {
|
|
1336
|
+
const scope = pathClaimScope(doc.path, claim);
|
|
1337
|
+
if (!scope) {
|
|
1338
|
+
result.skipped.push({
|
|
1339
|
+
check: "deleted-reference",
|
|
1340
|
+
doc: doc.path,
|
|
1341
|
+
reason: `Reference ${claim.excerpt} is outside the repository or has an unsupported path.`
|
|
1342
|
+
});
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (scope.candidates.some(optionalMasonPath)) continue;
|
|
1346
|
+
if (scope.candidates.some(gitMetadataPath)) {
|
|
1347
|
+
result.skipped.push({
|
|
1348
|
+
check: "deleted-reference",
|
|
1349
|
+
doc: doc.path,
|
|
1350
|
+
reason: `Reference ${claim.path} names Git metadata, whose layout varies between checkouts; it was not verified as a repository file.`
|
|
1351
|
+
});
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(Boolean)) continue;
|
|
1355
|
+
const candidates = await Promise.all(scope.candidates.map(async (file) => ({
|
|
1356
|
+
file,
|
|
1357
|
+
tracked: await lastCommitOf(ctx.root, file),
|
|
1358
|
+
parent: await pathExists(ctx.root, path11.posix.dirname(file))
|
|
1359
|
+
})));
|
|
1360
|
+
const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
|
|
1361
|
+
if (!candidate && claim.relativeTo !== "document") continue;
|
|
1362
|
+
const resolvedPath = candidate?.file ?? scope.candidates[0];
|
|
1363
|
+
const renamedTo = renames.get(resolvedPath) ?? null;
|
|
1364
|
+
const deletedInCommit = candidate?.tracked && !renamedTo ? await deletingCommitOf(ctx.root, resolvedPath) : null;
|
|
1365
|
+
const evidence = {
|
|
1366
|
+
kind: "missing-path",
|
|
1367
|
+
claimed: claim.path,
|
|
1368
|
+
resolvedPath,
|
|
1369
|
+
scope,
|
|
1370
|
+
renamedTo,
|
|
1371
|
+
deletedInCommit,
|
|
1372
|
+
everTracked: !!candidate?.tracked || !!renamedTo,
|
|
1373
|
+
parentDirExists: candidate?.parent ?? false
|
|
1374
|
+
};
|
|
1375
|
+
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
1376
|
+
const message2 = renamedTo ? `\`${resolvedPath}\` was renamed to \`${renamedTo}\`` : `\`${resolvedPath}\` does not exist` + (deletedInCommit ? ` \u2013 deleted in ${deletedInCommit.hash.slice(0, 7)} "${deletedInCommit.subject}" (${deletedInCommit.date.slice(0, 10)})` : "");
|
|
1377
|
+
if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
|
|
1378
|
+
result.issues.push({ type: "deleted-reference", message: message2, anchor, confidence: "certain", evidence });
|
|
1379
|
+
} else {
|
|
1380
|
+
result.advisories.push({ resolution: "recheck", type: "deleted-reference", message: message2 + (scope.basis === "document" ? "; the bare path's document/repository scope needs review." : "; no tracked history establishes it as a required file. Review for a typo, generated output, or example."), anchor, evidence });
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return result;
|
|
1385
|
+
}
|
|
1386
|
+
var init_deleted_reference = __esm({
|
|
1387
|
+
"src/audit/checks/deleted-reference.ts"() {
|
|
1388
|
+
"use strict";
|
|
1389
|
+
init_git();
|
|
1390
|
+
init_scope();
|
|
1391
|
+
init_checks();
|
|
1202
1392
|
}
|
|
1203
1393
|
});
|
|
1204
1394
|
|
|
1205
1395
|
// src/audit/checks/new-module.ts
|
|
1206
|
-
import
|
|
1396
|
+
import fg4 from "fast-glob";
|
|
1207
1397
|
import fs8 from "fs/promises";
|
|
1208
|
-
import
|
|
1398
|
+
import path12 from "path";
|
|
1209
1399
|
function escapeRegExp(text2) {
|
|
1210
1400
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1211
1401
|
}
|
|
@@ -1219,15 +1409,15 @@ function isMentioned(combinedDocs, name) {
|
|
|
1219
1409
|
async function checkNewModules(ctx) {
|
|
1220
1410
|
const result = emptyResult();
|
|
1221
1411
|
if (ctx.docs.length === 0) return result;
|
|
1222
|
-
const combinedDocs = ctx.docs
|
|
1412
|
+
const combinedDocs = moduleDocumentation(ctx.docs);
|
|
1223
1413
|
const primaryDoc = ctx.docs[0].path;
|
|
1224
1414
|
const checkedDocs = ctx.docs.map((d) => d.path);
|
|
1225
1415
|
const flag = async (dir, sourceFileCount) => {
|
|
1226
|
-
result.
|
|
1416
|
+
result.advisories.push({
|
|
1417
|
+
resolution: "recheck",
|
|
1227
1418
|
type: "new-module",
|
|
1228
|
-
message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in
|
|
1419
|
+
message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in the discovered documentation; review whether it needs documenting`,
|
|
1229
1420
|
anchor: { doc: primaryDoc, line: null, excerpt: dir },
|
|
1230
|
-
confidence: "likely",
|
|
1231
1421
|
evidence: {
|
|
1232
1422
|
kind: "unmentioned-dir",
|
|
1233
1423
|
dir,
|
|
@@ -1242,41 +1432,44 @@ async function checkNewModules(ctx) {
|
|
|
1242
1432
|
}
|
|
1243
1433
|
return result;
|
|
1244
1434
|
}
|
|
1435
|
+
function moduleDocumentation(docs) {
|
|
1436
|
+
return docs.map((doc) => doc.content + "\n" + (path12.posix.dirname(doc.path) === "." ? "" : path12.posix.dirname(doc.path) + "/")).join("\n");
|
|
1437
|
+
}
|
|
1245
1438
|
async function moduleCandidates(root, combinedDocs) {
|
|
1246
1439
|
const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
|
|
1247
1440
|
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
1248
1441
|
const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
|
|
1249
1442
|
const listSubdirs = async (dir) => {
|
|
1250
1443
|
const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1251
|
-
const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) =>
|
|
1444
|
+
const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink() && sources.some((file) => file.startsWith(path12.posix.join(dir, entry.name) + "/"))) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path12.posix.join(dir, entry.name));
|
|
1252
1445
|
const ignored = await gitIgnoredPaths(root, dirs);
|
|
1253
1446
|
const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
|
|
1254
|
-
return (await auditGlob(root, dir === "." ? "*" : `${
|
|
1447
|
+
return (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
|
|
1255
1448
|
ignore,
|
|
1256
1449
|
onlyDirectories: true,
|
|
1257
1450
|
label: "Module directory discovery",
|
|
1258
1451
|
select: (file) => visible.has(file)
|
|
1259
|
-
})).map((file) =>
|
|
1452
|
+
})).map((file) => path12.posix.basename(file));
|
|
1260
1453
|
};
|
|
1261
1454
|
const countSourceFiles = async (dir) => {
|
|
1262
1455
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1263
|
-
let
|
|
1456
|
+
let count4 = 0;
|
|
1264
1457
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1265
|
-
|
|
1458
|
+
count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
|
|
1266
1459
|
ignore,
|
|
1267
1460
|
followSymbolicLinks: false,
|
|
1268
1461
|
label: `Source discovery in ${dir}`
|
|
1269
1462
|
})).length;
|
|
1270
|
-
if (
|
|
1463
|
+
if (count4 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
|
|
1271
1464
|
}
|
|
1272
|
-
return
|
|
1465
|
+
return count4;
|
|
1273
1466
|
};
|
|
1274
1467
|
const candidates = [];
|
|
1275
1468
|
for (const topDir of await listSubdirs(".")) {
|
|
1276
1469
|
const topMentioned = isMentioned(combinedDocs, topDir);
|
|
1277
1470
|
if (!topMentioned) {
|
|
1278
|
-
const
|
|
1279
|
-
if (
|
|
1471
|
+
const count4 = await countSourceFiles(topDir);
|
|
1472
|
+
if (count4 >= 1) candidates.push({ dir: topDir, sourceFileCount: count4 });
|
|
1280
1473
|
continue;
|
|
1281
1474
|
}
|
|
1282
1475
|
const subdirs = await listSubdirs(topDir);
|
|
@@ -1284,9 +1477,9 @@ async function moduleCandidates(root, combinedDocs) {
|
|
|
1284
1477
|
if (mentioned.length < ENUMERATION_THRESHOLD) continue;
|
|
1285
1478
|
for (const sub of subdirs) {
|
|
1286
1479
|
if (isMentioned(combinedDocs, sub)) continue;
|
|
1287
|
-
const
|
|
1288
|
-
if (
|
|
1289
|
-
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount:
|
|
1480
|
+
const count4 = await countSourceFiles(`${topDir}/${sub}`);
|
|
1481
|
+
if (count4 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
|
|
1482
|
+
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count4 });
|
|
1290
1483
|
}
|
|
1291
1484
|
}
|
|
1292
1485
|
}
|
|
@@ -1325,7 +1518,7 @@ var init_new_module = __esm({
|
|
|
1325
1518
|
});
|
|
1326
1519
|
|
|
1327
1520
|
// src/audit/checks/stale-count.ts
|
|
1328
|
-
import
|
|
1521
|
+
import path13 from "path";
|
|
1329
1522
|
async function countGradleModules(root) {
|
|
1330
1523
|
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
1331
1524
|
const content2 = await readAuditInput(root, name);
|
|
@@ -1359,7 +1552,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1359
1552
|
return {
|
|
1360
1553
|
actual: matched.length,
|
|
1361
1554
|
countedFrom: "package.json workspaces",
|
|
1362
|
-
members: matched.map((m) =>
|
|
1555
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1363
1556
|
};
|
|
1364
1557
|
}
|
|
1365
1558
|
}
|
|
@@ -1390,7 +1583,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1390
1583
|
return {
|
|
1391
1584
|
actual: matched.length,
|
|
1392
1585
|
countedFrom: "pnpm-workspace.yaml",
|
|
1393
|
-
members: matched.map((m) =>
|
|
1586
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1394
1587
|
};
|
|
1395
1588
|
}
|
|
1396
1589
|
}
|
|
@@ -1412,8 +1605,8 @@ async function countCargoCrates(root) {
|
|
|
1412
1605
|
ignore: ["**/target/**"],
|
|
1413
1606
|
label: "Cargo workspace discovery"
|
|
1414
1607
|
});
|
|
1415
|
-
for (const m of matched) members.add(
|
|
1416
|
-
} else if (await readAuditInput(root,
|
|
1608
|
+
for (const m of matched) members.add(path13.dirname(m));
|
|
1609
|
+
} else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
|
|
1417
1610
|
members.add(entry);
|
|
1418
1611
|
}
|
|
1419
1612
|
}
|
|
@@ -1431,11 +1624,21 @@ async function resolveCountSource(root, claim) {
|
|
|
1431
1624
|
if (unit === "crate") return countCargoCrates(root);
|
|
1432
1625
|
return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
|
|
1433
1626
|
}
|
|
1627
|
+
async function resolveDocCountSource(root, doc, claim) {
|
|
1628
|
+
const scope = documentScope(doc);
|
|
1629
|
+
const source = await resolveCountSource(path13.join(root, scope), claim);
|
|
1630
|
+
if (!source || scope === ".") return source;
|
|
1631
|
+
return {
|
|
1632
|
+
...source,
|
|
1633
|
+
countedFrom: scope + "/" + source.countedFrom,
|
|
1634
|
+
members: source.members.map((member) => scope + "/" + member)
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1434
1637
|
async function checkStaleCounts(ctx) {
|
|
1435
1638
|
const result = emptyResult();
|
|
1436
1639
|
for (const doc of ctx.docs) {
|
|
1437
1640
|
for (const claim of doc.claims.counts) {
|
|
1438
|
-
const source = await
|
|
1641
|
+
const source = await resolveDocCountSource(ctx.root, doc.path, claim);
|
|
1439
1642
|
if (source === null) {
|
|
1440
1643
|
result.skipped.push({
|
|
1441
1644
|
check: "stale-count",
|
|
@@ -1467,6 +1670,7 @@ var MEMBERS_CAP;
|
|
|
1467
1670
|
var init_stale_count = __esm({
|
|
1468
1671
|
"src/audit/checks/stale-count.ts"() {
|
|
1469
1672
|
"use strict";
|
|
1673
|
+
init_docs();
|
|
1470
1674
|
init_inputs();
|
|
1471
1675
|
init_checks();
|
|
1472
1676
|
MEMBERS_CAP = 50;
|
|
@@ -1474,61 +1678,94 @@ var init_stale_count = __esm({
|
|
|
1474
1678
|
});
|
|
1475
1679
|
|
|
1476
1680
|
// src/audit/checks/dead-command.ts
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (raw === null) return null;
|
|
1681
|
+
import path14 from "path";
|
|
1682
|
+
function scriptsOf(raw) {
|
|
1480
1683
|
try {
|
|
1481
1684
|
const pkg = JSON.parse(raw);
|
|
1482
|
-
|
|
1685
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
|
|
1686
|
+
if (pkg.scripts === void 0) return [];
|
|
1687
|
+
return pkg.scripts && typeof pkg.scripts === "object" && !Array.isArray(pkg.scripts) && Object.values(pkg.scripts).every((value) => typeof value === "string") ? Object.keys(pkg.scripts) : null;
|
|
1483
1688
|
} catch {
|
|
1484
1689
|
return null;
|
|
1485
1690
|
}
|
|
1486
1691
|
}
|
|
1487
|
-
async function
|
|
1488
|
-
const
|
|
1489
|
-
const
|
|
1490
|
-
(
|
|
1491
|
-
|
|
1492
|
-
if (commandClaims.length === 0) return result;
|
|
1493
|
-
const rootScripts = await scriptsOf(ctx.root, "package.json");
|
|
1494
|
-
if (rootScripts === null) {
|
|
1495
|
-
result.skipped.push({
|
|
1496
|
-
check: "dead-command",
|
|
1497
|
-
reason: "no package.json at the repo root"
|
|
1498
|
-
});
|
|
1499
|
-
return result;
|
|
1500
|
-
}
|
|
1501
|
-
const rootSet = new Set(rootScripts);
|
|
1502
|
-
let workspaceScripts = null;
|
|
1503
|
-
let manifestsChecked = ["package.json"];
|
|
1504
|
-
const loadWorkspaceScripts = async () => {
|
|
1505
|
-
if (workspaceScripts !== null) return workspaceScripts;
|
|
1506
|
-
workspaceScripts = /* @__PURE__ */ new Set();
|
|
1507
|
-
const manifests = await commandManifests(ctx.root);
|
|
1508
|
-
manifestsChecked = ["package.json", ...manifests.sort()];
|
|
1509
|
-
for (const manifest of manifests) {
|
|
1510
|
-
const scripts = await scriptsOf(ctx.root, manifest);
|
|
1511
|
-
for (const name of scripts ?? []) workspaceScripts.add(name);
|
|
1512
|
-
}
|
|
1513
|
-
return workspaceScripts;
|
|
1692
|
+
async function commandInputs(root, docs) {
|
|
1693
|
+
const contents = /* @__PURE__ */ new Map();
|
|
1694
|
+
const read = async (file) => {
|
|
1695
|
+
if (!contents.has(file)) contents.set(file, await readAuditInput(root, file));
|
|
1696
|
+
return contents.get(file);
|
|
1514
1697
|
};
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1698
|
+
await read("package.json");
|
|
1699
|
+
let inventory;
|
|
1700
|
+
const resolutions = [];
|
|
1701
|
+
for (const doc of docs) for (const claim of doc.claims.commands) {
|
|
1702
|
+
const base = documentScope(doc.path);
|
|
1703
|
+
const directory = scopedPath(base, claim.directory ?? ".");
|
|
1704
|
+
const scope = {
|
|
1705
|
+
basis: claim.directory === void 0 ? "document" : "explicit",
|
|
1706
|
+
directory: directory ?? base,
|
|
1707
|
+
candidates: []
|
|
1708
|
+
};
|
|
1709
|
+
const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
|
|
1710
|
+
resolutions.push(resolution);
|
|
1711
|
+
if (claim.scopeUnknown || directory === null) {
|
|
1712
|
+
resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
let cursor = directory;
|
|
1716
|
+
while (true) {
|
|
1717
|
+
const manifest = path14.posix.join(cursor, "package.json");
|
|
1718
|
+
scope.candidates.push(manifest);
|
|
1719
|
+
const raw = await read(manifest);
|
|
1720
|
+
if (raw !== null) {
|
|
1721
|
+
resolution.manifest = manifest;
|
|
1722
|
+
const scripts = scriptsOf(raw);
|
|
1723
|
+
if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
|
|
1724
|
+
else resolution.scripts = scripts;
|
|
1725
|
+
break;
|
|
1530
1726
|
}
|
|
1531
|
-
|
|
1727
|
+
if (claim.directory !== void 0 || cursor === ".") break;
|
|
1728
|
+
cursor = path14.posix.dirname(cursor);
|
|
1729
|
+
}
|
|
1730
|
+
if (!resolution.manifest && !resolution.problem) {
|
|
1731
|
+
resolution.problem = `No package.json for the command's scope (${directory}).`;
|
|
1732
|
+
}
|
|
1733
|
+
if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
|
|
1734
|
+
inventory ??= await commandManifests(root);
|
|
1735
|
+
for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
|
|
1736
|
+
if (scope.candidates.includes(manifest)) continue;
|
|
1737
|
+
scope.candidates.push(manifest);
|
|
1738
|
+
const raw = await read(manifest);
|
|
1739
|
+
if (raw === null) continue;
|
|
1740
|
+
const scripts = scriptsOf(raw);
|
|
1741
|
+
if (scripts === null) resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
|
|
1742
|
+
else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
|
|
1746
|
+
}
|
|
1747
|
+
async function checkDeadCommands(ctx) {
|
|
1748
|
+
const result = emptyResult();
|
|
1749
|
+
const { resolutions } = await commandInputs(ctx.root, ctx.docs);
|
|
1750
|
+
for (const { doc, claim, scope, scripts, elsewhere, problem, manifest } of resolutions) {
|
|
1751
|
+
if (problem) {
|
|
1752
|
+
result.skipped.push({ check: "dead-command", doc, reason: problem });
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
if (scripts.includes(claim.scriptName)) continue;
|
|
1756
|
+
const evidence = {
|
|
1757
|
+
kind: "missing-script",
|
|
1758
|
+
scriptName: claim.scriptName,
|
|
1759
|
+
invocation: claim.invocation,
|
|
1760
|
+
manifestsChecked: scope.candidates,
|
|
1761
|
+
availableScripts: scripts.slice(0, AVAILABLE_SCRIPTS_CAP),
|
|
1762
|
+
scope
|
|
1763
|
+
};
|
|
1764
|
+
const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
|
|
1765
|
+
const message2 = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
|
|
1766
|
+
const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
|
|
1767
|
+
if (certain) result.issues.push({ type: "dead-command", message: message2, anchor, confidence: "certain", evidence });
|
|
1768
|
+
else result.advisories.push({ resolution: "recheck", type: "dead-command", message: message2 + (elsewhere.length ? `; it exists in ${elsewhere.join(", ")}. Clarify the intended working directory.` : "; the intended working directory needs review."), anchor, evidence });
|
|
1532
1769
|
}
|
|
1533
1770
|
return result;
|
|
1534
1771
|
}
|
|
@@ -1543,6 +1780,8 @@ var init_dead_command = __esm({
|
|
|
1543
1780
|
"src/audit/checks/dead-command.ts"() {
|
|
1544
1781
|
"use strict";
|
|
1545
1782
|
init_inputs();
|
|
1783
|
+
init_docs();
|
|
1784
|
+
init_scope();
|
|
1546
1785
|
init_checks();
|
|
1547
1786
|
AVAILABLE_SCRIPTS_CAP = 30;
|
|
1548
1787
|
}
|
|
@@ -1556,13 +1795,13 @@ async function git(root, args2) {
|
|
|
1556
1795
|
}
|
|
1557
1796
|
function withoutAndroidReleaseValues(text2) {
|
|
1558
1797
|
if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
|
|
1559
|
-
const
|
|
1798
|
+
const scopes2 = [];
|
|
1560
1799
|
const normalized = [];
|
|
1561
1800
|
let assignments = 0;
|
|
1562
1801
|
for (const line of text2.split("\n")) {
|
|
1563
1802
|
const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
|
|
1564
1803
|
const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
|
|
1565
|
-
if (assignment &&
|
|
1804
|
+
if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
|
|
1566
1805
|
normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
|
|
1567
1806
|
assignments++;
|
|
1568
1807
|
} else {
|
|
@@ -1571,12 +1810,12 @@ function withoutAndroidReleaseValues(text2) {
|
|
|
1571
1810
|
}
|
|
1572
1811
|
const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
|
|
1573
1812
|
for (const brace of code.matchAll(/[{}]/g)) {
|
|
1574
|
-
if (brace[0] === "{")
|
|
1575
|
-
else if (!
|
|
1576
|
-
else
|
|
1813
|
+
if (brace[0] === "{") scopes2.push(named ?? "unknown");
|
|
1814
|
+
else if (!scopes2.length) return null;
|
|
1815
|
+
else scopes2.pop();
|
|
1577
1816
|
}
|
|
1578
1817
|
}
|
|
1579
|
-
return assignments && !
|
|
1818
|
+
return assignments && !scopes2.length ? normalized.join("\n") : null;
|
|
1580
1819
|
}
|
|
1581
1820
|
async function releaseMetadataOnly(root, commit) {
|
|
1582
1821
|
if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
|
|
@@ -1607,6 +1846,12 @@ var init_release_metadata = __esm({
|
|
|
1607
1846
|
});
|
|
1608
1847
|
|
|
1609
1848
|
// src/audit/checks/deps-changed.ts
|
|
1849
|
+
function manifestPathspecs(doc) {
|
|
1850
|
+
const scope = documentScope(doc);
|
|
1851
|
+
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
1852
|
+
const escaped = scope.replace(/[\\*?\[\]]/g, "\\$&");
|
|
1853
|
+
return MANIFEST_PATHSPECS.map((spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec);
|
|
1854
|
+
}
|
|
1610
1855
|
async function checkDepsChanged(ctx) {
|
|
1611
1856
|
const result = emptyResult();
|
|
1612
1857
|
result.suppressedAdvisories = [];
|
|
@@ -1630,7 +1875,7 @@ async function checkDepsChanged(ctx) {
|
|
|
1630
1875
|
const range = await commitsTouchingSince(
|
|
1631
1876
|
ctx.root,
|
|
1632
1877
|
doc.lastCommit.hash,
|
|
1633
|
-
|
|
1878
|
+
manifestPathspecs(doc.path)
|
|
1634
1879
|
);
|
|
1635
1880
|
if (range === null) {
|
|
1636
1881
|
result.skipped.push({
|
|
@@ -1669,6 +1914,7 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
|
1669
1914
|
var init_deps_changed = __esm({
|
|
1670
1915
|
"src/audit/checks/deps-changed.ts"() {
|
|
1671
1916
|
"use strict";
|
|
1917
|
+
init_docs();
|
|
1672
1918
|
init_release_metadata();
|
|
1673
1919
|
init_git();
|
|
1674
1920
|
init_checks();
|
|
@@ -1691,7 +1937,7 @@ var init_deps_changed = __esm({
|
|
|
1691
1937
|
});
|
|
1692
1938
|
|
|
1693
1939
|
// src/context/lexical.ts
|
|
1694
|
-
import
|
|
1940
|
+
import path15 from "path";
|
|
1695
1941
|
var init_lexical = __esm({
|
|
1696
1942
|
"src/context/lexical.ts"() {
|
|
1697
1943
|
"use strict";
|
|
@@ -1875,7 +2121,7 @@ var init_provenance = __esm({
|
|
|
1875
2121
|
|
|
1876
2122
|
// src/decisions/decisions.ts
|
|
1877
2123
|
import fs9 from "fs/promises";
|
|
1878
|
-
import
|
|
2124
|
+
import path16 from "path";
|
|
1879
2125
|
import { createHash } from "crypto";
|
|
1880
2126
|
import { z as z4 } from "zod";
|
|
1881
2127
|
async function loadDecisionStore(rootDir) {
|
|
@@ -1926,9 +2172,9 @@ var init_decisions = __esm({
|
|
|
1926
2172
|
});
|
|
1927
2173
|
|
|
1928
2174
|
// src/decisions/drift.ts
|
|
1929
|
-
import
|
|
2175
|
+
import path17 from "path";
|
|
1930
2176
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
1931
|
-
const resolvedRoot =
|
|
2177
|
+
const resolvedRoot = path17.resolve(rootDir);
|
|
1932
2178
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
1933
2179
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
1934
2180
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -2044,11 +2290,303 @@ var init_checks = __esm({
|
|
|
2044
2290
|
}
|
|
2045
2291
|
});
|
|
2046
2292
|
|
|
2293
|
+
// src/audit/findings.ts
|
|
2294
|
+
import { createHash as createHash2 } from "crypto";
|
|
2295
|
+
import { z as z5 } from "zod";
|
|
2296
|
+
function findingId(finding) {
|
|
2297
|
+
const e = finding.evidence;
|
|
2298
|
+
let key;
|
|
2299
|
+
switch (e.kind) {
|
|
2300
|
+
case "missing-path":
|
|
2301
|
+
key = e.resolvedPath && e.resolvedPath !== e.claimed ? [e.claimed, e.resolvedPath] : e.claimed;
|
|
2302
|
+
break;
|
|
2303
|
+
case "unmentioned-dir":
|
|
2304
|
+
key = e.dir;
|
|
2305
|
+
break;
|
|
2306
|
+
case "count-mismatch":
|
|
2307
|
+
key = [e.unit.replace(/s$/, ""), e.countedFrom];
|
|
2308
|
+
break;
|
|
2309
|
+
case "missing-script":
|
|
2310
|
+
key = e.scope && e.scope.directory !== "." ? [e.scriptName, e.scope.directory] : e.scriptName;
|
|
2311
|
+
break;
|
|
2312
|
+
case "doc-behind-manifests":
|
|
2313
|
+
key = null;
|
|
2314
|
+
break;
|
|
2315
|
+
case "decision-anchor":
|
|
2316
|
+
key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
|
|
2317
|
+
break;
|
|
2318
|
+
}
|
|
2319
|
+
return digest([finding.type, finding.anchor.doc, key]);
|
|
2320
|
+
}
|
|
2321
|
+
var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema;
|
|
2322
|
+
var init_findings = __esm({
|
|
2323
|
+
"src/audit/findings.ts"() {
|
|
2324
|
+
"use strict";
|
|
2325
|
+
digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2326
|
+
checkSchema = z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
|
|
2327
|
+
commitSchema = z5.object({ hash: z5.string().regex(/^[a-f0-9]{40,64}$/), date: z5.string(), subject: z5.string() });
|
|
2328
|
+
anchorSchema = z5.object({ doc: z5.string(), line: z5.number().int().positive().nullable(), excerpt: z5.string().nullable() });
|
|
2329
|
+
count = z5.number().int().nonnegative();
|
|
2330
|
+
scopeSchema = z5.object({
|
|
2331
|
+
basis: z5.enum(["repository", "document", "document-link", "explicit"]),
|
|
2332
|
+
directory: z5.string(),
|
|
2333
|
+
candidates: z5.array(z5.string())
|
|
2334
|
+
});
|
|
2335
|
+
evidenceSchema = z5.discriminatedUnion("kind", [
|
|
2336
|
+
z5.object({
|
|
2337
|
+
kind: z5.literal("missing-path"),
|
|
2338
|
+
claimed: z5.string(),
|
|
2339
|
+
renamedTo: z5.string().nullable(),
|
|
2340
|
+
deletedInCommit: commitSchema.nullable(),
|
|
2341
|
+
everTracked: z5.boolean(),
|
|
2342
|
+
parentDirExists: z5.boolean(),
|
|
2343
|
+
resolvedPath: z5.string().optional(),
|
|
2344
|
+
scope: scopeSchema.optional()
|
|
2345
|
+
}),
|
|
2346
|
+
z5.object({
|
|
2347
|
+
kind: z5.literal("unmentioned-dir"),
|
|
2348
|
+
dir: z5.string(),
|
|
2349
|
+
sourceFileCount: count,
|
|
2350
|
+
firstCommit: commitSchema.nullable(),
|
|
2351
|
+
checkedDocs: z5.array(z5.string())
|
|
2352
|
+
}),
|
|
2353
|
+
z5.object({
|
|
2354
|
+
kind: z5.literal("count-mismatch"),
|
|
2355
|
+
claimed: count,
|
|
2356
|
+
actual: count,
|
|
2357
|
+
unit: z5.string(),
|
|
2358
|
+
countedFrom: z5.string(),
|
|
2359
|
+
members: z5.array(z5.string())
|
|
2360
|
+
}),
|
|
2361
|
+
z5.object({
|
|
2362
|
+
kind: z5.literal("missing-script"),
|
|
2363
|
+
scriptName: z5.string(),
|
|
2364
|
+
invocation: z5.string(),
|
|
2365
|
+
manifestsChecked: z5.array(z5.string()),
|
|
2366
|
+
availableScripts: z5.array(z5.string()),
|
|
2367
|
+
scope: scopeSchema.optional()
|
|
2368
|
+
}),
|
|
2369
|
+
z5.object({
|
|
2370
|
+
kind: z5.literal("doc-behind-manifests"),
|
|
2371
|
+
docLastCommit: commitSchema,
|
|
2372
|
+
manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
|
|
2373
|
+
totalCommits: count
|
|
2374
|
+
}),
|
|
2375
|
+
z5.object({
|
|
2376
|
+
kind: z5.literal("decision-anchor"),
|
|
2377
|
+
decisionId: z5.string(),
|
|
2378
|
+
title: z5.string(),
|
|
2379
|
+
changedFiles: z5.array(z5.string()),
|
|
2380
|
+
refreshedHash: z5.string(),
|
|
2381
|
+
provenance: z5.object({}).passthrough().optional()
|
|
2382
|
+
})
|
|
2383
|
+
]);
|
|
2384
|
+
findingSchema = z5.object({ message: z5.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
2385
|
+
issueSchema = findingSchema.extend({
|
|
2386
|
+
type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2387
|
+
confidence: z5.enum(["certain", "likely"])
|
|
2388
|
+
});
|
|
2389
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.literal("recheck").optional() });
|
|
2390
|
+
checkResultSchema = z5.object({
|
|
2391
|
+
issues: z5.array(issueSchema),
|
|
2392
|
+
advisories: z5.array(advisorySchema),
|
|
2393
|
+
suppressedAdvisories: z5.array(advisorySchema).optional(),
|
|
2394
|
+
skipped: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2398
|
+
|
|
2399
|
+
// src/audit/advisory-review.ts
|
|
2400
|
+
import fs10 from "fs/promises";
|
|
2401
|
+
import { execFile as execFile8 } from "child_process";
|
|
2402
|
+
import { promisify as promisify8 } from "util";
|
|
2403
|
+
import { z as z6 } from "zod";
|
|
2404
|
+
async function readRecord(root, id) {
|
|
2405
|
+
const raw = await readStoreJson(root, recordPath(id));
|
|
2406
|
+
if (raw === null) return null;
|
|
2407
|
+
const record = recordSchema.parse(raw);
|
|
2408
|
+
const { digest: stored, ...payload } = record;
|
|
2409
|
+
if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || event.evidence.fingerprint !== digest([event.evidence.scope, event.evidence.tree, event.evidence.lastScopeCommit, event.evidence.paths]))) {
|
|
2410
|
+
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2411
|
+
}
|
|
2412
|
+
return record;
|
|
2413
|
+
}
|
|
2414
|
+
function scopes(finding) {
|
|
2415
|
+
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2416
|
+
if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
2417
|
+
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
2418
|
+
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
2419
|
+
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
2420
|
+
else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
|
|
2421
|
+
else throw new Error("This advisory has no supported review scope.");
|
|
2422
|
+
return [...new Set(files)].map((file) => {
|
|
2423
|
+
if (normalizeRepoPath(file) !== file) throw new Error("Advisory contains an invalid scoped path: " + file);
|
|
2424
|
+
return `:(literal)${file}`;
|
|
2425
|
+
}).sort();
|
|
2426
|
+
}
|
|
2427
|
+
async function reviewEvidence(root, finding) {
|
|
2428
|
+
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
2429
|
+
const scope = scopes(finding);
|
|
2430
|
+
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2431
|
+
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2432
|
+
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
2433
|
+
const [tree, status, lastScopeCommit, document] = await Promise.all([
|
|
2434
|
+
tracked.length ? git2(root, "ls-tree", "-r", "-z", "HEAD", "--", ...tracked.map((file) => `:(literal)${file}`)) : "",
|
|
2435
|
+
git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
|
|
2436
|
+
git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
|
|
2437
|
+
readAuditInput(root, finding.anchor.doc)
|
|
2438
|
+
]);
|
|
2439
|
+
if (!document?.trim()) throw new Error("Original advisory document is missing, empty or unreadable.");
|
|
2440
|
+
if (tree.length > 2 * 1024 * 1024 || tree.split("\0").length > 2e4) throw new Error("Advisory scope is too large for a complete review; narrow the documentation scope.");
|
|
2441
|
+
for (const entry of tree.split("\0").filter(Boolean)) {
|
|
2442
|
+
const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
|
|
2443
|
+
if (entry.startsWith("120000 ") || entry.startsWith("160000 ")) throw new Error("Symlink or submodule in advisory review scope: " + file);
|
|
2444
|
+
await storePath(root, file);
|
|
2445
|
+
}
|
|
2446
|
+
const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
|
|
2447
|
+
(finding.evidence.scope?.candidates ?? [finding.evidence.resolvedPath ?? finding.evidence.claimed]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
|
|
2448
|
+
) : [];
|
|
2449
|
+
let dirty = status.length > 0;
|
|
2450
|
+
if (finding.evidence.kind === "missing-script") {
|
|
2451
|
+
for (const file of [...finding.evidence.manifestsChecked, ...finding.evidence.scope?.candidates ?? []]) {
|
|
2452
|
+
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
|
|
2456
|
+
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
2457
|
+
return {
|
|
2458
|
+
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
2459
|
+
dirty,
|
|
2460
|
+
documentDigest: digest(document)
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
async function decisionAssessment(root, finding, baselineHead) {
|
|
2464
|
+
const evidence = finding.evidence;
|
|
2465
|
+
if (evidence.kind !== "decision-anchor") return null;
|
|
2466
|
+
const id = findingId(finding);
|
|
2467
|
+
const store = await loadDecisionStore(root);
|
|
2468
|
+
if (store.diagnostics.length) throw new Error("Decision records have unavailable or malformed evidence; review cannot be confirmed.");
|
|
2469
|
+
const record = store.records.find((record2) => record2.id === evidence.decisionId);
|
|
2470
|
+
if (!record || record.version !== 2) return null;
|
|
2471
|
+
const events2 = record.history.filter((event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind));
|
|
2472
|
+
const event = events2.at(-1);
|
|
2473
|
+
if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length) return null;
|
|
2474
|
+
try {
|
|
2475
|
+
await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
|
|
2476
|
+
} catch {
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
const drift = await computeDecisionDrift(root, [record]);
|
|
2480
|
+
if (!drift.historyAvailable) throw new Error("Decision review history is unavailable.");
|
|
2481
|
+
const retired = record.status === "retired" && event.kind === "retired";
|
|
2482
|
+
const current = retired || record.status === "active" && record.approval === "accepted" && drift.freshness?.[record.id] === "current" && !drift.pendingProposals?.[record.id];
|
|
2483
|
+
return {
|
|
2484
|
+
id,
|
|
2485
|
+
status: current ? "current" : "reopened",
|
|
2486
|
+
outcome: event.kind,
|
|
2487
|
+
reviewer: event.actor,
|
|
2488
|
+
note: event.note,
|
|
2489
|
+
reviewedHead: event.evidence.headHash,
|
|
2490
|
+
recordPath: `.mason/decisions/${record.id}.json`,
|
|
2491
|
+
reason: current ? "The existing decision review records acceptance, reaffirmation or retirement covering the original finding." : "Decision content or anchor evidence changed after its review; review the decision again."
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
async function assessAdvisory(root, finding, baselineHead) {
|
|
2495
|
+
if ("confidence" in finding) return null;
|
|
2496
|
+
const id = findingId(finding);
|
|
2497
|
+
try {
|
|
2498
|
+
if (finding.evidence.kind === "decision-anchor") return await decisionAssessment(root, finding, baselineHead);
|
|
2499
|
+
const record = await readRecord(root, id);
|
|
2500
|
+
if (!record) return null;
|
|
2501
|
+
const event = record.events.at(-1);
|
|
2502
|
+
const current = await reviewEvidence(root, event.finding);
|
|
2503
|
+
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
2504
|
+
const base = {
|
|
2505
|
+
id,
|
|
2506
|
+
recordPath: recordPath(id),
|
|
2507
|
+
reviewer: event.reviewer,
|
|
2508
|
+
note: event.note,
|
|
2509
|
+
outcome: event.outcome,
|
|
2510
|
+
reviewedHead: event.evidence.head
|
|
2511
|
+
};
|
|
2512
|
+
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." };
|
|
2513
|
+
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." };
|
|
2514
|
+
} catch (error) {
|
|
2515
|
+
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
async function advisoryReviewInventory(root) {
|
|
2519
|
+
let names;
|
|
2520
|
+
try {
|
|
2521
|
+
names = await fs10.readdir(await storePath(root, ".mason/reviews/advisories"));
|
|
2522
|
+
} catch (error) {
|
|
2523
|
+
if (error.code === "ENOENT") return [];
|
|
2524
|
+
throw error;
|
|
2525
|
+
}
|
|
2526
|
+
if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
|
|
2527
|
+
return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
|
|
2528
|
+
}
|
|
2529
|
+
var execute2, git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2530
|
+
var init_advisory_review = __esm({
|
|
2531
|
+
"src/audit/advisory-review.ts"() {
|
|
2532
|
+
"use strict";
|
|
2533
|
+
init_findings();
|
|
2534
|
+
init_deps_changed();
|
|
2535
|
+
init_inputs();
|
|
2536
|
+
init_scope();
|
|
2537
|
+
init_storage();
|
|
2538
|
+
init_paths();
|
|
2539
|
+
init_store();
|
|
2540
|
+
init_decisions();
|
|
2541
|
+
init_drift2();
|
|
2542
|
+
execute2 = promisify8(execFile8);
|
|
2543
|
+
git2 = async (root, ...args2) => (await execute2("git", args2, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
2544
|
+
hashSchema = z6.string().regex(/^[a-f0-9]{64}$/);
|
|
2545
|
+
commitSchema2 = z6.string().regex(/^[a-f0-9]{40,64}$/);
|
|
2546
|
+
evidenceSchema2 = z6.object({
|
|
2547
|
+
head: commitSchema2,
|
|
2548
|
+
fingerprint: hashSchema,
|
|
2549
|
+
scope: z6.array(z6.string()).min(1).max(1e3),
|
|
2550
|
+
tree: z6.string().max(2 * 1024 * 1024),
|
|
2551
|
+
lastScopeCommit: commitSchema2,
|
|
2552
|
+
paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
|
|
2553
|
+
});
|
|
2554
|
+
eventSchema2 = z6.object({
|
|
2555
|
+
at: z6.string().datetime(),
|
|
2556
|
+
reviewer: z6.string().trim().min(1).max(200),
|
|
2557
|
+
note: z6.string().trim().min(1).max(2500),
|
|
2558
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
|
|
2559
|
+
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2560
|
+
evidence: evidenceSchema2
|
|
2561
|
+
});
|
|
2562
|
+
recordSchema = z6.object({ version: z6.literal(1), id: hashSchema, events: z6.array(eventSchema2).min(1).max(200), digest: hashSchema });
|
|
2563
|
+
reviewSummarySchema = z6.object({
|
|
2564
|
+
id: hashSchema,
|
|
2565
|
+
status: z6.enum(["current", "reopened", "deferred", "unverified"]),
|
|
2566
|
+
reason: z6.string(),
|
|
2567
|
+
recordPath: z6.string().optional(),
|
|
2568
|
+
reviewer: z6.string().optional(),
|
|
2569
|
+
note: z6.string().optional(),
|
|
2570
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
2571
|
+
reviewedHead: z6.string().optional()
|
|
2572
|
+
});
|
|
2573
|
+
advisoryReviewRequest = z6.object({
|
|
2574
|
+
baselinePath: z6.string().min(1),
|
|
2575
|
+
findingId: hashSchema,
|
|
2576
|
+
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
2577
|
+
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2578
|
+
note: eventSchema2.shape.note.optional(),
|
|
2579
|
+
reviewToken: hashSchema.optional()
|
|
2580
|
+
});
|
|
2581
|
+
recordPath = (id) => `.mason/reviews/advisories/${hashSchema.parse(id)}.json`;
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2584
|
+
|
|
2047
2585
|
// src/audit/audit.ts
|
|
2048
|
-
import
|
|
2049
|
-
import
|
|
2586
|
+
import fs11 from "fs/promises";
|
|
2587
|
+
import path18 from "path";
|
|
2050
2588
|
async function computeAudit(rootDir, options = {}) {
|
|
2051
|
-
const resolvedRoot =
|
|
2589
|
+
const resolvedRoot = path18.resolve(rootDir);
|
|
2052
2590
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2053
2591
|
if (docs.length === 0) return null;
|
|
2054
2592
|
const report = {
|
|
@@ -2059,6 +2597,8 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2059
2597
|
checksRun: [],
|
|
2060
2598
|
docs: docs.map((d) => ({
|
|
2061
2599
|
path: d.path,
|
|
2600
|
+
scope: d.scope,
|
|
2601
|
+
kind: d.kind,
|
|
2062
2602
|
lastCommit: d.lastCommit,
|
|
2063
2603
|
dirty: d.dirty,
|
|
2064
2604
|
lineCount: d.lineCount
|
|
@@ -2080,7 +2620,7 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2080
2620
|
}
|
|
2081
2621
|
let decisionsPresent = false;
|
|
2082
2622
|
try {
|
|
2083
|
-
await
|
|
2623
|
+
await fs11.access(path18.join(resolvedRoot, ".mason", "decisions"));
|
|
2084
2624
|
decisionsPresent = true;
|
|
2085
2625
|
} catch {
|
|
2086
2626
|
}
|
|
@@ -2103,6 +2643,8 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2103
2643
|
report.skippedChecks.push(...skipped);
|
|
2104
2644
|
}
|
|
2105
2645
|
report.clean = report.issues.length === 0;
|
|
2646
|
+
const reviews = (await Promise.all([...report.advisories, ...report.suppressedAdvisories].map((finding) => assessAdvisory(resolvedRoot, finding, headHash)))).filter((review) => review !== null);
|
|
2647
|
+
if (reviews.length) report.advisoryReviews = reviews;
|
|
2106
2648
|
return report;
|
|
2107
2649
|
}
|
|
2108
2650
|
var init_audit = __esm({
|
|
@@ -2113,45 +2655,24 @@ var init_audit = __esm({
|
|
|
2113
2655
|
init_docs();
|
|
2114
2656
|
init_types();
|
|
2115
2657
|
init_checks();
|
|
2658
|
+
init_advisory_review();
|
|
2116
2659
|
}
|
|
2117
2660
|
});
|
|
2118
2661
|
|
|
2119
2662
|
// src/audit/repair.ts
|
|
2120
|
-
import
|
|
2121
|
-
import
|
|
2122
|
-
import {
|
|
2123
|
-
import { z as
|
|
2124
|
-
function
|
|
2125
|
-
|
|
2126
|
-
let key;
|
|
2127
|
-
switch (e.kind) {
|
|
2128
|
-
case "missing-path":
|
|
2129
|
-
key = e.claimed;
|
|
2130
|
-
break;
|
|
2131
|
-
case "unmentioned-dir":
|
|
2132
|
-
key = e.dir;
|
|
2133
|
-
break;
|
|
2134
|
-
case "count-mismatch":
|
|
2135
|
-
key = [e.unit.replace(/s$/, ""), e.countedFrom];
|
|
2136
|
-
break;
|
|
2137
|
-
case "missing-script":
|
|
2138
|
-
key = e.scriptName;
|
|
2139
|
-
break;
|
|
2140
|
-
case "doc-behind-manifests":
|
|
2141
|
-
key = null;
|
|
2142
|
-
break;
|
|
2143
|
-
case "decision-anchor":
|
|
2144
|
-
key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
|
|
2145
|
-
break;
|
|
2146
|
-
}
|
|
2147
|
-
return digest([finding.type, finding.anchor.doc, key]);
|
|
2663
|
+
import fs12 from "fs/promises";
|
|
2664
|
+
import path19 from "path";
|
|
2665
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2666
|
+
import { z as z7 } from "zod";
|
|
2667
|
+
function recheckable(finding) {
|
|
2668
|
+
return !("confidence" in finding) && finding.resolution === "recheck" && ["deleted-reference", "dead-command", "new-module"].includes(finding.type) && ["missing-path", "missing-script", "unmentioned-dir"].includes(finding.evidence.kind);
|
|
2148
2669
|
}
|
|
2149
2670
|
function allFindings(report) {
|
|
2150
2671
|
return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
|
|
2151
2672
|
}
|
|
2152
2673
|
async function docState(root) {
|
|
2153
2674
|
const docs = [];
|
|
2154
|
-
for (const doc of
|
|
2675
|
+
for (const doc of await discoverDocPaths(root)) {
|
|
2155
2676
|
try {
|
|
2156
2677
|
const content2 = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
|
|
2157
2678
|
if (content2 === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
|
|
@@ -2173,8 +2694,8 @@ async function stableAudit(root, checks, options = {}) {
|
|
|
2173
2694
|
return report;
|
|
2174
2695
|
}
|
|
2175
2696
|
async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
2176
|
-
const root = await
|
|
2177
|
-
const selected =
|
|
2697
|
+
const root = await fs12.realpath(rootDir);
|
|
2698
|
+
const selected = z7.array(checkSchema).nonempty().parse(checks);
|
|
2178
2699
|
const report = await stableAudit(root, selected, options);
|
|
2179
2700
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
2180
2701
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
@@ -2189,23 +2710,29 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
|
2189
2710
|
await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
|
|
2190
2711
|
return { version: 1, action: "prepare", baselinePath, report };
|
|
2191
2712
|
}
|
|
2192
|
-
async function
|
|
2193
|
-
const root = await
|
|
2194
|
-
const declaredRoot =
|
|
2195
|
-
const relative =
|
|
2713
|
+
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
2714
|
+
const root = await fs12.realpath(rootDir);
|
|
2715
|
+
const declaredRoot = path19.resolve(rootDir);
|
|
2716
|
+
const relative = path19.isAbsolute(baselinePath) ? path19.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2196
2717
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
2197
2718
|
const { digest: savedDigest, ...payload } = stored;
|
|
2198
2719
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
2199
2720
|
if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
|
|
2200
2721
|
const original = stored.report;
|
|
2722
|
+
return { root, relative, original };
|
|
2723
|
+
}
|
|
2724
|
+
async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
2725
|
+
const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
|
|
2201
2726
|
const diagnostics = [];
|
|
2727
|
+
let reviewDigest;
|
|
2202
2728
|
let current = null;
|
|
2203
2729
|
try {
|
|
2730
|
+
reviewDigest = digest(await advisoryReviewInventory(root));
|
|
2204
2731
|
current = await stableAudit(root, original.checksRun, options);
|
|
2205
2732
|
if (!current) diagnostics.push("No context files remain available to audit.");
|
|
2206
2733
|
else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
|
|
2207
2734
|
for (const doc of original.docs) {
|
|
2208
|
-
if (!original.
|
|
2735
|
+
if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
|
|
2209
2736
|
const content2 = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
|
|
2210
2737
|
if (content2 === null || !content2.trim()) {
|
|
2211
2738
|
diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
|
|
@@ -2222,21 +2749,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2222
2749
|
const originalIds = new Set(originalFindings.map(findingId));
|
|
2223
2750
|
const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
|
|
2224
2751
|
for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
|
|
2225
|
-
const findings = originalFindings.map((finding) => {
|
|
2752
|
+
const findings = await Promise.all(originalFindings.map(async (finding) => {
|
|
2226
2753
|
const id = findingId(finding);
|
|
2227
2754
|
const now = currentById.get(id);
|
|
2228
2755
|
const base = { id, original: finding, ...now ? { current: now } : {} };
|
|
2229
2756
|
if (diagnostics.length || !current) {
|
|
2230
2757
|
return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
|
|
2231
2758
|
}
|
|
2232
|
-
if ("confidence" in finding
|
|
2759
|
+
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
2233
2760
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
2234
2761
|
}
|
|
2235
2762
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
2236
2763
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
2237
2764
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
2238
2765
|
}
|
|
2239
|
-
|
|
2766
|
+
const review = await assessAdvisory(root, now ?? finding, original.headHash);
|
|
2767
|
+
if (review) {
|
|
2768
|
+
if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
|
|
2769
|
+
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
2770
|
+
return { ...base, review, status: "review-required", reason: review.reason };
|
|
2771
|
+
}
|
|
2772
|
+
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
2240
2773
|
return {
|
|
2241
2774
|
...base,
|
|
2242
2775
|
status: "review-required",
|
|
@@ -2244,11 +2777,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2244
2777
|
};
|
|
2245
2778
|
}
|
|
2246
2779
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
2247
|
-
});
|
|
2780
|
+
}));
|
|
2781
|
+
try {
|
|
2782
|
+
if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
|
|
2783
|
+
diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
|
|
2784
|
+
for (const finding of findings) {
|
|
2785
|
+
finding.status = "unverified";
|
|
2786
|
+
finding.reason = diagnostics.at(-1);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
} catch (error) {
|
|
2790
|
+
diagnostics.push(String(error));
|
|
2791
|
+
for (const finding of findings) {
|
|
2792
|
+
finding.status = "unverified";
|
|
2793
|
+
finding.reason = "Advisory review evidence became unavailable during verification.";
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2248
2796
|
const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
|
|
2797
|
+
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
2249
2798
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
2250
2799
|
for (const f of findings) counts[f.status]++;
|
|
2251
|
-
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f));
|
|
2800
|
+
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)));
|
|
2252
2801
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
2253
2802
|
return {
|
|
2254
2803
|
version: 1,
|
|
@@ -2262,10 +2811,10 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2262
2811
|
diagnostics,
|
|
2263
2812
|
currentAudit: current,
|
|
2264
2813
|
counts,
|
|
2265
|
-
scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by
|
|
2814
|
+
scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by the check or covered by an explicit assessment of current evidence; inspect each finding's reason and review. This is not a certification of documentation or application correctness."
|
|
2266
2815
|
};
|
|
2267
2816
|
}
|
|
2268
|
-
var
|
|
2817
|
+
var count2, reportSchema, baselineSchema, repairFindingSchema;
|
|
2269
2818
|
var init_repair = __esm({
|
|
2270
2819
|
"src/audit/repair.ts"() {
|
|
2271
2820
|
"use strict";
|
|
@@ -2277,205 +2826,134 @@ var init_repair = __esm({
|
|
|
2277
2826
|
init_files();
|
|
2278
2827
|
init_paths();
|
|
2279
2828
|
init_types();
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
renamedTo: z5.string().nullable(),
|
|
2289
|
-
deletedInCommit: commitSchema.nullable(),
|
|
2290
|
-
everTracked: z5.boolean(),
|
|
2291
|
-
parentDirExists: z5.boolean()
|
|
2292
|
-
}),
|
|
2293
|
-
z5.object({
|
|
2294
|
-
kind: z5.literal("unmentioned-dir"),
|
|
2295
|
-
dir: z5.string(),
|
|
2296
|
-
sourceFileCount: count,
|
|
2297
|
-
firstCommit: commitSchema.nullable(),
|
|
2298
|
-
checkedDocs: z5.array(z5.string())
|
|
2299
|
-
}),
|
|
2300
|
-
z5.object({
|
|
2301
|
-
kind: z5.literal("count-mismatch"),
|
|
2302
|
-
claimed: count,
|
|
2303
|
-
actual: count,
|
|
2304
|
-
unit: z5.string(),
|
|
2305
|
-
countedFrom: z5.string(),
|
|
2306
|
-
members: z5.array(z5.string())
|
|
2307
|
-
}),
|
|
2308
|
-
z5.object({
|
|
2309
|
-
kind: z5.literal("missing-script"),
|
|
2310
|
-
scriptName: z5.string(),
|
|
2311
|
-
invocation: z5.string(),
|
|
2312
|
-
manifestsChecked: z5.array(z5.string()),
|
|
2313
|
-
availableScripts: z5.array(z5.string())
|
|
2314
|
-
}),
|
|
2315
|
-
z5.object({
|
|
2316
|
-
kind: z5.literal("doc-behind-manifests"),
|
|
2317
|
-
docLastCommit: commitSchema,
|
|
2318
|
-
manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
|
|
2319
|
-
totalCommits: count
|
|
2320
|
-
}),
|
|
2321
|
-
z5.object({
|
|
2322
|
-
kind: z5.literal("decision-anchor"),
|
|
2323
|
-
decisionId: z5.string(),
|
|
2324
|
-
title: z5.string(),
|
|
2325
|
-
changedFiles: z5.array(z5.string()),
|
|
2326
|
-
refreshedHash: z5.string(),
|
|
2327
|
-
provenance: z5.object({}).passthrough().optional()
|
|
2328
|
-
})
|
|
2329
|
-
]);
|
|
2330
|
-
findingSchema = z5.object({ message: z5.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
2331
|
-
issueSchema = findingSchema.extend({
|
|
2332
|
-
type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2333
|
-
confidence: z5.enum(["certain", "likely"])
|
|
2334
|
-
});
|
|
2335
|
-
advisorySchema = findingSchema.extend({ type: z5.enum(["deps-changed", "decision-anchor-drift"]) });
|
|
2336
|
-
checkResultSchema = z5.object({
|
|
2337
|
-
issues: z5.array(issueSchema),
|
|
2338
|
-
advisories: z5.array(advisorySchema),
|
|
2339
|
-
suppressedAdvisories: z5.array(advisorySchema).optional(),
|
|
2340
|
-
skipped: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
|
|
2341
|
-
});
|
|
2342
|
-
reportSchema = z5.object({
|
|
2343
|
-
version: z5.literal(1),
|
|
2344
|
-
root: z5.string(),
|
|
2345
|
-
gitAvailable: z5.literal(true),
|
|
2829
|
+
init_findings();
|
|
2830
|
+
init_advisory_review();
|
|
2831
|
+
init_findings();
|
|
2832
|
+
count2 = z7.number().int().nonnegative();
|
|
2833
|
+
reportSchema = z7.object({
|
|
2834
|
+
version: z7.literal(1),
|
|
2835
|
+
root: z7.string(),
|
|
2836
|
+
gitAvailable: z7.literal(true),
|
|
2346
2837
|
headHash: commitSchema.shape.hash,
|
|
2347
|
-
checksRun:
|
|
2348
|
-
docs:
|
|
2349
|
-
path:
|
|
2838
|
+
checksRun: z7.array(checkSchema).nonempty(),
|
|
2839
|
+
docs: z7.array(z7.object({
|
|
2840
|
+
path: z7.string().refine((value) => normalizeRepoPath(value) === value),
|
|
2841
|
+
scope: z7.string().optional(),
|
|
2842
|
+
kind: z7.enum(["instructions", "readme"]).optional(),
|
|
2350
2843
|
lastCommit: commitSchema.nullable(),
|
|
2351
|
-
dirty:
|
|
2352
|
-
lineCount:
|
|
2844
|
+
dirty: z7.boolean(),
|
|
2845
|
+
lineCount: count2
|
|
2353
2846
|
})).nonempty(),
|
|
2354
|
-
decisionsChecked:
|
|
2355
|
-
clean:
|
|
2356
|
-
issues:
|
|
2357
|
-
advisories:
|
|
2358
|
-
suppressedAdvisories:
|
|
2359
|
-
skippedChecks:
|
|
2847
|
+
decisionsChecked: z7.boolean(),
|
|
2848
|
+
clean: z7.boolean(),
|
|
2849
|
+
issues: z7.array(issueSchema),
|
|
2850
|
+
advisories: z7.array(advisorySchema),
|
|
2851
|
+
suppressedAdvisories: z7.array(advisorySchema).optional(),
|
|
2852
|
+
skippedChecks: z7.array(z7.object({ check: z7.string(), reason: z7.string(), doc: z7.string().optional() }))
|
|
2360
2853
|
});
|
|
2361
|
-
baselineSchema =
|
|
2362
|
-
kind:
|
|
2363
|
-
version:
|
|
2364
|
-
createdAt:
|
|
2854
|
+
baselineSchema = z7.object({
|
|
2855
|
+
kind: z7.literal("mason-audit-repair"),
|
|
2856
|
+
version: z7.literal(1),
|
|
2857
|
+
createdAt: z7.string().datetime(),
|
|
2365
2858
|
report: reportSchema,
|
|
2366
|
-
digest:
|
|
2859
|
+
digest: z7.string().regex(/^[a-f0-9]{64}$/)
|
|
2367
2860
|
});
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2861
|
+
repairFindingSchema = z7.object({
|
|
2862
|
+
id: z7.string(),
|
|
2863
|
+
original: z7.union([issueSchema, advisorySchema]),
|
|
2864
|
+
status: z7.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
2865
|
+
reason: z7.string(),
|
|
2866
|
+
current: z7.union([issueSchema, advisorySchema]).optional(),
|
|
2867
|
+
review: reviewSummarySchema.optional()
|
|
2375
2868
|
});
|
|
2376
2869
|
}
|
|
2377
2870
|
});
|
|
2378
2871
|
|
|
2379
2872
|
// src/automation/evidence.ts
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
checkCache: () => checkCache,
|
|
2383
|
-
git: () => git2,
|
|
2384
|
-
hash: () => hash,
|
|
2385
|
-
readInputs: () => readInputs,
|
|
2386
|
-
workspace: () => workspace
|
|
2387
|
-
});
|
|
2388
|
-
import fs12 from "fs/promises";
|
|
2389
|
-
import path17 from "path";
|
|
2873
|
+
import fs13 from "fs/promises";
|
|
2874
|
+
import path20 from "path";
|
|
2390
2875
|
import { createHash as createHash3 } from "crypto";
|
|
2391
|
-
import { execFile as
|
|
2392
|
-
import { promisify as
|
|
2393
|
-
import
|
|
2394
|
-
import { z as
|
|
2395
|
-
async function
|
|
2876
|
+
import { execFile as execFile9 } from "child_process";
|
|
2877
|
+
import { promisify as promisify9 } from "util";
|
|
2878
|
+
import fg5 from "fast-glob";
|
|
2879
|
+
import { z as z8 } from "zod";
|
|
2880
|
+
async function git3(root, ...args2) {
|
|
2396
2881
|
return (await exec7("git", args2, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
|
|
2397
2882
|
}
|
|
2398
2883
|
async function workspace(dir) {
|
|
2399
|
-
const root = await
|
|
2400
|
-
const gitDir = await
|
|
2884
|
+
const root = await fs13.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
|
|
2885
|
+
const gitDir = await fs13.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
2401
2886
|
let branch;
|
|
2402
2887
|
try {
|
|
2403
|
-
branch = (await
|
|
2888
|
+
branch = (await git3(root, "symbolic-ref", "--quiet", "HEAD")).trim();
|
|
2404
2889
|
} catch {
|
|
2405
2890
|
branch = "detached";
|
|
2406
2891
|
}
|
|
2407
2892
|
return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
|
|
2408
2893
|
}
|
|
2409
2894
|
async function readInputs(root) {
|
|
2895
|
+
const docPaths = await discoverDocPaths(root);
|
|
2410
2896
|
const [headText, docStatus, shallowPath, replacements] = await Promise.all([
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2897
|
+
git3(root, "rev-parse", "HEAD"),
|
|
2898
|
+
docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
|
|
2899
|
+
git3(root, "rev-parse", "--git-path", "shallow"),
|
|
2900
|
+
git3(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
|
|
2415
2901
|
]);
|
|
2416
2902
|
const head = headText.trim();
|
|
2417
2903
|
let shallow = null;
|
|
2418
2904
|
try {
|
|
2419
|
-
shallow = await
|
|
2905
|
+
shallow = await fs13.readFile(path20.resolve(root, shallowPath.trim()), "utf8");
|
|
2420
2906
|
} catch (error) {
|
|
2421
2907
|
if (error.code !== "ENOENT") throw error;
|
|
2422
2908
|
}
|
|
2423
2909
|
const docs = {};
|
|
2424
2910
|
const docContents = [];
|
|
2425
2911
|
const claims = [];
|
|
2426
|
-
for (const file of
|
|
2912
|
+
for (const file of docPaths) {
|
|
2427
2913
|
const text2 = await content(root, file);
|
|
2428
2914
|
docs[file] = text2 === null ? null : hash(text2);
|
|
2429
2915
|
docContents.push([file, text2]);
|
|
2430
2916
|
for (const claim of text2 ? extractClaims(text2).paths : []) {
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
const countClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).counts : []);
|
|
2438
|
-
const commandClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).commands : []);
|
|
2439
|
-
const rootPackage = await content(root, "package.json");
|
|
2440
|
-
let rootScripts = null;
|
|
2441
|
-
if (rootPackage !== null) {
|
|
2442
|
-
try {
|
|
2443
|
-
const pkg = JSON.parse(rootPackage);
|
|
2444
|
-
rootScripts = pkg?.scripts && typeof pkg.scripts === "object" ? Object.keys(pkg.scripts) : [];
|
|
2445
|
-
} catch {
|
|
2917
|
+
const scope = pathClaimScope(file, claim);
|
|
2918
|
+
if (scope?.candidates.some(gitMetadataPath)) continue;
|
|
2919
|
+
for (const candidate of scope?.candidates ?? []) {
|
|
2920
|
+
if (optionalMasonPath(candidate)) continue;
|
|
2921
|
+
claims.push([candidate, await pathExists(root, candidate), await pathExists(root, path20.posix.dirname(candidate))]);
|
|
2922
|
+
}
|
|
2446
2923
|
}
|
|
2447
2924
|
}
|
|
2448
|
-
const
|
|
2925
|
+
const parsedDocs = docContents.map(([file, text2]) => ({ path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") }));
|
|
2926
|
+
const combinedDocs = moduleDocumentation(parsedDocs);
|
|
2927
|
+
const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
|
|
2449
2928
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
2450
|
-
const decisionPresence = await
|
|
2929
|
+
const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
2451
2930
|
if (error.code === "ENOENT") return "absent";
|
|
2452
2931
|
throw error;
|
|
2453
2932
|
});
|
|
2454
|
-
const [modules, counts,
|
|
2933
|
+
const [modules, counts, commands, decisionFiles] = await Promise.all([
|
|
2455
2934
|
combinedDocs ? moduleCandidates(root, combinedDocs) : [],
|
|
2456
|
-
Promise.all(countClaims.map((claim) =>
|
|
2457
|
-
|
|
2458
|
-
|
|
2935
|
+
Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
|
|
2936
|
+
commandInputs(root, parsedDocs),
|
|
2937
|
+
fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
|
|
2459
2938
|
]);
|
|
2460
|
-
const packages = [["package.json", rootPackage], ...await Promise.all(manifests.sort().map(async (file) => [file, await content(root, file)]))];
|
|
2461
2939
|
const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
|
|
2462
2940
|
await storePath(root, file);
|
|
2463
2941
|
return [file, await content(root, file)];
|
|
2464
2942
|
}));
|
|
2465
2943
|
const [status, index2] = decisions.length ? await Promise.all([
|
|
2466
|
-
|
|
2467
|
-
|
|
2944
|
+
git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
2945
|
+
git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
2468
2946
|
]) : ["", ""];
|
|
2469
|
-
const common = [
|
|
2947
|
+
const common = [4, engineVersion, head, shallow, replacements, docContents];
|
|
2470
2948
|
const keys = {
|
|
2471
2949
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
2472
2950
|
"new-module": hash([common, modules]),
|
|
2473
2951
|
"stale-count": hash([common, counts]),
|
|
2474
|
-
"dead-command": hash([common,
|
|
2952
|
+
"dead-command": hash([common, commands]),
|
|
2475
2953
|
"deps-changed": hash([common, docStatus]),
|
|
2476
2954
|
"decision-anchor-drift": hash([common, decisionPresence, decisions, status, index2])
|
|
2477
2955
|
};
|
|
2478
|
-
return { fingerprint: hash(keys), head, docs, keys };
|
|
2956
|
+
return { fingerprint: hash([keys, await advisoryReviewInventory(root)]), head, docs, keys };
|
|
2479
2957
|
}
|
|
2480
2958
|
function checkCache(raw, inputs) {
|
|
2481
2959
|
let entries = {};
|
|
@@ -2502,11 +2980,12 @@ function checkCache(raw, inputs) {
|
|
|
2502
2980
|
return { version: 1, entries: canonical, digest: hash(canonical) };
|
|
2503
2981
|
} };
|
|
2504
2982
|
}
|
|
2505
|
-
var exec7, engineVersion, hash, content,
|
|
2983
|
+
var exec7, engineVersion, hash, content, cacheSchema;
|
|
2506
2984
|
var init_evidence = __esm({
|
|
2507
2985
|
"src/automation/evidence.ts"() {
|
|
2508
2986
|
"use strict";
|
|
2509
2987
|
init_docs();
|
|
2988
|
+
init_scope();
|
|
2510
2989
|
init_claims();
|
|
2511
2990
|
init_repair();
|
|
2512
2991
|
init_checks();
|
|
@@ -2515,20 +2994,20 @@ var init_evidence = __esm({
|
|
|
2515
2994
|
init_stale_count();
|
|
2516
2995
|
init_dead_command();
|
|
2517
2996
|
init_storage();
|
|
2518
|
-
|
|
2519
|
-
|
|
2997
|
+
init_advisory_review();
|
|
2998
|
+
exec7 = promisify9(execFile9);
|
|
2999
|
+
engineVersion = true ? "0.17.1" : "development";
|
|
2520
3000
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2521
3001
|
content = readAuditInput;
|
|
2522
|
-
|
|
2523
|
-
cacheSchema = z6.object({ version: z6.literal(1), entries: z6.record(z6.object({ key: z6.string(), result: checkResultSchema })), digest: z6.string() });
|
|
3002
|
+
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
2524
3003
|
}
|
|
2525
3004
|
});
|
|
2526
3005
|
|
|
2527
3006
|
// src/automation/execution.ts
|
|
2528
3007
|
import os2 from "os";
|
|
2529
|
-
import
|
|
3008
|
+
import fs14 from "fs/promises";
|
|
2530
3009
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2531
|
-
import { z as
|
|
3010
|
+
import { z as z9 } from "zod";
|
|
2532
3011
|
function parseExecution(raw) {
|
|
2533
3012
|
try {
|
|
2534
3013
|
return executionSchema.parse(raw);
|
|
@@ -2546,7 +3025,7 @@ function automationFailure(error) {
|
|
|
2546
3025
|
codes.add(String(cause.code));
|
|
2547
3026
|
cause = cause.cause;
|
|
2548
3027
|
}
|
|
2549
|
-
const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof
|
|
3028
|
+
const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z9.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
|
|
2550
3029
|
return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
|
|
2551
3030
|
}
|
|
2552
3031
|
function failureMessage(error) {
|
|
@@ -2654,7 +3133,7 @@ async function recordExecution(root, directory, event, run) {
|
|
|
2654
3133
|
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
2655
3134
|
}
|
|
2656
3135
|
} finally {
|
|
2657
|
-
await
|
|
3136
|
+
await fs14.rm(await storePath(root, owner), { force: true }).catch(() => {
|
|
2658
3137
|
});
|
|
2659
3138
|
}
|
|
2660
3139
|
}
|
|
@@ -2675,33 +3154,33 @@ var init_execution = __esm({
|
|
|
2675
3154
|
init_store();
|
|
2676
3155
|
init_evidence();
|
|
2677
3156
|
init_storage();
|
|
2678
|
-
failureSchema =
|
|
2679
|
-
code:
|
|
2680
|
-
message:
|
|
2681
|
-
retryable:
|
|
2682
|
-
receiptRecorded:
|
|
3157
|
+
failureSchema = z9.object({
|
|
3158
|
+
code: z9.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
|
|
3159
|
+
message: z9.string(),
|
|
3160
|
+
retryable: z9.boolean(),
|
|
3161
|
+
receiptRecorded: z9.boolean()
|
|
2683
3162
|
});
|
|
2684
|
-
attemptSchema =
|
|
2685
|
-
id:
|
|
2686
|
-
lease:
|
|
2687
|
-
event:
|
|
2688
|
-
startedAt:
|
|
2689
|
-
finishedAt:
|
|
2690
|
-
durationMs:
|
|
2691
|
-
pid:
|
|
2692
|
-
host:
|
|
2693
|
-
status:
|
|
2694
|
-
verificationStatus:
|
|
2695
|
-
reportPath:
|
|
3163
|
+
attemptSchema = z9.object({
|
|
3164
|
+
id: z9.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
3165
|
+
lease: z9.literal(true).optional(),
|
|
3166
|
+
event: z9.string(),
|
|
3167
|
+
startedAt: z9.string(),
|
|
3168
|
+
finishedAt: z9.string().optional(),
|
|
3169
|
+
durationMs: z9.number().nonnegative().optional(),
|
|
3170
|
+
pid: z9.number().int().positive(),
|
|
3171
|
+
host: z9.string(),
|
|
3172
|
+
status: z9.enum(["running", "completed", "failed", "unknown"]),
|
|
3173
|
+
verificationStatus: z9.string().optional(),
|
|
3174
|
+
reportPath: z9.string().optional(),
|
|
2696
3175
|
failure: failureSchema.optional()
|
|
2697
3176
|
});
|
|
2698
|
-
executionSchema =
|
|
2699
|
-
notificationsSchema =
|
|
2700
|
-
signature:
|
|
3177
|
+
executionSchema = z9.object({ version: z9.literal(1), attempts: z9.array(attemptSchema).max(160), discardedAttempts: z9.number().int().nonnegative().default(0) });
|
|
3178
|
+
notificationsSchema = z9.object({ version: z9.literal(1), sessions: z9.record(z9.object({
|
|
3179
|
+
signature: z9.string(),
|
|
2701
3180
|
code: failureSchema.shape.code,
|
|
2702
|
-
message:
|
|
2703
|
-
count:
|
|
2704
|
-
lastAt:
|
|
3181
|
+
message: z9.string(),
|
|
3182
|
+
count: z9.number().int().positive(),
|
|
3183
|
+
lastAt: z9.string()
|
|
2705
3184
|
})) });
|
|
2706
3185
|
leasePath = (directory, id) => `${directory}/executions/${id}.json`;
|
|
2707
3186
|
}
|
|
@@ -2709,7 +3188,7 @@ var init_execution = __esm({
|
|
|
2709
3188
|
|
|
2710
3189
|
// src/automation/runtime.ts
|
|
2711
3190
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2712
|
-
import { z as
|
|
3191
|
+
import { z as z10 } from "zod";
|
|
2713
3192
|
async function observeReadOnlyTool(dir, event) {
|
|
2714
3193
|
const ws = await workspace(dir);
|
|
2715
3194
|
await withLock(ws.root, ws.directory, async () => {
|
|
@@ -2787,7 +3266,7 @@ async function computeAnalysis(ws, state, inputs, event) {
|
|
|
2787
3266
|
baselinePaths: [],
|
|
2788
3267
|
reportPath: "",
|
|
2789
3268
|
findings: [],
|
|
2790
|
-
diagnostics: ["No
|
|
3269
|
+
diagnostics: ["No README.md or agent instruction files were discovered. Documentation capture is unavailable; other Mason tools remain usable."],
|
|
2791
3270
|
checks: { ran: [], reused: [], skipped: [] },
|
|
2792
3271
|
counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
|
|
2793
3272
|
capture: "unknown",
|
|
@@ -2798,7 +3277,7 @@ async function computeAnalysis(ws, state, inputs, event) {
|
|
|
2798
3277
|
const previous = await readStoreJson(ws.root, state.latest);
|
|
2799
3278
|
if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
|
|
2800
3279
|
try {
|
|
2801
|
-
await
|
|
3280
|
+
await git3(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
|
|
2802
3281
|
} catch {
|
|
2803
3282
|
throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
|
|
2804
3283
|
}
|
|
@@ -3003,43 +3482,76 @@ var init_runtime = __esm({
|
|
|
3003
3482
|
init_storage();
|
|
3004
3483
|
init_evidence();
|
|
3005
3484
|
init_store();
|
|
3006
|
-
SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved
|
|
3485
|
+
SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved findings no longer fail their checks or have a recorded assessment covering current evidence. Repair and review only within the user's task authorization.";
|
|
3007
3486
|
priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
|
|
3008
3487
|
cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
|
|
3009
|
-
analysisSchema =
|
|
3010
|
-
id:
|
|
3011
|
-
fingerprint:
|
|
3012
|
-
completedAt:
|
|
3013
|
-
baselineDigest:
|
|
3014
|
-
report:
|
|
3015
|
-
version:
|
|
3016
|
-
status:
|
|
3017
|
-
root:
|
|
3018
|
-
branch:
|
|
3019
|
-
head:
|
|
3020
|
-
baselinePaths:
|
|
3021
|
-
reportPath:
|
|
3022
|
-
findings:
|
|
3023
|
-
diagnostics:
|
|
3024
|
-
checks:
|
|
3025
|
-
ran:
|
|
3026
|
-
reused:
|
|
3027
|
-
skipped:
|
|
3488
|
+
analysisSchema = z10.object({
|
|
3489
|
+
id: z10.string(),
|
|
3490
|
+
fingerprint: z10.string(),
|
|
3491
|
+
completedAt: z10.number(),
|
|
3492
|
+
baselineDigest: z10.string(),
|
|
3493
|
+
report: z10.object({
|
|
3494
|
+
version: z10.literal(1),
|
|
3495
|
+
status: z10.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
|
|
3496
|
+
root: z10.string(),
|
|
3497
|
+
branch: z10.string(),
|
|
3498
|
+
head: z10.string(),
|
|
3499
|
+
baselinePaths: z10.array(z10.string()),
|
|
3500
|
+
reportPath: z10.string(),
|
|
3501
|
+
findings: z10.array(repairFindingSchema),
|
|
3502
|
+
diagnostics: z10.array(z10.string()),
|
|
3503
|
+
checks: z10.object({
|
|
3504
|
+
ran: z10.array(z10.string()),
|
|
3505
|
+
reused: z10.array(z10.string()),
|
|
3506
|
+
skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() }))
|
|
3028
3507
|
}),
|
|
3029
|
-
counts:
|
|
3030
|
-
capture:
|
|
3031
|
-
scope:
|
|
3508
|
+
counts: z10.object({ resolved: z10.number(), unresolved: z10.number(), "review-required": z10.number(), unverified: z10.number() }),
|
|
3509
|
+
capture: z10.enum(["observed", "unknown"]),
|
|
3510
|
+
scope: z10.string()
|
|
3032
3511
|
})
|
|
3033
3512
|
});
|
|
3034
|
-
sharedSchema =
|
|
3513
|
+
sharedSchema = z10.object({ analysis: analysisSchema, digest: z10.string() });
|
|
3035
3514
|
baselineDigest = async (ws, state) => hash(await Promise.all(
|
|
3036
3515
|
state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
|
|
3037
3516
|
));
|
|
3038
3517
|
}
|
|
3039
3518
|
});
|
|
3040
3519
|
|
|
3520
|
+
// src/automation/hook-root.ts
|
|
3521
|
+
import fs15 from "fs/promises";
|
|
3522
|
+
import path21 from "path";
|
|
3523
|
+
async function repositoryRoot(dir) {
|
|
3524
|
+
if (!path21.isAbsolute(dir)) throw new Error("Hook input working directory must be absolute.");
|
|
3525
|
+
try {
|
|
3526
|
+
return await fs15.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
|
|
3527
|
+
} catch (error) {
|
|
3528
|
+
if (error.code === "ENOENT" || /not a git repository/.test(String(error.stderr))) return null;
|
|
3529
|
+
throw error;
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
async function resolveHookRoot(host, cwd, options = {}) {
|
|
3533
|
+
const current = await repositoryRoot(cwd);
|
|
3534
|
+
if (current) return current;
|
|
3535
|
+
const projectDir = options.projectDir ?? (host === "claude" ? process.env.CLAUDE_PROJECT_DIR : void 0);
|
|
3536
|
+
const projectRoot = projectDir ? await repositoryRoot(projectDir) : null;
|
|
3537
|
+
if (projectDir && !projectRoot) throw new Error("Hook input is outside Git and the host project is no longer a Git repository. Restore its history before running mason check.");
|
|
3538
|
+
const launchRoot = await repositoryRoot(options.launchCwd ?? process.cwd());
|
|
3539
|
+
const roots = [...new Set([launchRoot, projectRoot].filter((root) => root !== null))];
|
|
3540
|
+
if (!roots.length) throw new Error("Hook input is outside a Git repository and no project root is available. Return to the project and run mason check.");
|
|
3541
|
+
if (roots.length > 1) throw new Error("Hook input is outside a Git repository and project roots disagree. Return to the intended checkout and run mason check; verification was not established.");
|
|
3542
|
+
const worktrees = (await git3(roots[0], "worktree", "list", "--porcelain", "-z")).split("\0").filter((field) => field.startsWith("worktree "));
|
|
3543
|
+
if (worktrees.length !== 1) throw new Error("Hook input is outside a Git repository with multiple worktrees. Return to the intended worktree and run mason check; the active checkout is unknown.");
|
|
3544
|
+
return roots[0];
|
|
3545
|
+
}
|
|
3546
|
+
var init_hook_root = __esm({
|
|
3547
|
+
"src/automation/hook-root.ts"() {
|
|
3548
|
+
"use strict";
|
|
3549
|
+
init_evidence();
|
|
3550
|
+
}
|
|
3551
|
+
});
|
|
3552
|
+
|
|
3041
3553
|
// src/setup/model.ts
|
|
3042
|
-
import { z as
|
|
3554
|
+
import { z as z11 } from "zod";
|
|
3043
3555
|
async function loadSetup(root) {
|
|
3044
3556
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
3045
3557
|
if (raw === null) return null;
|
|
@@ -3057,27 +3569,27 @@ var init_model = __esm({
|
|
|
3057
3569
|
"src/setup/model.ts"() {
|
|
3058
3570
|
"use strict";
|
|
3059
3571
|
init_storage();
|
|
3060
|
-
setupHostSchema =
|
|
3061
|
-
configuredVersion:
|
|
3062
|
-
revision:
|
|
3063
|
-
fingerprint:
|
|
3064
|
-
mcpFingerprint:
|
|
3065
|
-
instructions:
|
|
3572
|
+
setupHostSchema = z11.object({
|
|
3573
|
+
configuredVersion: z11.string(),
|
|
3574
|
+
revision: z11.string().uuid(),
|
|
3575
|
+
fingerprint: z11.string(),
|
|
3576
|
+
mcpFingerprint: z11.string(),
|
|
3577
|
+
instructions: z11.array(z11.string())
|
|
3066
3578
|
});
|
|
3067
|
-
setupSchema =
|
|
3579
|
+
setupSchema = z11.object({ version: z11.literal(2), hosts: z11.object({
|
|
3068
3580
|
codex: setupHostSchema.optional(),
|
|
3069
3581
|
claude: setupHostSchema.optional()
|
|
3070
3582
|
}) });
|
|
3071
3583
|
SETUP_PATH = ".mason/local/setup.json";
|
|
3072
|
-
receiptSchema =
|
|
3073
|
-
version:
|
|
3074
|
-
host:
|
|
3075
|
-
status:
|
|
3076
|
-
initialReportPath:
|
|
3077
|
-
initialBaselinePaths:
|
|
3078
|
-
root:
|
|
3079
|
-
revision:
|
|
3080
|
-
configuredAt:
|
|
3584
|
+
receiptSchema = z11.object({
|
|
3585
|
+
version: z11.literal(1),
|
|
3586
|
+
host: z11.enum(["codex", "claude"]),
|
|
3587
|
+
status: z11.enum(["installing", "configured"]),
|
|
3588
|
+
initialReportPath: z11.string(),
|
|
3589
|
+
initialBaselinePaths: z11.array(z11.string()),
|
|
3590
|
+
root: z11.string(),
|
|
3591
|
+
revision: z11.string().uuid(),
|
|
3592
|
+
configuredAt: z11.string().optional()
|
|
3081
3593
|
});
|
|
3082
3594
|
}
|
|
3083
3595
|
});
|
|
@@ -3088,20 +3600,21 @@ __export(launcher_exports, {
|
|
|
3088
3600
|
executingVersion: () => executingVersion,
|
|
3089
3601
|
globalCommand: () => globalCommand,
|
|
3090
3602
|
hookCommand: () => hookCommand,
|
|
3603
|
+
inactiveSetupMessage: () => inactiveSetupMessage,
|
|
3091
3604
|
installedCommand: () => installedCommand,
|
|
3092
3605
|
mcpCommand: () => mcpCommand,
|
|
3093
3606
|
prepareLaunch: () => prepareLaunch
|
|
3094
3607
|
});
|
|
3095
|
-
import { execFile as
|
|
3096
|
-
import { promisify as
|
|
3097
|
-
import
|
|
3608
|
+
import { execFile as execFile10 } from "child_process";
|
|
3609
|
+
import { promisify as promisify10 } from "util";
|
|
3610
|
+
import fs16 from "fs/promises";
|
|
3098
3611
|
function globalCommand(args2, platform = process.platform) {
|
|
3099
3612
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
|
|
3100
3613
|
}
|
|
3101
3614
|
async function installedCommand() {
|
|
3102
3615
|
const invocation = globalCommand(["internal-integration-version"]);
|
|
3103
3616
|
try {
|
|
3104
|
-
const { stdout } = await
|
|
3617
|
+
const { stdout } = await promisify10(execFile10)(
|
|
3105
3618
|
invocation.command,
|
|
3106
3619
|
invocation.args,
|
|
3107
3620
|
{ timeout: 5e3, maxBuffer: 8192, windowsHide: true }
|
|
@@ -3114,24 +3627,30 @@ async function installedCommand() {
|
|
|
3114
3627
|
}
|
|
3115
3628
|
}
|
|
3116
3629
|
async function prepareLaunch(host, options = {}) {
|
|
3117
|
-
const root = await
|
|
3630
|
+
const root = await fs16.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
|
|
3118
3631
|
const entry = (await loadSetup(root))?.hosts[host];
|
|
3632
|
+
process.env.MASON_SETUP_ROOT = root;
|
|
3633
|
+
process.env.MASON_SETUP_HOST = host;
|
|
3634
|
+
delete process.env.MASON_SETUP_REVISION;
|
|
3119
3635
|
if (!entry) {
|
|
3120
3636
|
if (options.allowInactive) return false;
|
|
3121
3637
|
throw new Error(`Mason is not configured locally for ${host}. Run mason setup --host ${host} in this checkout.`);
|
|
3122
3638
|
}
|
|
3123
|
-
process.env.MASON_SETUP_ROOT = root;
|
|
3124
|
-
process.env.MASON_SETUP_HOST = host;
|
|
3125
3639
|
process.env.MASON_SETUP_REVISION = entry.revision;
|
|
3126
3640
|
return true;
|
|
3127
3641
|
}
|
|
3642
|
+
function inactiveSetupMessage() {
|
|
3643
|
+
const host = process.env.MASON_SETUP_HOST;
|
|
3644
|
+
if (!process.env.MASON_SETUP_ROOT || process.env.MASON_SETUP_REVISION || host !== "claude" && host !== "codex") return null;
|
|
3645
|
+
return `Mason MCP is available, but automatic hooks are inactive in ${process.env.MASON_SETUP_ROOT}. Run mason setup --host ${host} in that checkout, review the host configuration, then restart the assistant. Setup has not been performed automatically.`;
|
|
3646
|
+
}
|
|
3128
3647
|
var executingVersion, mcpCommand, hookCommand;
|
|
3129
3648
|
var init_launcher = __esm({
|
|
3130
3649
|
"src/setup/launcher.ts"() {
|
|
3131
3650
|
"use strict";
|
|
3132
3651
|
init_evidence();
|
|
3133
3652
|
init_model();
|
|
3134
|
-
executingVersion = () => true ? "0.
|
|
3653
|
+
executingVersion = () => true ? "0.17.1" : "development";
|
|
3135
3654
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3136
3655
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3137
3656
|
}
|
|
@@ -3144,8 +3663,8 @@ __export(observations_exports, {
|
|
|
3144
3663
|
observeActivation: () => observeActivation,
|
|
3145
3664
|
readObservation: () => readObservation
|
|
3146
3665
|
});
|
|
3147
|
-
import
|
|
3148
|
-
import { z as
|
|
3666
|
+
import fs17 from "fs/promises";
|
|
3667
|
+
import { z as z12 } from "zod";
|
|
3149
3668
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
3150
3669
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
3151
3670
|
}
|
|
@@ -3158,11 +3677,17 @@ async function readObservation(root, directory, host, revision, version = execut
|
|
|
3158
3677
|
}
|
|
3159
3678
|
async function observeActivation(dir, event, options = {}) {
|
|
3160
3679
|
const host = process.env.MASON_SETUP_HOST, revision = process.env.MASON_SETUP_REVISION;
|
|
3161
|
-
if (host !== "codex" && host !== "claude" || !
|
|
3680
|
+
if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
|
|
3162
3681
|
try {
|
|
3682
|
+
if (!revision) {
|
|
3683
|
+
if ((await loadSetup(process.env.MASON_SETUP_ROOT))?.hosts[host]) {
|
|
3684
|
+
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.";
|
|
3685
|
+
}
|
|
3686
|
+
return inactiveSetupMessage();
|
|
3687
|
+
}
|
|
3163
3688
|
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];
|
|
3164
|
-
const ws = capturedDirectory ? { root: await
|
|
3165
|
-
if (ws.root !== await
|
|
3689
|
+
const ws = capturedDirectory ? { root: await fs17.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
3690
|
+
if (ws.root !== await fs17.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
3166
3691
|
const setup = await loadSetup(ws.root);
|
|
3167
3692
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
3168
3693
|
const directory = ws.directory + "/activation";
|
|
@@ -3210,26 +3735,26 @@ var init_observations = __esm({
|
|
|
3210
3735
|
init_storage();
|
|
3211
3736
|
init_model();
|
|
3212
3737
|
init_launcher();
|
|
3213
|
-
observationSchema =
|
|
3214
|
-
version:
|
|
3215
|
-
masonVersion:
|
|
3216
|
-
root:
|
|
3217
|
-
revision:
|
|
3218
|
-
host:
|
|
3219
|
-
contextCalls:
|
|
3220
|
-
lastContextAt:
|
|
3221
|
-
sessions:
|
|
3222
|
-
events:
|
|
3223
|
-
at:
|
|
3224
|
-
verificationStatus:
|
|
3225
|
-
reportPath:
|
|
3738
|
+
observationSchema = z12.object({
|
|
3739
|
+
version: z12.literal(1),
|
|
3740
|
+
masonVersion: z12.string(),
|
|
3741
|
+
root: z12.string(),
|
|
3742
|
+
revision: z12.string(),
|
|
3743
|
+
host: z12.enum(["codex", "claude"]),
|
|
3744
|
+
contextCalls: z12.number().int().nonnegative(),
|
|
3745
|
+
lastContextAt: z12.string().optional(),
|
|
3746
|
+
sessions: z12.record(z12.object({
|
|
3747
|
+
events: z12.array(z12.enum(events)),
|
|
3748
|
+
at: z12.string(),
|
|
3749
|
+
verificationStatus: z12.string().optional(),
|
|
3750
|
+
reportPath: z12.string().optional()
|
|
3226
3751
|
}))
|
|
3227
3752
|
});
|
|
3228
3753
|
}
|
|
3229
3754
|
});
|
|
3230
3755
|
|
|
3231
3756
|
// src/automation/adapters.ts
|
|
3232
|
-
import { z as
|
|
3757
|
+
import { z as z13 } from "zod";
|
|
3233
3758
|
function normalizeHook(host, raw) {
|
|
3234
3759
|
hostSchema.parse(host);
|
|
3235
3760
|
const input = inputSchema.parse(raw);
|
|
@@ -3243,7 +3768,7 @@ function normalizeHook(host, raw) {
|
|
|
3243
3768
|
stopHookActive: input.stop_hook_active || input.permission_mode === "plan"
|
|
3244
3769
|
} };
|
|
3245
3770
|
}
|
|
3246
|
-
async function runAutomationHook(host, stdin) {
|
|
3771
|
+
async function runAutomationHook(host, stdin, options = {}) {
|
|
3247
3772
|
let name = "";
|
|
3248
3773
|
let hook;
|
|
3249
3774
|
try {
|
|
@@ -3257,6 +3782,11 @@ async function runAutomationHook(host, stdin) {
|
|
|
3257
3782
|
const input = normalizeHook(host, raw);
|
|
3258
3783
|
hook = input;
|
|
3259
3784
|
name = input.name;
|
|
3785
|
+
if (options.managed) {
|
|
3786
|
+
input.cwd = await resolveHookRoot(host, input.cwd);
|
|
3787
|
+
const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
|
|
3788
|
+
if (!await prepareLaunch2(host, { dir: input.cwd, allowInactive: true })) return null;
|
|
3789
|
+
}
|
|
3260
3790
|
if (["before_tool", "after_tool"].includes(input.event.event) && input.readOnly) {
|
|
3261
3791
|
const observed = await observeReadOnlyTool(input.cwd, input.event);
|
|
3262
3792
|
const { observeActivation: observeActivation3 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
|
|
@@ -3289,6 +3819,9 @@ function managedHookCommand(host, platform = process.platform) {
|
|
|
3289
3819
|
function knownManagedHookCommands(host) {
|
|
3290
3820
|
return [managedHookCommand(host, "linux"), managedHookCommand(host, "win32"), `mason --setup-host ${host} auto hook --host ${host}`];
|
|
3291
3821
|
}
|
|
3822
|
+
function compatibleManagedHookCommands(host, platform = process.platform) {
|
|
3823
|
+
return [managedHookCommand(host, platform), `mason --setup-host ${host} auto hook --host ${host}`];
|
|
3824
|
+
}
|
|
3292
3825
|
function hookConfig(host, command2 = "npx --no-install --package mason-context mason-auto") {
|
|
3293
3826
|
const handler = { type: "command", command: command2 === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command2 + " hook --host " + host, timeout: 30 };
|
|
3294
3827
|
return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
|
|
@@ -3303,15 +3836,16 @@ var init_adapters = __esm({
|
|
|
3303
3836
|
init_execution();
|
|
3304
3837
|
init_runtime();
|
|
3305
3838
|
init_store();
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3839
|
+
init_hook_root();
|
|
3840
|
+
inputSchema = z13.object({
|
|
3841
|
+
cwd: z13.string().min(1),
|
|
3842
|
+
session_id: z13.string().min(1).max(500),
|
|
3843
|
+
hook_event_name: z13.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
|
|
3844
|
+
tool_name: z13.string().optional(),
|
|
3845
|
+
tool_use_id: z13.string().max(500).optional(),
|
|
3846
|
+
tool_input: z13.unknown().optional(),
|
|
3847
|
+
stop_hook_active: z13.boolean().optional(),
|
|
3848
|
+
permission_mode: z13.string().optional()
|
|
3315
3849
|
});
|
|
3316
3850
|
lifecycle = {
|
|
3317
3851
|
SessionStart: "session_start",
|
|
@@ -3331,8 +3865,8 @@ __export(files_exports, {
|
|
|
3331
3865
|
managedBlock: () => managedBlock,
|
|
3332
3866
|
readText: () => readText
|
|
3333
3867
|
});
|
|
3334
|
-
import
|
|
3335
|
-
import
|
|
3868
|
+
import fs18 from "fs/promises";
|
|
3869
|
+
import path22 from "path";
|
|
3336
3870
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
3337
3871
|
async function readText(root, file) {
|
|
3338
3872
|
try {
|
|
@@ -3361,14 +3895,14 @@ async function applyEdit(root, edit) {
|
|
|
3361
3895
|
if (edit.after === null) {
|
|
3362
3896
|
const file2 = await storePath(root, edit.path);
|
|
3363
3897
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
3364
|
-
await
|
|
3898
|
+
await fs18.unlink(file2);
|
|
3365
3899
|
return true;
|
|
3366
3900
|
}
|
|
3367
3901
|
const file = await storePath(root, edit.path, true);
|
|
3368
|
-
const temporary =
|
|
3902
|
+
const temporary = path22.join(path22.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
3369
3903
|
try {
|
|
3370
|
-
const mode = await
|
|
3371
|
-
const handle = await
|
|
3904
|
+
const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
|
|
3905
|
+
const handle = await fs18.open(temporary, "wx", mode);
|
|
3372
3906
|
try {
|
|
3373
3907
|
await handle.writeFile(edit.after, "utf8");
|
|
3374
3908
|
await handle.sync();
|
|
@@ -3376,10 +3910,10 @@ async function applyEdit(root, edit) {
|
|
|
3376
3910
|
await handle.close();
|
|
3377
3911
|
}
|
|
3378
3912
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
3379
|
-
await
|
|
3913
|
+
await fs18.rename(temporary, file);
|
|
3380
3914
|
return true;
|
|
3381
3915
|
} finally {
|
|
3382
|
-
await
|
|
3916
|
+
await fs18.rm(temporary, { force: true });
|
|
3383
3917
|
}
|
|
3384
3918
|
}
|
|
3385
3919
|
var init_files2 = __esm({
|
|
@@ -3400,7 +3934,7 @@ __export(ownership_exports, {
|
|
|
3400
3934
|
ownershipEdit: () => ownershipEdit,
|
|
3401
3935
|
ownershipSchema: () => ownershipSchema
|
|
3402
3936
|
});
|
|
3403
|
-
import { z as
|
|
3937
|
+
import { z as z14 } from "zod";
|
|
3404
3938
|
import { parse } from "smol-toml";
|
|
3405
3939
|
function blockSpan(text2, start, end) {
|
|
3406
3940
|
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
@@ -3451,30 +3985,30 @@ var init_ownership = __esm({
|
|
|
3451
3985
|
["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
|
|
3452
3986
|
["# mason:mcp:start", "# mason:mcp:end"]
|
|
3453
3987
|
];
|
|
3454
|
-
fileSchema =
|
|
3455
|
-
created:
|
|
3456
|
-
initialHash:
|
|
3457
|
-
blocks:
|
|
3458
|
-
mcpHash:
|
|
3459
|
-
hookCommands:
|
|
3460
|
-
hooks:
|
|
3988
|
+
fileSchema = z14.object({
|
|
3989
|
+
created: z14.boolean(),
|
|
3990
|
+
initialHash: z14.string(),
|
|
3991
|
+
blocks: z14.record(z14.string()),
|
|
3992
|
+
mcpHash: z14.string().optional(),
|
|
3993
|
+
hookCommands: z14.array(z14.string()).optional(),
|
|
3994
|
+
hooks: z14.record(z14.array(z14.string())).optional()
|
|
3461
3995
|
});
|
|
3462
|
-
ownershipSchema =
|
|
3996
|
+
ownershipSchema = z14.object({ version: z14.literal(1), files: z14.record(fileSchema) });
|
|
3463
3997
|
}
|
|
3464
3998
|
});
|
|
3465
3999
|
|
|
3466
4000
|
// src/mcp/init.ts
|
|
3467
|
-
import { z as
|
|
4001
|
+
import { z as z15 } from "zod";
|
|
3468
4002
|
var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
|
|
3469
4003
|
var init_init = __esm({
|
|
3470
4004
|
"src/mcp/init.ts"() {
|
|
3471
4005
|
"use strict";
|
|
3472
4006
|
init_storage();
|
|
3473
4007
|
init_files2();
|
|
3474
|
-
markerSchema =
|
|
3475
|
-
version:
|
|
3476
|
-
initializedAt:
|
|
3477
|
-
features:
|
|
4008
|
+
markerSchema = z15.object({
|
|
4009
|
+
version: z15.literal(1),
|
|
4010
|
+
initializedAt: z15.string(),
|
|
4011
|
+
features: z15.object({ confluence: z15.boolean().optional() }).passthrough().optional()
|
|
3478
4012
|
}).passthrough();
|
|
3479
4013
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
3480
4014
|
## Mason project knowledge
|
|
@@ -3484,12 +4018,14 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
3484
4018
|
- Task, bug, change request, or investigation of why the project works this way \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
|
|
3485
4019
|
- Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
|
|
3486
4020
|
- Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` before finishing. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
|
|
4021
|
+
- Before saving, compare the lesson with matching \`get_context\` records and any pending proposal. If new evidence changes the same lesson's assumptions, scope, or recommended action, revise it with \`save_decision(id: existingId)\`, including when it is accepted. Preserve supported rationale and sources, replace obsolete instructions, and add the new evidence. Create a separate record for a genuinely distinct lesson; skip unchanged restatements. Revision preserves history and leaves the accepted version operative until authorized review.
|
|
3487
4022
|
- Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, and failed verification means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
|
|
3488
4023
|
- Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
|
|
3489
4024
|
- 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.
|
|
3490
4025
|
- \`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.
|
|
3491
4026
|
|
|
3492
4027
|
- 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. Suppressed advisories remain unresolved; editing a doc does not approve it.
|
|
4028
|
+
- 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.
|
|
3493
4029
|
|
|
3494
4030
|
- 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.
|
|
3495
4031
|
|
|
@@ -3505,7 +4041,7 @@ ${CLAUDE_MD_SECTION}`;
|
|
|
3505
4041
|
|
|
3506
4042
|
1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
|
|
3507
4043
|
2. Address findings within the user's requested scope. Setup alone authorizes Mason host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
|
|
3508
|
-
3. When the task reveals a real lesson or constraint,
|
|
4044
|
+
3. When the task reveals a real lesson or constraint, compare matching \`get_context\` records first. If its assumptions, scope, or recommended action changed, revise that record using \`save_decision(id: existingId)\`; create a separate record for a genuinely distinct lesson and skip unchanged restatements. Include title, body, category, anchors, and known owner/source/actor information. Preserve supported rationale and sources, replace obsolete instructions, and add new evidence. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
|
|
3509
4045
|
|
|
3510
4046
|
${ASSISTANT_SETUP}
|
|
3511
4047
|
|
|
@@ -3546,29 +4082,6 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
|
|
|
3546
4082
|
}
|
|
3547
4083
|
});
|
|
3548
4084
|
|
|
3549
|
-
// src/utils/git.ts
|
|
3550
|
-
var git_exports = {};
|
|
3551
|
-
__export(git_exports, {
|
|
3552
|
-
isGitRepo: () => isGitRepo
|
|
3553
|
-
});
|
|
3554
|
-
import { execFile as execFile10 } from "child_process";
|
|
3555
|
-
import { promisify as promisify10 } from "util";
|
|
3556
|
-
async function isGitRepo(dir) {
|
|
3557
|
-
try {
|
|
3558
|
-
await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
3559
|
-
return true;
|
|
3560
|
-
} catch {
|
|
3561
|
-
return false;
|
|
3562
|
-
}
|
|
3563
|
-
}
|
|
3564
|
-
var exec8;
|
|
3565
|
-
var init_git2 = __esm({
|
|
3566
|
-
"src/utils/git.ts"() {
|
|
3567
|
-
"use strict";
|
|
3568
|
-
exec8 = promisify10(execFile10);
|
|
3569
|
-
}
|
|
3570
|
-
});
|
|
3571
|
-
|
|
3572
4085
|
// src/setup/config.ts
|
|
3573
4086
|
var config_exports = {};
|
|
3574
4087
|
__export(config_exports, {
|
|
@@ -3581,7 +4094,7 @@ __export(config_exports, {
|
|
|
3581
4094
|
});
|
|
3582
4095
|
import { isDeepStrictEqual } from "util";
|
|
3583
4096
|
import { parse as parse2, stringify } from "smol-toml";
|
|
3584
|
-
import { z as
|
|
4097
|
+
import { z as z16 } from "zod";
|
|
3585
4098
|
async function instructionEdits(root, host) {
|
|
3586
4099
|
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
3587
4100
|
const existing = found.find((f) => f.text !== null);
|
|
@@ -3613,7 +4126,7 @@ async function instructionEdits(root, host) {
|
|
|
3613
4126
|
function managedMcp(previous, host) {
|
|
3614
4127
|
const options = previous === void 0 ? {} : object.parse(previous);
|
|
3615
4128
|
for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
|
|
3616
|
-
return { ...options, ...mcpCommand(host) };
|
|
4129
|
+
return { ...options, ...host === "claude" ? { type: "stdio" } : {}, ...mcpCommand(host) };
|
|
3617
4130
|
}
|
|
3618
4131
|
async function mcpEdit(root, host) {
|
|
3619
4132
|
const file = mcpPath(host), before = await readText(root, file);
|
|
@@ -3650,19 +4163,13 @@ async function hookEdits(root, host) {
|
|
|
3650
4163
|
}
|
|
3651
4164
|
async function ancillaryEdits(root) {
|
|
3652
4165
|
const ignore = await readText(root, ".gitignore");
|
|
3653
|
-
const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
3654
|
-
const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
|
|
3655
|
-
let parentIgnored = false;
|
|
3656
|
-
try {
|
|
3657
|
-
if (await isGitRepo2(root)) parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
|
|
3658
|
-
} catch (error) {
|
|
3659
|
-
if (error.code !== 1) throw error;
|
|
3660
|
-
}
|
|
3661
|
-
const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
|
|
3662
4166
|
const rules = [
|
|
3663
|
-
|
|
4167
|
+
"!/.mason/",
|
|
4168
|
+
"/.mason/*",
|
|
3664
4169
|
"!/.mason/decisions/",
|
|
3665
4170
|
"!/.mason/decisions/**",
|
|
4171
|
+
"!/.mason/reviews/",
|
|
4172
|
+
"!/.mason/reviews/**",
|
|
3666
4173
|
"!/.mason/config.json",
|
|
3667
4174
|
"!/.mason/snapshot.json",
|
|
3668
4175
|
"/.mason/local/",
|
|
@@ -3676,13 +4183,14 @@ async function inspectHostConfig(root, host, plannedMcp) {
|
|
|
3676
4183
|
const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
|
|
3677
4184
|
const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
|
|
3678
4185
|
const expected = hookConfig(host, hookCommand(host));
|
|
4186
|
+
const compatible = compatibleManagedHookCommands(host);
|
|
3679
4187
|
const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
|
|
3680
4188
|
return {
|
|
3681
4189
|
mcp,
|
|
3682
4190
|
mcpDisabled: mcp?.enabled === false,
|
|
3683
4191
|
hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
|
|
3684
4192
|
event,
|
|
3685
|
-
(hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => handler.command
|
|
4193
|
+
(hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => compatible.includes(handler.command ?? "")).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
|
|
3686
4194
|
])),
|
|
3687
4195
|
disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
|
|
3688
4196
|
};
|
|
@@ -3697,7 +4205,7 @@ var init_config = __esm({
|
|
|
3697
4205
|
init_install();
|
|
3698
4206
|
init_files2();
|
|
3699
4207
|
init_launcher();
|
|
3700
|
-
object =
|
|
4208
|
+
object = z16.record(z16.unknown());
|
|
3701
4209
|
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
3702
4210
|
TOML_START = "# mason:mcp:start";
|
|
3703
4211
|
TOML_END = "# mason:mcp:end";
|
|
@@ -3715,11 +4223,11 @@ __export(install_exports, {
|
|
|
3715
4223
|
installAutomation: () => installAutomation,
|
|
3716
4224
|
installedAutomation: () => installedAutomation,
|
|
3717
4225
|
planAutomationInstall: () => planAutomationInstall,
|
|
3718
|
-
recordSchema: () =>
|
|
4226
|
+
recordSchema: () => recordSchema2
|
|
3719
4227
|
});
|
|
3720
|
-
import { z as
|
|
4228
|
+
import { z as z17 } from "zod";
|
|
3721
4229
|
async function installationRecord(root) {
|
|
3722
|
-
return
|
|
4230
|
+
return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
3723
4231
|
}
|
|
3724
4232
|
async function installAutomation(dir, host, command2) {
|
|
3725
4233
|
const ws = await workspace(dir);
|
|
@@ -3788,7 +4296,7 @@ async function installedAutomation(dir) {
|
|
|
3788
4296
|
}
|
|
3789
4297
|
return result;
|
|
3790
4298
|
}
|
|
3791
|
-
var groupSchema, automationConfigSchema,
|
|
4299
|
+
var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath;
|
|
3792
4300
|
var init_install = __esm({
|
|
3793
4301
|
"src/automation/install.ts"() {
|
|
3794
4302
|
"use strict";
|
|
@@ -3798,9 +4306,9 @@ var init_install = __esm({
|
|
|
3798
4306
|
init_store();
|
|
3799
4307
|
init_files2();
|
|
3800
4308
|
init_ownership();
|
|
3801
|
-
groupSchema =
|
|
3802
|
-
automationConfigSchema =
|
|
3803
|
-
|
|
4309
|
+
groupSchema = z17.object({ hooks: z17.array(z17.object({ type: z17.string(), command: z17.string().optional() }).passthrough()) }).passthrough();
|
|
4310
|
+
automationConfigSchema = z17.object({ hooks: z17.record(z17.array(groupSchema)).optional() }).passthrough();
|
|
4311
|
+
recordSchema2 = z17.object({ version: z17.literal(1), hosts: z17.record(z17.object({ command: z17.string() })) });
|
|
3804
4312
|
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
3805
4313
|
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
3806
4314
|
}
|
|
@@ -3814,8 +4322,8 @@ __export(teardown_exports, {
|
|
|
3814
4322
|
});
|
|
3815
4323
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
3816
4324
|
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
3817
|
-
import
|
|
3818
|
-
import { z as
|
|
4325
|
+
import fg6 from "fast-glob";
|
|
4326
|
+
import { z as z18 } from "zod";
|
|
3819
4327
|
async function planTeardown(root, selected) {
|
|
3820
4328
|
const edits = [], diagnostics = [];
|
|
3821
4329
|
const inputs = /* @__PURE__ */ new Map();
|
|
@@ -3835,7 +4343,7 @@ async function planTeardown(root, selected) {
|
|
|
3835
4343
|
const setupBefore = await read(SETUP_PATH);
|
|
3836
4344
|
const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
|
|
3837
4345
|
const automationBefore = await read(AUTOMATION_PATH);
|
|
3838
|
-
const automation = automationBefore === null ? null :
|
|
4346
|
+
const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
|
|
3839
4347
|
const commands = (host) => new Set([
|
|
3840
4348
|
automation?.hosts[host]?.command,
|
|
3841
4349
|
...ownership.files[configPath(host)]?.hookCommands ?? [],
|
|
@@ -3872,8 +4380,12 @@ async function planTeardown(root, selected) {
|
|
|
3872
4380
|
if (host === "codex" && BLOCKS[2].some((marker) => before.includes(marker))) throw new Error("The marked Mason MCP entry was removed or renamed; inspect the retained block manually.");
|
|
3873
4381
|
return;
|
|
3874
4382
|
}
|
|
3875
|
-
const
|
|
3876
|
-
|
|
4383
|
+
const ownedHash = ownership.files[mcpFile]?.mcpHash;
|
|
4384
|
+
const expected = ownedHash ? [ownedHash] : [
|
|
4385
|
+
hash(mcpCommand(host)),
|
|
4386
|
+
...host === "claude" ? [hash({ type: "stdio", ...mcpCommand(host) })] : []
|
|
4387
|
+
];
|
|
4388
|
+
if (!expected.includes(hash(servers.mason))) throw new Error("The Mason MCP entry was edited or its ownership is uncertain; retained for manual cleanup.");
|
|
3877
4389
|
delete servers.mason;
|
|
3878
4390
|
const desired = { ...config, [key]: servers };
|
|
3879
4391
|
if (!Object.keys(servers).length) delete desired[key];
|
|
@@ -3949,7 +4461,7 @@ async function planTeardown(root, selected) {
|
|
|
3949
4461
|
if (diagnostics.length) edits.splice(instructionEditStart);
|
|
3950
4462
|
if (!diagnostics.length) {
|
|
3951
4463
|
const cleanup = [];
|
|
3952
|
-
const receiptFiles = await
|
|
4464
|
+
const receiptFiles = await fg6(
|
|
3953
4465
|
selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
|
|
3954
4466
|
{ cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
|
|
3955
4467
|
);
|
|
@@ -3962,7 +4474,7 @@ async function planTeardown(root, selected) {
|
|
|
3962
4474
|
if (!remainingHost) await attempt(".mason/local/project.json", async () => {
|
|
3963
4475
|
const before = await read(".mason/local/project.json");
|
|
3964
4476
|
if (before === null) return;
|
|
3965
|
-
|
|
4477
|
+
z18.object({ version: z18.literal(1), initializedAt: z18.string() }).strict().parse(JSON.parse(before));
|
|
3966
4478
|
cleanup.push({ path: ".mason/local/project.json", before, after: null });
|
|
3967
4479
|
});
|
|
3968
4480
|
if (!diagnostics.length) {
|
|
@@ -4034,7 +4546,7 @@ var init_teardown = __esm({
|
|
|
4034
4546
|
init_model();
|
|
4035
4547
|
init_ownership();
|
|
4036
4548
|
hosts = ["codex", "claude"];
|
|
4037
|
-
object2 =
|
|
4549
|
+
object2 = z18.record(z18.unknown());
|
|
4038
4550
|
json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
4039
4551
|
}
|
|
4040
4552
|
});
|
|
@@ -4044,9 +4556,9 @@ import { execFile as execFile11 } from "child_process";
|
|
|
4044
4556
|
import { promisify as promisify11 } from "util";
|
|
4045
4557
|
async function buildMatrix(resolvedRoot) {
|
|
4046
4558
|
try {
|
|
4047
|
-
const { stdout: shallow } = await
|
|
4559
|
+
const { stdout: shallow } = await exec8("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
|
|
4048
4560
|
if (shallow.trim() === "true") return null;
|
|
4049
|
-
const { stdout } = await
|
|
4561
|
+
const { stdout } = await exec8(
|
|
4050
4562
|
"git",
|
|
4051
4563
|
["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
|
|
4052
4564
|
{ cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
|
|
@@ -4102,11 +4614,11 @@ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, all
|
|
|
4102
4614
|
findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
|
|
4103
4615
|
return findings;
|
|
4104
4616
|
}
|
|
4105
|
-
var
|
|
4617
|
+
var exec8, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
|
|
4106
4618
|
var init_cochange = __esm({
|
|
4107
4619
|
"src/review/cochange.ts"() {
|
|
4108
4620
|
"use strict";
|
|
4109
|
-
|
|
4621
|
+
exec8 = promisify11(execFile11);
|
|
4110
4622
|
HISTORY_COMMITS = 1500;
|
|
4111
4623
|
MIN_FILE_COMMITS = 5;
|
|
4112
4624
|
MIN_SHARED_COMMITS = 4;
|
|
@@ -4161,7 +4673,7 @@ var init_types2 = __esm({
|
|
|
4161
4673
|
});
|
|
4162
4674
|
|
|
4163
4675
|
// src/review/evidence/vitest.ts
|
|
4164
|
-
import { z as
|
|
4676
|
+
import { z as z19 } from "zod";
|
|
4165
4677
|
function parseVitest(raw, sourceRoot) {
|
|
4166
4678
|
const report = schema.parse(raw);
|
|
4167
4679
|
const findings = [], diagnostics = [];
|
|
@@ -4204,30 +4716,30 @@ function parseVitest(raw, sourceRoot) {
|
|
|
4204
4716
|
diagnostics
|
|
4205
4717
|
};
|
|
4206
4718
|
}
|
|
4207
|
-
var
|
|
4719
|
+
var count3, schema;
|
|
4208
4720
|
var init_vitest = __esm({
|
|
4209
4721
|
"src/review/evidence/vitest.ts"() {
|
|
4210
4722
|
"use strict";
|
|
4211
4723
|
init_paths2();
|
|
4212
4724
|
init_types2();
|
|
4213
|
-
|
|
4214
|
-
schema =
|
|
4215
|
-
success:
|
|
4216
|
-
numTotalTests:
|
|
4217
|
-
numPassedTests:
|
|
4218
|
-
numFailedTests:
|
|
4219
|
-
numPendingTests:
|
|
4220
|
-
numTodoTests:
|
|
4221
|
-
numFailedTestSuites:
|
|
4222
|
-
testResults:
|
|
4223
|
-
name:
|
|
4224
|
-
status:
|
|
4225
|
-
message:
|
|
4226
|
-
assertionResults:
|
|
4227
|
-
fullName:
|
|
4228
|
-
status:
|
|
4229
|
-
failureMessages:
|
|
4230
|
-
location:
|
|
4725
|
+
count3 = z19.number().int().nonnegative();
|
|
4726
|
+
schema = z19.object({
|
|
4727
|
+
success: z19.boolean(),
|
|
4728
|
+
numTotalTests: count3,
|
|
4729
|
+
numPassedTests: count3,
|
|
4730
|
+
numFailedTests: count3,
|
|
4731
|
+
numPendingTests: count3,
|
|
4732
|
+
numTodoTests: count3,
|
|
4733
|
+
numFailedTestSuites: count3,
|
|
4734
|
+
testResults: z19.array(z19.object({
|
|
4735
|
+
name: z19.string().min(1),
|
|
4736
|
+
status: z19.enum(["passed", "failed"]),
|
|
4737
|
+
message: z19.string().optional(),
|
|
4738
|
+
assertionResults: z19.array(z19.object({
|
|
4739
|
+
fullName: z19.string(),
|
|
4740
|
+
status: z19.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
4741
|
+
failureMessages: z19.array(z19.string()).nullable().optional(),
|
|
4742
|
+
location: z19.object({ line: count3, column: count3 }).nullable().optional()
|
|
4231
4743
|
}))
|
|
4232
4744
|
}))
|
|
4233
4745
|
});
|
|
@@ -4235,7 +4747,7 @@ var init_vitest = __esm({
|
|
|
4235
4747
|
});
|
|
4236
4748
|
|
|
4237
4749
|
// src/review/evidence/sarif.ts
|
|
4238
|
-
import { z as
|
|
4750
|
+
import { z as z20 } from "zod";
|
|
4239
4751
|
function parseSarif(raw, sourceRoot) {
|
|
4240
4752
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
4241
4753
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -4326,40 +4838,98 @@ var init_sarif = __esm({
|
|
|
4326
4838
|
"use strict";
|
|
4327
4839
|
init_paths2();
|
|
4328
4840
|
init_types2();
|
|
4329
|
-
index =
|
|
4330
|
-
artifact =
|
|
4331
|
-
message =
|
|
4332
|
-
level =
|
|
4333
|
-
location =
|
|
4334
|
-
schema2 =
|
|
4335
|
-
version:
|
|
4336
|
-
runs:
|
|
4337
|
-
tool:
|
|
4338
|
-
invocations:
|
|
4339
|
-
versionControlProvenance:
|
|
4340
|
-
originalUriBaseIds:
|
|
4341
|
-
artifacts:
|
|
4342
|
-
results:
|
|
4343
|
-
ruleId:
|
|
4841
|
+
index = z20.number().int().nonnegative();
|
|
4842
|
+
artifact = z20.object({ uri: z20.string().optional(), uriBaseId: z20.string().optional(), index: index.optional() });
|
|
4843
|
+
message = z20.object({ text: z20.string().optional(), markdown: z20.string().optional(), id: z20.string().optional(), arguments: z20.array(z20.string()).optional() });
|
|
4844
|
+
level = z20.enum(["error", "warning", "note", "none"]);
|
|
4845
|
+
location = z20.object({ physicalLocation: z20.object({ artifactLocation: artifact.optional(), region: z20.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
4846
|
+
schema2 = z20.object({
|
|
4847
|
+
version: z20.literal("2.1.0"),
|
|
4848
|
+
runs: z20.array(z20.object({
|
|
4849
|
+
tool: z20.object({ driver: z20.object({ name: z20.string().min(1), rules: z20.array(z20.object({ id: z20.string(), defaultConfiguration: z20.object({ level: level.optional() }).optional(), messageStrings: z20.record(message).optional() })).optional(), globalMessageStrings: z20.record(message).optional() }) }),
|
|
4850
|
+
invocations: z20.array(z20.object({ executionSuccessful: z20.boolean(), commandLine: z20.string().optional(), toolExecutionNotifications: z20.array(z20.object({ level: level.optional(), message })).optional() })).optional(),
|
|
4851
|
+
versionControlProvenance: z20.array(z20.object({ revisionId: z20.string().optional() })).optional(),
|
|
4852
|
+
originalUriBaseIds: z20.record(artifact).optional(),
|
|
4853
|
+
artifacts: z20.array(z20.object({ location: artifact.optional() })).optional(),
|
|
4854
|
+
results: z20.array(z20.object({
|
|
4855
|
+
ruleId: z20.string().optional(),
|
|
4344
4856
|
ruleIndex: index.optional(),
|
|
4345
4857
|
message,
|
|
4346
4858
|
level: level.optional(),
|
|
4347
|
-
kind:
|
|
4348
|
-
baselineState:
|
|
4349
|
-
suppressions:
|
|
4350
|
-
locations:
|
|
4351
|
-
relatedLocations:
|
|
4859
|
+
kind: z20.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
4860
|
+
baselineState: z20.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
4861
|
+
suppressions: z20.array(z20.object({ kind: z20.enum(["inSource", "external"]), status: z20.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
4862
|
+
locations: z20.array(location).optional(),
|
|
4863
|
+
relatedLocations: z20.array(location).optional()
|
|
4352
4864
|
})).optional()
|
|
4353
4865
|
}))
|
|
4354
4866
|
});
|
|
4355
4867
|
}
|
|
4356
4868
|
});
|
|
4357
4869
|
|
|
4870
|
+
// src/review/evidence/check.ts
|
|
4871
|
+
import { z as z21 } from "zod";
|
|
4872
|
+
function parseCheck(raw, sourceRoot) {
|
|
4873
|
+
const report = schema3.parse(raw);
|
|
4874
|
+
const diagnostics = [], findings = [];
|
|
4875
|
+
const counts = { total: report.results.length, passed: 0, failed: 0, skipped: 0 };
|
|
4876
|
+
for (const [index2, result] of report.results.entries()) {
|
|
4877
|
+
counts[result.status]++;
|
|
4878
|
+
const locations = result.locations.flatMap((location2) => {
|
|
4879
|
+
const file = evidencePath(location2.file, sourceRoot);
|
|
4880
|
+
if (!file) {
|
|
4881
|
+
diagnostics.push(`Check location is outside the declared checkout or invalid: ${location2.file}`);
|
|
4882
|
+
return [];
|
|
4883
|
+
}
|
|
4884
|
+
return [{ ...location2, file }];
|
|
4885
|
+
});
|
|
4886
|
+
if (result.status === "failed") findings.push({
|
|
4887
|
+
id: String(index2),
|
|
4888
|
+
...messagePreview([result.name, result.message].filter(Boolean).join("\n")),
|
|
4889
|
+
severity: "error",
|
|
4890
|
+
state: "active",
|
|
4891
|
+
locations
|
|
4892
|
+
});
|
|
4893
|
+
}
|
|
4894
|
+
if (!counts.total) diagnostics.push("No check results were recorded; an empty report is not passing evidence.");
|
|
4895
|
+
if (counts.skipped) diagnostics.push(`${counts.skipped} checks were skipped; verification is incomplete.`);
|
|
4896
|
+
return {
|
|
4897
|
+
outcome: counts.failed ? "failed" : !counts.total ? "unavailable" : !counts.passed ? "skipped" : "passed",
|
|
4898
|
+
counts,
|
|
4899
|
+
findings,
|
|
4900
|
+
incomplete: diagnostics.length > 0,
|
|
4901
|
+
diagnostics,
|
|
4902
|
+
...report.commit ? { reportedCommits: [report.commit] } : {}
|
|
4903
|
+
};
|
|
4904
|
+
}
|
|
4905
|
+
var schema3;
|
|
4906
|
+
var init_check = __esm({
|
|
4907
|
+
"src/review/evidence/check.ts"() {
|
|
4908
|
+
"use strict";
|
|
4909
|
+
init_paths2();
|
|
4910
|
+
init_types2();
|
|
4911
|
+
schema3 = z21.object({
|
|
4912
|
+
version: z21.literal(1),
|
|
4913
|
+
commit: z21.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
|
|
4914
|
+
results: z21.array(z21.object({
|
|
4915
|
+
name: z21.string().min(1).max(1e3),
|
|
4916
|
+
status: z21.enum(["passed", "failed", "skipped"]),
|
|
4917
|
+
message: z21.string().max(1e5).optional(),
|
|
4918
|
+
locations: z21.array(z21.object({
|
|
4919
|
+
file: z21.string().min(1).max(4e3),
|
|
4920
|
+
line: z21.number().int().positive().optional(),
|
|
4921
|
+
column: z21.number().int().positive().optional()
|
|
4922
|
+
})).max(100).default([])
|
|
4923
|
+
})).max(1e5)
|
|
4924
|
+
});
|
|
4925
|
+
}
|
|
4926
|
+
});
|
|
4927
|
+
|
|
4358
4928
|
// src/review/evidence.ts
|
|
4359
|
-
import
|
|
4360
|
-
import { z as
|
|
4929
|
+
import path23 from "path";
|
|
4930
|
+
import { z as z22 } from "zod";
|
|
4361
4931
|
function relativeArtifact(root, file) {
|
|
4362
|
-
const relative =
|
|
4932
|
+
const relative = path23.isAbsolute(file) ? path23.relative(root, file) : file;
|
|
4363
4933
|
const normalized = normalizeRepoPath(relative);
|
|
4364
4934
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
4365
4935
|
return normalized;
|
|
@@ -4398,7 +4968,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4398
4968
|
let manifest, raw;
|
|
4399
4969
|
try {
|
|
4400
4970
|
manifest = relativeArtifact(root, supplied);
|
|
4401
|
-
raw =
|
|
4971
|
+
raw = z22.object({ version: z22.literal(1), checks: z22.array(z22.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
4402
4972
|
} catch (error) {
|
|
4403
4973
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
4404
4974
|
continue;
|
|
@@ -4437,7 +5007,8 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4437
5007
|
counts: {},
|
|
4438
5008
|
incomplete: false,
|
|
4439
5009
|
diagnostics: [],
|
|
4440
|
-
truncated: false
|
|
5010
|
+
truncated: false,
|
|
5011
|
+
...check.environment ? { environment: check.environment } : {}
|
|
4441
5012
|
};
|
|
4442
5013
|
output.checks.push(result);
|
|
4443
5014
|
if (check.workingTreeClean !== true) result.diagnostics.push("The check's working tree was dirty or not recorded; its results cannot be attributed to the claimed commit alone.");
|
|
@@ -4450,13 +5021,13 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4450
5021
|
}
|
|
4451
5022
|
try {
|
|
4452
5023
|
if (!check.report) throw new Error("Completed check has no report artifact.");
|
|
4453
|
-
if (check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json; analysis checks require sarif.");
|
|
5024
|
+
if (check.report.format !== "mason-check-json" && check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json or mason-check-json; analysis checks require sarif or mason-check-json.");
|
|
4454
5025
|
const file = relativeArtifact(root, check.report.path);
|
|
4455
5026
|
const report = await readStoreJson(root, file);
|
|
4456
5027
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
4457
5028
|
const sourceRoot = check.sourceRoot ?? root;
|
|
4458
|
-
if (!
|
|
4459
|
-
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
5029
|
+
if (!path23.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
|
|
5030
|
+
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
4460
5031
|
result.outcome = parsed.outcome;
|
|
4461
5032
|
result.counts = parsed.counts;
|
|
4462
5033
|
result.incomplete = parsed.incomplete;
|
|
@@ -4476,7 +5047,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4476
5047
|
result.outcome = "failed";
|
|
4477
5048
|
result.diagnostics.push(`Check command exited ${check.exitCode} despite a report with no active failures.`);
|
|
4478
5049
|
}
|
|
4479
|
-
if (check.
|
|
5050
|
+
if (check.kind === "tests" && pairs === void 0) {
|
|
4480
5051
|
try {
|
|
4481
5052
|
pairs = (await buildTestMap(root)).paired;
|
|
4482
5053
|
} catch (error) {
|
|
@@ -4550,32 +5121,39 @@ var init_evidence2 = __esm({
|
|
|
4550
5121
|
init_provenance();
|
|
4551
5122
|
init_vitest();
|
|
4552
5123
|
init_sarif();
|
|
5124
|
+
init_check();
|
|
4553
5125
|
init_types2();
|
|
4554
|
-
checkSchema2 =
|
|
4555
|
-
id:
|
|
4556
|
-
kind:
|
|
4557
|
-
tool:
|
|
4558
|
-
command:
|
|
4559
|
-
commit:
|
|
4560
|
-
workingTreeClean:
|
|
4561
|
-
source:
|
|
4562
|
-
sourceRoot:
|
|
4563
|
-
status:
|
|
4564
|
-
reason:
|
|
4565
|
-
exitCode:
|
|
4566
|
-
|
|
5126
|
+
checkSchema2 = z22.object({
|
|
5127
|
+
id: z22.string().min(1).max(100),
|
|
5128
|
+
kind: z22.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
|
|
5129
|
+
tool: z22.string().min(1).max(200),
|
|
5130
|
+
command: z22.string().min(1).max(2e3),
|
|
5131
|
+
commit: z22.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
5132
|
+
workingTreeClean: z22.boolean().optional(),
|
|
5133
|
+
source: z22.string().max(2e3).optional(),
|
|
5134
|
+
sourceRoot: z22.string().min(1).max(4e3).optional(),
|
|
5135
|
+
status: z22.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
5136
|
+
reason: z22.string().min(1).max(2e3).optional(),
|
|
5137
|
+
exitCode: z22.number().int().nullable().optional(),
|
|
5138
|
+
environment: z22.object({
|
|
5139
|
+
toolVersion: z22.string().min(1).max(200),
|
|
5140
|
+
runtime: z22.string().min(1).max(200).optional(),
|
|
5141
|
+
platform: z22.string().min(1).max(200).optional(),
|
|
5142
|
+
configuration: z22.array(z22.string().min(1).max(4e3)).max(100).optional()
|
|
5143
|
+
}).optional(),
|
|
5144
|
+
report: z22.object({ format: z22.enum(["vitest-json", "sarif", "mason-check-json"]), path: z22.string().min(1).max(4e3) }).optional()
|
|
4567
5145
|
});
|
|
4568
5146
|
}
|
|
4569
5147
|
});
|
|
4570
5148
|
|
|
4571
5149
|
// src/review/review.ts
|
|
4572
|
-
import
|
|
4573
|
-
import
|
|
5150
|
+
import fs19 from "fs/promises";
|
|
5151
|
+
import path24 from "path";
|
|
4574
5152
|
import { execFile as execFile12 } from "child_process";
|
|
4575
5153
|
import { promisify as promisify12 } from "util";
|
|
4576
5154
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
4577
5155
|
try {
|
|
4578
|
-
const { stdout } = await
|
|
5156
|
+
const { stdout } = await exec9("git", ["merge-base", base, head], {
|
|
4579
5157
|
cwd: resolvedRoot
|
|
4580
5158
|
});
|
|
4581
5159
|
return stdout.trim() || null;
|
|
@@ -4586,7 +5164,7 @@ async function resolveMergeBase(resolvedRoot, base, head) {
|
|
|
4586
5164
|
async function defaultBase(resolvedRoot) {
|
|
4587
5165
|
for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
|
|
4588
5166
|
try {
|
|
4589
|
-
await
|
|
5167
|
+
await exec9("git", ["rev-parse", "--verify", "--quiet", ref], {
|
|
4590
5168
|
cwd: resolvedRoot
|
|
4591
5169
|
});
|
|
4592
5170
|
return ref;
|
|
@@ -4599,7 +5177,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
4599
5177
|
return matchingPaths(record.files, changedFiles);
|
|
4600
5178
|
}
|
|
4601
5179
|
async function computeReview(rootDir, base, options = {}) {
|
|
4602
|
-
const resolvedRoot =
|
|
5180
|
+
const resolvedRoot = path24.resolve(rootDir);
|
|
4603
5181
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
4604
5182
|
if (head === "unknown") return null;
|
|
4605
5183
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -4649,7 +5227,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
4649
5227
|
analyzed,
|
|
4650
5228
|
async (relPath) => {
|
|
4651
5229
|
try {
|
|
4652
|
-
await
|
|
5230
|
+
await fs19.access(path24.join(resolvedRoot, relPath));
|
|
4653
5231
|
return true;
|
|
4654
5232
|
} catch {
|
|
4655
5233
|
return false;
|
|
@@ -4682,7 +5260,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
4682
5260
|
}
|
|
4683
5261
|
return finalize();
|
|
4684
5262
|
}
|
|
4685
|
-
var
|
|
5263
|
+
var exec9, MAX_ANALYZED_FILES;
|
|
4686
5264
|
var init_review = __esm({
|
|
4687
5265
|
"src/review/review.ts"() {
|
|
4688
5266
|
"use strict";
|
|
@@ -4694,7 +5272,7 @@ var init_review = __esm({
|
|
|
4694
5272
|
init_cochange();
|
|
4695
5273
|
init_evidence2();
|
|
4696
5274
|
init_snapshot();
|
|
4697
|
-
|
|
5275
|
+
exec9 = promisify12(execFile12);
|
|
4698
5276
|
MAX_ANALYZED_FILES = 50;
|
|
4699
5277
|
}
|
|
4700
5278
|
});
|
|
@@ -4703,7 +5281,7 @@ var init_review = __esm({
|
|
|
4703
5281
|
async function auditSummary(root) {
|
|
4704
5282
|
try {
|
|
4705
5283
|
const report = await computeAudit(root);
|
|
4706
|
-
if (!report) return { status: "no-context-files", reason: "No
|
|
5284
|
+
if (!report) return { status: "no-context-files", reason: "No README.md or agent instruction files found to audit." };
|
|
4707
5285
|
if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
|
|
4708
5286
|
return {
|
|
4709
5287
|
...report,
|
|
@@ -4816,7 +5394,16 @@ async function setupStatus(dir) {
|
|
|
4816
5394
|
const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
|
|
4817
5395
|
const config = await inspectHostConfig(ws.root, host, void 0);
|
|
4818
5396
|
const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual3({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
|
|
4819
|
-
const
|
|
5397
|
+
const expectedHooks = hookConfig(host, hookCommand(host)).hooks;
|
|
5398
|
+
const currentHooks = isDeepStrictEqual3(config.hooks, expectedHooks);
|
|
5399
|
+
const compatibleHooks = Object.fromEntries(Object.entries(config.hooks).map(([event, groups]) => [
|
|
5400
|
+
event,
|
|
5401
|
+
groups.map((group) => ({ ...group, hooks: group.hooks.map((handler) => ({
|
|
5402
|
+
...handler,
|
|
5403
|
+
command: expectedHooks.SessionStart[0].hooks[0].command
|
|
5404
|
+
})) }))
|
|
5405
|
+
]));
|
|
5406
|
+
const hooks = isDeepStrictEqual3(compatibleHooks, expectedHooks) && !config.disabled;
|
|
4820
5407
|
const local = await loadSetupReceipt(ws.root, ws.directory, host);
|
|
4821
5408
|
const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
|
|
4822
5409
|
const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
|
|
@@ -4835,11 +5422,12 @@ async function setupStatus(dir) {
|
|
|
4835
5422
|
runtime: installed.available ? "global (" + installed.version + ")" : "unavailable-on-path",
|
|
4836
5423
|
mcp: mcp ? "configured" : "changed",
|
|
4837
5424
|
instructions: instructionsCurrent ? "current" : "changed",
|
|
4838
|
-
hookConfiguration: hooks ? "configured" : "disabled-or-changed",
|
|
5425
|
+
hookConfiguration: hooks ? currentHooks ? "configured" : "compatible" : "disabled-or-changed",
|
|
4839
5426
|
observedEvents,
|
|
4840
5427
|
contextCalls: observation?.contextCalls ?? 0,
|
|
4841
5428
|
verificationStatus: automation.status === "current" ? automation.verificationStatus ?? "unavailable" : "unavailable",
|
|
4842
|
-
pending
|
|
5429
|
+
pending,
|
|
5430
|
+
updates: hooks && !currentHooks ? ["A newer hook template is available. Run mason setup --host " + host + " to update it when convenient; review and commit the configuration changes."] : []
|
|
4843
5431
|
};
|
|
4844
5432
|
}
|
|
4845
5433
|
const statuses = Object.values(hosts2).map((host) => host.status);
|
|
@@ -4864,6 +5452,7 @@ function summarizeActivation(status) {
|
|
|
4864
5452
|
` Task context requests: ${state.contextCalls}; verification: ${state.verificationStatus}.`,
|
|
4865
5453
|
...state.pending.map((message2) => " Next: " + message2)
|
|
4866
5454
|
);
|
|
5455
|
+
lines.push(...state.updates.map((message2) => " Optional: " + message2));
|
|
4867
5456
|
}
|
|
4868
5457
|
if ("decisionRecords" in status) lines.push(`Decision records: ${status.decisionRecords}.`);
|
|
4869
5458
|
if (status.next) lines.push(status.next);
|
|
@@ -5012,16 +5601,16 @@ var init_setup = __esm({
|
|
|
5012
5601
|
});
|
|
5013
5602
|
|
|
5014
5603
|
// src/distribution/bundle.ts
|
|
5015
|
-
import
|
|
5016
|
-
import
|
|
5604
|
+
import fs20 from "fs/promises";
|
|
5605
|
+
import path25 from "path";
|
|
5017
5606
|
import { createHash as createHash4 } from "crypto";
|
|
5018
|
-
import { z as
|
|
5607
|
+
import { z as z23 } from "zod";
|
|
5019
5608
|
async function readBundle(root) {
|
|
5020
|
-
const bytes = await
|
|
5609
|
+
const bytes = await fs20.readFile(await storePath(root, "bundle.json"));
|
|
5021
5610
|
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
5022
5611
|
const windows = manifest.target.startsWith("win32-");
|
|
5023
5612
|
for (const file of Object.keys(manifest.files)) {
|
|
5024
|
-
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") ||
|
|
5613
|
+
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path25.isAbsolute(file)) {
|
|
5025
5614
|
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
5026
5615
|
}
|
|
5027
5616
|
}
|
|
@@ -5036,7 +5625,7 @@ async function verifyBundle(root, expectedHash) {
|
|
|
5036
5625
|
if (bundle.manifest.target !== `${process.platform}-${process.arch}`) throw new Error("Mason bundle is for another platform. Install this platform's bundle and rerun setup.");
|
|
5037
5626
|
const files = Object.keys(bundle.manifest.files);
|
|
5038
5627
|
for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
|
|
5039
|
-
if (sha256(await
|
|
5628
|
+
if (sha256(await fs20.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
|
|
5040
5629
|
}));
|
|
5041
5630
|
return bundle;
|
|
5042
5631
|
}
|
|
@@ -5046,31 +5635,31 @@ var init_bundle = __esm({
|
|
|
5046
5635
|
"use strict";
|
|
5047
5636
|
init_storage();
|
|
5048
5637
|
targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
|
5049
|
-
digest2 =
|
|
5050
|
-
bundleSchema =
|
|
5051
|
-
format:
|
|
5052
|
-
version:
|
|
5053
|
-
target:
|
|
5054
|
-
nodeVersion:
|
|
5055
|
-
files:
|
|
5638
|
+
digest2 = z23.string().regex(/^[a-f0-9]{64}$/);
|
|
5639
|
+
bundleSchema = z23.object({
|
|
5640
|
+
format: z23.literal(1),
|
|
5641
|
+
version: z23.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
5642
|
+
target: z23.enum(targets),
|
|
5643
|
+
nodeVersion: z23.string(),
|
|
5644
|
+
files: z23.record(digest2)
|
|
5056
5645
|
}).strict();
|
|
5057
5646
|
sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
5058
5647
|
}
|
|
5059
5648
|
});
|
|
5060
5649
|
|
|
5061
5650
|
// src/distribution/path.ts
|
|
5062
|
-
import
|
|
5651
|
+
import fs21 from "fs/promises";
|
|
5063
5652
|
import os3 from "os";
|
|
5064
|
-
import
|
|
5653
|
+
import path26 from "path";
|
|
5065
5654
|
import { execFile as execFile13 } from "child_process";
|
|
5066
5655
|
import { promisify as promisify13 } from "util";
|
|
5067
5656
|
import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
|
|
5068
|
-
import { z as
|
|
5657
|
+
import { z as z24 } from "zod";
|
|
5069
5658
|
async function configurePath(bin, previous, save, options = {}) {
|
|
5070
5659
|
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
5071
5660
|
const changes = [...previous];
|
|
5072
5661
|
const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
|
|
5073
|
-
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) =>
|
|
5662
|
+
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path26.posix.isAbsolute(entry) && path26.posix.resolve(entry) === path26.posix.resolve(bin));
|
|
5074
5663
|
const message2 = alreadyOnPath ? "Run: mason setup --host codex" : "Open a new terminal, then run: mason setup --host codex";
|
|
5075
5664
|
const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
|
|
5076
5665
|
${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --host codex`}` });
|
|
@@ -5088,33 +5677,33 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
5088
5677
|
}
|
|
5089
5678
|
return { status: "configured", changes, message: message2 };
|
|
5090
5679
|
}
|
|
5091
|
-
const shell =
|
|
5680
|
+
const shell = path26.basename(env.SHELL ?? os3.userInfo().shell ?? "");
|
|
5092
5681
|
const userHome = options.userHome ?? os3.homedir();
|
|
5093
5682
|
let profiles;
|
|
5094
5683
|
if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
|
|
5095
|
-
if (env.MASON_PROFILE) profiles = [
|
|
5096
|
-
else if (shell === "zsh") profiles = [
|
|
5097
|
-
else if (shell === "sh") profiles = [
|
|
5684
|
+
if (env.MASON_PROFILE) profiles = [path26.resolve(env.MASON_PROFILE)];
|
|
5685
|
+
else if (shell === "zsh") profiles = [path26.join(env.ZDOTDIR || userHome, ".zshrc")];
|
|
5686
|
+
else if (shell === "sh") profiles = [path26.join(userHome, ".profile")];
|
|
5098
5687
|
else {
|
|
5099
|
-
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) =>
|
|
5688
|
+
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path26.join(userHome, name));
|
|
5100
5689
|
let login = candidates[2];
|
|
5101
|
-
for (const file of candidates) if (await
|
|
5690
|
+
for (const file of candidates) if (await fs21.lstat(file).then(() => true, (error) => {
|
|
5102
5691
|
if (error.code === "ENOENT") return false;
|
|
5103
5692
|
throw error;
|
|
5104
5693
|
})) {
|
|
5105
5694
|
login = file;
|
|
5106
5695
|
break;
|
|
5107
5696
|
}
|
|
5108
|
-
profiles = [
|
|
5697
|
+
profiles = [path26.join(userHome, ".bashrc"), login];
|
|
5109
5698
|
}
|
|
5110
5699
|
const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
5111
5700
|
for (const profile of profiles) {
|
|
5112
|
-
const stat = await
|
|
5701
|
+
const stat = await fs21.lstat(profile).catch((error) => {
|
|
5113
5702
|
if (error.code === "ENOENT") return null;
|
|
5114
5703
|
throw error;
|
|
5115
5704
|
});
|
|
5116
|
-
const file = stat?.isSymbolicLink() ? await
|
|
5117
|
-
const original = await
|
|
5705
|
+
const file = stat?.isSymbolicLink() ? await fs21.realpath(profile) : path26.resolve(profile);
|
|
5706
|
+
const original = await fs21.readFile(file).catch((error) => {
|
|
5118
5707
|
if (error.code === "ENOENT" && !stat) return null;
|
|
5119
5708
|
throw error;
|
|
5120
5709
|
});
|
|
@@ -5129,8 +5718,8 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
5129
5718
|
changes.push({ kind: "profile", file, block, created: original === null });
|
|
5130
5719
|
await save(changes);
|
|
5131
5720
|
}
|
|
5132
|
-
await
|
|
5133
|
-
const handle = await
|
|
5721
|
+
await fs21.mkdir(path26.dirname(file), { recursive: true });
|
|
5722
|
+
const handle = await fs21.open(file, original === null ? "ax+" : "a+", 384);
|
|
5134
5723
|
try {
|
|
5135
5724
|
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
5136
5725
|
await handle.writeFile(block);
|
|
@@ -5154,13 +5743,13 @@ async function removePath(changes) {
|
|
|
5154
5743
|
await windowsPath("remove", change.entry, current, change.created);
|
|
5155
5744
|
continue;
|
|
5156
5745
|
}
|
|
5157
|
-
const stat = await
|
|
5746
|
+
const stat = await fs21.lstat(change.file).catch((error) => {
|
|
5158
5747
|
if (error.code === "ENOENT") return null;
|
|
5159
5748
|
throw error;
|
|
5160
5749
|
});
|
|
5161
5750
|
if (!stat) continue;
|
|
5162
5751
|
if (!stat.isFile()) throw new Error("profile type changed");
|
|
5163
|
-
const bytes = await
|
|
5752
|
+
const bytes = await fs21.readFile(change.file), text2 = bytes.toString("utf8");
|
|
5164
5753
|
if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
|
|
5165
5754
|
const index2 = text2.indexOf(change.block);
|
|
5166
5755
|
if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
|
|
@@ -5170,7 +5759,7 @@ async function removePath(changes) {
|
|
|
5170
5759
|
const temporary = change.file + ".mason-" + randomUUID7();
|
|
5171
5760
|
try {
|
|
5172
5761
|
if (stat.nlink > 1) throw new Error("profile has multiple hard links");
|
|
5173
|
-
const handle = await
|
|
5762
|
+
const handle = await fs21.open(temporary, "wx", stat.mode & 511);
|
|
5174
5763
|
try {
|
|
5175
5764
|
await handle.writeFile(next);
|
|
5176
5765
|
await handle.chmod(stat.mode & 511);
|
|
@@ -5179,11 +5768,11 @@ async function removePath(changes) {
|
|
|
5179
5768
|
} finally {
|
|
5180
5769
|
await handle.close();
|
|
5181
5770
|
}
|
|
5182
|
-
const current = await
|
|
5183
|
-
if (!current.isFile() || current.ino !== stat.ino || !(await
|
|
5184
|
-
await
|
|
5771
|
+
const current = await fs21.lstat(change.file);
|
|
5772
|
+
if (!current.isFile() || current.ino !== stat.ino || !(await fs21.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
|
|
5773
|
+
await fs21.rename(temporary, change.file);
|
|
5185
5774
|
} finally {
|
|
5186
|
-
await
|
|
5775
|
+
await fs21.rm(temporary, { force: true });
|
|
5187
5776
|
}
|
|
5188
5777
|
} catch (error) {
|
|
5189
5778
|
warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5217,20 +5806,20 @@ try {
|
|
|
5217
5806
|
@{ value = $next; kind = $kind } | ConvertTo-Json -Compress
|
|
5218
5807
|
} finally { $key.Dispose() }
|
|
5219
5808
|
`;
|
|
5220
|
-
const { stdout } = await
|
|
5809
|
+
const { stdout } = await execute3("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024 });
|
|
5221
5810
|
return JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
|
|
5222
5811
|
}
|
|
5223
|
-
var
|
|
5812
|
+
var execute3, pathChangeSchema, quote, normalizeWindowsEntry, hasWindowsEntry;
|
|
5224
5813
|
var init_path = __esm({
|
|
5225
5814
|
"src/distribution/path.ts"() {
|
|
5226
5815
|
"use strict";
|
|
5227
|
-
|
|
5228
|
-
pathChangeSchema =
|
|
5229
|
-
|
|
5230
|
-
|
|
5816
|
+
execute3 = promisify13(execFile13);
|
|
5817
|
+
pathChangeSchema = z24.discriminatedUnion("kind", [
|
|
5818
|
+
z24.object({ kind: z24.literal("profile"), file: z24.string(), block: z24.string(), created: z24.boolean() }),
|
|
5819
|
+
z24.object({ kind: z24.literal("windows"), entry: z24.string(), created: z24.boolean() })
|
|
5231
5820
|
]);
|
|
5232
5821
|
quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
5233
|
-
normalizeWindowsEntry = (value) =>
|
|
5822
|
+
normalizeWindowsEntry = (value) => path26.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
5234
5823
|
hasWindowsEntry = (value, entry, env = process.env) => {
|
|
5235
5824
|
const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
|
|
5236
5825
|
const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
|
|
@@ -5248,41 +5837,41 @@ __export(install_exports2, {
|
|
|
5248
5837
|
uninstallStandalone: () => uninstallStandalone,
|
|
5249
5838
|
upgradeStandalone: () => upgradeStandalone
|
|
5250
5839
|
});
|
|
5251
|
-
import
|
|
5840
|
+
import fs22 from "fs/promises";
|
|
5252
5841
|
import os4 from "os";
|
|
5253
|
-
import
|
|
5842
|
+
import path27 from "path";
|
|
5254
5843
|
import { fileURLToPath } from "url";
|
|
5255
5844
|
import { spawn } from "child_process";
|
|
5256
5845
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
5257
|
-
import { z as
|
|
5846
|
+
import { z as z25 } from "zod";
|
|
5258
5847
|
async function replace(file, content2, mode = 384) {
|
|
5259
5848
|
const temp = file + ".tmp-" + randomUUID8();
|
|
5260
5849
|
try {
|
|
5261
|
-
await
|
|
5262
|
-
await
|
|
5850
|
+
await fs22.writeFile(temp, content2, { mode, flag: "wx" });
|
|
5851
|
+
await fs22.rename(temp, file);
|
|
5263
5852
|
} finally {
|
|
5264
|
-
await
|
|
5853
|
+
await fs22.rm(temp, { force: true });
|
|
5265
5854
|
}
|
|
5266
5855
|
}
|
|
5267
5856
|
async function installStandalone(source) {
|
|
5268
5857
|
const bundle = await verifyBundle(source);
|
|
5269
|
-
const selectedHome =
|
|
5270
|
-
await
|
|
5271
|
-
const home = await
|
|
5272
|
-
const selectedBin =
|
|
5858
|
+
const selectedHome = path27.resolve(defaultHome());
|
|
5859
|
+
await fs22.mkdir(selectedHome, { recursive: true });
|
|
5860
|
+
const home = await fs22.realpath(selectedHome);
|
|
5861
|
+
const selectedBin = path27.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path27.join(home, "bin") : path27.join(os4.homedir(), ".local/bin")));
|
|
5273
5862
|
return withLock(home, ".install-lock", async () => {
|
|
5274
5863
|
const raw = await readStoreJson(home, "install.json");
|
|
5275
|
-
const previous = raw === null ? null :
|
|
5276
|
-
if (!previous) for (const file of await
|
|
5864
|
+
const previous = raw === null ? null : recordSchema3.parse(raw);
|
|
5865
|
+
if (!previous) for (const file of await fs22.readdir(home)) {
|
|
5277
5866
|
if (file === ".install-lock") continue;
|
|
5278
|
-
const directory =
|
|
5279
|
-
const stat = await
|
|
5280
|
-
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await
|
|
5867
|
+
const directory = path27.join(home, file);
|
|
5868
|
+
const stat = await fs22.lstat(directory);
|
|
5869
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs22.readdir(directory)).length) {
|
|
5281
5870
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
5282
5871
|
}
|
|
5283
5872
|
}
|
|
5284
|
-
await
|
|
5285
|
-
const bin = await
|
|
5873
|
+
await fs22.mkdir(selectedBin, { recursive: true });
|
|
5874
|
+
const bin = await fs22.realpath(selectedBin);
|
|
5286
5875
|
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
5287
5876
|
const id = bundle.manifestHash.slice(0, 24);
|
|
5288
5877
|
const destination = await storePath(home, "versions/" + id);
|
|
@@ -5293,10 +5882,10 @@ $previousToken = $env:MASON_UNINSTALL_TOKEN
|
|
|
5293
5882
|
$token = [Guid]::NewGuid().ToString()
|
|
5294
5883
|
$env:MASON_UNINSTALL_TOKEN = $token
|
|
5295
5884
|
try {
|
|
5296
|
-
& ${psQuote(
|
|
5885
|
+
& ${psQuote(path27.join(destination, "node.exe"))} ${psQuote(path27.join(destination, "app/dist/mason.js"))} @args
|
|
5297
5886
|
$code = $LASTEXITCODE
|
|
5298
5887
|
if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
|
|
5299
|
-
if ([IO.File]::ReadAllText(${psQuote(
|
|
5888
|
+
if ([IO.File]::ReadAllText(${psQuote(path27.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
|
|
5300
5889
|
Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
|
|
5301
5890
|
Write-Output ${psQuote("Removed standalone installation: " + home)}
|
|
5302
5891
|
}
|
|
@@ -5305,11 +5894,11 @@ try {
|
|
|
5305
5894
|
finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
|
|
5306
5895
|
`
|
|
5307
5896
|
} : { mason: `#!/bin/sh
|
|
5308
|
-
exec ${shQuote(
|
|
5897
|
+
exec ${shQuote(path27.join(destination, "node"))} ${shQuote(path27.join(destination, "app/dist/mason.js"))} "$@"
|
|
5309
5898
|
` };
|
|
5310
5899
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
5311
5900
|
const file = await storePath(bin, name);
|
|
5312
|
-
const old = await
|
|
5901
|
+
const old = await fs22.readFile(file, "utf8").catch((error) => {
|
|
5313
5902
|
if (error.code === "ENOENT") return null;
|
|
5314
5903
|
throw error;
|
|
5315
5904
|
});
|
|
@@ -5318,7 +5907,7 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5318
5907
|
const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
|
|
5319
5908
|
const checkRetired = async (name, expected) => {
|
|
5320
5909
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
5321
|
-
const actual = await
|
|
5910
|
+
const actual = await fs22.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
5322
5911
|
if (error.code === "ENOENT") return null;
|
|
5323
5912
|
throw error;
|
|
5324
5913
|
});
|
|
@@ -5328,28 +5917,28 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5328
5917
|
const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
|
|
5329
5918
|
await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
|
|
5330
5919
|
const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
|
|
5331
|
-
await
|
|
5920
|
+
await fs22.mkdir(stage);
|
|
5332
5921
|
try {
|
|
5333
5922
|
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
5334
5923
|
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
5335
|
-
await
|
|
5336
|
-
await
|
|
5924
|
+
await fs22.copyFile(from, to);
|
|
5925
|
+
await fs22.chmod(to, (await fs22.stat(from)).mode & 511);
|
|
5337
5926
|
}
|
|
5338
5927
|
await verifyBundle(stage, bundle.manifestHash);
|
|
5339
|
-
const
|
|
5928
|
+
const exists = await fs22.lstat(destination).then(() => true, (error) => {
|
|
5340
5929
|
if (error.code === "ENOENT") return false;
|
|
5341
5930
|
throw error;
|
|
5342
5931
|
});
|
|
5343
|
-
if (
|
|
5344
|
-
else await
|
|
5932
|
+
if (exists) await verifyBundle(destination, bundle.manifestHash);
|
|
5933
|
+
else await fs22.rename(stage, destination);
|
|
5345
5934
|
for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
5346
5935
|
for (const [name, expected] of retired) {
|
|
5347
5936
|
await checkRetired(name, expected);
|
|
5348
|
-
await
|
|
5937
|
+
await fs22.rm(await storePath(bin, name), { force: true });
|
|
5349
5938
|
}
|
|
5350
5939
|
await writeStoreJson(home, "install.json", receipt);
|
|
5351
5940
|
} finally {
|
|
5352
|
-
await
|
|
5941
|
+
await fs22.rm(stage, { recursive: true, force: true });
|
|
5353
5942
|
}
|
|
5354
5943
|
const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
|
|
5355
5944
|
receipt.pathChanges = changes;
|
|
@@ -5359,12 +5948,12 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5359
5948
|
});
|
|
5360
5949
|
}
|
|
5361
5950
|
async function currentInstallation() {
|
|
5362
|
-
const app =
|
|
5363
|
-
const bundle =
|
|
5951
|
+
const app = path27.dirname(path27.dirname(fileURLToPath(import.meta.url)));
|
|
5952
|
+
const bundle = path27.dirname(app), home = path27.dirname(path27.dirname(bundle));
|
|
5364
5953
|
const raw = await readStoreJson(home, "install.json");
|
|
5365
5954
|
if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
5366
|
-
const record =
|
|
5367
|
-
if (record.home !== await
|
|
5955
|
+
const record = recordSchema3.parse(raw);
|
|
5956
|
+
if (record.home !== await fs22.realpath(home) || path27.dirname(bundle) !== path27.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
|
|
5368
5957
|
return { home, bundle, record };
|
|
5369
5958
|
}
|
|
5370
5959
|
async function upgradeStandalone(version) {
|
|
@@ -5372,7 +5961,7 @@ async function upgradeStandalone(version) {
|
|
|
5372
5961
|
const { home, bundle, record } = await currentInstallation();
|
|
5373
5962
|
await verifyBundle(bundle);
|
|
5374
5963
|
const windows = process.platform === "win32";
|
|
5375
|
-
const script =
|
|
5964
|
+
const script = path27.join(bundle, windows ? "install.ps1" : "install.sh");
|
|
5376
5965
|
return new Promise((resolve, reject) => {
|
|
5377
5966
|
const child = spawn(
|
|
5378
5967
|
windows ? "powershell.exe" : "sh",
|
|
@@ -5393,7 +5982,7 @@ async function uninstallStandalone() {
|
|
|
5393
5982
|
for (const [name, expected] of Object.entries(record.launchers)) {
|
|
5394
5983
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
5395
5984
|
const file = await storePath(record.bin, name);
|
|
5396
|
-
const actual = await
|
|
5985
|
+
const actual = await fs22.readFile(file, "utf8").catch((error) => {
|
|
5397
5986
|
if (error.code === "ENOENT") return null;
|
|
5398
5987
|
throw error;
|
|
5399
5988
|
});
|
|
@@ -5401,12 +5990,12 @@ async function uninstallStandalone() {
|
|
|
5401
5990
|
}
|
|
5402
5991
|
for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
|
|
5403
5992
|
if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
|
|
5404
|
-
for (const name of Object.keys(record.launchers)) await
|
|
5993
|
+
for (const name of Object.keys(record.launchers)) await fs22.rm(await storePath(record.bin, name), { force: true });
|
|
5405
5994
|
});
|
|
5406
|
-
if (process.platform !== "win32") await
|
|
5995
|
+
if (process.platform !== "win32") await fs22.rm(home, { recursive: true, force: true });
|
|
5407
5996
|
return home;
|
|
5408
5997
|
}
|
|
5409
|
-
var
|
|
5998
|
+
var recordSchema3, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
|
|
5410
5999
|
var init_install2 = __esm({
|
|
5411
6000
|
"src/distribution/install.ts"() {
|
|
5412
6001
|
"use strict";
|
|
@@ -5414,21 +6003,21 @@ var init_install2 = __esm({
|
|
|
5414
6003
|
init_storage();
|
|
5415
6004
|
init_store();
|
|
5416
6005
|
init_path();
|
|
5417
|
-
|
|
5418
|
-
format:
|
|
5419
|
-
home:
|
|
5420
|
-
bin:
|
|
5421
|
-
current:
|
|
5422
|
-
version:
|
|
5423
|
-
launchers:
|
|
5424
|
-
previousLaunchers:
|
|
5425
|
-
pathChanges:
|
|
6006
|
+
recordSchema3 = z25.object({
|
|
6007
|
+
format: z25.literal(1),
|
|
6008
|
+
home: z25.string(),
|
|
6009
|
+
bin: z25.string(),
|
|
6010
|
+
current: z25.string().regex(/^[a-f0-9]{24}$/),
|
|
6011
|
+
version: z25.string(),
|
|
6012
|
+
launchers: z25.record(z25.string()),
|
|
6013
|
+
previousLaunchers: z25.record(z25.string()).optional(),
|
|
6014
|
+
pathChanges: z25.array(pathChangeSchema).optional()
|
|
5426
6015
|
});
|
|
5427
6016
|
shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
5428
6017
|
psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
5429
6018
|
WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason-launcher.ps1" %*\r\n';
|
|
5430
6019
|
launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
|
|
5431
|
-
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ?
|
|
6020
|
+
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path27.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path27.join(os4.homedir(), ".local/share/mason"));
|
|
5432
6021
|
}
|
|
5433
6022
|
});
|
|
5434
6023
|
|
|
@@ -5493,7 +6082,7 @@ function isHookCommand(argv) {
|
|
|
5493
6082
|
async function runAutomationCli(argv, stdin = "", io = {
|
|
5494
6083
|
out: (s) => process.stdout.write(s + "\n"),
|
|
5495
6084
|
err: (s) => process.stderr.write(s + "\n")
|
|
5496
|
-
}) {
|
|
6085
|
+
}, options = {}) {
|
|
5497
6086
|
let action = "";
|
|
5498
6087
|
try {
|
|
5499
6088
|
const { values, positionals } = parseCli(argv);
|
|
@@ -5513,7 +6102,9 @@ async function runAutomationCli(argv, stdin = "", io = {
|
|
|
5513
6102
|
return result.status === "complete" ? 0 : 2;
|
|
5514
6103
|
}
|
|
5515
6104
|
if (action === "hook") {
|
|
5516
|
-
const
|
|
6105
|
+
const host = hostSchema.parse(values.host);
|
|
6106
|
+
if (options.managedHost && options.managedHost !== host) throw new Error("Managed hook --host does not match its launcher.");
|
|
6107
|
+
const output = await runAutomationHook(host, stdin, { managed: !!options.managedHost });
|
|
5517
6108
|
if (output) io.out(JSON.stringify(output));
|
|
5518
6109
|
return 0;
|
|
5519
6110
|
}
|
|
@@ -5558,7 +6149,7 @@ var inputArgs = process.argv.slice(2);
|
|
|
5558
6149
|
var managedLaunch = inputArgs[0] === "--setup-host";
|
|
5559
6150
|
var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
|
|
5560
6151
|
var [command, ...args] = inputArgs;
|
|
5561
|
-
var usage = `Mason ${"0.
|
|
6152
|
+
var usage = `Mason ${"0.17.1"}
|
|
5562
6153
|
|
|
5563
6154
|
Usage: mason <command> [options]
|
|
5564
6155
|
|
|
@@ -5578,13 +6169,14 @@ mason-audit, mason-review, mason-drift, mason-hook and mason-mcp.`;
|
|
|
5578
6169
|
try {
|
|
5579
6170
|
if (managedLaunch) {
|
|
5580
6171
|
if (!setupHost || !["codex", "claude"].includes(setupHost) || !["mcp", "auto"].includes(command)) throw new Error("Invalid setup invocation.");
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
6172
|
+
if (!(command === "auto" && isHookCommand(args))) {
|
|
6173
|
+
const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
|
|
6174
|
+
await prepareLaunch2(setupHost, { allowInactive: command === "mcp" });
|
|
6175
|
+
}
|
|
5584
6176
|
}
|
|
5585
|
-
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.
|
|
6177
|
+
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.17.1" }));
|
|
5586
6178
|
else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
|
|
5587
|
-
else if (["--version", "-v"].includes(command)) console.log("0.
|
|
6179
|
+
else if (["--version", "-v"].includes(command)) console.log("0.17.1");
|
|
5588
6180
|
else if (command === "internal-install") {
|
|
5589
6181
|
if (args.length) throw new Error("internal-install takes no arguments.");
|
|
5590
6182
|
const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|
|
@@ -5607,7 +6199,12 @@ ${installed.path.message}`);
|
|
|
5607
6199
|
input += chunk.toString();
|
|
5608
6200
|
if (Buffer.byteLength(input) > 1024 * 1024) break;
|
|
5609
6201
|
}
|
|
5610
|
-
process.exitCode = await runAutomationCli(
|
|
6202
|
+
process.exitCode = await runAutomationCli(
|
|
6203
|
+
forwarded,
|
|
6204
|
+
input,
|
|
6205
|
+
void 0,
|
|
6206
|
+
{ managedHost: managedLaunch ? setupHost : void 0 }
|
|
6207
|
+
);
|
|
5611
6208
|
} else if (["audit", "review", "drift", "hook", "mcp"].includes(command)) {
|
|
5612
6209
|
const binary = new URL(`./mason-${command}.js`, import.meta.url);
|
|
5613
6210
|
process.argv = [process.execPath, fileURLToPath2(binary), ...args];
|