mason-context 0.20.2 → 0.22.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 +24 -5
- package/dist/mason-audit.js +1755 -698
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js +662 -193
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +862 -198
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +8713 -8484
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +1088 -380
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +297 -148
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +7566 -5905
- package/dist/mason.js.map +1 -1
- package/package.json +11 -4
package/dist/mason-review.js
CHANGED
|
@@ -35,7 +35,7 @@ import path12 from "path";
|
|
|
35
35
|
import { stripVTControlCharacters } from "util";
|
|
36
36
|
|
|
37
37
|
// src/review/review.ts
|
|
38
|
-
import
|
|
38
|
+
import fs7 from "fs/promises";
|
|
39
39
|
import path11 from "path";
|
|
40
40
|
import { execFile as execFile3 } from "child_process";
|
|
41
41
|
import { promisify as promisify3 } from "util";
|
|
@@ -62,7 +62,19 @@ async function profilePhase(label, run) {
|
|
|
62
62
|
|
|
63
63
|
// src/utils/git-read.ts
|
|
64
64
|
var execute = promisify(execFile);
|
|
65
|
-
var commands = /* @__PURE__ */ new Set([
|
|
65
|
+
var commands = /* @__PURE__ */ new Set([
|
|
66
|
+
"rev-parse",
|
|
67
|
+
"symbolic-ref",
|
|
68
|
+
"for-each-ref",
|
|
69
|
+
"ls-files",
|
|
70
|
+
"check-ignore",
|
|
71
|
+
"status",
|
|
72
|
+
"log",
|
|
73
|
+
"diff",
|
|
74
|
+
"ls-tree",
|
|
75
|
+
"show",
|
|
76
|
+
"merge-base"
|
|
77
|
+
]);
|
|
66
78
|
function execGit(args, options) {
|
|
67
79
|
const command = args.find((arg) => !arg.startsWith("-")) ?? "other";
|
|
68
80
|
return profilePhase("git." + (commands.has(command) ? command : "other"), async () => {
|
|
@@ -79,10 +91,15 @@ function execGit(args, options) {
|
|
|
79
91
|
|
|
80
92
|
// src/drift/drift.ts
|
|
81
93
|
import fs4 from "fs/promises";
|
|
82
|
-
import
|
|
94
|
+
import path4 from "path";
|
|
83
95
|
|
|
84
|
-
// src/
|
|
85
|
-
|
|
96
|
+
// src/utils/validation.ts
|
|
97
|
+
function isRecord(value) {
|
|
98
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
99
|
+
}
|
|
100
|
+
function isStringArray(value) {
|
|
101
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
102
|
+
}
|
|
86
103
|
|
|
87
104
|
// src/utils/files.ts
|
|
88
105
|
import fs from "fs/promises";
|
|
@@ -94,7 +111,8 @@ import fg from "fast-glob";
|
|
|
94
111
|
import path from "path";
|
|
95
112
|
function normalizeRepoPath(value) {
|
|
96
113
|
const slash = value.replace(/\\/g, "/");
|
|
97
|
-
if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash))
|
|
114
|
+
if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash))
|
|
115
|
+
return null;
|
|
98
116
|
if (slash.split("/").includes("..")) return null;
|
|
99
117
|
const normalized = path.posix.normalize(slash).replace(/\/$/, "");
|
|
100
118
|
return normalized === "." ? null : normalized;
|
|
@@ -109,11 +127,69 @@ function anchorMatches(anchor, file) {
|
|
|
109
127
|
return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
|
|
110
128
|
}
|
|
111
129
|
function matchingPaths(anchors, files) {
|
|
112
|
-
return [...new Set(files)].filter(
|
|
130
|
+
return [...new Set(files)].filter(
|
|
131
|
+
(file) => anchors.some((anchor) => anchorMatches(anchor, file))
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
function indexMatchingPaths(scopes, files) {
|
|
135
|
+
const node = () => ({ children: /* @__PURE__ */ new Map(), scopes: /* @__PURE__ */ new Set() });
|
|
136
|
+
const root = node();
|
|
137
|
+
const matches = scopes.map(() => []);
|
|
138
|
+
scopes.forEach((anchors, index2) => {
|
|
139
|
+
for (const anchor of anchors) {
|
|
140
|
+
const normalized = normalizeRepoPath(anchor);
|
|
141
|
+
if (!normalized) continue;
|
|
142
|
+
let current = root;
|
|
143
|
+
for (const part of normalized.split("/")) {
|
|
144
|
+
if (!current.children.has(part)) current.children.set(part, node());
|
|
145
|
+
current = current.children.get(part);
|
|
146
|
+
}
|
|
147
|
+
current.scopes.add(index2);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
for (const file of new Set(files)) {
|
|
151
|
+
const normalized = normalizeRepoPath(file);
|
|
152
|
+
if (!normalized) continue;
|
|
153
|
+
const selected = /* @__PURE__ */ new Set();
|
|
154
|
+
let current = root;
|
|
155
|
+
for (const part of normalized.split("/")) {
|
|
156
|
+
current = current.children.get(part);
|
|
157
|
+
if (!current) break;
|
|
158
|
+
for (const index2 of current.scopes) selected.add(index2);
|
|
159
|
+
}
|
|
160
|
+
for (const index2 of selected) matches[index2].push(file);
|
|
161
|
+
}
|
|
162
|
+
return matches;
|
|
113
163
|
}
|
|
114
164
|
|
|
115
165
|
// src/utils/files.ts
|
|
116
|
-
var SOURCE_EXTENSIONS = [
|
|
166
|
+
var SOURCE_EXTENSIONS = [
|
|
167
|
+
"ts",
|
|
168
|
+
"tsx",
|
|
169
|
+
"js",
|
|
170
|
+
"jsx",
|
|
171
|
+
"mts",
|
|
172
|
+
"cts",
|
|
173
|
+
"mjs",
|
|
174
|
+
"cjs",
|
|
175
|
+
"vue",
|
|
176
|
+
"svelte",
|
|
177
|
+
"kt",
|
|
178
|
+
"kts",
|
|
179
|
+
"java",
|
|
180
|
+
"py",
|
|
181
|
+
"go",
|
|
182
|
+
"rs",
|
|
183
|
+
"swift",
|
|
184
|
+
"rb",
|
|
185
|
+
"cs",
|
|
186
|
+
"cpp",
|
|
187
|
+
"c",
|
|
188
|
+
"h",
|
|
189
|
+
"hpp",
|
|
190
|
+
"dart",
|
|
191
|
+
"php"
|
|
192
|
+
];
|
|
117
193
|
var SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
|
|
118
194
|
var SOURCE_IGNORE = [
|
|
119
195
|
"**/node_modules/**",
|
|
@@ -141,11 +217,16 @@ var SOURCE_IGNORE = [
|
|
|
141
217
|
var MAX_SOURCE_BYTES = 1024 * 1024;
|
|
142
218
|
function isSensitiveFile(file) {
|
|
143
219
|
return file.split(/[\\/]/).some(
|
|
144
|
-
(part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(
|
|
220
|
+
(part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(
|
|
221
|
+
part
|
|
222
|
+
)
|
|
145
223
|
);
|
|
146
224
|
}
|
|
147
225
|
async function readBoundedFile(file, maxBytes) {
|
|
148
|
-
const handle = await fs.open(
|
|
226
|
+
const handle = await fs.open(
|
|
227
|
+
file,
|
|
228
|
+
constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW
|
|
229
|
+
);
|
|
149
230
|
try {
|
|
150
231
|
const stat = await handle.stat();
|
|
151
232
|
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
@@ -165,36 +246,44 @@ async function loadProjectConfig(root) {
|
|
|
165
246
|
try {
|
|
166
247
|
const canonicalRoot = await fs.realpath(root);
|
|
167
248
|
const configPath = await fs.realpath(path2.join(root, ".mason/config.json"));
|
|
168
|
-
if (!isWithinRoot(canonicalRoot, configPath))
|
|
249
|
+
if (!isWithinRoot(canonicalRoot, configPath))
|
|
250
|
+
throw new Error("Project configuration resolves outside the repository");
|
|
169
251
|
const raw = await readBoundedFile(configPath, 64 * 1024);
|
|
170
|
-
if (raw === null)
|
|
252
|
+
if (raw === null)
|
|
253
|
+
throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
|
|
171
254
|
const value = JSON.parse(raw);
|
|
172
|
-
if (!
|
|
255
|
+
if (!isRecord(value)) throw new Error("Expected a configuration object");
|
|
173
256
|
const config = {};
|
|
174
257
|
for (const key of ["patterns", "alwaysInclude", "ignore"]) {
|
|
175
258
|
if (value[key] === void 0) continue;
|
|
176
|
-
if (!
|
|
259
|
+
if (!isStringArray(value[key])) {
|
|
177
260
|
throw new Error(`Configuration ${key} must be an array of strings`);
|
|
178
261
|
}
|
|
179
262
|
config[key] = value[key];
|
|
180
263
|
}
|
|
181
264
|
if (value.audit !== void 0) {
|
|
182
265
|
const audit = value.audit;
|
|
183
|
-
if (!
|
|
266
|
+
if (!isRecord(audit)) throw new Error("Configuration audit must be an object");
|
|
184
267
|
const patterns = (raw2) => {
|
|
185
|
-
if (!
|
|
186
|
-
|
|
268
|
+
if (!isStringArray(raw2) || raw2.length > 100 || !raw2.every(
|
|
269
|
+
(s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s)
|
|
270
|
+
)) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
"Audit patterns must be at most 100 repository-relative globs without traversal or negation"
|
|
273
|
+
);
|
|
187
274
|
}
|
|
188
275
|
return raw2;
|
|
189
276
|
};
|
|
190
277
|
config.audit = {};
|
|
191
278
|
if (audit.include !== void 0) config.audit.include = patterns(audit.include);
|
|
192
279
|
if (audit.exclude !== void 0) {
|
|
193
|
-
if (!
|
|
280
|
+
if (!isRecord(audit.exclude))
|
|
281
|
+
throw new Error("audit.exclude must map check names to patterns");
|
|
194
282
|
const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
|
|
195
283
|
config.audit.exclude = {};
|
|
196
284
|
for (const [check, globs] of Object.entries(audit.exclude)) {
|
|
197
|
-
if (!ALL_CHECKS2.includes(check))
|
|
285
|
+
if (!ALL_CHECKS2.includes(check))
|
|
286
|
+
throw new Error("Unknown audit check: " + check);
|
|
198
287
|
config.audit.exclude[check] = patterns(globs);
|
|
199
288
|
}
|
|
200
289
|
}
|
|
@@ -202,7 +291,10 @@ async function loadProjectConfig(root) {
|
|
|
202
291
|
return config;
|
|
203
292
|
} catch (error) {
|
|
204
293
|
if (error.code === "ENOENT") return {};
|
|
205
|
-
throw new Error(
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`,
|
|
296
|
+
{ cause: error }
|
|
297
|
+
);
|
|
206
298
|
}
|
|
207
299
|
}
|
|
208
300
|
async function createFileAccess(rootDir) {
|
|
@@ -212,7 +304,10 @@ async function createFileAccess(rootDir) {
|
|
|
212
304
|
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
213
305
|
let gitFiles = null;
|
|
214
306
|
try {
|
|
215
|
-
const { stdout } = await execGit(
|
|
307
|
+
const { stdout } = await execGit(
|
|
308
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
|
|
309
|
+
{ cwd: root, maxBuffer: 50 * 1024 * 1024 }
|
|
310
|
+
);
|
|
216
311
|
gitFiles = new Set(stdout.split("\0").filter(Boolean));
|
|
217
312
|
} catch {
|
|
218
313
|
let inGit = false;
|
|
@@ -225,14 +320,17 @@ async function createFileAccess(rootDir) {
|
|
|
225
320
|
}
|
|
226
321
|
async function resolve(file) {
|
|
227
322
|
const relative = normalizeRepoPath(file);
|
|
228
|
-
if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative))
|
|
323
|
+
if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative))
|
|
324
|
+
return null;
|
|
229
325
|
const candidate = path2.join(root, relative);
|
|
230
326
|
try {
|
|
231
327
|
const real = await fs.realpath(candidate);
|
|
232
|
-
if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path2.relative(canonicalRoot, real)))
|
|
328
|
+
if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path2.relative(canonicalRoot, real)))
|
|
329
|
+
return null;
|
|
233
330
|
const stat = await fs.stat(real);
|
|
234
331
|
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
|
|
235
|
-
if (gitFiles && !gitFiles.has(path2.relative(canonicalRoot, real).split(path2.sep).join("/")))
|
|
332
|
+
if (gitFiles && !gitFiles.has(path2.relative(canonicalRoot, real).split(path2.sep).join("/")))
|
|
333
|
+
return null;
|
|
236
334
|
return real;
|
|
237
335
|
} catch {
|
|
238
336
|
return null;
|
|
@@ -248,7 +346,10 @@ async function createFileAccess(rootDir) {
|
|
|
248
346
|
if (!relative) return null;
|
|
249
347
|
const real = await resolve(relative);
|
|
250
348
|
if (!real) return null;
|
|
251
|
-
for (const rel of /* @__PURE__ */ new Set([
|
|
349
|
+
for (const rel of /* @__PURE__ */ new Set([
|
|
350
|
+
relative,
|
|
351
|
+
path2.relative(canonicalRoot, real).split(path2.sep).join("/")
|
|
352
|
+
])) {
|
|
252
353
|
if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
|
|
253
354
|
}
|
|
254
355
|
try {
|
|
@@ -258,7 +359,7 @@ async function createFileAccess(rootDir) {
|
|
|
258
359
|
return null;
|
|
259
360
|
}
|
|
260
361
|
}
|
|
261
|
-
return { root, config, list, read };
|
|
362
|
+
return { root, config, list, read, inventory: () => gitFiles ? [...gitFiles].sort() : null };
|
|
262
363
|
}
|
|
263
364
|
|
|
264
365
|
// src/utils/storage.ts
|
|
@@ -300,112 +401,47 @@ async function readStoreJson(root, relative) {
|
|
|
300
401
|
return parsed;
|
|
301
402
|
} catch (error) {
|
|
302
403
|
if (error.code === "ENOENT") return null;
|
|
303
|
-
throw new Error(
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`,
|
|
406
|
+
{ cause: error }
|
|
407
|
+
);
|
|
304
408
|
}
|
|
305
409
|
}
|
|
306
410
|
|
|
307
411
|
// src/snapshot/snapshot.ts
|
|
308
|
-
import { z } from "zod";
|
|
412
|
+
import { z as z2 } from "zod";
|
|
309
413
|
|
|
310
414
|
// src/utils/store-lock.ts
|
|
415
|
+
import { z } from "zod";
|
|
311
416
|
import fs3 from "fs/promises";
|
|
312
417
|
import os from "os";
|
|
313
|
-
|
|
314
|
-
// src/test-map.ts
|
|
315
|
-
import path4 from "path";
|
|
316
|
-
async function buildTestMap(dir) {
|
|
317
|
-
const rootDir = path4.resolve(dir);
|
|
318
|
-
const access = await createFileAccess(rootDir);
|
|
319
|
-
const testPatterns = [
|
|
320
|
-
"**/*.test.*",
|
|
321
|
-
"**/*.spec.*",
|
|
322
|
-
"**/*Test.kt",
|
|
323
|
-
"**/*Test.java",
|
|
324
|
-
"**/*Tests.kt",
|
|
325
|
-
"**/*Tests.java",
|
|
326
|
-
"**/test_*.py",
|
|
327
|
-
"**/*_test.py",
|
|
328
|
-
"**/*_test.go",
|
|
329
|
-
"**/*Tests.swift",
|
|
330
|
-
"**/*Test.swift",
|
|
331
|
-
"**/*_test.rs"
|
|
332
|
-
];
|
|
333
|
-
const testFiles = await access.list(testPatterns);
|
|
334
|
-
const sourceFiles = await access.list();
|
|
335
|
-
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
336
|
-
for (const file of sourceFiles) {
|
|
337
|
-
if (testFiles.includes(file)) continue;
|
|
338
|
-
const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
|
|
339
|
-
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
340
|
-
existing.push(file);
|
|
341
|
-
sourceByBaseName.set(baseName, existing);
|
|
342
|
-
}
|
|
343
|
-
const paired = [];
|
|
344
|
-
const unmatched = [];
|
|
345
|
-
for (const testFile of testFiles) {
|
|
346
|
-
const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
|
|
347
|
-
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
348
|
-
if (!sourceName) {
|
|
349
|
-
unmatched.push(testFile);
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
const candidates = sourceByBaseName.get(sourceName);
|
|
353
|
-
if (candidates && candidates.length > 0) {
|
|
354
|
-
const testDir = path4.dirname(testFile);
|
|
355
|
-
const bestMatch = candidates.reduce((best, candidate) => {
|
|
356
|
-
const candidateDir = path4.dirname(candidate);
|
|
357
|
-
const bestDir = path4.dirname(best);
|
|
358
|
-
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
359
|
-
const bestOverlap = commonSegments(testDir, bestDir);
|
|
360
|
-
return candidateOverlap > bestOverlap ? candidate : best;
|
|
361
|
-
});
|
|
362
|
-
paired.push({
|
|
363
|
-
test: testFile,
|
|
364
|
-
source: bestMatch,
|
|
365
|
-
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
366
|
-
});
|
|
367
|
-
} else {
|
|
368
|
-
unmatched.push(testFile);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
372
|
-
}
|
|
373
|
-
function commonSegments(pathA, pathB) {
|
|
374
|
-
const segsA = pathA.split("/");
|
|
375
|
-
const segsB = pathB.split("/");
|
|
376
|
-
let count2 = 0;
|
|
377
|
-
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
378
|
-
if (segsA[i] === segsB[i]) count2++;
|
|
379
|
-
else break;
|
|
380
|
-
}
|
|
381
|
-
return count2;
|
|
382
|
-
}
|
|
418
|
+
var lockOwnerSchema = z.object({ host: z.string(), pid: z.number().int().positive() });
|
|
383
419
|
|
|
384
420
|
// src/snapshot/snapshot.ts
|
|
385
|
-
var repoPath =
|
|
421
|
+
var repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
386
422
|
var verificationFields = {
|
|
387
|
-
refreshedHash:
|
|
388
|
-
verifiedAt:
|
|
389
|
-
verifiedHash:
|
|
390
|
-
verificationToken:
|
|
391
|
-
verificationFailed:
|
|
392
|
-
verificationNote:
|
|
423
|
+
refreshedHash: z2.string().optional(),
|
|
424
|
+
verifiedAt: z2.string().optional(),
|
|
425
|
+
verifiedHash: z2.string().optional(),
|
|
426
|
+
verificationToken: z2.string().optional(),
|
|
427
|
+
verificationFailed: z2.boolean().optional(),
|
|
428
|
+
verificationNote: z2.string().optional()
|
|
393
429
|
};
|
|
394
|
-
var featureSchema =
|
|
395
|
-
description:
|
|
396
|
-
files:
|
|
397
|
-
tests:
|
|
398
|
-
type:
|
|
430
|
+
var featureSchema = z2.object({
|
|
431
|
+
description: z2.string(),
|
|
432
|
+
files: z2.array(repoPath),
|
|
433
|
+
tests: z2.array(repoPath).optional(),
|
|
434
|
+
type: z2.enum(["capability", "infrastructure"]).optional(),
|
|
399
435
|
...verificationFields
|
|
400
436
|
}).passthrough();
|
|
401
|
-
var flowSchema =
|
|
402
|
-
var snapshotSchema =
|
|
403
|
-
version:
|
|
404
|
-
createdAt:
|
|
405
|
-
updatedAt:
|
|
406
|
-
gitHash:
|
|
407
|
-
features:
|
|
408
|
-
flows:
|
|
437
|
+
var flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
|
|
438
|
+
var snapshotSchema = z2.object({
|
|
439
|
+
version: z2.literal(2),
|
|
440
|
+
createdAt: z2.string(),
|
|
441
|
+
updatedAt: z2.string(),
|
|
442
|
+
gitHash: z2.string(),
|
|
443
|
+
features: z2.record(featureSchema),
|
|
444
|
+
flows: z2.record(flowSchema)
|
|
409
445
|
}).passthrough();
|
|
410
446
|
async function getCurrentGitHash(rootDir) {
|
|
411
447
|
try {
|
|
@@ -428,18 +464,25 @@ function parseChanges(output) {
|
|
|
428
464
|
if (!first) break;
|
|
429
465
|
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
430
466
|
const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
|
|
431
|
-
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/")))
|
|
467
|
+
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/")))
|
|
468
|
+
continue;
|
|
432
469
|
changes.push(change);
|
|
433
470
|
}
|
|
434
471
|
return changes;
|
|
435
472
|
}
|
|
436
473
|
function touchedPaths(changes) {
|
|
437
|
-
return [
|
|
474
|
+
return [
|
|
475
|
+
...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))
|
|
476
|
+
].sort();
|
|
438
477
|
}
|
|
439
478
|
async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
440
|
-
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
|
|
479
|
+
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
|
|
480
|
+
return null;
|
|
441
481
|
try {
|
|
442
|
-
const { stdout } = await execGit(
|
|
482
|
+
const { stdout } = await execGit(
|
|
483
|
+
["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"],
|
|
484
|
+
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
485
|
+
);
|
|
443
486
|
return parseChanges(stdout);
|
|
444
487
|
} catch {
|
|
445
488
|
return null;
|
|
@@ -448,122 +491,341 @@ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
|
448
491
|
async function getWorkingTree(resolvedRoot) {
|
|
449
492
|
try {
|
|
450
493
|
const [diff, untracked] = await Promise.all([
|
|
451
|
-
execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], {
|
|
452
|
-
|
|
494
|
+
execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], {
|
|
495
|
+
cwd: resolvedRoot,
|
|
496
|
+
maxBuffer: 10 * 1024 * 1024
|
|
497
|
+
}),
|
|
498
|
+
execGit(["ls-files", "-z", "--others", "--exclude-standard"], {
|
|
499
|
+
cwd: resolvedRoot,
|
|
500
|
+
maxBuffer: 10 * 1024 * 1024
|
|
501
|
+
})
|
|
453
502
|
]);
|
|
454
503
|
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
455
|
-
return {
|
|
504
|
+
return {
|
|
505
|
+
available: true,
|
|
506
|
+
changedFiles: [
|
|
507
|
+
.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])
|
|
508
|
+
].sort(),
|
|
509
|
+
untrackedFiles
|
|
510
|
+
};
|
|
456
511
|
} catch {
|
|
457
512
|
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
458
513
|
}
|
|
459
514
|
}
|
|
460
515
|
|
|
461
516
|
// src/decisions/provenance.ts
|
|
462
|
-
import { z as
|
|
517
|
+
import { z as z4 } from "zod";
|
|
518
|
+
import { createHash as createHash2 } from "crypto";
|
|
519
|
+
|
|
520
|
+
// src/decisions/anchors.ts
|
|
521
|
+
import { createHash } from "crypto";
|
|
522
|
+
import fs5 from "fs/promises";
|
|
523
|
+
import { constants as constants2 } from "fs";
|
|
524
|
+
import { z as z3 } from "zod";
|
|
525
|
+
import fg3 from "fast-glob";
|
|
526
|
+
|
|
527
|
+
// src/audit/inspection.ts
|
|
528
|
+
import fg2 from "fast-glob";
|
|
529
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
530
|
+
import path5 from "path";
|
|
531
|
+
var inspections = new AsyncLocalStorage2();
|
|
532
|
+
function inspectionRead(root, key, read) {
|
|
533
|
+
const inspection = inspections.getStore();
|
|
534
|
+
if (!inspection || inspection.root !== path5.resolve(root)) return read();
|
|
535
|
+
if (!inspection.reads.has(key)) {
|
|
536
|
+
const pending = read();
|
|
537
|
+
inspection.reads.set(key, pending);
|
|
538
|
+
void pending.catch(() => {
|
|
539
|
+
if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return inspection.reads.get(key);
|
|
543
|
+
}
|
|
544
|
+
function inspectionGit(args, options) {
|
|
545
|
+
return inspectionRead(
|
|
546
|
+
options.cwd,
|
|
547
|
+
"git:" + JSON.stringify([args, options]),
|
|
548
|
+
() => execGit(args, options)
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
var sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
|
|
552
|
+
var sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
|
|
553
|
+
|
|
554
|
+
// src/decisions/anchors.ts
|
|
555
|
+
var repoPath2 = z3.string().refine((p) => normalizeRepoPath(p) === p);
|
|
556
|
+
var anchorCaptureSchema = z3.object({
|
|
557
|
+
anchors: z3.array(repoPath2),
|
|
558
|
+
files: z3.record(repoPath2, z3.string().regex(/^[a-f0-9]{64}$/)),
|
|
559
|
+
complete: z3.boolean()
|
|
560
|
+
}).refine(
|
|
561
|
+
(capture) => JSON.stringify(capture.anchors) === JSON.stringify([...new Set(capture.anchors)].sort()) && matchingPaths(capture.anchors, Object.keys(capture.files)).length === Object.keys(capture.files).length,
|
|
562
|
+
"Capture paths must belong to its normalized, sorted anchor scope"
|
|
563
|
+
);
|
|
564
|
+
async function captureAnchorScopes(root, scopes) {
|
|
565
|
+
const union = [...new Set(scopes.flat())].sort();
|
|
566
|
+
let context;
|
|
567
|
+
const prepareContext = async () => {
|
|
568
|
+
const access = await inspectionRead(
|
|
569
|
+
root,
|
|
570
|
+
"decision-anchor-access",
|
|
571
|
+
() => createFileAccess(root)
|
|
572
|
+
);
|
|
573
|
+
const inventory = access.inventory();
|
|
574
|
+
if (!inventory) throw new Error("Git anchor inventory unavailable");
|
|
575
|
+
const ignored = await inspectionGit(
|
|
576
|
+
[
|
|
577
|
+
"ls-files",
|
|
578
|
+
"-z",
|
|
579
|
+
"--others",
|
|
580
|
+
"--ignored",
|
|
581
|
+
"--exclude-standard",
|
|
582
|
+
"--",
|
|
583
|
+
...union.map((anchor) => `:(literal)${anchor}`)
|
|
584
|
+
],
|
|
585
|
+
{ cwd: root, maxBuffer: 8 * 1024 * 1024 }
|
|
586
|
+
);
|
|
587
|
+
const files = [
|
|
588
|
+
.../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
|
|
589
|
+
].sort();
|
|
590
|
+
const patterns = await Promise.all(
|
|
591
|
+
union.map(async (anchor) => {
|
|
592
|
+
const literal = fg3.escapePath(anchor);
|
|
593
|
+
try {
|
|
594
|
+
return (await fs5.lstat(await storePath(root, anchor))).isDirectory() ? `${literal}/**/*` : literal;
|
|
595
|
+
} catch {
|
|
596
|
+
return literal;
|
|
597
|
+
}
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
const eligible = new Set(await access.list(patterns, { dot: true }));
|
|
601
|
+
const indexed = indexMatchingPaths(scopes, files);
|
|
602
|
+
const filesByScope = new Map(
|
|
603
|
+
scopes.map((scope, index2) => [JSON.stringify([...new Set(scope)].sort()), indexed[index2]])
|
|
604
|
+
);
|
|
605
|
+
return { filesByScope, eligible };
|
|
606
|
+
};
|
|
607
|
+
return Promise.all(
|
|
608
|
+
scopes.map((scope) => {
|
|
609
|
+
const anchors = [...new Set(scope)].sort();
|
|
610
|
+
return inspectionRead(
|
|
611
|
+
root,
|
|
612
|
+
"decision-anchor-capture:" + JSON.stringify(anchors),
|
|
613
|
+
async () => {
|
|
614
|
+
const capture = { anchors, files: {}, complete: true };
|
|
615
|
+
if (!anchors.length) return capture;
|
|
616
|
+
try {
|
|
617
|
+
const { eligible, filesByScope } = await (context ??= prepareContext());
|
|
618
|
+
const files = filesByScope.get(JSON.stringify(anchors));
|
|
619
|
+
if (files.length > 2e3) return { ...capture, complete: false };
|
|
620
|
+
for (const file of files) {
|
|
621
|
+
const evidence = await inspectionRead(
|
|
622
|
+
root,
|
|
623
|
+
"decision-anchor-file:" + file,
|
|
624
|
+
async () => {
|
|
625
|
+
try {
|
|
626
|
+
const absolute = await storePath(root, file);
|
|
627
|
+
await fs5.lstat(absolute);
|
|
628
|
+
if (!eligible.has(file)) return { complete: false };
|
|
629
|
+
const handle = await fs5.open(
|
|
630
|
+
absolute,
|
|
631
|
+
constants2.O_RDONLY | constants2.O_NONBLOCK | constants2.O_NOFOLLOW
|
|
632
|
+
);
|
|
633
|
+
try {
|
|
634
|
+
const stat = await handle.stat();
|
|
635
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
|
|
636
|
+
return { complete: false };
|
|
637
|
+
const bytes = Buffer.alloc(Math.min(stat.size + 1, MAX_SOURCE_BYTES + 1));
|
|
638
|
+
let size = 0;
|
|
639
|
+
while (size < bytes.length) {
|
|
640
|
+
const read = await handle.read(bytes, size, bytes.length - size, null);
|
|
641
|
+
if (!read.bytesRead) break;
|
|
642
|
+
size += read.bytesRead;
|
|
643
|
+
}
|
|
644
|
+
if (size === bytes.length) return { complete: false };
|
|
645
|
+
return {
|
|
646
|
+
complete: true,
|
|
647
|
+
hash: createHash("sha256").update(bytes.subarray(0, size)).digest("hex")
|
|
648
|
+
};
|
|
649
|
+
} finally {
|
|
650
|
+
await handle.close();
|
|
651
|
+
}
|
|
652
|
+
} catch (error) {
|
|
653
|
+
return { complete: error.code === "ENOENT" };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
if (!evidence.complete) capture.complete = false;
|
|
658
|
+
if (evidence.hash) capture.files[file] = evidence.hash;
|
|
659
|
+
}
|
|
660
|
+
} catch {
|
|
661
|
+
capture.complete = false;
|
|
662
|
+
}
|
|
663
|
+
return capture;
|
|
664
|
+
}
|
|
665
|
+
);
|
|
666
|
+
})
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
function changedCaptureFiles(before, after) {
|
|
670
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(before.files), ...Object.keys(after.files)])].filter((file) => before.files[file] !== after.files[file]).sort();
|
|
671
|
+
}
|
|
463
672
|
|
|
464
673
|
// src/context/trust.ts
|
|
465
674
|
function assessTrust(entry, freshness) {
|
|
466
675
|
const verification = entry.verificationFailed ? "failed" : entry.verifiedAt ? "passed" : "unverified";
|
|
467
676
|
const reasons = [];
|
|
468
|
-
if (freshness === "unknown")
|
|
469
|
-
|
|
470
|
-
|
|
677
|
+
if (freshness === "unknown")
|
|
678
|
+
reasons.push(
|
|
679
|
+
"Anchors, history, or working-tree evidence are unavailable; verify before relying on this entry."
|
|
680
|
+
);
|
|
681
|
+
if (freshness === "changed")
|
|
682
|
+
reasons.push(
|
|
683
|
+
"Anchored files changed; verify against current code before relying on this entry."
|
|
684
|
+
);
|
|
685
|
+
if (verification === "failed")
|
|
686
|
+
reasons.push(
|
|
687
|
+
`Verification failed: ${entry.verificationNote ?? "re-map this entry before relying on it"}`
|
|
688
|
+
);
|
|
471
689
|
if (verification === "unverified") reasons.push("No correctness verification has been recorded.");
|
|
472
|
-
return {
|
|
690
|
+
return {
|
|
691
|
+
freshness,
|
|
692
|
+
verification,
|
|
693
|
+
verifiedAt: entry.verifiedAt,
|
|
694
|
+
verifiedHash: entry.verifiedHash,
|
|
695
|
+
reasons
|
|
696
|
+
};
|
|
473
697
|
}
|
|
474
698
|
|
|
475
699
|
// src/decisions/provenance.ts
|
|
476
|
-
var text = (max) =>
|
|
477
|
-
var decisionSourceSchema =
|
|
478
|
-
kind:
|
|
700
|
+
var text = (max) => z4.string().trim().min(1).max(max);
|
|
701
|
+
var decisionSourceSchema = z4.object({
|
|
702
|
+
kind: z4.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
479
703
|
reference: text(1e3),
|
|
480
704
|
note: text(500).optional()
|
|
481
705
|
}).strict();
|
|
482
|
-
var attributionSchema =
|
|
706
|
+
var attributionSchema = z4.object({
|
|
483
707
|
owner: text(200).nullable().optional(),
|
|
484
|
-
sources:
|
|
708
|
+
sources: z4.array(decisionSourceSchema).max(20).optional(),
|
|
485
709
|
actor: text(200).optional()
|
|
486
710
|
});
|
|
487
|
-
var contentSchema =
|
|
488
|
-
title:
|
|
489
|
-
body:
|
|
490
|
-
category:
|
|
491
|
-
files:
|
|
711
|
+
var contentSchema = z4.object({
|
|
712
|
+
title: z4.string().min(1),
|
|
713
|
+
body: z4.string().min(1),
|
|
714
|
+
category: z4.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
715
|
+
files: z4.array(z4.string().refine((f) => normalizeRepoPath(f) !== null)),
|
|
492
716
|
owner: text(200).optional(),
|
|
493
|
-
sources:
|
|
717
|
+
sources: z4.array(decisionSourceSchema).max(20)
|
|
494
718
|
});
|
|
495
|
-
var approvalSchema =
|
|
496
|
-
var statusSchema =
|
|
497
|
-
var reviewEvidenceSchema =
|
|
498
|
-
baseHash:
|
|
499
|
-
headHash:
|
|
500
|
-
historyAvailable:
|
|
501
|
-
changedFiles:
|
|
502
|
-
localChanges:
|
|
719
|
+
var approvalSchema = z4.enum(["unreviewed", "proposed", "accepted"]);
|
|
720
|
+
var statusSchema = z4.enum(["active", "superseded", "retired"]);
|
|
721
|
+
var reviewEvidenceSchema = z4.object({
|
|
722
|
+
baseHash: z4.string(),
|
|
723
|
+
headHash: z4.string(),
|
|
724
|
+
historyAvailable: z4.boolean(),
|
|
725
|
+
changedFiles: z4.array(z4.string()),
|
|
726
|
+
localChanges: z4.array(z4.string())
|
|
503
727
|
});
|
|
504
|
-
var eventSchema =
|
|
505
|
-
kind:
|
|
506
|
-
|
|
728
|
+
var eventSchema = z4.object({
|
|
729
|
+
kind: z4.enum([
|
|
730
|
+
"imported",
|
|
731
|
+
"created",
|
|
732
|
+
"revised",
|
|
733
|
+
"accepted",
|
|
734
|
+
"reaffirmed",
|
|
735
|
+
"retired",
|
|
736
|
+
"superseded"
|
|
737
|
+
]),
|
|
738
|
+
at: z4.string().datetime(),
|
|
507
739
|
actor: text(200).optional(),
|
|
508
740
|
note: text(1500).optional(),
|
|
509
|
-
revision:
|
|
741
|
+
revision: z4.number().int().positive(),
|
|
510
742
|
content: contentSchema,
|
|
511
743
|
approval: approvalSchema,
|
|
512
744
|
status: statusSchema,
|
|
513
|
-
refreshedHash:
|
|
514
|
-
evidence: reviewEvidenceSchema.optional()
|
|
745
|
+
refreshedHash: z4.string(),
|
|
746
|
+
evidence: reviewEvidenceSchema.optional(),
|
|
747
|
+
capture: anchorCaptureSchema.optional()
|
|
515
748
|
});
|
|
516
|
-
var legacySchema =
|
|
517
|
-
version:
|
|
518
|
-
id:
|
|
519
|
-
title:
|
|
520
|
-
body:
|
|
749
|
+
var legacySchema = z4.object({
|
|
750
|
+
version: z4.literal(1),
|
|
751
|
+
id: z4.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
752
|
+
title: z4.string().min(1),
|
|
753
|
+
body: z4.string().min(1),
|
|
521
754
|
category: contentSchema.shape.category,
|
|
522
755
|
files: contentSchema.shape.files,
|
|
523
|
-
createdAt:
|
|
524
|
-
updatedAt:
|
|
525
|
-
refreshedHash:
|
|
526
|
-
status:
|
|
527
|
-
supersededBy:
|
|
756
|
+
createdAt: z4.string(),
|
|
757
|
+
updatedAt: z4.string(),
|
|
758
|
+
refreshedHash: z4.string(),
|
|
759
|
+
status: z4.enum(["active", "superseded"]),
|
|
760
|
+
supersededBy: z4.string().optional()
|
|
528
761
|
}).passthrough();
|
|
529
762
|
var currentSchema = legacySchema.extend({
|
|
530
|
-
version:
|
|
763
|
+
version: z4.literal(2),
|
|
531
764
|
status: statusSchema,
|
|
532
765
|
approval: approvalSchema,
|
|
533
|
-
revision:
|
|
766
|
+
revision: z4.number().int().positive(),
|
|
534
767
|
owner: text(200).optional(),
|
|
535
|
-
sources:
|
|
536
|
-
history:
|
|
768
|
+
sources: z4.array(decisionSourceSchema).max(20),
|
|
769
|
+
history: z4.array(eventSchema).min(1),
|
|
770
|
+
capture: anchorCaptureSchema.optional(),
|
|
771
|
+
inspections: z4.array(
|
|
772
|
+
z4.object({
|
|
773
|
+
at: z4.string().datetime(),
|
|
774
|
+
inspector: text(200),
|
|
775
|
+
note: text(1500),
|
|
776
|
+
outcome: z4.literal("no_contradiction_found"),
|
|
777
|
+
scope: z4.literal("operative").optional(),
|
|
778
|
+
decisionDigest: z4.string().regex(/^[a-f0-9]{64}$/),
|
|
779
|
+
headHash: z4.string(),
|
|
780
|
+
capture: anchorCaptureSchema
|
|
781
|
+
})
|
|
782
|
+
).max(200).optional()
|
|
537
783
|
}).superRefine((record, ctx) => {
|
|
538
784
|
const invalid = (message2) => ctx.addIssue({ code: "custom", message: message2 });
|
|
539
785
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
540
786
|
let previous;
|
|
541
787
|
for (const event of record.history) {
|
|
542
788
|
if (!previous) {
|
|
543
|
-
if (!["created", "imported"].includes(event.kind) || event.revision !== 1)
|
|
544
|
-
|
|
789
|
+
if (!["created", "imported"].includes(event.kind) || event.revision !== 1)
|
|
790
|
+
invalid("History must begin with creation or legacy import at revision 1");
|
|
791
|
+
if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed"))
|
|
792
|
+
invalid("Initial records cannot claim acceptance");
|
|
545
793
|
} else {
|
|
546
794
|
if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
|
|
547
795
|
if (previous.status !== "active") invalid("Archived decisions cannot be changed");
|
|
548
|
-
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0))
|
|
549
|
-
|
|
550
|
-
if (event.kind
|
|
551
|
-
|
|
796
|
+
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0))
|
|
797
|
+
invalid("Invalid revision sequence");
|
|
798
|
+
if (event.kind !== "revised" && !same(event.content, previous.content))
|
|
799
|
+
invalid("A review cannot silently revise decision content");
|
|
800
|
+
if (event.kind === "reaffirmed" && previous.approval !== "accepted")
|
|
801
|
+
invalid("Only accepted decisions can be reaffirmed");
|
|
802
|
+
if (event.kind === "accepted" && previous.approval === "accepted")
|
|
803
|
+
invalid("Use reaffirmation for an accepted decision");
|
|
552
804
|
const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
|
|
553
805
|
if (event.approval !== approval) invalid("Approval disagrees with review history");
|
|
554
|
-
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash)
|
|
806
|
+
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash)
|
|
807
|
+
invalid("Only a review can refresh the evidence baseline");
|
|
555
808
|
}
|
|
556
|
-
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active"))
|
|
557
|
-
|
|
809
|
+
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active"))
|
|
810
|
+
invalid("Lifecycle disagrees with history");
|
|
811
|
+
if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence))
|
|
812
|
+
invalid("Reviews require a named reviewer, reason, and code evidence");
|
|
558
813
|
if (["accepted", "reaffirmed"].includes(event.kind)) {
|
|
559
|
-
if (!event.content.owner || !event.content.sources.length)
|
|
560
|
-
|
|
814
|
+
if (!event.content.owner || !event.content.sources.length)
|
|
815
|
+
invalid("Accepted decisions require an owner and source");
|
|
816
|
+
if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length)
|
|
817
|
+
invalid("Acceptance requires a committed evidence baseline");
|
|
561
818
|
}
|
|
819
|
+
if (event.capture && !same([...event.content.files].sort(), event.capture.anchors))
|
|
820
|
+
invalid("Capture scope disagrees with decision anchors");
|
|
562
821
|
previous = event;
|
|
563
822
|
}
|
|
564
|
-
if (
|
|
823
|
+
if (previous && !same(previous.capture, record.capture))
|
|
824
|
+
invalid("Capture does not match the final history event");
|
|
825
|
+
if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash)
|
|
826
|
+
invalid("Decision does not match the final history event");
|
|
565
827
|
});
|
|
566
|
-
var decisionSchema =
|
|
828
|
+
var decisionSchema = z4.union([legacySchema, currentSchema]);
|
|
567
829
|
function decisionContent(record) {
|
|
568
830
|
return {
|
|
569
831
|
title: record.title,
|
|
@@ -578,7 +840,8 @@ function decisionApproval(record) {
|
|
|
578
840
|
return record.version === 1 ? "unreviewed" : record.approval;
|
|
579
841
|
}
|
|
580
842
|
function effectiveDecision(record) {
|
|
581
|
-
if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed")
|
|
843
|
+
if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed")
|
|
844
|
+
return record;
|
|
582
845
|
let index2 = record.history.length - 1;
|
|
583
846
|
while (index2 >= 0 && !["accepted", "reaffirmed"].includes(record.history[index2].kind)) index2--;
|
|
584
847
|
if (index2 < 0) return record;
|
|
@@ -590,13 +853,16 @@ function effectiveDecision(record) {
|
|
|
590
853
|
approval: "accepted",
|
|
591
854
|
revision: event.revision,
|
|
592
855
|
refreshedHash: event.refreshedHash,
|
|
856
|
+
capture: event.capture,
|
|
593
857
|
updatedAt: event.at,
|
|
594
858
|
history: record.history.slice(0, index2 + 1)
|
|
595
859
|
};
|
|
596
860
|
}
|
|
597
861
|
function decisionProvenance(record, freshness = "unknown") {
|
|
598
862
|
const approval = decisionApproval(record);
|
|
599
|
-
const review = record.version === 2 ? [...record.history].reverse().find(
|
|
863
|
+
const review = record.version === 2 ? [...record.history].reverse().find(
|
|
864
|
+
(e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision
|
|
865
|
+
) : void 0;
|
|
600
866
|
return {
|
|
601
867
|
approval,
|
|
602
868
|
revision: record.version === 2 ? record.revision : 0,
|
|
@@ -604,80 +870,218 @@ function decisionProvenance(record, freshness = "unknown") {
|
|
|
604
870
|
sources: record.version === 2 ? record.sources : [],
|
|
605
871
|
guidance: record.status !== "active" ? "historical" : approval === "accepted" ? "constraint" : approval === "proposed" ? "proposal" : "unreviewed",
|
|
606
872
|
reviewRequired: record.status === "active" && (approval !== "accepted" || freshness !== "current"),
|
|
607
|
-
lastReview: review ? {
|
|
873
|
+
lastReview: review ? {
|
|
874
|
+
reviewer: review.actor,
|
|
875
|
+
at: review.at,
|
|
876
|
+
note: review.note,
|
|
877
|
+
gitHash: review.refreshedHash
|
|
878
|
+
} : null
|
|
608
879
|
};
|
|
609
880
|
}
|
|
610
881
|
function decisionTrust(record, freshness) {
|
|
611
882
|
const review = decisionProvenance(record, freshness).lastReview;
|
|
612
|
-
return assessTrust(
|
|
883
|
+
return assessTrust(
|
|
884
|
+
review ? { verifiedAt: review.at, verifiedHash: review.gitHash } : {},
|
|
885
|
+
freshness
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
function decisionInspectionDigest(record, scope = "all") {
|
|
889
|
+
const effective = effectiveDecision(record);
|
|
890
|
+
if (scope === "operative")
|
|
891
|
+
return createHash2("sha256").update(
|
|
892
|
+
JSON.stringify([
|
|
893
|
+
"operative",
|
|
894
|
+
record.status,
|
|
895
|
+
decisionApproval(effective),
|
|
896
|
+
effective.version === 2 ? effective.revision : 0,
|
|
897
|
+
decisionContent(effective),
|
|
898
|
+
effective.refreshedHash
|
|
899
|
+
])
|
|
900
|
+
).digest("hex");
|
|
901
|
+
return createHash2("sha256").update(
|
|
902
|
+
JSON.stringify([
|
|
903
|
+
record.status,
|
|
904
|
+
decisionApproval(record),
|
|
905
|
+
record.version === 2 ? record.revision : 0,
|
|
906
|
+
decisionContent(record),
|
|
907
|
+
decisionContent(effective),
|
|
908
|
+
effective.refreshedHash
|
|
909
|
+
])
|
|
910
|
+
).digest("hex");
|
|
911
|
+
}
|
|
912
|
+
function latestDecisionInspection(record, scope = "all") {
|
|
913
|
+
return record.version === 2 ? record.inspections?.filter(
|
|
914
|
+
(i) => (scope === "operative" || i.scope !== "operative") && i.decisionDigest === decisionInspectionDigest(record, i.scope ?? "all")
|
|
915
|
+
).at(-1) : void 0;
|
|
613
916
|
}
|
|
614
917
|
function revisionKnowledge(record, freshness) {
|
|
615
|
-
return {
|
|
918
|
+
return {
|
|
919
|
+
...decisionContent(record),
|
|
920
|
+
...decisionProvenance(record, freshness),
|
|
921
|
+
trust: decisionTrust(record, freshness)
|
|
922
|
+
};
|
|
616
923
|
}
|
|
617
924
|
function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "unknown") {
|
|
618
925
|
const effective = effectiveDecision(record);
|
|
926
|
+
const inspection = latestDecisionInspection(
|
|
927
|
+
record,
|
|
928
|
+
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
929
|
+
);
|
|
619
930
|
return {
|
|
620
931
|
...revisionKnowledge(effective, freshness),
|
|
621
|
-
...
|
|
932
|
+
...inspection ? {
|
|
933
|
+
inspection: {
|
|
934
|
+
freshness,
|
|
935
|
+
at: inspection.at,
|
|
936
|
+
inspector: inspection.inspector,
|
|
937
|
+
note: inspection.note,
|
|
938
|
+
outcome: inspection.outcome,
|
|
939
|
+
scope: inspection.scope ?? "all",
|
|
940
|
+
headHash: inspection.headHash
|
|
941
|
+
}
|
|
942
|
+
} : {},
|
|
943
|
+
...effective !== record ? {
|
|
944
|
+
pendingProposal: {
|
|
945
|
+
...revisionKnowledge(record, proposalFreshness),
|
|
946
|
+
assessment: latestDecisionInspection(record) ? "no_contradiction_found" : "not_assessed"
|
|
947
|
+
}
|
|
948
|
+
} : {}
|
|
622
949
|
};
|
|
623
950
|
}
|
|
624
951
|
|
|
625
952
|
// src/decisions/decisions.ts
|
|
626
|
-
import
|
|
627
|
-
import
|
|
628
|
-
import { createHash } from "crypto";
|
|
629
|
-
import { z as
|
|
953
|
+
import fs6 from "fs/promises";
|
|
954
|
+
import path7 from "path";
|
|
955
|
+
import { createHash as createHash3 } from "crypto";
|
|
956
|
+
import { z as z5 } from "zod";
|
|
630
957
|
|
|
631
958
|
// src/context/lexical.ts
|
|
632
|
-
import
|
|
959
|
+
import path6 from "path";
|
|
633
960
|
|
|
634
961
|
// src/decisions/decisions.ts
|
|
635
962
|
var TITLE_MAX_CHARS = 80;
|
|
636
963
|
var BODY_RECOMMENDED_CHARS = 1500;
|
|
637
964
|
var BODY_MAX_CHARS = 2500;
|
|
638
|
-
var boundedText = (field, max, hint) =>
|
|
639
|
-
errorMap: (issue, ctx) => ({
|
|
965
|
+
var boundedText = (field, max, hint) => z5.string({
|
|
966
|
+
errorMap: (issue, ctx) => ({
|
|
967
|
+
message: issue.code === "too_big" && typeof ctx.data === "string" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError
|
|
968
|
+
})
|
|
640
969
|
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
641
|
-
var decisionTitleSchema = boundedText(
|
|
970
|
+
var decisionTitleSchema = boundedText(
|
|
971
|
+
"title",
|
|
972
|
+
TITLE_MAX_CHARS,
|
|
973
|
+
"Tighten it to a specific headline."
|
|
974
|
+
);
|
|
642
975
|
var decisionBodySchema = boundedText(
|
|
643
976
|
"body",
|
|
644
977
|
BODY_MAX_CHARS,
|
|
645
978
|
`Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
|
|
646
979
|
);
|
|
980
|
+
async function readDecisionInputs(rootDir) {
|
|
981
|
+
return structuredClone(
|
|
982
|
+
await inspectionRead(rootDir, "decision-store:inputs", async () => {
|
|
983
|
+
const inputs = [];
|
|
984
|
+
const diagnostics = [];
|
|
985
|
+
let entries;
|
|
986
|
+
try {
|
|
987
|
+
entries = await fs6.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
988
|
+
} catch (error) {
|
|
989
|
+
if (error.code !== "ENOENT")
|
|
990
|
+
diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
991
|
+
return { inputs, diagnostics };
|
|
992
|
+
}
|
|
993
|
+
for (const entry of entries.sort()) {
|
|
994
|
+
if (!entry.endsWith(".json")) continue;
|
|
995
|
+
const relative = `.mason/decisions/${entry}`;
|
|
996
|
+
try {
|
|
997
|
+
const raw = await readBoundedFile(await storePath(rootDir, relative), 10 * 1024 * 1024);
|
|
998
|
+
if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
|
|
999
|
+
inputs.push([relative, raw]);
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
if (error.code === "ENOENT") inputs.push([relative, null]);
|
|
1002
|
+
else
|
|
1003
|
+
diagnostics.push({
|
|
1004
|
+
path: relative,
|
|
1005
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return { inputs, diagnostics };
|
|
1010
|
+
})
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
647
1013
|
async function loadDecisionStore(rootDir) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1014
|
+
return structuredClone(
|
|
1015
|
+
await inspectionRead(rootDir, "decision-store:parsed", async () => {
|
|
1016
|
+
const raw = await readDecisionInputs(rootDir);
|
|
1017
|
+
const records = [];
|
|
1018
|
+
const diagnostics = [...raw.diagnostics];
|
|
1019
|
+
for (const [relative, content] of raw.inputs) {
|
|
1020
|
+
try {
|
|
1021
|
+
const record = decisionSchema.parse(content === null ? null : JSON.parse(content));
|
|
1022
|
+
if (relative !== `.mason/decisions/${record.id}.json`)
|
|
1023
|
+
throw new Error("Record id does not match its filename");
|
|
1024
|
+
records.push(record);
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
diagnostics.push({
|
|
1027
|
+
path: relative,
|
|
1028
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return { records, diagnostics };
|
|
1033
|
+
})
|
|
1034
|
+
);
|
|
669
1035
|
}
|
|
670
1036
|
|
|
671
1037
|
// src/decisions/drift.ts
|
|
672
|
-
import
|
|
1038
|
+
import path8 from "path";
|
|
673
1039
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
674
|
-
const resolvedRoot =
|
|
1040
|
+
const resolvedRoot = path8.resolve(rootDir);
|
|
675
1041
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
676
|
-
const report = {
|
|
677
|
-
|
|
1042
|
+
const report = {
|
|
1043
|
+
historyAvailable: true,
|
|
1044
|
+
totalDecisions: store.records.length,
|
|
1045
|
+
staleDecisions: {},
|
|
1046
|
+
freshness: {},
|
|
1047
|
+
diagnostics: store.diagnostics
|
|
1048
|
+
};
|
|
1049
|
+
const [head, workingTree] = await Promise.all([
|
|
1050
|
+
getCurrentGitHash(resolvedRoot),
|
|
1051
|
+
getWorkingTree(resolvedRoot)
|
|
1052
|
+
]);
|
|
678
1053
|
const changesByHash = /* @__PURE__ */ new Map();
|
|
679
|
-
const
|
|
1054
|
+
const baselines = store.records.filter((record) => record.status === "active").flatMap((record) => {
|
|
1055
|
+
const effective = effectiveDecision(record);
|
|
1056
|
+
return [.../* @__PURE__ */ new Set([effective, record])].flatMap((candidate) => {
|
|
1057
|
+
const inspection = latestDecisionInspection(
|
|
1058
|
+
record,
|
|
1059
|
+
candidate === effective && decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
1060
|
+
);
|
|
1061
|
+
const capture = inspection?.capture ?? (candidate.version === 2 ? candidate.capture : void 0);
|
|
1062
|
+
return capture ? [capture] : [];
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
const captures = await captureAnchorScopes(
|
|
1066
|
+
resolvedRoot,
|
|
1067
|
+
baselines.map((baseline) => baseline.anchors)
|
|
1068
|
+
);
|
|
1069
|
+
const currentCaptures = new Map(
|
|
1070
|
+
baselines.map((baseline, index2) => [JSON.stringify(baseline.anchors), captures[index2]])
|
|
1071
|
+
);
|
|
1072
|
+
const inspect = async (record, inspectionCapture) => {
|
|
680
1073
|
if (record.files.length === 0) return { freshness: "unknown", changedFiles: [] };
|
|
1074
|
+
const baseline = inspectionCapture ?? (record.version === 2 ? record.capture : void 0);
|
|
1075
|
+
if (baseline) {
|
|
1076
|
+
const current = currentCaptures.get(JSON.stringify(baseline.anchors));
|
|
1077
|
+
const hits2 = changedCaptureFiles(baseline, current).filter(
|
|
1078
|
+
(file) => matchingPaths(record.files, [file]).length
|
|
1079
|
+
);
|
|
1080
|
+
return {
|
|
1081
|
+
freshness: !baseline.complete || !current.complete ? "unknown" : hits2.length ? "changed" : "current",
|
|
1082
|
+
changedFiles: hits2
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
681
1085
|
let touched = changesByHash.get(record.refreshedHash);
|
|
682
1086
|
if (touched === void 0) {
|
|
683
1087
|
const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
|
|
@@ -687,15 +1091,26 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
687
1091
|
if (touched === null) report.historyAvailable = false;
|
|
688
1092
|
const hits = touched ? matchingPaths(record.files, touched) : [];
|
|
689
1093
|
const localHits = matchingPaths(record.files, workingTree.changedFiles);
|
|
690
|
-
return {
|
|
1094
|
+
return {
|
|
1095
|
+
freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current",
|
|
1096
|
+
changedFiles: [.../* @__PURE__ */ new Set([...hits, ...localHits])].sort()
|
|
1097
|
+
};
|
|
691
1098
|
};
|
|
692
1099
|
for (const record of store.records) {
|
|
693
1100
|
if (record.status !== "active") continue;
|
|
694
1101
|
const effective = effectiveDecision(record);
|
|
695
|
-
const
|
|
1102
|
+
const inspection = latestDecisionInspection(
|
|
1103
|
+
record,
|
|
1104
|
+
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
1105
|
+
);
|
|
1106
|
+
const state = await inspect(effective, inspection?.capture);
|
|
696
1107
|
report.freshness[record.id] = state.freshness;
|
|
697
1108
|
if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
|
|
698
|
-
if (effective !== record)
|
|
1109
|
+
if (effective !== record)
|
|
1110
|
+
(report.pendingProposals ??= {})[record.id] = await inspect(
|
|
1111
|
+
record,
|
|
1112
|
+
latestDecisionInspection(record)?.capture
|
|
1113
|
+
);
|
|
699
1114
|
}
|
|
700
1115
|
return report;
|
|
701
1116
|
}
|
|
@@ -711,7 +1126,9 @@ var MIN_COCHANGE_RATE = 0.6;
|
|
|
711
1126
|
var MAX_COMMIT_FILES = 30;
|
|
712
1127
|
async function buildMatrix(resolvedRoot) {
|
|
713
1128
|
try {
|
|
714
|
-
const { stdout: shallow } = await exec("git", ["rev-parse", "--is-shallow-repository"], {
|
|
1129
|
+
const { stdout: shallow } = await exec("git", ["rev-parse", "--is-shallow-repository"], {
|
|
1130
|
+
cwd: resolvedRoot
|
|
1131
|
+
});
|
|
715
1132
|
if (shallow.trim() === "true") return null;
|
|
716
1133
|
const { stdout } = await exec(
|
|
717
1134
|
"git",
|
|
@@ -772,10 +1189,80 @@ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, all
|
|
|
772
1189
|
|
|
773
1190
|
// src/review/evidence.ts
|
|
774
1191
|
import path10 from "path";
|
|
775
|
-
import { z as
|
|
1192
|
+
import { z as z9 } from "zod";
|
|
1193
|
+
|
|
1194
|
+
// src/test-map.ts
|
|
1195
|
+
import path9 from "path";
|
|
1196
|
+
async function buildTestMap(dir) {
|
|
1197
|
+
const rootDir = path9.resolve(dir);
|
|
1198
|
+
const access = await createFileAccess(rootDir);
|
|
1199
|
+
const testPatterns = [
|
|
1200
|
+
"**/*.test.*",
|
|
1201
|
+
"**/*.spec.*",
|
|
1202
|
+
"**/*Test.kt",
|
|
1203
|
+
"**/*Test.java",
|
|
1204
|
+
"**/*Tests.kt",
|
|
1205
|
+
"**/*Tests.java",
|
|
1206
|
+
"**/test_*.py",
|
|
1207
|
+
"**/*_test.py",
|
|
1208
|
+
"**/*_test.go",
|
|
1209
|
+
"**/*Tests.swift",
|
|
1210
|
+
"**/*Test.swift",
|
|
1211
|
+
"**/*_test.rs"
|
|
1212
|
+
];
|
|
1213
|
+
const testFiles = await access.list(testPatterns);
|
|
1214
|
+
const sourceFiles = await access.list();
|
|
1215
|
+
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
1216
|
+
for (const file of sourceFiles) {
|
|
1217
|
+
if (testFiles.includes(file)) continue;
|
|
1218
|
+
const baseName = path9.basename(file).replace(/\.[^.]+$/, "");
|
|
1219
|
+
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
1220
|
+
existing.push(file);
|
|
1221
|
+
sourceByBaseName.set(baseName, existing);
|
|
1222
|
+
}
|
|
1223
|
+
const paired = [];
|
|
1224
|
+
const unmatched = [];
|
|
1225
|
+
for (const testFile of testFiles) {
|
|
1226
|
+
const testBaseName = path9.basename(testFile).replace(/\.[^.]+$/, "");
|
|
1227
|
+
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
1228
|
+
if (!sourceName) {
|
|
1229
|
+
unmatched.push(testFile);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
const candidates = sourceByBaseName.get(sourceName);
|
|
1233
|
+
if (candidates && candidates.length > 0) {
|
|
1234
|
+
const testDir = path9.dirname(testFile);
|
|
1235
|
+
const bestMatch = candidates.reduce((best, candidate) => {
|
|
1236
|
+
const candidateDir = path9.dirname(candidate);
|
|
1237
|
+
const bestDir = path9.dirname(best);
|
|
1238
|
+
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
1239
|
+
const bestOverlap = commonSegments(testDir, bestDir);
|
|
1240
|
+
return candidateOverlap > bestOverlap ? candidate : best;
|
|
1241
|
+
});
|
|
1242
|
+
paired.push({
|
|
1243
|
+
test: testFile,
|
|
1244
|
+
source: bestMatch,
|
|
1245
|
+
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
1246
|
+
});
|
|
1247
|
+
} else {
|
|
1248
|
+
unmatched.push(testFile);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
1252
|
+
}
|
|
1253
|
+
function commonSegments(pathA, pathB) {
|
|
1254
|
+
const segsA = pathA.split("/");
|
|
1255
|
+
const segsB = pathB.split("/");
|
|
1256
|
+
let count2 = 0;
|
|
1257
|
+
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
1258
|
+
if (segsA[i] === segsB[i]) count2++;
|
|
1259
|
+
else break;
|
|
1260
|
+
}
|
|
1261
|
+
return count2;
|
|
1262
|
+
}
|
|
776
1263
|
|
|
777
1264
|
// src/review/evidence/vitest.ts
|
|
778
|
-
import { z as
|
|
1265
|
+
import { z as z6 } from "zod";
|
|
779
1266
|
|
|
780
1267
|
// src/review/evidence/paths.ts
|
|
781
1268
|
function evidencePath(value, sourceRoot, uri = false) {
|
|
@@ -811,26 +1298,30 @@ function messagePreview(message2) {
|
|
|
811
1298
|
}
|
|
812
1299
|
|
|
813
1300
|
// src/review/evidence/vitest.ts
|
|
814
|
-
var count =
|
|
815
|
-
var schema =
|
|
816
|
-
success:
|
|
1301
|
+
var count = z6.number().int().nonnegative();
|
|
1302
|
+
var schema = z6.object({
|
|
1303
|
+
success: z6.boolean(),
|
|
817
1304
|
numTotalTests: count,
|
|
818
1305
|
numPassedTests: count,
|
|
819
1306
|
numFailedTests: count,
|
|
820
1307
|
numPendingTests: count,
|
|
821
1308
|
numTodoTests: count,
|
|
822
1309
|
numFailedTestSuites: count,
|
|
823
|
-
testResults:
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1310
|
+
testResults: z6.array(
|
|
1311
|
+
z6.object({
|
|
1312
|
+
name: z6.string().min(1),
|
|
1313
|
+
status: z6.enum(["passed", "failed"]),
|
|
1314
|
+
message: z6.string().optional(),
|
|
1315
|
+
assertionResults: z6.array(
|
|
1316
|
+
z6.object({
|
|
1317
|
+
fullName: z6.string(),
|
|
1318
|
+
status: z6.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
1319
|
+
failureMessages: z6.array(z6.string()).nullable().optional(),
|
|
1320
|
+
location: z6.object({ line: count, column: count }).nullable().optional()
|
|
1321
|
+
})
|
|
1322
|
+
)
|
|
1323
|
+
})
|
|
1324
|
+
)
|
|
834
1325
|
});
|
|
835
1326
|
function parseVitest(raw, sourceRoot) {
|
|
836
1327
|
const report = schema.parse(raw);
|
|
@@ -838,7 +1329,8 @@ function parseVitest(raw, sourceRoot) {
|
|
|
838
1329
|
let passed = 0, failed = 0, skipped = 0;
|
|
839
1330
|
for (const [index2, suite] of report.testResults.entries()) {
|
|
840
1331
|
const file = evidencePath(suite.name, sourceRoot);
|
|
841
|
-
if (!file)
|
|
1332
|
+
if (!file)
|
|
1333
|
+
diagnostics.push(`Test path is outside the declared checkout or invalid: ${suite.name}`);
|
|
842
1334
|
let suiteFailures = 0;
|
|
843
1335
|
for (const [testIndex, test] of suite.assertionResults.entries()) {
|
|
844
1336
|
if (test.status === "passed") passed++;
|
|
@@ -850,58 +1342,118 @@ function parseVitest(raw, sourceRoot) {
|
|
|
850
1342
|
...messagePreview([test.fullName, ...test.failureMessages ?? []].join("\n")),
|
|
851
1343
|
severity: "error",
|
|
852
1344
|
state: "active",
|
|
853
|
-
locations: file ? [
|
|
1345
|
+
locations: file ? [
|
|
1346
|
+
{
|
|
1347
|
+
file,
|
|
1348
|
+
...test.location ? { line: test.location.line, column: test.location.column } : {}
|
|
1349
|
+
}
|
|
1350
|
+
] : []
|
|
854
1351
|
});
|
|
855
1352
|
} else skipped++;
|
|
856
1353
|
}
|
|
857
1354
|
if (suite.status === "failed" && !suiteFailures) {
|
|
858
|
-
findings.push({
|
|
1355
|
+
findings.push({
|
|
1356
|
+
id: `${index2}:suite`,
|
|
1357
|
+
...messagePreview(suite.message || `Test suite failed: ${suite.name}`),
|
|
1358
|
+
severity: "error",
|
|
1359
|
+
state: "active",
|
|
1360
|
+
locations: file ? [{ file }] : []
|
|
1361
|
+
});
|
|
859
1362
|
}
|
|
860
1363
|
}
|
|
861
1364
|
if (passed !== report.numPassedTests || failed !== report.numFailedTests || skipped !== report.numPendingTests + report.numTodoTests || passed + failed + skipped !== report.numTotalTests) {
|
|
862
1365
|
throw new Error("Vitest summary counts disagree with its assertion results");
|
|
863
1366
|
}
|
|
864
1367
|
const hasFailures = failed > 0 || report.numFailedTestSuites > 0 || report.testResults.some((s) => s.status === "failed");
|
|
865
|
-
if (report.success && hasFailures)
|
|
1368
|
+
if (report.success && hasFailures)
|
|
1369
|
+
throw new Error("Vitest success conflicts with failed tests or suites");
|
|
866
1370
|
const outcome = hasFailures || !report.success ? "failed" : !report.numTotalTests ? "unavailable" : !passed ? "skipped" : "passed";
|
|
867
|
-
if (!report.numTotalTests)
|
|
1371
|
+
if (!report.numTotalTests)
|
|
1372
|
+
diagnostics.push("No tests executed; an empty report is not passing test evidence.");
|
|
868
1373
|
if (skipped) diagnostics.push(`${skipped} tests were skipped, pending, disabled, or todo.`);
|
|
869
1374
|
return {
|
|
870
1375
|
outcome,
|
|
871
1376
|
findings,
|
|
872
|
-
counts: {
|
|
1377
|
+
counts: {
|
|
1378
|
+
total: report.numTotalTests,
|
|
1379
|
+
passed,
|
|
1380
|
+
failed,
|
|
1381
|
+
skipped,
|
|
1382
|
+
failedSuites: report.numFailedTestSuites
|
|
1383
|
+
},
|
|
873
1384
|
incomplete: skipped > 0 || diagnostics.length > 0,
|
|
874
1385
|
diagnostics
|
|
875
1386
|
};
|
|
876
1387
|
}
|
|
877
1388
|
|
|
878
1389
|
// src/review/evidence/sarif.ts
|
|
879
|
-
import { z as
|
|
880
|
-
var index =
|
|
881
|
-
var artifact =
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1390
|
+
import { z as z7 } from "zod";
|
|
1391
|
+
var index = z7.number().int().nonnegative();
|
|
1392
|
+
var artifact = z7.object({
|
|
1393
|
+
uri: z7.string().optional(),
|
|
1394
|
+
uriBaseId: z7.string().optional(),
|
|
1395
|
+
index: index.optional()
|
|
1396
|
+
});
|
|
1397
|
+
var message = z7.object({
|
|
1398
|
+
text: z7.string().optional(),
|
|
1399
|
+
markdown: z7.string().optional(),
|
|
1400
|
+
id: z7.string().optional(),
|
|
1401
|
+
arguments: z7.array(z7.string()).optional()
|
|
1402
|
+
});
|
|
1403
|
+
var level = z7.enum(["error", "warning", "note", "none"]);
|
|
1404
|
+
var location = z7.object({
|
|
1405
|
+
physicalLocation: z7.object({
|
|
1406
|
+
artifactLocation: artifact.optional(),
|
|
1407
|
+
region: z7.object({ startLine: index.optional(), startColumn: index.optional() }).optional()
|
|
1408
|
+
}).optional()
|
|
1409
|
+
});
|
|
1410
|
+
var schema2 = z7.object({
|
|
1411
|
+
version: z7.literal("2.1.0"),
|
|
1412
|
+
runs: z7.array(
|
|
1413
|
+
z7.object({
|
|
1414
|
+
tool: z7.object({
|
|
1415
|
+
driver: z7.object({
|
|
1416
|
+
name: z7.string().min(1),
|
|
1417
|
+
rules: z7.array(
|
|
1418
|
+
z7.object({
|
|
1419
|
+
id: z7.string(),
|
|
1420
|
+
defaultConfiguration: z7.object({ level: level.optional() }).optional(),
|
|
1421
|
+
messageStrings: z7.record(message).optional()
|
|
1422
|
+
})
|
|
1423
|
+
).optional(),
|
|
1424
|
+
globalMessageStrings: z7.record(message).optional()
|
|
1425
|
+
})
|
|
1426
|
+
}),
|
|
1427
|
+
invocations: z7.array(
|
|
1428
|
+
z7.object({
|
|
1429
|
+
executionSuccessful: z7.boolean(),
|
|
1430
|
+
commandLine: z7.string().optional(),
|
|
1431
|
+
toolExecutionNotifications: z7.array(z7.object({ level: level.optional(), message })).optional()
|
|
1432
|
+
})
|
|
1433
|
+
).optional(),
|
|
1434
|
+
versionControlProvenance: z7.array(z7.object({ revisionId: z7.string().optional() })).optional(),
|
|
1435
|
+
originalUriBaseIds: z7.record(artifact).optional(),
|
|
1436
|
+
artifacts: z7.array(z7.object({ location: artifact.optional() })).optional(),
|
|
1437
|
+
results: z7.array(
|
|
1438
|
+
z7.object({
|
|
1439
|
+
ruleId: z7.string().optional(),
|
|
1440
|
+
ruleIndex: index.optional(),
|
|
1441
|
+
message,
|
|
1442
|
+
level: level.optional(),
|
|
1443
|
+
kind: z7.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
1444
|
+
baselineState: z7.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
1445
|
+
suppressions: z7.array(
|
|
1446
|
+
z7.object({
|
|
1447
|
+
kind: z7.enum(["inSource", "external"]),
|
|
1448
|
+
status: z7.enum(["accepted", "underReview", "rejected"]).optional()
|
|
1449
|
+
})
|
|
1450
|
+
).nullable().optional(),
|
|
1451
|
+
locations: z7.array(location).optional(),
|
|
1452
|
+
relatedLocations: z7.array(location).optional()
|
|
1453
|
+
})
|
|
1454
|
+
).optional()
|
|
1455
|
+
})
|
|
1456
|
+
)
|
|
905
1457
|
});
|
|
906
1458
|
function parseSarif(raw, sourceRoot) {
|
|
907
1459
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
@@ -910,13 +1462,17 @@ function parseSarif(raw, sourceRoot) {
|
|
|
910
1462
|
const reportedCommits = [], reportedCommands = [], reportedTools = [];
|
|
911
1463
|
for (const [runIndex, run] of report.runs.entries()) {
|
|
912
1464
|
reportedTools.push(run.tool.driver.name);
|
|
913
|
-
reportedCommits.push(
|
|
1465
|
+
reportedCommits.push(
|
|
1466
|
+
...(run.versionControlProvenance ?? []).flatMap((v) => v.revisionId ? [v.revisionId] : [])
|
|
1467
|
+
);
|
|
914
1468
|
if (!run.invocations?.length) executed = false;
|
|
915
1469
|
for (const invocation of run.invocations ?? []) {
|
|
916
1470
|
if (invocation.commandLine) reportedCommands.push(invocation.commandLine);
|
|
917
1471
|
if (!invocation.executionSuccessful) executionFailed = true;
|
|
918
1472
|
for (const notification of invocation.toolExecutionNotifications ?? []) {
|
|
919
|
-
diagnostics.push(
|
|
1473
|
+
diagnostics.push(
|
|
1474
|
+
notification.message.text ?? notification.message.markdown ?? "SARIF tool execution notification"
|
|
1475
|
+
);
|
|
920
1476
|
if (notification.level === "error") executionFailed = true;
|
|
921
1477
|
}
|
|
922
1478
|
}
|
|
@@ -959,22 +1515,39 @@ function parseSarif(raw, sourceRoot) {
|
|
|
959
1515
|
const rule = result.ruleIndex !== void 0 ? run.tool.driver.rules?.[result.ruleIndex] : run.tool.driver.rules?.find((rule2) => rule2.id === result.ruleId);
|
|
960
1516
|
const template = result.message.id ? rule?.messageStrings?.[result.message.id] ?? run.tool.driver.globalMessageStrings?.[result.message.id] : void 0;
|
|
961
1517
|
const rawMessage = result.message.text ?? result.message.markdown ?? template?.text ?? template?.markdown;
|
|
962
|
-
if (!rawMessage)
|
|
1518
|
+
if (!rawMessage)
|
|
1519
|
+
diagnostics.push(`Message cannot be resolved for result ${runIndex}:${resultIndex}.`);
|
|
963
1520
|
const rendered = (rawMessage ?? `Unresolved SARIF message ${result.message.id ?? ""}`).replace(/\{(\d+)\}/g, (match, n) => result.message.arguments?.[Number(n)] ?? match);
|
|
964
1521
|
const locations = [];
|
|
965
1522
|
for (const entry of [...result.locations ?? [], ...result.relatedLocations ?? []]) {
|
|
966
1523
|
const ref = entry.physicalLocation?.artifactLocation;
|
|
967
1524
|
const uri = ref ? resolve(ref) : null;
|
|
968
1525
|
const file = uri ? evidencePath(uri, sourceRoot, true) : null;
|
|
969
|
-
if (file)
|
|
970
|
-
|
|
1526
|
+
if (file)
|
|
1527
|
+
locations.push({
|
|
1528
|
+
file,
|
|
1529
|
+
line: entry.physicalLocation?.region?.startLine,
|
|
1530
|
+
column: entry.physicalLocation?.region?.startColumn
|
|
1531
|
+
});
|
|
1532
|
+
else
|
|
1533
|
+
diagnostics.push(
|
|
1534
|
+
`Unresolved or out-of-checkout location for result ${runIndex}:${resultIndex}.`
|
|
1535
|
+
);
|
|
971
1536
|
}
|
|
972
1537
|
const severity = result.level ?? rule?.defaultConfiguration?.level ?? (kind === "fail" ? "warning" : "none");
|
|
973
|
-
findings.push({
|
|
1538
|
+
findings.push({
|
|
1539
|
+
id: `${runIndex}:${resultIndex}`,
|
|
1540
|
+
ruleId: result.ruleId ?? rule?.id,
|
|
1541
|
+
...messagePreview(rendered),
|
|
1542
|
+
severity: severity === "none" ? "note" : severity,
|
|
1543
|
+
state,
|
|
1544
|
+
locations
|
|
1545
|
+
});
|
|
974
1546
|
}
|
|
975
1547
|
}
|
|
976
1548
|
if (!report.runs.length) diagnostics.push("SARIF contains no analysis runs.");
|
|
977
|
-
if (executionFailed)
|
|
1549
|
+
if (executionFailed)
|
|
1550
|
+
diagnostics.push("The SARIF tool reported an unsuccessful analysis invocation.");
|
|
978
1551
|
return {
|
|
979
1552
|
outcome: executionFailed || !report.runs.length || report.runs.some((r) => !r.results) ? "unavailable" : active ? "failed" : "passed",
|
|
980
1553
|
findings,
|
|
@@ -989,20 +1562,24 @@ function parseSarif(raw, sourceRoot) {
|
|
|
989
1562
|
}
|
|
990
1563
|
|
|
991
1564
|
// src/review/evidence/check.ts
|
|
992
|
-
import { z as
|
|
993
|
-
var schema3 =
|
|
994
|
-
version:
|
|
995
|
-
commit:
|
|
996
|
-
results:
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1565
|
+
import { z as z8 } from "zod";
|
|
1566
|
+
var schema3 = z8.object({
|
|
1567
|
+
version: z8.literal(1),
|
|
1568
|
+
commit: z8.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
|
|
1569
|
+
results: z8.array(
|
|
1570
|
+
z8.object({
|
|
1571
|
+
name: z8.string().min(1).max(1e3),
|
|
1572
|
+
status: z8.enum(["passed", "failed", "skipped"]),
|
|
1573
|
+
message: z8.string().max(1e5).optional(),
|
|
1574
|
+
locations: z8.array(
|
|
1575
|
+
z8.object({
|
|
1576
|
+
file: z8.string().min(1).max(4e3),
|
|
1577
|
+
line: z8.number().int().positive().optional(),
|
|
1578
|
+
column: z8.number().int().positive().optional()
|
|
1579
|
+
})
|
|
1580
|
+
).max(100).default([])
|
|
1581
|
+
})
|
|
1582
|
+
).max(1e5)
|
|
1006
1583
|
});
|
|
1007
1584
|
function parseCheck(raw, sourceRoot) {
|
|
1008
1585
|
const report = schema3.parse(raw);
|
|
@@ -1013,21 +1590,26 @@ function parseCheck(raw, sourceRoot) {
|
|
|
1013
1590
|
const locations = result.locations.flatMap((location2) => {
|
|
1014
1591
|
const file = evidencePath(location2.file, sourceRoot);
|
|
1015
1592
|
if (!file) {
|
|
1016
|
-
diagnostics.push(
|
|
1593
|
+
diagnostics.push(
|
|
1594
|
+
`Check location is outside the declared checkout or invalid: ${location2.file}`
|
|
1595
|
+
);
|
|
1017
1596
|
return [];
|
|
1018
1597
|
}
|
|
1019
1598
|
return [{ ...location2, file }];
|
|
1020
1599
|
});
|
|
1021
|
-
if (result.status === "failed")
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1600
|
+
if (result.status === "failed")
|
|
1601
|
+
findings.push({
|
|
1602
|
+
id: String(index2),
|
|
1603
|
+
...messagePreview([result.name, result.message].filter(Boolean).join("\n")),
|
|
1604
|
+
severity: "error",
|
|
1605
|
+
state: "active",
|
|
1606
|
+
locations
|
|
1607
|
+
});
|
|
1028
1608
|
}
|
|
1029
|
-
if (!counts.total)
|
|
1030
|
-
|
|
1609
|
+
if (!counts.total)
|
|
1610
|
+
diagnostics.push("No check results were recorded; an empty report is not passing evidence.");
|
|
1611
|
+
if (counts.skipped)
|
|
1612
|
+
diagnostics.push(`${counts.skipped} checks were skipped; verification is incomplete.`);
|
|
1031
1613
|
return {
|
|
1032
1614
|
outcome: counts.failed ? "failed" : !counts.total ? "unavailable" : !counts.passed ? "skipped" : "passed",
|
|
1033
1615
|
counts,
|
|
@@ -1039,25 +1621,35 @@ function parseCheck(raw, sourceRoot) {
|
|
|
1039
1621
|
}
|
|
1040
1622
|
|
|
1041
1623
|
// src/review/evidence.ts
|
|
1042
|
-
var checkSchema =
|
|
1043
|
-
id:
|
|
1044
|
-
kind:
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1624
|
+
var checkSchema = z9.object({
|
|
1625
|
+
id: z9.string().min(1).max(100),
|
|
1626
|
+
kind: z9.enum([
|
|
1627
|
+
"tests",
|
|
1628
|
+
"static-analysis",
|
|
1629
|
+
"security",
|
|
1630
|
+
"complexity",
|
|
1631
|
+
"duplication",
|
|
1632
|
+
"documentation"
|
|
1633
|
+
]),
|
|
1634
|
+
tool: z9.string().min(1).max(200),
|
|
1635
|
+
command: z9.string().min(1).max(2e3),
|
|
1636
|
+
commit: z9.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
1637
|
+
workingTreeClean: z9.boolean().optional(),
|
|
1638
|
+
source: z9.string().max(2e3).optional(),
|
|
1639
|
+
sourceRoot: z9.string().min(1).max(4e3).optional(),
|
|
1640
|
+
status: z9.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
1641
|
+
reason: z9.string().min(1).max(2e3).optional(),
|
|
1642
|
+
exitCode: z9.number().int().nullable().optional(),
|
|
1643
|
+
environment: z9.object({
|
|
1644
|
+
toolVersion: z9.string().min(1).max(200),
|
|
1645
|
+
runtime: z9.string().min(1).max(200).optional(),
|
|
1646
|
+
platform: z9.string().min(1).max(200).optional(),
|
|
1647
|
+
configuration: z9.array(z9.string().min(1).max(4e3)).max(100).optional()
|
|
1059
1648
|
}).optional(),
|
|
1060
|
-
report:
|
|
1649
|
+
report: z9.object({
|
|
1650
|
+
format: z9.enum(["vitest-json", "sarif", "mason-check-json"]),
|
|
1651
|
+
path: z9.string().min(1).max(4e3)
|
|
1652
|
+
}).optional()
|
|
1061
1653
|
});
|
|
1062
1654
|
function relativeArtifact(root, file) {
|
|
1063
1655
|
const relative = path10.isAbsolute(file) ? path10.relative(root, file) : file;
|
|
@@ -1070,17 +1662,37 @@ function linkFinding(finding, changed, pairs, decisions, freshness) {
|
|
|
1070
1662
|
const paired = pairs.filter((pair) => located.includes(pair.test));
|
|
1071
1663
|
const relatedChangedFiles = located.filter((file) => changed.has(file)).map((file) => ({ file, relationship: "direct" }));
|
|
1072
1664
|
for (const pair of paired) {
|
|
1073
|
-
if (changed.has(pair.source) && !relatedChangedFiles.some((f) => f.file === pair.source))
|
|
1665
|
+
if (changed.has(pair.source) && !relatedChangedFiles.some((f) => f.file === pair.source))
|
|
1666
|
+
relatedChangedFiles.push({
|
|
1667
|
+
file: pair.source,
|
|
1668
|
+
relationship: "paired-test",
|
|
1669
|
+
confidence: pair.confidence
|
|
1670
|
+
});
|
|
1074
1671
|
}
|
|
1075
1672
|
const allFiles = [.../* @__PURE__ */ new Set([...located, ...paired.map((pair) => pair.source)])];
|
|
1076
1673
|
const acceptedDecisions = decisions.map(effectiveDecision).filter((d) => d.status === "active" && decisionApproval(d) === "accepted").map((d) => ({ d, viaFiles: matchingPaths(d.files, allFiles) })).filter((match) => match.viaFiles.length).map(({ d, viaFiles }) => {
|
|
1077
1674
|
const state = freshness[d.id] ?? "unknown", provenance = decisionProvenance(d, state);
|
|
1078
|
-
return {
|
|
1675
|
+
return {
|
|
1676
|
+
id: d.id,
|
|
1677
|
+
title: d.title,
|
|
1678
|
+
owner: provenance.owner,
|
|
1679
|
+
freshness: state,
|
|
1680
|
+
reviewRequired: provenance.reviewRequired,
|
|
1681
|
+
viaFiles
|
|
1682
|
+
};
|
|
1079
1683
|
});
|
|
1080
|
-
return {
|
|
1684
|
+
return {
|
|
1685
|
+
...finding,
|
|
1686
|
+
relatedChangedFiles,
|
|
1687
|
+
acceptedDecisions: acceptedDecisions.slice(0, 5),
|
|
1688
|
+
totalAcceptedDecisions: acceptedDecisions.length
|
|
1689
|
+
};
|
|
1081
1690
|
}
|
|
1082
1691
|
async function collectReviewEvidence(root, manifests, changedFiles, decisions, decisionFreshness = {}, reviewedHead) {
|
|
1083
|
-
const [headHash, workingTree] = await Promise.all([
|
|
1692
|
+
const [headHash, workingTree] = await Promise.all([
|
|
1693
|
+
reviewedHead ?? getCurrentGitHash(root),
|
|
1694
|
+
getWorkingTree(root)
|
|
1695
|
+
]);
|
|
1084
1696
|
const output = {
|
|
1085
1697
|
version: 1,
|
|
1086
1698
|
scope: "committed",
|
|
@@ -1094,12 +1706,13 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
1094
1706
|
};
|
|
1095
1707
|
const seen = /* @__PURE__ */ new Set(), changed = new Set(changedFiles);
|
|
1096
1708
|
let pairs;
|
|
1097
|
-
if (manifests.length > 10)
|
|
1709
|
+
if (manifests.length > 10)
|
|
1710
|
+
output.diagnostics.push("Only the first 10 evidence manifests were imported.");
|
|
1098
1711
|
for (const supplied of manifests.slice(0, 10)) {
|
|
1099
1712
|
let manifest, raw;
|
|
1100
1713
|
try {
|
|
1101
1714
|
manifest = relativeArtifact(root, supplied);
|
|
1102
|
-
raw =
|
|
1715
|
+
raw = z9.object({ version: z9.literal(1), checks: z9.array(z9.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
1103
1716
|
} catch (error) {
|
|
1104
1717
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
1105
1718
|
continue;
|
|
@@ -1117,7 +1730,9 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
1117
1730
|
}
|
|
1118
1731
|
const check = input.data;
|
|
1119
1732
|
if (seen.has(check.id)) {
|
|
1120
|
-
output.diagnostics.push(
|
|
1733
|
+
output.diagnostics.push(
|
|
1734
|
+
`Duplicate check id ${check.id}; give different runs distinct ids.`
|
|
1735
|
+
);
|
|
1121
1736
|
continue;
|
|
1122
1737
|
}
|
|
1123
1738
|
seen.add(check.id);
|
|
@@ -1142,22 +1757,34 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
1142
1757
|
...check.environment ? { environment: check.environment } : {}
|
|
1143
1758
|
};
|
|
1144
1759
|
output.checks.push(result);
|
|
1145
|
-
if (check.workingTreeClean !== true)
|
|
1146
|
-
|
|
1760
|
+
if (check.workingTreeClean !== true)
|
|
1761
|
+
result.diagnostics.push(
|
|
1762
|
+
"The check's working tree was dirty or not recorded; its results cannot be attributed to the claimed commit alone."
|
|
1763
|
+
);
|
|
1764
|
+
else if (result.commit && headHash !== "unknown")
|
|
1765
|
+
result.freshness = result.commit === headHash.toLowerCase() ? "current" : "stale";
|
|
1147
1766
|
else result.diagnostics.push("The tested commit or reviewed HEAD is unknown.");
|
|
1148
1767
|
if (check.status !== "completed") {
|
|
1149
1768
|
result.outcome = check.status;
|
|
1150
|
-
result.diagnostics.push(
|
|
1769
|
+
result.diagnostics.push(
|
|
1770
|
+
check.reason ?? "No reason was recorded for this skipped or unavailable check."
|
|
1771
|
+
);
|
|
1151
1772
|
continue;
|
|
1152
1773
|
}
|
|
1153
1774
|
try {
|
|
1154
1775
|
if (!check.report) throw new Error("Completed check has no report artifact.");
|
|
1155
|
-
if (check.report.format !== "mason-check-json" && check.kind === "tests" !== (check.report.format === "vitest-json"))
|
|
1776
|
+
if (check.report.format !== "mason-check-json" && check.kind === "tests" !== (check.report.format === "vitest-json"))
|
|
1777
|
+
throw new Error(
|
|
1778
|
+
"Test checks require vitest-json or mason-check-json; analysis checks require sarif or mason-check-json."
|
|
1779
|
+
);
|
|
1156
1780
|
const file = relativeArtifact(root, check.report.path);
|
|
1157
1781
|
const report = await readStoreJson(root, file);
|
|
1158
1782
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
1159
1783
|
const sourceRoot = check.sourceRoot ?? root;
|
|
1160
|
-
if (!path10.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot))
|
|
1784
|
+
if (!path10.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot))
|
|
1785
|
+
throw new Error(
|
|
1786
|
+
"sourceRoot must identify the absolute checkout root on the check runner."
|
|
1787
|
+
);
|
|
1161
1788
|
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
1162
1789
|
result.outcome = parsed.outcome;
|
|
1163
1790
|
result.counts = parsed.counts;
|
|
@@ -1165,18 +1792,26 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
1165
1792
|
result.diagnostics.push(...parsed.diagnostics);
|
|
1166
1793
|
result.reportedCommands = parsed.reportedCommands;
|
|
1167
1794
|
result.reportedTools = parsed.reportedTools;
|
|
1168
|
-
if (parsed.reportedCommits?.some(
|
|
1795
|
+
if (parsed.reportedCommits?.some(
|
|
1796
|
+
(commit) => !result.commit || commit.toLowerCase() !== result.commit
|
|
1797
|
+
)) {
|
|
1169
1798
|
result.freshness = "unknown";
|
|
1170
1799
|
result.incomplete = true;
|
|
1171
|
-
result.diagnostics.push(
|
|
1800
|
+
result.diagnostics.push(
|
|
1801
|
+
"Report revision metadata conflicts with, or cannot be tied to, the manifest's tested commit."
|
|
1802
|
+
);
|
|
1172
1803
|
}
|
|
1173
1804
|
if (check.exitCode == null && !parsed.executed) {
|
|
1174
1805
|
result.incomplete = true;
|
|
1175
|
-
result.diagnostics.push(
|
|
1806
|
+
result.diagnostics.push(
|
|
1807
|
+
"Neither an exit code nor successful analysis invocation was recorded; completion cannot be confirmed."
|
|
1808
|
+
);
|
|
1176
1809
|
}
|
|
1177
1810
|
if (check.exitCode != null && check.exitCode !== 0 && result.outcome === "passed") {
|
|
1178
1811
|
result.outcome = "failed";
|
|
1179
|
-
result.diagnostics.push(
|
|
1812
|
+
result.diagnostics.push(
|
|
1813
|
+
`Check command exited ${check.exitCode} despite a report with no active failures.`
|
|
1814
|
+
);
|
|
1180
1815
|
}
|
|
1181
1816
|
if (check.kind === "tests" && pairs === void 0) {
|
|
1182
1817
|
try {
|
|
@@ -1188,11 +1823,18 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
1188
1823
|
}
|
|
1189
1824
|
result.totalFindings = parsed.findings.length;
|
|
1190
1825
|
const testPairs = check.kind === "tests" ? pairs ?? [] : [];
|
|
1191
|
-
const relevant = /* @__PURE__ */ new Set([
|
|
1826
|
+
const relevant = /* @__PURE__ */ new Set([
|
|
1827
|
+
...changed,
|
|
1828
|
+
...testPairs.filter((p) => changed.has(p.source)).map((p) => p.test)
|
|
1829
|
+
]);
|
|
1192
1830
|
const touchesChange = (f) => f.locations.some((l) => relevant.has(l.file));
|
|
1193
|
-
parsed.findings.sort(
|
|
1831
|
+
parsed.findings.sort(
|
|
1832
|
+
(a, b) => Number(b.state === "active") - Number(a.state === "active") || Number(touchesChange(b)) - Number(touchesChange(a))
|
|
1833
|
+
);
|
|
1194
1834
|
result.findings = parsed.findings.slice(0, MAX_FINDINGS).map((f) => linkFinding(f, changed, testPairs, decisions, decisionFreshness));
|
|
1195
|
-
result.truncated = parsed.findings.length > MAX_FINDINGS || result.findings.some(
|
|
1835
|
+
result.truncated = parsed.findings.length > MAX_FINDINGS || result.findings.some(
|
|
1836
|
+
(f) => f.truncated || f.totalAcceptedDecisions > f.acceptedDecisions.length
|
|
1837
|
+
);
|
|
1196
1838
|
} catch (error) {
|
|
1197
1839
|
result.outcome = "unavailable";
|
|
1198
1840
|
result.incomplete = true;
|
|
@@ -1205,9 +1847,12 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
1205
1847
|
output.summary[check.outcome]++;
|
|
1206
1848
|
if (check.freshness !== "current") output.summary[check.freshness]++;
|
|
1207
1849
|
}
|
|
1208
|
-
if (output.checks.some((c) => c.outcome === "failed" && c.freshness === "current"))
|
|
1209
|
-
|
|
1210
|
-
else if (output.
|
|
1850
|
+
if (output.checks.some((c) => c.outcome === "failed" && c.freshness === "current"))
|
|
1851
|
+
output.status = "failed";
|
|
1852
|
+
else if (!output.checks.length || output.checks.every((c) => c.outcome === "unavailable"))
|
|
1853
|
+
output.status = "unavailable";
|
|
1854
|
+
else if (output.diagnostics.length || output.checks.some((c) => c.outcome !== "passed" || c.freshness !== "current" || c.incomplete))
|
|
1855
|
+
output.status = "incomplete";
|
|
1211
1856
|
else output.status = "passed";
|
|
1212
1857
|
return output;
|
|
1213
1858
|
}
|
|
@@ -1264,15 +1909,26 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
1264
1909
|
report.diagnostics = store.diagnostics;
|
|
1265
1910
|
const decisionDrift = await computeDecisionDrift(resolvedRoot, store.records);
|
|
1266
1911
|
if (options.evidence !== void 0) {
|
|
1267
|
-
report.evidence = await collectReviewEvidence(
|
|
1912
|
+
report.evidence = await collectReviewEvidence(
|
|
1913
|
+
resolvedRoot,
|
|
1914
|
+
options.evidence,
|
|
1915
|
+
changedFiles,
|
|
1916
|
+
store.records,
|
|
1917
|
+
decisionDrift.freshness,
|
|
1918
|
+
head
|
|
1919
|
+
);
|
|
1268
1920
|
if (store.diagnostics.length) {
|
|
1269
|
-
report.evidence.diagnostics.push(
|
|
1921
|
+
report.evidence.diagnostics.push(
|
|
1922
|
+
"Invalid decision records make knowledge associations incomplete; consult review diagnostics."
|
|
1923
|
+
);
|
|
1270
1924
|
if (report.evidence.status === "passed") report.evidence.status = "incomplete";
|
|
1271
1925
|
}
|
|
1272
1926
|
}
|
|
1273
1927
|
const finalize = async () => {
|
|
1274
1928
|
if (report.evidence && await getCurrentGitHash(resolvedRoot) !== head) {
|
|
1275
|
-
report.evidence.diagnostics.push(
|
|
1929
|
+
report.evidence.diagnostics.push(
|
|
1930
|
+
"HEAD changed during the review; rerun to obtain consistent change and knowledge associations."
|
|
1931
|
+
);
|
|
1276
1932
|
for (const check of report.evidence.checks) check.freshness = "unknown";
|
|
1277
1933
|
report.evidence.summary.stale = 0;
|
|
1278
1934
|
report.evidence.summary.unknown = report.evidence.checks.length;
|
|
@@ -1291,7 +1947,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
1291
1947
|
analyzed,
|
|
1292
1948
|
async (relPath) => {
|
|
1293
1949
|
try {
|
|
1294
|
-
await
|
|
1950
|
+
await fs7.access(path11.join(resolvedRoot, relPath));
|
|
1295
1951
|
return true;
|
|
1296
1952
|
} catch {
|
|
1297
1953
|
return false;
|
|
@@ -1311,7 +1967,11 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
1311
1967
|
const touched = anchorsTouched(effective, changedFiles);
|
|
1312
1968
|
const proposalTouched = effective !== record ? anchorsTouched(record, changedFiles) : [];
|
|
1313
1969
|
if (touched.length > 0 || proposalTouched.length > 0) {
|
|
1314
|
-
const { pendingProposal, ...knowledge } = decisionKnowledge(
|
|
1970
|
+
const { pendingProposal, ...knowledge } = decisionKnowledge(
|
|
1971
|
+
record,
|
|
1972
|
+
decisionDrift.freshness?.[record.id] ?? "unknown",
|
|
1973
|
+
decisionDrift.pendingProposals?.[record.id]?.freshness ?? "unknown"
|
|
1974
|
+
);
|
|
1315
1975
|
report.touchedDecisions.push({
|
|
1316
1976
|
...knowledge,
|
|
1317
1977
|
...pendingProposal ? { pendingProposal: { ...pendingProposal, touchedFiles: proposalTouched } } : {},
|
|
@@ -1388,36 +2048,71 @@ function parseArgs(argv) {
|
|
|
1388
2048
|
throw new Error(`Unknown argument: ${arg}`);
|
|
1389
2049
|
}
|
|
1390
2050
|
}
|
|
1391
|
-
if (parsed.requireEvidence && !parsed.evidence.length)
|
|
2051
|
+
if (parsed.requireEvidence && !parsed.evidence.length)
|
|
2052
|
+
throw new Error("--require-evidence requires at least one --evidence manifest");
|
|
1392
2053
|
return parsed;
|
|
1393
2054
|
}
|
|
1394
2055
|
var display = (value, limit = 500) => stripVTControlCharacters(value).replace(/[\r\n\t]/g, " ").slice(0, limit);
|
|
1395
2056
|
function evidenceSummary(report) {
|
|
1396
2057
|
const evidence = report.evidence;
|
|
1397
2058
|
if (!evidence) return [];
|
|
1398
|
-
const lines = [
|
|
1399
|
-
|
|
2059
|
+
const lines = [
|
|
2060
|
+
`Imported CI evidence: ${evidence.status} (commit ${evidence.headHash.slice(0, 12)}; committed scope).`
|
|
2061
|
+
];
|
|
2062
|
+
for (const diagnostic of evidence.diagnostics)
|
|
2063
|
+
lines.push(` Evidence unavailable: ${display(diagnostic)}`);
|
|
1400
2064
|
for (const check of evidence.checks) {
|
|
1401
|
-
lines.push(
|
|
1402
|
-
|
|
2065
|
+
lines.push(
|
|
2066
|
+
` [${check.kind}] ${display(check.id)}: ${check.outcome}, ${check.freshness} (${display(check.tool)})`
|
|
2067
|
+
);
|
|
2068
|
+
lines.push(
|
|
2069
|
+
` command: ${display(check.command)}; tested commit: ${check.commit ?? "unknown"}`
|
|
2070
|
+
);
|
|
1403
2071
|
if (check.environment) {
|
|
1404
|
-
lines.push(
|
|
1405
|
-
|
|
2072
|
+
lines.push(
|
|
2073
|
+
` environment: ${display(check.tool)} ${display(check.environment.toolVersion)}` + (check.environment.runtime ? `; ${display(check.environment.runtime)}` : "") + (check.environment.platform ? `; ${display(check.environment.platform)}` : "")
|
|
2074
|
+
);
|
|
2075
|
+
if (check.environment.configuration?.length)
|
|
2076
|
+
lines.push(
|
|
2077
|
+
" configuration: " + check.environment.configuration.map((value) => display(value)).join(", ")
|
|
2078
|
+
);
|
|
1406
2079
|
}
|
|
1407
|
-
lines.push(
|
|
2080
|
+
lines.push(
|
|
2081
|
+
` source: ${display(check.source ?? check.manifest)}; report: ${display(check.report?.path ?? "none")}`
|
|
2082
|
+
);
|
|
1408
2083
|
if (Object.keys(check.counts).length) lines.push(` counts: ${JSON.stringify(check.counts)}`);
|
|
1409
|
-
for (const diagnostic of check.diagnostics.slice(0, 5))
|
|
2084
|
+
for (const diagnostic of check.diagnostics.slice(0, 5))
|
|
2085
|
+
lines.push(` note: ${display(diagnostic)}`);
|
|
1410
2086
|
for (const finding of check.findings.slice(0, 10)) {
|
|
1411
2087
|
const locations = finding.locations.map((l) => `${l.file}${l.line ? `:${l.line}` : ""}`).join(", ") || "no resolved file location";
|
|
1412
|
-
lines.push(
|
|
1413
|
-
|
|
1414
|
-
|
|
2088
|
+
lines.push(
|
|
2089
|
+
` [${finding.state}/${finding.severity}] ${display(finding.ruleId ?? finding.id)} ${display(locations)}: ${display(finding.message)}`
|
|
2090
|
+
);
|
|
2091
|
+
if (finding.relatedChangedFiles.length)
|
|
2092
|
+
lines.push(
|
|
2093
|
+
` changed files: ${finding.relatedChangedFiles.map((f) => `${display(f.file)} (${f.relationship}${f.confidence ? `, ${f.confidence}` : ""})`).join(", ")}`
|
|
2094
|
+
);
|
|
2095
|
+
for (const decision of finding.acceptedDecisions)
|
|
2096
|
+
lines.push(
|
|
2097
|
+
` related accepted decision: ${display(decision.id)}; owner ${display(decision.owner ?? "unknown")}; freshness ${decision.freshness}${decision.reviewRequired ? "; needs review" : ""}`
|
|
2098
|
+
);
|
|
1415
2099
|
}
|
|
1416
|
-
if (check.totalFindings > 10 || check.truncated || check.diagnostics.length > 5)
|
|
2100
|
+
if (check.totalFindings > 10 || check.truncated || check.diagnostics.length > 5)
|
|
2101
|
+
lines.push(
|
|
2102
|
+
` Output abbreviated (${check.totalFindings} findings); inspect --json and the original artifact for full evidence.`
|
|
2103
|
+
);
|
|
1417
2104
|
}
|
|
1418
|
-
if (!evidence.workingTree.available)
|
|
1419
|
-
|
|
1420
|
-
|
|
2105
|
+
if (!evidence.workingTree.available)
|
|
2106
|
+
lines.push(
|
|
2107
|
+
" Working-tree state unavailable; imported evidence only describes the recorded commit."
|
|
2108
|
+
);
|
|
2109
|
+
else if (evidence.workingTree.changedFiles.length)
|
|
2110
|
+
lines.push(
|
|
2111
|
+
` ${evidence.workingTree.changedFiles.length} uncommitted paths are outside this evidence's scope.`
|
|
2112
|
+
);
|
|
2113
|
+
lines.push(
|
|
2114
|
+
" Related decisions are associations, not proven violations. Passing imported checks do not establish complete coverage."
|
|
2115
|
+
);
|
|
1421
2116
|
return lines;
|
|
1422
2117
|
}
|
|
1423
2118
|
function formatReviewSummary(report) {
|
|
@@ -1425,16 +2120,13 @@ function formatReviewSummary(report) {
|
|
|
1425
2120
|
lines.push(
|
|
1426
2121
|
`Diff vs ${report.base} (merge-base ${report.mergeBase.slice(0, 7)}): ${report.changedFiles.length} changed file${report.changedFiles.length === 1 ? "" : "s"}.`
|
|
1427
2122
|
);
|
|
1428
|
-
for (const diagnostic of report.diagnostics ?? [])
|
|
2123
|
+
for (const diagnostic of report.diagnostics ?? [])
|
|
2124
|
+
lines.push(`Invalid decision record ${diagnostic.path}: ${diagnostic.message}`);
|
|
1429
2125
|
if (report.truncated) {
|
|
1430
|
-
lines.push(
|
|
1431
|
-
`Large diff \u2013 co-change analysis limited to the first ${50} files.`
|
|
1432
|
-
);
|
|
2126
|
+
lines.push(`Large diff \u2013 co-change analysis limited to the first ${50} files.`);
|
|
1433
2127
|
}
|
|
1434
2128
|
if (!report.historyAvailable) {
|
|
1435
|
-
lines.push(
|
|
1436
|
-
"Git history unavailable (shallow clone?) \u2013 co-change analysis skipped."
|
|
1437
|
-
);
|
|
2129
|
+
lines.push("Git history unavailable (shallow clone?) \u2013 co-change analysis skipped.");
|
|
1438
2130
|
}
|
|
1439
2131
|
if (report.missingPartners.length > 0) {
|
|
1440
2132
|
lines.push("Possible forgotten co-change partners:");
|
|
@@ -1445,16 +2137,24 @@ function formatReviewSummary(report) {
|
|
|
1445
2137
|
}
|
|
1446
2138
|
}
|
|
1447
2139
|
if (report.touchedDecisions.length > 0) {
|
|
1448
|
-
lines.push(
|
|
2140
|
+
lines.push(
|
|
2141
|
+
"Recorded decisions touched by this diff (check approval and freshness before relying on them):"
|
|
2142
|
+
);
|
|
1449
2143
|
for (const d of report.touchedDecisions) {
|
|
1450
2144
|
lines.push(
|
|
1451
2145
|
` [decision] ${d.title} (${d.category}; ${d.approval ?? "unreviewed"}; owner ${d.owner ?? "unknown"}; freshness ${d.freshness ?? "unknown"}; via ${d.touchedFiles.join(", ") || "pending proposal"})`
|
|
1452
2146
|
);
|
|
1453
|
-
if (d.sources?.length)
|
|
1454
|
-
|
|
2147
|
+
if (d.sources?.length)
|
|
2148
|
+
lines.push(` sources: ${d.sources.map((s) => s.reference).join(", ")}`);
|
|
2149
|
+
if (d.lastReview)
|
|
2150
|
+
lines.push(
|
|
2151
|
+
` last review: ${d.lastReview.reviewer} at ${d.lastReview.gitHash.slice(0, 7)}`
|
|
2152
|
+
);
|
|
1455
2153
|
if (d.pendingProposal) {
|
|
1456
2154
|
const p = d.pendingProposal;
|
|
1457
|
-
lines.push(
|
|
2155
|
+
lines.push(
|
|
2156
|
+
` [proposed revision ${p.revision}] ${p.title} (owner ${p.owner ?? "unknown"}; freshness ${p.trust.freshness}; via ${p.touchedFiles.join(", ") || "no draft anchors touched"}); accepted revision ${d.revision} remains operative`
|
|
2157
|
+
);
|
|
1458
2158
|
}
|
|
1459
2159
|
}
|
|
1460
2160
|
}
|
|
@@ -1490,7 +2190,11 @@ async function runReviewCli(argv, io = {
|
|
|
1490
2190
|
);
|
|
1491
2191
|
return 2;
|
|
1492
2192
|
}
|
|
1493
|
-
const report = await computeReview(
|
|
2193
|
+
const report = await computeReview(
|
|
2194
|
+
rootDir,
|
|
2195
|
+
base,
|
|
2196
|
+
args.evidence.length ? { evidence: args.evidence } : {}
|
|
2197
|
+
);
|
|
1494
2198
|
if (!report) {
|
|
1495
2199
|
io.err(
|
|
1496
2200
|
`Could not resolve a merge base between ${base} and HEAD in ${rootDir} \u2013 not a git repository, unknown ref, or unrelated histories.`
|
|
@@ -1501,9 +2205,13 @@ async function runReviewCli(argv, io = {
|
|
|
1501
2205
|
io.out(JSON.stringify(report, null, 2));
|
|
1502
2206
|
} else {
|
|
1503
2207
|
io.out(formatReviewSummary(report));
|
|
1504
|
-
if (args.evidence.length && !args.requireEvidence)
|
|
2208
|
+
if (args.evidence.length && !args.requireEvidence)
|
|
2209
|
+
io.out(
|
|
2210
|
+
"Imported evidence is advisory for exit codes; use --require-evidence to require current passing checks."
|
|
2211
|
+
);
|
|
1505
2212
|
}
|
|
1506
|
-
if (args.requireEvidence && report.evidence?.status !== "passed")
|
|
2213
|
+
if (args.requireEvidence && report.evidence?.status !== "passed")
|
|
2214
|
+
return report.evidence?.status === "failed" ? 1 : 2;
|
|
1507
2215
|
return report.missingPartners.length > 0 ? 1 : 0;
|
|
1508
2216
|
}
|
|
1509
2217
|
|