@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/mreview/bundle.py
CHANGED
|
@@ -32,6 +32,7 @@ GROUP_TOPIC = "topic"
|
|
|
32
32
|
GROUP_BATCH = "batch"
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
# 生效条件:content 为假值(如空串)时返回 (None, None);content 为真且 WL.template_signature(content) 返回真值时返回 (s, 'strong');否则遍历 content.splitlines() 中 strip 后非空的行,遇首个非空行时其 WL._skeleton 长度 ≥ WL.MIN_SKELETON 则返回 ('h:' + sk, 'head'),否则立即返回 (None, None)(不再看后续行),无任何非空行时也返回 (None, None);
|
|
35
36
|
def _sig_of(content: str):
|
|
36
37
|
"""模板签名(强/弱)。返回 (sig, kind) 或 (None, None)。"""
|
|
37
38
|
if not content:
|
|
@@ -50,6 +51,7 @@ def _sig_of(content: str):
|
|
|
50
51
|
return None, None
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
# 生效条件:meta(dict)中按 ("branched_from", "branch_id") 顺序取到的首个 v 为真且 str(v) != 'None' 时返回 str(v);否则 meta.get('derived_from') 为 list/tuple 且非空时返回 str(d[0]);否则其为非空 str 且不等于 '[]' 与 'None' 时返回该 str 本身;以上皆不成立时返回 None;
|
|
53
55
|
def _lineage_key(meta: dict):
|
|
54
56
|
for f in ("branched_from", "branch_id"):
|
|
55
57
|
v = meta.get(f)
|
|
@@ -63,6 +65,7 @@ def _lineage_key(meta: dict):
|
|
|
63
65
|
return None
|
|
64
66
|
|
|
65
67
|
|
|
68
|
+
# 生效条件:meta 为真时先取 sorted(CF._tag_prefixes(meta)) 作为 pref,pref 为空且 tags 有真值元素时改用各 tag 的 str(t).split(':', 1)[0](跳过假值 tag)集合排序;pref 非空返回 '+'.join(pref);pref 为空时取 str((meta or {}).get('layer') or ''),该层名非空返回 'layer:' + layer,层名为假值(None/空串/0)则返回 None;
|
|
66
69
|
def _topic_key(meta: dict, tags: list):
|
|
67
70
|
pref = sorted(CF._tag_prefixes(meta)) if meta else []
|
|
68
71
|
if not pref and tags:
|
|
@@ -73,6 +76,7 @@ def _topic_key(meta: dict, tags: list):
|
|
|
73
76
|
return ("layer:" + layer) if layer else None
|
|
74
77
|
|
|
75
78
|
|
|
79
|
+
# 生效条件:content 经 _sig_of 得到的 sig 为真时返回 (GROUP_TEMPLATE, sig, kind);否则 meta 经 _lineage_key 得到的值非空时返回 (GROUP_LINEAGE, lin, None);否则 (meta, tags) 经 _topic_key 得到的值非空时返回 (GROUP_TOPIC, top, None);三者皆不成立时返回 (GROUP_BATCH, 'misc', None);
|
|
76
80
|
def _group_of(meta: dict, tags: list, content: str):
|
|
77
81
|
sig, kind = _sig_of(content)
|
|
78
82
|
if sig:
|
|
@@ -86,6 +90,7 @@ def _group_of(meta: dict, tags: list, content: str):
|
|
|
86
90
|
return GROUP_BATCH, "misc", None
|
|
87
91
|
|
|
88
92
|
|
|
93
|
+
# 生效条件:root 与 meta 均为真、且 meta.get('path') 经 or '' 再 str 后为非空路径时,把 rel 中的 '\\' 与 '/' 替换为 os.sep 并与 root 拼接,以 encoding='utf-8'、errors='replace' 打开并返回 f.read(int(limit))(limit 为 0 时读 0 字节);root 或 meta 为假值、rel 为空、或该打开/读取抛 OSError 时返回 None;
|
|
89
94
|
def _load_content(root: str, meta: dict, limit: int):
|
|
90
95
|
if not root or not meta:
|
|
91
96
|
return None
|
|
@@ -100,6 +105,7 @@ def _load_content(root: str, meta: dict, limit: int):
|
|
|
100
105
|
return None
|
|
101
106
|
|
|
102
107
|
|
|
108
|
+
# 生效条件:恒返回构造 dict——ref/node_id/proposal_id/origin/layer/role/importance/evidence_count/verification_basis/lifecycle_state 均以 .get 取值(缺键得 None),content_hash 先取 cand.get('content_hash') 为空再取 (meta or {}).get('content_hash'),tags/issue_kinds/evidence 经 or [] 后 list 化,excerpt 取 (content or '')[:int(limit)] 为空时(含 content 为空串或 limit 为 0)为 None;
|
|
103
109
|
def _entry(cand: dict, meta: dict, content: str, limit: int) -> dict:
|
|
104
110
|
return {"ref": cand.get("key"), "node_id": cand.get("node_id"),
|
|
105
111
|
"proposal_id": cand.get("proposal_id"), "origin": cand.get("origin"),
|
|
@@ -115,11 +121,13 @@ def _entry(cand: dict, meta: dict, content: str, limit: int) -> dict:
|
|
|
115
121
|
"excerpt": (content or "")[:int(limit)] or None}
|
|
116
122
|
|
|
117
123
|
|
|
124
|
+
# 生效条件:ids 与 size 为必需形参,size 先经 max(1, int(size))(0、负值或 1 均按 1)作为步长,返回 [ids[i:i + size] for i in range(0, len(ids), size)];ids 为空(如 [] 或空串)时返回 [];
|
|
118
125
|
def _chunks(ids: list, size: int) -> list:
|
|
119
126
|
size = max(1, int(size))
|
|
120
127
|
return [ids[i:i + size] for i in range(0, len(ids), size)]
|
|
121
128
|
|
|
122
129
|
|
|
130
|
+
# 生效条件:candidates(list)为必需输入,nodes 为假值时按 {} 取节点表;逐候选以 c.get('node_id') 查 meta(无 node_id 则 meta 为 {})、以 c.get('content') or '' 取内容,内容为空且 read_content 为真且 nid 为真时改用 _load_content(root, meta, content_limit) 的结果(再 or ''),并按 int(content_limit) 截断;随后按 _group_of(meta, c.get('tags') or [], content) 分组,非 GROUP_BATCH 且组内 id 数 < int(min_group)(min_group 为 0 时该比较恒不成立,即无降级)的组与既有 GROUP_BATCH 组一并降级,降级集合非空时并入 (GROUP_BATCH, 'misc')、为空则不建该组;再对每组按 int(max_per_bundle) 经 _chunks 切块生成带 entries 的 bundles 与 stats,返回 content_missing 为 contents 中该候选键对应值为假值的候选数;
|
|
123
131
|
def bundle(candidates: list, nodes=None, root=None, *, max_per_bundle=50,
|
|
124
132
|
min_group=2, read_content=True, content_limit=3000) -> dict:
|
|
125
133
|
"""候选清单 → 待评包(含条目上下文与机械字段,供级 3 装配与级 4 spec)。"""
|
|
@@ -33,6 +33,7 @@ PATROL_EPOCH = 3600.0 # 轮巡窗口推进周期(秒):默认每
|
|
|
33
33
|
COLD_IMPORTANCE_MIN = 0.6 # 冷节点抽样门槛(importance 解析失败按 0 计)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
# 生效条件:path 不满足 os.path.exists(path) 时返回 [];否则逐行 strip 后跳过空行与 json.JSONDecodeError 行,返回成功 json.loads 的每行结果列表。
|
|
36
37
|
def _read_jsonl(path: str) -> list:
|
|
37
38
|
if not os.path.exists(path):
|
|
38
39
|
return []
|
|
@@ -49,6 +50,7 @@ def _read_jsonl(path: str) -> list:
|
|
|
49
50
|
return out
|
|
50
51
|
|
|
51
52
|
|
|
53
|
+
# 生效条件:os.path.join(root, CF.ACCESS_LOG) 不存在时返回空集;否则逐行解析,rec.get("ids") 为 list/tuple 时仅加入其中真值元素的 str 形式,nid 取 rec.get("id") or rec.get("node_id") or rec.get("nid") 的真值并加入 str(nid),最终返回 seen & set(nodes)(仅保留 nodes 键内的 id)。
|
|
52
54
|
def _touched(root: str, nodes: dict) -> set:
|
|
53
55
|
"""access log 触达集(与 conformance._reach_metrics 同一解析口径:ids / id / node_id / nid)。"""
|
|
54
56
|
p = os.path.join(root, CF.ACCESS_LOG)
|
|
@@ -73,6 +75,7 @@ def _touched(root: str, nodes: dict) -> set:
|
|
|
73
75
|
return seen & set(nodes)
|
|
74
76
|
|
|
75
77
|
|
|
78
|
+
# 生效条件:str(layer)=="knowledge" 的节点在 role 为假值、CF._as_int(evidence_count)==0、或 {"doc","code"} 命中 CF._tag_prefixes(r) 时分别产出 missing_field/mixed_layer;任意节点在 CF._as_int(evidence_count)==0 且无 CF._basis_of(r) 且 lifecycle_state 为假值时产出 unanalyzed;str(content_hash) 既非 "None" 也非 "" 且同指纹计数 >1 时该组内每个节点产出 dup_content;edges 为 list/tuple 时逐条处理其中的 dict 边,当该边所有真值键(CF.CANONICAL_EDGE_KEYS 与 "type")中不含任一规范键时产出 edge_non_canonical 并停止该节点后续边。
|
|
76
79
|
def _assertion_items(nodes: dict) -> list:
|
|
77
80
|
"""断言集超限项 → 条目级(判据函数与 conformance 同源,不另立口径)。"""
|
|
78
81
|
out = []
|
|
@@ -116,6 +119,7 @@ def _assertion_items(nodes: dict) -> list:
|
|
|
116
119
|
return out
|
|
117
120
|
|
|
118
121
|
|
|
122
|
+
# 生效条件:当 root、nodes 给定,对 _read_jsonl(root/CF.INBOX_LOG) 解析出的 inbox 记录中 pid(str(r.get("pid") or ""))非空且不在 decisions 中 x.get("pid") 真值 str 集合的记录,生成 origin=proposal、issue_kind=gate_defer 条目,其中 layer 直接取 r.get("layer") 不回落、content 取 r.get("content") or ""、tags 取 r.get("tags") or []、content_hash 取 r.get("payload_hash") or ""、node_id 取 str(r.get("id") or "") or None;再对 _read_jsonl(root/FORGET_LOG) 中 str(r.get("verdict"))=="DEFER" 的记录,若 str(r.get("node_id") or "") 非空且该 nid 是 nodes 的键则加入 out,否则加入 skipped(source=defer_queue、reason=node_not_landed、detail 为 nid 或 str(r.get("reason")));返回 (out, skipped);
|
|
119
123
|
def _defer_items(root: str, nodes: dict):
|
|
120
124
|
"""闸门待裁决提案 + 遗忘日志 DEFER 留痕。返回 (items, skipped)。"""
|
|
121
125
|
ib = _read_jsonl(os.path.join(root, CF.INBOX_LOG))
|
|
@@ -144,9 +148,11 @@ def _defer_items(root: str, nodes: dict):
|
|
|
144
148
|
return out, skipped
|
|
145
149
|
|
|
146
150
|
|
|
151
|
+
# 生效条件:取 nodes 中不在 _touched(root, nodes) 触达集、且 importance 经 _imp 归一后 >= COLD_IMPORTANCE_MIN 的节点,按归一 importance 降序(同值按 node_id 升序)后取 [:int(limit)];limit=0 得空列表,limit 为负则排除末尾 |limit| 条而非空返回(源码无 int(limit)<=0 早退)。
|
|
147
152
|
def _cold_items(root: str, nodes: dict, limit: int) -> list:
|
|
148
153
|
seen = _touched(root, nodes)
|
|
149
154
|
|
|
155
|
+
# 生效条件:r 的 importance 为真值且 float() 可转换时返回其 float 值;importance 为假值(None/0/"")时回落 0.0;转换抛 TypeError 或 ValueError 时返回 0.0。
|
|
150
156
|
def _imp(r):
|
|
151
157
|
try:
|
|
152
158
|
return float(r.get("importance") or 0.0)
|
|
@@ -161,6 +167,7 @@ def _cold_items(root: str, nodes: dict, limit: int) -> list:
|
|
|
161
167
|
for i, (nid, r) in enumerate(picked)]
|
|
162
168
|
|
|
163
169
|
|
|
170
|
+
# 生效条件:int(window) <= 0 或 nodes 为假值时返回 [];否则按 sha1(nid.encode("utf-8")).hexdigest() 排序,从 (int(offset)*int(window)) % len(nodes) 起取 int(window) 条,不足时用头部元素环绕补足。
|
|
164
171
|
def _patrol_items(nodes: dict, window: int, offset: int) -> list:
|
|
165
172
|
if int(window) <= 0 or not nodes:
|
|
166
173
|
return []
|
|
@@ -175,6 +182,7 @@ def _patrol_items(nodes: dict, window: int, offset: int) -> list:
|
|
|
175
182
|
for nid in picked]
|
|
176
183
|
|
|
177
184
|
|
|
185
|
+
# 生效条件:items 逐条以 node_id 为真时 key=str(node_id)、否则 key="pid:"+str(proposal_id) 聚合;首次建房时 node_id 为真则取 nodes.get(node_id) or {} 作 meta,tags 取 it.get("tags") or meta.get("tags") or [],evidence 每聚合记录最多 8 条;结果 sources 去重后为空则置 ["-"],priority 取 _PRIORITY.get(s, 9) 的最小值,按 (priority, str(key)) 排序返回。
|
|
178
186
|
def _merge(items: list, nodes: dict) -> list:
|
|
179
187
|
"""按条目(节点/提案)聚合:一条候选 = 一个待评条目,多个问题挂在 issue_kinds 上。"""
|
|
180
188
|
acc = {}
|
|
@@ -211,6 +219,7 @@ def _merge(items: list, nodes: dict) -> list:
|
|
|
211
219
|
return out
|
|
212
220
|
|
|
213
221
|
|
|
222
|
+
# 生效条件:root 为假值(None/"")时回落 mdcg_root();nodes 为 None 时用 CF.load_index(root) or {};wanted 为 (sources or SOURCES) 与 SOURCES 的交集,按其中是否含 assertion/defer_queue/cold/patrol 分派取候选(cold 传 cold_limit,patrol 传 patrol_window 与偏移,patrol_offset 为 None 时偏移取 int((now if now is not None else time.time())//PATROL_EPOCH));limit 为真值时才把 merged 截到 int(limit);with_report 为真值时补 CF.check(root, check_paths=False) 的指标与失败断言 id。
|
|
214
223
|
def generate(root=None, *, sources=SOURCES, nodes=None, limit=None, cold_limit=500,
|
|
215
224
|
patrol_window=200, patrol_offset=None, with_report=False, now=None) -> dict:
|
|
216
225
|
"""四源候选生成。nodes 可注入(测试用合成库,免读真源)。"""
|
package/md_cg/mreview/govern.py
CHANGED
|
@@ -112,24 +112,29 @@ ACTIONS = ("role", "role_rollback", "role_history", "role_stats")
|
|
|
112
112
|
|
|
113
113
|
# ---- 通用工具 ------------------------------------------------------------
|
|
114
114
|
|
|
115
|
+
# 生效条件:传入 cg 即返回 os.path.join(cg.root, GOVERN_LOG),无分支。
|
|
115
116
|
def _log_path(cg) -> str:
|
|
116
117
|
return os.path.join(cg.root, GOVERN_LOG)
|
|
117
118
|
|
|
118
119
|
|
|
120
|
+
# 生效条件:fm 为假值(None/{})时按 {} 处理,其 .get("tags") 为假值(None/空串/空列表)时回落 [],否则对该值逐项过滤,只保留 isinstance(t, str) 的元素。
|
|
119
121
|
def _tags(fm: dict) -> list:
|
|
120
122
|
return [t for t in ((fm or {}).get("tags") or []) if isinstance(t, str)]
|
|
121
123
|
|
|
122
124
|
|
|
125
|
+
# 生效条件:detail 为假值(None/空串)时返回 kind,否则返回 "%s:%s" % (kind, detail)。
|
|
123
126
|
def _reason_key(kind: str, detail: str = None) -> str:
|
|
124
127
|
return kind if not detail else "%s:%s" % (kind, detail)
|
|
125
128
|
|
|
126
129
|
|
|
130
|
+
# 生效条件:box 缺 key 时以 box.get(key, 0) 取 0 再加 1 写回 box[key];key 已存在(含值为非数值)时直接对现值 +1。
|
|
127
131
|
def _bump(box: dict, key: str) -> None:
|
|
128
132
|
box[key] = box.get(key, 0) + 1
|
|
129
133
|
|
|
130
134
|
|
|
131
135
|
# ---- 靶子定位(rule 驱动)------------------------------------------------
|
|
132
136
|
|
|
137
|
+
# 生效条件:rules/rules_dir 经 _as_rules 得到的列表中存在 id == rule_id 的项、且该项 matcher.layer 非空、mechanical 中存在 check == CHECK_FIELD_ABSENT 且 field 为真的项时,返回 {'rule_id','title','field','layers','severity','remedy','llm'};该项不存在或 layers/field 为空则抛 ValueError。
|
|
133
138
|
def role_rule(*, rules=None, rules_dir=None, rule_id: str = RULE_ROLE) -> dict:
|
|
134
139
|
"""从 M1 规则库取「role 回填」靶规则 → `{field, layers, remedy, …}`。
|
|
135
140
|
|
|
@@ -158,6 +163,7 @@ def role_rule(*, rules=None, rules_dir=None, rule_id: str = RULE_ROLE) -> dict:
|
|
|
158
163
|
|
|
159
164
|
# ---- 取值推导(唯一入口:只搬运已声明的证据)------------------------------
|
|
160
165
|
|
|
166
|
+
# 生效条件:按 sources 逐档判定——含 SOURCE_TAGS 时取 fm 的 tags 内首个 ROLE_TAG_PREFIX 前缀标签,其值属 ALL_ROLES 则返回 (值, BASIS_TAG, SOURCE_TAGS, None),值域外则返回 (None,None,None, REASON_TAG_OOD 原因);tags 未定出时含 SOURCE_MAP 则以小写 writer 取 role_map 值,属 WORK_ROLES 返回 REASON_MAP_WORK 原因、属 ALL_ROLES 返回 (值,"WRITER_ROLE_MAP[writer]",SOURCE_MAP,None)、否则返回 REASON_MAP_OOD 原因(命中域外值即返回、不降级);再否且含 SOURCE_LAYER 且 LAYER_DEFAULT_ROLE 命中 e["layer"] 时返回 (值, BASIS_LAYER, SOURCE_LAYER, None);全部未命中则 writer 非空返回 REASON_WRITER_NO_MAP:writer,writer 为空返回 REASON_NO_SOURCE。
|
|
161
167
|
def _candidate_role(e, fm, *, sources, role_map) -> tuple:
|
|
162
168
|
"""→ `(role, basis, source, reason)`:有来源则 reason=None;无来源则 role=None。
|
|
163
169
|
|
|
@@ -192,6 +198,7 @@ def _candidate_role(e, fm, *, sources, role_map) -> tuple:
|
|
|
192
198
|
else REASON_NO_SOURCE)
|
|
193
199
|
|
|
194
200
|
|
|
201
|
+
# 生效条件:按序判定——_readable_guard(cg, e) 为假返回 ("denied", None);cg._read(e) 的 fm 为 None 返回 ("unreadable", None);crypto.is_encrypted(content) 为真返回 ("locked", None);fm.get("role") 非空白返回 ("present", None);否则 _candidate_role(e, fm, sources=sources, role_map=role_map) 的 role 为假返回 ("unfillable", {id,layer,reason,writer}),role 为真返回 ("", {id,layer,role,source,basis,before,had_key,writer})。
|
|
195
202
|
def _classify(cg, e, nid, *, sources, role_map) -> tuple:
|
|
196
203
|
"""单条裁决 → `(skip_reason, item|gap)`;skip_reason 为空串表示可回填。"""
|
|
197
204
|
if not _readable_guard(cg, e):
|
|
@@ -213,6 +220,7 @@ def _classify(cg, e, nid, *, sources, role_map) -> tuple:
|
|
|
213
220
|
"had_key": "role" in fm, "writer": fm.get("writer")}
|
|
214
221
|
|
|
215
222
|
|
|
223
|
+
# 生效条件:want 由必填 layers 决定;layer 为真值时须属 layers 否则抛 ValueError,且命中后 want 收窄为 {layer};ids 为真值时只保留白名单内 nid、为假值(None/[])时不过滤;prefix 为真值时只保留 nid 以之开头者;仅 str(e.get("layer")) 属 want 的条目按 nid 升序进入返回列表。
|
|
216
224
|
def _iter_scope(cg, *, layers, layer=None, prefix=None, ids=None) -> list:
|
|
217
225
|
"""按规则作用域遍历索引条目 → `[(nid, entry)]`(只读、nid 稳定序)。
|
|
218
226
|
|
|
@@ -241,6 +249,7 @@ def _iter_scope(cg, *, layers, layer=None, prefix=None, ids=None) -> list:
|
|
|
241
249
|
|
|
242
250
|
# ---- 预演 -----------------------------------------------------------------
|
|
243
251
|
|
|
252
|
+
# 生效条件:以 x 经 _as_cg 得 cg 并先经 role_rule(rules,rules_dir,rule_id) 取规则(缺规则/缺 layer/缺 field 时该步抛 ValueError),role_map 非空时的条目以小写去空白键合并进 WRITER_ROLE_MAP 副本,再遍历 _iter_scope(cg, layers=rule["layers"], layer=layer, prefix=prefix, ids=ids) 逐条 _classify 后返回不写盘的 rep;每条 item 仅在 limit is None 或 len(rep["items"]) < limit 时追加(limit=0 时 items 为空但 targeted 仍累加),sample 为真且 planned_ids 非空时以 max(1, len(planned_ids)//int(sample)) 为步长取前 int(sample) 项。
|
|
244
253
|
def role_plan(x, layer=None, limit=None, ids=None, prefix=None, *,
|
|
245
254
|
sources=DEFAULT_SOURCES, role_map=None, rule_id=RULE_ROLE,
|
|
246
255
|
rules=None, rules_dir=None, sample=0) -> dict:
|
|
@@ -298,6 +307,7 @@ def role_plan(x, layer=None, limit=None, ids=None, prefix=None, *,
|
|
|
298
307
|
|
|
299
308
|
# ---- 执行 / 回滚 / 留痕 / 对照 -------------------------------------------
|
|
300
309
|
|
|
310
|
+
# 生效条件:x 经 _as_cg,batch 为假值时回落 BATCH_DEFAULT;先调 role_plan 取 items,entry_ids 为真值时按 entry_id 收窄;逐项处理:索引无该节点或 _read 得 fm 为 None → skipped_missing,_readable_guard 为假 → skipped_denied,crypto.is_encrypted(content) 为真 → skipped_locked,fm["role"] 非空 → skipped_drift,否则写回 role 并 append_jsonl 留痕、written 递增;written 非零时 cg.rebuild_index(),返回含 plan_remaining 的 rep。
|
|
301
311
|
def role_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
|
|
302
312
|
batch=BATCH_DEFAULT, sources=DEFAULT_SOURCES, role_map=None,
|
|
303
313
|
rule_id=RULE_ROLE, rules=None, rules_dir=None, actor=None,
|
|
@@ -366,6 +376,7 @@ def role_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
|
|
|
366
376
|
return rep
|
|
367
377
|
|
|
368
378
|
|
|
379
|
+
# 生效条件:只处理 action == ACTION_ROLE 的留痕,且 batch 为真时要求 rec.get("batch") == batch(batch 为 None/假值时不按批次过滤)、entry_ids 为真时要求 rec.get("entry_id") ∈ set(entry_ids);rec 的 write_id 已在既有 ACTION_ROLE_ROLLBACK 记录中 → skipped_done,节点不在索引或 read 不到或加密 → missing,当前 fm.get("role") 为空白或与 rec.get("value") 不等 → conflict 不覆盖,相等时按 fm_before.get("had_key") 恢复 fm_before["role"] 或删除 role 键;reverted 非 0 时 cg.rebuild_index()。
|
|
369
380
|
def role_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
|
|
370
381
|
"""按留痕反向应用:撤销 role 回填。
|
|
371
382
|
|
|
@@ -423,6 +434,7 @@ def role_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
|
|
|
423
434
|
return rep
|
|
424
435
|
|
|
425
436
|
|
|
437
|
+
# 生效条件:action 为真时只保留 r.get("action") == action 的记录、batch 为真时只保留 r.get("batch") == batch 的记录,total/by_action 统计的是过滤后的 recs 全量;records 在 limit 为假值(0/None)时返回全部 recs,否则返回 recs[-int(limit):]。
|
|
426
438
|
def history(x, limit=100, action=None, batch=None) -> dict:
|
|
427
439
|
"""读 `_govern.jsonl` 留痕(治理动作的可审计面)。"""
|
|
428
440
|
cg = _as_cg(x)
|
|
@@ -437,6 +449,7 @@ def history(x, limit=100, action=None, batch=None) -> dict:
|
|
|
437
449
|
"by_action": by_action, "records": tail}
|
|
438
450
|
|
|
439
451
|
|
|
452
|
+
# 生效条件:x 经 _as_cg 且 role_rule(rules=rules, rules_dir=rules_dir, rule_id=rule_id) 命中并声明了 matcher.layer 与 field_absent.field 时,返回以 conformance.load_index(cg.root) 的节点为口径的统计(by_layer 计全部节点、by_value 只计 role 非空白者、target_met 由 role_ratio_kn >= float(THRESHOLDS["role_coverage_min"]) 决定、gap_to_target 取 max(0.0, thr-kn));规则缺失或声明不全时在 role_rule 处抛 ValueError。
|
|
440
453
|
def role_stats(x, *, rule_id=RULE_ROLE, rules=None, rules_dir=None) -> dict:
|
|
441
454
|
"""只读对照:role 覆盖率 + 取值分布(治理前后量化用)。
|
|
442
455
|
|
|
@@ -466,6 +479,7 @@ def role_stats(x, *, rule_id=RULE_ROLE, rules=None, rules_dir=None) -> dict:
|
|
|
466
479
|
|
|
467
480
|
# ---- 统一入口 ------------------------------------------------------------
|
|
468
481
|
|
|
482
|
+
# 生效条件:action 不在 ACTIONS 内时抛 ValueError;action == ACTION_ROLE 时以 kw.pop("apply", False) 为真调用 role_apply(x, **kw)、为假(含缺该键)调用 role_plan(x, **kw);其余情况以 {ACTION_ROLE_ROLLBACK: role_rollback, "role_history": history, "role_stats": role_stats}[action] 取 fn 并返回 fn(x, **kw)。
|
|
469
483
|
def run(x, action, **kw) -> dict:
|
|
470
484
|
"""`role`(`apply=True` 则执行)/ `role_rollback` / `role_history` / `role_stats`。"""
|
|
471
485
|
if action not in ACTIONS:
|
|
@@ -485,6 +499,7 @@ def run(x, action, **kw) -> dict:
|
|
|
485
499
|
# 须写在子命令**之后**:`govern plan --root <root>`。写在子命令之前会被顶层解析器
|
|
486
500
|
# 当作位置参数吃掉(`scripts/review_cli.py` 的同型坑,2026-09-15 实测更正)。
|
|
487
501
|
|
|
502
|
+
# 生效条件:s 为假值(None/空串)时直接返回 DEFAULT_SOURCES;否则按 "," 切分并 strip 丢弃空段,任一段不在 ALL_SOURCES 内即抛 ValueError,切分后 vals 为空(如 s=",")也抛 ValueError,其余返回该非空元组。
|
|
488
503
|
def _parse_sources(s) -> tuple:
|
|
489
504
|
"""CLI 侧来源档解析:逗号分隔,值域封闭(非法即抛,不静默降级)。"""
|
|
490
505
|
if not s:
|
|
@@ -498,6 +513,7 @@ def _parse_sources(s) -> tuple:
|
|
|
498
513
|
return vals
|
|
499
514
|
|
|
500
515
|
|
|
516
|
+
# 生效条件:无参数,调用即返回 argparse.ArgumentParser(add_help=False),其中已含 --root(default=None)、--rules-dir(default=None)、--json(store_true) 三项,无分支。
|
|
501
517
|
def _common_parser() -> argparse.ArgumentParser:
|
|
502
518
|
ap = argparse.ArgumentParser(add_help=False)
|
|
503
519
|
ap.add_argument("--root", default=None, help="真源根(缺省读环境变量 MDCG_ROOT)")
|
|
@@ -506,6 +522,7 @@ def _common_parser() -> argparse.ArgumentParser:
|
|
|
506
522
|
return ap
|
|
507
523
|
|
|
508
524
|
|
|
525
|
+
# 生效条件:传入 p 即向它注册 --layer/--prefix/--ids/--limit(type=int)/--sources 五个参数,无分支且无返回值。
|
|
509
526
|
def _add_scope_args(p) -> None:
|
|
510
527
|
p.add_argument("--layer", help="收窄到该层(须在规则 matcher 层之内)")
|
|
511
528
|
p.add_argument("--prefix", help="只处理该 id 前缀")
|
|
@@ -516,17 +533,20 @@ def _add_scope_args(p) -> None:
|
|
|
516
533
|
% ",".join(DEFAULT_SOURCES))
|
|
517
534
|
|
|
518
535
|
|
|
536
|
+
# 生效条件:传入 a 后返回 {'layer': a.layer, 'prefix': a.prefix, 'limit': a.limit, 'ids': [...], 'sources': _parse_sources(a.sources), 'rules_dir': a.rules_dir},其中 ids 由 a.ids(为假值即 None/空串)按 "," 切分去空段、结果为空则回落 None,sources 由 a.sources 经 _parse_sources 解析(假值 → DEFAULT_SOURCES,非法值抛 ValueError)。
|
|
519
537
|
def _scope_kw(a) -> dict:
|
|
520
538
|
return {"layer": a.layer, "prefix": a.prefix, "limit": a.limit,
|
|
521
539
|
"ids": [i.strip() for i in (a.ids or "").split(",") if i.strip()] or None,
|
|
522
540
|
"sources": _parse_sources(a.sources), "rules_dir": a.rules_dir}
|
|
523
541
|
|
|
524
542
|
|
|
543
|
+
# 生效条件:SOURCE_LAYER 在 sources 内时向 sys.stderr 打印 "警告:" + LAYER_DEFAULT_WARNING,否则不输出。
|
|
525
544
|
def _warn_layer_default(sources) -> None:
|
|
526
545
|
if SOURCE_LAYER in sources:
|
|
527
546
|
print("警告:" + LAYER_DEFAULT_WARNING, file=sys.stderr)
|
|
528
547
|
|
|
529
548
|
|
|
549
|
+
# 生效条件:传入 msg 即向 sys.stderr 打印该 msg 并返回 2,无分支。
|
|
530
550
|
def _die(msg: str) -> int:
|
|
531
551
|
print(msg, file=sys.stderr)
|
|
532
552
|
return 2
|
|
@@ -671,4 +691,4 @@ def main(argv=None) -> int:
|
|
|
671
691
|
|
|
672
692
|
|
|
673
693
|
if __name__ == "__main__":
|
|
674
|
-
sys.exit(main())
|
|
694
|
+
sys.exit(main())
|
package/md_cg/mreview/locate.py
CHANGED
|
@@ -113,12 +113,14 @@ _REF_DEAD_STATUSES = ("dangling", "stale")
|
|
|
113
113
|
_FIELD_LAYER_CACHE = None
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
# 生效条件:kind 经 str(kind or "").strip() 得 k(None/空串等假值 → 空串 ""),k 命中 KIND_ALIASES 时返回其规范名,否则原样返回 k。
|
|
116
117
|
def canonical_kind(kind) -> str:
|
|
117
118
|
"""M1/D1 用词 → D1 规范名(未知原样返回,不假装认路)。"""
|
|
118
119
|
k = str(kind or "").strip()
|
|
119
120
|
return KIND_ALIASES.get(k, k)
|
|
120
121
|
|
|
121
122
|
|
|
123
|
+
# 生效条件:仅当 rules 与 rules_dir 均为 None 且模块级 _FIELD_LAYER_CACHE 非 None 时直接返回该缓存;否则遍历 rules(dict 取其 "rules" 键的列表、非 dict 直接 list(rules))或 rules_dir 经 RS.load_rules 取得的 rules 列表,从限了 matcher.layer 的 mechanical 规则中收集 field_absent 的 spec["field"] 与 evidence_zero 的 evidence_count,返回 {字段: 排序列},且只在 rules 与 rules_dir 均为 None 时写回 _FIELD_LAYER_CACHE。
|
|
122
124
|
def field_layer_scope(rules=None, rules_dir=None) -> dict:
|
|
123
125
|
"""字段 → 适用层清单(**派生**自 M1 规则库,不在此另立一份)。
|
|
124
126
|
|
|
@@ -155,6 +157,7 @@ def field_layer_scope(rules=None, rules_dir=None) -> dict:
|
|
|
155
157
|
return out
|
|
156
158
|
|
|
157
159
|
|
|
160
|
+
# 生效条件:(scope or {}).get(field) 为假值(scope 为 None、空 dict 或 field 不在其中)→ 返回 True;want 为真值时仅当 str((meta or {}).get("layer")) 在 set(want) 内返回 True,否则 False。
|
|
158
161
|
def _layer_ok(field, scope, meta) -> bool:
|
|
159
162
|
"""字段层门限:**限层的字段只在该层的节点上检查**。
|
|
160
163
|
|
|
@@ -168,6 +171,7 @@ def _layer_ok(field, scope, meta) -> bool:
|
|
|
168
171
|
return str((meta or {}).get("layer")) in set(want)
|
|
169
172
|
|
|
170
173
|
|
|
174
|
+
# 生效条件:os.path.getmtime(path) 成功则返回该 mtime;抛 OSError 或 TypeError → 返回 None。
|
|
171
175
|
def _mtime(path):
|
|
172
176
|
"""文件 mtime(不可读 → `None`,不假装知道)。"""
|
|
173
177
|
try:
|
|
@@ -176,6 +180,7 @@ def _mtime(path):
|
|
|
176
180
|
return None
|
|
177
181
|
|
|
178
182
|
|
|
183
|
+
# 生效条件:root 或 rel(path 为真值时取 path,否则取 meta 的 "path")为空 → 返回 ("unknown", "无 root/path 可用,取不到盘上 mtime,成因未判定");两者都有时取节点文件与 CF.INDEX_FILE 的 mtime,任一为 None → 返回 ("unknown", "节点文件或索引快照不可读,成因未判定"),节点 mtime 减索引 mtime 之差 > MTIME_TOLERANCE → 返回 ("index_lag", …),否则返回 ("true_mismatch", …)。
|
|
179
184
|
def hash_mismatch_cause(meta, *, root=None, path=None) -> tuple:
|
|
180
185
|
"""`content_hash` 声明值 ≠ 正文实算值 → **成因**判定(靠 mtime 证据,不猜)。
|
|
181
186
|
|
|
@@ -204,12 +209,14 @@ def hash_mismatch_cause(meta, *, root=None, path=None) -> tuple:
|
|
|
204
209
|
|
|
205
210
|
# ---------------------------- 基础工具(纯函数) ----------------------------
|
|
206
211
|
|
|
212
|
+
# 生效条件:v 为 list/tuple/dict 时返回 not v(空容器 → True);其余类型返回 v is None 或 str(v).strip() 为 "" 或 "None"。
|
|
207
213
|
def _blank(v) -> bool:
|
|
208
214
|
if isinstance(v, (list, tuple, dict)):
|
|
209
215
|
return not v
|
|
210
216
|
return v is None or str(v).strip() in ("", "None")
|
|
211
217
|
|
|
212
218
|
|
|
219
|
+
# 生效条件:int(float(v)) 可算(含数字字符串)则返回该整数;抛 TypeError 或 ValueError(含 v 为 None、非数字串)→ 返回 None。
|
|
213
220
|
def _as_int(v):
|
|
214
221
|
try:
|
|
215
222
|
return int(float(v))
|
|
@@ -217,6 +224,7 @@ def _as_int(v):
|
|
|
217
224
|
return None
|
|
218
225
|
|
|
219
226
|
|
|
227
|
+
# 生效条件:float(v) 可算则返回该浮点数;抛 TypeError 或 ValueError(含 v 为 None、非数字串)→ 返回 None。
|
|
220
228
|
def _as_float(v):
|
|
221
229
|
try:
|
|
222
230
|
return float(v)
|
|
@@ -227,6 +235,7 @@ def _as_float(v):
|
|
|
227
235
|
_SENT_END = "。!?;!?;\n"
|
|
228
236
|
|
|
229
237
|
|
|
238
|
+
# 生效条件:content 为假值(None/空串)按 "" 处理,逐字符命中 _SENT_END 切出且 seg.strip() 非空的段以 len(out) 为序号追加 (索引, start, i+1, 原文),末尾 tail.strip() 非空亦追加;无合格段 → 返回空列表。
|
|
230
239
|
def sentence_spans(content: str) -> list:
|
|
231
240
|
"""正文 → `[(句索引, start, end, 原文)]`;空句不编号(索引连续,确定性)。"""
|
|
232
241
|
text, out, start = content or "", [], 0
|
|
@@ -246,6 +255,7 @@ def sentence_spans(content: str) -> list:
|
|
|
246
255
|
_RE_MARK = re.compile(r"^#[ \t]*(?P<mark>[^::\n]{1,16})[::][^\n]*", re.M)
|
|
247
256
|
|
|
248
257
|
|
|
258
|
+
# 生效条件:在 content(假值 → "")上用模块级 _RE_MARK 迭代匹配,以 m.group("mark").strip() 为键 setdefault 记下首次出现的 [m.start(), m.end());无匹配(含 content 为假值)→ 返回空 dict。
|
|
249
259
|
def mark_spans(content: str) -> dict:
|
|
250
260
|
"""正文 CCG 要素行 → `{要素名: [start, end)}`;同要素取首次出现(确定性)。"""
|
|
251
261
|
out = {}
|
|
@@ -254,6 +264,7 @@ def mark_spans(content: str) -> dict:
|
|
|
254
264
|
return out
|
|
255
265
|
|
|
256
266
|
|
|
267
|
+
# 生效条件:(text or "").split("\n") 的每行 strip 后非空、不以 "#" 开头且含 ":" 时,取首个冒号前的键 k,k 非空且尚未入表则记 (1-based 行号, [该行起始偏移, 起始偏移+len(line)]);text 为假值或无合格行 → 返回空 dict。
|
|
257
268
|
def key_line_spans(text: str) -> dict:
|
|
258
269
|
"""节点文件原文 → `{键: (1-based 行号, [start, end))}`。
|
|
259
270
|
|
|
@@ -271,6 +282,7 @@ def key_line_spans(text: str) -> dict:
|
|
|
271
282
|
return out
|
|
272
283
|
|
|
273
284
|
|
|
285
|
+
# 生效条件:index 为 dict 时其 "nodes" 为 dict 则返回 index["nodes"],否则返回 index 本身;index 为 None/非 dict 时调 CF.load_index(root),抛 OSError 或 ValueError → 返回 {},返回值为 dict 且其 "nodes" 为 dict → 返回该 "nodes",是 dict → 原样返回,否则返回 {}。
|
|
274
286
|
def _index(root, index=None) -> dict:
|
|
275
287
|
"""索引节点表 `{node_id: meta}`(兼容 load_index 的 `{"nodes": …}` 形态)。"""
|
|
276
288
|
if isinstance(index, dict):
|
|
@@ -284,6 +296,7 @@ def _index(root, index=None) -> dict:
|
|
|
284
296
|
return idx if isinstance(idx, dict) else {}
|
|
285
297
|
|
|
286
298
|
|
|
299
|
+
# 生效条件:_index(root, index) 中 node_id 对应值非 dict → 返回 None;否则用 meta.get("path")(绝对路径直接用,否则 join(root, str(rel or "")))读文件——OSError 时返回 content/text 为 None、fm 为 {} 的 dict,成功则把 NF.loads(text) 得到的 content 与 fm(假值 → {})连同 meta/path/text 一并返回。
|
|
287
300
|
def load_node(node_id, root, *, index=None) -> dict:
|
|
288
301
|
"""读一个节点(索引 meta + 文件 frontmatter + 正文原文)。
|
|
289
302
|
|
|
@@ -307,6 +320,7 @@ def load_node(node_id, root, *, index=None) -> dict:
|
|
|
307
320
|
"text": text, "path": fp}
|
|
308
321
|
|
|
309
322
|
|
|
323
|
+
# 生效条件:span 为 None → 返回 "";否则取 (text or "")[span[0]:span[1]] 去空白并把换行替换为 "⏎",长度超 SNIPPET_MAX 时截断并追加 "…"。
|
|
310
324
|
def _snippet(text, span) -> str:
|
|
311
325
|
"""命中片段(供人工核对肉眼确认「指的是不是这一句」)。"""
|
|
312
326
|
if span is None:
|
|
@@ -315,6 +329,7 @@ def _snippet(text, span) -> str:
|
|
|
315
329
|
return seg[:SNIPPET_MAX] + ("…" if len(seg) > SNIPPET_MAX else "")
|
|
316
330
|
|
|
317
331
|
|
|
332
|
+
# 生效条件:任意 node_id/kind/field/span/evidence 均原样写入返回 dict 的 node_id/issue_kind/field/span/evidence 键,可选 rule/line/sentence/snippet/peer/cause/severity 未传时为 None、status 未传时为 "located",不做任何校验。
|
|
318
333
|
def _hit(node_id, kind, field, span, evidence, *, rule=None, line=None,
|
|
319
334
|
sentence=None, snippet=None, peer=None, cause=None, status="located",
|
|
320
335
|
severity=None) -> dict:
|
|
@@ -330,6 +345,7 @@ def _hit(node_id, kind, field, span, evidence, *, rule=None, line=None,
|
|
|
330
345
|
"severity": severity}
|
|
331
346
|
|
|
332
347
|
|
|
348
|
+
# 生效条件:text 为真值、span 与 content 均非 None 且 text.find(content)>=0 时,返回 text.count("\n",0,min(off+span[0],len(text)))+1 的 1-based 行号;text 假值或 span/content 为 None 或 content 未找到时返回 None。
|
|
333
349
|
def _line_of(text, content, span):
|
|
334
350
|
"""正文区间 → 节点文件原文的 1-based 行号(供人工核对直接跳文件)。"""
|
|
335
351
|
if not text or span is None or content is None:
|
|
@@ -346,6 +362,7 @@ def _line_of(text, content, span):
|
|
|
346
362
|
# 新增判据 = 加函数 + 注册,不改 locate 主流程(与 M1「引擎冻结、规则可增删」同构)。
|
|
347
363
|
# ctx = {"fm", "text", "root", "index", "peers", "now"}
|
|
348
364
|
|
|
365
|
+
# 生效条件:在 node_id/meta/content/ctx 下,对 FM_SCAN_FIELDS 中除 verification_basis、condition_space 外且 _layer_ok(f,scope,meta) 为真的字段,若 meta.get(f) 与 ctx.get("fm") 或 {} 中的同名字段皆 _blank 则记 field_absent;对过 _layer_ok 且 _as_int(meta.get("evidence_count"))==0 记 evidence_zero;对 meta.get("importance") 非 None 且 _as_float 为 None 或不在 [0.0,1.0] 记 field_invalid;对 condition_space 经 meta 或回落 ctx.get("fm") 后 NF.condition_space_missing 非空记 condition_slots;对正文缺 NF.CCG_MARKS 行记 ccg_incomplete,返回这些命中列表。
|
|
349
366
|
def _loc_missing_field(node_id, meta, content, ctx):
|
|
350
367
|
"""frontmatter/正文结构字段为空(判据与 M1 `field_absent`/`evidence_zero` 同源)。
|
|
351
368
|
|
|
@@ -407,6 +424,7 @@ def _loc_missing_field(node_id, meta, content, ctx):
|
|
|
407
424
|
return hits
|
|
408
425
|
|
|
409
426
|
|
|
427
|
+
# 生效条件:meta.get("verification_basis") 为空时回落 ctx.get("fm") 或 {} 的 verification_basis,若两者皆 _blank 返回 basis_absent 命中;非空但 str(basis) 不在 NF.VERIFICATION_BASIS 返回 basis_enum 命中;在枚举内时按 CC.classify_track 依 meta.get("layer")、meta.get("tags") 与 content 判赛道,若 CC.basis_licensed 为假返回 basis_licensed 命中;否则返回 []。
|
|
410
428
|
def _loc_weak_source(node_id, meta, content, ctx):
|
|
411
429
|
"""验证基底缺失/越枚举/与赛道不相容(与 M1 `basis_licensed` 判据逐字同源)。"""
|
|
412
430
|
fm = ctx.get("fm") or {}
|
|
@@ -436,6 +454,7 @@ def _loc_weak_source(node_id, meta, content, ctx):
|
|
|
436
454
|
return []
|
|
437
455
|
|
|
438
456
|
|
|
457
|
+
# 生效条件:ctx.get("fm") 或 {} 中 code_ref 或 doc_ref 为非空 dict 且 RI.probe_ref(ref) 返回的 status 属于 _REF_DEAD_STATUSES(dangling 或 stale)时,返回对应 stale 命中(dangling 归因载体消失、stale 归因区间哈希不符);否则返回 []。
|
|
439
458
|
def _loc_stale(node_id, meta, content, ctx):
|
|
440
459
|
"""**依赖存在性**——声明的载体(源文件)已不存在 / 已漂移(真正的适用边界越出)。
|
|
441
460
|
|
|
@@ -486,6 +505,7 @@ def _loc_stale(node_id, meta, content, ctx):
|
|
|
486
505
|
return hits
|
|
487
506
|
|
|
488
507
|
|
|
508
|
+
# 生效条件:从 meta.get("condition_space") 或回落 ctx.get("fm") 的 condition_space(须为 dict)取 time_window,缺则取 meta.get("time_window"),若 NF.is_full_time_window(tw) 或 float(tw[1]) 抛 TypeError/ValueError/IndexError/KeyError 或 ctx.get("now") 为 None 或 hi>=float(now) 则返回 [];否则返回一条 severity="info" 的 observation_aged 命中。
|
|
489
509
|
def _loc_observation_aged(node_id, meta, content, ctx):
|
|
490
510
|
"""时间窗已过——**这是观测时刻,不是失效声明**(观测面,不进告警面)。
|
|
491
511
|
|
|
@@ -526,6 +546,7 @@ def _loc_observation_aged(node_id, meta, content, ctx):
|
|
|
526
546
|
snippet=_snippet(content, span))]
|
|
527
547
|
|
|
528
548
|
|
|
549
|
+
# 生效条件:h 取 str(meta.get("content_hash") or "").strip(),若 _blank(h) 则 h=NF.content_hash(content 或 "");对 ctx.get("peers") 或 [] 中 node_id 不等于本节点且 ph=str(p.get("hash") or "") 或回落 NF.content_hash(p.get("content") or "") 后非空且等于 h 的 peer 计入 matched;matched 非空时返回一条 dup 命中(span=[0,len(body)]),否则返回 []。
|
|
529
550
|
def _loc_dup(node_id, meta, content, ctx):
|
|
530
551
|
"""同组同内容指纹(与 M1 `dup_hash_group` 同判据;D1 名 `dup`)。"""
|
|
531
552
|
body = content if content is not None else ""
|
|
@@ -549,6 +570,7 @@ def _loc_dup(node_id, meta, content, ctx):
|
|
|
549
570
|
snippet=_snippet(body, span))]
|
|
550
571
|
|
|
551
572
|
|
|
573
|
+
# 生效条件:对 ctx.get("peers") 或 [] 中每个非自身 peer,按 sentence_spans 对齐 content 与 peer content 的句子,当同一句位上去数字骨架相同(WL._skeleton(seg) 非空、长度 ≥ WL.MIN_SKELETON 且等于 peer 骨架)且 seg.strip() != ptext.strip() 的句子数达到 MIN_FLOW_SENTENCES 时,为这些句各返回一条 template_flow 命中;否则返回 []。
|
|
552
574
|
def _loc_template_flow(node_id, meta, content, ctx):
|
|
553
575
|
"""同模板流水:与同组节点逐句「去数字骨架相同、字面不同」→ 指向那些句。
|
|
554
576
|
|
|
@@ -586,6 +608,7 @@ def _loc_template_flow(node_id, meta, content, ctx):
|
|
|
586
608
|
return hits
|
|
587
609
|
|
|
588
610
|
|
|
611
|
+
# 生效条件:meta.get("content_hash") 非 _blank 且 content 非 None 且 h != NF.content_hash(content) 时,返回一条 hash_declared_vs_actual 的 contradiction 命中(cause 由 hash_mismatch_cause 依 meta/ctx.get("root")/ctx.get("path") 判);ctx.get("fm") 或 {} 的 id 非 _blank 且 str(fm_id) != str(node_id) 时额外返回一条 id_declared_vs_index 命中;两者皆不成立返回 []。
|
|
589
612
|
def _loc_contradiction(node_id, meta, content, ctx):
|
|
590
613
|
"""**确定性**矛盾:声明与事实不符(指纹 / 标识)。
|
|
591
614
|
|
|
@@ -632,6 +655,7 @@ SEMANTIC_ONLY = {
|
|
|
632
655
|
}
|
|
633
656
|
|
|
634
657
|
|
|
658
|
+
# 生效条件:始终返回 SEMANTIC_ONLY 中以 str(kind or "").strip() 为键查得的值,缺键时返回空串(空串表示可定位)。
|
|
635
659
|
def blindspot_reason(kind) -> str:
|
|
636
660
|
"""D1 拒绝定位的类别 → 理由(空串表示可定位)。"""
|
|
637
661
|
return SEMANTIC_ONLY.get(str(kind or "").strip(), "")
|
|
@@ -639,6 +663,7 @@ def blindspot_reason(kind) -> str:
|
|
|
639
663
|
|
|
640
664
|
# ---------------------------- 主入口 ----------------------------
|
|
641
665
|
|
|
666
|
+
# 生效条件:items 中每项按 content=it.get("content") or ""、hash=str(it.get("hash") or "").strip() 或回落 NF.content_hash(content)、sk=WL.template_signature(content) or "" 预处理后,返回 {node_id: [同 hash 或同非空 sk 的其他 peer]};items 为空/None 返回空 dict。
|
|
642
667
|
def build_peers(items) -> dict:
|
|
643
668
|
"""`[{node_id, content, hash?, …}]` → `{node_id: [peer, …]}`。
|
|
644
669
|
|
|
@@ -668,6 +693,7 @@ def build_peers(items) -> dict:
|
|
|
668
693
|
return out
|
|
669
694
|
|
|
670
695
|
|
|
696
|
+
# 生效条件:对 issue_hint(None/str/dict 或其 list/tuple)逐 hint 分类,返回 (kinds, fields, blind):能 canonical_kind 到 ISSUE_KINDS 或 ADVISORY_KINDS 的入 kinds,blindspot_reason 非空的入 blind,其余非空 kind 与 field 入 fields;issue_hint 为 None 时 kinds/fields 为空集、blind 为空列表。
|
|
671
697
|
def _norm_hint(issue_hint):
|
|
672
698
|
"""issue_hint → `(kinds, fields, blindspot)`。
|
|
673
699
|
|
|
@@ -699,6 +725,7 @@ def _norm_hint(issue_hint):
|
|
|
699
725
|
return kinds, fields, blind
|
|
700
726
|
|
|
701
727
|
|
|
728
|
+
# 生效条件:当 meta 与 content 均非 None 时不读盘;否则 root 为 None 抛 ValueError,经 load_node 读不到节点返回 {"hits": [], "blindspot": blind+["节点 %s 不在索引"%node_id], "load": None};读到时补 meta/content/text/fm/path,按 kinds 非空时只跑 sorted(kinds)、否则 fields 非空或 issue_hint 为 None 时跑 sorted(ISSUE_KINDS)、否则 run=[] 执行 LOCATORS,对命中按 fields 过滤、补 line 并排序后返回 {"hits": hits, "blindspot": blind, "load": {...}}。
|
|
702
729
|
def locate_ex(node_id, issue_hint=None, *, meta=None, content=None, fm=None, text=None,
|
|
703
730
|
root=None, index=None, peers=None, now=None, path=None,
|
|
704
731
|
field_layers=None) -> dict:
|
|
@@ -761,11 +788,13 @@ def locate_ex(node_id, issue_hint=None, *, meta=None, content=None, fm=None, tex
|
|
|
761
788
|
"content_len": len(content or "")}}
|
|
762
789
|
|
|
763
790
|
|
|
791
|
+
# 生效条件:给定 node_id(必填)与可选 issue_hint 及 kw 后,直接返回 locate_ex(node_id, issue_hint, **kw) 结果的 "hits" 列表。
|
|
764
792
|
def locate(node_id, issue_hint=None, **kw) -> list:
|
|
765
793
|
"""**D1 契约入口**:`locate(node_id, issue_hint) -> [{field, span, issue_kind, evidence}]`。"""
|
|
766
794
|
return locate_ex(node_id, issue_hint, **kw)["hits"]
|
|
767
795
|
|
|
768
796
|
|
|
797
|
+
# 生效条件:给定 hits 与 key 后,返回 hits 中每个 h.get(key) 字符串化取值到出现次数的字典(键升序);hits 为 None/空时返回空 dict。
|
|
769
798
|
def _counts(hits, key) -> dict:
|
|
770
799
|
out = {}
|
|
771
800
|
for h in hits or []:
|
|
@@ -774,6 +803,7 @@ def _counts(hits, key) -> dict:
|
|
|
774
803
|
return dict(sorted(out.items()))
|
|
775
804
|
|
|
776
805
|
|
|
806
|
+
# 生效条件:items 显式给出时不读盘;items 为 None 时 root 为 None 抛 ValueError,否则按 node_ids 逐节点 load_node 组装 items(读不到的记入 missing);随后对每个 item 调 locate_ex 汇总 hits 或 clean,并返回含 nodes/hits/by_kind/by_field/by_rule/clean/missing 的 dict。
|
|
777
807
|
def locate_many(node_ids=None, *, root=None, index=None, items=None,
|
|
778
808
|
issue_hint=None, now=None) -> dict:
|
|
779
809
|
"""批量定位:**同一批内**互为对照(`dup` / `template_flow` 的组 = 本批)。
|
|
@@ -817,6 +847,7 @@ def locate_many(node_ids=None, *, root=None, index=None, items=None,
|
|
|
817
847
|
"clean": clean, "missing": missing}
|
|
818
848
|
|
|
819
849
|
|
|
850
|
+
# 生效条件:从 pkg.get("entries") or [] 取条目并跳过 e.get("node_id") 为空者,root 非 None 时逐个 load_node 取正文,取不到时回落 e.get("excerpt") or "",组装 items 调 locate_many 后返回其结果并附加 bundle_id 与 entries 数量;pkg 为 None 时 entries 为空列表。
|
|
820
851
|
def locate_package(pkg, *, root=None, index=None, issue_hint=None, now=None) -> dict:
|
|
821
852
|
"""M1 包 → 定位汇总(组的作用域 = 本包,与 M1 机械层同口径)。
|
|
822
853
|
|
|
@@ -844,6 +875,7 @@ def locate_package(pkg, *, root=None, index=None, issue_hint=None, now=None) ->
|
|
|
844
875
|
return out
|
|
845
876
|
|
|
846
877
|
|
|
878
|
+
# 生效条件:给定 hits 后返回 total=len(hits or [])、去重 node_id 数、排序后的 node_ids、以及 by_kind/by_field/by_rule 计数;hits 为 None/空时 total=0、nodes=0、node_ids=[]。
|
|
847
879
|
def summary(hits) -> dict:
|
|
848
880
|
"""命中汇总(审计留痕用)。"""
|
|
849
881
|
nodes = sorted({str(h.get("node_id")) for h in hits or []})
|
|
@@ -852,6 +884,7 @@ def summary(hits) -> dict:
|
|
|
852
884
|
"by_rule": _counts(hits, "rule")}
|
|
853
885
|
|
|
854
886
|
|
|
887
|
+
# 生效条件:给定 hits 后逐条生成 Markdown 行并返回表头加各行:field/line/snippet 取 h.get(...) or "—"(假值回落 "—"),span 为 None 时显示 "—" 否则 "起-止",evidence 取 (h.get("evidence") or "").replace("|","\\|")(假值回落空串)。
|
|
855
888
|
def markdown_table(hits) -> str:
|
|
856
889
|
"""人工核对清单:每行一条命中,末列留空供核对者填判定(D1 验收抽样用)。"""
|
|
857
890
|
head = ("| # | node_id | issue_kind | field | span | line | 片段 | evidence | 人工判定 |\n"
|
|
@@ -867,6 +900,7 @@ def markdown_table(hits) -> str:
|
|
|
867
900
|
return head + "\n".join(rows)
|
|
868
901
|
|
|
869
902
|
|
|
903
|
+
# 生效条件:解析 argv(缺省 sys.argv)后,--root 为假值(含默认 os.environ.get("MDCG_ROOT") 为 None 或空串)时打印提示并返回 2;--node 追加列表为空时打印提示并返回 2;否则以 --root 与 --node 调 locate_many,并按 --json 或 --markdown 输出后返回 0,两者皆无则逐行打印命中与汇总后返回 0。
|
|
870
904
|
def main(argv=None) -> int:
|
|
871
905
|
ap = argparse.ArgumentParser(prog="python -m md_cg.mreview.locate",
|
|
872
906
|
description="记忆评审 M3 · D1 字段级定位")
|