package store import ( "context" "database/sql" "errors" "fmt" "strings" "time" ) // ErrNoSuchTrace — the trace the correction names is gone or never existed. // Held apart from a write failure because it is the expected outcome of // correcting a turn older than the 14-day bound, and the surface should say that // rather than report a broken database. var ErrNoSuchTrace = errors.New("no such routing trace") // RoutingLabel is one correction: what he said, what she made of it, and what it // should have been. It is the only supervised signal in the box, so it outlives // the trace it came from (V-630, docs/plans/22-correcting-a-turn.md). type RoutingLabel struct { ID int64 `json:"id"` Ts time.Time `json:"ts"` Utterance string `json:"utterance"` // Was is the intent the cascade chose. Kept beside the target because the // pair is what names the confusion, and a label with no "was" cannot say // which boundary moved. Was string `json:"was"` // ShouldBe is the owner's target, and may be empty. "That was wrong, I am // not going to tell you what it was" is a usable negative, and requiring the // target would cost the cheap half of the gesture. ShouldBe string `json:"should_be"` Source string `json:"source"` EncoderID string `json:"encoder_id"` } // CorrectTurn records the owner's correction of one persisted turn. It promotes // the pair into routing_labels and stamps the trace, both in one transaction: // a stamped trace with no label would lose the signal when the trace expires, // and a label with no stamp would let the same turn be corrected twice. // // shouldBe empty is allowed and means "wrong, target unstated". func (s *Store) CorrectTurn(ctx context.Context, traceID int64, shouldBe string, now time.Time) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("correct turn: begin: %w", err) } defer func() { _ = tx.Rollback() }() var utterance, was, source, encoderID string err = tx.QueryRowContext(ctx, ` SELECT utterance, intent, source, encoder_id FROM routing_traces WHERE id = ?`, traceID).Scan(&utterance, &was, &source, &encoderID) if errors.Is(err, sql.ErrNoRows) { return ErrNoSuchTrace } if err != nil { return fmt.Errorf("correct turn: read trace: %w", err) } shouldBe = strings.TrimSpace(shouldBe) if _, err := tx.ExecContext(ctx, ` INSERT INTO routing_labels (ts, utterance, was, should_be, source, encoder_id) VALUES (?,?,?,?,?,?) ON CONFLICT(utterance) DO UPDATE SET ts = excluded.ts, was = excluded.was, should_be = excluded.should_be, source = excluded.source, encoder_id = excluded.encoder_id`, now.UnixMilli(), utterance, was, shouldBe, source, encoderID); err != nil { return fmt.Errorf("correct turn: write label: %w", err) } // The stamp is what the trace itself carries: "corrected", or the target he // gave. It expires with the trace, and that is fine — the label above is the // durable half. stamp := shouldBe if stamp == "" { stamp = "wrong" } if _, err := tx.ExecContext(ctx, `UPDATE routing_traces SET correction = ? WHERE id = ?`, stamp, traceID); err != nil { return fmt.Errorf("correct turn: stamp trace: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("correct turn: commit: %w", err) } return nil } // RoutingLabels returns the newest n corrections, newest first. Nothing prunes // them: 31 modes and 9 of them with no example at all is the problem this table // exists to solve, and a label is a few dozen bytes. func (s *Store) RoutingLabels(ctx context.Context, n int) ([]RoutingLabel, error) { rows, err := s.db.QueryContext(ctx, ` SELECT id, ts, utterance, was, should_be, source, encoder_id FROM routing_labels ORDER BY id DESC LIMIT ?`, n) if err != nil { return nil, fmt.Errorf("routing labels: %w", err) } defer rows.Close() var out []RoutingLabel for rows.Next() { var l RoutingLabel var tsMilli int64 if err := rows.Scan(&l.ID, &tsMilli, &l.Utterance, &l.Was, &l.ShouldBe, &l.Source, &l.EncoderID); err != nil { return nil, err } l.Ts = time.UnixMilli(tsMilli).UTC() out = append(out, l) } return out, rows.Err() }