Skip to content

feat(graph): compile apple_library through starlark rule impl#86

Merged
pepicrft merged 29 commits into
mainfrom
feat/apple-graph-actions
Jun 11, 2026
Merged

feat(graph): compile apple_library through starlark rule impl#86
pepicrft merged 29 commits into
mainfrom
feat/apple-graph-actions

Conversation

@pepicrft

Copy link
Copy Markdown
Contributor

Summary

  • Move apple_library build logic out of Rust shell-script placeholders into a starlark rule impl declared in crates/once-frontend/prelude/apple.star. The impl composes a real xcrun-driven swiftc -emit-library -static -emit-module command line through native globals (xcrun_swiftc, apple_triple, declare_output, run_action) and returns a provider dict.
  • Add the analysis layer in once-frontend that evaluates a rule impl per target with a ctx dict (label, typed attrs, glob-expanded srcs, dep providers, build_dir), collects the declared actions through a thread-local store, and exposes them as DeclaredActions. State threading uses a thread-local instead of Evaluator::extra because the workspace forbids unsafe (the latter requires deriving ProvidesStaticType).
  • Add the analysis driver in once-cli that walks deps for impl-bearing rules, converts each DeclaredAction into a once_core::Action with an input_digest composing the swiftc identity, source content digests, and dep action digests, then executes via run_with_cache. The other apple rule kinds (apple_framework, apple_application, apple_test_bundle) keep their existing placeholder scripts; a small RULES_WITH_IMPL list gates the new path.
  • Add a fixtures/apple_library/ workspace with AppCore and Greeter (the latter imports the former), plus four new shellspec cases (compile, recursive dep build, cache hit, dep-change invalidation) gated on xcrun availability. Update docs/guide/apple-graph.md with the rule-impl ctx surface and note the planned MCP-for-agents follow-up.

Testing

  • mise exec -- cargo test --workspace
  • mise exec -- cargo clippy --workspace --all-targets -- -D warnings
  • mise exec -- cargo fmt --all -- --check
  • mise exec -- cargo build --release
  • mise exec -- shellspec
  • Manual end-to-end on macOS: built apps/ios/AppCore and apps/ios/Greeter from the fixture; confirmed AppCore.a is a real current ar archive random library, Greeter resolves import AppCore through the -I search path on the dep's swiftmodule dir, cache hits on rebuild, and dep-source change invalidates the parent cache slot.

pepicrft and others added 2 commits June 10, 2026 17:58
When work units are independent, drive them concurrently rather than
serializing. Sequential code should be a deliberate choice for data
dependencies, not the default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The apple_library rule now carries an impl callable in the prelude that
composes a real swiftc command line and emits it through native globals
(xcrun_swiftc, apple_triple, declare_output, run_action). The analysis
pass evaluates the impl per target with a ctx dict carrying the typed
attrs, glob-expanded srcs, dep provider records, and the workspace
build directory; the actions the impl declares are converted into
cacheable RunCommand actions and executed through run_with_cache.

The action's input digest folds in the swiftc identity, source content
digests, and the dep action digests so a swap of Xcode or a change to
any transitive source invalidates the parent cache slot. Recursive
builds walk apple_library dependencies first and forward each dep's
swiftmodule directory through -I so import statements resolve.

The other apple rule kinds (apple_framework, apple_application,
apple_test_bundle) keep their placeholder shell scripts; a small
RULES_WITH_IMPL list gates the new path so unfinished rules don't
trigger analysis. Fixture-backed shellspec coverage exercises the
compile, the recursive build, the cache hit, and the dep-change
invalidation paths, gated on xcrun availability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread crates/once-cli/src/commands/graph/apple/srcs.rs Outdated
Comment thread crates/once-cli/src/commands/graph/apple/srcs.rs Outdated
Comment thread crates/once-cli/src/commands/graph/analysis.rs
pepicrft and others added 6 commits June 10, 2026 18:10
The Rust side now exposes only generic primitives: host_arch, host_os,
host_which, host_command, glob, declare_output, run_action. Anything
domain-specific (xcrun discovery, SDK name mapping, LLVM triple format,
file-extension filtering) lives in apple.star and is implemented in
starlark on top of these primitives. The Rust analysis layer becomes
an executor for whatever the prelude declares.

