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,113 @@
1
+ """Bridge a z.ai plain-text whole-file edit response into the shared apply payload.
2
+
3
+ z.ai's native executor returns a bare plain-text message (no JSON envelope), so
4
+ the model emits each edited file verbatim between path-qualified markers and a
5
+ completion sentinel. This parser converts that into the exact payload
6
+ ``apply_whole_file_edit`` consumes. It performs structural parsing plus a
7
+ defense-in-depth scope-set check only; ``apply_whole_file_edit`` remains the
8
+ sole authority on path safety and byte caps. Every non-conformance raises
9
+ ``AdapterError("response_contract_invalid")``.
10
+
11
+ Preamble and interstitial prose (e.g. "Here are the files:") are tolerated by
12
+ design — non-marker lines outside blocks are skipped and the live smoke relies
13
+ on this. Two accepted, documented consequences: (1) a content line byte-identical
14
+ to its own path-qualified end marker truncates at that line (line boundaries
15
+ follow ``str.splitlines`` — any Unicode line terminator, not only ``\\n`` —
16
+ though content bytes are always sliced verbatim from the raw message, so
17
+ extraction stays byte-exact); (2) prose containing a begin-marker-shaped line
18
+ yields a phantom block, which surfaces as a scope-set mismatch (rejected, not
19
+ applied). Path-qualified markers plus first-matching-end parsing make both
20
+ improbable.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import re
25
+
26
+ from .claude_executor import AdapterError
27
+ from .edit_apply import EDIT_RESULT_MARKER
28
+
29
+ EDIT_BEGIN_TEMPLATE = "===GRAPHITE BEGIN FILE {path}==="
30
+ EDIT_END_TEMPLATE = "===GRAPHITE END FILE {path}==="
31
+
32
+ # Trailing spaces/tabs and a CR are tolerated on marker lines (models append
33
+ # them constantly); a content line must still start with the exact marker
34
+ # prefix to match, so this stays false-positive-safe.
35
+ _BEGIN_RE = re.compile(r"^===GRAPHITE BEGIN FILE (?P<path>.+?)===[ \t]*\r?$")
36
+ _END_RE = re.compile(r"^===GRAPHITE END FILE (?P<path>.+?)===[ \t]*\r?$")
37
+
38
+
39
+ def _fail() -> AdapterError:
40
+ return AdapterError("response_contract_invalid")
41
+
42
+
43
+ def parse_whole_file_edit_text(message: str, *, edit_scope: tuple[str, ...]) -> dict:
44
+ """Parse a plain-text multi-file edit response into an apply payload.
45
+
46
+ Returns ``{"files": [{"path","content"}, …], "result": EDIT_RESULT_MARKER}``
47
+ with files ordered to match ``edit_scope``. Raises
48
+ ``AdapterError("response_contract_invalid")`` on any non-conformance.
49
+ """
50
+ if not isinstance(message, str) or not message:
51
+ raise _fail()
52
+ if (
53
+ not isinstance(edit_scope, tuple)
54
+ or not edit_scope
55
+ or not all(isinstance(path, str) for path in edit_scope)
56
+ or len(set(edit_scope)) != len(edit_scope)
57
+ ):
58
+ raise _fail()
59
+
60
+ lines = message.splitlines(keepends=True)
61
+ starts: list[int] = []
62
+ offset = 0
63
+ for line in lines:
64
+ starts.append(offset)
65
+ offset += len(line)
66
+ total = offset
67
+
68
+ files: list[dict[str, str]] = []
69
+ seen: set[str] = set()
70
+ covered: list[tuple[int, int]] = []
71
+ idx = 0
72
+ n = len(lines)
73
+ while idx < n:
74
+ begin = _BEGIN_RE.match(lines[idx].rstrip("\n"))
75
+ if begin is None:
76
+ idx += 1
77
+ continue
78
+ path = begin.group("path")
79
+ content_start = starts[idx] + len(lines[idx])
80
+ jdx = idx + 1
81
+ end_idx: int | None = None
82
+ while jdx < n:
83
+ stripped = lines[jdx].rstrip("\n")
84
+ end = _END_RE.match(stripped)
85
+ if end is not None and end.group("path") == path:
86
+ end_idx = jdx
87
+ break
88
+ if _BEGIN_RE.match(stripped) is not None:
89
+ break # a new begin before our end -> malformed block
90
+ jdx += 1
91
+ if end_idx is None:
92
+ raise _fail()
93
+ if path in seen:
94
+ raise _fail()
95
+ seen.add(path)
96
+ files.append({"path": path, "content": message[content_start:starts[end_idx]]})
97
+ covered.append((starts[idx], starts[end_idx] + len(lines[end_idx])))
98
+ idx = end_idx + 1
99
+
100
+ if not files or seen != set(edit_scope):
101
+ raise _fail()
102
+
103
+ residual: list[str] = []
104
+ cursor = 0
105
+ for block_start, block_end in covered:
106
+ residual.append(message[cursor:block_start])
107
+ cursor = block_end
108
+ residual.append(message[cursor:total])
109
+ if EDIT_RESULT_MARKER not in "".join(residual):
110
+ raise _fail()
111
+
112
+ ordered = sorted(files, key=lambda item: edit_scope.index(item["path"]))
113
+ return {"files": ordered, "result": EDIT_RESULT_MARKER}
@@ -0,0 +1,191 @@
1
+ """Isolated hardened adapter for one bounded z.ai chat completion (plain text)."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import math
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass, field
9
+
10
+ from .claude_executor import AdapterError
11
+ from .lifecycle import LifecycleProviderId
12
+ from .probe_runner import (
13
+ MAX_INFERENCE_REQUEST_BYTES,
14
+ MAX_INFERENCE_RESPONSE_BYTES,
15
+ MAX_INFERENCE_TIMEOUT_SECONDS,
16
+ HttpProbeEndpoint,
17
+ HttpProbeResult,
18
+ ProbeEndpointPurpose,
19
+ ProviderProbeError,
20
+ run_http_probe,
21
+ )
22
+ from .zai_probe import ZAI_HOST, ZaiPricing, zai_cost_microunits
23
+
24
+ MAX_TOKEN_COUNT = 10_000_000
25
+ MAX_COST_MICROUNITS = 1_000_000_000
26
+
27
+ HttpProbe = Callable[..., HttpProbeResult]
28
+
29
+ _EXECUTION_TRANSPORT_CODES = {
30
+ "probe_response_limit": "response_limit",
31
+ "probe_timeout": "timeout",
32
+ "probe_http_status": "http_status",
33
+ "probe_redirect_rejected": "http_status",
34
+ }
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class ZaiExecutionResult:
39
+ """Sanitized outcome of one bounded z.ai plain-text execution."""
40
+
41
+ effective_model: str
42
+ message: str = field(repr=False)
43
+ input_tokens: int
44
+ output_tokens: int
45
+ cost_microunits: int
46
+ duration_seconds: float
47
+ request_sha256: str
48
+ response_sha256: str
49
+
50
+
51
+ def _model_name(value: object) -> str:
52
+ if (
53
+ not isinstance(value, str)
54
+ or not value
55
+ or len(value) > 128
56
+ or any(character.isspace() for character in value)
57
+ ):
58
+ raise AdapterError("request_invalid")
59
+ return value
60
+
61
+
62
+ def _token(value: object) -> int:
63
+ if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= MAX_TOKEN_COUNT:
64
+ raise AdapterError("protocol")
65
+ return value
66
+
67
+
68
+ def execute_zai(
69
+ *,
70
+ api_key: str,
71
+ prompt: bytes,
72
+ requested_model: str,
73
+ expected_effective_model: str,
74
+ pricing: ZaiPricing,
75
+ max_output_tokens: int,
76
+ max_cost_microunits: int,
77
+ timeout_seconds: float,
78
+ transport: HttpProbe = run_http_probe,
79
+ ) -> ZaiExecutionResult:
80
+ """Perform exactly one bounded z.ai chat completion returning plain text; no retries."""
81
+ if not isinstance(api_key, str) or not api_key:
82
+ raise AdapterError("auth_required")
83
+ if len(api_key) > 4096 or any(character in api_key for character in "\r\n\x00"):
84
+ raise AdapterError("request_invalid")
85
+ if (
86
+ not isinstance(prompt, bytes)
87
+ or not prompt
88
+ or len(prompt) > MAX_INFERENCE_REQUEST_BYTES
89
+ ):
90
+ raise AdapterError("request_invalid")
91
+ try:
92
+ prompt_text = prompt.decode("utf-8")
93
+ except UnicodeDecodeError:
94
+ raise AdapterError("request_invalid") from None
95
+ requested = _model_name(requested_model)
96
+ expected = _model_name(expected_effective_model)
97
+ if not isinstance(pricing, ZaiPricing):
98
+ raise AdapterError("request_invalid")
99
+ for value, maximum in (
100
+ (max_output_tokens, MAX_TOKEN_COUNT),
101
+ (max_cost_microunits, MAX_COST_MICROUNITS),
102
+ ):
103
+ if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= maximum:
104
+ raise AdapterError("request_invalid")
105
+ if (
106
+ isinstance(timeout_seconds, bool)
107
+ or not isinstance(timeout_seconds, (int, float))
108
+ or not math.isfinite(timeout_seconds)
109
+ or not 0.1 <= timeout_seconds <= MAX_INFERENCE_TIMEOUT_SECONDS
110
+ ):
111
+ raise AdapterError("request_invalid")
112
+ payload = {
113
+ "max_tokens": max_output_tokens,
114
+ "messages": [{"content": prompt_text, "role": "user"}],
115
+ "model": requested,
116
+ "stream": False,
117
+ "temperature": 0,
118
+ # glm-5.2 is a reasoning model; with thinking on it consumes the entire
119
+ # output budget on reasoning_tokens and returns empty content
120
+ # (finish_reason=length), so the bounded plain-text answer is never
121
+ # emitted. Disable thinking so the exact response is produced
122
+ # deterministically within the token budget (z.ai honors this).
123
+ "thinking": {"type": "disabled"},
124
+ }
125
+ body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
126
+ if len(body) > MAX_INFERENCE_REQUEST_BYTES:
127
+ raise AdapterError("request_invalid")
128
+ endpoint = HttpProbeEndpoint(
129
+ LifecycleProviderId.ZAI,
130
+ "https",
131
+ ZAI_HOST,
132
+ 443,
133
+ ProbeEndpointPurpose.ZAI_CHAT_COMPLETIONS,
134
+ )
135
+ try:
136
+ result = transport(
137
+ endpoint=endpoint,
138
+ timeout_seconds=timeout_seconds,
139
+ request_body=body,
140
+ authorization=f"Bearer {api_key}",
141
+ max_response_bytes=MAX_INFERENCE_RESPONSE_BYTES,
142
+ )
143
+ except ProviderProbeError as error:
144
+ raise AdapterError(
145
+ _EXECUTION_TRANSPORT_CODES.get(error.code, "unavailable")
146
+ ) from None
147
+ except Exception:
148
+ raise AdapterError("unavailable") from None
149
+ if not isinstance(result, HttpProbeResult):
150
+ raise AdapterError("unavailable")
151
+ try:
152
+ envelope = json.loads(result.body.decode("utf-8"))
153
+ except (UnicodeDecodeError, json.JSONDecodeError, RecursionError):
154
+ raise AdapterError("unavailable") from None
155
+ if not isinstance(envelope, dict):
156
+ raise AdapterError("protocol")
157
+ reported_model = envelope.get("model")
158
+ if not isinstance(reported_model, str) or not reported_model:
159
+ raise AdapterError("model_identity_unverified")
160
+ if reported_model != expected:
161
+ raise AdapterError("model_mismatch")
162
+ choices = envelope.get("choices")
163
+ if not isinstance(choices, list) or len(choices) != 1 or not isinstance(choices[0], dict):
164
+ raise AdapterError("protocol")
165
+ message = choices[0].get("message")
166
+ if not isinstance(message, dict):
167
+ raise AdapterError("protocol")
168
+ content = message.get("content")
169
+ if not isinstance(content, str):
170
+ raise AdapterError("protocol")
171
+ usage = envelope.get("usage")
172
+ if not isinstance(usage, dict):
173
+ raise AdapterError("protocol")
174
+ input_tokens = _token(usage.get("prompt_tokens"))
175
+ output_tokens = _token(usage.get("completion_tokens"))
176
+ try:
177
+ cost = zai_cost_microunits(pricing, input_tokens=input_tokens, output_tokens=output_tokens)
178
+ except ProviderProbeError:
179
+ raise AdapterError("protocol") from None
180
+ if cost > max_cost_microunits:
181
+ raise AdapterError("cost_ceiling_exceeded")
182
+ return ZaiExecutionResult(
183
+ expected,
184
+ content,
185
+ input_tokens,
186
+ output_tokens,
187
+ cost,
188
+ result.duration_seconds,
189
+ hashlib.sha256(body).hexdigest(),
190
+ hashlib.sha256(result.body).hexdigest(),
191
+ )
@@ -0,0 +1,126 @@
1
+ """Local (no-network) z.ai identity + operator-pinned pricing for governed verification."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import re
7
+ from dataclasses import dataclass
8
+ from decimal import Decimal, InvalidOperation
9
+
10
+ from .contracts import CliIdentity, ProviderId
11
+ from .lifecycle import (
12
+ LifecycleProviderId,
13
+ ProviderCompatibilityPolicy,
14
+ ProviderRuntimeIdentity,
15
+ RuntimeKind,
16
+ )
17
+ from .probe_runner import ProviderProbeError
18
+
19
+ ZAI_HOST = "api.z.ai"
20
+ ZAI_CANONICAL_ENDPOINT = "https://api.z.ai/api/paas/v4"
21
+ ZAI_MODEL = "glm-5.2"
22
+ ZAI_API_CONTRACT_VERSION = "1.0.0"
23
+ ZAI_ADAPTER_PROTOCOL_VERSION = "1.0.0"
24
+ ZAI_CAPABILITIES = ("remote_inference",)
25
+ # z.ai model ids are single-segment (no vendor/model slash, unlike OpenRouter).
26
+ _ZAI_MODEL_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
27
+ _PRICE = re.compile(r"^(0|[0-9]{1,10}(\.[0-9]{1,18})?|\.[0-9]{1,18})$")
28
+
29
+ # Compatibility policy for lifecycle observe(): promotes a DISCOVERED z.ai
30
+ # runtime identity to VERIFICATION_REQUIRED once it matches this shape.
31
+ # No `_OPENROUTER_POLICY` (or any other per-provider policy constant)
32
+ # exists elsewhere in this codebase to clone; policies are otherwise built
33
+ # ad hoc at each call site. This constant is the zai-owned analog of where
34
+ # such a policy would live for OpenRouter (openrouter_probe.py).
35
+ _ZAI_POLICY = ProviderCompatibilityPolicy(
36
+ provider=LifecycleProviderId.ZAI,
37
+ runtime_kind=RuntimeKind.REMOTE_HTTPS,
38
+ policy_version="1.0.0",
39
+ minimum_version="1.0.0",
40
+ maximum_version_exclusive="2.0.0",
41
+ required_capabilities=ZAI_CAPABILITIES,
42
+ )
43
+
44
+
45
+ def _digest(value: object) -> str:
46
+ return hashlib.sha256(
47
+ json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
48
+ ).hexdigest()
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class ZaiPricing:
53
+ """Operator-pinned per-token USD prices as exact decimal strings."""
54
+
55
+ prompt: str
56
+ completion: str
57
+
58
+ def __post_init__(self) -> None:
59
+ for value in (self.prompt, self.completion):
60
+ if not isinstance(value, str) or len(value) > 64 or _PRICE.fullmatch(value) is None:
61
+ raise ProviderProbeError("probe_protocol_invalid")
62
+ try:
63
+ parsed = Decimal(value)
64
+ except InvalidOperation:
65
+ raise ProviderProbeError("probe_protocol_invalid") from None
66
+ if not Decimal(0) <= parsed <= Decimal(1):
67
+ raise ProviderProbeError("probe_protocol_invalid")
68
+
69
+ @property
70
+ def digest(self) -> str:
71
+ return _digest({"completion": self.completion, "prompt": self.prompt})
72
+
73
+
74
+ def zai_cost_microunits(pricing: ZaiPricing, *, input_tokens: int, output_tokens: int) -> int:
75
+ """Ceiling of the exact-decimal USD cost in microunits."""
76
+ if not isinstance(pricing, ZaiPricing):
77
+ raise ProviderProbeError("probe_request_invalid")
78
+ for value in (input_tokens, output_tokens):
79
+ if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= 100_000_000:
80
+ raise ProviderProbeError("probe_request_invalid")
81
+ cost = (
82
+ Decimal(input_tokens) * Decimal(pricing.prompt)
83
+ + Decimal(output_tokens) * Decimal(pricing.completion)
84
+ ) * Decimal(1_000_000)
85
+ whole = int(cost)
86
+ return whole if cost == whole else whole + 1
87
+
88
+
89
+ @dataclass(frozen=True, slots=True)
90
+ class ZaiPreflight:
91
+ identity: CliIdentity
92
+ runtime: ProviderRuntimeIdentity
93
+ pricing: ZaiPricing
94
+
95
+
96
+ def preflight_zai(*, model_id: str, observed_at: int, policy_version: str) -> ZaiPreflight:
97
+ """Construct the z.ai runtime + CLI identity and pinned pricing locally (no network)."""
98
+ if not isinstance(model_id, str) or _ZAI_MODEL_ID.fullmatch(model_id) is None:
99
+ raise ProviderProbeError("probe_request_invalid")
100
+ pricing = ZaiPricing(prompt="0.0000014", completion="0.0000044")
101
+ endpoint_digest = hashlib.sha256(ZAI_CANONICAL_ENDPOINT.encode("ascii")).hexdigest()
102
+ model_digest = _digest(model_id)
103
+ try:
104
+ runtime = ProviderRuntimeIdentity(
105
+ LifecycleProviderId.ZAI,
106
+ RuntimeKind.REMOTE_HTTPS,
107
+ ZAI_API_CONTRACT_VERSION,
108
+ endpoint_digest,
109
+ model_digest,
110
+ None, # routing_policy_digest forbidden for zai
111
+ ZAI_CAPABILITIES,
112
+ policy_version,
113
+ observed_at,
114
+ )
115
+ except ValueError:
116
+ raise ProviderProbeError("probe_request_invalid") from None
117
+ composite = _digest(
118
+ {"endpoint": endpoint_digest, "model": model_digest, "pricing": pricing.digest}
119
+ )
120
+ identity = CliIdentity(
121
+ ProviderId.ZAI,
122
+ composite,
123
+ ZAI_API_CONTRACT_VERSION,
124
+ ZAI_ADAPTER_PROTOCOL_VERSION,
125
+ )
126
+ return ZaiPreflight(identity, runtime, pricing)
graphite/savings.py ADDED
@@ -0,0 +1,84 @@
1
+ """Result-scaled counterfactual estimator for the savings display.
2
+
3
+ The numbers are ESTIMATES of what equivalent manual exploration would have
4
+ cost, scaled by what each graphite answer actually contained; they are never
5
+ measurements. ``methodology()`` prints the formula and constants next to any
6
+ report so the claim stays auditable.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any, Iterable
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class SavingsModel:
16
+ grep_tokens: int = 1000
17
+ grep_seconds: int = 20
18
+ read_seconds: int = 10
19
+ file_token_cap: int = 2000
20
+
21
+
22
+ MODEL = SavingsModel()
23
+
24
+
25
+ def _num(value: Any, cast: type) -> Any:
26
+ """Best-effort numeric coercion; schema-corrupt values count as zero."""
27
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
28
+ return cast(0)
29
+ return cast(value)
30
+
31
+
32
+ def estimate_entry(entry: dict[str, Any], model: SavingsModel = MODEL) -> dict[str, Any]:
33
+ files = entry.get("files") or []
34
+ count = len(files)
35
+ grep_rounds = 1 + count // 10
36
+ manual_tokens = grep_rounds * model.grep_tokens + sum(
37
+ min(_num(f.get("bytes", 0), int) // 4, model.file_token_cap) for f in files if isinstance(f, dict)
38
+ )
39
+ manual_seconds = float(grep_rounds * model.grep_seconds + count * model.read_seconds)
40
+ cost_tokens = _num(entry.get("output_bytes", 0), int) // 4
41
+ cost_seconds = _num(entry.get("wall_ms", 0), float) / 1000.0
42
+ return {
43
+ "tokens_saved": max(0, manual_tokens - cost_tokens),
44
+ "seconds_saved": max(0.0, manual_seconds - cost_seconds),
45
+ }
46
+
47
+
48
+ def summarize(entries: Iterable[dict[str, Any]], model: SavingsModel = MODEL) -> dict[str, Any]:
49
+ total_tokens = 0
50
+ total_seconds = 0.0
51
+ count = 0
52
+ by_cmd: dict[str, dict[str, Any]] = {}
53
+ for entry in entries:
54
+ est = estimate_entry(entry, model)
55
+ cmd = str(entry.get("cmd", "unknown"))
56
+ bucket = by_cmd.setdefault(cmd, {"count": 0, "tokens_saved": 0, "seconds_saved": 0.0})
57
+ bucket["count"] += 1
58
+ bucket["tokens_saved"] += est["tokens_saved"]
59
+ bucket["seconds_saved"] += est["seconds_saved"]
60
+ total_tokens += est["tokens_saved"]
61
+ total_seconds += est["seconds_saved"]
62
+ count += 1
63
+ return {
64
+ "count": count,
65
+ "tokens_saved": total_tokens,
66
+ "seconds_saved": total_seconds,
67
+ "by_cmd": by_cmd,
68
+ }
69
+
70
+
71
+ def methodology(model: SavingsModel = MODEL) -> str:
72
+ return (
73
+ "All figures are estimates of avoided manual exploration, never measurements. "
74
+ f"Model: an answer covering K files ~ (1 + K//10) grep rounds x {model.grep_tokens} tokens "
75
+ f"/ {model.grep_seconds}s each, plus per-file read of min(bytes/4, {model.file_token_cap}) tokens "
76
+ f"/ {model.read_seconds}s; minus graphite's actual cost (output bytes/4 tokens, measured wall time); "
77
+ "floored at zero."
78
+ )
79
+
80
+
81
+ def format_compact(tokens: int, seconds: float) -> str:
82
+ token_text = f"~{tokens / 1000:.1f}k tokens" if tokens >= 1000 else f"~{tokens} tokens"
83
+ time_text = f"~{seconds / 60:.0f} min" if seconds >= 90 else f"~{seconds:.0f}s"
84
+ return f"{token_text} / {time_text}"
graphite/ts_bridge.py ADDED
@@ -0,0 +1,142 @@
1
+ """Optional TypeScript compiler bridge for accurate import/export resolution."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import subprocess
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path, PurePosixPath
8
+ from typing import Iterable, Any
9
+
10
+ from .config import Config
11
+
12
+ _TS_LANGUAGES = {"javascript", "typescript", "tsx", "jsx"}
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class TypeScriptCompilerEdge:
17
+ source: str
18
+ target: str
19
+ specifier: str
20
+ syntax: str
21
+ relation: str
22
+ confidence: str
23
+ line: int | None = None
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class TypeScriptCompilerIndex:
28
+ available: bool
29
+ reason: str | None = None
30
+ typescript_version: str | None = None
31
+ config_path: str | None = None
32
+ import_map: dict[tuple[str, str], TypeScriptCompilerEdge] = field(default_factory=dict)
33
+ edges_by_file: dict[str, tuple[TypeScriptCompilerEdge, ...]] = field(default_factory=dict)
34
+
35
+ def resolve_import(self, rel_path: str, specifier: str) -> TypeScriptCompilerEdge | None:
36
+ return self.import_map.get((PurePosixPath(rel_path).as_posix(), specifier))
37
+
38
+ def supplemental_edges(self, rel_path: str) -> tuple[TypeScriptCompilerEdge, ...]:
39
+ return self.edges_by_file.get(PurePosixPath(rel_path).as_posix(), ())
40
+
41
+
42
+ _DISABLED = {"0", "false", "off", "none", "disabled", "heuristic"}
43
+
44
+
45
+ def build_typescript_index(root: Path, entries: Iterable[object], cfg: Config) -> TypeScriptCompilerIndex:
46
+ """Build a compiler-backed TS module-resolution index, falling back silently on failure."""
47
+ mode = cfg.typescript_resolver.strip().lower()
48
+ if mode in _DISABLED:
49
+ return TypeScriptCompilerIndex(available=False, reason="disabled")
50
+
51
+ rel_files = sorted(
52
+ PurePosixPath(entry.rel_path).as_posix()
53
+ for entry in entries
54
+ if getattr(entry, "language", None) in _TS_LANGUAGES
55
+ )
56
+ if not rel_files:
57
+ return TypeScriptCompilerIndex(available=False, reason="no_typescript_files")
58
+
59
+ script = Path(__file__).with_name("ts_resolver.mjs")
60
+ payload = json.dumps({"root": str(root), "files": rel_files, "symbolReferences": cfg.typescript_symbol_references}, ensure_ascii=False)
61
+ try:
62
+ completed = subprocess.run(
63
+ ["node", str(script)],
64
+ input=payload,
65
+ text=True,
66
+ # Node speaks UTF-8 in both directions, and the payload above is
67
+ # built with `ensure_ascii=False`, so it carries whatever non-ASCII
68
+ # appears in a repo's paths or identifiers. Without this the locale
69
+ # codec is used: on Windows that is cp1252, which cannot ENCODE the
70
+ # request and cannot DECODE node's reply. A decode failure there is
71
+ # silent -- it kills subprocess's reader thread and yields None.
72
+ encoding="utf-8",
73
+ errors="replace",
74
+ capture_output=True,
75
+ cwd=str(Path.cwd()),
76
+ timeout=cfg.typescript_resolver_timeout_seconds,
77
+ check=False,
78
+ )
79
+ except FileNotFoundError:
80
+ return TypeScriptCompilerIndex(available=False, reason="node_not_available")
81
+ except subprocess.TimeoutExpired:
82
+ return TypeScriptCompilerIndex(available=False, reason="timeout")
83
+ except Exception as exc:
84
+ return TypeScriptCompilerIndex(available=False, reason=f"bridge_error: {exc}")
85
+
86
+ if completed.returncode != 0:
87
+ detail = (completed.stderr or completed.stdout or "").strip()[:500]
88
+ return TypeScriptCompilerIndex(available=False, reason=f"node_exit_{completed.returncode}: {detail}")
89
+
90
+ try:
91
+ data = json.loads(completed.stdout)
92
+ except json.JSONDecodeError as exc:
93
+ return TypeScriptCompilerIndex(available=False, reason=f"invalid_json: {exc}")
94
+
95
+ if not data.get("ok"):
96
+ return TypeScriptCompilerIndex(available=False, reason=str(data.get("reason") or "unavailable"))
97
+
98
+ import_map: dict[tuple[str, str], TypeScriptCompilerEdge] = {}
99
+ edges_by_file: dict[str, list[TypeScriptCompilerEdge]] = {}
100
+ for raw in data.get("edges", []):
101
+ edge = _edge_from_raw(raw)
102
+ if edge is None:
103
+ continue
104
+ if edge.syntax == "import":
105
+ import_map[(edge.source, edge.specifier)] = edge
106
+ else:
107
+ edges_by_file.setdefault(edge.source, []).append(edge)
108
+
109
+ return TypeScriptCompilerIndex(
110
+ available=True,
111
+ typescript_version=data.get("typescriptVersion"),
112
+ config_path=data.get("configPath"),
113
+ import_map=import_map,
114
+ edges_by_file={k: tuple(v) for k, v in edges_by_file.items()},
115
+ )
116
+
117
+
118
+ def _edge_from_raw(raw: dict[str, Any]) -> TypeScriptCompilerEdge | None:
119
+ try:
120
+ source = PurePosixPath(str(raw["source"])).as_posix()
121
+ target = PurePosixPath(str(raw["target"])).as_posix()
122
+ specifier = str(raw["specifier"])
123
+ syntax = str(raw["syntax"])
124
+ relation = str(raw["relation"])
125
+ confidence = str(raw["confidence"])
126
+ line_raw = raw.get("line")
127
+ line = int(line_raw) if line_raw is not None else None
128
+ except (KeyError, TypeError, ValueError):
129
+ return None
130
+ if not source or not target or source == target:
131
+ return None
132
+ return TypeScriptCompilerEdge(
133
+ source=source,
134
+ target=target,
135
+ specifier=specifier,
136
+ syntax=syntax,
137
+ relation=relation,
138
+ confidence=confidence,
139
+ line=line,
140
+ )
141
+
142
+