@furongjun1999/dsh-memory 0.7.2 → 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 (114) hide show
  1. package/README.md +56 -47
  2. package/codebuddy/CODEBUDDY.md +6 -6
  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/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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  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_v28.md +217 -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_v29.md +270 -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_v30.md +231 -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_v31.md +281 -0
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +2 -2
  46. 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
  47. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +29 -8
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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 +6 -5
  54. 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
  55. package/dsh/cordis-patch-profile-web.example.yml +2 -2
  56. package/lib/bridge.js +9 -0
  57. package/lib/hooks.d.ts +8 -1
  58. package/lib/hooks.js +94 -32
  59. package/lib/lib/hook_audit.d.ts +55 -0
  60. package/lib/lib/hook_audit.js +208 -0
  61. package/lib/lib/mutual.js +5 -1
  62. package/lib/lib/roleplay_web.js +17 -2
  63. package/md_cg/auditview.py +248 -0
  64. package/md_cg/bench_e2e_judge.py +4 -2
  65. package/md_cg/branches.py +29 -3
  66. package/md_cg/consistency.py +38 -13
  67. package/md_cg/datapath.py +38 -11
  68. package/md_cg/forgetting.py +15 -2
  69. package/md_cg/freshness.py +32 -11
  70. package/md_cg/ghostref.py +130 -0
  71. package/md_cg/judgment_manifest.py +7 -0
  72. package/md_cg/mcp_server.py +58 -1
  73. package/md_cg/mdcg.py +4923 -4822
  74. package/md_cg/mdcos.py +31 -12
  75. package/md_cg/nodefile.py +77 -13
  76. package/md_cg/readcache.py +29 -4
  77. package/md_cg/selfreport.py +12 -3
  78. package/md_cg/sleep.py +129 -13
  79. package/md_cg/sources.py +1 -1
  80. package/md_cg/state_events.py +131 -0
  81. package/md_cg/sustain.py +1436 -1436
  82. package/md_cg/test_auditview.py +277 -0
  83. package/md_cg/test_autonomy_modes.py +2 -1
  84. package/md_cg/test_boot_openblas_guard.py +197 -0
  85. package/md_cg/test_ccg_form_parity.py +95 -4
  86. package/md_cg/test_cons200_scan_selection.py +4 -2
  87. package/md_cg/test_datapath_device_name.py +62 -3
  88. package/md_cg/test_generation_guard.py +5 -2
  89. package/md_cg/test_ghostref.py +182 -0
  90. package/md_cg/test_interop.py +1 -1
  91. package/md_cg/test_issue52_scan_condition_first.py +1 -1
  92. package/md_cg/test_lock.py +16 -7
  93. package/md_cg/test_m3_h9_semantic_guard.py +1 -1
  94. package/md_cg/test_n230_dirty_replay.py +375 -375
  95. package/md_cg/test_n238_body_text_parity.py +156 -0
  96. package/md_cg/test_n276_probe_hoist.py +199 -0
  97. package/md_cg/test_p4_freshness.py +95 -1
  98. package/md_cg/test_policy_required_ccg.py +3 -1
  99. package/md_cg/test_readcache_prodpath.py +66 -0
  100. package/md_cg/test_sleep.py +58 -1
  101. package/md_cg/test_sleep_p1.py +218 -2
  102. package/md_cg/test_state_events.py +178 -0
  103. package/md_cg/test_time_core_lint.py +8 -1
  104. package/md_cg/test_writepipe.py +8 -5
  105. package/md_cg/tokens.py +11 -2
  106. package/md_cg/writepipe.py +76 -0
  107. package/package.json +2 -2
  108. package/skills/plugin.json +1 -1
  109. package/src/bridge.ts +9 -0
  110. package/src/hooks.ts +576 -517
  111. package/src/lib/hook_audit.ts +243 -0
  112. package/src/lib/mutual.ts +5 -1
  113. package/src/lib/roleplay_web.ts +17 -2
  114. package/zcode/AGENTS.md +6 -6
package/md_cg/mdcos.py CHANGED
@@ -977,7 +977,9 @@ class MdCGOS(MdCG):
977
977
  # 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
978
978
  hits = [d for d in docs
979
979
  if self._like(d[2], d[1], terms,
980
- index_key_hits(d[0], terms, q))
980
+ index_key_hits(d[0], terms, q),
981
+ entry=d[0],
982
+ positive_body=self._positive_body)
981
983
  or (semantic_on() and d[1].get("semantic"))]
982
984
  out = try_stage(hits, TIER_BUCKET_LIKE)
983
985
  if out:
@@ -1004,7 +1006,9 @@ class MdCGOS(MdCG):
1004
1006
  _dif = set(_rstat.get("reach_diffused_paths") or ())
1005
1007
  hits_r = [d for d in docs_r
1006
1008
  if self._like(d[2], d[1], terms,
1007
- index_key_hits(d[0], terms, q))
1009
+ index_key_hits(d[0], terms, q),
1010
+ entry=d[0],
1011
+ positive_body=self._positive_body)
1008
1012
  or (semantic_on() and d[1].get("semantic"))
1009
1013
  or d[0].get("path") in _dif] # 图扩散补召回:无词面命中也放行进打分
1010
1014
  stat["pre_cap"] = len(hits_r) # 与 T2 同序:截断**前**的候选数
@@ -1038,7 +1042,9 @@ class MdCGOS(MdCG):
1038
1042
  # 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
1039
1043
  hits = [d for d in docs_all
1040
1044
  if self._like(d[2], d[1], terms,
1041
- index_key_hits(d[0], terms, q))
1045
+ index_key_hits(d[0], terms, q),
1046
+ entry=d[0],
1047
+ positive_body=self._positive_body)
1042
1048
  or (semantic_on() and d[1].get("semantic"))]
1043
1049
  stat["pre_cap"] = len(hits)
1044
1050
  stat["cap"] = GLOBAL_CAP
@@ -1088,7 +1094,9 @@ class MdCGOS(MdCG):
1088
1094
  # 语义摘要=检索面(设想核心),否则摘要层只在 LIKE 全空时生效
