mason-context 0.16.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +14 -10
- package/dist/mason-audit.js +1719 -751
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +1963 -1244
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +13 -0
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +13 -0
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +2777 -1934
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +127 -53
- package/dist/mason-review.js.map +1 -1
- package/dist/mason.js +2115 -1396
- 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) {
|
|
@@ -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,341 @@ 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
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
+
const absolute = await auditInputPath(root, file);
|
|
1070
|
+
try {
|
|
1071
|
+
await fs6.access(absolute);
|
|
1072
|
+
return true;
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
if (error.code === "ENOENT") return false;
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function optionalMasonPath(file) {
|
|
1079
|
+
return file.split("/").includes(".mason");
|
|
1080
|
+
}
|
|
1081
|
+
var init_scope = __esm({
|
|
1082
|
+
"src/audit/scope.ts"() {
|
|
1083
|
+
"use strict";
|
|
1084
|
+
init_inputs();
|
|
1085
|
+
init_docs();
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// src/audit/checks/deleted-reference.ts
|
|
1090
|
+
import path11 from "path";
|
|
1091
|
+
async function checkDeletedReferences(ctx) {
|
|
1092
|
+
const result = emptyResult();
|
|
1093
|
+
for (const doc of ctx.docs) {
|
|
1094
|
+
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
1095
|
+
const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
|
|
1096
|
+
for (const claim of doc.claims.paths) {
|
|
1097
|
+
const scope = pathClaimScope(doc.path, claim);
|
|
1098
|
+
if (!scope) {
|
|
1099
|
+
result.skipped.push({
|
|
1100
|
+
check: "deleted-reference",
|
|
1101
|
+
doc: doc.path,
|
|
1102
|
+
reason: `Reference ${claim.excerpt} is outside the repository or has an unsupported path.`
|
|
1103
|
+
});
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
if (scope.candidates.some(optionalMasonPath)) continue;
|
|
1107
|
+
if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(Boolean)) continue;
|
|
1108
|
+
const candidates = await Promise.all(scope.candidates.map(async (file) => ({
|
|
1109
|
+
file,
|
|
1110
|
+
tracked: await lastCommitOf(ctx.root, file),
|
|
1111
|
+
parent: await pathExists(ctx.root, path11.posix.dirname(file))
|
|
1112
|
+
})));
|
|
1113
|
+
const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
|
|
1114
|
+
if (!candidate && claim.relativeTo !== "document") continue;
|
|
1115
|
+
const resolvedPath = candidate?.file ?? scope.candidates[0];
|
|
1116
|
+
const renamedTo = renames.get(resolvedPath) ?? null;
|
|
1117
|
+
const deletedInCommit = candidate?.tracked && !renamedTo ? await deletingCommitOf(ctx.root, resolvedPath) : null;
|
|
1118
|
+
const evidence = {
|
|
1119
|
+
kind: "missing-path",
|
|
1120
|
+
claimed: claim.path,
|
|
1121
|
+
resolvedPath,
|
|
1122
|
+
scope,
|
|
1123
|
+
renamedTo,
|
|
1124
|
+
deletedInCommit,
|
|
1125
|
+
everTracked: !!candidate?.tracked || !!renamedTo,
|
|
1126
|
+
parentDirExists: candidate?.parent ?? false
|
|
1127
|
+
};
|
|
1128
|
+
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
1129
|
+
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)})` : "");
|
|
1130
|
+
if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
|
|
1131
|
+
result.issues.push({ type: "deleted-reference", message, anchor, confidence: "certain", evidence });
|
|
1132
|
+
} else {
|
|
1133
|
+
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 });
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return result;
|
|
1138
|
+
}
|
|
1139
|
+
var init_deleted_reference = __esm({
|
|
1140
|
+
"src/audit/checks/deleted-reference.ts"() {
|
|
1141
|
+
"use strict";
|
|
1142
|
+
init_git();
|
|
1143
|
+
init_scope();
|
|
1144
|
+
init_checks();
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
// src/audit/checks/new-module.ts
|
|
1149
|
+
import fg4 from "fast-glob";
|
|
1150
|
+
import fs7 from "fs/promises";
|
|
1151
|
+
import path12 from "path";
|
|
1152
|
+
function escapeRegExp(text2) {
|
|
1153
|
+
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1154
|
+
}
|
|
1155
|
+
function isMentioned(combinedDocs, name) {
|
|
1156
|
+
const re = new RegExp(
|
|
1157
|
+
`(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,
|
|
1158
|
+
"im"
|
|
1159
|
+
);
|
|
1160
|
+
return re.test(combinedDocs);
|
|
1161
|
+
}
|
|
1162
|
+
async function checkNewModules(ctx) {
|
|
1163
|
+
const result = emptyResult();
|
|
1164
|
+
if (ctx.docs.length === 0) return result;
|
|
1165
|
+
const combinedDocs = moduleDocumentation(ctx.docs);
|
|
1166
|
+
const primaryDoc = ctx.docs[0].path;
|
|
1167
|
+
const checkedDocs = ctx.docs.map((d) => d.path);
|
|
1168
|
+
const flag = async (dir, sourceFileCount) => {
|
|
1169
|
+
result.advisories.push({
|
|
1170
|
+
resolution: "recheck",
|
|
1171
|
+
type: "new-module",
|
|
1172
|
+
message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in the discovered documentation; review whether it needs documenting`,
|
|
1173
|
+
anchor: { doc: primaryDoc, line: null, excerpt: dir },
|
|
1174
|
+
evidence: {
|
|
1175
|
+
kind: "unmentioned-dir",
|
|
1176
|
+
dir,
|
|
1177
|
+
sourceFileCount,
|
|
1178
|
+
firstCommit: await firstCommitOf(ctx.root, dir),
|
|
1179
|
+
checkedDocs
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
};
|
|
1183
|
+
for (const candidate of await moduleCandidates(ctx.root, combinedDocs)) {
|
|
1184
|
+
await flag(candidate.dir, candidate.sourceFileCount);
|
|
1185
|
+
}
|
|
1186
|
+
return result;
|
|
1187
|
+
}
|
|
1188
|
+
function moduleDocumentation(docs) {
|
|
1189
|
+
return docs.map((doc) => doc.content + "\n" + (path12.posix.dirname(doc.path) === "." ? "" : path12.posix.dirname(doc.path) + "/")).join("\n");
|
|
1190
|
+
}
|
|
1191
|
+
async function moduleCandidates(root, combinedDocs) {
|
|
1192
|
+
const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
|
|
1193
|
+
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
1194
|
+
const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
|
|
1195
|
+
const listSubdirs = async (dir) => {
|
|
1196
|
+
const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1197
|
+
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));
|
|
1198
|
+
const ignored = await gitIgnoredPaths(root, dirs);
|
|
1199
|
+
const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
|
|
1200
|
+
return (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
|
|
1201
|
+
ignore,
|
|
1202
|
+
onlyDirectories: true,
|
|
1203
|
+
label: "Module directory discovery",
|
|
1204
|
+
select: (file) => visible.has(file)
|
|
1205
|
+
})).map((file) => path12.posix.basename(file));
|
|
962
1206
|
};
|
|
963
1207
|
const countSourceFiles = async (dir) => {
|
|
964
1208
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
965
|
-
let
|
|
1209
|
+
let count3 = 0;
|
|
966
1210
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
967
|
-
|
|
1211
|
+
count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
|
|
968
1212
|
ignore,
|
|
969
1213
|
followSymbolicLinks: false,
|
|
970
1214
|
label: `Source discovery in ${dir}`
|
|
971
1215
|
})).length;
|
|
972
|
-
if (
|
|
1216
|
+
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
1217
|
}
|
|
974
|
-
return
|
|
1218
|
+
return count3;
|
|
975
1219
|
};
|
|
976
1220
|
const candidates = [];
|
|
977
1221
|
for (const topDir of await listSubdirs(".")) {
|
|
978
1222
|
const topMentioned = isMentioned(combinedDocs, topDir);
|
|
979
1223
|
if (!topMentioned) {
|
|
980
|
-
const
|
|
981
|
-
if (
|
|
1224
|
+
const count3 = await countSourceFiles(topDir);
|
|
1225
|
+
if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
|
|
982
1226
|
continue;
|
|
983
1227
|
}
|
|
984
1228
|
const subdirs = await listSubdirs(topDir);
|
|
@@ -986,18 +1230,48 @@ async function moduleCandidates(root, combinedDocs) {
|
|
|
986
1230
|
if (mentioned.length < ENUMERATION_THRESHOLD) continue;
|
|
987
1231
|
for (const sub of subdirs) {
|
|
988
1232
|
if (isMentioned(combinedDocs, sub)) continue;
|
|
989
|
-
const
|
|
990
|
-
if (
|
|
991
|
-
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount:
|
|
1233
|
+
const count3 = await countSourceFiles(`${topDir}/${sub}`);
|
|
1234
|
+
if (count3 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
|
|
1235
|
+
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count3 });
|
|
992
1236
|
}
|
|
993
1237
|
}
|
|
994
1238
|
}
|
|
995
1239
|
return candidates;
|
|
996
1240
|
}
|
|
1241
|
+
var DIR_DENYLIST, SECOND_LEVEL_MIN_SOURCE_FILES, ENUMERATION_THRESHOLD;
|
|
1242
|
+
var init_new_module = __esm({
|
|
1243
|
+
"src/audit/checks/new-module.ts"() {
|
|
1244
|
+
"use strict";
|
|
1245
|
+
init_files();
|
|
1246
|
+
init_inputs();
|
|
1247
|
+
init_git();
|
|
1248
|
+
init_checks();
|
|
1249
|
+
DIR_DENYLIST = /* @__PURE__ */ new Set([
|
|
1250
|
+
"node_modules",
|
|
1251
|
+
"dist",
|
|
1252
|
+
"build",
|
|
1253
|
+
"out",
|
|
1254
|
+
"coverage",
|
|
1255
|
+
"target",
|
|
1256
|
+
"vendor",
|
|
1257
|
+
"__pycache__",
|
|
1258
|
+
"venv",
|
|
1259
|
+
".venv",
|
|
1260
|
+
".git",
|
|
1261
|
+
".gradle",
|
|
1262
|
+
".mason",
|
|
1263
|
+
".claude",
|
|
1264
|
+
".github",
|
|
1265
|
+
".vscode",
|
|
1266
|
+
".idea"
|
|
1267
|
+
]);
|
|
1268
|
+
SECOND_LEVEL_MIN_SOURCE_FILES = 2;
|
|
1269
|
+
ENUMERATION_THRESHOLD = 2;
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
997
1272
|
|
|
998
1273
|
// src/audit/checks/stale-count.ts
|
|
999
|
-
import
|
|
1000
|
-
var MEMBERS_CAP = 50;
|
|
1274
|
+
import path13 from "path";
|
|
1001
1275
|
async function countGradleModules(root) {
|
|
1002
1276
|
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
1003
1277
|
const content = await readAuditInput(root, name);
|
|
@@ -1031,7 +1305,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1031
1305
|
return {
|
|
1032
1306
|
actual: matched.length,
|
|
1033
1307
|
countedFrom: "package.json workspaces",
|
|
1034
|
-
members: matched.map((m) =>
|
|
1308
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1035
1309
|
};
|
|
1036
1310
|
}
|
|
1037
1311
|
}
|
|
@@ -1062,7 +1336,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1062
1336
|
return {
|
|
1063
1337
|
actual: matched.length,
|
|
1064
1338
|
countedFrom: "pnpm-workspace.yaml",
|
|
1065
|
-
members: matched.map((m) =>
|
|
1339
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1066
1340
|
};
|
|
1067
1341
|
}
|
|
1068
1342
|
}
|
|
@@ -1084,8 +1358,8 @@ async function countCargoCrates(root) {
|
|
|
1084
1358
|
ignore: ["**/target/**"],
|
|
1085
1359
|
label: "Cargo workspace discovery"
|
|
1086
1360
|
});
|
|
1087
|
-
for (const m of matched) members.add(
|
|
1088
|
-
} else if (await readAuditInput(root,
|
|
1361
|
+
for (const m of matched) members.add(path13.dirname(m));
|
|
1362
|
+
} else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
|
|
1089
1363
|
members.add(entry);
|
|
1090
1364
|
}
|
|
1091
1365
|
}
|
|
@@ -1103,11 +1377,21 @@ async function resolveCountSource(root, claim) {
|
|
|
1103
1377
|
if (unit === "crate") return countCargoCrates(root);
|
|
1104
1378
|
return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
|
|
1105
1379
|
}
|
|
1380
|
+
async function resolveDocCountSource(root, doc, claim) {
|
|
1381
|
+
const scope = documentScope(doc);
|
|
1382
|
+
const source = await resolveCountSource(path13.join(root, scope), claim);
|
|
1383
|
+
if (!source || scope === ".") return source;
|
|
1384
|
+
return {
|
|
1385
|
+
...source,
|
|
1386
|
+
countedFrom: scope + "/" + source.countedFrom,
|
|
1387
|
+
members: source.members.map((member) => scope + "/" + member)
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1106
1390
|
async function checkStaleCounts(ctx) {
|
|
1107
1391
|
const result = emptyResult();
|
|
1108
1392
|
for (const doc of ctx.docs) {
|
|
1109
1393
|
for (const claim of doc.claims.counts) {
|
|
1110
|
-
const source = await
|
|
1394
|
+
const source = await resolveDocCountSource(ctx.root, doc.path, claim);
|
|
1111
1395
|
if (source === null) {
|
|
1112
1396
|
result.skipped.push({
|
|
1113
1397
|
check: "stale-count",
|
|
@@ -1135,64 +1419,106 @@ async function checkStaleCounts(ctx) {
|
|
|
1135
1419
|
}
|
|
1136
1420
|
return result;
|
|
1137
1421
|
}
|
|
1422
|
+
var MEMBERS_CAP;
|
|
1423
|
+
var init_stale_count = __esm({
|
|
1424
|
+
"src/audit/checks/stale-count.ts"() {
|
|
1425
|
+
"use strict";
|
|
1426
|
+
init_docs();
|
|
1427
|
+
init_inputs();
|
|
1428
|
+
init_checks();
|
|
1429
|
+
MEMBERS_CAP = 50;
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1138
1432
|
|
|
1139
1433
|
// src/audit/checks/dead-command.ts
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
const raw = await readAuditInput(root, manifest);
|
|
1143
|
-
if (raw === null) return null;
|
|
1434
|
+
import path14 from "path";
|
|
1435
|
+
function scriptsOf(raw) {
|
|
1144
1436
|
try {
|
|
1145
1437
|
const pkg = JSON.parse(raw);
|
|
1146
|
-
|
|
1438
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
|
|
1439
|
+
if (pkg.scripts === void 0) return [];
|
|
1440
|
+
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
1441
|
} catch {
|
|
1148
1442
|
return null;
|
|
1149
1443
|
}
|
|
1150
1444
|
}
|
|
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;
|
|
1445
|
+
async function commandInputs(root, docs) {
|
|
1446
|
+
const contents = /* @__PURE__ */ new Map();
|
|
1447
|
+
const read = async (file) => {
|
|
1448
|
+
if (!contents.has(file)) contents.set(file, await readAuditInput(root, file));
|
|
1449
|
+
return contents.get(file);
|
|
1178
1450
|
};
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1451
|
+
await read("package.json");
|
|
1452
|
+
let inventory;
|
|
1453
|
+
const resolutions = [];
|
|
1454
|
+
for (const doc of docs) for (const claim of doc.claims.commands) {
|
|
1455
|
+
const base = documentScope(doc.path);
|
|
1456
|
+
const directory = scopedPath(base, claim.directory ?? ".");
|
|
1457
|
+
const scope = {
|
|
1458
|
+
basis: claim.directory === void 0 ? "document" : "explicit",
|
|
1459
|
+
directory: directory ?? base,
|
|
1460
|
+
candidates: []
|
|
1461
|
+
};
|
|
1462
|
+
const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
|
|
1463
|
+
resolutions.push(resolution);
|
|
1464
|
+
if (claim.scopeUnknown || directory === null) {
|
|
1465
|
+
resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
let cursor = directory;
|
|
1469
|
+
while (true) {
|
|
1470
|
+
const manifest = path14.posix.join(cursor, "package.json");
|
|
1471
|
+
scope.candidates.push(manifest);
|
|
1472
|
+
const raw = await read(manifest);
|
|
1473
|
+
if (raw !== null) {
|
|
1474
|
+
resolution.manifest = manifest;
|
|
1475
|
+
const scripts = scriptsOf(raw);
|
|
1476
|
+
if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
|
|
1477
|
+
else resolution.scripts = scripts;
|
|
1478
|
+
break;
|
|
1194
1479
|
}
|
|
1195
|
-
|
|
1480
|
+
if (claim.directory !== void 0 || cursor === ".") break;
|
|
1481
|
+
cursor = path14.posix.dirname(cursor);
|
|
1482
|
+
}
|
|
1483
|
+
if (!resolution.manifest && !resolution.problem) {
|
|
1484
|
+
resolution.problem = `No package.json for the command's scope (${directory}).`;
|
|
1485
|
+
}
|
|
1486
|
+
if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
|
|
1487
|
+
inventory ??= await commandManifests(root);
|
|
1488
|
+
for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
|
|
1489
|
+
if (scope.candidates.includes(manifest)) continue;
|
|
1490
|
+
scope.candidates.push(manifest);
|
|
1491
|
+
const raw = await read(manifest);
|
|
1492
|
+
if (raw === null) continue;
|
|
1493
|
+
const scripts = scriptsOf(raw);
|
|
1494
|
+
if (scripts === null) resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
|
|
1495
|
+
else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
|
|
1499
|
+
}
|
|
1500
|
+
async function checkDeadCommands(ctx) {
|
|
1501
|
+
const result = emptyResult();
|
|
1502
|
+
const { resolutions } = await commandInputs(ctx.root, ctx.docs);
|
|
1503
|
+
for (const { doc, claim, scope, scripts, elsewhere, problem, manifest } of resolutions) {
|
|
1504
|
+
if (problem) {
|
|
1505
|
+
result.skipped.push({ check: "dead-command", doc, reason: problem });
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
if (scripts.includes(claim.scriptName)) continue;
|
|
1509
|
+
const evidence = {
|
|
1510
|
+
kind: "missing-script",
|
|
1511
|
+
scriptName: claim.scriptName,
|
|
1512
|
+
invocation: claim.invocation,
|
|
1513
|
+
manifestsChecked: scope.candidates,
|
|
1514
|
+
availableScripts: scripts.slice(0, AVAILABLE_SCRIPTS_CAP),
|
|
1515
|
+
scope
|
|
1516
|
+
};
|
|
1517
|
+
const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
|
|
1518
|
+
const message = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
|
|
1519
|
+
const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
|
|
1520
|
+
if (certain) result.issues.push({ type: "dead-command", message, anchor, confidence: "certain", evidence });
|
|
1521
|
+
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
1522
|
}
|
|
1197
1523
|
return result;
|
|
1198
1524
|
}
|
|
@@ -1202,23 +1528,33 @@ function commandManifests(root) {
|
|
|
1202
1528
|
label: "Command manifest discovery"
|
|
1203
1529
|
});
|
|
1204
1530
|
}
|
|
1531
|
+
var AVAILABLE_SCRIPTS_CAP;
|
|
1532
|
+
var init_dead_command = __esm({
|
|
1533
|
+
"src/audit/checks/dead-command.ts"() {
|
|
1534
|
+
"use strict";
|
|
1535
|
+
init_inputs();
|
|
1536
|
+
init_docs();
|
|
1537
|
+
init_scope();
|
|
1538
|
+
init_checks();
|
|
1539
|
+
AVAILABLE_SCRIPTS_CAP = 30;
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1205
1542
|
|
|
1206
1543
|
// src/audit/release-metadata.ts
|
|
1207
1544
|
import { execFile as execFile7 } from "child_process";
|
|
1208
1545
|
import { promisify as promisify7 } from "util";
|
|
1209
|
-
var exec6 = promisify7(execFile7);
|
|
1210
1546
|
async function git(root, args) {
|
|
1211
1547
|
return (await exec6("git", args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
1212
1548
|
}
|
|
1213
1549
|
function withoutAndroidReleaseValues(text2) {
|
|
1214
1550
|
if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
|
|
1215
|
-
const
|
|
1551
|
+
const scopes2 = [];
|
|
1216
1552
|
const normalized = [];
|
|
1217
1553
|
let assignments = 0;
|
|
1218
1554
|
for (const line of text2.split("\n")) {
|
|
1219
1555
|
const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
|
|
1220
1556
|
const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
|
|
1221
|
-
if (assignment &&
|
|
1557
|
+
if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
|
|
1222
1558
|
normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
|
|
1223
1559
|
assignments++;
|
|
1224
1560
|
} else {
|
|
@@ -1227,12 +1563,12 @@ function withoutAndroidReleaseValues(text2) {
|
|
|
1227
1563
|
}
|
|
1228
1564
|
const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
|
|
1229
1565
|
for (const brace of code.matchAll(/[{}]/g)) {
|
|
1230
|
-
if (brace[0] === "{")
|
|
1231
|
-
else if (!
|
|
1232
|
-
else
|
|
1566
|
+
if (brace[0] === "{") scopes2.push(named ?? "unknown");
|
|
1567
|
+
else if (!scopes2.length) return null;
|
|
1568
|
+
else scopes2.pop();
|
|
1233
1569
|
}
|
|
1234
1570
|
}
|
|
1235
|
-
return assignments && !
|
|
1571
|
+
return assignments && !scopes2.length ? normalized.join("\n") : null;
|
|
1236
1572
|
}
|
|
1237
1573
|
async function releaseMetadataOnly(root, commit) {
|
|
1238
1574
|
if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
|
|
@@ -1254,23 +1590,21 @@ async function releaseMetadataOnly(root, commit) {
|
|
|
1254
1590
|
return false;
|
|
1255
1591
|
}
|
|
1256
1592
|
}
|
|
1593
|
+
var exec6;
|
|
1594
|
+
var init_release_metadata = __esm({
|
|
1595
|
+
"src/audit/release-metadata.ts"() {
|
|
1596
|
+
"use strict";
|
|
1597
|
+
exec6 = promisify7(execFile7);
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1257
1600
|
|
|
1258
1601
|
// 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
|
-
];
|
|
1602
|
+
function manifestPathspecs(doc) {
|
|
1603
|
+
const scope = documentScope(doc);
|
|
1604
|
+
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
1605
|
+
const escaped = scope.replace(/[\\*?\[\]]/g, "\\$&");
|
|
1606
|
+
return MANIFEST_PATHSPECS.map((spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec);
|
|
1607
|
+
}
|
|
1274
1608
|
async function checkDepsChanged(ctx) {
|
|
1275
1609
|
const result = emptyResult();
|
|
1276
1610
|
result.suppressedAdvisories = [];
|
|
@@ -1294,7 +1628,7 @@ async function checkDepsChanged(ctx) {
|
|
|
1294
1628
|
const range = await commitsTouchingSince(
|
|
1295
1629
|
ctx.root,
|
|
1296
1630
|
doc.lastCommit.hash,
|
|
1297
|
-
|
|
1631
|
+
manifestPathspecs(doc.path)
|
|
1298
1632
|
);
|
|
1299
1633
|
if (range === null) {
|
|
1300
1634
|
result.skipped.push({
|
|
@@ -1329,111 +1663,49 @@ async function checkDepsChanged(ctx) {
|
|
|
1329
1663
|
}
|
|
1330
1664
|
return result;
|
|
1331
1665
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1666
|
+
var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
1667
|
+
var init_deps_changed = __esm({
|
|
1668
|
+
"src/audit/checks/deps-changed.ts"() {
|
|
1669
|
+
"use strict";
|
|
1670
|
+
init_docs();
|
|
1671
|
+
init_release_metadata();
|
|
1672
|
+
init_git();
|
|
1673
|
+
init_checks();
|
|
1674
|
+
MANIFEST_COMMITS_CAP = 10;
|
|
1675
|
+
MANIFEST_PATHSPECS = [
|
|
1676
|
+
":(glob)**/package.json",
|
|
1677
|
+
":(glob)**/build.gradle.kts",
|
|
1678
|
+
":(glob)**/build.gradle",
|
|
1679
|
+
"settings.gradle.kts",
|
|
1680
|
+
"settings.gradle",
|
|
1681
|
+
"gradle/libs.versions.toml",
|
|
1682
|
+
":(glob)**/Cargo.toml",
|
|
1683
|
+
"go.mod",
|
|
1684
|
+
"pyproject.toml",
|
|
1685
|
+
"requirements.txt",
|
|
1686
|
+
"Gemfile",
|
|
1687
|
+
"composer.json"
|
|
1688
|
+
];
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1340
1691
|
|
|
1341
1692
|
// src/context/lexical.ts
|
|
1342
|
-
import
|
|
1693
|
+
import path15 from "path";
|
|
1694
|
+
var init_lexical = __esm({
|
|
1695
|
+
"src/context/lexical.ts"() {
|
|
1696
|
+
"use strict";
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
// src/context/trust.ts
|
|
1701
|
+
var init_trust = __esm({
|
|
1702
|
+
"src/context/trust.ts"() {
|
|
1703
|
+
"use strict";
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1343
1706
|
|
|
1344
1707
|
// src/decisions/provenance.ts
|
|
1345
1708
|
import { z as z2 } from "zod";
|
|
1346
|
-
var text = (max) => z2.string().trim().min(1).max(max);
|
|
1347
|
-
var decisionSourceSchema = z2.object({
|
|
1348
|
-
kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
1349
|
-
reference: text(1e3),
|
|
1350
|
-
note: text(500).optional()
|
|
1351
|
-
}).strict();
|
|
1352
|
-
var attributionSchema = z2.object({
|
|
1353
|
-
owner: text(200).nullable().optional(),
|
|
1354
|
-
sources: z2.array(decisionSourceSchema).max(20).optional(),
|
|
1355
|
-
actor: text(200).optional()
|
|
1356
|
-
});
|
|
1357
|
-
var contentSchema = z2.object({
|
|
1358
|
-
title: z2.string().min(1),
|
|
1359
|
-
body: z2.string().min(1),
|
|
1360
|
-
category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
1361
|
-
files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
|
|
1362
|
-
owner: text(200).optional(),
|
|
1363
|
-
sources: z2.array(decisionSourceSchema).max(20)
|
|
1364
|
-
});
|
|
1365
|
-
var approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
|
|
1366
|
-
var statusSchema = z2.enum(["active", "superseded", "retired"]);
|
|
1367
|
-
var reviewEvidenceSchema = z2.object({
|
|
1368
|
-
baseHash: z2.string(),
|
|
1369
|
-
headHash: z2.string(),
|
|
1370
|
-
historyAvailable: z2.boolean(),
|
|
1371
|
-
changedFiles: z2.array(z2.string()),
|
|
1372
|
-
localChanges: z2.array(z2.string())
|
|
1373
|
-
});
|
|
1374
|
-
var eventSchema = z2.object({
|
|
1375
|
-
kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
|
|
1376
|
-
at: z2.string().datetime(),
|
|
1377
|
-
actor: text(200).optional(),
|
|
1378
|
-
note: text(1500).optional(),
|
|
1379
|
-
revision: z2.number().int().positive(),
|
|
1380
|
-
content: contentSchema,
|
|
1381
|
-
approval: approvalSchema,
|
|
1382
|
-
status: statusSchema,
|
|
1383
|
-
refreshedHash: z2.string(),
|
|
1384
|
-
evidence: reviewEvidenceSchema.optional()
|
|
1385
|
-
});
|
|
1386
|
-
var legacySchema = z2.object({
|
|
1387
|
-
version: z2.literal(1),
|
|
1388
|
-
id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
1389
|
-
title: z2.string().min(1),
|
|
1390
|
-
body: z2.string().min(1),
|
|
1391
|
-
category: contentSchema.shape.category,
|
|
1392
|
-
files: contentSchema.shape.files,
|
|
1393
|
-
createdAt: z2.string(),
|
|
1394
|
-
updatedAt: z2.string(),
|
|
1395
|
-
refreshedHash: z2.string(),
|
|
1396
|
-
status: z2.enum(["active", "superseded"]),
|
|
1397
|
-
supersededBy: z2.string().optional()
|
|
1398
|
-
}).passthrough();
|
|
1399
|
-
var currentSchema = legacySchema.extend({
|
|
1400
|
-
version: z2.literal(2),
|
|
1401
|
-
status: statusSchema,
|
|
1402
|
-
approval: approvalSchema,
|
|
1403
|
-
revision: z2.number().int().positive(),
|
|
1404
|
-
owner: text(200).optional(),
|
|
1405
|
-
sources: z2.array(decisionSourceSchema).max(20),
|
|
1406
|
-
history: z2.array(eventSchema).min(1)
|
|
1407
|
-
}).superRefine((record, ctx) => {
|
|
1408
|
-
const invalid = (message) => ctx.addIssue({ code: "custom", message });
|
|
1409
|
-
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1410
|
-
let previous;
|
|
1411
|
-
for (const event of record.history) {
|
|
1412
|
-
if (!previous) {
|
|
1413
|
-
if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
|
|
1414
|
-
if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
|
|
1415
|
-
} else {
|
|
1416
|
-
if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
|
|
1417
|
-
if (previous.status !== "active") invalid("Archived decisions cannot be changed");
|
|
1418
|
-
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
|
|
1419
|
-
if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
|
|
1420
|
-
if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
|
|
1421
|
-
if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
|
|
1422
|
-
const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
|
|
1423
|
-
if (event.approval !== approval) invalid("Approval disagrees with review history");
|
|
1424
|
-
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
|
|
1425
|
-
}
|
|
1426
|
-
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
|
|
1427
|
-
if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
|
|
1428
|
-
if (["accepted", "reaffirmed"].includes(event.kind)) {
|
|
1429
|
-
if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
|
|
1430
|
-
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");
|
|
1431
|
-
}
|
|
1432
|
-
previous = event;
|
|
1433
|
-
}
|
|
1434
|
-
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");
|
|
1435
|
-
});
|
|
1436
|
-
var decisionSchema = z2.union([legacySchema, currentSchema]);
|
|
1437
1709
|
function decisionContent(record) {
|
|
1438
1710
|
return {
|
|
1439
1711
|
title: record.title,
|
|
@@ -1477,8 +1749,111 @@ function decisionProvenance(record, freshness = "unknown") {
|
|
|
1477
1749
|
lastReview: review ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
|
|
1478
1750
|
};
|
|
1479
1751
|
}
|
|
1752
|
+
var text, decisionSourceSchema, attributionSchema, contentSchema, approvalSchema, statusSchema, reviewEvidenceSchema, eventSchema, legacySchema, currentSchema, decisionSchema;
|
|
1753
|
+
var init_provenance = __esm({
|
|
1754
|
+
"src/decisions/provenance.ts"() {
|
|
1755
|
+
"use strict";
|
|
1756
|
+
init_paths();
|
|
1757
|
+
init_trust();
|
|
1758
|
+
text = (max) => z2.string().trim().min(1).max(max);
|
|
1759
|
+
decisionSourceSchema = z2.object({
|
|
1760
|
+
kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
1761
|
+
reference: text(1e3),
|
|
1762
|
+
note: text(500).optional()
|
|
1763
|
+
}).strict();
|
|
1764
|
+
attributionSchema = z2.object({
|
|
1765
|
+
owner: text(200).nullable().optional(),
|
|
1766
|
+
sources: z2.array(decisionSourceSchema).max(20).optional(),
|
|
1767
|
+
actor: text(200).optional()
|
|
1768
|
+
});
|
|
1769
|
+
contentSchema = z2.object({
|
|
1770
|
+
title: z2.string().min(1),
|
|
1771
|
+
body: z2.string().min(1),
|
|
1772
|
+
category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
1773
|
+
files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
|
|
1774
|
+
owner: text(200).optional(),
|
|
1775
|
+
sources: z2.array(decisionSourceSchema).max(20)
|
|
1776
|
+
});
|
|
1777
|
+
approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
|
|
1778
|
+
statusSchema = z2.enum(["active", "superseded", "retired"]);
|
|
1779
|
+
reviewEvidenceSchema = z2.object({
|
|
1780
|
+
baseHash: z2.string(),
|
|
1781
|
+
headHash: z2.string(),
|
|
1782
|
+
historyAvailable: z2.boolean(),
|
|
1783
|
+
changedFiles: z2.array(z2.string()),
|
|
1784
|
+
localChanges: z2.array(z2.string())
|
|
1785
|
+
});
|
|
1786
|
+
eventSchema = z2.object({
|
|
1787
|
+
kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
|
|
1788
|
+
at: z2.string().datetime(),
|
|
1789
|
+
actor: text(200).optional(),
|
|
1790
|
+
note: text(1500).optional(),
|
|
1791
|
+
revision: z2.number().int().positive(),
|
|
1792
|
+
content: contentSchema,
|
|
1793
|
+
approval: approvalSchema,
|
|
1794
|
+
status: statusSchema,
|
|
1795
|
+
refreshedHash: z2.string(),
|
|
1796
|
+
evidence: reviewEvidenceSchema.optional()
|
|
1797
|
+
});
|
|
1798
|
+
legacySchema = z2.object({
|
|
1799
|
+
version: z2.literal(1),
|
|
1800
|
+
id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
1801
|
+
title: z2.string().min(1),
|
|
1802
|
+
body: z2.string().min(1),
|
|
1803
|
+
category: contentSchema.shape.category,
|
|
1804
|
+
files: contentSchema.shape.files,
|
|
1805
|
+
createdAt: z2.string(),
|
|
1806
|
+
updatedAt: z2.string(),
|
|
1807
|
+
refreshedHash: z2.string(),
|
|
1808
|
+
status: z2.enum(["active", "superseded"]),
|
|
1809
|
+
supersededBy: z2.string().optional()
|
|
1810
|
+
}).passthrough();
|
|
1811
|
+
currentSchema = legacySchema.extend({
|
|
1812
|
+
version: z2.literal(2),
|
|
1813
|
+
status: statusSchema,
|
|
1814
|
+
approval: approvalSchema,
|
|
1815
|
+
revision: z2.number().int().positive(),
|
|
1816
|
+
owner: text(200).optional(),
|
|
1817
|
+
sources: z2.array(decisionSourceSchema).max(20),
|
|
1818
|
+
history: z2.array(eventSchema).min(1)
|
|
1819
|
+
}).superRefine((record, ctx) => {
|
|
1820
|
+
const invalid = (message) => ctx.addIssue({ code: "custom", message });
|
|
1821
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1822
|
+
let previous;
|
|
1823
|
+
for (const event of record.history) {
|
|
1824
|
+
if (!previous) {
|
|
1825
|
+
if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
|
|
1826
|
+
if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
|
|
1827
|
+
} else {
|
|
1828
|
+
if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
|
|
1829
|
+
if (previous.status !== "active") invalid("Archived decisions cannot be changed");
|
|
1830
|
+
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
|
|
1831
|
+
if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
|
|
1832
|
+
if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
|
|
1833
|
+
if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
|
|
1834
|
+
const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
|
|
1835
|
+
if (event.approval !== approval) invalid("Approval disagrees with review history");
|
|
1836
|
+
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
|
|
1837
|
+
}
|
|
1838
|
+
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
|
|
1839
|
+
if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
|
|
1840
|
+
if (["accepted", "reaffirmed"].includes(event.kind)) {
|
|
1841
|
+
if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
|
|
1842
|
+
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");
|
|
1843
|
+
}
|
|
1844
|
+
previous = event;
|
|
1845
|
+
}
|
|
1846
|
+
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");
|
|
1847
|
+
});
|
|
1848
|
+
decisionSchema = z2.union([legacySchema, currentSchema]);
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1480
1851
|
|
|
1481
1852
|
// src/decisions/decisions.ts
|
|
1853
|
+
import fs8 from "fs/promises";
|
|
1854
|
+
import path16 from "path";
|
|
1855
|
+
import { createHash } from "crypto";
|
|
1856
|
+
import { z as z3 } from "zod";
|
|
1482
1857
|
async function loadDecisionStore(rootDir) {
|
|
1483
1858
|
const records = [];
|
|
1484
1859
|
const diagnostics = [];
|
|
@@ -1502,10 +1877,34 @@ async function loadDecisionStore(rootDir) {
|
|
|
1502
1877
|
}
|
|
1503
1878
|
return { records, diagnostics };
|
|
1504
1879
|
}
|
|
1880
|
+
var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, boundedText, decisionTitleSchema, decisionBodySchema;
|
|
1881
|
+
var init_decisions = __esm({
|
|
1882
|
+
"src/decisions/decisions.ts"() {
|
|
1883
|
+
"use strict";
|
|
1884
|
+
init_storage();
|
|
1885
|
+
init_paths();
|
|
1886
|
+
init_snapshot();
|
|
1887
|
+
init_lexical();
|
|
1888
|
+
init_provenance();
|
|
1889
|
+
TITLE_MAX_CHARS = 80;
|
|
1890
|
+
BODY_RECOMMENDED_CHARS = 1500;
|
|
1891
|
+
BODY_MAX_CHARS = 2500;
|
|
1892
|
+
boundedText = (field, max, hint) => z3.string({
|
|
1893
|
+
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 })
|
|
1894
|
+
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
1895
|
+
decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
|
|
1896
|
+
decisionBodySchema = boundedText(
|
|
1897
|
+
"body",
|
|
1898
|
+
BODY_MAX_CHARS,
|
|
1899
|
+
`Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1505
1903
|
|
|
1506
1904
|
// src/decisions/drift.ts
|
|
1905
|
+
import path17 from "path";
|
|
1507
1906
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
1508
|
-
const resolvedRoot =
|
|
1907
|
+
const resolvedRoot = path17.resolve(rootDir);
|
|
1509
1908
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
1510
1909
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
1511
1910
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
@@ -1533,6 +1932,16 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
1533
1932
|
}
|
|
1534
1933
|
return report;
|
|
1535
1934
|
}
|
|
1935
|
+
var init_drift2 = __esm({
|
|
1936
|
+
"src/decisions/drift.ts"() {
|
|
1937
|
+
"use strict";
|
|
1938
|
+
init_drift();
|
|
1939
|
+
init_paths();
|
|
1940
|
+
init_snapshot();
|
|
1941
|
+
init_decisions();
|
|
1942
|
+
init_provenance();
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1536
1945
|
|
|
1537
1946
|
// src/audit/checks/decision-anchor.ts
|
|
1538
1947
|
async function checkDecisionAnchors(ctx) {
|
|
@@ -1576,180 +1985,50 @@ async function checkDecisionAnchors(ctx) {
|
|
|
1576
1985
|
}
|
|
1577
1986
|
return result;
|
|
1578
1987
|
}
|
|
1988
|
+
var init_decision_anchor = __esm({
|
|
1989
|
+
"src/audit/checks/decision-anchor.ts"() {
|
|
1990
|
+
"use strict";
|
|
1991
|
+
init_drift2();
|
|
1992
|
+
init_decisions();
|
|
1993
|
+
init_provenance();
|
|
1994
|
+
init_checks();
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1579
1997
|
|
|
1580
1998
|
// src/audit/checks/index.ts
|
|
1581
|
-
var CHECKS = {
|
|
1582
|
-
"deleted-reference": checkDeletedReferences,
|
|
1583
|
-
"new-module": checkNewModules,
|
|
1584
|
-
"stale-count": checkStaleCounts,
|
|
1585
|
-
"dead-command": checkDeadCommands,
|
|
1586
|
-
"deps-changed": checkDepsChanged,
|
|
1587
|
-
"decision-anchor-drift": checkDecisionAnchors
|
|
1588
|
-
};
|
|
1589
1999
|
function emptyResult() {
|
|
1590
2000
|
return { issues: [], advisories: [], skipped: [] };
|
|
1591
2001
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1610
|
-
decisionsChecked: false,
|
|
1611
|
-
issues: [],
|
|
1612
|
-
advisories: [],
|
|
1613
|
-
suppressedAdvisories: [],
|
|
1614
|
-
skippedChecks: [],
|
|
1615
|
-
clean: true
|
|
1616
|
-
};
|
|
1617
|
-
if (!report.gitAvailable) return report;
|
|
1618
|
-
const changesSinceDoc = /* @__PURE__ */ new Map();
|
|
1619
|
-
for (const doc of docs) {
|
|
1620
|
-
changesSinceDoc.set(
|
|
1621
|
-
doc.path,
|
|
1622
|
-
doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
|
|
1623
|
-
);
|
|
1624
|
-
}
|
|
1625
|
-
let decisionsPresent = false;
|
|
1626
|
-
try {
|
|
1627
|
-
await fs9.access(path15.join(resolvedRoot, ".mason", "decisions"));
|
|
1628
|
-
decisionsPresent = true;
|
|
1629
|
-
} catch {
|
|
1630
|
-
}
|
|
1631
|
-
report.decisionsChecked = decisionsPresent;
|
|
1632
|
-
const ctx = {
|
|
1633
|
-
root: resolvedRoot,
|
|
1634
|
-
docs,
|
|
1635
|
-
headHash,
|
|
1636
|
-
changesSinceDoc,
|
|
1637
|
-
decisionsPresent
|
|
1638
|
-
};
|
|
1639
|
-
const selected = options.checks ?? ALL_CHECKS;
|
|
1640
|
-
for (const name of ALL_CHECKS) {
|
|
1641
|
-
if (!selected.includes(name)) continue;
|
|
1642
|
-
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
|
|
1643
|
-
report.checksRun.push(name);
|
|
1644
|
-
report.issues.push(...issues);
|
|
1645
|
-
report.advisories.push(...advisories);
|
|
1646
|
-
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
1647
|
-
report.skippedChecks.push(...skipped);
|
|
2002
|
+
var CHECKS;
|
|
2003
|
+
var init_checks = __esm({
|
|
2004
|
+
"src/audit/checks/index.ts"() {
|
|
2005
|
+
"use strict";
|
|
2006
|
+
init_deleted_reference();
|
|
2007
|
+
init_new_module();
|
|
2008
|
+
init_stale_count();
|
|
2009
|
+
init_dead_command();
|
|
2010
|
+
init_deps_changed();
|
|
2011
|
+
init_decision_anchor();
|
|
2012
|
+
CHECKS = {
|
|
2013
|
+
"deleted-reference": checkDeletedReferences,
|
|
2014
|
+
"new-module": checkNewModules,
|
|
2015
|
+
"stale-count": checkStaleCounts,
|
|
2016
|
+
"dead-command": checkDeadCommands,
|
|
2017
|
+
"deps-changed": checkDepsChanged,
|
|
2018
|
+
"decision-anchor-drift": checkDecisionAnchors
|
|
2019
|
+
};
|
|
1648
2020
|
}
|
|
1649
|
-
report.clean = report.issues.length === 0;
|
|
1650
|
-
return report;
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
// src/audit/repair.ts
|
|
1654
|
-
import fs10 from "fs/promises";
|
|
1655
|
-
import path16 from "path";
|
|
1656
|
-
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
1657
|
-
import { z as z3 } from "zod";
|
|
1658
|
-
var checkSchema = z3.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
|
|
1659
|
-
var commitSchema = z3.object({ hash: z3.string().regex(/^[a-f0-9]{40,64}$/), date: z3.string(), subject: z3.string() });
|
|
1660
|
-
var anchorSchema = z3.object({ doc: z3.string(), line: z3.number().int().positive().nullable(), excerpt: z3.string().nullable() });
|
|
1661
|
-
var count = z3.number().int().nonnegative();
|
|
1662
|
-
var evidenceSchema = z3.discriminatedUnion("kind", [
|
|
1663
|
-
z3.object({
|
|
1664
|
-
kind: z3.literal("missing-path"),
|
|
1665
|
-
claimed: z3.string(),
|
|
1666
|
-
renamedTo: z3.string().nullable(),
|
|
1667
|
-
deletedInCommit: commitSchema.nullable(),
|
|
1668
|
-
everTracked: z3.boolean(),
|
|
1669
|
-
parentDirExists: z3.boolean()
|
|
1670
|
-
}),
|
|
1671
|
-
z3.object({
|
|
1672
|
-
kind: z3.literal("unmentioned-dir"),
|
|
1673
|
-
dir: z3.string(),
|
|
1674
|
-
sourceFileCount: count,
|
|
1675
|
-
firstCommit: commitSchema.nullable(),
|
|
1676
|
-
checkedDocs: z3.array(z3.string())
|
|
1677
|
-
}),
|
|
1678
|
-
z3.object({
|
|
1679
|
-
kind: z3.literal("count-mismatch"),
|
|
1680
|
-
claimed: count,
|
|
1681
|
-
actual: count,
|
|
1682
|
-
unit: z3.string(),
|
|
1683
|
-
countedFrom: z3.string(),
|
|
1684
|
-
members: z3.array(z3.string())
|
|
1685
|
-
}),
|
|
1686
|
-
z3.object({
|
|
1687
|
-
kind: z3.literal("missing-script"),
|
|
1688
|
-
scriptName: z3.string(),
|
|
1689
|
-
invocation: z3.string(),
|
|
1690
|
-
manifestsChecked: z3.array(z3.string()),
|
|
1691
|
-
availableScripts: z3.array(z3.string())
|
|
1692
|
-
}),
|
|
1693
|
-
z3.object({
|
|
1694
|
-
kind: z3.literal("doc-behind-manifests"),
|
|
1695
|
-
docLastCommit: commitSchema,
|
|
1696
|
-
manifestCommits: z3.array(commitSchema.extend({ files: z3.array(z3.string()) })),
|
|
1697
|
-
totalCommits: count
|
|
1698
|
-
}),
|
|
1699
|
-
z3.object({
|
|
1700
|
-
kind: z3.literal("decision-anchor"),
|
|
1701
|
-
decisionId: z3.string(),
|
|
1702
|
-
title: z3.string(),
|
|
1703
|
-
changedFiles: z3.array(z3.string()),
|
|
1704
|
-
refreshedHash: z3.string(),
|
|
1705
|
-
provenance: z3.object({}).passthrough().optional()
|
|
1706
|
-
})
|
|
1707
|
-
]);
|
|
1708
|
-
var findingSchema = z3.object({ message: z3.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
1709
|
-
var issueSchema = findingSchema.extend({
|
|
1710
|
-
type: z3.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
1711
|
-
confidence: z3.enum(["certain", "likely"])
|
|
1712
|
-
});
|
|
1713
|
-
var advisorySchema = findingSchema.extend({ type: z3.enum(["deps-changed", "decision-anchor-drift"]) });
|
|
1714
|
-
var checkResultSchema = z3.object({
|
|
1715
|
-
issues: z3.array(issueSchema),
|
|
1716
|
-
advisories: z3.array(advisorySchema),
|
|
1717
|
-
suppressedAdvisories: z3.array(advisorySchema).optional(),
|
|
1718
|
-
skipped: z3.array(z3.object({ check: z3.string(), reason: z3.string(), doc: z3.string().optional() }))
|
|
1719
2021
|
});
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
headHash: commitSchema.shape.hash,
|
|
1725
|
-
checksRun: z3.array(checkSchema).nonempty(),
|
|
1726
|
-
docs: z3.array(z3.object({
|
|
1727
|
-
path: z3.enum(DOC_CANDIDATES),
|
|
1728
|
-
lastCommit: commitSchema.nullable(),
|
|
1729
|
-
dirty: z3.boolean(),
|
|
1730
|
-
lineCount: count
|
|
1731
|
-
})).nonempty(),
|
|
1732
|
-
decisionsChecked: z3.boolean(),
|
|
1733
|
-
clean: z3.boolean(),
|
|
1734
|
-
issues: z3.array(issueSchema),
|
|
1735
|
-
advisories: z3.array(advisorySchema),
|
|
1736
|
-
suppressedAdvisories: z3.array(advisorySchema).optional(),
|
|
1737
|
-
skippedChecks: z3.array(z3.object({ check: z3.string(), reason: z3.string(), doc: z3.string().optional() }))
|
|
1738
|
-
});
|
|
1739
|
-
var baselineSchema = z3.object({
|
|
1740
|
-
kind: z3.literal("mason-audit-repair"),
|
|
1741
|
-
version: z3.literal(1),
|
|
1742
|
-
createdAt: z3.string().datetime(),
|
|
1743
|
-
report: reportSchema,
|
|
1744
|
-
digest: z3.string().regex(/^[a-f0-9]{64}$/)
|
|
1745
|
-
});
|
|
1746
|
-
var digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2022
|
+
|
|
2023
|
+
// src/audit/findings.ts
|
|
2024
|
+
import { createHash as createHash2 } from "crypto";
|
|
2025
|
+
import { z as z4 } from "zod";
|
|
1747
2026
|
function findingId(finding) {
|
|
1748
2027
|
const e = finding.evidence;
|
|
1749
2028
|
let key;
|
|
1750
2029
|
switch (e.kind) {
|
|
1751
2030
|
case "missing-path":
|
|
1752
|
-
key = e.claimed;
|
|
2031
|
+
key = e.resolvedPath && e.resolvedPath !== e.claimed ? [e.claimed, e.resolvedPath] : e.claimed;
|
|
1753
2032
|
break;
|
|
1754
2033
|
case "unmentioned-dir":
|
|
1755
2034
|
key = e.dir;
|
|
@@ -1758,7 +2037,7 @@ function findingId(finding) {
|
|
|
1758
2037
|
key = [e.unit.replace(/s$/, ""), e.countedFrom];
|
|
1759
2038
|
break;
|
|
1760
2039
|
case "missing-script":
|
|
1761
|
-
key = e.scriptName;
|
|
2040
|
+
key = e.scope && e.scope.directory !== "." ? [e.scriptName, e.scope.directory] : e.scriptName;
|
|
1762
2041
|
break;
|
|
1763
2042
|
case "doc-behind-manifests":
|
|
1764
2043
|
key = null;
|
|
@@ -1769,12 +2048,199 @@ function findingId(finding) {
|
|
|
1769
2048
|
}
|
|
1770
2049
|
return digest([finding.type, finding.anchor.doc, key]);
|
|
1771
2050
|
}
|
|
2051
|
+
var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema;
|
|
2052
|
+
var init_findings = __esm({
|
|
2053
|
+
"src/audit/findings.ts"() {
|
|
2054
|
+
"use strict";
|
|
2055
|
+
digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2056
|
+
checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
|
|
2057
|
+
commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
|
|
2058
|
+
anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
|
|
2059
|
+
count = z4.number().int().nonnegative();
|
|
2060
|
+
scopeSchema = z4.object({
|
|
2061
|
+
basis: z4.enum(["repository", "document", "document-link", "explicit"]),
|
|
2062
|
+
directory: z4.string(),
|
|
2063
|
+
candidates: z4.array(z4.string())
|
|
2064
|
+
});
|
|
2065
|
+
evidenceSchema = z4.discriminatedUnion("kind", [
|
|
2066
|
+
z4.object({
|
|
2067
|
+
kind: z4.literal("missing-path"),
|
|
2068
|
+
claimed: z4.string(),
|
|
2069
|
+
renamedTo: z4.string().nullable(),
|
|
2070
|
+
deletedInCommit: commitSchema.nullable(),
|
|
2071
|
+
everTracked: z4.boolean(),
|
|
2072
|
+
parentDirExists: z4.boolean(),
|
|
2073
|
+
resolvedPath: z4.string().optional(),
|
|
2074
|
+
scope: scopeSchema.optional()
|
|
2075
|
+
}),
|
|
2076
|
+
z4.object({
|
|
2077
|
+
kind: z4.literal("unmentioned-dir"),
|
|
2078
|
+
dir: z4.string(),
|
|
2079
|
+
sourceFileCount: count,
|
|
2080
|
+
firstCommit: commitSchema.nullable(),
|
|
2081
|
+
checkedDocs: z4.array(z4.string())
|
|
2082
|
+
}),
|
|
2083
|
+
z4.object({
|
|
2084
|
+
kind: z4.literal("count-mismatch"),
|
|
2085
|
+
claimed: count,
|
|
2086
|
+
actual: count,
|
|
2087
|
+
unit: z4.string(),
|
|
2088
|
+
countedFrom: z4.string(),
|
|
2089
|
+
members: z4.array(z4.string())
|
|
2090
|
+
}),
|
|
2091
|
+
z4.object({
|
|
2092
|
+
kind: z4.literal("missing-script"),
|
|
2093
|
+
scriptName: z4.string(),
|
|
2094
|
+
invocation: z4.string(),
|
|
2095
|
+
manifestsChecked: z4.array(z4.string()),
|
|
2096
|
+
availableScripts: z4.array(z4.string()),
|
|
2097
|
+
scope: scopeSchema.optional()
|
|
2098
|
+
}),
|
|
2099
|
+
z4.object({
|
|
2100
|
+
kind: z4.literal("doc-behind-manifests"),
|
|
2101
|
+
docLastCommit: commitSchema,
|
|
2102
|
+
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
2103
|
+
totalCommits: count
|
|
2104
|
+
}),
|
|
2105
|
+
z4.object({
|
|
2106
|
+
kind: z4.literal("decision-anchor"),
|
|
2107
|
+
decisionId: z4.string(),
|
|
2108
|
+
title: z4.string(),
|
|
2109
|
+
changedFiles: z4.array(z4.string()),
|
|
2110
|
+
refreshedHash: z4.string(),
|
|
2111
|
+
provenance: z4.object({}).passthrough().optional()
|
|
2112
|
+
})
|
|
2113
|
+
]);
|
|
2114
|
+
findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
2115
|
+
issueSchema = findingSchema.extend({
|
|
2116
|
+
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2117
|
+
confidence: z4.enum(["certain", "likely"])
|
|
2118
|
+
});
|
|
2119
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.literal("recheck").optional() });
|
|
2120
|
+
checkResultSchema = z4.object({
|
|
2121
|
+
issues: z4.array(issueSchema),
|
|
2122
|
+
advisories: z4.array(advisorySchema),
|
|
2123
|
+
suppressedAdvisories: z4.array(advisorySchema).optional(),
|
|
2124
|
+
skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
// src/automation/store.ts
|
|
2130
|
+
import fs9 from "fs/promises";
|
|
2131
|
+
import os from "os";
|
|
2132
|
+
import { z as z5 } from "zod";
|
|
2133
|
+
async function withLock(root, directory, run, waitMs = 5e3) {
|
|
2134
|
+
const file = await storePath(root, directory + "/lock", true);
|
|
2135
|
+
const deadline = Date.now() + waitMs;
|
|
2136
|
+
let handle;
|
|
2137
|
+
while (!handle) {
|
|
2138
|
+
try {
|
|
2139
|
+
handle = await fs9.open(file, "wx", 384);
|
|
2140
|
+
try {
|
|
2141
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
await handle.close().catch(() => {
|
|
2144
|
+
});
|
|
2145
|
+
handle = void 0;
|
|
2146
|
+
await fs9.rm(file, { force: true }).catch(() => {
|
|
2147
|
+
});
|
|
2148
|
+
throw error;
|
|
2149
|
+
}
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
if (error.code !== "EEXIST") throw error;
|
|
2152
|
+
try {
|
|
2153
|
+
const owner = JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2154
|
+
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
2155
|
+
try {
|
|
2156
|
+
process.kill(owner.pid, 0);
|
|
2157
|
+
} catch (probe) {
|
|
2158
|
+
if (probe.code === "ESRCH") {
|
|
2159
|
+
const reclaim = file + ".reclaim";
|
|
2160
|
+
let guard;
|
|
2161
|
+
try {
|
|
2162
|
+
guard = await fs9.open(reclaim, "wx", 384);
|
|
2163
|
+
const current = JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2164
|
+
if (current.pid === owner.pid && current.host === owner.host) await fs9.unlink(file);
|
|
2165
|
+
} finally {
|
|
2166
|
+
if (guard) {
|
|
2167
|
+
await guard.close();
|
|
2168
|
+
await fs9.rm(reclaim, { force: true });
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
} catch {
|
|
2175
|
+
}
|
|
2176
|
+
if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
|
|
2177
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
try {
|
|
2181
|
+
return await run();
|
|
2182
|
+
} finally {
|
|
2183
|
+
await handle.close();
|
|
2184
|
+
await fs9.unlink(file);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
var hostSchema, stateSchema;
|
|
2188
|
+
var init_store = __esm({
|
|
2189
|
+
"src/automation/store.ts"() {
|
|
2190
|
+
"use strict";
|
|
2191
|
+
init_storage();
|
|
2192
|
+
hostSchema = z5.enum(["claude", "codex"]);
|
|
2193
|
+
stateSchema = z5.object({
|
|
2194
|
+
version: z5.literal(1),
|
|
2195
|
+
root: z5.string(),
|
|
2196
|
+
gitDir: z5.string(),
|
|
2197
|
+
branch: z5.string(),
|
|
2198
|
+
baselines: z5.array(z5.object({ path: z5.string(), at: z5.string(), event: z5.string(), fingerprint: z5.string() })).max(128),
|
|
2199
|
+
sessions: z5.record(z5.object({
|
|
2200
|
+
host: hostSchema,
|
|
2201
|
+
seen: z5.string().nullable(),
|
|
2202
|
+
continued: z5.boolean(),
|
|
2203
|
+
initialIssues: z5.array(z5.string()),
|
|
2204
|
+
initialDocs: z5.record(z5.string().nullable()),
|
|
2205
|
+
lastUsed: z5.string(),
|
|
2206
|
+
mutationObserved: z5.boolean(),
|
|
2207
|
+
pending: z5.record(z5.string()),
|
|
2208
|
+
coverageGaps: z5.array(z5.string()),
|
|
2209
|
+
events: z5.record(z5.object({ at: z5.string(), count: z5.number().int().positive() }))
|
|
2210
|
+
})),
|
|
2211
|
+
updatedAt: z5.string(),
|
|
2212
|
+
fingerprint: z5.string().nullable(),
|
|
2213
|
+
latest: z5.string().nullable(),
|
|
2214
|
+
analysis: z5.object({ id: z5.string(), fingerprint: z5.string(), completedAt: z5.number(), path: z5.string() }).optional()
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
// src/audit/repair.ts
|
|
2220
|
+
var repair_exports = {};
|
|
2221
|
+
__export(repair_exports, {
|
|
2222
|
+
checkResultSchema: () => checkResultSchema,
|
|
2223
|
+
findingId: () => findingId,
|
|
2224
|
+
formatRepairSummary: () => formatRepairSummary,
|
|
2225
|
+
loadRepairBaseline: () => loadRepairBaseline,
|
|
2226
|
+
prepareRepair: () => prepareRepair,
|
|
2227
|
+
repairExitCode: () => repairExitCode,
|
|
2228
|
+
repairFindingSchema: () => repairFindingSchema,
|
|
2229
|
+
verifyRepair: () => verifyRepair
|
|
2230
|
+
});
|
|
2231
|
+
import fs10 from "fs/promises";
|
|
2232
|
+
import path18 from "path";
|
|
2233
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2234
|
+
import { z as z6 } from "zod";
|
|
2235
|
+
function recheckable(finding) {
|
|
2236
|
+
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);
|
|
2237
|
+
}
|
|
1772
2238
|
function allFindings(report) {
|
|
1773
2239
|
return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
|
|
1774
2240
|
}
|
|
1775
2241
|
async function docState(root) {
|
|
1776
2242
|
const docs = [];
|
|
1777
|
-
for (const doc of
|
|
2243
|
+
for (const doc of await discoverDocPaths(root)) {
|
|
1778
2244
|
try {
|
|
1779
2245
|
const content = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
|
|
1780
2246
|
if (content === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
|
|
@@ -1797,7 +2263,7 @@ async function stableAudit(root, checks, options = {}) {
|
|
|
1797
2263
|
}
|
|
1798
2264
|
async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
1799
2265
|
const root = await fs10.realpath(rootDir);
|
|
1800
|
-
const selected =
|
|
2266
|
+
const selected = z6.array(checkSchema).nonempty().parse(checks);
|
|
1801
2267
|
const report = await stableAudit(root, selected, options);
|
|
1802
2268
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
1803
2269
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
@@ -1812,23 +2278,29 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
|
1812
2278
|
await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
|
|
1813
2279
|
return { version: 1, action: "prepare", baselinePath, report };
|
|
1814
2280
|
}
|
|
1815
|
-
async function
|
|
2281
|
+
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
1816
2282
|
const root = await fs10.realpath(rootDir);
|
|
1817
|
-
const declaredRoot =
|
|
1818
|
-
const relative =
|
|
2283
|
+
const declaredRoot = path18.resolve(rootDir);
|
|
2284
|
+
const relative = path18.isAbsolute(baselinePath) ? path18.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
1819
2285
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
1820
2286
|
const { digest: savedDigest, ...payload } = stored;
|
|
1821
2287
|
if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
1822
2288
|
if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
|
|
1823
2289
|
const original = stored.report;
|
|
2290
|
+
return { root, relative, original };
|
|
2291
|
+
}
|
|
2292
|
+
async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
2293
|
+
const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
|
|
1824
2294
|
const diagnostics = [];
|
|
2295
|
+
let reviewDigest;
|
|
1825
2296
|
let current = null;
|
|
1826
2297
|
try {
|
|
2298
|
+
reviewDigest = digest(await advisoryReviewInventory(root));
|
|
1827
2299
|
current = await stableAudit(root, original.checksRun, options);
|
|
1828
2300
|
if (!current) diagnostics.push("No context files remain available to audit.");
|
|
1829
2301
|
else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
|
|
1830
2302
|
for (const doc of original.docs) {
|
|
1831
|
-
if (!original.
|
|
2303
|
+
if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
|
|
1832
2304
|
const content = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
|
|
1833
2305
|
if (content === null || !content.trim()) {
|
|
1834
2306
|
diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
|
|
@@ -1845,21 +2317,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
1845
2317
|
const originalIds = new Set(originalFindings.map(findingId));
|
|
1846
2318
|
const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
|
|
1847
2319
|
for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
|
|
1848
|
-
const findings = originalFindings.map((finding) => {
|
|
2320
|
+
const findings = await Promise.all(originalFindings.map(async (finding) => {
|
|
1849
2321
|
const id = findingId(finding);
|
|
1850
2322
|
const now = currentById.get(id);
|
|
1851
2323
|
const base = { id, original: finding, ...now ? { current: now } : {} };
|
|
1852
2324
|
if (diagnostics.length || !current) {
|
|
1853
2325
|
return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
|
|
1854
2326
|
}
|
|
1855
|
-
if ("confidence" in finding
|
|
2327
|
+
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
1856
2328
|
return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
|
|
1857
2329
|
}
|
|
1858
2330
|
const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
|
|
1859
2331
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
1860
2332
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
1861
2333
|
}
|
|
1862
|
-
|
|
2334
|
+
const review = await assessAdvisory(root, now ?? finding, original.headHash);
|
|
2335
|
+
if (review) {
|
|
2336
|
+
if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
|
|
2337
|
+
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
2338
|
+
return { ...base, review, status: "review-required", reason: review.reason };
|
|
2339
|
+
}
|
|
2340
|
+
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
1863
2341
|
return {
|
|
1864
2342
|
...base,
|
|
1865
2343
|
status: "review-required",
|
|
@@ -1867,11 +2345,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
1867
2345
|
};
|
|
1868
2346
|
}
|
|
1869
2347
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
1870
|
-
});
|
|
2348
|
+
}));
|
|
2349
|
+
try {
|
|
2350
|
+
if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
|
|
2351
|
+
diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
|
|
2352
|
+
for (const finding of findings) {
|
|
2353
|
+
finding.status = "unverified";
|
|
2354
|
+
finding.reason = diagnostics.at(-1);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
} catch (error) {
|
|
2358
|
+
diagnostics.push(String(error));
|
|
2359
|
+
for (const finding of findings) {
|
|
2360
|
+
finding.status = "unverified";
|
|
2361
|
+
finding.reason = "Advisory review evidence became unavailable during verification.";
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
1871
2364
|
const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
|
|
2365
|
+
const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
1872
2366
|
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
1873
2367
|
for (const f of findings) counts[f.status]++;
|
|
1874
|
-
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f));
|
|
2368
|
+
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)));
|
|
1875
2369
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
1876
2370
|
return {
|
|
1877
2371
|
version: 1,
|
|
@@ -1885,7 +2379,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
1885
2379
|
diagnostics,
|
|
1886
2380
|
currentAudit: current,
|
|
1887
2381
|
counts,
|
|
1888
|
-
scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by
|
|
2382
|
+
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."
|
|
1889
2383
|
};
|
|
1890
2384
|
}
|
|
1891
2385
|
function repairExitCode(report) {
|
|
@@ -1894,18 +2388,482 @@ function repairExitCode(report) {
|
|
|
1894
2388
|
function formatRepairSummary(report) {
|
|
1895
2389
|
return [
|
|
1896
2390
|
"Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
|
|
1897
|
-
...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason),
|
|
2391
|
+
...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason + "\n findingId: " + f.id),
|
|
1898
2392
|
...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
|
|
1899
2393
|
...report.diagnostics.map((d) => " [unverified] " + d),
|
|
1900
2394
|
...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
|
|
1901
2395
|
report.scope
|
|
1902
2396
|
].join("\n");
|
|
1903
2397
|
}
|
|
2398
|
+
var count2, reportSchema, baselineSchema, repairFindingSchema;
|
|
2399
|
+
var init_repair = __esm({
|
|
2400
|
+
"src/audit/repair.ts"() {
|
|
2401
|
+
"use strict";
|
|
2402
|
+
init_audit();
|
|
2403
|
+
init_docs();
|
|
2404
|
+
init_snapshot();
|
|
2405
|
+
init_drift();
|
|
2406
|
+
init_storage();
|
|
2407
|
+
init_files();
|
|
2408
|
+
init_paths();
|
|
2409
|
+
init_types();
|
|
2410
|
+
init_findings();
|
|
2411
|
+
init_advisory_review();
|
|
2412
|
+
init_findings();
|
|
2413
|
+
count2 = z6.number().int().nonnegative();
|
|
2414
|
+
reportSchema = z6.object({
|
|
2415
|
+
version: z6.literal(1),
|
|
2416
|
+
root: z6.string(),
|
|
2417
|
+
gitAvailable: z6.literal(true),
|
|
2418
|
+
headHash: commitSchema.shape.hash,
|
|
2419
|
+
checksRun: z6.array(checkSchema).nonempty(),
|
|
2420
|
+
docs: z6.array(z6.object({
|
|
2421
|
+
path: z6.string().refine((value) => normalizeRepoPath(value) === value),
|
|
2422
|
+
scope: z6.string().optional(),
|
|
2423
|
+
kind: z6.enum(["instructions", "readme"]).optional(),
|
|
2424
|
+
lastCommit: commitSchema.nullable(),
|
|
2425
|
+
dirty: z6.boolean(),
|
|
2426
|
+
lineCount: count2
|
|
2427
|
+
})).nonempty(),
|
|
2428
|
+
decisionsChecked: z6.boolean(),
|
|
2429
|
+
clean: z6.boolean(),
|
|
2430
|
+
issues: z6.array(issueSchema),
|
|
2431
|
+
advisories: z6.array(advisorySchema),
|
|
2432
|
+
suppressedAdvisories: z6.array(advisorySchema).optional(),
|
|
2433
|
+
skippedChecks: z6.array(z6.object({ check: z6.string(), reason: z6.string(), doc: z6.string().optional() }))
|
|
2434
|
+
});
|
|
2435
|
+
baselineSchema = z6.object({
|
|
2436
|
+
kind: z6.literal("mason-audit-repair"),
|
|
2437
|
+
version: z6.literal(1),
|
|
2438
|
+
createdAt: z6.string().datetime(),
|
|
2439
|
+
report: reportSchema,
|
|
2440
|
+
digest: z6.string().regex(/^[a-f0-9]{64}$/)
|
|
2441
|
+
});
|
|
2442
|
+
repairFindingSchema = z6.object({
|
|
2443
|
+
id: z6.string(),
|
|
2444
|
+
original: z6.union([issueSchema, advisorySchema]),
|
|
2445
|
+
status: z6.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
2446
|
+
reason: z6.string(),
|
|
2447
|
+
current: z6.union([issueSchema, advisorySchema]).optional(),
|
|
2448
|
+
review: reviewSummarySchema.optional()
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
});
|
|
2452
|
+
|
|
2453
|
+
// src/audit/advisory-review.ts
|
|
2454
|
+
import fs11 from "fs/promises";
|
|
2455
|
+
import { execFile as execFile8 } from "child_process";
|
|
2456
|
+
import { promisify as promisify8 } from "util";
|
|
2457
|
+
import { z as z7 } from "zod";
|
|
2458
|
+
async function readRecord(root, id) {
|
|
2459
|
+
const raw = await readStoreJson(root, recordPath(id));
|
|
2460
|
+
if (raw === null) return null;
|
|
2461
|
+
const record = recordSchema.parse(raw);
|
|
2462
|
+
const { digest: stored, ...payload } = record;
|
|
2463
|
+
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]))) {
|
|
2464
|
+
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2465
|
+
}
|
|
2466
|
+
return record;
|
|
2467
|
+
}
|
|
2468
|
+
function scopes(finding) {
|
|
2469
|
+
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2470
|
+
if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
2471
|
+
if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
2472
|
+
if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
2473
|
+
else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
2474
|
+
else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
|
|
2475
|
+
else throw new Error("This advisory has no supported review scope.");
|
|
2476
|
+
return [...new Set(files)].map((file) => {
|
|
2477
|
+
if (normalizeRepoPath(file) !== file) throw new Error("Advisory contains an invalid scoped path: " + file);
|
|
2478
|
+
return `:(literal)${file}`;
|
|
2479
|
+
}).sort();
|
|
2480
|
+
}
|
|
2481
|
+
async function reviewEvidence(root, finding) {
|
|
2482
|
+
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
|
|
2483
|
+
const scope = scopes(finding);
|
|
2484
|
+
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2485
|
+
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2486
|
+
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
2487
|
+
const [tree, status, lastScopeCommit, document] = await Promise.all([
|
|
2488
|
+
tracked.length ? git2(root, "ls-tree", "-r", "-z", "HEAD", "--", ...tracked.map((file) => `:(literal)${file}`)) : "",
|
|
2489
|
+
git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
|
|
2490
|
+
git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
|
|
2491
|
+
readAuditInput(root, finding.anchor.doc)
|
|
2492
|
+
]);
|
|
2493
|
+
if (!document?.trim()) throw new Error("Original advisory document is missing, empty or unreadable.");
|
|
2494
|
+
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.");
|
|
2495
|
+
for (const entry of tree.split("\0").filter(Boolean)) {
|
|
2496
|
+
const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
|
|
2497
|
+
if (entry.startsWith("120000 ") || entry.startsWith("160000 ")) throw new Error("Symlink or submodule in advisory review scope: " + file);
|
|
2498
|
+
await storePath(root, file);
|
|
2499
|
+
}
|
|
2500
|
+
const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
|
|
2501
|
+
(finding.evidence.scope?.candidates ?? [finding.evidence.resolvedPath ?? finding.evidence.claimed]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
|
|
2502
|
+
) : [];
|
|
2503
|
+
let dirty = status.length > 0;
|
|
2504
|
+
if (finding.evidence.kind === "missing-script") {
|
|
2505
|
+
for (const file of [...finding.evidence.manifestsChecked, ...finding.evidence.scope?.candidates ?? []]) {
|
|
2506
|
+
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
|
|
2510
|
+
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
|
|
2511
|
+
return {
|
|
2512
|
+
...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
2513
|
+
dirty,
|
|
2514
|
+
documentDigest: digest(document)
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
async function decisionAssessment(root, finding, baselineHead) {
|
|
2518
|
+
const evidence = finding.evidence;
|
|
2519
|
+
if (evidence.kind !== "decision-anchor") return null;
|
|
2520
|
+
const id = findingId(finding);
|
|
2521
|
+
const store = await loadDecisionStore(root);
|
|
2522
|
+
if (store.diagnostics.length) throw new Error("Decision records have unavailable or malformed evidence; review cannot be confirmed.");
|
|
2523
|
+
const record = store.records.find((record2) => record2.id === evidence.decisionId);
|
|
2524
|
+
if (!record || record.version !== 2) return null;
|
|
2525
|
+
const events = record.history.filter((event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind));
|
|
2526
|
+
const event = events.at(-1);
|
|
2527
|
+
if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length) return null;
|
|
2528
|
+
try {
|
|
2529
|
+
await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
|
|
2530
|
+
} catch {
|
|
2531
|
+
return null;
|
|
2532
|
+
}
|
|
2533
|
+
const drift = await computeDecisionDrift(root, [record]);
|
|
2534
|
+
if (!drift.historyAvailable) throw new Error("Decision review history is unavailable.");
|
|
2535
|
+
const retired = record.status === "retired" && event.kind === "retired";
|
|
2536
|
+
const current = retired || record.status === "active" && record.approval === "accepted" && drift.freshness?.[record.id] === "current" && !drift.pendingProposals?.[record.id];
|
|
2537
|
+
return {
|
|
2538
|
+
id,
|
|
2539
|
+
status: current ? "current" : "reopened",
|
|
2540
|
+
outcome: event.kind,
|
|
2541
|
+
reviewer: event.actor,
|
|
2542
|
+
note: event.note,
|
|
2543
|
+
reviewedHead: event.evidence.headHash,
|
|
2544
|
+
recordPath: `.mason/decisions/${record.id}.json`,
|
|
2545
|
+
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."
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
async function assessAdvisory(root, finding, baselineHead) {
|
|
2549
|
+
if ("confidence" in finding) return null;
|
|
2550
|
+
const id = findingId(finding);
|
|
2551
|
+
try {
|
|
2552
|
+
if (finding.evidence.kind === "decision-anchor") return await decisionAssessment(root, finding, baselineHead);
|
|
2553
|
+
const record = await readRecord(root, id);
|
|
2554
|
+
if (!record) return null;
|
|
2555
|
+
const event = record.events.at(-1);
|
|
2556
|
+
const current = await reviewEvidence(root, event.finding);
|
|
2557
|
+
await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
|
|
2558
|
+
const base = {
|
|
2559
|
+
id,
|
|
2560
|
+
recordPath: recordPath(id),
|
|
2561
|
+
reviewer: event.reviewer,
|
|
2562
|
+
note: event.note,
|
|
2563
|
+
outcome: event.outcome,
|
|
2564
|
+
reviewedHead: event.evidence.head
|
|
2565
|
+
};
|
|
2566
|
+
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." };
|
|
2567
|
+
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." };
|
|
2568
|
+
} catch (error) {
|
|
2569
|
+
return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
async function advisoryReviewInventory(root) {
|
|
2573
|
+
let names;
|
|
2574
|
+
try {
|
|
2575
|
+
names = await fs11.readdir(await storePath(root, ".mason/reviews/advisories"));
|
|
2576
|
+
} catch (error) {
|
|
2577
|
+
if (error.code === "ENOENT") return [];
|
|
2578
|
+
throw error;
|
|
2579
|
+
}
|
|
2580
|
+
if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
|
|
2581
|
+
return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
|
|
2582
|
+
}
|
|
2583
|
+
async function reviewAdvisory(rootDir, input) {
|
|
2584
|
+
const root = await fs11.realpath(rootDir), request = advisoryReviewRequest.parse(input);
|
|
2585
|
+
const { loadRepairBaseline: loadRepairBaseline2 } = await Promise.resolve().then(() => (init_repair(), repair_exports));
|
|
2586
|
+
const baseline = await loadRepairBaseline2(root, request.baselinePath);
|
|
2587
|
+
const finding = [...baseline.original.advisories, ...baseline.original.suppressedAdvisories ?? []].find((f) => findingId(f) === request.findingId);
|
|
2588
|
+
if (!finding) throw new Error("No advisory with this findingId in the original baseline. Provable issues must be repaired and rechecked.");
|
|
2589
|
+
if (finding.evidence.kind === "decision-anchor") return {
|
|
2590
|
+
status: "decision-review-required",
|
|
2591
|
+
decisionId: finding.evidence.decisionId,
|
|
2592
|
+
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."
|
|
2593
|
+
};
|
|
2594
|
+
const prepare = async () => {
|
|
2595
|
+
const record = await readRecord(root, request.findingId);
|
|
2596
|
+
const evidence = await reviewEvidence(root, finding);
|
|
2597
|
+
await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
|
|
2598
|
+
const token = digest({ baseline: baseline.original, finding, evidence, record });
|
|
2599
|
+
return { record, evidence, token };
|
|
2600
|
+
};
|
|
2601
|
+
if (request.action === "prepare") {
|
|
2602
|
+
const state = await prepare();
|
|
2603
|
+
let diff = null, diffNote;
|
|
2604
|
+
const from = finding.evidence.kind === "doc-behind-manifests" ? finding.evidence.docLastCommit.hash : baseline.original.headHash;
|
|
2605
|
+
try {
|
|
2606
|
+
const full = await git2(root, "diff", "--no-ext-diff", "--no-textconv", "--no-color", "--no-renames", "-U3", from, state.evidence.head, "--", ...state.evidence.scope);
|
|
2607
|
+
diff = full.slice(0, 16e3);
|
|
2608
|
+
if (full.length > diff.length) diffNote = "Diff preview truncated; inspect the full scoped diff before reviewing.";
|
|
2609
|
+
} catch {
|
|
2610
|
+
diffNote = "Diff preview unavailable; inspect the original cited evidence directly before reviewing.";
|
|
2611
|
+
}
|
|
2612
|
+
if ((await prepare()).token !== state.token) throw new Error("Review evidence changed during preparation; retry.");
|
|
2613
|
+
return {
|
|
2614
|
+
status: "prepared",
|
|
2615
|
+
findingId: request.findingId,
|
|
2616
|
+
finding,
|
|
2617
|
+
evidence: state.evidence,
|
|
2618
|
+
history: state.record?.events ?? [],
|
|
2619
|
+
reviewToken: state.token,
|
|
2620
|
+
diff,
|
|
2621
|
+
diffNote,
|
|
2622
|
+
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."
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
if (!request.reviewer || !request.note || !request.reviewToken) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
|
|
2626
|
+
return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
|
|
2627
|
+
const state = await prepare();
|
|
2628
|
+
if (state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
|
|
2629
|
+
if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
|
|
2630
|
+
if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
|
|
2631
|
+
const { dirty, documentDigest, ...evidence } = state.evidence;
|
|
2632
|
+
const event = eventSchema2.parse({
|
|
2633
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2634
|
+
reviewer: request.reviewer,
|
|
2635
|
+
note: request.note,
|
|
2636
|
+
outcome: request.action,
|
|
2637
|
+
finding,
|
|
2638
|
+
evidence
|
|
2639
|
+
});
|
|
2640
|
+
const payload = { version: 1, id: request.findingId, events: [...state.record?.events ?? [], event] };
|
|
2641
|
+
if ((await prepare()).token !== state.token) throw new Error("Review conflict: evidence changed while recording the assessment.");
|
|
2642
|
+
await writeStoreJson(root, recordPath(request.findingId), { ...payload, digest: digest(payload) });
|
|
2643
|
+
return {
|
|
2644
|
+
status: "recorded",
|
|
2645
|
+
recordPath: recordPath(request.findingId),
|
|
2646
|
+
event,
|
|
2647
|
+
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."
|
|
2648
|
+
};
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
var execute2, git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
2652
|
+
var init_advisory_review = __esm({
|
|
2653
|
+
"src/audit/advisory-review.ts"() {
|
|
2654
|
+
"use strict";
|
|
2655
|
+
init_findings();
|
|
2656
|
+
init_deps_changed();
|
|
2657
|
+
init_inputs();
|
|
2658
|
+
init_scope();
|
|
2659
|
+
init_storage();
|
|
2660
|
+
init_paths();
|
|
2661
|
+
init_store();
|
|
2662
|
+
init_decisions();
|
|
2663
|
+
init_drift2();
|
|
2664
|
+
execute2 = promisify8(execFile8);
|
|
2665
|
+
git2 = async (root, ...args) => (await execute2("git", args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
2666
|
+
hashSchema = z7.string().regex(/^[a-f0-9]{64}$/);
|
|
2667
|
+
commitSchema2 = z7.string().regex(/^[a-f0-9]{40,64}$/);
|
|
2668
|
+
evidenceSchema2 = z7.object({
|
|
2669
|
+
head: commitSchema2,
|
|
2670
|
+
fingerprint: hashSchema,
|
|
2671
|
+
scope: z7.array(z7.string()).min(1).max(1e3),
|
|
2672
|
+
tree: z7.string().max(2 * 1024 * 1024),
|
|
2673
|
+
lastScopeCommit: commitSchema2,
|
|
2674
|
+
paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
|
|
2675
|
+
});
|
|
2676
|
+
eventSchema2 = z7.object({
|
|
2677
|
+
at: z7.string().datetime(),
|
|
2678
|
+
reviewer: z7.string().trim().min(1).max(200),
|
|
2679
|
+
note: z7.string().trim().min(1).max(2500),
|
|
2680
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred"]),
|
|
2681
|
+
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2682
|
+
evidence: evidenceSchema2
|
|
2683
|
+
});
|
|
2684
|
+
recordSchema = z7.object({ version: z7.literal(1), id: hashSchema, events: z7.array(eventSchema2).min(1).max(200), digest: hashSchema });
|
|
2685
|
+
reviewSummarySchema = z7.object({
|
|
2686
|
+
id: hashSchema,
|
|
2687
|
+
status: z7.enum(["current", "reopened", "deferred", "unverified"]),
|
|
2688
|
+
reason: z7.string(),
|
|
2689
|
+
recordPath: z7.string().optional(),
|
|
2690
|
+
reviewer: z7.string().optional(),
|
|
2691
|
+
note: z7.string().optional(),
|
|
2692
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
|
|
2693
|
+
reviewedHead: z7.string().optional()
|
|
2694
|
+
});
|
|
2695
|
+
advisoryReviewRequest = z7.object({
|
|
2696
|
+
baselinePath: z7.string().min(1),
|
|
2697
|
+
findingId: hashSchema,
|
|
2698
|
+
action: z7.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
|
|
2699
|
+
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2700
|
+
note: eventSchema2.shape.note.optional(),
|
|
2701
|
+
reviewToken: hashSchema.optional()
|
|
2702
|
+
});
|
|
2703
|
+
recordPath = (id) => `.mason/reviews/advisories/${hashSchema.parse(id)}.json`;
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
|
|
2707
|
+
// src/audit/audit.ts
|
|
2708
|
+
import fs12 from "fs/promises";
|
|
2709
|
+
import path19 from "path";
|
|
2710
|
+
async function computeAudit(rootDir, options = {}) {
|
|
2711
|
+
const resolvedRoot = path19.resolve(rootDir);
|
|
2712
|
+
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2713
|
+
if (docs.length === 0) return null;
|
|
2714
|
+
const report = {
|
|
2715
|
+
version: 1,
|
|
2716
|
+
root: resolvedRoot,
|
|
2717
|
+
gitAvailable: headHash !== "unknown",
|
|
2718
|
+
headHash,
|
|
2719
|
+
checksRun: [],
|
|
2720
|
+
docs: docs.map((d) => ({
|
|
2721
|
+
path: d.path,
|
|
2722
|
+
scope: d.scope,
|
|
2723
|
+
kind: d.kind,
|
|
2724
|
+
lastCommit: d.lastCommit,
|
|
2725
|
+
dirty: d.dirty,
|
|
2726
|
+
lineCount: d.lineCount
|
|
2727
|
+
})),
|
|
2728
|
+
decisionsChecked: false,
|
|
2729
|
+
issues: [],
|
|
2730
|
+
advisories: [],
|
|
2731
|
+
suppressedAdvisories: [],
|
|
2732
|
+
skippedChecks: [],
|
|
2733
|
+
clean: true
|
|
2734
|
+
};
|
|
2735
|
+
if (!report.gitAvailable) return report;
|
|
2736
|
+
const changesSinceDoc = /* @__PURE__ */ new Map();
|
|
2737
|
+
for (const doc of docs) {
|
|
2738
|
+
changesSinceDoc.set(
|
|
2739
|
+
doc.path,
|
|
2740
|
+
doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
let decisionsPresent = false;
|
|
2744
|
+
try {
|
|
2745
|
+
await fs12.access(path19.join(resolvedRoot, ".mason", "decisions"));
|
|
2746
|
+
decisionsPresent = true;
|
|
2747
|
+
} catch {
|
|
2748
|
+
}
|
|
2749
|
+
report.decisionsChecked = decisionsPresent;
|
|
2750
|
+
const ctx = {
|
|
2751
|
+
root: resolvedRoot,
|
|
2752
|
+
docs,
|
|
2753
|
+
headHash,
|
|
2754
|
+
changesSinceDoc,
|
|
2755
|
+
decisionsPresent
|
|
2756
|
+
};
|
|
2757
|
+
const selected = options.checks ?? ALL_CHECKS;
|
|
2758
|
+
for (const name of ALL_CHECKS) {
|
|
2759
|
+
if (!selected.includes(name)) continue;
|
|
2760
|
+
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
|
|
2761
|
+
report.checksRun.push(name);
|
|
2762
|
+
report.issues.push(...issues);
|
|
2763
|
+
report.advisories.push(...advisories);
|
|
2764
|
+
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
2765
|
+
report.skippedChecks.push(...skipped);
|
|
2766
|
+
}
|
|
2767
|
+
report.clean = report.issues.length === 0;
|
|
2768
|
+
const reviews = (await Promise.all([...report.advisories, ...report.suppressedAdvisories].map((finding) => assessAdvisory(resolvedRoot, finding, headHash)))).filter((review) => review !== null);
|
|
2769
|
+
if (reviews.length) report.advisoryReviews = reviews;
|
|
2770
|
+
return report;
|
|
2771
|
+
}
|
|
2772
|
+
var init_audit = __esm({
|
|
2773
|
+
"src/audit/audit.ts"() {
|
|
2774
|
+
"use strict";
|
|
2775
|
+
init_drift();
|
|
2776
|
+
init_snapshot();
|
|
2777
|
+
init_docs();
|
|
2778
|
+
init_types();
|
|
2779
|
+
init_checks();
|
|
2780
|
+
init_advisory_review();
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
// src/audit/cli.ts
|
|
2785
|
+
init_audit();
|
|
2786
|
+
init_types();
|
|
2787
|
+
init_repair();
|
|
2788
|
+
init_findings();
|
|
2789
|
+
import path21 from "path";
|
|
2790
|
+
|
|
2791
|
+
// src/audit/advisory-cli.ts
|
|
2792
|
+
init_advisory_review();
|
|
2793
|
+
import path20 from "path";
|
|
2794
|
+
import { stripVTControlCharacters } from "util";
|
|
2795
|
+
var USAGE = `Usage: mason audit review --baseline <path> --finding <id> [options]
|
|
2796
|
+
|
|
2797
|
+
Prepare an advisory assessment against retained evidence (read-only by default).
|
|
2798
|
+
--dir <path> Repository root (default: current directory)
|
|
2799
|
+
--baseline <path> Original repair baseline
|
|
2800
|
+
--finding <id> findingId from repair verification or automation
|
|
2801
|
+
--outcome <value> Record addressed, inapplicable, or deferred
|
|
2802
|
+
--reviewer <name> Actual authorized reviewer
|
|
2803
|
+
--note <reason> Assessment reason
|
|
2804
|
+
--token <token> reviewToken returned by preparation
|
|
2805
|
+
--json Full evidence and review history
|
|
2806
|
+
|
|
2807
|
+
Recording requires prior preparation and committed relevant edits. Review records
|
|
2808
|
+
belong in Git under .mason/reviews/advisories/. Deferral remains outstanding.
|
|
2809
|
+
Decision findings use review_decision; this command never approves decisions.
|
|
2810
|
+
Exit 0: prepared/recorded; 2: unavailable, conflict, or decision review required.`;
|
|
2811
|
+
async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), err: (text2) => console.error(text2) }) {
|
|
2812
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2813
|
+
io.out(USAGE);
|
|
2814
|
+
return 0;
|
|
2815
|
+
}
|
|
2816
|
+
try {
|
|
2817
|
+
const flags = {};
|
|
2818
|
+
let json = false;
|
|
2819
|
+
for (let i = 0; i < args.length; i++) {
|
|
2820
|
+
const flag = args[i];
|
|
2821
|
+
if (flag === "--json") {
|
|
2822
|
+
json = true;
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
if (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
|
|
2826
|
+
const value = args[++i];
|
|
2827
|
+
if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
|
|
2828
|
+
flags[flag] = value;
|
|
2829
|
+
}
|
|
2830
|
+
if (!flags["--outcome"] && ["--reviewer", "--note", "--token"].some((key) => flags[key])) throw new Error("Use --outcome to record an assessment.");
|
|
2831
|
+
const input = {
|
|
2832
|
+
baselinePath: flags["--baseline"],
|
|
2833
|
+
findingId: flags["--finding"],
|
|
2834
|
+
action: flags["--outcome"] ?? "prepare",
|
|
2835
|
+
reviewer: flags["--reviewer"],
|
|
2836
|
+
note: flags["--note"],
|
|
2837
|
+
reviewToken: flags["--token"]
|
|
2838
|
+
};
|
|
2839
|
+
const result = await reviewAdvisory(path20.resolve(flags["--dir"] ?? process.cwd()), input);
|
|
2840
|
+
if (json) io.out(JSON.stringify(result, null, 2));
|
|
2841
|
+
else {
|
|
2842
|
+
const lines = [`Advisory review: ${result.status}.`];
|
|
2843
|
+
if ("finding" in result && result.finding && result.evidence) lines.push(
|
|
2844
|
+
`[${result.finding.type}] ${result.finding.anchor.doc}: ${result.finding.message}`,
|
|
2845
|
+
`Finding: ${result.findingId}`,
|
|
2846
|
+
`Prepared against commit ${result.evidence.head.slice(0, 12)}.`,
|
|
2847
|
+
result.evidence.dirty ? "Relevant files have uncommitted edits; commit them and prepare again before recording." : "Relevant files are committed.",
|
|
2848
|
+
`Review token: ${result.reviewToken}`,
|
|
2849
|
+
...result.diff ? ["Scoped changes:", result.diff] : [],
|
|
2850
|
+
...result.diffNote ? [result.diffNote] : []
|
|
2851
|
+
);
|
|
2852
|
+
if ("event" in result) lines.push(`${result.event.outcome} \u2014 ${result.event.reviewer}: ${result.event.note}`, `Record: ${result.recordPath}`);
|
|
2853
|
+
lines.push(result.hint);
|
|
2854
|
+
io.out(stripVTControlCharacters(lines.join("\n")));
|
|
2855
|
+
}
|
|
2856
|
+
return result.status === "decision-review-required" ? 2 : 0;
|
|
2857
|
+
} catch (error) {
|
|
2858
|
+
io.err(stripVTControlCharacters(error instanceof Error ? error.message : String(error)));
|
|
2859
|
+
return 2;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
1904
2862
|
|
|
1905
2863
|
// src/audit/cli.ts
|
|
1906
|
-
var
|
|
2864
|
+
var USAGE2 = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]
|
|
1907
2865
|
|
|
1908
|
-
Audits
|
|
2866
|
+
Audits README.md and agent instruction files at any depth, preserving filename case
|
|
1909
2867
|
against repo reality: referenced paths that no longer exist, undocumented
|
|
1910
2868
|
modules, stale counts, dead npm scripts, and manifests newer than the doc.
|
|
1911
2869
|
Deterministic: no LLM call, no network \u2013 safe for CI. Works on any repo with
|
|
@@ -1924,6 +2882,8 @@ Options:
|
|
|
1924
2882
|
${ALL_CHECKS.join(", ")}
|
|
1925
2883
|
--help Show this help
|
|
1926
2884
|
|
|
2885
|
+
Use mason audit review --help to prepare and record an authorized advisory assessment.
|
|
2886
|
+
|
|
1927
2887
|
Exit codes:
|
|
1928
2888
|
0 no issues (advisories may still be present)
|
|
1929
2889
|
1 provable issues found
|
|
@@ -1987,7 +2947,10 @@ function issueLine(issue) {
|
|
|
1987
2947
|
}
|
|
1988
2948
|
function formatAuditSummary(report) {
|
|
1989
2949
|
const lines = [];
|
|
1990
|
-
const
|
|
2950
|
+
const reviewed = new Set(report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
|
|
2951
|
+
const open = report.advisories.filter((finding) => !reviewed.has(findingId(finding)));
|
|
2952
|
+
const suppressed = (report.suppressedAdvisories ?? []).filter((finding) => !reviewed.has(findingId(finding)));
|
|
2953
|
+
const reviewCount = open.length + suppressed.length;
|
|
1991
2954
|
for (const doc of report.docs) {
|
|
1992
2955
|
const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
|
|
1993
2956
|
const committed = doc.lastCommit ? `last committed ${doc.lastCommit.date.slice(0, 10)}, ${doc.lastCommit.hash.slice(0, 7)}` : "untracked";
|
|
@@ -2004,9 +2967,9 @@ function formatAuditSummary(report) {
|
|
|
2004
2967
|
for (const issue of report.issues) {
|
|
2005
2968
|
if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
|
|
2006
2969
|
}
|
|
2007
|
-
if (
|
|
2970
|
+
if (open.length > 0) {
|
|
2008
2971
|
lines.push("Advisories (do not affect the exit code):");
|
|
2009
|
-
for (const advisory of
|
|
2972
|
+
for (const advisory of open) {
|
|
2010
2973
|
lines.push(` [${advisory.type}] ${advisory.anchor.doc}: ${advisory.message}`);
|
|
2011
2974
|
}
|
|
2012
2975
|
}
|
|
@@ -2015,7 +2978,9 @@ function formatAuditSummary(report) {
|
|
|
2015
2978
|
lines.push(` [skipped] ${skip.check}: ${skip.reason}`);
|
|
2016
2979
|
}
|
|
2017
2980
|
}
|
|
2018
|
-
|
|
2981
|
+
if (reviewed.size) lines.push(`${reviewed.size} advisories have recorded assessments covering current evidence (see --json).`);
|
|
2982
|
+
for (const review of report.advisoryReviews ?? []) if (review.status !== "current") lines.push(` [${review.status}] ${review.reason}`);
|
|
2983
|
+
for (const advisory of suppressed) {
|
|
2019
2984
|
lines.push(` [suppressed; unresolved] ${advisory.type} ${advisory.anchor.doc}: ${advisory.message}`);
|
|
2020
2985
|
}
|
|
2021
2986
|
lines.push(
|
|
@@ -2052,10 +3017,10 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
2052
3017
|
"- dead-command: replace with the correct script from availableScripts if an obvious rename exists; otherwise remove the command mention."
|
|
2053
3018
|
);
|
|
2054
3019
|
lines.push(
|
|
2055
|
-
"- new-module:
|
|
3020
|
+
"- new-module: review whether the directory needs documenting; an omission alone does not establish a defect. Preserve scope and describe only what you verified."
|
|
2056
3021
|
);
|
|
2057
3022
|
lines.push(
|
|
2058
|
-
"- ADVISORIES require a separate assessment
|
|
3023
|
+
"- 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."
|
|
2059
3024
|
);
|
|
2060
3025
|
lines.push("");
|
|
2061
3026
|
lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
|
|
@@ -2075,7 +3040,7 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
2075
3040
|
);
|
|
2076
3041
|
lines.push("");
|
|
2077
3042
|
lines.push(
|
|
2078
|
-
"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
|
|
3043
|
+
"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."
|
|
2079
3044
|
);
|
|
2080
3045
|
return lines.join("\n");
|
|
2081
3046
|
}
|
|
@@ -2085,6 +3050,7 @@ async function runAuditCli(argv, io = {
|
|
|
2085
3050
|
err: (line) => process.stderr.write(`${line}
|
|
2086
3051
|
`)
|
|
2087
3052
|
}) {
|
|
3053
|
+
if (argv[0] === "review") return runAdvisoryCli(argv.slice(1), io);
|
|
2088
3054
|
let args;
|
|
2089
3055
|
try {
|
|
2090
3056
|
args = parseArgs(argv);
|
|
@@ -2096,14 +3062,14 @@ async function runAuditCli(argv, io = {
|
|
|
2096
3062
|
}
|
|
2097
3063
|
} catch (error) {
|
|
2098
3064
|
io.err(error instanceof Error ? error.message : String(error));
|
|
2099
|
-
io.err(
|
|
3065
|
+
io.err(USAGE2);
|
|
2100
3066
|
return 2;
|
|
2101
3067
|
}
|
|
2102
3068
|
if (args.help) {
|
|
2103
|
-
io.out(
|
|
3069
|
+
io.out(USAGE2);
|
|
2104
3070
|
return 0;
|
|
2105
3071
|
}
|
|
2106
|
-
const rootDir =
|
|
3072
|
+
const rootDir = path21.resolve(args.dir);
|
|
2107
3073
|
if (args.baseline || args.prepareRepair) {
|
|
2108
3074
|
try {
|
|
2109
3075
|
if (args.baseline) {
|
|
@@ -2123,7 +3089,7 @@ ${formatAuditSummary(prepared.report)}`);
|
|
|
2123
3089
|
const report = await computeAudit(rootDir, { checks: args.checks });
|
|
2124
3090
|
if (!report) {
|
|
2125
3091
|
io.err(
|
|
2126
|
-
`No
|
|
3092
|
+
`No README.md or agent instruction files found in ${rootDir}.`
|
|
2127
3093
|
);
|
|
2128
3094
|
return 2;
|
|
2129
3095
|
}
|
|
@@ -2149,11 +3115,13 @@ ${formatAuditSummary(prepared.report)}`);
|
|
|
2149
3115
|
|
|
2150
3116
|
// bin/mason-audit.ts
|
|
2151
3117
|
runAuditCli(process.argv.slice(2)).then(
|
|
2152
|
-
(code) =>
|
|
3118
|
+
(code) => {
|
|
3119
|
+
process.exitCode = code;
|
|
3120
|
+
},
|
|
2153
3121
|
(err) => {
|
|
2154
3122
|
process.stderr.write(`mason-audit error: ${err}
|
|
2155
3123
|
`);
|
|
2156
|
-
process.
|
|
3124
|
+
process.exitCode = 2;
|
|
2157
3125
|
}
|
|
2158
3126
|
);
|
|
2159
3127
|
//# sourceMappingURL=mason-audit.js.map
|