Repository navigation
Surface cardinality-one replacements across core, CLI, agent and viewer (closes #2910) - #3079
Conversation
…er (closes #2910) create_relationship enforces a `one` end by evicting the prior edge rather than rejecting, which let a write silently steal an existing assignment. - core: create_relationship / _in_tx return the evicted edges (ReplacedEdge); rel_ops CreateRelOutput carries them; relationship groups gain far_cardinality (the other end's cardinality) - proto/daemon: CreateRelationshipResponse.replaced - CLI: `relationship create` prints a "Replaced relationship" line per eviction and includes `replaced` in --json - agent tool: create_relationship result includes `replaced` + a note so the model can tell the user who lost the link - frontend: Tauri command / dev-proxy return { replaced }; the modal and the single-select field confirm before a link that would take the target from its current holder (and, in the modal, before a `one` group drops its own current link); stale "daemon validates cardinality" comment corrected Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pragmatic Code Review: PR #3079 (closes #2910)Review Type: Initial Review SummaryThis is a solid change and the layering is right. The backend now returns the edges it evicted ( One correctness bug blocks merge. The frontend's Requirements Check
Code Review Findings1. 🔴 [Critical/Blocker] The mirror match uses only
A task therefore has two inbound This is the one thing the confirmation has to get right. A warning that fires on ordinary links teaches users to click through it (alarm fatigue), which defeats the purpose of the check. Fix: scope the mirror to the same declaration. The smallest change is to also require 2. 🟡 [Improvement] Evicted edges are reported in stored (forward) orientation, but the CLI's "Created" line repeats what the caller typed A write made through an 3. 🟡 [Improvement] The frontend discards the actual The confirmation is a pre-write prediction, which is a TOCTOU window: another client can assign the candidate between the check and the write. 4. 🟢 [Nit] The Nothing serializes 5. 🟢 [Nit]
6. 🟢 [Nit] The agent's The result StandardsNo lint suppression, no backward-compat shims, and no issue numbers in code comments. TestsThe core eviction assertions (forward, reverse, idempotent), the agent tool test, the modal decline/confirm/no-holder/many-end paths, and the field decline/confirm paths are all good. The only gap is the shared-name-across-schemas case (finding 1). Recommendation: REQUEST CHANGES. Fix finding 1 and add its test. Findings 2 and 3 are strongly recommended; the rest are optional. |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: REQUEST CHANGES (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
- farEndHolders matches the mirror group on reverseName too, so a task's project (another schema's same-named `tasks`) is no longer reported as a holder that linking an assignee would remove; unit test added - Document ReplacedEdge's stored-forward orientation - Drop ReplacedEdge's unused Serialize derive Skipped: post-write notice for unpredicted evictions (the window is a concurrent write between check and click; the reload shows the real state), lighter candidate lookup (click-time scale), and tool-description prose (untested prompt additions have measurably hurt E4B). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review addressed (e132428)✅ 🔴 #1 farEndHolders over-matched same-named relationships — the mirror group is now matched on ⏭️ 🟡 #3 post-write notice for unpredicted evictions — skipped. It only matters when another client writes between the check and the click (sub-second), and the reload after the mutation shows the true state. Worth revisiting if multi-user editing arrives. Pre-push gate passed. Frontend relationship tests: 88 passed. Re-review: not needed. The change is small and targeted: one added predicate, a new view-model field, a doc comment and a removed derive. |
Closes #2910.
create_relationshipenforces aoneend by evicting the prior edge, not rejecting — so a write could silently steal an assignment. This makes that visible on every surface.API shape (the design decision)
NodeService::create_relationship/create_relationship_in_tx→Result<Vec<ReplacedEdge>>(source, relationship name, target of each evicted edge; empty for a plain create, an idempotent repeat, or a builtin).rel_ops::CreateRelOutput.replaced→ protoCreateRelationshipResponse.replaced(repeated RelationshipEdge).far_cardinality(the other end's cardinality), so the viewer knows when a link can displace someone without fetching anything.Surfaces
relationship create: prints aReplaced relationship: A --[r]--> Bline per eviction;--jsonincludesreplaced.create_relationship: result includesreplaced: [{from_id, to_id, type}]+ a note telling the model to tell the user which link was removed. (packages/agent/src/local_agent/change — needs your sign-off per the issue.){ replaced }. Before an add whose far end isone, the viewer fetches the candidate's relationships andconfirm()s if it's currently held by someone else — in both the modal and the single-selectRelationshipField. In the modal, a local-onegroup (one with edge fields) also warns that its current link will be replaced. Declining writes nothing.relationship-viewer-service.ts: stale "daemon validates cardinality" comment replaced with the actual replace semantics.Tests
far_cardinalityasserted on both sides.create_relationship_reports_replaced_edge(real NodeService).farEndHolders/reassignmentPromptunit tests; modal (decline → no write, confirm → write, no holder → no prompt, farmany→ no lookup); field one-to-one decline/confirm.🤖 Generated with Claude Code