// engine_ops.mbt
// =====================================================================
// P10: 从 engine.mbt 拆出 — 同包(@lib)多文件,调用点零修改。
// 主题: style/back_align/term_conflicts/retrieve_for_prompt + 主动学习/联邦/蒸馏/可观测 (#21+ 长期能力)
// 主动学习候选:高不确定(低命中/低使用) + 角色多样性
pub fn ProphecyEngine::active_learning_candidates(
self : ProphecyEngine,
k : Int,
) -> Array[Json] {
self._recompute_values()
let pairs : Array[(String, Double)] = []
for mid, m in self.memories.iter2() {
let hr = hit_rate_of(m)
let unc = 1.0 - hr
let nov = 1.0 - clamp01(m.use_count.to_double() / 5.0)
pairs.push((mid, 0.6 * unc + 0.4 * nov))
}
sort_desc_by_key(pairs, fn(x){ x.1 })
let seen_role : Map[String, Int] = Map::from_iter(([] : Array[(String, Int)]).iter())
let out : Array[Json] = []
for i = 0; i < pairs.length(); i = i + 1 {
if out.length() >= k {
break
}
let mid = pairs[i].0
match self.memories.get(mid) {
Some(m) => {
let rk = role_of(m.text)
// 角色多样性:已覆盖角色延后选取,优先保证覆盖面
if seen_role.contains(rk) && out.length() < k && seen_role.length() < k {
continue
}
seen_role.set(rk, 1)
out.push(
obj([
("id", str_json(mid)),
("text", str_json(m.text)),
("uncertainty", num_json(r4(pairs[i].1))),
("role", str_json(rk)),
]),
)
}
None => ()
}
}
out
}
// 联邦:增量导出(供 FedAvg 聚合的对端差异计数)
pub fn ProphecyEngine::fed_export(self : ProphecyEngine) -> Json {
obj([
("added", num_json(self.fed_add.to_double())),
("updated", num_json(self.fed_upd.to_double())),
])
}
// 联邦:增量导入(合并对端聚合后的计数;权重合并由协调方完成)
pub fn ProphecyEngine::fed_import(self : ProphecyEngine, added : Int, updated : Int) -> Unit {
self.fed_add = self.fed_add + added
self.fed_upd = self.fed_upd + updated
self.invalidate_pred_cache()
}
// 蒸馏:注入只读领域偏置表(LoRA ΔW),供 predict 先验增强
pub fn ProphecyEngine::inject_distillation(
self : ProphecyEngine,
table : Map[String, Double],
) -> Unit {
for rk, dv in table.iter2() {
if rk != "" {
let cur = match self.domain_bias.get(rk) { Some(x) => x; None => 0.0 }
self.domain_bias.set(rk, cur + dv)
}
}
self.invalidate_pred_cache()
}
// ---------------------------------------------------------------------------
// D7 可解释:回放某记忆的来龙去脉
// ---------------------------------------------------------------------------
pub fn ProphecyEngine::explain(self : ProphecyEngine, mid : String) -> Json {
match self.memories.get(mid) {
Some(m) => {
let hr = hit_rate_of(m)
let via : Array[Json] = []
for nb, w in m.edges.iter2() {
via.push(obj([("to", str_json(nb)), ("w", num_json(r4(w)))]))
}
let predicts_to = match self.transitions.get(mid) {
Some(d) => dmap_to_json(d)
None => obj([])
}
obj([
("id", str_json(mid)),
("text", str_json(m.text)),
("type", str_json(m.mtype)),
("predictive_value", num_json(r4(m.predictive_value))),
("hit_rate", num_json(r4(hr))),
("out_edges", arr_json(via)),
("predicts_to", predicts_to),
])
}
None => obj([("error", str_json("unknown id"))])
}
}
// ---------------------------------------------------------------------------
// 指标
// ---------------------------------------------------------------------------
pub fn ProphecyEngine::hit_rate(self : ProphecyEngine) -> Double {
if self.stats_preds > 0 {
self.stats_hits.to_double() / self.stats_preds.to_double()
} else {
0.0
}
}
// 当前上下文窗口的记忆文本(供演示/审计查看)
pub fn ProphecyEngine::context_texts(self : ProphecyEngine) -> Array[String] {
let out : Array[String] = []
for mid in self.context {
match self.memories.get(mid) {
Some(m) => out.push(m.text)
None => out.push("")
}
}
out
}
// 当前上下文最后一个节点 id(演示中取反馈对象用)
pub fn ProphecyEngine::last_context_id(self : ProphecyEngine) -> String {
if self.context.length() == 0 {
""
} else {
self.context[self.context.length() - 1]
}
}
pub fn ProphecyEngine::stats_view(self : ProphecyEngine) -> Json {
self._recompute_values()
let mut avg_v = 0.0
for _, m in self.memories.iter2() {
avg_v = avg_v + m.predictive_value
}
if self.memories.length() > 0 {
avg_v = avg_v / self.memories.length().to_double()
}
let by_type : Map[String, Int] = Map::from_iter(([] : Array[(String, Int)]).iter())
for _, m in self.memories.iter2() {
let c = match by_type.get(m.mtype) { Some(x) => x; None => 0 }
by_type.set(m.mtype, c + 1)
}
obj([
("nodes", num_json(self.memories.length().to_double())),
("edges", num_json(self.edges_count().to_double())),
("episodes", num_json(self.episodes.length().to_double())),
("by_type", imap_to_json(by_type)),
("predict_hit_rate", num_json(r4(self.hit_rate()))),
("predict_calls", num_json(self.stats_preds.to_double())),
("avg_predictive_value", num_json(r4(avg_v))),
("remembers", num_json(self.stats_remembers.to_double())),
])
}
// ===== M1 风格一致性检查(启发式,确定性)=====
pub fn ProphecyEngine::style_check(self : ProphecyEngine, text : String) -> Json {
let issues : Array[Json] = []
let sentences = text.split("。").to_array().map(fn(x) { x.to_owned() })
// 1. 句长(>80 字符提示)
for s in sentences {
if s.length() > 80 {
issues.push(obj([
("rule", str_json("sentence_length")),
("level", str_json("info")),
("message", str_json("句子过长(" + s.length().to_string() + " 字符),建议拆分。")),
]))
}
}
// 2. 重复标点 / 连续空格
if str_contains(text, " ") {
issues.push(obj([
("rule", str_json("double_space")),
("level", str_json("warning")),
("message", str_json("存在连续空格。")),
]))
}
if str_contains(text, "..") || str_contains(text, "。。") {
issues.push(obj([
("rule", str_json("repeated_punct")),
("level", str_json("warning")),
("message", str_json("存在重复标点。")),
]))
}
// 3. 全角/半角括号混用
let has_full = str_contains(text, "(") || str_contains(text, ")")
let has_half = str_contains(text, "(") || str_contains(text, ")")
if has_full && has_half {
issues.push(obj([
("rule", str_json("bracket_mix")),
("level", str_json("info")),
("message", str_json("全角/半角括号混用,建议统一。")),
]))
}
// 4. 术语命中提示(术语库中出现的词)
let term_hits = self.enforce_terms(text)
let hit_count = match term_hits {
Json::Array(a) => a.length()
_ => 0
}
if hit_count > 0 {
issues.push(obj([
("rule", str_json("term_hits")),
("level", str_json("info")),
("message", str_json("命中 " + hit_count.to_string() + " 个库内术语,注意按术语库译文统一。")),
]))
}
arr_json(issues)
}
// ===== 风格一致报告(路线图 §四 长期「美」):记忆库分布统计 + 新译文偏离建议 =====
// 输入 text 可为空串(仅报告记忆库分布)或待检译文(追加偏离建议)。
// 统计口径:双语句对(tm 或带译文的 term,即 translation!="");句长=字符数。
// formal_score:库中通过 style_check 无格式问题的句对占比(格式达标率,非语言正式度)。
// 确定性:遍历 memories 顺序固定(iter2 按插入序),变体族按 root 排序输出,无随机。
pub fn ProphecyEngine::style_report(self : ProphecyEngine, text : String) -> Json {
let mut sent_n = 0
let mut sum_src = 0.0
let mut sum_tgt = 0.0
let mut formal_ok = 0
let mut term_n = 0
let term_variants : Map[String, Int] = Map::from_iter(([] : Array[(String, Int)]).iter())
// 句长分布桶(字符):<20 / 20-39 / 40-79 / >=80
let mut b0 = 0
let mut b1 = 0
let mut b2 = 0
let mut b3 = 0
for _, m in self.memories.iter2() {
// 仅统计双语节点(tm / 带译文的 term),排除 observe 工作流/指令节点(translation 恒空)
if m.text.length() == 0 || m.translation == "" {
continue
}
sent_n = sent_n + 1
let s = m.text.length().to_double()
let t = m.translation.length().to_double()
sum_src = sum_src + s
sum_tgt = sum_tgt + t
let is_formal = match self.style_check(m.text) {
Json::Array(a) => a.length() == 0
_ => false
}
if is_formal {
formal_ok = formal_ok + 1
}
if m.is_term {
term_n = term_n + 1
let root = term_root(m.text)
if root != "" {
let cur = match term_variants.get(root) { Some(v) => v; None => 0 }
term_variants.set(root, cur + 1)
}
}
if s >= 80.0 {
b3 = b3 + 1
} else if s >= 40.0 {
b2 = b2 + 1
} else if s >= 20.0 {
b1 = b1 + 1
} else {
b0 = b0 + 1
}
}
let n = sent_n.to_double()
let avg_src = if n > 0.0 { r4(sum_src / n) } else { 0.0 }
let avg_tgt = if n > 0.0 { r4(sum_tgt / n) } else { 0.0 }
let formal_score = if n > 0.0 { r4(formal_ok.to_double() / n) } else { 0.0 }
// 术语变体:按词根分组计数,>1 为潜在变体族;按 root 字典序输出保证确定性
let vlist : Array[(String, Int)] = []
for root, cnt in term_variants.iter2() {
if cnt > 1 {
vlist.push((root, cnt))
}
}
vlist.sort_by(fn(a, b) { a.0.compare(b.0) })
let variants_arr : Array[Json] = []
for item in vlist {
let (root, cnt) = item
variants_arr.push(obj([("root", str_json(root)), ("count", num_json(cnt.to_double()))]))
}
// 新译文偏离建议:长度偏离记忆库均值 ±50% 或含重复标点
let tips : Array[Json] = []
if text.length() > 0 {
let tl = text.length().to_double()
if n > 0.0 && avg_tgt > 0.0 {
let ratio = tl / avg_tgt
if ratio > 1.5 {
tips.push(obj([
("rule", str_json("length_deviation")),
("level", str_json("warning")),
("message", str_json("译文长度 " + text.length().to_string() + " 字,为库均值 " + avg_tgt.to_string() + " 的 " + r4(ratio).to_string() + "×(>1.5×),建议精简。")),
]))
} else if ratio < 0.5 {
tips.push(obj([
("rule", str_json("length_deviation")),
("level", str_json("warning")),
("message", str_json("译文长度 " + text.length().to_string() + " 字,为库均值 " + avg_tgt.to_string() + " 的 " + r4(ratio).to_string() + "×(<0.5×),疑似漏译。")),
]))
}
}
let style_issues = self.style_check(text)
match style_issues {
Json::Array(a) => {
for i in a {
tips.push(i)
}
}
_ => ()
}
}
obj([
("sentence_count", num_json(sent_n.to_double())),
("term_count", num_json(term_n.to_double())),
("avg_src_len", num_json(avg_src)),
("avg_tgt_len", num_json(avg_tgt)),
("formal_score", num_json(formal_score)),
("distribution", arr_json([num_json(b0.to_double()), num_json(b1.to_double()), num_json(b2.to_double()), num_json(b3.to_double())])),
("term_variants", arr_json(variants_arr)),
("tips", arr_json(tips)),
])
}
// 词根提取:中文取前 2 个汉字,拉丁词取首个空格分隔的单词;用于归并变体族(如 电池/电池组/电池包、switch/switcher)
fn term_root(t : String) -> String {
// 判断首字符是否为 ASCII 字母(拉丁)
let is_latin = match t.iter().next() {
Some(c) => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
None => false
}
if is_latin {
// 拉丁:取首个空格分隔的单词
let parts = t.split(" ").to_array()
return if parts.length() > 0 { parts[0].to_owned() } else { "" }
}
// 中文/其他:取前 2 个非空格字符
let chars : Array[String] = []
for i = 0; i < t.length(); i = i + 1 {
match t[i].to_char() {
Some(ch) => {
let c = ch.to_string()
if c != " " {
chars.push(c)
}
if chars.length() >= 2 {
break
}
}
None => ()
}
}
chars.join("")
}
// ===== M2 回译验证(LCS 对齐质量)=====
// P8:纯函数不读引擎状态;receiver 改 `_self` 消 unused 告警(保持 dot 调用语法不变)
pub fn ProphecyEngine::back_align(_self : ProphecyEngine, source : String, target : String) -> Json {
let ops = align_diff(source, target)
let mut match_n = 0
let mis : Array[Json] = []
let mut cur_a = ""
let mut cur_b = ""
for op in ops {
match op {
(0, _, _) => {
if cur_a != "" {
mis.push(obj([("side", str_json("a")), ("frag", str_json(cur_a))]))
cur_a = ""
}
if cur_b != "" {
mis.push(obj([("side", str_json("b")), ("frag", str_json(cur_b))]))
cur_b = ""
}
match_n = match_n + 1
}
(1, i, _) => {
if i < source.length() {
cur_a = cur_a + (match source[i].to_char() { Some(ch) => ch.to_string(); None => "" })
}
}
(2, _, j) => {
if j < target.length() {
cur_b = cur_b + (match target[j].to_char() { Some(ch) => ch.to_string(); None => "" })
}
}
_ => ()
}
}
if cur_a != "" {
mis.push(obj([("side", str_json("a")), ("frag", str_json(cur_a))]))
}
if cur_b != "" {
mis.push(obj([("side", str_json("b")), ("frag", str_json(cur_b))]))
}
let denom = if source.length() > target.length() { source.length() } else { target.length() }
let score = if denom > 0 { r4(match_n.to_double() / denom.to_double()) } else { 1.0 }
obj([
("align_score", num_json(score)),
("match_chars", num_json(match_n.to_double())),
("misaligns", arr_json(mis)),
])
}
// ===== M3 术语冲突检测(一词多译 / 多词一译)=====
pub fn ProphecyEngine::term_conflicts(self : ProphecyEngine) -> Json {
// 收集术语节点:text -> translation
let by_src : Map[String, Array[String]] = Map::from_iter(([] : Array[(String, Array[String])]).iter())
let by_tgt : Map[String, Array[String]] = Map::from_iter(([] : Array[(String, Array[String])]).iter())
for _, m in self.memories.iter2() {
if m.is_term && m.translation != "" {
let s1 = match by_src.get(m.text) {
Some(v) => v
None => []
}
let s2 = s1
let mut dup_src = false
for t in s2 {
if t == m.translation { dup_src = true }
}
if !dup_src {
s2.push(m.translation)
by_src.set(m.text, s2)
}
let t1 = match by_tgt.get(m.translation) {
Some(v) => v
None => []
}
let t2 = t1
let mut dup_tgt = false
for s in t2 {
if s == m.text { dup_tgt = true }
}
if !dup_tgt {
t2.push(m.text)
by_tgt.set(m.translation, t2)
}
}
}
let conflicts : Array[Json] = []
// 一词多译
for src, tgts in by_src.iter2() {
if tgts.length() > 1 {
let ts : Array[Json] = Array::map(tgts, fn(t) { str_json(t) })
conflicts.push(obj([
("kind", str_json("one_to_many")),
("term", str_json(src)),
("translations", arr_json(ts)),
]))
}
}
// 多词一译
for tgt, srcs in by_tgt.iter2() {
if srcs.length() > 1 {
let ss : Array[Json] = Array::map(srcs, fn(s) { str_json(s) })
conflicts.push(obj([
("kind", str_json("many_to_one")),
("translation", str_json(tgt)),
("terms", arr_json(ss)),
]))
}
}
arr_json(conflicts)
}
// ===== M1 风格一致性检查(启发式,确定性)=====
// ===== M5 TMPlm:为 LLM prompt 组装检索上下文(三段式,全复用既有能力)=====
// 返回 {"suggestions": TM 相似句, "terms": 术语命中, "glossary": 术语上下文}
pub fn ProphecyEngine::retrieve_for_prompt(
self : ProphecyEngine,
query : String,
k : Int,
threshold : Double,
) -> Json {
let suggestions = self.fuzzy_match(query, k, threshold)
let term_hits = self.enforce_terms(query)
// glossary:术语 → concordance 上下文(按 id 去重)
let gloss : Array[Json] = []
let seen : Map[String, Bool] = Map::from_iter(([] : Array[(String, Bool)]).iter())
match term_hits {
Json::Array(items) =>
for item in items {
let term = get_str(item, "term")
if term != "" {
let ctx = self.concordance(term, 3)
match ctx {
Json::Array(arr) =>
for c in arr {
let cid = get_str(c, "id")
if cid != "" && !seen.contains(cid) {
seen.set(cid, true)
gloss.push(c)
}
}
_ => ()
}
}
}
_ => ()
}
obj([
("suggestions", suggestions),
("terms", term_hits),
("glossary", arr_json(gloss)),
])
}
// ===== M5 TMPlm:为 LLM prompt 组装检索上下文(三段式,全复用既有能力)=====
// 返回 {"suggestions": TM 相似句, "terms": 术语命中, "glossary": 术语上下文}