mason-context 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0 — 2026-09-06
4
+
5
+ Mason now has one project setup operation for Codex and Claude Code. It preserves the original audit, installs a private pinned runtime, and configures MCP, lifecycle hooks, and assistant instructions without adding an npm manifest to the application. Status distinguishes configured integration from observed use.
6
+
7
+ - Add unified `mason-auto setup --host codex|claude` and explicit MCP `mason_init(mode: "setup", host)` onboarding. Preserve audit evidence before documentation edits; install a pinned private runtime without changing application manifests; merge MCP, lifecycle hooks, instructions, and ignore rules with repeatable/resumable setup.
8
+ - Add human-readable interactive setup status with structured JSON support. Distinguish configured, pending, active, and attention states using actual MCP context calls and complete hook lifecycles for the current installation and worktree/branch. Preserve native host trust and explicit disabled settings; configuration alone does not establish activation.
9
+ - Scope automation cache keys to the inputs each audit check observes. Generated build churn no longer reruns unrelated checks; documented generated paths, workspace membership, manifest contents, and decision evidence remain dependencies. Read independent instruction-file metadata concurrently.
10
+ - Classify automation failures in CLI JSON and MCP responses, preserve bounded execution receipts and durations, and keep unfinished or failed executions distinct from verified evidence. Report when storage exhaustion also prevents saving a failure receipt; clean up locks whose owner metadata could not be written.
11
+ - Omit dependency advisories for conservatively recognized Android release-version-only changes. Unknown or mixed manifest edits remain advisory, and original retained advisories still require review.
12
+
13
+ Install or upgrade from the target repository with `npx --package mason-context@0.13.0 mason-auto setup --host codex` (or `--host claude`). Review native MCP/hook trust and start a new session, then use `npx --package mason-context@0.13.0 mason-auto status` after an ordinary task. Setup never grants host trust. Existing manual installations remain supported; decision records and retained repair baselines require no migration.
14
+
15
+ Validation covers both hosts' setup, original evidence retention, resumable installs, native Claude guidance imports, configuration preservation, and uncertain activation states. Packaged npm installation, real MCP connections, deterministic hook replay, and fresh-clone recovery passed. Replay establishes mechanism behavior; broader agent usefulness and large-repository performance remain separate evidence gaps.
16
+
17
+ ## 0.12.0 — 2026-09-06
18
+
19
+ Mason can now run documentation checks automatically through Claude Code and Codex hooks. It preserves findings before edits can hide them, resumes retained evidence across sessions, and verifies repairs against the final commit. Hook installation is opt-in.
20
+
21
+ - Add a shared documentation automation runtime with Claude Code and Codex lifecycle adapters, `mason-auto` installation/status/check commands, and the `mason_automation` MCP tool. Retain original and newly discovered findings across sessions, isolate branch/worktree state, verify after the final commit, and distinguish configured hooks from observed runtime events.
22
+ - Cache audit checks by their evidence dependencies, retry skipped checks, serialize concurrent captures, and keep full reports accessible behind concise notifications. Request at most one task-relevant continuation per session; never infer advisory approval or expand repair authorization.
23
+ - Add ordinary-request automation evaluations for module renames and unrelated edits, with baseline, instructions, and hooks arms. Offline lifecycle replay is separate from live agent performance evidence.
24
+
25
+ Upgrade with `npm install -D mason-context@0.12.0` in each project that will use the default hook command. Run `npx mason-auto install --host claude` or `npx mason-auto install --host codex`, then start a new assistant session; Codex also requires review/trust through `/hooks`. Keep the host configuration and `.mason/automation.json` together in version control, and ignore `.mason/reports/`. Update any separately pinned MCP server command, restart it, and refresh the Mason instructions through `mason_init`. No decision-store migration is required.
26
+
27
+ Validation includes 414 automated tests, live rename/control smoke tests in both hosts, and a Codex trial in an existing Kotlin Multiplatform/iOS project. These trials observed capture and final-commit verification without requiring forced continuation. The controlled live forced-repair continuation test remains deferred; broader mistake-rate, false-positive, and large-repository performance claims remain unproven. See the [recorded smoke results](https://github.com/adrianczuczka/mason/blob/v0.12.0/bench/harness/automation/SMOKE_RESULTS.md).
28
+
3
29
  ## 0.11.0 — 2026-09-05
4
30
 
5
31
  Mason now keeps the original audit evidence visible while an assistant repairs documentation. A dependency warning suppressed by local edits stays unresolved, and a later documentation commit does not silently clear its review requirement.
package/README.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  ### Give coding assistants the lessons your team already learned, the changes they might miss, and evidence of what is still current.
10
10
 
11
+ Project direction and remaining evidence gaps are recorded in [the roadmap](ROADMAP.md). Use the [unified setup flow](#unified-project-setup) to connect Codex or Claude Code and enable automatic checks.
12
+
11
13
  Start with useful checks in an existing Git repository — no setup, map build, or model calls:
12
14
 
13
15
  ```bash
@@ -17,13 +19,14 @@ npx -p mason-context mason-review --dir . --base origin/main
17
19
 
18
20
  The audit checks claims in `AGENTS.md` and `CLAUDE.md` against the repository. The review checks committed changes from the merge base to HEAD for missing historical change partners and touched decisions. Choose the base branch you normally review against. Missing context files, unavailable history, and skipped checks are reported explicitly; these commands do not certify a patch's correctness.
19
21
 
20
- To capture and retrieve lessons while coding, connect Mason to your assistant:
22
+ To connect Mason and enable automatic checks, run this from your project:
21
23
 
22
24
  ```bash
23
- claude mcp add mason --scope user -- npx -p mason-context mason-mcp
25
+ npx --package mason-context@0.13.0 mason-auto setup --host codex
26
+ # For Claude Code, use --host claude.
24
27
  ```
25
28
 
26
- Restart Claude Code, then ask: *"Use Mason to check this project and set up decision capture."* `mason_init` returns the audit and review findings plus a short guide for adding Mason instructions to the project's existing `AGENTS.md` or `CLAUDE.md`. It does not build a map by default.
29
+ Setup retains the initial audit, installs a pinned private runtime, and merges MCP, hooks, and project instructions. Review the host's native trust settings, then start a new session and give the assistant a normal task. `npx --package mason-context@0.13.0 mason-auto status` reports observed use separately from configuration. See the [setup guide](#unified-project-setup) for details. A concept map remains optional.
27
30
 
28
31
  After resolving a real incident or settling a constraint, ask your assistant to record the reason with `save_decision`. On the next related task, `get_context` retrieves it as a proposal with file impact, tests, and trust evidence. Use `review_decision` when you are ready to record acceptance. Both work immediately, even without running setup.
29
32
 
@@ -129,8 +132,9 @@ Mason records assertions of review; it does not authenticate reviewer identity,
129
132
 
130
133
  | Tool | Purpose |
131
134
  |---|---|
132
- | `mason_init` | Read-only audit/review findings and quickstart guide; optional `base` for review, `evidence` for local CI manifests, `mode: "map"` for an architecture build. |
135
+ | `mason_init` | Read-only audit/review findings by default; optional `base`, CI `evidence`, and `mode: "map"`. `mode: "setup", host: "codex"` (or `"claude"`) installs runtime, MCP, instructions, and hooks using the shared setup engine. |
133
136
  | `mason_repair` | Prepare an audit repair baseline; verify the same original findings after edits. Reports unresolved advisories and unavailable checks. |
137
+ | `mason_automation` | Inspect configured hooks and observed events, or capture/resume and verify retained repair evidence across sessions. |
134
138
  | `mason_complete_init` | Records assistant instruction setup; preserves prior settings on repeated calls. |
135
139
  | `generate_snapshot_batch` | Map step — returns one batch of files for the assistant to summarize. |
136
140
  | `save_partial_snapshot` | Persists the partial map for one batch. |
@@ -266,9 +270,11 @@ What it checks:
266
270
  | `new-module` | a directory with source files that no context file mentions | likely |
267
271
  | `stale-count` | "6 packages" vs what the workspace manifest actually resolves to | certain |
268
272
  | `dead-command` | `npm run <script>` naming a script no package.json has | certain |
269
- | `deps-changed` | dependency manifests committed after the doc's last commit | advisory |
273
+ | `deps-changed` | dependency manifests committed after the doc's last commit, excluding proven Android release metadata | advisory |
270
274
  | `decision-anchor-drift` | a decision record whose anchor files changed (only when `.mason/decisions/` exists) | advisory |
271
275
 
276
+ The dependency advisory omits only recognized literal `versionName`/`versionCode` changes inside an Android `defaultConfig` block when every touched manifest qualifies. Dependency edits, computed values, unfamiliar syntax, and unrecognized metadata stay advisory. This filter does not approve or remove advisories already retained in a repair baseline.
277
+
272
278
  Issues drive the exit code; **advisories never do** — they're facts an agent can't close by editing the doc, so they're reported for humans instead. Every issue carries a `doc:line` anchor and git-derived evidence (the deleting commit, the rename target, the actual count and its source). A claim you want left alone — say, a deliberate reference to a removed directory — gets an ignore marker: `<!-- mason:ignore -->` on the line, or `<!-- mason:ignore-start -->` / `<!-- mason:ignore-end -->` around a block.
273
279
 
274
280
  ### The context files maintain themselves
@@ -314,6 +320,71 @@ jobs:
314
320
 
315
321
  Omit `agent-command` for detect-only mode: no agent, no credentials — the job fails when the context files have drifted, which is a reasonable default for repos that want the signal before the automation. Two GitHub notes: the repo setting **"Allow GitHub Actions to create and approve pull requests"** (Settings → Actions → General) must be enabled for the PR step, and PRs created with the default `GITHUB_TOKEN` don't trigger the repo's own CI — run your agent with PAT-backed auth if you need that.
316
322
 
323
+ ## Unified project setup
324
+
325
+ Available from 0.13.0. Run setup from the target Git repository, choosing the assistant you use:
326
+
327
+ ```bash
328
+ npx --package mason-context@0.13.0 mason-auto setup --host codex
329
+ # Use --host claude for Claude Code; add --dir /absolute/path/to/project to target another repository.
330
+ npx --package mason-context@0.13.0 mason-auto status
331
+ ```
332
+
333
+ Run setup once for each host you use. Repeating it also upgrades an existing integration to the executing Mason version. Versions before 0.13.0 require the manual hook installation below, which remains supported. For a local source build, run `npm run build` in Mason's checkout and invoke `node dist/mason-auto.js setup --dir /absolute/path/to/project --host codex`.
334
+
335
+ Setup retains the initial audit before editing instruction files, installs the executing Mason distribution under the ignored `.mason/runtime/` directory, and configures both MCP and lifecycle hooks to use that pinned runtime. Node 20+, npm, and Git are required; installation may download dependencies, with package scripts disabled. It does not create or change the application's npm manifest, so Kotlin, Python, and other repositories use the same flow. Ordinary hooks and MCP launches reuse the installed runtime without downloading packages.
336
+
337
+ Existing project guidance is preserved outside marked Mason blocks. Codex receives an `AGENTS.md` entry point; Claude Code receives or reuses a `CLAUDE.md` entry point, using a native `@AGENTS.md` import when that is the shared document (or `@../AGENTS.md` from `.claude/CLAUDE.md`). See [Claude Code memory imports](https://code.claude.com/docs/en/memory#agentsmd). Setup merges the named Mason MCP server and its recorded hooks while retaining unrelated settings and explicit disable options. It refuses malformed or ambiguous configuration and concurrent edits. Repeating the command resumes an interrupted install or updates the selected distribution without replacing the retained original audit.
338
+
339
+ The project changes are reviewable together: assistant instructions, `.gitignore`, `.mason/run.cjs`, `.mason/setup.json`, `.mason/automation.json`, `.mason/project.json`, and the selected host's configuration (`.codex/config.toml` and `.codex/hooks.json`, or `.mcp.json` and `.claude/settings.json`). Ignore rules keep runtime dependencies and `.mason/reports/` local while allowing decision records and setup metadata into version control. A new clone must run setup to install its own runtime; local evidence is not copied or inferred from committed configuration.
340
+
341
+ Finish activation in the host:
342
+
343
+ 1. Review the project's MCP and hook configuration through the host's native trust controls. Codex provides `/hooks` in its CLI; Claude Code requires approval for project MCP servers. Setup never changes trust on your behalf. See the [Codex hook documentation](https://learn.chatgpt.com/docs/hooks) and [Claude Code project MCP documentation](https://code.claude.com/docs/en/mcp).
344
+ 2. Start a new assistant session in that project and give it a normal task. The project instructions direct the assistant to request Mason context; hooks preserve and verify audit evidence during work.
345
+ 3. Run `mason-auto status` using the same installed build. Interactive output shows runtime/configuration health, observed events, task context requests, and verification. Use `--json` for structured output; piped status remains JSON.
346
+
347
+ `pending` means setup needs evidence of use. `active` requires a `get_context` call through the configured MCP server and all five lifecycle events in one session for the current setup revision and worktree/branch. `attention` identifies missing or changed configuration/runtime, disabled settings, or a failed verification attempt. Verification remains a separate result: observed activation does not prove a repair was correct or that Mason improved the task. Local receipts store counts, event names, and hashed session identifiers, not prompts or tool arguments. Higher-priority host settings can still prevent execution.
348
+
349
+ For an assistant already connected to this build, `mason_init` with `mode: "setup"` and `host: "codex"` or `"claude"` invokes the same engine. Its default quickstart remains read-only. Setup does not build a concept map, approve advisories, or create decision records; decisions should capture actual lessons from subsequent work.
350
+
351
+ ## Automatic documentation checks (mason-auto)
352
+
353
+ Mason can preserve documentation audit evidence and resume unfinished repairs through Claude Code or Codex lifecycle hooks. A shared engine owns the evidence, verification, and cache; each host adapter handles its event format. No concept map or model call is required for the checks.
354
+
355
+ Available from 0.12.0. Install or upgrade the package in each project where you want automatic checks:
356
+
357
+ ```bash
358
+ npm install -D mason-context@0.13.0
359
+ npx mason-auto install --host claude # Claude Code
360
+ npx mason-auto install --host codex # Codex; review/trust the hooks using /hooks
361
+ npx mason-auto status
362
+ ```
363
+
364
+ Install only the adapters you use. Installation merges the project's `.claude/settings.json` or `.codex/hooks.json`, preserves other hooks/settings, and records its own handler in `.mason/automation.json`. Repeating installation updates only those handlers. Keep the host configuration and `.mason/automation.json` together in version control; if you ignore all of `.mason/`, allow the installation record explicitly. Add `.mason/reports/` to your ignore rules. Start a new assistant session after installation. The default handler uses the locally installed package with `npx --no-install`; `--command` accepts an executable prefix for an existing installation.
365
+
366
+ When upgrading an existing MCP setup, update any separately pinned server command to `mason-context@0.13.0`, restart the server, and refresh the Mason instruction block through `mason_init`. Existing decisions and repair baselines need no migration. Upgrading the package alone does not install hooks.
367
+
368
+ `status` distinguishes configuration from observed events. Host versions, project trust, policy, and specialized tool paths can prevent hooks from running. Configuration alone is not evidence of automatic use. Codex requires review/trust of new or changed non-managed hooks. See the [Claude Code hook reference](https://code.claude.com/docs/en/hooks) and [Codex hook reference](https://learn.chatgpt.com/docs/hooks).
369
+
370
+ On session start, Mason recovers the current branch/worktree's evidence. Before and after tools, it checks for changed audit inputs and retains newly observed findings before another documentation edit can hide them. Shell and unknown tool calls are included because edits can happen outside a file-edit tool. At turn completion it verifies the retained findings. A relevant unresolved issue can request **one continuation per session**; advisories and unavailable checks never create a repair loop. Fixes remain the assistant's responsibility within the user's task scope.
371
+
372
+ Checks reuse cached results only when their dependencies match. Documentation and history, module candidates, documented workspace counts, command manifests, and decision evidence have separate invalidation keys. Unrelated generated build output does not invalidate these checks; explicitly documented generated files and workspace members still do. Changes to a dirty manifest invalidate its checks even when Git's status text is unchanged. Skipped checks are retried. Cache corruption causes recomputation; invalid original baselines or active state remain errors. Concurrent events serialize writes, and interrupted local writers' locks are recovered only when their process is gone. New reports are written atomically. Unchanged tool events reuse the existing full report.
373
+
374
+ ```bash
375
+ npx mason-auto check --json # Capture/resume the active evidence and verify it
376
+ # After any final documentation commit:
377
+ npx mason-auto check
378
+ ```
379
+
380
+ The equivalent MCP operation is `mason_automation(action: "check")`. Its response is concise and links the full local report. `status` is read-only; `check` writes evidence. Exit codes are 0 for verified checks, 1 for unresolved issues, and 2 for incomplete/unavailable checks. Original `mason_repair` baselines remain separately verifiable by their paths. Hook errors are visible and advisory; exit 0 from a hook means the host can continue, not that verification passed. CLI JSON and MCP failures include a category (`inputs-changed`, `storage-full`, `busy`, `invalid-input`, `history-unavailable`, `invalid-evidence`, `io-error`, or `internal`), retryability, and whether a failure receipt was saved. Changing inputs require another check on a stable checkout; they never produce a cached pass.
381
+
382
+ `status` includes a bounded history of the latest 32 execution attempts, their duration after lock acquisition, and the number of older receipts omitted. A completed attempt includes its verification outcome. A started attempt without a matching live local lock owner is unknown, and a failed or unfinished latest attempt prevents an older report from being presented as current verification. Storage exhaustion can prevent even a failure receipt from being saved; the caller reports that explicitly. Receipts contain no prompts or tool arguments. The existing `mason-hook` decision injector keeps its previous behavior.
383
+
384
+ Evidence is local to the worktree and branch. Switching assistants in that worktree resumes the same repair; another worktree or branch has separate state. Detached-HEAD commits retain evidence; moving that checkout to a different history requires inspection. Hooks follow the Git worktree of the event's working directory. Reports are not automatically transferred to CI. CI can call `mason-auto check` on retained local artifacts, or `mason-audit --verify-repair <baseline>` after restoring the original artifacts at their recorded root. A fresh checkout cannot reconstruct missing pre-edit evidence. Audited instruction files remain limited to `AGENTS.md`, `CLAUDE.md`, and `.claude/CLAUDE.md`. Automation bounds inventory at 100,000 paths and retained baselines at 128; exceeding a bound reports unavailable evidence without evicting unresolved findings. Symbolic links in the inspected inventory require an explicit audit instead of cached automation. This is not proof of arbitrary repository scale or universal tool interception.
385
+
386
+ The [automation evaluation](bench/harness/automation/README.md) compares ordinary module-renaming requests and unrelated edits across hosts, with baseline, instructions, and hooks arms. Deterministic replay verifies the mechanism; live sessions measure actual activation.
387
+
317
388
  ## Decision injection (mason-hook)
318
389
 
319
390
  Recorded knowledge only helps if it shows up. Retrieval tools depend on the model deciding to call them — and it often doesn't. `mason-hook` removes the gamble: it's a Claude Code `PostToolUse` hook that fires when a session reads or edits a file, looks up the decision records anchored to that file (exact path or directory prefix), and injects them into the model's context. Deterministic lookup, no LLM call, ~100ms, silent when nothing matches. Each decision is injected at most once per session, and records whose anchors drifted since verification carry a verify-before-relying marker.
@@ -508,7 +579,7 @@ If you used Mason before v0.4.0, the standalone `mason <command>` CLI has been r
508
579
  | `mason impact File.kt` | Ask your assistant: *"what would changing File.kt affect?"* — it calls `get_impact`. |
509
580
  | `mason snapshot --install-hook` | Removed. The map auto-refreshes when the assistant detects stale state. |
510
581
 
511
- The package provides `mason-mcp`, `mason-drift`, `mason-audit`, `mason-hook`, and `mason-review`. Running `mason` directly prints a migration message and exits.
582
+ The package provides `mason-mcp`, `mason-drift`, `mason-audit`, `mason-auto`, `mason-hook`, and `mason-review`. Running `mason` directly prints a migration message and exits.
512
583
 
513
584
  ## License
514
585
 
@@ -133,7 +133,7 @@ async function readStoreJson(root, relative) {
133
133
  return parsed;
134
134
  } catch (error) {
135
135
  if (error.code === "ENOENT") return null;
136
- throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`);
136
+ throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
137
137
  }
