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