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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Apply accepted candidates to the user workspace and roll them back."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from algocode.application.services.baseline_service import BaselineService
|
|
9
|
+
from algocode.application.services.candidate_service import CandidateService
|
|
10
|
+
from algocode.application.services.decision_service import DecisionService
|
|
11
|
+
from algocode.application.services.project_service import ProjectService
|
|
12
|
+
from algocode.application.services.task_service import TaskService
|
|
13
|
+
from algocode.domain.errors import DecisionError
|
|
14
|
+
from algocode.domain.events import EventEnvelope, EventType
|
|
15
|
+
from algocode.domain.model import CandidateId, CandidateStatus, DecisionOutcome, TaskId
|
|
16
|
+
from algocode.ports import ArtifactStore, EventStore
|
|
17
|
+
from algocode.workspace import GitRepository, GitWorkspaceManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ApplyService:
|
|
21
|
+
"""Apply or roll back accepted candidates with stale checks."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
event_store: EventStore,
|
|
26
|
+
task_service: TaskService,
|
|
27
|
+
project_service: ProjectService,
|
|
28
|
+
baseline_service: BaselineService,
|
|
29
|
+
candidate_service: CandidateService,
|
|
30
|
+
decision_service: DecisionService,
|
|
31
|
+
artifact_store: ArtifactStore,
|
|
32
|
+
data_dir: str | Path,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._event_store = event_store
|
|
35
|
+
self._task_service = task_service
|
|
36
|
+
self._project_service = project_service
|
|
37
|
+
self._baseline_service = baseline_service
|
|
38
|
+
self._candidate_service = candidate_service
|
|
39
|
+
self._decision_service = decision_service
|
|
40
|
+
self._artifact_store = artifact_store
|
|
41
|
+
self._data_dir = Path(data_dir)
|
|
42
|
+
|
|
43
|
+
async def apply(
|
|
44
|
+
self,
|
|
45
|
+
task_id: TaskId | str,
|
|
46
|
+
candidate_id: CandidateId | str,
|
|
47
|
+
) -> dict[str, object]:
|
|
48
|
+
task = await self._task_service.get_task(task_id)
|
|
49
|
+
candidate = await self._candidate_service.get(candidate_id)
|
|
50
|
+
if candidate.task_id != task.id:
|
|
51
|
+
raise DecisionError("candidate does not belong to the task")
|
|
52
|
+
decision = await self._decision_service.get_for_candidate(candidate.id)
|
|
53
|
+
if decision is None or decision.outcome is not DecisionOutcome.ACCEPTED:
|
|
54
|
+
raise DecisionError("candidate must be accepted before apply")
|
|
55
|
+
if candidate.status is CandidateStatus.APPLIED:
|
|
56
|
+
raise DecisionError("candidate is already applied")
|
|
57
|
+
baseline = await self._baseline_service.get_for_task(task.id)
|
|
58
|
+
if baseline is None:
|
|
59
|
+
raise DecisionError("task baseline is required before apply")
|
|
60
|
+
apply_base_revision = (
|
|
61
|
+
candidate.apply_base_revision.value
|
|
62
|
+
if candidate.apply_base_revision is not None
|
|
63
|
+
else baseline.revision.value
|
|
64
|
+
)
|
|
65
|
+
apply_base_snapshot_hash = candidate.apply_base_snapshot_hash or baseline.snapshot_hash
|
|
66
|
+
project = await self._project_service.get(task.project_id)
|
|
67
|
+
repository = await GitRepository.discover(project.root_path)
|
|
68
|
+
current = await repository.capture_snapshot(repository.root)
|
|
69
|
+
if (
|
|
70
|
+
current.revision != apply_base_revision
|
|
71
|
+
or current.snapshot_hash != apply_base_snapshot_hash
|
|
72
|
+
):
|
|
73
|
+
await self._mark_stale(task.id, candidate.id)
|
|
74
|
+
raise DecisionError("candidate is stale; re-verify before apply")
|
|
75
|
+
candidate_snapshot = await repository.capture_snapshot(Path(candidate.workspace_ref))
|
|
76
|
+
patch_ref = await self._artifact_store.put(
|
|
77
|
+
candidate_snapshot.patch,
|
|
78
|
+
kind="candidate-patch",
|
|
79
|
+
mime_type="application/x-git-patch",
|
|
80
|
+
metadata={"task_id": str(task.id), "candidate_id": str(candidate.id)},
|
|
81
|
+
)
|
|
82
|
+
manager = GitWorkspaceManager(repository, self._data_dir)
|
|
83
|
+
workspace = await self._candidate_service.workspace(candidate.id)
|
|
84
|
+
result = await manager.apply(workspace)
|
|
85
|
+
if not result.applied:
|
|
86
|
+
raise DecisionError(result.message or "candidate apply failed")
|
|
87
|
+
applied_snapshot = await repository.capture_snapshot(repository.root)
|
|
88
|
+
seq = await self._next_seq(str(task.id))
|
|
89
|
+
await self._event_store.append(
|
|
90
|
+
str(task.id),
|
|
91
|
+
seq - 1,
|
|
92
|
+
(
|
|
93
|
+
_event(
|
|
94
|
+
str(task.id),
|
|
95
|
+
seq,
|
|
96
|
+
EventType.CANDIDATE_APPLIED,
|
|
97
|
+
{
|
|
98
|
+
"candidate_id": str(candidate.id),
|
|
99
|
+
"workspace_id": workspace.id,
|
|
100
|
+
"reverse_patch_ref": _artifact_payload(patch_ref),
|
|
101
|
+
"applied_snapshot_hash": applied_snapshot.snapshot_hash,
|
|
102
|
+
},
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
return {
|
|
107
|
+
"candidate_id": str(candidate.id),
|
|
108
|
+
"applied": True,
|
|
109
|
+
"patch_ref": _artifact_payload(patch_ref),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async def rollback(
|
|
113
|
+
self,
|
|
114
|
+
task_id: TaskId | str,
|
|
115
|
+
candidate_id: CandidateId | str,
|
|
116
|
+
) -> dict[str, object]:
|
|
117
|
+
task = await self._task_service.get_task(task_id)
|
|
118
|
+
candidate = await self._candidate_service.get(candidate_id)
|
|
119
|
+
if candidate.status is not CandidateStatus.APPLIED:
|
|
120
|
+
raise DecisionError("candidate is not applied")
|
|
121
|
+
project = await self._project_service.get(task.project_id)
|
|
122
|
+
repository = await GitRepository.discover(project.root_path)
|
|
123
|
+
manager = GitWorkspaceManager(repository, self._data_dir)
|
|
124
|
+
workspace = await self._candidate_service.workspace(candidate.id)
|
|
125
|
+
await manager.rollback(workspace)
|
|
126
|
+
seq = await self._next_seq(str(task.id))
|
|
127
|
+
await self._event_store.append(
|
|
128
|
+
str(task.id),
|
|
129
|
+
seq - 1,
|
|
130
|
+
(
|
|
131
|
+
_event(
|
|
132
|
+
str(task.id),
|
|
133
|
+
seq,
|
|
134
|
+
EventType.CANDIDATE_ROLLED_BACK,
|
|
135
|
+
{"candidate_id": str(candidate.id), "workspace_id": workspace.id},
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
return {"candidate_id": str(candidate.id), "rolled_back": True}
|
|
140
|
+
|
|
141
|
+
async def _mark_stale(self, task_id: TaskId, candidate_id: CandidateId) -> None:
|
|
142
|
+
seq = await self._next_seq(str(task_id))
|
|
143
|
+
await self._event_store.append(
|
|
144
|
+
str(task_id),
|
|
145
|
+
seq - 1,
|
|
146
|
+
(
|
|
147
|
+
_event(
|
|
148
|
+
str(task_id),
|
|
149
|
+
seq,
|
|
150
|
+
EventType.CANDIDATE_REJECTED,
|
|
151
|
+
{"candidate_id": str(candidate_id), "status": CandidateStatus.STALE.value},
|
|
152
|
+
),
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def _next_seq(self, aggregate_id: str) -> int:
|
|
157
|
+
events = await self._event_store.read(aggregate_id)
|
|
158
|
+
return events[-1].seq + 1 if events else 1
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _event(
|
|
162
|
+
aggregate_id: str,
|
|
163
|
+
seq: int,
|
|
164
|
+
event_type: EventType,
|
|
165
|
+
payload: dict[str, object],
|
|
166
|
+
) -> EventEnvelope:
|
|
167
|
+
return EventEnvelope(
|
|
168
|
+
id=f"evt_{uuid4().hex}",
|
|
169
|
+
aggregate_id=aggregate_id,
|
|
170
|
+
seq=seq,
|
|
171
|
+
type=event_type,
|
|
172
|
+
payload=payload,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _artifact_payload(ref) -> dict[str, str]:
|
|
177
|
+
return {"uri": ref.uri, "sha256": ref.sha256}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Baseline capture orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from algocode.application.services.project_service import ProjectService
|
|
11
|
+
from algocode.application.services.task_service import TaskService
|
|
12
|
+
from algocode.benchmark.environment import compute_environment_hash
|
|
13
|
+
from algocode.domain.errors import BaselineBuildFailed, BaselineError, NotFoundError
|
|
14
|
+
from algocode.domain.events import EventEnvelope, EventType
|
|
15
|
+
from algocode.domain.model import Baseline, GitRevision, Language, TaskId
|
|
16
|
+
from algocode.languages import LanguageRegistry
|
|
17
|
+
from algocode.languages.types import BuildProfile, BuildResult
|
|
18
|
+
from algocode.ports import ArtifactStore, EventStore
|
|
19
|
+
from algocode.storage.sqlite.database import Database
|
|
20
|
+
from algocode.storage.sqlite.projections.baseline_projection import BaselineProjection
|
|
21
|
+
from algocode.workspace import GitRepository, GitWorkspaceManager
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaselineService:
|
|
25
|
+
"""Prepare an isolated workspace, build it, and persist a baseline."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
event_store: EventStore,
|
|
30
|
+
database: Database,
|
|
31
|
+
task_service: TaskService,
|
|
32
|
+
project_service: ProjectService,
|
|
33
|
+
baseline_projection: BaselineProjection,
|
|
34
|
+
language_registry: LanguageRegistry,
|
|
35
|
+
artifact_store: ArtifactStore,
|
|
36
|
+
workspace_manager_factory: Callable[[GitRepository], GitWorkspaceManager],
|
|
37
|
+
) -> None:
|
|
38
|
+
self._event_store = event_store
|
|
39
|
+
self._database = database
|
|
40
|
+
self._task_service = task_service
|
|
41
|
+
self._project_service = project_service
|
|
42
|
+
self._baseline_projection = baseline_projection
|
|
43
|
+
self._language_registry = language_registry
|
|
44
|
+
self._artifact_store = artifact_store
|
|
45
|
+
self._workspace_manager_factory = workspace_manager_factory
|
|
46
|
+
|
|
47
|
+
async def capture(
|
|
48
|
+
self,
|
|
49
|
+
task_id: TaskId | str,
|
|
50
|
+
*,
|
|
51
|
+
language: Language | str = Language.AUTO,
|
|
52
|
+
source_root: str = ".",
|
|
53
|
+
commands: tuple[tuple[str, ...], ...] = (),
|
|
54
|
+
timeout_seconds: int = 120,
|
|
55
|
+
) -> Baseline:
|
|
56
|
+
task = await self._task_service.get_task(task_id)
|
|
57
|
+
project = await self._project_service.get(task.project_id)
|
|
58
|
+
repository = await GitRepository.discover(project.root_path)
|
|
59
|
+
revision = await repository.head_revision()
|
|
60
|
+
started_seq = await self._next_seq(str(task.id))
|
|
61
|
+
await self._event_store.append(
|
|
62
|
+
str(task.id),
|
|
63
|
+
started_seq - 1,
|
|
64
|
+
(
|
|
65
|
+
_event(
|
|
66
|
+
str(task.id),
|
|
67
|
+
started_seq,
|
|
68
|
+
EventType.BASELINE_STARTED,
|
|
69
|
+
{"revision": revision, "project_id": str(project.id)},
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
workspace_manager = self._workspace_manager_factory(repository)
|
|
75
|
+
try:
|
|
76
|
+
workspace = await workspace_manager.prepare_baseline(str(task.id), revision)
|
|
77
|
+
detected = await self._language_registry.detect(workspace.path)
|
|
78
|
+
requested_language = Language(language)
|
|
79
|
+
if requested_language is Language.AUTO:
|
|
80
|
+
requested_language = (
|
|
81
|
+
project.language if project.language is not Language.AUTO else Language.AUTO
|
|
82
|
+
)
|
|
83
|
+
selected_language = self._language_registry.resolve_language(
|
|
84
|
+
requested_language,
|
|
85
|
+
detected,
|
|
86
|
+
)
|
|
87
|
+
adapter = self._language_registry.adapter_for(selected_language)
|
|
88
|
+
profile = BuildProfile(
|
|
89
|
+
commands=commands,
|
|
90
|
+
source_root=source_root,
|
|
91
|
+
timeout_seconds=timeout_seconds,
|
|
92
|
+
)
|
|
93
|
+
await adapter.prepare(workspace.path, profile)
|
|
94
|
+
build_result = await adapter.build(workspace.path, profile)
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
failed_seq = started_seq + 1
|
|
97
|
+
await self._event_store.append(
|
|
98
|
+
str(task.id),
|
|
99
|
+
started_seq,
|
|
100
|
+
(
|
|
101
|
+
_event(
|
|
102
|
+
str(task.id),
|
|
103
|
+
failed_seq,
|
|
104
|
+
EventType.BASELINE_FAILED,
|
|
105
|
+
{
|
|
106
|
+
"revision": revision,
|
|
107
|
+
"reason": str(exc),
|
|
108
|
+
},
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
if isinstance(exc, BaselineError):
|
|
113
|
+
raise
|
|
114
|
+
raise BaselineError(str(exc)) from exc
|
|
115
|
+
|
|
116
|
+
output_ref = await self._artifact_store.put(
|
|
117
|
+
build_result.output,
|
|
118
|
+
kind="baseline-build-output",
|
|
119
|
+
mime_type="text/plain",
|
|
120
|
+
metadata={"task_id": str(task.id), "revision": revision},
|
|
121
|
+
)
|
|
122
|
+
metadata_ref = await self._artifact_store.put(
|
|
123
|
+
_build_metadata(build_result),
|
|
124
|
+
kind="baseline-build-result",
|
|
125
|
+
mime_type="application/json",
|
|
126
|
+
metadata={"task_id": str(task.id), "revision": revision},
|
|
127
|
+
)
|
|
128
|
+
if not build_result.succeeded:
|
|
129
|
+
failed_seq = started_seq + 1
|
|
130
|
+
await self._event_store.append(
|
|
131
|
+
str(task.id),
|
|
132
|
+
started_seq,
|
|
133
|
+
(
|
|
134
|
+
_event(
|
|
135
|
+
str(task.id),
|
|
136
|
+
failed_seq,
|
|
137
|
+
EventType.BASELINE_FAILED,
|
|
138
|
+
{
|
|
139
|
+
"revision": revision,
|
|
140
|
+
"reason": f"baseline build exited with {build_result.exit_code}",
|
|
141
|
+
"build_result_ref": _artifact_payload(output_ref),
|
|
142
|
+
},
|
|
143
|
+
artifact_refs=(output_ref, metadata_ref),
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
raise BaselineBuildFailed(f"baseline build exited with code {build_result.exit_code}")
|
|
148
|
+
|
|
149
|
+
now = datetime.now(UTC)
|
|
150
|
+
baseline = Baseline(
|
|
151
|
+
id=f"base_{uuid4().hex}",
|
|
152
|
+
task_id=task.id,
|
|
153
|
+
revision=GitRevision(revision),
|
|
154
|
+
snapshot_hash=workspace.base_snapshot_hash,
|
|
155
|
+
environment_hash=compute_environment_hash(workspace.path),
|
|
156
|
+
build_result_ref=output_ref,
|
|
157
|
+
workspace_ref=str(workspace.path),
|
|
158
|
+
created_at=now,
|
|
159
|
+
)
|
|
160
|
+
captured_seq = started_seq + 1
|
|
161
|
+
await self._event_store.append(
|
|
162
|
+
str(task.id),
|
|
163
|
+
started_seq,
|
|
164
|
+
(
|
|
165
|
+
_event(
|
|
166
|
+
str(task.id),
|
|
167
|
+
captured_seq,
|
|
168
|
+
EventType.BASELINE_CAPTURED,
|
|
169
|
+
{
|
|
170
|
+
"baseline_id": baseline.id,
|
|
171
|
+
"revision": baseline.revision.value,
|
|
172
|
+
"snapshot_hash": baseline.snapshot_hash,
|
|
173
|
+
"environment_hash": baseline.environment_hash,
|
|
174
|
+
"build_result_ref": _artifact_payload(output_ref),
|
|
175
|
+
"correctness_result_ref": None,
|
|
176
|
+
"benchmark_result_ref": None,
|
|
177
|
+
"created_at": baseline.created_at.isoformat(),
|
|
178
|
+
"workspace_ref": str(workspace.path),
|
|
179
|
+
},
|
|
180
|
+
artifact_refs=(output_ref, metadata_ref),
|
|
181
|
+
),
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
return baseline
|
|
185
|
+
|
|
186
|
+
async def get(self, baseline_id: str) -> Baseline:
|
|
187
|
+
with self._database.connect() as connection:
|
|
188
|
+
baseline = self._baseline_projection.get(connection, baseline_id)
|
|
189
|
+
if baseline is None:
|
|
190
|
+
raise NotFoundError(f"baseline {baseline_id} was not found")
|
|
191
|
+
return baseline
|
|
192
|
+
|
|
193
|
+
async def get_for_task(self, task_id: TaskId | str) -> Baseline | None:
|
|
194
|
+
with self._database.connect() as connection:
|
|
195
|
+
return self._baseline_projection.get_for_task(connection, str(task_id))
|
|
196
|
+
|
|
197
|
+
async def _next_seq(self, aggregate_id: str) -> int:
|
|
198
|
+
events = await self._event_store.read(aggregate_id)
|
|
199
|
+
return events[-1].seq + 1 if events else 1
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _event(
|
|
203
|
+
aggregate_id: str,
|
|
204
|
+
seq: int,
|
|
205
|
+
event_type: EventType,
|
|
206
|
+
payload: dict[str, object],
|
|
207
|
+
*,
|
|
208
|
+
artifact_refs=(),
|
|
209
|
+
) -> EventEnvelope:
|
|
210
|
+
return EventEnvelope(
|
|
211
|
+
id=f"evt_{uuid4().hex}",
|
|
212
|
+
aggregate_id=aggregate_id,
|
|
213
|
+
seq=seq,
|
|
214
|
+
type=event_type,
|
|
215
|
+
payload=payload,
|
|
216
|
+
artifact_refs=tuple(artifact_refs),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _artifact_payload(ref) -> dict[str, str]:
|
|
221
|
+
return {"uri": ref.uri, "sha256": ref.sha256}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _build_metadata(result: BuildResult) -> bytes:
|
|
225
|
+
payload = {
|
|
226
|
+
"language": result.language.value,
|
|
227
|
+
"commands": [list(command) for command in result.commands],
|
|
228
|
+
"run_command": list(result.run_command),
|
|
229
|
+
"executable": str(result.executable) if result.executable is not None else None,
|
|
230
|
+
"exit_code": result.exit_code,
|
|
231
|
+
"duration_seconds": result.duration_seconds,
|
|
232
|
+
"diagnostics": [
|
|
233
|
+
{
|
|
234
|
+
"severity": diagnostic.severity,
|
|
235
|
+
"message": diagnostic.message,
|
|
236
|
+
"file": diagnostic.file,
|
|
237
|
+
"line": diagnostic.line,
|
|
238
|
+
"column": diagnostic.column,
|
|
239
|
+
}
|
|
240
|
+
for diagnostic in result.diagnostics
|
|
241
|
+
],
|
|
242
|
+
}
|
|
243
|
+
return json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
|