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.
Files changed (61) hide show
  1. worktree_controller/__init__.py +3 -0
  2. worktree_controller/activation_layout.py +483 -0
  3. worktree_controller/branch_retirement.py +1865 -0
  4. worktree_controller/canonical_refresh.py +150 -0
  5. worktree_controller/canonical_transition.py +324 -0
  6. worktree_controller/cleanup_attempts.py +442 -0
  7. worktree_controller/cleanup_evidence.py +75 -0
  8. worktree_controller/cleanup_recovery.py +912 -0
  9. worktree_controller/cli.py +1907 -0
  10. worktree_controller/controller.py +1974 -0
  11. worktree_controller/convergence.py +893 -0
  12. worktree_controller/delivered_closeout.py +928 -0
  13. worktree_controller/disposable.py +677 -0
  14. worktree_controller/environment.py +1038 -0
  15. worktree_controller/frozen_recovery.py +291 -0
  16. worktree_controller/git_backend.py +682 -0
  17. worktree_controller/governance.py +365 -0
  18. worktree_controller/hardlinks.py +294 -0
  19. worktree_controller/integration_cleanup.py +92 -0
  20. worktree_controller/integration_conflict.py +117 -0
  21. worktree_controller/integration_retry.py +526 -0
  22. worktree_controller/keyed_document_scope.py +162 -0
  23. worktree_controller/lifecycle.py +8992 -0
  24. worktree_controller/lifecycle_history.py +358 -0
  25. worktree_controller/lifecycle_observation.py +234 -0
  26. worktree_controller/metadata_codec.py +1694 -0
  27. worktree_controller/metadata_relocation.py +131 -0
  28. worktree_controller/models.py +546 -0
  29. worktree_controller/mount_diagnostics.py +324 -0
  30. worktree_controller/multiple_cleanup_recovery.py +730 -0
  31. worktree_controller/namespace_migration.py +1412 -0
  32. worktree_controller/native_session_recovery.py +171 -0
  33. worktree_controller/owner_actions.py +34 -0
  34. worktree_controller/policy.py +333 -0
  35. worktree_controller/preservation.py +983 -0
  36. worktree_controller/preserved_cleanup.py +849 -0
  37. worktree_controller/publication_execution.py +361 -0
  38. worktree_controller/reconciler.py +430 -0
  39. worktree_controller/recovery_guidance.py +205 -0
  40. worktree_controller/registration_backfill.py +166 -0
  41. worktree_controller/registration_prune.py +391 -0
  42. worktree_controller/rendering.py +179 -0
  43. worktree_controller/resource_admission.py +1736 -0
  44. worktree_controller/retirement_adapters.py +130 -0
  45. worktree_controller/retirement_execution.py +234 -0
  46. worktree_controller/semantics.py +459 -0
  47. worktree_controller/session.py +505 -0
  48. worktree_controller/session_admission.py +265 -0
  49. worktree_controller/source_cleanup.py +515 -0
  50. worktree_controller/source_preservation.py +692 -0
  51. worktree_controller/source_recovery.py +592 -0
  52. worktree_controller/store.py +1228 -0
  53. worktree_controller/tool_cli_contract.json +1791 -0
  54. worktree_controller/transaction_scope.py +876 -0
  55. worktree_controller/writer_lease.py +662 -0
  56. worktree_controller/writer_session.py +887 -0
  57. worktree_controller-1.30.4.dist-info/METADATA +3149 -0
  58. worktree_controller-1.30.4.dist-info/RECORD +61 -0
  59. worktree_controller-1.30.4.dist-info/WHEEL +4 -0
  60. worktree_controller-1.30.4.dist-info/entry_points.txt +5 -0
  61. worktree_controller-1.30.4.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,849 @@