138
138
  }
139
139
  async function writeStoreJson(root, relative, value) {
@@ -622,24 +622,24 @@ async function isDirty(resolvedRoot, relPath) {
622
622
  }
623
623
  }
624
624
  async function discoverDocs(resolvedRoot) {
625
- const docs = [];
626
- for (const candidate of DOC_CANDIDATES) {
625
+ const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
627
626
  let content;
628
627
  try {
629
628
  content = await fs4.readFile(path7.join(resolvedRoot, candidate), "utf-8");
630
629
  } catch {
631
- continue;
630
+ return null;
632
631
  }
633
- docs.push({
632
+ const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
633
+ return {
634
634
  path: candidate,
635
635
  content,
636
636
  lineCount: content.split("\n").length,
637
- lastCommit: await lastCommitOf(resolvedRoot, candidate),
638
- dirty: await isDirty(resolvedRoot, candidate),
637
+ lastCommit,
638
+ dirty,
639
639
  claims: extractClaims(content)
640
- });
641
- }
642
- return docs;
640
+ };
641
+ }));
642
+ return docs.filter((doc) => doc !== null);
643
643
  }
644
644
 
645
645
  // src/audit/types.ts
@@ -814,12 +814,19 @@ async function checkNewModules(ctx) {
814
814
  }
