Skip to content

Commit 748996f

Browse files
committed
PR-G3: wire optional real fingerprint into ContextChain sentinel path
Replace the hardcoded zero Binary16K placeholder in the empty-candidates sentinel of disambiguate() with a caller-injectable fingerprint. Since the contract crate is zero-dep and MarkovBundler lives in deepnsm, the approach adds disambiguate_with_fingerprint() and disambiguate_with_kernel_and_fingerprint() methods that accept an optional CrystalFingerprint. When Some, the sentinel uses it; when None, falls back to the original zero placeholder (backwards compatible). Adds 3 tests: provided fp propagates, None falls back to zero, non-empty candidates ignore the injected fp. https://claude.ai/code/session_01NYGrxVopyszZYgLBxe4hgj
1 parent 77c6292 commit 748996f

1 file changed

Lines changed: 170 additions & 6 deletions

File tree

crates/lance-graph-contract/src/grammar/context_chain.rs

Lines changed: 170 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,14 @@ pub struct ContextChain {
5353
/// existing consumers).
5454
///
5555
/// Empty-candidates contract: returns a sentinel result with
56-
/// `candidate_count = 0`, `winner_index = usize::MAX`, a zero
57-
/// `Binary16K` placeholder fingerprint, and `escalate_to_llm = true`.
58-
/// Callers should check `candidate_count == 0` (or `escalate_to_llm`)
59-
/// before reading `winner` / `chosen`.
56+
/// `candidate_count = 0`, `winner_index = usize::MAX`,
57+
/// `escalate_to_llm = true`, and either the caller-supplied
58+
/// `chosen_fingerprint` (when provided via
59+
/// `disambiguate_with_fingerprint` or
60+
/// `disambiguate_with_kernel_and_fingerprint`) or a zero `Binary16K`
61+
/// placeholder (the backwards-compatible default). Callers should
62+
/// check `candidate_count == 0` (or `escalate_to_llm`) before
63+
/// reading `winner` / `chosen`.
6064
#[derive(Debug, Clone)]
6165
pub struct DisambiguationResult {
6266
pub chosen: CrystalFingerprint,
@@ -310,6 +314,33 @@ impl ContextChain {
310314
self.disambiguate_with_kernel(i, candidates, WeightingKernel::default())
311315
}
312316

317+
/// Disambiguate with an externally-supplied fingerprint for the
318+
/// empty-candidates sentinel path. When `chosen_fingerprint` is
319+
/// `Some(fp)`, that fingerprint replaces the zero `Binary16K`
320+
/// placeholder in the sentinel result — allowing callers that have
321+
/// access to `MarkovBundler::role_bundle()` (in `deepnsm`) to
322+
/// inject the real bundled trajectory fingerprint without the
323+
/// contract crate taking a dependency on `deepnsm`.
324+
///
325+
/// When `chosen_fingerprint` is `None`, falls back to the original
326+
/// zero-sentinel behaviour (backwards compatible).
327+
pub fn disambiguate_with_fingerprint<I>(
328+
&self,
329+
i: usize,
330+
candidates: I,
331+
chosen_fingerprint: Option<CrystalFingerprint>,
332+
) -> DisambiguationResult
333+
where
334+
I: IntoIterator<Item = CrystalFingerprint>,
335+
{
336+
self.disambiguate_with_kernel_and_fingerprint(
337+
i,
338+
candidates,
339+
WeightingKernel::default(),
340+
chosen_fingerprint,
341+
)
342+
}
343+
313344
/// Kernel-aware variant of `disambiguate`. Identical contract; the
314345
/// supplied `kernel` is used when scoring each candidate replay via
315346
/// `total_coherence_with_kernel`.
@@ -319,6 +350,32 @@ impl ContextChain {
319350
candidates: I,
320351
kernel: WeightingKernel,
321352
) -> DisambiguationResult
353+
where
354+
I: IntoIterator<Item = CrystalFingerprint>,
355+
{
356+
self.disambiguate_with_kernel_and_fingerprint(
357+
i, candidates, kernel, None,
358+
)
359+
}
360+
361+
/// Full variant: kernel-aware disambiguation with an optional
362+
/// externally-supplied fingerprint for the empty-candidates sentinel.
363+
///
364+
/// When `chosen_fingerprint` is `Some(fp)`, the sentinel result uses
365+
/// `fp` instead of the zero `Binary16K` placeholder. This allows
366+
/// callers in `deepnsm` (which has access to `MarkovBundler`) to
367+
/// inject the real role-bundled trajectory fingerprint.
368+
///
369+
/// When `chosen_fingerprint` is `None`, the sentinel falls back to
370+
/// the zero `Binary16K` — preserving backwards compatibility with
371+
/// all existing callers.
372+
pub fn disambiguate_with_kernel_and_fingerprint<I>(
373+
&self,
374+
i: usize,
375+
candidates: I,
376+
kernel: WeightingKernel,
377+
chosen_fingerprint: Option<CrystalFingerprint>,
378+
) -> DisambiguationResult
322379
where
323380
I: IntoIterator<Item = CrystalFingerprint>,
324381
{
@@ -342,8 +399,15 @@ impl ContextChain {
342399
if scored.is_empty() {
343400
// Documented sentinel — never panic; callers gate on
344401
// `escalate_to_llm` or `candidate_count == 0`.
345-
let placeholder =
346-
CrystalFingerprint::Binary16K(Box::new([0u64; 256]));
402+
//
403+
// When a `chosen_fingerprint` is supplied (e.g. from
404+
// `MarkovBundler::role_bundle()` in deepnsm), use it
405+
// instead of the zero placeholder. This is the PR-G3
406+
// bridge: the contract crate stays zero-dep while the
407+
// caller injects the real bundled trajectory fingerprint.
408+
let placeholder = chosen_fingerprint.unwrap_or_else(|| {
409+
CrystalFingerprint::Binary16K(Box::new([0u64; 256]))
410+
});
347411
return DisambiguationResult {
348412
chosen: placeholder.clone(),
349413
coherence: 0.0,
@@ -804,6 +868,106 @@ mod tests {
804868
}
805869
}
806870

871+
// ── PR-G3 tests: real fingerprint in sentinel path ────────────────
872+
873+
/// `disambiguate_with_fingerprint` with empty candidates and a
874+
/// `Some(fp)` chosen_fingerprint propagates that fingerprint into
875+
/// the sentinel result instead of the zero placeholder.
876+
#[test]
877+
fn g3_sentinel_uses_provided_fingerprint() {
878+
let chain = fill_chain_with(&mk_fp(0x1));
879+
let real_fp = mk_fp(0xBEEF_CAFE_DEAD_F00D);
880+
let res = chain.disambiguate_with_fingerprint(
881+
0,
882+
Vec::<CrystalFingerprint>::new(),
883+
Some(real_fp.clone()),
884+
);
885+
// Sentinel metadata is unchanged.
886+
assert_eq!(res.candidate_count, 0);
887+
assert_eq!(res.winner_index, usize::MAX);
888+
assert!(res.escalate_to_llm, "empty must still escalate");
889+
assert!(res.alternatives.is_empty());
890+
assert_eq!(res.coherence, 0.0);
891+
assert_eq!(res.margin, 0.0);
892+
assert_eq!(res.dispersion, 0.0);
893+
// Winner and chosen carry the provided fingerprint, NOT zeros.
894+
match (&res.winner, &real_fp) {
895+
(CrystalFingerprint::Binary16K(a),
896+
CrystalFingerprint::Binary16K(b)) => {
897+
assert_eq!(**a, **b,
898+
"sentinel winner must be the provided fingerprint");
899+
}
900+
_ => panic!("expected Binary16K variant"),
901+
}
902+
match (&res.chosen, &real_fp) {
903+
(CrystalFingerprint::Binary16K(a),
904+
CrystalFingerprint::Binary16K(b)) => {
905+
assert_eq!(**a, **b,
906+
"sentinel chosen must be the provided fingerprint");
907+
}
908+
_ => panic!("expected Binary16K variant"),
909+
}
910+
// Verify it's NOT all zeros.
911+
match &res.winner {
912+
CrystalFingerprint::Binary16K(bits) => {
913+
assert!(!bits.iter().all(|&w| w == 0),
914+
"provided fingerprint must NOT be all-zero");
915+
}
916+
_ => unreachable!(),
917+
}
918+
}
919+
920+
/// `disambiguate_with_fingerprint` with `None` falls back to
921+
/// the zero sentinel — same as the original `disambiguate`.
922+
#[test]
923+
fn g3_sentinel_none_falls_back_to_zero() {
924+
let chain = fill_chain_with(&mk_fp(0x1));
925+
let res = chain.disambiguate_with_fingerprint(
926+
0,
927+
Vec::<CrystalFingerprint>::new(),
928+
None,
929+
);
930+
assert_eq!(res.candidate_count, 0);
931+
assert_eq!(res.winner_index, usize::MAX);
932+
assert!(res.escalate_to_llm);
933+
match &res.winner {
934+
CrystalFingerprint::Binary16K(bits) => {
935+
assert!(bits.iter().all(|&w| w == 0),
936+
"None should produce zero sentinel");
937+
}
938+
_ => panic!("sentinel must be Binary16K"),
939+
}
940+
}
941+
942+
/// `disambiguate_with_kernel_and_fingerprint` with non-empty
943+
/// candidates ignores the chosen_fingerprint (it only applies
944+
/// to the empty-candidates sentinel path).
945+
#[test]
946+
fn g3_nonempty_candidates_ignore_chosen_fingerprint() {
947+
let base = mk_fp(0x1111_2222_3333_4444);
948+
let mut chain = fill_chain_with(&base);
949+
chain.fingerprints[3] = None;
950+
951+
let injected = mk_fp(0xFFFF_FFFF_FFFF_FFFF);
952+
let res = chain.disambiguate_with_kernel_and_fingerprint(
953+
3,
954+
vec![base.clone()],
955+
WeightingKernel::default(),
956+
Some(injected),
957+
);
958+
// With one candidate, the winner is that candidate, not the
959+
// injected fingerprint.
960+
assert_eq!(res.candidate_count, 1);
961+
match (&res.winner, &base) {
962+
(CrystalFingerprint::Binary16K(a),
963+
CrystalFingerprint::Binary16K(b)) => {
964+
assert_eq!(**a, **b,
965+
"winner must be the actual candidate, not the injected fp");
966+
}
967+
_ => panic!("expected Binary16K variant"),
968+
}
969+
}
970+
807971
/// `WeightingKernel::default()` is `MexicanHat` (D4 chose this as
808972
/// the canonical kernel — focal-emphasizing with anticipation tail).
809973
#[test]

0 commit comments

Comments
 (0)