5.5.1 Activity read service — paged revision feed + the TOTAL diff-renderer registry (batched display resolution, noise policy, fallbacks)
Estimate: 30m
The mapping layer: lib/services/activityService.ts (read-only — no writes, no mutation routes) turning work_item_revision rows into renderable ActivityEntryDtos.
listHistory(workItemId, { cursor, order }, ctx) — view-gated (404 cross-workspace, finding #44), riding the EXISTING 1.4.6 repo cursor read (listByWorkItem pages newest-first by (changedAt, id) — rung-2; extend the repo with the asc variant for the oldest-first order, same index). Page size 20, totalCount for the tab badge/Show-more copy.
The renderer registry (lib/activity/renderers.ts): one entry per diff key, each producing a typed ActivityEntryDto part — { field, verb, from, to } display forms. Coverage is TOTAL over the audited vocabulary (the plan-time call-site audit): the scalar field keys from create/update/move/archive (title, descriptionMd → rendered as "updated the Description" without inlining the body, explanationMd/explanationSource, kind, priority, assigneeId, reporterId, dueDate, estimateMinutes, storyPoints, parentId, archivedAt, projectId, key/identifier, position), status (workflow LABELS via the project workflow, not raw keys), sprintId (sprint names — "moved to <Sprint>" / "moved to the backlog"), backlogRank, links (added/removed → identifiers + link kind), and the 5.1–5.4 shapes as they land (attachments, labels, components, customFields.<key> via the definition label, the comment-deleted record) — plus the generic fallback for any unknown key ("changed <key>", raw values stringified safely). Mistake #29: the lookup is total BY CONSTRUCTION — a registry miss falls through to the fallback, never throws, never silently drops.
Noise policy (explicit, not accidental): keys marked suppressed — pure position/backlogRank-only diffs and the denormalised key/identifier writes — produce NO entry (Jira shows no reorder noise); a diff mixing suppressed + renderable keys renders the renderable parts. The suppression list lives IN the registry so every key has an explicit disposition.
Display resolution, batched per page: collect the page's referenced ids (actors + assignee/reporter values → users; status keys → workflow labels; option ids → option labels; linked ids → identifiers; sprint ids → names) and resolve in ONE batched lookup set (no N+1). Deleted referents degrade to the stored id / a "former member" form — never a crash. created renders as the anchor entry; archived renders.
Acceptance criteria
listHistorypages both orders over the existing index withtotalCount; view-gated; cross-workspace 404; NO mutation surface exists anywhere in the new code (read-only contract).- The registry covers EVERY diff key found in the plan-time audit with explicit renderable/suppressed disposition + the generic fallback; a test enumerates the codebase's recordRevision call sites' keys against the registry (the totality guard — a new unregistered key fails the test, pointing at the fallback decision).
- Status renders workflow labels; sprint moves render names; links render identifiers; description edits never inline body text; suppressed keys produce no entry; mixed diffs render partially.
- Resolution is one batched lookup set per page (asserted — no per-entry queries); deleted referents render fallbacks.
pnpm test:coverage≥90% across the registry branches incl. fallback + suppression.
Context refs
lib/repositories/workItemRevisionRepository.ts(listByWorkItem— the existing cursor read to extend withasc) +lib/services/workItemRevisionsService.ts+ theWorkItemRevisionDtodiff shape- The plan-time diff-vocabulary audit in the Story 5.5 description (the 17 call sites);
notes.htmlmistake #29 (total lookups) workflowsService(status labels),sprintRepository(names), the 5.3 option/definition reads, the member DTOs — the resolution sources- finding #44 (404-not-403); finding #57 (paged, never load-all)