106 lines
4.1 KiB
Go
106 lines
4.1 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
|
|
"textmachine/backend/internal/membank"
|
|
"textmachine/backend/internal/store"
|
|
)
|
|
|
|
// waveselection.go: the record of WHAT EACH WAVE WAS SHOWN (backlog row 417, schema v17).
|
|
//
|
|
// It exists because the two waves see different banks and the old table cannot hold both. The draft wave
|
|
// selects over the BASE bank (mined rows excluded, so the first run of a book drafts with no mined
|
|
// terminology at all); the editor selects over the ENRICHED one. retrieval_state has one row per chunk, so
|
|
// the unit merge writes the editor's post-check onto the leader's DRAFT row and the editor's own injection
|
|
// counts never land anywhere — and its EVICTIONS landed nowhere at all, which chunkrun.go says in as many
|
|
// words. The consequence, measured on the paid run of 11.09: four terms shipped in a rendering the bank did
|
|
// not hold, and whether the bank had even reached the editor's prompt was not answerable from the database.
|
|
//
|
|
// Recomputed every run and idempotent per (position, wave), so a resume converges instead of accumulating.
|
|
// Observability only: nothing here is read on the money path and nothing here changes a request.
|
|
|
|
// recordWaveSelection persists one wave's selection at one position. The counts are derived from the SAME
|
|
// Selection the wave actually injected — not re-selected here — so the record cannot describe a different
|
|
// selection from the one that went to the model.
|
|
func (r *Runner) recordWaveSelection(snapID, wave string, chapter, chunkIdx int, sel membank.Selection) error {
|
|
ws := store.WaveSelection{
|
|
BookID: r.Book.BookID, Chapter: chapter, ChunkIdx: chunkIdx, Wave: wave, SnapshotID: snapID,
|
|
NSpoilerBlocked: len(sel.Rejected),
|
|
NEvicted: len(sel.Evicted),
|
|
InjectedSrcs: srcsJSON(sel.Injected),
|
|
EvictedSrcs: srcsJSON(sel.Evicted),
|
|
}
|
|
for _, p := range sel.Injected {
|
|
if p.Sticky {
|
|
ws.NSticky++
|
|
} else {
|
|
ws.NExactHits++
|
|
}
|
|
if p.Disp == membank.Ambiguous {
|
|
ws.NAmbiguousFlagged++
|
|
}
|
|
}
|
|
return r.Store.UpsertWaveSelection(ws)
|
|
}
|
|
|
|
// srcsJSON renders the source surfaces of a set of picked records, sorted and deduped, as a JSON array.
|
|
// Sorted because the column is compared across runs and across waves, and selection order is a priority
|
|
// order that a budget change would reshuffle without any row having moved; "[]" rather than "null" for the
|
|
// empty case, so a position with nothing shown reads as a list nobody filled rather than a missing field.
|
|
func srcsJSON(ps []membank.PickedEntry) string {
|
|
seen := map[string]bool{}
|
|
out := []string{}
|
|
for _, p := range ps {
|
|
if src := p.Src(); src != "" && !seen[src] {
|
|
seen[src] = true
|
|
out = append(out, src)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
b, err := json.Marshal(out)
|
|
if err != nil {
|
|
return "[]"
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// WaveInjection is one wave's book-wide injection totals — the summary that answers "did the bank reach
|
|
// this wave at all", which is the question the paid run of 11.09 could not put to the database.
|
|
type WaveInjection struct {
|
|
Wave string `json:"wave"`
|
|
Positions int `json:"positions"`
|
|
ExactHits int `json:"exact_hits"`
|
|
Sticky int `json:"sticky"`
|
|
Ambiguous int `json:"ambiguous"`
|
|
Blocked int `json:"spoiler_blocked"`
|
|
Evicted int `json:"evicted"`
|
|
}
|
|
|
|
// waveInjections folds the per-position records into one row per wave, in a fixed wave order so the report
|
|
// is stable. A wave with no rows is omitted rather than printed as zeroes: "this wave injected nothing" and
|
|
// "this run predates the table" are different facts, and only the first one deserves a line.
|
|
func waveInjections(rows []store.WaveSelection) []WaveInjection {
|
|
byWave := map[string]*WaveInjection{}
|
|
for _, w := range rows {
|
|
a := byWave[w.Wave]
|
|
if a == nil {
|
|
a = &WaveInjection{Wave: w.Wave}
|
|
byWave[w.Wave] = a
|
|
}
|
|
a.Positions++
|
|
a.ExactHits += w.NExactHits
|
|
a.Sticky += w.NSticky
|
|
a.Ambiguous += w.NAmbiguousFlagged
|
|
a.Blocked += w.NSpoilerBlocked
|
|
a.Evicted += w.NEvicted
|
|
}
|
|
var out []WaveInjection
|
|
for _, name := range []string{store.WaveDraft, store.WaveEdit} {
|
|
if a := byWave[name]; a != nil {
|
|
out = append(out, *a)
|
|
}
|
|
}
|
|
return out
|
|
}
|