The labeling doctrine — how labels earn their place here
Written as the resolution of the open discussion pinned in ROADMAP on
2026-07-31 (“labeling as a reachability feature”), and shipped alongside its
first two instances: the golden-values authority tier (this work unit) and
scope-aware candidate generation (the starvation fix). Every future label —
kind, tags, trust classes — is designed against this document instead of
re-deriving it.
The measured pattern, and the rule it implies
Labels used as scopes, filters and exact keys have all won here: wing
scoping, content_date, declared language, the poison-positive date-filter
design. Labels used as score modifiers have all lost: RRF −7.3pp,
room_cap −5.6pp, per-query channel rescaling −9.4pp (full rows in
ROADMAP’s failed table). The rule:
A label may decide who competes. It may never adjust how they score.
Within one query the order is fixed: the label filter constrains the candidate set, then the existing calibrated fusion ranks within it, untouched. “Filter after ranking” is refused — it spends the candidate pool on rows the caller excluded, which is the starvation defect restated.
Filters are not free: the starvation obligation
A filter combined with a prefilter inherits the scoped-starvation shape
(the corpus-wide top-k can exclude the scope entirely while the scope holds
the answer — pinned by test for wings, then found live in room). Any
label offered as a search filter MUST ride the scope-aware candidate
generation built for the fix: population resolved first through an index,
small scopes scanned exactly, large scopes membership-filtered with the
pool scaled to the scope’s own population. A new filterable label is
therefore an index + a scope-resolution entry, never a bare SQL WHERE.
Two mechanisms are exempt because no candidate pool exists on their path:
exact keys (the fp blind index; lookup_canonical) — immune to every
crowding and starvation shape by construction — and full scans.
A filter must also declare its unlabeled-rows policy. A query filtering
on a label most rows never carried returns near-nothing, silently — the
“silence” the never-guess doctrine forbids. The honest surface reports what
the filter excluded (a count is enough), or the label’s design states that
absence is meaningful (as with wing/room, which every drawer carries).
That obligation now has two instances and one implementation. kind
reports the in-scope drawers carrying no declared kind; min_trust
reports the wings below the floor; both report None rather than zero
when the caller set no filter, because “you set no floor” and “your floor
excluded nothing” are different statements. The implementation is shared
(Exclusions in the CLI crate, consumed by CLI, MCP and /v1 alike)
because it was written twice and each copy dropped one leg — /v1 and
the CLI disclosed the trust count and MCP did not. A policy this document
states once must be implemented once too, or the surfaces will disagree
about it.
Cost is not trust: the two axes
- Cost tiers, now measured (
undercroft-bench tagcost, LoCoMo corpus, 2026-08-02): declared-by-caller ≈ zero (a field on the write); rule-derived = 0.38 µs/drawer — 0.4 s per million (and read-live variants are free at write, which is also what makes scanner fixes retroactive); model-derived = 0.19 s/drawer on a served 1B CPU model — 2.2 days per million, ~5·10⁵× the rule arm — if it ever exists it is asynchronous enrichment after the verbatim write, never a write gate (write gating measured −27.7pp here, mem0’s rubric). - Trust tiers are orthogonal. A declared label is cheap but is still
only a claim by its declarer. Self-scoping needs no trust: a caller
filtering their own queries by their own labels harms only themselves.
A label that outranks other evidence needs review — which is exactly
what
review_stateon the authority tier is. A self-declared label is never a trust boundary: poison declareskind=decisionas easily as anything else. Trust labeling belongs to deployment-assigned facts (which wing, which source, at ingest) — controlled by the principal, not by the content’s author. - Model-assigned labels are extractor claims: they require extractor identity and receipts (the KG’s receipt pattern, one level up) before any surface may filter on them, and they may never feed a hard filter while unreviewed — extractor error would silently unreach content. The precondition SHIPPED: a KG fact records which model claimed it, and that identity lives inside the fact’s own HMAC (a third canonical extension on the support/authority precedent, so untouched facts keep byte-identical canonicals), which means a flipped attribution fails verification rather than laundering a claim onto a better-trusted extractor. No surface filters on it yet; the requirement above is what a first one must satisfy.
- A label crossing a trust boundary in transit is still only a claim by its sender. A signed export bundle’s manifest carries a sender-declared trust class beside the Ed25519 attestation: the signature proves who wrote it, never what it deserves. The receiving deployment’s own operator assigns trust on arrival, exactly as at ingest — the same rule as below, one machine further away.
The exposure rule on sealed vaults
A filterable label must be SQL-reachable, which on a sealed vault means one of exactly two shapes:
-
Closed-vocabulary enum in the clear — a deliberate, low-entropy, inventoried leak (the
wing/roomprecedent; the metadata-exposure and footprint tests fail until it is accounted for). -
Keyed blind index — truncated HMAC, the shape
fingerprint()uses: SQL equality with zero leak, no prefix/LIKE/range.Copy the shape, NOT the key.
fingerprint()is keyed with the vault’s rotatable MAC key, which is correct for what it is — a dedup LOOKUP key that rotation recomputes and nothing holds a reference to. A blind index is not that: re-keying one means re-indexing the corpus, and A10 unit 1 shipped a first version keyed withVault::tagthat would have moved every fact id on every rotation. Use a per-vault secret stored sealed inmeta, which rotation re-seals and never regenerates (kg.rs::kg_secret), and see the CLAUDE.md invariant an identifier is never derived from rotatable key material; neither is a blind-index key. Two riders that unit paid for: any UNKEYED digest of the same value elsewhere (an id, a fingerprint) is a confirmation oracle that blinding the column does not close, andaudit.record_idcarries these values in clear too — it holdstrust/{wing}andretention/{wing}today.
Free-form clear-text labels on sealed vaults are not offerable: a tag
like password-rotation-policy copies content-derived words into unsealed
metadata, which the verbatim-sealing invariant forbids. canonical_key
ships in the clear under rule 1’s spirit — it is queryable structure like
subject/predicate (the trade the KG header records) and must be named like
an identifier, never with content words that should stay sealed.
The authority tier, as the doctrine’s first instance
authority_class + review_state + canonical_key on KG facts
(consultation adopted item 1) instantiate every rule above:
- All three are declared (closed vocabulary, validated, audited through the chain) and HMAC-covered — a column flip without the vault key fails verification, so poison cannot approve itself.
lookup_canonicalis the exact-key door: an indexed SQL equality, answered before semantic recall for exact or high-risk asks, returning at most one active approved fact per key or nothing — declared truth outranking learned similarity, and never a guess. Promotion onto an occupied key supersedes the previous holder (audited); history keeps the closed fact.- The tier changes no score anywhere: it is a door beside retrieval, not a weight inside it.
What shipped, and what still waits
kindon drawers (consultation item 4) SHIPPED 2026-08-02, exactly as this document fixed it: declared closed vocabulary (undercroft_core::KIND_VOCAB, validated at the single write choke point, rejected never coerced), a clear-text inventoried column (exposure + footprint tests updated, both directions), the filter riding the gate-verified scope machinery (kind-starvation test with a raw premise), an unknown filter value erroring instead of silently emptying, and the unlabeled-rows count on/v1(unlabeled_excluded, besidetrust_excluded_wings), MCP and CLI. Its value instrument (undercroft-bench tagvalue) shipped with it: R@1/R@5 + wrong-kind@1, unfiltered vs filtered, on a corpus built so every key’s words live in two kinds — the number beside any claim the filter makes.- Trust labels (ingest-time, deployment-assigned) SHIPPED 2026-08-03
with the C3.3 defense cluster, on wing-as-trust-zone as designed and
obeying every rule above.
TRUST_VOCABis a closed vocabulary (quarantined | standard | trusted) assigned by the operator only — CLI and/v1, deliberately never MCP, because the surface an agent drives must not set the class that decides what it may retrieve. The assignment is HMAC-tagged and chain-audited, so a column flip without the vault key fails verification and a floored search refuses rather than quietly ranking on a forged class. It is consumed as a candidate-set floor (min_trustper request,UNDERCROFT_TRUST_FLOORper vault) resolved through the scope machinery before candidates are drawn — never a weight — so a quarantined wing can neither answer nor crowd a floored query, pinned by a starvation test with a raw premise. Unassigned meansstandard; naming a wing explicitly is self-scoping and bypasses the vault floor, never a request’s ownmin_trust. The same clause reaches the remote-index path from the one shared policy function, so an attached backend is not a route around it. A self-declaredkindremains ergonomics, never a trust boundary. - The quarantine wing is this doctrine’s hardest instance: a reserved
clear-text wing value that hard-excludes from every read returning
content unless the caller names it, and is refused outright on MCP.
Note what makes that legitimate rather than a silent filter — it is
operator-declared (
UNDERCROFT_ADMISSION), the write that lands there says so on every surface, and the review queue is an operator surface with its own scope. An exclusion nobody can see or opt into would be exactly the silence this document forbids. - Free-form tags wait for a product case, and ship blind-indexed if ever.