ADR-0015: Append-Only review_notes JSONB Array for Task Rejection History

Status

Accepted

Tags

fieldforce, jsonb, audit, history, immutability

Decision

Task rejection feedback is stored as an append-only JSONB array on ff_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 single review_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 text column: Only the latest rejection is preserved. Field team loses context on earlier revision requests. Rejected.
  • Separate ff_review_notes table: Adds a join and migration complexity. JSONB append is sufficient for the volume. Rejected.

How it works

Append flow (state machine reject action):
The UI displays all entries chronologically. The most recent entry is shown as the active feedback banner when status = needs_revision.

Known limitations

  • The review_notes array 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 reject action may append to review_notes — no other service method may modify this column
  • Never UPDATE or clear review_notes — treat any SQL UPDATE ff_tasks SET review_notes = '[]' outside the application as a data corruption event
  • review_notes is always []ReviewNote in 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

Bad pattern (do not generate)

Good pattern