1use std::collections::hash_map::DefaultHasher;
8use std::hash::{Hash, Hasher};
9
10use xlog_core::RelId;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum Topology {
15 Chain,
17 Star,
19 Fanout,
21 Fanin,
23}
24
25impl Topology {
26 pub const ALL: [Topology; 4] = [
28 Topology::Chain,
29 Topology::Star,
30 Topology::Fanout,
31 Topology::Fanin,
32 ];
33
34 pub fn as_str(&self) -> &'static str {
36 match self {
37 Topology::Chain => "chain",
38 Topology::Star => "star",
39 Topology::Fanout => "fanout",
40 Topology::Fanin => "fanin",
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy)]
47pub struct ExactInductionConfig {
48 pub k_per_topology: u32,
50 pub deterministic: bool,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct ScoredCandidate {
57 pub topology: Topology,
58 pub head_rel_idx: RelId,
59 pub left_rel_idx: RelId,
60 pub right_rel_idx: RelId,
61 pub positives_covered: u32,
62 pub negatives_covered: u32,
63 pub local_rank: u32,
65 pub next_positives_covered: u32,
68 pub next_negatives_covered: u32,
70 pub tie_class_size: u32,
72}
73
74#[derive(Debug, Default, Clone, PartialEq, Eq)]
76pub struct ExactInductionResult {
77 pub candidates: Vec<ScoredCandidate>,
79 pub total_scored: u32,
81 pub candidate_count: u32,
83 pub positive_count: u32,
85 pub negative_count: u32,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub enum RuleSourceKind {
92 Source,
94 Generated,
96 Mined,
98 Imported,
100 RuntimeInjected,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct InductionSupportRow {
107 pub relation: String,
109 pub row_index: u64,
111 pub row_hash: String,
113}
114
115impl InductionSupportRow {
116 pub fn new(relation: impl Into<String>, row_index: u64, row_hash: impl Into<String>) -> Self {
118 Self {
119 relation: relation.into(),
120 row_index,
121 row_hash: row_hash.into(),
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct InductionAlternative {
129 pub rule_fragment: String,
131 pub support_count: u64,
133 pub falsification_count: u64,
135}
136
137impl InductionAlternative {
138 pub fn new(
140 rule_fragment: impl Into<String>,
141 support_count: u64,
142 falsification_count: u64,
143 ) -> Self {
144 Self {
145 rule_fragment: rule_fragment.into(),
146 support_count,
147 falsification_count,
148 }
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct InducedRuleProvenance {
155 pub rule_id: String,
157 pub rule_fragment: String,
159 pub source_kind: RuleSourceKind,
161 pub generation_trace_hash: String,
163 pub search_space_size: u64,
165 pub predicate_inventory: Vec<String>,
167 pub support_rows: Vec<InductionSupportRow>,
169 pub rejected_alternatives: Vec<InductionAlternative>,
171 pub falsification_count: u64,
173}
174
175impl InducedRuleProvenance {
176 pub fn new(
178 rule_fragment: impl Into<String>,
179 search_space_size: u64,
180 predicate_inventory: Vec<String>,
181 ) -> Self {
182 let rule_fragment = rule_fragment.into();
183 let generation_trace_hash =
184 provenance_hash(&rule_fragment, search_space_size, &predicate_inventory);
185 Self {
186 rule_id: format!("generated:{}", generation_trace_hash),
187 rule_fragment,
188 source_kind: RuleSourceKind::Generated,
189 generation_trace_hash,
190 search_space_size,
191 predicate_inventory,
192 support_rows: Vec::new(),
193 rejected_alternatives: Vec::new(),
194 falsification_count: 0,
195 }
196 }
197
198 pub fn with_support_rows(mut self, support_rows: Vec<InductionSupportRow>) -> Self {
200 self.support_rows = support_rows;
201 self
202 }
203
204 pub fn with_rejected_alternatives(
206 mut self,
207 rejected_alternatives: Vec<InductionAlternative>,
208 ) -> Self {
209 self.rejected_alternatives = rejected_alternatives;
210 self
211 }
212
213 pub fn with_falsification_count(mut self, falsification_count: u64) -> Self {
215 self.falsification_count = falsification_count;
216 self
217 }
218}
219
220#[derive(Debug, Default, Clone, PartialEq, Eq)]
222pub struct InducedRuleRegistry {
223 rules: Vec<InducedRuleProvenance>,
224}
225
226impl InducedRuleRegistry {
227 pub fn new() -> Self {
229 Self::default()
230 }
231
232 pub fn register(&mut self, provenance: InducedRuleProvenance) -> String {
234 let rule_id = provenance.rule_id.clone();
235 self.rules.push(provenance);
236 rule_id
237 }
238
239 pub fn len(&self) -> usize {
241 self.rules.len()
242 }
243
244 pub fn is_empty(&self) -> bool {
246 self.rules.is_empty()
247 }
248
249 pub fn rules(&self) -> &[InducedRuleProvenance] {
251 &self.rules
252 }
253}
254
255fn provenance_hash(
256 rule_fragment: &str,
257 search_space_size: u64,
258 predicate_inventory: &[String],
259) -> String {
260 let mut hasher = DefaultHasher::new();
261 rule_fragment.hash(&mut hasher);
262 search_space_size.hash(&mut hasher);
263 predicate_inventory.hash(&mut hasher);
264 format!("{:016x}", hasher.finish())
265}