@furongjun1999/dsh-memory 0.6.0 → 0.7.0
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 +618 -572
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/README.md +1 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v20.md +283 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- 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 +52 -2
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +100 -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 +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +57 -9
- 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 +7 -2
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
- package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- 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 +460 -460
- package/lib/bridge.js +24 -2
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.js +16 -1
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/bench_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +48 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/freshness.py +511 -0
- package/md_cg/fsutil.py +454 -8
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/identity.py +3 -3
- package/md_cg/insight.py +24 -3
- package/md_cg/linkref.py +1 -1
- package/md_cg/logref.py +327 -0
- package/md_cg/mcp_server.py +4261 -3818
- package/md_cg/mdcg.py +4695 -3547
- package/md_cg/mdcos.py +4672 -4159
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +713 -576
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/sources.py +23 -6
- package/md_cg/srcindex.py +352 -0
- package/md_cg/stg.py +47 -3
- package/md_cg/subgraph.py +2 -2
- package/md_cg/sustain.py +1436 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_b1_auto_id_multiproc.py +277 -0
- package/md_cg/test_b1b2_write_face.py +417 -0
- package/md_cg/test_b2_sensitivity_landing.py +223 -0
- package/md_cg/test_b3_merge_keeps_content.py +576 -0
- package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
- package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_c3_transient_read_negative.py +793 -0
- package/md_cg/test_c8_search_rrf_gates.py +502 -0
- package/md_cg/test_ccg_form_parity.py +188 -0
- package/md_cg/test_ccgc.py +28 -3
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1387 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +519 -0
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
- package/md_cg/test_n225_nonobject_load.py +1110 -0
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +4 -3
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_p9c_dedup_hints.py +276 -0
- package/md_cg/test_policy_required_ccg.py +426 -0
- package/md_cg/test_protocol.py +22 -0
- package/md_cg/test_rank_parity_score_mode.py +522 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/test_v21r1_package_version.py +310 -0
- package/md_cg/test_writepipe.py +10 -4
- package/md_cg/tokens.py +225 -95
- package/md_cg/tool_face.py +4 -4
- package/md_cg/trust.py +49 -5
- package/md_cg/units.py +204 -6
- package/md_cg/weights.py +4 -4
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +4 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/hooks.ts +229 -16
- package/src/init.ts +15 -1
- package/src/lib/prompt_safety.ts +88 -1
- package/utf8_boot.py +237 -0
- package/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 代校验单点(generation)——「长驻进程是否还持有旧代代码」的唯一判据面
|
|
3
|
+
|
|
4
|
+
为什么存在(第 4 条根因,2026-09-30 已定因)
|
|
5
|
+
--------------------------------------------
|
|
6
|
+
本仓是 stdio 长驻 MCP 服务:`python -m md_cg.mcp_server` 只在**模块首次导入**时
|
|
7
|
+
读盘,此后一直用内存里的模块对象。而 md_cg 内有**大量函数内延迟导入**(AST 实测
|
|
8
|
+
函数内跨模块相对导入 328 处;其中相当一部分是为**避开循环导入**才写在函数里的)。
|
|
9
|
+
两者叠加 ⇒ 代码升级(重装/替换/`git pull`)后,长驻进程里「启动时已载入的旧代
|
|
10
|
+
模块」与「升级后第一次被惰性导入的新代模块」会**跨代混用**:
|
|
11
|
+
|
|
12
|
+
实测(0.6.1 发版轮):cg 写入返回
|
|
13
|
+
ImportError: cannot import name 'mint_auto_id' from 'md_cg.mdcg'
|
|
14
|
+
而同一时刻**新解释器**导入同一模块完全正常、盘面上 mdcg.py:1079 就是
|
|
15
|
+
`def mint_auto_id(...)` ⇒ 盘面无罪,是进程内的 md_cg.mdcg 停留在旧代。
|
|
16
|
+
|
|
17
|
+
处置(使用者裁定方案 b):把「**升级后须重启长驻 MCP 进程**」写成运维前提,并加
|
|
18
|
+
一道代校验把这句话变成可自检、可机械判定的东西。
|
|
19
|
+
|
|
20
|
+
本模块提供四件(外加一个自检器):
|
|
21
|
+
· ``fingerprint()`` —— 盘面 md_cg/**/*.py 的**内容**指纹(sha256 十六进制)
|
|
22
|
+
· ``STARTUP_FINGERPRINT`` —— 本模块被导入那一刻的指纹(=本进程的「代」)
|
|
23
|
+
· ``is_stale()`` —— 现算指纹 != 启动指纹
|
|
24
|
+
· ``report()`` —— 给 ``cg(op=info)`` 用的五键自报(纯增量)
|
|
25
|
+
· ``verify_delegations()`` —— 延迟导入自检器(见下)
|
|
26
|
+
|
|
27
|
+
为什么指纹用**内容哈希**而不是 mtime/size
|
|
28
|
+
------------------------------------------
|
|
29
|
+
「无改动重写」在打包/解压/校验回写里很常见(mtime 变、内容不变),用 mtime 会把
|
|
30
|
+
同代误报成换代 ⇒ 使用者被引导去做无谓的重启;用 size 更糟(不同内容同长度即漏报)。
|
|
31
|
+
内容哈希只认字节:无改动重写 = 同代(实测全量 416 个 .py / 约 11.7 MB 内容哈希
|
|
32
|
+
约 21ms,够便宜,可放在每次 op=info 上)。
|
|
33
|
+
|
|
34
|
+
``verify_delegations()`` 的判定语义(**这一条最要紧,别改成顶层 import**)
|
|
35
|
+
------------------------------------------------------------------------
|
|
36
|
+
它对盘面每个「函数内(非模块顶层)相对导入」逐项做「导入目标模块 → getattr 取
|
|
37
|
+
名字」。要点:
|
|
38
|
+
|
|
39
|
+
1. **必须在函数内做延迟导入,绝不能为了校验把它们提到模块顶层**——这些导入里
|
|
40
|
+
有相当一部分正是为避开循环导入才写在函数里的,提到顶层会直接制造循环导入。
|
|
41
|
+
校验动作本身(本函数)因此天然只能读盘 + 导入 + getattr。
|
|
42
|
+
2. `importlib.import_module` 对**已在本进程 sys.modules 里的模块直接返回内存实例**
|
|
43
|
+
——这正是跨代判据生效的地方:检查的是「本进程实际持有的那个模块对象」,
|
|
44
|
+
而不是盘面文件(盘面文件另由 fingerprint 覆盖)。
|
|
45
|
+
3. 判定三分(取值见返回 dict):
|
|
46
|
+
· **名字缺失 ⇒ 致命**:目标模块可导入、但其中没有这个名字 ⇒ 抛
|
|
47
|
+
``GenerationError``(点名子模块与名字 + 「请重启常驻 MCP 进程」指引)。
|
|
48
|
+
· **包内子模块缺失 ⇒ 可选缺失(非致命)**:`from . import x` / `from .pkg import x`
|
|
49
|
+
里 x 其实是**子模块名**而该子模块不在盘面(例:`whitebox_kb/aeis_core/api.py`
|
|
50
|
+
里 `from .knowledge import ingest_text`——该目录的 docstring 明写「不带走
|
|
51
|
+
vision/body/world_model 等,它们在 core.py 里全部是方法内惰性导入,**缺失即
|
|
52
|
+
自动降级**」)。这类是**设计上的可选降级面**,不是换代证据;若判致命,服务
|
|
53
|
+
将永远起不来(实测 25 项)。判别器=目标模块是否为包(有 ``__path__``):
|
|
54
|
+
是包才允许用「名字其实是子模块」解释。
|
|
55
|
+
· **目标模块本身导入不进 ⇒ 可选缺失(非致命)**:外部可选模块未装(同上语义)。
|
|
56
|
+
两条非致命项都**如实记进返回 dict 的 ``unavailable``**,不静默丢弃。
|
|
57
|
+
4. 结果**按目标模块为单位缓存**(同一进程只扫一次);扫描与校验**不写任何文件、
|
|
58
|
+
不联网、不做任何时间/随机依赖**(文件按路径排序后处理)。
|
|
59
|
+
|
|
60
|
+
不适用条件(negative)
|
|
61
|
+
----------------------
|
|
62
|
+
· 不适用于**已经换代**的判定:本模块只回答「盘面 vs 本进程启动代是否同形」,
|
|
63
|
+
不回答「哪一代更正确」;跨代处置一律是「重启常驻进程」,不做热重载
|
|
64
|
+
(热重载会与函数内延迟导入的避环设计冲突,且会把已加载模块的类身份打散)。
|
|
65
|
+
· ``verify_delegations()`` 不构成「所有延迟导入都能跑通」的完备证明:它只校验
|
|
66
|
+
**相对导入的名字存在性**(不执行被导入者、不校验签名/行为),也不覆盖
|
|
67
|
+
`import x.y` 绝对导入与 `importlib.import_module("...")` 字符串形态。
|
|
68
|
+
· 代校验只在**进程边界**有效:同一进程内改盘不会自动重载,这正是要重启的原因。
|
|
69
|
+
"""
|
|
70
|
+
from __future__ import annotations
|
|
71
|
+
|
|
72
|
+
import ast
|
|
73
|
+
import hashlib
|
|
74
|
+
import importlib
|
|
75
|
+
import os
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"GenerationError",
|
|
79
|
+
"code_root",
|
|
80
|
+
"fingerprint",
|
|
81
|
+
"STARTUP_FINGERPRINT",
|
|
82
|
+
"is_stale",
|
|
83
|
+
"report",
|
|
84
|
+
"verify_delegations",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
# 包名(本模块在 md_cg 包内;用 __package__ 而不是写字面量,便于副本/改名场景)
|
|
88
|
+
_PACKAGE = __package__ or "md_cg"
|
|
89
|
+
|
|
90
|
+
# 指纹与代际展示用前缀长度(12 位十六进制足够区分代际,且不把 info 响应撑大)
|
|
91
|
+
_GEN_PREFIX = 12
|
|
92
|
+
|
|
93
|
+
# 单进程内的延迟导入校验缓存(键 = 本模块 code_root(),值 = verify_delegations 返回 dict)
|
|
94
|
+
_VERIFY_CACHE: dict = {}
|
|
95
|
+
|
|
96
|
+
# 扫描时跳过的目录名(编译缓存不是源码;跳掉才不会被 .pyc 干扰指纹)
|
|
97
|
+
_SKIP_DIRS = {"__pycache__"}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class GenerationError(RuntimeError):
|
|
101
|
+
"""本进程与磁盘代码不同代(跨代混合快照)。
|
|
102
|
+
|
|
103
|
+
抛出时机:盘面上某处「函数内相对导入」所要的名字,在**本进程实际持有的**
|
|
104
|
+
目标模块里不存在(而该模块又不是包、无法用「子模块名」解释)。此时该延迟
|
|
105
|
+
导入一旦被执行就是 ImportError,且盘面自洽(新解释器导入正常)——唯一处置
|
|
106
|
+
是把**长驻进程重启**,让它重新只读一代代码。
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# 生效条件:无入参,恒返回本文件(md_cg/generation.py)所在目录的绝对路径,即盘面 md_cg 包目录。
|
|
111
|
+
def code_root() -> str:
|
|
112
|
+
"""盘面 md_cg 包目录(本文件所在目录)的绝对路径。"""
|
|
113
|
+
return os.path.dirname(os.path.abspath(__file__))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# 生效条件:root 为空/缺省时取 code_root();遍历其下 **.py** 源文件(跳过 __pycache__ 目录与 test_*.py),按相对路径排序后逐字节喂 sha256。
|
|
117
|
+
def _iter_source_files(root: str):
|
|
118
|
+
"""产出 (相对路径 posix 形态, 绝对路径),顺序=相对路径排序(确定性)。"""
|
|
119
|
+
found = []
|
|
120
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
121
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
|
122
|
+
for name in filenames:
|
|
123
|
+
if not name.endswith(".py"):
|
|
124
|
+
continue
|
|
125
|
+
# test_*.py 排除:守卫/测试文件不是长驻服务要惰性导入的运行面,
|
|
126
|
+
# 且它们处于持续改写中——纳入指纹会让「跑一次测试」就假报换代。
|
|
127
|
+
if name.startswith("test_"):
|
|
128
|
+
continue
|
|
129
|
+
full = os.path.join(dirpath, name)
|
|
130
|
+
rel = os.path.relpath(full, root).replace(os.sep, "/")
|
|
131
|
+
found.append((rel, full))
|
|
132
|
+
found.sort(key=lambda it: it[0])
|
|
133
|
+
return found
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# 生效条件:无入参(root 语义见 _iter_source_files);对每个源文件喂 (相对路径 UTF-8 字节 + NUL + 文件全部字节 + NUL),返回 sha256 十六进制串;不写任何文件。
|
|
137
|
+
def fingerprint() -> str:
|
|
138
|
+
"""盘面 md_cg 代码的**内容**指纹(sha256 十六进制,全 64 位)。
|
|
139
|
+
|
|
140
|
+
只用(相对路径, 文件字节)——不用 mtime/size:打包/解压/校验回写造成的
|
|
141
|
+
「无改动重写」会变 mtime 而不变内容,用 mtime 会把同代误报成换代。
|
|
142
|
+
"""
|
|
143
|
+
root = code_root()
|
|
144
|
+
digest = hashlib.sha256()
|
|
145
|
+
for rel, full in _iter_source_files(root):
|
|
146
|
+
digest.update(rel.encode("utf-8"))
|
|
147
|
+
digest.update(b"\x00")
|
|
148
|
+
with open(full, "rb") as fh:
|
|
149
|
+
digest.update(fh.read())
|
|
150
|
+
digest.update(b"\x00")
|
|
151
|
+
return digest.hexdigest()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# 本进程的「代」=本模块被导入那一刻的盘面指纹。导入后再改盘不会更新它——
|
|
155
|
+
# 这正是「长驻进程停在启动代」的机械表达(需要新代 ⇒ 重启进程)。
|
|
156
|
+
STARTUP_FINGERPRINT = fingerprint()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# 生效条件:无入参;现算 fingerprint() 与 STARTUP_FINGERPRINT 逐字符比较,不等即 True(盘面已换代,本进程仍持启动代)。
|
|
160
|
+
def is_stale() -> bool:
|
|
161
|
+
"""现算指纹 != 启动指纹 ⇒ 本进程与磁盘代码不同代。"""
|
|
162
|
+
return fingerprint() != STARTUP_FINGERPRINT
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# 生效条件:无入参;恒返回含 code_generation/disk_generation/stale_on_disk/restart_required/hint 五键的 dict;盘面读取失败(OSError)时按「已换代」保守上报并写进 hint,不抛异常(本函数挂在 cg(op=info) 上,诊断面不得把 info 拖崩)。
|
|
166
|
+
def report() -> dict:
|
|
167
|
+
"""代校验自报(供 ``cg(op=info)`` 纯增量挂载)。
|
|
168
|
+
|
|
169
|
+
五键语义:
|
|
170
|
+
· ``code_generation`` —— 启动代指纹前 12 位(本进程实际持有的一代)
|
|
171
|
+
· ``disk_generation`` —— 现算盘面指纹前 12 位(此刻盘面上的一代)
|
|
172
|
+
· ``stale_on_disk`` —— 两者是否不同(True ⇒ 磁盘代码已换代)
|
|
173
|
+
· ``restart_required`` —— 是否需要重启本进程(与 stale_on_disk 同真值)
|
|
174
|
+
· ``hint`` —— 中文一句话处置指引(可执行,不猜测)
|
|
175
|
+
"""
|
|
176
|
+
startup = STARTUP_FINGERPRINT[:_GEN_PREFIX]
|
|
177
|
+
try:
|
|
178
|
+
disk_full = fingerprint()
|
|
179
|
+
stale = disk_full != STARTUP_FINGERPRINT
|
|
180
|
+
disk = disk_full[:_GEN_PREFIX]
|
|
181
|
+
except OSError as exc: # 盘面读不到:保守按「已换代」报
|
|
182
|
+
stale = True
|
|
183
|
+
disk = None
|
|
184
|
+
read_error = repr(exc)
|
|
185
|
+
else:
|
|
186
|
+
read_error = None
|
|
187
|
+
if stale:
|
|
188
|
+
hint = ("检测到磁盘上的 md_cg 代码已换代(本进程仍是启动时载入的那一代):"
|
|
189
|
+
"请重启常驻 MCP 进程(重连/重启 mcp_server 即可,记忆数据无损)"
|
|
190
|
+
"后重试。")
|
|
191
|
+
if read_error:
|
|
192
|
+
hint = ("无法读取盘面代码指纹(%s)——按已换代保守上报:请重启常驻 "
|
|
193
|
+
"MCP 进程(重连/重启 mcp_server 即可,记忆数据无损)后重试。"
|
|
194
|
+
% read_error)
|
|
195
|
+
else:
|
|
196
|
+
hint = "本进程与磁盘 md_cg 代码同代,暂不需要重启常驻 MCP 进程。"
|
|
197
|
+
return {
|
|
198
|
+
"code_generation": startup,
|
|
199
|
+
"disk_generation": disk,
|
|
200
|
+
"stale_on_disk": bool(stale),
|
|
201
|
+
"restart_required": bool(stale),
|
|
202
|
+
"hint": hint,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ---------------------------------------------------------------- 延迟导入校验
|
|
207
|
+
# 生效条件:path 为本包(root)内某 .py 的绝对路径;返回其在包内的点分模块名,__init__.py 归并为包名本身(例:<root>/a/b/__init__.py → "md_cg.a.b")。
|
|
208
|
+
def _module_name_of(path: str, root: str) -> str:
|
|
209
|
+
"""源文件绝对路径 → 包内模块名(`__init__.py` 归并为包名本身)。"""
|
|
210
|
+
rel = os.path.relpath(path, os.path.dirname(root)).replace(os.sep, "/")
|
|
211
|
+
if rel.endswith(".py"):
|
|
212
|
+
rel = rel[:-3]
|
|
213
|
+
if rel.endswith("/__init__"):
|
|
214
|
+
rel = rel[: -len("/__init__")]
|
|
215
|
+
return rel.replace("/", ".")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# 生效条件:file_module 为引入语句所在文件的点分模块名,level ≥ 1 为相对层级(from 后点的个数),node_module 为语句里的模块部分(`from . import x` 形态为 None);返回解析后的绝对目标模块名(不校验其是否可导入,空包名时为 "")。
|
|
219
|
+
def _resolve_base(file_module: str, level: int, node_module):
|
|
220
|
+
"""把 `from <level 个点><node_module> import ...` 解析成绝对目标模块名。
|
|
221
|
+
|
|
222
|
+
level=1 指当前包;level=2 指上一级,依此类推(相对导入的层数语义)。
|
|
223
|
+
node_module 为 None 时(`from . import x` 形态)目标即所在包的**包名本身**。
|
|
224
|
+
"""
|
|
225
|
+
parts = file_module.split(".")
|
|
226
|
+
pkg = parts[:-1] # 文件所在包
|
|
227
|
+
up = level - 1
|
|
228
|
+
if up:
|
|
229
|
+
pkg = pkg[: len(pkg) - up] if up <= len(pkg) else []
|
|
230
|
+
if node_module:
|
|
231
|
+
return ".".join(pkg + [node_module]) if pkg else node_module
|
|
232
|
+
return ".".join(pkg)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# 生效条件:以 (文件名→模块名, 相对路径) 构造后 visit(ast);items 里为**函数体深度 >0** 的相对导入项(模块顶层与类体顶层不计),每项=(目标模块名, 被导入名, 相对路径, 行号, 是否 `from . import x` 形态)。
|
|
236
|
+
class _DelegationVisitor(ast.NodeVisitor):
|
|
237
|
+
"""收集**函数体内**(depth>0)的跨模块相对导入。
|
|
238
|
+
|
|
239
|
+
意义:模块顶层的 import 在导入期就会执行(换代必然同步);写在函数里的
|
|
240
|
+
导入要到该行被跑到才执行——正是它制造了「新代代码用旧代模块」的窗口。
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def __init__(self, file_module: str, rel_path: str):
|
|
244
|
+
self.file_module = file_module
|
|
245
|
+
self.rel_path = rel_path
|
|
246
|
+
self.depth = 0
|
|
247
|
+
self.items = [] # (target_base, name, rel_path, lineno, module_is_none)
|
|
248
|
+
|
|
249
|
+
def _visit_function(self, node):
|
|
250
|
+
self.depth += 1
|
|
251
|
+
self.generic_visit(node)
|
|
252
|
+
self.depth -= 1
|
|
253
|
+
|
|
254
|
+
def visit_FunctionDef(self, node): # noqa: N802 —— ast 回调名
|
|
255
|
+
self._visit_function(node)
|
|
256
|
+
|
|
257
|
+
def visit_AsyncFunctionDef(self, node): # noqa: N802
|
|
258
|
+
self._visit_function(node)
|
|
259
|
+
|
|
260
|
+
def visit_ImportFrom(self, node): # noqa: N802
|
|
261
|
+
if self.depth > 0 and node.level and node.level > 0:
|
|
262
|
+
base = _resolve_base(self.file_module, node.level, node.module)
|
|
263
|
+
for alias in node.names:
|
|
264
|
+
self.items.append((base, alias.name, self.rel_path,
|
|
265
|
+
node.lineno, node.module is None))
|
|
266
|
+
self.generic_visit(node)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# 生效条件:无入参;AST 扫盘面 md_cg/**/*.py(与 fingerprint 同排除集:__pycache__ 与 test_*.py)里所有函数内相对导入,对每一项「导入目标模块 → getattr 取名字」,名字缺失即抛 GenerationError(点名子模块与名字 + 重启指引);包内子模块缺失与目标模块导入不进这两类记入 unavailable(非致命);结果按 code_root 缓存,同一进程只扫一次;不写文件、不联网。
|
|
270
|
+
def verify_delegations() -> dict:
|
|
271
|
+
"""延迟导入自检器:盘面每个「函数内相对导入」是否能在当前模块面上解析。
|
|
272
|
+
|
|
273
|
+
返回 dict(键与语义稳定):
|
|
274
|
+
· ``ok`` —— 无致命不一致(True/False)
|
|
275
|
+
· ``scanned_files`` —— 参与扫描的源文件数
|
|
276
|
+
· ``delegations`` —— 扫到的函数内相对导入**项数**(一个 import 语句可多项)
|
|
277
|
+
· ``target_modules`` —— 去重后的目标模块数
|
|
278
|
+
· ``resolved`` —— 去重后成功解析的 (目标模块, 名字) 数
|
|
279
|
+
· ``unavailable`` —— 非致命项列表:[{target, name, rel_path, line, reason}]
|
|
280
|
+
· ``mismatches`` —— 致命项列表:[{target, name, rel_path, line}]
|
|
281
|
+
致命项非空时抛 ``GenerationError``(消息点名子模块与名字)。
|
|
282
|
+
同一进程内只扫一次(缓存),缓存键为 ``code_root()``。
|
|
283
|
+
|
|
284
|
+
文件集与 ``fingerprint()`` 同口径(跳过 ``__pycache__`` 与 ``test_*.py``):
|
|
285
|
+
长驻服务要惰性导入的是**运行面**,`test_*.py` 不属运行面(且处于持续改写中)。
|
|
286
|
+
注意本函数**不写任何文件**(不建缓存文件),缓存只在内存。
|
|
287
|
+
|
|
288
|
+
另:扫描期 `ast.parse` 失败(源码语法坏)会**原样抛出** `SyntaxError`——盘面
|
|
289
|
+
语法坏本就该 fail-closed(由调用方按启动闸处置),此处不吞成「可用」。
|
|
290
|
+
"""
|
|
291
|
+
root = code_root()
|
|
292
|
+
cache_key = root
|
|
293
|
+
if cache_key in _VERIFY_CACHE:
|
|
294
|
+
return _VERIFY_CACHE[cache_key]
|
|
295
|
+
|
|
296
|
+
items = []
|
|
297
|
+
files = _iter_source_files(root)
|
|
298
|
+
for rel, full in files:
|
|
299
|
+
with open(full, encoding="utf-8") as fh:
|
|
300
|
+
src = fh.read()
|
|
301
|
+
visitor = _DelegationVisitor(_module_name_of(full, root), rel)
|
|
302
|
+
visitor.visit(ast.parse(src, full))
|
|
303
|
+
items.extend(visitor.items)
|
|
304
|
+
|
|
305
|
+
# 去重(同一 (目标模块, 名字) 在多处延迟导入只需查一次;首次出现的引用位置留痕)
|
|
306
|
+
uniq: dict = {}
|
|
307
|
+
for base, name, rel, line, module_is_none in items:
|
|
308
|
+
uniq.setdefault((base, name), (base, name, rel, line, module_is_none))
|
|
309
|
+
|
|
310
|
+
resolved = 0
|
|
311
|
+
unavailable = []
|
|
312
|
+
mismatches = []
|
|
313
|
+
for (base, name), (_b, _n, rel, line, module_is_none) in sorted(uniq.items()):
|
|
314
|
+
try:
|
|
315
|
+
# 已加载则取内存实例(跨代判据;不是重读盘面文件)
|
|
316
|
+
target = importlib.import_module(base)
|
|
317
|
+
except ModuleNotFoundError as exc:
|
|
318
|
+
unavailable.append({"target": base, "name": name, "rel_path": rel,
|
|
319
|
+
"line": line, "reason": "目标模块不在盘面:%s" % exc})
|
|
320
|
+
continue
|
|
321
|
+
except Exception as exc: # noqa: BLE001 —— 导入期异常归入非致命并如实登记
|
|
322
|
+
unavailable.append({"target": base, "name": name, "rel_path": rel,
|
|
323
|
+
"line": line,
|
|
324
|
+
"reason": "目标模块导入失败(%s):%s"
|
|
325
|
+
% (type(exc).__name__, exc)})
|
|
326
|
+
continue
|
|
327
|
+
if hasattr(target, name):
|
|
328
|
+
resolved += 1
|
|
329
|
+
continue
|
|
330
|
+
if hasattr(target, "__path__"): # 目标模块是包 ⇒ 名字可能是子模块
|
|
331
|
+
try:
|
|
332
|
+
importlib.import_module("%s.%s" % (base, name))
|
|
333
|
+
resolved += 1
|
|
334
|
+
continue
|
|
335
|
+
except ModuleNotFoundError:
|
|
336
|
+
unavailable.append({
|
|
337
|
+
"target": base, "name": name, "rel_path": rel, "line": line,
|
|
338
|
+
"reason": "包内子模块不存在(可选降级面,非换代证据)"})
|
|
339
|
+
continue
|
|
340
|
+
except Exception as exc: # noqa: BLE001
|
|
341
|
+
unavailable.append({
|
|
342
|
+
"target": base, "name": name, "rel_path": rel, "line": line,
|
|
343
|
+
"reason": "子模块导入失败(%s):%s" % (type(exc).__name__, exc)})
|
|
344
|
+
continue
|
|
345
|
+
mismatches.append({"target": base, "name": name, "rel_path": rel,
|
|
346
|
+
"line": line, "module_is_none_form": module_is_none})
|
|
347
|
+
|
|
348
|
+
result = {
|
|
349
|
+
"ok": not mismatches,
|
|
350
|
+
"scanned_files": len(files),
|
|
351
|
+
"delegations": len(items),
|
|
352
|
+
"target_modules": len({base for base, _n in uniq}),
|
|
353
|
+
"resolved": resolved,
|
|
354
|
+
"unavailable": unavailable,
|
|
355
|
+
"mismatches": mismatches,
|
|
356
|
+
}
|
|
357
|
+
_VERIFY_CACHE[cache_key] = result
|
|
358
|
+
|
|
359
|
+
if mismatches:
|
|
360
|
+
raise GenerationError(_mismatch_message(mismatches))
|
|
361
|
+
return result
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# 生效条件:mismatches 非空(每项含 target/name/rel_path/line);返回多行中文消息——点名首项的子模块与名字、按「启动代 vs 盘面代」是否相等分流陈述,并给出含「重启常驻 MCP 进程」的可执行指引。
|
|
365
|
+
def _mismatch_message(mismatches) -> str:
|
|
366
|
+
"""跨代不一致的异常消息(点名子模块与名字 + 可执行指引)。
|
|
367
|
+
|
|
368
|
+
两代关系**分开陈述**,不套一句话:启动代≠盘面代 ⇒ 真·跨代(升级后未重启,
|
|
369
|
+
这正是本模块要防的那一种);启动代==盘面代 ⇒ 盘面自身不自洽(半升级/改坏/
|
|
370
|
+
删了定义但引用方还在)。两种都要求重启常驻进程,但只有前者能说「不同代」——
|
|
371
|
+
把后者也说成「不同代」是假读(实测出现过:两侧前缀都是 1afbcba9935a 却印着
|
|
372
|
+
「≠」),故此处按事实分流。
|
|
373
|
+
"""
|
|
374
|
+
head = mismatches[0]
|
|
375
|
+
startup = STARTUP_FINGERPRINT[:_GEN_PREFIX]
|
|
376
|
+
disk = _disk_prefix()
|
|
377
|
+
lines = [
|
|
378
|
+
"跨代混合快照:盘面上的函数内延迟导入在本进程的模块面上解析不了。",
|
|
379
|
+
" · 目标子模块:%s" % head["target"],
|
|
380
|
+
" · 找不到的名字:%s" % head["name"],
|
|
381
|
+
" · 引用处:%s:%s(函数内延迟导入)" % (head["rel_path"], head["line"]),
|
|
382
|
+
]
|
|
383
|
+
if startup != disk:
|
|
384
|
+
lines.append(" · 本进程启动代 %s ≠ 当前盘面代 %s" % (startup, disk))
|
|
385
|
+
else:
|
|
386
|
+
lines.append(" · 启动代与当前盘面代同为 %s:本次不一致不是「换代」本身,"
|
|
387
|
+
"而是盘面代码在该模块面上不自洽(半升级/被改坏/定义被删但"
|
|
388
|
+
"引用方仍在)" % startup)
|
|
389
|
+
if len(mismatches) > 1:
|
|
390
|
+
lines.append(" · 同类不一致共 %d 项:%s"
|
|
391
|
+
% (len(mismatches),
|
|
392
|
+
"、".join("%s.%s" % (m["target"], m["name"])
|
|
393
|
+
for m in mismatches[1:9])))
|
|
394
|
+
if startup != disk:
|
|
395
|
+
lines.append("本进程与磁盘代码不同代,请重启常驻 MCP 进程"
|
|
396
|
+
"(重连/重启 mcp_server 即可,记忆数据无损)后重试。")
|
|
397
|
+
else:
|
|
398
|
+
lines.append("请重新完整部署/安装本包(或修复该名字),并重启常驻 MCP 进程"
|
|
399
|
+
"(重连/重启 mcp_server 即可,记忆数据无损)后重试。")
|
|
400
|
+
return "\n".join(lines)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# 生效条件:无入参;现算 fingerprint() 前 12 位返回(盘面读不到时返回 "?" 而不抛——仅供异常消息展示)。
|
|
404
|
+
def _disk_prefix():
|
|
405
|
+
"""现算指纹前 12 位(读不到时返回 '?')——仅供异常消息展示。"""
|
|
406
|
+
try:
|
|
407
|
+
return fingerprint()[:_GEN_PREFIX]
|
|
408
|
+
except OSError:
|
|
409
|
+
return "?"
|
package/md_cg/hotcache.py
CHANGED
|
@@ -60,9 +60,12 @@ _BYPASS_EXTRA = ("query_expand",)
|
|
|
60
60
|
#: 影响检索结果的进程级开关(新增一个就登记一个——同 _KEYED_EXTRA 的纪律)。
|
|
61
61
|
#: MDCG_CN_GRAMS(v9 N78):mdcg.cn_recall_grams 每调用读 env(="0" 回退旧行为),
|
|
62
62
|
#: 改变 expand_query_terms 召回词集——漏登即运行期翻转后同 query 命中另一口径缓存。
|
|
63
|
+
#: MDCG_CHAIN_TYPES / MDCG_TEMPORAL_GAMMA(P3 图检索路,2026-10-01):前者改「因果路
|
|
64
|
+
#: 沿哪些边类型扩散」(改候选集合),后者改「时间路的衰减率」(改路内名次)——两者都
|
|
65
|
+
#: 直接改变融合结果,同属「漏登即跨口径串味」的一类。
|
|
63
66
|
_ENV_SWITCHES = ("MDCG_SEMANTIC", "MDCG_EN_ATOMS", "MDCG_UNIFY_QUERY",
|
|
64
67
|
"MDCG_RETRIEVAL_PIPELINE", "MDCG_S7_FRESHNESS",
|
|
65
|
-
"MDCG_CN_GRAMS")
|
|
68
|
+
"MDCG_CN_GRAMS", "MDCG_CHAIN_TYPES", "MDCG_TEMPORAL_GAMMA")
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
# 生效条件:无必需形参;返回 _ENV_SWITCHES 中每个变量名的 (名, 当前取值或 None) 元组(稳定、可哈希,随 query 缓存键一起参与比对)。
|
package/md_cg/identity.py
CHANGED
|
@@ -277,7 +277,7 @@ def infer_position(cg, subject_id):
|
|
|
277
277
|
tag = f"subject:{subject_id}"
|
|
278
278
|
nodes = cg.index.get("nodes") or {}
|
|
279
279
|
votes, n = {}, 0
|
|
280
|
-
for nid, e in nodes.items():
|
|
280
|
+
for nid, e in list(nodes.items()):
|
|
281
281
|
tg = set(e.get("tags") or [])
|
|
282
282
|
if tag not in tg or (tg & {TAG_ANCHOR, TAG_TRAIT}):
|
|
283
283
|
continue
|
|
@@ -313,7 +313,7 @@ def profile(cg, subject_id):
|
|
|
313
313
|
tag = f"subject:{subject_id}"
|
|
314
314
|
nodes = cg.index.get("nodes") or {}
|
|
315
315
|
anchors, traits = [], []
|
|
316
|
-
for nid, e in nodes.items():
|
|
316
|
+
for nid, e in list(nodes.items()):
|
|
317
317
|
tg = set(e.get("tags") or [])
|
|
318
318
|
if tag not in tg:
|
|
319
319
|
continue
|
|
@@ -342,7 +342,7 @@ def positions(cg, limit=0):
|
|
|
342
342
|
"""所有主体的位置效应分布(OS 视角:谁在记录/反思/验证/输出/维生)。"""
|
|
343
343
|
nodes = cg.index.get("nodes") or {}
|
|
344
344
|
counts = {}
|
|
345
|
-
for _nid_, e in nodes.items():
|
|
345
|
+
for _nid_, e in list(nodes.items()):
|
|
346
346
|
for t in (e.get("tags") or []):
|
|
347
347
|
if isinstance(t, str) and t.startswith("subject:"):
|
|
348
348
|
sid = t[len("subject:"):]
|
package/md_cg/insight.py
CHANGED
|
@@ -29,6 +29,7 @@ import math
|
|
|
29
29
|
import os
|
|
30
30
|
import time
|
|
31
31
|
|
|
32
|
+
from . import protect
|
|
32
33
|
from .fsutil import append_jsonl
|
|
33
34
|
|
|
34
35
|
INSIGHT_LOG = "_insight.jsonl"
|
|
@@ -78,7 +79,7 @@ def _append(cg, rec):
|
|
|
78
79
|
def _events(cg):
|
|
79
80
|
"""全部洞见事件节点 id(按 index 层标签粗筛,避免全量读盘)。"""
|
|
80
81
|
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
81
|
-
return [nid for nid, e in nodes.items()
|
|
82
|
+
return [nid for nid, e in list(nodes.items())
|
|
82
83
|
if TAG_EVENT in ((e or {}).get("tags") or [])]
|
|
83
84
|
|
|
84
85
|
|
|
@@ -232,7 +233,7 @@ def _normalize_evidence(evidence=None, v_types=None):
|
|
|
232
233
|
return out
|
|
233
234
|
|
|
234
235
|
|
|
235
|
-
# 生效条件:_read_event(cg,node_id) 取不到事件节点、或 verdict 经 str(verdict or "").strip().lower() 后非空且不是 verified/falsified 时抛 ValueError;verdict 为 None 或空白时按 v3 → v2 → v1 条数 ≥ V1_MIN_EVIDENCE 的顺序定 verified,有证据但不达门槛或无证据则保持 STATE_PENDING 并附 reason;显式 verdict 直接采信,verified 分支重要度保底 IMPORTANCE_FLOOR 并置保护位,falsified 分支打 TAG_FALSIFIED
|
|
236
|
+
# 生效条件:_read_event(cg,node_id) 取不到事件节点、或 verdict 经 str(verdict or "").strip().lower() 后非空且不是 verified/falsified 时抛 ValueError;verdict 为 None 或空白时按 v3 → v2 → v1 条数 ≥ V1_MIN_EVIDENCE 的顺序定 verified,有证据但不达门槛或无证据则保持 STATE_PENDING 并附 reason;显式 verdict 直接采信,verified 分支重要度保底 IMPORTANCE_FLOOR 并置保护位,falsified 分支打 TAG_FALSIFIED;裁决分支落盘前经 protect.guard_overwrite(层闸 + 保护闸),写盘后同步内存索引条目并**标脏**(cg._dirty[node_id]=ent,推进读缓存代际与索引增量日志)。
|
|
236
237
|
def verify(cg, node_id=None, evidence=None, v_types=None, verdict=None,
|
|
237
238
|
actor=None, note=""):
|
|
238
239
|
"""用 V1/V2/V3 外部证据裁决洞见事件。
|
|
@@ -243,6 +244,9 @@ def verify(cg, node_id=None, evidence=None, v_types=None, verdict=None,
|
|
|
243
244
|
· V1 可检索/被引用 需 **≥ V1_MIN_EVIDENCE 条** 才成立;
|
|
244
245
|
· 其余(无证据 / V1 不足)→ **保持 pending 并给出原因**,不判定。
|
|
245
246
|
verified 后:重要度保底 0.9(``IMPORTANCE_FLOOR``)并置保护位。
|
|
247
|
+
裁决分支落盘前过 ``protect.guard_overwrite``(层闸 + 保护闸),写盘后同步
|
|
248
|
+
内存索引条目并**标脏**(``cg._dirty[node_id]``,C-1)——否则读缓存默认开时
|
|
249
|
+
同进程「verify 后读」拿旧 insight_state/tags/importance。
|
|
246
250
|
"""
|
|
247
251
|
node = _read_event(cg, node_id)
|
|
248
252
|
if not node:
|
|
@@ -294,6 +298,14 @@ def verify(cg, node_id=None, evidence=None, v_types=None, verdict=None,
|
|
|
294
298
|
else:
|
|
295
299
|
tg.append(TAG_FALSIFIED)
|
|
296
300
|
fm["tags"] = tg
|
|
301
|
+
# N197(2026-09-28):本面是**既有节点的覆写**——原先直调 `cg._write_node`,
|
|
302
|
+
# 既无 `principal.require_layer_write` 亦无 `protect.guard_write`,与同一身份
|
|
303
|
+
# 对同层 `add` 的待遇相反(add 被 AccessDenied,这里照样落盘):越层腿
|
|
304
|
+
# (sustain layers=('self',) verify 他人的 contextual 事件)+ 保护腿
|
|
305
|
+
# (self/anchor/immutable 节点被无痕覆写、无快照无审计)都由这一处补闸收口。
|
|
306
|
+
# 层与敏感度取**本节点已读到的 frontmatter 真值**(不靠索引缺字段时的回落)。
|
|
307
|
+
protect.guard_overwrite(cg, node_id, layer=fm.get("layer"),
|
|
308
|
+
sensitivity=fm.get("sensitivity"), actor=actor)
|
|
297
309
|
cg._write_node(node_id, os.path.join(cg.root, node["path"]), fm,
|
|
298
310
|
node.get("content") or "")
|
|
299
311
|
ent = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id)
|
|
@@ -301,6 +313,15 @@ def verify(cg, node_id=None, evidence=None, v_types=None, verdict=None,
|
|
|
301
313
|
ent["importance"] = fm.get("importance")
|
|
302
314
|
ent["tags"] = list(tg)
|
|
303
315
|
ent["insight_state"] = final
|
|
316
|
+
# 标脏(N133 修复,对照先例 md_cg/mdcg.py 的 update_tags / verify 直写
|
|
317
|
+
# 分支 `self._dirty[node_id] = e`):只改内存 entry 时读缓存(默认开)
|
|
318
|
+
# 的 path_gen 不推进 ⇒ `_fresh` 继续判旧 fm 新鲜,同进程「verify 后读」
|
|
319
|
+
# 拿到旧 insight_state/tags/importance;标脏同时让本写进
|
|
320
|
+
# `_dirty → flush → _index_log` 重放。**不下沉进 `_write_node`**(该口
|
|
321
|
+
# 另有「只对账索引不落盘」的调用方,下沉会凭空产生写入代际与自重载)。
|
|
322
|
+
_dirty = getattr(cg, "_dirty", None)
|
|
323
|
+
if isinstance(_dirty, dict):
|
|
324
|
+
_dirty[node_id] = ent
|
|
304
325
|
_append(cg, {"action": "verify", "node_id": node_id, "state": final,
|
|
305
326
|
"decided": True, "reason": reason or "显式裁决",
|
|
306
327
|
"evidence": ev, "importance": fm.get("importance"),
|
|
@@ -417,7 +438,7 @@ def outlook(cg, window_days=None, sample_limit=8, recent_days=7, now=None):
|
|
|
417
438
|
imps, protected, no_neg, recent = [], 0, 0, 0
|
|
418
439
|
lo_recent = now - float(recent_days) * 86400.0
|
|
419
440
|
hi_imp_unprotected = []
|
|
420
|
-
for nid, e in nodes.items():
|
|
441
|
+
for nid, e in list(nodes.items()):
|
|
421
442
|
e = e or {}
|
|
422
443
|
layers[_layer_of(e)] = layers.get(_layer_of(e), 0) + 1
|
|
423
444
|
b = str(e.get("verification_basis") or "unset")
|