Skip to content

refactor: hoist cross-host policy into core; harden browser EffectsSink#83

Merged
xiduzo merged 1 commit into
mainfrom
refactor/cross-host-policy-deepening
Jun 26, 2026
Merged

refactor: hoist cross-host policy into core; harden browser EffectsSink#83
xiduzo merged 1 commit into
mainfrom
refactor/cross-host-policy-deepening

Conversation

@xiduzo

@xiduzo xiduzo commented Jun 26, 2026

Copy link
Copy Markdown
Owner

What

Two architecture deepenings, both instances of the same principle — policy belongs in core (sans-IO, once); hosts keep only their irreducible I/O primitives. The core runtime was already deep; these sharpen the two Runtime Host seams where host-agnostic policy had drifted into duplication or under-enforcement.

No behaviour change. Pure refactor + a compile-time guard.

1 · Subscription winner-selection hoisted into core

The "collapse subscriber wirings to one desired subscription per (broker, topic), with a deterministic tie-break" policy was implemented twice — desktop commands.rs (DesiredSub::beats) and browser mqtt-subscriptions.ts (beats/reconcileDesired) — held in sync only by a code comment. If the two hosts ever picked different winners for the same topic, behaviour would silently diverge.

  • New crates/microflow-core/src/runtime/subscriptions.rs: reconcile_desired + beats + DesiredSub/SubKind, with unit tests for the tie-break, collapse, and order-independence.
  • Browser reaches it through a new wasm binding reconcileSubscriptions(); mqtt-subscriptions.ts drops beats/reconcileDesired (now just the host-local diff + uid helpers).
  • Desktop commands.rs calls core reconcile_desired; local DesiredSub/beats/sub_kind deleted.
  • SubKind deduped 3 definitions → 1 (re-exported from core).

Each host still owns its own live-set diff and broker I/O (rumqttc vs mqtt.js) — those are genuinely per-platform and stay put.

2 · Browser EffectsSink drift gap closed structurally

The Effects::apply order lives in core once (ADR-0008), but its enforcement was asymmetric: a new Effects field is a compile error in every Rust sink, while the browser applyEffects would silently skip it, caught only if the conformance test was remembered.

  • effects-sink.ts rebuilt on an EFFECT_HANDLERS map + APPLY_ORDER tuple, both typed exhaustive over keyof Effects — a new field is now a TypeScript compile error (unhandled and/or unordered), mirroring the Rust trait guard.
  • LLM result handles in flow-reactor.ts typed EmitOf<"Llm"> so a catalog handle rename breaks the build — closes the spot where the browser hard-coded "thinking" while the desktop sourced it from a Rust const.

Both guards were demonstrated to fail compilation when deliberately broken, then reverted; the conformance test still asserts the runtime order.

Not included (for reviewers who saw the architecture review)

  • Generate the Rust register_all from the catalog — declined. It contradicts ADR-0007 and, on inspection, would remove ~1 line per node while still requiring catalog_parity.rs (a build script can't introspect trait impls). Poor trade; ADR-0007 stands.
  • A "collab-bridge" hiding Yjs/ReactFlow in the session layer — declined. Yjs imports are confined to 3 files in session/, there is no second CRDT, and the ReactFlow change types live only in the ReactFlow bridge. Sealing them would add a hypothetical seam with nothing varying across it.

Verification

  • cargo test: core 381 · wasm 6 · desktop 47 (incl. catalog_parity)
  • CI gates run locally: cargo clippy --workspace --all-targets -- -D warnings -W clippy::pedantic clean; cargo test --workspace --lib --tests green
  • tsc --noEmit clean; bun test src/lib/firmata 9 pass
  • CONTEXT.md updated (EffectsSink + the reconcile policy)

🤖 Generated with Claude Code

Two architecture deepenings, both "policy in core, primitives per host":

1. Subscription winner-selection -> microflow-core (runtime/subscriptions.rs:
   reconcile_desired + DesiredSub/SubKind). The collapse-to-one-sub-per-topic +
   deterministic tie-break ("beats") was mirrored in two languages (desktop
   commands.rs, browser mqtt-subscriptions.ts) held only by a comment; now it
   lives once, reaching the browser via the wasm reconcileSubscriptions()
   binding. SubKind deduped 3 defs -> 1 (re-exported from core). Each host keeps
   only its own live-set diff + broker I/O.

2. Browser EffectsSink (effects-sink.ts) made structurally exhaustive: an
   EFFECT_HANDLERS map + APPLY_ORDER tuple both typed over keyof Effects, so a
   new Effects field is a TS compile error, not just a conformance-test miss.
   LLM result handles in flow-reactor.ts typed EmitOf<"Llm"> so a catalog
   rename breaks the build (closes the hardcoded "thinking" leak).

No behaviour change. CONTEXT.md updated (EffectsSink + reconcile policy).
Verified: core 381 / wasm 6 / desktop 47 (incl catalog_parity) / tsc / firmata 9.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xiduzo xiduzo merged commit 67b05b9 into main Jun 26, 2026
7 checks passed
@xiduzo xiduzo deleted the refactor/cross-host-policy-deepening branch June 26, 2026 08:53
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