@furongjun1999/dsh-memory 0.4.8 → 0.4.10
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 +54 -26
- package/codebuddy/CODEBUDDY.md +196 -195
- package/codebuddy/README.md +13 -1
- package/codebuddy/mcp.json +9 -0
- package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
- package/docs/README.md +1 -1
- package/docs/discipline/harnesses.yaml +18 -7
- package/docs/discipline/templates/full.md.tmpl +4 -3
- package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
- package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
- package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
- package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
- package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
- package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
- package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -0
- 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 +102 -0
- package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
- package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
- package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -0
- package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
- package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
- package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -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 +47 -45
- package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/{206_v0.4.md → 206_v0.5.md} +92 -4
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -0
- package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
- package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
- package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +2 -2
- package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
- package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +2 -2
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +434 -433
- package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
- package/dsh/README.md +33 -0
- package/dsh/cordis.yml.example +13 -7
- package/dsh/hive-mcp-probe.mjs +94 -0
- package/dsh/hive-mcp.example.yml +62 -0
- package/dsh/update-lingshu.bat +11 -0
- package/dsh/update-lingshu.ps1 +337 -0
- package/lib/hooks.d.ts +3 -0
- package/lib/hooks.js +17 -23
- package/lib/index.d.ts +4 -2
- package/lib/index.js +29 -6
- package/lib/lib/datapath.d.ts +76 -1
- package/lib/lib/datapath.js +199 -13
- package/lib/lib/mdcg_client.d.ts +40 -3
- package/lib/lib/mdcg_client.js +46 -22
- package/lib/lib/mutual.js +4 -4
- package/lib/lib/token_store.js +4 -5
- package/md_cg/audit.py +17 -2
- package/md_cg/autonomy.py +86 -15
- package/md_cg/backfill.py +36 -1
- package/md_cg/backfill_bigdomain.py +34 -0
- package/md_cg/bench6_arms.py +28 -1
- package/md_cg/bench6_common.py +10 -1
- package/md_cg/bench6_competitors.py +6 -1
- package/md_cg/bench_axis_domain.py +9 -1
- package/md_cg/bench_blind_comp.py +7 -1
- package/md_cg/bench_en_atoms_public.py +9 -0
- package/md_cg/bench_governance.py +348 -0
- package/md_cg/bench_lme_zh.py +16 -1
- package/md_cg/bench_locomo.py +2 -1
- package/md_cg/bench_locomo_zh.py +16 -1
- package/md_cg/bench_locomo_zh_public.py +4 -1
- package/md_cg/bench_longmem.py +2 -1
- package/md_cg/bench_membench.py +27 -1
- package/md_cg/bench_p0.py +4 -1
- package/md_cg/bench_progressive.py +13 -1
- package/md_cg/bench_role_views.py +238 -0
- package/md_cg/bench_task_ab.py +8 -1
- package/md_cg/bench_task_ab_llm.py +13 -1
- package/md_cg/bench_unified_en.py +6 -1
- package/md_cg/bench_zh_mad.py +20 -1
- package/md_cg/blindspot_tickets.py +123 -0
- package/md_cg/branches.py +12 -1
- package/md_cg/build_postings.py +73 -0
- package/md_cg/ccgc.py +67 -2
- package/md_cg/census.py +5 -1
- package/md_cg/chain.py +24 -3
- package/md_cg/codeindex.py +134 -17
- package/md_cg/coldverify.py +265 -0
- package/md_cg/comment_gate.py +338 -0
- package/md_cg/cond_compose.py +190 -0
- package/md_cg/cond_facts.py +155 -0
- package/md_cg/cond_template.json +107 -0
- package/md_cg/condition_anchor.py +143 -0
- package/md_cg/conformance.py +69 -4
- package/md_cg/consistency.py +24 -1
- package/md_cg/consolidate.py +53 -2
- package/md_cg/corpus.py +4 -0
- package/md_cg/crosscheck.py +42 -2
- package/md_cg/crypto.py +35 -1
- package/md_cg/d_meta.py +310 -0
- package/md_cg/datapath.py +201 -26
- package/md_cg/docindex.py +122 -1
- package/md_cg/eval_common.py +29 -1
- package/md_cg/evidence.py +27 -1
- package/md_cg/evolution.py +21 -1
- package/md_cg/export.py +11 -1
- package/md_cg/forgetting.py +23 -1
- package/md_cg/fsutil.py +18 -1
- package/md_cg/hotcache.py +214 -0
- package/md_cg/hyperedge.py +251 -0
- package/md_cg/identity.py +18 -1
- package/md_cg/insight.py +17 -1
- package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
- package/md_cg/lexicon/build_standard_en.py +171 -168
- package/md_cg/lexicon/expand_en_zh.py +6 -0
- package/md_cg/lifecycle.py +12 -1
- package/md_cg/linkref.py +281 -0
- package/md_cg/links.py +29 -1
- package/md_cg/mcp_server.py +362 -43
- package/md_cg/md_whitebox.py +53 -1
- package/md_cg/mdcg.py +1003 -27
- package/md_cg/mdcos.py +558 -36
- package/md_cg/metacognition.py +37 -2
- package/md_cg/migrate.py +4 -0
- package/md_cg/migrate_aeis.py +221 -213
- package/md_cg/migrate_roleplay.py +8 -0
- package/md_cg/migrate_wisdom_graph.py +14 -1
- package/md_cg/mreview/__main__.py +3 -0
- package/md_cg/mreview/bundle.py +8 -0
- package/md_cg/mreview/candidates.py +9 -0
- package/md_cg/mreview/govern.py +21 -1
- package/md_cg/mreview/locate.py +34 -0
- package/md_cg/mreview/pipeline.py +29 -1
- package/md_cg/mreview/ruleset.py +16 -1
- package/md_cg/nodefile.py +233 -3
- package/md_cg/pooling.py +23 -1
- package/md_cg/postings.py +298 -0
- package/md_cg/predict.py +89 -9
- package/md_cg/progressive.py +3 -0
- package/md_cg/protect.py +14 -1
- package/md_cg/protocol.py +372 -0
- package/md_cg/provenance.py +262 -0
- package/md_cg/reach.py +453 -0
- package/md_cg/refindex.py +47 -2
- package/md_cg/refine.py +20 -1
- package/md_cg/roleviews.py +89 -0
- package/md_cg/routing.py +76 -0
- package/md_cg/scrub.py +63 -2
- package/md_cg/security.py +26 -1
- package/md_cg/self_state.py +64 -1
- package/md_cg/selfreport.py +151 -0
- package/md_cg/semantic/canonical.py +5 -0
- package/md_cg/semantic/en_normalizer.py +364 -355
- package/md_cg/semantic/zh_en_atoms.py +139 -136
- package/md_cg/signer.py +41 -1
- package/md_cg/sources.py +583 -547
- package/md_cg/statushdr.py +179 -0
- package/md_cg/stg.py +59 -18
- package/md_cg/subgraph.py +23 -0
- package/md_cg/sustain.py +56 -1
- package/md_cg/tasks.py +26 -2
- package/md_cg/test_autonomy.py +26 -0
- package/md_cg/test_bench_governance.py +102 -0
- package/md_cg/test_blindspot_tickets.py +166 -0
- package/md_cg/test_ccgc.py +10 -0
- package/md_cg/test_codeindex.py +338 -0
- package/md_cg/test_comment_gate.py +187 -0
- package/md_cg/test_cond_compose_anchors.py +76 -0
- package/md_cg/test_condition_anchor.py +82 -0
- package/md_cg/test_d_meta.py +412 -0
- package/md_cg/test_datapath_root.py +188 -0
- package/md_cg/test_gain_gate.py +47 -1
- package/md_cg/test_hot_cold.py +187 -0
- package/md_cg/test_hyperedge.py +245 -0
- package/md_cg/test_linkref.py +306 -0
- package/md_cg/test_md_access_parity.py +15 -3
- package/md_cg/test_mr_m1.py +108 -18
- package/md_cg/test_mr_m3.py +8 -1
- package/md_cg/test_p26_refindex.py +49 -20
- package/md_cg/test_p27_docindex.py +236 -2
- package/md_cg/test_p2_mcp.py +1 -1
- package/md_cg/test_p31_insight.py +24 -0
- package/md_cg/test_p44_md_whitebox.py +14 -1
- package/md_cg/test_protocol.py +243 -0
- package/md_cg/test_reach.py +378 -0
- package/md_cg/test_reach_keys.py +201 -0
- package/md_cg/test_reach_meta_exits.py +145 -0
- package/md_cg/test_read_clip.py +8 -4
- package/md_cg/test_retr_s1.py +340 -0
- package/md_cg/test_retr_s1b.py +209 -0
- package/md_cg/test_retr_s3.py +194 -0
- package/md_cg/test_retr_s4.py +163 -0
- package/md_cg/test_retr_s5.py +200 -0
- package/md_cg/test_retr_s6.py +157 -0
- package/md_cg/test_retr_s7.py +385 -0
- package/md_cg/test_retr_s8_time.py +316 -0
- package/md_cg/test_retr_s9_edges.py +286 -0
- package/md_cg/test_retr_s9_entity_ctx.py +175 -0
- package/md_cg/test_review_conformance.py +59 -2
- package/md_cg/test_role_views.py +354 -0
- package/md_cg/test_subproc_encoding.py +188 -0
- package/md_cg/test_trust.py +361 -0
- package/md_cg/test_units_poll.py +71 -0
- package/md_cg/test_v14_fixes.py +397 -0
- package/md_cg/test_validity_filter.py +280 -0
- package/md_cg/test_wisdom_md_store.py +7 -3
- package/md_cg/test_writepipe.py +5 -1
- package/md_cg/theory.py +16 -1
- package/md_cg/tokens.py +40 -8
- package/md_cg/tool_face.py +13 -2
- package/md_cg/trust.py +943 -0
- package/md_cg/twophase.py +12 -1
- package/md_cg/units.py +132 -10
- package/md_cg/vision_evidence.py +24 -1
- package/md_cg/weights.py +24 -1
- package/md_cg/whitebox.py +32 -1
- package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
- package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
- package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
- package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
- package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
- package/md_cg/writelimit.py +18 -4
- package/md_cg/writepipe.py +178 -7
- package/package.json +2 -2
- package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
- package/skills/skills/designer-perspective/scripts/designer.py +17 -1
- package/skills/skills/designer-perspective/tests/selftest.py +3 -1
- package/src/hooks.ts +17 -21
- package/src/index.ts +33 -6
- package/src/lib/datapath.ts +211 -13
- package/src/lib/mdcg_client.ts +64 -25
- package/src/lib/mutual.ts +411 -411
- package/src/lib/token_store.ts +4 -5
- package/zcode/AGENTS.md +196 -195
- package/zcode/README.md +4 -0
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
package/md_cg/docindex.py
CHANGED
|
@@ -58,6 +58,7 @@ DEFAULT_SENSITIVITY = "internal"
|
|
|
58
58
|
PRIVATE_HINTS = ("private", "secret", "internal", "未公开", "私有", "内部")
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
# 生效条件:override 为真值即返回 (override, "调用方显式指定");否则 path(假值按 "")按 "\" 与 "/" 分段,任一段小写含 PRIVATE_HINTS 中任一提示即返回 ("private", 路径段命中理由),全部不命中返回 (DEFAULT_SENSITIVITY, 默认密级理由);
|
|
61
62
|
def sensitivity_for(path, override=None):
|
|
62
63
|
"""返回 (密级, 依据)。override 优先;否则按路径段保守降级。
|
|
63
64
|
|
|
@@ -81,6 +82,7 @@ _FENCE = re.compile(r"^\s*(```+|~~~+)")
|
|
|
81
82
|
_ATX = re.compile(r"^(#{1,6})\s+(.+?)\s*#*\s*$")
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
# 生效条件:lines 非空且 lines[0].strip() == "---" 时,从下标 1 起找到首个 strip() == "---" 的行并返回其后一行下标 i+1;lines 为空、首行不是 "---" 或找不到闭合 "---" 时返回 0;
|
|
84
86
|
def _body_start(lines):
|
|
85
87
|
"""跳过开头 YAML frontmatter,返回正文起始行下标(0 基)。"""
|
|
86
88
|
if lines and lines[0].strip() == "---":
|
|
@@ -90,6 +92,7 @@ def _body_start(lines):
|
|
|
90
92
|
return 0
|
|
91
93
|
|
|
92
94
|
|
|
95
|
+
# 生效条件:从 start(默认 0)遍历 lines,未处于围栏时遇 _FENCE 匹配行打开同标记围栏、围栏内遇同标记行关闭并继续;围栏外 _ATX 匹配行追加 {level: 一级 # 个数, title: 去空白后的标题, lineno: i+1};返回 out 列表;start 不小于 len(lines) 时返回空列表;
|
|
93
96
|
def _headings(lines, start=0):
|
|
94
97
|
"""产出 ATX 标题 `{level,title,lineno}`;**围栏代码块内的 `#` 不算标题**。"""
|
|
95
98
|
out, fence = [], None
|
|
@@ -112,6 +115,7 @@ def _headings(lines, start=0):
|
|
|
112
115
|
return out
|
|
113
116
|
|
|
114
117
|
|
|
118
|
+
# 生效条件:title 真值时取其 strip 后小写,删去 `*[]() 与除 \w\s- 外字符,再把空白/下划线连成 "-" 并 strip("-");title 为假值(含 None、空串)时按空串处理并返回 "";
|
|
115
119
|
def _anchor(title):
|
|
116
120
|
a = (title or "").strip().lower()
|
|
117
121
|
a = re.sub(r"`|\*|\[|\]|\(|\)", "", a)
|
|
@@ -119,6 +123,7 @@ def _anchor(title):
|
|
|
119
123
|
return re.sub(r"[\s_]+", "-", a).strip("-")
|
|
120
124
|
|
|
121
125
|
|
|
126
|
+
# 生效条件:region_lines 逐行 strip 后跳过空行与 "---",去掉行首 #+ 和 >|*- 标记,以空格连接成 text;返回 text[:limit](limit 默认 MAX_SUMMARY;limit=0 返回 "",limit=None 返回全文,limit='' 时切片抛 TypeError,负 limit 按负索引切片);
|
|
122
127
|
def _summary(region_lines, limit=MAX_SUMMARY):
|
|
123
128
|
"""把一段正文压成一行摘要(去 markdown 噪声,不逐字保留)。"""
|
|
124
129
|
parts = []
|
|
@@ -133,6 +138,7 @@ def _summary(region_lines, limit=MAX_SUMMARY):
|
|
|
133
138
|
return text[:limit]
|
|
134
139
|
|
|
135
140
|
|
|
141
|
+
# 生效条件:以 heads[i]["level"]-1 为需匹配层级向前回溯,返回按层级递减补齐的祖先标题列表(不含 heads[i] 自身)。
|
|
136
142
|
def _path_titles(heads, i):
|
|
137
143
|
"""第 i 个标题的祖先链(不含自身),按层级补齐。"""
|
|
138
144
|
out, need = [], heads[i]["level"] - 1
|
|
@@ -145,11 +151,13 @@ def _path_titles(heads, i):
|
|
|
145
151
|
return out
|
|
146
152
|
|
|
147
153
|
|
|
154
|
+
# 生效条件:直接以 lines、lineno、end 调用 codeindex.region_hash 并返回其结果;
|
|
148
155
|
def _region_hash(lines, lineno, end):
|
|
149
156
|
# 唯一实现复用 codeindex.region_hash:两侧各写一份,漂移检测会悄悄失效。
|
|
150
157
|
return codeindex.region_hash(lines, lineno, end)
|
|
151
158
|
|
|
152
159
|
|
|
160
|
+
# 生效条件:ext = suffix 真值时原样使用的 suffix,否则取 os.path.splitext(path)[1].lower();ext 不在 SUFFIX 时抛 ValueError;在 SUFFIX 时把 source 按 "\n" 拆分,经 _body_start 与 _headings 得到标题,仅 level<=MAX_LEVEL 且非 small 的标题生成条目,小/过深子节摘要并入父摘要,返回 items;
|
|
153
161
|
def extract(source, path="", suffix=None):
|
|
154
162
|
"""抽取一份 md 的章节条目;按后缀分派。返回条目列表(可能为空)。
|
|
155
163
|
|
|
@@ -213,6 +221,7 @@ def extract(source, path="", suffix=None):
|
|
|
213
221
|
# --------------------------------------------------------------------------
|
|
214
222
|
# 渲染 / id
|
|
215
223
|
# --------------------------------------------------------------------------
|
|
224
|
+
# 生效条件:item.get("path") 缺键或为假值时 path 取 "",top 取 path.split("/")[0] or "."(故空 path 时 top=".");path 为真值时 top 取其 "/" 前首段,首段为空则 top=".";返回含 observation_position(大域=top)、time_window([nodefile.FULL_TIME_WINDOW_MIN, nodefile.FULL_TIME_WINDOW_MAX])、observation_tool、existence_constraint(以 path 拼入)的四槽字典;
|
|
216
225
|
def condition_space(item):
|
|
217
226
|
"""章节条目 → 条件空间四槽(纯函数,**唯一来源**)。
|
|
218
227
|
|
|
@@ -237,6 +246,7 @@ def condition_space(item):
|
|
|
237
246
|
}
|
|
238
247
|
|
|
239
248
|
|
|
249
|
+
# 生效条件:item 含 heading、path、lineno、end、anchor 键时渲染 7 行 CCG 文本(第2行取 condition_space(item) 文本),item.get("parent") 或 "" 假值回落 "(顶层章节)",item.get("summary_parts") 假值回落 "(该节无直接正文,见子节)",item.get("children") 假值回落空列表且不追加子节行,children 非空时追加 "# 子节:" + 前 12 个;返回以 "\n" 连接的行串;
|
|
240
250
|
def render(item):
|
|
241
251
|
"""章节条目 → CCG 6 行正文(可被 search 命中,不含全文)。
|
|
242
252
|
|
|
@@ -265,6 +275,7 @@ def render(item):
|
|
|
265
275
|
return "\n".join(lines)
|
|
266
276
|
|
|
267
277
|
|
|
278
|
+
# 生效条件:以 item["path"] + "#" + "/".join(item["heading_path"]) 为 key,item.get("dup", 1)(缺键取 1)大于 1 时追加 "#" + item["dup"],返回 "doc_" + sha1(key utf-8) hexdigest 前 12 位;
|
|
268
279
|
def node_id(item):
|
|
269
280
|
"""稳定 id:path#heading_path 的短哈希(重复索引幂等;同名用 dup 区分)。"""
|
|
270
281
|
key = item["path"] + "#" + "/".join(item["heading_path"])
|
|
@@ -273,6 +284,116 @@ def node_id(item):
|
|
|
273
284
|
return "doc_" + hashlib.sha1(key.encode("utf-8")).hexdigest()[:12]
|
|
274
285
|
|
|
275
286
|
|
|
287
|
+
# ==========================================================================
|
|
288
|
+
# fence 真源绑定(§3.5):条件卡 ↔ Markdown 真源的「往返列」
|
|
289
|
+
#
|
|
290
|
+
# 往返列 = 真源(root + path)+ 定位(anchor / heading_path)+ 行位(lineno/end)
|
|
291
|
+
# + 校验(hash)。**行位是易腐化量,键不含行位**:在章节上方插一段话会让整篇行号
|
|
292
|
+
# 位移,但键 `path#heading_path` 不变——故「同一章节」的匹配一律走键,行位只用于
|
|
293
|
+
# 回读原文与漂移判定。写侧唯一入口是 `refindex._doc_ref`(与索引同源,避免第二份
|
|
294
|
+
# 列口径);本节只补读侧:取列 / 校验 / 造键 / 反查行位。
|
|
295
|
+
# · 正向:卡片 → 真源(`doc_ref.lineno/end` + `refindex.read_ref` 回读原文区间)
|
|
296
|
+
# · 反向:真源行 → 卡片(`locate`;对账器 `--at path:line` 即此接口)
|
|
297
|
+
# ==========================================================================
|
|
298
|
+
|
|
299
|
+
#: 必需列:真源(root/path)+ 定位(anchor)+ 行位(lineno/end)+ 校验(hash)
|
|
300
|
+
BINDING_FIELDS = ("root", "path", "anchor", "lineno", "end", "hash")
|
|
301
|
+
#: 附加列:有则参与更精确匹配与人读,缺省不算绑定失败
|
|
302
|
+
BINDING_OPTIONAL = ("heading_path", "heading", "level", "lang", "precise")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# 生效条件:node 为 cg.get 产物(含 frontmatter 子字典)或 frontmatter dict 本身,且其 doc_ref 为 dict 时,返回按 BINDING_FIELDS + BINDING_OPTIONAL 投影的列值 dict(缺列以 None 占位);非 dict / 无 doc_ref 返回 None。
|
|
306
|
+
def binding_of(node):
|
|
307
|
+
"""取一条条件卡的往返列;**非索引节点返回 None**(不猜、不补默认值)。"""
|
|
308
|
+
fm = node
|
|
309
|
+
if isinstance(node, dict) and isinstance(node.get("frontmatter"), dict):
|
|
310
|
+
fm = node["frontmatter"]
|
|
311
|
+
if not isinstance(fm, dict):
|
|
312
|
+
return None
|
|
313
|
+
ref = fm.get("doc_ref")
|
|
314
|
+
if not isinstance(ref, dict) or not ref:
|
|
315
|
+
return None
|
|
316
|
+
return {k: ref.get(k) for k in BINDING_FIELDS + BINDING_OPTIONAL}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# 生效条件:b 为 dict 且 heading_path 为非空 list/tuple 时返回 path#join(heading_path, '/'),否则回落 path#anchor;b 非 dict 返回空串。
|
|
320
|
+
def binding_key(b):
|
|
321
|
+
"""稳定键 `path#heading_path`:**不含行位**(真源重排只动行位、不动键)。"""
|
|
322
|
+
if not isinstance(b, dict):
|
|
323
|
+
return ""
|
|
324
|
+
path = str(b.get("path") or "")
|
|
325
|
+
hp = b.get("heading_path")
|
|
326
|
+
if isinstance(hp, (list, tuple)) and hp:
|
|
327
|
+
return path + "#" + "/".join(str(x) for x in hp)
|
|
328
|
+
return path + "#" + str(b.get("anchor") or "")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# 生效条件:b 为 dict 时返回 'path#anchor'(与 render 正文里的「本条目属于」逐字同源);非 dict 返回空串。
|
|
332
|
+
def binding_slug(b):
|
|
333
|
+
"""人读定位串 `path#anchor`——与 `render` 正文里的「本条目属于」同源。"""
|
|
334
|
+
b = b if isinstance(b, dict) else {}
|
|
335
|
+
return f"{b.get('path') or ''}#{b.get('anchor') or ''}"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# 生效条件:b 为 dict 时逐列校验(缺列 / 类型错 / 行位越界),返回 {"ok": bool, "issues": [str, ...]};**不抛异常**——对账要逐条报告,不能因一条坏数据中断全库;行位仅在列齐且类型对时才判,避免级联噪声。
|
|
339
|
+
def validate_binding(b):
|
|
340
|
+
"""校验往返列形状。**不抛异常**:对账逐条报告,不能一条坏数据中断全库。"""
|
|
341
|
+
if not isinstance(b, dict):
|
|
342
|
+
return {"ok": False, "issues": ["绑定不是字典(该节点无 doc_ref)"]}
|
|
343
|
+
issues = []
|
|
344
|
+
for k in BINDING_FIELDS:
|
|
345
|
+
if b.get(k) in (None, ""):
|
|
346
|
+
issues.append(f"缺列 {k}")
|
|
347
|
+
for k in ("root", "path", "anchor", "hash"):
|
|
348
|
+
v = b.get(k)
|
|
349
|
+
if v not in (None, "") and not isinstance(v, str):
|
|
350
|
+
issues.append(f"{k} 应为字符串,实为 {type(v).__name__}")
|
|
351
|
+
if not issues:
|
|
352
|
+
try:
|
|
353
|
+
lo, hi = int(b["lineno"]), int(b["end"])
|
|
354
|
+
except (TypeError, ValueError):
|
|
355
|
+
issues.append("行位不是整数")
|
|
356
|
+
else:
|
|
357
|
+
if lo < 1:
|
|
358
|
+
issues.append(f"lineno 越界({lo} < 1)")
|
|
359
|
+
if hi < lo:
|
|
360
|
+
issues.append(f"end 早于 lineno({lo}-{hi})")
|
|
361
|
+
return {"ok": not issues, "issues": issues}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# 生效条件:old/new 任意为 dict 或假值;按 (path, anchor, heading_path, lineno, end, hash) 固定顺序返回取值不同的列名列表(空列表=同一章节同一行位),任一侧假值按空 dict 处理。
|
|
365
|
+
def binding_drift(old, new):
|
|
366
|
+
"""旧往返列 vs 新往返列 → 变化列名(顺序固定,供报告与测试断言)。"""
|
|
367
|
+
old, new = old if isinstance(old, dict) else {}, new if isinstance(new, dict) else {}
|
|
368
|
+
return [k for k in ("path", "anchor", "heading_path", "lineno", "end", "hash")
|
|
369
|
+
if old.get(k) != new.get(k)]
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# 生效条件:items 为 extract 产出的条目序列、lineno 可转 int;返回覆盖该行的条目中 **lineno 最大者**(嵌套即最内层,h3 优于其父 h2);不可转值或无可覆盖条目返回 None。
|
|
373
|
+
def locate(items, lineno):
|
|
374
|
+
"""反向定位:真源第 `lineno` 行 → 覆盖它的条目(嵌套取**最内层**)。
|
|
375
|
+
|
|
376
|
+
区间闭合:标题行算本节;多层嵌套时取 `lineno` 最大者即最内层。
|
|
377
|
+
"""
|
|
378
|
+
try:
|
|
379
|
+
ln = int(lineno)
|
|
380
|
+
except (TypeError, ValueError):
|
|
381
|
+
return None
|
|
382
|
+
hit = None
|
|
383
|
+
for it in items or []:
|
|
384
|
+
lo, hi = it.get("lineno"), it.get("end")
|
|
385
|
+
if lo is None or hi is None:
|
|
386
|
+
continue
|
|
387
|
+
try:
|
|
388
|
+
lo, hi = int(lo), int(hi)
|
|
389
|
+
except (TypeError, ValueError):
|
|
390
|
+
continue
|
|
391
|
+
if lo <= ln <= hi and (hit is None or lo > int(hit["lineno"])):
|
|
392
|
+
hit = it
|
|
393
|
+
return hit
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# 生效条件:以 root 为根 os.walk,patterns 假值回落 SUFFIX;files 达到 max_files 或 items 达到 max_items 时提前返回并置 stats["truncated"]/truncated_reason;fresh 非 None 且 fresh(rel, fp) 为真时跳过该文件读盘并计 skipped_unchanged;on_file 非 None 且 open/extract 成功后以 (rel, fp, got) 回调;名字在 SKIP_DIRS 的目录仅剪枝不记录,skip_dirs 经 codeindex.skip_matcher 命中的目录剪枝并记入 stats["skipped_dirs"];返回 (items, errors, stats)。
|
|
276
397
|
def index_dir(root, patterns=None, max_files=500, max_items=2000,
|
|
277
398
|
fresh=None, on_file=None, skip_dirs=None):
|
|
278
399
|
"""按大域(目录)遍历 md,产出 `(items, errors, stats)`。零 LLM。
|
|
@@ -350,4 +471,4 @@ def index_dir(root, patterns=None, max_files=500, max_items=2000,
|
|
|
350
471
|
return items, errors, stats
|
|
351
472
|
stats["files"] = files
|
|
352
473
|
stats["skipped_suffixes"] = sorted(s for s in seen_suffix if s and s not in pats)[:12]
|
|
353
|
-
return items, errors, stats
|
|
474
|
+
return items, errors, stats
|
package/md_cg/eval_common.py
CHANGED
|
@@ -58,6 +58,7 @@ PATHS = ("lexical", "bucket", "entity", "graph") # 引擎默认四路 RRF
|
|
|
58
58
|
PATHS_CAL = PATHS + ("semantic",) # 口径 B:显式启用条件结构路(负路由所在)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
# 生效条件:仅评测口径调用(主库不调用);必须双改 md_cg.mdcg 与 md_cg.mdcos 两份 from-import 的同名值(漏改其一即静默失效),置为 10**9;无返回值、不落盘;
|
|
61
62
|
def unlock_global_cap():
|
|
62
63
|
"""评测口径:解除 GLOBAL_CAP 截断(bench_membench patch_lexical_full 同法)。
|
|
63
64
|
|
|
@@ -74,6 +75,7 @@ def unlock_global_cap():
|
|
|
74
75
|
mo.GLOBAL_CAP = 10 ** 9
|
|
75
76
|
|
|
76
77
|
|
|
78
|
+
# 生效条件:仅评测口径调用(短条目语料下 jaccard 会退化);把 md_cg.mdcg.SCORE_MODE 置为 "jaccard"(mdcos 读同一份、无需双改);无返回值、不落盘;
|
|
77
79
|
def use_jaccard():
|
|
78
80
|
"""评测口径:词法打分切 jaccard(对称归一化,长度自惩罚)。
|
|
79
81
|
|
|
@@ -94,6 +96,7 @@ def use_jaccard():
|
|
|
94
96
|
m.SCORE_MODE = "jaccard"
|
|
95
97
|
|
|
96
98
|
|
|
99
|
+
# 生效条件:对 path 调用 open(path, encoding="utf-8") 后逐行读取,仅对 strip 后非空的 line yield json.loads(line);
|
|
97
100
|
def iter_jsonl(path):
|
|
98
101
|
with open(path, encoding="utf-8") as f:
|
|
99
102
|
for line in f:
|
|
@@ -102,6 +105,7 @@ def iter_jsonl(path):
|
|
|
102
105
|
yield json.loads(line)
|
|
103
106
|
|
|
104
107
|
|
|
108
|
+
# 生效条件:ds 等于 "lm" 时 path=LM_Q,否则 path=LC_Q;先由 iter_jsonl(path) 取全部 rows,再仅当 qtypes 为真值时保留 r["qtype"] 在 qtypes 中的行(qtypes 为 None/空串/空容器等假值则不过滤);
|
|
105
109
|
def load_questions(ds, qtypes=None):
|
|
106
110
|
"""ds: 'lm' | 'lc'。qtypes 过滤题型(None=全部)。"""
|
|
107
111
|
path = LM_Q if ds == "lm" else LC_Q
|
|
@@ -111,6 +115,7 @@ def load_questions(ds, qtypes=None):
|
|
|
111
115
|
return rows
|
|
112
116
|
|
|
113
117
|
|
|
118
|
+
# 生效条件:对 rows/n(seed 默认 7),若 not n 或 n >= len(rows) 则原样返回 rows,否则返回 random.Random(seed).sample(rows, n);
|
|
114
119
|
def sample_questions(rows, n, seed=7):
|
|
115
120
|
if not n or n >= len(rows):
|
|
116
121
|
return rows
|
|
@@ -119,12 +124,14 @@ def sample_questions(rows, n, seed=7):
|
|
|
119
124
|
|
|
120
125
|
# ------------------------------------------------------------------ 建库
|
|
121
126
|
|
|
127
|
+
# 生效条件:对 t,若 t.get("speaker") 为真值则返回 f"{t['speaker']}: {t['text']} [{t['date']}]",否则返回 f"{t['text']} [{t['date']}]";
|
|
122
128
|
def lm_turn_text(t):
|
|
123
129
|
"""LongMemEval turn → 入库文本(日期内联,词法路可召回时间词)。"""
|
|
124
130
|
return f"{t['speaker']}: {t['text']} [{t['date']}]" if t.get("speaker") \
|
|
125
131
|
else f"{t['text']} [{t['date']}]"
|
|
126
132
|
|
|
127
133
|
|
|
134
|
+
# 生效条件:对 r,若 r.get("title") 为真值则返回 f"{r['text']} [{r['title']}]",否则返回 r["text"];
|
|
128
135
|
def lc_turn_text(r):
|
|
129
136
|
"""LoCoMo turn → 入库文本(title 含 Data time 时间戳,内联保序)。"""
|
|
130
137
|
return f"{r['text']} [{r['title']}]" if r.get("title") else r["text"]
|
|
@@ -147,6 +154,7 @@ _STOP_HEADS = frozenset(
|
|
|
147
154
|
_NEG_MARKS = ("n't", " not ", " never ", "nobody", "nothing", "no one", "none of")
|
|
148
155
|
|
|
149
156
|
|
|
157
|
+
# 生效条件:对 text(limit 默认 4),按正则收集非 _STOP_HEADS 开头的大写词与 4 位年/时刻/日期数字并去重后,返回前 limit 项;
|
|
150
158
|
def _extract_entities(text, limit=4):
|
|
151
159
|
"""确定性实体近似:连续大写词(人名/地名)+ 日期/时间数字。只读 turn 正文。
|
|
152
160
|
纯数字编号("1.")不是实体——只保留年份/时刻/日期形态,否则列表编号会
|
|
@@ -162,6 +170,7 @@ def _extract_entities(text, limit=4):
|
|
|
162
170
|
return ents[:limit]
|
|
163
171
|
|
|
164
172
|
|
|
173
|
+
# 生效条件:对 title,若 title 为假值则返回空串,否则按 YYYY-MM-DD、英文星期日期、d/d/d 三个正则顺序取首个匹配串,无匹配返回空串;
|
|
165
174
|
def _date_tag(title):
|
|
166
175
|
"""从 title 抽规范化日期串做 tag。整段 title 入 tags 会污染实体路
|
|
167
176
|
(_path_entity 的 `query in t` 方向:长 tag 是误报源)。"""
|
|
@@ -174,6 +183,7 @@ def _date_tag(title):
|
|
|
174
183
|
return m.group(0) if m else ""
|
|
175
184
|
|
|
176
185
|
|
|
186
|
+
# 生效条件:r 为语料 turn 行(缺 text/role 等键时按空串处理);date_key/title_key 为 None 时对应日期取空串;返回 (CCG 五要素正文, tags, condition_space) 三元组,仅当原文出现强否定标记才生成不适用条件、否则显式写「(无)」;
|
|
177
187
|
def calibrate_turn(r, date_key=None, title_key=None):
|
|
178
188
|
"""turn 行 → (CCG 五要素正文, tags, condition_space)。
|
|
179
189
|
|
|
@@ -226,10 +236,12 @@ def calibrate_turn(r, date_key=None, title_key=None):
|
|
|
226
236
|
return body, tags, cond_space
|
|
227
237
|
|
|
228
238
|
|
|
239
|
+
# 生效条件:任意 t 与可选 ctx(默认 None)传入即原样转调 calibrate_turn(t, date_key="date", ctx=ctx),返回其结果;
|
|
229
240
|
def calibrate_lm_turn(t, ctx=None):
|
|
230
241
|
return calibrate_turn(t, date_key="date", ctx=ctx)
|
|
231
242
|
|
|
232
243
|
|
|
244
|
+
# 生效条件:任意 r 与可选 ctx(默认 None)传入即原样转调 calibrate_turn(r, title_key="title", ctx=ctx),返回其结果;
|
|
233
245
|
def calibrate_lc_turn(r, ctx=None):
|
|
234
246
|
return calibrate_turn(r, title_key="title", ctx=ctx)
|
|
235
247
|
|
|
@@ -239,6 +251,7 @@ def calibrate_lc_turn(r, ctx=None):
|
|
|
239
251
|
# 指代消解,把语料加工成可沿链行走的形态。以下三步全部是**确定性规则近似**
|
|
240
252
|
# (纯标准库、盲于查询集、随报告公开词表),不引入任何模型依赖。
|
|
241
253
|
|
|
254
|
+
# 生效条件:对 corpus_path(min_df 默认 3),逐行取 text=str(r.get("text") or "")、spk=str(r.get("speaker") or "").strip(),以 {spk}(spk 真值)或空集合并入正文大写词首词(排除 _STOP_HEADS)更新 df 计数,最后返回计数 >= min_df 的词集合;
|
|
242
255
|
def build_canon(corpus_path, min_df=3):
|
|
243
256
|
"""离线实体规范化:全语料扫一遍,聚合大写词/人名的文档频次(df)。
|
|
244
257
|
df≥min_df 的是 canonical 实体(会话成员 + 高频专名)——单 turn 局部抽取
|
|
@@ -268,11 +281,13 @@ _INTENT_HEADS = ("plan", "going to", "thinking of", "thinking about", "want to",
|
|
|
268
281
|
"prefer", "favorite", "love", "hate", "enjoy")
|
|
269
282
|
|
|
270
283
|
|
|
284
|
+
# 生效条件:对 text 小写并前后加空格得到 low,返回 _INTENT_HEADS 中满足 f" {h}" 出现在 low 的项;
|
|
271
285
|
def _intent_of(text):
|
|
272
286
|
low = f" {text.lower()} "
|
|
273
287
|
return [h for h in _INTENT_HEADS if f" {h}" in low]
|
|
274
288
|
|
|
275
289
|
|
|
290
|
+
# 生效条件:r 为语料 turn 行;ctx 为写入时全库视图(canon/intents/last_canon),ctx 为 None 时按空视图处理并退化为 v2 行为;返回 (CCG 五要素正文, tags, condition_space) 三元组;
|
|
276
291
|
def calibrate_turn(r, date_key=None, title_key=None, ctx=None):
|
|
277
292
|
"""turn 行 → (CCG 五要素正文, tags, condition_space)。
|
|
278
293
|
|
|
@@ -336,6 +351,7 @@ def calibrate_turn(r, date_key=None, title_key=None, ctx=None):
|
|
|
336
351
|
return body, tags, cond_space
|
|
337
352
|
|
|
338
353
|
|
|
354
|
+
# 生效条件:rebuild=True 且 os.path.isdir(root) 时先 rmtree(root);cg_cls 为假值(如 None)时回落 MdCGOS(root, autoflush=500);语料行数 n_rows ≤ cg.index["nodes"] 现有节点数(n_rows=0 的空语料也满足)时直接复用返回 cg;否则逐行写入——calib_of 为真走标定口径 B(先 build_canon,calib_of(r, {"canon": canon, "last_canon": last_canon}) 取 body/tags/condition_space),calib_of 为假(含 None)走 legacy 口径 A(用 text_of(r)),id 已在 cg.index["nodes"] 的行跳过,verbose 为真时每 20000 行打印进度,循环后 cg.flush() 再返回 cg;
|
|
339
355
|
def build_eval_cg(cg_cls, root, corpus_path, text_of, src_tag, verbose=True,
|
|
340
356
|
calib_of=None, rebuild=False):
|
|
341
357
|
"""幂等建库:节点数已达语料行数则直接复用。返回 cg(不 flush 句柄)。
|
|
@@ -389,11 +405,13 @@ def build_eval_cg(cg_cls, root, corpus_path, text_of, src_tag, verbose=True,
|
|
|
389
405
|
return cg
|
|
390
406
|
|
|
391
407
|
|
|
408
|
+
# 生效条件:对传入 cg,用闭包 cache(键为 entry["path"])包装其原 cg._read 并赋回 cg._read,返回该 cache;仅当 p = entry["path"] 不在 cache 时调用 orig(entry) 并缓存其(含假值)结果,p 已在 cache 中时直接返回缓存值;
|
|
392
409
|
def install_read_cache(cg):
|
|
393
410
|
"""评测只读:节点文件读进内存,避免逐次检索重复磁盘 I/O。"""
|
|
394
411
|
cache = {}
|
|
395
412
|
orig = cg._read
|
|
396
413
|
|
|
414
|
+
# 生效条件:entry["path"] 未在闭包 cache 中时调用 orig(entry) 存入并返回,已在 cache 中则直接返回缓存值(cache 与原 _read 由外层 install_read_cache 提供);
|
|
397
415
|
def _cached(entry):
|
|
398
416
|
p = entry["path"]
|
|
399
417
|
if p not in cache:
|
|
@@ -406,6 +424,7 @@ def install_read_cache(cg):
|
|
|
406
424
|
|
|
407
425
|
# ------------------------------------------------------------------ 检索与指标
|
|
408
426
|
|
|
427
|
+
# 生效条件:对 cg/query(k 默认 5、paths 默认 PATHS、judge 默认 False),若 fusion 为真值则加入 kw["fusion"],若 path_weights 为真值则加入 kw["path_weights"],若 context is not None(含空 dict/空串)则加入 kw["context"],再调用 cg.search_rrf(...);
|
|
409
428
|
def run_query(cg, query, k=5, paths=PATHS, judge=False, fusion=None,
|
|
410
429
|
path_weights=None, context=None):
|
|
411
430
|
"""单查询,返回 (结果四元组列表, meta)。
|
|
@@ -426,6 +445,7 @@ def run_query(cg, query, k=5, paths=PATHS, judge=False, fusion=None,
|
|
|
426
445
|
return cg.search_rrf(query, k=k, paths=paths, judge=judge, record=False, **kw)
|
|
427
446
|
|
|
428
447
|
|
|
448
|
+
# 生效条件:当 evidence 为真值且 res 中某 r 的 r[0]["id"] 属于 evidence 时返回首个 1-based 排名,否则(evidence 为假值或未命中)返回 0;
|
|
429
449
|
def first_evidence_rank(res, evidence):
|
|
430
450
|
"""首个证据 turn 的排名(1-based;未命中 0)。"""
|
|
431
451
|
if not evidence:
|
|
@@ -436,6 +456,7 @@ def first_evidence_rank(res, evidence):
|
|
|
436
456
|
return 0
|
|
437
457
|
|
|
438
458
|
|
|
459
|
+
# 生效条件:对 questions 中每个 it,若 context_of 为真值则 ctx=context_of(it) 否则 ctx=None;以 k/paths/judge/fusion/path_weights 调用 run_query,按 evidence_turns 算 first_evidence_rank,收集 qid/qtype/rank/top1_score/n_res 行,返回 rows;
|
|
439
460
|
def evaluate_group(cg, questions, k=5, paths=PATHS, judge=False, verbose=True,
|
|
440
461
|
fusion=None, path_weights=None, context_of=None):
|
|
441
462
|
"""一组题 → per-question 明细行。负例组传 judge=False(拒答看分数线)。
|
|
@@ -462,6 +483,7 @@ def evaluate_group(cg, questions, k=5, paths=PATHS, judge=False, verbose=True,
|
|
|
462
483
|
return rows
|
|
463
484
|
|
|
464
485
|
|
|
486
|
+
# 生效条件:当 rows 为真值时按 k(默认 5)计算 hit@1、hit@k、MRR、score_p10/p50 及 by_qtype 指标,否则返回 {"n": 0};
|
|
465
487
|
def summarize(rows, k=5):
|
|
466
488
|
"""hit@1 / hit@K / MRR + 分题型。score_p10 供拒答线校准。"""
|
|
467
489
|
if not rows:
|
|
@@ -492,6 +514,7 @@ def summarize(rows, k=5):
|
|
|
492
514
|
return out
|
|
493
515
|
|
|
494
516
|
|
|
517
|
+
# 生效条件:当 rows 非空时按 r["top1_score"] < line 统计拒答数并返回拒绝率,否则返回 {"n": 0};
|
|
495
518
|
def refusal_metrics(rows, line):
|
|
496
519
|
"""负例组:Top-1 分 < line 记为拒答。返回拒答率与明细统计。"""
|
|
497
520
|
n = len(rows)
|
|
@@ -502,6 +525,7 @@ def refusal_metrics(rows, line):
|
|
|
502
525
|
"refused": ref, "line": line}
|
|
503
526
|
|
|
504
527
|
|
|
528
|
+
# 生效条件:当 rows 非空时返回 r["top1_score"] < line 的行占比,否则返回 0.0;
|
|
505
529
|
def false_refusal_rate(rows, line):
|
|
506
530
|
"""正例误杀(被错误拒答的正例比例):正例 Top-1 分 < line。"""
|
|
507
531
|
n = len(rows)
|
|
@@ -510,6 +534,7 @@ def false_refusal_rate(rows, line):
|
|
|
510
534
|
return sum(1 for r in rows if r["top1_score"] < line) / n
|
|
511
535
|
|
|
512
536
|
|
|
537
|
+
# 生效条件:当 pos_rows 中存在 rank==1 的行时,返回这些行 top1_score 升序后 max(0, len//10) 索引处的值,否则返回 0.0;
|
|
513
538
|
def calibrate_line(pos_rows):
|
|
514
539
|
"""拒答线 = 正例 hit@1 题 Top-1 分的 10 分位。"""
|
|
515
540
|
scores = sorted(r["top1_score"] for r in pos_rows if r["rank"] == 1)
|
|
@@ -518,6 +543,7 @@ def calibrate_line(pos_rows):
|
|
|
518
543
|
return scores[max(0, len(scores) // 10)]
|
|
519
544
|
|
|
520
545
|
|
|
546
|
+
# 生效条件:当 name/payload 给出时,确保 RESULTS 目录(os.makedirs(RESULTS, exist_ok=True)),将 payload 以 ensure_ascii=False、indent=2 写入 RESULTS/name 并返回 path;
|
|
521
547
|
def save_result(name, payload):
|
|
522
548
|
"""评测结果 JSON 落盘(报告数据源)。"""
|
|
523
549
|
os.makedirs(RESULTS, exist_ok=True)
|
|
@@ -528,6 +554,7 @@ def save_result(name, payload):
|
|
|
528
554
|
return path
|
|
529
555
|
|
|
530
556
|
|
|
557
|
+
# 生效条件:对 groups(k 默认 5),若某组 s 的 s.get("n") 为假(缺 n 或 n=0)则打印 0 行并跳过,否则按 s["hit@1"]、s[f"hit@{k}"]、s["mrr"] 打印并遍历 s.get("by_qtype", {}) 输出子行;
|
|
531
558
|
def print_table(title, groups, k=5):
|
|
532
559
|
"""groups: {组名: summarize 输出}。"""
|
|
533
560
|
print(f"\n== {title}(证据命中口径,k={k})==")
|
|
@@ -544,5 +571,6 @@ def print_table(title, groups, k=5):
|
|
|
544
571
|
f"{st[f'hit@{k}']:>9.1%}{st['mrr']:>8.3f}")
|
|
545
572
|
|
|
546
573
|
|
|
574
|
+
# 生效条件:对任意 x,返回 f"{x * 100:.1f}%" 的百分比字符串;
|
|
547
575
|
def pct(x):
|
|
548
|
-
return f"{x * 100:.1f}%"
|
|
576
|
+
return f"{x * 100:.1f}%"
|
package/md_cg/evidence.py
CHANGED
|
@@ -62,22 +62,27 @@ class EvidenceError(Exception):
|
|
|
62
62
|
# 路径与身份
|
|
63
63
|
# --------------------------------------------------------------------------
|
|
64
64
|
|
|
65
|
+
# 生效条件:当传入 path 为真值时返回 path;否则若环境变量 SWARM_DIR_ENV 为真值则返回其值;否则返回模块级常量 DEFAULT_SWARM_DIR。
|
|
65
66
|
def swarm_dir(path: str = None) -> str:
|
|
66
67
|
return path or os.environ.get(SWARM_DIR_ENV) or DEFAULT_SWARM_DIR
|
|
67
68
|
|
|
68
69
|
|
|
70
|
+
# 生效条件:当传入 swarm 参数(假值回落到 swarm_dir 的默认逻辑)时,返回 os.path.join(swarm_dir(swarm), "peers")。
|
|
69
71
|
def peers_dir(swarm: str = None) -> str:
|
|
70
72
|
return os.path.join(swarm_dir(swarm), "peers")
|
|
71
73
|
|
|
72
74
|
|
|
75
|
+
# 生效条件:当传入 swarm 参数(假值回落到 swarm_dir 的默认逻辑)时,返回 os.path.join(swarm_dir(swarm), "inbox")。
|
|
73
76
|
def inbox_dir(swarm: str = None) -> str:
|
|
74
77
|
return os.path.join(swarm_dir(swarm), "inbox")
|
|
75
78
|
|
|
76
79
|
|
|
80
|
+
# 生效条件:当传入 root 为真值时返回 root;否则返回环境变量 ROOT_ENV 的值(可能为 None)。
|
|
77
81
|
def _root_of(root: str = None) -> str:
|
|
78
82
|
return root or os.environ.get(ROOT_ENV)
|
|
79
83
|
|
|
80
84
|
|
|
85
|
+
# 生效条件:当 explicit 或环境变量 NODE_ID_ENV strip 后非空时返回该值;否则若 _root_of(root) 返回真值,返回基于规范绝对路径的 'node-<12hex>';否则抛出 EvidenceError。
|
|
81
86
|
def node_id(root: str = None, explicit: str = None) -> str:
|
|
82
87
|
"""本节点身份。
|
|
83
88
|
|
|
@@ -95,6 +100,7 @@ def node_id(root: str = None, explicit: str = None) -> str:
|
|
|
95
100
|
return "node-" + hashlib.sha256(key.encode("utf-8")).hexdigest()[:12]
|
|
96
101
|
|
|
97
102
|
|
|
103
|
+
# 生效条件:包内 md_cg.theory 可导入且 _th.check() 成功时返回 {version, accepted, theory_ok};任何异常(版本层缺失或校验失败)一律吞掉返回 {},不阻断证据层主流程;
|
|
98
104
|
def _theory_state() -> dict:
|
|
99
105
|
try:
|
|
100
106
|
from . import theory as _th
|
|
@@ -106,11 +112,13 @@ def _theory_state() -> dict:
|
|
|
106
112
|
return {}
|
|
107
113
|
|
|
108
114
|
|
|
115
|
+
# 生效条件:当传入 obj 时,返回其 json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(',',':')).encode("utf-8") 字节。
|
|
109
116
|
def _canon(obj) -> bytes:
|
|
110
117
|
return json.dumps(obj, ensure_ascii=False, sort_keys=True,
|
|
111
118
|
separators=(",", ":")).encode("utf-8")
|
|
112
119
|
|
|
113
120
|
|
|
121
|
+
# 生效条件:当传入 s 时,返回 str(s) 中每个字符若为字母数字或 '_' '-' 则保留,否则替换为 '_' 的字符串。
|
|
114
122
|
def _slug(s) -> str:
|
|
115
123
|
return "".join(ch if (ch.isalnum() or ch in "_-") else "_" for ch in str(s))
|
|
116
124
|
|
|
@@ -119,6 +127,7 @@ def _slug(s) -> str:
|
|
|
119
127
|
# 节点名片(发现)
|
|
120
128
|
# --------------------------------------------------------------------------
|
|
121
129
|
|
|
130
|
+
# 生效条件:当传入 root、subsystem(默认常量 SUBSYSTEM)、signers_file 时,基于 _signer.policy_for 和 _signer.get_signer 返回名片字典,其中 root 为 os.path.abspath(_root_of(root)) 若 _root_of(root) 真值否则 None。
|
|
122
131
|
def card(root: str = None, *, subsystem: str = SUBSYSTEM,
|
|
123
132
|
signers_file: str = None) -> dict:
|
|
124
133
|
"""本节点名片:身份 + 版本 + 签名器公开信息(**不含密钥**)。"""
|
|
@@ -139,6 +148,7 @@ def card(root: str = None, *, subsystem: str = SUBSYSTEM,
|
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
|
|
151
|
+
# 生效条件:当传入 root、swarm、subsystem(默认常量 SUBSYSTEM)、signers_file 时,调用 card 得到 c,将 c 以 JSON 写入 peers_dir(swarm)/<c["node_id"]>.json,返回 {"ok": True, "file": p, "card": c}。
|
|
142
152
|
def publish_card(root: str = None, *, swarm: str = None,
|
|
143
153
|
subsystem: str = SUBSYSTEM, signers_file: str = None) -> dict:
|
|
144
154
|
"""把本节点名片写入共享目录 `peers/<node_id>.json`(0600)。"""
|
|
@@ -148,6 +158,7 @@ def publish_card(root: str = None, *, swarm: str = None,
|
|
|
148
158
|
return {"ok": True, "file": p, "card": c}
|
|
149
159
|
|
|
150
160
|
|
|
161
|
+
# 生效条件:当 peers_dir(swarm) 是目录时,遍历其中 .json 文件,加载为 dict 且 kind 等于常量 CARD_KIND 的名片,若 exclude_self 为真则排除 node_id 等于 node_id(root) 的名片(node_id(root) 抛 EvidenceError 时 self_id 为 None 不排除),返回按文件名排序的列表及计数;目录不存在则返回空列表。
|
|
151
162
|
def peers(swarm: str = None, *, exclude_self: bool = True,
|
|
152
163
|
root: str = None) -> dict:
|
|
153
164
|
"""发现共享目录里的对端名片(按 node_id 排序)。"""
|
|
@@ -183,6 +194,7 @@ _PACK_FIELDS = ("schema", "kind", "pack_id", "from_node", "from_root",
|
|
|
183
194
|
"theory", "created_at", "subjects", "items")
|
|
184
195
|
|
|
185
196
|
|
|
197
|
+
# 生效条件:当传入 pack 字典时,返回仅含模块级常量 _PACK_FIELDS 中字段(缺失字段取 pack.get(k) 得到 None)的规范化 JSON 字节。
|
|
186
198
|
def _pack_payload(pack: dict) -> bytes:
|
|
187
199
|
"""签名载荷:固定字段 + 排序序列化,同参数必同字节。"""
|
|
188
200
|
body = {}
|
|
@@ -191,6 +203,7 @@ def _pack_payload(pack: dict) -> bytes:
|
|
|
191
203
|
return _canon(body)
|
|
192
204
|
|
|
193
205
|
|
|
206
|
+
# 生效条件:当传入 cg、subjects、since 时,遍历 cg.index["nodes"] 中按 node_id 排序的节点,仅收集标签含常量 TAG_OBS、不含 TAG_ANCHOR/TAG_TRAIT、不含 TAG_XNODE、能提取 subject: 标签且该 subject 在 subjects(subjects 为真时)或不限制(subjects 假值时)、且若 since 为真且 ts 为真则要求 float(ts) >= float(since) 的节点,返回含 node_id、subject、text(截断 MAX_TEXT)、kind、role、layer、过滤后的 tags、importance、verification_basis、ts 的列表。
|
|
194
207
|
def _collect(cg, subjects=None, since=None) -> list:
|
|
195
208
|
"""收集本根**本地原始**观测证据(排除档案节点与已导入的跨节点证据)。"""
|
|
196
209
|
want = set(subjects or [])
|
|
@@ -237,6 +250,7 @@ def _collect(cg, subjects=None, since=None) -> list:
|
|
|
237
250
|
return items
|
|
238
251
|
|
|
239
252
|
|
|
253
|
+
# 生效条件:当传入 cg、subjects、since、swarm、subsystem(默认 SUBSYSTEM)、signers_file、require_signature(默认 True)时,收集证据并截断至常量 MAX_ITEMS,用 _signer.sign_for 以常量 SIGN_ACTION 签名,若 require_signature 为真且签名未成功则抛出 EvidenceError,否则返回含 pack、count、truncated、signed 的字典。
|
|
240
254
|
def export_pack(cg, *, subjects=None, since=None, swarm: str = None,
|
|
241
255
|
subsystem: str = SUBSYSTEM, signers_file: str = None,
|
|
242
256
|
require_signature: bool = True) -> dict:
|
|
@@ -275,6 +289,7 @@ def export_pack(cg, *, subjects=None, since=None, swarm: str = None,
|
|
|
275
289
|
"truncated": truncated, "signed": pack["sig"]["signed"]}
|
|
276
290
|
|
|
277
291
|
|
|
292
|
+
# 生效条件:当传入 pack、path、swarm 时,若 path 为假值则用 os.path.join(inbox_dir(swarm), f"{_slug(pack.get('from_node'))}__{pack.get('pack_id')}.json") 生成路径,将 pack 以 JSON 写入该路径并返回 {"ok": True, "file": path}。
|
|
278
293
|
def write_pack(pack: dict, path: str = None, swarm: str = None) -> dict:
|
|
279
294
|
"""把证据包落盘到收件箱(或指定路径)。"""
|
|
280
295
|
if not path:
|
|
@@ -288,6 +303,7 @@ def write_pack(pack: dict, path: str = None, swarm: str = None) -> dict:
|
|
|
288
303
|
# 证据包:导入
|
|
289
304
|
# --------------------------------------------------------------------------
|
|
290
305
|
|
|
306
|
+
# 生效条件:当 src 是 dict 时直接返回 src;否则以 src 为路径打开 JSON 文件并返回 json.load(f)。
|
|
291
307
|
def _read_pack(src) -> dict:
|
|
292
308
|
if isinstance(src, dict):
|
|
293
309
|
return src
|
|
@@ -295,6 +311,7 @@ def _read_pack(src) -> dict:
|
|
|
295
311
|
return json.load(f)
|
|
296
312
|
|
|
297
313
|
|
|
314
|
+
# 生效条件:当 pack 为 dict 且 kind 等于常量 KIND、int(pack.get("schema") or 0) 不大于常量 SCHEMA、from_node strip 后非空、items 为 list 时返回空字符串;否则按序返回对应错误字符串(非 dict、kind 不符、schema 过新、缺 from_node、缺 items)。
|
|
298
315
|
def _validate(pack: dict) -> str:
|
|
299
316
|
if not isinstance(pack, dict):
|
|
300
317
|
return "证据包不是 JSON 对象"
|
|
@@ -309,10 +326,12 @@ def _validate(pack: dict) -> str:
|
|
|
309
326
|
return ""
|
|
310
327
|
|
|
311
328
|
|
|
329
|
+
# 生效条件:当传入 from_node 和 orig 时,返回 f"xnode_{_slug(from_node)}_{_slug(orig)}"。
|
|
312
330
|
def _xnode_id(from_node: str, orig: str) -> str:
|
|
313
331
|
return f"xnode_{_slug(from_node)}_{_slug(orig)}"
|
|
314
332
|
|
|
315
333
|
|
|
334
|
+
# 生效条件:当传入 pack、reason、swarm、**kw 时,构建含 at、reason、from_node、pack_id 及 kw 中非 None 项的拒绝记录,尝试追加到 inbox_dir(swarm)/_rejected.jsonl(OSError 忽略),返回 {"ok": False, "rejected": True, "reason": reason, "from_node": ...} 并合并 kw 中非 None 项。
|
|
316
335
|
def _note_reject(pack: dict, reason: str, swarm: str = None, **kw) -> dict:
|
|
317
336
|
rec = {"at": time.time(), "reason": reason,
|
|
318
337
|
"from_node": (pack or {}).get("from_node"),
|
|
@@ -330,6 +349,7 @@ def _note_reject(pack: dict, reason: str, swarm: str = None, **kw) -> dict:
|
|
|
330
349
|
return out
|
|
331
350
|
|
|
332
351
|
|
|
352
|
+
# 生效条件:当传入 cg、src、subsystem(默认 SUBSYSTEM)、swarm、signers_file、archive(默认 True)时,依次读取 src 为 pack、_validate 校验、来源 node_id 不等于本节点、_signer.verify_for 验签成功、且有签名或 signer 等于 "null" 方可通过;通过则对 pack["items"] 前常量 MAX_ITEMS 项中 subject/text 非空者以 _identity.observe 写入(带常量 TAG_XNODE 和 SOURCE_PREFIX 标签,importance 默认 0.5,verification_basis 默认 "data"),统计 written/skipped,若 archive 为真则写包到收件箱,返回含 ok、from_node、pack_id、imported、skipped、truncated、node_ids、archived 的字典;任一拒绝分支调用 _note_reject 返回拒绝结果。
|
|
333
353
|
def import_pack(cg, src, *, subsystem: str = SUBSYSTEM, swarm: str = None,
|
|
334
354
|
signers_file: str = None, archive: bool = True) -> dict:
|
|
335
355
|
"""导入对端证据包:验签 → 落本根(带 `source:<node>` 标签)。
|
|
@@ -404,6 +424,7 @@ def import_pack(cg, src, *, subsystem: str = SUBSYSTEM, swarm: str = None,
|
|
|
404
424
|
# 查询
|
|
405
425
|
# --------------------------------------------------------------------------
|
|
406
426
|
|
|
427
|
+
# 生效条件:当传入 cg、subject、source、limit(默认 100)时,收集标签含常量 TAG_XNODE 的节点,若 subject 为真则仅保留 subject 标签等于 subject 的节点,若 source 为真则仅保留 source 标签等于 source 的节点,返回最后 int(limit or 100) 条记录(limit 为假值如 0 时取 100)的 count 与列表。
|
|
407
428
|
def evidence(cg, *, subject: str = None, source: str = None,
|
|
408
429
|
limit: int = 100) -> dict:
|
|
409
430
|
"""列本根已导入的跨节点证据(按来源/主体过滤)。"""
|
|
@@ -432,6 +453,7 @@ def evidence(cg, *, subject: str = None, source: str = None,
|
|
|
432
453
|
return {"count": len(out), "evidence": out}
|
|
433
454
|
|
|
434
455
|
|
|
456
|
+
# 生效条件:无参数调用时返回包含模块级常量 SUBSYSTEM、SIGN_ACTION、SWARM_DIR_ENV、NODE_ID_ENV、ROOT_ENV、MAX_ITEMS、MAX_TEXT 及 swarm_dir() 等字段的自描述字典。
|
|
435
457
|
def catalog() -> dict:
|
|
436
458
|
return {
|
|
437
459
|
"layer": "跨节点证据存储(蜂群互联 v0.3)",
|
|
@@ -453,6 +475,7 @@ def catalog() -> dict:
|
|
|
453
475
|
# 工具
|
|
454
476
|
# --------------------------------------------------------------------------
|
|
455
477
|
|
|
478
|
+
# 生效条件:当传入 path 和 obj 时,在 path 所在目录(若 dirname 为空则 ".")创建目录,将 obj 以 JSON 写入 path+".tmp"(ensure_ascii=False, indent=1, sort_keys=True),尝试 chmod 0600,替换到 path,返回 path。
|
|
456
479
|
def _write_json(path: str, obj) -> str:
|
|
457
480
|
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
458
481
|
tmp = path + ".tmp"
|
|
@@ -466,6 +489,7 @@ def _write_json(path: str, obj) -> str:
|
|
|
466
489
|
return path
|
|
467
490
|
|
|
468
491
|
|
|
492
|
+
# 生效条件:当传入 root 且 _root_of(root) 返回真值时,以 designer 角色 Principal(tenant 取 os.environ.get("MDCG_TENANT", "default") 的实际值,actor 取 os.environ.get("MDCG_ACTOR", "swarm-cli") 的实际值)打开 MdCGSecure;否则抛出 EvidenceError。
|
|
469
493
|
def _open_cg(root: str = None):
|
|
470
494
|
"""CLI 用:以 designer 身份打开根(令牌路径仍推荐走 MCP)。"""
|
|
471
495
|
from .mdcos import MdCGSecure
|
|
@@ -483,10 +507,12 @@ def _open_cg(root: str = None):
|
|
|
483
507
|
return MdCGSecure(r, principal=p)
|
|
484
508
|
|
|
485
509
|
|
|
510
|
+
# 生效条件:当传入 obj 时,打印 json.dumps(obj, ensure_ascii=False, indent=1, default=str)。
|
|
486
511
|
def _print(obj):
|
|
487
512
|
print(json.dumps(obj, ensure_ascii=False, indent=1, default=str))
|
|
488
513
|
|
|
489
514
|
|
|
515
|
+
# 生效条件:argv 为 None 时取 sys.argv[1:];经 argparse 解析后必填子命令(catalog/card/export/import 等)之一,参数缺失或非法由 argparse 直接退出;返回进程退出码;
|
|
490
516
|
def main(argv=None):
|
|
491
517
|
ap = argparse.ArgumentParser(
|
|
492
518
|
prog="python -m md_cg.evidence",
|
|
@@ -552,4 +578,4 @@ def main(argv=None):
|
|
|
552
578
|
|
|
553
579
|
|
|
554
580
|
if __name__ == "__main__":
|
|
555
|
-
sys.exit(main())
|
|
581
|
+
sys.exit(main())
|