5.5 Activity history feed
The per-issue History feed: every field change, transition, link/label/component/attachment change, sprint move, and comment deletion — rendered chronologically from the append-only work_item_revision trail that already exists (1.4.6; every mutating path records a diff — the rung-2 audit at plan time found 17 call sites and no gaps). This story adds NO write model: it is the read mapping (diff → human-readable entry), the All merged stream (comments + history interleaved), and the tab UI completing the Activity section Story 5.1 ships with the History seam reserved.
The Jira-verified shape (rung 1, checked at plan time). Activity filters All / Comments / History (Jira adds Work log — we have no worklog feature, so no tab), default Comments, with ONE per-user newest/oldest toggle spanning all tabs (the verified JRACLOUD-73076 behaviour — 5.1.5's toggle generalises to the section). A History entry is actor + "changed the <Field>" + old → new + timestamp, empty sides as "None" (the changelog null). History carries field edits, transitions, and comment deletions (who/when, never the content — exactly what 5.1.2 records); comment adds stay under Comments/All (the verified-safe default). And History is append-only — no edit or delete surface for anyone, admins included (mirrored: this story ships read-only, no mutation API exists).
The renderer registry (the real work). Diffs are machine shapes ({ assigneeId: { from: "u_1", to: "u_2" } }); the feed needs sentences ("Bo changed the Assignee: Odie → Mo"). A per-key registry maps every diff key the codebase writes — the scalar fields, status (workflow labels), sprintId/backlogRank (sprint moves render as "moved to Sprint X" / rank changes are NOISE — collapsed, see below), links (added/removed with identifiers), and the in-flight 5.1–5.4 shapes (attachments, labels, components, customFields.<key> via the definition label, comment-deleted) — TOTAL with a generic fallback (mistake #29: a lookup over an open set must cover every value; unknown future keys render a legible generic "changed <key>" entry, never crash or vanish). Display resolution is batched per page (users, status labels, option labels, linked identifiers — one lookup set per page, no N+1) and tolerant of deleted referents (a removed user/option/issue renders its stored id/name fallback, never a broken entry). Noise policy (a deliberate decision): pure position/backlogRank reorders and the denormalised key/identifier writes are suppressed from the feed (Jira does not show board-reorder noise in History; the data stays in the trail) — the registry marks keys renderable/suppressed explicitly, so suppression is also total, not accidental.
The All stream. Comments (5.1) and history entries interleave chronologically under All — a service-level merge of the two cursor-paged sources with a composite (timestamp, type, id) cursor, page-bounded from both sides (never fetch-all-then-sort; finding #57). The Comments tab stays exactly 5.1.5's surface; the History tab is revisions only; All is the merge with each entry in its native row grammar.
Completeness — the real-product states. The created revision renders as the feed anchor ("Bo created the issue"); archived renders; empty History ("No history yet" — practically unreachable since create writes one, but the state exists); loading skeleton rows; ErrorState; "Show more" at each tab's older edge; the sort toggle flipping all tabs together; viewer read-only (the whole surface is read-only anyway — no affordance delta); deleted-referent fallbacks. All drawn by 5.5.3, asserted in 5.5.5.
Out of scope (documented extension slots, each justified): a Work log tab (no time-tracking feature — the tab follows the feature if one ever lands); history for comment EDITS beyond the Edited tag (Jira keeps no comment version history either); cross-issue/project-level activity streams (Epic-6 reporting territory); retention/purge admin (the mirror has none — append-only is the contract); rendering rank/position noise (suppressed by the explicit policy above); realtime updates (the section refreshes like every other surface — the 5.1 decision).
Verification
- Pull the Story branch,
pnpm install,pnpm prisma migrate dev(NO migration — this story adds no write model; "No difference detected"),pnpm db:seed,pnpm dev. pnpm test:coverage— Vitest (real Postgres) over the activity read service (registry totality, batching, merge cursor) ≥90% per-file branch/fn/line.- History flow: sign in as
zhuyue@motir.co/!QAZ1qaz, open an issue with a history (edit title, change status, assign, link an issue, set a custom field, add+delete a comment first) → Activity → History (matching the 5.5.3 design): entries read as sentences with actor avatar + name + "changed the <Field>" + old → new (empty side "None"), the status entry shows workflow labels not keys, the link entry shows the identifier, the custom-field entry shows the field label and option label, the comment deletion shows who/when and NO content, and the oldest entry is "created the issue". - All flow: the All tab interleaves the comments and history entries in true timestamp order, each in its native row grammar; the Comments tab is unchanged (5.1.5); the section's ONE sort toggle flips all three tabs together; the default tab is Comments.
- Noise policy: drag the issue around a board/backlog (position/rank writes) → NO feed entries appear for pure reorders; the trail still holds the rows (DB check).
- Scale check (finding #57): seed an issue with 200+ revisions + 50 comments (the 5.5.5 fixture) → History first-paints one page + "Show more"; All pages via the composite cursor (network shows bounded reads from both sources, never fetch-all).
- Fallbacks: delete a user/option referenced by an old entry (or simulate) → the entry renders the stored fallback, never a crash; an artificially-injected unknown diff key renders the generic entry.
- Read-only: no mutation route/action exists for revisions (code audit + 404 on probing); cross-workspace reads 404 (finding #44).
pnpm test:e2e --grep activity— Playwright over the real stack: the History + All journeys above.- a11y check: the tabs are a proper
tablist(or the section's filter grammar from the design), entries are a labelled feed readable as text, the sweep over the populated section is clean; colour via--el-*, shape via element tokens.