mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-03 12:10:31 +00:00
5dfc2921b2
* feat(store): structured status-transition log + backfill (TASK-1637)
Add a status_transitions table capturing every item status change as a
structured, queryable row — written in the same tx as the item update and
never debounced — so the Reports surface (PLAN-1628) can reliably compute
the completed-throughput and cycle-time series.
- migrations/063 + pgmigrations/042: status_transitions table (dual-dialect),
indexed on (workspace_id, created_at) and (item_id, created_at)
- write-path hook in UpdateItemWithPreCheck records from→to on status change
- BackfillStatusTransitions: one-time startup replay parsing the historical
activities.metadata.changes blob (mirrors BackfillWikiLinks), gated on an
empty table; wired into cmd/pad/main.go
- models.StatusTransition + tests (capture, multi-hop, no-op, parser, backfill)
Spike (TASK-1629) found the activity log records status changes only as a
human-readable, debounce-coalesced metadata string — unusable for aggregation.
This is the foundation TASK-1630 (report aggregation) builds on.
* fix(store): record status transitions on item move too per Codex review (round 1)
MoveItemWithPreCheck rewrites fields outside UpdateItemWithPreCheck, so a
status-changing move override (pad item move ... --field status=done) was
not recorded in status_transitions, making the table non-canonical. Insert
the from→to row in the move tx as well, stamped with the target collection.
Adds move-path capture tests (status override + status-preserving move).
* fix(store): make status-transition backfill idempotent per Codex review (round 2)
The empty-table gate isn't atomic, so concurrent replays (a future
multi-replica Postgres deploy; single-instance today) could double-insert
historical rows and overcount reports. Give backfilled rows a deterministic,
activity-derived primary key ("bf_" + activity id) and a dialect-aware
conflict clause (ON CONFLICT DO NOTHING / INSERT OR IGNORE) so a re-run
no-ops instead of duplicating. Count only rows that actually land.
Write-path rows keep using a random newID(), so live data never collides.
* fix(store): accurate from_status under lock + document backfill caveats per Codex review (round 3)
1. from_status was read from the pre-lock `existing` snapshot. When no
precheck ran, a concurrent update (serialized behind the locks we hold)
could make it stale. Capture the status from a fresh in-tx read BEFORE
the UPDATE (reading after would see the new value and drop the hop).
Applied to both UpdateItemWithPreCheck and MoveItemWithPreCheck.
2. Backfill stamps historical rows with the item's current collection_id;
reconstructing the collection at each past status change would require
replaying move history. Documented as a best-effort, historical-only
caveat (exact for the common never-moved case; live write/move paths
stamp the collection at transition time).
* feat(store): track collection done-field + seed create-time transitions per Codex review (round 4)
1. Generalize capture from hard-coded "status" to each collection's done
field (DoneFieldKey: status, or BoardGroupBy field like stage/result for
hiring/interviewing). Add a field_key column recording which field the
row tracks (robust to later BoardGroupBy changes). Applied to update,
move, and backfill paths.
2. Seed a create-time "entered initial status" transition on CreateItem and
in the backfill (Pass 2), so an item created directly in a terminal value
still counts as a completion. Initial value reconstructed from the item's
earliest recorded change, else its current value.
Also: item_id FK is ON DELETE CASCADE so hard-deletes clean up transitions.
Tests cover non-status done-field, create-in-terminal, create-seed, and
cascade-on-delete; full store suite green.
35 lines
1.6 KiB
SQL
35 lines
1.6 KiB
SQL
-- Migration 042 (Postgres): structured status-transition log
|
|
-- (PLAN-1628 / TASK-1637).
|
|
--
|
|
-- Postgres counterpart to migrations/063_status_transitions.sql. Same intent:
|
|
-- a structured, queryable record of every item status change — written in the
|
|
-- same transaction as the item update, never debounced — so the Reports
|
|
-- aggregation can compute the completed-throughput and cycle-time series
|
|
-- without parsing the human-readable, debounce-coalesced activities feed.
|
|
--
|
|
-- Timestamps are stored as RFC3339 TEXT to match item_versions and the rest
|
|
-- of the schema (the application layer formats UTC strings via dialect.Now).
|
|
--
|
|
-- Historical rows are backfilled once at startup by
|
|
-- Store.BackfillStatusTransitions (parses activities.metadata.changes).
|
|
|
|
-- field_key records WHICH select field the transition tracks (usually
|
|
-- "status"; a collection may designate another via BoardGroupBy, e.g. hiring
|
|
-- Candidates → "stage"). from_status/to_status hold that field's old/new value.
|
|
CREATE TABLE IF NOT EXISTS status_transitions (
|
|
id TEXT PRIMARY KEY,
|
|
item_id TEXT NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
|
workspace_id TEXT NOT NULL REFERENCES workspaces(id),
|
|
collection_id TEXT NOT NULL,
|
|
field_key TEXT NOT NULL DEFAULT 'status',
|
|
from_status TEXT NOT NULL DEFAULT '',
|
|
to_status TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_status_transitions_ws_time
|
|
ON status_transitions(workspace_id, created_at);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_status_transitions_item
|
|
ON status_transitions(item_id, created_at);
|