mason-context 0.20.2 → 0.22.0

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