A consumer gate for the workspace: a crate is not "done" until something in the workspace actually calls it
Источник:
dev/fera-research/docs/oc4/idea-06-workspace-consumer-gate.md(read-only; сгенерировано scripts/sync-docs.mjs)
A consumer gate for the workspace: a crate is not “done” until something in the workspace actually calls it
Section titled “A consumer gate for the workspace: a crate is not “done” until something in the workspace actually calls it”- kind: idea
- area: tooling / testing / architecture
- evidence:
- Measured state of
~/dev/fera/fera(15 workspace members, 3,821 Rust lines): 6 crates hold real logic (protocol, domain, storage, ui-contract, ui-elm, ui-engine-vello = 3,736 lines) and all 6 have zero consumers outside their own test modules. ~900–1,000 of those 3,736 lines (~27%) are unreachable from any executable path. MemoryEngine(~/dev/fera/fera/core/crates/storage/src/memory.rs:18-26) — fully implemented, contract-tested, zero non-test callersOutboxRetry/RetryEntry/Backoff(~/dev/fera/fera/core/crates/domain/src/outbox.rs,reconnect.rs) — ~200 lines of correct, tested retry/backoff policy, zero non-test callersParleyTextEngine::layout(~/dev/fera/fera/core/crates/ui-engine-vello/src/text_engine.rs:94-103) — real Parley 0.11 integration, its only caller is its own test at:217ChatRepository(~/dev/fera/fera/core/crates/protocol/src/contract.rs:18-59) — zero consumers;ui-elmbypasses it viaMsg::into_chat_command(ui-elm/src/msg.rs:98-116)Effect::StoreMessages(~/dev/fera/fera/core/crates/ui-elm/src/effect.rs:31) — never constructed anywhereEnginetrait (~/dev/fera/fera/core/crates/ui-contract/src/lib.rs:248-265) — implemented byVelloEnginebut never used through the trait: nodyn Engine, no generic boundui-elm/Cargo.toml:13-14— depends only onfera-protocol+fera-domain; it therefore physically cannot produce aRenderTree, so the Elm reducer and the engine can never meet~/dev/fera/fera/Cargo.toml:30—warnings = "deny", butpubitems in lib crates are exempt fromdead_code, which is exactly why the above survives a green build~/dev/fera/fera/core/crates/storage/src/engine.rs:97—pub mod contractis#[cfg(test)]-gated, so a sanakirja engine in another crate cannot reuse the ADR-014 suite it was written for~/dev/fera/community/README.md— “Tests pin behavior; benchmarks prove optimizations; Contract tests before a second implementation of any trait”~/dev/fera/fera/CLAUDE.md:10-12— greencargo check+clippy+ tests before each commit
- Measured state of
Суть / Thesis
Section titled “Суть / Thesis”The project’s quality gate is real and strict — warnings = "deny", clippy
all + pedantic + nursery + cargo, unsafe_code denied, todo!/unimplemented!
denied, zero occurrences of either macro, green tests required before every
commit. It works exactly as designed.
And it cannot see the actual failure mode.
Every one of the ~1,000 unreachable lines above is fully tested, fully
lint-clean, and still not called by anything. MemoryEngine passes its
contract suite. Backoff::on_failure has unit tests. ParleyTextEngine::layout
is exercised at text_engine.rs:217. The gate reports green on all of them,
correctly, because “does this function work?” and “does anything use this
function?” are different questions and only the first is being asked.
warnings = "deny" doesn’t help either: dead_code never fires on pub items
in a lib crate. This isn’t a missing lint — it’s a wrong-shaped gate. The
project measures code quality per-item and misses system quality per-crate.
The structural consequence is visible in the architecture: the two longest
real chains in the repo cannot meet. Chain A (App::update → Vec<Effect>)
ends in a dropped value because no dispatcher exists. Chain B (RenderTree →
lower() → Vello Scene) ends in memory because there is no window and
scene() has no caller. They are not merely unconnected — ui-elm doesn’t
even depend on ui-contract, so no amount of bugfixing inside the existing
crates can join them.
A crate is not finished when its tests pass. It is finished when something else calls it.
Варианты / Options (with tradeoffs)
Section titled “Варианты / Options (with tradeoffs)”A. Keep the current gate. (status quo)
- Pros: zero new tooling; the gate is genuinely good at what it checks.
- Cons: 27% of real code is unreachable and green. Every such item is a standing invitation to build against an interface that was never actually exercised end-to-end. The real integration bugs are deferred to the moment the missing composition layer is written — i.e. all at once, at the end.
B. Add a general dead-code/unused-public-API scanner.
- Tools:
cargo-udeps(unused deps, wrong axis),cargo-semver-checks,cargo-public-api(API surface diff, wrong axis),cargo-machete. - Pros: familiar, off-the-shelf.
- Cons: all of these measure the wrong thing. None of them asks “is this crate in the dependency graph of something runnable?” A public API on a library crate is supposed to have no internal consumer. What is wrong here is not “unused public items” — it’s that six crates form a closed set with nothing outside it.
C. A workspace-level consumer gate (proposed). Express the missing invariant directly, in one script, no compiler trickery required.
Proposed solution
Section titled “Proposed solution”1. The invariant, in one sentence
Section titled “1. The invariant, in one sentence”A workspace member may not be marked done — and may not be added — until at least one other workspace member’s non-test, non-dev code depends on it.
This is a statement about the workspace’s dependency graph shape, which is exactly what is currently unconstrained.
2. Implementation: xtask unreach
Section titled “2. Implementation: xtask unreach”Composes with idea-01-fork-sync-xtask.md (one xtask, several subcommands —
the project’s tools/ convention is already one-binary-per-tool, so this is a
third subcommand, not a new repo).
xtask unreach [--strict]Mechanics, cheapest first:
- Read each member’s
Cargo.toml. Build the member dependency graph. Excludedev-dependencies(those are the test harness, not a consumer) and exclude[[test]]/#[cfg(test)]edges. - Report every member whose only in-workspace consumers are dev-deps or none. Six crates today.
- For each, classify by hand into:
leaf-by-design(annotated, e.g. a tool binary),seed(intentionally the first consumer, e.g.MemoryEngineas the contract-suite’s first implementation), ororphan(must be wired). Annotate with// xtask: seed — <one-line reason>next to the item. --strictfails CI when anorphanappears and when a new member is added with no consumer.
3. Two cheap high-value extras while the graph is already loaded
Section titled “3. Two cheap high-value extras while the graph is already loaded”(a) Reverse the storage contract-suite gate. storage/src/engine.rs:97 is
#[cfg(test)]. Move it behind a contract-tests feature so it is importable;
ADR-014 says the suite runs “against every one” of the implementations, and
today it structurally cannot. This is a 3-line change that unblocks the
sanakirja engine from reusing the suite — i.e. it makes the second
implementation cheaper at exactly the moment the plan calls for it.
(b) Require a compile-fail test for every typestate. protocol/src/state.rs:303-315
is the project’s headline guarantee (“send_message is not callable on
ProtocolClient<Unauthenticated>, verified at compile time, not by a runtime
check” — QA.md). The actual assertion is commented out; the test only runs
size_of_val. A refactor that reintroduced a runtime flag would pass.
The fix already exists in this ecosystem:
~/dev/fera/chroix-macros/tests/compile_fail.rs uses trybuild with
tests/ui/*.rs fixtures for exactly this purpose. So this is not a new
technique to research — it is a technique the project already wrote in a sibling
repo and never carried over. trybuild + one fixture per typestate transition
is a day’s work and converts the strongest architectural claim in the corpus
from an assertion into a fact.
4. Tradeoffs, honestly
Section titled “4. Tradeoffs, honestly”- The gate will produce false alarms.
launcher,manifest-signer,bench-sceneare binaries — legitimately leaf nodes. That is why the classification is explicit and annotated rather than automatic: the tool finds candidates, a human marks them once. - It is not a substitute for integration tests. “Something calls it” does not mean “it works when called”. But it is the precondition for integration testing — today there is nothing to integrate.
- It may feel bureaucratic for a solo author. Counter: the cost is one annotated line per crate, and the alternative is what already happened — 27% of the code green and unreachable, discovered by an outside reader rather than by the project’s own process.
5. Sequencing
Section titled “5. Sequencing”Run xtask unreach as a report first, commit the classification as a
tracked list (that list is, incidentally, a precise definition of what the
vertical integration layer must wire — it is the list of everything that
“works” but is disconnected). Enforce only after the classification exists.
Then the fix is mechanical and ordered: dispatcher → window/present →
ui-elm → ui-contract dependency → chat-list source.