Pattern · Trust

Trust-State Machine

Model memory as candidate, verified, rejected, or stale instead of placing every extracted fact in one truth bucket.

Intent

Represent the epistemic status of a memory explicitly and make promotion, rejection, correction, retrieval, and pruning depend on that status.

The problem

An LLM-generated fact, a user assertion, a document sentence, and a corroborated observation do not deserve the same authority. A single active/inactive flag hides that difference. Confidence scores alone are also ambiguous: they often mix truth, retrieval relevance, and model certainty.

The pattern

Start with a small state machine:

Diagram — the states a memory moves between and what moves it — corroboration and review promote, contradiction and correction reject, and reactivation is explicit rather than automatic
Diagram source
%% caption: the states a memory moves between and what moves it — corroboration and review promote, contradiction and correction reject, and reactivation is explicit rather than automatic
stateDiagram-v2
    [*] --> Candidate
    Candidate --> Verified: trusted source or corroboration
    Candidate --> Rejected: review or contradiction
    Verified --> Stale: age or superseding evidence
    Verified --> Rejected: correction
    Stale --> Verified: revalidation
    Rejected --> Candidate: explicit reactivation

Keep separate dimensions for:

  • Trust state: whether the memory may be treated as established.
  • Epistemic confidence: how strongly the evidence supports it.
  • Retrieval strength: how useful or reachable it has been.

Retrieval policy can then prefer verified memory, include candidates with visible uncertainty when necessary, and suppress rejected records from ordinary context.

Why it works

The state machine gives every transition a policy boundary. It creates places to require corroboration, human review, held-out evals, or privileged actors. It also makes correction and audit language precise: a memory was rejected, not merely assigned a lower similarity score.

Tradeoffs

More states create more transitions, UI, and operational policy. A candidate queue can grow forever. Verification can become theater if the verifier repeats the same model or source. Stale is temporal uncertainty, not proof of falsity. Promotion rules must be explainable and scoped to the consequences of being wrong.

For low-risk note retrieval, a full trust machine may be excessive. It matters most when memory changes decisions, identity, permissions, or long-lived behavior.

Cost to adopt

Build: a status column, the legal transitions, and a promotion rule. The column is trivial; the promotion rule is a policy decision that no schema makes for you.

Forces elsewhere: retrieval must filter by status, or the states are decoration. Every consumer — prompt assembly, exports, summaries, the UI — has to decide which states it accepts, and each is a place to get it wrong.

Ongoing: candidates pile up unless something promotes or expires them, and "what verifies a candidate" is a question most systems never answer, which is how a trust model quietly becomes an unused column.

Skip it if you have no verification signal at all. Three states with nothing able to move a memory between them is worse than one honest bucket.

Seen in the atlas

Read these first

  • OmniIntelligence — candidate, provisional, validated and deprecated on one column, with a lifecycle filter on all three injection queries.
  • AgentDatabase — a disputed state that takes a claim out of service without anyone deciding it is wrong, and a status that gates what the agent boots with.
  • Hats — a canary state between draft and active, injected into a deterministic half of runs so the other half is a control group.
  • Magic Context — a lifecycle status the reads filter on, beside a verification status that gates nothing: the difference between a state and a label.
  • Membrane — the enum without the read predicate, which is the failure this page exists to name.

Every instance

Portable Handoff contributes a narrow enforcement half without the filtering half. Its Trust enum is verified · observed · claimed · inferred · untrusted, its Provenance enum names eight channels, and cap_trust(provenance, trust) downgrades verified to claimed on any claim whose provenance is not git, tool, test, file or transcript — applied while parsing, so a capsule from another tool meets the same rule. The draft author writes the provenance as well, so the cap binds the pairing rather than the source: a claim declared tool and verified passes. The question this page raises — who is allowed to set the state — gets half an answer: local code sets it for the re-hashed files and, through a preflight file finalize reads back without re-checking, for the repository snapshot; the author sets it for everything else. Nothing acts on it: every state is rendered as a label and no read path filters, ranks or omits, so the mark is withheld.

Magic Context keeps two independent axes rather than one column, and only one of them is a state in this page's sense. status is active | permanent | archived — where a memory sits in its lifecycle — and ten reads of the memories table narrow on it. verificationStatus is unverified | verified | stale | flagged — what is known about its truth, written by a measurement against the files a memory describes — and it appears in no WHERE clause: it is surfaced to a reader and gates nothing. A memory can be active and stale at once, which a single enum cannot express; the axis that says so is the one no read path consults.

