@furongjun1999/dsh-memory 0.4.8 → 0.4.10

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 (232) hide show
  1. package/README.md +54 -26
  2. package/codebuddy/CODEBUDDY.md +196 -195
  3. package/codebuddy/README.md +13 -1
  4. package/codebuddy/mcp.json +9 -0
  5. package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
  6. package/docs/README.md +1 -1
  7. package/docs/discipline/harnesses.yaml +18 -7
  8. package/docs/discipline/templates/full.md.tmpl +4 -3
  9. package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
  10. package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
  11. package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
  12. package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
  13. package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
  14. package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
  15. package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -0
  16. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +102 -0
  17. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
  18. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
  19. 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 +503 -0
  20. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
  21. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
  22. package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
  23. package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -0
  24. 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 +47 -45
  25. package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/{206_v0.4.md → 206_v0.5.md} +92 -4
  26. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -0
  27. package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
  28. package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
  29. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +2 -2
  30. package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
  31. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +2 -2
  32. 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 +434 -433
  33. package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
  34. package/dsh/README.md +33 -0
  35. package/dsh/cordis.yml.example +13 -7
  36. package/dsh/hive-mcp-probe.mjs +94 -0
  37. package/dsh/hive-mcp.example.yml +62 -0
  38. package/dsh/update-lingshu.bat +11 -0
  39. package/dsh/update-lingshu.ps1 +337 -0
  40. package/lib/hooks.d.ts +3 -0
  41. package/lib/hooks.js +17 -23
  42. package/lib/index.d.ts +4 -2
  43. package/lib/index.js +29 -6
  44. package/lib/lib/datapath.d.ts +76 -1
  45. package/lib/lib/datapath.js +199 -13
  46. package/lib/lib/mdcg_client.d.ts +40 -3
  47. package/lib/lib/mdcg_client.js +46 -22
  48. package/lib/lib/mutual.js +4 -4
  49. package/lib/lib/token_store.js +4 -5
  50. package/md_cg/audit.py +17 -2
  51. package/md_cg/autonomy.py +86 -15
  52. package/md_cg/backfill.py +36 -1
  53. package/md_cg/backfill_bigdomain.py +34 -0
  54. package/md_cg/bench6_arms.py +28 -1
  55. package/md_cg/bench6_common.py +10 -1
  56. package/md_cg/bench6_competitors.py +6 -1
  57. package/md_cg/bench_axis_domain.py +9 -1
  58. package/md_cg/bench_blind_comp.py +7 -1
  59. package/md_cg/bench_en_atoms_public.py +9 -0
  60. package/md_cg/bench_governance.py +348 -0
  61. package/md_cg/bench_lme_zh.py +16 -1
  62. package/md_cg/bench_locomo.py +2 -1
  63. package/md_cg/bench_locomo_zh.py +16 -1
  64. package/md_cg/bench_locomo_zh_public.py +4 -1
  65. package/md_cg/bench_longmem.py +2 -1
  66. package/md_cg/bench_membench.py +27 -1
  67. package/md_cg/bench_p0.py +4 -1
  68. package/md_cg/bench_progressive.py +13 -1
  69. package/md_cg/bench_role_views.py +238 -0
  70. package/md_cg/bench_task_ab.py +8 -1
  71. package/md_cg/bench_task_ab_llm.py +13 -1
  72. package/md_cg/bench_unified_en.py +6 -1
  73. package/md_cg/bench_zh_mad.py +20 -1
  74. package/md_cg/blindspot_tickets.py +123 -0
  75. package/md_cg/branches.py +12 -1
  76. package/md_cg/build_postings.py +73 -0
  77. package/md_cg/ccgc.py +67 -2
  78. package/md_cg/census.py +5 -1
  79. package/md_cg/chain.py +24 -3
  80. package/md_cg/codeindex.py +134 -17
  81. package/md_cg/coldverify.py +265 -0
  82. package/md_cg/comment_gate.py +338 -0
  83. package/md_cg/cond_compose.py +190 -0
  84. package/md_cg/cond_facts.py +155 -0
  85. package/md_cg/cond_template.json +107 -0
  86. package/md_cg/condition_anchor.py +143 -0
  87. package/md_cg/conformance.py +69 -4
  88. package/md_cg/consistency.py +24 -1
  89. package/md_cg/consolidate.py +53 -2
  90. package/md_cg/corpus.py +4 -0
  91. package/md_cg/crosscheck.py +42 -2
  92. package/md_cg/crypto.py +35 -1
  93. package/md_cg/d_meta.py +310 -0
  94. package/md_cg/datapath.py +201 -26
  95. package/md_cg/docindex.py +122 -1
  96. package/md_cg/eval_common.py +29 -1
  97. package/md_cg/evidence.py +27 -1
  98. package/md_cg/evolution.py +21 -1
  99. package/md_cg/export.py +11 -1
  100. package/md_cg/forgetting.py +23 -1
  101. package/md_cg/fsutil.py +18 -1
  102. package/md_cg/hotcache.py +214 -0
  103. package/md_cg/hyperedge.py +251 -0
  104. package/md_cg/identity.py +18 -1
  105. package/md_cg/insight.py +17 -1
  106. package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
  107. package/md_cg/lexicon/build_standard_en.py +171 -168
  108. package/md_cg/lexicon/expand_en_zh.py +6 -0
  109. package/md_cg/lifecycle.py +12 -1
  110. package/md_cg/linkref.py +281 -0
  111. package/md_cg/links.py +29 -1
  112. package/md_cg/mcp_server.py +362 -43
  113. package/md_cg/md_whitebox.py +53 -1
  114. package/md_cg/mdcg.py +1003 -27
  115. package/md_cg/mdcos.py +558 -36
  116. package/md_cg/metacognition.py +37 -2
  117. package/md_cg/migrate.py +4 -0
  118. package/md_cg/migrate_aeis.py +221 -213
  119. package/md_cg/migrate_roleplay.py +8 -0
  120. package/md_cg/migrate_wisdom_graph.py +14 -1
  121. package/md_cg/mreview/__main__.py +3 -0
  122. package/md_cg/mreview/bundle.py +8 -0
  123. package/md_cg/mreview/candidates.py +9 -0
  124. package/md_cg/mreview/govern.py +21 -1
  125. package/md_cg/mreview/locate.py +34 -0
  126. package/md_cg/mreview/pipeline.py +29 -1
  127. package/md_cg/mreview/ruleset.py +16 -1
  128. package/md_cg/nodefile.py +233 -3
  129. package/md_cg/pooling.py +23 -1
  130. package/md_cg/postings.py +298 -0
  131. package/md_cg/predict.py +89 -9
  132. package/md_cg/progressive.py +3 -0
  133. package/md_cg/protect.py +14 -1
  134. package/md_cg/protocol.py +372 -0
  135. package/md_cg/provenance.py +262 -0
  136. package/md_cg/reach.py +453 -0
  137. package/md_cg/refindex.py +47 -2
  138. package/md_cg/refine.py +20 -1
  139. package/md_cg/roleviews.py +89 -0
  140. package/md_cg/routing.py +76 -0
  141. package/md_cg/scrub.py +63 -2
  142. package/md_cg/security.py +26 -1
  143. package/md_cg/self_state.py +64 -1
  144. package/md_cg/selfreport.py +151 -0
  145. package/md_cg/semantic/canonical.py +5 -0
  146. package/md_cg/semantic/en_normalizer.py +364 -355
  147. package/md_cg/semantic/zh_en_atoms.py +139 -136
  148. package/md_cg/signer.py +41 -1
  149. package/md_cg/sources.py +583 -547
  150. package/md_cg/statushdr.py +179 -0
  151. package/md_cg/stg.py +59 -18
  152. package/md_cg/subgraph.py +23 -0
  153. package/md_cg/sustain.py +56 -1
  154. package/md_cg/tasks.py +26 -2
  155. package/md_cg/test_autonomy.py +26 -0
  156. package/md_cg/test_bench_governance.py +102 -0
  157. package/md_cg/test_blindspot_tickets.py +166 -0
  158. package/md_cg/test_ccgc.py +10 -0
  159. package/md_cg/test_codeindex.py +338 -0
  160. package/md_cg/test_comment_gate.py +187 -0
  161. package/md_cg/test_cond_compose_anchors.py +76 -0
  162. package/md_cg/test_condition_anchor.py +82 -0
  163. package/md_cg/test_d_meta.py +412 -0
  164. package/md_cg/test_datapath_root.py +188 -0
  165. package/md_cg/test_gain_gate.py +47 -1
  166. package/md_cg/test_hot_cold.py +187 -0
  167. package/md_cg/test_hyperedge.py +245 -0
  168. package/md_cg/test_linkref.py +306 -0
  169. package/md_cg/test_md_access_parity.py +15 -3
  170. package/md_cg/test_mr_m1.py +108 -18
  171. package/md_cg/test_mr_m3.py +8 -1
  172. package/md_cg/test_p26_refindex.py +49 -20
  173. package/md_cg/test_p27_docindex.py +236 -2
  174. package/md_cg/test_p2_mcp.py +1 -1
  175. package/md_cg/test_p31_insight.py +24 -0
  176. package/md_cg/test_p44_md_whitebox.py +14 -1
  177. package/md_cg/test_protocol.py +243 -0
  178. package/md_cg/test_reach.py +378 -0
  179. package/md_cg/test_reach_keys.py +201 -0
  180. package/md_cg/test_reach_meta_exits.py +145 -0
  181. package/md_cg/test_read_clip.py +8 -4
  182. package/md_cg/test_retr_s1.py +340 -0
  183. package/md_cg/test_retr_s1b.py +209 -0
  184. package/md_cg/test_retr_s3.py +194 -0
  185. package/md_cg/test_retr_s4.py +163 -0
  186. package/md_cg/test_retr_s5.py +200 -0
  187. package/md_cg/test_retr_s6.py +157 -0
  188. package/md_cg/test_retr_s7.py +385 -0
  189. package/md_cg/test_retr_s8_time.py +316 -0
  190. package/md_cg/test_retr_s9_edges.py +286 -0
  191. package/md_cg/test_retr_s9_entity_ctx.py +175 -0
  192. package/md_cg/test_review_conformance.py +59 -2
  193. package/md_cg/test_role_views.py +354 -0
  194. package/md_cg/test_subproc_encoding.py +188 -0
  195. package/md_cg/test_trust.py +361 -0
  196. package/md_cg/test_units_poll.py +71 -0
  197. package/md_cg/test_v14_fixes.py +397 -0
  198. package/md_cg/test_validity_filter.py +280 -0
  199. package/md_cg/test_wisdom_md_store.py +7 -3
  200. package/md_cg/test_writepipe.py +5 -1
  201. package/md_cg/theory.py +16 -1
  202. package/md_cg/tokens.py +40 -8
  203. package/md_cg/tool_face.py +13 -2
  204. package/md_cg/trust.py +943 -0
  205. package/md_cg/twophase.py +12 -1
  206. package/md_cg/units.py +132 -10
  207. package/md_cg/vision_evidence.py +24 -1
  208. package/md_cg/weights.py +24 -1
  209. package/md_cg/whitebox.py +32 -1
  210. package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
  211. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
  212. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  213. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  214. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  215. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  216. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  217. package/md_cg/writelimit.py +18 -4
  218. package/md_cg/writepipe.py +178 -7
  219. package/package.json +2 -2
  220. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  221. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  222. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  223. package/src/hooks.ts +17 -21
  224. package/src/index.ts +33 -6
  225. package/src/lib/datapath.ts +211 -13
  226. package/src/lib/mdcg_client.ts +64 -25
  227. package/src/lib/mutual.ts +411 -411
  228. package/src/lib/token_store.ts +4 -5
  229. package/zcode/AGENTS.md +196 -195
  230. package/zcode/README.md +4 -0
  231. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  232. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
