Repository navigation
Description
A task's relationships panel cannot assign a person. The + Add menu offers only Blocks, Relates To, Duplicates — the three task↔task links task declares itself — while assignee, project and creator are absent, even though the data to render them is already in the payload.
Why they're missing
relationship-grouping.ts:315:
export function partitionGroups(groups: RelationshipGroupView[]): PartitionedGroups {
return {
populated: groups.filter((g) => g.rows.length > 0),
addable: groups.filter((g) => g.direction === 'out' && g.rows.length === 0)
};
}A task's assignee is the derived inverse of person's outbound tasks declaration, so its group has direction: 'in'. With no edges yet it satisfies neither branch and is dropped entirely. The current rationale:
empty INBOUND groups are dropped entirely. An inbound group is the same physical edge seen from the other end, owned by the other node's schema, so it has no Add of its own to justify standing open and empty.
That reasoning assumes the user will go author from the other end. For assignee that means: open the person, find tasks, add the task there — the inverse of how the operation is actually thought about. The same drop hides project (inverse of project's tasks) and creator (inverse of reported_tasks).
The data is present: get_node_relationships returns the inbound group with reverse_name: "assignee" and its cardinality, asserted end-to-end by person_task_assignee_relationship_assigns_and_clears_end_to_end (node_service/mod.rs:8207). The panel discards it before rendering.
The rule
Direction should not decide whether a control appears — it only decides which arguments to transpose when writing the edge. The local end's cardinality decides the verb:
| Local cardinality | Verb | Semantics |
|---|---|---|
One |
Assign / Set | single-select, replaces the existing edge |
Many |
Add | appends |
Applied to the current schemas:
- task ↔ person — task end
One→ assign a person; person endMany→ add a task. Asymmetric verbs for one edge. - task ↔ task (
blocks,relates_to,duplicates) —Many/Many→ add from both ends. - task ↔ project — task end
One→ assign the project; project endMany→ add tasks.
For an inbound group the local end is governed by reverse_cardinality and the far end by cardinality, so the transposition applies to the cardinality read as well as to the create arguments.
This also covers a case the current populated/addable split cannot express: a filled One end still needs a Change/Reassign control, whereas today a populated group gets no control at all.
Writing the edge
Creating from the inbound side must transpose the arguments — create_relationship(person_id, "tasks", task_id), not (task_id, "assignee", person_id) — because the stored relationship_type is the declared forward name.
Dependency
Depends on #2804. "Assign = replace" cannot be enforced by the UI alone: reverse_cardinality: One is currently unenforced at the store layer, so any other writer (agent tool, Play, Rust caller) can create a second assignee that a single-select control cannot represent. The replace-vs-reject decision in #2804 determines what this control does on re-assign.
Acceptance criteria
- An empty relationship group surfaces a control regardless of direction
- The verb is chosen by the local end's cardinality:
One→ assign/set (single-select),Many→ add (append) - For inbound groups, the local cardinality is read from
reverse_cardinalityand the label from the declaration'sreverse_name - Assigning from a task creates the edge with transposed arguments, and the person's panel then shows the task under
tasks - A populated
Onegroup offers change/reassign rather than no control -
Blocks/Relates To/Duplicatescontinue to behave as today (Manyboth ends) -
partitionGroupsunit tests updated to cover inbound groups and both cardinalities
Out of scope
Whether assignee should additionally be a first-class field on the task UI (rather than living only in the relationships panel) is a separate design question, not settled here.
Activity
malibio commentedon Sep 26, 2026
Frontend baseline: 273 files / 5355 tests passed. Scope note: #3050 (#2806) shipped the local-one half (form-field assign/reassign with transposed writes). This branch covers the remaining half: Add for empty inbound many groups in the Relationships modal.