@furongjun1999/dsh-memory 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -18
- package/docs/README.md +1 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/chain.py +47 -0
- package/md_cg/freshness.py +511 -0
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/mcp_server.py +128 -19
- package/md_cg/mdcg.py +509 -22
- package/md_cg/mdcos.py +203 -22
- package/md_cg/nodefile.py +74 -1
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/sustain.py +146 -9
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_h4_sustain_snapshot.py +14 -4
- package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_rank_parity_score_mode.py +6 -0
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/utf8_boot.py +237 -0
package/md_cg/mdcos.py
CHANGED
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import hashlib
|
|
22
22
|
import json
|
|
23
|
+
import math
|
|
23
24
|
import os
|
|
24
25
|
import re
|
|
25
26
|
import time
|
|
@@ -30,7 +31,9 @@ from .mdcg import (MdCG, expand_query_terms, bigrams, normalize_en, STATE_ACCEPT
|
|
|
30
31
|
TIER_BUCKET_SCAN, TIER_GLOBAL_LIKE, TIER_GLOBAL_SCAN,
|
|
31
32
|
GLOBAL_CAP, expand_query_terms_weighted,
|
|
32
33
|
expand_query_terms_llm, en_zh_bigrams, semantic_on,
|
|
33
|
-
cut_by_relevance, apply_retrieval_gates, NEG_ROUTE_LAYERS
|
|
34
|
+
cut_by_relevance, apply_retrieval_gates, NEG_ROUTE_LAYERS,
|
|
35
|
+
# P2-3(§5.4):六要素后两行索引键的召回判据单点(mdcg 侧)
|
|
36
|
+
index_key_hits, boundary_mark)
|
|
34
37
|
from . import (nodefile, routing, chain, subgraph, forgetting, protect,
|
|
35
38
|
identity, consistency, metacognition, crypto, sustain,
|
|
36
39
|
self_state, predict, evolution, weights, pooling,
|
|
@@ -64,6 +67,37 @@ DEFAULT_BUDGET = 1200 # recall 默认 token 预算
|
|
|
64
67
|
# 0 = 关闭截断,回到"超大一律跳过"的旧行为。
|
|
65
68
|
DEFAULT_MAX_ITEM_TOKENS = 250
|
|
66
69
|
|
|
70
|
+
# ---- P3-temporal:时间路的核与口径(设计稿 §6.3,裁定 12)-------------------
|
|
71
|
+
# 核**必须**来自唯一权威 `md_cg/whitebox_kb/aeis_core/time_core.py::cred_factor`
|
|
72
|
+
# (该模块 docstring 逐字点名 `exp(-t/τ)` 为 bug);本模块**不写任何指数核**。
|
|
73
|
+
#: γ(衰减率)可配 env —— 与 `MDCG_SPREAD_DECAY` 同款:缺失/非法即回落缺省。
|
|
74
|
+
TEMPORAL_GAMMA_ENV = "MDCG_TEMPORAL_GAMMA"
|
|
75
|
+
#: 半衰期真源 = `links.DECAY_DAYS`(30 天,`md_cg/links.py:53`)——不复制字面量,
|
|
76
|
+
#: 由 `temporal_gamma()` 现取;此处只写明「γ 缺省 = ln2 / 半衰期」这一换算。
|
|
77
|
+
#: 单位口径(**最易错处**):γ 是「每天」的速率,故 Δt 必须**以天为单位**喂入。
|
|
78
|
+
TEMPORAL_DT_UNIT = "day"
|
|
79
|
+
#: 分数下限(floor):`cred_factor(gamma, dt, floor, 1.0)` 的 floor。
|
|
80
|
+
#: 取值理由:0.5**10 ≈ 9.77e-4 < 1e-3 —— 30 天半衰期下 300 天(10 个半衰期)
|
|
81
|
+
#: 之后衰减已落到 1e-3 以下,继续区分无信息量;floor=1e-3 让**老节点/缺
|
|
82
|
+
#: created_at 的节点**仍带非零分进入本路,而不是被乘成 0 后不可召回
|
|
83
|
+
#:(读数见 `md_cg/test_time_core_lint.py` 的 G4 floor 组)。
|
|
84
|
+
TEMPORAL_SCORE_FLOOR = 0.001
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# 生效条件:environ(缺省 os.environ)里 TEMPORAL_GAMMA_ENV 为真值且 float() 可解析且 > 0 时返回该值;缺失/空串/不可解析/非正数一律回落 `math.log(2)/links.DECAY_DAYS`(缺省半衰期 30 天)。
|
|
88
|
+
def temporal_gamma(environ=None) -> float:
|
|
89
|
+
"""时间邻近度衰减率 γ(**每天**)的唯一读取点。"""
|
|
90
|
+
raw = (environ or os.environ).get(TEMPORAL_GAMMA_ENV)
|
|
91
|
+
if raw:
|
|
92
|
+
try:
|
|
93
|
+
g = float(raw)
|
|
94
|
+
except (TypeError, ValueError):
|
|
95
|
+
g = 0.0
|
|
96
|
+
if g > 0:
|
|
97
|
+
return g
|
|
98
|
+
from .links import DECAY_DAYS # 半衰期唯一真源(links.py:53,30 天)
|
|
99
|
+
return math.log(2.0) / float(DECAY_DAYS)
|
|
100
|
+
|
|
67
101
|
|
|
68
102
|
# 生效条件:text 为假值(None/空串)时返回 0,否则按「CJK 0.6/字 + 其余 /4」计算并返回 int(cjk*0.6 + other/4) + 1。
|
|
69
103
|
def est_tokens(text: str) -> int:
|
|
@@ -846,8 +880,9 @@ class MdCGOS(MdCG):
|
|
|
846
880
|
详见 _candidates。
|
|
847
881
|
"""
|
|
848
882
|
q = (query or "").strip()
|
|
849
|
-
# 批次 15 统一口径(unify.py
|
|
850
|
-
#
|
|
883
|
+
# 批次 15 统一口径(unify.py,2026-09-30 收窄作用域):只译**英文内容**
|
|
884
|
+
# → 标准原子序列,中文段逐字原样(不再整条归一);与
|
|
885
|
+
# MdCG.search/search_rrf 三入口同口径(归一层缺省关,显式 =1 才开)
|
|
851
886
|
from .semantic.unify import unify_query
|
|
852
887
|
q = unify_query(q)
|
|
853
888
|
if not q:
|
|
@@ -940,7 +975,9 @@ class MdCGOS(MdCG):
|
|
|
940
975
|
if in_bucket:
|
|
941
976
|
docs = self._read_many(in_bucket, stat)
|
|
942
977
|
# 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
|
|
943
|
-
hits = [d for d in docs
|
|
978
|
+
hits = [d for d in docs
|
|
979
|
+
if self._like(d[2], d[1], terms,
|
|
980
|
+
index_key_hits(d[0], terms, q))
|
|
944
981
|
or (semantic_on() and d[1].get("semantic"))]
|
|
945
982
|
out = try_stage(hits, TIER_BUCKET_LIKE)
|
|
946
983
|
if out:
|
|
@@ -966,7 +1003,8 @@ class MdCGOS(MdCG):
|
|
|
966
1003
|
docs_r = self._read_many(reach_entries, stat)
|
|
967
1004
|
_dif = set(_rstat.get("reach_diffused_paths") or ())
|
|
968
1005
|
hits_r = [d for d in docs_r
|
|
969
|
-
if self._like(d[2], d[1], terms
|
|
1006
|
+
if self._like(d[2], d[1], terms,
|
|
1007
|
+
index_key_hits(d[0], terms, q))
|
|
970
1008
|
or (semantic_on() and d[1].get("semantic"))
|
|
971
1009
|
or d[0].get("path") in _dif] # 图扩散补召回:无词面命中也放行进打分
|
|
972
1010
|
stat["pre_cap"] = len(hits_r) # 与 T2 同序:截断**前**的候选数
|
|
@@ -998,7 +1036,9 @@ class MdCGOS(MdCG):
|
|
|
998
1036
|
# 截断依据=相关度(同 MdCG.search:cap 值不变,改的是拿什么排序)
|
|
999
1037
|
docs_all = self._read_many(entries, stat)
|
|
1000
1038
|
# 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
|
|
1001
|
-
hits = [d for d in docs_all
|
|
1039
|
+
hits = [d for d in docs_all
|
|
1040
|
+
if self._like(d[2], d[1], terms,
|
|
1041
|
+
index_key_hits(d[0], terms, q))
|
|
1002
1042
|
or (semantic_on() and d[1].get("semantic"))]
|
|
1003
1043
|
stat["pre_cap"] = len(hits)
|
|
1004
1044
|
stat["cap"] = GLOBAL_CAP
|
|
@@ -1046,7 +1086,9 @@ class MdCGOS(MdCG):
|
|
|
1046
1086
|
docs = self._read_many(entries, stat)
|
|
1047
1087
|
# 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池——
|
|
1048
1088
|
# 语义摘要=检索面(设想核心),否则摘要层只在 LIKE 全空时生效
|
|
1049
|
-
hits = [d for d in docs
|
|
1089
|
+
hits = [d for d in docs
|
|
1090
|
+
if self._like(d[2], d[1], terms,
|
|
1091
|
+
index_key_hits(d[0], terms, query))
|
|
1050
1092
|
or (semantic_on() and d[1].get("semantic"))]
|
|
1051
1093
|
if not hits:
|
|
1052
1094
|
# 兜底池(LIKE 全空 = 无相关度信号):截断依据=importance/created_at
|
|
@@ -1158,7 +1200,8 @@ class MdCGOS(MdCG):
|
|
|
1158
1200
|
# 生效条件:当 seeds 非空且 seed_map 非空时,用 relation_types 或 CHAIN_TYPES_DEFAULT、max_depth 或 MAX_DEPTH_DEFAULT、decay 调用 chain.expand_from_seeds,仅保留 best 中仍存在于 entries(按 id(e))的节点,读取成功者加入 scored 并按分数降序返回 (scored, prov);seeds/seed_map/best 为空返回 ([], {});
|
|
1159
1201
|
def _path_chain(self, query, entries, seeds, context=None,
|
|
1160
1202
|
relation_types=None, max_depth=None, decay=0.9):
|
|
1161
|
-
"""
|
|
1203
|
+
"""关系链路径(= 设计稿 §6.2 的**因果路**):沿 causal/sequential/
|
|
1204
|
+
applies_to 边**多跳**扩散。
|
|
1162
1205
|
|
|
1163
1206
|
理论依据:`causal` = 条件依赖因果(A 是 B 成立的条件),
|
|
1164
1207
|
`dex_chain` 沿 causal 边正向展开、每步标注条件,**链 = 条件序列**。
|
|
@@ -1170,6 +1213,13 @@ class MdCGOS(MdCG):
|
|
|
1170
1213
|
本路按边类型权重(causal .85 / sequential .60 …)、逐跳乘边置信度、
|
|
1171
1214
|
默认 5 跳、visited 剪枝——「检索使用关系链」的落地。
|
|
1172
1215
|
|
|
1216
|
+
**边类型集可配(P3-causal)**:relation_types 缺省不再写死
|
|
1217
|
+
`CHAIN_TYPES_DEFAULT`,而走 `chain.chain_types_from_env()`(env
|
|
1218
|
+
`MDCG_CHAIN_TYPES`,缺省仍 = `CHAIN_TYPES_DEFAULT`)。动因:叙事/
|
|
1219
|
+
文档语料(按章切出的 doc_ref 库)里**没有 causal 边**,只有
|
|
1220
|
+
`reference` 与父章节结构边 `part_of`——写死即本路在这类库上恒空。
|
|
1221
|
+
未登记进 `EDGE_WEIGHTS` 的形态被读取点剔除(不静默走未知边)。
|
|
1222
|
+
|
|
1173
1223
|
返回 (scored, prov);prov[nid] 带该节点**最强链**的节点序列与条件序列,
|
|
1174
1224
|
使「为什么召回它」可审计。
|
|
1175
1225
|
"""
|
|
@@ -1182,7 +1232,8 @@ class MdCGOS(MdCG):
|
|
|
1182
1232
|
seed_map[nid] = float(s)
|
|
1183
1233
|
if not seed_map:
|
|
1184
1234
|
return [], {}
|
|
1185
|
-
rels = tuple(relation_types) if relation_types
|
|
1235
|
+
rels = (tuple(relation_types) if relation_types
|
|
1236
|
+
else chain.chain_types_from_env())
|
|
1186
1237
|
depth = chain.MAX_DEPTH_DEFAULT if max_depth is None else max_depth
|
|
1187
1238
|
best = chain.expand_from_seeds(self, seed_map, relation_types=rels,
|
|
1188
1239
|
max_depth=depth, decay=decay)
|
|
@@ -1209,6 +1260,65 @@ class MdCGOS(MdCG):
|
|
|
1209
1260
|
scored.sort(key=lambda x: (-x[1], str(x[0].get("id") or "")))
|
|
1210
1261
|
return scored, prov
|
|
1211
1262
|
|
|
1263
|
+
def _path_temporal(self, entries, q_start, q_end, start_op, end_op,
|
|
1264
|
+
now=None):
|
|
1265
|
+
"""时间路(P3-temporal,设计稿 §6.3):按**时间邻近度**排序的排名项。
|
|
1266
|
+
|
|
1267
|
+
口径(§〇.3-12 已裁,逐条钉在此处):
|
|
1268
|
+
|
|
1269
|
+
* **核走唯一权威**:`time_core.cred_factor(gamma, dt, floor, 1.0)`
|
|
1270
|
+
——本模块**不写任何指数核**(`exp(-t/τ)`、`×(1-factor)`、EMA 保持率
|
|
1271
|
+
均是 time_core docstring 点名的 bug 形态)。
|
|
1272
|
+
* **γ 缺省 = ln2 / 30 天**(半衰期 30 天,与 `links.py:53` 同刻度),
|
|
1273
|
+
env `MDCG_TEMPORAL_GAMMA` 可覆盖;唯一读取点 `temporal_gamma()`。
|
|
1274
|
+
* **Δt 取 `created_at`**(写入时刻,主链必有)。
|
|
1275
|
+
* **单位必须显式是「天」**:γ 是「每天」的速率,Δt 先除以 86400 再喂
|
|
1276
|
+
——喂秒会让衰减快 86400 倍(守卫 `test_time_core_lint.py` G3 组有该反证)。
|
|
1277
|
+
* **加 floor**(`TEMPORAL_SCORE_FLOOR`):老节点/缺 `created_at` 的节点
|
|
1278
|
+
不被乘成 0 而永不可召回。
|
|
1279
|
+
* **不做多跳扩散**:时间天然「1 跳」,本路是**排名项**(与 S4 层级加成
|
|
1280
|
+
同类),不是扩散路。
|
|
1281
|
+
* **种子不来自词法命中**:时间类问句常词面零重叠,故种子取
|
|
1282
|
+
**时间算子/区间命中集**——`entries`(已过 S1/S2 门控 ∧ 已过
|
|
1283
|
+
`trust.filter_by_time` 的索引标量过滤),**不 `_scan` 全库**、
|
|
1284
|
+
不读正文即可算分(`created_at` 在索引快照里)。
|
|
1285
|
+
|
|
1286
|
+
⚠ **理论边界标注**:理论原文 `exp(-γ·t)`(`docs/theory/智能论3.4.md:2122-2125`
|
|
1287
|
+
DEV-005)说的是 **confidence 衰减**;把它用于「时间邻近度」是**工程口径
|
|
1288
|
+
的类比迁移**,不是理论原话——同一核形状、不同语义(新近度权重)。
|
|
1289
|
+
"""
|
|
1290
|
+
if not any(x is not None for x in (q_start, q_end, start_op, end_op)):
|
|
1291
|
+
return [] # 无时间算子/区间 → 本路不启用(默认查询零变更)
|
|
1292
|
+
from .whitebox_kb.aeis_core import time_core as _tc # 懒导入(唯一权威)
|
|
1293
|
+
gamma = temporal_gamma()
|
|
1294
|
+
ref = q_end if q_end is not None else q_start
|
|
1295
|
+
if ref is None:
|
|
1296
|
+
ref = now if now is not None else time.time()
|
|
1297
|
+
out = []
|
|
1298
|
+
for e in entries:
|
|
1299
|
+
try:
|
|
1300
|
+
created = float(e.get("created_at") or 0.0)
|
|
1301
|
+
except (TypeError, ValueError):
|
|
1302
|
+
created = 0.0
|
|
1303
|
+
if created <= 0:
|
|
1304
|
+
# 写入时刻缺失:不猜测时刻,判「距参照无穷远」——floor 兜住
|
|
1305
|
+
#(仍可召回,不因缺字段整条消失)。
|
|
1306
|
+
dt_days = float("inf")
|
|
1307
|
+
else:
|
|
1308
|
+
# **单位=天**:86400 秒/天,显式换算(γ 是每天速率)
|
|
1309
|
+
dt_days = abs(created - float(ref)) / 86400.0
|
|
1310
|
+
score = _tc.cred_factor(gamma, dt_days, TEMPORAL_SCORE_FLOOR, 1.0)
|
|
1311
|
+
fm, c = self._read(e)
|
|
1312
|
+
if c is None:
|
|
1313
|
+
continue
|
|
1314
|
+
c = self._open_content(fm.get("id"), fm, c)
|
|
1315
|
+
if c is None:
|
|
1316
|
+
continue
|
|
1317
|
+
out.append(({"id": fm.get("id") or e["path"], "frontmatter": fm,
|
|
1318
|
+
"content": c, "path": e["path"]}, round(score, 6)))
|
|
1319
|
+
out.sort(key=lambda x: (-x[1], str(x[0].get("id") or "")))
|
|
1320
|
+
return out
|
|
1321
|
+
|
|
1212
1322
|
# 生效条件:以 expand or expand_query_terms_weighted 作扩展函数并对 query 调用(结果为假值按 {} 处理),从其中 pop "__source__"(缺键为 "whitebox")得 source,tw 为空返回 ([], source),否则对 entries 中存在 _weighted_coverage>0 的条目按 0.6·cov+0.3·aff+0.1·ctx_aff(context 非 None 时以 route_key(ctx, ctx.get("tags")) 得 ctx_domain,context 非 dict 时用 {})打分,返回 (out, source)。
|
|
1213
1323
|
def _path_fuzzy(self, query, entries, context=None, expand=None):
|
|
1214
1324
|
"""模糊路径(分级隶属度):返回 (scored, source)。
|
|
@@ -1343,7 +1453,9 @@ class MdCGOS(MdCG):
|
|
|
1343
1453
|
# 生效条件:query 去空白为空→empty_query、候选为空→no_candidates;否则按 paths 各路召回后融合(fusion=="max" 取各路最大贡献、否则求和),recall_only 中的路只以 0 分补池不参与打分,judge 与 judge_ranking 同时为真时对 fused 前 max(k*2,10) 条做资格裁决(REJECT/BLINDSPOT 剔除、DEFER 降权 0.5),否则直接取 fused 前 k;validity 真值时候选层剔除已过期节点,且 query 缓存按 validity 分键不串口径。
|
|
1344
1454
|
def search_rrf(self, query: str, k: int = 20, layer: str = None,
|
|
1345
1455
|
context=None, roles=None, include_work: bool = False,
|
|
1346
|
-
judge: bool = True,
|
|
1456
|
+
judge: bool = True,
|
|
1457
|
+
paths=("lexical", "bucket", "entity", "graph", "chain",
|
|
1458
|
+
"temporal"),
|
|
1347
1459
|
record: bool = True, query_expand=None,
|
|
1348
1460
|
path_weights=None, recall_only=None, fusion: str = "sum",
|
|
1349
1461
|
goal_text=None, judge_ranking: bool = False,
|
|
@@ -1358,14 +1470,28 @@ class MdCGOS(MdCG):
|
|
|
1358
1470
|
多路共同确认的记忆排在单路命中之前(对齐 noema 的 Fusion Recall)。
|
|
1359
1471
|
meta 含 per_path(各路的候选数与来源),可审计。
|
|
1360
1472
|
|
|
1361
|
-
paths: 基线四路(词法/条件桶/实体/图扩展)+
|
|
1473
|
+
paths: 基线四路(词法/条件桶/实体/图扩展)+ 增量路:
|
|
1362
1474
|
"fuzzy" 词表驱动(同义词组分级隶属度 + 大域 IDF)
|
|
1363
1475
|
"semantic" 条件结构驱动(CCG 生效条件 + condition_space 四槽;
|
|
1364
1476
|
不适用条件被整词命中即从本路剔除——条件级负路由)
|
|
1365
1477
|
"goal" 目标定向(第 5 篇第 3 章):以活跃目标文本(或显式
|
|
1366
1478
|
goal_text)扩展查询,给「与当前目标相关」的记忆加权;
|
|
1367
1479
|
无活跃目标时本路为空,等价于未启用。
|
|
1368
|
-
|
|
1480
|
+
"chain" **因果路**(P3-causal,设计稿 §6.2):以词法/实体命中为
|
|
1481
|
+
种子沿 edges 多跳扩散(复用 `md_cg/chain.py`:14 类边权、
|
|
1482
|
+
MAX_DEPTH 5、decay 0.9)。**代价口径(P3 遗留 ⑥ 如实
|
|
1483
|
+
降级,2026-10-01):不读正文,但邻接构建为 O(N) 索引
|
|
1484
|
+
条目级**(`chain.adjacency` 遍历全部索引节点;冷建代价
|
|
1485
|
+
读数见 `scripts/p2p4_probe.py` R7)——原表述「不走全表」
|
|
1486
|
+
与实测不符,已改正。
|
|
1487
|
+
"temporal" **时间路**(P3-temporal,设计稿 §6.3):按时间邻近度排序
|
|
1488
|
+
的**排名项**,核走 `time_core.cred_factor`;无时间算子/
|
|
1489
|
+
区间时恒空。
|
|
1490
|
+
缺省 = ("lexical","bucket","entity","graph","chain","temporal")
|
|
1491
|
+
—— 后两路按裁定 9「全进默认检索」进缺省集;**每路可单独关**:
|
|
1492
|
+
显式传不含该路的 paths 即关(既有调用方口径一字不变)。
|
|
1493
|
+
两条新路在**无 edges / 无时间参数**的库上自然为空(零多算),
|
|
1494
|
+
故纯词法查询的返回与改动前一致。
|
|
1369
1495
|
path_weights: {路名: 权重};缺省全部 1.0 → 与既有等权 RRF 完全一致。
|
|
1370
1496
|
用于压低**同质路**的贡献:等权融合下,两路对同一批候选给出不一致
|
|
1371
1497
|
排序时,RRF 会双重奖励「两路都靠前」的干扰项,把强路的 top-1 挤掉。
|
|
@@ -1375,6 +1501,8 @@ class MdCGOS(MdCG):
|
|
|
1375
1501
|
sum 奖励「多路共识」,但会系统性低估**单路独有**候选:当强路漏掉目标、
|
|
1376
1502
|
弱路捞到时,目标的单路贡献必然低于任何「两路都有排名」的干扰项。
|
|
1377
1503
|
max 只认「最好的一次排名」,不奖励共识,适合「任一路捞到即可」的召回。
|
|
1504
|
+
因果路是**单路独有召回型**,MCP 面(`mdcg_recall`)按其缺省 max
|
|
1505
|
+
(先例 `mcp_server.py` 的 fuzzy/semantic/goal 同款表达式)。
|
|
1378
1506
|
judge_ranking: 白箱终排(证据防火墙,显式启用)。融合排序只产生候选
|
|
1379
1507
|
(语义负责「不要漏」),资格裁决决定最终优先级(白箱负责「不要错」):
|
|
1380
1508
|
REJECT / BLINDSPOT 剔除,DEFER 降权 ×0.5,ACCEPT 保位。候选池取
|
|
@@ -1399,8 +1527,9 @@ class MdCGOS(MdCG):
|
|
|
1399
1527
|
本入口的对应物是 reach——不在本函数内。
|
|
1400
1528
|
"""
|
|
1401
1529
|
q = (query or "").strip()
|
|
1402
|
-
# 批次 15 统一口径(unify.py
|
|
1403
|
-
#
|
|
1530
|
+
# 批次 15 统一口径(unify.py,2026-09-30 收窄作用域):只译**英文内容**
|
|
1531
|
+
# → 标准原子序列(中文段逐字原样);归一后的 q 进热路径缓存键
|
|
1532
|
+
# (归一确定 → 缓存不串味)
|
|
1404
1533
|
from .semantic.unify import unify_query
|
|
1405
1534
|
q = unify_query(q)
|
|
1406
1535
|
if not q:
|
|
@@ -1479,6 +1608,13 @@ class MdCGOS(MdCG):
|
|
|
1479
1608
|
ranked = {} # path -> [(node, score)]
|
|
1480
1609
|
fuzzy_source = None
|
|
1481
1610
|
chain_prov = {}
|
|
1611
|
+
# P3-temporal:时间路的参照窗(与 `_candidates` 同一真源 `trust.check_time_args`
|
|
1612
|
+
# /`trust.parse_time`;未启用时留 None → 本路恒空,默认查询零变更)。
|
|
1613
|
+
# 只读索引标量(created_at 在索引快照里),不 `_scan` 全库、不读正文。
|
|
1614
|
+
_t_en, _t_ax, _t_why = trust.check_time_args(
|
|
1615
|
+
start_time, end_time, start_operator, end_operator, time_axis)
|
|
1616
|
+
_t_qs, _t_qe = ((trust.parse_time(start_time), trust.parse_time(end_time))
|
|
1617
|
+
if _t_en else (None, None))
|
|
1482
1618
|
if "lexical" in paths:
|
|
1483
1619
|
ranked["lexical"] = self._lexical(q, entries, stat)
|
|
1484
1620
|
if "bucket" in paths:
|
|
@@ -1490,6 +1626,12 @@ class MdCGOS(MdCG):
|
|
|
1490
1626
|
if "chain" in paths:
|
|
1491
1627
|
seeds = (ranked.get("lexical") or []) + (ranked.get("entity") or [])
|
|
1492
1628
|
ranked["chain"], chain_prov = self._path_chain(q, entries, seeds, context)
|
|
1629
|
+
if "temporal" in paths:
|
|
1630
|
+
# P3-temporal:时间路(排名项)。时间算子/区间未启用时恒空——默认
|
|
1631
|
+
# 查询(无时间参数)零变更;启用时种子取「已过 S1/S2 门控 ∧ 已过
|
|
1632
|
+
# 时间算子过滤」的 entries(索引标量直读,不 _scan 全库)。
|
|
1633
|
+
ranked["temporal"] = self._path_temporal(
|
|
1634
|
+
entries, _t_qs, _t_qe, start_operator, end_operator)
|
|
1493
1635
|
if "fuzzy" in paths:
|
|
1494
1636
|
ranked["fuzzy"], fuzzy_source = self._path_fuzzy(
|
|
1495
1637
|
q, entries, context, expand=query_expand)
|
|
@@ -1582,12 +1724,50 @@ class MdCGOS(MdCG):
|
|
|
1582
1724
|
fused = fused_all[:k]
|
|
1583
1725
|
|
|
1584
1726
|
results = []
|
|
1727
|
+
# P2-2(§5.2):默认召回路径上的**拒绝域双语义**——与 `MdCG._emit` 同款
|
|
1728
|
+
# 接线(同一个判据函数 `MdCG.judge_with_boundary`,此处不另写一份):
|
|
1729
|
+
# 边界命中 ⇒ 卡片带 `boundary_hit` 标记(可召回可展示);资格不受影响;
|
|
1730
|
+
# 边界命中数与资格 REJECT 数**分开计数**(boundary_counts)。
|
|
1731
|
+
boundary_counts = {"hit": 0, "terms": 0,
|
|
1732
|
+
"counts": {"accept": 0, "reject": 0, "defer": 0,
|
|
1733
|
+
"blindspot": 0}}
|
|
1585
1734
|
for nid, fs in fused:
|
|
1586
1735
|
node, s = node_by_id[nid]
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1736
|
+
if quals.get(nid):
|
|
1737
|
+
# judge_ranking(白箱终排)已算过资格 → 不重复判定,边界标记
|
|
1738
|
+
# 单独取(同一个 P0-4 判据函数 `MdCG.boundary_hit`)
|
|
1739
|
+
qual = quals[nid]
|
|
1740
|
+
_b = boundary_mark(node, q, context)
|
|
1741
|
+
_c = {"accept": 0, "reject": 0, "defer": 0, "blindspot": 0}
|
|
1742
|
+
_st = str(qual.get("state") or "").lower()
|
|
1743
|
+
if _st in _c:
|
|
1744
|
+
_c[_st] = 1
|
|
1745
|
+
if _b.get("hit"):
|
|
1746
|
+
boundary_counts["hit"] += 1
|
|
1747
|
+
boundary_counts["terms"] += len(_b.get("terms") or [])
|
|
1748
|
+
node["boundary_hit"] = True
|
|
1749
|
+
node["boundary_marker"] = _b.get("marker")
|
|
1750
|
+
node["boundary_terms"] = list(_b.get("terms") or [])
|
|
1751
|
+
for _k in _c:
|
|
1752
|
+
boundary_counts["counts"][_k] += _c[_k]
|
|
1753
|
+
elif judge:
|
|
1754
|
+
_jw = MdCG.judge_with_boundary(node, q, context)
|
|
1755
|
+
qual = dict(_jw["qualification"])
|
|
1756
|
+
_b = boundary_mark(node, q, context)
|
|
1757
|
+
if _b.get("hit"):
|
|
1758
|
+
boundary_counts["hit"] += 1
|
|
1759
|
+
boundary_counts["terms"] += len(_b.get("terms") or [])
|
|
1760
|
+
node["boundary_hit"] = True
|
|
1761
|
+
node["boundary_marker"] = _b.get("marker")
|
|
1762
|
+
node["boundary_terms"] = list(_b.get("terms") or [])
|
|
1763
|
+
for _k in ("accept", "reject", "defer", "blindspot"):
|
|
1764
|
+
boundary_counts["counts"][_k] += int(
|
|
1765
|
+
(_jw.get("counts") or {}).get(_k) or 0)
|
|
1766
|
+
else:
|
|
1767
|
+
qual = {"state": None, "reason": "judge_disabled"}
|
|
1590
1768
|
results.append((node, round(fs, 6), qual, prov.get(nid, [])))
|
|
1769
|
+
_bnd_meta = ({"boundary": boundary_counts}
|
|
1770
|
+
if boundary_counts["hit"] else {})
|
|
1591
1771
|
if record and results:
|
|
1592
1772
|
self.record_access([r[0]["id"] for r in results], "RRF")
|
|
1593
1773
|
# 热路径:写 query 结果缓存 —— **同样受 `_time_on` 约束**。
|
|
@@ -1603,7 +1783,7 @@ class MdCGOS(MdCG):
|
|
|
1603
1783
|
"expand_source": fuzzy_source,
|
|
1604
1784
|
"goal_used": goal_used,
|
|
1605
1785
|
"provenance": prov, **_tf_meta,
|
|
1606
|
-
**_gates_meta}, k=k, layer=layer,
|
|
1786
|
+
**_gates_meta, **_bnd_meta}, k=k, layer=layer,
|
|
1607
1787
|
session=session, branch=branch, validity=validity,
|
|
1608
1788
|
view=view, extra=_cache_extra)
|
|
1609
1789
|
return results, {"tier": "RRF", "scanned": stat["scanned"],
|
|
@@ -1614,7 +1794,7 @@ class MdCGOS(MdCG):
|
|
|
1614
1794
|
"expand_source": fuzzy_source,
|
|
1615
1795
|
"goal_used": goal_used,
|
|
1616
1796
|
"provenance": prov, **_tf_meta,
|
|
1617
|
-
**_gates_meta}
|
|
1797
|
+
**_gates_meta, **_bnd_meta}
|
|
1618
1798
|
|
|
1619
1799
|
# ================= 7. budget-driven pack =================
|
|
1620
1800
|
|
|
@@ -1638,11 +1818,12 @@ class MdCGOS(MdCG):
|
|
|
1638
1818
|
动机:旧策略是「跳过超大、继续试更小的」——预算紧张时形成**逆向淘汰**,
|
|
1639
1819
|
越有价值的详实条目越容易被排除(实测 budget=1500 时 16 条被刷、只装 2 条)。
|
|
1640
1820
|
|
|
1641
|
-
paths/query_expand
|
|
1642
|
-
|
|
1821
|
+
paths/query_expand 缺省时行为 = 既有四路 + P3 两条图检索路(chain/temporal,
|
|
1822
|
+
按裁定 9 进缺省集;无 edges / 无时间参数时自然为空);
|
|
1823
|
+
显式传 paths 才启用第 5/6 路,例如
|
|
1643
1824
|
("lexical","bucket","entity","graph","fuzzy") 词表驱动
|
|
1644
1825
|
("lexical","bucket","entity","graph","semantic") 条件结构驱动
|
|
1645
|
-
(…
|
|
1826
|
+
(… 八路全开 ) 叠加
|
|
1646
1827
|
include_recent=True 时,把近期事件窗口(第 5 篇第 3 章)附在包后,
|
|
1647
1828
|
保证当前任务的连续性;它不参与 RRF 正排,但计入 token 预算。
|
|
1648
1829
|
返回 {pack: [...], tokens_used, budget, skipped: [...], recent: [...], meta}
|
package/md_cg/nodefile.py
CHANGED
|
@@ -637,4 +637,77 @@ def cond_terms(text: str) -> list[str]:
|
|
|
637
637
|
if seg not in seen:
|
|
638
638
|
seen.add(seg)
|
|
639
639
|
out.append(seg)
|
|
640
|
-
return out
|
|
640
|
+
return out
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
# ---- §5.1 六要素的**索引角色**:那张表从 4 行长成 6 行(P2-1)------------------
|
|
644
|
+
#
|
|
645
|
+
# 理论真源 `docs/theory/智能论3.4.md:3053-3059` 的「要素 | 标记 | 内容 |
|
|
646
|
+
# **图的索引角色**」表**只有 4 行**(功能名/生效条件/子功能/执行)。v0.4 §5.1
|
|
647
|
+
# 依裁定 2 把表补成 6 行,后两行为**新增检索维度**;本常量即那 6 行的
|
|
648
|
+
# **代码侧唯一真源**(检索面、索引条目、守卫三处共用一份,禁止各写一份)。
|
|
649
|
+
#
|
|
650
|
+
# 表的两列语义:
|
|
651
|
+
# · 字段名 = CCG 六要素之一(`CCG_MARKS` 的子集,顺序即理论表序);
|
|
652
|
+
# · 索引键 = 该要素在检索面上的**键名**(前 4 行既有,后 2 行本批新增)。
|
|
653
|
+
# 「索引键」进索引条目(`MdCG._node_entry`)成为**免读文件的扁指标量**——
|
|
654
|
+
# 与既有 `time_window` / `observation_position` 同款理由(检索期不读盘)。
|
|
655
|
+
CCG_INDEX_ROLES = (
|
|
656
|
+
("功能名", "语义符号", "已有"),
|
|
657
|
+
("生效条件", "条件词", "已有(S2 门控)"),
|
|
658
|
+
("子功能", "结构词", "已有"),
|
|
659
|
+
("执行", "机制词", "已有"),
|
|
660
|
+
("验证方式", "后置条件词", "P2-1 新增:按验证手段检索"),
|
|
661
|
+
("不适用条件", "拒绝域词", "P2-1 新增:按边界检索(boundary_hit)"),
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
#: 新增两行的**字段名 → 索引键名**映射(索引条目里的扁平键;单一真源)。
|
|
665
|
+
#: 键名刻意带 `_terms` 后缀:它们是**词项列表**(扁指标量),不是正文行原样。
|
|
666
|
+
POSTCONDITION_FIELD = "验证方式"
|
|
667
|
+
REJECTION_FIELD = "不适用条件"
|
|
668
|
+
POSTCONDITION_TERMS_KEY = "postcondition_terms"
|
|
669
|
+
REJECTION_TERMS_KEY = "rejection_terms"
|
|
670
|
+
INDEX_TERMS_KEYS = (POSTCONDITION_TERMS_KEY, REJECTION_TERMS_KEY)
|
|
671
|
+
|
|
672
|
+
#: 索引词项的最小长度。口径与 `mdcg.NEG_MIN_TERM`(负条件判据的词长下限)
|
|
673
|
+
#: **同值同义**:单字符碎片(的/与/3)不是检索键。两处不可各自取值——
|
|
674
|
+
#: `md_cg/test_p2_six_elements.py` 有交叉断言钉住两常量相等。
|
|
675
|
+
ELEMENT_TERM_MIN = 2
|
|
676
|
+
|
|
677
|
+
#: 索引词项的切分面:槽分隔(;;)、短语分隔(,,、/)、括号与空白。
|
|
678
|
+
#: 为什么与 `cond_terms` 不同:那两个要素是**自由文本行**(不是条件空间四槽
|
|
679
|
+
#: 合成串),没有「槽标签:」前缀,故不需要剥标签那一步;切分面本身同族。
|
|
680
|
+
_ELEMENT_TERM_SPLIT_RE = re.compile(r"[;;,,、/()()\[\]【】{}\s]+")
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
# 生效条件:value 为假值(None/空串/纯空白)时按空文本处理返回 [];否则按「;;,,、/()()[]【】{}空白」切分、逐段 strip、丢弃长度 < ELEMENT_TERM_MIN 的段与纯数字段、命中 is_dep_sentinel(空值语义哨兵)或 is_placeholder_text(骨架占位)的段、以及已入选的重复段,返回保序去重的 out;
|
|
684
|
+
def element_terms_from_text(value, min_len: int = ELEMENT_TERM_MIN) -> list[str]:
|
|
685
|
+
"""要素文本 → 索引词项(**确定性切分,无语义猜测**)。
|
|
686
|
+
|
|
687
|
+
这是「六要素索引键」的**切分单点**:`MdCG._node_entry` 与守卫共用。
|
|
688
|
+
只做形态切分 + 空值语义剔除,不做任何同义/近义扩展(同 `cond_terms` 纪律)。
|
|
689
|
+
"""
|
|
690
|
+
out, seen = [], set()
|
|
691
|
+
for seg in _ELEMENT_TERM_SPLIT_RE.split(str(value or "")):
|
|
692
|
+
seg = seg.strip().strip("。..::")
|
|
693
|
+
if len(seg) < int(min_len) or seg.isdigit():
|
|
694
|
+
continue
|
|
695
|
+
if is_dep_sentinel(seg) or is_placeholder_text(seg):
|
|
696
|
+
continue
|
|
697
|
+
if seg not in seen:
|
|
698
|
+
seen.add(seg)
|
|
699
|
+
out.append(seg)
|
|
700
|
+
return out
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
# 生效条件:field_name 为 CCG 要素名且 content 含该行时,取该行值(走 ccg_field_value 单点)→ 经 element_terms_from_text 切分成词项列表;该行缺失/值为空时返回 [];
|
|
704
|
+
def ccg_element_terms(content: str, field_name: str,
|
|
705
|
+
min_len: int = ELEMENT_TERM_MIN) -> list[str]:
|
|
706
|
+
"""CCG 要素文本 → 索引词项(**取值走既有单点**,检索面不得另写正则)。
|
|
707
|
+
|
|
708
|
+
取值**必须**经 `ccg_field_value`(仓内 CCG 行解析的唯一真源:冒号可有可无、
|
|
709
|
+
首个命中行为准、与写入闸门 `data/policy.json` 同一行语义);本函数只在其上
|
|
710
|
+
叠加**切分**,绝不自己 `re.match(r"^#\\s*验证方式")` ——那会立刻长出第二套
|
|
711
|
+
行语义(`ccg_mark_present` 的取单点动因即此类分叉)。
|
|
712
|
+
"""
|
|
713
|
+
return element_terms_from_text(ccg_field_value(content, field_name), min_len)
|
|
@@ -31,6 +31,28 @@ from . import zh_en_atoms
|
|
|
31
31
|
WIN = 6 # 组合共现窗口(token 距离,探针实证口径)
|
|
32
32
|
_ATOMS_ZH = None
|
|
33
33
|
|
|
34
|
+
# 中文段判据(单点,2026-09-30 检索归一层作用域收窄时立):
|
|
35
|
+
# 中文 = CJK 统一表意文字基本区 U+4E00–U+9FFF。与 en_normalizer 分词字符类
|
|
36
|
+
# `[A-Za-z\u4e00-\u9fff]` 的中文区间同区间;rust/src/text.rs::is_zh 是同判据的
|
|
37
|
+
# Rust 侧单点(两侧归一层共用同一判据;改动须两侧同步,否则 rank_parity 漂移)。
|
|
38
|
+
# 消费方:unify.py::unify_query(中文段逐字保留)、rust/src/atoms.rs::Atoms::unify。
|
|
39
|
+
ZH_LO, ZH_HI = "\u4e00", "\u9fff"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# 生效条件:单参 ch 为长度恰为 1 的 str 时返回 ZH_LO <= ch <= ZH_HI(CJK 基本区);长度 != 1(空串/多字符串)一律返回 False(2026-09-30 清理批次把边界收紧为「长度恰为 1」——此前只在 ch 为空串时为假,多字符串按字典序比较可能被误判为真)。
|
|
43
|
+
def is_zh_char(ch):
|
|
44
|
+
"""中文段判据(单点):CJK 统一表意文字基本区 U+4E00–U+9FFF。
|
|
45
|
+
|
|
46
|
+
边界约定(2026-09-30 清理批次乙2):**仅长度恰为 1 的串可为真**——
|
|
47
|
+
空串与长度 >1 的串一律 False。此前只有空串为假(链式比较
|
|
48
|
+
`ZH_LO <= ch` 对空串即假),而多字符串按字典序比较:`is_zh_char("中文")`
|
|
49
|
+
判真(首字符在区间即真)=判据外溢。两侧调用点清点结果:本函数只有
|
|
50
|
+
`unify.py::_runs` 与 `test_unify_scope.py::zh_runs` 两处消费,均逐字符
|
|
51
|
+
传入 ⇒ 本收紧对存量行为零变更;Rust 侧 `text.rs::is_zh` 收 `char`,
|
|
52
|
+
天然无空/多字符二态,边界语义两侧一致(docstring 已互指)。
|
|
53
|
+
"""
|
|
54
|
+
return len(ch) == 1 and ZH_LO <= ch <= ZH_HI
|
|
55
|
+
|
|
34
56
|
|
|
35
57
|
# 生效条件:模块级常量 `_ATOMS_ZH` 为 None 时按 `__file__` 所在目录的 atoms.json 取 `data.get("atoms", [])` 各项 `zh` 建集合并缓存,非 None 时直接返回 `_ATOMS_ZH`。
|
|
36
58
|
def atoms_zh():
|
package/md_cg/semantic/unify.py
CHANGED
|
@@ -1,45 +1,96 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
"""灵枢 · 检索 query 统一归一层(批次 15
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
segment 展开为标准原子(「牛肉」→「牛 肉」,与 doc 侧
|
|
9
|
-
原子形态同构);
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
"""灵枢 · 检索 query 统一归一层(批次 15 立;2026-09-30 使用者裁定收窄作用域)
|
|
4
|
+
|
|
5
|
+
口径:**只对英文内容做翻译归一,中文内容原样不动**。
|
|
6
|
+
query 先按「中文段 / 非中文段」切开,只有**非中文段里的英文**进归一链路:
|
|
7
|
+
- 非中文段(含 ASCII 字母):en_normalizer(时态还原→停用词剔除→en→zh 语素
|
|
8
|
+
映射)→ segment 展开为标准原子(「牛肉」→「牛 肉」,与 doc 侧
|
|
9
|
+
fm.semantic 原子形态同构);
|
|
10
|
+
- 中文段(CJK 统一表意文字基本区 U+4E00–U+9FFF,判据单点 =
|
|
11
|
+
`canonical.is_zh_char`):**逐字保留、绝不切分、绝不送 segment**;
|
|
12
|
+
- 非英文段(数字/标点等,段内无 ASCII 字母):原样保留(不是英文即不翻译);
|
|
13
|
+
- 两段按原顺序以单空格 join,空白(段内/段间)一律折叠为单空格;
|
|
14
14
|
- 专名/未登录词原样保留(词表边界 unknown_keep,非错误)。
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
为什么收窄(2026-09-30 定因,实测于 locomo-zh-500 公开题池 500 题 / 567 池、
|
|
17
|
+
eval_common jaccard + GLOBAL_CAP 解除、k=5):旧口径「query 含任一 ASCII 字母
|
|
18
|
+
即整条归一」会把**中夹英** query 的中文部分逐字切开(「自我接纳」→
|
|
19
|
+
「自 我 接 纳」、「慈善跑」→「慈 善 跑」);500 题里含 ASCII 字母的 145 条
|
|
20
|
+
**全部**也含中文(无纯英文 query)。单点实测:整条归一 lexical hit@1 = 94.4% /
|
|
21
|
+
lexical,fuzzy = 87.2%;只译英文片段 lexical = 95.8% / lexical,fuzzy = 96.6%;
|
|
22
|
+
归一层全关 lexical = 96.4% / lexical,fuzzy = 97.2%。
|
|
23
|
+
|
|
24
|
+
开关:MDCG_UNIFY_QUERY **默认关**(2026-09-30 使用者裁定,缺省由 "1" 翻为 "0")。
|
|
25
|
+
定因:本层本职是让**英文** query 命中中文节点,对中文检索池是**纯开销**——
|
|
26
|
+
locomo-zh-500 公开题池(池 567 / 题 500)缺省关态 lexical hit@1 96.4% /
|
|
27
|
+
lexical+fuzzy 97.2%,开态 95.8% / 96.6%(各跑两遍逐位相同)。
|
|
28
|
+
|
|
29
|
+
三态判据(两侧同语义,唯一开态 = 显式 "1"):
|
|
30
|
+
|
|
31
|
+
MDCG_UNIFY_QUERY Python unify_on() Rust Atoms::from_env()
|
|
32
|
+
未设 False None(不载入)
|
|
33
|
+
"0" False None(不载入)
|
|
34
|
+
"1" True Some(Atoms)(载入)
|
|
35
|
+
|
|
36
|
+
显式 "1" 仍能开回(英文对照路要用);两侧同源 en_zh_map.json
|
|
37
|
+
(scripts/rank_parity.py --dataset 对拍在两侧同开关态进行)。
|
|
20
38
|
|
|
21
39
|
归一失败(semantic 模块缺失等)静默原样返回——与 en_zh_terms 同降级
|
|
22
40
|
风格,不阻断检索主链路。
|
|
41
|
+
|
|
42
|
+
两侧同构:rust/src/atoms.rs::Atoms::unify 同语义、同中文判据;逐位对拍 fixture
|
|
43
|
+
= `md_cg/semantic/unify_fixture.json`(本侧守卫 md_cg/test_unify_scope.py 与
|
|
44
|
+
Rust 单测 unify_mixed_fixture_matches_python 钉同一份期望值)。
|
|
23
45
|
"""
|
|
24
46
|
import os
|
|
25
47
|
import re
|
|
26
48
|
|
|
27
|
-
|
|
49
|
+
from .canonical import is_zh_char
|
|
50
|
+
|
|
51
|
+
# 生效条件:无 required 形参,锚定环境变量名 MDCG_UNIFY_QUERY;**未设**时取缺省 "0"(2026-09-30 使用者裁定:缺省关),当 os.environ.get("MDCG_UNIFY_QUERY", "0") == "1" 时返回 True,其余(未设 / "0" / 任何非 "1" 取值)返回 False。
|
|
28
52
|
def unify_on() -> bool:
|
|
29
|
-
"""
|
|
30
|
-
return os.environ.get("MDCG_UNIFY_QUERY", "
|
|
53
|
+
"""统一归一层开关(**默认关**;显式 =1 才开,=0/未设一律关)。"""
|
|
54
|
+
return os.environ.get("MDCG_UNIFY_QUERY", "0") == "1"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# 生效条件:单参 text 为任意字符串(空串返回空列表),按 canonical.is_zh_char 逐字符判中文段,相邻同判据字符归并为一个连续段,返回 [(是否中文段, 段文本), ...] 的保序列表。
|
|
58
|
+
def _runs(text):
|
|
59
|
+
"""按中文判据切连续段:[(True, "中文段"), (False, "非中文段"), ...]。"""
|
|
60
|
+
out = []
|
|
61
|
+
cur = None
|
|
62
|
+
buf = []
|
|
63
|
+
for ch in text:
|
|
64
|
+
k = is_zh_char(ch)
|
|
65
|
+
if buf and k != cur:
|
|
66
|
+
out.append((cur, "".join(buf)))
|
|
67
|
+
buf = []
|
|
68
|
+
cur = k
|
|
69
|
+
buf.append(ch)
|
|
70
|
+
if buf:
|
|
71
|
+
out.append((cur, "".join(buf)))
|
|
72
|
+
return out
|
|
31
73
|
|
|
32
74
|
|
|
33
|
-
# 生效条件:text 为 None
|
|
75
|
+
# 生效条件:text 为 None/空串、或开关 MDCG_UNIFY_QUERY 取值不为 "1"(含**未设**——缺省 "0",2026-09-30 起关)、或 strip 后不含 [A-Za-z] 时,原样返回 text(未 strip 的入参);否则按中文段(canonical.is_zh_char)切分——中文段与不含 ASCII 字母的非中文段逐字保留、含 ASCII 字母的非中文段经 canonical.query_atoms 归一——各段折空白后按原顺序单空格 join;归一产物为空(trim 后)或抛异常时原样返回 text。
|
|
34
76
|
def unify_query(text):
|
|
35
|
-
"""
|
|
77
|
+
"""检索入口统一归一:只译英文内容,中文段逐字原样。"""
|
|
36
78
|
t = (text or "").strip()
|
|
37
79
|
if not t or not unify_on() or not re.search(r"[A-Za-z]", t):
|
|
38
80
|
return text
|
|
39
81
|
try:
|
|
40
82
|
from .canonical import query_atoms
|
|
41
|
-
|
|
83
|
+
parts = []
|
|
84
|
+
for is_zh, run in _runs(t):
|
|
85
|
+
if is_zh or not re.search(r"[A-Za-z]", run):
|
|
86
|
+
seg = run # 中文段 / 非英文段:逐字保留
|
|
87
|
+
else:
|
|
88
|
+
atoms = query_atoms(run) # 非中文段里的英文:既有链路
|
|
89
|
+
seg = " ".join(a for a in atoms if a)
|
|
90
|
+
seg = " ".join(seg.split()) # 段内空白折叠为单空格
|
|
91
|
+
if seg:
|
|
92
|
+
parts.append(seg)
|
|
42
93
|
except Exception:
|
|
43
94
|
return text
|
|
44
|
-
u = " ".join(
|
|
95
|
+
u = " ".join(parts)
|
|
45
96
|
return u if u else text
|