ctx["srcs"] now carries the raw glob patterns from the manifest; the
impl calls glob(ctx["srcs"]) to expand them and filters by extension
itself. The crates/once-cli/src/commands/graph/apple submodule and the
once-frontend analysis_host helper are gone; the driver checks
rule_has_impl against the prelude rather than a hardcoded list, so the
set of impl-bearing kinds is owned by apple.star.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…escape test

The canonicalize-and-strip-prefix check that rejects globs resolving
outside the workspace was added without an exercise for the new
analysis.rs location after the apple/srcs.rs module went away. Add a
unix test that drops a symlink pointing at an external tempdir under
the package and asserts expand_globs surfaces the "outside the
workspace" error. Document on the function that the check is
best-effort against on-disk state (the workspace is trusted) and that
Windows junction coverage waits on Windows CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lean on Rust's ownership model in the graph build pipeline:

* `run_declared_actions` now takes `AnalysisResult` by value and
  destructures it, so the impl-returned provider record moves into
  `BuildOutcome` instead of being cloned. Each declared action is
  consumed by `into_iter`; its `env` moves into `Action::RunCommand`
  via a final destructure inside `declared_to_action`.
* `compose_input_digest` borrows everywhere it can: sources sort as
  `Vec<&str>` instead of a cloned `Vec<String>`, dep digests sort by
  index into the caller's slice instead of `to_vec()`, and the env
  walk drops its sort buffer entirely (`BTreeMap` already iterates in
  key order).
* `build_target` destructures the returned `BuildOutcome` so `outputs`
  moves into the run record rather than being cloned; `action_digest`
  is `Copy` and `cache_tag` is `&'static str`, so there's nothing left
  to clone.

Net effect: the heaviest fields (provider `JsonValue` tree, output
paths, env map) move from declaration through execution into the
returned record without intermediate clones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…holds

Second ownership pass:

* `BuildSession.targets` now stores `Arc<GraphTarget>` so the spawn
  path bumps a refcount instead of deep-cloning the row (and its
  `Vec<String>` srcs + `BTreeMap` attrs) twice per build task.
* `BuildOutcome` drops its `Clone` derive: the type has exactly one
  owner at a time, and the producer→outcomes→consumer transitions are
  all moves.
* `build_reachable` tracks `remaining_readers` per dep, so the last
  consumer moves a producer's `JsonValue` provider out of the session
  map via `outcomes.remove` instead of cloning it.
* `reachable_impl_targets` checks membership before insert so the
  owned `target_id` popped off the stack moves straight into the
  HashSet.
* `HostCache::which`/`command` release the mutex before the filesystem
  walk and `Command::output` so a slow `xcrun` spawn on one analysis
  task doesn't serialise sibling tasks asking about other binaries.
  Lock acquisition is encapsulated in small helpers so the slow path
  reads as plainly as the hot one.

Also document the build pipeline (BuildSession, AnalysisEngine,
HostCache, parallel scheduling, last-reader provider move, input
digest composition) in `docs/guide/apple-graph.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cache-related apple_spec examples seed the build with an
unconditional `once --format json build` before the `When call`. On
Linux runners `Skip if 'apple toolchain unavailable on this host'`
correctly skips the assertions, but shellspec still executes the
example body, so the priming run blows up with `xcrun not found on
PATH` and the example aborts with exit 2.

Mask the priming with `|| true` (and the `printf` followup with the
same) so the preamble is a no-op when the toolchain is unavailable,
while still doing useful work on macOS where the assertions run for
real.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 1 of Buck2 / Bazel parity. The previous provider record
exposed only `{swiftmodule_dir, archive}` — enough for a sibling
library to find this module's interface, but nothing for a future
linker, bundler, or test rule to compose its own command line.
Provider expansion is the load-bearing piece every other Round
depends on; without it modulemap generation, mixed-language
compilation, and multi-arch linking each have to rediscover the
graph.

New provider fields (rules_swift `SwiftInfo` / `CcInfo` shape):
* `objc_header`   — generated `<Module>-Swift.h` so downstream ObjC
  consumers can `#import` Swift symbols
* `transitive_swiftmodule_dirs`, `transitive_archives` — for `-I` and
  link inputs