1
+ """Exact immutable preservation receipt verification and FD-bound legacy deletion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ import stat
10
+ import uuid
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from . import preservation as p
15
+ from .disposable import _delete_fd_tree, _fd_inventory, _unlink_leaf
16
+ from .git_backend import head_paths, path_is_ignored, tracked_paths
17
+ from .models import ControllerError, MetadataRecord
18
+
19
+ CONTRACT = "legacy-preserved-state-cleanup/v1"
20
+ MAX_EVIDENCE_BYTES = 64 * 1024 * 1024
21
+
22
+
23
+ def _blocked(message: str) -> ControllerError:
24
+ return ControllerError("PRESERVATION_COVERAGE_INVALID", message)
25
+
26
+
27
+ def _document(directory: p._Directory, name: str) -> tuple[dict[str, Any], str]:
28
+ metadata = os.stat(name, dir_fd=directory.fd, follow_symlinks=False)
29
+ if metadata.st_size > MAX_EVIDENCE_BYTES:
30
+ raise _blocked("preservation evidence exceeds its bound")
31
+ with (
32
+ p._regular_fd(directory.fd, name, p._identity(metadata)) as fd,
33
+ os.fdopen(os.dup(fd), "rb") as stream,
34
+ ):
35
+ raw = stream.read(MAX_EVIDENCE_BYTES + 1)
36
+ if len(raw) > MAX_EVIDENCE_BYTES:
37
+ raise _blocked("preservation evidence exceeds its bound")
38
+ directory.check()
39
+ value = json.loads(raw)
40
+ if not isinstance(value, dict):
41
+ raise _blocked("invalid preservation document")
42
+ return value, hashlib.sha256(raw).hexdigest()
43
+
44
+
45
+ def coverage(
46
+ receipt_path: Path,
47
+ receipt_sha256: str,
48
+ *,
49
+ identity: dict[str, Any] | None = None,
50
+ verify_source: bool = True,
51
+ source_topology: dict[str, Any] | None = None,
52
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
53
+ """Verify raw evidence hashes and both trees; never changes Controller metadata."""
54
+ try:
55
+ return _coverage(
56
+ receipt_path,
57
+ receipt_sha256,
58
+ identity=identity,
59
+ verify_source=verify_source,
60
+ source_topology=source_topology,
61
+ )
62
+ except ControllerError:
63
+ raise
64
+ except (OSError, ValueError, TypeError, KeyError) as exc:
65
+ raise _blocked("preservation evidence is unavailable, malformed or changed") from exc
66
+
67
+
68
+ def _coverage(
69
+ receipt_path: Path,
70
+ receipt_sha256: str,
71
+ *,
72
+ identity: dict[str, Any] | None,
73
+ verify_source: bool,
74
+ source_topology: dict[str, Any] | None,
75
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
76
+ if not re.fullmatch(r"[0-9a-f]{64}", receipt_sha256 or ""):
77
+ raise _blocked("an explicit trusted preservation receipt SHA-256 is required")
78
+ if not receipt_path.is_absolute() or receipt_path.name != "receipt.json":
79
+ raise _blocked("canonical preservation receipt path required")
80
+ generation = receipt_path.parent
81
+ with p._absolute_directory(generation) as chain:
82
+ directory = chain[-1]
83
+ receipt, actual_receipt_sha = _document(directory, "receipt.json")
84
+ if actual_receipt_sha != receipt_sha256:
85
+ raise _blocked("preservation receipt bytes changed")
86
+ plan, _ = _document(directory, "plan.json")
87
+ manifest, manifest_sha = _document(directory, "manifest.json")
88
+ bound = plan["authority"]["identity"]
89
+ if identity is not None and bound != identity:
90
+ raise _blocked("preservation belongs to another allocation/worktree identity")
91
+ for value in (bound["repository_id"], bound["allocation_id"], plan["generation"]):
92
+ if str(uuid.UUID(value)) != value:
93
+ raise _blocked("noncanonical preservation identity")
94
+ expected = (
95
+ p.evidence_root() / bound["repository_id"] / bound["allocation_id"] / plan["generation"]
96
+ )
97
+ source_rel = plan.get("source_relative") or (
98
+ str(Path(plan["source"]).relative_to(Path(bound["target_path"])))
99
+ if "source" in plan
100
+ else p.SOURCE
101
+ )
102
+ source_rel = p.source_relative_path(source_rel)
103
+ source = Path(bound["target_path"]) / source_rel
104
+ if (
105
+ generation != expected
106
+ or generation.is_relative_to(Path(bound["target_path"]))
107
+ or plan["contract"] != p.CONTRACT
108
+ or plan["ready"] is not True
109
+ or plan["state_root"] != str(p.evidence_root())
110
+ or plan["source"] != str(source)
111
+ or plan["destination"] != str(generation)
112
+ or p._sha({k: v for k, v in plan.items() if k != "plan_sha256"}) != plan["plan_sha256"]
113
+ or receipt.get("contract") != p.CONTRACT
114
+ or receipt.get("verification") != "PASS"
115
+ or receipt.get("result") != "LEGACY_PRESERVATION_COMPLETE"
116
+ or receipt.get("receipt_id") != plan["generation"]
117
+ or receipt.get("allocation_source_identity") != bound
118
+ or receipt.get("source") != str(source)
119
+ or receipt.get("source_anchor") != plan["source_anchor"]
120
+ or receipt.get("generation") != str(generation)
121
+ or receipt.get("generation_identity") != directory.identity
122
+ or receipt.get("destination") != str(generation / "tree")
123
+ or receipt.get("manifest_sha256") != manifest_sha
124
+ or receipt.get("plan_sha256") != plan["plan_sha256"]
125
+ or manifest != plan["source_tree"]
126
+ or manifest.get("errors")
127
+ or receipt.get("inventory") != manifest["inventory"]
128
+ or receipt.get("source_tree_sha256") != manifest["tree_sha256"]
129
+ or receipt.get("preserved_tree_sha256") != manifest["tree_sha256"]
130
+ or receipt.get("source_identity_sha256") != p._sha(manifest["source_identities"])
131
+ ):
132
+ raise _blocked("receipt, manifest, generation or source binding differs")
133
+ p._destination_anchor(chain, plan["destination_anchor"])
134
+ if "cleanup_topology" in plan:
135
+ from .hardlinks import validate
136
+
137
+ validate(plan["cleanup_topology"])
138
+ if receipt.get("cleanup_topology_sha256") != plan["cleanup_topology"]["sha256"]:
139
+ raise _blocked("preservation topology evidence changed")
140
+ with p._open_node(directory.fd, "tree") as tree:
141
+ preserved = p.scan_tree(tree, (*chain, tree))
142
+ p._same_tree(preserved, manifest)
143
+ if p._sha(preserved.identities) != receipt["destination_identity_sha256"]:
144
+ raise _blocked("preserved node identity changed")
145
+ if verify_source:
146
+ worktree_root = Path(bound["target_path"])
147
+ if tracked_paths(worktree_root, source_rel) or head_paths(worktree_root, source_rel):
148
+ raise _blocked("preserved source contains tracked/staged files")
149
+ if not path_is_ignored(worktree_root, source_rel):
150
+ raise _blocked("preserved source is not ignored legacy state")
151
+ with p._absolute_source(source) as source_chain:
152
+ p._source_anchor(source_chain, plan)
153
+ expected = manifest
154
+ if source_topology is not None:
155
+ from .hardlinks import source_baseline
156
+
157
+ expected = source_baseline(manifest, source_topology, resource=source_rel)
158
+ p._same_source(p.scan_tree(source_chain[-1], source_chain), expected)
159
+ evidence = {
160
+ "contract": CONTRACT,
161
+ "receipt_path": str(receipt_path),
162
+ "receipt_sha256": receipt_sha256,
163
+ "manifest_sha256": manifest_sha,
164
+ "preservation_plan_sha256": plan["plan_sha256"],
165
+ "generation": str(generation),
166
+ "generation_identity": directory.identity,
167
+ "tree_sha256": manifest["tree_sha256"],
168
+ "source_anchor": plan["source_anchor"],
169
+ "source_identity_sha256": receipt["source_identity_sha256"],
170
+ "destination_identity_sha256": receipt["destination_identity_sha256"],
171
+ "inventory": manifest["inventory"],
172
+ "allocation_source_identity": bound,
173
+ }
174
+ return evidence, plan
175
+
176
+
177
+ def completed_actions(
178
+ store, transaction: str, proofs: tuple[dict[str, Any], ...],
179
+ *, initial_topology: dict[str, Any], cleanup_paths: list[str],
180
+ ) -> tuple[set[str], dict[str, Any]]:
181
+ from . import cleanup_evidence as e
182
+ from . import hardlinks
183
+
184
+ if str(uuid.UUID(transaction)) != transaction:
185
+ raise _blocked("canonical cleanup transaction identity required")
186
+ if [proof["resource"] for proof in proofs] != cleanup_paths[:len(proofs)]:
187
+ raise _blocked("cleanup action proofs differ from the exact ordered plan")
188
+ hardlinks.validate(initial_topology)
189
+ if initial_topology["resources"] != cleanup_paths:
190
+ raise _blocked("cleanup topology does not cover every planned resource")
191
+ completed: set[str] = set()
192
+ preceding = initial_topology
193
+ for step, proof in enumerate(proofs):
194
+ resource = p.source_relative_path(proof["resource"])
195
+ if resource in completed:
196
+ raise _blocked("duplicate cleanup action proof")
197
+ directory = store.cleanup_actions_dir / transaction
198
+ if not e.matches_reference(proof["before"], directory / f"{step:04d}-before.json") or not e.matches_reference(
199
+ proof["after"], directory / f"{step:04d}-after.json"
200
+ ):
201
+ raise _blocked("cleanup proof path differs from its exact transaction step")
202
+ before, _ = e.read(Path(proof["before"]["path"]), proof["before"]["sha256"])
203
+ after, _ = e.read(Path(proof["after"]["path"]), proof["after"]["sha256"])
204
+ if (
205
+ before.get("phase") != "BEFORE" or before.get("execution_state") != "ACTION_STARTED"
206
+ or after.get("phase") != "COMPLETED" or after.get("execution_state") != "ACTION_COMPLETED"
207
+ or before.get("resource") != resource or after.get("resource") != resource
208
+ or after.get("before") != proof["before"] or before["topology"] != preceding
209
+ ):
210
+ raise _blocked("cleanup action evidence does not prove exact completion")
211
+ mutation = hardlinks.transition(
212
+ before["topology"], after["topology"], resource,
213
+ after["mutation"].get("external_survivors"),
214
+ )
215
+ if after["mutation"] != mutation or before.get("contract") != mutation["contract"] or after.get("contract") != mutation["contract"]:
216
+ raise _blocked("cleanup transition evidence changed")
217
+ preceding = after["topology"]
218
+ completed.add(resource)
219
+ return completed, preceding
220
+
221
+
222
+ def common_source_root(plans: list[dict[str, Any]], target: Path) -> dict[str, int]:
223
+ """Bind cleanup to an original root pin, including compatible old nested proofs."""
224
+ pins = []
225
+ for plan in plans:
226
+ relative = p.source_relative_path(Path(plan["source"]).relative_to(target).as_posix())
227
+ ancestors = plan.get("source_ancestor_identities")
228
+ if ancestors is not None:
229
+ pin = ancestors.get(str(target))
230
+ if pin is None:
231
+ raise _blocked("nested preservation has no original checkout-root pin")
232
+ elif len(Path(relative).parts) == 1:
233
+ pin = plan["source_anchor"]["worktree"]
234
+ else:
235
+ continue
236
+ if set(pin) != {"device", "inode", "file_type"} or pin["file_type"] != stat.S_IFDIR:
237
+ raise _blocked("invalid original checkout-root identity")
238
+ pins.append(pin)
239
+ if not pins or any(pin != pins[0] for pin in pins):
240
+ raise _blocked("all preservation generations require one original checkout-root identity")
241
+ with p._absolute_directory(target) as chain:
242
+ if chain[-1].identity != pins[0]:
243
+ raise _blocked("checkout root differs from the preserved original identity")
244
+ return pins[0]
245
+
246
+
247
+ def source_absent(plan: dict[str, Any], target: Path) -> bool:
248
+ relative = p.source_relative_path(Path(plan["source"]).relative_to(target).as_posix())
249
+ source = target / relative
250
+ with p._absolute_directory(source.parent) as chain:
251
+ if chain[-1].identity != plan["source_anchor"]["worktree"]:
252
+ raise _blocked("original source parent identity changed")
253
+ if "source_ancestor_identities" in plan and p._chain_identities(chain) != plan["source_ancestor_identities"]:
254
+ raise _blocked("original source ancestor chain changed")
255
+ try:
256
+ os.stat(source.name, dir_fd=chain[-1].fd, follow_symlinks=False)
257
+ except FileNotFoundError:
258
+ return True
259
+ return False
260
+
261
+
262
+ def _source_key(plan: dict[str, Any]) -> str:
263
+ return p._sha({
264
+ "identity": plan["authority"]["identity"],
265
+ **{key: plan[key] for key in ("source", "source_anchor", "source_tree")},
266
+ })
267
+
268
+
269
+ def _reopened_source_identity(
270
+ previous: dict[str, Any], current: dict[str, Any], target: Path,
271
+ record: MetadataRecord | None,
272
+ ) -> bool:
273
+ from .git_backend import inventory, is_ancestor, resolve_commit
274
+
275
+ stable = {"repository_id", "allocation_id", "target_path", "owner", "authority_key"}
276
+ if (
277
+ record is None or set(previous) != set(current)
278
+ or set(current) != stable | {"expected_head", "expected_record_version"}
279
+ or any(previous[key] != current[key] for key in stable)
280
+ or current["target_path"] != str(target)
281
+ or current["allocation_id"] != record.allocation_id
282
+ or current["owner"] != record.owner or current["authority_key"] != record.authority_key
283
+ or current["expected_record_version"] != record.record_version
284
+ or record.record_version != len(record.history)
285
+ or record.role not in {"feature", "audit", "hotfix"}
286
+ or not isinstance(previous["expected_record_version"], int)
287
+ or isinstance(previous["expected_record_version"], bool)
288
+ or not 1 <= previous["expected_record_version"] < record.record_version
289
+ or record.lifecycle not in {"HANDOFF_READY", "PRESERVED"}
290
+ or not record.finalizer_record_id
291
+ or any(oid != current["expected_head"] for oid in (
292
+ record.handoff_commit, record.task_commit, record.finalizer_commit,
293
+ ))
294
+ ):
295
+ return False
296
+ # Supported records start with v1/history[0]; every official update appends
297
+ # exactly one event and advances CAS by one. Unknown/shortened history cannot
298
+ # establish which frozen allocation version a historical copy belonged to.
299
+ state = None
300
+ for event in record.history:
301
+ if event.get("from") != state or not event.get("to"):
302
+ return False
303
+ state = event["to"]
304
+ if state != record.lifecycle:
305
+ return False
306
+ old_version = previous["expected_record_version"]
307
+ state = record.history[old_version - 1]["to"]
308
+ if state not in {"HANDOFF_READY", "PRESERVED"}:
309
+ return False
310
+ opened = False
311
+ completed_cycle = False
312
+ for event in record.history[old_version:]:
313
+ if event.get("owner") != record.owner:
314
+ return False
315
+ kind, next_state = event.get("event"), event["to"]
316
+ if kind == "REOPENED" and state in {"HANDOFF_READY", "PRESERVED"} and next_state == "ACTIVE":
317
+ opened = True
318
+ elif (
319
+ opened and state == "ACTIVE"
320
+ and (kind, next_state) in {
321
+ ("HANDOFF_INTEGRATION_READY", "HANDOFF_READY"),
322
+ ("HANDOFF_PRESERVED", "PRESERVED"),
323
+ }
324
+ ):
325
+ opened = False
326
+ completed_cycle = True
327
+ elif kind not in {"REVIEWED", "SCOPE_AMENDED", "DISPOSABLE_REGISTERED"} or next_state != state:
328
+ return False
329
+ state = next_state
330
+ if opened or not completed_cycle or record.history[old_version - 1].get("owner") != record.owner:
331
+ return False
332
+ repository = inventory(target)
333
+ item = next((row for row in repository.worktrees if row.path == target), None)
334
+ return bool(
335
+ item is not None and not item.detached and item.branch
336
+ and item.head == current["expected_head"]
337
+ and resolve_commit(target, f"refs/heads/{item.branch}") == item.head
338
+ and resolve_commit(target, previous["expected_head"]) == previous["expected_head"]
339
+ and record.base_commit
340
+ and is_ancestor(target, record.base_commit, previous["expected_head"])
341
+ and is_ancestor(target, previous["expected_head"], current["expected_head"])
342
+ )
343
+
344
+
345
+ def _same_completed_source(
346
+ first: dict[str, Any], second: dict[str, Any], target: Path,
347
+ *, record: MetadataRecord | None = None,
348
+ ) -> bool:
349
+ # Only an actual completed action supplies the second plan. A rebuilt source
350
+ # node may supersede an older copy only across verified reopen/handoff history.
351
+ if (
352
+ first["source"] != second["source"]
353
+ or first.get("source_ancestor_identities") != second.get("source_ancestor_identities")
354
+ ):
355
+ return False
356
+ same_identity = first["authority"]["identity"] == second["authority"]["identity"]
357
+ if same_identity:
358
+ if first["source_anchor"] != second["source_anchor"]:
359
+ return False
360
+ elif (
361
+ first["source_anchor"]["worktree"] != second["source_anchor"]["worktree"]
362
+ or any(first["source_anchor"]["source"][key] != second["source_anchor"]["source"][key]
363
+ for key in ("device", "file_type"))
364
+ or not _reopened_source_identity(
365
+ first["authority"]["identity"], second["authority"]["identity"], target, record,
366
+ )
367
+ ):
368
+ return False
369
+ common_source_root([first, second], target)
370
+ return True
371
+
372
+
373
+ def _resumed_cleanup_identity(
374
+ previous: dict[str, Any], current: dict[str, Any], record: MetadataRecord | None,
375
+ ) -> bool:
376
+ """Reuse completed cleanup only after a same-head retained/resumed handoff.
377
+
378
+ The receipt keeps its original CAS. This proves a narrow official history
379
+ suffix, not a general exemption for older receipts or changed HEADs.
380
+ """
381
+ stable = {"repository_id", "allocation_id", "target_path", "expected_head", "owner", "authority_key"}
382
+ old = previous.get("expected_record_version")
383
+ if (
384
+ record is None or set(previous) != stable | {"expected_record_version"}
385
+ or set(current) != set(previous)
386
+ or any(previous[key] != current[key] for key in stable)
387
+ or not isinstance(old, int) or isinstance(old, bool)
388
+ or not 1 <= old < record.record_version
389
+ or current["expected_record_version"] != record.record_version
390
+ or record.record_version != len(record.history)
391
+ or record.role not in {"feature", "audit"}
392
+ or record.lifecycle != "HANDOFF_READY"
393
+ or record.owner != current["owner"] or record.authority_key != current["authority_key"]
394
+ or record.allocation_id != current["allocation_id"]
395
+ or not record.finalizer_record_id
396
+ or any(oid != current["expected_head"] for oid in (
397
+ record.task_commit, record.handoff_commit, record.finalizer_commit,
398
+ ))
399
+ ):
400
+ return False
401
+ state = None
402
+ for event in record.history[:old]:
403
+ if event.get("from") != state or not event.get("to") or event.get("owner") != record.owner:
404
+ return False
405
+ state = event["to"]
406
+ if state != "RETAINED":
407
+ return False
408
+ transitions = {
409
+ ("RESUMED", "RETAINED", "ACTIVE"),
410
+ ("PARKED", "ACTIVE", "RETAINED"),
411
+ ("HANDOFF_INTEGRATION_READY", "ACTIVE", "HANDOFF_READY"),
412
+ }
413
+ for event in record.history[old:]:
414
+ if (
415
+ event.get("from") != state or event.get("owner") != record.owner
416
+ or (event.get("event"), state, event.get("to")) not in transitions
417
+ ):
418
+ return False
419
+ state = event["to"]
420
+ return state == record.lifecycle
421
+
422
+
423
+ def _require_completion_coverage(
424
+ bound: dict[str, Any], plans: list[dict[str, Any]], *, record: MetadataRecord | None = None,
425
+ ) -> None:
426
+ """A completed copy with an absent source requires actual action evidence.
427
+
428
+ Inspect only this allocation's official generation metadata. Unreferenced
429
+ generation payloads are not opened, and source absence grants no authority.
430
+ """
431
+ from .source_preservation import CONTRACT as SOURCE_CONTRACT
432
+
433
+ allocation = p.evidence_root() / bound["repository_id"] / bound["allocation_id"]
434
+ if not os.path.lexists(allocation):
435
+ return
436
+ if any(
437
+ plan["authority"]["identity"] != bound
438
+ and not _resumed_cleanup_identity(plan["authority"]["identity"], bound, record)
439
+ for plan in plans
440
+ ):
441
+ raise _blocked("completed preservation belongs to another exact allocation identity")
442
+ covered = {_source_key(plan) for plan in plans}
443
+ with p._absolute_directory(allocation) as chain:
444
+ for name in sorted(os.listdir(chain[-1].fd)):
445
+ try:
446
+ if str(uuid.UUID(name)) != name:
447
+ raise _blocked("allocation contains a noncanonical generation entry")
448
+ except ValueError as exc:
449
+ raise _blocked("allocation contains a noncanonical generation entry") from exc
450
+ with p._absolute_directory(allocation / name) as generation:
451
+ directory = generation[-1]
452
+ try:
453
+ os.stat("receipt.json", dir_fd=directory.fd, follow_symlinks=False)
454
+ except FileNotFoundError:
455
+ continue
456
+ receipt, _raw_sha = _document(directory, "receipt.json")
457
+ plan, _ = _document(directory, "plan.json")
458
+ if (
459
+ receipt.get("contract") not in {p.CONTRACT, SOURCE_CONTRACT}
460
+ or receipt.get("contract") != plan.get("contract")
461
+ or receipt.get("verification") != "PASS" or receipt.get("receipt_id") != name
462
+ or plan.get("generation") != name
463
+ or receipt.get("generation") != str(allocation / name)
464
+ or receipt.get("generation_identity") != directory.identity
465
+ or receipt.get("allocation_source_identity") != plan["authority"]["identity"]
466
+ or plan.get("state_root") != str(p.evidence_root())
467
+ or plan.get("destination") != str(allocation / name)
468
+ or receipt.get("plan_sha256") != plan.get("plan_sha256")
469
+ or p._sha({k: v for k, v in plan.items() if k != "plan_sha256"}) != plan["plan_sha256"]
470
+ or plan["authority"]["identity"].get("repository_id") != bound["repository_id"]
471
+ or plan["authority"]["identity"].get("allocation_id") != bound["allocation_id"]
472
+ or plan["authority"]["identity"].get("target_path") != bound["target_path"]
473
+ ):
474
+ raise _blocked("allocation generation metadata changed")
475
+ if plan["contract"] == SOURCE_CONTRACT:
476
+ tree = plan["source_tree"]
477
+ portable = {key: tree[key] for key in (
478
+ "base_head", "base_tree", "dirty_paths", "files", "reconstructed_tree_oid",
479
+ )}
480
+ if (
481
+ receipt.get("result") != "SOURCE_PRESERVATION_COMPLETE"
482
+ or plan.get("cleanup_authorized") is not False
483
+ or receipt.get("cleanup_authorized") is not False
484
+ or any(key in plan or key in receipt for key in (
485
+ "source", "source_relative", "source_anchor", "source_ancestor_identities",
486
+ ))
487
+ or tree.get("base_head") != plan["authority"]["identity"]["expected_head"]
488
+ or not isinstance(tree.get("files"), list)
489
+ or tree.get("dirty_paths") != [row["path"] for row in tree["files"]]
490
+ or any(key in tree for key in ("manifest", "inventory", "errors"))
491
+ or tree["tree_sha256"] != p._sha(portable)
492
+ or receipt.get("tree_sha256") != tree["tree_sha256"]
493
+ or receipt.get("reconstructed_tree_oid") != tree["reconstructed_tree_oid"]
494
+ ):
495
+ raise _blocked("tracked-source generation metadata changed")
496
+ # Tracked-source restoration has its own completed-action verifier.
497
+ # Its copy grants no ignored-resource cleanup authority.
498
+ continue
499
+ tree = plan["source_tree"]
500
+ relative = p.source_relative_path(Path(plan["source"]).relative_to(Path(bound["target_path"])).as_posix())
501
+ if (
502
+ receipt.get("result") != "LEGACY_PRESERVATION_COMPLETE" or plan.get("ready") is not True
503
+ or plan.get("source_relative", relative) != relative
504
+ or receipt.get("source") != plan["source"] or receipt.get("source_anchor") != plan["source_anchor"]
505
+ or receipt.get("destination") != str(allocation / name / "tree")
506
+ or tree.get("errors") != [] or not isinstance(tree.get("manifest"), list)
507
+ or receipt.get("inventory") != tree["inventory"]
508
+ or receipt.get("source_tree_sha256") != tree["tree_sha256"]
509
+ or receipt.get("preserved_tree_sha256") != tree["tree_sha256"]
510
+ or receipt.get("source_identity_sha256") != p._sha(tree["source_identities"])
511
+ or tree["tree_sha256"] != p._sha([
512
+ {key: value for key, value in row.items() if key != "ownership"}
513
+ for row in tree["manifest"]
514
+ ])
515
+ ):
516
+ raise _blocked("legacy generation metadata changed")
517
+ if source_absent(plan, Path(bound["target_path"])) and (
518
+ not (plan["authority"]["identity"] == bound and _source_key(plan) in covered)
519
+ and not any(_same_completed_source(
520
+ plan, completed, Path(bound["target_path"]), record=record,
521
+ ) for completed in plans)
522
+ ):
523
+ raise _blocked("absent preserved source has no actual completed action evidence")
524
+
525
+
526
+ def _pending_preservation_plans(
527
+ store, item, bound: dict[str, Any], transaction: str, cleanup_recovery: dict[str, Any] | None,
528
+ ) -> list[dict[str, Any]]:
529
+ from . import cleanup_evidence as e
530
+ from .lifecycle import _release_plan_id
531
+
532
+ if str(uuid.UUID(transaction)) != transaction:
533
+ raise _blocked("canonical cleanup transaction identity required")
534
+ journal = store._read_json(store.transactions_dir / f"{transaction}.json")
535
+ plan = journal["cleanup_plan"]
536
+ completed = [row["path"] for row in journal["actions"] if row["status"] == "COMPLETED"]
537
+ if (
538
+ journal.get("transaction_id") != transaction or journal.get("command") != "resource-clean"
539
+ or journal.get("phase") != "PREPARED" or journal.get("branch") != item.branch
540
+ or any(journal.get(key) != bound[key] for key in (
541
+ "repository_id", "allocation_id", "target_path", "expected_head", "expected_record_version",
542
+ ))
543
+ or any(plan.get(key) != value for key, value in bound.items())
544
+ or plan.get("plan_id") != _release_plan_id({k: v for k, v in plan.items() if k != "plan_id"})
545
+ or [row["path"] for row in journal["actions"]] != plan["cleanup_paths"]
546
+ or completed != plan["cleanup_paths"][:len(completed)]
547
+ or (cleanup_recovery is not None and plan.get("recovery") != cleanup_recovery)
548
+ ):
549
+ raise _blocked("in-progress cleanup identity or action prefix changed")
550
+ proofs = []
551
+ for step, relative in enumerate(completed):
552
+ path = store.cleanup_actions_dir / transaction / f"{step:04d}-after.json"
553
+ after, digest = e.read(path)
554
+ proofs.append({
555
+ "resource": relative, "before": after["before"],
556
+ "after": {"path": str(path), "sha256": digest},
557
+ })
558
+ done, _ = completed_actions(
559
+ store, transaction, tuple(proofs), initial_topology=plan["hardlink_topology"], cleanup_paths=plan["cleanup_paths"],
560
+ )
561
+ evidence_set = plan.get("preservation_evidences")
562
+ if evidence_set is None:
563
+ evidence_set = [{"evidence": plan["preservation_evidence"]}] if plan.get("preservation_evidence") else []
564
+ preserved_plans = []
565
+ completed_plans = []
566
+ for entry in evidence_set:
567
+ evidence = entry["evidence"]
568
+ verified, preserved = coverage(
569
+ Path(evidence["receipt_path"]), evidence["receipt_sha256"], identity=bound, verify_source=False,
570
+ )
571
+ if verified != evidence:
572
+ raise _blocked("in-progress preservation generation changed")
573
+ preserved_plans.append(preserved)
574
+ relative = p.source_relative_path(Path(preserved["source"]).relative_to(item.path).as_posix())
575
+ if relative in done:
576
+ if not source_absent(preserved, item.path):
577
+ raise _blocked("completed preserved source reappeared")
578
+ completed_plans.append(preserved)
579
+ if preserved_plans:
580
+ common_source_root(preserved_plans, item.path)
581
+ return completed_plans
582
+
583
+
584
+ def cleanup_receipts(
585
+ store, item, record, repository_id: str | None, *,
586
+ expected_refs: list[dict[str, str]] | None = None,
587
+ cleanup_recovery: dict[str, Any] | None = None,
588
+ cleanup_transaction_id: str | None = None,
589
+ allow_pending_typed_no_action: bool = False,
590
+ include_no_action_refs: bool = False,
591
+ ) -> list[dict[str, str]]:
592
+ """Reverify exact-CAS completion bytes, action chain and preservation generations.
593
+
594
+ Verified zero-effect refusals are included only for the release core's
595
+ writer/runtime guard; archive consumers freeze successful completions only.
596
+ """
597
+ from . import cleanup_evidence as e
598
+ from . import hardlinks
599
+ from .lifecycle import (
600
+ MULTIPLE_PRESERVED_CLEANUP_CONTRACT,
601
+ TYPED_RESOURCE_CONTRACT,
602
+ _preservation_source_relative,
603
+ _release_plan_id,
604
+ _validate_multiple_preservation_cleanup,
605
+ )
606
+
607
+ bound = {
608
+ "repository_id": repository_id, "allocation_id": record.allocation_id,
609
+ "target_path": str(item.path), "expected_head": item.head,
610
+ "expected_record_version": record.record_version,
611
+ "owner": record.owner, "authority_key": record.authority_key,
612
+ }
613
+ refs = []
614
+ completed_preservation_plans = []
615
+ try:
616
+ if cleanup_recovery is not None:
617
+ from .cleanup_recovery import original
618
+
619
+ recovered_receipt, recovered_plan = original(
620
+ store, cleanup_recovery, target=item.path, allocation_id=record.allocation_id,
621
+ )
622
+ if (
623
+ any(recovered_receipt["plan"].get(key) != value for key, value in bound.items())
624
+ or recovered_plan["authority"]["identity"] != bound
625
+ ):
626
+ raise _blocked("original recovery identity or exact CAS changed")
627
+ from . import multiple_cleanup_recovery as multi_recovery
628
+
629
+ if cleanup_recovery.get("contract") == multi_recovery.CONTRACT:
630
+ _, preserved_plans, original_completed, _ = multi_recovery.details(
631
+ store, cleanup_recovery
632
+ )
633
+ completed_preservation_plans.extend(
634
+ plan for plan in preserved_plans
635
+ if _preservation_source_relative(plan, item.path) in original_completed
636
+ )
637
+ for path in sorted(store.completion_receipts_dir.glob("*.json")):
638
+ with p._absolute_directory(path.parent) as chain:
639
+ receipt, raw_sha = _document(chain[-1], path.name)
640
+ plan = receipt.get("plan", {})
641
+ if plan.get("allocation_id") != record.allocation_id:
642
+ continue
643
+ if plan.get("integration_abort_evidence") is not None:
644
+ from .integration_cleanup import abort_evidence
645
+
646
+ if abort_evidence(store, store._load_unlocked(), item, record) != plan["integration_abort_evidence"]:
647
+ raise _blocked("integration abort evidence changed after cleanup")
648
+ if cleanup_recovery is not None and path.stem == cleanup_recovery["original_transaction_id"]:
649
+ continue
650
+ if "preservation_evidences" in plan and (
651
+ receipt.get("contract") != MULTIPLE_PRESERVED_CLEANUP_CONTRACT
652
+ or receipt.get("result") != "CLEANED"
653
+ ):
654
+ from . import multiple_cleanup_recovery as multi_recovery
655
+
656
+ if multi_recovery.completed(store, path.stem, verify_generations=True):
657
+ completed_preservation_plans.extend(
658
+ multi_recovery.verified_completion(store, path.stem, bound=bound)
659
+ )
660
+ continue
661
+ raise _blocked("multiple cleanup is incomplete or downgraded")
662
+ if receipt.get("contract") not in {
663
+ CONTRACT, MULTIPLE_PRESERVED_CLEANUP_CONTRACT, TYPED_RESOURCE_CONTRACT,
664
+ }:
665
+ continue
666
+ from . import multiple_cleanup_recovery as multi_recovery
667
+
668
+ if plan.get("recovery", {}).get("contract") == multi_recovery.CONTRACT:
669
+ completed_preservation_plans.extend(
670
+ multi_recovery.recovered_receipt(store, receipt, bound)
671
+ )
672
+ refs.append({"receipt_id": path.stem, "path": str(path), "sha256": raw_sha})
673
+ continue
674
+ if receipt.get("result") == "FAILED_OR_PARTIAL":
675
+ from .cleanup_recovery import completed
676
+
677
+ if receipt.get("contract") == TYPED_RESOURCE_CONTRACT:
678
+ from .cleanup_attempts import _typed_journal, typed_terminal
679
+
680
+ if typed_terminal(store, path.stem) or allow_pending_typed_no_action:
681
+ journal, digest = e.read(store.transactions_dir / path.name)
682
+ verified_journal = _typed_journal(store, path.stem, digest)
683
+ if (
684
+ journal != verified_journal or journal["cleanup_plan"] != plan
685
+ or any(plan.get(key) != value for key, value in bound.items())
686
+ or plan.get("blockers") != []
687
+ or _release_plan_id({k: v for k, v in plan.items() if k != "plan_id"}) != plan.get("plan_id")
688
+ or plan.get("preservation_evidence") is not None or "preservation_evidences" in plan
689
+ or any(row.get("cleanup_action") == "remove-preserved-tree"
690
+ or row.get("classification") == "exact-preserved" for row in plan["resources"])
691
+ ):
692
+ raise _blocked("typed no-action refusal identity or proof shape changed")
693
+ if include_no_action_refs:
694
+ refs.append({"receipt_id": path.stem, "path": str(path), "sha256": raw_sha})
695
+ continue
696
+ if receipt.get("contract") == CONTRACT and completed(store, path.stem, verify_generations=True):
697
+ evidence = plan["preservation_evidence"]
698
+ verified, preserved = coverage(
699
+ Path(evidence["receipt_path"]), evidence["receipt_sha256"],
700
+ identity=bound, verify_source=False,
701
+ )
702
+ if verified != evidence:
703
+ raise _blocked("recovered original generation changed")
704
+ completed_preservation_plans.append(preserved)
705
+ continue
706
+ receipt_bound = {key: plan.get(key) for key in bound}
707
+ if receipt_bound != bound and not _resumed_cleanup_identity(receipt_bound, bound, record):
708
+ raise _blocked("cleanup completion identity or exact CAS changed")
709
+ if (
710
+ str(uuid.UUID(path.stem)) != path.stem
711
+ or receipt.get("result") != "CLEANED" or receipt.get("receipt_id") != path.stem
712
+ or plan.get("contract") != receipt["contract"]
713
+ or plan.get("blockers") != []
714
+ or _release_plan_id({k: v for k, v in plan.items() if k != "plan_id"}) != plan.get("plan_id")
715
+ ):
716
+ raise _blocked("cleanup completion identity or exact CAS changed")
717
+ preserved_rows = [
718
+ row for row in plan["resources"]
719
+ if row["cleanup_action"] == "remove-preserved-tree" or row["classification"] == "exact-preserved"
720
+ ]
721
+ expected_contract = (
722
+ MULTIPLE_PRESERVED_CLEANUP_CONTRACT if "preservation_evidences" in plan
723
+ else CONTRACT if plan.get("preservation_evidence") is not None or preserved_rows
724
+ else TYPED_RESOURCE_CONTRACT
725
+ )
726
+ if receipt["contract"] != expected_contract or receipt["post_scan"].get("contract") != expected_contract:
727
+ raise _blocked("cleanup contract differs from its exact preservation proof shape")
728
+ paths = plan["cleanup_paths"]
729
+ if not paths or len(set(paths)) != len(paths):
730
+ raise _blocked("cleanup completion has no exact resources")
731
+ for relative in paths:
732
+ p.source_relative_path(relative)
733
+ planned_paths = [r["path"] for r in plan["resources"] if r["cleanup_action"] == "remove-tree"] + [
734
+ r["path"] for r in plan["resources"] if r["cleanup_action"] == "remove-preserved-tree"
735
+ ]
736
+ proofs = tuple(receipt.get("action_evidence", []))
737
+ if (
738
+ paths != planned_paths
739
+ or receipt.get("actions") != [{"path": name, "status": "COMPLETED"} for name in paths]
740
+ or len(proofs) != len(paths)
741
+ ):
742
+ raise _blocked("cleanup completion has missing action proofs")
743
+ done, topology = completed_actions(
744
+ store, path.stem, proofs, initial_topology=plan["hardlink_topology"], cleanup_paths=paths,
745
+ )
746
+ post = receipt["post_scan"]
747
+ if (
748
+ done != set(paths) or topology != post.get("hardlink_topology")
749
+ or post.get("blockers") != [] or post.get("cleanup_paths") != []
750
+ or any(post.get(key) != value for key, value in receipt_bound.items())
751
+ or _release_plan_id({k: v for k, v in post.items() if k != "plan_id"}) != post.get("plan_id")
752
+ or any(os.path.lexists(item.path / relative) for relative in paths)
753
+ ):
754
+ raise _blocked("cleanup post-scan or completed action chain changed")
755
+ hardlinks.validate(topology)
756
+ preservation_plans = []
757
+ if receipt["contract"] == MULTIPLE_PRESERVED_CLEANUP_CONTRACT:
758
+ preservation_plans = _validate_multiple_preservation_cleanup(receipt, item, receipt_bound)
759
+ elif receipt["contract"] == CONTRACT:
760
+ evidence = plan["preservation_evidence"]
761
+ verified, preserved = coverage(
762
+ Path(evidence["receipt_path"]), evidence["receipt_sha256"],
763
+ identity=receipt_bound, verify_source=False,
764
+ )
765
+ relative = _preservation_source_relative(preserved, item.path)
766
+ resources = [r for r in plan["resources"] if r["cleanup_action"] == "remove-preserved-tree"]
767
+ if (
768
+ verified != evidence or len(resources) != 1
769
+ or resources[0]["path"] != relative or resources[0].get("evidence") != evidence
770
+ or resources[0].get("classification") != "exact-preserved" or relative not in done
771
+ or post.get("preservation_evidence") != evidence
772
+ ):
773
+ raise _blocked("single preservation proof differs from its exact source")
774
+ preservation_plans = [preserved]
775
+ for preserved in preservation_plans:
776
+ if not source_absent(preserved, item.path):
777
+ raise _blocked("completed preserved source reappeared")
778
+ completed_preservation_plans.extend(preservation_plans)
779
+ if preservation_plans:
780
+ original_root = common_source_root(preservation_plans, item.path)
781
+ else:
782
+ with p._absolute_directory(item.path) as chain:
783
+ original_root = chain[-1].identity
784
+ if any(anchor["worktree"] != original_root for anchor in plan["hardlink_topology"]["anchors"].values()):
785
+ raise _blocked("cleanup graph is bound to another checkout root")
786
+ refs.append({"receipt_id": path.stem, "path": str(path), "sha256": raw_sha})
787
+ if cleanup_transaction_id is not None:
788
+ completed_preservation_plans.extend(_pending_preservation_plans(
789
+ store, item, bound, cleanup_transaction_id, cleanup_recovery,
790
+ ))
791
+ _require_completion_coverage(bound, completed_preservation_plans, record=record)
792
+ if expected_refs is not None:
793
+ if not isinstance(expected_refs, list) or len(expected_refs) != len(refs):
794
+ raise _blocked("archive cleanup receipt set changed")
795
+ for frozen, actual in zip(expected_refs, refs, strict=True):
796
+ if (
797
+ set(frozen) != {"receipt_id", "path", "sha256"}
798
+ or frozen["receipt_id"] != actual["receipt_id"] or frozen["sha256"] != actual["sha256"]
799
+ or not e.matches_reference(frozen, Path(actual["path"]))
800
+ ):
801
+ raise _blocked("archive cleanup receipt bytes or exact path changed")
802
+ except ControllerError:
803
+ raise
804
+ except (OSError, ValueError, TypeError, KeyError, IndexError, StopIteration) as exc:
805
+ raise _blocked("cleanup completion evidence is unavailable, malformed or changed") from exc
806
+ return refs
807
+
808
+
809
+ def delete_preserved_state(root: Path, evidence: dict[str, Any]) -> dict[str, Any]:
810
+ from .hardlinks import external_contents, source_baseline
811
+
812
+ topology = evidence.get("source_topology")
813
+ original_evidence = {k: v for k, v in evidence.items() if k != "source_topology"}
814
+ current, plan = coverage(
815
+ Path(evidence["receipt_path"]), evidence["receipt_sha256"],
816
+ identity=evidence["allocation_source_identity"], source_topology=topology,
817
+ )
818
+ source_rel = p.source_relative_path(Path(plan["source"]).relative_to(root).as_posix())
819
+ source_full = root / source_rel
820
+ if current != original_evidence or plan["source"] != str(source_full):
821
+ raise _blocked("cleanup evidence changed")
822
+ with p._absolute_directory(source_full.parent) as ancestors:
823
+ with p._open_node(ancestors[-1].fd, source_full.name) as source:
824
+ chain = (*ancestors, source)
825
+ p._source_anchor(chain, plan)
826
+ expected = (
827
+ source_baseline(plan["source_tree"], topology, resource=source_rel)
828
+ if topology is not None else plan["source_tree"]
829
+ )
830
+ p._same_source(p.scan_tree(source, chain), expected)
831
+ leaf = not stat.S_ISDIR(expected["source_identities"]["."][2])
832
+ if not leaf:
833
+ entries, _ = _fd_inventory(source.fd, preserved_fifo=True)
834
+ if {path: p._identity(entry.metadata) for path, entry in entries.items()} != expected["source_identities"]:
835
+ raise _blocked("source changed after its final content verification")
836
+ return _delete_fd_tree(
837
+ source, ".", entries, chain,
838
+ external_contents=external_contents(topology, source_rel) if topology is not None else None,
839
+ )
840
+ # Close the inspection context before unlinking its name. A fresh O_PATH
841
+ # descriptor binds the exact leaf; all parent FDs remain pinned and checked.
842
+ row = expected["manifest"][0]
843
+ external = external_contents(topology, source_rel).get(".") if topology is not None else None
844
+ proof = _unlink_leaf(
845
+ ancestors[-1].fd, source_full.name, expected["source_identities"]["."],
846
+ link=row.get("target"), chain=ancestors, external=external,
847
+ )
848
+ key = f"{proof['identity'][0]}:{proof['identity'][1]}"
849
+ return {key: proof} if external is not None else {}