pbuffer 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {pbuffer-0.1.0/src/pbuffer.egg-info → pbuffer-0.2.0}/PKG-INFO +20 -5
- {pbuffer-0.1.0 → pbuffer-0.2.0}/README.md +19 -4
- {pbuffer-0.1.0 → pbuffer-0.2.0}/pyproject.toml +1 -1
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/__init__.py +9 -1
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/classify.py +6 -1
- pbuffer-0.2.0/src/patternbuffer/decay.py +62 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/indexes.py +151 -45
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/ingest.py +53 -8
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/porcelain.py +6 -1
- {pbuffer-0.1.0 → pbuffer-0.2.0/src/pbuffer.egg-info}/PKG-INFO +20 -5
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/pbuffer.egg-info/SOURCES.txt +3 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_confidence.py +21 -6
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_confidence_multiframe.py +9 -2
- pbuffer-0.2.0/tests/test_ingestion_fidelity_v2.py +323 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_shape_fix.py +20 -2
- pbuffer-0.2.0/tests/test_tracking_mode.py +517 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/LICENSE +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/setup.cfg +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/buffer.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/codec.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/dump.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/identity.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/mcp.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/model.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/project.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/refer.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/roles.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/salience.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/semantics.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/testing.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/thunks.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/tmaint.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/pbuffer.egg-info/dependency_links.txt +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/pbuffer.egg-info/entry_points.txt +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/pbuffer.egg-info/requires.txt +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/src/pbuffer.egg-info/top_level.txt +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_aka_correlation.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_awareness_projection.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_axis_heads.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_bounded_reads.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_buffer.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_build_session.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_classifier_event_safety.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_dump.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_exact_decimal.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_fidelity_audit.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_fold.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_fold_resolution.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_grammar.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_identity_containment_veto.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_identity_recall.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_identity_thunks.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_individuation_v2.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_ingest_frame_default.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_ingest_hardening.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_ingest_v2.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_mcp_wrapper.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_merge_verb.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_path_temporal.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_place_feature.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_porcelain.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_route.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_scaffold.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_situation_lens.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_triage_context.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_who_knows.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_world.py +0 -0
- {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_world_retrieval.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pbuffer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
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
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -27,8 +27,9 @@ One append-only log of perspective-scoped, time-indexed assertions about entitie
|
|
|
27
27
|
|
|
28
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
29
|
|
|
30
|
-
**Status: pre-1.0; substrate validated
|
|
31
|
-
front.** The engine passes its
|
|
30
|
+
**Status: pre-1.0; substrate validated in BOTH modes, the ingestion-fidelity
|
|
31
|
+
front closed by a measured contract.** The engine passes its 482-test
|
|
32
|
+
invariant suite, and the chapter test
|
|
32
33
|
(ingest a complete noir mystery, delete the prose, interrogate the store
|
|
33
34
|
against a hand-authored answer key) has been graded across three full runs.
|
|
34
35
|
The substrate's invariants held in all of them — sealed containers stayed
|
|
@@ -43,8 +44,20 @@ single commit) replaced serial prompt-iterated extraction after measurement
|
|
|
43
44
|
showed prose contracts fragment differently every round; identity and key
|
|
44
45
|
fragmentation died as failure classes, and pass-0 registry quality is where
|
|
45
46
|
the remaining extraction failures concentrate. Run 4, with its checkable
|
|
46
|
-
predictions, is in flight.
|
|
47
|
-
|
|
47
|
+
predictions, is in flight. Tracking mode is now proven as more than a policy
|
|
48
|
+
flag: trust decays with **unconfirmed wall time** under declared per-attribute
|
|
49
|
+
half-lives, while in fiction the page stays true forever — same log, same
|
|
50
|
+
reads, two honest physics.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install pbuffer # the engine (zero dependencies) — import patternbuffer
|
|
56
|
+
pip install pbuffer[mcp] # + the MCP server: patternbuffer-mcp --world w.world --world-id w:id
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
*(The PyPI distribution is `pbuffer` — the exact name `patternbuffer` was taken
|
|
60
|
+
by an unrelated project; the import is `patternbuffer` throughout.)*
|
|
48
61
|
|
|
49
62
|
## What this is
|
|
50
63
|
|
|
@@ -71,6 +84,8 @@ The buffer holds the pattern; materialization is re-entry; degradation is the fa
|
|
|
71
84
|
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
85
|
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
86
|
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.
|
|
87
|
+
9. ~~Tracking mode: the second-mode thesis, proven~~ (TRACKING-MODE-V1). Trust and story time are now formally separate axes. In **fiction**, confidence recency is *permanent* — the page is true, and story-time liveness belongs to salience, not trust. In **tracking** worlds (`observe_or_unknown`), trust decays with **unconfirmed wall time** under declared per-attribute half-lives (`DecayPolicy` — data, not config: `attr:in · decay_halflife_seconds · 2 days`), fail-closed (`unconfigured`/`unconfirmed` exclude recency and renormalize, never fake a score) with a finite-clock contract (a broken clock can never manufacture freshness). `confidence()` carries `recency`, `recency_status`, `last_confirmed_at_wallclock` on every result — "last confirmed June 19, 22 days unconfirmed" is one join. The mode selector is the *policy*, never a stance judgment: the engine applies **no plausibility bias** to observer reports in either mode. Receipts: `evals/the_grey_house/` (the replayable tracking eval), 14/14 scorecard.
|
|
88
|
+
10. ~~The ingestion-fidelity contract, evidence-gated end to end~~ (INGESTION-FIDELITY-V1 → V2). V1 shipped `fidelity_audit()` — the structural gap read (coreference collision groups, unstamped timed facts, orphans, open conflicts) the host joins for severity. The first live host's full rebuild then delivered the measured signals: the coreference seal took live collisions 8→4 with zero severe, and the audit residue decomposed into exactly one real defect class — standing personal properties (`build`, `occupation`, kinship, `can_*`) authored as timed state. V2 shipped that contract narrowly: timeless means *true across the world's whole history* (facts of origin only — everything acquired gets its earliest supported time; age is never timeless), the classifier prefers standing classes for enduring-baseline traits, kinship-of-origin is one host declaration (`attribute_default`), and merge-induced self-edges receipt distinctly (`merged_self_edge`). The remaining candidate (background-homonym coreference) was **parked forever with cause** — every residual group unvouched, none load-bearing, replay-proved harmless. Machinery without a measured victim doesn't ship.
|
|
74
89
|
|
|
75
90
|
## License
|
|
76
91
|
|
|
@@ -13,8 +13,9 @@ One append-only log of perspective-scoped, time-indexed assertions about entitie
|
|
|
13
13
|
|
|
14
14
|
> 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.
|
|
15
15
|
|
|
16
|
-
**Status: pre-1.0; substrate validated
|
|
17
|
-
front.** The engine passes its
|
|
16
|
+
**Status: pre-1.0; substrate validated in BOTH modes, the ingestion-fidelity
|
|
17
|
+
front closed by a measured contract.** The engine passes its 482-test
|
|
18
|
+
invariant suite, and the chapter test
|
|
18
19
|
(ingest a complete noir mystery, delete the prose, interrogate the store
|
|
19
20
|
against a hand-authored answer key) has been graded across three full runs.
|
|
20
21
|
The substrate's invariants held in all of them — sealed containers stayed
|
|
@@ -29,8 +30,20 @@ single commit) replaced serial prompt-iterated extraction after measurement
|
|
|
29
30
|
showed prose contracts fragment differently every round; identity and key
|
|
30
31
|
fragmentation died as failure classes, and pass-0 registry quality is where
|
|
31
32
|
the remaining extraction failures concentrate. Run 4, with its checkable
|
|
32
|
-
predictions, is in flight.
|
|
33
|
-
|
|
33
|
+
predictions, is in flight. Tracking mode is now proven as more than a policy
|
|
34
|
+
flag: trust decays with **unconfirmed wall time** under declared per-attribute
|
|
35
|
+
half-lives, while in fiction the page stays true forever — same log, same
|
|
36
|
+
reads, two honest physics.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install pbuffer # the engine (zero dependencies) — import patternbuffer
|
|
42
|
+
pip install pbuffer[mcp] # + the MCP server: patternbuffer-mcp --world w.world --world-id w:id
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
*(The PyPI distribution is `pbuffer` — the exact name `patternbuffer` was taken
|
|
46
|
+
by an unrelated project; the import is `patternbuffer` throughout.)*
|
|
34
47
|
|
|
35
48
|
## What this is
|
|
36
49
|
|
|
@@ -57,6 +70,8 @@ The buffer holds the pattern; materialization is re-entry; degradation is the fa
|
|
|
57
70
|
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.)
|
|
58
71
|
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.
|
|
59
72
|
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.
|
|
73
|
+
9. ~~Tracking mode: the second-mode thesis, proven~~ (TRACKING-MODE-V1). Trust and story time are now formally separate axes. In **fiction**, confidence recency is *permanent* — the page is true, and story-time liveness belongs to salience, not trust. In **tracking** worlds (`observe_or_unknown`), trust decays with **unconfirmed wall time** under declared per-attribute half-lives (`DecayPolicy` — data, not config: `attr:in · decay_halflife_seconds · 2 days`), fail-closed (`unconfigured`/`unconfirmed` exclude recency and renormalize, never fake a score) with a finite-clock contract (a broken clock can never manufacture freshness). `confidence()` carries `recency`, `recency_status`, `last_confirmed_at_wallclock` on every result — "last confirmed June 19, 22 days unconfirmed" is one join. The mode selector is the *policy*, never a stance judgment: the engine applies **no plausibility bias** to observer reports in either mode. Receipts: `evals/the_grey_house/` (the replayable tracking eval), 14/14 scorecard.
|
|
74
|
+
10. ~~The ingestion-fidelity contract, evidence-gated end to end~~ (INGESTION-FIDELITY-V1 → V2). V1 shipped `fidelity_audit()` — the structural gap read (coreference collision groups, unstamped timed facts, orphans, open conflicts) the host joins for severity. The first live host's full rebuild then delivered the measured signals: the coreference seal took live collisions 8→4 with zero severe, and the audit residue decomposed into exactly one real defect class — standing personal properties (`build`, `occupation`, kinship, `can_*`) authored as timed state. V2 shipped that contract narrowly: timeless means *true across the world's whole history* (facts of origin only — everything acquired gets its earliest supported time; age is never timeless), the classifier prefers standing classes for enduring-baseline traits, kinship-of-origin is one host declaration (`attribute_default`), and merge-induced self-edges receipt distinctly (`merged_self_edge`). The remaining candidate (background-homonym coreference) was **parked forever with cause** — every residual group unvouched, none load-bearing, replay-proved harmless. Machinery without a measured victim doesn't ship.
|
|
60
75
|
|
|
61
76
|
## License
|
|
62
77
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pbuffer"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -76,6 +76,10 @@ class World:
|
|
|
76
76
|
)
|
|
77
77
|
self.indexes.set_closure_provider(self.registry.closure)
|
|
78
78
|
self.indexes.set_correlation_provider(self.registry.correlation_set)
|
|
79
|
+
# TRACKING-MODE-V1: tracking-vs-non-tracking selects on policy ONLY
|
|
80
|
+
# (never stance — the no-bias invariant); deny is a resolution policy,
|
|
81
|
+
# not an epistemic mode, so it takes the non-tracking branch.
|
|
82
|
+
self.indexes.set_time_physics(policy == OBSERVE_OR_UNKNOWN, clock)
|
|
79
83
|
self.registry.set_kind_provider(lambda e: self.indexes.fold_key(e, "kind"))
|
|
80
84
|
self.salience_index = SalienceIndex(self.buffer, self.classifier, self.indexes)
|
|
81
85
|
self.indexes.set_salience_provider(self.salience_index.salience)
|
|
@@ -192,10 +196,14 @@ class World:
|
|
|
192
196
|
frame: str | list[str] = CANON,
|
|
193
197
|
as_of: float | None = None,
|
|
194
198
|
asserted_as_of: int | None = None,
|
|
199
|
+
now: float | None = None,
|
|
195
200
|
) -> dict:
|
|
201
|
+
# `now` is WALL time (TRACKING-MODE-V1): the staleness reference in
|
|
202
|
+
# tracking worlds, defaulting to the injected clock; unused in fiction.
|
|
203
|
+
# Never a second meaning for as_of (which stays valid time).
|
|
196
204
|
return self.indexes.confidence(
|
|
197
205
|
entity, attribute, frame=frame, as_of=as_of,
|
|
198
|
-
asserted_as_of=asserted_as_of,
|
|
206
|
+
asserted_as_of=asserted_as_of, now=now,
|
|
199
207
|
)
|
|
200
208
|
|
|
201
209
|
def neighborhood(self, entity: str, **kw) -> dict:
|
|
@@ -145,7 +145,12 @@ class Classifier:
|
|
|
145
145
|
"STATE: what it is RIGHT NOW (positions of movables, moods, conditions). "
|
|
146
146
|
"One event could flip it.\n\n"
|
|
147
147
|
"The mutability test resolves most ambiguity: could one event flip this "
|
|
148
|
-
"without re-authoring the world? -> STATE.
|
|
148
|
+
"without re-authoring the world? -> STATE. When the text presents a "
|
|
149
|
+
"property as an enduring baseline of the person — a physical trait, a "
|
|
150
|
+
"capability, a continuing role — prefer CONSTITUTIVE (what they are) or "
|
|
151
|
+
"DISPOSITIONAL (how they tend) over STATE; a temporary ability or "
|
|
152
|
+
"condition, or a time-relative quantity (age), remains STATE. "
|
|
153
|
+
"Asymmetric defaults: an "
|
|
149
154
|
"ambiguous property is STATE; ambiguous fixture containment is "
|
|
150
155
|
"CONSTITUTIVE."
|
|
151
156
|
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""DecayPolicy: tracking-world time physics as declared, rebuildable data.
|
|
2
|
+
|
|
3
|
+
TRACKING-MODE-V1 §B3: in an `observe_or_unknown` world, a fact's freshness
|
|
4
|
+
decays with UNCONFIRMED WALL TIME — per-attribute half-lives declared as
|
|
5
|
+
ordinary rows (never stored results; the policy is read fresh at each read and
|
|
6
|
+
rebuilds from the log). Deliberately SEPARATE from `AttributeSemantics`: decay
|
|
7
|
+
is world physics, not fold semantics — a declaration on the inviolable-core
|
|
8
|
+
`in` key must configure, not be rejected.
|
|
9
|
+
|
|
10
|
+
Declaration rows: `attr:<key> · decay_halflife_seconds · <finite positive n>`
|
|
11
|
+
World default: `attr:__world__ · decay_halflife_seconds · <n>`
|
|
12
|
+
|
|
13
|
+
Lookup for a served winner authored as attribute A (deterministic, Cx 563):
|
|
14
|
+
1. `attr:<A>` — exact policy for the winner's canonical authored attribute;
|
|
15
|
+
2. if A is in the containment family: `attr:in` — the public family subject
|
|
16
|
+
(the private `__containment__` fold sentinel is never host vocabulary);
|
|
17
|
+
3. `attr:__world__` — the world default.
|
|
18
|
+
First hit wins. Later declarations fold at current head (latest visible per
|
|
19
|
+
subject). Policy resolution is CURRENT-physics-over-history by design: as-of
|
|
20
|
+
confidence applies today's declared physics to historical facts (documented).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import math
|
|
26
|
+
|
|
27
|
+
from patternbuffer.buffer import PatternBuffer
|
|
28
|
+
|
|
29
|
+
PREDICATE = "decay_halflife_seconds"
|
|
30
|
+
WORLD_SUBJECT = "attr:__world__"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DecayPolicy:
|
|
34
|
+
def __init__(self, buffer: PatternBuffer, semantics) -> None:
|
|
35
|
+
self._buffer = buffer
|
|
36
|
+
self._semantics = semantics
|
|
37
|
+
|
|
38
|
+
def resolve(self, authored_attribute: str) -> float | None:
|
|
39
|
+
"""The half-life (seconds) governing a winner authored as
|
|
40
|
+
`authored_attribute`, or None (unconfigured)."""
|
|
41
|
+
v = self._latest(f"attr:{authored_attribute}")
|
|
42
|
+
if v is not None:
|
|
43
|
+
return v
|
|
44
|
+
family = (self._semantics.containment_family()
|
|
45
|
+
if self._semantics is not None else frozenset())
|
|
46
|
+
if authored_attribute in family and authored_attribute != "in":
|
|
47
|
+
v = self._latest("attr:in")
|
|
48
|
+
if v is not None:
|
|
49
|
+
return v
|
|
50
|
+
return self._latest(WORLD_SUBJECT)
|
|
51
|
+
|
|
52
|
+
def _latest(self, subject: str) -> float | None:
|
|
53
|
+
"""The latest visible valid declaration on `subject` (ordinary
|
|
54
|
+
supersession at current head); malformed values never activate."""
|
|
55
|
+
best = None
|
|
56
|
+
for row in self._buffer.visible(entity=subject, attribute=PREDICATE):
|
|
57
|
+
v = row.value
|
|
58
|
+
if (isinstance(v, (int, float)) and not isinstance(v, bool)
|
|
59
|
+
and math.isfinite(v) and v > 0):
|
|
60
|
+
if best is None or row.asserted_at > best.asserted_at:
|
|
61
|
+
best = row
|
|
62
|
+
return float(best.value) if best is not None else None
|
|
@@ -26,6 +26,7 @@ from patternbuffer.codec import (
|
|
|
26
26
|
exact_div,
|
|
27
27
|
exact_sum,
|
|
28
28
|
)
|
|
29
|
+
from patternbuffer.decay import DecayPolicy
|
|
29
30
|
from patternbuffer.model import ATTR_PREFIX, CANON, META_ATTRIBUTES, Assertion
|
|
30
31
|
from patternbuffer.semantics import AttributeSemantics, CONTAINMENT, builtin_default
|
|
31
32
|
|
|
@@ -108,6 +109,19 @@ class Indexes:
|
|
|
108
109
|
lambda eid, valid_as_of=None, asserted_as_of=None, frame=CANON: {eid}
|
|
109
110
|
)
|
|
110
111
|
self._salience = lambda eid, frame=CANON, as_of=None: 0.0
|
|
112
|
+
# TRACKING-MODE-V1: time physics. `_tracking` selects on
|
|
113
|
+
# policy == "observe_or_unknown" ONLY (never stance — the no-bias
|
|
114
|
+
# invariant); `_clock` is the World's injected wall clock; `_decay`
|
|
115
|
+
# reads declared half-lives fresh at each read (rebuildable).
|
|
116
|
+
self._tracking = False
|
|
117
|
+
self._clock: Callable[[], float] = lambda: 0.0
|
|
118
|
+
self._decay = DecayPolicy(buffer, self._semantics)
|
|
119
|
+
|
|
120
|
+
def set_time_physics(self, tracking: bool, clock: Callable[[], float]) -> None:
|
|
121
|
+
"""World wiring (TRACKING-MODE-V1): tracking = the operational selector
|
|
122
|
+
(`policy == "observe_or_unknown"`); clock = the injected wall clock."""
|
|
123
|
+
self._tracking = tracking
|
|
124
|
+
self._clock = clock
|
|
111
125
|
|
|
112
126
|
def set_identity_resolver(self, fn: Callable[[str], str]) -> None:
|
|
113
127
|
self._resolve = fn
|
|
@@ -207,8 +221,95 @@ class Indexes:
|
|
|
207
221
|
"last_observed_at": None,
|
|
208
222
|
"corroboration": 0,
|
|
209
223
|
"conflicted": False,
|
|
224
|
+
# TRACKING-MODE-V1: one payload shape everywhere — the three
|
|
225
|
+
# additive fields are present (null) on empty/set-valued/accrue.
|
|
226
|
+
"recency": None,
|
|
227
|
+
"recency_status": None,
|
|
228
|
+
"last_confirmed_at_wallclock": None,
|
|
210
229
|
}
|
|
211
230
|
|
|
231
|
+
def _time_trust(
|
|
232
|
+
self,
|
|
233
|
+
winner: Assertion,
|
|
234
|
+
entity: str,
|
|
235
|
+
attribute: str,
|
|
236
|
+
frames: list[str],
|
|
237
|
+
as_of: float | None,
|
|
238
|
+
asserted_as_of: int | None,
|
|
239
|
+
now: float | None,
|
|
240
|
+
) -> tuple[float | None, str, float | None]:
|
|
241
|
+
"""(recency, recency_status, last_confirmed_at_wallclock) —
|
|
242
|
+
TRACKING-MODE-V1's frozen branches.
|
|
243
|
+
|
|
244
|
+
Non-tracking (invent_under_canon / deny): ("permanent", 1.0, None) —
|
|
245
|
+
the page is true; story-time recency is salience's business (liveness),
|
|
246
|
+
never trust's. Tracking: recency = 2**(−age/half_life) with
|
|
247
|
+
age = max(0, now − stamp); "unconfigured"/"unconfirmed" return
|
|
248
|
+
recency=None (excluded + renormalized upstream — fail-closed: neither
|
|
249
|
+
fake permanence nor fake staleness)."""
|
|
250
|
+
if not self._tracking:
|
|
251
|
+
return 1.0, "permanent", None
|
|
252
|
+
# The finite-clock contract (Cx 604 #1), validated FIRST — before any
|
|
253
|
+
# branch can short-circuit: wall time must be a finite real number. A
|
|
254
|
+
# NaN stamp would make max(0, now−NaN) select 0.0 — a broken clock
|
|
255
|
+
# fabricating PERFECT freshness. A non-finite explicit/injected `now`
|
|
256
|
+
# fails loudly on every tracking read; non-finite STORED stamps never
|
|
257
|
+
# qualify as confirmations (enforced in _confirmation_stamp).
|
|
258
|
+
ref_now = float(now) if now is not None else float(self._clock())
|
|
259
|
+
if not math.isfinite(ref_now):
|
|
260
|
+
raise ValueError(
|
|
261
|
+
f"wall-clock 'now' must be finite, got {ref_now!r} "
|
|
262
|
+
"(a broken clock must fail, not fabricate freshness)")
|
|
263
|
+
half_life = self._decay.resolve(winner.attribute)
|
|
264
|
+
if half_life is None:
|
|
265
|
+
return None, "unconfigured", None
|
|
266
|
+
stamp = self._confirmation_stamp(
|
|
267
|
+
winner, entity, attribute, frames, as_of, asserted_as_of
|
|
268
|
+
)
|
|
269
|
+
if stamp is None:
|
|
270
|
+
return None, "unconfirmed", None
|
|
271
|
+
age = max(0.0, ref_now - float(stamp))
|
|
272
|
+
return 2.0 ** (-(age / float(half_life))), "configured", float(stamp)
|
|
273
|
+
|
|
274
|
+
def _confirmation_stamp(
|
|
275
|
+
self,
|
|
276
|
+
winner: Assertion,
|
|
277
|
+
entity: str,
|
|
278
|
+
attribute: str,
|
|
279
|
+
frames: list[str],
|
|
280
|
+
as_of: float | None,
|
|
281
|
+
asserted_as_of: int | None,
|
|
282
|
+
) -> float | None:
|
|
283
|
+
"""The latest visible A1 wall stamp among rows that CONFIRM the served
|
|
284
|
+
value: `stated`/`observed` provenance only, strict value-equivalence to
|
|
285
|
+
the winner, frame- and asserted_as_of-respecting (TRACKING-MODE-V1 §B4).
|
|
286
|
+
An assumed/inferred/conflicting/different-value row never refreshes it.
|
|
287
|
+
Fail-closed: a tracking row missing its mandatory stamp contributes no
|
|
288
|
+
confirmation."""
|
|
289
|
+
best: float | None = None
|
|
290
|
+
for f in frames:
|
|
291
|
+
for row in self._visible_key_rows(
|
|
292
|
+
entity, attribute, frame=f,
|
|
293
|
+
valid_as_of=as_of, asserted_as_of=asserted_as_of,
|
|
294
|
+
):
|
|
295
|
+
if row.status not in ("stated", "observed"):
|
|
296
|
+
continue
|
|
297
|
+
if not self._confidence_values_equivalent(row, winner):
|
|
298
|
+
continue
|
|
299
|
+
for meta in self._buffer.visible(
|
|
300
|
+
entity=row.id, attribute="learned_at_wallclock",
|
|
301
|
+
asserted_as_of=asserted_as_of,
|
|
302
|
+
):
|
|
303
|
+
v = meta.value
|
|
304
|
+
# FINITE only (Cx 604 #1): a NaN/inf stamp must fall to
|
|
305
|
+
# the honest `unconfirmed` branch, never qualify as a
|
|
306
|
+
# confirmation (fail-closed).
|
|
307
|
+
if (isinstance(v, (int, float)) and not isinstance(v, bool)
|
|
308
|
+
and math.isfinite(v)):
|
|
309
|
+
if best is None or float(v) > best:
|
|
310
|
+
best = float(v)
|
|
311
|
+
return best
|
|
312
|
+
|
|
212
313
|
def _confidence_values_equivalent(self, left: Assertion, right: Assertion) -> bool:
|
|
213
314
|
# Corroboration is "same value" (post-impl review): STRICT equivalence,
|
|
214
315
|
# not the loose approximate-bounds agreement — a {gte:40} bound must not
|
|
@@ -387,6 +488,7 @@ class Indexes:
|
|
|
387
488
|
frame: str | list[str] = CANON,
|
|
388
489
|
as_of: float | None = None,
|
|
389
490
|
asserted_as_of: int | None = None,
|
|
491
|
+
now: float | None = None,
|
|
390
492
|
) -> dict:
|
|
391
493
|
"""Derived trust score for one functional folded key.
|
|
392
494
|
|
|
@@ -406,7 +508,7 @@ class Indexes:
|
|
|
406
508
|
"""
|
|
407
509
|
if not isinstance(frame, str):
|
|
408
510
|
return self._confidence_multiframe(
|
|
409
|
-
entity, attribute, list(frame), as_of, asserted_as_of
|
|
511
|
+
entity, attribute, list(frame), as_of, asserted_as_of, now
|
|
410
512
|
)
|
|
411
513
|
entity = self._resolve(entity)
|
|
412
514
|
if self._semantics.is_set_valued(attribute) or self._semantics.is_accrue(attribute):
|
|
@@ -431,22 +533,13 @@ class Indexes:
|
|
|
431
533
|
# stated fact (post-impl review).
|
|
432
534
|
provenance = min(provenance, max(0.0, min(1.0, winner.confidence)))
|
|
433
535
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
frame=frame,
|
|
442
|
-
asserted_as_of=asserted_as_of,
|
|
443
|
-
)
|
|
444
|
-
ref = max(
|
|
445
|
-
(row.valid_from for row in closure_rows if row.valid_from is not None),
|
|
446
|
-
default=winner.valid_from,
|
|
447
|
-
)
|
|
448
|
-
age = max(0.0, float(ref) - float(winner.valid_from))
|
|
449
|
-
recency = 1.0 / (1.0 + age / params["recency_scale"])
|
|
536
|
+
# TRACKING-MODE-V1: the recency component. Non-tracking = permanent
|
|
537
|
+
# (the fiction anti-decay amendment — story-time decay removed from
|
|
538
|
+
# trust; liveness is salience's axis). Tracking = wall-clock age under
|
|
539
|
+
# the declared DecayPolicy, with the fail-closed null branches.
|
|
540
|
+
recency, recency_status, confirmed_at = self._time_trust(
|
|
541
|
+
winner, entity, attribute, [frame], as_of, asserted_as_of, now
|
|
542
|
+
)
|
|
450
543
|
|
|
451
544
|
source_classes = {self._source_class(winner, asserted_as_of)}
|
|
452
545
|
for assertion_id in fold.corroborated_by:
|
|
@@ -469,11 +562,20 @@ class Indexes:
|
|
|
469
562
|
)
|
|
470
563
|
|
|
471
564
|
weights = params["weights"]
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
+ weights["corroboration"]
|
|
476
|
-
|
|
565
|
+
if recency is None:
|
|
566
|
+
# unconfigured/unconfirmed: the recency term is EXCLUDED and its
|
|
567
|
+
# weight renormalized over the remaining components (fail-closed).
|
|
568
|
+
denom = weights["provenance"] + weights["corroboration"]
|
|
569
|
+
score = (
|
|
570
|
+
weights["provenance"] * provenance
|
|
571
|
+
+ weights["corroboration"] * corroboration_score
|
|
572
|
+
) / denom
|
|
573
|
+
else:
|
|
574
|
+
score = (
|
|
575
|
+
weights["provenance"] * provenance
|
|
576
|
+
+ weights["recency"] * recency
|
|
577
|
+
+ weights["corroboration"] * corroboration_score
|
|
578
|
+
)
|
|
477
579
|
if fold.conflicted:
|
|
478
580
|
score *= 0.5
|
|
479
581
|
score = min(1.0, max(0.0, score))
|
|
@@ -483,6 +585,9 @@ class Indexes:
|
|
|
483
585
|
"last_observed_at": winner.valid_from,
|
|
484
586
|
"corroboration": corroboration,
|
|
485
587
|
"conflicted": fold.conflicted,
|
|
588
|
+
"recency": recency,
|
|
589
|
+
"recency_status": recency_status,
|
|
590
|
+
"last_confirmed_at_wallclock": confirmed_at,
|
|
486
591
|
}
|
|
487
592
|
|
|
488
593
|
def _confidence_multiframe(
|
|
@@ -492,6 +597,7 @@ class Indexes:
|
|
|
492
597
|
frames: list[str],
|
|
493
598
|
as_of: float | None,
|
|
494
599
|
asserted_as_of: int | None,
|
|
600
|
+
now: float | None = None,
|
|
495
601
|
) -> dict:
|
|
496
602
|
"""Confidence over the read-union of `frames` (CONFIDENCE-MULTIFRAME-V1).
|
|
497
603
|
|
|
@@ -514,7 +620,8 @@ class Indexes:
|
|
|
514
620
|
if len(ordered) == 1:
|
|
515
621
|
# Reduction: a single distinct frame is byte-identical to the str
|
|
516
622
|
# path (and inherits its full V1 corroboration recovery).
|
|
517
|
-
return self.confidence(entity, attribute, ordered[0], as_of,
|
|
623
|
+
return self.confidence(entity, attribute, ordered[0], as_of,
|
|
624
|
+
asserted_as_of, now)
|
|
518
625
|
|
|
519
626
|
entity = self._resolve(entity)
|
|
520
627
|
if self._semantics.is_set_valued(attribute) or self._semantics.is_accrue(attribute):
|
|
@@ -555,23 +662,12 @@ class Indexes:
|
|
|
555
662
|
if winner.confidence is not None:
|
|
556
663
|
provenance = min(provenance, max(0.0, min(1.0, winner.confidence)))
|
|
557
664
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
refs = [
|
|
565
|
-
row.valid_from
|
|
566
|
-
for f in ordered
|
|
567
|
-
for row in self._buffer.visible(
|
|
568
|
-
entity_in=closure, frame=f, asserted_as_of=asserted_as_of
|
|
569
|
-
)
|
|
570
|
-
if row.valid_from is not None
|
|
571
|
-
]
|
|
572
|
-
ref = max(refs) if refs else winner.valid_from
|
|
573
|
-
age = max(0.0, float(ref) - float(winner.valid_from))
|
|
574
|
-
recency = 1.0 / (1.0 + age / params["recency_scale"])
|
|
665
|
+
# TRACKING-MODE-V1: same frozen branches as the single-frame path —
|
|
666
|
+
# one payload shape, the stamp taken per the EFFECTIVE winner over the
|
|
667
|
+
# frame union.
|
|
668
|
+
recency, recency_status, confirmed_at = self._time_trust(
|
|
669
|
+
winner, entity, attribute, ordered, as_of, asserted_as_of, now
|
|
670
|
+
)
|
|
575
671
|
|
|
576
672
|
# Corroboration: distinct source classes attesting the EFFECTIVE served
|
|
577
673
|
# value (Codex post-impl finding 2). Counting sources that back a
|
|
@@ -611,11 +707,18 @@ class Indexes:
|
|
|
611
707
|
)
|
|
612
708
|
|
|
613
709
|
weights = params["weights"]
|
|
614
|
-
|
|
615
|
-
weights["provenance"]
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
710
|
+
if recency is None:
|
|
711
|
+
denom = weights["provenance"] + weights["corroboration"]
|
|
712
|
+
score = (
|
|
713
|
+
weights["provenance"] * provenance
|
|
714
|
+
+ weights["corroboration"] * corroboration_score
|
|
715
|
+
) / denom
|
|
716
|
+
else:
|
|
717
|
+
score = (
|
|
718
|
+
weights["provenance"] * provenance
|
|
719
|
+
+ weights["recency"] * recency
|
|
720
|
+
+ weights["corroboration"] * corroboration_score
|
|
721
|
+
)
|
|
619
722
|
if conflicted:
|
|
620
723
|
score *= 0.5
|
|
621
724
|
score = min(1.0, max(0.0, score))
|
|
@@ -625,6 +728,9 @@ class Indexes:
|
|
|
625
728
|
"last_observed_at": winner.valid_from,
|
|
626
729
|
"corroboration": corroboration,
|
|
627
730
|
"conflicted": conflicted,
|
|
731
|
+
"recency": recency,
|
|
732
|
+
"recency_status": recency_status,
|
|
733
|
+
"last_confirmed_at_wallclock": confirmed_at,
|
|
628
734
|
}
|
|
629
735
|
|
|
630
736
|
def _fold_accrue(self, rows: list[Assertion]) -> FoldResult:
|
|
@@ -14,6 +14,7 @@ without it.
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import logging
|
|
17
|
+
import math
|
|
17
18
|
import re
|
|
18
19
|
import time
|
|
19
20
|
from dataclasses import dataclass
|
|
@@ -120,9 +121,16 @@ _EXTRACT_RULES_FULL = (
|
|
|
120
121
|
"- SPACE: emit connects_to edges for every passage/route the text "
|
|
121
122
|
"describes (stairs, gates, corridors); never invent an edge the text "
|
|
122
123
|
"does not support — vertical proximity is not connectivity.\n"
|
|
123
|
-
"- TIME: timeless=true ONLY for
|
|
124
|
-
"fixed adjacency)
|
|
125
|
-
"
|
|
124
|
+
"- TIME: timeless=true ONLY for what holds across the world's whole "
|
|
125
|
+
"history: identity/structure (kind, names, fixed adjacency) and facts of "
|
|
126
|
+
"origin (kinship of origin, innate traits presented as what the person "
|
|
127
|
+
"has always been). Everything acquired or mutable gets valid_from — a "
|
|
128
|
+
"dated onset when the text gives one ('became a soldier at the war'); "
|
|
129
|
+
"otherwise the earliest supported point (the entity's introduction or "
|
|
130
|
+
"the scene cursor). Time-relative quantities (age) are current state at "
|
|
131
|
+
"the cursor, never timeless. Standing-but-acquired properties "
|
|
132
|
+
"(occupation, scars, learned skills) are NOT timeless — stamp them at "
|
|
133
|
+
"their earliest supported time.\n"
|
|
126
134
|
"- Repeated habits, spoken catchphrases, confrontations, confessions, "
|
|
127
135
|
"and scheduled/conditional future events are assertions too (use event: "
|
|
128
136
|
"entities with caused_by where the text gives causality).\n"
|
|
@@ -147,8 +155,14 @@ _EXTRACT_RULES_LEAN = (
|
|
|
147
155
|
"- ALWAYS extract location changes: X moves/leaves/arrives => a new 'in' "
|
|
148
156
|
"row for X. Presence and departure are core state, never atmosphere "
|
|
149
157
|
"(a departure that goes unrecorded makes presence lie).\n"
|
|
150
|
-
"- timeless=true ONLY for
|
|
151
|
-
"
|
|
158
|
+
"- timeless=true ONLY for what holds across the world's whole history: "
|
|
159
|
+
"identity/structure (kind, names, fixed adjacency) and facts of origin "
|
|
160
|
+
"(kinship of origin, innate traits). Everything acquired or mutable gets "
|
|
161
|
+
"valid_from — the dated onset if given, else the earliest supported "
|
|
162
|
+
"point (the entity's introduction or the scene cursor). Time-relative "
|
|
163
|
+
"quantities (age) are current state at the cursor, never timeless; "
|
|
164
|
+
"standing-but-acquired properties (occupation, scars, learned skills) "
|
|
165
|
+
"are NOT timeless — stamp them at their earliest supported time.\n"
|
|
152
166
|
"- NEVER invent: extract only what the text supports; atmosphere is not an "
|
|
153
167
|
"assertion.\n"
|
|
154
168
|
"- The narrative voice is not an entity: never emit person: entities for "
|
|
@@ -449,10 +463,22 @@ class Ingestor:
|
|
|
449
463
|
):
|
|
450
464
|
self._record_skip(entity, attribute, value, "malformed_id")
|
|
451
465
|
return out
|
|
466
|
+
raw_entity, raw_value = entity, value
|
|
452
467
|
entity = self._registry.resolve(entity)
|
|
453
468
|
if value_type == "entity" and isinstance(value, str):
|
|
454
469
|
value = self._registry.resolve(value)
|
|
455
470
|
|
|
471
|
+
# Decay-policy declarations must be valid physics (TRACKING-MODE-V1
|
|
472
|
+
# §B3): a malformed half-life is skip-receipted at the gate, never
|
|
473
|
+
# silently active (DecayPolicy would ignore it — but a silent ignore
|
|
474
|
+
# reads as "declared" to the author; the receipt says it fell).
|
|
475
|
+
if attribute == "decay_halflife_seconds" and not (
|
|
476
|
+
isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
477
|
+
and math.isfinite(value) and value > 0
|
|
478
|
+
):
|
|
479
|
+
self._record_skip(entity, attribute, value, "invalid_decay_halflife")
|
|
480
|
+
return out
|
|
481
|
+
|
|
456
482
|
# Edge-granular structural guard (Part B): a single invalid edge
|
|
457
483
|
# (containment cycle / self-edge / lateral self-loop) is SKIPPED with a
|
|
458
484
|
# typed receipt — the invariant holds (it never enters) and the rest of
|
|
@@ -462,6 +488,16 @@ class Ingestor:
|
|
|
462
488
|
item.get("frame", CANON), None if timeless else valid_from,
|
|
463
489
|
)
|
|
464
490
|
if skip is not None:
|
|
491
|
+
# INGESTION-FIDELITY-V2 §D: two raw ids that resolve to one
|
|
492
|
+
# identity head post-`same_as` are a MERGE footprint, not an
|
|
493
|
+
# authored self-edge — the distinct reason (raw ids retained)
|
|
494
|
+
# makes the diagnosis actionable. Containment-only per the
|
|
495
|
+
# GREEN'd spec; a merged lateral loop keeps its original
|
|
496
|
+
# reason. Gate behavior is identical: the row never enters.
|
|
497
|
+
if (entity == value and raw_entity != raw_value
|
|
498
|
+
and self._semantics.is_containment(attribute)):
|
|
499
|
+
skip = (f"merged_self_edge: {raw_entity!r} and {raw_value!r} "
|
|
500
|
+
f"resolve to the same entity {entity!r} — {skip}")
|
|
465
501
|
self._record_skip(entity, attribute, value, skip)
|
|
466
502
|
return out # nothing appended
|
|
467
503
|
|
|
@@ -585,9 +621,18 @@ class Ingestor:
|
|
|
585
621
|
if not _ID_RE.fullmatch(pov):
|
|
586
622
|
raise ValueError(f"pov {pov!r} is not a valid entity id")
|
|
587
623
|
rules += (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
624
|
+
# The FULL deictic family incl. possessives (HD 126: "my hand"
|
|
625
|
+
# once bound sideways to the nearest NPC — fabricated canon).
|
|
626
|
+
# Scoped per Cx 570: singular deixis is exclusively the POV;
|
|
627
|
+
# plural deixis INCLUDES the POV without proving exclusive
|
|
628
|
+
# ownership or licensing guessed members.
|
|
629
|
+
f"- Viewpoint deixis: I, me, my, mine, myself — and you, your, "
|
|
630
|
+
f"yours when the narration addresses the viewpoint character — "
|
|
631
|
+
f"refer to {pov}; never mint a new entity for them, and a "
|
|
632
|
+
f"singular possessee (my hand, your coat) is NEVER attributed "
|
|
633
|
+
f"to any other present character. Plural we, us, our, ours "
|
|
634
|
+
f"INCLUDE {pov}: never guess the other members and never "
|
|
635
|
+
f"rebind the plural wholesale to a nearby character.\n"
|
|
591
636
|
)
|
|
592
637
|
prompt = f"{rules}{context}\n\nPASSAGE:\n{text}"
|
|
593
638
|
# RAW output, verbatim (INGEST-LATENCY-V2; Cx 545): extract() returns the
|
|
@@ -638,10 +638,15 @@ class Porcelain:
|
|
|
638
638
|
attribute: str,
|
|
639
639
|
frame: str | list[str] = CANON,
|
|
640
640
|
as_of: float | None = None,
|
|
641
|
+
now: float | None = None,
|
|
641
642
|
) -> dict:
|
|
642
643
|
# frame may be a list: trust over an observer's effective knowledge
|
|
643
644
|
# (knows:O ∪ public), mirroring multi-frame frame_diff.
|
|
644
|
-
|
|
645
|
+
# now (TRACKING-MODE-V1): the WALL-time staleness reference in tracking
|
|
646
|
+
# worlds (defaults to the injected clock; unused in fiction). The
|
|
647
|
+
# payload carries recency/recency_status/last_confirmed_at_wallclock.
|
|
648
|
+
return self._w.confidence(entity, attribute, frame=frame, as_of=as_of,
|
|
649
|
+
now=now)
|
|
645
650
|
|
|
646
651
|
def neighborhood(
|
|
647
652
|
self,
|