package seed import ( "bytes" "errors" "fmt" "io" "gopkg.in/yaml.v3" ) // decode.go: the STRICT readers of the two files this schema describes. // // They live with the schema rather than with a loader because the key set IS the format: a key the // struct does not have is a typo, and yaml.v3 DROPS one silently, so `gendr:` for `gender:` reads as // "not set" and the run looks normal (backlog row 212). The same class the strict `book.yaml` decoder // was bought against in pack-16 (config.LoadBook, dec.KnownFields(true)) — and these files are the ones // the owner edits BY HAND most often. Two readers share each of these documents (the memory bank loads // them, the decisions verb rewrites them), so the strictness has to sit where the format does or the // two would answer differently about the same bytes. // DecodeFile parses a seed YAML document. Strict: an unknown key is a loud error naming the line. func DecodeFile(raw []byte) (File, error) { return decodeStrict[File](raw) } // RejectFile is the owner's mined-term reject list: the src surfaces reviewed and DECLINED, which the // bank-mining emission drops — a declined term never re-enters a signature map or the auto-bank. It is a // PROPOSAL filter only — a reject never enters bank content — which is why it is deliberately not folded // into the snapshot. (It does not govern the STOP: that is the presented memory's question, D39.144.) type RejectFile struct { Rejects []Reject `yaml:"rejects"` } // Reject is one declined mined term. Note is the owner's optional reason, ignored by the miner but kept // so a reject list stays self-documenting (six months on, "why was this declined"). type Reject struct { Src string `yaml:"src"` Note string `yaml:"note,omitempty"` } // DecodeRejects parses a mined-rejects YAML document, strict for the same reason DecodeFile is: a // misspelled `rejcts:` used to read as an empty list, silently un-declining every term the owner // declined — they all re-enter the map and the auto-bank as if the review never happened. func DecodeRejects(raw []byte) (RejectFile, error) { return decodeStrict[RejectFile](raw) } // decodeStrict decodes one YAML document with unknown-key rejection. // // A stream carrying NO DOCUMENT decodes to the zero value rather than to an error: yaml.Decoder reports // that as io.EOF, while yaml.Unmarshal — which every caller used before this — accepted it as "no // records". An empty decision file is a legitimate state of both formats, so turning it into a load // failure would break a book over a file that says nothing. // // The EOF is what is tested, not the bytes. The first version pre-checked for whitespace-only content // instead, which is a DIFFERENT set: a file holding only COMMENTS has non-blank bytes and still carries // no document, so it fell through to the decoder and came back `seed: EOF` — a file that loaded fine // before this change, in the format whose own reject entries exist to be self-documenting, and on the // path that governs every book's glossary_seed. Asking the decoder is both simpler and the actual // question. func decodeStrict[T any](raw []byte) (T, error) { var out T dec := yaml.NewDecoder(bytes.NewReader(raw)) dec.KnownFields(true) if err := dec.Decode(&out); err != nil { if errors.Is(err, io.EOF) { var zero T return zero, nil // no document in the stream: empty, or comments only } var zero T return zero, fmt.Errorf("seed: %w", err) } return out, nil }