pbuffer 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ """Test doubles for the engine's single outside dependency.
2
+
3
+ The engine takes one injected callable ``(prompt, schema) -> json`` at
4
+ World construction (whitepaper §17.1). ``StubModel`` is that callable
5
+ for tests: it replays canned responses and records every call, so tests
6
+ can assert both what the engine asked and that deterministic paths made
7
+ no model call at all.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import re
14
+ from typing import Any, Callable
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class StubModelExhausted(AssertionError):
20
+ """Raised when the engine makes a model call the test did not script."""
21
+
22
+
23
+ class StubModel:
24
+ """A scripted ``(prompt, schema) -> json`` callable.
25
+
26
+ Responses are returned in FIFO order. Every call is recorded in
27
+ ``calls`` as ``(prompt, schema)`` tuples. An unscripted call raises
28
+ ``StubModelExhausted`` — the no-LLM-on-deterministic-paths invariant
29
+ (P7) is asserted by scripting zero responses.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ responses: list[Any] | None = None,
35
+ fallback: "Callable[[str, dict[str, Any]], Any] | None" = None,
36
+ ) -> None:
37
+ self._responses: list[Any] = list(responses or [])
38
+ self._fallback = fallback
39
+ self.calls: list[tuple[str, dict[str, Any]]] = []
40
+
41
+ def enqueue(self, response: Any) -> None:
42
+ """Script one more response."""
43
+ self._responses.append(response)
44
+
45
+ def __call__(self, prompt: str, schema: dict[str, Any]) -> Any:
46
+ self.calls.append((prompt, schema))
47
+ if self._responses:
48
+ response = self._responses.pop(0)
49
+ logger.debug("StubModel call #%d -> %r", len(self.calls), response)
50
+ return response
51
+ if self._fallback is not None:
52
+ return self._fallback(prompt, schema)
53
+ raise StubModelExhausted(
54
+ f"unscripted model call (call #{len(self.calls)}): "
55
+ f"{prompt[:120]!r}"
56
+ )
57
+
58
+
59
+ def rule_classifier_fallback(movable_prefixes: tuple[str, ...] = ("obj:",)):
60
+ """A deterministic classify-fallback for tests: places and furniture
61
+ are structure; everything else movable is STATE. Raises on non-classify
62
+ prompts so unscripted extraction/resolution still fails loudly."""
63
+
64
+ def _durability(subject: str) -> str:
65
+ if subject.startswith("place:") or subject.split(":")[-1] in {"desk", "drawer"}:
66
+ return "CONSTITUTIVE"
67
+ return "STATE"
68
+
69
+ def fallback(prompt: str, schema: dict[str, Any]) -> Any:
70
+ if not prompt.startswith("Classify the lifetime"):
71
+ raise StubModelExhausted(f"unscripted non-classify call: {prompt[:80]!r}")
72
+ # Batch path (INGEST-HARDENING-V1): the schema asks for `verdicts` and the
73
+ # prompt lists facts as "N. entity · attribute · value".
74
+ if "verdicts" in schema.get("properties", {}):
75
+ verdicts = []
76
+ for line in prompt.splitlines():
77
+ m = re.match(r"\s*(\d+)\.\s+(\S+)\s+·", line)
78
+ if m:
79
+ verdicts.append({
80
+ "index": int(m.group(1)),
81
+ "durability": _durability(m.group(2)),
82
+ "class_confidence": 0.9,
83
+ })
84
+ return {"verdicts": verdicts}
85
+ # Per-row path.
86
+ subject = ""
87
+ for line in prompt.splitlines():
88
+ if line.startswith("Subject: "):
89
+ subject = line.removeprefix("Subject: ")
90
+ return {"durability": _durability(subject), "class_confidence": 0.9}
91
+
92
+ return fallback
@@ -0,0 +1,245 @@
1
+ """Thunks and the resolver: the lazy world (whitepaper §8, P3).
2
+
3
+ An unresolved aspect is a first-class row (value_type='unresolved') whose
4
+ value carries its policy and any accreted constraints. Forcing evaluates
5
+ exactly once; the memo is the fold (generated rows supersede the thunk's
6
+ key). Thunks move without resolving — containment supersession touches
7
+ the holder, never the thunk row.
8
+
9
+ The resolver is the only component that may append `generated` (§12).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ from dataclasses import dataclass
17
+ from typing import Any, Callable
18
+
19
+ from patternbuffer.buffer import PatternBuffer
20
+ from patternbuffer.classify import CONSTITUTIVE, DISPOSITIONAL, Classifier
21
+ from patternbuffer.codec import json_default
22
+ from patternbuffer.indexes import Indexes
23
+ from patternbuffer.model import CANON, Assertion
24
+ from patternbuffer.roles import WriterRole
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ INVENT_UNDER_CANON = "invent_under_canon"
29
+ OBSERVE_OR_UNKNOWN = "observe_or_unknown"
30
+ DENY = "deny"
31
+ POLICIES = frozenset({INVENT_UNDER_CANON, OBSERVE_OR_UNKNOWN, DENY})
32
+
33
+ UNKNOWN = object() # the honest answer in observe_or_unknown worlds
34
+
35
+ _RESOLVE_SCHEMA = {
36
+ "type": "object",
37
+ "properties": {
38
+ "items": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": "object",
42
+ "properties": {
43
+ "value": {},
44
+ "detail": {"type": "string"},
45
+ },
46
+ "required": ["value"],
47
+ },
48
+ }
49
+ },
50
+ "required": ["items"],
51
+ }
52
+
53
+
54
+ class ResolutionDenied(PermissionError):
55
+ """The thunk's policy refuses resolution (deny/reserve)."""
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class ThunkView:
60
+ entity: str
61
+ aspect: str
62
+ policy: str
63
+ constraints: tuple[str, ...]
64
+ assertion_id: str
65
+
66
+
67
+ class Resolver:
68
+ def __init__(
69
+ self,
70
+ buffer: PatternBuffer,
71
+ classifier: Classifier,
72
+ indexes: Indexes,
73
+ role: WriterRole,
74
+ model: Callable[[str, dict], Any],
75
+ world_policy: str = INVENT_UNDER_CANON,
76
+ ) -> None:
77
+ if world_policy not in POLICIES:
78
+ raise ValueError(f"unknown policy {world_policy!r}")
79
+ self._buffer = buffer
80
+ self._classifier = classifier
81
+ self._indexes = indexes
82
+ self._role = role
83
+ self._model = model
84
+ self._world_policy = world_policy
85
+
86
+ # ----------------------------------------------------------- the table
87
+
88
+ def thunk_table(self, frame: str = CANON) -> list[ThunkView]:
89
+ """The frontier: open (un-superseded, unresolved) aspects."""
90
+ out = []
91
+ for row in self._buffer.visible(frame=frame):
92
+ if row.value_type != "unresolved":
93
+ continue
94
+ result = self._indexes.fold_key(row.entity, row.attribute, frame)
95
+ if result.winner is None or result.winner.id != row.id:
96
+ continue # superseded: resolved or overwritten
97
+ if self._resolved_marker(row) is not None:
98
+ continue
99
+ spec = row.value if isinstance(row.value, dict) else {}
100
+ out.append(
101
+ ThunkView(
102
+ entity=row.entity,
103
+ aspect=row.attribute,
104
+ policy=spec.get("policy", self._world_policy),
105
+ constraints=tuple(spec.get("constraints", ())),
106
+ assertion_id=row.id,
107
+ )
108
+ )
109
+ return out
110
+
111
+ def _resolved_marker(self, thunk_row: Assertion) -> Assertion | None:
112
+ markers = self._buffer.visible(entity=thunk_row.id, attribute="resolved_by")
113
+ return markers[0] if markers else None
114
+
115
+ # ------------------------------------------------------------- forcing
116
+
117
+ def resolve(
118
+ self,
119
+ entity: str,
120
+ aspect: str,
121
+ frame: str = CANON,
122
+ access: object | None = None,
123
+ ) -> list[Assertion] | object:
124
+ """Force a thunk per policy. Memoized: a second force serves the
125
+ cache. `access` is the observer-position seam (spec §9.2) —
126
+ accepted, not yet exercised in the spike."""
127
+ thunk = self._find_thunk_row(entity, aspect, frame)
128
+
129
+ # Memoized: a spent thunk serves its cache, forever, identically.
130
+ if thunk is not None:
131
+ marker = self._resolved_marker(thunk)
132
+ if marker is not None:
133
+ ids = marker.value if isinstance(marker.value, list) else [marker.value]
134
+ return [self._buffer.get(i) for i in ids]
135
+
136
+ # Concrete state on the key answers without any thunk machinery.
137
+ fold = self._indexes.fold_key(entity, aspect, frame)
138
+ if fold.winner is not None and fold.winner.value_type != "unresolved":
139
+ return [fold.winner]
140
+
141
+ if thunk is None:
142
+ return UNKNOWN if self._world_policy == OBSERVE_OR_UNKNOWN else []
143
+
144
+ spec = thunk.value if isinstance(thunk.value, dict) else {}
145
+ policy = spec.get("policy", self._world_policy)
146
+ if policy == DENY:
147
+ raise ResolutionDenied(f"{entity}·{aspect} is sealed (policy=deny)")
148
+ if policy == OBSERVE_OR_UNKNOWN:
149
+ return UNKNOWN # never invents; only observation resolves
150
+ return self._invent(thunk, spec, frame)
151
+
152
+ def _find_thunk_row(self, entity: str, aspect: str, frame: str) -> Assertion | None:
153
+ canonical = self._indexes.resolve_entity(entity)
154
+ rows = [
155
+ r
156
+ for r in self._buffer.visible(attribute=aspect, frame=frame)
157
+ if r.value_type == "unresolved"
158
+ and self._indexes.resolve_entity(r.entity) == canonical
159
+ ]
160
+ return rows[-1] if rows else None
161
+
162
+ def _invent(self, thunk_row: Assertion, spec: dict, frame: str) -> list[Assertion]:
163
+ constraints = self._inherited_constraints(thunk_row.entity, frame)
164
+ constraints += [str(c) for c in spec.get("constraints", ())]
165
+ prompt = (
166
+ f"Resolve an unestablished aspect of a fictional world: the "
167
+ f"{thunk_row.attribute} of {thunk_row.entity}.\n"
168
+ "Invent content CONSISTENT WITH every constraint below; introduce "
169
+ "nothing that contradicts them. Plain, concrete items only.\n\n"
170
+ "Constraints (canon, in force):\n- " + "\n- ".join(constraints or ["(none)"])
171
+ )
172
+ out = self._model(prompt, _RESOLVE_SCHEMA)
173
+ appended: list[Assertion] = []
174
+ if thunk_row.attribute == "contents":
175
+ # Contents are never literal rows on one key (P2: emptiness and
176
+ # contents derive from the tree). Invention mints entities with
177
+ # containment edges; the contents query serves them forever.
178
+ for item in out["items"]:
179
+ eid = f"obj:gen_{self._buffer.head() + 1}"
180
+ kind_row = self._buffer.append(
181
+ entity=eid, attribute="kind", value=str(item.get("kind", "object")),
182
+ frame=frame, status="generated", role=self._role,
183
+ )
184
+ name_row = self._buffer.append(
185
+ entity=eid, attribute="name", value=str(item["value"]),
186
+ frame=frame, status="generated", role=self._role,
187
+ )
188
+ edge = self._buffer.append(
189
+ entity=eid, attribute="in", value=thunk_row.entity,
190
+ value_type="entity", valid_from=thunk_row.valid_from,
191
+ frame=frame, status="generated", role=self._role,
192
+ )
193
+ self._classifier.classify(kind_row) # guardrail: CONSTITUTIVE
194
+ self._classifier.classify(name_row) # guardrail: CONSTITUTIVE
195
+ # The resolver holds the world context here: invented
196
+ # contents are movables. Judgment injected, log untouched.
197
+ self._classifier.set(edge.id, "STATE")
198
+ appended.append(edge)
199
+ else:
200
+ for item in out["items"]:
201
+ row = self._buffer.append(
202
+ entity=thunk_row.entity,
203
+ attribute=thunk_row.attribute,
204
+ value=item["value"],
205
+ value_type="literal",
206
+ valid_from=thunk_row.valid_from,
207
+ frame=frame,
208
+ status="generated",
209
+ role=self._role,
210
+ )
211
+ self._classifier.classify(row)
212
+ appended.append(row)
213
+ marker = self._buffer.append(
214
+ entity=thunk_row.id,
215
+ attribute="resolved_by",
216
+ value=[a.id for a in appended],
217
+ status="generated",
218
+ role=self._role,
219
+ )
220
+ self._classifier.classify(marker) # closed under its own operations
221
+ logger.info(
222
+ "resolved %s·%s -> %d generated row(s)",
223
+ thunk_row.entity, thunk_row.attribute, len(appended),
224
+ )
225
+ return appended
226
+
227
+ def _inherited_constraints(self, entity: str, frame: str) -> list[str]:
228
+ """Constraint inheritance: CONSTITUTIVE + DISPOSITIONAL rows of the
229
+ entity and every containment ancestor (whitepaper §8)."""
230
+ constraints: list[str] = []
231
+ scope = [entity, *self._indexes.locate(entity, frame=frame)]
232
+ for holder in scope:
233
+ for attr, result in self._indexes.current_state(holder, frame=frame).items():
234
+ row = result.winner
235
+ if row is None or row.value_type == "unresolved":
236
+ continue
237
+ if result.quantity is not None:
238
+ continue # an accrue running total is not a scene-invention
239
+ # constraint, and its winner is a delta row, never
240
+ # the value (never inherit a delta as a constraint)
241
+ if self._classifier.durability(row.id) in {CONSTITUTIVE, DISPOSITIONAL}:
242
+ constraints.append(
243
+ f"{holder} · {attr} · {json.dumps(row.value, default=json_default)}"
244
+ )
245
+ return constraints
@@ -0,0 +1,122 @@
1
+ """Truth maintenance: conflict detection (derived) and retraction (log).
2
+
3
+ A fired flag is a sidecar row pointing at the assertions in tension —
4
+ both rows coexist in the log untouched (whitepaper §5.1/§7.2). The flag
5
+ stands until an explicit retraction/supersession resolves it; resolution
6
+ is never required for the flag to exist, and a silent merge is the
7
+ failure the chapter test's feature 8 exists to catch.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from dataclasses import dataclass
14
+
15
+ from patternbuffer.buffer import PatternBuffer
16
+ from patternbuffer.classify import Classifier
17
+ from patternbuffer.indexes import Indexes
18
+ from patternbuffer.model import ATTR_PREFIX, Assertion
19
+ from patternbuffer.roles import WriterRole
20
+ from patternbuffer.semantics import AttributeSemantics
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ _SCHEMA = """
25
+ CREATE TABLE IF NOT EXISTS sidecar_conflicts (
26
+ key_entity TEXT NOT NULL,
27
+ key_attribute TEXT NOT NULL,
28
+ key_frame TEXT NOT NULL,
29
+ kind TEXT NOT NULL,
30
+ assertion_ids TEXT NOT NULL,
31
+ PRIMARY KEY (key_entity, key_attribute, key_frame, kind)
32
+ );
33
+ """
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class Conflict:
38
+ entity: str
39
+ attribute: str
40
+ frame: str
41
+ kind: str # 'constitutive_contradiction' | 'cross_source'
42
+ assertion_ids: tuple[str, ...]
43
+
44
+
45
+ class TruthMaintenance:
46
+ def __init__(
47
+ self,
48
+ buffer: PatternBuffer,
49
+ classifier: Classifier,
50
+ indexes: Indexes,
51
+ role: WriterRole,
52
+ semantics: AttributeSemantics | None = None,
53
+ ) -> None:
54
+ self._buffer = buffer
55
+ self._classifier = classifier
56
+ self._indexes = indexes
57
+ self._role = role
58
+ self._semantics = semantics or AttributeSemantics(buffer)
59
+ buffer.raw_connection().executescript(_SCHEMA)
60
+
61
+ def scan(self) -> list[Conflict]:
62
+ """Re-derive the conflict table from the log. Rebuildable: the
63
+ table is dropped and refilled on every scan."""
64
+ keys: set[tuple[str, str, str]] = set()
65
+ for row in self._buffer.visible():
66
+ if (
67
+ row.entity.startswith("a:")
68
+ or row.entity.startswith(ATTR_PREFIX)
69
+ or self._semantics.is_set_valued(row.attribute)
70
+ ):
71
+ continue
72
+ fold_attr = self._indexes.fold_attribute(row.attribute)
73
+ keys.add(
74
+ (
75
+ self._indexes._resolve(row.entity),
76
+ "in" if fold_attr == self._indexes.fold_attribute("in") else row.attribute,
77
+ row.frame,
78
+ )
79
+ )
80
+ conflicts: list[Conflict] = []
81
+ for entity, attribute, frame in sorted(keys):
82
+ result = self._indexes.fold_key(entity, attribute, frame)
83
+ if result.conflicted:
84
+ kind = (
85
+ "constitutive_contradiction"
86
+ if result.winner is not None
87
+ and self._classifier.durability(result.winner.id) == "CONSTITUTIVE"
88
+ else "cross_source"
89
+ )
90
+ conflicts.append(
91
+ Conflict(entity, attribute, frame, kind, result.conflicting)
92
+ )
93
+ conn = self._buffer.raw_connection()
94
+ conn.execute("DELETE FROM sidecar_conflicts")
95
+ for c in conflicts:
96
+ conn.execute(
97
+ "INSERT OR REPLACE INTO sidecar_conflicts VALUES (?, ?, ?, ?, ?)",
98
+ (c.entity, c.attribute, c.frame, c.kind, ",".join(c.assertion_ids)),
99
+ )
100
+ conn.commit()
101
+ if conflicts:
102
+ logger.info("truth maintenance: %d open conflict(s)", len(conflicts))
103
+ return conflicts
104
+
105
+ def open_conflicts(self) -> list[Conflict]:
106
+ rows = self._buffer.raw_connection().execute(
107
+ "SELECT key_entity, key_attribute, key_frame, kind, assertion_ids"
108
+ " FROM sidecar_conflicts ORDER BY key_entity"
109
+ ).fetchall()
110
+ return [Conflict(r[0], r[1], r[2], r[3], tuple(r[4].split(","))) for r in rows]
111
+
112
+ def retract(self, target: Assertion | str, reason: str) -> Assertion:
113
+ """Append a retraction meta-assertion. The target survives in the
114
+ log; folds exclude it from the retraction's asserted_at onward."""
115
+ target_id = target if isinstance(target, str) else target.id
116
+ return self._buffer.append(
117
+ entity=target_id,
118
+ attribute="retracts",
119
+ value=reason,
120
+ status="retracted",
121
+ role=self._role,
122
+ )
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: pbuffer
3
+ Version: 0.1.0
4
+ Summary: A world-state database: an append-only log of everything true in a world - places, people, objects, knowledge, time. Deploy it under a game, AI agent, or real-world tracker for permanent, queryable world memory: current state, location, history, and who-knows-what, answered by deterministic queries instead of model calls.
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=8; extra == "dev"
11
+ Provides-Extra: mcp
12
+ Requires-Dist: mcp<2,>=1.28; extra == "mcp"
13
+ Dynamic: license-file
14
+
15
+ # pattern-buffer
16
+
17
+ [![tests](https://github.com/5000Stadia/pattern-buffer/actions/workflows/ci.yml/badge.svg)](https://github.com/5000Stadia/pattern-buffer/actions/workflows/ci.yml)
18
+
19
+ > A data structure that maps whatever world it's shown — real or fiction.
20
+ > The memory under the holodeck.
21
+
22
+ **A pattern buffer that never degrades.**
23
+
24
+ One append-only log of perspective-scoped, time-indexed assertions about entities; every other structure — current state, space, knowledge, history, the rendered world — is a disposable projection over it. Fiction simulation and real-world tracking are the same machine under one policy switch.
25
+
26
+ **A world-encapsulation framework, engine-independent by construction.** The substrate's only outside dependency is an injected model callable `(prompt, schema) -> json` — no host concept (players, turns, scenes, sessions) ever reaches inside it — so any engine sits on top: a game runtime, a household or job-site tracker, an agent's long-term memory. Fiction and tracking aren't two systems; they are two *instances* of one substrate, differing only in resolution policy and provenance discipline.
27
+
28
+ > A player places a pipe in a drawer in session 12 of a D&D campaign. Two hundred sessions pass without a mention. At retirement, the player opens the drawer — and the pipe is there, exactly as placed, with the original moment's full history behind it. No maintenance was ever performed. In this architecture, **silence is persistence**: state is folded from the log, never re-inferred and never kept alive by mention. The world remembers so the model doesn't have to.
29
+
30
+ **Status: pre-1.0; substrate validated, ingestion fidelity is the open
31
+ front.** The engine passes its 419-test invariant suite, and the chapter test
32
+ (ingest a complete noir mystery, delete the prose, interrogate the store
33
+ against a hand-authored answer key) has been graded across three full runs.
34
+ The substrate's invariants held in all of them — sealed containers stayed
35
+ sealed, contradictions were flagged and never merged, knowledge frames never
36
+ leaked, the two-time-axis fold answered late-revealed history correctly. Each
37
+ run also surfaced exactly one engine refinement at the fold (a working
38
+ assumption outholding evidence; a wrong inference outholding authored canon),
39
+ each fixed, tested, and generalized the same day — that progression is now
40
+ the **evidence-rank** rule. A registry-first ingestion pipeline
41
+ (whole-document scaffold → parallel compact-grammar extraction → audited
42
+ single commit) replaced serial prompt-iterated extraction after measurement
43
+ showed prose contracts fragment differently every round; identity and key
44
+ fragmentation died as failure classes, and pass-0 registry quality is where
45
+ the remaining extraction failures concentrate. Run 4, with its checkable
46
+ predictions, is in flight. Not yet installable from PyPI; the shipped example
47
+ world with a zero-API-key query tour lands once a verified run stamps it.
48
+
49
+ ## What this is
50
+
51
+ A storage and retrieval substrate that lets a language model maintain a complete, queryable, durable model of a world — places, objects, people, relationships, histories, and unknowns — such that any state question is answered by a deterministic query, never by re-reading prose; the world accretes and is re-enterable; and the same engine serves authored fiction (a holodeck, a campaign, a mystery) and real-world tracking (a household, a job site, a vehicle), differing only in resolution policy and provenance discipline.
52
+
53
+ The buffer holds the pattern; materialization is re-entry; degradation is the failure we exist to prevent; the arch is how the operator steps outside the world to inspect it; and where nothing has been established, you don't get invented detail — **you see the grid.**
54
+
55
+ ## Documents
56
+
57
+ - **[docs/WHITEPAPER.md](docs/WHITEPAPER.md)** — the canonical, comprehensive design reference. Principles, primitives, durability, frames, provenance, thunks, reference resolution, ingestion discipline, identity, the operation algebra, projection, both modes, embedding hazards, evaluation, and the decision record. Start here.
58
+ - **[docs/LEXICON.md](docs/LEXICON.md)** — the working vocabulary and its naming discipline.
59
+ - **[docs/ADOPTION.md](docs/ADOPTION.md)** — for agents (and humans) integrating the library: exact signatures, typed outcomes, the three-seam recipe, MUST/NEVER rules.
60
+ - **[docs/HOST-DISCIPLINE.md](docs/HOST-DISCIPLINE.md)** — the adopter's discipline brief: the seven categories every fact must be classified along to ingest with fidelity, and the structural retrieval strategies (the four correlation axes + the correlation sweep) to surface every relevant, related detail for any subject.
61
+ - **[docs/INGESTION-PLAYBOOK.md](docs/INGESTION-PLAYBOOK.md)** — evidence-based rules for deserializing narrative into a world: the extractor contract, feeding mechanics, what the gate guards, and measured ingestion baselines.
62
+ - **[docs/reference/assertion-world-model-original.md](docs/reference/assertion-world-model-original.md)** — the framework-agnostic design document this project was founded on, preserved verbatim for lineage.
63
+
64
+ ## The road
65
+
66
+ 1. ~~Recover and commit the *Last Honest Meter* eval seed (the chapter test's ground truth).~~ Done — v1-final, `evals/last_honest_meter/`.
67
+ 2. ~~Engine spike: `PatternBuffer` + derived indexes + classifier + projector + resolver + `refer()` tier 1.~~ Done — `src/patternbuffer/`.
68
+ 3. ~~The chapter test, runs 1–4~~: substrate validated; registry-first ingestion (INGEST-V2) landed; run 4 scored **22/33 (best graded run, all remaining failures extraction-class not shape-class)** — the substrate holds; ingestion fidelity (pass-0 registry quality) is the open score lever. Receipts: `evals/results/`.
69
+ 4. ~~The shipped example world: `examples/anchor/`~~ — stamped (`STAMP.json`, run 4): a bible-verified noir mystery as a queryable database, with a zero-API-key scripted tour and 8 open truth-maintenance conflict flags shipped visibly (honesty is the product). Not yet packaged to PyPI.
70
+ 5. ~~The messy-dialogue micro-eval (the honest bridge from fiction ingestion to tracking-mode ingestion).~~ Done — **10/10** on the reality-divergence battery (`evals/results/2026-06-12-micro-v1-final/`); tracking-mode ingestion validated against sloppy conversational fragments.
71
+ 6. ~~The porcelain API + host integration.~~ Done — and proven in the strongest form. The frozen contract (`porcelain-v0.1`, additive-only): five load-bearing verbs — `ingest` (+`ingest_structured`) / `snapshot` / `ask` / `materialize` / `resolve`+`refer()` — over a fuller read-and-identity family (`state`, `where`, `aggregate`, `neighborhood`, `frame_diff`, `who_knows`, `correlate`, `route`, `entities`, `facts`, build sessions, `axis_heads`, …). The first *live* host, [Construct](https://github.com/5000Stadia/construct) (an interactive-fiction engine — `pattern` → `construct` loads it → `holonovel`), runs **entirely on this contract**: zero engine-internal reaches, its 800+-test suite green on the pure public surface. The engine-independent claim, demonstrated rather than asserted. (Intended primary host: [Kernos](https://github.com/5000Stadia/Kernos), adapter pattern.)
72
+ 7. ~~The interactive-fiction milestone (thunk stability across sessions, frame-scoped NPCs, multi-path mystery, clocks, loop closure).~~ Met in the field — every item is exercised in [Construct](https://github.com/5000Stadia/construct)'s live suite (the first host runs entirely on the frozen porcelain). These are host-layer criteria, proven by the adopter, not a further engine deliverable.
73
+ 8. ~~The MCP wrapper~~ — built: `pip install pbuffer[mcp]` → `patternbuffer-mcp --world w.world --world-id w:id` serves the frozen porcelain's **37 deterministic verbs** over stdio to any MCP client — read, query, and *build* a world with zero Python (the genuinely model-free subset; model-backed verbs are V1.1 via sampling). One server ↔ one world; a connected client is a fully **trusted world principal** — frame entitlement for untrusted consumers stays host-mediated. The framework-agnostic claim, proven by demonstration. Far field: the first host's V2 roadmap publicly assigns branch-worlds simulation and its curiosity loop to this engine — the two roadmaps meet in the middle.
74
+
75
+ ## License
76
+
77
+ MIT
@@ -0,0 +1,25 @@
1
+ patternbuffer/__init__.py,sha256=TldvxbxoDG2Uh5ZWic5vae41Lk2ygFV2nDtD0McCp2Y,14084
2
+ patternbuffer/buffer.py,sha256=EHwZ0dZpugw_pEcmw4alKldBcx-lpT6wa1zMHpBM34k,11502
3
+ patternbuffer/classify.py,sha256=UWclf-HX8NXvv90QdWhyPPrPNhkYPNhtbKlKoPSS8Kc,15829
4
+ patternbuffer/codec.py,sha256=yitrIOZKvzQwkUWDXfyXgl3ISZyaw7lqIsQ5TmaxY74,4544
5
+ patternbuffer/dump.py,sha256=LUmh9FWu1JyOg5qvsqwcKrE84f6jMJ_lcgMIwydJrTI,4152
6
+ patternbuffer/identity.py,sha256=lAa0lGp4YbPTGd5DjCCq8cwqmgwzdLHevbiw9eVtOss,62237
7
+ patternbuffer/indexes.py,sha256=vaGPmTsGnsRSndKbpvqC4VylrxpdIFn6T2nxQkrJUd8,71177
8
+ patternbuffer/ingest.py,sha256=0CuyO2j2ccnq3ixE6Tcv_3rn2mjHf6mTKlFjbDB48Ug,31479
9
+ patternbuffer/mcp.py,sha256=RNq7CrbMs0XvO0e3quGZdZDerDTeRBGzaW9lSsAtSf8,12707
10
+ patternbuffer/model.py,sha256=KD1qyvKGYaMfQqu_4IP_Z8gTBk80tTRDz_SvTMYwKB4,3594
11
+ patternbuffer/porcelain.py,sha256=ho9nKGABjYBcA4rpyTfhSCjGFZlC4pROlXdebscw45E,45546
12
+ patternbuffer/project.py,sha256=p5eTP2n9NhNY_QrqQgdjgUZ1whrOjuKzPtEHoOv2wzo,20069
13
+ patternbuffer/refer.py,sha256=MHPrHCTmcqsxBKl4GHGoZHwY6kiepjsxiO1Ku5PvcvA,11482
14
+ patternbuffer/roles.py,sha256=99cMRA9OBahf6FrsNhq3jieGJvJdRaacwMBmpaQLyJA,2832
15
+ patternbuffer/salience.py,sha256=6pW5UQlJzq9Saze7n--UGRA88g_TkhPhkPt29yD6Vgc,5881
16
+ patternbuffer/semantics.py,sha256=3pdyYVCg6F7U3-8wNkBSzr__NZPLM5za2Yz8iC6_qLA,7132
17
+ patternbuffer/testing.py,sha256=PrypcXSNw1zYARNs6nmpul9YiAAHxE4vnLVeX8twFYI,3554
18
+ patternbuffer/thunks.py,sha256=14kDo0lpPv3od4gUmFA7IER9VlW097QbgrBmFrLArHw,10076
19
+ patternbuffer/tmaint.py,sha256=uoQ4-fSDmjt0tcziIRozX5aubzVu_PKWRTX2eSuL2yw,4527
20
+ pbuffer-0.1.0.dist-info/licenses/LICENSE,sha256=hdX78D_ZnB_tB1okAu3IZkSQ7EHby2xULfQv0GtKNyQ,1067
21
+ pbuffer-0.1.0.dist-info/METADATA,sha256=zaOBHCuG1ceTgTQIf9F6o37SOsBChFZuc3Emq3IHMO8,8990
22
+ pbuffer-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
23
+ pbuffer-0.1.0.dist-info/entry_points.txt,sha256=pzltldXdPgdO0roKOrnJX09uDcKzZwbEoKRP-L3DEGQ,61
24
+ pbuffer-0.1.0.dist-info/top_level.txt,sha256=jVATFoYr1S5GedARCY2mZlb48Gu4CwJwrCuL3_6HZgI,14
25
+ pbuffer-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ patternbuffer-mcp = patternbuffer.mcp:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 5000Stadia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ patternbuffer