815
815
  });
816
816
  };
817
- for (const topDir of await listSubdirs(ctx.root)) {
818
- const absTop = path9.join(ctx.root, topDir);
817
+ for (const candidate of await moduleCandidates(ctx.root, combinedDocs)) {
818
+ await flag(candidate.dir, candidate.sourceFileCount);
819
+ }
820
+ return result;
821
+ }
822
+ async function moduleCandidates(root, combinedDocs) {
823
+ const candidates = [];
824
+ for (const topDir of await listSubdirs(root)) {
825
+ const absTop = path9.join(root, topDir);
819
826
  const topMentioned = isMentioned(combinedDocs, topDir);
820
827
  if (!topMentioned) {
821
828
  const count2 = await countSourceFiles(absTop);
822
- if (count2 >= 1) await flag(topDir, count2);
829
+ if (count2 >= 1) candidates.push({ dir: topDir, sourceFileCount: count2 });
823
830
  continue;
824
831
  }
825
832
  const subdirs = await listSubdirs(absTop);
@@ -829,11 +836,11 @@ async function checkNewModules(ctx) {
829
836
  if (isMentioned(combinedDocs, sub)) continue;
830
837
  const count2 = await countSourceFiles(path9.join(absTop, sub));
831
838
  if (count2 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
832
- await flag(`${topDir}/${sub}`, count2);
839
+ candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count2 });
833
840
  }
834
841
  }
