RFD-0030 — ox doc --document-deps and external-package documentation
Question
How does ox doc document a workspace whose dependencies resolve outside the workspace tree — registry, git, or path-deps — and how does the resulting site link out to those packages’ documentation?
Context
RFD-0029 ratified ox doc as a thin renderer over a stable JSON IR (oxc-doc::Site). RFD-0029 §3.2 + Phase 4 ship the React + Vite SSG that consumes that IR and emits the static HTML site. What neither RFD addressed is the cross-package navigation problem when the deps aren’t local.
Concretely, today (2026-05-05):
- A workspace whose members are all path-deps inside the workspace tree (e.g.
examples/lease-storywith all 7 packages underpackages/) renders correctly. Cross-package qname links resolve because the consumer site can find every dep’sSite.item_pagesin the bundle envelope. - A typical project — single-
[project]ox.tomldeclaringcofris = "0.2.2"from the registry — also “renders”: every concept page, every relation, every rule. But cross-package qname links 404. The renderer synthesizes URLs of the formcofris/0.2.2/cofris/concept.JournalEntry.htmlagainst the sametarget/doc/tree, butcofrislives at~/.argon/packages/cofris/0.2.2/(the cache), not undertarget/doc/. Links break silently. - Workspaces mixing path-deps and registry-deps work for the path side and 404 for the registry side. The new external-deps row in the workspace dep graph (RFD-0030 prep, landed 2026-05-05) at least makes the externals visible, but they have no click target.
The user-facing surface needs to handle the registry case as the default — it’s how most projects will be structured once Argon’s package registry is populated.
Cargo’s design is the reference. cargo doc documents the workspace + its transitive dependencies by default; --no-deps opts out. The output tree at target/doc/ is self-contained. Argon should match this UX: ox doc produces a docs tree that works whether or not the user has internet access, with deps’ source coming from the local cache.
Decision
Adopt Cargo’s mental model with three concrete commitments:
-
ox doc --document-depsbuilds docs for transitive intra-workspace + cached registry/git/path deps, producing a self-containedtarget/doc/tree. Default-off behaviour matches the user’s current expectation. The flag flips whendocs.argon.devships and--no-depsbecomes the new default — at that point externals get rewritten tohttps://docs.argon.dev/<pkg>/<ver>/URLs by the renderer. -
Dep docs come from the local resolver cache at
~/.argon/packages/<pkg>/<ver>/. The cache is already populated byox installand content-hash verified againstox.lock.ox doc --document-depswalks the lockfile’s transitive closure, opens each dep’s manifest, builds itsSiteagainst the cached source, and includes it in the bundle envelope passed to argon-doc-ui. No network. No registry-side cooperation required. -
The IR doesn’t change.
BundleEnvelope.sites: Vec<Site>already accepts arbitrary members;--document-depsjust feeds it more. The workspace dep graph’s “external” row collapses to zero when every external dep is locally documented (the IR builder demotes externals into fullWorkspaceMember-equivalents at envelope-construction time).
Mechanics
$ ox doc --document-deps # documents workspace + every transitive dep
$ ox doc # default: workspace only (today's behaviour)
$ ox doc --no-deps # explicit opt-out (post-docs.argon.dev default)
The implementation seam is argon/ox/src/main.rs::run_doc_workspace (and run_doc_single). After collecting the workspace’s own member sites, when --document-deps is set:
- Read
ox.lockto get the transitive closure of every member’s deps with their pinned versions. - For each external dep (source = registry / git / non-workspace path), locate the cached source:
~/.argon/packages/<pkg>/<ver>/. Verify content-hash against the lockfile entry; refuse divergence (same rule asox audit). - Build each dep’s
Sitevia the samebuild_doc_for_projectpath used for workspace members. Each dep is treated as a single-package project for elaboration purposes — its own dep-resolution closure is what the lockfile already pinned, no fresh resolution needed. - Append every external
SitetoBundleEnvelope.sites, mark them withis_external_dep: trueonSiteso the renderer can apply distinct breadcrumb/sidebar treatment (“From cofris 0.2.2 (registry)” subtitle on the package index, slightly muted card on the workspace landing). - The bundle envelope is invoked once as today — the SSG just iterates more sites.
Lockfile interaction
The lockfile is the source of truth for which dep versions get documented. ox doc --document-deps against a workspace with ufo = "0.2.4" in ox.lock documents ufo@0.2.4 from the cache. If ox.lock is missing, --document-deps errors out with a pointer to ox install (consistent with ox audit).
Caching strategy
Per-dep doc rendering is content-addressable. The output of building cofris@0.2.2’s Site from ~/.argon/packages/cofris/0.2.2/ depends only on:
- The dep’s source content-hash (already in
ox.lock). - The IR schema version (
oxc-doc::DOC_IR_SCHEMA_VERSION, currently1.1.0). - The set of doc extensions in scope from packages the dep transitively depends on.
The natural cache path is ~/.argon/cache/doc/<source-hash>-<schema>.json carrying the serialized Site. ox doc --document-deps checks this cache before re-elaborating; cache hits skip the entire elaborate pipeline for that dep. Cache invalidation is automatic — content-hash and schema bumps both produce new keys.
This is a substantial perf win for the common case: a workspace’s deps don’t change between ox doc runs, so re-rendering only touches the workspace’s own changed members.
URL synthesis
Three regimes:
| Mode | External-dep qname URL synthesis |
|---|---|
ox doc (default) | Cross-package links to deps render as styled text with no href (current Phase 4 behaviour for unresolvable qnames). |
ox doc --document-deps | Cross-package links resolve to relative paths in the same target/doc/ tree (../../<pkg>/<ver>/<pkg>/concept.X.html). |
ox doc post-docs.argon.dev | Cross-package links resolve to https://docs.argon.dev/<pkg>/<ver>/<pkg>/concept.X.html. The renderer reads the registry’s base URL from a workspace-level [doc] config or a built-in default. |
The third regime is preserved as the eventual default once docs.argon.dev exists. Until then, --document-deps is the way to get clickable cross-package links.
Workspace dep graph
WorkspaceSite.external_dependencies carries the externals today. Under --document-deps, the IR builder demotes each documented external into a full WorkspaceMemberSummary (with depth = 0 since externals are by definition foundations from the workspace’s perspective; sub-depth ranking among externals reads the dep graph and is computed inside the document-deps walk). The “external” row collapses; the foundation tier expands. Visually: the dep graph shows everything as first-class.
Site.is_external_dep flag
Pages from external deps are rendered identically to workspace members but marked so the renderer can:
- Add a “Dependency” subtitle on the package index (“Documented from
~/.argon/packages/cofris/0.2.2/”). - Disable doc-test execution (
ox test --docruns only on workspace-owned items per RFD-0029). - Skip the “View source on <host>” affordance once that lands.
The flag is a single optional field on Site; doesn’t change the wire shape for workspace-only renders.
Non-goals
- Network fetching during
ox doc. Deps must already be in the local cache.ox installis the network-touching command;ox docreads from disk only. (Future:ox doc --document-deps --auto-installcould implicitly invokeox installfirst, but that’s a UX-layer convenience over this RFD’s mechanics.) - Per-dep doc-extension scoping.
[package.docs.panel]declarations in dep manifests already scope by transitive workspace dep closure (RFD-0029 Phase 4 fix).--document-depsdoesn’t change this rule — a dep’s own manifest panels apply to its own pages, governed by the same closure rule. - Cross-version doc rendering (e.g. documenting
ufo@0.2.3andufo@0.2.4simultaneously). The lockfile pins one version per package; that’s what gets documented. Multi-version is acargo docnon-feature too. - Registry-side doc hosting.
docs.argon.devis out of scope here. This RFD specifies the local-side URL synthesis once that host exists; the host’s own architecture (build-on-publish, on-demand build, etc.) is a separate problem.
Open questions
-
Should
--document-depsbe the default beforedocs.argon.devships? Cargo defaults to documenting deps; we currently don’t. Defaulting to--document-depsproduces a working docs tree out of the box but slows downox docsubstantially (factor of N for an N-dep workspace, mitigated by the doc-cache). My current bias: default off until docs are fast enough that the perf hit is invisible, then flip. Open for discussion. -
Lockfile-less
--document-deps. A workspace member may runox doc --document-depsfrom a checked-out source tree that happens to haveox.lockpresent but never resolved (e.g., fresh clone,ox installnot yet run). Hard error or silently skip externals? Current bias: hard error pointing toox install, matchingox audit’s posture. -
Doc-cache eviction policy. Content-hashed entries never collide, but the cache grows monotonically. Evict by LRU once
~/.argon/cache/doc/exceeds N MB? Track via size + atime, periodic compaction inox install? Defer for now — wait for the cache to actually grow before solving. -
Path-dep externals semantics. A workspace member declaring
cofris = { path = "../shared/cofris" }has a path-dep external. Should--document-depsdocument it? Cargo’s behaviour: yes, but the path becomes a stability hazard (a path-dep at a different version than the workspace’s other consumers). My bias: yes, with a stern warning when the documented version diverges from any sibling member’s resolved version. -
is_external_depflag granularity. Single boolean is enough for the v0 surface, but future work (semver-check, doc-coverage, dep-graph analysis) may want richer metadata: source-kind, content-hash, lockfile-pinned version. Bias: ship the boolean now; widen to a struct (Option<ExternalSource>) when a second consumer needs it.
Implementation plan
| Phase | Work | Status |
|---|---|---|
| 0 | External-dep visibility in workspace dep graph (the “external” row, no click target). | Landed 2026-05-05 |
| 1 | RFD ratified + lockfile-walking helper in ox::resolve that yields the cached source path + content-hash per transitive dep. | This RFD |
| 2 | --document-deps flag plumbed through ox doc → run_doc_workspace + run_doc_single. Per-dep build_doc_for_project invocation against the cached source. | Follow-up commit |
| 3 | Doc-cache at ~/.argon/cache/doc/<hash>-<schema>.json. Skip elaborate when hit. | Follow-up commit |
| 4 | Site.is_external_dep: bool IR field + renderer-side breadcrumb / subtitle treatment. | Follow-up commit |
| 5 | URL-synthesis switch for the docs.argon.dev regime — workspace [doc] config field + renderer’s qname resolver respects it. | When docs.argon.dev exists |
| 6 | Default flip: ox doc becomes --document-deps (or docs.argon.dev-rewriting) by default; --no-deps opts out. | Coordinated with docs.argon.dev launch |
Connections
- Builds on RFD-0029 — Doc comments and
ox doc. The IR contract there is the load-bearing artifact this RFD extends. - Coordinates with the future
docs.argon.devregistry-doc host (separate effort). - Touches
ox.lockcontent-hash invariants — see RFD-0024 — Diagnostic codes for the OE3xxx package-loader code namespace; new errors here register under that scheme.