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,400 @@
1
+ """The lifecycle eval pack (WP-3.7, D22/D35): the D54 economy, guarded.
2
+
3
+ Three layers of protection over the currency/count machinery, and ONE gate:
4
+ `run_lifecycle_suite` checks the standing invariants AND re-runs every
5
+ planted canary, folds both into a single verdict, and records it — a green
6
+ lifecycle row in `eval_runs` means the whole pack held.
7
+
8
+ - **Invariants** — properties that must hold on the deployment state: the
9
+ currency cache agrees with its ledger (transactional, always checkable),
10
+ cached counts agree with a recompute, closure records agree with fact
11
+ state, and no fact is both closed and under an open flag. Count and
12
+ closure checks can lag legitimately while reconciliation is mid-flight,
13
+ so they run only when the pipeline is QUIESCENT — a busy deployment
14
+ defers them (visibly) instead of alarming falsely.
15
+ - **The flag-rate metric** — `support_withdrawn` flags per superseding
16
+ extractor generation, the live rollout canary (lifecycle §4). A
17
+ non-extractor basis bump (converter/blockizer/structurer) surfaces under
18
+ the unchanged extractor key — its spike is visible in the absolute
19
+ `flags_raised` jump, and each flag records its full basis coordinates
20
+ for exact attribution; a per-coordinate rate refinement is a D22
21
+ follow-up measurement.
22
+ - **Planted canaries** — every `restore_support` verdict plants a D35
23
+ canary (`spine/review.py`). The guard is the FACT's support, not the
24
+ original claim row: a fixed extractor legitimately re-derives the
25
+ content as a NEW claim (immutability) and the old one flips non-current
26
+ — the canary must pass then, and fail only when the fact's current
27
+ support silently vanishes again.
28
+ """
29
+
30
+ from uuid import UUID
31
+ from uuid import uuid4
32
+
33
+ from sqlalchemy import bindparam
34
+ from sqlalchemy import JSON
35
+ from sqlalchemy import text
36
+ from sqlalchemy import TextClause
37
+ from sqlalchemy.engine import Connection
38
+ from sqlalchemy.engine import Engine
39
+
40
+ from rememberstack.eval.harness import EvalHarness
41
+ from rememberstack.model import CanaryCase
42
+ from rememberstack.model import EvalSuite
43
+ from rememberstack.model import LifecycleReport
44
+ from rememberstack.spine.lifecycle import CURRENCY_CACHE_MISMATCH_SQL
45
+
46
+
47
+ def run_lifecycle_suite(
48
+ *, engine: Engine, deployment_id: UUID, component_version: str
49
+ ) -> LifecycleReport:
50
+ """The single lifecycle gate: invariants + planted canaries, one verdict.
51
+
52
+ All invariant reads share one REPEATABLE READ snapshot (no torn view
53
+ across statements). When reconcile/finalization work is in flight, the
54
+ count/closure checks defer to the next quiescent run — recorded on the
55
+ report, never silently skipped. The flag rate is a metric to watch
56
+ (its alarm threshold is an operations decision), never part of the
57
+ verdict.
58
+ """
59
+ violations: dict[str, tuple[str, ...]] = {}
60
+ with engine.connect().execution_options(
61
+ isolation_level="REPEATABLE READ"
62
+ ) as connection:
63
+ quiescent = (
64
+ connection.execute(
65
+ _COUNT_INFLIGHT_RECONCILES, {"deployment_id": deployment_id}
66
+ ).scalar_one()
67
+ == 0
68
+ )
69
+ for name, requires_quiescence, statement in _INVARIANTS:
70
+ if requires_quiescence and not quiescent:
71
+ continue # legitimately lagging mid-flight: deferred, visibly
72
+ offenders = tuple(
73
+ str(value)
74
+ for value in connection.execute(
75
+ statement, {"deployment_id": deployment_id}
76
+ ).scalars()
77
+ )
78
+ if offenders:
79
+ violations[name] = offenders
80
+ canary_failures = tuple(
81
+ f"{case.canary_id}: {case.description}"
82
+ for case in _load_canaries(
83
+ connection=connection, deployment_id=deployment_id
84
+ )
85
+ if not _canary_holds(connection=connection, case=case)
86
+ )
87
+ flag_rates = flag_rate_by_extractor(engine=engine, deployment_id=deployment_id)
88
+ report = LifecycleReport(
89
+ passed=not violations and not canary_failures,
90
+ quiescent=quiescent,
91
+ violations=violations,
92
+ canary_failures=canary_failures,
93
+ flag_rate_by_extractor=flag_rates,
94
+ )
95
+ with engine.begin() as connection:
96
+ connection.execute(
97
+ _RECORD_RUN,
98
+ {
99
+ "eval_run_id": uuid4(),
100
+ "deployment_id": deployment_id,
101
+ "component_version": component_version,
102
+ "metrics": report.model_dump(mode="json"),
103
+ "passed": report.passed,
104
+ },
105
+ )
106
+ return report
107
+
108
+
109
+ def flag_rate_by_extractor(
110
+ *, engine: Engine, deployment_id: UUID
111
+ ) -> dict[str, dict[str, float]]:
112
+ """`support_withdrawn` flags per superseding extractor generation.
113
+
114
+ The rollout canary (lifecycle §4): a spike right after an upgrade means
115
+ the new generation is failing to re-derive what the corpus still says.
116
+ Shape (a starting point, D22): per generation V —
117
+ ``flags_raised`` (flags whose diff names V as the superseding
118
+ generation), ``current_claims`` (V's live corpus coverage), and
119
+ ``flag_rate = flags / (flags + current_claims)`` — a saturating
120
+ proportion that reads 1.0 when a generation only withdraws and never
121
+ re-derives, and falls toward 0 as its coverage dominates.
122
+ """
123
+ with engine.connect() as connection:
124
+ coverage = {
125
+ row["extractor_version"]: row["current_claims"]
126
+ for row in connection.execute(
127
+ _COUNT_GENERATION_COVERAGE, {"deployment_id": deployment_id}
128
+ ).mappings()
129
+ }
130
+ flags = {
131
+ row["to_version"]: row["flags"]
132
+ for row in connection.execute(
133
+ _COUNT_FLAGS, {"deployment_id": deployment_id}
134
+ ).mappings()
135
+ }
136
+ return {
137
+ version: {
138
+ "current_claims": float(coverage.get(version, 0)),
139
+ "flags_raised": float(flags.get(version, 0)),
140
+ "flag_rate": (
141
+ flags.get(version, 0)
142
+ / (flags.get(version, 0) + coverage.get(version, 0))
143
+ if flags.get(version, 0) or coverage.get(version, 0)
144
+ else 0.0
145
+ ),
146
+ }
147
+ for version in sorted({*coverage, *flags})
148
+ }
149
+
150
+
151
+ def register_lifecycle_evaluator(*, harness: EvalHarness, engine: Engine) -> None:
152
+ """Bind the planted-canary evaluator for standalone harness runs.
153
+
154
+ The same guard `run_lifecycle_suite` applies — kept registered so the
155
+ generic harness surface can re-run the lifecycle canaries alongside
156
+ other suites.
157
+ """
158
+
159
+ def _evaluate(case: CanaryCase) -> bool:
160
+ with engine.connect() as connection:
161
+ return _canary_holds(connection=connection, case=case)
162
+
163
+ harness.register_evaluator(suite=EvalSuite.LIFECYCLE, evaluator=_evaluate)
164
+
165
+
166
+ def _canary_holds(*, connection: Connection, case: CanaryCase) -> bool:
167
+ """The planted guard: the fact's CURRENT support has not vanished again.
168
+
169
+ Immutability means a fixed extractor re-derives the content as a new
170
+ claim row and the restored one legitimately flips non-current — so the
171
+ guard checks the fact, not the original claim: at least one
172
+ current-testimony claim still supports it (the restored one or a
173
+ re-derived successor).
174
+ """
175
+ fact_kind = case.expected.get("fact_kind")
176
+ fact_id = case.expected.get("fact_id")
177
+ if not isinstance(fact_kind, str) or not isinstance(fact_id, str):
178
+ return False # a malformed canary never passes silently
179
+ statement = (
180
+ _RELATION_HAS_CURRENT_SUPPORT
181
+ if fact_kind == "relation"
182
+ else _OBSERVATION_HAS_CURRENT_SUPPORT
183
+ )
184
+ return bool(
185
+ connection.execute(statement, {"fact_id": UUID(fact_id)}).scalar_one_or_none()
186
+ )
187
+
188
+
189
+ def _load_canaries(
190
+ *, connection: Connection, deployment_id: UUID
191
+ ) -> tuple[CanaryCase, ...]:
192
+ """The deployment's planted lifecycle canaries."""
193
+ rows = connection.execute(
194
+ _SELECT_LIFECYCLE_CANARIES, {"deployment_id": deployment_id}
195
+ ).mappings()
196
+ return tuple(
197
+ CanaryCase(
198
+ canary_id=row["canary_id"],
199
+ suite=EvalSuite.LIFECYCLE,
200
+ description=row["description"],
201
+ input=row["input"],
202
+ expected=row["expected"],
203
+ )
204
+ for row in rows
205
+ )
206
+
207
+
208
+ _INVARIANTS: tuple[tuple[str, bool, TextClause], ...] = (
209
+ (
210
+ # the D33 pattern's contract: the ledger is truth, the flag is cache.
211
+ # Transactional (event + cache flip commit together), so it is
212
+ # checkable at ANY moment. A claim with no events must sit at the
213
+ # schema's initial state (current) — a false flag with an empty
214
+ # ledger is exactly the corruption this exists to catch.
215
+ "currency_cache_matches_ledger",
216
+ False,
217
+ text(CURRENCY_CACHE_MISMATCH_SQL),
218
+ ),
219
+ (
220
+ # D54: BOTH cached counts must equal a recompute (quiescent only —
221
+ # a mid-flight reconcile legitimately lags between transactions)
222
+ "relation_counts_match_recompute",
223
+ True,
224
+ text(
225
+ """
226
+ SELECT r.relation_id
227
+ FROM relations r
228
+ WHERE r.deployment_id = :deployment_id
229
+ AND (r.evidence_count <> (
230
+ SELECT count(DISTINCT e.doc_id)
231
+ FROM relation_evidence e
232
+ JOIN claims cl ON cl.claim_id = e.claim_id
233
+ WHERE e.relation_id = r.relation_id
234
+ AND e.stance = 'supports'
235
+ AND cl.is_current_testimony)
236
+ OR r.contradict_count <> (
237
+ SELECT count(DISTINCT e.doc_id)
238
+ FROM relation_evidence e
239
+ JOIN claims cl ON cl.claim_id = e.claim_id
240
+ WHERE e.relation_id = r.relation_id
241
+ AND e.stance = 'contradicts'
242
+ AND cl.is_current_testimony))
243
+ """
244
+ ),
245
+ ),
246
+ (
247
+ "observation_counts_match_recompute",
248
+ True,
249
+ text(
250
+ """
251
+ SELECT o.observation_id
252
+ FROM observations o
253
+ WHERE o.deployment_id = :deployment_id
254
+ AND (o.evidence_count <> (
255
+ SELECT count(DISTINCT e.doc_id)
256
+ FROM observation_evidence e
257
+ JOIN claims cl ON cl.claim_id = e.claim_id
258
+ WHERE e.observation_id = o.observation_id
259
+ AND e.stance = 'supports'
260
+ AND cl.is_current_testimony)
261
+ OR o.contradict_count <> (
262
+ SELECT count(DISTINCT e.doc_id)
263
+ FROM observation_evidence e
264
+ JOIN claims cl ON cl.claim_id = e.claim_id
265
+ WHERE e.observation_id = o.observation_id
266
+ AND e.stance = 'contradicts'
267
+ AND cl.is_current_testimony))
268
+ """
269
+ ),
270
+ ),
271
+ (
272
+ # every mechanical retraction record is real, both shapes: a live
273
+ # retraction adjudication's fact must actually be closed
274
+ "retraction_records_match_closures",
275
+ True,
276
+ text(
277
+ """
278
+ SELECT a.relation_id
279
+ FROM relation_adjudications a
280
+ JOIN relations r ON r.relation_id = a.relation_id
281
+ WHERE a.deployment_id = :deployment_id
282
+ AND a.outcome = 'retracted_source_removal'
283
+ AND a.superseded_by IS NULL
284
+ AND r.valid_until IS NULL
285
+ AND r.invalidated_at IS NULL
286
+ UNION ALL
287
+ SELECT a.observation_id
288
+ FROM observation_adjudications a
289
+ JOIN observations o ON o.observation_id = a.observation_id
290
+ WHERE a.deployment_id = :deployment_id
291
+ AND a.outcome = 'retracted_source_removal'
292
+ AND a.superseded_by IS NULL
293
+ AND o.invalidated_at IS NULL
294
+ """
295
+ ),
296
+ ),
297
+ (
298
+ # the §4 fork must never merge, both shapes: a fact under an open
299
+ # flag is the reviewer's to decide — it can never also have been
300
+ # mechanically retracted or invalidated behind the reviewer's back
301
+ "flagged_facts_never_closed",
302
+ True,
303
+ text(
304
+ """
305
+ SELECT r.relation_id
306
+ FROM relations r
307
+ JOIN review_queue q
308
+ ON q.candidate ->> 'fact_id' = r.relation_id::text
309
+ WHERE r.deployment_id = :deployment_id
310
+ AND q.item_kind = 'support_withdrawn'
311
+ AND q.status IN ('pending', 'deferred')
312
+ AND (r.invalidated_at IS NOT NULL
313
+ OR EXISTS (
314
+ SELECT 1 FROM relation_adjudications a
315
+ WHERE a.relation_id = r.relation_id
316
+ AND a.outcome = 'retracted_source_removal'
317
+ AND a.superseded_by IS NULL))
318
+ UNION ALL
319
+ SELECT o.observation_id
320
+ FROM observations o
321
+ JOIN review_queue q
322
+ ON q.candidate ->> 'fact_id' = o.observation_id::text
323
+ WHERE o.deployment_id = :deployment_id
324
+ AND q.item_kind = 'support_withdrawn'
325
+ AND q.status IN ('pending', 'deferred')
326
+ AND o.invalidated_at IS NOT NULL
327
+ """
328
+ ),
329
+ ),
330
+ )
331
+
332
+ _COUNT_INFLIGHT_RECONCILES = text(
333
+ """
334
+ SELECT count(*) FROM processing_state
335
+ WHERE deployment_id = :deployment_id
336
+ AND stage = 'reconcile'
337
+ AND status IN ('pending', 'running', 'failed')
338
+ """
339
+ )
340
+
341
+ _COUNT_GENERATION_COVERAGE = text(
342
+ """
343
+ SELECT extractor_version, count(*) AS current_claims
344
+ FROM claims
345
+ WHERE deployment_id = :deployment_id AND is_current_testimony
346
+ GROUP BY extractor_version
347
+ """
348
+ )
349
+
350
+ _COUNT_FLAGS = text(
351
+ """
352
+ SELECT q.candidate -> 'diff' ->> 'to_extractor_version' AS to_version,
353
+ count(*) AS flags
354
+ FROM review_queue q
355
+ WHERE q.deployment_id = :deployment_id
356
+ AND q.item_kind = 'support_withdrawn'
357
+ GROUP BY q.candidate -> 'diff' ->> 'to_extractor_version'
358
+ """
359
+ )
360
+
361
+ _RELATION_HAS_CURRENT_SUPPORT = text(
362
+ """
363
+ SELECT 1 FROM relation_evidence e
364
+ JOIN claims cl ON cl.claim_id = e.claim_id
365
+ WHERE e.relation_id = :fact_id
366
+ AND e.stance = 'supports'
367
+ AND cl.is_current_testimony
368
+ LIMIT 1
369
+ """
370
+ )
371
+
372
+ _OBSERVATION_HAS_CURRENT_SUPPORT = text(
373
+ """
374
+ SELECT 1 FROM observation_evidence e
375
+ JOIN claims cl ON cl.claim_id = e.claim_id
376
+ WHERE e.observation_id = :fact_id
377
+ AND e.stance = 'supports'
378
+ AND cl.is_current_testimony
379
+ LIMIT 1
380
+ """
381
+ )
382
+
383
+ _SELECT_LIFECYCLE_CANARIES = text(
384
+ """
385
+ SELECT canary_id, description, input, expected
386
+ FROM canary_cases
387
+ WHERE deployment_id = :deployment_id AND suite = 'lifecycle'
388
+ """
389
+ )
390
+
391
+ _RECORD_RUN = text(
392
+ """
393
+ INSERT INTO eval_runs (
394
+ eval_run_id, deployment_id, suite, component_version, metrics, passed
395
+ ) VALUES (
396
+ :eval_run_id, :deployment_id, 'lifecycle', :component_version,
397
+ :metrics, :passed
398
+ )
399
+ """
400
+ ).bindparams(bindparam("metrics", type_=JSON))
@@ -0,0 +1,49 @@
1
+ """Persistence boundary for the WP-7.2 portable scale report."""
2
+
3
+ from typing import Final
4
+ from uuid import UUID
5
+ from uuid import uuid4
6
+
7
+ from sqlalchemy import bindparam
8
+ from sqlalchemy import JSON
9
+ from sqlalchemy import text
10
+ from sqlalchemy.engine import Engine
11
+
12
+ from rememberstack.model import OperationalScaleReport
13
+
14
+ OPERATIONAL_SCALE_VERSION: Final = "operational-scale-2026.07"
15
+
16
+
17
+ def record_operational_scale_report(
18
+ *,
19
+ engine: Engine,
20
+ deployment_id: UUID,
21
+ report: OperationalScaleReport,
22
+ component_version: str = OPERATIONAL_SCALE_VERSION,
23
+ ) -> UUID:
24
+ """Append one complete provider-neutral scale report to eval history."""
25
+ eval_run_id = uuid4()
26
+ with engine.begin() as connection:
27
+ connection.execute(
28
+ _INSERT_RUN,
29
+ {
30
+ "eval_run_id": eval_run_id,
31
+ "deployment_id": deployment_id,
32
+ "component_version": component_version,
33
+ "metrics": report.model_dump(mode="json"),
34
+ "passed": report.passed,
35
+ },
36
+ )
37
+ return eval_run_id
38
+
39
+
40
+ _INSERT_RUN = text(
41
+ """
42
+ INSERT INTO eval_runs (
43
+ eval_run_id, deployment_id, suite, component_version, metrics, passed
44
+ ) VALUES (
45
+ :eval_run_id, :deployment_id, 'operational', :component_version,
46
+ :metrics, :passed
47
+ )
48
+ """
49
+ ).bindparams(bindparam("metrics", type_=JSON))