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.
- rememberstack/__init__.py +9 -0
- rememberstack/adapters/__init__.py +42 -0
- rememberstack/adapters/codex_writer.py +221 -0
- rememberstack/adapters/markitdown_converter.py +42 -0
- rememberstack/adapters/openrouter.py +136 -0
- rememberstack/adapters/selfhost/__init__.py +54 -0
- rememberstack/adapters/selfhost/forget.py +66 -0
- rememberstack/adapters/selfhost/git.py +374 -0
- rememberstack/adapters/selfhost/lance.py +328 -0
- rememberstack/adapters/selfhost/minio.py +279 -0
- rememberstack/adapters/selfhost/mounts.py +249 -0
- rememberstack/adapters/selfhost/object_store.py +130 -0
- rememberstack/adapters/selfhost/projection.py +80 -0
- rememberstack/adapters/selfhost/queue.py +137 -0
- rememberstack/adapters/selfhost/telemetry.py +45 -0
- rememberstack/adapters/selfhost/watcher.py +70 -0
- rememberstack/adapters/testing/__init__.py +15 -0
- rememberstack/adapters/testing/cost_meter.py +13 -0
- rememberstack/adapters/testing/model_provider.py +83 -0
- rememberstack/adapters/testing/queue.py +43 -0
- rememberstack/adapters/testing/telemetry.py +22 -0
- rememberstack/client.py +19 -0
- rememberstack/core/__init__.py +127 -0
- rememberstack/core/blockizer.py +189 -0
- rememberstack/core/chunker.py +216 -0
- rememberstack/core/consumption_skill.py +275 -0
- rememberstack/core/conversion.py +76 -0
- rememberstack/core/core_manifest.py +598 -0
- rememberstack/core/extension_packs.py +124 -0
- rememberstack/core/forget.py +17 -0
- rememberstack/core/knowledge_authored.py +276 -0
- rememberstack/core/knowledge_compile.py +215 -0
- rememberstack/core/knowledge_fact_sheet.py +210 -0
- rememberstack/core/knowledge_hashing.py +68 -0
- rememberstack/core/knowledge_planner.py +64 -0
- rememberstack/core/knowledge_writer.py +175 -0
- rememberstack/core/ranking.py +200 -0
- rememberstack/core/recipe_linter.py +149 -0
- rememberstack/core/section_snap.py +209 -0
- rememberstack/core/storage_routing.py +27 -0
- rememberstack/eval/__init__.py +53 -0
- rememberstack/eval/consumption.py +141 -0
- rememberstack/eval/contradiction.py +184 -0
- rememberstack/eval/harness.py +136 -0
- rememberstack/eval/lifecycle.py +400 -0
- rememberstack/eval/operational_scale.py +49 -0
- rememberstack/eval/resolution.py +255 -0
- rememberstack/eval/retrieval_spikes.py +50 -0
- rememberstack/eval/skeleton.py +231 -0
- rememberstack/llm/__init__.py +1 -0
- rememberstack/model/__init__.py +589 -0
- rememberstack/model/adjudication.py +100 -0
- rememberstack/model/auth.py +27 -0
- rememberstack/model/blocks.py +30 -0
- rememberstack/model/chunks.py +190 -0
- rememberstack/model/claims.py +162 -0
- rememberstack/model/client.py +98 -0
- rememberstack/model/clustering.py +54 -0
- rememberstack/model/component_version.py +124 -0
- rememberstack/model/consumption.py +88 -0
- rememberstack/model/conversion.py +31 -0
- rememberstack/model/deployment.py +53 -0
- rememberstack/model/documents.py +168 -0
- rememberstack/model/envelope.py +513 -0
- rememberstack/model/evaluation.py +72 -0
- rememberstack/model/forget.py +143 -0
- rememberstack/model/git.py +13 -0
- rememberstack/model/knowledge.py +840 -0
- rememberstack/model/knowledge_authored.py +325 -0
- rememberstack/model/knowledge_planner.py +431 -0
- rememberstack/model/lifecycle.py +42 -0
- rememberstack/model/model_provider.py +78 -0
- rememberstack/model/mounts.py +24 -0
- rememberstack/model/object_store.py +21 -0
- rememberstack/model/operational_scale.py +59 -0
- rememberstack/model/operations.py +153 -0
- rememberstack/model/processing.py +228 -0
- rememberstack/model/queue.py +73 -0
- rememberstack/model/recipes.py +83 -0
- rememberstack/model/relations.py +79 -0
- rememberstack/model/resolution.py +83 -0
- rememberstack/model/retrieval_spikes.py +62 -0
- rememberstack/model/sections.py +120 -0
- rememberstack/model/telemetry.py +30 -0
- rememberstack/ports/__init__.py +29 -0
- rememberstack/ports/auth.py +16 -0
- rememberstack/ports/connector.py +23 -0
- rememberstack/ports/cost_meter.py +17 -0
- rememberstack/ports/forget.py +20 -0
- rememberstack/ports/git.py +20 -0
- rememberstack/ports/model_provider.py +28 -0
- rememberstack/ports/mounts.py +16 -0
- rememberstack/ports/object_store.py +27 -0
- rememberstack/ports/p1_index.py +92 -0
- rememberstack/ports/purge.py +93 -0
- rememberstack/ports/queue.py +23 -0
- rememberstack/ports/telemetry.py +21 -0
- rememberstack/profiles/__init__.py +22 -0
- rememberstack/profiles/selfhost.py +324 -0
- rememberstack/profiles/selfhost_forget.py +158 -0
- rememberstack/profiles/selfhost_operations.py +95 -0
- rememberstack/py.typed +1 -0
- rememberstack/spine/__init__.py +93 -0
- rememberstack/spine/admission.py +26 -0
- rememberstack/spine/backfill.py +168 -0
- rememberstack/spine/catalog_contract.py +742 -0
- rememberstack/spine/chunk_catalog.py +237 -0
- rememberstack/spine/claim_catalog.py +298 -0
- rememberstack/spine/clustering.py +740 -0
- rememberstack/spine/component_versions.py +208 -0
- rememberstack/spine/consumption.py +81 -0
- rememberstack/spine/deployment_bootstrap.py +445 -0
- rememberstack/spine/document_catalog.py +621 -0
- rememberstack/spine/entity_registry.py +205 -0
- rememberstack/spine/extension_packs.py +220 -0
- rememberstack/spine/fact_catalog.py +571 -0
- rememberstack/spine/forget.py +1753 -0
- rememberstack/spine/knowledge.py +5467 -0
- rememberstack/spine/lifecycle.py +1071 -0
- rememberstack/spine/migrations/__init__.py +1 -0
- rememberstack/spine/migrations/_helpers.py +153 -0
- rememberstack/spine/migrations/env.py +58 -0
- rememberstack/spine/migrations/script.py.mako +27 -0
- rememberstack/spine/migrations/versions/__init__.py +1 -0
- rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
- rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
- rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
- rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
- rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
- rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
- rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
- rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
- rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
- rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
- rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
- rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
- rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
- rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
- rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
- rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
- rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
- rememberstack/spine/observation_adjudication.py +778 -0
- rememberstack/spine/operations.py +298 -0
- rememberstack/spine/projection.py +662 -0
- rememberstack/spine/recipes.py +276 -0
- rememberstack/spine/resolver.py +763 -0
- rememberstack/spine/review.py +650 -0
- rememberstack/spine/settings.py +22 -0
- rememberstack/spine/supersession.py +510 -0
- rememberstack/spine/sync.py +128 -0
- rememberstack/spine/work_ledger.py +816 -0
- rememberstack/surfaces/__init__.py +110 -0
- rememberstack/surfaces/cli.py +447 -0
- rememberstack/surfaces/consumption_skill.py +87 -0
- rememberstack/surfaces/graph_queries.py +698 -0
- rememberstack/surfaces/http_api.py +377 -0
- rememberstack/surfaces/mcp.py +67 -0
- rememberstack/surfaces/query_engine.py +1591 -0
- rememberstack/surfaces/recipe_executor.py +185 -0
- rememberstack/surfaces/recipe_surface.py +219 -0
- rememberstack/surfaces/remote_mcp.py +133 -0
- rememberstack/surfaces/sdk.py +324 -0
- rememberstack/workers/__init__.py +155 -0
- rememberstack/workers/base.py +312 -0
- rememberstack/workers/e0.py +577 -0
- rememberstack/workers/e1.py +425 -0
- rememberstack/workers/e2.py +525 -0
- rememberstack/workers/e3.py +434 -0
- rememberstack/workers/forget.py +299 -0
- rememberstack/workers/knowledge_authored.py +146 -0
- rememberstack/workers/knowledge_driver.py +735 -0
- rememberstack/workers/knowledge_fact_sheet.py +123 -0
- rememberstack/workers/knowledge_planner.py +325 -0
- rememberstack/workers/knowledge_writer.py +393 -0
- rememberstack/workers/operations.py +42 -0
- rememberstack/workers/p1.py +234 -0
- rememberstack/workers/p2.py +513 -0
- rememberstack/workers/p2_analytics.py +276 -0
- rememberstack/workers/p3.py +673 -0
- rememberstack/workers/reconcile.py +485 -0
- rememberstack/workers/sync.py +168 -0
- rememberstack-0.1.0.dist-info/METADATA +213 -0
- rememberstack-0.1.0.dist-info/RECORD +186 -0
- rememberstack-0.1.0.dist-info/WHEEL +4 -0
- rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
- rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
"""Deterministic Plane-K routing, scheduling, and single-commit driver."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from concurrent.futures import Future
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from pathlib import PurePosixPath
|
|
9
|
+
from tempfile import TemporaryDirectory
|
|
10
|
+
import traceback
|
|
11
|
+
from typing import Final
|
|
12
|
+
from typing import Protocol
|
|
13
|
+
from uuid import UUID
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
from pydantic import Field
|
|
17
|
+
from pydantic_settings import BaseSettings
|
|
18
|
+
from pydantic_settings import SettingsConfigDict
|
|
19
|
+
|
|
20
|
+
from rememberstack.core import knowledge_compile_order
|
|
21
|
+
from rememberstack.core import knowledge_content_hash
|
|
22
|
+
from rememberstack.core import validate_knowledge_page_output
|
|
23
|
+
from rememberstack.model import KnowledgeCommitCycleResult
|
|
24
|
+
from rememberstack.model import KnowledgeCompilationFailure
|
|
25
|
+
from rememberstack.model import KnowledgeCompileArtifact
|
|
26
|
+
from rememberstack.model import KnowledgeCompileContext
|
|
27
|
+
from rememberstack.model import KnowledgeConvertKindProposal
|
|
28
|
+
from rememberstack.model import KnowledgeEvidenceDelta
|
|
29
|
+
from rememberstack.model import KnowledgeEvidenceTarget
|
|
30
|
+
from rememberstack.model import KnowledgeMergePagesProposal
|
|
31
|
+
from rememberstack.model import KnowledgeMovePageProposal
|
|
32
|
+
from rememberstack.model import KnowledgePageCompileOutput
|
|
33
|
+
from rememberstack.model import KnowledgePageCompileRequest
|
|
34
|
+
from rememberstack.model import KnowledgePageKind
|
|
35
|
+
from rememberstack.model import KnowledgePendingCycle
|
|
36
|
+
from rememberstack.model import KnowledgePendingPlanDecision
|
|
37
|
+
from rememberstack.model import KnowledgeRetirePageProposal
|
|
38
|
+
from rememberstack.ports import KGitRemotePort
|
|
39
|
+
from rememberstack.spine.knowledge import KnowledgeControlPlane
|
|
40
|
+
from rememberstack.workers.knowledge_authored import KnowledgeAuthoredSynchronizer
|
|
41
|
+
|
|
42
|
+
KNOWLEDGE_DRIVER_VERSION: Final = "k-driver-2026.07"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class KnowledgeCommitSettings(BaseSettings):
|
|
46
|
+
"""Settings-owned bound on concurrent disjoint page compilers."""
|
|
47
|
+
|
|
48
|
+
model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_K_DRIVER_")
|
|
49
|
+
|
|
50
|
+
max_parallel_pages: int = Field(gt=0)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class KnowledgePageCompiler(Protocol):
|
|
54
|
+
"""The future WP-6.3/6.4 page compiler consumed by this mechanical loop."""
|
|
55
|
+
|
|
56
|
+
def compile_page(
|
|
57
|
+
self, *, request: KnowledgePageCompileRequest
|
|
58
|
+
) -> KnowledgePageCompileOutput:
|
|
59
|
+
"""Return one structured page without publishing or mutating another page."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class KnowledgeRoutingDriver:
|
|
64
|
+
"""Route an evidence delta, recompute manifests, and mark exact stale pages."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, *, control_plane: KnowledgeControlPlane) -> None:
|
|
67
|
+
"""Bind the deterministic driver to its Postgres control plane."""
|
|
68
|
+
self._control_plane = control_plane
|
|
69
|
+
|
|
70
|
+
def route_and_mark_stale(
|
|
71
|
+
self,
|
|
72
|
+
*,
|
|
73
|
+
deployment_id: UUID,
|
|
74
|
+
delta: KnowledgeEvidenceDelta,
|
|
75
|
+
contexts: Mapping[UUID, KnowledgeCompileContext],
|
|
76
|
+
tombstone: bool = False,
|
|
77
|
+
) -> tuple[UUID, ...]:
|
|
78
|
+
"""Narrow by keys/citations, then mark only manifest mismatches.
|
|
79
|
+
|
|
80
|
+
Routing may intentionally over-select because four inverted key kinds
|
|
81
|
+
cannot encode every rule parameter. The complete manifest comparison
|
|
82
|
+
is the correctness gate, so a coarse match can never fabricate stale
|
|
83
|
+
state.
|
|
84
|
+
"""
|
|
85
|
+
routed = self._control_plane.route_delta(
|
|
86
|
+
deployment_id=deployment_id, delta=delta
|
|
87
|
+
)
|
|
88
|
+
stale = self._control_plane.stale_artifacts(
|
|
89
|
+
deployment_id=deployment_id, contexts=contexts, artifact_ids=routed
|
|
90
|
+
)
|
|
91
|
+
marked = self._control_plane.mark_stale(artifacts=stale)
|
|
92
|
+
self._control_plane.route_notifications(
|
|
93
|
+
deployment_id=deployment_id, delta=delta, tombstone=tombstone
|
|
94
|
+
)
|
|
95
|
+
return marked
|
|
96
|
+
|
|
97
|
+
def mark_all_manifest_drift(
|
|
98
|
+
self, *, deployment_id: UUID, contexts: Mapping[UUID, KnowledgeCompileContext]
|
|
99
|
+
) -> tuple[UUID, ...]:
|
|
100
|
+
"""Catch sidecar, summary, rule, or writer-version drift without an E delta."""
|
|
101
|
+
stale = self._control_plane.stale_artifacts(
|
|
102
|
+
deployment_id=deployment_id, contexts=contexts
|
|
103
|
+
)
|
|
104
|
+
return self._control_plane.mark_stale(artifacts=stale)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class KnowledgeCommitDriver:
|
|
108
|
+
"""Compile a dependency-ordered batch and publish it through one git writer."""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
*,
|
|
113
|
+
control_plane: KnowledgeControlPlane,
|
|
114
|
+
git_remote: KGitRemotePort,
|
|
115
|
+
compiler: KnowledgePageCompiler,
|
|
116
|
+
settings: KnowledgeCommitSettings,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Bind the commit loop to its Postgres, git, and page-compiler seams."""
|
|
119
|
+
self._control_plane = control_plane
|
|
120
|
+
self._git_remote = git_remote
|
|
121
|
+
self._compiler = compiler
|
|
122
|
+
self._settings = settings
|
|
123
|
+
self._authored = KnowledgeAuthoredSynchronizer(control_plane=control_plane)
|
|
124
|
+
|
|
125
|
+
def run_cycle(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
deployment_id: UUID,
|
|
129
|
+
exclusions_by_artifact: Mapping[UUID, Collection[KnowledgeEvidenceTarget]],
|
|
130
|
+
) -> KnowledgeCommitCycleResult:
|
|
131
|
+
"""Recover prior work, compile stale pages, publish once, then finalize."""
|
|
132
|
+
with self._control_plane.commit_lease(deployment_id=deployment_id):
|
|
133
|
+
with TemporaryDirectory(prefix="rememberstack-k-cycle-") as temporary:
|
|
134
|
+
worktree = Path(temporary)
|
|
135
|
+
checkout = self._git_remote.checkout(destination=worktree)
|
|
136
|
+
recovered = self._recover_pending_cycles(
|
|
137
|
+
deployment_id=deployment_id,
|
|
138
|
+
worktree=worktree,
|
|
139
|
+
git_revision=checkout.root,
|
|
140
|
+
)
|
|
141
|
+
authored = self._authored.sync_checkout(
|
|
142
|
+
deployment_id=deployment_id,
|
|
143
|
+
worktree=worktree,
|
|
144
|
+
git_revision=checkout.root,
|
|
145
|
+
)
|
|
146
|
+
self._control_plane.materialize_due_dispatches(
|
|
147
|
+
deployment_id=deployment_id,
|
|
148
|
+
component_version=KNOWLEDGE_DRIVER_VERSION,
|
|
149
|
+
)
|
|
150
|
+
pending_decisions = self._control_plane.pending_plan_decisions(
|
|
151
|
+
deployment_id=deployment_id
|
|
152
|
+
)
|
|
153
|
+
plan_files_changed = _reconcile_plan_files(
|
|
154
|
+
decisions=pending_decisions, worktree=worktree
|
|
155
|
+
)
|
|
156
|
+
quarantined = self._quarantine_compiled_drift(
|
|
157
|
+
deployment_id=deployment_id, worktree=worktree
|
|
158
|
+
)
|
|
159
|
+
artifacts = self._control_plane.compile_artifacts(
|
|
160
|
+
deployment_id=deployment_id
|
|
161
|
+
)
|
|
162
|
+
schedule = knowledge_compile_order(artifacts=artifacts)
|
|
163
|
+
if not schedule:
|
|
164
|
+
revision = checkout.root
|
|
165
|
+
if plan_files_changed:
|
|
166
|
+
revision = self._git_remote.publish(worktree=worktree).root
|
|
167
|
+
stamped = self._control_plane.stamp_ready_plan_decisions(
|
|
168
|
+
deployment_id=deployment_id,
|
|
169
|
+
git_commit=revision,
|
|
170
|
+
present_paths=_present_paths(worktree=worktree),
|
|
171
|
+
)
|
|
172
|
+
return KnowledgeCommitCycleResult(
|
|
173
|
+
checkout_revision=checkout.root,
|
|
174
|
+
published_revision=(
|
|
175
|
+
revision if revision != checkout.root else None
|
|
176
|
+
),
|
|
177
|
+
recovered_cycle_ids=recovered,
|
|
178
|
+
quarantined_artifact_ids=quarantined,
|
|
179
|
+
stamped_plan_decision_ids=stamped,
|
|
180
|
+
registered_authored_artifact_ids=(
|
|
181
|
+
authored.registered_artifact_ids
|
|
182
|
+
),
|
|
183
|
+
synced_authored_artifact_ids=authored.synced_artifact_ids,
|
|
184
|
+
authored_lint_flag_artifact_ids=(
|
|
185
|
+
authored.lint_flag_artifact_ids
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
known_paths = self._control_plane.artifact_git_paths(
|
|
190
|
+
deployment_id=deployment_id
|
|
191
|
+
)
|
|
192
|
+
compiled = self._compile_schedule(
|
|
193
|
+
artifacts=artifacts,
|
|
194
|
+
schedule=schedule,
|
|
195
|
+
known_paths=known_paths,
|
|
196
|
+
worktree=worktree,
|
|
197
|
+
exclusions_by_artifact=exclusions_by_artifact,
|
|
198
|
+
)
|
|
199
|
+
for artifact, output in compiled:
|
|
200
|
+
_write_compiled_page(
|
|
201
|
+
worktree=worktree,
|
|
202
|
+
git_path=artifact.git_path,
|
|
203
|
+
markdown=output.markdown,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
cycle_id = uuid4()
|
|
207
|
+
compilations = tuple(output.compilation for _, output in compiled)
|
|
208
|
+
self._control_plane.record_pending_compilations(
|
|
209
|
+
cycle_id=cycle_id, compilations=compilations
|
|
210
|
+
)
|
|
211
|
+
published = self._git_remote.publish(worktree=worktree)
|
|
212
|
+
self._control_plane.commit_compilations(
|
|
213
|
+
compilations=compilations, git_commit=published.root
|
|
214
|
+
)
|
|
215
|
+
stamped = self._control_plane.stamp_ready_plan_decisions(
|
|
216
|
+
deployment_id=deployment_id,
|
|
217
|
+
git_commit=published.root,
|
|
218
|
+
present_paths=_present_paths(worktree=worktree),
|
|
219
|
+
)
|
|
220
|
+
return KnowledgeCommitCycleResult(
|
|
221
|
+
checkout_revision=checkout.root,
|
|
222
|
+
published_revision=published.root,
|
|
223
|
+
compiled_artifact_ids=tuple(
|
|
224
|
+
artifact.artifact_id for artifact, _ in compiled
|
|
225
|
+
),
|
|
226
|
+
recovered_cycle_ids=recovered,
|
|
227
|
+
quarantined_artifact_ids=quarantined,
|
|
228
|
+
stamped_plan_decision_ids=stamped,
|
|
229
|
+
registered_authored_artifact_ids=(authored.registered_artifact_ids),
|
|
230
|
+
synced_authored_artifact_ids=authored.synced_artifact_ids,
|
|
231
|
+
authored_lint_flag_artifact_ids=authored.lint_flag_artifact_ids,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _quarantine_compiled_drift(
|
|
235
|
+
self, *, deployment_id: UUID, worktree: Path
|
|
236
|
+
) -> tuple[UUID, ...]:
|
|
237
|
+
"""Preserve direct compiled-body changes before scheduling any compiler."""
|
|
238
|
+
quarantined: list[UUID] = []
|
|
239
|
+
for state in self._control_plane.compiled_content_states(
|
|
240
|
+
deployment_id=deployment_id
|
|
241
|
+
):
|
|
242
|
+
target = _worktree_target(worktree=worktree, git_path=state.git_path)
|
|
243
|
+
if target.is_file():
|
|
244
|
+
try:
|
|
245
|
+
edited_markdown = target.read_text(encoding="utf-8")
|
|
246
|
+
detected_hash = knowledge_content_hash(markdown=edited_markdown)
|
|
247
|
+
except UnicodeError:
|
|
248
|
+
edited_markdown = (
|
|
249
|
+
"# Quarantined compiled-page edit\n\n"
|
|
250
|
+
"The directly edited body is not valid UTF-8.\n"
|
|
251
|
+
)
|
|
252
|
+
detected_hash = knowledge_content_hash(markdown=edited_markdown)
|
|
253
|
+
else:
|
|
254
|
+
edited_markdown = (
|
|
255
|
+
"# Quarantined compiled-page deletion\n\n"
|
|
256
|
+
"The compiled body was deleted directly from the checkout.\n"
|
|
257
|
+
)
|
|
258
|
+
detected_hash = knowledge_content_hash(markdown="")
|
|
259
|
+
if detected_hash == state.content_hash:
|
|
260
|
+
continue
|
|
261
|
+
self._control_plane.quarantine_compiled_edit(
|
|
262
|
+
artifact_id=state.artifact_id,
|
|
263
|
+
detected_content_hash=detected_hash,
|
|
264
|
+
edited_markdown=edited_markdown,
|
|
265
|
+
driver_version=KNOWLEDGE_DRIVER_VERSION,
|
|
266
|
+
)
|
|
267
|
+
quarantined.append(state.artifact_id)
|
|
268
|
+
return tuple(quarantined)
|
|
269
|
+
|
|
270
|
+
def _recover_pending_cycles(
|
|
271
|
+
self, *, deployment_id: UUID, worktree: Path, git_revision: str
|
|
272
|
+
) -> tuple[UUID, ...]:
|
|
273
|
+
"""Finalize cycles present at HEAD and abandon those never published."""
|
|
274
|
+
artifacts = {
|
|
275
|
+
artifact.artifact_id: artifact
|
|
276
|
+
for artifact in self._control_plane.compile_artifacts(
|
|
277
|
+
deployment_id=deployment_id
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
recovered: list[UUID] = []
|
|
281
|
+
for cycle in self._control_plane.pending_cycles(deployment_id=deployment_id):
|
|
282
|
+
if _pending_cycle_matches_worktree(
|
|
283
|
+
cycle=cycle, artifacts=artifacts, worktree=worktree
|
|
284
|
+
):
|
|
285
|
+
self._control_plane.commit_compilations(
|
|
286
|
+
compilations=cycle.compilations, git_commit=git_revision
|
|
287
|
+
)
|
|
288
|
+
recovered.append(cycle.cycle_id)
|
|
289
|
+
else:
|
|
290
|
+
self._control_plane.fail_pending_cycle(
|
|
291
|
+
deployment_id=deployment_id,
|
|
292
|
+
cycle_id=cycle.cycle_id,
|
|
293
|
+
failure="remote HEAD does not contain the pending cycle output",
|
|
294
|
+
)
|
|
295
|
+
return tuple(recovered)
|
|
296
|
+
|
|
297
|
+
def _compile_schedule(
|
|
298
|
+
self,
|
|
299
|
+
*,
|
|
300
|
+
artifacts: tuple[KnowledgeCompileArtifact, ...],
|
|
301
|
+
schedule: tuple[KnowledgeCompileArtifact, ...],
|
|
302
|
+
known_paths: Collection[str],
|
|
303
|
+
worktree: Path,
|
|
304
|
+
exclusions_by_artifact: Mapping[UUID, Collection[KnowledgeEvidenceTarget]],
|
|
305
|
+
) -> tuple[tuple[KnowledgeCompileArtifact, KnowledgePageCompileOutput], ...]:
|
|
306
|
+
"""Compile disjoint pages by wave while carrying fresh summaries upward."""
|
|
307
|
+
summaries = {
|
|
308
|
+
artifact.artifact_id: artifact.page_summary
|
|
309
|
+
for artifact in artifacts
|
|
310
|
+
if artifact.page_summary is not None
|
|
311
|
+
}
|
|
312
|
+
model_summaries: dict[UUID | None, str] = {
|
|
313
|
+
artifact.scope_id: artifact.page_summary
|
|
314
|
+
for artifact in artifacts
|
|
315
|
+
if artifact.artifact_kind == "model_page"
|
|
316
|
+
and artifact.page_summary is not None
|
|
317
|
+
}
|
|
318
|
+
changed_summaries: set[UUID] = set()
|
|
319
|
+
changed_model_scopes: set[UUID | None] = set()
|
|
320
|
+
outputs: list[tuple[KnowledgeCompileArtifact, KnowledgePageCompileOutput]] = []
|
|
321
|
+
children = {
|
|
322
|
+
artifact.artifact_id: tuple(
|
|
323
|
+
child
|
|
324
|
+
for child in artifacts
|
|
325
|
+
if child.parent_artifact_id == artifact.artifact_id
|
|
326
|
+
)
|
|
327
|
+
for artifact in artifacts
|
|
328
|
+
}
|
|
329
|
+
with ThreadPoolExecutor(
|
|
330
|
+
max_workers=self._settings.max_parallel_pages,
|
|
331
|
+
thread_name_prefix="rememberstack-k-page",
|
|
332
|
+
) as executor:
|
|
333
|
+
for wave in _compile_waves(artifacts=artifacts, schedule=schedule):
|
|
334
|
+
eligible = tuple(
|
|
335
|
+
artifact
|
|
336
|
+
for artifact in wave
|
|
337
|
+
if artifact.stale
|
|
338
|
+
or {
|
|
339
|
+
child.artifact_id for child in children[artifact.artifact_id]
|
|
340
|
+
}.intersection(changed_summaries)
|
|
341
|
+
or (
|
|
342
|
+
artifact.artifact_kind != "model_page"
|
|
343
|
+
and artifact.scope_id in changed_model_scopes
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
futures: list[
|
|
347
|
+
tuple[KnowledgeCompileArtifact, Future[KnowledgePageCompileOutput]]
|
|
348
|
+
] = []
|
|
349
|
+
for artifact in eligible:
|
|
350
|
+
child_summaries = {
|
|
351
|
+
child.artifact_id: summaries[child.artifact_id]
|
|
352
|
+
for child in children[artifact.artifact_id]
|
|
353
|
+
if child.artifact_id in summaries
|
|
354
|
+
}
|
|
355
|
+
futures.append(
|
|
356
|
+
(
|
|
357
|
+
artifact,
|
|
358
|
+
executor.submit(
|
|
359
|
+
self._compile_one,
|
|
360
|
+
artifact=artifact,
|
|
361
|
+
child_summaries=child_summaries,
|
|
362
|
+
shared_model_summary=(
|
|
363
|
+
None
|
|
364
|
+
if artifact.artifact_kind == "model_page"
|
|
365
|
+
else model_summaries.get(artifact.scope_id)
|
|
366
|
+
),
|
|
367
|
+
known_paths=known_paths,
|
|
368
|
+
worktree=worktree,
|
|
369
|
+
exclusions=tuple(
|
|
370
|
+
exclusions_by_artifact.get(artifact.artifact_id, ())
|
|
371
|
+
),
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
wave_outputs, errors = self._collect_parallel_wave(futures=futures)
|
|
376
|
+
if errors:
|
|
377
|
+
self._record_aborted_wave(outputs=wave_outputs, errors=errors)
|
|
378
|
+
for artifact, output in wave_outputs:
|
|
379
|
+
new_summary = output.compilation.page_summary
|
|
380
|
+
if new_summary != artifact.page_summary:
|
|
381
|
+
changed_summaries.add(artifact.artifact_id)
|
|
382
|
+
if artifact.artifact_kind == "model_page":
|
|
383
|
+
changed_model_scopes.add(artifact.scope_id)
|
|
384
|
+
summaries[artifact.artifact_id] = new_summary
|
|
385
|
+
if artifact.artifact_kind == "model_page":
|
|
386
|
+
model_summaries[artifact.scope_id] = new_summary
|
|
387
|
+
outputs.append((artifact, output))
|
|
388
|
+
return tuple(outputs)
|
|
389
|
+
|
|
390
|
+
def _compile_one(
|
|
391
|
+
self,
|
|
392
|
+
*,
|
|
393
|
+
artifact: KnowledgeCompileArtifact,
|
|
394
|
+
child_summaries: Mapping[UUID, str],
|
|
395
|
+
shared_model_summary: str | None,
|
|
396
|
+
known_paths: Collection[str],
|
|
397
|
+
worktree: Path,
|
|
398
|
+
exclusions: tuple[KnowledgeEvidenceTarget, ...],
|
|
399
|
+
) -> KnowledgePageCompileOutput:
|
|
400
|
+
"""Compile and validate one page without touching shared checkout state."""
|
|
401
|
+
previous_markdown = (
|
|
402
|
+
None
|
|
403
|
+
if artifact.content_hash is None
|
|
404
|
+
else _read_optional_compiler_input(
|
|
405
|
+
worktree=worktree, git_path=artifact.git_path
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
curation_markdown = (
|
|
409
|
+
None
|
|
410
|
+
if artifact.curation_path is None
|
|
411
|
+
else _read_optional_compiler_input(
|
|
412
|
+
worktree=worktree, git_path=artifact.curation_path
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
output = self._compiler.compile_page(
|
|
416
|
+
request=KnowledgePageCompileRequest(
|
|
417
|
+
artifact=artifact,
|
|
418
|
+
child_summaries=dict(child_summaries),
|
|
419
|
+
shared_model_summary=shared_model_summary,
|
|
420
|
+
curation_hash=(
|
|
421
|
+
None
|
|
422
|
+
if curation_markdown is None
|
|
423
|
+
else knowledge_content_hash(markdown=curation_markdown)
|
|
424
|
+
),
|
|
425
|
+
curation_markdown=curation_markdown,
|
|
426
|
+
previous_markdown=previous_markdown,
|
|
427
|
+
exclusions=exclusions,
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
try:
|
|
431
|
+
validate_knowledge_page_output(
|
|
432
|
+
artifact=artifact,
|
|
433
|
+
output=output,
|
|
434
|
+
known_git_paths=known_paths,
|
|
435
|
+
exclusions=exclusions,
|
|
436
|
+
)
|
|
437
|
+
self._control_plane.validate_citations(
|
|
438
|
+
deployment_id=artifact.deployment_id,
|
|
439
|
+
citations=output.compilation.citations,
|
|
440
|
+
)
|
|
441
|
+
except Exception as error:
|
|
442
|
+
compilation = output.compilation
|
|
443
|
+
failure_trace = traceback.format_exc()
|
|
444
|
+
try:
|
|
445
|
+
self._control_plane.record_failed_compilation(
|
|
446
|
+
failure=KnowledgeCompilationFailure(
|
|
447
|
+
compilation_id=compilation.compilation_id,
|
|
448
|
+
deployment_id=compilation.deployment_id,
|
|
449
|
+
artifact_id=compilation.artifact_id,
|
|
450
|
+
inputs_hash=compilation.inputs_hash,
|
|
451
|
+
candidate_count=compilation.candidate_count,
|
|
452
|
+
claims_cut_count=compilation.claims_cut_count,
|
|
453
|
+
writer_version=compilation.writer_version,
|
|
454
|
+
failure=failure_trace,
|
|
455
|
+
session_transcript_uri=compilation.session_transcript_uri,
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
except Exception as ledger_error:
|
|
459
|
+
raise ExceptionGroup(
|
|
460
|
+
"page validation and failure-ledger recording both failed",
|
|
461
|
+
(error, ledger_error),
|
|
462
|
+
) from None
|
|
463
|
+
raise
|
|
464
|
+
return output
|
|
465
|
+
|
|
466
|
+
def _collect_parallel_wave(
|
|
467
|
+
self,
|
|
468
|
+
*,
|
|
469
|
+
futures: list[
|
|
470
|
+
tuple[KnowledgeCompileArtifact, Future[KnowledgePageCompileOutput]]
|
|
471
|
+
],
|
|
472
|
+
) -> tuple[
|
|
473
|
+
list[tuple[KnowledgeCompileArtifact, KnowledgePageCompileOutput]],
|
|
474
|
+
list[Exception],
|
|
475
|
+
]:
|
|
476
|
+
"""Await a whole wave so sibling sessions cannot outlive an aborted cycle."""
|
|
477
|
+
outputs: list[tuple[KnowledgeCompileArtifact, KnowledgePageCompileOutput]] = []
|
|
478
|
+
errors: list[Exception] = []
|
|
479
|
+
for artifact, future in futures:
|
|
480
|
+
try:
|
|
481
|
+
outputs.append((artifact, future.result()))
|
|
482
|
+
except Exception as error:
|
|
483
|
+
errors.append(error)
|
|
484
|
+
return outputs, errors
|
|
485
|
+
|
|
486
|
+
def _record_aborted_wave(
|
|
487
|
+
self,
|
|
488
|
+
*,
|
|
489
|
+
outputs: list[tuple[KnowledgeCompileArtifact, KnowledgePageCompileOutput]],
|
|
490
|
+
errors: list[Exception],
|
|
491
|
+
) -> None:
|
|
492
|
+
"""Ledger successful siblings discarded because their wave could not publish."""
|
|
493
|
+
ledger_errors: list[Exception] = []
|
|
494
|
+
for _, output in outputs:
|
|
495
|
+
compilation = output.compilation
|
|
496
|
+
try:
|
|
497
|
+
self._control_plane.record_failed_compilation(
|
|
498
|
+
failure=KnowledgeCompilationFailure(
|
|
499
|
+
compilation_id=compilation.compilation_id,
|
|
500
|
+
deployment_id=compilation.deployment_id,
|
|
501
|
+
artifact_id=compilation.artifact_id,
|
|
502
|
+
inputs_hash=compilation.inputs_hash,
|
|
503
|
+
candidate_count=compilation.candidate_count,
|
|
504
|
+
claims_cut_count=compilation.claims_cut_count,
|
|
505
|
+
writer_version=compilation.writer_version,
|
|
506
|
+
failure="parallel compile wave aborted because a sibling failed",
|
|
507
|
+
session_transcript_uri=compilation.session_transcript_uri,
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
except Exception as ledger_error:
|
|
511
|
+
ledger_errors.append(ledger_error)
|
|
512
|
+
combined = [*errors, *ledger_errors]
|
|
513
|
+
if len(combined) == 1:
|
|
514
|
+
raise combined[0]
|
|
515
|
+
raise ExceptionGroup("parallel compile wave failed", combined)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _compile_waves(
|
|
519
|
+
*,
|
|
520
|
+
artifacts: tuple[KnowledgeCompileArtifact, ...],
|
|
521
|
+
schedule: tuple[KnowledgeCompileArtifact, ...],
|
|
522
|
+
) -> tuple[tuple[KnowledgeCompileArtifact, ...], ...]:
|
|
523
|
+
"""Group a valid child-first order into disjoint dependency waves."""
|
|
524
|
+
by_id = {artifact.artifact_id: artifact for artifact in artifacts}
|
|
525
|
+
depths: dict[UUID, int] = {}
|
|
526
|
+
|
|
527
|
+
def depth(artifact: KnowledgeCompileArtifact) -> int:
|
|
528
|
+
known = depths.get(artifact.artifact_id)
|
|
529
|
+
if known is not None:
|
|
530
|
+
return known
|
|
531
|
+
parent = (
|
|
532
|
+
None
|
|
533
|
+
if artifact.parent_artifact_id is None
|
|
534
|
+
else by_id.get(artifact.parent_artifact_id)
|
|
535
|
+
)
|
|
536
|
+
value = 0 if parent is None else depth(parent) + 1
|
|
537
|
+
depths[artifact.artifact_id] = value
|
|
538
|
+
return value
|
|
539
|
+
|
|
540
|
+
models = tuple(
|
|
541
|
+
artifact for artifact in schedule if artifact.artifact_kind == "model_page"
|
|
542
|
+
)
|
|
543
|
+
root_indexes = tuple(
|
|
544
|
+
artifact
|
|
545
|
+
for artifact in schedule
|
|
546
|
+
if artifact.artifact_kind != "model_page"
|
|
547
|
+
and artifact.parent_artifact_id is None
|
|
548
|
+
and artifact.git_path == "_index.md"
|
|
549
|
+
)
|
|
550
|
+
regular = tuple(
|
|
551
|
+
artifact
|
|
552
|
+
for artifact in schedule
|
|
553
|
+
if artifact not in models and artifact not in root_indexes
|
|
554
|
+
)
|
|
555
|
+
waves: list[tuple[KnowledgeCompileArtifact, ...]] = []
|
|
556
|
+
if models:
|
|
557
|
+
waves.append(models)
|
|
558
|
+
for level in sorted({depth(artifact) for artifact in regular}, reverse=True):
|
|
559
|
+
waves.append(
|
|
560
|
+
tuple(artifact for artifact in regular if depth(artifact) == level)
|
|
561
|
+
)
|
|
562
|
+
if root_indexes:
|
|
563
|
+
waves.append(root_indexes)
|
|
564
|
+
return tuple(waves)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _reconcile_plan_files(
|
|
568
|
+
*, decisions: tuple[KnowledgePendingPlanDecision, ...], worktree: Path
|
|
569
|
+
) -> bool:
|
|
570
|
+
"""Apply accepted structure to the disposable checkout without generating prose."""
|
|
571
|
+
changed = False
|
|
572
|
+
for decision in decisions:
|
|
573
|
+
proposal = decision.proposal
|
|
574
|
+
if isinstance(proposal, KnowledgeMovePageProposal):
|
|
575
|
+
changed = (
|
|
576
|
+
_move_worktree_file(
|
|
577
|
+
worktree=worktree,
|
|
578
|
+
old_path=proposal.old_git_path,
|
|
579
|
+
new_path=proposal.new_git_path,
|
|
580
|
+
)
|
|
581
|
+
or changed
|
|
582
|
+
)
|
|
583
|
+
changed = (
|
|
584
|
+
_move_worktree_file(
|
|
585
|
+
worktree=worktree,
|
|
586
|
+
old_path=proposal.old_curation_path,
|
|
587
|
+
new_path=proposal.new_curation_path,
|
|
588
|
+
)
|
|
589
|
+
or changed
|
|
590
|
+
)
|
|
591
|
+
elif isinstance(proposal, KnowledgeMergePagesProposal):
|
|
592
|
+
for artifact_id in proposal.source_artifact_ids:
|
|
593
|
+
changed = (
|
|
594
|
+
_remove_worktree_file(
|
|
595
|
+
worktree=worktree, git_path=decision.artifact_paths[artifact_id]
|
|
596
|
+
)
|
|
597
|
+
or changed
|
|
598
|
+
)
|
|
599
|
+
elif isinstance(proposal, KnowledgeRetirePageProposal):
|
|
600
|
+
changed = (
|
|
601
|
+
_remove_worktree_file(
|
|
602
|
+
worktree=worktree,
|
|
603
|
+
git_path=decision.artifact_paths[proposal.artifact_id],
|
|
604
|
+
)
|
|
605
|
+
or changed
|
|
606
|
+
)
|
|
607
|
+
elif (
|
|
608
|
+
isinstance(proposal, KnowledgeConvertKindProposal)
|
|
609
|
+
and proposal.to_kind is KnowledgePageKind.COMPILED
|
|
610
|
+
):
|
|
611
|
+
changed = (
|
|
612
|
+
_preserve_handover_body(
|
|
613
|
+
worktree=worktree,
|
|
614
|
+
body_path=decision.artifact_paths[proposal.artifact_id],
|
|
615
|
+
curation_path=proposal.curation_path,
|
|
616
|
+
)
|
|
617
|
+
or changed
|
|
618
|
+
)
|
|
619
|
+
return changed
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _move_worktree_file(*, worktree: Path, old_path: str, new_path: str) -> bool:
|
|
623
|
+
"""Move one plan-owned file idempotently inside the checkout."""
|
|
624
|
+
if old_path == new_path:
|
|
625
|
+
return False
|
|
626
|
+
source = _worktree_target(worktree=worktree, git_path=old_path)
|
|
627
|
+
target = _worktree_target(worktree=worktree, git_path=new_path)
|
|
628
|
+
if source.exists() and target.exists():
|
|
629
|
+
raise ValueError(f"plan move has both source and target files: {old_path}")
|
|
630
|
+
if not source.exists():
|
|
631
|
+
return False
|
|
632
|
+
if not source.is_file():
|
|
633
|
+
raise ValueError(f"plan move source is not a file: {old_path}")
|
|
634
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
635
|
+
source.replace(target)
|
|
636
|
+
return True
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _remove_worktree_file(*, worktree: Path, git_path: str) -> bool:
|
|
640
|
+
"""Remove one retired machine-owned page idempotently from the checkout."""
|
|
641
|
+
target = _worktree_target(worktree=worktree, git_path=git_path)
|
|
642
|
+
if not target.exists():
|
|
643
|
+
return False
|
|
644
|
+
if not target.is_file():
|
|
645
|
+
raise ValueError(f"retired artifact path is not a file: {git_path}")
|
|
646
|
+
target.unlink()
|
|
647
|
+
return True
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _preserve_handover_body(
|
|
651
|
+
*, worktree: Path, body_path: str, curation_path: str | None
|
|
652
|
+
) -> bool:
|
|
653
|
+
"""Copy an author's former body into curation before a writer takes ownership."""
|
|
654
|
+
if curation_path is None:
|
|
655
|
+
raise ValueError("authored handover has no curation path")
|
|
656
|
+
body = _read_optional_compiler_input(worktree=worktree, git_path=body_path)
|
|
657
|
+
if body is None:
|
|
658
|
+
raise ValueError("authored handover body is missing")
|
|
659
|
+
target = _worktree_target(worktree=worktree, git_path=curation_path)
|
|
660
|
+
preserved = (
|
|
661
|
+
"# Authored handover source\n\n"
|
|
662
|
+
"The body below was preserved when this page became compiled.\n\n"
|
|
663
|
+
f"{body}"
|
|
664
|
+
)
|
|
665
|
+
if target.is_file():
|
|
666
|
+
existing = target.read_text(encoding="utf-8")
|
|
667
|
+
if body in existing:
|
|
668
|
+
return False
|
|
669
|
+
preserved = f"{existing.rstrip()}\n\n{preserved}"
|
|
670
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
671
|
+
target.write_text(preserved, encoding="utf-8")
|
|
672
|
+
return True
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _present_paths(*, worktree: Path) -> tuple[str, ...]:
|
|
676
|
+
"""Return normalized repository file paths used by plan readiness checks."""
|
|
677
|
+
return tuple(
|
|
678
|
+
sorted(
|
|
679
|
+
path.relative_to(worktree).as_posix()
|
|
680
|
+
for path in worktree.rglob("*")
|
|
681
|
+
if path.is_file() and ".git" not in path.relative_to(worktree).parts
|
|
682
|
+
)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _pending_cycle_matches_worktree(
|
|
687
|
+
*,
|
|
688
|
+
cycle: KnowledgePendingCycle,
|
|
689
|
+
artifacts: Mapping[UUID, KnowledgeCompileArtifact],
|
|
690
|
+
worktree: Path,
|
|
691
|
+
) -> bool:
|
|
692
|
+
"""Return whether remote HEAD contains every exact pending page body."""
|
|
693
|
+
for compilation in cycle.compilations:
|
|
694
|
+
artifact = artifacts.get(compilation.artifact_id)
|
|
695
|
+
if artifact is None:
|
|
696
|
+
return False
|
|
697
|
+
try:
|
|
698
|
+
target = _worktree_target(worktree=worktree, git_path=artifact.git_path)
|
|
699
|
+
except ValueError:
|
|
700
|
+
return False
|
|
701
|
+
if not target.is_file():
|
|
702
|
+
return False
|
|
703
|
+
try:
|
|
704
|
+
markdown = target.read_text(encoding="utf-8")
|
|
705
|
+
except UnicodeError:
|
|
706
|
+
return False
|
|
707
|
+
if knowledge_content_hash(markdown=markdown) != compilation.content_hash:
|
|
708
|
+
return False
|
|
709
|
+
return True
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _write_compiled_page(*, worktree: Path, git_path: str, markdown: str) -> None:
|
|
713
|
+
"""Atomically replace one validated path inside the disposable checkout."""
|
|
714
|
+
target = _worktree_target(worktree=worktree, git_path=git_path)
|
|
715
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
716
|
+
temporary = target.with_name(f".{target.name}.{uuid4()}.tmp")
|
|
717
|
+
temporary.write_text(markdown, encoding="utf-8")
|
|
718
|
+
temporary.replace(target)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _read_optional_compiler_input(*, worktree: Path, git_path: str) -> str | None:
|
|
722
|
+
"""Read one repository-native compiler input without widening its path surface."""
|
|
723
|
+
target = _worktree_target(worktree=worktree, git_path=git_path)
|
|
724
|
+
if not target.is_file():
|
|
725
|
+
return None
|
|
726
|
+
return target.read_text(encoding="utf-8")
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _worktree_target(*, worktree: Path, git_path: str) -> Path:
|
|
730
|
+
"""Map a validated repository-native POSIX path into a local checkout."""
|
|
731
|
+
target = worktree.joinpath(*PurePosixPath(git_path).parts)
|
|
732
|
+
root = worktree.resolve()
|
|
733
|
+
if target.is_symlink() or not target.resolve().is_relative_to(root):
|
|
734
|
+
raise ValueError(f"compiled page path escapes worktree: {git_path}")
|
|
735
|
+
return target
|