@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/mdcg.py
CHANGED
|
@@ -26,7 +26,7 @@ import hashlib
|
|
|
26
26
|
import threading
|
|
27
27
|
|
|
28
28
|
from . import (nodefile, protect, routing, subgraph, chain, provenance, pooling,
|
|
29
|
-
lifecycle)
|
|
29
|
+
lifecycle, reach, trust, roleviews)
|
|
30
30
|
from .fsutil import (FileLock, ShardedLog, atomic_write, append_jsonl,
|
|
31
31
|
read_jsonl, sweep_stale_temps)
|
|
32
32
|
|
|
@@ -38,6 +38,33 @@ LAYERS = ("anchor", "structural", "knowledge", "contextual", "self",
|
|
|
38
38
|
"rejected", "unresolved", "goals")
|
|
39
39
|
# 有条件分区的层:knowledge 是主检索层;负记忆/目标目录按自己的 MARKS 走,不路由
|
|
40
40
|
BUCKETED_LAYERS = ("knowledge",)
|
|
41
|
+
|
|
42
|
+
# ---- S4 层级激活优先级(契约 §3 S4)----
|
|
43
|
+
# 认知优先级:anchor/self(自我与锚点)先激活,其次 structural,再次 knowledge/contextual。
|
|
44
|
+
# 语义是**加成**而非过滤:只改初始激活值/排序,不改变任何门控(不会把被门控剔除的节点拉回来)。
|
|
45
|
+
LAYER_BOOST_DEFAULT = {
|
|
46
|
+
"anchor": 0.20, "self": 0.20, "structural": 0.15,
|
|
47
|
+
"knowledge": 0.10, "contextual": 0.05,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# 生效条件:env 为假值(None/空串)时返回 LAYER_BOOST_DEFAULT 的副本;否则按「层=值,层=值」解析,
|
|
52
|
+
# 忽略无等号的项与不可转 float 的值(负值夹到 0.0),未出现的层保留默认值,返回合并后的 dict。
|
|
53
|
+
def layer_boosts(env=None) -> dict:
|
|
54
|
+
"""层级加成表:`MDCG_LAYER_BOOST="anchor=0.3,knowledge=0.05"`(部分覆盖,未提及的层取默认)。"""
|
|
55
|
+
out = dict(LAYER_BOOST_DEFAULT)
|
|
56
|
+
raw = env if env is not None else os.environ.get("MDCG_LAYER_BOOST", "")
|
|
57
|
+
if not raw:
|
|
58
|
+
return out
|
|
59
|
+
for part in str(raw).split(","):
|
|
60
|
+
if "=" not in part:
|
|
61
|
+
continue
|
|
62
|
+
k, v = part.split("=", 1)
|
|
63
|
+
try:
|
|
64
|
+
out[k.strip()] = max(0.0, float(v.strip()))
|
|
65
|
+
except ValueError:
|
|
66
|
+
continue
|
|
67
|
+
return out
|
|
41
68
|
# 负记忆的 MARKS 必填(与 knowledge 的 5 要素不同)
|
|
42
69
|
NEG_MEMORY_MARKS = {
|
|
43
70
|
"rejected": ("假设", "否决原因", "验证"),
|
|
@@ -73,6 +100,7 @@ _LIVE_CGS = weakref.WeakSet()
|
|
|
73
100
|
_ATEXIT_HOOK = None
|
|
74
101
|
|
|
75
102
|
|
|
103
|
+
# 生效条件:当模块级 `_LIVE_CGS` 含实例时,函数对每个实例调用 close() 并吞掉异常,不返回值。
|
|
76
104
|
def _atexit_flush_all():
|
|
77
105
|
"""进程退出兜底:把仍存活实例的脏索引落盘(异常吞掉——退出路径不该再抛)。"""
|
|
78
106
|
for cg in list(_LIVE_CGS):
|
|
@@ -82,6 +110,7 @@ def _atexit_flush_all():
|
|
|
82
110
|
pass
|
|
83
111
|
|
|
84
112
|
|
|
113
|
+
# 生效条件:当模块级 `_ATEXIT_HOOK` 为 None 时注册 `_atexit_flush_all` 并缓存为 `_ATEXIT_HOOK`,否则直接返回已缓存的 `_ATEXIT_HOOK`。
|
|
85
114
|
def _register_atexit_hook():
|
|
86
115
|
global _ATEXIT_HOOK
|
|
87
116
|
if _ATEXIT_HOOK is None:
|
|
@@ -90,6 +119,7 @@ def _register_atexit_hook():
|
|
|
90
119
|
return _ATEXIT_HOOK
|
|
91
120
|
|
|
92
121
|
|
|
122
|
+
# 生效条件:当 `len(scored)` 与 `len(docs)` 不一致时,该分支不按 `scored` 排序而直接返回 `cut_report(docs, total, ...)` 并在 `stat` 非 None 时标记 `insert_fallback`;二者同长时按 `scored` 排序后截断到 `total` 并返回 `cut_report(ranked, total, ...)`。
|
|
93
123
|
def cut_by_relevance(docs, scored, total, pools=None, key_of=None, stat=None):
|
|
94
124
|
"""候选截断:**先按相关度排序,再截断到 `total`**(截断依据=相关度)。
|
|
95
125
|
|
|
@@ -116,7 +146,13 @@ def cut_by_relevance(docs, scored, total, pools=None, key_of=None, stat=None):
|
|
|
116
146
|
range(len(docs)),
|
|
117
147
|
key=lambda i: (-float(scored[i][1]),
|
|
118
148
|
-float(scored[i][0]["frontmatter"].get("importance") or 0),
|
|
119
|
-
-float(scored[i][0]["frontmatter"].get("created_at") or 0)
|
|
149
|
+
-float(scored[i][0]["frontmatter"].get("created_at") or 0),
|
|
150
|
+
# 终键:三级全等时按 nid 定序。没有它,sorted 的**稳定性**
|
|
151
|
+
# 会把顺序回落到输入序(docs = index["nodes"] 物理序),
|
|
152
|
+
# 于是「同分同权重同写入时刻」的结果顺序随写入序/重建序漂移
|
|
153
|
+
# —— 索引重建前后不可复现(S6 幂等实测:created_at 被量化到
|
|
154
|
+
# ~1ms,4 次快速写入常两两碰撞,导致 ~50% 概率重建后换序)。
|
|
155
|
+
str(scored[i][0].get("id") or "")))
|
|
120
156
|
ranked = [docs[i] for i in order]
|
|
121
157
|
if stat is not None:
|
|
122
158
|
stat["cut_order"] = "relevance"
|
|
@@ -165,6 +201,8 @@ TIER_BUCKET_LIKE = "T0_bucket_like"
|
|
|
165
201
|
TIER_BUCKET_SCAN = "T1_bucket_scan"
|
|
166
202
|
TIER_GLOBAL_LIKE = "T2_global_like"
|
|
167
203
|
TIER_GLOBAL_SCAN = "T3_global_scan"
|
|
204
|
+
# S3 图扩散激活(契约 §3 S3):从词法命中节点沿 edges 扩散得到的候选,单独成层以便审计
|
|
205
|
+
TIER_SPREAD = "T2b_spread_activation"
|
|
168
206
|
|
|
169
207
|
# 资格判定四态(该不该用——白箱第 1 篇核心)
|
|
170
208
|
STATE_ACCEPT = "ACCEPT" # 条件满足,有资格执行
|
|
@@ -180,6 +218,7 @@ VERIFICATION_BASIS = nodefile.VERIFICATION_BASIS
|
|
|
180
218
|
_EN_ZH_PRONOUNS = {"我", "你", "他", "她", "它", "我们", "你们", "他们"}
|
|
181
219
|
|
|
182
220
|
|
|
221
|
+
# 生效条件:无 required 形参,锚定环境变量名 MDCG_SEMANTIC;当 os.environ.get("MDCG_SEMANTIC") == "1" 时返回 True,否则返回 False。
|
|
183
222
|
def semantic_on() -> bool:
|
|
184
223
|
"""语义摘要路开关(MDCG_SEMANTIC=1,默认关闭零回归)。
|
|
185
224
|
|
|
@@ -193,6 +232,7 @@ def semantic_on() -> bool:
|
|
|
193
232
|
return os.environ.get("MDCG_SEMANTIC") == "1"
|
|
194
233
|
|
|
195
234
|
|
|
235
|
+
# 生效条件:当 `text` 含英文字母且环境变量 `MDCG_EN_ATOMS` 为 `'1'` 且 `normalize_en_query` 可用时,返回小写化并过滤后的中文语素列表;`text` 无字母、开关未开或归一化异常时返回 `[]`。
|
|
196
236
|
def en_zh_terms(text: str) -> list:
|
|
197
237
|
"""原子级中英归一 v1(2026-09-13 管线接入):英文词 → 中文语素召回词。
|
|
198
238
|
|
|
@@ -229,6 +269,7 @@ def en_zh_terms(text: str) -> list:
|
|
|
229
269
|
if t and (len(t) >= 2 or t not in _EN_ZH_PRONOUNS)]
|
|
230
270
|
|
|
231
271
|
|
|
272
|
+
# 生效条件:当 `text` 经 `en_zh_terms` 产出非空语素时,返回长度≥2 的语素集合与相邻中文语素拼接的 char-bigram 并集;语素为空时返回空集。
|
|
232
273
|
def en_zh_bigrams(text: str) -> set:
|
|
233
274
|
"""英→中语素的打分侧补充:中文语素进 query bigram 集合。
|
|
234
275
|
|
|
@@ -259,6 +300,7 @@ def en_zh_bigrams(text: str) -> set:
|
|
|
259
300
|
return grams
|
|
260
301
|
|
|
261
302
|
|
|
303
|
+
# 生效条件:当 `query` 为字符串时,返回整句、≥2 字符分词、同义词组展开、`cn_recall_grams` 及(若 `MDCG_EN_ATOMS=1`)英→中语素去重后的列表;无扩展项时至少含归一化整句。
|
|
262
304
|
def expand_query_terms(query: str) -> list:
|
|
263
305
|
"""与 aeis.core 同实现:整句 + 分词(≥2字符)+ 同义词组展开 + 英文归一化
|
|
264
306
|
+ 英→中语素召回扩展(en_zh_terms,MDCG_EN_ATOMS=1 开启,默认关)。"""
|
|
@@ -287,6 +329,7 @@ def expand_query_terms(query: str) -> list:
|
|
|
287
329
|
return list(dict.fromkeys(terms))
|
|
288
330
|
|
|
289
331
|
|
|
332
|
+
# 生效条件:当 `s` 为字符串时,先去掉空白字符;去空白后长度≤1 返回 `{s}`,否则返回所有相邻 2-gram 集合。
|
|
290
333
|
def bigrams(s: str) -> set:
|
|
291
334
|
s = "".join(s.split())
|
|
292
335
|
if len(s) <= 1:
|
|
@@ -301,6 +344,7 @@ CN_STOP_GRAMS = {
|
|
|
301
344
|
}
|
|
302
345
|
|
|
303
346
|
|
|
347
|
+
# 生效条件:当 query 非空且环境变量 MDCG_CN_GRAMS 不为 "0" 时,对 query 中长度 ≥ max(min_run,2) 的连续中文串切 2-gram,去重且排除 CN_STOP_GRAMS,最多 cap 个返回;MDCG_CN_GRAMS=="0" 或 query 为假值时返回 [](cap<=0 仍会 append 后立即返回首个 gram)。
|
|
304
348
|
def cn_recall_grams(query: str, min_run: int = 4, cap: int = 16) -> list:
|
|
305
349
|
"""构词法 v1 · 中文召回扩展:把连续中文串切成 2-gram 作为额外召回键。
|
|
306
350
|
|
|
@@ -362,6 +406,7 @@ EN_IRREGULAR = {
|
|
|
362
406
|
"dealt": "deal", "lent": "lend", "bent": "bend",
|
|
363
407
|
}
|
|
364
408
|
|
|
409
|
+
# 生效条件:当 `w` 为英文单词字符串时,先查 `EN_IRREGULAR` 映射,否则按保守后缀规则(-ing/-ed/-ies/-es/-s 等长度阈值)返回归一化词形;不适用规则时原样返回 `w`。
|
|
365
410
|
def strip_tense_en(w: str) -> str:
|
|
366
411
|
"""英文时态/复数归零(保守策略:宁可少剥不可误剥)"""
|
|
367
412
|
if w in EN_IRREGULAR:
|
|
@@ -383,6 +428,7 @@ def strip_tense_en(w: str) -> str:
|
|
|
383
428
|
return w[:-1]
|
|
384
429
|
return w
|
|
385
430
|
|
|
431
|
+
# 生效条件:给定 text(假值按 "" 处理),先清除非中文/空格/字母数字字符,再对长度 ≥2 的英文词小写、若在 EN_STOPWORDS 中剔除否则 strip_tense_en 去时态复数,压缩空白后返回;中文保持不变。
|
|
386
432
|
def normalize_en(text: str) -> str:
|
|
387
433
|
"""英文归一化:小写 + 去停用词 + 去时态复数。中文部分不动。
|
|
388
434
|
|
|
@@ -393,6 +439,7 @@ def normalize_en(text: str) -> str:
|
|
|
393
439
|
"""
|
|
394
440
|
# 清标点(保留中文字符和空格和数字)
|
|
395
441
|
cleaned = re.sub(r'[^\u4e00-\u9fff a-zA-Z0-9]', ' ', text or "")
|
|
442
|
+
# 生效条件:m.group(0) 小写后不在模块级常量 EN_STOPWORDS 中时返回 strip_tense_en(该小写词),命中 EN_STOPWORDS 时返回空串 "";
|
|
396
443
|
def _norm_word(m):
|
|
397
444
|
w = m.group(0).lower()
|
|
398
445
|
if w in EN_STOPWORDS:
|
|
@@ -424,6 +471,7 @@ SCORE_MODES = ("legacy", "jaccard")
|
|
|
424
471
|
SCORE_MODE = os.environ.get("MDCG_SCORE_MODE") or "legacy"
|
|
425
472
|
|
|
426
473
|
|
|
474
|
+
# 生效条件:当 `qb` 与 `nb` 均为非空集合时,若 `mode` 或模块级 `SCORE_MODE` 为 `'jaccard'` 返回交集/并集,否则返回交集/len(`qb`);任一为空返回 0.0。
|
|
427
475
|
def lexical_sim(qb: set, nb: set, mode: str = None) -> float:
|
|
428
476
|
"""查询/文档二元组集合的相似度。mode 缺省取模块级 `SCORE_MODE`。
|
|
429
477
|
|
|
@@ -438,6 +486,7 @@ def lexical_sim(qb: set, nb: set, mode: str = None) -> float:
|
|
|
438
486
|
return inter / len(qb)
|
|
439
487
|
|
|
440
488
|
|
|
489
|
+
# 生效条件:当 `query` 为字符串时,返回含整句、≥2 字符分词及命中 `SYNONYM_GROUPS_WEIGHTED` 组加权项(同名取最大权重)的字典;空串返回 `{}`。
|
|
441
490
|
def expand_query_terms_weighted(query: str) -> dict:
|
|
442
491
|
"""分级版查询扩展:返回 {词: 隶属度},隶属度 ∈ (0, 1]。
|
|
443
492
|
|
|
@@ -449,6 +498,7 @@ def expand_query_terms_weighted(query: str) -> dict:
|
|
|
449
498
|
q = query or ""
|
|
450
499
|
weights = {}
|
|
451
500
|
|
|
501
|
+
# 生效条件:w 为真值且 float(v) 严格大于 weights 中 w 的现有值(w 不在 weights 时基准为 0.0)才写入 weights[w];w 为 ""/0/None 等假值或 float(v) 不大于现有值时不做任何写入;
|
|
452
502
|
def put(w, v):
|
|
453
503
|
if w and float(v) > weights.get(w, 0.0):
|
|
454
504
|
weights[w] = float(v)
|
|
@@ -477,6 +527,7 @@ _LLM_EXPAND_PROMPT = (
|
|
|
477
527
|
)
|
|
478
528
|
|
|
479
529
|
|
|
530
|
+
# 生效条件:当 raw(假值按 "" 处理)去空白后首个 "[" 位置 i 满足 0 ≤ i 且末个 "]" 位置 j > i 时,返回 json.loads(s[i:j+1]);否则返回 None。
|
|
480
531
|
def _extract_json_array(raw: str):
|
|
481
532
|
"""从 LLM 输出里抠出第一个 JSON 数组(容忍前后废话/代码围栏)。"""
|
|
482
533
|
s = (raw or "").strip()
|
|
@@ -486,6 +537,7 @@ def _extract_json_array(raw: str):
|
|
|
486
537
|
return json.loads(s[i:j + 1])
|
|
487
538
|
|
|
488
539
|
|
|
540
|
+
# 生效条件:当 `query` 非空时,若 `cache` 传入且含该查询则返回其副本;若 `llm_fn` 为 None 返回带 `__source__='whitebox'` 的白箱加权字典;若 `llm_fn` 返回可解析 JSON 数组则合并至多 `max_terms` 项并标 `'llm'`;异常时回退白箱并标 `'whitebox_fallback'`;空 `query` 返回 `{}`。
|
|
489
541
|
def expand_query_terms_llm(query: str, llm_fn=None, cache=None,
|
|
490
542
|
max_terms: int = 12) -> dict:
|
|
491
543
|
"""LLM 查询侧扩展(黑箱只在**查询时刻**,索引侧全程白箱)。
|
|
@@ -541,7 +593,64 @@ def expand_query_terms_llm(query: str, llm_fn=None, cache=None,
|
|
|
541
593
|
return dict(base)
|
|
542
594
|
|
|
543
595
|
|
|
596
|
+
# 生效条件:enabled 为假值(默认取 MDCG_RETRIEVAL_PIPELINE=="1")时无条件删除 entry 中 big_domain / observation_position 两键并返回 entry;
|
|
597
|
+
# enabled 为真值时仅删除其中假值的键(真值原样保留)并返回 entry。
|
|
598
|
+
def _strip_empty_gate_fields(entry, enabled=None):
|
|
599
|
+
"""索引快照里的门控可选字段落键策略。
|
|
600
|
+
|
|
601
|
+
默认关闭(总开关未设)→ 两个键**一律不落**:索引条目与 search() 返回的候选 entry
|
|
602
|
+
形状与改动前逐字节一致(否则真实库中大量节点的 observation_position 会平铺进
|
|
603
|
+
候选,改变默认口径——独立复核 2026-09-19 指出)。
|
|
604
|
+
开启后 → 真值才落键(假值不落)。
|
|
605
|
+
"""
|
|
606
|
+
if enabled is None:
|
|
607
|
+
enabled = os.environ.get("MDCG_RETRIEVAL_PIPELINE") == "1"
|
|
608
|
+
for _k in ("big_domain", "observation_position"):
|
|
609
|
+
if _k in entry and (not enabled or not entry[_k]):
|
|
610
|
+
del entry[_k]
|
|
611
|
+
return entry
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
# 生效条件:entry 与其 frontmatter 的 condition_space 取 cs(缺键/假值按 {});ctx 为假值或非 dict 时无条件返回 True;
|
|
615
|
+
# ctx 与 cs 同时给出非空 observation_position 且 routing.normalize_domain 归一化后不同时返回 False;
|
|
616
|
+
# ctx 与 cs 同时给出长度 2 的 time_window 且两区间不相交时返回 False(区间相交或任一侧缺值/不可转 float 一律返回 True)。
|
|
617
|
+
def _cond_prefilter_pass(entry, ctx) -> bool:
|
|
618
|
+
"""S2 条件空间前置门控:**不读正文**即可判定的硬槽(契约 §3 S2)。
|
|
619
|
+
|
|
620
|
+
铁律(宁多勿漏):只剔除**明确不匹配**;任一侧信息不足一律放行。
|
|
621
|
+
· observation_position:双方都有且归一化后不同 → 不匹配;
|
|
622
|
+
· time_window:双方都有且**区间不相交** → 不匹配(相交即放行,
|
|
623
|
+
否则缺省窗口 [created_at, created_at+WINDOW] 会把历史观测整片判死);
|
|
624
|
+
· 其余槽(existence_constraint / observation_tool 等语义判定)留在 judge_qualification(后置)。
|
|
625
|
+
"""
|
|
626
|
+
if not isinstance(ctx, dict):
|
|
627
|
+
return True
|
|
628
|
+
# 索引快照把可判定硬槽平铺在 entry 上(免读文件);兼容直接传 condition_space 的调用方
|
|
629
|
+
entry = entry or {}
|
|
630
|
+
cs = entry.get("condition_space") or {}
|
|
631
|
+
want = ctx.get("observation_position") or ""
|
|
632
|
+
have = entry.get("observation_position") or cs.get("observation_position") or ""
|
|
633
|
+
if want and have:
|
|
634
|
+
try:
|
|
635
|
+
if routing.normalize_domain(str(want)) != routing.normalize_domain(str(have)):
|
|
636
|
+
return False
|
|
637
|
+
except Exception:
|
|
638
|
+
pass
|
|
639
|
+
# 索引快照把 time_window 平铺在 entry 上(不在 condition_space 里)——直接读 cs 会让 S2 时间门控永不生效
|
|
640
|
+
cw, nw = ctx.get("time_window"), (entry.get("time_window") or cs.get("time_window"))
|
|
641
|
+
if (isinstance(cw, (list, tuple)) and len(cw) == 2
|
|
642
|
+
and isinstance(nw, (list, tuple)) and len(nw) == 2):
|
|
643
|
+
try:
|
|
644
|
+
if float(cw[1]) < float(nw[0]) or float(nw[1]) < float(cw[0]):
|
|
645
|
+
return False
|
|
646
|
+
except (TypeError, ValueError):
|
|
647
|
+
return True
|
|
648
|
+
return True
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
# 生效条件:构造须传入 root,经 os.path.abspath 后以 exist_ok=True 创建该目录及 LAYERS 各层子目录;autoflush 无论取值(默认 64)都原样赋给实例。
|
|
544
652
|
class MdCG:
|
|
653
|
+
# 生效条件:root 传参即被 os.path.abspath 绝对化并 makedirs(exist_ok=True) 建立 root 与模块级 LAYERS 各层目录,autoflush(默认 64,含 0 等假值)原样存入 self.autoflush,随后 _load_index() 载入索引、sweep_stale_temps(self.root) 清扫,并把 self 登记进模块级 _LIVE_CGS;
|
|
545
654
|
def __init__(self, root: str, autoflush: int = 64):
|
|
546
655
|
self.root = os.path.abspath(root)
|
|
547
656
|
os.makedirs(self.root, exist_ok=True)
|
|
@@ -567,6 +676,7 @@ class MdCG:
|
|
|
567
676
|
|
|
568
677
|
# ---------- 索引(派生物,可重建) ----------
|
|
569
678
|
|
|
679
|
+
# 生效条件:os.path.exists(self.index_path) 为真、json.load 成功且其 "schema" 等于模块级 SCHEMA 时以该快照为基底,否则以 self._scan_nodes() 结果与空 buckets 新建;随后重放 ShardedLog.read_all(self.index_log_dir):记录无 id 跳过、e 为 None 则 pop 该 nid(tombstone)、e 非 None 则覆盖,最终 buckets 由 _count_buckets 重算;
|
|
570
680
|
def _load_index(self):
|
|
571
681
|
idx = None
|
|
572
682
|
if os.path.exists(self.index_path):
|
|
@@ -593,6 +703,7 @@ class MdCG:
|
|
|
593
703
|
return idx
|
|
594
704
|
|
|
595
705
|
@staticmethod
|
|
706
|
+
# 生效条件:nodes 须为带 values() 的映射且各元素支持 .get("bucket");仅当 bucket 取值为真值时才计入返回计数,缺键或假值均跳过。
|
|
596
707
|
def _count_buckets(nodes):
|
|
597
708
|
buckets = {}
|
|
598
709
|
for e in nodes.values():
|
|
@@ -601,6 +712,7 @@ class MdCG:
|
|
|
601
712
|
buckets[b] = buckets.get(b, 0) + 1
|
|
602
713
|
return buckets
|
|
603
714
|
|
|
715
|
+
# 生效条件:os.path.exists(self.index_path) 为真、JSON 可解析且 "schema" 等于 SCHEMA 时以该快照为基底,否则用空骨架 {"schema":SCHEMA,"nodes":{},"buckets":{}} 作基底(不重扫目录),再对 index_log_dir 逐条重放(无 id 跳过、e 为 None 则 pop、否则覆盖),落盘并清空日志后替换 self.index 并返回 idx;
|
|
604
716
|
def compact_index(self):
|
|
605
717
|
with FileLock(self.index_path):
|
|
606
718
|
idx = {"schema": SCHEMA, "nodes": {}, "buckets": {}}
|
|
@@ -626,6 +738,7 @@ class MdCG:
|
|
|
626
738
|
self.index = idx
|
|
627
739
|
return idx
|
|
628
740
|
|
|
741
|
+
# 生效条件:self._dirty 非空时才(必要时新建 ShardedLog)逐条 append、清空 _dirty 并关闭分片句柄;self._dirty 为空时立即返回、不写任何记录;
|
|
629
742
|
def flush(self):
|
|
630
743
|
if not self._dirty:
|
|
631
744
|
return
|
|
@@ -640,6 +753,7 @@ class MdCG:
|
|
|
640
753
|
# 每次 flush,句柄无需常驻;下一次 append 会按需重开。
|
|
641
754
|
self._log.close()
|
|
642
755
|
|
|
756
|
+
# 生效条件:随认知图对象生命周期结束调用、可重复;先 flush() 落未达 autoflush 阈值的脏索引(否则尾部写入虽在盘上但不可见),再关闭并置空日志句柄(句柄为假值时跳过关闭);
|
|
643
757
|
def close(self):
|
|
644
758
|
# 先落脏索引再关句柄:否则未达 autoflush 阈值的尾部写入会永久丢失,
|
|
645
759
|
# 已有 _index.json 的根重开时不会重扫目录,节点将「在盘上但不可见」。
|
|
@@ -648,6 +762,7 @@ class MdCG:
|
|
|
648
762
|
self._log.close()
|
|
649
763
|
self._log = None
|
|
650
764
|
|
|
765
|
+
# 生效条件:遍历模块级 LAYERS 各层目录下所有 .md 文件,读取抛 OSError 的跳过;nid 取 fm.get("id"),为假值时回落去掉 .md 的文件名;bucket 取父目录名、父目录等于层名时为 None;返回 nodes;
|
|
651
766
|
def _scan_nodes(self):
|
|
652
767
|
nodes = {}
|
|
653
768
|
for layer in LAYERS:
|
|
@@ -665,12 +780,16 @@ class MdCG:
|
|
|
665
780
|
nid = fm.get("id") or fn[:-3]
|
|
666
781
|
rel = os.path.relpath(p, self.root).replace("\\", "/")
|
|
667
782
|
parent = os.path.basename(dirpath)
|
|
668
|
-
nodes[nid] = {
|
|
783
|
+
nodes[nid] = _strip_empty_gate_fields({
|
|
669
784
|
"path": rel, "layer": fm.get("layer", layer),
|
|
670
785
|
# role 必须回填:它写在节点 frontmatter 里(写入时 role or "user"),
|
|
671
786
|
# 但索引重建时若不复制,os.roles 会全部退化为 (none),
|
|
672
787
|
# 来源归因打分随之失效(实测 48 条全丢)。
|
|
673
788
|
"role": fm.get("role"),
|
|
789
|
+
# content_kind 入快照:角色化读取视图(第四阶段 6.1)的
|
|
790
|
+
# 候选资格维度须免读文件可判(与 role 同款理由)。
|
|
791
|
+
# 纯增量键:批次 C 之前零消费方,view=None 零行为变更。
|
|
792
|
+
"content_kind": fm.get("content_kind"),
|
|
674
793
|
"tags": fm.get("tags", []),
|
|
675
794
|
"bucket": parent if parent != layer else None,
|
|
676
795
|
"importance": fm.get("importance", 0.5),
|
|
@@ -687,10 +806,24 @@ class MdCG:
|
|
|
687
806
|
# 写入路径也因此无需读盘就能校验迁移合法性。重建口径与
|
|
688
807
|
# _stage 一致(旧库无该字段 → None → state_of 视为 active)。
|
|
689
808
|
lifecycle.STATE_FIELD: fm.get(lifecycle.STATE_FIELD),
|
|
809
|
+
# 可验证记忆单元(trust):重建口径与 _stage 三件同源
|
|
810
|
+
# (验证态 / 依赖 / 双时间轴)——索引缺键即免读文件不可判。
|
|
811
|
+
trust.STATE_FIELD: fm.get(trust.STATE_FIELD),
|
|
812
|
+
trust.DEPS_FIELD: fm.get(trust.DEPS_FIELD),
|
|
813
|
+
trust.FROM_FIELD: fm.get(trust.FROM_FIELD),
|
|
814
|
+
trust.UNTIL_FIELD: fm.get(trust.UNTIL_FIELD),
|
|
815
|
+
# 规范时间轴键(2026-09-19 阶段一):与 _stage 同口径,
|
|
816
|
+
# 重建索引后新旧口径一致(检索面 validity 须免读盘可判)。
|
|
817
|
+
trust.EFFECTIVE_FROM_FIELD: fm.get(trust.EFFECTIVE_FROM_FIELD),
|
|
818
|
+
trust.EFFECTIVE_UNTIL_FIELD: fm.get(trust.EFFECTIVE_UNTIL_FIELD),
|
|
690
819
|
# 记忆演化分支(④):重建口径与 _stage 一致
|
|
691
820
|
"branch_id": fm.get("branch_id"),
|
|
692
821
|
"branched_from": fm.get("branched_from"),
|
|
693
822
|
"evidence_count": fm.get("evidence_count", 0),
|
|
823
|
+
# S1 大域先验:域标签入索引快照 → 候选收敛零读文件(与 add() 同口径)
|
|
824
|
+
"big_domain": fm.get("big_domain"),
|
|
825
|
+
# S2 条件门控所需的可判定硬槽(免读文件即可门控)
|
|
826
|
+
"observation_position": (fm.get("condition_space") or {}).get("observation_position"),
|
|
694
827
|
# 嵌套子图 / 关系边入索引快照:递归展开与链式遍历免读文件
|
|
695
828
|
"subgraph": fm.get("subgraph"),
|
|
696
829
|
"edges": fm.get("edges") or [],
|
|
@@ -702,9 +835,14 @@ class MdCG:
|
|
|
702
835
|
# G8 派生溯源:frontmatter 声明入索引 → 悬空巡检零读文件
|
|
703
836
|
"derived_from": fm.get("derived_from") or [],
|
|
704
837
|
"derived_relation": fm.get("derived_relation"),
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
838
|
+
})
|
|
839
|
+
# 索引序确定性:按 nid 排序返回。os.walk 的遍历序是**文件系统事实**
|
|
840
|
+
# (NTFS 上常为字母序,但换 FS / 目录碎片化后不保证),若直接作为
|
|
841
|
+
# index["nodes"] 的物理序,就会让「完全并列」的结果顺序依赖重建路径。
|
|
842
|
+
# 排序后重建序恒定(增量路径另由 cut_by_relevance 的 nid 终键兜住)。
|
|
843
|
+
return {k: nodes[k] for k in sorted(nodes)}
|
|
844
|
+
|
|
845
|
+
# 生效条件:每次调用都以 self._scan_nodes() 的结果重建 nodes 与 buckets,在 FileLock 下 atomic_write 覆盖 index_path 并 ShardedLog.clear(index_log_dir),随后替换 self.index、清空 _dirty 并返回 idx(无 .md 时也照样覆盖为空索引);
|
|
708
846
|
def rebuild_index(self):
|
|
709
847
|
nodes = self._scan_nodes()
|
|
710
848
|
idx = {"schema": SCHEMA, "nodes": nodes,
|
|
@@ -718,13 +856,16 @@ class MdCG:
|
|
|
718
856
|
|
|
719
857
|
# ---------- 写 ----------
|
|
720
858
|
|
|
859
|
+
# 生效条件:node_id/content 必填,layer 不在 LAYERS 内、或 verification_basis 非 None 且不在 VERIFICATION_BASIS 内时抛 ValueError;consistency 为真且 _cons.check 判 REJECT 时,on_conflict="reject" 抛 ConsistencyError、on_conflict="defer" 返回 None,verdict 为 BLINDSPOT 且 on_conflict="defer" 同样返回 None,其余情形完成写盘/入索引后返回 node_id。
|
|
721
860
|
def add(self, node_id: str, content: str, layer: str = "knowledge",
|
|
722
861
|
tags=None, condition_space=None, importance: float = 0.5,
|
|
723
862
|
confidence: float = 0.6, edges=None, verification_basis: str = None,
|
|
724
863
|
non_applicable_conditions=None, override: bool = False,
|
|
725
864
|
consistency: bool = False, on_conflict: str = "reject",
|
|
726
865
|
derived_from=None, relation: str = provenance.DEFAULT_RELATION,
|
|
727
|
-
semantic: str = None,
|
|
866
|
+
semantic: str = None, depends_on=None, valid_from=None,
|
|
867
|
+
valid_until=None, effective_from=None, effective_until=None,
|
|
868
|
+
believed_at=None, verification_state: str = None, **extra) -> str:
|
|
728
869
|
"""写入一个节点。
|
|
729
870
|
|
|
730
871
|
verification_basis: 外部验证基底(白箱信任的硬门槛),
|
|
@@ -745,6 +886,22 @@ class MdCG:
|
|
|
745
886
|
衍生层,正文原文无损;OOV token 记 fm.semantic_oov 警告不拒绝
|
|
746
887
|
(词表覆盖有限,拒绝会堵死合法写入)。检索面经
|
|
747
888
|
MDCG_SEMANTIC=1 开启组合共现打分(mdcg._score)。
|
|
889
|
+
depends_on:(可验证记忆单元)本节点**依赖**的节点 id,单个或列表。落
|
|
890
|
+
frontmatter.depends_on(= CCG「子功能」槽的落字段,见 nodefile)。
|
|
891
|
+
被依赖单元变动/证伪时,本节点由 trust.mark_dependents **同步**
|
|
892
|
+
标 doubted(一跳,低成本);多跳交巡检 trust.propagate。
|
|
893
|
+
valid_from / valid_until: 双时间轴(何时开始成立 / 何时不再成立),
|
|
894
|
+
使时效可判定:未生效 / 生效中 / 已过期(trust.validity)。
|
|
895
|
+
**历史名**——2026-09-19 阶段一新增规范名 `effective_from` /
|
|
896
|
+
`effective_until`(新调用方优先用它;旧参数名照旧落旧键,
|
|
897
|
+
读取侧由 trust.FROM_ALIASES/UNTIL_ALIASES 统一回落,存量不迁移)。
|
|
898
|
+
believed_at: 信念时间(体系**何时确认此条**)——取代/审核的锚。
|
|
899
|
+
**不是效力语义**:既非「未生效」也非「已过期」,与双时间轴物理隔离
|
|
900
|
+
(绝不并入 scrub 的任一键族);覆写默认继承,缺省不写。
|
|
901
|
+
verification_state: 验证态(unverified/verified/doubted/rechecking/expired,
|
|
902
|
+
真源 trust.py)。**覆写既有节点时默认继承**——add 是全量重建
|
|
903
|
+
fm,不显式继承会把已 verified 静默打回 unverified(与
|
|
904
|
+
lifecycle_state 同构的坑);显式传入则走迁移裁决,非法即拒。
|
|
748
905
|
"""
|
|
749
906
|
if layer not in LAYERS:
|
|
750
907
|
raise ValueError(f"未知层:{layer}(允许:{LAYERS})")
|
|
@@ -810,6 +967,19 @@ class MdCG:
|
|
|
810
967
|
"evidence_count": 0, "positive_evidence": 0, "negative_evidence": 0,
|
|
811
968
|
}
|
|
812
969
|
fm.update(extra)
|
|
970
|
+
# S1 大域先验:写入时固化「内容 → 大域」(契约 §3 S1)。
|
|
971
|
+
# 为何在写入侧:检索侧要按域收敛,节点就必须带域;query 侧分类器已存在,
|
|
972
|
+
# 缺的只是这一列节点元数据(审计偏差 4 的根因)。调用方可显式传入覆盖。
|
|
973
|
+
# 无有效域信号(classify_text 返回 None)时**不写**该字段 → 留在兜底池。
|
|
974
|
+
# 默认(MDCG_RETRIEVAL_PIPELINE 未设)**不写**该字段:默认口径与改动前一致;
|
|
975
|
+
# 开启后新写入的节点开始积累域标签,历史节点由 md_cg.backfill_bigdomain 补齐。
|
|
976
|
+
if os.environ.get("MDCG_RETRIEVAL_PIPELINE") == "1" and not fm.get("big_domain"):
|
|
977
|
+
try:
|
|
978
|
+
_bd = routing.classify_text(content)
|
|
979
|
+
except Exception:
|
|
980
|
+
_bd = None
|
|
981
|
+
if _bd:
|
|
982
|
+
fm["big_domain"] = _bd
|
|
813
983
|
# 生命周期状态(② 显式状态机,真源 `lifecycle.py`):add 是**全量重建
|
|
814
984
|
# fm** 而非增量更新,故必须显式处理状态——否则已定型/已降权节点会被静默
|
|
815
985
|
# 打回 active(与 ④ rewrite 必须重传 branch_id 同构的坑)。口径:
|
|
@@ -845,6 +1015,112 @@ class MdCG:
|
|
|
845
1015
|
or _pe.get("protected") or _pe.get("immutable")),
|
|
846
1016
|
override=override)
|
|
847
1017
|
fm[lifecycle.STATE_FIELD] = want_state
|
|
1018
|
+
# 可验证记忆单元(真源 `trust.py`):验证态 + 依赖声明 + 双时间轴。
|
|
1019
|
+
# 与 lifecycle 同构的坑——add 是**全量重建 fm**:既有 verified 节点被普通
|
|
1020
|
+
# 覆写时若不显式继承,会静默打回 unverified。故默认继承,显式传入才裁决。
|
|
1021
|
+
prev_vstate = trust.state_of(prev_entry)
|
|
1022
|
+
if prev_entry and trust.STATE_FIELD not in prev_entry:
|
|
1023
|
+
prev_vstate = trust.state_of(
|
|
1024
|
+
(self.get(node_id) or {}).get("frontmatter"))
|
|
1025
|
+
_v_alias = fm.pop(trust.STATE_FIELD, None) # **extra 通道的兼容写法
|
|
1026
|
+
want_vstate = verification_state or _v_alias
|
|
1027
|
+
if want_vstate is None:
|
|
1028
|
+
fm[trust.STATE_FIELD] = prev_vstate
|
|
1029
|
+
else:
|
|
1030
|
+
_pe_v = prev_entry or {}
|
|
1031
|
+
trust.require_transition(
|
|
1032
|
+
prev_vstate, want_vstate,
|
|
1033
|
+
protected=bool(fm.get("protected") or fm.get("immutable")
|
|
1034
|
+
or _pe_v.get("protected") or _pe_v.get("immutable")),
|
|
1035
|
+
override=override)
|
|
1036
|
+
fm[trust.STATE_FIELD] = want_vstate
|
|
1037
|
+
# 依赖声明(CCG「子功能」的落字段)与双时间轴:**覆写即继承**语义——
|
|
1038
|
+
# 一次普通覆写抹掉依赖,会让失效传播从源头断链(下游永远收不到存疑信号)。
|
|
1039
|
+
_deps = trust.as_deps(depends_on) or trust.as_deps(fm.get(trust.DEPS_FIELD))
|
|
1040
|
+
if not _deps and prev_entry:
|
|
1041
|
+
_deps = trust.as_deps(prev_entry.get(trust.DEPS_FIELD))
|
|
1042
|
+
if _deps:
|
|
1043
|
+
fm[trust.DEPS_FIELD] = _deps
|
|
1044
|
+
else:
|
|
1045
|
+
fm.pop(trust.DEPS_FIELD, None) # 不保留空列表噪声
|
|
1046
|
+
# 双时间轴键族(2026-09-19 阶段一):规范键 `effective_*` 优先、历史键
|
|
1047
|
+
# `valid_*` 回落。**落键口径**——显式新参数 → 落规范键;旧参数名 → 照旧落
|
|
1048
|
+
# 旧键(既有调用面字节不变,零破坏);均未给 → 本次 fm → 旧快照,任一有值即继承
|
|
1049
|
+
# (不把 `valid_from` 悄悄改名,避免无谓 diff)。同族双写必留歧义,故落一键、剔另一键。
|
|
1050
|
+
for _spec, _legacy, _nv in (
|
|
1051
|
+
(trust.EFFECTIVE_FROM_FIELD, trust.FROM_FIELD, effective_from),
|
|
1052
|
+
(trust.EFFECTIVE_UNTIL_FIELD, trust.UNTIL_FIELD, effective_until)):
|
|
1053
|
+
if _nv is not None:
|
|
1054
|
+
fm[_spec] = _nv
|
|
1055
|
+
fm.pop(_legacy, None)
|
|
1056
|
+
continue
|
|
1057
|
+
_lv = valid_from if _spec == trust.EFFECTIVE_FROM_FIELD else valid_until
|
|
1058
|
+
if _lv is not None:
|
|
1059
|
+
fm[_legacy] = _lv
|
|
1060
|
+
fm.pop(_spec, None)
|
|
1061
|
+
continue
|
|
1062
|
+
_val, _key = None, None
|
|
1063
|
+
for _k in (_spec, _legacy):
|
|
1064
|
+
if fm.get(_k) not in (None, ""):
|
|
1065
|
+
_val, _key = fm.get(_k), _k
|
|
1066
|
+
break
|
|
1067
|
+
if _val is None and prev_entry:
|
|
1068
|
+
for _k in (_spec, _legacy):
|
|
1069
|
+
if prev_entry.get(_k) not in (None, ""):
|
|
1070
|
+
_val, _key = prev_entry.get(_k), _k
|
|
1071
|
+
break
|
|
1072
|
+
fm.pop(_spec, None)
|
|
1073
|
+
fm.pop(_legacy, None)
|
|
1074
|
+
if _val not in (None, ""):
|
|
1075
|
+
fm[_key] = _val
|
|
1076
|
+
# 过期时刻冗余(2026-09-20 补全阶段一声明的设计:nodefile.EXPIRED_AT_FIELD
|
|
1077
|
+
# 注释自书「由 effective_until 派生,供审计/对账免计算直读」,但派生逻辑
|
|
1078
|
+
# 从未落地——scrub._EXPIRY_KEYS 读取侧已消费该键,写入侧缺失即恒空)。
|
|
1079
|
+
# 冗余字段不独立存活:终点在则随终点落、终点失则剔除(防与终点漂移)。
|
|
1080
|
+
_end = None
|
|
1081
|
+
for _k in trust.UNTIL_ALIASES:
|
|
1082
|
+
if fm.get(_k) not in (None, ""):
|
|
1083
|
+
_end = fm.get(_k)
|
|
1084
|
+
break
|
|
1085
|
+
if _end is not None:
|
|
1086
|
+
fm[trust.EXPIRED_FIELD] = _end
|
|
1087
|
+
else:
|
|
1088
|
+
fm.pop(trust.EXPIRED_FIELD, None)
|
|
1089
|
+
# 审计 / 血缘向字段(**不进索引白名单** → 索引快照里没有这些键)的统一回读
|
|
1090
|
+
# 来源:惰性读节点文件一次、多字段共用。照搬 prev_entry 会让已落盘字段在
|
|
1091
|
+
# 下次普通覆写时静默丢失(lifecycle_state / verification_state 两度踩的同一坑)。
|
|
1092
|
+
_old = {}
|
|
1093
|
+
|
|
1094
|
+
def _old_fm(key):
|
|
1095
|
+
if not prev_entry:
|
|
1096
|
+
return None
|
|
1097
|
+
if "fm" not in _old:
|
|
1098
|
+
_old["fm"] = (self.get(node_id) or {}).get("frontmatter") or {}
|
|
1099
|
+
return _old["fm"].get(key)
|
|
1100
|
+
|
|
1101
|
+
# 信念时间(体系何时确认此条):与效力时间**物理隔离**的第三类语义——
|
|
1102
|
+
# 覆写即继承,缺省不写空值噪声。
|
|
1103
|
+
_bel = believed_at if believed_at is not None else fm.get(trust.BELIEVED_FIELD)
|
|
1104
|
+
if _bel in (None, ""):
|
|
1105
|
+
_bel = _old_fm(trust.BELIEVED_FIELD)
|
|
1106
|
+
if _bel is None and prev_entry:
|
|
1107
|
+
_bel = prev_entry.get(trust.BELIEVED_FIELD) # 防御:万一被透传
|
|
1108
|
+
if _bel in (None, ""):
|
|
1109
|
+
fm.pop(trust.BELIEVED_FIELD, None)
|
|
1110
|
+
else:
|
|
1111
|
+
fm[trust.BELIEVED_FIELD] = _bel
|
|
1112
|
+
# 巩固 / 归纳留痕(2026-09-19 阶段一 · 真源 md_cg/consolidate.py):成员侧
|
|
1113
|
+
# 「巩固进哪一条 + 何时」与概念侧「前身是谁 + 何时」同族(nodefile.
|
|
1114
|
+
# CONSOLIDATION_FIELDS)——同属审计/血缘向、索引里没有,故同样回读继承;
|
|
1115
|
+
# 缺了它,概念节点被一次普通覆写(如审核 edit/merge 重写)就丢掉前身清单,
|
|
1116
|
+
# 「任一合并条目可定位全部前身」即断。
|
|
1117
|
+
for _cf in nodefile.CONSOLIDATION_FIELDS:
|
|
1118
|
+
if fm.get(_cf) in (None, "", [], {}):
|
|
1119
|
+
_cv = _old_fm(_cf)
|
|
1120
|
+
if _cv in (None, "", [], {}):
|
|
1121
|
+
fm.pop(_cf, None)
|
|
1122
|
+
else:
|
|
1123
|
+
fm[_cf] = _cv
|
|
848
1124
|
# G8 派生溯源:把「来源声明」写进 frontmatter(单一真相源),台账为派生物。
|
|
849
1125
|
# 只声明事实、不做校验式拒绝——关系名非法仅回退默认值,不阻断写入。
|
|
850
1126
|
parents = provenance.as_list(derived_from)
|
|
@@ -861,7 +1137,7 @@ class MdCG:
|
|
|
861
1137
|
# 私有内容封装(默认恒等;MdCGSecure 覆盖为 AEAD 加密)。
|
|
862
1138
|
# 索引派生同样基于落盘内容,保证与 _scan_nodes 重建结果一致。
|
|
863
1139
|
sealed = self._write_node(node_id, path, fm, content)
|
|
864
|
-
self._stage(node_id, {
|
|
1140
|
+
self._stage(node_id, _strip_empty_gate_fields({
|
|
865
1141
|
"path": os.path.relpath(path, self.root).replace("\\", "/"),
|
|
866
1142
|
"layer": layer, "tags": tags, "bucket": bucket,
|
|
867
1143
|
"importance": importance, "created_at": fm["created_at"],
|
|
@@ -871,6 +1147,8 @@ class MdCG:
|
|
|
871
1147
|
"temporal": fm.get("temporal"),
|
|
872
1148
|
"spatial": fm.get("spatial"),
|
|
873
1149
|
"time_window": cs.get("time_window"),
|
|
1150
|
+
"big_domain": fm.get("big_domain"),
|
|
1151
|
+
"observation_position": cs.get("observation_position"),
|
|
874
1152
|
"subgraph": fm.get("subgraph"),
|
|
875
1153
|
"edges": fm.get("edges") or [],
|
|
876
1154
|
"protected": fm.get("protected"),
|
|
@@ -882,14 +1160,27 @@ class MdCG:
|
|
|
882
1160
|
"writer": fm.get("writer"),
|
|
883
1161
|
"session": fm.get("session"),
|
|
884
1162
|
"harness": fm.get("harness"),
|
|
1163
|
+
# 角色化读取视图(第四阶段 6.1):候选资格维度入快照(免读文件
|
|
1164
|
+
# 可判,与 _scan_nodes 同口径)。
|
|
1165
|
+
"content_kind": fm.get("content_kind"),
|
|
885
1166
|
# 记忆演化分支(④):fork 副本带分支归属与溯源主支
|
|
886
1167
|
"branch_id": fm.get("branch_id"),
|
|
887
1168
|
"branched_from": fm.get("branched_from"),
|
|
888
1169
|
# 生命周期状态(②):索引快照透出 → 免读文件可查(与 _scan_nodes 同口径)
|
|
889
1170
|
lifecycle.STATE_FIELD: fm.get(lifecycle.STATE_FIELD),
|
|
890
|
-
|
|
1171
|
+
# 可验证记忆单元(trust):验证态 + 依赖 + 双时间轴入快照 → 免读文件可查
|
|
1172
|
+
trust.STATE_FIELD: fm.get(trust.STATE_FIELD),
|
|
1173
|
+
trust.DEPS_FIELD: fm.get(trust.DEPS_FIELD),
|
|
1174
|
+
trust.FROM_FIELD: fm.get(trust.FROM_FIELD),
|
|
1175
|
+
trust.UNTIL_FIELD: fm.get(trust.UNTIL_FIELD),
|
|
1176
|
+
# 规范时间轴键(2026-09-19 阶段一):检索面 validity 过滤免读盘即可判定。
|
|
1177
|
+
# `believed_at`(信念时间)属审计/取代锚,**不进索引热点**(只落 fm)。
|
|
1178
|
+
trust.EFFECTIVE_FROM_FIELD: fm.get(trust.EFFECTIVE_FROM_FIELD),
|
|
1179
|
+
trust.EFFECTIVE_UNTIL_FIELD: fm.get(trust.EFFECTIVE_UNTIL_FIELD),
|
|
1180
|
+
}))
|
|
891
1181
|
subgraph.invalidate_cache(self)
|
|
892
1182
|
chain.invalidate_cache(self)
|
|
1183
|
+
trust.invalidate_cache(self) # 依赖声明可能变化 → 反查索引作废
|
|
893
1184
|
# G8 常态化建链:仅对**新增**节点、仅在显式声明来源时建边。
|
|
894
1185
|
# 硬约束:建链失败绝不阻断写入(record 永不抛,失败降级留痕)。
|
|
895
1186
|
if parents:
|
|
@@ -899,6 +1190,7 @@ class MdCG:
|
|
|
899
1190
|
actor=extra.get("actor"))
|
|
900
1191
|
return node_id
|
|
901
1192
|
|
|
1193
|
+
# 生效条件:node_id、dst、reason、actor、override 全部原样转交 lifecycle.set_state 并返回其结果,本方法自身不做校验、分支或参数回落;
|
|
902
1194
|
def set_state(self, node_id: str, dst: str, reason: str = None,
|
|
903
1195
|
actor: str = None, override: bool = False) -> dict:
|
|
904
1196
|
"""节点生命周期状态推进(② 显式状态机:**唯一推进入口**,见 lifecycle.py)。
|
|
@@ -910,6 +1202,31 @@ class MdCG:
|
|
|
910
1202
|
return lifecycle.set_state(self, node_id, dst, reason=reason,
|
|
911
1203
|
actor=actor, override=override)
|
|
912
1204
|
|
|
1205
|
+
# 生效条件:无条件转调 trust.set_state(self, node_id, dst, reason=..., actor=..., evidence=..., method=..., trigger=..., override=...) 并原样返回其结果,本方法自身不做校验、分支或参数回落;
|
|
1206
|
+
def set_verification(self, node_id: str, dst: str, reason: str = None,
|
|
1207
|
+
actor: str = None, evidence: str = None,
|
|
1208
|
+
method: str = None, trigger: str = None,
|
|
1209
|
+
override: bool = False) -> dict:
|
|
1210
|
+
"""节点**验证态**推进(真源 `trust.py`:唯一推进入口)。
|
|
1211
|
+
|
|
1212
|
+
与 `set_state`(生命周期)**正交**:一个管「节点是否还在服役」
|
|
1213
|
+
(active/converged/demoted/archived),一个管「它现在还成不成立」
|
|
1214
|
+
(unverified/verified/doubted/rechecking/expired)。两套状态集不共享常量。
|
|
1215
|
+
|
|
1216
|
+
非法迁移走负路由(`ok=False` + `error` 机器码,不抛);受保护节点不接受
|
|
1217
|
+
降级(需 `override=True`);审计追加 `<root>/_trust.jsonl`,索引快照同步。
|
|
1218
|
+
"""
|
|
1219
|
+
return trust.set_state(self, node_id, dst, reason=reason,
|
|
1220
|
+
actor=actor, evidence=evidence,
|
|
1221
|
+
method=method, trigger=trigger,
|
|
1222
|
+
override=override)
|
|
1223
|
+
|
|
1224
|
+
# 生效条件:只读转调 trust.describe(self, node_id) 并返回其结果,自身不做校验或分支;
|
|
1225
|
+
def verification(self, node_id: str) -> dict:
|
|
1226
|
+
"""单节点验证态全貌(验证态 + 依赖 + 反查 + 时间轴 + 履历)。只读。"""
|
|
1227
|
+
return trust.describe(self, node_id)
|
|
1228
|
+
|
|
1229
|
+
# 生效条件:每次调用都延迟导入 twophase 并以原样 apply(含 False)与 limit(含 0)转调 twophase.reconcile(self, apply=apply, limit=limit) 并返回,自身不做参数回落;
|
|
913
1230
|
def reconcile_writes(self, apply: bool = True, limit: int = 2000) -> dict:
|
|
914
1231
|
"""写入两段式对账(③ 两段式提交):把崩溃遗留的半途写入**补账或标记**。
|
|
915
1232
|
|
|
@@ -920,6 +1237,7 @@ class MdCG:
|
|
|
920
1237
|
from . import twophase # 延迟导入:与写路径解耦,避免模块环
|
|
921
1238
|
return twophase.reconcile(self, apply=apply, limit=limit)
|
|
922
1239
|
|
|
1240
|
+
# 生效条件:每次调用都延迟导入 twophase 并以原样 limit(含 0)转调 twophase.pending(self, limit=limit) 并返回,自身不做参数回落;
|
|
923
1241
|
def pending_writes(self, limit: int = 200) -> list:
|
|
924
1242
|
"""未结清的写入意图(有 intent 无 outcome)——只读,不改账本。"""
|
|
925
1243
|
from . import twophase
|
|
@@ -938,6 +1256,7 @@ class MdCG:
|
|
|
938
1256
|
# 按 child_id 去重,重复追加返回 False 不落盘。
|
|
939
1257
|
# ------------------------------------------------------------------
|
|
940
1258
|
|
|
1259
|
+
# 生效条件:self.get(node_id) 返回假值(取不到节点)时返回 None;取到时返回 (frontmatter or {}, content or "", root 下 node.get("path") 或回落 f"{node_id}.md");
|
|
941
1260
|
def _edge_node(self, node_id):
|
|
942
1261
|
"""取节点 (fm, content, full_path);不存在返回 None。"""
|
|
943
1262
|
node = self.get(node_id)
|
|
@@ -946,6 +1265,7 @@ class MdCG:
|
|
|
946
1265
|
return (node.get("frontmatter") or {}, node.get("content") or "",
|
|
947
1266
|
os.path.join(self.root, node.get("path") or f"{node_id}.md"))
|
|
948
1267
|
|
|
1268
|
+
# 生效条件:self.index["nodes"].get(node_id) 非 None 时把该条目 edges 置为 fm.get("edges") or []、subgraph 置为 fm.get("subgraph") 并标脏;条目为 None 则完全不同步(subgraph 缓存失效调用被静默吞异常);
|
|
949
1269
|
def _sync_edge_entry(self, node_id, fm):
|
|
950
1270
|
"""边域变更后同步索引 entry(edges/subgraph 键)并标脏。"""
|
|
951
1271
|
entry = self.index["nodes"].get(node_id)
|
|
@@ -959,6 +1279,7 @@ class MdCG:
|
|
|
959
1279
|
except Exception:
|
|
960
1280
|
pass
|
|
961
1281
|
|
|
1282
|
+
# 生效条件:node_id 取不到节点返回 False;fm.edges 非 list 时重置为 [];已存在 dict 边其 str(target or "") 与 str(relation_type or "") 同时等于 edge 归一值(缺键为 "")时返回 False(幂等),否则追加 dict(edge)、写盘并同步后返回 True;
|
|
962
1283
|
def append_edge(self, node_id: str, edge: dict) -> bool:
|
|
963
1284
|
"""向既有节点追加一条出边(fm.edges),幂等去重。
|
|
964
1285
|
|
|
@@ -983,6 +1304,7 @@ class MdCG:
|
|
|
983
1304
|
self._sync_edge_entry(node_id, fm)
|
|
984
1305
|
return True
|
|
985
1306
|
|
|
1307
|
+
# 生效条件:node_id 取不到节点返回 False;subgraph 非 dict 时重置为 {"nodes":[]}、其 nodes 非 list 时重置为 [];child_id 已在列表中返回 False(幂等),否则追加 str(child_id)、写盘并同步后返回 True;
|
|
986
1308
|
def append_subgraph_node(self, node_id: str, child_id: str) -> bool:
|
|
987
1309
|
"""向既有父节点追加层级子节点(fm.subgraph.nodes),幂等去重。
|
|
988
1310
|
|
|
@@ -1006,6 +1328,7 @@ class MdCG:
|
|
|
1006
1328
|
self._sync_edge_entry(node_id, fm)
|
|
1007
1329
|
return True
|
|
1008
1330
|
|
|
1331
|
+
# 生效条件:node_id 取不到节点返回 False;tags 非 list 时重置为 [];先按 remove(假值按空集合)删除、再把 add(假值按空列表)中不在结果内的 str 项追加,结果等于原 tags 时返回 False 不落盘,否则写盘、同步索引 tags、标脏并返回 True;
|
|
1009
1332
|
def update_tags(self, node_id: str, add=None, remove=None) -> bool:
|
|
1010
1333
|
"""节点 tags 增删(节点状态更新窄原语——causal 候选状态迁移面)。
|
|
1011
1334
|
|
|
@@ -1036,6 +1359,7 @@ class MdCG:
|
|
|
1036
1359
|
self._dirty[node_id] = entry
|
|
1037
1360
|
return True
|
|
1038
1361
|
|
|
1362
|
+
# 生效条件:node_id 取不到节点返回 False;在 fm.get("edges") or [] 中找首个 dict 且 str(target or "")==str(target_id)、str(relation_type or "")==str(relation_type) 的边,找不到返回 False;命中则把该边 condition_space 置为 dict(condition_space or {})、写盘并同步后返回 True;
|
|
1039
1363
|
def set_edge_condition(self, node_id: str, target_id: str,
|
|
1040
1364
|
relation_type: str, condition_space: dict) -> bool:
|
|
1041
1365
|
"""改既有节点上指定出边的 condition_space(模式分离更新面)。
|
|
@@ -1061,6 +1385,7 @@ class MdCG:
|
|
|
1061
1385
|
self._sync_edge_entry(node_id, fm)
|
|
1062
1386
|
return True
|
|
1063
1387
|
|
|
1388
|
+
# 生效条件:nid("rej_"+sha1(hypothesis) 前 10 位)已在 self.index["nodes"] 中时直接返回该 nid 不写盘(幂等);否则以 hypothesis/reason/verification_basis(默认 "test")拼正文并 add(..., layer="rejected", importance=0.0, **extra) 返回新 nid;
|
|
1064
1389
|
def add_rejected(self, hypothesis: str, reason: str, verification_basis: str = "test",
|
|
1065
1390
|
tags=None, **extra) -> str:
|
|
1066
1391
|
"""第 5 篇 L2:负记忆——失败/否决的假设库。重复证伪幂等。"""
|
|
@@ -1074,6 +1399,7 @@ class MdCG:
|
|
|
1074
1399
|
tags=tags, verification_basis=verification_basis,
|
|
1075
1400
|
importance=0.0, **extra) # importance=0:不被检索优先
|
|
1076
1401
|
|
|
1402
|
+
# 生效条件:nid("unr_"+sha1(question) 前 10 位)已在 self.index["nodes"] 中时直接返回;否则拼接 question、known_clues(假值渲染「(暂无)」)、context 非空才追加「现场」行、goal(假值渲染「(未设定)」)与 verification_basis 后 add(layer="unresolved", importance=0.3);
|
|
1077
1403
|
def add_unresolved(self, question: str, known_clues: str = "",
|
|
1078
1404
|
goal: str = "", verification_basis: str = "data",
|
|
1079
1405
|
tags=None, context: str = "", **extra) -> str:
|
|
@@ -1097,6 +1423,7 @@ class MdCG:
|
|
|
1097
1423
|
# ---------- 目标槽(白箱第 5 篇第 3 章「目标」)----------
|
|
1098
1424
|
|
|
1099
1425
|
@staticmethod
|
|
1426
|
+
# 生效条件:传入 goal 即返回渲染文本;conditions 为假值(含默认空串)时「生效条件」行填「(未声明——视为任意情境下有效)」,action 为假值时填默认执行说明。
|
|
1100
1427
|
def _goal_content(goal, conditions="", action=""):
|
|
1101
1428
|
"""目标节点的 CCG 渲染:目标不是知识,但按 CCG 格式落盘,
|
|
1102
1429
|
保证 judge_qualification 能给出 ACCEPT 而非 BLINDSPOT。"""
|
|
@@ -1107,6 +1434,7 @@ class MdCG:
|
|
|
1107
1434
|
f"# 验证方式:other\n"
|
|
1108
1435
|
f"# 不适用条件:目标状态为 done/dropped 时不再参与定向\n")
|
|
1109
1436
|
|
|
1437
|
+
# 生效条件:status 不属于模块级 GOAL_STATUSES 时抛 ValueError;goal 经 (goal or "").strip() 后为空(None/空串/纯空白)时抛 ValueError("goal 不能为空");否则按 "goal_"+sha1(goal utf-8) 前 10 位生成 gid 并 add(layer="goals", importance=float(priority), goal_status=status, deadline=deadline, ...);
|
|
1110
1438
|
def add_goal(self, goal: str, priority: float = 0.5, deadline=None,
|
|
1111
1439
|
conditions: str = "", action: str = "", tags=None,
|
|
1112
1440
|
status: str = "active", **extra) -> str:
|
|
@@ -1128,6 +1456,7 @@ class MdCG:
|
|
|
1128
1456
|
goal_text=goal, goal_status=status,
|
|
1129
1457
|
deadline=deadline, **extra)
|
|
1130
1458
|
|
|
1459
|
+
# 生效条件:self.index["nodes"].get(node_id) 为假值或其 layer 不等于 "goals" 时返回 None;_read 得到的 fm 为 None 时返回 None;否则返回 dict,其中 goal 取 goal_text 假值回落 ""、status 取 goal_status 假值回落 "active"、priority/created_at 取 importance/created_at 假值回落 0.0;
|
|
1131
1460
|
def _goal_entry(self, node_id):
|
|
1132
1461
|
e = self.index["nodes"].get(node_id)
|
|
1133
1462
|
if not e or e.get("layer") != "goals":
|
|
@@ -1143,6 +1472,7 @@ class MdCG:
|
|
|
1143
1472
|
"created_at": float(fm.get("created_at") or 0.0),
|
|
1144
1473
|
"path": e["path"]}
|
|
1145
1474
|
|
|
1475
|
+
# 生效条件:遍历 self.index["nodes"] 中 layer=="goals" 的节点,_goal_entry 取不到者跳过;status 为假值时不过滤、否则仅保留 status 相等者;按 (-priority, -created_at) 降序,limit 为真值时截断 out[:limit],否则返回全量;
|
|
1146
1476
|
def list_goals(self, status=None, limit=None):
|
|
1147
1477
|
"""列出目标,按 (priority, created_at) 降序。status 过滤 active/done/dropped。"""
|
|
1148
1478
|
out = []
|
|
@@ -1152,13 +1482,18 @@ class MdCG:
|
|
|
1152
1482
|
g = self._goal_entry(nid)
|
|
1153
1483
|
if g and (status is None or g["status"] == status):
|
|
1154
1484
|
out.append(g)
|
|
1155
|
-
|
|
1485
|
+
# 终键 nid:同 priority 且 created_at 碰撞(量化到 ~1ms)时定序,
|
|
1486
|
+
# 否则并列顺序回落到 self.index["nodes"] 的物理序(增量路径=写入序)。
|
|
1487
|
+
out.sort(key=lambda g: (-g["priority"], -g["created_at"],
|
|
1488
|
+
str(g.get("id") or "")))
|
|
1156
1489
|
return out[:limit] if limit else out
|
|
1157
1490
|
|
|
1491
|
+
# 生效条件:等价于 list_goals(status="active", limit=limit),limit 缺省 5;返回按 (-priority, -created_at) 降序的活跃目标列表;
|
|
1158
1492
|
def active_goals(self, limit: int = 5):
|
|
1159
1493
|
"""当前活跃目标(默认最多 5 条)——检索定向的默认来源。"""
|
|
1160
1494
|
return self.list_goals(status="active", limit=limit)
|
|
1161
1495
|
|
|
1496
|
+
# 生效条件:status 不属于模块级 GOAL_STATUSES 时抛 ValueError;self.get(node_id) 取不到节点或其 frontmatter 的 layer 不等于 "goals" 时返回 None;否则写回 goal_status 与 status_changed_at 并返回 {"id": node_id, "status": status};
|
|
1162
1497
|
def set_goal_status(self, node_id: str, status: str):
|
|
1163
1498
|
"""目标状态机:active → done/dropped(可回退)。"""
|
|
1164
1499
|
if status not in GOAL_STATUSES:
|
|
@@ -1173,6 +1508,7 @@ class MdCG:
|
|
|
1173
1508
|
fm, node["content"])
|
|
1174
1509
|
return {"id": node_id, "status": status}
|
|
1175
1510
|
|
|
1511
|
+
# 生效条件:goal 为真值(非 None/空串/0)时返回 str(goal);goal 为假值时返回 self.active_goals(limit=limit) 中非空 goal 文本以空格拼接的字符串;
|
|
1176
1512
|
def goal_text(self, goal=None, limit: int = 5) -> str:
|
|
1177
1513
|
"""检索定向用的目标文本:显式 goal 优先,否则拼接活跃目标。"""
|
|
1178
1514
|
if goal:
|
|
@@ -1182,6 +1518,7 @@ class MdCG:
|
|
|
1182
1518
|
|
|
1183
1519
|
# ---------- 近期事件滚动窗口(白箱第 5 篇第 3 章「近期事件」)----------
|
|
1184
1520
|
|
|
1521
|
+
# 生效条件:role 为假值回落 "user"、text 为假值回落 ""、tags/meta 为假值回落 []/{} 后拼成 rec 追加到 recent_log,再按 window(默认模块级 DEFAULT_RECENT_WINDOW)调 roll_recent 截断,返回 {"event": rec, "dropped": dropped};
|
|
1185
1522
|
def remember_event(self, role: str, text: str, tags=None, meta=None,
|
|
1186
1523
|
window: int = DEFAULT_RECENT_WINDOW):
|
|
1187
1524
|
"""追加一条近期事件(原始、未结构化),并按窗口滚动截断。"""
|
|
@@ -1192,6 +1529,7 @@ class MdCG:
|
|
|
1192
1529
|
dropped = self.roll_recent(window)
|
|
1193
1530
|
return {"event": rec, "dropped": dropped}
|
|
1194
1531
|
|
|
1532
|
+
# 生效条件:read_jsonl(recent_log) 条数 <= window(默认 DEFAULT_RECENT_WINDOW)时返回 0 不写盘;否则在 FileLock 下 atomic_write 保留 recs[-window:] 并返回 len(recs) - len(keep);window 为 0 时 recs[-0:] 即全部记录,返回 0 且不实际截断。
|
|
1195
1533
|
def roll_recent(self, window: int = DEFAULT_RECENT_WINDOW) -> int:
|
|
1196
1534
|
"""把窗口截断到最近 window 条,返回丢弃条数(未超限则 0,幂等)。"""
|
|
1197
1535
|
recs = list(read_jsonl(self.recent_log))
|
|
@@ -1203,6 +1541,7 @@ class MdCG:
|
|
|
1203
1541
|
json.dumps(r, ensure_ascii=False) + "\n" for r in keep))
|
|
1204
1542
|
return len(recs) - len(keep)
|
|
1205
1543
|
|
|
1544
|
+
# 生效条件:roles 为真值时按 r.get("role") 是否在 set(roles) 内过滤;since 非 None 时按 float(r.get("t") or 0) >= float(since) 过滤;limit 为真值时取末 limit 条、为 None/0 等假值时取全部;newest_first 为真值时反转返回,否则按原序返回;
|
|
1206
1545
|
def recent_events(self, limit: int = 20, roles=None, since=None,
|
|
1207
1546
|
newest_first: bool = True):
|
|
1208
1547
|
"""读取近期事件(默认最新在前)。roles 过滤角色,since 过滤时间戳。"""
|
|
@@ -1215,6 +1554,7 @@ class MdCG:
|
|
|
1215
1554
|
out = recs[-limit:] if limit else recs
|
|
1216
1555
|
return list(reversed(out)) if newest_first else out
|
|
1217
1556
|
|
|
1557
|
+
# 生效条件:无条件读取全部记录并在 FileLock 下把 recent_log 原子写空,返回被清条数(无记录时为 0);
|
|
1218
1558
|
def clear_recent(self):
|
|
1219
1559
|
"""清空近期窗口(返回被清条数)。"""
|
|
1220
1560
|
recs = list(read_jsonl(self.recent_log))
|
|
@@ -1222,6 +1562,7 @@ class MdCG:
|
|
|
1222
1562
|
atomic_write(self.recent_log, "")
|
|
1223
1563
|
return len(recs)
|
|
1224
1564
|
|
|
1565
|
+
# 生效条件:无条件把 entry 写入 _dirty[node_id] 与 index["nodes"][node_id];entry.get("bucket") 为真值时该桶计数 +1;当 len(self._dirty) >= self.autoflush 时调 flush()(autoflush 为 0 时每次标脏都立即 flush);
|
|
1225
1566
|
def _stage(self, node_id, entry):
|
|
1226
1567
|
self._dirty[node_id] = entry
|
|
1227
1568
|
self.index["nodes"][node_id] = entry
|
|
@@ -1231,6 +1572,7 @@ class MdCG:
|
|
|
1231
1572
|
if len(self._dirty) >= self.autoflush:
|
|
1232
1573
|
self.flush()
|
|
1233
1574
|
|
|
1575
|
+
# 生效条件:无条件 pop index["nodes"][node_id](不存在则无操作);被 pop 的条目有真值 bucket 时该桶计数 -1,减后 <=0 则删除该桶键;随后无论是否命中都把 _dirty[node_id] 置 None(删除 tombstone)并立即调 flush() 持久化;
|
|
1234
1576
|
def _unstage(self, node_id):
|
|
1235
1577
|
"""摘除索引条目并**持久化**——与 `_stage` 对称的删除原语。
|
|
1236
1578
|
|
|
@@ -1253,15 +1595,18 @@ class MdCG:
|
|
|
1253
1595
|
|
|
1254
1596
|
# ---------- 内容封装钩子(默认恒等;MdCGSecure 覆盖为「私有内容加密」)----
|
|
1255
1597
|
|
|
1598
|
+
# 生效条件:默认实现无条件原样返回 content,node_id 与 sensitivity 均不改变返回值;
|
|
1256
1599
|
def _seal_content(self, node_id: str, content: str,
|
|
1257
1600
|
sensitivity: str = None) -> str:
|
|
1258
1601
|
"""写入前的正文封装钩子。默认原样返回;加密实现见 `crypto.seal_node`。"""
|
|
1259
1602
|
return content
|
|
1260
1603
|
|
|
1604
|
+
# 生效条件:默认实现无条件返回传入的 content(不返回 None),node_id 与 fm 不参与;
|
|
1261
1605
|
def _open_content(self, node_id: str, fm: dict, content: str):
|
|
1262
1606
|
"""读取后的正文解封钩子。返回 None 表示不可读(无密钥 / 身份不符)。"""
|
|
1263
1607
|
return content
|
|
1264
1608
|
|
|
1609
|
+
# 生效条件:无条件以 fm.get("sensitivity") 调 _seal_content 封装后执行 atomic_write(path, nodefile.dumps(fm, sealed), durable=durable) 并返回 sealed,durable 原样透传、无校验;
|
|
1265
1610
|
def _write_node(self, node_id: str, path: str, fm: dict, content: str,
|
|
1266
1611
|
durable: bool = False):
|
|
1267
1612
|
"""统一节点写盘口:先封装再原子写。**所有写盘点都应走这里**,
|
|
@@ -1270,8 +1615,74 @@ class MdCG:
|
|
|
1270
1615
|
atomic_write(path, nodefile.dumps(fm, sealed), durable=durable)
|
|
1271
1616
|
return sealed
|
|
1272
1617
|
|
|
1618
|
+
# ---------- S1 前置元数据:域标签回填 ----------
|
|
1619
|
+
|
|
1620
|
+
# 生效条件:dry_run 为真时只统计不改盘;否则遍历 index["nodes"],get(nid) 取不到(无密钥/文件缺失)计入 unreadable,
|
|
1621
|
+
# fm 已有 big_domain 计入 already,content 为 None 计入 unreadable,routing.classify_text(content) 为 None 计入 no_signal,
|
|
1622
|
+
# 其余写 fm["big_domain"] → _write_node → 同步索引 entry["big_domain"];limit 为真值时 written 达 limit 即停;写完 flush();返回统计 dict。
|
|
1623
|
+
def backfill_big_domain(self, dry_run: bool = False, limit: int = None) -> dict:
|
|
1624
|
+
"""为缺 `big_domain` 的节点补域标签(S1 大域收敛的前置元数据;幂等)。
|
|
1625
|
+
|
|
1626
|
+
只改这一列元数据,不动 content/importance/edges/条件空间;
|
|
1627
|
+
走 `get()`(解密)→ `_write_node()`(重新封装),保证加密库不会双重封装。
|
|
1628
|
+
"""
|
|
1629
|
+
st = {"seen": 0, "already": 0, "written": 0, "no_signal": 0,
|
|
1630
|
+
"unreadable": 0, "index_synced": 0, "dry_run": bool(dry_run)}
|
|
1631
|
+
# 前置:功能未开启时**不做任何写入**——域标签是 S1 的元数据,默认口径不得被改变:
|
|
1632
|
+
# 否则「回填写索引」会与「_scan_nodes 默认关剥键」冲突,写/重建两条路口径不一致。
|
|
1633
|
+
if os.environ.get("MDCG_RETRIEVAL_PIPELINE") != "1":
|
|
1634
|
+
st["skipped"] = "pipeline_disabled"
|
|
1635
|
+
return st
|
|
1636
|
+
for nid, e in list((self.index.get("nodes") or {}).items()):
|
|
1637
|
+
if limit and st["written"] >= limit:
|
|
1638
|
+
break
|
|
1639
|
+
st["seen"] += 1
|
|
1640
|
+
got = self.get(nid)
|
|
1641
|
+
if not got:
|
|
1642
|
+
st["unreadable"] += 1
|
|
1643
|
+
continue
|
|
1644
|
+
fm, content = got["frontmatter"], got["content"]
|
|
1645
|
+
_fm_dom = fm.get("big_domain")
|
|
1646
|
+
if _fm_dom:
|
|
1647
|
+
# 对账支路:frontmatter 已有标签但索引快照没同步(历史部分失败/旧索引)→ 修索引,不重写文件
|
|
1648
|
+
if e.get("big_domain") != _fm_dom:
|
|
1649
|
+
st["index_synced"] += 1
|
|
1650
|
+
if dry_run: # dry-run 语义:只统计,不改内存也不落盘
|
|
1651
|
+
continue
|
|
1652
|
+
e["big_domain"] = _fm_dom
|
|
1653
|
+
self._stage(nid, _strip_empty_gate_fields(e))
|
|
1654
|
+
else:
|
|
1655
|
+
st["already"] += 1
|
|
1656
|
+
continue
|
|
1657
|
+
if content is None:
|
|
1658
|
+
st["unreadable"] += 1
|
|
1659
|
+
continue
|
|
1660
|
+
try:
|
|
1661
|
+
dom = routing.classify_text(content)
|
|
1662
|
+
except Exception:
|
|
1663
|
+
dom = None
|
|
1664
|
+
if not dom:
|
|
1665
|
+
st["no_signal"] += 1
|
|
1666
|
+
continue
|
|
1667
|
+
if dry_run:
|
|
1668
|
+
st["written"] += 1
|
|
1669
|
+
continue
|
|
1670
|
+
fm["big_domain"] = dom
|
|
1671
|
+
self._write_node(nid, os.path.join(self.root, e["path"]), fm, content)
|
|
1672
|
+
e["big_domain"] = dom
|
|
1673
|
+
# 必须走 _stage:索引持久化靠 _dirty → flush → _index_log 重放,
|
|
1674
|
+
# 只改内存 entry 会在重启后丢掉标签(S1 失效,且二次回填因 fm 已有标签而跳过)
|
|
1675
|
+
self._stage(nid, _strip_empty_gate_fields(e))
|
|
1676
|
+
st["written"] += 1
|
|
1677
|
+
# 写入与「只对账索引」两种情形都要落盘:索引持久化靠 _dirty → flush → _index_log 重放,
|
|
1678
|
+
# 只 _stage 不 flush 会在重启后丢掉标签(对账支路尤其容易漏)。
|
|
1679
|
+
if not dry_run and (st["written"] or st["index_synced"]):
|
|
1680
|
+
self.flush()
|
|
1681
|
+
return st
|
|
1682
|
+
|
|
1273
1683
|
# ---------- 读 ----------
|
|
1274
1684
|
|
|
1685
|
+
# 生效条件:index["nodes"].get(node_id) 为假值时回落 self._dirty.get(node_id),仍为假值返回 None;打开 root 下 e["path"] 抛 OSError 返回 None;_open_content 返回 None(无密钥/身份不符)返回 None;否则返回 {id, frontmatter, content, path};
|
|
1275
1686
|
def get(self, node_id: str):
|
|
1276
1687
|
e = self.index["nodes"].get(node_id) or self._dirty.get(node_id)
|
|
1277
1688
|
if not e:
|
|
@@ -1287,6 +1698,7 @@ class MdCG:
|
|
|
1287
1698
|
return None # 有节点但无密钥 → 不可读即不存在
|
|
1288
1699
|
return {"id": node_id, "frontmatter": fm, "content": content, "path": e["path"]}
|
|
1289
1700
|
|
|
1701
|
+
# 生效条件:打开 os.path.join(self.root, entry["path"]) 成功时返回 nodefile.loads 的 (fm, content);抛 OSError 时返回 (None, None)(entry 的 "path" 按源码直接取键,无 .get 回落);
|
|
1290
1702
|
def _read(self, entry):
|
|
1291
1703
|
p = os.path.join(self.root, entry["path"])
|
|
1292
1704
|
try:
|
|
@@ -1298,6 +1710,7 @@ class MdCG:
|
|
|
1298
1710
|
# ---------- 资格判定(与性能 tier 正交)----------
|
|
1299
1711
|
|
|
1300
1712
|
@staticmethod
|
|
1713
|
+
# 生效条件:content 为假值时按空串扫描并返回 "";仅当某行去空白后以 "#" 开头、包含 name,且按全角或半角冒号切出的 head 去空白后等于 name 时返回该值,否则返回 ""。
|
|
1301
1714
|
def _ccg_line(content: str, name: str) -> str:
|
|
1302
1715
|
"""取 CCG 正文 `# <name>:` 行的值。
|
|
1303
1716
|
|
|
@@ -1317,6 +1730,7 @@ class MdCG:
|
|
|
1317
1730
|
return ""
|
|
1318
1731
|
|
|
1319
1732
|
@staticmethod
|
|
1733
|
+
# 生效条件:cond_text 为假值时按空串返回空列表;仅当按槽分隔与槽内分隔切出的短语长度≥2、非纯数字且不含时间维哨兵短语时进入返回列表,重复短语只保留首次。
|
|
1320
1734
|
def _cond_terms(cond_text: str):
|
|
1321
1735
|
"""生效条件声明 → 匹配短语列表(确定性切分,无语义猜测)。
|
|
1322
1736
|
|
|
@@ -1343,6 +1757,7 @@ class MdCG:
|
|
|
1343
1757
|
return out
|
|
1344
1758
|
|
|
1345
1759
|
@staticmethod
|
|
1760
|
+
# 生效条件:node_dict 的 content 经 ccg_completeness 判为不完整 → BLINDSPOT;否则由 query 与 context(仅当 context 为 dict 时并入)合成情境串,命中 frontmatter 的任一 non_applicable_conditions 词 → REJECT;否则情境非空(query 去空白后非空,或 context 为真)且「生效条件」文本非空、非"无条件"、其词项全未命中且词项非空 → DEFER;否则无 verification_basis → DEFER;否则 ACCEPT;
|
|
1346
1761
|
def judge_qualification(node_dict, query: str, context=None):
|
|
1347
1762
|
"""四态判定(白箱第 1/2 篇)。
|
|
1348
1763
|
|
|
@@ -1418,10 +1833,13 @@ class MdCG:
|
|
|
1418
1833
|
|
|
1419
1834
|
# ---------- 检索(性能阶梯 + 资格判定)----------
|
|
1420
1835
|
|
|
1836
|
+
# 生效条件:query strip 后为空即返回 ([], {"tier": None, "reason": "empty_query", "scanned": 0});非空时按 T0–T3 性能阶梯取候选(layer / session / branch / validity / view 为假值时对应维度不过滤),judge 为真时对每条结果附独立的四态资格判定;返回 (results, meta);
|
|
1421
1837
|
def search(self, query: str, layer: str = None, k: int = 20,
|
|
1422
1838
|
context=None, min_results: int = 1, record: bool = True,
|
|
1423
1839
|
include_neg: bool = True, judge: bool = True, pools=None,
|
|
1424
|
-
session=None, branch=None
|
|
1840
|
+
session=None, branch=None, validity=None,
|
|
1841
|
+
start_time=None, end_time=None, start_operator=None,
|
|
1842
|
+
end_operator=None, time_axis=None, view=None):
|
|
1425
1843
|
"""返回 (results, meta)。results = [(node_dict, score, qualification)]。
|
|
1426
1844
|
|
|
1427
1845
|
meta 含 tier(性能层级)、scanned(读取节点数)、bucket(路由桶)、candidates。
|
|
@@ -1434,11 +1852,26 @@ class MdCG:
|
|
|
1434
1852
|
pools:(§七 召回分池)None=关闭(默认,沿用 GLOBAL_CAP 平截,原行为);
|
|
1435
1853
|
True=内置显式权重表;dict=自定义表(各池 cap_ratio 之和必须 == 1.0)。
|
|
1436
1854
|
也受 MDCG_POOLING=1 影响(载体侧开关)。分池只重分配截断额度、不抬高上限。
|
|
1855
|
+
validity:时效过滤(显式启用,缺省 None 不过滤)——真值时剔除**已过期**
|
|
1856
|
+
(valid_until 已过)节点;**未生效(valid_from 未到)保留**,因其与
|
|
1857
|
+
「已失效」语义相反(scrub 纪律「valid_from 绝不并入 _EXPIRY_KEYS」)。
|
|
1858
|
+
判定走 trust.validity 唯一真源;时间轴缺失/端点不可解析 → 不过滤(不猜测)。
|
|
1859
|
+
start_time/end_time/start_operator/end_operator/time_axis:时间算子
|
|
1860
|
+
(阶段二 4.1,**显式启用**)——按 `time_axis` 轴把候选收敛到查询时间窗内。
|
|
1861
|
+
轴未指定 → 回落 `effective`;轴/算子非法、孤 operator、start>end 一律
|
|
1862
|
+
`ValueError`(fail-closed 只针对**调用方误用**,不针对节点缺字段)。
|
|
1863
|
+
效力轴缺字段 fail-open(保留)、观察轴缺字段 fail-closed(剔除)。
|
|
1864
|
+
审计落 `meta["time_filter"]`,且**仅在启用时**落键(默认关零变更)。
|
|
1865
|
+
view:角色化读取视图(第四阶段 6.1,显式启用;roleviews.py 规则表)——
|
|
1866
|
+
main/verifier/receipt 三视图各自声明候选资格(content_kind/
|
|
1867
|
+
layer/role 维度),view 非空时叠加资格谓词;非法视图
|
|
1868
|
+
ValueError(fail-closed 只针对调用方误用)。
|
|
1437
1869
|
"""
|
|
1438
1870
|
q = (query or "").strip()
|
|
1439
1871
|
if not q:
|
|
1440
1872
|
return [], {"tier": None, "reason": "empty_query", "scanned": 0}
|
|
1441
1873
|
pool_cfg = pooling.resolve(pooling.from_env(pools))
|
|
1874
|
+
now = time.time() if validity else None # 统一取一次 now,保判定口径一致
|
|
1442
1875
|
|
|
1443
1876
|
terms = expand_query_terms(q)
|
|
1444
1877
|
# 英文归一化后再取 bigram(小写+去停用词+去时态;中文不动)
|
|
@@ -1452,9 +1885,175 @@ class MdCG:
|
|
|
1452
1885
|
and (not session or e.get("session") == session)
|
|
1453
1886
|
# 分支实验场:默认(branch=None)分支节点全部隐身;
|
|
1454
1887
|
# branch=<id> 时主支 + 本分支可见、其他分支仍隐身
|
|
1455
|
-
and e.get("branch_id") in (None, branch)
|
|
1888
|
+
and e.get("branch_id") in (None, branch)
|
|
1889
|
+
# 角色化读取视图(第四阶段 6.1):view 非空时叠加资格谓词
|
|
1890
|
+
# (非法视图 ValueError——fail-closed 只针对调用方误用)
|
|
1891
|
+
and (view is None or roleviews.matches(e, view))
|
|
1892
|
+
# 时效:只在显式启用时排除已过期(not_yet 保留)
|
|
1893
|
+
and not (validity and trust.is_expired(e, now=now))]
|
|
1894
|
+
|
|
1895
|
+
# 默认关:索引里可能残留门控字段(曾开启过 / 回填过)→ 返回前剥离,
|
|
1896
|
+
# 保证候选 entry 形状与「从未启用过本功能」逐字节一致(独立复核 2026-09-19)。
|
|
1897
|
+
# 只在确有残留时才拷贝(默认路径无额外开销)。
|
|
1898
|
+
if (os.environ.get("MDCG_RETRIEVAL_PIPELINE") != "1" and entries
|
|
1899
|
+
and any(("big_domain" in e) or ("observation_position" in e)
|
|
1900
|
+
for e in entries)):
|
|
1901
|
+
entries = [_strip_empty_gate_fields(dict(e), enabled=False)
|
|
1902
|
+
for e in entries]
|
|
1903
|
+
|
|
1904
|
+
# ---- 时间算子(阶段二 4.1):候选**资格**过滤(在 S1/S2 收敛之前)----
|
|
1905
|
+
# 与 validity 各司其职、互不替代:
|
|
1906
|
+
# validity = 「是否已失效」——默认关、只排过期、未生效一律保留;
|
|
1907
|
+
# 时间算子 = 「是否落在查询时间窗内」——轴 + 算子显式启用,双端可空 = 无界。
|
|
1908
|
+
# fail-closed 只针对**调用方误用**(轴/算子非法、孤 operator、start>end →
|
|
1909
|
+
# ValueError);节点缺字段按**轴策略**处置:效力轴 fail-open(可选声明,
|
|
1910
|
+
# 缺 = 沉默不是否认)、观察轴 fail-closed(写入侧保证存在,缺 = 数据异常,
|
|
1911
|
+
# 须计数可见而非静默放行)。审计块仅在启用时落键(默认关零变更纪律)。
|
|
1912
|
+
_tf_audit = None
|
|
1913
|
+
_en_t, _ax_t, _why_t = trust.check_time_args(
|
|
1914
|
+
start_time, end_time, start_operator, end_operator, time_axis)
|
|
1915
|
+
if _why_t:
|
|
1916
|
+
raise ValueError(_why_t)
|
|
1917
|
+
if _en_t:
|
|
1918
|
+
_qs_t, _qe_t = trust.parse_time(start_time), trust.parse_time(end_time)
|
|
1919
|
+
entries, _dr_t, _ms_t = trust.filter_by_time(
|
|
1920
|
+
entries, _ax_t, _qs_t, _qe_t, start_operator, end_operator)
|
|
1921
|
+
_tf_audit = trust.time_filter_meta(
|
|
1922
|
+
axis=_ax_t,
|
|
1923
|
+
mode="endpoint" if (start_operator or end_operator) else "overlap",
|
|
1924
|
+
start=_qs_t, end=_qe_t, start_operator=start_operator,
|
|
1925
|
+
end_operator=end_operator, dropped=_dr_t, axis_missing=_ms_t,
|
|
1926
|
+
applied=True)
|
|
1927
|
+
|
|
1928
|
+
# ---- S1/S2 检索前门控(契约 docs/hive/检索路径与认知结构契约_v0.1.md)----
|
|
1929
|
+
# 历史偏差:大域先验与条件空间都只在「扫完 + 排完」之后才用(审计偏差 2/4)。
|
|
1930
|
+
# 这里把它们前移到候选构建:候选先按域收敛、再按可判定硬槽门控。
|
|
1931
|
+
# 默认(MDCG_RETRIEVAL_PIPELINE 未设)全关 → 行为与改动前等价。
|
|
1932
|
+
big_domain = routing.big_domain_classify(terms)
|
|
1933
|
+
big_scores = routing.big_domain_score_breakdown(terms)
|
|
1934
|
+
gates = {}
|
|
1935
|
+
# S3 子开关**必须显式 =1**(与 S1/S2 的「未设=开」不同,是有意的非对称):
|
|
1936
|
+
# S3 会新增候选并引入新的 tier(TIER_SPREAD),在总开关开启时若默认随开,
|
|
1937
|
+
# 会让「启用 S1/S2」的用户静默多出一层结果——独立复核(2026-09-19)要求显式启用。
|
|
1938
|
+
# 参数:hops(默认 2)、decay(默认 0.5)、gain(默认 0.2)。
|
|
1939
|
+
_s3 = (os.environ.get("MDCG_RETRIEVAL_PIPELINE") == "1"
|
|
1940
|
+
and os.environ.get("MDCG_GATE_S3_SPREAD") == "1")
|
|
1941
|
+
# S4 层级激活优先级:同样必须显式 =1(新层加成会改变排序,属显式启用项)
|
|
1942
|
+
_s4 = (os.environ.get("MDCG_RETRIEVAL_PIPELINE") == "1"
|
|
1943
|
+
and os.environ.get("MDCG_GATE_S4_LAYER") == "1")
|
|
1944
|
+
# S7 倒排候选层(只作候选生成器;召回与全表扫描一致);见 md_cg/postings.py
|
|
1945
|
+
_s7 = (os.environ.get("MDCG_RETRIEVAL_PIPELINE") == "1"
|
|
1946
|
+
and os.environ.get("MDCG_GATE_S7_POSTINGS") == "1")
|
|
1947
|
+
_s7_narrowed = False
|
|
1948
|
+
if _s4 and entries:
|
|
1949
|
+
_bo = layer_boosts()
|
|
1950
|
+
_lc = {}
|
|
1951
|
+
for _e in entries:
|
|
1952
|
+
_l = str(_e.get("layer") or "")
|
|
1953
|
+
_lc[_l] = _lc.get(_l, 0) + 1
|
|
1954
|
+
gates["s4"] = {"boosts": _bo, "layers": _lc}
|
|
1955
|
+
try:
|
|
1956
|
+
_s3_hops = max(1, int(os.environ.get("MDCG_SPREAD_HOPS", "2")))
|
|
1957
|
+
except ValueError:
|
|
1958
|
+
_s3_hops = 2
|
|
1959
|
+
try:
|
|
1960
|
+
_s3_decay = min(1.0, max(0.0, float(os.environ.get("MDCG_SPREAD_DECAY", "0.5"))))
|
|
1961
|
+
except ValueError:
|
|
1962
|
+
_s3_decay = 0.5
|
|
1963
|
+
try:
|
|
1964
|
+
_s3_gain = min(1.0, max(0.0, float(os.environ.get("MDCG_SPREAD_GAIN", "0.2"))))
|
|
1965
|
+
except ValueError:
|
|
1966
|
+
_s3_gain = 0.2
|
|
1967
|
+
if os.environ.get("MDCG_RETRIEVAL_PIPELINE") == "1" and entries:
|
|
1968
|
+
if os.environ.get("MDCG_GATE_S1_DOMAIN", "1") != "0" and big_domain:
|
|
1969
|
+
# 域内 ∪ 未标域(兜底池):无域标签的历史节点绝不能因「域内够多」被丢
|
|
1970
|
+
#(契约 §3 S1 不变量:ORPHAN/未标域必须可被召回)
|
|
1971
|
+
same = [e for e in entries
|
|
1972
|
+
if not e.get("big_domain") or e.get("big_domain") == big_domain]
|
|
1973
|
+
# 召回安全:域内候选不足以支撑 min_results 时不收敛,留全量兜底
|
|
1974
|
+
if len(same) >= max(1, min_results):
|
|
1975
|
+
gates["s1"] = {"domain": big_domain, "in": len(same),
|
|
1976
|
+
"dropped": len(entries) - len(same)}
|
|
1977
|
+
entries = same
|
|
1978
|
+
else:
|
|
1979
|
+
gates["s1"] = {"domain": big_domain, "in": len(same),
|
|
1980
|
+
"dropped": 0, "fallback": "insufficient"}
|
|
1981
|
+
elif os.environ.get("MDCG_GATE_S1_DOMAIN", "1") != "0":
|
|
1982
|
+
gates["s1"] = {"domain": None, "reason": "no_domain_signal"}
|
|
1983
|
+
# ---- S1b 细口径桶收敛(设计见 docs/hive/检索收敛实测与S1b设计_v0.1.md)----
|
|
1984
|
+
# 为何:真实库 91.1% 节点有非 orphan 桶、期望扫描仅 2.1%(细口径),但 T0/T1 桶路
|
|
1985
|
+
# 只在调用方传 context 时可用;普通 search(q) 无 context 就只能全扫。S1b 让 query 侧
|
|
1986
|
+
# 自己推断候选桶(只用索引,不读文件),把这份收敛拿回来。
|
|
1987
|
+
# 召回安全:orphan/无桶节点恒留兜底;命中不足 min_results 即回退(不改 entries)。
|
|
1988
|
+
# 开关与参数就地定义(位于“总开关块”内,不在 S4 块内):S1b 只依赖总开关 + MDCG_GATE_S1B_BUCKET。
|
|
1989
|
+
_s1b = (os.environ.get("MDCG_GATE_S1B_BUCKET") == "1")
|
|
1990
|
+
try:
|
|
1991
|
+
_s1b_topk = int(os.environ.get("MDCG_BUCKET_TOPK", "3"))
|
|
1992
|
+
except ValueError:
|
|
1993
|
+
_s1b_topk = 3
|
|
1994
|
+
try:
|
|
1995
|
+
_s1b_minsim = min(1.0, max(0.0, float(os.environ.get("MDCG_BUCKET_MIN_SIM", "0.34"))))
|
|
1996
|
+
except ValueError:
|
|
1997
|
+
_s1b_minsim = 0.34
|
|
1998
|
+
if _s1b and entries:
|
|
1999
|
+
if _s1b_topk <= 0:
|
|
2000
|
+
gates["s1b"] = {"keys": [], "reason": "disabled_by_topk"}
|
|
2001
|
+
else:
|
|
2002
|
+
_sizes = {}
|
|
2003
|
+
for _e in entries:
|
|
2004
|
+
_b = _e.get("bucket")
|
|
2005
|
+
if _b and _b != routing.ORPHAN:
|
|
2006
|
+
_sizes[_b] = _sizes.get(_b, 0) + 1
|
|
2007
|
+
_best = {}
|
|
2008
|
+
for _b in _sizes:
|
|
2009
|
+
_kk = routing.bucket_key_readable(_b)
|
|
2010
|
+
if not _kk:
|
|
2011
|
+
continue
|
|
2012
|
+
_sim = 0.0
|
|
2013
|
+
for _t in terms:
|
|
2014
|
+
_sv = routing.domain_similarity(_t, _kk)
|
|
2015
|
+
if _sv > _sim:
|
|
2016
|
+
_sim = _sv
|
|
2017
|
+
if _sim >= _s1b_minsim:
|
|
2018
|
+
_best[_b] = _sim
|
|
2019
|
+
if not _best:
|
|
2020
|
+
gates["s1b"] = {"keys": [], "reason": "no_key_match",
|
|
2021
|
+
"in": len(entries), "buckets": len(_sizes)}
|
|
2022
|
+
else:
|
|
2023
|
+
_picked = sorted(_best.items(),
|
|
2024
|
+
key=lambda kv: (-kv[1], -_sizes[kv[0]],
|
|
2025
|
+
str(kv[0])))[:_s1b_topk]
|
|
2026
|
+
_keep = {_b for _b, _ in _picked}
|
|
2027
|
+
_kept = [e for e in entries
|
|
2028
|
+
if (e.get("bucket") in _keep)
|
|
2029
|
+
or not e.get("bucket")
|
|
2030
|
+
or e.get("bucket") == routing.ORPHAN]
|
|
2031
|
+
gates["s1b"] = {"keys": [_b for _b, _ in _picked],
|
|
2032
|
+
"sims": [round(_v, 4) for _, _v in _picked],
|
|
2033
|
+
"in": len(entries), "out": len(_kept),
|
|
2034
|
+
"sizes": {_b: _sizes[_b] for _b, _ in _picked}}
|
|
2035
|
+
if len(_kept) >= max(1, min_results):
|
|
2036
|
+
entries = _kept
|
|
2037
|
+
else:
|
|
2038
|
+
# 回退:entries 保持不变 → 审计的 out 必须记「真实输出规模」,
|
|
2039
|
+
# 命中桶本可保留的数量另存 would_keep(独立复核 2026-09-19 指出口径误导)
|
|
2040
|
+
gates["s1b"]["would_keep"] = len(_kept)
|
|
2041
|
+
gates["s1b"]["out"] = len(entries)
|
|
2042
|
+
gates["s1b"]["fallback"] = "insufficient"
|
|
2043
|
+
if os.environ.get("MDCG_GATE_S2_COND", "1") != "0" and isinstance(context, dict):
|
|
2044
|
+
kept = [e for e in entries if _cond_prefilter_pass(e, context)]
|
|
2045
|
+
gates["s2"] = {"in": len(entries), "out": len(kept),
|
|
2046
|
+
"dropped": len(entries) - len(kept)}
|
|
2047
|
+
# 门控清空则回退(宁多勿漏)
|
|
2048
|
+
if kept:
|
|
2049
|
+
entries = kept
|
|
2050
|
+
else:
|
|
2051
|
+
gates["s2"]["fallback"] = "empty"
|
|
1456
2052
|
if not entries:
|
|
1457
|
-
|
|
2053
|
+
_m = {"tier": None, "reason": "no_candidates", "scanned": 0}
|
|
2054
|
+
if gates: # 默认关时 gates 为空 → 不落键(口径与改动前一致)
|
|
2055
|
+
_m["gates"] = gates
|
|
2056
|
+
return [], _m
|
|
1458
2057
|
|
|
1459
2058
|
# 负记忆覆盖:查询词是否已被否决议过
|
|
1460
2059
|
neg_coverage = []
|
|
@@ -1475,17 +2074,18 @@ class MdCG:
|
|
|
1475
2074
|
if any(t in content for t in terms):
|
|
1476
2075
|
neg_coverage.append(e)
|
|
1477
2076
|
|
|
1478
|
-
stat = {"scanned": 0, "query": q}
|
|
2077
|
+
stat = {"scanned": 0, "query": q, "gates": gates}
|
|
2078
|
+
if _tf_audit:
|
|
2079
|
+
stat["time_filter"] = _tf_audit
|
|
1479
2080
|
route_bucket = None
|
|
1480
2081
|
if context is not None:
|
|
1481
2082
|
ctx = context if isinstance(context, dict) else {}
|
|
1482
2083
|
route_bucket = routing.bucket_dir(
|
|
1483
2084
|
routing.route_key(ctx, ctx.get("tags")))
|
|
1484
2085
|
|
|
1485
|
-
# 阶段 1
|
|
1486
|
-
big_domain = routing.big_domain_classify(terms)
|
|
1487
|
-
big_scores = routing.big_domain_score_breakdown(terms)
|
|
2086
|
+
# 阶段 1 大域打分已在候选构建前算好(S1 门控要用);此处不再重复计算。
|
|
1488
2087
|
|
|
2088
|
+
# 生效条件:docs 经 self._score(docs, q, qb, pool_cfg) 后分数 >0 的条数达到闭包阈值 min_results 时返回 self._emit(scored, k, tier, stat, route_bucket, record, len(docs), judge, context, neg_coverage, big_domain, big_scores, pool_cfg)(tier 原样透传);未达阈值返回 None;
|
|
1489
2089
|
def try_stage(docs, tier):
|
|
1490
2090
|
scored = self._score(docs, q, qb, pool_cfg)
|
|
1491
2091
|
valid = sum(1 for _, s in scored if s > 0)
|
|
@@ -1510,14 +2110,150 @@ class MdCG:
|
|
|
1510
2110
|
if out:
|
|
1511
2111
|
return out
|
|
1512
2112
|
|
|
2113
|
+
# T1'(reach):大域收敛 → 条件门控 → 图扩散(2026-09-18 新增)。
|
|
2114
|
+
# 层级仍报 TIER_GLOBAL_LIKE(它就是「全量 LIKE」阶段的收敛实现,test_p0 契约
|
|
2115
|
+
# 「无 context 走全量阶梯」不因优化而变);收敛与否由 meta.reach* 字段区分。
|
|
2116
|
+
# 收敛集是 LIKE 命中集与词法打分>0 集合的**超集**(必要条件倒排,见
|
|
2117
|
+
# md_cg/test_reach.py 包含性断言),故本阶段不可能丢召回;任一不适用条件
|
|
2118
|
+
# (MDCG_REACH 未开启 / 单字符 term / 索引不可用 / 收敛集为空)整段跳过,由下方全量阶段兜底。
|
|
2119
|
+
# 默认关(契约 §7「不在默认路径上启用新行为」):整段不进入 → meta 键集合与改动前逐字节一致。
|
|
2120
|
+
reach_entries, _rstat = None, {}
|
|
2121
|
+
if reach.enabled():
|
|
2122
|
+
reach_entries, _rstat = reach.narrow(self, entries, terms, qb, context, q=q)
|
|
2123
|
+
if _rstat:
|
|
2124
|
+
stat.update(_rstat)
|
|
2125
|
+
if reach_entries is not None:
|
|
2126
|
+
_pre_scan = int(stat.get("scanned") or 0) # 收敛阶段读盘基线(供回退审计)
|
|
2127
|
+
docs_r = self._read_many(reach_entries, stat)
|
|
2128
|
+
_dif = set(_rstat.get("reach_diffused_paths") or ())
|
|
2129
|
+
hits_r = [d for d in docs_r
|
|
2130
|
+
if self._like(d[2], d[1], terms)
|
|
2131
|
+
or (semantic_on() and d[1].get("semantic"))
|
|
2132
|
+
or d[0].get("path") in _dif] # 图扩散补召回:无词面命中也放行进打分
|
|
2133
|
+
stat["pre_cap"] = len(hits_r) # 与 T2 同序:截断**前**的候选数
|
|
2134
|
+
stat["cap"] = GLOBAL_CAP
|
|
2135
|
+
# 与 T2 **无条件**同序调用(不可按 cap 短路:cut_by_relevance 还会写
|
|
2136
|
+
# 池账 pool_taken/cands/lost,短路会让 meta.pools.taken 缺失 → test_p43(13) 红)
|
|
2137
|
+
hits_r, _rep = cut_by_relevance(hits_r, self._score(hits_r, q, qb, pool_cfg),
|
|
2138
|
+
GLOBAL_CAP, pools=pool_cfg,
|
|
2139
|
+
key_of=pooling.doc_key, stat=stat)
|
|
2140
|
+
pooling.record_audit(stat, _rep)
|
|
2141
|
+
out = try_stage(hits_r, TIER_GLOBAL_LIKE)
|
|
2142
|
+
if out:
|
|
2143
|
+
return out
|
|
2144
|
+
# 收敛阶段未产出结果 → 继续走下方全量 T2/T3;此时必须**改写 reach 审计**,
|
|
2145
|
+
# 否则 meta.reach 谎报 converged、A3 也会把「收敛读+全量读」双计当成收敛(r14 复核取证)
|
|
2146
|
+
stat["reach"] = "reverted" # 已回退:本次结果不是收敛路径产出的
|
|
2147
|
+
stat["reach_reverted"] = True
|
|
2148
|
+
# 收敛阶段确实读过盘 → 如实暴露其读盘量,A3 可据此扣减(**不**回滚 scanned:
|
|
2149
|
+
# 累计读盘是事实;也**不**清除 reach_build_docs:首建成本真实发生过,r16 复核取证)
|
|
2150
|
+
stat["reach_reverted_docs"] = int(stat.get("scanned") or 0) - _pre_scan
|
|
2151
|
+
for _k in ("reach_seed", "reach_diffused", "reach_hops", "reach_diffused_paths",
|
|
2152
|
+
"reach_fresh_nodes"):
|
|
2153
|
+
stat.pop(_k, None)
|
|
2154
|
+
|
|
1513
2155
|
# T2:跨桶 LIKE(§七 截断点:分池截断,索引类不再挤掉知识类)
|
|
1514
2156
|
# 截断依据=相关度(先全量打分再排序截断):命中集沿 entries(目录枚举序)
|
|
1515
2157
|
# 排列,原来「取前 GLOBAL_CAP 条再打分」等价于用写入顺序抽签决定谁进
|
|
1516
2158
|
# 候选池。cap 值不变,变的只是拿什么排序(见 cut_by_relevance)。
|
|
2159
|
+
# ---- S7 倒排候选层:用发布表取出「含查询词全部 bigram」的节点作为候选 ----
|
|
2160
|
+
# 候选 ⊇ 真命中集 → 随后仍走既有 `_like` 精确过滤 → 结果与全表扫描一致(契约 §7「候选内加速」定位)。
|
|
2161
|
+
# 不可用时(单字词 / 无发布表 / 空候选)一律回退全表;T3 兜底也强制走全量(保持兜底口径不变)。
|
|
2162
|
+
entries_full = entries
|
|
2163
|
+
if _s7 and entries:
|
|
2164
|
+
_pd = None
|
|
2165
|
+
try:
|
|
2166
|
+
from . import postings as _pd
|
|
2167
|
+
except Exception:
|
|
2168
|
+
_pd = None
|
|
2169
|
+
if _pd is None:
|
|
2170
|
+
gates["s7"] = {"reason": "no_module", "in": len(entries_full),
|
|
2171
|
+
"fallback": "full_scan"}
|
|
2172
|
+
else:
|
|
2173
|
+
# 发布表是**快照**:节点被改写后可能漏掉命中 → 先验快照指纹,过期即回退全量
|
|
2174
|
+
# (宁可慢,不可丢召回)。指纹只做 stat(≈目录数,本库 1,074 目录 ≈ 0.03s)。
|
|
2175
|
+
# MDCG_S7_FRESHNESS=skip 只供离线对照实测使用;生产默认 auto。
|
|
2176
|
+
# 组合语义(S7×语义路):MDCG_SEMANTIC=1 时「frontmatter 有 semantic 的节点」
|
|
2177
|
+
# 在 _like 之外**无条件入池**(见下方 hits 构造),而该标志只在节点文件里
|
|
2178
|
+
# (索引快照没有该键)→ 候选阶段无法识别它们,窄化会漏读 → 漏召回。
|
|
2179
|
+
# 故语义路开启时 S7 一律不窄化(独立复核 2026-09-19 REJECT 第 1 条)。
|
|
2180
|
+
_sem = semantic_on()
|
|
2181
|
+
_stale = ""
|
|
2182
|
+
if not _sem and os.environ.get("MDCG_S7_FRESHNESS") != "skip":
|
|
2183
|
+
_stale = _pd.stale_reason(self.root, self.index.get("nodes") or {})
|
|
2184
|
+
if _sem:
|
|
2185
|
+
gates["s7"] = {"reason": "semantic_on", "in": len(entries_full),
|
|
2186
|
+
"fallback": "full_scan"}
|
|
2187
|
+
elif _stale:
|
|
2188
|
+
gates["s7"] = {"reason": "stale_index:" + _stale,
|
|
2189
|
+
"in": len(entries_full), "fallback": "full_scan"}
|
|
2190
|
+
else:
|
|
2191
|
+
_ids7, _why = _pd.candidates(self.root, terms)
|
|
2192
|
+
if _ids7 is None:
|
|
2193
|
+
gates["s7"] = {"reason": _why, "in": len(entries_full),
|
|
2194
|
+
"fallback": "full_scan"}
|
|
2195
|
+
else:
|
|
2196
|
+
# 保持**索引原序**(不是发布表序):候选集是过滤,不是重排。
|
|
2197
|
+
# 完全并列(同分同重要性)时排序稳定,故只有原序一致,S7 的
|
|
2198
|
+
# results 才能与全表扫描逐字节一致(独立复核口径)。
|
|
2199
|
+
_keep = []
|
|
2200
|
+
for _e in entries_full:
|
|
2201
|
+
_nid = _e.get("id") or os.path.splitext(
|
|
2202
|
+
os.path.basename(_e.get("path") or ""))[0]
|
|
2203
|
+
if _nid in _ids7:
|
|
2204
|
+
_keep.append(_e)
|
|
2205
|
+
gates["s7"] = {"cands": len(_keep), "in": len(entries_full),
|
|
2206
|
+
"terms": len(terms)}
|
|
2207
|
+
if _keep:
|
|
2208
|
+
entries = _keep
|
|
2209
|
+
_s7_narrowed = True
|
|
2210
|
+
else:
|
|
2211
|
+
gates["s7"]["fallback"] = "no_candidate"
|
|
2212
|
+
_s7_scan0 = stat["scanned"] # S7 窄化前的扫描基线(供 T3 兜底还原口径)
|
|
1517
2213
|
docs_all = self._read_many(entries, stat)
|
|
1518
2214
|
# 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
|
|
1519
2215
|
hits = [d for d in docs_all if self._like(d[2], d[1], terms)
|
|
1520
2216
|
or (semantic_on() and d[1].get("semantic"))]
|
|
2217
|
+
# ---- S3 图扩散激活(契约 §3 S3;flag 控,默认关)----
|
|
2218
|
+
# 为何:edges 一直只被写入、检索从不使用(审计偏差 1)。这里从词法命中节点沿
|
|
2219
|
+
# edges 双向扩散,把「关联但词面不重叠」的记忆作为独立一层候选(TIER_SPREAD)。
|
|
2220
|
+
# 约束:扩散只走索引里的 edges(不读文件);**不得引入未通过 S1/S2 门控**的节点;
|
|
2221
|
+
# 命中不足时自然落回原 T2/T3 路径(无召回损失)。
|
|
2222
|
+
if _s3 and hits:
|
|
2223
|
+
# 组合语义(S3×S7):S7 只是 T2 的候选加速器,**不得**改变 S3 的可达域。
|
|
2224
|
+
# 若把窄化后的 entries 当 allowed,S7+S3 会把扩散限制在词法候选内,
|
|
2225
|
+
# 恰好丢掉 S3 的目标(「只沿 edges 可达、词面无交集」的节点)→ 组合退化。
|
|
2226
|
+
# 故 allowed 恒取 S1/S2 门控后的全量候选(S7 关时 entries 即全量,行为不变)。
|
|
2227
|
+
_s3_pool = entries_full if _s7_narrowed else entries
|
|
2228
|
+
_act, _seeds = self._spread_activation(_s3_pool, hits, _s3_hops, _s3_decay)
|
|
2229
|
+
if _act:
|
|
2230
|
+
_by_path = {e.get("path"): e for e in _s3_pool}
|
|
2231
|
+
_extra_entries = [_by_path[p] for p in _act if p in _by_path]
|
|
2232
|
+
_extra_docs = self._read_many(_extra_entries, stat)
|
|
2233
|
+
_seen = {d[0]["path"] for d in hits}
|
|
2234
|
+
_extra_docs = [d for d in _extra_docs if d[0]["path"] not in _seen]
|
|
2235
|
+
_sc_hits = self._score(hits, q, qb, pool_cfg)
|
|
2236
|
+
_sc_extra = self._score(_extra_docs, q, qb, pool_cfg)
|
|
2237
|
+
_bonus = [(doc, max(sc, _act.get(doc["path"], 0.0) * _s3_gain))
|
|
2238
|
+
for doc, sc in _sc_extra]
|
|
2239
|
+
_merged = _sc_hits + _bonus
|
|
2240
|
+
_valid = sum(1 for _, s in _merged if s > 0)
|
|
2241
|
+
gates["s3"] = {"seeds": len(_seeds), "expanded": len(_bonus),
|
|
2242
|
+
"hops": _s3_hops, "decay": _s3_decay,
|
|
2243
|
+
"gain": _s3_gain, "valid": _valid}
|
|
2244
|
+
if _valid >= min_results:
|
|
2245
|
+
_merged.sort(key=lambda x: (-x[1], -float(
|
|
2246
|
+
x[0]["frontmatter"].get("importance") or 0),
|
|
2247
|
+
str(x[0].get("id") or "")))
|
|
2248
|
+
if record and _merged[:k]:
|
|
2249
|
+
self.record_access([r[0]["id"] for r in _merged[:k]], TIER_SPREAD)
|
|
2250
|
+
return self._emit(_merged, k, TIER_SPREAD, stat, route_bucket,
|
|
2251
|
+
record, len(_merged), judge, context,
|
|
2252
|
+
neg_coverage, big_domain, big_scores, pool_cfg)
|
|
2253
|
+
else:
|
|
2254
|
+
gates["s3"] = {"seeds": len(_seeds), "expanded": 0,
|
|
2255
|
+
"hops": _s3_hops, "reason": "no_edges"}
|
|
2256
|
+
|
|
1521
2257
|
stat["pre_cap"] = len(hits)
|
|
1522
2258
|
stat["cap"] = GLOBAL_CAP
|
|
1523
2259
|
hits, _rep = cut_by_relevance(hits, self._score(hits, q, qb, pool_cfg),
|
|
@@ -1529,6 +2265,18 @@ class MdCG:
|
|
|
1529
2265
|
return out
|
|
1530
2266
|
|
|
1531
2267
|
# T3:全量兜底(同为分池截断点;截断依据同为相关度,importance 作次级键)
|
|
2268
|
+
# S7 只作 T2 的候选加速;T2 未达阈值时兜底必须回到**全量**,否则兜底口径被 S7 改变(与改动前不等价)。
|
|
2269
|
+
if _s7_narrowed:
|
|
2270
|
+
# 口径还原:T3 是「全量兜底」,S7 只是 T2 的加速器——回退时 scanned 必须
|
|
2271
|
+
# 与「从未窄化」逐值一致,否则同配置的审计指标被候选层改变(独立复核要求)。
|
|
2272
|
+
# 窄化那趟的读取量另记为 attempted(诚实计量,不混入 scanned)。
|
|
2273
|
+
_attempted = stat["scanned"] - _s7_scan0
|
|
2274
|
+
stat["scanned"] = _s7_scan0
|
|
2275
|
+
entries = entries_full
|
|
2276
|
+
docs_all = self._read_many(entries, stat)
|
|
2277
|
+
_s7_narrowed = False
|
|
2278
|
+
gates.setdefault("s7", {}).update(
|
|
2279
|
+
{"fallback": "t3_full", "attempted": _attempted})
|
|
1532
2280
|
stat["pre_cap"] = len(docs_all)
|
|
1533
2281
|
stat["cap"] = GLOBAL_CAP
|
|
1534
2282
|
picked, _rep = cut_by_relevance(docs_all,
|
|
@@ -1541,6 +2289,54 @@ class MdCG:
|
|
|
1541
2289
|
record, len(picked), judge,
|
|
1542
2290
|
context, neg_coverage, big_domain, big_scores, pool_cfg)
|
|
1543
2291
|
|
|
2292
|
+
# 生效条件:无条件按 hits 的 path 与 allowed 集合做双向邻接扩散(只读 index["nodes"] 的 edges/target,不读节点文件),
|
|
2293
|
+
# hops<=0 或无命中/无 allowed 时返回 ({}, seeds);否则返回 ({被扩散节点 path: decay**跳数}, seeds),不含种子自身;
|
|
2294
|
+
# 目标必须是 allowed(未过 S1/S2 门控的节点一律不引入)且在 index 中存在。
|
|
2295
|
+
def _spread_activation(self, entries, hits, hops, decay):
|
|
2296
|
+
"""从词法命中节点沿 edges 双向扩散(契约 §3 S3)。
|
|
2297
|
+
|
|
2298
|
+
只用索引快照(`edges`/`target`)不读文件;`allowed` = 已通过 S1/S2 门控的候选,
|
|
2299
|
+
因此扩散**不可能**把未过门控的节点拉进结果。返回 ({nid: 激活值}, 种子 id 集合)。
|
|
2300
|
+
"""
|
|
2301
|
+
nodes = self.index.get("nodes") or {}
|
|
2302
|
+
allowed, seeds = set(), set()
|
|
2303
|
+
for e in entries:
|
|
2304
|
+
nid = e.get("id") or os.path.splitext(os.path.basename(e.get("path") or ""))[0]
|
|
2305
|
+
if nid in nodes:
|
|
2306
|
+
allowed.add(nid)
|
|
2307
|
+
for d in hits:
|
|
2308
|
+
p = d[0].get("path")
|
|
2309
|
+
nid = os.path.splitext(os.path.basename(p or ""))[0]
|
|
2310
|
+
if nid in nodes and nid in allowed:
|
|
2311
|
+
seeds.add(nid)
|
|
2312
|
+
if hops <= 0 or not seeds:
|
|
2313
|
+
return {}, seeds
|
|
2314
|
+
adj = {}
|
|
2315
|
+
for nid in allowed:
|
|
2316
|
+
for ed in (nodes[nid].get("edges") or []):
|
|
2317
|
+
if isinstance(ed, dict):
|
|
2318
|
+
t = str(ed.get("target") or "")
|
|
2319
|
+
if t:
|
|
2320
|
+
adj.setdefault(nid, set()).add(t)
|
|
2321
|
+
adj.setdefault(t, set()).add(nid)
|
|
2322
|
+
act, seen, frontier = {}, set(seeds), set(seeds)
|
|
2323
|
+
for hop in range(1, hops + 1):
|
|
2324
|
+
nxt = set()
|
|
2325
|
+
for nid in frontier:
|
|
2326
|
+
for t in adj.get(nid, ()):
|
|
2327
|
+
if t in seen or t not in allowed or t not in nodes:
|
|
2328
|
+
continue
|
|
2329
|
+
seen.add(t)
|
|
2330
|
+
nxt.add(t)
|
|
2331
|
+
# 键用**节点文件路径**(与 search 的 entries/doc["path"] 同口径,避免 id/path 混用)
|
|
2332
|
+
act[nodes[t].get("path") or t] = decay ** hop
|
|
2333
|
+
frontier = nxt
|
|
2334
|
+
if not frontier:
|
|
2335
|
+
break
|
|
2336
|
+
return act, seeds
|
|
2337
|
+
|
|
2338
|
+
|
|
2339
|
+
# 生效条件:entries 逐条经 _read 得 content 为 None 的跳过、_open_content 返回 None 的跳过,其余以 (e, fm, c) 进入 docs,并把 len(docs) 累加进 stat["scanned"] 后返回 docs;
|
|
1544
2340
|
def _read_many(self, entries, stat):
|
|
1545
2341
|
docs = []
|
|
1546
2342
|
for e in entries:
|
|
@@ -1555,6 +2351,7 @@ class MdCG:
|
|
|
1555
2351
|
return docs
|
|
1556
2352
|
|
|
1557
2353
|
@staticmethod
|
|
2354
|
+
# 生效条件:terms 为空时返回 False;否则任一 t 在 positive_body(content) 的小写串中出现,或该 t 的小写形式出现在 fm 的 tags(tags 取自 fm.get("tags") or [],缺键或假值按空列表拼接)小写串中即返回 True。
|
|
1558
2355
|
def _like(content, fm, terms):
|
|
1559
2356
|
# 负条件行(`# 不适用条件:`)是反例声明,不作召回键:命中它只应由
|
|
1560
2357
|
# judge_qualification 走 REJECT,不能把节点召回。tags 仍参与匹配。
|
|
@@ -1565,6 +2362,7 @@ class MdCG:
|
|
|
1565
2362
|
tags_l = tags.lower()
|
|
1566
2363
|
return any(t in body_l or t.lower() in tags_l for t in terms)
|
|
1567
2364
|
|
|
2365
|
+
# 生效条件:semantic_on() 为真、semantic.canonical.pair_hits 导入成功且该 doc 的 fm 有 semantic 时 raw = min(1.0, max(lexical_sim(qb, 该文档 normalize_en 后的 bigram, mode), pair_hits(fm["semantic"], q)) + tag_bonus),否则 raw = min(1.0, sim + tag_bonus);tag_bonus 仅在 fm.tags 中有 t 满足 str(t) in q 或 q in str(t) 时为 0.05,否则 0.0;pools 为真值时 raw 再乘 pooling.weight_of(...) 并夹到 [0,1];返回 scored 列表;
|
|
1568
2366
|
def _score(self, docs, q, qb, pools=None, mode=None):
|
|
1569
2367
|
# 语义摘要路(MDCG_SEMANTIC=1 opt-in,默认关闭零回归):
|
|
1570
2368
|
# doc 侧 fm.semantic 标准原子序列 × query 归一序列的组合窗口共现分
|
|
@@ -1580,6 +2378,10 @@ class MdCG:
|
|
|
1580
2378
|
_pair_hits = _canon.pair_hits
|
|
1581
2379
|
except Exception:
|
|
1582
2380
|
sem_on = False
|
|
2381
|
+
# S4 层级激活优先级(契约 §3 S4):加成而非过滤;子开关须显式 =1
|
|
2382
|
+
_s4_on = (os.environ.get("MDCG_RETRIEVAL_PIPELINE") == "1"
|
|
2383
|
+
and os.environ.get("MDCG_GATE_S4_LAYER") == "1")
|
|
2384
|
+
_boost = layer_boosts() if _s4_on else None
|
|
1583
2385
|
scored = []
|
|
1584
2386
|
for e, fm, c in docs:
|
|
1585
2387
|
# 归一化 content 后取 bigram(与 query 侧 normalize_en 对称)
|
|
@@ -1596,17 +2398,110 @@ class MdCG:
|
|
|
1596
2398
|
if pools: # §七 降权:乘数只来自显式权重表(可复算)
|
|
1597
2399
|
raw = max(0.0, min(1.0, raw * pooling.weight_of(
|
|
1598
2400
|
fm.get("id") or e["path"], e, pools)))
|
|
2401
|
+
if _boost: # S4:层级加成(最后一步;上限仍夹在 1.0)
|
|
2402
|
+
raw = min(1.0, raw + _boost.get(str(fm.get("layer") or ""), 0.0))
|
|
1599
2403
|
scored.append(({"id": fm.get("id") or e["path"], "frontmatter": fm,
|
|
1600
2404
|
"content": c, "path": e["path"]}, raw))
|
|
1601
2405
|
return scored
|
|
1602
2406
|
|
|
1603
2407
|
|
|
2408
|
+
# 生效条件:scored 按 (-分数, -importance) 排序后取前 k 条逐条判定——judge 为真值时调 judge_qualification(r[0], stat["query"] 或 "", context),否则 qual={"state":None,"reason":"judge_disabled"};再把 neg_coverage 前 3 条以 id=其 path 追加到 out 末尾(该项 layer=="rejected"→STATE_REJECT,否则 STATE_DEFER,读文件抛 OSError 则跳过);record 为真且 results 非空时调 record_access;pool_plan 以 pooling.plan(stat["cap"] 或模块级 GLOBAL_CAP, pools) 生成,stat["pool_taken"] 为真时并入 taken/cands/lost;返回 (out, 含 tier/scanned/bucket/candidates/pre_cap/cap/cut_order/pools/big_domain 的审计 dict);
|
|
1604
2409
|
def _emit(self, scored, k, tier, stat, bucket, record, candidates,
|
|
1605
2410
|
judge, context, neg_coverage, big_domain=None, big_scores=None,
|
|
1606
2411
|
pools=None):
|
|
2412
|
+
# ---- S5 负记忆抑制(契约 §3 S5;flag 控、默认关)----
|
|
2413
|
+
# 现状:rejected/unresolved 命中只会被「附加」到结果里(sensing),对正候选毫无影响。
|
|
2414
|
+
# 这里把它变成**抑制信号**:与负记忆「边相邻」或「词面高度重叠」的正候选按
|
|
2415
|
+
# w ← w × (1-λ) 降权(只降权、不删除、下限 0);开关默认关 → 行为与改动前一致。
|
|
2416
|
+
_s5 = (os.environ.get("MDCG_RETRIEVAL_PIPELINE") == "1"
|
|
2417
|
+
and os.environ.get("MDCG_GATE_S5_NEG") == "1")
|
|
2418
|
+
_s5_lam, _s5_thr, _s5_hits = 0.5, 0.5, []
|
|
2419
|
+
# λ/阈值的解析必须在「scored 是否为空」之外:否则 scored 为空时负记忆条目会按默认 λ 降权、
|
|
2420
|
+
# 审计也会记错配置(独立复核 2026-09-19 指出)
|
|
2421
|
+
if _s5 and neg_coverage:
|
|
2422
|
+
try:
|
|
2423
|
+
_s5_lam = min(1.0, max(0.0, float(os.environ.get("MDCG_NEG_LAMBDA", "0.5"))))
|
|
2424
|
+
except ValueError:
|
|
2425
|
+
_s5_lam = 0.5
|
|
2426
|
+
try:
|
|
2427
|
+
_s5_thr = min(1.0, max(0.0, float(os.environ.get("MDCG_NEG_SIM", "0.5"))))
|
|
2428
|
+
except ValueError:
|
|
2429
|
+
_s5_thr = 0.5
|
|
2430
|
+
if _s5 and neg_coverage and scored:
|
|
2431
|
+
_negs = []
|
|
2432
|
+
for _nc in neg_coverage[:10]:
|
|
2433
|
+
try:
|
|
2434
|
+
with open(os.path.join(self.root, _nc["path"]), encoding="utf-8") as _f:
|
|
2435
|
+
_fm_n, _c_n = nodefile.loads(_f.read())
|
|
2436
|
+
except Exception:
|
|
2437
|
+
continue # 解析异常同样放行(信息不足不得中断检索)
|
|
2438
|
+
_negs.append((str(_fm_n.get("id") or os.path.splitext(
|
|
2439
|
+
os.path.basename(_nc["path"]))[0]), _fm_n, _c_n))
|
|
2440
|
+
for _i, (_doc, _sc) in enumerate(scored):
|
|
2441
|
+
_pid = str(_doc.get("id") or _doc.get("path") or "")
|
|
2442
|
+
_pt = {str(_e.get("target") or "")
|
|
2443
|
+
for _e in (_doc["frontmatter"].get("edges") or [])
|
|
2444
|
+
if isinstance(_e, dict)}
|
|
2445
|
+
_hit = False
|
|
2446
|
+
for _nid, _nfm, _nc_content in _negs:
|
|
2447
|
+
_nt = {str(_e.get("target") or "")
|
|
2448
|
+
for _e in (_nfm.get("edges") or []) if isinstance(_e, dict)}
|
|
2449
|
+
if _nid == _pid or _pid in _nt or _nid in _pt:
|
|
2450
|
+
_hit = True
|
|
2451
|
+
break
|
|
2452
|
+
try:
|
|
2453
|
+
_sim = lexical_sim(bigrams(normalize_en(_doc.get("content") or "")),
|
|
2454
|
+
bigrams(normalize_en(_nc_content)))
|
|
2455
|
+
except Exception:
|
|
2456
|
+
_sim = 0.0
|
|
2457
|
+
if _sim >= _s5_thr:
|
|
2458
|
+
_hit = True
|
|
2459
|
+
break
|
|
2460
|
+
if _hit:
|
|
2461
|
+
scored[_i] = (_doc, max(0.0, _sc * (1.0 - _s5_lam)))
|
|
2462
|
+
_s5_hits.append(_pid)
|
|
2463
|
+
# 审计只在「确有负记忆命中」时落键(与 S1/S2「产生信息才落键」同规则)
|
|
2464
|
+
if _s5 and neg_coverage:
|
|
2465
|
+
_g = stat.setdefault("gates", {})
|
|
2466
|
+
_g["s5"] = {"lambda": _s5_lam, "threshold": _s5_thr,
|
|
2467
|
+
"neg": len(neg_coverage), "suppressed": len(_s5_hits),
|
|
2468
|
+
"suppressed_ids": _s5_hits[:20]}
|
|
1607
2469
|
scored.sort(key=lambda x: (-x[1],
|
|
1608
|
-
-float(x[0]["frontmatter"].get("importance") or 0)
|
|
2470
|
+
-float(x[0]["frontmatter"].get("importance") or 0),
|
|
2471
|
+
str(x[0].get("id") or "")))
|
|
1609
2472
|
results = scored[:k]
|
|
2473
|
+
# ---- S6 一致性交叉验证(契约 §3 S6;flag 控、默认关)----
|
|
2474
|
+
# 只读复用 crosscheck 的「赛道 × 来源执照」判定:对 top-k 逐个给出赛道、声明依据是否被
|
|
2475
|
+
# 该赛道许可、以及断言条数。**不进主排序**(scored/out 的次序一律不动),只落审计摘要。
|
|
2476
|
+
_s6 = (os.environ.get("MDCG_RETRIEVAL_PIPELINE") == "1"
|
|
2477
|
+
and os.environ.get("MDCG_GATE_S6_CROSSCHECK") == "1")
|
|
2478
|
+
if _s6:
|
|
2479
|
+
try:
|
|
2480
|
+
from . import crosscheck as _cc
|
|
2481
|
+
except Exception:
|
|
2482
|
+
_cc = None
|
|
2483
|
+
if _cc is not None:
|
|
2484
|
+
_rows6, _track6, _flagged6 = [], {}, []
|
|
2485
|
+
for _r in results:
|
|
2486
|
+
_fm6 = _r[0].get("frontmatter") or {}
|
|
2487
|
+
_c6 = _r[0].get("content") or ""
|
|
2488
|
+
try:
|
|
2489
|
+
_tk6 = _cc.classify_track(_fm6, _c6)
|
|
2490
|
+
_bs6 = _fm6.get("verification_basis")
|
|
2491
|
+
_ok6 = bool(_cc.basis_licensed(_tk6, _bs6)) if _bs6 else False
|
|
2492
|
+
_cl6 = len(_cc.extract_claims(_fm6, _c6))
|
|
2493
|
+
except Exception:
|
|
2494
|
+
_tk6, _bs6, _ok6, _cl6 = "undetermined", None, False, 0
|
|
2495
|
+
_track6[_tk6] = _track6.get(_tk6, 0) + 1
|
|
2496
|
+
_rid6 = _r[0].get("id") or _r[0].get("path")
|
|
2497
|
+
_rows6.append({"id": _rid6, "track": _tk6, "basis": _bs6,
|
|
2498
|
+
"licensed": _ok6, "claims": _cl6})
|
|
2499
|
+
# 只在「赛道已定 ∧ 声明了依据 ∧ 依据不被该赛道许可」时点名(赛道未定不点名)
|
|
2500
|
+
if _tk6 in ("science", "humanities") and _bs6 and not _ok6:
|
|
2501
|
+
_flagged6.append({"id": _rid6, "track": _tk6, "basis": _bs6})
|
|
2502
|
+
stat.setdefault("gates", {})["s6"] = {
|
|
2503
|
+
"checked": len(_rows6), "by_track": _track6,
|
|
2504
|
+
"flagged": _flagged6, "rows": _rows6}
|
|
1610
2505
|
# 资格判定(与性能正交)
|
|
1611
2506
|
out = []
|
|
1612
2507
|
for r in results:
|
|
@@ -1628,7 +2523,8 @@ class MdCG:
|
|
|
1628
2523
|
"content": content, "path": nc["path"]}
|
|
1629
2524
|
qual = {"state": STATE_REJECT if nc["layer"] == "rejected" else STATE_DEFER,
|
|
1630
2525
|
"reason": f"查询已被{nc['layer']}层覆盖:见 {nc['path']}"}
|
|
1631
|
-
|
|
2526
|
+
# S5 开时:负记忆条目不再与正候选同权(契约 §3 S5)
|
|
2527
|
+
out.append((entry, (max(0.0, 1.0 - _s5_lam) if _s5 else 1.0), qual))
|
|
1632
2528
|
if record and results:
|
|
1633
2529
|
self.record_access([r[0]["id"] for r in results], tier)
|
|
1634
2530
|
pool_plan = pooling.plan(stat.get("cap") or GLOBAL_CAP, pools)
|
|
@@ -1638,8 +2534,8 @@ class MdCG:
|
|
|
1638
2534
|
pool_plan["taken"] = dict(stat["pool_taken"])
|
|
1639
2535
|
pool_plan["cands"] = dict(stat.get("pool_cands") or {})
|
|
1640
2536
|
pool_plan["lost"] = dict(stat.get("pool_lost") or {})
|
|
1641
|
-
|
|
1642
|
-
|
|
2537
|
+
meta = {"tier": tier, "scanned": stat["scanned"], "bucket": bucket,
|
|
2538
|
+
"candidates": candidates,
|
|
1643
2539
|
# §七 分池审计:截断前候选数 / 全局额度 / 生效分池计划
|
|
1644
2540
|
"pre_cap": stat.get("pre_cap"), "cap": stat.get("cap"),
|
|
1645
2541
|
# 截断依据审计:relevance=先打分再排序截断(现行)
|
|
@@ -1648,10 +2544,33 @@ class MdCG:
|
|
|
1648
2544
|
"covered_neg": [nc["path"] for nc in neg_coverage],
|
|
1649
2545
|
# 阶段 1 大域收敛结果 + 完整打分明细(白箱可审计)
|
|
1650
2546
|
"big_domain": big_domain,
|
|
1651
|
-
"big_domain_scores": big_scores
|
|
2547
|
+
"big_domain_scores": big_scores,
|
|
2548
|
+
# reach 收敛层审计(2026-09-18):reach=converged/off/short_term/...
|
|
2549
|
+
# 与 reach_seed(倒排命中数)/reach_diffused(图扩散增量)——
|
|
2550
|
+
# A3「触碰量随查询变化」的观测面(scanned 只报读盘数)
|
|
2551
|
+
"reach": stat.get("reach"),
|
|
2552
|
+
"reach_seed": stat.get("reach_seed"),
|
|
2553
|
+
"reach_diffused": stat.get("reach_diffused"),
|
|
2554
|
+
"reach_hops": stat.get("reach_hops"),
|
|
2555
|
+
# 首建全库读盘量(仅本次调用发生重建时出现)与新节点并入数
|
|
2556
|
+
"reach_build_docs": stat.get("reach_build_docs"),
|
|
2557
|
+
"reach_fresh_nodes": stat.get("reach_fresh_nodes"),
|
|
2558
|
+
# 回退审计:reach_reverted=True 表示本次结果来自全量路径;
|
|
2559
|
+
# reach_reverted_docs 为收敛阶段已发生的读盘量(A3 可据此扣减)
|
|
2560
|
+
"reach_reverted": stat.get("reach_reverted"),
|
|
2561
|
+
"reach_reverted_docs": stat.get("reach_reverted_docs")}
|
|
2562
|
+
# 分阶段审计仅在门控真的产生信息时落键(默认关闭 → meta 与改动前逐字节一致;契约 §5.3)
|
|
2563
|
+
if stat.get("gates"):
|
|
2564
|
+
meta["gates"] = stat["gates"]
|
|
2565
|
+
# 时间算子审计(阶段二 4.1):同规则——只在时间算子真的启用时落键,
|
|
2566
|
+
# 未启用则 meta 键集合与改动前逐字节一致(默认关零变更纪律)。
|
|
2567
|
+
if stat.get("time_filter"):
|
|
2568
|
+
meta["time_filter"] = stat["time_filter"]
|
|
2569
|
+
return out, meta
|
|
1652
2570
|
|
|
1653
2571
|
# ---------- 五大单元之四:反思 / 验证 / 输出 ----------
|
|
1654
2572
|
|
|
2573
|
+
# 生效条件:无条件计算 d_prev/_compute_d(query, results)、states(results 为假值时 states 为空列表)与 D_meta 边界压力向量(d_meta.compute(self, query, results),MDCG_D_META 关闭时三代理恒 0.0),并在追加前取 reflection_log 末条的 d_meta 作前值(无前值时为 None)算出 d_meta_delta,随后尝试追加 reflection_log(OSError 静默吞掉),返回含 user_feedback、d_meta、d_meta_delta 的反思 dict;
|
|
1655
2574
|
def reflect(self, query: str, results, user_feedback: str = None):
|
|
1656
2575
|
"""反思单元(白箱第 3 篇第 13 章)。
|
|
1657
2576
|
|
|
@@ -1659,14 +2578,26 @@ class MdCG:
|
|
|
1659
2578
|
输出:反思记录(追加到 _reflection.jsonl)
|
|
1660
2579
|
|
|
1661
2580
|
反思内容:
|
|
1662
|
-
- 信息差 D(t,C)
|
|
2581
|
+
- 信息差 D(t,C) 增量(**D_task**:`_compute_d` 口径零变更)
|
|
1663
2582
|
- 命中节点的资格态分布
|
|
1664
2583
|
- 是否触发 BLINDSPOT(覆盖率不足)
|
|
1665
2584
|
- 用户反馈时记录「预测与事实的偏差」→ 知识飞轮的入口
|
|
2585
|
+
|
|
2586
|
+
双 D 分离(智能论3.4 §2.7.0 DEV-002/002a):**并列**落 `d_meta`
|
|
2587
|
+
(边界压力向量,三代理各自 [0,1]、**不合成单值**)与 `d_meta_delta`
|
|
2588
|
+
(与上一条反思的逐字段差,首条为 None)。D_meta **不参与**
|
|
2589
|
+
`_compute_d`——D_task 与 D_meta 对象不同、数值不互换;此处只做结构性
|
|
2590
|
+
投影落痕,供 predict / autonomy / self_state 三个上层消费。
|
|
2591
|
+
`MDCG_D_META` 关闭时 `d_meta` 三代理恒 0.0 且 `d_meta_delta=None`
|
|
2592
|
+
(显式回退留痕,不是缺键)。
|
|
1666
2593
|
"""
|
|
2594
|
+
from . import d_meta as _d_meta # 惰性导入:叶子只读模块,防循环
|
|
1667
2595
|
d_prev = self._last_d()
|
|
1668
2596
|
d_curr = self._compute_d(query, results)
|
|
1669
2597
|
states = [r[2]["state"] for r in results] if results else []
|
|
2598
|
+
meta = _d_meta.compute(self, query=query, results=results)
|
|
2599
|
+
recs = self.last_d_records()
|
|
2600
|
+
meta_prev = recs[-1].get("d_meta") if recs else None
|
|
1670
2601
|
reflection = {
|
|
1671
2602
|
"t": time.time(),
|
|
1672
2603
|
"query": query,
|
|
@@ -1677,6 +2608,10 @@ class MdCG:
|
|
|
1677
2608
|
"states": dict((s, states.count(s)) for s in set(states)),
|
|
1678
2609
|
"n_results": len(results),
|
|
1679
2610
|
"feedback": user_feedback,
|
|
2611
|
+
"d_meta": {k: float(meta.get(k) or 0.0)
|
|
2612
|
+
for k in _d_meta.PROXY_KEYS},
|
|
2613
|
+
"d_meta_delta": (_d_meta.diff(meta_prev, meta)
|
|
2614
|
+
if meta.get("enabled") else None),
|
|
1680
2615
|
}
|
|
1681
2616
|
try:
|
|
1682
2617
|
append_jsonl(self.reflection_log, reflection)
|
|
@@ -1684,6 +2619,7 @@ class MdCG:
|
|
|
1684
2619
|
pass
|
|
1685
2620
|
return reflection
|
|
1686
2621
|
|
|
2622
|
+
# 生效条件:list(read_jsonl(self.reflection_log)) 至少 2 条记录时返回倒数第二条的 "d_curr"(该键缺失时回落 1.0,键在而其值为 None 时返回 None),不足 2 条时返回 1.0。
|
|
1687
2623
|
def _d_prev2(self):
|
|
1688
2624
|
"""上一次的前一次 D 值,用于二阶差分。"""
|
|
1689
2625
|
recs = list(read_jsonl(self.reflection_log))
|
|
@@ -1691,16 +2627,19 @@ class MdCG:
|
|
|
1691
2627
|
return recs[-2].get("d_curr", 1.0)
|
|
1692
2628
|
return 1.0
|
|
1693
2629
|
|
|
2630
|
+
# 生效条件:list(read_jsonl(self.reflection_log)) 非空时返回最后一条的 "d_curr"(该键缺失时回落 1.0,键在而其值为 None 时返回 None),为空时返回 1.0。
|
|
1694
2631
|
def _last_d(self):
|
|
1695
2632
|
recs = list(read_jsonl(self.reflection_log))
|
|
1696
2633
|
if recs:
|
|
1697
2634
|
return recs[-1].get("d_curr", 1.0)
|
|
1698
2635
|
return 1.0
|
|
1699
2636
|
|
|
2637
|
+
# 生效条件:无条件返回 list(read_jsonl(self.reflection_log))(日志为空时返回空列表)。
|
|
1700
2638
|
def last_d_records(self):
|
|
1701
2639
|
"""反思日志全量记录(测试/审计用)。"""
|
|
1702
2640
|
return list(read_jsonl(self.reflection_log))
|
|
1703
2641
|
|
|
2642
|
+
# 生效条件:results 为假值(空列表/None)时返回 1.0;否则返回 max(0.0, 1.0 - (r[1]>0 且 r[2]["state"]==STATE_ACCEPT 的条数 / len(results))),query 不参与计算;
|
|
1704
2643
|
def _compute_d(self, query, results):
|
|
1705
2644
|
"""信息差 D 的简化度量(白箱第 4 篇):
|
|
1706
2645
|
D = 1 - 有效命中比例,0=信息差为零(完美),1=完全空白。
|
|
@@ -1717,6 +2656,7 @@ class MdCG:
|
|
|
1717
2656
|
if r[1] > 0 and r[2]["state"] == STATE_ACCEPT)
|
|
1718
2657
|
return max(0.0, 1.0 - accept / len(results))
|
|
1719
2658
|
|
|
2659
|
+
# 生效条件:to_layer 不属于模块级 LAYERS 时抛 ValueError;self.get(node_id) 取不到节点或当前层(fm.layer 或路径首段)等于 to_layer 时返回 None;否则调 protect.guard_move 后写入 demotion 审计、搬文件到目标层并按新层 _stage、重算 buckets,返回 {id, from, to, path, reason};
|
|
1720
2660
|
def _move_layer(self, node_id: str, to_layer: str, reason: str = ""):
|
|
1721
2661
|
"""把节点搬到另一层(可信度降级用),同步索引与 demotion 审计字段。"""
|
|
1722
2662
|
if to_layer not in LAYERS:
|
|
@@ -1742,7 +2682,7 @@ class MdCG:
|
|
|
1742
2682
|
and os.path.exists(old_full)):
|
|
1743
2683
|
os.remove(old_full)
|
|
1744
2684
|
rel = os.path.relpath(new_path, self.root).replace("\\", "/")
|
|
1745
|
-
self._stage(node_id, {
|
|
2685
|
+
self._stage(node_id, _strip_empty_gate_fields({
|
|
1746
2686
|
"path": rel, "layer": to_layer, "tags": fm.get("tags", []),
|
|
1747
2687
|
"bucket": None, "importance": fm.get("importance", 0.5),
|
|
1748
2688
|
"created_at": fm.get("created_at", 0),
|
|
@@ -1752,12 +2692,15 @@ class MdCG:
|
|
|
1752
2692
|
node["content"].encode("utf-8")).hexdigest()[:12],
|
|
1753
2693
|
"temporal": fm.get("temporal"), "spatial": fm.get("spatial"),
|
|
1754
2694
|
"time_window": (fm.get("condition_space") or {}).get("time_window"),
|
|
2695
|
+
"big_domain": fm.get("big_domain"),
|
|
2696
|
+
"observation_position": (fm.get("condition_space") or {}).get("observation_position"),
|
|
1755
2697
|
"evidence_count": fm.get("evidence_count", 0),
|
|
1756
|
-
})
|
|
2698
|
+
}))
|
|
1757
2699
|
self.index["buckets"] = self._count_buckets(self.index["nodes"])
|
|
1758
2700
|
return {"id": node_id, "from": from_layer, "to": to_layer,
|
|
1759
2701
|
"path": rel, "reason": reason}
|
|
1760
2702
|
|
|
2703
|
+
# 生效条件:verdict 不在 {confirmed,weakened,falsified} 时抛 ValueError;node_id 取不到节点返回 None;verdict=="falsified" 时以 content[:200] 为假设写入 rejected 层、删除原文件并 _unstage,返回 {"action":"falsified","new_id":None,"evidence_count":None,"demoted":None};confirmed/weakened 时 confidence 分别 +0.05 / -0.15(夹到 [0,0.99] 并 round 2)、evidence_count 与 positive_evidence/negative_evidence 各 +1、追加 evidence_log,且仅当 weakened 后 confidence < DEMOTE_CONFIDENCE 且 layer=="knowledge" 时调 _move_layer 到 contextual 并 set_state("demoted"),否则 _write_node 回写并同步索引 evidence_count;
|
|
1761
2704
|
def verify(self, node_id: str, evidence: str, verdict: str):
|
|
1762
2705
|
"""验证单元(白箱第 3 篇第 13 章):对节点做一次外部验证裁决。
|
|
1763
2706
|
|
|
@@ -1783,8 +2726,15 @@ class MdCG:
|
|
|
1783
2726
|
# 从原位置删除(节点进入 rejected 层)
|
|
1784
2727
|
os.remove(os.path.join(self.root, node["path"]))
|
|
1785
2728
|
self._unstage(node_id) # 同上:索引删除必须可重放(防幽灵条目)
|
|
2729
|
+
trust.invalidate_cache(self) # 索引已变 → 反查索引作废
|
|
2730
|
+
# 地基塌了:直接下游立即标存疑(**同步一跳**,永不抛、不阻断本次裁决)
|
|
2731
|
+
propagation = trust.mark_dependents(
|
|
2732
|
+
self, node_id,
|
|
2733
|
+
reason=f"上游 {node_id} 被证伪(falsified)——依赖的地基已不存在",
|
|
2734
|
+
actor="verify:falsified", trigger="verify_falsified")
|
|
1786
2735
|
return {"action": "falsified", "new_id": None,
|
|
1787
|
-
"evidence_count": None, "demoted": None
|
|
2736
|
+
"evidence_count": None, "demoted": None,
|
|
2737
|
+
"propagation": propagation}
|
|
1788
2738
|
# confirmed/weakened:调整 confidence(白箱第 5 篇:
|
|
1789
2739
|
# 反例的权重应该比正例大——这里用非对称步长实现)
|
|
1790
2740
|
fm = node["frontmatter"]
|
|
@@ -1816,11 +2766,33 @@ class MdCG:
|
|
|
1816
2766
|
e = self.index["nodes"].get(node_id)
|
|
1817
2767
|
if e is not None:
|
|
1818
2768
|
e["evidence_count"] = fm["evidence_count"]
|
|
2769
|
+
# 验证态流转(可验证记忆单元):外部证据裁决**就是**验证态迁移。
|
|
2770
|
+
# confirmed → verified(首次转正;已在 verified 则幂等 no-op)
|
|
2771
|
+
# weakened → doubted(证据被削弱 ⇒ 存疑,待复核)
|
|
2772
|
+
# 受保护节点不接受降级——负路由 ok=False 只说明「不可降」,不回滚已完成的
|
|
2773
|
+
# 证据计数(与上方 lifecycle 降级同风格:不假装成功,也不推翻既成事实)。
|
|
2774
|
+
if verdict == "confirmed":
|
|
2775
|
+
ver = self.set_verification(
|
|
2776
|
+
node_id, "verified",
|
|
2777
|
+
reason=f"外部裁决 confirmed:{str(evidence)[:80]}",
|
|
2778
|
+
actor="verify:confirmed", evidence=evidence,
|
|
2779
|
+
method=fm.get("verification_basis"), trigger="verify")
|
|
2780
|
+
else:
|
|
2781
|
+
ver = self.set_verification(
|
|
2782
|
+
node_id, "doubted",
|
|
2783
|
+
reason=f"外部裁决 weakened:{str(evidence)[:80]}",
|
|
2784
|
+
actor="verify:weakened", evidence=evidence, trigger="verify")
|
|
2785
|
+
# 结论已变 ⇒ 下游的「地基动了」:同步一跳传播(永不抛,降级不阻断)
|
|
2786
|
+
propagation = trust.mark_dependents(
|
|
2787
|
+
self, node_id, reason=f"上游验证结论变为 {verdict}",
|
|
2788
|
+
actor=f"verify:{verdict}", trigger=f"verify_{verdict}")
|
|
1819
2789
|
return {"action": verdict, "confidence": fm["confidence"],
|
|
1820
|
-
"evidence_count": fm["evidence_count"], "demoted": demoted
|
|
2790
|
+
"evidence_count": fm["evidence_count"], "demoted": demoted,
|
|
2791
|
+
"verification": ver, "propagation": propagation}
|
|
1821
2792
|
|
|
1822
2793
|
# ---------- 知识飞轮(白箱第 2 篇第 8 章)----------
|
|
1823
2794
|
|
|
2795
|
+
# 生效条件:error_report 是 str 时经 json.loads 解析,否则按映射使用;question 由 query/actual_state/expected_state 缺键回落 "?" 拼成;detail 为 list 时非 dict 元素跳过,其中 type=="same_condition_divergence"、"condition_clash" 及其他真值 type 各渲染一条现场短语并以 " | " 连接作为 context(known_clues 取 missing 或 "")传给 add_unresolved,返回 {"unresolved_id": nid, "question": question};
|
|
1824
2796
|
def flywheel_step(self, error_report: str):
|
|
1825
2797
|
"""知识飞轮入口:错误 → 寻找遗漏条件 → 验证 → 结构更新。
|
|
1826
2798
|
|
|
@@ -1862,6 +2834,7 @@ class MdCG:
|
|
|
1862
2834
|
|
|
1863
2835
|
# ---------- 访问计数:append-only,检索路径不写节点文件 ----------
|
|
1864
2836
|
|
|
2837
|
+
# 生效条件:无条件尝试把 {"t": time.time(), "ids": list(node_ids), "tier": tier} 追加到 access_log,append 抛 OSError 时静默吞掉;
|
|
1865
2838
|
def record_access(self, node_ids, tier=None):
|
|
1866
2839
|
try:
|
|
1867
2840
|
append_jsonl(self.access_log,
|
|
@@ -1869,6 +2842,7 @@ class MdCG:
|
|
|
1869
2842
|
except OSError:
|
|
1870
2843
|
pass
|
|
1871
2844
|
|
|
2845
|
+
# 生效条件:无 required 形参;遍历 self.access_log 的 read_jsonl 记录,ts 取 rec.get("t", 0)(缺键回落 0),对 rec.get("ids", []) 中每个 nid 计数加一,且 ts 大于该 nid 已记最大 ts 时更新 last[nid]=ts,返回 (counts, last);
|
|
1872
2846
|
def access_counts(self):
|
|
1873
2847
|
counts, last = {}, {}
|
|
1874
2848
|
for rec in read_jsonl(self.access_log):
|
|
@@ -1879,6 +2853,7 @@ class MdCG:
|
|
|
1879
2853
|
last[nid] = ts
|
|
1880
2854
|
return counts, last
|
|
1881
2855
|
|
|
2856
|
+
# 生效条件:access_counts() 的 counts 为空返回 0;否则对每个 nid 在索引中存在且 _read 返回 fm 非 None 的节点累加 access_count、last_access 取 max 并落盘、n +1;最后在 FileLock 下清空 access_log 并返回 n(索引缺该 id 或读盘失败的不计)。
|
|
1882
2857
|
def compact_access(self):
|
|
1883
2858
|
counts, last = self.access_counts()
|
|
1884
2859
|
if not counts:
|
|
@@ -1901,6 +2876,7 @@ class MdCG:
|
|
|
1901
2876
|
|
|
1902
2877
|
# ---------- 健康度 ----------
|
|
1903
2878
|
|
|
2879
|
+
# 生效条件:无条件以 index.get("buckets", {}) 生成分桶健康度并记 total_nodes,再遍历 index["nodes"] 逐条读文件(抛 OSError 跳过),按 layer 统计 total/ccg_complete/verification_basis_set/neg_conditions_set,另统计 rejected 与 unresolved 层数量,返回 h;
|
|
1904
2880
|
def health(self):
|
|
1905
2881
|
"""扩充:分桶健康度 + 5 要素完整度 + 验证基底覆盖率 + 负记忆密度。"""
|
|
1906
2882
|
h = routing.bucket_health(self.index.get("buckets", {}))
|