82 lines
3.6 KiB
Go
82 lines
3.6 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"textmachine/backend/internal/checks"
|
|
"textmachine/backend/internal/membank"
|
|
"textmachine/backend/internal/store"
|
|
)
|
|
|
|
// voicerun.go: the driver half of the pack-19 flaggers — the adapter that turns the bank's chapter
|
|
// projection into the checker's plain-value registry, and the two $0 calls the final wave makes.
|
|
//
|
|
// Both are OBSERVABILITY: neither can flag a chunk, neither touches the wire, and both are recomputed
|
|
// from stored text on every run, so a resume re-derives them without re-billing anything.
|
|
|
|
// voiceRegistry adapts membank's chapter projection to the checker vocabulary. Two packages, two value
|
|
// types, one trivial mapping — which is what keeps checks free of store types and membank free of
|
|
// checker types. Empty when the book has no voice/address content (the flagger is then inert).
|
|
func voiceRegistry(mem *membank.Bank, chapter int) checks.VoiceRegistry {
|
|
if mem == nil {
|
|
return checks.VoiceRegistry{}
|
|
}
|
|
chars, addrs := mem.VoiceProjection(chapter)
|
|
if len(chars) == 0 {
|
|
return checks.VoiceRegistry{}
|
|
}
|
|
reg := checks.VoiceRegistry{Chars: make([]checks.VoiceCharacter, 0, len(chars))}
|
|
for _, c := range chars {
|
|
reg.Chars = append(reg.Chars, checks.VoiceCharacter{
|
|
Key: c.Key, Name: c.Name, Forms: c.Forms, SelfRef: c.SelfRef, NG: c.NG,
|
|
})
|
|
}
|
|
for _, a := range addrs {
|
|
reg.Pairs = append(reg.Pairs, checks.VoiceAddressPair{
|
|
Speaker: a.Speaker, Addressee: a.Addressee, Register: a.Register,
|
|
})
|
|
}
|
|
return reg
|
|
}
|
|
|
|
// runVoiceChecks runs the voice flagger over one unit's shipped text, when the gate is on. A book with
|
|
// the gate off, or with no voice content, gets a zero result and writes zeros — byte-identical to a run
|
|
// before this existed.
|
|
func (r *Runner) runVoiceChecks(mem *membank.Bank, chapter int, source, final string) checks.VoiceResult {
|
|
if !r.Pipeline.Gates.Voice.Enabled || final == "" {
|
|
return checks.VoiceResult{}
|
|
}
|
|
return checks.RunVoiceChecks(source, final, voiceRegistry(mem, chapter), r.checkers)
|
|
}
|
|
|
|
// spoilerLeaks checks the chapter's REJECTED bank rows against the shipped text: a rendering the spoiler
|
|
// window excluded that reached the reader anyway. Unlike the voice flagger it is NOT gated — the spoiler
|
|
// window is a standing safety mechanism (C1), not an opt-in measurement, and a leak is the one thing the
|
|
// window exists to prevent. $0 and observability-only, like every sibling on the retrieval-state row.
|
|
func (r *Runner) spoilerLeaks(mem *membank.Bank, sel membank.Selection, final string) []membank.SpoilerLeak {
|
|
if mem == nil || final == "" || len(sel.Rejected) == 0 {
|
|
return nil
|
|
}
|
|
return mem.SpoilerLeaks(sel.Rejected, final)
|
|
}
|
|
|
|
// setVoiceState writes the two pack-19 flagger results onto a retrieval-state row. Zero results write
|
|
// zeros and an empty detail, so a gate-off book's row is byte-identical to what it was before these
|
|
// columns existed. Shared by the draft-chunk and edit-unit paths so the two cannot drift.
|
|
func setVoiceState(rs *store.RetrievalState, voice checks.VoiceResult, leaks []membank.SpoilerLeak) {
|
|
rs.NVoiceFlags, rs.VoiceDetail = voice.Total(), ""
|
|
// The detail carries the WHOLE result, not only the flagged axes: the denominators (replies,
|
|
// attributed, source replies) are what make the count readable, and the axis-D indicator is reported
|
|
// beside them precisely because it is excluded from the count.
|
|
if voice.Total() > 0 || voice.PairRegister > 0 || voice.Replies > 0 {
|
|
if b, err := json.Marshal(voice); err == nil {
|
|
rs.VoiceDetail = string(b)
|
|
}
|
|
}
|
|
rs.NSpoilerLeaks, rs.SpoilerLeakDetail = len(leaks), ""
|
|
if len(leaks) > 0 {
|
|
if b, err := json.Marshal(leaks); err == nil {
|
|
rs.SpoilerLeakDetail = string(b)
|
|
}
|
|
}
|
|
}
|