/// self_search.mbt —— 调研先行 / 自搜索能力的最小可计算结构(域无关)
///
/// 原则:**凡与目标功能相关的都应先调研**——对外部库/项目/论文「搜索 → 分析 → 评估可移植性后融入」。
/// 本模块把这一方法论固化为纯逻辑的计算步骤:候选清洗、按 (name, source) 判重、
/// 基于可移植动作词的启发式可移植性评估、去重+过滤后有序输出。
///
/// 外部搜索不内置网络:搜索命令由调用方经 `runner` **注入**给 run_external_search,
/// 本模块不硬编码任何盘符/绝对路径。未注入(默认 None)→ 返回 Err「未注入搜索执行器」;
/// 测试用 mock 注入验证成功/失败路径。
///|
/// 搜索候选:一次外部调研产出的「可移植资产」候选。
///
/// - name:候选名(去重键分量一,如库名/论文名)
/// - title:展示标题
/// - source:来源(去重键分量二,如 "crate" / "gh" / "paper" / "blogs")
/// - mechanism:实现机制/原理描述(供可移植性启发式评估)
/// - portable:是否可移植(assess_portability 输出)
pub struct SearchCandidate {
name : String
title : String
source : String
mechanism : String
portable : Bool
}
///|
/// 基础清洗:对五个字段做 trim;portable 依据 mechanism 的启发式(assess_portability)。
/// 去重键 = (trimmed name, trimmed source)。
pub fn analyze_candidate(
name : String,
title : String,
source : String,
mechanism : String,
) -> SearchCandidate {
// 先清洗出各字段,再依 mechanism 计算可移植性。结构体固定字段,故构造两份(其一仅用于评估)。
let pre = SearchCandidate::{
name: name.trim().to_owned(),
title: title.trim().to_owned(),
source: source.trim().to_owned(),
mechanism: mechanism.trim().to_owned(),
portable: false,
}
SearchCandidate::{
name: pre.name,
title: pre.title,
source: pre.source,
mechanism: pre.mechanism,
portable: assess_portability(pre),
}
}
///|
/// 判重:候选去重键一致(name 与 source 均相等)即视为重复。
pub fn is_duplicate_candidate(a : SearchCandidate, b : SearchCandidate) -> Bool {
a.name == b.name && a.source == b.source
}
///|
/// 可移植性启发式评估(粗糙但可复现,规则写死在此注释):
///
/// - mechanism 非空;
/// - 长度合理:长度 ≥ 4(过短视为信息不足,不可移植);
/// - 含可移植性动作词(大小写不敏感):port / portable / porting /
/// adapt / wrapper / wrap / reuse / extract / portability / 移植 / 封装 / 复用 /
/// embed / integrate / conforms / compat / decouple;
///
/// 满足全部三条 → true;否则 false。该评估是**计算**而非 LLM 自评,供 distill 过滤硬门槛。
pub fn assess_portability(c : SearchCandidate) -> Bool {
let m = c.mechanism
if m.length() < 4 {
return false
}
let words : Array[String] = [
"port", "portable", "porting", "portability", "adapt", "wrapper", "wrap", "reuse",
"extract", "embed", "integrate", "conforms", "compat", "decouple", "移植",
"封装", "复用",
]
for w in words {
if contains_ci(m, w) {
return true
}
}
false
}
///|
/// 蒸馏:去重(同 name+source 只留首个)+ 只保留 portable 的 + 按 source 有序输出。
/// 「输出有序」依 source 字符串升序(字典序)排列,稳定(同 source 保持原始先后)。
pub fn distill_candidates(
cands : Array[SearchCandidate],
) -> Array[SearchCandidate] {
let unique : Array[SearchCandidate] = []
for c in cands {
if !unique.any(fn(u) { is_duplicate_candidate(u, c) }) {
unique.push(c)
}
}
let portable : Array[SearchCandidate] = []
for c in unique {
if c.portable {
portable.push(c)
}
}
// 插入排序:按 source 升序,稳定。
for i = 1; i < portable.length(); i = i + 1 {
let key = portable[i]
let mut j = i
while j > 0 && portable[j - 1].source > key.source {
portable[j] = portable[j - 1]
j = j - 1
}
portable[j] = key
}
portable
}
///|
/// 经注入命令执行外部搜索,返回原始文本(成功 Ok / 失败 Err)。
///
/// 不内置网络、不硬编码路径;搜索执行器由调用方通过 **注入参数** `runner` 传入
/// (供上层接入,测试用 mock 注入)。未注入(默认 None)→ Err「未注入搜索执行器」。
pub fn run_external_search(
cmd : String,
args : Array[String],
runner? : ((String, Array[String]) -> Result[String, String])? = None,
) -> Result[String, String] {
match runner {
Some(f) => f(cmd, args)
None => Err("self_search: 未注入搜索执行器,跳过外部搜索")
}
}
///|
/// 大小写不敏感的包含判断(不依赖具体 core 大小写 API,保证确定性)。
fn contains_ci(hay : String, needle : String) -> Bool {
if needle.length() == 0 {
return true
}
if hay.length() < needle.length() {
return false
}
let mut start = 0
while start + needle.length() <= hay.length() {
if has_match_ci(hay, start, needle) {
return true
}
start = start + 1
}
false
}
///|
/// ASCII 大写码点转小写(非大写码点原样返回)。String 索引产出 UInt16 码点。
fn lower_code(c : UInt16) -> UInt16 {
if c >= 65 && c <= 90 {
c + 32
} else {
c
}
}
///|
/// 单码点大小写不敏感比较(ASCII 域内有效;中文无大小写,恒等)。
fn char_eq_ci(a : UInt16, b : UInt16) -> Bool {
lower_code(a) == lower_code(b)
}
///|
/// hay 从 start 起是否与 needle 逐码点(大小写不敏感)匹配。
fn has_match_ci(hay : String, start : Int, needle : String) -> Bool {
let mut k = 0
while k < needle.length() {
if !char_eq_ci(hay[start + k], needle[k]) {
return false
}
k = k + 1
}
true
}