@furongjun1999/dsh-memory 0.5.1 → 0.6.1

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 (187) hide show
  1. package/README.md +588 -552
  2. package/codebuddy/CODEBUDDY.md +10 -10
  3. package/data/policy.json +27 -0
  4. package/docs/discipline/harnesses.yaml +31 -0
  5. package/docs/discipline/templates/full.md.tmpl +1 -1
  6. package/docs/discipline/templates/rules.mdc.tmpl +1 -1
  7. package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225v2_/345/217/202/350/200/203dsh-TUI_v1.0.md +248 -0
  8. package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225/346/225/210/346/236/234/351/252/214/350/257/201_v1.0.md +209 -0
  9. package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
  10. 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
  11. package/docs/eval/P1b2_/350/257/273/351/235/242/344/273/243/351/231/205/344/277/256/345/244/215_v1.0.md +124 -0
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. package/docs/eval//345/217/221/345/270/20306_/344/270/200/351/224/256/351/205/215/347/275/256/344/270/216DSH0172_v1.0.md +171 -0
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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_v18.md +183 -0
  23. 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_v19.md +207 -0
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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 +20 -0
  31. 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
  32. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -0
  33. 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
  34. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  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 +460 -460
  41. package/dsh/cordis-patch-profile-web.example.yml +35 -0
  42. package/lib/bridge.js +24 -2
  43. package/lib/cli.d.ts +3 -0
  44. package/lib/cli.js +66 -0
  45. package/lib/hooks.d.ts +31 -2
  46. package/lib/hooks.js +219 -16
  47. package/lib/init.d.ts +88 -0
  48. package/lib/init.js +302 -0
  49. package/lib/lib/prompt_safety.d.ts +52 -1
  50. package/lib/lib/prompt_safety.js +76 -1
  51. package/md_cg/audit.py +652 -379
  52. package/md_cg/bench6_arms.py +1 -1
  53. package/md_cg/bench_p0.py +1 -1
  54. package/md_cg/branches.py +2 -2
  55. package/md_cg/ccgc.py +1071 -1006
  56. package/md_cg/chain.py +1 -1
  57. package/md_cg/consistency.py +97 -9
  58. package/md_cg/consolidate.py +34 -9
  59. package/md_cg/crypto.py +59 -26
  60. package/md_cg/docindex.py +16 -1
  61. package/md_cg/evidence.py +585 -582
  62. package/md_cg/evolution.py +17 -1
  63. package/md_cg/export.py +1 -1
  64. package/md_cg/forgetting.py +309 -13
  65. package/md_cg/fsutil.py +454 -8
  66. package/md_cg/identity.py +3 -3
  67. package/md_cg/insight.py +24 -3
  68. package/md_cg/linkref.py +1 -1
  69. package/md_cg/logref.py +327 -0
  70. package/md_cg/mcp_server.py +4152 -3818
  71. package/md_cg/mdcg.py +4208 -3462
  72. package/md_cg/mdcos.py +4491 -4137
  73. package/md_cg/mreview/pipeline.py +15 -1
  74. package/md_cg/nodefile.py +639 -575
  75. package/md_cg/protect.py +158 -11
  76. package/md_cg/protocol.py +41 -2
  77. package/md_cg/provenance.py +22 -4
  78. package/md_cg/reach.py +4 -4
  79. package/md_cg/readcache.py +76 -15
  80. package/md_cg/reconcile.py +1 -1
  81. package/md_cg/refindex.py +246 -37
  82. package/md_cg/refine.py +1 -1
  83. package/md_cg/review_cli.py +49 -4
  84. package/md_cg/routing.py +32 -5
  85. package/md_cg/run_tests.py +17 -0
  86. package/md_cg/scrub.py +30 -3
  87. package/md_cg/security.py +47 -1
  88. package/md_cg/self_state.py +5 -5
  89. package/md_cg/sources.py +23 -6
  90. package/md_cg/srcindex.py +352 -0
  91. package/md_cg/stg.py +47 -3
  92. package/md_cg/subgraph.py +2 -2
  93. package/md_cg/sustain.py +1299 -1168
  94. package/md_cg/tasks.py +469 -470
  95. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  96. package/md_cg/test_b1b2_write_face.py +417 -0
  97. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  98. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  99. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  100. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  101. package/md_cg/test_c3_transient_read_negative.py +793 -0
  102. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  103. package/md_cg/test_ccg_form_parity.py +188 -0
  104. package/md_cg/test_ccgc.py +28 -3
  105. package/md_cg/test_govern_directread.py +17 -4
  106. package/md_cg/test_h2_session_view_norm.py +233 -0
  107. package/md_cg/test_h4_sustain_snapshot.py +1377 -0
  108. package/md_cg/test_hive_ingest.py +285 -285
  109. package/md_cg/test_index_crossprocess_reload.py +301 -0
  110. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  111. package/md_cg/test_issue43_default_policy.py +865 -0
  112. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  113. package/md_cg/test_linkref.py +10 -3
  114. package/md_cg/test_lock.py +2 -2
  115. package/md_cg/test_logref.py +1109 -0
  116. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  117. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  118. package/md_cg/test_mr_m2.py +15 -3
  119. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  120. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  121. package/md_cg/test_n176_link_trust.py +221 -0
  122. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  123. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  124. package/md_cg/test_n195_writepath_reload.py +283 -0
  125. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  126. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  127. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  128. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  129. package/md_cg/test_n201_proposal_visibility.py +382 -0
  130. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  131. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  132. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  133. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  134. package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
  135. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  136. package/md_cg/test_n225_nonobject_load.py +1110 -0
  137. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  138. package/md_cg/test_neg_condition_hits.py +333 -0
  139. package/md_cg/test_neg_tail_honesty.py +663 -0
  140. package/md_cg/test_none_id_write_guard.py +166 -0
  141. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  142. package/md_cg/test_p1.py +5 -5
  143. package/md_cg/test_p11_consistency.py +161 -12
  144. package/md_cg/test_p26_refindex.py +2 -2
  145. package/md_cg/test_p27_docindex.py +777 -774
  146. package/md_cg/test_p28_refcheck.py +740 -13
  147. package/md_cg/test_p29_session_ingest_export.py +2 -2
  148. package/md_cg/test_p2_mcp.py +12 -1
  149. package/md_cg/test_p30_maintain.py +33 -2
  150. package/md_cg/test_p31_insight.py +1 -0
  151. package/md_cg/test_p9c_dedup_hints.py +276 -0
  152. package/md_cg/test_policy_required_ccg.py +426 -0
  153. package/md_cg/test_protocol.py +22 -0
  154. package/md_cg/test_rank_parity_score_mode.py +516 -0
  155. package/md_cg/test_read_face_input_gates.py +363 -0
  156. package/md_cg/test_read_face_semantics.py +489 -0
  157. package/md_cg/test_recall_face_guards.py +812 -0
  158. package/md_cg/test_rejected_credential_forms.py +188 -0
  159. package/md_cg/test_rejected_redact.py +146 -0
  160. package/md_cg/test_retr_s1b.py +2 -2
  161. package/md_cg/test_retr_s5.py +20 -7
  162. package/md_cg/test_review_cli_attribution.py +177 -0
  163. package/md_cg/test_review_cli_visibility.py +235 -0
  164. package/md_cg/test_review_conformance.py +21 -3
  165. package/md_cg/test_security_audit_v21.py +2 -2
  166. package/md_cg/test_server_version.py +67 -0
  167. package/md_cg/test_srcindex.py +171 -0
  168. package/md_cg/test_tenant_env_override_warn.py +63 -50
  169. package/md_cg/test_token_lowercase_form.py +315 -0
  170. package/md_cg/test_v21r1_package_version.py +310 -0
  171. package/md_cg/test_writepipe.py +10 -4
  172. package/md_cg/tokens.py +225 -95
  173. package/md_cg/tool_face.py +4 -4
  174. package/md_cg/trust.py +49 -5
  175. package/md_cg/units.py +204 -6
  176. package/md_cg/weights.py +4 -4
  177. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  178. package/md_cg/writelimit.py +42 -8
  179. package/md_cg/writepipe.py +651 -554
  180. package/package.json +8 -2
  181. package/skills/plugin.json +1 -1
  182. package/src/bridge.ts +25 -2
  183. package/src/cli.ts +65 -0
  184. package/src/hooks.ts +229 -16
  185. package/src/init.ts +361 -0
  186. package/src/lib/prompt_safety.ts +88 -1
  187. package/zcode/AGENTS.md +10 -10
