@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,793 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""C-3 守卫 · 瞬态读失败不得固化为「节点从检索面消失」(FI-M02 / N134)。
|
|
3
|
+
|
|
4
|
+
来源:设计者裁定的 C-3 契约(瞬态读失败被负缓存固化)。
|
|
5
|
+
|
|
6
|
+
缺陷(N134,docs/eval/缺陷挖掘_自主迭代_v16.md:86):`MdCG._read` 把 OSError
|
|
7
|
+
(含**瞬态**失败:独占句柄 / 资源剥夺)与「文件不存在 / 越界」一律归成
|
|
8
|
+
`(None, None)`,而 `readcache._cached` 对任何返回值——含 `(None, None)`——一律
|
|
9
|
+
入缓存,且该 path 无写事件时 `_fresh` 判定恒真 ⇒ 一次瞬态 OS 失败被固化成
|
|
10
|
+
「该节点从检索面永久消失,直到进程重启或该 path 再写盘」(cache 条目字面
|
|
11
|
+
`(gen, (None, None))`);而 `cg.get` 直读不走缓存照常可读 ⇒「get 能读、
|
|
12
|
+
search 搜不到」撕裂(P1 fail-closed 缺席 + T4 静默损伤)。
|
|
13
|
+
|
|
14
|
+
契约四项 ↔ 断言组:
|
|
15
|
+
① 读路径区分「瞬时/终态」且**判别点单点**(缓存层不得再猜一遍)
|
|
16
|
+
—— G5(源码结构:判别函数唯一、缓存层不出现异常类型判别)
|
|
17
|
+
+ G1(三态标签实测:瞬时 → READ_FAIL_TRANSIENT;真缺 → 终态)
|
|
18
|
+
② 读失败不得以「新鲜」身份固化(本实现取**不入缓存**,见 readcache.install
|
|
19
|
+
文档串的取舍论证)—— G1(cache 无条目 / 释放后**首次**查询即命中)
|
|
20
|
+
+ G2(无判别面载体的保守侧:宁可重读,不可固化可能是瞬时的缺失)
|
|
21
|
+
③ 不得静默:模块级计数 + 有界样本、该记账面可被守卫读取 —— G6
|
|
22
|
+
④ 成功路径对外返回值语义**逐位不变**(独立 oracle 对照,不靠推理)
|
|
23
|
+
—— G3(缓存开 / 缓存关 / 变异体三路逐位一致)+ G4(盘上真值独立 oracle)
|
|
24
|
+
|
|
25
|
+
Windows 实测项:G7(与 chaos 用例 FI-M02 同款 ctypes.WinDLL 独占句柄置景:
|
|
26
|
+
持锁期 miss、释放后命中)。非 Windows 该组 SKIP 且**不计入断言数**(诚实声明:
|
|
27
|
+
本平台不可实测),缺口本体的跨平台判据由 G1 的 OSError 注入承担。
|
|
28
|
+
|
|
29
|
+
运行:
|
|
30
|
+
python -X utf8 -m md_cg.test_c3_transient_read_negative # 正向
|
|
31
|
+
python -X utf8 -m md_cg.test_c3_transient_read_negative --mutate # 定点变异自证
|
|
32
|
+
python -X utf8 -m md_cg.test_c3_transient_read_negative --mutate --list # 只列变异表
|
|
33
|
+
|
|
34
|
+
退出码(fail-closed):0 = 全绿;1 = 有断言失败 / 变异未按预期转红;
|
|
35
|
+
2 = **ANCHOR-MISS**(变异锚点在当前源码里找不到,说明实现改了却没同步本表——
|
|
36
|
+
本表就是判据,锚点漂移必须硬失败,不得静默跳过)。
|
|
37
|
+
|
|
38
|
+
**基线纪律**:本守卫**不以 git HEAD(或任何提交快照)为基线源**——所有
|
|
39
|
+
「修复前」形态都由**在当前工作区源码上做定点文本变异**得到(`_MUTATIONS`),
|
|
40
|
+
基线随代码一起走、不随提交漂移(本仓已有「基线绑提交即失效」的教训)。
|
|
41
|
+
变异表每条记 `expect_red`,实际红数不等即判失败。
|
|
42
|
+
"""
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import builtins
|
|
46
|
+
import contextlib
|
|
47
|
+
import io
|
|
48
|
+
import json
|
|
49
|
+
import os
|
|
50
|
+
import shutil
|
|
51
|
+
import sys
|
|
52
|
+
import tempfile
|
|
53
|
+
import time
|
|
54
|
+
import types
|
|
55
|
+
|
|
56
|
+
# ── 隔离前置:必须在 import md_cg 子模块**之前**(tokens/theory 等在模块级把
|
|
57
|
+
# aux_root() 冻成常量)——否则会指到真实 ~/.mdcg 并在那里建密钥/令牌库。
|
|
58
|
+
# 与 md_cg/test_opt_batch1_md_cg.py 同款前置(同一纪律)。 ──
|
|
59
|
+
_TMP = tempfile.mkdtemp(prefix="c3trn_")
|
|
60
|
+
os.environ["MDCG_AUX_ROOT"] = os.path.join(_TMP, "auxroot")
|
|
61
|
+
os.environ["MDCG_MASTER_KEY"] = "ab" * 32 # 哑主密钥(绝不触真实密钥面)
|
|
62
|
+
os.environ["MDCG_TOKEN_FILE"] = os.path.join(_TMP, "tokens.json")
|
|
63
|
+
os.environ["MDCG_ROOT"] = os.path.join(_TMP, "cgroot")
|
|
64
|
+
for _k in ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_CLEARANCE", "MDCG_SESSION",
|
|
65
|
+
"DSH_SESSION_ID", "MDCG_LEGACY_ENV_AUTH", "MDCG_LEGACY_ENV_ADMIN",
|
|
66
|
+
"MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_STATE_ROOT",
|
|
67
|
+
"MDCG_DATA_ROOT", "MDCG_TENANT_REGISTRY", "MDCG_VERIFIER_MODULES"):
|
|
68
|
+
os.environ.pop(_k, None)
|
|
69
|
+
|
|
70
|
+
from . import fsutil, mdcg as mdcg_mod, readcache # noqa: E402
|
|
71
|
+
from .mdcos import MdCGSecure # noqa: E402
|
|
72
|
+
from .security import Principal # noqa: E402
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
76
|
+
except AttributeError:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
80
|
+
_REPO = os.path.dirname(_HERE)
|
|
81
|
+
|
|
82
|
+
# 被测模块的**可替换引用**:变异模式会把其中一项换成 exec 出来的变异副本,
|
|
83
|
+
# 断言组一律经本表取模块(不直接闭包捕获原模块),否则变异对断言不可见。
|
|
84
|
+
MOD = {"readcache": readcache}
|
|
85
|
+
|
|
86
|
+
_PASS: list = []
|
|
87
|
+
_FAIL: list = []
|
|
88
|
+
_SKIP: list = []
|
|
89
|
+
_OPEN: list = []
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def ok(cond, msg, extra=""):
|
|
93
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
94
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
95
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
96
|
+
return bool(cond)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def skip(msg):
|
|
100
|
+
_SKIP.append(msg)
|
|
101
|
+
print(" SKIP " + msg)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _cleanup(path) -> bool:
|
|
105
|
+
import gc
|
|
106
|
+
for _ in range(6):
|
|
107
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
108
|
+
if not os.path.exists(path):
|
|
109
|
+
return True
|
|
110
|
+
gc.collect()
|
|
111
|
+
time.sleep(0.2)
|
|
112
|
+
return not os.path.exists(path)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _princ(role="designer", clearance="secret", can_admin=True):
|
|
116
|
+
from . import tokens
|
|
117
|
+
spec = tokens.role_spec(role)
|
|
118
|
+
return Principal(tenant="default", actor="c3trn-" + role,
|
|
119
|
+
clearance=clearance, can_write=bool(spec["can_write"]),
|
|
120
|
+
can_admin=can_admin, role=role,
|
|
121
|
+
layers_allow=spec["layers_allow"],
|
|
122
|
+
ops_allow=spec["ops_allow"], auth_mode="test")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _secured(tag: str):
|
|
126
|
+
"""建一个临时库 + designer 身份;返回 (cg, tmpdir)。"""
|
|
127
|
+
d = tempfile.mkdtemp(prefix="c3trn_%s_" % tag)
|
|
128
|
+
cg = MdCGSecure(os.path.join(d, "root"), principal=_princ())
|
|
129
|
+
_OPEN.append(cg)
|
|
130
|
+
return cg, d
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _close_all():
|
|
134
|
+
for cg in _OPEN:
|
|
135
|
+
try:
|
|
136
|
+
cg.close()
|
|
137
|
+
except Exception: # noqa: BLE001 —— 清理面不得再抛
|
|
138
|
+
pass
|
|
139
|
+
_OPEN.clear()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class _OpenGate:
|
|
143
|
+
"""读闸门:接管 `md_cg.mdcg` 模块内的 `open`(模块全局名遮蔽内建),
|
|
144
|
+
对指定 basename 抛注入异常,其余原样委托;并对每次 open 计数。
|
|
145
|
+
|
|
146
|
+
为什么改模块全局而不是 builtins:`_read_status` / `get` 里的 `open` 是
|
|
147
|
+
mdcg 模块内的全局名查找——改这一处只影响 mdcg 自己的文件打开面,不波及其他
|
|
148
|
+
库(json/临时件)也不会漏到进程外。**注入面就是真实的 `except OSError`**:
|
|
149
|
+
抛出的 PermissionError(13) 与 Windows 共享冲突(WinError 32)/ 权限剥夺
|
|
150
|
+
在判别面上同类(均非 FileNotFoundError ⇒ 瞬时)。
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, exc=None):
|
|
154
|
+
self._real = builtins.open
|
|
155
|
+
self._failing: set = set()
|
|
156
|
+
self._exc = exc or PermissionError(13, "sharing violation")
|
|
157
|
+
self.counts: dict = {}
|
|
158
|
+
|
|
159
|
+
def install(self):
|
|
160
|
+
mdcg_mod.open = self._gate
|
|
161
|
+
|
|
162
|
+
def remove(self):
|
|
163
|
+
if getattr(mdcg_mod, "open", None) is self._gate:
|
|
164
|
+
del mdcg_mod.open
|
|
165
|
+
|
|
166
|
+
def _gate(self, file, *a, **k):
|
|
167
|
+
name = os.path.basename(str(file))
|
|
168
|
+
self.counts[name] = self.counts.get(name, 0) + 1
|
|
169
|
+
if name in self._failing:
|
|
170
|
+
raise self._exc
|
|
171
|
+
return self._real(file, *a, **k)
|
|
172
|
+
|
|
173
|
+
def fail(self, *paths):
|
|
174
|
+
self._failing = {os.path.basename(p) for p in paths}
|
|
175
|
+
|
|
176
|
+
def release(self):
|
|
177
|
+
self._failing = set()
|
|
178
|
+
|
|
179
|
+
def md_opens(self) -> dict:
|
|
180
|
+
return {k: v for k, v in self.counts.items() if k.endswith(".md")}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ════════════════════════════════════════════════ 无故障路径的独立 oracle
|
|
184
|
+
# 标记词取**双字中文**:bigram 面下彼此零重叠(实测每个标记只命中自己的节点),
|
|
185
|
+
# 拉丁串(marker01zz 之类)会因 bigram 共享("er"/"zz")互串,不能当独立判据。
|
|
186
|
+
_NODES = (
|
|
187
|
+
("c3_n01", "镜湖 苹果 共用词"),
|
|
188
|
+
("c3_n02", "霜桥 香蕉 共用词"),
|
|
189
|
+
("c3_n03", "赤鲸 橘子 共用词"),
|
|
190
|
+
("c3_n04", "寒鸦 葡萄 共用词"),
|
|
191
|
+
("c3_n05", "碧玺 西瓜 共用词"),
|
|
192
|
+
("c3_n06", "苍梧 柠檬 共用词"),
|
|
193
|
+
("c3_n07", "丹枫 樱桃 共用词"),
|
|
194
|
+
("c3_n08", "青鸾 蜜桃 共用词"),
|
|
195
|
+
)
|
|
196
|
+
# 独立性:期望命中集合由**构造**给出(每个标记只出现在一个节点里),
|
|
197
|
+
# 不来自被测代码的任何输出——这就是本组 oracle 的「独立」处。
|
|
198
|
+
_EXPECT_HITS = {
|
|
199
|
+
"镜湖": ["c3_n01"],
|
|
200
|
+
"霜桥": ["c3_n02"],
|
|
201
|
+
"碧玺": ["c3_n05"],
|
|
202
|
+
"共用词": [n[0] for n in _NODES],
|
|
203
|
+
}
|
|
204
|
+
_BATTERY = tuple(_EXPECT_HITS)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _build_lib(root):
|
|
208
|
+
cg = MdCGSecure(root, principal=_princ())
|
|
209
|
+
for nid, body in _NODES:
|
|
210
|
+
cg.add(nid, "# 功能名:C-3 夹具 %s\n# 正文:%s\n" % (nid, body),
|
|
211
|
+
layer="knowledge", importance=0.5)
|
|
212
|
+
cg.flush()
|
|
213
|
+
return cg
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _snap(cg):
|
|
217
|
+
"""无故障路径的可比快照:查询结果(id / 分数 / 资格态 / meta 关键项)
|
|
218
|
+
+ 每节点 get 全量。`record=False` 保证查询本身零写(三次对照纯读)。"""
|
|
219
|
+
out = []
|
|
220
|
+
for q in _BATTERY:
|
|
221
|
+
res, meta = cg.search(q, record=False)
|
|
222
|
+
out.append({
|
|
223
|
+
"q": q,
|
|
224
|
+
"ids": [r[0].get("id") for r in res],
|
|
225
|
+
"scores": [repr(r[1]) for r in res],
|
|
226
|
+
"states": [(r[2] or {}).get("state") if isinstance(r[2], dict)
|
|
227
|
+
else r[2] for r in res],
|
|
228
|
+
"meta": {k: meta.get(k) for k in
|
|
229
|
+
("tier", "scanned", "candidates", "covered_neg")},
|
|
230
|
+
})
|
|
231
|
+
for nid in sorted(cg.index["nodes"]):
|
|
232
|
+
g = cg.get(nid)
|
|
233
|
+
out.append({"id": nid,
|
|
234
|
+
"fm": g["frontmatter"] if g else None,
|
|
235
|
+
"content": g["content"] if g else None})
|
|
236
|
+
return json.dumps(out, ensure_ascii=False, sort_keys=True, default=str)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ================================================================ G0 前置自证
|
|
240
|
+
def g0():
|
|
241
|
+
print("== G0 隔离与前置自证 ==")
|
|
242
|
+
ok(readcache.enabled(),
|
|
243
|
+
"G0a 读缓存默认开(C-3 的成立前提:未设 MDCG_READ_CACHE 时 enabled() 为真)")
|
|
244
|
+
from . import tokens
|
|
245
|
+
ok(os.path.abspath(tokens.token_file()).lower().startswith(_TMP.lower()),
|
|
246
|
+
"G0b 令牌库落在守卫临时目录(绝不触真实凭据)",
|
|
247
|
+
"tok=%s" % tokens.token_file())
|
|
248
|
+
# 基线源自证(反「基线绑提交」):变异加载器直接 open 当前工作区源文件,
|
|
249
|
+
# 读到的就是它此刻在盘上的内容——不含任何提交快照读取面。
|
|
250
|
+
_rel = os.path.join("md_cg", "readcache.py")
|
|
251
|
+
_live = open(os.path.join(_REPO, _rel), encoding="utf-8").read()
|
|
252
|
+
_mut, _src = _load_mutated(_rel, _MUT_ADMIT_OLD, _MUT_ADMIT_NEW)
|
|
253
|
+
ok(_mut is not None and _src == _live,
|
|
254
|
+
"G0c 变异基线源 = 当前工作区源码(非 git HEAD/提交快照)",
|
|
255
|
+
"lead=%s" % (_mut is not None))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# =========================================== G1 主判据(跨平台 OSError 注入)
|
|
259
|
+
def g1():
|
|
260
|
+
print("== G1 瞬态读失败:不入缓存 + 释放即命中(跨平台注入)==")
|
|
261
|
+
cg, d = _secured("g1")
|
|
262
|
+
gate = _OpenGate()
|
|
263
|
+
try:
|
|
264
|
+
for nid, body in _NODES[:3]:
|
|
265
|
+
cg.add(nid, "# 功能名:C-3 主判据 %s\n# 正文:%s\n" % (nid, body),
|
|
266
|
+
layer="knowledge", importance=0.5)
|
|
267
|
+
cg.flush()
|
|
268
|
+
MOD["readcache"].install(cg)
|
|
269
|
+
gate.install()
|
|
270
|
+
|
|
271
|
+
e1 = cg.index["nodes"]["c3_n01"]
|
|
272
|
+
p1 = cg._node_disk_path(e1)
|
|
273
|
+
key1 = e1["path"]
|
|
274
|
+
|
|
275
|
+
# 前置自证(防夹具空转):无故障时冷缓存查询命中全部 3 条
|
|
276
|
+
MOD["readcache"].clear(cg)
|
|
277
|
+
pre = _ids(cg.search("共用词"))
|
|
278
|
+
ok(sorted(pre) == ["c3_n01", "c3_n02", "c3_n03"],
|
|
279
|
+
"G1a 前置:无故障时查询命中全部 3 节点(查询面本身有效)", pre)
|
|
280
|
+
ok(cg._read_cache.get(key1) is not None,
|
|
281
|
+
"G1a2 前置:成功结果已入缓存(该 path 有缓存条目)",
|
|
282
|
+
"cache=%r" % (cg._read_cache.get(key1),))
|
|
283
|
+
|
|
284
|
+
# ── 注入瞬态读失败(只对 c3_n01 的文件)──
|
|
285
|
+
# 每次注入查询前 clear:确保真的走读路径(缓存命中会掩盖注入)
|
|
286
|
+
fsutil.reset_transient_read_stats()
|
|
287
|
+
gate.fail(p1)
|
|
288
|
+
MOD["readcache"].clear(cg)
|
|
289
|
+
ids_l1 = _ids(cg.search("共用词"))
|
|
290
|
+
ids_l2 = _ids(cg.search("共用词"))
|
|
291
|
+
gate.counts.clear() # 只看稳态:缓存已装齐
|
|
292
|
+
ids_l3 = _ids(cg.search("共用词"))
|
|
293
|
+
opens_steady = gate.md_opens()
|
|
294
|
+
entry_l = cg._read_cache.get(key1)
|
|
295
|
+
stat_l = fsutil.transient_read_stats()
|
|
296
|
+
|
|
297
|
+
ok(ids_l1 == ["c3_n02", "c3_n03"] and ids_l2 == ["c3_n02", "c3_n03"]
|
|
298
|
+
and ids_l3 == ["c3_n02", "c3_n03"],
|
|
299
|
+
"G1b 读失败 fail-closed:不可读节点不参与检索(不泄漏半读内容),"
|
|
300
|
+
"同池其余节点不受累(可隔离)",
|
|
301
|
+
"locked_hits=%s/%s/%s" % (ids_l1, ids_l2, ids_l3))
|
|
302
|
+
ok(entry_l is None,
|
|
303
|
+
"G1c 【②】负结果**不入缓存**:该 path 无缓存条目(修前为字面 "
|
|
304
|
+
"(gen,(None,None)) 的固化条目——本项即定点变异红项的靶心)",
|
|
305
|
+
"cache[%r]=%r" % (key1, entry_l))
|
|
306
|
+
ok(stat_l[0] >= 3 and any(os.path.basename(s[0]) == "c3_n01.md"
|
|
307
|
+
and s[1] == "PermissionError"
|
|
308
|
+
and s[2] == 13 for s in stat_l[1]),
|
|
309
|
+
"G1d 【③】不得静默:瞬时读失败计入模块级计数且样本含 (path, 异常类型,"
|
|
310
|
+
" errno)(守卫可读的记账面)",
|
|
311
|
+
"stats=%r" % (stat_l,))
|
|
312
|
+
ok(opens_steady == {"c3_n01.md": 1},
|
|
313
|
+
"G1e 【②代价面】重试有界(稳态一次查询):失败 path 恰 1 次 open,"
|
|
314
|
+
"成功节点零重读(缓存仍命中)——不是整池重读的性能悬崖",
|
|
315
|
+
"opens=%r" % (opens_steady,))
|
|
316
|
+
|
|
317
|
+
# ── 三态标签直接实测(判别点的产物)──
|
|
318
|
+
st = cg._read_status(e1)
|
|
319
|
+
ok(st[0] is None and st[1] is None
|
|
320
|
+
and st[2] == fsutil.READ_FAIL_TRANSIENT,
|
|
321
|
+
"G1f 【①】三态标签实测:瞬时读失败 → 第三元素 = READ_FAIL_TRANSIENT"
|
|
322
|
+
"(缓存层消费的就是这个标签,不看异常类型)",
|
|
323
|
+
"status=%r" % (st,))
|
|
324
|
+
|
|
325
|
+
# ── 释放:**首次**查询即命中 ──
|
|
326
|
+
gate.release()
|
|
327
|
+
ids_r1 = _ids(cg.search("共用词"))
|
|
328
|
+
ids_r2 = _ids(cg.search("共用词"))
|
|
329
|
+
ok(ids_r1 == ["c3_n01", "c3_n02", "c3_n03"]
|
|
330
|
+
and ids_r2 == ["c3_n01", "c3_n02", "c3_n03"],
|
|
331
|
+
"G1g 【②】释放后**首次**查询即命中(瞬态窗口为零:一次可重试失败不被"
|
|
332
|
+
"固化成「节点从检索面消失」)",
|
|
333
|
+
"after_release=%s/%s" % (ids_r1, ids_r2))
|
|
334
|
+
|
|
335
|
+
g = cg.get("c3_n01")
|
|
336
|
+
ok(g is not None and g["frontmatter"].get("importance") == 0.5
|
|
337
|
+
and "c3_n01" in ids_r1,
|
|
338
|
+
"G1h 撕裂消除:cg.get 直读与 search 同批可见(修前「get 能读、"
|
|
339
|
+
"search 搜不到」)",
|
|
340
|
+
"get=%s" % (g is not None))
|
|
341
|
+
|
|
342
|
+
# ── 终态(真缺)仍照旧入缓存:不构成重读风暴 ──
|
|
343
|
+
cg2, d2 = _secured("g1term")
|
|
344
|
+
try:
|
|
345
|
+
cg2.add("c3_t1", "# 功能名:真缺夹具\n# 正文:真缺标记 共用词\n",
|
|
346
|
+
layer="knowledge")
|
|
347
|
+
cg2.flush()
|
|
348
|
+
MOD["readcache"].install(cg2)
|
|
349
|
+
e_t = cg2.index["nodes"]["c3_t1"]
|
|
350
|
+
kt = e_t["path"]
|
|
351
|
+
pt = cg2._node_disk_path(e_t)
|
|
352
|
+
os.remove(pt) # 真删(非瞬态)→ FileNotFoundError
|
|
353
|
+
gate.counts.clear()
|
|
354
|
+
gate.fail() # 闸门不拦,交给 OS 真报 ENOENT
|
|
355
|
+
_ids(cg2.search("真缺标记"))
|
|
356
|
+
ent_t = cg2._read_cache.get(kt)
|
|
357
|
+
ok(ent_t is not None and ent_t[1] == (None, None),
|
|
358
|
+
"G1i 【②范围】终态「真缺」**仍入缓存**((gen,(None,None)))——"
|
|
359
|
+
"「不入缓存」只针对瞬时失败,缺文件节点不会被每查询重试",
|
|
360
|
+
"cache[%r]=%r" % (kt, ent_t))
|
|
361
|
+
ok(cg2._read_status(e_t)[2] is None,
|
|
362
|
+
"G1j 【①】三态标签实测:FileNotFoundError → 终态(第三元素 None)",
|
|
363
|
+
"status=%r" % (cg2._read_status(e_t),))
|
|
364
|
+
finally:
|
|
365
|
+
_close_all_of([cg2])
|
|
366
|
+
_cleanup(d2)
|
|
367
|
+
finally:
|
|
368
|
+
gate.remove()
|
|
369
|
+
_close_all()
|
|
370
|
+
_cleanup(d)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _ids(hits):
|
|
374
|
+
return sorted(x[0].get("id") for x in hits[0]) if hits and hits[0] else []
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _close_all_of(cgs):
|
|
378
|
+
for c in cgs:
|
|
379
|
+
try:
|
|
380
|
+
c.close()
|
|
381
|
+
except Exception: # noqa: BLE001
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# ========================================= G2 无判别面载体的保守侧(回落分支)
|
|
386
|
+
def g2():
|
|
387
|
+
print("== G2 无三态面载体的保守侧(非 MdCG 载体的回落分支)==")
|
|
388
|
+
|
|
389
|
+
class _Stub:
|
|
390
|
+
"""只有二态 `_read` 的载体(无 `_read_status`)——覆盖 install 的回落分支。"""
|
|
391
|
+
|
|
392
|
+
def __init__(self, vals):
|
|
393
|
+
self._dirty = {}
|
|
394
|
+
self._vals = vals
|
|
395
|
+
self.calls = []
|
|
396
|
+
|
|
397
|
+
def _read(self, entry):
|
|
398
|
+
self.calls.append(entry["path"])
|
|
399
|
+
return self._vals[entry["path"]]
|
|
400
|
+
|
|
401
|
+
stub = _Stub({"p/ok.md": ({"id": "ok"}, "正文"),
|
|
402
|
+
"p/miss.md": (None, None)})
|
|
403
|
+
MOD["readcache"].install(stub)
|
|
404
|
+
ok(stub._read_uncached({"path": "p/ok.md"}) == ({"id": "ok"}, "正文"),
|
|
405
|
+
"G2a 回落分支在位:_read_uncached 仍是穿透缓存的原始二态读"
|
|
406
|
+
"(direct_read 的真源形状不变)")
|
|
407
|
+
e1 = {"path": "p/ok.md"}
|
|
408
|
+
e2 = {"path": "p/miss.md"}
|
|
409
|
+
stub.calls.clear() # G2a 的直读不计入计数面
|
|
410
|
+
stub._read(e1)
|
|
411
|
+
stub._read(e1)
|
|
412
|
+
ok(stub.calls.count("p/ok.md") == 1,
|
|
413
|
+
"G2b 成功结果照旧入缓存(二次读零重读)", stub.calls)
|
|
414
|
+
stub._read(e2)
|
|
415
|
+
stub._read(e2)
|
|
416
|
+
ok(stub.calls.count("p/miss.md") == 2,
|
|
417
|
+
"G2c 无判别面时**保守不接纳**空结果(宁可重读,不可固化可能是瞬时的"
|
|
418
|
+
"缺失)——回落分支不得回缺陷形态", stub.calls)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# ============================================ G3 无故障路径逐位一致(三路对照)
|
|
422
|
+
def g3():
|
|
423
|
+
print("== G3 无故障路径逐位一致(缓存开 / 缓存关 / 变异体 三路对照)==")
|
|
424
|
+
d = tempfile.mkdtemp(prefix="c3trn_g3_")
|
|
425
|
+
try:
|
|
426
|
+
root = os.path.join(d, "root")
|
|
427
|
+
cg0 = _build_lib(root)
|
|
428
|
+
# 先把「构造期望」与盘上真值对齐(独立 oracle,见 G4)
|
|
429
|
+
for q, want in _EXPECT_HITS.items():
|
|
430
|
+
got = _ids(cg0.search(q, record=False))
|
|
431
|
+
ok(got == sorted(want),
|
|
432
|
+
"G3pre 构造期望命中:%s → %s(oracle 来自夹具构造而非被测输出)"
|
|
433
|
+
% (q, want), "got=%s" % (got,))
|
|
434
|
+
_close_all_of([cg0])
|
|
435
|
+
|
|
436
|
+
# A:修复后 + 缓存开(生产默认态)
|
|
437
|
+
cgA = MdCGSecure(root, principal=_princ())
|
|
438
|
+
MOD["readcache"].install(cgA)
|
|
439
|
+
snapA = _snap(cgA)
|
|
440
|
+
_close_all_of([cgA])
|
|
441
|
+
|
|
442
|
+
# B:修复后 + 缓存关(MDCG_READ_CACHE=0 —— 逐次读盘的既有口径)
|
|
443
|
+
old = os.environ.get("MDCG_READ_CACHE")
|
|
444
|
+
os.environ["MDCG_READ_CACHE"] = "0"
|
|
445
|
+
try:
|
|
446
|
+
cgB = MdCGSecure(root, principal=_princ())
|
|
447
|
+
finally:
|
|
448
|
+
if old is None:
|
|
449
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
450
|
+
else:
|
|
451
|
+
os.environ["MDCG_READ_CACHE"] = old
|
|
452
|
+
ok(not hasattr(cgB, "_read_cache"),
|
|
453
|
+
"G3a 对照 B 确为「缓存关」形态(无 _read_cache 属性)")
|
|
454
|
+
snapB = _snap(cgB)
|
|
455
|
+
_close_all_of([cgB])
|
|
456
|
+
|
|
457
|
+
# C:变异体(撤掉「瞬时读失败不入缓存」判据 = 修复前接纳口径)+ 缓存开
|
|
458
|
+
mut, _src = _load_mutated(os.path.join("md_cg", "readcache.py"),
|
|
459
|
+
_MUT_ADMIT_OLD, _MUT_ADMIT_NEW)
|
|
460
|
+
live = MOD["readcache"]
|
|
461
|
+
MOD["readcache"] = mut
|
|
462
|
+
try:
|
|
463
|
+
cgC = MdCGSecure(root, principal=_princ())
|
|
464
|
+
mut.install(cgC)
|
|
465
|
+
snapC = _snap(cgC)
|
|
466
|
+
_close_all_of([cgC])
|
|
467
|
+
finally:
|
|
468
|
+
MOD["readcache"] = live
|
|
469
|
+
|
|
470
|
+
ok(snapA == snapB,
|
|
471
|
+
"G3b 【④】缓存开 == 缓存关:逐位一致(读缓存对无故障查询结果零影响)",
|
|
472
|
+
"diffA/B=%s" % (_first_diff(snapA, snapB),))
|
|
473
|
+
ok(snapA == snapC,
|
|
474
|
+
"G3c 【④】修复后 == 修复前接纳口径(变异体):无故障路径逐位一致"
|
|
475
|
+
"(修复只动失败路径的接纳判据,成功路径语义不变)",
|
|
476
|
+
"diffA/C=%s" % (_first_diff(snapA, snapC),))
|
|
477
|
+
finally:
|
|
478
|
+
_cleanup(d)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _first_diff(a: str, b: str) -> str:
|
|
482
|
+
if a == b:
|
|
483
|
+
return "无"
|
|
484
|
+
for i, (ca, cb) in enumerate(zip(a, b)):
|
|
485
|
+
if ca != cb:
|
|
486
|
+
lo = max(0, i - 60)
|
|
487
|
+
return "首个差异 @%d:A…%r | B…%r" % (i, a[lo:i + 40], b[lo:i + 40])
|
|
488
|
+
return "长度不同:A=%d B=%d" % (len(a), len(b))
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# ================================================ G4 盘上真值独立 oracle
|
|
492
|
+
def g4():
|
|
493
|
+
print("== G4 盘上真值独立 oracle(不依赖被测读路径)==")
|
|
494
|
+
cg, d = _secured("g4")
|
|
495
|
+
try:
|
|
496
|
+
for nid, body in _NODES[:4]:
|
|
497
|
+
cg.add(nid, "# 功能名:C-3 oracle %s\n# 正文:%s\n" % (nid, body),
|
|
498
|
+
layer="knowledge", importance=0.5)
|
|
499
|
+
cg.flush()
|
|
500
|
+
MOD["readcache"].install(cg)
|
|
501
|
+
bad = []
|
|
502
|
+
for nid, body in _NODES[:4]:
|
|
503
|
+
g = cg.get(nid)
|
|
504
|
+
raw = open(os.path.join(cg.root, g["path"]), encoding="utf-8").read()
|
|
505
|
+
# 独立判据:正文(去尾换行)与关键 frontmatter 值必须**逐字**出现在
|
|
506
|
+
# 原始文件字节里(不借用 md_cg 的解析器),且 marker 在正文中。
|
|
507
|
+
if (g["content"].rstrip("\n") not in raw
|
|
508
|
+
or body not in raw
|
|
509
|
+
or str(g["frontmatter"].get("id")) not in raw
|
|
510
|
+
or str(g["frontmatter"].get("layer")) not in raw):
|
|
511
|
+
bad.append(nid)
|
|
512
|
+
ok(not bad,
|
|
513
|
+
"G4a 【④】get 的 content/frontmatter 与盘上原始文件逐字一致"
|
|
514
|
+
"(4 节点全过;oracle 只做文件字节比对,不经 _read/_read_status)",
|
|
515
|
+
"mismatch=%s" % bad)
|
|
516
|
+
finally:
|
|
517
|
+
_close_all()
|
|
518
|
+
_cleanup(d)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# ==================================================== G5 单点结构(防副本回归)
|
|
522
|
+
def g5():
|
|
523
|
+
print("== G5 单点结构(判别唯一、缓存层不重判)==")
|
|
524
|
+
fs = open(os.path.join(_HERE, "fsutil.py"), encoding="utf-8").read()
|
|
525
|
+
rc = open(os.path.join(_HERE, "readcache.py"), encoding="utf-8").read()
|
|
526
|
+
md = open(os.path.join(_HERE, "mdcg.py"), encoding="utf-8").read()
|
|
527
|
+
|
|
528
|
+
def code(src):
|
|
529
|
+
return "\n".join(l for l in src.splitlines()
|
|
530
|
+
if not l.strip().startswith("#"))
|
|
531
|
+
|
|
532
|
+
ok(code(fs).count("def classify_read_failure") == 1,
|
|
533
|
+
"G5a 判别函数单点:fsutil.classify_read_failure 恰好一处定义",
|
|
534
|
+
code(fs).count("def classify_read_failure"))
|
|
535
|
+
ok(code(md).count("fsutil.classify_read_failure(exc)") == 1,
|
|
536
|
+
"G5b 判别**调用**单点:mdcg 全文件仅一处调用(_note_read_oserror)——"
|
|
537
|
+
"读路径不给第二份判据",
|
|
538
|
+
code(md).count("fsutil.classify_read_failure(exc)"))
|
|
539
|
+
ok("isinstance(exc" not in code(rc)
|
|
540
|
+
and "except OSError" not in code(rc)
|
|
541
|
+
and "classify_read_failure(" not in code(rc),
|
|
542
|
+
"G5c 缓存层不重判:readcache.py 可执行行内既不捕获 OSError、也不做异常"
|
|
543
|
+
"类型判别、更不调用判别函数(不猜第二遍),只消费 _read_status 的第三"
|
|
544
|
+
"元素标签")
|
|
545
|
+
ok("if val[2] is not None:" in code(rc),
|
|
546
|
+
"G5d 接纳判据按**标签**(_read_status 第三元素)而非返回值形状")
|
|
547
|
+
ok(code(md).count("def _read_status") == 1
|
|
548
|
+
and "return self._read_status(entry)[:2]" in code(md),
|
|
549
|
+
"G5e 二态对外面单点转发:_read 恰为 _read_status 的前两项投影"
|
|
550
|
+
"(无第二份读实现)")
|
|
551
|
+
ok(code(md).count("def _note_read_oserror") == 1,
|
|
552
|
+
"G5f 记账口单点:mdcg 内 _note_read_oserror 恰好一处定义")
|
|
553
|
+
# 对照先例在位(N225 同风格记账面仍在,防本批把旧面拆掉)
|
|
554
|
+
ok("TRANSIENT_READ_FAILURES" in fs and "NONOBJECT_ROW_SKIPS" in fs,
|
|
555
|
+
"G5g 记账面与 N225 坏行记账同模块同风格并存(未互相顶替)")
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# ==================================================== G6 记账面(计数/样本/有界)
|
|
559
|
+
def g6():
|
|
560
|
+
print("== G6 记账面(模块级计数 + 有界样本 + stderr 告警 + 基线可重置)==")
|
|
561
|
+
fsutil.reset_transient_read_stats()
|
|
562
|
+
ok(fsutil.transient_read_stats() == (0, ()),
|
|
563
|
+
"G6a 记账面可重置(守卫/运维建基线用)",
|
|
564
|
+
fsutil.transient_read_stats())
|
|
565
|
+
exc = PermissionError(13, "sharing violation")
|
|
566
|
+
buf = io.StringIO()
|
|
567
|
+
with contextlib.redirect_stderr(buf):
|
|
568
|
+
wrote = fsutil.note_transient_read_failure("knowledge/orphan/x.md", exc)
|
|
569
|
+
err = buf.getvalue()
|
|
570
|
+
n1, s1 = fsutil.transient_read_stats()
|
|
571
|
+
ok(n1 == 1 and s1 and s1[0][0].endswith("x.md")
|
|
572
|
+
and s1[0][1] == "PermissionError" and s1[0][2] == 13,
|
|
573
|
+
"G6b 记账:计数累加 + 样本三元组 (path, 异常类型名, errno)", (n1, s1))
|
|
574
|
+
ok(wrote and "[fsutil]" in err and "x.md" in err and "累计 1 次" in err,
|
|
575
|
+
"G6b2 告警面:失败时向 stderr 写一行(含 path/异常/累计数)——T4 静默"
|
|
576
|
+
"损伤不再复现", err.strip()[:120])
|
|
577
|
+
buf2 = io.StringIO()
|
|
578
|
+
with contextlib.redirect_stderr(buf2):
|
|
579
|
+
for i in range(100):
|
|
580
|
+
fsutil.note_transient_read_failure("knowledge/orphan/y%02d.md" % i,
|
|
581
|
+
exc)
|
|
582
|
+
n2, s2 = fsutil.transient_read_stats()
|
|
583
|
+
ok(n2 == 101 and len(s2) == fsutil._TRANSIENT_READ_SAMPLE_CAP,
|
|
584
|
+
"G6c 样本有界(不随失败次数线性涨)、计数不受限",
|
|
585
|
+
"count=%d samples=%d" % (n2, len(s2)))
|
|
586
|
+
ok(buf2.getvalue().count("[fsutil]")
|
|
587
|
+
== fsutil._TRANSIENT_READ_WARN_CAP - 1,
|
|
588
|
+
"G6d 告警限流:101 次失败只写 %d 行(首 %d 次各一行,其后静默但计数/样本"
|
|
589
|
+
"持续更新)——热路径防刷屏,信息面不减"
|
|
590
|
+
% (fsutil._TRANSIENT_READ_WARN_CAP - 1, fsutil._TRANSIENT_READ_WARN_CAP),
|
|
591
|
+
buf2.getvalue().count("[fsutil]"))
|
|
592
|
+
fsutil.reset_transient_read_stats()
|
|
593
|
+
ok(fsutil.transient_read_stats() == (0, ()),
|
|
594
|
+
"G6e 记账面复位(不留脏基线给后续用例)")
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# ==================================================== G7 Windows 实测(非 Windows SKIP)
|
|
598
|
+
def g7():
|
|
599
|
+
print("== G7 Windows 实测:ctypes.WinDLL 独占句柄(FI-M02 同款置景)==")
|
|
600
|
+
if os.name != "nt":
|
|
601
|
+
skip("G7 非 Windows:ctypes.WinDLL 独占句柄置景不可用(SKIP,不计入断言"
|
|
602
|
+
"数)——缺口本体的跨平台判据由 G1 的 OSError 注入承担")
|
|
603
|
+
return
|
|
604
|
+
import ctypes
|
|
605
|
+
GENERIC_RW = 0x80000000 | 0x40000000
|
|
606
|
+
OPEN_EXISTING = 3
|
|
607
|
+
INVALID = ctypes.c_void_p(-1).value
|
|
608
|
+
cg, d = _secured("g7")
|
|
609
|
+
try:
|
|
610
|
+
cg.add("c3_w1", "# 功能名:持锁夹具\n# 正文:独占锁 共用词zz\n",
|
|
611
|
+
layer="knowledge", importance=0.5)
|
|
612
|
+
cg.flush()
|
|
613
|
+
MOD["readcache"].install(cg)
|
|
614
|
+
e = cg.index["nodes"]["c3_w1"]
|
|
615
|
+
p = cg._node_disk_path(e)
|
|
616
|
+
key = e["path"]
|
|
617
|
+
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
618
|
+
k32.CreateFileW.restype = ctypes.c_void_p
|
|
619
|
+
k32.CreateFileW.argtypes = [ctypes.c_wchar_p, ctypes.c_uint32,
|
|
620
|
+
ctypes.c_uint32, ctypes.c_void_p,
|
|
621
|
+
ctypes.c_uint32, ctypes.c_uint32,
|
|
622
|
+
ctypes.c_void_p]
|
|
623
|
+
h = k32.CreateFileW(p, GENERIC_RW, 0, None, OPEN_EXISTING, 0, None)
|
|
624
|
+
held = h not in (None, INVALID)
|
|
625
|
+
ok(held, "G7a 注入生效:CreateFileW(dwShareMode=0) 独占持住节点 md 文件",
|
|
626
|
+
"handle=%s" % h)
|
|
627
|
+
try:
|
|
628
|
+
MOD["readcache"].clear(cg) # 冷缓存:确保本次走读路径
|
|
629
|
+
ids_lock = _ids(cg.search("独占锁")) if held else None
|
|
630
|
+
entry_lock = cg._read_cache.get(key) if held else None
|
|
631
|
+
stat_lock = fsutil.transient_read_stats() if held else None
|
|
632
|
+
finally:
|
|
633
|
+
if held:
|
|
634
|
+
k32.CloseHandle(h)
|
|
635
|
+
ok(ids_lock == [],
|
|
636
|
+
"G7b 持锁期搜索 **miss**(fail-closed:不可读即不参与检索)",
|
|
637
|
+
"hits=%s" % (ids_lock,))
|
|
638
|
+
ok(entry_lock is None,
|
|
639
|
+
"G7c 持锁期负结果**不入缓存**(修前此处为 (gen,(None,None)) 固化条目)",
|
|
640
|
+
"cache=%r" % (entry_lock,))
|
|
641
|
+
ids_r = _ids(cg.search("独占锁"))
|
|
642
|
+
ok(ids_r == ["c3_w1"],
|
|
643
|
+
"G7d 释放句柄后**首次**查询即命中(瞬态窗口为零)",
|
|
644
|
+
"after=%s" % (ids_r,))
|
|
645
|
+
ok(stat_lock is not None and stat_lock[0] >= 1,
|
|
646
|
+
"G7e 持锁期记账可见(计数 ≥ 1)", stat_lock)
|
|
647
|
+
finally:
|
|
648
|
+
_close_all()
|
|
649
|
+
_cleanup(d)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
# ================================================================ 变异表
|
|
653
|
+
# 每条 = (名称, 模块键, 变异形态, 源码锚点, 替换文本, 预期转红条数)。
|
|
654
|
+
# 锚点必须逐字命中当前源码(找不到 → ANCHOR-MISS → 退出码 2,fail-closed)。
|
|
655
|
+
# 形态:swap = 整模块替换(断言组经 MOD 取);attr = 把变异体的同名函数注入
|
|
656
|
+
# 活模块的同名属性(判别函数被 mdcg 以模块属性调用,换副本即生效)。
|
|
657
|
+
_MUT_ADMIT_OLD = (" val = orig_status(entry)\n"
|
|
658
|
+
" if val[2] is not None:\n"
|
|
659
|
+
" # C-3:瞬时读失败**不入缓存**(本次即返回,"
|
|
660
|
+
"下次查询重试该 path)。\n"
|
|
661
|
+
" return val\n")
|
|
662
|
+
_MUT_ADMIT_NEW = (" val = orig_status(entry)\n"
|
|
663
|
+
" if False: # MUT:撤掉「瞬时读失败不入缓存」判据\n"
|
|
664
|
+
" return val\n")
|
|
665
|
+
_MUT_CLS_OLD = (" if isinstance(exc, FileNotFoundError):\n"
|
|
666
|
+
" return READ_FAIL_ABSENT\n"
|
|
667
|
+
" return READ_FAIL_TRANSIENT\n")
|
|
668
|
+
_MUT_CLS_NEW = (" return READ_FAIL_ABSENT # MUT:一律判终态(瞬时被当不存在)\n")
|
|
669
|
+
|
|
670
|
+
_MUTATIONS = (
|
|
671
|
+
("C-3a 撤掉「瞬时读失败不入缓存」的接纳判据(回缺陷形态)",
|
|
672
|
+
"readcache", "swap", _MUT_ADMIT_OLD, _MUT_ADMIT_NEW, 8),
|
|
673
|
+
("C-3b 判别退化:一律判终态(瞬时失败被当「不存在」)",
|
|
674
|
+
"fsutil", "attr", _MUT_CLS_OLD, _MUT_CLS_NEW, 9),
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
_RELPATH = {
|
|
678
|
+
"readcache": os.path.join("md_cg", "readcache.py"),
|
|
679
|
+
"fsutil": os.path.join("md_cg", "fsutil.py"),
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _load_mutated(relpath: str, old: str, new: str):
|
|
684
|
+
"""把 `relpath` 的源码做定点文本替换后 exec 成独立模块(不落盘、不改工作区)。"""
|
|
685
|
+
path = os.path.join(_REPO, relpath)
|
|
686
|
+
src = open(path, encoding="utf-8").read()
|
|
687
|
+
if old not in src:
|
|
688
|
+
return None, src
|
|
689
|
+
mut_src = src.replace(old, new, 1)
|
|
690
|
+
mod = types.ModuleType("md_cg._c3trn_mut")
|
|
691
|
+
mod.__package__ = "md_cg"
|
|
692
|
+
mod.__file__ = path
|
|
693
|
+
exec(compile(mut_src, path, "exec"), mod.__dict__) # noqa: S102 —— 定点变异
|
|
694
|
+
return mod, src
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _run_groups() -> int:
|
|
698
|
+
_PASS.clear()
|
|
699
|
+
_FAIL.clear()
|
|
700
|
+
_SKIP.clear()
|
|
701
|
+
for g in (g0, g1, g2, g3, g4, g5, g6, g7):
|
|
702
|
+
try:
|
|
703
|
+
g()
|
|
704
|
+
except Exception as exc: # noqa: BLE001
|
|
705
|
+
import traceback
|
|
706
|
+
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
707
|
+
print(traceback.format_exc()[-800:])
|
|
708
|
+
_close_all()
|
|
709
|
+
return len(_FAIL)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _mutate_mode(list_only: bool = False) -> int:
|
|
713
|
+
print("!! 定点变异自证:逐条把修复点改回缺陷形态,套件必须按预期条数转红\n")
|
|
714
|
+
if list_only:
|
|
715
|
+
for name, key, kind, old, new, exp in _MUTATIONS:
|
|
716
|
+
print(" %-46s %-10s %-5s expect_red=%d" % (name, key, kind, exp))
|
|
717
|
+
return 0
|
|
718
|
+
|
|
719
|
+
anchor_miss = []
|
|
720
|
+
bad = []
|
|
721
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
722
|
+
clean = _run_groups()
|
|
723
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
724
|
+
if clean:
|
|
725
|
+
bad.append("未变异基线即失败")
|
|
726
|
+
|
|
727
|
+
for name, key, kind, old, new, expect in _MUTATIONS:
|
|
728
|
+
mod, src = _load_mutated(_RELPATH[key], old, new)
|
|
729
|
+
if mod is None:
|
|
730
|
+
print(" ANCHOR-MISS %s —— 锚点在 %s 当前源码里找不到"
|
|
731
|
+
"(实现改了却没同步本表;基线不得静默漂移)"
|
|
732
|
+
% (name, _RELPATH[key]))
|
|
733
|
+
anchor_miss.append(name)
|
|
734
|
+
continue
|
|
735
|
+
live_mod = MOD.get(key)
|
|
736
|
+
live_attr = None
|
|
737
|
+
if kind == "swap":
|
|
738
|
+
MOD[key] = mod
|
|
739
|
+
else:
|
|
740
|
+
live_mod = sys.modules["md_cg." + key]
|
|
741
|
+
live_attr = getattr(live_mod, _mut_func_name(old))
|
|
742
|
+
setattr(live_mod, _mut_func_name(old), getattr(mod, _mut_func_name(old)))
|
|
743
|
+
try:
|
|
744
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
745
|
+
fails_extra = _run_groups()
|
|
746
|
+
detail = buf.getvalue()
|
|
747
|
+
finally:
|
|
748
|
+
if kind == "swap":
|
|
749
|
+
MOD[key] = live_mod
|
|
750
|
+
else:
|
|
751
|
+
setattr(live_mod, _mut_func_name(old), live_attr)
|
|
752
|
+
red = [l for l in detail.splitlines() if l.strip().startswith("FAIL ")]
|
|
753
|
+
verdict = "红" if fails_extra else "**仍全绿 = 该判据空转**"
|
|
754
|
+
mark = "OK " if fails_extra == expect else "MISMATCH"
|
|
755
|
+
print(" %s %-46s 红项=%d 预期=%d %s"
|
|
756
|
+
% (mark, name, fails_extra, expect, verdict))
|
|
757
|
+
for l in red:
|
|
758
|
+
print(" " + l.strip()[5:])
|
|
759
|
+
if fails_extra != expect:
|
|
760
|
+
bad.append("%s(红=%d 预期=%d)" % (name, fails_extra, expect))
|
|
761
|
+
|
|
762
|
+
if anchor_miss:
|
|
763
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
764
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
765
|
+
return 2
|
|
766
|
+
print("\n定点变异自证:%s"
|
|
767
|
+
% ("PASS(每处判据都被打红且恰好命中预期项数)" if not bad
|
|
768
|
+
else "FAIL —— " + "、".join(bad)))
|
|
769
|
+
return 0 if not bad else 1
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _mut_func_name(old: str) -> str:
|
|
773
|
+
"""attr 形态变异的目标函数名(按锚点文本判定,防两处锚点串味)。"""
|
|
774
|
+
return "classify_read_failure" if "FileNotFoundError" in old else ""
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def main() -> int:
|
|
778
|
+
if "--mutate" in sys.argv:
|
|
779
|
+
try:
|
|
780
|
+
return _mutate_mode("--list" in sys.argv)
|
|
781
|
+
finally:
|
|
782
|
+
_cleanup(_TMP)
|
|
783
|
+
try:
|
|
784
|
+
n_fail = _run_groups()
|
|
785
|
+
print("\nC-3 守卫(瞬态读失败不得固化):%d 通过,%d 失败,%d 跳过"
|
|
786
|
+
% (len(_PASS), n_fail, len(_SKIP)))
|
|
787
|
+
return 0 if not n_fail else 1
|
|
788
|
+
finally:
|
|
789
|
+
_cleanup(_TMP)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
if __name__ == "__main__":
|
|
793
|
+
sys.exit(main())
|