11.7.7 The ACTIVITY read over `/api/v1` — the change trail beside the comments, and its opaque composite cursor
The last uncovered read: a work item's change trail, and the merged view that interleaves it with the discussion. GET /api/v1/work-items/{key}/comments already ships and covers one of the three views; the other two have no v1 path.
What to build
The activity read in its three views, adapting activityService.listHistory (changes), commentsService.listComments (comments) and activityService.listAll (both, interleaved). Scope: read. Path and the relationship to the shipped comments endpoint per 11.7.1 Q1 — the comments endpoint is published API under §8 and cannot be withdrawn, whichever shape that decision chose.
The all cursor is the hard part
The merged view pages over two sources, so its cursor is an OPAQUE COMPOSITE carrying both positions. Three properties, all of which a naive implementation breaks:
- A client must never construct, parse, split or merge one. It is echoed back exactly as received.
- It is collection-scoped — a cursor issued for the
allview, handed to thechangesview, is a 422, not a silent reset onto a meaningless position. v1's signed envelope already gives this; the card's job is not to defeat it. - Interleaving must be stable across pages. Two sources merged by timestamp with equal timestamps and no tiebreak will duplicate or drop rows at a page boundary — the failure that only appears on real data, and never in a fixture where every row is a second apart.
Views are not a second endpoint each
Whatever 11.7.1 pinned, the three views share one representation of an entry and one paging contract. An entry's shape stays LOOSE in the way the shipped DTO is loose — a part kind or value type a client has never seen must render as its generic form rather than break the read, because a published client meets newer servers.
Scope BOUNDARY
Ends at the activity read. It does NOT change, move or deprecate the shipped comments endpoint. It does NOT add comment WRITE, edit or delete — writing a comment already ships, and edit/delete are moderation-gated operations the ADR's scope table does not map. It does NOT change activityService or what an entry records. It does NOT touch the MCP tool.
Acceptance criteria
- The endpoint serves all three views, gated on
read, declared in the registry and present in the emitted spec. - The shipped
…/commentsendpoint still exists and behaves identically — asserted, since §8 forbids withdrawing it. - The
allcursor round-trips as an opaque value; a cursor from another collection or another view is refused with the mapped 422, never silently reset. - Paging the
allview over rows with IDENTICAL timestamps returns every row exactly once — asserted with a fixture built to collide, because a stable-order bug is invisible on any fixture where timestamps differ. - An entry whose part kind or value type the schema does not recognise is returned in its generic form rather than failing validation.
- Empty, single-page and multi-page cases each behave, including an item with comments but no changes and the reverse.
- The payload matches the MCP tool's for the same item and view, field for field.
- Cross-tenant keys return 404; a token without
readis refused 403. - The per-file coverage floor (≥90%) holds on every new file.
Context refs
- 11.7.1 Q1 — the path decision and how it sits beside the shipped comments endpoint.
- 11.7.3 — the schema module and the activity-entry shape.
lib/services/activityService.ts—listAllandlistHistory;lib/services/commentsService.ts—listComments.lib/mcp/tools/getWorkItemActivity.ts— the three views, the opaque composite cursor, and the loose-entry contract to mirror. Read, not imported.lib/dto/activity.ts+lib/dto/comments.ts— the shapes mapped FROM, including the open part-kind union.app/api/v1/work-items/[key]/comments/route.ts— the shipped sibling this must not disturb.lib/api/v1/pagination.ts— the signed collection-scoped cursor envelope the composite rides inside.tests/mcp/get-work-item-activity.test.ts— the shipped behaviour to agree with.- Story: 11.7.