rememberstack 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.
Files changed (186) hide show
  1. rememberstack/__init__.py +9 -0
  2. rememberstack/adapters/__init__.py +42 -0
  3. rememberstack/adapters/codex_writer.py +221 -0
  4. rememberstack/adapters/markitdown_converter.py +42 -0
  5. rememberstack/adapters/openrouter.py +136 -0
  6. rememberstack/adapters/selfhost/__init__.py +54 -0
  7. rememberstack/adapters/selfhost/forget.py +66 -0
  8. rememberstack/adapters/selfhost/git.py +374 -0
  9. rememberstack/adapters/selfhost/lance.py +328 -0
  10. rememberstack/adapters/selfhost/minio.py +279 -0
  11. rememberstack/adapters/selfhost/mounts.py +249 -0
  12. rememberstack/adapters/selfhost/object_store.py +130 -0
  13. rememberstack/adapters/selfhost/projection.py +80 -0
  14. rememberstack/adapters/selfhost/queue.py +137 -0
  15. rememberstack/adapters/selfhost/telemetry.py +45 -0
  16. rememberstack/adapters/selfhost/watcher.py +70 -0
  17. rememberstack/adapters/testing/__init__.py +15 -0
  18. rememberstack/adapters/testing/cost_meter.py +13 -0
  19. rememberstack/adapters/testing/model_provider.py +83 -0
  20. rememberstack/adapters/testing/queue.py +43 -0
  21. rememberstack/adapters/testing/telemetry.py +22 -0
  22. rememberstack/client.py +19 -0
  23. rememberstack/core/__init__.py +127 -0
  24. rememberstack/core/blockizer.py +189 -0
  25. rememberstack/core/chunker.py +216 -0
  26. rememberstack/core/consumption_skill.py +275 -0
  27. rememberstack/core/conversion.py +76 -0
  28. rememberstack/core/core_manifest.py +598 -0
  29. rememberstack/core/extension_packs.py +124 -0
  30. rememberstack/core/forget.py +17 -0
  31. rememberstack/core/knowledge_authored.py +276 -0
  32. rememberstack/core/knowledge_compile.py +215 -0
  33. rememberstack/core/knowledge_fact_sheet.py +210 -0
  34. rememberstack/core/knowledge_hashing.py +68 -0
  35. rememberstack/core/knowledge_planner.py +64 -0
  36. rememberstack/core/knowledge_writer.py +175 -0
  37. rememberstack/core/ranking.py +200 -0
  38. rememberstack/core/recipe_linter.py +149 -0
  39. rememberstack/core/section_snap.py +209 -0
  40. rememberstack/core/storage_routing.py +27 -0
  41. rememberstack/eval/__init__.py +53 -0
  42. rememberstack/eval/consumption.py +141 -0
  43. rememberstack/eval/contradiction.py +184 -0
  44. rememberstack/eval/harness.py +136 -0
  45. rememberstack/eval/lifecycle.py +400 -0
  46. rememberstack/eval/operational_scale.py +49 -0
  47. rememberstack/eval/resolution.py +255 -0
  48. rememberstack/eval/retrieval_spikes.py +50 -0
  49. rememberstack/eval/skeleton.py +231 -0
  50. rememberstack/llm/__init__.py +1 -0
  51. rememberstack/model/__init__.py +589 -0
  52. rememberstack/model/adjudication.py +100 -0
  53. rememberstack/model/auth.py +27 -0
  54. rememberstack/model/blocks.py +30 -0
  55. rememberstack/model/chunks.py +190 -0
  56. rememberstack/model/claims.py +162 -0
  57. rememberstack/model/client.py +98 -0
  58. rememberstack/model/clustering.py +54 -0
  59. rememberstack/model/component_version.py +124 -0
  60. rememberstack/model/consumption.py +88 -0
  61. rememberstack/model/conversion.py +31 -0
  62. rememberstack/model/deployment.py +53 -0
  63. rememberstack/model/documents.py +168 -0
  64. rememberstack/model/envelope.py +513 -0
  65. rememberstack/model/evaluation.py +72 -0
  66. rememberstack/model/forget.py +143 -0
  67. rememberstack/model/git.py +13 -0
  68. rememberstack/model/knowledge.py +840 -0
  69. rememberstack/model/knowledge_authored.py +325 -0
  70. rememberstack/model/knowledge_planner.py +431 -0
  71. rememberstack/model/lifecycle.py +42 -0
  72. rememberstack/model/model_provider.py +78 -0
  73. rememberstack/model/mounts.py +24 -0
  74. rememberstack/model/object_store.py +21 -0
  75. rememberstack/model/operational_scale.py +59 -0
  76. rememberstack/model/operations.py +153 -0
  77. rememberstack/model/processing.py +228 -0
  78. rememberstack/model/queue.py +73 -0
  79. rememberstack/model/recipes.py +83 -0
  80. rememberstack/model/relations.py +79 -0
  81. rememberstack/model/resolution.py +83 -0
  82. rememberstack/model/retrieval_spikes.py +62 -0
  83. rememberstack/model/sections.py +120 -0
  84. rememberstack/model/telemetry.py +30 -0
  85. rememberstack/ports/__init__.py +29 -0
  86. rememberstack/ports/auth.py +16 -0
  87. rememberstack/ports/connector.py +23 -0
  88. rememberstack/ports/cost_meter.py +17 -0
  89. rememberstack/ports/forget.py +20 -0
  90. rememberstack/ports/git.py +20 -0
  91. rememberstack/ports/model_provider.py +28 -0
  92. rememberstack/ports/mounts.py +16 -0
  93. rememberstack/ports/object_store.py +27 -0
  94. rememberstack/ports/p1_index.py +92 -0
  95. rememberstack/ports/purge.py +93 -0
  96. rememberstack/ports/queue.py +23 -0
  97. rememberstack/ports/telemetry.py +21 -0
  98. rememberstack/profiles/__init__.py +22 -0
  99. rememberstack/profiles/selfhost.py +324 -0
  100. rememberstack/profiles/selfhost_forget.py +158 -0
  101. rememberstack/profiles/selfhost_operations.py +95 -0
  102. rememberstack/py.typed +1 -0
  103. rememberstack/spine/__init__.py +93 -0
  104. rememberstack/spine/admission.py +26 -0
  105. rememberstack/spine/backfill.py +168 -0
  106. rememberstack/spine/catalog_contract.py +742 -0
  107. rememberstack/spine/chunk_catalog.py +237 -0
  108. rememberstack/spine/claim_catalog.py +298 -0
  109. rememberstack/spine/clustering.py +740 -0
  110. rememberstack/spine/component_versions.py +208 -0
  111. rememberstack/spine/consumption.py +81 -0
  112. rememberstack/spine/deployment_bootstrap.py +445 -0
  113. rememberstack/spine/document_catalog.py +621 -0
  114. rememberstack/spine/entity_registry.py +205 -0
  115. rememberstack/spine/extension_packs.py +220 -0
  116. rememberstack/spine/fact_catalog.py +571 -0
  117. rememberstack/spine/forget.py +1753 -0
  118. rememberstack/spine/knowledge.py +5467 -0
  119. rememberstack/spine/lifecycle.py +1071 -0
  120. rememberstack/spine/migrations/__init__.py +1 -0
  121. rememberstack/spine/migrations/_helpers.py +153 -0
  122. rememberstack/spine/migrations/env.py +58 -0
  123. rememberstack/spine/migrations/script.py.mako +27 -0
  124. rememberstack/spine/migrations/versions/__init__.py +1 -0
  125. rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
  126. rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
  127. rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
  128. rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
  129. rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
  130. rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
  131. rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
  132. rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
  133. rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
  134. rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
  135. rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
  136. rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
  137. rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
  138. rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
  139. rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
  140. rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
  141. rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
  142. rememberstack/spine/observation_adjudication.py +778 -0
  143. rememberstack/spine/operations.py +298 -0
  144. rememberstack/spine/projection.py +662 -0
  145. rememberstack/spine/recipes.py +276 -0
  146. rememberstack/spine/resolver.py +763 -0
  147. rememberstack/spine/review.py +650 -0
  148. rememberstack/spine/settings.py +22 -0
  149. rememberstack/spine/supersession.py +510 -0
  150. rememberstack/spine/sync.py +128 -0
  151. rememberstack/spine/work_ledger.py +816 -0
  152. rememberstack/surfaces/__init__.py +110 -0
  153. rememberstack/surfaces/cli.py +447 -0
  154. rememberstack/surfaces/consumption_skill.py +87 -0
  155. rememberstack/surfaces/graph_queries.py +698 -0
  156. rememberstack/surfaces/http_api.py +377 -0
  157. rememberstack/surfaces/mcp.py +67 -0
  158. rememberstack/surfaces/query_engine.py +1591 -0
  159. rememberstack/surfaces/recipe_executor.py +185 -0
  160. rememberstack/surfaces/recipe_surface.py +219 -0
  161. rememberstack/surfaces/remote_mcp.py +133 -0
  162. rememberstack/surfaces/sdk.py +324 -0
  163. rememberstack/workers/__init__.py +155 -0
  164. rememberstack/workers/base.py +312 -0
  165. rememberstack/workers/e0.py +577 -0
  166. rememberstack/workers/e1.py +425 -0
  167. rememberstack/workers/e2.py +525 -0
  168. rememberstack/workers/e3.py +434 -0
  169. rememberstack/workers/forget.py +299 -0
  170. rememberstack/workers/knowledge_authored.py +146 -0
  171. rememberstack/workers/knowledge_driver.py +735 -0
  172. rememberstack/workers/knowledge_fact_sheet.py +123 -0
  173. rememberstack/workers/knowledge_planner.py +325 -0
  174. rememberstack/workers/knowledge_writer.py +393 -0
  175. rememberstack/workers/operations.py +42 -0
  176. rememberstack/workers/p1.py +234 -0
  177. rememberstack/workers/p2.py +513 -0
  178. rememberstack/workers/p2_analytics.py +276 -0
  179. rememberstack/workers/p3.py +673 -0
  180. rememberstack/workers/reconcile.py +485 -0
  181. rememberstack/workers/sync.py +168 -0
  182. rememberstack-0.1.0.dist-info/METADATA +213 -0
  183. rememberstack-0.1.0.dist-info/RECORD +186 -0
  184. rememberstack-0.1.0.dist-info/WHEEL +4 -0
  185. rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
  186. rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,513 @@
