Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-1403Done

5.8.3 Schema + service — parse refs on write → auto-create `relates_to` link

Type: code · Executor: coding_agent · Repo: motir-core. Blocked by 5.8.2 (the parse/resolve helper).

The core of the feature: on every write that can contain a reference, parse it and record the relationship. Hooks the SAME write paths that already re-parse @user mentions.

Schema (small migration). Add WorkItemLink.source enum (manual | mention, default manual) — provenance, mirroring the attachments link-on-write source column (5.2.3). It marks auto-created edges (for render/affordance + so we never need a separate work_item_mention table — the relates_to row IS the record). FK/relations unchanged; @@unique([fromId, toId, kind]) already backstops dedup.

Service logic (4-layer; all DB work inside the existing $transactions). In workItemsService.createWorkItem (~L667) and updateWorkItem (~L989), for the descriptionMd, explanationMd, AND title fields (today only descriptionMd is parsed, for the notification event; explanation/title are NOT parsed at all — extend them), and in commentsService.addComment (~L187) / editComment (~L284):

  1. parseWorkItemRefs(text) (5.8.2) -> token ids + bare keys.
  2. Resolve to work items in the SAME workspace the author can VIEW (reuse the browsable-project / viewable scope already used by quickSearch / resolveMentionableIds); resolve bare keys -> ids; drop unresolved/forbidden silently (the Jira mention rule). Drop self-references.
  3. For each resolved target, check any existing link between the pair in ANY kind, EITHER direction via a new workItemLinkRepository.findAnyBetween(aId, bId, tx) (queries is_blocked_by/relates_to/duplicates/clones both ways). If none, create relates_to (source: mention) — reusing the linkWorkItems create path (reciprocal row + revision) so all invariants hold. If a link already exists in ANY relationship, do nothing (the user's directive — never downgrade a blocked_by into an extra relates_to).
  4. Idempotent + concurrency-safe: the create races (two saves, or save + manual link). SELECT ... FOR UPDATE the contended rows / rely on @@unique([fromId,toId,kind]) and catch P2002 -> treat as already-linked (the lock-before-read-derived-update + typed-race rule). The auto-link is a DB write -> it belongs INSIDE the write tx (no external side effect).
  5. Non-destructive: removing a reference from text NEVER deletes the link (a relates_to the user may now rely on). Decided over diff-and-delete to avoid surprising relationship loss; re-adding is idempotent.

Acceptance criteria

  • Saving a description/explanation/title/comment containing a motir: token or a bare MOTIR-N key creates exactly one relates_to WorkItemLink (source: mention) per distinct, viewable, non-self target with no pre-existing link.
  • A pair already linked in another kind (blocked_by / blocks / duplicates / clones) is left untouched — no relates_to added.
  • Re-saving the same text adds no duplicate link; removing the reference leaves the link intact.
  • Concurrent writes that both reference the same target produce exactly one link (a real-concurrency test with a warm pool, accepting either winner — the TOCTOU guard).
  • Forbidden/cross-project/unresolved refs are dropped silently, never error.
  • Unit tests for the resolve+gate+dedup logic; the user-mention notification path is unchanged.

Context refs: lib/services/workItemsService.ts (createWorkItem ~629/667, updateWorkItem ~945/989, linkWorkItems ~2166, findReciprocal), lib/repositories/workItemLinkRepository.ts (create, findReciprocal, add findAnyBetween), lib/services/commentsService.ts (addComment ~181, editComment ~278), prisma/schema.prisma (WorkItemLink, WorkItemLinkKind), lib/mentions/workItemRefs.ts (5.8.2).