@furongjun1999/dsh-memory 0.7.1 → 0.7.3

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 (125) hide show
  1. package/README.md +58 -48
  2. package/codebuddy/CODEBUDDY.md +9 -9
  3. package/docs/README.md +148 -144
  4. package/docs/discipline/harnesses.yaml +16 -8
  5. package/docs/discipline/templates/zcode-user.md.tmpl +49 -0
  6. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_/345/205/255/345/256/266/350/256/260/345/277/206/347/263/273/347/273/237/345/220/214/345/260/272/345/256/236/346/265/213_v3.1.md +340 -0
  7. 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 +2 -2
  8. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +1 -1
  9. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +1 -1
  10. 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 +1 -1
  11. 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 +2 -2
  12. package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +5 -5
  13. package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +9 -9
  14. package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +4 -4
  15. package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +1 -1
  16. package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +1 -1
  17. package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +3 -3
  18. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +3 -3
  19. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +7 -7
  20. 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 +2 -2
  21. 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 +1 -1
  22. 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 +7 -7
  23. package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +10 -10
  24. 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 +4 -0
  25. package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +1 -1
  26. package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +1 -1
  27. package/docs/eval//345/217/221/345/270/20310_/350/257/273/351/235/242/346/215/237/345/235/217UTF8/345/256/266/346/227/217/346/224/266/345/217/243_v1.0.md +71 -0
  28. package/docs/eval//345/217/221/345/270/20311_/345/244/226/346/212/245/345/205/255/350/277/236/344/277/256/344/270/216/344/270/226/347/225/214/346/250/241/345/236/213/345/272/225/345/272/247_v1.0.md +91 -0
  29. package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +2 -2
  30. package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/270/200/346/211/271_v1.0.md +195 -0
  31. package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/272/214/346/211/271B6_/346/234/254/346/234/272/350/267/257/345/276/204/347/233/270/345/257/271/345/214/226_v1.0.md +78 -0
  32. package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +1 -1
  33. package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +3 -3
  34. package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +1 -1
  35. 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 +1 -1
  36. 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_v26.md +196 -0
  37. 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_v27.md +259 -0
  38. 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_v28.md +217 -0
  39. 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_v29.md +270 -0
  40. 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_v30.md +231 -0
  41. 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_v31.md +281 -0
  42. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +1 -1
  43. 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
  44. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +1 -1
  45. package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +17 -17
  46. package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +1 -1
  47. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +2 -2
  48. 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 +42 -41
  49. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +29 -8
  50. package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +71 -1
  51. 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 +1 -1
  52. package/docs/plans//345/244/232/344/270/273/344/275/223/344/270/226/347/225/214/346/250/241/345/236/213_/345/257/271/351/275/220/350/257/204/344/274/260/344/270/216/350/220/275/345/234/260/350/256/276/350/256/241_v0.1.md +141 -0
  53. package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +2 -2
  54. package/docs/plans//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/350/256/276/350/256/241_v0.1.md +107 -0
  55. 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 +9 -8
  56. package/docs//347/247/244_/350/256/260/345/277/206/347/263/273/347/273/237/350/257/204/346/265/213/350/247/204/350/214/203_v2.1.md +225 -0
  57. package/dsh/cordis-patch-profile-web.example.yml +2 -2
  58. package/lib/bridge.js +9 -0
  59. package/lib/hooks.d.ts +8 -1
  60. package/lib/hooks.js +94 -32
  61. package/lib/lib/hook_audit.d.ts +55 -0
  62. package/lib/lib/hook_audit.js +208 -0
  63. package/lib/lib/mutual.js +5 -1
  64. package/lib/lib/roleplay_web.js +17 -2
  65. package/md_cg/auditview.py +248 -0
  66. package/md_cg/bench6_arms.py +10 -6
  67. package/md_cg/bench_e2e_judge.py +4 -2
  68. package/md_cg/branches.py +29 -3
  69. package/md_cg/census.py +2 -1
  70. package/md_cg/consistency.py +38 -13
  71. package/md_cg/datapath.py +38 -11
  72. package/md_cg/forgetting.py +15 -2
  73. package/md_cg/freshness.py +32 -11
  74. package/md_cg/ghostref.py +130 -0
  75. package/md_cg/judgment_manifest.py +7 -0
  76. package/md_cg/mcp_server.py +58 -1
  77. package/md_cg/mdcg.py +4923 -4764
  78. package/md_cg/mdcos.py +40 -15
  79. package/md_cg/mreview/locate.py +1 -1
  80. package/md_cg/nodefile.py +77 -13
  81. package/md_cg/readcache.py +29 -4
  82. package/md_cg/rollback.py +12 -3
  83. package/md_cg/routing.py +10 -1
  84. package/md_cg/selfreport.py +12 -3
  85. package/md_cg/sleep.py +129 -13
  86. package/md_cg/sources.py +1 -1
  87. package/md_cg/state_events.py +131 -0
  88. package/md_cg/sustain.py +1436 -1436
  89. package/md_cg/test_auditview.py +277 -0
  90. package/md_cg/test_autonomy_modes.py +2 -1
  91. package/md_cg/test_boot_openblas_guard.py +197 -0
  92. package/md_cg/test_ccg_form_parity.py +95 -4
  93. package/md_cg/test_cons200_scan_selection.py +4 -2
  94. package/md_cg/test_corrupt_utf8_read_surfaces.py +451 -0
  95. package/md_cg/test_datapath_device_name.py +62 -3
  96. package/md_cg/test_generation_guard.py +20 -11
  97. package/md_cg/test_ghostref.py +182 -0
  98. package/md_cg/test_health_corrupt_utf8.py +37 -0
  99. package/md_cg/test_index_crossprocess_reload.py +39 -24
  100. package/md_cg/test_interop.py +1 -1
  101. package/md_cg/test_issue52_scan_condition_first.py +1 -1
  102. package/md_cg/test_issue53_cipher_health_hash.py +385 -0
  103. package/md_cg/test_lock.py +16 -7
  104. package/md_cg/test_m3_h9_semantic_guard.py +1 -1
  105. package/md_cg/test_n230_dirty_replay.py +375 -375
  106. package/md_cg/test_n238_body_text_parity.py +156 -0
  107. package/md_cg/test_n276_probe_hoist.py +199 -0
  108. package/md_cg/test_p4_freshness.py +95 -1
  109. package/md_cg/test_policy_required_ccg.py +3 -1
  110. package/md_cg/test_readcache_prodpath.py +66 -0
  111. package/md_cg/test_sleep.py +58 -1
  112. package/md_cg/test_sleep_p1.py +218 -2
  113. package/md_cg/test_state_events.py +178 -0
  114. package/md_cg/test_time_core_lint.py +8 -1
  115. package/md_cg/test_writepipe.py +8 -5
  116. package/md_cg/tokens.py +11 -2
  117. package/md_cg/writepipe.py +76 -0
  118. package/package.json +2 -2
  119. package/skills/plugin.json +1 -1
  120. package/src/bridge.ts +9 -0
  121. package/src/hooks.ts +576 -517
  122. package/src/lib/hook_audit.ts +243 -0
  123. package/src/lib/mutual.ts +5 -1
  124. package/src/lib/roleplay_web.ts +17 -2
  125. package/zcode/AGENTS.md +9 -9
