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,299 @@
1
+ """Straight-line D74 request admission and portable-manifest acceptance."""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Protocol
6
+ from uuid import UUID
7
+
8
+ from rememberstack.model import ClaimedWork
9
+ from rememberstack.model import ForgetInProgressError
10
+ from rememberstack.model import ForgetManifest
11
+ from rememberstack.model import ForgetManifestStatus
12
+ from rememberstack.model import ForgetRedactionRequiredError
13
+ from rememberstack.model import NonRetryableHandlerError
14
+ from rememberstack.ports import ForgetManifestPort
15
+ from rememberstack.ports import KGitPurgePort
16
+ from rememberstack.ports import ObjectPurgePort
17
+ from rememberstack.ports import P1PurgePort
18
+ from rememberstack.ports import ProjectionPurgePort
19
+ from rememberstack.ports.cost_meter import CostMeterPort
20
+ from rememberstack.spine import ForgetCatalog
21
+ from rememberstack.workers.base import HandlerOutcome
22
+ from rememberstack.workers.knowledge_driver import KnowledgeCommitDriver
23
+ from rememberstack.workers.p2 import GraphRebuildWorker
24
+ from rememberstack.workers.p3 import CorpusFsBuilder
25
+ from rememberstack.workers.reconcile import DeletionService
26
+
27
+
28
+ class ForgetProjectionRebuilder(Protocol):
29
+ """Publish clean P2/P3 snapshots from the scrubbed authoritative spine."""
30
+
31
+ def rebuild_without_lineage(self, *, deployment_id: UUID, forget_id: UUID) -> None:
32
+ """Build and publish both clean projections idempotently."""
33
+ ...
34
+
35
+
36
+ class ForgetKnowledgeRebuilder(Protocol):
37
+ """Recompile affected machine-owned K pages after source removal."""
38
+
39
+ def recompile_without_lineage(
40
+ self, *, deployment_id: UUID, artifact_ids: tuple[UUID, ...]
41
+ ) -> None:
42
+ """Publish sanitized current compiled files before history erasure."""
43
+ ...
44
+
45
+
46
+ class ProjectionPairForgetRebuilder:
47
+ """Publish clean P2 and P3 through the existing whole-rebuild workers."""
48
+
49
+ def __init__(
50
+ self, *, graph: GraphRebuildWorker, corpus: CorpusFsBuilder, workdir: Path
51
+ ) -> None:
52
+ """Bind the two production builders and one explicit scratch root."""
53
+ self._graph = graph
54
+ self._corpus = corpus
55
+ self._workdir = workdir
56
+
57
+ def rebuild_without_lineage(self, *, deployment_id: UUID, forget_id: UUID) -> None:
58
+ """Publish both clean pointers; retries may safely publish a newer pair."""
59
+ self._graph.rebuild(
60
+ deployment_id=deployment_id, workdir=self._workdir / str(forget_id)
61
+ )
62
+ self._corpus.build(deployment_id=deployment_id)
63
+
64
+
65
+ class KnowledgeCycleForgetRebuilder:
66
+ """Compile the affected stale pages through the existing single K driver."""
67
+
68
+ def __init__(self, *, driver: KnowledgeCommitDriver) -> None:
69
+ """Bind the production commit cycle after PostgreSQL marks pages stale."""
70
+ self._driver = driver
71
+
72
+ def recompile_without_lineage(
73
+ self, *, deployment_id: UUID, artifact_ids: tuple[UUID, ...]
74
+ ) -> None:
75
+ """Run one K commit only when the manifest names affected artifacts."""
76
+ if artifact_ids:
77
+ self._driver.run_cycle(
78
+ deployment_id=deployment_id, exclusions_by_artifact={}
79
+ )
80
+
81
+
82
+ class HardForgetService:
83
+ """Prepare, freeze, durably append, and enqueue one lineage forget."""
84
+
85
+ def __init__(
86
+ self,
87
+ *,
88
+ catalog: ForgetCatalog,
89
+ manifest_store: ForgetManifestPort,
90
+ k_git: KGitPurgePort,
91
+ ) -> None:
92
+ """Bind the local materialization and the two portable boundaries."""
93
+ self._catalog = catalog
94
+ self._manifest_store = manifest_store
95
+ self._k_git = k_git
96
+
97
+ def request(
98
+ self,
99
+ *,
100
+ deployment_id: UUID,
101
+ doc_id: UUID,
102
+ forget_id: UUID,
103
+ requested_at: datetime,
104
+ ) -> ForgetManifest:
105
+ """Accept one request only after its exact manifest is durably appended."""
106
+ existing = self._catalog.record_for_doc(
107
+ deployment_id=deployment_id, doc_id=doc_id
108
+ )
109
+ if existing is None:
110
+ self._require_redacted(deployment_id=deployment_id, doc_id=doc_id)
111
+ progress = self._catalog.prepare(
112
+ deployment_id=deployment_id, doc_id=doc_id, forget_id=forget_id
113
+ )
114
+ if progress.status in (
115
+ ForgetManifestStatus.ACCEPTED,
116
+ ForgetManifestStatus.COMPLETE,
117
+ ):
118
+ if progress.manifest is None:
119
+ raise RuntimeError(
120
+ f"accepted forget_id {forget_id} has no local manifest"
121
+ )
122
+ self._manifest_store.append(manifest=progress.manifest)
123
+ return progress.manifest
124
+ manifest = progress.manifest
125
+ if manifest is None:
126
+ if not self._catalog.ordinary_work_is_drained(deployment_id=deployment_id):
127
+ raise ForgetInProgressError(
128
+ f"forget_id {forget_id} is waiting for ordinary work to drain"
129
+ )
130
+ try:
131
+ self._require_redacted(deployment_id=deployment_id, doc_id=doc_id)
132
+ except ForgetRedactionRequiredError:
133
+ self._catalog.cancel_unstored_preparation(
134
+ deployment_id=deployment_id, forget_id=forget_id
135
+ )
136
+ raise
137
+ manifest = self._catalog.inventory_and_store_manifest(
138
+ deployment_id=deployment_id,
139
+ doc_id=doc_id,
140
+ forget_id=forget_id,
141
+ requested_at=requested_at,
142
+ )
143
+ self._manifest_store.append(manifest=manifest)
144
+ self._catalog.accept_and_enqueue(manifest=manifest)
145
+ return manifest
146
+
147
+ def _require_redacted(self, *, deployment_id: UUID, doc_id: UUID) -> None:
148
+ """Refuse owner-controlled K citations without rewriting their prose."""
149
+ paths = tuple(
150
+ sorted(
151
+ set(
152
+ self._k_git.blocking_redaction_paths(
153
+ deployment_id=deployment_id, doc_id=doc_id
154
+ )
155
+ )
156
+ )
157
+ )
158
+ if paths:
159
+ raise ForgetRedactionRequiredError(paths=paths)
160
+
161
+
162
+ class HardForgetHandler:
163
+ """Run one accepted manifest through the single resumable purge sequence."""
164
+
165
+ def __init__(
166
+ self,
167
+ *,
168
+ catalog: ForgetCatalog,
169
+ deletion: DeletionService,
170
+ object_purgers: tuple[ObjectPurgePort, ...],
171
+ p1: P1PurgePort,
172
+ projection_rebuilder: ForgetProjectionRebuilder,
173
+ projection_purger: ProjectionPurgePort,
174
+ knowledge_rebuilder: ForgetKnowledgeRebuilder,
175
+ k_git: KGitPurgePort,
176
+ ) -> None:
177
+ """Bind existing lifecycle/rebuild services and narrow purge capabilities."""
178
+ if not object_purgers:
179
+ raise ValueError("hard-forget requires at least one object-store purger")
180
+ self._catalog = catalog
181
+ self._deletion = deletion
182
+ self._object_purgers = object_purgers
183
+ self._p1 = p1
184
+ self._projection_rebuilder = projection_rebuilder
185
+ self._projection_purger = projection_purger
186
+ self._knowledge_rebuilder = knowledge_rebuilder
187
+ self._k_git = k_git
188
+
189
+ def handle(self, *, work: ClaimedWork, meter: CostMeterPort) -> HandlerOutcome:
190
+ """Honor every idempotent stage in order and reopen only after verification."""
191
+ del meter
192
+ forget_id = _forget_id(work=work)
193
+ manifest = self._catalog.manifest_for(
194
+ deployment_id=work.deployment_id, forget_id=forget_id
195
+ )
196
+ self.honor(manifest=manifest)
197
+ return HandlerOutcome()
198
+
199
+ def honor(self, *, manifest: ForgetManifest) -> None:
200
+ """Run the shared purge path used by both the worker and readiness replay."""
201
+ self._deletion.delete_lineage(
202
+ deployment_id=manifest.deployment_id, doc_id=manifest.doc_id
203
+ )
204
+ self._catalog.scrub_postgres(manifest=manifest)
205
+ for purger in self._object_purgers:
206
+ purger.purge_objects(keys=manifest.object_keys, prefixes=())
207
+ self._p1.purge_rows(
208
+ deployment_id=manifest.deployment_id,
209
+ chunk_ids=manifest.chunk_ids,
210
+ claim_ids=manifest.claim_ids,
211
+ fact_ids=manifest.fact_ids,
212
+ entity_ids=manifest.entity_ids,
213
+ )
214
+ self._projection_rebuilder.rebuild_without_lineage(
215
+ deployment_id=manifest.deployment_id, forget_id=manifest.forget_id
216
+ )
217
+ self._projection_purger.purge_projections(
218
+ deployment_id=manifest.deployment_id, prefixes=manifest.projection_prefixes
219
+ )
220
+ self._knowledge_rebuilder.recompile_without_lineage(
221
+ deployment_id=manifest.deployment_id, artifact_ids=manifest.k_artifact_ids
222
+ )
223
+ self._k_git.purge_artifacts(
224
+ deployment_id=manifest.deployment_id,
225
+ forget_id=manifest.forget_id,
226
+ artifact_ids=manifest.k_artifact_ids,
227
+ )
228
+ for purger in self._object_purgers:
229
+ purger.verify_objects_purged(keys=manifest.object_keys, prefixes=())
230
+ self._p1.verify_rows_purged(
231
+ deployment_id=manifest.deployment_id,
232
+ chunk_ids=manifest.chunk_ids,
233
+ claim_ids=manifest.claim_ids,
234
+ fact_ids=manifest.fact_ids,
235
+ entity_ids=manifest.entity_ids,
236
+ )
237
+ self._projection_purger.verify_projections_purged(
238
+ deployment_id=manifest.deployment_id, prefixes=manifest.projection_prefixes
239
+ )
240
+ self._k_git.verify_artifacts_purged(
241
+ deployment_id=manifest.deployment_id,
242
+ forget_id=manifest.forget_id,
243
+ artifact_ids=manifest.k_artifact_ids,
244
+ )
245
+ self._catalog.verify_postgres_scrubbed(manifest=manifest)
246
+ self._catalog.mark_complete(manifest=manifest)
247
+
248
+
249
+ class HardForgetReadiness:
250
+ """Re-honor portable intent before one deployment accepts serving traffic."""
251
+
252
+ def __init__(
253
+ self,
254
+ *,
255
+ catalog: ForgetCatalog,
256
+ manifest_store: ForgetManifestPort,
257
+ request_service: HardForgetService,
258
+ handler: HardForgetHandler,
259
+ ) -> None:
260
+ """Bind one source of intent to the same request and purge implementations."""
261
+ self._catalog = catalog
262
+ self._manifest_store = manifest_store
263
+ self._request_service = request_service
264
+ self._handler = handler
265
+
266
+ def ensure_ready(self, *, deployment_id: UUID) -> tuple[UUID, ...]:
267
+ """Recover preparation, rematerialize, and re-honor every portable manifest."""
268
+ pending = self._catalog.preparing_record(deployment_id=deployment_id)
269
+ if pending is not None:
270
+ self._request_service.request(
271
+ deployment_id=pending.deployment_id,
272
+ doc_id=pending.doc_id,
273
+ forget_id=pending.forget_id,
274
+ requested_at=(
275
+ pending.manifest.requested_at
276
+ if pending.manifest is not None
277
+ else pending.prepared_at
278
+ ),
279
+ )
280
+ manifests = self._manifest_store.manifests(deployment_id=deployment_id)
281
+ for manifest in manifests:
282
+ self._catalog.materialize_portable(manifest=manifest)
283
+ self._handler.honor(manifest=manifest)
284
+ return tuple(manifest.forget_id for manifest in manifests)
285
+
286
+
287
+ def _forget_id(*, work: ClaimedWork) -> UUID:
288
+ """Read the required portable identity from one accepted worker payload."""
289
+ value = (work.payload or {}).get("forget_id")
290
+ if not isinstance(value, str):
291
+ raise NonRetryableHandlerError(
292
+ f"hard-forget work {work.processing_id} carries no forget_id"
293
+ )
294
+ try:
295
+ return UUID(value)
296
+ except ValueError as error:
297
+ raise NonRetryableHandlerError(
298
+ f"hard-forget work {work.processing_id} has invalid forget_id"
299
+ ) from error
@@ -0,0 +1,146 @@
1
+ """Authored checkout synchronization and reliable subscription dispatch delivery."""
2
+
3
+ from pathlib import Path
4
+ from typing import Protocol
5
+ from uuid import UUID
6
+
7
+ from rememberstack.core import knowledge_content_hash
8
+ from rememberstack.core import parse_knowledge_authored_frontmatter
9
+ from rememberstack.model import ClaimedWork
10
+ from rememberstack.model import KnowledgeAuthoredPageSync
11
+ from rememberstack.model import KnowledgeAuthoredSyncResult
12
+ from rememberstack.model import KnowledgeDispatchStatus
13
+ from rememberstack.model import KnowledgePageKind
14
+ from rememberstack.model import KnowledgeWorkflowDelivery
15
+ from rememberstack.model import NonRetryableHandlerError
16
+ from rememberstack.model import PipelineStage
17
+ from rememberstack.model import ProcessingTarget
18
+ from rememberstack.ports.cost_meter import CostMeterPort
19
+ from rememberstack.spine.knowledge import KnowledgeCompilationError
20
+ from rememberstack.spine.knowledge import KnowledgeControlPlane
21
+ from rememberstack.spine.knowledge import KnowledgeDispatchUnavailableError
22
+ from rememberstack.workers.base import HandlerOutcome
23
+
24
+
25
+ class KnowledgeWorkflowDispatcher(Protocol):
26
+ """External workflow-delivery port; consumers are idempotent by dispatch ID."""
27
+
28
+ def deliver(self, *, delivery: KnowledgeWorkflowDelivery) -> None:
29
+ """Deliver one delta-carrying authored/subscription notification."""
30
+ ...
31
+
32
+
33
+ class KnowledgeAuthoredSynchronizer:
34
+ """Discover authored Markdown in one exact checkout and sync its declarations."""
35
+
36
+ def __init__(self, *, control_plane: KnowledgeControlPlane) -> None:
37
+ """Bind checkout discovery to the authoritative Plane-K control state."""
38
+ self._control_plane = control_plane
39
+
40
+ def sync_checkout(
41
+ self, *, deployment_id: UUID, worktree: Path, git_revision: str
42
+ ) -> KnowledgeAuthoredSyncResult:
43
+ """Register new Markdown as authored and resync every known authored body."""
44
+ states = self._control_plane.artifact_path_states(deployment_id=deployment_id)
45
+ bodies = {state.git_path: state for state in states}
46
+ curation_paths = {
47
+ state.curation_path for state in states if state.curation_path is not None
48
+ }
49
+ registered: list[UUID] = []
50
+ synced: list[UUID] = []
51
+ linted: list[UUID] = []
52
+ for target in sorted(worktree.rglob("*.md")):
53
+ relative = target.relative_to(worktree)
54
+ if ".git" in relative.parts:
55
+ continue
56
+ _require_safe_authored_target(
57
+ worktree=worktree, target=target, git_path=relative.as_posix()
58
+ )
59
+ git_path = relative.as_posix()
60
+ state = bodies.get(git_path)
61
+ if git_path in curation_paths or (
62
+ state is not None and state.page_kind is KnowledgePageKind.COMPILED
63
+ ):
64
+ continue
65
+ markdown = target.read_text(encoding="utf-8")
66
+ result = self._control_plane.sync_authored_page(
67
+ sync=KnowledgeAuthoredPageSync(
68
+ deployment_id=deployment_id,
69
+ git_path=git_path,
70
+ markdown=markdown,
71
+ content_hash=knowledge_content_hash(markdown=markdown),
72
+ git_revision=git_revision,
73
+ declaration=parse_knowledge_authored_frontmatter(markdown=markdown),
74
+ )
75
+ )
76
+ if result.registered:
77
+ registered.append(result.artifact_id)
78
+ else:
79
+ synced.append(result.artifact_id)
80
+ if result.lint_flagged:
81
+ linted.append(result.artifact_id)
82
+ return KnowledgeAuthoredSyncResult(
83
+ registered_artifact_ids=tuple(registered),
84
+ synced_artifact_ids=tuple(synced),
85
+ lint_flag_artifact_ids=tuple(linted),
86
+ )
87
+
88
+
89
+ class KnowledgeDispatchHandler:
90
+ """Deliver one D67 knowledge-dispatch target through the external workflow port."""
91
+
92
+ def __init__(
93
+ self,
94
+ *,
95
+ control_plane: KnowledgeControlPlane,
96
+ dispatcher: KnowledgeWorkflowDispatcher,
97
+ ) -> None:
98
+ """Bind domain dispatch state to one delivery adapter."""
99
+ self._control_plane = control_plane
100
+ self._dispatcher = dispatcher
101
+
102
+ def handle(self, *, work: ClaimedWork, meter: CostMeterPort) -> HandlerOutcome:
103
+ """Deliver at least once and keep both delivery/mirror failures visible."""
104
+ del meter
105
+ if (
106
+ work.target_kind is not ProcessingTarget.KNOWLEDGE_DISPATCH
107
+ or work.stage is not PipelineStage.DISPATCH_KNOWLEDGE
108
+ ):
109
+ raise NonRetryableHandlerError(
110
+ "dispatch handler requires the knowledge_dispatch/dispatch_knowledge route"
111
+ )
112
+ try:
113
+ record = self._control_plane.begin_dispatch(dispatch_id=work.target_id)
114
+ except KnowledgeDispatchUnavailableError as error:
115
+ raise NonRetryableHandlerError(str(error)) from error
116
+ if record.status is KnowledgeDispatchStatus.DONE:
117
+ return HandlerOutcome()
118
+ delivery = KnowledgeWorkflowDelivery(
119
+ dispatch_id=record.dispatch_id,
120
+ workflow_endpoint=record.workflow_endpoint,
121
+ payload=record.payload,
122
+ )
123
+ try:
124
+ self._dispatcher.deliver(delivery=delivery)
125
+ except Exception as delivery_error:
126
+ try:
127
+ self._control_plane.fail_dispatch(dispatch_id=record.dispatch_id)
128
+ except Exception as mirror_error:
129
+ raise ExceptionGroup(
130
+ "workflow delivery and dispatch failure-mirror both failed",
131
+ [delivery_error, mirror_error],
132
+ ) from delivery_error
133
+ raise
134
+ self._control_plane.complete_dispatch(dispatch_id=record.dispatch_id)
135
+ return HandlerOutcome()
136
+
137
+
138
+ def _require_safe_authored_target(
139
+ *, worktree: Path, target: Path, git_path: str
140
+ ) -> None:
141
+ """Reject symlinked or resolved Markdown paths outside the exact checkout."""
142
+ root = worktree.resolve()
143
+ if target.is_symlink() or not target.resolve().is_relative_to(root):
144
+ raise KnowledgeCompilationError(
145
+ f"authored page path escapes worktree: {git_path}"
146
+ )