mason-context 0.8.1 → 0.10.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 +25 -0
- package/README.md +178 -35
- package/dist/mason-audit.js +368 -149
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js +479 -209
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +337 -123
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +3459 -1451
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +1309 -0
- package/dist/mason-review.js.map +1 -0
- package/package.json +9 -3
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,185 @@ 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
|
+
};
|
|
984
1199
|
}
|
|
985
|
-
|
|
1200
|
+
function decisionApproval(record) {
|
|
1201
|
+
return record.version === 1 ? "unreviewed" : record.approval;
|
|
1202
|
+
}
|
|
1203
|
+
function decisionProvenance(record, freshness = "unknown") {
|
|
1204
|
+
const approval = decisionApproval(record);
|
|
1205
|
+
const review = record.version === 2 ? [...record.history].reverse().find((e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision) : void 0;
|
|
1206
|
+
return {
|
|
1207
|
+
approval,
|
|
1208
|
+
revision: record.version === 2 ? record.revision : 0,
|
|
1209
|
+
owner: record.version === 2 ? record.owner ?? null : null,
|
|
1210
|
+
sources: record.version === 2 ? record.sources : [],
|
|
1211
|
+
guidance: record.status !== "active" ? "historical" : approval === "accepted" ? "constraint" : approval === "proposed" ? "proposal" : "unreviewed",
|
|
1212
|
+
reviewRequired: record.status === "active" && (approval !== "accepted" || freshness !== "current"),
|
|
1213
|
+
lastReview: review ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/decisions/decisions.ts
|
|
1218
|
+
async function loadDecisionStore(rootDir) {
|
|
1219
|
+
const records = [];
|
|
1220
|
+
const diagnostics = [];
|
|
986
1221
|
let entries;
|
|
987
1222
|
try {
|
|
988
|
-
entries = await fs8.readdir(
|
|
989
|
-
} catch {
|
|
990
|
-
|
|
1223
|
+
entries = await fs8.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
1226
|
+
return { records, diagnostics };
|
|
991
1227
|
}
|
|
992
|
-
const
|
|
993
|
-
for (const entry of entries) {
|
|
1228
|
+
for (const entry of entries.sort()) {
|
|
994
1229
|
if (!entry.endsWith(".json")) continue;
|
|
1230
|
+
const relative = `.mason/decisions/${entry}`;
|
|
995
1231
|
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;
|
|
1232
|
+
const record = decisionSchema.parse(await readStoreJson(rootDir, relative));
|
|
1233
|
+
if (entry !== `${record.id}.json`) throw new Error("Record id does not match its filename");
|
|
1234
|
+
records.push(record);
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
diagnostics.push({ path: relative, message: error instanceof Error ? error.message : String(error) });
|
|
1007
1237
|
}
|
|
1008
1238
|
}
|
|
1009
|
-
return records
|
|
1239
|
+
return { records, diagnostics };
|
|
1010
1240
|
}
|
|
1011
1241
|
|
|
1012
1242
|
// src/decisions/drift.ts
|
|
1013
1243
|
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);
|
|
1244
|
+
const resolvedRoot = path14.resolve(rootDir);
|
|
1245
|
+
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
1246
|
+
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
1247
|
+
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
1022
1248
|
const changesByHash = /* @__PURE__ */ new Map();
|
|
1023
|
-
for (const record of records) {
|
|
1024
|
-
if (record.status !== "active"
|
|
1025
|
-
if (record.
|
|
1249
|
+
for (const record of store.records) {
|
|
1250
|
+
if (record.status !== "active") continue;
|
|
1251
|
+
if (record.files.length === 0) {
|
|
1252
|
+
report.freshness[record.id] = "unknown";
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1026
1255
|
let touched = changesByHash.get(record.refreshedHash);
|
|
1027
1256
|
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
|
-
}
|
|
1257
|
+
const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
|
|
1258
|
+
touched = changes === null ? null : touchedPaths(changes);
|
|
1041
1259
|
changesByHash.set(record.refreshedHash, touched);
|
|
1042
1260
|
}
|
|
1043
|
-
if (touched === null)
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (hits.length > 0) {
|
|
1049
|
-
report.staleDecisions[record.id] = hits;
|
|
1050
|
-
}
|
|
1261
|
+
if (touched === null) report.historyAvailable = false;
|
|
1262
|
+
const hits = touched ? matchingPaths(record.files, touched) : [];
|
|
1263
|
+
if (hits.length) report.staleDecisions[record.id] = hits;
|
|
1264
|
+
const localHits = matchingPaths(record.files, workingTree.changedFiles);
|
|
1265
|
+
report.freshness[record.id] = touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current";
|
|
1051
1266
|
}
|
|
1052
1267
|
return report;
|
|
1053
1268
|
}
|
|
@@ -1056,7 +1271,9 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
1056
1271
|
async function checkDecisionAnchors(ctx) {
|
|
1057
1272
|
const result = emptyResult();
|
|
1058
1273
|
if (!ctx.decisionsPresent) return result;
|
|
1059
|
-
const
|
|
1274
|
+
const store = await loadDecisionStore(ctx.root);
|
|
1275
|
+
const records = store.records;
|
|
1276
|
+
for (const diagnostic of store.diagnostics) result.skipped.push({ check: "decision-anchor-drift", reason: `${diagnostic.path}: ${diagnostic.message}` });
|
|
1060
1277
|
const drift = await computeDecisionDrift(ctx.root, records);
|
|
1061
1278
|
if (!drift.historyAvailable) {
|
|
1062
1279
|
result.skipped.push({
|
|
@@ -1068,9 +1285,10 @@ async function checkDecisionAnchors(ctx) {
|
|
|
1068
1285
|
for (const [id, changedFiles] of Object.entries(drift.staleDecisions)) {
|
|
1069
1286
|
const record = byId.get(id);
|
|
1070
1287
|
if (!record) continue;
|
|
1288
|
+
const provenance = decisionProvenance(record, drift.freshness?.[id] ?? "unknown");
|
|
1071
1289
|
result.advisories.push({
|
|
1072
1290
|
type: "decision-anchor-drift",
|
|
1073
|
-
message: `decision "${record.title}" has anchor files that changed since
|
|
1291
|
+
message: `decision "${record.title}" (${provenance.approval}) has anchor files that changed since its evidence baseline \u2013 needs human review`,
|
|
1074
1292
|
anchor: {
|
|
1075
1293
|
doc: `.mason/decisions/${id}.json`,
|
|
1076
1294
|
line: null,
|
|
@@ -1078,6 +1296,7 @@ async function checkDecisionAnchors(ctx) {
|
|
|
1078
1296
|
},
|
|
1079
1297
|
evidence: {
|
|
1080
1298
|
kind: "decision-anchor",
|
|
1299
|
+
provenance,
|
|
1081
1300
|
decisionId: id,
|
|
1082
1301
|
title: record.title,
|
|
1083
1302
|
changedFiles,
|
|
@@ -1103,7 +1322,7 @@ function emptyResult() {
|
|
|
1103
1322
|
|
|
1104
1323
|
// src/audit/audit.ts
|
|
1105
1324
|
async function computeAudit(rootDir, options = {}) {
|
|
1106
|
-
const resolvedRoot =
|
|
1325
|
+
const resolvedRoot = path15.resolve(rootDir);
|
|
1107
1326
|
const docs = await discoverDocs(resolvedRoot);
|
|
1108
1327
|
if (docs.length === 0) return null;
|
|
1109
1328
|
const headHash = await getCurrentGitHash(resolvedRoot);
|
|
@@ -1133,7 +1352,7 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
1133
1352
|
}
|
|
1134
1353
|
let decisionsPresent = false;
|
|
1135
1354
|
try {
|
|
1136
|
-
await fs9.access(
|
|
1355
|
+
await fs9.access(path15.join(resolvedRoot, ".mason", "decisions"));
|
|
1137
1356
|
decisionsPresent = true;
|
|
1138
1357
|
} catch {
|
|
1139
1358
|
}
|
|
@@ -1327,7 +1546,7 @@ async function runAuditCli(argv, io = {
|
|
|
1327
1546
|
io.out(USAGE);
|
|
1328
1547
|
return 0;
|
|
1329
1548
|
}
|
|
1330
|
-
const rootDir =
|
|
1549
|
+
const rootDir = path16.resolve(args.dir);
|
|
1331
1550
|
const report = await computeAudit(rootDir, { checks: args.checks });
|
|
1332
1551
|
if (!report) {
|
|
1333
1552
|
io.err(
|