@@ -0,0 +1,238 @@
1
+ # -*- coding: utf-8 -*-
2
+ # 功能名:B4 分片目录缺失自愈的**独立**判别力守卫(长驻进程内 `_index_log/` 被删后 append 必自愈、必留痕、read_all 不回归)
3
+ # 生效条件:`md_cg/fsutil.py` 的 `ShardedLog`(`__init__` 首建 / `append` 打开前复查)经唯一实现点 `ensure_shard_dir` 保证分片目录存在时;本守卫以真实 `MdCG` 实例在沙箱库根上端到端跑,**不读在役库**。
4
+ # 子功能:①端到端自愈(写 1 条 → rmtree `_index_log/` → 再写 3 条全成功 + 目录重建 + 分片记录与读面可见);②可观测(记账 + 有界样本 + stderr 告警,重建失败也记账);③`read_all` 既有语义(非目录 → `[]`);④单点(AST:全文件 `ensure_shard_dir` 只有 1 份定义,`ShardedLog` 内 2 处调用点全是委托、无裸 makedirs 副本)
5
+ # 执行:python -X utf8 -m md_cg.test_b4_shard_dir_selfheal_guard(`scripts/run_tests.py md_cg` 按 `md_cg/test_*.py` 自动收集)
6
+ # 验证方式:本文件三段(A 自愈 / B 留痕 / C 既有语义 + D 单点 + E 失败结构化);定点变异自证 = `%TEMP%\mutate_b4_guard.py`(M1 抽掉 `append` 的 `ensure_shard_dir` 调用 → 必红;M2 抽掉 `note_shard_dir_rebuild` 调用 → 必红;逐变异 finally 回写原文)
7
+ # 不适用条件:①只在「句柄为 None 的重开点」复查——已持有打开句柄期间目录被删不在本守卫面内(Windows 上被打开的文件无法删除,生产写点 `MdCG.flush` 每批写完即 close ⇒ 必经重开点);②跨进程并发唯一性/自愈不在本面;③不覆盖 MCP 出口渲染(另有 test_b4_shard_dir_selfheal.py 的 E 组)
8
+ """B4 守卫(独立复核版):分片目录缺失自愈 —— 判别力守卫 + 定点变异自证。
9
+
10
+ 病灶(修复前,长驻进程内 `_index_log/` 被删):
11
+ ``ShardedLog`` 只在 ``__init__`` 里 ``makedirs``,``append`` 首次 ``open`` 前不
12
+ 复查目录 ⇒ 目录被删后**每次** append 都在 ``open`` 抛 ``FileNotFoundError``、
13
+ 目录永不重建、索引静默落后于已落盘的正文 ``.md``。
14
+
15
+ 修复(单点):``md_cg/fsutil.py:663`` ``ensure_shard_dir`` 是分片目录存在性的
16
+ **唯一**实现,两处调用点均为委托——``fsutil.py:706``(``__init__``, "init",首建
17
+ 不记账)与 ``fsutil.py:724``(``append``, "append",打开前复查、记账 + 告警)。
18
+
19
+ 本文件的三条硬断言(对应派单 ①②③)与两条加固(单点 / 失败结构化):
20
+ A 自愈端到端:删目录后再写三条全成功,目录重建、分片记录与读面都可见
21
+ B 容忍 ≠ 静默:自愈被记账(+1)、样本指向本库、stderr 有告警
22
+ C 既有语义:``read_all`` 对不存在的目录 / 非目录路径仍返回 ``[]``
23
+ D 单点:``ensure_shard_dir`` 只有一份定义,``ShardedLog`` 内只有两处委托调用
24
+ E 重建失败:结构化 ``ShardDirError``(code 可机读)且失败也记账 + 告警
25
+
26
+ 沙箱(派单硬约束 1):临时根 ``tempfile.mkdtemp``;``MDCG_ROOT`` /
27
+ ``MDCG_AUX_ROOT`` / ``MDCG_MASTER_KEY`` 全在沙箱内(且必须在任何 md_cg 子模块
28
+ import 之前设好——``crypto`` 的密钥路径在 import 期求值);跑完 rmtree,
29
+ **绝不碰在役库与在役 serve**。
30
+ """
31
+ import ast
32
+ import contextlib
33
+ import io
34
+ import os
35
+ import shutil
36
+ import sys
37
+ import tempfile
38
+
39
+ # 沙箱必须在**任何** md_cg 子模块 import 之前设好。
40
+ _SANDBOX = tempfile.mkdtemp(prefix="b4_guard_sandbox_")
41
+ os.environ["MDCG_AUX_ROOT"] = _SANDBOX
42
+ os.environ["MDCG_ROOT"] = os.path.join(_SANDBOX, "root")
43
+ os.environ["MDCG_MASTER_KEY"] = os.urandom(32).hex()
44
+ os.environ.pop("MDCG_POLICY_FILE", None)
45
+
46
+ from . import fsutil, mdcg # noqa: E402
47
+
48
+ _FSUTIL_SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)),
49
+ "fsutil.py")
50
+
51
+ _OK = []
52
+ _BAD = []
53
+
54
+
55
+ def _check(name, cond, detail=""):
56
+ """一条断言 = 一个可定点变异的判据;失红时原样打印断言名与读数。"""
57
+ if cond:
58
+ _OK.append(name)
59
+ print(" ok %s" % name)
60
+ else:
61
+ _BAD.append(name)
62
+ print(" RED %s [%s]" % (name, detail))
63
+
64
+
65
+ def main():
66
+ log_dir = None
67
+ try:
68
+ # ================= A 自愈端到端(派单 ①) =================
69
+ print("== A 自愈端到端:删掉库根 _index_log/ 后再写三条 ==")
70
+ root = os.path.join(_SANDBOX, "lib_a")
71
+ cg = mdcg.MdCG(root, autoflush=1) # autoflush=1 ⇒ 每写必 flush
72
+ log_dir = os.path.join(root, "_index_log")
73
+
74
+ fsutil.reset_shard_dir_stats()
75
+ err1 = None
76
+ try:
77
+ cg.add("b4g_n1", "B4G 第一条(删目录前)", layer="knowledge")
78
+ except Exception as exc: # noqa: BLE001
79
+ err1 = "%s: %s" % (type(exc).__name__, exc)
80
+ _check("B4G-A1 首建:写第一条成功且分片目录由 __init__ 建出",
81
+ err1 is None and os.path.isdir(log_dir),
82
+ repr(err1) + " isdir=%s" % os.path.isdir(log_dir))
83
+ # 首建是正常路径,不该记账(记账面要能区分「病态自愈」与「建库」)
84
+ _check("B4G-A2 首建不记账(where=init ⇒ 重建计数仍 0)",
85
+ fsutil.shard_dir_stats()[0] == 0,
86
+ "rebuilds=%d" % fsutil.shard_dir_stats()[0])
87
+ # 长驻进程形态:flush 已 close 句柄但**保留** ShardedLog 实例
88
+ shard_before = getattr(getattr(cg, "_log", None), "path", None)
89
+ _check("B4G-A3 前置:ShardedLog 实例在(句柄已 close、实例仍在)",
90
+ shard_before is not None
91
+ and os.path.isfile(shard_before)
92
+ and getattr(cg._log, "_fh", "x") is None,
93
+ "path=%r" % (shard_before,))
94
+
95
+ shutil.rmtree(log_dir)
96
+ _check("B4G-A4 前置:库根 _index_log/ 确已删除",
97
+ not os.path.isdir(log_dir), "isdir=%s" % os.path.isdir(log_dir))
98
+
99
+ fsutil.reset_shard_dir_stats()
100
+ stderr_buf = io.StringIO()
101
+ errs = []
102
+ with contextlib.redirect_stderr(stderr_buf):
103
+ for i, nid in enumerate(("b4g_n2", "b4g_n3", "b4g_n4"), 1):
104
+ try:
105
+ cg.add(nid, "B4G 删目录后第%d条(标记%d)" % (i, i),
106
+ layer="knowledge")
107
+ except Exception as exc: # noqa: BLE001
108
+ errs.append("%s → %s: %s" % (nid, type(exc).__name__, exc))
109
+ _check("B4G-A5 删目录后再写三条:全部成功(无异常)",
110
+ not errs, "; ".join(errs))
111
+ _check("B4G-A6 分片目录被重建",
112
+ os.path.isdir(log_dir), "isdir=%s" % os.path.isdir(log_dir))
113
+ _check("B4G-A7 重建后分片路径照常(仍是 self._log.path 同一分片名)",
114
+ bool(shard_before) and os.path.isfile(shard_before),
115
+ "path=%r isfile=%s" % (shard_before,
116
+ os.path.isfile(shard_before)))
117
+
118
+ # ---- 读面:分片重放面(read_all)与节点读面(新实例 get) ----
119
+ recs = fsutil.ShardedLog.read_all(log_dir)
120
+ got_ids = set(r.get("id") for r in recs if isinstance(r, dict))
121
+ want = {"b4g_n2", "b4g_n3", "b4g_n4"}
122
+ _check("B4G-A8 删目录后三条记录真落进重建的分片(read_all 含 n2/n3/n4)",
123
+ want.issubset(got_ids), "ids=%s" % sorted(got_ids))
124
+
125
+ cg2 = mdcg.MdCG(root, autoflush=1) # 全新实例 = 后续读面
126
+ got = cg2.get("b4g_n4")
127
+ _check("B4G-A9 后续读面读到新节点(新实例 get 返回正文含标记3)",
128
+ isinstance(got, dict) and "标记3" in (got.get("content") or ""),
129
+ repr(got and got.get("content"))[:80])
130
+
131
+ # ================= B 容忍 ≠ 静默(派单 ②) =================
132
+ print("== B 自愈可观测:记账 / 样本 / stderr 告警 ==")
133
+ rebuilds, samples, failures, _fsamples = fsutil.shard_dir_stats()
134
+ _check("B4G-B1 自愈被记账(reset 后重建计数恰 +1)",
135
+ rebuilds == 1, "rebuilds=%d failures=%d" % (rebuilds, failures))
136
+ abs_dir = os.path.abspath(log_dir)
137
+ _check("B4G-B2 记账样本指向本库分片目录且调用点标签为 append",
138
+ any(p == abs_dir and w == "append" for p, w in samples),
139
+ "samples=%r" % (samples,))
140
+ err_text = stderr_buf.getvalue()
141
+ _check("B4G-B3 自愈有 stderr 告警(不是静默吞掉)",
142
+ "分片目录缺失,已重建" in err_text
143
+ and "调用点 append" in err_text
144
+ and os.path.basename(log_dir) in err_text,
145
+ repr(err_text[:120]))
146
+
147
+ # ================= C read_all 既有语义不回归(派单 ③) =================
148
+ print("== C read_all 对非目录仍返回空列表(既有语义) ==")
149
+ missing = os.path.join(_SANDBOX, "no_such_shard_dir")
150
+ _check("B4G-C1 read_all(不存在的目录) == []",
151
+ fsutil.ShardedLog.read_all(missing) == [],
152
+ repr(fsutil.ShardedLog.read_all(missing))[:60])
153
+ fpath = os.path.join(_SANDBOX, "shard_is_a_file.txt")
154
+ with open(fpath, "w", encoding="utf-8") as fh:
155
+ fh.write("占位:路径是文件不是目录\n")
156
+ _check("B4G-C2 read_all(路径是文件) == []",
157
+ fsutil.ShardedLog.read_all(fpath) == [],
158
+ repr(fsutil.ShardedLog.read_all(fpath))[:60])
159
+
160
+ # ================= D 单点(判据只有一份实现,第二处必须委托) =================
161
+ print("== D 单点:ensure_shard_dir 只有一份定义 + 两处委托调用 ==")
162
+ with open(_FSUTIL_SRC, encoding="utf-8") as fh:
163
+ src = fh.read()
164
+ tree = ast.parse(src)
165
+ defs = [n for n in tree.body
166
+ if isinstance(n, ast.FunctionDef) and n.name == "ensure_shard_dir"]
167
+ _check("B4G-D1 全 fsutil.py 内 `def ensure_shard_dir` 只有 1 份",
168
+ len(defs) == 1, "defs=%d" % len(defs))
169
+
170
+ cls = [n for n in tree.body
171
+ if isinstance(n, ast.ClassDef) and n.name == "ShardedLog"]
172
+ calls = {}
173
+ naked_mkdir = []
174
+ if cls:
175
+ for m in cls[0].body:
176
+ if not isinstance(m, ast.FunctionDef):
177
+ continue
178
+ for node in ast.walk(m):
179
+ if isinstance(node, ast.Call):
180
+ fn = node.func
181
+ if isinstance(fn, ast.Name) and fn.id == "ensure_shard_dir":
182
+ where = None
183
+ if len(node.args) >= 2 and isinstance(
184
+ node.args[1], ast.Constant):
185
+ where = node.args[1].value
186
+ calls.setdefault(m.name, []).append(where)
187
+ if isinstance(fn, ast.Attribute) and fn.attr in (
188
+ "makedirs", "mkdir"):
189
+ naked_mkdir.append("%s:%s" % (m.name, fn.attr))
190
+ _check("B4G-D2 ShardedLog.__init__ 委托 ensure_shard_dir(...,\"init\")",
191
+ "init" in calls.get("__init__", []), "calls=%r" % (calls,))
192
+ _check("B4G-D3 ShardedLog.append 委托 ensure_shard_dir(...,\"append\")",
193
+ "append" in calls.get("append", []), "calls=%r" % (calls,))
194
+ _check("B4G-D4 ShardedLog 内无裸 makedirs/mkdir 副本",
195
+ not naked_mkdir, "naked=%r" % (naked_mkdir,))
196
+
197
+ # ================= E 重建失败:结构化 + 也留痕(加固) =================
198
+ print("== E 重建失败:结构化 ShardDirError(code 可机读)+ 失败也记账 ==")
199
+ e_dir = os.path.join(_SANDBOX, "lib_e", "_index_log")
200
+ os.makedirs(os.path.dirname(e_dir), exist_ok=True)
201
+ elog = fsutil.ShardedLog(e_dir) # init 首建
202
+ elog.append({"id": "b4g_e1"})
203
+ elog.close() # 复现「句柄为 None 的重开点」
204
+ shutil.rmtree(e_dir)
205
+ with open(e_dir, "w", encoding="utf-8") as fh:
206
+ fh.write("占位:用文件占住分片目录路径\n") # makedirs 必失败
207
+ f_before = fsutil.shard_dir_stats()[2]
208
+ ebuf = io.StringIO()
209
+ eerr = None
210
+ with contextlib.redirect_stderr(ebuf):
211
+ try:
212
+ elog.append({"id": "b4g_e2"})
213
+ except Exception as exc: # noqa: BLE001
214
+ eerr = exc
215
+ _check("B4G-E1 重建失败抛结构化 ShardDirError(不是裸 OSError 族)",
216
+ isinstance(eerr, fsutil.ShardDirError),
217
+ "%s: %s" % (type(eerr).__name__, str(eerr)[:80]))
218
+ _check("B4G-E2 仍是 OSError 子类 + code 可机读 + message 首字段含 [code]",
219
+ isinstance(eerr, OSError)
220
+ and getattr(eerr, "code", None) == fsutil.SHARD_DIR_ERR_CODE
221
+ and str(eerr).startswith("[%s]" % fsutil.SHARD_DIR_ERR_CODE),
222
+ "code=%r" % (getattr(eerr, "code", None),))
223
+ f_after = fsutil.shard_dir_stats()[2]
224
+ _check("B4G-E3 重建失败也记账 + 告警(异常可能被上层兜底吞掉)",
225
+ f_after == f_before + 1 and "重建失败" in ebuf.getvalue(),
226
+ "failures %d→%d stderr=%r" % (f_before, f_after,
227
+ ebuf.getvalue()[:100]))
228
+
229
+ finally:
230
+ shutil.rmtree(_SANDBOX, ignore_errors=True)
231
+
232
+ print("\n==== B4 独立守卫:%d 通过%s ====" % (
233
+ len(_OK), ",%d 失败:%s" % (len(_BAD), "; ".join(_BAD)) if _BAD else ""))
234
+ return 1 if _BAD else 0
235
+
236
+
237
+ if __name__ == "__main__":
238
+ sys.exit(main())