1use serde::{Deserialize, Serialize};
5
6pub mod learner;
7pub mod procedure;
8pub mod vitals;
9
10pub use learner::{FailureKind, ProposalEnvelope, TrajectoryOutcome, TrajectoryStep};
11pub use procedure::{
12 ExperienceBundle, ExperienceEvent, ProcedureArtifact, ProcedureArtifactFormat,
13 ProcedureExecutionPlan, ProcedureExecutionStep, ProcedureLifecycle, ProcedurePatch,
14 ProcedureReuseOutcome, ProcedureStep, ProcedureStepKind, ProcedureVerification,
15};
16pub use vitals::{CognitivePhase, CognitiveVitals, VitalsGate};
17
18pub mod telemetry {
20 pub const CAPABILITY_PROFILE_STATE: &str = "capability_profile_state";
22 pub const FRESHNESS_STATE_AT_DECISION: &str = "freshness_state_at_decision";
24 pub const IMPACT_CHECKED_BEFORE_WRITE: &str = "impact_checked_before_write";
26 pub const TRAJECTORY_RECORDED: &str = "trajectory_recorded";
28 pub const TRAJECTORY_OUTCOME: &str = "trajectory_outcome";
29 pub const TRAJECTORY_STEP_DURATION_MS: &str = "trajectory_step_duration_ms";
30 pub const FAILURE_RECORDED: &str = "failure_recorded";
31 pub const FAILURE_RESOLUTION_HIT: &str = "failure_resolution_hit";
32 pub const FAILURE_PREVENTED_COUNT: &str = "failure_prevented_count";
33 pub const PROPOSAL_VALIDATED: &str = "proposal_validated";
34 pub const PROPOSAL_ADOPTED: &str = "proposal_adopted";
35 pub const PROCEDURE_MINTED: &str = "procedure_minted";
36 pub const PROCEDURE_REUSED: &str = "procedure_reused";
37 pub const PROCEDURE_PATCH_PROPOSED: &str = "procedure_patch_proposed";
38 pub const PROCEDURE_PATCH_ADOPTED: &str = "procedure_patch_adopted";
39 pub const COMPRESSION_PROFILE_TUNED: &str = "compression_profile_tuned";
40 pub const COMPRESSION_CACHE_HIT: &str = "compression_cache_hit";
41 pub const PERSONA_AXIS_DELTA: &str = "persona_axis_delta";
42 pub const VITALS_GATE_AT_TURN: &str = "vitals_gate_at_turn";
43 pub const CONTEXT_COMPILER_COMPOSE: &str = "context_compiler_compose";
46 pub const CONTEXT_COMPILER_TIER_UPGRADED: &str = "context_compiler_tier_upgraded";
48 pub const CONTEXT_COMPILER_SUMMARIZER_FAILED: &str = "context_compiler_summarizer_failed";
50 pub const CONTEXT_COMPILER_BUDGET_EXCEEDED: &str = "context_compiler_budget_exceeded";
52 pub const CONTEXT_COMPILER_BLOCK_EMITTED: &str = "context_compiler_block_emitted";
54}
55
56pub mod context_compiler {
62 pub mod segment_kind {
64 pub const SYSTEM_PROMPT: &str = "system_prompt";
65 pub const OLDER_TURN: &str = "older_turn";
66 pub const RECENT_TURN: &str = "recent_turn";
67 pub const TOOL_DEFINITIONS: &str = "tool_definitions";
68 pub const TOOL_RESULT: &str = "tool_result";
69 pub const USER_PROMPT: &str = "user_prompt";
70 pub const ANCHORED_SUMMARY_RECALL: &str = "anchored_summary_recall";
71 pub const MEMORY_BLOCK: &str = "memory_block";
72 }
73
74 pub mod tier {
76 pub const HEURISTIC: &str = "heuristic";
77 pub const HEURISTIC_SUMMARIZATION: &str = "heuristic_summarization";
78 pub const HEURISTIC_SUMMARIZATION_EMBEDDING: &str = "heuristic_summarization_embedding";
79 }
80}
81
82pub const CONTRACT_SCHEMA_VERSION: u32 = 1;
84
85pub const LEARNER_SCHEMA_VERSION: u32 = 1;
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum RepoIntelToolClass {
92 Query,
93 Context,
94 Impact,
95 DetectChanges,
96 Cypher,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum RepoIntelCapabilityState {
104 Ready,
106 Degraded,
108 Absent,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RepoIntelCapabilityProfile {
115 pub schema_version: u32,
116 pub state: RepoIntelCapabilityState,
117 pub classes_present: Vec<RepoIntelToolClass>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub note: Option<String>,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum ContextFreshness {
128 Fresh,
130 Stale,
132 Unknown,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(rename_all = "snake_case")]
139pub enum ImpactDecision {
140 AllowExecute,
142 RequireImpactFirst,
144 BlockUntilFresh,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct RecommendedToolStep {
151 pub tool: String,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub reason: Option<String>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct RecommendedNextTools {
159 pub schema_version: u32,
160 pub steps: Vec<RecommendedToolStep>,
161}
162
163impl Default for RecommendedNextTools {
164 fn default() -> Self {
165 Self {
166 schema_version: CONTRACT_SCHEMA_VERSION,
167 steps: Vec::new(),
168 }
169 }
170}
171
172impl RecommendedNextTools {
173 pub fn golden_default_chain() -> Self {
174 Self {
175 schema_version: CONTRACT_SCHEMA_VERSION,
176 steps: vec![
177 RecommendedToolStep {
178 tool: "ainl_validate".into(),
179 reason: Some("Strict check after edits".into()),
180 },
181 RecommendedToolStep {
182 tool: "ainl_compile".into(),
183 reason: Some("IR before diff/impact".into()),
184 },
185 RecommendedToolStep {
186 tool: "ainl_ir_diff".into(),
187 reason: Some("Blast radius vs prior IR when available".into()),
188 },
189 RecommendedToolStep {
190 tool: "ainl_run".into(),
191 reason: Some("Execute only after validation + impact awareness".into()),
192 },
193 ],
194 }
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use std::path::PathBuf;
202
203 #[test]
204 fn cognitive_vitals_json_roundtrip() {
205 let v = CognitiveVitals {
206 gate: VitalsGate::Pass,
207 phase: "reasoning:0.71".into(),
208 trust: 0.82,
209 mean_logprob: -0.4,
210 entropy: 0.12,
211 sample_tokens: 12,
212 };
213 let j = serde_json::to_value(&v).unwrap();
214 let back: CognitiveVitals = serde_json::from_value(j).unwrap();
215 assert_eq!(v, back);
216 }
217
218 #[test]
219 fn trajectory_step_json_roundtrip() {
220 let s = TrajectoryStep {
221 step_id: "s1".into(),
222 timestamp_ms: 1,
223 adapter: "http".into(),
224 operation: "GET".into(),
225 inputs_preview: None,
226 outputs_preview: None,
227 duration_ms: 3,
228 success: true,
229 error: None,
230 vitals: None,
231 freshness_at_step: Some(ContextFreshness::Fresh),
232 frame_vars: None,
233 tool_telemetry: None,
234 };
235 let j = serde_json::to_value(&s).unwrap();
236 let back: TrajectoryStep = serde_json::from_value(j).unwrap();
237 assert_eq!(s, back);
238 }
239
240 #[test]
241 fn contract_json_roundtrip() {
242 let p = RepoIntelCapabilityProfile {
243 schema_version: CONTRACT_SCHEMA_VERSION,
244 state: RepoIntelCapabilityState::Ready,
245 classes_present: vec![RepoIntelToolClass::Impact, RepoIntelToolClass::Query],
246 note: None,
247 };
248 let j = serde_json::to_value(&p).unwrap();
249 let back: RepoIntelCapabilityProfile = serde_json::from_value(j).unwrap();
250 assert_eq!(p.state, back.state);
251 }
252
253 #[test]
254 fn golden_fixture_file_matches_recommended_chain() {
255 let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
256 p.push("tests/fixtures/contract_v1.json");
257 let raw = std::fs::read_to_string(&p).expect("fixture");
258 let v: serde_json::Value = serde_json::from_str(&raw).expect("json");
259 let steps = v["RecommendedNextTools"]["steps"]
260 .as_array()
261 .expect("steps");
262 let tools: Vec<String> = steps
263 .iter()
264 .filter_map(|s| s.get("tool").and_then(|t| t.as_str().map(String::from)))
265 .collect();
266 assert_eq!(
267 tools,
268 vec!["ainl_validate", "ainl_compile", "ainl_ir_diff", "ainl_run"]
269 );
270 }
271
272 #[test]
273 fn cognitive_phase_json_roundtrip() {
274 let p = CognitivePhase::Retrieval;
275 let j = serde_json::to_value(p).unwrap();
276 let back: CognitivePhase = serde_json::from_value(j).unwrap();
277 assert_eq!(p, back);
278 }
279
280 #[test]
281 fn trajectory_outcome_json_roundtrip() {
282 for o in [
283 TrajectoryOutcome::Success,
284 TrajectoryOutcome::PartialSuccess,
285 TrajectoryOutcome::Failure,
286 TrajectoryOutcome::Aborted,
287 ] {
288 let j = serde_json::to_value(o).unwrap();
289 let back: TrajectoryOutcome = serde_json::from_value(j).unwrap();
290 assert_eq!(o, back);
291 }
292 }
293
294 #[test]
295 fn failure_kind_json_roundtrip_variants() {
296 let cases = vec![
297 FailureKind::AdapterTypo {
298 offered: "httP".into(),
299 suggestion: Some("http".into()),
300 },
301 FailureKind::ValidatorReject {
302 rule: "no_raw_shell".into(),
303 },
304 FailureKind::AdapterTimeout {
305 adapter: "web".into(),
306 ms: 5000,
307 },
308 FailureKind::ToolError {
309 tool: "file_read".into(),
310 message: "ENOENT".into(),
311 },
312 FailureKind::LoopGuardFire {
313 tool: "noop".into(),
314 repeat_count: 3,
315 },
316 FailureKind::Other {
317 message: "misc".into(),
318 },
319 ];
320 for fk in cases {
321 let j = serde_json::to_value(&fk).unwrap();
322 let back: FailureKind = serde_json::from_value(j).unwrap();
323 assert_eq!(fk, back);
324 }
325 }
326
327 #[test]
328 fn proposal_envelope_json_roundtrip() {
329 let pe = ProposalEnvelope {
330 schema_version: LEARNER_SCHEMA_VERSION,
331 original_hash: "abc".into(),
332 proposed_hash: "def".into(),
333 kind: "promote_pattern".into(),
334 rationale: "recurrence".into(),
335 freshness_at_proposal: ContextFreshness::Stale,
336 impact_decision: ImpactDecision::RequireImpactFirst,
337 };
338 let j = serde_json::to_value(&pe).unwrap();
339 let back: ProposalEnvelope = serde_json::from_value(j).unwrap();
340 assert_eq!(pe, back);
341 }
342
343 #[test]
344 fn trajectory_step_with_nested_vitals_roundtrip() {
345 let v = CognitiveVitals {
346 gate: VitalsGate::Warn,
347 phase: "reasoning:0.5".into(),
348 trust: 0.5,
349 mean_logprob: -0.2,
350 entropy: 0.1,
351 sample_tokens: 8,
352 };
353 let s = TrajectoryStep {
354 step_id: "s2".into(),
355 timestamp_ms: 2,
356 adapter: "builtin".into(),
357 operation: "list".into(),
358 inputs_preview: Some("a".into()),
359 outputs_preview: Some("b".into()),
360 duration_ms: 9,
361 success: false,
362 error: Some("boom".into()),
363 vitals: Some(v.clone()),
364 freshness_at_step: Some(ContextFreshness::Unknown),
365 frame_vars: None,
366 tool_telemetry: None,
367 };
368 let j = serde_json::to_value(&s).unwrap();
369 let back: TrajectoryStep = serde_json::from_value(j).unwrap();
370 assert_eq!(s, back);
371 }
372
373 #[test]
374 fn telemetry_learner_keys_are_unique_and_non_empty() {
375 use telemetry::*;
376 let keys = [
377 TRAJECTORY_RECORDED,
378 TRAJECTORY_OUTCOME,
379 TRAJECTORY_STEP_DURATION_MS,
380 FAILURE_RECORDED,
381 FAILURE_RESOLUTION_HIT,
382 FAILURE_PREVENTED_COUNT,
383 PROPOSAL_VALIDATED,
384 PROPOSAL_ADOPTED,
385 COMPRESSION_PROFILE_TUNED,
386 COMPRESSION_CACHE_HIT,
387 PERSONA_AXIS_DELTA,
388 VITALS_GATE_AT_TURN,
389 ];
390 for k in keys {
391 assert!(!k.is_empty());
392 }
393 for i in 0..keys.len() {
394 for j in (i + 1)..keys.len() {
395 assert_ne!(keys[i], keys[j], "duplicate telemetry key");
396 }
397 }
398 }
399}