* `transitive_sdk_frameworks`, `transitive_weak_sdk_frameworks`,
  `transitive_sdk_dylibs` — frameworks/dylibs propagated to the link
* `transitive_linkopts`, `transitive_defines` — flags / `-D` macros
  that flow with the module

A `_collect_transitive` helper keeps insertion order while deduping,
matching Bazel's depset(order=topological) semantics.

New schema attributes:
* `target_sdk_version` — distinct from `minimum_os` (Buck2 splits
  deployment target from build SDK; we follow suit)
* `defines` — `-D` flags, propagated transitively
* `emit_dsym` — adds `-g` to swiftc so a future dsymutil step has DWARF
  to extract a `.dSYM` from

`apple_library` now always emits the ObjC interop header alongside
the `.swiftmodule` / `.swiftdoc` / `.a`. The spec asserts the new
output exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread crates/once-frontend/prelude/apple.star
Round 2 of Buck2 / Bazel parity for apple_library.

* `exported_deps` (Buck2 parity): direct deps are private to this
  target's compile -I path; only `exported_deps` flow their
  transitive swiftmodule_dirs through to consumers. Link-affecting
  fields (archives, frameworks, linkopts, defines) still propagate
  unconditionally because the link line needs everything reachable.
  The provider now also surfaces `label_id` so the impl can match
  dep records against the exported_deps list.
* `sdk_variant` (`simulator` | `device`): picks `iphoneos` vs
  `iphonesimulator`, `appletvos` vs `appletvsimulator`, etc., and
  strips the `-simulator` triple suffix for device builds. macOS
  ignores the variant.
* `xcode_developer_dir`: pins a specific Xcode by overlaying
  `DEVELOPER_DIR` on the `xcrun` invocation. The dir is folded into
  `swiftc_identity` so different Xcodes partition the action cache.
* `alwayslink`: provider hint a future linker rule will read to emit
  `-Wl,-force_load` for archives that need whole-archive linking
  (XCTest test discovery, ObjC categories). Each
  `transitive_alwayslink_archives` is accumulated alongside the
  normal `transitive_archives` list.

Supporting changes:
* `host_command(argv, env=...)` now takes an optional env dict on
  the starlark side. The Rust `HostCache` keys results on
  `(argv, env)` so a different `DEVELOPER_DIR` produces a different
  cache slot.
* `run_action(..., env=...)` was already plumbed; the prelude now
  passes the xcrun env through so the swiftc compile honours the
  pinned Xcode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread crates/once-frontend/prelude/apple.star
Comment thread crates/once-frontend/src/analysis.rs
pepicrft and others added 3 commits June 10, 2026 19:26
Round 3 of Buck2 / Bazel parity. apple_library now compiles real
mixed-language libraries:

* Source filters route each glob match to its own driver:
  swiftc for `.swift`, clang for `.m`/`.mm`/`.c`, clang++ for
  `.cc`/`.cpp`/`.cxx`.
* `bridging_header` attr threads `-import-objc-header <path>` into
  swiftc so Swift sources see ObjC symbols. The bridging header and
  every `exported_headers` entry feed the swiftc action as
  hash-tracked inputs so an edit to either invalidates the cache.
* `_xcrun_clang` resolves `clang` + the SDK sysroot via
  `xcrun --show-sdk-path` and folds both plus DEVELOPER_DIR into a
  separate `once.apple.clang.v1` identity so clang and swiftc
  partition the cache independently.
* Each non-Swift source becomes its own `clang -c` action keyed
  by a sanitised filename, so independent objects compile in
  parallel through the existing BuildSession scheduler.
* `_xcrun_libtool` runs a static `libtool` merge that combines the
  Swift-only intermediate `<module>-swift.a` with every clang
  object into the final `<module>.a`. Swift-only libraries skip
  the merge and emit the archive directly from swiftc;
  Swift-less libraries libtool the clang objects on their own.
* `exported_headers` now flow through the provider as workspace-
  relative paths plus parent-dir lists. Consumers' compile gets
  `-I` for each transitive header dir, and swiftc receives them as
  `-Xcc -I` so its underlying Clang invocation can locate dep
  headers via the bridging header.
* New attr: `clang_flags` is honoured per-source.

