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.
Files changed (80) hide show
  1. gitgrip-1.5.0.dist-info/METADATA +13 -0
  2. gitgrip-1.5.0.dist-info/RECORD +80 -0
  3. gitgrip-1.5.0.dist-info/WHEEL +5 -0
  4. gitgrip-1.5.0.dist-info/entry_points.txt +2 -0
  5. gitgrip-1.5.0.dist-info/top_level.txt +2 -0
  6. gr2/__init__.py +0 -0
  7. gr2/overlay/__init__.py +6 -0
  8. gr2/overlay/activate.py +196 -0
  9. gr2/overlay/agent_manifest.py +138 -0
  10. gr2/overlay/cli.py +181 -0
  11. gr2/overlay/cross_repo.py +124 -0
  12. gr2/overlay/drivers.py +113 -0
  13. gr2/overlay/introspection.py +155 -0
  14. gr2/overlay/language_drivers.py +115 -0
  15. gr2/overlay/objects.py +412 -0
  16. gr2/overlay/perf.py +251 -0
  17. gr2/overlay/refs.py +36 -0
  18. gr2/overlay/trust.py +150 -0
  19. gr2/overlay/types.py +69 -0
  20. gr2/overlay/units.py +313 -0
  21. gr2/overlay/workspace_spec.py +59 -0
  22. gr2/prototypes/__init__.py +0 -0
  23. gr2/prototypes/cache_materialization_probe.py +190 -0
  24. gr2/prototypes/concurrent_event_stress.py +199 -0
  25. gr2/prototypes/concurrent_lease_stress.py +240 -0
  26. gr2/prototypes/concurrent_workspace_cap_stress.py +231 -0
  27. gr2/prototypes/contribution_protocol.py +665 -0
  28. gr2/prototypes/cross_mode_lane_stress.py +986 -0
  29. gr2/prototypes/jsonl_store.py +158 -0
  30. gr2/prototypes/lane_workspace_prototype.py +2088 -0
  31. gr2/prototypes/layout_model_probe.py +139 -0
  32. gr2/prototypes/propagation_daemon.py +546 -0
  33. gr2/prototypes/propagation_state_machine.py +1478 -0
  34. gr2/prototypes/python_exec_playground.py +194 -0
  35. gr2/prototypes/python_hook_runtime_playground.py +240 -0
  36. gr2/prototypes/python_migration_playground.py +144 -0
  37. gr2/prototypes/python_review_checkout_playground.py +242 -0
  38. gr2/prototypes/python_spec_apply_playground.py +282 -0
  39. gr2/prototypes/real_git_lane_materialization.py +248 -0
  40. gr2/prototypes/real_git_playground.py +334 -0
  41. gr2/prototypes/recall_lane_history.py +274 -0
  42. gr2/prototypes/repo_maintenance_prototype.py +659 -0
  43. gr2/prototypes/repo_transport_probe.py +147 -0
  44. gr2/python_cli/__init__.py +2 -0
  45. gr2/python_cli/__main__.py +6 -0
  46. gr2/python_cli/add.py +51 -0
  47. gr2/python_cli/app.py +2516 -0
  48. gr2/python_cli/branch.py +67 -0
  49. gr2/python_cli/channel_bridge.py +131 -0
  50. gr2/python_cli/clone_exec.py +1019 -0
  51. gr2/python_cli/commit.py +199 -0
  52. gr2/python_cli/config.py +291 -0
  53. gr2/python_cli/env_exec.py +419 -0
  54. gr2/python_cli/events.py +529 -0
  55. gr2/python_cli/execops.py +372 -0
  56. gr2/python_cli/failures.py +98 -0
  57. gr2/python_cli/file_exec.py +256 -0
  58. gr2/python_cli/gitops.py +226 -0
  59. gr2/python_cli/grip.py +1337 -0
  60. gr2/python_cli/grip_cli.py +493 -0
  61. gr2/python_cli/hooks.py +450 -0
  62. gr2/python_cli/launch_exec.py +786 -0
  63. gr2/python_cli/merge_verification.py +274 -0
  64. gr2/python_cli/migration.py +985 -0
  65. gr2/python_cli/open_gr_review.py +699 -0
  66. gr2/python_cli/platform.py +441 -0
  67. gr2/python_cli/pr.py +487 -0
  68. gr2/python_cli/project_review.py +314 -0
  69. gr2/python_cli/prune.py +365 -0
  70. gr2/python_cli/push.py +172 -0
  71. gr2/python_cli/review.py +462 -0
  72. gr2/python_cli/review_ephemeral.py +143 -0
  73. gr2/python_cli/review_run.py +621 -0
  74. gr2/python_cli/spec_apply.py +1285 -0
  75. gr2/python_cli/staging_cleanup.py +205 -0
  76. gr2/python_cli/syncops.py +920 -0
  77. gr2/python_cli/target.py +100 -0
  78. gr2/python_cli/workspace_snapshot.py +105 -0
  79. gr2/schemas/gr2-materialization-plan-v1.schema.json +191 -0
  80. 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