Files
hyperguild/ingestion/internal/claim/claim_test.go
T
mathias ffceb635ab
CI / Lint / Test / Vet (pull_request) Successful in 40s
CI / Mirror to GitHub (pull_request) Skipped
feat(brain): validate the claim-property envelope on write (brain#21)
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.
2026-08-22 09:39:04 +02:00

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