1#![forbid(unsafe_code)]
21
22pub mod lang;
23pub mod runtime;
24pub mod symbols;
25
26pub use runtime::{crash_marker, SmokeInvocation};
27pub use symbols::{defined_symbols, expected_symbols};
28
29use perspt_sdk::{
30 AgentDomainPackage, CorrectionDirection, DomainDetection, DomainId, DomainScope,
31 EnergyComponent, EnergyModel, ResidualClass, ResidualEvent, ResidualSchema, ResidualWeight,
32 StabilityClaim, WorkspaceSnapshot,
33};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum CodingLanguage {
38 Rust,
39 Python,
40 TypeScript,
41}
42
43#[derive(Debug, Clone, Default)]
45pub struct CodingDomain;
46
47impl CodingDomain {
48 pub fn new() -> Self {
49 Self
50 }
51
52 fn classes() -> Vec<ResidualClass> {
54 vec![
55 ResidualClass::Syntax,
56 ResidualClass::Type,
57 ResidualClass::Build,
58 ResidualClass::TestFailure,
59 ResidualClass::Lint,
60 ResidualClass::Format,
61 ResidualClass::Runtime,
62 ResidualClass::Dependency,
63 ResidualClass::Manifest,
64 ResidualClass::ImportGraph,
65 ResidualClass::SymbolMismatch,
66 ResidualClass::InterfaceMismatch,
67 ResidualClass::OwnershipViolation,
68 ResidualClass::Policy,
69 ResidualClass::Regression,
70 ResidualClass::SensorUnavailable,
71 ResidualClass::ToolFailure,
72 ResidualClass::SheafInconsistency,
73 ]
74 }
75}
76
77impl AgentDomainPackage for CodingDomain {
78 fn domain_id(&self) -> DomainId {
79 DomainId::new("coding")
80 }
81
82 fn detect(&self, workspace: &WorkspaceSnapshot) -> DomainDetection {
83 let mut evidence = Vec::new();
84 for marker in ["Cargo.toml", "pyproject.toml", "package.json", "go.mod"] {
85 if workspace.has_file_named(marker) {
86 evidence.push(format!("found {marker}"));
87 }
88 }
89 let activated = !evidence.is_empty();
90 DomainDetection {
91 domain: self.domain_id(),
92 activated,
93 confidence: if activated { 0.95 } else { 0.0 },
94 evidence,
95 }
96 }
97
98 fn residual_schema(&self, _scope: &DomainScope) -> ResidualSchema {
99 ResidualSchema::new(Self::classes())
100 }
101
102 fn energy_model(&self, scope: &DomainScope) -> EnergyModel {
103 use EnergyComponent::*;
104 use ResidualClass::*;
105 let weights = vec![
110 ResidualWeight::new(Syntax, Syn, 4.0).with_hard_threshold(0.0),
111 ResidualWeight::new(Type, Syn, 3.0).with_hard_threshold(0.0),
112 ResidualWeight::new(Build, Syn, 3.0).with_hard_threshold(0.0),
113 ResidualWeight::new(ImportGraph, Str, 2.0),
114 ResidualWeight::new(SymbolMismatch, Str, 2.0),
115 ResidualWeight::new(InterfaceMismatch, Str, 2.5),
116 ResidualWeight::new(OwnershipViolation, Str, 2.0),
117 ResidualWeight::new(Policy, Str, 1.0),
118 ResidualWeight::new(Dependency, Str, 1.5),
119 ResidualWeight::new(Manifest, Str, 1.5),
120 ResidualWeight::new(Lint, Str, 0.5),
121 ResidualWeight::new(Format, Str, 0.25),
122 ResidualWeight::new(TestFailure, Log, 2.0),
123 ResidualWeight::new(Runtime, Log, 2.0),
124 ResidualWeight::new(Regression, Log, 3.0),
125 ResidualWeight::new(SensorUnavailable, Boot, 1.0),
126 ResidualWeight::new(ToolFailure, Boot, 1.0),
127 ResidualWeight::new(SheafInconsistency, Sheaf, 2.0),
128 ];
129
130 let mut model = EnergyModel::new("coding", 0.5).with_correction_budget(4);
131 model.residual_weights = weights;
132 model.energy_tolerance = 0.0;
133 model.stability_claim = Some(StabilityClaim::not_claimed(format!(
135 "coding scope: {}",
136 scope.label
137 )));
138 model
139 }
140
141 fn correction_directions(&self, residuals: &[ResidualEvent]) -> Vec<CorrectionDirection> {
142 let mut directions = Vec::new();
143 for r in residuals {
144 if let Some(d) = correction_for(r) {
145 directions.push(d);
146 }
147 }
148 directions
149 }
150}
151
152fn correction_for(residual: &ResidualEvent) -> Option<CorrectionDirection> {
156 match residual.class {
157 ResidualClass::ImportGraph => {
158 let symbol = residual
160 .affected_symbols
161 .first()
162 .map(|s| s.name.clone())
163 .unwrap_or_else(|| "the missing item".to_string());
164 Some(
165 CorrectionDirection::new(
166 ResidualClass::ImportGraph,
167 format!(
168 "resolve the unresolved import for `{symbol}`: add the missing `use` path \
169 or declare the missing `mod`, do not regenerate unrelated code"
170 ),
171 )
172 .with_paths(residual.affected_paths.clone())
173 .with_rationale(
174 "unresolved imports are structural; the fix is an import/module \
175 declaration, not a behavioral rewrite",
176 ),
177 )
178 }
179 ResidualClass::Type => Some(
180 CorrectionDirection::new(
181 ResidualClass::Type,
182 "reconcile the type mismatch at the reported span; adjust the expression or the \
183 declared signature, keeping the public interface stable",
184 )
185 .with_paths(residual.affected_paths.clone()),
186 ),
187 ResidualClass::Dependency | ResidualClass::Manifest => Some(
188 CorrectionDirection::new(
189 residual.class,
190 "repair the dependency/manifest: add or pin the missing crate/package and sync \
191 the lockfile through an approved dependency-mutation effect",
192 )
193 .with_paths(residual.affected_paths.clone()),
194 ),
195 ResidualClass::TestFailure => Some(
196 CorrectionDirection::new(
197 ResidualClass::TestFailure,
198 "address the failing test by fixing the implementation it attributes to; do not \
199 weaken or delete the assertion",
200 )
201 .with_paths(residual.affected_paths.clone()),
202 ),
203 ResidualClass::SensorUnavailable | ResidualClass::ToolFailure => None,
206 _ => None,
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use perspt_sdk::{score_candidate, IndependenceRoute, ResidualSeverity, SensorRef, SymbolRef};
214
215 fn lsp_import_residual() -> ResidualEvent {
216 let mut r = ResidualEvent::new(
217 "n1",
218 0,
219 ResidualClass::ImportGraph,
220 ResidualSeverity::Error,
221 1.0,
222 SensorRef::new("rust-analyzer", IndependenceRoute::Lsp),
223 )
224 .unwrap();
225 r.affected_symbols = vec![SymbolRef {
226 name: "Bar".into(),
227 container: Some("crate::foo".into()),
228 }];
229 r.affected_paths = vec!["src/main.rs".into()];
230 r
231 }
232
233 #[test]
234 fn detects_coding_domain_from_cargo_toml() {
235 let domain = CodingDomain::new();
236 let ws = WorkspaceSnapshot::new("/repo", vec!["Cargo.toml".into(), "src/main.rs".into()]);
237 let detection = domain.detect(&ws);
238 assert!(detection.activated);
239 assert_eq!(detection.domain, DomainId::new("coding"));
240 }
241
242 #[test]
243 fn energy_model_validates_and_is_measured_only() {
244 let domain = CodingDomain::new();
245 let model = domain.energy_model(&DomainScope::default());
246 assert!(model.validate().is_ok());
247 let claim = model.stability_claim.unwrap();
248 assert!(
249 !claim.claims_floor(),
250 "coding domain must remain NotClaimed"
251 );
252 }
253
254 #[test]
255 fn rust_unresolved_import_yields_import_direction_not_retry() {
256 let domain = CodingDomain::new();
257 let directions = domain.correction_directions(&[lsp_import_residual()]);
258 assert_eq!(directions.len(), 1);
259 assert_eq!(directions[0].addresses, ResidualClass::ImportGraph);
260 assert!(directions[0].instruction.contains("Bar"));
261 assert!(directions[0].instruction.contains("use"));
262 }
263
264 #[test]
265 fn degraded_sensor_has_no_correction_direction() {
266 let domain = CodingDomain::new();
267 let r = ResidualEvent::new(
268 "n1",
269 0,
270 ResidualClass::SensorUnavailable,
271 ResidualSeverity::Blocking,
272 1.0,
273 SensorRef::new("cargo", IndependenceRoute::DeterministicTool),
274 )
275 .unwrap();
276 assert!(domain.correction_directions(&[r]).is_empty());
277 }
278
279 #[test]
280 fn coding_energy_model_scores_real_residuals() {
281 let domain = CodingDomain::new();
282 let model = domain.energy_model(&DomainScope::default());
283 let score = score_candidate(&model, &[lsp_import_residual()]).unwrap();
285 assert_eq!(score.total, 2.0);
286 assert_eq!(score.components.v_str, 2.0);
287 }
288}