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.
Files changed (68) hide show
  1. {pbuffer-0.1.0/src/pbuffer.egg-info → pbuffer-0.2.0}/PKG-INFO +20 -5
  2. {pbuffer-0.1.0 → pbuffer-0.2.0}/README.md +19 -4
  3. {pbuffer-0.1.0 → pbuffer-0.2.0}/pyproject.toml +1 -1
  4. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/__init__.py +9 -1
  5. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/classify.py +6 -1
  6. pbuffer-0.2.0/src/patternbuffer/decay.py +62 -0
  7. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/indexes.py +151 -45
  8. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/ingest.py +53 -8
  9. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/porcelain.py +6 -1
  10. {pbuffer-0.1.0 → pbuffer-0.2.0/src/pbuffer.egg-info}/PKG-INFO +20 -5
  11. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/pbuffer.egg-info/SOURCES.txt +3 -0
  12. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_confidence.py +21 -6
  13. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_confidence_multiframe.py +9 -2
  14. pbuffer-0.2.0/tests/test_ingestion_fidelity_v2.py +323 -0
  15. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_shape_fix.py +20 -2
  16. pbuffer-0.2.0/tests/test_tracking_mode.py +517 -0
  17. {pbuffer-0.1.0 → pbuffer-0.2.0}/LICENSE +0 -0
  18. {pbuffer-0.1.0 → pbuffer-0.2.0}/setup.cfg +0 -0
  19. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/buffer.py +0 -0
  20. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/codec.py +0 -0
  21. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/dump.py +0 -0
  22. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/identity.py +0 -0
  23. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/mcp.py +0 -0
  24. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/model.py +0 -0
  25. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/project.py +0 -0
  26. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/refer.py +0 -0
  27. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/roles.py +0 -0
  28. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/salience.py +0 -0
  29. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/semantics.py +0 -0
  30. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/testing.py +0 -0
  31. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/thunks.py +0 -0
  32. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/patternbuffer/tmaint.py +0 -0
  33. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/pbuffer.egg-info/dependency_links.txt +0 -0
  34. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/pbuffer.egg-info/entry_points.txt +0 -0
  35. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/pbuffer.egg-info/requires.txt +0 -0
  36. {pbuffer-0.1.0 → pbuffer-0.2.0}/src/pbuffer.egg-info/top_level.txt +0 -0
  37. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_aka_correlation.py +0 -0
  38. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_awareness_projection.py +0 -0
  39. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_axis_heads.py +0 -0
  40. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_bounded_reads.py +0 -0
  41. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_buffer.py +0 -0
  42. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_build_session.py +0 -0
  43. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_classifier_event_safety.py +0 -0
  44. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_dump.py +0 -0
  45. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_exact_decimal.py +0 -0
  46. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_fidelity_audit.py +0 -0
  47. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_fold.py +0 -0
  48. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_fold_resolution.py +0 -0
  49. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_grammar.py +0 -0
  50. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_identity_containment_veto.py +0 -0
  51. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_identity_recall.py +0 -0
  52. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_identity_thunks.py +0 -0
  53. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_individuation_v2.py +0 -0
  54. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_ingest_frame_default.py +0 -0
  55. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_ingest_hardening.py +0 -0
  56. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_ingest_v2.py +0 -0
  57. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_mcp_wrapper.py +0 -0
  58. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_merge_verb.py +0 -0
  59. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_path_temporal.py +0 -0
  60. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_place_feature.py +0 -0
  61. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_porcelain.py +0 -0
  62. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_route.py +0 -0
  63. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_scaffold.py +0 -0
  64. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_situation_lens.py +0 -0
  65. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_triage_context.py +0 -0
  66. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_who_knows.py +0 -0
  67. {pbuffer-0.1.0 → pbuffer-0.2.0}/tests/test_world.py +0 -0
  68. {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.1.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, ingestion fidelity is the open
31
- front.** The engine passes its 419-test invariant suite, and the chapter test
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. Not yet installable from PyPI; the shipped example
47
- world with a zero-API-key query tour lands once a verified run stamps it.
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, ingestion fidelity is the open
17
- front.** The engine passes its 419-test invariant suite, and the chapter test
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. Not yet installable from PyPI; the shipped example
33
- world with a zero-API-key query tour lands once a verified run stamps it.
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.1.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. Asymmetric defaults: an "
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
- if winner.valid_from is None:
435
- recency = 1.0
436
- else:
437
- ref = as_of
438
- if ref is None:
439
- closure_rows = self._buffer.visible(
440
- entity_in=sorted(self._closure_of(entity)),
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
- score = (
473
- weights["provenance"] * provenance
474
- + weights["recency"] * recency
475
- + weights["corroboration"] * corroboration_score
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, asserted_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
- if winner.valid_from is None:
559
- recency = 1.0
560
- else:
561
- ref = as_of
562
- if ref is None:
563
- closure = sorted(self._closure_of(entity))
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
- score = (
615
- weights["provenance"] * provenance
616
- + weights["recency"] * recency
617
- + weights["corroboration"] * corroboration_score
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 identity and structure (kind, names, "
124
- "fixed adjacency). Everything that happens or holds-at-a-time gets "
125
- "valid_from on the provided timeline.\n"
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 identity/structure (kind, names, fixed adjacency); "
151
- "everything else gets valid_from.\n"
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
- f"- First/second-person pronouns (I, you, we) referring to the "
589
- f"viewpoint character are {pov} never mint a new entity for "
590
- f"them.\n"
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
- return self._w.confidence(entity, attribute, frame=frame, as_of=as_of)
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,