@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/nodefile.py
CHANGED
|
@@ -39,6 +39,7 @@ CCG 要素的语义对应(白箱第 1 篇第 17 章):
|
|
|
39
39
|
"""
|
|
40
40
|
import hashlib
|
|
41
41
|
import json
|
|
42
|
+
import re
|
|
42
43
|
import time
|
|
43
44
|
|
|
44
45
|
_DELIM = "---"
|
|
@@ -54,7 +55,7 @@ CCG_REQUIRED = CCG_MARKS
|
|
|
54
55
|
# 六要素不是注释/描述,而是**接口契约**——每行在契约里有一个确定角色:
|
|
55
56
|
# 功能名 = 签名 signature(可执行入口符号)
|
|
56
57
|
# 生效条件 = 前置条件 precondition(由 condition_space 四槽合成,唯一入口)
|
|
57
|
-
# 子功能 = 依赖 dependency
|
|
58
|
+
# 子功能 = 内部子功能分解(自述)**兼** 依赖 dependency(跨节点)
|
|
58
59
|
# 执行 = 调用 invocation
|
|
59
60
|
# 验证方式 = 后置条件 postcondition + test
|
|
60
61
|
# 不适用条件 = 拒绝域 rejection_domain
|
|
@@ -62,14 +63,51 @@ CCG_REQUIRED = CCG_MARKS
|
|
|
62
63
|
# 的既有硬约束同源:缺前置条件的接口无法判定可否调用,故缺参即编译错误。
|
|
63
64
|
# 本常量是术语的**唯一真源**;其余模块(如 ccgc.CONTRACT_ROLES)一律引用本处,
|
|
64
65
|
# 禁止各自再定义一份,防「术语双写法」漂移。
|
|
66
|
+
#
|
|
67
|
+
# 「子功能」的**双语义**(收窄裁定 b · 使用者 2026-09-19)——这是本槽位的既成事实,
|
|
68
|
+
# 不能只看本常量的 "dependency" 四字:理论真源(智能论 3.4 §6章.3 六要素表)把该行
|
|
69
|
+
# 定义为「内部子功能分解(①②③)」= **对自身的描述**;本常量记其契约角色为
|
|
70
|
+
# dependency = **寄生于他者**。同一槽位承载两种语义,**靠形态区分**(不是靠猜):
|
|
71
|
+
# · 自然语言描述本单元内部构成 → **自述**,不构成依赖声明,无需 depends_on;
|
|
72
|
+
# · `@<节点 id>` 显式引用其它单元 → **跨节点依赖声明**,必须落 depends_on。
|
|
73
|
+
# 判据真源 = `declares_dependency`(本模块);闸门 = `ccgc._check_deps` /
|
|
74
|
+
# `writepipe._gate_deps`(两闸共用同一判据,收窄一处即两闸同步)。
|
|
75
|
+
# **不得再退回「值非哨兵即声明」**:CCG 编译产物六要素必含本行,退回即让每个节点
|
|
76
|
+
# 落库后被 E050 永久锁死(test_ccgc V16f 实证),且与理论真源的自述口径直接冲突。
|
|
65
77
|
CCG_CONTRACT_ROLES = {
|
|
66
78
|
"功能名": "签名 signature(可执行入口符号)",
|
|
67
79
|
"生效条件": "前置条件 precondition",
|
|
68
|
-
"子功能": "依赖 dependency"
|
|
80
|
+
"子功能": "内部子功能分解 self_decomposition / 依赖 dependency"
|
|
81
|
+
"(双语义,以 @<节点 id> 显式引用为界)",
|
|
69
82
|
"执行": "调用 invocation",
|
|
70
83
|
"验证方式": "后置条件 postcondition + test",
|
|
71
84
|
"不适用条件": "拒绝域 rejection_domain",
|
|
72
85
|
}
|
|
86
|
+
|
|
87
|
+
# ---- 裁定 C(Phase 0 契约裁决 · 2026-09-17):索引元条件与功能生效条件字段分家 ----
|
|
88
|
+
# 病根:codeindex.render 曾把**机械推导的索引元条件**(本地源码仓 / 全时窗 /
|
|
89
|
+
# AST 工具 / 文件可读)写成「生效条件」行,与「功能生效条件」(人工声明:
|
|
90
|
+
# 这段代码在何种输入/状态下正确)**共用同一字段名**。检索侧
|
|
91
|
+
# mdcos._ccg_field 取首个匹配 → 源码里人工写的生效条件行被永久压制 →
|
|
92
|
+
# 「补了注释」与「没补」在检索结果上不可区分。
|
|
93
|
+
# 处置(沿用 backfill.py 已裁定先例「观测位置 ≠ 生效条件」):
|
|
94
|
+
# · 「生效条件」只承载**功能前置条件**,来源仅限人工/源码声明,
|
|
95
|
+
# **不由 render 合成**(合成即冒充);
|
|
96
|
+
# · 索引元条件改由 INDEX_META_MARK 承载,与 CCG_MARKS **零重名**
|
|
97
|
+
# (机械可判,见 is_ccg_mark)。
|
|
98
|
+
# 权威契约:docs/mdcg/代码评审与条件化注释_契约_v0.1.md
|
|
99
|
+
INDEX_META_MARK = "索引元条件"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# 生效条件:name 无论为何值先经 str().strip() 归一(name 为 None 时即字符串 "None"),归一结果落在模块常量 CCG_MARKS 中即返回 True,否则 False。
|
|
103
|
+
def is_ccg_mark(name: str) -> bool:
|
|
104
|
+
"""该字段名是否为 CCG 六要素之一(合成区零重名契约的机械判据)。
|
|
105
|
+
|
|
106
|
+
给「合成区字段名 ∩ CCG_MARKS = ∅」提供**可执行**判据,而不是靠注释约定:
|
|
107
|
+
test_codeindex 逐行核合成区用到的字段名。
|
|
108
|
+
"""
|
|
109
|
+
return str(name).strip() in CCG_MARKS
|
|
110
|
+
|
|
73
111
|
# 外部验证基底的可取值(frontmatter.verification_basis)
|
|
74
112
|
#
|
|
75
113
|
# 分两档(口径:文科宽松、理科严格):
|
|
@@ -85,7 +123,156 @@ REPRODUCIBLE_BASIS = ("compiler", "test", "measurement", "formal_proof", "data")
|
|
|
85
123
|
#: 来源一致性档:文科断言可用(来源表述一致即可)
|
|
86
124
|
CONSISTENCY_BASIS = ("textbook", "public_kb")
|
|
87
125
|
|
|
126
|
+
# ---- 裁定 D(2026-09-19):可验证记忆单元的**字段名真源** --------------------------
|
|
127
|
+
# 「子功能」的契约角色是**依赖 dependency**(见 CCG_CONTRACT_ROLES),其落字段即
|
|
128
|
+
# `depends_on`——名字与角色对齐(不叫 sub_features,避免同一概念两种写法)。
|
|
129
|
+
# 依赖一旦失效,下游的「还成不成立」就变了,故它与验证态、时间轴同属一层。
|
|
130
|
+
DEPENDS_ON_FIELD = "depends_on"
|
|
131
|
+
|
|
132
|
+
#: 双时间轴:何时开始成立 / 何时不再成立(判定未生效·生效中·已过期)。
|
|
133
|
+
#: `valid_until` 既有(已被 scrub 的过期消费面使用);`valid_from` 本轮补齐。
|
|
134
|
+
VALID_FROM_FIELD = "valid_from"
|
|
135
|
+
VALID_UNTIL_FIELD = "valid_until"
|
|
136
|
+
|
|
137
|
+
#: 双时间轴的**规范键**(2026-09-19 阶段一):新写入落此;历史键 `valid_from` /
|
|
138
|
+
#: `valid_until` 保留为**读取侧回落别名**(存量不迁移、零破坏)。本处只登记字段名;
|
|
139
|
+
#: **行为真源**(取值优先级 `effective_* > valid_* > 其余别名` 与三态判定)见
|
|
140
|
+
#: md_cg/trust.py 的 FROM_ALIASES / UNTIL_ALIASES / validity()。
|
|
141
|
+
EFFECTIVE_FROM_FIELD = "effective_from"
|
|
142
|
+
EFFECTIVE_UNTIL_FIELD = "effective_until"
|
|
143
|
+
|
|
144
|
+
#: 信念时间:体系**何时确认此条**——取代(supersede)/ 审核的排序锚。
|
|
145
|
+
#: **第三类语义**:既不是「尚未开始」也不是「已经结束」,故**不入** scrub 的任一
|
|
146
|
+
#: 键族(并入即把「已确认」误判成「已生效 / 已失效」)。物理隔离守卫见
|
|
147
|
+
#: test_validity_filter.py 的交叉断言。
|
|
148
|
+
BELIEVED_AT_FIELD = "believed_at"
|
|
149
|
+
|
|
150
|
+
#: 过期时刻(写盘冗余:由 effective_until 派生,供审计 / 对账免计算直读)。
|
|
151
|
+
EXPIRED_AT_FIELD = "expired_at"
|
|
152
|
+
|
|
153
|
+
#: 巩固留痕(2026-09-19 阶段一):何时巩固 / 巩固进哪一条(成员 → 概念 id)。
|
|
154
|
+
#: 与 `promoted_at`(层迁移时刻)同族——把「归并产物可溯源」从流程记录升为一等字段。
|
|
155
|
+
#: **行为真源**(写入侧)见 md_cg/consolidate.py 的 induce / promote。
|
|
156
|
+
CONSOLIDATED_AT_FIELD = "consolidated_at"
|
|
157
|
+
CONSOLIDATED_INTO_FIELD = "consolidated_into"
|
|
158
|
+
|
|
159
|
+
#: 归纳来源(概念节点侧):前身成员清单 / 归纳时刻。与 `consolidated_*` 同族——
|
|
160
|
+
#: 成员侧列「巩固进哪一条」,概念侧列「前身是谁」,两侧互查即完整血缘。
|
|
161
|
+
INDUCED_FROM_FIELD = "induced_from"
|
|
162
|
+
INDUCED_AT_FIELD = "induced_at"
|
|
163
|
+
|
|
164
|
+
#: 巩固 / 归纳留痕字段族(单一真源):全族**不进索引白名单**(审计/血缘向,非查询
|
|
165
|
+
#: 热点),故 `add()` 覆写时须**回读节点文件**继承(索引快照里没有这些键)——
|
|
166
|
+
#: 否则概念节点被一次普通覆写(如审核 edit/merge 重写)即丢掉前身清单。
|
|
167
|
+
CONSOLIDATION_FIELDS = (CONSOLIDATED_AT_FIELD, CONSOLIDATED_INTO_FIELD,
|
|
168
|
+
INDUCED_FROM_FIELD, INDUCED_AT_FIELD)
|
|
169
|
+
|
|
170
|
+
#: 验证态字段。**刻意不叫 `state`**——该名已被裁决四态(ACCEPT/REJECT/DEFER/
|
|
171
|
+
#: BLINDSPOT)占用,`lifecycle_state` 的先例同此动机(观测位置不同即命名不同)。
|
|
172
|
+
#: 状态枚举与合法迁移表的**行为真源是 md_cg/trust.py**;本处只登记字段名,
|
|
173
|
+
#: **不复制枚举**——复制即制造第二份真源,与 CCG_CONTRACT_ROLES 的单一真源纪律相悖。
|
|
174
|
+
VERIFICATION_STATE_FIELD = "verification_state"
|
|
175
|
+
|
|
176
|
+
# ---- 「子功能」声明的**值语义**:哨兵与解析 --------------------------------------
|
|
177
|
+
# 行存在 ≠ 有声明:`# 子功能:无` 是合法填充(绝大多数知识节点不依赖任何单元),
|
|
178
|
+
# 若把它也算成「声明了依赖」,全库普通节点都会被 E050 硬拒——闸门立刻沦为噪声源。
|
|
179
|
+
# 故必须区分「行存在」与「构成真实声明」,判据收在这里(单一真源,ccgc/writepipe 共用)。
|
|
180
|
+
DEP_DECL_SENTINELS = ("无依赖", "无", "不适用", "不需要", "none", "n/a", "na", "-", "—")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# 生效条件:value 为 None 或 strip 后为空 → True;strip 后等于 DEP_DECL_SENTINELS 任一项、或为该哨兵加尾部括号说明(如「无(不依赖其他单元)」)→ True;其余 False(不做语义判断);
|
|
184
|
+
def is_dep_sentinel(value) -> bool:
|
|
185
|
+
"""纯函数:依赖槽的值是否属于「无依赖」哨兵。
|
|
186
|
+
|
|
187
|
+
只认**完全相等**或**哨兵+括号说明**两种形态,不做语义猜测——「无法确定」
|
|
188
|
+
这类真实陈述不得被误判为「无」(宁可多要求一次 depends_on,不可漏掉声明)。
|
|
189
|
+
"""
|
|
190
|
+
s = "" if value is None else str(value).strip()
|
|
191
|
+
if not s:
|
|
192
|
+
return True
|
|
193
|
+
for mark in DEP_DECL_SENTINELS:
|
|
194
|
+
if s == mark:
|
|
195
|
+
return True
|
|
196
|
+
for lp, rp in (("(", ")"), ("(", ")")):
|
|
197
|
+
if s.startswith(mark + lp) and s.endswith(rp):
|
|
198
|
+
return True
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# 生效条件:content 中不存在名字等于 field_name 的 `# 字段:` 行 → 返回 None;存在 → 返回首个命中行首个冒号之后的 strip 结果(可为空串);
|
|
203
|
+
def ccg_field_value(content, field_name: str):
|
|
204
|
+
"""读取 `# <字段>:<值>` 的值(首个命中行;全/半角冒号兼容)。无该行 → None。
|
|
205
|
+
|
|
206
|
+
与 `ccgc._upsert_ccg_line` 的解析口径一致(去 `#`→按冒号切名字→名字相等即
|
|
207
|
+
命中)——即**写入口径与读入口径共用同一条行语义**,避免「写进去读不出」。
|
|
208
|
+
"""
|
|
209
|
+
for ln in (content or "").split("\n"):
|
|
210
|
+
s = ln.strip()
|
|
211
|
+
if not s.startswith("#"):
|
|
212
|
+
continue
|
|
213
|
+
if s.lstrip("#").strip().split(":")[0].split(":")[0].strip() != field_name:
|
|
214
|
+
continue
|
|
215
|
+
for p in ("# " + field_name + ":", "# " + field_name + ":"):
|
|
216
|
+
if p in ln:
|
|
217
|
+
return ln.split(p, 1)[1].strip()
|
|
218
|
+
return ""
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ---- 「子功能」行中的**跨节点引用**:显式标记与提取 -------------------------------
|
|
223
|
+
# 为什么需要它(收窄判据 · 使用者 2026-09-19 裁定 b):「子功能」的契约角色是
|
|
224
|
+
# dependency,但同一个槽里实际承载着两种语义完全不同的内容——
|
|
225
|
+
# ① **自述子功能**:本单元**内部**由哪些子步骤/子模块构成(如「按扩展名把文件
|
|
226
|
+
# 路由到对应摄取器」)。它描述的是自身,不引用任何其它记忆单元。
|
|
227
|
+
# ② **跨节点依赖**:本单元的成立与否**寄生于另一个记忆单元**(如 `@code_xxx`)。
|
|
228
|
+
# 只有它才是失效传播的入口,也只有它才需要落 `depends_on`。
|
|
229
|
+
# 旧判据「值非哨兵即声明依赖」把①一并判成②:CCG 编译产物六要素必含「子功能」
|
|
230
|
+
# 行,于是该节点一旦落库就被永久要求 `depends_on`——第二次 link 必被 E050 硬拒
|
|
231
|
+
# (test_ccgc V16f 实证:合法内容被自己的闸门锁死)。故**收窄判据:只有显式引用
|
|
232
|
+
# 形态才构成跨节点依赖声明**,自然语言自述不算(依赖不是必填元数据,不得默认要求)。
|
|
233
|
+
#
|
|
234
|
+
# 形态约定:`@` 紧跟节点 id(`[A-Za-z0-9_]` 起头,可含 `.`/`-`);`@` 左侧不得是
|
|
235
|
+
# 标识符字符——后半条排除 `user@host`(邮箱等)被误读为引用。中文自然语言里 `@`
|
|
236
|
+
# 极罕见,故该标记本身就是「这是引用」的显式信号,**不依赖语义猜测**(与哨兵
|
|
237
|
+
# 「只认完全相等」同一纪律:能机械判的绝不猜)。
|
|
238
|
+
DEP_REF_MARK = "@"
|
|
239
|
+
DEP_REF_RE = re.compile(r"(?<![A-Za-z0-9_])@([A-Za-z0-9_][A-Za-z0-9_.\-]*)")
|
|
88
240
|
|
|
241
|
+
|
|
242
|
+
# 生效条件:value 为 None 时按空串处理 → 返回空列表;为任意值且 str 化后正则无命中 → 空列表;命中 → 按出现顺序返回去重后的 id 列表。
|
|
243
|
+
def dep_refs(value) -> list:
|
|
244
|
+
"""从「子功能」行的值中提取**显式跨节点引用**(`@<节点 id>`)→ 去重保序列表。
|
|
245
|
+
|
|
246
|
+
只做**形态提取**,不校验目标是否存在——「声称依赖」与「目标存在」是两件事,
|
|
247
|
+
混在一处会让「声称依赖一个并不存在的节点」被静默当成「没声明」(E051 永不触发)。
|
|
248
|
+
"""
|
|
249
|
+
if value is None:
|
|
250
|
+
return []
|
|
251
|
+
out = []
|
|
252
|
+
for m in DEP_REF_RE.finditer(str(value)):
|
|
253
|
+
if m.group(1) not in out:
|
|
254
|
+
out.append(m.group(1))
|
|
255
|
+
return out
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# 生效条件:content 中「# 子功能:」行缺失 → False;行存在且其值为空或命中依赖哨兵(is_dep_sentinel)→ False;行存在且值含显式跨节点引用(dep_refs 非空)→ True;其余(自然语言自述子功能)→ False;
|
|
259
|
+
def declares_dependency(content) -> bool:
|
|
260
|
+
"""「# 子功能:」是否构成**跨节点依赖声明**(行存在 ∧ 值非哨兵 ∧ 含 `@<id>`)。
|
|
261
|
+
|
|
262
|
+
这是 E050 硬拒的**前置判据真源**:只有**显式声称**依赖某节点,才要求
|
|
263
|
+
`depends_on` 给出可解析目标。行缺失、哨兵、以及**自述子功能**(自然语言描述
|
|
264
|
+
本单元的内部构成)一律按「未声明」处理——依赖不是必填元数据,把自述当声明会
|
|
265
|
+
让每个 CCG 节点(六要素必含「子功能」行)永久被要求 depends_on,闸门反过来
|
|
266
|
+
锁死合法写入。**只有「声称依赖却不落字段」才是契约违规**(声称与落盘不一致,
|
|
267
|
+
被依赖单元变动时下游无处可传,传播链从源头断掉)。
|
|
268
|
+
"""
|
|
269
|
+
val = ccg_field_value(content, "子功能")
|
|
270
|
+
if val is None or is_dep_sentinel(val):
|
|
271
|
+
return False
|
|
272
|
+
return bool(dep_refs(val))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# 生效条件:content 为 None 或空串时按源码的 content or "" 回落空串计算,非空时按其原值编码,一律返回 sha256(utf-8) 十六进制摘要的前 12 位(content 的 frontmatter 不计入口径由调用方保证)。
|
|
89
276
|
def content_hash(content: str) -> str:
|
|
90
277
|
"""节点正文的**内容指纹**(sha256 前 12 位)——全仓唯一实现。
|
|
91
278
|
|
|
@@ -102,6 +289,7 @@ def content_hash(content: str) -> str:
|
|
|
102
289
|
return hashlib.sha256((content or "").encode("utf-8")).hexdigest()[:12]
|
|
103
290
|
|
|
104
291
|
|
|
292
|
+
# 生效条件:frontmatter(任意 dict,键按 sorted 输出)与 content(任意 str)均无前置校验即生效;content 不以 "\n" 结尾时补一个换行,返回 "---" + 各键的 "{k}: {json.dumps(v, ensure_ascii=False)}" 行 + "---" 行 + 正文。
|
|
105
293
|
def dumps(frontmatter: dict, content: str) -> str:
|
|
106
294
|
lines = [_DELIM]
|
|
107
295
|
for k in sorted(frontmatter):
|
|
@@ -113,6 +301,7 @@ def dumps(frontmatter: dict, content: str) -> str:
|
|
|
113
301
|
return "\n".join(lines) + "\n" + body
|
|
114
302
|
|
|
115
303
|
|
|
304
|
+
# 生效条件:text 以 "---\n" 开头且其后存在 "\n---\n" 时才解析——head 段内含 ":" 的行按首个 ":" 拆键值(json.loads 成功取值、抛 ValueError 则保留原字符串),不含 ":" 的行跳过,返回 (fm, content);不满足上述两个起始条件时返回 ({}, text)。
|
|
116
305
|
def loads(text: str):
|
|
117
306
|
"""返回 (frontmatter dict, content str)。非法格式返回 ({}, 原文)。"""
|
|
118
307
|
if not text.startswith(_DELIM + "\n"):
|
|
@@ -135,6 +324,7 @@ def loads(text: str):
|
|
|
135
324
|
return fm, content
|
|
136
325
|
|
|
137
326
|
|
|
327
|
+
# 生效条件:content 中出现 "# {mark}:" 或 "# {mark}:"(中/英文冒号)即把该 mark 计入 present 与 required_present;complete 为 required_present 覆盖全部 CCG_REQUIRED、all_present 为 present 覆盖全部 CCG_MARKS,ratio = len(required_present)/len(CCG_REQUIRED),四键连同两个清单一起返回。
|
|
138
328
|
def ccg_completeness(content: str) -> dict:
|
|
139
329
|
"""CCG 要素齐全度——白箱可审计性的量化指标。
|
|
140
330
|
|
|
@@ -157,6 +347,7 @@ def ccg_completeness(content: str) -> dict:
|
|
|
157
347
|
}
|
|
158
348
|
|
|
159
349
|
|
|
350
|
+
# 生效条件:fm 的 "verification_basis" 缺键或取值为 None 时(.get 回落 None)直接返回 False;取值非 None 时,仅当该值属于模块常量 VERIFICATION_BASIS 才返回 True。
|
|
160
351
|
def verification_basis_valid(fm: dict) -> bool:
|
|
161
352
|
"""frontmatter.verification_basis 是否落在可接受枚举里。"""
|
|
162
353
|
vb = fm.get("verification_basis")
|
|
@@ -173,6 +364,7 @@ def verification_basis_valid(fm: dict) -> bool:
|
|
|
173
364
|
PLACEHOLDER_MARKERS = ("骨架锚点", "内容待填充", "待填充", "骨架节点")
|
|
174
365
|
|
|
175
366
|
|
|
367
|
+
# 生效条件:value 为 None 或 str(value).strip() 为空串时返回 True;否则仅当去空白后的字符串包含 PLACEHOLDER_MARKERS 中任一标记词时返回 True,其余返回 False(不做语义判断)。
|
|
176
368
|
def is_placeholder_text(value) -> bool:
|
|
177
369
|
"""占位标记的纯函数判定:空值或含占位标记 → 不可渲染为事实。
|
|
178
370
|
|
|
@@ -185,6 +377,7 @@ def is_placeholder_text(value) -> bool:
|
|
|
185
377
|
return any(m in s for m in PLACEHOLDER_MARKERS)
|
|
186
378
|
|
|
187
379
|
|
|
380
|
+
# 生效条件:content 中含 "# 不适用条件:"(全角冒号)或 "# 不适用条件:"(半角冒号)即返回 True,两者都不出现返回 False。
|
|
188
381
|
def has_non_applicable(content: str) -> bool:
|
|
189
382
|
"""是否声明了不适用条件——REJECT 路径成立的必要条件。
|
|
190
383
|
|
|
@@ -198,6 +391,7 @@ def has_non_applicable(content: str) -> bool:
|
|
|
198
391
|
NEG_FIELD = "不适用条件"
|
|
199
392
|
|
|
200
393
|
|
|
394
|
+
# 生效条件:content 为 None 时按 "" 处理;逐行 strip 后,仅当该行以 "#" 开头且 lstrip("#").strip() 又以 NEG_FIELD 开头时丢弃该行,其余行原样保留(保留原缩进),返回保留行的 "\n".join。
|
|
201
395
|
def positive_body(content: str) -> str:
|
|
202
396
|
"""剥离 `# 不适用条件:` 行后的正文——负条件不作召回键。
|
|
203
397
|
|
|
@@ -253,6 +447,7 @@ FULL_TIME_WINDOW_TEXT = "全时窗(任意时刻成立)"
|
|
|
253
447
|
LEGACY_POSITION_PREFIX = "观测位置:"
|
|
254
448
|
|
|
255
449
|
|
|
450
|
+
# 生效条件:value 为 list/tuple 时用「、」连接各元素 str().strip() 后非空的部分(空元素跳过);value 为 None 时返回 "";其余类型返回 str(value).strip()。
|
|
256
451
|
def _as_slot_text(value) -> str:
|
|
257
452
|
"""槽值 → 单行文本;列表值用「、」连接(「;」留给槽间分隔,不可混用)。"""
|
|
258
453
|
if isinstance(value, (list, tuple)):
|
|
@@ -262,6 +457,7 @@ def _as_slot_text(value) -> str:
|
|
|
262
457
|
return str(value).strip()
|
|
263
458
|
|
|
264
459
|
|
|
460
|
+
# 生效条件:value 可被 value[0]、value[1] 取下标并 float 化,且 lo <= FULL_TIME_WINDOW_MIN 同时 hi >= FULL_TIME_WINDOW_MAX 时返回 True;取值或 float 转换抛 TypeError/ValueError/IndexError/KeyError 时返回 False。
|
|
265
461
|
def is_full_time_window(value) -> bool:
|
|
266
462
|
"""时间窗是否覆盖全时窗(任意时刻成立)。解析不了 → False(不冒充已声明)。"""
|
|
267
463
|
try:
|
|
@@ -271,6 +467,7 @@ def is_full_time_window(value) -> bool:
|
|
|
271
467
|
return lo <= FULL_TIME_WINDOW_MIN and hi >= FULL_TIME_WINDOW_MAX
|
|
272
468
|
|
|
273
469
|
|
|
470
|
+
# 生效条件:float(value) 能被 time.gmtime 接受时按 UTC 返回 "%Y-%m-%d %H:%M";转换或格式化抛 TypeError/ValueError/OSError/OverflowError 时返回 ""。
|
|
274
471
|
def _fmt_ts(value) -> str:
|
|
275
472
|
"""unix 时间戳 → 「YYYY-MM-DD HH:MM」(UTC);解析不了 → ""。"""
|
|
276
473
|
try:
|
|
@@ -279,6 +476,7 @@ def _fmt_ts(value) -> str:
|
|
|
279
476
|
return ""
|
|
280
477
|
|
|
281
478
|
|
|
479
|
+
# 生效条件:先判 is_full_time_window(value) 为真则直接返回 FULL_TIME_WINDOW_TEXT(不落裸数组);否则取 value[0]、value[1] 的 UTC 文本(下标/取值抛 TypeError/IndexError/KeyError 返回 ""),两者有任一为空串也返回 "",均非空时返回 "{lo}~{hi}(UTC)"。
|
|
282
480
|
def time_window_text(value) -> str:
|
|
283
481
|
"""时间窗 → 可读文本。
|
|
284
482
|
|
|
@@ -296,6 +494,7 @@ def time_window_text(value) -> str:
|
|
|
296
494
|
return f"{lo}~{hi}(UTC)"
|
|
297
495
|
|
|
298
496
|
|
|
497
|
+
# 生效条件:cs 为非 dict 时返回 "";否则取 cs.get(key)(缺 key 键得 None),key == "time_window" 走时间窗文本、其余键走槽值文本,结果为假值或经 is_placeholder_text 判为占位时返回 "",否则返回该文本。
|
|
299
498
|
def condition_slot_text(cs, key: str) -> str:
|
|
300
499
|
"""单槽 → 可读值;缺失/空/待填充占位 → ""(不冒充已声明)。"""
|
|
301
500
|
if not isinstance(cs, dict):
|
|
@@ -307,6 +506,7 @@ def condition_slot_text(cs, key: str) -> str:
|
|
|
307
506
|
return s
|
|
308
507
|
|
|
309
508
|
|
|
509
|
+
# 生效条件:cs(任意值,非 dict 由 condition_slot_text 兜为 "")下,仅把 CONDITION_SLOTS 中 condition_slot_text(cs, key) 返回非空串的槽按固定顺序收集为 (槽名, 标签, 文本) 列表,缺失槽不写入。
|
|
310
510
|
def condition_space_slots(cs) -> list:
|
|
311
511
|
"""→ [(槽名, 标签, 文本)],只含**已声明**的槽(缺失槽不写)。"""
|
|
312
512
|
out = []
|
|
@@ -317,12 +517,14 @@ def condition_space_slots(cs) -> list:
|
|
|
317
517
|
return out
|
|
318
518
|
|
|
319
519
|
|
|
520
|
+
# 生效条件:cs(任意值)下,以 condition_space_slots(cs) 实际产出的槽名为已声明集合 have,返回 CONDITION_SLOTS 中不在 have 里的键名列表(已声明槽不计)。
|
|
320
521
|
def condition_space_missing(cs) -> list:
|
|
321
522
|
"""缺失槽名清单(待补台账用);已声明槽不计。"""
|
|
322
523
|
have = {k for k, _l, _t in condition_space_slots(cs)}
|
|
323
524
|
return [k for k, _l in CONDITION_SLOTS if k not in have]
|
|
324
525
|
|
|
325
526
|
|
|
527
|
+
# 生效条件:cs 任意值;require_full(默认 True)为真且 condition_space_slots(cs) 的槽数不等于 len(CONDITION_SLOTS) 时返回 "";否则按固定顺序用「;」连接已声明槽的 "{label}:{text}"(require_full 为 False 时仅部分槽也照渲,无槽则返回 "")。
|
|
326
528
|
def condition_space_text(cs, require_full: bool = True) -> str:
|
|
327
529
|
"""condition_space → 单行生效条件声明。**唯一合成入口**(纯函数,无 IO)。
|
|
328
530
|
|
|
@@ -337,10 +539,38 @@ def condition_space_text(cs, require_full: bool = True) -> str:
|
|
|
337
539
|
return ";".join(f"{label}:{text}" for _k, label, text in slots)
|
|
338
540
|
|
|
339
541
|
|
|
542
|
+
# 生效条件:text 为 None 时按 "" 处理;去空白后的字符串以 LEGACY_POSITION_PREFIX 开头且长度严格大于该前缀长度时返回 True,其余(含仅等于前缀本身、空串)返回 False。
|
|
340
543
|
def is_legacy_position_condition(text) -> bool:
|
|
341
544
|
"""文本是否为「观测位置:X」形态的单槽冒充(旧口径残留)。
|
|
342
545
|
|
|
343
546
|
只认前缀形态,不做语义猜测:用于审计与迁移定位,不参与正常渲染。
|
|
344
547
|
"""
|
|
345
548
|
s = "" if text is None else str(text).strip()
|
|
346
|
-
return s.startswith(LEGACY_POSITION_PREFIX) and len(s) > len(LEGACY_POSITION_PREFIX)
|
|
549
|
+
return s.startswith(LEGACY_POSITION_PREFIX) and len(s) > len(LEGACY_POSITION_PREFIX)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# 生效条件:text 为假值(None/空串)时按空文本处理返回 [];否则按「;/;」拆槽、槽内含「:」或「:」时取首个分隔符之后的内容,再按「,,、/()()」切短语,丢弃长度 <2、纯数字及命中时间维哨兵关键词的短语并去重后返回 out;
|
|
553
|
+
def cond_terms(text: str) -> list[str]:
|
|
554
|
+
"""生效条件声明 → 匹配短语列表(确定性切分,无语义猜测)。
|
|
555
|
+
|
|
556
|
+
切分规则:按槽分隔「;/;」拆槽(condition_space_text 以「;」连四槽)
|
|
557
|
+
→ 每槽剥「槽标签:」前缀(载体/位置、时间、方法、约束等标签是通用词,
|
|
558
|
+
参与命中必误判)→ 槽内按「,,、/()」切短语 → 丢弃长度 <2、纯数字、
|
|
559
|
+
全时窗哨兵短语(全时窗 = 时间维无信息量,不因其未命中而降级)。
|
|
560
|
+
"""
|
|
561
|
+
out, seen = [], set()
|
|
562
|
+
for slot in re.split(r"[;;]", str(text or "")):
|
|
563
|
+
if ":" in slot:
|
|
564
|
+
slot = slot.split(":", 1)[1]
|
|
565
|
+
elif ":" in slot:
|
|
566
|
+
slot = slot.split(":", 1)[1]
|
|
567
|
+
for seg in re.split(r"[,,、/()()]", slot):
|
|
568
|
+
seg = seg.strip()
|
|
569
|
+
if len(seg) < 2 or seg.isdigit():
|
|
570
|
+
continue
|
|
571
|
+
if "全时窗" in seg or "任意时刻" in seg:
|
|
572
|
+
continue
|
|
573
|
+
if seg not in seen:
|
|
574
|
+
seen.add(seg)
|
|
575
|
+
out.append(seg)
|
|
576
|
+
return out
|
package/md_cg/pooling.py
CHANGED
|
@@ -61,6 +61,7 @@ class PoolError(ValueError):
|
|
|
61
61
|
# 分类 / 校验 / 计划
|
|
62
62
|
# --------------------------------------------------------------------------
|
|
63
63
|
|
|
64
|
+
# 生效条件:pools 为 None 或 False 时返回 None;pools is True 时先替换为模块常量 WEIGHTS 再交 validate;pools 为其他值(含 dict)时直接交 validate(pools)。
|
|
64
65
|
def resolve(pools):
|
|
65
66
|
"""把 `pools` 参数解成生效配置:None→关闭;True→内置表;dict→校验后副本。"""
|
|
66
67
|
if pools is None or pools is False:
|
|
@@ -70,6 +71,7 @@ def resolve(pools):
|
|
|
70
71
|
return validate(pools)
|
|
71
72
|
|
|
72
73
|
|
|
74
|
+
# 生效条件:entry 为假值(含 None)时按 entry or {} 处理,其 "layer" 去假值后经 str() 属模块常量 NEG_LAYERS → 返回 POOL_NEGATIVE;否则 node_id 去假值转 str 后以模块常量 INDEX_PREFIXES 起始,或 entry 的 "tags"(假值按 [])小写后任一元素恰为 index/artifact/code_index/doc_index → 返回 POOL_INDEX;其余 → POOL_KNOWLEDGE。
|
|
73
75
|
def pool_of(node_id, entry=None) -> str:
|
|
74
76
|
"""节点归池(确定性、只看 id 前缀 / 层 / 标签,不读文件)。"""
|
|
75
77
|
e = entry or {}
|
|
@@ -84,6 +86,7 @@ def pool_of(node_id, entry=None) -> str:
|
|
|
84
86
|
return POOL_KNOWLEDGE
|
|
85
87
|
|
|
86
88
|
|
|
89
|
+
# 生效条件:pools 非 dict、缺 POOL_ORDER 中任一池、pools[p] or {} 不是 dict、float(spec.get("cap_ratio")) 或 float(spec.get("weight", 1.0)) 抛 TypeError/ValueError、ratio<=0、weight<=0、或各池 cap_ratio 之和与 1.0 之差超过 _RATIO_EPS 时抛 PoolError,全部通过才返回各项为 float 的 out(desc 经 spec.get("desc") or WEIGHTS[p]["desc"] 补齐)。
|
|
87
90
|
def validate(pools) -> dict:
|
|
88
91
|
"""校验并归一化权重表(**硬约束:额度之和必须恰为 1.0**)。"""
|
|
89
92
|
if not isinstance(pools, dict):
|
|
@@ -114,6 +117,7 @@ def validate(pools) -> dict:
|
|
|
114
117
|
return out
|
|
115
118
|
|
|
116
119
|
|
|
120
|
+
# 生效条件:total 先 int(total),total<=0 时各池返回 0;total < len(POOL_ORDER) 时把 total 全给 POOL_KNOWLEDGE、其余池为 0;否则每池保底 1、余量按 left*float(pools[p]["cap_ratio"]) 取 floor 后,余数按小数部分降序(并列按 POOL_ORDER 序)补 1,返回额度之和恰为 total 的 out。
|
|
117
121
|
def caps(total: int, pools) -> dict:
|
|
118
122
|
"""按比例分配额度;**各池额度之和恰等于 total**。
|
|
119
123
|
|
|
@@ -141,6 +145,7 @@ def caps(total: int, pools) -> dict:
|
|
|
141
145
|
return out
|
|
142
146
|
|
|
143
147
|
|
|
148
|
+
# 生效条件:total 给出后,pools 为 None/False 等使 resolve(pools) 返回假值时返回 {'enabled': False, 'total': int(total), 'note': ...};cfg 为真时用 caps(total, cfg) 并逐池取 cfg[p]["weight"]/cfg[p]["cap_ratio"],返回启用态计划。
|
|
144
149
|
def plan(total: int, pools=None) -> dict:
|
|
145
150
|
"""分池计划(供审计 / A/B 复算):额度 + 系数 + 是否启用。"""
|
|
146
151
|
cfg = resolve(pools)
|
|
@@ -155,6 +160,7 @@ def plan(total: int, pools=None) -> dict:
|
|
|
155
160
|
"index_prefixes": list(INDEX_PREFIXES)}
|
|
156
161
|
|
|
157
162
|
|
|
163
|
+
# 生效条件:pools 为 None/False 使 resolve(pools) 返回假值时返回 1.0;cfg 为真时返回 float(cfg[pool_of(node_id, entry)]["weight"]),其中 entry 缺省为 None。
|
|
158
164
|
def weight_of(node_id, entry=None, pools=None) -> float:
|
|
159
165
|
"""节点的打分乘数(未启用分池 → 1.0,保证原行为)。"""
|
|
160
166
|
cfg = resolve(pools)
|
|
@@ -163,6 +169,7 @@ def weight_of(node_id, entry=None, pools=None) -> float:
|
|
|
163
169
|
return float(cfg[pool_of(node_id, entry)]["weight"])
|
|
164
170
|
|
|
165
171
|
|
|
172
|
+
# 生效条件:docs 与 total 给出后无条件调用 take(docs, total, pools=pools, key_of=key_of) 并只返回其第 0 项,pools 与 key_of 缺省为 None 原样透传。
|
|
166
173
|
def allocate(docs, total: int, *, pools=None, key_of=None) -> list:
|
|
167
174
|
"""分池截断(只要结果;需要各池实取数用 `take()`)。未启用 → 平截(原行为)。
|
|
168
175
|
|
|
@@ -172,6 +179,7 @@ def allocate(docs, total: int, *, pools=None, key_of=None) -> list:
|
|
|
172
179
|
return take(docs, total, pools=pools, key_of=key_of)[0]
|
|
173
180
|
|
|
174
181
|
|
|
182
|
+
# 生效条件:cfg 为真时先 quota=caps(total, cfg),遍历 docs 时 key_of 为真则用 key_of(d) 解出 (nid, entry)、否则把 d 解包为 (nid, entry),按 pool_of(nid, entry) 入 buckets;backflow 为真时以 total 减去各池 min(len(buckets[p]), quota[p]) 得 left,按 POOL_ORDER 只对尚有 room 的池补 quota 到 left 用尽为止,返回 (buckets, quota)。
|
|
175
183
|
def _bucketize(docs, total: int, cfg, key_of, backflow: bool):
|
|
176
184
|
"""归池 + 分额 + 回流 → `(buckets, quota)`(`take` 与 `cut_report` 共用)。"""
|
|
177
185
|
quota = caps(total, cfg)
|
|
@@ -193,6 +201,7 @@ def _bucketize(docs, total: int, cfg, key_of, backflow: bool):
|
|
|
193
201
|
return buckets, quota
|
|
194
202
|
|
|
195
203
|
|
|
204
|
+
# 生效条件:docs 经 list(docs or [])(None/空容器→[]),pools 为 None/False 使 resolve(pools) 返回假值时返回 (docs[:int(total)], {});cfg 为真时转为 cut_report(docs, total, pools=pools, key_of=key_of, backflow=backflow) 并返回 (picked, report["taken"])。
|
|
196
205
|
def take(docs, total: int, *, pools=None, key_of=None, backflow: bool = True):
|
|
197
206
|
"""分池截断并**回报各池实取数** → `(picked, taken)`。
|
|
198
207
|
|
|
@@ -210,6 +219,7 @@ def take(docs, total: int, *, pools=None, key_of=None, backflow: bool = True):
|
|
|
210
219
|
return picked, report["taken"]
|
|
211
220
|
|
|
212
221
|
|
|
222
|
+
# 生效条件:docs 经 list(docs or [])、total 经 int(total),pools 为 None/False 使 resolve(pools) 返回假值时返回 (docs[:total], {"enabled": False});cfg 为真时用 _bucketize(docs, total, cfg, key_of, backflow),按 POOL_ORDER 逐池取 buckets[p][:quota[p]] 拼接 picked 并记录 taken/cands/lost,返回启用态完整 report。
|
|
213
223
|
def cut_report(docs, total: int, *, pools=None, key_of=None,
|
|
214
224
|
backflow: bool = True):
|
|
215
225
|
"""同 `take`,但回报**完整池账** → `(picked, report)`。
|
|
@@ -235,11 +245,13 @@ def cut_report(docs, total: int, *, pools=None, key_of=None,
|
|
|
235
245
|
"cands": cands, "taken": taken, "lost": lost}
|
|
236
246
|
|
|
237
247
|
|
|
248
|
+
# 生效条件:docs 与 total 给出后无条件调用 take(docs, total, pools=pools, key_of=doc_key) 并只返回其第 0 项,pools 缺省为 None 原样透传。
|
|
238
249
|
def cut(docs, total: int, *, pools=None) -> list:
|
|
239
250
|
"""检索 T2/T3 截断点专用:`docs = [(entry, fm, content)]`(只取结果)。"""
|
|
240
251
|
return take(docs, total, pools=pools, key_of=doc_key)[0]
|
|
241
252
|
|
|
242
253
|
|
|
254
|
+
# 生效条件:report 为真值且 report.get("enabled") 为真时,把 dict(report["taken"])/dict(report["cands"])/dict(report["lost"]) 写入 stat 的 pool_taken/pool_cands/pool_lost;否则一个键都不写,始终返回 stat。
|
|
243
255
|
def record_audit(stat: dict, report) -> dict:
|
|
244
256
|
"""把 `cut_report` 的池账落进检索 `stat`(**关闭态不写** → 不伪造池账)。
|
|
245
257
|
|
|
@@ -252,6 +264,7 @@ def record_audit(stat: dict, report) -> dict:
|
|
|
252
264
|
return stat
|
|
253
265
|
|
|
254
266
|
|
|
267
|
+
# 生效条件:d[1].get("id") 取到真值(非 None/空串等假值)时以其为 node_id,否则回落 d[0]["path"],并总是把 d[0] 作为 entry 返回;d[0] 无 "path" 键时在回落分支抛 KeyError。
|
|
255
268
|
def doc_key(d):
|
|
256
269
|
"""检索文档三元组 `(entry, fm, content)` → `(node_id, entry)`。"""
|
|
257
270
|
return (d[1].get("id") or d[0]["path"]), d[0]
|
|
@@ -261,6 +274,7 @@ def doc_key(d):
|
|
|
261
274
|
# 口径冻结与复测(只读)
|
|
262
275
|
# --------------------------------------------------------------------------
|
|
263
276
|
|
|
277
|
+
# 生效条件:xs 先按 float 排序,xs 为空时返回 0.0;否则取 idx=max(0, min(len(xs)-1, ceil(q*len(xs))-1)) 并返回 xs[idx],q 本身未做取值范围校验。
|
|
264
278
|
def _pct(xs, q):
|
|
265
279
|
"""分位数(最近秩法,确定性;零依赖)。"""
|
|
266
280
|
xs = sorted(float(x) for x in xs)
|
|
@@ -270,6 +284,7 @@ def _pct(xs, q):
|
|
|
270
284
|
return xs[idx]
|
|
271
285
|
|
|
272
286
|
|
|
287
|
+
# 生效条件:ids=sorted((cg.index.get("nodes") or {}).keys()) 为空时返回 [];否则 n 经 max(1, int(n))(n=0 会变成 1)、step=max(1, len(ids)//max(1,int(n))),对 ids[::step][:max(1,int(n))] 逐个 cg._read(entry),读失败或无 content 则 continue,首个以 # 开头的行按「含全角冒号取其后、否则去 # 与空格」生成 q,q 非空才追加,返回 out。
|
|
273
288
|
def bench_queries(cg, n: int = 50) -> list:
|
|
274
289
|
"""从索引**确定性**取样查询词(按 id 排序等距抽,取节点标题行)。
|
|
275
290
|
|
|
@@ -308,10 +323,12 @@ DELTA_KEYS = ("p95_scanned", "p50_scanned", "mean_candidates",
|
|
|
308
323
|
"index_share", "knowledge_share", "pool_lost_rate")
|
|
309
324
|
|
|
310
325
|
|
|
326
|
+
# 生效条件:xs 为真值(非空容器)时返回 sum(xs)/len(xs),xs 为假值(空容器或 None)时返回 0.0。
|
|
311
327
|
def _mean(xs):
|
|
312
328
|
return (sum(xs) / len(xs)) if xs else 0.0
|
|
313
329
|
|
|
314
330
|
|
|
331
|
+
# 生效条件:res 为假值(None/空列表)时返回空 dict;否则对每个 r 取 node=r[0] or {},以 pool_of(node.get("id"), node.get("frontmatter")) 归池并累加计数,返回 out。
|
|
315
332
|
def _pool_counts(res) -> dict:
|
|
316
333
|
"""结果集的归池构成(对分池**敏感**的口径:索引类是否吃满召回)。"""
|
|
317
334
|
out = {}
|
|
@@ -322,6 +339,7 @@ def _pool_counts(res) -> dict:
|
|
|
322
339
|
return out
|
|
323
340
|
|
|
324
341
|
|
|
342
|
+
# 生效条件:queries 为真值时用 list(queries)、为假值(None/空列表)时改用 bench_queries(cg, n=n_queries);逐 q 调 cg.search(q, k=int(k), record=False, judge=judge, pools=pools) 且该调用抛异常则跳过该 q;meta.get("pre_cap") 为 None 时回落 meta.get("candidates")/cap,仅 cap 为真且 pre>cap 才计入截断与损失,且 pl.get("lost") 与 pl.get("cands") 均非空才计入 pool_lost_rate。
|
|
325
343
|
def measure(cg, queries=None, *, k: int = 20, pools=None, judge: bool = False,
|
|
326
344
|
n_queries: int = 50) -> dict:
|
|
327
345
|
"""同口径跑一遍(**只读**):系统成本口径 + 结果构成口径 + 池级截断损失。
|
|
@@ -387,12 +405,14 @@ def measure(cg, queries=None, *, k: int = 20, pools=None, judge: bool = False,
|
|
|
387
405
|
"同口径前后对比只看方向与幅度,不当绝对结论")}
|
|
388
406
|
|
|
389
407
|
|
|
408
|
+
# 生效条件:调用即从 md_cg.mdcg 取 GLOBAL_CAP(getattr 缺省 0),取到假值(0/None/空串)时经 or 0 回落 0,返回 int(...)。
|
|
390
409
|
def meta_cap(cg) -> int:
|
|
391
410
|
"""读当前全局额度(避免硬编码漂移)。"""
|
|
392
411
|
from . import mdcg
|
|
393
412
|
return int(getattr(mdcg, "GLOBAL_CAP", 0) or 0)
|
|
394
413
|
|
|
395
414
|
|
|
415
|
+
# 生效条件:queries 为真值时用 list(queries)、为假值(None/空列表)时用 bench_queries(cg, n=n_queries);before 恒以 pools=None 调用 measure,after 在 pools 为 None 时用模块常量 WEIGHTS、pools 显式(含 False)时原样传入;delta 只统计 DELTA_KEYS 中 before/after 两侧均为 int/float 的键,其余不参与 Δ 计算。
|
|
396
416
|
def compare(cg, queries=None, *, k: int = 20, pools=None, n_queries: int = 50) -> dict:
|
|
397
417
|
"""§七 要求的「同口径复测」:关闭态 vs 启用态 一并给出 + 差值 + 副作用归因。"""
|
|
398
418
|
qs = list(queries) if queries else bench_queries(cg, n=n_queries)
|
|
@@ -418,6 +438,7 @@ def compare(cg, queries=None, *, k: int = 20, pools=None, n_queries: int = 50) -
|
|
|
418
438
|
"只说明该口径测不到池内重分配")}
|
|
419
439
|
|
|
420
440
|
|
|
441
|
+
# 生效条件:无入参,调用即返回由模块常量 POOL_ORDER/WEIGHTS/INDEX_PREFIXES/NEG_LAYERS/ENV_SWITCH 组装的自描述 dict,cap_ratio_sum 为 round(sum(WEIGHTS[p]["cap_ratio"] for p in POOL_ORDER), 12)。
|
|
421
442
|
def catalog() -> dict:
|
|
422
443
|
"""自描述(供 MCP / 人工核对)。"""
|
|
423
444
|
return {"layer": "召回分池与降权(§七)",
|
|
@@ -441,6 +462,7 @@ def catalog() -> dict:
|
|
|
441
462
|
}
|
|
442
463
|
|
|
443
464
|
|
|
465
|
+
# 生效条件:pools 非 None 且非 False 时原样返回 pools;pools 为 None 或 False 时读 os.environ.get(ENV_SWITCH),缺失/空串经 or "" 归空串并 strip().lower(),属于 ("1","on","true","yes","y") 则返回 True,否则返回 None。
|
|
444
466
|
def from_env(pools=None):
|
|
445
467
|
"""载体侧开关:`pools` 显式给出时优先;否则读 `MDCG_POOLING`(默认关)。"""
|
|
446
468
|
if pools is not None and pools is not False:
|
|
@@ -448,4 +470,4 @@ def from_env(pools=None):
|
|
|
448
470
|
v = (os.environ.get(ENV_SWITCH) or "").strip().lower()
|
|
449
471
|
if v in ("1", "on", "true", "yes", "y"):
|
|
450
472
|
return True
|
|
451
|
-
return None
|
|
473
|
+
return None
|