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
nexus/domain/skills.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Frozen Day 7 Skill values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from pathlib import PurePosixPath
|
|
9
|
+
|
|
10
|
+
_SKILL_ID = re.compile(r"^[a-z][a-z0-9-]{0,63}$")
|
|
11
|
+
_SEMVER_CORE = re.compile(r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SkillSource(StrEnum):
|
|
15
|
+
REPOSITORY = "repository"
|
|
16
|
+
USER_GLOBAL = "user_global"
|
|
17
|
+
BUILTIN = "builtin"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class SkillLocation:
|
|
22
|
+
source: SkillSource
|
|
23
|
+
relative_path: str
|
|
24
|
+
|
|
25
|
+
def __post_init__(self) -> None:
|
|
26
|
+
if not isinstance(self.source, SkillSource) or not isinstance(self.relative_path, str):
|
|
27
|
+
raise ValueError("Skill location fields are invalid.")
|
|
28
|
+
path = PurePosixPath(self.relative_path)
|
|
29
|
+
if (
|
|
30
|
+
path.is_absolute()
|
|
31
|
+
or len(path.parts) != 2
|
|
32
|
+
or path.parts[-1] != "SKILL.md"
|
|
33
|
+
or not _SKILL_ID.fullmatch(path.parts[0])
|
|
34
|
+
or self.relative_path != path.as_posix()
|
|
35
|
+
):
|
|
36
|
+
raise ValueError("Skill location must use <skill-id>/SKILL.md.")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class SkillMetadata:
|
|
41
|
+
skill_id: str
|
|
42
|
+
name: str
|
|
43
|
+
description: str
|
|
44
|
+
usage_scenario: str
|
|
45
|
+
source: SkillSource
|
|
46
|
+
priority: int
|
|
47
|
+
version: str
|
|
48
|
+
location: SkillLocation
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
if not isinstance(self.skill_id, str) or not _SKILL_ID.fullmatch(self.skill_id):
|
|
52
|
+
raise ValueError("Skill ID is invalid.")
|
|
53
|
+
for field_name, value, maximum in (
|
|
54
|
+
("name", self.name, 100),
|
|
55
|
+
("description", self.description, 500),
|
|
56
|
+
("usage_scenario", self.usage_scenario, 500),
|
|
57
|
+
):
|
|
58
|
+
if not isinstance(value, str):
|
|
59
|
+
raise ValueError(f"Skill {field_name} is invalid.")
|
|
60
|
+
normalized = value.strip()
|
|
61
|
+
if not normalized or len(normalized) > maximum:
|
|
62
|
+
raise ValueError(f"Skill {field_name} is invalid.")
|
|
63
|
+
object.__setattr__(self, field_name, normalized)
|
|
64
|
+
if (
|
|
65
|
+
isinstance(self.priority, bool)
|
|
66
|
+
or not isinstance(self.priority, int)
|
|
67
|
+
or not 0 <= self.priority <= 100
|
|
68
|
+
):
|
|
69
|
+
raise ValueError("Skill priority must be an integer from 0 through 100.")
|
|
70
|
+
if not isinstance(self.version, str) or not _SEMVER_CORE.fullmatch(self.version):
|
|
71
|
+
raise ValueError("Skill version must use SemVer core form.")
|
|
72
|
+
if not isinstance(self.location, SkillLocation) or not isinstance(
|
|
73
|
+
self.source, SkillSource
|
|
74
|
+
):
|
|
75
|
+
raise ValueError("Skill source and location are invalid.")
|
|
76
|
+
if self.location.source is not self.source:
|
|
77
|
+
raise ValueError("Skill source and location source must match.")
|
|
78
|
+
if PurePosixPath(self.location.relative_path).parts[0] != self.skill_id:
|
|
79
|
+
raise ValueError("Skill directory and ID must match.")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True, slots=True)
|
|
83
|
+
class SelectedSkill:
|
|
84
|
+
metadata: SkillMetadata
|
|
85
|
+
body: str
|
|
86
|
+
selected_reason: str
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
if (
|
|
90
|
+
not isinstance(self.body, str)
|
|
91
|
+
or not self.body.strip()
|
|
92
|
+
or "\r" in self.body
|
|
93
|
+
or not self.body.endswith("\n")
|
|
94
|
+
or self.body.endswith("\n\n")
|
|
95
|
+
):
|
|
96
|
+
raise ValueError("Selected Skill body is not normalized.")
|
|
97
|
+
normalized_reason = _selection_summary(self.selected_reason)
|
|
98
|
+
object.__setattr__(self, "selected_reason", normalized_reason)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True, slots=True)
|
|
102
|
+
class SkillSelectionResult:
|
|
103
|
+
selected_skill_ids: tuple[str, ...]
|
|
104
|
+
selection_reason_summary: str
|
|
105
|
+
|
|
106
|
+
def __post_init__(self) -> None:
|
|
107
|
+
if isinstance(self.selected_skill_ids, str):
|
|
108
|
+
raise ValueError("Selected Skill IDs must be a sequence of identities.")
|
|
109
|
+
skill_ids = tuple(self.selected_skill_ids)
|
|
110
|
+
if len(skill_ids) > 2 or len(skill_ids) != len(set(skill_ids)) or any(
|
|
111
|
+
not isinstance(skill_id, str) or not _SKILL_ID.fullmatch(skill_id)
|
|
112
|
+
for skill_id in skill_ids
|
|
113
|
+
):
|
|
114
|
+
raise ValueError("Selected Skill IDs must be valid and unique.")
|
|
115
|
+
object.__setattr__(self, "selected_skill_ids", skill_ids)
|
|
116
|
+
object.__setattr__(
|
|
117
|
+
self,
|
|
118
|
+
"selection_reason_summary",
|
|
119
|
+
_selection_summary(self.selection_reason_summary),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _selection_summary(value: str) -> str:
|
|
124
|
+
if not isinstance(value, str):
|
|
125
|
+
raise ValueError("Skill selection summary is invalid.")
|
|
126
|
+
normalized = value.strip()
|
|
127
|
+
if not normalized or len(normalized) > 1000:
|
|
128
|
+
raise ValueError("Skill selection summary is invalid.")
|
|
129
|
+
return normalized
|
nexus/domain/tooling.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Nexus-owned Day 3 tool, policy, and sandbox values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
JsonObject = dict[str, object]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RiskLevel(StrEnum):
|
|
13
|
+
SAFE = "SAFE"
|
|
14
|
+
WRITE = "WRITE"
|
|
15
|
+
DANGEROUS = "DANGEROUS"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ApprovalDecision(StrEnum):
|
|
19
|
+
PENDING = "PENDING"
|
|
20
|
+
APPROVED = "APPROVED"
|
|
21
|
+
DENIED = "DENIED"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PolicyDecision(StrEnum):
|
|
25
|
+
ALLOWED = "ALLOWED"
|
|
26
|
+
DENIED = "DENIED"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class ToolInvocation:
|
|
31
|
+
invocation_id: str
|
|
32
|
+
tool_name: str
|
|
33
|
+
arguments: JsonObject
|
|
34
|
+
run_id: str
|
|
35
|
+
session_id: str
|
|
36
|
+
|
|
37
|
+
def __post_init__(self) -> None:
|
|
38
|
+
_validate_uuid(self.invocation_id, "invocation_id")
|
|
39
|
+
_validate_uuid(self.run_id, "run_id")
|
|
40
|
+
_validate_uuid(self.session_id, "session_id")
|
|
41
|
+
if not self.tool_name:
|
|
42
|
+
raise ValueError("tool_name must not be empty.")
|
|
43
|
+
if any(not isinstance(key, str) for key in self.arguments):
|
|
44
|
+
raise ValueError("Tool argument keys must be strings.")
|
|
45
|
+
if not _is_json_value(self.arguments):
|
|
46
|
+
raise ValueError("Tool arguments must contain only JSON-compatible values.")
|
|
47
|
+
object.__setattr__(self, "arguments", dict(self.arguments))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class ToolError:
|
|
52
|
+
code: str
|
|
53
|
+
message: str
|
|
54
|
+
retryable: bool
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
if not self.code or not self.message:
|
|
58
|
+
raise ValueError("ToolError code and message must not be empty.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class ToolResult:
|
|
63
|
+
invocation_id: str
|
|
64
|
+
tool_name: str
|
|
65
|
+
success: bool
|
|
66
|
+
output: JsonObject | None
|
|
67
|
+
error: ToolError | None
|
|
68
|
+
risk_level: RiskLevel
|
|
69
|
+
policy_decision: PolicyDecision
|
|
70
|
+
approval_decision: ApprovalDecision | None
|
|
71
|
+
duration_ms: int
|
|
72
|
+
|
|
73
|
+
def __post_init__(self) -> None:
|
|
74
|
+
_validate_uuid(self.invocation_id, "invocation_id")
|
|
75
|
+
if not self.tool_name:
|
|
76
|
+
raise ValueError("tool_name must not be empty.")
|
|
77
|
+
if self.duration_ms < 0:
|
|
78
|
+
raise ValueError("duration_ms must not be negative.")
|
|
79
|
+
if self.success and self.error is not None:
|
|
80
|
+
raise ValueError("A successful ToolResult cannot contain an error.")
|
|
81
|
+
if self.success and self.policy_decision is not PolicyDecision.ALLOWED:
|
|
82
|
+
raise ValueError("A successful ToolResult must be allowed by policy.")
|
|
83
|
+
if not self.success and self.error is None:
|
|
84
|
+
raise ValueError("A failed ToolResult must contain a ToolError.")
|
|
85
|
+
if self.output is not None and not _is_json_value(self.output):
|
|
86
|
+
raise ValueError("Tool output must contain only JSON-compatible values.")
|
|
87
|
+
if self.output is not None:
|
|
88
|
+
object.__setattr__(self, "output", dict(self.output))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True, slots=True)
|
|
92
|
+
class SandboxRequest:
|
|
93
|
+
operation: str
|
|
94
|
+
argv: list[str]
|
|
95
|
+
cwd: str
|
|
96
|
+
timeout_seconds: float
|
|
97
|
+
|
|
98
|
+
def __post_init__(self) -> None:
|
|
99
|
+
if not self.operation:
|
|
100
|
+
raise ValueError("Sandbox operation must not be empty.")
|
|
101
|
+
if not self.argv or any(
|
|
102
|
+
not isinstance(item, str) or not item for item in self.argv
|
|
103
|
+
):
|
|
104
|
+
raise ValueError("Sandbox argv must contain non-empty strings.")
|
|
105
|
+
if not self.cwd:
|
|
106
|
+
raise ValueError("Sandbox cwd must not be empty.")
|
|
107
|
+
if not 0 < self.timeout_seconds <= 300.0:
|
|
108
|
+
raise ValueError("Sandbox timeout must be in (0, 300].")
|
|
109
|
+
object.__setattr__(self, "argv", list(self.argv))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True, slots=True)
|
|
113
|
+
class SandboxResult:
|
|
114
|
+
argv: list[str]
|
|
115
|
+
cwd: str
|
|
116
|
+
risk_level: RiskLevel
|
|
117
|
+
policy_decision: PolicyDecision
|
|
118
|
+
exit_code: int | None
|
|
119
|
+
stdout: str
|
|
120
|
+
stderr: str
|
|
121
|
+
duration_ms: int
|
|
122
|
+
timed_out: bool
|
|
123
|
+
output_truncated: bool
|
|
124
|
+
error: ToolError | None
|
|
125
|
+
|
|
126
|
+
def __post_init__(self) -> None:
|
|
127
|
+
if self.duration_ms < 0:
|
|
128
|
+
raise ValueError("duration_ms must not be negative.")
|
|
129
|
+
if self.policy_decision is PolicyDecision.DENIED and self.exit_code is not None:
|
|
130
|
+
raise ValueError("A denied SandboxResult cannot have an exit code.")
|
|
131
|
+
if self.timed_out and self.exit_code is not None:
|
|
132
|
+
raise ValueError("A timed-out SandboxResult cannot have an exit code.")
|
|
133
|
+
object.__setattr__(self, "argv", list(self.argv))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def risk_rank(risk: RiskLevel) -> int:
|
|
137
|
+
return {
|
|
138
|
+
RiskLevel.SAFE: 0,
|
|
139
|
+
RiskLevel.WRITE: 1,
|
|
140
|
+
RiskLevel.DANGEROUS: 2,
|
|
141
|
+
}[risk]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _validate_uuid(value: str, field_name: str) -> None:
|
|
145
|
+
try:
|
|
146
|
+
parsed = UUID(value)
|
|
147
|
+
except ValueError as exc:
|
|
148
|
+
raise ValueError(f"{field_name} must be a UUID string.") from exc
|
|
149
|
+
if str(parsed) != value:
|
|
150
|
+
raise ValueError(f"{field_name} must use canonical UUID text.")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _is_json_value(value: object) -> bool:
|
|
154
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
155
|
+
return True
|
|
156
|
+
if isinstance(value, list):
|
|
157
|
+
return all(_is_json_value(item) for item in value)
|
|
158
|
+
if isinstance(value, dict):
|
|
159
|
+
return all(
|
|
160
|
+
isinstance(key, str) and _is_json_value(item)
|
|
161
|
+
for key, item in value.items()
|
|
162
|
+
)
|
|
163
|
+
return False
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Day 4 validation values and deterministic repairability rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from nexus.domain.tooling import JsonObject, ToolResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ValidationStatus(StrEnum):
|
|
13
|
+
PASS = "PASS"
|
|
14
|
+
FAIL = "FAIL"
|
|
15
|
+
UNKNOWN = "UNKNOWN"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ValidationConfidence(StrEnum):
|
|
19
|
+
HIGH = "HIGH"
|
|
20
|
+
MEDIUM = "MEDIUM"
|
|
21
|
+
LOW = "LOW"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ValidationCheckKind(StrEnum):
|
|
25
|
+
TEST = "TEST"
|
|
26
|
+
BUILD = "BUILD"
|
|
27
|
+
LINT = "LINT"
|
|
28
|
+
TYPE_CHECK = "TYPE_CHECK"
|
|
29
|
+
REPOSITORY_COMMAND = "REPOSITORY_COMMAND"
|
|
30
|
+
GENERATED_TARGETED_TEST = "GENERATED_TARGETED_TEST"
|
|
31
|
+
BASIC_EXECUTION = "BASIC_EXECUTION"
|
|
32
|
+
DIFF_INSPECTION = "DIFF_INSPECTION"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class ValidationCheck:
|
|
37
|
+
check_id: str
|
|
38
|
+
sequence: int
|
|
39
|
+
kind: ValidationCheckKind
|
|
40
|
+
tool_name: str
|
|
41
|
+
arguments: JsonObject
|
|
42
|
+
selection_reason: str
|
|
43
|
+
required: bool
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
_validate_uuid(self.check_id, "check_id")
|
|
47
|
+
if self.sequence < 1 or not self.tool_name or not self.selection_reason.strip():
|
|
48
|
+
raise ValueError("ValidationCheck identity and description are invalid.")
|
|
49
|
+
if not _is_json_value(self.arguments):
|
|
50
|
+
raise ValueError("ValidationCheck arguments must be JSON-compatible.")
|
|
51
|
+
object.__setattr__(self, "arguments", dict(self.arguments))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class ValidationCheckResult:
|
|
56
|
+
check: ValidationCheck
|
|
57
|
+
status: ValidationStatus
|
|
58
|
+
tool_result: ToolResult | None
|
|
59
|
+
evidence_summary: str
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
if not self.evidence_summary.strip():
|
|
63
|
+
raise ValueError("Validation evidence_summary must not be empty.")
|
|
64
|
+
if self.tool_result is not None and (
|
|
65
|
+
self.tool_result.invocation_id != self.check.check_id
|
|
66
|
+
or self.tool_result.tool_name != self.check.tool_name
|
|
67
|
+
):
|
|
68
|
+
raise ValueError("Validation Tool evidence has mismatched identity.")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class ValidationPlan:
|
|
73
|
+
checks: tuple[ValidationCheck, ...]
|
|
74
|
+
|
|
75
|
+
def __post_init__(self) -> None:
|
|
76
|
+
checks = tuple(self.checks)
|
|
77
|
+
if [check.sequence for check in checks] != list(range(1, len(checks) + 1)):
|
|
78
|
+
raise ValueError("ValidationCheck sequence must be contiguous from one.")
|
|
79
|
+
if len({check.check_id for check in checks}) != len(checks):
|
|
80
|
+
raise ValueError("ValidationCheck IDs must be unique.")
|
|
81
|
+
object.__setattr__(self, "checks", checks)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True, slots=True)
|
|
85
|
+
class ValidationResult:
|
|
86
|
+
selected_checks: tuple[ValidationCheck, ...]
|
|
87
|
+
executed_checks: tuple[ValidationCheckResult, ...]
|
|
88
|
+
status: ValidationStatus
|
|
89
|
+
confidence: ValidationConfidence
|
|
90
|
+
repairable: bool
|
|
91
|
+
repair_count: int
|
|
92
|
+
summary: str
|
|
93
|
+
|
|
94
|
+
def __post_init__(self) -> None:
|
|
95
|
+
object.__setattr__(self, "selected_checks", tuple(self.selected_checks))
|
|
96
|
+
object.__setattr__(self, "executed_checks", tuple(self.executed_checks))
|
|
97
|
+
if self.repair_count < 0 or not self.summary.strip():
|
|
98
|
+
raise ValueError("ValidationResult count and summary are invalid.")
|
|
99
|
+
if self.status is not ValidationStatus.FAIL and self.repairable:
|
|
100
|
+
raise ValueError("Only a failed ValidationResult may be repairable.")
|
|
101
|
+
if self.repairable != aggregate_repairable(self.status, self.executed_checks):
|
|
102
|
+
raise ValueError("ValidationResult repairable does not match frozen aggregation.")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
_REPAIRABLE_KINDS = {
|
|
106
|
+
ValidationCheckKind.TEST,
|
|
107
|
+
ValidationCheckKind.BUILD,
|
|
108
|
+
ValidationCheckKind.LINT,
|
|
109
|
+
ValidationCheckKind.TYPE_CHECK,
|
|
110
|
+
ValidationCheckKind.GENERATED_TARGETED_TEST,
|
|
111
|
+
ValidationCheckKind.BASIC_EXECUTION,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_mechanically_repairable(result: ValidationCheckResult) -> bool:
|
|
116
|
+
tool_result = result.tool_result
|
|
117
|
+
return (
|
|
118
|
+
result.status is ValidationStatus.FAIL
|
|
119
|
+
and result.check.kind in _REPAIRABLE_KINDS
|
|
120
|
+
and tool_result is not None
|
|
121
|
+
and tool_result.error is not None
|
|
122
|
+
and tool_result.error.code == "COMMAND_EXIT_NONZERO"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def aggregate_repairable(
|
|
127
|
+
status: ValidationStatus,
|
|
128
|
+
results: tuple[ValidationCheckResult, ...],
|
|
129
|
+
) -> bool:
|
|
130
|
+
if status is not ValidationStatus.FAIL:
|
|
131
|
+
return False
|
|
132
|
+
required_failures = tuple(
|
|
133
|
+
result
|
|
134
|
+
for result in results
|
|
135
|
+
if result.check.required and result.status is ValidationStatus.FAIL
|
|
136
|
+
)
|
|
137
|
+
return bool(required_failures) and all(
|
|
138
|
+
is_mechanically_repairable(result) for result in required_failures
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _validate_uuid(value: str, field_name: str) -> None:
|
|
143
|
+
try:
|
|
144
|
+
parsed = UUID(value)
|
|
145
|
+
except (AttributeError, TypeError, ValueError) as exc:
|
|
146
|
+
raise ValueError(f"{field_name} must be a canonical UUID string.") from exc
|
|
147
|
+
if str(parsed) != value:
|
|
148
|
+
raise ValueError(f"{field_name} must be a canonical UUID string.")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_json_value(value: object) -> bool:
|
|
152
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
153
|
+
return True
|
|
154
|
+
if isinstance(value, list):
|
|
155
|
+
return all(_is_json_value(item) for item in value)
|
|
156
|
+
if isinstance(value, dict):
|
|
157
|
+
return all(
|
|
158
|
+
isinstance(key, str) and _is_json_value(item)
|
|
159
|
+
for key, item in value.items()
|
|
160
|
+
)
|
|
161
|
+
return False
|
nexus/errors/__init__.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Nexus-owned error types exposed across stable boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NexusError(Exception):
|
|
7
|
+
"""Base class for safe, structured Nexus failures."""
|
|
8
|
+
|
|
9
|
+
default_code = "NEXUS_ERROR"
|
|
10
|
+
default_retryable = False
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
message: str,
|
|
15
|
+
*,
|
|
16
|
+
code: str | None = None,
|
|
17
|
+
retryable: bool | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.code = code or self.default_code
|
|
21
|
+
self.retryable = self.default_retryable if retryable is None else retryable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigurationError(NexusError):
|
|
25
|
+
"""The resolved Nexus configuration is missing or invalid."""
|
|
26
|
+
|
|
27
|
+
default_code = "CONFIGURATION_ERROR"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ModelError(NexusError):
|
|
31
|
+
"""A model operation failed behind the ModelGateway boundary."""
|
|
32
|
+
|
|
33
|
+
default_code = "MODEL_ERROR"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MCPError(NexusError):
|
|
37
|
+
"""An MCP connection, discovery, or call failed behind the adapter boundary."""
|
|
38
|
+
|
|
39
|
+
default_code = "MCP_ERROR"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SessionError(NexusError):
|
|
43
|
+
"""A session operation failed a Nexus business rule or persistence boundary."""
|
|
44
|
+
|
|
45
|
+
default_code = "SESSION_ERROR"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ToolExecutionError(NexusError):
|
|
49
|
+
"""A tool operation failed behind the structured Tool boundary."""
|
|
50
|
+
|
|
51
|
+
default_code = "TOOL_EXECUTION_ERROR"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ContextError(NexusError):
|
|
55
|
+
"""Repository exploration or bounded context construction failed."""
|
|
56
|
+
|
|
57
|
+
default_code = "CONTEXT_ERROR"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ValidationError(NexusError):
|
|
61
|
+
"""Validation planning or execution could not produce truthful evidence."""
|
|
62
|
+
|
|
63
|
+
default_code = "VALIDATION_ERROR"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PermissionDeniedError(NexusError):
|
|
67
|
+
"""A centralized security policy denied an operation."""
|
|
68
|
+
|
|
69
|
+
default_code = "PERMISSION_DENIED"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"ConfigurationError",
|
|
74
|
+
"ContextError",
|
|
75
|
+
"MCPError",
|
|
76
|
+
"ModelError",
|
|
77
|
+
"NexusError",
|
|
78
|
+
"PermissionDeniedError",
|
|
79
|
+
"SessionError",
|
|
80
|
+
"ToolExecutionError",
|
|
81
|
+
"ValidationError",
|
|
82
|
+
]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Nexus-owned deterministic evaluation subsystem."""
|
|
2
|
+
|
|
3
|
+
from nexus.evaluation.evaluator import DefaultDeterministicEvaluator
|
|
4
|
+
from nexus.evaluation.loader import EvalSuiteLoader
|
|
5
|
+
from nexus.evaluation.models import * # noqa: F403
|
|
6
|
+
|
|
7
|
+
__all__ = ["DefaultDeterministicEvaluator", "EvalSuiteLoader"]
|