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,815 @@
1
+ """Deterministic eligibility, ranking, and promotion-confidence policy."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ from dataclasses import dataclass
9
+ from typing import Final
10
+
11
+ from .contracts import (
12
+ CapabilitySnapshot,
13
+ CliIdentity,
14
+ Effort,
15
+ PermissionMode,
16
+ ProviderId,
17
+ RiskTier,
18
+ TaskCategory,
19
+ TaskProfile,
20
+ )
21
+ from .registry import (
22
+ BUNDLED_PROFILES,
23
+ ModelRole,
24
+ RegistrySnapshot,
25
+ UsageClass,
26
+ lifecycle_is_eligible,
27
+ profile_is_eligible,
28
+ )
29
+
30
+ POLICY_VERSION: Final = "2"
31
+ EVIDENCE_VERSION: Final = "1"
32
+ _Z_95: Final = 1.959963984540054
33
+
34
+ _ROLE_BONUSES: Final = {
35
+ TaskCategory.DOCUMENTATION: {
36
+ ModelRole.CODING_PRIMARY: 100,
37
+ ModelRole.CODING: 40,
38
+ },
39
+ TaskCategory.ISOLATED_CODE: {
40
+ ModelRole.CODING_PRIMARY: 100,
41
+ ModelRole.CODING: 40,
42
+ },
43
+ TaskCategory.FEATURE: {
44
+ ModelRole.CODING_PRIMARY: 100,
45
+ ModelRole.CODING: 40,
46
+ ModelRole.AGENTIC: 20,
47
+ },
48
+ TaskCategory.REFACTOR: {
49
+ ModelRole.CODING_PRIMARY: 80,
50
+ ModelRole.CODING: 40,
51
+ ModelRole.REASONING: 20,
52
+ },
53
+ TaskCategory.ARCHITECTURE: {
54
+ ModelRole.LONG_CONTEXT: 80,
55
+ ModelRole.REASONING: 40,
56
+ },
57
+ }
58
+ _USAGE_PENALTIES: Final = {
59
+ UsageClass.MEDIUM: 0,
60
+ UsageClass.HIGH: 40,
61
+ }
62
+ _CODE_CAPABILITIES: Final = frozenset({"code"})
63
+ _REFACTOR_CAPABILITIES: Final = frozenset({"code", "reasoning"})
64
+ _ARCHITECTURE_CAPABILITIES: Final = frozenset({"architecture"})
65
+ CLI_MINIMUM_LEARNING_SAMPLES: Final = 20
66
+ CLI_RECENCY_HALF_LIFE_DAYS: Final = 30
67
+ _LEARNED_PROVIDERS: Final = ("claude-code", "codex")
68
+ _LEARNED_RISK_CEILINGS: Final = {"claude-code": "medium", "codex": "medium"}
69
+ _LEARNED_PERMISSION_CEILING: Final = "workspace-write"
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class CandidateMetrics:
74
+ model_id: str
75
+ effort: Effort
76
+ repository_success_millis: int | None
77
+ global_success_millis: int
78
+ expected_input_tokens: int
79
+ expected_output_tokens: int
80
+ expected_latency_ms: int
81
+ retry_rate_millis: int
82
+ escalation_rate_millis: int
83
+ quota_scarcity_millis: int
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class _ValidatedCandidateMetrics:
88
+ reliability: int
89
+ input_tokens: int
90
+ output_tokens: int
91
+ latency: int
92
+ retry: int
93
+ escalation: int
94
+ scarcity: int
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class PolicyGates:
99
+ graph_valid: bool = True
100
+ graph_fresh: bool = True
101
+ registry_fresh: bool = True
102
+ data_policy_allowed: bool = True
103
+ storage_available: bool = True
104
+ task_evaluated: bool = False
105
+ context_tokens: int = 1_000
106
+ budget_tokens: int = 10_000
107
+ current_date: str = "2026-07-14"
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class ScoredCandidate:
112
+ model_id: str
113
+ effort: Effort
114
+ score: int
115
+ components: tuple[tuple[str, int], ...]
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class PolicyResult:
120
+ selected: ScoredCandidate | None
121
+ ranked: tuple[ScoredCandidate, ...]
122
+ manual_handoff: bool
123
+ recommended_channels: tuple[str, ...]
124
+ reasons: tuple[str, ...]
125
+ policy_version: str = POLICY_VERSION
126
+ evidence_version: str = EVIDENCE_VERSION
127
+
128
+
129
+ @dataclass(frozen=True)
130
+ class PromotionEligibility:
131
+ eligible: bool
132
+ lower_bound: float
133
+ reason: str
134
+ required_samples: int | None
135
+ required_lower_bound: float | None
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class CliCandidateMetrics:
140
+ capability_snapshot_digest: str
141
+ effort: Effort
142
+ repository_success_millis: int | None
143
+ global_success_millis: int
144
+ sample_count: int
145
+ expected_input_tokens: int
146
+ expected_output_tokens: int
147
+ expected_latency_ms: int
148
+ quota_scarcity_millis: int
149
+ success_count: int | None = None
150
+ latest_observed_at: int | None = None
151
+ cost_status: str = "unknown"
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class CliPolicyGates:
156
+ graph_valid: bool = True
157
+ graph_fresh: bool = True
158
+ data_policy_allowed: bool = True
159
+ storage_available: bool = True
160
+ authenticated_providers: tuple[ProviderId, ...] = (
161
+ ProviderId.CLAUDE_CODE,
162
+ ProviderId.CODEX,
163
+ )
164
+ current_cli_identities: tuple[CliIdentity, ...] = ()
165
+ permission_mode: PermissionMode = PermissionMode.WORKSPACE_WRITE
166
+ context_tokens: int = 1_000
167
+ budget_tokens: int = 10_000
168
+ now: int = 150
169
+
170
+
171
+ @dataclass(frozen=True)
172
+ class ScoredCliCandidate:
173
+ provider: ProviderId
174
+ requested_model: str
175
+ effective_model: str
176
+ effort: Effort
177
+ capability_snapshot_digest: str
178
+ score: int
179
+ components: tuple[tuple[str, int], ...]
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class CliPolicyResult:
184
+ selected: ScoredCliCandidate | None
185
+ ranked: tuple[ScoredCliCandidate, ...]
186
+ manual_handoff: bool
187
+ reasons: tuple[str, ...]
188
+ policy_version: str = "3"
189
+
190
+
191
+ @dataclass(frozen=True)
192
+ class CliLearnedPolicy:
193
+ policy_version: str
194
+ parent_version: str | None
195
+ scoring_weights: tuple[tuple[str, int], ...]
196
+ minimum_samples: int = CLI_MINIMUM_LEARNING_SAMPLES
197
+ recency_half_life_days: int = CLI_RECENCY_HALF_LIFE_DAYS
198
+ source_policy_version: str = "3"
199
+
200
+ def __post_init__(self) -> None:
201
+ if not isinstance(self.policy_version, str) or not self.policy_version:
202
+ raise ValueError("policy_version_invalid")
203
+ if self.parent_version is not None and (
204
+ not isinstance(self.parent_version, str) or not self.parent_version
205
+ ):
206
+ raise ValueError("parent_policy_version_invalid")
207
+ if (
208
+ not isinstance(self.scoring_weights, tuple)
209
+ or not self.scoring_weights
210
+ or len({name for name, _ in self.scoring_weights}) != len(self.scoring_weights)
211
+ ):
212
+ raise ValueError("policy_weights_invalid")
213
+ allowed = {"reliability", "tokens", "latency", "scarcity", "effort", "confidence", "recency"}
214
+ for name, value in self.scoring_weights:
215
+ if name not in allowed:
216
+ raise ValueError("policy_weights_invalid")
217
+ _bounded_metric(value, "policy_weights_invalid", 1_000)
218
+ if (
219
+ isinstance(self.minimum_samples, bool)
220
+ or not isinstance(self.minimum_samples, int)
221
+ or not 20 <= self.minimum_samples <= 10_000
222
+ ):
223
+ raise ValueError("policy_minimum_samples_invalid")
224
+ if (
225
+ isinstance(self.recency_half_life_days, bool)
226
+ or not isinstance(self.recency_half_life_days, int)
227
+ or not 7 <= self.recency_half_life_days <= 365
228
+ ):
229
+ raise ValueError("policy_recency_invalid")
230
+
231
+ def to_payload(self) -> dict[str, object]:
232
+ """Return fixed guardrails plus tunable ranking weights only."""
233
+ return {
234
+ "schema_version": 1,
235
+ "autonomy": False,
236
+ "allowed_providers": list(_LEARNED_PROVIDERS),
237
+ "permission_ceiling": _LEARNED_PERMISSION_CEILING,
238
+ "risk_ceilings": dict(_LEARNED_RISK_CEILINGS),
239
+ "scoring_weights": dict(sorted(self.scoring_weights)),
240
+ "minimum_samples": self.minimum_samples,
241
+ "recency_half_life_days": self.recency_half_life_days,
242
+ "source_policy_version": self.source_policy_version,
243
+ }
244
+
245
+
246
+ @dataclass(frozen=True)
247
+ class PolicyComparisonEvidence:
248
+ baseline_version: str
249
+ candidate_version: str
250
+ evidence_hash: str
251
+ sample_count: int
252
+ score_delta_millis: int
253
+ promotion_eligible: bool
254
+ reason: str
255
+
256
+
257
+ def cli_policy_payload_hash(candidate: CliLearnedPolicy) -> str:
258
+ payload = json.dumps(
259
+ candidate.to_payload(), sort_keys=True, separators=(",", ":"), ensure_ascii=True
260
+ ).encode("utf-8")
261
+ return hashlib.sha256(payload).hexdigest()
262
+
263
+
264
+ def sign_cli_policy(candidate: CliLearnedPolicy, evidence_hash: str, signing_key: bytes) -> str:
265
+ if not isinstance(signing_key, bytes) or len(signing_key) < 32:
266
+ raise ValueError("policy_signing_key_invalid")
267
+ if not isinstance(evidence_hash, str) or len(evidence_hash) != 64 or any(
268
+ character not in "0123456789abcdef" for character in evidence_hash
269
+ ):
270
+ raise ValueError("evidence_hash_invalid")
271
+ signed = json.dumps(
272
+ {
273
+ "evidence_hash": evidence_hash,
274
+ "parent_version": candidate.parent_version,
275
+ "payload_hash": cli_policy_payload_hash(candidate),
276
+ "policy_version": candidate.policy_version,
277
+ },
278
+ sort_keys=True, separators=(",", ":"), ensure_ascii=True,
279
+ ).encode("utf-8")
280
+ return hmac.new(signing_key, signed, hashlib.sha256).hexdigest()
281
+
282
+
283
+ def compare_cli_policy_candidate(
284
+ candidate: CliLearnedPolicy,
285
+ *,
286
+ evidence_hash: str,
287
+ sample_count: int,
288
+ baseline_score_millis: int,
289
+ candidate_score_millis: int,
290
+ ) -> PolicyComparisonEvidence:
291
+ sample_count = _bounded_metric(sample_count, "sample_count_invalid")
292
+ baseline = _bounded_metric(baseline_score_millis, "baseline_score_invalid", 1_000)
293
+ proposed = _bounded_metric(candidate_score_millis, "candidate_score_invalid", 1_000)
294
+ if len(evidence_hash) != 64 or any(c not in "0123456789abcdef" for c in evidence_hash):
295
+ raise ValueError("evidence_hash_invalid")
296
+ enough = sample_count >= candidate.minimum_samples
297
+ improved = proposed > baseline
298
+ return PolicyComparisonEvidence(
299
+ candidate.source_policy_version,
300
+ candidate.policy_version,
301
+ evidence_hash,
302
+ sample_count,
303
+ proposed - baseline,
304
+ enough and improved,
305
+ "candidate_improves_with_sufficient_evidence"
306
+ if enough and improved
307
+ else "sample_size_insufficient" if not enough else "candidate_not_better",
308
+ )
309
+
310
+
311
+ def _global_failures(gates: PolicyGates) -> list[str]:
312
+ failures: list[str] = []
313
+ for allowed, reason in (
314
+ (gates.graph_valid, "graph_invalid"),
315
+ (gates.graph_fresh, "graph_stale"),
316
+ (gates.registry_fresh, "registry_stale"),
317
+ (gates.data_policy_allowed, "data_policy_blocked"),
318
+ (gates.storage_available, "storage_unavailable"),
319
+ ):
320
+ if not isinstance(allowed, bool) or not allowed:
321
+ failures.append(reason)
322
+ if (
323
+ isinstance(gates.context_tokens, bool)
324
+ or not isinstance(gates.context_tokens, int)
325
+ or gates.context_tokens < 0
326
+ ):
327
+ failures.append("context_invalid")
328
+ if (
329
+ isinstance(gates.budget_tokens, bool)
330
+ or not isinstance(gates.budget_tokens, int)
331
+ or gates.budget_tokens < 0
332
+ ):
333
+ failures.append("budget_invalid")
334
+ return failures
335
+
336
+
337
+ def _bounded_metric(value: object, code: str, maximum: int = 10**9) -> int:
338
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
339
+ raise ValueError(code)
340
+ if value < 0 or value > maximum or int(value) != value:
341
+ raise ValueError(code)
342
+ return int(value)
343
+
344
+
345
+ def _validate_candidate_metrics(candidate: CandidateMetrics) -> _ValidatedCandidateMetrics:
346
+ global_success = _bounded_metric(
347
+ candidate.global_success_millis,
348
+ "global_success_invalid",
349
+ 1_000,
350
+ )
351
+ repository_success = candidate.repository_success_millis
352
+ reliability = (
353
+ global_success
354
+ if repository_success is None
355
+ else _bounded_metric(repository_success, "repository_success_invalid", 1_000)
356
+ )
357
+ return _ValidatedCandidateMetrics(
358
+ reliability=reliability,
359
+ input_tokens=_bounded_metric(
360
+ candidate.expected_input_tokens,
361
+ "expected_input_tokens_invalid",
362
+ ),
363
+ output_tokens=_bounded_metric(
364
+ candidate.expected_output_tokens,
365
+ "expected_output_tokens_invalid",
366
+ ),
367
+ latency=_bounded_metric(candidate.expected_latency_ms, "expected_latency_invalid"),
368
+ retry=_bounded_metric(candidate.retry_rate_millis, "retry_rate_invalid", 1_000),
369
+ escalation=_bounded_metric(
370
+ candidate.escalation_rate_millis,
371
+ "escalation_rate_invalid",
372
+ 1_000,
373
+ ),
374
+ scarcity=_bounded_metric(
375
+ candidate.quota_scarcity_millis,
376
+ "quota_scarcity_invalid",
377
+ 1_000,
378
+ ),
379
+ )
380
+
381
+
382
+ def _accepted_capabilities(category: TaskCategory) -> frozenset[str]:
383
+ if category is TaskCategory.ARCHITECTURE:
384
+ return _ARCHITECTURE_CAPABILITIES
385
+ if category is TaskCategory.REFACTOR:
386
+ return _REFACTOR_CAPABILITIES
387
+ return _CODE_CAPABILITIES
388
+
389
+
390
+ def _candidate_failure(
391
+ task: TaskProfile,
392
+ snapshot: RegistrySnapshot,
393
+ candidate: CandidateMetrics,
394
+ gates: PolicyGates,
395
+ ) -> str | None:
396
+ entry = BUNDLED_PROFILES.get(candidate.model_id)
397
+ if entry is None:
398
+ return "model_profile_missing"
399
+ try:
400
+ effort = Effort(candidate.effort)
401
+ except (TypeError, ValueError):
402
+ return "effort_unsupported"
403
+ if effort not in entry.profile.supported_efforts:
404
+ return "effort_unsupported"
405
+ if not profile_is_eligible(candidate.model_id, task.risk):
406
+ return "risk_ineligible"
407
+ if not lifecycle_is_eligible(entry.retirement_date, gates.current_date):
408
+ return "model_retiring"
409
+ inventory_model = next(
410
+ (model for model in snapshot.models if model.model_id == candidate.model_id),
411
+ None,
412
+ )
413
+ if inventory_model is None:
414
+ return "model_unavailable"
415
+ expected_input = _bounded_metric(candidate.expected_input_tokens, "expected_input_tokens_invalid")
416
+ expected_output = _bounded_metric(candidate.expected_output_tokens, "expected_output_tokens_invalid")
417
+ context_limit = min(
418
+ entry.profile.context_window_tokens,
419
+ inventory_model.context_window_tokens or entry.profile.context_window_tokens,
420
+ )
421
+ if max(gates.context_tokens, expected_input) + expected_output > context_limit:
422
+ return "context_exceeded"
423
+ if expected_input + expected_output > gates.budget_tokens:
424
+ return "budget_exceeded"
425
+ if not gates.task_evaluated and not entry.profile.provisional:
426
+ return "task_not_evaluated"
427
+ accepted_capabilities = _accepted_capabilities(task.category)
428
+ if accepted_capabilities.isdisjoint(entry.profile.capabilities):
429
+ return "capability_missing"
430
+ return None
431
+
432
+
433
+ def _score(
434
+ candidate: CandidateMetrics,
435
+ task: TaskProfile,
436
+ metrics: _ValidatedCandidateMetrics,
437
+ ) -> ScoredCandidate:
438
+ entry = BUNDLED_PROFILES[candidate.model_id]
439
+ token_penalty = min(250, (metrics.input_tokens + metrics.output_tokens) // 100)
440
+ latency_penalty = min(200, metrics.latency // 1_000)
441
+ retry_penalty = metrics.retry // 5
442
+ escalation_penalty = metrics.escalation // 5
443
+ scarcity_penalty = metrics.scarcity // 10
444
+ role_bonuses = _ROLE_BONUSES.get(task.category, {})
445
+ role_fit_bonus = max((role_bonuses.get(role, 0) for role in entry.roles), default=0)
446
+ usage_penalty = _USAGE_PENALTIES[entry.usage_class]
447
+ score = max(
448
+ 0,
449
+ min(
450
+ 1_000,
451
+ metrics.reliability
452
+ + role_fit_bonus
453
+ - usage_penalty
454
+ - token_penalty
455
+ - latency_penalty
456
+ - retry_penalty
457
+ - escalation_penalty
458
+ - scarcity_penalty,
459
+ ),
460
+ )
461
+ return ScoredCandidate(
462
+ candidate.model_id,
463
+ Effort(candidate.effort),
464
+ score,
465
+ (
466
+ ("reliability", metrics.reliability),
467
+ ("role_fit_bonus", role_fit_bonus),
468
+ ("usage_penalty", usage_penalty),
469
+ ("token_penalty", token_penalty),
470
+ ("latency_penalty", latency_penalty),
471
+ ("retry_penalty", retry_penalty),
472
+ ("escalation_penalty", escalation_penalty),
473
+ ("quota_scarcity_penalty", scarcity_penalty),
474
+ ),
475
+ )
476
+
477
+
478
+ def rank_candidates(
479
+ task: TaskProfile,
480
+ snapshot: RegistrySnapshot,
481
+ candidates: tuple[CandidateMetrics, ...],
482
+ gates: PolicyGates,
483
+ ) -> PolicyResult:
484
+ """Apply hard gates then rank eligible candidates deterministically."""
485
+ global_failures = _global_failures(gates)
486
+ if global_failures:
487
+ return PolicyResult(
488
+ None,
489
+ (),
490
+ True,
491
+ ("claude_code", "codex"),
492
+ tuple(global_failures),
493
+ )
494
+ scored: list[ScoredCandidate] = []
495
+ rejected: list[str] = []
496
+ for candidate in candidates:
497
+ metrics = _validate_candidate_metrics(candidate)
498
+ failure = _candidate_failure(task, snapshot, candidate, gates)
499
+ if failure is not None:
500
+ rejected.append(failure)
501
+ continue
502
+ scored.append(_score(candidate, task, metrics))
503
+ ranked = tuple(
504
+ sorted(scored, key=lambda item: (-item.score, item.model_id, item.effort.value))
505
+ )
506
+ if not ranked:
507
+ return PolicyResult(
508
+ None,
509
+ (),
510
+ True,
511
+ ("claude_code", "codex"),
512
+ tuple(sorted(set(rejected))) or ("no_candidates",),
513
+ )
514
+ return PolicyResult(
515
+ ranked[0],
516
+ ranked,
517
+ False,
518
+ (),
519
+ ("eligible_ranked",),
520
+ )
521
+
522
+
523
+ _RISK_ORDER: Final = {RiskTier.LOW: 0, RiskTier.MEDIUM: 1, RiskTier.HIGH: 2}
524
+ _EFFORT_PENALTY: Final = {
525
+ Effort.DEFAULT: 30,
526
+ Effort.LOW: 0,
527
+ Effort.MEDIUM: 10,
528
+ Effort.HIGH: 30,
529
+ Effort.XHIGH: 60,
530
+ Effort.MAX: 80,
531
+ }
532
+
533
+
534
+ def _cli_global_failures(gates: CliPolicyGates) -> list[str]:
535
+ failures: list[str] = []
536
+ for allowed, reason in (
537
+ (gates.graph_valid, "graph_invalid"),
538
+ (gates.graph_fresh, "graph_stale"),
539
+ (gates.data_policy_allowed, "data_policy_blocked"),
540
+ (gates.storage_available, "storage_unavailable"),
541
+ ):
542
+ if not isinstance(allowed, bool) or not allowed:
543
+ failures.append(reason)
544
+ try:
545
+ providers = tuple(ProviderId(item) for item in gates.authenticated_providers)
546
+ PermissionMode(gates.permission_mode)
547
+ except (TypeError, ValueError):
548
+ failures.append("policy_gate_invalid")
549
+ providers = ()
550
+ if len(set(providers)) != len(providers):
551
+ failures.append("policy_gate_invalid")
552
+ if (
553
+ not isinstance(gates.current_cli_identities, tuple)
554
+ or any(not isinstance(item, CliIdentity) for item in gates.current_cli_identities)
555
+ or len({item.provider for item in gates.current_cli_identities})
556
+ != len(gates.current_cli_identities)
557
+ ):
558
+ failures.append("policy_gate_invalid")
559
+ for value, reason in (
560
+ (gates.context_tokens, "context_invalid"),
561
+ (gates.budget_tokens, "budget_invalid"),
562
+ (gates.now, "policy_time_invalid"),
563
+ ):
564
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
565
+ failures.append(reason)
566
+ if failures:
567
+ return failures
568
+ return failures
569
+
570
+
571
+ def rank_cli_candidates(
572
+ task: TaskProfile,
573
+ snapshots: tuple[CapabilitySnapshot, ...],
574
+ candidates: tuple[CliCandidateMetrics, ...],
575
+ gates: CliPolicyGates,
576
+ ) -> CliPolicyResult:
577
+ """Apply provider-neutral hard gates before deterministic CLI profile scoring."""
578
+ failures = _cli_global_failures(gates)
579
+ if failures:
580
+ return CliPolicyResult(None, (), True, tuple(sorted(set(failures))))
581
+ snapshot_by_digest: dict[str, CapabilitySnapshot] = {}
582
+ for snapshot in snapshots:
583
+ if not isinstance(snapshot, CapabilitySnapshot) or snapshot.digest in snapshot_by_digest:
584
+ raise ValueError("capability_snapshot_invalid")
585
+ snapshot_by_digest[snapshot.digest] = snapshot
586
+ authenticated = {ProviderId(item) for item in gates.authenticated_providers}
587
+ current_identities = {item.provider: item for item in gates.current_cli_identities}
588
+ required_permission = PermissionMode(gates.permission_mode)
589
+ scored: list[ScoredCliCandidate] = []
590
+ rejected: list[str] = []
591
+ for candidate in candidates:
592
+ if not isinstance(candidate, CliCandidateMetrics):
593
+ raise ValueError("candidate_invalid")
594
+ snapshot = snapshot_by_digest.get(candidate.capability_snapshot_digest)
595
+ if snapshot is None:
596
+ rejected.append("capability_snapshot_missing")
597
+ continue
598
+ profile = snapshot.profile
599
+ if snapshot.expires_at <= gates.now:
600
+ rejected.append("capability_snapshot_expired")
601
+ continue
602
+ if profile.provider not in authenticated:
603
+ rejected.append("provider_unauthenticated")
604
+ continue
605
+ if current_identities.get(profile.provider) != snapshot.identity:
606
+ rejected.append("cli_identity_changed")
607
+ continue
608
+ if profile.permission_mode is not required_permission:
609
+ rejected.append("permission_mismatch")
610
+ continue
611
+ try:
612
+ effort = Effort(candidate.effort)
613
+ except (TypeError, ValueError):
614
+ rejected.append("effort_unsupported")
615
+ continue
616
+ if effort not in profile.supported_efforts:
617
+ rejected.append("effort_unsupported")
618
+ continue
619
+ if _RISK_ORDER[task.risk] > _RISK_ORDER[profile.risk_ceiling]:
620
+ rejected.append("risk_ineligible")
621
+ continue
622
+ input_tokens = _bounded_metric(
623
+ candidate.expected_input_tokens, "expected_input_tokens_invalid"
624
+ )
625
+ output_tokens = _bounded_metric(
626
+ candidate.expected_output_tokens, "expected_output_tokens_invalid"
627
+ )
628
+ if max(gates.context_tokens, input_tokens) + output_tokens > profile.context_window_tokens:
629
+ rejected.append("context_exceeded")
630
+ continue
631
+ if input_tokens + output_tokens > gates.budget_tokens:
632
+ rejected.append("budget_exceeded")
633
+ continue
634
+ if _accepted_capabilities(task.category).isdisjoint(profile.capabilities):
635
+ rejected.append("capability_missing")
636
+ continue
637
+ sample_count = _bounded_metric(candidate.sample_count, "sample_count_invalid")
638
+ if candidate.cost_status != "unknown":
639
+ raise ValueError("cost_status_invalid")
640
+ global_success = _bounded_metric(
641
+ candidate.global_success_millis, "global_success_invalid", 1_000
642
+ )
643
+ repository_success = candidate.repository_success_millis
644
+ reliability = global_success
645
+ if sample_count >= 5 and repository_success is not None:
646
+ reliability = _bounded_metric(
647
+ repository_success, "repository_success_invalid", 1_000
648
+ )
649
+ if candidate.success_count is None:
650
+ confidence_penalty = 150
651
+ else:
652
+ successes = _bounded_metric(candidate.success_count, "success_count_invalid")
653
+ if successes > sample_count:
654
+ raise ValueError("success_count_invalid")
655
+ confidence_penalty = (
656
+ 150
657
+ if sample_count < CLI_MINIMUM_LEARNING_SAMPLES
658
+ else min(200, round((1 - wilson_lower_bound(successes, sample_count)) * 200))
659
+ )
660
+ if candidate.latest_observed_at is None:
661
+ recency_penalty = 50
662
+ else:
663
+ latest = _bounded_metric(candidate.latest_observed_at, "latest_observed_at_invalid")
664
+ if latest > gates.now:
665
+ raise ValueError("latest_observed_at_invalid")
666
+ age_days = (gates.now - latest) // 86_400
667
+ recency_penalty = min(100, age_days * 2)
668
+ latency = _bounded_metric(candidate.expected_latency_ms, "expected_latency_invalid")
669
+ scarcity = _bounded_metric(
670
+ candidate.quota_scarcity_millis, "quota_scarcity_invalid", 1_000
671
+ )
672
+ token_penalty = min(250, (input_tokens + output_tokens) // 100)
673
+ latency_penalty = min(200, latency // 1_000)
674
+ scarcity_penalty = scarcity // 10
675
+ effort_penalty = _EFFORT_PENALTY[effort]
676
+ cost_unknown_penalty = 25
677
+ score = max(
678
+ 0,
679
+ min(
680
+ 1_000,
681
+ reliability
682
+ - token_penalty
683
+ - latency_penalty
684
+ - scarcity_penalty
685
+ - effort_penalty
686
+ - confidence_penalty
687
+ - recency_penalty
688
+ - cost_unknown_penalty,
689
+ ),
690
+ )
691
+ scored.append(
692
+ ScoredCliCandidate(
693
+ profile.provider,
694
+ profile.requested_model,
695
+ profile.effective_model,
696
+ effort,
697
+ snapshot.digest,
698
+ score,
699
+ (
700
+ ("reliability", reliability),
701
+ ("token_penalty", token_penalty),
702
+ ("latency_penalty", latency_penalty),
703
+ ("quota_scarcity_penalty", scarcity_penalty),
704
+ ("effort_penalty", effort_penalty),
705
+ ("confidence_penalty", confidence_penalty),
706
+ ("recency_penalty", recency_penalty),
707
+ ("cost_unknown_penalty", cost_unknown_penalty),
708
+ ),
709
+ )
710
+ )
711
+ ranked = tuple(
712
+ sorted(
713
+ scored,
714
+ key=lambda item: (
715
+ -item.score,
716
+ item.provider.value,
717
+ item.effective_model,
718
+ item.effort.value,
719
+ item.capability_snapshot_digest,
720
+ ),
721
+ )
722
+ )
723
+ if not ranked:
724
+ return CliPolicyResult(
725
+ None,
726
+ (),
727
+ True,
728
+ tuple(sorted(set(rejected))) or ("no_candidates",),
729
+ )
730
+ return CliPolicyResult(ranked[0], ranked, False, ("eligible_ranked",))
731
+
732
+
733
+ def wilson_lower_bound(successes: int, total: int) -> float:
734
+ """Return the two-sided 95% Wilson score lower bound."""
735
+ if (
736
+ isinstance(successes, bool)
737
+ or isinstance(total, bool)
738
+ or not isinstance(successes, int)
739
+ or not isinstance(total, int)
740
+ or successes < 0
741
+ or total < 0
742
+ or successes > total
743
+ ):
744
+ raise ValueError("confidence_counts_invalid")
745
+ if total == 0:
746
+ return 0.0
747
+ proportion = successes / total
748
+ z_squared = _Z_95 * _Z_95
749
+ denominator = 1 + z_squared / total
750
+ center = proportion + z_squared / (2 * total)
751
+ margin = _Z_95 * math.sqrt(
752
+ (proportion * (1 - proportion) + z_squared / (4 * total)) / total
753
+ )
754
+ return max(0.0, (center - margin) / denominator)
755
+
756
+
757
+ def promotion_eligibility(
758
+ risk: RiskTier | str,
759
+ *,
760
+ successes: int,
761
+ total: int,
762
+ severe_failures: int,
763
+ ) -> PromotionEligibility:
764
+ """Evaluate evidence only; this function never grants execution authority."""
765
+ normalized = RiskTier(risk)
766
+ lower = wilson_lower_bound(successes, total)
767
+ if (
768
+ isinstance(severe_failures, bool)
769
+ or not isinstance(severe_failures, int)
770
+ or severe_failures < 0
771
+ or severe_failures > total
772
+ ):
773
+ raise ValueError("severe_failure_count_invalid")
774
+ if normalized is RiskTier.HIGH:
775
+ return PromotionEligibility(
776
+ False,
777
+ lower,
778
+ "high_risk_permanent_gate",
779
+ None,
780
+ None,
781
+ )
782
+ required_samples, required_lower = (
783
+ (50, 0.90) if normalized is RiskTier.LOW else (100, 0.95)
784
+ )
785
+ if severe_failures:
786
+ return PromotionEligibility(
787
+ False,
788
+ lower,
789
+ "severe_failure_open",
790
+ required_samples,
791
+ required_lower,
792
+ )
793
+ if total < required_samples:
794
+ return PromotionEligibility(
795
+ False,
796
+ lower,
797
+ "sample_size_insufficient",
798
+ required_samples,
799
+ required_lower,
800
+ )
801
+ if lower < required_lower:
802
+ return PromotionEligibility(
803
+ False,
804
+ lower,
805
+ "confidence_insufficient",
806
+ required_samples,
807
+ required_lower,
808
+ )
809
+ return PromotionEligibility(
810
+ True,
811
+ lower,
812
+ "evidence_threshold_met",
813
+ required_samples,
814
+ required_lower,
815
+ )