1
+ """The response envelope (D49): the answer's machine-readable self-account.
2
+
3
+ Every query-engine result carries its grain, validity, freshness stamps, the
4
+ nominate-then-drop honesty count (D48), and — when the answer is a "no" — a
5
+ typed negative from the fixed taxonomy (retrieval §5). The walking skeleton
6
+ carries the minimal envelope; the full contract grows on these same fields.
7
+ """
8
+
9
+ from enum import StrEnum
10
+ from typing import Annotated
11
+ from uuid import UUID
12
+
13
+ from pydantic import BaseModel
14
+ from pydantic import ConfigDict
15
+ from pydantic import Field
16
+ from pydantic import model_validator
17
+
18
+ from rememberstack.model.adjudication import TranscriptEntry
19
+ from rememberstack.model.queue import UTCDateTime
20
+
21
+
22
+ class Grain(StrEnum):
23
+ """The D49 grain type-system: what kind of truth a result is."""
24
+
25
+ FACT = "fact"
26
+ EVIDENCE = "evidence"
27
+ COMPILED = "compiled"
28
+ COMPOSITE = "composite"
29
+
30
+
31
+ class NegativeKind(StrEnum):
32
+ """The fixed negative-answer taxonomy (S29/S39/S55).
33
+
34
+ Deliberately no `denied` kind: content-level authorization is a library
35
+ non-goal (retrieval §9), and hard-deleted (forgotten) content is
36
+ indistinguishable-from-never-existed (S55), so it surfaces as
37
+ `unknown_entity`/`known_empty`, never a distinct kind. Freezing the
38
+ taxonomy now is safe precisely because of these two omissions —
39
+ retrofitting a kind onto a deployed API breaks consumers.
40
+ """
41
+
42
+ UNKNOWN_ENTITY = "unknown_entity"
43
+ KNOWN_EMPTY = "known_empty"
44
+ BOUNDARY = "boundary"
45
+
46
+
47
+ class IdentityRegime(StrEnum):
48
+ """Which identity boundary answered a read (S61).
49
+
50
+ `current` (the default) follows today's aliases and merge redirects even
51
+ under a past `believed_at`; `as_of` means the identity boundary was
52
+ reconstructed as it stood at the queried instant (the transcript-based
53
+ `identity_as_of` recipe). The envelope always states which, so an audit
54
+ read can never silently mix today's identities with yesterday's beliefs.
55
+ """
56
+
57
+ CURRENT = "current"
58
+ AS_OF = "as_of"
59
+
60
+
61
+ class FactSupport(StrEnum):
62
+ """Whether a fact still has current-testimony support (D54).
63
+
64
+ `current` is the normal state; `withdrawn` means every source that
65
+ asserted the fact has stopped (an open `support_withdrawn` review flag) —
66
+ the fact is *flagged, not vanished*, so an agent sees the ground moved
67
+ before planning against it. A withdrawn fact is still returned.
68
+ """
69
+
70
+ CURRENT = "current"
71
+ WITHDRAWN = "withdrawn"
72
+
73
+
74
+ class Negative(BaseModel):
75
+ """One typed 'no': each kind demands a different agent reaction."""
76
+
77
+ model_config = ConfigDict(frozen=True, extra="forbid")
78
+
79
+ kind: NegativeKind
80
+ explanation: Annotated[str, Field(min_length=1)]
81
+ workaround: str | None = None
82
+
83
+
84
+ class Validity(BaseModel):
85
+ """A result's bi-temporal state as hydration re-read it (D48)."""
86
+
87
+ model_config = ConfigDict(frozen=True, extra="forbid")
88
+
89
+ valid_from: UTCDateTime | None
90
+ valid_until: UTCDateTime | None
91
+ ingested_at: UTCDateTime
92
+ invalidated_at: UTCDateTime | None
93
+
94
+
95
+ class KFreshness(BaseModel):
96
+ """The compiled-grain honesty block (retrieval §5): a K page's timestamp.
97
+
98
+ A compiled answer is pre-paid synthesis *with a timestamp*, so any answer
99
+ that consumed a K page carries when it compiled, whether it is stale
100
+ (inputs changed since), and how many evidence-change flags are still open
101
+ against it — the reader-facing flag surface (k_layers spike 9). An agent
102
+ sees "this page has 3 unresolved flags" before planning against it (S34).
103
+ """
104
+
105
+ model_config = ConfigDict(frozen=True, extra="forbid")
106
+
107
+ compiled_at: UTCDateTime | None = None
108
+ stale: bool = False
109
+ open_flags: int = Field(default=0, ge=0)
110
+
111
+
112
+ class Freshness(BaseModel):
113
+ """Per-source freshness stamps (S42): what lag the answer could carry.
114
+
115
+ Each contributing channel also exposes its **`believed_at` horizon**: the
116
+ oldest system-time a query can reach before the channel can no longer
117
+ answer. `None` means unbounded — under D69 the hot P2 relation view keeps
118
+ every relation whose endpoints stay emitted, so P2's horizon is null.
119
+ Whenever a horizon is finite, a `believed_at` before it must return a
120
+ `boundary` (retrieval §3), never a silent truncation.
121
+ """
122
+
123
+ model_config = ConfigDict(frozen=True, extra="forbid")
124
+
125
+ pg_live_ts: UTCDateTime
126
+ p1_written_inline: bool = True # the skeleton writes P1 inline; a real
127
+ # write-lag horizon replaces this constant with measurement (retrieval §5)
128
+ p1_believed_at_horizon: UTCDateTime | None = None # None = unbounded
129
+ p2_snapshot_version: str | None = None # which graph snapshot answered
130
+ p2_snapshot_ts: UTCDateTime | None = None
131
+ p2_believed_at_horizon: UTCDateTime | None = None # None = unbounded (D69)
132
+ k: KFreshness | None = None # present only when the answer consumed a K page
133
+
134
+
135
+ class EntityCandidate(BaseModel):
136
+ """One ranked resolve candidate (never a silent guess, S51)."""
137
+
138
+ model_config = ConfigDict(frozen=True, extra="forbid")
139
+
140
+ entity_id: UUID
141
+ canonical_name: str
142
+ type: str
143
+ tier: str # which resolution tier surfaced it (T0 in the skeleton)
144
+ context_hits: int = 0
145
+
146
+
147
+ class CoMember(BaseModel):
148
+ """One other side of a contradiction, surfaced with the fact (S23).
149
+
150
+ A light record — enough to see the competing claim and hydrate it — so a
151
+ contradiction block can carry several sides without recursion.
152
+ """
153
+
154
+ model_config = ConfigDict(frozen=True, extra="forbid")
155
+
156
+ fact_id: UUID
157
+ label: str
158
+ evidence_count: int
159
+ validity: Validity
160
+
161
+
162
+ class Contradiction(BaseModel):
163
+ """The S23 contract block: a fact's live contradiction, never one-sided.
164
+
165
+ Returning one side of a live contradiction group without its others is a
166
+ contract violation, not a ranking choice ("contradictions are surfaced,
167
+ never silently resolved"). The bounded form: co-members come back INLINE
168
+ up to a guaranteed cap (typical groups are 2–3 sides — both FY2023
169
+ revenue figures together, each with its own evidence handle); beyond the
170
+ cap the block still always carries `group_id`, `returned`, `total`, and a
171
+ `continuation`. One-sided is never a valid answer.
172
+ """
173
+
174
+ model_config = ConfigDict(frozen=True, extra="forbid")
175
+
176
+ group_id: UUID
177
+ co_members: tuple[CoMember, ...] = ()
178
+ returned: int = Field(ge=0)
179
+ total: int = Field(ge=0)
180
+ continuation: str | None = None
181
+
182
+
183
+ class FactResult(BaseModel):
184
+ """One fact-grain record: a live relation or observation, hydrated."""
185
+
186
+ model_config = ConfigDict(frozen=True, extra="forbid")
187
+
188
+ fact_id: UUID
189
+ kind: str # relation | observation
190
+ label: str
191
+ evidence_count: int
192
+ validity: Validity
193
+ contradiction_group: UUID | None = None # the raw group id (S23)
194
+ contradiction: Contradiction | None = None # the surfaced co-members (S23)
195
+ support: FactSupport = FactSupport.CURRENT # D54: withdrawn is flagged, not gone
196
+
197
+
198
+ class EvidenceResult(BaseModel):
199
+ """One evidence-grain record: a claim with its provenance anchors."""
200
+
201
+ model_config = ConfigDict(frozen=True, extra="forbid")
202
+
203
+ claim_id: UUID
204
+ doc_id: UUID
205
+ chunk_id: UUID
206
+ claim_text: str
207
+ source_span: str
208
+ char_start: int
209
+ char_end: int
210
+ is_attributed: bool
211
+ is_current_testimony: bool
212
+
213
+
214
+ class SourceRecord(BaseModel):
215
+ """One hydrated source document handle (S5: down to the artifact URI)."""
216
+
217
+ model_config = ConfigDict(frozen=True, extra="forbid")
218
+
219
+ doc_id: UUID
220
+ title: str | None
221
+ source_kind: str
222
+ markdown_uri: str | None
223
+
224
+
225
+ class GraphNode(BaseModel):
226
+ """One entity the traversal reached, with its hop distance."""
227
+
228
+ model_config = ConfigDict(frozen=True, extra="forbid")
229
+
230
+ entity_id: UUID
231
+ name: str
232
+ type: str
233
+ hops: int = Field(ge=0)
234
+
235
+
236
+ class GraphEdge(BaseModel):
237
+ """One traversed relation, carrying its bi-temporal state."""
238
+
239
+ model_config = ConfigDict(frozen=True, extra="forbid")
240
+
241
+ relation_id: UUID
242
+ subject_id: UUID
243
+ object_id: UUID
244
+ predicate: str
245
+ fact: str | None
246
+ evidence_count: int
247
+ valid_from: UTCDateTime | None
248
+ valid_until: UTCDateTime | None
249
+ ingested_at: UTCDateTime | None
250
+ invalidated_at: UTCDateTime | None
251
+
252
+
253
+ class GraphPath(BaseModel):
254
+ """One connection between two entities — a COMPOUND result.
255
+
256
+ A path revalidates as a unit (S17/S21): if hydration drops any edge,
257
+ the whole path drops, because a path with a hole is not a shorter
258
+ path — it is a different (and false) claim about connection.
259
+ """
260
+
261
+ model_config = ConfigDict(frozen=True, extra="forbid")
262
+
263
+ length: int = Field(ge=1)
264
+ nodes: tuple[GraphNode, ...] = Field(min_length=2)
265
+ edges: tuple[GraphEdge, ...] = Field(min_length=1)
266
+
267
+
268
+ class RankedItem(BaseModel):
269
+ """One item in a fused or reranked ordering (retrieval §3: `fuse`/`rerank`).
270
+
271
+ `score` is the operator's output — the RRF sum for `fuse`, the signal
272
+ value for `rerank` — and the tuple order IS the rank. `signals` keeps
273
+ each contributing value visible, because the rerankers are meant to be
274
+ inspectable stages (D9), not a black-box sort.
275
+ """
276
+
277
+ model_config = ConfigDict(frozen=True, extra="forbid")
278
+
279
+ item_id: UUID
280
+ score: float
281
+ signals: dict[str, float] = Field(default_factory=dict)
282
+
283
+
284
+ class ChangeRecord(BaseModel):
285
+ """One entry in the `delta` change feed (S13/S14/S30).
286
+
287
+ `kind` is what changed (relation | observation | claim | page) and
288
+ `change` is how (new | invalidated | capped | recompiled). `at` is the
289
+ instant that placed it in the feed — the ingestion, invalidation, or
290
+ recompilation time the caller's `since` was compared against — so a
291
+ follow-up `delta` can resume from the last `at` it saw.
292
+ """
293
+
294
+ model_config = ConfigDict(frozen=True, extra="forbid")
295
+
296
+ kind: str # relation | observation | claim | page
297
+ change: str # new | invalidated | capped | recompiled
298
+ id: UUID
299
+ label: str | None
300
+ at: UTCDateTime
301
+
302
+
303
+ class AggregateBucket(BaseModel):
304
+ """One group in an enumerated aggregate (retrieval §9): a key and its count.
305
+
306
+ `key` is the group label — a predicate, an object entity, a timeline
307
+ period, or an entity id rendered as text — and `null` for the single
308
+ bucket of a plain count. `entity_id` is populated when the group IS an
309
+ entity (group-by-object, delta-top-entities, typed-absence), so the
310
+ agent can hop straight to it without re-resolving the label.
311
+ """
312
+
313
+ model_config = ConfigDict(frozen=True, extra="forbid")
314
+
315
+ key: str | None
316
+ count: int = Field(ge=0)
317
+ entity_id: UUID | None = None
318
+
319
+
320
+ class AggregateReport(BaseModel):
321
+ """An enumerated aggregate's result: the form asked, and its buckets.
322
+
323
+ Aggregation is enumerated, never general (retrieval §9): each `form`
324
+ is a bounded SQL shape with a predictable cost. `total` is the sum
325
+ across buckets (or the single count); `bounded_by` names the cap when
326
+ the shape rides a bounded feed (e.g. delta-top-entities), so a reader
327
+ knows the ranking is over the window, not all of history.
328
+ """
329
+
330
+ model_config = ConfigDict(frozen=True, extra="forbid")
331
+
332
+ form: str
333
+ buckets: tuple[AggregateBucket, ...] = ()
334
+ total: int = Field(ge=0)
335
+ bounded_by: str | None = None
336
+
337
+
338
+ class PageRef(BaseModel):
339
+ """One K page the `pages_about` discovery index reports (S31/S45).
340
+
341
+ The rule-key inverted index that routes writes, read backwards: which
342
+ pages exist about an entity or key. `stale` mirrors the refresh state —
343
+ a page whose inputs changed but has not recompiled — so discovery never
344
+ presents an out-of-date page as fresh without saying so.
345
+ """
346
+
347
+ model_config = ConfigDict(frozen=True, extra="forbid")
348
+
349
+ artifact_id: UUID
350
+ page_kind: str
351
+ git_path: str | None
352
+ page_summary: str | None
353
+ last_compiled_at: UTCDateTime | None
354
+ status: str
355
+ stale: bool = False
356
+ open_review_flags: int = Field(default=0, ge=0)
357
+ redaction_required: bool = False
358
+
359
+
360
+ class ScanRow(BaseModel):
361
+ """One row of a `scan` batch export (S53): id, kind, label, feed instant.
362
+
363
+ The batch surface streams the same zero-LLM reads as the interactive
364
+ primitives under a separate resource pool (retrieval §9). A row is
365
+ deliberately minimal — id plus enough to route a hydrate — because a
366
+ scan is an export to a compiler or auditor, not a rendered answer.
367
+ """
368
+
369
+ model_config = ConfigDict(frozen=True, extra="forbid")
370
+
371
+ kind: str
372
+ id: UUID
373
+ label: str | None
374
+ at: UTCDateTime | None = None
375
+
376
+
377
+ class Truncation(BaseModel):
378
+ """The explicit cap marker (S18/S49): no silent top-k ever.
379
+
380
+ ``estimated_total`` is what the traversal could see before the cap;
381
+ ``continuation`` carries the opaque cursor a follow-up call passes back.
382
+ """
383
+
384
+ model_config = ConfigDict(frozen=True, extra="forbid")
385
+
386
+ truncated: bool
387
+ returned: int = Field(ge=0)
388
+ estimated_total: int = Field(ge=0)
389
+ total_is_exact: bool = True # false when the count itself hit its cap
390
+ continuation: str | None = None
391
+
392
+
393
+ class EnvelopePart(BaseModel):
394
+ """One single-grain section of a composite answer (S47).
395
+
396
+ A mixed answer — S47's "everything Alice *said* about pricing, plus what
397
+ we *believe*" — is EXPLICITLY two-part, never blended: each part carries
398
+ its own grain and its own single-grain results, so the fact/evidence
399
+ discipline is never diluted. Single-grain answers skip `parts` and read
400
+ flat off the top-level fields.
401
+ """
402
+
403
+ model_config = ConfigDict(frozen=True, extra="forbid")
404
+
405
+ grain: Grain # strictly single-grain: fact | evidence | compiled (S47)
406
+ label: str | None = None # e.g. "said" vs "believed"
407
+ facts: tuple[FactResult, ...] = ()
408
+ evidence: tuple[EvidenceResult, ...] = ()
409
+ sources: tuple[SourceRecord, ...] = ()
410
+ nodes: tuple[GraphNode, ...] = ()
411
+ aggregate: AggregateReport | None = None
412
+ pages: tuple[PageRef, ...] = ()
413
+ truncation: Truncation | None = None
414
+
415
+ @model_validator(mode="after")
416
+ def _payload_matches_grain(self) -> "EnvelopePart":
417
+ """A part is strictly single-grain: it carries only the payload its
418
+ own grain owns — a fact part holds facts (and graph nodes / an
419
+ aggregate), an evidence part holds claims, a compiled part holds K
420
+ pages. Carrying another grain's payload, or being composite, is the
421
+ blending S47 forbids. `sources` (hydration handles) is cross-grain."""
422
+ if self.grain is Grain.COMPOSITE:
423
+ raise ValueError("a composite part is not single-grain (S47)")
424
+ owned = {
425
+ Grain.FACT: {"facts", "nodes", "aggregate"},
426
+ Grain.EVIDENCE: {"evidence"},
427
+ Grain.COMPILED: {"pages"},
428
+ }[self.grain]
429
+ populated = {
430
+ name
431
+ for name, value in (
432
+ ("facts", self.facts),
433
+ ("evidence", self.evidence),
434
+ ("nodes", self.nodes),
435
+ ("aggregate", self.aggregate),
436
+ ("pages", self.pages),
437
+ )
438
+ if value
439
+ }
440
+ stray = populated - owned
441
+ if stray:
442
+ raise ValueError(
443
+ f"a {self.grain.value}-grain part carries {sorted(stray)}"
444
+ " belonging to another grain (S47)"
445
+ )
446
+ return self
447
+
448
+
449
+ class Envelope(BaseModel):
450
+ """The D49 envelope: results plus the answer's machine-readable self-account.
451
+
452
+ A single-grain answer (the common case) reads flat: the top-level
453
+ `grain` and the matching result tuple. A `composite` answer sets
454
+ `grain = composite` and carries `parts[]`, each strictly single-grain
455
+ (S47) — so a caller never has to disentangle blended grains.
456
+ """
457
+
458
+ model_config = ConfigDict(frozen=True, extra="forbid")
459
+
460
+ grain: Grain
461
+ parts: tuple["EnvelopePart", ...] = () # S47: composite ⇒ read parts[]
462
+ as_of_valid_at: UTCDateTime | None = None # echo of the applied valid_at
463
+ as_of_believed_at: UTCDateTime | None = None # echo of the applied believed_at
464
+ identity_regime: IdentityRegime = IdentityRegime.CURRENT # S61: which regime
465
+ entities: tuple[EntityCandidate, ...] = ()
466
+ facts: tuple[FactResult, ...] = ()
467
+ evidence: tuple[EvidenceResult, ...] = ()
468
+ sources: tuple[SourceRecord, ...] = ()
469
+ transcript: tuple["TranscriptEntry", ...] = () # S8: the audit surface
470
+ nodes: tuple[GraphNode, ...] = () # S18: neighborhood members
471
+ paths: tuple[GraphPath, ...] = () # S17/S21: compound connections
472
+ edges: tuple[GraphEdge, ...] = () # the traversed relations
473
+ ranking: tuple[RankedItem, ...] = () # S46: fused / reranked order
474
+ changes: tuple[ChangeRecord, ...] = () # S13/S14/S30: the delta feed
475
+ aggregate: AggregateReport | None = None # S26–S30/S40: enumerated only
476
+ pages: tuple[PageRef, ...] = () # S31/S45: pages_about discovery
477
+ freshness: Freshness
478
+ truncation: Truncation | None = None # S18/S49: caps are never silent
479
+ dropped_by_hydration: int = 0
480
+ negative: Negative | None = None
481
+
482
+ @model_validator(mode="after")
483
+ def _composite_uses_parts(self) -> "Envelope":
484
+ """Enforce the S47 discipline: `parts` belong only to a composite
485
+ answer, and a composite's data lives IN its parts, never blended into
486
+ the top-level result tuples. (Each part's own single-grain purity is
487
+ checked on `EnvelopePart`.) A flat compound answer with no parts —
488
+ hydrate's fact-with-evidence bundle — is untouched."""
489
+ if not self.parts:
490
+ return self
491
+ if self.grain is not Grain.COMPOSITE:
492
+ raise ValueError("only a composite envelope carries parts[]")
493
+ blended = self.aggregate is not None or any(
494
+ (
495
+ self.entities,
496
+ self.facts,
497
+ self.evidence,
498
+ self.sources,
499
+ self.transcript,
500
+ self.nodes,
501
+ self.paths,
502
+ self.edges,
503
+ self.ranking,
504
+ self.changes,
505
+ self.pages,
506
+ )
507
+ )
508
+ if blended:
509
+ raise ValueError(
510
+ "a composite answer's data lives in parts[], never blended"
511
+ " into the top-level result tuples (S47)"
512
+ )
513
+ return self
@@ -0,0 +1,72 @@
1
+ """Typed records for the D22 evaluation harness: suites, canaries, reports."""
2
+
3
+ from enum import StrEnum
4
+ from uuid import UUID
5
+
6
+ from pydantic import BaseModel
7
+ from pydantic import ConfigDict
8
+
9
+
10
+ class EvalSuite(StrEnum):
11
+ """Exact values of the binding Postgres ``eval_suite`` enum."""
12
+
13
+ RESOLUTION = "resolution"
14
+ SELECTION = "selection"
15
+ GROUNDING = "grounding"
16
+ RETRIEVAL = "retrieval"
17
+ CONTRADICTION = "contradiction"
18
+ LIFECYCLE = "lifecycle"
19
+ OPERATIONAL = "operational"
20
+
21
+
22
+ class CanaryCase(BaseModel):
23
+ """One known-tricky regression case re-run per version (registries §10)."""
24
+
25
+ model_config = ConfigDict(frozen=True, extra="forbid")
26
+
27
+ canary_id: UUID
28
+ suite: EvalSuite
29
+ description: str
30
+ input: dict[str, object]
31
+ expected: dict[str, object]
32
+
33
+
34
+ class CaseFailure(BaseModel):
35
+ """One failed case with the reason a reviewer needs."""
36
+
37
+ model_config = ConfigDict(frozen=True, extra="forbid")
38
+
39
+ canary_id: UUID
40
+ description: str
41
+ reason: str
42
+
43
+
44
+ class SuiteReport(BaseModel):
45
+ """One suite run: totals, failures, and the pass verdict CI gates on."""
46
+
47
+ model_config = ConfigDict(frozen=True, extra="forbid")
48
+
49
+ suite: EvalSuite
50
+ total_cases: int
51
+ failures: tuple[CaseFailure, ...]
52
+
53
+ @property
54
+ def passed(self) -> bool:
55
+ """A suite passes only with zero failures (empty suites pass)."""
56
+ return not self.failures
57
+
58
+
59
+ class LifecycleReport(BaseModel):
60
+ """One lifecycle-suite run: invariant verdicts + the flag-rate metric.
61
+
62
+ ``passed`` reflects the invariants alone; the flag rate is the watched
63
+ rollout canary (its alarm threshold is an operations decision).
64
+ """
65
+
66
+ model_config = ConfigDict(frozen=True, extra="forbid")
67
+
68
+ passed: bool
69
+ quiescent: bool = True # count/closure checks defer while mid-flight
70
+ violations: dict[str, tuple[str, ...]] = {}
71
+ canary_failures: tuple[str, ...] = ()
72
+ flag_rate_by_extractor: dict[str, dict[str, float]] = {}