First half of brain#21: the applicability envelope — half_life (volatile| seasonal|durable), applies (local|homelab|universal), and claim_provenance (asserted|inferred). The strength half (confidence, contested) follows once the ingest-time contradiction detector exists to populate it. writeHallNote already passed unknown frontmatter keys through verbatim, so preservation was free — a test now pins it so a refactor of the merge loop cannot silently drop the fields. What was missing is validation, and it fails closed: - a value outside the enum is rejected. An unvalidated value is worse than an absent one, because a lint skips what it cannot parse and the note then reads as classified while nothing acts on it; - an envelope field without claim_provenance is rejected. A value a model inferred must never be indistinguishable from one Mathias asserted — otherwise a single classifier pass launders machine guesses into apparent human judgement across 1113 notes, which is precisely how a weak claim hardens into accepted fact; - provenance without any envelope field is rejected too: it says who set fields that are not there. Validation runs before MkdirAll, so a rejected write leaves nothing behind — not even an empty hall directory. The field is `applies`, not `scope`, as brain#19 proposed: `scope:` has been in use since brain#5 for glossary terms (homelab-core, project:<repo>), a disjoint value space on the same key. The shipped schema keeps the name. Notes with none of these fields are byte-identical to before. Bulk backfill across the corpus stays out of scope — the same failure mode as the extraction pass that produced 28 wrong wikilinks out of 67. Schema documented in brain/CLAUDE.md (via brain_edit_root, which produced the #97 audit record for the rewrite). Refs brain#21, brain#19 (superseded), brain#5, hyperguild#97.
90 lines
2.9 KiB
Go
90 lines
2.9 KiB
Go
// Package claim_test covers the claim-property envelope (brain#21).
|
|
package claim_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/claim"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestValidateEnvelope(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
fm map[string]string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "absent is fine — the fields are opt-in on new writes",
|
|
fm: map[string]string{"wing": "homelab", "hall": "facts"},
|
|
},
|
|
{
|
|
name: "complete envelope with provenance",
|
|
fm: map[string]string{
|
|
"half_life": "durable", "applies": "universal", "claim_provenance": "asserted",
|
|
},
|
|
},
|
|
{
|
|
name: "inferred provenance is equally valid — it just must be stated",
|
|
fm: map[string]string{
|
|
"half_life": "volatile", "applies": "local", "claim_provenance": "inferred",
|
|
},
|
|
},
|
|
{
|
|
// A value nobody can act on is worse than no value: a lint
|
|
// silently skips it, so the note looks classified and isn't.
|
|
name: "unknown half_life value",
|
|
fm: map[string]string{"half_life": "forever", "claim_provenance": "asserted"},
|
|
wantErr: `half_life: "forever" is not one of volatile, seasonal, durable`,
|
|
},
|
|
{
|
|
name: "unknown applies value",
|
|
fm: map[string]string{"applies": "galaxy", "claim_provenance": "asserted"},
|
|
wantErr: `applies: "galaxy" is not one of local, homelab, universal`,
|
|
},
|
|
{
|
|
// brain#21: a value a model inferred must never be
|
|
// indistinguishable from one Mathias asserted. Writing the field
|
|
// without saying who set it is exactly that laundering.
|
|
name: "envelope field without provenance",
|
|
fm: map[string]string{"half_life": "durable"},
|
|
wantErr: "claim_provenance is required when half_life or applies is set",
|
|
},
|
|
{
|
|
name: "applies without provenance",
|
|
fm: map[string]string{"applies": "homelab"},
|
|
wantErr: "claim_provenance is required when half_life or applies is set",
|
|
},
|
|
{
|
|
name: "unknown provenance value",
|
|
fm: map[string]string{"half_life": "durable", "claim_provenance": "probably"},
|
|
wantErr: `claim_provenance: "probably" is not one of asserted, inferred`,
|
|
},
|
|
{
|
|
// Provenance alone says who set fields that aren't there.
|
|
name: "provenance without any envelope field",
|
|
fm: map[string]string{"claim_provenance": "asserted"},
|
|
wantErr: "claim_provenance is set but neither half_life nor applies is",
|
|
},
|
|
{
|
|
// The glossary wing has used scope: homelab-core since #5. The
|
|
// envelope deliberately does NOT claim that key (brain#21).
|
|
name: "glossary scope is untouched",
|
|
fm: map[string]string{"scope": "homelab-core", "term": "koala"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := claim.ValidateEnvelope(tt.fm)
|
|
if tt.wantErr == "" {
|
|
assert.NoError(t, err)
|
|
return
|
|
}
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
})
|
|
}
|
|
}
|