Fixture: `fixtures/apple_library_mixed` declares a `Mixed` library
with a bridging header, an ObjC `.m`, and a Swift `.swift` that
calls into the ObjC class. The new shellspec assertion verifies
the swift+objc compile produces `Mixed-swift.a`, the per-source
`.o`, and the libtool-merged `Mixed.a`.

Deferred to Round 3b: modulemap generation, header maps,
`enable_modules`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 3b — clang modules support for apple_library.

* New `write_file(path, content)` starlark global emits a literal
  file as a cacheable action. Content is folded into the action's
  `toolchain_identity` so any edit to the rendered text
  invalidates the produced file.
* `enable_modules = true` triggers modulemap generation from
  `exported_headers`. The modulemap is written to
  `<build_dir>/module.modulemap` and reference each exported header
  by a path relative to its own location.
* Modulemaps propagate transitively via `transitive_modulemaps`.
  Consumers get `-fmodule-map-file=<path>` on the clang command line
  and `-Xcc -fmodule-map-file=<path>` on the swiftc command line so
  `import` of a clang-module dep resolves without manual flags.
* `-fmodules` is added to clang and `-Xcc -fmodules` to swiftc when
  the attr is set.
* The mixed fixture now declares `enable_modules = true` and the
  manual e2e run on macOS confirms the modulemap renders correctly
  alongside the libtool-merged archive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document the new mixed-language compile flow (swiftc + clang +
libtool), the provider record shape consumers read, and the full
attribute reference (platform/triple, toolchain pin, sources,
module flags, link inputs, dep edges). Also call out the still-
open follow-ups (multi-arch + lipo, select() / transitions, Swift
macros, header maps) so the parity gap with Buck2 / Bazel is
explicit in the docs rather than buried in commit history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread crates/once-frontend/src/analysis.rs Outdated
Comment thread crates/once-frontend/src/analysis.rs
pepicrft and others added 8 commits June 11, 2026 11:39
`dep["label_id"]` carries the normalised target id (e.g.
`apps/ios/AppCore`), but `exported_deps` entries come straight from
`[target.attrs]` and may be written as `./Sibling`, `../web/Common`,
or a root-relative `apps/ios/AppCore`. The old direct-membership
check (`dep_label in exported_deps`) silently failed for every
relative-style reference, so the compile-time privacy boundary
defaulted to "nothing exported".

Add `_resolve_dep_ref(ref, package)` to the prelude — same
semantics as Once's Rust-side `normalize_manifest_target`,
implemented in starlark so the impl can normalise on the fly. The
exported_deps loop builds a normalised id set and tests dep label
membership against it. Verified end-to-end by building Greeter
with `exported_deps = ["./AppCore"]` under `[target.attrs]`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous `write_file` script inlined the shell-quoted path
directly inside `$(dirname ...)`. A path containing a single quote
serialises to the `'a'\''b'` form, which the shell tokenises
inside command substitution before `dirname` sees it; the result is
brittle and produces wrong output for paths with quotes.

Bind the path to `__once_path` first, then derive the parent
directory with the POSIX `${var%/*}` parameter expansion. The
content is still shell-quoted as before. Add focused unit tests
for `shell_quote` edge cases, structural assertions on the emitted
script, and an end-to-end check that runs the generated script on
a path with a quoted parent directory to lock in the fix. Also
cover the `CommandKey` env-aware caching with a test that proves a
different env value produces a distinct cache slot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a fixture whose Swift source uses `#error("...")` inside
`#if !ONCE_DEFINES_PRESENT`, so a successful build is positive
evidence that `defines = ["ONCE_DEFINES_PRESENT"]` made it to
swiftc. The same target also flips `emit_dsym = true`, exercising
the `-g` path. The new shellspec example asserts the build
completes and the canonical Swift outputs land in `.once/out`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Move `guide/apple-graph.md` → `guide/graph/apple.md` so the Apple
  rule deep-dive lives under a real Graph section instead of being a
  loose sibling.
* Add `guide/graph/index.md`: the conceptual orientation for Once's
  graph — three-layer model (scripts → script targets → graph
  targets), what a target looks like in `once.toml`, the built-in
  rule set, capabilities, providers, action caching + `BuildSession`,
  the configurability story, and the agent workflow surface.
* Reorder the sidebar so Scripts (the migration ramp) appears above
  Graph (where teams move when they need richer relationships). Graph
  now mirrors Scripts with an Overview entry above the rule deep-dive.

