textmachine/backend/internal/store/bankbasis.go

86 lines
3.4 KiB
Go

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()
}