@furongjun1999/dsh-memory 0.4.7 → 0.4.8

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.
Files changed (172) hide show
  1. package/README.md +92 -37
  2. package/codebuddy/CODEBUDDY.md +195 -185
  3. package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -0
  4. package/docs/README.md +111 -0
  5. package/docs/discipline/harnesses.yaml +215 -152
  6. package/docs/discipline/templates/full.md.tmpl +60 -58
  7. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.md +127 -0
  8. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.png +0 -0
  9. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_/347/201/265/346/236/242_vs_dejavu_/347/273/237/344/270/200/350/257/204/345/210/206_v7.md +202 -0
  10. package/docs/experiments/m4-role-probe/census.py +86 -0
  11. package/docs/experiments/m4-role-probe/probe.py +89 -0
  12. package/docs/experiments/mapped_confidence/bench_mapped_conf.py +216 -0
  13. package/docs/experiments/mapped_confidence/post_check.py +75 -0
  14. package/docs/experiments/mapped_confidence/repro_thirdparty_tol.py +83 -0
  15. package/docs/experiments/mapped_confidence/result_mapped_conf.json +215 -0
  16. package/docs/{ → hive/}/350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +14 -2
  17. package/docs/{README → mdcg/README}/350/257/246/347/273/206/347/211/210_v0.4.5.md +9 -9
  18. package/docs/{ → mdcg/}/344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md +1 -1
  19. 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 +261 -0
  20. package/docs/{ → mdcg/}/345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +1 -1
  21. 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 +148 -0
  22. package/docs/{ → mdcg/}/346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +2 -2
  23. package/docs/{ → mdcg/}/347/201/265/346/236/242/344/270/211/345/261/202/346/213/206/345/210/206/350/247/204/345/210/222_v0.1.md +181 -181
  24. package/docs/{ → mdcg/}/347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +2 -2
  25. package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +1 -1
  26. 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 +4 -4
  27. package/docs/{ → mdcg/}/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_MdCGOS/344/270/216MCP/346/216/245/345/205/245_v0.1.md +2 -2
  28. package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -0
  29. package/docs/{ → swarm/}/350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +3 -3
  30. package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -0
  31. package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -0
  32. 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 +134 -0
  33. package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -0
  34. 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
  35. package/docs/{ → theory/}/347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +1 -1
  36. 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 +221 -0
  37. package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -0
  38. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/346/236/266/346/236/204/345/255/246/344/271/240_/345/244/226/351/203/250/347/220/206/350/256/272/345/257/271/347/205/247/344/270/216/350/267/257/347/272/277/344/272/244/346/216/245_20260916.md +98 -0
  39. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/347/245/236/347/273/217/347/275/221/347/273/234/346/236/266/346/236/204_/346/200/273/347/272/262/344/270/216/344/272/244/346/216/245_20260916.md +109 -0
  40. 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 +34 -6
  41. package/docs//347/231/275/347/256/261/345/214/226/347/272/262/351/242/206_/344/270/211/351/241/271/347/233/256/345/255/246/344/271/240/346/200/273/346/236/266/346/236/204/344/270/216Ghidra/344/272/244/346/216/245_20260916.md +104 -0
  42. package/docs//350/256/260/345/277/206/350/257/204/345/256/241/347/263/273/347/273/237_/347/253/213/351/241/271/350/256/276/350/256/241/344/270/216/346/226/275/345/267/245/344/272/244/346/216/245_20260915.md +183 -0
  43. package/dsh/README.md +1 -1
  44. package/lib/hooks.d.ts +5 -0
  45. package/lib/hooks.js +10 -1
  46. package/lib/lib/prompt_safety.d.ts +48 -0
  47. package/lib/lib/prompt_safety.js +62 -0
  48. package/md_cg/audit.py +76 -2
  49. package/md_cg/bench_membench.py +10 -5
  50. package/md_cg/bench_progressive.py +48 -9
  51. package/md_cg/branches.py +275 -0
  52. package/md_cg/ccgc.py +884 -0
  53. package/md_cg/conformance.py +662 -0
  54. package/md_cg/consistency.py +1 -1
  55. package/md_cg/corpus.py +1 -1
  56. package/md_cg/eval_common.py +6 -5
  57. package/md_cg/forgetting.py +11 -3
  58. package/md_cg/fsutil.py +63 -1
  59. package/md_cg/identity.py +2 -2
  60. package/md_cg/insight.py +2 -2
  61. package/md_cg/lifecycle.py +262 -0
  62. package/md_cg/mcp_server.py +547 -140
  63. package/md_cg/mdcg.py +206 -18
  64. package/md_cg/mdcos.py +512 -50
  65. package/md_cg/metacognition.py +2 -2
  66. package/md_cg/mreview/__init__.py +25 -0
  67. package/md_cg/mreview/__main__.py +107 -0
  68. package/md_cg/mreview/bundle.py +170 -0
  69. package/md_cg/mreview/candidates.py +253 -0
  70. package/md_cg/mreview/govern.py +674 -0
  71. package/md_cg/mreview/locate.py +905 -0
  72. package/md_cg/mreview/pipeline.py +701 -0
  73. package/md_cg/mreview/rules/duplication.json +21 -0
  74. package/md_cg/mreview/rules/field_coverage.json +54 -0
  75. package/md_cg/mreview/rules/source_license.json +21 -0
  76. package/md_cg/mreview/rules/template_flow.json +21 -0
  77. package/md_cg/mreview/ruleset.py +238 -0
  78. package/md_cg/nodefile.py +38 -0
  79. package/md_cg/predict.py +24 -6
  80. package/md_cg/protect.py +4 -4
  81. package/md_cg/refindex.py +41 -2
  82. package/md_cg/scrub.py +5 -1
  83. package/md_cg/self_state.py +1 -1
  84. package/md_cg/semantic/en_normalizer.py +85 -25
  85. package/md_cg/sustain.py +18 -0
  86. package/md_cg/tasks.py +447 -0
  87. package/md_cg/test_action_derive.py +203 -0
  88. package/md_cg/test_audit_rotate.py +270 -0
  89. package/md_cg/test_branches.py +249 -0
  90. package/md_cg/test_ccg_perturb.py +1 -1
  91. package/md_cg/test_ccgc.py +423 -0
  92. package/md_cg/test_conformance.py +343 -0
  93. package/md_cg/test_en_pipeline.py +24 -0
  94. package/md_cg/test_health_scale.py +173 -0
  95. package/md_cg/test_identity_attribution.py +6 -0
  96. package/md_cg/test_index_durability.py +224 -0
  97. package/md_cg/test_lifecycle.py +309 -0
  98. package/md_cg/test_mr_m1.py +679 -0
  99. package/md_cg/test_mr_m2.py +587 -0
  100. package/md_cg/test_mr_m3.py +703 -0
  101. package/md_cg/test_mr_m4.py +485 -0
  102. package/md_cg/test_p1.py +1 -1
  103. package/md_cg/test_p11_consistency.py +1 -1
  104. package/md_cg/test_p12_metacognition.py +2 -2
  105. package/md_cg/test_p16_self_state.py +1 -1
  106. package/md_cg/test_p26_refindex.py +1 -1
  107. package/md_cg/test_p27_docindex.py +22 -2
  108. package/md_cg/test_p28_refcheck.py +1 -1
  109. package/md_cg/test_p29_session_ingest_export.py +1 -1
  110. package/md_cg/test_p2_mcp.py +7 -1
  111. package/md_cg/test_p38_contextualize.py +1 -1
  112. package/md_cg/test_p39_vision_evidence.py +1 -1
  113. package/md_cg/test_p40_refine_worklist.py +1 -1
  114. package/md_cg/test_p41_evolve_patrol.py +1 -1
  115. package/md_cg/test_p42_provenance.py +1 -1
  116. package/md_cg/test_p43_pooling.py +41 -13
  117. package/md_cg/test_read_clip.py +137 -0
  118. package/md_cg/test_review_conformance.py +310 -0
  119. package/md_cg/test_tasks.py +409 -0
  120. package/md_cg/test_tool_face.py +189 -0
  121. package/md_cg/test_twophase.py +286 -0
  122. package/md_cg/test_writepipe.py +210 -0
  123. package/md_cg/theory.py +1 -1
  124. package/md_cg/tokens.py +95 -5
  125. package/md_cg/tool_face.py +250 -0
  126. package/md_cg/twophase.py +221 -0
  127. package/md_cg/units.py +546 -0
  128. package/md_cg/vision_evidence.py +1 -1
  129. package/md_cg/weights.py +1 -1
  130. package/md_cg/whitebox.py +1 -1
  131. package/md_cg/whitebox_kb/data/verify_cache.json +28 -0
  132. package/md_cg/whitebox_kb/data/verify_savings.jsonl +46 -0
  133. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  134. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  135. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
  136. package/md_cg/writelimit.py +19 -3
  137. package/md_cg/writepipe.py +372 -0
  138. package/package.json +1 -1
  139. package/src/hooks.ts +10 -1
  140. package/src/lib/prompt_safety.ts +62 -0
  141. package/zcode/AGENTS.md +195 -185
  142. package/docs//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +0 -221
  143. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md" +0 -0
  144. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md" +0 -0
  145. /package/docs/{ → eval/}/345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md" +0 -0
  146. /package/docs/{ → eval/}/346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md" +0 -0
  147. /package/docs/{guardrail-charter.md → mdcg/guardrail-charter.md} +0 -0
  148. /package/docs/{lingshu_tutorial.html → mdcg/lingshu_tutorial.html} +0 -0
  149. /package/docs/{memory-assessment.html → mdcg/memory-assessment.html} +0 -0
  150. /package/docs/{memory_score.html → mdcg/memory_score.html} +0 -0
  151. /package/docs/{memory_score.png → mdcg/memory_score.png} +0 -0
  152. /package/docs/{release_v0.3.0.md → mdcg/release_v0.3.0.md} +0 -0
  153. /package/docs/{release_v0.4.5.md → mdcg/release_v0.4.5.md} +0 -0
  154. /package/docs/{tool_table_v0.3.0.md → mdcg/tool_table_v0.3.0.md} +0 -0
  155. /package/docs/{ → mdcg/}/344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md" +0 -0
  156. /package/docs/{ → mdcg/}/347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md" +0 -0
  157. /package/docs/{ → mdcg/}/347/201/265/346/236/242MCP/345/267/245/345/205/267/346/200/273/350/241/250_v3.4.md" +0 -0
  158. /package/docs/{ → mdcg/}/347/201/265/346/236/242_/350/207/252/346/210/221/345/261/202/345/256/232/344/271/211.md" +0 -0
  159. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_MD/347/233/256/345/275/225/346/226/271/346/241/210_v0.1.md" +0 -0
  160. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md" +0 -0
  161. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276/344/275/234/344/270/272/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_/350/257/204/344/274/260/344/270/216/350/267/257/347/272/277_v0.1.md" +0 -0
  162. /package/docs/{ → plans/}/345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md" +0 -0
  163. /package/docs/{ → plans/}/347/237/245/350/257/206/345/233/276/350/260/261/351/241/271/347/233/256_/344/272/244/346/216/245/346/226/207/346/241/243_20260914.md" +0 -0
  164. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/344/275/277/347/224/250/346/212/245/345/221/212_20260913.md" +0 -0
  165. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/345/212/237/350/203/275/350/257/264/346/230/216_v0.6.md" +0 -0
  166. /package/docs/{ → swarm/}/350/234/202/347/276/244/347/233/262/345/214/272/346/240/207/350/256/260_v1.0.md" +0 -0
  167. /package/docs/{ → theory/}/344/277/241/346/201/257/345/267/256/344/270/272/344/273/200/344/271/210/345/277/205/347/204/266/345/255/230/345/234/250/344/270/224/350/207/252/347/204/266/346/211/251/345/244/247.md" +0 -0
  168. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/345/205/254/347/220/206/345/214/226/345/237/272/347/237/263.md" +0 -0
  169. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/350/256/244/347/237/245/350/277/207/347/250/213.md" +0 -0
  170. /package/docs/{ → theory/}/346/231/272/350/203/275/350/256/2723.4.md" +0 -0
  171. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/346/230/257/344/273/200/344/271/210/357/274/237.md" +0 -0
  172. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/347/263/273/345/210/227/302/267/347/254/254/344/272/224/347/257/207/357/274/232/350/256/251AI/347/234/237/346/255/243/350/243/205/344/270/212/350/256/260/345/277/206.md" +0 -0
