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
144
planned
1,362
shipped

Motir · Work items

MOTIR-1852Done

11.2 Work items over `/api/v1` — list/search, read, create, update, transition, comments and dependency links

The core resource of the API. Work items are what Motir is about, so this is the story that makes /api/v1 worth integrating against: an external client can read, search, create, update, transition, comment on and link work items without touching the web app.

blocked_by 11.1 — every endpoint here composes that story's route wrapper, scope gate, pagination and error envelope. It adds one convention of its own, recorded as an ADR amendment (11.2.1) rather than improvised per endpoint.

The journey step — the verification recipe

A developer holding a scoped PAT: list a project's work items → filter them → read one in full → create one → move it through its workflow → comment → link a dependency → archive and restore. Independently testable end to end via curl or any HTTP client the moment it lands; 11.2.12 automates exactly that walk as an external client.

What to build

  • GET /api/v1/projects/{projectKey}/work-items — the paginated list, with the same filter grammar the /items view and search_work_items share (the versioned FilterAST, lib/filters/ast.tsone query grammar, never a parallel one).
  • GET /api/v1/work-items/{key} — the detail aggregate: fields, lineage, children, dependency edges, readiness verdict.
  • POST /api/v1/projects/{projectKey}/work-items — create (kind, parent, the leaf-authoring fields).
  • PATCH /api/v1/work-items/{key} — update the editable field set, including re-file (parentKey) and re-classify (kind), with optional If-Match optimistic concurrency.
  • GET / POST /api/v1/work-items/{key}/transitions — the legal moves from here, and the move itself; an illegal move returns the allowed targets as data, which is what makes the API self-teaching.
  • GET / POST /api/v1/work-items/{key}/comments.
  • GET / POST / DELETE /api/v1/work-items/{key}/links — the dependency + relationship edges, blocked_by included.
  • POST /api/v1/work-items/{key}/archive and its restore, gated on work_items:archive.

Scope mapping is per operation, not per resource: reads take read, mutations work_items:write, archive/restore work_items:archive. The destructive cascade delete stays gated on work_items:delete and is deliberately NOT exposed in this story — see the boundary.

What the expansion pass CORRECTED (2026-08-03)

This story was authored as pure transport — "a thin adapter over an already-shipped service method — no service, repository, mapper or migration changes". Verifying that against origin/main @ c4ec51b1 found it false in three places, each recorded rather than absorbed:

  1. The list endpoint cannot be a pure adapter. getProjectIssuesList is LIMIT/OFFSET and clamps page size to ISSUE_LIST_PAGE_SIZE = 50, while the ADR forbids offset paging and pins a 100 ceiling; paginateKeyset slices a fully-read array, which over 1800+ items is the unbounded read this story's own criteria forbid. No keyset read of work items exists. So a keyset-paged read is added at the repository + service layer (11.2.3) under a bounded carve-out the ADR amendment records: a new page ADDRESSING over an unchanged predicate — same result set, no new filter axis, no new gate, no new field. The comments endpoint takes the same carve-out for an optional page-size parameter.
  2. The responses need a schema layer, and it belongs HERE. The two shipped routes shape rows inline; ten endpoints returning one resource cannot. 11.4 had claimed the per-resource schema modules, which would mean shipping ad-hoc shapes now and retrofitting them later — the exact drift 11.4 exists to prevent. Corrected: each resource story ships its zod response schemas next to its routes; 11.4 assembles, guards and publishes. A v1 response is a schema's output, never a service DTO passed through.
  3. An API-created item has no honest provenance. WorkItemPlanningSource is native | mcp | manual — no api value — and rows created before that is fixed are unattributable permanently, so the enum + its totality sweep is a prerequisite of create (11.2.5), not a follow-up.

Completeness — planned to real-product scale

Every collection here is unbounded in a real tenant (the Motir project itself holds 1800+ items), so: keyset cursor pagination on every list, the FilterAST for narrowing, and no "load every row" read anywhere. Empty, not-found, forbidden and validation-failure responses are part of the deliverable, not the green path only. Concurrent writes follow the shipped services' existing locking — this story adds no new read-derived write and must not introduce one; where a service already offers a concurrency precondition (expectedUpdatedAt), the API exposes it rather than hiding it.

