Skip to content

Federated IBD matching: a front door, and a memory - #43

Merged
JamesKane merged 3 commits into
mainfrom
feat/ibd-matching-ux
Aug 2, 2026
Merged

Federated IBD matching: a front door, and a memory#43
JamesKane merged 3 commits into
mainfrom
feat/ibd-matching-ux

Conversation

@JamesKane

Copy link
Copy Markdown
Owner

Backlog item 1.6. The IBD engine has been complete for months — detection, identity math, the X3DH/AES-GCM exchange channel, signed attestations, the pairwise chromosome browser. What it never had was a coherent surface, or any memory of a conversation in progress.

What was wrong

  • No durable state. Outbound request status lived in a UI HashMap, so a restart forgot that we had asked anyone anything. Only completed exchanges persisted (ibd_exchange_result).
  • Three disjoint cards — suggestions, exchange inbox, results — buried in one subject's IBD tab, with no shared lifecycle, even though a conversation is keyed by our DID and the broker's request URI and belongs to no subject in particular.
  • Two AppView endpoints that existed and were never called. /ibd/dismiss had no button. /ibd/attest was worse: a completed comparison stayed private forever, so the discovery graph could not grow from its own results.
  • Consent was an Accept button in a table row, showing a truncated urn:ibd:… and a purpose string — and nothing else.

What this adds

M1 — the ledger. Migration 0041_ibd_request + navigator-store/src/ibd_request.rs: one durable row per conversation, keyed by the broker request_uri. App::refresh_matching reconciles it in three passes (/exchange/incoming → adopt, /exchange/pending → advance non-terminal, stored results → EXCHANGED). insert_if_absent is the primitive that matters — re-polling can never walk back a decision made locally. A failing pass degrades the view rather than emptying it.

M2 — the surface. Top-level Nav::Matching (🔗) with Suggestions / Requests / Results, replacing the per-subject discovery cards. The subject tab keeps its results card and gains a link across.

M5 — both endpoints wired. ibd_dismiss and ibd_attest, with canonical signing strings mirroring du_db::ibd::messages byte for byte (the cM figure is signed at {:.1} to match).

Judgement calls worth reviewing

  • Attest is gated on mutual agreement and on knowing both AppView sample handles. Filing a one-sided figure from a comparison our own run disputes would put a claim on the discovery graph we do not believe. A direct (non-suggestion) request never carries the handles, so it is a silent no-op rather than an error.
  • Status is honest about what this edge can know. The broker is symmetric-blind: a partner's refusal is indistinguishable from silence. Both stay Requested, and Declined means we declined. There is deliberately no "they said no" state, because we are never told.
  • Consent is a modal. Accepting reveals our DID and puts our IBD-panel dosages on the wire. Both are irreversible, so the dialog states them under three headings: what you send, what they learn, what never leaves the device.
  • Subject pickers are filter + virtualized show_rows, not ComboBox — a workspace can hold 10k subjects and a ComboBox builds a widget per entry per frame. The second commit converts the pre-existing one in consensus_ibd_section, which also rebuilt the whole roster every frame whether or not its popup was open.

Depends on

JamesKane/decodingus#29/ibd/suggestions must return the caller's own target_sample_guid, or owns_sample can never be satisfied and attest is unreachable. Navigator parses it as Option, so this branch degrades cleanly without it: everything works except attesting.

Testing

cargo clippy --all-targets clean · cargo test --workspace green (0 failures) · en/es locales at parity · the new nav glyph 🔗 added to the icon_glyph_tests list so it is actually checked (🧬 shipped as tofu once).

New offline coverage: ledger round-trip and the consent-preservation invariant (navigator-store), the failure/forget lifecycle, and that attest is skipped without handles or agreement (navigator-app).

⚠️ Not validated live. The two-peer flow against a running AppView has not been run. Everything here is contract-correct and offline-tested; the over-the-wire round trip is the remaining integration milestone, as it was for the exchange channel itself.

Note for the reviewer

cargo fmt --all reformats ~124 files — the repo is already rustfmt-drifted at main (stable rustfmt disagrees with main too, so it is not a toolchain artifact). I reverted all of that and re-applied my edits by hand, so this diff carries no formatting noise. The drift is untouched and still wants its own commit.

Deferred

Background polling + an unread badge for inbound consent requests (the Community 🔔 pattern); a Settings discoverability opt-in; a UI path for the direct exchange_request(partner_did, …) initiator, still test-only; the segment ideogram for persisted exchange results.

🤖 Generated with Claude Code

JamesKane and others added 3 commits August 2, 2026 11:47
The IBD engine has been complete for months — detection, the X3DH/AES-GCM
exchange channel, signed attestations, the chromosome browser — but a matching
conversation had nowhere to live. Outbound requests were held in a UI HashMap,
so a restart forgot that we had asked anyone anything; only *completed*
exchanges persisted. Discovery, consent, and results were three disjoint cards
buried in one subject's tab, even though a conversation is keyed by our DID and
the broker's request URI and belongs to no subject in particular.

Adds the ledger the flow was missing (`ibd_request`, migration 0041) and
`App::refresh_matching` to reconcile it against the broker in one pass. The
reconciler adopts what `/exchange/incoming` reports with `insert_if_absent`, so
re-polling can never walk back a decision made locally, and a failing pass
degrades the view instead of emptying it.

Two AppView endpoints that existed but were never called are now wired.
`/ibd/dismiss` gives dismissal an actual button. `/ibd/attest` is the one that
mattered: without it a completed comparison stayed private forever and the
discovery graph could not grow from its own results. Attest is gated on both
parties agreeing on the summary — filing a one-sided figure from a comparison
our own run disputes would put a claim on the graph we do not believe.

Status is deliberately honest about what this edge can know. The broker is
symmetric-blind, so a partner's refusal is indistinguishable from silence; both
stay "waiting on them", and DECLINED means *we* declined.

Consent is a modal, not a row button, because accepting reveals our DID and puts
our IBD-panel dosages on the wire. It says so, in three headings: what you send,
what they learn, what never leaves the device.

The subject tab keeps its results card and gains a link across. The subject
picker is a filter plus a virtualized list rather than a ComboBox — a workspace
can hold 10k subjects, and a ComboBox builds a widget per entry per frame.

Companion AppView change (decodingus): `/ibd/suggestions` now returns the
caller's own `target_sample_guid`. Without it `owns_sample` could never be
satisfied from the edge and attest was unreachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The remaining flat `ComboBox` over every other subject in the workspace, left
behind when the Matching tab's picker was rebuilt. Two costs, both proportional
to workspace size and both paid per frame: the `others` vec cloned every
subject's `donor_identifier` whether or not the popup was open, and the popup
itself built one widget per entry. At 10k subjects that is a stall on a card
that is merely *visible*.

Same shape as the Matching picker now — current choice plus a Change reveal,
then a filter over a virtualized `show_rows` list, so the cost is the rows on
screen. The selected label is a lookup rather than a scan of a copied roster,
and the "no other subjects" check is a short-circuiting `any` instead of
building the vec to ask whether it is empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JamesKane
JamesKane merged commit d3054b9 into main Aug 2, 2026
6 checks passed
@JamesKane
JamesKane deleted the feat/ibd-matching-ux branch August 3, 2026 10:31
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