Gini declares a five-value enum — proposed | active | archived | rejected | conflicted — behind a CHECK constraint, and all four recall channels admit only active. The only native write of a non-default value is archived, for observation rows; proposed, rejected and conflicted are produced by no engine path and arrive only through a migration importer, where the filter honours them. Gini also carries a network of world | experience | opinion | observation, so what kind of claim it is stays separate from how much it is believed.

Verel remains the reference for how states participate in recall, promotion, consolidation, and pruning, and for separating epistemic confidence from retrieval strength. RainBox ties the transitions to an actor model and an operator review queue.

Daimon keeps two questions apart and gives only one of them a filter. Its verbatim / inferred field is a provenance label fixed at write: the item ships with the class the extractor chose, the quote is matched against the transcript and a miss forces the item down to inferred, and an outcome claim with no tool-result citation is forced down even when its quote matched. Supersession, overturn and contradiction then act as demotions, and the project's comments rule out the filter for them explicitly: an overturned item is still evidence, so it "ranks down and renders flagged rather than disappearing."

The state that withholds is a person's. A quarantine ledger folds to candidate, active, dismissed or released per value, and an active value is withheld by one read view that the briefing, the recall index and the viewer all read through — never inserted into the index, and judged again on every hit — until a human releases it, while an unreadable ledger withholds every item. An agent may propose one and cannot confirm it. The source states the rule in one line — "a human-confirmed verdict may withhold; a machine signal only ever ranks down" — and the report carries trust_state on that ledger. The id-addressed reads and every ledger printer go through the same view, and the project's read census asserts that no surface shows a quarantined value to an agent.

Its refutation ledger answers a different question: how do you know a human approved it. A refutation — an approach recorded as having lost under cited evidence — folds to candidate, active or overturned, and the transition to active requires human authority. What makes it worth copying is that authority is not a field the caller sets: every row stamps the channel the write actually arrived through (cli-agent, cli-tty, ui, signed, mechanical), and a table maps channel to authority. A --by human flag was deleted for being "a flag whose only function was to let the caller assert its own authority… an actor acting as witness for its own claim"; what survives is --by agent, which can only narrow. The human path is the absence of that flag plus an interactive terminal, and the two strongest channels are unreachable from the CLI entirely, "because a channel an agent can reach by shelling out is the deleted --by human renamed." A hand-edited ratified: true on an agent row changes nothing, because the fold re-derives authority from the channel — there is a test for exactly that. The design states its own ceiling in the same comment: nothing local is unforgeable, and what this earns is provenance rather than proof. A revision returns an active record to candidate unless the revising channel is itself human, so approval attaches to the content and not to the row.

The same design also separates lifecycle from epistemics the way Magic Context does, but by location rather than by column. Trust lives on the item and is append-only; liveness — resolved, reopened, superseded-candidate, forgotten — lives in an event log folded at read time. That split has a concrete payoff: the system can add a lifecycle state without rewriting a single stored memory, and a rejection recorded in the wrong stream would hide the item instead of demoting it, which its own source comments call out as the reason the two logs are kept apart.

Two systems show the states are only half the work. Gini declares conflicted and no engine path ever writes it, and MateClaw ships a dedicated ContradictionDetector and a member-gated resolve endpoint whose recorded verdict nothing reads. A conflict state nothing enters, and detection without a path to resolution, both leave the operator holding a list.

Counterexamples remain instructive. Holographic collapses truth and reachability into one trust_score that feedback mutates directly. Mercury grades confidence, importance, and durability separately — good — but assigns all three at extraction, and a merge only ratchets each upward with Math.max, so they are estimates rather than states that change with evidence. Cognee has rich provenance and ontology validity with no factual promotion state; Claude-Mem and A-MEM activate generated content with none at all.

Graphify is the label without the state. Its three bands — preferred, tentative, contested — are none of them supplied by a model; all three are derived on each run from a directory of outcome-tagged Q&A files, from a signed, time-decayed score and a count threshold. A source cited by one successful answer is tentative, and only a second distinct result moves it to preferred; the threshold is a parameter with a test asserting it is not hardcoded, and the docstring gives the reason — "one save can't mint a trusted lesson." contested is entered by the presence of both a positive and a negative signal, then given a verdict by the sign of a 30-day-half-life score. Every consumer then annotates and nothing withholds: the band is appended to the node line the model reads, printed, coloured and collected into a start-here list, and the code that loads the overlay says it is there to annotate "display-only". So a tentative node is shown exactly as a preferred one is, with a different suffix — named bands on a confidence score, used for ordering and advice, which is why the report withholds trust_state.

Two design choices carry over to a real state machine. The bands are stored on a derived sidecar and recomputed wholesale, never written back into the structural store — so the layer is disposable and rebuildable, and changing the threshold or the half-life is an experiment rather than a migration. And the band reaches the model as a suffix on the thing it qualifies (learning=contested:stale beside the node), rather than as a separate block a reader has to correlate.

CLIO is a tier that costs something and withholds nothing. Its two states — unverified and trusted — act in three independent channels: a literal [UNVERIFIED] badge on the entry in the per-request memory block, a 0.3x multiplier in the score that decides which entries survive when a type exceeds its cap, and a 30-day age-out against 90 with doubled confidence decay in the consolidation pass. None of them filters. The selector, score_ltm, has no tier term, so the two states compete for injection on equal terms although the project's docs say otherwise. That is the ranking side of the line the rubric draws, and the report carries no trust_state for it. A tier that only filters is invisible to the model, and one that only badges is invisible to the ranker, which is where CLIO's selector leaves it.

Promotion needs two corroborations from distinct agent:session pairs, with the source identities stored as an array rather than a count so independence is checkable, and the unconditional override is wired only to the /memory promote slash command a person types. The identity is where it gives way. Both shipped entry points assign CLIO_AGENT_ID and CLIO_SESSION_ID at startup, and the library falls back to 'unknown' for anything that embeds it without them — a fallback a committed regression test pins deliberately beside a second subtest asserting that two distinct sources promote. And add_corroboration is an operation of the model's own memory tool whose schema exposes source_agent and source_session as optional strings, passed straight into the sybil key, so two calls naming two sources promote an entry to [TRUSTED] without a second agent or a person. update_ltm, on the same tool, rewrites a trusted entry's text and leaves its tier and source list in place.

Two lessons generalize. A trust threshold is only as good as the identity it counts: if independence is the property, the identity must come from the runtime, not from a default and not from a caller-supplied argument. And a silent fallback converts missing configuration into a policy change — // 'unknown' is a defect for every caller that does not set the variables.

Memory Palace is a procedural instance, and its state reaches the read path directly. review_state is draft | human_reviewed | rejected, validated on construction, and recommend_for_trigger returns only human_reviewed rows — the docstring calls this "the read-side enforcement of the draft-by-default invariant". approve_draft refuses a rejected row ("create a new draft instead of approving a rejection") and increment_success refuses anything not approved, "so the success counter truly reflects approved usage". The gap is the one this pattern always has to be checked for: nothing compares a new draft's source_hashes against rejected rows, so the state is durable for a row and not for a claim.

Two systems in this atlas show the failure mode from opposite directions. memory-lancedb-pro has the enum (pending | confirmed | archived) and a read-path gate on it, and then wrote the candidate state out of existence: every writer emits confirmed, four of them with the comment "write confirmed to unblock auto-recall". The gate survives over an empty population. YesMem has the opposite half — a real trust model computed from use count, source and importance that downgrades a supersede to pending_confirmation — and no state machine to receive it: nothing reads or clears that value, so the correction is simply dropped. A trust state needs both the field and a transition somebody can make.

Midas is a deliberate non-instance. Its provenance field is a discrete four-value enum consulted on the decision path, and it is not a trust state: it records where a memory came from, not what anyone believes about it. Separating authority from credence turns out to be the more useful axis for gating actions, and conflating the two because the field is an enum is the easy mistake.

breadcrumbs is the promotion half of this pattern on its own, without the mark, and the interesting part is where it puts the refusal. Its semantic tier carries two states — asserted and verified — and store_fact() writes asserted unconditionally, on a stated rule: "Nothing an agent stores starts verified." Promotion goes through one function, verify_fact(), which raises before it writes on empty evidence, an unnamed verifier, an unknown authority class, agent authority, and a verifier equal to the asserting actor. The first refusal states the rule:

"verified requires naming the oracle (a CI run, a data assertion, a human ruling); an agent may not mark its own claim verified with nothing behind it"

The refusals convert "the model said so" from a default into something a caller has to lie about deliberately, and the authority class and both names are caller-supplied strings, so the lie is a renamed argument. The context block renders the oracle inline beside the value, so a reader of the prompt can see which claims nobody checked. That label is all the state does: build_context emits every fact that passes its time and scope masks whatever its status, no read withholds an asserted one, and the mark is withheld. Its correction ledger sharpens the same idea into an admission rule — only ci_failure, data_assertion, operator_ruling or reverted_pr count — with one exclusion worth copying verbatim: "Model-vs-model disagreement is never a correction." A stronger model disagreeing with a cheaper one has no ground truth behind it, and a self-improvement loop that admits it optimises a proxy.

OmniIntelligence runs two state machines on one row and wires them in one direction, which is the arrangement this pattern usually collapses. Lifecycle status — candidate → provisional → validated → deprecated — decides whether a pattern may be injected. An evidence tier — unmeasured → observed → measured → verified — decides whether it may advance. Evidence gates status and status never touches evidence, so "how sure are we" and "what is this allowed to do" cannot contaminate each other.

Two mechanisms are worth taking whole. The tier is monotonic in SQL: the UPDATE that writes it carries a CASE mapping each tier to a weight and only matches when the new weight exceeds the stored one, so a redelivered message, a concurrent writer and a caller with a stale read all fail by touching no rows. And the transitions are asymmetric with a stated band — promotion at a 60% success rate over five injections, demotion at 40% over ten plus a five-failure streak and a 24-hour cooldown — with the 20-point gap named in the constant's own docstring as the thing that keeps variance from flip-flopping a pattern between states, and the operator's override bounded so the band cannot be closed.

The failure is instructive for anyone building a ladder. verified is a valid value, is gated on, and is written by nothing: the sole writer's pure function returns only OBSERVED or MEASURED, above a docstring saying verification "requires independent validation (not computed here)". A top state nothing can reach makes every gate that names it unsatisfiable while reading, from the schema and from the enum, exactly like a state that works.

OmniNode's knowledge base gives each kind of claim its own vocabulary instead of one shared enum, which is the cheapest refinement on this page. A decision record moves through proposed, accepted, superseded, deprecated or rejected; an architectural pivot through observed, emerging, accepted, historical or superseded; a doctrine principle only draft, accepted or deprecated. The vocabularies are Pydantic Literals discriminated on a type field, exported into the published JSON schema by the validator that enforces them, so the contract cannot drift from the check. A pivot also carries confidence — low, medium, high — as a separate field from status, which is this pattern's central discipline written into a schema: how sure you are and whether the claim is current are different questions, and one column cannot answer both.

The same repository shows the limit of a schema-only state machine. Nothing reads a status back: supersedes and superseded_by are validated as lists of strings and never checked against each other, so the corpus's single supersession pair is reciprocal by hand. A state machine enforced at the boundary and nowhere else is a vocabulary, not a machine.

Flatbread Proof builds the machine and leaves the readers outside it. Its Decisions move from proposed to accepted, rejected or superseded only through typed mutations: acceptance refuses a non-proposed target and a second accepted Decision on the same question Issue, rejects the proposed siblings in one transaction, and a revived Decision cannot be accepted while the Decision that rejected it stands (packages/proof/src/decision-lifecycle.ts:31-116). No read excludes a row by that state, and a Finding's invalidates edge leaves its target accepted (packages/flatbread/src/proof/read.ts:581-602, packages/proof/src/planner.ts:428-440). The only property a read filters on is a retracted boolean. Enforcing transitions at the writer settles which states exist; only a read predicate decides whether an overturned record reaches the next session.

Ouroboros splits the axis one further step than this page usually argues for, and the reason is recorded in the code. Its ledger carries LedgerSource — what kind of authority a value rests on, from USER_GOAL down through REPO_FACT, CONSERVATIVE_DEFAULT and ASSUMPTION — and, orthogonally, DecisionProvenance: how the decision was reached, from USER_CONFIRMED, MODEL_INFERRED, TIMEOUT_DEFAULT, LATERAL_CONSENSUS and MAINTAINER_POLICY. The module says the second axis was added because a timeout-defaulted decision had been indistinguishable from a user-confirmed one, so degraded specifications executed silently — the failure this page's confidence-versus-status split exists to prevent, found one level up. Only the two model-derived provenances face a clarity gate before the state may execute; the other three pass unconditionally, which is a machine rather than a vocabulary because the gate is where the states are read. USER_CONFIRMED comes from a regex over transcript section labels: where text sat, not who wrote it.

