@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
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):
@@ -0,0 +1,131 @@
1
+ # -*- coding: utf-8 -*-
2
+ """状态事件账本(append-only)——多主体世界模型 L0「事件必记账」的最小承载。
3
+
4
+ 背景(对齐评估 v0.1,§2 L0):CD-WHALE-01(鲸娘语料 731 轮)的六类缺陷全部落在
5
+ 「迁移/撤回/启用/来源」四个**状态操作**上(属性记得住、状态记不住);CTP-P5
6
+ 回放的敏感性边界一证明「现行侧若只有状态陈述(T2)而无事件记录(T1),设施回滚
7
+ 只能判 DEFER」——故「事件必记账」是状态链(基线→变更→后续引用)与退役/否定
8
+ 语义的前置(跨切纪律 1)。
9
+
10
+ 设计约束(对齐评估 v0.1 复核修订后的口径):
11
+ · **append-only**:`_recent.jsonl` 为窗口滚动面(`DEFAULT_RECENT_WINDOW=200`,
12
+ `roll_recent` 超限即截断),不可作一等记录;本账本只追加、不读改写、不截断。
13
+ · **单一真源**:事件是源、槽位现值是投影(未来 L2 寄存器由本账本物化;
14
+ 落盘位置与 `_evolution` 台账/审计同级:`<root>/_state_events.jsonl`)。
15
+ · **五元结构**:主体 subject / 槽位 slot / 旧值 old → 新值 new / 轮或序号
16
+ seq / 出处 evidence(+ kind 五类变迁事件 + actor 断言者 + t 时间戳)。
17
+
18
+ KINDS 五类对齐《角色扮演一致性锚调研 v0.1》§3.4「变迁事件」:
19
+ 迁移 migration / 撤回 retraction / 启用 enablement / 购买获得 acquisition /
20
+ 替换 replacement。「撤回」语义:old 有值而 new 为空(值作废、无新值);
21
+ 「启用」:old 空而 new 有值;「迁移/替换」:old 与 new 皆有值。
22
+ 分类本身不进判据(记账不判语义),只作检索面标签。
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import os
28
+ import sys
29
+ import time
30
+
31
+ from .fsutil import FileLock
32
+
33
+ #: 账本文件名(库根下,与 `_recent.jsonl`/`_write_2pc.jsonl` 同级)。
34
+ STATE_EVENTS_NAME = "_state_events.jsonl"
35
+
36
+ #: 五类变迁事件(角色扮演一致性锚调研 v0.1 §3.4)。
37
+ KINDS = ("migration", "retraction", "enablement", "acquisition", "replacement")
38
+
39
+ #: 坏行容忍记账(容忍 ≠ 静默——本仓 N225 已确立的判据:跳过要可观测)。
40
+ BAD_ROWS = 0 # 累计**跳过动作次数**(同一坏行每次 read 均计一次;非按行去重)
41
+ _BAD_ROW_SAMPLES = [] # 有界样本:(行号, 行首 80 字符, 异常类型名)
42
+ _BAD_ROW_SAMPLE_CAP = 8
43
+
44
+
45
+ def _note_bad_row(lineno: int, line: str, exc: Exception):
46
+ global BAD_ROWS
47
+ BAD_ROWS += 1
48
+ if len(_BAD_ROW_SAMPLES) < _BAD_ROW_SAMPLE_CAP:
49
+ _BAD_ROW_SAMPLES.append((lineno, line[:80], type(exc).__name__))
50
+ sys.stderr.write(
51
+ f"[state_events] 跳过坏行(第 {lineno} 行,累计 {BAD_ROWS}):"
52
+ f"{type(exc).__name__}: {exc}\n")
53
+
54
+
55
+ # 生效条件:cg 可解析出 root 时,返回 os.path.join(cg.root, STATE_EVENTS_NAME)。
56
+ def ledger_path(cg) -> str:
57
+ return os.path.join(cg.root, STATE_EVENTS_NAME)
58
+
59
+
60
+ # 生效条件:subject 与 slot 去空白后必须非空(否则 ValueError);kind 非 None 时必须属于
61
+ # KINDS(否则 ValueError);old 与 new 不得同时为 None(否则 ValueError);通过后在
62
+ # FileLock(ledger_path) 内以追加模式写一行 JSON(ensure_ascii=False,flush+fsync),
63
+ # 返回落盘记录 dict(不改任何既有文件、不读改写、不截断)。
64
+ def append(cg, subject, slot, old=None, new=None, kind=None, seq=None,
65
+ evidence="", actor="", extra=None) -> dict:
66
+ """追加一条状态事件(append-only;跨进程锁;崩溃不留半截文件)。"""
67
+ sub = str(subject or "").strip()
68
+ sl = str(slot or "").strip()
69
+ if not sub:
70
+ raise ValueError("状态事件必须声明主体(subject)")
71
+ if not sl:
72
+ raise ValueError("状态事件必须声明槽位(slot)")
73
+ if kind is not None and kind not in KINDS:
74
+ raise ValueError(f"未知变迁事件类型:{kind}(允许:{KINDS})")
75
+ if old is None and new is None:
76
+ raise ValueError("状态事件 old 与 new 不能同时为空(空事件无意义)")
77
+ rec = {
78
+ "t": time.time(),
79
+ "subject": sub,
80
+ "slot": sl,
81
+ "old": old,
82
+ "new": new,
83
+ "kind": kind,
84
+ "seq": seq,
85
+ "evidence": str(evidence or ""),
86
+ "actor": str(actor or ""),
87
+ }
88
+ if extra:
89
+ rec.update(extra)
90
+ p = ledger_path(cg)
91
+ os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
92
+ with FileLock(p):
93
+ with open(p, "a", encoding="utf-8") as f:
94
+ f.write(json.dumps(rec, ensure_ascii=False) + "\n")
95
+ f.flush()
96
+ os.fsync(f.fileno())
97
+ return rec
98
+
99
+
100
+ # 生效条件:ledger_path 不存在时返回 [];存在时逐行 json.loads(空行跳过;非对象载荷或
101
+ # JSON 解析失败按坏行处置——计数+有界样本+stderr 告警后跳过,不中断读);subject/slot/kind
102
+ # **非 None** 时按对应字段过滤(空串亦为合法过滤值,2026-10-05 复核口径钉正);limit 为
103
+ # 真值时只保留最后 limit 条;返回记录列表(落盘序)。
104
+ def read(cg, subject=None, slot=None, kind=None, limit=0) -> list:
105
+ """读回(可按 subject/slot/kind 过滤;limit=0 不截断)。坏行跳过并记账。"""
106
+ p = ledger_path(cg)
107
+ out = []
108
+ if not os.path.exists(p):
109
+ return out
110
+ with open(p, encoding="utf-8", errors="replace") as f:
111
+ for i, line in enumerate(f, 1):
112
+ line = line.strip()
113
+ if not line:
114
+ continue
115
+ try:
116
+ rec = json.loads(line)
117
+ if not isinstance(rec, dict):
118
+ raise ValueError("行载荷非对象")
119
+ except Exception as e: # noqa: BLE001 —— 容忍≠静默:记账后跳过
120
+ _note_bad_row(i, line, e)
121
+ continue
122
+ if subject is not None and rec.get("subject") != subject:
123
+ continue
124
+ if slot is not None and rec.get("slot") != slot:
125
+ continue
126
+ if kind is not None and rec.get("kind") != kind:
127
+ continue
128
+ out.append(rec)
129
+ if limit:
130
+ out = out[-int(limit):]
131
+ return out