refactor: hoist cross-host policy into core; harden browser EffectsSink#83
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 — desktopcommands.rs(DesiredSub::beats) and browsermqtt-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.crates/microflow-core/src/runtime/subscriptions.rs:reconcile_desired+beats+DesiredSub/SubKind, with unit tests for the tie-break, collapse, and order-independence.reconcileSubscriptions();mqtt-subscriptions.tsdropsbeats/reconcileDesired(now just the host-local diff + uid helpers).commands.rscalls corereconcile_desired; localDesiredSub/beats/sub_kinddeleted.SubKinddeduped 3 definitions → 1 (re-exported from core).Each host still owns its own live-set diff and broker I/O (
rumqttcvsmqtt.js) — those are genuinely per-platform and stay put.2 · Browser EffectsSink drift gap closed structurally
The
Effects::applyorder lives in core once (ADR-0008), but its enforcement was asymmetric: a newEffectsfield is a compile error in every Rust sink, while the browserapplyEffectswould silently skip it, caught only if the conformance test was remembered.effects-sink.tsrebuilt on anEFFECT_HANDLERSmap +APPLY_ORDERtuple, both typed exhaustive overkeyof Effects— a new field is now a TypeScript compile error (unhandled and/or unordered), mirroring the Rust trait guard.flow-reactor.tstypedEmitOf<"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)
register_allfrom the catalog — declined. It contradicts ADR-0007 and, on inspection, would remove ~1 line per node while still requiringcatalog_parity.rs(a build script can't introspect trait impls). Poor trade; ADR-0007 stands.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)cargo clippy --workspace --all-targets -- -D warnings -W clippy::pedanticclean;cargo test --workspace --lib --testsgreentsc --noEmitclean;bun test src/lib/firmata9 passCONTEXT.mdupdated (EffectsSink + the reconcile policy)🤖 Generated with Claude Code