Skip to content

Commit 5d3ea88

Browse files
authored
Merge pull request #263 from AdaWorldAPI/claude/teleport-session-setup-wMZfb
feat: TD-INT 1–14 complete + LF-21/22/90/91/92 + W-1..4
2 parents 70218e4 + d59994b commit 5d3ea88

16 files changed

Lines changed: 1523 additions & 42 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/causal-edge/src/edge.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,37 @@ impl CausalEdge64 {
225225
| (((m as u8 as u64) & BITS3_MASK) << CAUSAL_SHIFT);
226226
}
227227

228+
/// Match if this edge's causal mask contains AT LEAST the bits in `query_mask`.
229+
///
230+
/// `query_mask` is interpreted as the low 3 bits of the Pearl 2³ packing
231+
/// (S=0b100, P=0b010, O=0b001 — see [`CausalMask`]). Higher bits are ignored.
232+
///
233+
/// This is the query-side predicate used by graph WHERE clauses to filter
234+
/// edges by causal type. For example, `query_mask = CausalMask::PO as u8`
235+
/// (`0b011`) matches every edge whose causal mask has at least the P and O
236+
/// planes active — i.e. interventional edges (`PO`) and counterfactual
237+
/// edges (`SPO`), but not pure association (`SO`).
238+
///
239+
/// Semantics:
240+
/// - `query_mask == edge_mask`: full match
241+
/// - `query_mask` is a subset of `edge_mask`: subset match
242+
/// - `query_mask` and `edge_mask` are disjoint (sharing no required bits):
243+
/// no match
244+
/// - `query_mask == 0` (`CausalMask::None`): matches every edge — there
245+
/// are no required bits, so the predicate is vacuously satisfied.
246+
#[inline(always)]
247+
pub const fn matches_causal(&self, query_mask: u8) -> bool {
248+
let q = query_mask & 0b111;
249+
let edge_mask = ((self.0 >> CAUSAL_SHIFT) & BITS3_MASK) as u8;
250+
(edge_mask & q) == q
251+
}
252+
253+
/// Type-safe variant of [`Self::matches_causal`] taking a [`CausalMask`].
254+
#[inline(always)]
255+
pub fn matches_causal_mask(&self, query_mask: CausalMask) -> bool {
256+
self.matches_causal(query_mask as u8)
257+
}
258+
228259
/// Is the S-plane active in the current causal projection?
229260
#[inline(always)]
230261
pub fn s_active(self) -> bool { (self.0 >> CAUSAL_SHIFT) & 0b100 != 0 }
@@ -635,4 +666,91 @@ mod tests {
635666
assert_eq!(std::mem::size_of::<CausalEdge64>(), 8,
636667
"CausalEdge64 must be exactly 8 bytes");
637668
}
669+
670+
// ─── matches_causal: query-side Pearl 2³ predicate (TD-INT-7) ────
671+
672+
fn make_edge(mask: CausalMask) -> CausalEdge64 {
673+
CausalEdge64::pack(
674+
10, 20, 30, 200, 200,
675+
mask, 0, InferenceType::Deduction,
676+
PlasticityState::ALL_FROZEN, 0,
677+
)
678+
}
679+
680+
#[test]
681+
fn test_matches_causal_full_match() {
682+
// query_mask == edge_mask: must match.
683+
let edge = make_edge(CausalMask::PO);
684+
assert!(edge.matches_causal(CausalMask::PO as u8));
685+
assert!(edge.matches_causal_mask(CausalMask::PO));
686+
687+
let edge_spo = make_edge(CausalMask::SPO);
688+
assert!(edge_spo.matches_causal(CausalMask::SPO as u8));
689+
}
690+
691+
#[test]
692+
fn test_matches_causal_subset_match() {
693+
// query_mask is a strict subset of edge_mask: must match.
694+
// SPO (0b111) contains PO (0b011), SO (0b101), SP (0b110), S, P, O.
695+
let edge = make_edge(CausalMask::SPO);
696+
assert!(edge.matches_causal(CausalMask::PO as u8),
697+
"SPO edge should match PO query (PO bits are subset of SPO)");
698+
assert!(edge.matches_causal(CausalMask::SO as u8),
699+
"SPO edge should match SO query");
700+
assert!(edge.matches_causal(CausalMask::P as u8),
701+
"SPO edge should match single-plane P query");
702+
assert!(edge.matches_causal_mask(CausalMask::S));
703+
704+
// PO (0b011) contains O (0b001) and P (0b010), but NOT S (0b100).
705+
let edge_po = make_edge(CausalMask::PO);
706+
assert!(edge_po.matches_causal(CausalMask::O as u8));
707+
assert!(edge_po.matches_causal(CausalMask::P as u8));
708+
}
709+
710+
#[test]
711+
fn test_matches_causal_non_match() {
712+
// query_mask requires bits the edge does not have: must NOT match.
713+
// SO edge (0b101) does NOT have the P plane (0b010).
714+
let edge_so = make_edge(CausalMask::SO);
715+
assert!(!edge_so.matches_causal(CausalMask::P as u8));
716+
assert!(!edge_so.matches_causal(CausalMask::PO as u8),
717+
"SO edge must not match PO query — P bit is missing");
718+
assert!(!edge_so.matches_causal_mask(CausalMask::SPO),
719+
"SO edge must not match SPO query — P bit is missing");
720+
721+
// P-only edge (0b010) does NOT match SO query (0b101).
722+
let edge_p = make_edge(CausalMask::P);
723+
assert!(!edge_p.matches_causal(CausalMask::SO as u8));
724+
assert!(!edge_p.matches_causal(CausalMask::S as u8));
725+
assert!(!edge_p.matches_causal(CausalMask::O as u8));
726+
}
727+
728+
#[test]
729+
fn test_matches_causal_zero_mask_matches_anything() {
730+
// query_mask == 0 has no required bits → vacuously matches every edge.
731+
// This is the documented semantics: zero is the predicate-true element
732+
// of the bit lattice (no requirements means nothing to fail).
733+
for variant in [
734+
CausalMask::None, CausalMask::O, CausalMask::P, CausalMask::PO,
735+
CausalMask::S, CausalMask::SO, CausalMask::SP, CausalMask::SPO,
736+
] {
737+
let edge = make_edge(variant);
738+
assert!(edge.matches_causal(0),
739+
"zero query_mask must match edge with mask {variant:?}");
740+
assert!(edge.matches_causal_mask(CausalMask::None),
741+
"CausalMask::None query must match edge with mask {variant:?}");
742+
}
743+
}
744+
745+
#[test]
746+
fn test_matches_causal_high_bits_ignored() {
747+
// matches_causal must mask query down to the low 3 bits, so callers
748+
// passing a u8 with stray high bits get the same result as passing
749+
// the cleaned value.
750+
let edge = make_edge(CausalMask::PO);
751+
// 0b1111_0011 → low 3 bits = 0b011 = PO.
752+
assert!(edge.matches_causal(0b1111_0011));
753+
// 0b1111_0100 → low 3 bits = 0b100 = S — not present in PO edge.
754+
assert!(!edge.matches_causal(0b1111_0100));
755+
}
638756
}

