mason-context 0.12.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,19 @@
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
+
3
17
  ## 0.12.0 — 2026-09-06
4
18
 
5
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.
package/README.md CHANGED
@@ -8,7 +8,7 @@
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).
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
12
 
13
13
  Start with useful checks in an existing Git repository — no setup, map build, or model calls:
14
14
 
@@ -19,13 +19,14 @@ npx -p mason-context mason-review --dir . --base origin/main
19
19
 
20
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.
21
21
 
22
- 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:
23
23
 
24
24
  ```bash
25
- 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.
26
27
  ```
27
28
 
28
- 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.
29
30
 
30
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.
31
32
 
@@ -131,7 +132,7 @@ Mason records assertions of review; it does not authenticate reviewer identity,
131
132
 
132
133
  | Tool | Purpose |
133
134
  |---|---|
134
- | `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. |
135
136
  | `mason_repair` | Prepare an audit repair baseline; verify the same original findings after edits. Reports unresolved advisories and unavailable checks. |
136
137
  | `mason_automation` | Inspect configured hooks and observed events, or capture/resume and verify retained repair evidence across sessions. |
137
138
  | `mason_complete_init` | Records assistant instruction setup; preserves prior settings on repeated calls. |
@@ -269,9 +270,11 @@ What it checks:
269
270
  | `new-module` | a directory with source files that no context file mentions | likely |
270
271
  | `stale-count` | "6 packages" vs what the workspace manifest actually resolves to | certain |
271
272
  | `dead-command` | `npm run <script>` naming a script no package.json has | certain |
272
- | `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 |
273
274
  | `decision-anchor-drift` | a decision record whose anchor files changed (only when `.mason/decisions/` exists) | advisory |
274
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
+
275
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.
276
279
 
277
280
  ### The context files maintain themselves
@@ -317,6 +320,34 @@ jobs:
317
320
 
318
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.
319
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
+
320
351
  ## Automatic documentation checks (mason-auto)
321
352
 
322
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.
@@ -324,7 +355,7 @@ Mason can preserve documentation audit evidence and resume unfinished repairs th
324
355
  Available from 0.12.0. Install or upgrade the package in each project where you want automatic checks:
325
356
 
326
357
  ```bash
327
- npm install -D mason-context@0.12.0
358
+ npm install -D mason-context@0.13.0
328
359
  npx mason-auto install --host claude # Claude Code
329
360
  npx mason-auto install --host codex # Codex; review/trust the hooks using /hooks
330
361
  npx mason-auto status
@@ -332,13 +363,13 @@ npx mason-auto status
332
363
 
333
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.
334
365
 
335
- When upgrading an existing MCP setup, update any separately pinned server command to `mason-context@0.12.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.
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.
336
367
 
337
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).
338
369
 
339
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.
340
371
 
341
- Checks reuse cached results only when their dependencies match. Documentation and history, file inventory, manifests, and decision evidence have separate invalidation keys. 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.
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.
342
373
 
343
374
  ```bash
344
375
  npx mason-auto check --json # Capture/resume the active evidence and verify it
@@ -346,7 +377,9 @@ npx mason-auto check --json # Capture/resume the active evidence and verify it
346
377
  npx mason-auto check
347
378
  ```
348
379
 
349
- 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; the existing `mason-hook` decision injector keeps its previous behavior.
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.
350
383
 
351
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.
352
385
 
@@ -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,