agent-lifecycle-kit 1.50.1__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.
- agent_lifecycle/__init__.py +5 -0
- agent_lifecycle/__main__.py +5 -0
- agent_lifecycle/_version.py +3 -0
- agent_lifecycle/adapter_sessions/__init__.py +41 -0
- agent_lifecycle/adapter_sessions/contracts.py +133 -0
- agent_lifecycle/adapter_sessions/env.py +44 -0
- agent_lifecycle/adapter_sessions/launcher.py +101 -0
- agent_lifecycle/adapter_sessions/process.py +39 -0
- agent_lifecycle/adapter_sessions/redaction.py +30 -0
- agent_lifecycle/adapter_sessions/session_store.py +64 -0
- agent_lifecycle/adapter_sessions/task_intake.py +450 -0
- agent_lifecycle/adapter_sessions/workflow_bridge.py +154 -0
- agent_lifecycle/audit/__init__.py +67 -0
- agent_lifecycle/audit/bug_forensics.py +94 -0
- agent_lifecycle/audit/implementation.py +516 -0
- agent_lifecycle/audit/ownership.py +119 -0
- agent_lifecycle/audit/proof_integrity.py +814 -0
- agent_lifecycle/audit/review_verdict.py +116 -0
- agent_lifecycle/changesets/__init__.py +5 -0
- agent_lifecycle/changesets/git.py +35 -0
- agent_lifecycle/cli/__init__.py +5 -0
- agent_lifecycle/cli/adapter.py +360 -0
- agent_lifecycle/cli/dispatch.py +1220 -0
- agent_lifecycle/cli/followup.py +89 -0
- agent_lifecycle/cli/main.py +41 -0
- agent_lifecycle/cli/metrics_parser.py +49 -0
- agent_lifecycle/cli/parsers.py +618 -0
- agent_lifecycle/cli/policy.py +101 -0
- agent_lifecycle/cli/progress_hooks.py +117 -0
- agent_lifecycle/cli/worktree.py +74 -0
- agent_lifecycle/compiler/__init__.py +17 -0
- agent_lifecycle/compiler/small_model_packets.py +322 -0
- agent_lifecycle/compiler/task_packets.py +229 -0
- agent_lifecycle/context/__init__.py +20 -0
- agent_lifecycle/context/episode_retrieval.py +30 -0
- agent_lifecycle/context/external_memory.py +71 -0
- agent_lifecycle/context/profiles.py +104 -0
- agent_lifecycle/context/rendering.py +226 -0
- agent_lifecycle/contracts/__init__.py +25 -0
- agent_lifecycle/contracts/adapter_contract_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_event_schemas.py +118 -0
- agent_lifecycle/contracts/adapter_session_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_task_schemas.py +90 -0
- agent_lifecycle/contracts/audit_schemas.py +133 -0
- agent_lifecycle/contracts/bug_forensics_schemas.py +400 -0
- agent_lifecycle/contracts/canonical.py +55 -0
- agent_lifecycle/contracts/compatibility.py +242 -0
- agent_lifecycle/contracts/context_model_schemas.py +265 -0
- agent_lifecycle/contracts/core_schemas.py +252 -0
- agent_lifecycle/contracts/cross_check_schemas.py +121 -0
- agent_lifecycle/contracts/errors.py +26 -0
- agent_lifecycle/contracts/evidence_import_schemas.py +152 -0
- agent_lifecycle/contracts/host_capability_schemas.py +64 -0
- agent_lifecycle/contracts/import_dialect_schemas.py +166 -0
- agent_lifecycle/contracts/metric_schemas.py +315 -0
- agent_lifecycle/contracts/paths.py +31 -0
- agent_lifecycle/contracts/plan_contract_schemas.py +148 -0
- agent_lifecycle/contracts/policy_schemas.py +350 -0
- agent_lifecycle/contracts/progress_bridge_schemas.py +114 -0
- agent_lifecycle/contracts/progress_hook_schemas.py +119 -0
- agent_lifecycle/contracts/proof_integrity_schemas.py +240 -0
- agent_lifecycle/contracts/release_contract_schemas.py +476 -0
- agent_lifecycle/contracts/review_mesh_recommendation_schemas.py +64 -0
- agent_lifecycle/contracts/review_mesh_schemas.py +222 -0
- agent_lifecycle/contracts/review_quality_schemas.py +109 -0
- agent_lifecycle/contracts/runner_schemas.py +91 -0
- agent_lifecycle/contracts/runner_worktree_schemas.py +389 -0
- agent_lifecycle/contracts/sandbox_schemas.py +159 -0
- agent_lifecycle/contracts/schema_builders.py +23 -0
- agent_lifecycle/contracts/schemas.py +93 -0
- agent_lifecycle/contracts/status_goal_schemas.py +469 -0
- agent_lifecycle/contracts/task_template_schemas.py +96 -0
- agent_lifecycle/contracts/usage_export_schemas.py +68 -0
- agent_lifecycle/diagnostics/__init__.py +17 -0
- agent_lifecycle/diagnostics/bundles.py +124 -0
- agent_lifecycle/diagnostics/readiness.py +733 -0
- agent_lifecycle/evidence_index/__init__.py +39 -0
- agent_lifecycle/evidence_index/core.py +272 -0
- agent_lifecycle/evidence_index/episode_index.py +329 -0
- agent_lifecycle/evidence_index/external_context.py +276 -0
- agent_lifecycle/followup/__init__.py +23 -0
- agent_lifecycle/followup/records.py +404 -0
- agent_lifecycle/freeze/__init__.py +5 -0
- agent_lifecycle/freeze/locks.py +23 -0
- agent_lifecycle/goal/__init__.py +15 -0
- agent_lifecycle/goal/records.py +244 -0
- agent_lifecycle/goal/view.py +109 -0
- agent_lifecycle/host_protocol/__init__.py +65 -0
- agent_lifecycle/host_protocol/acp_capability.py +195 -0
- agent_lifecycle/host_protocol/capabilities.py +268 -0
- agent_lifecycle/host_protocol/contracts.py +214 -0
- agent_lifecycle/host_protocol/event_capture.py +390 -0
- agent_lifecycle/host_protocol/events.py +196 -0
- agent_lifecycle/host_protocol/inspection.py +194 -0
- agent_lifecycle/host_protocol/inspection_common.py +310 -0
- agent_lifecycle/host_protocol/inspection_cursor.py +164 -0
- agent_lifecycle/host_protocol/inspection_gemini_cli.py +163 -0
- agent_lifecycle/host_protocol/inspection_hermes.py +161 -0
- agent_lifecycle/host_protocol/inspection_kimi_code.py +169 -0
- agent_lifecycle/host_protocol/inspection_opencode.py +147 -0
- agent_lifecycle/host_protocol/inspection_qwen_code.py +143 -0
- agent_lifecycle/host_protocol/receipts.py +58 -0
- agent_lifecycle/host_protocol/scaffold.py +329 -0
- agent_lifecycle/host_protocol/validation.py +229 -0
- agent_lifecycle/imports/__init__.py +76 -0
- agent_lifecycle/imports/agentskills_profile.py +54 -0
- agent_lifecycle/imports/bmad_profile.py +49 -0
- agent_lifecycle/imports/constitution_adr.py +91 -0
- agent_lifecycle/imports/dialect_profiles.py +121 -0
- agent_lifecycle/imports/external_agent.py +28 -0
- agent_lifecycle/imports/external_dialects.py +486 -0
- agent_lifecycle/imports/external_workflow.py +28 -0
- agent_lifecycle/imports/multi_markdown.py +111 -0
- agent_lifecycle/imports/openspec_profile.py +49 -0
- agent_lifecycle/imports/planning.py +536 -0
- agent_lifecycle/imports/spec_kit_profile.py +49 -0
- agent_lifecycle/imports/spec_kitty_profile.py +49 -0
- agent_lifecycle/metrics/__init__.py +60 -0
- agent_lifecycle/metrics/cost_collection.py +315 -0
- agent_lifecycle/metrics/costs.py +203 -0
- agent_lifecycle/metrics/outcome_index.py +330 -0
- agent_lifecycle/metrics/phase_resources.py +208 -0
- agent_lifecycle/metrics/recommendations.py +507 -0
- agent_lifecycle/metrics/regression_signals.py +64 -0
- agent_lifecycle/metrics/usage_export.py +390 -0
- agent_lifecycle/model_routing/__init__.py +19 -0
- agent_lifecycle/model_routing/profiles.py +323 -0
- agent_lifecycle/model_routing/receipts.py +140 -0
- agent_lifecycle/model_routing/resolver.py +566 -0
- agent_lifecycle/neutrality/__init__.py +12 -0
- agent_lifecycle/neutrality/__main__.py +6 -0
- agent_lifecycle/neutrality/authority.py +265 -0
- agent_lifecycle/neutrality/canonical.py +28 -0
- agent_lifecycle/neutrality/cli.py +294 -0
- agent_lifecycle/neutrality/ed25519.py +124 -0
- agent_lifecycle/neutrality/errors.py +89 -0
- agent_lifecycle/neutrality/gate.py +269 -0
- agent_lifecycle/neutrality/paths.py +92 -0
- agent_lifecycle/neutrality/policy.py +76 -0
- agent_lifecycle/neutrality/receipt.py +135 -0
- agent_lifecycle/neutrality/scanner.py +310 -0
- agent_lifecycle/neutrality/test_runner.py +116 -0
- agent_lifecycle/planning/__init__.py +47 -0
- agent_lifecycle/planning/acceptance_markdown.py +159 -0
- agent_lifecycle/planning/completeness.py +362 -0
- agent_lifecycle/planning/continuity.py +255 -0
- agent_lifecycle/planning/templates.py +294 -0
- agent_lifecycle/planning/tiering.py +87 -0
- agent_lifecycle/planning/validation.py +72 -0
- agent_lifecycle/policy/__init__.py +37 -0
- agent_lifecycle/policy/adaptive_lifecycle.py +416 -0
- agent_lifecycle/policy/apply.py +44 -0
- agent_lifecycle/policy/proposals.py +189 -0
- agent_lifecycle/policy/quality_floor.py +144 -0
- agent_lifecycle/policy/runtime_receipts.py +160 -0
- agent_lifecycle/py.typed +1 -0
- agent_lifecycle/quality/__init__.py +89 -0
- agent_lifecycle/quality/bug_forensics.py +727 -0
- agent_lifecycle/quality/bug_forensics_advisor.py +88 -0
- agent_lifecycle/quality/bug_forensics_recipes.py +239 -0
- agent_lifecycle/quality/cross_check.py +437 -0
- agent_lifecycle/quality/failure_classification.py +318 -0
- agent_lifecycle/quality/packs.py +265 -0
- agent_lifecycle/reporting/__init__.py +37 -0
- agent_lifecycle/reporting/change_summary.py +156 -0
- agent_lifecycle/reporting/event_feed.py +101 -0
- agent_lifecycle/reporting/progress_bridge.py +153 -0
- agent_lifecycle/reporting/progress_hooks.py +161 -0
- agent_lifecycle/reporting/progress_terminal.py +95 -0
- agent_lifecycle/reporting/progress_view.py +284 -0
- agent_lifecycle/reporting/status_view.py +125 -0
- agent_lifecycle/reporting/usage_export.py +59 -0
- agent_lifecycle/review/__init__.py +5 -0
- agent_lifecycle/review/validation.py +29 -0
- agent_lifecycle/review_mesh/__init__.py +83 -0
- agent_lifecycle/review_mesh/assignments.py +160 -0
- agent_lifecycle/review_mesh/contracts.py +685 -0
- agent_lifecycle/review_mesh/operator_templates.py +320 -0
- agent_lifecycle/review_mesh/quorum.py +38 -0
- agent_lifecycle/review_mesh/recommendation.py +487 -0
- agent_lifecycle/review_mesh/results.py +98 -0
- agent_lifecycle/review_mesh/synthesis.py +102 -0
- agent_lifecycle/runner/__init__.py +47 -0
- agent_lifecycle/runner/attempt_snapshots.py +244 -0
- agent_lifecycle/runner/core.py +653 -0
- agent_lifecycle/runner/sandbox_receipts.py +628 -0
- agent_lifecycle/specification/__init__.py +23 -0
- agent_lifecycle/specification/completion_check.py +135 -0
- agent_lifecycle/specification/completion_gate.py +501 -0
- agent_lifecycle/specification/completion_signal.py +75 -0
- agent_lifecycle/specification/validation.py +48 -0
- agent_lifecycle/workflow/__init__.py +79 -0
- agent_lifecycle/workflow/artifacts.py +74 -0
- agent_lifecycle/workflow/budget_decisions.py +370 -0
- agent_lifecycle/workflow/budget_policy.py +140 -0
- agent_lifecycle/workflow/budget_receipts.py +70 -0
- agent_lifecycle/workflow/bug_forensics_gates.py +284 -0
- agent_lifecycle/workflow/controller.py +45 -0
- agent_lifecycle/workflow/events.py +50 -0
- agent_lifecycle/workflow/final_proof_integrity.py +74 -0
- agent_lifecycle/workflow/finalization.py +422 -0
- agent_lifecycle/workflow/gates.py +185 -0
- agent_lifecycle/workflow/implementation_audit_gate.py +126 -0
- agent_lifecycle/workflow/leases.py +220 -0
- agent_lifecycle/workflow/lineage.py +108 -0
- agent_lifecycle/workflow/managed_runner.py +167 -0
- agent_lifecycle/workflow/model_usage.py +106 -0
- agent_lifecycle/workflow/next_action.py +86 -0
- agent_lifecycle/workflow/operation_kernel.py +98 -0
- agent_lifecycle/workflow/plan_adoption.py +439 -0
- agent_lifecycle/workflow/query.py +52 -0
- agent_lifecycle/workflow/review_mesh_gate.py +93 -0
- agent_lifecycle/workflow/reviews.py +168 -0
- agent_lifecycle/workflow/run_transitions.py +163 -0
- agent_lifecycle/workflow/sandbox_policy.py +214 -0
- agent_lifecycle/workflow/selectors.py +57 -0
- agent_lifecycle/workflow/state.py +130 -0
- agent_lifecycle/workflow/task_transitions.py +421 -0
- agent_lifecycle/worktree/__init__.py +17 -0
- agent_lifecycle/worktree/isolation.py +407 -0
- agent_lifecycle_kit-1.50.1.dist-info/METADATA +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/RECORD +227 -0
- agent_lifecycle_kit-1.50.1.dist-info/WHEEL +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/entry_points.txt +3 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/LICENSE +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/NOTICE +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""Advisory lifecycle mode recommendations from cost reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_lifecycle.contracts import LifecycleError, canonical_digest
|
|
8
|
+
from agent_lifecycle.metrics.outcome_index import QUALITY_COST_SIGNALS_SCHEMA
|
|
9
|
+
from agent_lifecycle.metrics.costs import (
|
|
10
|
+
COST_CATEGORIES,
|
|
11
|
+
DEFAULT_MODE_LIMITS,
|
|
12
|
+
cost_ratios,
|
|
13
|
+
summarize_usage_confidence,
|
|
14
|
+
validate_lifecycle_cost_report,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
BASELINES_SCHEMA = "agent-lifecycle-baselines.v1"
|
|
18
|
+
BASELINE_VALIDATION_SCHEMA = "agent-lifecycle-baselines-validation.v1"
|
|
19
|
+
STATISTICS_SCHEMA = "agent-lifecycle-overhead-statistics.v1"
|
|
20
|
+
RECOMMENDATION_SCHEMA = "agent-lifecycle-recommendation.v1"
|
|
21
|
+
RECOMMENDATION_SUMMARY_SCHEMA = "agent-lifecycle-recommendation-summary.v1"
|
|
22
|
+
MODE_ORDER = tuple(DEFAULT_MODE_LIMITS)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_lifecycle_baselines(profile: dict[str, Any]) -> dict[str, Any]:
|
|
26
|
+
blockers: list[dict[str, Any]] = []
|
|
27
|
+
if profile.get("schemaVersion") != BASELINES_SCHEMA:
|
|
28
|
+
blockers.append({"code": "baseline-schema", "message": "unsupported lifecycle baseline schemaVersion"})
|
|
29
|
+
if profile.get("productionPromotionClaimed") is not False:
|
|
30
|
+
blockers.append({"code": "baseline-production-claim", "message": "baseline profile must not claim production promotion"})
|
|
31
|
+
minimum_reports = profile.get("minimumReportsForConfidence")
|
|
32
|
+
if not isinstance(minimum_reports, int) or isinstance(minimum_reports, bool) or minimum_reports < 1:
|
|
33
|
+
blockers.append({"code": "baseline-minimum-reports", "message": "minimumReportsForConfidence must be a positive integer"})
|
|
34
|
+
mode_order = profile.get("modeOrder")
|
|
35
|
+
if "modeOrder" in profile and (not isinstance(mode_order, list) or tuple(mode_order) != MODE_ORDER):
|
|
36
|
+
blockers.append({"code": "baseline-mode-order", "message": "modeOrder must match lifecycle modes"})
|
|
37
|
+
risk_floors = profile.get("riskFloors")
|
|
38
|
+
if not isinstance(risk_floors, dict):
|
|
39
|
+
blockers.append({"code": "baseline-risk-floors", "message": "riskFloors must be an object"})
|
|
40
|
+
risk_floors = {}
|
|
41
|
+
for risk, mode in risk_floors.items():
|
|
42
|
+
if not isinstance(risk, str) or mode not in MODE_ORDER:
|
|
43
|
+
blockers.append({"code": "baseline-risk-floor-mode", "risk": risk, "mode": mode})
|
|
44
|
+
task_shapes = profile.get("taskShapes")
|
|
45
|
+
if not isinstance(task_shapes, dict) or not task_shapes:
|
|
46
|
+
blockers.append({"code": "baseline-task-shapes", "message": "taskShapes must be a non-empty object"})
|
|
47
|
+
task_shapes = {}
|
|
48
|
+
for shape, config in task_shapes.items():
|
|
49
|
+
_validate_shape(shape, config, blockers)
|
|
50
|
+
body = {
|
|
51
|
+
"schemaVersion": BASELINE_VALIDATION_SCHEMA,
|
|
52
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
53
|
+
"taskShapeCount": len(task_shapes),
|
|
54
|
+
"blockers": blockers,
|
|
55
|
+
"profileDigest": canonical_digest(profile),
|
|
56
|
+
}
|
|
57
|
+
return {**body, "validationDigest": canonical_digest(body)}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def require_lifecycle_recommendation_pass(recommendation: dict[str, Any]) -> dict[str, Any]:
|
|
61
|
+
if recommendation.get("status") == "FAIL":
|
|
62
|
+
raise LifecycleError(
|
|
63
|
+
"lifecycle-recommendation-failed",
|
|
64
|
+
"lifecycle recommendation failed",
|
|
65
|
+
{"recommendation": recommendation},
|
|
66
|
+
)
|
|
67
|
+
return recommendation
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def summarize_lifecycle_overhead(reports: list[dict[str, Any]]) -> dict[str, Any]:
|
|
71
|
+
totals = _empty_totals()
|
|
72
|
+
usage_entries: list[dict[str, Any]] = []
|
|
73
|
+
report_digests: list[str] = []
|
|
74
|
+
blockers: list[dict[str, Any]] = []
|
|
75
|
+
for index, report in enumerate(reports):
|
|
76
|
+
validation = validate_lifecycle_cost_report(report)
|
|
77
|
+
if validation["status"] != "PASS":
|
|
78
|
+
blockers.append({"code": "cost-report-invalid", "index": index, "validation": validation})
|
|
79
|
+
report_digests.append(validation["reportDigest"])
|
|
80
|
+
for category in COST_CATEGORIES:
|
|
81
|
+
totals[category]["tokens"] += validation["totals"][category]["tokens"]
|
|
82
|
+
totals[category]["steps"] += validation["totals"][category]["steps"]
|
|
83
|
+
entries = report.get("entries")
|
|
84
|
+
if isinstance(entries, list):
|
|
85
|
+
usage_entries.extend(item for item in entries if isinstance(item, dict))
|
|
86
|
+
totals["overall"] = {
|
|
87
|
+
"tokens": sum(totals[category]["tokens"] for category in COST_CATEGORIES),
|
|
88
|
+
"steps": sum(totals[category]["steps"] for category in COST_CATEGORIES),
|
|
89
|
+
}
|
|
90
|
+
ratios = cost_ratios(totals)
|
|
91
|
+
body = {
|
|
92
|
+
"schemaVersion": STATISTICS_SCHEMA,
|
|
93
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
94
|
+
"reportCount": len(reports),
|
|
95
|
+
"reportDigests": sorted(report_digests),
|
|
96
|
+
"totals": totals,
|
|
97
|
+
"ratios": ratios,
|
|
98
|
+
"usageConfidence": summarize_usage_confidence(usage_entries),
|
|
99
|
+
"averageProductValidationSteps": _average(totals["productValidation"]["steps"], len(reports)),
|
|
100
|
+
"blockers": blockers,
|
|
101
|
+
}
|
|
102
|
+
return {**body, "statisticsDigest": canonical_digest(body)}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def recommend_lifecycle_mode(
|
|
106
|
+
*,
|
|
107
|
+
reports: list[dict[str, Any]],
|
|
108
|
+
baseline_profile: dict[str, Any],
|
|
109
|
+
task_shape: str = "feature",
|
|
110
|
+
current_mode: str | None = None,
|
|
111
|
+
sdd_tier: str | None = None,
|
|
112
|
+
risk_flags: list[str] | None = None,
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
baseline_validation = validate_lifecycle_baselines(baseline_profile)
|
|
115
|
+
stats = summarize_lifecycle_overhead(reports)
|
|
116
|
+
blockers = list(baseline_validation["blockers"])
|
|
117
|
+
if stats["status"] != "PASS":
|
|
118
|
+
blockers.extend(stats["blockers"])
|
|
119
|
+
shape = _task_shape(baseline_profile, task_shape, blockers)
|
|
120
|
+
if baseline_validation["status"] != "PASS" or stats["status"] != "PASS" or shape is None:
|
|
121
|
+
body = _failed_recommendation(task_shape, current_mode, baseline_validation, stats, blockers)
|
|
122
|
+
return {**body, "recommendationDigest": canonical_digest(body)}
|
|
123
|
+
|
|
124
|
+
floor = _quality_floor_mode(
|
|
125
|
+
task_shape=task_shape,
|
|
126
|
+
baseline_profile=baseline_profile,
|
|
127
|
+
sdd_tier=sdd_tier,
|
|
128
|
+
risk_flags=risk_flags or [],
|
|
129
|
+
)
|
|
130
|
+
warnings = _warnings(stats, shape, baseline_profile)
|
|
131
|
+
weak_data = _weak_data(stats, baseline_profile)
|
|
132
|
+
confidence = _confidence(stats, weak_data=weak_data, warning_count=len(warnings))
|
|
133
|
+
recommended = _recommended_mode(shape, floor=floor, current_mode=current_mode, confidence=confidence, warnings=warnings)
|
|
134
|
+
body = {
|
|
135
|
+
"schemaVersion": RECOMMENDATION_SCHEMA,
|
|
136
|
+
"status": "PASS",
|
|
137
|
+
"taskShape": task_shape,
|
|
138
|
+
"currentMode": current_mode,
|
|
139
|
+
"recommendedMode": recommended,
|
|
140
|
+
"confidence": confidence,
|
|
141
|
+
"advisoryOnly": True,
|
|
142
|
+
"autoApply": False,
|
|
143
|
+
"qualityFloor": floor,
|
|
144
|
+
"qualityFloorPreserved": _mode_index(recommended) >= _mode_index(floor),
|
|
145
|
+
"warnings": warnings,
|
|
146
|
+
"reasons": _reasons(confidence, recommended, floor, weak_data, warnings),
|
|
147
|
+
"statistics": stats,
|
|
148
|
+
"baselineValidation": baseline_validation,
|
|
149
|
+
"productionPromotionClaimed": False,
|
|
150
|
+
}
|
|
151
|
+
body["compactSummary"] = build_lifecycle_recommendation_summary(body)
|
|
152
|
+
return {**body, "recommendationDigest": canonical_digest(body)}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def recommend_from_quality_cost_signals(
|
|
156
|
+
*,
|
|
157
|
+
signals: dict[str, Any],
|
|
158
|
+
baseline_profile: dict[str, Any],
|
|
159
|
+
task_shape: str = "feature",
|
|
160
|
+
current_mode: str | None = None,
|
|
161
|
+
sdd_tier: str | None = None,
|
|
162
|
+
risk_flags: list[str] | None = None,
|
|
163
|
+
) -> dict[str, Any]:
|
|
164
|
+
"""Recommend a lifecycle mode from local outcome-index signals."""
|
|
165
|
+
|
|
166
|
+
baseline_validation = validate_lifecycle_baselines(baseline_profile)
|
|
167
|
+
blockers = list(baseline_validation["blockers"])
|
|
168
|
+
if not isinstance(signals, dict) or signals.get("schemaVersion") != QUALITY_COST_SIGNALS_SCHEMA:
|
|
169
|
+
blockers.append({"code": "quality-cost-signals-schema"})
|
|
170
|
+
signal_rows: list[dict[str, Any]] = []
|
|
171
|
+
else:
|
|
172
|
+
signal_rows = [item for item in signals.get("signals", []) if isinstance(item, dict)]
|
|
173
|
+
if signals.get("status") != "PASS":
|
|
174
|
+
blockers.extend(signals.get("blockers", []))
|
|
175
|
+
blockers.extend(_unsafe_quality_cost_signal_blockers(signals))
|
|
176
|
+
shape = _task_shape(baseline_profile, task_shape, blockers)
|
|
177
|
+
if baseline_validation["status"] != "PASS" or shape is None or blockers:
|
|
178
|
+
body = _failed_recommendation(task_shape, current_mode, baseline_validation, {}, blockers)
|
|
179
|
+
body["qualityCostSignals"] = signals if isinstance(signals, dict) else {}
|
|
180
|
+
return {**body, "recommendationDigest": canonical_digest(body)}
|
|
181
|
+
|
|
182
|
+
floor = _quality_floor_mode(
|
|
183
|
+
task_shape=task_shape,
|
|
184
|
+
baseline_profile=baseline_profile,
|
|
185
|
+
sdd_tier=sdd_tier,
|
|
186
|
+
risk_flags=risk_flags or [],
|
|
187
|
+
)
|
|
188
|
+
relevant = [item for item in signal_rows if item.get("taskShape") == task_shape]
|
|
189
|
+
best = _best_quality_cost_signal(relevant)
|
|
190
|
+
warnings = _quality_cost_warnings(best, signals=signals, profile=baseline_profile)
|
|
191
|
+
confidence = _quality_cost_confidence(best, baseline_profile, warning_count=len(warnings))
|
|
192
|
+
current = current_mode if current_mode in MODE_ORDER else None
|
|
193
|
+
candidate = str(best.get("lifecycleMode")) if best and best.get("lifecycleMode") in MODE_ORDER else str(shape["defaultMode"])
|
|
194
|
+
if confidence == "LOW":
|
|
195
|
+
candidate = current or str(shape["defaultMode"])
|
|
196
|
+
recommended = _max_mode(candidate, floor)
|
|
197
|
+
body = {
|
|
198
|
+
"schemaVersion": RECOMMENDATION_SCHEMA,
|
|
199
|
+
"status": "PASS",
|
|
200
|
+
"taskShape": task_shape,
|
|
201
|
+
"currentMode": current_mode,
|
|
202
|
+
"recommendedMode": recommended,
|
|
203
|
+
"confidence": confidence,
|
|
204
|
+
"advisoryOnly": True,
|
|
205
|
+
"autoApply": False,
|
|
206
|
+
"qualityFloor": floor,
|
|
207
|
+
"qualityFloorPreserved": _mode_index(recommended) >= _mode_index(floor),
|
|
208
|
+
"warnings": warnings,
|
|
209
|
+
"reasons": _quality_cost_reasons(confidence, recommended, floor, best, warnings),
|
|
210
|
+
"statistics": {
|
|
211
|
+
"schemaVersion": "agent-quality-cost-learning-statistics.v1",
|
|
212
|
+
"status": signals.get("status"),
|
|
213
|
+
"outcomeIndexDigest": signals.get("outcomeIndexDigest"),
|
|
214
|
+
"signalsDigest": signals.get("signalsDigest") or canonical_digest(signals),
|
|
215
|
+
"groupCount": signals.get("groupCount", 0),
|
|
216
|
+
"taskCount": signals.get("taskCount", 0),
|
|
217
|
+
"selectedSignal": best,
|
|
218
|
+
},
|
|
219
|
+
"baselineValidation": baseline_validation,
|
|
220
|
+
"qualityCostSignals": {
|
|
221
|
+
"signalsDigest": signals.get("signalsDigest") or canonical_digest(signals),
|
|
222
|
+
"outcomeIndexDigest": signals.get("outcomeIndexDigest"),
|
|
223
|
+
"selectedSignal": best,
|
|
224
|
+
},
|
|
225
|
+
"rollback": {
|
|
226
|
+
"strategy": "discard recommendation and keep prior lifecycle routing policy",
|
|
227
|
+
"restore": [{"path": f"taskShapes.{task_shape}.defaultMode", "value": current_mode}],
|
|
228
|
+
"requiresReview": True,
|
|
229
|
+
},
|
|
230
|
+
"productionPromotionClaimed": False,
|
|
231
|
+
}
|
|
232
|
+
body["compactSummary"] = build_lifecycle_recommendation_summary(body)
|
|
233
|
+
return {**body, "recommendationDigest": canonical_digest(body)}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def build_lifecycle_recommendation_summary(recommendation: dict[str, Any]) -> dict[str, Any]:
|
|
237
|
+
stats = recommendation.get("statistics") if isinstance(recommendation.get("statistics"), dict) else {}
|
|
238
|
+
ratios = stats.get("ratios") if isinstance(stats.get("ratios"), dict) else {}
|
|
239
|
+
warnings = recommendation.get("warnings") if isinstance(recommendation.get("warnings"), list) else []
|
|
240
|
+
return {
|
|
241
|
+
"schemaVersion": RECOMMENDATION_SUMMARY_SCHEMA,
|
|
242
|
+
"latestUserIntent": "Choose the lightest lifecycle mode that preserves the required quality floor.",
|
|
243
|
+
"activeDecisions": [
|
|
244
|
+
f"taskShape={recommendation.get('taskShape')}",
|
|
245
|
+
f"recommendedMode={recommendation.get('recommendedMode')}",
|
|
246
|
+
f"confidence={recommendation.get('confidence')}",
|
|
247
|
+
f"pipelineTokenShare={ratios.get('pipelineTokenShare', 0.0)}",
|
|
248
|
+
],
|
|
249
|
+
"openBlockers": [],
|
|
250
|
+
"acceptedEvidence": [
|
|
251
|
+
{
|
|
252
|
+
"id": "lifecycle-recommendation",
|
|
253
|
+
"status": recommendation.get("status"),
|
|
254
|
+
"warningCount": len(warnings),
|
|
255
|
+
}
|
|
256
|
+
],
|
|
257
|
+
"changedFiles": [],
|
|
258
|
+
"nextRequiredAction": "review advisory recommendation before changing lifecycle mode",
|
|
259
|
+
"doNotDo": [
|
|
260
|
+
"Do not auto-apply recommendations.",
|
|
261
|
+
"Do not lower required validation for high-risk task classes.",
|
|
262
|
+
],
|
|
263
|
+
"recommendedMode": recommendation.get("recommendedMode"),
|
|
264
|
+
"confidence": recommendation.get("confidence"),
|
|
265
|
+
"warnings": warnings[:8],
|
|
266
|
+
"qualityFloorPreserved": recommendation.get("qualityFloorPreserved") is True,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _validate_shape(shape: str, config: Any, blockers: list[dict[str, Any]]) -> None:
|
|
271
|
+
if not isinstance(config, dict):
|
|
272
|
+
blockers.append({"code": "baseline-shape-config", "taskShape": shape})
|
|
273
|
+
return
|
|
274
|
+
for key in ("defaultMode", "minMode"):
|
|
275
|
+
if config.get(key) not in MODE_ORDER:
|
|
276
|
+
blockers.append({"code": "baseline-shape-mode", "taskShape": shape, "field": key})
|
|
277
|
+
for key in ("overheadWarningShare", "coordinationWarningShare"):
|
|
278
|
+
value = config.get(key)
|
|
279
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool) or value <= 0 or value > 1:
|
|
280
|
+
blockers.append({"code": "baseline-shape-share", "taskShape": shape, "field": key})
|
|
281
|
+
threshold = config.get("reviewStepWarningThreshold")
|
|
282
|
+
if not isinstance(threshold, int) or isinstance(threshold, bool) or threshold < 0:
|
|
283
|
+
blockers.append({"code": "baseline-shape-review-threshold", "taskShape": shape})
|
|
284
|
+
if _mode_index(str(config.get("defaultMode"))) < _mode_index(str(config.get("minMode"))):
|
|
285
|
+
blockers.append({"code": "baseline-shape-default-below-min", "taskShape": shape})
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _empty_totals() -> dict[str, dict[str, int]]:
|
|
289
|
+
return {category: {"tokens": 0, "steps": 0} for category in COST_CATEGORIES}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _average(value: int, count: int) -> float:
|
|
293
|
+
return round(value / count, 6) if count else 0.0
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _task_shape(profile: dict[str, Any], task_shape: str, blockers: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
297
|
+
shapes = profile.get("taskShapes")
|
|
298
|
+
if not isinstance(shapes, dict) or not isinstance(shapes.get(task_shape), dict):
|
|
299
|
+
blockers.append({"code": "baseline-task-shape-missing", "taskShape": task_shape})
|
|
300
|
+
return None
|
|
301
|
+
return shapes[task_shape]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _warnings(stats: dict[str, Any], shape: dict[str, Any], profile: dict[str, Any]) -> list[dict[str, Any]]:
|
|
305
|
+
warnings: list[dict[str, Any]] = []
|
|
306
|
+
ratios = stats.get("ratios", {})
|
|
307
|
+
pipeline_share = float(ratios.get("pipelineTokenShare", 0.0))
|
|
308
|
+
coordination_share = _coordination_share(stats)
|
|
309
|
+
if pipeline_share > float(shape["overheadWarningShare"]):
|
|
310
|
+
warnings.append({"code": "pipeline-token-share-high", "value": pipeline_share, "limit": shape["overheadWarningShare"]})
|
|
311
|
+
if coordination_share > float(shape["coordinationWarningShare"]):
|
|
312
|
+
warnings.append({"code": "coordination-token-share-high", "value": coordination_share, "limit": shape["coordinationWarningShare"]})
|
|
313
|
+
if stats.get("averageProductValidationSteps", 0) > shape["reviewStepWarningThreshold"]:
|
|
314
|
+
warnings.append({"code": "review-step-cost-high", "value": stats["averageProductValidationSteps"], "limit": shape["reviewStepWarningThreshold"]})
|
|
315
|
+
confidence = stats.get("usageConfidence", {})
|
|
316
|
+
if confidence.get("missingEntries", 0) > 0:
|
|
317
|
+
warnings.append({"code": "missing-usage", "entries": confidence["missingEntries"]})
|
|
318
|
+
if stats.get("reportCount", 0) < int(profile.get("minimumReportsForConfidence", 2)):
|
|
319
|
+
warnings.append({"code": "weak-statistics", "reportCount": stats.get("reportCount", 0)})
|
|
320
|
+
if stats.get("status") != "PASS":
|
|
321
|
+
warnings.append({"code": "invalid-cost-report", "blockerCount": len(stats.get("blockers", []))})
|
|
322
|
+
return warnings
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _coordination_share(stats: dict[str, Any]) -> float:
|
|
326
|
+
totals = stats.get("totals", {})
|
|
327
|
+
overall = totals.get("overall", {}).get("tokens", 0)
|
|
328
|
+
coordination = totals.get("coordination", {}).get("tokens", 0)
|
|
329
|
+
return round(coordination / overall, 6) if overall else 0.0
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _weak_data(stats: dict[str, Any], profile: dict[str, Any]) -> bool:
|
|
333
|
+
usage = stats.get("usageConfidence", {})
|
|
334
|
+
minimum = int(profile.get("minimumReportsForConfidence", 2))
|
|
335
|
+
return stats.get("reportCount", 0) < minimum or usage.get("missingEntries", 0) > 0 or stats.get("status") != "PASS"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _confidence(stats: dict[str, Any], *, weak_data: bool, warning_count: int) -> str:
|
|
339
|
+
if weak_data:
|
|
340
|
+
return "LOW"
|
|
341
|
+
if stats.get("reportCount", 0) >= 3 and warning_count == 0:
|
|
342
|
+
return "HIGH"
|
|
343
|
+
return "MEDIUM"
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _recommended_mode(
|
|
347
|
+
shape: dict[str, Any],
|
|
348
|
+
*,
|
|
349
|
+
floor: str,
|
|
350
|
+
current_mode: str | None,
|
|
351
|
+
confidence: str,
|
|
352
|
+
warnings: list[dict[str, Any]],
|
|
353
|
+
) -> str:
|
|
354
|
+
current = current_mode if current_mode in MODE_ORDER else None
|
|
355
|
+
if confidence == "LOW":
|
|
356
|
+
return _max_mode(current or str(shape["defaultMode"]), floor)
|
|
357
|
+
if shape.get("highRisk") is True:
|
|
358
|
+
return _max_mode(current or str(shape["defaultMode"]), floor)
|
|
359
|
+
overhead_warning = any(item["code"] in {"pipeline-token-share-high", "coordination-token-share-high"} for item in warnings)
|
|
360
|
+
target = str(shape["minMode"] if overhead_warning else shape["defaultMode"])
|
|
361
|
+
return _max_mode(target, floor)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _quality_floor_mode(
|
|
365
|
+
*,
|
|
366
|
+
task_shape: str,
|
|
367
|
+
baseline_profile: dict[str, Any],
|
|
368
|
+
sdd_tier: str | None,
|
|
369
|
+
risk_flags: list[str],
|
|
370
|
+
) -> str:
|
|
371
|
+
from agent_lifecycle.policy.quality_floor import quality_floor_mode
|
|
372
|
+
|
|
373
|
+
return quality_floor_mode(
|
|
374
|
+
task_shape=task_shape,
|
|
375
|
+
baseline_profile=baseline_profile,
|
|
376
|
+
sdd_tier=sdd_tier,
|
|
377
|
+
risk_flags=risk_flags,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _max_mode(first: str, second: str) -> str:
|
|
382
|
+
from agent_lifecycle.policy.quality_floor import max_mode
|
|
383
|
+
|
|
384
|
+
return max_mode(first, second)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _mode_index(mode: str) -> int:
|
|
388
|
+
from agent_lifecycle.policy.quality_floor import mode_index
|
|
389
|
+
|
|
390
|
+
return mode_index(mode)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _best_quality_cost_signal(rows: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
394
|
+
if not rows:
|
|
395
|
+
return None
|
|
396
|
+
return sorted(
|
|
397
|
+
rows,
|
|
398
|
+
key=lambda item: (
|
|
399
|
+
-float(item.get("successRate", 0.0)),
|
|
400
|
+
float(item.get("blockerRate", 1.0)),
|
|
401
|
+
float(item.get("averageTokens", 0.0)),
|
|
402
|
+
-int(item.get("sampleCount", 0)),
|
|
403
|
+
),
|
|
404
|
+
)[0]
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _quality_cost_warnings(
|
|
408
|
+
selected: dict[str, Any] | None,
|
|
409
|
+
*,
|
|
410
|
+
signals: dict[str, Any],
|
|
411
|
+
profile: dict[str, Any],
|
|
412
|
+
) -> list[dict[str, Any]]:
|
|
413
|
+
warnings: list[dict[str, Any]] = []
|
|
414
|
+
if selected is None:
|
|
415
|
+
warnings.append({"code": "quality-cost-no-local-signal"})
|
|
416
|
+
return warnings
|
|
417
|
+
if int(selected.get("sampleCount", 0)) < int(profile.get("minimumReportsForConfidence", 2)):
|
|
418
|
+
warnings.append({"code": "quality-cost-weak-samples", "sampleCount": selected.get("sampleCount", 0)})
|
|
419
|
+
if float(selected.get("successRate", 0.0)) < 0.8:
|
|
420
|
+
warnings.append({"code": "quality-cost-success-rate-low", "successRate": selected.get("successRate", 0.0)})
|
|
421
|
+
if float(selected.get("blockerRate", 0.0)) > 0:
|
|
422
|
+
warnings.append({"code": "quality-cost-blocker-rate-nonzero", "blockerRate": selected.get("blockerRate", 0.0)})
|
|
423
|
+
if signals.get("telemetryStarted") is not False:
|
|
424
|
+
warnings.append({"code": "quality-cost-telemetry-unsafe"})
|
|
425
|
+
if signals.get("providerModelLeaderboard") is not False:
|
|
426
|
+
warnings.append({"code": "quality-cost-provider-leaderboard-unsafe"})
|
|
427
|
+
if signals.get("monetaryFieldsUsed") is not False:
|
|
428
|
+
warnings.append({"code": "quality-cost-monetary-fields-unsafe"})
|
|
429
|
+
if signals.get("productionPromotionClaimed") is not False:
|
|
430
|
+
warnings.append({"code": "quality-cost-production-claim-unsafe"})
|
|
431
|
+
return warnings
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _unsafe_quality_cost_signal_blockers(signals: dict[str, Any]) -> list[dict[str, Any]]:
|
|
435
|
+
blockers = []
|
|
436
|
+
for field, code in (
|
|
437
|
+
("telemetryStarted", "quality-cost-telemetry-started"),
|
|
438
|
+
("providerModelLeaderboard", "quality-cost-provider-leaderboard"),
|
|
439
|
+
("monetaryFieldsUsed", "quality-cost-monetary-fields-used"),
|
|
440
|
+
("productionPromotionClaimed", "quality-cost-production-promotion-claimed"),
|
|
441
|
+
):
|
|
442
|
+
if signals.get(field) is not False:
|
|
443
|
+
blockers.append({"code": code, "field": field})
|
|
444
|
+
return blockers
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _quality_cost_confidence(selected: dict[str, Any] | None, profile: dict[str, Any], *, warning_count: int) -> str:
|
|
448
|
+
if selected is None:
|
|
449
|
+
return "LOW"
|
|
450
|
+
samples = int(selected.get("sampleCount", 0))
|
|
451
|
+
success = float(selected.get("successRate", 0.0))
|
|
452
|
+
blocker_rate = float(selected.get("blockerRate", 0.0))
|
|
453
|
+
if samples < int(profile.get("minimumReportsForConfidence", 2)) or success < 0.8 or blocker_rate > 0:
|
|
454
|
+
return "LOW"
|
|
455
|
+
if samples >= 5 and success >= 0.9 and warning_count == 0:
|
|
456
|
+
return "HIGH"
|
|
457
|
+
return "MEDIUM"
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _quality_cost_reasons(
|
|
461
|
+
confidence: str,
|
|
462
|
+
recommended: str,
|
|
463
|
+
floor: str,
|
|
464
|
+
selected: dict[str, Any] | None,
|
|
465
|
+
warnings: list[dict[str, Any]],
|
|
466
|
+
) -> list[str]:
|
|
467
|
+
reasons = [f"confidence-{confidence.lower()}", f"quality-floor-{floor}", f"recommended-{recommended}"]
|
|
468
|
+
if selected is not None:
|
|
469
|
+
reasons.append(f"local-samples-{selected.get('sampleCount', 0)}")
|
|
470
|
+
reasons.extend(item["code"] for item in warnings[:6] if isinstance(item.get("code"), str))
|
|
471
|
+
return reasons
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _reasons(confidence: str, recommended: str, floor: str, weak_data: bool, warnings: list[dict[str, Any]]) -> list[str]:
|
|
475
|
+
reasons = [f"recommendedMode={recommended}", f"qualityFloor={floor}", f"confidence={confidence}"]
|
|
476
|
+
if weak_data:
|
|
477
|
+
reasons.append("statistics are weak or incomplete, so the safer current/floor mode is retained")
|
|
478
|
+
for warning in warnings:
|
|
479
|
+
reasons.append(f"warning:{warning['code']}")
|
|
480
|
+
return reasons
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _failed_recommendation(
|
|
484
|
+
task_shape: str,
|
|
485
|
+
current_mode: str | None,
|
|
486
|
+
baseline_validation: dict[str, Any],
|
|
487
|
+
stats: dict[str, Any],
|
|
488
|
+
blockers: list[dict[str, Any]],
|
|
489
|
+
) -> dict[str, Any]:
|
|
490
|
+
return {
|
|
491
|
+
"schemaVersion": RECOMMENDATION_SCHEMA,
|
|
492
|
+
"status": "FAIL",
|
|
493
|
+
"taskShape": task_shape,
|
|
494
|
+
"currentMode": current_mode,
|
|
495
|
+
"recommendedMode": current_mode if current_mode in MODE_ORDER else "standard",
|
|
496
|
+
"confidence": "LOW",
|
|
497
|
+
"advisoryOnly": True,
|
|
498
|
+
"autoApply": False,
|
|
499
|
+
"qualityFloor": "standard",
|
|
500
|
+
"qualityFloorPreserved": True,
|
|
501
|
+
"warnings": [{"code": "recommendation-blocked", "blockerCount": len(blockers)}],
|
|
502
|
+
"reasons": ["baseline profile is invalid or task shape is unknown"],
|
|
503
|
+
"statistics": stats,
|
|
504
|
+
"baselineValidation": baseline_validation,
|
|
505
|
+
"blockers": blockers,
|
|
506
|
+
"productionPromotionClaimed": False,
|
|
507
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Summarize lifecycle regression signals for policy decisions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_lifecycle.contracts import canonical_digest
|
|
8
|
+
|
|
9
|
+
REGRESSION_SCHEMA = "agent-lifecycle-regression-signals.v1"
|
|
10
|
+
BLOCKING_SIGNAL_TYPES = {
|
|
11
|
+
"failedFinalAudit",
|
|
12
|
+
"reopenedWork",
|
|
13
|
+
"rollback",
|
|
14
|
+
"repeatedRemediation",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def summarize_regression_signals(signals: list[dict[str, Any]] | None) -> dict[str, Any]:
|
|
19
|
+
normalized: list[dict[str, Any]] = []
|
|
20
|
+
blockers: list[dict[str, Any]] = []
|
|
21
|
+
for index, signal in enumerate(signals or []):
|
|
22
|
+
item = _normalize_signal(index, signal, blockers)
|
|
23
|
+
if item is not None:
|
|
24
|
+
normalized.append(item)
|
|
25
|
+
blocking_signals = [
|
|
26
|
+
item
|
|
27
|
+
for item in normalized
|
|
28
|
+
if item["count"] > 0 and (item["type"] in BLOCKING_SIGNAL_TYPES or item["severity"] in {"HIGH", "BLOCKER"})
|
|
29
|
+
]
|
|
30
|
+
body = {
|
|
31
|
+
"schemaVersion": REGRESSION_SCHEMA,
|
|
32
|
+
"status": "FAIL" if blockers else ("BLOCK" if blocking_signals else "PASS"),
|
|
33
|
+
"signalCount": len(normalized),
|
|
34
|
+
"signals": normalized,
|
|
35
|
+
"blockingSignals": blocking_signals,
|
|
36
|
+
"blockers": blockers,
|
|
37
|
+
"productionPromotionClaimed": False,
|
|
38
|
+
}
|
|
39
|
+
return {**body, "signalsDigest": canonical_digest(body)}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _normalize_signal(index: int, signal: dict[str, Any], blockers: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
43
|
+
if not isinstance(signal, dict):
|
|
44
|
+
blockers.append({"code": "regression-signal-type", "index": index})
|
|
45
|
+
return None
|
|
46
|
+
signal_type = signal.get("type")
|
|
47
|
+
if not isinstance(signal_type, str) or not signal_type:
|
|
48
|
+
blockers.append({"code": "regression-signal-missing-type", "index": index})
|
|
49
|
+
return None
|
|
50
|
+
count = signal.get("count", 1)
|
|
51
|
+
if not isinstance(count, int) or isinstance(count, bool) or count < 0:
|
|
52
|
+
blockers.append({"code": "regression-signal-count", "index": index, "type": signal_type})
|
|
53
|
+
return None
|
|
54
|
+
severity = signal.get("severity", "MEDIUM")
|
|
55
|
+
if severity not in {"LOW", "MEDIUM", "HIGH", "BLOCKER"}:
|
|
56
|
+
blockers.append({"code": "regression-signal-severity", "index": index, "type": signal_type})
|
|
57
|
+
return None
|
|
58
|
+
return {
|
|
59
|
+
"type": signal_type,
|
|
60
|
+
"count": count,
|
|
61
|
+
"severity": severity,
|
|
62
|
+
"source": signal.get("source"),
|
|
63
|
+
"note": signal.get("note"),
|
|
64
|
+
}
|