1089
1095
  hits = [d for d in docs
1090
1096
  if self._like(d[2], d[1], terms,
1091
- index_key_hits(d[0], terms, query))
1097
+ index_key_hits(d[0], terms, query),
1098
+ entry=d[0],
1099
+ positive_body=self._positive_body)
1092
1100
  or (semantic_on() and d[1].get("semantic"))]
1093
1101
  if not hits:
1094
1102
  # 兜底池(LIKE 全空 = 无相关度信号):截断依据=importance/created_at
@@ -1346,8 +1354,10 @@ class MdCGOS(MdCG):
1346
1354
  out = []
1347
1355
  for e, fm, c in self._read_many(entries, stat):
1348
1356
  tags = " ".join(str(t) for t in (fm.get("tags") or []))
1349
- # 负条件行不作召回键(反例命中应由 judge 走 REJECT,不该召回节点)
1350
- cov = _weighted_coverage(tw, f"{nodefile.positive_body(c)} {tags}")
1357
+ # 负条件行不作召回键(反例命中应由 judge 走 REJECT,不该召回节点)。
1358
+ # N273:走 `_positive_body` 钩子(readcache 启用时 path 键控缓存
1359
+ # 版)——与 `_like` 侧同一条派生物缓存口径,不再是第二处直调。
1360
+ cov = _weighted_coverage(tw, f"{self._positive_body(e, c)} {tags}")
1351
1361
  if cov <= 0.0:
1352
1362
  continue
1353
1363
  e_dom = routing.route_key(None, e.get("tags"))