Type sweep — what this story does NOT need, deliberately

Swept the complete subtask-type set so an omission is a decision: design — no rendered surface (the reference page is 11.4's and carries its own design subtask). manual/human — no external account, secret, DNS or dashboard step; PAT issuance already ships and the rate-limit env vars landed with 11.1. deploy — no new infrastructure. copy / translate — the one user-visible string this story adds is the provenance label in 11.2.5, which carries its own en/zh catalog pair. legal — no new artifact. research/spike — the decision card carries the grounding. So: one decision, nine code, two test.

No acceptance video. No user-observable surface — exempt under the acceptance-video rule's non-UI carve-out, accepting on its tests alone, exactly as 11.1 did. The conformance suite is this story's E2E, in the form the story actually has: the client is curl.

Scope BOUNDARY

Ends at the work-item resource and its sub-resources. It does NOT ship projects, sprints, backlog or the ready set (11.3) — sprint assignment included, which is sprints:write; does NOT ship attachments or file upload (presigned and session-bound — a separate, later card, and no card here defers work to it); does NOT expose the irreversible cascade delete (work_items:delete), a deliberate omission for a first public surface exactly as that scope is OFF by default on a token today — adding it later is additive and safe, whereas shipping it early is not; does NOT expose comment EDIT or DELETE (moderation-gated operations the ADR's scope table does not map); does NOT expose workflow administration; and does NOT write the OpenAPI spec (11.4).

On services: beyond the two bounded read-addressing changes named above, it changes no service, repository or migration. If an endpoint here wants DATA a service does not expose, or a new filter axis, gate or field, that is a card in the owning feature's epic — not a service change smuggled in at the edge.

Acceptance criteria

  • Every listed endpoint exists, enforces its mapped scope, and returns 11.1's envelope shapes — asserted per endpoint.
  • The list endpoint accepts the versioned FilterAST and produces the SAME result set as the /items view for an identical filter (the one-grammar contract — asserted end to end, so the API can never disagree with the web app about what a filter means).
  • Every list endpoint pages by keyset cursor; none loads an unbounded result set, and none silently caps below the documented 100 ceiling.
  • A paged scan of a collection being written concurrently never skips or duplicates a row.
  • Every response body is produced by a declared zod schema, not hand-shaped inline, and carries no internal cuid where a MOTIR-<n> key exists.
  • An illegal status transition returns the allowed targets as structured data; a legal one persists and is visible on a subsequent read.
  • A read-only token is refused (403) on every mutating endpoint, and archive is refused without work_items:archive.
  • Cross-tenant reads and writes are refused with the product's 404-not-403 behaviour, asserted for every endpoint that takes a key.
  • Every domain error an endpoint can raise has a deliberate row in the v1 status map, proven by a test that drives it — an unmapped error becoming a bare 500 is a defect, not a default.
  • No route calls Prisma or opens a transaction; each is one service call (the 4-layer contract).
  • The cascade delete is NOT reachable through any /api/v1 route in this story — asserted, so the omission cannot be undone by accident.
  • The per-file coverage floor (≥90%) holds on every new file.

Context refs

  • lib/services/workItemsService.ts — the service methods every endpoint adapts.
  • lib/mcp/tools/ — one tool per operation above; each proves its service path already exists and shows the argument shape. Note the v1 routes must NOT import from it — the two surfaces align through schemas, not imports.
  • lib/filters/ast.ts + lib/filters/registry.ts — the versioned FilterAST grammar and its field set (kind · status · priority · type · assignee · reporter · sprint · text · created · updated · due · storyPoints · estimate · lbl · cmp). There is no parent facet — if an endpoint here needs one, that is a registry card, not an inline query.
  • lib/dto/workItems.ts — the internal DTO shapes the v1 schemas map FROM.
  • lib/api/v1/ — the shipped wrapper, error envelope, pagination and rate limiter this story composes.
  • docs/decisions/public-api-conventions.md — the contract, as amended by 11.2.1.
  • Foundation: 11.1. Parent epic: the public REST API.