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.
Files changed (165) hide show
  1. algocode/__init__.py +5 -0
  2. algocode/__main__.py +4 -0
  3. algocode/acceptance/__init__.py +12 -0
  4. algocode/acceptance/matrix.py +69 -0
  5. algocode/acceptance/probe.py +206 -0
  6. algocode/acceptance/service.py +244 -0
  7. algocode/acceptance/types.py +58 -0
  8. algocode/application/__init__.py +1 -0
  9. algocode/application/services/__init__.py +25 -0
  10. algocode/application/services/apply_service.py +177 -0
  11. algocode/application/services/baseline_service.py +243 -0
  12. algocode/application/services/benchmark_service.py +628 -0
  13. algocode/application/services/candidate_service.py +169 -0
  14. algocode/application/services/contract_service.py +619 -0
  15. algocode/application/services/correctness_service.py +387 -0
  16. algocode/application/services/decision_service.py +228 -0
  17. algocode/application/services/project_bootstrap_service.py +891 -0
  18. algocode/application/services/project_service.py +125 -0
  19. algocode/application/services/project_state.py +219 -0
  20. algocode/application/services/report_service.py +323 -0
  21. algocode/application/services/task_service.py +77 -0
  22. algocode/approval/service.py +77 -0
  23. algocode/approval/types.py +29 -0
  24. algocode/benchmark/__init__.py +17 -0
  25. algocode/benchmark/engine.py +214 -0
  26. algocode/benchmark/environment.py +105 -0
  27. algocode/benchmark/spec.py +85 -0
  28. algocode/benchmark/types.py +128 -0
  29. algocode/bootstrap.py +268 -0
  30. algocode/cli/__init__.py +1 -0
  31. algocode/cli/commands/__init__.py +1 -0
  32. algocode/cli/commands/accept.py +72 -0
  33. algocode/cli/commands/api.py +220 -0
  34. algocode/cli/commands/apply.py +94 -0
  35. algocode/cli/commands/baseline.py +100 -0
  36. algocode/cli/commands/benchmark.py +227 -0
  37. algocode/cli/commands/candidate.py +155 -0
  38. algocode/cli/commands/correctness.py +197 -0
  39. algocode/cli/commands/diff.py +73 -0
  40. algocode/cli/commands/doctor.py +149 -0
  41. algocode/cli/commands/eval.py +115 -0
  42. algocode/cli/commands/experiment.py +151 -0
  43. algocode/cli/commands/gate.py +61 -0
  44. algocode/cli/commands/init.py +133 -0
  45. algocode/cli/commands/model.py +115 -0
  46. algocode/cli/commands/optimize.py +252 -0
  47. algocode/cli/commands/provider_test.py +90 -0
  48. algocode/cli/commands/report.py +59 -0
  49. algocode/cli/commands/retry.py +107 -0
  50. algocode/cli/commands/review.py +154 -0
  51. algocode/cli/commands/status.py +136 -0
  52. algocode/cli/commands/task.py +146 -0
  53. algocode/cli/context.py +86 -0
  54. algocode/cli/main.py +79 -0
  55. algocode/cli/output.py +70 -0
  56. algocode/config/__init__.py +6 -0
  57. algocode/config/global_file.py +54 -0
  58. algocode/config/loader.py +181 -0
  59. algocode/config/model.py +193 -0
  60. algocode/context/__init__.py +1 -0
  61. algocode/context/builder.py +410 -0
  62. algocode/context/estimator.py +25 -0
  63. algocode/context/facts.py +37 -0
  64. algocode/context/types.py +158 -0
  65. algocode/correctness/__init__.py +6 -0
  66. algocode/correctness/engine.py +452 -0
  67. algocode/correctness/protected.py +47 -0
  68. algocode/correctness/spec.py +108 -0
  69. algocode/correctness/types.py +45 -0
  70. algocode/domain/__init__.py +1 -0
  71. algocode/domain/commands/__init__.py +19 -0
  72. algocode/domain/errors/__init__.py +70 -0
  73. algocode/domain/events/__init__.py +5 -0
  74. algocode/domain/events/envelope.py +67 -0
  75. algocode/domain/model/__init__.py +76 -0
  76. algocode/domain/model/entities.py +151 -0
  77. algocode/domain/model/enums.py +119 -0
  78. algocode/domain/model/ids.py +36 -0
  79. algocode/domain/model/values.py +18 -0
  80. algocode/eval/__init__.py +1 -0
  81. algocode/eval/harness.py +377 -0
  82. algocode/eval/provider.py +135 -0
  83. algocode/eval/service.py +209 -0
  84. algocode/eval/suites.py +121 -0
  85. algocode/eval/types.py +76 -0
  86. algocode/languages/__init__.py +7 -0
  87. algocode/languages/cpp.py +288 -0
  88. algocode/languages/discovery.py +29 -0
  89. algocode/languages/python.py +94 -0
  90. algocode/languages/registry.py +60 -0
  91. algocode/languages/runner.py +68 -0
  92. algocode/languages/types.py +88 -0
  93. algocode/observability/__init__.py +1 -0
  94. algocode/policy/__init__.py +1 -0
  95. algocode/policy/engine.py +111 -0
  96. algocode/policy/types.py +44 -0
  97. algocode/ports/__init__.py +22 -0
  98. algocode/ports/artifact_store.py +22 -0
  99. algocode/ports/clock.py +12 -0
  100. algocode/ports/event_store.py +23 -0
  101. algocode/ports/language.py +30 -0
  102. algocode/ports/policy.py +7 -0
  103. algocode/ports/provider.py +12 -0
  104. algocode/ports/resource.py +7 -0
  105. algocode/ports/workspace.py +19 -0
  106. algocode/project_layout.py +135 -0
  107. algocode/providers/__init__.py +36 -0
  108. algocode/providers/anthropic.py +264 -0
  109. algocode/providers/errors.py +58 -0
  110. algocode/providers/factory.py +86 -0
  111. algocode/providers/fake.py +148 -0
  112. algocode/providers/openai_compatible.py +353 -0
  113. algocode/providers/types.py +101 -0
  114. algocode/report/__init__.py +1 -0
  115. algocode/resources/__init__.py +6 -0
  116. algocode/resources/local.py +235 -0
  117. algocode/resources/types.py +35 -0
  118. algocode/runtime/__init__.py +1 -0
  119. algocode/runtime/agent.py +2238 -0
  120. algocode/runtime/analysis.py +187 -0
  121. algocode/runtime/gates.py +69 -0
  122. algocode/runtime/locks.py +61 -0
  123. algocode/runtime/model_log.py +265 -0
  124. algocode/runtime/optimization_record.py +332 -0
  125. algocode/runtime/planning.py +73 -0
  126. algocode/runtime/repair_memory.py +195 -0
  127. algocode/runtime/task_lock.py +61 -0
  128. algocode/sandbox/local.py +71 -0
  129. algocode/sandbox/runner.py +393 -0
  130. algocode/security/__init__.py +6 -0
  131. algocode/security/credentials.py +75 -0
  132. algocode/security/redaction.py +83 -0
  133. algocode/storage/__init__.py +1 -0
  134. algocode/storage/artifacts/__init__.py +5 -0
  135. algocode/storage/artifacts/file_artifact_store.py +90 -0
  136. algocode/storage/events/__init__.py +5 -0
  137. algocode/storage/events/sqlite_event_store.py +135 -0
  138. algocode/storage/paths.py +27 -0
  139. algocode/storage/sqlite/__init__.py +5 -0
  140. algocode/storage/sqlite/approval_store.py +65 -0
  141. algocode/storage/sqlite/database.py +51 -0
  142. algocode/storage/sqlite/migrations.py +298 -0
  143. algocode/storage/sqlite/projections/__init__.py +12 -0
  144. algocode/storage/sqlite/projections/baseline_projection.py +137 -0
  145. algocode/storage/sqlite/projections/benchmark_projection.py +210 -0
  146. algocode/storage/sqlite/projections/candidate_projection.py +177 -0
  147. algocode/storage/sqlite/projections/correctness_projection.py +148 -0
  148. algocode/storage/sqlite/projections/decision_projection.py +95 -0
  149. algocode/storage/sqlite/projections/project_projection.py +84 -0
  150. algocode/storage/sqlite/projections/task_projection.py +194 -0
  151. algocode/structured_output.py +153 -0
  152. algocode/tools/__init__.py +26 -0
  153. algocode/tools/builtins/__init__.py +1 -0
  154. algocode/tools/builtins/actions.py +707 -0
  155. algocode/tools/builtins/filesystem.py +459 -0
  156. algocode/tools/registry.py +201 -0
  157. algocode/tools/types.py +57 -0
  158. algocode/workspace/__init__.py +12 -0
  159. algocode/workspace/git.py +470 -0
  160. algocode/workspace/types.py +31 -0
  161. algocode_agent-0.1.0.dist-info/METADATA +678 -0
  162. algocode_agent-0.1.0.dist-info/RECORD +165 -0
  163. algocode_agent-0.1.0.dist-info/WHEEL +4 -0
  164. algocode_agent-0.1.0.dist-info/entry_points.txt +2 -0
  165. algocode_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
algocode/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Algocode package."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
algocode/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from algocode.cli.main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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
+ ]