Skip to content

Task relationships panel can't assign a person: empty inbound groups are dropped, so cardinality never drives the affordance #2805

Closed

Description

@malibio

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 end Many → 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 end Many → 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_cardinality and the label from the declaration's reverse_name
  • Assigning from a task creates the edge with transposed arguments, and the person's panel then shows the task under tasks
  • A populated One group offers change/reassign rather than no control
  • Blocks/Relates To/Duplicates continue to behave as today (Many both ends)
  • partitionGroups unit 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

  1. malibio commented on Sep 26, 2026

    @malibio
    CollaboratorAuthor

    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.

  2. self-assigned this
    on Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions