Audience: any engine considering adopting pattern-buffer as its
world-state substrate (Construct, Kernos, your host). Status: pre-1.0,
living. This is the discipline brief — the mechanistic responsibilities
the host must hold so the buffer details and surfaces with utmost
proficiency. For the wiring contract and exact signatures see
ADOPTION.md; for narrative-extraction depth and measured
baselines see INGESTION-PLAYBOOK.md; for the
principles see WHITEPAPER.md. This document is the map
that ties them together.
The engine never observes and never guesses. It has no background process, no will to look at the world, no inference it runs unbidden. It documents exactly what you present through its one gate, and it surfaces exactly what you ask for along the structure it maintains. Therefore:
Fidelity in both directions is the host's responsibility, exercised through a disciplined contract. The engine's job is to make that discipline enforceable and honest — to refuse malformed truth, to never fabricate, and to keep every fact time-indexed, attributed, and queryable. Proficiency is a partnership: you categorize well going in, and you query along the grain coming out.
Everything below is the operationalization of that sentence.
The mechanistic responsibility: every fact you will ever want to retrieve must arrive as a well-formed assertion through the gate, correctly categorized along the axes below. A fact that arrives mis-categorized is not wrong data — it is invisible data: it lands on the wrong fold key, in the wrong frame, at the wrong time, and no retrieval will find it. Ingestion discipline is retrieval discipline, paid in advance.
For each thing you decide to record, the host (or its extractor) must pin all seven. These are not optional metadata; they are what makes a fact findable and correlatable.
-
Identity — which entity, canonically. A namespaced id (
person:/place:/obj:/event:/doc:+ snake_case). Attach every referring expression as analias; when a thing is later named, keep its id and add the name; when you minted a duplicate, emitsame_as. Why it's load-bearing: identity is the spine of retrieval — the engine's identity closure is what gathers all of a subject's facts under one query. Fragment the identity and you fragment the subject; a later query sees a third of the truth. Cheap to keep, expensive to repair. -
Attribute — the canonical key. Use the engine's canonical names (
infor all containment/location;connects_tofor passages;kindfor what a thing is) and stable domain vocabulary otherwise. Know whether a key is functional (one value at a time —color,in: later supersedes earlier) or set-valued (alias,connects_to: values accumulate, never conflict), and whether a numeric quantity is accrue (absolute baseline plus signed deltas). Why: the fold is key-local. Two names for the same attribute (wall_colorvscolor) split a fact across keys the corroborate/conflict machinery will never see meet. (The gate canonicalizes a built-in set for you — lean on it, but don't fragment what it doesn't know.) -
Value & value_type.
entitywhen the value is another id (so walks and correlations traverse it);literalfor scalars/structured;deltafor a signed numeric increment on anaccrueattribute;unresolvedfor a deliberately-deferred aspect (a thunk — see §below). Express approximate quantities as structured bounds ({"gte": 40000}) so a later precise reading refines rather than contradicts. For fungible quantities, usefold_policy="accrue"and numericliteral/deltarows; the engine sums int/float values and keeps the ledger append-only. -
Time — both axes, deliberately.
valid_from/valid_tois world time (when it was true in the world); the engine separately recordsasserted_at(when you told it). Stamp the world-time of the fact, not the moment of narration — a Ch.3 revelation about Ch.1 gets Ch.1'svalid_from. Reservetimelessfor identity and structure ONLY (kind, names, fixed adjacency); everything that holds-at-a-time gets a stamp. Why: as-of queries are first-class; mis-stamped time makes a fact unreachable at the moment it actually mattered. -
Frame — whose truth.
canoncarries facts about the world at their true historical time, even when revealed late or learned by a character.knows:<entity>rows are additional copies marking that someone knows a thing — never replacements for canon.plot:holds host-owned arc structure. Why (the costliest known ingestion failure): putting late-revealed canon into aknows:frame leaves canon honestly empty at probe time — the fact exists, but not where ground-truth queries look. -
Provenance — how strongly, from whom.
stated(asserted fact) /observed/inferred(a deduction) /assumed(a working theory);source_doc=<doc:id>for document claims, a speaker id for reported speech;correctionto supersede a prior wrong row. Why: provenance is the engine's immune system. Evidence rank decides folds (a stated fact overrides an assumed one without a false "conflict"); the source chain is what a downstream consumer reads to decide how much to trust an answer. -
Relations & causality — extract every one the text supports, none it doesn't. Containment (
in), the lateral graph (connects_to,adjacent_to), and causality (caused_by→ anevent:entity). Name each relational category you need or the extractor will under-emit them. The equally hard negative: never an edge the text doesn't support — vertical proximity is not connectivity. Relations are the correlation graph retrieval walks; an un-extracted edge is a correlation that can never surface.
- Atmosphere and texture are not assertions. "The air smelled of rain" is not a fact about an entity.
- Never invent the negative. Absence of an assertion is not an assertion of absence. If the prose didn't say the wall was clean, do not record "no graffiti" — a later "was it clean?" must honestly return unknown, not no.
- Never author what is derivable. Location, emptiness, age, staleness, containment chains, salience — these are computed from the log on retrieval. Storing them as facts is the fragmentation the engine exists to prevent.
When something is "not yet in view," do not invent it and do not omit the
entity. Record the aspect as unresolved with a policy
(invent_under_canon / observe_or_unknown / deny). It rides along
un-pinned (an object can move while its contents stay a sealed thunk)
until something forces it — at which point the resolution is memoized into
the log and is stable forever, across processes. This is how "my office
walls, color unknown" becomes "blue" the day it's painted, with everything
before that honestly unresolved.
The substrate discovers what is; it never stores or infers what is not. The unknown has three positive states, and choosing the right one is a host responsibility:
- Unestablished — no row. A query returns honest
unknown. (Most things.) - Established-deferred — an
unresolvedthunk: a real placeholder the resolver will later force (the box whose contents you'll define on open). - Established-unknowable — a
denythunk with a reason: a positive assertion that the thing is canonically unknown (the mystery box that stays shut — everyone who looked inside died). Usedeny, not an absent row (which reads as deferred and the resolver would invent an answer), and not a "mysterious" tag (a stored property → a membrane breach).
Knowledge is per observer, sparse, and never a grid. Each observer (player,
each NPC) is a knows:<id> frame holding only what that observer knows.
What an observer does not know is structural absence — never a stored
"X doesn't know Y" row. "What does X not know that's true?" is computed:
frame_diff(canon, knows:X) (dramatic irony); inter-character gaps are
frame_diff(knows:A, knows:B). With N NPCs this is the same operation over more
pairs — computed on demand, nothing materialized. Record a character's
knowledge only where load-bearing; the 44th NPC who learned nothing has no
rows and reads as honest silence.
For effective knowledge that includes common/public facts, pass a b-frame
union: frame_diff(canon, [knows:X, public], scope). A fact is covered if any
b-frame agrees; private false belief is reported only when no public/common
frame corrects it.
The perception-write pattern (how a character learns during play). When an
observer perceives or is told a fact, write that one row into their frame —
knows:<id> · attribute · value, the value frozen at perception time —
sparse, per-event, never a canon copy, never × N. Frozen is deliberate: if
canon later changes, their frame still holds the old value, and
frame_diff(canon, knows:<id>) surfaces it as stale/false belief — a
feature, not a bug. Do not store a marker that resolves to current canon
(it would silently auto-update what the character "knows"). Optionally log a
perception event: with caused_by as causal scaffolding; the frame row is the
durable fact.
The membrane-test — the one guard against bloat and fabrication: could the
engine recompute this row from other present facts? If yes, it is derived —
never store it; only an irreducible observation may be a fact. Staleness,
current-confidence, salience, "doesn't know," presumed-empty are all
recomputable → none stored. (Source-confidence at assertion is different — it
is irreducible provenance, legitimately the row's confidence field.)
- Exact money / precision → integer minor-units. Numbers are int or
float already (
12.5,0.7work). For exact money, model the minor unit as an integer ($19.99 → 1999): integer arithmetic is exact, theaccrueledger sums without float error, and no new type is needed. - Observed completeness → a positive fact, never read from absence. "Is
there a 4th key?" is honest
unknownby default. If a character counts, assert it (keyring · count · 3 (observed)/ acomplete_as_ofstamp); the "4th?" question is then relational (the asserted count vs the query), never an inference from missing rows. Closed-world answers without a closed-world assumption. - Trust / staleness →
confidence, computed, never stored.confidence(e, attr)returns a derived trust score (provenance × recency × corroboration) pluslast_observed_at(valid-time) and — in tracking worlds —last_confirmed_at_wallclock+ a mode-scopedrecency(TRACKING-MODE-V1: wall-clock decay under your declared per-attribute half-lives; permanent in fiction — the page is true). Render staleness from the wallclock field. A three-campaign-years-stale fact keeps full validity and — in fiction — undiminished confidence; only wall-clock unconfirmed age (tracking worlds) decays trust. Never delete it, never store a decayingconfidencerow (the membrane).
- Scaffold identity first. Establish a registry (ids/names/aliases/kinds
- timeline + place graph) before extracting facts, so identity is globally
consistent by construction. In live play the registry grows turn by turn;
in batch it's established once over the document — same interface
(
establish/extend), different calling pattern.
- timeline + place graph) before extracting facts, so identity is globally
consistent by construction. In live play the registry grows turn by turn;
in batch it's established once over the document — same interface
(
- Chunk small, on discourse boundaries (~3.5k chars); thread the registry, not a transcript.
- One explicit time convention, anchored in the text's own cues; the scene cursor is the fallback stamp.
- Defer classification and batch it — the durability sidecar is rebuildable by design.
- Harden the model shim like a flaky network client (timeouts, backoff, quota-exhaustion detection, resume-by-chunk).
These fire with no prompt help; design pipelines to rely on them:
attribute canonicalization (with receipts in the log), cursor
stamping of any unstamped non-timeless row, role-checked appends (your
renderer/narrator cannot write; nothing you emit can become
generated/default), truth maintenance (a contradiction survives
ingestion as a flagged contradiction, both rows alive — never a silent
merge), assumption quarantine (a theory can't outrank a later
observation), containment-cycle rejection at the write gate, and the
WorldMismatch guard (foreign-world rows refused). Malformed rows are
rejected/quarantined, not silently swallowed — but they're also not stored,
so a quarantined row is a lost fact: get the categories right.
The mechanistic responsibility: relevant detail and its correlations surface only if you query along the structure the engine maintains. The log is truth; reads are derived projections; the engine gives you a small set of typed verbs that each follow one kind of structure. Proficiency is choosing the right verb, scoping by the subject's closure, fixing the perspective, and reading what comes back honestly.
-
Resolve the subject to its canonical identity FIRST. A subject named in prose ("my brass measuring spoon", "the clerk") is not yet an id. Run it through
refer(description, scope=, frame=)→ aResolution(resolved/candidates/underdetermined), or letask()do it.referstrips articles/possessives and resolves through the identity closure, so aliases andsame_asmerges all collapse to one canonical id. Skip this and you query a fragment — facts logged under a sibling alias stay invisible. Anunderdeterminedresult is your ask to put to the user; the engine will not guess below its confidence floor. -
Choose the read that matches the question. The verb map:
You want… Use Surfaces one fact (a key's value) state(entity, attribute, …)the folded winner + conflict flag everything about a subject snapshot(ids, lens=…)/materializea coherent bundle of folded facts entities satisfying a numeric bound where(attribute, op, value, …)ids whose folded numeric value matches a collection rollup aggregate(container, attribute, op, …)sum/count/min/max/avg over numeric folded values of contents()where something is locate(entity)containment chain, nearest container first what's inside / co-located contents(container)members (emptiness = [], derived)is X reachable from Y path(a, b)a route, or None(no false connectivity)what structurally matters around X neighborhood(entity, depth=…, …)X's folded state plus bounded, salience-ranked correlates how likely X should survive a budget salience(entity, …)a derived ranking score, not truth what happened (to whom) events(participants=…, kind=, since=, until=)the event spine, filtered what A knows that B doesn't frame_diff(a, b, scope)divergent/absent facts; bmay be a frame uniona natural-language question ask(question, frame=, as_of=)one parse + refer + folds → Answerforce a deferred aspect resolve(entity, aspect)the thunk's resolution per policy Pick the narrowest verb that answers the question —
statefor one key,snapshotfor the bundle.askis the convenience front door (it composes refer + state + locate + events); the structured verbs give you control and zero model cost. -
Fix the perspective — frame and time — every read.
frameselects whose knowledge you're reading (canonfor ground truth,knows:<id>for a character's beliefs — sparse by design, so absence there is meaningful).as_ofselects which world-time moment; omit it for "now / head". You can also pin the knowledge axis (asserted_as_of) to ask "what did we know as of then". Absence is honest: a read that returnsunknownmeans the world never recorded it in that frame at that time — that is an answer, not an error, and never means false. -
Follow correlations through the structure — the four axes. To surface what's related to a subject, walk the edges the engine maintains rather than full-text scanning:
- Spatial:
locate(up the containment tree) andcontents(down) — co-located objects, the room a person is in, what a container holds.pathfor connectivity across the lateral graph. - Causal / temporal:
events(participants=[X])for X's history, andcaused_byedges to trace why a state changed (the paint event behind the blue wall). Events are the scaffolding that orders change. - Identity: the closure already gathers facts logged under any of a subject's aliases/merged ids — which is why step 1 is non-negotiable.
- Knowledge:
frame_diff(canon, knows:X, scope)is the precise tool for "what is true that X doesn't know" (or the reverse) — perspective correlation, computed, not guessed.
- Spatial:
-
Read provenance and conflict on every fact — never launder. Each
Factcarries{status, source_chain, assertion_id}; astateread can come backconflictedwith a holdingwinnerand theconflictingids. Surface the holding answer and the dispute to your consumer; don't silently pick a side the engine deliberately refused to. Agenerated/defaultvalue is render-coherence fill, never a fact — do not promote it. -
Use
neighborhoodfor the common correlation sweep. It packages the fixed structural axes — containment, lateral graph, entity-valued relations, and participant/causal event edges — into one bounded read. Setedge_kindswhen you need only a subset, and usebudgetonly to shape the returned neighbor list; it never invents, resolves, or paints the frontier.salience()is the same derived scorer used for ranking and materialization budgets. -
Scope by closure, not by log scan. Query the subject and its containment/identity closure; let the engine's indexes do the retrieval. The read path is built so a fold costs the size of the subject's closure, not the size of the log — so never re-scan the whole world host-side to find correlates. Ask the structured verb; it already walks the index.
-
Honor the typed outcomes.
underdeterminedrefer → your ask to the user.UNKNOWNfromresolvein a tracking world → "I don't know" is a representable, correct answer.Materialization.unresolvednames the frontier (thunks in scope),defaultsare coherence fills, not facts, andquantitiesare derived totals, not stored ledger rows. Branch on these; don't coerce them into a guess. -
Never cache derived truth host-side. Location, emptiness, current state, salience, staleness — re-query them. They are cheap, as-of-correct on every call, and the moment you cache one you've recreated the fragmentation the substrate exists to prevent.
Use the engine's packaged bounded read for the usual case:
1. id = refer("…X…", scope, frame).entity_id
2. nb = neighborhood(id, depth=1, frame=, as_of=, budget=)
For specialized reads, compose the same primitives (snapshot, locate,
contents, events, frame_diff) directly. neighborhood is the default
replacement for a host-side correlation sweep because it is identity-closed,
frame/as-of scoped, fanout/depth bounded, and provenance-preserving.
- MUST resolve a referring expression to a canonical id before querying; a fragment id returns a fragment of the truth.
- MUST pass
frameand (when time matters)as_ofon every read; a read with the wrong perspective is a confidently wrong answer. - MUST treat
unknown/underdetermined/UNKNOWNas first-class answers and routeunderdetermined/conflict to the consumer. - MUST surface provenance and conflict; NEVER present a
conflictedwinner as settled or promote agenerated/defaultto a fact. - NEVER full-text-scan the log host-side for correlates — use
neighborhood()or walk the structural edges (locate/contents/path/events/caused_by/closure). - NEVER cache a derived value as truth; re-query.
The summary: ingest along seven axes so nothing is invisible; retrieve along the four structural correlations so nothing relevant stays buried — and let the engine keep both honest.
This is not theory: Construct (the first live host) already implements most of it, often under its own names. If you're adopting, study its turn loop — it is the reference. The mapping:
| Discipline (this brief) | Construct's named pattern |
|---|---|
| Ingest via the gate; renderer never writes | render-to-canon is a separate ingest(prose, …) step, gated by a RENDER_LEASH ("introduce no new entities/facts beyond the briefing") and a post-render concealment audit that flags unlicensed rows |
knows:<id> are additive copies the host maintains |
_mirror_rows() — after each ingest, touched canon facts are mirrored into the player frame; this is the host's discovery-gating |
| Deferred truth = a thunk you force | furnish_scene() — seed an unresolved scene description, resolve() it, mirror the result (the thunk lifecycle, concretely) |
| World-time stamping, avoid simultaneity ties | TURN_EPOCH — authoring rows at small coordinates, play rows at 1000+turn, so authoring and play never tie on valid_from |
| Don't store derived/host state | pacing counters live in a session:main ledger frame in the buffer, reread each turn — not host memory |
| Frame perspective is structural | four frame namespaces treated as categories: canon, knows:<id>, plot:<arc>, session:<host> |
| The navigator/narrator two-lane (whitepaper A4) | implemented exactly: a deterministic navigator reads plot:; the narrator briefing is drawn solely from knows:player and provably carries zero plot: rows |
| Resolve identity before querying | refer(desc, frame="canon") at adjudication and movement, then locate() to verify |
| Scope reads by closure, not the log | arc_scope — the arc's entity closure, computed once at session zero, passed to every read |
| Bundle reads; don't N+1 the fold | SnapshotReads — a few snapshot() materializations per turn serve all condition atoms; per-key state() reads are counted as point_reads |
frame_diff for perspective correlation |
the irony delta — frame_diff("canon", "knows:player", scope) drives pacing |
| Honest absence / frontier | three-valued Truth.TRUE/FALSE/INDETERMINATE propagated through arc-condition expressions |
Two workarounds the engine has since retired (don't copy them into a new host):
- Construct shipped a host-side possessive/article-stripping retry on
ask()("the host half"). The engine now normalizes determiners and derives a canon scope from aknows:<id>frame insideask/refer(LIVE-FINDINGS Fix 3) — the host retry is now redundant. - Construct caches the immutable arc structure in a sidecar (
arc_cache) and leans onSnapshotReadsbecause per-key folds once cost seconds at play scale. The read path is now closure-scoped and indexed (letter 037), so a fold costs the subject's closure, not the log — bulk-snapshot caching is still a fine optimization, but the multi-minute pressure that forced it is gone.
The lesson the reference implementation teaches: almost every host "workaround" was either (a) a discipline this brief now names as the correct host responsibility, or (b) a temporary paper over an engine gap that the engine then closed. When you hit friction, ask which it is before building around it — and tell the engine maintainers.