Why
docs/design/product.md §6 promises that "I reported this three months ago, who removed it?" always has an answer. It does not: the answer is written and nothing reads it.
The journal table has been there since the first migration (00001_initial_schema.sql), and it is written on every transition (db/queries/intake.sql:80). It carries from_state, to_state, cause, actor_id, actor_kind, the inputs the computation consumed and the rule_version that produced it.
grep -rn "journal" apps/server/db/queries/*.sql returns one INSERT and three comments. No SELECT. No endpoint. The trace exists, is complete, and is unreachable.
Why it is not urgent, and why it is not optional
Nobody misses it in the first weeks: a reviewer looking at a case can see its current state, and the person who changed it is usually in the room. It starts mattering at the horizon the promise names — months — which is exactly when the answer becomes hard to reconstruct any other way.
So this can wait for real use to say what shape it should take. It cannot be dropped: a design that promises an answer and a database that holds it, with nothing between them, is the kind of gap that stays open for a year.
Build
Undecided on purpose — the shape should follow what somebody actually asks for. Two candidates:
GET /projects/{slug}/cases/{caseId}/history, which is what §9 already lists. A case's transitions, in order, each naming its actor and its cause.
- Per comment, since the question in §6 is about a comment rather than a case. The journal keys on
case_id, so answering it per comment means either a join through the case or a column that does not exist yet.
Whichever it is, it reads ReadProject on the project the case belongs to, like everything else (#71), and closed_test.go picks it up without being touched.
What it must not become
Not an audit export. §10 puts that out of scope for v1, and a history endpoint is not a way to smuggle it in.
Not a second source of truth for state. The current state is stored and computed by the server (ADR 0002). The journal says how it got there; it does not get to disagree about where it is.
Validation
- A case that was validated, commented, delivered and accepted answers with those four transitions, in order, each naming who caused it.
actor_kind distinguishes a person from a program, since that is derived from how the caller authenticated and is the reason the journal is worth keeping (ADR 0018).
- Rows written before identity existed still read, naming whatever they named. The journal is append-only and history is not rewritten to look answered.
- A caller with no membership on the project gets the same refusal as on every other read of it.
Why
docs/design/product.md§6 promises that "I reported this three months ago, who removed it?" always has an answer. It does not: the answer is written and nothing reads it.The
journaltable has been there since the first migration (00001_initial_schema.sql), and it is written on every transition (db/queries/intake.sql:80). It carriesfrom_state,to_state,cause,actor_id,actor_kind, theinputsthe computation consumed and therule_versionthat produced it.grep -rn "journal" apps/server/db/queries/*.sqlreturns one INSERT and three comments. No SELECT. No endpoint. The trace exists, is complete, and is unreachable.Why it is not urgent, and why it is not optional
Nobody misses it in the first weeks: a reviewer looking at a case can see its current state, and the person who changed it is usually in the room. It starts mattering at the horizon the promise names — months — which is exactly when the answer becomes hard to reconstruct any other way.
So this can wait for real use to say what shape it should take. It cannot be dropped: a design that promises an answer and a database that holds it, with nothing between them, is the kind of gap that stays open for a year.
Build
Undecided on purpose — the shape should follow what somebody actually asks for. Two candidates:
GET /projects/{slug}/cases/{caseId}/history, which is what §9 already lists. A case's transitions, in order, each naming its actor and its cause.case_id, so answering it per comment means either a join through the case or a column that does not exist yet.Whichever it is, it reads
ReadProjecton the project the case belongs to, like everything else (#71), andclosed_test.gopicks it up without being touched.What it must not become
Not an audit export. §10 puts that out of scope for v1, and a history endpoint is not a way to smuggle it in.
Not a second source of truth for state. The current state is stored and computed by the server (ADR 0002). The journal says how it got there; it does not get to disagree about where it is.
Validation
actor_kinddistinguishes a person from a program, since that is derived from how the caller authenticated and is the reason the journal is worth keeping (ADR 0018).