@@ -4416,9 +4426,15 @@ class MdCGOS(MdCG):
4416
4426
  def check_consistency(self, content, layer=None, condition_space=None,
4417
4427
  non_applicable_conditions=None, tags=None,
4418
4428
  exclude=None, limit=consistency.MAX_SCAN,
4419
- depth=consistency.MAX_DEPTH, auto_flywheel=False):
4429
+ depth=consistency.MAX_DEPTH, auto_flywheel=False,
4430
+ log_write=True):
4420
4431
  """不落盘地预检一条待写内容是否与既有节点/纪律冲突(三级决策)。
4421
4432
 
4433
+ log_write(路线 C,2026-10-05,透传 consistency.check 同名参数):
4434
+ 缺省 True=原行为(落 `_consistency.jsonl` 台账);**只读消费面**
4435
+ (auditview 证据审计)传 False——判定照常返回、台账不写(「不落盘
4436
+ 预检」承诺的补全:既有 log 是无条件写,只读面消费会变成隐式写)。
4437
+
4422
4438
  对齐《智能的公理化基石》§十一(情绪=信息差二阶变化,独立不参与信任)、
4423
4439
  条件论「反题」(预测与事实冲突)、:273(递归受深度/节点/循环/增益门槛约束)。
4424
4440
 
@@ -4436,7 +4452,7 @@ class MdCGOS(MdCG):
4436
4452
  if not _is_null_condition(x)],
4437
4453
  tags=tags,
4438
4454
  exclude=exclude, limit=limit, depth=depth,
4439
- auto_flywheel=auto_flywheel)
4455
+ auto_flywheel=auto_flywheel, log_write=log_write)
4440
4456
 
4441
4457
  # 生效条件:当 limit 传入时,以 limit(默认 100)调用 consistency.history 并返回其结果;本函数不改变 limit;
4442
4458
  def consistency_history(self, limit=100):
@@ -5103,8 +5119,8 @@ class MdCGSecure(MdCGOS):
5103
5119
  def _neg_coverage(self, terms):
5104
5120
  return [e for e in super()._neg_coverage(terms) if self._readable(e)]
5105
5121
 
5106
- # 生效条件:node_id 在 self.index["nodes"] 中存在且 self._readable(e) 为假时返回 None,否则转 super().get(node_id);索引缺该条目时**先做代际探活重载再判可见性**(N196:索引缺条目 ≡ 本进程陈旧,不得据以跳过读隔离),重载后仍无条目才回落 super().get 的 not_found 语义;super().get 返回真值节点后用**同一次读取所得的 frontmatter**(sensitivity/session)再判一次可见性,为假即返回 None(N213:命中陈旧条目的路径不得以旧快照当判据、以新盘面当正文);
5107
- def get(self, node_id: str):
5122
+ # 生效条件:probe 为真(缺省,行为与改动前逐位一致)时,索引缺该条目才做代际探活重载(N196)且 super().get 照旧探活;probe=False 时两处探活一并跳过(N276:调用方已在本批读之前探过一次,批量循环内的逐节点读不再各自重探);其余判据与 probe 无关:node_id 在 self.index["nodes"] 中存在且 self._readable(e) 为假时返回 None,否则转 super().get(node_id, probe=probe);索引缺该条目时**先做代际探活重载再判可见性**(N196:索引缺条目 ≡ 本进程陈旧,不得据以跳过读隔离),重载后仍无条目才回落 super().get 的 not_found 语义;super().get 返回真值节点后用**同一次读取所得的 frontmatter**(sensitivity/session)再判一次可见性,为假即返回 None(N213:命中陈旧条目的路径不得以旧快照当判据、以新盘面当正文);
5123
+ def get(self, node_id: str, probe: bool = True):
5108
5124
  e = self.index["nodes"].get(node_id)
5109
5125
  if e is None:
5110
5126
  # N196(2026-09-28):索引缺条目**不**等于「节点不存在」——本进程
@@ -5114,11 +5130,14 @@ class MdCGSecure(MdCGOS):
5114
5130
  # 第二次读才归 None,因为那时条目已随重载进入索引)。先探活重载
5115
5131
  # 再判可见性;真不存在时 e 仍为 None,super().get 照旧返回 None。
5116
5132
  # 成本只在**未命中**路径(一次 stat),命中路径零变化。
5117
- self._maybe_reload_index()
5133
+ # N276:probe=False(批内单探针)时本探活同跳——语义窗口见
5134
+ # forgetting.redundancy 同注(本批读期间他进程写入不探知)。
5135
+ if probe:
5136
+ self._maybe_reload_index()
5118
5137
  e = self.index["nodes"].get(node_id)
5119
5138
  if e is not None and not self._readable(e):
5120
5139
  return None # 读隔离:不可见即不存在
5121
- node = super().get(node_id)
5140
+ node = super().get(node_id, probe=probe)
5122
5141
  if node is None:
5123
5142
  return None
5124
5143
  # N213(2026-09-28):命中陈旧条目的路径此前**零探活**——可见性判据取自
package/md_cg/nodefile.py CHANGED
@@ -123,6 +123,22 @@ REPRODUCIBLE_BASIS = ("compiler", "test", "measurement", "formal_proof", "data")
123
123
  #: 来源一致性档:文科断言可用(来源表述一致即可)
124
124
  CONSISTENCY_BASIS = ("textbook", "public_kb")
125
125
 
126
+ # ---- 多主体世界模型对齐(2026-10-05):检验强度字段真源 ----------------------------
127
+ # 「检验强度」分级(过程追踪过程 tracing 方法,见对齐评估 v0.1 §2 L1):一条断言
128
+ # 的**证据能带走多少裁决权重**——hoop(必要证据:不满足即证伪)/ smoking_gun
129
+ # (决定性:单独即可确立)/ doubly_decisive(同时排除竞争假设)。与
130
+ # `verification_basis`(来源**类型**)正交:一个说「凭什么类别的证据」,一个说
131
+ # 「证据的裁决力有多强」。缺省 None=未声明(**不落 fm 键**,存量零迁移、读取侧
132
+ # `.get()` 回落);显式传入时校验闭集(与 VERIFICATION_BASIS 同风格)。
133
+ CHECK_STRENGTHS = ("hoop", "smoking_gun", "doubly_decisive")
134
+ CHECK_STRENGTH_FIELD = "check_strength"
135
+
136
+ #: 幽灵引用标记字段(A2,多主体世界模型对齐 v0.1 §2 L1「转引不得升级」):
137
+ #: 写入链检测到的**无可解析出处**的回指短语清单(短语层),落 fm 供后续
138
+ #: 裁决/检索看见。**正文属性**:每次写入按当次检测重算、不继承(与
139
+ #: check_strength 的声明继承语义相反——正文变了检测就该变)。
140
+ UNCERTAIN_REFS_FIELD = "uncertain_refs"
141
+
126
142
  # ---- 裁定 D(2026-09-19):可验证记忆单元的**字段名真源** --------------------------
127
143
  # 「子功能」的契约角色是**依赖 dependency**(见 CCG_CONTRACT_ROLES),其落字段即
128
144
  # `depends_on`——名字与角色对齐(不叫 sub_features,避免同一概念两种写法)。
@@ -386,6 +402,48 @@ def ccg_mark_present(content: str, mark: str) -> bool:
386
402
  return False
387
403
 
388
404
 
405
+ # 生效条件:content 为真时逐行扫描——命中 marks 中任一要素的 CCG 标题行(判据单点 _ccg_heading_rest,`^#\s*<要素>` 冒号可有可无)即整段剥除:行内带值(`# 生效条件:v`/前缀式)只剥该行,裸标题连同其后首个非空、非标题行(该字段的值行,取值口径同 ccg_field_value)一并剥除;其余原行原样保留(含缩进)以 "\n" 连接返回;content 假值按空串返回。
406
+ def _strip_ccg_segments(content, marks=CCG_MARKS):
407
+ """剥除 CCG 声明**整段字段**后的文本——剥除算法单点。
408
+
409
+ 单点动因(N238 同族):六要素允许无冒号形态(`# 不适用条件` + 换行 + 值行),
410
+ **值行才是内容本体**;只剥标题行会把该字段的正文留在正文里——同一条内容写成
411
+ 带冒号时剥净、写成无冒号时残留,两种写法给出两种结论(该缺陷在
412
+ `consistency._body_text` 已按 N238 收口,本函数把**剥除算法本身**也收成一份,
413
+ 防下游再各写一份循环而漂移)。调用方:`positive_body`(只剥不适用条件)与
414
+ `consistency._body_text`(剥全部六要素)。
415
+
416
+ 判据一律走单点(`_ccg_heading_rest`,与写入闸门 data/policy.json 同一行语义):
417
+ 缩进标题与二级标题不构成「已声明」,与 `ccg_mark_present` 同边界。
418
+ """
419
+ lines = (content or "").split("\n")
420
+ out = []
421
+ i, n = 0, len(lines)
422
+ while i < n:
423
+ rest = None
424
+ for mark in marks:
425
+ rest = _ccg_heading_rest(lines[i], mark)
426
+ if rest is not None:
427
+ break
428
+ if rest is None:
429
+ out.append(lines[i])
430
+ i += 1
431
+ continue
432
+ i += 1
433
+ if rest.strip():
434
+ continue # 行内已带值(冒号式 `# 字段:v` / 前缀式)→ 只剥该行
435
+ while i < n: # 裸标题:其后首个非空、非标题行是值行,同剥
436
+ t = lines[i].strip()
437
+ if not t:
438
+ i += 1
439
+ continue
440
+ if t.startswith("#"):
441
+ break # 下一个标题 ⇒ 本字段值为空,不消费
442
+ i += 1
443
+ break
444
+ return "\n".join(out)
445
+
446
+
389
447
  # 生效条件:content 中出现 "# {mark}:" 或 "# {mark}:"(中/英文冒号)或仅 "# {mark}" 标题行即把该 mark 计入 present 与 required_present;complete 为 required_present 覆盖全部 CCG_REQUIRED、all_present 为 present 覆盖全部 CCG_MARKS,ratio = len(required_present)/len(CCG_REQUIRED),四键连同两个清单一起返回。
390
448
  def ccg_completeness(content: str) -> dict:
391
449
  """CCG 要素齐全度——白箱可审计性的量化指标。
@@ -441,39 +499,45 @@ def is_placeholder_text(value) -> bool:
441
499
  return any(m in s for m in PLACEHOLDER_MARKERS)
442
500
 
443
501
 
444
- # 生效条件:content 中含 "# 不适用条件:"(全角冒号)或 "# 不适用条件:"(半角冒号)即返回 True,两者都不出现返回 False。
502
+ # 生效条件:content 中存在 NEG_FIELD("不适用条件")的 CCG 标题行(判据单点 ccg_mark_present,`^#\s*不适用条件`,冒号可有可无,与写入闸门同一行语义)时返回 True,否则 False。
445
503
  def has_non_applicable(content: str) -> bool:
