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,325 @@
1
+ """Typed WP-6.6 authored-page declarations, review flags, and dispatches."""
2
+
3
+ from enum import StrEnum
4
+ from pathlib import PurePosixPath
5
+ from uuid import UUID
6
+
7
+ from pydantic import BaseModel
8
+ from pydantic import ConfigDict
9
+ from pydantic import Field
10
+ from pydantic import field_validator
11
+
12
+ from rememberstack.model.knowledge import KnowledgeCitation
13
+ from rememberstack.model.knowledge import KnowledgeEvidenceDelta
14
+ from rememberstack.model.knowledge import KnowledgeLayer
15
+ from rememberstack.model.knowledge import KnowledgePageKind
16
+ from rememberstack.model.knowledge import KnowledgeRuleParams
17
+
18
+
19
+ def _safe_markdown_path(*, value: str, field: str) -> str:
20
+ """Require one normalized repository-relative Markdown path."""
21
+ path = PurePosixPath(value)
22
+ if (
23
+ not value
24
+ or path.is_absolute()
25
+ or ".." in path.parts
26
+ or str(path) != value
27
+ or path.suffix != ".md"
28
+ ):
29
+ raise ValueError(f"{field} must be a normalized relative Markdown path")
30
+ return value
31
+
32
+
33
+ def _citation_key(*, citation: KnowledgeCitation) -> tuple[str, ...]:
34
+ """Return one stable total-order key for a citation declaration."""
35
+ return (
36
+ citation.role.value,
37
+ "" if citation.claim_lineage_id is None else str(citation.claim_lineage_id),
38
+ citation.claim_chunk_content_hash or "",
39
+ "" if citation.relation_id is None else str(citation.relation_id),
40
+ "" if citation.doc_id is None else str(citation.doc_id),
41
+ )
42
+
43
+
44
+ def _rule_key(*, rule: KnowledgeRuleParams) -> str:
45
+ """Return one stable serialization key for a typed watch rule."""
46
+ return rule.model_dump_json(exclude_none=True)
47
+
48
+
49
+ class KnowledgeAuthoredReviewReason(StrEnum):
50
+ """Why an authored owner needs attention without an automatic rewrite."""
51
+
52
+ EVIDENCE_CHANGED = "evidence_changed"
53
+ PAGE_RECOMPILED = "page_recompiled"
54
+ DECLARATION_MISSING = "declaration_missing"
55
+ TOMBSTONE = "tombstone"
56
+
57
+
58
+ class KnowledgeSubscriptionStatus(StrEnum):
59
+ """Lifecycle states of one registered workflow subscriber."""
60
+
61
+ ACTIVE = "active"
62
+ PAUSED = "paused"
63
+ RETIRED = "retired"
64
+
65
+
66
+ class KnowledgeDispatchStatus(StrEnum):
67
+ """Delivery mirror stored beside one append-only dispatch payload."""
68
+
69
+ PENDING = "pending"
70
+ RUNNING = "running"
71
+ DONE = "done"
72
+ FAILED = "failed"
73
+
74
+
75
+ class KnowledgeAuthoredDeclaration(BaseModel):
76
+ """The optional ``cites`` and ``watch`` declarations in one authored page."""
77
+
78
+ model_config = ConfigDict(frozen=True, extra="forbid")
79
+
80
+ citations: tuple[KnowledgeCitation, ...] | None = None
81
+ watch_rules: tuple[KnowledgeRuleParams, ...] | None = None
82
+ watched_page_paths: tuple[str, ...] | None = None
83
+
84
+ @field_validator("citations")
85
+ @classmethod
86
+ def canonicalize_citations(
87
+ cls, value: tuple[KnowledgeCitation, ...] | None
88
+ ) -> tuple[KnowledgeCitation, ...] | None:
89
+ """Deduplicate explicit citation coordinates deterministically."""
90
+ if value is None:
91
+ return None
92
+ return tuple(sorted(set(value), key=lambda item: _citation_key(citation=item)))
93
+
94
+ @field_validator("watch_rules")
95
+ @classmethod
96
+ def canonicalize_rules(
97
+ cls, value: tuple[KnowledgeRuleParams, ...] | None
98
+ ) -> tuple[KnowledgeRuleParams, ...] | None:
99
+ """Deduplicate watch rules by their canonical typed representation."""
100
+ if value is None:
101
+ return None
102
+ unique = {_rule_key(rule=item): item for item in value}
103
+ return tuple(unique[key] for key in sorted(unique))
104
+
105
+ @field_validator("watched_page_paths")
106
+ @classmethod
107
+ def canonicalize_page_paths(
108
+ cls, value: tuple[str, ...] | None
109
+ ) -> tuple[str, ...] | None:
110
+ """Normalize and deduplicate page-watch paths."""
111
+ if value is None:
112
+ return None
113
+ return tuple(
114
+ sorted(
115
+ {
116
+ _safe_markdown_path(value=path, field="watched_page_paths")
117
+ for path in value
118
+ }
119
+ )
120
+ )
121
+
122
+
123
+ class KnowledgeAuthoredPageSync(BaseModel):
124
+ """One authored Markdown file observed at an exact git revision."""
125
+
126
+ model_config = ConfigDict(frozen=True, extra="forbid")
127
+
128
+ deployment_id: UUID
129
+ git_path: str
130
+ markdown: str
131
+ content_hash: str = Field(pattern=r"^[0-9a-f]{64}$")
132
+ git_revision: str = Field(min_length=1)
133
+ declaration: KnowledgeAuthoredDeclaration
134
+ layer: KnowledgeLayer = KnowledgeLayer.K1
135
+
136
+ @field_validator("git_path")
137
+ @classmethod
138
+ def require_git_path(cls, value: str) -> str:
139
+ """Keep authored files inside the repository Markdown namespace."""
140
+ return _safe_markdown_path(value=value, field="git_path")
141
+
142
+
143
+ class KnowledgeAuthoredSyncResult(BaseModel):
144
+ """What one checkout-to-Postgres authored synchronization changed."""
145
+
146
+ model_config = ConfigDict(frozen=True, extra="forbid")
147
+
148
+ registered_artifact_ids: tuple[UUID, ...] = ()
149
+ synced_artifact_ids: tuple[UUID, ...] = ()
150
+ lint_flag_artifact_ids: tuple[UUID, ...] = ()
151
+
152
+
153
+ class KnowledgeAuthoredPageSyncResult(BaseModel):
154
+ """The atomic database outcome for one authored Markdown file."""
155
+
156
+ model_config = ConfigDict(frozen=True, extra="forbid")
157
+
158
+ artifact_id: UUID
159
+ registered: bool = False
160
+ content_changed: bool = False
161
+ lint_flagged: bool = False
162
+
163
+
164
+ class KnowledgeSubscriptionCreate(BaseModel):
165
+ """One workflow endpoint and its mechanically evaluated interests."""
166
+
167
+ model_config = ConfigDict(frozen=True, extra="forbid")
168
+
169
+ subscription_id: UUID
170
+ deployment_id: UUID
171
+ name: str = Field(min_length=1)
172
+ workflow_endpoint: str = Field(min_length=1)
173
+ debounce_seconds: int = Field(gt=0)
174
+ scope_id: UUID | None = None
175
+ created_by: str = Field(min_length=1)
176
+ rules: tuple[KnowledgeRuleParams, ...] = ()
177
+ watched_page_paths: tuple[str, ...] = ()
178
+
179
+ @field_validator("rules")
180
+ @classmethod
181
+ def canonicalize_subscription_rules(
182
+ cls, value: tuple[KnowledgeRuleParams, ...]
183
+ ) -> tuple[KnowledgeRuleParams, ...]:
184
+ """Deduplicate subscription rules deterministically."""
185
+ unique = {_rule_key(rule=item): item for item in value}
186
+ return tuple(unique[key] for key in sorted(unique))
187
+
188
+ @field_validator("watched_page_paths")
189
+ @classmethod
190
+ def canonicalize_subscription_paths(cls, value: tuple[str, ...]) -> tuple[str, ...]:
191
+ """Normalize subscription page-watch paths."""
192
+ return tuple(
193
+ sorted(
194
+ {
195
+ _safe_markdown_path(value=path, field="watched_page_paths")
196
+ for path in value
197
+ }
198
+ )
199
+ )
200
+
201
+
202
+ class KnowledgeAuthoredReviewPayload(BaseModel):
203
+ """A mergeable delta carried by a standing authored-review flag or dispatch."""
204
+
205
+ model_config = ConfigDict(frozen=True, extra="forbid")
206
+
207
+ reasons: tuple[KnowledgeAuthoredReviewReason, ...] = Field(min_length=1)
208
+ delta: KnowledgeEvidenceDelta = Field(default_factory=KnowledgeEvidenceDelta)
209
+ page_refs: tuple[str, ...] = ()
210
+ citations_added: tuple[str, ...] = ()
211
+ citations_removed: tuple[str, ...] = ()
212
+ evidence_invalidated: int = Field(default=0, ge=0)
213
+ redaction_required: bool = False
214
+
215
+ @field_validator("reasons")
216
+ @classmethod
217
+ def canonicalize_reasons(
218
+ cls, value: tuple[KnowledgeAuthoredReviewReason, ...]
219
+ ) -> tuple[KnowledgeAuthoredReviewReason, ...]:
220
+ """Keep merged flag reasons set-valued and deterministic."""
221
+ return tuple(sorted(set(value), key=lambda item: item.value))
222
+
223
+ @field_validator("page_refs", "citations_added", "citations_removed")
224
+ @classmethod
225
+ def canonicalize_strings(cls, value: tuple[str, ...]) -> tuple[str, ...]:
226
+ """Keep payload identifier collections deterministic."""
227
+ return tuple(sorted(set(value)))
228
+
229
+
230
+ class KnowledgeNotificationResult(BaseModel):
231
+ """IDs of authored flags and subscription batches affected by one route."""
232
+
233
+ model_config = ConfigDict(frozen=True, extra="forbid")
234
+
235
+ authored_artifact_ids: tuple[UUID, ...] = ()
236
+ dispatch_ids: tuple[UUID, ...] = ()
237
+
238
+
239
+ class KnowledgeAuthoredReviewState(BaseModel):
240
+ """Reader-facing warning state for one authored page."""
241
+
242
+ model_config = ConfigDict(frozen=True, extra="forbid")
243
+
244
+ artifact_id: UUID
245
+ open_flag_count: int = Field(ge=0)
246
+ redaction_required: bool = False
247
+ payloads: tuple[KnowledgeAuthoredReviewPayload, ...] = ()
248
+
249
+
250
+ class KnowledgeDispatchRecord(BaseModel):
251
+ """One claimed subscriber delivery read from the append-only dispatch ledger."""
252
+
253
+ model_config = ConfigDict(frozen=True, extra="forbid")
254
+
255
+ dispatch_id: UUID
256
+ deployment_id: UUID
257
+ subscription_id: UUID
258
+ workflow_endpoint: str
259
+ payload: KnowledgeAuthoredReviewPayload
260
+ status: KnowledgeDispatchStatus
261
+
262
+
263
+ class KnowledgeDispatchMaterialization(BaseModel):
264
+ """One due dispatch and its idempotent D67 processing row."""
265
+
266
+ model_config = ConfigDict(frozen=True, extra="forbid")
267
+
268
+ dispatch_id: UUID
269
+ processing_id: UUID
270
+ created: bool
271
+
272
+
273
+ class KnowledgeArtifactPathState(BaseModel):
274
+ """Git path ownership needed by checkout synchronization."""
275
+
276
+ model_config = ConfigDict(frozen=True, extra="forbid")
277
+
278
+ artifact_id: UUID
279
+ git_path: str
280
+ page_kind: KnowledgePageKind
281
+ curation_path: str | None = None
282
+
283
+
284
+ class KnowledgeWorkflowDelivery(BaseModel):
285
+ """The idempotent delta-carrying call made to an external workflow endpoint."""
286
+
287
+ model_config = ConfigDict(frozen=True, extra="forbid")
288
+
289
+ dispatch_id: UUID
290
+ workflow_endpoint: str
291
+ payload: KnowledgeAuthoredReviewPayload
292
+
293
+
294
+ def merge_knowledge_deltas(
295
+ *, left: KnowledgeEvidenceDelta, right: KnowledgeEvidenceDelta
296
+ ) -> KnowledgeEvidenceDelta:
297
+ """Union two evidence deltas without losing their typed grains."""
298
+ return KnowledgeEvidenceDelta(
299
+ relation_ids=tuple(
300
+ sorted(set(left.relation_ids).union(right.relation_ids), key=str)
301
+ ),
302
+ observation_ids=tuple(
303
+ sorted(set(left.observation_ids).union(right.observation_ids), key=str)
304
+ ),
305
+ claim_ids=tuple(sorted(set(left.claim_ids).union(right.claim_ids), key=str)),
306
+ doc_ids=tuple(sorted(set(left.doc_ids).union(right.doc_ids), key=str)),
307
+ community_ids=tuple(
308
+ sorted(set(left.community_ids).union(right.community_ids), key=str)
309
+ ),
310
+ )
311
+
312
+
313
+ def merge_authored_review_payloads(
314
+ *, left: KnowledgeAuthoredReviewPayload, right: KnowledgeAuthoredReviewPayload
315
+ ) -> KnowledgeAuthoredReviewPayload:
316
+ """Coalesce two notification payloads into one deterministic debounce batch."""
317
+ return KnowledgeAuthoredReviewPayload(
318
+ reasons=(*left.reasons, *right.reasons),
319
+ delta=merge_knowledge_deltas(left=left.delta, right=right.delta),
320
+ page_refs=(*left.page_refs, *right.page_refs),
321
+ citations_added=(*left.citations_added, *right.citations_added),
322
+ citations_removed=(*left.citations_removed, *right.citations_removed),
323
+ evidence_invalidated=left.evidence_invalidated + right.evidence_invalidated,
324
+ redaction_required=left.redaction_required or right.redaction_required,
325
+ )