crates/cognitive-shader-driver/src/cypher_bridge.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,35 @@
2323
//! - Anything else — `StepStatus::Skipped` with "unsupported cypher
2424
//! construct" reasoning. No failure: the downstream can plan around it.
2525
26+
use lance_graph_contract::crystal::fingerprint::CrystalFingerprint;
27+
use lance_graph_contract::grammar::context_chain::{ContextChain, DisambiguationResult};
2628
use lance_graph_contract::nars::InferenceType;
2729
use lance_graph_contract::orchestration::{
2830
OrchestrationBridge, OrchestrationError, StepDomain, StepStatus, UnifiedStep,
2931
};
3032
use lance_graph_contract::plan::ThinkingContext;
3133
use lance_graph_contract::thinking::ThinkingStyle;
3234

35+
/// TD-INT-6 — disambiguation hook for multi-candidate Cypher parses.
36+
///
37+
/// When a real parser returns N candidate parse trees for an ambiguous
38+
/// query, this helper consults the live `ContextChain` to pick the
39+
/// candidate whose insertion-coherence at position `i` is highest.
40+
/// Today's regex stub returns a single candidate, so this is a dormant
41+
/// call site — wire in place; activation lives at parser commit time.
42+
pub fn disambiguate_parse_candidates(
43+
chain: &ContextChain,
44+
position: usize,
45+
candidates: Vec<CrystalFingerprint>,
46+
) -> Result<CrystalFingerprint, DisambiguationResult> {
47+
let result = chain.disambiguate(position, candidates);
48+
if result.escalate_to_llm {
49+
Err(result)
50+
} else {
51+
Ok(result.chosen.clone())
52+
}
53+
}
54+
3355
/// Bridge for `lg.cypher` step_types. Stateless in Phase 1; an SPO store
3456
/// handle slots in here when Phase 2 wires the real parser + BindSpace.
3557
pub struct CypherBridge;
@@ -219,4 +241,21 @@ mod tests {
219241
other => panic!("expected DomainUnavailable, got {:?}", other),
220242
}
221243
}
244+
245+
/// TD-INT-6 — empty candidate list escalates.
246+
#[test]
247+
fn disambiguate_empty_candidates_escalates() {
248+
let chain = ContextChain::new(8);
249+
let result = disambiguate_parse_candidates(&chain, 0, Vec::new());
250+
assert!(result.is_err(), "empty candidates must escalate");
251+
}
252+
253+
/// TD-INT-6 — single candidate escalates (margin = 0).
254+
#[test]
255+
fn disambiguate_single_candidate_escalates() {
256+
let chain = ContextChain::new(8);
257+
let cand = CrystalFingerprint::Binary16K(Box::new([0u64; 256]));
258+
let result = disambiguate_parse_candidates(&chain, 0, vec![cand]);
259+
assert!(result.is_err(), "single candidate must escalate");
260+
}
222261
}

0 commit comments

Comments
 (0)