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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""patternbuffer: an append-only world-state substrate.
|
|
2
|
+
|
|
3
|
+
One append-only log of perspective-scoped, time-indexed assertions per
|
|
4
|
+
world; every other structure — current state, space, knowledge, history,
|
|
5
|
+
the rendered world — is a disposable projection over it.
|
|
6
|
+
|
|
7
|
+
The engine is host-blind: its single outside dependency is an injected
|
|
8
|
+
model callable ``(prompt, schema) -> json``. See docs/WHITEPAPER.md.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Callable
|
|
16
|
+
|
|
17
|
+
from patternbuffer.buffer import PatternBuffer
|
|
18
|
+
from patternbuffer.classify import Classifier
|
|
19
|
+
from patternbuffer.identity import IdentityRegistry
|
|
20
|
+
from patternbuffer.indexes import Indexes
|
|
21
|
+
from patternbuffer.ingest import Ingestor
|
|
22
|
+
from patternbuffer.model import ATTR_PREFIX, CANON, META_ATTRIBUTES
|
|
23
|
+
from patternbuffer.project import Materialization, Projector
|
|
24
|
+
from patternbuffer.refer import Refer, Resolution
|
|
25
|
+
from patternbuffer.roles import _make_engine_roles
|
|
26
|
+
from patternbuffer.salience import SalienceIndex
|
|
27
|
+
from patternbuffer.semantics import AttributeSemantics
|
|
28
|
+
from patternbuffer.thunks import (
|
|
29
|
+
INVENT_UNDER_CANON,
|
|
30
|
+
OBSERVE_OR_UNKNOWN,
|
|
31
|
+
POLICIES,
|
|
32
|
+
Resolver,
|
|
33
|
+
)
|
|
34
|
+
from patternbuffer.tmaint import TruthMaintenance
|
|
35
|
+
|
|
36
|
+
__version__ = "0.0.1"
|
|
37
|
+
|
|
38
|
+
STANCES = frozenset({"fiction", "reality", "hypothetical"}) # letter 026; fixed enum
|
|
39
|
+
__all__ = ["World", "Materialization", "Resolution", "__version__"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class World:
|
|
43
|
+
"""The named, individual unit: one PatternBuffer + its physics
|
|
44
|
+
(resolution policy, decay configuration) + derived indexes (§16).
|
|
45
|
+
|
|
46
|
+
``World("campaign.world", world_id="w:campaign", model=callable)`` is
|
|
47
|
+
the entire harness interface — a parameter, not a framework. Role
|
|
48
|
+
capabilities are minted here and nowhere else.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
path: str | Path,
|
|
54
|
+
world_id: str,
|
|
55
|
+
model: Callable[[str, dict], Any] | None = None,
|
|
56
|
+
policy: str = INVENT_UNDER_CANON,
|
|
57
|
+
clock: Callable[[], float] = time.time,
|
|
58
|
+
stance: str = "fiction",
|
|
59
|
+
title: str = "",
|
|
60
|
+
description: str = "",
|
|
61
|
+
attribute_default: Callable[[str], dict | None] | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
if policy not in POLICIES:
|
|
64
|
+
raise ValueError(f"unknown policy {policy!r}")
|
|
65
|
+
if stance not in STANCES:
|
|
66
|
+
raise ValueError(f"unknown stance {stance!r} (fixed enum; letter 026)")
|
|
67
|
+
self.world_id = world_id
|
|
68
|
+
self.policy = policy
|
|
69
|
+
self.buffer = PatternBuffer(path, world_id)
|
|
70
|
+
self.semantics = AttributeSemantics(self.buffer)
|
|
71
|
+
roles = _make_engine_roles()
|
|
72
|
+
self.classifier = Classifier(self.buffer, model or _no_model, self.semantics)
|
|
73
|
+
self.registry = IdentityRegistry(self.buffer, roles["ingestor"], self.semantics)
|
|
74
|
+
self.indexes = Indexes(
|
|
75
|
+
self.buffer, self.classifier, self.registry.resolve, self.semantics
|
|
76
|
+
)
|
|
77
|
+
self.indexes.set_closure_provider(self.registry.closure)
|
|
78
|
+
self.indexes.set_correlation_provider(self.registry.correlation_set)
|
|
79
|
+
self.registry.set_kind_provider(lambda e: self.indexes.fold_key(e, "kind"))
|
|
80
|
+
self.salience_index = SalienceIndex(self.buffer, self.classifier, self.indexes)
|
|
81
|
+
self.indexes.set_salience_provider(self.salience_index.salience)
|
|
82
|
+
self.truth = TruthMaintenance(
|
|
83
|
+
self.buffer, self.classifier, self.indexes, roles["truth_maintenance"],
|
|
84
|
+
self.semantics,
|
|
85
|
+
)
|
|
86
|
+
# SHAPE-FIX-V1 retype wiring: retraction authority + rules-mode
|
|
87
|
+
# durability for the corrected kind row (the set_kind_provider idiom —
|
|
88
|
+
# truth/classifier post-date the registry).
|
|
89
|
+
self.registry.set_retract_provider(self.truth.retract)
|
|
90
|
+
self.registry.set_classify_provider(
|
|
91
|
+
lambda rows: self.classifier.classify_rows(rows, model=False)
|
|
92
|
+
)
|
|
93
|
+
# Win 4 (durable-contradiction veto, HD 089): the registry reads
|
|
94
|
+
# standing folds + durability verdicts through late-bound lookups.
|
|
95
|
+
self.registry.set_fold_provider(self.indexes.fold_key)
|
|
96
|
+
self.registry.set_durability_provider(self.classifier.durability)
|
|
97
|
+
self.resolver = Resolver(
|
|
98
|
+
self.buffer, self.classifier, self.indexes, roles["resolver"],
|
|
99
|
+
model or _no_model, policy,
|
|
100
|
+
)
|
|
101
|
+
self.projector = Projector(
|
|
102
|
+
self.buffer, self.classifier, self.indexes, self.semantics,
|
|
103
|
+
self.salience_index.salience,
|
|
104
|
+
)
|
|
105
|
+
self.ingestor = Ingestor(
|
|
106
|
+
self.buffer, self.classifier, self.registry, roles["ingestor"],
|
|
107
|
+
model, observe_mode=(policy == OBSERVE_OR_UNKNOWN), clock=clock,
|
|
108
|
+
resolver_role=roles["resolver"],
|
|
109
|
+
semantics=self.semantics,
|
|
110
|
+
attribute_default=attribute_default,
|
|
111
|
+
# Write-time containment-cycle gate (HD 002): the ancestor walk
|
|
112
|
+
# is the derived containment chain — locate folds the family.
|
|
113
|
+
containment_ancestors=lambda parent, frame, vf: set(
|
|
114
|
+
self.indexes.locate(parent, frame=frame, valid_as_of=vf)
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
self.refer = Refer(self.buffer, self.indexes, self.registry, model,
|
|
118
|
+
ingestor=self.ingestor)
|
|
119
|
+
# The World Charter (letter 026): a fresh world's genesis write
|
|
120
|
+
# asserts its self-description — stance is ontological stored
|
|
121
|
+
# truth (does this world claim to describe reality?), distinct
|
|
122
|
+
# from operational policy. Ordinary appends; amendable forever.
|
|
123
|
+
if self.buffer.head() == 0:
|
|
124
|
+
charter = [
|
|
125
|
+
{"entity": "world:self", "attribute": "kind", "value": "world",
|
|
126
|
+
"timeless": True},
|
|
127
|
+
{"entity": "world:self", "attribute": "stance", "value": stance,
|
|
128
|
+
"timeless": True},
|
|
129
|
+
]
|
|
130
|
+
if title:
|
|
131
|
+
charter.append({"entity": "world:self", "attribute": "title",
|
|
132
|
+
"value": title, "timeless": True})
|
|
133
|
+
if description:
|
|
134
|
+
charter.append({"entity": "world:self", "attribute": "description",
|
|
135
|
+
"value": description, "timeless": True})
|
|
136
|
+
self.ingestor.classify_inline = False
|
|
137
|
+
self.ingest_structured(charter)
|
|
138
|
+
self.ingestor.classify_inline = True
|
|
139
|
+
for row in self.buffer.all_rows():
|
|
140
|
+
self.classifier.set(row.id, "CONSTITUTIVE", 1.0)
|
|
141
|
+
|
|
142
|
+
# Reads (deterministic, no LLM — P7).
|
|
143
|
+
|
|
144
|
+
def locate(self, entity: str, **kw) -> list[str]:
|
|
145
|
+
return self.indexes.locate(entity, **kw)
|
|
146
|
+
|
|
147
|
+
def contents(self, container: str, **kw) -> list[str]:
|
|
148
|
+
return self.indexes.contents(container, **kw)
|
|
149
|
+
|
|
150
|
+
# PLACE-FEATURE-ABSTRACTION-V1 — the compositional axis (part_of).
|
|
151
|
+
def composition(self, entity: str, **kw) -> list[str]:
|
|
152
|
+
return self.indexes.composition(entity, **kw)
|
|
153
|
+
|
|
154
|
+
def features(self, place: str, **kw) -> list[str]:
|
|
155
|
+
return self.indexes.features(place, **kw)
|
|
156
|
+
|
|
157
|
+
# WHO-KNOWS-INVERSE-V1 — the computed "who knows X" read.
|
|
158
|
+
def who_knows(self, entity: str, attribute: str, value: Any = None, **kw) -> list[str]:
|
|
159
|
+
return self.indexes.who_knows(entity, attribute, value, **kw)
|
|
160
|
+
|
|
161
|
+
def state(self, entity: str, attribute: str, frame: str = CANON, **kw):
|
|
162
|
+
return self.indexes.fold_key(entity, attribute, frame, **kw)
|
|
163
|
+
|
|
164
|
+
# AKA-CORRELATION-V1 — the explicit, opt-in correlated identity surface.
|
|
165
|
+
def state_union(self, entity: str, attribute: str, frame: str = CANON, **kw):
|
|
166
|
+
return self.indexes.state_union(entity, attribute, frame, **kw)
|
|
167
|
+
|
|
168
|
+
def correlations(self, entity: str, **kw) -> list[str]:
|
|
169
|
+
return self.registry.correlations(entity, **kw)
|
|
170
|
+
|
|
171
|
+
def correlate(self, a: str, b: str, evidence: str, **kw) -> dict:
|
|
172
|
+
return self.registry.correlate(a, b, evidence, **kw)
|
|
173
|
+
|
|
174
|
+
def correlation_conflicts(self, **kw) -> list[dict]:
|
|
175
|
+
return self.registry.correlation_conflicts(**kw)
|
|
176
|
+
|
|
177
|
+
def path(self, a: str, b: str, **kw) -> list[str] | None:
|
|
178
|
+
return self.indexes.path(a, b, **kw)
|
|
179
|
+
|
|
180
|
+
def route(self, a: str, b: str, **kw) -> dict:
|
|
181
|
+
return self.indexes.route(a, b, **kw)
|
|
182
|
+
|
|
183
|
+
def salience(
|
|
184
|
+
self, entity: str, frame: str = CANON, as_of: float | None = None
|
|
185
|
+
) -> float:
|
|
186
|
+
return self.salience_index.salience(entity, frame, as_of)
|
|
187
|
+
|
|
188
|
+
def confidence(
|
|
189
|
+
self,
|
|
190
|
+
entity: str,
|
|
191
|
+
attribute: str,
|
|
192
|
+
frame: str | list[str] = CANON,
|
|
193
|
+
as_of: float | None = None,
|
|
194
|
+
asserted_as_of: int | None = None,
|
|
195
|
+
) -> dict:
|
|
196
|
+
return self.indexes.confidence(
|
|
197
|
+
entity, attribute, frame=frame, as_of=as_of,
|
|
198
|
+
asserted_as_of=asserted_as_of,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def neighborhood(self, entity: str, **kw) -> dict:
|
|
202
|
+
return self.indexes.neighborhood(entity, **kw)
|
|
203
|
+
|
|
204
|
+
def aggregate(self, container: str, member_attribute: str, op: str, **kw) -> dict:
|
|
205
|
+
return self.indexes.aggregate(container, member_attribute, op, **kw)
|
|
206
|
+
|
|
207
|
+
def fidelity_audit(self, frame: str = CANON, as_of: float | None = None) -> dict:
|
|
208
|
+
"""Structural ingestion-fidelity gaps, derived (INGESTION-FIDELITY-V1).
|
|
209
|
+
Read-only; run after a build's classification + `truth.scan()` (it never
|
|
210
|
+
classifies or scans). The engine surfaces gaps; the host joins arc/cast
|
|
211
|
+
severity and drives targeted re-extraction."""
|
|
212
|
+
from patternbuffer.classify import EVENT, STATE
|
|
213
|
+
|
|
214
|
+
collisions = self.registry.name_collisions(frame=frame, valid_as_of=as_of)
|
|
215
|
+
|
|
216
|
+
unstamped: list[dict] = []
|
|
217
|
+
entities: set[str] = set()
|
|
218
|
+
for row in self.buffer.visible(frame=frame):
|
|
219
|
+
if row.entity.startswith("a:") or row.entity.startswith(ATTR_PREFIX):
|
|
220
|
+
continue
|
|
221
|
+
eid = self.registry.resolve(row.entity)
|
|
222
|
+
if eid.startswith(("obj:", "person:")):
|
|
223
|
+
entities.add(eid)
|
|
224
|
+
# Meta/identity edges (same_as/distinct_from/aka/source…) are
|
|
225
|
+
# classified EVENT and carry no valid_from BY DESIGN — bookkeeping,
|
|
226
|
+
# never a spine fact; they are not an unstamped gap.
|
|
227
|
+
if row.attribute in META_ATTRIBUTES:
|
|
228
|
+
continue
|
|
229
|
+
if row.valid_from is None and self.classifier.get(row.id) is not None \
|
|
230
|
+
and self.classifier.durability(row.id) in (STATE, EVENT):
|
|
231
|
+
unstamped.append({"entity": eid, "attribute": row.attribute,
|
|
232
|
+
"assertion_id": row.id})
|
|
233
|
+
|
|
234
|
+
orphans = sorted(
|
|
235
|
+
e for e in entities
|
|
236
|
+
if not self.indexes.locate(e, frame=frame, valid_as_of=as_of)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
conflicts = [
|
|
240
|
+
{"entity": c.entity, "attribute": c.attribute, "frame": c.frame,
|
|
241
|
+
"kind": c.kind, "assertion_ids": list(c.assertion_ids)}
|
|
242
|
+
for c in self.truth.open_conflicts()
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
live = sum(1 for g in collisions if g["live"])
|
|
246
|
+
return {
|
|
247
|
+
"frame": frame,
|
|
248
|
+
"name_collisions": collisions,
|
|
249
|
+
"unstamped_timed": unstamped,
|
|
250
|
+
"orphan_entities": orphans,
|
|
251
|
+
"open_conflicts": conflicts,
|
|
252
|
+
"summary": {
|
|
253
|
+
"name_collisions": live, # live-fragmentation groups only
|
|
254
|
+
"name_collisions_total": len(collisions),
|
|
255
|
+
"unstamped_timed": len(unstamped),
|
|
256
|
+
"orphan_entities": len(orphans),
|
|
257
|
+
"open_conflicts": len(conflicts),
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
def materialize(self, scope, **kw) -> Materialization:
|
|
262
|
+
return self.projector.materialize(scope, **kw)
|
|
263
|
+
|
|
264
|
+
# Writes (each behind its role).
|
|
265
|
+
|
|
266
|
+
def ingest(self, text: str, context: str = "", frame: str | None = None,
|
|
267
|
+
classify: str = "inline", extract: str = "full",
|
|
268
|
+
cursor_authoritative: bool = False, pov: str | None = None) -> list:
|
|
269
|
+
return self.ingestor.ingest(text, context, frame=frame, classify=classify,
|
|
270
|
+
extract=extract,
|
|
271
|
+
cursor_authoritative=cursor_authoritative,
|
|
272
|
+
pov=pov)
|
|
273
|
+
|
|
274
|
+
def extract(self, text: str, context: str = "", extract: str = "full",
|
|
275
|
+
pov: str | None = None) -> list:
|
|
276
|
+
"""Read-only extraction (INGEST-LATENCY-V2): the host parallelizes these,
|
|
277
|
+
then ingest_structured()s the results serially. No write. `pov`
|
|
278
|
+
(SHAPE-FIX-V1 4c): the viewpoint entity id for deixis binding."""
|
|
279
|
+
return self.ingestor.extract(text, context, extract=extract, pov=pov)
|
|
280
|
+
|
|
281
|
+
def ingest_structured(self, items: list[dict], frame: str | None = None,
|
|
282
|
+
classify: str = "inline",
|
|
283
|
+
cursor_authoritative: bool = False) -> list:
|
|
284
|
+
return self.ingestor.ingest_structured(
|
|
285
|
+
items, frame=frame, classify=classify,
|
|
286
|
+
cursor_authoritative=cursor_authoritative)
|
|
287
|
+
|
|
288
|
+
def resolve(self, entity: str, aspect: str, frame: str = CANON, access=None):
|
|
289
|
+
return self.resolver.resolve(entity, aspect, frame, access)
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def porcelain(self):
|
|
293
|
+
"""The frozen host surface (PORCELAIN-V1). Lazy; import-local to
|
|
294
|
+
avoid circularity."""
|
|
295
|
+
if not hasattr(self, "_porcelain"):
|
|
296
|
+
from patternbuffer.porcelain import Porcelain
|
|
297
|
+
self._porcelain = Porcelain(self)
|
|
298
|
+
return self._porcelain
|
|
299
|
+
|
|
300
|
+
def charter(self) -> dict[str, Any]:
|
|
301
|
+
"""The world's self-description, read from world:self (letter 026)."""
|
|
302
|
+
out: dict[str, Any] = {}
|
|
303
|
+
for attr, result in self.indexes.current_state("world:self").items():
|
|
304
|
+
if result.winner is not None:
|
|
305
|
+
out[attr] = result.winner.value
|
|
306
|
+
return out
|
|
307
|
+
|
|
308
|
+
def close(self) -> None:
|
|
309
|
+
# An open build session never outlives the buffer: abort (restore the
|
|
310
|
+
# classify toggle, classify nothing) before closing (BUILD-SESSION-V1).
|
|
311
|
+
if hasattr(self, "_porcelain"):
|
|
312
|
+
self._porcelain.abort_build()
|
|
313
|
+
self.buffer.close()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _no_model(prompt: str, schema: dict) -> Any:
|
|
317
|
+
raise RuntimeError("no model callable injected for this operation")
|
patternbuffer/buffer.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""PatternBuffer: the append-only assertion log — the only truth (P1).
|
|
2
|
+
|
|
3
|
+
One SQLite file per world, and ``world_id`` stamped on every row as a
|
|
4
|
+
cross-wiring guard (the 1:1 invariant, spec §3.1/§4). Append-only is
|
|
5
|
+
enforced twice: the Python surface exposes no update/delete, and SQL
|
|
6
|
+
triggers raise on UPDATE/DELETE as belt-and-braces against future code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import sqlite3
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from patternbuffer.codec import decode_hook, json_default
|
|
18
|
+
from patternbuffer.model import (
|
|
19
|
+
ATTR_PREFIX,
|
|
20
|
+
CANON,
|
|
21
|
+
INVIOLABLE_CORE,
|
|
22
|
+
SEMANTICS_PREDICATES,
|
|
23
|
+
STATUSES,
|
|
24
|
+
VALUE_TYPES,
|
|
25
|
+
Assertion,
|
|
26
|
+
)
|
|
27
|
+
from patternbuffer.roles import WriterRole
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
_SCHEMA = """
|
|
32
|
+
CREATE TABLE IF NOT EXISTS world_meta (
|
|
33
|
+
key TEXT PRIMARY KEY,
|
|
34
|
+
value TEXT NOT NULL
|
|
35
|
+
);
|
|
36
|
+
CREATE TABLE IF NOT EXISTS assertions (
|
|
37
|
+
seq INTEGER PRIMARY KEY,
|
|
38
|
+
id TEXT UNIQUE NOT NULL,
|
|
39
|
+
world_id TEXT NOT NULL,
|
|
40
|
+
entity TEXT NOT NULL,
|
|
41
|
+
attribute TEXT NOT NULL,
|
|
42
|
+
value_type TEXT NOT NULL,
|
|
43
|
+
value TEXT NOT NULL,
|
|
44
|
+
valid_from REAL,
|
|
45
|
+
valid_to REAL,
|
|
46
|
+
frame TEXT NOT NULL DEFAULT 'canon',
|
|
47
|
+
status TEXT NOT NULL,
|
|
48
|
+
confidence REAL,
|
|
49
|
+
asserted_at INTEGER NOT NULL
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS ix_assertions_key
|
|
52
|
+
ON assertions (entity, attribute, frame);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS ix_assertions_attr_value
|
|
54
|
+
ON assertions (attribute, value);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS ix_assertions_value
|
|
56
|
+
ON assertions (value);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS ix_assertions_frame
|
|
58
|
+
ON assertions (frame, attribute);
|
|
59
|
+
CREATE TRIGGER IF NOT EXISTS assertions_append_only_update
|
|
60
|
+
BEFORE UPDATE ON assertions
|
|
61
|
+
BEGIN SELECT RAISE(ABORT, 'append-only: UPDATE forbidden'); END;
|
|
62
|
+
CREATE TRIGGER IF NOT EXISTS assertions_append_only_delete
|
|
63
|
+
BEFORE DELETE ON assertions
|
|
64
|
+
BEGIN SELECT RAISE(ABORT, 'append-only: DELETE forbidden'); END;
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class WorldMismatch(ValueError):
|
|
69
|
+
"""A buffer was opened or written with the wrong world_id (1:1 invariant)."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PatternBuffer:
|
|
73
|
+
"""The append-only store. Holds exactly one world's log, forever."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, path: str | Path, world_id: str) -> None:
|
|
76
|
+
self.path = Path(path)
|
|
77
|
+
self._conn = sqlite3.connect(self.path)
|
|
78
|
+
self._conn.executescript(_SCHEMA)
|
|
79
|
+
existing = self._meta("world_id")
|
|
80
|
+
if existing is None:
|
|
81
|
+
self._conn.execute(
|
|
82
|
+
"INSERT INTO world_meta (key, value) VALUES ('world_id', ?)", (world_id,)
|
|
83
|
+
)
|
|
84
|
+
self._conn.commit()
|
|
85
|
+
elif existing != world_id:
|
|
86
|
+
self._conn.close()
|
|
87
|
+
raise WorldMismatch(
|
|
88
|
+
f"buffer at {self.path} belongs to world {existing!r}, not {world_id!r}"
|
|
89
|
+
)
|
|
90
|
+
self.world_id = world_id
|
|
91
|
+
self.rows_read = 0 # deserialization counter (read-path scaling guard, 037)
|
|
92
|
+
|
|
93
|
+
def _meta(self, key: str) -> str | None:
|
|
94
|
+
row = self._conn.execute(
|
|
95
|
+
"SELECT value FROM world_meta WHERE key = ?", (key,)
|
|
96
|
+
).fetchone()
|
|
97
|
+
return row[0] if row else None
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------- write
|
|
100
|
+
|
|
101
|
+
def append(
|
|
102
|
+
self,
|
|
103
|
+
*,
|
|
104
|
+
entity: str,
|
|
105
|
+
attribute: str,
|
|
106
|
+
value: Any,
|
|
107
|
+
role: WriterRole,
|
|
108
|
+
status: str,
|
|
109
|
+
value_type: str = "literal",
|
|
110
|
+
valid_from: float | None = None,
|
|
111
|
+
valid_to: float | None = None,
|
|
112
|
+
frame: str = CANON,
|
|
113
|
+
confidence: float | None = None,
|
|
114
|
+
) -> Assertion:
|
|
115
|
+
"""Append one assertion. The only write path that exists."""
|
|
116
|
+
role.check(status)
|
|
117
|
+
if status not in STATUSES:
|
|
118
|
+
raise ValueError(f"unknown status {status!r}")
|
|
119
|
+
if value_type not in VALUE_TYPES:
|
|
120
|
+
raise ValueError(f"unknown value_type {value_type!r}")
|
|
121
|
+
seq = self.head() + 1
|
|
122
|
+
row = Assertion(
|
|
123
|
+
seq=seq,
|
|
124
|
+
id=f"a:{seq}",
|
|
125
|
+
world_id=self.world_id,
|
|
126
|
+
entity=entity,
|
|
127
|
+
attribute=attribute,
|
|
128
|
+
value_type=value_type,
|
|
129
|
+
value=value,
|
|
130
|
+
valid_from=valid_from,
|
|
131
|
+
valid_to=valid_to,
|
|
132
|
+
frame=frame,
|
|
133
|
+
status=status,
|
|
134
|
+
confidence=confidence,
|
|
135
|
+
asserted_at=seq,
|
|
136
|
+
)
|
|
137
|
+
self._insert(row)
|
|
138
|
+
return row
|
|
139
|
+
|
|
140
|
+
def _insert(self, row: Assertion) -> None:
|
|
141
|
+
if row.world_id != self.world_id:
|
|
142
|
+
raise WorldMismatch(
|
|
143
|
+
f"assertion stamped for world {row.world_id!r} cannot enter "
|
|
144
|
+
f"buffer of world {self.world_id!r}"
|
|
145
|
+
)
|
|
146
|
+
self._validate_attr_semantics_insert(row)
|
|
147
|
+
self._conn.execute(
|
|
148
|
+
"INSERT INTO assertions (seq, id, world_id, entity, attribute, value_type,"
|
|
149
|
+
" value, valid_from, valid_to, frame, status, confidence, asserted_at)"
|
|
150
|
+
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
151
|
+
(
|
|
152
|
+
row.seq,
|
|
153
|
+
row.id,
|
|
154
|
+
row.world_id,
|
|
155
|
+
row.entity,
|
|
156
|
+
row.attribute,
|
|
157
|
+
row.value_type,
|
|
158
|
+
json.dumps(row.value, sort_keys=True, default=json_default),
|
|
159
|
+
row.valid_from,
|
|
160
|
+
row.valid_to,
|
|
161
|
+
row.frame,
|
|
162
|
+
row.status,
|
|
163
|
+
row.confidence,
|
|
164
|
+
row.asserted_at,
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
self._conn.commit()
|
|
168
|
+
logger.debug("append %s: (%s · %s)", row.id, row.entity, row.attribute)
|
|
169
|
+
|
|
170
|
+
def _validate_attr_semantics_insert(self, row: Assertion) -> None:
|
|
171
|
+
"""Shared attr:* authority guard for append() and dump.build replay."""
|
|
172
|
+
if not row.entity.startswith(ATTR_PREFIX) or row.attribute not in SEMANTICS_PREDICATES:
|
|
173
|
+
return
|
|
174
|
+
target = row.entity[len(ATTR_PREFIX):]
|
|
175
|
+
if target in INVIOLABLE_CORE:
|
|
176
|
+
raise ValueError(
|
|
177
|
+
f"cannot declare semantics for inviolable core attribute {target!r}"
|
|
178
|
+
)
|
|
179
|
+
prior = self._conn.execute(
|
|
180
|
+
"SELECT id FROM assertions"
|
|
181
|
+
" WHERE attribute = ? AND entity NOT LIKE ? AND entity NOT LIKE ?"
|
|
182
|
+
" ORDER BY seq LIMIT 1",
|
|
183
|
+
(target, ATTR_PREFIX + "%", "a:%"),
|
|
184
|
+
).fetchone()
|
|
185
|
+
if prior is not None:
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"cannot declare semantics for attribute {target!r} after "
|
|
188
|
+
f"folded data already exists ({prior[0]})"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# ----------------------------------------------------------------- read
|
|
192
|
+
|
|
193
|
+
def head(self) -> int:
|
|
194
|
+
"""The current log head (last seq; 0 for an empty log)."""
|
|
195
|
+
row = self._conn.execute("SELECT COALESCE(MAX(seq), 0) FROM assertions").fetchone()
|
|
196
|
+
return int(row[0])
|
|
197
|
+
|
|
198
|
+
def get(self, assertion_id: str) -> Assertion | None:
|
|
199
|
+
rows = self._select("WHERE id = ?", (assertion_id,))
|
|
200
|
+
return rows[0] if rows else None
|
|
201
|
+
|
|
202
|
+
def all_rows(self) -> list[Assertion]:
|
|
203
|
+
"""The full log in append order (dump/rebuild substrate)."""
|
|
204
|
+
return self._select("", ())
|
|
205
|
+
|
|
206
|
+
def visible(
|
|
207
|
+
self,
|
|
208
|
+
*,
|
|
209
|
+
entity: str | None = None,
|
|
210
|
+
entity_in: list[str] | None = None,
|
|
211
|
+
entity_prefix: str | None = None,
|
|
212
|
+
attribute: str | None = None,
|
|
213
|
+
attribute_in: list[str] | None = None,
|
|
214
|
+
value: Any | None = None,
|
|
215
|
+
value_type: str | None = None,
|
|
216
|
+
frame: str | None = None,
|
|
217
|
+
valid_as_of: float | None = None,
|
|
218
|
+
asserted_as_of: int | None = None,
|
|
219
|
+
) -> list[Assertion]:
|
|
220
|
+
"""Rows visible at ``(valid_as_of, asserted_as_of)`` — spec §4.2.
|
|
221
|
+
|
|
222
|
+
Visibility is class-blind; which visible row a fold serves is
|
|
223
|
+
durability-dependent and lives in the derived indexes, not here.
|
|
224
|
+
Omitted bounds mean no bound (now / log head). Retraction
|
|
225
|
+
meta-rows themselves are excluded from folds, and a row is
|
|
226
|
+
invisible once a ``retracts`` row targeting it is itself visible
|
|
227
|
+
on the asserted axis.
|
|
228
|
+
"""
|
|
229
|
+
aao = self.head() if asserted_as_of is None else asserted_as_of
|
|
230
|
+
clauses = ["a.asserted_at <= ?", "a.status != 'retracted'"]
|
|
231
|
+
params: list[Any] = [aao]
|
|
232
|
+
if entity is not None:
|
|
233
|
+
clauses.append("a.entity = ?")
|
|
234
|
+
params.append(entity)
|
|
235
|
+
if entity_in is not None:
|
|
236
|
+
clauses.append(f"a.entity IN ({','.join('?' * len(entity_in))})")
|
|
237
|
+
params.extend(entity_in)
|
|
238
|
+
if entity_prefix is not None:
|
|
239
|
+
clauses.append("a.entity LIKE ?")
|
|
240
|
+
params.append(entity_prefix + "%")
|
|
241
|
+
if attribute is not None:
|
|
242
|
+
clauses.append("a.attribute = ?")
|
|
243
|
+
params.append(attribute)
|
|
244
|
+
if attribute_in is not None:
|
|
245
|
+
clauses.append(f"a.attribute IN ({','.join('?' * len(attribute_in))})")
|
|
246
|
+
params.extend(attribute_in)
|
|
247
|
+
if value is not None:
|
|
248
|
+
clauses.append("a.value = ?")
|
|
249
|
+
# Encoded-byte equality: a Decimal query matches its stored row at
|
|
250
|
+
# the authored scale ("12.50" != "12.5" — append-only fidelity).
|
|
251
|
+
params.append(json.dumps(value, sort_keys=True, default=json_default))
|
|
252
|
+
if value_type is not None:
|
|
253
|
+
clauses.append("a.value_type = ?")
|
|
254
|
+
params.append(value_type)
|
|
255
|
+
if frame is not None:
|
|
256
|
+
clauses.append("a.frame = ?")
|
|
257
|
+
params.append(frame)
|
|
258
|
+
if valid_as_of is not None:
|
|
259
|
+
clauses.append("(a.valid_from IS NULL OR a.valid_from <= ?)")
|
|
260
|
+
params.append(valid_as_of)
|
|
261
|
+
clauses.append("(a.valid_to IS NULL OR a.valid_to > ?)")
|
|
262
|
+
params.append(valid_as_of)
|
|
263
|
+
clauses.append(
|
|
264
|
+
"NOT EXISTS (SELECT 1 FROM assertions r"
|
|
265
|
+
" WHERE r.entity = a.id AND r.attribute = 'retracts' AND r.asserted_at <= ?)"
|
|
266
|
+
)
|
|
267
|
+
params.append(aao)
|
|
268
|
+
return self._select("WHERE " + " AND ".join(clauses), tuple(params))
|
|
269
|
+
|
|
270
|
+
def max_valid_from(self) -> float | None:
|
|
271
|
+
"""The valid-time high-water mark over ALL rows, all frames (None when
|
|
272
|
+
no timed rows exist). Log coordinates are real after retraction —
|
|
273
|
+
monotone by construction (AXIS-HEAD-V1)."""
|
|
274
|
+
row = self._conn.execute("SELECT MAX(valid_from) FROM assertions").fetchone()
|
|
275
|
+
return row[0]
|
|
276
|
+
|
|
277
|
+
def _select(self, where: str, params: tuple) -> list[Assertion]:
|
|
278
|
+
cur = self._conn.execute(
|
|
279
|
+
"SELECT seq, id, world_id, entity, attribute, value_type, value,"
|
|
280
|
+
" valid_from, valid_to, frame, status, confidence, asserted_at"
|
|
281
|
+
f" FROM assertions a {where} ORDER BY seq",
|
|
282
|
+
params,
|
|
283
|
+
)
|
|
284
|
+
out = [
|
|
285
|
+
Assertion(
|
|
286
|
+
seq=r[0],
|
|
287
|
+
id=r[1],
|
|
288
|
+
world_id=r[2],
|
|
289
|
+
entity=r[3],
|
|
290
|
+
attribute=r[4],
|
|
291
|
+
value_type=r[5],
|
|
292
|
+
value=json.loads(r[6], object_hook=decode_hook),
|
|
293
|
+
valid_from=r[7],
|
|
294
|
+
valid_to=r[8],
|
|
295
|
+
frame=r[9],
|
|
296
|
+
status=r[10],
|
|
297
|
+
confidence=r[11],
|
|
298
|
+
asserted_at=r[12],
|
|
299
|
+
)
|
|
300
|
+
for r in cur.fetchall()
|
|
301
|
+
]
|
|
302
|
+
self.rows_read += len(out)
|
|
303
|
+
return out
|
|
304
|
+
|
|
305
|
+
# ------------------------------------------------------------- plumbing
|
|
306
|
+
|
|
307
|
+
def raw_connection(self) -> sqlite3.Connection:
|
|
308
|
+
"""For tests asserting the triggers; never used by engine code."""
|
|
309
|
+
return self._conn
|
|
310
|
+
|
|
311
|
+
def close(self) -> None:
|
|
312
|
+
self._conn.close()
|