@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,343 @@
1
+ # -*- coding: utf-8 -*-
2
+ """conformance 断言集自测(Pi⑤)。
3
+
4
+ 纪律(吸取 test_p27【9】教训):**自备数据源,不依赖仓库外目录状态**——
5
+ 外部 clone(无 docs/experiments、无真源库)下必须同样全绿。故:
6
+
7
+ * 主测面 = `tempfile` 内**合成库**(确定性、可注入缺陷)→ 验证「注入即检出」;
8
+ * 真源库只在**存在时**做一次集成基线复核(不存在则 SKIP,不报 FAIL)。
9
+
10
+ 覆盖:
11
+ A 合成库健康态 → verdict 判定 + strict 语义
12
+ B 五条不变量 fail-closed:类型空间封闭 / 边目标可解析 / 索引↔盘一致 /
13
+ 层-目录一致 / 重复度上限(各注入缺陷一次)
14
+ C 边键规范 / 边类型声明 → **WARN 非 FAIL**(读侧兼容,不冒充硬失败)
15
+ D 健康指标 BLINDSPOT(样本不足时不冒充 PASS)
16
+ E G3 unanalyzed 派生判据(四类排除)
17
+ F 基线不劣化:劣化→FAIL、持平→ok
18
+ G 零写入(跑一遍 check 后全库指纹逐字节不变)
19
+ H CLI:坏库 BLINDSPOT 退出码 2 / FAIL 退出码 1 / --json / --baseline / --strict
20
+ I G1 结构完整性(5 空间声明 + 方向分歧 + 文档滞后登记)
21
+ J sustain 挂点(只读、异常不炸、status 透出)
22
+
23
+ 运行:python -m md_cg.test_conformance
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import hashlib
28
+ import io
29
+ import json
30
+ import os
31
+ import shutil
32
+ import sys
33
+ import tempfile
34
+ from contextlib import redirect_stdout
35
+
36
+ from . import conformance as C
37
+
38
+ PASS = FAIL = 0
39
+ FAILS = []
40
+
41
+
42
+ def ok(cond, label):
43
+ global PASS, FAIL
44
+ if cond:
45
+ PASS += 1
46
+ else:
47
+ FAIL += 1
48
+ FAILS.append(label)
49
+ print(f" FAIL {label}")
50
+
51
+
52
+ # ---------------------------- 合成库 ----------------------------
53
+
54
+ def node(nid, layer="knowledge", **kw):
55
+ rec = {"id": nid, "layer": layer, "path": f"{layer}/{nid}.md",
56
+ "content": f"内容 {nid}", "content_hash": f"hash_{nid}", "tags": []}
57
+ rec.update(kw)
58
+ return rec
59
+
60
+
61
+ DEFAULT_ACCESS = ({"t": 1, "ids": ["n1", "n2"], "tier": "core"},)
62
+
63
+
64
+ def build(root, nodes, *, decisions=None, inbox=None, access=DEFAULT_ACCESS, audit=(),
65
+ write_files=True):
66
+ os.makedirs(root, exist_ok=True)
67
+ with open(os.path.join(root, "_index.json"), "w", encoding="utf-8") as f:
68
+ json.dump({"nodes": nodes}, f, ensure_ascii=False)
69
+ hp = os.path.join(root, "hippocampus")
70
+ os.makedirs(hp, exist_ok=True)
71
+ for name, rows in (("decisions.jsonl", decisions), ("inbox.jsonl", inbox)):
72
+ with open(os.path.join(hp, name), "w", encoding="utf-8") as f:
73
+ for r in rows or []:
74
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
75
+ with open(os.path.join(root, "_access.log"), "w", encoding="utf-8") as f:
76
+ for r in access:
77
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
78
+ with open(os.path.join(root, "_audit.jsonl"), "w", encoding="utf-8") as f:
79
+ for r in audit:
80
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
81
+ if write_files:
82
+ for r in nodes.values():
83
+ if not r.get("path"):
84
+ continue
85
+ p = os.path.join(root, r["path"])
86
+ os.makedirs(os.path.dirname(p), exist_ok=True)
87
+ with open(p, "w", encoding="utf-8") as f:
88
+ f.write(r.get("content", ""))
89
+ return root
90
+
91
+
92
+ def dec(n, decision="accept"):
93
+ return [{"pid": f"p{i}", "decision": decision} for i in range(n)]
94
+
95
+
96
+ def failing(rep):
97
+ return [c["id"] for c in rep["checks"] if c["level"] == "FAIL" and not c["ok"]]
98
+
99
+
100
+ def warn_no(rep):
101
+ return [c["id"] for c in rep["checks"] if c["level"] == "WARN" and not c["ok"]]
102
+
103
+
104
+ def level_of(rep, cid):
105
+ return next((c["level"] for c in rep["checks"] if c["id"] == cid), None)
106
+
107
+
108
+ def snapshot(root):
109
+ out = {}
110
+ for dp, _, fns in os.walk(root):
111
+ for fn in fns:
112
+ p = os.path.join(dp, fn)
113
+ with open(p, "rb") as f:
114
+ out[os.path.relpath(p, root)] = hashlib.md5(f.read()).hexdigest()
115
+ return out
116
+
117
+
118
+ # 健康态:knowledge 层三节点 role/basis/evidence 齐备(判据=knowledge 层口径),
119
+ # 混层 1/3=33% < 60% 上限,access 覆盖 2/5=40% ≥ 30% 下限,闸门样本 25 ≥ 20
120
+ HEALTHY = {
121
+ "n1": node("n1", tags=["doc:note"], role="knowledge-card",
122
+ verification_basis="test", evidence_count=2),
123
+ "n2": node("n2", role="knowledge-card", verification_basis="test",
124
+ evidence_count=3, lifecycle_state="active"),
125
+ "n3": node("n3", layer="contextual"),
126
+ "n4": node("n4", layer="self"),
127
+ "n5": node("n5", role="knowledge-card", verification_basis="textbook"),
128
+ }
129
+
130
+
131
+ def main():
132
+ print("== test_conformance: 合成库主测面 ==")
133
+ tmp = tempfile.mkdtemp(prefix="conf_")
134
+ try:
135
+ # ---------- A 健康态 ----------
136
+ r1 = build(os.path.join(tmp, "healthy"), dict(HEALTHY), decisions=dec(25))
137
+ rep = C.check(r1)
138
+ ok(rep["verdict"] == "PASS", f"A1 健康库 verdict=PASS(实得 {rep['verdict']})")
139
+ ok(not failing(rep), f"A2 健康库无 FAIL(实得 {failing(rep)})")
140
+ ok(level_of(rep, "stratum.role_coverage") == "BLINDSPOT",
141
+ "A3 样本不足 → 比率指标 BLINDSPOT")
142
+ rep_s = C.check(r1, strict=True)
143
+ ok(rep_s["verdict"] != "PASS", "A4 strict 下 BLINDSPOT 不算 PASS")
144
+ ok(C.check(r1)["counts"] == rep["counts"], "A5 复跑确定性(计数一致)")
145
+
146
+ # ---------- B 不变量 fail-closed ----------
147
+ b1 = dict(HEALTHY)
148
+ b1["bad"] = node("bad", layer="不存在的层")
149
+ r2 = build(os.path.join(tmp, "b1"), b1, decisions=dec(25))
150
+ rep = C.check(r2)
151
+ ok("enum.layer.closed" in failing(rep), "B1 层越界 → FAIL enum.layer.closed")
152
+ ok(rep["verdict"] == "FAIL", "B2 有 FAIL → verdict=FAIL")
153
+
154
+ b2 = dict(HEALTHY)
155
+ b2["n1"] = node("n1", edges=[{"relation_type": "part_of", "target": "ghost"}])
156
+ r3 = build(os.path.join(tmp, "b2"), b2, decisions=dec(25))
157
+ ok("edge.target.resolvable" in failing(C.check(r3)),
158
+ "B3 悬空边 → FAIL edge.target.resolvable")
159
+
160
+ b3 = dict(HEALTHY)
161
+ b3["n1"] = node("n1", path="contextual/n1.md") # layer=knowledge
162
+ r4 = build(os.path.join(tmp, "b3"), b3, decisions=dec(25))
163
+ ok("index.layer.dir.match" in failing(C.check(r4)),
164
+ "B4 层-目录不一致 → FAIL index.layer.dir.match")
165
+
166
+ b4 = dict(HEALTHY)
167
+ b4["n5"] = node("n5", path="") # path 缺失
168
+ r5 = build(os.path.join(tmp, "b4"), b4, decisions=dec(25))
169
+ ok("index.path.present" in failing(C.check(r5)),
170
+ "B5 path 缺失 → FAIL index.path.present")
171
+
172
+ b5 = dict(HEALTHY)
173
+ b5["n5"] = node("n5", path="knowledge/nowhere.md") # 盘上不存在
174
+ r6 = build(os.path.join(tmp, "b5"), b5, decisions=dec(25), write_files=False)
175
+ ok("index.path.present" in failing(C.check(r6)),
176
+ "B6 盘上文件不存在 → FAIL index.path.present")
177
+ ok(not failing(C.check(r6, check_paths=False)),
178
+ "B7 --no-path-check 跳过查盘后不报该 FAIL")
179
+
180
+ b6 = dict(HEALTHY)
181
+ for i in range(C.DUP_GROUP_MAX + 1):
182
+ b6[f"d{i}"] = node(f"d{i}", content_hash="same")
183
+ r7 = build(os.path.join(tmp, "b6"), b6, decisions=dec(25))
184
+ ok("dup.content.no_blowup" in failing(C.check(r7)),
185
+ f"B8 重复组 > {C.DUP_GROUP_MAX} → FAIL dup.content.no_blowup")
186
+
187
+ # ---------- C 边键/边类型 = WARN ----------
188
+ c1 = dict(HEALTHY)
189
+ c1["n1"] = node("n1", edges=[{"type": "part_of", "target": "n2"},
190
+ {"relation_type": "causal", "target": "n3"}])
191
+ r8 = build(os.path.join(tmp, "c1"), c1, decisions=dec(25))
192
+ rep = C.check(r8)
193
+ ok(rep["edges"]["non_canonical"] == 1, "C1 legacy type 键计数 =1")
194
+ ok(level_of(rep, "edge.key.canonical") == "WARN", "C2 边键规范判为 WARN")
195
+ ok("edge.key.canonical" not in failing(rep), "C3 边键 legacy 不判 FAIL(读侧兼容)")
196
+ ok("part_of" not in rep["g1"]["edge_type"]["undeclared_used"],
197
+ "C4 已声明边类型不误报")
198
+
199
+ c2 = dict(HEALTHY)
200
+ c2["n1"] = node("n1", edges=[{"relation_type": "bogus_rel", "target": "n2"}])
201
+ r9 = build(os.path.join(tmp, "c2"), c2, decisions=dec(25))
202
+ rep = C.check(r9)
203
+ ok("bogus_rel" in rep["g1"]["edge_type"]["undeclared_used"],
204
+ "C5 未声明边类型被 G1 列出")
205
+ ok(level_of(rep, "edge.rel.declared") == "WARN", "C6 未声明边类型判为 WARN")
206
+
207
+ # ---------- D 数据源缺失 ----------
208
+ r10 = os.path.join(tmp, "d1")
209
+ build(r10, dict(HEALTHY), decisions=dec(25))
210
+ os.remove(os.path.join(r10, "_access.log"))
211
+ rep = C.check(r10)
212
+ ok(level_of(rep, "reach.ratio") == "BLINDSPOT", "D1 触达日志缺失 → BLINDSPOT")
213
+ ok(rep["reach"]["ratio"] is None, "D2 触达率如实置 None 不猜测")
214
+ r11 = os.path.join(tmp, "d2")
215
+ build(r11, dict(HEALTHY), decisions=[])
216
+ ok(level_of(C.check(r11), "gate.sample") == "WARN", "D3 闸门样本不足 → WARN")
217
+
218
+ # ---------- E G3 unanalyzed 派生 ----------
219
+ e1 = {"u1": node("u1"), # 三者皆空 → unanalyzed
220
+ "u2": node("u2", verification_basis="test"), # 有基底 → 非
221
+ "u3": node("u3", evidence_count=2), # 有证据 → 非
222
+ "u4": node("u4", lifecycle_state="converged")} # 有状态 → 非
223
+ r12 = build(os.path.join(tmp, "e1"), e1, decisions=dec(25))
224
+ un = C.check(r12)["unanalyzed"]
225
+ ok(un["count"] == 1 and un["sample"] == ["u1"],
226
+ f"E1 unanalyzed 派生判据(实得 {un['count']})")
227
+ ok(C.check(r12)["coverage"]["nodes"] == 4, "E2 节点计数正确")
228
+
229
+ # ---------- F 基线不劣化 ----------
230
+ rep_ok = C.check(r1)
231
+ m = C.metrics_of(rep_ok)
232
+ base = {"metrics": dict(m)}
233
+ ok(not [c for c in C.check(r1, baseline=base)["checks"]
234
+ if c["id"].startswith("baseline.") and not c["ok"]],
235
+ "F1 与自身基线比对不劣化")
236
+ base_bad = {"metrics": dict(m, non_canonical=0, mixed_ratio=0.0, unanalyzed=0)}
237
+ bad = [c["id"] for c in C.check(r8, baseline=base_bad)["checks"]
238
+ if c["id"].startswith("baseline.") and not c["ok"]]
239
+ ok("baseline.non_canonical" in bad, "F2 非规范边劣化 → FAIL 基线项")
240
+ ok(all(c["id"] in m for c in [{"id": k} for k in
241
+ ("mixed_ratio", "non_canonical", "dangling",
242
+ "dup_groups", "unanalyzed")]),
243
+ "F3 比对键 ⊆ metrics_of 输出(无键名漂移)")
244
+ ok("reach_ratio" in m, "F4 触达率入基线存档")
245
+
246
+ # ---------- G 零写入 ----------
247
+ r13 = build(os.path.join(tmp, "g1"), dict(HEALTHY), decisions=dec(25))
248
+ before = snapshot(r13)
249
+ C.check(r13)
250
+ C.check(r13, strict=True)
251
+ C.report_summary(r13)
252
+ after = snapshot(r13)
253
+ ok(before == after, "G1 跑三遍后全库指纹逐字节不变(只读零写入)")
254
+
255
+ # ---------- H CLI ----------
256
+ buf = io.StringIO()
257
+ with redirect_stdout(buf):
258
+ rc_bad = C.main(["--root", os.path.join(tmp, "nonexistent")])
259
+ out = buf.getvalue()
260
+ ok(rc_bad == 2, "H1 坏库退出码 2(BLINDSPOT 非 PASS)")
261
+ ok("BLINDSPOT" in out and "缺失维度" in out, "H2 坏库输出含缺失维度提示")
262
+ with redirect_stdout(io.StringIO()):
263
+ rc_fail = C.main(["--root", r2])
264
+ ok(rc_fail == 1, "H3 verdict=FAIL 退出码 1")
265
+ jp = os.path.join(tmp, "out.json")
266
+ bp = os.path.join(tmp, "base.json")
267
+ with redirect_stdout(io.StringIO()):
268
+ rc_ok = C.main(["--root", r1, "--json", jp, "--write-baseline", bp])
269
+ ok(rc_ok == 0, "H4 健康库退出码 0")
270
+ ok(json.load(open(jp, encoding="utf-8"))["verdict"] == "PASS", "H5 --json 落盘可用")
271
+ ok("metrics" in json.load(open(bp, encoding="utf-8")), "H6 --write-baseline 落盘可用")
272
+ with redirect_stdout(io.StringIO()):
273
+ rc_cmp = C.main(["--root", r1, "--baseline", bp])
274
+ ok(rc_cmp == 0, "H7 --baseline 比对通路可用")
275
+
276
+ # ---------- I G1 结构 ----------
277
+ sp = C.enum_spaces()
278
+ ok(set(sp) == {"layer", "edge_type", "derived_relation",
279
+ "verification_basis", "lifecycle_state"},
280
+ f"I1 五个类型空间齐备(实得 {sorted(sp)})")
281
+ ok(all(v["declared"] and v["source"] for v in sp.values()),
282
+ "I2 每空间都有声明值 + 模块真源")
283
+ ok("knowledge" in sp["layer"]["declared"], "I3 layer 真源含 knowledge")
284
+ g1 = C.check(r1)["g1"]
285
+ ok(set(("_direction_conflicts", "_verified_consistent", "_doc_code_drift",
286
+ "_cross_space_overlap", "_new_type_touchpoints")) <= set(g1),
287
+ "I4 G1 汇总字段齐备")
288
+ ok(g1["role"]["declared"] is None and g1["role"]["closed"] is None,
289
+ "I5 无真源空间(role)标 None→BLINDSPOT 不冒充封闭")
290
+ ok("hierarchical" in [d["literal"] for d in g1["_direction_conflicts"]],
291
+ "I6 方向口径分歧已登记")
292
+ ok(sp["edge_type"]["declared"] and "part_of" in sp["edge_type"]["declared"],
293
+ "I7 边类型真源可用")
294
+
295
+ # ---------- J sustain 挂点 ----------
296
+ from . import sustain as S
297
+ ok(hasattr(S.SustainLoop, "_tick_conformance"), "J1 SustainLoop 有 conformance 挂点")
298
+ loop = S.SustainLoop(type("CG", (), {"root": r1})(), name="t_conf", d=tmp)
299
+ loop._tick_conformance()
300
+ ok(loop.last_conformance["ok"] is True, "J2 挂点跑通且 ok=True")
301
+ ok("last_conformance" in loop.status(), "J3 status 透出结论")
302
+ loop2 = S.SustainLoop(type("CG", (), {"root": os.path.join(tmp, "nope")})(),
303
+ name="t_conf2", d=tmp)
304
+ loop2._tick_conformance()
305
+ ok(loop2.last_conformance["ok"] is False
306
+ and loop2.last_conformance["verdict"] == "BLINDSPOT",
307
+ "J4 坏库不抛异常、如实标 BLINDSPOT")
308
+ ok(C.report_summary(os.path.join(tmp, "nope2"))["verdict"] == "BLINDSPOT",
309
+ "J5 显式坏路径 report_summary 不炸且如实 BLINDSPOT")
310
+
311
+ # ---------- 集成(真源存在才跑,缺失即 SKIP)----------
312
+ real = "D:/Program Files/2_ai/AEIS/data/mdcg"
313
+ if os.path.exists(os.path.join(real, "_index.json")):
314
+ rp = C.check(real, check_paths=False)
315
+ cv = rp["coverage"]
316
+ print(f" [集成] 真源 nodes={rp['nodes']} 混层比={cv['mixed_ratio'] * 100:.1f}%"
317
+ f" dir_mismatch={rp['paths']['layer_mismatch']}"
318
+ f" 闸门={rp['gate']['decisions_total']} verdict={rp['verdict']}")
319
+ ok(rp["nodes"] > 5000, "K1 真源节点数 > 5000")
320
+ ok(0.50 <= cv["mixed_ratio"] <= 0.65,
321
+ f"K2 混层比复现 9-15 基线 58.4%(实得 {cv['mixed_ratio'] * 100:.1f}%)")
322
+ ok(rp["paths"]["layer_mismatch"] == 0, "K3 层-目录一致 = 0(9-15 基线)")
323
+ ok(rp["gate"]["decisions_total"] >= 100, "K4 闸门样本 ≥ 100")
324
+ ok(cv["role_ratio"] < 0.10, "K5 role 覆盖率仍处治理区(<10%)")
325
+ else:
326
+ print(" [集成] SKIP 真源库不存在(外部 clone 正常情形)")
327
+ finally:
328
+ shutil.rmtree(tmp, ignore_errors=True)
329
+
330
+ print(f"\nPASS={PASS} FAIL={FAIL}")
331
+ if FAILS:
332
+ print("失败项:")
333
+ for f in FAILS:
334
+ print(" -", f)
335
+ return 1
336
+ print("conformance 断言集自测全绿。")
337
+ return 0
338
+
339
+
340
+ if __name__ == "__main__":
341
+ if hasattr(sys.stdout, "reconfigure"):
342
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
343
+ sys.exit(main())
@@ -63,6 +63,30 @@ t3, _ = en_normalizer.normalize_en_query("The dogs were running")
63
63
  ok("the" not in [x.lower() for x in t3] and "were" not in [x.lower() for x in t3],
64
64
  "停用词剔除: %s" % t3)
65
65
 
66
+ # ---- 2b · 置信分层与第三方暴露面修正(2026-09-15,第三方 LoCoMo 报告发现 5)----
67
+ # 置信信号:mapped 词带 source(manual=人工校对层 / cedict=机械反查层)与
68
+ # low_confidence(cedict 层=True——第一义项直译语境失配风险,警告不拒绝)
69
+ t4, d4 = en_normalizer.normalize_en_query("The scavenger hunt was fun")
70
+ ok("寻宝游戏" in t4, "内嵌短语扫描 scavenger hunt→寻宝游戏(长文本内生效): %s" % t4)
71
+ ok(d4 and d4[0].get("action") == "phrase_inline", "phrase_inline 留痕: %s" % d4[:1])
72
+ ok(en_normalizer.normalize_en_query("scared")[0] == ["害怕"],
73
+ "手工层修正 scared→害怕(错切链路 scar×CEDICT 创痕)")
74
+ ok(en_normalizer.normalize_en_query("shelter")[0] == ["收容所"], "手工层修正 shelter→收容所")
75
+ ok(en_normalizer.normalize_en_query("tough")[0] == ["坚强"], "手工层修正 tough→坚强")
76
+ ok(en_normalizer.normalize_en_query("three times a week")[0] == ["每周三次"],
77
+ "短语 three times a week→每周三次")
78
+ ok(en_normalizer.normalize_en_query("times")[0] == ["次"], "手工层修正 times→次")
79
+ t6, d6 = en_normalizer.normalize_en_query("great and thanks")
80
+ lowc = [dd for dd in d6 if dd.get("action") == "mapped"]
81
+ ok(lowc and all(dd.get("source") == "cedict" and dd.get("low_confidence") is True
82
+ for dd in lowc), "CEDICT 机械层命中=低置信: %s" % d6)
83
+ t7, d7 = en_normalizer.normalize_en_query("pottery was great")
84
+ mfd = [dd for dd in d7 if dd.get("orig", "").lower() == "pottery"]
85
+ ok(mfd and mfd[0].get("source") == "manual" and mfd[0].get("low_confidence") is False,
86
+ "人工校对层=高置信+来源透出: %s" % mfd)
87
+ ok(en_normalizer.low_confidence_terms(d6) == ["great", "thanks"],
88
+ "low_confidence_terms 审计提取(去重保序): %s" % en_normalizer.low_confidence_terms(d6))
89
+
66
90
  # ---- 3 · zh_en_atoms:中文→标准英文原子序列 ----
67
91
  ok(zh_en_atoms.serialize("我昨天吃牛肉") == "i yester day eat cow meat",
68
92
  "serialize 复合词拆原子")
@@ -0,0 +1,173 @@
1
+ # -*- coding: utf-8 -*-
2
+ """体检读数规模纪律验证(O(1) 元数据口径,替代全量扫描)。
3
+
4
+ 背景(第 4 条取证):审计日志无上界增长(实测 ~2 条/s、累计 ~21.5 M 条 / 4.0 GB),
5
+ 而健康度只需要「量级」这一 O(1) 信息。旧实现 `len(list(read_jsonl(p)))` 把只读体检
6
+ 绑到 O(n) 全量解析上(4.0 GB → RSS 5 GB+、数十分钟不返回、整条 MCP 通道被一次调用
7
+ 堵死);改用流式数行仍要 O(n) 磁盘 IO,随运行时长线性劣化——同属「指标与代价错配」。
8
+
9
+ 断言面:
10
+ ① 缺失文件:零读数不炸(bytes=0 / events=0 / exact=True)
11
+ ② 阈值内:events 精确 == 日志行数(口径与物化读一致,改造不丢读数)
12
+ ③ 超阈值:只读元数据 + 尾部估算,exact=False,且**不触发任何全量计数**(O(n) 守卫)
13
+ ④ 估算忠于量级:与真实行数相对误差 < 20%(可用但不假装精确)
14
+ ⑤ health_os 集成:审计面走 _log_scale,**read_jsonl/count_jsonl 都不得再碰审计路径**
15
+ (防退回物化读/全量扫的核心回归守卫)
16
+ ⑥ 口径连续:小审计文件下 health_os 仍给出精确条数 + exact=True
17
+
18
+ 运行:python -m md_cg.test_health_scale
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import shutil
25
+ import tempfile
26
+
27
+ from . import mdcos as mdcos_mod
28
+ from .mdcos import MdCGOS
29
+
30
+ PASS = FAIL = 0
31
+ FAILS = []
32
+
33
+
34
+ def ok(cond, label):
35
+ global PASS, FAIL
36
+ if cond:
37
+ PASS += 1
38
+ else:
39
+ FAIL += 1
40
+ FAILS.append(label)
41
+ print(f" FAIL {label}")
42
+
43
+
44
+ def _write_log(path, n, pad=100):
45
+ with open(path, "w", encoding="utf-8") as f:
46
+ for i in range(n):
47
+ f.write(json.dumps({"t": float(i), "op": "add", "i": i,
48
+ "pad": "x" * pad}) + "\n")
49
+ return os.path.getsize(path)
50
+
51
+
52
+ class _Spy:
53
+ """记录 mdcos 模块级 read_jsonl / count_jsonl 的调用路径(探针)。"""
54
+
55
+ def __enter__(self):
56
+ self.read, self.count = [], []
57
+ self._read, self._count = mdcos_mod.read_jsonl, mdcos_mod.count_jsonl
58
+ spy = self
59
+
60
+ def _r(p, *a, **k):
61
+ spy.read.append(p)
62
+ return spy._read(p, *a, **k)
63
+
64
+ def _c(p, *a, **k):
65
+ spy.count.append(p)
66
+ return spy._count(p, *a, **k)
67
+
68
+ mdcos_mod.read_jsonl, mdcos_mod.count_jsonl = _r, _c
69
+ return self
70
+
71
+ def __exit__(self, *exc):
72
+ mdcos_mod.read_jsonl, mdcos_mod.count_jsonl = self._read, self._count
73
+ return False
74
+
75
+
76
+ def main():
77
+ tmp = tempfile.mkdtemp(prefix="mdcg_scale_")
78
+ try:
79
+ root = os.path.join(tmp, "root")
80
+ os.makedirs(root, exist_ok=True)
81
+ cg = MdCGOS(root)
82
+ audit = cg.audit_log
83
+
84
+ # ---------- ① 缺失文件 ----------
85
+ s = cg._log_scale(os.path.join(root, "_nope.jsonl"))
86
+ ok(s["bytes"] == 0 and s["events"] == 0 and s["exact"] is True,
87
+ "①缺失日志 → 零读数不炸(bytes=0/events=0/exact=True)")
88
+
89
+ # ---------- ② 阈值内精确 ----------
90
+ n = 200
91
+ size = _write_log(audit, n)
92
+ cg.AUDIT_COUNT_MAX_BYTES = size + 1
93
+ s = cg._log_scale(audit)
94
+ ok(s["exact"] is True and s["events"] == n and s["bytes"] == size,
95
+ "②规模在阈值内 → 精确条数(%d 行,与物化读口径一致)" % n)
96
+
97
+ # ---------- ③ 超阈值:O(1),不触发全量计数 ----------
98
+ cg.AUDIT_COUNT_MAX_BYTES = 1024 # 人工压低阈值,等价于 GB 级日志
99
+ with _Spy() as spy:
100
+ s = cg._log_scale(audit)
101
+ ok(s["exact"] is False and s["bytes"] == size,
102
+ "③超阈值 → exact=False,bytes 仍为 O(1) 精确元数据")
103
+ ok(s["events"] is not None and s["events"] > 0,
104
+ "③b超阈值仍给读数(尾部采样估算),不是拒不回答")
105
+ ok(not spy.count and not spy.read,
106
+ "③c超阈值分支零全量调用(未 count_jsonl / 未 read_jsonl)——O(n) 守卫")
107
+
108
+ # ---------- ④ 估算忠于量级 ----------
109
+ rel = abs(s["events"] - n) / float(n)
110
+ ok(rel < 0.20,
111
+ "④估算与真实行数相对误差 %.1f%% < 20%%(量级可用,不假装精确)" % (rel * 100))
112
+
113
+ # ---------- ⑤ health_os 集成:审计路径零全量读 ----------
114
+ with _Spy() as spy:
115
+ h = cg.health_os()
116
+ o = h["os"]
117
+ ok(o["audit_events_exact"] is False and o["audit_bytes"] == size
118
+ and o["audit_events"] == s["events"],
119
+ "⑤health_os 透出审计量级(exact=False/bytes 精确/events=估算)")
120
+ ok(not any("_audit.jsonl" in p for p in spy.read),
121
+ "⑤bhealth_os 不再对审计日志做物化读(read_jsonl 未被以审计路径调用)")
122
+ ok(not any("_audit.jsonl" in p for p in spy.count),
123
+ "⑤chealth_os 不再对审计日志做全量计数(count_jsonl 未被以审计路径调用)")
124
+
125
+ # ---------- ⑥ 口径连续:小日志仍精确 ----------
126
+ cg.AUDIT_COUNT_MAX_BYTES = 64 << 20
127
+ root2 = os.path.join(tmp, "root2")
128
+ os.makedirs(root2, exist_ok=True)
129
+ cg2 = MdCGOS(root2)
130
+ _write_log(cg2.audit_log, 37)
131
+ o2 = cg2.health_os()["os"]
132
+ ok(o2["audit_events"] == 37 and o2["audit_events_exact"] is True,
133
+ "⑥小日志 → health_os 仍给精确条数(37 行,改造不丢读数)")
134
+
135
+ # ---------- ⑦ 真实写入风格:流式行数 == 物化条数(口径等价) ----------
136
+ import random
137
+
138
+ from .fsutil import append_jsonl, read_jsonl
139
+ real = os.path.join(root, "_real.jsonl")
140
+ for i in range(50):
141
+ append_jsonl(real, {"i": i, "pad": "y" * random.randint(1, 80)})
142
+ n_real = len(list(read_jsonl(real)))
143
+ s_real = cg._log_scale(real)
144
+ ok(s_real["events"] == n_real == 50 and s_real["exact"] is True,
145
+ "⑦完整写入日志:流式行数 %s == 物化条数 %d(口径逐位相等)"
146
+ % (s_real["events"], n_real))
147
+ os.remove(real)
148
+
149
+ # ---------- ⑧ 半截尾行边界:下界口径,最多差 1 行 ----------
150
+ bad = os.path.join(root, "_bad.jsonl")
151
+ with open(bad, "w", encoding="utf-8") as f:
152
+ f.write('{"ok": 1}\nnot-json\n{"ok": 2}') # 末行未终止 + 非法 JSON
153
+ sb = cg._log_scale(bad)
154
+ n_material = len(list(read_jsonl(bad)))
155
+ ok(sb["events"] == 2 and sb["exact"] is True,
156
+ "⑧半截尾行不计入(换行数 2 vs 物化条数 %d)——下界口径,差 1 行在量级容差内"
157
+ % n_material)
158
+ os.remove(bad)
159
+
160
+ cg.close()
161
+ cg2.close()
162
+ finally:
163
+ shutil.rmtree(tmp, ignore_errors=True)
164
+
165
+ print(f"\nhealth_scale: {PASS} pass / {FAIL} fail")
166
+ if FAILS:
167
+ for f in FAILS:
168
+ print(f" - {f}")
169
+ raise SystemExit(1 if FAIL else 0)
170
+
171
+
172
+ if __name__ == "__main__":
173
+ main()
@@ -17,6 +17,12 @@ import os
17
17
 
18
18
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
19
19
 
20
+ # Windows cmd 默认 GBK 代码页:带圈数字 ⑪⑫⑬ 等不在 GBK 内,打印即
21
+ # UnicodeEncodeError,且崩在断言之后、报告之前 —— 同一测试「因环境而异」。
22
+ # 测试自带 UTF-8 兜底,不依赖调用方记得加 -X utf8(可复现性纪律)。
23
+ if hasattr(sys.stdout, "reconfigure"):
24
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
25
+
20
26
  from md_cg.mdcg import MdCG
21
27
  from md_cg.mdcos import MdCGSecure
22
28
  from md_cg.security import DEFAULT_SENSITIVITY, Principal