ainl_improvement_proposals/
lib.rs1mod ledger;
9
10pub use ledger::{
11 AdoptGraphPayload, AdoptResult, ImprovementProposalId, ImprovementProposalListItem,
12 ImprovementProposalRow, ProposalLedger, ProposalLedgerError,
13};
14
15use ainl_contracts::ProposalEnvelope;
16use sha2::{Digest, Sha256};
17
18pub mod proposal_kind {
19 pub const PATTERN_PROMOTE: &str = "pattern_promote";
20 pub const PROCEDURE_MINT: &str = "procedure_mint";
21 pub const PROCEDURE_PATCH: &str = "procedure_patch";
22 pub const PROCEDURE_PROMOTE: &str = "procedure_promote";
23 pub const PROCEDURE_DEPRECATE: &str = "procedure_deprecate";
24 pub const GRAPH_PATCH_FROM_PROCEDURE: &str = "graph_patch_from_procedure";
25}
26
27#[must_use]
28pub fn is_known_proposal_kind(kind: &str) -> bool {
29 matches!(
30 kind,
31 proposal_kind::PATTERN_PROMOTE
32 | proposal_kind::PROCEDURE_MINT
33 | proposal_kind::PROCEDURE_PATCH
34 | proposal_kind::PROCEDURE_PROMOTE
35 | proposal_kind::PROCEDURE_DEPRECATE
36 | proposal_kind::GRAPH_PATCH_FROM_PROCEDURE
37 )
38}
39
40#[must_use]
42pub fn sha256_hex_lower(s: &str) -> String {
43 let d = Sha256::digest(s.as_bytes());
44 hex::encode(d)
45}
46
47#[must_use]
49pub fn proposed_hash_matches(envelope: &ProposalEnvelope, proposed_ainl_text: &str) -> bool {
50 let h = sha256_hex_lower(proposed_ainl_text);
51 h == envelope.proposed_hash.trim().to_ascii_lowercase()
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use crate::{ProposalLedger, ProposalLedgerError};
58 use ainl_contracts::{
59 ContextFreshness, ImpactDecision, ProposalEnvelope, LEARNER_SCHEMA_VERSION,
60 };
61 use uuid::Uuid;
62
63 fn sample_envelope(ainl: &str) -> ProposalEnvelope {
64 let ph = sha256_hex_lower(ainl);
65 ProposalEnvelope {
66 schema_version: LEARNER_SCHEMA_VERSION,
67 original_hash: "a".repeat(64),
68 proposed_hash: ph,
69 kind: "pattern_promote".into(),
70 rationale: "test".into(),
71 freshness_at_proposal: ContextFreshness::Fresh,
72 impact_decision: ImpactDecision::AllowExecute,
73 }
74 }
75
76 #[test]
77 fn procedure_proposal_kinds_are_known() {
78 assert!(is_known_proposal_kind(proposal_kind::PROCEDURE_MINT));
79 assert!(is_known_proposal_kind(proposal_kind::PROCEDURE_PATCH));
80 assert!(!is_known_proposal_kind("unknown"));
81 }
82
83 #[test]
84 fn submit_rejects_hash_mismatch() {
85 let tmp = tempfile::tempdir().unwrap();
86 let db = tmp.path().join("prop.db");
87 let p = ProposalLedger::open(&db).unwrap();
88 let env = sample_envelope("graph\nt1\n");
89 let err = p
90 .submit("a1", &env, "wrong\n")
91 .expect_err("expected mismatch");
92 assert!(matches!(err, ProposalLedgerError::HashMismatch));
93 }
94
95 #[test]
96 fn accept_and_reject_paths() {
97 let tmp = tempfile::tempdir().unwrap();
98 let db = tmp.path().join("prop2.db");
99 let p = ProposalLedger::open(&db).unwrap();
100 let text = "graph\nt1\n";
101 let env = sample_envelope(text);
102 let id = p.submit("a1", &env, text).expect("insert");
103
104 let r_ok = p
105 .validate_and_record(id, |s: &str| if s == text { Ok(()) } else { Err("bad") })
106 .expect("validate");
107 assert!(r_ok.accepted, "{r_ok:?}");
108 let row = p.get(id).unwrap().expect("row");
109 assert!(row.accepted);
110 assert!(row.validation_error.is_none());
111
112 let id2 = p
113 .submit("a1", &sample_envelope("other\n"), "other\n")
114 .expect("insert2");
115 let r_no = p
116 .validate_and_record(id2, |_s: &str| Err::<(), _>("no"))
117 .expect("valid");
118 assert!(!r_no.accepted);
119 assert_eq!(r_no.error.as_deref(), Some("no"));
120 let row2 = p.get(id2).unwrap().expect("row2");
121 assert!(!row2.accepted);
122 assert!(row2.adopted_at.is_none());
123 assert!(p.get(Uuid::new_v4()).unwrap().is_none());
124 }
125
126 #[test]
127 fn mark_adopted_after_validation() {
128 let tmp = tempfile::tempdir().unwrap();
129 let db = tmp.path().join("prop3.db");
130 let p = ProposalLedger::open(&db).unwrap();
131 let text = "graph\nt1\n";
132 let env = sample_envelope(text);
133 let id = p.submit("a1", &env, text).expect("insert");
134 p.validate_and_record(id, |s: &str| if s == text { Ok(()) } else { Err("bad") })
135 .expect("validate");
136 p.mark_adopted(id, "node-1").expect("adopted");
137 let r = p.get(id).unwrap().expect("row");
138 assert!(r.adopted_at.is_some());
139 assert_eq!(r.adopted_graph_node_id.as_deref(), Some("node-1"));
140 }
141}