@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24

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 (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -1,10 +1,26 @@
1
+ /**
2
+ * 账本回归(ADR-15 执行锚点,run-all §28):
3
+ * 1 事务原子性:事务内注入异常 → 整体回滚,账本无痕
4
+ * 2 红线进事务:conditions 缺失拒绝且零事件落账
5
+ * 3 幂等物理化:愿望同名/效用语义键/行程 trip_id 重放 no-op;wish_id 语义派生稳定
6
+ * 4 守门随账本:权重变更守门/词表外日期拒收/重叠冲突即停/负面清单拒收
7
+ * 5 confirm-outcome 单事务:效用+行程同生(跨文件分叉不可能)
8
+ * 6 fold 重建:DROP 投影 → 重放 → 与直读一致(账本/投影永不分叉)
9
+ * 7 rewind:fold 截到历史 seq → 投影回到历史时点;无参 rebuild 回最新
10
+ * 8 one-shot 迁移:旧 JSON/JSONL → events;快照先行;重复 ensure 不重复导入
11
+ * 9 durable 工单崩溃恢复:子进程 settle 前 exit 9 → 恢复零重算(exactly-once)
12
+ * 10 pending_writes saga:幂等键去重/L3 确认一次/补偿/审计事件链/what-if 分叉隔离
13
+ * 11 ADR-16 tenant scope:append/read/fold/rebuild/迁移边界/同 id/跨进程 reopen 全隔离;
14
+ * 精确 fold 反例、kind+limit 截断、workflow/forget 行归属都必须可负向验红
15
+ * 运行(在 ts/ 下):npx tsx scripts/ledger-tests.ts
16
+ */
1
17
  import Database from 'better-sqlite3';
2
18
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
3
19
  import { tmpdir } from 'node:os';
4
20
  import { join } from 'node:path';
5
21
  import { spawnSync } from 'node:child_process';
6
22
  import { persistAsyncTicket } from '../src/loop.js';
7
- import { ensureLedger, openLedgerIfExists } from '../src/state-ledger.js';
23
+ import { ensureLedger, ledgerDbPath, ledgerExists, openLedgerIfExists, readWishPoolWithFallback } from '../src/state-ledger.js';
8
24
  import { parseFlightPackToSpec } from '../src/unified.js';
9
25
  let pass = 0;
10
26
  let fail = 0;
@@ -12,168 +28,84 @@ function assert(cond, msg) {
12
28
  if (cond) {
13
29
  pass++;
14
30
  console.log(` ok - ${msg}`);
15
- } else {
31
+ }
32
+ else {
16
33
  fail++;
17
34
  console.error(` FAIL - ${msg}`);
18
35
  }
19
36
  }
20
37
  function terminalOutcomeOf(stdout) {
21
38
  const line = stdout.trim().split('\n').filter(Boolean).at(-1);
22
- if (!line) return null;
39
+ if (!line)
40
+ return null;
23
41
  try {
24
42
  const value = JSON.parse(line);
25
- if (value.schema !== 'gotry_async_terminal.v1' || typeof value.ticket_id !== 'string' || value.status !== 'succeeded' && value.status !== 'failed' || typeof value.passed !== 'number' || typeof value.total !== 'number' || typeof value.checks !== 'object' || !Array.isArray(value.failed_checks)) return null;
43
+ if (value.schema !== 'gotry_async_terminal.v1'
44
+ || typeof value.ticket_id !== 'string'
45
+ || (value.status !== 'succeeded' && value.status !== 'failed')
46
+ || typeof value.passed !== 'number'
47
+ || typeof value.total !== 'number'
48
+ || typeof value.checks !== 'object'
49
+ || !Array.isArray(value.failed_checks))
50
+ return null;
26
51
  return value;
27
- } catch {
52
+ }
53
+ catch {
28
54
  return null;
29
55
  }
30
56
  }
57
+ // ---- 1-7:fresh root 基座面 ------------------------------------------------------
31
58
  const root = mkdtempSync(join(tmpdir(), 'gotry-ledger-'));
32
59
  const ledger = ensureLedger(root);
33
60
  const r1 = ledger.appendMotivationPatch({
34
- weights: {
35
- escape_rest: 0.7
36
- },
37
- evidence: [
38
- '用户原话:想去湖边什么都不干'
39
- ],
40
- hard: {
41
- wake_not_before: '06:30'
42
- }
61
+ weights: { escape_rest: 0.7 }, evidence: ['用户原话:想去湖边什么都不干'], hard: { wake_not_before: '06:30' },
43
62
  });
44
63
  assert(r1.saved === true, '画像补丁落账(saved)');
45
- const r2 = ledger.appendMotivationPatch({
46
- evidence: [
47
- '用户原话:想去湖边什么都不干'
48
- ]
49
- });
64
+ const r2 = ledger.appendMotivationPatch({ evidence: ['用户原话:想去湖边什么都不干'] });
50
65
  assert(r2.saved === false, '同 evidence 重放幂等(mergeProfile 守门 → 零事件)');
51
66
  const before = ledger.countEvents();
52
67
  let threw = false;
53
68
  try {
54
- ledger.db.transaction(()=>{
55
- ledger.insertEvent({
56
- actor: 'test',
57
- kind: 'probe',
58
- payload: {
59
- x: 1
60
- }
61
- });
69
+ ledger.db.transaction(() => {
70
+ ledger.insertEvent({ actor: 'test', kind: 'probe', payload: { x: 1 } });
62
71
  throw new Error('boom');
63
72
  })();
64
- } catch {
73
+ }
74
+ catch {
65
75
  threw = true;
66
76
  }
67
77
  assert(threw && ledger.countEvents() === before, '事务内异常 → 整体回滚,账本无痕(崩溃一致性的单事务证明)');
68
78
  threw = false;
69
79
  try {
70
- ledger.appendWish({
71
- name: '无条件憧憬',
72
- reason: '',
73
- conditions: undefined
74
- });
75
- } catch {
80
+ ledger.appendWish({ name: '无条件憧憬', reason: '', conditions: undefined });
81
+ }
82
+ catch {
76
83
  threw = true;
77
84
  }
78
85
  assert(threw && ledger.countEvents() === before, 'conditions 红线在事务内拒绝且零事件(红线进 schema 层)');
79
- const w1 = ledger.appendWish({
80
- name: '大理·洱海',
81
- reason: '',
82
- conditions: {
83
- days: 5,
84
- budget_cny: 4950,
85
- best_months: [
86
- 3,
87
- 4
88
- ]
89
- }
90
- });
91
- const w2 = ledger.appendWish({
92
- name: '大理·洱海',
93
- reason: '两周年想再去',
94
- conditions: {
95
- days: 6
96
- }
97
- });
86
+ const w1 = ledger.appendWish({ name: '大理·洱海', reason: '', conditions: { days: 5, budget_cny: 4950, best_months: [3, 4] } });
87
+ const w2 = ledger.appendWish({ name: '大理·洱海', reason: '两周年想再去', conditions: { days: 6 } });
98
88
  assert(w1.added === true && w2.added === false && w1.wish_id === w2.wish_id, '同名愿望幂等更新,wish_id 语义派生稳定');
99
89
  assert(ledger.readWishPool().length === 1, '愿望池投影恰 1 条');
100
- const u1 = ledger.appendUtilityEvent({
101
- wish_id: w1.wish_id,
102
- kind: 'recalled',
103
- ts: '2026-08-28T00:00:00Z',
104
- ctx: 'test'
105
- });
106
- const u2 = ledger.appendUtilityEvent({
107
- wish_id: w1.wish_id,
108
- kind: 'recalled',
109
- ts: '2026-08-28T00:00:00Z',
110
- ctx: 'test'
111
- });
90
+ const u1 = ledger.appendUtilityEvent({ wish_id: w1.wish_id, kind: 'recalled', ts: '2026-08-28T00:00:00Z', ctx: 'test' });
91
+ const u2 = ledger.appendUtilityEvent({ wish_id: w1.wish_id, kind: 'recalled', ts: '2026-08-28T00:00:00Z', ctx: 'test' });
112
92
  assert(u1.appended === true && u2.appended === false, '效用事件语义键幂等(重放 no-op)');
113
- ledger.appendUtilityEvent({
114
- wish_id: w1.wish_id,
115
- kind: 'verified_outcome',
116
- ts: '2026-08-28T00:00:00Z',
117
- ctx: 'test-noattr'
118
- });
93
+ ledger.appendUtilityEvent({ wish_id: w1.wish_id, kind: 'verified_outcome', ts: '2026-08-28T00:00:00Z', ctx: 'test-noattr' });
119
94
  assert(ledger.readUtilityEvents().at(-1)?.kind === 'applied', '无归因 verified 降级 applied(六件语义纪律随账本)');
120
- const t1 = ledger.appendTripEvent({
121
- destination: '大理',
122
- start: '2026-10-01',
123
- source: 'user-verbatim',
124
- evidence: '用户原话:国庆去了大理'
125
- });
126
- const t1dup = ledger.appendTripEvent({
127
- destination: '大理',
128
- start: '2026-10-01',
129
- source: 'user-verbatim',
130
- evidence: '用户原话:国庆去了大理'
131
- });
95
+ const t1 = ledger.appendTripEvent({ destination: '大理', start: '2026-10-01', source: 'user-verbatim', evidence: '用户原话:国庆去了大理' });
96
+ const t1dup = ledger.appendTripEvent({ destination: '大理', start: '2026-10-01', source: 'user-verbatim', evidence: '用户原话:国庆去了大理' });
132
97
  assert(t1.appended === true && t1dup.appended === false, '行程 trip_id 幂等');
133
- const tOverlap = ledger.appendTripEvent({
134
- destination: '大理',
135
- start: '2026-10-03',
136
- source: 'user-verbatim',
137
- evidence: '重叠探针'
138
- });
98
+ const tOverlap = ledger.appendTripEvent({ destination: '大理', start: '2026-10-03', source: 'user-verbatim', evidence: '重叠探针' });
139
99
  assert(tOverlap.appended === false && /重叠/.test(tOverlap.reason ?? ''), '同目的地日期重叠冲突即停(由人裁决)');
140
- const tBad = ledger.appendTripEvent({
141
- destination: 'X地',
142
- start: '十月一',
143
- source: 'user-verbatim',
144
- evidence: 'x'
145
- });
100
+ const tBad = ledger.appendTripEvent({ destination: 'X地', start: '十月一', source: 'user-verbatim', evidence: 'x' });
146
101
  assert(tBad.appended === false, '词表外日期拒收(不猜)');
147
- const cBad = ledger.appendCompanion({
148
- label: '爸爸',
149
- constraints: {
150
- health: [
151
- '手机号13800001111'
152
- ]
153
- },
154
- evidence: '探针'
155
- });
102
+ const cBad = ledger.appendCompanion({ label: '爸爸', constraints: { health: ['手机号13800001111'] }, evidence: '探针' });
156
103
  assert(cBad.appended === false && /负面清单/.test(cBad.reason ?? ''), '同行人负面清单拒收(证件/手机号不入库)');
157
- const cOk = ledger.appendCompanion({
158
- label: '爸爸',
159
- constraints: {
160
- health: [
161
- '轻度高血压'
162
- ]
163
- },
164
- evidence: '用户原话:爸爸65轻度高血压'
165
- });
104
+ const cOk = ledger.appendCompanion({ label: '爸爸', constraints: { health: ['轻度高血压'] }, evidence: '用户原话:爸爸65轻度高血压' });
166
105
  assert(cOk.appended === true && ledger.readCompanions().length === 1, '同行人正常入账');
167
106
  const co = ledger.confirmOutcome({
168
- wishId: w1.wish_id,
169
- attribution: 'helpful',
170
- detail: '成了',
171
- trip: {
172
- destination: '大理',
173
- start: '2026-10-01',
174
- source: 'wish-confirmed',
175
- evidence: `wish ${w1.wish_id} confirm-outcome(helpful)`
176
- }
107
+ wishId: w1.wish_id, attribution: 'helpful', detail: '成了',
108
+ trip: { destination: '大理', start: '2026-10-01', source: 'wish-confirmed', evidence: `wish ${w1.wish_id} confirm-outcome(helpful)` },
177
109
  });
178
110
  assert(co.recorded === true, 'confirm-outcome:verified_outcome 效用事件落账');
179
111
  assert(co.trip?.appended === false && /重叠/.test(co.trip?.reason ?? ''), 'confirm-outcome 单事务:行程过守门(重叠拒收,与文件版语义一致)——两写同生或同拒');
@@ -182,76 +114,25 @@ const profileBefore = ledger.readMotivation();
182
114
  ledger.db.exec('DELETE FROM projection_docs; DELETE FROM projection_items');
183
115
  ledger.rebuildProjections();
184
116
  assert(JSON.stringify(ledger.readWishPool()) === poolBefore, 'fold 重建:愿望池与直读逐字节一致');
185
- assert(JSON.stringify(ledger.readMotivation()?.weights) === JSON.stringify(profileBefore?.weights) && (ledger.readMotivation()?.evidence?.length ?? 0) === (profileBefore?.evidence?.length ?? 0), 'fold 重建:画像与直读一致');
117
+ assert(JSON.stringify(ledger.readMotivation()?.weights) === JSON.stringify(profileBefore?.weights)
118
+ && (ledger.readMotivation()?.evidence?.length ?? 0) === (profileBefore?.evidence?.length ?? 0), 'fold 重建:画像与直读一致');
186
119
  assert(ledger.readCompanions().length === 1 && ledger.readTrips().length === 1, 'fold 重建:同行人投影与行程日志一致');
187
- const wishAddedSeqs = ledger.readEvents('wish.added', 10).map((e)=>e.seq);
120
+ const wishAddedSeqs = ledger.readEvents('wish.added', 10).map(e => e.seq);
188
121
  const firstWishSeq = Math.min(...wishAddedSeqs);
189
- ledger.appendWish({
190
- name: '普吉',
191
- reason: '',
192
- conditions: {
193
- days: 5
194
- }
195
- });
122
+ ledger.appendWish({ name: '普吉', reason: '', conditions: { days: 5 } });
196
123
  ledger.rebuildProjections(firstWishSeq);
197
124
  assert(ledger.readWishPool().length === 1, `rewind 至 seq ${firstWishSeq}:投影回到历史时点(LangGraph fork 同构)`);
198
125
  ledger.rebuildProjections();
199
126
  assert(ledger.readWishPool().length === 2, 'rebuild 无参回到最新(events 是唯一权威,投影随时可重建)');
127
+ // ---- 8:one-shot 迁移 --------------------------------------------------------------
200
128
  const mroot = mkdtempSync(join(tmpdir(), 'gotry-migrate-'));
201
129
  const mdir = join(mroot, 'gotry-state');
202
- mkdirSync(mdir, {
203
- recursive: true
204
- });
205
- writeFileSync(join(mdir, 'motivation-profile.json'), JSON.stringify({
206
- weights: {
207
- curiosity: 0.6
208
- },
209
- evidence: [
210
- '原话A'
211
- ],
212
- hard: {},
213
- updated_at: '2026-08-01T00:00:00Z'
214
- }));
215
- writeFileSync(join(mdir, 'wish-pool.json'), JSON.stringify([
216
- {
217
- wish_id: 'wLEGACY1',
218
- name: '京都',
219
- conditions: {
220
- days: 7
221
- },
222
- added_at: '2026-08-01T00:00:00Z'
223
- }
224
- ]));
225
- writeFileSync(join(mdir, 'memory-utility.jsonl'), JSON.stringify({
226
- schema: 'memory_utility_observation.v0',
227
- event_id: 'wLEGACY1|recalled||',
228
- wish_id: 'wLEGACY1',
229
- kind: 'recalled',
230
- ts: '2026-08-02T00:00:00Z'
231
- }) + '\n');
232
- writeFileSync(join(mdir, 'trips.jsonl'), JSON.stringify({
233
- schema: 'travel_timeline.v1',
234
- trip_id: '京都|2026-04-01|user-verbatim',
235
- destination: '京都',
236
- start: '2026-04-01',
237
- source: 'user-verbatim',
238
- evidence: '原话',
239
- ts: '2026-08-01T00:00:00Z'
240
- }) + '\n');
241
- writeFileSync(join(mdir, 'companions.json'), JSON.stringify([
242
- {
243
- schema: 'companion_profile.v1',
244
- companion_id: '妈妈',
245
- label: '妈妈',
246
- constraints: {
247
- mobility: '步行≤3h'
248
- },
249
- evidence: [
250
- '原话'
251
- ],
252
- ts: '2026-08-01T00:00:00Z'
253
- }
254
- ]));
130
+ mkdirSync(mdir, { recursive: true });
131
+ writeFileSync(join(mdir, 'motivation-profile.json'), JSON.stringify({ weights: { curiosity: 0.6 }, evidence: ['原话A'], hard: {}, updated_at: '2026-08-01T00:00:00Z' }));
132
+ writeFileSync(join(mdir, 'wish-pool.json'), JSON.stringify([{ wish_id: 'wLEGACY1', name: '京都', conditions: { days: 7 }, added_at: '2026-08-01T00:00:00Z' }]));
133
+ writeFileSync(join(mdir, 'memory-utility.jsonl'), JSON.stringify({ schema: 'memory_utility_observation.v0', event_id: 'wLEGACY1|recalled||', wish_id: 'wLEGACY1', kind: 'recalled', ts: '2026-08-02T00:00:00Z' }) + '\n');
134
+ writeFileSync(join(mdir, 'trips.jsonl'), JSON.stringify({ schema: 'travel_timeline.v1', trip_id: '京都|2026-04-01|user-verbatim', destination: '京都', start: '2026-04-01', source: 'user-verbatim', evidence: '原话', ts: '2026-08-01T00:00:00Z' }) + '\n');
135
+ writeFileSync(join(mdir, 'companions.json'), JSON.stringify([{ schema: 'companion_profile.v1', companion_id: '妈妈', label: '妈妈', constraints: { mobility: '步行≤3h' }, evidence: ['原话'], ts: '2026-08-01T00:00:00Z' }]));
255
136
  const m1 = ensureLedger(mroot);
256
137
  assert(m1.readMotivation()?.weights?.['curiosity'] === 0.6, '迁移:画像整档入账');
257
138
  assert(String(m1.readWishPool()[0]?.wish_id) === 'wLEGACY1', '迁移:愿望保留原主键');
@@ -260,38 +141,104 @@ assert(existsSync(join(mdir, 'pre-ledger-backup', 'wish-pool.json')), '迁移:
260
141
  const migratedCount = m1.countEvents();
261
142
  ensureLedger(mroot);
262
143
  assert(m1.countEvents() === migratedCount, '重复 ensure 不重复导入(kv 旗标 + 幂等键双保险)');
144
+ const tenantMroot = mkdtempSync(join(tmpdir(), 'gotry-migrate-tenant-boundary-'));
145
+ const tenantMdir = join(tenantMroot, 'gotry-state');
146
+ mkdirSync(tenantMdir, { recursive: true });
147
+ writeFileSync(join(tenantMdir, 'wish-pool.json'), JSON.stringify([{ wish_id: 'wLOCALLEGACY', name: '本地旧愿望', conditions: { days: 2 }, added_at: '2026-08-01T00:00:00Z' }]));
148
+ assert(!ledgerExists(tenantMroot), '迁移边界:非 local fallback 前 DB 不存在');
149
+ assert(readWishPoolWithFallback(tenantMroot, 'tenant-a').length === 0 && !ledgerExists(tenantMroot), '迁移边界:非 local 只读 fallback 不读取旧本地 JSON,也不创建 DB');
150
+ assert(existsSync(join(tenantMdir, 'wish-pool.json')), '迁移边界:非 local 只读 fallback 不触碰 legacy 文件');
151
+ const tenantFirst = ensureLedger(tenantMroot, 'tenant-a');
152
+ const localLegacy = ensureLedger(tenantMroot);
153
+ assert(tenantFirst.readWishPool().length === 0, '迁移边界:非 local 首次打开不把旧文件猜入该租户');
154
+ assert(localLegacy.readWishPool()[0]?.wish_id === 'wLOCALLEGACY', '迁移边界:旧 JSON/JSONL 只可安全归入默认 local');
155
+ assert(localLegacy.db.prepare('SELECT COUNT(*) AS n FROM events WHERE tenant_id != \'local\'').get().n === 0, '迁移边界:旧文件导入事件 tenant_id 全为 local,不伪造历史归属');
156
+ const v1root = mkdtempSync(join(tmpdir(), 'gotry-v1-tenant-migration-'));
157
+ mkdirSync(join(v1root, 'gotry-state'), { recursive: true });
158
+ const v1db = new Database(ledgerDbPath(v1root));
159
+ v1db.exec(`
160
+ CREATE TABLE events (
161
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
162
+ ts TEXT NOT NULL,
163
+ actor TEXT NOT NULL,
164
+ kind TEXT NOT NULL,
165
+ subject_id TEXT NOT NULL DEFAULT '',
166
+ payload TEXT NOT NULL,
167
+ idem_key TEXT,
168
+ run_id TEXT
169
+ );
170
+ CREATE UNIQUE INDEX events_idem ON events(idem_key) WHERE idem_key IS NOT NULL;
171
+ `);
172
+ v1db.prepare('INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key, run_id) VALUES (?, ?, ?, ?, ?, ?, NULL)')
173
+ .run('2026-08-28T00:00:00.000Z', 'system:v1', 'wish.added', 'wV1', JSON.stringify({ wish: { wish_id: 'wV1', name: 'v1旧愿望', reason: '', conditions: { days: 4 }, added_at: '2026-08-28T00:00:00.000Z' } }), 'v1:wish');
174
+ v1db.close();
175
+ const v1Tenant = ensureLedger(v1root, 'tenant-a');
176
+ const v1Local = ensureLedger(v1root);
177
+ assert(v1Tenant.readWishPool().length === 0, 'schema v1→v2:非 local 打开旧 DB 不吸收 local 事件');
178
+ assert(v1Local.readWishPool()[0]?.wish_id === 'wV1', 'schema v1→v2:旧 events 回填 local 后重建 local 投影');
179
+ assert(v1Local.db.prepare('SELECT COUNT(*) AS n FROM events WHERE tenant_id = \'local\'').get().n === 1, 'schema v1→v2:events tenant_id 回填为 local 且不丢 seq/idempotency');
180
+ const tenantMrootProc = mkdtempSync(join(tmpdir(), 'gotry-migrate-tenant-boundary-proc-'));
181
+ const tenantMdirProc = join(tenantMrootProc, 'gotry-state');
182
+ mkdirSync(tenantMdirProc, { recursive: true });
183
+ writeFileSync(join(tenantMdirProc, 'wish-pool.json'), JSON.stringify([{ wish_id: 'wPROCLEGACY', name: '子进程旧愿望', reason: 'legacy-local', conditions: { days: 3 }, added_at: '2026-08-03T00:00:00Z' }]));
184
+ const legacyProc = spawnSync('npx', ['tsx', '--eval', `
185
+ import { ensureLedger, ledgerExists, readWishPoolWithFallback } from './src/state-ledger.js'
186
+ const root = ${JSON.stringify(tenantMrootProc)}
187
+ if (ledgerExists(root)) process.exit(2)
188
+ if (readWishPoolWithFallback(root, 'tenant-a').length !== 0 || ledgerExists(root)) process.exit(3)
189
+ const tenant = ensureLedger(root, 'tenant-a')
190
+ const local = ensureLedger(root)
191
+ const foreign = local.db.prepare("SELECT COUNT(*) AS n FROM events WHERE tenant_id != 'local'").get().n
192
+ if (tenant.readWishPool().length !== 0 || local.readWishPool()[0]?.wish_id !== 'wPROCLEGACY' || foreign !== 0) process.exit(4)
193
+ `], { encoding: 'utf-8', cwd: process.cwd(), env: process.env });
194
+ assert(legacyProc.status === 0, `legacy JSON 跨进程首次非 local 迁移只归 local 且 fallback 不创建 DB(实际 ${legacyProc.status}:${(legacyProc.stderr ?? '').slice(0, 300)})`);
195
+ const v1rootProc = mkdtempSync(join(tmpdir(), 'gotry-v1-tenant-migration-proc-'));
196
+ mkdirSync(join(v1rootProc, 'gotry-state'), { recursive: true });
197
+ const v1dbProc = new Database(ledgerDbPath(v1rootProc));
198
+ v1dbProc.exec(`
199
+ CREATE TABLE events (
200
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
201
+ ts TEXT NOT NULL,
202
+ actor TEXT NOT NULL,
203
+ kind TEXT NOT NULL,
204
+ subject_id TEXT NOT NULL DEFAULT '',
205
+ payload TEXT NOT NULL,
206
+ idem_key TEXT,
207
+ run_id TEXT
208
+ );
209
+ CREATE UNIQUE INDEX events_idem ON events(idem_key) WHERE idem_key IS NOT NULL;
210
+ `);
211
+ v1dbProc.prepare('INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key, run_id) VALUES (?, ?, ?, ?, ?, ?, NULL)')
212
+ .run('2026-08-28T00:00:00.000Z', 'system:v1', 'wish.added', 'wV1PROC', JSON.stringify({ wish: { wish_id: 'wV1PROC', name: 'v1子进程旧愿望', reason: 'v1-local', conditions: { days: 5 }, added_at: '2026-08-28T00:00:00.000Z' } }), 'v1proc:wish');
213
+ v1dbProc.close();
214
+ const v1Proc = spawnSync('npx', ['tsx', '--eval', `
215
+ import { ensureLedger } from './src/state-ledger.js'
216
+ const root = ${JSON.stringify(v1rootProc)}
217
+ const tenant = ensureLedger(root, 'tenant-a')
218
+ const local = ensureLedger(root)
219
+ const localRows = local.db.prepare("SELECT COUNT(*) AS n FROM events WHERE tenant_id = 'local'").get().n
220
+ if (tenant.readWishPool().length !== 0 || local.readWishPool()[0]?.wish_id !== 'wV1PROC' || localRows !== 1) process.exit(5)
221
+ `], { encoding: 'utf-8', cwd: process.cwd(), env: process.env });
222
+ assert(v1Proc.status === 0, `schema v1→v2 跨进程首次非 local 打开不吸收 local 事件(实际 ${v1Proc.status}:${(v1Proc.stderr ?? '').slice(0, 300)})`);
223
+ // ---- 9:durable 工单崩溃恢复(exactly-once) ----------------------------------------
263
224
  const wroot = mkdtempSync(join(tmpdir(), 'gotry-wf-'));
264
225
  const countFile = join(wroot, 'solve-count.txt');
265
226
  writeFileSync(countFile, '');
266
- const crash = spawnSync('npx', [
267
- 'tsx',
268
- 'scripts/ledger-workflow-crash.ts',
269
- wroot,
270
- 'dp-crash1',
271
- countFile
272
- ], {
273
- encoding: 'utf-8'
274
- });
227
+ const crash = spawnSync('npx', ['tsx', 'scripts/ledger-workflow-crash.ts', wroot, 'dp-crash1', countFile], { encoding: 'utf-8' });
275
228
  assert(crash.status === 9, `崩溃探针按设计 exit 9(实际 ${crash.status}:${(crash.stderr ?? '').slice(0, 200)})`);
276
229
  const runRow = openLedgerIfExists(wroot)?.getWorkflowRun('dp-crash1');
277
230
  assert(runRow?.status === 'pending', '崩溃后工单仍 pending(账本权威未损)');
278
231
  const count1 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
279
232
  assert(count1 === 1, '崩溃前真实求解恰 1 次');
280
- const resume = spawnSync('npx', [
281
- 'tsx',
282
- 'scripts/async-collect.ts',
283
- 'dp-crash1',
284
- wroot
285
- ], {
286
- encoding: 'utf-8',
287
- env: {
288
- ...process.env,
289
- GOTRY_SOLVE_COUNT_FILE: countFile
290
- }
233
+ const resume = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', 'dp-crash1', wroot], {
234
+ encoding: 'utf-8', env: { ...process.env, GOTRY_SOLVE_COUNT_FILE: countFile },
291
235
  });
292
236
  const failedOutcome = terminalOutcomeOf(resume.stdout ?? '');
293
237
  assert(resume.status === 2, `非4/4 恢复进程 exit 2(实际 ${resume.status}:${(resume.stderr ?? '').slice(0, 300)})`);
294
- assert(failedOutcome?.status === 'failed' && failedOutcome.ticket_id === 'dp-crash1' && failedOutcome.passed < failedOutcome.total && failedOutcome.failed_checks.length > 0, '非4/4 输出 gotry_async_terminal.v1/failed 与失败项');
238
+ assert(failedOutcome?.status === 'failed'
239
+ && failedOutcome.ticket_id === 'dp-crash1'
240
+ && failedOutcome.passed < failedOutcome.total
241
+ && failedOutcome.failed_checks.length > 0, '非4/4 输出 gotry_async_terminal.v1/failed 与失败项');
295
242
  const count2 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
296
243
  assert(count2 === 1, '恢复时 done 步骤零重算(exactly-once:求解计数仍 1,不重复花钱)');
297
244
  const failedDeliverablePath = join(wroot, 'gotry-state', 'async', 'dp-crash1.deliverable.md');
@@ -299,77 +246,50 @@ assert(existsSync(failedDeliverablePath), '交付物视图已落盘(.deliverable
299
246
  const failedLedger = openLedgerIfExists(wroot);
300
247
  assert(failedLedger?.getWorkflowRun('dp-crash1')?.status === 'failed', '非4/4 账本终态 failed');
301
248
  assert(JSON.stringify(failedLedger?.getWorkflowTerminalOutcome('dp-crash1')) === JSON.stringify(failedOutcome), '非4/4 结构化终态随 async.failed 事件落账');
302
- const replay = spawnSync('npx', [
303
- 'tsx',
304
- 'scripts/async-collect.ts',
305
- 'dp-crash1',
306
- wroot
307
- ], {
308
- encoding: 'utf-8',
309
- env: {
310
- ...process.env,
311
- GOTRY_SOLVE_COUNT_FILE: countFile
312
- }
249
+ const replay = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', 'dp-crash1', wroot], {
250
+ encoding: 'utf-8', env: { ...process.env, GOTRY_SOLVE_COUNT_FILE: countFile },
313
251
  });
314
252
  assert(replay.status === 2 && JSON.stringify(terminalOutcomeOf(replay.stdout ?? '')) === JSON.stringify(failedOutcome), 'failed 终态复诵保持 exit 2 与同一结构化结果');
315
253
  const count3 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
316
254
  assert(count3 === 1, '终态复诵零重算');
317
255
  const failedDeliverable = readFileSync(failedDeliverablePath, 'utf-8');
318
256
  rmSync(failedDeliverablePath);
319
- const replayAfterMissingView = spawnSync('npx', [
320
- 'tsx',
321
- 'scripts/async-collect.ts',
322
- 'dp-crash1',
323
- wroot
324
- ], {
325
- encoding: 'utf-8',
326
- env: {
327
- ...process.env,
328
- GOTRY_SOLVE_COUNT_FILE: countFile
329
- }
257
+ const replayAfterMissingView = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', 'dp-crash1', wroot], {
258
+ encoding: 'utf-8', env: { ...process.env, GOTRY_SOLVE_COUNT_FILE: countFile },
330
259
  });
331
- assert(replayAfterMissingView.status === 2 && JSON.stringify(terminalOutcomeOf(replayAfterMissingView.stdout ?? '')) === JSON.stringify(failedOutcome), 'failed 终态缺失视图时仍复诵同一 exit 2 与结构化结果');
260
+ assert(replayAfterMissingView.status === 2
261
+ && JSON.stringify(terminalOutcomeOf(replayAfterMissingView.stdout ?? '')) === JSON.stringify(failedOutcome), 'failed 终态缺失视图时仍复诵同一 exit 2 与结构化结果');
332
262
  assert(existsSync(failedDeliverablePath) && readFileSync(failedDeliverablePath, 'utf-8') === failedDeliverable, 'failed 终态复诵重建缺失 .deliverable.md 视图');
333
263
  const countAfterViewRepair = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
334
264
  assert(countAfterViewRepair === 1, '终态视图修复零重算');
265
+ // state-cli tick 是另一个 durable driver;即使旧调用未显式透传 outcome,
266
+ // settle 层也必须从账本中的权威 state/step 恢复机器终态,不能误结算为成功。
335
267
  const tickRoot = mkdtempSync(join(tmpdir(), 'gotry-wf-tick-failed-'));
336
268
  const tickTicket = {
337
269
  id: 'dp-tick-failed1',
338
270
  objective: 'state-cli 非4/4 机器终态回归',
339
271
  requestedAt: new Date().toISOString(),
340
- etaLabel: '秒级'
272
+ etaLabel: '秒级',
341
273
  };
342
274
  const tickState = {
343
- calendar: {
344
- year: 2026,
345
- assertedWeekdays: {}
346
- },
275
+ calendar: { year: 2026, assertedWeekdays: {} },
347
276
  profile: {},
348
277
  gates: [],
349
- wishes: []
278
+ wishes: [],
350
279
  };
351
280
  await persistAsyncTicket(tickTicket, tickState, tickRoot);
352
- const tick = spawnSync('npx', [
353
- 'tsx',
354
- 'scripts/state-cli.ts',
355
- 'tick',
356
- tickRoot
357
- ], {
358
- encoding: 'utf-8'
359
- });
281
+ const tick = spawnSync('npx', ['tsx', 'scripts/state-cli.ts', 'tick', tickRoot], { encoding: 'utf-8' });
360
282
  assert(tick.status === 0, `state-cli tick 完成回收(实际 ${tick.status}:${(tick.stderr ?? '').slice(0, 200)})`);
361
283
  const tickLedger = openLedgerIfExists(tickRoot);
362
284
  const tickOutcome = tickLedger?.getWorkflowTerminalOutcome(tickTicket.id);
363
- assert(tickLedger?.getWorkflowRun(tickTicket.id)?.status === 'failed' && tickOutcome?.schema === 'gotry_async_terminal.v1' && tickOutcome.status === 'failed' && tickOutcome.passed === 0 && tickOutcome.failed_checks.length === 4, 'state-cli tick 未透传 outcome 时仍从权威账本恢复非4/4 failed 终态');
364
- const tickReplay = spawnSync('npx', [
365
- 'tsx',
366
- 'scripts/async-collect.ts',
367
- tickTicket.id,
368
- tickRoot
369
- ], {
370
- encoding: 'utf-8'
371
- });
372
- assert(tickReplay.status === 2 && JSON.stringify(terminalOutcomeOf(tickReplay.stdout ?? '')) === JSON.stringify(tickOutcome), 'state-cli 写入的 failed 终态可由 collector 幂等复诵为同一 outcome/exit 2');
285
+ assert(tickLedger?.getWorkflowRun(tickTicket.id)?.status === 'failed'
286
+ && tickOutcome?.schema === 'gotry_async_terminal.v1'
287
+ && tickOutcome.status === 'failed'
288
+ && tickOutcome.passed === 0
289
+ && tickOutcome.failed_checks.length === 4, 'state-cli tick 未透传 outcome 时仍从权威账本恢复非4/4 failed 终态');
290
+ const tickReplay = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', tickTicket.id, tickRoot], { encoding: 'utf-8' });
291
+ assert(tickReplay.status === 2
292
+ && JSON.stringify(terminalOutcomeOf(tickReplay.stdout ?? '')) === JSON.stringify(tickOutcome), 'state-cli 写入的 failed 终态可由 collector 幂等复诵为同一 outcome/exit 2');
373
293
  const successRoot = mkdtempSync(join(tmpdir(), 'gotry-wf-success-'));
374
294
  const successCountFile = join(successRoot, 'solve-count.txt');
375
295
  writeFileSync(successCountFile, '');
@@ -377,73 +297,49 @@ const successTicket = {
377
297
  id: 'dp-success1',
378
298
  objective: '4/4 机器终态回归',
379
299
  requestedAt: new Date().toISOString(),
380
- etaLabel: '秒级'
300
+ etaLabel: '秒级',
381
301
  };
382
302
  const successSpec = parseFlightPackToSpec(JSON.parse(readFileSync(join('..', 'data', 'flights_2026.json'), 'utf-8')));
383
303
  successSpec.budgetCny = 9000;
384
304
  const successState = {
385
- calendar: {
386
- year: 2026,
387
- assertedWeekdays: {}
388
- },
305
+ calendar: { year: 2026, assertedWeekdays: {} },
389
306
  profile: {},
390
307
  gates: [],
391
308
  wishes: [],
392
- spec: successSpec
309
+ spec: successSpec,
393
310
  };
394
311
  await persistAsyncTicket(successTicket, successState, successRoot);
395
- const success = spawnSync('npx', [
396
- 'tsx',
397
- 'scripts/async-collect.ts',
398
- successTicket.id,
399
- successRoot
400
- ], {
401
- encoding: 'utf-8',
402
- env: {
403
- ...process.env,
404
- GOTRY_SOLVE_COUNT_FILE: successCountFile
405
- }
312
+ const success = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', successTicket.id, successRoot], {
313
+ encoding: 'utf-8', env: { ...process.env, GOTRY_SOLVE_COUNT_FILE: successCountFile },
406
314
  });
407
315
  const successOutcome = terminalOutcomeOf(success.stdout ?? '');
408
- assert(success.status === 0 && successOutcome?.status === 'succeeded' && successOutcome.ticket_id === successTicket.id && successOutcome.passed === 4 && successOutcome.total === 4 && successOutcome.failed_checks.length === 0, `4/4 输出 gotry_async_terminal.v1/succeeded 且 exit 0(实际 ${success.status})`);
316
+ assert(success.status === 0
317
+ && successOutcome?.status === 'succeeded'
318
+ && successOutcome.ticket_id === successTicket.id
319
+ && successOutcome.passed === 4
320
+ && successOutcome.total === 4
321
+ && successOutcome.failed_checks.length === 0, `4/4 输出 gotry_async_terminal.v1/succeeded 且 exit 0(实际 ${success.status})`);
409
322
  const successLedger = openLedgerIfExists(successRoot);
410
- assert(successLedger?.getWorkflowRun(successTicket.id)?.status === 'settled' && successLedger.getWorkflowTerminalOutcome(successTicket.id)?.['status'] === 'succeeded', '4/4 账本终态 settled 且结构化结果随 async.settled 落账');
411
- const successReplay = spawnSync('npx', [
412
- 'tsx',
413
- 'scripts/async-collect.ts',
414
- successTicket.id,
415
- successRoot
416
- ], {
417
- encoding: 'utf-8',
418
- env: {
419
- ...process.env,
420
- GOTRY_SOLVE_COUNT_FILE: successCountFile
421
- }
323
+ assert(successLedger?.getWorkflowRun(successTicket.id)?.status === 'settled'
324
+ && successLedger.getWorkflowTerminalOutcome(successTicket.id)?.['status'] === 'succeeded', '4/4 账本终态 settled 且结构化结果随 async.settled 落账');
325
+ const successReplay = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', successTicket.id, successRoot], {
326
+ encoding: 'utf-8', env: { ...process.env, GOTRY_SOLVE_COUNT_FILE: successCountFile },
422
327
  });
423
328
  const successCount = readFileSync(successCountFile, 'utf-8').split('\n').filter(Boolean).length;
424
- assert(successReplay.status === 0 && successCount === 1 && JSON.stringify(terminalOutcomeOf(successReplay.stdout ?? '')) === JSON.stringify(successOutcome), 'succeeded 终态复诵保持 exit 0、同一结构化结果且零重算');
425
- const pw1 = ledger.requestPendingWrite({
426
- idemKey: 'booking:demo-1',
427
- seam: 'flight-order-confirm',
428
- payload: {
429
- flight: 'MU123'
430
- }
431
- });
432
- const pw2 = ledger.requestPendingWrite({
433
- idemKey: 'booking:demo-1',
434
- seam: 'flight-order-confirm',
435
- payload: {
436
- flight: 'MU123'
437
- }
438
- });
329
+ assert(successReplay.status === 0
330
+ && successCount === 1
331
+ && JSON.stringify(terminalOutcomeOf(successReplay.stdout ?? '')) === JSON.stringify(successOutcome), 'succeeded 终态复诵保持 exit 0、同一结构化结果且零重算');
332
+ // ---- 10:pending_writes saga + what-if 分叉 -----------------------------------------
333
+ const pw1 = ledger.requestPendingWrite({ idemKey: 'booking:demo-1', seam: 'flight-order-confirm', payload: { flight: 'MU123' } });
334
+ const pw2 = ledger.requestPendingWrite({ idemKey: 'booking:demo-1', seam: 'flight-order-confirm', payload: { flight: 'MU123' } });
439
335
  assert(pw1.created === true && pw2.created === false, 'pending_writes 幂等键去重(同一确认不可能登记两次)');
440
336
  const cf1 = ledger.confirmPendingWrite('booking:demo-1', 'PNR-ABC');
441
337
  const cf2 = ledger.confirmPendingWrite('booking:demo-1', 'PNR-ABC2');
442
338
  assert(cf1.ok === true && cf2.ok === false && cf2.status === 'confirmed', 'L3 确认只能发生一次,二连击被拒');
443
339
  const cp1 = ledger.compensatePendingWrite('booking:demo-1', '用户改签退款');
444
340
  assert(cp1.ok === true && ledger.listPendingWrites()[0]?.status === 'compensated', 'saga 补偿可达(pending/confirmed → compensated)');
445
- const audit = ledger.readEvents(undefined, 100).filter((e)=>e.kind.startsWith('write.'));
446
- assert(audit.some((e)=>e.kind === 'write.pending') && audit.some((e)=>e.kind === 'write.confirmed') && audit.some((e)=>e.kind === 'write.compensated'), '写权审计事件链完整(append-only,WriteGate 的 receipt 落点)');
341
+ const audit = ledger.readEvents(undefined, 100).filter(e => e.kind.startsWith('write.'));
342
+ assert(audit.some(e => e.kind === 'write.pending') && audit.some(e => e.kind === 'write.confirmed') && audit.some(e => e.kind === 'write.compensated'), '写权审计事件链完整(append-only,WriteGate 的 receipt 落点)');
447
343
  const forkPath = join(root, 'whatif.db');
448
344
  ledger.forkWhatIf(forkPath);
449
345
  const forkDb = new Database(forkPath);
@@ -451,45 +347,134 @@ forkDb.prepare("INSERT INTO events (ts, actor, kind, subject_id, payload) VALUES
451
347
  const forkCount = forkDb.prepare('SELECT COUNT(*) AS n FROM events').get().n;
452
348
  forkDb.close();
453
349
  assert(ledger.countEvents() === forkCount - 1, 'what-if 分叉:副本写入不触正本(VACUUM INTO 预演)');
454
- const localLedger = ensureLedger(root);
455
- const u123 = ensureLedger(root, 'u123');
456
- localLedger.appendWish({
457
- name: '本地愿望',
458
- reason: '',
459
- conditions: {
460
- days: 3
461
- }
462
- });
463
- u123.appendWish({
464
- name: '云端愿望',
465
- reason: '',
466
- conditions: {
467
- days: 7
468
- }
469
- });
470
- assert(localLedger.readWishPool().some((w)=>String(w.name) === '本地愿望'), '本地租户读到自己的愿望');
471
- assert(!localLedger.readWishPool().some((w)=>String(w.name) === '云端愿望'), '本地租户读不到云端愿望(隔离)');
472
- assert(u123.readWishPool().some((w)=>String(w.name) === '云端愿望'), '云端租户读到自己的愿望');
473
- assert(!u123.readWishPool().some((w)=>String(w.name) === '本地愿望'), '云端租户读不到本地愿望(隔离)');
474
- assert(localLedger.tenant === 'local' && u123.tenant === 'u123', 'tenant_id 从第一天就是一等字段');
475
- rmSync(root, {
476
- recursive: true,
477
- force: true
478
- });
479
- rmSync(mroot, {
480
- recursive: true,
481
- force: true
482
- });
483
- rmSync(wroot, {
484
- recursive: true,
485
- force: true
486
- });
487
- rmSync(successRoot, {
488
- recursive: true,
489
- force: true
490
- });
350
+ // ---- 11:双形态(ADR-16):tenant scope 贯穿 append/read/fold/rebuild/API ----------------
351
+ const tenantRoot = mkdtempSync(join(tmpdir(), 'gotry-tenant-scope-'));
352
+ const localLedger = ensureLedger(tenantRoot); // 默认 tenant='local'
353
+ const tenantA = ensureLedger(tenantRoot, 'tenant-a');
354
+ const tenantB = ensureLedger(tenantRoot, 'tenant-b');
355
+ const localWish = localLedger.appendWish({ name: '同 id 愿望', reason: 'local0', conditions: { owner: 'local0' } });
356
+ const wishA = tenantA.appendWish({ name: '同 id 愿望', reason: 'a0', conditions: { owner: 'a0' } });
357
+ const wishB = tenantB.appendWish({ name: '同 id 愿望', reason: 'b0', conditions: { owner: 'b0' } });
358
+ tenantA.appendWish({ name: '同 id 愿望', reason: 'a1', conditions: { owner: 'a1' } });
359
+ tenantB.appendWish({ name: '同 id 愿望', reason: 'b1', conditions: { owner: 'b1' } });
360
+ const utilA = tenantA.appendUtilityEvent({ wish_id: wishA.wish_id, kind: 'recalled', ts: '2026-09-08T00:00:00.000Z', ctx: 'same-ctx' });
361
+ const utilB = tenantB.appendUtilityEvent({ wish_id: wishB.wish_id, kind: 'recalled', ts: '2026-09-08T00:00:00.000Z', ctx: 'same-ctx' });
362
+ assert(localWish.wish_id === wishA.wish_id && wishA.wish_id === wishB.wish_id, '同业务 id/wish_id 可跨 local/tenant-a/tenant-b 并存');
363
+ assert(utilA.appended === true && utilB.appended === true, '同 event_id/idempotency key 跨租户独立追加(UNIQUE tenant-scoped)');
364
+ const tenantRows = localLedger.db.prepare('SELECT tenant_id, kind, subject_id, idem_key FROM events WHERE subject_id = ? ORDER BY tenant_id, seq').all(wishA.wish_id);
365
+ assert(tenantRows.filter(r => r.kind === 'wish.added').map(r => r.tenant_id).sort().join(',') === 'local,tenant-a,tenant-b'
366
+ && tenantRows.filter(r => r.kind === 'wish.updated').map(r => r.tenant_id).sort().join(',') === 'tenant-a,tenant-b', '非 local 写入 events 保留真实 owner,默认 local 兼容不变');
367
+ assert(tenantA.readEvents(undefined, 100).every(e => e.tenant_id === 'tenant-a')
368
+ && tenantB.readEvents(undefined, 100).every(e => e.tenant_id === 'tenant-b')
369
+ && localLedger.readEvents(undefined, 100).every(e => e.tenant_id === 'local'), 'readEvents 默认与 kind 过滤前均先按 ledger.tenant 隔离');
370
+ assert(tenantA.readEvents('wish.added', 10).length === 1 && tenantB.readEvents('wish.added', 10).length === 1, 'readEvents(kind) 也按租户过滤');
371
+ assert(!localLedger.readWishPool().some(w => w.conditions.owner === 'a1')
372
+ && !tenantA.readWishPool().some(w => w.conditions.owner === 'b1'), '直写投影:默认 local/tenant-a/tenant-b 同 id 互不覆盖');
373
+ const localBeforeRebuild = JSON.stringify(localLedger.readWishPool());
374
+ const aBeforeRebuild = JSON.stringify(tenantA.readWishPool());
375
+ const bBeforeRebuild = JSON.stringify(tenantB.readWishPool());
376
+ tenantA.rebuildProjections();
377
+ assert(JSON.stringify(tenantA.readWishPool()) === aBeforeRebuild, 'tenant-a rebuild 只读自己的 events 并还原交错 update 后投影');
378
+ assert(JSON.stringify(tenantB.readWishPool()) === bBeforeRebuild && JSON.stringify(localLedger.readWishPool()) === localBeforeRebuild, 'tenant-a rebuild 不改写 tenant-b/local 投影');
379
+ tenantA.rebuildProjections();
380
+ assert(JSON.stringify(tenantA.readWishPool()) === aBeforeRebuild, 'tenant-a 重复 rebuild 幂等');
381
+ tenantB.rebuildProjections();
382
+ assert(JSON.stringify(tenantB.readWishPool()) === bBeforeRebuild, 'tenant-b rebuild 在 tenant-a 同 item_id 存在时不串读 wish.updated fold 行');
383
+ localLedger.rebuildProjections();
384
+ assert(JSON.stringify(localLedger.readWishPool()) === localBeforeRebuild, '默认 local rebuild 兼容且不吸收非 local events');
385
+ const reopenScript = `
386
+ import { ensureLedger } from './src/state-ledger.js'
387
+ const root = ${JSON.stringify(tenantRoot)}
388
+ const a = ensureLedger(root, 'tenant-a')
389
+ const b = ensureLedger(root, 'tenant-b')
390
+ const local = ensureLedger(root)
391
+ const aWish = a.readWishPool()[0]
392
+ const bWish = b.readWishPool()[0]
393
+ const localWish = local.readWishPool()[0]
394
+ const ok = aWish?.name === '同 id 愿望'
395
+ && bWish?.name === '同 id 愿望'
396
+ && localWish?.name === '同 id 愿望'
397
+ && aWish?.conditions?.owner === 'a1'
398
+ && bWish?.conditions?.owner === 'b1'
399
+ && localWish?.conditions?.owner === 'local0'
400
+ && a.readEvents(undefined, 100).every(e => e.tenant_id === 'tenant-a')
401
+ && b.readEvents(undefined, 100).every(e => e.tenant_id === 'tenant-b')
402
+ && local.readEvents(undefined, 100).every(e => e.tenant_id === 'local')
403
+ if (!ok) {
404
+ console.error(JSON.stringify({ aWish, bWish, localWish, aEvents: a.readEvents(undefined, 100), bEvents: b.readEvents(undefined, 100), localEvents: local.readEvents(undefined, 100) }))
405
+ process.exit(1)
406
+ }
407
+ `;
408
+ const reopenTenant = spawnSync('npx', ['tsx', '--eval', reopenScript], { encoding: 'utf-8', cwd: process.cwd(), env: process.env });
409
+ assert(reopenTenant.status === 0, `跨进程 reopen 后租户事件/投影仍隔离(实际 ${reopenTenant.status}:${(reopenTenant.stderr ?? '').slice(0, 300)})`);
410
+ assert(localLedger.tenant === 'local' && tenantA.tenant === 'tenant-a' && tenantB.tenant === 'tenant-b', 'tenant_id 从第一天就是一等字段');
411
+ const probeASeq = tenantA.insertEvent({ actor: 'test:tenant-scope', kind: 'tenant.probe', subjectId: 'probe-a', payload: { tenant: 'A' }, ts: '2026-09-08T00:00:10.000Z' });
412
+ const probeBSeq = tenantB.insertEvent({ actor: 'test:tenant-scope', kind: 'tenant.probe', subjectId: 'probe-b-newer', payload: { tenant: 'B' }, ts: '2026-09-08T00:00:11.000Z' });
413
+ const aProbe = tenantA.readEvents('tenant.probe', 1)[0];
414
+ assert((probeASeq ?? 0) < (probeBSeq ?? 0) && aProbe?.tenant_id === 'tenant-a' && aProbe.subject_id === 'probe-a', 'readEvents(kind, limit) 先按 tenant 过滤再截断:tenant-b 较新事件不抢 tenant-a limit=1');
415
+ const foldFixtureRoot = mkdtempSync(join(tmpdir(), 'gotry-tenant-fold-fixture-'));
416
+ const foldA = ensureLedger(foldFixtureRoot, 'tenant-a');
417
+ const foldB = ensureLedger(foldFixtureRoot, 'tenant-b');
418
+ const sameFoldId = 'w-fold-same-id';
419
+ const foldWishA = { wish_id: sameFoldId, name: '同主键反例', reason: 'A-fixed-reason', conditions: { owner: 'A-fixed', days: 5 }, added_at: '2026-09-08T00:00:00.000Z' };
420
+ const foldWishB = { wish_id: sameFoldId, name: '同主键反例', reason: 'B-fixed-reason', conditions: { owner: 'B-fixed', days: 9 }, added_at: '2026-09-08T00:00:01.000Z' };
421
+ const foldInsert = foldA.db.prepare(`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id) VALUES (?, ?, 'test:fold-fixture', ?, ?, ?, ?, NULL)`);
422
+ foldInsert.run('tenant-a', '2026-09-08T00:00:00.000Z', 'wish.added', sameFoldId, JSON.stringify({ wish: foldWishA }), 'fold:a:add');
423
+ foldInsert.run('tenant-b', '2026-09-08T00:00:01.000Z', 'wish.added', sameFoldId, JSON.stringify({ wish: foldWishB }), 'fold:b:add');
424
+ foldInsert.run('tenant-b', '2026-09-08T00:00:02.000Z', 'wish.updated', sameFoldId, JSON.stringify({ wish_id: sameFoldId, patch: { muted: true } }), 'fold:b:muted');
425
+ const fixtureEvents = foldA.db.prepare('SELECT tenant_id, kind, subject_id FROM events WHERE subject_id = ? ORDER BY seq').all(sameFoldId);
426
+ assert(fixtureEvents.map(e => `${e.tenant_id}:${e.kind}`).join(',') === 'tenant-a:wish.added,tenant-b:wish.added,tenant-b:wish.updated', '确定性 fold fixture:同 wish_id 的 A/B events 拥有固定且正确 tenant 归属');
427
+ foldA.rebuildProjections();
428
+ const foldABefore = JSON.stringify(foldA.readWishPool()[0]);
429
+ foldB.rebuildProjections();
430
+ const foldAAfter = foldA.readWishPool()[0];
431
+ const foldBAfter = foldB.readWishPool()[0];
432
+ const unscopedFoldRow = foldB.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND item_id = ?`).get(sameFoldId);
433
+ const unscopedFoldDoc = unscopedFoldRow ? JSON.parse(unscopedFoldRow.doc) : null;
434
+ assert(JSON.stringify(foldAAfter) === foldABefore, '精确 fold 反例:A rebuild 后字段固定,B rebuild 不改变 A 投影');
435
+ assert(foldBAfter?.reason === 'B-fixed-reason'
436
+ && foldBAfter.conditions.owner === 'B-fixed'
437
+ && foldBAfter.conditions.days === 9
438
+ && foldBAfter.added_at === '2026-09-08T00:00:01.000Z'
439
+ && foldBAfter.muted === true, '精确 fold 反例:B 仅 patch muted=true 后仍保留 B 的 reason/conditions/added_at');
440
+ assert(unscopedFoldDoc?.reason === 'A-fixed-reason', '负向控制:若 wish.updated fold 去掉 tenant 条件,该 fixture 会稳定先读 A 行并把 B 变成 A');
441
+ const workflowRoot = mkdtempSync(join(tmpdir(), 'gotry-tenant-workflow-'));
442
+ const wfA = ensureLedger(workflowRoot, 'tenant-a');
443
+ const wfB = ensureLedger(workflowRoot, 'tenant-b');
444
+ wfA.createWorkflowRun({ id: 'wf-same', goal: 'tenant-a-goal', ticket: { tenant: 'A' }, state: { tenant: 'A' } });
445
+ wfB.createWorkflowRun({ id: 'wf-same', goal: 'tenant-b-goal', ticket: { tenant: 'B' }, state: { tenant: 'B' } });
446
+ wfB.markStepIntent('wf-same', 'step-1');
447
+ wfB.markStepDone('wf-same', 'step-1', { ok: true });
448
+ wfB.settleWorkflowRun('wf-same', 'tenant-b deliverable', { schema: 'test', status: 'succeeded' });
449
+ const workflowRows = wfA.db.prepare('SELECT tenant_id, goal, status, deliverable FROM workflow_runs WHERE id = ? ORDER BY tenant_id').all('wf-same');
450
+ assert(wfA.getWorkflowRun('wf-same')?.status === 'pending'
451
+ && wfB.getWorkflowRun('wf-same')?.status === 'settled'
452
+ && workflowRows.map(r => `${r.tenant_id}:${r.goal}:${r.status}:${r.deliverable ?? ''}`).join('|') === 'tenant-a:tenant-a-goal:pending:|tenant-b:tenant-b-goal:settled:tenant-b deliverable', '非 local workflow_runs 同 id 行按 tenant 绑定,tenant-b settle 不改 tenant-a pending');
453
+ assert(wfA.getWorkflowStep('wf-same', 'step-1') === undefined && wfB.getWorkflowStep('wf-same', 'step-1')?.status === 'done', '非 local workflow_steps 行按 tenant 绑定');
454
+ const wfBAsyncEvents = wfB.readEvents(undefined, 20).filter(e => e.kind.startsWith('async.'));
455
+ assert(wfBAsyncEvents.some(e => e.kind === 'async.settled') && wfBAsyncEvents.every(e => e.tenant_id === 'tenant-b'), '非 local workflow 审计事件 owner 不回落 local且含 settled 终态');
456
+ const forgetRoot = mkdtempSync(join(tmpdir(), 'gotry-tenant-forget-'));
457
+ const forgetA = ensureLedger(forgetRoot, 'tenant-a');
458
+ const forgetB = ensureLedger(forgetRoot, 'tenant-b');
459
+ const forgetAId = forgetA.appendWish({ name: '同 id 可忘愿望', reason: 'keep-A', conditions: { owner: 'A' } }).wish_id;
460
+ const forgetBId = forgetB.appendWish({ name: '同 id 可忘愿望', reason: 'drop-B', conditions: { owner: 'B' } }).wish_id;
461
+ forgetB.forgetSubject([{ kinds: ['wish.added', 'wish.updated', 'memory_utility.event'], subjectId: forgetBId }]);
462
+ const forgetRows = forgetA.db.prepare('SELECT tenant_id, COUNT(*) AS n FROM events WHERE subject_id = ? GROUP BY tenant_id ORDER BY tenant_id').all(forgetAId);
463
+ assert(forgetAId === forgetBId && forgetA.readWishPool().length === 1 && forgetB.readWishPool().length === 0, '非 local forget 只删目标 tenant 投影,A 同 id 愿望不受影响');
464
+ assert(forgetRows.map(r => `${r.tenant_id}:${r.n}`).join(',') === 'tenant-a:1' && forgetB.readEvents('forget.executed', 1)[0]?.tenant_id === 'tenant-b', '非 local forget 事件删除范围与审计事件 owner 均按 tenant 绑定');
465
+ // ---- 收尾 --------------------------------------------------------------------------
466
+ rmSync(root, { recursive: true, force: true });
467
+ rmSync(mroot, { recursive: true, force: true });
468
+ rmSync(tenantMroot, { recursive: true, force: true });
469
+ rmSync(tenantMrootProc, { recursive: true, force: true });
470
+ rmSync(v1root, { recursive: true, force: true });
471
+ rmSync(v1rootProc, { recursive: true, force: true });
472
+ rmSync(tenantRoot, { recursive: true, force: true });
473
+ rmSync(foldFixtureRoot, { recursive: true, force: true });
474
+ rmSync(workflowRoot, { recursive: true, force: true });
475
+ rmSync(forgetRoot, { recursive: true, force: true });
476
+ rmSync(wroot, { recursive: true, force: true });
477
+ rmSync(successRoot, { recursive: true, force: true });
491
478
  console.log(`\nLEDGER TESTS: ${pass} ok, ${fail} fail`);
492
- if (fail > 0) process.exit(1);
493
-
494
-
495
- //# sourceURL=ts/scripts/ledger-tests.ts
479
+ if (fail > 0)
480
+ process.exit(1);