Skip to main content

deepstrike_core/
lexical.rs

1//! Shared lexical vocabulary for deterministic in-kernel value scoring.
2//!
3//! Compression utility (goal overlap, directive dependency) and memory curation
4//! (fuzzy dedupe) must speak ONE term vocabulary — and it must discriminate for
5//! CJK text, where there is no whitespace between words: per-character unigrams
6//! make every Chinese unit overlap every Chinese goal, and whitespace tokens
7//! make a whole Chinese sentence a single token. Terms here are lowercased word
8//! runs plus Han character bigrams, matching the host-side recall rankers
9//! (node `memory/ranking.ts`, python `memory/ranking.py`) so knowledge
10//! residency, compression, and memory recall share one notion of "relevant".
11
12use std::collections::BTreeSet;
13
14/// Deterministic term set of `text`.
15///
16/// - ASCII word runs (`[a-z0-9_\-/.:]`, lowercased) of more than one char are
17///   one term each — call ids, paths, and URLs stay whole.
18/// - Non-ASCII alphanumeric runs of more than one char are one term each; a
19///   lone non-ASCII char is kept as its own term.
20/// - Runs containing Han chars additionally emit every adjacent-char bigram,
21///   the standard whitespace-free segmentation proxy.
22///
23/// Punctuation (ASCII and fullwidth alike) and whitespace never become terms.
24pub fn terms(text: &str) -> BTreeSet<String> {
25    let mut output = BTreeSet::new();
26    let mut ascii_run = String::new();
27    let mut wide_run: Vec<char> = Vec::new();
28    for character in text.chars().flat_map(char::to_lowercase) {
29        if character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '/' | '.' | ':') {
30            flush_wide(&mut wide_run, &mut output);
31            ascii_run.push(character);
32        } else if !character.is_ascii() && character.is_alphanumeric() {
33            flush_ascii(&mut ascii_run, &mut output);
34            wide_run.push(character);
35        } else {
36            flush_ascii(&mut ascii_run, &mut output);
37            flush_wide(&mut wide_run, &mut output);
38        }
39    }
40    flush_ascii(&mut ascii_run, &mut output);
41    flush_wide(&mut wide_run, &mut output);
42    output
43}
44
45/// Number of terms shared by both sets.
46pub fn overlap_count(left: &BTreeSet<String>, right: &BTreeSet<String>) -> u32 {
47    left.intersection(right).count() as u32
48}
49
50/// Term-set Jaccard similarity over the shared vocabulary.
51///
52/// Inputs whose combined vocabulary is empty score 0.0 — no lexical evidence is
53/// treated as "not a duplicate", so degenerate content fails open (both records
54/// are kept) rather than merging on nothing.
55pub fn jaccard(left: &str, right: &str) -> f64 {
56    let left = terms(left);
57    let right = terms(right);
58    let union = left.union(&right).count();
59    if union == 0 {
60        return 0.0;
61    }
62    left.intersection(&right).count() as f64 / union as f64
63}
64
65fn flush_ascii(run: &mut String, output: &mut BTreeSet<String>) {
66    // All-ASCII, so byte length equals char count.
67    if run.len() > 1 {
68        output.insert(std::mem::take(run));
69    } else {
70        run.clear();
71    }
72}
73
74fn flush_wide(run: &mut Vec<char>, output: &mut BTreeSet<String>) {
75    match run.len() {
76        0 => return,
77        1 => {
78            output.insert(run[0].to_string());
79        }
80        _ => {
81            output.insert(run.iter().collect());
82            if run.iter().copied().any(is_han) {
83                for pair in run.windows(2) {
84                    output.insert(pair.iter().collect());
85                }
86            }
87        }
88    }
89    run.clear();
90}
91
92/// Han ranges mirrored from the host rankers (BMP unified + ext A, compat, ext B+).
93fn is_han(character: char) -> bool {
94    matches!(
95        u32::from(character),
96        0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0x20000..=0x3134F
97    )
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn ascii_words_paths_and_ids_stay_whole() {
106        let output = terms("Read src/main.rs via call_abc-123, then STOP.");
107        assert!(output.contains("read"));
108        assert!(output.contains("src/main.rs"));
109        assert!(output.contains("call_abc-123"));
110        // Single ASCII chars never become terms; punctuation is stripped.
111        assert!(!output.contains("a"));
112        assert!(!output.contains(","));
113    }
114
115    #[test]
116    fn han_runs_emit_bigrams_not_unigrams() {
117        let output = terms("实现用户登录");
118        assert!(output.contains("实现"));
119        assert!(output.contains("用户"));
120        assert!(output.contains("户登"));
121        assert!(output.contains("实现用户登录"));
122        assert!(!output.contains("实"), "unigram noise must be gone");
123    }
124
125    #[test]
126    fn chinese_goal_overlap_discriminates() {
127        let goal = terms("实现用户登录功能");
128        let on_topic = terms("已完成用户登录表单");
129        let off_topic = terms("今天天气很好我们去公园");
130        assert!(overlap_count(&goal, &on_topic) >= 2);
131        assert_eq!(overlap_count(&goal, &off_topic), 0);
132    }
133
134    #[test]
135    fn fullwidth_punctuation_is_not_a_term() {
136        let output = terms("完成了。下一步:测试!");
137        assert!(!output.contains("。"));
138        assert!(!output.contains(":"));
139        assert!(output.contains("完成"));
140    }
141
142    #[test]
143    fn lone_wide_char_is_kept() {
144        assert!(terms("改 a").contains("改"));
145    }
146
147    #[test]
148    fn mixed_ascii_and_han_split_into_both_vocabularies() {
149        let output = terms("部署v2服务到prod环境");
150        assert!(output.contains("v2"));
151        assert!(output.contains("prod"));
152        assert!(output.contains("部署"));
153        assert!(output.contains("服务"));
154        assert!(output.contains("环境"));
155    }
156
157    #[test]
158    fn jaccard_near_duplicate_chinese_scores_high() {
159        let near = jaccard("用户偏好深色模式界面", "用户偏好浅色模式界面");
160        let unrelated = jaccard("用户偏好深色模式界面", "周五之前完成部署上线");
161        assert!(near > 0.5, "near-duplicates must be detectable: {near}");
162        assert!(unrelated < 0.2, "unrelated must stay low: {unrelated}");
163    }
164
165    #[test]
166    fn jaccard_identical_english_is_one_and_empty_vocabulary_is_zero() {
167        assert_eq!(jaccard("prefer cargo nextest", "prefer cargo nextest"), 1.0);
168        assert_eq!(jaccard("", ""), 0.0);
169    }
170
171    #[test]
172    fn non_han_scripts_keep_whole_words_without_bigrams() {
173        let output = terms("привет мир");
174        assert!(output.contains("привет"));
175        assert!(output.contains("мир"));
176        assert!(!output.contains("пр"));
177    }
178}