446
504
  """是否声明了不适用条件——REJECT 路径成立的必要条件。
447
505
 
448
506
  没有不适用条件的节点在白箱下不能 REJECT(无法证明「不适用」),
449
507
  只能 ACCEPT 或 BLINDSPOT。这是第 1 篇第 10 章从 28%→88% 的关键。
508
+
509
+ A3(N238 同族残面):判据单点 = `ccg_mark_present`——本函数原先只认
510
+ 「`# 不适用条件:`/`# 不适用条件:`」两种**带冒号**的字面量,无冒号形态
511
+ (`# 不适用条件` + 换行 + 值行)被判成「没声明」:同一条正文写不写冒号
512
+ 给出两种结论,而「冒号可有可无」已由单点定案(test_ccg_form_parity ①-⑦)。
513
+ 消费面:索引条目 `has_neg_conditions`(mdcg `_entry` / rebuild / `_stage`),
514
+ 进而 `export`/`insight`/`sustain`/`predict` 的统计与边界读数。
450
515
  """
451
- return "# 不适用条件:" in content or "# 不适用条件:" in content
516
+ return ccg_mark_present(content, NEG_FIELD)
452
517
 
453
518
 
454
519
  #: 负条件字段名——它是**反例声明**,不是召回键
455
520
  NEG_FIELD = "不适用条件"
456
521
 
457
522
 
458
- # 生效条件:content 为 None 时按 "" 处理;逐行 strip 后,仅当该行以 "#" 开头且 lstrip("#").strip() 又以 NEG_FIELD 开头时丢弃该行,其余行原样保留(保留原缩进),返回保留行的 "\n".join。
523
+ # 生效条件:content 为 None 时按 "" 处理;逐行剥除 NEG_FIELD 的整段 CCG 声明(剥除算法单点 _strip_ccg_segments——行内带值只剥该行,裸标题连同其后首个非空、非标题的值行一并剥除),其余行原样保留(含原缩进),返回保留行的 "\n".join。
459
524
  def positive_body(content: str) -> str:
460
- """剥离 `# 不适用条件:` 行后的正文——负条件不作召回键。
525
+ """剥离 `# 不适用条件` 声明段后的正文——负条件不作召回键。
461
526
 
462
527
  不适用条件声明的是「什么时候**不**适用」,其触发词是反例。一旦把它当召回
463
528
  键,查询命中反例时节点反而被召回——**恰好在它不该适用的地方被召回**,属实
464
529
  质性错误。反例的正确去向是 `judge_qualification` 的 REJECT 路径(读
465
530
  `frontmatter.non_applicable_conditions`),而不是召回。
466
531
 
467
- 只剥离真正的 CCG 行(以 `#` 开头且字段名匹配),避免误伤正文里恰好以
468
- 「不适用条件」开头的普通句子。
532
+ 只剥离真正的 CCG 行(判据单点 `_ccg_heading_rest`:`# <字段>` 标题前缀、
533
+ 冒号可有可无),避免误伤正文里恰好以「不适用条件」开头的普通句子。
534
+
535
+ A3(N238 同族残面):剥的必须是**整段字段**——无冒号形态(`# 不适用条件` +
536
+ 换行 + 值行)原先只剥标题行,值行(内容本体)残留 ⇒ 反例词面照样进召回键
537
+ (`MdCG._like` 实测命中),与带冒号写法的行为相反。剥除算法收在单点
538
+ `_strip_ccg_segments`(与 `consistency._body_text` 共用一份循环)。
469
539
  """
470
- keep = []
471
- for ln in (content or "").split("\n"):
472
- s = ln.strip()
473
- if s.startswith("#") and s.lstrip("#").strip().startswith(NEG_FIELD):
474
- continue
475
- keep.append(ln)
476
- return "\n".join(keep)
540
+ return _strip_ccg_segments(content, (NEG_FIELD,))
477
541
 
478
542
 
479
543
  # ---- 条件空间 → 生效条件声明 -------------------------------------------------
@@ -25,7 +25,9 @@
25
25
  2. 缓存**解析产物** `(fm, content)`(benchmark 版只缓存原文,parse 仍逐次),
26
26
  **并缓存文档侧检索派生物** `_doc_norm_bigrams`(归一化 bigram——批次 21
27
27
  实测本机热点 88% 在此而非 I/O:内容不变则派生物不变,随读缓存常驻;
28
- Rust `load_docs` 预计算 stripped/db_len 同款理论);
28
+ Rust `load_docs` 预计算 stripped/db_len 同款理论)与 `_positive_body`
29
+ (剥除不适用条件后的召回键正文——N273,8000 池实测 ~102ms/查询,
30
+ 与 bigram 同一 path 键控 + 脏集失效口径,两者恒同步);
29
31
  3. 进程内一致性边界(与 MdStore 相同的诚实边界):跨进程/外部直接改写
30
32
  md 文件不保证可见——认知图的多进程形态(每智能体一进程)各持快照。
31
33
  4. **读失败不固化(C-3 / FI-M02 / N134,2026-09-29)**:只接纳「成功」与
@@ -67,7 +69,7 @@ def direct_read(cg, entry):
67
69
  return fn(entry)
68
70
 
69
71
 
70
- # 生效条件:cg 提供 _read_status(MdCG/MdCGSecure 恒有)时按其**三态标签**包装:第三元素为 None(成功 / 终态真缺)者照脏集精确失效口径入缓存,第三元素非 None(瞬时读失败,fsutil.READ_FAIL_TRANSIENT)者**一律不入缓存**(C-3:不得把可重试的读失败以「新鲜」身份固化);cg 无 _read_status(非 MdCG 载体)时回落包装 cg._read,且空结果一律不接纳(无判别面时的保守侧:宁可重读,不可固化可能是瞬时的缺失);cg._doc_norm_bigrams 存在时同款包装其派生物(_score 热点:文档侧归一化 bigram 只依赖 content,随读缓存一并常驻);包装后 cg._read 与 cg._doc_norm_bigrams 走缓存、cg._read_cache/_norm_bigrams_cache 为缓存字典、cg._read_uncached 为**穿透缓存的原始二态 _read**(direct_read 的真源,返回形状与 install 前逐位一致);返回缓存字典。
72
+ # 生效条件:cg 提供 _read_status(MdCG/MdCGSecure 恒有)时按其**三态标签**包装:第三元素为 None(成功 / 终态真缺)者照脏集精确失效口径入缓存,第三元素非 None(瞬时读失败,fsutil.READ_FAIL_TRANSIENT)者**一律不入缓存**(C-3:不得把可重试的读失败以「新鲜」身份固化);cg 无 _read_status(非 MdCG 载体)时回落包装 cg._read,且空结果一律不接纳(无判别面时的保守侧:宁可重读,不可固化可能是瞬时的缺失);cg._doc_norm_bigrams 存在时同款包装其派生物(_score 热点:文档侧归一化 bigram 只依赖 content,随读缓存一并常驻);cg._positive_body 存在时同款包装其派生物(_like 热点:剥除不适用条件后的正文只依赖 content,N273 与前者同一 path 键控 + 脏集失效口径);包装后 cg._read 与 cg._doc_norm_bigrams/cg._positive_body 走缓存、cg._read_cache/_norm_bigrams_cache/_positive_body_cache 为缓存字典、cg._read_uncached 为**穿透缓存的原始二态 _read**(direct_read 的真源,返回形状与 install 前逐位一致);返回缓存字典。
71
73
  def install(cg):
72
74
  """把 `cg._read`(与派生物钩子)包成**脏集精确失效**的常驻缓存。
