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/project.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""The projector: materialize() (whitepaper §13; spec §9.1).
|
|
2
|
+
|
|
3
|
+
The projector writes nothing durable. Out-of-frame rows are absent from
|
|
4
|
+
the payload — filtering happens at source, in the row selection, never
|
|
5
|
+
as a marking pass. The CONSTITUTIVE spine is budget-exempt. Closed-world
|
|
6
|
+
render commitments are payload-only `default` marks that can never
|
|
7
|
+
harden into canon.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from typing import Callable
|
|
16
|
+
|
|
17
|
+
from patternbuffer.buffer import PatternBuffer
|
|
18
|
+
from patternbuffer.classify import CONSTITUTIVE, DISPOSITIONAL, EVENT, STATE, Classifier
|
|
19
|
+
from patternbuffer.indexes import Indexes
|
|
20
|
+
from patternbuffer.model import ATTR_PREFIX, CANON, Assertion
|
|
21
|
+
from patternbuffer.semantics import AttributeSemantics
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
LENSES = frozenset(
|
|
26
|
+
{"establishing_set", "current_state", "what_happened", "character_sheet", "situation"}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Kind defaults: render-coherence fills, never fact claims (whitepaper
|
|
30
|
+
# §3.1). Deliberately tiny in the spike.
|
|
31
|
+
KIND_DEFAULTS: dict[str, dict[str, object]] = {
|
|
32
|
+
"room": {"lighting": "unremarkable", "walls": "plain"},
|
|
33
|
+
"person": {"build": "average"},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class DefaultFill:
|
|
39
|
+
entity: str
|
|
40
|
+
attribute: str
|
|
41
|
+
value: object
|
|
42
|
+
status: str = "default" # payload-only; no role can append this
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Materialization:
|
|
47
|
+
scope: tuple[str, ...]
|
|
48
|
+
frame: str
|
|
49
|
+
as_of: float | None
|
|
50
|
+
asserted_as_of: int | None
|
|
51
|
+
lens: str
|
|
52
|
+
assertions: list[Assertion] = field(default_factory=list)
|
|
53
|
+
defaults: list[DefaultFill] = field(default_factory=list)
|
|
54
|
+
conflicted_keys: list[tuple[str, str]] = field(default_factory=list)
|
|
55
|
+
unresolved: list[tuple[str, str]] = field(default_factory=list) # the visible frontier
|
|
56
|
+
quantities: list[tuple[str, str, int | float | Decimal]] = field(default_factory=list)
|
|
57
|
+
truncated: int = 0 # rows dropped to budget (never CONSTITUTIVE)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Projector:
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
buffer: PatternBuffer,
|
|
64
|
+
classifier: Classifier,
|
|
65
|
+
indexes: Indexes,
|
|
66
|
+
semantics: AttributeSemantics | None = None,
|
|
67
|
+
salience: Callable[[str, str, float | None], float] | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
self._buffer = buffer
|
|
70
|
+
self._classifier = classifier
|
|
71
|
+
self._indexes = indexes
|
|
72
|
+
self._semantics = semantics or AttributeSemantics(buffer)
|
|
73
|
+
self._salience = salience or (lambda entity, frame, as_of: 0.0)
|
|
74
|
+
|
|
75
|
+
# ------------------------------------------------------------ scoping
|
|
76
|
+
|
|
77
|
+
def _scope_entities(self, scope: str | list[str], frame: str,
|
|
78
|
+
valid_as_of: float | None, asserted_as_of: int | None) -> list[str]:
|
|
79
|
+
"""A scope is an entity list, or a container whose subtree it is."""
|
|
80
|
+
roots = [scope] if isinstance(scope, str) else list(scope)
|
|
81
|
+
out: list[str] = []
|
|
82
|
+
seen: set[str] = set()
|
|
83
|
+
frontier = [self._indexes.resolve_entity(r) for r in roots]
|
|
84
|
+
while frontier:
|
|
85
|
+
e = frontier.pop(0)
|
|
86
|
+
if e in seen:
|
|
87
|
+
continue
|
|
88
|
+
seen.add(e)
|
|
89
|
+
out.append(e)
|
|
90
|
+
frontier.extend(
|
|
91
|
+
self._indexes.contents(e, frame, valid_as_of, asserted_as_of)
|
|
92
|
+
)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
def _establishing_scope(self, scope: str | list[str], frame: str,
|
|
96
|
+
valid_as_of: float | None, asserted_as_of: int | None) -> list[str]:
|
|
97
|
+
"""Subtree membership by establishing containment edges."""
|
|
98
|
+
roots = {self._indexes.resolve_entity(r)
|
|
99
|
+
for r in ([scope] if isinstance(scope, str) else scope)}
|
|
100
|
+
by_entity: dict[str, list[Assertion]] = {}
|
|
101
|
+
for row in self._buffer.visible(
|
|
102
|
+
frame=frame, valid_as_of=valid_as_of, asserted_as_of=asserted_as_of
|
|
103
|
+
):
|
|
104
|
+
if (
|
|
105
|
+
row.attribute in self._semantics.containment_family()
|
|
106
|
+
and not row.entity.startswith("a:")
|
|
107
|
+
and not row.entity.startswith(ATTR_PREFIX)
|
|
108
|
+
):
|
|
109
|
+
by_entity.setdefault(self._indexes.resolve_entity(row.entity), []).append(row)
|
|
110
|
+
parent: dict[str, str] = {}
|
|
111
|
+
for entity, rows in by_entity.items():
|
|
112
|
+
qualifying = [
|
|
113
|
+
r for r in rows
|
|
114
|
+
if r.value_type == "entity"
|
|
115
|
+
and (
|
|
116
|
+
self._classifier.durability(r.id) != STATE
|
|
117
|
+
or not self._indexes._is_event_effect(r, asserted_as_of)
|
|
118
|
+
)
|
|
119
|
+
]
|
|
120
|
+
if qualifying:
|
|
121
|
+
first = min(qualifying, key=lambda r: (r.valid_from or float("-inf"), r.asserted_at))
|
|
122
|
+
parent[entity] = self._indexes.resolve_entity(first.value)
|
|
123
|
+
out, ordering = set(roots), list(roots)
|
|
124
|
+
changed = True
|
|
125
|
+
while changed:
|
|
126
|
+
changed = False
|
|
127
|
+
for entity, p in parent.items():
|
|
128
|
+
if p in out and entity not in out:
|
|
129
|
+
out.add(entity)
|
|
130
|
+
ordering.append(entity)
|
|
131
|
+
changed = True
|
|
132
|
+
return ordering
|
|
133
|
+
|
|
134
|
+
# -------------------------------------------------------------- lenses
|
|
135
|
+
|
|
136
|
+
def materialize(
|
|
137
|
+
self,
|
|
138
|
+
scope: str | list[str],
|
|
139
|
+
as_of: float | None = None,
|
|
140
|
+
frame: str = CANON,
|
|
141
|
+
lens: str = "current_state",
|
|
142
|
+
budget: int | None = None,
|
|
143
|
+
asserted_as_of: int | None = None,
|
|
144
|
+
since: float | None = None,
|
|
145
|
+
correlated: bool = False,
|
|
146
|
+
features: bool = False,
|
|
147
|
+
) -> Materialization:
|
|
148
|
+
"""`since` (letter 029): lower bound for the what_happened lens —
|
|
149
|
+
with `as_of` it gives occurred(window=[since, as_of]) matching.
|
|
150
|
+
|
|
151
|
+
`correlated`/`features` (AWARENESS-READS-V1.1, opt-in): fold each entity
|
|
152
|
+
over its `aka` correlation union (the scene-wide form of `state_union`),
|
|
153
|
+
and/or augment the scope with each place's `part_of`-feature children
|
|
154
|
+
(the place-with-features view). Default off = unchanged projection."""
|
|
155
|
+
if lens not in LENSES:
|
|
156
|
+
raise ValueError(f"unknown lens {lens!r}")
|
|
157
|
+
if correlated and lens == "establishing_set":
|
|
158
|
+
# The establishing view is the world AT CREATION — before any identity
|
|
159
|
+
# reveal — so there is nothing to correlate. The combination is
|
|
160
|
+
# incoherent; guard it rather than thread the flag through the
|
|
161
|
+
# specialized first-state path (elegance over forced orthogonality).
|
|
162
|
+
raise ValueError(
|
|
163
|
+
"correlated=True is not supported with lens='establishing_set' "
|
|
164
|
+
"(the world-at-creation view predates identity reveals)"
|
|
165
|
+
)
|
|
166
|
+
if lens == "establishing_set":
|
|
167
|
+
# The world at rest: the scope walk itself uses establishing
|
|
168
|
+
# containment, or entities the plot moved away vanish from the
|
|
169
|
+
# very lens meant to show where they started.
|
|
170
|
+
entities = self._establishing_scope(scope, frame, as_of, asserted_as_of)
|
|
171
|
+
else:
|
|
172
|
+
entities = self._scope_entities(scope, frame, as_of, asserted_as_of)
|
|
173
|
+
if features:
|
|
174
|
+
# AWARENESS-READS-V1.1 Win 2: inline each scope place's part_of
|
|
175
|
+
# children (one level), pre-projection — a clean entity-list add
|
|
176
|
+
# (composition is a separate axis from the containment scope walk).
|
|
177
|
+
seen = set(entities)
|
|
178
|
+
for place in list(entities):
|
|
179
|
+
for child in self._indexes.features(place, frame, as_of, asserted_as_of):
|
|
180
|
+
if child not in seen:
|
|
181
|
+
seen.add(child)
|
|
182
|
+
entities.append(child)
|
|
183
|
+
m = Materialization(
|
|
184
|
+
scope=tuple(entities), frame=frame, as_of=as_of,
|
|
185
|
+
asserted_as_of=asserted_as_of, lens=lens,
|
|
186
|
+
)
|
|
187
|
+
if lens == "what_happened":
|
|
188
|
+
self._lens_events(m, entities, since)
|
|
189
|
+
elif lens == "character_sheet":
|
|
190
|
+
self._lens_sheet(m, entities, correlated=correlated)
|
|
191
|
+
elif lens == "situation":
|
|
192
|
+
self._lens_situation(m, entities, budget, correlated=correlated)
|
|
193
|
+
else:
|
|
194
|
+
self._lens_state(m, entities, establishing=(lens == "establishing_set"),
|
|
195
|
+
correlated=correlated)
|
|
196
|
+
self._fill_defaults(m)
|
|
197
|
+
if lens != "situation":
|
|
198
|
+
# situation budgets its live-events bucket internally and never
|
|
199
|
+
# truncates the standing-truth floor (SITUATION-LENS-V1 §Overflow).
|
|
200
|
+
self._shape_to_budget(m, budget)
|
|
201
|
+
return m
|
|
202
|
+
|
|
203
|
+
def _lens_state(
|
|
204
|
+
self,
|
|
205
|
+
m: Materialization,
|
|
206
|
+
entities: list[str],
|
|
207
|
+
establishing: bool,
|
|
208
|
+
served: dict[str, str] | None = None,
|
|
209
|
+
correlated: bool = False,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Standing truth. When `served` is supplied (situation lens), it
|
|
212
|
+
collects the ids of the rows the fold actually serves now, tagged
|
|
213
|
+
``"concrete"`` (a surviving effect) or ``"open"`` (an unresolved
|
|
214
|
+
winner = an open thread) — the set the live-event walk reaches back
|
|
215
|
+
from (SITUATION-LENS-V1). ``correlated`` folds over the `aka` union."""
|
|
216
|
+
for entity in entities:
|
|
217
|
+
folded = self._indexes.current_state(
|
|
218
|
+
entity, m.frame, m.as_of, m.asserted_as_of, correlated=correlated
|
|
219
|
+
)
|
|
220
|
+
for attr, result in sorted(folded.items()):
|
|
221
|
+
if result.quantity is not None:
|
|
222
|
+
if result.conflicted:
|
|
223
|
+
m.conflicted_keys.append((entity, attr))
|
|
224
|
+
m.quantities.append((entity, attr, result.quantity))
|
|
225
|
+
if served is not None:
|
|
226
|
+
# accrue serves the total via quantities; the ledger
|
|
227
|
+
# rows are its live effects.
|
|
228
|
+
for r in result._ledger_rows:
|
|
229
|
+
served[r.id] = "concrete"
|
|
230
|
+
continue
|
|
231
|
+
rows = result._value_rows or (
|
|
232
|
+
(result.winner,) if result.winner is not None else ()
|
|
233
|
+
)
|
|
234
|
+
if not rows:
|
|
235
|
+
continue
|
|
236
|
+
if result.conflicted:
|
|
237
|
+
m.conflicted_keys.append((entity, attr))
|
|
238
|
+
if served is not None:
|
|
239
|
+
# both sides are standing truth; both are served.
|
|
240
|
+
for cid in result.conflicting:
|
|
241
|
+
served[cid] = "concrete"
|
|
242
|
+
for row in rows:
|
|
243
|
+
if row.value_type == "unresolved":
|
|
244
|
+
m.unresolved.append((entity, attr))
|
|
245
|
+
if served is not None:
|
|
246
|
+
# an unresolved winner with no resolved_by is the
|
|
247
|
+
# open-thread state (fold_key already drops spent
|
|
248
|
+
# thunks): mark it open.
|
|
249
|
+
served[row.id] = "open"
|
|
250
|
+
continue # the frontier is named, never painted (§10)
|
|
251
|
+
durability = self._classifier.durability(row.id)
|
|
252
|
+
if establishing and durability == STATE and not result._value_rows:
|
|
253
|
+
row = self._establishing_state(entity, attr, m) or None
|
|
254
|
+
if row is None:
|
|
255
|
+
continue
|
|
256
|
+
if served is not None:
|
|
257
|
+
served[row.id] = "concrete"
|
|
258
|
+
m.assertions.append(row)
|
|
259
|
+
|
|
260
|
+
def _establishing_state(
|
|
261
|
+
self, entity: str, attr: str, m: Materialization
|
|
262
|
+
) -> Assertion | None:
|
|
263
|
+
"""First STATE by (valid_from, asserted_at) with no caused_by edge
|
|
264
|
+
to an EVENT — the world at rest (spec §9.1); honors world_defining."""
|
|
265
|
+
rows = [
|
|
266
|
+
r
|
|
267
|
+
for r in self._buffer.visible(
|
|
268
|
+
frame=m.frame,
|
|
269
|
+
valid_as_of=m.as_of, asserted_as_of=m.asserted_as_of,
|
|
270
|
+
)
|
|
271
|
+
if self._indexes.resolve_entity(r.entity) == entity
|
|
272
|
+
and not r.entity.startswith("a:")
|
|
273
|
+
and not r.entity.startswith(ATTR_PREFIX)
|
|
274
|
+
and self._indexes.fold_attribute(r.attribute) == attr # already fold-keyed
|
|
275
|
+
and r.value_type != "unresolved"
|
|
276
|
+
and self._classifier.durability(r.id) == STATE
|
|
277
|
+
]
|
|
278
|
+
qualifying = []
|
|
279
|
+
for r in rows:
|
|
280
|
+
pinned = bool(self._buffer.visible(entity=r.id, attribute="world_defining"))
|
|
281
|
+
if pinned:
|
|
282
|
+
return r
|
|
283
|
+
if not self._indexes._is_event_effect(r, m.asserted_as_of):
|
|
284
|
+
qualifying.append(r)
|
|
285
|
+
if not qualifying:
|
|
286
|
+
return None
|
|
287
|
+
return min(qualifying, key=lambda r: (r.valid_from or float("-inf"), r.asserted_at))
|
|
288
|
+
|
|
289
|
+
def _lens_situation(
|
|
290
|
+
self, m: Materialization, entities: list[str], budget: int | None,
|
|
291
|
+
correlated: bool = False,
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Re-entry retrieval (SITUATION-LENS-V1): the standing-truth floor
|
|
294
|
+
plus only the *live* events anchored to the scope; closed history is
|
|
295
|
+
dropped. Liveness is derived every read from present facts — an event
|
|
296
|
+
is live iff it has an open thread (a) or a still-served effect (b) —
|
|
297
|
+
and nothing is stored (the membrane, A6). ``correlated`` is threaded so
|
|
298
|
+
the boolean stays lens-orthogonal (Cx final)."""
|
|
299
|
+
served: dict[str, str] = {}
|
|
300
|
+
self._lens_state(m, entities, establishing=False, served=served,
|
|
301
|
+
correlated=correlated)
|
|
302
|
+
floor = len(m.assertions)
|
|
303
|
+
if not served:
|
|
304
|
+
return
|
|
305
|
+
# One batched walk: served effect/thread rows -> their causing events.
|
|
306
|
+
# caused_by sits on the EFFECT row (entity = effect-row id, value =
|
|
307
|
+
# event entity); a row in `served` carrying one means its event has a
|
|
308
|
+
# surviving effect (b) or, for an "open" row, an open thread (a).
|
|
309
|
+
# Anchoring is therefore implicit: an event surfaces only because it
|
|
310
|
+
# produced a still-served fact ABOUT a scope entity — never because a
|
|
311
|
+
# mobile participant merely stands in scope.
|
|
312
|
+
live_events: set[str] = set()
|
|
313
|
+
for meta in self._buffer.visible(
|
|
314
|
+
entity_in=sorted(served),
|
|
315
|
+
attribute="caused_by",
|
|
316
|
+
value_type="entity",
|
|
317
|
+
frame=m.frame,
|
|
318
|
+
asserted_as_of=m.asserted_as_of,
|
|
319
|
+
):
|
|
320
|
+
if isinstance(meta.value, str):
|
|
321
|
+
live_events.add(self._indexes.resolve_entity(meta.value))
|
|
322
|
+
if not live_events:
|
|
323
|
+
return
|
|
324
|
+
# Emit the EVENT-durability rows for the live events, recency-ordered.
|
|
325
|
+
event_rows: list[Assertion] = []
|
|
326
|
+
for row in self._buffer.visible(
|
|
327
|
+
frame=m.frame, valid_as_of=None, asserted_as_of=m.asserted_as_of
|
|
328
|
+
):
|
|
329
|
+
if self._classifier.durability(row.id) != EVENT:
|
|
330
|
+
continue
|
|
331
|
+
if row.entity.startswith("a:") or row.entity.startswith(ATTR_PREFIX):
|
|
332
|
+
continue
|
|
333
|
+
if self._indexes.resolve_entity(row.entity) not in live_events:
|
|
334
|
+
continue
|
|
335
|
+
if m.as_of is not None and row.valid_from is not None and row.valid_from > m.as_of:
|
|
336
|
+
continue
|
|
337
|
+
event_rows.append(row)
|
|
338
|
+
event_rows.sort(
|
|
339
|
+
key=lambda r: (r.valid_from if r.valid_from is not None else float("-inf"), r.seq)
|
|
340
|
+
)
|
|
341
|
+
if budget is not None:
|
|
342
|
+
# The floor is never truncated; the live-events bucket yields to
|
|
343
|
+
# budget, keeping the most-recent (recency, not popularity).
|
|
344
|
+
keep = max(0, budget - floor)
|
|
345
|
+
if len(event_rows) > keep:
|
|
346
|
+
m.truncated += len(event_rows) - keep
|
|
347
|
+
event_rows = event_rows[len(event_rows) - keep:]
|
|
348
|
+
m.assertions.extend(event_rows)
|
|
349
|
+
|
|
350
|
+
def _lens_events(self, m: Materialization, entities: list[str],
|
|
351
|
+
since: float | None = None) -> None:
|
|
352
|
+
in_scope = set(entities)
|
|
353
|
+
events = []
|
|
354
|
+
for row in self._buffer.visible(
|
|
355
|
+
frame=m.frame, valid_as_of=None, asserted_as_of=m.asserted_as_of
|
|
356
|
+
):
|
|
357
|
+
if self._classifier.durability(row.id) != EVENT:
|
|
358
|
+
continue
|
|
359
|
+
if row.entity.startswith("a:") or row.entity.startswith(ATTR_PREFIX):
|
|
360
|
+
continue # meta rows ride with their subjects
|
|
361
|
+
subject = self._indexes.resolve_entity(row.entity)
|
|
362
|
+
object_ = (
|
|
363
|
+
self._indexes.resolve_entity(row.value)
|
|
364
|
+
if row.value_type == "entity" and isinstance(row.value, str)
|
|
365
|
+
else None
|
|
366
|
+
)
|
|
367
|
+
if subject in in_scope or object_ in in_scope or not in_scope:
|
|
368
|
+
if m.as_of is not None and row.valid_from is not None and row.valid_from > m.as_of:
|
|
369
|
+
continue
|
|
370
|
+
if since is not None and (row.valid_from is None or row.valid_from < since):
|
|
371
|
+
continue
|
|
372
|
+
events.append(row)
|
|
373
|
+
events.sort(key=lambda r: (r.valid_from if r.valid_from is not None else float("-inf"), r.seq))
|
|
374
|
+
m.assertions.extend(events)
|
|
375
|
+
|
|
376
|
+
def _lens_sheet(self, m: Materialization, entities: list[str],
|
|
377
|
+
correlated: bool = False) -> None:
|
|
378
|
+
"""One entity's accumulated card, frame-respecting. ``correlated`` folds
|
|
379
|
+
over the `aka` union (a dual-persona sheet) — lens-orthogonal (Cx final)."""
|
|
380
|
+
for entity in entities[:1]:
|
|
381
|
+
folded = self._indexes.current_state(entity, m.frame, m.as_of,
|
|
382
|
+
m.asserted_as_of, correlated=correlated)
|
|
383
|
+
for attr, result in sorted(folded.items()):
|
|
384
|
+
if result.winner is None:
|
|
385
|
+
continue
|
|
386
|
+
if result.quantity is not None:
|
|
387
|
+
if result.conflicted:
|
|
388
|
+
m.conflicted_keys.append((entity, attr))
|
|
389
|
+
m.quantities.append((entity, attr, result.quantity))
|
|
390
|
+
continue
|
|
391
|
+
if result.conflicted:
|
|
392
|
+
m.conflicted_keys.append((entity, attr))
|
|
393
|
+
for row in result._value_rows or (result.winner,):
|
|
394
|
+
if row.value_type == "unresolved":
|
|
395
|
+
m.unresolved.append((entity, attr))
|
|
396
|
+
continue
|
|
397
|
+
m.assertions.append(row)
|
|
398
|
+
|
|
399
|
+
# --------------------------------------------------- defaults + budget
|
|
400
|
+
|
|
401
|
+
def _fill_defaults(self, m: Materialization) -> None:
|
|
402
|
+
"""Closed-world render commitments, marked `default` in the payload
|
|
403
|
+
only (open-world store / closed-world projection, §13)."""
|
|
404
|
+
if m.lens == "what_happened":
|
|
405
|
+
return
|
|
406
|
+
present: dict[str, set[str]] = {}
|
|
407
|
+
kinds: dict[str, str] = {}
|
|
408
|
+
for row in m.assertions:
|
|
409
|
+
e = self._indexes.resolve_entity(row.entity)
|
|
410
|
+
present.setdefault(e, set()).add(row.attribute)
|
|
411
|
+
if row.attribute == "kind" and isinstance(row.value, str):
|
|
412
|
+
kinds[e] = row.value
|
|
413
|
+
for entity, kind in kinds.items():
|
|
414
|
+
for attr, value in KIND_DEFAULTS.get(kind, {}).items():
|
|
415
|
+
if attr not in present.get(entity, set()):
|
|
416
|
+
m.defaults.append(DefaultFill(entity, attr, value))
|
|
417
|
+
|
|
418
|
+
def _shape_to_budget(self, m: Materialization, budget: int | None) -> None:
|
|
419
|
+
"""The budget invariant: the CONSTITUTIVE spine is exempt; compress
|
|
420
|
+
the rest by salience, never drop identity and structure (§13)."""
|
|
421
|
+
if budget is None or len(m.assertions) <= budget:
|
|
422
|
+
return
|
|
423
|
+
spine = [
|
|
424
|
+
r for r in m.assertions if self._classifier.durability(r.id) == CONSTITUTIVE
|
|
425
|
+
]
|
|
426
|
+
rest = [r for r in m.assertions if r not in spine]
|
|
427
|
+
rest.sort( # salience: projection-time ranking, never stored (P1)
|
|
428
|
+
key=lambda r: (
|
|
429
|
+
self._salience(self._indexes.resolve_entity(r.entity), m.frame, m.as_of),
|
|
430
|
+
r.asserted_at,
|
|
431
|
+
),
|
|
432
|
+
reverse=True,
|
|
433
|
+
)
|
|
434
|
+
keep = max(0, budget - len(spine))
|
|
435
|
+
m.truncated = len(rest) - keep
|
|
436
|
+
m.assertions = spine + rest[:keep]
|