@@ -0,0 +1,662 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · 数据健康不变量断言集(Pi⑤)+ G1 类型空间正交性审计 + G3 unanalyzed 显式占位
3
+
4
+ 把 2026-09-15 的一次性审计改写为**周期可复跑**的断言集(混层比例阈值 / 重复度阈值 /
5
+ 字段覆盖率下限 / 闸门四态分布),挂 sustain 周期巡检;超阈值**只告警,不自动改数据**。
6
+
7
+ 三条纪律:
8
+ 1. **不变量 fail-closed**:类型空间封闭性 / 边键规范 / 边目标可解析 / 索引↔盘一致 /
9
+ 重复度不劣化——任一 FAIL = 库结构失真,须人工处置。
10
+ 2. **健康指标只告警**:混层比 / role 覆盖率 / evidence 覆盖率 / 分析覆盖率 /
11
+ 闸门四态 / 触达率——低于靶值 WARN,不阻断、不改数据。
12
+ 3. **缺数据源 → BLINDSPOT**(如实上报),绝不冒充 PASS。
13
+
14
+ G1(Ghidra 交接 §5.4):输出各类型空间的「声明值 / 实测值 / 未声明已用 / 声明未用 /
15
+ 跨空间重叠 / 方向口径分歧」——类型空间不封闭是写入侧漂移的前兆。
16
+
17
+ G3(同 §5.4):unanalyzed 采用**派生断言**(不新增字段、零写入),判据
18
+ `evidence_count==0 ∧ verification_basis 空 ∧ lifecycle_state 空`。
19
+ 不做字段级占位的理由:生命周期是单向降级轴,与分析覆盖正交,塞同一字段破坏正交性。
20
+
21
+ 用法:
22
+ python -m md_cg.conformance [--root R] [--json out.json] [--baseline b.json]
23
+ [--no-path-check] [--strict]
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import os
30
+ import sys
31
+ import time
32
+ from collections import Counter, deque
33
+
34
+ REPORT_VERSION = 1
35
+ INDEX_FILE = "_index.json"
36
+ ACCESS_LOG = "_access.log"
37
+ DECISION_LOG = os.path.join("hippocampus", "decisions.jsonl")
38
+ INBOX_LOG = os.path.join("hippocampus", "inbox.jsonl")
39
+ DEFAULT_ROOT = os.path.join(
40
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "mdcg")
41
+ #: _audit.jsonl 只读尾部窗口行数(append-only 全史可数十 MB,周期巡检不扫全史)
42
+ AUDIT_TAIL = 20000
43
+
44
+
45
+ def _default_root() -> str:
46
+ """根解析沿用本仓约定:env `MDCG_ROOT` > `data/paths.json` > 插件仓 `data/mdcg`。
47
+
48
+ 走 `datapath.mdcg_root()`(兜底纪律:与其它工具同源解析,不另立一套)。
49
+ """
50
+ try:
51
+ from .datapath import mdcg_root
52
+ got = mdcg_root()
53
+ if got:
54
+ return got
55
+ except Exception: # noqa: BLE001
56
+ pass
57
+ return DEFAULT_ROOT
58
+
59
+ # 阈值口径 = 9-15 审计基线(2026-09-16 复跑实测:混层 6468/11076=58.4% / role 2.13% /
60
+ # verification_basis 60.3% / evidence 0.10% / dir_mismatch 0 / 闸门 142 条) + M4 治理靶值。
61
+ # 比率型阈值只表达「离靶多远」,不阻断任何写入。
62
+ # 注:触达率随 _access.log 滚动追加而单调增长,9-15 快照 7.0% → 09-16 实测 8.7%,
63
+ # 差异来自时间而非口径;故**不纳入基线不劣化比对**(日志轮转会误报)。
64
+ THRESHOLDS = {
65
+ "stratum_ratio_max": 0.60,
66
+ "stratum_target": 0.30,
67
+ "role_coverage_min": 0.90,
68
+ "evidence_coverage_min": 0.50,
69
+ "analysis_coverage_min": 0.90,
70
+ "reach_ratio_min": 0.30,
71
+ "gate_sample_min": 20,
72
+ # 样本下限:低于此值比率型指标不可判(如仓内自建语料),一律 BLINDSPOT 不冒充 PASS
73
+ "min_nodes": 200,
74
+ }
75
+
76
+ #: 单一内容指纹的最大同组节点数(超过即「同模板批量写入」体征,须人工治理)
77
+ DUP_GROUP_MAX = 200
78
+
79
+ # 写侧规范边键。取证更正(2026-09-16):subgraph.children_index:122 / parents_index:162
80
+ # 已含 `or e.get("type")` 容错,**读侧不忽略** legacy `type`(`subgraph` 内 normalize_edge
81
+ # 的 docstring 仍写「会被静默忽略」,属文档滞后于代码,已登记 DOC_CODE_DRIFT)。
82
+ # 仍记为规范项:chain.edge_rel 与 subgraph 双兼容是本库私约,非 md_cg 读侧(Rust 引擎 /
83
+ # 外部消费者)可能只读 relation_type。
84
+ CANONICAL_EDGE_KEYS = ("relation_type", "relation")
85
+ LAYER_DIRS = ("knowledge", "contextual", "self", "structural", "anchor",
86
+ "rejected", "unresolved", "goals", "data", "trash")
87
+
88
+ # G1 静态登记:已取证的方向口径分歧(事实,非推断)
89
+ KNOWN_DIRECTION_CONFLICTS = [
90
+ {"literal": "hierarchical",
91
+ "conflict": "md_cg subgraph 视其为「本节点是子、target 是父」"
92
+ "(children_index:128);白箱源库语义为 source 是父 —— 同名反义,"
93
+ "直接迁移整树倒置",
94
+ "workaround": "migrate_wisdom_graph.SRC_REL_MAP 迁移时改写为 contains(语义等价)"},
95
+ ]
96
+ # G1 静态登记:已核对**无**方向分歧的项(防止「看起来像冲突」被反复误报)
97
+ VERIFIED_CONSISTENT = [
98
+ {"literal": "part_of / parent_of / contains",
99
+ "verified": "children_index:128-135 与 parents_index:171-178 逐字对称、方向取反,自洽"},
100
+ ]
101
+ # G1 已取证的**文档滞后于代码**实例(陈述与实现不符,非缺陷但会误导写侧)
102
+ DOC_CODE_DRIFT = [
103
+ {"where": "subgraph.normalize_edge docstring:96",
104
+ "claims": "「早期迁移器写的 type 会被静默忽略(subgraph 不兼容)」",
105
+ "fact": "children_index:122 / parents_index:162 已含 `or e.get(\"type\")` 容错 —— "
106
+ "读侧兼容,陈述已过期"},
107
+ ]
108
+
109
+ # G1 静态登记:新增类型的散落点(回答「新增类型要不要改引擎」)
110
+ NEW_TYPE_TOUCHPOINTS = {
111
+ "layer": ["mdcg.LAYERS", "mdcg.BUCKETED_LAYERS", "mdcg.NEG_MEMORY_MARKS",
112
+ "nodefile(fm 契约)", "routing(bucket)", "MCP 工具面描述"],
113
+ "edge_type": ["chain.EDGE_WEIGHTS", "subgraph(父子方向分支)",
114
+ "migrate_wisdom_graph.SRC_REL_MAP"],
115
+ }
116
+
117
+
118
+ def load_index(root: str) -> dict:
119
+ """读索引快照(唯一必需数据源);损坏即抛——断言集不建立在猜测上。"""
120
+ with open(os.path.join(root, INDEX_FILE), encoding="utf-8") as f:
121
+ raw = json.load(f)
122
+ nodes = raw.get("nodes") if isinstance(raw, dict) else None
123
+ if not isinstance(nodes, dict):
124
+ raise ValueError(f"索引格式异常(缺 nodes 字典): {root}/{INDEX_FILE}")
125
+ return nodes
126
+
127
+
128
+ def _read_jsonl(path: str) -> list:
129
+ if not os.path.exists(path):
130
+ return []
131
+ out = []
132
+ with open(path, encoding="utf-8", errors="replace") as f:
133
+ for line in f:
134
+ line = line.strip()
135
+ if not line:
136
+ continue
137
+ try:
138
+ out.append(json.loads(line))
139
+ except json.JSONDecodeError:
140
+ continue
141
+ return out
142
+
143
+
144
+ def _tag_prefixes(rec: dict) -> set:
145
+ return {str(t).split(":", 1)[0] for t in (rec.get("tags") or []) if ":" in str(t)}
146
+
147
+
148
+ def _basis_of(r: dict):
149
+ b = r.get("verification_basis")
150
+ if isinstance(b, (list, tuple)):
151
+ return [str(x) for x in b if x]
152
+ return [str(b)] if b else []
153
+
154
+
155
+ def enum_spaces() -> dict:
156
+ """各类型空间的声明值(模块真源取不到 → declared=None,报告标 BLINDSPOT)。"""
157
+ from .mdcg import LAYERS
158
+ spaces = {"layer": {"declared": sorted(LAYERS), "source": "mdcg.LAYERS"}}
159
+
160
+ def _grab(mod: str, attr: str, key: str):
161
+ try:
162
+ m = __import__(f"md_cg.{mod}", fromlist=[attr])
163
+ val = getattr(m, attr, None)
164
+ except Exception: # noqa: BLE001
165
+ val = None
166
+ spaces[key] = {"declared": sorted(val) if val else None,
167
+ "source": f"{mod}.{attr}"}
168
+
169
+ _grab("chain", "EDGE_WEIGHTS", "edge_type")
170
+ _grab("provenance", "RELATIONS", "derived_relation")
171
+ _grab("nodefile", "VERIFICATION_BASIS", "verification_basis")
172
+ _grab("lifecycle", "STATES", "lifecycle_state")
173
+ return spaces
174
+
175
+
176
+ # ---------------------------- 指标 ----------------------------
177
+
178
+ def _dup_metrics(nodes: dict) -> dict:
179
+ ch = Counter(str(r.get("content_hash")) for r in nodes.values())
180
+ groups = {h: c for h, c in ch.items() if c > 1 and h not in ("None", "")}
181
+ return {"groups": len(groups), "nodes": sum(groups.values()),
182
+ "largest": sorted(groups.values(), reverse=True)[:5]}
183
+
184
+
185
+ def _edge_metrics(nodes: dict) -> dict:
186
+ types, key_mix = Counter(), Counter()
187
+ total = dangling = with_edges = non_canonical = 0
188
+ samples = []
189
+ for nid, r in nodes.items():
190
+ es = r.get("edges") or []
191
+ if not isinstance(es, (list, tuple)) or not es:
192
+ continue
193
+ with_edges += 1
194
+ for e in es:
195
+ if not isinstance(e, dict):
196
+ continue
197
+ total += 1
198
+ keys = [k for k in CANONICAL_EDGE_KEYS + ("type",) if e.get(k)]
199
+ types[str(e.get("relation_type") or e.get("relation") or e.get("type"))] += 1
200
+ key_mix["+".join(keys) if keys else "<无关系键>"] += 1
201
+ if not any(k in CANONICAL_EDGE_KEYS for k in keys):
202
+ non_canonical += 1
203
+ if len(samples) < 5:
204
+ samples.append({"node": nid, "edge": dict(e)})
205
+ tgt = e.get("target") or e.get("target_id")
206
+ if tgt is None or str(tgt) not in nodes:
207
+ dangling += 1
208
+ return {"nodes_with_edges": with_edges, "total": total,
209
+ "types": dict(types.most_common(20)), "key_mix": dict(key_mix.most_common(10)),
210
+ "non_canonical": non_canonical, "dangling": dangling, "samples": samples}
211
+
212
+
213
+ def _path_metrics(root: str, nodes: dict, check_exists: bool) -> dict:
214
+ missing = mismatch = 0
215
+ samples = []
216
+ for r in nodes.values():
217
+ layer, p = str(r.get("layer") or ""), str(r.get("path") or "")
218
+ if not p:
219
+ missing += 1
220
+ if len(samples) < 5:
221
+ samples.append({"id": r.get("id"), "why": "path 缺失"})
222
+ continue
223
+ rel = p.replace("\\", "/")
224
+ head = rel.split("/", 1)[0]
225
+ if layer and head != layer and head in LAYER_DIRS:
226
+ mismatch += 1
227
+ if len(samples) < 5:
228
+ samples.append({"id": r.get("id"), "path": p, "layer": layer})
229
+ if check_exists and not os.path.exists(os.path.join(root, rel)):
230
+ missing += 1
231
+ if len(samples) < 5:
232
+ samples.append({"id": r.get("id"), "path": p, "why": "文件不存在"})
233
+ return {"missing": missing, "layer_mismatch": mismatch, "samples": samples}
234
+
235
+
236
+ def _gate_metrics(root: str) -> dict:
237
+ dec = _read_jsonl(os.path.join(root, DECISION_LOG))
238
+ inbox = _read_jsonl(os.path.join(root, INBOX_LOG))
239
+ ops = Counter()
240
+ # _audit.jsonl 是 append-only 且可达数十 MB(真源 15.9MB / 7.4 万行)——
241
+ # 只读尾部窗口(近 AUDIT_TAIL 行)取 op 分布,避免周期巡检在此处 O(全史)。
242
+ audit = os.path.join(root, "_audit.jsonl")
243
+ tail = []
244
+ if os.path.exists(audit):
245
+ with open(audit, encoding="utf-8", errors="replace") as f:
246
+ tail = list(deque(f, maxlen=AUDIT_TAIL))
247
+ for line in tail:
248
+ if not line.strip():
249
+ continue
250
+ try:
251
+ ops[str(json.loads(line).get("op"))] += 1
252
+ except json.JSONDecodeError:
253
+ continue
254
+ return {"decisions": dict(Counter(str(r.get("decision") or r.get("status") or "?")
255
+ for r in dec).most_common(10)),
256
+ "decisions_total": len(dec),
257
+ "inbox_pending": max(0, len(inbox) - len(dec)),
258
+ "audit_ops": dict(ops.most_common(6)), "audit_tail_lines": len(tail),
259
+ "audit_tail_window": AUDIT_TAIL}
260
+
261
+
262
+ def _reach_metrics(root: str, nodes: dict) -> dict:
263
+ p = os.path.join(root, ACCESS_LOG)
264
+ if not os.path.exists(p):
265
+ return {"distinct": None, "ratio": None, "lines": None}
266
+ seen, lines = set(), 0
267
+ with open(p, encoding="utf-8", errors="replace") as f:
268
+ for line in f:
269
+ line = line.strip()
270
+ if not line:
271
+ continue
272
+ lines += 1
273
+ try:
274
+ rec = json.loads(line)
275
+ except json.JSONDecodeError:
276
+ continue
277
+ # 实际落盘形态:{"t":..., "ids":[<nid>,...], "tier":...}(_access.log 逐次追加)
278
+ got = rec.get("ids")
279
+ if isinstance(got, (list, tuple)):
280
+ seen.update(str(x) for x in got if x)
281
+ nid = rec.get("id") or rec.get("node_id") or rec.get("nid")
282
+ if nid:
283
+ seen.add(str(nid))
284
+ hit = len(seen & set(nodes))
285
+ return {"distinct": hit, "ratio": hit / max(len(nodes), 1), "lines": lines}
286
+
287
+
288
+ def _coverage_metrics(nodes: dict) -> dict:
289
+ """混层口径 + 字段覆盖率(9-15 审计基线口径,逐项可复现)。
290
+
291
+ **分母口径取证(2026-09-16,关键)**:审计的「role 99.99% 为空」「evidence_count
292
+ 仅 6 条」是 **knowledge 层**分母;全库分母会摊薄成「role 2.13% / evidence 12 条」,
293
+ 把一个 99.99% 的空缺伪装成「还行」。故两类分母并存上报,**健康判据一律取
294
+ knowledge 层口径**(`*_ratio_kn`),全库口径只作对照片段。
295
+ """
296
+ kn = [r for r in nodes.values() if str(r.get("layer")) == "knowledge"]
297
+ mixed = sum(1 for r in kn if {"doc", "code"} & _tag_prefixes(r))
298
+ n, nk = len(nodes), max(len(kn), 1)
299
+ return {"nodes": n, "knowledge": len(kn), "mixed_layer": mixed,
300
+ "mixed_ratio": mixed / nk,
301
+ "role_ratio": sum(1 for r in nodes.values() if r.get("role")) / max(n, 1),
302
+ "basis_ratio": sum(1 for r in nodes.values() if _basis_of(r)) / max(n, 1),
303
+ "evidence_ratio": sum(1 for r in nodes.values()
304
+ if _as_int(r.get("evidence_count")) > 0) / max(n, 1),
305
+ "state_ratio": sum(1 for r in nodes.values()
306
+ if r.get("lifecycle_state")) / max(n, 1),
307
+ # —— knowledge 层口径(= 审计原口径,判据用这组)——
308
+ "role_ratio_kn": sum(1 for r in kn if r.get("role")) / nk,
309
+ "basis_ratio_kn": sum(1 for r in kn if _basis_of(r)) / nk,
310
+ "evidence_ratio_kn": sum(1 for r in kn
311
+ if _as_int(r.get("evidence_count")) > 0) / nk}
312
+
313
+
314
+ def _as_int(v) -> int:
315
+ try:
316
+ return int(v)
317
+ except (TypeError, ValueError):
318
+ return 0
319
+
320
+
321
+ # ---------------------------- G3 · unanalyzed 派生集合 ----------------------------
322
+
323
+ def unanalyzed(nodes: dict) -> dict:
324
+ """G3:unanalyzed **派生**判据(不新增字段、零写入)。
325
+
326
+ 判据 = `evidence_count==0 ∧ verification_basis 空 ∧ lifecycle_state 空`
327
+ —— 三者皆空 = 「无一维分析痕迹」,与「分析结论为负」不同(后者会留 basis)。
328
+
329
+ 不做字段级占位的理由(Ghidra 交接 §5.4):
330
+ * 生命周期是**单向降级轴**(active→converged→demoted→archived),与分析覆盖正交,
331
+ 塞进同一字段即破坏正交性;
332
+ * **逃逸口已存在**——`verification_basis` 声明集里有 `other`(真源 273 条在用),
333
+ 「分析过但无法归类」有明确归宿,故「basis 为空」不再与「分析结论为空」混淆。
334
+ """
335
+ ids = [nid for nid, r in nodes.items()
336
+ if _as_int(r.get("evidence_count")) == 0
337
+ and not _basis_of(r)
338
+ and not r.get("lifecycle_state")]
339
+ return {"count": len(ids), "ratio": len(ids) / max(len(nodes), 1),
340
+ "sample": ids[:5]}
341
+
342
+
343
+ # ---------------------------- G1 · 类型空间正交性审计 ----------------------------
344
+
345
+ def _usage(nodes: dict, edges: dict) -> dict:
346
+ """各类型空间的**实测**取值(与 enum_spaces 的声明值对照)。"""
347
+ from .chain import edge_rel
348
+ used = {
349
+ "layer": Counter(str(r.get("layer")) for r in nodes.values() if r.get("layer")),
350
+ "role": Counter(str(r.get("role")) for r in nodes.values() if r.get("role")),
351
+ "verification_basis": Counter(b for r in nodes.values() for b in _basis_of(r)),
352
+ "derived_relation": Counter(str(r.get("derived_relation"))
353
+ for r in nodes.values() if r.get("derived_relation")),
354
+ "lifecycle_state": Counter(str(r.get("lifecycle_state"))
355
+ for r in nodes.values() if r.get("lifecycle_state")),
356
+ "tag_prefix": Counter(p for r in nodes.values() for p in _tag_prefixes(r)),
357
+ }
358
+ rels = Counter()
359
+ for r in nodes.values():
360
+ for e in (r.get("edges") or []):
361
+ if isinstance(e, dict):
362
+ rels[edge_rel(e)] += 1
363
+ used["edge_type"] = Counter({k: v for k, v in rels.items() if k})
364
+ return used
365
+
366
+
367
+ def _g1_audit(nodes: dict, edges: dict) -> dict:
368
+ """G1:声明值 / 实测值 / 未声明已用 / 声明未用 / 跨空间重叠 / 方向口径分歧。"""
369
+ spaces = enum_spaces()
370
+ used = _usage(nodes, edges)
371
+ out = {}
372
+ for name, spec in spaces.items():
373
+ dec = set(spec.get("declared") or [])
374
+ u = set(used.get(name, {}).keys())
375
+ out[name] = {
376
+ "source": spec.get("source"),
377
+ "declared": sorted(dec),
378
+ "used": dict(used.get(name, {}).most_common(30)),
379
+ "undeclared_used": sorted(u - dec),
380
+ "declared_unused": sorted(dec - u),
381
+ "closed": (spec.get("declared") is not None and not (u - dec)),
382
+ }
383
+ for name in ("role", "tag_prefix"):
384
+ out[name] = {"source": None, "declared": None,
385
+ "used": dict(used.get(name, {}).most_common(30)),
386
+ "undeclared_used": sorted(used.get(name, {})),
387
+ "declared_unused": [], "closed": None}
388
+ # 跨空间重叠:同一字面量出现在 ≥2 个空间 → 解析歧义风险
389
+ owners = Counter()
390
+ for name, spec in out.items():
391
+ for lit in (spec.get("declared") or []):
392
+ owners[str(lit)] += 1
393
+ out["_cross_space_overlap"] = sorted(l for l, c in owners.items() if c > 1) + \
394
+ sorted(set(used.get("layer", {})) & set(used.get("tag_prefix", {})))
395
+ out["_direction_conflicts"] = KNOWN_DIRECTION_CONFLICTS
396
+ out["_verified_consistent"] = VERIFIED_CONSISTENT
397
+ out["_doc_code_drift"] = DOC_CODE_DRIFT
398
+ out["_new_type_touchpoints"] = NEW_TYPE_TOUCHPOINTS
399
+ return out
400
+
401
+
402
+ # ---------------------------- 断言集 ----------------------------
403
+
404
+ def _ck(cid: str, level: str, ok: bool, detail: str) -> dict:
405
+ return {"id": cid, "level": level, "ok": bool(ok), "detail": detail}
406
+
407
+
408
+ def check(root: str, *, check_paths: bool = True,
409
+ baseline: dict = None, strict: bool = False) -> dict:
410
+ """跑一遍全部断言,返回报告 dict(零写入:不修任何数据、不落任何文件)。"""
411
+ t0 = time.time()
412
+ nodes = load_index(root)
413
+ n = len(nodes)
414
+ small = n < THRESHOLDS["min_nodes"]
415
+ cov = _coverage_metrics(nodes)
416
+ edges = _edge_metrics(nodes)
417
+ paths = _path_metrics(root, nodes, check_paths)
418
+ gate = _gate_metrics(root)
419
+ reach = _reach_metrics(root, nodes)
420
+ un = unanalyzed(nodes)
421
+ g1 = _g1_audit(nodes, edges)
422
+ dup = _dup_metrics(nodes)
423
+ checks = []
424
+ T = THRESHOLDS
425
+
426
+ # ---- 不变量(fail-closed)----
427
+ for space, use_key in (("layer", "layer"), ("verification_basis", "verification_basis"),
428
+ ("derived_relation", "derived_relation"),
429
+ ("lifecycle_state", "lifecycle_state")):
430
+ spec = g1[space]
431
+ undecl = spec["undeclared_used"]
432
+ checks.append(_ck(f"enum.{space}.closed", "FAIL", not undecl,
433
+ f"声明源={spec['source']};未声明已用={undecl or '无'}"))
434
+ checks.append(_ck("edge.rel.declared", "WARN",
435
+ not g1["edge_type"]["undeclared_used"],
436
+ f"未声明边类型={g1['edge_type']['undeclared_used'] or '无'}"
437
+ "(读侧退化为 chain.DEFAULT_EDGE_WEIGHT=0.50,不炸但权重失真)"))
438
+ checks.append(_ck("edge.key.canonical", "WARN", edges["non_canonical"] == 0,
439
+ f"非规范键边 {edges['non_canonical']}/{edges['total']}"
440
+ f"(写侧规范={CANONICAL_EDGE_KEYS};md_cg 读侧兼容 type,"
441
+ "非 md_cg 读侧不兼容——存量债,判据=不劣化)"))
442
+ checks.append(_ck("edge.target.resolvable", "FAIL", edges["dangling"] == 0,
443
+ f"悬空边 {edges['dangling']}/{edges['total']}(图断裂)"))
444
+ checks.append(_ck("index.path.present", "FAIL", paths["missing"] == 0,
445
+ f"path 缺失/文件不存在 {paths['missing']}"
446
+ + ("" if check_paths else "(--no-path-check 未查盘)")))
447
+ checks.append(_ck("index.layer.dir.match", "FAIL", paths["layer_mismatch"] == 0,
448
+ f"层-目录不一致 {paths['layer_mismatch']}(9-15 基线=0)"))
449
+ top_dup = dup["largest"][0] if dup["largest"] else 0
450
+ checks.append(_ck("dup.content.no_blowup", "FAIL", top_dup <= DUP_GROUP_MAX,
451
+ f"内容指纹重复组 {dup['groups']} / 涉及节点 {dup['nodes']}"
452
+ f" / 最大组 {dup['largest'][:3]}(最大组上限 {DUP_GROUP_MAX})"))
453
+
454
+ # ---- 健康指标(只告警)----
455
+ def _ratio(cid, val, low, name):
456
+ if small or val is None:
457
+ checks.append(_ck(cid, "BLINDSPOT", True,
458
+ f"{name} 不可判(样本 {n} < {T['min_nodes']} / 数据源缺失)"))
459
+ return
460
+ checks.append(_ck(cid, "WARN", val >= low, f"{name}={val * 100:.1f}%(下限 {low:.0%})"))
461
+
462
+ mr = None if small else cov["mixed_ratio"]
463
+ checks.append(_ck("stratum.mixed_ratio",
464
+ "BLINDSPOT" if mr is None else "WARN",
465
+ mr is None or mr <= T["stratum_ratio_max"],
466
+ "混层比(knowledge 中 doc∪code 标签)=N/A(样本不足)" if mr is None else
467
+ f"混层比(knowledge 中 doc∪code 标签)={mr * 100:.1f}%"
468
+ f"(上限 {T['stratum_ratio_max']:.0%},治理靶 {T['stratum_target']:.0%})"))
469
+ _ratio("stratum.role_coverage", cov["role_ratio_kn"], T["role_coverage_min"],
470
+ "role 覆盖率[knowledge]")
471
+ _ratio("stratum.evidence_coverage", cov["evidence_ratio_kn"],
472
+ T["evidence_coverage_min"], "evidence 覆盖率[knowledge]")
473
+ _ratio("analysis.coverage", 1 - un["ratio"], T["analysis_coverage_min"],
474
+ f"分析覆盖率(非 unanalyzed;unanalyzed={un['count']})")
475
+ _ratio("reach.ratio", None if small else reach["ratio"], T["reach_ratio_min"], "触达率")
476
+ checks.append(_ck("gate.sample", "WARN",
477
+ gate["decisions_total"] >= T["gate_sample_min"],
478
+ f"闸门裁决样本 {gate['decisions_total']} 条 {gate['decisions']}"
479
+ f"(下限 {T['gate_sample_min']})"))
480
+
481
+ # ---- 基线不劣化(有 baseline 时才可判)----
482
+ if baseline:
483
+ checks += _regression({"mixed_ratio": cov["mixed_ratio"],
484
+ "non_canonical": edges["non_canonical"],
485
+ "dangling": edges["dangling"],
486
+ "dup_groups": dup["groups"],
487
+ "unanalyzed": un["count"]}, baseline)
488
+
489
+ fails = [c for c in checks if c["level"] == "FAIL" and not c["ok"]]
490
+ warns = [c for c in checks if c["level"] == "WARN" and not c["ok"]]
491
+ blind = [c for c in checks if c["level"] == "BLINDSPOT"]
492
+ v = "FAIL" if fails else ("WARN" if (warns or (strict and blind)) else "PASS")
493
+ return {"report_version": REPORT_VERSION, "t": t0, "elapsed_s": round(time.time() - t0, 2),
494
+ "root": os.path.abspath(root), "verdict": v,
495
+ "nodes": n, "checks": checks,
496
+ "counts": {"fail": len(fails), "warn": len(warns), "blindspot": len(blind),
497
+ "total": len(checks)},
498
+ "coverage": cov, "dup": dup, "edges": edges, "paths": paths,
499
+ "gate": gate, "reach": reach, "unanalyzed": un, "g1": g1,
500
+ "thresholds": T}
501
+
502
+
503
+ def _regression(cur: dict, baseline: dict) -> list:
504
+ """与基线比对:单调量只准不变或改善(重复度/悬空/非规范键/混层/unanalyzed)。
505
+
506
+ 只收**单调有害量**(越大越坏);触达率等随运行时间自然增长的量不进比对,
507
+ 否则日志轮转即误报。
508
+ """
509
+ base = (baseline or {}).get("metrics") or {}
510
+ out = []
511
+ for key, val in cur.items():
512
+ b = base.get(key)
513
+ if b is None or val is None:
514
+ continue
515
+ worse = val > b + 1e-9
516
+ out.append(_ck(f"baseline.{key}", "FAIL", not worse,
517
+ f"现值 {val:.4f} vs 基线 {b:.4f}"
518
+ + ("(劣化)" if worse else "(未劣化)")))
519
+ return out
520
+
521
+
522
+ def metrics_of(rep: dict) -> dict:
523
+ """抽成可比对的扁平指标(--baseline 的写入面)。"""
524
+ return {"mixed_ratio": rep["coverage"]["mixed_ratio"],
525
+ "non_canonical": rep["edges"]["non_canonical"],
526
+ "dangling": rep["edges"]["dangling"],
527
+ "dup_groups": rep["dup"]["groups"],
528
+ "unanalyzed": rep["unanalyzed"]["count"],
529
+ "nodes": rep["nodes"],
530
+ "role_ratio": rep["coverage"]["role_ratio"],
531
+ "basis_ratio": rep["coverage"]["basis_ratio"],
532
+ "evidence_ratio": rep["coverage"]["evidence_ratio"],
533
+ "role_ratio_kn": rep["coverage"]["role_ratio_kn"],
534
+ "evidence_ratio_kn": rep["coverage"]["evidence_ratio_kn"],
535
+ "reach_ratio": rep["reach"]["ratio"],
536
+ "gate_decisions": rep["gate"]["decisions_total"]}
537
+
538
+
539
+ # ---------------------------- 报告 ----------------------------
540
+
541
+ def render(rep: dict) -> str:
542
+ L = []
543
+ a = L.append
544
+ a(f"== conformance v{rep['report_version']} · {rep['root']}")
545
+ a(f" verdict: {rep['verdict']} nodes={rep['nodes']} "
546
+ f"checks={rep['counts']['total']} (fail {rep['counts']['fail']} / "
547
+ f"warn {rep['counts']['warn']} / blindspot {rep['counts']['blindspot']})")
548
+ a("\n-- 断言集(FAIL=fail-closed 不变量 / WARN=健康指标只告警)--")
549
+ for c in rep["checks"]:
550
+ mark = {"FAIL": "!!", "WARN": " ?", "BLINDSPOT": " ~"}.get(c["level"], " .")
551
+ ok = "ok " if c["ok"] else "NO "
552
+ a(f" [{mark}] {ok}{c['id']}: {c['detail']}")
553
+ c = rep["coverage"]
554
+ a(f"\n-- 口径复核(9-15 基线)--")
555
+ a(f" nodes={c['nodes']} knowledge={c['knowledge']} 混层={c['mixed_layer']}"
556
+ f" ({c['mixed_ratio'] * 100:.1f}%)")
557
+ a(f" dir_mismatch={rep['paths']['layer_mismatch']} path_missing={rep['paths']['missing']}")
558
+ a(f" 覆盖率[knowledge 层·判据口径] role={c['role_ratio_kn'] * 100:.3f}%"
559
+ f" basis={c['basis_ratio_kn'] * 100:.1f}% evidence={c['evidence_ratio_kn'] * 100:.3f}%")
560
+ a(f" 覆盖率为对照[全库口径] role={c['role_ratio'] * 100:.2f}%"
561
+ f" basis={c['basis_ratio'] * 100:.1f}% evidence={c['evidence_ratio'] * 100:.2f}%"
562
+ f" state={c['state_ratio'] * 100:.1f}%")
563
+ a(f" 重复 content_hash 组={rep['dup']['groups']} 节点={rep['dup']['nodes']}"
564
+ f" 最大组={rep['dup']['largest'][:3]}")
565
+ a(f" 边 total={rep['edges']['total']} 非规范键={rep['edges']['non_canonical']}"
566
+ f" 悬空={rep['edges']['dangling']} 类型={rep['edges']['types']}")
567
+ r = rep["reach"]
568
+ a(f" 触达 distinct={r['distinct']} ratio="
569
+ + ("N/A" if r['ratio'] is None else f"{r['ratio'] * 100:.1f}%") + f" log_lines={r['lines']}")
570
+ a(f" 闸门 decisions={rep['gate']['decisions_total']} {rep['gate']['decisions']}"
571
+ f" inbox_pending={rep['gate']['inbox_pending']}")
572
+ a(f" 审计 op(尾窗 {rep['gate']['audit_tail_lines']}"
573
+ f"/{rep['gate']['audit_tail_window']} 行)={rep['gate']['audit_ops']}")
574
+ a(f" G3 unanalyzed 派生={rep['unanalyzed']['count']}"
575
+ f" ({rep['unanalyzed']['ratio'] * 100:.1f}%)")
576
+ a("\n-- G1 类型空间正交性 --")
577
+ for name, spec in rep["g1"].items():
578
+ if name.startswith("_"):
579
+ continue
580
+ a(f" [{name}] 源={spec['source']} 封闭={spec['closed']}")
581
+ a(f" 实测={spec['used']}")
582
+ if spec["undeclared_used"]:
583
+ a(f" ★未声明已用={spec['undeclared_used']}")
584
+ if spec["declared_unused"]:
585
+ a(f" 声明未用={spec['declared_unused']}")
586
+ a(f" 跨空间重叠={rep['g1']['_cross_space_overlap'] or '无'}")
587
+ a(f" 方向口径分歧={[d['literal'] for d in rep['g1']['_direction_conflicts']]}")
588
+ a(f" 已核对无分歧={[d['literal'] for d in rep['g1']['_verified_consistent']]}")
589
+ a(f" 文档滞后于代码={[d['where'] for d in rep['g1']['_doc_code_drift']]}")
590
+ a(f" 新增类型触点={rep['g1']['_new_type_touchpoints']}")
591
+ return "\n".join(L)
592
+
593
+
594
+ def register_sustain(sustain_module) -> None: # pragma: no cover
595
+ """挂点说明(供 sustain 侧最小侵入调用)。
596
+
597
+ 周期巡检**不加新 tick**:`_tick_tidy()` 已是「contextual 存量治理」的
598
+ 读侧巡检,本断言集复用同一节奏(tidy_interval),由 `report_summary(cg)`
599
+ 只取结论不落盘——避免为只读检查新增后台周期与 env 开关。
600
+ """
601
+ sustain_module.conformance_summary = report_summary
602
+
603
+
604
+ def report_summary(cg_or_root) -> dict:
605
+ """给常驻循环用的**轻量**结论(不渲染全文、不写盘):verdict + 计数。"""
606
+ root = getattr(cg_or_root, "root", None) or cg_or_root or _default_root()
607
+ try:
608
+ rep = check(root, check_paths=False)
609
+ except Exception as e: # noqa: BLE001
610
+ return {"ok": False, "verdict": "BLINDSPOT", "error": f"{type(e).__name__}: {e}"}
611
+ return {"ok": rep["verdict"] != "FAIL", "verdict": rep["verdict"],
612
+ "fail": rep["counts"]["fail"], "warn": rep["counts"]["warn"],
613
+ "blindspot": rep["counts"]["blindspot"],
614
+ "failed_ids": [c["id"] for c in rep["checks"]
615
+ if c["level"] == "FAIL" and not c["ok"]],
616
+ "t": rep["t"]}
617
+
618
+
619
+ def main(argv=None) -> int:
620
+ p = argparse.ArgumentParser(prog="python -m md_cg.conformance",
621
+ description="数据健康不变量断言集(只读,零写入)")
622
+ p.add_argument("--root", default=None,
623
+ help="认知图根(默认 MDCG_ROOT > data/paths.json > 插件仓 data/mdcg)")
624
+ p.add_argument("--json", dest="json_out", default=None)
625
+ p.add_argument("--baseline", default=None, help="基线报告 json(比对不劣化)")
626
+ p.add_argument("--write-baseline", default=None, help="把本次指标写成新基线")
627
+ p.add_argument("--no-path-check", action="store_true", help="不查盘上文件是否存在")
628
+ p.add_argument("--strict", action="store_true", help="BLINDSPOT 也视为非 PASS")
629
+ a = p.parse_args(argv)
630
+
631
+ root = a.root or _default_root()
632
+ baseline = None
633
+ if a.baseline:
634
+ with open(a.baseline, encoding="utf-8") as f:
635
+ baseline = json.load(f)
636
+ try:
637
+ rep = check(root, check_paths=not a.no_path_check,
638
+ baseline=baseline, strict=a.strict)
639
+ except (OSError, ValueError) as e:
640
+ # 数据源不可读 = BLINDSPOT(如实上报),绝不打印「通过」
641
+ print(f"== conformance v{REPORT_VERSION} · {os.path.abspath(root)}")
642
+ print(f" verdict: BLINDSPOT —— 索引不可读: {type(e).__name__}: {e}")
643
+ print(f" 缺失维度: 数据源({INDEX_FILE});修复后重跑,勿以本结果为「通过」。")
644
+ return 2
645
+ print(render(rep))
646
+ if a.write_baseline:
647
+ with open(a.write_baseline, "w", encoding="utf-8") as f:
648
+ json.dump({"report_version": REPORT_VERSION, "t": rep["t"],
649
+ "root": rep["root"], "metrics": metrics_of(rep)},
650
+ f, ensure_ascii=False, indent=2)
651
+ print(f"\n 基线已写入: {a.write_baseline}")
652
+ if a.json_out:
653
+ with open(a.json_out, "w", encoding="utf-8") as f:
654
+ json.dump(rep, f, ensure_ascii=False, indent=2, default=str)
655
+ print(f" 报告已写入: {a.json_out}")
656
+ return 0 if rep["verdict"] != "FAIL" else 1
657
+
658
+
659
+ if __name__ == "__main__":
660
+ if hasattr(sys.stdout, "reconfigure"):
661
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
662
+ sys.exit(main())
@@ -3,7 +3,7 @@
3
3
 
4
4
  理论出处(本仓原文,非外部知识):
5
5
 
6
- · **情绪 = 信息差的二阶变化 d²D/dt²**(`docs/智能的公理化基石.md` §十一,:412-510)
6
+ · **情绪 = 信息差的二阶变化 d²D/dt²**(`docs/theory/智能的公理化基石.md` §十一,:412-510)
7
7
  工程端对应 `emotional_bias`(approaching / avoiding / stable),并且原文明确:
8
8
  「情绪通道**独立、不参与信任计算**」。故本模块 L0 只做**流程调度**,
9
9
  绝不改动 confidence / 资格判定——避免把情绪混进事实判断。