73
75
 
@@ -189,14 +191,37 @@ def install(cg):
189
191
 
190
192
  cg._doc_norm_bigrams = _cached_nb
191
193
  cg._norm_bigrams_cache = nb_cache
194
+
195
+ # N273(第 32 轮性能面):`_positive_body` 派生物同款缓存层——同 `entry`
196
+ # path 键、同 `_fresh` 脏集判据、同装载/失效时机(与 `_doc_norm_bigrams`
197
+ # 一字不差的同形结构,故两者命中率与失效行为恒同步)。修前 `_like` 每
198
+ # 查询对每个候选节点直调 `nodefile.positive_body`(逐行扫 CCG 声明段),
199
+ # 8000 池实测每次查询 ~102ms(占热态查询 ~47%)——它却是与 bigram 同
200
+ # 性质的纯函数(只依赖 content),缓存不对称在此收口。
201
+ pb_cache = {}
202
+ if hasattr(cg, "_positive_body"):
203
+ orig_pb = cg._positive_body
204
+
205
+ def _cached_pb(entry, content):
206
+ p = entry["path"]
207
+ gen = getattr(cg._dirty, "write_gen", len(cg._dirty))
208
+ hit = pb_cache.get(p)
209
+ if hit is not None and _fresh(p, hit):
210
+ return hit[1]
211
+ val = orig_pb(entry, content)
212
+ pb_cache[p] = (gen, val)
213
+ return val
214
+
215
+ cg._positive_body = _cached_pb
216
+ cg._positive_body_cache = pb_cache
192
217
  return cache
193
218
 
194
219
 
195
- # 生效条件:cg._read_cache 与 cg._norm_bigrams_cache 均清空(写侧显式兜底;哨兵之外的强制手段),返回清空的条目总数;无缓存时返回 0。
220
+ # 生效条件:cg._read_cache、cg._norm_bigrams_cache 与 cg._positive_body_cache 均清空(写侧显式兜底;哨兵之外的强制手段),返回清空的条目总数;无缓存时返回 0。
196
221
  def clear(cg) -> int:
197
222
  """强制清空(外部批量改写文件后可手动调;正常写路径无需——哨兵自动失效)。"""
198
223
  n = 0
199
- for attr in ("_read_cache", "_norm_bigrams_cache"):
224
+ for attr in ("_read_cache", "_norm_bigrams_cache", "_positive_body_cache"):
200
225
  c = getattr(cg, attr, None)
201
226
  if c:
202
227
  n += len(c)
@@ -126,10 +126,19 @@ def read_all() -> dict:
126
126
  return out
127
127
 
128
128
 
129
- # 生效条件:pids 为可迭代的 pid 集合(通常是「当前进程表中仍在的 md_cg 进程」);删除自报目录中 pid 不在该集合内的 *.json 与遗留 *.tmp,返回删除条数;目录不存在或删除失败逐项吞掉,不抛。
130
- def purge(pids) -> int:
131
- """清掉不属于「仍在的进程」的自报文件(防目录随重启次数膨胀)。"""
129
+ # 生效条件:pids 为可迭代的 pid 集合(通常是「当前进程表中仍在的 md_cg 进程」),allow_empty 为破坏性动作的显式开关;keep 非空时删除自报目录中 pid 不在该集合内的 *.json 与遗留 *.tmp 并返回删除条数;keep 为**空集**且 allow_empty=False(默认)时一律不删并返回 0——空集既可能是「在役进程真的全部退出」也可能是「探测失败/未经校验」,两者不可分,故清空须显式开启;目录不存在或删除失败逐项吞掉,不抛。
130
+ def purge(pids, *, allow_empty: bool = False) -> int:
131
+ """清掉不属于「仍在的进程」的自报文件(防目录随重启次数膨胀)。
132
+
133
+ N255(2026-10-05):keep 为空集时**默认不删任何东西**(fail-closed)——空集既可能来自
134
+ 「在役进程确已全部退出」,也可能来自**一次失败/未经校验的探测**。后者在本仓实证过:
135
+ `scripts/mdcg_stale_servers.py` 的 scan() 尾部曾无条件调用 `purge([])`(探测失败 →
136
+ pids 为空),把自报目录下全部 *.json 一次删光(沙盒 3 件预置全灭)。确要清空时,由
137
+ **探测已成功**的调用方显式传 `allow_empty=True`(见该脚本的 purge_reports())。
138
+ """
132
139
  keep = {int(p) for p in (pids or ())}
140
+ if not keep and not allow_empty:
141
+ return 0
133
142
  n = 0
134
143
  try:
135
144
  names = os.listdir(SELF_REPORT_DIR)
package/md_cg/sleep.py CHANGED
@@ -31,12 +31,18 @@
31
31
 
32
32
  四阶段(§4.4)里本模块的落点:
33
33
 
