Repository navigation
Let the relationships modal's size follow the node's data (closes #2385) - #2426
Conversation
The panel rendered one full section per SCHEMA-DECLARED relationship, so an `adr` node with seven declared relationships and no edges showed seven headers reading "0 items", seven "+ Add" buttons, and nothing that was actually data. Its size scaled with the schema, making a richly-modelled type most overwhelming exactly when it had least to say. Only groups that have edges get a section now. Empty outbound relationships collapse behind one "Add relationship" chooser; empty inbound groups are dropped entirely, since they have no Add of their own to justify standing open. `partitionGroups` owns that rule so it is unit-testable rather than inline. Inbound groups are read-only. An inbound group is the same physical edge seen from the other end, declared on and owned by the OTHER node's schema: editing it here would write to a node the panel isn't showing, an Add would have to invert the picker's meaning (pick a source, not a target), and it would simply fail whenever the other type doesn't declare the forward relationship. Instead they carry an "incoming · read-only" badge and a dashed rule — distinguishable without reading the arrow — and their rows navigate to the node that owns the edge, where it can be edited properly. Controls are per row instead of behind a panel-wide mode. The `editMode` toggle is gone, along with the Done/Close footer (the dialog's ✕ is now the single dismiss control). Edge values render read-only in their columns at all times; ✏️ opens `EdgePropertiesModal` for that one edge, and appears only where the schema declares `edge_fields` — a relationship with none shows ❌ alone rather than an editor with nothing to edit. Editing one row leaves every other row, and the panel behind, untouched; per-row drafts and immediate persistence are unchanged. The edge editor is a dialog rather than inline cells or a popover because each relationship declares its own `edge_fields`, so the form is per-relationship and variable in length — and it needs room to grow for the collection↔person RBAC fields. Its input helpers move to `edge-field-input.ts` so the add form and the editor render a given field type identically. Verified in dev:browser against an isolated daemon: seven declared groups and no edges renders zero sections, one Add control, and no scrolling; adding a first edge persists and the section appears; per-row edit saves and closes; a group with no edge fields shows no ✏️; an inbound row navigates to the owner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
Pragmatic Code Review — PR #2426Review Type: Initial Review SummaryThis is a strong, well-reasoned change and a clear net positive. The core insight — the panel's size should track the node's DATA, not its schema's declared relationship count — is correct, and it is implemented in the right place: The commit message is exemplary — it explains why inbound is read-only (ownership, picker inversion, cross-type failure modes) rather than restating the diff. The extraction of Findings below are two 🟡 correctness/lifecycle issues worth addressing and several 🟢 observations. Nothing is a blocker. Requirements CheckAgainst the acceptance criteria in issue #2385:
14 / 14 addressed, one with a caveat. Code Review Findings🟡 Important:
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
…pshot
Both 🟡 findings from the PR review shared one root cause: `editing` and
`addGroup` captured view OBJECTS, but every mutation reloads `view` into a
wholly new object graph, so anything outliving a mutation was rendering — and
writing from — a pre-reload snapshot.
Now the modal holds `editingKey` / `addGroupKey` and derives the live group and
row through new `findGroupByKey` / `findRowByKey` resolvers in the pure module.
Consequences: the editor always shows post-reload values; it collapses to null
(closing itself) once the edge it points at is gone; and `runSearch` re-reads
the group AFTER its await, so the duplicate-target filter can no longer offer
an already-linked node using stale rows.
Putting the resolvers in `relationship-grouping.ts` also answers the review's
testing note — the staleness lived in the one layer pure-module tests could not
reach. Seven new tests cover it, including that a resolver returns the
post-reload row (asserted by identity, not just value), that a deleted row
resolves to null, and that same-named groups facing opposite directions never
cross-resolve.
Also from the review:
- Documented WHY ad-hoc edge keys are read-only rather than free-text editable:
an undeclared key has no type, and the wholesale-replace update would coerce a
stored number or boolean to a string on first save. Showing the value and
declining to edit it is the honest option.
- Documented why `formatEdgeFieldLabel` deliberately is not `humanizeName` —
callers pair it with a `capitalize` class, so consolidating them would render
`Due By` where `Due by` is intended.
- Inbound rows: dropped the redundant `title` that duplicated visible type text,
and gave both navigating controls an action-describing accessible name
("Open <node> to edit this relationship") to match the explicit labels on the
✏️/❌ controls.
Verified in dev:browser against an isolated daemon, with two edges in one group:
editing row 2 changes only row 2 in both UI and storage; deleting an edge out of
band while its editor is open makes the save fail cleanly with the daemon's
error surfaced and no resurrection, and the row drops on the next reload;
Cancel closes without disturbing the panel. 233 files / 4893 tests passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
Review feedback addressed — commit
|
Pragmatic Code Review — PR #2426 (Re-Review)Review Type: Re-Review Previous Review Summary
Verification of Previous Findings
The 🟢 #5 resolution went slightly beyond the suggestion: Scrutiny Items — AssessmentReactivity of the
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
The re-review caught that resolving `editing` by key self-healed in the READ
direction only. A self-close never routes through `cancelEdit` — the `{#if}`
unmounts the child directly and `onOpenChange` does not fire on unmount — so
`editingKey` and its `edgeDrafts` entry dangled.
That mattered because neither key is unique to an edge: a group key is
`direction:name:targetType` and a row id is the TARGET NODE's id. Re-adding the
same target to the same relationship therefore rebuilt a row the dangling key
resolved against again, popping the editor open unbidden over a brand-new edge,
pre-filled with the deleted edge's draft.
Two effects now drop the key the moment resolution misses — one for the editor
(clearing its draft with it), one for the target picker. `rowKey` is built from
a shared `draftKey(groupKey, rowId)` so the effect and the row-object path
cannot drift on how a draft is addressed.
Also from the re-review:
- Corrected the docblock and comment that read as a guarantee the code does not
make. Self-closing is a reaction to a RELOAD, not a save-time check; a save
racing an out-of-band delete still reaches the daemon and still relies on its
error being surfaced. The comments now say so explicitly, so the daemon-error
path is not mistaken for dead code.
- `runSearch`'s `catch`/`finally` now check the request is still current before
clearing results or the spinner, so a superseded response cannot blank what
replaced it.
Verified the exact scenario the re-review described, which the previous manual
pass stopped short of: draft typed on row B, B's edge deleted out of band, a
modal-driven removal forcing an in-place reload (editor self-closes here), B's
target re-added, another reload. The editor stays closed, "GHOST DRAFT" appears
nowhere in the DOM, and the recreated edge keeps its own value.
Worth recording what is NOT a bug, since it looks like one: keeping the editor
open across a delete/re-add and then pressing Save DOES write the typed draft to
the recreated edge. No reload intervenes, so nothing resolves to null and no
effect fires — and a user pressing Save is asking to write what they typed.
Two tests pin the reproducible-key fact the fix rests on: a stale key resolves
again once the same target is re-added (to a different row object), and a group
key survives the empty→populated promotion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
Re-review feedback addressed — commit
|
Pragmatic Code Review — PR #2426 (Re-Review, round 2)Review Type: Re-Review (round 2) Previous Review Summary
Verification of Previous Findings
The 🟡 diagnosis in the round-1 review was correct and the fix targets the actual root cause rather than the symptom. Worth crediting the specific insight: the reason this was a real bug and not a theoretical one is that neither key is unique to an edge — Primary Scrutiny Item — the two new
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review round 2): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Comments only, no behaviour change. Two facts that took tracing to establish and that a future reader should not have to rediscover: - The effect writes state it transitively reads, which is the shape of a reactivity loop. It terminates on the first run because nulling `editingKey` makes its own guard false. - `load()` blanking `view` looks like it should trip the effect and close a legitimately-open editor. It cannot: `load()` calls `resetTransient()` in the same synchronous block, which has already cleared both keys, so Svelte evaluates the guard as false. Also notes the ordering constraint in the body — `stale` must be read before the key it derives from is nulled — which the re-review flagged as reading incidental. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
Round-2 feedback addressed — commit
|
| Commit | Round | Verdict |
|---|---|---|
3feaab30 |
The redesign | APPROVE — 2 🟡, 4 🟢 |
6aac7786 |
All 6 addressed | APPROVE — 1 new 🟡, 2 🟢 |
620d7bd2 |
All 3 addressed | APPROVE, merge as is — 0 new |
ac92bc13 |
Optional 🟢 nit | — |
14/14 acceptance criteria delivered and browser-verified. 233 files / 4895 tests passing (baseline 232 / 4863; +32 new). eslint + svelte-check 0/0 across every round; full pre-push gate green on all four pushes.
Follow-ups worth their own issues, not this PR
- Save across a delete/re-add writes the open draft to the recreated edge. Judged correct — Save means "persist this" — and the reviewer agreed a save-time identity check would be over-engineering, since closing the true race needs daemon-minted edge identity threaded through the view model and wire format.
- The modal is still ~1100 lines. The issue cited 1044 as evidence of structural weight, but what remains is largely the per-group view-settings popover (~85 lines of columns/sort/filter), unrelated to Relationships modal renders a full section per declared type, so an empty node shows six 'Add' buttons and no data #2385. Splitting it out would be a clean separate change.
🤖 Generated with Claude Code
…elationships-modal # Conflicts: # packages/desktop-app/src/lib/components/relationships/relationship-viewer-modal.svelte
…elationships-modal
Merged
|
…2432) The panel shipped in #2426 stopped scaling with the SCHEMA — declared-but- empty relationships no longer each got a section. It still scaled with the node's DATA: every populated relationship rendered its own full-width table, stacked vertically, so a node with six of them was six tables and a scrollbar. Now a rail on the left lists the relationships the node actually HAS, one row each with its edge count, and the right pane shows every edge of the selected one. The rail is the same size whatever the node's connectivity. Two panes rather than three because expansion does the third pane's job in place: a row expands to its own edge properties, so properties stay one click away and a single-target relationship needs no special case — it simply shows its one edge. That also retires edge-properties-modal.svelte, which existed because a table cell could not hold a variable-length form; a nested dialog over the panel is not something to keep once the row can hold the form itself. ## The rail encodes the ownership boundary Order is: relationships this node owns, then `+ Add`, then a divider, then INCOMING · READ-ONLY. The control's POSITION says what the user may create, so no per-item badge has to, and the detail pane carries no "read-only" badge either — the rail heading above the selection already said it, and the absent controls are what actually hold the rule. Inbound rows show their edge properties with the same values, read-only, and offer the owning node instead of an editor. Verified in the browser that an edit made from the conference side reads back identically from the speaker's side, which is the point: it is one physical edge, not a copy. ## `+ Add` is the single creation path The old split — inline `+ Add` on populated relationships, a separate chooser for empty ones — collapses into the rail's one control, listing only what is still addable. `partitionGroups`' `addable` filter already computed that set; the chooser popover it fed is gone. ## What was deleted, and why that was a decision The per-group view-settings subsystem from #1918 — selectable columns (edge fields and target-schema fields), sort, filter, persisted per (nodeType, relationshipName, direction) — is removed: two services, three test files, and the two service functions that existed only to feed it (`fetchTargetSchemaFields`, `fetchNodesProperties`). The two-pane design's answer to "see edge data" is expanding a row, not configuring columns, and a right pane offering both would be two mechanisms fighting over the same job. Left in place they would have been unreachable code. Net -1457 lines. ## One bug found in the browser, and its test Removing a relationship's last edge left the detail pane showing a stale header with no rows. The re-selection effect asked whether the selected KEY still resolved — and it does: removing the last edge does not remove the group, it stays declared, which is exactly what makes it addable again. The rail is built from `populated`, so the check has to be against that list, not against resolvability. Fixed, with a unit test that pins the distinction rather than the symptom. Tests: 230 files / 4877 passed. The drop from 4916 is the 40 view-settings tests deleted above. Verified in `dev:browser` against an isolated NODESPACE_HOME with purpose- built conference/speaker/venue schemas: rail selection, expansion (one row at a time, independent not accordion), enum labels rendering as labels, auto-save persisting across a close/reopen, inbound read-only with values visible, target links opening via focusOrOpenNode, removal, and the full `+ Add` flow including cardinality filtering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK
…2432) (#2446) * Browse relationships in two panes instead of stacked tables (closes #2432) The panel shipped in #2426 stopped scaling with the SCHEMA — declared-but- empty relationships no longer each got a section. It still scaled with the node's DATA: every populated relationship rendered its own full-width table, stacked vertically, so a node with six of them was six tables and a scrollbar. Now a rail on the left lists the relationships the node actually HAS, one row each with its edge count, and the right pane shows every edge of the selected one. The rail is the same size whatever the node's connectivity. Two panes rather than three because expansion does the third pane's job in place: a row expands to its own edge properties, so properties stay one click away and a single-target relationship needs no special case — it simply shows its one edge. That also retires edge-properties-modal.svelte, which existed because a table cell could not hold a variable-length form; a nested dialog over the panel is not something to keep once the row can hold the form itself. ## The rail encodes the ownership boundary Order is: relationships this node owns, then `+ Add`, then a divider, then INCOMING · READ-ONLY. The control's POSITION says what the user may create, so no per-item badge has to, and the detail pane carries no "read-only" badge either — the rail heading above the selection already said it, and the absent controls are what actually hold the rule. Inbound rows show their edge properties with the same values, read-only, and offer the owning node instead of an editor. Verified in the browser that an edit made from the conference side reads back identically from the speaker's side, which is the point: it is one physical edge, not a copy. ## `+ Add` is the single creation path The old split — inline `+ Add` on populated relationships, a separate chooser for empty ones — collapses into the rail's one control, listing only what is still addable. `partitionGroups`' `addable` filter already computed that set; the chooser popover it fed is gone. ## What was deleted, and why that was a decision The per-group view-settings subsystem from #1918 — selectable columns (edge fields and target-schema fields), sort, filter, persisted per (nodeType, relationshipName, direction) — is removed: two services, three test files, and the two service functions that existed only to feed it (`fetchTargetSchemaFields`, `fetchNodesProperties`). The two-pane design's answer to "see edge data" is expanding a row, not configuring columns, and a right pane offering both would be two mechanisms fighting over the same job. Left in place they would have been unreachable code. Net -1457 lines. ## One bug found in the browser, and its test Removing a relationship's last edge left the detail pane showing a stale header with no rows. The re-selection effect asked whether the selected KEY still resolved — and it does: removing the last edge does not remove the group, it stays declared, which is exactly what makes it addable again. The rail is built from `populated`, so the check has to be against that list, not against resolvability. Fixed, with a unit test that pins the distinction rather than the symptom. Tests: 230 files / 4877 passed. The drop from 4916 is the 40 view-settings tests deleted above. Verified in `dev:browser` against an isolated NODESPACE_HOME with purpose- built conference/speaker/venue schemas: rail selection, expansion (one row at a time, independent not accordion), enum labels rendering as labels, auto-save persisting across a close/reopen, inbound read-only with values visible, target links opening via focusOrOpenNode, removal, and the full `+ Add` flow including cardinality filtering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK * Address review: restore ad-hoc edge-key visibility, retire the layout concept The review found a regression I introduced, not just dead code. Both halves of finding 1 are fixed, and fixing the first one is what makes the second one safe. **Ad-hoc edge keys had become invisible.** The panel keyed row expansion off `group.edgeFields` — the DECLARED fields — so an edge carrying only properties the schema never declared rendered as unexpandable, with its values unreachable in the UI. The old stacked-table modal surfaced those keys as read-only columns via `groupEdgeColumns`, and `groupSupportsEdgeEditing`'s docstring still promised exactly that while the code no longer delivered it. Expansion now runs over `edgeColumns` — the union of declared fields and keys actually present on stored edges. Editing still keys off the declaration: an undeclared key has no `type`, and the update path replaces edge properties wholesale, so an editor could only guess a free-text input and would coerce a stored number or boolean to a string on first save. Showing the value and declining to edit it is the honest option, which is what the docstring always said. Corrected it to describe the mechanism that now delivers it. That also gave the read-only hint a second case to distinguish. It keyed off `editable`, which is false for two unrelated reasons, and told the user to "edit these from the node that owns this relationship" — actively misleading for an OUTBOUND relationship this node already owns, where the real answer is to declare the field on the schema. Split into two messages. **`layout` / `groupLayout` / `RelationshipLayout` deleted.** The table-vs-chips distinction existed only to choose between the old modal's two renderings. With `edgeColumns` back in production use, `groupLayout` had one remaining caller: a guard inside `groupEdgeColumns` that returned `[]` for a "chips" group — redundant, since the union is empty in exactly that case anyway. Removed the guard, then the function, the type and the view-model member. Its three unit tests are replaced by three that pin the union rule instead, including the undeclared-key case that was broken. **Also from the review:** `disabled={busy}` on the enum `Select.Root`, which its checkbox and text siblings had and it did not — a concurrent-write window against a wholesale-replace API. Plus `aria-controls` linking each expander to the panel it toggles, and `role="alert"` on both error banners. Not changed: the reviewer asked the author to confirm the wording of the "empty rail with + Add" criterion. The behaviour is right — a node with no edges shows `+ Add` and nothing else — so this is a question about the issue's phrasing, not the code. Verified in the browser with a purpose-built schema declaring NO edge fields, given an edge storing one anyway: the row now expands, shows `FITTING: snug`, renders zero inputs, and explains that declaring the field is what makes it editable. Tests: 230 files / 4877 passed. svelte-check + eslint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK * Address re-review: carry the row's label, document the group-wide union Four nits from the re-review. Three taken, one declined. **`(issue #1918)` in a doc comment** violates CLAUDE.md's rule against embedding issue numbers in code — the constraint is described directly or referenced by ADR, not by issue. Removed; grepped the rest of the relationship files and found no others. **`openTarget` was dropping a label it already had.** It called `focusOrOpenNode` with only `nodeType`, so every target opened as "Loading..." until its viewer mounted — even though the row holds a resolved label. `title` is exactly the option phase 1 added for a caller in this position, and the collection viewer already uses it the same way. **The group-wide union was undocumented.** `expandableFields`' comment explained why `edgeColumns` beats `edgeFields` but not that the union spans the whole group, which is the surprising half: a row whose own edge lacks a property still shows it, empty. That is deliberate — per-row columns would make the expander flicker down a list of near-identical rows and would hide the difference between "this edge has no note" and "no edge here has a note", which in a group where three of four carry one is information. Declared fields already behave this way. Written down so the next reader files it as intent rather than a bug. **`{@const open}` shadowing the `$bindable` prop** — renamed to `rowOpen`. Checked first whether it was a live bug: it is not. `open = false` in `openTarget` sits outside the each-block and correctly assigns the prop, and the shadowed name is only read within the block. Renamed anyway because a collision with a bindable prop is a trap for whoever edits this next, and it costs nothing. **Declined: renaming `expandableFields`.** It returns `EdgeRow[]` rather than fields, so the name is imprecise — but "field" is the domain's word here, shared by the docstring, `edgeFields`, `RawEdgeField` and `edge-field-input.ts`. A more literally accurate name would read less like the code around it. Worth revisiting only if `EdgeRow` grows beyond a name and an optional declaration. Tests: 230 files / 4877 passed. svelte-check + eslint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK * Follow DESIGN.md's conventions for the relationship rail Checked the panel against nodespace-docs/DESIGN.md rather than against the tokens alone. Colors were already tokenized — no hex, rgb, or Tailwind palette literals anywhere — but four things diverged from the documented system. **The rail's selected row used `bg-accent`.** In this palette `--accent` is `176 100% 24%`, a saturated dark teal, paired with a near-black `--accent-foreground`: a brand color and a legibility problem, not a selection surface. DESIGN.md's Sidebar spec is explicit — "navigation items use `active-nav-background` when active, `muted` on hover" — and the rail is a navigation list. Confirmed in the browser: the selected row now computes to `rgb(234,234,236)` in light and `rgb(34,34,37)` in dark, the cool-neutral hue-240 surface the sidebar uses, instead of teal. Worth naming why `bg-accent` was not simply wrong: nine other components use it — dropdown focus, autocomplete selection, calendar cells. The distinction is that those are all TRANSIENT highlight. A rail selection is persistent structural state, which is what `active-nav-background` is for. **Hover was hardcoded as `bg-muted/60`.** DESIGN.md: "derive hover states from `--hover-background` / `--hover-foreground` — never hardcode hover colors." Now reads those variables. Both tokens are consumed through `hsl(var(...))` in a scoped style block rather than Tailwind classes, because neither is mapped in the Tailwind config. The navigation sidebar reads them the same way, so this follows the one existing consumer instead of inventing a second access path. **The "Back" button used `variant="ghost"`.** DESIGN.md: "no outline/ghost variants in the current system — use secondary for low-emphasis actions." Switched. Note the ghost variant is defined as `hover:bg-accent`, so it was a second route to the same teal the first fix removed. **`text-[0.6875rem]` (5 uses) was an arbitrary value** where the type scale has `text-xs` (0.75rem, the documented `body-xs` role). It was the only file in the codebase using that size; other components style the same uppercase micro-label with `text-xs`. Inherited from the modal this replaced rather than introduced here, but no reason to carry it forward. Verified in the browser in both light and dark, with computed colors read back rather than eyeballed. Tests: 230 files / 4877 passed. svelte-check + eslint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #2385.
The problem
The panel rendered one full section per schema-declared relationship, so its size tracked the schema rather than the node's data. An
adrnode with seven declared relationships and no edges rendered seven headers all reading "0 items", seven identical "+ Add" buttons, and nothing that was actually data — most overwhelming exactly when it had least to say.What changed
Empty types no longer get sections. Only groups with edges render as sections. Empty outbound relationships collapse behind a single "Add relationship" chooser; empty inbound groups are dropped entirely, since they have no Add of their own to justify standing open.
partitionGroupsowns that rule in the pure module so it is unit-testable rather than inline in the template.Inbound groups are read-only. An inbound group is the same physical edge seen from the other end, declared on and owned by the other node's schema. Offering Add there misrepresented ownership, silently inverted the picker's semantics (pick a source, not a target), and could not work at all when the other type doesn't declare the forward relationship. They now carry an
incoming · read-onlybadge and a dashed rule — distinguishable without reading the arrow direction — and their rows navigate to the node that owns the edge, where it can be edited properly.Controls are per row, not behind a panel-wide mode.
editModeis deleted, along with the Done/Close footer — the dialog's ✕ is the single dismiss control. Edge values render read-only in their columns at all times; ✏️ opens an editor for that one edge and appears only where the schema declaresedge_fields, so a relationship with none shows ❌ alone rather than an editor with nothing to edit. Editing one row leaves every other row — and the panel behind it — untouched.Why a dialog for edge properties, rather than inline cells or a popover: each relationship declares its own
edge_fields, so the form is per-relationship and variable in length, and it needs room to grow for the collection↔person RBAC fields. Its input helpers moved toedge-field-input.tsso the add form and the row editor render a given field type identically.Verification
Driven in
dev:browseragainst an isolated daemon (NODESPACE_HOMEpointed at a throwaway copy, per ADR-053), using anadrschema with four outbound relationships — one carryingedge_fields— and two reverse names:depends_on(noedge_fields) shows ❌ and no ✏️Superseded Byshows no Add/edit/remove; its row navigates to the owning ADRTests: 233 files / 4886 passing (baseline 232 / 4863 — 23 new tests covering the partition rule, the edit-affordance predicate, and the edge-field input helpers). Lint and
svelte-checkclean.Notes for review
window.confirmon removing a required relationship's last edge is unchanged, but is now reachable without first entering an edit mode — same behavior, more exposure.🤖 Generated with Claude Code
https://claude.ai/code/session_01PtzaT8FoAP9ut43eveHUdK