-- +goose Up -- The reporting database (research/23 §3): state materialized from the engine's event stream and -- from `tmctl status --json`. It is NOT an event store — no history, no replay. Every table below -- is a projection the API reads directly. -- -- Vocabularies are checked in DDL because the PLATFORM owns them: these values are produced by our -- own materializer, so an unknown one is a defect, not data. Engine vocabulary (stage names, flag -- reasons, dispositions) is deliberately absent — it never crosses this seam. create table books ( id text primary key, owner_id text not null references users (id) on delete cascade, title text not null, -- Codes, never names: a language name in a shared layer is pair-specific data (canon §2). source_lang text not null, target_lang text not null, genre text not null default '', status text not null check (status in ( 'uploading', 'parsing', 'not_started', 'translating', 'awaiting_bank', 'finalizing', 'ready', 'stopped', 'rejected', 'failed', 'paused')), chapter_count integer not null default 0, character_count bigint not null default 0, note_count integer not null default 0, added_at timestamptz not null default now(), -- ONE monotonic counter per book. Every book-scoped read and every SSE frame id of that book's -- run carry it, which is what makes "drop a read older than an event already applied" -- decidable at all (K-4 proposal). Bumped once per materializing transaction; the rows that -- transaction touched are stamped with the new value, so a delta read is `revision > $1`. revision bigint not null default 0, -- The engine project directory: book.yaml, the source file and the engine's private SQLite -- live here. The platform never opens that SQLite (D39.85 §4) — it only spawns tmctl with this -- as the working directory. workdir text not null, engine_book_id text not null, source_sha256 bytea, -- The chunker version the persisted manifest was produced under. A change re-numbers chapters -- (engine backlog row 100), and chapter ids must survive that: they are ours, not the engine's. chunker_version text not null default '' ); create index books_owner_idx on books (owner_id, added_at desc, id); create table runs ( id text primary key, book_id text not null references books (id) on delete cascade, -- The book vocabulary minus the values that describe a book without a run. status text not null check (status in ( 'translating', 'awaiting_bank', 'finalizing', 'ready', 'stopped', 'failed', 'paused')), verify_bank boolean not null, started_at timestamptz not null default now(), finished_at timestamptz, -- Progress is per phase because a unit is done only once its edit resolved: one end-to-end -- counter reads zero for the whole draft wave (contract §2.5; engine backlog row 99). draft_done integer not null default 0, draft_total integer not null default 0, edit_done integer not null default 0, edit_total integer not null default 0, eta_seconds integer, revision bigint not null default 0 ); -- One live run per book. The queue serializes by book_id (P-3) and the engine holds an EXCLUSIVE -- lock on the project file anyway, so a second live run is a defect the database should refuse -- rather than a state the API has to explain. create unique index runs_one_live_per_book on runs (book_id) where finished_at is null; create index runs_book_idx on runs (book_id, started_at desc); -- One engine PROCESS per attempt. The ratified idempotency key is (run_id, seq) — where run_id is -- the ENGINE's per-invocation id, not the platform run id: resume spawns a new process whose seq -- restarts at 1, so keying on the platform run would silently drop every event of attempt 2. create table run_attempts ( id bigint generated always as identity primary key, run_id text not null references runs (id) on delete cascade, attempt_no integer not null, -- Echoed by the stream's `hello` frame (engine backlog rows 102/103). Null until the handshake -- arrives: the row is created when the process is spawned, before it says anything. engine_run_id text, started_at timestamptz not null default now(), ended_at timestamptz, exit_code integer, -- A high-water mark instead of an event log: the reporting database materializes state, it -- does not source events (research/23 §3). A duplicate line inside one stream is dropped by -- seq <= last_seq, and the mark moves in the same transaction as the effect it guards. last_seq bigint not null default 0, unique (run_id, attempt_no) ); create unique index run_attempts_engine_run_idx on run_attempts (engine_run_id); create table chapters ( id text primary key, book_id text not null references books (id) on delete cascade, -- Displayed ordinal, NOT a key: numbering is dense, so editing the source shifts every later -- chapter (chunk/chunker.go:99-105). The key is the opaque id, which the platform keeps -- stable across re-chunks. number integer not null, -- Empty when the book's data carries no heading: there is no hardwired "Chapter N" form -- (owner, 04.08 — K-3), and a book without chapter numbers is legal. heading text not null default '', units_total integer not null default 0, -- K-10 is open. Both phase counters exist so that answering it is a projection change rather -- than a migration; units_done stays the aggregate the resync path can restore today. units_draft_done integer not null default 0, units_edit_done integer not null default 0, units_done integer not null default 0, note_count integer not null default 0, revision bigint not null default 0, unique (book_id, number) ); create index chapters_book_order_idx on chapters (book_id, number); -- Delta reads on reconnect: "everything in this book newer than the revision the client holds". create index chapters_book_revision_idx on chapters (book_id, revision); create table units ( id text primary key, chapter_id text not null references chapters (id) on delete cascade, ordinal integer not null, source text not null, target text not null default '', -- Derived from the PAIR, not from the verdict: a flagged unit legally ships WITH text -- (contract §2.7). The two checks below ARE that derivation, so a materializer bug that -- writes "translated with no text" fails here instead of reaching a reader. state text not null check (state in ('translated', 'withheld', 'pending')), revision bigint not null default 0, unique (chapter_id, ordinal), constraint units_translated_has_text check (state <> 'translated' or target <> ''), constraint units_unshipped_is_empty check (state = 'translated' or target = '') ); create index units_chapter_revision_idx on units (chapter_id, revision); create table notes ( id text primary key, book_id text not null references books (id) on delete cascade, chapter_id text references chapters (id) on delete cascade, unit_id text references units (id) on delete cascade, -- The ENGINE's flag reason, stored and never projected. The product phrase and the severity -- step are applied at read time from the contract's map (companion appendix A, K-6), so the -- owner's wording lands as a data change with no backfill — and an unknown reason still gets -- a neutral phrase instead of a hole. reason text not null, created_at timestamptz not null default now(), revision bigint not null default 0 ); create index notes_book_revision_idx on notes (book_id, revision); create index notes_unit_idx on notes (unit_id); create table bank_terms ( id text primary key, book_id text not null references books (id) on delete cascade, src text not null, dst text not null default '', -- Null is legal and means "the engine did not decide": a ruby candidate that is neither a name -- nor a place carries no type (membank/memseed.go:323-326). Such a row still needs signing. kind text check (kind in ('name', 'place', 'title', 'term', 'nickname')), status text not null check (status in ('auto', 'draft', 'approved')), origin text not null check (origin in ('seed', 'ruby', 'mined')), sense text not null default '', since_chapter integer not null default 0, until_chapter integer not null default 0, revision bigint not null default 0, -- The engine's own uniqueness key (store/migrate.go:202). Without the window the same src -- arrives as several legal rows that look like duplicates. unique (book_id, src, sense, since_chapter, until_chapter) ); create index bank_terms_book_revision_idx on bank_terms (book_id, revision); -- Signing is NOT a row edit: the pipeline replaces a book's whole glossary from its deterministic -- inputs each run (seeding.go:18/110), so a decision is an accumulating instruction that the -- worker writes into the mined-delta / mined-rejects files before resuming. create table bank_decisions ( book_id text not null references books (id) on delete cascade, term_id text not null references bank_terms (id) on delete cascade, action text not null check (action in ('promote', 'decline')), dst text not null default '', decided_at timestamptz not null default now(), decided_by text not null references users (id), primary key (book_id, term_id), -- The contract's conditional requirement, enforced where it cannot be generated away: an -- approved term with an empty rendering matches nothing yet reads as an intended one. constraint bank_decisions_promote_has_dst check (action <> 'promote' or dst <> '') ); create table exports ( id text primary key, book_id text not null references books (id) on delete cascade, format text not null, ready boolean not null default false, -- Storage key of the artifact. The download URL is minted per request for the owner and is -- never indexable (PT-34), so it is not a column. artifact text not null default '', created_at timestamptz not null default now(), ready_at timestamptz, failed_reason text not null default '' ); create index exports_book_idx on exports (book_id, created_at desc); -- +goose Down drop table exports; drop table bank_decisions; drop table bank_terms; drop table notes; drop table units; drop table chapters; drop table run_attempts; drop table runs; drop table books;