34
- ① 物化 `materialize()` —— 持根级锁;主库真源面逐字节不变
34
+ ① 物化 `materialize()` —— 持根级锁;挂上影子即成功(`ok=True`);
35
+ 真源面在窗口内漂移只记 `face_stable=False`,
36
+ **不阻断迭代**(漂移交 ③ 的并发闸按「主库
37
+ 优先」逐项处置——issue #60)
35
38
  ② 迭代 (P1;在影子上跑,本模块不管)
36
39
  ③ 对账+提交 `reconcile_and_commit()` —— 持根级锁;Δ 为空即零提交零写入
37
40
  ④ 合并 `merge()` —— 持根级锁;写主库的正是「应写的那部分」
38
41
 
39
- 脚本式用法(只读判读):`python -X utf8 -m md_cg.sleep --status`
42
+ 脚本式用法:`python -X utf8 -m md_cg.sleep --status`(只读判读,含上一轮读数)|
43
+ `--once`(立即跑一轮,不等常驻 tick,同样过四道门)|
44
+ `--once --dry-run`(只预览:跑盘点与四道门,不物化、不记账)|
45
+ `--audit`(只跑两条机械判据)
40
46
  """
41
47
  from __future__ import annotations
42
48
 
@@ -352,7 +358,7 @@ def _busy(phase: str, timeout: float) -> dict:
352
358
  "只有一个能进)")}
353
359
 
354
360
 
355
- # 生效条件:root 为数据根(缺省取 mdcg_root());在 <state_root>/sleep/sleep 的根级锁内,建/复用独立 git 目录并(首次)把真源面入册为 main 基线,再 git worktree add -b sleep/<ts> <shadow> main(分支已存在则复用该分支重试一次);返回含 ok/phase/busy/git_dir/shadow/branch/pathspecs/lock_acquired/source_face_delta/lock 的字典——ok 为真且 source_face_delta 三键全空 = 主库真源面逐字节未变。
361
+ # 生效条件:root 为数据根(缺省取 mdcg_root());在 <state_root>/sleep/sleep 的根级锁内,建/复用独立 git 目录并(首次)把真源面入册为 main 基线,再 git worktree add -b sleep/<ts> <shadow> main(分支已存在则复用该分支重试一次);返回含 ok/face_stable/phase/busy/lock_held/git_dir/shadow/branch/pathspecs/baseline/source_face_delta/lock 的字典——**ok = 物化动作是否成功**(正常挂上影子的成功路径恒真;取不到根级锁或影子未能挂上时保持假);**face_stable = 主库真源面在物化窗口内逐字节未变**(原 ok 的真值;漂移不阻断物化,交 ③ 的并发闸按「主库优先」处置)。
356
362
  def materialize(root: str = None, *, git_dir_path: str = None,
357
363
  shadow: str = None, ts: str = None,
358
364
  timeout: float = DEFAULT_LOCK_TIMEOUT) -> dict:
@@ -360,6 +366,11 @@ def materialize(root: str = None, *, git_dir_path: str = None,
360
366
 
361
367
  幂等:影子已存在时先摘(`worktree remove --force`,非注册目录退化为 rmtree)
362
368
  再挂。分支已存在(同一秒重入)时退化为复用该分支。
369
+
370
+ `ok` 与 `face_stable` 是**两件事**(issue #60):前者只说「影子挂上了没」,
371
+ 后者才是「物化窗口内主库真源面有没有被别的写方动过」。窗口内漂移**不阻断**
372
+ 迭代——活跃工作区(每轮都有新增 contextual/*.md)正是如此;同一 id 的改动
373
+ 由 ③ 的并发闸(`baseline_face` 水位,主库优先)逐项裁决,那才是漂移的处置点。
363
374
  """
364
375
  root = os.path.abspath(root or mdcg_root())
365
376
  G = git_dir_path or git_dir()
@@ -391,7 +402,11 @@ def materialize(root: str = None, *, git_dir_path: str = None,
391
402
  reused = False
392
403
  after = source_face_hashes(root)
393
404
  delta = face_delta(before, after)
394
- return {"ok": not (delta["added"] or delta["removed"] or delta["changed"]),
405
+ # ok = **物化动作成功**(影子已挂上);真源面漂移与否是另一个读数
406
+ # (face_stable),不再拿来否决整轮迭代(issue #60)。
407
+ return {"ok": True,
408
+ "face_stable": not (delta["added"] or delta["removed"]
409
+ or delta["changed"]),
395
410
  "busy": False, "lock_held": True, "phase": "materialize",
396
411
  "git_dir": G, "shadow": S, "branch": branch,
397
412
  "branch_reused": reused, "repo_created": rep["created"],
@@ -655,6 +670,34 @@ def batch_merged(batch: str) -> bool:
655
670
  for r in read_ledger())
656
671
 
657
672
 
673
+ # 生效条件:无入参;返回台账里的**下一轮序号**(已有记录中最大的 round + 1;空台账/无有效 round 时为 1)——CLI `--once` 接着历史轮次编号,不另起一套计数。
674
+ def next_round_index() -> int:
675
+ nums = [r.get("round") for r in read_ledger()]
676
+ nums = [n for n in nums if isinstance(n, int) and not isinstance(n, bool)]
677
+ return (max(nums) + 1) if nums else 1
678
+
679
+
680
+ # 生效条件:rec 为 run_cycle 落台账的记录字典时,返回 ⑦记录步里的 phases 字典(缺该步或形态不符时返回 {})。
681
+ def _record_phases(rec: dict) -> dict:
682
+ for s in (rec.get("steps") or []):
683
+ if isinstance(s, dict) and s.get("step") == "record":
684
+ return s.get("phases") or {}
685
+ return {}
686
+
687
+
688
+ # 生效条件:rec 为 run_cycle 落台账的记录字典时返回「本轮未迭代原因」文案或 None——先取门拦 `rec["skipped"]`,再取 ②③④ 的 skip 文案(物化未完成等);已迭代的一轮返回 None。CLI `--once` 与 `--status` 的同一文案源(不另写第二份判据)。
689
+ def cycle_not_iterated_reason(rec: dict):
690
+ if not isinstance(rec, dict):
691
+ return None
692
+ if rec.get("skipped"):
693
+ return rec["skipped"]
694
+ for s in (rec.get("steps") or []):
695
+ if isinstance(s, dict) and s.get("step") in ("induce", "promote", "scrub") \
696
+ and s.get("skipped"):
697
+ return s["skipped"]
698
+ return None
699
+
700
+
658
701
  # 生效条件:root 给定且 md_cg.mdcos 可导入时返回该 root 上的 MdCGOS 实例;导入失败抛原异常。
659
702
  def _open_cg(root: str):
660
703
  from .mdcos import MdCGOS
@@ -1151,13 +1194,19 @@ def ledger_steps(*, scan: dict, iterated: dict = None, delta_digest: dict = None
1151
1194
  return steps
1152
1195
 
1153
1196
 
1154
- # 生效条件:给定 cg(主库)与各相位开关时,按 §3.2 的稳态判据与 §4.4 的四阶段跑一轮睡眠周期并返回台账记录;enabled=False / merge_mode="never" / 窗口外 / 候选数为 0 四种情形**只记账不动手**(②③④记 skipped);batch 为已合并批次时直接返回 {"idempotent": True} 且零动作。
1197
+ # 生效条件:给定 cg(主库)与各相位开关时,按 §3.2 的稳态判据与 §4.4 的四阶段跑一轮睡眠周期并返回台账记录;enabled=False / merge_mode="never" / 窗口外 / 候选数为 0 四种情形**只记账不动手**(②③④记 gate_reason);物化未完成(busy/error)时 ②③④ 记「物化未完成:…」(与门拦文案**区分**);batch 为已合并批次时直接返回 {"idempotent": True} 且零动作;dry_run=True 时只跑感知盘点与四道门评估(**不物化、不记账**)。
1155
1198
  def run_cycle(cg=None, *, root: str = None, batch: str = None,
1156
1199
  enabled: bool = True, merge_mode: str = None,
1157
1200
  scrub_apply: bool = None, window: str = None,
1158
1201
  round_index: int = 1, now=None, git_dir_path: str = None,
1159
- shadow: str = None, timeout: float = DEFAULT_LOCK_TIMEOUT) -> dict:
1160
- """一轮睡眠周期(§3.1 九步 + §4.4 四阶段)。"""
1202
+ shadow: str = None, dry_run: bool = False,
1203
+ timeout: float = DEFAULT_LOCK_TIMEOUT) -> dict:
1204
+ """一轮睡眠周期(§3.1 九步 + §4.4 四阶段)。
1205
+
1206
+ `dry_run=True` 是**预览档**(`--once --dry-run`):只跑 ① 感知盘点与四道门
1207
+ 评估(总开关 / 合并策略 / 窗口 / 稳态),**不物化、不记账**——早退点刻意
1208
+ 置于 `materialize` 之前,也不触 `_append_ledger`。
1209
+ """
1161
1210
  root = os.path.abspath(root or (cg.root if cg is not None else mdcg_root()))
1162
1211
  if cg is None:
1163
1212
  cg = _open_cg(root)
@@ -1179,7 +1228,10 @@ def run_cycle(cg=None, *, root: str = None, batch: str = None,
1179
1228
  candidates=None, steps=ledger_steps(
1180
1229
  scan={}, skip_reason="幂等:同批次已合并",
1181
1230
  round_index=round_index, prev_candidates=prev))
1182
- _append_ledger(rec)
1231
+ if dry_run:
1232
+ rec["dry_run"] = True # 预览档:**不记账**
1233
+ else:
1234
+ _append_ledger(rec)
1183
1235
  return rec
1184
1236
 
1185
1237
  # ① 感知盘点(主库,只读)——稳态判据的口径
@@ -1200,14 +1252,37 @@ def run_cycle(cg=None, *, root: str = None, batch: str = None,
1200
1252
  elif not scan["candidates"]:
1201
1253
  gate_reason = "稳态:盘点候选数为 0(只记账不动手)"
1202
1254
 
1255
+ if dry_run:
1256
+ # 预览档:四道门已评完即止——**不物化、不记账**(连影子都不建)。
1257
+ rec.update(dry_run=True, skipped=gate_reason,
1258
+ planned=(None if gate_reason
1259
+ else "将执行物化→迭代→对账→合并"),
1260
+ steps=ledger_steps(scan=scan, round_index=round_index,
1261
+ prev_candidates=prev,
1262
+ skip_reason=gate_reason or ""))
1263
+ return rec
1264
+
1203
1265
  phases, iterated, delta, digest = {}, None, {}, {"n": 0, "ids": [], "kinds": {}}
1266
+ mat_fail = None
1204
1267
  if gate_reason is None:
1205
1268
  m = materialize(root, git_dir_path=git_dir_path, shadow=shadow,
1206
1269
  ts=b, timeout=timeout)
1207
1270
  phases["materialize"] = {k: m.get(k) for k in
1208
- ("ok", "busy", "branch", "shadow", "git_dir",
1209
- "baseline", "source_face_delta")}
1271
+ ("ok", "face_stable", "busy", "branch", "shadow",
1272
+ "git_dir", "baseline", "source_face_delta",
1273
+ "reason", "error")}
1274
+ if not m.get("ok"):
1275
+ # 物化**未完成**(取不到根级锁 / 影子未能挂上)——与「门拦」区分记账:
1276
+ # 门拦是「按设计不动手」,这里是「想动手却没做成」,用户要能一眼分开。
1277
+ mat_fail = ("物化未完成:根级锁被占(另一进程在周期内)"
1278
+ if m.get("busy")
1279
+ else "物化未完成:%s" % (m.get("error") or "未知原因"))
1210
1280
  if m.get("ok"):
1281
+ # **物化成功即迭代**(issue #60):`ok` 现在只说「影子挂上了没」;
1282
+ # 物化窗口内主库真源面的漂移另记 `face_stable`,**不阻断**迭代——
1283
+ # 活跃工作区(每轮都有新写入)本就如此。迭代期间主库被改动/新增的
1284
+ # 同一 id,由下面对账段的并发闸(`baseline_face` 水位)逐项裁
1285
+ # 「主库优先」——那才是漂移的处置点,不是在这里自我否决。
1211
1286
  S = m["shadow"]
1212
1287
  iterated = iterate(S, scrub_apply=scrub)
1213
1288
  phases["iterate"] = iterated
@@ -1255,25 +1330,64 @@ def run_cycle(cg=None, *, root: str = None, batch: str = None,
1255
1330
  rec["steps"] = ledger_steps(scan=scan, iterated=iterated,
1256
1331
  delta_digest=digest, phases=phases,
1257
1332
  round_index=round_index, prev_candidates=prev,
1258
- skip_reason=gate_reason or "")
1333
+ skip_reason=gate_reason or mat_fail or "")
1259
1334
  rec["skipped"] = gate_reason
1260
1335
  _append_ledger(rec)
1261
1336
  return rec
1262
1337
 
1263
1338
 
1264
- # 生效条件:argv 含 --status 时打印当前解析结果(git_dir / shadow / lock / pathspecs / tracking / history 四读数)的 JSON 并返回 0;argv 含 --audit 时只打印两条机械判据结论(不合规返回 1);无参数时打印解析结果。
1339
+ # 生效条件:recs 为 read_ledger(limit=1) 的返回(空列表或单条记录)时,返回该记录的 CLI 视图(t/batch/round/candidates/skipped / ②③④ skip 文案 / phases.materialize 摘要 / 未迭代原因);无记录返回 None。
1340
+ def _last_cycle_view(recs):
1341
+ if not recs:
1342
+ return None
1343
+ rec = recs[-1]
1344
+ return {"t": rec.get("t"), "batch": rec.get("batch"),
1345
+ "round": rec.get("round"), "candidates": rec.get("candidates"),
1346
+ "skipped": rec.get("skipped"),
1347
+ "steps_skip": {s.get("step"): s.get("skipped")
1348
+ for s in (rec.get("steps") or [])
1349
+ if isinstance(s, dict)
1350
+ and s.get("step") in ("induce", "promote", "scrub")},
1351
+ "materialize": _record_phases(rec).get("materialize"),
1352
+ "not_iterated_reason": cycle_not_iterated_reason(rec)}
1353
+
1354
+
1355
+ # 生效条件:argv 含 --once 时以 env 读取器为参数跑一轮 run_cycle(round 取 next_round_index();--dry-run 同用时只预览、不物化不记账)并打印整条记录与「本轮未迭代原因」(未迭代时,文案与台账同源);argv 含 --audit 时只打印两条机械判据结论(不合规返回 1);--dry-run **不与 --once 同用**时返回 2(报错,不执行任何动作);其余情形打印当前解析结果(git_dir / shadow / lock / pathspecs / tracking / history / **last_cycle** 读数)的 JSON 并返回 0。
1265
1356
  def _main(argv=None) -> int:
1266
- ap = argparse.ArgumentParser(description="睡眠周期 git 机制(只读判读面)")
1357
+ ap = argparse.ArgumentParser(description="睡眠周期 git 机制(只读判读 + 手动一轮)")
1267
1358
  ap.add_argument("--root", default=None, help="数据根(缺省 mdcg_root())")
1268
1359
  ap.add_argument("--git-dir", dest="git_dir_path", default=None)
1269
1360
  ap.add_argument("--shadow", default=None)
1270
1361
  ap.add_argument("--status", action="store_true", help="打印当前解析与判据读数")
1271
1362
  ap.add_argument("--audit", action="store_true", help="只跑两条机械判据")
1363
+ ap.add_argument("--once", action="store_true",
1364
+ help="立即跑一轮睡眠周期(不等常驻循环的 tick;同样过四道门)")
1365
+ ap.add_argument("--dry-run", dest="dry_run", action="store_true",
1366
+ help="只预览(须与 --once 同用):跑盘点与四道门,不物化、不记账")
1272
1367
  a = ap.parse_args(argv)
1368
+ if a.dry_run and not a.once:
1369
+ sys.stderr.write("--dry-run 须与 --once 同用:"
1370
+ "单独使用不执行任何动作(预览一轮请用 --once --dry-run)\n")
1371
+ return 2
1273
1372
  root = os.path.abspath(a.root or mdcg_root())
1274
1373
  G = a.git_dir_path or git_dir()
1275
1374
  shadow = a.shadow or shadow_dir()
1276
1375
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
1376
+ if a.once:
1377
+ rec = run_cycle(None, root=root, enabled=sleep_enabled(),
1378
+ merge_mode=sleep_merge_mode(),
1379
+ scrub_apply=sleep_scrub_apply(), window=sleep_window(),
1380
+ round_index=next_round_index(), dry_run=a.dry_run,
1381
+ git_dir_path=a.git_dir_path, shadow=a.shadow)
1382
+ print(json.dumps(rec, ensure_ascii=False, indent=2))
1383
+ if rec.get("dry_run"):
1384
+ print("预览:" + (rec.get("planned")
1385
+ or ("门拦:%s" % (rec.get("skipped") or ""))))
1386
+ else:
1387
+ why = cycle_not_iterated_reason(rec)
1388
+ if why:
1389
+ print("本轮未迭代原因:" + why)
1390
+ return 0
1277
1391
  if a.audit:
1278
1392
  tr = audit_tracking(root, git_dir_path=G)
1279
1393
  hi = audit_history(root, git_dir_path=G)
@@ -1289,6 +1403,8 @@ def _main(argv=None) -> int:
1289
1403
  if os.path.isdir(G):
1290
1404
  out["tracking"] = audit_tracking(root, git_dir_path=G)
1291
1405
  out["history"] = audit_history(root, git_dir_path=G)
1406
+ # 上一轮读数:从 CLI 就能看到「上次为何没迭代」(门拦 / 物化未完成 / 幂等)。
1407
+ out["last_cycle"] = _last_cycle_view(read_ledger(limit=1))
1292
1408
  print(json.dumps(out, ensure_ascii=False, indent=2))
1293
1409
  return 0
1294
1410
 
package/md_cg/sources.py CHANGED
@@ -258,7 +258,7 @@ class DSHSessionSource(Source):
258
258
 
259
259
 
260
260
  # --------------------------------------------------------------------------
261
- # 蜂巢任务事件源(M6:hive/jobs → contextual,D:\2_ai 蜂巢记忆架构设计.md §5)
261
+ # 蜂巢任务事件源(M6:hive/jobs → contextual,外部设计稿 蜂巢记忆架构设计.md §5)
262
262
  # --------------------------------------------------------------------------
263
263
 
264
264
  class HiveJobsSource(Source):