mason-context 0.16.3 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/dist/mason-audit.js +1728 -770
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +1434 -822
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +3549 -2865
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +80 -19
- package/dist/mason-review.js.map +1 -1
- package/dist/mason.js +1543 -946
- package/dist/mason.js.map +1 -1
- package/package.json +5 -1
package/dist/mason-audit.js
CHANGED
|
@@ -1,30 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import fs3 from "fs/promises";
|
|
12
|
-
import path6 from "path";
|
|
13
|
-
import { execFile as execFile3 } from "child_process";
|
|
14
|
-
import { promisify as promisify3 } from "util";
|
|
15
|
-
|
|
16
|
-
// src/snapshot/snapshot.ts
|
|
17
|
-
import path5 from "path";
|
|
18
|
-
import { execFile as execFile2 } from "child_process";
|
|
19
|
-
import { promisify as promisify2 } from "util";
|
|
20
|
-
|
|
21
|
-
// src/utils/files.ts
|
|
22
|
-
import fs from "fs/promises";
|
|
23
|
-
import { constants } from "fs";
|
|
24
|
-
import path2 from "path";
|
|
25
|
-
import { execFile } from "child_process";
|
|
26
|
-
import { promisify } from "util";
|
|
27
|
-
import fg from "fast-glob";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
28
11
|
|
|
29
12
|
// src/utils/paths.ts
|
|
30
13
|
import path from "path";
|
|
@@ -47,35 +30,19 @@ function anchorMatches(anchor, file) {
|
|
|
47
30
|
function matchingPaths(anchors, files) {
|
|
48
31
|
return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
|
|
49
32
|
}
|
|
33
|
+
var init_paths = __esm({
|
|
34
|
+
"src/utils/paths.ts"() {
|
|
35
|
+
"use strict";
|
|
36
|
+
}
|
|
37
|
+
});
|
|
50
38
|
|
|
51
39
|
// src/utils/files.ts
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"**/build/**",
|
|
59
|
-
"**/.gradle/**",
|
|
60
|
-
"**/target/**",
|
|
61
|
-
"**/.git/**",
|
|
62
|
-
"**/.mason/**",
|
|
63
|
-
"**/vendor/**",
|
|
64
|
-
"**/__pycache__/**",
|
|
65
|
-
"**/venv/**",
|
|
66
|
-
"**/.venv/**",
|
|
67
|
-
"**/*.min.*",
|
|
68
|
-
"**/*.map",
|
|
69
|
-
"**/*.lock",
|
|
70
|
-
"**/generated/**",
|
|
71
|
-
"**/*.generated.*",
|
|
72
|
-
"**/R.java",
|
|
73
|
-
"**/BuildConfig.java",
|
|
74
|
-
"**/package-lock.json",
|
|
75
|
-
"**/yarn.lock",
|
|
76
|
-
"**/pnpm-lock.yaml"
|
|
77
|
-
];
|
|
78
|
-
var MAX_SOURCE_BYTES = 1024 * 1024;
|
|
40
|
+
import fs from "fs/promises";
|
|
41
|
+
import { constants } from "fs";
|
|
42
|
+
import path2 from "path";
|
|
43
|
+
import { execFile } from "child_process";
|
|
44
|
+
import { promisify } from "util";
|
|
45
|
+
import fg from "fast-glob";
|
|
79
46
|
async function readBoundedFile(file, maxBytes) {
|
|
80
47
|
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
81
48
|
try {
|
|
@@ -116,6 +83,40 @@ async function loadProjectConfig(root) {
|
|
|
116
83
|
throw new Error(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`);
|
|
117
84
|
}
|
|
118
85
|
}
|
|
86
|
+
var exec, SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
|
|
87
|
+
var init_files = __esm({
|
|
88
|
+
"src/utils/files.ts"() {
|
|
89
|
+
"use strict";
|
|
90
|
+
init_paths();
|
|
91
|
+
exec = promisify(execFile);
|
|
92
|
+
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"];
|
|
93
|
+
SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
|
|
94
|
+
SOURCE_IGNORE = [
|
|
95
|
+
"**/node_modules/**",
|
|
96
|
+
"**/dist/**",
|
|
97
|
+
"**/build/**",
|
|
98
|
+
"**/.gradle/**",
|
|
99
|
+
"**/target/**",
|
|
100
|
+
"**/.git/**",
|
|
101
|
+
"**/.mason/**",
|
|
102
|
+
"**/vendor/**",
|
|
103
|
+
"**/__pycache__/**",
|
|
104
|
+
"**/venv/**",
|
|
105
|
+
"**/.venv/**",
|
|
106
|
+
"**/*.min.*",
|
|
107
|
+
"**/*.map",
|
|
108
|
+
"**/*.lock",
|
|
109
|
+
"**/generated/**",
|
|
110
|
+
"**/*.generated.*",
|
|
111
|
+
"**/R.java",
|
|
112
|
+
"**/BuildConfig.java",
|
|
113
|
+
"**/package-lock.json",
|
|
114
|
+
"**/yarn.lock",
|
|
115
|
+
"**/pnpm-lock.yaml"
|
|
116
|
+
];
|
|
117
|
+
MAX_SOURCE_BYTES = 1024 * 1024;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
119
120
|
|
|
120
121
|
// src/utils/storage.ts
|
|
121
122
|
import fs2 from "fs/promises";
|
|
@@ -179,39 +180,28 @@ async function writeStoreJson(root, relative, value) {
|
|
|
179
180
|
await fs2.rm(temporary, { force: true });
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
var init_storage = __esm({
|
|
184
|
+
"src/utils/storage.ts"() {
|
|
185
|
+
"use strict";
|
|
186
|
+
init_paths();
|
|
187
|
+
init_files();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
185
190
|
|
|
186
191
|
// src/test-map.ts
|
|
187
192
|
import path4 from "path";
|
|
193
|
+
var init_test_map = __esm({
|
|
194
|
+
"src/test-map.ts"() {
|
|
195
|
+
"use strict";
|
|
196
|
+
init_files();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
188
199
|
|
|
189
200
|
// src/snapshot/snapshot.ts
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
verifiedAt: z.string().optional(),
|
|
195
|
-
verifiedHash: z.string().optional(),
|
|
196
|
-
verificationFailed: z.boolean().optional(),
|
|
197
|
-
verificationNote: z.string().optional()
|
|
198
|
-
};
|
|
199
|
-
var featureSchema = z.object({
|
|
200
|
-
description: z.string(),
|
|
201
|
-
files: z.array(repoPath),
|
|
202
|
-
tests: z.array(repoPath).optional(),
|
|
203
|
-
type: z.enum(["capability", "infrastructure"]).optional(),
|
|
204
|
-
...verificationFields
|
|
205
|
-
}).passthrough();
|
|
206
|
-
var flowSchema = z.object({ description: z.string(), chain: z.array(repoPath), ...verificationFields }).passthrough();
|
|
207
|
-
var snapshotSchema = z.object({
|
|
208
|
-
version: z.literal(2),
|
|
209
|
-
createdAt: z.string(),
|
|
210
|
-
updatedAt: z.string(),
|
|
211
|
-
gitHash: z.string(),
|
|
212
|
-
features: z.record(featureSchema),
|
|
213
|
-
flows: z.record(flowSchema)
|
|
214
|
-
}).passthrough();
|
|
201
|
+
import path5 from "path";
|
|
202
|
+
import { execFile as execFile2 } from "child_process";
|
|
203
|
+
import { promisify as promisify2 } from "util";
|
|
204
|
+
import { z } from "zod";
|
|
215
205
|
async function getCurrentGitHash(rootDir) {
|
|
216
206
|
try {
|
|
217
207
|
const { stdout } = await exec2("git", ["rev-parse", "HEAD"], {
|
|
@@ -222,9 +212,48 @@ async function getCurrentGitHash(rootDir) {
|
|
|
222
212
|
return "unknown";
|
|
223
213
|
}
|
|
224
214
|
}
|
|
215
|
+
var exec2, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
|
|
216
|
+
var init_snapshot = __esm({
|
|
217
|
+
"src/snapshot/snapshot.ts"() {
|
|
218
|
+
"use strict";
|
|
219
|
+
init_files();
|
|
220
|
+
init_files();
|
|
221
|
+
init_storage();
|
|
222
|
+
init_paths();
|
|
223
|
+
init_test_map();
|
|
224
|
+
exec2 = promisify2(execFile2);
|
|
225
|
+
repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
226
|
+
verificationFields = {
|
|
227
|
+
refreshedHash: z.string().optional(),
|
|
228
|
+
verifiedAt: z.string().optional(),
|
|
229
|
+
verifiedHash: z.string().optional(),
|
|
230
|
+
verificationFailed: z.boolean().optional(),
|
|
231
|
+
verificationNote: z.string().optional()
|
|
232
|
+
};
|
|
233
|
+
featureSchema = z.object({
|
|
234
|
+
description: z.string(),
|
|
235
|
+
files: z.array(repoPath),
|
|
236
|
+
tests: z.array(repoPath).optional(),
|
|
237
|
+
type: z.enum(["capability", "infrastructure"]).optional(),
|
|
238
|
+
...verificationFields
|
|
239
|
+
}).passthrough();
|
|
240
|
+
flowSchema = z.object({ description: z.string(), chain: z.array(repoPath), ...verificationFields }).passthrough();
|
|
241
|
+
snapshotSchema = z.object({
|
|
242
|
+
version: z.literal(2),
|
|
243
|
+
createdAt: z.string(),
|
|
244
|
+
updatedAt: z.string(),
|
|
245
|
+
gitHash: z.string(),
|
|
246
|
+
features: z.record(featureSchema),
|
|
247
|
+
flows: z.record(flowSchema)
|
|
248
|
+
}).passthrough();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
225
251
|
|
|
226
252
|
// src/drift/drift.ts
|
|
227
|
-
|
|
253
|
+
import fs3 from "fs/promises";
|
|
254
|
+
import path6 from "path";
|
|
255
|
+
import { execFile as execFile3 } from "child_process";
|
|
256
|
+
import { promisify as promisify3 } from "util";
|
|
228
257
|
function parseChanges(output) {
|
|
229
258
|
const fields = output.split("\0");
|
|
230
259
|
const changes = [];
|
|
@@ -263,16 +292,100 @@ async function getWorkingTree(resolvedRoot) {
|
|
|
263
292
|
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
264
293
|
}
|
|
265
294
|
}
|
|
295
|
+
var exec3;
|
|
296
|
+
var init_drift = __esm({
|
|
297
|
+
"src/drift/drift.ts"() {
|
|
298
|
+
"use strict";
|
|
299
|
+
init_snapshot();
|
|
300
|
+
init_paths();
|
|
301
|
+
exec3 = promisify3(execFile3);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
266
304
|
|
|
267
|
-
// src/audit/
|
|
268
|
-
import fs4 from "fs/promises";
|
|
305
|
+
// src/audit/commands.ts
|
|
269
306
|
import path7 from "path";
|
|
270
|
-
|
|
271
|
-
|
|
307
|
+
function splitCommands(line) {
|
|
308
|
+
const parts = [];
|
|
309
|
+
let quote = "", start = 0;
|
|
310
|
+
for (let i = 0; i < line.length; i++) {
|
|
311
|
+
const char = line[i];
|
|
312
|
+
if (char === "\\") {
|
|
313
|
+
i++;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (quote) {
|
|
317
|
+
if (char === quote) quote = "";
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (char === "'" || char === '"') {
|
|
321
|
+
quote = char;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (char === ";" || line.slice(i, i + 2) === "&&") {
|
|
325
|
+
parts.push(line.slice(start, i));
|
|
326
|
+
if (char === "&") i++;
|
|
327
|
+
start = i + 1;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
parts.push(line.slice(start));
|
|
331
|
+
return parts;
|
|
332
|
+
}
|
|
333
|
+
function commandClaims(line, lineNo, cwd, shell = false) {
|
|
334
|
+
const claims = [];
|
|
335
|
+
const segments = splitCommands(line);
|
|
336
|
+
for (const segment of segments) {
|
|
337
|
+
const text2 = segment.trim().replace(/^\$\s+/, "");
|
|
338
|
+
if (text2.startsWith("#") || /^(?:echo|printf)\b/.test(text2)) continue;
|
|
339
|
+
const cd = text2.match(/^cd\s+(.+)$/);
|
|
340
|
+
if (cd) {
|
|
341
|
+
const directory = literal(cd[1]);
|
|
342
|
+
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path7.posix.join(cwd ?? ".", directory);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
for (const match of text2.matchAll(COMMAND)) {
|
|
346
|
+
const flags = match[2].trim();
|
|
347
|
+
const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
|
|
348
|
+
let directory = cwd;
|
|
349
|
+
let scopeUnknown;
|
|
350
|
+
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
351
|
+
if (selector) {
|
|
352
|
+
const selected = literal(selector[1]);
|
|
353
|
+
directory = selected === null || cwd === null ? null : path7.posix.join(cwd ?? ".", selected);
|
|
354
|
+
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
355
|
+
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
356
|
+
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
357
|
+
scopeUnknown = "Package selection or conditional options after the script need review.";
|
|
358
|
+
}
|
|
359
|
+
if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
|
|
360
|
+
scopeUnknown = "The command's working directory or shell control flow could not be resolved.";
|
|
361
|
+
}
|
|
362
|
+
claims.push({
|
|
363
|
+
scriptName: match[3],
|
|
364
|
+
invocation: match[0],
|
|
365
|
+
excerpt: match[0],
|
|
366
|
+
line: lineNo,
|
|
367
|
+
...directory !== void 0 && directory !== null ? { directory } : {},
|
|
368
|
+
...scopeUnknown ? { scopeUnknown } : {}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (/\b(?:cd|pushd|popd)\b/.test(text2)) cwd = null;
|
|
372
|
+
}
|
|
373
|
+
return { claims, cwd };
|
|
374
|
+
}
|
|
375
|
+
var TOKEN, COMMAND, literal;
|
|
376
|
+
var init_commands = __esm({
|
|
377
|
+
"src/audit/commands.ts"() {
|
|
378
|
+
"use strict";
|
|
379
|
+
TOKEN = `(?:"[^"\\n]*"|'[^'\\n]*'|[^\\s\`;&|]+)`;
|
|
380
|
+
COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
|
|
381
|
+
literal = (value) => {
|
|
382
|
+
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
383
|
+
return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path7.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
});
|
|
272
387
|
|
|
273
388
|
// src/audit/tree.ts
|
|
274
|
-
var MIN_GLYPH_LINES = 3;
|
|
275
|
-
var GLYPHS = ["\u251C\u2500\u2500", "\u2514\u2500\u2500"];
|
|
276
389
|
function glyphIndex(line) {
|
|
277
390
|
for (const glyph of GLYPHS) {
|
|
278
391
|
const idx = line.indexOf(glyph);
|
|
@@ -341,46 +454,17 @@ function extractTreeClaims(blockLines, blockStartLine) {
|
|
|
341
454
|
}
|
|
342
455
|
return claims;
|
|
343
456
|
}
|
|
457
|
+
var MIN_GLYPH_LINES, GLYPHS;
|
|
458
|
+
var init_tree = __esm({
|
|
459
|
+
"src/audit/tree.ts"() {
|
|
460
|
+
"use strict";
|
|
461
|
+
MIN_GLYPH_LINES = 3;
|
|
462
|
+
GLYPHS = ["\u251C\u2500\u2500", "\u2514\u2500\u2500"];
|
|
463
|
+
}
|
|
464
|
+
});
|
|
344
465
|
|
|
345
466
|
// src/audit/claims.ts
|
|
346
|
-
|
|
347
|
-
"package.json",
|
|
348
|
-
"package-lock.json",
|
|
349
|
-
"pnpm-workspace.yaml",
|
|
350
|
-
"tsconfig.json",
|
|
351
|
-
"tsup.config.ts",
|
|
352
|
-
"vitest.config.ts",
|
|
353
|
-
"Makefile",
|
|
354
|
-
"Dockerfile",
|
|
355
|
-
"docker-compose.yml",
|
|
356
|
-
"Cargo.toml",
|
|
357
|
-
"go.mod",
|
|
358
|
-
"go.sum",
|
|
359
|
-
"pyproject.toml",
|
|
360
|
-
"requirements.txt",
|
|
361
|
-
"Gemfile",
|
|
362
|
-
"composer.json",
|
|
363
|
-
"settings.gradle.kts",
|
|
364
|
-
"settings.gradle",
|
|
365
|
-
"build.gradle.kts",
|
|
366
|
-
"build.gradle",
|
|
367
|
-
"manifest.json",
|
|
368
|
-
"server.json",
|
|
369
|
-
"README.md",
|
|
370
|
-
"CHANGELOG.md",
|
|
371
|
-
"LICENSE",
|
|
372
|
-
"CLAUDE.md",
|
|
373
|
-
"AGENTS.md",
|
|
374
|
-
".gitignore",
|
|
375
|
-
".env.example"
|
|
376
|
-
]);
|
|
377
|
-
var SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
|
|
378
|
-
var COMMAND_RE = /\b(npm|pnpm|yarn)\s+run\s+([A-Za-z0-9:_.-]+)/g;
|
|
379
|
-
var COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
|
|
380
|
-
var COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
|
|
381
|
-
var IGNORE_LINE = "<!-- mason:ignore -->";
|
|
382
|
-
var IGNORE_START = "<!-- mason:ignore-start -->";
|
|
383
|
-
var IGNORE_END = "<!-- mason:ignore-end -->";
|
|
467
|
+
import MarkdownIt from "markdown-it";
|
|
384
468
|
function normalizePathToken(token) {
|
|
385
469
|
let t = token.trim();
|
|
386
470
|
if (!t) return null;
|
|
@@ -444,11 +528,34 @@ function extractClaims(content) {
|
|
|
444
528
|
const counts = [];
|
|
445
529
|
const commands = /* @__PURE__ */ new Map();
|
|
446
530
|
const addPath = (claim) => {
|
|
447
|
-
|
|
531
|
+
const key = [claim.relativeTo ?? "scope", claim.path].join(":");
|
|
532
|
+
if (!paths.has(key)) paths.set(key, claim);
|
|
448
533
|
};
|
|
449
534
|
const addCommand = (claim) => {
|
|
450
|
-
|
|
535
|
+
const key = JSON.stringify([claim.scriptName, claim.directory, claim.scopeUnknown]);
|
|
536
|
+
if (!commands.has(key)) commands.set(key, claim);
|
|
451
537
|
};
|
|
538
|
+
const parsed = markdown.parse(lines.map((line, i) => ignored[i] ? "" : line).join("\n"), {});
|
|
539
|
+
for (const token of parsed) {
|
|
540
|
+
if (token.type !== "inline" || !token.map) continue;
|
|
541
|
+
let line = token.map[0] + 1;
|
|
542
|
+
for (const child of token.children ?? []) {
|
|
543
|
+
const target = child.type === "link_open" ? child.attrGet("href") : child.type === "image" ? child.attrGet("src") : null;
|
|
544
|
+
if (target && !/^(?:[a-z][a-z0-9+.-]*:|[\/#~])/i.test(target)) {
|
|
545
|
+
let pathname = null;
|
|
546
|
+
try {
|
|
547
|
+
pathname = decodeURIComponent(target.split(/[?#]/)[0]);
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
if (pathname && !/[$*?{}<>\\\x00-\x1f]/.test(pathname)) {
|
|
551
|
+
addPath({ path: pathname, line, excerpt: target, relativeTo: "document" });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (child.type === "softbreak" || child.type === "hardbreak") line++;
|
|
555
|
+
else line += child.content.split("\n").length - 1;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
let shellCwd;
|
|
452
559
|
let inFence = false;
|
|
453
560
|
let fenceInfo = "";
|
|
454
561
|
let fenceMarker = "";
|
|
@@ -479,6 +586,7 @@ function extractClaims(content) {
|
|
|
479
586
|
fenceMarker = fenceMatch[1][0];
|
|
480
587
|
fenceInfo = fenceMatch[2].trim().toLowerCase();
|
|
481
588
|
blockLines = [];
|
|
589
|
+
shellCwd = void 0;
|
|
482
590
|
blockStartLine = lineNo + 1;
|
|
483
591
|
} else if (fenceMatch[1][0] === fenceMarker) {
|
|
484
592
|
inFence = false;
|
|
@@ -489,14 +597,9 @@ function extractClaims(content) {
|
|
|
489
597
|
if (inFence) {
|
|
490
598
|
blockLines.push(ignored[i] ? "" : line);
|
|
491
599
|
if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
invocation: m[0],
|
|
496
|
-
line: lineNo,
|
|
497
|
-
excerpt: m[0]
|
|
498
|
-
});
|
|
499
|
-
}
|
|
600
|
+
const scanned = commandClaims(line, lineNo, shellCwd, true);
|
|
601
|
+
shellCwd = scanned.cwd;
|
|
602
|
+
for (const claim of scanned.claims) addCommand(claim);
|
|
500
603
|
}
|
|
501
604
|
continue;
|
|
502
605
|
}
|
|
@@ -523,14 +626,10 @@ function extractClaims(content) {
|
|
|
523
626
|
excerpt: m[0]
|
|
524
627
|
});
|
|
525
628
|
}
|
|
526
|
-
for (const
|
|
527
|
-
addCommand(
|
|
528
|
-
scriptName: m[2],
|
|
529
|
-
invocation: m[0],
|
|
530
|
-
line: lineNo,
|
|
531
|
-
excerpt: m[0]
|
|
532
|
-
});
|
|
629
|
+
for (const match of line.matchAll(/`([^`]+)`/g)) {
|
|
630
|
+
for (const claim of commandClaims(match[1], lineNo, void 0, true).claims) addCommand(claim);
|
|
533
631
|
}
|
|
632
|
+
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims) addCommand(claim);
|
|
534
633
|
}
|
|
535
634
|
if (inFence) processBlock();
|
|
536
635
|
return {
|
|
@@ -539,12 +638,56 @@ function extractClaims(content) {
|
|
|
539
638
|
commands: [...commands.values()]
|
|
540
639
|
};
|
|
541
640
|
}
|
|
641
|
+
var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
|
|
642
|
+
var init_claims = __esm({
|
|
643
|
+
"src/audit/claims.ts"() {
|
|
644
|
+
"use strict";
|
|
645
|
+
init_commands();
|
|
646
|
+
init_tree();
|
|
647
|
+
ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
648
|
+
"package.json",
|
|
649
|
+
"package-lock.json",
|
|
650
|
+
"pnpm-workspace.yaml",
|
|
651
|
+
"tsconfig.json",
|
|
652
|
+
"tsup.config.ts",
|
|
653
|
+
"vitest.config.ts",
|
|
654
|
+
"Makefile",
|
|
655
|
+
"Dockerfile",
|
|
656
|
+
"docker-compose.yml",
|
|
657
|
+
"Cargo.toml",
|
|
658
|
+
"go.mod",
|
|
659
|
+
"go.sum",
|
|
660
|
+
"pyproject.toml",
|
|
661
|
+
"requirements.txt",
|
|
662
|
+
"Gemfile",
|
|
663
|
+
"composer.json",
|
|
664
|
+
"settings.gradle.kts",
|
|
665
|
+
"settings.gradle",
|
|
666
|
+
"build.gradle.kts",
|
|
667
|
+
"build.gradle",
|
|
668
|
+
"manifest.json",
|
|
669
|
+
"server.json",
|
|
670
|
+
"README.md",
|
|
671
|
+
"CHANGELOG.md",
|
|
672
|
+
"LICENSE",
|
|
673
|
+
"CLAUDE.md",
|
|
674
|
+
"AGENTS.md",
|
|
675
|
+
".gitignore",
|
|
676
|
+
".env.example"
|
|
677
|
+
]);
|
|
678
|
+
SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
|
|
679
|
+
markdown = new MarkdownIt("commonmark", { html: false });
|
|
680
|
+
COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
|
|
681
|
+
COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
|
|
682
|
+
IGNORE_LINE = "<!-- mason:ignore -->";
|
|
683
|
+
IGNORE_START = "<!-- mason:ignore-start -->";
|
|
684
|
+
IGNORE_END = "<!-- mason:ignore-end -->";
|
|
685
|
+
}
|
|
686
|
+
});
|
|
542
687
|
|
|
543
688
|
// src/audit/git.ts
|
|
544
689
|
import { execFile as execFile4 } from "child_process";
|
|
545
690
|
import { promisify as promisify4 } from "util";
|
|
546
|
-
var exec4 = promisify4(execFile4);
|
|
547
|
-
var COMMIT_FORMAT = "%H%x09%cI%x09%s";
|
|
548
691
|
function parseCommitLine(line) {
|
|
549
692
|
const parts = line.split(" ");
|
|
550
693
|
if (parts.length < 3 || !parts[0]) return null;
|
|
@@ -554,7 +697,7 @@ async function lastCommitOf(resolvedRoot, relPath) {
|
|
|
554
697
|
try {
|
|
555
698
|
const { stdout } = await exec4(
|
|
556
699
|
"git",
|
|
557
|
-
["log", "-1", `--format=${COMMIT_FORMAT}`, "--", relPath],
|
|
700
|
+
["log", "-1", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
558
701
|
{ cwd: resolvedRoot }
|
|
559
702
|
);
|
|
560
703
|
const line = stdout.trim().split("\n")[0];
|
|
@@ -573,7 +716,7 @@ async function deletingCommitOf(resolvedRoot, relPath) {
|
|
|
573
716
|
"--diff-filter=D",
|
|
574
717
|
`--format=${COMMIT_FORMAT}`,
|
|
575
718
|
"--",
|
|
576
|
-
relPath
|
|
719
|
+
`:(literal)${relPath}`
|
|
577
720
|
],
|
|
578
721
|
{ cwd: resolvedRoot }
|
|
579
722
|
);
|
|
@@ -587,7 +730,7 @@ async function firstCommitOf(resolvedRoot, relPath) {
|
|
|
587
730
|
try {
|
|
588
731
|
const { stdout } = await exec4(
|
|
589
732
|
"git",
|
|
590
|
-
["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", relPath],
|
|
733
|
+
["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
591
734
|
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
592
735
|
);
|
|
593
736
|
const line = stdout.trim().split("\n")[0];
|
|
@@ -624,164 +767,24 @@ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
|
|
|
624
767
|
return null;
|
|
625
768
|
}
|
|
626
769
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
".claude/CLAUDE.md"
|
|
634
|
-
];
|
|
635
|
-
async function isDirty(resolvedRoot, relPath) {
|
|
636
|
-
try {
|
|
637
|
-
const { stdout } = await exec5(
|
|
638
|
-
"git",
|
|
639
|
-
["status", "--porcelain", "--", relPath],
|
|
640
|
-
{ cwd: resolvedRoot }
|
|
641
|
-
);
|
|
642
|
-
return stdout.trim().length > 0;
|
|
643
|
-
} catch {
|
|
644
|
-
return false;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
async function discoverDocs(resolvedRoot) {
|
|
648
|
-
const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
|
|
649
|
-
let content;
|
|
650
|
-
try {
|
|
651
|
-
content = await fs4.readFile(path7.join(resolvedRoot, candidate), "utf-8");
|
|
652
|
-
} catch {
|
|
653
|
-
return null;
|
|
654
|
-
}
|
|
655
|
-
const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
|
|
656
|
-
return {
|
|
657
|
-
path: candidate,
|
|
658
|
-
content,
|
|
659
|
-
lineCount: content.split("\n").length,
|
|
660
|
-
lastCommit,
|
|
661
|
-
dirty,
|
|
662
|
-
claims: extractClaims(content)
|
|
663
|
-
};
|
|
664
|
-
}));
|
|
665
|
-
return docs.filter((doc) => doc !== null);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// src/audit/types.ts
|
|
669
|
-
var ALL_CHECKS = [
|
|
670
|
-
"deleted-reference",
|
|
671
|
-
"new-module",
|
|
672
|
-
"stale-count",
|
|
673
|
-
"dead-command",
|
|
674
|
-
"deps-changed",
|
|
675
|
-
"decision-anchor-drift"
|
|
676
|
-
];
|
|
677
|
-
|
|
678
|
-
// src/audit/checks/deleted-reference.ts
|
|
679
|
-
import fs5 from "fs/promises";
|
|
680
|
-
import path8 from "path";
|
|
681
|
-
async function exists(absPath) {
|
|
682
|
-
try {
|
|
683
|
-
await fs5.access(absPath);
|
|
684
|
-
return true;
|
|
685
|
-
} catch {
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
async function checkDeletedReferences(ctx) {
|
|
690
|
-
const result = emptyResult();
|
|
691
|
-
for (const doc of ctx.docs) {
|
|
692
|
-
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
693
|
-
const renames = /* @__PURE__ */ new Map();
|
|
694
|
-
for (const change of changes ?? []) {
|
|
695
|
-
if (change.status === "renamed" && change.previousPath) {
|
|
696
|
-
renames.set(change.previousPath, change.path);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
for (const claim of doc.claims.paths) {
|
|
700
|
-
if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
|
|
701
|
-
continue;
|
|
702
|
-
}
|
|
703
|
-
if (await exists(path8.join(ctx.root, claim.path))) continue;
|
|
704
|
-
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
705
|
-
const renamedTo = renames.get(claim.path) ?? null;
|
|
706
|
-
if (renamedTo) {
|
|
707
|
-
result.issues.push({
|
|
708
|
-
type: "deleted-reference",
|
|
709
|
-
message: `\`${claim.path}\` was renamed to \`${renamedTo}\``,
|
|
710
|
-
anchor,
|
|
711
|
-
confidence: "certain",
|
|
712
|
-
evidence: {
|
|
713
|
-
kind: "missing-path",
|
|
714
|
-
claimed: claim.path,
|
|
715
|
-
renamedTo,
|
|
716
|
-
deletedInCommit: null,
|
|
717
|
-
everTracked: true,
|
|
718
|
-
parentDirExists: true
|
|
719
|
-
}
|
|
720
|
-
});
|
|
721
|
-
continue;
|
|
722
|
-
}
|
|
723
|
-
const tracked = await lastCommitOf(ctx.root, claim.path);
|
|
724
|
-
if (tracked) {
|
|
725
|
-
const deleted = await deletingCommitOf(ctx.root, claim.path);
|
|
726
|
-
const detail = deleted ? ` \u2013 deleted in ${deleted.hash.slice(0, 7)} "${deleted.subject}" (${deleted.date.slice(0, 10)})` : "";
|
|
727
|
-
result.issues.push({
|
|
728
|
-
type: "deleted-reference",
|
|
729
|
-
message: `\`${claim.path}\` no longer exists${detail}`,
|
|
730
|
-
anchor,
|
|
731
|
-
confidence: "certain",
|
|
732
|
-
evidence: {
|
|
733
|
-
kind: "missing-path",
|
|
734
|
-
claimed: claim.path,
|
|
735
|
-
renamedTo: null,
|
|
736
|
-
deletedInCommit: deleted,
|
|
737
|
-
everTracked: true,
|
|
738
|
-
parentDirExists: await exists(
|
|
739
|
-
path8.join(ctx.root, path8.dirname(claim.path))
|
|
740
|
-
)
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
|
-
const parentDirExists = await exists(
|
|
746
|
-
path8.join(ctx.root, path8.dirname(claim.path))
|
|
747
|
-
);
|
|
748
|
-
if (!parentDirExists) continue;
|
|
749
|
-
const issue = {
|
|
750
|
-
type: "deleted-reference",
|
|
751
|
-
message: `\`${claim.path}\` does not exist (never tracked in git \u2013 possible typo or invented path)`,
|
|
752
|
-
anchor,
|
|
753
|
-
confidence: "likely",
|
|
754
|
-
evidence: {
|
|
755
|
-
kind: "missing-path",
|
|
756
|
-
claimed: claim.path,
|
|
757
|
-
renamedTo: null,
|
|
758
|
-
deletedInCommit: null,
|
|
759
|
-
everTracked: false,
|
|
760
|
-
parentDirExists: true
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
result.issues.push(issue);
|
|
764
|
-
}
|
|
770
|
+
var exec4, COMMIT_FORMAT;
|
|
771
|
+
var init_git = __esm({
|
|
772
|
+
"src/audit/git.ts"() {
|
|
773
|
+
"use strict";
|
|
774
|
+
exec4 = promisify4(execFile4);
|
|
775
|
+
COMMIT_FORMAT = "%H%x09%cI%x09%s";
|
|
765
776
|
}
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// src/audit/checks/new-module.ts
|
|
770
|
-
import fg3 from "fast-glob";
|
|
771
|
-
import fs7 from "fs/promises";
|
|
772
|
-
import path10 from "path";
|
|
777
|
+
});
|
|
773
778
|
|
|
774
779
|
// src/audit/inputs.ts
|
|
775
|
-
import
|
|
780
|
+
import fs4 from "fs/promises";
|
|
776
781
|
import * as nativeFs from "fs";
|
|
777
|
-
import
|
|
778
|
-
import { execFile as
|
|
779
|
-
import { promisify as
|
|
782
|
+
import path8 from "path";
|
|
783
|
+
import { execFile as execFile5 } from "child_process";
|
|
784
|
+
import { promisify as promisify5 } from "util";
|
|
780
785
|
import fg2 from "fast-glob";
|
|
781
|
-
var execute = promisify6(execFile6);
|
|
782
|
-
var MAX_INPUTS = 1e5;
|
|
783
786
|
async function auditInputPath(root, file) {
|
|
784
|
-
if (file === ".") return
|
|
787
|
+
if (file === ".") return fs4.realpath(root);
|
|
785
788
|
try {
|
|
786
789
|
return await storePath(root, file);
|
|
787
790
|
} catch (error) {
|
|
@@ -797,7 +800,7 @@ async function readAuditInput(root, file) {
|
|
|
797
800
|
if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
|
|
798
801
|
return value;
|
|
799
802
|
} catch (error) {
|
|
800
|
-
if (error.code
|
|
803
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
|
|
801
804
|
throw error;
|
|
802
805
|
}
|
|
803
806
|
}
|
|
@@ -805,7 +808,7 @@ async function auditGlob(root, patterns, options) {
|
|
|
805
808
|
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
806
809
|
const directories = /* @__PURE__ */ new Set();
|
|
807
810
|
const readdir2 = ((directory, opts, callback) => {
|
|
808
|
-
const relative =
|
|
811
|
+
const relative = path8.relative(root, directory).split(path8.sep).join("/") || ".";
|
|
809
812
|
directories.add(relative);
|
|
810
813
|
if (directories.size > MAX_INPUTS) {
|
|
811
814
|
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
@@ -816,7 +819,7 @@ async function auditGlob(root, patterns, options) {
|
|
|
816
819
|
}, callback);
|
|
817
820
|
});
|
|
818
821
|
const lstat2 = ((file, callback) => {
|
|
819
|
-
const relative =
|
|
822
|
+
const relative = path8.relative(root, file).split(path8.sep).join("/") || ".";
|
|
820
823
|
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
821
824
|
});
|
|
822
825
|
const stream = fg2.stream(patterns, {
|
|
@@ -844,7 +847,7 @@ async function auditGlob(root, patterns, options) {
|
|
|
844
847
|
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
845
848
|
return counts;
|
|
846
849
|
}, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
847
|
-
throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir,
|
|
850
|
+
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.`);
|
|
848
851
|
}
|
|
849
852
|
}
|
|
850
853
|
} finally {
|
|
@@ -885,100 +888,351 @@ async function gitIgnoredPaths(root, files) {
|
|
|
885
888
|
throw error;
|
|
886
889
|
}
|
|
887
890
|
}
|
|
891
|
+
var execute, MAX_INPUTS;
|
|
892
|
+
var init_inputs = __esm({
|
|
893
|
+
"src/audit/inputs.ts"() {
|
|
894
|
+
"use strict";
|
|
895
|
+
init_files();
|
|
896
|
+
init_storage();
|
|
897
|
+
execute = promisify5(execFile5);
|
|
898
|
+
MAX_INPUTS = 1e5;
|
|
899
|
+
}
|
|
900
|
+
});
|
|
888
901
|
|
|
889
|
-
// src/audit/
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
".vscode",
|
|
907
|
-
".idea"
|
|
908
|
-
]);
|
|
909
|
-
var SECOND_LEVEL_MIN_SOURCE_FILES = 2;
|
|
910
|
-
var ENUMERATION_THRESHOLD = 2;
|
|
911
|
-
function escapeRegExp(text2) {
|
|
912
|
-
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
913
|
-
}
|
|
914
|
-
function isMentioned(combinedDocs, name) {
|
|
915
|
-
const re = new RegExp(
|
|
916
|
-
`(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,
|
|
917
|
-
"im"
|
|
918
|
-
);
|
|
919
|
-
return re.test(combinedDocs);
|
|
902
|
+
// src/audit/docs.ts
|
|
903
|
+
import fs5 from "fs/promises";
|
|
904
|
+
import path9 from "path";
|
|
905
|
+
import { execFile as execFile6 } from "child_process";
|
|
906
|
+
import { promisify as promisify6 } from "util";
|
|
907
|
+
import fg3 from "fast-glob";
|
|
908
|
+
async function isDirty(resolvedRoot, relPath) {
|
|
909
|
+
try {
|
|
910
|
+
const { stdout } = await exec5(
|
|
911
|
+
"git",
|
|
912
|
+
["status", "--porcelain", "--", `:(literal)${relPath}`],
|
|
913
|
+
{ cwd: resolvedRoot }
|
|
914
|
+
);
|
|
915
|
+
return stdout.trim().length > 0;
|
|
916
|
+
} catch {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
920
919
|
}
|
|
921
|
-
async function
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
920
|
+
async function discoverDocPaths(root) {
|
|
921
|
+
let candidates = [];
|
|
922
|
+
try {
|
|
923
|
+
const { stdout } = await exec5(
|
|
924
|
+
"git",
|
|
925
|
+
[
|
|
926
|
+
"ls-files",
|
|
927
|
+
"-z",
|
|
928
|
+
"--cached",
|
|
929
|
+
"--others",
|
|
930
|
+
"--exclude-standard",
|
|
931
|
+
"--",
|
|
932
|
+
":(icase,glob)**/readme.md",
|
|
933
|
+
":(icase,glob)**/agents.md",
|
|
934
|
+
":(icase,glob)**/claude.md"
|
|
935
|
+
],
|
|
936
|
+
{ cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
|
|
937
|
+
);
|
|
938
|
+
candidates = stdout.split("\0").filter(Boolean);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
try {
|
|
941
|
+
await exec5("git", ["rev-parse", "--git-dir"], { cwd: root });
|
|
942
|
+
} catch {
|
|
943
|
+
return localDocPaths(root);
|
|
944
|
+
}
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
947
|
+
candidates.push(...await localDocPaths(root));
|
|
948
|
+
const config = await loadProjectConfig(root);
|
|
949
|
+
const excluded = [
|
|
950
|
+
"**/node_modules/**",
|
|
951
|
+
"**/vendor/**",
|
|
952
|
+
"**/dist/**",
|
|
953
|
+
"**/build/**",
|
|
954
|
+
"**/target/**",
|
|
955
|
+
"**/.git/**",
|
|
956
|
+
"**/.venv/**",
|
|
957
|
+
"**/venv/**",
|
|
958
|
+
".mason/**",
|
|
959
|
+
...config.ignore ?? []
|
|
960
|
+
];
|
|
961
|
+
const paths = [];
|
|
962
|
+
const unique = [...new Set(candidates)].sort();
|
|
963
|
+
for (let offset = 0; offset < unique.length; offset += 256) {
|
|
964
|
+
paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg3.escapePath(p)), {
|
|
965
|
+
ignore: excluded,
|
|
966
|
+
followSymbolicLinks: false,
|
|
967
|
+
label: "Documentation discovery"
|
|
968
|
+
}));
|
|
969
|
+
if (paths.length > 1e4) throw new Error("Documentation discovery exceeds 10,000 relevant files; narrow .mason/config.json ignore patterns.");
|
|
970
|
+
}
|
|
971
|
+
const priority = (file) => {
|
|
972
|
+
const index = DOC_CANDIDATES.findIndex((p) => p.toLowerCase() === file.toLowerCase());
|
|
973
|
+
return index < 0 ? DOC_CANDIDATES.length : index;
|
|
941
974
|
};
|
|
942
|
-
|
|
943
|
-
|
|
975
|
+
return paths.sort((a, b) => priority(a) - priority(b) || a.localeCompare(b));
|
|
976
|
+
}
|
|
977
|
+
async function localDocPaths(root) {
|
|
978
|
+
const paths = [];
|
|
979
|
+
for (const dir of [".", ".claude"]) {
|
|
980
|
+
let entries;
|
|
981
|
+
try {
|
|
982
|
+
entries = await fs5.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
983
|
+
} catch (error) {
|
|
984
|
+
if (error.code === "ENOENT") continue;
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
987
|
+
for (const entry of entries) if (/^(?:agents|claude|readme)\.md$/i.test(entry.name)) {
|
|
988
|
+
paths.push(path9.posix.join(dir, entry.name));
|
|
989
|
+
}
|
|
944
990
|
}
|
|
945
|
-
return
|
|
991
|
+
return paths;
|
|
946
992
|
}
|
|
947
|
-
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
993
|
+
function documentScope(file) {
|
|
994
|
+
const parent = path9.posix.dirname(file);
|
|
995
|
+
return path9.posix.basename(parent).toLowerCase() === ".claude" ? path9.posix.dirname(parent) : parent;
|
|
996
|
+
}
|
|
997
|
+
async function discoverDocs(resolvedRoot) {
|
|
998
|
+
const docs = [];
|
|
999
|
+
for (const candidate of await discoverDocPaths(resolvedRoot)) {
|
|
1000
|
+
const content = await readAuditInput(resolvedRoot, candidate);
|
|
1001
|
+
if (content === null) throw new Error("Document disappeared during discovery: " + candidate);
|
|
1002
|
+
const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
|
|
1003
|
+
docs.push({
|
|
1004
|
+
path: candidate,
|
|
1005
|
+
content,
|
|
1006
|
+
scope: documentScope(candidate),
|
|
1007
|
+
kind: /^readme\.md$/i.test(path9.posix.basename(candidate)) ? "readme" : "instructions",
|
|
1008
|
+
lineCount: content.split("\n").length,
|
|
1009
|
+
lastCommit,
|
|
1010
|
+
dirty,
|
|
1011
|
+
claims: extractClaims(content)
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
return docs;
|
|
1015
|
+
}
|
|
1016
|
+
var exec5, DOC_CANDIDATES;
|
|
1017
|
+
var init_docs = __esm({
|
|
1018
|
+
"src/audit/docs.ts"() {
|
|
1019
|
+
"use strict";
|
|
1020
|
+
init_claims();
|
|
1021
|
+
init_git();
|
|
1022
|
+
init_inputs();
|
|
1023
|
+
init_files();
|
|
1024
|
+
exec5 = promisify6(execFile6);
|
|
1025
|
+
DOC_CANDIDATES = [
|
|
1026
|
+
"AGENTS.md",
|
|
1027
|
+
"CLAUDE.md",
|
|
1028
|
+
".claude/CLAUDE.md"
|
|
1029
|
+
];
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
// src/audit/types.ts
|
|
1034
|
+
var ALL_CHECKS;
|
|
1035
|
+
var init_types = __esm({
|
|
1036
|
+
"src/audit/types.ts"() {
|
|
1037
|
+
"use strict";
|
|
1038
|
+
ALL_CHECKS = [
|
|
1039
|
+
"deleted-reference",
|
|
1040
|
+
"new-module",
|
|
1041
|
+
"stale-count",
|
|
1042
|
+
"dead-command",
|
|
1043
|
+
"deps-changed",
|
|
1044
|
+
"decision-anchor-drift"
|
|
1045
|
+
];
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// src/audit/scope.ts
|
|
1050
|
+
import fs6 from "fs/promises";
|
|
1051
|
+
import path10 from "path";
|
|
1052
|
+
function scopedPath(directory, value) {
|
|
1053
|
+
if (path10.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
|
|
1054
|
+
const resolved = path10.posix.normalize(path10.posix.join(directory, value));
|
|
1055
|
+
return resolved === ".." || resolved.startsWith("../") ? null : resolved;
|
|
1056
|
+
}
|
|
1057
|
+
function pathClaimScope(doc, claim) {
|
|
1058
|
+
const directory = claim.relativeTo === "document" ? path10.posix.dirname(doc) : documentScope(doc);
|
|
1059
|
+
const local = scopedPath(directory, claim.path);
|
|
1060
|
+
if (!local) return null;
|
|
1061
|
+
const candidates = claim.relativeTo === "document" || directory === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
|
|
1062
|
+
return {
|
|
1063
|
+
basis: claim.relativeTo === "document" ? "document-link" : directory === "." ? "repository" : "document",
|
|
1064
|
+
directory,
|
|
1065
|
+
candidates
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
async function pathExists(root, file) {
|
|
1069
|
+
try {
|
|
1070
|
+
await fs6.access(await auditInputPath(root, file));
|
|
1071
|
+
return true;
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return false;
|
|
1074
|
+
throw error;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function gitMetadataPath(file) {
|
|
1078
|
+
return file.split("/").includes(".git");
|
|
1079
|
+
}
|
|
1080
|
+
function optionalMasonPath(file) {
|
|
1081
|
+
return file.split("/").includes(".mason");
|
|
1082
|
+
}
|
|
1083
|
+
var init_scope = __esm({
|
|
1084
|
+
"src/audit/scope.ts"() {
|
|
1085
|
+
"use strict";
|
|
1086
|
+
init_inputs();
|
|
1087
|
+
init_docs();
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
// src/audit/checks/deleted-reference.ts
|
|
1092
|
+
import path11 from "path";
|
|
1093
|
+
async function checkDeletedReferences(ctx) {
|
|
1094
|
+
const result = emptyResult();
|
|
1095
|
+
for (const doc of ctx.docs) {
|
|
1096
|
+
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
1097
|
+
const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
|
|
1098
|
+
for (const claim of doc.claims.paths) {
|
|
1099
|
+
const scope = pathClaimScope(doc.path, claim);
|
|
1100
|
+
if (!scope) {
|
|
1101
|
+
result.skipped.push({
|
|
1102
|
+
check: "deleted-reference",
|
|
1103
|
+
doc: doc.path,
|
|
1104
|
+
reason: `Reference ${claim.excerpt} is outside the repository or has an unsupported path.`
|
|
1105
|
+
});
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (scope.candidates.some(optionalMasonPath)) continue;
|
|
1109
|
+
if (scope.candidates.some(gitMetadataPath)) {
|
|
1110
|
+
result.skipped.push({
|
|
1111
|
+
check: "deleted-reference",
|
|
1112
|
+
doc: doc.path,
|
|
1113
|
+
reason: `Reference ${claim.path} names Git metadata, whose layout varies between checkouts; it was not verified as a repository file.`
|
|
1114
|
+
});
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(Boolean)) continue;
|
|
1118
|
+
const candidates = await Promise.all(scope.candidates.map(async (file) => ({
|
|
1119
|
+
file,
|
|
1120
|
+
tracked: await lastCommitOf(ctx.root, file),
|
|
1121
|
+
parent: await pathExists(ctx.root, path11.posix.dirname(file))
|
|
1122
|
+
})));
|
|
1123
|
+
const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
|
|
1124
|
+
if (!candidate && claim.relativeTo !== "document") continue;
|
|
1125
|
+
const resolvedPath = candidate?.file ?? scope.candidates[0];
|
|
1126
|
+
const renamedTo = renames.get(resolvedPath) ?? null;
|
|
1127
|
+
const deletedInCommit = candidate?.tracked && !renamedTo ? await deletingCommitOf(ctx.root, resolvedPath) : null;
|
|
1128
|
+
const evidence = {
|
|
1129
|
+
kind: "missing-path",
|
|
1130
|
+
claimed: claim.path,
|
|
1131
|
+
resolvedPath,
|
|
1132
|
+
scope,
|
|
1133
|
+
renamedTo,
|
|
1134
|
+
deletedInCommit,
|
|
1135
|
+
everTracked: !!candidate?.tracked || !!renamedTo,
|
|
1136
|
+
parentDirExists: candidate?.parent ?? false
|
|
1137
|
+
};
|
|
1138
|
+
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
1139
|
+
const message = 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)})` : "");
|
|
1140
|
+
if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
|
|
1141
|
+
result.issues.push({ type: "deleted-reference", message, anchor, confidence: "certain", evidence });
|
|
1142
|
+
} else {
|
|
1143
|
+
result.advisories.push({ resolution: "recheck", type: "deleted-reference", message: message + (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 });
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return result;
|
|
1148
|
+
}
|
|
1149
|
+
var init_deleted_reference = __esm({
|
|
1150
|
+
"src/audit/checks/deleted-reference.ts"() {
|
|
1151
|
+
"use strict";
|
|
1152
|
+
init_git();
|
|
1153
|
+
init_scope();
|
|
1154
|
+
init_checks();
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// src/audit/checks/new-module.ts
|
|
1159
|
+
import fg4 from "fast-glob";
|
|
1160
|
+
import fs7 from "fs/promises";
|
|
1161
|
+
import path12 from "path";
|
|
1162
|
+
function escapeRegExp(text2) {
|
|
1163
|
+
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1164
|
+
}
|
|
1165
|
+
function isMentioned(combinedDocs, name) {
|
|
1166
|
+
const re = new RegExp(
|
|
1167
|
+
`(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,
|
|
1168
|
+
"im"
|
|
1169
|
+
);
|
|
1170
|
+
return re.test(combinedDocs);
|
|
1171
|
+
}
|
|
1172
|
+
async function checkNewModules(ctx) {
|
|
1173
|
+
const result = emptyResult();
|
|
1174
|
+
if (ctx.docs.length === 0) return result;
|
|
1175
|
+
const combinedDocs = moduleDocumentation(ctx.docs);
|
|
1176
|
+
const primaryDoc = ctx.docs[0].path;
|
|
1177
|
+
const checkedDocs = ctx.docs.map((d) => d.path);
|
|
1178
|
+
const flag = async (dir, sourceFileCount) => {
|
|
1179
|
+
result.advisories.push({
|
|
1180
|
+
resolution: "recheck",
|
|
1181
|
+
type: "new-module",
|
|
1182
|
+
message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in the discovered documentation; review whether it needs documenting`,
|
|
1183
|
+
anchor: { doc: primaryDoc, line: null, excerpt: dir },
|
|
1184
|
+
evidence: {
|
|
1185
|
+
kind: "unmentioned-dir",
|
|
1186
|
+
dir,
|
|
1187
|
+
sourceFileCount,
|
|
1188
|
+
firstCommit: await firstCommitOf(ctx.root, dir),
|
|
1189
|
+
checkedDocs
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
};
|
|
1193
|
+
for (const candidate of await moduleCandidates(ctx.root, combinedDocs)) {
|
|
1194
|
+
await flag(candidate.dir, candidate.sourceFileCount);
|
|
1195
|
+
}
|
|
1196
|
+
return result;
|
|
1197
|
+
}
|
|
1198
|
+
function moduleDocumentation(docs) {
|
|
1199
|
+
return docs.map((doc) => doc.content + "\n" + (path12.posix.dirname(doc.path) === "." ? "" : path12.posix.dirname(doc.path) + "/")).join("\n");
|
|
1200
|
+
}
|
|
1201
|
+
async function moduleCandidates(root, combinedDocs) {
|
|
1202
|
+
const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
|
|
1203
|
+
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
1204
|
+
const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
|
|
1205
|
+
const listSubdirs = async (dir) => {
|
|
1206
|
+
const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1207
|
+
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));
|
|
1208
|
+
const ignored = await gitIgnoredPaths(root, dirs);
|
|
1209
|
+
const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
|
|
1210
|
+
return (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
|
|
1211
|
+
ignore,
|
|
1212
|
+
onlyDirectories: true,
|
|
1213
|
+
label: "Module directory discovery",
|
|
960
1214
|
select: (file) => visible.has(file)
|
|
961
|
-
})).map((file) =>
|
|
1215
|
+
})).map((file) => path12.posix.basename(file));
|
|
962
1216
|
};
|
|
963
1217
|
const countSourceFiles = async (dir) => {
|
|
964
1218
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
965
|
-
let
|
|
1219
|
+
let count3 = 0;
|
|
966
1220
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
967
|
-
|
|
1221
|
+
count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
|
|
968
1222
|
ignore,
|
|
969
1223
|
followSymbolicLinks: false,
|
|
970
1224
|
label: `Source discovery in ${dir}`
|
|
971
1225
|
})).length;
|
|
972
|
-
if (
|
|
1226
|
+
if (count3 > 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.`);
|
|
973
1227
|
}
|
|
974
|
-
return
|
|
1228
|
+
return count3;
|
|
975
1229
|
};
|
|
976
1230
|
const candidates = [];
|
|
977
1231
|
for (const topDir of await listSubdirs(".")) {
|
|
978
1232
|
const topMentioned = isMentioned(combinedDocs, topDir);
|
|
979
1233
|
if (!topMentioned) {
|
|
980
|
-
const
|
|
981
|
-
if (
|
|
1234
|
+
const count3 = await countSourceFiles(topDir);
|
|
1235
|
+
if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
|
|
982
1236
|
continue;
|
|
983
1237
|
}
|
|
984
1238
|
const subdirs = await listSubdirs(topDir);
|
|
@@ -986,18 +1240,48 @@ async function moduleCandidates(root, combinedDocs) {
|
|
|
986
1240
|
if (mentioned.length < ENUMERATION_THRESHOLD) continue;
|
|
987
1241
|
for (const sub of subdirs) {
|
|
988
1242
|
if (isMentioned(combinedDocs, sub)) continue;
|
|
989
|
-
const
|
|
990
|
-
if (
|
|
991
|
-
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount:
|
|
1243
|
+
const count3 = await countSourceFiles(`${topDir}/${sub}`);
|
|
1244
|
+
if (count3 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
|
|
1245
|
+
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count3 });
|
|
992
1246
|
}
|
|
993
1247
|
}
|
|
994
1248
|
}
|
|
995
1249
|
return candidates;
|
|
996
1250
|
}
|
|
1251
|
+
var DIR_DENYLIST, SECOND_LEVEL_MIN_SOURCE_FILES, ENUMERATION_THRESHOLD;
|
|
1252
|
+
var init_new_module = __esm({
|
|
1253
|
+
"src/audit/checks/new-module.ts"() {
|
|
1254
|
+
"use strict";
|
|
1255
|
+
init_files();
|
|
1256
|
+
init_inputs();
|
|
1257
|
+
init_git();
|
|
1258
|
+
init_checks();
|
|
1259
|
+
DIR_DENYLIST = /* @__PURE__ */ new Set([
|
|
1260
|
+
"node_modules",
|
|
1261
|
+
"dist",
|
|
1262
|
+
"build",
|
|
1263
|
+
"out",
|
|
1264
|
+
"coverage",
|
|
1265
|
+
"target",
|
|
1266
|
+
"vendor",
|
|
1267
|
+
"__pycache__",
|
|
1268
|
+
"venv",
|
|
1269
|
+
".venv",
|
|
1270
|
+
".git",
|
|
1271
|
+
".gradle",
|
|
1272
|
+
".mason",
|
|
1273
|
+
".claude",
|
|
1274
|
+
".github",
|
|
1275
|
+
".vscode",
|
|
1276
|
+
".idea"
|
|
1277
|
+
]);
|
|
1278
|
+
SECOND_LEVEL_MIN_SOURCE_FILES = 2;
|
|
1279
|
+
ENUMERATION_THRESHOLD = 2;
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
997
1282
|
|
|
998
1283
|
// src/audit/checks/stale-count.ts
|
|
999
|
-
import
|
|
1000
|
-
var MEMBERS_CAP = 50;
|
|
1284
|
+
import path13 from "path";
|
|
1001
1285
|
async function countGradleModules(root) {
|
|
1002
1286
|
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
1003
1287
|
const content = await readAuditInput(root, name);
|
|
@@ -1031,7 +1315,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1031
1315
|
return {
|
|
1032
1316
|
actual: matched.length,
|
|
1033
1317
|
countedFrom: "package.json workspaces",
|
|
1034
|
-
members: matched.map((m) =>
|
|
1318
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1035
1319
|
};
|
|
1036
1320
|
}
|
|
1037
1321
|
}
|
|
@@ -1062,7 +1346,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1062
1346
|
return {
|
|
1063
1347
|
actual: matched.length,
|
|
1064
1348
|
countedFrom: "pnpm-workspace.yaml",
|
|
1065
|
-
members: matched.map((m) =>
|
|
1349
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1066
1350
|
};
|
|
1067
1351
|
}
|
|
1068
1352
|
}
|
|
@@ -1084,8 +1368,8 @@ async function countCargoCrates(root) {
|
|
|
1084
1368
|
ignore: ["**/target/**"],
|
|
1085
1369
|
label: "Cargo workspace discovery"
|
|
1086
1370
|
});
|
|
1087
|
-
for (const m of matched) members.add(
|
|
1088
|
-
} else if (await readAuditInput(root,
|
|
1371
|
+
for (const m of matched) members.add(path13.dirname(m));
|
|
1372
|
+
} else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
|
|
1089
1373
|
members.add(entry);
|
|
1090
1374
|
}
|
|
1091
1375
|
}
|
|
@@ -1103,11 +1387,21 @@ async function resolveCountSource(root, claim) {
|
|
|
1103
1387
|
if (unit === "crate") return countCargoCrates(root);
|
|
1104
1388
|
return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
|
|
1105
1389
|
}
|
|
1390
|
+
async function resolveDocCountSource(root, doc, claim) {
|
|
1391
|
+
const scope = documentScope(doc);
|
|
1392
|
+
const source = await resolveCountSource(path13.join(root, scope), claim);
|
|
1393
|
+
if (!source || scope === ".") return source;
|
|
1394
|
+
return {
|
|
1395
|
+
...source,
|
|
1396
|
+
countedFrom: scope + "/" + source.countedFrom,
|
|
1397
|
+
members: source.members.map((member) => scope + "/" + member)
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1106
1400
|
async function checkStaleCounts(ctx) {
|
|
1107
1401
|
const result = emptyResult();
|
|
1108
1402
|
for (const doc of ctx.docs) {
|
|
1109
1403
|
for (const claim of doc.claims.counts) {
|
|
1110
|
-
const source = await
|
|
1404
|
+
const source = await resolveDocCountSource(ctx.root, doc.path, claim);
|
|
1111
1405
|
if (source === null) {
|
|
1112
1406
|
result.skipped.push({
|
|
1113
1407
|
check: "stale-count",
|
|
@@ -1135,64 +1429,106 @@ async function checkStaleCounts(ctx) {
|
|
|
1135
1429
|
}
|
|
1136
1430
|
return result;
|
|
1137
1431
|
}
|
|
1432
|
+
var MEMBERS_CAP;
|
|
1433
|
+
var init_stale_count = __esm({
|
|
1434
|
+
"src/audit/checks/stale-count.ts"() {
|
|
1435
|
+
"use strict";
|
|
1436
|
+
init_docs();
|
|
1437
|
+
init_inputs();
|
|
1438
|
+
init_checks();
|
|
1439
|
+
MEMBERS_CAP = 50;
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1138
1442
|
|
|
1139
1443
|
// src/audit/checks/dead-command.ts
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
const raw = await readAuditInput(root, manifest);
|
|
1143
|
-
if (raw === null) return null;
|
|
1444
|
+
import path14 from "path";
|
|
1445
|
+
function scriptsOf(raw) {
|
|
1144
1446
|
try {
|
|
1145
1447
|
const pkg = JSON.parse(raw);
|
|
1146
|
-
|
|
1448
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
|
|
1449
|
+
if (pkg.scripts === void 0) return [];
|
|
1450
|
+
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;
|
|
1147
1451
|
} catch {
|
|
1148
1452
|
return null;
|
|
1149
1453
|
}
|
|
1150
1454
|
}
|
|
1151
|
-
async function
|
|
1152
|
-
const
|
|
1153
|
-
const
|
|
1154
|
-
(
|
|
1155
|
-
|
|
1156
|
-
if (commandClaims.length === 0) return result;
|
|
1157
|
-
const rootScripts = await scriptsOf(ctx.root, "package.json");
|
|
1158
|
-
if (rootScripts === null) {
|
|
1159
|
-
result.skipped.push({
|
|
1160
|
-
check: "dead-command",
|
|
1161
|
-
reason: "no package.json at the repo root"
|
|
1162
|
-
});
|
|
1163
|
-
return result;
|
|
1164
|
-
}
|
|
1165
|
-
const rootSet = new Set(rootScripts);
|
|
1166
|
-
let workspaceScripts = null;
|
|
1167
|
-
let manifestsChecked = ["package.json"];
|
|
1168
|
-
const loadWorkspaceScripts = async () => {
|
|
1169
|
-
if (workspaceScripts !== null) return workspaceScripts;
|
|
1170
|
-
workspaceScripts = /* @__PURE__ */ new Set();
|
|
1171
|
-
const manifests = await commandManifests(ctx.root);
|
|
1172
|
-
manifestsChecked = ["package.json", ...manifests.sort()];
|
|
1173
|
-
for (const manifest of manifests) {
|
|
1174
|
-
const scripts = await scriptsOf(ctx.root, manifest);
|
|
1175
|
-
for (const name of scripts ?? []) workspaceScripts.add(name);
|
|
1176
|
-
}
|
|
1177
|
-
return workspaceScripts;
|
|
1455
|
+
async function commandInputs(root, docs) {
|
|
1456
|
+
const contents = /* @__PURE__ */ new Map();
|
|
1457
|
+
const read = async (file) => {
|
|
1458
|
+
if (!contents.has(file)) contents.set(file, await readAuditInput(root, file));
|
|
1459
|
+
return contents.get(file);
|
|
1178
1460
|
};
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1461
|
+
await read("package.json");
|
|
1462
|
+
let inventory;
|
|
1463
|
+
const resolutions = [];
|
|
1464
|
+
for (const doc of docs) for (const claim of doc.claims.commands) {
|
|
1465
|
+
const base = documentScope(doc.path);
|
|
1466
|
+
const directory = scopedPath(base, claim.directory ?? ".");
|
|
1467
|
+
const scope = {
|
|
1468
|
+
basis: claim.directory === void 0 ? "document" : "explicit",
|
|
1469
|
+
directory: directory ?? base,
|
|
1470
|
+
candidates: []
|
|
1471
|
+
};
|
|
1472
|
+
const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
|
|
1473
|
+
resolutions.push(resolution);
|
|
1474
|
+
if (claim.scopeUnknown || directory === null) {
|
|
1475
|
+
resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
let cursor = directory;
|
|
1479
|
+
while (true) {
|
|
1480
|
+
const manifest = path14.posix.join(cursor, "package.json");
|
|
1481
|
+
scope.candidates.push(manifest);
|
|
1482
|
+
const raw = await read(manifest);
|
|
1483
|
+
if (raw !== null) {
|
|
1484
|
+
resolution.manifest = manifest;
|
|
1485
|
+
const scripts = scriptsOf(raw);
|
|
1486
|
+
if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
|
|
1487
|
+
else resolution.scripts = scripts;
|
|
1488
|
+
break;
|
|
1194
1489
|
}
|
|
1195
|
-
|
|
1490
|
+
if (claim.directory !== void 0 || cursor === ".") break;
|
|
1491
|
+
cursor = path14.posix.dirname(cursor);
|
|
1492
|
+
}
|
|
1493
|
+
if (!resolution.manifest && !resolution.problem) {
|
|
1494
|
+
resolution.problem = `No package.json for the command's scope (${directory}).`;
|
|
1495
|
+
}
|
|
1496
|
+
if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
|
|
1497
|
+
inventory ??= await commandManifests(root);
|
|
1498
|
+
for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
|
|
1499
|
+
if (scope.candidates.includes(manifest)) continue;
|
|
1500
|
+
scope.candidates.push(manifest);
|
|
1501
|
+
const raw = await read(manifest);
|
|
1502
|
+
if (raw === null) continue;
|
|
1503
|
+
const scripts = scriptsOf(raw);
|
|
1504
|
+
if (scripts === null) resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
|
|
1505
|
+
else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
|
|
1509
|
+
}
|
|
1510
|
+
async function checkDeadCommands(ctx) {
|
|
1511
|
+
const result = emptyResult();
|
|
1512
|
+
const { resolutions } = await commandInputs(ctx.root, ctx.docs);
|
|
1513
|
+
for (const { doc, claim, scope, scripts, elsewhere, problem, manifest } of resolutions) {
|
|
1514
|
+
if (problem) {
|
|
1515
|
+
result.skipped.push({ check: "dead-command", doc, reason: problem });
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
if (scripts.includes(claim.scriptName)) continue;
|
|
1519
|
+
const evidence = {
|
|
1520
|
+
kind: "missing-script",
|
|
1521
|
+
scriptName: claim.scriptName,
|
|
1522
|
+
invocation: claim.invocation,
|
|
1523
|
+
manifestsChecked: scope.candidates,
|
|
1524
|
+
availableScripts: scripts.slice(0, AVAILABLE_SCRIPTS_CAP),
|
|
1525
|
+
scope
|
|
1526
|
+
};
|
|
1527
|
+
const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
|
|
1528
|
+
const message = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
|
|
1529
|
+
const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
|
|
1530
|
+
if (certain) result.issues.push({ type: "dead-command", message, anchor, confidence: "certain", evidence });
|
|
1531
|
+
else result.advisories.push({ resolution: "recheck", type: "dead-command", message: message + (elsewhere.length ? `; it exists in ${elsewhere.join(", ")}. Clarify the intended working directory.` : "; the intended working directory needs review."), anchor, evidence });
|
|
1196
1532
|
}
|
|
1197
1533
|
return result;
|
|
1198
1534
|
}
|
|
@@ -1202,23 +1538,33 @@ function commandManifests(root) {
|
|
|
1202
1538
|
label: "Command manifest discovery"
|
|
1203
1539
|
});
|
|
1204
1540
|
}
|
|
1541
|
+
var AVAILABLE_SCRIPTS_CAP;
|
|
1542
|
+
var init_dead_command = __esm({
|
|
1543
|
+
"src/audit/checks/dead-command.ts"() {
|
|
1544
|
+
"use strict";
|
|
1545
|
+
init_inputs();
|
|
1546
|
+
init_docs();
|
|
1547
|
+
init_scope();
|
|
1548
|
+
init_checks();
|
|
1549
|
+
AVAILABLE_SCRIPTS_CAP = 30;
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1205
1552
|
|
|
1206
1553
|
// src/audit/release-metadata.ts
|
|
1207
1554
|
import { execFile as execFile7 } from "child_process";
|
|
1208
1555
|
import { promisify as promisify7 } from "util";
|
|
1209
|
-
var exec6 = promisify7(execFile7);
|
|
1210
1556
|
async function git(root, args) {
|
|
1211
1557
|
return (await exec6("git", args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
1212
1558
|
}
|
|
1213
1559
|
function withoutAndroidReleaseValues(text2) {
|
|
1214
1560
|
if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
|
|
1215
|
-
const
|
|
1561
|
+
const scopes2 = [];
|
|
1216
1562
|
const normalized = [];
|
|
1217
1563
|
let assignments = 0;
|
|
1218
1564
|
for (const line of text2.split("\n")) {
|
|
1219
1565
|
const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
|
|
1220
1566
|
const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
|
|
1221
|
-
if (assignment &&
|
|
1567
|
+
if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
|
|
1222
1568
|
normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
|
|
1223
1569
|
assignments++;
|
|
1224
1570
|
} else {
|
|
@@ -1227,12 +1573,12 @@ function withoutAndroidReleaseValues(text2) {
|
|
|
1227
1573
|
}
|
|
1228
1574
|
const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
|
|
1229
1575
|
for (const brace of code.matchAll(/[{}]/g)) {
|
|
1230
|
-
if (brace[0] === "{")
|
|
1231
|
-
else if (!
|
|
1232
|
-
else
|
|
1576
|
+
if (brace[0] === "{") scopes2.push(named ?? "unknown");
|
|
1577
|
+
else if (!scopes2.length) return null;
|
|
1578
|
+
else scopes2.pop();
|
|
1233
1579
|
}
|
|
1234
1580
|
}
|
|
1235
|
-
return assignments && !
|
|
1581
|
+
return assignments && !scopes2.length ? normalized.join("\n") : null;
|
|
1236
1582
|
}
|
|
1237
1583
|
async function releaseMetadataOnly(root, commit) {
|
|
1238
1584
|
if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
|
|
@@ -1254,23 +1600,21 @@ async function releaseMetadataOnly(root, commit) {
|
|
|
1254
1600
|
return false;
|
|
1255
1601
|
}
|
|
1256
1602
|
}
|
|
1603
|
+
var exec6;
|
|
1604
|
+
var init_release_metadata = __esm({
|
|
1605
|
+
"src/audit/release-metadata.ts"() {
|
|
1606
|
+
"use strict";
|
|
1607
|
+
exec6 = promisify7(execFile7);
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1257
1610
|
|
|
1258
1611
|
// src/audit/checks/deps-changed.ts
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
"
|
|
1262
|
-
"
|
|
1263
|
-
":(glob)
|
|
1264
|
-
|
|
1265
|
-
"settings.gradle",
|
|
1266
|
-
"gradle/libs.versions.toml",
|
|
1267
|
-
":(glob)**/Cargo.toml",
|
|
1268
|
-
"go.mod",
|
|
1269
|
-
"pyproject.toml",
|
|
1270
|
-
"requirements.txt",
|
|
1271
|
-
"Gemfile",
|
|
1272
|
-
"composer.json"
|
|
1273
|
-
];
|
|
1612
|
+
function manifestPathspecs(doc) {
|
|
1613
|
+
const scope = documentScope(doc);
|
|
1614
|
+
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
1615
|
+
const escaped = scope.replace(/[\\*?\[\]]/g, "\\$&");
|
|
1616
|
+
return MANIFEST_PATHSPECS.map((spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec);
|
|
1617
|
+
}
|
|
1274
1618
|
async function checkDepsChanged(ctx) {
|
|
1275
1619
|
const result = emptyResult();
|
|
1276
1620
|
result.suppressedAdvisories = [];
|
|
@@ -1294,7 +1638,7 @@ async function checkDepsChanged(ctx) {
|
|
|
1294
1638
|
const range = await commitsTouchingSince(
|
|
1295
1639
|
ctx.root,
|
|
1296
1640
|
doc.lastCommit.hash,
|
|
1297
|
-
|
|
1641
|
+
manifestPathspecs(doc.path)
|
|
1298
1642
|
);
|
|
1299
1643
|
if (range === null) {
|
|
1300
1644
|
result.skipped.push({
|
|
@@ -1329,112 +1673,49 @@ async function checkDepsChanged(ctx) {
|
|
|
1329
1673
|
}
|
|
1330
1674
|
return result;
|
|
1331
1675
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1676
|
+
var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
1677
|
+
var init_deps_changed = __esm({
|
|
1678
|
+
"src/audit/checks/deps-changed.ts"() {
|
|
1679
|
+
"use strict";
|
|
1680
|
+
init_docs();
|
|
1681
|
+
init_release_metadata();
|
|
1682
|
+
init_git();
|
|
1683
|
+
init_checks();
|
|
1684
|
+
MANIFEST_COMMITS_CAP = 10;
|
|
1685
|
+
MANIFEST_PATHSPECS = [
|
|
1686
|
+
":(glob)**/package.json",
|
|
1687
|
+
":(glob)**/build.gradle.kts",
|
|
1688
|
+
":(glob)**/build.gradle",
|
|
1689
|
+
"settings.gradle.kts",
|
|
1690
|
+
"settings.gradle",
|
|
1691
|
+
"gradle/libs.versions.toml",
|
|
1692
|
+
":(glob)**/Cargo.toml",
|
|
1693
|
+
"go.mod",
|
|
1694
|
+
"pyproject.toml",
|
|
1695
|
+
"requirements.txt",
|
|
1696
|
+
"Gemfile",
|
|
1697
|
+
"composer.json"
|
|
1698
|
+
];
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1341
1701
|
|
|
1342
1702
|
// src/context/lexical.ts
|
|
1343
|
-
import
|
|
1703
|
+
import path15 from "path";
|
|
1704
|
+
var init_lexical = __esm({
|
|
1705
|
+
"src/context/lexical.ts"() {
|
|
1706
|
+
"use strict";
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
// src/context/trust.ts
|
|
1711
|
+
var init_trust = __esm({
|
|
1712
|
+
"src/context/trust.ts"() {
|
|
1713
|
+
"use strict";
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1344
1716
|
|
|
1345
1717
|
// src/decisions/provenance.ts
|
|
1346
1718
|
import { z as z2 } from "zod";
|
|
1347
|
-
var text = (max) => z2.string().trim().min(1).max(max);
|
|
1348
|
-
var decisionSourceSchema = z2.object({
|
|
1349
|
-
kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
1350
|
-
reference: text(1e3),
|
|
1351
|
-
note: text(500).optional()
|
|
1352
|
-
}).strict();
|
|
1353
|
-
var attributionSchema = z2.object({
|
|
1354
|
-
owner: text(200).nullable().optional(),
|
|
1355
|
-
sources: z2.array(decisionSourceSchema).max(20).optional(),
|
|
1356
|
-
actor: text(200).optional()
|
|
1357
|
-
});
|
|
1358
|
-
var contentSchema = z2.object({
|
|
1359
|
-
title: z2.string().min(1),
|
|
1360
|
-
body: z2.string().min(1),
|
|
1361
|
-
category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
1362
|
-
files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
|
|
1363
|
-
owner: text(200).optional(),
|
|
1364
|
-
sources: z2.array(decisionSourceSchema).max(20)
|
|
1365
|
-
});
|
|
1366
|
-
var approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
|
|
1367
|
-
var statusSchema = z2.enum(["active", "superseded", "retired"]);
|
|
1368
|
-
var reviewEvidenceSchema = z2.object({
|
|
1369
|
-
baseHash: z2.string(),
|
|
1370
|
-
headHash: z2.string(),
|
|
1371
|
-
historyAvailable: z2.boolean(),
|
|
1372
|
-
changedFiles: z2.array(z2.string()),
|
|
1373
|
-
localChanges: z2.array(z2.string())
|
|
1374
|
-
});
|
|
1375
|
-
var eventSchema = z2.object({
|
|
1376
|
-
kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
|
|
1377
|
-
at: z2.string().datetime(),
|
|
1378
|
-
actor: text(200).optional(),
|
|
1379
|
-
note: text(1500).optional(),
|
|
1380
|
-
revision: z2.number().int().positive(),
|
|
1381
|
-
content: contentSchema,
|
|
1382
|
-
approval: approvalSchema,
|
|
1383
|
-
status: statusSchema,
|
|
1384
|
-
refreshedHash: z2.string(),
|
|
1385
|
-
evidence: reviewEvidenceSchema.optional()
|
|
1386
|
-
});
|
|
1387
|
-
var legacySchema = z2.object({
|
|
1388
|
-
version: z2.literal(1),
|
|
1389
|
-
id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
1390
|
-
title: z2.string().min(1),
|
|
1391
|
-
body: z2.string().min(1),
|
|
1392
|
-
category: contentSchema.shape.category,
|
|
1393
|
-
files: contentSchema.shape.files,
|
|
1394
|
-
createdAt: z2.string(),
|
|
1395
|
-
updatedAt: z2.string(),
|
|
1396
|
-
refreshedHash: z2.string(),
|
|
1397
|
-
status: z2.enum(["active", "superseded"]),
|
|
1398
|
-
supersededBy: z2.string().optional()
|
|
1399
|
-
}).passthrough();
|
|
1400
|
-
var currentSchema = legacySchema.extend({
|
|
1401
|
-
version: z2.literal(2),
|
|
1402
|
-
status: statusSchema,
|
|
1403
|
-
approval: approvalSchema,
|
|
1404
|
-
revision: z2.number().int().positive(),
|
|
1405
|
-
owner: text(200).optional(),
|
|
1406
|
-
sources: z2.array(decisionSourceSchema).max(20),
|
|
1407
|
-
history: z2.array(eventSchema).min(1)
|
|
1408
|
-
}).superRefine((record, ctx) => {
|
|
1409
|
-
const invalid = (message) => ctx.addIssue({ code: "custom", message });
|
|
1410
|
-
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1411
|
-
let previous;
|
|
1412
|
-
for (const event of record.history) {
|
|
1413
|
-
if (!previous) {
|
|
1414
|
-
if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
|
|
1415
|
-
if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
|
|
1416
|
-
} else {
|
|
1417
|
-
if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
|
|
1418
|
-
if (previous.status !== "active") invalid("Archived decisions cannot be changed");
|
|
1419
|
-
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
|
|
1420
|
-
if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
|
|
1421
|
-
if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
|
|
1422
|
-
if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
|
|
1423
|
-
const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
|
|
1424
|
-
if (event.approval !== approval) invalid("Approval disagrees with review history");
|
|
1425
|
-
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
|
|
1426
|
-
}
|
|
1427
|
-
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
|
|
1428
|
-
if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
|
|
1429
|
-
if (["accepted", "reaffirmed"].includes(event.kind)) {
|
|
1430
|
-
if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
|
|
1431
|
-
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");
|
|
1432
|
-
}
|
|
1433
|
-
previous = event;
|
|
1434
|
-
}
|
|
1435
|
-
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");
|
|
1436
|
-
});
|
|
1437
|
-
var decisionSchema = z2.union([legacySchema, currentSchema]);
|
|
1438
1719
|
function decisionContent(record) {
|
|
1439
1720
|
return {
|
|
1440
1721
|
title: record.title,
|
|
@@ -1478,20 +1759,111 @@ function decisionProvenance(record, freshness = "unknown") {
|
|
|
1478
1759
|
lastReview: review ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
|
|
1479
1760
|
};
|
|
1480
1761
|
}
|
|
1762
|
+
var text, decisionSourceSchema, attributionSchema, contentSchema, approvalSchema, statusSchema, reviewEvidenceSchema, eventSchema, legacySchema, currentSchema, decisionSchema;
|
|
1763
|
+
var init_provenance = __esm({
|
|
1764
|
+
"src/decisions/provenance.ts"() {
|
|
1765
|
+
"use strict";
|
|
1766
|
+
init_paths();
|
|
1767
|
+
init_trust();
|
|
1768
|
+
text = (max) => z2.string().trim().min(1).max(max);
|
|
1769
|
+
decisionSourceSchema = z2.object({
|
|
1770
|
+
kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
1771
|
+
reference: text(1e3),
|
|
1772
|
+
note: text(500).optional()
|
|
1773
|
+
}).strict();
|
|
1774
|
+
attributionSchema = z2.object({
|
|
1775
|
+
owner: text(200).nullable().optional(),
|
|
1776
|
+
sources: z2.array(decisionSourceSchema).max(20).optional(),
|
|
1777
|
+
actor: text(200).optional()
|
|
1778
|
+
});
|
|
1779
|
+
contentSchema = z2.object({
|
|
1780
|
+
title: z2.string().min(1),
|
|
1781
|
+
body: z2.string().min(1),
|
|
1782
|
+
category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
1783
|
+
files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
|
|
1784
|
+
owner: text(200).optional(),
|
|
1785
|
+
sources: z2.array(decisionSourceSchema).max(20)
|
|
1786
|
+
});
|
|
1787
|
+
approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
|
|
1788
|
+
statusSchema = z2.enum(["active", "superseded", "retired"]);
|
|
1789
|
+
reviewEvidenceSchema = z2.object({
|
|
1790
|
+
baseHash: z2.string(),
|
|
1791
|
+
headHash: z2.string(),
|
|
1792
|
+
historyAvailable: z2.boolean(),
|
|
1793
|
+
changedFiles: z2.array(z2.string()),
|
|
1794
|
+
localChanges: z2.array(z2.string())
|
|
1795
|
+
});
|
|
1796
|
+
eventSchema = z2.object({
|
|
1797
|
+
kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
|
|
1798
|
+
at: z2.string().datetime(),
|
|
1799
|
+
actor: text(200).optional(),
|
|
1800
|
+
note: text(1500).optional(),
|
|
1801
|
+
revision: z2.number().int().positive(),
|
|
1802
|
+
content: contentSchema,
|
|
1803
|
+
approval: approvalSchema,
|
|
1804
|
+
status: statusSchema,
|
|
1805
|
+
refreshedHash: z2.string(),
|
|
1806
|
+
evidence: reviewEvidenceSchema.optional()
|
|
1807
|
+
});
|
|
1808
|
+
legacySchema = z2.object({
|
|
1809
|
+
version: z2.literal(1),
|
|
1810
|
+
id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
1811
|
+
title: z2.string().min(1),
|
|
1812
|
+
body: z2.string().min(1),
|
|
1813
|
+
category: contentSchema.shape.category,
|
|
1814
|
+
files: contentSchema.shape.files,
|
|
1815
|
+
createdAt: z2.string(),
|
|
1816
|
+
updatedAt: z2.string(),
|
|
1817
|
+
refreshedHash: z2.string(),
|
|
1818
|
+
status: z2.enum(["active", "superseded"]),
|
|
1819
|
+
supersededBy: z2.string().optional()
|
|
1820
|
+
}).passthrough();
|
|
1821
|
+
currentSchema = legacySchema.extend({
|
|
1822
|
+
version: z2.literal(2),
|
|
1823
|
+
status: statusSchema,
|
|
1824
|
+
approval: approvalSchema,
|
|
1825
|
+
revision: z2.number().int().positive(),
|
|
1826
|
+
owner: text(200).optional(),
|
|
1827
|
+
sources: z2.array(decisionSourceSchema).max(20),
|
|
1828
|
+
history: z2.array(eventSchema).min(1)
|
|
1829
|
+
}).superRefine((record, ctx) => {
|
|
1830
|
+
const invalid = (message) => ctx.addIssue({ code: "custom", message });
|
|
1831
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1832
|
+
let previous;
|
|
1833
|
+
for (const event of record.history) {
|
|
1834
|
+
if (!previous) {
|
|
1835
|
+
if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
|
|
1836
|
+
if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
|
|
1837
|
+
} else {
|
|
1838
|
+
if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
|
|
1839
|
+
if (previous.status !== "active") invalid("Archived decisions cannot be changed");
|
|
1840
|
+
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
|
|
1841
|
+
if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
|
|
1842
|
+
if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
|
|
1843
|
+
if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
|
|
1844
|
+
const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
|
|
1845
|
+
if (event.approval !== approval) invalid("Approval disagrees with review history");
|
|
1846
|
+
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
|
|
1847
|
+
}
|
|
1848
|
+
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
|
|
1849
|
+
if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
|
|
1850
|
+
if (["accepted", "reaffirmed"].includes(event.kind)) {
|
|
1851
|
+
if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
|
|
1852
|
+
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");
|
|
1853
|
+
}
|
|
1854
|
+
previous = event;
|
|
1855
|
+
}
|
|
1856
|
+
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");
|
|
1857
|
+
});
|
|
1858
|
+
decisionSchema = z2.union([legacySchema, currentSchema]);
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1481
1861
|
|
|
1482
1862
|
// src/decisions/decisions.ts
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
errorMap: (issue, ctx) => ({ message: issue.code === "too_big" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError })
|
|
1488
|
-
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
1489
|
-
var decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
|
|
1490
|
-
var decisionBodySchema = boundedText(
|
|
1491
|
-
"body",
|
|
1492
|
-
BODY_MAX_CHARS,
|
|
1493
|
-
`Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
|
|
1494
|
-
);
|
|
1863
|
+
import fs8 from "fs/promises";
|
|
1864
|
+
import path16 from "path";
|
|
1865
|
+
import { createHash } from "crypto";
|
|
1866
|
+
import { z as z3 } from "zod";
|
|
1495
1867
|
async function loadDecisionStore(rootDir) {
|
|
1496
1868
|
const records = [];
|
|
1497
1869
|
const diagnostics = [];
|
|
@@ -1515,10 +1887,34 @@ async function loadDecisionStore(rootDir) {
|
|
|
1515
1887
|
}
|
|
1516
1888
|
return { records, diagnostics };
|
|
1517
1889
|
}
|
|
1890
|
+
var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, boundedText, decisionTitleSchema, decisionBodySchema;
|
|
1891
|
+
var init_decisions = __esm({
|
|
1892
|
+
"src/decisions/decisions.ts"() {
|
|
1893
|
+
"use strict";
|
|
1894
|
+
init_storage();
|
|
1895
|
+
init_paths();
|
|
1896
|
+
init_snapshot();
|
|
1897
|
+
init_lexical();
|
|
1898
|
+
init_provenance();
|
|
1899
|
+
TITLE_MAX_CHARS = 80;
|
|
1900
|
+
BODY_RECOMMENDED_CHARS = 1500;
|
|
1901
|
+
BODY_MAX_CHARS = 2500;
|
|
1902
|
+
boundedText = (field, max, hint) => z3.string({
|
|
1903
|
+
errorMap: (issue, ctx) => ({ message: issue.code === "too_big" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError })
|
|
1904
|
+
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
1905
|
+
decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
|
|
1906
|
+
decisionBodySchema = boundedText(
|
|
1907
|
+
"body",
|
|
1908
|
+
BODY_MAX_CHARS,
|
|
1909
|
+
`Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1518
1913
|
|
|
1519
1914
|
// src/decisions/drift.ts
|
|
1915
|
+
import path17 from "path";
|
|
1520
1916
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
1521
|
-
const resolvedRoot =
|
|
1917
|
+
const resolvedRoot = path17.resolve(rootDir);
|
|
1522
1918
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
1523
1919
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
1524
1920
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -1546,6 +1942,16 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
1546
1942
|
}
|
|
1547
1943
|
return report;
|
|
1548
1944
|
}
|
|
1945
|
+
var init_drift2 = __esm({
|
|
1946
|
+
"src/decisions/drift.ts"() {
|
|
1947
|
+
"use strict";
|
|
1948
|
+
init_drift();
|
|
1949
|
+
init_paths();
|
|
1950
|
+
init_snapshot();
|
|
1951
|
+
init_decisions();
|
|
1952
|
+
init_provenance();
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1549
1955
|
|
|
1550
1956
|
// src/audit/checks/decision-anchor.ts
|
|
1551
1957
|
async function checkDecisionAnchors(ctx) {
|
|
@@ -1589,187 +1995,50 @@ async function checkDecisionAnchors(ctx) {
|
|
|
1589
1995
|
}
|
|
1590
1996
|
return result;
|
|
1591
1997
|
}
|
|
1998
|
+
var init_decision_anchor = __esm({
|
|
1999
|
+
"src/audit/checks/decision-anchor.ts"() {
|
|
2000
|
+
"use strict";
|
|
2001
|
+
init_drift2();
|
|
2002
|
+
init_decisions();
|
|
2003
|
+
init_provenance();
|
|
2004
|
+
init_checks();
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
1592
2007
|
|
|
1593
2008
|
// src/audit/checks/index.ts
|
|
1594
|
-
var CHECKS = {
|
|
1595
|
-
"deleted-reference": checkDeletedReferences,
|
|
1596
|
-
"new-module": checkNewModules,
|
|
1597
|
-
"stale-count": checkStaleCounts,
|
|
1598
|
-
"dead-command": checkDeadCommands,
|
|
1599
|
-
"deps-changed": checkDepsChanged,
|
|
1600
|
-
"decision-anchor-drift": checkDecisionAnchors
|
|
1601
|
-
};
|
|
1602
2009
|
function emptyResult() {
|
|
1603
2010
|
return { issues: [], advisories: [], skipped: [] };
|
|
1604
2011
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
}
|
|
1623
|
-
decisionsChecked: false,
|
|
1624
|
-
issues: [],
|
|
1625
|
-
advisories: [],
|
|
1626
|
-
suppressedAdvisories: [],
|
|
1627
|
-
skippedChecks: [],
|
|
1628
|
-
clean: true
|
|
1629
|
-
};
|
|
1630
|
-
if (!report.gitAvailable) return report;
|
|
1631
|
-
const changesSinceDoc = /* @__PURE__ */ new Map();
|
|
1632
|
-
for (const doc of docs) {
|
|
1633
|
-
changesSinceDoc.set(
|
|
1634
|
-
doc.path,
|
|
1635
|
-
doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
|
|
1636
|
-
);
|
|
1637
|
-
}
|
|
1638
|
-
let decisionsPresent = false;
|
|
1639
|
-
try {
|
|
1640
|
-
await fs9.access(path15.join(resolvedRoot, ".mason", "decisions"));
|
|
1641
|
-
decisionsPresent = true;
|
|
1642
|
-
} catch {
|
|
1643
|
-
}
|
|
1644
|
-
report.decisionsChecked = decisionsPresent;
|
|
1645
|
-
const ctx = {
|
|
1646
|
-
root: resolvedRoot,
|
|
1647
|
-
docs,
|
|
1648
|
-
headHash,
|
|
1649
|
-
changesSinceDoc,
|
|
1650
|
-
decisionsPresent
|
|
1651
|
-
};
|
|
1652
|
-
const selected = options.checks ?? ALL_CHECKS;
|
|
1653
|
-
for (const name of ALL_CHECKS) {
|
|
1654
|
-
if (!selected.includes(name)) continue;
|
|
1655
|
-
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
|
|
1656
|
-
report.checksRun.push(name);
|
|
1657
|
-
report.issues.push(...issues);
|
|
1658
|
-
report.advisories.push(...advisories);
|
|
1659
|
-
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
1660
|
-
report.skippedChecks.push(...skipped);
|
|
2012
|
+
var CHECKS;
|
|
2013
|
+
var init_checks = __esm({
|
|
2014
|
+
"src/audit/checks/index.ts"() {
|
|
2015
|
+
"use strict";
|
|
2016
|
+
init_deleted_reference();
|
|
2017
|
+
init_new_module();
|
|
2018
|
+
init_stale_count();
|
|
2019
|
+
init_dead_command();
|
|
2020
|
+
init_deps_changed();
|
|
2021
|
+
init_decision_anchor();
|
|
2022
|
+
CHECKS = {
|
|
2023
|
+
"deleted-reference": checkDeletedReferences,
|
|
2024
|
+
"new-module": checkNewModules,
|
|
2025
|
+
"stale-count": checkStaleCounts,
|
|
2026
|
+
"dead-command": checkDeadCommands,
|
|
2027
|
+
"deps-changed": checkDepsChanged,
|
|
2028
|
+
"decision-anchor-drift": checkDecisionAnchors
|
|
2029
|
+
};
|
|
1661
2030
|
}
|
|
1662
|
-
|
|
1663
|
-
return report;
|
|
1664
|
-
}
|
|
2031
|
+
});
|
|
1665
2032
|
|
|
1666
|
-
// src/audit/
|
|
1667
|
-
import
|
|
1668
|
-
import path16 from "path";
|
|
1669
|
-
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
2033
|
+
// src/audit/findings.ts
|
|
2034
|
+
import { createHash as createHash2 } from "crypto";
|
|
1670
2035
|
import { z as z4 } from "zod";
|
|
1671
|
-
var checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
|
|
1672
|
-
var commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
|
|
1673
|
-
var anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
|
|
1674
|
-
var count = z4.number().int().nonnegative();
|
|
1675
|
-
var evidenceSchema = z4.discriminatedUnion("kind", [
|
|
1676
|
-
z4.object({
|
|
1677
|
-
kind: z4.literal("missing-path"),
|
|
1678
|
-
claimed: z4.string(),
|
|
1679
|
-
renamedTo: z4.string().nullable(),
|
|
1680
|
-
deletedInCommit: commitSchema.nullable(),
|
|
1681
|
-
everTracked: z4.boolean(),
|
|
1682
|
-
parentDirExists: z4.boolean()
|
|
1683
|
-
}),
|
|
1684
|
-
z4.object({
|
|
1685
|
-
kind: z4.literal("unmentioned-dir"),
|
|
1686
|
-
dir: z4.string(),
|
|
1687
|
-
sourceFileCount: count,
|
|
1688
|
-
firstCommit: commitSchema.nullable(),
|
|
1689
|
-
checkedDocs: z4.array(z4.string())
|
|
1690
|
-
}),
|
|
1691
|
-
z4.object({
|
|
1692
|
-
kind: z4.literal("count-mismatch"),
|
|
1693
|
-
claimed: count,
|
|
1694
|
-
actual: count,
|
|
1695
|
-
unit: z4.string(),
|
|
1696
|
-
countedFrom: z4.string(),
|
|
1697
|
-
members: z4.array(z4.string())
|
|
1698
|
-
}),
|
|
1699
|
-
z4.object({
|
|
1700
|
-
kind: z4.literal("missing-script"),
|
|
1701
|
-
scriptName: z4.string(),
|
|
1702
|
-
invocation: z4.string(),
|
|
1703
|
-
manifestsChecked: z4.array(z4.string()),
|
|
1704
|
-
availableScripts: z4.array(z4.string())
|
|
1705
|
-
}),
|
|
1706
|
-
z4.object({
|
|
1707
|
-
kind: z4.literal("doc-behind-manifests"),
|
|
1708
|
-
docLastCommit: commitSchema,
|
|
1709
|
-
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
1710
|
-
totalCommits: count
|
|
1711
|
-
}),
|
|
1712
|
-
z4.object({
|
|
1713
|
-
kind: z4.literal("decision-anchor"),
|
|
1714
|
-
decisionId: z4.string(),
|
|
1715
|
-
title: z4.string(),
|
|
1716
|
-
changedFiles: z4.array(z4.string()),
|
|
1717
|
-
refreshedHash: z4.string(),
|
|
1718
|
-
provenance: z4.object({}).passthrough().optional()
|
|
1719
|
-
})
|
|
1720
|
-
]);
|
|
1721
|
-
var findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
1722
|
-
var issueSchema = findingSchema.extend({
|
|
1723
|
-
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
1724
|
-
confidence: z4.enum(["certain", "likely"])
|
|
1725
|
-
});
|
|
1726
|
-
var advisorySchema = findingSchema.extend({ type: z4.enum(["deps-changed", "decision-anchor-drift"]) });
|
|
1727
|
-
var checkResultSchema = z4.object({
|
|
1728
|
-
issues: z4.array(issueSchema),
|
|
1729
|
-
advisories: z4.array(advisorySchema),
|
|
1730
|
-
suppressedAdvisories: z4.array(advisorySchema).optional(),
|
|
1731
|
-
skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
|
|
1732
|
-
});
|
|
1733
|
-
var reportSchema = z4.object({
|
|
1734
|
-
version: z4.literal(1),
|
|
1735
|
-
root: z4.string(),
|
|
1736
|
-
gitAvailable: z4.literal(true),
|
|
1737
|
-
headHash: commitSchema.shape.hash,
|
|
1738
|
-
checksRun: z4.array(checkSchema).nonempty(),
|
|
1739
|
-
docs: z4.array(z4.object({
|
|
1740
|
-
path: z4.enum(DOC_CANDIDATES),
|
|
1741
|
-
lastCommit: commitSchema.nullable(),
|
|
1742
|
-
dirty: z4.boolean(),
|
|
1743
|
-
lineCount: count
|
|
1744
|
-
})).nonempty(),
|
|
1745
|
-
decisionsChecked: z4.boolean(),
|
|
1746
|
-
clean: z4.boolean(),
|
|
1747
|
-
issues: z4.array(issueSchema),
|
|
1748
|
-
advisories: z4.array(advisorySchema),
|
|
1749
|
-
suppressedAdvisories: z4.array(advisorySchema).optional(),
|
|
1750
|
-
skippedChecks: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
|
|
1751
|
-
});
|
|
1752
|
-
var baselineSchema = z4.object({
|
|
1753
|
-
kind: z4.literal("mason-audit-repair"),
|
|
1754
|
-
version: z4.literal(1),
|
|
1755
|
-
createdAt: z4.string().datetime(),
|
|
1756
|
-
report: reportSchema,
|
|
1757
|
-
digest: z4.string().regex(/^[a-f0-9]{64}$/)
|
|
1758
|
-
});
|
|
1759
|
-
var digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
1760
|
-
var repairFindingSchema = z4.object({
|
|
1761
|
-
id: z4.string(),
|
|
1762
|
-
original: z4.union([issueSchema, advisorySchema]),
|
|
1763
|
-
status: z4.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
1764
|
-
reason: z4.string(),
|
|
1765
|
-
current: z4.union([issueSchema, advisorySchema]).optional()
|
|
1766
|
-
});
|
|
1767
2036
|
function findingId(finding) {
|
|
1768
2037
|
const e = finding.evidence;
|
|
1769
2038
|
let key;
|
|
1770
2039
|
switch (e.kind) {
|
|
1771
2040
|
case "missing-path":
|
|
1772
|
-
key = e.claimed;
|
|
2041
|
+
key = e.resolvedPath && e.resolvedPath !== e.claimed ? [e.claimed, e.resolvedPath] : e.claimed;
|
|
1773
2042
|
break;
|
|
1774
2043
|
case "unmentioned-dir":
|
|
1775
2044
|
key = e.dir;
|
|
@@ -1778,7 +2047,7 @@ function findingId(finding) {
|
|
|
1778
2047
|
key = [e.unit.replace(/s$/, ""), e.countedFrom];
|
|
1779
2048
|
break;
|
|
1780
2049
|
case "missing-script":
|
|
1781
|
-
key = e.scriptName;
|
|
2050
|
+
key = e.scope && e.scope.directory !== "." ? [e.scriptName, e.scope.directory] : e.scriptName;
|
|
1782
2051
|
break;
|
|
1783
2052
|
case "doc-behind-manifests":
|
|
1784
2053
|
key = null;
|
|
@@ -1789,12 +2058,199 @@ function findingId(finding) {
|
|
|
1789
2058
|
}
|
|
1790
2059
|
return digest([finding.type, finding.anchor.doc, key]);
|
|
1791
2060
|
}
|
|
2061
|
+
var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema;
|
|
2062
|
+
var init_findings = __esm({
|
|
2063
|
+
"src/audit/findings.ts"() {
|
|
2064
|
+
"use strict";
|
|
2065
|
+
digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2066
|
+
checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
|
|
2067
|
+
commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
|
|
2068
|
+
anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
|
|
2069
|
+
count = z4.number().int().nonnegative();
|
|
2070
|
+
scopeSchema = z4.object({
|
|
2071
|
+
basis: z4.enum(["repository", "document", "document-link", "explicit"]),
|
|
2072
|
+
directory: z4.string(),
|
|
2073
|
+
candidates: z4.array(z4.string())
|
|
2074
|
+
});
|
|
2075
|
+
evidenceSchema = z4.discriminatedUnion("kind", [
|
|
2076
|
+
z4.object({
|
|
2077
|
+
kind: z4.literal("missing-path"),
|
|
2078
|
+
claimed: z4.string(),
|
|
2079
|
+
renamedTo: z4.string().nullable(),
|
|
2080
|
+
deletedInCommit: commitSchema.nullable(),
|
|
2081
|
+
everTracked: z4.boolean(),
|
|
2082
|
+
parentDirExists: z4.boolean(),
|
|
2083
|
+
resolvedPath: z4.string().optional(),
|
|
2084
|
+
scope: scopeSchema.optional()
|
|
2085
|
+
}),
|
|
2086
|
+
z4.object({
|
|
2087
|
+
kind: z4.literal("unmentioned-dir"),
|
|
2088
|
+
dir: z4.string(),
|
|
2089
|
+
sourceFileCount: count,
|
|
2090
|
+
firstCommit: commitSchema.nullable(),
|
|
2091
|
+
checkedDocs: z4.array(z4.string())
|
|
2092
|
+
}),
|
|
2093
|
+
z4.object({
|
|
2094
|
+
kind: z4.literal("count-mismatch"),
|
|
2095
|
+
claimed: count,
|
|
2096
|
+
actual: count,
|
|
2097
|
+
unit: z4.string(),
|
|
2098
|
+
countedFrom: z4.string(),
|
|
2099
|
+
members: z4.array(z4.string())
|
|
2100
|
+
}),
|
|
2101
|
+
z4.object({
|
|
2102
|
+
kind: z4.literal("missing-script"),
|
|
2103
|
+
scriptName: z4.string(),
|
|
2104
|
+
invocation: z4.string(),
|
|
2105
|
+
manifestsChecked: z4.array(z4.string()),
|
|
2106
|
+
availableScripts: z4.array(z4.string()),
|
|
2107
|
+
scope: scopeSchema.optional()
|
|
2108
|
+
}),
|
|
2109
|
+
z4.object({
|
|
2110
|
+
kind: z4.literal("doc-behind-manifests"),
|
|
2111
|
+
docLastCommit: commitSchema,
|
|
2112
|
+
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
2113
|
+
totalCommits: count
|
|
2114
|
+
}),
|
|
2115
|
+
z4.object({
|
|
2116
|
+
kind: z4.literal("decision-anchor"),
|
|
2117
|
+
decisionId: z4.string(),
|
|
2118
|
+
title: z4.string(),
|
|
2119
|
+
changedFiles: z4.array(z4.string()),
|
|
2120
|
+
refreshedHash: z4.string(),
|
|
2121
|
+
provenance: z4.object({}).passthrough().optional()
|
|
2122
|
+
})
|
|
2123
|
+
]);
|
|
2124
|
+
findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
2125
|
+
issueSchema = findingSchema.extend({
|
|
2126
|
+
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2127
|
+
confidence: z4.enum(["certain", "likely"])
|
|
2128
|
+
});
|
|
2129
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.literal("recheck").optional() });
|
|
2130
|
+
checkResultSchema = z4.object({
|
|
2131
|
+
issues: z4.array(issueSchema),
|
|
2132
|
+
advisories: z4.array(advisorySchema),
|
|
2133
|
+
suppressedAdvisories: z4.array(advisorySchema).optional(),
|
|
2134
|
+
skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
// src/automation/store.ts
|
|
2140
|
+
import fs9 from "fs/promises";
|
|
2141
|
+
import os from "os";
|
|
2142
|
+
import { z as z5 } from "zod";
|
|
2143
|
+
async function withLock(root, directory, run, waitMs = 5e3) {
|
|
2144
|
+
const file = await storePath(root, directory + "/lock", true);
|
|
2145
|
+
const deadline = Date.now() + waitMs;
|
|
2146
|
+
let handle;
|
|
2147
|
+
while (!handle) {
|
|
2148
|
+
try {
|
|
2149
|
+
handle = await fs9.open(file, "wx", 384);
|
|
2150
|
+
try {
|
|
2151
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
2152
|
+
} catch (error) {
|
|
2153
|
+
await handle.close().catch(() => {
|
|
2154
|
+
});
|
|
2155
|
+
handle = void 0;
|
|
2156
|
+
await fs9.rm(file, { force: true }).catch(() => {
|
|
2157
|
+
});
|
|
2158
|
+
throw error;
|
|
2159
|
+
}
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
if (error.code !== "EEXIST") throw error;
|
|
2162
|
+
try {
|
|
2163
|
+
const owner = JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2164
|
+
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
2165
|
+
try {
|
|
2166
|
+
process.kill(owner.pid, 0);
|
|
2167
|
+
} catch (probe) {
|
|
2168
|
+
if (probe.code === "ESRCH") {
|
|
2169
|
+
const reclaim = file + ".reclaim";
|
|
2170
|
+
let guard;
|
|
2171
|
+
try {
|
|
2172
|
+
guard = await fs9.open(reclaim, "wx", 384);
|
|
2173
|
+
const current = JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2174
|
+
if (current.pid === owner.pid && current.host === owner.host) await fs9.unlink(file);
|
|
2175
|
+
} finally {
|
|
2176
|
+
if (guard) {
|
|
2177
|
+
await guard.close();
|
|
2178
|
+
await fs9.rm(reclaim, { force: true });
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
} catch {
|
|
2185
|
+
}
|
|
2186
|
+
if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
|
|
2187
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
try {
|
|
2191
|
+
return await run();
|
|
2192
|
+
} finally {
|
|
2193
|
+
await handle.close();
|
|
2194
|
+
await fs9.unlink(file);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
var hostSchema, stateSchema;
|
|
2198
|
+
var init_store = __esm({
|
|
2199
|
+
"src/automation/store.ts"() {
|
|
2200
|
+
"use strict";
|
|
2201
|
+
init_storage();
|
|
2202
|
+
hostSchema = z5.enum(["claude", "codex"]);
|
|
2203
|
+
stateSchema = z5.object({
|
|
2204
|
+
version: z5.literal(1),
|
|
2205
|
+
root: z5.string(),
|
|
2206
|
+
gitDir: z5.string(),
|
|
2207
|
+
branch: z5.string(),
|
|
2208
|
+
baselines: z5.array(z5.object({ path: z5.string(), at: z5.string(), event: z5.string(), fingerprint: z5.string() })).max(128),
|
|
2209
|
+
sessions: z5.record(z5.object({
|
|
2210
|
+
host: hostSchema,
|
|
2211
|
+
seen: z5.string().nullable(),
|
|
2212
|
+
continued: z5.boolean(),
|
|
2213
|
+
initialIssues: z5.array(z5.string()),
|
|
2214
|
+
initialDocs: z5.record(z5.string().nullable()),
|
|
2215
|
+
lastUsed: z5.string(),
|
|
2216
|
+
mutationObserved: z5.boolean(),
|
|
2217
|
+
pending: z5.record(z5.string()),
|
|
2218
|
+
coverageGaps: z5.array(z5.string()),
|
|
2219
|
+
events: z5.record(z5.object({ at: z5.string(), count: z5.number().int().positive() }))
|
|
2220
|
+
})),
|
|
2221
|
+
updatedAt: z5.string(),
|
|
2222
|
+
fingerprint: z5.string().nullable(),
|
|
2223
|
+
latest: z5.string().nullable(),
|
|
2224
|
+
analysis: z5.object({ id: z5.string(), fingerprint: z5.string(), completedAt: z5.number(), path: z5.string() }).optional()
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
// src/audit/repair.ts
|
|
2230
|
+
var repair_exports = {};
|
|
2231
|
+
__export(repair_exports, {
|
|
2232
|
+
checkResultSchema: () => checkResultSchema,
|
|
2233
|
+
findingId: () => findingId,
|
|
2234
|
+
formatRepairSummary: () => formatRepairSummary,
|
|
2235
|
+
loadRepairBaseline: () => loadRepairBaseline,
|
|
2236
|
+
prepareRepair: () => prepareRepair,
|
|
2237
|
+
repairExitCode: () => repairExitCode,
|
|
2238
|
+
repairFindingSchema: () => repairFindingSchema,
|
|
2239
|
+
verifyRepair: () => verifyRepair
|
|
2240
|
+
});
|
|
2241
|
+
import fs10 from "fs/promises";
|
|
2242
|
+
import path18 from "path";
|
|
2243
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2244
|
+
import { z as z6 } from "zod";
|
|
2245
|
+
function recheckable(finding) {
|
|
2246
|
+
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);
|
|
2247
|
+
}
|
|
1792
2248
|
function allFindings(report) {
|
|
1793
2249
|
return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
|
|
1794
2250
|
}
|
|
1795
2251
|
async function docState(root) {
|
|
1796
2252
|
const docs = [];
|
|
1797
|
-
for (const doc of
|
|
2253
|
+
for (const doc of await discoverDocPaths(root)) {
|
|
1798
2254
|
try {
|
|
1799
2255
|
const content = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
|
|
1800
2256
|
if (content === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
|
|
@@ -1817,7 +2273,7 @@ async function stableAudit(root, checks, options = {}) {
|
|
|
1817
2273
|
}
|
|
1818
2274
|
async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
1819
2275
|
const root = await fs10.realpath(rootDir);
|
|
1820
|
-
const selected =
|
|
2276
|
+
const selected = z6.array(checkSchema).nonempty().parse(checks);
|
|
1821
2277
|
const report = await stableAudit(root, selected, options);
|
|
1822
2278
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
1823
2279
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
@@ -1832,23 +2288,29 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
|
1832
2288
|
await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
|
|
1833
2289
|
return { version: 1, action: "prepare", baselinePath, report };
|
|
1834
2290
|
}
|
|
1835
|
-
async function
|
|
2291
|
+
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
1836
2292
|
const root = await fs10.realpath(rootDir);
|
|
1837
|
-
const declaredRoot =
|
|
1838
|
-
const relative =
|
|
2293
|
+
const declaredRoot = path18.resolve(rootDir);
|
|
2294
|
+
const relative = path18.isAbsolute(baselinePath) ? path18.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
1839
2295
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
1840
2296
|
const { digest: savedDigest, ...payload } = stored;
|
|
1841
2297
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
1842
2298
|
if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
|
|
1843
2299
|
const original = stored.report;
|
|
2300
|
+
return { root, relative, original };
|
|
2301
|
+
}
|
|
2302
|
+
async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
2303
|
+
const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
|
|
1844
2304
|
const diagnostics = [];
|
|
2305
|
+
let reviewDigest;
|
|
1845
2306
|
let current = null;
|
|
1846
2307
|
try {
|
|
2308
|
+
reviewDigest = digest(await advisoryReviewInventory(root));
|
|
1847
2309
|
current = await stableAudit(root, original.checksRun, options);
|
|
1848
2310
|
if (!current) diagnostics.push("No context files remain available to audit.");
|
|
1849
2311
|
else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
|
|
1850
2312
|
for (const doc of original.docs) {
|
|
1851
|
-
if (!original.
|
|
2313
|
+
if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
|
|
1852
2314
|
const content = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
|
|
1853
2315
|
if (content === null || !content.trim()) {
|
|
1854
2316
|
diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
|
|
@@ -1865,21 +2327,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
1865
2327
|
const originalIds = new Set(originalFindings.map(findingId));
|
|
1866
2328
|
const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
|
|
1867
2329
|
for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
|
|
1868
|
-
const findings = originalFindings.map((finding) => {
|
|
2330
|
+
const findings = await Promise.all(originalFindings.map(async (finding) => {
|
|
1869
2331
|
const id = findingId(finding);
|
|
1870
2332
|
const now = currentById.get(id);
|
|
1871
2333
|
const base = { id, original: finding, ...now ? { current: now } : {} };
|
|
1872
2334
|
if (diagnostics.length || !current) {
|
|
1873
2335
|
return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
|
|
1874
2336
|
}
|
|
1875
|
-
if ("confidence" in finding
|
|
2337
|
+
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
1876
2338
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
1877
2339
|
}
|
|
1878
2340
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
1879
2341
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
1880
2342
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
1881
2343
|
}
|
|
1882
|
-
|
|
2344
|
+
const review = await assessAdvisory(root, now ?? finding, original.headHash);
|
|
2345
|
+
if (review) {
|
|
2346
|
+
if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
|
|
2347
|
+
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
2348
|
+
return { ...base, review, status: "review-required", reason: review.reason };
|
|
2349
|
+
}
|
|
2350
|
+
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
1883
2351
|
return {
|
|
1884
2352
|
...base,
|
|
1885
2353
|
status: "review-required",
|
|
@@ -1887,11 +2355,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
1887
2355
|
};
|
|
1888
2356
|
}
|
|
1889
2357
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
1890
|
-
});
|
|
2358
|
+
}));
|
|
2359
|
+
try {
|
|
2360
|
+
if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
|
|
2361
|
+
diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
|
|
2362
|
+
for (const finding of findings) {
|
|
2363
|
+
finding.status = "unverified";
|
|
2364
|
+
finding.reason = diagnostics.at(-1);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
diagnostics.push(String(error));
|
|
2369
|
+
for (const finding of findings) {
|
|
2370
|
+
finding.status = "unverified";
|
|
2371
|
+
finding.reason = "Advisory review evidence became unavailable during verification.";
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
1891
2374
|
const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
|
|
2375
|
+
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
1892
2376
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
1893
2377
|
for (const f of findings) counts[f.status]++;
|
|
1894
|
-
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f));
|
|
2378
|
+
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)));
|
|
1895
2379
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
1896
2380
|
return {
|
|
1897
2381
|
version: 1,
|
|
@@ -1905,7 +2389,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
1905
2389
|
diagnostics,
|
|
1906
2390
|
currentAudit: current,
|
|
1907
2391
|
counts,
|
|
1908
|
-
scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by
|
|
2392
|
+
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."
|
|
1909
2393
|
};
|
|
1910
2394
|
}
|
|
1911
2395
|
function repairExitCode(report) {
|
|
@@ -1914,18 +2398,482 @@ function repairExitCode(report) {
|
|
|
1914
2398
|
function formatRepairSummary(report) {
|
|
1915
2399
|
return [
|
|
1916
2400
|
"Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
|
|
1917
|
-
...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason),
|
|
2401
|
+
...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason + "\n findingId: " + f.id),
|
|
1918
2402
|
...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
|
|
1919
2403
|
...report.diagnostics.map((d) => " [unverified] " + d),
|
|
1920
2404
|
...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
|
|
1921
2405
|
report.scope
|
|
1922
2406
|
].join("\n");
|
|
1923
2407
|
}
|
|
2408
|
+
var count2, reportSchema, baselineSchema, repairFindingSchema;
|
|
2409
|
+
var init_repair = __esm({
|
|
2410
|
+
"src/audit/repair.ts"() {
|
|
2411
|
+
"use strict";
|
|
2412
|
+
init_audit();
|
|
2413
|
+
init_docs();
|
|
2414
|
+
init_snapshot();
|
|
2415
|
+
init_drift();
|
|
2416
|
+
init_storage();
|
|
2417
|
+
init_files();
|
|
2418
|
+
init_paths();
|
|
2419
|
+
init_types();
|
|
2420
|
+
init_findings();
|
|
2421
|
+
init_advisory_review();
|
|
2422
|
+
init_findings();
|
|
2423
|
+
count2 = z6.number().int().nonnegative();
|
|
2424
|
+
reportSchema = z6.object({
|
|
2425
|
+
version: z6.literal(1),
|
|
2426
|
+
root: z6.string(),
|
|
2427
|
+
gitAvailable: z6.literal(true),
|
|
2428
|
+
headHash: commitSchema.shape.hash,
|
|
2429
|
+
checksRun: z6.array(checkSchema).nonempty(),
|
|
2430
|
+
docs: z6.array(z6.object({
|
|
2431
|
+
path: z6.string().refine((value) => normalizeRepoPath(value) === value),
|
|
2432
|
+
scope: z6.string().optional(),
|
|
2433
|
+
kind: z6.enum(["instructions", "readme"]).optional(),
|
|
2434
|
+
lastCommit: commitSchema.nullable(),
|
|
2435
|
+
dirty: z6.boolean(),
|
|
2436
|
+
lineCount: count2
|
|
2437
|
+
})).nonempty(),
|
|
2438
|
+
decisionsChecked: z6.boolean(),
|
|
2439
|
+
clean: z6.boolean(),
|
|
2440
|
+
issues: z6.array(issueSchema),
|
|
2441
|
+
advisories: z6.array(advisorySchema),
|
|
2442
|
+
suppressedAdvisories: z6.array(advisorySchema).optional(),
|
|
2443
|
+
skippedChecks: z6.array(z6.object({ check: z6.string(), reason: z6.string(), doc: z6.string().optional() }))
|
|
2444
|
+
});
|
|
2445
|
+
baselineSchema = z6.object({
|
|
2446
|
+
kind: z6.literal("mason-audit-repair"),
|
|
2447
|
+
version: z6.literal(1),
|
|
2448
|
+
createdAt: z6.string().datetime(),
|
|
2449
|
+
report: reportSchema,
|
|
2450
|
+
digest: z6.string().regex(/^[a-f0-9]{64}$/)
|
|
2451
|
+
});
|
|
2452
|
+
repairFindingSchema = z6.object({
|
|
2453
|
+
id: z6.string(),
|
|
2454
|
+
original: z6.union([issueSchema, advisorySchema]),
|
|
2455
|
+
status: z6.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
2456
|
+
reason: z6.string(),
|
|
2457
|
+
current: z6.union([issueSchema, advisorySchema]).optional(),
|
|
2458
|
+
review: reviewSummarySchema.optional()
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2462
|
+
|
|
2463
|
+
// src/audit/advisory-review.ts
|
|
2464
|
+
import fs11 from "fs/promises";
|
|
2465
|
+
import { execFile as execFile8 } from "child_process";
|
|
2466
|
+
import { promisify as promisify8 } from "util";
|
|
2467
|
+
import { z as z7 } from "zod";
|
|
2468
|
+
async function readRecord(root, id) {
|
|
2469
|
+
const raw = await readStoreJson(root, recordPath(id));
|
|
2470
|
+
if (raw === null) return null;
|
|
2471
|
+
const record = recordSchema.parse(raw);
|
|
2472
|
+
const { digest: stored, ...payload } = record;
|
|
2473
|
+
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]))) {
|
|
2474
|
+
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2475
|
+
}
|
|
2476
|
+
return record;
|
|
2477
|
+
}
|
|
2478
|
+
function scopes(finding) {
|
|
2479
|
+
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2480
|
+
if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
2481
|
+
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
2482
|
+
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
2483
|
+
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
2484
|
+
else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
|
|
2485
|
+
else throw new Error("This advisory has no supported review scope.");
|
|
2486
|
+
return [...new Set(files)].map((file) => {
|
|
2487
|
+
if (normalizeRepoPath(file) !== file) throw new Error("Advisory contains an invalid scoped path: " + file);
|
|
2488
|
+
return `:(literal)${file}`;
|
|
2489
|
+
}).sort();
|
|
2490
|
+
}
|
|
2491
|
+
async function reviewEvidence(root, finding) {
|
|
2492
|
+
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
2493
|
+
const scope = scopes(finding);
|
|
2494
|
+
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2495
|
+
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2496
|
+
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
2497
|
+
const [tree, status, lastScopeCommit, document] = await Promise.all([
|
|
2498
|
+
tracked.length ? git2(root, "ls-tree", "-r", "-z", "HEAD", "--", ...tracked.map((file) => `:(literal)${file}`)) : "",
|
|
2499
|
+
git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
|
|
2500
|
+
git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
|
|
2501
|
+
readAuditInput(root, finding.anchor.doc)
|
|
2502
|
+
]);
|
|
2503
|
+
if (!document?.trim()) throw new Error("Original advisory document is missing, empty or unreadable.");
|
|
2504
|
+
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.");
|
|
2505
|
+
for (const entry of tree.split("\0").filter(Boolean)) {
|
|
2506
|
+
const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
|
|
2507
|
+
if (entry.startsWith("120000 ") || entry.startsWith("160000 ")) throw new Error("Symlink or submodule in advisory review scope: " + file);
|
|
2508
|
+
await storePath(root, file);
|
|
2509
|
+
}
|
|
2510
|
+
const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
|
|
2511
|
+
(finding.evidence.scope?.candidates ?? [finding.evidence.resolvedPath ?? finding.evidence.claimed]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
|
|
2512
|
+
) : [];
|
|
2513
|
+
let dirty = status.length > 0;
|
|
2514
|
+
if (finding.evidence.kind === "missing-script") {
|
|
2515
|
+
for (const file of [...finding.evidence.manifestsChecked, ...finding.evidence.scope?.candidates ?? []]) {
|
|
2516
|
+
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
|
|
2520
|
+
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
2521
|
+
return {
|
|
2522
|
+
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
2523
|
+
dirty,
|
|
2524
|
+
documentDigest: digest(document)
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
async function decisionAssessment(root, finding, baselineHead) {
|
|
2528
|
+
const evidence = finding.evidence;
|
|
2529
|
+
if (evidence.kind !== "decision-anchor") return null;
|
|
2530
|
+
const id = findingId(finding);
|
|
2531
|
+
const store = await loadDecisionStore(root);
|
|
2532
|
+
if (store.diagnostics.length) throw new Error("Decision records have unavailable or malformed evidence; review cannot be confirmed.");
|
|
2533
|
+
const record = store.records.find((record2) => record2.id === evidence.decisionId);
|
|
2534
|
+
if (!record || record.version !== 2) return null;
|
|
2535
|
+
const events = record.history.filter((event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind));
|
|
2536
|
+
const event = events.at(-1);
|
|
2537
|
+
if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length) return null;
|
|
2538
|
+
try {
|
|
2539
|
+
await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
|
|
2540
|
+
} catch {
|
|
2541
|
+
return null;
|
|
2542
|
+
}
|
|
2543
|
+
const drift = await computeDecisionDrift(root, [record]);
|
|
2544
|
+
if (!drift.historyAvailable) throw new Error("Decision review history is unavailable.");
|
|
2545
|
+
const retired = record.status === "retired" && event.kind === "retired";
|
|
2546
|
+
const current = retired || record.status === "active" && record.approval === "accepted" && drift.freshness?.[record.id] === "current" && !drift.pendingProposals?.[record.id];
|
|
2547
|
+
return {
|
|
2548
|
+
id,
|
|
2549
|
+
status: current ? "current" : "reopened",
|
|
2550
|
+
outcome: event.kind,
|
|
2551
|
+
reviewer: event.actor,
|
|
2552
|
+
note: event.note,
|
|
2553
|
+
reviewedHead: event.evidence.headHash,
|
|
2554
|
+
recordPath: `.mason/decisions/${record.id}.json`,
|
|
2555
|
+
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."
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
async function assessAdvisory(root, finding, baselineHead) {
|
|
2559
|
+
if ("confidence" in finding) return null;
|
|
2560
|
+
const id = findingId(finding);
|
|
2561
|
+
try {
|
|
2562
|
+
if (finding.evidence.kind === "decision-anchor") return await decisionAssessment(root, finding, baselineHead);
|
|
2563
|
+
const record = await readRecord(root, id);
|
|
2564
|
+
if (!record) return null;
|
|
2565
|
+
const event = record.events.at(-1);
|
|
2566
|
+
const current = await reviewEvidence(root, event.finding);
|
|
2567
|
+
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
2568
|
+
const base = {
|
|
2569
|
+
id,
|
|
2570
|
+
recordPath: recordPath(id),
|
|
2571
|
+
reviewer: event.reviewer,
|
|
2572
|
+
note: event.note,
|
|
2573
|
+
outcome: event.outcome,
|
|
2574
|
+
reviewedHead: event.evidence.head
|
|
2575
|
+
};
|
|
2576
|
+
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." };
|
|
2577
|
+
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." };
|
|
2578
|
+
} catch (error) {
|
|
2579
|
+
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
async function advisoryReviewInventory(root) {
|
|
2583
|
+
let names;
|
|
2584
|
+
try {
|
|
2585
|
+
names = await fs11.readdir(await storePath(root, ".mason/reviews/advisories"));
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
if (error.code === "ENOENT") return [];
|
|
2588
|
+
throw error;
|
|
2589
|
+
}
|
|
2590
|
+
if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
|
|
2591
|
+
return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
|
|
2592
|
+
}
|
|
2593
|
+
async function reviewAdvisory(rootDir, input) {
|
|
2594
|
+
const root = await fs11.realpath(rootDir), request = advisoryReviewRequest.parse(input);
|
|
2595
|
+
const { loadRepairBaseline: loadRepairBaseline2 } = await Promise.resolve().then(() => (init_repair(), repair_exports));
|
|
2596
|
+
const baseline = await loadRepairBaseline2(root, request.baselinePath);
|
|
2597
|
+
const finding = [...baseline.original.advisories, ...baseline.original.suppressedAdvisories ?? []].find((f) => findingId(f) === request.findingId);
|
|
2598
|
+
if (!finding) throw new Error("No advisory with this findingId in the original baseline. Provable issues must be repaired and rechecked.");
|
|
2599
|
+
if (finding.evidence.kind === "decision-anchor") return {
|
|
2600
|
+
status: "decision-review-required",
|
|
2601
|
+
decisionId: finding.evidence.decisionId,
|
|
2602
|
+
hint: "Use review_decision to prepare evidence and record authorized acceptance, reaffirmation or retirement, then verify this original repair baseline. An advisory assessment cannot approve a decision."
|
|
2603
|
+
};
|
|
2604
|
+
const prepare = async () => {
|
|
2605
|
+
const record = await readRecord(root, request.findingId);
|
|
2606
|
+
const evidence = await reviewEvidence(root, finding);
|
|
2607
|
+
await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
|
|
2608
|
+
const token = digest({ baseline: baseline.original, finding, evidence, record });
|
|
2609
|
+
return { record, evidence, token };
|
|
2610
|
+
};
|
|
2611
|
+
if (request.action === "prepare") {
|
|
2612
|
+
const state = await prepare();
|
|
2613
|
+
let diff = null, diffNote;
|
|
2614
|
+
const from = finding.evidence.kind === "doc-behind-manifests" ? finding.evidence.docLastCommit.hash : baseline.original.headHash;
|
|
2615
|
+
try {
|
|
2616
|
+
const full = await git2(root, "diff", "--no-ext-diff", "--no-textconv", "--no-color", "--no-renames", "-U3", from, state.evidence.head, "--", ...state.evidence.scope);
|
|
2617
|
+
diff = full.slice(0, 16e3);
|
|
2618
|
+
if (full.length > diff.length) diffNote = "Diff preview truncated; inspect the full scoped diff before reviewing.";
|
|
2619
|
+
} catch {
|
|
2620
|
+
diffNote = "Diff preview unavailable; inspect the original cited evidence directly before reviewing.";
|
|
2621
|
+
}
|
|
2622
|
+
if ((await prepare()).token !== state.token) throw new Error("Review evidence changed during preparation; retry.");
|
|
2623
|
+
return {
|
|
2624
|
+
status: "prepared",
|
|
2625
|
+
findingId: request.findingId,
|
|
2626
|
+
finding,
|
|
2627
|
+
evidence: state.evidence,
|
|
2628
|
+
history: state.record?.events ?? [],
|
|
2629
|
+
reviewToken: state.token,
|
|
2630
|
+
diff,
|
|
2631
|
+
diffNote,
|
|
2632
|
+
hint: "Inspect the original finding, scoped document and relevant code changes. Record addressed, inapplicable or deferred only when the user or cited project review authorizes that assessment. Supply the actual reviewer, reason in note and reviewToken. Relevant edits must be committed first. Deferral remains outstanding. Review identities are recorded assertions, not authenticated approvals."
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
if (!request.reviewer || !request.note || !request.reviewToken) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
|
|
2636
|
+
return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
|
|
2637
|
+
const state = await prepare();
|
|
2638
|
+
if (state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
|
|
2639
|
+
if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
|
|
2640
|
+
if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
|
|
2641
|
+
const { dirty, documentDigest, ...evidence } = state.evidence;
|
|
2642
|
+
const event = eventSchema2.parse({
|
|
2643
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2644
|
+
reviewer: request.reviewer,
|
|
2645
|
+
note: request.note,
|
|
2646
|
+
outcome: request.action,
|
|
2647
|
+
finding,
|
|
2648
|
+
evidence
|
|
2649
|
+
});
|
|
2650
|
+
const payload = { version: 1, id: request.findingId, events: [...state.record?.events ?? [], event] };
|
|
2651
|
+
if ((await prepare()).token !== state.token) throw new Error("Review conflict: evidence changed while recording the assessment.");
|
|
2652
|
+
await writeStoreJson(root, recordPath(request.findingId), { ...payload, digest: digest(payload) });
|
|
2653
|
+
return {
|
|
2654
|
+
status: "recorded",
|
|
2655
|
+
recordPath: recordPath(request.findingId),
|
|
2656
|
+
event,
|
|
2657
|
+
hint: "Review and commit this assessment record through the normal project workflow, then verify the original repair baseline. Relevant changes reopen the assessment; deferral remains outstanding."
|
|
2658
|
+
};
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
var execute2, git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2662
|
+
var init_advisory_review = __esm({
|
|
2663
|
+
"src/audit/advisory-review.ts"() {
|
|
2664
|
+
"use strict";
|
|
2665
|
+
init_findings();
|
|
2666
|
+
init_deps_changed();
|
|
2667
|
+
init_inputs();
|
|
2668
|
+
init_scope();
|
|
2669
|
+
init_storage();
|
|
2670
|
+
init_paths();
|
|
2671
|
+
init_store();
|
|
2672
|
+
init_decisions();
|
|
2673
|
+
init_drift2();
|
|
2674
|
+
execute2 = promisify8(execFile8);
|
|
2675
|
+
git2 = async (root, ...args) => (await execute2("git", args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
2676
|
+
hashSchema = z7.string().regex(/^[a-f0-9]{64}$/);
|
|
2677
|
+
commitSchema2 = z7.string().regex(/^[a-f0-9]{40,64}$/);
|
|
2678
|
+
evidenceSchema2 = z7.object({
|
|
2679
|
+
head: commitSchema2,
|
|
2680
|
+
fingerprint: hashSchema,
|
|
2681
|
+
scope: z7.array(z7.string()).min(1).max(1e3),
|
|
2682
|
+
tree: z7.string().max(2 * 1024 * 1024),
|
|
2683
|
+
lastScopeCommit: commitSchema2,
|
|
2684
|
+
paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
|
|
2685
|
+
});
|
|
2686
|
+
eventSchema2 = z7.object({
|
|
2687
|
+
at: z7.string().datetime(),
|
|
2688
|
+
reviewer: z7.string().trim().min(1).max(200),
|
|
2689
|
+
note: z7.string().trim().min(1).max(2500),
|
|
2690
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred"]),
|
|
2691
|
+
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2692
|
+
evidence: evidenceSchema2
|
|
2693
|
+
});
|
|
2694
|
+
recordSchema = z7.object({ version: z7.literal(1), id: hashSchema, events: z7.array(eventSchema2).min(1).max(200), digest: hashSchema });
|
|
2695
|
+
reviewSummarySchema = z7.object({
|
|
2696
|
+
id: hashSchema,
|
|
2697
|
+
status: z7.enum(["current", "reopened", "deferred", "unverified"]),
|
|
2698
|
+
reason: z7.string(),
|
|
2699
|
+
recordPath: z7.string().optional(),
|
|
2700
|
+
reviewer: z7.string().optional(),
|
|
2701
|
+
note: z7.string().optional(),
|
|
2702
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
2703
|
+
reviewedHead: z7.string().optional()
|
|
2704
|
+
});
|
|
2705
|
+
advisoryReviewRequest = z7.object({
|
|
2706
|
+
baselinePath: z7.string().min(1),
|
|
2707
|
+
findingId: hashSchema,
|
|
2708
|
+
action: z7.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
2709
|
+
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2710
|
+
note: eventSchema2.shape.note.optional(),
|
|
2711
|
+
reviewToken: hashSchema.optional()
|
|
2712
|
+
});
|
|
2713
|
+
recordPath = (id) => `.mason/reviews/advisories/${hashSchema.parse(id)}.json`;
|
|
2714
|
+
}
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
// src/audit/audit.ts
|
|
2718
|
+
import fs12 from "fs/promises";
|
|
2719
|
+
import path19 from "path";
|
|
2720
|
+
async function computeAudit(rootDir, options = {}) {
|
|
2721
|
+
const resolvedRoot = path19.resolve(rootDir);
|
|
2722
|
+
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2723
|
+
if (docs.length === 0) return null;
|
|
2724
|
+
const report = {
|
|
2725
|
+
version: 1,
|
|
2726
|
+
root: resolvedRoot,
|
|
2727
|
+
gitAvailable: headHash !== "unknown",
|
|
2728
|
+
headHash,
|
|
2729
|
+
checksRun: [],
|
|
2730
|
+
docs: docs.map((d) => ({
|
|
2731
|
+
path: d.path,
|
|
2732
|
+
scope: d.scope,
|
|
2733
|
+
kind: d.kind,
|
|
2734
|
+
lastCommit: d.lastCommit,
|
|
2735
|
+
dirty: d.dirty,
|
|
2736
|
+
lineCount: d.lineCount
|
|
2737
|
+
})),
|
|
2738
|
+
decisionsChecked: false,
|
|
2739
|
+
issues: [],
|
|
2740
|
+
advisories: [],
|
|
2741
|
+
suppressedAdvisories: [],
|
|
2742
|
+
skippedChecks: [],
|
|
2743
|
+
clean: true
|
|
2744
|
+
};
|
|
2745
|
+
if (!report.gitAvailable) return report;
|
|
2746
|
+
const changesSinceDoc = /* @__PURE__ */ new Map();
|
|
2747
|
+
for (const doc of docs) {
|
|
2748
|
+
changesSinceDoc.set(
|
|
2749
|
+
doc.path,
|
|
2750
|
+
doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
let decisionsPresent = false;
|
|
2754
|
+
try {
|
|
2755
|
+
await fs12.access(path19.join(resolvedRoot, ".mason", "decisions"));
|
|
2756
|
+
decisionsPresent = true;
|
|
2757
|
+
} catch {
|
|
2758
|
+
}
|
|
2759
|
+
report.decisionsChecked = decisionsPresent;
|
|
2760
|
+
const ctx = {
|
|
2761
|
+
root: resolvedRoot,
|
|
2762
|
+
docs,
|
|
2763
|
+
headHash,
|
|
2764
|
+
changesSinceDoc,
|
|
2765
|
+
decisionsPresent
|
|
2766
|
+
};
|
|
2767
|
+
const selected = options.checks ?? ALL_CHECKS;
|
|
2768
|
+
for (const name of ALL_CHECKS) {
|
|
2769
|
+
if (!selected.includes(name)) continue;
|
|
2770
|
+
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
|
|
2771
|
+
report.checksRun.push(name);
|
|
2772
|
+
report.issues.push(...issues);
|
|
2773
|
+
report.advisories.push(...advisories);
|
|
2774
|
+
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
2775
|
+
report.skippedChecks.push(...skipped);
|
|
2776
|
+
}
|
|
2777
|
+
report.clean = report.issues.length === 0;
|
|
2778
|
+
const reviews = (await Promise.all([...report.advisories, ...report.suppressedAdvisories].map((finding) => assessAdvisory(resolvedRoot, finding, headHash)))).filter((review) => review !== null);
|
|
2779
|
+
if (reviews.length) report.advisoryReviews = reviews;
|
|
2780
|
+
return report;
|
|
2781
|
+
}
|
|
2782
|
+
var init_audit = __esm({
|
|
2783
|
+
"src/audit/audit.ts"() {
|
|
2784
|
+
"use strict";
|
|
2785
|
+
init_drift();
|
|
2786
|
+
init_snapshot();
|
|
2787
|
+
init_docs();
|
|
2788
|
+
init_types();
|
|
2789
|
+
init_checks();
|
|
2790
|
+
init_advisory_review();
|
|
2791
|
+
}
|
|
2792
|
+
});
|
|
1924
2793
|
|
|
1925
2794
|
// src/audit/cli.ts
|
|
1926
|
-
|
|
2795
|
+
init_audit();
|
|
2796
|
+
init_types();
|
|
2797
|
+
init_repair();
|
|
2798
|
+
init_findings();
|
|
2799
|
+
import path21 from "path";
|
|
2800
|
+
|
|
2801
|
+
// src/audit/advisory-cli.ts
|
|
2802
|
+
init_advisory_review();
|
|
2803
|
+
import path20 from "path";
|
|
2804
|
+
import { stripVTControlCharacters } from "util";
|
|
2805
|
+
var USAGE = `Usage: mason audit review --baseline <path> --finding <id> [options]
|
|
1927
2806
|
|
|
1928
|
-
|
|
2807
|
+
Prepare an advisory assessment against retained evidence (read-only by default).
|
|
2808
|
+
--dir <path> Repository root (default: current directory)
|
|
2809
|
+
--baseline <path> Original repair baseline
|
|
2810
|
+
--finding <id> findingId from repair verification or automation
|
|
2811
|
+
--outcome <value> Record addressed, inapplicable, or deferred
|
|
2812
|
+
--reviewer <name> Actual authorized reviewer
|
|
2813
|
+
--note <reason> Assessment reason
|
|
2814
|
+
--token <token> reviewToken returned by preparation
|
|
2815
|
+
--json Full evidence and review history
|
|
2816
|
+
|
|
2817
|
+
Recording requires prior preparation and committed relevant edits. Review records
|
|
2818
|
+
belong in Git under .mason/reviews/advisories/. Deferral remains outstanding.
|
|
2819
|
+
Decision findings use review_decision; this command never approves decisions.
|
|
2820
|
+
Exit 0: prepared/recorded; 2: unavailable, conflict, or decision review required.`;
|
|
2821
|
+
async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), err: (text2) => console.error(text2) }) {
|
|
2822
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2823
|
+
io.out(USAGE);
|
|
2824
|
+
return 0;
|
|
2825
|
+
}
|
|
2826
|
+
try {
|
|
2827
|
+
const flags = {};
|
|
2828
|
+
let json = false;
|
|
2829
|
+
for (let i = 0; i < args.length; i++) {
|
|
2830
|
+
const flag = args[i];
|
|
2831
|
+
if (flag === "--json") {
|
|
2832
|
+
json = true;
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
if (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
|
|
2836
|
+
const value = args[++i];
|
|
2837
|
+
if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
|
|
2838
|
+
flags[flag] = value;
|
|
2839
|
+
}
|
|
2840
|
+
if (!flags["--outcome"] && ["--reviewer", "--note", "--token"].some((key) => flags[key])) throw new Error("Use --outcome to record an assessment.");
|
|
2841
|
+
const input = {
|
|
2842
|
+
baselinePath: flags["--baseline"],
|
|
2843
|
+
findingId: flags["--finding"],
|
|
2844
|
+
action: flags["--outcome"] ?? "prepare",
|
|
2845
|
+
reviewer: flags["--reviewer"],
|
|
2846
|
+
note: flags["--note"],
|
|
2847
|
+
reviewToken: flags["--token"]
|
|
2848
|
+
};
|
|
2849
|
+
const result = await reviewAdvisory(path20.resolve(flags["--dir"] ?? process.cwd()), input);
|
|
2850
|
+
if (json) io.out(JSON.stringify(result, null, 2));
|
|
2851
|
+
else {
|
|
2852
|
+
const lines = [`Advisory review: ${result.status}.`];
|
|
2853
|
+
if ("finding" in result && result.finding && result.evidence) lines.push(
|
|
2854
|
+
`[${result.finding.type}] ${result.finding.anchor.doc}: ${result.finding.message}`,
|
|
2855
|
+
`Finding: ${result.findingId}`,
|
|
2856
|
+
`Prepared against commit ${result.evidence.head.slice(0, 12)}.`,
|
|
2857
|
+
result.evidence.dirty ? "Relevant files have uncommitted edits; commit them and prepare again before recording." : "Relevant files are committed.",
|
|
2858
|
+
`Review token: ${result.reviewToken}`,
|
|
2859
|
+
...result.diff ? ["Scoped changes:", result.diff] : [],
|
|
2860
|
+
...result.diffNote ? [result.diffNote] : []
|
|
2861
|
+
);
|
|
2862
|
+
if ("event" in result) lines.push(`${result.event.outcome} \u2014 ${result.event.reviewer}: ${result.event.note}`, `Record: ${result.recordPath}`);
|
|
2863
|
+
lines.push(result.hint);
|
|
2864
|
+
io.out(stripVTControlCharacters(lines.join("\n")));
|
|
2865
|
+
}
|
|
2866
|
+
return result.status === "decision-review-required" ? 2 : 0;
|
|
2867
|
+
} catch (error) {
|
|
2868
|
+
io.err(stripVTControlCharacters(error instanceof Error ? error.message : String(error)));
|
|
2869
|
+
return 2;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// src/audit/cli.ts
|
|
2874
|
+
var USAGE2 = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]
|
|
2875
|
+
|
|
2876
|
+
Audits README.md and agent instruction files at any depth, preserving filename case
|
|
1929
2877
|
against repo reality: referenced paths that no longer exist, undocumented
|
|
1930
2878
|
modules, stale counts, dead npm scripts, and manifests newer than the doc.
|
|
1931
2879
|
Deterministic: no LLM call, no network \u2013 safe for CI. Works on any repo with
|
|
@@ -1944,6 +2892,8 @@ Options:
|
|
|
1944
2892
|
${ALL_CHECKS.join(", ")}
|
|
1945
2893
|
--help Show this help
|
|
1946
2894
|
|
|
2895
|
+
Use mason audit review --help to prepare and record an authorized advisory assessment.
|
|
2896
|
+
|
|
1947
2897
|
Exit codes:
|
|
1948
2898
|
0 no issues (advisories may still be present)
|
|
1949
2899
|
1 provable issues found
|
|
@@ -2007,7 +2957,10 @@ function issueLine(issue) {
|
|
|
2007
2957
|
}
|
|
2008
2958
|
function formatAuditSummary(report) {
|
|
2009
2959
|
const lines = [];
|
|
2010
|
-
const
|
|
2960
|
+
const reviewed = new Set(report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
2961
|
+
const open = report.advisories.filter((finding) => !reviewed.has(findingId(finding)));
|
|
2962
|
+
const suppressed = (report.suppressedAdvisories ?? []).filter((finding) => !reviewed.has(findingId(finding)));
|
|
2963
|
+
const reviewCount = open.length + suppressed.length;
|
|
2011
2964
|
for (const doc of report.docs) {
|
|
2012
2965
|
const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
|
|
2013
2966
|
const committed = doc.lastCommit ? `last committed ${doc.lastCommit.date.slice(0, 10)}, ${doc.lastCommit.hash.slice(0, 7)}` : "untracked";
|
|
@@ -2024,9 +2977,9 @@ function formatAuditSummary(report) {
|
|
|
2024
2977
|
for (const issue of report.issues) {
|
|
2025
2978
|
if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
|
|
2026
2979
|
}
|
|
2027
|
-
if (
|
|
2980
|
+
if (open.length > 0) {
|
|
2028
2981
|
lines.push("Advisories (do not affect the exit code):");
|
|
2029
|
-
for (const advisory of
|
|
2982
|
+
for (const advisory of open) {
|
|
2030
2983
|
lines.push(` [${advisory.type}] ${advisory.anchor.doc}: ${advisory.message}`);
|
|
2031
2984
|
}
|
|
2032
2985
|
}
|
|
@@ -2035,7 +2988,9 @@ function formatAuditSummary(report) {
|
|
|
2035
2988
|
lines.push(` [skipped] ${skip.check}: ${skip.reason}`);
|
|
2036
2989
|
}
|
|
2037
2990
|
}
|
|
2038
|
-
|
|
2991
|
+
if (reviewed.size) lines.push(`${reviewed.size} advisories have recorded assessments covering current evidence (see --json).`);
|
|
2992
|
+
for (const review of report.advisoryReviews ?? []) if (review.status !== "current") lines.push(` [${review.status}] ${review.reason}`);
|
|
2993
|
+
for (const advisory of suppressed) {
|
|
2039
2994
|
lines.push(` [suppressed; unresolved] ${advisory.type} ${advisory.anchor.doc}: ${advisory.message}`);
|
|
2040
2995
|
}
|
|
2041
2996
|
lines.push(
|
|
@@ -2072,10 +3027,10 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
2072
3027
|
"- dead-command: replace with the correct script from availableScripts if an obvious rename exists; otherwise remove the command mention."
|
|
2073
3028
|
);
|
|
2074
3029
|
lines.push(
|
|
2075
|
-
"- new-module:
|
|
3030
|
+
"- new-module: review whether the directory needs documenting; an omission alone does not establish a defect. Preserve scope and describe only what you verified."
|
|
2076
3031
|
);
|
|
2077
3032
|
lines.push(
|
|
2078
|
-
"- ADVISORIES require a separate assessment
|
|
3033
|
+
"- Historical ADVISORIES require a separate authorized assessment using review_advisory with the baselinePath and findingId from verification. Prepare and inspect evidence first; record addressed, inapplicable or deferred with the actual reviewer, note and reviewToken. Decision findings use review_decision. Candidates marked resolution: recheck can be rerun to see whether the condition remains; this does not establish semantic approval. Report what remains unknown."
|
|
2079
3034
|
);
|
|
2080
3035
|
lines.push("");
|
|
2081
3036
|
lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
|
|
@@ -2095,7 +3050,7 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
2095
3050
|
);
|
|
2096
3051
|
lines.push("");
|
|
2097
3052
|
lines.push(
|
|
2098
|
-
"After edits, call mason_repair with action: verify and the original baselinePath, or mason-audit --verify-repair <baselinePath> --dir <project>. Repeat against the same baseline after any final documentation commit. Summarize resolved, unresolved, review-required, unverified, and new findings with their evidence. Do not report a suppressed or unavailable check as fixed. This audit covers the listed
|
|
3053
|
+
"After edits, call mason_repair with action: verify and the original baselinePath, or mason-audit --verify-repair <baselinePath> --dir <project>. Repeat against the same baseline after any final documentation commit. Summarize resolved, unresolved, review-required, unverified, and new findings with their evidence. Do not report a suppressed or unavailable check as fixed. This audit covers the listed documents and supported claims; application behavior and code examples need their own validation."
|
|
2099
3054
|
);
|
|
2100
3055
|
return lines.join("\n");
|
|
2101
3056
|
}
|
|
@@ -2105,6 +3060,7 @@ async function runAuditCli(argv, io = {
|
|
|
2105
3060
|
err: (line) => process.stderr.write(`${line}
|
|
2106
3061
|
`)
|
|
2107
3062
|
}) {
|
|
3063
|
+
if (argv[0] === "review") return runAdvisoryCli(argv.slice(1), io);
|
|
2108
3064
|
let args;
|
|
2109
3065
|
try {
|
|
2110
3066
|
args = parseArgs(argv);
|
|
@@ -2116,14 +3072,14 @@ async function runAuditCli(argv, io = {
|
|
|
2116
3072
|
}
|
|
2117
3073
|
} catch (error) {
|
|
2118
3074
|
io.err(error instanceof Error ? error.message : String(error));
|
|
2119
|
-
io.err(
|
|
3075
|
+
io.err(USAGE2);
|
|
2120
3076
|
return 2;
|
|
2121
3077
|
}
|
|
2122
3078
|
if (args.help) {
|
|
2123
|
-
io.out(
|
|
3079
|
+
io.out(USAGE2);
|
|
2124
3080
|
return 0;
|
|
2125
3081
|
}
|
|
2126
|
-
const rootDir =
|
|
3082
|
+
const rootDir = path21.resolve(args.dir);
|
|
2127
3083
|
if (args.baseline || args.prepareRepair) {
|
|
2128
3084
|
try {
|
|
2129
3085
|
if (args.baseline) {
|
|
@@ -2143,7 +3099,7 @@ ${formatAuditSummary(prepared.report)}`);
|
|
|
2143
3099
|
const report = await computeAudit(rootDir, { checks: args.checks });
|
|
2144
3100
|
if (!report) {
|
|
2145
3101
|
io.err(
|
|
2146
|
-
`No
|
|
3102
|
+
`No README.md or agent instruction files found in ${rootDir}.`
|
|
2147
3103
|
);
|
|
2148
3104
|
return 2;
|
|
2149
3105
|
}
|
|
@@ -2169,11 +3125,13 @@ ${formatAuditSummary(prepared.report)}`);
|
|
|
2169
3125
|
|
|
2170
3126
|
// bin/mason-audit.ts
|
|
2171
3127
|
runAuditCli(process.argv.slice(2)).then(
|
|
2172
|
-
(code) =>
|
|
3128
|
+
(code) => {
|
|
3129
|
+
process.exitCode = code;
|
|
3130
|
+
},
|
|
2173
3131
|
(err) => {
|
|
2174
3132
|
process.stderr.write(`mason-audit error: ${err}
|
|
2175
3133
|
`);
|
|
2176
|
-
process.
|
|
3134
|
+
process.exitCode = 2;
|
|
2177
3135
|
}
|
|
2178
3136
|
);
|
|
2179
3137
|
//# sourceMappingURL=mason-audit.js.map
|