@@ -0,0 +1,248 @@
1
+ # -*- coding: utf-8 -*-
2
+ """L5 证据审计面:只读组装器(路线 C)——「现值+出处链+两存/备择+盲区+退役史」。
3
+
4
+ 多主体世界模型对齐 v0.1 §4 路线 C:「给节点 id 输出『现值+出处链+两存/备择+
5
+ 盲区+退役史』。**纯读、零写路径改动**」。命名约束(设计稿复核修订):**不得
6
+ 复用 `view` 参数名**(已被 roleviews 占用,mdcos._candidates 的 view 面)。
7
+
8
+ 五面数据源(全部只读):
9
+ · 现值 —— `cg.get` 的 fm + 正文(层/生命态/验证态/时效/A1 检验强度/
10
+ A2 幽灵标记/归属面);
11
+ · 出处链 —— fm.derived_from(血缘)+ fm.edges(它引用谁)+ **反查**
12
+ 索引条目 edges(谁引用它,内存扫描免读盘)+ provenance 台账
13
+ `_link.jsonl` 相关行;
14
+ · 两存/备择 —— `cg.check_consistency(..., auto_flywheel=False, exclude=<自身>)`
15
+ 的**只读复用**(与写入链同一判据):同条件空间分歧/对照数/
16
+ 截断如实透出(「两存之为默认、冲突一等」的可见化);
17
+ · 盲区 —— negative/positive evidence 计数、非适用条件、A2 幽灵标记、
18
+ trust.validity 判定(未生效/已过期/未知)、verification_state;
19
+ · 退役史 —— fm.state_history / fm.verification_history(lifecycle/trust
20
+ 的既有 HISTORY_FIELD 快照)+ `evolution.history` 台账。
21
+
22
+ 成本如实:备择面复用一致性检查,大库上有候选截断(截断计数如实带出,hint
23
+ 与写入面同款);本组装器**不落盘、不改任何状态**。超出 cap 的面截断并计数。
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import os
28
+
29
+ __all__ = ["build"]
30
+
31
+ #: 各面截断上限(超限计数并如实带出 truncated)。
32
+ DEFAULT_CAP = 8
33
+
34
+
35
+ def _readable_guard(cg):
36
+ """读隔离谓词(与 linkref.known_ids 同口径):不可调用时全放行。"""
37
+ fn = getattr(cg, "_readable", None)
38
+ return fn if callable(fn) else (lambda _e: True)
39
+
40
+
41
+ def _referenced_by(cg, node_id, cap):
42
+ """反查:谁引用了本节点(索引条目 edges 的内存扫描;免读盘)。
43
+
44
+ **读隔离**(2026-10-05 复核 R3):扫描按 `cg._readable` 过滤——不可读
45
+ 节点的 id 与其引用关系不得经本面泄漏(与 linkref.known_ids /
46
+ evolution.entries 同口径:「不可见即不存在」)。
47
+ """
48
+ # H-4 纪律(test_h4_sustain_snapshot G1a):从 cg.index 取回的 nodes
49
+ # 须先取**快照**再迭代——并发写者 flush 时裸迭代会撞「字典变更」。
50
+ _live = (getattr(cg, "index", None) or {}).get("nodes") or {}
51
+ nodes = dict(_live)
52
+ guard = _readable_guard(cg)
53
+ out, total = [], 0
54
+ for nid, e in nodes.items():
55
+ if nid == node_id:
56
+ continue
57
+ try:
58
+ if not guard(e):
59
+ continue
60
+ except Exception: # noqa: BLE001 —— fail-closed
61
+ continue
62
+ for ed in (e.get("edges") or []):
63
+ if str(ed.get("target")) == node_id:
64
+ total += 1
65
+ if len(out) < cap:
66
+ out.append({"from": nid, "relation": ed.get("relation_type"),
67
+ "confidence": ed.get("confidence"),
68
+ "source": ed.get("source")})
69
+ break
70
+ return {"items": out, "total": total, "truncated": total > len(out)}
71
+
72
+
73
+ def _link_rows(cg, node_id, cap):
74
+ """provenance 台账(`_link.jsonl`)里与本节点相关的行。
75
+
76
+ **schema 对齐 provenance.make_edge**(2026-10-05 复核 R1 修):真字段是
77
+ `child/parent/rel/t/actor`(原实现按 `target/source/relation/at` 匹配
78
+ ⇒ 恒读空,整面死)。「与本节点相关」= child==本节点(它派生自谁)或
79
+ parent==本节点(谁派生自它)。
80
+
81
+ **行级读隔离**(2026-10-05 复验 R4 修):行是否可见取决于**另一端**——
82
+ 「不可见即不存在」的口径下,verify 身份不得经本面看到不可读 secret
83
+ 节点的 id 与其派生关系(复验实证:修复前与 designer 视角逐位相同)。
84
+ 另一端条目不在索引或不可读 ⇒ **整行不出**(fail-closed,与
85
+ `_referenced_by` 的 guard 同口径;自身端必可读——本函数只在本节点
86
+ 已可读[build 的 cg.get 成功]之后被调用)。
87
+ """
88
+ try:
89
+ from . import provenance
90
+ path = provenance.ledger_file(getattr(cg, "root", None))
91
+ except Exception: # noqa: BLE001
92
+ return {"items": [], "total": 0, "truncated": False}
93
+ if not path or not os.path.exists(path):
94
+ return {"items": [], "total": 0, "truncated": False}
95
+ _live = (getattr(cg, "index", None) or {}).get("nodes") or {}
96
+ nodes = dict(_live) # H-4 快照纪律(迭代前取快照)
97
+ guard = _readable_guard(cg)
98
+ out, total = [], 0
99
+ try:
100
+ with open(path, encoding="utf-8", errors="replace") as f:
101
+ for line in f:
102
+ line = line.strip()
103
+ if not line:
104
+ continue
105
+ try:
106
+ import json
107
+ rec = json.loads(line)
108
+ except Exception: # noqa: BLE001
109
+ continue
110
+ if not isinstance(rec, dict):
111
+ continue
112
+ c, p = str(rec.get("child")), str(rec.get("parent"))
113
+ if c == node_id:
114
+ other = p
115
+ elif p == node_id:
116
+ other = c
117
+ else:
118
+ continue
119
+ e_other = nodes.get(other)
120
+ if e_other is None:
121
+ continue # 另一端不在索引(含已删)→ 不出
122
+ try:
123
+ if not guard(e_other):
124
+ continue # 另一端不可读 → 整行不出
125
+ except Exception: # noqa: BLE001 —— fail-closed
126
+ continue
127
+ total += 1
128
+ if len(out) < cap:
129
+ out.append({k: rec.get(k) for k in
130
+ ("child", "parent", "rel", "t", "actor", "note")})
131
+ except Exception: # noqa: BLE001 —— 只读面不抛
132
+ pass
133
+ return {"items": out, "total": total, "truncated": total > len(out)}
134
+
135
+
136
+ def build(cg, node_id, cap=DEFAULT_CAP) -> dict:
137
+ """组装五面只读视图。node_id 不存在 ⇒ {"ok": False, "error": "node_not_found"}。
138
+
139
+ cap 防御(2026-10-05 复核 Y2):非数/None 回落缺省、钳 [1, 64]——
140
+ 直调面与 MCP 面同语义(MCP 面的钳制在分发层,直调 API 不得裸抛 TypeError)。
141
+ """
142
+ try:
143
+ cap = int(cap)
144
+ except (TypeError, ValueError):
145
+ cap = DEFAULT_CAP
146
+ cap = max(1, min(64, cap))
147
+ nid = str(node_id or "").strip()
148
+ if not nid:
149
+ return {"ok": False, "error": "node_not_found", "node_id": node_id}
150
+ rec = cg.get(nid)
151
+ if not rec:
152
+ return {"ok": False, "error": "node_not_found", "node_id": nid}
153
+ fm = rec.get("frontmatter") or {}
154
+ content = rec.get("content") or ""
155
+ from . import evolution, lifecycle, nodefile, trust
156
+
157
+ # ---- 面一:现值 ----
158
+ try:
159
+ _validity = trust.validity(fm)
160
+ except Exception as exc: # noqa: BLE001
161
+ _validity = "%s: %s" % (type(exc).__name__, exc)
162
+ present = {
163
+ "layer": fm.get("layer"),
164
+ "lifecycle_state": lifecycle.state_of(fm),
165
+ "verification_state": trust.state_of(fm),
166
+ "validity": _validity,
167
+ "importance": fm.get("importance"),
168
+ "sensitivity": fm.get("sensitivity"),
169
+ "created_at": fm.get("created_at"),
170
+ "tags": fm.get("tags"),
171
+ "verification_basis": fm.get("verification_basis"),
172
+ nodefile.CHECK_STRENGTH_FIELD: fm.get(nodefile.CHECK_STRENGTH_FIELD),
173
+ nodefile.UNCERTAIN_REFS_FIELD: fm.get(nodefile.UNCERTAIN_REFS_FIELD),
174
+ "writer": fm.get("writer"), "harness": fm.get("harness"),
175
+ "session": fm.get("session"),
176
+ "content_chars": len(content),
177
+ }
178
+
179
+ # ---- 面二:出处链 ----
180
+ _edges = list(fm.get("edges") or [])
181
+ sources = {
182
+ "derived_from": fm.get("derived_from"),
183
+ "derived_relation": fm.get("derived_relation"),
184
+ "refers_to": {"items": _edges[:cap], "total": len(_edges),
185
+ "truncated": len(_edges) > cap},
186
+ "referenced_by": _referenced_by(cg, nid, cap),
187
+ "link_ledger": _link_rows(cg, nid, cap),
188
+ }
189
+
190
+ # ---- 面三:两存/备择(一致性只读复用;不建单、不落盘)----
191
+ alternatives = {"comparable": None, "verdict": None, "conflicts": [],
192
+ "missing": [], "truncated": None, "scanned": None}
193
+ try:
194
+ # log_write=False(复核 R2 修):只读消费面——判定照常、台账不写
195
+ #(consistency.log 原为无条件追加,审计会变成隐式写)。
196
+ cvd = cg.check_consistency(
197
+ content, layer=fm.get("layer") or "knowledge",
198
+ condition_space=fm.get("condition_space"),
199
+ non_applicable_conditions=fm.get("non_applicable_conditions"),
200
+ tags=fm.get("tags"), exclude=nid, auto_flywheel=False,
201
+ log_write=False)
202
+ if isinstance(cvd, dict):
203
+ cf = list(cvd.get("conflicts") or [])
204
+ alternatives = {
205
+ "verdict": cvd.get("verdict"), "reason": cvd.get("reason"),
206
+ "conflict_strength": cvd.get("conflict_strength"),
207
+ "comparable": cvd.get("comparable"),
208
+ "scanned": cvd.get("scanned"), "truncated": cvd.get("truncated"),
209
+ "conflicts": cf[:cap],
210
+ "conflicts_total": len(cf), "conflicts_truncated": len(cf) > cap,
211
+ "missing": list(cvd.get("missing") or [])[:cap],
212
+ "hint": cvd.get("hint"),
213
+ }
214
+ except Exception as exc: # noqa: BLE001 —— 只读面不抛
215
+ alternatives["error"] = "%s: %s" % (type(exc).__name__, exc)
216
+
217
+ # ---- 面四:盲区 ----
218
+ blindspots = {
219
+ "negative_evidence": fm.get("negative_evidence", 0),
220
+ "positive_evidence": fm.get("positive_evidence", 0),
221
+ "non_applicable_conditions": fm.get("non_applicable_conditions"),
222
+ nodefile.UNCERTAIN_REFS_FIELD: fm.get(nodefile.UNCERTAIN_REFS_FIELD),
223
+ "verification_state": trust.state_of(fm),
224
+ "validity": _validity,
225
+ "note": ("未决不进二值:DEFER/unresolved 痕迹在『两存/备择』与 "
226
+ "negative_evidence 面;本面只呈现,不下判断。"),
227
+ }
228
+
229
+ # ---- 面五:退役史 ----
230
+ _sh = list(fm.get(lifecycle.HISTORY_FIELD) or [])
231
+ _vh = list(fm.get(trust.HISTORY_FIELD) or [])
232
+ _ev = []
233
+ try:
234
+ _ev = list((evolution.history(cg, nid, limit=cap) or {}).get("entries") or [])
235
+ except Exception: # noqa: BLE001
236
+ pass
237
+ history = {
238
+ lifecycle.HISTORY_FIELD: _sh[-cap:],
239
+ trust.HISTORY_FIELD: _vh[-cap:],
240
+ "evolution": _ev[:cap],
241
+ "totals": {lifecycle.HISTORY_FIELD: len(_sh),
242
+ trust.HISTORY_FIELD: len(_vh), "evolution_scanned": len(_ev)},
243
+ }
244
+
245
+ return {"ok": True, "node_id": nid, "cap": cap,
246
+ "present": present, "sources": sources,
247
+ "alternatives": alternatives, "blindspots": blindspots,
248
+ "history": history}
@@ -172,14 +172,18 @@ class LingshuAdapter(Adapter):
172
172
  "file_head": head})
173
173
  return out
174
174
 
175
- # 生效条件:无参数,遍历 self.cg.index["nodes"] 各节点的 e.get("bucket"),其缺失或为假值(None/空串等)时归入 "<none>" 计数,再把计数字典交 routing.bucket_health 并返回其结果。
175
+ # 生效条件:无参数,计数与生产同源(MdCG._count_buckets:按 entry.bucket 现算、只含分桶层条目),再把计数字典交 routing.bucket_health 并返回其结果。
176
176
  def bucket_health(self):
177
- """桶健康度自检(routing.bucket_health):取证条件路由是否有区分力。"""
177
+ """桶健康度自检(routing.bucket_health):取证条件路由是否有区分力。
178
+
179
+ issue #53-B④:此前本地版把非分桶层节点全归入 `"<none>"` 一桶——在只含
180
+ contextual 的 bench 库上恒得「单桶/巨桶」报警,读数与生产口径(分桶
181
+ 只覆盖 BUCKETED_LAYERS)不一致。改为委托 `MdCG._count_buckets`
182
+ (同源单点:与快照装载/rebuild 同一口径)。
183
+ """
178
184
  from md_cg import routing
179
- counts = {}
180
- for e in list(self.cg.index["nodes"].values()):
181
- b = e.get("bucket") or "<none>"
182
- counts[b] = counts.get(b, 0) + 1
185
+ from md_cg.mdcg import MdCG
186
+ counts = MdCG._count_buckets(self.cg.index["nodes"])
183
187
  return routing.bucket_health(counts)
184
188
 
185
189
 
@@ -40,7 +40,8 @@
40
40
  python -X utf8 -m md_cg.bench_e2e_judge --quick # 冒烟 20题×level 0,1×无LLM
41
41
  python -X utf8 -m md_cg.bench_e2e_judge # 全量 120题×4级×3臂+LLM两轮
42
42
  python -X utf8 -m md_cg.bench_e2e_judge --skip-llm # 只跑确定性两臂
43
- 环境:DEEPSEEK_API_KEY(LLM 臂);数据落 --data-root(默认 D:/program/test/e2e_judge)
43
+ 环境:DEEPSEEK_API_KEY(LLM 臂);数据落 --data-root(默认 <仓根父目录>/test/e2e_judge,
44
+ 可用 MDCG_E2E_JUDGE_ROOT 覆盖)
44
45
  """
