algo-cli-runtime 0.14.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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,136 @@
1
+ """CUSUM detection for sustained runtime performance changes (ALGO.md L3)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import statistics
7
+ from dataclasses import asdict, dataclass
8
+ from enum import Enum
9
+ from typing import Iterable
10
+
11
+
12
+ class RegressionState(str, Enum):
13
+ INSUFFICIENT_DATA = "insufficient_data"
14
+ STABLE = "stable"
15
+ IMPROVING = "improving"
16
+ REGRESSING = "regressing"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class CUSUMResult:
21
+ state: RegressionState
22
+ sample_count: int
23
+ baseline: float | None
24
+ scale: float | None
25
+ positive_score: float
26
+ negative_score: float
27
+ change_index: int | None
28
+ reason: str
29
+
30
+ def to_dict(self) -> dict[str, object]:
31
+ payload = asdict(self)
32
+ payload["state"] = self.state.value
33
+ return payload
34
+
35
+
36
+ def detect_cusum(
37
+ samples: Iterable[float],
38
+ *,
39
+ warmup: int = 5,
40
+ slack_scale: float = 0.5,
41
+ threshold_scale: float = 5.0,
42
+ min_consecutive: int = 2,
43
+ ) -> CUSUMResult:
44
+ """Detect a sustained upward/downward shift from a robust warmup baseline."""
45
+ values: list[float] = []
46
+ for sample in samples:
47
+ try:
48
+ value = float(sample)
49
+ except (TypeError, ValueError):
50
+ continue
51
+ if math.isfinite(value) and value >= 0.0:
52
+ values.append(value)
53
+ warmup = max(3, int(warmup))
54
+ min_consecutive = max(2, int(min_consecutive))
55
+ if len(values) < warmup + min_consecutive:
56
+ return CUSUMResult(
57
+ RegressionState.INSUFFICIENT_DATA,
58
+ len(values),
59
+ None,
60
+ None,
61
+ 0.0,
62
+ 0.0,
63
+ None,
64
+ f"need at least {warmup + min_consecutive} finite samples",
65
+ )
66
+
67
+ baseline_values = values[:warmup]
68
+ baseline = statistics.median(baseline_values)
69
+ deviations = [abs(value - baseline) for value in baseline_values]
70
+ robust_scale = statistics.median(deviations) * 1.4826
71
+ scale = max(robust_scale, abs(baseline) * 0.02, 1e-6)
72
+ slack = max(0.0, float(slack_scale)) * scale
73
+ threshold = max(1.0, float(threshold_scale)) * scale
74
+
75
+ positive = 0.0
76
+ negative = 0.0
77
+ positive_run = 0
78
+ negative_run = 0
79
+ positive_start: int | None = None
80
+ negative_start: int | None = None
81
+ for index, value in enumerate(values[warmup:], start=warmup):
82
+ delta = value - baseline
83
+ positive = max(0.0, positive + delta - slack)
84
+ negative = min(0.0, negative + delta + slack)
85
+
86
+ if delta > slack:
87
+ positive_run += 1
88
+ positive_start = index if positive_start is None else positive_start
89
+ else:
90
+ positive = 0.0
91
+ positive_run = 0
92
+ positive_start = None
93
+ if delta < -slack:
94
+ negative_run += 1
95
+ negative_start = index if negative_start is None else negative_start
96
+ else:
97
+ negative = 0.0
98
+ negative_run = 0
99
+ negative_start = None
100
+
101
+ if positive >= threshold and positive_run >= min_consecutive:
102
+ return CUSUMResult(
103
+ RegressionState.REGRESSING,
104
+ len(values),
105
+ baseline,
106
+ scale,
107
+ positive,
108
+ negative,
109
+ positive_start,
110
+ "sustained latency increase crossed the CUSUM decision threshold",
111
+ )
112
+ if abs(negative) >= threshold and negative_run >= min_consecutive:
113
+ return CUSUMResult(
114
+ RegressionState.IMPROVING,
115
+ len(values),
116
+ baseline,
117
+ scale,
118
+ positive,
119
+ negative,
120
+ negative_start,
121
+ "sustained latency decrease crossed the CUSUM decision threshold",
122
+ )
123
+
124
+ return CUSUMResult(
125
+ RegressionState.STABLE,
126
+ len(values),
127
+ baseline,
128
+ scale,
129
+ positive,
130
+ negative,
131
+ None,
132
+ "no sustained shift crossed the CUSUM decision threshold",
133
+ )
134
+
135
+
136
+ __all__ = ["CUSUMResult", "RegressionState", "detect_cusum"]
@@ -0,0 +1,308 @@
1
+ """Pure, fail-closed grading for the Algo CLI harness scorecard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ from collections.abc import Iterable, Mapping
8
+ from typing import Any
9
+
10
+
11
+ SCHEMA_VERSION = 2
12
+ SCORED_GATE_COUNT = 10
13
+ MAX_SCORE = float(SCORED_GATE_COUNT)
14
+ VALID_STATUSES = frozenset({"pass", "warn", "unavailable", "fail", "error"})
15
+ STATUS_POINTS = {
16
+ "pass": 1.0,
17
+ "warn": 0.5,
18
+ "unavailable": 0.0,
19
+ "fail": 0.0,
20
+ "error": 0.0,
21
+ }
22
+
23
+
24
+ def _safe_text(value: Any) -> str:
25
+ if value is None:
26
+ return ""
27
+ try:
28
+ return str(value)
29
+ except Exception:
30
+ return f"<{type(value).__name__}>"
31
+
32
+
33
+ def _json_safe(value: Any, *, _seen: set[int] | None = None, _depth: int = 0) -> Any:
34
+ """Return a JSON-compatible copy without trusting arbitrary evidence objects."""
35
+ if value is None or isinstance(value, (str, bool, int)):
36
+ return value
37
+ if isinstance(value, float):
38
+ return value if math.isfinite(value) else None
39
+ if _depth >= 8:
40
+ return _safe_text(value)
41
+
42
+ seen = _seen if _seen is not None else set()
43
+ identity = id(value)
44
+ if identity in seen:
45
+ return "<recursive>"
46
+
47
+ if isinstance(value, Mapping):
48
+ seen.add(identity)
49
+ try:
50
+ return {
51
+ _safe_text(key): _json_safe(item, _seen=seen, _depth=_depth + 1)
52
+ for key, item in value.items()
53
+ }
54
+ except Exception:
55
+ return _safe_text(value)
56
+ finally:
57
+ seen.discard(identity)
58
+ if isinstance(value, (list, tuple)):
59
+ seen.add(identity)
60
+ try:
61
+ return [
62
+ _json_safe(item, _seen=seen, _depth=_depth + 1)
63
+ for item in value
64
+ ]
65
+ except Exception:
66
+ return _safe_text(value)
67
+ finally:
68
+ seen.discard(identity)
69
+ if isinstance(value, (set, frozenset)):
70
+ seen.add(identity)
71
+ try:
72
+ items = [_json_safe(item, _seen=seen, _depth=_depth + 1) for item in value]
73
+ return sorted(items, key=_safe_text)
74
+ except Exception:
75
+ return _safe_text(value)
76
+ finally:
77
+ seen.discard(identity)
78
+ return _safe_text(value)
79
+
80
+
81
+ def _evidence_text(value: Any) -> str:
82
+ safe = _json_safe(value)
83
+ if isinstance(safe, (dict, list)):
84
+ try:
85
+ return json.dumps(safe, ensure_ascii=False, sort_keys=True)
86
+ except (TypeError, ValueError):
87
+ return _safe_text(safe)
88
+ return _safe_text(safe)
89
+
90
+
91
+ def _has_structured_evidence(value: Any) -> bool:
92
+ if value is None:
93
+ return False
94
+ if isinstance(value, (str, bytes, Mapping, list, tuple, set, frozenset)):
95
+ return bool(value)
96
+ return True
97
+
98
+
99
+ def _materialize(values: Iterable[Any] | None, *, label: str) -> tuple[list[Any], str | None]:
100
+ if values is None:
101
+ return [], f"{label} must be an iterable, not null"
102
+ if isinstance(values, (str, bytes, Mapping)):
103
+ return [], f"{label} must be an iterable of mappings"
104
+ try:
105
+ return list(values), None
106
+ except Exception as exc:
107
+ return [], f"could not read {label}: {_safe_text(exc)}"
108
+
109
+
110
+ def _normalize_check(raw: Any, position: int) -> tuple[dict[str, Any], list[str]]:
111
+ errors: list[str] = []
112
+ if not isinstance(raw, Mapping):
113
+ return (
114
+ {
115
+ "name": f"invalid-gate-{position + 1}",
116
+ "status": "error",
117
+ "critical": True,
118
+ "evidence": f"gate definition is {type(raw).__name__}, not a mapping",
119
+ "recommendation": "Provide a mapping with name, status, and evidence.",
120
+ },
121
+ [f"gate {position + 1} is not a mapping"],
122
+ )
123
+
124
+ name = _safe_text(raw.get("name")).strip()
125
+ if not name:
126
+ name = f"unnamed-gate-{position + 1}"
127
+ errors.append(f"gate {position + 1} is missing a name")
128
+
129
+ raw_status = raw.get("status")
130
+ status = _safe_text(raw_status).strip().lower()
131
+ if status not in VALID_STATUSES:
132
+ errors.append(f"gate {name!r} has invalid status {status or '<missing>'!r}")
133
+ status = "error"
134
+
135
+ raw_critical = raw.get("critical", False)
136
+ if isinstance(raw_critical, bool):
137
+ critical = raw_critical
138
+ else:
139
+ errors.append(f"gate {name!r} critical must be a boolean")
140
+ critical = True
141
+ status = "error"
142
+
143
+ evidence = _evidence_text(raw.get("evidence", "")).strip()
144
+ recommendation = _evidence_text(raw.get("recommendation", "")).strip()
145
+ metrics_present = "metrics" in raw
146
+ metrics = _json_safe(raw.get("metrics")) if metrics_present else None
147
+
148
+ if status in {"pass", "warn"} and not evidence and not _has_structured_evidence(metrics):
149
+ errors.append(f"gate {name!r} cannot be {status} without evidence")
150
+ status = "error"
151
+ critical = True
152
+ recommendation = recommendation or "Attach human-readable evidence or structured metrics."
153
+
154
+ if errors:
155
+ critical = True
156
+
157
+ normalized: dict[str, Any] = {
158
+ "name": name,
159
+ "status": status,
160
+ "critical": critical,
161
+ "evidence": evidence,
162
+ "recommendation": recommendation,
163
+ }
164
+ if metrics_present:
165
+ normalized["metrics"] = metrics
166
+ return normalized, errors
167
+
168
+
169
+ def _normalize_capability(raw: Any, position: int) -> dict[str, Any]:
170
+ if not isinstance(raw, Mapping):
171
+ return {
172
+ "name": f"invalid-capability-{position + 1}",
173
+ "status": "error",
174
+ "evidence": f"capability definition is {type(raw).__name__}, not a mapping",
175
+ "recommendation": "Provide a mapping for this optional capability.",
176
+ "scored": False,
177
+ "points": 0.0,
178
+ "max_points": 0.0,
179
+ }
180
+
181
+ aliases = {
182
+ "ready": "pass",
183
+ "degraded": "warn",
184
+ "disabled": "unavailable",
185
+ "blocked": "fail",
186
+ }
187
+ raw_status = _safe_text(raw.get("status", "unavailable")).strip().lower()
188
+ status = aliases.get(raw_status, raw_status)
189
+ if status not in VALID_STATUSES:
190
+ status = "error"
191
+ normalized: dict[str, Any] = {
192
+ "name": _safe_text(raw.get("name")).strip() or f"capability-{position + 1}",
193
+ "status": status,
194
+ "evidence": _evidence_text(raw.get("evidence", "")).strip(),
195
+ "recommendation": _evidence_text(raw.get("recommendation", "")).strip(),
196
+ "scored": False,
197
+ "points": 0.0,
198
+ "max_points": 0.0,
199
+ }
200
+ if "metrics" in raw:
201
+ normalized["metrics"] = _json_safe(raw.get("metrics"))
202
+ return normalized
203
+
204
+
205
+ def finalize_scorecard(
206
+ checks: Iterable[Mapping[str, Any]] | None,
207
+ capabilities: Iterable[Mapping[str, Any]] | None = (),
208
+ ) -> dict[str, Any]:
209
+ """Finalize exactly ten evidence-backed gates into a JSON-ready scorecard.
210
+
211
+ Optional capabilities are normalized for display but never affect points or
212
+ overall status. Invalid scored-gate schemas fail closed and are returned as
213
+ validation errors instead of raising.
214
+ """
215
+ raw_checks, collection_error = _materialize(checks, label="checks")
216
+ validation_errors = [collection_error] if collection_error else []
217
+ normalized_checks: list[dict[str, Any]] = []
218
+ for position, raw in enumerate(raw_checks):
219
+ normalized, errors = _normalize_check(raw, position)
220
+ normalized_checks.append(normalized)
221
+ validation_errors.extend(errors)
222
+
223
+ if len(normalized_checks) != SCORED_GATE_COUNT:
224
+ validation_errors.append(
225
+ f"expected exactly {SCORED_GATE_COUNT} scored gates, got {len(normalized_checks)}"
226
+ )
227
+ # With the wrong denominator there is no meaningful partial grade.
228
+ # Preserve the definitions for diagnosis but fail every supplied gate
229
+ # closed so the reported score remains the sum of normalized points.
230
+ for check in normalized_checks:
231
+ check["status"] = "error"
232
+ check["critical"] = True
233
+ if not check["recommendation"]:
234
+ check["recommendation"] = "Fix the scorecard gate schema before grading."
235
+
236
+ names: dict[str, list[int]] = {}
237
+ for position, check in enumerate(normalized_checks):
238
+ names.setdefault(check["name"].casefold(), []).append(position)
239
+ for positions in names.values():
240
+ if len(positions) < 2:
241
+ continue
242
+ duplicate_name = normalized_checks[positions[0]]["name"]
243
+ validation_errors.append(f"duplicate scored gate name: {duplicate_name!r}")
244
+ for position in positions:
245
+ normalized_checks[position]["status"] = "error"
246
+ normalized_checks[position]["critical"] = True
247
+ if not normalized_checks[position]["recommendation"]:
248
+ normalized_checks[position]["recommendation"] = "Use a unique scored gate name."
249
+
250
+ for check in normalized_checks:
251
+ check["points"] = STATUS_POINTS[check["status"]]
252
+ check["max_points"] = 1.0
253
+ check["scored"] = True
254
+
255
+ raw_capabilities, capability_error = _materialize(capabilities, label="capabilities")
256
+ normalized_capabilities = [
257
+ _normalize_capability(raw, position)
258
+ for position, raw in enumerate(raw_capabilities)
259
+ ]
260
+ if capability_error:
261
+ normalized_capabilities.append(
262
+ _normalize_capability(
263
+ {
264
+ "name": "capability-schema",
265
+ "status": "error",
266
+ "evidence": capability_error,
267
+ },
268
+ len(normalized_capabilities),
269
+ )
270
+ )
271
+
272
+ score = float(sum(float(check["points"]) for check in normalized_checks))
273
+ critical_failure = any(
274
+ check["critical"] and check["status"] in {"fail", "error"}
275
+ for check in normalized_checks
276
+ )
277
+ all_pass = (
278
+ not validation_errors
279
+ and len(normalized_checks) == SCORED_GATE_COUNT
280
+ and all(check["status"] == "pass" for check in normalized_checks)
281
+ )
282
+ if all_pass and score == MAX_SCORE:
283
+ overall_status = "ready"
284
+ elif validation_errors or critical_failure:
285
+ overall_status = "blocked"
286
+ else:
287
+ overall_status = "degraded"
288
+
289
+ return {
290
+ "schema_version": SCHEMA_VERSION,
291
+ "score": score,
292
+ "max_score": MAX_SCORE,
293
+ "overall_status": overall_status,
294
+ "scored_gate_count": len(normalized_checks),
295
+ "checks": normalized_checks,
296
+ "capabilities": normalized_capabilities,
297
+ "validation_errors": validation_errors,
298
+ }
299
+
300
+
301
+ __all__ = [
302
+ "MAX_SCORE",
303
+ "SCHEMA_VERSION",
304
+ "SCORED_GATE_COUNT",
305
+ "STATUS_POINTS",
306
+ "VALID_STATUSES",
307
+ "finalize_scorecard",
308
+ ]
@@ -0,0 +1,84 @@
1
+ """Session distribution analysis (I11).
2
+
3
+ Fable-5 traces are heavy-tailed: the top five sessions carried roughly a third
4
+ of the corpus. This module makes that bias visible for harness stats and
5
+ session-summary reporting.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class SessionDistribution:
14
+ session_count: int
15
+ total_rows: int
16
+ largest_session: int
17
+ median_session: float
18
+ top_sessions: tuple[tuple[str, int], ...]
19
+ heavy_tail: bool
20
+ effective_top_n: int
21
+ summary: str
22
+
23
+ def top_n_share(self, n: int) -> float:
24
+ if self.total_rows <= 0 or n <= 0:
25
+ return 0.0
26
+ return sum(count for _name, count in self.top_sessions[:n]) / self.total_rows
27
+
28
+ def to_dict(self) -> dict:
29
+ return {
30
+ "session_count": self.session_count,
31
+ "total_rows": self.total_rows,
32
+ "largest_session": self.largest_session,
33
+ "median_session": self.median_session,
34
+ "top_sessions": list(self.top_sessions),
35
+ "heavy_tail": self.heavy_tail,
36
+ "effective_top_n": self.effective_top_n,
37
+ "top_share": round(self.top_n_share(self.effective_top_n), 3),
38
+ "top5_share": round(self.top_n_share(5), 3),
39
+ "summary": self.summary,
40
+ }
41
+
42
+
43
+ def _median(values: list[int]) -> float:
44
+ if not values:
45
+ return 0.0
46
+ ordered = sorted(values)
47
+ mid = len(ordered) // 2
48
+ if len(ordered) % 2:
49
+ return float(ordered[mid])
50
+ return (ordered[mid - 1] + ordered[mid]) / 2
51
+
52
+
53
+ def summarize_session_distribution(session_counts: dict[str, int], *, heavy_tail_top_n: int = 5, heavy_tail_share: float = 0.33) -> SessionDistribution:
54
+ """Summarize per-session row counts and flag heavy-tail risk.
55
+
56
+ For large corpora this uses the Fable-5 top-5 concentration rule. For very
57
+ small corpora, ``top_n`` is adapted to roughly the largest half of sessions
58
+ so a four-session corpus can still reveal obvious concentration.
59
+ """
60
+ cleaned = {str(name): max(0, int(count)) for name, count in (session_counts or {}).items()}
61
+ sorted_counts = tuple(sorted(cleaned.items(), key=lambda item: item[1], reverse=True))
62
+ counts = [count for _name, count in sorted_counts]
63
+ total = sum(counts)
64
+ effective_top_n = min(max(1, heavy_tail_top_n), max(1, len(sorted_counts) // 2)) if sorted_counts else 0
65
+ top_share = 0.0 if total <= 0 or effective_top_n <= 0 else sum(count for _name, count in sorted_counts[:effective_top_n]) / total
66
+ heavy = len(sorted_counts) > 1 and top_share >= heavy_tail_share
67
+ summary = (
68
+ f"sessions={len(sorted_counts)}, rows={total}, largest={counts[0] if counts else 0}, "
69
+ f"median={_median(counts):.1f}, top{effective_top_n}_share={top_share:.2f}, "
70
+ f"heavy_tail={heavy}"
71
+ )
72
+ return SessionDistribution(
73
+ session_count=len(sorted_counts),
74
+ total_rows=total,
75
+ largest_session=counts[0] if counts else 0,
76
+ median_session=_median(counts),
77
+ top_sessions=sorted_counts,
78
+ heavy_tail=heavy,
79
+ effective_top_n=effective_top_n,
80
+ summary=summary,
81
+ )
82
+
83
+
84
+ __all__ = ["SessionDistribution", "summarize_session_distribution"]