textmachine/backend/internal/membank/problems.go

99 lines
4.7 KiB
Go

package membank
import (
"fmt"
"strings"
)
// problems.go: the seed loader's verdict as a LIST OF SUBJECTS instead of a sentence.
//
// The loader has always accumulated one message per fault and joined them at the end. The join is what
// the decisions door cannot use: it refuses only the faults its own call INTRODUCED and reports the
// rest, and it tells the two apart by comparing the verdict before and after. A verdict that names
// every broken term in ONE string changes entirely when one of them is fixed, so repairing a book one
// term at a time was refused with the text of a term the decision never touched — the door
// contradicting its own promise (ApplyResult.Preexisting) that a book whose files already contradict
// each other stays repairable through it.
//
// The comparison key is the SUBJECT, not the message. For every check in this package the message is
// already per-subject and stable when OTHER subjects are repaired — verified check by check: the
// collision messages name the pair or the shared key and nothing positional, the gender and duplicate
// messages name their own row. The exceptions are the three "record N has no identity at all" messages,
// which have no surface to name and so named a POSITION; a position moves when an earlier record is
// removed, which is exactly the false refusal above. Those three carry an explicit positionless subject
// (addKeyed) while their text keeps the position, which is what an operator needs to find the record.
// Problem is one fault of a seed document: the subject it is ABOUT, and what to tell a human.
type Problem struct {
// Subject identifies what the fault is about. Two problems with the same subject are the same fault
// for the purpose of "did this call introduce it?" — see problems.go.
Subject string
// Text is the human message, the same bytes the loader has always printed.
Text string
}
// SeedProblems is the loader's refusal: the document does not load, and here is every reason.
//
// It is an error whose Error() is byte-identical to the joined message the loader returned before it
// existed, so every caller that only prints is untouched; a caller that has to reason about the faults
// one at a time uses errors.As.
type SeedProblems struct {
Path string
Problems []Problem
}
func (p SeedProblems) Error() string {
texts := make([]string, len(p.Problems))
for i, x := range p.Problems {
texts[i] = x.Text
}
return fmt.Sprintf("glossary seed %s:\n - %s", p.Path, strings.Join(texts, "\n - "))
}
// subjectOf builds a comparison key from a fault's CLASS and the IDENTITY of the term it is about —
// and from nothing else.
//
// ⚠ IT MUST NEVER CONTAIN A FIELD THIS DOOR CAN REWRITE. `dst`, `type`, `status` and `note` are the
// things a decision changes, and three of these checks name the RENDERING in their message because a
// human reading it needs to see it. Keyed on the message, a fault about a term therefore looked BRAND
// NEW the moment the door changed that term's rendering — and the call was refused over a fault it did
// not introduce. Reproduced: a delta row carrying `gender: badvalue` (which no decision can touch —
// this door has no gender channel at all) plus one decision changing that term's `dst` → exit 14,
// `rejected` and `preexisting_problems` holding the SAME fault with two different renderings. The only
// repair left was the text editor this door exists to abolish. ⚠ The shallow fix — decomposing the
// loader's joined verdict — is NOT enough on its own: it stops one level short of this.
//
// The class tag is what keeps two DIFFERENT faults about one term apart, so a new one cannot hide
// behind an old one.
func subjectOf(class, src, sense string, since, until int) string {
return fmt.Sprintf("%s|%s|%s|%d|%d", class, src, sense, since, until)
}
// problemTexts is the human half of a verdict — what the exported checks return and what a report
// prints.
func problemTexts(in []Problem) []string {
if len(in) == 0 {
return nil
}
out := make([]string, len(in))
for i, p := range in {
out[i] = p.Text
}
return out
}
// problemList accumulates the loader's verdict.
type problemList []Problem
// add records a fault whose message is already stable under repairs of other subjects, which is the
// common case: the message names its own row and nothing else.
func (p *problemList) add(text string) {
*p = append(*p, Problem{Subject: text, Text: text})
}
// addKeyed records a fault whose message embeds something that is NOT part of its identity — a
// record's position, a status, a rendering: whatever a human needs to SEE that the identity does not
// contain.
func (p *problemList) addKeyed(subject, text string) {
*p = append(*p, Problem{Subject: subject, Text: text})
}