worktree-controller 1.30.4__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.
- worktree_controller/__init__.py +3 -0
- worktree_controller/activation_layout.py +483 -0
- worktree_controller/branch_retirement.py +1865 -0
- worktree_controller/canonical_refresh.py +150 -0
- worktree_controller/canonical_transition.py +324 -0
- worktree_controller/cleanup_attempts.py +442 -0
- worktree_controller/cleanup_evidence.py +75 -0
- worktree_controller/cleanup_recovery.py +912 -0
- worktree_controller/cli.py +1907 -0
- worktree_controller/controller.py +1974 -0
- worktree_controller/convergence.py +893 -0
- worktree_controller/delivered_closeout.py +928 -0
- worktree_controller/disposable.py +677 -0
- worktree_controller/environment.py +1038 -0
- worktree_controller/frozen_recovery.py +291 -0
- worktree_controller/git_backend.py +682 -0
- worktree_controller/governance.py +365 -0
- worktree_controller/hardlinks.py +294 -0
- worktree_controller/integration_cleanup.py +92 -0
- worktree_controller/integration_conflict.py +117 -0
- worktree_controller/integration_retry.py +526 -0
- worktree_controller/keyed_document_scope.py +162 -0
- worktree_controller/lifecycle.py +8992 -0
- worktree_controller/lifecycle_history.py +358 -0
- worktree_controller/lifecycle_observation.py +234 -0
- worktree_controller/metadata_codec.py +1694 -0
- worktree_controller/metadata_relocation.py +131 -0
- worktree_controller/models.py +546 -0
- worktree_controller/mount_diagnostics.py +324 -0
- worktree_controller/multiple_cleanup_recovery.py +730 -0
- worktree_controller/namespace_migration.py +1412 -0
- worktree_controller/native_session_recovery.py +171 -0
- worktree_controller/owner_actions.py +34 -0
- worktree_controller/policy.py +333 -0
- worktree_controller/preservation.py +983 -0
- worktree_controller/preserved_cleanup.py +849 -0
- worktree_controller/publication_execution.py +361 -0
- worktree_controller/reconciler.py +430 -0
- worktree_controller/recovery_guidance.py +205 -0
- worktree_controller/registration_backfill.py +166 -0
- worktree_controller/registration_prune.py +391 -0
- worktree_controller/rendering.py +179 -0
- worktree_controller/resource_admission.py +1736 -0
- worktree_controller/retirement_adapters.py +130 -0
- worktree_controller/retirement_execution.py +234 -0
- worktree_controller/semantics.py +459 -0
- worktree_controller/session.py +505 -0
- worktree_controller/session_admission.py +265 -0
- worktree_controller/source_cleanup.py +515 -0
- worktree_controller/source_preservation.py +692 -0
- worktree_controller/source_recovery.py +592 -0
- worktree_controller/store.py +1228 -0
- worktree_controller/tool_cli_contract.json +1791 -0
- worktree_controller/transaction_scope.py +876 -0
- worktree_controller/writer_lease.py +662 -0
- worktree_controller/writer_session.py +887 -0
- worktree_controller-1.30.4.dist-info/METADATA +3149 -0
- worktree_controller-1.30.4.dist-info/RECORD +61 -0
- worktree_controller-1.30.4.dist-info/WHEEL +4 -0
- worktree_controller-1.30.4.dist-info/entry_points.txt +5 -0
- worktree_controller-1.30.4.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Owner-reviewed recovery of published frozen checkouts without rewriting evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, replace
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import NAMESPACE_URL, uuid5
|
|
9
|
+
|
|
10
|
+
from .controller import status
|
|
11
|
+
from .git_backend import (
|
|
12
|
+
branch_exists,
|
|
13
|
+
git_write,
|
|
14
|
+
inventory,
|
|
15
|
+
is_ancestor,
|
|
16
|
+
resolve_commit,
|
|
17
|
+
validate_branch_name,
|
|
18
|
+
)
|
|
19
|
+
from .lifecycle import (
|
|
20
|
+
_cancel_release_intents,
|
|
21
|
+
_typed_runtime_blockers,
|
|
22
|
+
_typed_runtime_evidence,
|
|
23
|
+
_updated_record,
|
|
24
|
+
)
|
|
25
|
+
from .models import ControllerError
|
|
26
|
+
from .policy import load_policy
|
|
27
|
+
from .semantics import assert_record_version as _cas
|
|
28
|
+
from .semantics import integration_intent_abandoned, publication_ref
|
|
29
|
+
from .semantics import managed_record as _managed_record
|
|
30
|
+
from .store import MetadataStore, record_to_dict
|
|
31
|
+
from .transaction_scope import conflicts
|
|
32
|
+
from .writer_lease import holder_is_live, process_group_is_live
|
|
33
|
+
|
|
34
|
+
CONTRACT = "published-checkout-recovery/v1"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def recover_frozen_checkout(
|
|
38
|
+
repository_path: Path,
|
|
39
|
+
*,
|
|
40
|
+
target: Path,
|
|
41
|
+
expected_record_version: int,
|
|
42
|
+
expected_head: str,
|
|
43
|
+
owner: str,
|
|
44
|
+
branch: str | None = None,
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""Recover a published source advance, or reattach a published integration.
|
|
47
|
+
|
|
48
|
+
The immutable preparation receipt makes interruption safe: a retry can finish
|
|
49
|
+
only the same record/HEAD/ref transition. No working files, index, historical
|
|
50
|
+
publication receipts or commits are rewritten. Normal release gates still run.
|
|
51
|
+
"""
|
|
52
|
+
command = "recover-published" if branch is not None else "recover-handoff"
|
|
53
|
+
initial = inventory(repository_path)
|
|
54
|
+
store = MetadataStore(initial.common_dir)
|
|
55
|
+
with store.lock(exclusive=True):
|
|
56
|
+
repository = inventory(repository_path)
|
|
57
|
+
snapshot = store._load_unlocked()
|
|
58
|
+
item, record = _managed_record(repository, snapshot, target)
|
|
59
|
+
_cas(record, expected_record_version)
|
|
60
|
+
if owner != record.owner:
|
|
61
|
+
raise ControllerError("OWNER_MISMATCH", "recovery requires the recorded owner")
|
|
62
|
+
if item.head != expected_head:
|
|
63
|
+
raise ControllerError("IDENTITY_MISMATCH", "recovery HEAD differs from expected HEAD")
|
|
64
|
+
if item.canonical or item.locked or item.prunable or item.staged:
|
|
65
|
+
raise ControllerError("BLOCK_DRIFT", "canonical, locked, prunable or staged checkout")
|
|
66
|
+
if conflicts(store, allocation=record.allocation_id, target=item.path):
|
|
67
|
+
raise ControllerError(
|
|
68
|
+
"INTERRUPTED_TRANSACTION", "resolve the allocation transaction first"
|
|
69
|
+
)
|
|
70
|
+
formal_ref = publication_ref(item.path, load_policy(repository.canonical_path))
|
|
71
|
+
if formal_ref is None or not is_ancestor(item.path, item.head, formal_ref):
|
|
72
|
+
raise ControllerError(
|
|
73
|
+
"UNPUBLISHED_HEAD", "observed HEAD must already be on the publication ref"
|
|
74
|
+
)
|
|
75
|
+
for intent in snapshot.integration_intents.values():
|
|
76
|
+
if (
|
|
77
|
+
record.allocation_id in (*intent.source_allocations, intent.allocation_id)
|
|
78
|
+
and intent.state not in {"REMOTE_VERIFIED", "RELEASED"}
|
|
79
|
+
and not integration_intent_abandoned(intent)
|
|
80
|
+
):
|
|
81
|
+
raise ControllerError(
|
|
82
|
+
"ACTIVE_INTEGRATION", "an unfinished integration still consumes this allocation"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
lease = snapshot.writer_leases.get(record.allocation_id)
|
|
86
|
+
if lease and (holder_is_live(lease.holder) or process_group_is_live(lease.holder)):
|
|
87
|
+
raise ControllerError("LIVE_WRITER", "a live writer or process group blocks recovery")
|
|
88
|
+
runtime_snapshot = replace(
|
|
89
|
+
snapshot,
|
|
90
|
+
writer_leases={
|
|
91
|
+
key: value
|
|
92
|
+
for key, value in snapshot.writer_leases.items()
|
|
93
|
+
if key != record.allocation_id
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
blockers = _typed_runtime_blockers(_typed_runtime_evidence(item, record, runtime_snapshot))
|
|
97
|
+
if blockers:
|
|
98
|
+
raise ControllerError(blockers[0]["code"], blockers[0]["message"])
|
|
99
|
+
|
|
100
|
+
receipt_id = str(
|
|
101
|
+
uuid5(
|
|
102
|
+
NAMESPACE_URL,
|
|
103
|
+
":".join(
|
|
104
|
+
(
|
|
105
|
+
CONTRACT,
|
|
106
|
+
str(snapshot.repository_id),
|
|
107
|
+
record.allocation_id,
|
|
108
|
+
str(expected_record_version),
|
|
109
|
+
expected_head,
|
|
110
|
+
command,
|
|
111
|
+
branch or "",
|
|
112
|
+
)
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
receipt_path = store.completion_receipts_dir / f"{receipt_id}.json"
|
|
117
|
+
prepared = store._read_json(receipt_path) if receipt_path.exists() else None
|
|
118
|
+
if prepared and (
|
|
119
|
+
prepared.get("contract") != CONTRACT
|
|
120
|
+
or prepared.get("receipt_id") != receipt_id
|
|
121
|
+
or prepared.get("record") != record_to_dict(record)
|
|
122
|
+
or prepared.get("expected_head") != expected_head
|
|
123
|
+
or prepared.get("branch") != branch
|
|
124
|
+
or prepared.get("target_path") != str(item.path)
|
|
125
|
+
or prepared.get("repository_id") != snapshot.repository_id
|
|
126
|
+
or prepared.get("command") != command
|
|
127
|
+
or (lease is not None and prepared.get("writer_lease") != asdict(lease))
|
|
128
|
+
or (branch is None and prepared.get("observed_branch") != item.branch)
|
|
129
|
+
):
|
|
130
|
+
raise ControllerError("BLOCK_DRIFT", "immutable recovery preparation differs")
|
|
131
|
+
|
|
132
|
+
receipts = [
|
|
133
|
+
receipt
|
|
134
|
+
for receipt in snapshot.publication_receipts.values()
|
|
135
|
+
if receipt.repository_id == snapshot.repository_id
|
|
136
|
+
and receipt.status == "REMOTE_VERIFIED"
|
|
137
|
+
and receipt.lease_released_at
|
|
138
|
+
and is_ancestor(item.path, receipt.candidate_oid, formal_ref)
|
|
139
|
+
]
|
|
140
|
+
if branch is None:
|
|
141
|
+
frozen = record.handoff_commit
|
|
142
|
+
if (
|
|
143
|
+
record.role not in {"feature", "audit", "hotfix"}
|
|
144
|
+
or record.lifecycle not in {"HANDOFF_READY", "PRESERVED"}
|
|
145
|
+
or item.detached
|
|
146
|
+
or not item.branch
|
|
147
|
+
or not frozen
|
|
148
|
+
or not record.finalizer_record_id
|
|
149
|
+
or frozen != record.task_commit
|
|
150
|
+
or frozen != record.finalizer_commit
|
|
151
|
+
or frozen == item.head
|
|
152
|
+
or not is_ancestor(item.path, frozen, item.head)
|
|
153
|
+
):
|
|
154
|
+
raise ControllerError(
|
|
155
|
+
"INVALID_FROZEN_SOURCE",
|
|
156
|
+
"recovery requires an attached, published advance of a frozen source",
|
|
157
|
+
)
|
|
158
|
+
receipts = [
|
|
159
|
+
receipt
|
|
160
|
+
for receipt in receipts
|
|
161
|
+
if any(
|
|
162
|
+
result.get("source_allocation_id") == record.allocation_id
|
|
163
|
+
and result.get("source_handoff_oid") == frozen
|
|
164
|
+
and result.get("absorbed_oid") == frozen
|
|
165
|
+
and result.get("mode") == "ancestry"
|
|
166
|
+
and is_ancestor(item.path, frozen, receipt.candidate_oid)
|
|
167
|
+
for result in receipt.source_results
|
|
168
|
+
)
|
|
169
|
+
]
|
|
170
|
+
else:
|
|
171
|
+
validate_branch_name(item.path, branch)
|
|
172
|
+
if (
|
|
173
|
+
record.role != "integration"
|
|
174
|
+
or record.lifecycle != "PUBLISHED"
|
|
175
|
+
or not item.clean
|
|
176
|
+
or not record.finalizer_record_id
|
|
177
|
+
or record.task_commit != item.head
|
|
178
|
+
or record.finalizer_commit != item.head
|
|
179
|
+
or (not item.detached and not (prepared and item.branch == branch))
|
|
180
|
+
):
|
|
181
|
+
raise ControllerError(
|
|
182
|
+
"INVALID_PUBLISHED_CHECKOUT",
|
|
183
|
+
"recovery requires a clean, detached published integration",
|
|
184
|
+
)
|
|
185
|
+
if any(
|
|
186
|
+
other.key != item.key and other.branch == branch for other in repository.worktrees
|
|
187
|
+
):
|
|
188
|
+
raise ControllerError("BRANCH_IN_USE", "recovery branch is checked out elsewhere")
|
|
189
|
+
if branch_exists(item.path, branch) and (
|
|
190
|
+
not prepared or resolve_commit(item.path, f"refs/heads/{branch}") != item.head
|
|
191
|
+
):
|
|
192
|
+
raise ControllerError(
|
|
193
|
+
"BRANCH_EXISTS", "recovery must create a fresh retained branch"
|
|
194
|
+
)
|
|
195
|
+
receipts = [
|
|
196
|
+
receipt
|
|
197
|
+
for receipt in receipts
|
|
198
|
+
if receipt.allocation_id == record.allocation_id
|
|
199
|
+
and receipt.candidate_oid == item.head
|
|
200
|
+
and receipt.finalizer_record_id == record.finalizer_record_id
|
|
201
|
+
]
|
|
202
|
+
if not receipts:
|
|
203
|
+
raise ControllerError(
|
|
204
|
+
"MISSING_PUBLICATION_EVIDENCE", "exact released publication evidence is required"
|
|
205
|
+
)
|
|
206
|
+
publication = sorted(receipts, key=lambda value: value.receipt_id)[0]
|
|
207
|
+
if prepared and prepared.get("publication_receipt_id") not in {
|
|
208
|
+
r.receipt_id for r in receipts
|
|
209
|
+
}:
|
|
210
|
+
raise ControllerError("BLOCK_DRIFT", "prepared publication evidence is no longer valid")
|
|
211
|
+
if prepared is None:
|
|
212
|
+
prepared = {
|
|
213
|
+
"contract": CONTRACT,
|
|
214
|
+
"receipt_id": receipt_id,
|
|
215
|
+
"repository_id": snapshot.repository_id,
|
|
216
|
+
"command": command,
|
|
217
|
+
"target_path": str(item.path),
|
|
218
|
+
"record": record_to_dict(record),
|
|
219
|
+
"expected_head": item.head,
|
|
220
|
+
"branch": branch,
|
|
221
|
+
"observed_branch": item.branch,
|
|
222
|
+
"publication_receipt_id": publication.receipt_id,
|
|
223
|
+
"publication_ref": formal_ref,
|
|
224
|
+
"publication_oid": resolve_commit(item.path, formal_ref),
|
|
225
|
+
"writer_lease": asdict(lease) if lease else None,
|
|
226
|
+
"staged": item.staged,
|
|
227
|
+
"modified": item.modified,
|
|
228
|
+
"untracked": item.untracked,
|
|
229
|
+
}
|
|
230
|
+
store.write_completion_receipt(receipt_id, prepared)
|
|
231
|
+
|
|
232
|
+
if branch is not None:
|
|
233
|
+
if not branch_exists(item.path, branch):
|
|
234
|
+
git_write(
|
|
235
|
+
item.path,
|
|
236
|
+
("update-ref", f"refs/heads/{branch}", item.head, "0" * len(item.head)),
|
|
237
|
+
)
|
|
238
|
+
latest, _ = _managed_record(inventory(repository_path), snapshot, target)
|
|
239
|
+
if latest.head != expected_head or not latest.clean:
|
|
240
|
+
raise ControllerError(
|
|
241
|
+
"BLOCK_DRIFT", "checkout changed during recovery; retained ref is preserved"
|
|
242
|
+
)
|
|
243
|
+
if latest.detached:
|
|
244
|
+
# Repoint only HEAD; unlike checkout this never rewrites files or runs hooks.
|
|
245
|
+
git_write(item.path, ("symbolic-ref", "HEAD", f"refs/heads/{branch}"))
|
|
246
|
+
elif latest.branch != branch:
|
|
247
|
+
raise ControllerError("BLOCK_DRIFT", "checkout branch changed during recovery")
|
|
248
|
+
updated = _updated_record(record, "PUBLISHED_CHECKOUT_REATTACHED")
|
|
249
|
+
else:
|
|
250
|
+
_cancel_release_intents(
|
|
251
|
+
store,
|
|
252
|
+
snapshot,
|
|
253
|
+
record.allocation_id,
|
|
254
|
+
event="FROZEN_SOURCE_RECOVERED",
|
|
255
|
+
blocker="REOPENED",
|
|
256
|
+
)
|
|
257
|
+
updated = _updated_record(
|
|
258
|
+
record,
|
|
259
|
+
"PUBLISHED_SOURCE_RECOVERED",
|
|
260
|
+
lifecycle="ACTIVE",
|
|
261
|
+
base_commit=item.head,
|
|
262
|
+
handoff_commit=None,
|
|
263
|
+
task_commit=None,
|
|
264
|
+
finalizer_commit=None,
|
|
265
|
+
finalizer_record_id=None,
|
|
266
|
+
)
|
|
267
|
+
if lease:
|
|
268
|
+
store.remove_writer_lease(record.allocation_id)
|
|
269
|
+
updated = replace(
|
|
270
|
+
updated,
|
|
271
|
+
history=(
|
|
272
|
+
*updated.history[:-1],
|
|
273
|
+
{
|
|
274
|
+
**updated.history[-1],
|
|
275
|
+
"recovery_receipt_id": receipt_id,
|
|
276
|
+
},
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
store.replace_record(updated)
|
|
280
|
+
payload = status(repository_path)
|
|
281
|
+
payload["command"] = command
|
|
282
|
+
payload["decision"] = {
|
|
283
|
+
"code": "FROZEN_CHECKOUT_RECOVERED",
|
|
284
|
+
"allocation_id": updated.allocation_id,
|
|
285
|
+
"record_version": updated.record_version,
|
|
286
|
+
"lifecycle": updated.lifecycle,
|
|
287
|
+
"receipt_id": receipt_id,
|
|
288
|
+
"head": expected_head,
|
|
289
|
+
"branch": branch,
|
|
290
|
+
}
|
|
291
|
+
return payload
|