gitgrip 1.5.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.
- gitgrip-1.5.0.dist-info/METADATA +13 -0
- gitgrip-1.5.0.dist-info/RECORD +80 -0
- gitgrip-1.5.0.dist-info/WHEEL +5 -0
- gitgrip-1.5.0.dist-info/entry_points.txt +2 -0
- gitgrip-1.5.0.dist-info/top_level.txt +2 -0
- gr2/__init__.py +0 -0
- gr2/overlay/__init__.py +6 -0
- gr2/overlay/activate.py +196 -0
- gr2/overlay/agent_manifest.py +138 -0
- gr2/overlay/cli.py +181 -0
- gr2/overlay/cross_repo.py +124 -0
- gr2/overlay/drivers.py +113 -0
- gr2/overlay/introspection.py +155 -0
- gr2/overlay/language_drivers.py +115 -0
- gr2/overlay/objects.py +412 -0
- gr2/overlay/perf.py +251 -0
- gr2/overlay/refs.py +36 -0
- gr2/overlay/trust.py +150 -0
- gr2/overlay/types.py +69 -0
- gr2/overlay/units.py +313 -0
- gr2/overlay/workspace_spec.py +59 -0
- gr2/prototypes/__init__.py +0 -0
- gr2/prototypes/cache_materialization_probe.py +190 -0
- gr2/prototypes/concurrent_event_stress.py +199 -0
- gr2/prototypes/concurrent_lease_stress.py +240 -0
- gr2/prototypes/concurrent_workspace_cap_stress.py +231 -0
- gr2/prototypes/contribution_protocol.py +665 -0
- gr2/prototypes/cross_mode_lane_stress.py +986 -0
- gr2/prototypes/jsonl_store.py +158 -0
- gr2/prototypes/lane_workspace_prototype.py +2088 -0
- gr2/prototypes/layout_model_probe.py +139 -0
- gr2/prototypes/propagation_daemon.py +546 -0
- gr2/prototypes/propagation_state_machine.py +1478 -0
- gr2/prototypes/python_exec_playground.py +194 -0
- gr2/prototypes/python_hook_runtime_playground.py +240 -0
- gr2/prototypes/python_migration_playground.py +144 -0
- gr2/prototypes/python_review_checkout_playground.py +242 -0
- gr2/prototypes/python_spec_apply_playground.py +282 -0
- gr2/prototypes/real_git_lane_materialization.py +248 -0
- gr2/prototypes/real_git_playground.py +334 -0
- gr2/prototypes/recall_lane_history.py +274 -0
- gr2/prototypes/repo_maintenance_prototype.py +659 -0
- gr2/prototypes/repo_transport_probe.py +147 -0
- gr2/python_cli/__init__.py +2 -0
- gr2/python_cli/__main__.py +6 -0
- gr2/python_cli/add.py +51 -0
- gr2/python_cli/app.py +2516 -0
- gr2/python_cli/branch.py +67 -0
- gr2/python_cli/channel_bridge.py +131 -0
- gr2/python_cli/clone_exec.py +1019 -0
- gr2/python_cli/commit.py +199 -0
- gr2/python_cli/config.py +291 -0
- gr2/python_cli/env_exec.py +419 -0
- gr2/python_cli/events.py +529 -0
- gr2/python_cli/execops.py +372 -0
- gr2/python_cli/failures.py +98 -0
- gr2/python_cli/file_exec.py +256 -0
- gr2/python_cli/gitops.py +226 -0
- gr2/python_cli/grip.py +1337 -0
- gr2/python_cli/grip_cli.py +493 -0
- gr2/python_cli/hooks.py +450 -0
- gr2/python_cli/launch_exec.py +786 -0
- gr2/python_cli/merge_verification.py +274 -0
- gr2/python_cli/migration.py +985 -0
- gr2/python_cli/open_gr_review.py +699 -0
- gr2/python_cli/platform.py +441 -0
- gr2/python_cli/pr.py +487 -0
- gr2/python_cli/project_review.py +314 -0
- gr2/python_cli/prune.py +365 -0
- gr2/python_cli/push.py +172 -0
- gr2/python_cli/review.py +462 -0
- gr2/python_cli/review_ephemeral.py +143 -0
- gr2/python_cli/review_run.py +621 -0
- gr2/python_cli/spec_apply.py +1285 -0
- gr2/python_cli/staging_cleanup.py +205 -0
- gr2/python_cli/syncops.py +920 -0
- gr2/python_cli/target.py +100 -0
- gr2/python_cli/workspace_snapshot.py +105 -0
- gr2/schemas/gr2-materialization-plan-v1.schema.json +191 -0
- gr2_overlay/__init__.py +37 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Receipt-keyed staged-input cleanup (S4 cleanup slice).
|
|
2
|
+
|
|
3
|
+
Carved out of S4-C so a DELETE never shares a review surface with a copy. C
|
|
4
|
+
stages, projects, and publishes a receipt, and deletes nothing. This module is
|
|
5
|
+
the only thing in the system permitted to remove a staged input.
|
|
6
|
+
|
|
7
|
+
Contract: MaterializationPlan v1 staged-input durability invariant 8.
|
|
8
|
+
|
|
9
|
+
"Do not delete a staged project-file input until the final materialization
|
|
10
|
+
receipt containing that operation's evidence has been atomically published
|
|
11
|
+
and made durable. A successful destination write or in-memory operation
|
|
12
|
+
result is not acknowledgement. If cleanup is interrupted after receipt
|
|
13
|
+
publication, rerun performs the idempotent cleanup from the verified
|
|
14
|
+
receipt."
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
THE PAIRED INVARIANT WITH S4-C
|
|
18
|
+
------------------------------
|
|
19
|
+
Cleanup deletes the ONLY remaining copy of the projected content. That is
|
|
20
|
+
survivable for exactly one reason: C proves a projection is current from the
|
|
21
|
+
DESTINATION alone, by hashing it against source_sha256, so it never needs the
|
|
22
|
+
staged input again.
|
|
23
|
+
|
|
24
|
+
Cleanup may delete a staged input only because C can re-establish currency
|
|
25
|
+
from the destination alone. The two slices are safe only TOGETHER.
|
|
26
|
+
|
|
27
|
+
Neither module can state that on its own, which is why it is the thing most
|
|
28
|
+
likely to rot. TestCrossSliceSeam runs the whole sequence -- project, receipt,
|
|
29
|
+
cleanup, project again -- and is the probe for the seam itself.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
WHERE AUTHORITY COMES FROM, AND WHY IT IS SPLIT
|
|
33
|
+
----------------------------------------------
|
|
34
|
+
the RECEIPT answers "MAY I delete?" -- is this plan durably acknowledged
|
|
35
|
+
the frozen PLAN answers "WHAT may I delete?" -- the validated paths
|
|
36
|
+
|
|
37
|
+
The obvious design reads source_path out of the receipt. That is wrong, and it
|
|
38
|
+
is validation-vs-use masking one more time: NOTHING IN THE RECEIPT IS SIGNED, so
|
|
39
|
+
anyone who can write that file can set plan_hash correctly -- it is a public
|
|
40
|
+
value, not a secret -- and point the evidence at someone's real work. Those path
|
|
41
|
+
strings were validated as PLAN fields; the receipt is a different artifact read
|
|
42
|
+
at a later time, and its bytes are not the validated ones.
|
|
43
|
+
|
|
44
|
+
Invariant 8 says cleanup runs "from the VERIFIED receipt", and verification is
|
|
45
|
+
exactly the receipt's job. Taking the paths from the immutable capability loses
|
|
46
|
+
nothing, because a plan_hash match forecloses any receipt/plan disagreement.
|
|
47
|
+
|
|
48
|
+
Working, not exhaustively hardened (Layne's 2026-07-27 prototype doctrine) --
|
|
49
|
+
but the delete-side guards are not the place to economise, so the acknowledgement
|
|
50
|
+
gate and the never-escalate rule are complete.
|
|
51
|
+
"""
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import hashlib
|
|
55
|
+
import json
|
|
56
|
+
import os
|
|
57
|
+
import stat
|
|
58
|
+
from pathlib import Path
|
|
59
|
+
|
|
60
|
+
from .spec_apply import (
|
|
61
|
+
MaterializationPlanError,
|
|
62
|
+
ValidatedPlan,
|
|
63
|
+
_read_canonical_workspace_spec_bytes,
|
|
64
|
+
canonicalize_workspace_path,
|
|
65
|
+
compute_plan_hash,
|
|
66
|
+
materialization_receipt_path,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class StagingCleanupError(MaterializationPlanError):
|
|
71
|
+
"""A staged input could not be removed safely."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _require_workspace_binding(validated: ValidatedPlan, workspace_root: Path) -> None:
|
|
75
|
+
try:
|
|
76
|
+
spec_bytes = _read_canonical_workspace_spec_bytes(workspace_root)
|
|
77
|
+
except MaterializationPlanError as exc:
|
|
78
|
+
raise StagingCleanupError(
|
|
79
|
+
f"cannot clean up against {workspace_root}: its canonical WorkspaceSpec "
|
|
80
|
+
f"is unreadable ({exc})"
|
|
81
|
+
) from exc
|
|
82
|
+
if hashlib.sha256(spec_bytes).hexdigest() != validated.workspace_spec_sha256:
|
|
83
|
+
raise StagingCleanupError(
|
|
84
|
+
f"plan is bound to WorkspaceSpec {validated.workspace_spec_sha256} but "
|
|
85
|
+
f"{workspace_root} has a different one -- deleting against a workspace "
|
|
86
|
+
"this plan was not validated for would resolve every path elsewhere"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _load_acknowledging_receipt(
|
|
91
|
+
validated: ValidatedPlan, *, workspace_root: Path, receipt_path: Path
|
|
92
|
+
) -> dict[str, object]:
|
|
93
|
+
"""The MAY-I half. Returns the receipt only if it durably acknowledges THIS
|
|
94
|
+
plan; raises otherwise.
|
|
95
|
+
|
|
96
|
+
Three checks, each seeing a failure the others cannot:
|
|
97
|
+
|
|
98
|
+
- CANONICAL LOCATION. The receipt is the authority to delete. Accepting
|
|
99
|
+
one from an arbitrary path makes that authority forgeable by anyone who
|
|
100
|
+
can write a file anywhere in the workspace.
|
|
101
|
+
- EXISTENCE. Invariant 8's core prohibition. A successful projection and a
|
|
102
|
+
correct destination are explicitly NOT acknowledgement.
|
|
103
|
+
- PLAN_HASH, not plan_id. plan_id is an opaque token that may repeat
|
|
104
|
+
across successive plans for the same unit, so matching it alone would
|
|
105
|
+
let yesterday's receipt authorise today's deletions. The bound is the
|
|
106
|
+
CONTENT hash."""
|
|
107
|
+
canonical = materialization_receipt_path(workspace_root, validated.plan_id)
|
|
108
|
+
if receipt_path.resolve() != canonical.resolve():
|
|
109
|
+
raise StagingCleanupError(
|
|
110
|
+
f"receipt {receipt_path} is not the canonical receipt for this plan "
|
|
111
|
+
f"({canonical}) -- the authority to delete is not accepted from an "
|
|
112
|
+
"arbitrary path"
|
|
113
|
+
)
|
|
114
|
+
if not canonical.is_file():
|
|
115
|
+
raise StagingCleanupError(
|
|
116
|
+
f"no durable receipt at {canonical} -- invariant 8 forbids removing a "
|
|
117
|
+
"staged input before the receipt carrying its evidence is published, and "
|
|
118
|
+
"a successful destination write is not acknowledgement"
|
|
119
|
+
)
|
|
120
|
+
try:
|
|
121
|
+
receipt = json.loads(canonical.read_text())
|
|
122
|
+
except json.JSONDecodeError as exc:
|
|
123
|
+
raise StagingCleanupError(f"receipt {canonical} is not valid JSON: {exc}") from exc
|
|
124
|
+
|
|
125
|
+
if receipt.get("plan_id") != validated.plan_id:
|
|
126
|
+
raise StagingCleanupError(
|
|
127
|
+
f"receipt at {canonical} does not acknowledge this plan: it records "
|
|
128
|
+
f"plan_id {receipt.get('plan_id')!r}, not {validated.plan_id!r}"
|
|
129
|
+
)
|
|
130
|
+
expected_hash = compute_plan_hash(validated.plan)
|
|
131
|
+
if receipt.get("plan_hash") != expected_hash:
|
|
132
|
+
raise StagingCleanupError(
|
|
133
|
+
f"receipt at {canonical} records plan_hash {receipt.get('plan_hash')!r}, "
|
|
134
|
+
f"not this plan's {expected_hash!r} -- an opaque plan_id may be reused "
|
|
135
|
+
"across re-emissions, so the content hash is the bound"
|
|
136
|
+
)
|
|
137
|
+
return receipt
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _remove_staged_input(path: Path) -> bool:
|
|
141
|
+
"""Remove one staged input. Returns False if it was already gone.
|
|
142
|
+
|
|
143
|
+
NEVER escalates. Between C's execute and this call the path could have
|
|
144
|
+
become a directory; the tempting fix is rmtree and the correct answer is to
|
|
145
|
+
refuse. unlink() on a symlink removes the LINK rather than its target, which
|
|
146
|
+
is already the safe behaviour, so the input that actually matters here is a
|
|
147
|
+
directory."""
|
|
148
|
+
try:
|
|
149
|
+
mode = os.lstat(path).st_mode
|
|
150
|
+
except FileNotFoundError:
|
|
151
|
+
return False
|
|
152
|
+
if not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)):
|
|
153
|
+
raise StagingCleanupError(
|
|
154
|
+
f"staged input {path} is not a regular file -- refusing to remove it. "
|
|
155
|
+
"Cleanup never escalates to a recursive delete, whatever is sitting at "
|
|
156
|
+
"a path the plan expected a file at"
|
|
157
|
+
)
|
|
158
|
+
path.unlink()
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cleanup_staged_inputs(
|
|
163
|
+
validated: ValidatedPlan, *, workspace_root: Path, receipt_path: Path
|
|
164
|
+
) -> list[str]:
|
|
165
|
+
"""Remove the staged inputs of a durably acknowledged plan.
|
|
166
|
+
|
|
167
|
+
Returns the workspace-relative paths actually removed -- empty when there
|
|
168
|
+
was nothing left to do, which is what makes a rerun after an interrupted
|
|
169
|
+
cleanup a no-op rather than an error. Invariant 8's tail requires exactly
|
|
170
|
+
that: the caller cannot know which state it is in, and neither the receipt
|
|
171
|
+
nor the filesystem records whether cleanup ran.
|
|
172
|
+
|
|
173
|
+
Paths come from the frozen plan, never from the receipt -- see the module
|
|
174
|
+
docstring. The receipt says whether deletion is permitted; it does not get
|
|
175
|
+
to say what gets deleted."""
|
|
176
|
+
workspace_root = Path(os.fspath(workspace_root))
|
|
177
|
+
validated.verify(require_provenance=True)
|
|
178
|
+
_require_workspace_binding(validated, workspace_root)
|
|
179
|
+
_load_acknowledging_receipt(
|
|
180
|
+
validated, workspace_root=workspace_root, receipt_path=receipt_path
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
removed: list[str] = []
|
|
184
|
+
failures: list[str] = []
|
|
185
|
+
for index, op in enumerate(validated.plan["operations"]):
|
|
186
|
+
if op.get("kind") != "project_file":
|
|
187
|
+
continue
|
|
188
|
+
declared = str(op["source_path"])
|
|
189
|
+
source = canonicalize_workspace_path(
|
|
190
|
+
workspace_root, declared, field_name=f"operations[{index}].source_path"
|
|
191
|
+
)
|
|
192
|
+
# Attempt every input, then report. Aborting on the first failure would
|
|
193
|
+
# let one stuck file block cleanup of the others forever, and cleanup is
|
|
194
|
+
# idempotent precisely so a partial pass is safe to repeat.
|
|
195
|
+
try:
|
|
196
|
+
if _remove_staged_input(source):
|
|
197
|
+
removed.append(declared)
|
|
198
|
+
except StagingCleanupError as exc:
|
|
199
|
+
failures.append(str(exc))
|
|
200
|
+
|
|
201
|
+
if failures:
|
|
202
|
+
raise StagingCleanupError(
|
|
203
|
+
"some staged inputs could not be removed:\n" + "\n".join(failures)
|
|
204
|
+
)
|
|
205
|
+
return removed
|