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
graphite/overlays.py ADDED
@@ -0,0 +1,713 @@
1
+ """Non-authoritative, identity-bound storage for optional graph enrichment."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import math
7
+ import os
8
+ import re
9
+ import stat
10
+ import tempfile
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Final
14
+ from urllib.parse import urlsplit
15
+
16
+ from .config import Config
17
+ from .freshness import FreshnessLimitError, check_graph_freshness
18
+ from .graph_io import GraphReadError, load_validated_graph_bundle
19
+ from .llm import enrich_report
20
+
21
+ OVERLAY_SCHEMA_VERSION: Final = 1
22
+ MAX_OVERLAY_FILE_BYTES: Final = 1024 * 1024
23
+ MAX_SUMMARY_BYTES: Final = 64 * 1024
24
+ MAX_CANONICAL_MANIFEST_BYTES: Final = 16 * 1024 * 1024
25
+
26
+ _HEX_64 = re.compile(r"^[0-9a-f]{64}$")
27
+ _REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
28
+ _PROVIDERS = frozenset({"ollama", "openrouter"})
29
+ _FAILURE_CATEGORIES = frozenset(
30
+ {"authentication", "configuration", "connection", "provider_error", "timeout"}
31
+ )
32
+ _IDENTITY_FIELDS = (
33
+ "overlay_identity_digest",
34
+ "provider",
35
+ "provider_lifecycle_identity_digest",
36
+ "model_identity_digest",
37
+ "routing_policy_digest",
38
+ )
39
+ _PAYLOAD_FILE = re.compile(r"^payload-([0-9a-f]{64})\.json$")
40
+
41
+
42
+ class OverlayError(RuntimeError):
43
+ """A stable, path-free overlay failure."""
44
+
45
+
46
+ def _digest(value: object, code: str, *, optional: bool = False) -> str | None:
47
+ if value is None and optional:
48
+ return None
49
+ if not isinstance(value, str) or _HEX_64.fullmatch(value) is None:
50
+ raise ValueError(code)
51
+ return value
52
+
53
+
54
+ def _canonical_digest(value: object) -> str:
55
+ encoded = json.dumps(
56
+ value, sort_keys=True, separators=(",", ":"), ensure_ascii=True
57
+ ).encode("utf-8")
58
+ return hashlib.sha256(encoded).hexdigest()
59
+
60
+
61
+ def _is_reparse(metadata: os.stat_result) -> bool:
62
+ return bool(getattr(metadata, "st_file_attributes", 0) & _REPARSE_POINT)
63
+
64
+
65
+ def _is_link_or_reparse(metadata: os.stat_result) -> bool:
66
+ return stat.S_ISLNK(metadata.st_mode) or _is_reparse(metadata)
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class OverlayRequest:
71
+ repository_root: Path
72
+ output_dir: Path
73
+ provider: str
74
+ provider_lifecycle_identity_digest: str
75
+ model_identity_digest: str
76
+ routing_policy_digest: str | None
77
+ created_at: int
78
+
79
+ def __post_init__(self) -> None:
80
+ if not isinstance(self.repository_root, Path) or not self.repository_root.is_absolute():
81
+ raise ValueError("repository_root_invalid")
82
+ if not isinstance(self.output_dir, Path):
83
+ raise ValueError("output_dir_invalid")
84
+ if self.provider not in _PROVIDERS:
85
+ raise ValueError("provider_invalid")
86
+ lifecycle = _digest(
87
+ self.provider_lifecycle_identity_digest,
88
+ "provider_identity_digest_invalid",
89
+ )
90
+ model = _digest(self.model_identity_digest, "model_identity_digest_invalid")
91
+ routing = _digest(
92
+ self.routing_policy_digest,
93
+ "routing_policy_digest_invalid",
94
+ optional=True,
95
+ )
96
+ if self.provider == "openrouter" and routing is None:
97
+ raise ValueError("routing_policy_digest_invalid")
98
+ if self.provider != "openrouter" and routing is not None:
99
+ raise ValueError("routing_policy_digest_invalid")
100
+ if (
101
+ isinstance(self.created_at, bool)
102
+ or not isinstance(self.created_at, int)
103
+ or not 0 <= self.created_at <= 10**12
104
+ ):
105
+ raise ValueError("created_at_invalid")
106
+ object.__setattr__(self, "provider_lifecycle_identity_digest", lifecycle)
107
+ object.__setattr__(self, "model_identity_digest", model)
108
+ object.__setattr__(self, "routing_policy_digest", routing)
109
+
110
+ @property
111
+ def overlay_identity_digest(self) -> str:
112
+ return _canonical_digest(
113
+ {
114
+ "provider": self.provider,
115
+ "provider_lifecycle_identity_digest": self.provider_lifecycle_identity_digest,
116
+ "model_identity_digest": self.model_identity_digest,
117
+ "routing_policy_digest": self.routing_policy_digest,
118
+ }
119
+ )
120
+
121
+
122
+ def _lexical_output_root(request: OverlayRequest) -> Path:
123
+ root = request.repository_root
124
+ output = request.output_dir if request.output_dir.is_absolute() else root / request.output_dir
125
+ lexical = output.absolute()
126
+ try:
127
+ relative = lexical.relative_to(root)
128
+ except ValueError as exc:
129
+ raise OverlayError("overlay_output_outside_root") from exc
130
+ if ".." in relative.parts:
131
+ raise OverlayError("overlay_output_outside_root")
132
+ return lexical
133
+
134
+
135
+ def overlay_directory(request: OverlayRequest) -> Path:
136
+ """Return the identity-derived overlay directory without touching storage."""
137
+ return (
138
+ _lexical_output_root(request)
139
+ / "overlays"
140
+ / request.provider
141
+ / request.overlay_identity_digest
142
+ )
143
+
144
+
145
+ def _validate_root(root: Path) -> None:
146
+ try:
147
+ metadata = root.lstat()
148
+ resolved = root.resolve(strict=True)
149
+ except OSError as exc:
150
+ raise OverlayError("overlay_root_invalid") from exc
151
+ if _is_link_or_reparse(metadata) or not stat.S_ISDIR(metadata.st_mode) or resolved != root:
152
+ raise OverlayError("overlay_root_invalid")
153
+
154
+
155
+ def _validate_directory(path: Path) -> None:
156
+ try:
157
+ metadata = path.lstat()
158
+ except OSError as exc:
159
+ raise OverlayError("overlay_path_unavailable") from exc
160
+ if _is_link_or_reparse(metadata):
161
+ raise OverlayError("overlay_path_reparse")
162
+ if not stat.S_ISDIR(metadata.st_mode):
163
+ raise OverlayError("overlay_path_invalid")
164
+
165
+
166
+ def _prepare_directory(request: OverlayRequest) -> Path:
167
+ root = request.repository_root
168
+ _validate_root(root)
169
+ output = _lexical_output_root(request)
170
+ _validate_directory(output)
171
+ current = output
172
+ for part in ("overlays", request.provider, request.overlay_identity_digest):
173
+ current /= part
174
+ if not current.exists() and not current.is_symlink():
175
+ try:
176
+ if os.name == "nt":
177
+ from .probe_workspace import _create_private_windows_directory
178
+
179
+ _create_private_windows_directory(current.parent, current.name)
180
+ else:
181
+ current.mkdir(mode=0o700)
182
+ except FileExistsError:
183
+ pass
184
+ except OSError as exc:
185
+ raise OverlayError("overlay_path_unavailable") from exc
186
+ _validate_directory(current)
187
+ if os.name == "nt":
188
+ try:
189
+ from .probe_workspace import _set_private_windows_dacl
190
+
191
+ _set_private_windows_dacl(current)
192
+ except OSError as exc:
193
+ raise OverlayError("overlay_permissions_failed") from exc
194
+ else:
195
+ try:
196
+ os.chmod(current, 0o700)
197
+ except OSError as exc:
198
+ raise OverlayError("overlay_permissions_failed") from exc
199
+ return current
200
+
201
+
202
+ def _validate_existing_storage_path(request: OverlayRequest) -> None:
203
+ """Reject an existing hostile component before processing provider config."""
204
+ root = request.repository_root
205
+ _validate_root(root)
206
+ current = _lexical_output_root(request)
207
+ _validate_directory(current)
208
+ for part in ("overlays", request.provider, request.overlay_identity_digest):
209
+ current /= part
210
+ if not current.exists() and not current.is_symlink():
211
+ break
212
+ _validate_directory(current)
213
+
214
+
215
+ def _read_json(path: Path) -> dict[str, Any]:
216
+ try:
217
+ metadata = path.lstat()
218
+ except FileNotFoundError:
219
+ raise OverlayError("overlay_missing") from None
220
+ except OSError as exc:
221
+ raise OverlayError("overlay_unreadable") from exc
222
+ if _is_link_or_reparse(metadata):
223
+ raise OverlayError("overlay_path_reparse")
224
+ if not stat.S_ISREG(metadata.st_mode):
225
+ raise OverlayError("overlay_path_invalid")
226
+ if metadata.st_size > MAX_OVERLAY_FILE_BYTES:
227
+ raise OverlayError("overlay_too_large")
228
+ flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
229
+ try:
230
+ descriptor = os.open(path, flags)
231
+ try:
232
+ opened = os.fstat(descriptor)
233
+ chunks: list[bytes] = []
234
+ remaining = MAX_OVERLAY_FILE_BYTES + 1
235
+ while remaining > 0:
236
+ chunk = os.read(descriptor, min(64 * 1024, remaining))
237
+ if not chunk:
238
+ break
239
+ chunks.append(chunk)
240
+ remaining -= len(chunk)
241
+ raw = b"".join(chunks)
242
+ after = os.fstat(descriptor)
243
+ finally:
244
+ os.close(descriptor)
245
+ except OSError as exc:
246
+ raise OverlayError("overlay_unreadable") from exc
247
+ def signature(item: os.stat_result) -> tuple[int, int, int, int]:
248
+ return (item.st_dev, item.st_ino, item.st_size, item.st_mtime_ns)
249
+ if signature(opened) != signature(metadata) or signature(after) != signature(opened):
250
+ raise OverlayError("overlay_changed")
251
+ if len(raw) > MAX_OVERLAY_FILE_BYTES:
252
+ raise OverlayError("overlay_too_large")
253
+ try:
254
+ payload = json.loads(raw.decode("utf-8"))
255
+ except (UnicodeDecodeError, json.JSONDecodeError, RecursionError) as exc:
256
+ raise OverlayError("overlay_invalid") from exc
257
+ if not isinstance(payload, dict):
258
+ raise OverlayError("overlay_invalid")
259
+ return payload
260
+
261
+
262
+ def _read_bytes(path: Path) -> bytes:
263
+ try:
264
+ metadata = path.lstat()
265
+ except FileNotFoundError:
266
+ raise OverlayError("overlay_missing") from None
267
+ except OSError as exc:
268
+ raise OverlayError("overlay_unreadable") from exc
269
+ if _is_link_or_reparse(metadata):
270
+ raise OverlayError("overlay_path_reparse")
271
+ if not stat.S_ISREG(metadata.st_mode):
272
+ raise OverlayError("overlay_path_invalid")
273
+ if metadata.st_size > MAX_OVERLAY_FILE_BYTES:
274
+ raise OverlayError("overlay_too_large")
275
+ try:
276
+ raw = path.read_bytes()
277
+ except OSError as exc:
278
+ raise OverlayError("overlay_unreadable") from exc
279
+ if len(raw) > MAX_OVERLAY_FILE_BYTES:
280
+ raise OverlayError("overlay_too_large")
281
+ return raw
282
+
283
+
284
+ def _json_bytes(value: dict[str, Any]) -> bytes:
285
+ return (
286
+ json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
287
+ + "\n"
288
+ ).encode("utf-8")
289
+
290
+
291
+ def _atomic_write_secure(path: Path, payload: dict[str, Any]) -> str:
292
+ raw = _json_bytes(payload)
293
+ if len(raw) > MAX_OVERLAY_FILE_BYTES:
294
+ raise OverlayError("overlay_too_large")
295
+ _validate_directory(path.parent)
296
+ if path.exists() or path.is_symlink():
297
+ try:
298
+ existing = path.lstat()
299
+ except OSError as exc:
300
+ raise OverlayError("overlay_write_failed") from exc
301
+ if _is_link_or_reparse(existing):
302
+ raise OverlayError("overlay_path_reparse")
303
+ if not stat.S_ISREG(existing.st_mode):
304
+ raise OverlayError("overlay_path_invalid")
305
+ try:
306
+ descriptor, temporary_name = tempfile.mkstemp(
307
+ prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent)
308
+ )
309
+ except OSError as exc:
310
+ raise OverlayError("overlay_write_failed") from exc
311
+ temporary = Path(temporary_name)
312
+ try:
313
+ if hasattr(os, "fchmod"):
314
+ os.fchmod(descriptor, 0o600)
315
+ with os.fdopen(descriptor, "wb") as handle:
316
+ descriptor = -1
317
+ handle.write(raw)
318
+ handle.flush()
319
+ os.fsync(handle.fileno())
320
+ _validate_directory(path.parent)
321
+ os.replace(temporary, path)
322
+ if os.name == "nt":
323
+ from .probe_workspace import _set_private_windows_dacl
324
+
325
+ _set_private_windows_dacl(path)
326
+ else:
327
+ os.chmod(path, 0o600)
328
+ except Exception as exc:
329
+ if descriptor >= 0:
330
+ os.close(descriptor)
331
+ try:
332
+ temporary.unlink(missing_ok=True)
333
+ finally:
334
+ if isinstance(exc, OverlayError):
335
+ raise
336
+ raise OverlayError("overlay_write_failed") from exc
337
+ return hashlib.sha256(raw).hexdigest()
338
+
339
+
340
+ def canonical_graph_fingerprint(path: Path, *, root: Path) -> str:
341
+ """Hash the validated canonical bundle using routing's canonical JSON contract."""
342
+ try:
343
+ bundle, _ = load_validated_graph_bundle(path, root=root)
344
+ except GraphReadError as exc:
345
+ raise OverlayError(f"canonical_{exc.code}") from exc
346
+ return _canonical_digest(bundle)
347
+
348
+
349
+ def _canonical_bundle(request: OverlayRequest, cfg: Config) -> tuple[dict[str, Any], str]:
350
+ output = _lexical_output_root(request)
351
+ manifest_path = output / ".graphite_manifest.json"
352
+ try:
353
+ manifest_metadata = manifest_path.lstat()
354
+ except OSError as exc:
355
+ raise OverlayError("canonical_graph_missing") from exc
356
+ if _is_link_or_reparse(manifest_metadata):
357
+ raise OverlayError("canonical_graph_reparse")
358
+ if (
359
+ not stat.S_ISREG(manifest_metadata.st_mode)
360
+ or manifest_metadata.st_size > MAX_CANONICAL_MANIFEST_BYTES
361
+ ):
362
+ raise OverlayError("canonical_graph_invalid")
363
+ selected = cfg.canonical_graph()
364
+ selected.output_dir = output
365
+ try:
366
+ freshness = check_graph_freshness(
367
+ request.repository_root,
368
+ selected,
369
+ max_manifest_bytes=MAX_CANONICAL_MANIFEST_BYTES,
370
+ )
371
+ except FreshnessLimitError as exc:
372
+ raise OverlayError("canonical_graph_invalid") from exc
373
+ if freshness.get("stale", True):
374
+ raise OverlayError("canonical_graph_stale")
375
+ try:
376
+ bundle, _ = load_validated_graph_bundle(
377
+ output / "graph.json", root=request.repository_root
378
+ )
379
+ except GraphReadError as exc:
380
+ raise OverlayError(f"canonical_{exc.code}") from exc
381
+ return bundle, _canonical_digest(bundle)
382
+
383
+
384
+ def _validate_model_config(request: OverlayRequest, cfg: Config) -> None:
385
+ if cfg.llm_provider.strip().casefold().replace("_", "-") != request.provider:
386
+ raise OverlayError("overlay_provider_mismatch")
387
+ expected_mode = "local" if request.provider == "ollama" else "cloud"
388
+ if cfg.llm_mode != expected_mode:
389
+ raise OverlayError("overlay_mode_invalid")
390
+ if (
391
+ not isinstance(cfg.llm_model, str)
392
+ or not cfg.llm_model
393
+ or len(cfg.llm_model) > 256
394
+ or any(ord(character) < 32 for character in cfg.llm_model)
395
+ ):
396
+ raise OverlayError("overlay_model_invalid")
397
+ if (
398
+ not math.isfinite(cfg.llm_timeout_seconds)
399
+ or not 0 < cfg.llm_timeout_seconds <= 300
400
+ or isinstance(cfg.llm_max_input_chars, bool)
401
+ or not 1 <= cfg.llm_max_input_chars <= 1_000_000
402
+ or isinstance(cfg.llm_max_output_tokens, bool)
403
+ or not 1 <= cfg.llm_max_output_tokens <= 4_096
404
+ ):
405
+ raise OverlayError("overlay_limits_invalid")
406
+ if request.provider == "ollama":
407
+ endpoint = cfg.llm_base_url or "http://localhost:11434"
408
+ parsed = urlsplit(endpoint)
409
+ if (
410
+ parsed.scheme != "http"
411
+ or parsed.hostname not in {"localhost", "127.0.0.1", "::1"}
412
+ or parsed.username is not None
413
+ or parsed.password is not None
414
+ or parsed.path not in {"", "/"}
415
+ or parsed.query
416
+ or parsed.fragment
417
+ ):
418
+ raise OverlayError("overlay_endpoint_invalid")
419
+ elif cfg.llm_base_url is not None and cfg.llm_base_url.rstrip("/") != "https://openrouter.ai/api/v1":
420
+ raise OverlayError("overlay_endpoint_invalid")
421
+
422
+
423
+ def _identity_values(request: OverlayRequest) -> dict[str, Any]:
424
+ return {
425
+ "overlay_identity_digest": request.overlay_identity_digest,
426
+ "provider": request.provider,
427
+ "provider_lifecycle_identity_digest": request.provider_lifecycle_identity_digest,
428
+ "model_identity_digest": request.model_identity_digest,
429
+ "routing_policy_digest": request.routing_policy_digest,
430
+ }
431
+
432
+
433
+ def _check_collision(directory: Path, request: OverlayRequest) -> None:
434
+ manifest_path = directory / "manifest.json"
435
+ if not manifest_path.exists() and not manifest_path.is_symlink():
436
+ return
437
+ try:
438
+ existing = _read_json(manifest_path)
439
+ except OverlayError as exc:
440
+ raise OverlayError("overlay_identity_collision") from exc
441
+ expected = _identity_values(request)
442
+ if any(existing.get(field) != expected[field] for field in _IDENTITY_FIELDS):
443
+ raise OverlayError("overlay_identity_collision")
444
+
445
+
446
+ def _validate_success_manifest(manifest: dict[str, Any]) -> None:
447
+ try:
448
+ provider = manifest["provider"]
449
+ payload_file = manifest["payload_file"]
450
+ payload_digest = manifest["payload_sha256"]
451
+ created_at = manifest["created_at"]
452
+ limits = manifest["limits"]
453
+ except KeyError as exc:
454
+ raise OverlayError("overlay_invalid") from exc
455
+ payload_match = _PAYLOAD_FILE.fullmatch(payload_file) if isinstance(payload_file, str) else None
456
+ if (
457
+ manifest.get("schema_version") != OVERLAY_SCHEMA_VERSION
458
+ or manifest.get("non_authoritative") is not True
459
+ or manifest.get("outcome_category") != "succeeded"
460
+ or provider not in _PROVIDERS
461
+ or payload_match is None
462
+ or isinstance(created_at, bool)
463
+ or not isinstance(created_at, int)
464
+ or not 0 <= created_at <= 10**12
465
+ or not isinstance(limits, dict)
466
+ or set(limits)
467
+ != {"timeout_seconds", "max_input_chars", "max_output_tokens"}
468
+ ):
469
+ raise OverlayError("overlay_invalid")
470
+ try:
471
+ normalized_payload_digest = _digest(
472
+ payload_digest, "payload_digest_invalid"
473
+ )
474
+ except ValueError as exc:
475
+ raise OverlayError("overlay_invalid") from exc
476
+ if normalized_payload_digest != payload_match.group(1):
477
+ raise OverlayError("overlay_invalid")
478
+ for field, code in (
479
+ ("canonical_graph_fingerprint", "canonical_graph_fingerprint_invalid"),
480
+ ("overlay_identity_digest", "overlay_identity_digest_invalid"),
481
+ ("provider_lifecycle_identity_digest", "provider_identity_digest_invalid"),
482
+ ("model_identity_digest", "model_identity_digest_invalid"),
483
+ ):
484
+ try:
485
+ _digest(manifest.get(field), code)
486
+ except ValueError as exc:
487
+ raise OverlayError("overlay_invalid") from exc
488
+ routing = manifest.get("routing_policy_digest")
489
+ try:
490
+ _digest(routing, "routing_policy_digest_invalid", optional=True)
491
+ except ValueError as exc:
492
+ raise OverlayError("overlay_invalid") from exc
493
+ if (provider == "openrouter") != (routing is not None):
494
+ raise OverlayError("overlay_invalid")
495
+ timeout = limits["timeout_seconds"]
496
+ max_input = limits["max_input_chars"]
497
+ max_output = limits["max_output_tokens"]
498
+ if (
499
+ isinstance(timeout, bool)
500
+ or not isinstance(timeout, (int, float))
501
+ or not math.isfinite(timeout)
502
+ or not 0 < timeout <= 300
503
+ or isinstance(max_input, bool)
504
+ or not isinstance(max_input, int)
505
+ or not 1 <= max_input <= 1_000_000
506
+ or isinstance(max_output, bool)
507
+ or not isinstance(max_output, int)
508
+ or not 1 <= max_output <= 4_096
509
+ ):
510
+ raise OverlayError("overlay_invalid")
511
+ expected_identity = _canonical_digest(
512
+ {
513
+ "provider": provider,
514
+ "provider_lifecycle_identity_digest": manifest[
515
+ "provider_lifecycle_identity_digest"
516
+ ],
517
+ "model_identity_digest": manifest["model_identity_digest"],
518
+ "routing_policy_digest": routing,
519
+ }
520
+ )
521
+ if manifest["overlay_identity_digest"] != expected_identity:
522
+ raise OverlayError("overlay_invalid")
523
+
524
+
525
+ def _failure_category(result: dict[str, Any]) -> str:
526
+ category = result.get("error_category")
527
+ return category if category in _FAILURE_CATEGORIES else "provider_error"
528
+
529
+
530
+ def _result_payload(result: dict[str, Any]) -> dict[str, Any] | None:
531
+ summary = result.get("summary")
532
+ values = (
533
+ result.get("input_tokens", 0),
534
+ result.get("output_tokens", 0),
535
+ result.get("tokens", 0),
536
+ )
537
+ if (
538
+ not isinstance(summary, str)
539
+ or not summary
540
+ or len(summary.encode("utf-8")) > MAX_SUMMARY_BYTES
541
+ or any(isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= 10**9 for value in values)
542
+ ):
543
+ return None
544
+ return {
545
+ "schema_version": OVERLAY_SCHEMA_VERSION,
546
+ "non_authoritative": True,
547
+ "summary": summary,
548
+ "input_tokens": values[0],
549
+ "output_tokens": values[1],
550
+ "total_tokens": values[2],
551
+ }
552
+
553
+
554
+ def _base_record(request: OverlayRequest, canonical_fingerprint: str) -> dict[str, Any]:
555
+ return {
556
+ "schema_version": OVERLAY_SCHEMA_VERSION,
557
+ "non_authoritative": True,
558
+ "canonical_graph_fingerprint": canonical_fingerprint,
559
+ **_identity_values(request),
560
+ "created_at": request.created_at,
561
+ }
562
+
563
+
564
+ def _write_failure(
565
+ directory: Path,
566
+ request: OverlayRequest,
567
+ canonical_fingerprint: str,
568
+ category: str,
569
+ ) -> dict[str, Any]:
570
+ marker = {
571
+ **_base_record(request, canonical_fingerprint),
572
+ "outcome_category": "failed",
573
+ "failure_category": category if category in _FAILURE_CATEGORIES else "provider_error",
574
+ }
575
+ _atomic_write_secure(directory / "failure.json", marker)
576
+ return marker
577
+
578
+
579
+ def build_overlay(
580
+ request: OverlayRequest,
581
+ cfg: Config,
582
+ *,
583
+ enrich: Callable[[dict[str, Any], dict[str, Any], dict[str, Any], Config], dict[str, Any]] | None = None,
584
+ ) -> dict[str, Any]:
585
+ """Build one optional overlay without mutating canonical graph artifacts."""
586
+ _validate_existing_storage_path(request)
587
+ _validate_model_config(request, cfg)
588
+ bundle, canonical_fingerprint = _canonical_bundle(request, cfg)
589
+ directory = _prepare_directory(request)
590
+ _check_collision(directory, request)
591
+
592
+ if request.provider == "openrouter" and not cfg.llm_api_key:
593
+ return _write_failure(
594
+ directory, request, canonical_fingerprint, "authentication"
595
+ )
596
+
597
+ graph_data = {
598
+ "nodes": bundle.get("nodes", []),
599
+ "edges": bundle.get("edges", []),
600
+ "metadata": {
601
+ "node_count": (bundle.get("metadata") or {}).get("node_count", 0),
602
+ "edge_count": (bundle.get("metadata") or {}).get("edge_count", 0),
603
+ },
604
+ }
605
+ clusters = {
606
+ "clusters": bundle.get("clusters", []),
607
+ "count": (bundle.get("metadata") or {}).get("community_count", 0),
608
+ }
609
+ analysis = bundle.get("analysis") or {}
610
+ runner = enrich or enrich_report
611
+ try:
612
+ result = runner(graph_data, clusters, analysis, cfg)
613
+ except Exception:
614
+ result = {"status": "error", "error_category": "provider_error"}
615
+ if not isinstance(result, dict) or result.get("status") != "ok":
616
+ selected = result if isinstance(result, dict) else {}
617
+ return _write_failure(
618
+ directory,
619
+ request,
620
+ canonical_fingerprint,
621
+ _failure_category(selected),
622
+ )
623
+ payload = _result_payload(result)
624
+ if payload is None:
625
+ return _write_failure(
626
+ directory, request, canonical_fingerprint, "provider_error"
627
+ )
628
+
629
+ _, current_fingerprint = _canonical_bundle(request, cfg)
630
+ if current_fingerprint != canonical_fingerprint:
631
+ raise OverlayError("canonical_graph_changed")
632
+
633
+ payload_bytes = _json_bytes(payload)
634
+ payload_digest = hashlib.sha256(payload_bytes).hexdigest()
635
+ payload_name = f"payload-{payload_digest}.json"
636
+ payload_path = directory / payload_name
637
+ if payload_path.exists() or payload_path.is_symlink():
638
+ if _read_bytes(payload_path) != payload_bytes:
639
+ raise OverlayError("overlay_payload_collision")
640
+ written_digest = payload_digest
641
+ else:
642
+ written_digest = _atomic_write_secure(payload_path, payload)
643
+ if written_digest != payload_digest:
644
+ raise OverlayError("overlay_write_failed")
645
+ manifest = {
646
+ **_base_record(request, canonical_fingerprint),
647
+ "limits": {
648
+ "timeout_seconds": float(cfg.llm_timeout_seconds),
649
+ "max_input_chars": cfg.llm_max_input_chars,
650
+ "max_output_tokens": cfg.llm_max_output_tokens,
651
+ },
652
+ "outcome_category": "succeeded",
653
+ "payload_file": payload_name,
654
+ "payload_sha256": payload_digest,
655
+ }
656
+ _atomic_write_secure(directory / "manifest.json", manifest)
657
+ return manifest
658
+
659
+
660
+ def evaluate_overlay_staleness(
661
+ manifest: dict[str, Any],
662
+ *,
663
+ canonical_graph_fingerprint: str,
664
+ provider_lifecycle_identity_digest: str,
665
+ model_identity_digest: str,
666
+ routing_policy_digest: str | None,
667
+ ) -> dict[str, Any]:
668
+ """Evaluate opt-in overlay freshness without affecting canonical freshness."""
669
+ _validate_success_manifest(manifest)
670
+ current_graph = _digest(
671
+ canonical_graph_fingerprint, "canonical_graph_fingerprint_invalid"
672
+ )
673
+ current_provider = _digest(
674
+ provider_lifecycle_identity_digest, "provider_identity_digest_invalid"
675
+ )
676
+ current_model = _digest(model_identity_digest, "model_identity_digest_invalid")
677
+ current_routing = _digest(
678
+ routing_policy_digest, "routing_policy_digest_invalid", optional=True
679
+ )
680
+ reasons: list[str] = []
681
+ if manifest.get("canonical_graph_fingerprint") != current_graph:
682
+ reasons.append("canonical_graph_changed")
683
+ if manifest.get("model_identity_digest") != current_model:
684
+ reasons.append("model_identity_changed")
685
+ if manifest.get("provider_lifecycle_identity_digest") != current_provider:
686
+ reasons.append("provider_identity_changed")
687
+ if manifest.get("routing_policy_digest") != current_routing:
688
+ reasons.append("routing_policy_changed")
689
+ return {
690
+ "status": "stale" if reasons else "current",
691
+ "stale_reasons": sorted(reasons),
692
+ }
693
+
694
+
695
+ def load_overlay_manifest(request: OverlayRequest) -> dict[str, Any]:
696
+ """Opt in to reading a bounded manifest for one exact overlay identity."""
697
+ _validate_existing_storage_path(request)
698
+ directory = overlay_directory(request)
699
+ manifest = _read_json(directory / "manifest.json")
700
+ _validate_success_manifest(manifest)
701
+ expected = _identity_values(request)
702
+ if any(manifest.get(field) != expected[field] for field in _IDENTITY_FIELDS):
703
+ raise OverlayError("overlay_identity_collision")
704
+ payload = _read_bytes(directory / manifest["payload_file"])
705
+ if hashlib.sha256(payload).hexdigest() != manifest["payload_sha256"]:
706
+ raise OverlayError("overlay_invalid")
707
+ try:
708
+ decoded = json.loads(payload.decode("utf-8"))
709
+ except (UnicodeDecodeError, json.JSONDecodeError, RecursionError) as exc:
710
+ raise OverlayError("overlay_invalid") from exc
711
+ if not isinstance(decoded, dict):
712
+ raise OverlayError("overlay_invalid")
713
+ return manifest