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.
Files changed (112) hide show
  1. graphite/__init__.py +41 -0
  2. graphite/__main__.py +7 -0
  3. graphite/_cleanup_worker.py +525 -0
  4. graphite/activation.py +164 -0
  5. graphite/agent_hooks.py +577 -0
  6. graphite/agent_settings.py +226 -0
  7. graphite/analyze.py +146 -0
  8. graphite/answer_contract.py +420 -0
  9. graphite/bootstrap.py +210 -0
  10. graphite/buildlock.py +99 -0
  11. graphite/cache.py +131 -0
  12. graphite/channel.py +1325 -0
  13. graphite/cli.py +3053 -0
  14. graphite/cluster.py +111 -0
  15. graphite/config.py +209 -0
  16. graphite/context.py +355 -0
  17. graphite/daemon.py +745 -0
  18. graphite/daemon_health.py +733 -0
  19. graphite/debt.py +118 -0
  20. graphite/dependency_install.py +1597 -0
  21. graphite/detach.py +33 -0
  22. graphite/doctor.py +678 -0
  23. graphite/doctor_probes.py +2100 -0
  24. graphite/engine_identity.py +238 -0
  25. graphite/export/__init__.py +6 -0
  26. graphite/export/html.py +244 -0
  27. graphite/export/json.py +39 -0
  28. graphite/export/md.py +68 -0
  29. graphite/extract/__init__.py +4 -0
  30. graphite/extract/ast.py +1964 -0
  31. graphite/freshness.py +127 -0
  32. graphite/git.py +406 -0
  33. graphite/graph.py +117 -0
  34. graphite/graph_io.py +188 -0
  35. graphite/health.py +147 -0
  36. graphite/hook_entry.py +68 -0
  37. graphite/hookinstall.py +224 -0
  38. graphite/hookshim.py +86 -0
  39. graphite/incident_ledger.py +247 -0
  40. graphite/ingest.py +279 -0
  41. graphite/init.py +791 -0
  42. graphite/io.py +32 -0
  43. graphite/listing.py +51 -0
  44. graphite/llm.py +518 -0
  45. graphite/llm_probe.py +157 -0
  46. graphite/mcp.py +7 -0
  47. graphite/mcp_server.py +450 -0
  48. graphite/natural_query.py +252 -0
  49. graphite/overlays.py +713 -0
  50. graphite/probe_process.py +879 -0
  51. graphite/probe_workspace.py +728 -0
  52. graphite/process_contracts.py +22 -0
  53. graphite/provider_observer.py +397 -0
  54. graphite/query.py +646 -0
  55. graphite/query_plan.py +97 -0
  56. graphite/replacement_audit.py +291 -0
  57. graphite/resolve.py +660 -0
  58. graphite/review.py +782 -0
  59. graphite/routing/__init__.py +5 -0
  60. graphite/routing/approval.py +362 -0
  61. graphite/routing/classifier.py +169 -0
  62. graphite/routing/claude_executor.py +419 -0
  63. graphite/routing/claude_probe.py +102 -0
  64. graphite/routing/cli_identity.py +84 -0
  65. graphite/routing/codex_executor.py +383 -0
  66. graphite/routing/codex_probe.py +93 -0
  67. graphite/routing/context_builder.py +327 -0
  68. graphite/routing/contracts.py +802 -0
  69. graphite/routing/diff_policy.py +468 -0
  70. graphite/routing/edit_apply.py +166 -0
  71. graphite/routing/effort.py +43 -0
  72. graphite/routing/lifecycle.py +771 -0
  73. graphite/routing/lifecycle_operator.py +227 -0
  74. graphite/routing/lifecycle_service.py +555 -0
  75. graphite/routing/lifecycle_storage.py +977 -0
  76. graphite/routing/ollama_executor.py +341 -0
  77. graphite/routing/ollama_probe.py +72 -0
  78. graphite/routing/openrouter_executor.py +338 -0
  79. graphite/routing/openrouter_probe.py +188 -0
  80. graphite/routing/policy.py +815 -0
  81. graphite/routing/probe_runner.py +543 -0
  82. graphite/routing/process_runner.py +523 -0
  83. graphite/routing/profiles.py +554 -0
  84. graphite/routing/prompt.py +58 -0
  85. graphite/routing/registry.py +444 -0
  86. graphite/routing/route_pool.py +629 -0
  87. graphite/routing/route_pool_execution.py +275 -0
  88. graphite/routing/schema_validation.py +169 -0
  89. graphite/routing/service.py +1263 -0
  90. graphite/routing/settings.py +99 -0
  91. graphite/routing/shadow.py +201 -0
  92. graphite/routing/storage.py +4001 -0
  93. graphite/routing/telemetry.py +346 -0
  94. graphite/routing/worktree.py +259 -0
  95. graphite/routing/zai_edit.py +113 -0
  96. graphite/routing/zai_executor.py +191 -0
  97. graphite/routing/zai_probe.py +126 -0
  98. graphite/savings.py +84 -0
  99. graphite/ts_bridge.py +142 -0
  100. graphite/ts_resolver.mjs +314 -0
  101. graphite/typescript_activation.py +1586 -0
  102. graphite/usage_ledger.py +156 -0
  103. graphite/validation.py +148 -0
  104. graphite/watch.py +167 -0
  105. graphite/windows_job.py +368 -0
  106. graphite/windows_startup.py +144 -0
  107. graphite/windows_task.py +212 -0
  108. graphite_code-0.3.0.dist-info/METADATA +743 -0
  109. graphite_code-0.3.0.dist-info/RECORD +112 -0
  110. graphite_code-0.3.0.dist-info/WHEEL +4 -0
  111. graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
  112. graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,327 @@
