1#![cfg_attr(test, allow(clippy::expect_used))]
2
3use forge_memory_bridge::{ImportProjectionRecord, ProjectionImportBatchV3};
4use recursive_kernel_core::{constraint_compiler_operator, ConstraintUnit, OperatorMetadata};
5use schemars::JsonSchema;
6use semantic_memory_forge::{ConstraintSeedKind, ExportRecordSemanticsV3};
7use serde::{Deserialize, Serialize};
8use stack_ids::{ConstraintId, ContentDigest, OracleSliceId, RegionDigestId, RegionId, ScopeKey};
9use std::collections::{BTreeMap, BTreeSet};
10
11fn compiler_operator() -> OperatorMetadata {
12 constraint_compiler_operator()
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
16pub struct CompilerPolicy {
17 pub policy_version: String,
18 pub include_hyperedges: bool,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)]
22#[serde(rename_all = "snake_case")]
23pub enum ConstraintDegradation {
24 MissingClaimFamily,
25 MissingAssertionGroup,
26 MissingRelationGroup,
27 ThinExport,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
31pub struct InferenceNode {
32 pub node_id: String,
33 pub kind: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
37pub struct InferenceHyperedge {
38 pub edge_id: String,
39 pub member_node_ids: Vec<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
43pub struct InvalidationCone {
44 pub source_node_id: String,
45 pub affected_node_ids: Vec<String>,
46 pub affected_hyperedge_ids: Vec<String>,
47 pub affected_constraint_ids: Vec<ConstraintId>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
51pub struct OracleSliceCandidate {
52 pub oracle_slice_id: OracleSliceId,
53 pub node_ids: Vec<String>,
54}
55
56#[derive(
57 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
58)]
59#[serde(rename_all = "snake_case")]
60pub enum GraphSurfaceKind {
61 Storage,
62 Retrieval,
63 Inference,
64 Repair,
65 Control,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
69pub struct CompilationBoundary {
70 pub from_surface: GraphSurfaceKind,
71 pub to_surface: GraphSurfaceKind,
72 pub artifact_families: Vec<String>,
73 pub deterministic: bool,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
77pub struct GraphGeometryManifest {
78 pub surfaces: Vec<GraphSurfaceKind>,
79 pub compilation_boundaries: Vec<CompilationBoundary>,
80 pub no_silent_collapse: bool,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
84pub struct CompiledRegion {
85 pub region_id: RegionId,
86 pub region_digest_id: RegionDigestId,
87 pub node_ids: Vec<String>,
88 pub hyperedge_ids: Vec<String>,
89 pub constraint_ids: Vec<ConstraintId>,
90 pub bounded_default_unit_of_work: bool,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
94pub struct CompileOutput {
95 pub graph_hash: ContentDigest,
96 pub scope_key: ScopeKey,
97 pub geometry_manifest: GraphGeometryManifest,
98 pub nodes: Vec<InferenceNode>,
99 pub hyperedges: Vec<InferenceHyperedge>,
100 pub constraints: Vec<ConstraintUnit>,
101 pub regions: Vec<CompiledRegion>,
102 pub invalidation_cones: Vec<InvalidationCone>,
103 pub degradations: Vec<ConstraintDegradation>,
104 pub oracle_candidates: Vec<OracleSliceCandidate>,
105}
106
107pub fn compile_batch(batch: &ProjectionImportBatchV3, policy: &CompilerPolicy) -> CompileOutput {
109 let compiler = compiler_operator();
110 let mut nodes = BTreeMap::<String, InferenceNode>::new();
111 let mut hyperedge_members = BTreeMap::<String, BTreeSet<String>>::new();
112 let mut hyperedge_kinds = BTreeMap::<String, String>::new();
113 let mut degradations = Vec::new();
114 let mut nuisance_constraints = Vec::new();
115 let auxiliary_records_stay_thin = batch.records.iter().any(|record| {
116 record.semantics.as_ref().is_some_and(|semantics| {
117 semantics.export_confidence_class
118 == semantic_memory_forge::ExportConfidenceClass::ThinExport
119 })
120 });
121
122 for rich_record in &batch.records {
123 if !matches!(
124 rich_record.record,
125 ImportProjectionRecord::ClaimVersion(_) | ImportProjectionRecord::RelationVersion(_)
126 ) {
127 if rich_record.semantics.is_none()
128 && (matches!(rich_record.record, ImportProjectionRecord::Episode(_))
129 || (auxiliary_records_stay_thin
130 && matches!(
131 rich_record.record,
132 ImportProjectionRecord::EntityAlias(_)
133 | ImportProjectionRecord::EvidenceRef(_)
134 )))
135 {
136 degradations.push(ConstraintDegradation::ThinExport);
137 }
138 continue;
139 }
140
141 let node_id = stable_node_id(&rich_record.record);
142 let kind = record_kind(&rich_record.record).to_string();
143 nodes.insert(
144 node_id.clone(),
145 InferenceNode {
146 node_id: node_id.clone(),
147 kind,
148 },
149 );
150
151 match &rich_record.semantics {
152 Some(semantics) => {
153 materialize_nuisance_state(
154 &mut nodes,
155 &mut hyperedge_members,
156 &mut hyperedge_kinds,
157 &mut nuisance_constraints,
158 &node_id,
159 semantics,
160 policy.include_hyperedges,
161 );
162 match rich_record.record {
163 ImportProjectionRecord::ClaimVersion(_) => {
164 if policy.include_hyperedges {
165 if let Some(group_id) = &semantics.assertion_group_id {
166 let edge_id = format!("assertion_group:{group_id}");
167 hyperedge_members
168 .entry(edge_id.clone())
169 .or_default()
170 .insert(node_id.clone());
171 hyperedge_kinds.entry(edge_id).or_insert_with(|| {
172 constraint_seed_name(semantics.constraint_seed_kind.as_ref())
173 });
174 } else {
175 degradations.push(ConstraintDegradation::MissingAssertionGroup);
176 }
177 }
178 }
179 ImportProjectionRecord::RelationVersion(_) => {
180 if policy.include_hyperedges {
181 if let Some(group_id) = &semantics.relation_group_id {
182 let edge_id = format!("relation_group:{group_id}");
183 hyperedge_members
184 .entry(edge_id.clone())
185 .or_default()
186 .insert(node_id.clone());
187 hyperedge_kinds.entry(edge_id).or_insert_with(|| {
188 constraint_seed_name(semantics.constraint_seed_kind.as_ref())
189 });
190 } else {
191 degradations.push(ConstraintDegradation::MissingRelationGroup);
192 }
193 }
194 }
195 ImportProjectionRecord::Episode(_)
196 | ImportProjectionRecord::EntityAlias(_)
197 | ImportProjectionRecord::EvidenceRef(_) => {}
198 }
199 }
200 None => {
201 degradations.push(ConstraintDegradation::ThinExport);
202 }
203 }
204
205 if let Some(semantics) = &rich_record.semantics {
206 if semantics.claim_family_id.is_none()
207 && matches!(rich_record.record, ImportProjectionRecord::ClaimVersion(_))
208 {
209 degradations.push(ConstraintDegradation::MissingClaimFamily);
210 }
211 if semantics.relation_group_id.is_none()
212 && matches!(
213 rich_record.record,
214 ImportProjectionRecord::RelationVersion(_)
215 )
216 {
217 degradations.push(ConstraintDegradation::MissingRelationGroup);
218 }
219 }
220 }
221
222 let mut nodes: Vec<_> = nodes.into_values().collect();
223 nodes.sort_by(|a, b| a.node_id.cmp(&b.node_id));
224
225 let mut hyperedges = Vec::new();
226 let mut constraints = Vec::new();
227 for (edge_id, members) in hyperedge_members {
228 let mut member_node_ids: Vec<_> = members.into_iter().collect();
229 member_node_ids.sort();
230
231 hyperedges.push(InferenceHyperedge {
232 edge_id: edge_id.clone(),
233 member_node_ids: member_node_ids.clone(),
234 });
235 constraints.push(ConstraintUnit {
236 constraint_id: ConstraintId::new(format!("constraint:{edge_id}")),
237 kind: hyperedge_kinds
238 .get(&edge_id)
239 .cloned()
240 .unwrap_or_else(|| "group".into()),
241 variable_ids: member_node_ids,
242 operator_id: compiler.operator_id.clone(),
243 });
244 }
245 constraints.extend(nuisance_constraints);
246
247 hyperedges.sort_by(|a, b| a.edge_id.cmp(&b.edge_id));
248 constraints.sort_by(|a, b| a.constraint_id.as_str().cmp(b.constraint_id.as_str()));
249 let geometry_manifest = graph_geometry_manifest();
250 let regions = compile_regions(&nodes, &hyperedges, &constraints);
251 let invalidation_cones = build_invalidation_cones(&hyperedges, &constraints);
252 degradations.sort();
253 degradations.dedup();
254
255 let oracle_candidate_node_ids = nodes
256 .iter()
257 .filter(|node| node.kind != "nuisance_state")
258 .map(|node| node.node_id.clone())
259 .collect::<Vec<_>>();
260 let oracle_candidates = if degradations.is_empty()
261 && !oracle_candidate_node_ids.is_empty()
262 && oracle_candidate_node_ids.len() <= 8
263 {
264 vec![OracleSliceCandidate {
265 oracle_slice_id: OracleSliceId::new(format!("oracle:{}", batch.source_envelope_id)),
266 node_ids: oracle_candidate_node_ids,
267 }]
268 } else {
269 vec![]
270 };
271
272 let graph_hash = match ContentDigest::compute_json(&(
273 policy,
274 &batch.scope_key,
275 &geometry_manifest,
276 &nodes,
277 &hyperedges,
278 &constraints,
279 ®ions,
280 &invalidation_cones,
281 °radations,
282 )) {
283 Ok(digest) => digest,
284 Err(err) => ContentDigest::compute_str(&format!(
285 "constraint-compiler.graph-hash-serialization-error:{err}"
286 )),
287 };
288
289 CompileOutput {
290 graph_hash,
291 scope_key: batch.scope_key.clone(),
292 geometry_manifest,
293 nodes,
294 hyperedges,
295 constraints,
296 regions,
297 invalidation_cones,
298 degradations,
299 oracle_candidates,
300 }
301}
302
303fn graph_geometry_manifest() -> GraphGeometryManifest {
304 GraphGeometryManifest {
305 surfaces: vec![
306 GraphSurfaceKind::Storage,
307 GraphSurfaceKind::Retrieval,
308 GraphSurfaceKind::Inference,
309 GraphSurfaceKind::Repair,
310 GraphSurfaceKind::Control,
311 ],
312 compilation_boundaries: vec![
313 CompilationBoundary {
314 from_surface: GraphSurfaceKind::Storage,
315 to_surface: GraphSurfaceKind::Retrieval,
316 artifact_families: vec!["candidate_expansion".into(), "query_scope".into()],
317 deterministic: true,
318 },
319 CompilationBoundary {
320 from_surface: GraphSurfaceKind::Retrieval,
321 to_surface: GraphSurfaceKind::Inference,
322 artifact_families: vec!["candidate_claims".into(), "evidence_refs".into()],
323 deterministic: true,
324 },
325 CompilationBoundary {
326 from_surface: GraphSurfaceKind::Inference,
327 to_surface: GraphSurfaceKind::Repair,
328 artifact_families: vec![
329 "syndrome".into(),
330 "witness".into(),
331 "invalidation_manifest".into(),
332 ],
333 deterministic: true,
334 },
335 CompilationBoundary {
336 from_surface: GraphSurfaceKind::Control,
337 to_surface: GraphSurfaceKind::Inference,
338 artifact_families: vec!["region_budget".into(), "execution_budget".into()],
339 deterministic: true,
340 },
341 CompilationBoundary {
342 from_surface: GraphSurfaceKind::Inference,
343 to_surface: GraphSurfaceKind::Control,
344 artifact_families: vec!["control_receipt".into(), "residual".into()],
345 deterministic: true,
346 },
347 ],
348 no_silent_collapse: true,
349 }
350}
351
352fn compile_regions(
353 nodes: &[InferenceNode],
354 hyperedges: &[InferenceHyperedge],
355 constraints: &[ConstraintUnit],
356) -> Vec<CompiledRegion> {
357 let mut node_neighbors = BTreeMap::<String, BTreeSet<String>>::new();
358 let mut edges_by_node = BTreeMap::<String, BTreeSet<String>>::new();
359
360 for node in nodes {
361 node_neighbors.entry(node.node_id.clone()).or_default();
362 edges_by_node.entry(node.node_id.clone()).or_default();
363 }
364
365 for hyperedge in hyperedges {
366 for node_id in &hyperedge.member_node_ids {
367 edges_by_node
368 .entry(node_id.clone())
369 .or_default()
370 .insert(hyperedge.edge_id.clone());
371 for peer in &hyperedge.member_node_ids {
372 if peer != node_id {
373 node_neighbors
374 .entry(node_id.clone())
375 .or_default()
376 .insert(peer.clone());
377 }
378 }
379 }
380 }
381
382 let mut visited = BTreeSet::new();
383 let constraint_lookup = constraints
384 .iter()
385 .map(|constraint| {
386 (
387 constraint
388 .constraint_id
389 .as_str()
390 .strip_prefix("constraint:")
391 .unwrap_or(constraint.constraint_id.as_str())
392 .to_string(),
393 constraint.constraint_id.clone(),
394 )
395 })
396 .collect::<BTreeMap<_, _>>();
397 let mut regions = Vec::new();
398
399 for node in nodes {
400 if !visited.insert(node.node_id.clone()) {
401 continue;
402 }
403
404 let mut queue = vec![node.node_id.clone()];
405 let mut node_ids = BTreeSet::new();
406 let mut hyperedge_ids = BTreeSet::new();
407 let mut constraint_ids = BTreeSet::new();
408
409 while let Some(current) = queue.pop() {
410 node_ids.insert(current.clone());
411
412 if let Some(edge_ids) = edges_by_node.get(¤t) {
413 for edge_id in edge_ids {
414 hyperedge_ids.insert(edge_id.clone());
415 if let Some(constraint_id) = constraint_lookup.get(edge_id) {
416 constraint_ids.insert(constraint_id.clone());
417 }
418 }
419 }
420
421 if let Some(neighbors) = node_neighbors.get(¤t) {
422 for neighbor in neighbors {
423 if visited.insert(neighbor.clone()) {
424 queue.push(neighbor.clone());
425 }
426 }
427 }
428 }
429
430 let node_ids = node_ids.into_iter().collect::<Vec<_>>();
431 let hyperedge_ids = hyperedge_ids.into_iter().collect::<Vec<_>>();
432 let constraint_ids = constraint_ids.into_iter().collect::<Vec<_>>();
433 let region_digest =
434 match ContentDigest::compute_json(&(&node_ids, &hyperedge_ids, &constraint_ids)) {
435 Ok(digest) => digest,
436 Err(err) => ContentDigest::compute_str(&format!(
437 "constraint-compiler.region-digest-serialization-error:{err}"
438 )),
439 };
440 regions.push(CompiledRegion {
441 region_id: RegionId::new(format!("region:{}", region_digest.hex())),
442 region_digest_id: RegionDigestId::new(format!("region-digest:{}", region_digest.hex())),
443 node_ids,
444 hyperedge_ids,
445 constraint_ids,
446 bounded_default_unit_of_work: true,
447 });
448 }
449
450 regions.sort_by(|a, b| a.region_id.as_str().cmp(b.region_id.as_str()));
451 regions
452}
453
454fn constraint_seed_name(kind: Option<&ConstraintSeedKind>) -> String {
455 match kind {
456 Some(ConstraintSeedKind::Hyperedge) => "hyperedge".into(),
457 Some(ConstraintSeedKind::MutualExclusion) => "mutual_exclusion".into(),
458 Some(ConstraintSeedKind::TemporalCoherence) => "temporal_coherence".into(),
459 Some(ConstraintSeedKind::CausalRefutation) => "causal_refutation".into(),
460 Some(ConstraintSeedKind::NuisanceDisclosure) => "nuisance_disclosure".into(),
461 None => "group".into(),
462 }
463}
464
465fn materialize_nuisance_state(
466 nodes: &mut BTreeMap<String, InferenceNode>,
467 hyperedge_members: &mut BTreeMap<String, BTreeSet<String>>,
468 hyperedge_kinds: &mut BTreeMap<String, String>,
469 nuisance_constraints: &mut Vec<ConstraintUnit>,
470 source_node_id: &str,
471 semantics: &ExportRecordSemanticsV3,
472 include_hyperedges: bool,
473) {
474 let Some(nuisance_key) = nuisance_node_id(semantics) else {
475 return;
476 };
477
478 nodes
479 .entry(nuisance_key.clone())
480 .or_insert_with(|| InferenceNode {
481 node_id: nuisance_key.clone(),
482 kind: "nuisance_state".into(),
483 });
484
485 let edge_id = format!("nuisance_edge:{source_node_id}:{nuisance_key}");
486 if include_hyperedges {
487 hyperedge_members
488 .entry(edge_id.clone())
489 .or_default()
490 .extend([source_node_id.to_string(), nuisance_key.clone()]);
491 hyperedge_kinds
492 .entry(edge_id.clone())
493 .or_insert_with(|| "nuisance_disclosure".into());
494 return;
495 }
496
497 nuisance_constraints.push(ConstraintUnit {
498 constraint_id: ConstraintId::new(format!("constraint:{edge_id}")),
499 kind: "nuisance_disclosure".into(),
500 variable_ids: vec![source_node_id.to_string(), nuisance_key],
501 operator_id: compiler_operator().operator_id,
502 });
503}
504
505fn nuisance_node_id(semantics: &ExportRecordSemanticsV3) -> Option<String> {
506 fn hash_sorted_values(builder: &mut blake3::Hasher, values: &[String]) {
507 let mut normalized = values.to_vec();
508 normalized.sort();
509 normalized.dedup();
510 for value in normalized {
511 builder.update(value.as_bytes());
512 }
513 }
514
515 if let Some(version) = semantics.comparability_snapshot_version.as_ref() {
516 return Some(format!("nuisance:comparability:{version}"));
517 }
518
519 let snapshot = semantics.nuisance_snapshot.as_ref()?;
520 let mut builder = blake3::Hasher::new();
521 if let Some(value) = snapshot.environment_fingerprint.as_deref() {
522 builder.update(value.as_bytes());
523 }
524 if let Some(value) = snapshot.toolchain_version.as_deref() {
525 builder.update(value.as_bytes());
526 }
527 if let Some(value) = snapshot.dependency_set_hash.as_deref() {
528 builder.update(value.as_bytes());
529 }
530 hash_sorted_values(&mut builder, &snapshot.scope_mismatch_markers);
531 hash_sorted_values(&mut builder, &snapshot.measurement_notes);
532 hash_sorted_values(&mut builder, &snapshot.selection_bias_markers);
533 Some(format!("nuisance:snapshot:{}", builder.finalize().to_hex()))
534}
535
536fn build_invalidation_cones(
537 hyperedges: &[InferenceHyperedge],
538 constraints: &[ConstraintUnit],
539) -> Vec<InvalidationCone> {
540 let mut edges_by_node = BTreeMap::<String, BTreeSet<String>>::new();
541 for hyperedge in hyperedges {
542 for node_id in &hyperedge.member_node_ids {
543 edges_by_node
544 .entry(node_id.clone())
545 .or_default()
546 .insert(hyperedge.edge_id.clone());
547 }
548 }
549
550 let constraints_by_edge: BTreeMap<_, _> = constraints
551 .iter()
552 .map(|constraint| {
553 (
554 constraint
555 .constraint_id
556 .as_str()
557 .strip_prefix("constraint:")
558 .unwrap_or(constraint.constraint_id.as_str())
559 .to_string(),
560 constraint.constraint_id.clone(),
561 )
562 })
563 .collect();
564
565 let mut cones = Vec::new();
566 for (source_node_id, edge_ids) in edges_by_node {
567 let mut affected_node_ids = BTreeSet::new();
568 let mut affected_constraint_ids = BTreeSet::new();
569
570 for edge_id in &edge_ids {
571 if let Some(hyperedge) = hyperedges.iter().find(|edge| edge.edge_id == *edge_id) {
572 affected_node_ids.extend(hyperedge.member_node_ids.iter().cloned());
573 }
574 if let Some(constraint_id) = constraints_by_edge.get(edge_id) {
575 affected_constraint_ids.insert(constraint_id.clone());
576 }
577 }
578
579 cones.push(InvalidationCone {
580 source_node_id,
581 affected_node_ids: affected_node_ids.into_iter().collect(),
582 affected_hyperedge_ids: edge_ids.into_iter().collect(),
583 affected_constraint_ids: affected_constraint_ids.into_iter().collect(),
584 });
585 }
586 cones.sort_by(|a, b| a.source_node_id.cmp(&b.source_node_id));
587 cones
588}
589
590fn stable_node_id(record: &ImportProjectionRecord) -> String {
591 match record {
592 ImportProjectionRecord::ClaimVersion(claim) => claim.claim_version_id.to_string(),
593 ImportProjectionRecord::RelationVersion(relation) => {
594 relation.relation_version_id.to_string()
595 }
596 ImportProjectionRecord::Episode(episode) => episode.episode_id.to_string(),
597 ImportProjectionRecord::EntityAlias(alias) => {
598 format!("alias:{}:{}", alias.canonical_entity_id, alias.alias_text)
599 }
600 ImportProjectionRecord::EvidenceRef(evidence) => {
601 format!("evidence:{}:{}", evidence.claim_id, evidence.fetch_handle)
602 }
603 }
604}
605
606fn record_kind(record: &ImportProjectionRecord) -> &'static str {
607 match record {
608 ImportProjectionRecord::ClaimVersion(_) => "claim_version",
609 ImportProjectionRecord::RelationVersion(_) => "relation_version",
610 ImportProjectionRecord::Episode(_) => "episode",
611 ImportProjectionRecord::EntityAlias(_) => "entity_alias",
612 ImportProjectionRecord::EvidenceRef(_) => "evidence_ref",
613 }
614}
615
616#[cfg(test)]
617#[path = "lib_tests.rs"]
618mod tests;