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,571 @@
1
+ """The E3 fact catalog: relation/observation upserts, evidence, D54 counting.
2
+
3
+ Redundancy collapses here (D2): the same fact from many claims is one row plus
4
+ evidence links, and `evidence_count` is the number of DISTINCT DOCUMENT
5
+ LINEAGES with current-testimony support — re-extraction generations, document
6
+ versions, and within-document repetition never inflate it (D54).
7
+ """
8
+
9
+ from collections.abc import Iterator
10
+ from contextlib import contextmanager
11
+ import re
12
+ from typing import Final
13
+ from uuid import UUID
14
+ from uuid import uuid4
15
+
16
+ from sqlalchemy import bindparam
17
+ from sqlalchemy import JSON
18
+ from sqlalchemy import text
19
+ from sqlalchemy.engine import Engine
20
+
21
+ from rememberstack.model import FactForLabeling
22
+ from rememberstack.model import ObservationForEmbedding
23
+ from rememberstack.model import OtherPredicateGrammarError
24
+ from rememberstack.model import RelationUpsert
25
+
26
+ OTHER_PREDICATE_GRAMMAR: Final = re.compile(r"other:[a-z][a-z0-9_]{1,40}")
27
+ """The D5 escape-value grammar: short snake_case behind the other: prefix."""
28
+
29
+
30
+ class FactCatalog:
31
+ """Relation and observation writes over an explicitly composed engine."""
32
+
33
+ def __init__(self, *, engine: Engine) -> None:
34
+ """Bind the catalog to the spine database."""
35
+ self._engine = engine
36
+
37
+ def upsert_relation(
38
+ self,
39
+ *,
40
+ deployment_id: UUID,
41
+ subject_entity_id: UUID,
42
+ predicate: str,
43
+ object_entity_id: UUID,
44
+ claim_id: UUID,
45
+ doc_id: UUID,
46
+ normalizer_version: str,
47
+ ) -> RelationUpsert:
48
+ """Land one asserted fact: one relation row, evidence-once, recount.
49
+
50
+ An existing believed relation for the (s, p, o) key is reused; the
51
+ evidence link is ON CONFLICT DO NOTHING (a retry can never inflate the
52
+ count); the D54 recount runs in the same transaction.
53
+ """
54
+ with self._engine.begin() as connection:
55
+ key = {
56
+ "deployment_id": deployment_id,
57
+ "subject_entity_id": subject_entity_id,
58
+ "predicate": predicate,
59
+ "object_entity_id": object_entity_id,
60
+ }
61
+ # serialize concurrent upserts of one fact key (Codex review):
62
+ connection.execute(
63
+ _LOCK_FACT,
64
+ {
65
+ "key": f"{deployment_id}:rel:{subject_entity_id}"
66
+ f":{predicate}:{object_entity_id}"
67
+ },
68
+ )
69
+ existing = connection.execute(_SELECT_RELATION, key).scalar_one_or_none()
70
+ relation_id = existing if existing is not None else uuid4()
71
+ if existing is None:
72
+ # a re-occurring fact (a prior CLOSED row exists) starts a
73
+ # fresh row whose window opens at the re-occurrence boundary,
74
+ # so the EXCLUDE constraint's non-overlap holds and the new
75
+ # spell is adjudicable (Codex review; D41 refines the time):
76
+ reoccurrence_boundary = connection.execute(
77
+ _LATEST_CLOSED_UNTIL, key
78
+ ).scalar_one_or_none()
79
+ connection.execute(
80
+ _INSERT_RELATION,
81
+ {
82
+ **key,
83
+ "relation_id": relation_id,
84
+ "valid_from": reoccurrence_boundary,
85
+ "normalizer_version": normalizer_version,
86
+ },
87
+ )
88
+ connection.execute( # the D5 promotion funnel's ranking input
89
+ _BUMP_PREDICATE_USAGE,
90
+ {"deployment_id": deployment_id, "predicate": predicate},
91
+ )
92
+ connection.execute(
93
+ _INSERT_RELATION_EVIDENCE,
94
+ {
95
+ "deployment_id": deployment_id,
96
+ "relation_id": relation_id,
97
+ "claim_id": claim_id,
98
+ "doc_id": doc_id,
99
+ "normalizer_version": normalizer_version,
100
+ },
101
+ )
102
+ connection.execute(_RECOUNT_RELATION, {"relation_id": relation_id})
103
+ return RelationUpsert(relation_id=relation_id, created=existing is None)
104
+
105
+ def upsert_observation(
106
+ self,
107
+ *,
108
+ deployment_id: UUID,
109
+ subject_entity_id: UUID,
110
+ statement: str,
111
+ claim_id: UUID,
112
+ doc_id: UUID,
113
+ normalizer_version: str,
114
+ ) -> UUID:
115
+ """Land one entity-anchored statement (D43) with the novelty gate.
116
+
117
+ The gate: an identical live statement on the entity is the same
118
+ observation (evidence collapses onto it); anything else coexists as a
119
+ new row — fail-safe, never silently resolved. Each mint records an
120
+ append-only `add` adjudication by the novelty_gate rung (D4).
121
+ """
122
+ with self._engine.begin() as connection:
123
+ connection.execute(
124
+ _LOCK_FACT,
125
+ {"key": f"{deployment_id}:obs:{subject_entity_id}:{statement}"},
126
+ )
127
+ existing = connection.execute(
128
+ _SELECT_OBSERVATION,
129
+ {
130
+ "deployment_id": deployment_id,
131
+ "subject_entity_id": subject_entity_id,
132
+ "statement": statement,
133
+ },
134
+ ).scalar_one_or_none()
135
+ observation_id = existing if existing is not None else uuid4()
136
+ if existing is None:
137
+ connection.execute(
138
+ _INSERT_OBSERVATION,
139
+ {
140
+ "observation_id": observation_id,
141
+ "deployment_id": deployment_id,
142
+ "subject_entity_id": subject_entity_id,
143
+ "statement": statement,
144
+ "normalizer_version": normalizer_version,
145
+ },
146
+ )
147
+ connection.execute(
148
+ _INSERT_OBS_ADJUDICATION,
149
+ {
150
+ "adjudication_id": uuid4(),
151
+ "deployment_id": deployment_id,
152
+ "observation_id": observation_id,
153
+ "triggering_claim_id": claim_id,
154
+ "features": {"statement": statement},
155
+ "adjudicator_version": normalizer_version,
156
+ },
157
+ )
158
+ connection.execute(
159
+ _INSERT_OBS_EVIDENCE,
160
+ {
161
+ "deployment_id": deployment_id,
162
+ "observation_id": observation_id,
163
+ "claim_id": claim_id,
164
+ "doc_id": doc_id,
165
+ "normalizer_version": normalizer_version,
166
+ },
167
+ )
168
+ connection.execute(_RECOUNT_OBSERVATION, {"observation_id": observation_id})
169
+ return observation_id
170
+
171
+ @contextmanager
172
+ def label_lock(self, *, deployment_id: UUID) -> Iterator[None]:
173
+ """Serialize concurrent label sweeps for one deployment.
174
+
175
+ A session-scoped advisory lock on a dedicated connection, held for
176
+ the whole label+embed pass (which spans several transactions) — two
177
+ document jobs can never interleave labels and vectors on one fact.
178
+ """
179
+ with self._engine.connect() as connection:
180
+ connection.execute(
181
+ _ACQUIRE_LABEL_LOCK, {"key": f"{deployment_id}:label-facts"}
182
+ )
183
+ connection.commit()
184
+ try:
185
+ yield
186
+ finally:
187
+ connection.execute(
188
+ _RELEASE_LABEL_LOCK, {"key": f"{deployment_id}:label-facts"}
189
+ )
190
+ connection.commit()
191
+
192
+ def relations_for_labeling(
193
+ self, *, deployment_id: UUID, doc_id: UUID, label_version: str
194
+ ) -> tuple[FactForLabeling, ...]:
195
+ """The document's relations still lacking this label generation.
196
+
197
+ Scoped by evidence doc_id so a document job's work is proportional to
198
+ the document, not the deployment.
199
+ """
200
+ with self._engine.connect() as connection:
201
+ rows = (
202
+ connection.execute(
203
+ _SELECT_RELATIONS_FOR_LABELING,
204
+ {
205
+ "deployment_id": deployment_id,
206
+ "doc_id": doc_id,
207
+ "label_version": label_version,
208
+ },
209
+ )
210
+ .mappings()
211
+ .all()
212
+ )
213
+ return tuple(FactForLabeling.model_validate(dict(row)) for row in rows)
214
+
215
+ def record_fact_label(
216
+ self, *, relation_id: UUID, label: str, label_version: str
217
+ ) -> None:
218
+ """Stamp one relation's readable label, ref, and generation (D8)."""
219
+ with self._engine.begin() as connection:
220
+ connection.execute(
221
+ _STAMP_FACT_LABEL,
222
+ {
223
+ "relation_id": relation_id,
224
+ "label": label,
225
+ "label_version": label_version,
226
+ },
227
+ )
228
+
229
+ def observations_for_embedding(
230
+ self, *, deployment_id: UUID, doc_id: UUID, label_version: str
231
+ ) -> tuple[ObservationForEmbedding, ...]:
232
+ """The document's observations still lacking this label generation."""
233
+ with self._engine.connect() as connection:
234
+ rows = (
235
+ connection.execute(
236
+ _SELECT_OBSERVATIONS_FOR_EMBEDDING,
237
+ {
238
+ "deployment_id": deployment_id,
239
+ "doc_id": doc_id,
240
+ "label_version": label_version,
241
+ },
242
+ )
243
+ .mappings()
244
+ .all()
245
+ )
246
+ return tuple(ObservationForEmbedding.model_validate(dict(row)) for row in rows)
247
+
248
+ def record_observation_embedding(
249
+ self, *, observation_id: UUID, label_version: str
250
+ ) -> None:
251
+ """Stamp one observation's label ref and generation (D8)."""
252
+ with self._engine.begin() as connection:
253
+ connection.execute(
254
+ _STAMP_OBSERVATION_EMBEDDING,
255
+ {"observation_id": observation_id, "label_version": label_version},
256
+ )
257
+
258
+ def ensure_other_predicate(self, *, deployment_id: UUID, predicate: str) -> None:
259
+ """Register one `other:<freetext>` escape value (tier=other, D5/D18).
260
+
261
+ The grammar is enforced HERE, at the spine authority (Codex review) —
262
+ callers' routing regexes are conveniences, not the gate. The FK holds
263
+ (the row exists before any relation uses it); the permissive core
264
+ parent `related_to` anchors it; usage_count ranks it for the periodic
265
+ promotion review (registries §7).
266
+ """
267
+ if not OTHER_PREDICATE_GRAMMAR.fullmatch(predicate):
268
+ raise OtherPredicateGrammarError(
269
+ f"{predicate!r} is not a valid other:<short_snake_case> value"
270
+ )
271
+ with self._engine.begin() as connection:
272
+ connection.execute(
273
+ _INSERT_OTHER_PREDICATE,
274
+ {"deployment_id": deployment_id, "predicate": predicate},
275
+ )
276
+
277
+ def promotion_candidates(
278
+ self, *, deployment_id: UUID, limit: int = 20
279
+ ) -> tuple[tuple[str, int], ...]:
280
+ """The D5 funnel surface: tier=other predicates ranked by usage."""
281
+ with self._engine.connect() as connection:
282
+ rows = connection.execute(
283
+ _SELECT_PROMOTION_CANDIDATES,
284
+ {"deployment_id": deployment_id, "limit": limit},
285
+ ).all()
286
+ return tuple((predicate, usage) for predicate, usage in rows)
287
+
288
+ def predicate_prompt_lines(self, *, deployment_id: UUID) -> str:
289
+ """The governed vocabulary rendered for prompts (registries §4):
290
+ one line per active non-other predicate with meaning and synonyms."""
291
+ with self._engine.connect() as connection:
292
+ rows = connection.execute(
293
+ _SELECT_PREDICATE_PROMPT, {"deployment_id": deployment_id}
294
+ ).all()
295
+ lines = []
296
+ for predicate, description, synonyms in rows:
297
+ line = f"- {predicate}: {description}"
298
+ if synonyms:
299
+ line += f" (synonyms: {', '.join(synonyms)})"
300
+ lines.append(line)
301
+ return "\n".join(lines)
302
+
303
+ def active_predicates(self, *, deployment_id: UUID) -> dict[str, str | None]:
304
+ """The governed vocabulary: active predicate → its parent (D5/D18)."""
305
+ with self._engine.connect() as connection:
306
+ rows = connection.execute(
307
+ _SELECT_PREDICATES, {"deployment_id": deployment_id}
308
+ ).all()
309
+ return {predicate: parent for predicate, parent in rows}
310
+
311
+ def predicate_signatures(
312
+ self, *, deployment_id: UUID
313
+ ) -> dict[str, tuple[tuple[str, str], ...]]:
314
+ """Allowed (subject_type, object_type) pairs per predicate (D18)."""
315
+ with self._engine.connect() as connection:
316
+ rows = connection.execute(
317
+ _SELECT_SIGNATURES, {"deployment_id": deployment_id}
318
+ ).all()
319
+ signatures: dict[str, list[tuple[str, str]]] = {}
320
+ for predicate, subject_type, object_type in rows:
321
+ signatures.setdefault(predicate, []).append((subject_type, object_type))
322
+ return {predicate: tuple(pairs) for predicate, pairs in signatures.items()}
323
+
324
+ def entity_type_parents(self, *, deployment_id: UUID) -> dict[str, str | None]:
325
+ """The registry type hierarchy: type → parent (extend-never-fork)."""
326
+ with self._engine.connect() as connection:
327
+ rows = connection.execute(
328
+ _SELECT_TYPE_PARENTS, {"deployment_id": deployment_id}
329
+ ).all()
330
+ return {entity_type: parent for entity_type, parent in rows}
331
+
332
+
333
+ _LOCK_FACT = text("SELECT pg_advisory_xact_lock(hashtextextended(:key, 0))")
334
+
335
+ _SELECT_RELATION = text(
336
+ """
337
+ SELECT relation_id FROM relations
338
+ WHERE deployment_id = :deployment_id
339
+ AND subject_entity_id = :subject_entity_id
340
+ AND predicate = :predicate
341
+ AND object_entity_id = :object_entity_id
342
+ AND invalidated_at IS NULL
343
+ AND (valid_until IS NULL OR valid_until > now())
344
+ """
345
+ )
346
+
347
+ _INSERT_RELATION = text(
348
+ """
349
+ INSERT INTO relations (
350
+ relation_id, deployment_id, subject_entity_id, predicate,
351
+ object_entity_id, valid_from, normalizer_version
352
+ ) VALUES (
353
+ :relation_id, :deployment_id, :subject_entity_id, :predicate,
354
+ :object_entity_id, :valid_from, :normalizer_version
355
+ )
356
+ """
357
+ )
358
+
359
+ _INSERT_RELATION_EVIDENCE = text(
360
+ """
361
+ INSERT INTO relation_evidence (
362
+ deployment_id, relation_id, claim_id, doc_id, stance, normalizer_version
363
+ ) VALUES (
364
+ :deployment_id, :relation_id, :claim_id, :doc_id, 'supports',
365
+ :normalizer_version
366
+ )
367
+ ON CONFLICT (relation_id, claim_id) DO NOTHING
368
+ """
369
+ )
370
+
371
+ _RECOUNT_RELATION = text(
372
+ """
373
+ UPDATE relations SET evidence_count = (
374
+ SELECT count(DISTINCT evidence.doc_id)
375
+ FROM relation_evidence evidence
376
+ JOIN claims ON claims.claim_id = evidence.claim_id
377
+ WHERE evidence.relation_id = :relation_id
378
+ AND evidence.stance = 'supports'
379
+ AND claims.is_current_testimony
380
+ ), updated_at = now()
381
+ WHERE relation_id = :relation_id
382
+ """
383
+ )
384
+
385
+ _SELECT_OBSERVATION = text(
386
+ """
387
+ SELECT observation_id FROM observations
388
+ WHERE deployment_id = :deployment_id
389
+ AND subject_entity_id = :subject_entity_id
390
+ AND statement = :statement
391
+ AND invalidated_at IS NULL
392
+ """
393
+ )
394
+
395
+ _INSERT_OBSERVATION = text(
396
+ """
397
+ INSERT INTO observations (
398
+ observation_id, deployment_id, subject_entity_id, statement,
399
+ obs_label, normalizer_version
400
+ ) VALUES (
401
+ :observation_id, :deployment_id, :subject_entity_id, :statement,
402
+ :statement, :normalizer_version
403
+ )
404
+ """
405
+ )
406
+
407
+ _INSERT_OBS_ADJUDICATION = text(
408
+ """
409
+ INSERT INTO observation_adjudications (
410
+ adjudication_id, deployment_id, observation_id, outcome, method,
411
+ confidence, triggering_claim_id, features, adjudicator_version
412
+ ) VALUES (
413
+ :adjudication_id, :deployment_id, :observation_id, 'add', 'novelty_gate',
414
+ 1.0, :triggering_claim_id, :features, :adjudicator_version
415
+ )
416
+ """
417
+ ).bindparams(bindparam("features", type_=JSON))
418
+
419
+ _INSERT_OBS_EVIDENCE = text(
420
+ """
421
+ INSERT INTO observation_evidence (
422
+ deployment_id, observation_id, claim_id, doc_id, stance, normalizer_version
423
+ ) VALUES (
424
+ :deployment_id, :observation_id, :claim_id, :doc_id, 'supports',
425
+ :normalizer_version
426
+ )
427
+ ON CONFLICT (observation_id, claim_id) DO NOTHING
428
+ """
429
+ )
430
+
431
+ _RECOUNT_OBSERVATION = text(
432
+ """
433
+ UPDATE observations SET evidence_count = (
434
+ SELECT count(DISTINCT evidence.doc_id)
435
+ FROM observation_evidence evidence
436
+ JOIN claims ON claims.claim_id = evidence.claim_id
437
+ WHERE evidence.observation_id = :observation_id
438
+ AND evidence.stance = 'supports'
439
+ AND claims.is_current_testimony
440
+ ), updated_at = now()
441
+ WHERE observation_id = :observation_id
442
+ """
443
+ )
444
+
445
+ _SELECT_PREDICATES = text(
446
+ """
447
+ SELECT predicate, parent_predicate FROM predicates
448
+ WHERE deployment_id = :deployment_id AND status = 'active'
449
+ """
450
+ )
451
+
452
+ _SELECT_SIGNATURES = text(
453
+ """
454
+ SELECT predicate, subject_type, object_type FROM predicate_signatures
455
+ WHERE deployment_id = :deployment_id
456
+ """
457
+ )
458
+
459
+ _SELECT_TYPE_PARENTS = text(
460
+ """
461
+ SELECT type, parent_type FROM entity_types
462
+ WHERE deployment_id = :deployment_id
463
+ """
464
+ )
465
+
466
+ _SELECT_RELATIONS_FOR_LABELING = text(
467
+ """
468
+ SELECT r.relation_id, subject.canonical_name AS subject_name, r.predicate,
469
+ object.canonical_name AS object_name, r.status::text AS status
470
+ FROM relations r
471
+ JOIN entities subject ON subject.entity_id = r.subject_entity_id
472
+ JOIN entities object ON object.entity_id = r.object_entity_id
473
+ WHERE r.deployment_id = :deployment_id
474
+ AND (r.fact_label_version IS NULL OR r.fact_label_version <> :label_version)
475
+ AND EXISTS (
476
+ SELECT 1 FROM relation_evidence e
477
+ WHERE e.relation_id = r.relation_id AND e.doc_id = :doc_id
478
+ )
479
+ ORDER BY r.created_at, r.relation_id
480
+ """
481
+ )
482
+
483
+ _ACQUIRE_LABEL_LOCK = text("SELECT pg_advisory_lock(hashtextextended(:key, 0))")
484
+ _RELEASE_LABEL_LOCK = text("SELECT pg_advisory_unlock(hashtextextended(:key, 0))")
485
+
486
+ _STAMP_FACT_LABEL = text(
487
+ """
488
+ UPDATE relations
489
+ SET fact_label = :label,
490
+ fact_label_version = :label_version,
491
+ fact_label_embedding_ref = relation_id::text,
492
+ updated_at = now()
493
+ WHERE relation_id = :relation_id
494
+ """
495
+ )
496
+
497
+ _SELECT_OBSERVATIONS_FOR_EMBEDDING = text(
498
+ """
499
+ SELECT observation_id, obs_label, status::text AS status
500
+ FROM observations
501
+ WHERE observations.deployment_id = :deployment_id
502
+ AND (obs_label_version IS NULL OR obs_label_version <> :label_version)
503
+ AND EXISTS (
504
+ SELECT 1 FROM observation_evidence e
505
+ WHERE e.observation_id = observations.observation_id
506
+ AND e.doc_id = :doc_id
507
+ )
508
+ ORDER BY created_at, observation_id
509
+ """
510
+ )
511
+
512
+ _STAMP_OBSERVATION_EMBEDDING = text(
513
+ """
514
+ UPDATE observations
515
+ SET obs_label_version = :label_version,
516
+ obs_label_embedding_ref = observation_id::text,
517
+ updated_at = now()
518
+ WHERE observation_id = :observation_id
519
+ """
520
+ )
521
+
522
+ _BUMP_PREDICATE_USAGE = text(
523
+ """
524
+ UPDATE predicates SET usage_count = usage_count + 1
525
+ WHERE deployment_id = :deployment_id AND predicate = :predicate
526
+ """
527
+ )
528
+
529
+ _INSERT_OTHER_PREDICATE = text(
530
+ """
531
+ INSERT INTO predicates (
532
+ deployment_id, predicate, parent_predicate, description, tier
533
+ ) VALUES (
534
+ :deployment_id, :predicate, 'related_to',
535
+ 'normalizer-emitted other: escape value (D5 funnel; promote on demand)',
536
+ 'other'
537
+ )
538
+ ON CONFLICT (deployment_id, predicate) DO NOTHING
539
+ """
540
+ )
541
+
542
+ _SELECT_PROMOTION_CANDIDATES = text(
543
+ """
544
+ SELECT predicate, usage_count FROM predicates
545
+ WHERE deployment_id = :deployment_id AND tier = 'other'
546
+ AND status = 'active'
547
+ ORDER BY usage_count DESC, predicate
548
+ LIMIT :limit
549
+ """
550
+ )
551
+
552
+ _SELECT_PREDICATE_PROMPT = text(
553
+ """
554
+ SELECT predicate, description, synonyms FROM predicates
555
+ WHERE deployment_id = :deployment_id AND status = 'active'
556
+ AND tier <> 'other'
557
+ ORDER BY predicate
558
+ """
559
+ )
560
+
561
+ _LATEST_CLOSED_UNTIL = text(
562
+ """
563
+ SELECT max(valid_until) FROM relations
564
+ WHERE deployment_id = :deployment_id
565
+ AND subject_entity_id = :subject_entity_id
566
+ AND predicate = :predicate
567
+ AND object_entity_id = :object_entity_id
568
+ AND invalidated_at IS NULL
569
+ AND valid_until IS NOT NULL
570
+ """
571
+ )