mason-context 0.20.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,40 +1,56 @@
1
1
  #!/usr/bin/env node
2
2
  if (!import.meta.url.endsWith("/mason-runtime.js")) await import("./mason-runtime.js").then(m => m.registerRuntime());
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __esm = (fn, res) => function __init() {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ };
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
3
12
 
4
- // src/hook/hook.ts
5
- import fs6 from "fs/promises";
6
- import path10 from "path";
7
- import os2 from "os";
8
-
9
- // src/decisions/decisions.ts
10
- import fs4 from "fs/promises";
11
- import path7 from "path";
12
- import { createHash } from "crypto";
13
- import { z as z3 } from "zod";
13
+ // src/audit/types.ts
14
+ var types_exports = {};
15
+ __export(types_exports, {
16
+ ALL_CHECKS: () => ALL_CHECKS
17
+ });
18
+ var ALL_CHECKS;
19
+ var init_types = __esm({
20
+ "src/audit/types.ts"() {
21
+ "use strict";
22
+ ALL_CHECKS = [
23
+ "deleted-reference",
24
+ "new-module",
25
+ "stale-count",
26
+ "dead-command",
27
+ "deps-changed",
28
+ "decision-anchor-drift"
29
+ ];
30
+ }
31
+ });
14
32
 
15
- // src/utils/storage.ts
16
- import fs2 from "fs/promises";
17
- import path3 from "path";
18
- import { randomUUID } from "crypto";
33
+ // src/hook/hook.ts
34
+ import { z as z6 } from "zod";
19
35
 
