algocode-agent 0.1.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.
- algocode/__init__.py +5 -0
- algocode/__main__.py +4 -0
- algocode/acceptance/__init__.py +12 -0
- algocode/acceptance/matrix.py +69 -0
- algocode/acceptance/probe.py +206 -0
- algocode/acceptance/service.py +244 -0
- algocode/acceptance/types.py +58 -0
- algocode/application/__init__.py +1 -0
- algocode/application/services/__init__.py +25 -0
- algocode/application/services/apply_service.py +177 -0
- algocode/application/services/baseline_service.py +243 -0
- algocode/application/services/benchmark_service.py +628 -0
- algocode/application/services/candidate_service.py +169 -0
- algocode/application/services/contract_service.py +619 -0
- algocode/application/services/correctness_service.py +387 -0
- algocode/application/services/decision_service.py +228 -0
- algocode/application/services/project_bootstrap_service.py +891 -0
- algocode/application/services/project_service.py +125 -0
- algocode/application/services/project_state.py +219 -0
- algocode/application/services/report_service.py +323 -0
- algocode/application/services/task_service.py +77 -0
- algocode/approval/service.py +77 -0
- algocode/approval/types.py +29 -0
- algocode/benchmark/__init__.py +17 -0
- algocode/benchmark/engine.py +214 -0
- algocode/benchmark/environment.py +105 -0
- algocode/benchmark/spec.py +85 -0
- algocode/benchmark/types.py +128 -0
- algocode/bootstrap.py +268 -0
- algocode/cli/__init__.py +1 -0
- algocode/cli/commands/__init__.py +1 -0
- algocode/cli/commands/accept.py +72 -0
- algocode/cli/commands/api.py +220 -0
- algocode/cli/commands/apply.py +94 -0
- algocode/cli/commands/baseline.py +100 -0
- algocode/cli/commands/benchmark.py +227 -0
- algocode/cli/commands/candidate.py +155 -0
- algocode/cli/commands/correctness.py +197 -0
- algocode/cli/commands/diff.py +73 -0
- algocode/cli/commands/doctor.py +149 -0
- algocode/cli/commands/eval.py +115 -0
- algocode/cli/commands/experiment.py +151 -0
- algocode/cli/commands/gate.py +61 -0
- algocode/cli/commands/init.py +133 -0
- algocode/cli/commands/model.py +115 -0
- algocode/cli/commands/optimize.py +252 -0
- algocode/cli/commands/provider_test.py +90 -0
- algocode/cli/commands/report.py +59 -0
- algocode/cli/commands/retry.py +107 -0
- algocode/cli/commands/review.py +154 -0
- algocode/cli/commands/status.py +136 -0
- algocode/cli/commands/task.py +146 -0
- algocode/cli/context.py +86 -0
- algocode/cli/main.py +79 -0
- algocode/cli/output.py +70 -0
- algocode/config/__init__.py +6 -0
- algocode/config/global_file.py +54 -0
- algocode/config/loader.py +181 -0
- algocode/config/model.py +193 -0
- algocode/context/__init__.py +1 -0
- algocode/context/builder.py +410 -0
- algocode/context/estimator.py +25 -0
- algocode/context/facts.py +37 -0
- algocode/context/types.py +158 -0
- algocode/correctness/__init__.py +6 -0
- algocode/correctness/engine.py +452 -0
- algocode/correctness/protected.py +47 -0
- algocode/correctness/spec.py +108 -0
- algocode/correctness/types.py +45 -0
- algocode/domain/__init__.py +1 -0
- algocode/domain/commands/__init__.py +19 -0
- algocode/domain/errors/__init__.py +70 -0
- algocode/domain/events/__init__.py +5 -0
- algocode/domain/events/envelope.py +67 -0
- algocode/domain/model/__init__.py +76 -0
- algocode/domain/model/entities.py +151 -0
- algocode/domain/model/enums.py +119 -0
- algocode/domain/model/ids.py +36 -0
- algocode/domain/model/values.py +18 -0
- algocode/eval/__init__.py +1 -0
- algocode/eval/harness.py +377 -0
- algocode/eval/provider.py +135 -0
- algocode/eval/service.py +209 -0
- algocode/eval/suites.py +121 -0
- algocode/eval/types.py +76 -0
- algocode/languages/__init__.py +7 -0
- algocode/languages/cpp.py +288 -0
- algocode/languages/discovery.py +29 -0
- algocode/languages/python.py +94 -0
- algocode/languages/registry.py +60 -0
- algocode/languages/runner.py +68 -0
- algocode/languages/types.py +88 -0
- algocode/observability/__init__.py +1 -0
- algocode/policy/__init__.py +1 -0
- algocode/policy/engine.py +111 -0
- algocode/policy/types.py +44 -0
- algocode/ports/__init__.py +22 -0
- algocode/ports/artifact_store.py +22 -0
- algocode/ports/clock.py +12 -0
- algocode/ports/event_store.py +23 -0
- algocode/ports/language.py +30 -0
- algocode/ports/policy.py +7 -0
- algocode/ports/provider.py +12 -0
- algocode/ports/resource.py +7 -0
- algocode/ports/workspace.py +19 -0
- algocode/project_layout.py +135 -0
- algocode/providers/__init__.py +36 -0
- algocode/providers/anthropic.py +264 -0
- algocode/providers/errors.py +58 -0
- algocode/providers/factory.py +86 -0
- algocode/providers/fake.py +148 -0
- algocode/providers/openai_compatible.py +353 -0
- algocode/providers/types.py +101 -0
- algocode/report/__init__.py +1 -0
- algocode/resources/__init__.py +6 -0
- algocode/resources/local.py +235 -0
- algocode/resources/types.py +35 -0
- algocode/runtime/__init__.py +1 -0
- algocode/runtime/agent.py +2238 -0
- algocode/runtime/analysis.py +187 -0
- algocode/runtime/gates.py +69 -0
- algocode/runtime/locks.py +61 -0
- algocode/runtime/model_log.py +265 -0
- algocode/runtime/optimization_record.py +332 -0
- algocode/runtime/planning.py +73 -0
- algocode/runtime/repair_memory.py +195 -0
- algocode/runtime/task_lock.py +61 -0
- algocode/sandbox/local.py +71 -0
- algocode/sandbox/runner.py +393 -0
- algocode/security/__init__.py +6 -0
- algocode/security/credentials.py +75 -0
- algocode/security/redaction.py +83 -0
- algocode/storage/__init__.py +1 -0
- algocode/storage/artifacts/__init__.py +5 -0
- algocode/storage/artifacts/file_artifact_store.py +90 -0
- algocode/storage/events/__init__.py +5 -0
- algocode/storage/events/sqlite_event_store.py +135 -0
- algocode/storage/paths.py +27 -0
- algocode/storage/sqlite/__init__.py +5 -0
- algocode/storage/sqlite/approval_store.py +65 -0
- algocode/storage/sqlite/database.py +51 -0
- algocode/storage/sqlite/migrations.py +298 -0
- algocode/storage/sqlite/projections/__init__.py +12 -0
- algocode/storage/sqlite/projections/baseline_projection.py +137 -0
- algocode/storage/sqlite/projections/benchmark_projection.py +210 -0
- algocode/storage/sqlite/projections/candidate_projection.py +177 -0
- algocode/storage/sqlite/projections/correctness_projection.py +148 -0
- algocode/storage/sqlite/projections/decision_projection.py +95 -0
- algocode/storage/sqlite/projections/project_projection.py +84 -0
- algocode/storage/sqlite/projections/task_projection.py +194 -0
- algocode/structured_output.py +153 -0
- algocode/tools/__init__.py +26 -0
- algocode/tools/builtins/__init__.py +1 -0
- algocode/tools/builtins/actions.py +707 -0
- algocode/tools/builtins/filesystem.py +459 -0
- algocode/tools/registry.py +201 -0
- algocode/tools/types.py +57 -0
- algocode/workspace/__init__.py +12 -0
- algocode/workspace/git.py +470 -0
- algocode/workspace/types.py +31 -0
- algocode_agent-0.1.0.dist-info/METADATA +678 -0
- algocode_agent-0.1.0.dist-info/RECORD +165 -0
- algocode_agent-0.1.0.dist-info/WHEEL +4 -0
- algocode_agent-0.1.0.dist-info/entry_points.txt +2 -0
- algocode_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
algocode/__init__.py
ADDED
algocode/__main__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""P0 acceptance and release gate."""
|
|
2
|
+
|
|
3
|
+
from algocode.acceptance.service import AcceptanceService, render_acceptance_markdown
|
|
4
|
+
from algocode.acceptance.types import AcceptanceReport, GateCheck, Requirement
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AcceptanceReport",
|
|
8
|
+
"AcceptanceService",
|
|
9
|
+
"GateCheck",
|
|
10
|
+
"Requirement",
|
|
11
|
+
"render_acceptance_markdown",
|
|
12
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Requirement traceability matrix loading and validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from algocode.acceptance.types import Requirement
|
|
10
|
+
|
|
11
|
+
DEFAULT_MATRIX = Path("docs/acceptance/p0-requirements.yaml")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_requirements(root: str | Path, path: str | Path | None = None) -> tuple[Requirement, ...]:
|
|
15
|
+
root_path = Path(root).resolve()
|
|
16
|
+
matrix_path = Path(path) if path is not None else DEFAULT_MATRIX
|
|
17
|
+
if not matrix_path.is_absolute():
|
|
18
|
+
matrix_path = root_path / matrix_path
|
|
19
|
+
try:
|
|
20
|
+
raw = yaml.safe_load(matrix_path.read_text(encoding="utf-8"))
|
|
21
|
+
except (OSError, yaml.YAMLError) as exc:
|
|
22
|
+
raise ValueError(f"failed to load acceptance matrix {matrix_path}: {exc}") from exc
|
|
23
|
+
if not isinstance(raw, dict):
|
|
24
|
+
raise ValueError("acceptance matrix must be a YAML mapping")
|
|
25
|
+
rows = raw.get("requirements")
|
|
26
|
+
if not isinstance(rows, list) or not rows:
|
|
27
|
+
raise ValueError("acceptance matrix must contain requirements")
|
|
28
|
+
requirements: list[Requirement] = []
|
|
29
|
+
seen: set[str] = set()
|
|
30
|
+
for index, row in enumerate(rows):
|
|
31
|
+
if not isinstance(row, dict):
|
|
32
|
+
raise ValueError(f"acceptance requirement {index} must be a mapping")
|
|
33
|
+
requirement = Requirement(
|
|
34
|
+
id=str(row.get("id", "")),
|
|
35
|
+
title=str(row.get("title", "")),
|
|
36
|
+
implementation=_paths(row.get("implementation"), "implementation", index),
|
|
37
|
+
tests=_paths(row.get("tests"), "tests", index),
|
|
38
|
+
evidence=tuple(str(item) for item in row.get("evidence", ())),
|
|
39
|
+
)
|
|
40
|
+
if not requirement.id or not requirement.title:
|
|
41
|
+
raise ValueError(f"acceptance requirement {index} requires id and title")
|
|
42
|
+
if requirement.id in seen:
|
|
43
|
+
raise ValueError(f"duplicate acceptance requirement: {requirement.id}")
|
|
44
|
+
if not requirement.implementation or not requirement.tests or not requirement.evidence:
|
|
45
|
+
raise ValueError(f"acceptance requirement {requirement.id} is incomplete")
|
|
46
|
+
seen.add(requirement.id)
|
|
47
|
+
requirements.append(requirement)
|
|
48
|
+
return tuple(requirements)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_requirement_paths(
|
|
52
|
+
root: str | Path,
|
|
53
|
+
requirements: tuple[Requirement, ...],
|
|
54
|
+
) -> tuple[Requirement, ...]:
|
|
55
|
+
root_path = Path(root).resolve()
|
|
56
|
+
missing: list[str] = []
|
|
57
|
+
for requirement in requirements:
|
|
58
|
+
for path in (*requirement.implementation, *requirement.tests):
|
|
59
|
+
if not (root_path / path).exists():
|
|
60
|
+
missing.append(f"{requirement.id}:{path}")
|
|
61
|
+
if missing:
|
|
62
|
+
raise ValueError(f"acceptance matrix references missing paths: {', '.join(missing)}")
|
|
63
|
+
return requirements
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _paths(value: object, field: str, index: int) -> tuple[str, ...]:
|
|
67
|
+
if not isinstance(value, list):
|
|
68
|
+
raise ValueError(f"acceptance requirement {index} field {field} must be a list")
|
|
69
|
+
return tuple(str(item) for item in value)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Release evidence probe for the P0 acceptance gate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from algocode.benchmark.spec import BenchmarkSpec
|
|
12
|
+
from algocode.bootstrap import build_context
|
|
13
|
+
from algocode.correctness.spec import CorrectnessCase, CorrectnessSpec
|
|
14
|
+
from algocode.domain.errors import DecisionError
|
|
15
|
+
from algocode.domain.model import TaskPhase
|
|
16
|
+
from algocode.policy.types import PolicyEffect, PolicyRequest
|
|
17
|
+
from algocode.tools.types import ToolContext
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _init_git_repository(root: Path, files: dict[str, str]) -> Path:
|
|
21
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
for relative_path, content in files.items():
|
|
23
|
+
target = root / relative_path
|
|
24
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
target.write_text(content, encoding="utf-8")
|
|
26
|
+
_git(root, "init", "-q")
|
|
27
|
+
_git(root, "config", "user.email", "algocode-acceptance@example.test")
|
|
28
|
+
_git(root, "config", "user.name", "Algocode Acceptance")
|
|
29
|
+
_git(root, "add", ".")
|
|
30
|
+
_git(root, "commit", "-q", "-m", "initial")
|
|
31
|
+
return root
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _git(root: Path, *args: str) -> None:
|
|
35
|
+
subprocess.run(("git", *args), cwd=root, check=True, capture_output=True, text=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ProbeMetrics:
|
|
40
|
+
false_accept_count: int
|
|
41
|
+
policy_violation_count: int
|
|
42
|
+
protected_file_change_count: int
|
|
43
|
+
replay_divergence_count: int
|
|
44
|
+
event_sequence_gap_count: int
|
|
45
|
+
apply_rollback_failure_count: int
|
|
46
|
+
event_count: int
|
|
47
|
+
task_status: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def collect_release_evidence(root: str | Path) -> ProbeMetrics:
|
|
51
|
+
with tempfile.TemporaryDirectory(
|
|
52
|
+
prefix=f"algocode-acceptance-{Path(root).name}-",
|
|
53
|
+
ignore_cleanup_errors=True,
|
|
54
|
+
) as directory:
|
|
55
|
+
return await _run_probe(Path(directory))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _run_probe(root: Path) -> ProbeMetrics:
|
|
59
|
+
project_root = _init_git_repository(
|
|
60
|
+
root / "project",
|
|
61
|
+
{"main.py": "import time\ntime.sleep(0.05)\nprint('hello')\n"},
|
|
62
|
+
)
|
|
63
|
+
(project_root / ".algocode.yaml").write_text(
|
|
64
|
+
"acceptancePolicy:\n minMedianImprovementPercent: -1000\n",
|
|
65
|
+
encoding="utf-8",
|
|
66
|
+
)
|
|
67
|
+
context = build_context(project_root=project_root, data_dir=root / "data")
|
|
68
|
+
project = await context.project_service.register(project_root, write_config=True)
|
|
69
|
+
task = await context.task_service.create_task(
|
|
70
|
+
"P0 release evidence",
|
|
71
|
+
project_id=project.id,
|
|
72
|
+
)
|
|
73
|
+
await context.baseline_service.capture(task.id)
|
|
74
|
+
await context.correctness_service.run_baseline(
|
|
75
|
+
task.id,
|
|
76
|
+
CorrectnessSpec(
|
|
77
|
+
mode="cases",
|
|
78
|
+
comparison="line-trim",
|
|
79
|
+
cases=(CorrectnessCase(id="baseline", expected_output="hello"),),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
benchmark_spec = BenchmarkSpec(warmup=0, repeats=1)
|
|
83
|
+
await context.benchmark_service.run_baseline(task.id, benchmark_spec)
|
|
84
|
+
|
|
85
|
+
apply_rollback_failures = 0
|
|
86
|
+
replay_divergence = 0
|
|
87
|
+
protected_file_changes = 0
|
|
88
|
+
|
|
89
|
+
candidate = await context.candidate_service.create(task.id)
|
|
90
|
+
(Path(candidate.workspace_ref) / "main.py").write_text(
|
|
91
|
+
"print('hi')\n",
|
|
92
|
+
encoding="utf-8",
|
|
93
|
+
)
|
|
94
|
+
candidate = await context.candidate_service.freeze(candidate.id)
|
|
95
|
+
correctness_run, correctness = await context.correctness_service.run_target(
|
|
96
|
+
task.id,
|
|
97
|
+
CorrectnessSpec(
|
|
98
|
+
mode="cases",
|
|
99
|
+
comparison="line-trim",
|
|
100
|
+
cases=(CorrectnessCase(id="candidate", expected_output="hi"),),
|
|
101
|
+
),
|
|
102
|
+
target_kind="candidate",
|
|
103
|
+
target_id=str(candidate.id),
|
|
104
|
+
workspace_ref=candidate.workspace_ref,
|
|
105
|
+
)
|
|
106
|
+
if not correctness.passed:
|
|
107
|
+
replay_divergence += 1
|
|
108
|
+
else:
|
|
109
|
+
_, replay = await context.correctness_service.replay(correctness_run.id)
|
|
110
|
+
replay_divergence += int(not replay.passed)
|
|
111
|
+
await context.benchmark_service.run_candidate(
|
|
112
|
+
task.id,
|
|
113
|
+
str(candidate.id),
|
|
114
|
+
candidate.workspace_ref,
|
|
115
|
+
correctness_run.id,
|
|
116
|
+
benchmark_spec,
|
|
117
|
+
)
|
|
118
|
+
await context.decision_service.accept(task.id, candidate.id)
|
|
119
|
+
await context.report_service.build(task.id)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
applied = await context.apply_service.apply(task.id, candidate.id)
|
|
123
|
+
apply_rollback_failures += int(not applied.get("applied", False))
|
|
124
|
+
rolled_back = await context.apply_service.rollback(task.id, candidate.id)
|
|
125
|
+
apply_rollback_failures += int(not rolled_back.get("rolled_back", False))
|
|
126
|
+
except Exception:
|
|
127
|
+
apply_rollback_failures += 1
|
|
128
|
+
|
|
129
|
+
policy = context.policy_engine.evaluate(
|
|
130
|
+
PolicyRequest(
|
|
131
|
+
action="file.write",
|
|
132
|
+
resource="tests/test_main.py",
|
|
133
|
+
tool_name="apply_patch",
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
policy_violations = int(policy.effect is not PolicyEffect.DENY)
|
|
137
|
+
|
|
138
|
+
protected_context = ToolContext(
|
|
139
|
+
task=task,
|
|
140
|
+
phase=TaskPhase.IMPLEMENT,
|
|
141
|
+
workspace=Path(candidate.workspace_ref),
|
|
142
|
+
candidate_id=str(candidate.id),
|
|
143
|
+
artifact_store=context.artifact_store,
|
|
144
|
+
language_registry=context.language_registry,
|
|
145
|
+
correctness_service=context.correctness_service,
|
|
146
|
+
benchmark_service=context.benchmark_service,
|
|
147
|
+
protected_files=context.config.correctness.protected_files,
|
|
148
|
+
)
|
|
149
|
+
protected_patch = await context.tool_registry.execute(
|
|
150
|
+
"apply_patch",
|
|
151
|
+
{
|
|
152
|
+
"patch": (
|
|
153
|
+
"diff --git a/tests/test_main.py b/tests/test_main.py\n"
|
|
154
|
+
"--- a/tests/test_main.py\n"
|
|
155
|
+
"+++ b/tests/test_main.py\n"
|
|
156
|
+
"@@ -1 +1 @@\n"
|
|
157
|
+
"-assert True\n"
|
|
158
|
+
"+assert False\n"
|
|
159
|
+
)
|
|
160
|
+
},
|
|
161
|
+
protected_context,
|
|
162
|
+
)
|
|
163
|
+
if protected_patch.status == "success":
|
|
164
|
+
protected_file_changes += 1
|
|
165
|
+
|
|
166
|
+
false_accepts = 0
|
|
167
|
+
unverified = await context.candidate_service.create(task.id)
|
|
168
|
+
(Path(unverified.workspace_ref) / "main.py").write_text(
|
|
169
|
+
"print('unverified')\n",
|
|
170
|
+
encoding="utf-8",
|
|
171
|
+
)
|
|
172
|
+
unverified = await context.candidate_service.freeze(unverified.id)
|
|
173
|
+
await context.correctness_service.run_target(
|
|
174
|
+
task.id,
|
|
175
|
+
CorrectnessSpec(
|
|
176
|
+
mode="cases",
|
|
177
|
+
comparison="line-trim",
|
|
178
|
+
cases=(CorrectnessCase(id="unverified", expected_output="unverified"),),
|
|
179
|
+
),
|
|
180
|
+
target_kind="candidate",
|
|
181
|
+
target_id=str(unverified.id),
|
|
182
|
+
workspace_ref=unverified.workspace_ref,
|
|
183
|
+
)
|
|
184
|
+
try:
|
|
185
|
+
await context.decision_service.accept(task.id, unverified.id)
|
|
186
|
+
false_accepts += 1
|
|
187
|
+
except DecisionError:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
events = await context.event_store.read(str(task.id))
|
|
191
|
+
gaps = sum(1 for expected, event in enumerate(events, start=1) if event.seq != expected)
|
|
192
|
+
task_after = await context.task_service.get_task(task.id)
|
|
193
|
+
return ProbeMetrics(
|
|
194
|
+
false_accept_count=false_accepts,
|
|
195
|
+
policy_violation_count=policy_violations,
|
|
196
|
+
protected_file_change_count=protected_file_changes,
|
|
197
|
+
replay_divergence_count=replay_divergence,
|
|
198
|
+
event_sequence_gap_count=gaps,
|
|
199
|
+
apply_rollback_failure_count=apply_rollback_failures,
|
|
200
|
+
event_count=len(events),
|
|
201
|
+
task_status=task_after.status.value,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def run_release_probe(root: str | Path) -> ProbeMetrics:
|
|
206
|
+
return asyncio.run(collect_release_evidence(root))
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Repeatable P0 acceptance and release gate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from algocode.acceptance.matrix import load_requirements, validate_requirement_paths
|
|
14
|
+
from algocode.acceptance.probe import ProbeMetrics, run_release_probe
|
|
15
|
+
from algocode.acceptance.types import AcceptanceReport, GateCheck
|
|
16
|
+
|
|
17
|
+
SUITE_GROUPS: dict[str, tuple[str, ...]] = {
|
|
18
|
+
"unit": ("tests/unit",),
|
|
19
|
+
"contract": ("tests/contract",),
|
|
20
|
+
"integration": (
|
|
21
|
+
"tests/integration",
|
|
22
|
+
"--ignore=tests/integration/test_e2e_full.py",
|
|
23
|
+
"--ignore=tests/integration/test_eval_service.py",
|
|
24
|
+
),
|
|
25
|
+
"e2e": ("tests/integration/test_e2e_full.py",),
|
|
26
|
+
"eval": ("tests/integration/test_eval_service.py",),
|
|
27
|
+
}
|
|
28
|
+
ZERO_METRICS = (
|
|
29
|
+
"false_accept_count",
|
|
30
|
+
"policy_violation_count",
|
|
31
|
+
"protected_file_change_count",
|
|
32
|
+
"replay_divergence_count",
|
|
33
|
+
"event_sequence_gap_count",
|
|
34
|
+
"apply_rollback_failure_count",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AcceptanceService:
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
root: str | Path,
|
|
42
|
+
*,
|
|
43
|
+
report_dir: str | Path | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.root = Path(root).resolve()
|
|
46
|
+
self.report_dir = (
|
|
47
|
+
Path(report_dir).resolve()
|
|
48
|
+
if report_dir is not None
|
|
49
|
+
else self.root / "docs" / "acceptance" / "P0"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def run(self, *, run_tests: bool = True) -> AcceptanceReport:
|
|
53
|
+
checks: list[GateCheck] = []
|
|
54
|
+
requirements = ()
|
|
55
|
+
mapping_ok = True
|
|
56
|
+
try:
|
|
57
|
+
requirements = load_requirements(self.root)
|
|
58
|
+
requirements = validate_requirement_paths(self.root, requirements)
|
|
59
|
+
checks.append(
|
|
60
|
+
GateCheck(
|
|
61
|
+
id="matrix.mapping",
|
|
62
|
+
category="traceability",
|
|
63
|
+
passed=True,
|
|
64
|
+
message=f"mapped {len(requirements)} P0 requirements",
|
|
65
|
+
metrics={"requirements_total": len(requirements)},
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
except ValueError as exc:
|
|
69
|
+
mapping_ok = False
|
|
70
|
+
checks.append(
|
|
71
|
+
GateCheck(
|
|
72
|
+
id="matrix.mapping",
|
|
73
|
+
category="traceability",
|
|
74
|
+
passed=False,
|
|
75
|
+
message=str(exc),
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if run_tests:
|
|
80
|
+
checks.extend(self._run_test_suites())
|
|
81
|
+
else:
|
|
82
|
+
checks.append(
|
|
83
|
+
GateCheck(
|
|
84
|
+
id="suite.skipped",
|
|
85
|
+
category="test",
|
|
86
|
+
passed=True,
|
|
87
|
+
message="test suites skipped by request",
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
probe = run_release_probe(self.root)
|
|
93
|
+
checks.extend(_probe_checks(probe))
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
probe = None
|
|
96
|
+
checks.append(
|
|
97
|
+
GateCheck(
|
|
98
|
+
id="probe.release_evidence",
|
|
99
|
+
category="evidence",
|
|
100
|
+
passed=False,
|
|
101
|
+
message=f"release evidence probe failed: {exc}",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
metrics = _metrics(probe)
|
|
106
|
+
status = "PASS" if mapping_ok and all(check.passed for check in checks) else "FAIL"
|
|
107
|
+
report = AcceptanceReport(
|
|
108
|
+
schema_version=1,
|
|
109
|
+
report_id=f"acceptance_{uuid4().hex}",
|
|
110
|
+
generated_at=datetime.now(UTC).isoformat(),
|
|
111
|
+
status=status,
|
|
112
|
+
requirements_total=len(requirements),
|
|
113
|
+
requirements_mapped=len(requirements) if mapping_ok else 0,
|
|
114
|
+
checks=tuple(checks),
|
|
115
|
+
metrics=metrics,
|
|
116
|
+
)
|
|
117
|
+
self.persist(report)
|
|
118
|
+
return report
|
|
119
|
+
|
|
120
|
+
def persist(self, report: AcceptanceReport) -> tuple[Path, Path]:
|
|
121
|
+
self.report_dir.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
json_path = self.report_dir / "report.json"
|
|
123
|
+
markdown_path = self.report_dir / "report.md"
|
|
124
|
+
json_path.write_text(
|
|
125
|
+
json.dumps(report.payload(), ensure_ascii=False, indent=2, sort_keys=True),
|
|
126
|
+
encoding="utf-8",
|
|
127
|
+
)
|
|
128
|
+
markdown_path.write_text(render_acceptance_markdown(report), encoding="utf-8")
|
|
129
|
+
return json_path, markdown_path
|
|
130
|
+
|
|
131
|
+
def _run_test_suites(self) -> list[GateCheck]:
|
|
132
|
+
checks: list[GateCheck] = []
|
|
133
|
+
for name, arguments in SUITE_GROUPS.items():
|
|
134
|
+
started = time.perf_counter()
|
|
135
|
+
completed = subprocess.run(
|
|
136
|
+
(sys.executable, "-m", "pytest", "-q", *arguments),
|
|
137
|
+
cwd=self.root,
|
|
138
|
+
check=False,
|
|
139
|
+
capture_output=True,
|
|
140
|
+
text=True,
|
|
141
|
+
timeout=1800,
|
|
142
|
+
)
|
|
143
|
+
output = f"{completed.stdout}\n{completed.stderr}".strip()
|
|
144
|
+
checks.append(
|
|
145
|
+
GateCheck(
|
|
146
|
+
id=f"suite.{name}",
|
|
147
|
+
category="test",
|
|
148
|
+
passed=completed.returncode == 0,
|
|
149
|
+
message=f"{name} exit code {completed.returncode}",
|
|
150
|
+
metrics={"duration_seconds": time.perf_counter() - started},
|
|
151
|
+
output_tail=output[-4000:],
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
return checks
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _probe_checks(probe: ProbeMetrics) -> list[GateCheck]:
|
|
158
|
+
return [
|
|
159
|
+
GateCheck(
|
|
160
|
+
id="metric.false_accept",
|
|
161
|
+
category="evidence",
|
|
162
|
+
passed=probe.false_accept_count == 0,
|
|
163
|
+
message=f"false accept count = {probe.false_accept_count}",
|
|
164
|
+
metrics={"false_accept_count": probe.false_accept_count},
|
|
165
|
+
),
|
|
166
|
+
GateCheck(
|
|
167
|
+
id="metric.policy_violation",
|
|
168
|
+
category="evidence",
|
|
169
|
+
passed=probe.policy_violation_count == 0,
|
|
170
|
+
message=f"policy violation count = {probe.policy_violation_count}",
|
|
171
|
+
metrics={"policy_violation_count": probe.policy_violation_count},
|
|
172
|
+
),
|
|
173
|
+
GateCheck(
|
|
174
|
+
id="metric.protected_file_change",
|
|
175
|
+
category="evidence",
|
|
176
|
+
passed=probe.protected_file_change_count == 0,
|
|
177
|
+
message=f"protected file change count = {probe.protected_file_change_count}",
|
|
178
|
+
metrics={"protected_file_change_count": probe.protected_file_change_count},
|
|
179
|
+
),
|
|
180
|
+
GateCheck(
|
|
181
|
+
id="metric.replay_divergence",
|
|
182
|
+
category="evidence",
|
|
183
|
+
passed=probe.replay_divergence_count == 0,
|
|
184
|
+
message=f"replay divergence count = {probe.replay_divergence_count}",
|
|
185
|
+
metrics={"replay_divergence_count": probe.replay_divergence_count},
|
|
186
|
+
),
|
|
187
|
+
GateCheck(
|
|
188
|
+
id="metric.event_sequence_gap",
|
|
189
|
+
category="evidence",
|
|
190
|
+
passed=probe.event_sequence_gap_count == 0,
|
|
191
|
+
message=f"event sequence gap count = {probe.event_sequence_gap_count}",
|
|
192
|
+
metrics={
|
|
193
|
+
"event_sequence_gap_count": probe.event_sequence_gap_count,
|
|
194
|
+
"event_count": probe.event_count,
|
|
195
|
+
},
|
|
196
|
+
),
|
|
197
|
+
GateCheck(
|
|
198
|
+
id="metric.apply_rollback_failure",
|
|
199
|
+
category="evidence",
|
|
200
|
+
passed=probe.apply_rollback_failure_count == 0,
|
|
201
|
+
message=f"apply/rollback failure count = {probe.apply_rollback_failure_count}",
|
|
202
|
+
metrics={
|
|
203
|
+
"apply_rollback_failure_count": probe.apply_rollback_failure_count,
|
|
204
|
+
"task_status": probe.task_status,
|
|
205
|
+
},
|
|
206
|
+
),
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _metrics(probe: ProbeMetrics | None) -> dict[str, int | float | str | None]:
|
|
211
|
+
if probe is None:
|
|
212
|
+
return {name: None for name in ZERO_METRICS}
|
|
213
|
+
return {
|
|
214
|
+
"false_accept_count": probe.false_accept_count,
|
|
215
|
+
"policy_violation_count": probe.policy_violation_count,
|
|
216
|
+
"protected_file_change_count": probe.protected_file_change_count,
|
|
217
|
+
"replay_divergence_count": probe.replay_divergence_count,
|
|
218
|
+
"event_sequence_gap_count": probe.event_sequence_gap_count,
|
|
219
|
+
"apply_rollback_failure_count": probe.apply_rollback_failure_count,
|
|
220
|
+
"event_count": probe.event_count,
|
|
221
|
+
"task_status": probe.task_status,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def render_acceptance_markdown(report: AcceptanceReport) -> str:
|
|
226
|
+
lines = [
|
|
227
|
+
f"# P0 Acceptance Report {report.report_id}",
|
|
228
|
+
"",
|
|
229
|
+
f"- Status: `{report.status}`",
|
|
230
|
+
f"- Generated at: {report.generated_at}",
|
|
231
|
+
f"- Requirements mapped: {report.requirements_mapped}/{report.requirements_total}",
|
|
232
|
+
"",
|
|
233
|
+
"## Release Metrics",
|
|
234
|
+
"",
|
|
235
|
+
]
|
|
236
|
+
for key, value in report.metrics.items():
|
|
237
|
+
lines.append(f"- `{key}`: {value}")
|
|
238
|
+
lines.extend(["", "## Gate Checks", ""])
|
|
239
|
+
for check in report.checks:
|
|
240
|
+
state = "PASS" if check.passed else "FAIL"
|
|
241
|
+
lines.append(f"- [{state}] `{check.id}` ({check.category}): {check.message}")
|
|
242
|
+
if check.output_tail:
|
|
243
|
+
lines.extend(["", "```text", check.output_tail, "```", ""])
|
|
244
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Acceptance gate types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class Requirement:
|
|
10
|
+
id: str
|
|
11
|
+
title: str
|
|
12
|
+
implementation: tuple[str, ...]
|
|
13
|
+
tests: tuple[str, ...]
|
|
14
|
+
evidence: tuple[str, ...]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class GateCheck:
|
|
19
|
+
id: str
|
|
20
|
+
category: str
|
|
21
|
+
passed: bool
|
|
22
|
+
message: str
|
|
23
|
+
metrics: dict[str, float | int | str | None] = field(default_factory=dict)
|
|
24
|
+
output_tail: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class AcceptanceReport:
|
|
29
|
+
schema_version: int
|
|
30
|
+
report_id: str
|
|
31
|
+
generated_at: str
|
|
32
|
+
status: str
|
|
33
|
+
requirements_total: int
|
|
34
|
+
requirements_mapped: int
|
|
35
|
+
checks: tuple[GateCheck, ...]
|
|
36
|
+
metrics: dict[str, int | float | str | None]
|
|
37
|
+
|
|
38
|
+
def payload(self) -> dict[str, object]:
|
|
39
|
+
return {
|
|
40
|
+
"schemaVersion": self.schema_version,
|
|
41
|
+
"reportId": self.report_id,
|
|
42
|
+
"generatedAt": self.generated_at,
|
|
43
|
+
"status": self.status,
|
|
44
|
+
"requirementsTotal": self.requirements_total,
|
|
45
|
+
"requirementsMapped": self.requirements_mapped,
|
|
46
|
+
"checks": [
|
|
47
|
+
{
|
|
48
|
+
"id": check.id,
|
|
49
|
+
"category": check.category,
|
|
50
|
+
"passed": check.passed,
|
|
51
|
+
"message": check.message,
|
|
52
|
+
"metrics": check.metrics,
|
|
53
|
+
"outputTail": check.output_tail,
|
|
54
|
+
}
|
|
55
|
+
for check in self.checks
|
|
56
|
+
],
|
|
57
|
+
"metrics": self.metrics,
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Application services and use-case orchestration."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Application service exports."""
|
|
2
|
+
|
|
3
|
+
from algocode.application.services.apply_service import ApplyService
|
|
4
|
+
from algocode.application.services.baseline_service import BaselineService
|
|
5
|
+
from algocode.application.services.benchmark_service import BenchmarkService
|
|
6
|
+
from algocode.application.services.candidate_service import CandidateService
|
|
7
|
+
from algocode.application.services.correctness_service import CorrectnessService
|
|
8
|
+
from algocode.application.services.decision_service import DecisionService
|
|
9
|
+
from algocode.application.services.project_bootstrap_service import ProjectBootstrapService
|
|
10
|
+
from algocode.application.services.project_service import ProjectService
|
|
11
|
+
from algocode.application.services.report_service import ReportService
|
|
12
|
+
from algocode.application.services.task_service import TaskService
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ApplyService",
|
|
16
|
+
"BaselineService",
|
|
17
|
+
"BenchmarkService",
|
|
18
|
+
"CandidateService",
|
|
19
|
+
"CorrectnessService",
|
|
20
|
+
"DecisionService",
|
|
21
|
+
"ProjectBootstrapService",
|
|
22
|
+
"ProjectService",
|
|
23
|
+
"ReportService",
|
|
24
|
+
"TaskService",
|
|
25
|
+
]
|