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,393 @@
1
+ """Stock-harness prose compiler for deterministic two-band Plane-K pages."""
2
+
3
+ from collections.abc import Callable
4
+ from datetime import datetime
5
+ from datetime import UTC
6
+ import json
7
+ from pathlib import PurePosixPath
8
+ import re
9
+ import traceback
10
+ from typing import Final
11
+ from typing import Protocol
12
+ from uuid import UUID
13
+ from uuid import uuid4
14
+
15
+ from pydantic import Field
16
+ from pydantic import field_validator
17
+ from pydantic import TypeAdapter
18
+ from pydantic_settings import BaseSettings
19
+ from pydantic_settings import SettingsConfigDict
20
+
21
+ from rememberstack.core import cap_knowledge_writer_bundle
22
+ from rememberstack.core import compose_knowledge_page
23
+ from rememberstack.core import knowledge_content_hash
24
+ from rememberstack.core import knowledge_inputs_hash
25
+ from rememberstack.core import knowledge_summary_hash
26
+ from rememberstack.core import knowledge_writer_coverage
27
+ from rememberstack.core import render_knowledge_fact_sheet
28
+ from rememberstack.core import render_knowledge_writer_bundle
29
+ from rememberstack.model import KnowledgeCitation
30
+ from rememberstack.model import KnowledgeCompilationFailure
31
+ from rememberstack.model import KnowledgeCompilationWrite
32
+ from rememberstack.model import KnowledgeCompileContext
33
+ from rememberstack.model import KnowledgePageCompileOutput
34
+ from rememberstack.model import KnowledgePageCompileRequest
35
+ from rememberstack.model import KnowledgeWriterBundle
36
+ from rememberstack.model import KnowledgeWriterSessionRequest
37
+ from rememberstack.model import KnowledgeWriterSessionResult
38
+ from rememberstack.model import KnowledgeWriterSuggestion
39
+ from rememberstack.model import ObjectKey
40
+ from rememberstack.model import UTCDateTime
41
+ from rememberstack.ports import MountPublisherPort
42
+ from rememberstack.ports import ObjectStorePort
43
+ from rememberstack.spine.knowledge import KnowledgeControlPlane
44
+ from rememberstack.workers.knowledge_fact_sheet import KnowledgeFactSheetCompiler
45
+
46
+ KNOWLEDGE_WRITER_VERSION: Final = "k-writer-2026.07"
47
+
48
+ _WRITER_PROMPT: Final = """You compile exactly one Plane-K prose band.
49
+ Your working directory is the declared output/ surface. Read ../INSTRUCTIONS.md and every
50
+ prepared input under ../bundle/ and ../context/. Memory mount locations are listed in
51
+ ../context/memory_mounts.json and are read-only. Do not use the
52
+ internet, initialize git, commit, or edit outside this temporary workspace.
53
+
54
+ Write only these declared files in the current directory (archived under output/):
55
+ - prose.md: non-empty prose band; do not reproduce the generated fact sheet.
56
+ - citations.json: JSON array of objects with role supports|contradicts|cites and exactly one
57
+ target: claim_lineage_id + claim_chunk_content_hash together, relation_id, or doc_id. Copy
58
+ stable claim coordinates and IDs exactly from available evidence.
59
+ - summary.md: a two- or three-sentence page summary for parent compilers.
60
+ - suggestions.json: JSON array of optional planner suggestions with action,
61
+ rationale, and payload. Use [] when there are none. Suggestions never take action.
62
+
63
+ Treat curation exclusions as binding. The driver will independently validate every file,
64
+ citation, link, count, and final content hash before anything can enter git.
65
+ """
66
+
67
+ _CITATIONS_ADAPTER = TypeAdapter(tuple[KnowledgeCitation, ...])
68
+ _SUGGESTIONS_ADAPTER = TypeAdapter(tuple[KnowledgeWriterSuggestion, ...])
69
+ _UTC_ADAPTER = TypeAdapter(UTCDateTime)
70
+
71
+
72
+ def _utc_now() -> datetime:
73
+ """Return the current aware UTC time for production compilation metadata."""
74
+ return datetime.now(tz=UTC)
75
+
76
+
77
+ class KnowledgeWriterSettings(BaseSettings):
78
+ """Settings-owned model, timeout, cap, and transcript-key policy."""
79
+
80
+ model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_K_WRITER_")
81
+
82
+ model: str = Field(min_length=1)
83
+ timeout_seconds: int = Field(gt=0)
84
+ residue_claim_limit: int = Field(ge=0)
85
+ evidence_claims_per_fact: int = Field(ge=0)
86
+ transcript_prefix: str = Field(min_length=1)
87
+
88
+ @field_validator("transcript_prefix")
89
+ @classmethod
90
+ def require_safe_transcript_prefix(cls, value: str) -> str:
91
+ """Keep immutable transcript objects under one normalized relative prefix."""
92
+ path = PurePosixPath(value)
93
+ if path.is_absolute() or ".." in path.parts or str(path) != value:
94
+ raise ValueError("transcript_prefix must be a normalized relative path")
95
+ return value.rstrip("/")
96
+
97
+
98
+ class KnowledgeWriterError(RuntimeError):
99
+ """A writer session failed before producing acceptable declared output."""
100
+
101
+
102
+ class KnowledgeWriterSession(Protocol):
103
+ """The narrow stock-harness seam used only by the Plane-K writer."""
104
+
105
+ def run_session(
106
+ self, *, request: KnowledgeWriterSessionRequest
107
+ ) -> "KnowledgeWriterSessionResult":
108
+ """Return raw declared files and a transcript without accepting output."""
109
+ ...
110
+
111
+
112
+ class KnowledgeProseCompiler:
113
+ """Compile one prose page from an exact capped bundle and generated fact sheet."""
114
+
115
+ def __init__(
116
+ self,
117
+ *,
118
+ control_plane: KnowledgeControlPlane,
119
+ writer_session: KnowledgeWriterSession,
120
+ transcript_store: ObjectStorePort,
121
+ mount_publisher: MountPublisherPort,
122
+ settings: KnowledgeWriterSettings,
123
+ clock: Callable[[], datetime] = _utc_now,
124
+ ) -> None:
125
+ """Bind authoritative hydration, stock harness, ledger store, and settings."""
126
+ self._control_plane = control_plane
127
+ self._writer_session = writer_session
128
+ self._transcript_store = transcript_store
129
+ self._mount_publisher = mount_publisher
130
+ self._settings = settings
131
+ self._clock = clock
132
+ self.writer_version = _writer_version(settings=settings)
133
+
134
+ def compile_page(
135
+ self, *, request: KnowledgePageCompileRequest
136
+ ) -> KnowledgePageCompileOutput:
137
+ """Run one writer, archive its transcript, then build validated two-band output."""
138
+ artifact = request.artifact
139
+ if artifact.artifact_kind == "fact_sheet":
140
+ raise KnowledgeWriterError(
141
+ "prose compiler cannot compile a fact-sheet-only page"
142
+ )
143
+ _validate_curation_input(request=request)
144
+ child_summary_hashes = tuple(
145
+ knowledge_summary_hash(summary=summary)
146
+ for summary in request.child_summaries.values()
147
+ )
148
+ shared_model_summary_hash = (
149
+ None
150
+ if request.shared_model_summary is None
151
+ else knowledge_summary_hash(summary=request.shared_model_summary)
152
+ )
153
+ hydrated = self._control_plane.writer_bundle(
154
+ artifact_id=artifact.artifact_id,
155
+ context=KnowledgeCompileContext(
156
+ curation_hash=request.curation_hash,
157
+ shared_model_summary_hash=shared_model_summary_hash,
158
+ writer_version=self.writer_version,
159
+ ),
160
+ child_summary_hashes=child_summary_hashes,
161
+ )
162
+ bundle = cap_knowledge_writer_bundle(
163
+ bundle=hydrated,
164
+ exclusions=request.exclusions,
165
+ residue_claim_limit=self._settings.residue_claim_limit,
166
+ evidence_claims_per_fact=self._settings.evidence_claims_per_fact,
167
+ )
168
+ compilation_id = uuid4()
169
+ transcript_uri: str | None = None
170
+ candidate_count = len(bundle.fact_sheet.facts) + len(bundle.claim_groups)
171
+ try:
172
+ mounts = self._mount_publisher.publish(deployment_id=artifact.deployment_id)
173
+ if mounts.deployment_id != artifact.deployment_id:
174
+ raise KnowledgeWriterError("mount publisher crossed deployments")
175
+ session_id = uuid4()
176
+ result = self._writer_session.run_session(
177
+ request=KnowledgeWriterSessionRequest(
178
+ session_id=session_id,
179
+ model=self._settings.model,
180
+ prompt=_WRITER_PROMPT,
181
+ timeout_seconds=self._settings.timeout_seconds,
182
+ input_files=_writer_inputs(
183
+ request=request,
184
+ bundle=bundle,
185
+ mounts_json=mounts.model_dump_json(),
186
+ ),
187
+ mounts=mounts,
188
+ )
189
+ )
190
+ if result.session_id != session_id:
191
+ raise KnowledgeWriterError("writer returned a different session ID")
192
+ transcript_uri = self._archive_transcript(
193
+ request=request, session_id=session_id, transcript=result.transcript
194
+ )
195
+ if result.timed_out:
196
+ raise KnowledgeWriterError("writer session timed out")
197
+ if result.exit_code != 0:
198
+ raise KnowledgeWriterError(
199
+ f"writer session exited with status {result.exit_code}"
200
+ )
201
+ prose, citations, summary, suggestions = _parse_writer_outputs(
202
+ files=result.output_files
203
+ )
204
+ citations = _unique_citations(citations=citations)
205
+ self._control_plane.validate_citations(
206
+ deployment_id=artifact.deployment_id, citations=citations
207
+ )
208
+ coverage = knowledge_writer_coverage(bundle=bundle, citations=citations)
209
+ rendered = render_knowledge_fact_sheet(
210
+ snapshot=bundle.fact_sheet,
211
+ compiled_at=_UTC_ADAPTER.validate_python(self._clock()),
212
+ citation_count=len(citations),
213
+ candidate_count=coverage.candidate_count,
214
+ )
215
+ markdown = compose_knowledge_page(
216
+ prose_markdown=prose, fact_sheet_markdown=rendered.markdown
217
+ )
218
+ return KnowledgePageCompileOutput(
219
+ compilation=KnowledgeCompilationWrite(
220
+ compilation_id=compilation_id,
221
+ deployment_id=artifact.deployment_id,
222
+ artifact_id=artifact.artifact_id,
223
+ inputs_hash=knowledge_inputs_hash(
224
+ snapshot=bundle.fact_sheet.input_snapshot
225
+ ),
226
+ candidate_count=coverage.candidate_count,
227
+ uncited_count=coverage.uncited_count,
228
+ claims_cut_count=bundle.claims_cut_count,
229
+ citations=citations,
230
+ suggestions=suggestions,
231
+ writer_version=self.writer_version,
232
+ page_summary=summary,
233
+ content_hash=knowledge_content_hash(markdown=markdown),
234
+ tokens=result.tokens,
235
+ cost_usd=result.cost_usd,
236
+ session_transcript_uri=transcript_uri,
237
+ ),
238
+ markdown=markdown,
239
+ )
240
+ except Exception as error:
241
+ failure_trace = traceback.format_exc()
242
+ try:
243
+ self._control_plane.record_failed_compilation(
244
+ failure=KnowledgeCompilationFailure(
245
+ compilation_id=compilation_id,
246
+ deployment_id=artifact.deployment_id,
247
+ artifact_id=artifact.artifact_id,
248
+ inputs_hash=knowledge_inputs_hash(
249
+ snapshot=bundle.fact_sheet.input_snapshot
250
+ ),
251
+ candidate_count=candidate_count,
252
+ claims_cut_count=bundle.claims_cut_count,
253
+ writer_version=self.writer_version,
254
+ failure=failure_trace,
255
+ session_transcript_uri=transcript_uri,
256
+ )
257
+ )
258
+ except Exception as ledger_error:
259
+ raise ExceptionGroup(
260
+ "writer compilation and failure-ledger recording both failed",
261
+ (error, ledger_error),
262
+ ) from None
263
+ raise
264
+
265
+ def _archive_transcript(
266
+ self, *, request: KnowledgePageCompileRequest, session_id: UUID, transcript: str
267
+ ) -> str:
268
+ """Persist the complete session ledger before interpreting any writer file."""
269
+ key = ObjectKey(
270
+ f"{self._settings.transcript_prefix}/"
271
+ f"{request.artifact.deployment_id}/{request.artifact.artifact_id}/"
272
+ f"{session_id}.json"
273
+ )
274
+ self._transcript_store.write_bytes(key=key, content=transcript.encode("utf-8"))
275
+ return key.root
276
+
277
+
278
+ class KnowledgePageCompilerRouter:
279
+ """Route fact-sheet-only pages away from the stock writer session entirely."""
280
+
281
+ def __init__(
282
+ self,
283
+ *,
284
+ fact_sheet_compiler: KnowledgeFactSheetCompiler,
285
+ prose_compiler: KnowledgeProseCompiler,
286
+ ) -> None:
287
+ """Bind the two page compilers behind the existing driver protocol."""
288
+ self._fact_sheet_compiler = fact_sheet_compiler
289
+ self._prose_compiler = prose_compiler
290
+
291
+ def compile_page(
292
+ self, *, request: KnowledgePageCompileRequest
293
+ ) -> KnowledgePageCompileOutput:
294
+ """Skip the writer exactly for artifacts designated as fact-sheet-only."""
295
+ if request.artifact.artifact_kind == "fact_sheet":
296
+ return self._fact_sheet_compiler.compile_page(request=request)
297
+ return self._prose_compiler.compile_page(request=request)
298
+
299
+
300
+ def _writer_inputs(
301
+ *,
302
+ request: KnowledgePageCompileRequest,
303
+ bundle: KnowledgeWriterBundle,
304
+ mounts_json: str,
305
+ ) -> dict[str, str]:
306
+ """Prepare the fixed workspace inputs accepted by the stock harness adapter."""
307
+ children = {
308
+ str(artifact_id): summary
309
+ for artifact_id, summary in sorted(
310
+ request.child_summaries.items(), key=lambda item: str(item[0])
311
+ )
312
+ }
313
+ return {
314
+ "INSTRUCTIONS.md": _WRITER_PROMPT,
315
+ "bundle/evidence.json": render_knowledge_writer_bundle(bundle=bundle),
316
+ "context/curation.md": request.curation_markdown or "_No curation guidance._\n",
317
+ "context/previous_page.md": request.previous_markdown or "_No prior page._\n",
318
+ "context/child_summaries.json": f"{json.dumps(children, sort_keys=True, indent=2)}\n",
319
+ "context/shared_model.md": request.shared_model_summary
320
+ or "_No shared model summary._\n",
321
+ "context/memory_mounts.json": f"{mounts_json}\n",
322
+ }
323
+
324
+
325
+ def _parse_writer_outputs(
326
+ *, files: dict[str, str]
327
+ ) -> tuple[
328
+ str, tuple[KnowledgeCitation, ...], str, tuple[KnowledgeWriterSuggestion, ...]
329
+ ]:
330
+ """Parse only declared writer files into the typed compiler contract."""
331
+ required = ("output/prose.md", "output/citations.json", "output/summary.md")
332
+ missing = tuple(path for path in required if path not in files)
333
+ if missing:
334
+ raise KnowledgeWriterError(
335
+ f"writer omitted required outputs: {', '.join(missing)}"
336
+ )
337
+ prose = files["output/prose.md"].strip()
338
+ summary = files["output/summary.md"].strip()
339
+ if not prose:
340
+ raise KnowledgeWriterError("writer prose output is empty")
341
+ if _sentence_count(text=summary) not in (2, 3):
342
+ raise KnowledgeWriterError("writer summary must contain two or three sentences")
343
+ try:
344
+ citations = _CITATIONS_ADAPTER.validate_json(files["output/citations.json"])
345
+ suggestions = _SUGGESTIONS_ADAPTER.validate_json(
346
+ files.get("output/suggestions.json", "[]")
347
+ )
348
+ except ValueError as error:
349
+ raise KnowledgeWriterError(
350
+ "writer JSON output violates the typed contract"
351
+ ) from error
352
+ return prose, citations, summary, suggestions
353
+
354
+
355
+ def _validate_curation_input(*, request: KnowledgePageCompileRequest) -> None:
356
+ """Bind the raw sidecar supplied to the harness to its D45 hash input."""
357
+ expected = (
358
+ None
359
+ if request.curation_markdown is None
360
+ else knowledge_content_hash(markdown=request.curation_markdown)
361
+ )
362
+ if request.curation_hash != expected:
363
+ raise KnowledgeWriterError("curation markdown does not match curation hash")
364
+
365
+
366
+ def _sentence_count(*, text: str) -> int:
367
+ """Count terminally punctuated summary sentences with one deterministic rule."""
368
+ return len(re.findall(r"[^.!?]+[.!?](?:\s|$)", text.strip()))
369
+
370
+
371
+ def _unique_citations(
372
+ *, citations: tuple[KnowledgeCitation, ...]
373
+ ) -> tuple[KnowledgeCitation, ...]:
374
+ """Apply deterministic set semantics to repeated writer citations."""
375
+ keyed = {
376
+ (
377
+ item.role.value,
378
+ str(item.claim_lineage_id),
379
+ item.claim_chunk_content_hash or "",
380
+ str(item.relation_id),
381
+ str(item.doc_id),
382
+ ): item
383
+ for item in citations
384
+ }
385
+ return tuple(keyed[key] for key in sorted(keyed))
386
+
387
+
388
+ def _writer_version(*, settings: KnowledgeWriterSettings) -> str:
389
+ """Make prompt, model, cap, and timeout settings hash-visible in D45 manifests."""
390
+ fingerprint = knowledge_summary_hash(
391
+ summary=f"{_WRITER_PROMPT}\n{settings.model_dump_json()}"
392
+ )
393
+ return f"{KNOWLEDGE_WRITER_VERSION}:{fingerprint[:16]}"
@@ -0,0 +1,42 @@
1
+ """Thin operational services that compose spine truth with delivery ports."""
2
+
3
+ from datetime import datetime
4
+ from uuid import UUID
5
+
6
+ from rememberstack.model import DeadLetterReplayResult
7
+ from rememberstack.model import ProcessingLane
8
+ from rememberstack.ports.queue import TaskQueuePort
9
+ from rememberstack.spine.work_ledger import WorkLedger
10
+
11
+
12
+ class DeadLetterReplayer:
13
+ """Replay one row through the ledger, then announce its committed route."""
14
+
15
+ def __init__(self, *, ledger: WorkLedger, queue: TaskQueuePort) -> None:
16
+ """Bind authoritative mutation and non-authoritative delivery."""
17
+ self._ledger = ledger
18
+ self._queue = queue
19
+
20
+ def replay(
21
+ self,
22
+ *,
23
+ deployment_id: UUID,
24
+ processing_id: UUID,
25
+ attempt_allowance: int = 1,
26
+ lane: ProcessingLane | None = None,
27
+ not_before: datetime | None = None,
28
+ ) -> DeadLetterReplayResult:
29
+ """Commit one explicit replay transition, then wake its returned route."""
30
+ replayed = self._ledger.replay_dead_letter(
31
+ deployment_id=deployment_id,
32
+ processing_id=processing_id,
33
+ attempt_allowance=attempt_allowance,
34
+ lane=lane,
35
+ not_before=not_before,
36
+ )
37
+ self._queue.announce(
38
+ processing_id=replayed.processing_id,
39
+ route_snapshot=replayed.route,
40
+ not_before_snapshot=replayed.not_before,
41
+ )
42
+ return replayed
@@ -0,0 +1,234 @@
1
+ """The P1 inline writers (D8, retrieval §5): the claims and facts channels.
2
+
3
+ The claims channel is the needle index — every accepted claim embedded with
4
+ its `is_current_testimony` scalar, so the DEFAULT claims search filters to
5
+ current testimony without touching Postgres. The facts channel carries the
6
+ human-readable labels of relations (generated once per label generation) and
7
+ observations (their statements), embedded beside their status scalar.
8
+ """
9
+
10
+ from typing import Final
11
+ from uuid import UUID
12
+
13
+ from pydantic import Field
14
+ from pydantic_settings import BaseSettings
15
+ from pydantic_settings import SettingsConfigDict
16
+
17
+ from rememberstack.model import ClaimedWork
18
+ from rememberstack.model import EmbeddingRequest
19
+ from rememberstack.model import FactLabelResponse
20
+ from rememberstack.model import ModelRequest
21
+ from rememberstack.model import NonRetryableHandlerError
22
+ from rememberstack.model import P1ClaimRow
23
+ from rememberstack.model import P1FactRow
24
+ from rememberstack.ports.cost_meter import CostMeterPort
25
+ from rememberstack.ports.model_provider import ModelProviderPort
26
+ from rememberstack.ports.p1_index import ClaimIndexPort
27
+ from rememberstack.ports.p1_index import FactIndexPort
28
+ from rememberstack.spine.chunk_catalog import ChunkCatalog
29
+ from rememberstack.spine.claim_catalog import ClaimCatalog
30
+ from rememberstack.spine.fact_catalog import FactCatalog
31
+ from rememberstack.workers.base import HandlerOutcome
32
+
33
+ P1_EMBED_CLAIMS_VERSION: Final = "p1-embed-claims-2026.07"
34
+ """The claim-embed stage's component version (the model rides settings)."""
35
+
36
+ FACT_LABEL_VERSION: Final = "p1-fact-label-2026.07"
37
+ """The fact-labeler prompt generation (regenerated only on version bump)."""
38
+
39
+ _FACT_LABEL_PROMPT: Final = (
40
+ "Write one short natural sentence stating this fact, nothing else: "
41
+ "{subject} —[{predicate}]→ {object}"
42
+ )
43
+
44
+
45
+ class P1Settings(BaseSettings):
46
+ """The P1 writer model bindings (D61/D63/D70)."""
47
+
48
+ model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_P1_")
49
+
50
+ embedding_model: str = Field(default="qwen/qwen3-embedding-8b")
51
+ label_model: str = Field(default="openai/gpt-5.6-luna")
52
+
53
+
54
+ class EmbedClaimsHandler:
55
+ """The claim-embed stage: one version's claims into the P1 claims channel."""
56
+
57
+ def __init__(
58
+ self,
59
+ *,
60
+ claim_catalog: ClaimCatalog,
61
+ chunk_catalog: ChunkCatalog,
62
+ model_provider: ModelProviderPort,
63
+ claim_index: ClaimIndexPort,
64
+ settings: P1Settings,
65
+ chunker_version: str,
66
+ ) -> None:
67
+ """Bind the handler to its catalogs, provider, index, and generation."""
68
+ self._claim_catalog = claim_catalog
69
+ self._chunk_catalog = chunk_catalog
70
+ self._model_provider = model_provider
71
+ self._claim_index = claim_index
72
+ self._settings = settings
73
+ self._chunker_version = chunker_version
74
+
75
+ def handle(self, *, work: ClaimedWork, meter: CostMeterPort) -> HandlerOutcome:
76
+ """Embed the version's not-yet-embedded claims as one document batch."""
77
+ source = self._chunk_catalog.chunk_source(
78
+ representation_id=_payload_uuid(work=work, field="representation_id")
79
+ )
80
+ chunks = self._chunk_catalog.chunks_for_embedding(
81
+ representation_id=source.representation_id,
82
+ chunker_version=self._chunker_version,
83
+ )
84
+ claims = self._claim_catalog.claims_for_embedding(
85
+ chunk_ids=tuple(chunk.chunk_id for chunk in chunks),
86
+ embedding_version=self._settings.embedding_model,
87
+ )
88
+ if not claims:
89
+ return HandlerOutcome() # replay: refs already stamped (D7)
90
+ response = self._model_provider.embed(
91
+ request=EmbeddingRequest(
92
+ model=self._settings.embedding_model,
93
+ texts=tuple(claim.claim_text for claim in claims),
94
+ )
95
+ )
96
+ meter.record(call_key="embed_claims", tier="embedding", usage=response.usage)
97
+ self._claim_index.upsert_claims(
98
+ rows=tuple(
99
+ P1ClaimRow(
100
+ claim_id=claim.claim_id,
101
+ deployment_id=work.deployment_id,
102
+ doc_id=claim.doc_id,
103
+ chunk_id=claim.chunk_id,
104
+ text=claim.claim_text,
105
+ is_current_testimony=claim.is_current_testimony,
106
+ is_attributed=claim.is_attributed,
107
+ vector=vector,
108
+ )
109
+ for claim, vector in zip(claims, response.vectors, strict=True)
110
+ )
111
+ )
112
+ self._claim_catalog.record_claim_embeddings(
113
+ claim_ids=tuple(claim.claim_id for claim in claims),
114
+ embedding_version=self._settings.embedding_model,
115
+ )
116
+ return HandlerOutcome()
117
+
118
+
119
+ class LabelFactsHandler:
120
+ """The fact-label stage: readable labels for relations, embedded with
121
+ observation statements into the P1 facts channel (D8)."""
122
+
123
+ def __init__(
124
+ self,
125
+ *,
126
+ facts: FactCatalog,
127
+ model_provider: ModelProviderPort,
128
+ fact_index: FactIndexPort,
129
+ settings: P1Settings,
130
+ ) -> None:
131
+ """Bind the handler to the fact catalog, provider, and facts index."""
132
+ self._facts = facts
133
+ self._model_provider = model_provider
134
+ self._fact_index = fact_index
135
+ self._settings = settings
136
+
137
+ def handle(self, *, work: ClaimedWork, meter: CostMeterPort) -> HandlerOutcome:
138
+ """Label and embed the document's facts still lacking this generation.
139
+
140
+ Ordering is the invariant (Codex review): the index write lands
141
+ BEFORE any PG stamp, so Postgres never advertises a Lance ref that
142
+ was not written — a crash mid-pass re-labels the remainder on retry.
143
+ The generation stamp folds in the embedding model (D63): a model
144
+ change re-labels and re-embeds instead of silently skipping.
145
+ Concurrent document jobs serialize on the deployment label lock.
146
+ """
147
+ doc_id = _payload_uuid(work=work, field="doc_id")
148
+ generation = f"{FACT_LABEL_VERSION}+{self._settings.embedding_model}"
149
+ with self._facts.label_lock(deployment_id=work.deployment_id):
150
+ rows: list[P1FactRow] = []
151
+ for relation in self._facts.relations_for_labeling(
152
+ deployment_id=work.deployment_id,
153
+ doc_id=doc_id,
154
+ label_version=generation,
155
+ ):
156
+ label_call = self._model_provider.generate(
157
+ request=ModelRequest(
158
+ model=self._settings.label_model,
159
+ prompt=_FACT_LABEL_PROMPT.format(
160
+ subject=relation.subject_name,
161
+ predicate=relation.predicate,
162
+ object=relation.object_name,
163
+ ),
164
+ ),
165
+ response_type=FactLabelResponse,
166
+ )
167
+ meter.record(
168
+ call_key=f"label_relation:{relation.relation_id}",
169
+ tier="label",
170
+ usage=label_call.usage,
171
+ )
172
+ label = label_call.output.label
173
+ rows.append(
174
+ P1FactRow(
175
+ fact_id=relation.relation_id,
176
+ deployment_id=work.deployment_id,
177
+ kind="relation",
178
+ label=label,
179
+ status=relation.status,
180
+ vector=(0.0,), # replaced below by the batch embedding
181
+ )
182
+ )
183
+ rows.extend(
184
+ P1FactRow(
185
+ fact_id=observation.observation_id,
186
+ deployment_id=work.deployment_id,
187
+ kind="observation",
188
+ label=observation.obs_label,
189
+ status=observation.status,
190
+ vector=(0.0,),
191
+ )
192
+ for observation in self._facts.observations_for_embedding(
193
+ deployment_id=work.deployment_id,
194
+ doc_id=doc_id,
195
+ label_version=generation,
196
+ )
197
+ )
198
+ if not rows:
199
+ return HandlerOutcome()
200
+ response = self._model_provider.embed(
201
+ request=EmbeddingRequest(
202
+ model=self._settings.embedding_model,
203
+ texts=tuple(row.label for row in rows),
204
+ )
205
+ )
206
+ meter.record(call_key="embed_facts", tier="embedding", usage=response.usage)
207
+ self._fact_index.upsert_facts(
208
+ rows=tuple(
209
+ row.model_copy(update={"vector": vector})
210
+ for row, vector in zip(rows, response.vectors, strict=True)
211
+ )
212
+ )
213
+ for row in rows: # stamps only after the index write landed
214
+ if row.kind == "relation":
215
+ self._facts.record_fact_label(
216
+ relation_id=row.fact_id,
217
+ label=row.label,
218
+ label_version=generation,
219
+ )
220
+ else:
221
+ self._facts.record_observation_embedding(
222
+ observation_id=row.fact_id, label_version=generation
223
+ )
224
+ return HandlerOutcome()
225
+
226
+
227
+ def _payload_uuid(*, work: ClaimedWork, field: str) -> UUID:
228
+ """Read a required UUID from the claimed payload; absence is non-retryable."""
229
+ value = (work.payload or {}).get(field)
230
+ if not isinstance(value, str):
231
+ raise NonRetryableHandlerError(
232
+ f"stage {work.stage} work {work.processing_id} carries no {field!r} payload"
233
+ )
234
+ return UUID(value)