1
+ """Contained, bounded context selection with a public outbound manifest."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import base64
6
+ import json
7
+ import os
8
+ import re
9
+ import stat
10
+ from dataclasses import dataclass
11
+ from pathlib import Path, PurePosixPath
12
+ from typing import Any, Final
13
+
14
+ from .contracts import TaskRequest
15
+ from .settings import RoutingSettings
16
+
17
+ CONTEXT_SCHEMA_VERSION: Final = "1"
18
+ _REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
19
+ _EXCLUDED_COMPONENTS = frozenset(
20
+ {
21
+ ".git",
22
+ ".graphite",
23
+ ".cache",
24
+ ".pytest_cache",
25
+ ".mypy_cache",
26
+ ".ruff_cache",
27
+ "__pycache__",
28
+ "graph-out",
29
+ "node_modules",
30
+ "dist",
31
+ "build",
32
+ "coverage",
33
+ "vendor",
34
+ ".venv",
35
+ "venv",
36
+ }
37
+ )
38
+ _SENSITIVE_SUFFIXES = frozenset({".pem", ".key", ".p12", ".pfx", ".crt", ".cer", ".der"})
39
+ _SENSITIVE_NAMES = frozenset(
40
+ {
41
+ ".npmrc",
42
+ ".pypirc",
43
+ "credentials",
44
+ "credentials.json",
45
+ "id_rsa",
46
+ "id_ed25519",
47
+ "keystore",
48
+ }
49
+ )
50
+ _ENCODED_TOKEN = re.compile(r"(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{24,}={0,2}(?![A-Za-z0-9+/])")
51
+ _SECRET_PATTERNS = (
52
+ re.compile(r"(?i)(?:api[_-]?key|secret|token|password)\s*[:=]\s*[^\s]{8,}"),
53
+ re.compile(r"(?i)authorization\s*:\s*bearer\s+[^\s]{8,}"),
54
+ re.compile(r"-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----"),
55
+ re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
56
+ )
57
+
58
+
59
+ class ContextError(RuntimeError):
60
+ """A stable, path-free context construction failure."""
61
+
62
+ def __init__(self, code: str) -> None:
63
+ self.code = code
64
+ super().__init__(code)
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class ManifestItem:
69
+ path: str
70
+ byte_count: int
71
+ sha256: str
72
+ reason: str
73
+ redaction_count: int = 0
74
+
75
+ def to_dict(self) -> dict[str, Any]:
76
+ return {
77
+ "path": self.path,
78
+ "byte_count": self.byte_count,
79
+ "sha256": self.sha256,
80
+ "reason": self.reason,
81
+ "redaction_count": self.redaction_count,
82
+ }
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class PrivateContextItem:
87
+ path: str
88
+ content: str
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class OutboundManifest:
93
+ schema_version: str
94
+ items: tuple[ManifestItem, ...]
95
+ total_bytes: int
96
+ excluded_count: int
97
+ manifest_hash: str
98
+
99
+ def to_dict(self) -> dict[str, Any]:
100
+ return {
101
+ "schema_version": self.schema_version,
102
+ "items": [item.to_dict() for item in self.items],
103
+ "total_bytes": self.total_bytes,
104
+ "excluded_count": self.excluded_count,
105
+ "manifest_hash": self.manifest_hash,
106
+ }
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class ContextBundle:
111
+ manifest: OutboundManifest
112
+ private_items: tuple[PrivateContextItem, ...]
113
+
114
+
115
+ def _is_reparse(metadata: os.stat_result) -> bool:
116
+ return bool(getattr(metadata, "st_file_attributes", 0) & _REPARSE_POINT)
117
+
118
+
119
+ def _signature(metadata: os.stat_result) -> tuple[int, int, int, int]:
120
+ return (
121
+ int(metadata.st_dev),
122
+ int(metadata.st_ino),
123
+ int(metadata.st_size),
124
+ int(metadata.st_mtime_ns),
125
+ )
126
+
127
+
128
+ def _normalize_relative(value: object) -> str | None:
129
+ if not isinstance(value, str) or not value or "\x00" in value:
130
+ return None
131
+ normalized = value.replace("\\", "/")
132
+ pure = PurePosixPath(normalized)
133
+ if normalized.startswith("/") or any(part in {"", ".", ".."} for part in pure.parts):
134
+ return None
135
+ if pure.parts and re.match(r"^[A-Za-z]:$", pure.parts[0]):
136
+ return None
137
+ return pure.as_posix()
138
+
139
+
140
+ def _candidate_reasons(request: TaskRequest, graph: Any) -> dict[str, str]:
141
+ reasons: dict[str, str] = {target: "explicit_target" for target in request.targets}
142
+ target_set = set(request.targets)
143
+ matched_nodes: list[str] = []
144
+ try:
145
+ nodes = graph.nodes(data=True)
146
+ except (AttributeError, TypeError):
147
+ return reasons
148
+ for node_id, data in nodes:
149
+ if isinstance(data, dict) and data.get("source_file") in target_set:
150
+ matched_nodes.append(str(node_id))
151
+ for node in sorted(set(matched_nodes)):
152
+ try:
153
+ dependencies = sorted(graph.successors(node), key=str)
154
+ dependents = sorted(graph.predecessors(node), key=str)
155
+ except (AttributeError, KeyError):
156
+ continue
157
+ for neighbor, reason in (
158
+ *((item, "dependency") for item in dependencies),
159
+ *((item, "dependent") for item in dependents),
160
+ ):
161
+ try:
162
+ data = graph.nodes[neighbor]
163
+ except (AttributeError, KeyError):
164
+ continue
165
+ relative = _normalize_relative(data.get("source_file") if isinstance(data, dict) else None)
166
+ if relative is not None:
167
+ reasons.setdefault(relative, reason)
168
+ return reasons
169
+
170
+
171
+ def _excluded_by_name(relative: str, exclusions: frozenset[str]) -> bool:
172
+ pure = PurePosixPath(relative)
173
+ lowered_parts = tuple(part.casefold() for part in pure.parts)
174
+ name = pure.name.casefold()
175
+ if any(part in _EXCLUDED_COMPONENTS for part in lowered_parts):
176
+ return True
177
+ if (
178
+ name == ".env"
179
+ or name.startswith(".env.")
180
+ or name in _SENSITIVE_NAMES
181
+ or pure.suffix.casefold() in _SENSITIVE_SUFFIXES
182
+ or pure.suffix.casefold() in {".jks", ".kdbx", ".keystore"}
183
+ ):
184
+ return True
185
+ return any(relative == exclusion or relative.startswith(exclusion.rstrip("/") + "/") for exclusion in exclusions)
186
+
187
+
188
+ def _read_stable(root: Path, relative: str, limit: int) -> bytes | None:
189
+ candidate = root / PurePosixPath(relative)
190
+ current = root
191
+ for part in PurePosixPath(relative).parts[:-1]:
192
+ current /= part
193
+ try:
194
+ metadata = current.lstat()
195
+ except OSError:
196
+ return None
197
+ if stat.S_ISLNK(metadata.st_mode) or _is_reparse(metadata) or not stat.S_ISDIR(metadata.st_mode):
198
+ return None
199
+ try:
200
+ before = candidate.lstat()
201
+ resolved = candidate.resolve(strict=True)
202
+ resolved.relative_to(root)
203
+ except (OSError, ValueError):
204
+ return None
205
+ if stat.S_ISLNK(before.st_mode) or _is_reparse(before) or not stat.S_ISREG(before.st_mode):
206
+ return None
207
+ if before.st_size > limit:
208
+ return None
209
+ flags = os.O_RDONLY | getattr(os, "O_BINARY", 0)
210
+ try:
211
+ descriptor = os.open(candidate, flags)
212
+ try:
213
+ opened = os.fstat(descriptor)
214
+ if _signature(opened) != _signature(before):
215
+ raise ContextError("context_file_changed")
216
+ chunks: list[bytes] = []
217
+ remaining = limit + 1
218
+ while remaining > 0:
219
+ chunk = os.read(descriptor, min(64 * 1024, remaining))
220
+ if not chunk:
221
+ break
222
+ chunks.append(chunk)
223
+ remaining -= len(chunk)
224
+ after = os.fstat(descriptor)
225
+ finally:
226
+ os.close(descriptor)
227
+ except ContextError:
228
+ raise
229
+ except OSError:
230
+ return None
231
+ data = b"".join(chunks)
232
+ if len(data) > limit:
233
+ return None
234
+ if _signature(after) != _signature(opened):
235
+ raise ContextError("context_file_changed")
236
+ return data
237
+
238
+
239
+ def _safe_text(data: bytes) -> str | None:
240
+ if b"\x00" in data:
241
+ return None
242
+ try:
243
+ text = data.decode("utf-8")
244
+ except UnicodeDecodeError:
245
+ return None
246
+ if any(pattern.search(text) for pattern in _SECRET_PATTERNS):
247
+ return None
248
+ for match in _ENCODED_TOKEN.finditer(text):
249
+ try:
250
+ decoded = base64.b64decode(match.group(0), validate=True).decode("utf-8")
251
+ except (ValueError, UnicodeDecodeError):
252
+ continue
253
+ if any(pattern.search(decoded) for pattern in _SECRET_PATTERNS):
254
+ return None
255
+ return text
256
+
257
+
258
+ def build_routing_context(
259
+ request: TaskRequest,
260
+ graph: Any,
261
+ settings: RoutingSettings,
262
+ *,
263
+ exclusions: tuple[str, ...] = (),
264
+ ) -> ContextBundle:
265
+ """Build private context plus a source-free public approval manifest."""
266
+ root = request.repository_root
267
+ try:
268
+ metadata = root.lstat()
269
+ except OSError as exc:
270
+ raise ContextError("context_root_invalid") from exc
271
+ if stat.S_ISLNK(metadata.st_mode) or _is_reparse(metadata) or not stat.S_ISDIR(metadata.st_mode):
272
+ raise ContextError("context_root_invalid")
273
+ root = root.resolve(strict=True)
274
+ if not 1 <= settings.max_context_files <= 128:
275
+ raise ContextError("context_file_limit_invalid")
276
+ if not 16_384 <= settings.max_context_bytes <= 4_194_304:
277
+ raise ContextError("context_byte_limit_invalid")
278
+ normalized_exclusions = frozenset(
279
+ item for value in exclusions if (item := _normalize_relative(value)) is not None
280
+ )
281
+ reasons = _candidate_reasons(request, graph)
282
+ items: list[ManifestItem] = []
283
+ private: list[PrivateContextItem] = []
284
+ excluded_count = 0
285
+ total_bytes = 0
286
+ for raw_relative in sorted(reasons):
287
+ relative = _normalize_relative(raw_relative)
288
+ if relative is None or _excluded_by_name(relative, normalized_exclusions):
289
+ excluded_count += 1
290
+ continue
291
+ if len(items) >= settings.max_context_files:
292
+ excluded_count += 1
293
+ continue
294
+ remaining = settings.max_context_bytes - total_bytes
295
+ if remaining <= 0:
296
+ excluded_count += 1
297
+ continue
298
+ data = _read_stable(root, relative, remaining)
299
+ if data is None:
300
+ excluded_count += 1
301
+ continue
302
+ text = _safe_text(data)
303
+ if text is None:
304
+ excluded_count += 1
305
+ continue
306
+ digest = hashlib.sha256(data).hexdigest()
307
+ items.append(ManifestItem(relative, len(data), digest, reasons[raw_relative]))
308
+ total_bytes += len(data)
309
+ if request.data_policy == "source_allowed":
310
+ private.append(PrivateContextItem(relative, text))
311
+ material = {
312
+ "schema_version": CONTEXT_SCHEMA_VERSION,
313
+ "items": [item.to_dict() for item in items],
314
+ "total_bytes": total_bytes,
315
+ "excluded_count": excluded_count,
316
+ }
317
+ manifest_hash = hashlib.sha256(
318
+ json.dumps(material, sort_keys=True, separators=(",", ":")).encode("utf-8")
319
+ ).hexdigest()
320
+ manifest = OutboundManifest(
321
+ CONTEXT_SCHEMA_VERSION,
322
+ tuple(items),
323
+ total_bytes,
324
+ excluded_count,
325
+ manifest_hash,
326
+ )
327
+ return ContextBundle(manifest, tuple(private))