Its pre-commit form repeats the split under different names: CandidateContentSource for where content came from, ConfirmationAuthority for who signed off, and CandidateResolution for where it stands, so "why do you believe that" and "who approved it" have separate answers and both are stored.

Hats puts a canary arm between draft and active, with a control group no other system on this page keeps. A lesson distilled from a failed run enters at draft with confidence 0.5, becomes canary on first injection, and while it is unproven inCanarySlice — an FNV-1a hash of runId:lessonId against a 0.5 share (src/memory/lessons.ts) — decides whether this run sees it at all. The runs that do not are a control group, deterministically so: the same run and the same lesson always decide the same way, so a rerun is comparable. Promotion then reads outcomes rather than intentions — two acceptances at confidence ≥ 0.6 promotes; three rejections or confidence below 0.2 disables with retiredReason: 'contradicted by outcomes'; and six injections with zero tag matches disables it as "never matched — expired as noise", which separates wrong from useless as exits. An explicit human correction skips the ladder and is stored active at 0.9, verbatim, because a correction is not a hypothesis. What the machine does not do is record its own transitions anywhere: the counters and retiredReason are overwritten in place by a whole-file rewrite, and the runtime's hash-chained audit log is never told.

AgentDatabase adds the state this page's implementations mostly lack: a claim can be taken out of service without anyone deciding it is wrong. dispute moves a record to disputed and sets conflict.state to unresolved, and the eligibility filter excludes it — so a contested memory stops being answerable while the adjudication stays open, instead of forcing a winner at the moment of conflict, which is the moment there is least information. Caura and dense-mem hold the same position with their own values: Caura's conflicted and dense-mem's disputed both fall outside the status set their recall reads admit.

Two details are worth copying beside it. The transition is written before the mutation: append_transition appends {transaction_id, operation, recorded_at, recorded_by, from_status, to_status, valid_to_before, valid_to_after, reason} into recorded_time.transitions, then sets the new status — so the audit trail is a property of the record rather than a side table that can drift from it, and project_record_at can replay it backwards to answer what the status was at a given record time. And the refusals are stable error codes carrying no record content, on the reasoning stated in the exception class: a failure message about a memory should not become a second copy of the memory.

The caution is the same one the report states. The status field is populated across three values in the live store — 6 active, 108 candidate, 84 retired — and disputed has never been used, no record carries a transition, and nothing has ever superseded anything. A state machine whose interesting transitions have not run is a design, and the difference between a design and a mechanism is visible only in the data.

Scope Recall answers the question this page asks first — who may set the state — by giving the answer to no caller. claim_versions.state is a SQL CHECK over proposed, active, disputed, superseded and retracted, and select_effective never returns a proposed or a retracted version, so the state decides whether a memory may answer rather than where it ranks. No promote, approve or apply verb exists on either adapter's tool surface: a proposal becomes active because qualify finds an authority-bearing root among the sources it cites, and stays proposed otherwise. The candidate evaluator may re-propose a claim against fresh sources, and its prompt ends "Do not choose a fact state: Core qualification owns that decision" — enforced by the code never reading a state from the model's reply.

Utopia is the version of this pattern where the states are tables rather than an enum column, and its source argues the choice in terms that apply to either. A claim sits in pending_facts, facts, derived_facts or rejected_facts; confirm and reject move it. The source rejects the status-column version explicitly: roughly fifty queries select live facts by invalidated_at IS NULL, so a forgotten predicate would put an unconfirmed fact on the graph, while a forgotten UNION only makes the queue invisible — "failing in the direction where the mistake is visible". The same judgement was made once before for derived facts, after a first attempt with a flag on facts let the low-confidence review queue offer deductions for confirmation and let the temporal engine close a human assertion with a machine conclusion. Beside the states sits a confidence float that gates automatic interval closing and fills the low-confidence queue and filters no retrieval result at all — with a comment saying a person's verdict is not expressed as a number, it goes in the ledger.