45
46
  from __future__ import annotations
46
47
 
@@ -64,7 +65,8 @@ from md_cg.mdcos import MdCGOS # noqa: E402
64
65
  from md_cg import nodefile # noqa: E402
65
66
 
66
67
  DATA_SRC = os.path.join(HERE, "data", "benchmarks", "locomo-zh-500")
67
- DEFAULT_ROOT = r"D:\program\test\e2e_judge"
68
+ DEFAULT_ROOT = os.environ.get("MDCG_E2E_JUDGE_ROOT") or os.path.join(
69
+ os.path.dirname(HERE), "test", "e2e_judge")
68
70
  FUSION_KW = dict(paths=("lexical", "bucket", "entity", "graph", "fuzzy"),
69
71
  fusion="sum")
70
72
  LEVELS_DEFAULT = (0, 1, 2, 4)
package/md_cg/branches.py CHANGED
@@ -111,6 +111,12 @@ def fork(cg, node_ids, branch_id=None, note=None) -> dict:
111
111
  condition_space=fm.get("condition_space"),
112
112
  importance=float(fm.get("importance", 0.5)),
113
113
  verification_basis=fm.get("verification_basis") or "test",
114
+ # A1(2026-10-05,复核补):断言质量字段随副本携带(与
115
+ # verification_basis 同族;缺省 None 时 add 不落键,零行为变化)。
116
+ check_strength=fm.get("check_strength"),
117
+ # A2(2026-10-05):幽灵引用标记随副本携带(正文属性——副本
118
+ # 正文与主支一致则标记有效;缺省 None 不落键)。
119
+ uncertain_refs=fm.get("uncertain_refs"),
114
120
  non_applicable_conditions=fm.get("non_applicable_conditions"),
