feat(brain): validate the claim-property envelope on write (brain#21) #102

Merged
mathias merged 1 commits from agent/brain21-claim-envelope into main 2026-08-22 08:58:34 +00:00
Owner

First half of brain#21 — the applicability envelope. Sprint B item 1 in the re-sequenced order, because this half improves a check that already runs rather than waiting on anything.

half_life:        volatile | seasonal | durable    # how fast the claim decays
applies:          local | homelab | universal      # how widely it holds
claim_provenance: asserted | inferred              # who set the two above

What was actually missing

writeHallNote already passes unknown frontmatter keys through verbatim, so preservation was free — I found that before writing any code. A test now pins it so a future refactor of the merge loop cannot silently drop the fields.

What was missing is validation, and it fails closed:

Rejected Why
value outside the enum Worse than an absent value — a lint skips what it cannot parse, so the note reads as classified while nothing acts on it
envelope field without claim_provenance A value a model inferred must never look like one you asserted. Without this, one classifier pass launders guesses into apparent human judgement across 1113 notes — the exact failure the field exists to prevent
claim_provenance with no envelope field It says who set fields that aren't there

Validation runs before MkdirAll, so a rejected write leaves nothing behind — not even an empty hall directory. A note with none of these fields is byte-identical to before.

applies, not scope

brain#19 proposed scope. Measured against the live corpus first: scope: is already in use by 10 notes, all glossary terms on brain#5's schema (homelab-core, project:jepa-fx-risk) — same key, disjoint value space. The shipped schema keeps the name; the unshipped proposal takes another. Free now, expensive after any backfill. brain#19 is closed as superseded by brain#21 with this recorded.

Tests

Red-first, in both places. The unit table failed on the four reject cases against a stub; the two write-path reject tests failed against the real handler:

--- FAIL: TestWriteNote_RejectsInvalidClaimEnvelopeAndWritesNothing
--- FAIL: TestWriteNote_RejectsClaimFieldWithoutProvenance

The preservation test passed from the start — it pins existing behaviour, and I've said so in its comment rather than letting it look like new work. One case asserts the glossary's scope: homelab-core is untouched by any of this.

task check exit 0.

Documentation shipped with it

brain/CLAUDE.md gained a Claim properties section — the enums, why applies isn't scope, and what asserted vs inferred is protecting against. Written through brain_edit_root, which produced the audit record #97 added:

actor: brain_edit_root | items: ['CLAUDE.md']
sec: governance file rewritten: CLAUDE.md before=6a8b96f8… after=0ec99b7f…

That is #97's first use on a real governance change rather than a test edit. The ntfy half of #97 is still dark — see #100.

Deliberately not in this PR

  • The strength half (confidence, contested) — it wants the contradiction detector first, or contested has nothing to point at.
  • The lint that acts on these fields. brain#21 requires it to declare an expected finding count before merging, because the audit report already carries 1668 findings and a loosely-calibrated check would bury it. That belongs with the strength half.
  • Any backfill. New writes only.

Risk: LOW — additive and opt-in. The one behaviour change is that a previously-silent invalid value now fails the write; nothing in the corpus uses these keys today, so no existing caller is affected.

First half of brain#21 — the applicability envelope. Sprint B item 1 in the re-sequenced order, because this half improves a check that already runs rather than waiting on anything. ```yaml half_life: volatile | seasonal | durable # how fast the claim decays applies: local | homelab | universal # how widely it holds claim_provenance: asserted | inferred # who set the two above ``` ## What was actually missing `writeHallNote` already passes unknown frontmatter keys through verbatim, so **preservation was free** — I found that before writing any code. A test now pins it so a future refactor of the merge loop cannot silently drop the fields. What was missing is **validation**, and it fails closed: | Rejected | Why | |---|---| | value outside the enum | Worse than an absent value — a lint skips what it cannot parse, so the note reads as classified while nothing acts on it | | envelope field without `claim_provenance` | A value a model inferred must never look like one you asserted. Without this, one classifier pass launders guesses into apparent human judgement across 1113 notes — the exact failure the field exists to prevent | | `claim_provenance` with no envelope field | It says who set fields that aren't there | Validation runs **before** `MkdirAll`, so a rejected write leaves nothing behind — not even an empty hall directory. A note with none of these fields is byte-identical to before. ## `applies`, not `scope` brain#19 proposed `scope`. Measured against the live corpus first: `scope:` is already in use by 10 notes, all glossary terms on brain#5's schema (`homelab-core`, `project:jepa-fx-risk`) — same key, disjoint value space. The shipped schema keeps the name; the unshipped proposal takes another. Free now, expensive after any backfill. brain#19 is closed as superseded by brain#21 with this recorded. ## Tests Red-first, in both places. The unit table failed on the four reject cases against a stub; the two write-path reject tests failed against the real handler: ``` --- FAIL: TestWriteNote_RejectsInvalidClaimEnvelopeAndWritesNothing --- FAIL: TestWriteNote_RejectsClaimFieldWithoutProvenance ``` The preservation test passed from the start — it pins existing behaviour, and I've said so in its comment rather than letting it look like new work. One case asserts the glossary's `scope: homelab-core` is untouched by any of this. `task check` exit 0. ## Documentation shipped with it `brain/CLAUDE.md` gained a **Claim properties** section — the enums, why `applies` isn't `scope`, and what `asserted` vs `inferred` is protecting against. Written through `brain_edit_root`, which produced the audit record #97 added: ``` actor: brain_edit_root | items: ['CLAUDE.md'] sec: governance file rewritten: CLAUDE.md before=6a8b96f8… after=0ec99b7f… ``` That is #97's first use on a real governance change rather than a test edit. The ntfy half of #97 is still dark — see #100. ## Deliberately not in this PR - **The strength half** (`confidence`, `contested`) — it wants the contradiction detector first, or `contested` has nothing to point at. - **The lint that acts on these fields.** brain#21 requires it to declare an expected finding count before merging, because the audit report already carries 1668 findings and a loosely-calibrated check would bury it. That belongs with the strength half. - **Any backfill.** New writes only. **Risk:** LOW — additive and opt-in. The one behaviour change is that a previously-silent invalid value now fails the write; nothing in the corpus uses these keys today, so no existing caller is affected.
mathias added 1 commit 2026-08-22 07:39:32 +00:00
feat(brain): validate the claim-property envelope on write (brain#21)
CI / Lint / Test / Vet (pull_request) Successful in 40s
CI / Mirror to GitHub (pull_request) Skipped
ffceb635ab
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.
mathias merged commit 54237af7ad into main 2026-08-22 08:58:34 +00:00
Sign in to join this conversation.