mason-context 0.16.3 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/dist/mason-audit.js +1718 -770
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +1359 -813
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +3279 -2616
- 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 +1422 -876
- 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 (error.code === "ENOENT") 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);
|
|
747
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;
|
|
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,50 @@ 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
|
+
const absolute = await auditInputPath(root, file);
|
|
1098
|
+
try {
|
|
1099
|
+
await fs6.access(absolute);
|
|
1100
|
+
return true;
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
if (error.code === "ENOENT") return false;
|
|
1103
|
+
throw error;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function optionalMasonPath(file) {
|
|
1107
|
+
return file.split("/").includes(".mason");
|
|
1108
|
+
}
|
|
1109
|
+
var init_scope = __esm({
|
|
1110
|
+
"src/audit/scope.ts"() {
|
|
1111
|
+
"use strict";
|
|
1112
|
+
init_inputs();
|
|
1113
|
+
init_docs();
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
775
1117
|
// src/test-map.ts
|
|
776
|
-
import
|
|
1118
|
+
import path8 from "path";
|
|
777
1119
|
async function buildTestMap(dir) {
|
|
778
|
-
const rootDir =
|
|
1120
|
+
const rootDir = path8.resolve(dir);
|
|
779
1121
|
const access = await createFileAccess(rootDir);
|
|
780
1122
|
const testPatterns = [
|
|
781
1123
|
"**/*.test.*",
|
|
@@ -796,7 +1138,7 @@ async function buildTestMap(dir) {
|
|
|
796
1138
|
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
797
1139
|
for (const file of sourceFiles) {
|
|
798
1140
|
if (testFiles.includes(file)) continue;
|
|
799
|
-
const baseName =
|
|
1141
|
+
const baseName = path8.basename(file).replace(/\.[^.]+$/, "");
|
|
800
1142
|
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
801
1143
|
existing.push(file);
|
|
802
1144
|
sourceByBaseName.set(baseName, existing);
|
|
@@ -804,7 +1146,7 @@ async function buildTestMap(dir) {
|
|
|
804
1146
|
const paired = [];
|
|
805
1147
|
const unmatched = [];
|
|
806
1148
|
for (const testFile of testFiles) {
|
|
807
|
-
const testBaseName =
|
|
1149
|
+
const testBaseName = path8.basename(testFile).replace(/\.[^.]+$/, "");
|
|
808
1150
|
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
809
1151
|
if (!sourceName) {
|
|
810
1152
|
unmatched.push(testFile);
|
|
@@ -812,10 +1154,10 @@ async function buildTestMap(dir) {
|
|
|
812
1154
|
}
|
|
813
1155
|
const candidates = sourceByBaseName.get(sourceName);
|
|
814
1156
|
if (candidates && candidates.length > 0) {
|
|
815
|
-
const testDir =
|
|
1157
|
+
const testDir = path8.dirname(testFile);
|
|
816
1158
|
const bestMatch = candidates.reduce((best, candidate) => {
|
|
817
|
-
const candidateDir =
|
|
818
|
-
const bestDir =
|
|
1159
|
+
const candidateDir = path8.dirname(candidate);
|
|
1160
|
+
const bestDir = path8.dirname(best);
|
|
819
1161
|
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
820
1162
|
const bestOverlap = commonSegments(testDir, bestDir);
|
|
821
1163
|
return candidateOverlap > bestOverlap ? candidate : best;
|
|
@@ -834,12 +1176,12 @@ async function buildTestMap(dir) {
|
|
|
834
1176
|
function commonSegments(pathA, pathB) {
|
|
835
1177
|
const segsA = pathA.split("/");
|
|
836
1178
|
const segsB = pathB.split("/");
|
|
837
|
-
let
|
|
1179
|
+
let count4 = 0;
|
|
838
1180
|
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
839
|
-
if (segsA[i] === segsB[i])
|
|
1181
|
+
if (segsA[i] === segsB[i]) count4++;
|
|
840
1182
|
else break;
|
|
841
1183
|
}
|
|
842
|
-
return
|
|
1184
|
+
return count4;
|
|
843
1185
|
}
|
|
844
1186
|
var init_test_map = __esm({
|
|
845
1187
|
"src/test-map.ts"() {
|
|
@@ -849,9 +1191,9 @@ var init_test_map = __esm({
|
|
|
849
1191
|
});
|
|
850
1192
|
|
|
851
1193
|
// src/snapshot/snapshot.ts
|
|
852
|
-
import
|
|
853
|
-
import { execFile as
|
|
854
|
-
import { promisify as
|
|
1194
|
+
import path9 from "path";
|
|
1195
|
+
import { execFile as execFile5 } from "child_process";
|
|
1196
|
+
import { promisify as promisify5 } from "util";
|
|
855
1197
|
import { z as z2 } from "zod";
|
|
856
1198
|
async function inspectSnapshot(rootDir) {
|
|
857
1199
|
try {
|
|
@@ -884,7 +1226,7 @@ var init_snapshot = __esm({
|
|
|
884
1226
|
init_storage();
|
|
885
1227
|
init_paths();
|
|
886
1228
|
init_test_map();
|
|
887
|
-
exec4 =
|
|
1229
|
+
exec4 = promisify5(execFile5);
|
|
888
1230
|
repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
889
1231
|
verificationFields = {
|
|
890
1232
|
refreshedHash: z2.string().optional(),
|
|
@@ -913,10 +1255,10 @@ var init_snapshot = __esm({
|
|
|
913
1255
|
});
|
|
914
1256
|
|
|
915
1257
|
// src/drift/drift.ts
|
|
916
|
-
import
|
|
917
|
-
import
|
|
918
|
-
import { execFile as
|
|
919
|
-
import { promisify as
|
|
1258
|
+
import fs7 from "fs/promises";
|
|
1259
|
+
import path10 from "path";
|
|
1260
|
+
import { execFile as execFile6 } from "child_process";
|
|
1261
|
+
import { promisify as promisify6 } from "util";
|
|
920
1262
|
function parseChanges(output) {
|
|
921
1263
|
const fields = output.split("\0");
|
|
922
1264
|
const changes = [];
|
|
@@ -961,7 +1303,7 @@ var init_drift = __esm({
|
|
|
961
1303
|
"use strict";
|
|
962
1304
|
init_snapshot();
|
|
963
1305
|
init_paths();
|
|
964
|
-
exec5 =
|
|
1306
|
+
exec5 = promisify6(execFile6);
|
|
965
1307
|
}
|
|
966
1308
|
});
|
|
967
1309
|
|
|
@@ -982,230 +1324,68 @@ var init_types = __esm({
|
|
|
982
1324
|
});
|
|
983
1325
|
|
|
984
1326
|
// src/audit/checks/deleted-reference.ts
|
|
985
|
-
import
|
|
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
|
-
}
|
|
1327
|
+
import path11 from "path";
|
|
995
1328
|
async function checkDeletedReferences(ctx) {
|
|
996
1329
|
const result = emptyResult();
|
|
997
1330
|
for (const doc of ctx.docs) {
|
|
998
|
-
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
999
|
-
const renames =
|
|
1000
|
-
for (const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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.`);
|
|
1331
|
+
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
1332
|
+
const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
|
|
1333
|
+
for (const claim of doc.claims.paths) {
|
|
1334
|
+
const scope = pathClaimScope(doc.path, claim);
|
|
1335
|
+
if (!scope) {
|
|
1336
|
+
result.skipped.push({
|
|
1337
|
+
check: "deleted-reference",
|
|
1338
|
+
doc: doc.path,
|
|
1339
|
+
reason: `Reference ${claim.excerpt} is outside the repository or has an unsupported path.`
|
|
1340
|
+
});
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (scope.candidates.some(optionalMasonPath)) continue;
|
|
1344
|
+
if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(Boolean)) continue;
|
|
1345
|
+
const candidates = await Promise.all(scope.candidates.map(async (file) => ({
|
|
1346
|
+
file,
|
|
1347
|
+
tracked: await lastCommitOf(ctx.root, file),
|
|
1348
|
+
parent: await pathExists(ctx.root, path11.posix.dirname(file))
|
|
1349
|
+
})));
|
|
1350
|
+
const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
|
|
1351
|
+
if (!candidate && claim.relativeTo !== "document") continue;
|
|
1352
|
+
const resolvedPath = candidate?.file ?? scope.candidates[0];
|
|
1353
|
+
const renamedTo = renames.get(resolvedPath) ?? null;
|
|
1354
|
+
const deletedInCommit = candidate?.tracked && !renamedTo ? await deletingCommitOf(ctx.root, resolvedPath) : null;
|
|
1355
|
+
const evidence = {
|
|
1356
|
+
kind: "missing-path",
|
|
1357
|
+
claimed: claim.path,
|
|
1358
|
+
resolvedPath,
|
|
1359
|
+
scope,
|
|
1360
|
+
renamedTo,
|
|
1361
|
+
deletedInCommit,
|
|
1362
|
+
everTracked: !!candidate?.tracked || !!renamedTo,
|
|
1363
|
+
parentDirExists: candidate?.parent ?? false
|
|
1364
|
+
};
|
|
1365
|
+
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
1366
|
+
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)})` : "");
|
|
1367
|
+
if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
|
|
1368
|
+
result.issues.push({ type: "deleted-reference", message: message2, anchor, confidence: "certain", evidence });
|
|
1369
|
+
} else {
|
|
1370
|
+
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 });
|
|
1154
1371
|
}
|
|
1155
1372
|
}
|
|
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();
|
|
1176
|
-
}
|
|
1177
|
-
async function gitIgnoredPaths(root, files) {
|
|
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");
|
|
1187
|
-
try {
|
|
1188
|
-
return new Set((await operation).stdout.split("\0").filter(Boolean));
|
|
1189
|
-
} catch (error) {
|
|
1190
|
-
if (error.code === 1) return /* @__PURE__ */ new Set();
|
|
1191
|
-
throw error;
|
|
1192
1373
|
}
|
|
1374
|
+
return result;
|
|
1193
1375
|
}
|
|
1194
|
-
var
|
|
1195
|
-
|
|
1196
|
-
"src/audit/inputs.ts"() {
|
|
1376
|
+
var init_deleted_reference = __esm({
|
|
1377
|
+
"src/audit/checks/deleted-reference.ts"() {
|
|
1197
1378
|
"use strict";
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
MAX_INPUTS = 1e5;
|
|
1379
|
+
init_git();
|
|
1380
|
+
init_scope();
|
|
1381
|
+
init_checks();
|
|
1202
1382
|
}
|
|
1203
1383
|
});
|
|
1204
1384
|
|
|
1205
1385
|
// src/audit/checks/new-module.ts
|
|
1206
|
-
import
|
|
1386
|
+
import fg4 from "fast-glob";
|
|
1207
1387
|
import fs8 from "fs/promises";
|
|
1208
|
-
import
|
|
1388
|
+
import path12 from "path";
|
|
1209
1389
|
function escapeRegExp(text2) {
|
|
1210
1390
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1211
1391
|
}
|
|
@@ -1219,15 +1399,15 @@ function isMentioned(combinedDocs, name) {
|
|
|
1219
1399
|
async function checkNewModules(ctx) {
|
|
1220
1400
|
const result = emptyResult();
|
|
1221
1401
|
if (ctx.docs.length === 0) return result;
|
|
1222
|
-
const combinedDocs = ctx.docs
|
|
1402
|
+
const combinedDocs = moduleDocumentation(ctx.docs);
|
|
1223
1403
|
const primaryDoc = ctx.docs[0].path;
|
|
1224
1404
|
const checkedDocs = ctx.docs.map((d) => d.path);
|
|
1225
1405
|
const flag = async (dir, sourceFileCount) => {
|
|
1226
|
-
result.
|
|
1406
|
+
result.advisories.push({
|
|
1407
|
+
resolution: "recheck",
|
|
1227
1408
|
type: "new-module",
|
|
1228
|
-
message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in
|
|
1409
|
+
message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in the discovered documentation; review whether it needs documenting`,
|
|
1229
1410
|
anchor: { doc: primaryDoc, line: null, excerpt: dir },
|
|
1230
|
-
confidence: "likely",
|
|
1231
1411
|
evidence: {
|
|
1232
1412
|
kind: "unmentioned-dir",
|
|
1233
1413
|
dir,
|
|
@@ -1242,41 +1422,44 @@ async function checkNewModules(ctx) {
|
|
|
1242
1422
|
}
|
|
1243
1423
|
return result;
|
|
1244
1424
|
}
|
|
1425
|
+
function moduleDocumentation(docs) {
|
|
1426
|
+
return docs.map((doc) => doc.content + "\n" + (path12.posix.dirname(doc.path) === "." ? "" : path12.posix.dirname(doc.path) + "/")).join("\n");
|
|
1427
|
+
}
|
|
1245
1428
|
async function moduleCandidates(root, combinedDocs) {
|
|
1246
1429
|
const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
|
|
1247
1430
|
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
1248
1431
|
const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
|
|
1249
1432
|
const listSubdirs = async (dir) => {
|
|
1250
1433
|
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) =>
|
|
1434
|
+
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
1435
|
const ignored = await gitIgnoredPaths(root, dirs);
|
|
1253
1436
|
const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
|
|
1254
|
-
return (await auditGlob(root, dir === "." ? "*" : `${
|
|
1437
|
+
return (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
|
|
1255
1438
|
ignore,
|
|
1256
1439
|
onlyDirectories: true,
|
|
1257
1440
|
label: "Module directory discovery",
|
|
1258
1441
|
select: (file) => visible.has(file)
|
|
1259
|
-
})).map((file) =>
|
|
1442
|
+
})).map((file) => path12.posix.basename(file));
|
|
1260
1443
|
};
|
|
1261
1444
|
const countSourceFiles = async (dir) => {
|
|
1262
1445
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1263
|
-
let
|
|
1446
|
+
let count4 = 0;
|
|
1264
1447
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1265
|
-
|
|
1448
|
+
count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
|
|
1266
1449
|
ignore,
|
|
1267
1450
|
followSymbolicLinks: false,
|
|
1268
1451
|
label: `Source discovery in ${dir}`
|
|
1269
1452
|
})).length;
|
|
1270
|
-
if (
|
|
1453
|
+
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
1454
|
}
|
|
1272
|
-
return
|
|
1455
|
+
return count4;
|
|
1273
1456
|
};
|
|
1274
1457
|
const candidates = [];
|
|
1275
1458
|
for (const topDir of await listSubdirs(".")) {
|
|
1276
1459
|
const topMentioned = isMentioned(combinedDocs, topDir);
|
|
1277
1460
|
if (!topMentioned) {
|
|
1278
|
-
const
|
|
1279
|
-
if (
|
|
1461
|
+
const count4 = await countSourceFiles(topDir);
|
|
1462
|
+
if (count4 >= 1) candidates.push({ dir: topDir, sourceFileCount: count4 });
|
|
1280
1463
|
continue;
|
|
1281
1464
|
}
|
|
1282
1465
|
const subdirs = await listSubdirs(topDir);
|
|
@@ -1284,9 +1467,9 @@ async function moduleCandidates(root, combinedDocs) {
|
|
|
1284
1467
|
if (mentioned.length < ENUMERATION_THRESHOLD) continue;
|
|
1285
1468
|
for (const sub of subdirs) {
|
|
1286
1469
|
if (isMentioned(combinedDocs, sub)) continue;
|
|
1287
|
-
const
|
|
1288
|
-
if (
|
|
1289
|
-
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount:
|
|
1470
|
+
const count4 = await countSourceFiles(`${topDir}/${sub}`);
|
|
1471
|
+
if (count4 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
|
|
1472
|
+
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count4 });
|
|
1290
1473
|
}
|
|
1291
1474
|
}
|
|
1292
1475
|
}
|
|
@@ -1325,7 +1508,7 @@ var init_new_module = __esm({
|
|
|
1325
1508
|
});
|
|
1326
1509
|
|
|
1327
1510
|
// src/audit/checks/stale-count.ts
|
|
1328
|
-
import
|
|
1511
|
+
import path13 from "path";
|
|
1329
1512
|
async function countGradleModules(root) {
|
|
1330
1513
|
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
1331
1514
|
const content2 = await readAuditInput(root, name);
|
|
@@ -1359,7 +1542,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1359
1542
|
return {
|
|
1360
1543
|
actual: matched.length,
|
|
1361
1544
|
countedFrom: "package.json workspaces",
|
|
1362
|
-
members: matched.map((m) =>
|
|
1545
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1363
1546
|
};
|
|
1364
1547
|
}
|
|
1365
1548
|
}
|
|
@@ -1390,7 +1573,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1390
1573
|
return {
|
|
1391
1574
|
actual: matched.length,
|
|
1392
1575
|
countedFrom: "pnpm-workspace.yaml",
|
|
1393
|
-
members: matched.map((m) =>
|
|
1576
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1394
1577
|
};
|
|
1395
1578
|
}
|
|
1396
1579
|
}
|
|
@@ -1412,8 +1595,8 @@ async function countCargoCrates(root) {
|
|
|
1412
1595
|
ignore: ["**/target/**"],
|
|
1413
1596
|
label: "Cargo workspace discovery"
|
|
1414
1597
|
});
|
|
1415
|
-
for (const m of matched) members.add(
|
|
1416
|
-
} else if (await readAuditInput(root,
|
|
1598
|
+
for (const m of matched) members.add(path13.dirname(m));
|
|
1599
|
+
} else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
|
|
1417
1600
|
members.add(entry);
|
|
1418
1601
|
}
|
|
1419
1602
|
}
|
|
@@ -1431,11 +1614,21 @@ async function resolveCountSource(root, claim) {
|
|
|
1431
1614
|
if (unit === "crate") return countCargoCrates(root);
|
|
1432
1615
|
return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
|
|
1433
1616
|
}
|
|
1617
|
+
async function resolveDocCountSource(root, doc, claim) {
|
|
1618
|
+
const scope = documentScope(doc);
|
|
1619
|
+
const source = await resolveCountSource(path13.join(root, scope), claim);
|
|
1620
|
+
if (!source || scope === ".") return source;
|
|
1621
|
+
return {
|
|
1622
|
+
...source,
|
|
1623
|
+
countedFrom: scope + "/" + source.countedFrom,
|
|
1624
|
+
members: source.members.map((member) => scope + "/" + member)
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1434
1627
|
async function checkStaleCounts(ctx) {
|
|
1435
1628
|
const result = emptyResult();
|
|
1436
1629
|
for (const doc of ctx.docs) {
|
|
1437
1630
|
for (const claim of doc.claims.counts) {
|
|
1438
|
-
const source = await
|
|
1631
|
+
const source = await resolveDocCountSource(ctx.root, doc.path, claim);
|
|
1439
1632
|
if (source === null) {
|
|
1440
1633
|
result.skipped.push({
|
|
1441
1634
|
check: "stale-count",
|
|
@@ -1467,6 +1660,7 @@ var MEMBERS_CAP;
|
|
|
1467
1660
|
var init_stale_count = __esm({
|
|
1468
1661
|
"src/audit/checks/stale-count.ts"() {
|
|
1469
1662
|
"use strict";
|
|
1663
|
+
init_docs();
|
|
1470
1664
|
init_inputs();
|
|
1471
1665
|
init_checks();
|
|
1472
1666
|
MEMBERS_CAP = 50;
|
|
@@ -1474,61 +1668,94 @@ var init_stale_count = __esm({
|
|
|
1474
1668
|
});
|
|
1475
1669
|
|
|
1476
1670
|
// src/audit/checks/dead-command.ts
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (raw === null) return null;
|
|
1671
|
+
import path14 from "path";
|
|
1672
|
+
function scriptsOf(raw) {
|
|
1480
1673
|
try {
|
|
1481
1674
|
const pkg = JSON.parse(raw);
|
|
1482
|
-
|
|
1675
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
|
|
1676
|
+
if (pkg.scripts === void 0) return [];
|
|
1677
|
+
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
1678
|
} catch {
|
|
1484
1679
|
return null;
|
|
1485
1680
|
}
|
|
1486
1681
|
}
|
|
1682
|
+
async function commandInputs(root, docs) {
|
|
1683
|
+
const contents = /* @__PURE__ */ new Map();
|
|
1684
|
+
const read = async (file) => {
|
|
1685
|
+
if (!contents.has(file)) contents.set(file, await readAuditInput(root, file));
|
|
1686
|
+
return contents.get(file);
|
|
1687
|
+
};
|
|
1688
|
+
await read("package.json");
|
|
1689
|
+
let inventory;
|
|
1690
|
+
const resolutions = [];
|
|
1691
|
+
for (const doc of docs) for (const claim of doc.claims.commands) {
|
|
1692
|
+
const base = documentScope(doc.path);
|
|
1693
|
+
const directory = scopedPath(base, claim.directory ?? ".");
|
|
1694
|
+
const scope = {
|
|
1695
|
+
basis: claim.directory === void 0 ? "document" : "explicit",
|
|
1696
|
+
directory: directory ?? base,
|
|
1697
|
+
candidates: []
|
|
1698
|
+
};
|
|
1699
|
+
const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
|
|
1700
|
+
resolutions.push(resolution);
|
|
1701
|
+
if (claim.scopeUnknown || directory === null) {
|
|
1702
|
+
resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
let cursor = directory;
|
|
1706
|
+
while (true) {
|
|
1707
|
+
const manifest = path14.posix.join(cursor, "package.json");
|
|
1708
|
+
scope.candidates.push(manifest);
|
|
1709
|
+
const raw = await read(manifest);
|
|
1710
|
+
if (raw !== null) {
|
|
1711
|
+
resolution.manifest = manifest;
|
|
1712
|
+
const scripts = scriptsOf(raw);
|
|
1713
|
+
if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
|
|
1714
|
+
else resolution.scripts = scripts;
|
|
1715
|
+
break;
|
|
1716
|
+
}
|
|
1717
|
+
if (claim.directory !== void 0 || cursor === ".") break;
|
|
1718
|
+
cursor = path14.posix.dirname(cursor);
|
|
1719
|
+
}
|
|
1720
|
+
if (!resolution.manifest && !resolution.problem) {
|
|
1721
|
+
resolution.problem = `No package.json for the command's scope (${directory}).`;
|
|
1722
|
+
}
|
|
1723
|
+
if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
|
|
1724
|
+
inventory ??= await commandManifests(root);
|
|
1725
|
+
for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
|
|
1726
|
+
if (scope.candidates.includes(manifest)) continue;
|
|
1727
|
+
scope.candidates.push(manifest);
|
|
1728
|
+
const raw = await read(manifest);
|
|
1729
|
+
if (raw === null) continue;
|
|
1730
|
+
const scripts = scriptsOf(raw);
|
|
1731
|
+
if (scripts === null) resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
|
|
1732
|
+
else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
|
|
1736
|
+
}
|
|
1487
1737
|
async function checkDeadCommands(ctx) {
|
|
1488
1738
|
const result = emptyResult();
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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);
|
|
1739
|
+
const { resolutions } = await commandInputs(ctx.root, ctx.docs);
|
|
1740
|
+
for (const { doc, claim, scope, scripts, elsewhere, problem, manifest } of resolutions) {
|
|
1741
|
+
if (problem) {
|
|
1742
|
+
result.skipped.push({ check: "dead-command", doc, reason: problem });
|
|
1743
|
+
continue;
|
|
1512
1744
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
invocation: claim.invocation,
|
|
1528
|
-
manifestsChecked,
|
|
1529
|
-
availableScripts: rootScripts.slice(0, AVAILABLE_SCRIPTS_CAP)
|
|
1530
|
-
}
|
|
1531
|
-
});
|
|
1745
|
+
if (scripts.includes(claim.scriptName)) continue;
|
|
1746
|
+
const evidence = {
|
|
1747
|
+
kind: "missing-script",
|
|
1748
|
+
scriptName: claim.scriptName,
|
|
1749
|
+
invocation: claim.invocation,
|
|
1750
|
+
manifestsChecked: scope.candidates,
|
|
1751
|
+
availableScripts: scripts.slice(0, AVAILABLE_SCRIPTS_CAP),
|
|
1752
|
+
scope
|
|
1753
|
+
};
|
|
1754
|
+
const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
|
|
1755
|
+
const message2 = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
|
|
1756
|
+
const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
|
|
1757
|
+
if (certain) result.issues.push({ type: "dead-command", message: message2, anchor, confidence: "certain", evidence });
|
|
1758
|
+
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
1759
|
}
|
|
1533
1760
|
return result;
|
|
1534
1761
|
}
|
|
@@ -1543,6 +1770,8 @@ var init_dead_command = __esm({
|
|
|
1543
1770
|
"src/audit/checks/dead-command.ts"() {
|
|
1544
1771
|
"use strict";
|
|
1545
1772
|
init_inputs();
|
|
1773
|
+
init_docs();
|
|
1774
|
+
init_scope();
|
|
1546
1775
|
init_checks();
|
|
1547
1776
|
AVAILABLE_SCRIPTS_CAP = 30;
|
|
1548
1777
|
}
|
|
@@ -1556,13 +1785,13 @@ async function git(root, args2) {
|
|
|
1556
1785
|
}
|
|
1557
1786
|
function withoutAndroidReleaseValues(text2) {
|
|
1558
1787
|
if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
|
|
1559
|
-
const
|
|
1788
|
+
const scopes2 = [];
|
|
1560
1789
|
const normalized = [];
|
|
1561
1790
|
let assignments = 0;
|
|
1562
1791
|
for (const line of text2.split("\n")) {
|
|
1563
1792
|
const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
|
|
1564
1793
|
const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
|
|
1565
|
-
if (assignment &&
|
|
1794
|
+
if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
|
|
1566
1795
|
normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
|
|
1567
1796
|
assignments++;
|
|
1568
1797
|
} else {
|
|
@@ -1571,12 +1800,12 @@ function withoutAndroidReleaseValues(text2) {
|
|
|
1571
1800
|
}
|
|
1572
1801
|
const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
|
|
1573
1802
|
for (const brace of code.matchAll(/[{}]/g)) {
|
|
1574
|
-
if (brace[0] === "{")
|
|
1575
|
-
else if (!
|
|
1576
|
-
else
|
|
1803
|
+
if (brace[0] === "{") scopes2.push(named ?? "unknown");
|
|
1804
|
+
else if (!scopes2.length) return null;
|
|
1805
|
+
else scopes2.pop();
|
|
1577
1806
|
}
|
|
1578
1807
|
}
|
|
1579
|
-
return assignments && !
|
|
1808
|
+
return assignments && !scopes2.length ? normalized.join("\n") : null;
|
|
1580
1809
|
}
|
|
1581
1810
|
async function releaseMetadataOnly(root, commit) {
|
|
1582
1811
|
if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
|
|
@@ -1607,6 +1836,12 @@ var init_release_metadata = __esm({
|
|
|
1607
1836
|
});
|
|
1608
1837
|
|
|
1609
1838
|
// src/audit/checks/deps-changed.ts
|
|
1839
|
+
function manifestPathspecs(doc) {
|
|
1840
|
+
const scope = documentScope(doc);
|
|
1841
|
+
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
1842
|
+
const escaped = scope.replace(/[\\*?\[\]]/g, "\\$&");
|
|
1843
|
+
return MANIFEST_PATHSPECS.map((spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec);
|
|
1844
|
+
}
|
|
1610
1845
|
async function checkDepsChanged(ctx) {
|
|
1611
1846
|
const result = emptyResult();
|
|
1612
1847
|
result.suppressedAdvisories = [];
|
|
@@ -1630,7 +1865,7 @@ async function checkDepsChanged(ctx) {
|
|
|
1630
1865
|
const range = await commitsTouchingSince(
|
|
1631
1866
|
ctx.root,
|
|
1632
1867
|
doc.lastCommit.hash,
|
|
1633
|
-
|
|
1868
|
+
manifestPathspecs(doc.path)
|
|
1634
1869
|
);
|
|
1635
1870
|
if (range === null) {
|
|
1636
1871
|
result.skipped.push({
|
|
@@ -1669,6 +1904,7 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
|
1669
1904
|
var init_deps_changed = __esm({
|
|
1670
1905
|
"src/audit/checks/deps-changed.ts"() {
|
|
1671
1906
|
"use strict";
|
|
1907
|
+
init_docs();
|
|
1672
1908
|
init_release_metadata();
|
|
1673
1909
|
init_git();
|
|
1674
1910
|
init_checks();
|
|
@@ -1691,7 +1927,7 @@ var init_deps_changed = __esm({
|
|
|
1691
1927
|
});
|
|
1692
1928
|
|
|
1693
1929
|
// src/context/lexical.ts
|
|
1694
|
-
import
|
|
1930
|
+
import path15 from "path";
|
|
1695
1931
|
var init_lexical = __esm({
|
|
1696
1932
|
"src/context/lexical.ts"() {
|
|
1697
1933
|
"use strict";
|
|
@@ -1875,7 +2111,7 @@ var init_provenance = __esm({
|
|
|
1875
2111
|
|
|
1876
2112
|
// src/decisions/decisions.ts
|
|
1877
2113
|
import fs9 from "fs/promises";
|
|
1878
|
-
import
|
|
2114
|
+
import path16 from "path";
|
|
1879
2115
|
import { createHash } from "crypto";
|
|
1880
2116
|
import { z as z4 } from "zod";
|
|
1881
2117
|
async function loadDecisionStore(rootDir) {
|
|
@@ -1926,9 +2162,9 @@ var init_decisions = __esm({
|
|
|
1926
2162
|
});
|
|
1927
2163
|
|
|
1928
2164
|
// src/decisions/drift.ts
|
|
1929
|
-
import
|
|
2165
|
+
import path17 from "path";
|
|
1930
2166
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
1931
|
-
const resolvedRoot =
|
|
2167
|
+
const resolvedRoot = path17.resolve(rootDir);
|
|
1932
2168
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
1933
2169
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
1934
2170
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -2009,46 +2245,338 @@ async function checkDecisionAnchors(ctx) {
|
|
|
2009
2245
|
}
|
|
2010
2246
|
return result;
|
|
2011
2247
|
}
|
|
2012
|
-
var init_decision_anchor = __esm({
|
|
2013
|
-
"src/audit/checks/decision-anchor.ts"() {
|
|
2014
|
-
"use strict";
|
|
2015
|
-
init_drift2();
|
|
2016
|
-
init_decisions();
|
|
2017
|
-
init_provenance();
|
|
2018
|
-
init_checks();
|
|
2248
|
+
var init_decision_anchor = __esm({
|
|
2249
|
+
"src/audit/checks/decision-anchor.ts"() {
|
|
2250
|
+
"use strict";
|
|
2251
|
+
init_drift2();
|
|
2252
|
+
init_decisions();
|
|
2253
|
+
init_provenance();
|
|
2254
|
+
init_checks();
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
|
|
2258
|
+
// src/audit/checks/index.ts
|
|
2259
|
+
function emptyResult() {
|
|
2260
|
+
return { issues: [], advisories: [], skipped: [] };
|
|
2261
|
+
}
|
|
2262
|
+
var CHECKS;
|
|
2263
|
+
var init_checks = __esm({
|
|
2264
|
+
"src/audit/checks/index.ts"() {
|
|
2265
|
+
"use strict";
|
|
2266
|
+
init_deleted_reference();
|
|
2267
|
+
init_new_module();
|
|
2268
|
+
init_stale_count();
|
|
2269
|
+
init_dead_command();
|
|
2270
|
+
init_deps_changed();
|
|
2271
|
+
init_decision_anchor();
|
|
2272
|
+
CHECKS = {
|
|
2273
|
+
"deleted-reference": checkDeletedReferences,
|
|
2274
|
+
"new-module": checkNewModules,
|
|
2275
|
+
"stale-count": checkStaleCounts,
|
|
2276
|
+
"dead-command": checkDeadCommands,
|
|
2277
|
+
"deps-changed": checkDepsChanged,
|
|
2278
|
+
"decision-anchor-drift": checkDecisionAnchors
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
});
|
|
2282
|
+
|
|
2283
|
+
// src/audit/findings.ts
|
|
2284
|
+
import { createHash as createHash2 } from "crypto";
|
|
2285
|
+
import { z as z5 } from "zod";
|
|
2286
|
+
function findingId(finding) {
|
|
2287
|
+
const e = finding.evidence;
|
|
2288
|
+
let key;
|
|
2289
|
+
switch (e.kind) {
|
|
2290
|
+
case "missing-path":
|
|
2291
|
+
key = e.resolvedPath && e.resolvedPath !== e.claimed ? [e.claimed, e.resolvedPath] : e.claimed;
|
|
2292
|
+
break;
|
|
2293
|
+
case "unmentioned-dir":
|
|
2294
|
+
key = e.dir;
|
|
2295
|
+
break;
|
|
2296
|
+
case "count-mismatch":
|
|
2297
|
+
key = [e.unit.replace(/s$/, ""), e.countedFrom];
|
|
2298
|
+
break;
|
|
2299
|
+
case "missing-script":
|
|
2300
|
+
key = e.scope && e.scope.directory !== "." ? [e.scriptName, e.scope.directory] : e.scriptName;
|
|
2301
|
+
break;
|
|
2302
|
+
case "doc-behind-manifests":
|
|
2303
|
+
key = null;
|
|
2304
|
+
break;
|
|
2305
|
+
case "decision-anchor":
|
|
2306
|
+
key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2309
|
+
return digest([finding.type, finding.anchor.doc, key]);
|
|
2310
|
+
}
|
|
2311
|
+
var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema;
|
|
2312
|
+
var init_findings = __esm({
|
|
2313
|
+
"src/audit/findings.ts"() {
|
|
2314
|
+
"use strict";
|
|
2315
|
+
digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2316
|
+
checkSchema = z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
|
|
2317
|
+
commitSchema = z5.object({ hash: z5.string().regex(/^[a-f0-9]{40,64}$/), date: z5.string(), subject: z5.string() });
|
|
2318
|
+
anchorSchema = z5.object({ doc: z5.string(), line: z5.number().int().positive().nullable(), excerpt: z5.string().nullable() });
|
|
2319
|
+
count = z5.number().int().nonnegative();
|
|
2320
|
+
scopeSchema = z5.object({
|
|
2321
|
+
basis: z5.enum(["repository", "document", "document-link", "explicit"]),
|
|
2322
|
+
directory: z5.string(),
|
|
2323
|
+
candidates: z5.array(z5.string())
|
|
2324
|
+
});
|
|
2325
|
+
evidenceSchema = z5.discriminatedUnion("kind", [
|
|
2326
|
+
z5.object({
|
|
2327
|
+
kind: z5.literal("missing-path"),
|
|
2328
|
+
claimed: z5.string(),
|
|
2329
|
+
renamedTo: z5.string().nullable(),
|
|
2330
|
+
deletedInCommit: commitSchema.nullable(),
|
|
2331
|
+
everTracked: z5.boolean(),
|
|
2332
|
+
parentDirExists: z5.boolean(),
|
|
2333
|
+
resolvedPath: z5.string().optional(),
|
|
2334
|
+
scope: scopeSchema.optional()
|
|
2335
|
+
}),
|
|
2336
|
+
z5.object({
|
|
2337
|
+
kind: z5.literal("unmentioned-dir"),
|
|
2338
|
+
dir: z5.string(),
|
|
2339
|
+
sourceFileCount: count,
|
|
2340
|
+
firstCommit: commitSchema.nullable(),
|
|
2341
|
+
checkedDocs: z5.array(z5.string())
|
|
2342
|
+
}),
|
|
2343
|
+
z5.object({
|
|
2344
|
+
kind: z5.literal("count-mismatch"),
|
|
2345
|
+
claimed: count,
|
|
2346
|
+
actual: count,
|
|
2347
|
+
unit: z5.string(),
|
|
2348
|
+
countedFrom: z5.string(),
|
|
2349
|
+
members: z5.array(z5.string())
|
|
2350
|
+
}),
|
|
2351
|
+
z5.object({
|
|
2352
|
+
kind: z5.literal("missing-script"),
|
|
2353
|
+
scriptName: z5.string(),
|
|
2354
|
+
invocation: z5.string(),
|
|
2355
|
+
manifestsChecked: z5.array(z5.string()),
|
|
2356
|
+
availableScripts: z5.array(z5.string()),
|
|
2357
|
+
scope: scopeSchema.optional()
|
|
2358
|
+
}),
|
|
2359
|
+
z5.object({
|
|
2360
|
+
kind: z5.literal("doc-behind-manifests"),
|
|
2361
|
+
docLastCommit: commitSchema,
|
|
2362
|
+
manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
|
|
2363
|
+
totalCommits: count
|
|
2364
|
+
}),
|
|
2365
|
+
z5.object({
|
|
2366
|
+
kind: z5.literal("decision-anchor"),
|
|
2367
|
+
decisionId: z5.string(),
|
|
2368
|
+
title: z5.string(),
|
|
2369
|
+
changedFiles: z5.array(z5.string()),
|
|
2370
|
+
refreshedHash: z5.string(),
|
|
2371
|
+
provenance: z5.object({}).passthrough().optional()
|
|
2372
|
+
})
|
|
2373
|
+
]);
|
|
2374
|
+
findingSchema = z5.object({ message: z5.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
2375
|
+
issueSchema = findingSchema.extend({
|
|
2376
|
+
type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2377
|
+
confidence: z5.enum(["certain", "likely"])
|
|
2378
|
+
});
|
|
2379
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.literal("recheck").optional() });
|
|
2380
|
+
checkResultSchema = z5.object({
|
|
2381
|
+
issues: z5.array(issueSchema),
|
|
2382
|
+
advisories: z5.array(advisorySchema),
|
|
2383
|
+
suppressedAdvisories: z5.array(advisorySchema).optional(),
|
|
2384
|
+
skipped: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
|
|
2389
|
+
// src/audit/advisory-review.ts
|
|
2390
|
+
import fs10 from "fs/promises";
|
|
2391
|
+
import { execFile as execFile8 } from "child_process";
|
|
2392
|
+
import { promisify as promisify8 } from "util";
|
|
2393
|
+
import { z as z6 } from "zod";
|
|
2394
|
+
async function readRecord(root, id) {
|
|
2395
|
+
const raw = await readStoreJson(root, recordPath(id));
|
|
2396
|
+
if (raw === null) return null;
|
|
2397
|
+
const record = recordSchema.parse(raw);
|
|
2398
|
+
const { digest: stored, ...payload } = record;
|
|
2399
|
+
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]))) {
|
|
2400
|
+
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2401
|
+
}
|
|
2402
|
+
return record;
|
|
2403
|
+
}
|
|
2404
|
+
function scopes(finding) {
|
|
2405
|
+
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2406
|
+
if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
2407
|
+
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
2408
|
+
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
2409
|
+
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
2410
|
+
else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
|
|
2411
|
+
else throw new Error("This advisory has no supported review scope.");
|
|
2412
|
+
return [...new Set(files)].map((file) => {
|
|
2413
|
+
if (normalizeRepoPath(file) !== file) throw new Error("Advisory contains an invalid scoped path: " + file);
|
|
2414
|
+
return `:(literal)${file}`;
|
|
2415
|
+
}).sort();
|
|
2416
|
+
}
|
|
2417
|
+
async function reviewEvidence(root, finding) {
|
|
2418
|
+
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
2419
|
+
const scope = scopes(finding);
|
|
2420
|
+
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2421
|
+
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2422
|
+
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
2423
|
+
const [tree, status, lastScopeCommit, document] = await Promise.all([
|
|
2424
|
+
tracked.length ? git2(root, "ls-tree", "-r", "-z", "HEAD", "--", ...tracked.map((file) => `:(literal)${file}`)) : "",
|
|
2425
|
+
git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
|
|
2426
|
+
git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
|
|
2427
|
+
readAuditInput(root, finding.anchor.doc)
|
|
2428
|
+
]);
|
|
2429
|
+
if (!document?.trim()) throw new Error("Original advisory document is missing, empty or unreadable.");
|
|
2430
|
+
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.");
|
|
2431
|
+
for (const entry of tree.split("\0").filter(Boolean)) {
|
|
2432
|
+
const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
|
|
2433
|
+
if (entry.startsWith("120000 ") || entry.startsWith("160000 ")) throw new Error("Symlink or submodule in advisory review scope: " + file);
|
|
2434
|
+
await storePath(root, file);
|
|
2435
|
+
}
|
|
2436
|
+
const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
|
|
2437
|
+
(finding.evidence.scope?.candidates ?? [finding.evidence.resolvedPath ?? finding.evidence.claimed]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
|
|
2438
|
+
) : [];
|
|
2439
|
+
let dirty = status.length > 0;
|
|
2440
|
+
if (finding.evidence.kind === "missing-script") {
|
|
2441
|
+
for (const file of [...finding.evidence.manifestsChecked, ...finding.evidence.scope?.candidates ?? []]) {
|
|
2442
|
+
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
|
|
2446
|
+
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
2447
|
+
return {
|
|
2448
|
+
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
2449
|
+
dirty,
|
|
2450
|
+
documentDigest: digest(document)
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
async function decisionAssessment(root, finding, baselineHead) {
|
|
2454
|
+
const evidence = finding.evidence;
|
|
2455
|
+
if (evidence.kind !== "decision-anchor") return null;
|
|
2456
|
+
const id = findingId(finding);
|
|
2457
|
+
const store = await loadDecisionStore(root);
|
|
2458
|
+
if (store.diagnostics.length) throw new Error("Decision records have unavailable or malformed evidence; review cannot be confirmed.");
|
|
2459
|
+
const record = store.records.find((record2) => record2.id === evidence.decisionId);
|
|
2460
|
+
if (!record || record.version !== 2) return null;
|
|
2461
|
+
const events2 = record.history.filter((event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind));
|
|
2462
|
+
const event = events2.at(-1);
|
|
2463
|
+
if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length) return null;
|
|
2464
|
+
try {
|
|
2465
|
+
await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
|
|
2466
|
+
} catch {
|
|
2467
|
+
return null;
|
|
2468
|
+
}
|
|
2469
|
+
const drift = await computeDecisionDrift(root, [record]);
|
|
2470
|
+
if (!drift.historyAvailable) throw new Error("Decision review history is unavailable.");
|
|
2471
|
+
const retired = record.status === "retired" && event.kind === "retired";
|
|
2472
|
+
const current = retired || record.status === "active" && record.approval === "accepted" && drift.freshness?.[record.id] === "current" && !drift.pendingProposals?.[record.id];
|
|
2473
|
+
return {
|
|
2474
|
+
id,
|
|
2475
|
+
status: current ? "current" : "reopened",
|
|
2476
|
+
outcome: event.kind,
|
|
2477
|
+
reviewer: event.actor,
|
|
2478
|
+
note: event.note,
|
|
2479
|
+
reviewedHead: event.evidence.headHash,
|
|
2480
|
+
recordPath: `.mason/decisions/${record.id}.json`,
|
|
2481
|
+
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."
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
async function assessAdvisory(root, finding, baselineHead) {
|
|
2485
|
+
if ("confidence" in finding) return null;
|
|
2486
|
+
const id = findingId(finding);
|
|
2487
|
+
try {
|
|
2488
|
+
if (finding.evidence.kind === "decision-anchor") return await decisionAssessment(root, finding, baselineHead);
|
|
2489
|
+
const record = await readRecord(root, id);
|
|
2490
|
+
if (!record) return null;
|
|
2491
|
+
const event = record.events.at(-1);
|
|
2492
|
+
const current = await reviewEvidence(root, event.finding);
|
|
2493
|
+
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
2494
|
+
const base = {
|
|
2495
|
+
id,
|
|
2496
|
+
recordPath: recordPath(id),
|
|
2497
|
+
reviewer: event.reviewer,
|
|
2498
|
+
note: event.note,
|
|
2499
|
+
outcome: event.outcome,
|
|
2500
|
+
reviewedHead: event.evidence.head
|
|
2501
|
+
};
|
|
2502
|
+
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." };
|
|
2503
|
+
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." };
|
|
2504
|
+
} catch (error) {
|
|
2505
|
+
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
async function advisoryReviewInventory(root) {
|
|
2509
|
+
let names;
|
|
2510
|
+
try {
|
|
2511
|
+
names = await fs10.readdir(await storePath(root, ".mason/reviews/advisories"));
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
if (error.code === "ENOENT") return [];
|
|
2514
|
+
throw error;
|
|
2019
2515
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
// src/audit/checks/index.ts
|
|
2023
|
-
function emptyResult() {
|
|
2024
|
-
return { issues: [], advisories: [], skipped: [] };
|
|
2516
|
+
if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
|
|
2517
|
+
return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
|
|
2025
2518
|
}
|
|
2026
|
-
var
|
|
2027
|
-
var
|
|
2028
|
-
"src/audit/
|
|
2519
|
+
var execute2, git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2520
|
+
var init_advisory_review = __esm({
|
|
2521
|
+
"src/audit/advisory-review.ts"() {
|
|
2029
2522
|
"use strict";
|
|
2030
|
-
|
|
2031
|
-
init_new_module();
|
|
2032
|
-
init_stale_count();
|
|
2033
|
-
init_dead_command();
|
|
2523
|
+
init_findings();
|
|
2034
2524
|
init_deps_changed();
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
};
|
|
2525
|
+
init_inputs();
|
|
2526
|
+
init_scope();
|
|
2527
|
+
init_storage();
|
|
2528
|
+
init_paths();
|
|
2529
|
+
init_store();
|
|
2530
|
+
init_decisions();
|
|
2531
|
+
init_drift2();
|
|
2532
|
+
execute2 = promisify8(execFile8);
|
|
2533
|
+
git2 = async (root, ...args2) => (await execute2("git", args2, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
2534
|
+
hashSchema = z6.string().regex(/^[a-f0-9]{64}$/);
|
|
2535
|
+
commitSchema2 = z6.string().regex(/^[a-f0-9]{40,64}$/);
|
|
2536
|
+
evidenceSchema2 = z6.object({
|
|
2537
|
+
head: commitSchema2,
|
|
2538
|
+
fingerprint: hashSchema,
|
|
2539
|
+
scope: z6.array(z6.string()).min(1).max(1e3),
|
|
2540
|
+
tree: z6.string().max(2 * 1024 * 1024),
|
|
2541
|
+
lastScopeCommit: commitSchema2,
|
|
2542
|
+
paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
|
|
2543
|
+
});
|
|
2544
|
+
eventSchema2 = z6.object({
|
|
2545
|
+
at: z6.string().datetime(),
|
|
2546
|
+
reviewer: z6.string().trim().min(1).max(200),
|
|
2547
|
+
note: z6.string().trim().min(1).max(2500),
|
|
2548
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
|
|
2549
|
+
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2550
|
+
evidence: evidenceSchema2
|
|
2551
|
+
});
|
|
2552
|
+
recordSchema = z6.object({ version: z6.literal(1), id: hashSchema, events: z6.array(eventSchema2).min(1).max(200), digest: hashSchema });
|
|
2553
|
+
reviewSummarySchema = z6.object({
|
|
2554
|
+
id: hashSchema,
|
|
2555
|
+
status: z6.enum(["current", "reopened", "deferred", "unverified"]),
|
|
2556
|
+
reason: z6.string(),
|
|
2557
|
+
recordPath: z6.string().optional(),
|
|
2558
|
+
reviewer: z6.string().optional(),
|
|
2559
|
+
note: z6.string().optional(),
|
|
2560
|
+
outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
2561
|
+
reviewedHead: z6.string().optional()
|
|
2562
|
+
});
|
|
2563
|
+
advisoryReviewRequest = z6.object({
|
|
2564
|
+
baselinePath: z6.string().min(1),
|
|
2565
|
+
findingId: hashSchema,
|
|
2566
|
+
action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
2567
|
+
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2568
|
+
note: eventSchema2.shape.note.optional(),
|
|
2569
|
+
reviewToken: hashSchema.optional()
|
|
2570
|
+
});
|
|
2571
|
+
recordPath = (id) => `.mason/reviews/advisories/${hashSchema.parse(id)}.json`;
|
|
2044
2572
|
}
|
|
2045
2573
|
});
|
|
2046
2574
|
|
|
2047
2575
|
// src/audit/audit.ts
|
|
2048
|
-
import
|
|
2049
|
-
import
|
|
2576
|
+
import fs11 from "fs/promises";
|
|
2577
|
+
import path18 from "path";
|
|
2050
2578
|
async function computeAudit(rootDir, options = {}) {
|
|
2051
|
-
const resolvedRoot =
|
|
2579
|
+
const resolvedRoot = path18.resolve(rootDir);
|
|
2052
2580
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2053
2581
|
if (docs.length === 0) return null;
|
|
2054
2582
|
const report = {
|
|
@@ -2059,6 +2587,8 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2059
2587
|
checksRun: [],
|
|
2060
2588
|
docs: docs.map((d) => ({
|
|
2061
2589
|
path: d.path,
|
|
2590
|
+
scope: d.scope,
|
|
2591
|
+
kind: d.kind,
|
|
2062
2592
|
lastCommit: d.lastCommit,
|
|
2063
2593
|
dirty: d.dirty,
|
|
2064
2594
|
lineCount: d.lineCount
|
|
@@ -2080,7 +2610,7 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2080
2610
|
}
|
|
2081
2611
|
let decisionsPresent = false;
|
|
2082
2612
|
try {
|
|
2083
|
-
await
|
|
2613
|
+
await fs11.access(path18.join(resolvedRoot, ".mason", "decisions"));
|
|
2084
2614
|
decisionsPresent = true;
|
|
2085
2615
|
} catch {
|
|
2086
2616
|
}
|
|
@@ -2103,6 +2633,8 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2103
2633
|
report.skippedChecks.push(...skipped);
|
|
2104
2634
|
}
|
|
2105
2635
|
report.clean = report.issues.length === 0;
|
|
2636
|
+
const reviews = (await Promise.all([...report.advisories, ...report.suppressedAdvisories].map((finding) => assessAdvisory(resolvedRoot, finding, headHash)))).filter((review) => review !== null);
|
|
2637
|
+
if (reviews.length) report.advisoryReviews = reviews;
|
|
2106
2638
|
return report;
|
|
2107
2639
|
}
|
|
2108
2640
|
var init_audit = __esm({
|
|
@@ -2113,45 +2645,24 @@ var init_audit = __esm({
|
|
|
2113
2645
|
init_docs();
|
|
2114
2646
|
init_types();
|
|
2115
2647
|
init_checks();
|
|
2648
|
+
init_advisory_review();
|
|
2116
2649
|
}
|
|
2117
2650
|
});
|
|
2118
2651
|
|
|
2119
2652
|
// 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]);
|
|
2653
|
+
import fs12 from "fs/promises";
|
|
2654
|
+
import path19 from "path";
|
|
2655
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2656
|
+
import { z as z7 } from "zod";
|
|
2657
|
+
function recheckable(finding) {
|
|
2658
|
+
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
2659
|
}
|
|
2149
2660
|
function allFindings(report) {
|
|
2150
2661
|
return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
|
|
2151
2662
|
}
|
|
2152
2663
|
async function docState(root) {
|
|
2153
2664
|
const docs = [];
|
|
2154
|
-
for (const doc of
|
|
2665
|
+
for (const doc of await discoverDocPaths(root)) {
|
|
2155
2666
|
try {
|
|
2156
2667
|
const content2 = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
|
|
2157
2668
|
if (content2 === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
|
|
@@ -2173,8 +2684,8 @@ async function stableAudit(root, checks, options = {}) {
|
|
|
2173
2684
|
return report;
|
|
2174
2685
|
}
|
|
2175
2686
|
async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
2176
|
-
const root = await
|
|
2177
|
-
const selected =
|
|
2687
|
+
const root = await fs12.realpath(rootDir);
|
|
2688
|
+
const selected = z7.array(checkSchema).nonempty().parse(checks);
|
|
2178
2689
|
const report = await stableAudit(root, selected, options);
|
|
2179
2690
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
2180
2691
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
@@ -2189,23 +2700,29 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
|
2189
2700
|
await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
|
|
2190
2701
|
return { version: 1, action: "prepare", baselinePath, report };
|
|
2191
2702
|
}
|
|
2192
|
-
async function
|
|
2193
|
-
const root = await
|
|
2194
|
-
const declaredRoot =
|
|
2195
|
-
const relative =
|
|
2703
|
+
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
2704
|
+
const root = await fs12.realpath(rootDir);
|
|
2705
|
+
const declaredRoot = path19.resolve(rootDir);
|
|
2706
|
+
const relative = path19.isAbsolute(baselinePath) ? path19.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2196
2707
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
2197
2708
|
const { digest: savedDigest, ...payload } = stored;
|
|
2198
2709
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
2199
2710
|
if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
|
|
2200
2711
|
const original = stored.report;
|
|
2712
|
+
return { root, relative, original };
|
|
2713
|
+
}
|
|
2714
|
+
async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
2715
|
+
const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
|
|
2201
2716
|
const diagnostics = [];
|
|
2717
|
+
let reviewDigest;
|
|
2202
2718
|
let current = null;
|
|
2203
2719
|
try {
|
|
2720
|
+
reviewDigest = digest(await advisoryReviewInventory(root));
|
|
2204
2721
|
current = await stableAudit(root, original.checksRun, options);
|
|
2205
2722
|
if (!current) diagnostics.push("No context files remain available to audit.");
|
|
2206
2723
|
else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
|
|
2207
2724
|
for (const doc of original.docs) {
|
|
2208
|
-
if (!original.
|
|
2725
|
+
if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
|
|
2209
2726
|
const content2 = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
|
|
2210
2727
|
if (content2 === null || !content2.trim()) {
|
|
2211
2728
|
diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
|
|
@@ -2222,21 +2739,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2222
2739
|
const originalIds = new Set(originalFindings.map(findingId));
|
|
2223
2740
|
const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
|
|
2224
2741
|
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) => {
|
|
2742
|
+
const findings = await Promise.all(originalFindings.map(async (finding) => {
|
|
2226
2743
|
const id = findingId(finding);
|
|
2227
2744
|
const now = currentById.get(id);
|
|
2228
2745
|
const base = { id, original: finding, ...now ? { current: now } : {} };
|
|
2229
2746
|
if (diagnostics.length || !current) {
|
|
2230
2747
|
return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
|
|
2231
2748
|
}
|
|
2232
|
-
if ("confidence" in finding
|
|
2749
|
+
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
2233
2750
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
2234
2751
|
}
|
|
2235
2752
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
2236
2753
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
2237
2754
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
2238
2755
|
}
|
|
2239
|
-
|
|
2756
|
+
const review = await assessAdvisory(root, now ?? finding, original.headHash);
|
|
2757
|
+
if (review) {
|
|
2758
|
+
if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
|
|
2759
|
+
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
2760
|
+
return { ...base, review, status: "review-required", reason: review.reason };
|
|
2761
|
+
}
|
|
2762
|
+
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
2240
2763
|
return {
|
|
2241
2764
|
...base,
|
|
2242
2765
|
status: "review-required",
|
|
@@ -2244,11 +2767,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2244
2767
|
};
|
|
2245
2768
|
}
|
|
2246
2769
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
2247
|
-
});
|
|
2770
|
+
}));
|
|
2771
|
+
try {
|
|
2772
|
+
if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
|
|
2773
|
+
diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
|
|
2774
|
+
for (const finding of findings) {
|
|
2775
|
+
finding.status = "unverified";
|
|
2776
|
+
finding.reason = diagnostics.at(-1);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
diagnostics.push(String(error));
|
|
2781
|
+
for (const finding of findings) {
|
|
2782
|
+
finding.status = "unverified";
|
|
2783
|
+
finding.reason = "Advisory review evidence became unavailable during verification.";
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2248
2786
|
const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
|
|
2787
|
+
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
2249
2788
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
2250
2789
|
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));
|
|
2790
|
+
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
2791
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
2253
2792
|
return {
|
|
2254
2793
|
version: 1,
|
|
@@ -2262,10 +2801,10 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2262
2801
|
diagnostics,
|
|
2263
2802
|
currentAudit: current,
|
|
2264
2803
|
counts,
|
|
2265
|
-
scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by
|
|
2804
|
+
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
2805
|
};
|
|
2267
2806
|
}
|
|
2268
|
-
var
|
|
2807
|
+
var count2, reportSchema, baselineSchema, repairFindingSchema;
|
|
2269
2808
|
var init_repair = __esm({
|
|
2270
2809
|
"src/audit/repair.ts"() {
|
|
2271
2810
|
"use strict";
|
|
@@ -2277,101 +2816,45 @@ var init_repair = __esm({
|
|
|
2277
2816
|
init_files();
|
|
2278
2817
|
init_paths();
|
|
2279
2818
|
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),
|
|
2819
|
+
init_findings();
|
|
2820
|
+
init_advisory_review();
|
|
2821
|
+
init_findings();
|
|
2822
|
+
count2 = z7.number().int().nonnegative();
|
|
2823
|
+
reportSchema = z7.object({
|
|
2824
|
+
version: z7.literal(1),
|
|
2825
|
+
root: z7.string(),
|
|
2826
|
+
gitAvailable: z7.literal(true),
|
|
2346
2827
|
headHash: commitSchema.shape.hash,
|
|
2347
|
-
checksRun:
|
|
2348
|
-
docs:
|
|
2349
|
-
path:
|
|
2828
|
+
checksRun: z7.array(checkSchema).nonempty(),
|
|
2829
|
+
docs: z7.array(z7.object({
|
|
2830
|
+
path: z7.string().refine((value) => normalizeRepoPath(value) === value),
|
|
2831
|
+
scope: z7.string().optional(),
|
|
2832
|
+
kind: z7.enum(["instructions", "readme"]).optional(),
|
|
2350
2833
|
lastCommit: commitSchema.nullable(),
|
|
2351
|
-
dirty:
|
|
2352
|
-
lineCount:
|
|
2834
|
+
dirty: z7.boolean(),
|
|
2835
|
+
lineCount: count2
|
|
2353
2836
|
})).nonempty(),
|
|
2354
|
-
decisionsChecked:
|
|
2355
|
-
clean:
|
|
2356
|
-
issues:
|
|
2357
|
-
advisories:
|
|
2358
|
-
suppressedAdvisories:
|
|
2359
|
-
skippedChecks:
|
|
2837
|
+
decisionsChecked: z7.boolean(),
|
|
2838
|
+
clean: z7.boolean(),
|
|
2839
|
+
issues: z7.array(issueSchema),
|
|
2840
|
+
advisories: z7.array(advisorySchema),
|
|
2841
|
+
suppressedAdvisories: z7.array(advisorySchema).optional(),
|
|
2842
|
+
skippedChecks: z7.array(z7.object({ check: z7.string(), reason: z7.string(), doc: z7.string().optional() }))
|
|
2360
2843
|
});
|
|
2361
|
-
baselineSchema =
|
|
2362
|
-
kind:
|
|
2363
|
-
version:
|
|
2364
|
-
createdAt:
|
|
2844
|
+
baselineSchema = z7.object({
|
|
2845
|
+
kind: z7.literal("mason-audit-repair"),
|
|
2846
|
+
version: z7.literal(1),
|
|
2847
|
+
createdAt: z7.string().datetime(),
|
|
2365
2848
|
report: reportSchema,
|
|
2366
|
-
digest:
|
|
2849
|
+
digest: z7.string().regex(/^[a-f0-9]{64}$/)
|
|
2367
2850
|
});
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2851
|
+
repairFindingSchema = z7.object({
|
|
2852
|
+
id: z7.string(),
|
|
2853
|
+
original: z7.union([issueSchema, advisorySchema]),
|
|
2854
|
+
status: z7.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
2855
|
+
reason: z7.string(),
|
|
2856
|
+
current: z7.union([issueSchema, advisorySchema]).optional(),
|
|
2857
|
+
review: reviewSummarySchema.optional()
|
|
2375
2858
|
});
|
|
2376
2859
|
}
|
|
2377
2860
|
});
|
|
@@ -2380,102 +2863,94 @@ var init_repair = __esm({
|
|
|
2380
2863
|
var evidence_exports = {};
|
|
2381
2864
|
__export(evidence_exports, {
|
|
2382
2865
|
checkCache: () => checkCache,
|
|
2383
|
-
git: () =>
|
|
2866
|
+
git: () => git3,
|
|
2384
2867
|
hash: () => hash,
|
|
2385
2868
|
readInputs: () => readInputs,
|
|
2386
2869
|
workspace: () => workspace
|
|
2387
2870
|
});
|
|
2388
|
-
import
|
|
2389
|
-
import
|
|
2871
|
+
import fs13 from "fs/promises";
|
|
2872
|
+
import path20 from "path";
|
|
2390
2873
|
import { createHash as createHash3 } from "crypto";
|
|
2391
|
-
import { execFile as
|
|
2392
|
-
import { promisify as
|
|
2393
|
-
import
|
|
2394
|
-
import { z as
|
|
2395
|
-
async function
|
|
2874
|
+
import { execFile as execFile9 } from "child_process";
|
|
2875
|
+
import { promisify as promisify9 } from "util";
|
|
2876
|
+
import fg5 from "fast-glob";
|
|
2877
|
+
import { z as z8 } from "zod";
|
|
2878
|
+
async function git3(root, ...args2) {
|
|
2396
2879
|
return (await exec7("git", args2, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
|
|
2397
2880
|
}
|
|
2398
2881
|
async function workspace(dir) {
|
|
2399
|
-
const root = await
|
|
2400
|
-
const gitDir = await
|
|
2882
|
+
const root = await fs13.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
|
|
2883
|
+
const gitDir = await fs13.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
2401
2884
|
let branch;
|
|
2402
2885
|
try {
|
|
2403
|
-
branch = (await
|
|
2886
|
+
branch = (await git3(root, "symbolic-ref", "--quiet", "HEAD")).trim();
|
|
2404
2887
|
} catch {
|
|
2405
2888
|
branch = "detached";
|
|
2406
2889
|
}
|
|
2407
2890
|
return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
|
|
2408
2891
|
}
|
|
2409
2892
|
async function readInputs(root) {
|
|
2893
|
+
const docPaths = await discoverDocPaths(root);
|
|
2410
2894
|
const [headText, docStatus, shallowPath, replacements] = await Promise.all([
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2895
|
+
git3(root, "rev-parse", "HEAD"),
|
|
2896
|
+
docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
|
|
2897
|
+
git3(root, "rev-parse", "--git-path", "shallow"),
|
|
2898
|
+
git3(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
|
|
2415
2899
|
]);
|
|
2416
2900
|
const head = headText.trim();
|
|
2417
2901
|
let shallow = null;
|
|
2418
2902
|
try {
|
|
2419
|
-
shallow = await
|
|
2903
|
+
shallow = await fs13.readFile(path20.resolve(root, shallowPath.trim()), "utf8");
|
|
2420
2904
|
} catch (error) {
|
|
2421
2905
|
if (error.code !== "ENOENT") throw error;
|
|
2422
2906
|
}
|
|
2423
2907
|
const docs = {};
|
|
2424
2908
|
const docContents = [];
|
|
2425
2909
|
const claims = [];
|
|
2426
|
-
for (const file of
|
|
2910
|
+
for (const file of docPaths) {
|
|
2427
2911
|
const text2 = await content(root, file);
|
|
2428
2912
|
docs[file] = text2 === null ? null : hash(text2);
|
|
2429
2913
|
docContents.push([file, text2]);
|
|
2430
2914
|
for (const claim of text2 ? extractClaims(text2).paths : []) {
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
const combinedDocs = docContents.map(([, text2]) => text2 ?? "").join("\n");
|
|
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 {
|
|
2915
|
+
const scope = pathClaimScope(file, claim);
|
|
2916
|
+
for (const candidate of scope?.candidates ?? []) {
|
|
2917
|
+
if (optionalMasonPath(candidate)) continue;
|
|
2918
|
+
claims.push([candidate, await pathExists(root, candidate), await pathExists(root, path20.posix.dirname(candidate))]);
|
|
2919
|
+
}
|
|
2446
2920
|
}
|
|
2447
2921
|
}
|
|
2448
|
-
const
|
|
2922
|
+
const parsedDocs = docContents.map(([file, text2]) => ({ path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") }));
|
|
2923
|
+
const combinedDocs = moduleDocumentation(parsedDocs);
|
|
2924
|
+
const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
|
|
2449
2925
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
2450
|
-
const decisionPresence = await
|
|
2926
|
+
const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
2451
2927
|
if (error.code === "ENOENT") return "absent";
|
|
2452
2928
|
throw error;
|
|
2453
2929
|
});
|
|
2454
|
-
const [modules, counts,
|
|
2930
|
+
const [modules, counts, commands, decisionFiles] = await Promise.all([
|
|
2455
2931
|
combinedDocs ? moduleCandidates(root, combinedDocs) : [],
|
|
2456
|
-
Promise.all(countClaims.map((claim) =>
|
|
2457
|
-
|
|
2458
|
-
|
|
2932
|
+
Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
|
|
2933
|
+
commandInputs(root, parsedDocs),
|
|
2934
|
+
fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
|
|
2459
2935
|
]);
|
|
2460
|
-
const packages = [["package.json", rootPackage], ...await Promise.all(manifests.sort().map(async (file) => [file, await content(root, file)]))];
|
|
2461
2936
|
const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
|
|
2462
2937
|
await storePath(root, file);
|
|
2463
2938
|
return [file, await content(root, file)];
|
|
2464
2939
|
}));
|
|
2465
2940
|
const [status, index2] = decisions.length ? await Promise.all([
|
|
2466
|
-
|
|
2467
|
-
|
|
2941
|
+
git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
2942
|
+
git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
2468
2943
|
]) : ["", ""];
|
|
2469
|
-
const common = [
|
|
2944
|
+
const common = [4, engineVersion, head, shallow, replacements, docContents];
|
|
2470
2945
|
const keys = {
|
|
2471
2946
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
2472
2947
|
"new-module": hash([common, modules]),
|
|
2473
2948
|
"stale-count": hash([common, counts]),
|
|
2474
|
-
"dead-command": hash([common,
|
|
2949
|
+
"dead-command": hash([common, commands]),
|
|
2475
2950
|
"deps-changed": hash([common, docStatus]),
|
|
2476
2951
|
"decision-anchor-drift": hash([common, decisionPresence, decisions, status, index2])
|
|
2477
2952
|
};
|
|
2478
|
-
return { fingerprint: hash(keys), head, docs, keys };
|
|
2953
|
+
return { fingerprint: hash([keys, await advisoryReviewInventory(root)]), head, docs, keys };
|
|
2479
2954
|
}
|
|
2480
2955
|
function checkCache(raw, inputs) {
|
|
2481
2956
|
let entries = {};
|
|
@@ -2502,11 +2977,12 @@ function checkCache(raw, inputs) {
|
|
|
2502
2977
|
return { version: 1, entries: canonical, digest: hash(canonical) };
|
|
2503
2978
|
} };
|
|
2504
2979
|
}
|
|
2505
|
-
var exec7, engineVersion, hash, content,
|
|
2980
|
+
var exec7, engineVersion, hash, content, cacheSchema;
|
|
2506
2981
|
var init_evidence = __esm({
|
|
2507
2982
|
"src/automation/evidence.ts"() {
|
|
2508
2983
|
"use strict";
|
|
2509
2984
|
init_docs();
|
|
2985
|
+
init_scope();
|
|
2510
2986
|
init_claims();
|
|
2511
2987
|
init_repair();
|
|
2512
2988
|
init_checks();
|
|
@@ -2515,20 +2991,20 @@ var init_evidence = __esm({
|
|
|
2515
2991
|
init_stale_count();
|
|
2516
2992
|
init_dead_command();
|
|
2517
2993
|
init_storage();
|
|
2518
|
-
|
|
2519
|
-
|
|
2994
|
+
init_advisory_review();
|
|
2995
|
+
exec7 = promisify9(execFile9);
|
|
2996
|
+
engineVersion = true ? "0.17.0" : "development";
|
|
2520
2997
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2521
2998
|
content = readAuditInput;
|
|
2522
|
-
|
|
2523
|
-
cacheSchema = z6.object({ version: z6.literal(1), entries: z6.record(z6.object({ key: z6.string(), result: checkResultSchema })), digest: z6.string() });
|
|
2999
|
+
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
2524
3000
|
}
|
|
2525
3001
|
});
|
|
2526
3002
|
|
|
2527
3003
|
// src/automation/execution.ts
|
|
2528
3004
|
import os2 from "os";
|
|
2529
|
-
import
|
|
3005
|
+
import fs14 from "fs/promises";
|
|
2530
3006
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2531
|
-
import { z as
|
|
3007
|
+
import { z as z9 } from "zod";
|
|
2532
3008
|
function parseExecution(raw) {
|
|
2533
3009
|
try {
|
|
2534
3010
|
return executionSchema.parse(raw);
|
|
@@ -2546,7 +3022,7 @@ function automationFailure(error) {
|
|
|
2546
3022
|
codes.add(String(cause.code));
|
|
2547
3023
|
cause = cause.cause;
|
|
2548
3024
|
}
|
|
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
|
|
3025
|
+
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
3026
|
return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
|
|
2551
3027
|
}
|
|
2552
3028
|
function failureMessage(error) {
|
|
@@ -2654,7 +3130,7 @@ async function recordExecution(root, directory, event, run) {
|
|
|
2654
3130
|
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
2655
3131
|
}
|
|
2656
3132
|
} finally {
|
|
2657
|
-
await
|
|
3133
|
+
await fs14.rm(await storePath(root, owner), { force: true }).catch(() => {
|
|
2658
3134
|
});
|
|
2659
3135
|
}
|
|
2660
3136
|
}
|
|
@@ -2675,33 +3151,33 @@ var init_execution = __esm({
|
|
|
2675
3151
|
init_store();
|
|
2676
3152
|
init_evidence();
|
|
2677
3153
|
init_storage();
|
|
2678
|
-
failureSchema =
|
|
2679
|
-
code:
|
|
2680
|
-
message:
|
|
2681
|
-
retryable:
|
|
2682
|
-
receiptRecorded:
|
|
3154
|
+
failureSchema = z9.object({
|
|
3155
|
+
code: z9.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
|
|
3156
|
+
message: z9.string(),
|
|
3157
|
+
retryable: z9.boolean(),
|
|
3158
|
+
receiptRecorded: z9.boolean()
|
|
2683
3159
|
});
|
|
2684
|
-
attemptSchema =
|
|
2685
|
-
id:
|
|
2686
|
-
lease:
|
|
2687
|
-
event:
|
|
2688
|
-
startedAt:
|
|
2689
|
-
finishedAt:
|
|
2690
|
-
durationMs:
|
|
2691
|
-
pid:
|
|
2692
|
-
host:
|
|
2693
|
-
status:
|
|
2694
|
-
verificationStatus:
|
|
2695
|
-
reportPath:
|
|
3160
|
+
attemptSchema = z9.object({
|
|
3161
|
+
id: z9.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
3162
|
+
lease: z9.literal(true).optional(),
|
|
3163
|
+
event: z9.string(),
|
|
3164
|
+
startedAt: z9.string(),
|
|
3165
|
+
finishedAt: z9.string().optional(),
|
|
3166
|
+
durationMs: z9.number().nonnegative().optional(),
|
|
3167
|
+
pid: z9.number().int().positive(),
|
|
3168
|
+
host: z9.string(),
|
|
3169
|
+
status: z9.enum(["running", "completed", "failed", "unknown"]),
|
|
3170
|
+
verificationStatus: z9.string().optional(),
|
|
3171
|
+
reportPath: z9.string().optional(),
|
|
2696
3172
|
failure: failureSchema.optional()
|
|
2697
3173
|
});
|
|
2698
|
-
executionSchema =
|
|
2699
|
-
notificationsSchema =
|
|
2700
|
-
signature:
|
|
3174
|
+
executionSchema = z9.object({ version: z9.literal(1), attempts: z9.array(attemptSchema).max(160), discardedAttempts: z9.number().int().nonnegative().default(0) });
|
|
3175
|
+
notificationsSchema = z9.object({ version: z9.literal(1), sessions: z9.record(z9.object({
|
|
3176
|
+
signature: z9.string(),
|
|
2701
3177
|
code: failureSchema.shape.code,
|
|
2702
|
-
message:
|
|
2703
|
-
count:
|
|
2704
|
-
lastAt:
|
|
3178
|
+
message: z9.string(),
|
|
3179
|
+
count: z9.number().int().positive(),
|
|
3180
|
+
lastAt: z9.string()
|
|
2705
3181
|
})) });
|
|
2706
3182
|
leasePath = (directory, id) => `${directory}/executions/${id}.json`;
|
|
2707
3183
|
}
|
|
@@ -2709,7 +3185,7 @@ var init_execution = __esm({
|
|
|
2709
3185
|
|
|
2710
3186
|
// src/automation/runtime.ts
|
|
2711
3187
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2712
|
-
import { z as
|
|
3188
|
+
import { z as z10 } from "zod";
|
|
2713
3189
|
async function observeReadOnlyTool(dir, event) {
|
|
2714
3190
|
const ws = await workspace(dir);
|
|
2715
3191
|
await withLock(ws.root, ws.directory, async () => {
|
|
@@ -2787,7 +3263,7 @@ async function computeAnalysis(ws, state, inputs, event) {
|
|
|
2787
3263
|
baselinePaths: [],
|
|
2788
3264
|
reportPath: "",
|
|
2789
3265
|
findings: [],
|
|
2790
|
-
diagnostics: ["No
|
|
3266
|
+
diagnostics: ["No README.md or agent instruction files were discovered. Documentation capture is unavailable; other Mason tools remain usable."],
|
|
2791
3267
|
checks: { ran: [], reused: [], skipped: [] },
|
|
2792
3268
|
counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
|
|
2793
3269
|
capture: "unknown",
|
|
@@ -2798,7 +3274,7 @@ async function computeAnalysis(ws, state, inputs, event) {
|
|
|
2798
3274
|
const previous = await readStoreJson(ws.root, state.latest);
|
|
2799
3275
|
if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
|
|
2800
3276
|
try {
|
|
2801
|
-
await
|
|
3277
|
+
await git3(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
|
|
2802
3278
|
} catch {
|
|
2803
3279
|
throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
|
|
2804
3280
|
}
|
|
@@ -3003,35 +3479,35 @@ var init_runtime = __esm({
|
|
|
3003
3479
|
init_storage();
|
|
3004
3480
|
init_evidence();
|
|
3005
3481
|
init_store();
|
|
3006
|
-
SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved
|
|
3482
|
+
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
3483
|
priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
|
|
3008
3484
|
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:
|
|
3485
|
+
analysisSchema = z10.object({
|
|
3486
|
+
id: z10.string(),
|
|
3487
|
+
fingerprint: z10.string(),
|
|
3488
|
+
completedAt: z10.number(),
|
|
3489
|
+
baselineDigest: z10.string(),
|
|
3490
|
+
report: z10.object({
|
|
3491
|
+
version: z10.literal(1),
|
|
3492
|
+
status: z10.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
|
|
3493
|
+
root: z10.string(),
|
|
3494
|
+
branch: z10.string(),
|
|
3495
|
+
head: z10.string(),
|
|
3496
|
+
baselinePaths: z10.array(z10.string()),
|
|
3497
|
+
reportPath: z10.string(),
|
|
3498
|
+
findings: z10.array(repairFindingSchema),
|
|
3499
|
+
diagnostics: z10.array(z10.string()),
|
|
3500
|
+
checks: z10.object({
|
|
3501
|
+
ran: z10.array(z10.string()),
|
|
3502
|
+
reused: z10.array(z10.string()),
|
|
3503
|
+
skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() }))
|
|
3028
3504
|
}),
|
|
3029
|
-
counts:
|
|
3030
|
-
capture:
|
|
3031
|
-
scope:
|
|
3505
|
+
counts: z10.object({ resolved: z10.number(), unresolved: z10.number(), "review-required": z10.number(), unverified: z10.number() }),
|
|
3506
|
+
capture: z10.enum(["observed", "unknown"]),
|
|
3507
|
+
scope: z10.string()
|
|
3032
3508
|
})
|
|
3033
3509
|
});
|
|
3034
|
-
sharedSchema =
|
|
3510
|
+
sharedSchema = z10.object({ analysis: analysisSchema, digest: z10.string() });
|
|
3035
3511
|
baselineDigest = async (ws, state) => hash(await Promise.all(
|
|
3036
3512
|
state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
|
|
3037
3513
|
));
|
|
@@ -3039,7 +3515,7 @@ var init_runtime = __esm({
|
|
|
3039
3515
|
});
|
|
3040
3516
|
|
|
3041
3517
|
// src/setup/model.ts
|
|
3042
|
-
import { z as
|
|
3518
|
+
import { z as z11 } from "zod";
|
|
3043
3519
|
async function loadSetup(root) {
|
|
3044
3520
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
3045
3521
|
if (raw === null) return null;
|
|
@@ -3057,27 +3533,27 @@ var init_model = __esm({
|
|
|
3057
3533
|
"src/setup/model.ts"() {
|
|
3058
3534
|
"use strict";
|
|
3059
3535
|
init_storage();
|
|
3060
|
-
setupHostSchema =
|
|
3061
|
-
configuredVersion:
|
|
3062
|
-
revision:
|
|
3063
|
-
fingerprint:
|
|
3064
|
-
mcpFingerprint:
|
|
3065
|
-
instructions:
|
|
3536
|
+
setupHostSchema = z11.object({
|
|
3537
|
+
configuredVersion: z11.string(),
|
|
3538
|
+
revision: z11.string().uuid(),
|
|
3539
|
+
fingerprint: z11.string(),
|
|
3540
|
+
mcpFingerprint: z11.string(),
|
|
3541
|
+
instructions: z11.array(z11.string())
|
|
3066
3542
|
});
|
|
3067
|
-
setupSchema =
|
|
3543
|
+
setupSchema = z11.object({ version: z11.literal(2), hosts: z11.object({
|
|
3068
3544
|
codex: setupHostSchema.optional(),
|
|
3069
3545
|
claude: setupHostSchema.optional()
|
|
3070
3546
|
}) });
|
|
3071
3547
|
SETUP_PATH = ".mason/local/setup.json";
|
|
3072
|
-
receiptSchema =
|
|
3073
|
-
version:
|
|
3074
|
-
host:
|
|
3075
|
-
status:
|
|
3076
|
-
initialReportPath:
|
|
3077
|
-
initialBaselinePaths:
|
|
3078
|
-
root:
|
|
3079
|
-
revision:
|
|
3080
|
-
configuredAt:
|
|
3548
|
+
receiptSchema = z11.object({
|
|
3549
|
+
version: z11.literal(1),
|
|
3550
|
+
host: z11.enum(["codex", "claude"]),
|
|
3551
|
+
status: z11.enum(["installing", "configured"]),
|
|
3552
|
+
initialReportPath: z11.string(),
|
|
3553
|
+
initialBaselinePaths: z11.array(z11.string()),
|
|
3554
|
+
root: z11.string(),
|
|
3555
|
+
revision: z11.string().uuid(),
|
|
3556
|
+
configuredAt: z11.string().optional()
|
|
3081
3557
|
});
|
|
3082
3558
|
}
|
|
3083
3559
|
});
|
|
@@ -3092,16 +3568,16 @@ __export(launcher_exports, {
|
|
|
3092
3568
|
mcpCommand: () => mcpCommand,
|
|
3093
3569
|
prepareLaunch: () => prepareLaunch
|
|
3094
3570
|
});
|
|
3095
|
-
import { execFile as
|
|
3096
|
-
import { promisify as
|
|
3097
|
-
import
|
|
3571
|
+
import { execFile as execFile10 } from "child_process";
|
|
3572
|
+
import { promisify as promisify10 } from "util";
|
|
3573
|
+
import fs15 from "fs/promises";
|
|
3098
3574
|
function globalCommand(args2, platform = process.platform) {
|
|
3099
3575
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
|
|
3100
3576
|
}
|
|
3101
3577
|
async function installedCommand() {
|
|
3102
3578
|
const invocation = globalCommand(["internal-integration-version"]);
|
|
3103
3579
|
try {
|
|
3104
|
-
const { stdout } = await
|
|
3580
|
+
const { stdout } = await promisify10(execFile10)(
|
|
3105
3581
|
invocation.command,
|
|
3106
3582
|
invocation.args,
|
|
3107
3583
|
{ timeout: 5e3, maxBuffer: 8192, windowsHide: true }
|
|
@@ -3114,7 +3590,7 @@ async function installedCommand() {
|
|
|
3114
3590
|
}
|
|
3115
3591
|
}
|
|
3116
3592
|
async function prepareLaunch(host, options = {}) {
|
|
3117
|
-
const root = await
|
|
3593
|
+
const root = await fs15.realpath((await git3(process.cwd(), "rev-parse", "--show-toplevel")).trim());
|
|
3118
3594
|
const entry = (await loadSetup(root))?.hosts[host];
|
|
3119
3595
|
if (!entry) {
|
|
3120
3596
|
if (options.allowInactive) return false;
|
|
@@ -3131,7 +3607,7 @@ var init_launcher = __esm({
|
|
|
3131
3607
|
"use strict";
|
|
3132
3608
|
init_evidence();
|
|
3133
3609
|
init_model();
|
|
3134
|
-
executingVersion = () => true ? "0.
|
|
3610
|
+
executingVersion = () => true ? "0.17.0" : "development";
|
|
3135
3611
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3136
3612
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3137
3613
|
}
|
|
@@ -3144,8 +3620,8 @@ __export(observations_exports, {
|
|
|
3144
3620
|
observeActivation: () => observeActivation,
|
|
3145
3621
|
readObservation: () => readObservation
|
|
3146
3622
|
});
|
|
3147
|
-
import
|
|
3148
|
-
import { z as
|
|
3623
|
+
import fs16 from "fs/promises";
|
|
3624
|
+
import { z as z12 } from "zod";
|
|
3149
3625
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
3150
3626
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
3151
3627
|
}
|
|
@@ -3161,8 +3637,8 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
3161
3637
|
if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
|
|
3162
3638
|
try {
|
|
3163
3639
|
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
|
|
3640
|
+
const ws = capturedDirectory ? { root: await fs16.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
3641
|
+
if (ws.root !== await fs16.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
3166
3642
|
const setup = await loadSetup(ws.root);
|
|
3167
3643
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
3168
3644
|
const directory = ws.directory + "/activation";
|
|
@@ -3210,26 +3686,26 @@ var init_observations = __esm({
|
|
|
3210
3686
|
init_storage();
|
|
3211
3687
|
init_model();
|
|
3212
3688
|
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:
|
|
3689
|
+
observationSchema = z12.object({
|
|
3690
|
+
version: z12.literal(1),
|
|
3691
|
+
masonVersion: z12.string(),
|
|
3692
|
+
root: z12.string(),
|
|
3693
|
+
revision: z12.string(),
|
|
3694
|
+
host: z12.enum(["codex", "claude"]),
|
|
3695
|
+
contextCalls: z12.number().int().nonnegative(),
|
|
3696
|
+
lastContextAt: z12.string().optional(),
|
|
3697
|
+
sessions: z12.record(z12.object({
|
|
3698
|
+
events: z12.array(z12.enum(events)),
|
|
3699
|
+
at: z12.string(),
|
|
3700
|
+
verificationStatus: z12.string().optional(),
|
|
3701
|
+
reportPath: z12.string().optional()
|
|
3226
3702
|
}))
|
|
3227
3703
|
});
|
|
3228
3704
|
}
|
|
3229
3705
|
});
|
|
3230
3706
|
|
|
3231
3707
|
// src/automation/adapters.ts
|
|
3232
|
-
import { z as
|
|
3708
|
+
import { z as z13 } from "zod";
|
|
3233
3709
|
function normalizeHook(host, raw) {
|
|
3234
3710
|
hostSchema.parse(host);
|
|
3235
3711
|
const input = inputSchema.parse(raw);
|
|
@@ -3303,15 +3779,15 @@ var init_adapters = __esm({
|
|
|
3303
3779
|
init_execution();
|
|
3304
3780
|
init_runtime();
|
|
3305
3781
|
init_store();
|
|
3306
|
-
inputSchema =
|
|
3307
|
-
cwd:
|
|
3308
|
-
session_id:
|
|
3309
|
-
hook_event_name:
|
|
3310
|
-
tool_name:
|
|
3311
|
-
tool_use_id:
|
|
3312
|
-
tool_input:
|
|
3313
|
-
stop_hook_active:
|
|
3314
|
-
permission_mode:
|
|
3782
|
+
inputSchema = z13.object({
|
|
3783
|
+
cwd: z13.string().min(1),
|
|
3784
|
+
session_id: z13.string().min(1).max(500),
|
|
3785
|
+
hook_event_name: z13.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
|
|
3786
|
+
tool_name: z13.string().optional(),
|
|
3787
|
+
tool_use_id: z13.string().max(500).optional(),
|
|
3788
|
+
tool_input: z13.unknown().optional(),
|
|
3789
|
+
stop_hook_active: z13.boolean().optional(),
|
|
3790
|
+
permission_mode: z13.string().optional()
|
|
3315
3791
|
});
|
|
3316
3792
|
lifecycle = {
|
|
3317
3793
|
SessionStart: "session_start",
|
|
@@ -3331,8 +3807,8 @@ __export(files_exports, {
|
|
|
3331
3807
|
managedBlock: () => managedBlock,
|
|
3332
3808
|
readText: () => readText
|
|
3333
3809
|
});
|
|
3334
|
-
import
|
|
3335
|
-
import
|
|
3810
|
+
import fs17 from "fs/promises";
|
|
3811
|
+
import path21 from "path";
|
|
3336
3812
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
3337
3813
|
async function readText(root, file) {
|
|
3338
3814
|
try {
|
|
@@ -3361,14 +3837,14 @@ async function applyEdit(root, edit) {
|
|
|
3361
3837
|
if (edit.after === null) {
|
|
3362
3838
|
const file2 = await storePath(root, edit.path);
|
|
3363
3839
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
3364
|
-
await
|
|
3840
|
+
await fs17.unlink(file2);
|
|
3365
3841
|
return true;
|
|
3366
3842
|
}
|
|
3367
3843
|
const file = await storePath(root, edit.path, true);
|
|
3368
|
-
const temporary =
|
|
3844
|
+
const temporary = path21.join(path21.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
3369
3845
|
try {
|
|
3370
|
-
const mode = await
|
|
3371
|
-
const handle = await
|
|
3846
|
+
const mode = await fs17.stat(file).then((s) => s.mode & 511, () => 384);
|
|
3847
|
+
const handle = await fs17.open(temporary, "wx", mode);
|
|
3372
3848
|
try {
|
|
3373
3849
|
await handle.writeFile(edit.after, "utf8");
|
|
3374
3850
|
await handle.sync();
|
|
@@ -3376,10 +3852,10 @@ async function applyEdit(root, edit) {
|
|
|
3376
3852
|
await handle.close();
|
|
3377
3853
|
}
|
|
3378
3854
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
3379
|
-
await
|
|
3855
|
+
await fs17.rename(temporary, file);
|
|
3380
3856
|
return true;
|
|
3381
3857
|
} finally {
|
|
3382
|
-
await
|
|
3858
|
+
await fs17.rm(temporary, { force: true });
|
|
3383
3859
|
}
|
|
3384
3860
|
}
|
|
3385
3861
|
var init_files2 = __esm({
|
|
@@ -3400,7 +3876,7 @@ __export(ownership_exports, {
|
|
|
3400
3876
|
ownershipEdit: () => ownershipEdit,
|
|
3401
3877
|
ownershipSchema: () => ownershipSchema
|
|
3402
3878
|
});
|
|
3403
|
-
import { z as
|
|
3879
|
+
import { z as z14 } from "zod";
|
|
3404
3880
|
import { parse } from "smol-toml";
|
|
3405
3881
|
function blockSpan(text2, start, end) {
|
|
3406
3882
|
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
@@ -3451,30 +3927,30 @@ var init_ownership = __esm({
|
|
|
3451
3927
|
["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
|
|
3452
3928
|
["# mason:mcp:start", "# mason:mcp:end"]
|
|
3453
3929
|
];
|
|
3454
|
-
fileSchema =
|
|
3455
|
-
created:
|
|
3456
|
-
initialHash:
|
|
3457
|
-
blocks:
|
|
3458
|
-
mcpHash:
|
|
3459
|
-
hookCommands:
|
|
3460
|
-
hooks:
|
|
3930
|
+
fileSchema = z14.object({
|
|
3931
|
+
created: z14.boolean(),
|
|
3932
|
+
initialHash: z14.string(),
|
|
3933
|
+
blocks: z14.record(z14.string()),
|
|
3934
|
+
mcpHash: z14.string().optional(),
|
|
3935
|
+
hookCommands: z14.array(z14.string()).optional(),
|
|
3936
|
+
hooks: z14.record(z14.array(z14.string())).optional()
|
|
3461
3937
|
});
|
|
3462
|
-
ownershipSchema =
|
|
3938
|
+
ownershipSchema = z14.object({ version: z14.literal(1), files: z14.record(fileSchema) });
|
|
3463
3939
|
}
|
|
3464
3940
|
});
|
|
3465
3941
|
|
|
3466
3942
|
// src/mcp/init.ts
|
|
3467
|
-
import { z as
|
|
3943
|
+
import { z as z15 } from "zod";
|
|
3468
3944
|
var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
|
|
3469
3945
|
var init_init = __esm({
|
|
3470
3946
|
"src/mcp/init.ts"() {
|
|
3471
3947
|
"use strict";
|
|
3472
3948
|
init_storage();
|
|
3473
3949
|
init_files2();
|
|
3474
|
-
markerSchema =
|
|
3475
|
-
version:
|
|
3476
|
-
initializedAt:
|
|
3477
|
-
features:
|
|
3950
|
+
markerSchema = z15.object({
|
|
3951
|
+
version: z15.literal(1),
|
|
3952
|
+
initializedAt: z15.string(),
|
|
3953
|
+
features: z15.object({ confluence: z15.boolean().optional() }).passthrough().optional()
|
|
3478
3954
|
}).passthrough();
|
|
3479
3955
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
3480
3956
|
## Mason project knowledge
|
|
@@ -3484,12 +3960,14 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
3484
3960
|
- 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
3961
|
- Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
|
|
3486
3962
|
- 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.
|
|
3963
|
+
- 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
3964
|
- 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
3965
|
- 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
3966
|
- 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
3967
|
- \`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
3968
|
|
|
3492
3969
|
- 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.
|
|
3970
|
+
- 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
3971
|
|
|
3494
3972
|
- 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
3973
|
|
|
@@ -3505,7 +3983,7 @@ ${CLAUDE_MD_SECTION}`;
|
|
|
3505
3983
|
|
|
3506
3984
|
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
3985
|
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,
|
|
3986
|
+
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
3987
|
|
|
3510
3988
|
${ASSISTANT_SETUP}
|
|
3511
3989
|
|
|
@@ -3551,8 +4029,8 @@ var git_exports = {};
|
|
|
3551
4029
|
__export(git_exports, {
|
|
3552
4030
|
isGitRepo: () => isGitRepo
|
|
3553
4031
|
});
|
|
3554
|
-
import { execFile as
|
|
3555
|
-
import { promisify as
|
|
4032
|
+
import { execFile as execFile11 } from "child_process";
|
|
4033
|
+
import { promisify as promisify11 } from "util";
|
|
3556
4034
|
async function isGitRepo(dir) {
|
|
3557
4035
|
try {
|
|
3558
4036
|
await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
@@ -3565,7 +4043,7 @@ var exec8;
|
|
|
3565
4043
|
var init_git2 = __esm({
|
|
3566
4044
|
"src/utils/git.ts"() {
|
|
3567
4045
|
"use strict";
|
|
3568
|
-
exec8 =
|
|
4046
|
+
exec8 = promisify11(execFile11);
|
|
3569
4047
|
}
|
|
3570
4048
|
});
|
|
3571
4049
|
|
|
@@ -3581,7 +4059,7 @@ __export(config_exports, {
|
|
|
3581
4059
|
});
|
|
3582
4060
|
import { isDeepStrictEqual } from "util";
|
|
3583
4061
|
import { parse as parse2, stringify } from "smol-toml";
|
|
3584
|
-
import { z as
|
|
4062
|
+
import { z as z16 } from "zod";
|
|
3585
4063
|
async function instructionEdits(root, host) {
|
|
3586
4064
|
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
3587
4065
|
const existing = found.find((f) => f.text !== null);
|
|
@@ -3650,11 +4128,11 @@ async function hookEdits(root, host) {
|
|
|
3650
4128
|
}
|
|
3651
4129
|
async function ancillaryEdits(root) {
|
|
3652
4130
|
const ignore = await readText(root, ".gitignore");
|
|
3653
|
-
const { git:
|
|
4131
|
+
const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
3654
4132
|
const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
|
|
3655
4133
|
let parentIgnored = false;
|
|
3656
4134
|
try {
|
|
3657
|
-
if (await isGitRepo2(root)) parentIgnored = !!(await
|
|
4135
|
+
if (await isGitRepo2(root)) parentIgnored = !!(await git4(root, "check-ignore", "--no-index", ".mason")).trim();
|
|
3658
4136
|
} catch (error) {
|
|
3659
4137
|
if (error.code !== 1) throw error;
|
|
3660
4138
|
}
|
|
@@ -3663,6 +4141,8 @@ async function ancillaryEdits(root) {
|
|
|
3663
4141
|
...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
|
|
3664
4142
|
"!/.mason/decisions/",
|
|
3665
4143
|
"!/.mason/decisions/**",
|
|
4144
|
+
"!/.mason/reviews/",
|
|
4145
|
+
"!/.mason/reviews/**",
|
|
3666
4146
|
"!/.mason/config.json",
|
|
3667
4147
|
"!/.mason/snapshot.json",
|
|
3668
4148
|
"/.mason/local/",
|
|
@@ -3697,7 +4177,7 @@ var init_config = __esm({
|
|
|
3697
4177
|
init_install();
|
|
3698
4178
|
init_files2();
|
|
3699
4179
|
init_launcher();
|
|
3700
|
-
object =
|
|
4180
|
+
object = z16.record(z16.unknown());
|
|
3701
4181
|
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
3702
4182
|
TOML_START = "# mason:mcp:start";
|
|
3703
4183
|
TOML_END = "# mason:mcp:end";
|
|
@@ -3715,11 +4195,11 @@ __export(install_exports, {
|
|
|
3715
4195
|
installAutomation: () => installAutomation,
|
|
3716
4196
|
installedAutomation: () => installedAutomation,
|
|
3717
4197
|
planAutomationInstall: () => planAutomationInstall,
|
|
3718
|
-
recordSchema: () =>
|
|
4198
|
+
recordSchema: () => recordSchema2
|
|
3719
4199
|
});
|
|
3720
|
-
import { z as
|
|
4200
|
+
import { z as z17 } from "zod";
|
|
3721
4201
|
async function installationRecord(root) {
|
|
3722
|
-
return
|
|
4202
|
+
return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
3723
4203
|
}
|
|
3724
4204
|
async function installAutomation(dir, host, command2) {
|
|
3725
4205
|
const ws = await workspace(dir);
|
|
@@ -3788,7 +4268,7 @@ async function installedAutomation(dir) {
|
|
|
3788
4268
|
}
|
|
3789
4269
|
return result;
|
|
3790
4270
|
}
|
|
3791
|
-
var groupSchema, automationConfigSchema,
|
|
4271
|
+
var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath;
|
|
3792
4272
|
var init_install = __esm({
|
|
3793
4273
|
"src/automation/install.ts"() {
|
|
3794
4274
|
"use strict";
|
|
@@ -3798,9 +4278,9 @@ var init_install = __esm({
|
|
|
3798
4278
|
init_store();
|
|
3799
4279
|
init_files2();
|
|
3800
4280
|
init_ownership();
|
|
3801
|
-
groupSchema =
|
|
3802
|
-
automationConfigSchema =
|
|
3803
|
-
|
|
4281
|
+
groupSchema = z17.object({ hooks: z17.array(z17.object({ type: z17.string(), command: z17.string().optional() }).passthrough()) }).passthrough();
|
|
4282
|
+
automationConfigSchema = z17.object({ hooks: z17.record(z17.array(groupSchema)).optional() }).passthrough();
|
|
4283
|
+
recordSchema2 = z17.object({ version: z17.literal(1), hosts: z17.record(z17.object({ command: z17.string() })) });
|
|
3804
4284
|
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
3805
4285
|
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
3806
4286
|
}
|
|
@@ -3814,8 +4294,8 @@ __export(teardown_exports, {
|
|
|
3814
4294
|
});
|
|
3815
4295
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
3816
4296
|
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
3817
|
-
import
|
|
3818
|
-
import { z as
|
|
4297
|
+
import fg6 from "fast-glob";
|
|
4298
|
+
import { z as z18 } from "zod";
|
|
3819
4299
|
async function planTeardown(root, selected) {
|
|
3820
4300
|
const edits = [], diagnostics = [];
|
|
3821
4301
|
const inputs = /* @__PURE__ */ new Map();
|
|
@@ -3835,7 +4315,7 @@ async function planTeardown(root, selected) {
|
|
|
3835
4315
|
const setupBefore = await read(SETUP_PATH);
|
|
3836
4316
|
const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
|
|
3837
4317
|
const automationBefore = await read(AUTOMATION_PATH);
|
|
3838
|
-
const automation = automationBefore === null ? null :
|
|
4318
|
+
const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
|
|
3839
4319
|
const commands = (host) => new Set([
|
|
3840
4320
|
automation?.hosts[host]?.command,
|
|
3841
4321
|
...ownership.files[configPath(host)]?.hookCommands ?? [],
|
|
@@ -3949,7 +4429,7 @@ async function planTeardown(root, selected) {
|
|
|
3949
4429
|
if (diagnostics.length) edits.splice(instructionEditStart);
|
|
3950
4430
|
if (!diagnostics.length) {
|
|
3951
4431
|
const cleanup = [];
|
|
3952
|
-
const receiptFiles = await
|
|
4432
|
+
const receiptFiles = await fg6(
|
|
3953
4433
|
selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
|
|
3954
4434
|
{ cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
|
|
3955
4435
|
);
|
|
@@ -3962,7 +4442,7 @@ async function planTeardown(root, selected) {
|
|
|
3962
4442
|
if (!remainingHost) await attempt(".mason/local/project.json", async () => {
|
|
3963
4443
|
const before = await read(".mason/local/project.json");
|
|
3964
4444
|
if (before === null) return;
|
|
3965
|
-
|
|
4445
|
+
z18.object({ version: z18.literal(1), initializedAt: z18.string() }).strict().parse(JSON.parse(before));
|
|
3966
4446
|
cleanup.push({ path: ".mason/local/project.json", before, after: null });
|
|
3967
4447
|
});
|
|
3968
4448
|
if (!diagnostics.length) {
|
|
@@ -4034,14 +4514,14 @@ var init_teardown = __esm({
|
|
|
4034
4514
|
init_model();
|
|
4035
4515
|
init_ownership();
|
|
4036
4516
|
hosts = ["codex", "claude"];
|
|
4037
|
-
object2 =
|
|
4517
|
+
object2 = z18.record(z18.unknown());
|
|
4038
4518
|
json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
4039
4519
|
}
|
|
4040
4520
|
});
|
|
4041
4521
|
|
|
4042
4522
|
// src/review/cochange.ts
|
|
4043
|
-
import { execFile as
|
|
4044
|
-
import { promisify as
|
|
4523
|
+
import { execFile as execFile12 } from "child_process";
|
|
4524
|
+
import { promisify as promisify12 } from "util";
|
|
4045
4525
|
async function buildMatrix(resolvedRoot) {
|
|
4046
4526
|
try {
|
|
4047
4527
|
const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
|
|
@@ -4106,7 +4586,7 @@ var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_R
|
|
|
4106
4586
|
var init_cochange = __esm({
|
|
4107
4587
|
"src/review/cochange.ts"() {
|
|
4108
4588
|
"use strict";
|
|
4109
|
-
exec9 =
|
|
4589
|
+
exec9 = promisify12(execFile12);
|
|
4110
4590
|
HISTORY_COMMITS = 1500;
|
|
4111
4591
|
MIN_FILE_COMMITS = 5;
|
|
4112
4592
|
MIN_SHARED_COMMITS = 4;
|
|
@@ -4161,7 +4641,7 @@ var init_types2 = __esm({
|
|
|
4161
4641
|
});
|
|
4162
4642
|
|
|
4163
4643
|
// src/review/evidence/vitest.ts
|
|
4164
|
-
import { z as
|
|
4644
|
+
import { z as z19 } from "zod";
|
|
4165
4645
|
function parseVitest(raw, sourceRoot) {
|
|
4166
4646
|
const report = schema.parse(raw);
|
|
4167
4647
|
const findings = [], diagnostics = [];
|
|
@@ -4204,30 +4684,30 @@ function parseVitest(raw, sourceRoot) {
|
|
|
4204
4684
|
diagnostics
|
|
4205
4685
|
};
|
|
4206
4686
|
}
|
|
4207
|
-
var
|
|
4687
|
+
var count3, schema;
|
|
4208
4688
|
var init_vitest = __esm({
|
|
4209
4689
|
"src/review/evidence/vitest.ts"() {
|
|
4210
4690
|
"use strict";
|
|
4211
4691
|
init_paths2();
|
|
4212
4692
|
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:
|
|
4693
|
+
count3 = z19.number().int().nonnegative();
|
|
4694
|
+
schema = z19.object({
|
|
4695
|
+
success: z19.boolean(),
|
|
4696
|
+
numTotalTests: count3,
|
|
4697
|
+
numPassedTests: count3,
|
|
4698
|
+
numFailedTests: count3,
|
|
4699
|
+
numPendingTests: count3,
|
|
4700
|
+
numTodoTests: count3,
|
|
4701
|
+
numFailedTestSuites: count3,
|
|
4702
|
+
testResults: z19.array(z19.object({
|
|
4703
|
+
name: z19.string().min(1),
|
|
4704
|
+
status: z19.enum(["passed", "failed"]),
|
|
4705
|
+
message: z19.string().optional(),
|
|
4706
|
+
assertionResults: z19.array(z19.object({
|
|
4707
|
+
fullName: z19.string(),
|
|
4708
|
+
status: z19.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
4709
|
+
failureMessages: z19.array(z19.string()).nullable().optional(),
|
|
4710
|
+
location: z19.object({ line: count3, column: count3 }).nullable().optional()
|
|
4231
4711
|
}))
|
|
4232
4712
|
}))
|
|
4233
4713
|
});
|
|
@@ -4235,7 +4715,7 @@ var init_vitest = __esm({
|
|
|
4235
4715
|
});
|
|
4236
4716
|
|
|
4237
4717
|
// src/review/evidence/sarif.ts
|
|
4238
|
-
import { z as
|
|
4718
|
+
import { z as z20 } from "zod";
|
|
4239
4719
|
function parseSarif(raw, sourceRoot) {
|
|
4240
4720
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
4241
4721
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -4326,40 +4806,98 @@ var init_sarif = __esm({
|
|
|
4326
4806
|
"use strict";
|
|
4327
4807
|
init_paths2();
|
|
4328
4808
|
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:
|
|
4809
|
+
index = z20.number().int().nonnegative();
|
|
4810
|
+
artifact = z20.object({ uri: z20.string().optional(), uriBaseId: z20.string().optional(), index: index.optional() });
|
|
4811
|
+
message = z20.object({ text: z20.string().optional(), markdown: z20.string().optional(), id: z20.string().optional(), arguments: z20.array(z20.string()).optional() });
|
|
4812
|
+
level = z20.enum(["error", "warning", "note", "none"]);
|
|
4813
|
+
location = z20.object({ physicalLocation: z20.object({ artifactLocation: artifact.optional(), region: z20.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
4814
|
+
schema2 = z20.object({
|
|
4815
|
+
version: z20.literal("2.1.0"),
|
|
4816
|
+
runs: z20.array(z20.object({
|
|
4817
|
+
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() }) }),
|
|
4818
|
+
invocations: z20.array(z20.object({ executionSuccessful: z20.boolean(), commandLine: z20.string().optional(), toolExecutionNotifications: z20.array(z20.object({ level: level.optional(), message })).optional() })).optional(),
|
|
4819
|
+
versionControlProvenance: z20.array(z20.object({ revisionId: z20.string().optional() })).optional(),
|
|
4820
|
+
originalUriBaseIds: z20.record(artifact).optional(),
|
|
4821
|
+
artifacts: z20.array(z20.object({ location: artifact.optional() })).optional(),
|
|
4822
|
+
results: z20.array(z20.object({
|
|
4823
|
+
ruleId: z20.string().optional(),
|
|
4344
4824
|
ruleIndex: index.optional(),
|
|
4345
4825
|
message,
|
|
4346
4826
|
level: level.optional(),
|
|
4347
|
-
kind:
|
|
4348
|
-
baselineState:
|
|
4349
|
-
suppressions:
|
|
4350
|
-
locations:
|
|
4351
|
-
relatedLocations:
|
|
4827
|
+
kind: z20.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
4828
|
+
baselineState: z20.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
4829
|
+
suppressions: z20.array(z20.object({ kind: z20.enum(["inSource", "external"]), status: z20.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
4830
|
+
locations: z20.array(location).optional(),
|
|
4831
|
+
relatedLocations: z20.array(location).optional()
|
|
4352
4832
|
})).optional()
|
|
4353
4833
|
}))
|
|
4354
4834
|
});
|
|
4355
4835
|
}
|
|
4356
4836
|
});
|
|
4357
4837
|
|
|
4838
|
+
// src/review/evidence/check.ts
|
|
4839
|
+
import { z as z21 } from "zod";
|
|
4840
|
+
function parseCheck(raw, sourceRoot) {
|
|
4841
|
+
const report = schema3.parse(raw);
|
|
4842
|
+
const diagnostics = [], findings = [];
|
|
4843
|
+
const counts = { total: report.results.length, passed: 0, failed: 0, skipped: 0 };
|
|
4844
|
+
for (const [index2, result] of report.results.entries()) {
|
|
4845
|
+
counts[result.status]++;
|
|
4846
|
+
const locations = result.locations.flatMap((location2) => {
|
|
4847
|
+
const file = evidencePath(location2.file, sourceRoot);
|
|
4848
|
+
if (!file) {
|
|
4849
|
+
diagnostics.push(`Check location is outside the declared checkout or invalid: ${location2.file}`);
|
|
4850
|
+
return [];
|
|
4851
|
+
}
|
|
4852
|
+
return [{ ...location2, file }];
|
|
4853
|
+
});
|
|
4854
|
+
if (result.status === "failed") findings.push({
|
|
4855
|
+
id: String(index2),
|
|
4856
|
+
...messagePreview([result.name, result.message].filter(Boolean).join("\n")),
|
|
4857
|
+
severity: "error",
|
|
4858
|
+
state: "active",
|
|
4859
|
+
locations
|
|
4860
|
+
});
|
|
4861
|
+
}
|
|
4862
|
+
if (!counts.total) diagnostics.push("No check results were recorded; an empty report is not passing evidence.");
|
|
4863
|
+
if (counts.skipped) diagnostics.push(`${counts.skipped} checks were skipped; verification is incomplete.`);
|
|
4864
|
+
return {
|
|
4865
|
+
outcome: counts.failed ? "failed" : !counts.total ? "unavailable" : !counts.passed ? "skipped" : "passed",
|
|
4866
|
+
counts,
|
|
4867
|
+
findings,
|
|
4868
|
+
incomplete: diagnostics.length > 0,
|
|
4869
|
+
diagnostics,
|
|
4870
|
+
...report.commit ? { reportedCommits: [report.commit] } : {}
|
|
4871
|
+
};
|
|
4872
|
+
}
|
|
4873
|
+
var schema3;
|
|
4874
|
+
var init_check = __esm({
|
|
4875
|
+
"src/review/evidence/check.ts"() {
|
|
4876
|
+
"use strict";
|
|
4877
|
+
init_paths2();
|
|
4878
|
+
init_types2();
|
|
4879
|
+
schema3 = z21.object({
|
|
4880
|
+
version: z21.literal(1),
|
|
4881
|
+
commit: z21.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
|
|
4882
|
+
results: z21.array(z21.object({
|
|
4883
|
+
name: z21.string().min(1).max(1e3),
|
|
4884
|
+
status: z21.enum(["passed", "failed", "skipped"]),
|
|
4885
|
+
message: z21.string().max(1e5).optional(),
|
|
4886
|
+
locations: z21.array(z21.object({
|
|
4887
|
+
file: z21.string().min(1).max(4e3),
|
|
4888
|
+
line: z21.number().int().positive().optional(),
|
|
4889
|
+
column: z21.number().int().positive().optional()
|
|
4890
|
+
})).max(100).default([])
|
|
4891
|
+
})).max(1e5)
|
|
4892
|
+
});
|
|
4893
|
+
}
|
|
4894
|
+
});
|
|
4895
|
+
|
|
4358
4896
|
// src/review/evidence.ts
|
|
4359
|
-
import
|
|
4360
|
-
import { z as
|
|
4897
|
+
import path22 from "path";
|
|
4898
|
+
import { z as z22 } from "zod";
|
|
4361
4899
|
function relativeArtifact(root, file) {
|
|
4362
|
-
const relative =
|
|
4900
|
+
const relative = path22.isAbsolute(file) ? path22.relative(root, file) : file;
|
|
4363
4901
|
const normalized = normalizeRepoPath(relative);
|
|
4364
4902
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
4365
4903
|
return normalized;
|
|
@@ -4398,7 +4936,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4398
4936
|
let manifest, raw;
|
|
4399
4937
|
try {
|
|
4400
4938
|
manifest = relativeArtifact(root, supplied);
|
|
4401
|
-
raw =
|
|
4939
|
+
raw = z22.object({ version: z22.literal(1), checks: z22.array(z22.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
4402
4940
|
} catch (error) {
|
|
4403
4941
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
4404
4942
|
continue;
|
|
@@ -4437,7 +4975,8 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4437
4975
|
counts: {},
|
|
4438
4976
|
incomplete: false,
|
|
4439
4977
|
diagnostics: [],
|
|
4440
|
-
truncated: false
|
|
4978
|
+
truncated: false,
|
|
4979
|
+
...check.environment ? { environment: check.environment } : {}
|
|
4441
4980
|
};
|
|
4442
4981
|
output.checks.push(result);
|
|
4443
4982
|
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 +4989,13 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4450
4989
|
}
|
|
4451
4990
|
try {
|
|
4452
4991
|
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.");
|
|
4992
|
+
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
4993
|
const file = relativeArtifact(root, check.report.path);
|
|
4455
4994
|
const report = await readStoreJson(root, file);
|
|
4456
4995
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
4457
4996
|
const sourceRoot = check.sourceRoot ?? root;
|
|
4458
|
-
if (!
|
|
4459
|
-
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
4997
|
+
if (!path22.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
|
|
4998
|
+
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
4460
4999
|
result.outcome = parsed.outcome;
|
|
4461
5000
|
result.counts = parsed.counts;
|
|
4462
5001
|
result.incomplete = parsed.incomplete;
|
|
@@ -4476,7 +5015,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4476
5015
|
result.outcome = "failed";
|
|
4477
5016
|
result.diagnostics.push(`Check command exited ${check.exitCode} despite a report with no active failures.`);
|
|
4478
5017
|
}
|
|
4479
|
-
if (check.
|
|
5018
|
+
if (check.kind === "tests" && pairs === void 0) {
|
|
4480
5019
|
try {
|
|
4481
5020
|
pairs = (await buildTestMap(root)).paired;
|
|
4482
5021
|
} catch (error) {
|
|
@@ -4550,29 +5089,36 @@ var init_evidence2 = __esm({
|
|
|
4550
5089
|
init_provenance();
|
|
4551
5090
|
init_vitest();
|
|
4552
5091
|
init_sarif();
|
|
5092
|
+
init_check();
|
|
4553
5093
|
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
|
-
|
|
5094
|
+
checkSchema2 = z22.object({
|
|
5095
|
+
id: z22.string().min(1).max(100),
|
|
5096
|
+
kind: z22.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
|
|
5097
|
+
tool: z22.string().min(1).max(200),
|
|
5098
|
+
command: z22.string().min(1).max(2e3),
|
|
5099
|
+
commit: z22.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
5100
|
+
workingTreeClean: z22.boolean().optional(),
|
|
5101
|
+
source: z22.string().max(2e3).optional(),
|
|
5102
|
+
sourceRoot: z22.string().min(1).max(4e3).optional(),
|
|
5103
|
+
status: z22.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
5104
|
+
reason: z22.string().min(1).max(2e3).optional(),
|
|
5105
|
+
exitCode: z22.number().int().nullable().optional(),
|
|
5106
|
+
environment: z22.object({
|
|
5107
|
+
toolVersion: z22.string().min(1).max(200),
|
|
5108
|
+
runtime: z22.string().min(1).max(200).optional(),
|
|
5109
|
+
platform: z22.string().min(1).max(200).optional(),
|
|
5110
|
+
configuration: z22.array(z22.string().min(1).max(4e3)).max(100).optional()
|
|
5111
|
+
}).optional(),
|
|
5112
|
+
report: z22.object({ format: z22.enum(["vitest-json", "sarif", "mason-check-json"]), path: z22.string().min(1).max(4e3) }).optional()
|
|
4567
5113
|
});
|
|
4568
5114
|
}
|
|
4569
5115
|
});
|
|
4570
5116
|
|
|
4571
5117
|
// src/review/review.ts
|
|
4572
|
-
import
|
|
4573
|
-
import
|
|
4574
|
-
import { execFile as
|
|
4575
|
-
import { promisify as
|
|
5118
|
+
import fs18 from "fs/promises";
|
|
5119
|
+
import path23 from "path";
|
|
5120
|
+
import { execFile as execFile13 } from "child_process";
|
|
5121
|
+
import { promisify as promisify13 } from "util";
|
|
4576
5122
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
4577
5123
|
try {
|
|
4578
5124
|
const { stdout } = await exec10("git", ["merge-base", base, head], {
|
|
@@ -4599,7 +5145,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
4599
5145
|
return matchingPaths(record.files, changedFiles);
|
|
4600
5146
|
}
|
|
4601
5147
|
async function computeReview(rootDir, base, options = {}) {
|
|
4602
|
-
const resolvedRoot =
|
|
5148
|
+
const resolvedRoot = path23.resolve(rootDir);
|
|
4603
5149
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
4604
5150
|
if (head === "unknown") return null;
|
|
4605
5151
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -4649,7 +5195,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
4649
5195
|
analyzed,
|
|
4650
5196
|
async (relPath) => {
|
|
4651
5197
|
try {
|
|
4652
|
-
await
|
|
5198
|
+
await fs18.access(path23.join(resolvedRoot, relPath));
|
|
4653
5199
|
return true;
|
|
4654
5200
|
} catch {
|
|
4655
5201
|
return false;
|
|
@@ -4694,7 +5240,7 @@ var init_review = __esm({
|
|
|
4694
5240
|
init_cochange();
|
|
4695
5241
|
init_evidence2();
|
|
4696
5242
|
init_snapshot();
|
|
4697
|
-
exec10 =
|
|
5243
|
+
exec10 = promisify13(execFile13);
|
|
4698
5244
|
MAX_ANALYZED_FILES = 50;
|
|
4699
5245
|
}
|
|
4700
5246
|
});
|
|
@@ -4703,7 +5249,7 @@ var init_review = __esm({
|
|
|
4703
5249
|
async function auditSummary(root) {
|
|
4704
5250
|
try {
|
|
4705
5251
|
const report = await computeAudit(root);
|
|
4706
|
-
if (!report) return { status: "no-context-files", reason: "No
|
|
5252
|
+
if (!report) return { status: "no-context-files", reason: "No README.md or agent instruction files found to audit." };
|
|
4707
5253
|
if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
|
|
4708
5254
|
return {
|
|
4709
5255
|
...report,
|
|
@@ -5012,16 +5558,16 @@ var init_setup = __esm({
|
|
|
5012
5558
|
});
|
|
5013
5559
|
|
|
5014
5560
|
// src/distribution/bundle.ts
|
|
5015
|
-
import
|
|
5016
|
-
import
|
|
5561
|
+
import fs19 from "fs/promises";
|
|
5562
|
+
import path24 from "path";
|
|
5017
5563
|
import { createHash as createHash4 } from "crypto";
|
|
5018
|
-
import { z as
|
|
5564
|
+
import { z as z23 } from "zod";
|
|
5019
5565
|
async function readBundle(root) {
|
|
5020
|
-
const bytes = await
|
|
5566
|
+
const bytes = await fs19.readFile(await storePath(root, "bundle.json"));
|
|
5021
5567
|
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
5022
5568
|
const windows = manifest.target.startsWith("win32-");
|
|
5023
5569
|
for (const file of Object.keys(manifest.files)) {
|
|
5024
|
-
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") ||
|
|
5570
|
+
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path24.isAbsolute(file)) {
|
|
5025
5571
|
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
5026
5572
|
}
|
|
5027
5573
|
}
|
|
@@ -5036,7 +5582,7 @@ async function verifyBundle(root, expectedHash) {
|
|
|
5036
5582
|
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
5583
|
const files = Object.keys(bundle.manifest.files);
|
|
5038
5584
|
for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
|
|
5039
|
-
if (sha256(await
|
|
5585
|
+
if (sha256(await fs19.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
|
|
5040
5586
|
}));
|
|
5041
5587
|
return bundle;
|
|
5042
5588
|
}
|
|
@@ -5046,31 +5592,31 @@ var init_bundle = __esm({
|
|
|
5046
5592
|
"use strict";
|
|
5047
5593
|
init_storage();
|
|
5048
5594
|
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:
|
|
5595
|
+
digest2 = z23.string().regex(/^[a-f0-9]{64}$/);
|
|
5596
|
+
bundleSchema = z23.object({
|
|
5597
|
+
format: z23.literal(1),
|
|
5598
|
+
version: z23.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
5599
|
+
target: z23.enum(targets),
|
|
5600
|
+
nodeVersion: z23.string(),
|
|
5601
|
+
files: z23.record(digest2)
|
|
5056
5602
|
}).strict();
|
|
5057
5603
|
sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
5058
5604
|
}
|
|
5059
5605
|
});
|
|
5060
5606
|
|
|
5061
5607
|
// src/distribution/path.ts
|
|
5062
|
-
import
|
|
5608
|
+
import fs20 from "fs/promises";
|
|
5063
5609
|
import os3 from "os";
|
|
5064
|
-
import
|
|
5065
|
-
import { execFile as
|
|
5066
|
-
import { promisify as
|
|
5610
|
+
import path25 from "path";
|
|
5611
|
+
import { execFile as execFile14 } from "child_process";
|
|
5612
|
+
import { promisify as promisify14 } from "util";
|
|
5067
5613
|
import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
|
|
5068
|
-
import { z as
|
|
5614
|
+
import { z as z24 } from "zod";
|
|
5069
5615
|
async function configurePath(bin, previous, save, options = {}) {
|
|
5070
5616
|
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
5071
5617
|
const changes = [...previous];
|
|
5072
5618
|
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) =>
|
|
5619
|
+
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path25.posix.isAbsolute(entry) && path25.posix.resolve(entry) === path25.posix.resolve(bin));
|
|
5074
5620
|
const message2 = alreadyOnPath ? "Run: mason setup --host codex" : "Open a new terminal, then run: mason setup --host codex";
|
|
5075
5621
|
const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
|
|
5076
5622
|
${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --host codex`}` });
|
|
@@ -5088,33 +5634,33 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
5088
5634
|
}
|
|
5089
5635
|
return { status: "configured", changes, message: message2 };
|
|
5090
5636
|
}
|
|
5091
|
-
const shell =
|
|
5637
|
+
const shell = path25.basename(env.SHELL ?? os3.userInfo().shell ?? "");
|
|
5092
5638
|
const userHome = options.userHome ?? os3.homedir();
|
|
5093
5639
|
let profiles;
|
|
5094
5640
|
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 = [
|
|
5641
|
+
if (env.MASON_PROFILE) profiles = [path25.resolve(env.MASON_PROFILE)];
|
|
5642
|
+
else if (shell === "zsh") profiles = [path25.join(env.ZDOTDIR || userHome, ".zshrc")];
|
|
5643
|
+
else if (shell === "sh") profiles = [path25.join(userHome, ".profile")];
|
|
5098
5644
|
else {
|
|
5099
|
-
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) =>
|
|
5645
|
+
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path25.join(userHome, name));
|
|
5100
5646
|
let login = candidates[2];
|
|
5101
|
-
for (const file of candidates) if (await
|
|
5647
|
+
for (const file of candidates) if (await fs20.lstat(file).then(() => true, (error) => {
|
|
5102
5648
|
if (error.code === "ENOENT") return false;
|
|
5103
5649
|
throw error;
|
|
5104
5650
|
})) {
|
|
5105
5651
|
login = file;
|
|
5106
5652
|
break;
|
|
5107
5653
|
}
|
|
5108
|
-
profiles = [
|
|
5654
|
+
profiles = [path25.join(userHome, ".bashrc"), login];
|
|
5109
5655
|
}
|
|
5110
5656
|
const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
5111
5657
|
for (const profile of profiles) {
|
|
5112
|
-
const stat = await
|
|
5658
|
+
const stat = await fs20.lstat(profile).catch((error) => {
|
|
5113
5659
|
if (error.code === "ENOENT") return null;
|
|
5114
5660
|
throw error;
|
|
5115
5661
|
});
|
|
5116
|
-
const file = stat?.isSymbolicLink() ? await
|
|
5117
|
-
const original = await
|
|
5662
|
+
const file = stat?.isSymbolicLink() ? await fs20.realpath(profile) : path25.resolve(profile);
|
|
5663
|
+
const original = await fs20.readFile(file).catch((error) => {
|
|
5118
5664
|
if (error.code === "ENOENT" && !stat) return null;
|
|
5119
5665
|
throw error;
|
|
5120
5666
|
});
|
|
@@ -5129,8 +5675,8 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
5129
5675
|
changes.push({ kind: "profile", file, block, created: original === null });
|
|
5130
5676
|
await save(changes);
|
|
5131
5677
|
}
|
|
5132
|
-
await
|
|
5133
|
-
const handle = await
|
|
5678
|
+
await fs20.mkdir(path25.dirname(file), { recursive: true });
|
|
5679
|
+
const handle = await fs20.open(file, original === null ? "ax+" : "a+", 384);
|
|
5134
5680
|
try {
|
|
5135
5681
|
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
5136
5682
|
await handle.writeFile(block);
|
|
@@ -5154,13 +5700,13 @@ async function removePath(changes) {
|
|
|
5154
5700
|
await windowsPath("remove", change.entry, current, change.created);
|
|
5155
5701
|
continue;
|
|
5156
5702
|
}
|
|
5157
|
-
const stat = await
|
|
5703
|
+
const stat = await fs20.lstat(change.file).catch((error) => {
|
|
5158
5704
|
if (error.code === "ENOENT") return null;
|
|
5159
5705
|
throw error;
|
|
5160
5706
|
});
|
|
5161
5707
|
if (!stat) continue;
|
|
5162
5708
|
if (!stat.isFile()) throw new Error("profile type changed");
|
|
5163
|
-
const bytes = await
|
|
5709
|
+
const bytes = await fs20.readFile(change.file), text2 = bytes.toString("utf8");
|
|
5164
5710
|
if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
|
|
5165
5711
|
const index2 = text2.indexOf(change.block);
|
|
5166
5712
|
if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
|
|
@@ -5170,7 +5716,7 @@ async function removePath(changes) {
|
|
|
5170
5716
|
const temporary = change.file + ".mason-" + randomUUID7();
|
|
5171
5717
|
try {
|
|
5172
5718
|
if (stat.nlink > 1) throw new Error("profile has multiple hard links");
|
|
5173
|
-
const handle = await
|
|
5719
|
+
const handle = await fs20.open(temporary, "wx", stat.mode & 511);
|
|
5174
5720
|
try {
|
|
5175
5721
|
await handle.writeFile(next);
|
|
5176
5722
|
await handle.chmod(stat.mode & 511);
|
|
@@ -5179,11 +5725,11 @@ async function removePath(changes) {
|
|
|
5179
5725
|
} finally {
|
|
5180
5726
|
await handle.close();
|
|
5181
5727
|
}
|
|
5182
|
-
const current = await
|
|
5183
|
-
if (!current.isFile() || current.ino !== stat.ino || !(await
|
|
5184
|
-
await
|
|
5728
|
+
const current = await fs20.lstat(change.file);
|
|
5729
|
+
if (!current.isFile() || current.ino !== stat.ino || !(await fs20.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
|
|
5730
|
+
await fs20.rename(temporary, change.file);
|
|
5185
5731
|
} finally {
|
|
5186
|
-
await
|
|
5732
|
+
await fs20.rm(temporary, { force: true });
|
|
5187
5733
|
}
|
|
5188
5734
|
} catch (error) {
|
|
5189
5735
|
warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5217,20 +5763,20 @@ try {
|
|
|
5217
5763
|
@{ value = $next; kind = $kind } | ConvertTo-Json -Compress
|
|
5218
5764
|
} finally { $key.Dispose() }
|
|
5219
5765
|
`;
|
|
5220
|
-
const { stdout } = await
|
|
5766
|
+
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
5767
|
return JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
|
|
5222
5768
|
}
|
|
5223
|
-
var
|
|
5769
|
+
var execute3, pathChangeSchema, quote, normalizeWindowsEntry, hasWindowsEntry;
|
|
5224
5770
|
var init_path = __esm({
|
|
5225
5771
|
"src/distribution/path.ts"() {
|
|
5226
5772
|
"use strict";
|
|
5227
|
-
|
|
5228
|
-
pathChangeSchema =
|
|
5229
|
-
|
|
5230
|
-
|
|
5773
|
+
execute3 = promisify14(execFile14);
|
|
5774
|
+
pathChangeSchema = z24.discriminatedUnion("kind", [
|
|
5775
|
+
z24.object({ kind: z24.literal("profile"), file: z24.string(), block: z24.string(), created: z24.boolean() }),
|
|
5776
|
+
z24.object({ kind: z24.literal("windows"), entry: z24.string(), created: z24.boolean() })
|
|
5231
5777
|
]);
|
|
5232
5778
|
quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
5233
|
-
normalizeWindowsEntry = (value) =>
|
|
5779
|
+
normalizeWindowsEntry = (value) => path25.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
5234
5780
|
hasWindowsEntry = (value, entry, env = process.env) => {
|
|
5235
5781
|
const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
|
|
5236
5782
|
const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
|
|
@@ -5248,41 +5794,41 @@ __export(install_exports2, {
|
|
|
5248
5794
|
uninstallStandalone: () => uninstallStandalone,
|
|
5249
5795
|
upgradeStandalone: () => upgradeStandalone
|
|
5250
5796
|
});
|
|
5251
|
-
import
|
|
5797
|
+
import fs21 from "fs/promises";
|
|
5252
5798
|
import os4 from "os";
|
|
5253
|
-
import
|
|
5799
|
+
import path26 from "path";
|
|
5254
5800
|
import { fileURLToPath } from "url";
|
|
5255
5801
|
import { spawn } from "child_process";
|
|
5256
5802
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
5257
|
-
import { z as
|
|
5803
|
+
import { z as z25 } from "zod";
|
|
5258
5804
|
async function replace(file, content2, mode = 384) {
|
|
5259
5805
|
const temp = file + ".tmp-" + randomUUID8();
|
|
5260
5806
|
try {
|
|
5261
|
-
await
|
|
5262
|
-
await
|
|
5807
|
+
await fs21.writeFile(temp, content2, { mode, flag: "wx" });
|
|
5808
|
+
await fs21.rename(temp, file);
|
|
5263
5809
|
} finally {
|
|
5264
|
-
await
|
|
5810
|
+
await fs21.rm(temp, { force: true });
|
|
5265
5811
|
}
|
|
5266
5812
|
}
|
|
5267
5813
|
async function installStandalone(source) {
|
|
5268
5814
|
const bundle = await verifyBundle(source);
|
|
5269
|
-
const selectedHome =
|
|
5270
|
-
await
|
|
5271
|
-
const home = await
|
|
5272
|
-
const selectedBin =
|
|
5815
|
+
const selectedHome = path26.resolve(defaultHome());
|
|
5816
|
+
await fs21.mkdir(selectedHome, { recursive: true });
|
|
5817
|
+
const home = await fs21.realpath(selectedHome);
|
|
5818
|
+
const selectedBin = path26.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path26.join(home, "bin") : path26.join(os4.homedir(), ".local/bin")));
|
|
5273
5819
|
return withLock(home, ".install-lock", async () => {
|
|
5274
5820
|
const raw = await readStoreJson(home, "install.json");
|
|
5275
|
-
const previous = raw === null ? null :
|
|
5276
|
-
if (!previous) for (const file of await
|
|
5821
|
+
const previous = raw === null ? null : recordSchema3.parse(raw);
|
|
5822
|
+
if (!previous) for (const file of await fs21.readdir(home)) {
|
|
5277
5823
|
if (file === ".install-lock") continue;
|
|
5278
|
-
const directory =
|
|
5279
|
-
const stat = await
|
|
5280
|
-
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await
|
|
5824
|
+
const directory = path26.join(home, file);
|
|
5825
|
+
const stat = await fs21.lstat(directory);
|
|
5826
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs21.readdir(directory)).length) {
|
|
5281
5827
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
5282
5828
|
}
|
|
5283
5829
|
}
|
|
5284
|
-
await
|
|
5285
|
-
const bin = await
|
|
5830
|
+
await fs21.mkdir(selectedBin, { recursive: true });
|
|
5831
|
+
const bin = await fs21.realpath(selectedBin);
|
|
5286
5832
|
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
5287
5833
|
const id = bundle.manifestHash.slice(0, 24);
|
|
5288
5834
|
const destination = await storePath(home, "versions/" + id);
|
|
@@ -5293,10 +5839,10 @@ $previousToken = $env:MASON_UNINSTALL_TOKEN
|
|
|
5293
5839
|
$token = [Guid]::NewGuid().ToString()
|
|
5294
5840
|
$env:MASON_UNINSTALL_TOKEN = $token
|
|
5295
5841
|
try {
|
|
5296
|
-
& ${psQuote(
|
|
5842
|
+
& ${psQuote(path26.join(destination, "node.exe"))} ${psQuote(path26.join(destination, "app/dist/mason.js"))} @args
|
|
5297
5843
|
$code = $LASTEXITCODE
|
|
5298
5844
|
if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
|
|
5299
|
-
if ([IO.File]::ReadAllText(${psQuote(
|
|
5845
|
+
if ([IO.File]::ReadAllText(${psQuote(path26.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
|
|
5300
5846
|
Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
|
|
5301
5847
|
Write-Output ${psQuote("Removed standalone installation: " + home)}
|
|
5302
5848
|
}
|
|
@@ -5305,11 +5851,11 @@ try {
|
|
|
5305
5851
|
finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
|
|
5306
5852
|
`
|
|
5307
5853
|
} : { mason: `#!/bin/sh
|
|
5308
|
-
exec ${shQuote(
|
|
5854
|
+
exec ${shQuote(path26.join(destination, "node"))} ${shQuote(path26.join(destination, "app/dist/mason.js"))} "$@"
|
|
5309
5855
|
` };
|
|
5310
5856
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
5311
5857
|
const file = await storePath(bin, name);
|
|
5312
|
-
const old = await
|
|
5858
|
+
const old = await fs21.readFile(file, "utf8").catch((error) => {
|
|
5313
5859
|
if (error.code === "ENOENT") return null;
|
|
5314
5860
|
throw error;
|
|
5315
5861
|
});
|
|
@@ -5318,7 +5864,7 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5318
5864
|
const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
|
|
5319
5865
|
const checkRetired = async (name, expected) => {
|
|
5320
5866
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
5321
|
-
const actual = await
|
|
5867
|
+
const actual = await fs21.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
5322
5868
|
if (error.code === "ENOENT") return null;
|
|
5323
5869
|
throw error;
|
|
5324
5870
|
});
|
|
@@ -5328,28 +5874,28 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5328
5874
|
const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
|
|
5329
5875
|
await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
|
|
5330
5876
|
const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
|
|
5331
|
-
await
|
|
5877
|
+
await fs21.mkdir(stage);
|
|
5332
5878
|
try {
|
|
5333
5879
|
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
5334
5880
|
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
5335
|
-
await
|
|
5336
|
-
await
|
|
5881
|
+
await fs21.copyFile(from, to);
|
|
5882
|
+
await fs21.chmod(to, (await fs21.stat(from)).mode & 511);
|
|
5337
5883
|
}
|
|
5338
5884
|
await verifyBundle(stage, bundle.manifestHash);
|
|
5339
|
-
const
|
|
5885
|
+
const exists = await fs21.lstat(destination).then(() => true, (error) => {
|
|
5340
5886
|
if (error.code === "ENOENT") return false;
|
|
5341
5887
|
throw error;
|
|
5342
5888
|
});
|
|
5343
|
-
if (
|
|
5344
|
-
else await
|
|
5889
|
+
if (exists) await verifyBundle(destination, bundle.manifestHash);
|
|
5890
|
+
else await fs21.rename(stage, destination);
|
|
5345
5891
|
for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
5346
5892
|
for (const [name, expected] of retired) {
|
|
5347
5893
|
await checkRetired(name, expected);
|
|
5348
|
-
await
|
|
5894
|
+
await fs21.rm(await storePath(bin, name), { force: true });
|
|
5349
5895
|
}
|
|
5350
5896
|
await writeStoreJson(home, "install.json", receipt);
|
|
5351
5897
|
} finally {
|
|
5352
|
-
await
|
|
5898
|
+
await fs21.rm(stage, { recursive: true, force: true });
|
|
5353
5899
|
}
|
|
5354
5900
|
const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
|
|
5355
5901
|
receipt.pathChanges = changes;
|
|
@@ -5359,12 +5905,12 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5359
5905
|
});
|
|
5360
5906
|
}
|
|
5361
5907
|
async function currentInstallation() {
|
|
5362
|
-
const app =
|
|
5363
|
-
const bundle =
|
|
5908
|
+
const app = path26.dirname(path26.dirname(fileURLToPath(import.meta.url)));
|
|
5909
|
+
const bundle = path26.dirname(app), home = path26.dirname(path26.dirname(bundle));
|
|
5364
5910
|
const raw = await readStoreJson(home, "install.json");
|
|
5365
5911
|
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
|
|
5912
|
+
const record = recordSchema3.parse(raw);
|
|
5913
|
+
if (record.home !== await fs21.realpath(home) || path26.dirname(bundle) !== path26.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
|
|
5368
5914
|
return { home, bundle, record };
|
|
5369
5915
|
}
|
|
5370
5916
|
async function upgradeStandalone(version) {
|
|
@@ -5372,7 +5918,7 @@ async function upgradeStandalone(version) {
|
|
|
5372
5918
|
const { home, bundle, record } = await currentInstallation();
|
|
5373
5919
|
await verifyBundle(bundle);
|
|
5374
5920
|
const windows = process.platform === "win32";
|
|
5375
|
-
const script =
|
|
5921
|
+
const script = path26.join(bundle, windows ? "install.ps1" : "install.sh");
|
|
5376
5922
|
return new Promise((resolve, reject) => {
|
|
5377
5923
|
const child = spawn(
|
|
5378
5924
|
windows ? "powershell.exe" : "sh",
|
|
@@ -5393,7 +5939,7 @@ async function uninstallStandalone() {
|
|
|
5393
5939
|
for (const [name, expected] of Object.entries(record.launchers)) {
|
|
5394
5940
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
5395
5941
|
const file = await storePath(record.bin, name);
|
|
5396
|
-
const actual = await
|
|
5942
|
+
const actual = await fs21.readFile(file, "utf8").catch((error) => {
|
|
5397
5943
|
if (error.code === "ENOENT") return null;
|
|
5398
5944
|
throw error;
|
|
5399
5945
|
});
|
|
@@ -5401,12 +5947,12 @@ async function uninstallStandalone() {
|
|
|
5401
5947
|
}
|
|
5402
5948
|
for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
|
|
5403
5949
|
if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
|
|
5404
|
-
for (const name of Object.keys(record.launchers)) await
|
|
5950
|
+
for (const name of Object.keys(record.launchers)) await fs21.rm(await storePath(record.bin, name), { force: true });
|
|
5405
5951
|
});
|
|
5406
|
-
if (process.platform !== "win32") await
|
|
5952
|
+
if (process.platform !== "win32") await fs21.rm(home, { recursive: true, force: true });
|
|
5407
5953
|
return home;
|
|
5408
5954
|
}
|
|
5409
|
-
var
|
|
5955
|
+
var recordSchema3, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
|
|
5410
5956
|
var init_install2 = __esm({
|
|
5411
5957
|
"src/distribution/install.ts"() {
|
|
5412
5958
|
"use strict";
|
|
@@ -5414,21 +5960,21 @@ var init_install2 = __esm({
|
|
|
5414
5960
|
init_storage();
|
|
5415
5961
|
init_store();
|
|
5416
5962
|
init_path();
|
|
5417
|
-
|
|
5418
|
-
format:
|
|
5419
|
-
home:
|
|
5420
|
-
bin:
|
|
5421
|
-
current:
|
|
5422
|
-
version:
|
|
5423
|
-
launchers:
|
|
5424
|
-
previousLaunchers:
|
|
5425
|
-
pathChanges:
|
|
5963
|
+
recordSchema3 = z25.object({
|
|
5964
|
+
format: z25.literal(1),
|
|
5965
|
+
home: z25.string(),
|
|
5966
|
+
bin: z25.string(),
|
|
5967
|
+
current: z25.string().regex(/^[a-f0-9]{24}$/),
|
|
5968
|
+
version: z25.string(),
|
|
5969
|
+
launchers: z25.record(z25.string()),
|
|
5970
|
+
previousLaunchers: z25.record(z25.string()).optional(),
|
|
5971
|
+
pathChanges: z25.array(pathChangeSchema).optional()
|
|
5426
5972
|
});
|
|
5427
5973
|
shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
5428
5974
|
psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
5429
5975
|
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
5976
|
launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
|
|
5431
|
-
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ?
|
|
5977
|
+
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path26.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path26.join(os4.homedir(), ".local/share/mason"));
|
|
5432
5978
|
}
|
|
5433
5979
|
});
|
|
5434
5980
|
|
|
@@ -5558,7 +6104,7 @@ var inputArgs = process.argv.slice(2);
|
|
|
5558
6104
|
var managedLaunch = inputArgs[0] === "--setup-host";
|
|
5559
6105
|
var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
|
|
5560
6106
|
var [command, ...args] = inputArgs;
|
|
5561
|
-
var usage = `Mason ${"0.
|
|
6107
|
+
var usage = `Mason ${"0.17.0"}
|
|
5562
6108
|
|
|
5563
6109
|
Usage: mason <command> [options]
|
|
5564
6110
|
|
|
@@ -5582,9 +6128,9 @@ try {
|
|
|
5582
6128
|
const active = await prepareLaunch2(setupHost, { allowInactive: command === "auto" && isHookCommand(args) });
|
|
5583
6129
|
if (!active) process.exit(0);
|
|
5584
6130
|
}
|
|
5585
|
-
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.
|
|
6131
|
+
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.17.0" }));
|
|
5586
6132
|
else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
|
|
5587
|
-
else if (["--version", "-v"].includes(command)) console.log("0.
|
|
6133
|
+
else if (["--version", "-v"].includes(command)) console.log("0.17.0");
|
|
5588
6134
|
else if (command === "internal-install") {
|
|
5589
6135
|
if (args.length) throw new Error("internal-install takes no arguments.");
|
|
5590
6136
|
const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|