Implementation Plan
Living plan for building out carve. CARVE_DESIGN.md is the
architecture (the what and why); this document tracks status and the
ordered remaining work (the when and in what order). It supersedes the
month-by-month sketch in CARVE_DESIGN.md Β§11. Update it as milestones land; keep
it honest.
Last reviewed: 2026-06-24.
Status snapshot
Legend: β done & tested Β· π‘ partial Β· β¬ not started.
| Capability | State | Notes |
|---|---|---|
io (atomic write, read) |
β | |
process (subprocess capture) |
β | POSIX; Windows later |
cdb (model + JSON + atomic write) |
β | deterministic output |
command (de-Bazel argv) |
π‘ | M2 quirks landed; nvcc/emscripten/cross-host canonicalization left |
aquery (proto parse, param-file expand, path resolve) |
β | vendored trimmed analysis_v2.proto |
sidecar (schema, Load/Save, diff, project-scoped merge) |
β | HeaderIndex built & persisted; written_at stamped |
refresh (in-process aquery, execroot, merge, multi-project) |
β | M1 done: scan-deps, incremental, staleness, header index, --jobs |
scan_deps (clang DependencyScanningTool) |
β | wired into refresh; gated linux+macos |
cli + //carve:carve |
β | all four subcommands wired: refresh + prune + aggregate + shard |
| e2e harness, CI, pre-commit, toolchains_llvm, proto matchers | β | ASan/LSan/UBSan, TSan, and Linux MSan coverage for LLVM-free targets |
Layer B (carve_refresh rule) |
β | bazel run //:refresh; run-based (nested-bazel resolved) |
| Layer C (aspect + shards) | β | cc_carve_aspect + carve_aspect_refresh + shard + aggregate; per-action cacheable shards |
| Differential harness vs Hedron / clangd validation | β | tools/cdb_diff.py + docs/differential-report.md (M3) |
Distribution (.bcr/, prebuilt binaries, release) |
π‘ | release scaffolding landed (.bcr/ + release/publish workflows, source-only); not yet published |
| Windows | β¬ |
Bottom line: Layer A produces a correct-shaped CDB with header coverage and
incremental refresh. scan-deps is wired into refresh; unchanged actions
reuse cached headers; editing a header re-scans only its owning actions;
unresolved (unbuilt generated) headers are not cached and are retried;
written_at and the persisted HeaderIndex are in place; scanning is
parallelized (--jobs). M1βM5 are complete, plus the prune, aggregate,
and shard subcommands - all four CLI subcommands are wired and tested. All three
layers work: refresh (A), carve_refresh (B, bazel run //:refresh), and the
Layer C aspect (cc_carve_aspect + carve_aspect_refresh) that emits one
cacheable shard per compile action and aggregates them. Next: M6 (release +
distribution) - the last milestone.
Milestones (dependency-ordered)
M1 - Wire scan-deps into refresh (the keystone)
Realizes incremental refresh and header coverage; everything downstream assumes it.
- β
For each compile action, call
scan_deps::ScanDependencies(argv, execroot); store results inActionRecord.headers. (#9) - β
Reuse on unchanged actions:
MergeRecordskeeps cached headers, andrefreshre-scans only added/changed actions (HasMatchingRecord). (#10) - β
Set
ActionRecord.written_atvia an injected clock (deterministic in tests) - unblocksprune. (#11) - β
Build and persist the
HeaderIndex(header β owningaction_keys, canonical owner = lex-min) next to the sidecar so an edited header maps to the action(s) to re-scan (design Β§4.4βΒ§4.5). - β
Header-staleness invalidation: on refresh, an action whose cached header (or source) changed on disk (mtime past
written_at) is re-scanned even though its command is unchanged, viaFindReusableRecord+ arescannedset passed toMergeRecords. One-second granularity. - β
Missing generated headers: a failed scan leaves the record unstamped so the next refresh re-scans it (cache only a complete scan, design Β§4.2);
RunRefreshreturnsRefreshStatsand the binary surfaces an unresolved-headers count. - β
Parallelize scanning across actions (
--jobs, default hardware concurrency): anabsl::Mutex-guarded worker pool (fully thread-safety-annotated, enforced by-Wthread-safety). The scan decision stays serial so the sidecar is deterministic. Runtime TSan CI covers the LLVM-free targets; LLVM-linking targets remain excluded from sanitizer builds because they consume prebuilt archives. - β
Platform/optionality decision - resolved (option b).
scan_depsis gated linux+macos and injected intorefreshas aHeaderScanner, so the core CDB still builds everywhere and header-scanning is the enhancement; the gate propagates to thecarvebinary and e2e test.
Acceptance: carve refresh on this repo populates headers; editing a header invalidates only owning actions; refresh stays idempotent; unit + e2e tests cover header population, reuse, and missing-header caching.
M2 - Complete the de-Bazel quirk inventory (CARVE_DESIGN Β§4.3)
Independent of M1; can run in parallel. One golden test per quirk (design Β§9.2).
Done (golden-tested in carve/command, wired through refresh):
-
β
-fno-canonical-system-headersstrip (clangd#1004) and-gcc-toolchainstrip (clangd#1248). -
β ccache wrapper: drop a leading
ccacheso argv[0] is the real compiler. -
β MSVC
/showIncludes[:user] strip;-fmodules-cache-path=bazel-out/...strip. -
β Apple
wrapped_clang+__BAZEL_XCODE_*:command::ResolveXcodePlaceholderssubstitutes the developer-dir/SDK paths;refreshinvokes an injectedXcodeResolver(the binary resolves viaxcode-select/xcrunon macOS) only when a command carries a placeholder. -
β
parse_headersactions are filtered implicitly: their "source" is a header, soFindSourcefinds no TU and the action is skipped. -
β Execroot/absolute-path canonicalization of the persisted sidecar (the
cross-host determinism property, Β§9). scan-deps resolves generated and external
headers to absolute, per-host cache paths (.../execroot/_main/...);
command::RelativizeToExecrootrewrites them execroot-relative at storage
(refresh::ScanHeaders,shard::BuildShard), andCachedScanIsStaleresolves
them back against the execroot to stat. The sidecar and header index now hold
zero absolute paths (verified end-to-end on this repo: ~15k β 0) - so they are
byte-identical across machines and remote-cache-shareable. The CDBfileis also
emitted execroot-relative (clangd resolves it againstdirectory=execroot, the
same path as before). A property test asserts the sidecar has no absolute paths.
Remaining (deferred - niche toolchains or a design-level property; best validated against M3's corpus):
- β¬ Full CDB workspace-relative rewriting (
directory=workspace root + the
//externalsymlink choreography, Β§10) so the emitted database is itself
relocatable. The CDB stays execroot-rooted until then (clangd consumes it
correctly); this is the separate, larger change. Windows junctions are out of scope. - β¬ NVCCβclang flag translation (CUDA); Emscripten driver indirection; Windows command-line-length param-file specifics (carve already expands
@paramfiles via aquery--include_param_files).
Acceptance: golden test per quirk; platform-specific ones skipped where the toolchain is absent.
M3 - Differential harness + clangd validation β
Correctness gate before building Layer B/C on top.
- β
tools/cdb_diff.py: a normalizing CDB differ (keys by workspace-relative source, ignores volatile output/dep/seed tokens), with a--selftestin CI. Diffs carve's CDB against any reference (e.g. a Hedron CDB). - β
clangd verified working on carve itself:
clangd --checkbuilds the preamble/AST/index with zero compilation diagnostics using a toolchain-matched clang 22 (docs/differential-report.md). - β
carve-vs-Hedron differences explained in the report (scan-deps vs
clang -M, sidecar incrementality, header index, execroot-absolute vs workspace-relative paths). A live Hedron diff just needs a Hedron CDB pointed atcdb_diff.py; Hedron is intentionally not wired into carve (the tool it replaces) - a corpus run over external repos remains as optional follow-up.
M4 - Layer B: carve_refresh rule β
bazel run //:refresh as the CDB entry point (design Β§4.6).
- β
Nested-bazel concern resolved: a
bazel runtarget, not a build action. carve invokesbazel aquery/bazel info; spawning bazel inside a build action is the nested-bazel trap (lock/server contention, sandboxing).carve_refresh(rules/carve.bzl) generates a runfiles-aware launcher that runs the carve binary after the outer build and writes the CDB to$BUILD_WORKSPACE_DIRECTORY. - β
Dogfooded:
bazel run //:refreshwrites the repo'scompile_commands.json(22 entries). Analysis-tested (rules/carve_test.bzl): the target is runnable and carries the carve binary in its runfiles (no nested bazel in the test).
(The design's literal bazel build //:compile_commands shape is not viable for the reason above; the README/usage now shows bazel run //:refresh.)
M5 - Layer C: aspect + shards
Per-action, individually-cacheable shards for huge repos (design Β§4.7).
- β
aggregatesubcommand (carve/aggregate): the offline merge half - unions
independently-produced sidecars (de-dup by (project_id, action_key), keep
most-recently-written, deterministic order) and emits one CDB without running
bazel aquery. Sharesrefresh::EntriesFromRecords. - β
shardsubcommand (carve/shard): the per-action invocation the aspect
schedules -carve shard --action_key=β¦ --command_file=β¦ --source=β¦ --out=β¦
de-Bazels the command, resolves Xcode placeholders, scans headers (failed scan
β unstamped, re-scanned later), and writes a one-record shard whose shape
matchesrefresh. Verified end-to-end:shardβaggregateβ valid CDB. - β
The emitting aspect (
rules/cc_carve_aspect.bzl):cc_carve_aspectwalks
the cc graph and schedules one cacheablecarve_shardbuild action per compile
action, reading the fully-expanded command fromaction.argv(no@param-file
indirection - Bazel expands it). Shards land in thecarve_shardsoutput group.
carve_aspect_refresh(rules/carve.bzl) is thebazel rundriver: it builds
the shards, then aggregates them against the real execroot. - β
Per-action invalidation via Bazel: a shard's content is a function of its
compile command, so the shard action's only input is itscommand_file. Bazel
re-runs only the shards whose command changed; content edits don't re-shard
(the entry is unchanged). Shards are not header-scanned - the leancarve_shard
tool links no scanner; the database does not use headers and Bazel owns
invalidation. Validated in CI by an analysis test (the wiring) and abuild_test
that builds the whole Layer C path with the lean tools (no LLVM build).
Acceptance: aspect emits shards (done); carve aggregate merges them (done);
a compile-command change re-shards only the affected action (done - via Bazel's
action cache on command_file). M5 complete.
-
β Lean, LLVM-free Layer C tools: the aspect's per-action exec tool is the
scan-free//carve:carve_shard, andcarve_aspect_refresh's merge tool is
//carve:carve_aggregate. Neither linksscan_deps/LLVM - sharding records the
command, aggregation merges protos, and neither needs a compiler. Building the
whole Layer C path is now LLVM-free (βseconds, not the tens-of-minutes exec-config
LLVM build the fullcarverequired), so thebuild_testrejoined CI. -
β First-party scoping (
exclude_external_sources): the aspect shards only
main-repo compile actions by default, skipping external-repo targets (clangd
resolves their headers via first-party entries'-Iflags, so they need no
entries). It is an aspect parameter oncarve_aspect_refresh, defaultTrue;
setFalseto shard the whole transitive graph. -
β Layer C header recording (
ASPECT_M): opt-inrecord_headersaspect
parameter, default off so shards stay build-free. When set, each shard consumes
its compile action's own-MF .../x.ddependency file -- robust reuse of the
make-format-Moutput Bazel already generates for include validation (re-running
the wrapped compiler standalone with-Mproved unreliable, so we reuse the
real depfile) -- andcarve_shardparses it (lean, no LLVM, via the lifted
command::ParseMakeDependencies), storing the exact#includeset
execroot-relative assource_kind = ASPECT_M. Enabling it couples sharding to
building each TU (the.dis a compile byproduct). Validated end-to-end against
an external consumer. -
β
aggregatebuilds + persists aHeaderIndex: after merging the shard
records,aggregatebuilds the header -> owning-action_keyindex from their
recordedheaders(reusing the sharedsidecar::BuildHeaderIndex, no logic
duplicated) and writes it toheaders-index.binpbnext to the output database -
the same filename and placementrefreshuses beside its sidecar. A header owned
by several actions lists all owners (lex-min canonical owner first); the
empty-headers case (record_headersoff) still writes the index, matching
refresh. This closes Layer C's header-index parity withrefresh.
M6 - 0.1 release + distribution
.bcr/ metadata, release automation, prebuilt binaries for common platforms (design Β§7); decide Windows in-or-out.
- β
Release scaffolding (source-only):
.bcr/metadata +presubmit.yml;
.github/workflows/release.ymlpublishes numeric SemVer tags through a
verified draft.publish.yamlkeeps BCR publication separate, while
release_prep.shbuilds a reproducible archive without changing the checkout. - π‘ Cut the actual release:
MODULE.bazel, changelog, archive consumer, and
BCR test module are prepared for0.1.1. Push the signed tag; the release
workflow validates tagged source and the packaged consumer, attaches and
verifies the archive on a mutable draft, then publishes it directly as an
immutable stable release. Dispatch the Pages workflow so the root site
follows the new release. BCR publication remains an independent, deferred
manual workflow. - β
Consumability gap: resolved with a dependency-safe module extension
for the prebuilt LLVM distribution. Consumers no longer compile@llvm-projector
need Carve's former C++17per_file_coptworkaround. Carve links only the
required static Clang/LLVM component archives from that distribution. - β¬ Prebuilt-distribution coverage (deferred): Windows and macOS x86_64
remain limited by upstream LLVM archive availability. Add those platforms
when official matching compiler and development-library distributions exist.
Acceptance: a bzlmod consumer can bazel_dep(name = "mboworks_carve") and get a working CDB; tagged release.
Cross-cutting / parallelizable
- β
prunesubcommand (carve/prune): GC sidecar rows whosewritten_atis older than--prune_after_days; unstamped rows kept.carve prune --sidecar=... --prune_after_days=30. - β
aggregatesubcommand (carve/aggregate): merges independently-produced
sidecars into one CDB offline. βshardsubcommand (carve/shard): builds one
per-action shard. βcc_carve_aspect+carve_aspect_refresh(Layer C): the
aspect schedulesshardper compile action and aggregates the shards. - Parallel scan-deps (
--jobs) - fold into M1. - Property tests: β
cross-host determinism -
refresh_testasserts the sidecar holds no absolute paths after a refresh whose scanner returns execroot-absolute headers (M2 canonicalization). β idempotency -refresh_testasserts that refreshing twice over identical inputs (fixed clock, deterministic scanner) yields a byte-identical sidecar. - Keep docs/test-plan.md at zero open debts.
- Latency reality: Layers A/B re-run
bazel aqueryevery refresh (design Β§3.1); sub-second incrementality on huge repos is a Layer C (M5) property - don't over-promise before then.
Sequencing rationale
M1 first - it converts the working skeleton into the actual product (incrementality + headers). M2 runs alongside (independent). M3 validates correctness before B/C are layered on. M4 β M5 (B before C, per the design's layering). Release (M6) last. Riskiest dependency (LLVM linkage) and toolchain are already done.