20
- // src/utils/paths.ts
21
- import path from "path";
22
- function normalizeRepoPath(value) {
23
- const slash = value.replace(/\\/g, "/");
24
- if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
25
- if (slash.split("/").includes("..")) return null;
26
- const normalized = path.posix.normalize(slash).replace(/\/$/, "");
27
- return normalized === "." ? null : normalized;
28
- }
29
- function anchorMatches(anchor, file) {
30
- const a = normalizeRepoPath(anchor);
31
- const f = normalizeRepoPath(file);
32
- return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
36
+ // src/utils/validation.ts
37
+ function isRecord(value) {
38
+ return value !== null && typeof value === "object" && !Array.isArray(value);
33
39
  }
34
- function matchingPaths(anchors, files) {
35
- return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
40
+ function isStringArray(value) {
41
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
36
42
  }
37
43
 
44
+ // src/hook/hook.ts
45
+ import fs7 from "fs/promises";
46
+ import path11 from "path";
47
+ import os2 from "os";
48
+
49
+ // src/audit/inspection.ts
50
+ import fg2 from "fast-glob";
51
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
52
+ import path3 from "path";
53
+
38
54
  // src/utils/git-read.ts
39
55
  import { execFile } from "child_process";
40
56
  import { promisify } from "util";
@@ -57,7 +73,19 @@ async function profilePhase(label, run) {
57
73
 
58
74
  // src/utils/git-read.ts
59
75
  var execute = promisify(execFile);
60
- var commands = /* @__PURE__ */ new Set(["rev-parse", "symbolic-ref", "for-each-ref", "ls-files", "check-ignore", "status", "log", "diff", "ls-tree", "show", "merge-base"]);
76
+ var commands = /* @__PURE__ */ new Set([
77
+ "rev-parse",
78
+ "symbolic-ref",
79
+ "for-each-ref",
80
+ "ls-files",
81
+ "check-ignore",
82
+ "status",
83
+ "log",
84
+ "diff",
85
+ "ls-tree",
86
+ "show",
87
+ "merge-base"
88
+ ]);
61
89
  function execGit(args, options) {
62
90
  const command = args.find((arg) => !arg.startsWith("-")) ?? "other";
63
91
  return profilePhase("git." + (commands.has(command) ? command : "other"), async () => {
@@ -77,11 +105,127 @@ import fs from "fs/promises";
77
105
  import { constants } from "fs";
78
106
  import path2 from "path";
79
107
  import fg from "fast-glob";
80
- 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"];
108
+
109
+ // src/utils/paths.ts
110
+ import path from "path";
111
+ function normalizeRepoPath(value) {
112
+ const slash = value.replace(/\\/g, "/");
113
+ if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash))
114
+ return null;
115
+ if (slash.split("/").includes("..")) return null;
116
+ const normalized = path.posix.normalize(slash).replace(/\/$/, "");
117
+ return normalized === "." ? null : normalized;
118
+ }
119
+ function isWithinRoot(root, candidate) {
120
+ const relative = path.relative(root, candidate);
121
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
122
+ }
123
+ function anchorMatches(anchor, file) {
124
+ const a = normalizeRepoPath(anchor);
125
+ const f = normalizeRepoPath(file);
126
+ return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
127
+ }
128
+ function matchingPaths(anchors, files) {
129
+ return [...new Set(files)].filter(
130
+ (file) => anchors.some((anchor) => anchorMatches(anchor, file))
131
+ );
132
+ }
133
+ function indexMatchingPaths(scopes, files) {
134
+ const node = () => ({ children: /* @__PURE__ */ new Map(), scopes: /* @__PURE__ */ new Set() });
135
+ const root = node();
136
+ const matches = scopes.map(() => []);
137
+ scopes.forEach((anchors, index) => {
138
+ for (const anchor of anchors) {
139
+ const normalized = normalizeRepoPath(anchor);
140
+ if (!normalized) continue;
141
+ let current = root;
142
+ for (const part of normalized.split("/")) {
143
+ if (!current.children.has(part)) current.children.set(part, node());
144
+ current = current.children.get(part);
145
+ }
146
+ current.scopes.add(index);
147
+ }
148
+ });
149
+ for (const file of new Set(files)) {
150
+ const normalized = normalizeRepoPath(file);
151
+ if (!normalized) continue;
152
+ const selected = /* @__PURE__ */ new Set();
153
+ let current = root;
154
+ for (const part of normalized.split("/")) {
155
+ current = current.children.get(part);
156
+ if (!current) break;
157
+ for (const index of current.scopes) selected.add(index);
158
+ }
159
+ for (const index of selected) matches[index].push(file);
160
+ }
161
+ return matches;
162
+ }
163
+
164
+ // src/utils/files.ts
165
+ var SOURCE_EXTENSIONS = [
166
+ "ts",
167
+ "tsx",
168
+ "js",
169
+ "jsx",
170
+ "mts",
171
+ "cts",
172
+ "mjs",
173
+ "cjs",
174
+ "vue",
175
+ "svelte",
176
+ "kt",
177
+ "kts",
178
+ "java",
179
+ "py",
180
+ "go",
181
+ "rs",
182
+ "swift",
183
+ "rb",
184
+ "cs",
185
+ "cpp",
186
+ "c",
187
+ "h",
188
+ "hpp",
189
+ "dart",
190
+ "php"
191
+ ];
81
192
  var SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
193
+ var SOURCE_IGNORE = [
194
+ "**/node_modules/**",
195
+ "**/dist/**",
196
+ "**/build/**",
197
+ "**/.gradle/**",
198
+ "**/target/**",
199
+ "**/.git/**",
200
+ "**/.mason/**",
201
+ "**/vendor/**",
202
+ "**/__pycache__/**",
203
+ "**/venv/**",
204
+ "**/.venv/**",
205
+ "**/*.min.*",
206
+ "**/*.map",
207
+ "**/*.lock",
208
+ "**/generated/**",
209
+ "**/*.generated.*",
210
+ "**/R.java",
211
+ "**/BuildConfig.java",
212
+ "**/package-lock.json",
213
+ "**/yarn.lock",
214
+ "**/pnpm-lock.yaml"
215
+ ];
82
216
  var MAX_SOURCE_BYTES = 1024 * 1024;
217
+ function isSensitiveFile(file) {
218
+ return file.split(/[\\/]/).some(
219
+ (part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(
220
+ part
221
+ )
222
+ );
223
+ }
83
224
  async function readBoundedFile(file, maxBytes) {
84
- const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
225
+ const handle = await fs.open(
226
+ file,
227
+ constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW
228
+ );
85
229
  try {
86
230
  const stat = await handle.stat();
87
231
  if (!stat.isFile() || stat.size > maxBytes) return null;
@@ -97,15 +241,168 @@ async function readBoundedFile(file, maxBytes) {
97
241
  await handle.close();
98
242
  }
99
243
  }
244
+ async function loadProjectConfig(root) {
245
+ try {
246
+ const canonicalRoot = await fs.realpath(root);
247
+ const configPath = await fs.realpath(path2.join(root, ".mason/config.json"));
248
+ if (!isWithinRoot(canonicalRoot, configPath))
249
+ throw new Error("Project configuration resolves outside the repository");
250
+ const raw = await readBoundedFile(configPath, 64 * 1024);
251
+ if (raw === null)
252
+ throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
253
+ const value = JSON.parse(raw);
254
+ if (!isRecord(value)) throw new Error("Expected a configuration object");
255
+ const config = {};
256
+ for (const key of ["patterns", "alwaysInclude", "ignore"]) {
257
+ if (value[key] === void 0) continue;
258
+ if (!isStringArray(value[key])) {
259
+ throw new Error(`Configuration ${key} must be an array of strings`);
260
+ }
261
+ config[key] = value[key];
262
+ }
263
+ if (value.audit !== void 0) {
264
+ const audit = value.audit;
265
+ if (!isRecord(audit)) throw new Error("Configuration audit must be an object");
266
+ const patterns = (raw2) => {
267
+ if (!isStringArray(raw2) || raw2.length > 100 || !raw2.every(
268
+ (s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s)
269
+ )) {
270
+ throw new Error(
271
+ "Audit patterns must be at most 100 repository-relative globs without traversal or negation"
272
+ );
273
+ }
274
+ return raw2;
275
+ };
276
+ config.audit = {};
277
+ if (audit.include !== void 0) config.audit.include = patterns(audit.include);
278
+ if (audit.exclude !== void 0) {
279
+ if (!isRecord(audit.exclude))
280
+ throw new Error("audit.exclude must map check names to patterns");
281
+ const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
282
+ config.audit.exclude = {};
283
+ for (const [check, globs] of Object.entries(audit.exclude)) {
284
+ if (!ALL_CHECKS2.includes(check))
285
+ throw new Error("Unknown audit check: " + check);
286
+ config.audit.exclude[check] = patterns(globs);
287
+ }
288
+ }
289
+ }
290
+ return config;
291
+ } catch (error) {
292
+ if (error.code === "ENOENT") return {};
293
+ throw new Error(
294
+ `Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`,
295
+ { cause: error }
296
+ );
297
+ }
298
+ }
299
+ async function createFileAccess(rootDir) {
300
+ const root = path2.resolve(rootDir);
301
+ const canonicalRoot = await fs.realpath(root).catch(() => root);
302
+ const config = await loadProjectConfig(root);
303
+ const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
304
+ let gitFiles = null;
305
+ try {
306
+ const { stdout } = await execGit(
307
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
308
+ { cwd: root, maxBuffer: 50 * 1024 * 1024 }
309
+ );
310
+ gitFiles = new Set(stdout.split("\0").filter(Boolean));
311
+ } catch {
312
+ let inGit = false;
313
+ try {
314
+ await execGit(["rev-parse", "--git-dir"], { cwd: root });
315
+ inGit = true;
316
+ } catch {
317
+ }
318
+ if (inGit) throw new Error("Cannot enumerate Git files safely");
319
+ }
320
+ async function resolve(file) {
321
+ const relative = normalizeRepoPath(file);
322
+ if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative))
323
+ return null;
324
+ const candidate = path2.join(root, relative);
325
+ try {
326
+ const real = await fs.realpath(candidate);
327
+ if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path2.relative(canonicalRoot, real)))
328
+ return null;
329
+ const stat = await fs.stat(real);
330
+ if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
331
+ if (gitFiles && !gitFiles.has(path2.relative(canonicalRoot, real).split(path2.sep).join("/")))
332
+ return null;
333
+ return real;
334
+ } catch {
335
+ return null;
336
+ }
337
+ }
338
+ async function list(patterns = SOURCE_GLOB, options = {}) {
339
+ const found = await fg(patterns, { cwd: root, ignore, followSymbolicLinks: false, ...options });
340
+ const safe = await Promise.all(found.map(async (f) => await resolve(f) ? f : null));
341
+ return safe.filter((f) => f !== null).sort();
342
+ }
343
+ async function read(file) {
344
+ const relative = normalizeRepoPath(file);
345
+ if (!relative) return null;
346
+ const real = await resolve(relative);
347
+ if (!real) return null;
348
+ for (const rel of /* @__PURE__ */ new Set([
349
+ relative,
350
+ path2.relative(canonicalRoot, real).split(path2.sep).join("/")
351
+ ])) {
352
+ if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
353
+ }
354
+ try {
355
+ const content = await readBoundedFile(real, MAX_SOURCE_BYTES);
356
+ return content === null ? null : { path: relative, content, totalLines: content.split("\n").length };
357
+ } catch {
358
+ return null;
359
+ }
360
+ }
361
+ return { root, config, list, read, inventory: () => gitFiles ? [...gitFiles].sort() : null };
362
+ }
363
+
364
+ // src/audit/inspection.ts
365
+ var inspections = new AsyncLocalStorage2();
366
+ function inspectionRead(root, key, read) {
367
+ const inspection = inspections.getStore();
368
+ if (!inspection || inspection.root !== path3.resolve(root)) return read();
369
+ if (!inspection.reads.has(key)) {
370
+ const pending = read();
371
+ inspection.reads.set(key, pending);
372
+ void pending.catch(() => {
373
+ if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
374
+ });
375
+ }
376
+ return inspection.reads.get(key);
377
+ }
378
+ function inspectionGit(args, options) {
379
+ return inspectionRead(
380
+ options.cwd,
381
+ "git:" + JSON.stringify([args, options]),
382
+ () => execGit(args, options)
383
+ );
384
+ }
385
+ var sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
386
+ var sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
387
+
388
+ // src/decisions/anchors.ts
389
+ import { createHash } from "crypto";
390
+ import fs3 from "fs/promises";
391
+ import { constants as constants2 } from "fs";
392
+ import { z } from "zod";
393
+ import fg3 from "fast-glob";
100
394
 
101
395
  // src/utils/storage.ts
396
+ import fs2 from "fs/promises";
397
+ import path4 from "path";
398
+ import { randomUUID } from "crypto";
102
399
  async function storePath(root, relative, createParents = false) {
103
400
  const normalized = normalizeRepoPath(relative);
104
401
  if (!normalized) throw new Error(`Invalid store path: ${relative}`);
105
402
  let current = await fs2.realpath(root);
106
403
  const parts = normalized.split("/");
107
404
  for (let i = 0; i < parts.length; i++) {
108
- current = path3.join(current, parts[i]);
405
+ current = path4.join(current, parts[i]);
109
406
  let stat;
110
407
  try {
111
408
  stat = await fs2.lstat(current);
@@ -124,56 +421,170 @@ async function storePath(root, relative, createParents = false) {
124
421
  }
125
422
  return current;
126
423
  }
127
- async function readStoreJson(root, relative) {
128
- try {
129
- const file = await storePath(root, relative);
130
- const raw = await readBoundedFile(file, 10 * 1024 * 1024);
131
- if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
132
- const parsed = JSON.parse(raw);
133
- if (parsed === null) throw new Error("expected a JSON object, received null");
134
- return parsed;
135
- } catch (error) {
136
- if (error.code === "ENOENT") return null;
137
- throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
138
- }
424
+
425
+ // src/decisions/anchors.ts
426
+ var repoPath = z.string().refine((p) => normalizeRepoPath(p) === p);
427
+ var anchorCaptureSchema = z.object({
428
+ anchors: z.array(repoPath),
429
+ files: z.record(repoPath, z.string().regex(/^[a-f0-9]{64}$/)),
430
+ complete: z.boolean()
431
+ }).refine(
432
+ (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,
433
+ "Capture paths must belong to its normalized, sorted anchor scope"
434
+ );
435
+ async function captureAnchorScopes(root, scopes) {
436
+ const union = [...new Set(scopes.flat())].sort();
437
+ let context;
438
+ const prepareContext = async () => {
439
+ const access = await inspectionRead(
440
+ root,
441
+ "decision-anchor-access",
442
+ () => createFileAccess(root)
443
+ );
444
+ const inventory = access.inventory();
445
+ if (!inventory) throw new Error("Git anchor inventory unavailable");
446
+ const ignored = await inspectionGit(
447
+ [
448
+ "ls-files",
449
+ "-z",
450
+ "--others",
451
+ "--ignored",
452
+ "--exclude-standard",
453
+ "--",
454
+ ...union.map((anchor) => `:(literal)${anchor}`)
455
+ ],
456
+ { cwd: root, maxBuffer: 8 * 1024 * 1024 }
457
+ );
458
+ const files = [
459
+ .../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
460
+ ].sort();
461
+ const patterns = await Promise.all(
462
+ union.map(async (anchor) => {
463
+ const literal = fg3.escapePath(anchor);
464
+ try {
465
+ return (await fs3.lstat(await storePath(root, anchor))).isDirectory() ? `${literal}/**/*` : literal;
466
+ } catch {
467
+ return literal;
468
+ }
469
+ })
470
+ );
471
+ const eligible = new Set(await access.list(patterns, { dot: true }));
472
+ const indexed = indexMatchingPaths(scopes, files);
473
+ const filesByScope = new Map(
474
+ scopes.map((scope, index) => [JSON.stringify([...new Set(scope)].sort()), indexed[index]])
475
+ );
476
+ return { filesByScope, eligible };
477
+ };
478
+ return Promise.all(
479
+ scopes.map((scope) => {
480
+ const anchors = [...new Set(scope)].sort();
481
+ return inspectionRead(
482
+ root,
483
+ "decision-anchor-capture:" + JSON.stringify(anchors),
484
+ async () => {
485
+ const capture = { anchors, files: {}, complete: true };
486
+ if (!anchors.length) return capture;
487
+ try {
488
+ const { eligible, filesByScope } = await (context ??= prepareContext());
489
+ const files = filesByScope.get(JSON.stringify(anchors));
490
+ if (files.length > 2e3) return { ...capture, complete: false };
491
+ for (const file of files) {
492
+ const evidence = await inspectionRead(
493
+ root,
494
+ "decision-anchor-file:" + file,
495
+ async () => {
496
+ try {
497
+ const absolute = await storePath(root, file);
498
+ await fs3.lstat(absolute);
499
+ if (!eligible.has(file)) return { complete: false };
500
+ const handle = await fs3.open(
501
+ absolute,
502
+ constants2.O_RDONLY | constants2.O_NONBLOCK | constants2.O_NOFOLLOW
503
+ );
504
+ try {
505
+ const stat = await handle.stat();
506
+ if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
507
+ return { complete: false };
508
+ const bytes = Buffer.alloc(Math.min(stat.size + 1, MAX_SOURCE_BYTES + 1));
509
+ let size = 0;
510
+ while (size < bytes.length) {
511
+ const read = await handle.read(bytes, size, bytes.length - size, null);
512
+ if (!read.bytesRead) break;
513
+ size += read.bytesRead;
514
+ }
515
+ if (size === bytes.length) return { complete: false };
516
+ return {
517
+ complete: true,
518
+ hash: createHash("sha256").update(bytes.subarray(0, size)).digest("hex")
519
+ };
520
+ } finally {
521
+ await handle.close();
522
+ }
523
+ } catch (error) {
524
+ return { complete: error.code === "ENOENT" };
525
+ }
526
+ }
527
+ );
528
+ if (!evidence.complete) capture.complete = false;
529
+ if (evidence.hash) capture.files[file] = evidence.hash;
530
+ }
531
+ } catch {
532
+ capture.complete = false;
533
+ }
534
+ return capture;
535
+ }
536
+ );
537
+ })
538
+ );
139
539
  }
540
+ function changedCaptureFiles(before, after) {
541
+ return [.../* @__PURE__ */ new Set([...Object.keys(before.files), ...Object.keys(after.files)])].filter((file) => before.files[file] !== after.files[file]).sort();
542
+ }
543
+
544
+ // src/decisions/decisions.ts
545
+ import fs5 from "fs/promises";
546
+ import path8 from "path";
547
+ import { createHash as createHash3 } from "crypto";
548
+ import { z as z5 } from "zod";
140
549
 
141
550
  // src/snapshot/snapshot.ts
142
- import path5 from "path";
143
- import { z } from "zod";
551
+ import path6 from "path";
552
+ import { z as z3 } from "zod";
144
553
 
145
554
  // src/utils/store-lock.ts
146
- import fs3 from "fs/promises";
555
+ import { z as z2 } from "zod";
556
+ import fs4 from "fs/promises";
147
557
  import os from "os";
558
+ var lockOwnerSchema = z2.object({ host: z2.string(), pid: z2.number().int().positive() });
148
559
 
149
560
  // src/test-map.ts
150
- import path4 from "path";
561
+ import path5 from "path";
151
562
 
152
563
  // src/snapshot/snapshot.ts
153
- var repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
564
+ var repoPath2 = z3.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
154
565
  var verificationFields = {
155
- refreshedHash: z.string().optional(),
156
- verifiedAt: z.string().optional(),
157
- verifiedHash: z.string().optional(),
158
- verificationToken: z.string().optional(),
159
- verificationFailed: z.boolean().optional(),
160
- verificationNote: z.string().optional()
566
+ refreshedHash: z3.string().optional(),
567
+ verifiedAt: z3.string().optional(),
568
+ verifiedHash: z3.string().optional(),
569
+ verificationToken: z3.string().optional(),
570
+ verificationFailed: z3.boolean().optional(),
571
+ verificationNote: z3.string().optional()
161
572
  };
162
- var featureSchema = z.object({
163
- description: z.string(),
164
- files: z.array(repoPath),
165
- tests: z.array(repoPath).optional(),
166
- type: z.enum(["capability", "infrastructure"]).optional(),
573
+ var featureSchema = z3.object({
574
+ description: z3.string(),
575
+ files: z3.array(repoPath2),
576
+ tests: z3.array(repoPath2).optional(),
577
+ type: z3.enum(["capability", "infrastructure"]).optional(),
167
578
  ...verificationFields
168
579
  }).passthrough();
169
- var flowSchema = z.object({ description: z.string(), chain: z.array(repoPath), ...verificationFields }).passthrough();
170
- var snapshotSchema = z.object({
171
- version: z.literal(2),
172
- createdAt: z.string(),
173
- updatedAt: z.string(),
174
- gitHash: z.string(),
175
- features: z.record(featureSchema),
176
- flows: z.record(flowSchema)
580
+ var flowSchema = z3.object({ description: z3.string(), chain: z3.array(repoPath2), ...verificationFields }).passthrough();
581
+ var snapshotSchema = z3.object({
582
+ version: z3.literal(2),
583
+ createdAt: z3.string(),
584
+ updatedAt: z3.string(),
585
+ gitHash: z3.string(),
586
+ features: z3.record(featureSchema),
587
+ flows: z3.record(flowSchema)
177
588
  }).passthrough();
178
589
  async function getCurrentGitHash(rootDir) {
179
590
  try {
@@ -187,114 +598,167 @@ async function getCurrentGitHash(rootDir) {
187
598
  }
188
599
 
189
600
  // src/context/lexical.ts
190
- import path6 from "path";
601
+ import path7 from "path";
191
602
 
192
603
  // src/decisions/provenance.ts
193
- import { z as z2 } from "zod";
604
+ import { z as z4 } from "zod";
605
+ import { createHash as createHash2 } from "crypto";
194
606
 
195
607
  // src/context/trust.ts
196
608
  function assessTrust(entry, freshness) {
197
609
  const verification = entry.verificationFailed ? "failed" : entry.verifiedAt ? "passed" : "unverified";
198
610
  const reasons = [];
199
- if (freshness === "unknown") reasons.push("Anchors, history, or working-tree evidence are unavailable; verify before relying on this entry.");
200
- if (freshness === "changed") reasons.push("Anchored files changed; verify against current code before relying on this entry.");
201
- if (verification === "failed") reasons.push(`Verification failed: ${entry.verificationNote ?? "re-map this entry before relying on it"}`);
611
+ if (freshness === "unknown")
612
+ reasons.push(
613
+ "Anchors, history, or working-tree evidence are unavailable; verify before relying on this entry."
614
+ );
615
+ if (freshness === "changed")
616
+ reasons.push(
617
+ "Anchored files changed; verify against current code before relying on this entry."
618
+ );
619
+ if (verification === "failed")
620
+ reasons.push(
621
+ `Verification failed: ${entry.verificationNote ?? "re-map this entry before relying on it"}`
622
+ );
202
623
  if (verification === "unverified") reasons.push("No correctness verification has been recorded.");
203
- return { freshness, verification, verifiedAt: entry.verifiedAt, verifiedHash: entry.verifiedHash, reasons };
624
+ return {
625
+ freshness,
626
+ verification,
627
+ verifiedAt: entry.verifiedAt,
628
+ verifiedHash: entry.verifiedHash,
629
+ reasons
630
+ };
204
631
  }
205
632
 
206
633
  // src/decisions/provenance.ts
207
- var text = (max) => z2.string().trim().min(1).max(max);
208
- var decisionSourceSchema = z2.object({
209
- kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
634
+ var text = (max) => z4.string().trim().min(1).max(max);
635
+ var decisionSourceSchema = z4.object({
636
+ kind: z4.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
210
637
  reference: text(1e3),
211
638
  note: text(500).optional()
212
639
  }).strict();
213
- var attributionSchema = z2.object({
640
+ var attributionSchema = z4.object({
214
641
  owner: text(200).nullable().optional(),
215
- sources: z2.array(decisionSourceSchema).max(20).optional(),
642
+ sources: z4.array(decisionSourceSchema).max(20).optional(),
216
643
  actor: text(200).optional()
217
644
  });
218
- var contentSchema = z2.object({
219
- title: z2.string().min(1),
220
- body: z2.string().min(1),
221
- category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
222
- files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
645
+ var contentSchema = z4.object({
646
+ title: z4.string().min(1),
647
+ body: z4.string().min(1),
648
+ category: z4.enum(["decision", "gotcha", "deprecation", "convention"]),
649
+ files: z4.array(z4.string().refine((f) => normalizeRepoPath(f) !== null)),
223
650
  owner: text(200).optional(),
224
- sources: z2.array(decisionSourceSchema).max(20)
651
+ sources: z4.array(decisionSourceSchema).max(20)
225
652
  });
226
- var approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
227
- var statusSchema = z2.enum(["active", "superseded", "retired"]);
228
- var reviewEvidenceSchema = z2.object({
229
- baseHash: z2.string(),
230
- headHash: z2.string(),
231
- historyAvailable: z2.boolean(),
232
- changedFiles: z2.array(z2.string()),
233
- localChanges: z2.array(z2.string())
653
+ var approvalSchema = z4.enum(["unreviewed", "proposed", "accepted"]);
654
+ var statusSchema = z4.enum(["active", "superseded", "retired"]);
655
+ var reviewEvidenceSchema = z4.object({
656
+ baseHash: z4.string(),
657
+ headHash: z4.string(),
658
+ historyAvailable: z4.boolean(),
659
+ changedFiles: z4.array(z4.string()),
660
+ localChanges: z4.array(z4.string())
234
661
  });
235
- var eventSchema = z2.object({
236
- kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
237
- at: z2.string().datetime(),
662
+ var eventSchema = z4.object({
663
+ kind: z4.enum([
664
+ "imported",
665
+ "created",
666
+ "revised",
667
+ "accepted",
668
+ "reaffirmed",
669
+ "retired",
670
+ "superseded"
671
+ ]),
672
+ at: z4.string().datetime(),
238
673
  actor: text(200).optional(),
239
674
  note: text(1500).optional(),
240
- revision: z2.number().int().positive(),
675
+ revision: z4.number().int().positive(),
241
676
  content: contentSchema,
242
677
  approval: approvalSchema,
243
678
  status: statusSchema,
244
- refreshedHash: z2.string(),
245
- evidence: reviewEvidenceSchema.optional()
679
+ refreshedHash: z4.string(),
680
+ evidence: reviewEvidenceSchema.optional(),
681
+ capture: anchorCaptureSchema.optional()
246
682
  });
247
- var legacySchema = z2.object({
248
- version: z2.literal(1),
249
- id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
250
- title: z2.string().min(1),
251
- body: z2.string().min(1),
683
+ var legacySchema = z4.object({
684
+ version: z4.literal(1),
685
+ id: z4.string().regex(/^[a-zA-Z0-9_-]+$/),
686
+ title: z4.string().min(1),
687
+ body: z4.string().min(1),
252
688
  category: contentSchema.shape.category,
253
689
  files: contentSchema.shape.files,
254
- createdAt: z2.string(),
255
- updatedAt: z2.string(),
256
- refreshedHash: z2.string(),
257
- status: z2.enum(["active", "superseded"]),
258
- supersededBy: z2.string().optional()
690
+ createdAt: z4.string(),
691
+ updatedAt: z4.string(),
692
+ refreshedHash: z4.string(),
693
+ status: z4.enum(["active", "superseded"]),
694
+ supersededBy: z4.string().optional()
259
695
  }).passthrough();
260
696
  var currentSchema = legacySchema.extend({
261
- version: z2.literal(2),
697
+ version: z4.literal(2),
262
698
  status: statusSchema,
263
699
  approval: approvalSchema,
264
- revision: z2.number().int().positive(),
700
+ revision: z4.number().int().positive(),
265
701
  owner: text(200).optional(),
266
- sources: z2.array(decisionSourceSchema).max(20),
267
- history: z2.array(eventSchema).min(1)
702
+ sources: z4.array(decisionSourceSchema).max(20),
703
+ history: z4.array(eventSchema).min(1),
704
+ capture: anchorCaptureSchema.optional(),
705
+ inspections: z4.array(
706
+ z4.object({
707
+ at: z4.string().datetime(),
708
+ inspector: text(200),
709
+ note: text(1500),
710
+ outcome: z4.literal("no_contradiction_found"),
711
+ decisionDigest: z4.string().regex(/^[a-f0-9]{64}$/),
712
+ headHash: z4.string(),
713
+ capture: anchorCaptureSchema
714
+ })
715
+ ).max(200).optional()
268
716
  }).superRefine((record, ctx) => {
269
717
  const invalid = (message) => ctx.addIssue({ code: "custom", message });
270
718
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
271
719
  let previous;
272
720
  for (const event of record.history) {
273
721
  if (!previous) {
274
- if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
275
- if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
722
+ if (!["created", "imported"].includes(event.kind) || event.revision !== 1)
723
+ invalid("History must begin with creation or legacy import at revision 1");
724
+ if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed"))
725
+ invalid("Initial records cannot claim acceptance");
276
726
  } else {
277
727
  if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
278
728
  if (previous.status !== "active") invalid("Archived decisions cannot be changed");
279
- if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
280
- if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
281
- if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
282
- if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
729
+ if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0))
730
+ invalid("Invalid revision sequence");
731
+ if (event.kind !== "revised" && !same(event.content, previous.content))
732
+ invalid("A review cannot silently revise decision content");
733
+ if (event.kind === "reaffirmed" && previous.approval !== "accepted")
734
+ invalid("Only accepted decisions can be reaffirmed");
735
+ if (event.kind === "accepted" && previous.approval === "accepted")
736
+ invalid("Use reaffirmation for an accepted decision");
283
737
  const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
284
738
  if (event.approval !== approval) invalid("Approval disagrees with review history");
285
- if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
739
+ if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash)
740
+ invalid("Only a review can refresh the evidence baseline");
286
741
  }
287
- if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
288
- if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
742
+ if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active"))
743
+ invalid("Lifecycle disagrees with history");
744
+ if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence))
745
+ invalid("Reviews require a named reviewer, reason, and code evidence");
289
746
  if (["accepted", "reaffirmed"].includes(event.kind)) {
290
- if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
291
- 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");
747
+ if (!event.content.owner || !event.content.sources.length)
748
+ invalid("Accepted decisions require an owner and source");
749
+ if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length)
750
+ invalid("Acceptance requires a committed evidence baseline");
292
751
  }
752
+ if (event.capture && !same([...event.content.files].sort(), event.capture.anchors))
753
+ invalid("Capture scope disagrees with decision anchors");
293
754
  previous = event;
294
755
  }
295
- 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");
756
+ if (previous && !same(previous.capture, record.capture))
757
+ invalid("Capture does not match the final history event");
758
+ if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash)
759
+ invalid("Decision does not match the final history event");
296
760
  });
297
- var decisionSchema = z2.union([legacySchema, currentSchema]);
761
+ var decisionSchema = z4.union([legacySchema, currentSchema]);
298
762
  function decisionContent(record) {
299
763
  return {
300
764
  title: record.title,
@@ -309,7 +773,8 @@ function decisionApproval(record) {
309
773
  return record.version === 1 ? "unreviewed" : record.approval;
310
774
  }
311
775
  function effectiveDecision(record) {
312
- if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed") return record;
776
+ if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed")
777
+ return record;
313
778
  let index = record.history.length - 1;
314
779
  while (index >= 0 && !["accepted", "reaffirmed"].includes(record.history[index].kind)) index--;
315
780
  if (index < 0) return record;
@@ -321,6 +786,7 @@ function effectiveDecision(record) {
321
786
  approval: "accepted",
322
787
  revision: event.revision,
323
788
  refreshedHash: event.refreshedHash,
789
+ capture: event.capture,
324
790
  updatedAt: event.at,
325
791
  history: record.history.slice(0, index + 1)
326
792
  };
@@ -330,7 +796,9 @@ function decisionAnchors(record) {
330
796
  }
331
797
  function decisionProvenance(record, freshness = "unknown") {
332
798
  const approval = decisionApproval(record);
333
- const review = record.version === 2 ? [...record.history].reverse().find((e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision) : void 0;
799
+ const review = record.version === 2 ? [...record.history].reverse().find(
800
+ (e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision
801
+ ) : void 0;
334
802
  return {
335
803
  approval,
336
804
  revision: record.version === 2 ? record.revision : 0,
@@ -338,71 +806,149 @@ function decisionProvenance(record, freshness = "unknown") {
338
806
  sources: record.version === 2 ? record.sources : [],
339
807
  guidance: record.status !== "active" ? "historical" : approval === "accepted" ? "constraint" : approval === "proposed" ? "proposal" : "unreviewed",
340
808
  reviewRequired: record.status === "active" && (approval !== "accepted" || freshness !== "current"),
341
- lastReview: review ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
809
+ lastReview: review ? {
810
+ reviewer: review.actor,
811
+ at: review.at,
812
+ note: review.note,
813
+ gitHash: review.refreshedHash
814
+ } : null
342
815
  };
343
816
  }
344
817
  function decisionTrust(record, freshness) {
345
818
  const review = decisionProvenance(record, freshness).lastReview;
346
- return assessTrust(review ? { verifiedAt: review.at, verifiedHash: review.gitHash } : {}, freshness);
819
+ return assessTrust(
820
+ review ? { verifiedAt: review.at, verifiedHash: review.gitHash } : {},
821
+ freshness
822
+ );
823
+ }
824
+ function decisionInspectionDigest(record) {
825
+ const effective = effectiveDecision(record);
826
+ return createHash2("sha256").update(
827
+ JSON.stringify([
828
+ record.status,
829
+ decisionApproval(record),
830
+ record.version === 2 ? record.revision : 0,
831
+ decisionContent(record),
832
+ decisionContent(effective),
833
+ effective.refreshedHash
834
+ ])
835
+ ).digest("hex");
836
+ }
837
+ function latestDecisionInspection(record) {
838
+ return record.version === 2 ? record.inspections?.filter((i) => i.decisionDigest === decisionInspectionDigest(record)).at(-1) : void 0;
347
839
  }
348
840
  function revisionKnowledge(record, freshness) {
349
- return { ...decisionContent(record), ...decisionProvenance(record, freshness), trust: decisionTrust(record, freshness) };
841
+ return {
842
+ ...decisionContent(record),
843
+ ...decisionProvenance(record, freshness),
844
+ trust: decisionTrust(record, freshness)
845
+ };
350
846
  }
351
847
  function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "unknown") {
352
848
  const effective = effectiveDecision(record);
849
+ const inspection = latestDecisionInspection(record);
353
850
  return {
354
851
  ...revisionKnowledge(effective, freshness),
852
+ ...inspection ? {
853
+ inspection: {
854
+ freshness,
855
+ at: inspection.at,
856
+ inspector: inspection.inspector,
857
+ note: inspection.note,
858
+ outcome: inspection.outcome,
859
+ headHash: inspection.headHash
860
+ }
861
+ } : {},
355
862
  ...effective !== record ? { pendingProposal: revisionKnowledge(record, proposalFreshness) } : {}
356
863
  };
357
864
  }
358
- var DECISION_GUIDANCE = "Accepted decisions are recorded team constraints, subject to freshness checks. A pendingProposal is an unaccepted replacement; the accepted revision remains operative until explicit acceptance or retirement. Proposals are suggestions; legacy unreviewed records need confirmation. Use review_decision to inspect provenance and record an authorized review; identities and sources are recorded assertions, not authenticated proof.";
865
+ var DECISION_GUIDANCE = "Accepted decisions are recorded team constraints, subject to freshness checks. A pendingProposal is an unaccepted replacement; the accepted revision remains operative until explicit acceptance or retirement. Proposals are suggestions; legacy unreviewed records need confirmation. Use review_decision to prepare evidence and record a separate source inspection without human approval, or an authorized human review; identities and sources are recorded assertions, not authenticated proof.";
359
866
 
360
867
  // src/decisions/decisions.ts
361
868
  var TITLE_MAX_CHARS = 80;
362
869
  var BODY_RECOMMENDED_CHARS = 1500;
363
870
  var BODY_MAX_CHARS = 2500;
364
- var boundedText = (field, max, hint) => z3.string({
365
- 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 })
871
+ var boundedText = (field, max, hint) => z5.string({
872
+ errorMap: (issue, ctx) => ({
873
+ 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
874
+ })
366
875
  }).trim().min(1, `${field} must be non-empty`).max(max);
367
- var decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
876
+ var decisionTitleSchema = boundedText(
877
+ "title",
878
+ TITLE_MAX_CHARS,
879
+ "Tighten it to a specific headline."
880
+ );
368
881
  var decisionBodySchema = boundedText(
369
882
  "body",
370
883
  BODY_MAX_CHARS,
371
884
  `Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
372
885
  );
886
+ async function readDecisionInputs(rootDir) {
887
+ return structuredClone(
888
+ await inspectionRead(rootDir, "decision-store:inputs", async () => {
889
+ const inputs = [];
890
+ const diagnostics = [];
891
+ let entries;
892
+ try {
893
+ entries = await fs5.readdir(await storePath(rootDir, ".mason/decisions"));
894
+ } catch (error) {
895
+ if (error.code !== "ENOENT")
896
+ diagnostics.push({ path: ".mason/decisions", message: String(error) });
897
+ return { inputs, diagnostics };
898
+ }
899
+ for (const entry of entries.sort()) {
900
+ if (!entry.endsWith(".json")) continue;
901
+ const relative = `.mason/decisions/${entry}`;
902
+ try {
903
+ const raw = await readBoundedFile(await storePath(rootDir, relative), 10 * 1024 * 1024);
904
+ if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
905
+ inputs.push([relative, raw]);
906
+ } catch (error) {
907
+ if (error.code === "ENOENT") inputs.push([relative, null]);
908
+ else
909
+ diagnostics.push({
910
+ path: relative,
911
+ message: error instanceof Error ? error.message : String(error)
912
+ });
913
+ }
914
+ }
915
+ return { inputs, diagnostics };
916
+ })
917
+ );
918
+ }
373
919
  async function loadDecisionStore(rootDir) {
374
- const records = [];
375
- const diagnostics = [];
376
- let entries;
377
- try {
378
- entries = await fs4.readdir(await storePath(rootDir, ".mason/decisions"));
379
- } catch (error) {
380
- if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
381
- return { records, diagnostics };
382
- }
383
- for (const entry of entries.sort()) {
384
- if (!entry.endsWith(".json")) continue;
385
- const relative = `.mason/decisions/${entry}`;
386
- try {
387
- const record = decisionSchema.parse(await readStoreJson(rootDir, relative));
388
- if (entry !== `${record.id}.json`) throw new Error("Record id does not match its filename");
389
- records.push(record);
390
- } catch (error) {
391
- diagnostics.push({ path: relative, message: error instanceof Error ? error.message : String(error) });
392
- }
393
- }
394
- return { records, diagnostics };
920
+ return structuredClone(
921
+ await inspectionRead(rootDir, "decision-store:parsed", async () => {
922
+ const raw = await readDecisionInputs(rootDir);
923
+ const records = [];
924
+ const diagnostics = [...raw.diagnostics];
925
+ for (const [relative, content] of raw.inputs) {
926
+ try {
927
+ const record = decisionSchema.parse(content === null ? null : JSON.parse(content));
928
+ if (relative !== `.mason/decisions/${record.id}.json`)
929
+ throw new Error("Record id does not match its filename");
930
+ records.push(record);
931
+ } catch (error) {
932
+ diagnostics.push({
933
+ path: relative,
934
+ message: error instanceof Error ? error.message : String(error)
935
+ });
936
+ }
937
+ }
938
+ return { records, diagnostics };
939
+ })
940
+ );
395
941
  }
396
942
 
397
943
  // src/hook/hook.ts
398
- import { createHash as createHash2 } from "crypto";
944
+ import { createHash as createHash4 } from "crypto";
399
945
 
400
946
  // src/decisions/drift.ts
401
- import path9 from "path";
947
+ import path10 from "path";
402
948
 
403
949
  // src/drift/drift.ts
404
- import fs5 from "fs/promises";
405
- import path8 from "path";
950
+ import fs6 from "fs/promises";
951
+ import path9 from "path";
406
952
  function parseChanges(output) {
407
953
  const fields = output.split("\0");
408
954
  const changes = [];
@@ -412,18 +958,25 @@ function parseChanges(output) {
412
958
  if (!first) break;
413
959
  const second = /^[RC]/.test(code) ? fields[i++] : void 0;
414
960
  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 };
415
- if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
961
+ if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/")))
962
+ continue;
416
963
  changes.push(change);
417
964
  }
418
965
  return changes;
419
966
  }
420
967
  function touchedPaths(changes) {
421
- return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
968
+ return [
969
+ ...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))
970
+ ].sort();
422
971
  }
423
972
  async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
424
- if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
973
+ if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
974
+ return null;
425
975
  try {
426
- const { stdout } = await execGit(["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
976
+ const { stdout } = await execGit(
977
+ ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"],
978
+ { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
979
+ );
427
980
  return parseChanges(stdout);
428
981
  } catch {
429
982
  return null;
@@ -432,11 +985,23 @@ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
432
985
  async function getWorkingTree(resolvedRoot) {
433
986
  try {
434
987
  const [diff, untracked] = await Promise.all([
435
- execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
436
- execGit(["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
988
+ execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], {
989
+ cwd: resolvedRoot,
990
+ maxBuffer: 10 * 1024 * 1024
991
+ }),
992
+ execGit(["ls-files", "-z", "--others", "--exclude-standard"], {
993
+ cwd: resolvedRoot,
994
+ maxBuffer: 10 * 1024 * 1024
995
+ })
437
996
  ]);
438
997
  const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
439
- return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
998
+ return {
999
+ available: true,
1000
+ changedFiles: [
1001
+ .../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])
1002
+ ].sort(),
1003
+ untrackedFiles
1004
+ };
440
1005
  } catch {
441
1006
  return { available: false, changedFiles: [], untrackedFiles: [] };
442
1007
  }
@@ -444,13 +1009,47 @@ async function getWorkingTree(resolvedRoot) {
444
1009
 
445
1010
  // src/decisions/drift.ts
446
1011
  async function computeDecisionDrift(rootDir, decisions) {
447
- const resolvedRoot = path9.resolve(rootDir);
1012
+ const resolvedRoot = path10.resolve(rootDir);
448
1013
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
449
- const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
450
- const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
1014
+ const report = {
1015
+ historyAvailable: true,
1016
+ totalDecisions: store.records.length,
1017
+ staleDecisions: {},
1018
+ freshness: {},
1019
+ diagnostics: store.diagnostics
1020
+ };
1021
+ const [head, workingTree] = await Promise.all([
1022
+ getCurrentGitHash(resolvedRoot),
1023
+ getWorkingTree(resolvedRoot)
1024
+ ]);
451
1025
  const changesByHash = /* @__PURE__ */ new Map();
452
- const inspect = async (record) => {
1026
+ const baselines = store.records.filter((record) => record.status === "active").flatMap((record) => {
1027
+ const inspection = latestDecisionInspection(record);
1028
+ if (inspection) return [inspection.capture];
1029
+ return [.../* @__PURE__ */ new Set([effectiveDecision(record), record])].flatMap(
1030
+ (candidate) => candidate.version === 2 && candidate.capture ? [candidate.capture] : []
1031
+ );
1032
+ });
1033
+ const captures = await captureAnchorScopes(
1034
+ resolvedRoot,
1035
+ baselines.map((baseline) => baseline.anchors)
1036
+ );
1037
+ const currentCaptures = new Map(
1038
+ baselines.map((baseline, index) => [JSON.stringify(baseline.anchors), captures[index]])
1039
+ );
1040
+ const inspect = async (record, inspectionCapture) => {
453
1041
  if (record.files.length === 0) return { freshness: "unknown", changedFiles: [] };
1042
+ const baseline = inspectionCapture ?? (record.version === 2 ? record.capture : void 0);
1043
+ if (baseline) {
1044
+ const current = currentCaptures.get(JSON.stringify(baseline.anchors));
1045
+ const hits2 = changedCaptureFiles(baseline, current).filter(
1046
+ (file) => matchingPaths(record.files, [file]).length
1047
+ );
1048
+ return {
1049
+ freshness: !baseline.complete || !current.complete ? "unknown" : hits2.length ? "changed" : "current",
1050
+ changedFiles: hits2
1051
+ };
1052
+ }
454
1053
  let touched = changesByHash.get(record.refreshedHash);
455
1054
  if (touched === void 0) {
456
1055
  const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
@@ -460,15 +1059,20 @@ async function computeDecisionDrift(rootDir, decisions) {
460
1059
  if (touched === null) report.historyAvailable = false;
461
1060
  const hits = touched ? matchingPaths(record.files, touched) : [];
462
1061
  const localHits = matchingPaths(record.files, workingTree.changedFiles);
463
- return { freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current", changedFiles: hits };
1062
+ return {
1063
+ freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current",
1064
+ changedFiles: [.../* @__PURE__ */ new Set([...hits, ...localHits])].sort()
1065
+ };
464
1066
  };
465
1067
  for (const record of store.records) {
466
1068
  if (record.status !== "active") continue;
467
1069
  const effective = effectiveDecision(record);
468
- const state = await inspect(effective);
1070
+ const inspection = latestDecisionInspection(record);
1071
+ const state = await inspect(effective, inspection?.capture);
469
1072
  report.freshness[record.id] = state.freshness;
470
1073
  if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
471
- if (effective !== record) (report.pendingProposals ??= {})[record.id] = await inspect(record);
1074
+ if (effective !== record)
1075
+ (report.pendingProposals ??= {})[record.id] = await inspect(record, inspection?.capture);
472
1076
  }
473
1077
  return report;
474
1078
  }
@@ -477,9 +1081,17 @@ async function computeDecisionDrift(rootDir, decisions) {
477
1081
  var MAX_INJECTED_DECISIONS = 3;
478
1082
  var MAX_WALK_UP = 30;
479
1083
  var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write"]);
1084
+ var hookStdinSchema = z6.object({
1085
+ session_id: z6.string().optional(),
1086
+ agent_id: z6.string().optional(),
1087
+ cwd: z6.string().optional(),
1088
+ hook_event_name: z6.string().optional(),
1089
+ tool_name: z6.string().optional(),
1090
+ tool_input: z6.object({ file_path: z6.string().optional() }).optional()
1091
+ });
480
1092
  async function exists(p) {
481
1093
  try {
482
- await fs6.access(p);
1094
+ await fs7.access(p);
483
1095
  return true;
484
1096
  } catch {
485
1097
  return false;
@@ -488,9 +1100,9 @@ async function exists(p) {
488
1100
  async function findMasonRoot(startDir) {
489
1101
  let dir = startDir;
490
1102
  for (let i = 0; i < MAX_WALK_UP; i++) {
491
- if (await exists(path10.join(dir, ".mason", "decisions"))) return dir;
492
- if (await exists(path10.join(dir, ".git"))) return null;
493
- const parent = path10.dirname(dir);
1103
+ if (await exists(path11.join(dir, ".mason", "decisions"))) return dir;
1104
+ if (await exists(path11.join(dir, ".git"))) return null;
1105
+ const parent = path11.dirname(dir);
494
1106
  if (parent === dir) return null;
495
1107
  dir = parent;
496
1108
  }
@@ -508,8 +1120,8 @@ function stateKey(input) {
508
1120
  }
509
1121
  async function loadInjected(stateFile) {
510
1122
  try {
511
- const parsed = JSON.parse(await fs6.readFile(stateFile, "utf-8"));
512
- return new Set(Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : []);
1123
+ const parsed = JSON.parse(await fs7.readFile(stateFile, "utf-8"));
1124
+ return new Set(isStringArray(parsed) ? parsed : []);
513
1125
  } catch {
514
1126
  return /* @__PURE__ */ new Set();
515
1127
  }
@@ -517,7 +1129,7 @@ async function loadInjected(stateFile) {
517
1129
  function formatContext(relPath, records, drift) {
518
1130
  const lines = [];
519
1131
  lines.push(
520
- `Mason: decision knowledge relevant to ${relPath} or updated since this session saw it. This replaces earlier guidance for the same decision id. ${DECISION_GUIDANCE} Retired or superseded records are no longer active. Do not modify decision records in .mason/decisions/.`
1132
+ `Mason: decision knowledge relevant to ${relPath} or updated since this session saw it. This replaces earlier guidance for the same decision id. ${DECISION_GUIDANCE} Retired or superseded records are no longer active. Use Mason tools to record decision changes and source inspections; do not edit the JSON in .mason/decisions/ directly.`
521
1133
  );
522
1134
  const append = (id, knowledge, label, freshness) => {
523
1135
  const stale = freshness === "current" ? "" : freshness === "changed" ? " [recorded against changed files \u2013 verify against current code before relying on it]" : " [freshness unknown \u2013 verify against current code before relying on it]";
@@ -526,16 +1138,31 @@ function formatContext(relPath, records, drift) {
526
1138
  );
527
1139
  };
528
1140
  for (const record of records) {
529
- const knowledge = decisionKnowledge(record, drift.freshness?.[record.id] ?? "unknown", drift.pendingProposals?.[record.id]?.freshness ?? "unknown");
530
- append(record.id, knowledge, record.status === "active" ? knowledge.approval : record.status, knowledge.trust.freshness);
531
- if (knowledge.pendingProposal) append(record.id, knowledge.pendingProposal, "proposed", knowledge.pendingProposal.trust.freshness);
1141
+ const knowledge = decisionKnowledge(
1142
+ record,
1143
+ drift.freshness?.[record.id] ?? "unknown",
1144
+ drift.pendingProposals?.[record.id]?.freshness ?? "unknown"
1145
+ );
1146
+ append(
1147
+ record.id,
1148
+ knowledge,
1149
+ record.status === "active" ? knowledge.approval : record.status,
1150
+ knowledge.trust.freshness
1151
+ );
1152
+ if (knowledge.pendingProposal)
1153
+ append(
1154
+ record.id,
1155
+ knowledge.pendingProposal,
1156
+ "proposed",
1157
+ knowledge.pendingProposal.trust.freshness
1158
+ );
532
1159
  }
533
1160
  return lines.join("\n");
534
1161
  }
535
1162
  async function runHook(stdinText, env = {}) {
536
1163
  let input;
537
1164
  try {
538
- input = JSON.parse(stdinText);
1165
+ input = hookStdinSchema.parse(JSON.parse(stdinText));
539
1166
  } catch {
540
1167
  return null;
541
1168
  }
@@ -543,18 +1170,23 @@ async function runHook(stdinText, env = {}) {
543
1170
  if (input.tool_name && !SUPPORTED_TOOLS.has(input.tool_name)) return null;
544
1171
  const filePath = input.tool_input?.file_path;
545
1172
  if (!filePath || typeof filePath !== "string") return null;
546
- const absPath = path10.isAbsolute(filePath) ? filePath : path10.resolve(input.cwd ?? process.cwd(), filePath);
547
- const root = await findMasonRoot(path10.dirname(absPath));
1173
+ const absPath = path11.isAbsolute(filePath) ? filePath : path11.resolve(input.cwd ?? process.cwd(), filePath);
1174
+ const root = await findMasonRoot(path11.dirname(absPath));
548
1175
  if (!root) return null;
549
- const relPath = path10.relative(root, absPath).split(path10.sep).join("/");
1176
+ const relPath = path11.relative(root, absPath).split(path11.sep).join("/");
550
1177
  if (relPath.startsWith("..")) return null;
551
1178
  const { records } = await loadDecisionStore(root);
552
1179
  const stateDir = env.stateDir ?? os2.tmpdir();
553
- const stateFile = path10.join(stateDir, `mason-hook-${createHash2("sha256").update(root).digest("hex").slice(0, 12)}-${stateKey(input)}.json`);
1180
+ const stateFile = path11.join(
1181
+ stateDir,
1182
+ `mason-hook-${createHash4("sha256").update(root).digest("hex").slice(0, 12)}-${stateKey(input)}.json`
1183
+ );
554
1184
  const injected = await loadInjected(stateFile);
555
- const recordKey = (record) => `${record.id}:${createHash2("sha256").update(JSON.stringify(record)).digest("hex")}`;
1185
+ const recordKey = (record) => `${record.id}:${createHash4("sha256").update(JSON.stringify(record)).digest("hex")}`;
556
1186
  const previouslyInjected = (record) => [...injected].some((key) => key === record.id || key.startsWith(`${record.id}:`));
557
- const fresh = records.filter((r) => !injected.has(recordKey(r)) && (previouslyInjected(r) || r.status === "active" && anchorsCover(r, relPath)));
1187
+ const fresh = records.filter(
1188
+ (r) => !injected.has(recordKey(r)) && (previouslyInjected(r) || r.status === "active" && anchorsCover(r, relPath))
1189
+ );
558
1190
  if (fresh.length === 0) return null;
559
1191
  fresh.sort((a, b) => {
560
1192
  const withdrawn = Number(b.status !== "active") - Number(a.status !== "active");
@@ -567,7 +1199,7 @@ async function runHook(stdinText, env = {}) {
567
1199
  const drift = await computeDecisionDrift(root, selected);
568
1200
  for (const record of selected) injected.add(recordKey(record));
569
1201
  try {
570
- await fs6.writeFile(stateFile, JSON.stringify([...injected]), "utf-8");
1202
+ await fs7.writeFile(stateFile, JSON.stringify([...injected]), "utf-8");
571
1203
  } catch {
572
1204
  }
573
1205
  return JSON.stringify({
@@ -635,7 +1267,9 @@ async function readStdin() {
635
1267
  if (process.stdin.isTTY) return "";
636
1268
  const chunks = [];
637
1269
  for await (const chunk of process.stdin) {
638
- chunks.push(Buffer.from(chunk));
1270
+ const value = chunk;
1271
+ if (typeof value === "string" || value instanceof Uint8Array) chunks.push(Buffer.from(value));
1272
+ else throw new Error("Unsupported stdin chunk.");
639
1273
  }
640
1274
  return Buffer.concat(chunks).toString("utf-8");
641
1275
  }