package store // bankbasis.go: the settled basis (v19) — what this book has already decided, and the fingerprint of the // evidence each decision was taken on. Dumb storage like stopmemory.go and ruby.go: the fingerprint, the // predicate and every rule about WHEN a row may be served live in the pipeline; this layer writes rows and // reads them back. // // ⚠ IT REPLACES, WHERE ITS NEIGHBOUR ONLY ACCUMULATES. bank_stop_presented has no delete on purpose — // trimming it re-arms a stop the owner has already been shown. This table is the opposite: a term decided // again must overwrite its row, or the predicate keeps comparing against evidence the book has moved past // and serves an answer that was right two purchases ago. Same shape, deliberately different guarantee. // BankBasisRow is one decided surface as the store holds it. type BankBasisRow struct { SrcKey string FPVersion string FPWhole string FPCanon string Dst string Type string Gender string } // BankBasisForBook returns the book's settled basis keyed by the normalized source key. An empty table is // an empty map: nothing has been decided yet, which is the state of every book before its first purchase. func (s *Store) BankBasisForBook(bookID string) (map[string]BankBasisRow, error) { ctx, cancel := opContext() defer cancel() rows, err := s.r.QueryContext(ctx, ` SELECT src_key, fp_version, fp_whole, fp_canon, dst, type, gender FROM bank_basis WHERE book_id = ?`, bookID) if err != nil { return nil, err } defer rows.Close() out := map[string]BankBasisRow{} for rows.Next() { var r BankBasisRow if err := rows.Scan(&r.SrcKey, &r.FPVersion, &r.FPWhole, &r.FPCanon, &r.Dst, &r.Type, &r.Gender); err != nil { return nil, err } out[r.SrcKey] = r } return out, rows.Err() } // PutBankBasis writes the run's decisions, in ONE transaction, replacing whatever the book held for the // same surfaces. // // ⛔ A ROW WITH NO RENDERING IS REFUSED RATHER THAN STORED. The basis exists to hand an answer back, and an // empty one handed back is the silent regression the whole mechanism is built against: the emission would // fall to the raw first-chunk guess at status `auto` and the run would look cheaper while the bank got // poorer. Refusing here means the predicate cannot serve emptiness even if a caller upstream stops // checking — the guarantee lives at the write, where a caller cannot forget it. func (s *Store) PutBankBasis(bookID string, rows []BankBasisRow) error { if len(rows) == 0 { return nil } ctx, cancel := opContext() defer cancel() tx, err := s.w.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() for _, r := range rows { if r.SrcKey == "" || r.Dst == "" || r.FPWhole == "" || r.FPVersion == "" { continue } if _, err := tx.ExecContext(ctx, ` INSERT INTO bank_basis (book_id, src_key, fp_version, fp_whole, fp_canon, dst, type, gender, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(book_id, src_key) DO UPDATE SET fp_version = excluded.fp_version, fp_whole = excluded.fp_whole, fp_canon = excluded.fp_canon, dst = excluded.dst, type = excluded.type, gender = excluded.gender, updated_at = excluded.updated_at`, bookID, r.SrcKey, r.FPVersion, r.FPWhole, r.FPCanon, r.Dst, r.Type, r.Gender); err != nil { return err } } return tx.Commit() }