Verified by `vitepress build`: both `guide/graph/index.html` and
`guide/graph/apple.html` render and the sitemap picks them up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convention is to keep em dashes out of user-facing docs (rewrite with
a colon, comma, semicolon, or sentence break). Rewrite the dashes in
guide/graph/index.md and the one I missed in guide/graph/apple.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The page had grown into a mid-depth tutorial: rule-impl mechanics,
BuildSession internals, the configurability epic, agent workflows.
Move all of that down to the per-domain deep dives where it belongs.

The overview now keeps just the load-bearing concepts:

* Where the graph fits in Once's three-layer model.
* What a target looks like in `once.toml`.
* The rule domains the prelude ships today (Apple), linked to the
  deep dive.
* Capabilities the CLI dispatches on.

About 50 lines instead of 150.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a `Reference` top-nav entry that points at `/reference/`, with a
dedicated sidebar listing every top-level subcommand. The CLI pages
under `/reference/cli/` are generated, not hand-written.

Generation lives in a hidden `once reference --out <dir>` subcommand
implemented in `crates/once-cli/src/reference.rs`. It walks the
`clap::Command` tree exposed by `Cli::command()`, skips hidden
commands and the auto-added `help`, and emits one markdown file per
node:

  out/
    index.md       link list of top-level commands
    build.md       one file per top-level command
    cache.md
    cache/
      stats.md     nested commands live under their parent's dir
      action.md
      action/get.md
      ...

Each page renders the about line, a synopsis box, the long-about
description, a positional Arguments table, and an Options table with
flag/value/default/description columns. Intermediate commands list
their subcommand children as links.

Wired into `docs/package.json` as `build:reference`, which is run
before `vitepress build` and `vitepress dev` so the site never sees a
stale snapshot. Re-running it after touching `crates/once-cli/src/cli.rs`
keeps the docs in lockstep with the binary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread crates/once-cli/src/reference.rs
Comment thread crates/once-cli/src/reference.rs
The `build:reference` script ran `cargo build --release -p once-cli`
before `vitepress build`, but the GitHub Actions docs job has no
`libcap-ng-dev` installed (it's only set up on the test/build jobs
per AGENTS.md), so the link step failed with
`rust-lld: error: unable to find library -lcap-ng` and `build docs`
went red.

The generated markdown is committed to the repo, so the docs build
only needs to render what's on disk. Drop the pre-hook from both
`build` and `dev` scripts; keep `build:reference` available as a
manual command to refresh the markdown after touching
`crates/once-cli/src/cli.rs`. The Reference overview now documents
that workflow.

While here, expand `long_about` on every top-level command (build,
test, cache, auth, toolchain, query, runtime) so the generated
pages render a real Description section instead of just the short
about line, and teach the generator to drop the redundant leading
summary that clap auto-prepends to `long_about` so the page doesn't
print the same sentence twice. Reference markdown re-generated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread crates/once-cli/src/reference.rs
pepicrft and others added 3 commits June 11, 2026 16:09
* Hoist the subcommand bullet's `parent` path to a local so every
  interpolation in the link line is an inline capture; the prior
  format string mixed an implicit positional with three named
  captures, which read like a bug even though the output was right.
* Split `write_command_files` into `render_command_page` plus
  `build_synopsis`, `render_arguments`, `render_options`, and
  `render_subcommands` helpers. The original was a 100+ line
  function that tripped the clippy `too_many_lines` lint after the
  format tweaks; each piece is now small enough to read top to
  bottom.
* Drop the "Generated from `crates/once-cli/src/cli.rs`. Re-run
  `npm run build:reference`" disclaimer from the rendered index
  page; that line was meant for contributors, not first-time
  readers, and it leaked an internal source path into the public
  docs. Replace with a one-line orientation aimed at the reader.
* Add unit tests for `trim_leading_about` (about with and without
  trailing period, long that exactly matches about, missing about,
  empty input), `file_path_for` (top-level, two-level, three-level
  nesting), `walk` (skips hidden subs, descends, records about /
  long_about / children), and end-to-end `write_command_files` /
  `write_index` (asserts synopsis / description / subcommand
  rendering and that the index never leaks the internal source
  paths). 13 tests in the `reference::tests` module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Link `build`, `run`, `test`, and `query` mentions in the Graph
  overview to their CLI reference pages so the first time a reader
  sees a verb they can pivot straight to the flag/argument detail.