GrayMatter declares the vocabulary and wires nothing to it. Its confidence field holds verified, inferred or unverified, validated at the write path and rejected otherwise — the shape this page asks for, including a state that should withhold a memory from being treated as true. Every reader displays it: the explain receipt copies it, the TUI prints it, the note exporter writes it into frontmatter. It appears in no ranking, no filter and no consolidation decision, and the field's own comment concedes "it never affects ranking, decay or pruning." Its only producer sits on the Go library interface and on no MCP tool or CLI command, so an agent cannot mark its own inference unverified even for display. The result is a store of claims wearing the vocabulary of a store of beliefs.

Membrane has the enum and not the predicate, which is the failure this page exists to name. Its revision status is active · contested · retracted, declared with per-value doc comments citing the specification, and four code paths write it: contest, retract, supersede and a semantic re-activation. A search for reads of that field at the tree root returns those writes, one erasure in the audit path and the copy into the gRPC response — no filter and no ranking term, so a retracted record comes back labelled retracted and only a caller who reads the label knows. What retrieval sees instead is the salience of zero that retraction and merge set at the same moment, and that only excludes anything when the caller passes a positive minimum, which both SDKs default to zero and the project's own lifecycle eval raises so its retraction scenario passes. Contest sets the state and leaves salience alone, so the one status that means withhold this pending resolution changes nothing about what retrieval returns. The repair is a status predicate on the root query and on graph-neighbour hydration, which applies no salience floor at all, so a superseded record can return beside its replacement even under a positive minimum.

aios (rexleimo) earns the filtering half and seals the transition. Its claimStatus is decided inside the append from the writer's runtime identity, a shared write without a publish capability lands candidate, and every default memo read drops candidates. But authorize denies promote, reject and expire unconditionally, so the state has an entry and no exit. The writers that pass no identity — the MCP and Pi write tools the model holds — land verified as local-user, which is the case this page's question about who may set the state exists to catch.

AlphaOne ai-memory puts its one trust state on a lifecycle column built for work items, and earns the mark on the filter rather than the vocabulary. quarantined is system-only — refused as caller input and absent from every caller transition — and is set by federation receive on a relayed write whose author signature did not verify, when AI_MEMORY_FED_QUARANTINE_UNATTRIBUTED is on. Reads admit only an allow-list of five other states, so the row is stored but invisible until a signed copy of the same id verifies and a raw update returns it to open. Local writes never enter the state, and the knob defaults off.

Attune AI builds the states and then forbids using them. Curated memories resolve to settled, check-before-acting or suspect from a human verdict log, with wrong forcing a tombstoned basis and a substantive edit voiding a verification through a content digest. Its decision D1 — "Nothing here may filter a memory out of a result on the basis of age" — is applied to every recall surface, so a memory a person judged wrong is returned with the label suspect · judged WRONG. The rule is right about age and over-applied to an explicit rejection, which is the distinction the mark turns on.

Graft writes a doubt state and spends it nowhere. STALE is set by maintain resolve --action stale in an audited transaction, and the skill offers it for a note the agent could not confirm. Every search arm selects state IN (0,1), so it filters nothing; the query and hook outputs carry no state, so it labels nothing; and every maintenance scan selects state = 0, so it drops the note out of the one process that would revisit it. A test pins "stale nodes stay searchable". SUPERSEDED and RETIRED do filter, and both mean replaced or removed rather than disbelieved, so the report withholds trust_state.

MuscleMemory keeps the states honest and lets the client supply the evidence that moves them. A revision is provisional, validated or flagged and a procedure provisional, active, flagged or retired; search, plan composition and related-skill links all require active, validated and a person's approval (packages/musclememory/retrieval.py:228-230, :409-410, planning.py:40-41). flagged is terminal for a revision: no later success clears it (service.py:895-905). The transitions are driven by outcomes the caller reports, and the compiler stamps every revision "Completion evidence is caller-attested" (compiler.py:212). retired is terminal too: a revise of a retired procedure is refused (service.py:510-513), and retiring stores a fingerprint that refuses the same skill on any later learn. The hooks produce flagged in one case only, when a session was handed a skill and that skill's own test ran last and failed (service.py:1232-1289).

Tests to require

  • Prove candidates cannot enter verified-only context.
  • Exercise every allowed and forbidden state transition.
  • Verify rejected records do not return through normal recall.
  • Test that retrieval feedback changes usefulness signals, not truth automatically.
  • Verify stale records can be revalidated without losing history.
  • Test actor permissions for promotion, rejection, and override.