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,771 @@
1
+ """Provider-neutral runtime lifecycle authority contracts.
2
+
3
+ This module is intentionally pure. It normalizes identity, compatibility, and
4
+ transition decisions without discovering executables, contacting endpoints, or
5
+ granting provider execution authority.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import re
12
+ from dataclasses import dataclass
13
+ from enum import StrEnum
14
+ from typing import Any, ClassVar
15
+
16
+ from .contracts import PublicRecord
17
+
18
+ MAX_CAPABILITIES = 32
19
+ MAX_IDENTIFIER_LENGTH = 128
20
+
21
+ _HEX_64 = re.compile(r"^[0-9a-f]{64}$")
22
+ _SAFE_IDENTIFIER = re.compile(r"^[a-z0-9][a-z0-9._-]{0,127}$")
23
+ _SEMANTIC_VERSION = re.compile(
24
+ r"^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)"
25
+ r"(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$"
26
+ )
27
+
28
+
29
+ class RuntimeKind(StrEnum):
30
+ LOCAL_CLI = "local-cli"
31
+ LOCAL_HTTP = "local-http"
32
+ REMOTE_HTTPS = "remote-https"
33
+
34
+
35
+ class LifecycleProviderId(StrEnum):
36
+ CLAUDE_CODE = "claude-code"
37
+ CODEX = "codex"
38
+ OLLAMA = "ollama"
39
+ OPENROUTER = "openrouter"
40
+ ZAI = "zai"
41
+
42
+
43
+ class ProviderLifecycleState(StrEnum):
44
+ DISCOVERED = "discovered"
45
+ COMPATIBLE = "compatible"
46
+ VERIFICATION_REQUIRED = "verification_required"
47
+ ACTIVE = "active"
48
+ INCOMPATIBLE = "incompatible"
49
+ UNAVAILABLE = "unavailable"
50
+
51
+
52
+ class IdentityChange(StrEnum):
53
+ UNCHANGED = "unchanged"
54
+ HASH_ONLY = "hash_only"
55
+ PATCH = "patch"
56
+ MINOR = "minor"
57
+ MAJOR = "major"
58
+ CAPABILITY = "capability"
59
+ ENDPOINT = "endpoint"
60
+ MODEL_DIGEST = "model_digest"
61
+ ROUTING_POLICY = "routing_policy"
62
+ POLICY = "policy"
63
+
64
+
65
+ class CompatibilityProbeLevel(StrEnum):
66
+ NONE = "none"
67
+ STANDARD = "standard"
68
+ EXPANDED = "expanded"
69
+
70
+
71
+ class LifecycleReasonCode(StrEnum):
72
+ DISCOVERED = "discovered"
73
+ IDENTITY_UNCHANGED = "identity_unchanged"
74
+ HASH_CHANGED = "hash_changed"
75
+ PATCH_CHANGED = "patch_changed"
76
+ MINOR_CHANGED = "minor_changed"
77
+ MAJOR_CHANGED = "major_changed"
78
+ CAPABILITY_CHANGED = "capability_changed"
79
+ ENDPOINT_CHANGED = "endpoint_changed"
80
+ MODEL_DIGEST_CHANGED = "model_digest_changed"
81
+ ROUTING_POLICY_CHANGED = "routing_policy_changed"
82
+ POLICY_CHANGED = "policy_changed"
83
+ RUNTIME_MISSING = "runtime_missing"
84
+ CREDENTIAL_UNHEALTHY = "credential_unhealthy"
85
+ REQUIRED_CAPABILITY_MISSING = "required_capability_missing"
86
+ POLICY_RANGE_UNSUPPORTED = "policy_range_unsupported"
87
+ PROBE_FAILED = "probe_failed"
88
+ COMPATIBILITY_CONFIRMED = "compatibility_confirmed"
89
+ VERIFICATION_ACCEPTED = "verification_accepted"
90
+ POLICY_PROMOTED = "policy_promoted"
91
+ PATCH_CARRIED_FORWARD = "patch_carried_forward"
92
+
93
+
94
+ _PROVIDER_RUNTIME_KINDS = {
95
+ LifecycleProviderId.CLAUDE_CODE: RuntimeKind.LOCAL_CLI,
96
+ LifecycleProviderId.CODEX: RuntimeKind.LOCAL_CLI,
97
+ LifecycleProviderId.OLLAMA: RuntimeKind.LOCAL_HTTP,
98
+ LifecycleProviderId.OPENROUTER: RuntimeKind.REMOTE_HTTPS,
99
+ LifecycleProviderId.ZAI: RuntimeKind.REMOTE_HTTPS,
100
+ }
101
+
102
+
103
+ def _enum(value: object, enum_type: type[StrEnum], *, code: str) -> StrEnum:
104
+ try:
105
+ return enum_type(value)
106
+ except (TypeError, ValueError) as exc:
107
+ raise ValueError(code) from exc
108
+
109
+
110
+ def _hash(value: object, *, code: str, optional: bool = False) -> str | None:
111
+ if value is None and optional:
112
+ return None
113
+ if not isinstance(value, str) or _HEX_64.fullmatch(value) is None:
114
+ raise ValueError(code)
115
+ return value
116
+
117
+
118
+ def _semantic_version(value: object, *, code: str) -> str:
119
+ if (
120
+ not isinstance(value, str)
121
+ or len(value) > MAX_IDENTIFIER_LENGTH
122
+ or _SEMANTIC_VERSION.fullmatch(value) is None
123
+ ):
124
+ raise ValueError(code)
125
+ return value
126
+
127
+
128
+ def _identifier(value: object, *, code: str) -> str:
129
+ if (
130
+ not isinstance(value, str)
131
+ or len(value) > MAX_IDENTIFIER_LENGTH
132
+ or _SAFE_IDENTIFIER.fullmatch(value) is None
133
+ ):
134
+ raise ValueError(code)
135
+ return value
136
+
137
+
138
+ def _timestamp(value: object, *, code: str) -> int:
139
+ if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= 10**12:
140
+ raise ValueError(code)
141
+ return value
142
+
143
+
144
+ def _capabilities(value: object) -> tuple[str, ...]:
145
+ if not isinstance(value, (tuple, list)) or not value or len(value) > MAX_CAPABILITIES:
146
+ raise ValueError("capabilities_invalid")
147
+ normalized = tuple(_identifier(item, code="capabilities_invalid") for item in value)
148
+ if len(set(normalized)) != len(normalized):
149
+ raise ValueError("capabilities_invalid")
150
+ return tuple(sorted(normalized))
151
+
152
+
153
+ def _canonical_digest(value: dict[str, Any]) -> str:
154
+ payload = json.dumps(
155
+ value, sort_keys=True, separators=(",", ":"), ensure_ascii=True
156
+ ).encode("utf-8")
157
+ return hashlib.sha256(payload).hexdigest()
158
+
159
+
160
+ def _validate_boundary(
161
+ provider: LifecycleProviderId, runtime_kind: RuntimeKind, *, code: str
162
+ ) -> None:
163
+ if _PROVIDER_RUNTIME_KINDS.get(provider) is not runtime_kind:
164
+ raise ValueError(code)
165
+
166
+
167
+ def _version_triplet(value: str) -> tuple[int, int, int]:
168
+ core = value.split("-", 1)[0].split("+", 1)[0]
169
+ major, minor, patch = core.split(".")
170
+ return int(major), int(minor), int(patch)
171
+
172
+
173
+ def _version_key(
174
+ value: str,
175
+ ) -> tuple[int, int, int, int, tuple[tuple[int, int | str], ...]]:
176
+ without_build = value.split("+", 1)[0]
177
+ core, separator, prerelease = without_build.partition("-")
178
+ major, minor, patch = (int(part) for part in core.split("."))
179
+ if not separator:
180
+ return major, minor, patch, 1, ()
181
+ components: list[tuple[int, int | str]] = []
182
+ for component in prerelease.split("."):
183
+ components.append(
184
+ (0, int(component)) if component.isdigit() else (1, component)
185
+ )
186
+ return major, minor, patch, 0, tuple(components)
187
+
188
+
189
+ @dataclass(frozen=True)
190
+ class ProviderRuntimeIdentity(PublicRecord):
191
+ provider: LifecycleProviderId
192
+ runtime_kind: RuntimeKind
193
+ version: str
194
+ runtime_digest: str
195
+ model_identity_digest: str | None
196
+ routing_policy_digest: str | None
197
+ capabilities: tuple[str, ...]
198
+ policy_version: str
199
+ observed_at: int
200
+
201
+ _public_fields: ClassVar[tuple[str, ...]] = (
202
+ "provider",
203
+ "runtime_kind",
204
+ "version",
205
+ "runtime_digest",
206
+ "model_identity_digest",
207
+ "routing_policy_digest",
208
+ "capabilities",
209
+ "policy_version",
210
+ "observed_at",
211
+ )
212
+
213
+ def __post_init__(self) -> None:
214
+ provider = _enum(self.provider, LifecycleProviderId, code="provider_invalid")
215
+ runtime_kind = _enum(self.runtime_kind, RuntimeKind, code="runtime_kind_invalid")
216
+ assert isinstance(provider, LifecycleProviderId)
217
+ assert isinstance(runtime_kind, RuntimeKind)
218
+ _validate_boundary(provider, runtime_kind, code="provider_runtime_kind_invalid")
219
+ model_digest = _hash(
220
+ self.model_identity_digest, code="model_identity_digest_invalid", optional=True
221
+ )
222
+ routing_digest = _hash(
223
+ self.routing_policy_digest, code="routing_policy_digest_invalid", optional=True
224
+ )
225
+ if provider in {
226
+ LifecycleProviderId.OLLAMA,
227
+ LifecycleProviderId.OPENROUTER,
228
+ LifecycleProviderId.ZAI,
229
+ } and model_digest is None:
230
+ raise ValueError("model_identity_digest_invalid")
231
+ if provider is LifecycleProviderId.OPENROUTER and routing_digest is None:
232
+ raise ValueError("routing_policy_digest_invalid")
233
+ if provider is not LifecycleProviderId.OPENROUTER and routing_digest is not None:
234
+ raise ValueError("routing_policy_digest_invalid")
235
+ if runtime_kind is RuntimeKind.LOCAL_CLI and model_digest is not None:
236
+ raise ValueError("model_identity_digest_invalid")
237
+ object.__setattr__(self, "provider", provider)
238
+ object.__setattr__(self, "runtime_kind", runtime_kind)
239
+ object.__setattr__(
240
+ self, "version", _semantic_version(self.version, code="runtime_version_invalid")
241
+ )
242
+ object.__setattr__(
243
+ self, "runtime_digest", _hash(self.runtime_digest, code="runtime_digest_invalid")
244
+ )
245
+ object.__setattr__(self, "model_identity_digest", model_digest)
246
+ object.__setattr__(self, "routing_policy_digest", routing_digest)
247
+ object.__setattr__(self, "capabilities", _capabilities(self.capabilities))
248
+ object.__setattr__(
249
+ self,
250
+ "policy_version",
251
+ _semantic_version(self.policy_version, code="policy_version_invalid"),
252
+ )
253
+ object.__setattr__(
254
+ self, "observed_at", _timestamp(self.observed_at, code="observed_at_invalid")
255
+ )
256
+
257
+ @property
258
+ def digest(self) -> str:
259
+ return _canonical_digest(
260
+ {
261
+ "provider": self.provider.value,
262
+ "runtime_kind": self.runtime_kind.value,
263
+ "version": self.version,
264
+ "runtime_digest": self.runtime_digest,
265
+ "model_identity_digest": self.model_identity_digest,
266
+ "routing_policy_digest": self.routing_policy_digest,
267
+ "capabilities": list(self.capabilities),
268
+ "policy_version": self.policy_version,
269
+ }
270
+ )
271
+
272
+
273
+ @dataclass(frozen=True)
274
+ class ProviderCompatibilityPolicy(PublicRecord):
275
+ provider: LifecycleProviderId
276
+ runtime_kind: RuntimeKind
277
+ policy_version: str
278
+ minimum_version: str
279
+ maximum_version_exclusive: str
280
+ required_capabilities: tuple[str, ...]
281
+
282
+ _public_fields: ClassVar[tuple[str, ...]] = (
283
+ "provider",
284
+ "runtime_kind",
285
+ "policy_version",
286
+ "minimum_version",
287
+ "maximum_version_exclusive",
288
+ "required_capabilities",
289
+ )
290
+
291
+ def __post_init__(self) -> None:
292
+ provider = _enum(self.provider, LifecycleProviderId, code="provider_invalid")
293
+ runtime_kind = _enum(self.runtime_kind, RuntimeKind, code="runtime_kind_invalid")
294
+ assert isinstance(provider, LifecycleProviderId)
295
+ assert isinstance(runtime_kind, RuntimeKind)
296
+ _validate_boundary(provider, runtime_kind, code="compatibility_boundary_invalid")
297
+ minimum = _semantic_version(
298
+ self.minimum_version, code="compatibility_version_range_invalid"
299
+ )
300
+ maximum = _semantic_version(
301
+ self.maximum_version_exclusive, code="compatibility_version_range_invalid"
302
+ )
303
+ if _version_key(minimum) >= _version_key(maximum):
304
+ raise ValueError("compatibility_version_range_invalid")
305
+ object.__setattr__(self, "provider", provider)
306
+ object.__setattr__(self, "runtime_kind", runtime_kind)
307
+ object.__setattr__(
308
+ self,
309
+ "policy_version",
310
+ _semantic_version(self.policy_version, code="policy_version_invalid"),
311
+ )
312
+ object.__setattr__(self, "minimum_version", minimum)
313
+ object.__setattr__(self, "maximum_version_exclusive", maximum)
314
+ object.__setattr__(self, "required_capabilities", _capabilities(self.required_capabilities))
315
+
316
+ @property
317
+ def digest(self) -> str:
318
+ return _canonical_digest(self.to_dict())
319
+
320
+ def supports(self, identity: ProviderRuntimeIdentity) -> bool:
321
+ if not isinstance(identity, ProviderRuntimeIdentity):
322
+ raise ValueError("identity_invalid")
323
+ if identity.provider is not self.provider or identity.runtime_kind is not self.runtime_kind:
324
+ return False
325
+ if identity.policy_version != self.policy_version:
326
+ return False
327
+ version = _version_key(identity.version)
328
+ if not (
329
+ _version_key(self.minimum_version)
330
+ <= version
331
+ < _version_key(self.maximum_version_exclusive)
332
+ ):
333
+ return False
334
+ return set(self.required_capabilities).issubset(identity.capabilities)
335
+
336
+
337
+ @dataclass(frozen=True)
338
+ class ProviderCompatibilityAssessment(PublicRecord):
339
+ change: IdentityChange
340
+ state: ProviderLifecycleState
341
+ reason: LifecycleReasonCode
342
+ probe_level: CompatibilityProbeLevel
343
+
344
+ _public_fields: ClassVar[tuple[str, ...]] = (
345
+ "change",
346
+ "state",
347
+ "reason",
348
+ "probe_level",
349
+ )
350
+
351
+ def __post_init__(self) -> None:
352
+ object.__setattr__(
353
+ self, "change", _enum(self.change, IdentityChange, code="identity_change_invalid")
354
+ )
355
+ object.__setattr__(
356
+ self, "state", _enum(self.state, ProviderLifecycleState, code="lifecycle_state_invalid")
357
+ )
358
+ object.__setattr__(
359
+ self, "reason", _enum(self.reason, LifecycleReasonCode, code="lifecycle_reason_invalid")
360
+ )
361
+ object.__setattr__(
362
+ self,
363
+ "probe_level",
364
+ _enum(self.probe_level, CompatibilityProbeLevel, code="probe_level_invalid"),
365
+ )
366
+ if self.state is ProviderLifecycleState.ACTIVE and not (
367
+ (
368
+ self.change is IdentityChange.UNCHANGED
369
+ and self.reason is LifecycleReasonCode.IDENTITY_UNCHANGED
370
+ and self.probe_level is CompatibilityProbeLevel.NONE
371
+ )
372
+ or (
373
+ self.change is IdentityChange.PATCH
374
+ and self.reason is LifecycleReasonCode.PATCH_CARRIED_FORWARD
375
+ and self.probe_level is CompatibilityProbeLevel.STANDARD
376
+ )
377
+ ):
378
+ raise ValueError("assessment_authority_invalid")
379
+
380
+
381
+ def classify_identity_change(
382
+ previous: ProviderRuntimeIdentity, current: ProviderRuntimeIdentity
383
+ ) -> IdentityChange:
384
+ if not isinstance(previous, ProviderRuntimeIdentity) or not isinstance(
385
+ current, ProviderRuntimeIdentity
386
+ ):
387
+ raise ValueError("identity_invalid")
388
+ if (
389
+ previous.provider is not current.provider
390
+ or previous.runtime_kind is not current.runtime_kind
391
+ ):
392
+ raise ValueError("identity_boundary_invalid")
393
+ if previous.digest == current.digest:
394
+ return IdentityChange.UNCHANGED
395
+ previous_version = _version_triplet(previous.version)
396
+ current_version = _version_triplet(current.version)
397
+ if previous_version[0] != current_version[0]:
398
+ return IdentityChange.MAJOR
399
+ if previous_version[1] != current_version[1]:
400
+ return IdentityChange.MINOR
401
+ if previous.model_identity_digest != current.model_identity_digest:
402
+ return IdentityChange.MODEL_DIGEST
403
+ if previous.routing_policy_digest != current.routing_policy_digest:
404
+ return IdentityChange.ROUTING_POLICY
405
+ if previous.capabilities != current.capabilities:
406
+ return IdentityChange.CAPABILITY
407
+ if previous.policy_version != current.policy_version:
408
+ return IdentityChange.POLICY
409
+ if (
410
+ previous.runtime_digest != current.runtime_digest
411
+ and current.runtime_kind is not RuntimeKind.LOCAL_CLI
412
+ ):
413
+ return IdentityChange.ENDPOINT
414
+ if previous.version != current.version or previous_version[2] != current_version[2]:
415
+ return IdentityChange.PATCH
416
+ if previous.runtime_digest != current.runtime_digest:
417
+ return IdentityChange.HASH_ONLY
418
+ raise ValueError("identity_change_unclassified")
419
+
420
+
421
+ _CHANGE_REASONS = {
422
+ IdentityChange.UNCHANGED: LifecycleReasonCode.IDENTITY_UNCHANGED,
423
+ IdentityChange.HASH_ONLY: LifecycleReasonCode.HASH_CHANGED,
424
+ IdentityChange.PATCH: LifecycleReasonCode.PATCH_CHANGED,
425
+ IdentityChange.MINOR: LifecycleReasonCode.MINOR_CHANGED,
426
+ IdentityChange.MAJOR: LifecycleReasonCode.MAJOR_CHANGED,
427
+ IdentityChange.CAPABILITY: LifecycleReasonCode.CAPABILITY_CHANGED,
428
+ IdentityChange.ENDPOINT: LifecycleReasonCode.ENDPOINT_CHANGED,
429
+ IdentityChange.MODEL_DIGEST: LifecycleReasonCode.MODEL_DIGEST_CHANGED,
430
+ IdentityChange.ROUTING_POLICY: LifecycleReasonCode.ROUTING_POLICY_CHANGED,
431
+ IdentityChange.POLICY: LifecycleReasonCode.POLICY_CHANGED,
432
+ }
433
+
434
+ _STANDARD_CHANGES = frozenset({IdentityChange.HASH_ONLY, IdentityChange.PATCH})
435
+ _EXPANDED_CHANGES = frozenset(
436
+ {
437
+ IdentityChange.MINOR,
438
+ IdentityChange.CAPABILITY,
439
+ IdentityChange.ENDPOINT,
440
+ IdentityChange.MODEL_DIGEST,
441
+ IdentityChange.ROUTING_POLICY,
442
+ IdentityChange.POLICY,
443
+ }
444
+ )
445
+
446
+
447
+ def assess_identity_change(
448
+ previous: ProviderRuntimeIdentity | None,
449
+ current: ProviderRuntimeIdentity,
450
+ policy: ProviderCompatibilityPolicy,
451
+ *,
452
+ runtime_available: bool = True,
453
+ credential_healthy: bool = True,
454
+ standard_probe_passed: bool = True,
455
+ expanded_probe_passed: bool = True,
456
+ existing_state: ProviderLifecycleState = ProviderLifecycleState.COMPATIBLE,
457
+ ) -> ProviderCompatibilityAssessment:
458
+ if not isinstance(current, ProviderRuntimeIdentity) or not isinstance(
459
+ policy, ProviderCompatibilityPolicy
460
+ ):
461
+ raise ValueError("identity_invalid")
462
+ if current.provider is not policy.provider or current.runtime_kind is not policy.runtime_kind:
463
+ raise ValueError("compatibility_boundary_invalid")
464
+ existing = _enum(
465
+ existing_state, ProviderLifecycleState, code="lifecycle_state_invalid"
466
+ )
467
+ assert isinstance(existing, ProviderLifecycleState)
468
+ if not isinstance(runtime_available, bool):
469
+ raise ValueError("runtime_available_invalid")
470
+ if not isinstance(credential_healthy, bool):
471
+ raise ValueError("credential_health_invalid")
472
+ if not isinstance(standard_probe_passed, bool) or not isinstance(
473
+ expanded_probe_passed, bool
474
+ ):
475
+ raise ValueError("probe_result_invalid")
476
+ if not runtime_available:
477
+ return ProviderCompatibilityAssessment(
478
+ IdentityChange.UNCHANGED,
479
+ ProviderLifecycleState.UNAVAILABLE,
480
+ LifecycleReasonCode.RUNTIME_MISSING,
481
+ CompatibilityProbeLevel.NONE,
482
+ )
483
+ if not credential_healthy:
484
+ return ProviderCompatibilityAssessment(
485
+ IdentityChange.UNCHANGED,
486
+ ProviderLifecycleState.UNAVAILABLE,
487
+ LifecycleReasonCode.CREDENTIAL_UNHEALTHY,
488
+ CompatibilityProbeLevel.NONE,
489
+ )
490
+ if previous is None:
491
+ if not policy.supports(current):
492
+ reason = (
493
+ LifecycleReasonCode.REQUIRED_CAPABILITY_MISSING
494
+ if not set(policy.required_capabilities).issubset(current.capabilities)
495
+ else LifecycleReasonCode.POLICY_RANGE_UNSUPPORTED
496
+ )
497
+ return ProviderCompatibilityAssessment(
498
+ IdentityChange.UNCHANGED,
499
+ ProviderLifecycleState.INCOMPATIBLE,
500
+ reason,
501
+ CompatibilityProbeLevel.STANDARD,
502
+ )
503
+ return ProviderCompatibilityAssessment(
504
+ IdentityChange.UNCHANGED,
505
+ ProviderLifecycleState.DISCOVERED,
506
+ LifecycleReasonCode.DISCOVERED,
507
+ CompatibilityProbeLevel.STANDARD,
508
+ )
509
+ change = classify_identity_change(previous, current)
510
+ if change is IdentityChange.MAJOR:
511
+ return ProviderCompatibilityAssessment(
512
+ change,
513
+ ProviderLifecycleState.INCOMPATIBLE,
514
+ LifecycleReasonCode.MAJOR_CHANGED,
515
+ CompatibilityProbeLevel.NONE,
516
+ )
517
+ if not set(policy.required_capabilities).issubset(current.capabilities):
518
+ return ProviderCompatibilityAssessment(
519
+ change,
520
+ ProviderLifecycleState.INCOMPATIBLE,
521
+ LifecycleReasonCode.REQUIRED_CAPABILITY_MISSING,
522
+ CompatibilityProbeLevel.EXPANDED,
523
+ )
524
+ if not policy.supports(current):
525
+ return ProviderCompatibilityAssessment(
526
+ change,
527
+ ProviderLifecycleState.INCOMPATIBLE,
528
+ LifecycleReasonCode.POLICY_RANGE_UNSUPPORTED,
529
+ CompatibilityProbeLevel.NONE,
530
+ )
531
+ if change is IdentityChange.UNCHANGED:
532
+ return ProviderCompatibilityAssessment(
533
+ change,
534
+ existing,
535
+ LifecycleReasonCode.IDENTITY_UNCHANGED,
536
+ CompatibilityProbeLevel.NONE,
537
+ )
538
+ probe_level = (
539
+ CompatibilityProbeLevel.STANDARD
540
+ if change in _STANDARD_CHANGES
541
+ else CompatibilityProbeLevel.EXPANDED
542
+ )
543
+ probe_passed = (
544
+ standard_probe_passed
545
+ if probe_level is CompatibilityProbeLevel.STANDARD
546
+ else expanded_probe_passed
547
+ )
548
+ if not probe_passed:
549
+ return ProviderCompatibilityAssessment(
550
+ change,
551
+ ProviderLifecycleState.INCOMPATIBLE,
552
+ LifecycleReasonCode.PROBE_FAILED,
553
+ probe_level,
554
+ )
555
+ if (
556
+ change is IdentityChange.PATCH
557
+ and current.runtime_kind is RuntimeKind.LOCAL_CLI
558
+ and existing is ProviderLifecycleState.ACTIVE
559
+ ):
560
+ return ProviderCompatibilityAssessment(
561
+ change,
562
+ ProviderLifecycleState.ACTIVE,
563
+ LifecycleReasonCode.PATCH_CARRIED_FORWARD,
564
+ probe_level,
565
+ )
566
+ return ProviderCompatibilityAssessment(
567
+ change,
568
+ ProviderLifecycleState.VERIFICATION_REQUIRED,
569
+ _CHANGE_REASONS[change],
570
+ probe_level,
571
+ )
572
+
573
+
574
+ _VALID_TRANSITIONS = {
575
+ None: frozenset(
576
+ {
577
+ ProviderLifecycleState.DISCOVERED,
578
+ ProviderLifecycleState.INCOMPATIBLE,
579
+ ProviderLifecycleState.UNAVAILABLE,
580
+ }
581
+ ),
582
+ ProviderLifecycleState.DISCOVERED: frozenset(
583
+ {
584
+ ProviderLifecycleState.COMPATIBLE,
585
+ ProviderLifecycleState.VERIFICATION_REQUIRED,
586
+ ProviderLifecycleState.INCOMPATIBLE,
587
+ ProviderLifecycleState.UNAVAILABLE,
588
+ }
589
+ ),
590
+ ProviderLifecycleState.COMPATIBLE: frozenset(
591
+ {
592
+ ProviderLifecycleState.ACTIVE,
593
+ ProviderLifecycleState.VERIFICATION_REQUIRED,
594
+ ProviderLifecycleState.INCOMPATIBLE,
595
+ ProviderLifecycleState.UNAVAILABLE,
596
+ }
597
+ ),
598
+ ProviderLifecycleState.VERIFICATION_REQUIRED: frozenset(
599
+ {
600
+ ProviderLifecycleState.ACTIVE,
601
+ ProviderLifecycleState.INCOMPATIBLE,
602
+ ProviderLifecycleState.UNAVAILABLE,
603
+ }
604
+ ),
605
+ ProviderLifecycleState.ACTIVE: frozenset(
606
+ {
607
+ ProviderLifecycleState.VERIFICATION_REQUIRED,
608
+ ProviderLifecycleState.INCOMPATIBLE,
609
+ ProviderLifecycleState.UNAVAILABLE,
610
+ }
611
+ ),
612
+ ProviderLifecycleState.INCOMPATIBLE: frozenset(
613
+ {
614
+ ProviderLifecycleState.VERIFICATION_REQUIRED,
615
+ ProviderLifecycleState.UNAVAILABLE,
616
+ }
617
+ ),
618
+ ProviderLifecycleState.UNAVAILABLE: frozenset(
619
+ {
620
+ ProviderLifecycleState.DISCOVERED,
621
+ ProviderLifecycleState.INCOMPATIBLE,
622
+ }
623
+ ),
624
+ }
625
+
626
+
627
+ @dataclass(frozen=True)
628
+ class ProviderLifecycleEvent(PublicRecord):
629
+ event_id: str
630
+ provider: LifecycleProviderId
631
+ runtime_kind: RuntimeKind
632
+ previous_identity_digest: str | None
633
+ current_identity_digest: str | None
634
+ previous_state: ProviderLifecycleState | None
635
+ current_state: ProviderLifecycleState
636
+ reason: LifecycleReasonCode
637
+ policy_version: str
638
+ occurred_at: int
639
+
640
+ _public_fields: ClassVar[tuple[str, ...]] = (
641
+ "event_id",
642
+ "provider",
643
+ "runtime_kind",
644
+ "previous_identity_digest",
645
+ "current_identity_digest",
646
+ "previous_state",
647
+ "current_state",
648
+ "reason",
649
+ "policy_version",
650
+ "occurred_at",
651
+ )
652
+
653
+ def __post_init__(self) -> None:
654
+ provider = _enum(self.provider, LifecycleProviderId, code="provider_invalid")
655
+ runtime_kind = _enum(self.runtime_kind, RuntimeKind, code="runtime_kind_invalid")
656
+ assert isinstance(provider, LifecycleProviderId)
657
+ assert isinstance(runtime_kind, RuntimeKind)
658
+ _validate_boundary(provider, runtime_kind, code="provider_runtime_kind_invalid")
659
+ previous_state = (
660
+ None
661
+ if self.previous_state is None
662
+ else _enum(
663
+ self.previous_state,
664
+ ProviderLifecycleState,
665
+ code="lifecycle_state_invalid",
666
+ )
667
+ )
668
+ current_state = _enum(
669
+ self.current_state, ProviderLifecycleState, code="lifecycle_state_invalid"
670
+ )
671
+ reason = _enum(self.reason, LifecycleReasonCode, code="lifecycle_reason_invalid")
672
+ assert previous_state is None or isinstance(previous_state, ProviderLifecycleState)
673
+ assert isinstance(current_state, ProviderLifecycleState)
674
+ assert isinstance(reason, LifecycleReasonCode)
675
+ previous_digest = _hash(
676
+ self.previous_identity_digest,
677
+ code="previous_identity_digest_invalid",
678
+ optional=True,
679
+ )
680
+ current_digest = _hash(
681
+ self.current_identity_digest,
682
+ code="current_identity_digest_invalid",
683
+ optional=True,
684
+ )
685
+ if previous_state is None and previous_digest is not None:
686
+ raise ValueError("lifecycle_identity_invalid")
687
+ if (
688
+ previous_state is not None
689
+ and previous_state is not ProviderLifecycleState.UNAVAILABLE
690
+ and previous_digest is None
691
+ ):
692
+ raise ValueError("lifecycle_identity_invalid")
693
+ if current_state is not ProviderLifecycleState.UNAVAILABLE and current_digest is None:
694
+ raise ValueError("lifecycle_identity_invalid")
695
+ if previous_state is current_state:
696
+ identity_unchanged = previous_digest == current_digest
697
+ if identity_unchanged is not (
698
+ reason is LifecycleReasonCode.IDENTITY_UNCHANGED
699
+ ):
700
+ raise ValueError("lifecycle_transition_invalid")
701
+ if not identity_unchanged:
702
+ if current_state is ProviderLifecycleState.ACTIVE:
703
+ if reason is not LifecycleReasonCode.PATCH_CARRIED_FORWARD:
704
+ raise ValueError("lifecycle_transition_invalid")
705
+ elif reason not in set(_CHANGE_REASONS.values()):
706
+ raise ValueError("lifecycle_transition_invalid")
707
+ elif current_state not in _VALID_TRANSITIONS[previous_state]:
708
+ raise ValueError("lifecycle_transition_invalid")
709
+ if reason is LifecycleReasonCode.PATCH_CARRIED_FORWARD and not (
710
+ previous_state is ProviderLifecycleState.ACTIVE
711
+ and current_state is ProviderLifecycleState.ACTIVE
712
+ and previous_digest != current_digest
713
+ ):
714
+ raise ValueError("lifecycle_transition_invalid")
715
+ if (
716
+ current_state is ProviderLifecycleState.ACTIVE
717
+ and previous_state is not current_state
718
+ and reason is not LifecycleReasonCode.VERIFICATION_ACCEPTED
719
+ ):
720
+ raise ValueError("lifecycle_transition_invalid")
721
+ if previous_state is None:
722
+ initial_reasons = {
723
+ ProviderLifecycleState.DISCOVERED: frozenset({LifecycleReasonCode.DISCOVERED}),
724
+ ProviderLifecycleState.INCOMPATIBLE: frozenset(
725
+ {
726
+ LifecycleReasonCode.REQUIRED_CAPABILITY_MISSING,
727
+ LifecycleReasonCode.POLICY_RANGE_UNSUPPORTED,
728
+ LifecycleReasonCode.PROBE_FAILED,
729
+ }
730
+ ),
731
+ ProviderLifecycleState.UNAVAILABLE: frozenset(
732
+ {
733
+ LifecycleReasonCode.RUNTIME_MISSING,
734
+ LifecycleReasonCode.CREDENTIAL_UNHEALTHY,
735
+ }
736
+ ),
737
+ }
738
+ if reason not in initial_reasons[current_state]:
739
+ raise ValueError("lifecycle_transition_invalid")
740
+ if (
741
+ current_state is ProviderLifecycleState.UNAVAILABLE
742
+ and previous_state is not current_state
743
+ and reason
744
+ not in {
745
+ LifecycleReasonCode.RUNTIME_MISSING,
746
+ LifecycleReasonCode.CREDENTIAL_UNHEALTHY,
747
+ }
748
+ ):
749
+ raise ValueError("lifecycle_transition_invalid")
750
+ if (
751
+ previous_state is ProviderLifecycleState.INCOMPATIBLE
752
+ and current_state is ProviderLifecycleState.VERIFICATION_REQUIRED
753
+ and reason is not LifecycleReasonCode.POLICY_PROMOTED
754
+ ):
755
+ raise ValueError("lifecycle_transition_invalid")
756
+ object.__setattr__(self, "event_id", _identifier(self.event_id, code="event_id_invalid"))
757
+ object.__setattr__(self, "provider", provider)
758
+ object.__setattr__(self, "runtime_kind", runtime_kind)
759
+ object.__setattr__(self, "previous_identity_digest", previous_digest)
760
+ object.__setattr__(self, "current_identity_digest", current_digest)
761
+ object.__setattr__(self, "previous_state", previous_state)
762
+ object.__setattr__(self, "current_state", current_state)
763
+ object.__setattr__(self, "reason", reason)
764
+ object.__setattr__(
765
+ self,
766
+ "policy_version",
767
+ _semantic_version(self.policy_version, code="policy_version_invalid"),
768
+ )
769
+ object.__setattr__(
770
+ self, "occurred_at", _timestamp(self.occurred_at, code="occurred_at_invalid")
771
+ )