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.
@@ -31,7 +31,7 @@ var init_types = __esm({
31
31
  });
32
32
 
33
33
  // src/drift/cli.ts
34
- import path10 from "path";
34
+ import path9 from "path";
35
35
 
36
36
  // src/utils/git-read.ts
37
37
  import { execFile } from "child_process";
@@ -55,7 +55,19 @@ async function profilePhase(label, run) {
55
55
 
56
56
  // src/utils/git-read.ts
57
57
  var execute = promisify(execFile);
58
- var commands = /* @__PURE__ */ new Set(["rev-parse", "symbolic-ref", "for-each-ref", "ls-files", "check-ignore", "status", "log", "diff", "ls-tree", "show", "merge-base"]);
58
+ var commands = /* @__PURE__ */ new Set([
59
+ "rev-parse",
60
+ "symbolic-ref",
61
+ "for-each-ref",
62
+ "ls-files",
63
+ "check-ignore",
64
+ "status",
65
+ "log",
66
+ "diff",
67
+ "ls-tree",
68
+ "show",
69
+ "merge-base"
70
+ ]);
59
71
  function execGit(args, options) {
60
72
  const command = args.find((arg) => !arg.startsWith("-")) ?? "other";
61
73
  return profilePhase("git." + (commands.has(command) ? command : "other"), async () => {
@@ -72,10 +84,15 @@ function execGit(args, options) {
72
84
 
73
85
  // src/drift/drift.ts
74
86
  import fs4 from "fs/promises";
75
- import path6 from "path";
87
+ import path4 from "path";
76
88
 
77
- // src/snapshot/snapshot.ts
78
- import path5 from "path";
89
+ // src/utils/validation.ts
90
+ function isRecord(value) {
91
+ return value !== null && typeof value === "object" && !Array.isArray(value);
92
+ }
93
+ function isStringArray(value) {
94
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
95
+ }
79
96
 
80
97
  // src/utils/files.ts
81
98
  import fs from "fs/promises";
@@ -87,7 +104,8 @@ import fg from "fast-glob";
87
104
  import path from "path";
88
105
  function normalizeRepoPath(value) {
89
106
  const slash = value.replace(/\\/g, "/");
90
- if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
107
+ if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash))
108
+ return null;
91
109
  if (slash.split("/").includes("..")) return null;
92
110
  const normalized = path.posix.normalize(slash).replace(/\/$/, "");
93
111
  return normalized === "." ? null : normalized;
@@ -102,11 +120,69 @@ function anchorMatches(anchor, file) {
102
120
  return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
103
121
  }
104
122
  function matchingPaths(anchors, files) {
105
- return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
123
+ return [...new Set(files)].filter(
124
+ (file) => anchors.some((anchor) => anchorMatches(anchor, file))
125
+ );
126
+ }
127
+ function indexMatchingPaths(scopes, files) {
128
+ const node = () => ({ children: /* @__PURE__ */ new Map(), scopes: /* @__PURE__ */ new Set() });
129
+ const root = node();
130
+ const matches = scopes.map(() => []);
131
+ scopes.forEach((anchors, index) => {
132
+ for (const anchor of anchors) {
133
+ const normalized = normalizeRepoPath(anchor);
134
+ if (!normalized) continue;
135
+ let current = root;
136
+ for (const part of normalized.split("/")) {
137
+ if (!current.children.has(part)) current.children.set(part, node());
138
+ current = current.children.get(part);
139
+ }
140
+ current.scopes.add(index);
141
+ }
142
+ });
143
+ for (const file of new Set(files)) {
144
+ const normalized = normalizeRepoPath(file);
145
+ if (!normalized) continue;
146
+ const selected = /* @__PURE__ */ new Set();
147
+ let current = root;
148
+ for (const part of normalized.split("/")) {
149
+ current = current.children.get(part);
150
+ if (!current) break;
151
+ for (const index of current.scopes) selected.add(index);
152
+ }
153
+ for (const index of selected) matches[index].push(file);
154
+ }
155
+ return matches;
106
156
  }
107
157
 
108
158
  // src/utils/files.ts
109
- 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"];
159
+ var SOURCE_EXTENSIONS = [
160
+ "ts",
161
+ "tsx",
162
+ "js",
163
+ "jsx",
164
+ "mts",
165
+ "cts",
166
+ "mjs",
167
+ "cjs",
168
+ "vue",
169
+ "svelte",
170
+ "kt",
171
+ "kts",
172
+ "java",
173
+ "py",
174
+ "go",
175
+ "rs",
176
+ "swift",
177
+ "rb",
178
+ "cs",
179
+ "cpp",
180
+ "c",
181
+ "h",
182
+ "hpp",
183
+ "dart",
184
+ "php"
185
+ ];
110
186
  var SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
111
187
  var SOURCE_IGNORE = [
112
188
  "**/node_modules/**",
@@ -134,11 +210,16 @@ var SOURCE_IGNORE = [
134
210
  var MAX_SOURCE_BYTES = 1024 * 1024;
135
211
  function isSensitiveFile(file) {
136
212
  return file.split(/[\\/]/).some(
137
- (part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(part)
213
+ (part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(
214
+ part
215
+ )
138
216
  );
139
217
  }
140
218
  async function readBoundedFile(file, maxBytes) {
141
- const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
219
+ const handle = await fs.open(
220
+ file,
221
+ constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW
222
+ );
142
223
  try {
143
224
  const stat = await handle.stat();
144
225
  if (!stat.isFile() || stat.size > maxBytes) return null;
@@ -158,36 +239,44 @@ async function loadProjectConfig(root) {
158
239
  try {
159
240
  const canonicalRoot = await fs.realpath(root);
160
241
  const configPath = await fs.realpath(path2.join(root, ".mason/config.json"));
161
- if (!isWithinRoot(canonicalRoot, configPath)) throw new Error("Project configuration resolves outside the repository");
242
+ if (!isWithinRoot(canonicalRoot, configPath))
243
+ throw new Error("Project configuration resolves outside the repository");
162
244
  const raw = await readBoundedFile(configPath, 64 * 1024);
163
- if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
245
+ if (raw === null)
246
+ throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
164
247
  const value = JSON.parse(raw);
165
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
248
+ if (!isRecord(value)) throw new Error("Expected a configuration object");
166
249
  const config = {};
167
250
  for (const key of ["patterns", "alwaysInclude", "ignore"]) {
168
251
  if (value[key] === void 0) continue;
169
- if (!Array.isArray(value[key]) || !value[key].every((s) => typeof s === "string")) {
252
+ if (!isStringArray(value[key])) {
170
253
  throw new Error(`Configuration ${key} must be an array of strings`);
171
254
  }
172
255
  config[key] = value[key];
173
256
  }
174
257
  if (value.audit !== void 0) {
175
258
  const audit = value.audit;
176
- if (!audit || typeof audit !== "object" || Array.isArray(audit)) throw new Error("Configuration audit must be an object");
259
+ if (!isRecord(audit)) throw new Error("Configuration audit must be an object");
177
260
  const patterns = (raw2) => {
178
- if (!Array.isArray(raw2) || raw2.length > 100 || !raw2.every((s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s))) {
179
- throw new Error("Audit patterns must be at most 100 repository-relative globs without traversal or negation");
261
+ if (!isStringArray(raw2) || raw2.length > 100 || !raw2.every(
262
+ (s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s)
263
+ )) {
264
+ throw new Error(
265
+ "Audit patterns must be at most 100 repository-relative globs without traversal or negation"
266
+ );
180
267
  }
181
268
  return raw2;
182
269
  };
183
270
  config.audit = {};
184
271
  if (audit.include !== void 0) config.audit.include = patterns(audit.include);
185
272
  if (audit.exclude !== void 0) {
186
- if (!audit.exclude || typeof audit.exclude !== "object" || Array.isArray(audit.exclude)) throw new Error("audit.exclude must map check names to patterns");
273
+ if (!isRecord(audit.exclude))
274
+ throw new Error("audit.exclude must map check names to patterns");
187
275
  const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
188
276
  config.audit.exclude = {};
189
277
  for (const [check, globs] of Object.entries(audit.exclude)) {
190
- if (!ALL_CHECKS2.includes(check)) throw new Error("Unknown audit check: " + check);
278
+ if (!ALL_CHECKS2.includes(check))
279
+ throw new Error("Unknown audit check: " + check);
191
280
  config.audit.exclude[check] = patterns(globs);
192
281
  }
193
282
  }
@@ -195,7 +284,10 @@ async function loadProjectConfig(root) {
195
284
  return config;
196
285
  } catch (error) {
197
286
  if (error.code === "ENOENT") return {};
198
- throw new Error(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`);
287
+ throw new Error(
288
+ `Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`,
289
+ { cause: error }
290
+ );
199
291
  }
200
292
  }
201
293
  async function createFileAccess(rootDir) {
@@ -205,7 +297,10 @@ async function createFileAccess(rootDir) {
205
297
  const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
206
298
  let gitFiles = null;
207
299
  try {
208
- const { stdout } = await execGit(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
300
+ const { stdout } = await execGit(
301
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
302
+ { cwd: root, maxBuffer: 50 * 1024 * 1024 }
303
+ );
209
304
  gitFiles = new Set(stdout.split("\0").filter(Boolean));
210
305
  } catch {
211
306
  let inGit = false;
@@ -218,14 +313,17 @@ async function createFileAccess(rootDir) {
218
313
  }
219
314
  async function resolve(file) {
220
315
  const relative = normalizeRepoPath(file);
221
- if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative)) return null;
316
+ if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative))
317
+ return null;
222
318
  const candidate = path2.join(root, relative);
223
319
  try {
224
320
  const real = await fs.realpath(candidate);
225
- if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path2.relative(canonicalRoot, real))) return null;
321
+ if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path2.relative(canonicalRoot, real)))
322
+ return null;
226
323
  const stat = await fs.stat(real);
227
324
  if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
228
- if (gitFiles && !gitFiles.has(path2.relative(canonicalRoot, real).split(path2.sep).join("/"))) return null;
325
+ if (gitFiles && !gitFiles.has(path2.relative(canonicalRoot, real).split(path2.sep).join("/")))
326
+ return null;
229
327
  return real;
230
328
  } catch {
231
329
  return null;
@@ -241,7 +339,10 @@ async function createFileAccess(rootDir) {
241
339
  if (!relative) return null;
242
340
  const real = await resolve(relative);
243
341
  if (!real) return null;
244
- for (const rel of /* @__PURE__ */ new Set([relative, path2.relative(canonicalRoot, real).split(path2.sep).join("/")])) {
342
+ for (const rel of /* @__PURE__ */ new Set([
343
+ relative,
344
+ path2.relative(canonicalRoot, real).split(path2.sep).join("/")
345
+ ])) {
245
346
  if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
246
347
  }
247
348
  try {
@@ -251,7 +352,7 @@ async function createFileAccess(rootDir) {
251
352
  return null;
252
353
  }
253
354
  }
254
- return { root, config, list, read };
355
+ return { root, config, list, read, inventory: () => gitFiles ? [...gitFiles].sort() : null };
255
356
  }
256
357
 
257
358
  // src/utils/storage.ts
@@ -293,45 +394,47 @@ async function readStoreJson(root, relative) {
293
394
  return parsed;
294
395
  } catch (error) {
295
396
  if (error.code === "ENOENT") return null;
296
- throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
397
+ throw new Error(
398
+ `Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`,
399
+ { cause: error }
400
+ );
297
401
  }
298
402
  }
299
403
 
300
404
  // src/snapshot/snapshot.ts
301
- import { z } from "zod";
405
+ import { z as z2 } from "zod";
302
406
 
303
407
  // src/utils/store-lock.ts
408
+ import { z } from "zod";
304
409
  import fs3 from "fs/promises";
305
410
  import os from "os";
306
-
307
- // src/test-map.ts
308
- import path4 from "path";
411
+ var lockOwnerSchema = z.object({ host: z.string(), pid: z.number().int().positive() });
309
412
 
310
413
  // src/snapshot/snapshot.ts
311
- var repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
414
+ var repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
312
415
  var verificationFields = {
313
- refreshedHash: z.string().optional(),
314
- verifiedAt: z.string().optional(),
315
- verifiedHash: z.string().optional(),
316
- verificationToken: z.string().optional(),
317
- verificationFailed: z.boolean().optional(),
318
- verificationNote: z.string().optional()
416
+ refreshedHash: z2.string().optional(),
417
+ verifiedAt: z2.string().optional(),
418
+ verifiedHash: z2.string().optional(),
419
+ verificationToken: z2.string().optional(),
420
+ verificationFailed: z2.boolean().optional(),
421
+ verificationNote: z2.string().optional()
319
422
  };
320
- var featureSchema = z.object({
321
- description: z.string(),
322
- files: z.array(repoPath),
323
- tests: z.array(repoPath).optional(),
324
- type: z.enum(["capability", "infrastructure"]).optional(),
423
+ var featureSchema = z2.object({
424
+ description: z2.string(),
425
+ files: z2.array(repoPath),
426
+ tests: z2.array(repoPath).optional(),
427
+ type: z2.enum(["capability", "infrastructure"]).optional(),
325
428
  ...verificationFields
326
429
  }).passthrough();
327
- var flowSchema = z.object({ description: z.string(), chain: z.array(repoPath), ...verificationFields }).passthrough();
328
- var snapshotSchema = z.object({
329
- version: z.literal(2),
330
- createdAt: z.string(),
331
- updatedAt: z.string(),
332
- gitHash: z.string(),
333
- features: z.record(featureSchema),
334
- flows: z.record(flowSchema)
430
+ var flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
431
+ var snapshotSchema = z2.object({
432
+ version: z2.literal(2),
433
+ createdAt: z2.string(),
434
+ updatedAt: z2.string(),
435
+ gitHash: z2.string(),
436
+ features: z2.record(featureSchema),
437
+ flows: z2.record(flowSchema)
335
438
  }).passthrough();
336
439
  async function loadSnapshot(rootDir) {
337
440
  const parsed = await readStoreJson(rootDir, ".mason/snapshot.json");
@@ -366,18 +469,25 @@ function parseChanges(output) {
366
469
  if (!first) break;
367
470
  const second = /^[RC]/.test(code) ? fields[i++] : void 0;
368
471
  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 };
369
- if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
472
+ if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/")))
473
+ continue;
370
474
  changes.push(change);
371
475
  }
372
476
  return changes;
373
477
  }
374
478
  function touchedPaths(changes) {
375
- return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
479
+ return [
480
+ ...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))
481
+ ].sort();
376
482
  }
377
483
  async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
378
- if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
484
+ if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
485
+ return null;
379
486
  try {
380
- const { stdout } = await execGit(["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
487
+ const { stdout } = await execGit(
488
+ ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"],
489
+ { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
490
+ );
381
491
  return parseChanges(stdout);
382
492
  } catch {
383
493
  return null;
@@ -386,11 +496,23 @@ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
386
496
  async function getWorkingTree(resolvedRoot) {
387
497
  try {
388
498
  const [diff, untracked] = await Promise.all([
389
- execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
390
- execGit(["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
499
+ execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], {
500
+ cwd: resolvedRoot,
501
+ maxBuffer: 10 * 1024 * 1024
502
+ }),
503
+ execGit(["ls-files", "-z", "--others", "--exclude-standard"], {
504
+ cwd: resolvedRoot,
505
+ maxBuffer: 10 * 1024 * 1024
506
+ })
391
507
  ]);
392
508
  const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
393
- return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
509
+ return {
510
+ available: true,
511
+ changedFiles: [
512
+ .../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])
513
+ ].sort(),
514
+ untrackedFiles
515
+ };
394
516
  } catch {
395
517
  return { available: false, changedFiles: [], untrackedFiles: [] };
396
518
  }
@@ -398,10 +520,9 @@ async function getWorkingTree(resolvedRoot) {
398
520
  async function countCommitsBehind(resolvedRoot, fromHash) {
399
521
  if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-")) return null;
400
522
  try {
401
- const { stdout } = await execGit(
402
- ["rev-list", "--count", `${fromHash}..HEAD`],
403
- { cwd: resolvedRoot }
404
- );
523
+ const { stdout } = await execGit(["rev-list", "--count", `${fromHash}..HEAD`], {
524
+ cwd: resolvedRoot
525
+ });
405
526
  const count = Number.parseInt(stdout.trim(), 10);
406
527
  return Number.isNaN(count) ? null : count;
407
528
  } catch {
@@ -423,7 +544,7 @@ async function findGhostFiles(resolvedRoot, mappedFiles) {
423
544
  const ghosts = [];
424
545
  for (const file of mappedFiles) {
425
546
  try {
426
- await fs4.access(path6.join(resolvedRoot, file));
547
+ await fs4.access(path4.join(resolvedRoot, file));
427
548
  } catch {
428
549
  ghosts.push(file);
429
550
  }
@@ -431,17 +552,25 @@ async function findGhostFiles(resolvedRoot, mappedFiles) {
431
552
  return ghosts.sort();
432
553
  }
433
554
  async function computeDrift(rootDir) {
434
- const root = path6.resolve(rootDir);
555
+ const root = path4.resolve(rootDir);
435
556
  const snapshot = await loadSnapshot(root);
436
557
  if (!snapshot) return null;
437
- const [headHash, workingTree] = await Promise.all([getCurrentGitHash(root), getWorkingTree(root)]);
558
+ const [headHash, workingTree] = await Promise.all([
559
+ getCurrentGitHash(root),
560
+ getWorkingTree(root)
561
+ ]);
438
562
  const hashFor = (entry) => entry.refreshedHash ?? snapshot.gitHash;
439
563
  const entries = [...Object.values(snapshot.features), ...Object.values(snapshot.flows)];
440
564
  const hashes = /* @__PURE__ */ new Set([snapshot.gitHash, ...entries.map(hashFor)]);
441
565
  const changesByHash = /* @__PURE__ */ new Map();
442
- await Promise.all([...hashes].map(async (hash) => {
443
- changesByHash.set(hash, hash === headHash && headHash !== "unknown" ? [] : await getChangesWithStatus(root, hash));
444
- }));
566
+ await Promise.all(
567
+ [...hashes].map(async (hash) => {
568
+ changesByHash.set(
569
+ hash,
570
+ hash === headHash && headHash !== "unknown" ? [] : await getChangesWithStatus(root, hash)
571
+ );
572
+ })
573
+ );
445
574
  const historyAvailable = headHash !== "unknown" && [...changesByHash.values()].every((changes) => changes !== null);
446
575
  const mappedFiles = collectMappedFiles(snapshot);
447
576
  const report = {
@@ -467,7 +596,9 @@ async function computeDrift(rootDir) {
467
596
  failed: [...Object.entries(snapshot.features), ...Object.entries(snapshot.flows)].filter(([, e]) => e.verificationFailed).map(([name]) => name)
468
597
  }
469
598
  };
470
- const counts = await Promise.all([...hashes].map((hash) => hash === headHash ? 0 : countCommitsBehind(root, hash)));
599
+ const counts = await Promise.all(
600
+ [...hashes].map(async (hash) => hash === headHash ? 0 : countCommitsBehind(root, hash))
601
+ );
471
602
  const knownCounts = counts.filter((n) => n !== null);
472
603
  report.commitsBehind = knownCounts.length ? Math.max(...knownCounts) : null;
473
604
  const check = (name, files, hash, staleEntries, freshness) => {
@@ -478,7 +609,13 @@ async function computeDrift(rootDir) {
478
609
  freshness[name] = files.length === 0 || changes === null || changes === void 0 || !workingTree.available ? "unknown" : committedHits.length || localHits.length || files.some((f) => report.ghostFiles.includes(f)) ? "changed" : "current";
479
610
  };
480
611
  for (const [name, feature] of Object.entries(snapshot.features)) {
481
- check(name, [...feature.files, ...feature.tests ?? []], hashFor(feature), report.staleFeatures, report.featureFreshness);
612
+ check(
613
+ name,
614
+ [...feature.files, ...feature.tests ?? []],
615
+ hashFor(feature),
616
+ report.staleFeatures,
617
+ report.featureFreshness
618
+ );
482
619
  }
483
620
  for (const [name, flow] of Object.entries(snapshot.flows)) {
484
621
  check(name, flow.chain, hashFor(flow), report.staleFlows, report.flowFreshness);
@@ -488,7 +625,10 @@ async function computeDrift(rootDir) {
488
625
  const sourceFiles = new Set(await listSourceFiles(root));
489
626
  let committedFiles = /* @__PURE__ */ new Set();
490
627
  try {
491
- const { stdout } = await execGit(["ls-tree", "-r", "--name-only", "-z", "HEAD"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
628
+ const { stdout } = await execGit(["ls-tree", "-r", "--name-only", "-z", "HEAD"], {
629
+ cwd: root,
630
+ maxBuffer: 50 * 1024 * 1024
631
+ });
492
632
  committedFiles = new Set(stdout.split("\0").filter(Boolean));
493
633
  } catch {
494
634
  report.historyAvailable = false;
@@ -497,123 +637,323 @@ async function computeDrift(rootDir) {
497
637
  report.unmappedFiles = [...sourceFiles].filter((f) => committedFiles.has(f) && !mappedFiles.has(f)).sort();
498
638
  const renames = /* @__PURE__ */ new Map();
499
639
  for (const change of allChanges) {
500
- if (change.status === "renamed" && change.previousPath) renames.set(`${change.previousPath}\0${change.path}`, { from: change.previousPath, to: change.path });
640
+ if (change.status === "renamed" && change.previousPath)
641
+ renames.set(`${change.previousPath}\0${change.path}`, {
642
+ from: change.previousPath,
643
+ to: change.path
644
+ });
501
645
  }
502
646
  report.renames = [...renames.values()];
503
- const changedMapped = /* @__PURE__ */ new Set([...Object.values(report.staleFeatures).flat(), ...Object.values(report.staleFlows).flat()]);
647
+ const changedMapped = /* @__PURE__ */ new Set([
648
+ ...Object.values(report.staleFeatures).flat(),
649
+ ...Object.values(report.staleFlows).flat()
650
+ ]);
504
651
  const committedGhosts = report.ghostFiles.filter((f) => !workingTree.changedFiles.includes(f));
505
652
  report.stale ||= changedMapped.size > 0 || report.unmappedFiles.length > 0 || committedGhosts.length > 0;
506
653
  if (!report.historyAvailable) report.recommendation = "full-rebuild";
507
654
  else if (!report.stale) report.recommendation = "up-to-date";
508
- else report.recommendation = changedMapped.size >= FULL_REBUILD_MIN_CHANGED_MAPPED_FILES && changedMapped.size / Math.max(1, mappedFiles.size) > FULL_REBUILD_FRACTION ? "full-rebuild" : "incremental";
655
+ else
656
+ report.recommendation = changedMapped.size >= FULL_REBUILD_MIN_CHANGED_MAPPED_FILES && changedMapped.size / Math.max(1, mappedFiles.size) > FULL_REBUILD_FRACTION ? "full-rebuild" : "incremental";
509
657
  return report;
510
658
  }
511
659
 
660
+ // src/decisions/anchors.ts
661
+ import { createHash } from "crypto";
662
+ import fs5 from "fs/promises";
663
+ import { constants as constants2 } from "fs";
664
+ import { z as z3 } from "zod";
665
+ import fg3 from "fast-glob";
666
+
667
+ // src/audit/inspection.ts
668
+ import fg2 from "fast-glob";
669
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
670
+ import path5 from "path";
671
+ var inspections = new AsyncLocalStorage2();
672
+ function inspectionRead(root, key, read) {
673
+ const inspection = inspections.getStore();
674
+ if (!inspection || inspection.root !== path5.resolve(root)) return read();
675
+ if (!inspection.reads.has(key)) {
676
+ const pending = read();
677
+ inspection.reads.set(key, pending);
678
+ void pending.catch(() => {
679
+ if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
680
+ });
681
+ }
682
+ return inspection.reads.get(key);
683
+ }
684
+ function inspectionGit(args, options) {
685
+ return inspectionRead(
686
+ options.cwd,
687
+ "git:" + JSON.stringify([args, options]),
688
+ () => execGit(args, options)
689
+ );
690
+ }
691
+ var sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
692
+ var sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
693
+
694
+ // src/decisions/anchors.ts
695
+ var repoPath2 = z3.string().refine((p) => normalizeRepoPath(p) === p);
696
+ var anchorCaptureSchema = z3.object({
697
+ anchors: z3.array(repoPath2),
698
+ files: z3.record(repoPath2, z3.string().regex(/^[a-f0-9]{64}$/)),
699
+ complete: z3.boolean()
700
+ }).refine(
701
+ (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,
702
+ "Capture paths must belong to its normalized, sorted anchor scope"
703
+ );
704
+ async function captureAnchorScopes(root, scopes) {
705
+ const union = [...new Set(scopes.flat())].sort();
706
+ let context;
707
+ const prepareContext = async () => {
708
+ const access = await inspectionRead(
709
+ root,
710
+ "decision-anchor-access",
711
+ () => createFileAccess(root)
712
+ );
713
+ const inventory = access.inventory();
714
+ if (!inventory) throw new Error("Git anchor inventory unavailable");
715
+ const ignored = await inspectionGit(
716
+ [
717
+ "ls-files",
718
+ "-z",
719
+ "--others",
720
+ "--ignored",
721
+ "--exclude-standard",
722
+ "--",
723
+ ...union.map((anchor) => `:(literal)${anchor}`)
724
+ ],
725
+ { cwd: root, maxBuffer: 8 * 1024 * 1024 }
726
+ );
727
+ const files = [
728
+ .../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
729
+ ].sort();
730
+ const patterns = await Promise.all(
731
+ union.map(async (anchor) => {
732
+ const literal = fg3.escapePath(anchor);
733
+ try {
734
+ return (await fs5.lstat(await storePath(root, anchor))).isDirectory() ? `${literal}/**/*` : literal;
735
+ } catch {
736
+ return literal;
737
+ }
738
+ })
739
+ );
740
+ const eligible = new Set(await access.list(patterns, { dot: true }));
741
+ const indexed = indexMatchingPaths(scopes, files);
742
+ const filesByScope = new Map(
743
+ scopes.map((scope, index) => [JSON.stringify([...new Set(scope)].sort()), indexed[index]])
744
+ );
745
+ return { filesByScope, eligible };
746
+ };
747
+ return Promise.all(
748
+ scopes.map((scope) => {
749
+ const anchors = [...new Set(scope)].sort();
750
+ return inspectionRead(
751
+ root,
752
+ "decision-anchor-capture:" + JSON.stringify(anchors),
753
+ async () => {
754
+ const capture = { anchors, files: {}, complete: true };
755
+ if (!anchors.length) return capture;
756
+ try {
757
+ const { eligible, filesByScope } = await (context ??= prepareContext());
758
+ const files = filesByScope.get(JSON.stringify(anchors));
759
+ if (files.length > 2e3) return { ...capture, complete: false };
760
+ for (const file of files) {
761
+ const evidence = await inspectionRead(
762
+ root,
763
+ "decision-anchor-file:" + file,
764
+ async () => {
765
+ try {
766
+ const absolute = await storePath(root, file);
767
+ await fs5.lstat(absolute);
768
+ if (!eligible.has(file)) return { complete: false };
769
+ const handle = await fs5.open(
770
+ absolute,
771
+ constants2.O_RDONLY | constants2.O_NONBLOCK | constants2.O_NOFOLLOW
772
+ );
773
+ try {
774
+ const stat = await handle.stat();
775
+ if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
776
+ return { complete: false };
777
+ const bytes = Buffer.alloc(Math.min(stat.size + 1, MAX_SOURCE_BYTES + 1));
778
+ let size = 0;
779
+ while (size < bytes.length) {
780
+ const read = await handle.read(bytes, size, bytes.length - size, null);
781
+ if (!read.bytesRead) break;
782
+ size += read.bytesRead;
783
+ }
784
+ if (size === bytes.length) return { complete: false };
785
+ return {
786
+ complete: true,
787
+ hash: createHash("sha256").update(bytes.subarray(0, size)).digest("hex")
788
+ };
789
+ } finally {
790
+ await handle.close();
791
+ }
792
+ } catch (error) {
793
+ return { complete: error.code === "ENOENT" };
794
+ }
795
+ }
796
+ );
797
+ if (!evidence.complete) capture.complete = false;
798
+ if (evidence.hash) capture.files[file] = evidence.hash;
799
+ }
800
+ } catch {
801
+ capture.complete = false;
802
+ }
803
+ return capture;
804
+ }
805
+ );
806
+ })
807
+ );
808
+ }
809
+ function changedCaptureFiles(before, after) {
810
+ return [.../* @__PURE__ */ new Set([...Object.keys(before.files), ...Object.keys(after.files)])].filter((file) => before.files[file] !== after.files[file]).sort();
811
+ }
812
+
512
813
  // src/decisions/drift.ts
513
- import path9 from "path";
814
+ import path8 from "path";
514
815
 
515
816
  // src/decisions/decisions.ts
516
- import fs5 from "fs/promises";
517
- import path8 from "path";
518
- import { createHash } from "crypto";
519
- import { z as z3 } from "zod";
817
+ import fs6 from "fs/promises";
818
+ import path7 from "path";
819
+ import { createHash as createHash3 } from "crypto";
820
+ import { z as z5 } from "zod";
520
821
 
521
822
  // src/context/lexical.ts
522
- import path7 from "path";
823
+ import path6 from "path";
523
824
 
524
825
  // src/decisions/provenance.ts
525
- import { z as z2 } from "zod";
526
- var text = (max) => z2.string().trim().min(1).max(max);
527
- var decisionSourceSchema = z2.object({
528
- kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
826
+ import { z as z4 } from "zod";
827
+ import { createHash as createHash2 } from "crypto";
828
+ var text = (max) => z4.string().trim().min(1).max(max);
829
+ var decisionSourceSchema = z4.object({
830
+ kind: z4.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
529
831
  reference: text(1e3),
530
832
  note: text(500).optional()
531
833
  }).strict();
532
- var attributionSchema = z2.object({
834
+ var attributionSchema = z4.object({
533
835
  owner: text(200).nullable().optional(),
534
- sources: z2.array(decisionSourceSchema).max(20).optional(),
836
+ sources: z4.array(decisionSourceSchema).max(20).optional(),
535
837
  actor: text(200).optional()
536
838
  });
537
- var contentSchema = z2.object({
538
- title: z2.string().min(1),
539
- body: z2.string().min(1),
540
- category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
541
- files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
839
+ var contentSchema = z4.object({
840
+ title: z4.string().min(1),
841
+ body: z4.string().min(1),
842
+ category: z4.enum(["decision", "gotcha", "deprecation", "convention"]),
843
+ files: z4.array(z4.string().refine((f) => normalizeRepoPath(f) !== null)),
542
844
  owner: text(200).optional(),
543
- sources: z2.array(decisionSourceSchema).max(20)
845
+ sources: z4.array(decisionSourceSchema).max(20)
544
846
  });
545
- var approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
546
- var statusSchema = z2.enum(["active", "superseded", "retired"]);
547
- var reviewEvidenceSchema = z2.object({
548
- baseHash: z2.string(),
549
- headHash: z2.string(),
550
- historyAvailable: z2.boolean(),
551
- changedFiles: z2.array(z2.string()),
552
- localChanges: z2.array(z2.string())
847
+ var approvalSchema = z4.enum(["unreviewed", "proposed", "accepted"]);
848
+ var statusSchema = z4.enum(["active", "superseded", "retired"]);
849
+ var reviewEvidenceSchema = z4.object({
850
+ baseHash: z4.string(),
851
+ headHash: z4.string(),
852
+ historyAvailable: z4.boolean(),
853
+ changedFiles: z4.array(z4.string()),
854
+ localChanges: z4.array(z4.string())
553
855
  });
554
- var eventSchema = z2.object({
555
- kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
556
- at: z2.string().datetime(),
856
+ var eventSchema = z4.object({
857
+ kind: z4.enum([
858
+ "imported",
859
+ "created",
860
+ "revised",
861
+ "accepted",
862
+ "reaffirmed",
863
+ "retired",
864
+ "superseded"
865
+ ]),
866
+ at: z4.string().datetime(),
557
867
  actor: text(200).optional(),
558
868
  note: text(1500).optional(),
559
- revision: z2.number().int().positive(),
869
+ revision: z4.number().int().positive(),
560
870
  content: contentSchema,
561
871
  approval: approvalSchema,
562
872
  status: statusSchema,
563
- refreshedHash: z2.string(),
564
- evidence: reviewEvidenceSchema.optional()
873
+ refreshedHash: z4.string(),
874
+ evidence: reviewEvidenceSchema.optional(),
875
+ capture: anchorCaptureSchema.optional()
565
876
  });
566
- var legacySchema = z2.object({
567
- version: z2.literal(1),
568
- id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
569
- title: z2.string().min(1),
570
- body: z2.string().min(1),
877
+ var legacySchema = z4.object({
878
+ version: z4.literal(1),
879
+ id: z4.string().regex(/^[a-zA-Z0-9_-]+$/),
880
+ title: z4.string().min(1),
881
+ body: z4.string().min(1),
571
882
  category: contentSchema.shape.category,
572
883
  files: contentSchema.shape.files,
573
- createdAt: z2.string(),
574
- updatedAt: z2.string(),
575
- refreshedHash: z2.string(),
576
- status: z2.enum(["active", "superseded"]),
577
- supersededBy: z2.string().optional()
884
+ createdAt: z4.string(),
885
+ updatedAt: z4.string(),
886
+ refreshedHash: z4.string(),
887
+ status: z4.enum(["active", "superseded"]),
888
+ supersededBy: z4.string().optional()
578
889
  }).passthrough();
579
890
  var currentSchema = legacySchema.extend({
580
- version: z2.literal(2),
891
+ version: z4.literal(2),
581
892
  status: statusSchema,
582
893
  approval: approvalSchema,
583
- revision: z2.number().int().positive(),
894
+ revision: z4.number().int().positive(),
584
895
  owner: text(200).optional(),
585
- sources: z2.array(decisionSourceSchema).max(20),
586
- history: z2.array(eventSchema).min(1)
896
+ sources: z4.array(decisionSourceSchema).max(20),
897
+ history: z4.array(eventSchema).min(1),
898
+ capture: anchorCaptureSchema.optional(),
899
+ inspections: z4.array(
900
+ z4.object({
901
+ at: z4.string().datetime(),
902
+ inspector: text(200),
903
+ note: text(1500),
904
+ outcome: z4.literal("no_contradiction_found"),
905
+ scope: z4.literal("operative").optional(),
906
+ decisionDigest: z4.string().regex(/^[a-f0-9]{64}$/),
907
+ headHash: z4.string(),
908
+ capture: anchorCaptureSchema
909
+ })
910
+ ).max(200).optional()
587
911
  }).superRefine((record, ctx) => {
588
912
  const invalid = (message) => ctx.addIssue({ code: "custom", message });
589
913
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
590
914
  let previous;
591
915
  for (const event of record.history) {
592
916
  if (!previous) {
593
- if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
594
- if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
917
+ if (!["created", "imported"].includes(event.kind) || event.revision !== 1)
918
+ invalid("History must begin with creation or legacy import at revision 1");
919
+ if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed"))
920
+ invalid("Initial records cannot claim acceptance");
595
921
  } else {
596
922
  if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
597
923
  if (previous.status !== "active") invalid("Archived decisions cannot be changed");
598
- if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
599
- if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
600
- if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
601
- if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
924
+ if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0))
925
+ invalid("Invalid revision sequence");
926
+ if (event.kind !== "revised" && !same(event.content, previous.content))
927
+ invalid("A review cannot silently revise decision content");
928
+ if (event.kind === "reaffirmed" && previous.approval !== "accepted")
929
+ invalid("Only accepted decisions can be reaffirmed");
930
+ if (event.kind === "accepted" && previous.approval === "accepted")
931
+ invalid("Use reaffirmation for an accepted decision");
602
932
  const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
603
933
  if (event.approval !== approval) invalid("Approval disagrees with review history");
604
- if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
934
+ if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash)
935
+ invalid("Only a review can refresh the evidence baseline");
605
936
  }
606
- if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
607
- if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
937
+ if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active"))
938
+ invalid("Lifecycle disagrees with history");
939
+ if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence))
940
+ invalid("Reviews require a named reviewer, reason, and code evidence");
608
941
  if (["accepted", "reaffirmed"].includes(event.kind)) {
609
- if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
610
- 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");
942
+ if (!event.content.owner || !event.content.sources.length)
943
+ invalid("Accepted decisions require an owner and source");
944
+ if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length)
945
+ invalid("Acceptance requires a committed evidence baseline");
611
946
  }
947
+ if (event.capture && !same([...event.content.files].sort(), event.capture.anchors))
948
+ invalid("Capture scope disagrees with decision anchors");
612
949
  previous = event;
613
950
  }
614
- 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");
951
+ if (previous && !same(previous.capture, record.capture))
952
+ invalid("Capture does not match the final history event");
953
+ if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash)
954
+ invalid("Decision does not match the final history event");
615
955
  });
616
- var decisionSchema = z2.union([legacySchema, currentSchema]);
956
+ var decisionSchema = z4.union([legacySchema, currentSchema]);
617
957
  function decisionContent(record) {
618
958
  return {
619
959
  title: record.title,
@@ -624,8 +964,12 @@ function decisionContent(record) {
624
964
  sources: Array.isArray(record.sources) ? record.sources : []
625
965
  };
626
966
  }
967
+ function decisionApproval(record) {
968
+ return record.version === 1 ? "unreviewed" : record.approval;
969
+ }
627
970
  function effectiveDecision(record) {
628
- if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed") return record;
971
+ if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed")
972
+ return record;
629
973
  let index = record.history.length - 1;
630
974
  while (index >= 0 && !["accepted", "reaffirmed"].includes(record.history[index].kind)) index--;
631
975
  if (index < 0) return record;
@@ -637,57 +981,164 @@ function effectiveDecision(record) {
637
981
  approval: "accepted",
638
982
  revision: event.revision,
639
983
  refreshedHash: event.refreshedHash,
984
+ capture: event.capture,
640
985
  updatedAt: event.at,
641
986
  history: record.history.slice(0, index + 1)
642
987
  };
643
988
  }
989
+ function decisionInspectionDigest(record, scope = "all") {
990
+ const effective = effectiveDecision(record);
991
+ if (scope === "operative")
992
+ return createHash2("sha256").update(
993
+ JSON.stringify([
994
+ "operative",
995
+ record.status,
996
+ decisionApproval(effective),
997
+ effective.version === 2 ? effective.revision : 0,
998
+ decisionContent(effective),
999
+ effective.refreshedHash
1000
+ ])
1001
+ ).digest("hex");
1002
+ return createHash2("sha256").update(
1003
+ JSON.stringify([
1004
+ record.status,
1005
+ decisionApproval(record),
1006
+ record.version === 2 ? record.revision : 0,
1007
+ decisionContent(record),
1008
+ decisionContent(effective),
1009
+ effective.refreshedHash
1010
+ ])
1011
+ ).digest("hex");
1012
+ }
1013
+ function latestDecisionInspection(record, scope = "all") {
1014
+ return record.version === 2 ? record.inspections?.filter(
1015
+ (i) => (scope === "operative" || i.scope !== "operative") && i.decisionDigest === decisionInspectionDigest(record, i.scope ?? "all")
1016
+ ).at(-1) : void 0;
1017
+ }
644
1018
 
645
1019
  // src/decisions/decisions.ts
646
1020
  var TITLE_MAX_CHARS = 80;
647
1021
  var BODY_RECOMMENDED_CHARS = 1500;
648
1022
  var BODY_MAX_CHARS = 2500;
649
- var boundedText = (field, max, hint) => z3.string({
650
- 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 })
1023
+ var boundedText = (field, max, hint) => z5.string({
1024
+ errorMap: (issue, ctx) => ({
1025
+ 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
1026
+ })
651
1027
  }).trim().min(1, `${field} must be non-empty`).max(max);
652
- var decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
1028
+ var decisionTitleSchema = boundedText(
1029
+ "title",
1030
+ TITLE_MAX_CHARS,
1031
+ "Tighten it to a specific headline."
1032
+ );
653
1033
  var decisionBodySchema = boundedText(
654
1034
  "body",
655
1035
  BODY_MAX_CHARS,
656
1036
  `Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
657
1037
  );
1038
+ async function readDecisionInputs(rootDir) {
1039
+ return structuredClone(
1040
+ await inspectionRead(rootDir, "decision-store:inputs", async () => {
1041
+ const inputs = [];
1042
+ const diagnostics = [];
1043
+ let entries;
1044
+ try {
1045
+ entries = await fs6.readdir(await storePath(rootDir, ".mason/decisions"));
1046
+ } catch (error) {
1047
+ if (error.code !== "ENOENT")
1048
+ diagnostics.push({ path: ".mason/decisions", message: String(error) });
1049
+ return { inputs, diagnostics };
1050
+ }
1051
+ for (const entry of entries.sort()) {
1052
+ if (!entry.endsWith(".json")) continue;
1053
+ const relative = `.mason/decisions/${entry}`;
1054
+ try {
1055
+ const raw = await readBoundedFile(await storePath(rootDir, relative), 10 * 1024 * 1024);
1056
+ if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
1057
+ inputs.push([relative, raw]);
1058
+ } catch (error) {
1059
+ if (error.code === "ENOENT") inputs.push([relative, null]);
1060
+ else
1061
+ diagnostics.push({
1062
+ path: relative,
1063
+ message: error instanceof Error ? error.message : String(error)
1064
+ });
1065
+ }
1066
+ }
1067
+ return { inputs, diagnostics };
1068
+ })
1069
+ );
1070
+ }
658
1071
  async function loadDecisionStore(rootDir) {
659
- const records = [];
660
- const diagnostics = [];
661
- let entries;
662
- try {
663
- entries = await fs5.readdir(await storePath(rootDir, ".mason/decisions"));
664
- } catch (error) {
665
- if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
666
- return { records, diagnostics };
667
- }
668
- for (const entry of entries.sort()) {
669
- if (!entry.endsWith(".json")) continue;
670
- const relative = `.mason/decisions/${entry}`;
671
- try {
672
- const record = decisionSchema.parse(await readStoreJson(rootDir, relative));
673
- if (entry !== `${record.id}.json`) throw new Error("Record id does not match its filename");
674
- records.push(record);
675
- } catch (error) {
676
- diagnostics.push({ path: relative, message: error instanceof Error ? error.message : String(error) });
677
- }
678
- }
679
- return { records, diagnostics };
1072
+ return structuredClone(
1073
+ await inspectionRead(rootDir, "decision-store:parsed", async () => {
1074
+ const raw = await readDecisionInputs(rootDir);
1075
+ const records = [];
1076
+ const diagnostics = [...raw.diagnostics];
1077
+ for (const [relative, content] of raw.inputs) {
1078
+ try {
1079
+ const record = decisionSchema.parse(content === null ? null : JSON.parse(content));
1080
+ if (relative !== `.mason/decisions/${record.id}.json`)
1081
+ throw new Error("Record id does not match its filename");
1082
+ records.push(record);
1083
+ } catch (error) {
1084
+ diagnostics.push({
1085
+ path: relative,
1086
+ message: error instanceof Error ? error.message : String(error)
1087
+ });
1088
+ }
1089
+ }
1090
+ return { records, diagnostics };
1091
+ })
1092
+ );
680
1093
  }
681
1094
 
682
1095
  // src/decisions/drift.ts
683
1096
  async function computeDecisionDrift(rootDir, decisions) {
684
- const resolvedRoot = path9.resolve(rootDir);
1097
+ const resolvedRoot = path8.resolve(rootDir);
685
1098
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
686
- const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
687
- const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
1099
+ const report = {
1100
+ historyAvailable: true,
1101
+ totalDecisions: store.records.length,
1102
+ staleDecisions: {},
1103
+ freshness: {},
1104
+ diagnostics: store.diagnostics
1105
+ };
1106
+ const [head, workingTree] = await Promise.all([
1107
+ getCurrentGitHash(resolvedRoot),
1108
+ getWorkingTree(resolvedRoot)
1109
+ ]);
688
1110
  const changesByHash = /* @__PURE__ */ new Map();
689
- const inspect = async (record) => {
1111
+ const baselines = store.records.filter((record) => record.status === "active").flatMap((record) => {
1112
+ const effective = effectiveDecision(record);
1113
+ return [.../* @__PURE__ */ new Set([effective, record])].flatMap((candidate) => {
1114
+ const inspection = latestDecisionInspection(
1115
+ record,
1116
+ candidate === effective && decisionApproval(effective) === "accepted" ? "operative" : "all"
1117
+ );
1118
+ const capture = inspection?.capture ?? (candidate.version === 2 ? candidate.capture : void 0);
1119
+ return capture ? [capture] : [];
1120
+ });
1121
+ });
1122
+ const captures = await captureAnchorScopes(
1123
+ resolvedRoot,
1124
+ baselines.map((baseline) => baseline.anchors)
1125
+ );
1126
+ const currentCaptures = new Map(
1127
+ baselines.map((baseline, index) => [JSON.stringify(baseline.anchors), captures[index]])
1128
+ );
1129
+ const inspect = async (record, inspectionCapture) => {
690
1130
  if (record.files.length === 0) return { freshness: "unknown", changedFiles: [] };
1131
+ const baseline = inspectionCapture ?? (record.version === 2 ? record.capture : void 0);
1132
+ if (baseline) {
1133
+ const current = currentCaptures.get(JSON.stringify(baseline.anchors));
1134
+ const hits2 = changedCaptureFiles(baseline, current).filter(
1135
+ (file) => matchingPaths(record.files, [file]).length
1136
+ );
1137
+ return {
1138
+ freshness: !baseline.complete || !current.complete ? "unknown" : hits2.length ? "changed" : "current",
1139
+ changedFiles: hits2
1140
+ };
1141
+ }
691
1142
  let touched = changesByHash.get(record.refreshedHash);
692
1143
  if (touched === void 0) {
693
1144
  const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
@@ -697,15 +1148,26 @@ async function computeDecisionDrift(rootDir, decisions) {
697
1148
  if (touched === null) report.historyAvailable = false;
698
1149
  const hits = touched ? matchingPaths(record.files, touched) : [];
699
1150
  const localHits = matchingPaths(record.files, workingTree.changedFiles);
700
- return { freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current", changedFiles: hits };
1151
+ return {
1152
+ freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current",
1153
+ changedFiles: [.../* @__PURE__ */ new Set([...hits, ...localHits])].sort()
1154
+ };
701
1155
  };
702
1156
  for (const record of store.records) {
703
1157
  if (record.status !== "active") continue;
704
1158
  const effective = effectiveDecision(record);
705
- const state = await inspect(effective);
1159
+ const inspection = latestDecisionInspection(
1160
+ record,
1161
+ decisionApproval(effective) === "accepted" ? "operative" : "all"
1162
+ );
1163
+ const state = await inspect(effective, inspection?.capture);
706
1164
  report.freshness[record.id] = state.freshness;
707
1165
  if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
708
- if (effective !== record) (report.pendingProposals ??= {})[record.id] = await inspect(record);
1166
+ if (effective !== record)
1167
+ (report.pendingProposals ??= {})[record.id] = await inspect(
1168
+ record,
1169
+ latestDecisionInspection(record)?.capture
1170
+ );
709
1171
  }
710
1172
  return report;
711
1173
  }
@@ -758,9 +1220,17 @@ function parseArgs(argv) {
758
1220
  }
759
1221
  function formatDriftSummary(report) {
760
1222
  if (!report.stale) {
761
- const lines2 = [`Concept map is up to date against committed source (HEAD ${report.headHash.slice(0, 7)}).`];
762
- if (report.workingTree?.changedFiles.length) lines2.push(`Working tree has ${report.workingTree.changedFiles.length} changed files; live context checks report these separately.`);
763
- if (report.verification?.failed.length) lines2.push(`Verification FAILED: ${report.verification.failed.join(", ")}. Correct these entries before relying on them.`);
1223
+ const lines2 = [
1224
+ `Concept map is up to date against committed source (HEAD ${report.headHash.slice(0, 7)}).`
1225
+ ];
1226
+ if (report.workingTree?.changedFiles.length)
1227
+ lines2.push(
1228
+ `Working tree has ${report.workingTree.changedFiles.length} changed files; live context checks report these separately.`
1229
+ );
1230
+ if (report.verification?.failed.length)
1231
+ lines2.push(
1232
+ `Verification FAILED: ${report.verification.failed.join(", ")}. Correct these entries before relying on them.`
1233
+ );
764
1234
  return lines2.join("\n");
765
1235
  }
766
1236
  const lines = [];
@@ -779,9 +1249,7 @@ function formatDriftSummary(report) {
779
1249
  );
780
1250
  }
781
1251
  if (staleFlows.length > 0) {
782
- lines.push(
783
- `Stale flows (${staleFlows.length}/${report.totalFlows}): ${staleFlows.join(", ")}`
784
- );
1252
+ lines.push(`Stale flows (${staleFlows.length}/${report.totalFlows}): ${staleFlows.join(", ")}`);
785
1253
  }
786
1254
  if (report.unmappedFiles.length > 0) {
787
1255
  lines.push(
@@ -820,18 +1288,14 @@ function formatRefreshPrompt(report, decisionDrift) {
820
1288
  )
821
1289
  );
822
1290
  lines.push("");
823
- const scopedFiles = [...report.changedFiles, ...report.unmappedFiles];
824
- if (!report.historyAvailable || report.recommendation === "full-rebuild") {
825
- lines.push(
826
- "PROCEDURE (full rebuild): run the complete Map-Reduce build. Call generate_snapshot_batch repeatedly (follow nextOffset until null), calling save_partial_snapshot after each batch, then reduce_snapshot, then save_snapshot once with the unified map. Derive features ONLY from files shown in each batch prompt \u2014 never invent paths."
827
- );
828
- } else {
1291
+ lines.push(
1292
+ "PROCEDURE (scoped refresh): inspect the changed and unmapped files with your own source tools. Use get_snapshot to read the existing map, then save_snapshot with only evidence-supported corrected features/flows. Remove deleted entries with removeFeatures/removeFlows. Managed batch creation and reduction have been removed."
1293
+ );
1294
+ if (!report.historyAvailable || report.recommendation === "full-rebuild")
829
1295
  lines.push(
830
- "PROCEDURE (scoped refresh): call generate_snapshot_batch with the files list below \u2014 the SAME list on every call \u2014 following nextOffset until null, calling save_partial_snapshot after each batch. Then call reduce_snapshot (it merges into the existing map, preserving untouched entries) and save_snapshot once. Use save_snapshot's removeFeatures/removeFlows for features that no longer exist (see ghostFiles/renames). Derive features ONLY from files shown in each batch prompt \u2014 never invent paths."
1296
+ "History or coverage is insufficient for a bounded refresh. Inspect all affected entries directly; do not claim freshness for entries you have not checked."
831
1297
  );
832
- lines.push("");
833
- lines.push(`files: ${JSON.stringify(scopedFiles)}`);
834
- }
1298
+ lines.push(`files: ${JSON.stringify([...report.changedFiles, ...report.unmappedFiles])}`);
835
1299
  const staleDecisionIds = Object.keys(decisionDrift.staleDecisions);
836
1300
  if (staleDecisionIds.length > 0) {
837
1301
  lines.push("");
@@ -840,11 +1304,12 @@ function formatRefreshPrompt(report, decisionDrift) {
840
1304
  );
841
1305
  }
842
1306
  const changedProposals = Object.entries(decisionDrift.pendingProposals ?? {}).filter(([, state]) => state.freshness !== "current").map(([id]) => id);
843
- if (changedProposals.length) lines.push(`NOTE: pending proposals [${changedProposals.join(", ")}] need source review. Their accepted revisions remain operative. Do NOT modify or accept these proposals during automated map refresh.`);
1307
+ if (changedProposals.length)
1308
+ lines.push(
1309
+ `NOTE: pending proposals [${changedProposals.join(", ")}] need source review. Their accepted revisions remain operative. Do NOT modify or accept these proposals during automated map refresh.`
1310
+ );
844
1311
  lines.push("");
845
- lines.push(
846
- "Finish by confirming the map was saved and summarizing which entries changed."
847
- );
1312
+ lines.push("Finish by confirming the map was saved and summarizing which entries changed.");
848
1313
  return lines.join("\n");
849
1314
  }
850
1315
  async function runDriftCli(argv, io = {
@@ -868,7 +1333,7 @@ async function runDriftCli(argv, io = {
868
1333
  io.out(USAGE);
869
1334
  return 0;
870
1335
  }
871
- const rootDir = path10.resolve(args.dir);
1336
+ const rootDir = path9.resolve(args.dir);
872
1337
  let report;
873
1338
  try {
874
1339
  report = await computeDrift(rootDir);
@@ -878,7 +1343,7 @@ async function runDriftCli(argv, io = {
878
1343
  }
879
1344
  if (!report) {
880
1345
  io.err(
881
- `No Mason snapshot found at ${path10.join(rootDir, ".mason", "snapshot.json")}. Build one via the Mason MCP server first.`
1346
+ `No Mason snapshot found at ${path9.join(rootDir, ".mason", "snapshot.json")}. Managed map creation has been removed; decisions and context work without a map.`
882
1347
  );
883
1348
  return 2;
884
1349
  }
@@ -890,9 +1355,7 @@ async function runDriftCli(argv, io = {
890
1355
  }
891
1356
  const decisionDrift = await computeDecisionDrift(rootDir);
892
1357
  if (args.refreshPrompt) {
893
- io.out(
894
- report.stale ? formatRefreshPrompt(report, decisionDrift) : formatDriftSummary(report)
895
- );
1358
+ io.out(report.stale ? formatRefreshPrompt(report, decisionDrift) : formatDriftSummary(report));
896
1359
  return report.stale ? 1 : 0;
897
1360
  }
898
1361
  if (args.json) {
@@ -903,9 +1366,13 @@ async function runDriftCli(argv, io = {
903
1366
  io.out(JSON.stringify(output, null, 2));
904
1367
  } else {
905
1368
  const lines = [formatDriftSummary(report)];
906
- for (const diagnostic of decisionDrift.diagnostics ?? []) lines.push(`Invalid decision record ${diagnostic.path}: ${diagnostic.message}`);
1369
+ for (const diagnostic of decisionDrift.diagnostics ?? [])
1370
+ lines.push(`Invalid decision record ${diagnostic.path}: ${diagnostic.message}`);
907
1371
  const unknownIds = Object.entries(decisionDrift.freshness ?? {}).filter(([, state]) => state === "unknown").map(([id]) => id);
908
- if (unknownIds.length) lines.push(`Decision freshness unknown: ${unknownIds.join(", ")}. Verify before relying on them.`);
1372
+ if (unknownIds.length)
1373
+ lines.push(
1374
+ `Decision freshness unknown: ${unknownIds.join(", ")}. Verify before relying on them.`
1375
+ );
909
1376
  const staleIds = Object.keys(decisionDrift.staleDecisions);
910
1377
  if (staleIds.length > 0) {
911
1378
  lines.push(
@@ -913,7 +1380,9 @@ async function runDriftCli(argv, io = {
913
1380
  );
914
1381
  }
915
1382
  for (const [id, proposal] of Object.entries(decisionDrift.pendingProposals ?? {})) {
916
- lines.push(`Pending proposal ${id}: freshness ${proposal.freshness}; the accepted revision remains operative.`);
1383
+ lines.push(
1384
+ `Pending proposal ${id}: freshness ${proposal.freshness}; the accepted revision remains operative.`
1385
+ );
917
1386
  }
918
1387
  io.out(lines.join("\n"));
919
1388
  }