* Rewrite the standalone Reference index page from a contributor
  note ("Generated from clap definitions in
  crates/once-cli/src/cli.rs. Run npm run build:reference …") into
  a one-line orientation pointed at the reader. The "how it stays
  in sync" detail is a maintainer concern; it doesn't need to
  greet first-time visitors.
* Re-run the generator so `docs/reference/cli/index.md` picks up
  the matching copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A first-time reader landing on guide/graph/apple cares about what the
Apple rule set does today, not about the RFC it implements or the
Bazel and Buck2 rules we drew inspiration from. Rewrite the opening
into a single load-bearing paragraph and move the RFC plus prior-art
links to a "Prior Art and Tracking" footer at the bottom of the page
as credits/references.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pepicrft and others added 5 commits June 11, 2026 16:17
…copy

* Rewrite `docs/guide/graph/apple.md`. The Commands walkthrough used
  to be a dense soup of `xcrun --sdk`, `swiftc -emit-library`, and
  `clang -c` flags interleaved with prose; split it into a short
  "what build/run/test produce" section and a focused
  "apple_library compile" subsection with three labelled bullets
  (Swift, C-family, mixed) plus a one-line note on
  modulemap propagation. Drop the "Built-In Rules" line that pointed
  at `crates/once-frontend/prelude/apple.star`, and remove the "Rule
  Implementations" and "Build Pipeline" sections wholesale (they
  exposed internal Rust types like `BuildSession`,
  `AnalysisEngine`, `Arc<GraphTarget>`, `tokio::task::JoinSet`, and
  `JsonValue` that the reader doesn't have to call). The provider
  table becomes a short prose paragraph that lists the field
  families instead of every name.
* Stop referencing `crates/once/swift/Once.swift` in the Swift SDK
  guide; describe the vendoring step in user terms instead.
* Add a "no source code paths in user-facing docs" rule under
  `AGENTS.md` Style. Source paths rot under refactors, mean nothing
  to a reader who isn't editing the repo, and leak implementation
  detail through the public surface. Describe behavior, link to the
  reference, or quote `once.toml` shapes instead.

Verified by `vitepress build` (clean) and a final grep sweep for
`crates/`, internal Rust type names, and em dashes (all zero) over
`docs/guide/` and `docs/reference/index.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Define "rule schema" the first time the term appears. In the Graph
  overview the third bullet of the three-layer model glosses it
  ("the contract for a kind: which attributes it accepts, which
  providers each dep edge expects, which providers it emits, and
  which capabilities it exposes"). In the Apple page a one-sentence
  follow-up under the `once query schema` example carries the same
  definition at point of use. Once the term is grounded the later
  "Schema introspection" mention in the agent workflows section
  reads naturally.
* Drop the "follows RFC 0001: Once Build Graph" link from the Apple
  page's Prior Art section. Reader-facing docs don't need to
  reference internal planning artifacts; what they do need (Bazel
  rules_apple, Bazel apple_binary, Buck2 apple_library and friends)
  stays.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`once mcp` runs an MCP 2024-11-05 server over stdio so a coding
agent (Claude Desktop, an IDE plug-in, an Anthropic SDK script) can
inspect the typed build graph without scraping CLI prose. The
server handles the initialize handshake, the `notifications/
initialized` post-handshake notification, `tools/list`, and
`tools/call`, then surfaces three read-only tools that wrap the
existing query verbs:

  - once_query_targets: list every declared target, optionally
    filtered by rule kind.
  - once_query_capabilities: return the capabilities a target
    exposes, with output groups and required inputs.
  - once_query_schema: return the typed contract for a rule kind.

Each tool returns the same record shape the corresponding
`once query` command produces, serialised to JSON inside the MCP
`content` envelope. Tool-level failures land as `isError: true`
results so the agent sees the message instead of a protocol error.

Action invocation (call a graph action, return a digest, query the
cached outputs / logs / provider record by that digest later) stays
follow-up work; the action cache and CAS already key everything by
digest, so the read surface is ready to grow into that shape when
it lands.

Wiring:
- `Cmd::Mcp { workspace }` accepts an optional `--workspace` and
  falls back to the global `-C/--directory`.
- `dispatch::run_command` now splits each multi-subcommand verb
  (toolchain, query, runtime) into a small helper so it stays under
  the clippy line-count limit.
- Reference docs regenerate to include `once mcp` and the sidebar
  picks it up between `exec` and `query`.
- The Apple Graph page swaps its "agent workflows (planned)"
  blurb for a real walk-through of the three tools.

Verification:
- 7 new unit tests in commands::mcp::tests (initialize handshake,
  tools/list contents, unknown method, missing-argument tool error
  surfacing through `isError`, parse-error null-id reply,
  notifications suppressing the reply, schema tool returns the
  apple_library contract). 197/197 lib tests green.
- Manual stdio smoke test against fixtures/apple_library: feeding
  initialize + notifications/initialized + tools/call returns the
  two-target list as JSON content (see commit body of follow-up
  if regression).
- vitepress build clean; cargo clippy + fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CLI reference at `/reference/cli/mcp` covers the binary flags,
and the Apple Graph guide page walks through the tools in prose,
but the protocol surface itself (transport, handshake, tool input
schemas, return shapes, error model, Claude Desktop wiring) had no
reference home.

Add `docs/reference/mcp.md`. Documents:

* Transport: newline-delimited JSON-RPC 2.0 over stdio, how to spawn
  the server with `--workspace` or `-C/--directory`.
* Handshake: the MCP 2024-11-05 sequence (initialize, the server
  reply, notifications/initialized, tools/list, tools/call), with
  literal request/reply JSON for the first round-trip.
* Tool catalog: each of the three tools (`once_query_targets`,
  `once_query_capabilities`, `once_query_schema`) gets its input
  schema and an example return shape that matches what
  `once query --format json` emits.
* Error model: a table mapping each failure surface (parse error,
  unknown method, malformed params, unknown tool, missing argument,
  no-match lookup) to whether it lands as a JSON-RPC error or an
  `isError: true` tool result.
* Worked example session showing a full client/server exchange.
* Claude Desktop `mcpServers` config snippet so readers can wire
  the server into Claude Desktop without leaving the page.
* "Not yet on the wire" note that the read surface is ready for the
  call-and-query-by-digest follow-up.

Reference index page now lists the CLI and MCP references side by
side. Sidebar gets a "Protocols" group under the CLI group so the
MCP page is one click from anywhere in the reference. Apple Graph
agent-workflows blurb now links the term "Model Context Protocol"
to the new reference page so readers can pivot from the use-case to
the spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…evel

* Restructure the MCP reference into a top-level sidebar group with
  Overview + Tools sub-pages, sitting alongside CLI. The hand-written
  Overview at `/reference/mcp/` keeps the stable prose (transport,
  handshake, error model, example session, Claude Desktop wiring).
  The Tools page at `/reference/mcp/tools` is generated.
* Drop the "Not yet on the wire" roadmap section from the overview;
  reference docs describe what's on the wire, not what isn't.
* Refactor `commands::mcp::tool_definitions()` into a shared
  `tool_catalog()` returning structured `ToolDefinition` records
  (name, short description, long markdown description, JSON Schema
  input, and an example return). The runtime `tools/list` reply and
  the doc generator now read from the same record, so the names,
  descriptions, and schemas can't drift.
* Teach `once reference --out <root>` to write both `cli/*.md` and
  `mcp/tools.md` under one root. `docs/package.json` `build:reference`
  switches from `--out docs/reference/cli` to `--out docs/reference`
  and pre-cleans both targets. `Cmd::Reference` is unchanged on the
  surface; only the layout under the target dir grows.
* Regenerate the reference; the new `docs/reference/mcp/tools.md`
  carries each tool's section, input schema (pretty-printed from
  the same JSON Schema the server validates against), and example
  return. The reference index page and the Apple Graph agent-
  workflows blurb relink to `/reference/mcp/` (trailing slash for
  the directory route).

Verified: cargo clippy + fmt clean, 197/197 lib tests green
(commands::mcp::tests 7/7, reference::tests 13/13), vitepress build
clean with both reference subtrees rendered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pepicrft pepicrft merged commit 46f14b2 into main Jun 11, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant