Every ticket as the root of its provenance graph — goal, patch, approval, runs and (as the ticket-class build lands) mandate, manifestations, guarantees, journey, artifacts and children. Rendered through the <<<TicketGraphList>>> tag.
goal
Extend the Go/PHP route equivalence harness to cover the 24 WRITE (POST) routes. Today they are ALL
skipped, so the governance spine has ZERO equivalence proof between the two engines.
THE GAP (measured, re-runnable):
cd congruency-go && go test . -run TestRouteEquivalence -v
-> "skipped write (POST) routes (24): agent.register, artifact.record, authorize_process,
binding.record, bug.record, doc.record, edge.record, guarantee.record, job.put,
journey.record, mandate.record, policy.create, process_status, run.record, search.record,
source.record, telemetry.record, ticket.approve, ticket.assign, ticket.create, ticket.kind,
ticket.lesson, ticket.progress, work.record"
The GET routes all PASS byte-identical. The writes are simply not checked - verifying them would
mutate the live db, so the harness declines. That is an honest skip, not a bug, but it leaves the
MOST DANGEROUS routes unverified: process_status, ticket.create, ticket.assign, edge.record.
WHY IT MATTERS - THIS CLASS ALREADY BIT US. #279 shipped a pair_loop change where process_status set
ACTIVE *before* ticket.assign. Nothing verified that write path. It merged, and EVERY new dispatch
died with HTTP 500 ("a ticket authorizes an agent: assign one before it can go ACTIVE") until it was
reverted. An unverified write path is exactly how that happened, and the Go port could diverge from
PHP on any of these 24 with nothing to notice.
THE PATTERN ALREADY EXISTS - DO NOT INVENT ONE. per_unit_test_harness.py already "copies it to a
throwaway scratch db under" a temp dir and drives the target against that. Reuse that discipline.
DELIVERABLE: congruency-go/equivalence_test.go (extend it; do not fork a second harness).
1. THROWAWAY DB, NEVER THE LIVE ONE. Per POST route: copy the state db to a temp scratch file, point
BOTH engines at the copy, run the route once against each (two independent copies - PHP gets one,
Go gets one, from the SAME pristine snapshot so neither sees the other's writes).
2. EQUIVALENCE FOR A WRITE IS TWO THINGS, NOT ONE:
(a) the RESPONSE bodies must match byte-identically (as the GET tests already assert), AND
(b) the RESULTING DB STATE must match. Compare the post-state of the touched table(s) - the
affected rows, not the whole db (ts/rowid/autoincrement will legitimately differ). Normalize
or exclude volatile columns (strftime('%s','now') timestamps, AUTOINCREMENT ids) and SAY IN
THE TEST which columns were excluded and why. A write route that returns identical JSON but
writes different rows is exactly the #279 failure and MUST fail this test.
3. ROUTES ARE DATA - both engines read the SAME `api_routes` row (62 rows; boot/rest.php and
congruency-go/db.go are two engines over one table). So the test is: same row + same bound params
-> same response + same resulting rows. Drive every route from its row, not from hand-written
per-route code.
4. INPUTS. Each POST route needs valid bound params. Derive them from the route's declared `params`
column where possible; where a route needs a precondition (process_status ACTIVE requires a prior
ticket.assign - the #279 lesson), set that precondition up IN THE SCRATCH DB and say so.
5. NO SILENT SKIPS. If a route genuinely cannot be driven (needs a live agent, external side effect),
it may be skipped ONLY with a NAMED reason printed in the test output. The current blanket
"skipped write (POST) routes (24)" is what hid this for so long. Zero unnamed skips.
6. READ-ONLY TOWARD THE WORLD: no writes to the live state db, no network, no agents spawned. Assert
it: capture the live db's mtime+size before and after and fail if either changed.
ACCEPTANCE: `go test . -run TestRouteEquivalence` still green; all 24 POST routes are either PROVEN
(response + resulting rows identical Go vs PHP) or skipped with a NAMED reason; process_status,
ticket.create, ticket.assign and edge.record are among the PROVEN set (they are the spine); the live
state db is byte-unchanged by the run; the volatile-column exclusions are enumerated in the test.
NOTE FOR THE FILER (not the worker): once these proofs exist they are the natural first rows for the
`guarantees` table (ticket_id, clause_seq, digest) + guarantee.held, which is BUILT and has 0 rows.
That wiring is deliberately OUT OF SCOPE here - this ticket delivers the proofs, not the ledger.
RUN ON CLAUDE.