mason-context 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mason-mcp.js CHANGED
@@ -2832,7 +2832,13 @@ function managedBlock(text2, start, end, body) {
2832
2832
  async function applyEdit(root, edit) {
2833
2833
  const current = await readText(root, edit.path);
2834
2834
  if (current === edit.after) return false;
2835
- if (current !== edit.before) throw new Error("Setup input changed during installation: " + edit.path + ". Rerun setup to resume.");
2835
+ if (current !== edit.before) throw new Error("Integration input changed during update: " + edit.path + ". Rerun the requested command to resume.");
2836
+ if (edit.after === null) {
2837
+ const file2 = await storePath(root, edit.path);
2838
+ if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
2839
+ await fs13.unlink(file2);
2840
+ return true;
2841
+ }
2836
2842
  const file = await storePath(root, edit.path, true);
2837
2843
  const temporary = path19.join(path19.dirname(file), ".mason-setup-" + randomUUID3() + ".tmp");
2838
2844
  try {
@@ -2844,7 +2850,7 @@ async function applyEdit(root, edit) {
2844
2850
  } finally {
2845
2851
  await handle.close();
2846
2852
  }
2847
- if (await readText(root, edit.path) !== edit.before) throw new Error("Setup input changed during installation: " + edit.path);
2853
+ if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
2848
2854
  await fs13.rename(temporary, file);
2849
2855
  return true;
2850
2856
  } finally {
@@ -3130,7 +3136,7 @@ var init_evidence = __esm({
3130
3136
  init_dead_command();
3131
3137
  init_storage();
3132
3138
  exec11 = promisify12(execFile12);
3133
- engineVersion = true ? "0.15.0" : "development";
3139
+ engineVersion = true ? "0.16.1" : "development";
3134
3140
  hash = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
3135
3141
  content = readAuditInput;
3136
3142
  internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
@@ -3494,6 +3500,79 @@ var init_adapters = __esm({
3494
3500
  }
3495
3501
  });
3496
3502
 
3503
+ // src/setup/ownership.ts
3504
+ var ownership_exports = {};
3505
+ __export(ownership_exports, {
3506
+ BLOCKS: () => BLOCKS,
3507
+ OWNERSHIP_PATH: () => OWNERSHIP_PATH,
3508
+ blockSpan: () => blockSpan,
3509
+ completedHookOwnership: () => completedHookOwnership,
3510
+ ownershipEdit: () => ownershipEdit,
3511
+ ownershipSchema: () => ownershipSchema
3512
+ });
3513
+ import { z as z10 } from "zod";
3514
+ import { parse } from "smol-toml";
3515
+ function blockSpan(text2, start, end) {
3516
+ const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
3517
+ if (!starts && !ends) return null;
3518
+ const from = text2.indexOf(start), to = text2.indexOf(end) + end.length;
3519
+ if (starts !== 1 || ends !== 1 || to <= from) throw new Error("Ambiguous Mason markers; inspect the marked block manually.");
3520
+ return { from, to, text: text2.slice(from, to) };
3521
+ }
3522
+ async function ownershipEdit(root, edits, hookCommands = {}) {
3523
+ const before = await readText(root, OWNERSHIP_PATH);
3524
+ const record = before === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(before));
3525
+ for (const edit of edits) {
3526
+ const previous = record.files[edit.path];
3527
+ const entry = {
3528
+ created: edit.before === null || (previous?.created ?? false),
3529
+ initialHash: edit.before === null || previous?.initialHash === hash(edit.before) ? hash(edit.after) : previous?.initialHash ?? hash(edit.after),
3530
+ blocks: Object.fromEntries(BLOCKS.flatMap(([start, end]) => {
3531
+ const block = blockSpan(edit.after, start, end);
3532
+ return block ? [[start, hash(block.text)]] : [];
3533
+ }))
3534
+ };
3535
+ const mcp = edit.path === ".codex/config.toml" ? parse(edit.after).mcp_servers?.mason : edit.path === ".mcp.json" ? JSON.parse(edit.after).mcpServers?.mason : void 0;
3536
+ const hooks = edit.path === ".codex/hooks.json" || edit.path === ".claude/settings.json" ? JSON.parse(edit.after).hooks : void 0;
3537
+ record.files[edit.path] = {
3538
+ ...entry,
3539
+ ...mcp === void 0 ? {} : { mcpHash: hash(mcp) },
3540
+ ...hookCommands[edit.path] ? { hookCommands: [.../* @__PURE__ */ new Set([...previous?.hookCommands ?? [], hookCommands[edit.path]])] } : {},
3541
+ ...hooks === void 0 ? {} : { hooks: Object.fromEntries(Object.entries(hooks).map(([event, groups]) => [event, groups.flatMap((group) => group.hooks.map(hash))])) }
3542
+ };
3543
+ }
3544
+ return { path: OWNERSHIP_PATH, before, after: JSON.stringify(record, null, 2) + "\n" };
3545
+ }
3546
+ async function completedHookOwnership(root, file, command) {
3547
+ const before = await readText(root, OWNERSHIP_PATH);
3548
+ const record = ownershipSchema.parse(JSON.parse(before ?? "null"));
3549
+ record.files[file].hookCommands = [command];
3550
+ return { path: OWNERSHIP_PATH, before, after: JSON.stringify(record, null, 2) + "\n" };
3551
+ }
3552
+ var OWNERSHIP_PATH, BLOCKS, fileSchema, ownershipSchema;
3553
+ var init_ownership = __esm({
3554
+ "src/setup/ownership.ts"() {
3555
+ "use strict";
3556
+ init_evidence();
3557
+ init_files2();
3558
+ OWNERSHIP_PATH = ".mason/local/integration.json";
3559
+ BLOCKS = [
3560
+ ["<!-- mason:start -->", "<!-- mason:end -->"],
3561
+ ["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
3562
+ ["# mason:mcp:start", "# mason:mcp:end"]
3563
+ ];
3564
+ fileSchema = z10.object({
3565
+ created: z10.boolean(),
3566
+ initialHash: z10.string(),
3567
+ blocks: z10.record(z10.string()),
3568
+ mcpHash: z10.string().optional(),
3569
+ hookCommands: z10.array(z10.string()).optional(),
3570
+ hooks: z10.record(z10.array(z10.string())).optional()
3571
+ });
3572
+ ownershipSchema = z10.object({ version: z10.literal(1), files: z10.record(fileSchema) });
3573
+ }
3574
+ });
3575
+
3497
3576
  // src/automation/install.ts
3498
3577
  var install_exports = {};
3499
3578
  __export(install_exports, {
@@ -3502,24 +3581,28 @@ __export(install_exports, {
3502
3581
  configPath: () => configPath,
3503
3582
  installAutomation: () => installAutomation,
3504
3583
  installedAutomation: () => installedAutomation,
3505
- planAutomationInstall: () => planAutomationInstall
3584
+ planAutomationInstall: () => planAutomationInstall,
3585
+ recordSchema: () => recordSchema
3506
3586
  });
3507
- import { z as z10 } from "zod";
3587
+ import { z as z11 } from "zod";
3508
3588
  async function installationRecord(root) {
3509
3589
  return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
3510
3590
  }
3511
3591
  async function installAutomation(dir, host, command) {
3512
3592
  const ws = await workspace(dir);
3513
- return withLock(ws.root, ".mason/reports/automation-install", () => installLocked(ws.root, host, command));
3593
+ return withLock(ws.root, ".mason/reports/setup-lock", () => installLocked(ws.root, host, command));
3514
3594
  }
3515
3595
  async function planAutomationInstall(root, host, command) {
3516
3596
  const file = configPath(host);
3517
- const existing = automationConfigSchema.parse(await readStoreJson(root, file) ?? {});
3597
+ const before = await readText(root, file);
3598
+ const existing = automationConfigSchema.parse(JSON.parse(before ?? "{}"));
3518
3599
  const record = await installationRecord(root);
3519
3600
  const desired = hookConfig(host, command);
3520
3601
  const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
3521
3602
  const previous = record.hosts[host]?.command;
3522
- const owned = /* @__PURE__ */ new Set([previous, newCommand]);
3603
+ const ownershipText = await readText(root, OWNERSHIP_PATH);
3604
+ const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
3605
+ const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand]);
3523
3606
  const hooks = existing.hooks ?? {};
3524
3607
  for (const event of HOOK_EVENTS) {
3525
3608
  hooks[event] = (hooks[event] ?? []).map((group) => ({
@@ -3529,15 +3612,20 @@ async function planAutomationInstall(root, host, command) {
3529
3612
  hooks[event].push(...desired.hooks[event]);
3530
3613
  }
3531
3614
  record.hosts[host] = { command: newCommand };
3532
- return { file, config: { ...existing, hooks }, record, newCommand };
3615
+ return { file, before, config: { ...existing, hooks }, record, newCommand };
3533
3616
  }
3534
3617
  async function installLocked(root, host, command) {
3535
- const { file, config, record, newCommand } = await planAutomationInstall(root, host, command);
3618
+ const { file, before, config, record, newCommand } = await planAutomationInstall(root, host, command);
3536
3619
  const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
3537
3620
  const { applyEdit: applyEdit2 } = await Promise.resolve().then(() => (init_files2(), files_exports));
3538
- for (const edit of await ancillaryEdits2(root)) await applyEdit2(root, edit);
3539
- await writeStoreJson(root, file, config);
3621
+ const { ownershipEdit: ownershipEdit2, completedHookOwnership: completedHookOwnership2 } = await Promise.resolve().then(() => (init_ownership(), ownership_exports));
3622
+ const edit = { path: file, before, after: JSON.stringify(config, null, 2) + "\n" };
3623
+ const ancillary = await ancillaryEdits2(root);
3624
+ await applyEdit2(root, await ownershipEdit2(root, [edit], { [file]: newCommand }));
3625
+ for (const edit2 of ancillary) await applyEdit2(root, edit2);
3626
+ await applyEdit2(root, edit);
3540
3627
  await writeStoreJson(root, AUTOMATION_PATH, record);
3628
+ await applyEdit2(root, await completedHookOwnership2(root, file, newCommand));
3541
3629
  return {
3542
3630
  version: 1,
3543
3631
  host,
@@ -3575,9 +3663,11 @@ var init_install = __esm({
3575
3663
  init_evidence();
3576
3664
  init_adapters();
3577
3665
  init_store();
3578
- groupSchema = z10.object({ hooks: z10.array(z10.object({ type: z10.string(), command: z10.string().optional() }).passthrough()) }).passthrough();
3579
- automationConfigSchema = z10.object({ hooks: z10.record(z10.array(groupSchema)).optional() }).passthrough();
3580
- recordSchema = z10.object({ version: z10.literal(1), hosts: z10.record(z10.object({ command: z10.string() })) });
3666
+ init_files2();
3667
+ init_ownership();
3668
+ groupSchema = z11.object({ hooks: z11.array(z11.object({ type: z11.string(), command: z11.string().optional() }).passthrough()) }).passthrough();
3669
+ automationConfigSchema = z11.object({ hooks: z11.record(z11.array(groupSchema)).optional() }).passthrough();
3670
+ recordSchema = z11.object({ version: z11.literal(1), hosts: z11.record(z11.object({ command: z11.string() })) });
3581
3671
  AUTOMATION_PATH = ".mason/local/automation.json";
3582
3672
  configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3583
3673
  }
@@ -3592,7 +3682,7 @@ __export(model_exports, {
3592
3682
  setupHostSchema: () => setupHostSchema,
3593
3683
  setupSchema: () => setupSchema
3594
3684
  });
3595
- import { z as z11 } from "zod";
3685
+ import { z as z12 } from "zod";
3596
3686
  async function loadSetup(root) {
3597
3687
  const raw = await readStoreJson(root, SETUP_PATH);
3598
3688
  if (raw === null) return null;
@@ -3610,27 +3700,27 @@ var init_model = __esm({
3610
3700
  "src/setup/model.ts"() {
3611
3701
  "use strict";
3612
3702
  init_storage();
3613
- setupHostSchema = z11.object({
3614
- configuredVersion: z11.string(),
3615
- revision: z11.string().uuid(),
3616
- fingerprint: z11.string(),
3617
- mcpFingerprint: z11.string(),
3618
- instructions: z11.array(z11.string())
3703
+ setupHostSchema = z12.object({
3704
+ configuredVersion: z12.string(),
3705
+ revision: z12.string().uuid(),
3706
+ fingerprint: z12.string(),
3707
+ mcpFingerprint: z12.string(),
3708
+ instructions: z12.array(z12.string())
3619
3709
  });
3620
- setupSchema = z11.object({ version: z11.literal(2), hosts: z11.object({
3710
+ setupSchema = z12.object({ version: z12.literal(2), hosts: z12.object({
3621
3711
  codex: setupHostSchema.optional(),
3622
3712
  claude: setupHostSchema.optional()
3623
3713
  }) });
3624
3714
  SETUP_PATH = ".mason/local/setup.json";
3625
- receiptSchema = z11.object({
3626
- version: z11.literal(1),
3627
- host: z11.enum(["codex", "claude"]),
3628
- status: z11.enum(["installing", "configured"]),
3629
- initialReportPath: z11.string(),
3630
- initialBaselinePaths: z11.array(z11.string()),
3631
- root: z11.string(),
3632
- revision: z11.string().uuid(),
3633
- configuredAt: z11.string().optional()
3715
+ receiptSchema = z12.object({
3716
+ version: z12.literal(1),
3717
+ host: z12.enum(["codex", "claude"]),
3718
+ status: z12.enum(["installing", "configured"]),
3719
+ initialReportPath: z12.string(),
3720
+ initialBaselinePaths: z12.array(z12.string()),
3721
+ root: z12.string(),
3722
+ revision: z12.string().uuid(),
3723
+ configuredAt: z12.string().optional()
3634
3724
  });
3635
3725
  }
3636
3726
  });
@@ -3662,7 +3752,7 @@ var init_launcher = __esm({
3662
3752
  "use strict";
3663
3753
  init_evidence();
3664
3754
  init_model();
3665
- executingVersion = () => true ? "0.15.0" : "development";
3755
+ executingVersion = () => true ? "0.16.1" : "development";
3666
3756
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3667
3757
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3668
3758
  }
@@ -3679,8 +3769,8 @@ __export(config_exports, {
3679
3769
  mcpPath: () => mcpPath
3680
3770
  });
3681
3771
  import { isDeepStrictEqual } from "util";
3682
- import { parse, stringify } from "smol-toml";
3683
- import { z as z12 } from "zod";
3772
+ import { parse as parse2, stringify } from "smol-toml";
3773
+ import { z as z13 } from "zod";
3684
3774
  async function instructionEdits(root, host) {
3685
3775
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3686
3776
  const existing = found.find((f) => f.text !== null);
@@ -3721,7 +3811,7 @@ async function mcpEdit(root, host) {
3721
3811
  const servers2 = object.parse(config2.mcpServers ?? {});
3722
3812
  return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host) } }, null, 2) + "\n" };
3723
3813
  }
3724
- const config = parse(before ?? "");
3814
+ const config = parse2(before ?? "");
3725
3815
  const managed = (before ?? "").includes(TOML_START);
3726
3816
  const servers = config.mcp_servers;
3727
3817
  const desired = managedMcp(servers?.mason, host);
@@ -3733,7 +3823,7 @@ async function mcpEdit(root, host) {
3733
3823
  for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
3734
3824
  }
3735
3825
  const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
3736
- const parsed = parse(after);
3826
+ const parsed = parse2(after);
3737
3827
  const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
3738
3828
  if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
3739
3829
  return { path: file, before, after };
@@ -3771,7 +3861,7 @@ async function ancillaryEdits(root) {
3771
3861
  }
3772
3862
  async function inspectHostConfig(root, host, plannedMcp) {
3773
3863
  const text2 = plannedMcp ?? await readText(root, mcpPath(host));
3774
- const config = host === "codex" ? parse(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
3864
+ const config = host === "codex" ? parse2(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
3775
3865
  const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
3776
3866
  const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
3777
3867
  const expected = hookConfig(host, hookCommand(host));
@@ -3796,7 +3886,7 @@ var init_config = __esm({
3796
3886
  init_install();
3797
3887
  init_files2();
3798
3888
  init_launcher();
3799
- object = z12.record(z12.unknown());
3889
+ object = z13.record(z13.unknown());
3800
3890
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
3801
3891
  TOML_START = "# mason:mcp:start";
3802
3892
  TOML_END = "# mason:mcp:end";
@@ -3806,7 +3896,7 @@ var init_config = __esm({
3806
3896
  });
3807
3897
 
3808
3898
  // src/mcp/init.ts
3809
- import { z as z13 } from "zod";
3899
+ import { z as z14 } from "zod";
3810
3900
  async function loadProjectMarker(rootDir) {
3811
3901
  const raw = await readStoreJson(rootDir, ".mason/local/project.json");
3812
3902
  if (raw === null) return null;
@@ -3814,8 +3904,8 @@ async function loadProjectMarker(rootDir) {
3814
3904
  return { ...marker, features: await loadProjectFeatures(rootDir) };
3815
3905
  }
3816
3906
  async function loadProjectFeatures(rootDir) {
3817
- const config = z13.record(z13.unknown()).parse(await readStoreJson(rootDir, ".mason/config.json") ?? {});
3818
- return z13.object({ confluence: z13.boolean().optional() }).passthrough().parse(config.features ?? {});
3907
+ const config = z14.record(z14.unknown()).parse(await readStoreJson(rootDir, ".mason/config.json") ?? {});
3908
+ return z14.object({ confluence: z14.boolean().optional() }).passthrough().parse(config.features ?? {});
3819
3909
  }
3820
3910
  async function saveProjectMarker(rootDir, marker) {
3821
3911
  const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
@@ -3827,8 +3917,8 @@ async function projectMarkerEdits(rootDir, marker) {
3827
3917
  const edits = [{ path: file, before: await readText(rootDir, file), after: JSON.stringify(local, null, 2) + "\n" }];
3828
3918
  if (features && Object.keys(features).length) {
3829
3919
  const configPath2 = ".mason/config.json", before = await readText(rootDir, configPath2);
3830
- const config = z13.record(z13.unknown()).parse(JSON.parse(before ?? "{}"));
3831
- config.features = { ...z13.record(z13.unknown()).parse(config.features ?? {}), ...features };
3920
+ const config = z14.record(z14.unknown()).parse(JSON.parse(before ?? "{}"));
3921
+ config.features = { ...z14.record(z14.unknown()).parse(config.features ?? {}), ...features };
3832
3922
  edits.push({ path: configPath2, before, after: JSON.stringify(config, null, 2) + "\n" });
3833
3923
  }
3834
3924
  return edits;
@@ -3842,19 +3932,19 @@ var init_init = __esm({
3842
3932
  "use strict";
3843
3933
  init_storage();
3844
3934
  init_files2();
3845
- markerSchema = z13.object({
3846
- version: z13.literal(1),
3847
- initializedAt: z13.string(),
3848
- features: z13.object({ confluence: z13.boolean().optional() }).passthrough().optional()
3935
+ markerSchema = z14.object({
3936
+ version: z14.literal(1),
3937
+ initializedAt: z14.string(),
3938
+ features: z14.object({ confluence: z14.boolean().optional() }).passthrough().optional()
3849
3939
  }).passthrough();
3850
3940
  CLAUDE_MD_SECTION = `<!-- mason:start -->
3851
3941
  ## Mason project knowledge
3852
3942
 
3853
3943
  Mason provides recorded decisions and file impact over MCP. A concept map is optional.
3854
3944
 
3855
- - Task, bug, or change request \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
3945
+ - Task, bug, change request, or investigation of why the project works this way \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
3856
3946
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
3857
- - Learned something the code cannot explain (a failed approach, an incident's cause, a workaround's reason, a review-settled convention) \u2192 \`save_decision\` with rationale, anchors, and any known owner, sources, and recorder. It creates a proposal immediately without setup or a map. Never invent attribution or record code-derivable facts, session trivia, or secrets.
3947
+ - Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` before finishing. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
3858
3948
  - Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, and failed verification means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
3859
3949
  - Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
3860
3950
  - For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
@@ -4039,7 +4129,7 @@ var init_types2 = __esm({
4039
4129
  });
4040
4130
 
4041
4131
  // src/review/evidence/vitest.ts
4042
- import { z as z14 } from "zod";
4132
+ import { z as z15 } from "zod";
4043
4133
  function parseVitest(raw, sourceRoot) {
4044
4134
  const report = schema.parse(raw);
4045
4135
  const findings = [], diagnostics = [];
@@ -4088,24 +4178,24 @@ var init_vitest = __esm({
4088
4178
  "use strict";
4089
4179
  init_paths2();
4090
4180
  init_types2();
4091
- count2 = z14.number().int().nonnegative();
4092
- schema = z14.object({
4093
- success: z14.boolean(),
4181
+ count2 = z15.number().int().nonnegative();
4182
+ schema = z15.object({
4183
+ success: z15.boolean(),
4094
4184
  numTotalTests: count2,
4095
4185
  numPassedTests: count2,
4096
4186
  numFailedTests: count2,
4097
4187
  numPendingTests: count2,
4098
4188
  numTodoTests: count2,
4099
4189
  numFailedTestSuites: count2,
4100
- testResults: z14.array(z14.object({
4101
- name: z14.string().min(1),
4102
- status: z14.enum(["passed", "failed"]),
4103
- message: z14.string().optional(),
4104
- assertionResults: z14.array(z14.object({
4105
- fullName: z14.string(),
4106
- status: z14.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4107
- failureMessages: z14.array(z14.string()).nullable().optional(),
4108
- location: z14.object({ line: count2, column: count2 }).nullable().optional()
4190
+ testResults: z15.array(z15.object({
4191
+ name: z15.string().min(1),
4192
+ status: z15.enum(["passed", "failed"]),
4193
+ message: z15.string().optional(),
4194
+ assertionResults: z15.array(z15.object({
4195
+ fullName: z15.string(),
4196
+ status: z15.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4197
+ failureMessages: z15.array(z15.string()).nullable().optional(),
4198
+ location: z15.object({ line: count2, column: count2 }).nullable().optional()
4109
4199
  }))
4110
4200
  }))
4111
4201
  });
@@ -4113,7 +4203,7 @@ var init_vitest = __esm({
4113
4203
  });
4114
4204
 
4115
4205
  // src/review/evidence/sarif.ts
4116
- import { z as z15 } from "zod";
4206
+ import { z as z16 } from "zod";
4117
4207
  function parseSarif(raw, sourceRoot) {
4118
4208
  const report = schema2.parse(raw), findings = [], diagnostics = [];
4119
4209
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -4204,29 +4294,29 @@ var init_sarif = __esm({
4204
4294
  "use strict";
4205
4295
  init_paths2();
4206
4296
  init_types2();
4207
- index = z15.number().int().nonnegative();
4208
- artifact = z15.object({ uri: z15.string().optional(), uriBaseId: z15.string().optional(), index: index.optional() });
4209
- message = z15.object({ text: z15.string().optional(), markdown: z15.string().optional(), id: z15.string().optional(), arguments: z15.array(z15.string()).optional() });
4210
- level = z15.enum(["error", "warning", "note", "none"]);
4211
- location = z15.object({ physicalLocation: z15.object({ artifactLocation: artifact.optional(), region: z15.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4212
- schema2 = z15.object({
4213
- version: z15.literal("2.1.0"),
4214
- runs: z15.array(z15.object({
4215
- tool: z15.object({ driver: z15.object({ name: z15.string().min(1), rules: z15.array(z15.object({ id: z15.string(), defaultConfiguration: z15.object({ level: level.optional() }).optional(), messageStrings: z15.record(message).optional() })).optional(), globalMessageStrings: z15.record(message).optional() }) }),
4216
- invocations: z15.array(z15.object({ executionSuccessful: z15.boolean(), commandLine: z15.string().optional(), toolExecutionNotifications: z15.array(z15.object({ level: level.optional(), message })).optional() })).optional(),
4217
- versionControlProvenance: z15.array(z15.object({ revisionId: z15.string().optional() })).optional(),
4218
- originalUriBaseIds: z15.record(artifact).optional(),
4219
- artifacts: z15.array(z15.object({ location: artifact.optional() })).optional(),
4220
- results: z15.array(z15.object({
4221
- ruleId: z15.string().optional(),
4297
+ index = z16.number().int().nonnegative();
4298
+ artifact = z16.object({ uri: z16.string().optional(), uriBaseId: z16.string().optional(), index: index.optional() });
4299
+ message = z16.object({ text: z16.string().optional(), markdown: z16.string().optional(), id: z16.string().optional(), arguments: z16.array(z16.string()).optional() });
4300
+ level = z16.enum(["error", "warning", "note", "none"]);
4301
+ location = z16.object({ physicalLocation: z16.object({ artifactLocation: artifact.optional(), region: z16.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4302
+ schema2 = z16.object({
4303
+ version: z16.literal("2.1.0"),
4304
+ runs: z16.array(z16.object({
4305
+ tool: z16.object({ driver: z16.object({ name: z16.string().min(1), rules: z16.array(z16.object({ id: z16.string(), defaultConfiguration: z16.object({ level: level.optional() }).optional(), messageStrings: z16.record(message).optional() })).optional(), globalMessageStrings: z16.record(message).optional() }) }),
4306
+ invocations: z16.array(z16.object({ executionSuccessful: z16.boolean(), commandLine: z16.string().optional(), toolExecutionNotifications: z16.array(z16.object({ level: level.optional(), message })).optional() })).optional(),
4307
+ versionControlProvenance: z16.array(z16.object({ revisionId: z16.string().optional() })).optional(),
4308
+ originalUriBaseIds: z16.record(artifact).optional(),
4309
+ artifacts: z16.array(z16.object({ location: artifact.optional() })).optional(),
4310
+ results: z16.array(z16.object({
4311
+ ruleId: z16.string().optional(),
4222
4312
  ruleIndex: index.optional(),
4223
4313
  message,
4224
4314
  level: level.optional(),
4225
- kind: z15.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4226
- baselineState: z15.enum(["new", "unchanged", "updated", "absent"]).optional(),
4227
- suppressions: z15.array(z15.object({ kind: z15.enum(["inSource", "external"]), status: z15.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4228
- locations: z15.array(location).optional(),
4229
- relatedLocations: z15.array(location).optional()
4315
+ kind: z16.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4316
+ baselineState: z16.enum(["new", "unchanged", "updated", "absent"]).optional(),
4317
+ suppressions: z16.array(z16.object({ kind: z16.enum(["inSource", "external"]), status: z16.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4318
+ locations: z16.array(location).optional(),
4319
+ relatedLocations: z16.array(location).optional()
4230
4320
  })).optional()
4231
4321
  }))
4232
4322
  });
@@ -4235,7 +4325,7 @@ var init_sarif = __esm({
4235
4325
 
4236
4326
  // src/review/evidence.ts
4237
4327
  import path21 from "path";
4238
- import { z as z16 } from "zod";
4328
+ import { z as z17 } from "zod";
4239
4329
  function relativeArtifact(root, file) {
4240
4330
  const relative = path21.isAbsolute(file) ? path21.relative(root, file) : file;
4241
4331
  const normalized = normalizeRepoPath(relative);
@@ -4276,7 +4366,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4276
4366
  let manifest, raw;
4277
4367
  try {
4278
4368
  manifest = relativeArtifact(root, supplied);
4279
- raw = z16.object({ version: z16.literal(1), checks: z16.array(z16.unknown()) }).parse(await readStoreJson(root, manifest));
4369
+ raw = z17.object({ version: z17.literal(1), checks: z17.array(z17.unknown()) }).parse(await readStoreJson(root, manifest));
4280
4370
  } catch (error) {
4281
4371
  output.diagnostics.push(`${supplied}: ${String(error)}`);
4282
4372
  continue;
@@ -4429,19 +4519,19 @@ var init_evidence2 = __esm({
4429
4519
  init_vitest();
4430
4520
  init_sarif();
4431
4521
  init_types2();
4432
- checkSchema2 = z16.object({
4433
- id: z16.string().min(1).max(100),
4434
- kind: z16.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4435
- tool: z16.string().min(1).max(200),
4436
- command: z16.string().min(1).max(2e3),
4437
- commit: z16.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4438
- workingTreeClean: z16.boolean().optional(),
4439
- source: z16.string().max(2e3).optional(),
4440
- sourceRoot: z16.string().min(1).max(4e3).optional(),
4441
- status: z16.enum(["completed", "skipped", "unavailable"]).default("completed"),
4442
- reason: z16.string().min(1).max(2e3).optional(),
4443
- exitCode: z16.number().int().nullable().optional(),
4444
- report: z16.object({ format: z16.enum(["vitest-json", "sarif"]), path: z16.string().min(1).max(4e3) }).optional()
4522
+ checkSchema2 = z17.object({
4523
+ id: z17.string().min(1).max(100),
4524
+ kind: z17.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4525
+ tool: z17.string().min(1).max(200),
4526
+ command: z17.string().min(1).max(2e3),
4527
+ commit: z17.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4528
+ workingTreeClean: z17.boolean().optional(),
4529
+ source: z17.string().max(2e3).optional(),
4530
+ sourceRoot: z17.string().min(1).max(4e3).optional(),
4531
+ status: z17.enum(["completed", "skipped", "unavailable"]).default("completed"),
4532
+ reason: z17.string().min(1).max(2e3).optional(),
4533
+ exitCode: z17.number().int().nullable().optional(),
4534
+ report: z17.object({ format: z17.enum(["vitest-json", "sarif"]), path: z17.string().min(1).max(4e3) }).optional()
4445
4535
  });
4446
4536
  }
4447
4537
  });
@@ -4661,16 +4751,204 @@ var init_onboarding = __esm({
4661
4751
  }
4662
4752
  });
4663
4753
 
4754
+ // src/impact/references.ts
4755
+ import path23 from "path";
4756
+ function templateEnd(content2, start) {
4757
+ let i = start + 1, interpolation = 0;
4758
+ while (i < content2.length) {
4759
+ const c = content2[i];
4760
+ if (c === "\\") {
4761
+ i += 2;
4762
+ continue;
4763
+ }
4764
+ if (!interpolation) {
4765
+ if (c === "`") return i + 1;
4766
+ if (c === "$" && content2[i + 1] === "{") {
4767
+ interpolation = 1;
4768
+ i += 2;
4769
+ continue;
4770
+ }
4771
+ } else {
4772
+ if (c === "`") {
4773
+ i = templateEnd(content2, i);
4774
+ continue;
4775
+ }
4776
+ if (c === '"' || c === "'") {
4777
+ const quote = c;
4778
+ for (i++; i < content2.length; i++) {
4779
+ if (content2[i] === "\\") i++;
4780
+ else if (content2[i] === quote) {
4781
+ i++;
4782
+ break;
4783
+ }
4784
+ }
4785
+ continue;
4786
+ }
4787
+ if (content2.startsWith("//", i)) {
4788
+ const end = content2.indexOf("\n", i + 2);
4789
+ i = end < 0 ? content2.length : end;
4790
+ continue;
4791
+ }
4792
+ if (content2.startsWith("/*", i)) {
4793
+ const end = content2.indexOf("*/", i + 2);
4794
+ i = end < 0 ? content2.length : end + 2;
4795
+ continue;
4796
+ }
4797
+ if (c === "{") interpolation++;
4798
+ if (c === "}") interpolation--;
4799
+ }
4800
+ i++;
4801
+ }
4802
+ return content2.length;
4803
+ }
4804
+ function tokens(content2) {
4805
+ const result = [];
4806
+ const re = /\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$)|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|[\w$]+|[^\s]/g;
4807
+ let match;
4808
+ while (match = re.exec(content2)) {
4809
+ const value = match[0];
4810
+ if (value.startsWith("//") || value.startsWith("/*")) continue;
4811
+ if (/^["'`]/.test(value)) {
4812
+ if (value[0] === "`") {
4813
+ re.lastIndex = templateEnd(content2, match.index);
4814
+ result.push({ value: "<opaque>" });
4815
+ continue;
4816
+ }
4817
+ result.push({ value: value.slice(1, -1), string: !value.includes("\\") && value[0] !== "`" });
4818
+ if (!result.at(-1).string) result[result.length - 1] = { value: "<opaque>" };
4819
+ } else result.push({ value });
4820
+ }
4821
+ return result;
4822
+ }
4823
+ function imports(content2) {
4824
+ const ts = tokens(content2), found = [];
4825
+ for (let i = 0; i < ts.length; i++) {
4826
+ const t = ts[i];
4827
+ if (t.string || ts[i - 1]?.value === ".") continue;
4828
+ if (["require", "import"].includes(t.value) && ts[i + 1]?.value === "(" && ts[i + 2]?.string && ts[i + 3]?.value === ")") {
4829
+ found.push(ts[i + 2].value);
4830
+ }
4831
+ if (!["import", "export"].includes(t.value) || ts[i + 1]?.value === "(") continue;
4832
+ if (t.value === "import" && ts[i + 1]?.string) {
4833
+ found.push(ts[i + 1].value);
4834
+ continue;
4835
+ }
4836
+ for (let j = i + 1; j < Math.min(ts.length, i + 150); j++) {
4837
+ if ([";", "=", "import", "export"].includes(ts[j].value) && !ts[j].string) break;
4838
+ if (ts[j].value === "from" && !ts[j].string && ts[j + 1]?.string) {
4839
+ found.push(ts[j + 1].value);
4840
+ break;
4841
+ }
4842
+ }
4843
+ }
4844
+ return found;
4845
+ }
4846
+ function qualifiedName(ts, start) {
4847
+ let name = "";
4848
+ for (let i = start; i < ts.length; i++) {
4849
+ if (ts[i].string || !/^[A-Za-z_$][\w$]*$/.test(ts[i].value)) break;
4850
+ name += ts[i].value;
4851
+ if (ts[i + 1]?.value !== "." || !/^[A-Za-z_$][\w$]*$/.test(ts[i + 2]?.value ?? "")) break;
4852
+ name += ".";
4853
+ i++;
4854
+ }
4855
+ return name;
4856
+ }
4857
+ function declaredSymbols(content2) {
4858
+ const ts = tokens(content2);
4859
+ const packageAt = ts.findIndex((t) => !t.string && t.value === "package");
4860
+ if (packageAt < 0) return [];
4861
+ const pkg = qualifiedName(ts, packageAt + 1), names = [];
4862
+ let depth = 0;
4863
+ for (let i = 0; i < ts.length; i++) {
4864
+ if (ts[i].string) continue;
4865
+ if (ts[i].value === "{") depth++;
4866
+ if (ts[i].value === "}") depth--;
4867
+ if (depth === 0 && ["class", "interface", "object", "enum", "typealias", "fun"].includes(ts[i].value) && /^[A-Za-z_$][\w$]*$/.test(ts[i + 1]?.value ?? "") && ts[i + 1].value !== "class") names.push(pkg + "." + ts[i + 1].value);
4868
+ }
4869
+ return names;
4870
+ }
4871
+ function jvmImports(content2) {
4872
+ const ts = tokens(content2), result = [];
4873
+ for (let i = 0; i < ts.length; i++) if (!ts[i].string && ts[i].value === "import") {
4874
+ const name = qualifiedName(ts, i + (ts[i + 1]?.value === "static" ? 2 : 1));
4875
+ if (name) result.push(name);
4876
+ }
4877
+ return result;
4878
+ }
4879
+ function resolveImport(file, specifier, available) {
4880
+ if (!specifier.startsWith("./") && !specifier.startsWith("../")) return null;
4881
+ const relative = path23.posix.normalize(path23.posix.join(path23.posix.dirname(file), specifier));
4882
+ if (relative.startsWith("../")) return null;
4883
+ if (available.has(relative)) return relative;
4884
+ const ext = path23.posix.extname(relative);
4885
+ const replacements = { ".js": [".ts", ".tsx"], ".mjs": [".mts"], ".cjs": [".cts"] };
4886
+ const candidates = ext ? (replacements[ext] ?? []).map((e) => relative.slice(0, -ext.length) + e) : jsExtensions.flatMap((e) => [relative + e, relative + "/index" + e]);
4887
+ const found = candidates.filter((f) => available.has(f));
4888
+ return found.length === 1 ? found[0] : null;
4889
+ }
4890
+ function mentionsPath(content2, target) {
4891
+ return new RegExp(`(?<![\\w./\\\\-])(?:\\./)?${escape(target)}(?![\\w./\\\\-])`).test(content2);
4892
+ }
4893
+ function createReferenceMatcher(targets, available, targetContents = /* @__PURE__ */ new Map()) {
4894
+ const counts = /* @__PURE__ */ new Map();
4895
+ for (const file of available) {
4896
+ const name = path23.posix.basename(file, path23.posix.extname(file));
4897
+ counts.set(name, (counts.get(name) ?? 0) + 1);
4898
+ }
4899
+ const jvmTargets = new Map([...targetContents].filter(([file]) => /\.(?:kt|kts|java)$/.test(file)).map(([file, content2]) => [file, declaredSymbols(content2)]));
4900
+ return (source) => {
4901
+ if (targets.includes(source.path)) return [];
4902
+ const resolved = new Set(jsExtensions.includes(path23.posix.extname(source.path)) ? imports(source.content).map((s) => resolveImport(source.path, s, available)).filter(Boolean) : []);
4903
+ if (/\.(?:kt|kts|java)$/.test(source.path)) {
4904
+ const imported = jvmImports(source.content);
4905
+ for (const [target, symbols] of jvmTargets) if (symbols.some((symbol) => imported.some((name) => name === symbol || name.startsWith(symbol + ".")))) resolved.add(target);
4906
+ }
4907
+ const groups = /* @__PURE__ */ new Map();
4908
+ const add = (evidence, match) => groups.set(evidence, [...groups.get(evidence) ?? [], match]);
4909
+ for (const target of targets) {
4910
+ if (resolved.has(target)) {
4911
+ add("resolved-import", target);
4912
+ continue;
4913
+ }
4914
+ if (mentionsPath(source.content, target)) {
4915
+ add("explicit-path", target);
4916
+ continue;
4917
+ }
4918
+ const ext = path23.posix.extname(target), name = path23.posix.basename(target, ext);
4919
+ if (!/\.(?:[cm]?[jt]sx?|py|kt|java|go|rs|swift|rb|cs|cpp|c|h|hpp|dart|php)$/.test(ext) || name.startsWith(".") || name.length < 3 || genericNames.has(name.toLowerCase()) || counts.get(name) !== 1) continue;
4920
+ if (word(name).test(source.content)) {
4921
+ add("name-candidate", name);
4922
+ }
4923
+ }
4924
+ return [...groups].map(([evidence, matches]) => ({ file: source.path, matches, kind: evidence === "resolved-import" ? "import" : "mention", evidence }));
4925
+ };
4926
+ }
4927
+ function sortReferences(result) {
4928
+ const rank = { "resolved-import": 0, "explicit-path": 1, "name-candidate": 2 };
4929
+ return result.sort((a, b) => rank[a.evidence] - rank[b.evidence] || b.matches.length - a.matches.length || a.file.localeCompare(b.file));
4930
+ }
4931
+ var jsExtensions, genericNames, escape, word;
4932
+ var init_references = __esm({
4933
+ "src/impact/references.ts"() {
4934
+ "use strict";
4935
+ jsExtensions = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
4936
+ genericNames = /* @__PURE__ */ new Set(["index", "main", "app", "init", "config", "settings", "types", "utils", "test", "tests"]);
4937
+ escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4938
+ word = (s) => new RegExp(`(?<![\\w$])${escape(s)}(?![\\w$])`);
4939
+ }
4940
+ });
4941
+
4664
4942
  // src/impact/impact.ts
4665
4943
  var impact_exports = {};
4666
4944
  __export(impact_exports, {
4667
4945
  analyzeImpact: () => analyzeImpact
4668
4946
  });
4669
- import path23 from "path";
4947
+ import path24 from "path";
4670
4948
  import { execFile as execFile16 } from "child_process";
4671
4949
  import { promisify as promisify16 } from "util";
4672
4950
  async function analyzeImpact(rootDir, targetFiles) {
4673
- const resolvedRoot = path23.resolve(rootDir);
4951
+ const resolvedRoot = path24.resolve(rootDir);
4674
4952
  const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
4675
4953
  const [cochange, references, tests] = await Promise.all([
4676
4954
  getCochangeFiles(resolvedRoot, resolvedTargets),
@@ -4747,52 +5025,24 @@ async function getCochangeFiles(rootDir, targetFiles) {
4747
5025
  })).filter((e) => e.cochangeRate >= 0.3 || e.sharedCommits >= 3).sort((a, b) => b.cochangeRate - a.cochangeRate).slice(0, 20);
4748
5026
  }
4749
5027
  async function getReferences(rootDir, targetFiles) {
4750
- const searchNames = /* @__PURE__ */ new Set();
4751
- for (const target of targetFiles) {
4752
- const basename = path23.basename(target).replace(/\.[^.]+$/, "");
4753
- searchNames.add(basename);
4754
- }
4755
5028
  const access = await createFileAccess(rootDir);
4756
5029
  const allSourceFiles = await access.list(SOURCE_GLOB);
4757
- const targetSet = new Set(targetFiles);
4758
- const filesToSearch = allSourceFiles.filter((f) => !targetSet.has(f));
4759
- const results = /* @__PURE__ */ new Map();
4760
- const importLine = /^\s*(import\b|from\b.*\bimport\b|const\b.*=\s*require\(|use\b|#include\b|require\s*\()/;
4761
- const batchSize = 50;
4762
- for (let i = 0; i < filesToSearch.length; i += batchSize) {
4763
- const batch = filesToSearch.slice(i, i + batchSize);
4764
- await Promise.all(
4765
- batch.map(async (file) => {
4766
- try {
4767
- const full = await access.read(file);
4768
- if (!full) return;
4769
- const content2 = full.content;
4770
- const lines = content2.split("\n");
4771
- for (const name of searchNames) {
4772
- const regex = new RegExp(`\\b${escapeRegex(name)}\\b`);
4773
- if (!regex.test(content2)) continue;
4774
- if (!results.has(file)) {
4775
- results.set(file, { matches: /* @__PURE__ */ new Set(), isImport: false });
4776
- }
4777
- const entry = results.get(file);
4778
- entry.matches.add(name);
4779
- if (!entry.isImport && lines.some((l) => regex.test(l) && importLine.test(l))) {
4780
- entry.isImport = true;
4781
- }
4782
- }
4783
- } catch {
4784
- }
4785
- })
4786
- );
5030
+ const available = new Set(allSourceFiles);
5031
+ const targetContents = /* @__PURE__ */ new Map();
5032
+ for (const target of targetFiles) {
5033
+ const source = await access.read(target);
5034
+ if (source) {
5035
+ available.add(target);
5036
+ targetContents.set(target, source.content);
5037
+ }
4787
5038
  }
4788
- return [...results.entries()].map(([file, { matches, isImport }]) => ({
4789
- file,
4790
- matches: [...matches],
4791
- kind: isImport ? "import" : "mention"
4792
- })).sort((a, b) => {
4793
- if (a.kind !== b.kind) return a.kind === "import" ? -1 : 1;
4794
- return b.matches.length - a.matches.length;
4795
- });
5039
+ const match = createReferenceMatcher(targetFiles, available, targetContents);
5040
+ const references = [];
5041
+ for (let i = 0; i < allSourceFiles.length; i += 50) {
5042
+ const batch = await Promise.all(allSourceFiles.slice(i, i + 50).map((file) => access.read(file)));
5043
+ for (const source of batch) if (source) references.push(...match(source));
5044
+ }
5045
+ return sortReferences(references);
4796
5046
  }
4797
5047
  async function getRelatedTests(rootDir, targetFiles) {
4798
5048
  const testPatterns = [
@@ -4812,9 +5062,9 @@ async function getRelatedTests(rootDir, targetFiles) {
4812
5062
  const testFiles = await (await createFileAccess(rootDir)).list(testPatterns);
4813
5063
  const results = [];
4814
5064
  for (const target of targetFiles) {
4815
- const targetBaseName = path23.basename(target).replace(/\.[^.]+$/, "");
5065
+ const targetBaseName = path24.basename(target).replace(/\.[^.]+$/, "");
4816
5066
  for (const testFile of testFiles) {
4817
- const testBaseName = path23.basename(testFile).replace(/\.[^.]+$/, "");
5067
+ const testBaseName = path24.basename(testFile).replace(/\.[^.]+$/, "");
4818
5068
  const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
4819
5069
  if (sourceName === targetBaseName) {
4820
5070
  results.push({
@@ -4826,15 +5076,13 @@ async function getRelatedTests(rootDir, targetFiles) {
4826
5076
  }
4827
5077
  return results;
4828
5078
  }
4829
- function escapeRegex(str) {
4830
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4831
- }
4832
5079
  var exec14;
4833
5080
  var init_impact = __esm({
4834
5081
  "src/impact/impact.ts"() {
4835
5082
  "use strict";
4836
5083
  init_files();
4837
5084
  init_paths();
5085
+ init_references();
4838
5086
  exec14 = promisify16(execFile16);
4839
5087
  }
4840
5088
  });
@@ -4844,13 +5092,13 @@ var assemble_exports = {};
4844
5092
  __export(assemble_exports, {
4845
5093
  assembleContext: () => assembleContext
4846
5094
  });
4847
- import path24 from "path";
5095
+ import path25 from "path";
4848
5096
  import fs17 from "fs/promises";
4849
5097
  async function collectImpact(root, candidates) {
4850
5098
  const targets = /* @__PURE__ */ new Set();
4851
5099
  let sourceFiles;
4852
5100
  for (const candidate of sanitizeRepoPaths(candidates)) {
4853
- const stat = await fs17.stat(path24.join(root, candidate)).catch(() => null);
5101
+ const stat = await fs17.stat(path25.join(root, candidate)).catch(() => null);
4854
5102
  if (stat?.isDirectory()) {
4855
5103
  sourceFiles ??= await (await createFileAccess(root)).list();
4856
5104
  for (const file of sourceFiles) {
@@ -4864,13 +5112,19 @@ async function collectImpact(root, candidates) {
4864
5112
  }
4865
5113
  if (!targets.size) return { impact: null, relatedTests: [] };
4866
5114
  const result = await analyzeImpact(root, [...targets]);
5115
+ const referenceCandidatesOmitted = result.references.filter((r) => r.evidence === "name-candidate").length;
4867
5116
  return {
4868
- impact: { targets: result.targetFiles, cochange: result.cochange, references: result.references.slice(0, 10) },
5117
+ impact: {
5118
+ targets: result.targetFiles,
5119
+ cochange: result.cochange,
5120
+ references: result.references.filter((r) => r.evidence !== "name-candidate").slice(0, 10),
5121
+ ...referenceCandidatesOmitted ? { referenceCandidatesOmitted } : {}
5122
+ },
4869
5123
  relatedTests: [...new Set(result.tests.map((t) => t.file))]
4870
5124
  };
4871
5125
  }
4872
5126
  async function assembleContext(rootDir, task, files) {
4873
- const resolvedRoot = path24.resolve(rootDir);
5127
+ const resolvedRoot = path25.resolve(rootDir);
4874
5128
  const mapState = await inspectSnapshot(resolvedRoot);
4875
5129
  const snapshot = mapState.snapshot;
4876
5130
  const store = await loadDecisionStore(resolvedRoot);
@@ -5072,7 +5326,7 @@ __export(observations_exports, {
5072
5326
  readObservation: () => readObservation
5073
5327
  });
5074
5328
  import fs18 from "fs/promises";
5075
- import { z as z17 } from "zod";
5329
+ import { z as z18 } from "zod";
5076
5330
  function observationPath(directory, host, revision, version = executingVersion()) {
5077
5331
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
5078
5332
  }
@@ -5137,19 +5391,19 @@ var init_observations = __esm({
5137
5391
  init_storage();
5138
5392
  init_model();
5139
5393
  init_launcher();
5140
- observationSchema = z17.object({
5141
- version: z17.literal(1),
5142
- masonVersion: z17.string(),
5143
- root: z17.string(),
5144
- revision: z17.string(),
5145
- host: z17.enum(["codex", "claude"]),
5146
- contextCalls: z17.number().int().nonnegative(),
5147
- lastContextAt: z17.string().optional(),
5148
- sessions: z17.record(z17.object({
5149
- events: z17.array(z17.enum(events)),
5150
- at: z17.string(),
5151
- verificationStatus: z17.string().optional(),
5152
- reportPath: z17.string().optional()
5394
+ observationSchema = z18.object({
5395
+ version: z18.literal(1),
5396
+ masonVersion: z18.string(),
5397
+ root: z18.string(),
5398
+ revision: z18.string(),
5399
+ host: z18.enum(["codex", "claude"]),
5400
+ contextCalls: z18.number().int().nonnegative(),
5401
+ lastContextAt: z18.string().optional(),
5402
+ sessions: z18.record(z18.object({
5403
+ events: z18.array(z18.enum(events)),
5404
+ at: z18.string(),
5405
+ verificationStatus: z18.string().optional(),
5406
+ reportPath: z18.string().optional()
5153
5407
  }))
5154
5408
  });
5155
5409
  }
@@ -5289,6 +5543,8 @@ async function setupProject(dir, options = {}) {
5289
5543
  const setupBefore = await readText(ws.root, SETUP_PATH);
5290
5544
  const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after)).mcp);
5291
5545
  const desiredHooks = hookConfig(host, hookCommand(host)).hooks;
5546
+ const configuredHook = desiredHooks.SessionStart[0].hooks[0].command;
5547
+ const ownership = await ownershipEdit(ws.root, [...instructions, mcp, hooks[0]], { [hooks[0].path]: configuredHook });
5292
5548
  const fingerprint = hash({
5293
5549
  command: "mason",
5294
5550
  mcp: mcpFingerprint,
@@ -5315,8 +5571,10 @@ async function setupProject(dir, options = {}) {
5315
5571
  revision
5316
5572
  });
5317
5573
  const changedFiles = [];
5574
+ if (await applyEdit(ws.root, ownership)) changedFiles.push(ownership.path);
5318
5575
  for (const edit of edits) if (await applyEdit(ws.root, edit)) changedFiles.push(edit.path);
5319
5576
  if (await applyEdit(ws.root, { path: SETUP_PATH, before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(SETUP_PATH);
5577
+ if (await applyEdit(ws.root, await completedHookOwnership(ws.root, hooks[0].path, configuredHook)) && !changedFiles.includes(ownership.path)) changedFiles.push(ownership.path);
5320
5578
  const checked = await automate(ws.root, { event: "turn_start" });
5321
5579
  const configured = await inspectHostConfig(ws.root, host, void 0);
5322
5580
  await writeStoreJson(ws.root, receiptPath, {
@@ -5375,6 +5633,7 @@ var init_setup = __esm({
5375
5633
  init_adapters();
5376
5634
  init_model();
5377
5635
  init_status();
5636
+ init_ownership();
5378
5637
  }
5379
5638
  });
5380
5639
 
@@ -5386,8 +5645,8 @@ __export(client_exports, {
5386
5645
  function createConfluenceClient(config, fetchFn = fetch) {
5387
5646
  const baseUrl = config.baseUrl.replace(/\/+$/, "");
5388
5647
  const auth = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
5389
- async function call(method, path28, body) {
5390
- const res = await fetchFn(`${baseUrl}${path28}`, {
5648
+ async function call(method, path29, body) {
5649
+ const res = await fetchFn(`${baseUrl}${path29}`, {
5391
5650
  method,
5392
5651
  headers: {
5393
5652
  Authorization: auth,
@@ -5399,7 +5658,7 @@ function createConfluenceClient(config, fetchFn = fetch) {
5399
5658
  if (!res.ok) {
5400
5659
  const text2 = await res.text();
5401
5660
  throw new Error(
5402
- `Confluence ${method} ${path28} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
5661
+ `Confluence ${method} ${path29} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
5403
5662
  );
5404
5663
  }
5405
5664
  if (res.status === 204) return null;
@@ -5501,15 +5760,15 @@ __export(config_exports2, {
5501
5760
  validateProvider: () => validateProvider
5502
5761
  });
5503
5762
  import fs19 from "fs/promises";
5504
- import path25 from "path";
5763
+ import path26 from "path";
5505
5764
  import os3 from "os";
5506
5765
  import { execFile as execFile17 } from "child_process";
5507
5766
  import { promisify as promisify17 } from "util";
5508
5767
  function configDir() {
5509
- return path25.join(os3.homedir(), ".mason");
5768
+ return path26.join(os3.homedir(), ".mason");
5510
5769
  }
5511
5770
  function configFile() {
5512
- return path25.join(configDir(), "config.json");
5771
+ return path26.join(configDir(), "config.json");
5513
5772
  }
5514
5773
  async function loadConfig() {
5515
5774
  try {
@@ -5589,21 +5848,21 @@ var init_url = __esm({
5589
5848
  });
5590
5849
 
5591
5850
  // src/confluence/renderer.ts
5592
- function escape(value) {
5851
+ function escape2(value) {
5593
5852
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5594
5853
  }
5595
5854
  function infoPanel(text2) {
5596
- return `<ac:structured-macro ac:name="info"><ac:rich-text-body><p>${escape(text2)}</p></ac:rich-text-body></ac:structured-macro>`;
5855
+ return `<ac:structured-macro ac:name="info"><ac:rich-text-body><p>${escape2(text2)}</p></ac:rich-text-body></ac:structured-macro>`;
5597
5856
  }
5598
5857
  function featurePageTitle(prefix, name) {
5599
5858
  return `${prefix}${name}`;
5600
5859
  }
5601
5860
  function renderFeaturePage(options) {
5602
- const overviewBody = `<h2>What it does</h2><p>${escape(options.productDescription)}</p>`;
5861
+ const overviewBody = `<h2>What it does</h2><p>${escape2(options.productDescription)}</p>`;
5603
5862
  const flowsBody = options.flowDescriptions.length ? `<h2>How it fits in</h2><ul>` + options.flowDescriptions.map(
5604
- (f) => `<li><strong>${escape(f.name)}</strong> \u2014 ${escape(f.description)}</li>`
5863
+ (f) => `<li><strong>${escape2(f.name)}</strong> \u2014 ${escape2(f.description)}</li>`
5605
5864
  ).join("") + `</ul>` : "";
5606
- const navBody = `<p><ac:link><ri:page ri:content-title="${escape(options.indexPageTitle)}"/><ac:plain-text-link-body><![CDATA[Back to ${options.indexPageTitle}]]></ac:plain-text-link-body></ac:link></p>`;
5865
+ const navBody = `<p><ac:link><ri:page ri:content-title="${escape2(options.indexPageTitle)}"/><ac:plain-text-link-body><![CDATA[Back to ${options.indexPageTitle}]]></ac:plain-text-link-body></ac:link></p>`;
5607
5866
  const footer = infoPanel(
5608
5867
  `Generated from code by Mason. This page is overwritten on each sync \u2014 edit the code, not the page.`
5609
5868
  );
@@ -5619,7 +5878,7 @@ function renderIndexPage(options) {
5619
5878
  }
5620
5879
  const list = `<h2>Features</h2><ul>` + options.featureTitles.map((name) => {
5621
5880
  const pageTitle = featurePageTitle(options.featurePrefix, name);
5622
- return `<li><ac:link><ri:page ri:content-title="${escape(pageTitle)}"/><ac:plain-text-link-body><![CDATA[${name}]]></ac:plain-text-link-body></ac:link></li>`;
5881
+ return `<li><ac:link><ri:page ri:content-title="${escape2(pageTitle)}"/><ac:plain-text-link-body><![CDATA[${name}]]></ac:plain-text-link-body></ac:link></li>`;
5623
5882
  }).join("") + `</ul>`;
5624
5883
  const banner = infoPanel(
5625
5884
  `Generated from code by Mason. Maintained automatically \u2014 edit the code, not this page.`
@@ -5630,33 +5889,33 @@ function renderChangelogSection(section) {
5630
5889
  const segments = [];
5631
5890
  if (section.addedFeatures.length) {
5632
5891
  segments.push(
5633
- `<p><strong>Added features:</strong> ${section.addedFeatures.map(escape).join(", ")}</p>`
5892
+ `<p><strong>Added features:</strong> ${section.addedFeatures.map(escape2).join(", ")}</p>`
5634
5893
  );
5635
5894
  }
5636
5895
  if (section.removedFeatures.length) {
5637
5896
  segments.push(
5638
- `<p><strong>Removed features:</strong> ${section.removedFeatures.map(escape).join(", ")}</p>`
5897
+ `<p><strong>Removed features:</strong> ${section.removedFeatures.map(escape2).join(", ")}</p>`
5639
5898
  );
5640
5899
  }
5641
5900
  if (section.changedFeatures.length) {
5642
5901
  segments.push(
5643
- `<p><strong>Updated features:</strong> ${section.changedFeatures.map(escape).join(", ")}</p>`
5902
+ `<p><strong>Updated features:</strong> ${section.changedFeatures.map(escape2).join(", ")}</p>`
5644
5903
  );
5645
5904
  }
5646
5905
  if (section.addedFlows.length) {
5647
5906
  segments.push(
5648
- `<p><strong>Added flows:</strong> ${section.addedFlows.map(escape).join(", ")}</p>`
5907
+ `<p><strong>Added flows:</strong> ${section.addedFlows.map(escape2).join(", ")}</p>`
5649
5908
  );
5650
5909
  }
5651
5910
  if (section.removedFlows.length) {
5652
5911
  segments.push(
5653
- `<p><strong>Removed flows:</strong> ${section.removedFlows.map(escape).join(", ")}</p>`
5912
+ `<p><strong>Removed flows:</strong> ${section.removedFlows.map(escape2).join(", ")}</p>`
5654
5913
  );
5655
5914
  }
5656
5915
  if (segments.length === 0) {
5657
5916
  segments.push(`<p><em>No meaningful changes detected.</em></p>`);
5658
5917
  }
5659
- return `<h3>${escape(section.syncedAt)}</h3>` + segments.join("");
5918
+ return `<h3>${escape2(section.syncedAt)}</h3>` + segments.join("");
5660
5919
  }
5661
5920
  function renderChangelogPage(sections) {
5662
5921
  if (sections.length === 0) {
@@ -5682,16 +5941,16 @@ var init_renderer = __esm({
5682
5941
 
5683
5942
  // src/confluence/diff.ts
5684
5943
  import fs20 from "fs/promises";
5685
- import path26 from "path";
5944
+ import path27 from "path";
5686
5945
  import { createHash as createHash5 } from "crypto";
5687
5946
  function hashDescription(description) {
5688
5947
  return createHash5("sha256").update(description, "utf8").digest("hex");
5689
5948
  }
5690
5949
  function syncStateDir(rootDir) {
5691
- return path26.join(rootDir, ".mason");
5950
+ return path27.join(rootDir, ".mason");
5692
5951
  }
5693
5952
  function syncStatePath(rootDir) {
5694
- return path26.join(syncStateDir(rootDir), "confluence-sync.json");
5953
+ return path27.join(syncStateDir(rootDir), "confluence-sync.json");
5695
5954
  }
5696
5955
  async function loadSyncState(rootDir) {
5697
5956
  try {
@@ -6255,12 +6514,12 @@ var init_sync = __esm({
6255
6514
  init_provenance();
6256
6515
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6257
6516
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6258
- import { z as z18 } from "zod";
6517
+ import { z as z19 } from "zod";
6259
6518
 
6260
6519
  // src/mcp/tools.ts
6261
6520
  init_repair();
6262
6521
  import fs21 from "fs/promises";
6263
- import path27 from "path";
6522
+ import path28 from "path";
6264
6523
  import { execFile as execFile19 } from "child_process";
6265
6524
  import { promisify as promisify19 } from "util";
6266
6525
 
@@ -7155,7 +7414,7 @@ async function buildContext(dir) {
7155
7414
  };
7156
7415
  }
7157
7416
  async function analyzeProject(dir) {
7158
- const rootDir = path27.resolve(dir);
7417
+ const rootDir = path28.resolve(dir);
7159
7418
  const context = await buildContext(rootDir);
7160
7419
  const results = await runAll(context);
7161
7420
  const projectSnapshot = await detectProjectSnapshot(rootDir);
@@ -7210,7 +7469,7 @@ async function detectProjectSnapshot(rootDir) {
7210
7469
  const present = [];
7211
7470
  for (const file of buildFiles) {
7212
7471
  try {
7213
- await fs21.access(path27.join(rootDir, file));
7472
+ await fs21.access(path28.join(rootDir, file));
7214
7473
  present.push(file);
7215
7474
  } catch {
7216
7475
  }
@@ -7252,7 +7511,7 @@ async function detectProjectSnapshot(rootDir) {
7252
7511
  const sourceFiles = await access.list();
7253
7512
  const fileCounts = {};
7254
7513
  for (const file of sourceFiles) {
7255
- const ext = path27.extname(file).slice(1);
7514
+ const ext = path28.extname(file).slice(1);
7256
7515
  fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
7257
7516
  }
7258
7517
  return {
@@ -7263,7 +7522,7 @@ async function detectProjectSnapshot(rootDir) {
7263
7522
  };
7264
7523
  }
7265
7524
  async function getCodeSamples(dir, count3 = 15) {
7266
- const rootDir = path27.resolve(dir);
7525
+ const rootDir = path28.resolve(dir);
7267
7526
  const samples = await sampleFiles(rootDir, count3);
7268
7527
  const output = {
7269
7528
  note: "These are previews (first ~60 lines). Read the file directly with your own tools to see it in full.",
@@ -7306,7 +7565,7 @@ async function unmappedContextResponse(rootDir) {
7306
7565
  });
7307
7566
  }
7308
7567
  async function getProjectStructure(dir) {
7309
- const rootDir = path27.resolve(dir);
7568
+ const rootDir = path28.resolve(dir);
7310
7569
  const allFiles = await (await createFileAccess(rootDir)).list("**/*");
7311
7570
  const dirInfo = /* @__PURE__ */ new Map();
7312
7571
  for (const file of allFiles) {
@@ -7318,7 +7577,7 @@ async function getProjectStructure(dir) {
7318
7577
  }
7319
7578
  const info = dirInfo.get(dirPath);
7320
7579
  info.fileCount++;
7321
- const ext = path27.extname(file).slice(1);
7580
+ const ext = path28.extname(file).slice(1);
7322
7581
  if (ext) {
7323
7582
  info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
7324
7583
  }
@@ -7363,7 +7622,7 @@ async function buildChangedFilePreviews(rootDir, changedFiles) {
7363
7622
  return previews2;
7364
7623
  }
7365
7624
  async function getSnapshot(dir) {
7366
- const rootDir = path27.resolve(dir);
7625
+ const rootDir = path28.resolve(dir);
7367
7626
  const snapshot = await loadSnapshot(rootDir);
7368
7627
  if (!snapshot) {
7369
7628
  return unmappedContextResponse(rootDir);
@@ -7464,7 +7723,7 @@ function driftHint(report) {
7464
7723
  return "Read the changed files under staleFeatures/staleFlows, update those entries (fold unmappedFiles into the right features), and call save_snapshot with only the affected entries \u2014 unchanged entries are preserved. Drop ghostFiles from any entries that reference them, and delete features/flows that no longer exist via save_snapshot's removeFeatures/removeFlows.";
7465
7724
  }
7466
7725
  async function checkDrift(dir) {
7467
- const rootDir = path27.resolve(dir);
7726
+ const rootDir = path28.resolve(dir);
7468
7727
  const report = await computeDrift(rootDir);
7469
7728
  if (!report) {
7470
7729
  return JSON.stringify({
@@ -7495,7 +7754,7 @@ async function checkDrift(dir) {
7495
7754
  return JSON.stringify({ exists: true, ...report, verification, hint });
7496
7755
  }
7497
7756
  async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_SIZE, files) {
7498
- const rootDir = path27.resolve(dir);
7757
+ const rootDir = path28.resolve(dir);
7499
7758
  const scoped = files !== void 0 && files.length > 0;
7500
7759
  const scopeFiles = scoped ? sanitizePaths(rootDir, files) : void 0;
7501
7760
  const batch = await prepareSnapshotBatch(dir, offset, batchSize, scopeFiles);
@@ -7542,7 +7801,7 @@ async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_
7542
7801
  );
7543
7802
  }
7544
7803
  async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
7545
- const rootDir = path27.resolve(dir);
7804
+ const rootDir = path28.resolve(dir);
7546
7805
  for (const feat of Object.values(features)) {
7547
7806
  feat.files = sanitizePaths(rootDir, feat.files);
7548
7807
  if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
@@ -7571,7 +7830,7 @@ async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
7571
7830
  );
7572
7831
  }
7573
7832
  async function reduceSnapshot(dir) {
7574
- const rootDir = path27.resolve(dir);
7833
+ const rootDir = path28.resolve(dir);
7575
7834
  const partials = await loadAllPartials(rootDir);
7576
7835
  if (partials.length === 0) {
7577
7836
  return JSON.stringify(
@@ -7633,7 +7892,7 @@ async function reduceSnapshot(dir) {
7633
7892
  );
7634
7893
  }
7635
7894
  async function fullAnalysis(dir) {
7636
- const rootDir = path27.resolve(dir);
7895
+ const rootDir = path28.resolve(dir);
7637
7896
  const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
7638
7897
  analyzeProject(dir),
7639
7898
  getProjectStructure(dir),
@@ -7662,7 +7921,7 @@ function sanitizePaths(rootDir, files) {
7662
7921
  return sanitizeRepoPaths(files);
7663
7922
  }
7664
7923
  async function saveSnapshotData(dir, features, flows, removeFeatures = [], removeFlows = []) {
7665
- const rootDir = path27.resolve(dir);
7924
+ const rootDir = path28.resolve(dir);
7666
7925
  const gitHash = await getCurrentGitHash(rootDir);
7667
7926
  const now = (/* @__PURE__ */ new Date()).toISOString();
7668
7927
  for (const feat of Object.values(features)) {
@@ -7746,7 +8005,7 @@ async function saveSnapshotData(dir, features, flows, removeFeatures = [], remov
7746
8005
  });
7747
8006
  }
7748
8007
  async function getImpact(dir, files) {
7749
- const rootDir = path27.resolve(dir);
8008
+ const rootDir = path28.resolve(dir);
7750
8009
  const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
7751
8010
  const result = await analyzeImpact2(rootDir, files);
7752
8011
  return JSON.stringify(result, null, 2);
@@ -7755,7 +8014,7 @@ var VERIFY_DEFAULT_SAMPLE = 5;
7755
8014
  var VERIFY_MAX_FILES_PER_ENTRY = 8;
7756
8015
  var VERIFY_SKELETON_CHARS = 500;
7757
8016
  async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
7758
- const rootDir = path27.resolve(dir);
8017
+ const rootDir = path28.resolve(dir);
7759
8018
  const snapshot = await loadSnapshot(rootDir);
7760
8019
  if (!snapshot) {
7761
8020
  return JSON.stringify({
@@ -7820,7 +8079,7 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
7820
8079
  });
7821
8080
  }
7822
8081
  async function saveVerification(dir, verdicts) {
7823
- const rootDir = path27.resolve(dir);
8082
+ const rootDir = path28.resolve(dir);
7824
8083
  const snapshot = await loadSnapshot(rootDir);
7825
8084
  if (!snapshot) {
7826
8085
  return JSON.stringify({ exists: false, hint: "No concept map exists." });
@@ -7858,16 +8117,16 @@ async function saveVerification(dir, verdicts) {
7858
8117
  });
7859
8118
  }
7860
8119
  async function saveDecision(dir, input) {
7861
- const rootDir = path27.resolve(dir);
8120
+ const rootDir = path28.resolve(dir);
7862
8121
  const { upsertDecision: upsertDecision2 } = await Promise.resolve().then(() => (init_decisions(), decisions_exports));
7863
8122
  const result = await upsertDecision2(rootDir, input);
7864
8123
  return JSON.stringify(result);
7865
8124
  }
7866
8125
  async function reviewDecision2(dir, input) {
7867
- return JSON.stringify(await reviewDecision(path27.resolve(dir), input));
8126
+ return JSON.stringify(await reviewDecision(path28.resolve(dir), input));
7868
8127
  }
7869
8128
  async function getContext(dir, task, files) {
7870
- const rootDir = path27.resolve(dir);
8129
+ const rootDir = path28.resolve(dir);
7871
8130
  const { assembleContext: assembleContext2 } = await Promise.resolve().then(() => (init_assemble(), assemble_exports));
7872
8131
  const bundle = await assembleContext2(rootDir, task, files);
7873
8132
  return JSON.stringify(bundle);
@@ -7909,7 +8168,7 @@ async function masonInit(dir, options = {}) {
7909
8168
  return JSON.stringify(await setupProject2(dir, options), null, 2);
7910
8169
  }
7911
8170
  if (options.host) throw new Error("host applies only to mode: setup.");
7912
- const rootDir = path27.resolve(dir);
8171
+ const rootDir = path28.resolve(dir);
7913
8172
  const marker = await loadProjectMarker(rootDir);
7914
8173
  const { loadSetup: loadSetup2 } = await Promise.resolve().then(() => (init_model(), model_exports));
7915
8174
  const setup = await loadSetup2(rootDir);
@@ -7929,7 +8188,7 @@ async function masonInit(dir, options = {}) {
7929
8188
  );
7930
8189
  }
7931
8190
  async function masonCompleteInit(dir, options = {}) {
7932
- const rootDir = path27.resolve(dir);
8191
+ const rootDir = path28.resolve(dir);
7933
8192
  const existing = await loadProjectMarker(rootDir);
7934
8193
  const features = await loadProjectFeatures(rootDir);
7935
8194
  const marker = {
@@ -8038,7 +8297,7 @@ async function masonSetConfluence(input) {
8038
8297
  );
8039
8298
  }
8040
8299
  async function exportToConfluenceTool(dir, overrides) {
8041
- const rootDir = path27.resolve(dir);
8300
+ const rootDir = path28.resolve(dir);
8042
8301
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
8043
8302
  const { exportToConfluence: exportToConfluence2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
8044
8303
  const config = await loadConfig2();
@@ -8076,7 +8335,7 @@ function createMcpServer() {
8076
8335
  const server = new McpServer(
8077
8336
  {
8078
8337
  name: "mason",
8079
- version: "0.15.0"
8338
+ version: "0.16.1"
8080
8339
  },
8081
8340
  {
8082
8341
  instructions: 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, failed verification needs correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
@@ -8086,11 +8345,11 @@ function createMcpServer() {
8086
8345
  "mason_init",
8087
8346
  "Inspect this project now: returns documentation audit findings, committed-diff review findings, decision/map status, and a quickstart playbook. Quickstart and map modes are read-only and deterministic. Explicit mode: setup configures MCP, instructions and lifecycle hooks to use mason on PATH while retaining original audit evidence; use it only when the user requests setup. Optional host selects codex or claude. Optional base selects the review comparison; evidence imports CI manifests with check outcomes, commit freshness, and links to changed files and accepted decisions. mode: map returns the full Map-Reduce build workflow. Repeat calls refresh findings even after setup.",
8088
8347
  {
8089
- dir: z18.string().describe("Absolute path to the project root directory"),
8090
- mode: z18.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
8091
- host: z18.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
8092
- base: z18.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
8093
- evidence: z18.array(z18.string()).max(10).optional().describe("Repository-local CI evidence manifests to include in the review. Imports Vitest JSON and SARIF without executing check commands.")
8348
+ dir: z19.string().describe("Absolute path to the project root directory"),
8349
+ mode: z19.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
8350
+ host: z19.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
8351
+ base: z19.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
8352
+ evidence: z19.array(z19.string()).max(10).optional().describe("Repository-local CI evidence manifests to include in the review. Imports Vitest JSON and SARIF without executing check commands.")
8094
8353
  },
8095
8354
  async ({ dir, mode, host, base, evidence }) => {
8096
8355
  const result = await masonInit(dir, { mode, host, base, evidence });
@@ -8101,8 +8360,8 @@ function createMcpServer() {
8101
8360
  "mason_automation",
8102
8361
  "Inspect installed automation and observed host events, or resume/check retained documentation repair evidence across sessions. status is read-only; check saves local baselines and verification reports without editing source or approving advisories. Returns concise results with a full report path. Works without a map.",
8103
8362
  {
8104
- dir: z18.string().describe("Absolute path to the project directory"),
8105
- action: z18.enum(["status", "check"]).describe("Inspect configuration and receipts, or capture/resume and verify original audit evidence.")
8363
+ dir: z19.string().describe("Absolute path to the project directory"),
8364
+ action: z19.enum(["status", "check"]).describe("Inspect configuration and receipts, or capture/resume and verify original audit evidence.")
8106
8365
  },
8107
8366
  async ({ dir, action }) => ({ content: [{ type: "text", text: await masonAutomation(dir, action) }] })
8108
8367
  );
@@ -8110,10 +8369,10 @@ function createMcpServer() {
8110
8369
  "mason_repair",
8111
8370
  "Track documentation repairs against original audit evidence. prepare saves a local baseline and returns a scoped work order; verify reads that baseline and reports resolved, unresolved, review-required, unverified, and new findings. Suppressed advisories remain unresolved. Does not edit documentation or approve decisions. No map required.",
8112
8371
  {
8113
- dir: z18.string().describe("Absolute path to the project root directory"),
8114
- action: z18.enum(["prepare", "verify"]),
8115
- baselinePath: z18.string().optional().describe("Original baseline path returned by prepare; required for verify."),
8116
- checks: z18.array(z18.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"])).min(1).optional().describe("Optional audit check subset for prepare. Verification always uses the original checks.")
8372
+ dir: z19.string().describe("Absolute path to the project root directory"),
8373
+ action: z19.enum(["prepare", "verify"]),
8374
+ baselinePath: z19.string().optional().describe("Original baseline path returned by prepare; required for verify."),
8375
+ checks: z19.array(z19.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"])).min(1).optional().describe("Optional audit check subset for prepare. Verification always uses the original checks.")
8117
8376
  },
8118
8377
  async ({ dir, action, baselinePath, checks }) => {
8119
8378
  const result = await masonRepair(dir, { action, baselinePath, checks });
@@ -8124,8 +8383,8 @@ function createMcpServer() {
8124
8383
  "mason_complete_init",
8125
8384
  "Record assistant instruction setup locally in ignored .mason/local/project.json, with feature settings in shared .mason/config.json. Other tools work without this marker. Repeated calls preserve the original setup time and existing settings; pass confluenceConfigured only to change that setting.",
8126
8385
  {
8127
- dir: z18.string().describe("Absolute path to the project root directory"),
8128
- confluenceConfigured: z18.boolean().optional().describe("Set the Confluence setup status; omit to preserve the existing value")
8386
+ dir: z19.string().describe("Absolute path to the project root directory"),
8387
+ confluenceConfigured: z19.boolean().optional().describe("Set the Confluence setup status; omit to preserve the existing value")
8129
8388
  },
8130
8389
  async ({ dir, confluenceConfigured }) => {
8131
8390
  const result = await masonCompleteInit(dir, { confluenceConfigured });
@@ -8136,11 +8395,11 @@ function createMcpServer() {
8136
8395
  "mason_set_confluence",
8137
8396
  "Configure Confluence credentials. Two-step flow: (1) call without `spaceKey` to validate the credentials and receive a list of available spaces \u2014 relay them to the user. (2) call again with the same `baseUrl`/`email`/`apiToken` plus the chosen `spaceKey` to persist. Credentials are stored in `~/.mason/config.json`. Warn the user that the API token will be visible in chat history before they paste it.",
8138
8397
  {
8139
- baseUrl: z18.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
8140
- email: z18.string().describe("User's Atlassian account email"),
8141
- apiToken: z18.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
8142
- spaceKey: z18.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
8143
- parentPageId: z18.string().optional().describe("Optional parent page ID under which Mason's index page is created")
8398
+ baseUrl: z19.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
8399
+ email: z19.string().describe("User's Atlassian account email"),
8400
+ apiToken: z19.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
8401
+ spaceKey: z19.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
8402
+ parentPageId: z19.string().optional().describe("Optional parent page ID under which Mason's index page is created")
8144
8403
  },
8145
8404
  async ({ baseUrl, email, apiToken, spaceKey, parentPageId }) => {
8146
8405
  const result = await masonSetConfluence({
@@ -8157,7 +8416,7 @@ function createMcpServer() {
8157
8416
  "full_analysis",
8158
8417
  "One-shot orientation for a project WITHOUT a concept map (get_snapshot returned exists:false). Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source mapping. On a mapped project, prefer get_snapshot \u2014 it is cheaper and answers feature/architecture questions directly.",
8159
8418
  {
8160
- dir: z18.string().describe("Absolute path to the project root directory")
8419
+ dir: z19.string().describe("Absolute path to the project root directory")
8161
8420
  },
8162
8421
  async ({ dir }) => {
8163
8422
  const result = await fullAnalysis(dir);
@@ -8170,7 +8429,7 @@ function createMcpServer() {
8170
8429
  "analyze_project",
8171
8430
  "Run git history analysis on a codebase. Returns commit convention patterns, stale directories, and frequently changed files. These are aggregate stats across hundreds of commits that would be expensive to compute manually.",
8172
8431
  {
8173
- dir: z18.string().describe("Absolute path to the project root directory")
8432
+ dir: z19.string().describe("Absolute path to the project root directory")
8174
8433
  },
8175
8434
  async ({ dir }) => {
8176
8435
  const result = await analyzeProject(dir);
@@ -8183,8 +8442,8 @@ function createMcpServer() {
8183
8442
  "get_code_samples",
8184
8443
  "Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Read files natively for full content.",
8185
8444
  {
8186
- dir: z18.string().describe("Absolute path to the project root directory"),
8187
- count: z18.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
8445
+ dir: z19.string().describe("Absolute path to the project root directory"),
8446
+ count: z19.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
8188
8447
  },
8189
8448
  async ({ dir, count: count3 }) => {
8190
8449
  const result = await getCodeSamples(dir, count3);
@@ -8197,7 +8456,7 @@ function createMcpServer() {
8197
8456
  "get_snapshot",
8198
8457
  "Return the optional feature-to-file architecture map with drift and trust evidence. If no map exists, returns exists:false plus project structure, Git signals, and test pairs. Decision capture, get_context, and get_impact still work. No initialization required.",
8199
8458
  {
8200
- dir: z18.string().describe("Absolute path to the project root directory")
8459
+ dir: z19.string().describe("Absolute path to the project root directory")
8201
8460
  },
8202
8461
  async ({ dir }) => {
8203
8462
  const result = await getSnapshot(dir);
@@ -8210,9 +8469,9 @@ function createMcpServer() {
8210
8469
  "get_context",
8211
8470
  "Assemble task context: matching decisions with rationale, approval, owner, sources, last review, and freshness, plus related tests, file impact, and any available map entries. Proposals are suggestions; legacy records are unreviewed; accepted decisions are constraints subject to freshness. No initialization or map required. Pass task and optional files. map.status and diagnostics preserve missing or invalid knowledge. Impact covers up to three unique files, expanding directory anchors.",
8212
8471
  {
8213
- dir: z18.string().describe("Absolute path to the project root directory"),
8214
- task: z18.string().describe("The task, bug, or change request in natural language \u2014 e.g. 'add rate limiting to the API client' or a ticket description"),
8215
- files: z18.array(z18.string()).optional().describe("Optional file paths already known to be involved (e.g. from a diff or stack trace). Entries containing them are boosted above pure text matches.")
8472
+ dir: z19.string().describe("Absolute path to the project root directory"),
8473
+ task: z19.string().describe("The task, bug, or change request in natural language \u2014 e.g. 'add rate limiting to the API client' or a ticket description"),
8474
+ files: z19.array(z19.string()).optional().describe("Optional file paths already known to be involved (e.g. from a diff or stack trace). Entries containing them are boosted above pure text matches.")
8216
8475
  },
8217
8476
  async ({ dir, task, files }) => {
8218
8477
  const result = await getContext(dir, task, files);
@@ -8228,10 +8487,10 @@ function createMcpServer() {
8228
8487
  "generate_snapshot_batch",
8229
8488
  "Map step of the concept-map build. Returns one batch of source files (skeletons of every file in the batch plus a few deeper-read bodies for grounding), along with a system prompt instructing you to derive features and flows for ONLY this batch. Call repeatedly with the returned `nextOffset` until it is null, calling `save_partial_snapshot` between each call. Use product-natural feature names so partials merge cleanly in the reduce step.",
8230
8489
  {
8231
- dir: z18.string().describe("Absolute path to the project root directory"),
8232
- offset: z18.number().int().optional().describe("0-indexed file offset to start the batch at. Omit on the first call; pass the `nextOffset` from the previous response for subsequent calls."),
8233
- batchSize: z18.number().int().optional().describe("Files per batch. Defaults to 50."),
8234
- files: z18.array(z18.string()).optional().describe("Scope the batch walk to this explicit file list \u2014 e.g. the drift set from mason_check_drift (changedFiles + unmappedFiles). Pass the SAME list on every batch call of one refresh run. Triggers refresh mode: reduce_snapshot will merge the partials into the existing map instead of rebuilding it.")
8490
+ dir: z19.string().describe("Absolute path to the project root directory"),
8491
+ offset: z19.number().int().optional().describe("0-indexed file offset to start the batch at. Omit on the first call; pass the `nextOffset` from the previous response for subsequent calls."),
8492
+ batchSize: z19.number().int().optional().describe("Files per batch. Defaults to 50."),
8493
+ files: z19.array(z19.string()).optional().describe("Scope the batch walk to this explicit file list \u2014 e.g. the drift set from mason_check_drift (changedFiles + unmappedFiles). Pass the SAME list on every batch call of one refresh run. Triggers refresh mode: reduce_snapshot will merge the partials into the existing map instead of rebuilding it.")
8235
8494
  },
8236
8495
  async ({ dir, offset, batchSize, files }) => {
8237
8496
  const result = await generateSnapshotBatch(dir, offset, batchSize, files);
@@ -8244,23 +8503,23 @@ function createMcpServer() {
8244
8503
  "save_partial_snapshot",
8245
8504
  "Persist the partial concept map you derived for one batch. Call this once per batch, with the `batchId` from the `generate_snapshot_batch` response. Partials accumulate in `.mason/partial-snapshots/` and are merged in the reduce step.",
8246
8505
  {
8247
- dir: z18.string().describe("Absolute path to the project root directory"),
8248
- batchId: z18.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
8249
- offset: z18.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
8250
- features: z18.record(
8251
- z18.object({
8252
- description: z18.string(),
8253
- files: z18.array(z18.string()),
8254
- tests: z18.array(z18.string()).optional(),
8255
- type: z18.enum(["capability", "infrastructure"]).optional().describe(
8506
+ dir: z19.string().describe("Absolute path to the project root directory"),
8507
+ batchId: z19.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
8508
+ offset: z19.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
8509
+ features: z19.record(
8510
+ z19.object({
8511
+ description: z19.string(),
8512
+ files: z19.array(z19.string()),
8513
+ tests: z19.array(z19.string()).optional(),
8514
+ type: z19.enum(["capability", "infrastructure"]).optional().describe(
8256
8515
  '"capability" (user-facing functionality) or "infrastructure" (internal plumbing with no end user \u2014 DI/service wiring, config, logging, adapters). Defaults to "capability".'
8257
8516
  )
8258
8517
  })
8259
8518
  ).describe("Partial features for this batch only \u2014 files outside the batch will be added by other partials."),
8260
- flows: z18.record(
8261
- z18.object({
8262
- description: z18.string(),
8263
- chain: z18.array(z18.string())
8519
+ flows: z19.record(
8520
+ z19.object({
8521
+ description: z19.string(),
8522
+ chain: z19.array(z19.string())
8264
8523
  })
8265
8524
  ).describe("Partial flows whose entire chain is in this batch. Cross-batch flows are reconstructed in reduce.")
8266
8525
  },
@@ -8275,7 +8534,7 @@ function createMcpServer() {
8275
8534
  "reduce_snapshot",
8276
8535
  "Reduce step of the concept-map build. Returns every partial snapshot plus a system prompt asking you to merge them into one coherent project-wide map. Resolve platform variants into single product features, dedupe near-duplicates, and ensure no file is dropped. After producing the unified map, call `save_snapshot` to persist it (this also clears the partials).",
8277
8536
  {
8278
- dir: z18.string().describe("Absolute path to the project root directory")
8537
+ dir: z19.string().describe("Absolute path to the project root directory")
8279
8538
  },
8280
8539
  async ({ dir }) => {
8281
8540
  const result = await reduceSnapshot(dir);
@@ -8288,25 +8547,25 @@ function createMcpServer() {
8288
8547
  "save_snapshot",
8289
8548
  "Save a concept-to-files map as a persistent project snapshot. Maps feature names and data flows to the files that implement them. Persists across conversations \u2014 future sessions can call get_snapshot to instantly find relevant files. No API key needed \u2014 you are the LLM generating the map.",
8290
8549
  {
8291
- dir: z18.string().describe("Absolute path to the project root directory"),
8292
- features: z18.record(
8293
- z18.object({
8294
- description: z18.string().describe("One-line description of the feature"),
8295
- files: z18.array(z18.string()).describe("File paths that implement this feature"),
8296
- tests: z18.array(z18.string()).optional().describe("Test file paths for this feature"),
8297
- type: z18.enum(["capability", "infrastructure"]).optional().describe(
8550
+ dir: z19.string().describe("Absolute path to the project root directory"),
8551
+ features: z19.record(
8552
+ z19.object({
8553
+ description: z19.string().describe("One-line description of the feature"),
8554
+ files: z19.array(z19.string()).describe("File paths that implement this feature"),
8555
+ tests: z19.array(z19.string()).optional().describe("Test file paths for this feature"),
8556
+ type: z19.enum(["capability", "infrastructure"]).optional().describe(
8298
8557
  'Classification: "capability" for user-facing functionality, "infrastructure" for internal plumbing with no end user (DI/service wiring, config, logging, adapters). Capabilities are published to Confluence; infrastructure stays in the AI concept map only. Defaults to "capability".'
8299
8558
  )
8300
8559
  })
8301
8560
  ).describe("Map of feature names to their implementing files"),
8302
- flows: z18.record(
8303
- z18.object({
8304
- description: z18.string().describe("One-line description of the flow"),
8305
- chain: z18.array(z18.string()).describe("Ordered list of file paths showing data/call flow")
8561
+ flows: z19.record(
8562
+ z19.object({
8563
+ description: z19.string().describe("One-line description of the flow"),
8564
+ chain: z19.array(z19.string()).describe("Ordered list of file paths showing data/call flow")
8306
8565
  })
8307
8566
  ).describe("Map of flow names to ordered file chains"),
8308
- removeFeatures: z18.array(z18.string()).optional().describe("Feature names to delete from the existing map \u2014 for features that were renamed or no longer exist. Applied before merging; only meaningful on incremental saves."),
8309
- removeFlows: z18.array(z18.string()).optional().describe("Flow names to delete from the existing map. Applied before merging; only meaningful on incremental saves.")
8567
+ removeFeatures: z19.array(z19.string()).optional().describe("Feature names to delete from the existing map \u2014 for features that were renamed or no longer exist. Applied before merging; only meaningful on incremental saves."),
8568
+ removeFlows: z19.array(z19.string()).optional().describe("Flow names to delete from the existing map. Applied before merging; only meaningful on incremental saves.")
8310
8569
  },
8311
8570
  async ({ dir, features, flows, removeFeatures, removeFlows }) => {
8312
8571
  const result = await saveSnapshotData(
@@ -8325,17 +8584,17 @@ function createMcpServer() {
8325
8584
  "save_decision",
8326
8585
  "Capture or revise a decision proposal with rationale, anchors, optional owner, sources, and a known actor. No setup or map required. Writes a local record and preserves content history. Changes create a pending proposal while the last accepted revision remains operative; unchanged content does not re-verify or refresh it. Use review_decision for authorized acceptance or reaffirmation. A proposal cannot supersede a record with an operative accepted revision; review its replacement and retire the original separately.",
8327
8586
  {
8328
- dir: z18.string().describe("Absolute path to the project root directory"),
8329
- title: z18.string().max(80).describe("Short, specific headline \u2014 becomes the stable record id"),
8330
- body: z18.string().max(1500).describe("The knowledge itself: what was tried/decided, why, and what to avoid. Must contain information NOT derivable by reading the code."),
8331
- category: z18.enum(["decision", "gotcha", "deprecation", "convention"]),
8332
- files: z18.array(z18.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
8333
- id: z18.string().optional().describe("Existing id to revise. Changed content becomes a proposal; unchanged content leaves the review and freshness untouched."),
8334
- supersedes: z18.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
8587
+ dir: z19.string().describe("Absolute path to the project root directory"),
8588
+ title: z19.string().max(80).describe("Short, specific headline \u2014 becomes the stable record id"),
8589
+ body: z19.string().max(1500).describe("The knowledge itself: what was tried/decided, why, and what to avoid. Must contain information NOT derivable by reading the code."),
8590
+ category: z19.enum(["decision", "gotcha", "deprecation", "convention"]),
8591
+ files: z19.array(z19.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
8592
+ id: z19.string().optional().describe("Existing id to revise. Changed content becomes a proposal; unchanged content leaves the review and freshness untouched."),
8593
+ supersedes: z19.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
8335
8594
  owner: attributionSchema.shape.owner.describe("Responsible person or team, when known. Null clears it. Required for acceptance."),
8336
8595
  sources: attributionSchema.shape.sources.describe("Known PR, issue, incident, discussion, or document references. Omit to preserve; [] clears. At least one is required for acceptance."),
8337
8596
  actor: attributionSchema.shape.actor.describe("Known person or agent recording this revision. Omit if unknown; do not infer from Git identity."),
8338
- force: z18.boolean().optional().describe("Save even when a near-duplicate was detected")
8597
+ force: z19.boolean().optional().describe("Save even when a near-duplicate was detected")
8339
8598
  },
8340
8599
  async ({ dir, title, body, category, files, id, supersedes, force, owner, sources, actor }) => {
8341
8600
  const result = await saveDecision(dir, {
@@ -8357,12 +8616,12 @@ function createMcpServer() {
8357
8616
  "review_decision",
8358
8617
  "Prepare a decision review: returns the full record and history, any operative accepted revision, provenance, changes and previews for both sets of anchors, and a reviewToken. Then record accept, reaffirm, or retire with that token, the authorized reviewer, and a reason. Acceptance replaces the operative revision; retirement withdraws the entire record including its proposal. Acceptance requires owner, source, readable Git HEAD, and committed anchor changes. Changed records or code invalidate the token. Identities and approvals are recorded assertions for normal PR review, not authenticated proof.",
8359
8618
  {
8360
- dir: z18.string().describe("Absolute path to the project root directory"),
8361
- id: z18.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
8362
- action: z18.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
8363
- reviewer: z18.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
8364
- note: z18.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
8365
- reviewToken: z18.string().regex(/^[a-f0-9]{64}$/).optional().describe("Token from the prepared review; rejects stale record or code revisions")
8619
+ dir: z19.string().describe("Absolute path to the project root directory"),
8620
+ id: z19.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
8621
+ action: z19.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
8622
+ reviewer: z19.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
8623
+ note: z19.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
8624
+ reviewToken: z19.string().regex(/^[a-f0-9]{64}$/).optional().describe("Token from the prepared review; rejects stale record or code revisions")
8366
8625
  },
8367
8626
  async ({ dir, ...input }) => {
8368
8627
  const result = await reviewDecision2(dir, input);
@@ -8373,7 +8632,7 @@ function createMcpServer() {
8373
8632
  "mason_check_drift",
8374
8633
  "Check how far the concept map has drifted from HEAD. Deterministic (git + filesystem, no LLM). Returns which features/flows are stale and the changed files behind them, new source files not yet mapped, ghost files (mapped but deleted), renames, and a `recommendation`: `up-to-date` (nothing to do), `incremental` (update just the stale entries via save_snapshot), or `full-rebuild` (re-run the Map-Reduce build). Call this before trusting the map in a long session, or periodically to keep the map and any synced wikis fresh.",
8375
8634
  {
8376
- dir: z18.string().describe("Absolute path to the project root directory")
8635
+ dir: z19.string().describe("Absolute path to the project root directory")
8377
8636
  },
8378
8637
  async ({ dir }) => {
8379
8638
  const result = await checkDrift(dir);
@@ -8386,8 +8645,8 @@ function createMcpServer() {
8386
8645
  "verify_snapshot",
8387
8646
  "Spot-check the concept map's CORRECTNESS (drift checks freshness; this checks entries were right to begin with). Returns a sample of entries \u2014 always the never-verified and least-recently-verified first \u2014 with skeletons of their claimed files, for you to judge whether the files actually implement what the entry claims. Report verdicts back via save_verification. Run periodically, or after an automated refresh wrote entries no human reviewed.",
8388
8647
  {
8389
- dir: z18.string().describe("Absolute path to the project root directory"),
8390
- sample: z18.number().int().optional().describe("Entries to sample (default 5)")
8648
+ dir: z19.string().describe("Absolute path to the project root directory"),
8649
+ sample: z19.number().int().optional().describe("Entries to sample (default 5)")
8391
8650
  },
8392
8651
  async ({ dir, sample }) => {
8393
8652
  const result = await verifySnapshot(dir, sample);
@@ -8398,11 +8657,11 @@ function createMcpServer() {
8398
8657
  "save_verification",
8399
8658
  "Record verify_snapshot verdicts. Entries judged ok are stamped verifiedAt; failures are flagged verificationFailed with your note and surface in get_context, get_snapshot, and mason_check_drift until corrected. Verdict notes are required for failures.",
8400
8659
  {
8401
- dir: z18.string().describe("Absolute path to the project root directory"),
8402
- verdicts: z18.record(
8403
- z18.object({
8404
- ok: z18.boolean(),
8405
- note: z18.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
8660
+ dir: z19.string().describe("Absolute path to the project root directory"),
8661
+ verdicts: z19.record(
8662
+ z19.object({
8663
+ ok: z19.boolean(),
8664
+ note: z19.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
8406
8665
  })
8407
8666
  ).describe("Entry name \u2192 verdict, exactly as returned by verify_snapshot")
8408
8667
  },
@@ -8413,10 +8672,10 @@ function createMcpServer() {
8413
8672
  );
8414
8673
  server.tool(
8415
8674
  "get_impact",
8416
- "Trace the impact of changing files: historical co-change partners, references, and related tests. Deterministic, read-only, and usable without initialization, saved decisions, or a concept map.",
8675
+ "Trace historical co-change partners, related tests, and references with evidence: resolved imports, explicit paths, or uncertain name candidates. Candidates are not proven dependencies. Deterministic and read-only; no initialization or concept map required.",
8417
8676
  {
8418
- dir: z18.string().describe("Absolute path to the project root directory"),
8419
- files: z18.array(z18.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
8677
+ dir: z19.string().describe("Absolute path to the project root directory"),
8678
+ files: z19.array(z19.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
8420
8679
  },
8421
8680
  async ({ dir, files }) => {
8422
8681
  const result = await getImpact(dir, files);
@@ -8429,12 +8688,12 @@ function createMcpServer() {
8429
8688
  "export_to_confluence",
8430
8689
  "Sync the project's concept map to Confluence as product-readable wiki pages: an index page, one page per feature (PM-language descriptions, no file paths), and a changelog page. Mason replaces managed page bodies; manual edits to those bodies are overwritten. Requires `mason_set_confluence` to have been called first.",
8431
8690
  {
8432
- dir: z18.string().describe("Absolute path to the project root directory"),
8433
- spaceKey: z18.string().optional().describe("Override the configured space key"),
8434
- parentPageId: z18.string().optional().describe("Override the configured parent page ID"),
8435
- indexPageTitle: z18.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
8436
- changelogPageTitle: z18.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
8437
- featurePagePrefix: z18.string().optional().describe("Prefix for each feature page title (default: 'Feature: ')")
8691
+ dir: z19.string().describe("Absolute path to the project root directory"),
8692
+ spaceKey: z19.string().optional().describe("Override the configured space key"),
8693
+ parentPageId: z19.string().optional().describe("Override the configured parent page ID"),
8694
+ indexPageTitle: z19.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
8695
+ changelogPageTitle: z19.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
8696
+ featurePagePrefix: z19.string().optional().describe("Prefix for each feature page title (default: 'Feature: ')")
8438
8697
  },
8439
8698
  async ({ dir, spaceKey, parentPageId, indexPageTitle, changelogPageTitle, featurePagePrefix }) => {
8440
8699
  const result = await exportToConfluenceTool(dir, {