115
121
  consistency=False,
116
122
  derived_from=[nid], relation="split_from",
@@ -156,8 +162,16 @@ def rewrite(cg, node_id, content, tags=None, importance=None,
156
162
  if isinstance(content, dict) or content is None:
157
163
  return {"ok": False, "error": "content 须为字符串"}
158
164
  for k in ("branch_id", "branched_from", "branch_note", "derived_from",
159
- "relation", "merged_from_branch", "override", "consistency"):
160
- extra.pop(k, None) # 归属/血缘/写法闸不可经 extra 篡改
165
+ "relation", "merged_from_branch", "override", "consistency",
166
+ "uncertain_refs"):
167
+ extra.pop(k, None) # 归属/血缘/写法闸/检测产物不可经 extra 篡改
168
+ # A2(2026-10-05,复核 yellow-1 修):标记按**新正文**重算——本函数是
169
+ # 「改正文的唯一正路」,携带旧标记会与正文脱节(干净正文带陈旧标记 /
170
+ # 幽灵正文漏标,双向都错);与 mdcg.add「正文属性、按当次检测重算」
171
+ # 的声明同口径。
172
+ from . import ghostref as _ghostref
173
+ _ur = _ghostref.find_ghost_phrases(
174
+ str(content or ""), known=set((cg.index.get("nodes") or {}).keys()))
161
175
  cg.add(nid, str(content or ""),
162
176
  layer=fm.get("layer") or e.get("layer") or "knowledge",
163
177
  tags=list(tags if tags is not None else (fm.get("tags") or [])),
@@ -165,6 +179,10 @@ def rewrite(cg, node_id, content, tags=None, importance=None,
165
179
  else (fm.get("importance") or 0.5)),
166
180
  condition_space=fm.get("condition_space"),
167
181
  verification_basis=fm.get("verification_basis") or "test",
182
+ # A1(2026-10-05,复核补):断言质量字段随重写携带(同上)。
183
+ check_strength=fm.get("check_strength"),
184
+ # A2(2026-10-05):幽灵引用标记按新正文重算(见上)。
185
+ uncertain_refs=_ur or None,
168
186
  non_applicable_conditions=fm.get("non_applicable_conditions"),
169
187
  override=True, consistency=False,
170
188
  derived_from=list(fm.get("derived_from") or []),
@@ -203,13 +221,21 @@ def merge(cg, branch_id, reason=None) -> dict:
203
221
  chain = list(old_fm.get("derived_from") or [])
204
222
  if nid not in chain:
205
223
  chain.append(nid)
206
- cg.add(orig, (src or {}).get("content") or "",
224
+ # A2(2026-10-05,复核 yellow-2① 修):主支正文 = 分支正文 ⇒ 标记按
225
+ # 该正文重算(此前不携带 = 幽灵正文回写后无标记,方向与「宁标勿漏」
226
+ # 相反;fork/rewrite 已接线,此处补齐同族)。
227
+ from . import ghostref as _ghostref
228
+ _mcontent = (src or {}).get("content") or ""
229
+ _mur = _ghostref.find_ghost_phrases(
230
+ _mcontent, known=set((cg.index.get("nodes") or {}).keys()))
231
+ cg.add(orig, _mcontent,
207
232
  layer=old_fm.get("layer") or e.get("layer") or "knowledge",
208
233
  tags=list(old_fm.get("tags") or e.get("tags") or []),
209
234
  condition_space=src_fm.get("condition_space"),
210
235
  importance=float(old_fm.get("importance", 0.5)),
211
236
  verification_basis=old_fm.get("verification_basis") or "test",
212
237
  non_applicable_conditions=old_fm.get("non_applicable_conditions"),
238
+ uncertain_refs=_mur or None,
213
239
  override=True, consistency=False,
214
240
  derived_from=chain, relation="merged_from",
215
241
  merged_from_branch=bid, actor="branch_merge")
package/md_cg/census.py CHANGED
@@ -52,7 +52,8 @@ def load(root):
52
52
  try:
53
53
  with open(p, encoding="utf-8") as f:
54
54
  fm, _content = nodefile.loads(f.read())
55
- except OSError:
55
+ except (OSError, UnicodeDecodeError):
56
+ # 损坏非 UTF-8 节点跳过(与 PR #54 读面同口径)
56
57
  continue
57
58
  if not fm:
58
59
  continue
@@ -51,6 +51,7 @@ import os
51
51
  import time
52
52
 
53
53
  from .mdcg import expand_query_terms_weighted
54
+ from . import nodefile
54
55
 
55
56
  # --------------------------------------------------------------------------
56
57
  # 常量(全部可审计、可调)
@@ -250,21 +251,28 @@ def _ban_hit(content, neg_texts):
250
251
  return False
251
252
 
252
253
 
253
- # 生效条件:content 为真时按行过滤,跳过 strip 后以 "#" 开头且含全角或半角冒号的行,其余原行以换行连接返回;content 假值按空串返回 "";
254
+ # 生效条件:委托 nodefile._strip_ccg_segments(content, nodefile.CCG_MARKS)(剥除算法单点)——命中 CCG 要素标题行(判据单点 nodefile._ccg_heading_rest,`^#\s*<要素>` 冒号可有可无)即整段剥除:行内带值(`# 生效条件:v`/前缀式)只剥该行,裸标题(`# 不适用条件`)连同其后首个非空、非标题行(该字段的值行,取值口径同 nodefile.ccg_field_value)一并剥除;其余原行以换行连接返回;content 假值按空串返回 "";
254
255
  def _body_text(content):
255
- """去掉 CCG 声明行(`# 字段:值`)后的正文。
256
+ """去掉 CCG 声明**整段字段**后的正文。
256
257
 
257
258
  自否定看的是「正文/生效条件是否与不适用条件矛盾」,不能把节点自己声明的
258
259
  `# 不适用条件:X` 当成 X 出现在正文里——否则**每个**声明了不适用条件的
259
260
  正常节点都会被误判为自相矛盾(真实 CCG 条目普遍带该字段)。
261
+
262
+ N238:判据复用**单点** `nodefile._ccg_heading_rest`(与写入闸门
263
+ data/policy.json 同一行语义、`mdcos._ccg_field` / `ccg_mark_present` 同源),
264
+ 并连同**无冒号形态的值行**一起剥。此前本地判据是「以 # 开头**且含冒号**」,
265
+ 裸标题 `# 不适用条件` 与其值行双双残留正文:同内容写成带冒号时
266
+ `_body_text == ''`(不命中),写成无冒号时 body 含禁令短语 ⇒ `_ban_hit`
267
+ 恒中 ⇒ self_negation / strength=1.0 ⇒ REJECT(writepipe 下合法节点被拦入
268
+ review_queue 并自动建飞轮工单)。「冒号可有可无」已由
269
+ `nodefile.ccg_mark_present` / `ccg_field_value` 定案(test_ccg_form_parity
270
+ 验收 PASS),本处是漏网的分叉。
271
+
272
+ A3 追认:剥除算法本身也已收单点 `nodefile._strip_ccg_segments`(六要素全剥)
273
+ ——本处不再自持循环,避免与 `nodefile.positive_body` 各写一份而再次漂移。
260
274
  """
261
- out = []
262
- for line in (content or "").splitlines():
263
- s = line.strip()
264
- if s.startswith("#") and (":" in s or ":" in s):
265
- continue
266
- out.append(line)
267
- return "\n".join(out)
275
+ return nodefile._strip_ccg_segments(content, nodefile.CCG_MARKS)
268
276
 
269
277
 
270
278
  # 生效条件:text 假值时按空串处理,返回把任意连续空白(半角/全角空格、\t、\r、\n)折叠为单个半角空格并去掉首尾空白后的字符串;纯空白文本返回空串 "";
@@ -484,16 +492,21 @@ def _recursive_reflect(cg, seeds, tw_pos, tw_neg, max_depth=MAX_DEPTH,
484
492
  # 主入口:三级决策
485
493
  # --------------------------------------------------------------------------
486
494
 
487
- # 生效条件:以 cg.index.nodes 为既有节点、content(假值按 "")经 _new_terms 得 pos/neg 并算 tw_pos/tw_neg(non_applicable_conditions 入参先按 mdcos._is_null_condition 剔除空值语义哨兵——⑧ 漏斗单点);选面(cons200;收口轮①加面内例外):快照级预筛剔「tw_pos 与 tw_neg 皆空(新节点未声明可展开条件)时的非纪律候选」,且**剔除永不触及修前扫描面**(过滤后索引序前 limit 内候选一律保留——面内例外;「修后检出 ⊇ 修前」由此成为与「条目 tags 与盘面 fm.tags 同源」无关的结构保证),候选超 limit 时精比面=相关面(_sift_score 序)前 limit ∪ 保底面(过滤后索引序前 limit ∩ 候选),记 scanned(候选数)/kept(精比数)/truncated(scanned>kept)/hint;按循环中 hard(自否定或纪律命中)→ divergences(同条件槽且 concl < CONCLUSION_SAME)→ strength ≥ CLASH_HIGH → strength ≥ CLASH_LOW → comparable==0 且 (pos or neg) → 否则 ACCEPT 的顺序定 verdict;DEFER 且 int(depth)>0 且 emo["bias"] != "approaching" 时调 _recursive_reflect 补 recursion,auto_flywheel 且 verdict == REJECT 时加 unresolved_id,最后 log 并返回 rec;
495
+ # 生效条件:以 cg.index.nodes 为既有节点、content(假值按 "")经 _new_terms 得 pos/neg 并算 tw_pos/tw_neg(non_applicable_conditions 入参先按 mdcos._is_null_condition 剔除空值语义哨兵——⑧ 漏斗单点);选面(cons200;收口轮①加面内例外):快照级预筛剔「tw_pos 与 tw_neg 皆空(新节点未声明可展开条件)时的非纪律候选」,且**剔除永不触及修前扫描面**(过滤后索引序前 limit 内候选一律保留——面内例外;「修后检出 ⊇ 修前」由此成为与「条目 tags 与盘面 fm.tags 同源」无关的结构保证),候选超 limit 时精比面=相关面(_sift_score 序)前 limit ∪ 保底面(过滤后索引序前 limit ∩ 候选),记 scanned(候选数)/kept(精比数)/truncated(scanned>kept)/hint;按循环中 hard(自否定或纪律命中)→ divergences(同条件槽且 concl < CONCLUSION_SAME)→ strength ≥ CLASH_HIGH → strength ≥ CLASH_LOW → comparable==0 且 (pos or neg) → 否则 ACCEPT 的顺序定 verdict;DEFER 且 int(depth)>0 且 emo["bias"] != "approaching" 时调 _recursive_reflect 补 recursion,auto_flywheel 且 verdict == REJECT 时加 unresolved_id,最后 log_write 为真时 log 并返回 rec;N276:精比环的索引代际探针循环外单探一次(环内 cg.get(nid, probe=False);无探活面载体回落 cg.get(nid),行为零变);
488
496
  def check(cg, content, layer=None, condition_space=None,
489
497
  non_applicable_conditions=None, tags=None, exclude=None,
490
498
  limit=MAX_SCAN, depth=MAX_DEPTH, auto_flywheel=False,
491
- query=None):
499
+ query=None, log_write=True):
492
500
  """节点间自动冲突检测(L0 情绪 → L1 反思 → L2 递归反思)。
493
501
 
494
502
  返回完整判据(可审计):
495
503
  verdict / reason / conflict_strength / emotional / conflicts[] /
496
504
  recursion{} / missing[] / unresolved_id(若触发飞轮)
505
+
506
+ log_write(路线 C,2026-10-05):缺省 True=原行为(每次判定落
507
+ `_consistency.jsonl` 台账)。**只读消费面**(如 auditview 的证据审计)
508
+ 须传 False——判定结果照常返回,但不写台账(复核 R2:审计面承诺
509
+ 「纯读、不落盘」,而 log() 无条件追加会把每次审计变成一次写)。
497
510
  + 选面读数 scanned / kept / truncated(超限时另附 hint)
498
511
 
499
512
  verdict:
@@ -619,9 +632,20 @@ def check(cg, content, layer=None, condition_space=None,
619
632
  scanned = len(cand) # 预筛后候选数(截断前)
620
633
  kept = len(scan_face) # 实际精比数(相关面 ∪ 保底面)
621
634
  truncated = scanned > kept
635
+ # N276(第 32 轮性能面):scan_face 精比环内逐节点 cg.get 会各自重探索引
636
+ # 签名(快照 stat + listdir + 逐分片 stat,单点实测 ~38µs;本库 200 次/
637
+ # 调用全同签名重探)——循环外单探一次、环内 probe=False;语义窗口=本批
638
+ # 读期间「他进程写入」不被探知(毫秒级,与读缓存已声明的进程内一致性
639
+ # 边界同款);无探活面的载体(测试桩)回落原 cg.get(nid),行为零变。
640
+ _probe = getattr(cg, "_maybe_reload_index", None)
641
+ if callable(_probe):
642
+ _probe()
643
+ else:
644
+ _probe = None
622
645
  seeds = []
623
646
  for nid, e in scan_face:
624
- node = cg.get(nid) or {}
647
+ node = (cg.get(nid, probe=False) if _probe is not None
648
+ else cg.get(nid)) or {}
625
649
  fm = node.get("frontmatter") or {}
626
650
  body = node.get("content") or ""
627
651
  e_pos, e_neg = _declared(fm, body)
@@ -769,7 +793,8 @@ def check(cg, content, layer=None, condition_space=None,
769
793
  if auto_flywheel and verdict in FLYWHEEL_TRIGGERS:
770
794
  rec["unresolved_id"] = _fire_flywheel(cg, query or content, verdict,
771
795
  reason, missing, allc)
772
- log(cg, rec)
796
+ if log_write:
797
+ log(cg, rec)
773
798
  return rec
774
799
 
775
800
 
package/md_cg/datapath.py CHANGED
@@ -50,22 +50,49 @@ ENV_AUX_ROOT = "MDCG_AUX_ROOT"
50
50
  DEFAULT_AUX_DIRNAME = ".mdcg"
51
51
 
52
52
 
53
+ #: Windows 保留设备名全集(末段直判用;比对前统一 .upper(),故此处全大写):
54
+ #: DOS 设备名 CON/PRN/AUX/NUL、控制台伪设备 CONIN$/CONOUT$、COM1-9/LPT1-9,
55
+ #: 以及 Windows 承认的上标数字变体 COM¹²³/LPT¹²³(上标 0 与 4-9 不是保留名)。
56
+ _RESERVED_DEVICE_NAMES = frozenset(
57
+ ["CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$"]
58
+ + [f"COM{i}" for i in range(1, 10)]
59
+ + [f"LPT{i}" for i in range(1, 10)]
60
+ + ["COM\u00b9", "COM\u00b2", "COM\u00b3",
61
+ "LPT\u00b9", "LPT\u00b2", "LPT\u00b3"])
62
+
63
+
64
+ # 生效条件:纯字符串判定(不触盘、无平台分支——平台闸在调用处 _abs_host_path);
65
+ # 传入 path 归一("/" → "\")后去尾随反斜杠取末段,末段按首个 "." 截断取 stem、
66
+ # 再去尾随空格,stem.upper() 命中 _RESERVED_DEVICE_NAMES 即返回 True,否则 False。
67
+ # 末段直判不依赖 OS/Python 版本的 GetFullPathNameW 吞并行为——较新 Windows 构建
68
+ # 不再把「目录\aux」吞成 \\.\aux(issue #57),前缀判据随之失效,本函数是稳定判据。
69
+ def _reserved_device_tail(path: str) -> bool:
70
+ seg = path.replace("/", "\\").rstrip("\\").split("\\")[-1]
71
+ stem = seg.split(".", 1)[0].rstrip(" ")
72
+ return stem.upper() in _RESERVED_DEVICE_NAMES
73
+
74
+
53
75
  # 生效条件:p 按仓库既有口径 expanduser+abspath 归一;平台为 Windows(os.name=="nt")
54
- # 且归一结果以设备命名空间前缀("\\\\.\\")开头时抛 ValueError——保留设备名末段
55
- # (aux/con/nul/prn/com1-9/lpt1-9 等)会被 GetFullPathNameW 吞成设备路径
56
- # (例 D:\sandbox\aux → \\.\aux),目录语义静默丢失,密钥/令牌/数据覆盖键随之
57
- # 静默失联;非 Windows 平台该形态是合法目录名字面量,不判定。归一是纯字符串
58
- # 操作不触盘,路径无需存在即可复现。env_key 传覆盖键名(env 变量名或 paths.json
59
- # 键),仅用于错误消息定位误配来源;为空时消息以「该路径」指代。
76
+ # 且命中两因之一时抛 ValueError——① 归一结果以设备命名空间前缀("\\\\.\\")开头:
77
+ # 保留设备名末段(aux/con/nul/prn/com1-9/lpt1-9 等)被 GetFullPathNameW 吞成设备
78
+ # 路径(例 D:\sandbox\aux → \\.\aux);② 归一结果末段直判为保留设备名
79
+ # (_reserved_device_tail):较新 Windows 构建不再吞并(issue #57),旧前缀判据
80
+ # 失效,末段直判不依赖 OS 行为——两因并置互为兜底。后果同:目录语义静默丢失,
81
+ # 密钥/令牌/数据覆盖键随之静默失联;非 Windows 平台该形态是合法目录名字面量,
82
+ # 不判定。归一是纯字符串操作不触盘,路径无需存在即可复现。env_key 传覆盖键名
83
+ # (env 变量名或 paths.json 键),仅用于错误消息定位误配来源;为空时消息以
84
+ # 「该路径」指代。
60
85
  def _abs_host_path(p: str, env_key: str = "") -> str:
61
86
  r = os.path.abspath(os.path.expanduser(p))
62
- if os.name == "nt" and r.startswith("\\\\.\\"):
87
+ if os.name == "nt" and (r.startswith("\\\\.\\")
88
+ or _reserved_device_tail(r)):
63
89
  who = env_key or "该路径"
64
90
  raise ValueError(
65
- f"{who} 归一后解析为 Windows 设备命名空间路径 {r!r}:末段是 Windows "
66
- "保留设备名(aux/con/nul/prn/com1-9/lpt1-9 等),GetFullPathNameW 会把"
67
- "整个目录吞成设备路径,目录语义静默丢失(落在此处的密钥/令牌/数据会"
68
- f"静默失联);请把 {who} 改指向末段不含保留设备名的普通目录。")
91
+ f"{who} 归一后末段是 Windows 保留设备名({r!r}):GetFullPathNameW "
92
+ "可能把整个目录吞成设备路径(\\\\.\\aux 形态,目录语义静默丢失);"
93
+ "较新 Windows 构建即便不吞并,保留设备名末段也一律按误配拒绝——"
94
+ "本守卫末段直判不依赖 OS 吞并行为(issue #57)。请把 "
95
+ f"{who} 改指向末段不含保留设备名的普通目录。")
69
96
  return r
70
97
 
71
98
 
@@ -174,7 +174,7 @@ def self_coverage(cg, node_id, content):
174
174
  return round(_coverage(bigrams(payload(content)), body), 4)
175
175
 
176
176
 
177
- # 生效条件:content 经 payload/bigrams 得空集合时直接返回零值 best(max=0.0、with=None、compared=0);否则遍历 cg.index 的 nodes,跳过 nid==exclude,layer 为真值时只比较 str(layer 字段 or "")==layer 的节点,cg.get(nid) 抛异常/返回假值、或该节点 content 的 bigrams 为空则跳过,每计入一个节点后若 n>=limit 立即 break(故 limit 为 0 或负数时只比较首项即停),返回覆盖度最大者 best(无覆盖度提升时不更新 with/jaccard,compared 为实际计入数)。
177
+ # 生效条件:content 经 payload/bigrams 得空集合时直接返回零值 best(max=0.0、with=None、compared=0);否则**循环外经 cg._maybe_reload_index() 单探一次**(N276:无该方法/不可调用的载体跳过,环内回落原 cg.get(nid)),遍历 cg.index 的 nodes,跳过 nid==exclude,layer 为真值时只比较 str(layer 字段 or "")==layer 的节点,cg.get(nid, probe=False)(无探活面的载体回落 cg.get(nid))抛异常/返回假值、或该节点 content 的 bigrams 为空则跳过,每计入一个节点后若 n>=limit 立即 break(故 limit 为 0 或负数时只比较首项即停),返回覆盖度最大者 best(无覆盖度提升时不更新 with/jaccard,compared 为实际计入数)。
178
178
  def redundancy(cg, content, layer="contextual", exclude=None, limit=MAX_COMPARE):
179
179
  """Q1 重复?——新内容被既有同层节点覆盖的最大比例。"""
180
180
  new = bigrams(payload(content))
@@ -182,6 +182,18 @@ def redundancy(cg, content, layer="contextual", exclude=None, limit=MAX_COMPARE)
182
182
  if not new:
183
183
  return best
184
184
  nodes = ((getattr(cg, "index", None) or {}).get("nodes") or {})
185
+ # N276(第 32 轮性能面):**批内代际探针只做一次**——循环里每次 cg.get
186
+ # 都会经 MdCG.get 首行重算索引签名(快照 stat + listdir + 逐分片 stat,
187
+ # 单点实测 ~38µs;本库 240 次/调用全同签名重探)。此处循环外探一次、
188
+ # 环内 cg.get(nid, probe=False)。语义窗口=**本调用期间「他进程写入」
189
+ # 不被探知**(毫秒级;读取环本就按当次快照判定,与读缓存/热缓存已声明
190
+ # 的进程内一致性边界同款,故注释记账即可);无探活面的载体(测试桩)
191
+ # 回落原 cg.get(nid),行为零变。
192
+ _probe = getattr(cg, "_maybe_reload_index", None)
193
+ if callable(_probe):
194
+ _probe()
195
+ else:
196
+ _probe = None
185
197
  n = 0
186
198
  for nid in list(nodes.keys()):
187
199
  if nid == exclude:
@@ -189,7 +201,8 @@ def redundancy(cg, content, layer="contextual", exclude=None, limit=MAX_COMPARE)
189
201
  if layer and str(nodes[nid].get("layer") or "") != layer:
190
202
  continue
191
203
  try:
192
- node = cg.get(nid)
204
+ node = (cg.get(nid, probe=False) if _probe is not None
205
+ else cg.get(nid))
193
206
  except Exception:
194
207
  node = None
195
208
  if not node: