Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

The whole dylib module axis is unimplemented and undocumented-as-deferred: modules/ does not exist, and three ADRs say subsystems are swappable

Источник: dev/fera-research/docs/oc1/015-module-axis-unimplemented-undocumented.md (read-only; сгенерировано scripts/sync-docs.mjs)

The whole dylib module axis is unimplemented and undocumented-as-deferred: modules/ does not exist, and three ADRs say subsystems are swappable

Section titled “The whole dylib module axis is unimplemented and undocumented-as-deferred: modules/ does not exist, and three ADRs say subsystems are swappable”
  • kind: risk

  • area: modules

  • evidence — repo reality (~/dev/fera/fera, git log ed4d66e pre-migration snapshot, 10 commits, all 2026-10):

    • core/crates/ = domain, media, notification, predictor, protocol, search, storage, trust, ui-contract, ui-elm, ui-engine-vello — eleven ordinary in-process workspace crates (1–6 .rs files each).

    • No modules/ directory. ls fera/modules → empty.

    • No crate-type = cdylib anywhere (grep -rn "crate-type" --include=Cargo.toml → 0).

    • No nylon dependency anywhere in the workspace Cargo.toml files.

    • Commit 575d16e says “workspace skeleton per TECHSPEC §0” — but TECHSPEC §0’s second half (TECHSPEC.md:30-39) is entirely /modules/* nylon-ring cdylibs. The commit implemented the first half only.

    • ui-engine-vello (the Engine axis) is a core crate, not a dylib.

    • What the docs say should be there:

      • TECHSPEC.md:30-39 — /modules/: storage-sanakirja, search-milli, renderer-gpui, style-macos|win|linux, feature-macos|win|linux, native-macos|linux-gtk|winui, media-decoder-baseline, media-decoder-ffmpeg, calls-tgcalls — all “nylon-ring cdylib”.
      • ADR.md:24-27 — “Any heavy subsystem (storage, search, protocol, media decode, UI renderer, UI style, UI platform-feature layer, predictor) can be replaced without recompiling or restarting the whole application”.
      • ADR.md:47-51 — “First-party / performance-critical modules (storage, search, protocol, media decode, UI engine/style/feature, predictor) are native dylibs loaded via nylon-ring”.
      • ADR.md:772-780 (ADR-020) — per-struct granularity, layer: shared | types | impl, topological load order, module count “grows”; OQ.md:627-631 — “hundreds of small dylibs, not a handful of big ones”.
      • ADR.md:425-446 (ADR-024) — distribution, channels, N-1 rollback, developer mode.
      • ADR.md:854-855 — “The host maintains compatibility surfaces for edition N-1 (adapters) — an accepted per-edition maintenance cost.”
    • Three subsystems are actively being built as in-process crates, which contradicts three separate ADRs:

      Subsystem Docs say Repo says
      predictor ADR.md:24-27, :47-51 — swappable native module TECHSPEC.md:25 /core/crates/predictor/; TECHSPEC.md:214 “Crate: predictor (part of core)”; repo: core crate
      protocol ADR.md:24-25, :47-49 — swappable native module TECHSPEC.md:20 /core/crates/protocol/; repo: core crate; and ADR-026 says it is our own fork — a C-ABI dylib for a Rust-native typestate API is a contradiction in ambition
      UI engine ADR.md:239-243, TECHSPEC.md:266 — Engine axis, nylon-ring dylib repo: core/crates/ui-engine-vello, in-process
    • Nothing in the corpus records the deferral. No ADR, no OQ, no TECHSPEC phase note says “modules come later”. TECHSPEC §10 step 4 (TECHSPEC.md:461-462) does put the nylon-ring boundary at step 4 — “prove hot-swap works” — but §0 lists the modules as present from day one, and steps 1–3 depend on crates that per ADR-001/002 should be dylibs.

  • severity: high

The corpus’s organising idea — ADR-001’s micro-kernel with hot-swappable subsystems — has no implementation and no written deferral. That is not merely a status note; it has four concrete consequences:

  1. The trait contracts are being designed against a boundary that does not exist. ADR-005 (batch-shaped APIs), ADR-014 (contract tests per trait), ADR-020 (#[repr(C)] by-value, per-struct seams), ADR-021 (three version axes), ADR-022 (translators) all describe the dylib boundary. The real crates so far are plain Rust traits — which is the right first move (you cannot design an ABI before you know the shape of the calls), but it means the ABI-relevant decisions are being made implicitly by ordinary Rust API design and then must be reverse-fit into a C ABI later. The specific danger: any design choice that is free in-process and expensive across FFI (passing String/Vec, returning owned collections, deep trait hierarchies, generics) will get baked in and then block the ADR-020 boundary. My finding 010 (ABI: #[repr(C)] POD vs pinned rustc) is exactly this conversation happening in prose while the code moves the other way.

  2. The predictor and protocol cases are decisions, not omissions. TECHSPEC §0/§5/§1 put them in core; ADR-001/002 list them as swappable. One has to give. For protocol the choice is forced by ADR-026: our own fork with a typestate API (TECHSPEC.md:65-81) cannot sensibly be a C-ABI dylib — the typestate is a compile-time device and would be erased at the boundary. Recommend explicitly: protocol is core, not a module (and amend ADR-001/002’s list). For predictor, TECHSPEC §5’s placement is defensible (it needs storage access and is small), so the cheaper fix is to drop it from ADR-001/002’s list.

  3. Every QA module gate is currently untestable. QA.md:123-159 (version mismatch → fallback, hash mismatch → warning, hot-reload with zero dropped calls, signature rejection, CPU-feature selection, topological load order) presuppose a loader and a manifest. None can run, so the module tier of QA is decorative — and per my finding 014, two of those gates are also wrong.

  4. The nylon-ring dependency itself is unvetted in the corpus. The design leans entirely on it (ADR.md:19-21: explicit version + struct-size validation, graceful unload with draining, host-leased zero-copy output buffers) and on forking it (OQ.md:874-883, items (a)–(d)), yet there is no ADR for the dependency choice, no license/ maintenance assessment, and — per my findings 009/010 — the fork plan and the unload/ABI semantics are both unresolved. OPTIM-IDEAS-4:236 advises not to fork before a Phase-0 baseline; OPTIM-IDEAS-2:48-50 and -5:25 say “since the plan is to fork”. The repo has not even depended on it yet, which is the one place where being undecided is cheap.

Варианты решения (tradeoffs)

Section titled “Варианты решения (tradeoffs)”
  • (A) Write the deferral down and split the ADRs. Keep core-crate boundaries for now; state in TECHSPEC §0/§10 that the module axis lands at step 4 and that steps 1–3 use in-process traits; amend ADR-001/002 to say which subsystems are actually module candidates.
  • (B) Build the boundary first (extract one small module — e.g. media-decoder-baseline, which is the most self-contained) to learn the ABI before the contracts harden. Highest information value per unit of work; delays features.
  • (C) Abandon hot-swapping for first-party subsystems, keeping modules only for third-party plugins (WASM) where the sandbox story is the point. Massive simplification: no manifest layering, no per-struct seams, no N-1 rollback, no dev mode, no QA module tier. Contradicts ADR-001’s core premise and the author’s stated reason for it (ADR.md:11-14: “heavily, repeatedly re-optimized … a monolithic binary would make swapping individual subsystems prohibitively expensive”).
  • (D) Keep building in-process and hope the boundary is easy later. Reject — that’s how the #[repr(C)]/vtable/erasure questions end up decided by accident.

(A) + (B), with (C)’s thinking applied where it is actually true.

  1. New ADR (032-ish) “What is a module, and when” that settles the candidate list honestly:
    • Real module candidates (self-contained, ABI-friendly, genuinely swappable): media decoders, the Engine axis, Native/Feature per-OS modules, search. These have narrow, data-shaped interfaces.
    • Core, not modules: protocol (typestate is compile-time; ADR-026’s fork is a Rust library), predictor (needs storage + is small), trust (it is the host side of the mechanism), ui-elm/ui-contract (the ABI-defining layer itself). Amend ADR-001:24-27 and ADR-002:47-51 to match, and note that the predictor/protocol rows in those lists are the ones that created the contradiction.
  2. Record the deferral in TECHSPEC §0/§10 so the layout stops implying the modules exist; make §0’s tree show the current state plus the step-4 target (the repo did this implicitly in commit 575d16e; the doc should say it).
  3. Extract one real module early (media-decoder-baseline is the best candidate: narrow trait, no state, already a stub crate) to discover the ABI’s actual constraints before StorageEngine/SearchEngine harden — that is finding 016’s subject.
  4. Add a QA gate that is runnable today: “no public API in ui-contract/protocol returns String/Vec/Box<dyn Trait> across a planned module boundary” — a lint-level check that keeps the door open for (B) without needing the loader.
  5. Give nylon-ring an ADR when it is first depended on: version, license, maintenance posture, what exactly it guarantees (struct-size validation, draining, leased buffers — ADR.md:19-21), and the fork gate from OPTIM-IDEAS-4:236 (baseline + profiling before forking). Mark OPTIM-IDEAS-2.md:48-50 / -5.md:25 as superseded on the fork-now question.