mason-context 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +171 -37
- package/dist/mason-audit.js +395 -153
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js +504 -209
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +392 -126
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +3506 -1465
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +1048 -105
- package/dist/mason-review.js.map +1 -1
- package/package.json +7 -2
package/dist/mason-audit.js
CHANGED
|
@@ -1,50 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/audit/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import path16 from "path";
|
|
5
5
|
|
|
6
6
|
// src/audit/audit.ts
|
|
7
7
|
import fs9 from "fs/promises";
|
|
8
|
-
import
|
|
8
|
+
import path15 from "path";
|
|
9
9
|
|
|
10
10
|
// src/drift/drift.ts
|
|
11
11
|
import fs3 from "fs/promises";
|
|
12
|
-
import
|
|
12
|
+
import path6 from "path";
|
|
13
13
|
import { execFile as execFile3 } from "child_process";
|
|
14
14
|
import { promisify as promisify3 } from "util";
|
|
15
15
|
|
|
16
16
|
// src/snapshot/snapshot.ts
|
|
17
|
-
import
|
|
18
|
-
import path3 from "path";
|
|
17
|
+
import path5 from "path";
|
|
19
18
|
import { execFile as execFile2 } from "child_process";
|
|
20
19
|
import { promisify as promisify2 } from "util";
|
|
21
|
-
import fg3 from "fast-glob";
|
|
22
20
|
|
|
23
|
-
// src/
|
|
21
|
+
// src/utils/files.ts
|
|
24
22
|
import fs from "fs/promises";
|
|
25
|
-
import
|
|
23
|
+
import { constants } from "fs";
|
|
24
|
+
import path2 from "path";
|
|
26
25
|
import { execFile } from "child_process";
|
|
27
26
|
import { promisify } from "util";
|
|
28
27
|
import fg from "fast-glob";
|
|
29
|
-
var exec = promisify(execFile);
|
|
30
|
-
|
|
31
|
-
// src/test-map.ts
|
|
32
|
-
import path2 from "path";
|
|
33
|
-
import fg2 from "fast-glob";
|
|
34
28
|
|
|
35
|
-
// src/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
29
|
+
// src/utils/paths.ts
|
|
30
|
+
import path from "path";
|
|
31
|
+
function normalizeRepoPath(value) {
|
|
32
|
+
const slash = value.replace(/\\/g, "/");
|
|
33
|
+
if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
|
|
34
|
+
if (slash.split("/").includes("..")) return null;
|
|
35
|
+
const normalized = path.posix.normalize(slash).replace(/\/$/, "");
|
|
36
|
+
return normalized === "." ? null : normalized;
|
|
37
|
+
}
|
|
38
|
+
function anchorMatches(anchor, file) {
|
|
39
|
+
const a = normalizeRepoPath(anchor);
|
|
40
|
+
const f = normalizeRepoPath(file);
|
|
41
|
+
return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
|
|
46
42
|
}
|
|
47
|
-
|
|
43
|
+
function matchingPaths(anchors, files) {
|
|
44
|
+
return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/utils/files.ts
|
|
48
|
+
var exec = promisify(execFile);
|
|
49
|
+
var SOURCE_EXTENSIONS = ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs", "vue", "svelte", "kt", "kts", "java", "py", "go", "rs", "swift", "rb", "cs", "cpp", "c", "h", "hpp", "dart", "php"];
|
|
50
|
+
var SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
|
|
48
51
|
var SOURCE_IGNORE = [
|
|
49
52
|
"**/node_modules/**",
|
|
50
53
|
"**/dist/**",
|
|
@@ -52,58 +55,171 @@ var SOURCE_IGNORE = [
|
|
|
52
55
|
"**/.gradle/**",
|
|
53
56
|
"**/target/**",
|
|
54
57
|
"**/.git/**",
|
|
58
|
+
"**/.mason/**",
|
|
55
59
|
"**/vendor/**",
|
|
56
60
|
"**/__pycache__/**",
|
|
57
61
|
"**/venv/**",
|
|
58
62
|
"**/.venv/**",
|
|
59
63
|
"**/*.min.*",
|
|
60
64
|
"**/*.map",
|
|
65
|
+
"**/*.lock",
|
|
61
66
|
"**/generated/**",
|
|
67
|
+
"**/*.generated.*",
|
|
62
68
|
"**/R.java",
|
|
63
|
-
"**/BuildConfig.java"
|
|
69
|
+
"**/BuildConfig.java",
|
|
70
|
+
"**/package-lock.json",
|
|
71
|
+
"**/yarn.lock",
|
|
72
|
+
"**/pnpm-lock.yaml"
|
|
64
73
|
];
|
|
74
|
+
var MAX_SOURCE_BYTES = 1024 * 1024;
|
|
75
|
+
async function readBoundedFile(file, maxBytes) {
|
|
76
|
+
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
77
|
+
try {
|
|
78
|
+
const stat = await handle.stat();
|
|
79
|
+
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
80
|
+
const buffer = Buffer.alloc(Math.min(maxBytes + 1, stat.size + 1));
|
|
81
|
+
let bytes = 0;
|
|
82
|
+
while (bytes < buffer.length) {
|
|
83
|
+
const result = await handle.read(buffer, bytes, buffer.length - bytes, null);
|
|
84
|
+
if (result.bytesRead === 0) break;
|
|
85
|
+
bytes += result.bytesRead;
|
|
86
|
+
}
|
|
87
|
+
return bytes === buffer.length ? null : buffer.subarray(0, bytes).toString("utf8");
|
|
88
|
+
} finally {
|
|
89
|
+
await handle.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/utils/storage.ts
|
|
94
|
+
import fs2 from "fs/promises";
|
|
95
|
+
import path3 from "path";
|
|
96
|
+
import { randomUUID } from "crypto";
|
|
97
|
+
async function storePath(root, relative, createParents = false) {
|
|
98
|
+
const normalized = normalizeRepoPath(relative);
|
|
99
|
+
if (!normalized) throw new Error(`Invalid store path: ${relative}`);
|
|
100
|
+
let current = await fs2.realpath(root);
|
|
101
|
+
const parts = normalized.split("/");
|
|
102
|
+
for (let i = 0; i < parts.length; i++) {
|
|
103
|
+
current = path3.join(current, parts[i]);
|
|
104
|
+
let stat;
|
|
105
|
+
try {
|
|
106
|
+
stat = await fs2.lstat(current);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.code !== "ENOENT") throw error;
|
|
109
|
+
if (createParents && i < parts.length - 1) {
|
|
110
|
+
try {
|
|
111
|
+
await fs2.mkdir(current);
|
|
112
|
+
} catch (mkdirError) {
|
|
113
|
+
if (mkdirError.code !== "EEXIST") throw mkdirError;
|
|
114
|
+
}
|
|
115
|
+
stat = await fs2.lstat(current);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (stat?.isSymbolicLink()) throw new Error(`Symlink in store path: ${relative}`);
|
|
119
|
+
}
|
|
120
|
+
return current;
|
|
121
|
+
}
|
|
122
|
+
async function readStoreJson(root, relative) {
|
|
123
|
+
try {
|
|
124
|
+
const file = await storePath(root, relative);
|
|
125
|
+
const raw = await readBoundedFile(file, 10 * 1024 * 1024);
|
|
126
|
+
if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
|
|
127
|
+
const parsed = JSON.parse(raw);
|
|
128
|
+
if (parsed === null) throw new Error("expected a JSON object, received null");
|
|
129
|
+
return parsed;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error.code === "ENOENT") return null;
|
|
132
|
+
throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/snapshot/snapshot.ts
|
|
137
|
+
import { z } from "zod";
|
|
138
|
+
|
|
139
|
+
// src/test-map.ts
|
|
140
|
+
import path4 from "path";
|
|
141
|
+
|
|
142
|
+
// src/snapshot/snapshot.ts
|
|
143
|
+
var exec2 = promisify2(execFile2);
|
|
144
|
+
var repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
145
|
+
var verificationFields = {
|
|
146
|
+
refreshedHash: z.string().optional(),
|
|
147
|
+
verifiedAt: z.string().optional(),
|
|
148
|
+
verifiedHash: z.string().optional(),
|
|
149
|
+
verificationFailed: z.boolean().optional(),
|
|
150
|
+
verificationNote: z.string().optional()
|
|
151
|
+
};
|
|
152
|
+
var featureSchema = z.object({
|
|
153
|
+
description: z.string(),
|
|
154
|
+
files: z.array(repoPath),
|
|
155
|
+
tests: z.array(repoPath).optional(),
|
|
156
|
+
type: z.enum(["capability", "infrastructure"]).optional(),
|
|
157
|
+
...verificationFields
|
|
158
|
+
}).passthrough();
|
|
159
|
+
var flowSchema = z.object({ description: z.string(), chain: z.array(repoPath), ...verificationFields }).passthrough();
|
|
160
|
+
var snapshotSchema = z.object({
|
|
161
|
+
version: z.literal(2),
|
|
162
|
+
createdAt: z.string(),
|
|
163
|
+
updatedAt: z.string(),
|
|
164
|
+
gitHash: z.string(),
|
|
165
|
+
features: z.record(featureSchema),
|
|
166
|
+
flows: z.record(flowSchema)
|
|
167
|
+
}).passthrough();
|
|
168
|
+
async function getCurrentGitHash(rootDir) {
|
|
169
|
+
try {
|
|
170
|
+
const { stdout } = await exec2("git", ["rev-parse", "HEAD"], {
|
|
171
|
+
cwd: rootDir
|
|
172
|
+
});
|
|
173
|
+
return stdout.trim();
|
|
174
|
+
} catch {
|
|
175
|
+
return "unknown";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
65
178
|
|
|
66
179
|
// src/drift/drift.ts
|
|
67
180
|
var exec3 = promisify3(execFile3);
|
|
68
|
-
|
|
69
|
-
|
|
181
|
+
function parseChanges(output) {
|
|
182
|
+
const fields = output.split("\0");
|
|
183
|
+
const changes = [];
|
|
184
|
+
for (let i = 0; i < fields.length && fields[i]; ) {
|
|
185
|
+
const code = fields[i++];
|
|
186
|
+
const first = fields[i++];
|
|
187
|
+
if (!first) break;
|
|
188
|
+
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
189
|
+
const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
|
|
190
|
+
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
|
|
191
|
+
changes.push(change);
|
|
192
|
+
}
|
|
193
|
+
return changes;
|
|
194
|
+
}
|
|
195
|
+
function touchedPaths(changes) {
|
|
196
|
+
return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
|
|
197
|
+
}
|
|
198
|
+
async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
199
|
+
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
|
|
70
200
|
try {
|
|
71
|
-
const { stdout } = await exec3(
|
|
72
|
-
|
|
73
|
-
["diff", "--name-status", "-M", fromHash, "HEAD"],
|
|
74
|
-
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
75
|
-
);
|
|
76
|
-
const changes = [];
|
|
77
|
-
for (const line of stdout.split("\n")) {
|
|
78
|
-
if (!line.trim()) continue;
|
|
79
|
-
const parts = line.split(" ");
|
|
80
|
-
if (parts.some((p) => p.startsWith(".mason/"))) continue;
|
|
81
|
-
const code = parts[0];
|
|
82
|
-
if (code.startsWith("R") && parts.length >= 3) {
|
|
83
|
-
changes.push({
|
|
84
|
-
status: "renamed",
|
|
85
|
-
path: parts[2],
|
|
86
|
-
previousPath: parts[1]
|
|
87
|
-
});
|
|
88
|
-
} else if (code.startsWith("C") && parts.length >= 3) {
|
|
89
|
-
changes.push({ status: "added", path: parts[2] });
|
|
90
|
-
} else if (code === "A" && parts.length >= 2) {
|
|
91
|
-
changes.push({ status: "added", path: parts[1] });
|
|
92
|
-
} else if (code === "D" && parts.length >= 2) {
|
|
93
|
-
changes.push({ status: "deleted", path: parts[1] });
|
|
94
|
-
} else if (parts.length >= 2) {
|
|
95
|
-
changes.push({ status: "modified", path: parts[1] });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return changes;
|
|
201
|
+
const { stdout } = await exec3("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
202
|
+
return parseChanges(stdout);
|
|
99
203
|
} catch {
|
|
100
204
|
return null;
|
|
101
205
|
}
|
|
102
206
|
}
|
|
207
|
+
async function getWorkingTree(resolvedRoot) {
|
|
208
|
+
try {
|
|
209
|
+
const [diff, untracked] = await Promise.all([
|
|
210
|
+
exec3("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
|
|
211
|
+
exec3("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
|
|
212
|
+
]);
|
|
213
|
+
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
214
|
+
return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
|
|
215
|
+
} catch {
|
|
216
|
+
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
103
219
|
|
|
104
220
|
// src/audit/docs.ts
|
|
105
221
|
import fs4 from "fs/promises";
|
|
106
|
-
import
|
|
222
|
+
import path7 from "path";
|
|
107
223
|
import { execFile as execFile5 } from "child_process";
|
|
108
224
|
import { promisify as promisify5 } from "util";
|
|
109
225
|
|
|
@@ -486,7 +602,7 @@ async function discoverDocs(resolvedRoot) {
|
|
|
486
602
|
for (const candidate of DOC_CANDIDATES) {
|
|
487
603
|
let content;
|
|
488
604
|
try {
|
|
489
|
-
content = await fs4.readFile(
|
|
605
|
+
content = await fs4.readFile(path7.join(resolvedRoot, candidate), "utf-8");
|
|
490
606
|
} catch {
|
|
491
607
|
continue;
|
|
492
608
|
}
|
|
@@ -514,7 +630,7 @@ var ALL_CHECKS = [
|
|
|
514
630
|
|
|
515
631
|
// src/audit/checks/deleted-reference.ts
|
|
516
632
|
import fs5 from "fs/promises";
|
|
517
|
-
import
|
|
633
|
+
import path8 from "path";
|
|
518
634
|
async function exists(absPath) {
|
|
519
635
|
try {
|
|
520
636
|
await fs5.access(absPath);
|
|
@@ -537,7 +653,7 @@ async function checkDeletedReferences(ctx) {
|
|
|
537
653
|
if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
|
|
538
654
|
continue;
|
|
539
655
|
}
|
|
540
|
-
if (await exists(
|
|
656
|
+
if (await exists(path8.join(ctx.root, claim.path))) continue;
|
|
541
657
|
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
542
658
|
const renamedTo = renames.get(claim.path) ?? null;
|
|
543
659
|
if (renamedTo) {
|
|
@@ -573,14 +689,14 @@ async function checkDeletedReferences(ctx) {
|
|
|
573
689
|
deletedInCommit: deleted,
|
|
574
690
|
everTracked: true,
|
|
575
691
|
parentDirExists: await exists(
|
|
576
|
-
|
|
692
|
+
path8.join(ctx.root, path8.dirname(claim.path))
|
|
577
693
|
)
|
|
578
694
|
}
|
|
579
695
|
});
|
|
580
696
|
continue;
|
|
581
697
|
}
|
|
582
698
|
const parentDirExists = await exists(
|
|
583
|
-
|
|
699
|
+
path8.join(ctx.root, path8.dirname(claim.path))
|
|
584
700
|
);
|
|
585
701
|
if (!parentDirExists) continue;
|
|
586
702
|
const issue = {
|
|
@@ -604,8 +720,8 @@ async function checkDeletedReferences(ctx) {
|
|
|
604
720
|
}
|
|
605
721
|
|
|
606
722
|
// src/audit/checks/new-module.ts
|
|
607
|
-
import
|
|
608
|
-
import
|
|
723
|
+
import fg2 from "fast-glob";
|
|
724
|
+
import path9 from "path";
|
|
609
725
|
var DIR_DENYLIST = /* @__PURE__ */ new Set([
|
|
610
726
|
"node_modules",
|
|
611
727
|
"dist",
|
|
@@ -627,8 +743,8 @@ var DIR_DENYLIST = /* @__PURE__ */ new Set([
|
|
|
627
743
|
]);
|
|
628
744
|
var SECOND_LEVEL_MIN_SOURCE_FILES = 2;
|
|
629
745
|
var ENUMERATION_THRESHOLD = 2;
|
|
630
|
-
function escapeRegExp(
|
|
631
|
-
return
|
|
746
|
+
function escapeRegExp(text2) {
|
|
747
|
+
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
632
748
|
}
|
|
633
749
|
function isMentioned(combinedDocs, name) {
|
|
634
750
|
const re = new RegExp(
|
|
@@ -638,7 +754,7 @@ function isMentioned(combinedDocs, name) {
|
|
|
638
754
|
return re.test(combinedDocs);
|
|
639
755
|
}
|
|
640
756
|
async function listSubdirs(absDir) {
|
|
641
|
-
const dirs = await
|
|
757
|
+
const dirs = await fg2("*", {
|
|
642
758
|
cwd: absDir,
|
|
643
759
|
onlyDirectories: true,
|
|
644
760
|
suppressErrors: true
|
|
@@ -646,7 +762,7 @@ async function listSubdirs(absDir) {
|
|
|
646
762
|
return dirs.filter((d) => !DIR_DENYLIST.has(d)).sort();
|
|
647
763
|
}
|
|
648
764
|
async function countSourceFiles(absDir) {
|
|
649
|
-
const files = await
|
|
765
|
+
const files = await fg2(SOURCE_GLOB, {
|
|
650
766
|
cwd: absDir,
|
|
651
767
|
ignore: SOURCE_IGNORE,
|
|
652
768
|
suppressErrors: true
|
|
@@ -675,7 +791,7 @@ async function checkNewModules(ctx) {
|
|
|
675
791
|
});
|
|
676
792
|
};
|
|
677
793
|
for (const topDir of await listSubdirs(ctx.root)) {
|
|
678
|
-
const absTop =
|
|
794
|
+
const absTop = path9.join(ctx.root, topDir);
|
|
679
795
|
const topMentioned = isMentioned(combinedDocs, topDir);
|
|
680
796
|
if (!topMentioned) {
|
|
681
797
|
const count = await countSourceFiles(absTop);
|
|
@@ -687,7 +803,7 @@ async function checkNewModules(ctx) {
|
|
|
687
803
|
if (mentioned.length < ENUMERATION_THRESHOLD) continue;
|
|
688
804
|
for (const sub of subdirs) {
|
|
689
805
|
if (isMentioned(combinedDocs, sub)) continue;
|
|
690
|
-
const count = await countSourceFiles(
|
|
806
|
+
const count = await countSourceFiles(path9.join(absTop, sub));
|
|
691
807
|
if (count >= SECOND_LEVEL_MIN_SOURCE_FILES) {
|
|
692
808
|
await flag(`${topDir}/${sub}`, count);
|
|
693
809
|
}
|
|
@@ -698,8 +814,8 @@ async function checkNewModules(ctx) {
|
|
|
698
814
|
|
|
699
815
|
// src/audit/checks/stale-count.ts
|
|
700
816
|
import fs6 from "fs/promises";
|
|
701
|
-
import
|
|
702
|
-
import
|
|
817
|
+
import path10 from "path";
|
|
818
|
+
import fg3 from "fast-glob";
|
|
703
819
|
var MEMBERS_CAP = 50;
|
|
704
820
|
async function readIfExists(absPath) {
|
|
705
821
|
try {
|
|
@@ -710,7 +826,7 @@ async function readIfExists(absPath) {
|
|
|
710
826
|
}
|
|
711
827
|
async function countGradleModules(root) {
|
|
712
828
|
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
713
|
-
const content = await readIfExists(
|
|
829
|
+
const content = await readIfExists(path10.join(root, name));
|
|
714
830
|
if (content === null) continue;
|
|
715
831
|
const members = [];
|
|
716
832
|
for (const call of content.matchAll(/include\s*\(([^)]*)\)/g)) {
|
|
@@ -724,26 +840,26 @@ async function countGradleModules(root) {
|
|
|
724
840
|
return null;
|
|
725
841
|
}
|
|
726
842
|
async function countNpmWorkspaces(root) {
|
|
727
|
-
const pkgRaw = await readIfExists(
|
|
843
|
+
const pkgRaw = await readIfExists(path10.join(root, "package.json"));
|
|
728
844
|
if (pkgRaw !== null) {
|
|
729
845
|
try {
|
|
730
846
|
const pkg = JSON.parse(pkgRaw);
|
|
731
847
|
const globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
|
|
732
848
|
if (globs.length > 0) {
|
|
733
|
-
const matched = await
|
|
849
|
+
const matched = await fg3(
|
|
734
850
|
globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
|
|
735
851
|
{ cwd: root, ignore: ["**/node_modules/**"] }
|
|
736
852
|
);
|
|
737
853
|
return {
|
|
738
854
|
actual: matched.length,
|
|
739
855
|
countedFrom: "package.json workspaces",
|
|
740
|
-
members: matched.map((m) =>
|
|
856
|
+
members: matched.map((m) => path10.dirname(m)).sort()
|
|
741
857
|
};
|
|
742
858
|
}
|
|
743
859
|
} catch {
|
|
744
860
|
}
|
|
745
861
|
}
|
|
746
|
-
const pnpmRaw = await readIfExists(
|
|
862
|
+
const pnpmRaw = await readIfExists(path10.join(root, "pnpm-workspace.yaml"));
|
|
747
863
|
if (pnpmRaw !== null) {
|
|
748
864
|
const globs = [];
|
|
749
865
|
let inPackages = false;
|
|
@@ -762,21 +878,21 @@ async function countNpmWorkspaces(root) {
|
|
|
762
878
|
}
|
|
763
879
|
}
|
|
764
880
|
if (globs.length > 0) {
|
|
765
|
-
const matched = await
|
|
881
|
+
const matched = await fg3(
|
|
766
882
|
globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
|
|
767
883
|
{ cwd: root, ignore: ["**/node_modules/**"] }
|
|
768
884
|
);
|
|
769
885
|
return {
|
|
770
886
|
actual: matched.length,
|
|
771
887
|
countedFrom: "pnpm-workspace.yaml",
|
|
772
|
-
members: matched.map((m) =>
|
|
888
|
+
members: matched.map((m) => path10.dirname(m)).sort()
|
|
773
889
|
};
|
|
774
890
|
}
|
|
775
891
|
}
|
|
776
892
|
return null;
|
|
777
893
|
}
|
|
778
894
|
async function countCargoCrates(root) {
|
|
779
|
-
const content = await readIfExists(
|
|
895
|
+
const content = await readIfExists(path10.join(root, "Cargo.toml"));
|
|
780
896
|
if (content === null) return null;
|
|
781
897
|
const membersBlock = content.match(/members\s*=\s*\[([\s\S]*?)\]/);
|
|
782
898
|
if (!membersBlock) return null;
|
|
@@ -787,12 +903,12 @@ async function countCargoCrates(root) {
|
|
|
787
903
|
const members = /* @__PURE__ */ new Set();
|
|
788
904
|
for (const entry of entries) {
|
|
789
905
|
if (/[*?[\]{}]/.test(entry)) {
|
|
790
|
-
const matched = await
|
|
906
|
+
const matched = await fg3(`${entry.replace(/\/+$/, "")}/Cargo.toml`, {
|
|
791
907
|
cwd: root,
|
|
792
908
|
ignore: ["**/target/**"]
|
|
793
909
|
});
|
|
794
|
-
for (const m of matched) members.add(
|
|
795
|
-
} else if (await readIfExists(
|
|
910
|
+
for (const m of matched) members.add(path10.dirname(m));
|
|
911
|
+
} else if (await readIfExists(path10.join(root, entry, "Cargo.toml")) !== null) {
|
|
796
912
|
members.add(entry);
|
|
797
913
|
}
|
|
798
914
|
}
|
|
@@ -837,8 +953,8 @@ async function checkStaleCounts(ctx) {
|
|
|
837
953
|
|
|
838
954
|
// src/audit/checks/dead-command.ts
|
|
839
955
|
import fs7 from "fs/promises";
|
|
840
|
-
import
|
|
841
|
-
import
|
|
956
|
+
import path11 from "path";
|
|
957
|
+
import fg4 from "fast-glob";
|
|
842
958
|
var AVAILABLE_SCRIPTS_CAP = 30;
|
|
843
959
|
async function scriptsOf(absManifest) {
|
|
844
960
|
try {
|
|
@@ -854,7 +970,7 @@ async function checkDeadCommands(ctx) {
|
|
|
854
970
|
(doc) => doc.claims.commands.map((claim) => ({ doc, claim }))
|
|
855
971
|
);
|
|
856
972
|
if (commandClaims.length === 0) return result;
|
|
857
|
-
const rootScripts = await scriptsOf(
|
|
973
|
+
const rootScripts = await scriptsOf(path11.join(ctx.root, "package.json"));
|
|
858
974
|
if (rootScripts === null) {
|
|
859
975
|
result.skipped.push({
|
|
860
976
|
check: "dead-command",
|
|
@@ -868,7 +984,7 @@ async function checkDeadCommands(ctx) {
|
|
|
868
984
|
const loadWorkspaceScripts = async () => {
|
|
869
985
|
if (workspaceScripts !== null) return workspaceScripts;
|
|
870
986
|
workspaceScripts = /* @__PURE__ */ new Set();
|
|
871
|
-
const manifests = await
|
|
987
|
+
const manifests = await fg4("**/package.json", {
|
|
872
988
|
cwd: ctx.root,
|
|
873
989
|
ignore: [
|
|
874
990
|
"**/node_modules/**",
|
|
@@ -879,7 +995,7 @@ async function checkDeadCommands(ctx) {
|
|
|
879
995
|
});
|
|
880
996
|
manifestsChecked = ["package.json", ...manifests.sort()];
|
|
881
997
|
for (const manifest of manifests) {
|
|
882
|
-
const scripts = await scriptsOf(
|
|
998
|
+
const scripts = await scriptsOf(path11.join(ctx.root, manifest));
|
|
883
999
|
for (const name of scripts ?? []) workspaceScripts.add(name);
|
|
884
1000
|
}
|
|
885
1001
|
return workspaceScripts;
|
|
@@ -968,86 +1084,205 @@ async function checkDepsChanged(ctx) {
|
|
|
968
1084
|
}
|
|
969
1085
|
|
|
970
1086
|
// src/decisions/drift.ts
|
|
971
|
-
import
|
|
1087
|
+
import path14 from "path";
|
|
972
1088
|
|
|
973
1089
|
// src/decisions/decisions.ts
|
|
974
1090
|
import fs8 from "fs/promises";
|
|
975
|
-
import
|
|
1091
|
+
import path13 from "path";
|
|
976
1092
|
import { createHash } from "crypto";
|
|
977
1093
|
|
|
978
1094
|
// src/context/lexical.ts
|
|
979
|
-
import
|
|
1095
|
+
import path12 from "path";
|
|
980
1096
|
|
|
981
|
-
// src/decisions/
|
|
982
|
-
|
|
983
|
-
|
|
1097
|
+
// src/decisions/provenance.ts
|
|
1098
|
+
import { z as z2 } from "zod";
|
|
1099
|
+
var text = (max) => z2.string().trim().min(1).max(max);
|
|
1100
|
+
var decisionSourceSchema = z2.object({
|
|
1101
|
+
kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
1102
|
+
reference: text(1e3),
|
|
1103
|
+
note: text(500).optional()
|
|
1104
|
+
}).strict();
|
|
1105
|
+
var attributionSchema = z2.object({
|
|
1106
|
+
owner: text(200).nullable().optional(),
|
|
1107
|
+
sources: z2.array(decisionSourceSchema).max(20).optional(),
|
|
1108
|
+
actor: text(200).optional()
|
|
1109
|
+
});
|
|
1110
|
+
var contentSchema = z2.object({
|
|
1111
|
+
title: z2.string().min(1),
|
|
1112
|
+
body: z2.string().min(1),
|
|
1113
|
+
category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
1114
|
+
files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
|
|
1115
|
+
owner: text(200).optional(),
|
|
1116
|
+
sources: z2.array(decisionSourceSchema).max(20)
|
|
1117
|
+
});
|
|
1118
|
+
var approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
|
|
1119
|
+
var statusSchema = z2.enum(["active", "superseded", "retired"]);
|
|
1120
|
+
var reviewEvidenceSchema = z2.object({
|
|
1121
|
+
baseHash: z2.string(),
|
|
1122
|
+
headHash: z2.string(),
|
|
1123
|
+
historyAvailable: z2.boolean(),
|
|
1124
|
+
changedFiles: z2.array(z2.string()),
|
|
1125
|
+
localChanges: z2.array(z2.string())
|
|
1126
|
+
});
|
|
1127
|
+
var eventSchema = z2.object({
|
|
1128
|
+
kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
|
|
1129
|
+
at: z2.string().datetime(),
|
|
1130
|
+
actor: text(200).optional(),
|
|
1131
|
+
note: text(1500).optional(),
|
|
1132
|
+
revision: z2.number().int().positive(),
|
|
1133
|
+
content: contentSchema,
|
|
1134
|
+
approval: approvalSchema,
|
|
1135
|
+
status: statusSchema,
|
|
1136
|
+
refreshedHash: z2.string(),
|
|
1137
|
+
evidence: reviewEvidenceSchema.optional()
|
|
1138
|
+
});
|
|
1139
|
+
var legacySchema = z2.object({
|
|
1140
|
+
version: z2.literal(1),
|
|
1141
|
+
id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
1142
|
+
title: z2.string().min(1),
|
|
1143
|
+
body: z2.string().min(1),
|
|
1144
|
+
category: contentSchema.shape.category,
|
|
1145
|
+
files: contentSchema.shape.files,
|
|
1146
|
+
createdAt: z2.string(),
|
|
1147
|
+
updatedAt: z2.string(),
|
|
1148
|
+
refreshedHash: z2.string(),
|
|
1149
|
+
status: z2.enum(["active", "superseded"]),
|
|
1150
|
+
supersededBy: z2.string().optional()
|
|
1151
|
+
}).passthrough();
|
|
1152
|
+
var currentSchema = legacySchema.extend({
|
|
1153
|
+
version: z2.literal(2),
|
|
1154
|
+
status: statusSchema,
|
|
1155
|
+
approval: approvalSchema,
|
|
1156
|
+
revision: z2.number().int().positive(),
|
|
1157
|
+
owner: text(200).optional(),
|
|
1158
|
+
sources: z2.array(decisionSourceSchema).max(20),
|
|
1159
|
+
history: z2.array(eventSchema).min(1)
|
|
1160
|
+
}).superRefine((record, ctx) => {
|
|
1161
|
+
const invalid = (message) => ctx.addIssue({ code: "custom", message });
|
|
1162
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1163
|
+
let previous;
|
|
1164
|
+
for (const event of record.history) {
|
|
1165
|
+
if (!previous) {
|
|
1166
|
+
if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
|
|
1167
|
+
if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
|
|
1168
|
+
} else {
|
|
1169
|
+
if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
|
|
1170
|
+
if (previous.status !== "active") invalid("Archived decisions cannot be changed");
|
|
1171
|
+
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
|
|
1172
|
+
if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
|
|
1173
|
+
if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
|
|
1174
|
+
if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
|
|
1175
|
+
const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
|
|
1176
|
+
if (event.approval !== approval) invalid("Approval disagrees with review history");
|
|
1177
|
+
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
|
|
1178
|
+
}
|
|
1179
|
+
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
|
|
1180
|
+
if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
|
|
1181
|
+
if (["accepted", "reaffirmed"].includes(event.kind)) {
|
|
1182
|
+
if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
|
|
1183
|
+
if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length) invalid("Acceptance requires a committed evidence baseline");
|
|
1184
|
+
}
|
|
1185
|
+
previous = event;
|
|
1186
|
+
}
|
|
1187
|
+
if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash) invalid("Decision does not match the final history event");
|
|
1188
|
+
});
|
|
1189
|
+
var decisionSchema = z2.union([legacySchema, currentSchema]);
|
|
1190
|
+
function decisionContent(record) {
|
|
1191
|
+
return {
|
|
1192
|
+
title: record.title,
|
|
1193
|
+
body: record.body,
|
|
1194
|
+
category: record.category,
|
|
1195
|
+
files: record.files,
|
|
1196
|
+
...typeof record.owner === "string" ? { owner: record.owner } : {},
|
|
1197
|
+
sources: Array.isArray(record.sources) ? record.sources : []
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function decisionApproval(record) {
|
|
1201
|
+
return record.version === 1 ? "unreviewed" : record.approval;
|
|
984
1202
|
}
|
|
985
|
-
|
|
1203
|
+
function effectiveDecision(record) {
|
|
1204
|
+
if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed") return record;
|
|
1205
|
+
let index = record.history.length - 1;
|
|
1206
|
+
while (index >= 0 && !["accepted", "reaffirmed"].includes(record.history[index].kind)) index--;
|
|
1207
|
+
if (index < 0) return record;
|
|
1208
|
+
const event = record.history[index];
|
|
1209
|
+
return {
|
|
1210
|
+
...record,
|
|
1211
|
+
...event.content,
|
|
1212
|
+
owner: event.content.owner,
|
|
1213
|
+
approval: "accepted",
|
|
1214
|
+
revision: event.revision,
|
|
1215
|
+
refreshedHash: event.refreshedHash,
|
|
1216
|
+
updatedAt: event.at,
|
|
1217
|
+
history: record.history.slice(0, index + 1)
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function decisionProvenance(record, freshness = "unknown") {
|
|
1221
|
+
const approval = decisionApproval(record);
|
|
1222
|
+
const review = record.version === 2 ? [...record.history].reverse().find((e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision) : void 0;
|
|
1223
|
+
return {
|
|
1224
|
+
approval,
|
|
1225
|
+
revision: record.version === 2 ? record.revision : 0,
|
|
1226
|
+
owner: record.version === 2 ? record.owner ?? null : null,
|
|
1227
|
+
sources: record.version === 2 ? record.sources : [],
|
|
1228
|
+
guidance: record.status !== "active" ? "historical" : approval === "accepted" ? "constraint" : approval === "proposed" ? "proposal" : "unreviewed",
|
|
1229
|
+
reviewRequired: record.status === "active" && (approval !== "accepted" || freshness !== "current"),
|
|
1230
|
+
lastReview: review ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/decisions/decisions.ts
|
|
1235
|
+
async function loadDecisionStore(rootDir) {
|
|
1236
|
+
const records = [];
|
|
1237
|
+
const diagnostics = [];
|
|
986
1238
|
let entries;
|
|
987
1239
|
try {
|
|
988
|
-
entries = await fs8.readdir(
|
|
989
|
-
} catch {
|
|
990
|
-
|
|
1240
|
+
entries = await fs8.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
1243
|
+
return { records, diagnostics };
|
|
991
1244
|
}
|
|
992
|
-
const
|
|
993
|
-
for (const entry of entries) {
|
|
1245
|
+
for (const entry of entries.sort()) {
|
|
994
1246
|
if (!entry.endsWith(".json")) continue;
|
|
1247
|
+
const relative = `.mason/decisions/${entry}`;
|
|
995
1248
|
try {
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
if (parsed.version !== 1 || !parsed.id || !parsed.title || !parsed.body) {
|
|
1002
|
-
continue;
|
|
1003
|
-
}
|
|
1004
|
-
records.push(parsed);
|
|
1005
|
-
} catch {
|
|
1006
|
-
continue;
|
|
1249
|
+
const record = decisionSchema.parse(await readStoreJson(rootDir, relative));
|
|
1250
|
+
if (entry !== `${record.id}.json`) throw new Error("Record id does not match its filename");
|
|
1251
|
+
records.push(record);
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
diagnostics.push({ path: relative, message: error instanceof Error ? error.message : String(error) });
|
|
1007
1254
|
}
|
|
1008
1255
|
}
|
|
1009
|
-
return records
|
|
1256
|
+
return { records, diagnostics };
|
|
1010
1257
|
}
|
|
1011
1258
|
|
|
1012
1259
|
// src/decisions/drift.ts
|
|
1013
1260
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
1014
|
-
const resolvedRoot =
|
|
1015
|
-
const
|
|
1016
|
-
const report = {
|
|
1017
|
-
|
|
1018
|
-
totalDecisions: records.length,
|
|
1019
|
-
staleDecisions: {}
|
|
1020
|
-
};
|
|
1021
|
-
const head = await getCurrentGitHash(resolvedRoot);
|
|
1261
|
+
const resolvedRoot = path14.resolve(rootDir);
|
|
1262
|
+
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
1263
|
+
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
1264
|
+
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
1022
1265
|
const changesByHash = /* @__PURE__ */ new Map();
|
|
1023
|
-
|
|
1024
|
-
if (record.
|
|
1025
|
-
if (record.refreshedHash === head) continue;
|
|
1266
|
+
const inspect = async (record) => {
|
|
1267
|
+
if (record.files.length === 0) return { freshness: "unknown", changedFiles: [] };
|
|
1026
1268
|
let touched = changesByHash.get(record.refreshedHash);
|
|
1027
1269
|
if (touched === void 0) {
|
|
1028
|
-
const changes = await getChangesWithStatus(
|
|
1029
|
-
|
|
1030
|
-
record.refreshedHash
|
|
1031
|
-
);
|
|
1032
|
-
if (changes === null) {
|
|
1033
|
-
touched = null;
|
|
1034
|
-
} else {
|
|
1035
|
-
touched = /* @__PURE__ */ new Set();
|
|
1036
|
-
for (const change of changes) {
|
|
1037
|
-
touched.add(change.path);
|
|
1038
|
-
if (change.previousPath) touched.add(change.previousPath);
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1270
|
+
const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
|
|
1271
|
+
touched = changes === null ? null : touchedPaths(changes);
|
|
1041
1272
|
changesByHash.set(record.refreshedHash, touched);
|
|
1042
1273
|
}
|
|
1043
|
-
if (touched === null)
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1274
|
+
if (touched === null) report.historyAvailable = false;
|
|
1275
|
+
const hits = touched ? matchingPaths(record.files, touched) : [];
|
|
1276
|
+
const localHits = matchingPaths(record.files, workingTree.changedFiles);
|
|
1277
|
+
return { freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current", changedFiles: hits };
|
|
1278
|
+
};
|
|
1279
|
+
for (const record of store.records) {
|
|
1280
|
+
if (record.status !== "active") continue;
|
|
1281
|
+
const effective = effectiveDecision(record);
|
|
1282
|
+
const state = await inspect(effective);
|
|
1283
|
+
report.freshness[record.id] = state.freshness;
|
|
1284
|
+
if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
|
|
1285
|
+
if (effective !== record) (report.pendingProposals ??= {})[record.id] = await inspect(record);
|
|
1051
1286
|
}
|
|
1052
1287
|
return report;
|
|
1053
1288
|
}
|
|
@@ -1056,7 +1291,9 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
1056
1291
|
async function checkDecisionAnchors(ctx) {
|
|
1057
1292
|
const result = emptyResult();
|
|
1058
1293
|
if (!ctx.decisionsPresent) return result;
|
|
1059
|
-
const
|
|
1294
|
+
const store = await loadDecisionStore(ctx.root);
|
|
1295
|
+
const records = store.records;
|
|
1296
|
+
for (const diagnostic of store.diagnostics) result.skipped.push({ check: "decision-anchor-drift", reason: `${diagnostic.path}: ${diagnostic.message}` });
|
|
1060
1297
|
const drift = await computeDecisionDrift(ctx.root, records);
|
|
1061
1298
|
if (!drift.historyAvailable) {
|
|
1062
1299
|
result.skipped.push({
|
|
@@ -1064,13 +1301,17 @@ async function checkDecisionAnchors(ctx) {
|
|
|
1064
1301
|
reason: "some decision base commits are unreachable (shallow clone?)"
|
|
1065
1302
|
});
|
|
1066
1303
|
}
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1304
|
+
const changed = records.flatMap((record) => [
|
|
1305
|
+
{ record: effectiveDecision(record), changedFiles: drift.staleDecisions[record.id] ?? [], freshness: drift.freshness?.[record.id] ?? "unknown" },
|
|
1306
|
+
{ record, changedFiles: drift.pendingProposals?.[record.id]?.changedFiles ?? [], freshness: drift.pendingProposals?.[record.id]?.freshness ?? "unknown" }
|
|
1307
|
+
]);
|
|
1308
|
+
for (const { record, changedFiles, freshness } of changed) {
|
|
1309
|
+
if (!changedFiles.length) continue;
|
|
1310
|
+
const id = record.id;
|
|
1311
|
+
const provenance = decisionProvenance(record, freshness);
|
|
1071
1312
|
result.advisories.push({
|
|
1072
1313
|
type: "decision-anchor-drift",
|
|
1073
|
-
message: `decision "${record.title}" has anchor files that changed since
|
|
1314
|
+
message: `decision "${record.title}" (${provenance.approval}) has anchor files that changed since its evidence baseline \u2013 needs human review`,
|
|
1074
1315
|
anchor: {
|
|
1075
1316
|
doc: `.mason/decisions/${id}.json`,
|
|
1076
1317
|
line: null,
|
|
@@ -1078,6 +1319,7 @@ async function checkDecisionAnchors(ctx) {
|
|
|
1078
1319
|
},
|
|
1079
1320
|
evidence: {
|
|
1080
1321
|
kind: "decision-anchor",
|
|
1322
|
+
provenance,
|
|
1081
1323
|
decisionId: id,
|
|
1082
1324
|
title: record.title,
|
|
1083
1325
|
changedFiles,
|
|
@@ -1103,7 +1345,7 @@ function emptyResult() {
|
|
|
1103
1345
|
|
|
1104
1346
|
// src/audit/audit.ts
|
|
1105
1347
|
async function computeAudit(rootDir, options = {}) {
|
|
1106
|
-
const resolvedRoot =
|
|
1348
|
+
const resolvedRoot = path15.resolve(rootDir);
|
|
1107
1349
|
const docs = await discoverDocs(resolvedRoot);
|
|
1108
1350
|
if (docs.length === 0) return null;
|
|
1109
1351
|
const headHash = await getCurrentGitHash(resolvedRoot);
|
|
@@ -1133,7 +1375,7 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
1133
1375
|
}
|
|
1134
1376
|
let decisionsPresent = false;
|
|
1135
1377
|
try {
|
|
1136
|
-
await fs9.access(
|
|
1378
|
+
await fs9.access(path15.join(resolvedRoot, ".mason", "decisions"));
|
|
1137
1379
|
decisionsPresent = true;
|
|
1138
1380
|
} catch {
|
|
1139
1381
|
}
|
|
@@ -1327,7 +1569,7 @@ async function runAuditCli(argv, io = {
|
|
|
1327
1569
|
io.out(USAGE);
|
|
1328
1570
|
return 0;
|
|
1329
1571
|
}
|
|
1330
|
-
const rootDir =
|
|
1572
|
+
const rootDir = path16.resolve(args.dir);
|
|
1331
1573
|
const report = await computeAudit(rootDir, { checks: args.checks });
|
|
1332
1574
|
if (!report) {
|
|
1333
1575
|
io.err(
|