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-2051Done

11.2.9 `GET` + `POST` + `DELETE /api/v1/work-items/{key}/links` — the dependency and relationship edges, `blocked_by` included

The edges are what make Motir's data a plan rather than a list — blocked_by is the edge the ready set reads, so an integration that cannot write one cannot express a dependency at all. app/api/v1/work-items/[key]/links/route.ts carries GET (scope: 'read'), POST and DELETE (both scope: 'work_items:write').

What to build

  • GET — this item's edges in one shape: blockedBy · blocks · relatesTo · duplicates · clones, each entry { key, title, status, relationship }. The groups come from the aggregate workItemsService.getIssueDetail already resolves (one service call; do not assemble them from several reads), presented through the schema module so the group shape here and inside the detail resource are literally the same declaration — two shapes for one concept is how they drift.
  • POST — body { toKey, relationship } where relationship is the shipped set (blocked_by · blocks · relates_to · duplicates · clones), applied via workItemsService.linkWorkItems. The FROM side is the path key; edges may cross projects inside the workspace, never across workspaces. 201 with the created edge.
  • DELETE — addressed by endpoints, not by link id: ?toKey=&relationship=, applied via workItemsService.unlinkWorkItemsByEndpoints, which is idempotent and returns whether a row was actually removed. So DELETE returns 204 whether or not an edge was there — the correct HTTP reading of an idempotent delete, and it means a retried teardown is safe. No internal linkId appears on the wire in either direction (ADR §7).
  • Domain rows for DOMAIN_ERROR_STATUS, each proven by a test: SELF_LINK and WORK_ITEM_LINK_CYCLE → 422 (the caller can fix the request); DUPLICATE_LINK409 (a conflict with existing state, not a malformed request — the shipped service deliberately throws rather than silently succeeding on a manual link); CROSS_WORKSPACE_LINK / WORKSPACE_MISMATCH_LINK404 on the target key, because confirming the other item exists in another tenant is the existence oracle ADR §4 forbids; WORK_ITEM_LINK_NOT_FOUND → 404 for a read that names a missing edge. 409 is a status the ADR §4 table does not list — append the row with its condition, as a new condition rather than a changed one (additive under §8).

Scope BOUNDARY

Ends at this item's edges. It does not expose link CANDIDATE search (listLinkCandidates serves a picker UI, not an API contract), does not touch parent/child structure (re-filing is parentKey on 11.2.6's PATCH — a parent is not a link and must not be settable through both), and does not compute or expose readiness beyond what the detail resource already carries. It changes no link service or repository, and adds no relationship kind.

Acceptance criteria

  • GET …/links returns all five groups (empty arrays when a group has no edges, never omitted keys — an absent key and an empty group are different things to a typed client).
  • The group shape is the SAME schema declaration the detail resource uses — asserted by parsing both bodies against it.
  • POST with relationship: 'blocked_by' creates the edge, and the target immediately shows the reciprocal direction on ITS GET …/links — the round trip a dependency-writing integration depends on.
  • A relates_to edge shows on both items (the shipped reciprocal), while a blocked_by edge shows as blockedBy on one and blocks on the other.
  • DELETE is idempotent: the second call returns 204 exactly like the first, and a DELETE naming an edge that never existed is also 204.
  • Re-creating an existing link returns 409 with code: 'DUPLICATE_LINK'; a self-link and a cycle each return 422 with their own codes.
  • A toKey in another workspace returns 404 — indistinguishable from a key that does not exist.
  • A read-only token gets 200 on GET and 403 on both writes.
  • The ADR §4 status table carries the 409 row with its condition.
  • Every export composes withV1Route with its own scope; no Prisma, no transaction in the route.
  • The per-file coverage floor (≥90%) holds on every new file.

Context refs

  • lib/services/workItemsService.tslinkWorkItems, unlinkWorkItemsByEndpoints (and its idempotent contract), getIssueDetail's link groups.
  • lib/workItems/linkErrors.tsSELF_LINK · WORK_ITEM_LINK_CYCLE · DUPLICATE_LINK · CROSS_WORKSPACE_LINK · WORKSPACE_MISMATCH_LINK · WORK_ITEM_LINK_NOT_FOUND.
  • lib/dto/workItemLinks.tsWorkItemLinkDto, the shape mapped from.
  • lib/mcp/tools/linkWorkItems.ts — the shipped tool proving both service paths and the endpoint-addressed unlink.
  • lib/mcp/dependencyEdges.ts — how the MCP surface already projects edges, for shape consistency.
  • Producer: 11.2.2 (the schema module). Sibling that owns re-parenting: 11.2.6. Parent story: 11.2.