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 +26 -0
- package/README.md +77 -6
- package/dist/mason-audit.js +105 -33
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +4487 -0
- package/dist/mason-auto.js.map +1 -0
- package/dist/mason-drift.js +1 -1
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +1 -1
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +5974 -4308
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +1 -1
- package/dist/mason-review.js.map +1 -1
- package/package.json +4 -1
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
|
|
22
|
+
To connect Mason and enable automatic checks, run this from your project:
|
|
21
23
|
|
|
22
24
|
```bash
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/dist/mason-audit.js
CHANGED
|
@@ -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
|
-
|
|
630
|
+
return null;
|
|
632
631
|
}
|
|
633
|
-
|
|
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
|
|
638
|
-
dirty
|
|
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
|
|
818
|
-
|
|
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)
|
|
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
|
-
|
|
839
|
+
candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count2 });
|
|
833
840
|
}
|
|
834
841
|
}
|
|
835
842
|
}
|
|
836
|
-
return
|
|
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
|
|
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) {
|