ADR-0015: Append-Only review_notes JSONB Array for Task Rejection History
Status
AcceptedTags
fieldforce, jsonb, audit, history, immutabilityDecision
Task rejection feedback is stored as an append-only JSONB array onff_tasks.review_notes (default '[]'). Each rejection appends a {date, note, reviewer_id, reviewer_name} object. Nothing is ever overwritten or deleted from this column. The Go domain type is []ReviewNote — never nil, empty slice when no rejections.
Why
A task can go through multiple rejection cycles (completed → needs_revision → completed → needs_revision). A singlereview_note text column silently overwrites previous feedback, losing context that field teams need to understand what changed between cycles. A separate table adds a join and migration complexity for an ordered list of at most 2–3 entries per task.
Rejected alternatives:
- Single
review_note textcolumn: Only the latest rejection is preserved. Field team loses context on earlier revision requests. Rejected. - Separate
ff_review_notestable: Adds a join and migration complexity. JSONB append is sufficient for the volume. Rejected.
How it works
reject action):
status = needs_revision.
Known limitations
- The
review_notesarray grows indefinitely if a task is rejected many times. In practice, tasks rarely exceed 2–3 rejection cycles. If a task accumulates >20 entries, it is a process problem, not a data problem. - Entries cannot be amended after the fact — the immutability is intentional (audit integrity). If a reviewer entered incorrect feedback, they must add a corrective entry.
Rules for agents
- Only the state machine’s
rejectaction may append toreview_notes— no other service method may modify this column - Never UPDATE or clear
review_notes— treat any SQLUPDATE ff_tasks SET review_notes = '[]'outside the application as a data corruption event review_notesis always[]ReviewNotein Go — never nil; initialize to empty slice when scanning a NULL value from DB- Do not expose a write endpoint for
review_notes— it is system-populated only