graphite-code 0.3.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.
- graphite/__init__.py +41 -0
- graphite/__main__.py +7 -0
- graphite/_cleanup_worker.py +525 -0
- graphite/activation.py +164 -0
- graphite/agent_hooks.py +577 -0
- graphite/agent_settings.py +226 -0
- graphite/analyze.py +146 -0
- graphite/answer_contract.py +420 -0
- graphite/bootstrap.py +210 -0
- graphite/buildlock.py +99 -0
- graphite/cache.py +131 -0
- graphite/channel.py +1325 -0
- graphite/cli.py +3053 -0
- graphite/cluster.py +111 -0
- graphite/config.py +209 -0
- graphite/context.py +355 -0
- graphite/daemon.py +745 -0
- graphite/daemon_health.py +733 -0
- graphite/debt.py +118 -0
- graphite/dependency_install.py +1597 -0
- graphite/detach.py +33 -0
- graphite/doctor.py +678 -0
- graphite/doctor_probes.py +2100 -0
- graphite/engine_identity.py +238 -0
- graphite/export/__init__.py +6 -0
- graphite/export/html.py +244 -0
- graphite/export/json.py +39 -0
- graphite/export/md.py +68 -0
- graphite/extract/__init__.py +4 -0
- graphite/extract/ast.py +1964 -0
- graphite/freshness.py +127 -0
- graphite/git.py +406 -0
- graphite/graph.py +117 -0
- graphite/graph_io.py +188 -0
- graphite/health.py +147 -0
- graphite/hook_entry.py +68 -0
- graphite/hookinstall.py +224 -0
- graphite/hookshim.py +86 -0
- graphite/incident_ledger.py +247 -0
- graphite/ingest.py +279 -0
- graphite/init.py +791 -0
- graphite/io.py +32 -0
- graphite/listing.py +51 -0
- graphite/llm.py +518 -0
- graphite/llm_probe.py +157 -0
- graphite/mcp.py +7 -0
- graphite/mcp_server.py +450 -0
- graphite/natural_query.py +252 -0
- graphite/overlays.py +713 -0
- graphite/probe_process.py +879 -0
- graphite/probe_workspace.py +728 -0
- graphite/process_contracts.py +22 -0
- graphite/provider_observer.py +397 -0
- graphite/query.py +646 -0
- graphite/query_plan.py +97 -0
- graphite/replacement_audit.py +291 -0
- graphite/resolve.py +660 -0
- graphite/review.py +782 -0
- graphite/routing/__init__.py +5 -0
- graphite/routing/approval.py +362 -0
- graphite/routing/classifier.py +169 -0
- graphite/routing/claude_executor.py +419 -0
- graphite/routing/claude_probe.py +102 -0
- graphite/routing/cli_identity.py +84 -0
- graphite/routing/codex_executor.py +383 -0
- graphite/routing/codex_probe.py +93 -0
- graphite/routing/context_builder.py +327 -0
- graphite/routing/contracts.py +802 -0
- graphite/routing/diff_policy.py +468 -0
- graphite/routing/edit_apply.py +166 -0
- graphite/routing/effort.py +43 -0
- graphite/routing/lifecycle.py +771 -0
- graphite/routing/lifecycle_operator.py +227 -0
- graphite/routing/lifecycle_service.py +555 -0
- graphite/routing/lifecycle_storage.py +977 -0
- graphite/routing/ollama_executor.py +341 -0
- graphite/routing/ollama_probe.py +72 -0
- graphite/routing/openrouter_executor.py +338 -0
- graphite/routing/openrouter_probe.py +188 -0
- graphite/routing/policy.py +815 -0
- graphite/routing/probe_runner.py +543 -0
- graphite/routing/process_runner.py +523 -0
- graphite/routing/profiles.py +554 -0
- graphite/routing/prompt.py +58 -0
- graphite/routing/registry.py +444 -0
- graphite/routing/route_pool.py +629 -0
- graphite/routing/route_pool_execution.py +275 -0
- graphite/routing/schema_validation.py +169 -0
- graphite/routing/service.py +1263 -0
- graphite/routing/settings.py +99 -0
- graphite/routing/shadow.py +201 -0
- graphite/routing/storage.py +4001 -0
- graphite/routing/telemetry.py +346 -0
- graphite/routing/worktree.py +259 -0
- graphite/routing/zai_edit.py +113 -0
- graphite/routing/zai_executor.py +191 -0
- graphite/routing/zai_probe.py +126 -0
- graphite/savings.py +84 -0
- graphite/ts_bridge.py +142 -0
- graphite/ts_resolver.mjs +314 -0
- graphite/typescript_activation.py +1586 -0
- graphite/usage_ledger.py +156 -0
- graphite/validation.py +148 -0
- graphite/watch.py +167 -0
- graphite/windows_job.py +368 -0
- graphite/windows_startup.py +144 -0
- graphite/windows_task.py +212 -0
- graphite_code-0.3.0.dist-info/METADATA +743 -0
- graphite_code-0.3.0.dist-info/RECORD +112 -0
- graphite_code-0.3.0.dist-info/WHEEL +4 -0
- graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
- graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Append-only verified evidence and typed privacy-safe aggregate export."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .contracts import (
|
|
11
|
+
EvidenceProvenance,
|
|
12
|
+
ExecutionReceipt,
|
|
13
|
+
Effort,
|
|
14
|
+
ProviderId,
|
|
15
|
+
RiskTier,
|
|
16
|
+
TaskCategory,
|
|
17
|
+
VerifiedOutcome,
|
|
18
|
+
)
|
|
19
|
+
from .registry import BUNDLED_PROFILES
|
|
20
|
+
from .storage import AggregateRecord, AggregateStore, RepositoryStore
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class EvidenceCorrelation:
|
|
25
|
+
task_id: str
|
|
26
|
+
decision_id: str
|
|
27
|
+
graph_fingerprint: str
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
if not self.task_id or not self.decision_id:
|
|
31
|
+
raise ValueError("evidence_correlation_invalid")
|
|
32
|
+
if len(self.graph_fingerprint) != 64 or any(
|
|
33
|
+
character not in "0123456789abcdef" for character in self.graph_fingerprint
|
|
34
|
+
):
|
|
35
|
+
raise ValueError("evidence_correlation_invalid")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class EvidenceSummary:
|
|
40
|
+
sample_count: int
|
|
41
|
+
success_count: int
|
|
42
|
+
severe_failure_count: int
|
|
43
|
+
human_count: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ReviewDefectClass(StrEnum):
|
|
47
|
+
CORRECTNESS = "correctness"
|
|
48
|
+
SECURITY = "security"
|
|
49
|
+
RELIABILITY = "reliability"
|
|
50
|
+
MAINTAINABILITY = "maintainability"
|
|
51
|
+
PERFORMANCE = "performance"
|
|
52
|
+
TEST_COVERAGE = "test_coverage"
|
|
53
|
+
REQUIREMENT_MISS = "requirement_miss"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class HumanVerdict(StrEnum):
|
|
57
|
+
ACCEPTED = "accepted"
|
|
58
|
+
REJECTED = "rejected"
|
|
59
|
+
REWORK = "rework"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_SAFE_MODEL = re.compile(
|
|
63
|
+
r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(/[A-Za-z0-9][A-Za-z0-9._:+-]{0,127})?$"
|
|
64
|
+
)
|
|
65
|
+
_HEX_64 = re.compile(r"^[0-9a-f]{64}$")
|
|
66
|
+
_VALIDATION_OUTCOMES = frozenset({"passed", "failed", "blocked", "not_run"})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _bounded_optional(value: int | None, code: str, maximum: int) -> int | None:
|
|
70
|
+
if value is None:
|
|
71
|
+
return None
|
|
72
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= maximum:
|
|
73
|
+
raise ValueError(code)
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _bounded_required(value: object, code: str, maximum: int) -> int:
|
|
78
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= maximum:
|
|
79
|
+
raise ValueError(code)
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class CliTelemetryRecord:
|
|
85
|
+
"""The complete public telemetry schema; raw artifacts have no field to enter."""
|
|
86
|
+
|
|
87
|
+
provider: ProviderId
|
|
88
|
+
requested_model: str
|
|
89
|
+
effective_model: str
|
|
90
|
+
effort: Effort
|
|
91
|
+
capability_snapshot_digest: str
|
|
92
|
+
category: TaskCategory
|
|
93
|
+
risk: RiskTier
|
|
94
|
+
latency_ms: int | None
|
|
95
|
+
input_tokens: int | None
|
|
96
|
+
output_tokens: int | None
|
|
97
|
+
changed_file_count: int
|
|
98
|
+
changed_byte_count: int
|
|
99
|
+
validation_outcome: str
|
|
100
|
+
review_defect_classes: tuple[ReviewDefectClass, ...]
|
|
101
|
+
rework_count: int
|
|
102
|
+
human_verdict: HumanVerdict | None
|
|
103
|
+
provenance: EvidenceProvenance
|
|
104
|
+
observed_at: int
|
|
105
|
+
diff_sha256: str | None = None
|
|
106
|
+
cost_status: str = "unknown"
|
|
107
|
+
|
|
108
|
+
def __post_init__(self) -> None:
|
|
109
|
+
object.__setattr__(self, "provider", ProviderId(self.provider))
|
|
110
|
+
object.__setattr__(self, "effort", Effort(self.effort))
|
|
111
|
+
object.__setattr__(self, "category", TaskCategory(self.category))
|
|
112
|
+
object.__setattr__(self, "risk", RiskTier(self.risk))
|
|
113
|
+
object.__setattr__(self, "provenance", EvidenceProvenance(self.provenance))
|
|
114
|
+
for value in (self.requested_model, self.effective_model):
|
|
115
|
+
if not isinstance(value, str) or _SAFE_MODEL.fullmatch(value) is None:
|
|
116
|
+
raise ValueError("telemetry_model_invalid")
|
|
117
|
+
if (
|
|
118
|
+
not isinstance(self.capability_snapshot_digest, str)
|
|
119
|
+
or _HEX_64.fullmatch(self.capability_snapshot_digest) is None
|
|
120
|
+
):
|
|
121
|
+
raise ValueError("telemetry_snapshot_invalid")
|
|
122
|
+
_bounded_optional(self.latency_ms, "telemetry_latency_invalid", 86_400_000)
|
|
123
|
+
_bounded_optional(self.input_tokens, "telemetry_usage_invalid", 100_000_000)
|
|
124
|
+
_bounded_optional(self.output_tokens, "telemetry_usage_invalid", 100_000_000)
|
|
125
|
+
for name, maximum in (
|
|
126
|
+
("changed_file_count", 100_000),
|
|
127
|
+
("changed_byte_count", 10**12),
|
|
128
|
+
("rework_count", 1_000),
|
|
129
|
+
("observed_at", 10**12),
|
|
130
|
+
):
|
|
131
|
+
_bounded_required(getattr(self, name), f"telemetry_{name}_invalid", maximum)
|
|
132
|
+
if self.validation_outcome not in _VALIDATION_OUTCOMES:
|
|
133
|
+
raise ValueError("telemetry_validation_outcome_invalid")
|
|
134
|
+
if not isinstance(self.review_defect_classes, tuple):
|
|
135
|
+
raise ValueError("telemetry_defect_classes_invalid")
|
|
136
|
+
defects = tuple(ReviewDefectClass(item) for item in self.review_defect_classes)
|
|
137
|
+
if len(defects) > len(ReviewDefectClass) or len(set(defects)) != len(defects):
|
|
138
|
+
raise ValueError("telemetry_defect_classes_invalid")
|
|
139
|
+
object.__setattr__(self, "review_defect_classes", tuple(sorted(defects, key=str)))
|
|
140
|
+
if self.human_verdict is not None:
|
|
141
|
+
object.__setattr__(self, "human_verdict", HumanVerdict(self.human_verdict))
|
|
142
|
+
if self.cost_status != "unknown":
|
|
143
|
+
raise ValueError("telemetry_cost_status_invalid")
|
|
144
|
+
if self.diff_sha256 is not None and (
|
|
145
|
+
not isinstance(self.diff_sha256, str)
|
|
146
|
+
or _HEX_64.fullmatch(self.diff_sha256) is None
|
|
147
|
+
):
|
|
148
|
+
raise ValueError("telemetry_diff_hash_invalid")
|
|
149
|
+
|
|
150
|
+
def to_dict(self) -> dict[str, Any]:
|
|
151
|
+
return {
|
|
152
|
+
"provider": self.provider.value,
|
|
153
|
+
"requested_model": self.requested_model,
|
|
154
|
+
"effective_model": self.effective_model,
|
|
155
|
+
"effort": self.effort.value,
|
|
156
|
+
"capability_snapshot_digest": self.capability_snapshot_digest,
|
|
157
|
+
"category": self.category.value,
|
|
158
|
+
"risk": self.risk.value,
|
|
159
|
+
"latency_ms": self.latency_ms,
|
|
160
|
+
"input_tokens": self.input_tokens,
|
|
161
|
+
"output_tokens": self.output_tokens,
|
|
162
|
+
"cost_status": self.cost_status,
|
|
163
|
+
"changed_file_count": self.changed_file_count,
|
|
164
|
+
"changed_byte_count": self.changed_byte_count,
|
|
165
|
+
"validation_outcome": self.validation_outcome,
|
|
166
|
+
"review_defect_classes": [item.value for item in self.review_defect_classes],
|
|
167
|
+
"rework_count": self.rework_count,
|
|
168
|
+
"human_verdict": None if self.human_verdict is None else self.human_verdict.value,
|
|
169
|
+
"provenance": self.provenance.value,
|
|
170
|
+
"observed_at": self.observed_at,
|
|
171
|
+
"diff_sha256": self.diff_sha256,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def record_cli_telemetry(store: RepositoryStore, record: CliTelemetryRecord) -> bool:
|
|
176
|
+
if not isinstance(record, CliTelemetryRecord):
|
|
177
|
+
raise ValueError("cli_telemetry_record_invalid")
|
|
178
|
+
return store.record_cli_telemetry(record.to_dict())
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(frozen=True)
|
|
182
|
+
class WeightedTelemetrySummary:
|
|
183
|
+
samples: int
|
|
184
|
+
weighted_samples_millis: int
|
|
185
|
+
weighted_success_millis: int
|
|
186
|
+
success_millis: int
|
|
187
|
+
latest_observed_at: int | None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def summarize_cli_telemetry(
|
|
191
|
+
records: tuple[CliTelemetryRecord, ...],
|
|
192
|
+
*,
|
|
193
|
+
now: int,
|
|
194
|
+
half_life_days: int = 30,
|
|
195
|
+
) -> WeightedTelemetrySummary:
|
|
196
|
+
"""Compute deterministic recency-weighted evidence; never infer missing cost."""
|
|
197
|
+
if isinstance(now, bool) or not isinstance(now, int) or now < 0:
|
|
198
|
+
raise ValueError("telemetry_time_invalid")
|
|
199
|
+
if isinstance(half_life_days, bool) or not isinstance(half_life_days, int) or half_life_days < 1:
|
|
200
|
+
raise ValueError("telemetry_half_life_invalid")
|
|
201
|
+
weighted_total = weighted_success = 0
|
|
202
|
+
latest: int | None = None
|
|
203
|
+
for record in records:
|
|
204
|
+
if not isinstance(record, CliTelemetryRecord) or record.observed_at > now:
|
|
205
|
+
raise ValueError("cli_telemetry_record_invalid")
|
|
206
|
+
age_days = (now - record.observed_at) // 86_400
|
|
207
|
+
weight = max(1, round(1_000 * (0.5 ** (age_days / half_life_days))))
|
|
208
|
+
weighted_total += weight
|
|
209
|
+
success = (
|
|
210
|
+
record.validation_outcome == "passed"
|
|
211
|
+
and not record.review_defect_classes
|
|
212
|
+
and record.human_verdict is not HumanVerdict.REJECTED
|
|
213
|
+
)
|
|
214
|
+
weighted_success += weight * int(success)
|
|
215
|
+
latest = record.observed_at if latest is None else max(latest, record.observed_at)
|
|
216
|
+
success_millis = 0 if weighted_total == 0 else weighted_success * 1_000 // weighted_total
|
|
217
|
+
return WeightedTelemetrySummary(
|
|
218
|
+
len(records), weighted_total, weighted_success, success_millis, latest
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def record_verified_outcome(
|
|
223
|
+
store: RepositoryStore,
|
|
224
|
+
outcome: VerifiedOutcome,
|
|
225
|
+
correlation: EvidenceCorrelation,
|
|
226
|
+
) -> bool:
|
|
227
|
+
"""Append an outcome after binding it to the recorded execution identity."""
|
|
228
|
+
linked = store.execution_evidence(outcome.execution_id)
|
|
229
|
+
expected = {
|
|
230
|
+
"task_id": correlation.task_id,
|
|
231
|
+
"decision_id": correlation.decision_id,
|
|
232
|
+
"graph_fingerprint": correlation.graph_fingerprint,
|
|
233
|
+
}
|
|
234
|
+
if linked != expected:
|
|
235
|
+
raise ValueError("evidence_correlation_invalid")
|
|
236
|
+
if outcome.provenance in {
|
|
237
|
+
EvidenceProvenance.MACHINE_VERIFIED,
|
|
238
|
+
EvidenceProvenance.CI_IMPORTED,
|
|
239
|
+
} and any(
|
|
240
|
+
value is None
|
|
241
|
+
for value in (outcome.build_passed, outcome.tests_passed, outcome.security_passed)
|
|
242
|
+
):
|
|
243
|
+
raise ValueError("evidence_verification_incomplete")
|
|
244
|
+
success = bool(
|
|
245
|
+
outcome.build_passed is True
|
|
246
|
+
and outcome.tests_passed is True
|
|
247
|
+
and outcome.security_passed is True
|
|
248
|
+
and not outcome.escalated
|
|
249
|
+
and not outcome.reverted
|
|
250
|
+
and not outcome.severe_failure
|
|
251
|
+
)
|
|
252
|
+
if outcome.provenance is EvidenceProvenance.HUMAN:
|
|
253
|
+
success = outcome.human_accepted is True and not outcome.reverted
|
|
254
|
+
if outcome.provenance is EvidenceProvenance.REVERSION:
|
|
255
|
+
success = False
|
|
256
|
+
return store.record_outcome(
|
|
257
|
+
outcome.outcome_id,
|
|
258
|
+
outcome.execution_id,
|
|
259
|
+
outcome.provenance.value,
|
|
260
|
+
success,
|
|
261
|
+
outcome.severe_failure,
|
|
262
|
+
outcome.recorded_at,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def evidence_summary(store: RepositoryStore) -> EvidenceSummary:
|
|
267
|
+
"""Derive current evidence without mutating historical events."""
|
|
268
|
+
cutoff = store.latest_incident_review()
|
|
269
|
+
rows = [
|
|
270
|
+
row
|
|
271
|
+
for row in store.outcome_evidence_rows()
|
|
272
|
+
if cutoff is None or int(row["recorded_at"]) > cutoff
|
|
273
|
+
]
|
|
274
|
+
human_count = sum(row["provenance"] == EvidenceProvenance.HUMAN.value for row in rows)
|
|
275
|
+
grouped: dict[str, list[dict[str, object]]] = {}
|
|
276
|
+
for row in rows:
|
|
277
|
+
grouped.setdefault(str(row["execution_id"]), []).append(row)
|
|
278
|
+
samples = successes = severe = 0
|
|
279
|
+
for events in grouped.values():
|
|
280
|
+
admitted = [
|
|
281
|
+
row for row in events
|
|
282
|
+
if row["provenance"] in {
|
|
283
|
+
EvidenceProvenance.MACHINE_VERIFIED.value,
|
|
284
|
+
EvidenceProvenance.CI_IMPORTED.value,
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
if not admitted:
|
|
288
|
+
continue
|
|
289
|
+
samples += 1
|
|
290
|
+
reverted = any(
|
|
291
|
+
row["provenance"] == EvidenceProvenance.REVERSION.value for row in events
|
|
292
|
+
)
|
|
293
|
+
latest = admitted[-1]
|
|
294
|
+
successes += int(bool(latest["success"]) and not reverted)
|
|
295
|
+
severe += int(any(bool(row["severe_failure"]) for row in admitted))
|
|
296
|
+
return EvidenceSummary(samples, successes, severe, human_count)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def close_incident_review(
|
|
300
|
+
store: RepositoryStore,
|
|
301
|
+
execution_id: str,
|
|
302
|
+
*,
|
|
303
|
+
reviewed_at: int,
|
|
304
|
+
) -> bool:
|
|
305
|
+
rows = store.outcome_evidence_rows()
|
|
306
|
+
if not any(
|
|
307
|
+
row["execution_id"] == execution_id and bool(row["severe_failure"])
|
|
308
|
+
for row in rows
|
|
309
|
+
):
|
|
310
|
+
raise ValueError("incident_review_invalid")
|
|
311
|
+
return store.close_incident(execution_id, reviewed_at)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _bucket(value: int | None) -> int:
|
|
315
|
+
if value is None or value <= 0:
|
|
316
|
+
return 0
|
|
317
|
+
return min(20, int(math.log2(value)) + 1)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def export_sanitized_aggregate(
|
|
321
|
+
aggregate: AggregateStore,
|
|
322
|
+
receipt: ExecutionReceipt,
|
|
323
|
+
*,
|
|
324
|
+
category: TaskCategory,
|
|
325
|
+
risk: RiskTier,
|
|
326
|
+
policy_version: str,
|
|
327
|
+
recorded_at: int,
|
|
328
|
+
) -> bool:
|
|
329
|
+
"""Build an aggregate solely from allowlisted typed fields."""
|
|
330
|
+
if receipt.model_id not in BUNDLED_PROFILES:
|
|
331
|
+
raise ValueError("model_id_invalid")
|
|
332
|
+
if isinstance(recorded_at, bool) or not isinstance(recorded_at, int) or recorded_at < 0:
|
|
333
|
+
raise ValueError("recorded_at_invalid")
|
|
334
|
+
record = AggregateRecord(
|
|
335
|
+
model_id=receipt.model_id,
|
|
336
|
+
effort=receipt.effort.value,
|
|
337
|
+
category=TaskCategory(category).value,
|
|
338
|
+
risk=RiskTier(risk).value,
|
|
339
|
+
outcome=receipt.outcome.value,
|
|
340
|
+
input_bucket=_bucket(receipt.input_tokens),
|
|
341
|
+
output_bucket=_bucket(receipt.output_tokens),
|
|
342
|
+
latency_bucket=_bucket(receipt.latency_ms),
|
|
343
|
+
policy_version=policy_version,
|
|
344
|
+
recorded_day=recorded_at // 86_400,
|
|
345
|
+
)
|
|
346
|
+
return aggregate.write(record)
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Approval-bound, isolated Git worktree preparation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import stat
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Final
|
|
10
|
+
|
|
11
|
+
from graphite.git import (
|
|
12
|
+
GitError,
|
|
13
|
+
GitOutputLimitError,
|
|
14
|
+
GitResult,
|
|
15
|
+
GitRunner,
|
|
16
|
+
GitTimeoutError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_TASK_ID: Final = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
20
|
+
_COMMIT: Final = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
|
|
21
|
+
_REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
|
|
22
|
+
GIT_TIMEOUT_SECONDS: Final = 15.0
|
|
23
|
+
MAX_GIT_METADATA_BYTES: Final = 8 * 1024 * 1024
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WorktreeError(RuntimeError):
|
|
27
|
+
"""Stable, path-free worktree preparation failure.
|
|
28
|
+
|
|
29
|
+
`cause` is a diagnostic carried in the MESSAGE only, and only ever an
|
|
30
|
+
exception class name. See `DiffPolicyError` for the reasoning; the two are
|
|
31
|
+
deliberately the same shape because the routing service catches them
|
|
32
|
+
together and cannot tell which one it is holding.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, code: str, cause: str | None = None) -> None:
|
|
36
|
+
self.code = code
|
|
37
|
+
self.cause = cause
|
|
38
|
+
super().__init__(f"{code} ({cause})" if cause else code)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class TaskWorktree:
|
|
43
|
+
worktree_id: str
|
|
44
|
+
root: Path
|
|
45
|
+
git_common_dir: Path
|
|
46
|
+
baseline_commit: str
|
|
47
|
+
status: str = "prepared"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_CLEANUP_TERMINAL_STATES: Final = frozenset(
|
|
51
|
+
{"accepted", "rejected", "abandoned", "quarantined"}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_reparse(metadata: os.stat_result) -> bool:
|
|
56
|
+
return bool(getattr(metadata, "st_file_attributes", 0) & _REPARSE_POINT)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _canonical_directory(path: Path, code: str) -> Path:
|
|
60
|
+
if not isinstance(path, Path) or not path.is_absolute():
|
|
61
|
+
raise WorktreeError(code)
|
|
62
|
+
try:
|
|
63
|
+
metadata = path.lstat()
|
|
64
|
+
resolved = path.resolve(strict=True)
|
|
65
|
+
except OSError:
|
|
66
|
+
raise WorktreeError(code) from None
|
|
67
|
+
if stat.S_ISLNK(metadata.st_mode) or _is_reparse(metadata) or not stat.S_ISDIR(metadata.st_mode):
|
|
68
|
+
raise WorktreeError(code)
|
|
69
|
+
return resolved
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _secure_directory(path: Path, code: str) -> Path:
|
|
73
|
+
try:
|
|
74
|
+
path.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
75
|
+
except OSError:
|
|
76
|
+
raise WorktreeError(code) from None
|
|
77
|
+
resolved = _canonical_directory(path, code)
|
|
78
|
+
if os.name != "nt":
|
|
79
|
+
try:
|
|
80
|
+
path.chmod(0o700)
|
|
81
|
+
except OSError:
|
|
82
|
+
raise WorktreeError(code) from None
|
|
83
|
+
return resolved
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _run(runner: GitRunner, arguments: list[str], *, maximum: int = MAX_GIT_METADATA_BYTES) -> GitResult:
|
|
87
|
+
try:
|
|
88
|
+
return runner.run(
|
|
89
|
+
arguments,
|
|
90
|
+
timeout_seconds=GIT_TIMEOUT_SECONDS,
|
|
91
|
+
max_stdout_bytes=maximum,
|
|
92
|
+
)
|
|
93
|
+
except GitTimeoutError:
|
|
94
|
+
raise WorktreeError("git_timeout") from None
|
|
95
|
+
except GitOutputLimitError:
|
|
96
|
+
raise WorktreeError("git_output_limit") from None
|
|
97
|
+
except GitError as exc:
|
|
98
|
+
# See the matching branch in `diff_policy._run_git` (graphite#37).
|
|
99
|
+
raise WorktreeError("git_unavailable", exc.diagnostic()) from None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _decode_line(output: bytes, code: str) -> str:
|
|
103
|
+
try:
|
|
104
|
+
value = output.decode("utf-8").rstrip("\r\n")
|
|
105
|
+
except UnicodeDecodeError:
|
|
106
|
+
raise WorktreeError(code) from None
|
|
107
|
+
if not value or "\x00" in value or "\n" in value or "\r" in value:
|
|
108
|
+
raise WorktreeError(code)
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _resolve_git_path(root: Path, raw: bytes, code: str) -> Path:
|
|
113
|
+
value = _decode_line(raw, code)
|
|
114
|
+
candidate = Path(value)
|
|
115
|
+
if not candidate.is_absolute():
|
|
116
|
+
candidate = root / candidate
|
|
117
|
+
try:
|
|
118
|
+
return candidate.resolve(strict=True)
|
|
119
|
+
except OSError:
|
|
120
|
+
raise WorktreeError(code) from None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _validate_index(output: bytes) -> None:
|
|
124
|
+
if output and not output.endswith(b"\0"):
|
|
125
|
+
raise WorktreeError("git_protocol")
|
|
126
|
+
folded: dict[str, str] = {}
|
|
127
|
+
for record in output[:-1].split(b"\0") if output else ():
|
|
128
|
+
try:
|
|
129
|
+
header, raw_path = record.split(b"\t", 1)
|
|
130
|
+
mode, _object_id, stage = header.split(b" ")
|
|
131
|
+
path = raw_path.decode("utf-8")
|
|
132
|
+
except (ValueError, UnicodeDecodeError):
|
|
133
|
+
raise WorktreeError("git_protocol") from None
|
|
134
|
+
if stage != b"0" or mode in {b"120000", b"160000"}:
|
|
135
|
+
raise WorktreeError("source_special_file")
|
|
136
|
+
if not path or "\\" in path or "\x00" in path:
|
|
137
|
+
raise WorktreeError("git_protocol")
|
|
138
|
+
collision = path.casefold()
|
|
139
|
+
if collision in folded and folded[collision] != path:
|
|
140
|
+
raise WorktreeError("source_path_collision")
|
|
141
|
+
folded[collision] = path
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def create_task_worktree(
|
|
145
|
+
*,
|
|
146
|
+
source_root: Path,
|
|
147
|
+
state_root: Path,
|
|
148
|
+
task_id: str,
|
|
149
|
+
approved_commit: str,
|
|
150
|
+
) -> TaskWorktree:
|
|
151
|
+
"""Create one detached worktree only from an exact clean approved commit."""
|
|
152
|
+
if not isinstance(task_id, str) or _TASK_ID.fullmatch(task_id) is None:
|
|
153
|
+
raise WorktreeError("task_id_invalid")
|
|
154
|
+
if not isinstance(approved_commit, str) or _COMMIT.fullmatch(approved_commit) is None:
|
|
155
|
+
raise WorktreeError("commit_invalid")
|
|
156
|
+
source = _canonical_directory(source_root, "source_root_invalid")
|
|
157
|
+
state_candidate = state_root.resolve(strict=False)
|
|
158
|
+
try:
|
|
159
|
+
state_candidate.relative_to(source)
|
|
160
|
+
except ValueError:
|
|
161
|
+
pass
|
|
162
|
+
else:
|
|
163
|
+
raise WorktreeError("state_root_invalid")
|
|
164
|
+
state = _secure_directory(state_root, "state_root_invalid")
|
|
165
|
+
tasks = _secure_directory(state / "tasks", "state_root_invalid")
|
|
166
|
+
target = tasks / task_id
|
|
167
|
+
if target.exists() or target.is_symlink():
|
|
168
|
+
raise WorktreeError("worktree_exists")
|
|
169
|
+
|
|
170
|
+
runner = GitRunner(source)
|
|
171
|
+
top = _run(runner, ["rev-parse", "--show-toplevel"])
|
|
172
|
+
if top.returncode != 0 or _resolve_git_path(source, top.stdout, "source_root_invalid") != source:
|
|
173
|
+
raise WorktreeError("source_root_invalid")
|
|
174
|
+
head = _run(runner, ["rev-parse", "HEAD"])
|
|
175
|
+
if head.returncode != 0 or _decode_line(head.stdout, "git_protocol") != approved_commit:
|
|
176
|
+
raise WorktreeError("source_commit_drift")
|
|
177
|
+
status_result = _run(runner, ["status", "--porcelain=v1", "-z", "--untracked-files=all"])
|
|
178
|
+
if status_result.returncode != 0:
|
|
179
|
+
raise WorktreeError("source_root_invalid")
|
|
180
|
+
if status_result.stdout:
|
|
181
|
+
raise WorktreeError("source_dirty")
|
|
182
|
+
index = _run(runner, ["ls-files", "-s", "-z"])
|
|
183
|
+
if index.returncode != 0:
|
|
184
|
+
raise WorktreeError("source_root_invalid")
|
|
185
|
+
_validate_index(index.stdout)
|
|
186
|
+
source_common_result = _run(runner, ["rev-parse", "--git-common-dir"])
|
|
187
|
+
if source_common_result.returncode != 0:
|
|
188
|
+
raise WorktreeError("source_root_invalid")
|
|
189
|
+
source_common = _resolve_git_path(source, source_common_result.stdout, "source_root_invalid")
|
|
190
|
+
|
|
191
|
+
added = _run(
|
|
192
|
+
runner,
|
|
193
|
+
["worktree", "add", "--detach", "--no-checkout", str(target), approved_commit],
|
|
194
|
+
)
|
|
195
|
+
if added.returncode != 0:
|
|
196
|
+
raise WorktreeError("worktree_create_failed")
|
|
197
|
+
checkout_runner = GitRunner(target)
|
|
198
|
+
checkout = _run(checkout_runner, ["checkout", "--detach", approved_commit])
|
|
199
|
+
if checkout.returncode != 0:
|
|
200
|
+
raise WorktreeError("worktree_quarantined")
|
|
201
|
+
prepared_root = _canonical_directory(target, "worktree_quarantined")
|
|
202
|
+
prepared_head = _run(checkout_runner, ["rev-parse", "HEAD"])
|
|
203
|
+
if prepared_head.returncode != 0 or _decode_line(prepared_head.stdout, "git_protocol") != approved_commit:
|
|
204
|
+
raise WorktreeError("worktree_quarantined")
|
|
205
|
+
symbolic = _run(checkout_runner, ["symbolic-ref", "-q", "HEAD"])
|
|
206
|
+
if symbolic.returncode == 0:
|
|
207
|
+
raise WorktreeError("worktree_quarantined")
|
|
208
|
+
common_result = _run(checkout_runner, ["rev-parse", "--git-common-dir"])
|
|
209
|
+
if common_result.returncode != 0:
|
|
210
|
+
raise WorktreeError("worktree_quarantined")
|
|
211
|
+
common = _resolve_git_path(prepared_root, common_result.stdout, "worktree_quarantined")
|
|
212
|
+
if common != source_common:
|
|
213
|
+
raise WorktreeError("worktree_quarantined")
|
|
214
|
+
final_status = _run(
|
|
215
|
+
checkout_runner, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]
|
|
216
|
+
)
|
|
217
|
+
if final_status.returncode != 0 or final_status.stdout:
|
|
218
|
+
raise WorktreeError("worktree_quarantined")
|
|
219
|
+
return TaskWorktree(task_id, prepared_root, common, approved_commit)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def cleanup_task_worktree(
|
|
223
|
+
worktree: TaskWorktree,
|
|
224
|
+
*,
|
|
225
|
+
state_root: Path,
|
|
226
|
+
task_id: str,
|
|
227
|
+
terminal_status: str,
|
|
228
|
+
authority_granted: bool,
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Remove a terminal task worktree only under explicit, rebound authority."""
|
|
231
|
+
if authority_granted is not True:
|
|
232
|
+
raise WorktreeError("cleanup_authority_required")
|
|
233
|
+
if (
|
|
234
|
+
not isinstance(worktree, TaskWorktree)
|
|
235
|
+
or task_id != worktree.worktree_id
|
|
236
|
+
or terminal_status not in _CLEANUP_TERMINAL_STATES
|
|
237
|
+
):
|
|
238
|
+
raise WorktreeError("cleanup_state_invalid")
|
|
239
|
+
state = _canonical_directory(state_root, "state_root_invalid")
|
|
240
|
+
expected = state / "tasks" / task_id
|
|
241
|
+
root = _canonical_directory(worktree.root, "cleanup_containment_invalid")
|
|
242
|
+
try:
|
|
243
|
+
expected_resolved = expected.resolve(strict=True)
|
|
244
|
+
except OSError:
|
|
245
|
+
raise WorktreeError("cleanup_containment_invalid") from None
|
|
246
|
+
if root != expected_resolved or root != worktree.root:
|
|
247
|
+
raise WorktreeError("cleanup_containment_invalid")
|
|
248
|
+
checkout_runner = GitRunner(root)
|
|
249
|
+
common_result = _run(checkout_runner, ["rev-parse", "--git-common-dir"])
|
|
250
|
+
if common_result.returncode != 0:
|
|
251
|
+
raise WorktreeError("cleanup_containment_invalid")
|
|
252
|
+
common = _resolve_git_path(root, common_result.stdout, "cleanup_containment_invalid")
|
|
253
|
+
if common != worktree.git_common_dir or common.name != ".git":
|
|
254
|
+
raise WorktreeError("cleanup_containment_invalid")
|
|
255
|
+
source = _canonical_directory(common.parent, "cleanup_containment_invalid")
|
|
256
|
+
source_runner = GitRunner(source)
|
|
257
|
+
removed = _run(source_runner, ["worktree", "remove", "--force", str(root)])
|
|
258
|
+
if removed.returncode != 0 or root.exists() or root.is_symlink():
|
|
259
|
+
raise WorktreeError("cleanup_failed")
|