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/mcp.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""The MCP wrapper (MCP-WRAPPER-V1): the frozen porcelain served over MCP.
|
|
2
|
+
|
|
3
|
+
Any MCP client drives a world with zero Python — the engine-independence claim
|
|
4
|
+
(whitepaper §17.2) demonstrated beyond the Python host. Pure adapter: an
|
|
5
|
+
explicit registry of the porcelain's deterministic verbs, a dispatch layer, and
|
|
6
|
+
one mechanical wire envelope over `world.porcelain.*` — no engine surface, no
|
|
7
|
+
reflection, no model callable (the server's World has none, and the exposed
|
|
8
|
+
parameter domains are the genuinely model-free subset).
|
|
9
|
+
|
|
10
|
+
The engine core never imports this module; the `mcp` SDK is imported lazily and
|
|
11
|
+
ships in the `pbuffer[mcp]` extra (distribution name; import stays patternbuffer). Trust boundary: a connected client is a
|
|
12
|
+
FULLY TRUSTED world principal (it can name any frame; annotations are hints,
|
|
13
|
+
never authorization) — untrusted consumers go through a host-mediated surface.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import dataclasses
|
|
20
|
+
import inspect
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import threading
|
|
25
|
+
import types as _pytypes
|
|
26
|
+
import typing
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from patternbuffer import World
|
|
30
|
+
from patternbuffer.codec import encode_out
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# ------------------------------------------------------------- the registry
|
|
35
|
+
# Explicit and exhaustive (Cx 521 #2): the 37 deterministic tools. A porcelain
|
|
36
|
+
# addition CANNOT appear here by reflection — it must be classified V1/V1.1 and
|
|
37
|
+
# added deliberately (the registry-completeness test enforces this).
|
|
38
|
+
|
|
39
|
+
V1_READS = (
|
|
40
|
+
"snapshot", "state", "state_union", "where", "aggregate", "confidence",
|
|
41
|
+
"locate", "contents", "composition", "features", "path", "route",
|
|
42
|
+
"neighborhood", "salience", "frame_diff", "who_knows", "events",
|
|
43
|
+
"entities", "facts", "fidelity_audit", "axis_heads", "proposals",
|
|
44
|
+
"correlations", "correlation_conflicts", "typing_conflicts",
|
|
45
|
+
)
|
|
46
|
+
V1_MUTATIONS = (
|
|
47
|
+
"ingest_structured", "retract", "reconcile", "adjudicate_deferred",
|
|
48
|
+
"confirm", "merge", "reject", "correlate", "retype",
|
|
49
|
+
"begin_build", "seal_build", "abort_build",
|
|
50
|
+
)
|
|
51
|
+
V1_TOOLS = V1_READS + V1_MUTATIONS
|
|
52
|
+
|
|
53
|
+
# V1.1 (MCP sampling), deliberately absent: extract, ingest, ask, resolve.
|
|
54
|
+
|
|
55
|
+
# Conservatively destructive (Cx 521 #4): retracts, collapses identity, or
|
|
56
|
+
# supersedes the effective view. The complement of this set within mutations —
|
|
57
|
+
# including begin_build/abort_build — is explicitly destructiveHint=False.
|
|
58
|
+
DESTRUCTIVE = frozenset({
|
|
59
|
+
"retract", "merge", "confirm", "retype", "adjudicate_deferred",
|
|
60
|
+
"reconcile", "seal_build",
|
|
61
|
+
})
|
|
62
|
+
# Retry-safe mutations only (reads are all idempotent).
|
|
63
|
+
IDEMPOTENT_MUTATIONS = frozenset({"abort_build", "reject", "correlate"})
|
|
64
|
+
|
|
65
|
+
# Declared schema overrides (Cx 521 #2): exactly the parameters whose frozen
|
|
66
|
+
# signatures don't carry a sufficient annotation, plus the model-free domain
|
|
67
|
+
# narrowings (Cx 521 #1).
|
|
68
|
+
_STR_OR_LIST = {"anyOf": [{"type": "string"},
|
|
69
|
+
{"type": "array", "items": {"type": "string"}}]}
|
|
70
|
+
SCHEMA_OVERRIDES: dict[tuple[str, str], dict] = {
|
|
71
|
+
("snapshot", "scope"): _STR_OR_LIST,
|
|
72
|
+
("frame_diff", "scope"): _STR_OR_LIST,
|
|
73
|
+
("where", "value"): {"anyOf": [
|
|
74
|
+
{"type": "number"},
|
|
75
|
+
{"type": "object",
|
|
76
|
+
"properties": {"$decimal": {"type": "string"}},
|
|
77
|
+
"required": ["$decimal"], "additionalProperties": False},
|
|
78
|
+
]},
|
|
79
|
+
# The genuinely model-free subset: inline/batch REACH the model path even
|
|
80
|
+
# on a no-model World (the _no_model sentinel fires; the classifier falls
|
|
81
|
+
# back) — so the tool exposes only rules|defer, defaulting to rules.
|
|
82
|
+
("ingest_structured", "classify"): {"type": "string",
|
|
83
|
+
"enum": ["rules", "defer"],
|
|
84
|
+
"default": "rules"},
|
|
85
|
+
}
|
|
86
|
+
# Parameters omitted from the tool surface entirely (the server always uses
|
|
87
|
+
# the safe value): seal_build(model=) would reach the batch model path.
|
|
88
|
+
OMITTED_PARAMS: frozenset[tuple[str, str]] = frozenset({("seal_build", "model")})
|
|
89
|
+
|
|
90
|
+
_OUTPUT_SCHEMA = {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {"result": {}}, # the envelope: {"result": <JSON value>}
|
|
93
|
+
"required": ["result"],
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _annotation_to_schema(ann: Any) -> dict:
|
|
98
|
+
"""A frozen-signature annotation → JSON schema (explicit map, no guessing)."""
|
|
99
|
+
if ann is inspect.Parameter.empty or ann is Any:
|
|
100
|
+
return {}
|
|
101
|
+
origin = typing.get_origin(ann)
|
|
102
|
+
if origin in (typing.Union, _pytypes.UnionType):
|
|
103
|
+
args = typing.get_args(ann)
|
|
104
|
+
parts = [_annotation_to_schema(a) for a in args]
|
|
105
|
+
if type(None) in args and len(args) == 2:
|
|
106
|
+
other = next(a for a in args if a is not type(None))
|
|
107
|
+
inner = _annotation_to_schema(other)
|
|
108
|
+
if list(inner.keys()) == ["type"]:
|
|
109
|
+
return {"type": [inner["type"], "null"]}
|
|
110
|
+
return {"anyOf": parts}
|
|
111
|
+
if origin is list:
|
|
112
|
+
(item,) = typing.get_args(ann) or (Any,)
|
|
113
|
+
return {"type": "array", "items": _annotation_to_schema(item)}
|
|
114
|
+
return {
|
|
115
|
+
str: {"type": "string"}, bool: {"type": "boolean"},
|
|
116
|
+
int: {"type": "integer"}, float: {"type": "number"},
|
|
117
|
+
dict: {"type": "object"}, type(None): {"type": "null"},
|
|
118
|
+
}.get(ann, {})
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _input_schema(name: str) -> dict:
|
|
122
|
+
from patternbuffer.porcelain import Porcelain
|
|
123
|
+
method = getattr(Porcelain, name)
|
|
124
|
+
sig = inspect.signature(method)
|
|
125
|
+
# Porcelain uses postponed annotations (`from __future__ import
|
|
126
|
+
# annotations`), so signature annotations are STRINGS — resolve them to
|
|
127
|
+
# runtime types first (Cx 533 #1: comparing strings against types silently
|
|
128
|
+
# emitted `{}` for 83 params, letting typed garbage cross SDK validation).
|
|
129
|
+
hints = typing.get_type_hints(method)
|
|
130
|
+
props: dict[str, dict] = {}
|
|
131
|
+
required: list[str] = []
|
|
132
|
+
for p in list(sig.parameters.values())[1:]: # skip self
|
|
133
|
+
if (name, p.name) in OMITTED_PARAMS:
|
|
134
|
+
continue
|
|
135
|
+
schema = SCHEMA_OVERRIDES.get((name, p.name))
|
|
136
|
+
if schema is None:
|
|
137
|
+
ann = hints.get(p.name, inspect.Parameter.empty)
|
|
138
|
+
schema = _annotation_to_schema(ann)
|
|
139
|
+
if not schema and ann not in (inspect.Parameter.empty, Any):
|
|
140
|
+
# an annotated param MUST map to a real schema — a silent {}
|
|
141
|
+
# is exactly the 533 failure mode; fail at registry build.
|
|
142
|
+
raise TypeError(f"{name}.{p.name}: unmapped annotation {ann!r}")
|
|
143
|
+
if p.default is not inspect.Parameter.empty and p.default is not None:
|
|
144
|
+
schema = {**schema, "default": p.default}
|
|
145
|
+
props[p.name] = schema
|
|
146
|
+
if p.default is inspect.Parameter.empty:
|
|
147
|
+
required.append(p.name)
|
|
148
|
+
out = {"type": "object", "properties": props, "additionalProperties": False}
|
|
149
|
+
if required:
|
|
150
|
+
out["required"] = required
|
|
151
|
+
return out
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _annotations(name: str) -> dict:
|
|
155
|
+
"""The exact ToolAnnotations table (Cx 521 #4) — explicit complement."""
|
|
156
|
+
read = name in V1_READS
|
|
157
|
+
out = {
|
|
158
|
+
"readOnlyHint": read,
|
|
159
|
+
"idempotentHint": read or name in IDEMPOTENT_MUTATIONS,
|
|
160
|
+
"openWorldHint": False, # a bound local world is a closed domain
|
|
161
|
+
}
|
|
162
|
+
if not read:
|
|
163
|
+
out["destructiveHint"] = name in DESTRUCTIVE
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def build_registry() -> dict[str, dict]:
|
|
168
|
+
"""name -> {description, inputSchema, outputSchema, annotations}."""
|
|
169
|
+
from patternbuffer.porcelain import Porcelain
|
|
170
|
+
reg: dict[str, dict] = {}
|
|
171
|
+
for name in V1_TOOLS:
|
|
172
|
+
doc = (inspect.getdoc(getattr(Porcelain, name)) or name).strip()
|
|
173
|
+
reg[name] = {
|
|
174
|
+
"description": doc.split("\n\n")[0],
|
|
175
|
+
"inputSchema": _input_schema(name),
|
|
176
|
+
"outputSchema": _OUTPUT_SCHEMA,
|
|
177
|
+
"annotations": _annotations(name),
|
|
178
|
+
}
|
|
179
|
+
return reg
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ------------------------------------------------------------- dispatch
|
|
183
|
+
|
|
184
|
+
def _normalize(ret: Any) -> Any:
|
|
185
|
+
"""Porcelain return → plain JSON value (Receipt/any dataclass → dict)."""
|
|
186
|
+
if hasattr(ret, "to_dict"):
|
|
187
|
+
return ret.to_dict()
|
|
188
|
+
if dataclasses.is_dataclass(ret) and not isinstance(ret, type):
|
|
189
|
+
return dataclasses.asdict(ret)
|
|
190
|
+
return ret
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def dispatch(world: World, name: str, arguments: dict[str, Any],
|
|
194
|
+
lock: threading.Lock | None = None) -> dict:
|
|
195
|
+
"""Validate, call the porcelain verb, wrap in the wire envelope.
|
|
196
|
+
|
|
197
|
+
Runtime validation (Cx 521 #1) rejects the model-reaching modes BEFORE
|
|
198
|
+
dispatch — enforcement, not description. Mutations serialize under `lock`
|
|
199
|
+
(build-session state is server/world state, not per-request state)."""
|
|
200
|
+
if name not in V1_TOOLS:
|
|
201
|
+
raise ValueError(f"unknown tool {name!r}")
|
|
202
|
+
verb = getattr(world.porcelain, name)
|
|
203
|
+
allowed = set(inspect.signature(verb).parameters)
|
|
204
|
+
args = dict(arguments or {})
|
|
205
|
+
unknown = set(args) - allowed
|
|
206
|
+
if unknown:
|
|
207
|
+
raise ValueError(f"{name}: unknown argument(s) {sorted(unknown)}")
|
|
208
|
+
if name == "ingest_structured":
|
|
209
|
+
classify = args.setdefault("classify", "rules")
|
|
210
|
+
if classify not in ("rules", "defer"):
|
|
211
|
+
raise ValueError(
|
|
212
|
+
"ingest_structured over MCP accepts classify='rules'|'defer' "
|
|
213
|
+
"only — 'inline'/'batch' reach the model path, which this "
|
|
214
|
+
"no-model server does not carry (MCP-WRAPPER-V1)")
|
|
215
|
+
if name == "seal_build":
|
|
216
|
+
if "model" in args:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
"seal_build over MCP does not accept 'model' — the batch "
|
|
219
|
+
"model path is not carried by this no-model server; the seal "
|
|
220
|
+
"always runs rules-only (MCP-WRAPPER-V1)")
|
|
221
|
+
args["model"] = False
|
|
222
|
+
if name in V1_MUTATIONS and lock is not None:
|
|
223
|
+
with lock:
|
|
224
|
+
ret = verb(**args)
|
|
225
|
+
else:
|
|
226
|
+
ret = verb(**args)
|
|
227
|
+
return {"result": encode_out(_normalize(ret))}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------- the server
|
|
231
|
+
|
|
232
|
+
def build_server(world: World):
|
|
233
|
+
"""Bind the registry + dispatch to an MCP low-level Server (lazy SDK)."""
|
|
234
|
+
from mcp.server.lowlevel import Server # the [mcp] extra
|
|
235
|
+
from mcp import types
|
|
236
|
+
|
|
237
|
+
registry = build_registry()
|
|
238
|
+
lock = threading.Lock()
|
|
239
|
+
server = Server("patternbuffer")
|
|
240
|
+
|
|
241
|
+
@server.list_tools()
|
|
242
|
+
async def _list_tools() -> list:
|
|
243
|
+
return [
|
|
244
|
+
types.Tool(
|
|
245
|
+
name=name,
|
|
246
|
+
description=spec["description"],
|
|
247
|
+
inputSchema=spec["inputSchema"],
|
|
248
|
+
outputSchema=spec["outputSchema"],
|
|
249
|
+
annotations=types.ToolAnnotations(**spec["annotations"]),
|
|
250
|
+
)
|
|
251
|
+
for name, spec in registry.items()
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
@server.call_tool() # SDK validates input, wraps dict as structuredContent
|
|
255
|
+
async def _call_tool(name: str, arguments: dict) -> dict:
|
|
256
|
+
# Returning the envelope dict: the SDK places it in structuredContent
|
|
257
|
+
# AND serializes the same object into one TextContent block — the
|
|
258
|
+
# spec's wire convention, natively.
|
|
259
|
+
return dispatch(world, name, arguments, lock=lock)
|
|
260
|
+
|
|
261
|
+
return server
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def main(argv: list[str] | None = None) -> int:
|
|
265
|
+
"""`patternbuffer-mcp --world PATH --world-id ID` (env fallbacks)."""
|
|
266
|
+
parser = argparse.ArgumentParser(
|
|
267
|
+
prog="patternbuffer-mcp",
|
|
268
|
+
description="Serve one pattern-buffer world over MCP (stdio). "
|
|
269
|
+
"One server, one world (the 1:1 invariant).")
|
|
270
|
+
parser.add_argument("--world", default=os.environ.get("PATTERNBUFFER_WORLD"),
|
|
271
|
+
help="path to the .world file (env PATTERNBUFFER_WORLD)")
|
|
272
|
+
parser.add_argument("--world-id",
|
|
273
|
+
default=os.environ.get("PATTERNBUFFER_WORLD_ID"),
|
|
274
|
+
help="the world's id (env PATTERNBUFFER_WORLD_ID); "
|
|
275
|
+
"required — a mismatch against an existing file "
|
|
276
|
+
"fails loudly (WorldMismatch)")
|
|
277
|
+
args = parser.parse_args(argv)
|
|
278
|
+
if not args.world or not args.world_id:
|
|
279
|
+
parser.error("--world and --world-id are both required "
|
|
280
|
+
"(or PATTERNBUFFER_WORLD / PATTERNBUFFER_WORLD_ID)")
|
|
281
|
+
try:
|
|
282
|
+
from mcp.server.stdio import stdio_server
|
|
283
|
+
except ImportError:
|
|
284
|
+
parser.error("the MCP SDK is not installed — pip install 'pbuffer[mcp]'")
|
|
285
|
+
|
|
286
|
+
world = World(args.world, world_id=args.world_id) # no model callable
|
|
287
|
+
server = build_server(world)
|
|
288
|
+
|
|
289
|
+
import anyio
|
|
290
|
+
|
|
291
|
+
async def _run() -> None:
|
|
292
|
+
async with stdio_server() as (read, write):
|
|
293
|
+
await server.run(read, write, server.create_initialization_options())
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
anyio.run(_run)
|
|
297
|
+
finally:
|
|
298
|
+
world.close() # aborts any open build session; buffer closed
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
raise SystemExit(main())
|
patternbuffer/model.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""The assertion row shape and the engine's fixed vocabularies.
|
|
2
|
+
|
|
3
|
+
The model is triples-on-triples: assertions are addressable entities,
|
|
4
|
+
so everything *about* an assertion is itself an assertion. Hot fields
|
|
5
|
+
are denormalized into columns (whitepaper §3.2); nothing is ever
|
|
6
|
+
special-cased.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# Provenance vocabulary (whitepaper §7). `default` appears only in
|
|
15
|
+
# materialization payloads; it is included here because the renderer-
|
|
16
|
+
# facing payload reuses the vocabulary, but no role may append it.
|
|
17
|
+
STATUSES = frozenset(
|
|
18
|
+
{"stated", "observed", "inferred", "assumed", "generated", "default", "retracted"}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# The containment family folds as ONE logical key (spec §7, letter 002
|
|
22
|
+
# Q1): a new family edge supersedes the prior one as a single operation.
|
|
23
|
+
CONTAINMENT_FAMILY = frozenset({"in", "within", "held_by", "worn_by", "carried_by"})
|
|
24
|
+
|
|
25
|
+
# The compositional axis (PLACE-FEATURE-ABSTRACTION-V1): structural part-of-the-
|
|
26
|
+
# whole, distinct from movable containment. A burrow is `part_of` a hillside;
|
|
27
|
+
# an actor in the burrow is NOT thereby located in the hillside. Read by
|
|
28
|
+
# composition()/features(), never by locate()/contents()/route().
|
|
29
|
+
COMPOSITION_FAMILY = frozenset({"part_of"})
|
|
30
|
+
|
|
31
|
+
# Fixed structural predicates (spec §4.3). Domain vocabulary emerges
|
|
32
|
+
# freely through the canonicalization gate; these never do.
|
|
33
|
+
STRUCTURAL_PREDICATES = (
|
|
34
|
+
frozenset({"kind", "connects_to", "adjacent_to", "caused_by"})
|
|
35
|
+
| CONTAINMENT_FAMILY | COMPOSITION_FAMILY
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Attribute-semantics declarations are assertions about attr:<name> entities.
|
|
39
|
+
# These low-level names live here so the buffer guard can enforce authority
|
|
40
|
+
# without importing the semantics service.
|
|
41
|
+
ATTR_PREFIX = "attr:"
|
|
42
|
+
SEMANTICS_PREDICATES = frozenset(
|
|
43
|
+
{"arity", "relation_family", "fold_policy", "structural"}
|
|
44
|
+
)
|
|
45
|
+
INVIOLABLE_CORE = STRUCTURAL_PREDICATES | {"same_as", "maybe_same_as", "distinct_from", "aka"}
|
|
46
|
+
|
|
47
|
+
# The engine's own meta-attributes (subjects of these rows are assertion
|
|
48
|
+
# ids or carry engine semantics).
|
|
49
|
+
META_ATTRIBUTES = frozenset(
|
|
50
|
+
{
|
|
51
|
+
"superseded_by",
|
|
52
|
+
"retracts",
|
|
53
|
+
"source",
|
|
54
|
+
"same_as",
|
|
55
|
+
"maybe_same_as",
|
|
56
|
+
"distinct_from",
|
|
57
|
+
"aka",
|
|
58
|
+
"canonicalized_from",
|
|
59
|
+
"resolved_by",
|
|
60
|
+
"justified_by",
|
|
61
|
+
"world_defining",
|
|
62
|
+
"correction_proposal",
|
|
63
|
+
"source_valid_from", # INGEST-LATENCY-V2: demoted cursor-authoritative coord
|
|
64
|
+
}
|
|
65
|
+
) | SEMANTICS_PREDICATES
|
|
66
|
+
|
|
67
|
+
# Set-valued attributes: multiple coexisting values are data, never a
|
|
68
|
+
# contradiction — a conflict requires a functional key (run-4 finding:
|
|
69
|
+
# 62 of 77 flags were names/aliases/edges misread as disputes).
|
|
70
|
+
SET_VALUED_ATTRIBUTES = frozenset({"name", "alias", "connects_to", "adjacent_to",
|
|
71
|
+
"maybe_same_as", "same_as", "distinct_from", "aka"})
|
|
72
|
+
|
|
73
|
+
VALUE_TYPES = frozenset({"entity", "literal", "unresolved", "delta"})
|
|
74
|
+
|
|
75
|
+
CANON = "canon"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class Assertion:
|
|
80
|
+
"""One fact. Append-only; never edited (whitepaper §3.2).
|
|
81
|
+
|
|
82
|
+
``valid_from``/``valid_to`` are world time (None = timeless, legal
|
|
83
|
+
only for CONSTITUTIVE/DISPOSITIONAL rows — enforced at the gate, not
|
|
84
|
+
here). ``asserted_at`` is transaction time: the log sequence number,
|
|
85
|
+
permanently (spec §4.2).
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
seq: int
|
|
89
|
+
id: str
|
|
90
|
+
world_id: str
|
|
91
|
+
entity: str
|
|
92
|
+
attribute: str
|
|
93
|
+
value_type: str
|
|
94
|
+
value: Any
|
|
95
|
+
valid_from: float | None
|
|
96
|
+
valid_to: float | None
|
|
97
|
+
frame: str
|
|
98
|
+
status: str
|
|
99
|
+
confidence: float | None
|
|
100
|
+
asserted_at: int
|