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}) }