feat(node): W1c — composition root, Node exposes 7 seams as Arc<dyn> handles (#1285)#72
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
Realizes the #1285 W1c milestone with the LOCKED "Option A" shape (main decider): Node stays ONE concrete struct implementing all 7 seam traits (unchanged from W1b) — it is NOT split into 7 separate structs. The composition root exposes each seam as a self-referential Arc<dyn SeamTrait> upcast of the SAME Arc<Node> (Node::as_chain_source/as_peer_network/ as_rpc_dispatch/as_content_server/as_capsule_store/as_key_manager — no Arc::new_cyclic, no separate object, so no possibility of the seam-4<->5 / 5<->6 cross-seam cycles the W1c design recon surfaced). dig-node-service (the single user-facing binary) demonstrates the pattern end-to-end: AppState now holds content_server: Arc<dyn ContentServer> alongside node: Arc<Node> — the SAME object, two shapes — and the loopback plaintext serve path (server::serve_resource/serve_miss) is wired through the seam handle instead of the concrete Node. This makes the seam boundary genuinely injectable (a test/FFI caller can hold Arc<dyn ContentServer>) and lets a later wave (W2-W5) repoint ONE such handle at a different concrete implementation without touching the other 6 seams or any consumer holding a different seam's handle. The INNER cross-seam reaches the W1b carves left in place (self.proxy, self.p2p_content(), the direct self.anchored_root_resolver field read, self.maybe_backfill_capsule, RpcDispatch::dispatch calling into every other seam) are UNCHANGED by this pass, deliberately — de-tangling them into true per-seam structs is deferred to the follow-up epic #1357, sequenced after W2-W5 (which reshape several of these exact edges as each seam's concrete implementation is actually replaced; de-tangling now risks redoing the same work again). Folds dig-node-service's dig-urn-resolver dep 0.3.1 -> 0.4.0 (main-confirmed wire-compatible, byte-identical). Adds SPEC.md §13 "Internal architecture — the 7 seams": the trait/module table, the cross-seam-vocabulary-only rule, the Option-A composition-root shape, the thin-binary invariant, and the #1357 deferred-de-tangle note. Version: MINOR (dig-node-core 0.13.8->0.14.0, workspace/binary 0.47.8-> 0.48.0) — the outer Arc<dyn> handoff adds new public API (the 6 upcast constructors on Node) and changes dig-node-service's AppState shape (additive: a new content_server field, still built only via build_state). No existing public signature was removed or changed; this is a compatible new-capability bump, never a breaking one. Green bar: cargo build/test --workspace --locked exit 0, 0 failures across every suite INCLUDING openrpc_drift_guard (7/7 passed — the objective invariant proving the served RPC surface didn't drift). cargo clippy --workspace --all-targets --locked -D warnings clean. cargo fmt --all -- --check clean. Depends on W1b-6 (merged, #1285). This is the #1285 MILESTONE — on merge the 7-seam architecture is fully realized: 7 architecturally-separated seam traits + one thin glue binary holding no seam logic. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
W1c correctness gate (fresh context, effort:high) — PASS on code.
Verified against PR head aaa087c (full diff = 7 files, +135/-10; git diff 3c72d75..aaa087c matches exactly — no hidden changes):
- Upcasts are the SAME object, no new alloc — all 6 as_() are
Arc::clone(self) as Arc<dyn Trait>(lib.rs), an Arc→Arc unsizing coercion. No Arc::new / Arc::new_cyclic, no state copy. Same object ⇒ same state ⇒ behaviour-preserving. - No behaviour change from the rewire — AppState.content_server = node.as_content_server() (server.rs build_state); the 3 loopback sites (serve_resource, serve_miss ×2) now dispatch through Arc to the SAME Node impl (impl ContentServer for Node, content_serve.rs:267). serve_content_plaintext + manifest_paths are trait methods with identical signatures — dynamic dispatch to the identical code.
- openrpc_drift_guard — RPC surface untouched (rpc.rs/meta.rs/dispatch not in diff), so the 7/7 no-drift invariant cannot have drifted; the guard test runs in the CI 'Test + coverage' job.
- No new tangle — diff introduces ONLY the 6 additive upcast ctors + the AppState field + 3 rewired sites. No new cross-seam reach; self.proxy/self.p2p_content/self.anchored_root_resolver are unchanged from base = exactly #1357's pre-existing scope.
Also: MINOR bump justified + both files agree (core 0.14.0, service 0.48.0, §5.1-additive: 6 new pub ctors + additive AppState field); dig-urn-resolver 0.3.1→0.4.0 present (wire-compatible); SPEC §13 accurate + references #1357; 0 unresolved review threads.
BEFORE MERGE (non-code, orchestrator): (a) PR title + body are STALE — title still reads '(DESIGN RECON, awaiting decider)' and the body is the recon note; the decision is locked (Option A) and this IS the implementation. The title becomes the squash-commit message, so update it to a clean conventional-commit summary that references #1285/#1357 (body should reference #1357). (b) CI: 'Test + coverage' and the native-package builds were still IN_PROGRESS at review time — merge only once all required checks are green.
W1c — the #1285 composition-root milestone (Option A, locked by the orchestrator).
Arc<dyn SeamTrait>via 6 self-referential upcast constructors (as_chain_source/as_peer_network/as_rpc_dispatch/as_content_server/as_capsule_store/as_key_manager) — eachArc::clone(self) as Arc<dyn Trait>(same object, no new alloc). Consumers/FFI/tests holdArc<dyn SeamTrait>(injectable fakes + per-seam swappable for W2-W5).content_server: Arc<dyn ContentServer>; loopback serve rewired through the seam handle (e2e demonstration).Arc<dyn>;openrpc_drift_guard7/7 green.dig-urn-resolver0.3.1 -> 0.4.0 (wire-compatible). Added SPEC §13 (seam architecture).Refs #1285. Inner seam de-tangle (multi-struct decomposition, cross-seam via trait handles, cycles broken by injection) deferred to #1357, sequenced after W2-W5 reshape the 4<->5 / 5<->6 edges.