835
842
  }
836
- return result;
843
+ return candidates;
837
844
  }
838
845
 
839
846
  // src/audit/checks/stale-count.ts
@@ -1016,15 +1023,7 @@ async function checkDeadCommands(ctx) {
1016
1023
  const loadWorkspaceScripts = async () => {
1017
1024
  if (workspaceScripts !== null) return workspaceScripts;
1018
1025
  workspaceScripts = /* @__PURE__ */ new Set();
1019
- const manifests = await fg4("**/package.json", {
1020
- cwd: ctx.root,
1021
- ignore: [
1022
- "**/node_modules/**",
1023
- "**/dist/**",
1024
- "**/build/**",
1025
- "package.json"
1026
- ]
1027
- });
1026
+ const manifests = await commandManifests(ctx.root);
1028
1027
  manifestsChecked = ["package.json", ...manifests.sort()];
1029
1028
  for (const manifest of manifests) {
1030
1029
  const scripts = await scriptsOf(path11.join(ctx.root, manifest));
@@ -1052,6 +1051,64 @@ async function checkDeadCommands(ctx) {
1052
1051
  }
1053
1052
  return result;
1054
1053
  }
1054
+ function commandManifests(root) {
1055
+ return fg4("**/package.json", {
1056
+ cwd: root,
1057
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", ".mason/reports/**", "package.json"]
1058
+ });
1059
+ }
1060
+
1061
+ // src/audit/release-metadata.ts
1062
+ import { execFile as execFile6 } from "child_process";
1063
+ import { promisify as promisify6 } from "util";
1064
+ var exec6 = promisify6(execFile6);
1065
+ async function git(root, args) {
1066
+ return (await exec6("git", args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
1067
+ }
1068
+ function withoutAndroidReleaseValues(text2) {
1069
+ if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
1070
+ const scopes = [];
1071
+ const normalized = [];
1072
+ let assignments = 0;
1073
+ for (const line of text2.split("\n")) {
1074
+ const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
1075
+ const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
1076
+ if (assignment && scopes.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
1077
+ normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
1078
+ assignments++;
1079
+ } else {
1080
+ if (/\bversion(?:Name|Code)\b/.test(line)) return null;
1081
+ normalized.push(line);
1082
+ }
1083
+ const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
1084
+ for (const brace of code.matchAll(/[{}]/g)) {
1085
+ if (brace[0] === "{") scopes.push(named ?? "unknown");
1086
+ else if (!scopes.length) return null;
1087
+ else scopes.pop();
1088
+ }
1089
+ }
1090
+ return assignments && !scopes.length ? normalized.join("\n") : null;
1091
+ }
1092
+ async function releaseMetadataOnly(root, commit) {
1093
+ if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
1094
+ try {
1095
+ const parents = (await git(root, ["rev-list", "--parents", "-n", "1", commit.hash])).trim().split(/\s+/);
1096
+ if (parents.length !== 2) return false;
1097
+ for (const file of commit.files) {
1098
+ const raw = await git(root, ["diff", "--raw", "-z", "--no-renames", "--no-ext-diff", "--no-textconv", parents[1], commit.hash, "--", file]);
1099
+ if (!/^:(100644|100755) \1 [a-f0-9]+ [a-f0-9]+ M\0/.test(raw)) return false;
1100
+ const [before, after] = await Promise.all([
1101
+ git(root, ["show", parents[1] + ":" + file]),
1102
+ git(root, ["show", commit.hash + ":" + file])
1103
+ ]);
1104
+ const previous = withoutAndroidReleaseValues(before);
1105
+ if (previous === null || previous !== withoutAndroidReleaseValues(after)) return false;
1106
+ }
1107
+ return true;
1108
+ } catch {
1109
+ return false;
1110
+ }
1111
+ }
1055
1112
 
1056
1113
  // src/audit/checks/deps-changed.ts
1057
1114
  var MANIFEST_COMMITS_CAP = 10;
@@ -1072,6 +1129,7 @@ var MANIFEST_PATHSPECS = [
1072
1129
  async function checkDepsChanged(ctx) {
1073
1130
  const result = emptyResult();
1074
1131
  result.suppressedAdvisories = [];
1132
+ const releaseOnly = /* @__PURE__ */ new Map();
1075
1133
  for (const doc of ctx.docs) {
1076
1134
  if (!doc.lastCommit) {
1077
1135
  result.skipped.push({
@@ -1101,6 +1159,15 @@ async function checkDepsChanged(ctx) {
1101
1159
  });
1102
1160
  continue;
1103
1161
  }
1162
+ const relevant = [];
1163
+ for (const commit of range.commits) {
1164
+ if (!releaseOnly.has(commit.hash) && releaseOnly.size < 100) {
1165
+ releaseOnly.set(commit.hash, await releaseMetadataOnly(ctx.root, commit));
1166
+ }
1167
+ if (!releaseOnly.get(commit.hash)) relevant.push(commit);
1168
+ }
1169
+ range.commits = relevant;
1170
+ range.total = relevant.length;
1104
1171
  if (range.total === 0) continue;
1105
1172
  const latest = range.commits[0];
1106
1173
  (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
@@ -1381,9 +1448,8 @@ function emptyResult() {
1381
1448
  // src/audit/audit.ts
1382
1449
  async function computeAudit(rootDir, options = {}) {
1383
1450
  const resolvedRoot = path15.resolve(rootDir);
1384
- const docs = await discoverDocs(resolvedRoot);
1451
+ const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
1385
1452
  if (docs.length === 0) return null;
1386
- const headHash = await getCurrentGitHash(resolvedRoot);
1387
1453
  const report = {
1388
1454
  version: 1,
1389
1455
  root: resolvedRoot,
@@ -1428,7 +1494,7 @@ async function computeAudit(rootDir, options = {}) {
1428
1494
  const selected = options.checks ?? ALL_CHECKS;
1429
1495
  for (const name of ALL_CHECKS) {
1430
1496
  if (!selected.includes(name)) continue;
1431
- const { issues, advisories, suppressedAdvisories, skipped } = await CHECKS[name](ctx);
1497
+ const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
1432
1498
  report.checksRun.push(name);
1433
1499
  report.issues.push(...issues);
1434
1500
  report.advisories.push(...advisories);
@@ -1500,6 +1566,12 @@ var issueSchema = findingSchema.extend({
1500
1566
  confidence: z3.enum(["certain", "likely"])
1501
1567
  });
1502
1568
  var advisorySchema = findingSchema.extend({ type: z3.enum(["deps-changed", "decision-anchor-drift"]) });
1569
+ var checkResultSchema = z3.object({
1570
+ issues: z3.array(issueSchema),
1571
+ advisories: z3.array(advisorySchema),
1572
+ suppressedAdvisories: z3.array(advisorySchema).optional(),
1573
+ skipped: z3.array(z3.object({ check: z3.string(), reason: z3.string(), doc: z3.string().optional() }))
1574
+ });
1503
1575
  var reportSchema = z3.object({
1504
1576
  version: z3.literal(1),
1505
1577
  root: z3.string(),
@@ -1569,19 +1641,19 @@ async function docState(root) {
1569
1641
  }
1570
1642
  return digest(docs);
1571
1643
  }
1572
- async function stableAudit(root, checks) {
1644
+ async function stableAudit(root, checks, options = {}) {
1573
1645
  const head = await getCurrentGitHash(root);
1574
1646
  const before = await docState(root);
1575
- const report = await computeAudit(root, { checks });
1647
+ const report = await computeAudit(root, { ...options, checks });
1576
1648
  if (head !== await getCurrentGitHash(root) || before !== await docState(root) || report && report.headHash !== head) {
1577
1649
  throw new Error("HEAD or context files changed during the audit; retry against a stable checkout.");
1578
1650
  }
1579
1651
  return report;
1580
1652
  }
1581
- async function prepareRepair(rootDir, checks = ALL_CHECKS) {
1653
+ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
1582
1654
  const root = await fs10.realpath(rootDir);
1583
1655
  const selected = z3.array(checkSchema).nonempty().parse(checks);
1584
- const report = await stableAudit(root, selected);
1656
+ const report = await stableAudit(root, selected, options);
1585
1657
  if (!report) throw new Error("No context files found to prepare a repair.");
1586
1658
  if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
1587
1659
  const storedReport = reportSchema.parse(report);
@@ -1595,7 +1667,7 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS) {
1595
1667
  await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
1596
1668
  return { version: 1, action: "prepare", baselinePath, report };
1597
1669
  }
1598
- async function verifyRepair(rootDir, baselinePath) {
1670
+ async function verifyRepair(rootDir, baselinePath, options = {}) {
1599
1671
  const root = await fs10.realpath(rootDir);
1600
1672
  const declaredRoot = path16.resolve(rootDir);
1601
1673
  const relative = path16.isAbsolute(baselinePath) ? path16.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
@@ -1607,7 +1679,7 @@ async function verifyRepair(rootDir, baselinePath) {
1607
1679
  const diagnostics = [];
1608
1680
  let current = null;
1609
1681
  try {
1610
- current = await stableAudit(root, original.checksRun);
1682
+ current = await stableAudit(root, original.checksRun, options);
1611
1683
  if (!current) diagnostics.push("No context files remain available to audit.");
1612
1684
  else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
1613
1685
  for (const doc of original.docs) {