@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.
- package/README.md +588 -552
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- package/docs/eval/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
- 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
- package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
- package/docs/eval/P1b2_/350/257/273/351/235/242/344/273/243/351/231/205/344/277/256/345/244/215_v1.0.md +124 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
- package/docs/eval//345/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
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v18.md +183 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v19.md +207 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v20.md +283 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/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
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
- package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
- package/dsh/cordis-patch-profile-web.example.yml +35 -0
- package/lib/bridge.js +24 -2
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +66 -0
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.d.ts +88 -0
- package/lib/init.js +302 -0
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +1 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evidence.py +585 -582
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/fsutil.py +454 -8
- package/md_cg/identity.py +3 -3
- package/md_cg/insight.py +24 -3
- package/md_cg/linkref.py +1 -1
- package/md_cg/logref.py +327 -0
- package/md_cg/mcp_server.py +4152 -3818
- package/md_cg/mdcg.py +4208 -3462
- package/md_cg/mdcos.py +4491 -4137
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +639 -575
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/review_cli.py +49 -4
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/sources.py +23 -6
- package/md_cg/srcindex.py +352 -0
- package/md_cg/stg.py +47 -3
- package/md_cg/subgraph.py +2 -2
- package/md_cg/sustain.py +1299 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_b1_auto_id_multiproc.py +277 -0
- package/md_cg/test_b1b2_write_face.py +417 -0
- package/md_cg/test_b2_sensitivity_landing.py +223 -0
- package/md_cg/test_b3_merge_keeps_content.py +576 -0
- package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
- package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
- package/md_cg/test_c3_transient_read_negative.py +793 -0
- package/md_cg/test_c8_search_rrf_gates.py +502 -0
- package/md_cg/test_ccg_form_parity.py +188 -0
- package/md_cg/test_ccgc.py +28 -3
- package/md_cg/test_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1377 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_index_crossprocess_reload.py +301 -0
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
- package/md_cg/test_n225_nonobject_load.py +1110 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +166 -0
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p9c_dedup_hints.py +276 -0
- package/md_cg/test_policy_required_ccg.py +426 -0
- package/md_cg/test_protocol.py +22 -0
- package/md_cg/test_rank_parity_score_mode.py +516 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- package/md_cg/test_review_cli_attribution.py +177 -0
- package/md_cg/test_review_cli_visibility.py +235 -0
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_v21r1_package_version.py +310 -0
- package/md_cg/test_writepipe.py +10 -4
- package/md_cg/tokens.py +225 -95
- package/md_cg/tool_face.py +4 -4
- package/md_cg/trust.py +49 -5
- package/md_cg/units.py +204 -6
- package/md_cg/weights.py +4 -4
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +8 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/cli.ts +65 -0
- package/src/hooks.ts +229 -16
- package/src/init.ts +361 -0
- package/src/lib/prompt_safety.ts +88 -1
- 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())
|