// Package claim validates the claim-property frontmatter that says when and // where a note's claim holds, and who decided that (brain#21). // // Two families were designed together and land separately. This is the // applicability envelope: // // half_life: volatile | seasonal | durable — how fast it decays // applies: local | homelab | universal — how widely it holds // claim_provenance: asserted | inferred — who set the above // // The strength family (confidence, contested) follows once the ingest-time // contradiction detector exists to populate it. // // Why the envelope is validated rather than merely accepted: a value outside // the enum is worse than an absent one. A lint skips what it cannot parse, so // the note reads as classified while nothing acts on it — the field becomes // decoration that suppresses the very review it was added to trigger. // // Why `applies` and not `scope`: `scope:` has been in use since brain#5 for // glossary terms (`homelab-core`, `project:`) — a disjoint value space // on the same key. The shipped schema keeps the name; this one takes another. package claim import ( "fmt" "strings" ) // Field names, exported so writers and lints agree on the spelling. const ( FieldHalfLife = "half_life" FieldApplies = "applies" FieldProvenance = "claim_provenance" ) var ( halfLives = []string{"volatile", "seasonal", "durable"} appliesTo = []string{"local", "homelab", "universal"} provenances = []string{"asserted", "inferred"} ) // ValidateEnvelope checks the envelope fields of one note's frontmatter. // // Absent fields are valid: the envelope is opt-in for new writes, and a // corpus-wide backfill is deliberately out of scope (brain#21) — inferring // 1113 notes' properties in bulk has the same failure mode as the extraction // pass that produced 28 wrong wikilinks out of 67. func ValidateEnvelope(fm map[string]string) error { halfLife, hasHalfLife := present(fm, FieldHalfLife) applies, hasApplies := present(fm, FieldApplies) prov, hasProv := present(fm, FieldProvenance) if hasHalfLife { if err := oneOf(FieldHalfLife, halfLife, halfLives); err != nil { return err } } if hasApplies { if err := oneOf(FieldApplies, applies, appliesTo); err != nil { return err } } if hasProv { if err := oneOf(FieldProvenance, prov, provenances); err != nil { return err } } // The field and its provenance travel together or not at all. A value // whose author is unknown cannot be trusted differently from one Mathias // asserted, which is how an inferred guess hardens into accepted fact. switch { case (hasHalfLife || hasApplies) && !hasProv: return fmt.Errorf("%s is required when %s or %s is set", FieldProvenance, FieldHalfLife, FieldApplies) case hasProv && !hasHalfLife && !hasApplies: return fmt.Errorf("%s is set but neither %s nor %s is", FieldProvenance, FieldHalfLife, FieldApplies) } return nil } // present reports a trimmed field value and whether it was set to anything // non-empty. A key written with an empty value is treated as absent rather // than as an error: that is a caller emitting a placeholder, not asserting. func present(fm map[string]string, key string) (string, bool) { v := strings.TrimSpace(fm[key]) return v, v != "" } func oneOf(field, value string, allowed []string) error { for _, a := range allowed { if a == value { return nil } } return fmt.Errorf("%s: %q is not one of %s", field, value, strings.Join(allowed, ", ")) }