@@ -0,0 +1,316 @@
1
+ # -*- coding: utf-8 -*-
2
+ """S8 时间算子探针(阶段二 4.1,脚本式,exit code 定成败)。
3
+
4
+ 用法:python -X utf8 -m md_cg.test_retr_s8_time
5
+ 覆盖:
6
+ - 纯函数:轴归一与封闭枚举、按轴取窗、重叠 vs 端点模式、入参 fail-closed
7
+ - 候选层过滤:效力轴 fail-open(缺字段=沉默不是否认)/ 观察轴 fail-closed
8
+ (缺字段=数据异常,须计数可见)
9
+ - meta["time_filter"] 审计块;默认关 meta 键集合不变
10
+ - 绕开热路径缓存(**读+写双侧**):缓存键不含时间参数,读侧失守会返回未过滤
11
+ 结果、写侧失守会把过滤结果覆盖进缓存让后续默认查询少结果,两者都是静默错答
12
+ - stg 四 op 轴感知(observed 缺省逐位兼容 / effective 生效 / 非法轴 fail-closed)
13
+ - recall 透传
14
+
15
+ 语料 15 节点(同一正文 token「阿尔法」,保证都被召回,差异只在时间字段):
16
+ 效力组 7:eff_a[100,200] eff_b[300,400] eff_c[150,350] eff_none(无端点)
17
+ eff_halffrom[500,-] eff_halfuntil[-,50] eff_d[1000,1100]
18
+ 观察组 7:obs_1000/2000/3000/8000/9000(temporal) obs_win_4k_5k/6k_7k(time_window)
19
+ 旧库 1:legacy_noaxis(手工抹掉 temporal 与 time_window → 观察轴不可判定)
20
+ """
21
+ import glob
22
+ import os
23
+ import sys
24
+ import tempfile
25
+
26
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27
+
28
+ from md_cg import hotcache, nodefile, stg, trust # noqa: E402
29
+ from md_cg.mdcg import MdCG # noqa: E402
30
+ from md_cg.mdcos import MdCGSecure # noqa: E402
31
+ from md_cg.security import Principal # noqa: E402
32
+
33
+ passed = 0
34
+ failed = 0
35
+ TEXT = "阿尔法 时间 探针"
36
+ QUERY = "阿尔法"
37
+ #: 效力轴上**无字段**的节点(按效力轴 fail-open 策略保留):eff_none + 观察组 7 + 旧库 1
38
+ _NO_EFF_AXIS = {"eff_none", "legacy_noaxis",
39
+ "obs_1000", "obs_2000", "obs_3000", "obs_t_8000", "obs_t_9000",
40
+ "obs_win_4k_5k", "obs_win_6k_7k"}
41
+
42
+
43
+ def check(name, cond, detail=""):
44
+ global passed, failed
45
+ if cond:
46
+ passed += 1
47
+ print(" [PASS] " + name)
48
+ else:
49
+ failed += 1
50
+ print(" [FAIL] " + name + " " + detail)
51
+
52
+
53
+ def _ids(results):
54
+ return [r[0]["id"] for r in results]
55
+
56
+
57
+ def _node_file(root, nid):
58
+ hits = glob.glob(os.path.join(root, "**", nid + ".md"), recursive=True)
59
+ return hits[0] if hits else None
60
+
61
+
62
+ def _build(root):
63
+ cg = MdCG(root)
64
+ for nid, kw in (
65
+ ("eff_a", {"effective_from": 100, "effective_until": 200}),
66
+ ("eff_b", {"effective_from": 300, "effective_until": 400}),
67
+ ("eff_c", {"effective_from": 150, "effective_until": 350}),
68
+ ("eff_none", {}),
69
+ ("eff_halffrom", {"effective_from": 500}),
70
+ ("eff_halfuntil", {"effective_until": 50}),
71
+ ("eff_d", {"effective_from": 1000, "effective_until": 1100}),
72
+ ("obs_1000", {"temporal": 1000.0}),
73
+ ("obs_2000", {"temporal": 2000.0}),
74
+ ("obs_3000", {"temporal": 3000.0}),
75
+ ("obs_win_4k_5k", {"condition_space": {"time_window": [4000, 5000]}}),
76
+ ("obs_win_6k_7k", {"condition_space": {"time_window": [6000, 7000]}}),
77
+ ("obs_t_8000", {"temporal": 8000.0}),
78
+ ("obs_t_9000", {"temporal": 9000.0}),
79
+ ("legacy_noaxis", {}),
80
+ ):
81
+ cg.add(nid, TEXT, "knowledge", **kw)
82
+ cg.flush()
83
+ # 旧库形态:手工抹掉观察轴两字段(add 会强制补 time_window,只能改文件模拟)
84
+ p = _node_file(root, "legacy_noaxis")
85
+ fm, content = nodefile.loads(open(p, encoding="utf-8").read())
86
+ fm.pop("temporal", None)
87
+ cs = dict(fm.get("condition_space") or {})
88
+ cs.pop("time_window", None)
89
+ fm["condition_space"] = cs
90
+ open(p, "w", encoding="utf-8").write(nodefile.dumps(fm, content))
91
+ cg.rebuild_index()
92
+ return cg
93
+
94
+
95
+ def main():
96
+ # ================= 1) 纯函数层 =================
97
+ check("轴归一:None → effective;大小写归一;believed 抛 ValueError",
98
+ trust.time_axis_of(None) == "effective"
99
+ and trust.time_axis_of("OBSERVED") == "observed"
100
+ and trust.time_axis_of("effective") == "effective"
101
+ and _raises(ValueError, lambda: trust.time_axis_of("believed")),
102
+ str(trust.TIME_AXES))
103
+
104
+ check("重叠模式:有交 True / 无交 False / 候选两端不可解析 False",
105
+ trust.window_match(10, 20, 15, 25) is True
106
+ and trust.window_match(10, 20, 30, 40) is False
107
+ and trust.window_match(None, None, 1, 2) is False)
108
+
109
+ check("端点模式:B1 缺省 gte/lte;单端算子不误杀全空",
110
+ trust.window_match(10, 20, 5, 25, "gte", "lte") is True
111
+ and trust.window_match(10, 20, 15, 25, "gte", "lte") is False
112
+ and trust.window_match(10, 20, 5, None, "gte", None) is True
113
+ and trust.window_match(3, 20, 5, None, "gte", None) is False)
114
+
115
+ check("入参 fail-closed 四则:未启用 / 孤算子 / 逆序 / 非法轴",
116
+ trust.check_time_args()[:2] == (False, None)
117
+ and trust.check_time_args(start_operator="gte")[2] != ""
118
+ and trust.check_time_args(start_time=300, end_time=100)[2] != ""
119
+ and trust.check_time_args(start_time=100, time_axis="believed")[2] != "")
120
+
121
+ check("按轴取窗:观察轴 temporal 优先于 time_window;效力轴别名回落 valid_from",
122
+ trust.time_window_of({"temporal": 7.0,
123
+ "condition_space": {"time_window": [1, 2]}},
124
+ "observed") == (7.0, 7.0)
125
+ and trust.time_window_of({"valid_from": 3, "valid_until": 9},
126
+ "effective") == (3, 9)
127
+ and trust.time_window_of({}, "effective") == (None, None))
128
+
129
+ _kept, _dr, _ms = trust.filter_by_time(
130
+ [{"effective_from": 1, "effective_until": 2}, {}], "effective", 5, 9)
131
+ _kept2, _dr2, _ms2 = trust.filter_by_time([{}], "observed", 5, 9)
132
+ check("轴策略:效力轴缺字段 fail-open 保留;观察轴缺字段 fail-closed 计入 missing",
133
+ _dr == 1 and _ms == 0 and _dr2 == 1 and _ms2 == 1)
134
+
135
+ # ================= 2) 建库 =================
136
+ root = tempfile.mkdtemp(prefix="retr_s8_")
137
+ cg = _build(root)
138
+
139
+ # 默认关(无时间入参)基线
140
+ # 注:热路径接入点是 `MdCGSecure.search_rrf`,`MdCG.search` **无缓存**——
141
+ # 故「cached 标记」不能在此处取证(否则恒真的弱断言),见第 9 节。
142
+ r0, m0 = cg.search(QUERY, k=20, judge=False, record=False)
143
+ base = sorted(_ids(r0))
144
+ r0b, m0b = cg.search(QUERY, k=20, judge=False, record=False)
145
+ check("默认关:meta 无 time_filter、候选 15 条、二次同口径可复算",
146
+ "time_filter" not in m0 and len(base) == 15
147
+ and sorted(_ids(r0b)) == base,
148
+ "n=%d keys=%s" % (len(base), sorted(m0.keys())))
149
+
150
+ # ================= 3) 效力轴(重叠模式)=================
151
+ r1, m1 = cg.search(QUERY, k=20, judge=False, record=False,
152
+ start_time=150, end_time=250, time_axis="effective")
153
+ ids1 = set(_ids(r1))
154
+ tf1 = m1.get("time_filter") or {}
155
+ check("效力轴重叠:窗内相交保留、窗外剔除、无端点 fail-open 保留",
156
+ {"eff_a", "eff_c", "eff_none"} <= ids1
157
+ and not ({"eff_b", "eff_halffrom", "eff_halfuntil", "eff_d"} & ids1)
158
+ and len(ids1) == 11,
159
+ str(sorted(ids1)))
160
+ check("效力轴:审计块 axis/mode/applied 正确、dropped 与存活数记账自洽",
161
+ tf1.get("axis") == "effective" and tf1.get("mode") == "overlap"
162
+ and tf1.get("applied") is True and tf1.get("dropped") == 4
163
+ and tf1.get("dropped") + len(ids1) == 15
164
+ and tf1.get("start") == 150 and tf1.get("end") == 250,
165
+ str(tf1))
166
+
167
+ # ================= 4) 观察轴(窗口语义 + fail-closed)=================
168
+ r2, m2 = cg.search(QUERY, k=20, judge=False, record=False,
169
+ start_time=2500, end_time=3500, time_axis="observed")
170
+ ids2 = _ids(r2)
171
+ tf2 = m2.get("time_filter") or {}
172
+ check("观察轴:temporal 命中唯一、效力组按观测窗不相交剔除",
173
+ ids2 == ["obs_3000"], str(ids2))
174
+ check("观察轴:无字段节点 fail-closed 并计入 axis_missing",
175
+ tf2.get("axis_missing") == 1 and tf2.get("dropped") == 14
176
+ and tf2.get("dropped") + len(ids2) == 15, str(tf2))
177
+
178
+ # ================= 5) 端点模式(单端算子)=================
179
+ r3, m3 = cg.search(QUERY, k=20, judge=False, record=False,
180
+ start_time=150, start_operator="gte",
181
+ time_axis="effective")
182
+ ids3 = set(_ids(r3))
183
+ tf3 = m3.get("time_filter") or {}
184
+ # 只给起点算子 → 只筛起点侧:eff_a(起点 100<150)与 eff_halfuntil(起点缺失、
185
+ # 端点模式下该侧不可比较)被剔除;eff_none 与全部观察组/旧库节点在效力轴**无字段**
186
+ # → fail-open 保留(沉默不是否认)。此处非空集本身即旧缺陷(单端算子恒空)的回归证据。
187
+ check("端点模式:单端 gte 只筛起点侧且不返回空集、无该轴字段者 fail-open 保留",
188
+ ids3 == {"eff_b", "eff_c", "eff_d", "eff_halffrom"} | _NO_EFF_AXIS
189
+ and tf3.get("mode") == "endpoint"
190
+ and tf3.get("start_operator") == "gte"
191
+ and tf3.get("dropped") == 2, str(sorted(ids3)))
192
+
193
+ # ================= 6) 误用 fail-closed =================
194
+ check("误用三则经库层抛 ValueError(孤算子 / 逆序 / 非法轴)",
195
+ _raises(ValueError, lambda: cg.search(
196
+ QUERY, k=5, judge=False, record=False, start_operator="gte"))
197
+ and _raises(ValueError, lambda: cg.search(
198
+ QUERY, k=5, judge=False, record=False, start_time=300, end_time=100))
199
+ and _raises(ValueError, lambda: cg.search(
200
+ QUERY, k=5, judge=False, record=False, start_time=100,
201
+ time_axis="believed")))
202
+
203
+ # ================= 7) stg 四 op 轴感知 =================
204
+ tl_obs = stg.timeline(cg, limit=100, time_axis="observed")
205
+ tl_eff = stg.timeline(cg, limit=100, time_axis="effective")
206
+ check("stg timeline:观察轴 14 条(旧库节点不可判定被跳过)、效力轴 4 条(两端齐备)",
207
+ tl_obs["count"] == 14 and tl_eff["count"] == 4,
208
+ "obs=%s eff=%s" % (tl_obs["count"], tl_eff["count"]))
209
+ check("stg timeline:缺省轴与显式 observed 逐位一致(旧行为兼容)",
210
+ stg.timeline(cg, limit=100)["count"] == tl_obs["count"])
211
+
212
+ # 返回结构取证:relation 的时间结果在 ["time"]["relation"](非顶层 time_relation)
213
+ rel_eff = stg.relation(cg, "eff_a", "eff_b", time_axis="effective")
214
+ rel_obs = stg.relation(cg, "eff_a", "eff_b", time_axis="observed")
215
+ check("stg relation:效力轴取 effective 区间(eff_a before eff_b);观察轴取写入观测时刻(区间不同)",
216
+ rel_eff["time"]["relation"] == "before"
217
+ and rel_eff["time"]["a"] == (100.0, 200.0)
218
+ and rel_obs["time"]["a"] != (100.0, 200.0),
219
+ "%s / %s" % (rel_eff["time"], rel_obs["time"]))
220
+
221
+ an_eff = stg.anchors(cg, time_window=[150, 250], limit=50,
222
+ time_axis="effective")
223
+ an_obs = stg.anchors(cg, time_window=[150, 250], limit=50,
224
+ time_axis="observed")
225
+ check("stg anchors:效力轴命中 2(eff_a overlaps + eff_c contains)、观察轴 0",
226
+ an_eff["count"] == 2 and an_obs["count"] == 0,
227
+ "eff=%s obs=%s" % (an_eff["count"], an_obs["count"]))
228
+
229
+ check("stg 非法轴 fail-closed(四 op 同源 trust.time_axis_of)",
230
+ _raises(ValueError, lambda: stg.consistency(cg, time_axis="believed"))
231
+ and _raises(ValueError, lambda: stg.relation(
232
+ cg, "eff_a", "eff_b", time_axis="believed")))
233
+
234
+ # ================= 8) 关→开→关 默认口径不变 =================
235
+ # 置于 Secure 层操作**之前**:那些调用会记录访问(recall 无法关 record),
236
+ # 可能与基线产生与时间算子无关的差异,污染「首末一致」这一断言。
237
+ r5, m5 = cg.search(QUERY, k=20, judge=False, record=False)
238
+ check("默认口径等价:关→开→关 首末一致且无 time_filter 键",
239
+ sorted(_ids(r5)) == base and "time_filter" not in m5)
240
+
241
+ # ================= 9) 热缓存:命中 + 时间算子读/写双侧绕行 =================
242
+ sec = MdCGSecure(root, principal=Principal(
243
+ actor="test-s8", clearance="secret", can_write=True,
244
+ role="designer", auth_mode="local-cli"))
245
+ hotcache.attach(sec)
246
+ _sa, sma = sec.search_rrf(QUERY, k=20, judge=False, record=False)
247
+ _sb, smb = sec.search_rrf(QUERY, k=20, judge=False, record=False)
248
+ check("热缓存:同参二次 search_rrf 命中(cached=True)且结果一致",
249
+ sma.get("cached") is not True and smb.get("cached") is True
250
+ and [r[0]["id"] for r in _sa] == [r[0]["id"] for r in _sb],
251
+ "a=%s b=%s" % (sma.get("cached"), smb.get("cached")))
252
+
253
+ # 上一步已把 15 条全量结果写进缓存键(键不含时间参数)。带时间算子的查询
254
+ # 若只绕开「读」而漏掉「写」,则返回的 11 条会把缓存覆盖成子集 —— 故下方
255
+ # 「随后默认查询仍为 15 条」是**写侧绕行**的判别性证据。
256
+ _t1, mt1 = sec.search_rrf(QUERY, k=20, judge=False, record=False,
257
+ start_time=150, end_time=250,
258
+ time_axis="effective")
259
+ check("时间算子启用时绕开热缓存:缓存已有 15 条全量,仍返回过滤后 11 条、meta 无 cached",
260
+ "cached" not in mt1 and len(_t1) == 11
261
+ and sorted(r[0]["id"] for r in _t1) == sorted(ids1),
262
+ "n=%d cached=%s" % (len(_t1), mt1.get("cached")))
263
+ _d2, md2 = sec.search_rrf(QUERY, k=20, judge=False, record=False)
264
+ check("时间算子查询不污染缓存(写侧同受绕行约束):其后默认查询仍为全量 15 条",
265
+ len(_d2) == 15 and md2.get("cached") is True,
266
+ "n=%d cached=%s" % (len(_d2), md2.get("cached")))
267
+
268
+ # ================= 10) recall 透传 =================
269
+ out = sec.recall(QUERY, k=20, judge=False, start_time=2500,
270
+ end_time=3500, time_axis="observed")
271
+ mr = out.get("meta") or {}
272
+ ids_r = [e["id"] for e in (out.get("pack") or [])]
273
+ check("recall 透传:meta 带 time_filter(观察轴)且过滤真实生效",
274
+ (mr.get("time_filter") or {}).get("axis") == "observed"
275
+ and ids_r == ["obs_3000"], str(ids_r))
276
+
277
+ # ================= 11) Secure.search 常规路径审计块(MCP cg(op=read) 实走此路)=================
278
+ # 判别性:本节的观测量=「过滤生效 ∧ meta 带审计块」**同时**成立,缺一即 FAIL。
279
+ # 缺陷史(2026-09-20 取证):候选层过滤已在 `_candidates` 完成(结果正确),但
280
+ # `MdCGSecure.search` 的**常规返回路径**未把 `_time_filter_stat` 交给 `_emit` ——
281
+ # 审计块此前只在 no_candidates 分支落键 → 「过滤确实生效而审计不可见」(静默,
282
+ # 调用方无法区分「未启用」与「启用但候选全 fail-open 保留」)。
283
+ # 只用 `cg.search`(父类,本无此缺口)或 `search_rrf`(另行落键)**均测不到**该缺口,
284
+ # 故单列一节:本节的红/绿随该唯一缺口翻转,是它的判别性守卫。
285
+ _s11, m11 = sec.search(QUERY, k=20, judge=False, record=False,
286
+ start_time=150, end_time=250, time_axis="effective")
287
+ tf11 = m11.get("time_filter") or {}
288
+ check("Secure.search 常规路径:过滤生效且 meta 带 time_filter(审计块不被吞)",
289
+ sorted(r[0]["id"] for r in _s11) == sorted(ids1)
290
+ and tf11.get("axis") == "effective" and tf11.get("mode") == "overlap"
291
+ and tf11.get("applied") is True and tf11.get("dropped") == 4,
292
+ "n=%d tf=%s" % (len(_s11), tf11))
293
+
294
+ _s11b, m11b = sec.search(QUERY, k=20, judge=False, record=False)
295
+ check("Secure.search 默认关:候选不变且 meta 键集合不含 time_filter(默认关零变更)",
296
+ "time_filter" not in m11b and len(_s11b) == 15,
297
+ "n=%d keys=%s" % (len(_s11b), sorted(m11b.keys())))
298
+
299
+ print("\ntest_retr_s8: %d 通过 / %d 失败" % (passed, failed))
300
+ return 0 if failed == 0 else 1
301
+
302
+
303
+ def _raises(exc, fn):
304
+ try:
305
+ fn()
306
+ except exc:
307
+ return True
308
+ except Exception:
309
+ return False
310
+ return False
311
+
312
+
313
+ if __name__ == "__main__":
314
+ if hasattr(sys.stdout, "reconfigure"):
315
+ sys.stdout.reconfigure(encoding="utf-8")
316
+ sys.exit(main())
@@ -0,0 +1,286 @@
1
+ # -*- coding: utf-8 -*-
2
+ """S9 三元组反查原语探针(阶段二 4.2,脚本式,exit code 定成败)。
3
+
4
+ 用法:python -X utf8 -m md_cg.test_retr_s9_edges
5
+ 覆盖:
6
+ - 纯函数:ordering/aggregation 封闭枚举、按轴取边窗(边只有观察轴 `t`)、
7
+ 模式分叉、端点摘要(索引级零读盘)
8
+ - 谓词反查:child / parent / relation / batch 与 `edges()` 同源同义
9
+ - 时间条件:缺省轴 = observed(**与 search 缺省 effective 有意不同**)、
10
+ observed fail-closed(缺 `t` 剔除并计入 axis_missing)、
11
+ effective 在边上 fail-open(如实 no-op,不是静默错答)
12
+ - 排序 / 分页 / 聚合:聚合基于**分页前全集**(分页不漂移)
13
+ - fail-closed 清单:非法枚举 / offset<0 / limit<=0 / limit>上限 /
14
+ 非法 relation / 孤算子 / 逆序窗 / 非法轴
15
+ - 零回归:`provenance.edges()` 逐位不变;反查**不写盘**(台账字节不变)
16
+
17
+ 语料(6 条固定边 + 1 条索引兜底边):
18
+ 写路径 3:a1→p1(derived_from) a2→p1(extracted_from) a3→p2(merged_from)
19
+ 显式 t 3:e1→p1 t=1000 batch=b1 / e2→p1 t=2000 batch=b1 / e3→p2 t=3000 batch=b2 refined_from
20
+ 索引兜底 1:ghost→p2(台账行被摘除 → 只能从 frontmatter 恢复,**无 t**)
21
+ """
22
+ import json
23
+ import os
24
+ import sys
25
+ import tempfile
26
+
27
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
28
+
29
+ from md_cg import fsutil, provenance # noqa: E402
30
+ from md_cg.mdcg import MdCG # noqa: E402
31
+
32
+ passed = 0
33
+ failed = 0
34
+ T = "阿尔法 三元组 反查 探针"
35
+ #: 写路径 3 条边的真实写入时刻(time.time())远大于显式 t(1000~3000)
36
+ _NOW = ("a1", "a2", "a3")
37
+
38
+
39
+ def check(name, cond, detail=""):
40
+ global passed, failed
41
+ if cond:
42
+ passed += 1
43
+ print(" [PASS] " + name)
44
+ else:
45
+ failed += 1
46
+ print(" [FAIL] " + name + " " + detail)
47
+
48
+
49
+ def _raises(exc, fn):
50
+ try:
51
+ fn()
52
+ except exc:
53
+ return True
54
+ except Exception: # noqa: BLE001
55
+ return False
56
+ return False
57
+
58
+
59
+ def _ids(res):
60
+ return [e.get("child") for e in res["edges"]]
61
+
62
+
63
+ def _build(root):
64
+ cg = MdCG(root)
65
+ for nid in ("p1", "p2"):
66
+ cg.add(nid, T, "knowledge")
67
+ cg.add("a1", T, "knowledge", derived_from=["p1"])
68
+ cg.add("a2", T, "knowledge", derived_from=["p1"], relation="extracted_from")
69
+ cg.add("a3", T, "knowledge", derived_from=["p2"], relation="merged_from")
70
+ cg.flush()
71
+ # 显式 t 的 3 条边(record() 不接 t,直接 make_edge + append)
72
+ provenance.append(root, [
73
+ provenance.make_edge("e1", "p1", relation="derived_from",
74
+ batch="b1", actor="t-actor", t=1000.0),
75
+ provenance.make_edge("e2", "p1", relation="derived_from",
76
+ batch="b1", actor="t-actor", t=2000.0),
77
+ provenance.make_edge("e3", "p2", relation="refined_from",
78
+ batch="b2", actor="t-actor", t=3000.0),
79
+ ])
80
+ return cg
81
+
82
+
83
+ def _strip_ledger(root, child):
84
+ """摘掉某个 child 的台账行 → 该边只能由 frontmatter(索引)恢复。"""
85
+ p = provenance.ledger_file(root)
86
+ rows = [r for r in fsutil.read_jsonl(p) if r.get("child") != child]
87
+ fsutil.atomic_write(p, "".join(
88
+ json.dumps(r, ensure_ascii=False, separators=(",", ":")) + "\n"
89
+ for r in rows))
90
+
91
+
92
+ def main():
93
+ # ================= 1) 纯函数层 =================
94
+ check("ordering 归一:None/空白 → desc;大小写归一;未知抛 ProvenanceError",
95
+ provenance._ordering_of(None) == "desc"
96
+ and provenance._ordering_of(" ") == "desc"
97
+ and provenance._ordering_of("ASC") == "asc"
98
+ and _raises(provenance.ProvenanceError,
99
+ lambda: provenance._ordering_of("up")),
100
+ str(provenance.ORDERINGS))
101
+
102
+ check("aggregation 归一:None/空白 → None(不聚合);大小写归一;未知抛错",
103
+ provenance._aggregation_of(None) is None
104
+ and provenance._aggregation_of("BY_PARENT") == "by_parent"
105
+ and _raises(provenance.ProvenanceError,
106
+ lambda: provenance._aggregation_of("by_actor")),
107
+ str(provenance.AGGREGATIONS))
108
+
109
+ check("边取窗:observed 用 t(单点窗);缺 t 不可判定;effective 一律不可判定",
110
+ provenance._edge_window({"t": 100}, "observed") == (100.0, 100.0, False)
111
+ and provenance._edge_window({}, "observed") == (None, None, True)
112
+ and provenance._edge_window({"t": 100}, "effective") == (None, None, True))
113
+
114
+ check("模式分叉:无算子=overlap;任一端算子=endpoint",
115
+ provenance._mode_of(None, None) == "overlap"
116
+ and provenance._mode_of("gte", None) == "endpoint"
117
+ and provenance._mode_of(None, "lte") == "endpoint")
118
+
119
+ # ================= 2) 建库 + 基线 =================
120
+ root = tempfile.mkdtemp(prefix="retr_s9_")
121
+ cg = _build(root)
122
+ ledger_bytes = open(provenance.ledger_file(root), "rb").read()
123
+
124
+ d = provenance._node_digest(cg, "p1")
125
+ check("端点摘要:在索引内 present=True 且带 layer;缺失 present=False(不猜)",
126
+ d.get("present") is True and d.get("layer") == "knowledge"
127
+ and provenance._node_digest(cg, "no_such")["present"] is False,
128
+ str(d))
129
+
130
+ r0 = provenance.find_edges(cg)
131
+ check("基线:无过滤 → total=6 / matched=6 / returned=6、只读、默认 desc + limit 50",
132
+ r0["ok"] is True and r0["readonly"] is True and r0["op"] == "edges"
133
+ and r0["total"] == 6 and r0["matched"] == 6 and r0["returned"] == 6
134
+ and r0["ordering"] == "desc" and r0["limit"] == 50 and r0["offset"] == 0,
135
+ str({k: r0[k] for k in ("total", "matched", "returned", "limit")}))
136
+ check("基线:默认排序 desc → 写路径边(真实 t)在前、显式 t 边倒序在后",
137
+ set(_ids(r0)[:3]) == set(_NOW)
138
+ and _ids(r0)[3:] == ["e3", "e2", "e1"], str(_ids(r0)))
139
+ check("基线:未启用时间条件 → 审计块 applied=False 且不虚报 dropped",
140
+ (r0["time_filter"] or {}).get("applied") is False
141
+ and (r0["time_filter"] or {}).get("dropped") == 0
142
+ and r0["aggregates"] is None and r0["nodes_expanded"] is False,
143
+ str(r0["time_filter"]))
144
+
145
+ # ================= 3) 谓词反查(与 edges() 同源同义)=================
146
+ check("按任意端 / 谓词 / 批次反查:child / parent / relation / batch 各自等值过滤",
147
+ provenance.find_edges(cg, child="a1")["total"] == 1
148
+ and provenance.find_edges(cg, parent="p1")["total"] == 4
149
+ and provenance.find_edges(cg, relation="derived_from")["total"] == 3
150
+ and provenance.find_edges(cg, batch="b1")["total"] == 2
151
+ and provenance.find_edges(cg, parent="p1",
152
+ relation="derived_from")["total"] == 3,
153
+ "%s/%s" % (provenance.find_edges(cg, parent="p1")["total"],
154
+ provenance.find_edges(cg, relation="derived_from")["total"]))
155
+ check("复合谓词:child+parent+relation 三键同时约束(交集语义)",
156
+ provenance.find_edges(cg, child="e1", parent="p1",
157
+ relation="derived_from")["total"] == 1
158
+ and provenance.find_edges(cg, parent="p1",
159
+ relation="merged_from")["total"] == 0)
160
+
161
+ # ================= 4) 时间条件(缺省轴 observed)=================
162
+ rt = provenance.find_edges(cg, start_time=2500, end_time=3500)
163
+ check("缺省轴=observed(**不是** search 的 effective):窗内只剩 e3、窗外全部剔除",
164
+ _ids(rt) == ["e3"] and rt["matched"] == 1 and rt["total"] == 6,
165
+ str(_ids(rt)))
166
+ check("时间审计块:axis/mode/applied 正确,dropped + matched == total(可复算)",
167
+ rt["time_filter"]["axis"] == "observed"
168
+ and rt["time_filter"]["mode"] == "overlap"
169
+ and rt["time_filter"]["applied"] is True
170
+ and rt["time_filter"]["dropped"] == 5
171
+ and rt["time_filter"]["dropped"] + rt["matched"] == rt["total"],
172
+ str(rt["time_filter"]))
173
+
174
+ re_ = provenance.find_edges(cg, start_time=2000, start_operator="gte")
175
+ check("端点模式:单端 gte 只筛起点侧(e1 剔除、e2/e3 与写路径边保留)",
176
+ "e1" not in _ids(re_) and "e2" in _ids(re_) and "e3" in _ids(re_)
177
+ and re_["matched"] == 5 and re_["time_filter"]["mode"] == "endpoint"
178
+ and re_["time_filter"]["dropped"] == 1
179
+ and re_["time_filter"]["start_operator"] == "gte",
180
+ str(_ids(re_)))
181
+
182
+ ra = provenance.find_edges(cg, time_axis="effective",
183
+ start_time=2500, end_time=3500)
184
+ check("边在效力轴无字段 → fail-open 如实 no-op(dropped=0 且 applied=True,不冒充过滤成功)",
185
+ ra["matched"] == 6 and ra["time_filter"]["axis"] == "effective"
186
+ and ra["time_filter"]["dropped"] == 0
187
+ and ra["time_filter"]["applied"] is True,
188
+ str(ra["time_filter"]))
189
+
190
+ # ================= 5) 排序 / 分页 / 聚合 =================
191
+ rasc = provenance.find_edges(cg, ordering="asc")
192
+ check("排序 asc:显式 t 边正序在前(t 全序),与 desc 严格互反",
193
+ _ids(rasc)[:3] == ["e1", "e2", "e3"]
194
+ and _ids(rasc) == list(reversed(_ids(r0))), str(_ids(rasc)))
195
+
196
+ rpg = provenance.find_edges(cg, limit=2, offset=1)
197
+ check("分页:limit/offset 作用于排序后序列(offset=1 limit=2 → 取第 2、3 位)",
198
+ rpg["returned"] == 2 and rpg["matched"] == 6
199
+ and _ids(rpg) == _ids(r0)[1:3], str(_ids(rpg)))
200
+
201
+ rag = provenance.find_edges(cg, aggregation="by_parent", limit=1)
202
+ check("聚合 by_parent:基于**分页前全集**分桶(p1=4 / p2=2,limit 不缩水计数)",
203
+ rag["returned"] == 1 and rag["aggregation"] == "by_parent"
204
+ and [(b["key"], b["count"]) for b in rag["aggregates"]]
205
+ == [("p1", 4), ("p2", 2)],
206
+ str(rag["aggregates"]))
207
+ check("聚合 by_relation / by_child:谓词分布与逐 child 计数正确",
208
+ [(b["key"], b["count"]) for b in
209
+ provenance.find_edges(cg, aggregation="by_relation")["aggregates"]]
210
+ == [("derived_from", 3), ("extracted_from", 1), ("merged_from", 1),
211
+ ("refined_from", 1)]
212
+ and all(b["count"] == 1 for b in provenance.find_edges(
213
+ cg, aggregation="by_child")["aggregates"]))
214
+
215
+ rex = provenance.find_edges(cg, expand_nodes=True, child="e1")
216
+ check("expand:端点摘要 present 与索引一致(p1 在库 / e1 只是台账 child 不在库);边为副本不污染源",
217
+ len(rex["edges"]) == 1
218
+ and rex["edges"][0]["parent_node"]["present"] is True
219
+ and rex["edges"][0]["child_node"]["present"] is False
220
+ and rex["nodes_expanded"] is True
221
+ and "child_node" not in provenance.all_edges(cg)[0],
222
+ str(rex["edges"][0].get("child_node")))
223
+
224
+ # ================= 6) 索引兜底边(无 t)与 observed 策略联动 =================
225
+ cg.add("ghost", T, "knowledge", derived_from=["p2"])
226
+ _strip_ledger(root, "ghost")
227
+ rg = provenance.find_edges(cg)
228
+ ghost = [e for e in rg["edges"] if e.get("child") == "ghost"]
229
+ check("台账缺行的边由索引恢复(origin=index、无 t 字段)——反查覆盖台账 ∪ 索引",
230
+ len(ghost) == 1 and ghost[0].get("origin") == "index"
231
+ and "t" not in ghost[0] and rg["total"] == 7, str(ghost))
232
+
233
+ rgt = provenance.find_edges(cg, start_time=2500, end_time=3500)
234
+ check("观察轴 fail-closed:无 t 的索引兜底边不可判定 → 剔除并计入 axis_missing",
235
+ rgt["matched"] == 1 and rgt["time_filter"]["axis_missing"] == 1
236
+ and rgt["time_filter"]["dropped"] == 6
237
+ and rgt["time_filter"]["dropped"] + rgt["matched"] == rgt["total"] == 7,
238
+ str(rgt["time_filter"]))
239
+
240
+ # ================= 7) fail-closed 清单 =================
241
+ P = provenance.ProvenanceError
242
+ check("入参 fail-closed:非法 ordering / aggregation / relation",
243
+ _raises(P, lambda: provenance.find_edges(cg, ordering="up"))
244
+ and _raises(P, lambda: provenance.find_edges(cg, aggregation="by_actor"))
245
+ and _raises(P, lambda: provenance.find_edges(cg, relation="bogus")))
246
+ check("分页 fail-closed:offset<0 / limit<=0 / limit>上限(0 不当「全部」)",
247
+ _raises(P, lambda: provenance.find_edges(cg, offset=-1))
248
+ and _raises(P, lambda: provenance.find_edges(cg, limit=0))
249
+ and _raises(P, lambda: provenance.find_edges(cg, limit=501)),
250
+ "MAX=%s" % provenance.MAX_FIND_LIMIT)
251
+ check("时间误用 fail-closed(复用 trust.check_time_args 唯一校验点):孤算子 / 逆序 / 非法轴",
252
+ _raises(P, lambda: provenance.find_edges(cg, start_operator="gte"))
253
+ and _raises(P, lambda: provenance.find_edges(cg, start_time=3000,
254
+ end_time=1000))
255
+ and _raises(P, lambda: provenance.find_edges(cg, time_axis="believed"))
256
+ and _raises(P, lambda: provenance.find_edges(cg, start_time=1000,
257
+ time_axis="believed")))
258
+
259
+ # ================= 8) 零回归 + 只读性 =================
260
+ legacy = provenance.edges(root)
261
+ check("零回归:provenance.edges()(台账口径)逐位不变——写入顺序、6 条,新原语未改动它",
262
+ len(legacy) == 6
263
+ and [e.get("child") for e in legacy]
264
+ == ["a1", "a2", "a3", "e1", "e2", "e3"],
265
+ str([e.get("child") for e in legacy]))
266
+ check("口径分层:edges()=台账only(6) vs all_edges()=台账∪索引(7),差异只在索引兜底边",
267
+ len(provenance.all_edges(cg)) == 7
268
+ and {e.get("child") for e in provenance.all_edges(cg)}
269
+ - {e.get("child") for e in legacy} == {"ghost"})
270
+ # 台账在 §6 被有意摘行,故此处**重新快照**再比较(对比 §2 快照是错的)
271
+ snap = open(provenance.ledger_file(root), "rb").read()
272
+ provenance.find_edges(cg, expand_nodes=True, aggregation="by_relation")
273
+ provenance.find_edges(cg, start_time=1000, end_time=9000)
274
+ provenance.find_edges(cg, limit=3, offset=2, ordering="asc")
275
+ check("只读性:全部反查调用后台账字节不变(含 expand / 聚合 / 时间路径)",
276
+ open(provenance.ledger_file(root), "rb").read() == snap and len(snap) > 0,
277
+ "ledger=%s" % provenance.ledger_file(root))
278
+
279
+ print("\ntest_retr_s9: %d 通过 / %d 失败" % (passed, failed))
280
+ return 0 if failed == 0 else 1
281
+
282
+
283
+ if __name__ == "__main__":
284
+ if hasattr(sys.stdout, "reconfigure"):
285
+ sys.stdout.reconfigure(encoding="utf-8")
286
+ sys.exit(main())