Skip to main content

ainl_contracts/
lib.rs

1//! Cross-runtime contracts for GitNexus-style repo intelligence and impact-first AINL flows.
2//! Crate has **no** `openfang_*` dependencies so it can ship stand-alone or with AI_Native_Lang tooling.
3
4use 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
18/// Telemetry / metrics field names — keep identical across ArmaraOS, AINL MCP, and optional inference-server.
19pub mod telemetry {
20    /// Label: normalized repo-intel capability state (e.g. `ready`, `degraded`, `absent`).
21    pub const CAPABILITY_PROFILE_STATE: &str = "capability_profile_state";
22    /// Label: context freshness at decision time.
23    pub const FRESHNESS_STATE_AT_DECISION: &str = "freshness_state_at_decision";
24    /// Counter/gauge: whether impact was assessed before a risky write.
25    pub const IMPACT_CHECKED_BEFORE_WRITE: &str = "impact_checked_before_write";
26    /// Trajectory + failure + proposal + compression (learner suite).
27    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    /// Context-compiler suite (`ainl-context-compiler`, Phase 6 of SELF_LEARNING_INTEGRATION_MAP).
44    /// Histogram/counter: a single `compose()` call summary.
45    pub const CONTEXT_COMPILER_COMPOSE: &str = "context_compiler_compose";
46    /// Counter: tier upgraded mid-session (e.g. heuristic → heuristic_summarization).
47    pub const CONTEXT_COMPILER_TIER_UPGRADED: &str = "context_compiler_tier_upgraded";
48    /// Counter: summarizer call failed and the orchestrator auto-degraded for that turn.
49    pub const CONTEXT_COMPILER_SUMMARIZER_FAILED: &str = "context_compiler_summarizer_failed";
50    /// Counter: budget exceeded after best-effort compaction (safety-net truncation applied).
51    pub const CONTEXT_COMPILER_BUDGET_EXCEEDED: &str = "context_compiler_budget_exceeded";
52    /// Counter: a single segment was emitted into the composed prompt.
53    pub const CONTEXT_COMPILER_BLOCK_EMITTED: &str = "context_compiler_block_emitted";
54}
55
56/// Context-compiler shared vocabulary (Phase 6 of SELF_LEARNING_INTEGRATION_MAP §15.1).
57///
58/// Lets other AINL hosts read context-compiler telemetry without taking a hard dependency on the
59/// `ainl-context-compiler` crate itself. The strings here intentionally mirror the variant names
60/// in `ainl_context_compiler::{SegmentKind, Tier}`.
61pub mod context_compiler {
62    /// Stable lowercase labels for `SegmentKind` (mirrors the crate enum).
63    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    /// Stable lowercase labels for `Tier`.
75    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
82/// Version for JSON serialization of policy contract payloads (bump on breaking enum changes).
83pub const CONTRACT_SCHEMA_VERSION: u32 = 1;
84
85/// Schema version for [`ProposalEnvelope`] and other learner wire types.
86pub const LEARNER_SCHEMA_VERSION: u32 = 1;
87
88/// Class of repo-intelligence MCP tool (GitNexus-class naming).
89#[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    /// Optional graph query surface (e.g. Cypher).
97    Cypher,
98}
99
100/// Aggregate readiness for repo-intelligence MCP capabilities.
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum RepoIntelCapabilityState {
104    /// At least impact + (query or context) detected across tools.
105    Ready,
106    /// Some classes present but not enough for full blast-radius workflow.
107    Degraded,
108    /// No repo-intelligence tools detected.
109    Absent,
110}
111
112/// Profile returned by [`ainl_repo_intel`](crate) normalization.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RepoIntelCapabilityProfile {
115    pub schema_version: u32,
116    pub state: RepoIntelCapabilityState,
117    /// Which [`RepoIntelToolClass`] values had at least one matching tool.
118    pub classes_present: Vec<RepoIntelToolClass>,
119    /// Human-readable note (optional).
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub note: Option<String>,
122}
123
124/// Freshness of code/repo context for safe edits (independent of inference-server).
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum ContextFreshness {
128    /// Index/context is in sync or confidently current.
129    Fresh,
130    /// Known stale (e.g. index behind HEAD).
131    Stale,
132    /// Cannot determine — treat conservatively in strict modes.
133    Unknown,
134}
135
136/// Decision gate for executing versus gathering more context.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(rename_all = "snake_case")]
139pub enum ImpactDecision {
140    /// Safe to proceed with compile/run per policy.
141    AllowExecute,
142    /// Prefer impact/diff/context tools first.
143    RequireImpactFirst,
144    /// Block run until context is refreshed or user confirms.
145    BlockUntilFresh,
146}
147
148/// One recommended tool step in the impact-first chain (AINL MCP names or logical ids).
149#[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/// Ordered recommendation list (validate → compile → impact/diff → run).
157#[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}