package store import ( "context" "database/sql" "errors" ) // jobs.go: snapshots + durable jobs. Джоба = глава×стадия; сохранность — чанк // (checkpoints, см. ledger.go). Snapshot материализует контекст джобы, чтобы // request-hash был детерминирован на всю её жизнь (§3.1–3.2). // UpsertSnapshot stores a materialized job-context snapshot (idempotent: the // snapshot_id is itself a content hash, so re-inserting the same id is a // no-op). func (s *Store) UpsertSnapshot(snapshotID, briefHash, payloadJSON string) error { ctx, cancel := opContext() defer cancel() _, err := s.w.ExecContext(ctx, ` INSERT INTO snapshots (snapshot_id, brief_hash, payload) VALUES (?, ?, ?) ON CONFLICT (snapshot_id) DO NOTHING`, snapshotID, briefHash, payloadJSON) return err } // Job is one chapter×stage unit of work. type Job struct { ID int64 BookID string Chapter int Stage string Status string SnapshotID string } // EnsureJob returns the existing job for (book, chapter, stage) or creates it // bound to snapshotID. An existing job KEEPS its original snapshot — Р6: из // ключа resume исключаются волатильные части, подтверждение термина между // запусками не должно инвалидировать уже переведённые чанки. func (s *Store) EnsureJob(bookID string, chapter int, stage, snapshotID string) (*Job, error) { ctx, cancel := opContext() defer cancel() if _, err := s.w.ExecContext(ctx, ` INSERT INTO jobs (book_id, chapter, stage, snapshot_id) VALUES (?, ?, ?, ?) ON CONFLICT (book_id, chapter, stage) DO NOTHING`, bookID, chapter, stage, snapshotID); err != nil { return nil, err } return s.getJob(ctx, bookID, chapter, stage) } func (s *Store) getJob(ctx context.Context, bookID string, chapter int, stage string) (*Job, error) { j := Job{BookID: bookID, Chapter: chapter, Stage: stage} err := s.r.QueryRowContext(ctx, `SELECT id, status, snapshot_id FROM jobs WHERE book_id = ? AND chapter = ? AND stage = ?`, bookID, chapter, stage).Scan(&j.ID, &j.Status, &j.SnapshotID) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return &j, nil } // SetJobStatus transitions a job. func (s *Store) SetJobStatus(jobID int64, status string) error { ctx, cancel := opContext() defer cancel() _, err := s.w.ExecContext(ctx, `UPDATE jobs SET status = ?, updated_at = datetime('now') WHERE id = ?`, status, jobID) return err } // UpdateJobSnapshot re-pins a job to a new snapshot — ЯВНАЯ команда оператора // (tmctl --resnapshot): пере-перевод уже оплаченных чанков случается только // осознанно, никогда как тихий побочный эффект правки конфига (Р6). func (s *Store) UpdateJobSnapshot(jobID int64, snapshotID string) error { ctx, cancel := opContext() defer cancel() _, err := s.w.ExecContext(ctx, `UPDATE jobs SET snapshot_id = ?, updated_at = datetime('now') WHERE id = ?`, snapshotID, jobID) return err }