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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- 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"]
|