nexus-coding-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.
- nexus/__init__.py +4 -0
- nexus/application/__init__.py +2 -0
- nexus/application/approval_service.py +130 -0
- nexus/application/diff_service.py +177 -0
- nexus/application/event_publisher.py +180 -0
- nexus/application/execution_context.py +29 -0
- nexus/application/execution_ledger.py +126 -0
- nexus/application/model_call_context.py +29 -0
- nexus/application/observed_model_gateway.py +194 -0
- nexus/application/plan_approval_service.py +250 -0
- nexus/application/planning.py +668 -0
- nexus/application/runtime.py +624 -0
- nexus/application/session_service.py +411 -0
- nexus/application/structured_output.py +96 -0
- nexus/application/telemetry.py +396 -0
- nexus/application/telemetry_subscriber.py +216 -0
- nexus/application/tool_runtime.py +496 -0
- nexus/application/tracing_dispatcher.py +283 -0
- nexus/application/validation.py +293 -0
- nexus/config/__init__.py +17 -0
- nexus/config/loader.py +253 -0
- nexus/config/models.py +310 -0
- nexus/context/__init__.py +6 -0
- nexus/context/budget.py +24 -0
- nexus/context/builder.py +92 -0
- nexus/context/chunking.py +65 -0
- nexus/context/explorer.py +246 -0
- nexus/context/integration.py +63 -0
- nexus/context/manager.py +328 -0
- nexus/context/retrieval.py +261 -0
- nexus/domain/__init__.py +2 -0
- nexus/domain/agent_decision.py +113 -0
- nexus/domain/agent_state.py +49 -0
- nexus/domain/approvals.py +64 -0
- nexus/domain/context.py +108 -0
- nexus/domain/exploration.py +86 -0
- nexus/domain/mcp.py +51 -0
- nexus/domain/model.py +173 -0
- nexus/domain/observability.py +326 -0
- nexus/domain/persistence.py +122 -0
- nexus/domain/planning.py +329 -0
- nexus/domain/ports/__init__.py +61 -0
- nexus/domain/ports/agent_decision.py +9 -0
- nexus/domain/ports/approval_unit_of_work.py +27 -0
- nexus/domain/ports/checkpoint_provider.py +11 -0
- nexus/domain/ports/context.py +71 -0
- nexus/domain/ports/events.py +21 -0
- nexus/domain/ports/graph_runtime.py +30 -0
- nexus/domain/ports/mcp.py +23 -0
- nexus/domain/ports/model_gateway.py +21 -0
- nexus/domain/ports/model_input_budget.py +15 -0
- nexus/domain/ports/planning.py +62 -0
- nexus/domain/ports/repositories.py +54 -0
- nexus/domain/ports/repository_context.py +18 -0
- nexus/domain/ports/session_unit_of_work.py +33 -0
- nexus/domain/ports/skills.py +41 -0
- nexus/domain/ports/tooling.py +39 -0
- nexus/domain/ports/tracer.py +13 -0
- nexus/domain/ports/validation.py +32 -0
- nexus/domain/runtime_events.py +445 -0
- nexus/domain/skills.py +129 -0
- nexus/domain/tooling.py +163 -0
- nexus/domain/validation.py +161 -0
- nexus/errors/__init__.py +82 -0
- nexus/evaluation/__init__.py +7 -0
- nexus/evaluation/assertions.py +4 -0
- nexus/evaluation/evaluator.py +263 -0
- nexus/evaluation/fixtures.py +36 -0
- nexus/evaluation/harness.py +606 -0
- nexus/evaluation/loader.py +415 -0
- nexus/evaluation/metrics.py +27 -0
- nexus/evaluation/models.py +362 -0
- nexus/evaluation/report.py +43 -0
- nexus/evaluation/runner.py +480 -0
- nexus/infrastructure/__init__.py +2 -0
- nexus/infrastructure/asyncio_compat.py +13 -0
- nexus/infrastructure/bootstrap/__init__.py +17 -0
- nexus/infrastructure/bootstrap/composition.py +663 -0
- nexus/infrastructure/checkpoint/__init__.py +5 -0
- nexus/infrastructure/checkpoint/postgres.py +62 -0
- nexus/infrastructure/database.py +39 -0
- nexus/infrastructure/embedding.py +61 -0
- nexus/infrastructure/graph/__init__.py +6 -0
- nexus/infrastructure/graph/day4_runtime.py +966 -0
- nexus/infrastructure/graph/langgraph_runtime.py +172 -0
- nexus/infrastructure/mcp/__init__.py +5 -0
- nexus/infrastructure/mcp/manager.py +380 -0
- nexus/infrastructure/model_gateway/__init__.py +6 -0
- nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
- nexus/infrastructure/observability/__init__.py +7 -0
- nexus/infrastructure/observability/console.py +31 -0
- nexus/infrastructure/observability/langsmith.py +236 -0
- nexus/infrastructure/observability/structured_logger.py +70 -0
- nexus/infrastructure/persistence/__init__.py +13 -0
- nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
- nexus/infrastructure/persistence/models.py +146 -0
- nexus/infrastructure/persistence/repositories.py +326 -0
- nexus/infrastructure/persistence/unit_of_work.py +82 -0
- nexus/infrastructure/repository_files.py +117 -0
- nexus/infrastructure/sandbox/__init__.py +5 -0
- nexus/infrastructure/sandbox/local_process.py +353 -0
- nexus/infrastructure/semantic.py +276 -0
- nexus/infrastructure/semantic_models.py +56 -0
- nexus/interfaces/__init__.py +2 -0
- nexus/interfaces/cli/__init__.py +2 -0
- nexus/interfaces/cli/__main__.py +6 -0
- nexus/interfaces/cli/app.py +266 -0
- nexus/interfaces/cli/renderer.py +118 -0
- nexus/security/__init__.py +14 -0
- nexus/security/approval_policies.py +73 -0
- nexus/security/command_policy.py +117 -0
- nexus/security/executables.py +107 -0
- nexus/security/git_commands.py +65 -0
- nexus/security/workspace.py +93 -0
- nexus/skills/__init__.py +7 -0
- nexus/skills/builtin/__init__.py +1 -0
- nexus/skills/builtin/debug-python/SKILL.md +26 -0
- nexus/skills/builtin/review-repository/SKILL.md +26 -0
- nexus/skills/builtin/write-tests/SKILL.md +26 -0
- nexus/skills/loader.py +439 -0
- nexus/skills/registry.py +146 -0
- nexus/skills/selector.py +152 -0
- nexus/tools/__init__.py +6 -0
- nexus/tools/editing.py +312 -0
- nexus/tools/mcp.py +160 -0
- nexus/tools/native.py +654 -0
- nexus/tools/registry.py +27 -0
- nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
- nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
- nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- nexus_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Frozen Day 9 evaluation contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
|
|
13
|
+
from nexus.domain.model import TokenUsage
|
|
14
|
+
from nexus.domain.observability import TraceRunFinish
|
|
15
|
+
from nexus.domain.planning import TerminalStatus
|
|
16
|
+
from nexus.domain.runtime_events import RuntimeStatus
|
|
17
|
+
from nexus.domain.tooling import JsonObject
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class RequiredFact:
|
|
22
|
+
fact_id: str
|
|
23
|
+
description: str
|
|
24
|
+
required_terms: tuple[str, ...]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EvalAssertionType(StrEnum):
|
|
28
|
+
VALIDATION_PASSES = "validation_passes"
|
|
29
|
+
NO_UNAUTHORIZED_CHANGES = "no_unauthorized_changes"
|
|
30
|
+
NO_FORBIDDEN_CHANGES = "no_forbidden_changes"
|
|
31
|
+
NO_REPOSITORY_CHANGES = "no_repository_changes"
|
|
32
|
+
REQUIRED_PATH_CHANGED = "required_path_changed"
|
|
33
|
+
REQUIRED_PATH_CREATED = "required_path_created"
|
|
34
|
+
REQUIRED_TEXT_IN_FILE = "required_text_in_file"
|
|
35
|
+
REQUIRED_FINAL_FACTS = "required_final_facts"
|
|
36
|
+
REQUIRED_SECURITY_EVIDENCE = "required_security_evidence"
|
|
37
|
+
TARGETED_TEST_ADDED = "targeted_test_added"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class EvalAssertion:
|
|
42
|
+
assertion_type: EvalAssertionType
|
|
43
|
+
path: str | None = None
|
|
44
|
+
expected_text: str | None = None
|
|
45
|
+
required_facts: tuple[RequiredFact, ...] = ()
|
|
46
|
+
security_codes: tuple[str, ...] = ()
|
|
47
|
+
expected_test_node_ids: tuple[str, ...] = ()
|
|
48
|
+
metadata: JsonObject | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class EvalCase:
|
|
53
|
+
case_id: str
|
|
54
|
+
initial_repo_state: str
|
|
55
|
+
task: str
|
|
56
|
+
allowed_files: tuple[str, ...]
|
|
57
|
+
forbidden_files: tuple[str, ...]
|
|
58
|
+
pre_validation_command: tuple[str, ...]
|
|
59
|
+
validation_command: tuple[str, ...]
|
|
60
|
+
validation_timeout_seconds: int | None
|
|
61
|
+
test_discovery_command: tuple[str, ...]
|
|
62
|
+
expected_behavior: str
|
|
63
|
+
deterministic_success_conditions: tuple[EvalAssertion, ...]
|
|
64
|
+
max_reasonable_steps: int
|
|
65
|
+
metadata: JsonObject | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class FileFingerprint:
|
|
70
|
+
path: str
|
|
71
|
+
kind: str
|
|
72
|
+
content_sha256: str | None
|
|
73
|
+
executable: bool
|
|
74
|
+
symlink_target: str | None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class ForbiddenPathSnapshot:
|
|
79
|
+
path: str
|
|
80
|
+
exists: bool
|
|
81
|
+
kind: str | None
|
|
82
|
+
content_sha256: str | None
|
|
83
|
+
executable: bool | None
|
|
84
|
+
symlink_target: str | None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class RepositoryState:
|
|
89
|
+
files: Mapping[str, FileFingerprint]
|
|
90
|
+
forbidden_snapshots: Mapping[str, ForbiddenPathSnapshot]
|
|
91
|
+
|
|
92
|
+
def __post_init__(self) -> None:
|
|
93
|
+
object.__setattr__(self, "files", MappingProxyType(dict(self.files)))
|
|
94
|
+
object.__setattr__(
|
|
95
|
+
self,
|
|
96
|
+
"forbidden_snapshots",
|
|
97
|
+
MappingProxyType(dict(self.forbidden_snapshots)),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True, slots=True)
|
|
102
|
+
class EvalForbiddenPathEvidence:
|
|
103
|
+
path: str
|
|
104
|
+
before: ForbiddenPathSnapshot
|
|
105
|
+
after: ForbiddenPathSnapshot
|
|
106
|
+
changed: bool
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True, slots=True)
|
|
110
|
+
class EvalRepositoryEvidence:
|
|
111
|
+
changed_files: tuple[str, ...]
|
|
112
|
+
created_files: tuple[str, ...]
|
|
113
|
+
deleted_files: tuple[str, ...]
|
|
114
|
+
forbidden_paths: tuple[EvalForbiddenPathEvidence, ...]
|
|
115
|
+
final_text_files: Mapping[str, str]
|
|
116
|
+
|
|
117
|
+
def __post_init__(self) -> None:
|
|
118
|
+
object.__setattr__(self, "changed_files", tuple(self.changed_files))
|
|
119
|
+
object.__setattr__(self, "created_files", tuple(self.created_files))
|
|
120
|
+
object.__setattr__(self, "deleted_files", tuple(self.deleted_files))
|
|
121
|
+
object.__setattr__(self, "forbidden_paths", tuple(self.forbidden_paths))
|
|
122
|
+
object.__setattr__(self, "final_text_files", MappingProxyType(dict(self.final_text_files)))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class EvalHarnessOperation(StrEnum):
|
|
126
|
+
GIT_INIT = "GIT_INIT"
|
|
127
|
+
GIT_ADD_BASELINE = "GIT_ADD_BASELINE"
|
|
128
|
+
GIT_COMMIT_BASELINE = "GIT_COMMIT_BASELINE"
|
|
129
|
+
GIT_STATUS = "GIT_STATUS"
|
|
130
|
+
GIT_DIFF = "GIT_DIFF"
|
|
131
|
+
GIT_LS_FILES = "GIT_LS_FILES"
|
|
132
|
+
VALIDATION = "VALIDATION"
|
|
133
|
+
TEST_DISCOVERY = "TEST_DISCOVERY"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=True)
|
|
137
|
+
class EvalHarnessCommand:
|
|
138
|
+
operation: EvalHarnessOperation
|
|
139
|
+
argv: tuple[str, ...]
|
|
140
|
+
cwd: Path
|
|
141
|
+
timeout_seconds: int
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True, slots=True)
|
|
145
|
+
class EvalHarnessCommandResult:
|
|
146
|
+
operation: EvalHarnessOperation
|
|
147
|
+
allowed: bool
|
|
148
|
+
denial_code: str | None
|
|
149
|
+
exit_code: int | None
|
|
150
|
+
stdout_summary: str
|
|
151
|
+
stderr_summary: str
|
|
152
|
+
duration_ms: int
|
|
153
|
+
timed_out: bool
|
|
154
|
+
infrastructure_error: str | None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SecurityEvidenceSource(StrEnum):
|
|
158
|
+
COMMAND_POLICY = "COMMAND_POLICY"
|
|
159
|
+
APPROVAL_POLICY = "APPROVAL_POLICY"
|
|
160
|
+
TOOL_RUNTIME = "TOOL_RUNTIME"
|
|
161
|
+
WORKSPACE_GUARD = "WORKSPACE_GUARD"
|
|
162
|
+
MCP_POLICY = "MCP_POLICY"
|
|
163
|
+
RUNTIME_EVENT = "RUNTIME_EVENT"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SecurityDecision(StrEnum):
|
|
167
|
+
DENIED = "DENIED"
|
|
168
|
+
BLOCKED = "BLOCKED"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True, slots=True)
|
|
172
|
+
class SecurityEvidence:
|
|
173
|
+
source: SecurityEvidenceSource
|
|
174
|
+
operation: str
|
|
175
|
+
decision: SecurityDecision
|
|
176
|
+
error_code: str
|
|
177
|
+
resource_summary: str | None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class EvalTraceCollector(Protocol):
|
|
181
|
+
def record_finish(self, finish: TraceRunFinish) -> None: ...
|
|
182
|
+
|
|
183
|
+
def get_finish(self, run_id: str) -> TraceRunFinish | None: ...
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass(frozen=True, slots=True)
|
|
187
|
+
class EvalMetrics:
|
|
188
|
+
agent_steps: int
|
|
189
|
+
llm_calls: int
|
|
190
|
+
tool_calls: int
|
|
191
|
+
replans: int
|
|
192
|
+
repairs: int
|
|
193
|
+
latency_ms: int
|
|
194
|
+
token_usage: TokenUsage
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class EvalOutcome(StrEnum):
|
|
198
|
+
PASS = "PASS"
|
|
199
|
+
TASK_FAILED = "TASK_FAILED"
|
|
200
|
+
INFRASTRUCTURE_ERROR = "INFRASTRUCTURE_ERROR"
|
|
201
|
+
EVALUATOR_ERROR = "EVALUATOR_ERROR"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class EvalValidationStatus(StrEnum):
|
|
205
|
+
NOT_RUN = "NOT_RUN"
|
|
206
|
+
PASSED = "PASSED"
|
|
207
|
+
FAILED = "FAILED"
|
|
208
|
+
TIMED_OUT = "TIMED_OUT"
|
|
209
|
+
INFRASTRUCTURE_ERROR = "INFRASTRUCTURE_ERROR"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class EvalPytestNodeOutcome(StrEnum):
|
|
213
|
+
PASSED = "PASSED"
|
|
214
|
+
FAILED = "FAILED"
|
|
215
|
+
SKIPPED = "SKIPPED"
|
|
216
|
+
XFAILED = "XFAILED"
|
|
217
|
+
XPASSED = "XPASSED"
|
|
218
|
+
ERROR = "ERROR"
|
|
219
|
+
NOT_RUN = "NOT_RUN"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass(frozen=True, slots=True)
|
|
223
|
+
class EvalPytestNodeResult:
|
|
224
|
+
node_id: str
|
|
225
|
+
outcome: EvalPytestNodeOutcome
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@dataclass(frozen=True, slots=True)
|
|
229
|
+
class EvalValidationResult:
|
|
230
|
+
status: EvalValidationStatus
|
|
231
|
+
command: tuple[str, ...]
|
|
232
|
+
exit_code: int | None
|
|
233
|
+
stdout_summary: str
|
|
234
|
+
stderr_summary: str
|
|
235
|
+
duration_ms: int
|
|
236
|
+
timeout_seconds: int | None
|
|
237
|
+
infrastructure_error: str | None
|
|
238
|
+
pytest_node_results: tuple[EvalPytestNodeResult, ...] = ()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class EvalDiscoveryStatus(StrEnum):
|
|
242
|
+
NOT_RUN = "NOT_RUN"
|
|
243
|
+
PASSED = "PASSED"
|
|
244
|
+
FAILED = "FAILED"
|
|
245
|
+
TIMED_OUT = "TIMED_OUT"
|
|
246
|
+
INFRASTRUCTURE_ERROR = "INFRASTRUCTURE_ERROR"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass(frozen=True, slots=True)
|
|
250
|
+
class EvalDiscoveryResult:
|
|
251
|
+
status: EvalDiscoveryStatus
|
|
252
|
+
command: tuple[str, ...]
|
|
253
|
+
exit_code: int | None
|
|
254
|
+
collected_node_ids: tuple[str, ...]
|
|
255
|
+
stdout_summary: str
|
|
256
|
+
stderr_summary: str
|
|
257
|
+
duration_ms: int
|
|
258
|
+
timeout_seconds: int | None
|
|
259
|
+
infrastructure_error: str | None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass(frozen=True, slots=True)
|
|
263
|
+
class EvalCheckEvidence:
|
|
264
|
+
pre_validation: EvalValidationResult
|
|
265
|
+
post_validation: EvalValidationResult
|
|
266
|
+
pre_discovery: EvalDiscoveryResult
|
|
267
|
+
post_discovery: EvalDiscoveryResult
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass(frozen=True, slots=True)
|
|
271
|
+
class EvalExecutionResult:
|
|
272
|
+
case_id: str
|
|
273
|
+
run_id: str | None
|
|
274
|
+
session_id: str | None
|
|
275
|
+
runtime_status: RuntimeStatus | None
|
|
276
|
+
terminal_status: TerminalStatus | None
|
|
277
|
+
error_code: str | None
|
|
278
|
+
final_result_summary: str | None
|
|
279
|
+
repository: EvalRepositoryEvidence
|
|
280
|
+
checks: EvalCheckEvidence
|
|
281
|
+
metrics: EvalMetrics | None
|
|
282
|
+
security_evidence: tuple[SecurityEvidence, ...]
|
|
283
|
+
infrastructure_error: str | None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass(frozen=True, slots=True)
|
|
287
|
+
class EvalAssertionResult:
|
|
288
|
+
assertion_type: EvalAssertionType
|
|
289
|
+
passed: bool
|
|
290
|
+
evidence: tuple[str, ...]
|
|
291
|
+
failure_reason: str | None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass(frozen=True, slots=True)
|
|
295
|
+
class DeterministicEvaluationResult:
|
|
296
|
+
outcome: EvalOutcome
|
|
297
|
+
runtime_completed: bool
|
|
298
|
+
validation_passed: bool | None
|
|
299
|
+
constraints_passed: bool
|
|
300
|
+
forbidden_change_detected: bool
|
|
301
|
+
unauthorized_change_detected: bool
|
|
302
|
+
assertion_results: tuple[EvalAssertionResult, ...]
|
|
303
|
+
failure_reason: str | None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class DeterministicEvaluator(Protocol):
|
|
307
|
+
async def evaluate(
|
|
308
|
+
self, case: EvalCase, execution: EvalExecutionResult
|
|
309
|
+
) -> DeterministicEvaluationResult: ...
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass(frozen=True, slots=True)
|
|
313
|
+
class EvalCaseLoadError:
|
|
314
|
+
source_path: str
|
|
315
|
+
case_id_hint: str | None
|
|
316
|
+
code: str
|
|
317
|
+
message: str
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@dataclass(frozen=True, slots=True)
|
|
321
|
+
class EvalSuiteLoadResult:
|
|
322
|
+
cases: tuple[EvalCase, ...]
|
|
323
|
+
errors: tuple[EvalCaseLoadError, ...]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@dataclass(frozen=True, slots=True)
|
|
327
|
+
class EvalCaseReport:
|
|
328
|
+
case_id: str
|
|
329
|
+
outcome: EvalOutcome
|
|
330
|
+
run_id: str | None
|
|
331
|
+
session_id: str | None
|
|
332
|
+
runtime_status: RuntimeStatus | None
|
|
333
|
+
terminal_status: TerminalStatus | None
|
|
334
|
+
error_code: str | None
|
|
335
|
+
deterministic_result: DeterministicEvaluationResult
|
|
336
|
+
metrics: EvalMetrics | None
|
|
337
|
+
repository: EvalRepositoryEvidence
|
|
338
|
+
checks: EvalCheckEvidence
|
|
339
|
+
security_evidence: tuple[SecurityEvidence, ...]
|
|
340
|
+
efficiency_warning: bool
|
|
341
|
+
evidence: tuple[str, ...]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@dataclass(frozen=True, slots=True)
|
|
345
|
+
class EvalSuiteError:
|
|
346
|
+
code: str
|
|
347
|
+
message: str
|
|
348
|
+
source_path: str | None
|
|
349
|
+
case_id_hint: str | None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@dataclass(frozen=True, slots=True)
|
|
353
|
+
class EvalSuiteReport:
|
|
354
|
+
schema_version: str
|
|
355
|
+
generated_at: datetime
|
|
356
|
+
cases: tuple[EvalCaseReport, ...]
|
|
357
|
+
suite_errors: tuple[EvalSuiteError, ...]
|
|
358
|
+
total_cases: int
|
|
359
|
+
passed_cases: int
|
|
360
|
+
task_failed_cases: int
|
|
361
|
+
infrastructure_error_cases: int
|
|
362
|
+
evaluator_error_cases: int
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Stable JSON serialization and report writing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import fields, is_dataclass
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import MappingProxyType
|
|
12
|
+
|
|
13
|
+
from nexus.evaluation.models import EvalSuiteReport
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_report(
|
|
17
|
+
report: EvalSuiteReport, reports_root: Path, *, write_baseline: bool = False
|
|
18
|
+
) -> tuple[Path, Path | None]:
|
|
19
|
+
reports_root.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
stamp = report.generated_at.astimezone(UTC).strftime("%Y%m%dT%H%M%SZ")
|
|
21
|
+
target = reports_root / f"eval-report-{stamp}.json"
|
|
22
|
+
payload = json.dumps(_json_value(report), ensure_ascii=False, indent=2) + "\n"
|
|
23
|
+
target.write_text(payload, encoding="utf-8")
|
|
24
|
+
baseline = reports_root / "baseline-v1.json"
|
|
25
|
+
baseline_written = None
|
|
26
|
+
if write_baseline and not baseline.exists():
|
|
27
|
+
baseline.write_text(payload, encoding="utf-8")
|
|
28
|
+
baseline_written = baseline
|
|
29
|
+
return target, baseline_written
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _json_value(value: object) -> object:
|
|
33
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
34
|
+
return {item.name: _json_value(getattr(value, item.name)) for item in fields(value)}
|
|
35
|
+
if isinstance(value, Enum):
|
|
36
|
+
return value.value
|
|
37
|
+
if isinstance(value, datetime):
|
|
38
|
+
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
39
|
+
if isinstance(value, (Mapping, MappingProxyType)):
|
|
40
|
+
return {str(key): _json_value(item) for key, item in value.items()}
|
|
41
|
+
if isinstance(value, (tuple, list)):
|
|
42
|
+
return [_json_value(item) for item in value]
|
|
43
|
+
return value
|