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.
- patternbuffer/__init__.py +317 -0
- patternbuffer/buffer.py +312 -0
- patternbuffer/classify.py +359 -0
- patternbuffer/codec.py +122 -0
- patternbuffer/dump.py +113 -0
- patternbuffer/identity.py +1271 -0
- patternbuffer/indexes.py +1642 -0
- patternbuffer/ingest.py +617 -0
- patternbuffer/mcp.py +303 -0
- patternbuffer/model.py +100 -0
- patternbuffer/porcelain.py +943 -0
- patternbuffer/project.py +436 -0
- patternbuffer/refer.py +256 -0
- patternbuffer/roles.py +77 -0
- patternbuffer/salience.py +170 -0
- patternbuffer/semantics.py +192 -0
- patternbuffer/testing.py +92 -0
- patternbuffer/thunks.py +245 -0
- patternbuffer/tmaint.py +122 -0
- pbuffer-0.1.0.dist-info/METADATA +77 -0
- pbuffer-0.1.0.dist-info/RECORD +25 -0
- pbuffer-0.1.0.dist-info/WHEEL +5 -0
- pbuffer-0.1.0.dist-info/entry_points.txt +2 -0
- pbuffer-0.1.0.dist-info/licenses/LICENSE +21 -0
- pbuffer-0.1.0.dist-info/top_level.txt +1 -0
patternbuffer/testing.py
ADDED
|
@@ -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
|
patternbuffer/thunks.py
ADDED
|
@@ -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
|
patternbuffer/tmaint.py
ADDED
|
@@ -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
|
+
[](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,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
|