@furongjun1999/dsh-memory 0.4.8 → 0.4.9
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 +6 -3
- package/lib/lib/mdcg_client.js +6 -5
- 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_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 +129 -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 +12 -8
- 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/routing.py
CHANGED
|
@@ -29,6 +29,7 @@ _SKELETON_TAIL = re.compile(r"(知识点)?内容\s*[((]按骨架填充[))]\s
|
|
|
29
29
|
ORPHAN = "orphan"
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
# 生效条件:raw 为真值时按 NFKC 归一化并 strip,最多 3 轮剥离 _INSTANCE_TAIL(无变化即停)、再剥 _SKELETON_TAIL、空白转下划线后返回;raw 为假值(None/空串)或处理后为空串时返回 ORPHAN。
|
|
32
33
|
def normalize_domain(raw: str) -> str:
|
|
33
34
|
"""把自由文本的域描述归一化成稳定的短键。
|
|
34
35
|
|
|
@@ -50,6 +51,7 @@ def normalize_domain(raw: str) -> str:
|
|
|
50
51
|
return s or ORPHAN
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
# 生效条件:tags 为真值时取其中首个 str(t) 以 "domain:" 开头的项,返回其前缀后内容的 normalize_domain 结果;无此标签且 condition_space 为真值时返回 normalize_domain(condition_space.get("observation_position"))(缺键即 None 归一为 ORPHAN);tags 为假值且 condition_space 为假值时返回 ORPHAN。
|
|
53
55
|
def route_key(condition_space: dict = None, tags=None) -> str:
|
|
54
56
|
"""导出条件路由键。优先级:tags 的 domain: > observation_position 归一化 > orphan。
|
|
55
57
|
|
|
@@ -65,6 +67,7 @@ def route_key(condition_space: dict = None, tags=None) -> str:
|
|
|
65
67
|
return ORPHAN
|
|
66
68
|
|
|
67
69
|
|
|
70
|
+
# 生效条件:key 等于 ORPHAN 时原样返回 ORPHAN;否则返回 "cond_" + 把 key 中非[\w中文-]字符替换为下划线并截前 24 字符的串 + "_" + key 的 sha256(utf-8)前 8 位十六进制。
|
|
68
71
|
def bucket_dir(key: str) -> str:
|
|
69
72
|
"""路由键 → 目录名。中文键保留可读前缀 + 短哈希,避免文件系统非法字符/超长。"""
|
|
70
73
|
if key == ORPHAN:
|
|
@@ -115,6 +118,7 @@ BIG_DOMAINS = {
|
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
|
|
121
|
+
# 生效条件:terms 为真值时对 BIG_DOMAINS 每个大域统计命中词数,最高分为 0 或 terms 为假值(None/空)时返回 None,否则返回最高分大域名(并列取 BIG_DOMAINS 迭代序首个最高分)。
|
|
118
122
|
def big_domain_classify(terms) -> str | None:
|
|
119
123
|
"""阶段 1:14 大域并行打分 → 收敛到 top-1。
|
|
120
124
|
|
|
@@ -137,6 +141,7 @@ def big_domain_classify(terms) -> str | None:
|
|
|
137
141
|
return best[0]
|
|
138
142
|
|
|
139
143
|
|
|
144
|
+
# 生效条件:terms 为假值(None/空)时返回 {每个 BIG_DOMAINS 域: 0};terms 为真值时返回 {大域: 该域词表命中 terms 中词的个数}。
|
|
140
145
|
def big_domain_score_breakdown(terms) -> dict:
|
|
141
146
|
"""暴露打分明细,便于审计与回归测试。"""
|
|
142
147
|
if not terms:
|
|
@@ -156,6 +161,7 @@ def big_domain_score_breakdown(terms) -> dict:
|
|
|
156
161
|
# 老函数保持不动 → P0/P1 基线可比性不受影响;新函数只供新路径(fuzzy)使用。
|
|
157
162
|
# ---------------------------------------------------------------------------
|
|
158
163
|
|
|
164
|
+
# 生效条件:term 或 word 为假值(None/空串)返回 0.0;相等返回 1.0;word 是 term 子串返回 len(word)/len(term);term 是 word 子串返回 len(term)/len(word);二者无包含关系返回 0.0。
|
|
159
165
|
def membership(term: str, word: str) -> float:
|
|
160
166
|
"""词 term 对代表词 word 的隶属度(0.0~1.0,越接近 1 越隶属)。
|
|
161
167
|
|
|
@@ -175,6 +181,7 @@ def membership(term: str, word: str) -> float:
|
|
|
175
181
|
return 0.0
|
|
176
182
|
|
|
177
183
|
|
|
184
|
+
# 生效条件:无入参,恒遍历模块级常量 BIG_DOMAINS 的各域代表词表,返回 {代表词: 包含该词的域个数}。
|
|
178
185
|
def _build_domain_df() -> dict:
|
|
179
186
|
"""代表词 → 覆盖它的大域数(IDF 的分母)。"""
|
|
180
187
|
df = {}
|
|
@@ -187,6 +194,7 @@ def _build_domain_df() -> dict:
|
|
|
187
194
|
_DOMAIN_DF = _build_domain_df()
|
|
188
195
|
|
|
189
196
|
|
|
197
|
+
# 生效条件:恒返回 math.log(1.0 + len(BIG_DOMAINS) / _DOMAIN_DF.get(word, 1)),即 word 在 _DOMAIN_DF 中取其覆盖域数,缺键(含任何未登记值)时按 _DOMAIN_DF.get 的默认 1 代入。
|
|
190
198
|
def domain_idf(word: str) -> float:
|
|
191
199
|
"""代表词的区分度权重:log(1 + 大域总数 / 覆盖它的大域数)。
|
|
192
200
|
|
|
@@ -196,6 +204,7 @@ def domain_idf(word: str) -> float:
|
|
|
196
204
|
return math.log(1.0 + len(BIG_DOMAINS) / _DOMAIN_DF.get(word, 1))
|
|
197
205
|
|
|
198
206
|
|
|
207
|
+
# 生效条件:terms 是 dict 时以其条目(键 str(t) 以 "__" 开头者剔除、值为 float)为词权重并忽略 weights,否则以 wmap.get(t, 1.0) 为权重(weights 为假值即 None/空 dict 时 wmap 为空、全部权重取 1.0,terms 为假值则词集为空);逐域累加「词权重 × 该词在域词表内最大(membership × domain_idf)」,权重 <=0 的词跳过,返回 {大域: round(得分, 6)}。
|
|
199
208
|
def big_domain_score_weighted(terms, weights=None) -> dict:
|
|
200
209
|
"""阶段 1(分级版):14 大域并行打分,按「隶属度 × IDF」加权。
|
|
201
210
|
|
|
@@ -226,6 +235,7 @@ def big_domain_score_weighted(terms, weights=None) -> dict:
|
|
|
226
235
|
return out
|
|
227
236
|
|
|
228
237
|
|
|
238
|
+
# 生效条件:以 terms、weights 计算各域加权得分(terms 为假值时各域均为 0.0),得分为空或最高分 <= min_score(默认 0.0)时返回 None,否则返回最高分大域名。
|
|
229
239
|
def big_domain_classify_weighted(terms, weights=None, min_score: float = 0.0):
|
|
230
240
|
"""阶段 1(分级版)收敛到 top-1;全部低于 min_score → None。"""
|
|
231
241
|
scores = big_domain_score_weighted(terms, weights)
|
|
@@ -237,6 +247,7 @@ def big_domain_classify_weighted(terms, weights=None, min_score: float = 0.0):
|
|
|
237
247
|
return best[0]
|
|
238
248
|
|
|
239
249
|
|
|
250
|
+
# 生效条件:a 或 b 为假值(None/空串)返回 0.0;相等返回 1.0;a 是 b 子串返回 len(a)/len(b);b 是 a 子串返回 len(b)/len(a);否则按二者二元组字符集合的 Jaccard 返回 len(ga & gb)/len(ga | gb),任一集合为空(如单字符键)时返回 0.0。
|
|
240
251
|
def domain_similarity(a: str, b: str) -> float:
|
|
241
252
|
"""两个归一化域键的相似度(0~1):相同 1.0;包含取长度比;否则二元组 Jaccard。
|
|
242
253
|
|
|
@@ -258,6 +269,7 @@ def domain_similarity(a: str, b: str) -> float:
|
|
|
258
269
|
return len(ga & gb) / len(ga | gb)
|
|
259
270
|
|
|
260
271
|
|
|
272
|
+
# 生效条件:counts 各值之和为 0(含空 dict 与全 0 计数)时返回 {'ok': True, 'reason': 'empty', 'buckets': 0};否则在最大桶占比 >30%、桶数 >1 且单例桶占比 >50%、期望扫描 >30% 中命中的项写入 problems,返回含 ok(=problems 为空)、buckets、nodes、max_bucket_share、singleton_ratio、expected_scan、problems 的 dict。
|
|
261
273
|
def bucket_health(counts: dict) -> dict:
|
|
262
274
|
"""分区健康度自检。分桶键一旦退化(巨桶或碎片化),条件路由就是纸面收益,
|
|
263
275
|
必须在写入侧就能发现,而不是等召回变差才回头查。
|
|
@@ -288,3 +300,67 @@ def bucket_health(counts: dict) -> dict:
|
|
|
288
300
|
"expected_scan": expected_scan,
|
|
289
301
|
"problems": problems,
|
|
290
302
|
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# 节点侧大域标签(S1 大域先验收敛的前置元数据)
|
|
307
|
+
#
|
|
308
|
+
# 为何需要:`big_domain_classify` 一直只作用于 **query 侧**(terms),节点侧没有域字段,
|
|
309
|
+
# 于是「14 大域并行打分」算完只能写进 meta(审计偏差 4:先验算出来却当成报告)。
|
|
310
|
+
# 要让大域先验真正参与候选收敛,节点必须在写入时固化自己的域。
|
|
311
|
+
# 口径:与查询侧同一个分类器(BIG_DOMAINS + big_domain_classify),保证两侧同构。
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
# 域词取词:中文(≥2 字)与拉丁词(≥2 字)。词表命中用「包含关系」判定,
|
|
315
|
+
# 故 2 字以上的切口足以覆盖 1~4 字代表词(如「工程」命中「工程师」)。
|
|
316
|
+
DOMAIN_TERM_RE = re.compile(r"[\u4e00-\u9fff]{2,}|[A-Za-z][A-Za-z0-9_]{1,}")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# 生效条件:text 为假值(None/空串)时返回 [];否则以 DOMAIN_TERM_RE 取词、按出现顺序去重后返回,
|
|
320
|
+
# 最多 limit 个(limit 为假值/负数时按空处理,返回 [])。
|
|
321
|
+
def domain_terms(text, limit: int = 400) -> list:
|
|
322
|
+
"""从节点正文/标题抽「域词」(供 big_domain_classify 使用)。
|
|
323
|
+
|
|
324
|
+
只做最小分词:中文按 ≥2 字连续片段、拉丁按 [A-Za-z][A-Za-z0-9_]+ 取词;
|
|
325
|
+
去重保序,最多 limit 个(默认 400,避免长文把分类器拖慢)。
|
|
326
|
+
"""
|
|
327
|
+
if not text or not limit or limit < 1:
|
|
328
|
+
return []
|
|
329
|
+
out, seen = [], set()
|
|
330
|
+
for m in DOMAIN_TERM_RE.finditer(str(text)):
|
|
331
|
+
w = m.group(0)
|
|
332
|
+
if w in seen:
|
|
333
|
+
continue
|
|
334
|
+
seen.add(w)
|
|
335
|
+
out.append(w)
|
|
336
|
+
if len(out) >= limit:
|
|
337
|
+
break
|
|
338
|
+
return out
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# 生效条件:bucket 为假值或等于 ORPHAN 时返回空串;否则去掉 "cond_" 前缀、并当末段为 8 位十六进制哈希时剥掉该段,返回剩余的可读键。
|
|
342
|
+
def bucket_key_readable(bucket: str) -> str:
|
|
343
|
+
"""桶目录名 → 可读键(S1b query 侧桶推断用):'cond_感知系统_d94e90d2' → '感知系统'。
|
|
344
|
+
|
|
345
|
+
与 `bucket_dir` 互逆(丢哈希段);`orphan`/空值返回空串(S1b 不把 orphan 当键,orphan 恒作兜底)。
|
|
346
|
+
"""
|
|
347
|
+
if not bucket or bucket == ORPHAN:
|
|
348
|
+
return ""
|
|
349
|
+
s = str(bucket)
|
|
350
|
+
if s.startswith("cond_"):
|
|
351
|
+
s = s[len("cond_"):]
|
|
352
|
+
head, sep, tail = s.rpartition("_")
|
|
353
|
+
if sep and len(tail) == 8 and all(c in "0123456789abcdef" for c in tail):
|
|
354
|
+
s = head
|
|
355
|
+
return s
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# 生效条件:text 为假值或取不到任何域词时返回 None;否则返回 big_domain_classify(domain_terms(text))
|
|
359
|
+
# 的结果(无有效域信号时亦为 None,调用方据此决定是否落域字段)。
|
|
360
|
+
def classify_text(text, limit: int = 400):
|
|
361
|
+
"""正文 → 大域名(节点侧域标签真源)。与查询侧共用 big_domain_classify。
|
|
362
|
+
|
|
363
|
+
返回 None 表示「无有效域信号」——此时**不写** big_domain 字段,
|
|
364
|
+
该节点留在 ORPHAN/兜底池(S1 收敛时必须能被兜底召回,见契约 §3 S1 不变量)。
|
|
365
|
+
"""
|
|
366
|
+
return big_domain_classify(domain_terms(text, limit))
|
package/md_cg/scrub.py
CHANGED
|
@@ -71,6 +71,9 @@ STRATUM_WEIGHTS = {"stale": 0.20, "low_conf": 0.20, "disputed": 0.15,
|
|
|
71
71
|
CONTAMINATION = {
|
|
72
72
|
"contradiction": ("high", ("weaken", "demote")),
|
|
73
73
|
"expired": ("high", ("weaken", "demote")),
|
|
74
|
+
# 未生效(双时间轴另一侧,2026-09-19):**不是错误**——只提示「此刻不适用」,
|
|
75
|
+
# 故 severity=info、处置仅 hint(不 weaken / 不 demote / 不删)。
|
|
76
|
+
"not_yet": ("info", ("hint",)),
|
|
74
77
|
"duplicate": ("medium", ("hint",)),
|
|
75
78
|
"orphan_noise": ("low", ("weaken", "demote")),
|
|
76
79
|
"unverified": ("info", ("hint",)),
|
|
@@ -82,10 +85,12 @@ SEVERITY_ORDER = {"high": 3, "medium": 2, "low": 1, "info": 0}
|
|
|
82
85
|
# 工具
|
|
83
86
|
# --------------------------------------------------------------------------
|
|
84
87
|
|
|
88
|
+
# 生效条件:当 cg 的 index 为真且其 "nodes" 为真时返回该值,否则(cg 无 index、index 为假值、缺 "nodes" 或 "nodes" 为假值)返回 {};
|
|
85
89
|
def _nodes(cg) -> dict:
|
|
86
90
|
return (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
87
91
|
|
|
88
92
|
|
|
93
|
+
# 生效条件:cg 与 nid 传入后,若 cache 非 None 且 nid 已在 cache 中则直接返回 cache[nid](即使其值为假值);否则尝试 cg.get(nid),异常或返回假值时按空节点处理,再取其中的 "frontmatter" 真值,若为假值则用 {};cache 非 None 时把结果写入 cache[nid] 后返回;
|
|
89
94
|
def _fm_of(cg, nid, cache=None) -> dict:
|
|
90
95
|
"""取节点 frontmatter(带可选缓存)。不可读(缺密钥 / 已删)→ {}。"""
|
|
91
96
|
if cache is not None and nid in cache:
|
|
@@ -101,6 +106,7 @@ def _fm_of(cg, nid, cache=None) -> dict:
|
|
|
101
106
|
return fm
|
|
102
107
|
|
|
103
108
|
|
|
109
|
+
# 生效条件:cg.access_counts() 调用成功时返回其结果(访问次数, 最后访问时间);调用抛出任何 Exception 时返回 ({}, {});
|
|
104
110
|
def _access(cg):
|
|
105
111
|
"""(访问次数, 最后访问时间):读访问日志,未 compact 的也算。"""
|
|
106
112
|
try:
|
|
@@ -109,6 +115,7 @@ def _access(cg):
|
|
|
109
115
|
return {}, {}
|
|
110
116
|
|
|
111
117
|
|
|
118
|
+
# 生效条件:`from . import chain` 成功且 chain.adjacency(cg) 正常返回时返回该 dict,导入或调用抛任何异常时返回 {}。
|
|
112
119
|
def _adjacency(cg) -> dict:
|
|
113
120
|
try:
|
|
114
121
|
from . import chain
|
|
@@ -117,6 +124,7 @@ def _adjacency(cg) -> dict:
|
|
|
117
124
|
return {}
|
|
118
125
|
|
|
119
126
|
|
|
127
|
+
# 生效条件:float(ts or 0) 抛 TypeError/ValueError 时返回 0.0,转换后为 0(含 ts 为 0/空串/None 等假值)时返回 0.0,否则返回 (now - ts)/86400.0。
|
|
120
128
|
def _days(ts, now) -> float:
|
|
121
129
|
try:
|
|
122
130
|
ts = float(ts or 0)
|
|
@@ -125,6 +133,7 @@ def _days(ts, now) -> float:
|
|
|
125
133
|
return (now - ts) / 86400.0 if ts else 0.0
|
|
126
134
|
|
|
127
135
|
|
|
136
|
+
# 生效条件:调用即返回带 t 与 op 的 rec;写 append_jsonl(os.path.join(cg.root, SCRUB_LOG), rec) 抛任何异常都被吞掉,不影响返回值。
|
|
128
137
|
def _log(cg, op: str, **rec) -> dict:
|
|
129
138
|
rec = dict(rec, t=time.time(), op=op)
|
|
130
139
|
try:
|
|
@@ -134,6 +143,7 @@ def _log(cg, op: str, **rec) -> dict:
|
|
|
134
143
|
return rec
|
|
135
144
|
|
|
136
145
|
|
|
146
|
+
# 生效条件:仅当 read_jsonl(cg.root 下 SCRUB_LOG) 的记录 op=="decontaminate" 且 ok 为真时,把 (rec.get("node_id"), rec.get("kind")) 收进返回集合;无此类记录返回空集。
|
|
137
147
|
def _handled(cg) -> set:
|
|
138
148
|
"""已处置过的 (node_id, kind):保证去污染幂等(审计即状态)。"""
|
|
139
149
|
out = set()
|
|
@@ -147,6 +157,7 @@ def _handled(cg) -> set:
|
|
|
147
157
|
# ① 记忆抽查
|
|
148
158
|
# --------------------------------------------------------------------------
|
|
149
159
|
|
|
160
|
+
# 生效条件:当 cg 的节点/访问/邻接数据可取时,对每个「layer 不在 SELF_LAYERS」的节点(源码仅以 `if layer in SELF_LAYERS: continue` 排除,未要求 layer 非空)按 now、stale_days、unverified_days 判定入池——last 访问时间距今≥stale_days 时入 stale,last 为假值且 created_at 距今≥stale_days 时入 stale,访问计数 acc≥3 入 hot,邻接度 deg==0 且 layer=="contextual" 入 orphan,evidence_count≤0 且 layer=="knowledge" 且 age_d≥unverified_days 入 unverified,evidence_count>0 且 reads<int(max_reads) 且 _fm_of 返回非空前台时按 negative_evidence>0 入 disputed、按 confidence<LOW_CONF(confidence 缺键回落 0.6)入 low_conf(max_reads 为 0 时 int(max_reads)=0,reads<0 恒假,故 disputed/low_conf 不产生),每个节点无条件入 random 池,最后按各池 key 排序返回 pools(片段仅见排序段,未展示抽样阶段);多分支无法一句话覆盖全部分支。
|
|
150
161
|
def _pool_candidates(cg, *, now, stale_days, unverified_days, max_reads=200):
|
|
151
162
|
"""构造各层候选池(不读文件的部分先用索引 + 访问日志)。"""
|
|
152
163
|
nodes = _nodes(cg)
|
|
@@ -210,6 +221,7 @@ def _pool_candidates(cg, *, now, stale_days, unverified_days, max_reads=200):
|
|
|
210
221
|
return pools
|
|
211
222
|
|
|
212
223
|
|
|
224
|
+
# 生效条件:strategy=="random" 时直接返回 {"random": int(n)};strategy=="risk" 时按剔除 hot/random 后的 STRATUM_WEIGHTS 权重分配;其它策略名走全权重分配,两者都把 int(n) 余量补进已存在的 random 键否则补进 stale。
|
|
213
225
|
def _quota(n: int, strategy: str) -> dict:
|
|
214
226
|
if strategy == "random":
|
|
215
227
|
return {"random": int(n)}
|
|
@@ -231,6 +243,7 @@ def _quota(n: int, strategy: str) -> dict:
|
|
|
231
243
|
return out
|
|
232
244
|
|
|
233
245
|
|
|
246
|
+
# 生效条件:k<=0 或 pool 为假值(空池)时返回 [],否则取池前 k*3 项后用 random.Random(f"{seed}:{stratum}") 稳定洗牌并返回前 k 项(池长不足 k*3 时对全池洗牌)。
|
|
234
247
|
def _pick(pool, k: int, seed, stratum: str):
|
|
235
248
|
"""从池中取 k 个:风险最高的 3k 个入池,再按 seed 稳定洗牌。"""
|
|
236
249
|
if k <= 0 or not pool:
|
|
@@ -242,6 +255,7 @@ def _pick(pool, k: int, seed, stratum: str):
|
|
|
242
255
|
return cand[:k]
|
|
243
256
|
|
|
244
257
|
|
|
258
|
+
# 生效条件:strategy 经 str(strategy or "stratified").lower() 后属于 stratified/risk/random 时返回带分层标签的样本(seed 为 None 时取 0,n 为 0 时 picked 为空),否则抛 ValueError。
|
|
245
259
|
def sample(cg, n: int = DEFAULT_SAMPLE, *, strategy: str = "stratified",
|
|
246
260
|
seed=None, stale_days: float = STALE_DAYS,
|
|
247
261
|
unverified_days: float = UNVERIFIED_DAYS,
|
|
@@ -284,12 +298,14 @@ def sample(cg, n: int = DEFAULT_SAMPLE, *, strategy: str = "stratified",
|
|
|
284
298
|
# ② 联想
|
|
285
299
|
# --------------------------------------------------------------------------
|
|
286
300
|
|
|
301
|
+
# 生效条件:a 或 b 为假值(空集)时返回 0.0,否则返回 len(a & b)/len(a | b)。
|
|
287
302
|
def _jaccard(a: set, b: set) -> float:
|
|
288
303
|
if not a or not b:
|
|
289
304
|
return 0.0
|
|
290
305
|
return len(a & b) / len(a | b)
|
|
291
306
|
|
|
292
307
|
|
|
308
|
+
# 生效条件:对 node_id 汇总关系链(chain.walk 出边与入边,max_depth=int(hops))、子图层级(subgraph.expand 命中项与父索引逐级上溯 int(hops) 层)以及 lexical 为真时的词法近邻(只在 _nodes 前 int(max_scan) 个节点内比 bigram、sim≥min_sim 者),返回按 weight 降序的 items[:int(limit)]。
|
|
293
309
|
def associate(cg, node_id: str, *, hops: int = DEFAULT_HOPS, limit: int = 30,
|
|
294
310
|
lexical: bool = True, min_sim: float = 0.25,
|
|
295
311
|
max_scan: int = MAX_ASSOC_SCAN) -> dict:
|
|
@@ -300,6 +316,7 @@ def associate(cg, node_id: str, *, hops: int = DEFAULT_HOPS, limit: int = 30,
|
|
|
300
316
|
"""
|
|
301
317
|
related = {}
|
|
302
318
|
|
|
319
|
+
# 生效条件:nid 为真且 nid != node_id 时,构造 rec={'node_id':nid,'via':via,'weight':round(float(weight),4),'depth':int(depth)} 并并入 extra;仅当 related 中 nid 不存在或 rec['weight'] 大于已有 weight 时更新 related[nid];nid 为假或等于 node_id 时直接返回;
|
|
303
320
|
def put(nid, via, weight, depth=1, **extra):
|
|
304
321
|
if not nid or nid == node_id:
|
|
305
322
|
return
|
|
@@ -385,13 +402,19 @@ def associate(cg, node_id: str, *, hops: int = DEFAULT_HOPS, limit: int = 30,
|
|
|
385
402
|
# ③ 去污染:确定性判据
|
|
386
403
|
# --------------------------------------------------------------------------
|
|
387
404
|
|
|
388
|
-
|
|
405
|
+
# 已结束键族(2026-09-19 阶段一:补规范名 `effective_until` 与冗余时刻 `expired_at`)。
|
|
406
|
+
# 纪律:`believed_at`(信念时间)**两族都不入**——它既不是「已结束」也不是「尚未开始」,
|
|
407
|
+
# 而是「体系何时确认此条」的取代/审核锚。键族真源见 md_cg/trust.py(FROM_ALIASES/UNTIL_ALIASES),
|
|
408
|
+
# 两侧新增键须同步(交叉守卫 test_validity_filter)。
|
|
409
|
+
_EXPIRY_KEYS = ("effective_until", "valid_until", "expires_at", "expire_at",
|
|
410
|
+
"expiry", "deadline", "expired_at")
|
|
389
411
|
_SKIP_KEYS = ("功能名", "执行", "条件", "来源", "标签", "状态", "备注", "标题",
|
|
390
412
|
"描述", "name", "id", "title", "layer", "tags", "说明")
|
|
391
413
|
_NEG_WORDS = ("禁止", "不得", "不要", "不能", "切勿", "避免", "不应", "不可")
|
|
392
414
|
_POS_WORDS = ("应当", "建议", "必须", "需要", "可以", "允许", "推荐", "宜")
|
|
393
415
|
|
|
394
416
|
|
|
417
|
+
# 生效条件:v 为 bool 返回 None;v 为 int/float 时仅 f>1e8 返回 f 否则 None;str(v or "").strip() 为空返回 None;否则按 "%Y-%m-%dT%H:%M:%S"/"%Y-%m-%d %H:%M:%S"/"%Y-%m-%d" 依次取前 19/19/10 字符尝试解析,全失败后再试 float(s),>1e8 返回否则 None,float 亦失败返回 None。
|
|
395
418
|
def _to_ts(v):
|
|
396
419
|
"""宽松时间解析:秒级时间戳 / ISO / `YYYY-MM-DD`。无法识别 → None。"""
|
|
397
420
|
if isinstance(v, bool):
|
|
@@ -415,6 +438,7 @@ def _to_ts(v):
|
|
|
415
438
|
return None
|
|
416
439
|
|
|
417
440
|
|
|
441
|
+
# 生效条件:对 (content or "").splitlines() 的每行去 # 后,先试中文全角 ":"、无则试 ":",切出的键长度在 2–12、值非空且键不在 _SKIP_KEYS 时以 setdefault 记录(每键只留首次出现),无合格行返回 {}。
|
|
418
442
|
def _kv_pairs(content) -> dict:
|
|
419
443
|
"""抽 `键:值` 对(中文/英文冒号),跳过结构字段。"""
|
|
420
444
|
out = {}
|
|
@@ -432,12 +456,14 @@ def _kv_pairs(content) -> dict:
|
|
|
432
456
|
return out
|
|
433
457
|
|
|
434
458
|
|
|
459
|
+
# 生效条件:c 取 content or "",含 _NEG_WORDS 任一返回 -1 分量、含 _POS_WORDS 任一返回 +1 分量,结果为两者之和(都不含时为 0)。
|
|
435
460
|
def _polarity(content) -> int:
|
|
436
461
|
c = content or ""
|
|
437
462
|
return (-1 if any(w in c for w in _NEG_WORDS) else 0) + \
|
|
438
463
|
(1 if any(w in c for w in _POS_WORDS) else 0)
|
|
439
464
|
|
|
440
465
|
|
|
466
|
+
# 生效条件:按 _EXPIRY_KEYS 顺序遍历 fm,返回首个满足「键在 fm 中且 _to_ts 非 None 且 ts<now」的 (k, fm.get(k));该键解析为 None 或 ts≥now 时继续检查后续键,全不满足返回 None。
|
|
441
467
|
def _expired(fm, now):
|
|
442
468
|
for k in _EXPIRY_KEYS:
|
|
443
469
|
if k in fm:
|
|
@@ -447,6 +473,25 @@ def _expired(fm, now):
|
|
|
447
473
|
return None
|
|
448
474
|
|
|
449
475
|
|
|
476
|
+
# 未生效键(双时间轴的起点,2026-09-19 · 真源 md_cg/trust.py)。
|
|
477
|
+
# 纪律一:`valid_from` **绝不并入 `_EXPIRY_KEYS`**——两者语义相反(「尚未开始」vs
|
|
478
|
+
# 「已经结束」),并入会让「未来才生效」被误判为「已失效」并触发 weaken/demote。
|
|
479
|
+
# 纪律二:`believed_at`(信念时间)同上,**两族都不入**——第三类语义(体系何时确认)。
|
|
480
|
+
_NOT_YET_KEYS = ("valid_from", "valid_since", "effective_from", "starts_at")
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# 生效条件:按 _NOT_YET_KEYS 顺序遍历 fm,返回首个满足「键在 fm 中且 _to_ts 非 None 且 ts>now」的 (k, fm.get(k));全不满足返回 None。
|
|
484
|
+
def _not_yet(fm, now):
|
|
485
|
+
"""宽松判定「尚未生效」(与 `_expired` 同口径,方向相反)。"""
|
|
486
|
+
for k in _NOT_YET_KEYS:
|
|
487
|
+
if k in fm:
|
|
488
|
+
ts = _to_ts(fm.get(k))
|
|
489
|
+
if ts is not None and ts > now:
|
|
490
|
+
return k, fm.get(k)
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# 生效条件:对 content 取 kv_pairs、bigrams 和 polarity,遍历 related(若 related 为假值则视为空)的前 int(max_compare) 个 r,以 r["node_id"] 调 cg.get;若某 oid 节点可读非空,先在其 content 与 content 的共同键中找到值不同者并返回 {'with':oid,'via':r.get('via'),'why':'同键不同值:...'};否则若极性乘积 <0 且双方 bigram 非空,且共享 bigram 数 >=3 且 ratio>=0.15,返回 {'with':oid,'via':r.get('via'),'why':'极性相反且共享内容:...'};全部遍历完无命中则返回 None;
|
|
450
495
|
def _contradiction(cg, content, related, max_compare=10):
|
|
451
496
|
"""与同族节点比对:同键不同值 / 极性相反且共享 bigram。"""
|
|
452
497
|
kv_a = _kv_pairs(content)
|
|
@@ -476,6 +521,7 @@ def _contradiction(cg, content, related, max_compare=10):
|
|
|
476
521
|
return None
|
|
477
522
|
|
|
478
523
|
|
|
524
|
+
# 生效条件:ids 在 node_ids 为 None 时取索引全部键、为 str 时取单元素列表、否则取 list(node_ids),limit 为真值时截断为前 int(limit) 个;跳过 layer 在 SELF_LAYERS 的节点和 cg.get 取不到正文的节点,对剩余每个节点按 min_severity 门限累加 expired/unverified/orphan_noise/duplicate/contradiction,返回 ok=无 high 且无 medium 的结果。
|
|
479
525
|
def audit(cg, node_ids=None, *, hops: int = 1, min_severity: str = "info",
|
|
480
526
|
limit=None, unverified_days: float = UNVERIFIED_DAYS,
|
|
481
527
|
lexical: bool = True, min_sim: float = 0.15) -> dict:
|
|
@@ -512,6 +558,7 @@ def audit(cg, node_ids=None, *, hops: int = 1, min_severity: str = "info",
|
|
|
512
558
|
content = node.get("content") or ""
|
|
513
559
|
age_d = _days(fm.get("created_at") or e.get("created_at"), now)
|
|
514
560
|
|
|
561
|
+
# 生效条件:kind 是 CONTAMINATION 的键(否则 KeyError)且 CONTAMINATION[kind] 取出的 sev 在 SEVERITY_ORDER 中的值(缺键按 0)不小于闭包变量 min_severity 在 SEVERITY_ORDER 中的值(缺键按 0)时,把 {node_id, layer, kind, severity, detail, fix} 用 **extra 覆盖更新后追加到闭包 issues;sev 的值更小则直接 return 不追加(该函数无返回值)。
|
|
515
562
|
def add(kind, detail, **extra):
|
|
516
563
|
sev, actions = CONTAMINATION[kind]
|
|
517
564
|
if SEVERITY_ORDER.get(sev, 0) < SEVERITY_ORDER.get(min_severity, 0):
|
|
@@ -525,6 +572,12 @@ def audit(cg, node_ids=None, *, hops: int = 1, min_severity: str = "info",
|
|
|
525
572
|
if hit:
|
|
526
573
|
add("expired", f"时效已过:{hit[0]}={hit[1]}", evidence=hit[0])
|
|
527
574
|
|
|
575
|
+
# 双时间轴另一侧(2026-09-19):尚未生效**不是错误**——只提示「此刻不适用」,
|
|
576
|
+
# 处置由 CONTAMINATION["not_yet"] 定为 info 级 + 仅 hint(不 weaken / 不 demote)。
|
|
577
|
+
ny = _not_yet(fm, now)
|
|
578
|
+
if ny:
|
|
579
|
+
add("not_yet", f"尚未生效:{ny[0]}={ny[1]}", evidence=ny[0])
|
|
580
|
+
|
|
528
581
|
if (layer == "knowledge" and age_d >= unverified_days
|
|
529
582
|
and int(e.get("evidence_count") or 0) <= 0):
|
|
530
583
|
add("unverified", f"knowledge 层 {age_d:.0f} 天零验证")
|
|
@@ -562,6 +615,7 @@ def audit(cg, node_ids=None, *, hops: int = 1, min_severity: str = "info",
|
|
|
562
615
|
"t": now}
|
|
563
616
|
|
|
564
617
|
|
|
618
|
+
# 生效条件:循环 max(1, int(times)) 次调用 cg.verify(nid, "scrub:"+kind+":"+str(detail)[:120], "weakened"),返回最后一次调用结果(times≤0 时按 1 次执行)。
|
|
565
619
|
def _weaken(cg, nid, kind, detail, times=1):
|
|
566
620
|
res = None
|
|
567
621
|
for _ in range(max(1, int(times))):
|
|
@@ -569,6 +623,7 @@ def _weaken(cg, nid, kind, detail, times=1):
|
|
|
569
623
|
return res
|
|
570
624
|
|
|
571
625
|
|
|
626
|
+
# 生效条件:kinds 为真且 issue 的 kind 不在其中则跳过;kind 为 duplicate/unverified 记 hint,已出现在 _handled 记 skip,protect.is_protected 为真且 override 为假记 skip_protected,dry_run 为真记 planned;仅 dry_run 为假时对余下 issue 执行 _weaken,并在 demote 为真、severity 为 high 或 low 且当前层非 contextual 时降级到 contextual,返回计数与 actions。
|
|
572
627
|
def decontaminate(cg, node_ids=None, *, kinds=None, dry_run: bool = True,
|
|
573
628
|
min_severity: str = "medium", hops: int = 1, actor=None,
|
|
574
629
|
override: bool = False, weaken_times: int = 1,
|
|
@@ -650,6 +705,7 @@ def decontaminate(cg, node_ids=None, *, kinds=None, dry_run: bool = True,
|
|
|
650
705
|
# ④ 校准偏差
|
|
651
706
|
# --------------------------------------------------------------------------
|
|
652
707
|
|
|
708
|
+
# 生效条件:对 cg 中每个「layer 不在 SELF_LAYERS」且 evidence_count≥int(min_evidence)(min_evidence=0 时该比较恒假而不早退)的节点,若 protect.is_protected 为假或 override 为真,且 cg.get(nid) 未抛异常并返回真值节点,则取 fm.get("confidence", 0.6)(缺键才回落 0.6,键存在为 None/假值不回落)为 old,算出 round(max(0.0, min(0.99, old+float(offset))),4),与 old 差<1e-9 时跳过,否则写 fm["confidence"] 与 fm["calibration"] 并调用 cg._write_node 成功时 adjusted+1(写回异常被吞掉不计数),返回 (adjusted, skipped),其中 skipped 只累计「layer 属 SELF_LAYERS」或被 protect 拦下且非 override 的节点。
|
|
653
709
|
def _apply_offset(cg, offset, *, override=False, min_evidence=1):
|
|
654
710
|
nodes = _nodes(cg)
|
|
655
711
|
adjusted = skipped = 0
|
|
@@ -686,6 +742,7 @@ def _apply_offset(cg, offset, *, override=False, min_evidence=1):
|
|
|
686
742
|
return adjusted, skipped
|
|
687
743
|
|
|
688
744
|
|
|
745
|
+
# 生效条件:cg 和 apply/override/actor/max_offset/min_evidence 传入后,若 metacognition.calibration(cg) 返回 ok 假,则返回 {'ok':False,'reason':cal.get('reason') or 'insufficient_data',...};若 ok 真,则用 gap=float(cal.get('gap') or 0.0) 和 max_offset 计算 offset=round(max(-max_offset,min(max_offset,-gap)),4),对 cal.get('bins') or [] 中 accuracy 非 None 的 bin 生成 bins_bias 并排序;仅当 apply 为真且 abs(offset)>1e-9 时调用 _apply_offset(cg,offset,override=override,min_evidence=min_evidence) 并写日志,最后返回 ok True 及 verdict/gap/建议 offset 等字段;
|
|
689
746
|
def calibrate(cg, *, apply: bool = False, override: bool = False, actor=None,
|
|
690
747
|
max_offset: float = MAX_OFFSET, min_evidence: int = 1) -> dict:
|
|
691
748
|
"""校准偏差:自报置信 vs 实测正确率 → 偏置建议(`apply=True` 才写回)。"""
|
|
@@ -734,6 +791,7 @@ def calibrate(cg, *, apply: bool = False, override: bool = False, actor=None,
|
|
|
734
791
|
# 一轮完整自净 + 审计
|
|
735
792
|
# --------------------------------------------------------------------------
|
|
736
793
|
|
|
794
|
+
# 生效条件:cg 与 n/seed/dry_run/hops/strategy/apply_calibration/actor 传入后,按 n 与 strategy、seed 调 sample;对 sample 结果前 8 个 node_id 按 hops 调 associate;按 dry_run/hops/actor 调 decontaminate;按 apply_calibration/actor 调 calibrate;若 decontaminate 的 audit.issues 中存在 severity 为 high 或 medium 的项则 out.ok 为 False,否则为 True,并返回含 sample/associate/audit/decontaminate/calibration/t 的 out;
|
|
737
795
|
def sweep(cg, *, n: int = DEFAULT_SAMPLE, seed=None, dry_run: bool = True,
|
|
738
796
|
hops: int = DEFAULT_HOPS, strategy: str = "stratified",
|
|
739
797
|
apply_calibration: bool = False, actor=None) -> dict:
|
|
@@ -758,11 +816,13 @@ def sweep(cg, *, n: int = DEFAULT_SAMPLE, seed=None, dry_run: bool = True,
|
|
|
758
816
|
return out
|
|
759
817
|
|
|
760
818
|
|
|
819
|
+
# 生效条件:返回 cg.root 下 SCRUB_LOG 的全部记录条数 n 与 recs[-int(limit):](limit=0 时切片为 recs[0:] 即返回全部记录)。
|
|
761
820
|
def history(cg, limit: int = 100) -> dict:
|
|
762
821
|
recs = list(read_jsonl(os.path.join(cg.root, SCRUB_LOG)))
|
|
763
822
|
return {"n": len(recs), "records": recs[-int(limit):]}
|
|
764
823
|
|
|
765
824
|
|
|
825
|
+
# 生效条件:读取 cg.root 下 SCRUB_LOG 的 JSONL 记录,过滤 op=="sweep" 得 sweeps、op=="decontaminate" 且 ok 为真得 decs;last 为 sweeps 最后一项或 None;返回 {'sweeps':len(sweeps),'decontaminated':len(decs),'last_sweep':last 的 t/n_issues/n_high_medium/applied/dry_run/calibration 或 None};
|
|
766
826
|
def summary(cg) -> dict:
|
|
767
827
|
"""给 health_os / 自维持循环用的只读摘要。"""
|
|
768
828
|
recs = list(read_jsonl(os.path.join(cg.root, SCRUB_LOG)))
|
|
@@ -779,6 +839,7 @@ def summary(cg) -> dict:
|
|
|
779
839
|
if last else None)}
|
|
780
840
|
|
|
781
841
|
|
|
842
|
+
# 生效条件:无入参调用即返回固定的 actions 列表、STRATA 列表、CONTAMINATION 映射(每项取 severity 与 actions)与 STALE_DAYS/UNVERIFIED_DAYS/LOW_CONF/ORPHAN_IMPORTANCE/MAX_OFFSET 阈值字典。
|
|
782
843
|
def catalog() -> dict:
|
|
783
844
|
return {"actions": ["sample", "associate", "audit", "decontaminate",
|
|
784
845
|
"calibrate", "sweep", "history", "summary", "catalog"],
|
|
@@ -789,4 +850,4 @@ def catalog() -> dict:
|
|
|
789
850
|
"unverified_days": UNVERIFIED_DAYS,
|
|
790
851
|
"low_conf": LOW_CONF,
|
|
791
852
|
"orphan_importance": ORPHAN_IMPORTANCE,
|
|
792
|
-
"max_offset": MAX_OFFSET}}
|
|
853
|
+
"max_offset": MAX_OFFSET}}
|
package/md_cg/security.py
CHANGED
|
@@ -28,6 +28,7 @@ class AccessDenied(Exception):
|
|
|
28
28
|
"""权限拒绝(读/写/管理)。"""
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
# 生效条件:形参 level 为模块级常量 SENSITIVITY_ORDER 中的元素时返回其下标,否则抛 AccessDenied。
|
|
31
32
|
def _rank(level: str) -> int:
|
|
32
33
|
try:
|
|
33
34
|
return SENSITIVITY_ORDER.index(level)
|
|
@@ -35,6 +36,7 @@ def _rank(level: str) -> int:
|
|
|
35
36
|
raise AccessDenied(f"未知敏感度/密级:{level}") from None
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
# 生效条件:全部形参均可省略、clearance 默认取模块常量 DEFAULT_SENSITIVITY,构造时先经 _rank(clearance) 校验,随后 session 为假值(含空串)回落为 sess_+uuid 十二位、role 假值回落 "system"、expires_at 为假值(含 0)置 None、layers_allow/ops_allow 为 None 时保持 None 否则转 tuple、theory_ok 经 bool() 转换。
|
|
38
40
|
class Principal:
|
|
39
41
|
"""调用方身份(一次会话一个)。
|
|
40
42
|
|
|
@@ -65,6 +67,7 @@ class Principal:
|
|
|
65
67
|
theory_version —— 当前声明的协议版本(审计与 whoami 用)
|
|
66
68
|
"""
|
|
67
69
|
|
|
70
|
+
# 生效条件:clearance 须为模块级常量 SENSITIVITY_ORDER 成员(否则 _rank 抛 AccessDenied),session 为假值(含 None/空串)时生成 sess_ 随机串,expires_at 为假值(含 None/0)时存 None 否则 float(expires_at),layers_allow/ops_allow 为 None 时存 None 否则 tuple 化。
|
|
68
71
|
def __init__(self, tenant: str = "default", actor: str = "system",
|
|
69
72
|
clearance: str = DEFAULT_SENSITIVITY, can_write: bool = True,
|
|
70
73
|
can_admin: bool = False, session: str = None,
|
|
@@ -99,31 +102,38 @@ class Principal:
|
|
|
99
102
|
|
|
100
103
|
# ---------- 基础判定 ----------
|
|
101
104
|
|
|
105
|
+
# 生效条件:形参 sensitivity 与 self.clearance 均可被 _rank 映射到模块级常量 SENSITIVITY_ORDER 中时,返回前者排名是否不高于后者;任一不在其中则 _rank 抛 AccessDenied。
|
|
102
106
|
def allows(self, sensitivity: str) -> bool:
|
|
103
107
|
"""clearance 是否覆盖该敏感度(可读/可写)。"""
|
|
104
108
|
return _rank(sensitivity) <= _rank(self.clearance)
|
|
105
109
|
|
|
110
|
+
# 生效条件:无 required 形参或模块级常量前置,仅当 self.expires_at(来自 __init__ 的 expires_at)不为 None 且 time.time() 大于它时返回 True,否则返回 False。
|
|
106
111
|
def expired(self) -> bool:
|
|
107
112
|
return self.expires_at is not None and time.time() > self.expires_at
|
|
108
113
|
|
|
109
114
|
@staticmethod
|
|
115
|
+
# 生效条件:allow 为 None 时返回 True(未声明=不限制),allow 非 None 时返回 "'*' in allow 或 name in allow" 的布尔结果。
|
|
110
116
|
def _in_scope(allow, name: str) -> bool:
|
|
111
117
|
if allow is None: # 未声明 = 不限制(兼容直接构造)
|
|
112
118
|
return True
|
|
113
119
|
return "*" in allow or name in allow
|
|
114
120
|
|
|
121
|
+
# 生效条件:layer 为假值(None/空串)时以 "knowledge" 参与判定,返回 self._in_scope(self.layers_allow, layer or "knowledge") 的结果。
|
|
115
122
|
def allows_layer(self, layer: str) -> bool:
|
|
116
123
|
return self._in_scope(self.layers_allow, layer or "knowledge")
|
|
117
124
|
|
|
125
|
+
# 生效条件:op 为假值(None/空串)时以 "" 参与,先经 strip().lower() 归一化,返回 self._in_scope(self.ops_allow, (op or "").strip().lower()) 的结果。
|
|
118
126
|
def allows_op(self, op: str) -> bool:
|
|
119
127
|
return self._in_scope(self.ops_allow, (op or "").strip().lower())
|
|
120
128
|
|
|
121
129
|
# ---------- 强制校验(越权即 AccessDenied) ----------
|
|
122
130
|
|
|
131
|
+
# 生效条件:无 required 形参或模块级常量前置,当 self.expired()(来自 __init__ 的 expires_at 与当前时间比较)为 True 时抛 AccessDenied,否则无操作返回 None。
|
|
123
132
|
def _require_live(self):
|
|
124
133
|
if self.expired():
|
|
125
134
|
raise AccessDenied(f"actor={self.actor} 令牌已过期")
|
|
126
135
|
|
|
136
|
+
# 生效条件:先调用 self._require_live()(过期则抛 AccessDenied);再要求 self.allows_op(op) 为 True,否则抛 AccessDenied。
|
|
127
137
|
def require_op(self, op: str):
|
|
128
138
|
self._require_live()
|
|
129
139
|
if not self.allows_op(op):
|
|
@@ -131,6 +141,7 @@ class Principal:
|
|
|
131
141
|
f"角色 {self.role} 无权执行 op={op}(作用域 "
|
|
132
142
|
f"{list(self.ops_allow) if self.ops_allow is not None else '不限'})")
|
|
133
143
|
|
|
144
|
+
# 生效条件:依次要求 self._require_live() 未抛异常、self.theory_ok 为 True、self.can_write 为 True、self.allows(sensitivity) 为 True;任一不满足则抛 AccessDenied。
|
|
134
145
|
def require_write(self, sensitivity: str):
|
|
135
146
|
self._require_live()
|
|
136
147
|
if not self.theory_ok:
|
|
@@ -143,6 +154,7 @@ class Principal:
|
|
|
143
154
|
raise AccessDenied(
|
|
144
155
|
f"写入敏感度 {sensitivity} 超出 clearance {self.clearance}")
|
|
145
156
|
|
|
157
|
+
# 生效条件:先要求 self.require_write(sensitivity) 未抛异常;随后将形参 layer 为假值(None/空串)时按 "knowledge" 处理,并要求 self.allows_layer(layer) 为 True,否则抛 AccessDenied。
|
|
146
158
|
def require_layer_write(self, layer: str, sensitivity: str):
|
|
147
159
|
"""写层校验:密级 + 层白名单双闸门(核心私有内容不可越权修改)。"""
|
|
148
160
|
self.require_write(sensitivity)
|
|
@@ -152,6 +164,7 @@ class Principal:
|
|
|
152
164
|
f"角色 {self.role} 无权写入 {layer} 层"
|
|
153
165
|
f"(可写层 {list(self.layers_allow) if self.layers_allow is not None else '不限'})")
|
|
154
166
|
|
|
167
|
+
# 生效条件:要求 self._require_live() 未抛异常、self.theory_ok 为 True、self.can_admin 为 True 时通过;否则抛 AccessDenied(消息用形参 op 标记操作)。
|
|
155
168
|
def require_admin(self, op: str):
|
|
156
169
|
self._require_live()
|
|
157
170
|
if not self.theory_ok:
|
|
@@ -160,6 +173,7 @@ class Principal:
|
|
|
160
173
|
if not self.can_admin:
|
|
161
174
|
raise AccessDenied(f"actor={self.actor} 无管理权限({op})")
|
|
162
175
|
|
|
176
|
+
# 生效条件:无 required 形参或模块级常量前置,返回包含 self 各属性(tenant/actor/clearance/can_write/can_admin/session/harness/unit/role/token_id/parent/auth_mode/expires_at/theory_ok/theory_version 及 layers_allow/ops_allow)的 dict;其中 layers_allow/ops_allow 为 None 时值为 None,否则 list 化。
|
|
163
177
|
def as_dict(self):
|
|
164
178
|
return {"tenant": self.tenant, "actor": self.actor,
|
|
165
179
|
"clearance": self.clearance, "can_write": self.can_write,
|
|
@@ -175,12 +189,14 @@ class Principal:
|
|
|
175
189
|
"ops_allow": (None if self.ops_allow is None
|
|
176
190
|
else list(self.ops_allow))}
|
|
177
191
|
|
|
192
|
+
# 生效条件:无前置;仅返回 tenant/actor/role/clearance/write/admin 的短摘要用于日志与排障,不含 token、密钥材料与能力白名单明细;
|
|
178
193
|
def __repr__(self):
|
|
179
194
|
return (f"Principal(tenant={self.tenant!r}, actor={self.actor!r}, "
|
|
180
195
|
f"role={self.role!r}, clearance={self.clearance!r}, "
|
|
181
196
|
f"write={self.can_write}, admin={self.can_admin})")
|
|
182
197
|
|
|
183
198
|
|
|
199
|
+
# 生效条件:path 为 None 时落至 os.path.expanduser("~") 下的 .mdcg/_tenants.json,path 非 None(含空串)时按传入值使用,并在构造内以 self._load() 的返回填充 self.data。
|
|
184
200
|
class TenantRegistry:
|
|
185
201
|
"""租户注册表:tenant → {root, clearance_cap, description}。
|
|
186
202
|
|
|
@@ -188,12 +204,14 @@ class TenantRegistry:
|
|
|
188
204
|
设计意图:私有租户的 root 指向仓库外目录,开源仓库里只放 public 租户的根。
|
|
189
205
|
"""
|
|
190
206
|
|
|
207
|
+
# 生效条件:形参 path 为 None 时取 ~/.mdcg/_tenants.json,否则取 path;self.data 初始化为 _load() 结果(self.path 经 os.path.exists 为真且 JSON 解析为 dict 时取该 dict,否则回落 {"schema":1,"tenants":{}})。
|
|
191
208
|
def __init__(self, path: str = None):
|
|
192
209
|
if path is None:
|
|
193
210
|
path = os.path.join(os.path.expanduser("~"), ".mdcg", "_tenants.json")
|
|
194
211
|
self.path = path
|
|
195
212
|
self.data = self._load()
|
|
196
213
|
|
|
214
|
+
# 生效条件:当 self.path 经 os.path.exists 为真且内容可解析为 dict 时返回该 dict;否则(os.path.exists 为假、非 dict、JSON 解析失败或 OSError)返回 {"schema":1,"tenants":{}}。
|
|
197
215
|
def _load(self):
|
|
198
216
|
if os.path.exists(self.path):
|
|
199
217
|
try:
|
|
@@ -205,6 +223,7 @@ class TenantRegistry:
|
|
|
205
223
|
pass
|
|
206
224
|
return {"schema": 1, "tenants": {}}
|
|
207
225
|
|
|
226
|
+
# 生效条件:无 required 形参或模块级常量前置,将 self.data 以 JSON 写入 self.path + ".tmp",随后 os.replace 到 self.path;目录名称为空时用 "." 创建。
|
|
208
227
|
def _save(self):
|
|
209
228
|
os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True)
|
|
210
229
|
tmp = self.path + ".tmp"
|
|
@@ -212,6 +231,7 @@ class TenantRegistry:
|
|
|
212
231
|
json.dump(self.data, f, ensure_ascii=False, indent=1)
|
|
213
232
|
os.replace(tmp, self.path)
|
|
214
233
|
|
|
234
|
+
# 生效条件:形参 clearance_cap 须为模块级常量 SENSITIVITY_ORDER 成员(否则 _rank 抛 AccessDenied);形参 tenant/root 提供后写入 self.data["tenants"](要求 self.data 含 "tenants" 键,否则 KeyError),_save 成功则返回新登记项。
|
|
215
235
|
def register(self, tenant: str, root: str, clearance_cap: str = DEFAULT_SENSITIVITY,
|
|
216
236
|
description: str = ""):
|
|
217
237
|
_rank(clearance_cap)
|
|
@@ -224,20 +244,25 @@ class TenantRegistry:
|
|
|
224
244
|
self._save()
|
|
225
245
|
return self.data["tenants"][tenant]
|
|
226
246
|
|
|
247
|
+
# 生效条件:self.data 含 "tenants" 键(否则 KeyError),且该映射中存在形参 tenant 时返回其值,否则返回 None(.get 缺键回落 None,键存在值为 None 也返回 None)。
|
|
227
248
|
def get(self, tenant: str):
|
|
228
249
|
return self.data["tenants"].get(tenant)
|
|
229
250
|
|
|
251
|
+
# 生效条件:self.get(tenant) 返回真值(非 None/空 dict 等)时返回 t["root"](若 t 无 "root" 键则 KeyError);返回假值时返回 None。
|
|
230
252
|
def root_of(self, tenant: str):
|
|
231
253
|
t = self.get(tenant)
|
|
232
254
|
return t["root"] if t else None
|
|
233
255
|
|
|
256
|
+
# 生效条件:self.get(tenant) 返回真值时返回 t["clearance_cap"](缺键则 KeyError);返回假值时返回模块级常量 DEFAULT_SENSITIVITY。
|
|
234
257
|
def cap_of(self, tenant: str):
|
|
235
258
|
t = self.get(tenant)
|
|
236
259
|
return t["clearance_cap"] if t else DEFAULT_SENSITIVITY
|
|
237
260
|
|
|
261
|
+
# 生效条件:self.data 含 "tenants" 键时返回其浅拷贝 dict(self.data["tenants"]);该键缺失时按 self.data["tenants"] 取值会 KeyError,无默认回落。
|
|
238
262
|
def all(self):
|
|
239
263
|
return dict(self.data["tenants"])
|
|
240
264
|
|
|
265
|
+
# 生效条件:cap 由 cap_of(tenant) 决定(形参 tenant 未注册时取模块级常量 DEFAULT_SENSITIVITY);clearance 为假值(含 None/空串)时 want 取 cap,否则先取 clearance,再在 _rank(want) > _rank(cap) 时夹紧为 cap;actor 为假值时取 tenant;返回 Principal(...)。
|
|
241
266
|
def principal_for(self, tenant: str, actor: str = None, clearance: str = None,
|
|
242
267
|
can_write: bool = True, can_admin: bool = False,
|
|
243
268
|
session: str = None) -> Principal:
|
|
@@ -247,4 +272,4 @@ class TenantRegistry:
|
|
|
247
272
|
if _rank(want) > _rank(cap):
|
|
248
273
|
want = cap
|
|
249
274
|
return Principal(tenant=tenant, actor=actor or tenant, clearance=want,
|
|
250
|
-
can_write=can_write, can_admin=can_admin, session=session)
|
|
275
|
+
can_write=can_write, can_admin=can_admin, session=session)
|