@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.28

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 (533) hide show
  1. package/README.md +172 -219
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1788 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -56
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +161 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +553 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +947 -110
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +676 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/provider-detail-sanitize.js +64 -0
  41. package/dist/capabilities/resilience.js +191 -0
  42. package/dist/capabilities/route-provider-conformance.js +341 -0
  43. package/dist/capabilities/session/action-cache.js +50 -44
  44. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  45. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  46. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  47. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  48. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  49. package/dist/capabilities/session/benchmark.js +189 -0
  50. package/dist/capabilities/session/bridge-ledger.js +224 -0
  51. package/dist/capabilities/session/extension-bridge.js +636 -0
  52. package/dist/capabilities/session/extension-channel.js +109 -0
  53. package/dist/capabilities/session/extension-distribution.js +230 -0
  54. package/dist/capabilities/session/extract.js +31 -21
  55. package/dist/capabilities/session/golden-score.js +114 -0
  56. package/dist/capabilities/session/health-watch.js +215 -0
  57. package/dist/capabilities/session/read-guard.js +75 -24
  58. package/dist/capabilities/session/static-flight-golden.js +150 -0
  59. package/dist/capabilities/session/transport.js +97 -83
  60. package/dist/capabilities/session/wizard.js +117 -0
  61. package/dist/capabilities/session-consent.js +178 -0
  62. package/dist/capabilities/session-login.js +170 -0
  63. package/dist/capabilities/session-search.js +635 -44
  64. package/dist/capabilities/spawn-bounded.js +203 -0
  65. package/dist/capabilities/sponsor-plugin.js +341 -0
  66. package/dist/capabilities/visa-policy.js +140 -0
  67. package/dist/capabilities/weather.js +243 -138
  68. package/dist/data/session-golden-20.json +25 -0
  69. package/dist/data/sf-golden-manifest.json +103 -0
  70. package/dist/data/sf-static-routes.json +91 -0
  71. package/dist/scripts/action-cache-tests.js +24 -59
  72. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  73. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  74. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  75. package/dist/scripts/agent-reach-error-tests.js +354 -0
  76. package/dist/scripts/agent-reach-tests.js +26 -18
  77. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  78. package/dist/scripts/anything-path-measure-tests.js +133 -0
  79. package/dist/scripts/anything-path-measure.js +150 -0
  80. package/dist/scripts/anything-tests.js +250 -60
  81. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  82. package/dist/scripts/artifacts-capability-tests.js +727 -0
  83. package/dist/scripts/async-collect.js +50 -11
  84. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  85. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  86. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  87. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  88. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  89. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  90. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  91. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  92. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  93. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  94. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  95. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
  96. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  97. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  98. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  99. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  100. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  101. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  102. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  103. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  104. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  105. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  106. package/dist/scripts/booking-executor-tests.js +80 -0
  107. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  108. package/dist/scripts/booking-saga-tests.js +125 -0
  109. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  110. package/dist/scripts/bootstrap-tests.js +1549 -0
  111. package/dist/scripts/bridge-events-tests.js +239 -0
  112. package/dist/scripts/bridge-ledger-tests.js +428 -0
  113. package/dist/scripts/build-changelog.js +238 -0
  114. package/dist/scripts/build-metrics-report.js +331 -0
  115. package/dist/scripts/changelog-tests.js +118 -0
  116. package/dist/scripts/channel-probe-tests.js +103 -0
  117. package/dist/scripts/channel-probe.js +169 -0
  118. package/dist/scripts/channel-registry-tests.js +208 -0
  119. package/dist/scripts/city-scenario-tier-tests.js +234 -0
  120. package/dist/scripts/companion-tests.js +25 -70
  121. package/dist/scripts/dida-cookie-probe.js +87 -0
  122. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  123. package/dist/scripts/dida-sw-probe.js +114 -0
  124. package/dist/scripts/diff-test.js +17 -12
  125. package/dist/scripts/doctor-tests.js +600 -0
  126. package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
  127. package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
  128. package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
  129. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  130. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  131. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  132. package/dist/scripts/dsh-llm-tests.js +437 -0
  133. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  134. package/dist/scripts/dsh-runtime-closure.js +162 -0
  135. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  136. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  137. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  138. package/dist/scripts/effect-tests.js +539 -0
  139. package/dist/scripts/engine-run.js +1 -3
  140. package/dist/scripts/engine-tests.js +16 -10
  141. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  142. package/dist/scripts/evaluation-contract-tests.js +268 -0
  143. package/dist/scripts/extension-distribution-cli.js +39 -0
  144. package/dist/scripts/extension-distribution-tests.js +443 -0
  145. package/dist/scripts/extension-tests.js +988 -0
  146. package/dist/scripts/external-event-tests.js +350 -0
  147. package/dist/scripts/fact-gate-tests.js +869 -0
  148. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  149. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  150. package/dist/scripts/flight-malformed-tests.js +159 -0
  151. package/dist/scripts/flyai-setup-tests.js +228 -0
  152. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  153. package/dist/scripts/flyai-tests.js +692 -0
  154. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  155. package/dist/scripts/fx-contract-tests.js +201 -0
  156. package/dist/scripts/g5-guard-tests.js +150 -0
  157. package/dist/scripts/g5-guard.js +193 -0
  158. package/dist/scripts/gen-lunar-anchors.js +106 -0
  159. package/dist/scripts/geo-atlas-tests.js +283 -0
  160. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  161. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  162. package/dist/scripts/gotry-try-demo.js +49 -0
  163. package/dist/scripts/ground-transfer-tests.js +956 -0
  164. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  165. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  166. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  167. package/dist/scripts/hbcli-tests.js +121 -69
  168. package/dist/scripts/health-watch-cli.js +73 -0
  169. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  170. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  171. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  172. package/dist/scripts/i18n-tests.js +41 -0
  173. package/dist/scripts/incident-tests.js +281 -87
  174. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  175. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  176. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  177. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  178. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  179. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  180. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  181. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  182. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  183. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  184. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  185. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  186. package/dist/scripts/itinerary-deck-tests.js +435 -0
  187. package/dist/scripts/itinerary-html-tests.js +426 -0
  188. package/dist/scripts/journey-tests.js +22 -27
  189. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  190. package/dist/scripts/kernel-manifest-gate.js +126 -0
  191. package/dist/scripts/kernel-manifest-tests.js +212 -0
  192. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  193. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  194. package/dist/scripts/kernel-manifest.js +369 -0
  195. package/dist/scripts/lavish-local-tests.js +1226 -0
  196. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  197. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  198. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  199. package/dist/scripts/ledger-tests.js +382 -246
  200. package/dist/scripts/ledger-workflow-crash.js +10 -20
  201. package/dist/scripts/m3-cohort-tests.js +714 -0
  202. package/dist/scripts/m3-cohort.js +310 -0
  203. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  204. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  205. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  206. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  207. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  208. package/dist/scripts/memory-capture-tests.js +242 -61
  209. package/dist/scripts/memory-decay-tests.js +18 -52
  210. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  211. package/dist/scripts/memory-lifecycle.js +196 -0
  212. package/dist/scripts/memory-metrics.js +24 -12
  213. package/dist/scripts/memory-value-report.js +631 -0
  214. package/dist/scripts/memory-value-tests.js +328 -0
  215. package/dist/scripts/metrics-report-tests.js +151 -0
  216. package/dist/scripts/model-override-e2e.js +137 -0
  217. package/dist/scripts/nightly-evidence-tests.js +203 -0
  218. package/dist/scripts/nightly-evidence.js +303 -0
  219. package/dist/scripts/nudge-digest.js +39 -28
  220. package/dist/scripts/observability-tests.js +381 -0
  221. package/dist/scripts/onboarding-tests.js +161 -0
  222. package/dist/scripts/opensky-check.js +18 -13
  223. package/dist/scripts/opensky-tests.js +28 -28
  224. package/dist/scripts/outcome-projection-tests.js +427 -0
  225. package/dist/scripts/persona-sim-tests.js +603 -0
  226. package/dist/scripts/persona-sim.js +1081 -0
  227. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  228. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  229. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  230. package/dist/scripts/price-drift-tests.js +340 -0
  231. package/dist/scripts/price-drift-watch.js +447 -0
  232. package/dist/scripts/probe-poi-tests.js +53 -45
  233. package/dist/scripts/product-metrics.js +483 -0
  234. package/dist/scripts/publish-preverify.js +110 -0
  235. package/dist/scripts/realtime-pricing-tests.js +76 -0
  236. package/dist/scripts/recall-tests.js +513 -0
  237. package/dist/scripts/replay-async.js +22 -18
  238. package/dist/scripts/replay-real.js +18 -18
  239. package/dist/scripts/replay.js +54 -82
  240. package/dist/scripts/route-provider-conformance-tests.js +391 -0
  241. package/dist/scripts/session-attach-diagnose.js +20 -20
  242. package/dist/scripts/session-attach-poc.js +34 -37
  243. package/dist/scripts/session-benchmark.js +180 -0
  244. package/dist/scripts/session-dida-live-e2e.js +199 -0
  245. package/dist/scripts/session-extract-tests.js +14 -35
  246. package/dist/scripts/session-link-tests.js +478 -0
  247. package/dist/scripts/session-login.js +27 -46
  248. package/dist/scripts/session-tests.js +796 -96
  249. package/dist/scripts/session-zone-crash.js +49 -0
  250. package/dist/scripts/session-zone-ledger-tests.js +432 -0
  251. package/dist/scripts/session-zone-observation-tests.js +492 -0
  252. package/dist/scripts/session-zone-observe.js +135 -0
  253. package/dist/scripts/session-zone-wiring-tests.js +566 -0
  254. package/dist/scripts/session-zones-tests.js +374 -0
  255. package/dist/scripts/sf-live-benchmark.js +367 -0
  256. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  257. package/dist/scripts/sf-live-cli-tests.js +92 -0
  258. package/dist/scripts/sf-live-options.js +46 -0
  259. package/dist/scripts/sf-manifest-tests.js +30 -0
  260. package/dist/scripts/sf-manifest.js +33 -0
  261. package/dist/scripts/sf-soft-score-tests.js +67 -0
  262. package/dist/scripts/sf-summary-tests.js +324 -0
  263. package/dist/scripts/sf-summary.js +525 -0
  264. package/dist/scripts/share-tests.js +432 -0
  265. package/dist/scripts/skeleton-check.js +16 -14
  266. package/dist/scripts/skeleton-integration-test.js +28 -11
  267. package/dist/scripts/skills-contract-tests.js +51 -46
  268. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  269. package/dist/scripts/smoke.js +690 -246
  270. package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
  271. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  272. package/dist/scripts/state-cli-tests.js +226 -101
  273. package/dist/scripts/state-cli.js +565 -198
  274. package/dist/scripts/state-repair-cli.js +66 -0
  275. package/dist/scripts/state-repair-tests.js +140 -0
  276. package/dist/scripts/static-golden-tests.js +165 -0
  277. package/dist/scripts/time-eval-tests.js +212 -192
  278. package/dist/scripts/travel-timeline-tests.js +29 -93
  279. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  280. package/dist/scripts/turn-handoff-collect.js +161 -0
  281. package/dist/scripts/turn-policy-tests.js +55 -0
  282. package/dist/scripts/typed-contract-canary.js +176 -0
  283. package/dist/scripts/tz-resolver-sanity.js +50 -0
  284. package/dist/scripts/tz-resolver-tests.js +584 -0
  285. package/dist/scripts/unified-tests.js +60 -20
  286. package/dist/scripts/visa-policy-tests.js +62 -0
  287. package/dist/scripts/weather-tests.js +276 -41
  288. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  289. package/dist/scripts/write-gate-tests.js +522 -0
  290. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  291. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  292. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  293. package/dist/scripts/z3-race-tests.js +46 -0
  294. package/dist/src/artifact-gate.js +829 -0
  295. package/dist/src/backend/kernel.js +91 -0
  296. package/dist/src/backend/modules/booking-copilot.js +92 -0
  297. package/dist/src/backend/modules/booking-executor.js +124 -0
  298. package/dist/src/backend/modules/session-search.js +551 -0
  299. package/dist/src/benchmark-agent-conformance.js +578 -0
  300. package/dist/src/benchmark-environment-bridge.js +515 -0
  301. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  302. package/dist/src/benchmark-tool-isolation.js +151 -0
  303. package/dist/src/bookable-facts.js +533 -0
  304. package/dist/src/booking-saga.js +97 -0
  305. package/dist/src/booking-surface/availability-policy.js +697 -0
  306. package/dist/src/booking-surface/booking-intent.js +43 -0
  307. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  308. package/dist/src/booking-surface/canonical-schema.js +116 -0
  309. package/dist/src/booking-surface/contracts.js +75 -0
  310. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  311. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  312. package/dist/src/booking-surface/dsh-worker.js +98 -0
  313. package/dist/src/booking-surface/error-codes.js +231 -0
  314. package/dist/src/booking-surface/index.js +9 -0
  315. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  316. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  317. package/dist/src/booking-surface/profile.js +24 -0
  318. package/dist/src/booking-surface/recovery-chain.js +367 -0
  319. package/dist/src/booking-surface/runtime.js +1968 -0
  320. package/dist/src/booking-surface/server.js +674 -0
  321. package/dist/src/booking-surface/startup.js +168 -0
  322. package/dist/src/booking-surface/validation.js +261 -0
  323. package/dist/src/bridge.js +20 -13
  324. package/dist/src/city-scenario-tier.js +378 -0
  325. package/dist/src/companions.js +48 -74
  326. package/dist/src/contracts.js +21 -43
  327. package/dist/src/dsh-llm.js +171 -115
  328. package/dist/src/engine.js +276 -193
  329. package/dist/src/evaluation-cadence.js +97 -0
  330. package/dist/src/evaluation-contracts.js +283 -0
  331. package/dist/src/flight-pack-adapter.js +54 -0
  332. package/dist/src/flight-pack-contract.js +2 -0
  333. package/dist/src/flyai-setup-tool.js +70 -0
  334. package/dist/src/gotry-backend.js +125 -0
  335. package/dist/src/hotel-date-gate.js +118 -0
  336. package/dist/src/i18n.js +88 -0
  337. package/dist/src/index.js +2385 -881
  338. package/dist/src/itinerary-deck.js +139 -0
  339. package/dist/src/itinerary-doc-shared.js +661 -0
  340. package/dist/src/itinerary-html.js +131 -0
  341. package/dist/src/journey.js +155 -108
  342. package/dist/src/lavish-tools.js +400 -0
  343. package/dist/src/ledger-repair-apply.js +538 -0
  344. package/dist/src/ledger-repair-plan.js +463 -0
  345. package/dist/src/loop.js +331 -161
  346. package/dist/src/m3-cohort.js +1577 -0
  347. package/dist/src/memory-capture.js +180 -32
  348. package/dist/src/memory-decay.js +30 -14
  349. package/dist/src/memory-lifecycle.js +1310 -0
  350. package/dist/src/memory-utility.js +27 -40
  351. package/dist/src/mock-llm.js +36 -61
  352. package/dist/src/model.js +81 -36
  353. package/dist/src/outcome-projection.js +746 -0
  354. package/dist/src/realtime-pricing.js +103 -0
  355. package/dist/src/recall/card.js +74 -0
  356. package/dist/src/recall/evaluator.js +140 -0
  357. package/dist/src/recall/integration.js +17 -0
  358. package/dist/src/recall/tick.js +166 -0
  359. package/dist/src/session-link/plan-it-action.js +48 -0
  360. package/dist/src/session-link/session-link.js +248 -0
  361. package/dist/src/session-zone-ledger.js +312 -0
  362. package/dist/src/session-zone-observation.js +354 -0
  363. package/dist/src/session-zone-wiring.js +658 -0
  364. package/dist/src/session-zones.js +510 -0
  365. package/dist/src/share/adapters.js +51 -0
  366. package/dist/src/share/share-consent.js +76 -0
  367. package/dist/src/share/share-deck.js +58 -0
  368. package/dist/src/share/share-token.js +146 -0
  369. package/dist/src/slot-spec.js +84 -106
  370. package/dist/src/state-ledger.js +696 -524
  371. package/dist/src/state-repair.js +299 -0
  372. package/dist/src/subagent-job-id-guard.js +58 -0
  373. package/dist/src/time-anchor.js +231 -33
  374. package/dist/src/tool-packet.js +20 -8
  375. package/dist/src/travel-slots.js +58 -35
  376. package/dist/src/travel-timeline.js +44 -63
  377. package/dist/src/turn-deadline.js +287 -0
  378. package/dist/src/turn-policy.js +118 -0
  379. package/dist/src/tz-resolver.js +143 -0
  380. package/dist/src/unified.js +600 -360
  381. package/dist/src/wish-pool.js +42 -12
  382. package/dist/src/write-gate.js +495 -0
  383. package/dist/src/z3-shared.js +284 -0
  384. package/extension/README.md +21 -0
  385. package/extension/README.zh-CN.md +21 -0
  386. package/extension/background.js +463 -0
  387. package/extension/content-bridge.js +270 -0
  388. package/extension/content-main.js +111 -0
  389. package/extension/manifest.json +60 -0
  390. package/package.json +407 -14
  391. package/schemas/booking.intent.schema.json +193 -0
  392. package/schemas/booking.surface.schema.json +2622 -0
  393. package/ts/capabilities/agent-reach-deep.ts +114 -0
  394. package/ts/capabilities/agent-reach.ts +198 -0
  395. package/ts/capabilities/anything.ts +282 -0
  396. package/ts/capabilities/artifacts.ts +428 -0
  397. package/ts/capabilities/fact-log.ts +54 -0
  398. package/ts/capabilities/flyai-config.ts +388 -0
  399. package/ts/capabilities/flyai.ts +1274 -0
  400. package/ts/capabilities/hbcli.ts +253 -71
  401. package/ts/capabilities/incident-log.ts +88 -59
  402. package/ts/capabilities/itinerary-artifact.ts +214 -0
  403. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  404. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  405. package/ts/capabilities/opensky.ts +121 -0
  406. package/ts/capabilities/session/action-cache.ts +148 -0
  407. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  408. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  409. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  410. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  411. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  412. package/ts/capabilities/session/benchmark.ts +276 -0
  413. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  414. package/ts/capabilities/session/extension-bridge.ts +794 -0
  415. package/ts/capabilities/session/extension-channel.ts +144 -0
  416. package/ts/capabilities/session/extension-distribution.ts +266 -0
  417. package/ts/capabilities/session/extract.ts +54 -0
  418. package/ts/capabilities/session/golden-score.ts +139 -0
  419. package/ts/capabilities/session/health-watch.ts +264 -0
  420. package/ts/capabilities/session/read-guard.ts +141 -0
  421. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  422. package/ts/capabilities/session/transport.ts +146 -0
  423. package/ts/capabilities/session/wizard.ts +140 -0
  424. package/ts/capabilities/session-consent.ts +211 -0
  425. package/ts/capabilities/session-login.ts +208 -0
  426. package/ts/capabilities/session-search.ts +893 -0
  427. package/ts/capabilities/weather.ts +330 -0
  428. package/ts/dsh-runtime/vendor/README.md +68 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  440. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  441. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  442. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  443. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  444. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  445. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  446. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  447. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  448. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  449. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  450. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  451. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  452. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  453. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  454. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  455. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  456. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  457. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  458. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  459. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  460. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  461. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  462. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  463. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  464. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  465. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  466. package/ts/package.json +293 -3
  467. package/ts/scripts/async-collect.ts +91 -0
  468. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  469. package/ts/scripts/skeleton-check.ts +1 -1
  470. package/ts/scripts/skeleton-integration-test.ts +23 -1
  471. package/ts/scripts/state-cli.ts +586 -0
  472. package/ts/scripts/turn-handoff-collect.ts +178 -0
  473. package/ts/src/artifact-gate.ts +991 -0
  474. package/ts/src/benchmark-agent-conformance.ts +619 -0
  475. package/ts/src/benchmark-environment-bridge.ts +538 -0
  476. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  477. package/ts/src/benchmark-tool-isolation.ts +166 -0
  478. package/ts/src/bookable-facts.ts +837 -0
  479. package/ts/src/booking-saga.ts +133 -0
  480. package/ts/src/booking-surface/availability-policy.ts +522 -0
  481. package/ts/src/booking-surface/booking-intent.ts +86 -0
  482. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  483. package/ts/src/booking-surface/canonical-schema.js +116 -0
  484. package/ts/src/booking-surface/contracts.ts +475 -0
  485. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  486. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  487. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  488. package/ts/src/booking-surface/error-codes.ts +241 -0
  489. package/ts/src/booking-surface/index.ts +9 -0
  490. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  491. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  492. package/ts/src/booking-surface/profile.ts +42 -0
  493. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  494. package/ts/src/booking-surface/runtime.ts +1793 -0
  495. package/ts/src/booking-surface/server.ts +733 -0
  496. package/ts/src/booking-surface/startup.ts +201 -0
  497. package/ts/src/booking-surface/validation.ts +217 -0
  498. package/ts/src/companions.ts +113 -0
  499. package/ts/src/contracts.ts +17 -3
  500. package/ts/src/dsh-llm.ts +152 -17
  501. package/ts/src/flyai-setup-tool.ts +77 -0
  502. package/ts/src/index.ts +2123 -200
  503. package/ts/src/itinerary-deck.ts +189 -0
  504. package/ts/src/itinerary-doc-shared.ts +741 -0
  505. package/ts/src/itinerary-html.ts +171 -0
  506. package/ts/src/loop.ts +234 -26
  507. package/ts/src/memory-capture.ts +283 -0
  508. package/ts/src/memory-decay.ts +55 -0
  509. package/ts/src/memory-utility.ts +70 -0
  510. package/ts/src/mock-llm.ts +4 -8
  511. package/ts/src/model.ts +88 -2
  512. package/ts/src/recall/card.ts +106 -0
  513. package/ts/src/recall/evaluator.ts +170 -0
  514. package/ts/src/recall/integration.ts +35 -0
  515. package/ts/src/recall/tick.ts +213 -0
  516. package/ts/src/session-link/plan-it-action.ts +78 -0
  517. package/ts/src/session-link/session-link.ts +275 -0
  518. package/ts/src/share/adapters.ts +93 -0
  519. package/ts/src/share/share-consent.ts +105 -0
  520. package/ts/src/share/share-deck.ts +85 -0
  521. package/ts/src/share/share-token.ts +166 -0
  522. package/ts/src/slot-spec.ts +172 -0
  523. package/ts/src/state-ledger.ts +1166 -0
  524. package/ts/src/subagent-job-id-guard.ts +72 -0
  525. package/ts/src/time-anchor.ts +338 -0
  526. package/ts/src/tool-packet.ts +32 -0
  527. package/ts/src/travel-slots.ts +219 -0
  528. package/ts/src/travel-timeline.ts +83 -0
  529. package/ts/src/turn-deadline.ts +366 -0
  530. package/ts/src/turn-policy.ts +154 -0
  531. package/ts/src/unified.ts +444 -105
  532. package/ts/src/wish-pool.ts +79 -0
  533. package/dist/scripts/session-attach-wait.js +0 -41
@@ -0,0 +1,1076 @@
1
+ /**
2
+ * Simulated-trigger drill for the dormant D-15 multi-user / multi-writer
3
+ * tracker (issue #275, docs/architecture.md row D-15 and section 10).
4
+ *
5
+ * HONESTY CONTRACT — read before trusting any line of output:
6
+ * Every result below is `simulated_trigger_drill` / `synthetic`. N child
7
+ * processes on an mkdtemp root are NOT a second real user, NOT a multi-machine
8
+ * deployment and NOT an approved AaaS initiative. Issue #275's own acceptance
9
+ * text says "fixtures do not count as production rollout evidence", so nothing
10
+ * here satisfies the trigger: tracking issue #275 was closed on 2026-10-05 as
11
+ * DEFERRED (not completed — open a new issue when the trigger appears), and
12
+ * this file enables no replication and no multi-writer path. What the drill proves is that the
13
+ * machinery that already exists holds its SAFETY invariants when a
14
+ * multi-writer trigger is simulated, and it pins exactly where the
15
+ * unadmitted path degrades.
16
+ *
17
+ * Sections:
18
+ * A0 cold-open race: N processes opening a FRESH ledger simultaneously.
19
+ * This is where the drill found defect D15-1. The founder decided to fix
20
+ * it on 2026-10-05, so the expectation here is FLIPPED: every opener must
21
+ * now succeed, and the only admitted failure is the typed
22
+ * `LedgerBusyError` (`code: 'GOTRY_LEDGER_BUSY'`). A raw `SQLITE_BUSY*`
23
+ * out of `openDb` is a REGRESSION, not a host-timing fact.
24
+ * A0b deterministic D15-1 regression: a start-gunned child opens a fresh
25
+ * ledger exactly while another process holds a write lock in
26
+ * rollback-journal mode, so the child's `journal_mode = WAL` switch must
27
+ * wait. Before the fix `busy_timeout` was installed AFTER that switch and
28
+ * the child threw a raw SQLITE_BUSY with no retry; now it waits and wins.
29
+ * A0c deterministic exhaustion: the same race with the lock never released
30
+ * inside the open budget. The open must fail with the TYPED error — this
31
+ * is the one admitted failure shape, and it is asserted, not observed.
32
+ * A N processes appending concurrently to ONE tenant and to TWO tenants:
33
+ * idem-key dedupe, no lost updates, tenant isolation, integrity_check
34
+ * B SIGKILL at named crash points inside one transaction, plus
35
+ * timing-varied kills on the real product write path -> reopen ->
36
+ * all-or-nothing and fold integrity
37
+ * C stale-claim rejection with fencing tokens under N-process contention
38
+ * (extends the 2-process race already covered in write-gate-tests)
39
+ * D SQLite online backup taken under live write load -> restore -> the
40
+ * restored fold equals the source prefix at the snapshot point
41
+ * E Litestream / cr-sqlite absence pinned as "needs real trigger +
42
+ * dependency decision", so no document can claim them
43
+ *
44
+ * Kernel discipline: `src/state-ledger.ts` is read-only here. Crash points are
45
+ * injected through the ledger's PUBLIC surface (`ledger.db.transaction` +
46
+ * `insertEvent`), never by editing the module.
47
+ *
48
+ * Child processes are plain `node` processes running `.mts` workers (Node's
49
+ * native type stripping), deliberately NOT tsx: a tsx wrapper is a separate
50
+ * parent process, so killing it would leave the real worker running and the
51
+ * drill would both leak and measure a moving target.
52
+ *
53
+ * Run: cd ts && npx tsx scripts/drill-multiuser-ledger-tests.ts
54
+ */
55
+ import Database from 'better-sqlite3';
56
+ import { spawn } from 'node:child_process';
57
+ import { createHash } from 'node:crypto';
58
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
59
+ import { tmpdir } from 'node:os';
60
+ import { join, resolve } from 'node:path';
61
+ import { makeWishId, openDb } from '../src/state-ledger.js';
62
+ import { WriteGate } from '../src/write-gate.js';
63
+ const DRILL_LABEL = 'simulated_trigger_drill';
64
+ const TS_DIR = resolve(import.meta.dirname, '..');
65
+ const TSX_CLI = join(TS_DIR, 'node_modules', 'tsx', 'dist', 'cli.mjs');
66
+ const STATE_LEDGER_URL = `file://${join(TS_DIR, 'src', 'state-ledger.ts')}`;
67
+ const WRITE_GATE_URL = `file://${join(TS_DIR, 'src', 'write-gate.ts')}`;
68
+ const SUITE_BUDGET_MS = 180_000;
69
+ const CHILD_BUDGET_MS = 10_000;
70
+ let pass = 0;
71
+ const failures = [];
72
+ const observations = [];
73
+ const children = new Set();
74
+ /** Every pid ever spawned, plus its process group when detached. Never cleared. */
75
+ const trackedPids = new Set();
76
+ const trackedGroups = new Set();
77
+ /**
78
+ * Kill every live child, every detached process group, and every pid ever
79
+ * spawned. Safe to call repeatedly, and callable from the deadline path (where
80
+ * `process.exit` would otherwise skip `finally` and strand the two workers that
81
+ * loop forever by design).
82
+ */
83
+ function reapAll() {
84
+ for (const groupPid of trackedGroups) {
85
+ try {
86
+ process.kill(-groupPid, 'SIGKILL');
87
+ }
88
+ catch { /* group already gone */ }
89
+ }
90
+ for (const child of children) {
91
+ try {
92
+ child.kill('SIGKILL');
93
+ }
94
+ catch { /* already gone */ }
95
+ }
96
+ children.clear();
97
+ for (const pid of trackedPids) {
98
+ try {
99
+ process.kill(pid, 'SIGKILL');
100
+ }
101
+ catch { /* already gone */ }
102
+ }
103
+ }
104
+ function ok(condition, message) {
105
+ if (condition) {
106
+ pass += 1;
107
+ return;
108
+ }
109
+ failures.push(message);
110
+ console.error(` FAIL - ${message}`);
111
+ }
112
+ function eq(actual, expected, message) {
113
+ ok(actual === expected, `${message} (actual=${JSON.stringify(actual)} expected=${JSON.stringify(expected)})`);
114
+ }
115
+ function observe(line) {
116
+ observations.push(line);
117
+ console.log(` observed: ${line}`);
118
+ }
119
+ /**
120
+ * Spawn one bounded worker.
121
+ *
122
+ * `runner: 'node'` (default) is a DIRECT child, so `child.kill` really kills the
123
+ * worker and the exit signal is observable — required by every arm that kills.
124
+ * `runner: 'tsx'` is needed only for modules Node's native type stripping
125
+ * refuses (`src/write-gate.ts` uses a constructor parameter property, which is
126
+ * `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`); tsx inserts its own process, so those
127
+ * children get their own process group and are reaped by group signal.
128
+ */
129
+ function startChild(scriptPath, env, runner = 'node') {
130
+ const detached = runner === 'tsx' && process.platform !== 'win32';
131
+ const child = spawn(process.execPath, runner === 'tsx' ? [TSX_CLI, scriptPath] : [scriptPath], {
132
+ cwd: TS_DIR,
133
+ env: { ...process.env, ...env },
134
+ stdio: ['ignore', 'pipe', 'pipe'],
135
+ detached,
136
+ });
137
+ const groupPid = detached ? child.pid ?? null : null;
138
+ const reap = () => {
139
+ if (groupPid !== null) {
140
+ try {
141
+ process.kill(-groupPid, 'SIGKILL');
142
+ return;
143
+ }
144
+ catch { /* group already gone */ }
145
+ }
146
+ try {
147
+ child.kill('SIGKILL');
148
+ }
149
+ catch { /* already gone */ }
150
+ };
151
+ children.add(child);
152
+ if (child.pid)
153
+ trackedPids.add(child.pid);
154
+ if (groupPid !== null)
155
+ trackedGroups.add(groupPid);
156
+ const stdout = [];
157
+ const stderr = [];
158
+ child.stdout?.on('data', (c) => stdout.push(c));
159
+ child.stderr?.on('data', (c) => stderr.push(c));
160
+ const done = new Promise((resolveRun) => {
161
+ let settled = false;
162
+ const timer = setTimeout(() => {
163
+ if (settled)
164
+ return;
165
+ reap();
166
+ settled = true;
167
+ resolveRun({ exit: null, signal: 'SIGKILL', stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString(), timedOut: true });
168
+ }, CHILD_BUDGET_MS);
169
+ const finish = (exit, signal) => {
170
+ if (settled)
171
+ return;
172
+ settled = true;
173
+ clearTimeout(timer);
174
+ resolveRun({ exit, signal, stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString(), timedOut: false });
175
+ };
176
+ child.once('error', (error) => { stderr.push(Buffer.from(`spawn_error:${error.message}`)); finish(null, null); });
177
+ child.once('close', (exit, signal) => { children.delete(child); finish(exit, signal); });
178
+ });
179
+ return { child, done, reap };
180
+ }
181
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
182
+ /** Wait for a readiness marker, bailing out early if a watched worker died. */
183
+ async function waitForReady(path, budgetMs, watch) {
184
+ const deadline = Date.now() + budgetMs;
185
+ while (Date.now() < deadline) {
186
+ if (existsSync(path))
187
+ return true;
188
+ if (watch.some((c) => c.exitCode !== null || c.signalCode !== null))
189
+ return existsSync(path);
190
+ await sleep(5);
191
+ }
192
+ return existsSync(path);
193
+ }
194
+ function lastJsonLine(run) {
195
+ const line = run.stdout.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('{')).at(-1);
196
+ return line ? JSON.parse(line) : null;
197
+ }
198
+ /** Direct SQLite inspection, independent of the module-level ledger cache. */
199
+ function inspect(dbPath) {
200
+ const db = new Database(dbPath, { readonly: true });
201
+ try {
202
+ const integrity = db.pragma('integrity_check', { simple: true });
203
+ const events = db.prepare('SELECT COUNT(*) AS n FROM events').get().n;
204
+ const wishItems = db.prepare("SELECT COUNT(*) AS n FROM projection_items WHERE subject = 'wish_pool'").get().n;
205
+ const duplicateIdem = db.prepare(`
206
+ SELECT COUNT(*) AS n FROM (
207
+ SELECT tenant_id, idem_key, COUNT(*) AS c FROM events WHERE idem_key IS NOT NULL
208
+ GROUP BY tenant_id, idem_key HAVING c > 1
209
+ )`).get().n;
210
+ const distinctIdemAppend = db.prepare("SELECT COUNT(DISTINCT tenant_id || char(31) || idem_key) AS n FROM events WHERE kind = 'drill.append' AND idem_key IS NOT NULL").get().n;
211
+ const seqs = db.prepare('SELECT seq FROM events ORDER BY seq').all().map((r) => r.seq);
212
+ const perTenant = {};
213
+ for (const row of db.prepare("SELECT tenant_id, COUNT(*) AS n FROM events WHERE kind = 'drill.append' GROUP BY tenant_id").all()) {
214
+ perTenant[row.tenant_id] = row.n;
215
+ }
216
+ return { integrity, events, wishItems, duplicateIdem, distinctIdemAppend, seqs, perTenant };
217
+ }
218
+ finally {
219
+ db.close();
220
+ }
221
+ }
222
+ /**
223
+ * Fold integrity on an isolated copy: rebuild must reproduce the direct read.
224
+ *
225
+ * The copy MUST include the `-wal` and `-shm` sidecars. The ledger runs in WAL
226
+ * mode and the victims here are SIGKILLed, so nothing checkpoints: copying only
227
+ * `gotry-state.db` yields a file with no committed rows, and `before === after`
228
+ * would then compare empty to empty and pass vacuously. The returned `events`
229
+ * count lets every caller prove the copy actually saw the same rows the
230
+ * in-place `inspect()` saw.
231
+ */
232
+ function foldMatchesDirectRead(sourceDbPath, tenant) {
233
+ const scratch = realpathSync(mkdtempSync(join(tmpdir(), 'gotry-drill-fold-')));
234
+ mkdirSync(join(scratch, 'gotry-state'), { recursive: true });
235
+ const destDbPath = join(scratch, 'gotry-state', 'gotry-state.db');
236
+ copyFileSync(sourceDbPath, destDbPath);
237
+ for (const sidecar of ['-wal', '-shm']) {
238
+ if (existsSync(sourceDbPath + sidecar))
239
+ copyFileSync(sourceDbPath + sidecar, destDbPath + sidecar);
240
+ }
241
+ const ledger = openDb(scratch, tenant);
242
+ try {
243
+ const events = ledger.db.prepare('SELECT COUNT(*) AS n FROM events').get();
244
+ const before = JSON.stringify({ wishes: ledger.readWishPool(), motivation: ledger.readMotivation() });
245
+ ledger.rebuildProjections();
246
+ const after = JSON.stringify({ wishes: ledger.readWishPool(), motivation: ledger.readMotivation() });
247
+ return { match: before === after, events: events.n };
248
+ }
249
+ finally {
250
+ ledger.close();
251
+ rmSync(scratch, { recursive: true, force: true });
252
+ }
253
+ }
254
+ /** Create the ledger file once from the parent, so cold-open races stay in A0. */
255
+ function seedLedgerFile(stateRoot, tenants) {
256
+ for (const tenant of tenants) {
257
+ const ledger = openDb(stateRoot, tenant);
258
+ ledger.close();
259
+ }
260
+ }
261
+ // ---- child fixture sources (plain node `.mts`, no tsx wrapper) --------------
262
+ /**
263
+ * A0 worker: open a fresh ledger as soon as the module graph is loaded, with no
264
+ * start gun. The natural jitter of N simultaneous `better-sqlite3` imports is
265
+ * what clusters the processes on the very first open, which is the window the
266
+ * race lives in; a file-based start gun lets the first winner finish the WAL
267
+ * switch before the others arrive and the race stops reproducing.
268
+ */
269
+ const COLD_OPEN_WORKER = `
270
+ const { openDb } = await import(process.env.STATE_LEDGER_URL)
271
+ let outcome
272
+ try {
273
+ const ledger = openDb(process.env.STATE_ROOT, process.env.TENANT)
274
+ ledger.insertEvent({ actor: 'drill:cold', kind: 'drill.cold', payload: {}, idemKey: 'cold:' + process.env.WORKER_ID })
275
+ ledger.close()
276
+ outcome = { ok: true }
277
+ } catch (error) {
278
+ const frames = String(error?.stack ?? '').split('\\n').map((l) => l.trim())
279
+ const ledgerFrame = frames.find((l) => l.includes('state-ledger.ts')) ?? ''
280
+ outcome = { ok: false, code: error?.code ?? 'unknown', where: (frames[1] ?? '').replace(/^at /, '').split('/').pop() + ' <- ' + ledgerFrame.replace(/^at /, '').split('/').pop() }
281
+ }
282
+ console.log(JSON.stringify({ workerId: process.env.WORKER_ID, outcome }))
283
+ `;
284
+ /**
285
+ * A0b/A0c worker: start-gunned cold open against a ledger whose write lock is
286
+ * held by the parent. Deterministic by construction — the child reports ready
287
+ * BEFORE the parent takes the lock, and only opens after the start gun, so the
288
+ * open is guaranteed to land inside the contended window.
289
+ *
290
+ * It reports the error SHAPE, which is the whole point of the D15-1 flip: a
291
+ * typed `LedgerBusyError` (name + `code: 'GOTRY_LEDGER_BUSY'`) is admitted, a
292
+ * raw SQLite code out of `openDb` is not.
293
+ */
294
+ const BLOCKED_OPEN_WORKER = `
295
+ import { writeFileSync, existsSync } from 'node:fs'
296
+ const { openDb } = await import(process.env.STATE_LEDGER_URL)
297
+
298
+ writeFileSync(process.env.READY_PATH, 'ready')
299
+ while (!existsSync(process.env.GO_PATH)) await new Promise((r) => setTimeout(r, 2))
300
+
301
+ const started = Date.now()
302
+ let outcome
303
+ try {
304
+ const ledger = openDb(process.env.STATE_ROOT, 'local')
305
+ ledger.insertEvent({ actor: 'drill:blocked', kind: 'drill.blocked', payload: {}, idemKey: 'blocked:open' })
306
+ const journal = String(ledger.db.pragma('journal_mode', { simple: true }) ?? '')
307
+ ledger.close()
308
+ outcome = { ok: true, journal }
309
+ } catch (error) {
310
+ outcome = {
311
+ ok: false,
312
+ name: error?.name ?? 'unknown',
313
+ code: error?.code ?? 'unknown',
314
+ sqliteCode: error?.sqliteCode ?? null,
315
+ attempts: error?.attempts ?? null,
316
+ }
317
+ }
318
+ console.log(JSON.stringify({ outcome, elapsedMs: Date.now() - started }))
319
+ `;
320
+ /**
321
+ * A worker. `MODE=append` hammers the append-only event face with an
322
+ * overlapping idem-key set; `MODE=wish` hammers the read-modify-write product
323
+ * path with one shared wish name, retrying a bounded number of times on SQLite
324
+ * contention codes the way a real multi-writer client would have to.
325
+ */
326
+ const APPEND_WORKER = `
327
+ import { writeFileSync, existsSync } from 'node:fs'
328
+ const { openDb } = await import(process.env.STATE_LEDGER_URL)
329
+
330
+ const tenant = process.env.TENANT
331
+ const workerId = process.env.WORKER_ID
332
+ const ledger = openDb(process.env.STATE_ROOT, tenant)
333
+ ledger.db.pragma('busy_timeout = ' + process.env.BUSY_TIMEOUT_MS)
334
+
335
+ writeFileSync(process.env.READY_PATH, 'ready')
336
+ while (!existsSync(process.env.GO_PATH)) await new Promise((r) => setTimeout(r, 2))
337
+
338
+ const outcomes = { inserted: 0, deduped: 0, added: 0, updated: 0, retries: 0, errors: {} }
339
+ const note = (error) => {
340
+ const code = error?.code ?? error?.constructor?.name ?? 'unknown'
341
+ outcomes.errors[code] = (outcomes.errors[code] ?? 0) + 1
342
+ }
343
+ const contention = (error) => error?.code === 'SQLITE_BUSY' || error?.code === 'SQLITE_BUSY_SNAPSHOT'
344
+
345
+ if (process.env.MODE === 'append') {
346
+ for (const key of JSON.parse(process.env.IDEM_KEYS)) {
347
+ try {
348
+ const seq = ledger.insertEvent({ actor: 'drill:' + workerId, kind: 'drill.append', subjectId: key, payload: { worker: workerId, key }, idemKey: key })
349
+ if (seq === null) outcomes.deduped += 1
350
+ else outcomes.inserted += 1
351
+ } catch (error) { note(error) }
352
+ }
353
+ } else {
354
+ const MAX_ATTEMPTS = 12
355
+ for (let round = 0; round < Number(process.env.WISH_ROUNDS); round += 1) {
356
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
357
+ try {
358
+ const result = ledger.appendWish({ name: process.env.WISH_NAME, reason: 'drill ' + workerId, conditions: { month: '2026-11', worker: workerId } })
359
+ if (result.added) outcomes.added += 1
360
+ else outcomes.updated += 1
361
+ break
362
+ } catch (error) {
363
+ if (contention(error) && attempt < MAX_ATTEMPTS - 1) {
364
+ outcomes.retries += 1
365
+ await new Promise((r) => setTimeout(r, 5 + attempt * 10))
366
+ continue
367
+ }
368
+ note(error)
369
+ break
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ ledger.close()
376
+ console.log(JSON.stringify({ workerId, tenant, mode: process.env.MODE, outcomes }))
377
+ `;
378
+ /** B worker: named crash points inside one transaction via the public surface. */
379
+ const CRASH_WORKER = `
380
+ import { writeFileSync } from 'node:fs'
381
+ const { openDb } = await import(process.env.STATE_LEDGER_URL)
382
+
383
+ const point = process.env.CRASH_POINT
384
+ const ledger = openDb(process.env.STATE_ROOT, 'local')
385
+ const die = () => { writeFileSync(process.env.MARKER_PATH, point); process.kill(process.pid, 'SIGKILL') }
386
+
387
+ if (point === 'before-transaction') die()
388
+
389
+ const run = ledger.db.transaction(() => {
390
+ ledger.insertEvent({
391
+ actor: 'drill:crash',
392
+ kind: 'wish.added',
393
+ subjectId: 'w-drill',
394
+ payload: { wish: { wish_id: 'w-drill', name: 'drill wish', conditions: { month: '2026-11' } } },
395
+ idemKey: 'drill:crash:wish',
396
+ })
397
+ if (point === 'after-event-insert') die()
398
+ ledger.db.prepare("INSERT INTO projection_items (tenant_id, subject, item_id, doc, ord) VALUES (?, 'wish_pool', ?, ?, 0)")
399
+ .run('local', 'w-drill', JSON.stringify({ wish_id: 'w-drill', name: 'drill wish', conditions: { month: '2026-11' } }))
400
+ if (point === 'after-projection-write') die()
401
+ })
402
+ run()
403
+ if (point === 'after-commit') die()
404
+ ledger.close()
405
+ console.log(JSON.stringify({ point, completed: true }))
406
+ `;
407
+ /** B worker: the real product write path, killed by the parent mid-flight. */
408
+ const PRODUCT_WRITE_WORKER = `
409
+ import { writeFileSync } from 'node:fs'
410
+ const { openDb } = await import(process.env.STATE_LEDGER_URL)
411
+ const ledger = openDb(process.env.STATE_ROOT, 'local')
412
+ writeFileSync(process.env.READY_PATH, 'ready')
413
+ for (let n = 0; ; n += 1) {
414
+ try {
415
+ ledger.appendWish({ name: 'drill wish ' + n, reason: 'product path', conditions: { month: '2026-11', n } })
416
+ ledger.appendMotivationPatch({ weights: { comfort: 0.5 }, evidence: ['drill evidence ' + n] })
417
+ } catch { /* the parent kills this worker mid-flight on purpose */ }
418
+ await new Promise((r) => setTimeout(r, 1))
419
+ }
420
+ `;
421
+ /** C worker: one of N dispatchers racing for a single queued outbox intent. */
422
+ const CLAIM_WORKER = `
423
+ import { writeFileSync, existsSync } from 'node:fs'
424
+ const { openDb } = await import(process.env.STATE_LEDGER_URL)
425
+ const { WriteGate } = await import(process.env.WRITE_GATE_URL)
426
+
427
+ const ledger = openDb(process.env.STATE_ROOT, 'local')
428
+ ledger.db.pragma('busy_timeout = 8000')
429
+ const gate = new WriteGate(ledger)
430
+ writeFileSync(process.env.READY_PATH, 'ready')
431
+ while (!existsSync(process.env.GO_PATH)) await new Promise((r) => setTimeout(r, 2))
432
+
433
+ let verdict
434
+ try {
435
+ verdict = gate.claimForDispatch({ idemKey: process.env.IDEM_KEY, claimedBy: 'drill-worker-' + process.env.WORKER_ID, leaseUntil: process.env.LEASE_UNTIL })
436
+ } catch (error) {
437
+ verdict = { ok: false, reason: 'threw:' + (error?.code ?? error?.message ?? 'unknown') }
438
+ }
439
+ ledger.close()
440
+ console.log(JSON.stringify({ workerId: process.env.WORKER_ID, verdict }))
441
+ `;
442
+ /** D worker: continuous write load while the parent takes an online backup. */
443
+ const LOAD_WORKER = `
444
+ import { writeFileSync } from 'node:fs'
445
+ const { openDb } = await import(process.env.STATE_LEDGER_URL)
446
+ const ledger = openDb(process.env.STATE_ROOT, 'local')
447
+ ledger.db.pragma('busy_timeout = 8000')
448
+ writeFileSync(process.env.READY_PATH, 'ready')
449
+ for (let n = 0; ; n += 1) {
450
+ try {
451
+ ledger.insertEvent({ actor: 'drill:load', kind: 'drill.load', subjectId: 'load', payload: { n }, idemKey: 'load:' + n })
452
+ } catch { /* contention is the point; the parent asserts safety */ }
453
+ if (n % 32 === 0) await new Promise((r) => setTimeout(r, 1))
454
+ }
455
+ `;
456
+ // The suite-level bound is a generous diagnostic backstop, not a performance
457
+ // assertion: every child already carries its own 10s bound, and this suite runs
458
+ // roughly 50 short-lived children. It is sized so a slow shared CI runner
459
+ // cannot turn a dormant-path drill red. `process.exit` skips `finally`, so the
460
+ // deadline path must reap and clean up itself — otherwise it would strand the
461
+ // product-write and load workers, which loop forever by design.
462
+ const suiteDeadline = setTimeout(() => {
463
+ console.error(`DRILL BUDGET EXCEEDED (${SUITE_BUDGET_MS}ms) — reaping before exit`);
464
+ try {
465
+ reapAll();
466
+ }
467
+ catch { /* best effort */ }
468
+ try {
469
+ rmSync(workRoot, { recursive: true, force: true });
470
+ }
471
+ catch { /* best effort */ }
472
+ process.exit(1);
473
+ }, SUITE_BUDGET_MS);
474
+ suiteDeadline.unref();
475
+ const workRoot = realpathSync(mkdtempSync(join(tmpdir(), 'gotry-drill-d15-')));
476
+ try {
477
+ const coldOpenWorker = join(workRoot, 'cold-open-worker.mts');
478
+ const blockedOpenWorker = join(workRoot, 'blocked-open-worker.mts');
479
+ const appendWorker = join(workRoot, 'append-worker.mts');
480
+ const crashWorker = join(workRoot, 'crash-worker.mts');
481
+ const productWorker = join(workRoot, 'product-write-worker.mts');
482
+ const claimWorker = join(workRoot, 'claim-worker.mts');
483
+ const loadWorker = join(workRoot, 'load-worker.mts');
484
+ writeFileSync(coldOpenWorker, COLD_OPEN_WORKER);
485
+ writeFileSync(blockedOpenWorker, BLOCKED_OPEN_WORKER);
486
+ writeFileSync(appendWorker, APPEND_WORKER);
487
+ writeFileSync(crashWorker, CRASH_WORKER);
488
+ writeFileSync(productWorker, PRODUCT_WRITE_WORKER);
489
+ writeFileSync(claimWorker, CLAIM_WORKER);
490
+ writeFileSync(loadWorker, LOAD_WORKER);
491
+ // ---- A0: cold-open race on a fresh ledger -------------------------------
492
+ console.log(`-- A0 cold-open race: N processes creating the same fresh ledger [${DRILL_LABEL}]`);
493
+ {
494
+ const WORKERS = 6;
495
+ const ROUNDS = 3;
496
+ let totalWorkers = 0;
497
+ let totalSucceeded = 0;
498
+ const allCodes = [];
499
+ const callsites = new Set();
500
+ for (let round = 0; round < ROUNDS; round += 1) {
501
+ const stateRoot = realpathSync(mkdtempSync(join(workRoot, `cold-open-${round}-`)));
502
+ const started = [];
503
+ try {
504
+ for (let w = 0; w < WORKERS; w += 1) {
505
+ started.push(startChild(coldOpenWorker, {
506
+ STATE_LEDGER_URL,
507
+ STATE_ROOT: stateRoot,
508
+ TENANT: 'local',
509
+ WORKER_ID: String(w),
510
+ }));
511
+ }
512
+ const runs = await Promise.all(started.map((s) => s.done));
513
+ const reports = runs.map((r) => lastJsonLine(r));
514
+ ok(reports.every((r) => r !== null), `A0-1[r${round}] every cold-open worker reported (stderr=${runs.map((r) => r.stderr.slice(-120)).join('|')})`);
515
+ const succeeded = reports.filter((r) => r?.outcome.ok === true).length;
516
+ const failed = reports.filter((r) => r?.outcome.ok === false);
517
+ totalWorkers += WORKERS;
518
+ totalSucceeded += succeeded;
519
+ for (const report of failed) {
520
+ if (report?.outcome.code)
521
+ allCodes.push(report.outcome.code);
522
+ if (report?.outcome.where)
523
+ callsites.add(report.outcome.where);
524
+ }
525
+ // SAFETY must hold whatever the liveness outcome is.
526
+ const dbPath = join(stateRoot, 'gotry-state', 'gotry-state.db');
527
+ const state = inspect(dbPath);
528
+ eq(state.integrity, 'ok', `A0-2[r${round}] SAFETY: the ledger file is intact after a cold-open race`);
529
+ eq(state.duplicateIdem, 0, `A0-3[r${round}] SAFETY: no duplicated idem key survived the race`);
530
+ const a0Fold = foldMatchesDirectRead(dbPath, 'local');
531
+ eq(a0Fold.match, true, `A0-4[r${round}] SAFETY: fold rebuild equals the direct read after the race`);
532
+ eq(a0Fold.events, state.events, `A0-4b[r${round}] the fold copy saw the same ${state.events} events as the in-place read (not a vacuous empty-vs-empty comparison)`);
533
+ ok(succeeded >= 1, `A0-5[r${round}] at least one process created the ledger (${succeeded}/${WORKERS})`);
534
+ }
535
+ finally {
536
+ for (const s of started)
537
+ s.reap();
538
+ }
539
+ }
540
+ // FLIPPED 2026-10-05 (D15-1 fixed, founder decision). Before the fix this
541
+ // block only observed the defect, because WHICH process lost the cold-open
542
+ // race was host-timing dependent (0 to 9 of 18 across local runs) and a raw
543
+ // SQLITE_BUSY was the recorded baseline. `openDb` now installs
544
+ // `busy_timeout` before anything contendable, retries the WAL switch and the
545
+ // schema transaction under a wall-clock budget, and takes the schema lock
546
+ // with BEGIN IMMEDIATE, so a lost race is no longer an outcome: it is either
547
+ // a successful open or, if the whole budget is exhausted, the TYPED
548
+ // `LedgerBusyError`. Both halves below are therefore GATED.
549
+ //
550
+ // IF THIS FLIPS BACK (a raw SQLITE_BUSY* reappears out of openDb), the fix
551
+ // has regressed — do not re-soften this to an observation. The ordering
552
+ // (`busy_timeout` first) and the IMMEDIATE schema transaction in
553
+ // `src/state-ledger.ts openDb` are what make it hold.
554
+ const distinctCodes = [...new Set(allCodes)].sort();
555
+ observe(`A0 cold-open race, ${ROUNDS} rounds x ${WORKERS} processes on a fresh root: ${totalSucceeded}/${totalWorkers} opened successfully, ${allCodes.length} raised ${JSON.stringify(distinctCodes)} out of openDb`);
556
+ eq(totalSucceeded, totalWorkers, `A0-6 every cold-open process opened the fresh ledger (${totalSucceeded}/${totalWorkers}); failures=${JSON.stringify(distinctCodes)} callsites=${JSON.stringify([...callsites])}`);
557
+ ok(distinctCodes.every((c) => String(c) === 'GOTRY_LEDGER_BUSY'), `A0-7 the only admitted cold-open failure is the typed LedgerBusyError (observed ${JSON.stringify(distinctCodes)}). A raw SQLITE_BUSY/SQLITE_BUSY_SNAPSHOT here is the D15-1 regression, not a timing difference.`);
558
+ }
559
+ // ---- A0b/A0c: deterministic D15-1 regression, no timing luck ------------
560
+ console.log('-- A0b/A0c deterministic cold open against a held write lock');
561
+ {
562
+ /**
563
+ * Run one start-gunned cold open while the parent holds a write lock.
564
+ *
565
+ * The ledger file is created here in ROLLBACK-JOURNAL mode on purpose: that
566
+ * forces the child's `openDb` to perform the `journal_mode = WAL` switch,
567
+ * which needs an exclusive lock, which is exactly the statement that threw
568
+ * a raw SQLITE_BUSY before the fix (it ran before `busy_timeout` existed).
569
+ * `holdMs = null` means "never release inside the open budget".
570
+ */
571
+ const openUnderHeldLock = async (label, holdMs) => {
572
+ const stateRoot = realpathSync(mkdtempSync(join(workRoot, `held-lock-${label}-`)));
573
+ mkdirSync(join(stateRoot, 'gotry-state'), { recursive: true });
574
+ const dbPath = join(stateRoot, 'gotry-state', 'gotry-state.db');
575
+ const holder = new Database(dbPath);
576
+ holder.pragma('journal_mode = delete');
577
+ holder.exec('CREATE TABLE IF NOT EXISTS drill_lock_holder (x)');
578
+ const readyPath = join(stateRoot, 'ready');
579
+ const goPath = join(stateRoot, 'go');
580
+ const started = startChild(blockedOpenWorker, { STATE_LEDGER_URL, STATE_ROOT: stateRoot, READY_PATH: readyPath, GO_PATH: goPath });
581
+ let locked = false;
582
+ try {
583
+ ok(await waitForReady(readyPath, 9_000, [started.child]), `${label} the opener reached the start gun before the lock was taken`);
584
+ holder.exec('BEGIN IMMEDIATE');
585
+ locked = true;
586
+ writeFileSync(goPath, 'go');
587
+ if (holdMs !== null) {
588
+ await sleep(holdMs);
589
+ holder.exec('ROLLBACK');
590
+ locked = false;
591
+ }
592
+ const run = await started.done;
593
+ eq(run.timedOut, false, `${label} the opener finished inside its bound (stderr=${run.stderr.slice(-200)})`);
594
+ return lastJsonLine(run);
595
+ }
596
+ finally {
597
+ started.reap();
598
+ if (locked) {
599
+ try {
600
+ holder.exec('ROLLBACK');
601
+ }
602
+ catch { /* already rolled back */ }
603
+ }
604
+ holder.close();
605
+ }
606
+ };
607
+ // A0b: the lock is released well inside the open budget -> the open must win.
608
+ const released = await openUnderHeldLock('A0b', 400);
609
+ ok(released !== null, 'A0b the opener reported an outcome');
610
+ eq(released?.outcome.ok, true, `A0b SAFETY+LIVENESS a cold open that starts while another process holds the write lock now waits and succeeds (outcome=${JSON.stringify(released?.outcome)}). Before the fix this threw a raw SQLITE_BUSY out of pragma('journal_mode = WAL') because busy_timeout was installed after it.`);
611
+ eq(String(released?.outcome.journal ?? '').toLowerCase(), 'wal', 'A0b the opener really completed the WAL switch it had to wait for');
612
+ // A0c: the lock is never released -> the ONE admitted failure shape.
613
+ const exhausted = await openUnderHeldLock('A0c', null);
614
+ ok(exhausted !== null, 'A0c the opener reported an outcome');
615
+ eq(exhausted?.outcome.ok, false, `A0c with the write lock never released, the open must not claim success (outcome=${JSON.stringify(exhausted?.outcome)})`);
616
+ eq(exhausted?.outcome.name, 'LedgerBusyError', `A0c exhaustion surfaces the TYPED error, not a raw SQLite error (name=${JSON.stringify(exhausted?.outcome.name)})`);
617
+ eq(exhausted?.outcome.code, 'GOTRY_LEDGER_BUSY', `A0c the typed error carries the stable code (code=${JSON.stringify(exhausted?.outcome.code)})`);
618
+ ok(typeof exhausted?.outcome.sqliteCode === 'string' && /^SQLITE_(BUSY|PROTOCOL)/.test(exhausted.outcome.sqliteCode), `A0c the typed error keeps the underlying SQLite code for diagnosis (sqliteCode=${JSON.stringify(exhausted?.outcome.sqliteCode)})`);
619
+ observe(`A0b/A0c deterministic held-lock opens: released-at-400ms open succeeded in ${released?.elapsedMs}ms; never-released open gave up with the typed error in ${exhausted?.elapsedMs}ms`);
620
+ }
621
+ // ---- A: N concurrent writers, one tenant and two tenants ----------------
622
+ console.log('-- A N concurrent writers: idem dedupe / no lost updates / tenant isolation');
623
+ {
624
+ const WORKERS = 3;
625
+ const KEYS = 16;
626
+ const WISH_ROUNDS = 2;
627
+ const WISH_NAME = 'drill shared wish';
628
+ const BUSY_TIMEOUT_MS = 2_000;
629
+ const idemKeys = Array.from({ length: KEYS }, (_, i) => `drill:key:${i}`);
630
+ const runBatch = async (stateRoot, tenants, mode) => {
631
+ const goPath = join(stateRoot, `go-${mode}`);
632
+ const started = [];
633
+ const readyPaths = [];
634
+ const totals = {
635
+ inserted: 0, deduped: 0, added: 0, updated: 0, retries: 0,
636
+ errors: {}, attempts: 0,
637
+ /** Per-tenant successful writes; lets safety assertions stay exact without assuming liveness. */
638
+ perTenantSuccess: {},
639
+ };
640
+ try {
641
+ for (const tenant of tenants) {
642
+ for (let w = 0; w < WORKERS; w += 1) {
643
+ const readyPath = join(stateRoot, `ready-${mode}-${tenant}-${w}`);
644
+ readyPaths.push(readyPath);
645
+ started.push(startChild(appendWorker, {
646
+ STATE_LEDGER_URL,
647
+ STATE_ROOT: stateRoot,
648
+ TENANT: tenant,
649
+ WORKER_ID: `${tenant}-${w}`,
650
+ MODE: mode,
651
+ IDEM_KEYS: JSON.stringify(idemKeys),
652
+ WISH_ROUNDS: String(WISH_ROUNDS),
653
+ WISH_NAME,
654
+ BUSY_TIMEOUT_MS: String(BUSY_TIMEOUT_MS),
655
+ READY_PATH: readyPath,
656
+ GO_PATH: goPath,
657
+ }));
658
+ }
659
+ }
660
+ let allReady = true;
661
+ for (const readyPath of readyPaths)
662
+ allReady = (await waitForReady(readyPath, 9_000, started.map((s) => s.child))) && allReady;
663
+ ok(allReady, `A[${tenants.length}t/${mode}] every writer process reached the callsite before the start gun`);
664
+ writeFileSync(goPath, 'go');
665
+ const runs = await Promise.all(started.map((s) => s.done));
666
+ for (const run of runs) {
667
+ eq(run.timedOut, false, `A[${tenants.length}t/${mode}] worker finished inside its bound`);
668
+ const report = lastJsonLine(run);
669
+ ok(report !== null, `A[${tenants.length}t/${mode}] worker emitted a report (exit=${run.exit} stderr=${run.stderr.slice(-300)})`);
670
+ if (!report)
671
+ continue;
672
+ totals.inserted += report.outcomes.inserted;
673
+ totals.deduped += report.outcomes.deduped;
674
+ totals.added += report.outcomes.added;
675
+ totals.updated += report.outcomes.updated;
676
+ totals.retries += report.outcomes.retries;
677
+ const success = mode === 'append'
678
+ ? report.outcomes.inserted + report.outcomes.deduped
679
+ : report.outcomes.added + report.outcomes.updated;
680
+ totals.perTenantSuccess[report.tenant] = (totals.perTenantSuccess[report.tenant] ?? 0) + success;
681
+ for (const [code, n] of Object.entries(report.outcomes.errors))
682
+ totals.errors[code] = (totals.errors[code] ?? 0) + n;
683
+ }
684
+ totals.attempts = (mode === 'append' ? KEYS : WISH_ROUNDS) * WORKERS * tenants.length;
685
+ return totals;
686
+ }
687
+ finally {
688
+ for (const s of started)
689
+ s.reap();
690
+ }
691
+ };
692
+ for (const tenants of [['local'], ['local', 'tenant-b']]) {
693
+ const stateRoot = realpathSync(mkdtempSync(join(workRoot, `append-${tenants.length}t-`)));
694
+ seedLedgerFile(stateRoot, tenants);
695
+ const dbPath = join(stateRoot, 'gotry-state', 'gotry-state.db');
696
+ const label = `A[${tenants.length}t]`;
697
+ // SAFETY vs LIVENESS: the gating assertions below are the ones that must
698
+ // hold on any host at any speed. How many attempts a slow 2-vCPU runner
699
+ // gets through, and which SQLite contention codes it sees, is liveness —
700
+ // reported through observe(), never gated, so a dormant unadmitted path
701
+ // cannot flake the regression red.
702
+ const CONTENTION_CODES = ['SQLITE_BUSY', 'SQLITE_BUSY_SNAPSHOT', 'SQLITE_CANTOPEN', 'SQLITE_PROTOCOL', 'SQLITE_IOERR_SHMOPEN'];
703
+ const onlyContention = (errors) => Object.keys(errors).every((c) => CONTENTION_CODES.includes(c));
704
+ // Arm 1: append-only event face (the admitted write shape).
705
+ const appendTotals = await runBatch(stateRoot, tenants, 'append');
706
+ const appendErrors = Object.values(appendTotals.errors).reduce((a, b) => a + b, 0);
707
+ const state = inspect(dbPath);
708
+ eq(state.integrity, 'ok', `${label} SAFETY integrity_check ok after ${WORKERS * tenants.length} concurrent writer processes`);
709
+ eq(state.duplicateIdem, 0, `${label} SAFETY no (tenant_id, idem_key) pair appears twice: the UNIQUE index held across processes`);
710
+ eq(state.distinctIdemAppend, appendTotals.inserted, `${label} SAFETY the ledger holds exactly the ${appendTotals.inserted} appends the workers believe they inserted — no lost update and no phantom row`);
711
+ eq(appendTotals.inserted + appendTotals.deduped + appendErrors, appendTotals.attempts, `${label} SAFETY every append attempt is an insert, a dedupe or a named error — no silent loss`);
712
+ ok(appendTotals.inserted <= KEYS * tenants.length, `${label} SAFETY the insert count never exceeds the distinct key space (${appendTotals.inserted} <= ${KEYS * tenants.length})`);
713
+ ok(onlyContention(appendTotals.errors), `${label} SAFETY any append failure is a known SQLite contention code, never a logic error (codes=${JSON.stringify(appendTotals.errors)})`);
714
+ for (const tenant of tenants) {
715
+ ok((state.perTenant[tenant] ?? 0) <= KEYS, `${label} SAFETY tenant ${tenant} never holds more than its own ${KEYS} append rows (${state.perTenant[tenant] ?? 0})`);
716
+ }
717
+ if (appendErrors === 0) {
718
+ eq(appendTotals.inserted, KEYS * tenants.length, `${label} with zero contention errors, every distinct idem key landed exactly once per tenant`);
719
+ for (const tenant of tenants) {
720
+ eq(state.perTenant[tenant], KEYS, `${label} with zero contention errors, tenant ${tenant} owns exactly its own ${KEYS} append rows`);
721
+ }
722
+ }
723
+ else {
724
+ observe(`${label} LIVENESS the append-only face hit ${appendErrors} contention errors on this host ${JSON.stringify(appendTotals.errors)}; safety invariants still held, so the key-space completeness check was reported instead of gated`);
725
+ }
726
+ // Arm 2: read-modify-write product path, one shared wish name.
727
+ const wishTotals = await runBatch(stateRoot, tenants, 'wish');
728
+ const wishErrors = Object.values(wishTotals.errors).reduce((a, b) => a + b, 0);
729
+ eq(wishTotals.added + wishTotals.updated + wishErrors, wishTotals.attempts, `${label} SAFETY every wish attempt is accounted for (added, updated, or a named error)`);
730
+ ok(onlyContention(wishTotals.errors), `${label} SAFETY any unrecovered wish failure is a known SQLite contention code (codes=${JSON.stringify(wishTotals.errors)})`);
731
+ ok(wishTotals.added <= tenants.length, `${label} SAFETY at most one "added" per tenant — the rest must see the existing row (${wishTotals.added} <= ${tenants.length})`);
732
+ eq(inspect(dbPath).integrity, 'ok', `${label} SAFETY integrity_check still ok after the read-modify-write batch`);
733
+ // FLIPPED 2026-10-05 (D15-2 fixed, founder decision). `appendWish` and the
734
+ // other read-first ledger transactions now run BEGIN IMMEDIATE, so the
735
+ // read-then-upgrade shape that returned SQLITE_BUSY_SNAPSHOT *without ever
736
+ // calling the busy handler* no longer exists; contention becomes a
737
+ // busy-handler wait, with a bounded in-ledger retry behind it. The worker
738
+ // keeps its caller-side retry loop purely as an INSTRUMENT: it now has to
739
+ // count zero. Before the fix six processes needed 6 to 9 retries to finish
740
+ // 12 attempts, and an earlier run left 8 of 12 unrecovered.
741
+ eq(wishTotals.errors ? Object.keys(wishTotals.errors).length : 0, 0, `${label} D15-2 the read-modify-write face leaves nothing unrecovered (${wishErrors} errors ${JSON.stringify(wishTotals.errors)})`);
742
+ eq(wishTotals.retries, 0, `${label} D15-2 the read-modify-write face needs ZERO caller-side retries now that the ledger takes the write lock up front (${wishTotals.retries} retries over ${wishTotals.attempts} attempts). IF THIS FLIPS BACK, a read-first ledger transaction lost its .immediate() — see the transaction audit table above runWrite in src/state-ledger.ts.`);
743
+ for (const tenant of tenants) {
744
+ const ledger = openDb(stateRoot, tenant);
745
+ try {
746
+ // A tenant whose every attempt lost the race legitimately has no row.
747
+ const expectedWishRows = (wishTotals.perTenantSuccess[tenant] ?? 0) > 0 ? 1 : 0;
748
+ const rows = ledger.readWishPool().filter((w) => w.name === WISH_NAME);
749
+ eq(rows.length, expectedWishRows, `${label} SAFETY tenant ${tenant} has exactly ${expectedWishRows} projection row(s) for the shared wish name (${wishTotals.perTenantSuccess[tenant] ?? 0} successful writes)`);
750
+ if (expectedWishRows === 1) {
751
+ eq(rows[0]?.wish_id, makeWishId(WISH_NAME), `${label} SAFETY tenant ${tenant} uses the stable name-derived wish id`);
752
+ }
753
+ const appends = ledger.readEvents('drill.append', 1_000);
754
+ eq(appends.length, state.perTenant[tenant] ?? 0, `${label} SAFETY tenant ${tenant} reads exactly the rows the database holds for it`);
755
+ eq(appends.every((r) => r.tenant_id === tenant), true, `${label} SAFETY no foreign tenant row leaks into the ${tenant} read`);
756
+ }
757
+ finally {
758
+ ledger.close();
759
+ }
760
+ }
761
+ const aFold = foldMatchesDirectRead(dbPath, 'local');
762
+ eq(aFold.match, true, `${label} fold rebuild reproduces the direct read (the log stays the authority)`);
763
+ eq(aFold.events, inspect(dbPath).events, `${label} the fold copy saw the same event count as the in-place read (not a vacuous comparison)`);
764
+ observe(`${label} append-only face, ${WORKERS * tenants.length} processes: ${appendTotals.inserted} inserted, ${appendTotals.deduped} deduped, ${appendErrors} errors`);
765
+ observe(`${label} read-modify-write face, ${WORKERS * tenants.length} processes: ${wishTotals.added} added, ${wishTotals.updated} updated, ${wishTotals.retries} caller-side retries needed on SQLITE_BUSY/SQLITE_BUSY_SNAPSHOT, ${wishErrors} unrecovered`);
766
+ }
767
+ }
768
+ // ---- B: SIGKILL mid-transaction, reopen, all-or-nothing -----------------
769
+ console.log('-- B crash drill: SIGKILL at named transaction points and at varied moments');
770
+ {
771
+ const POINTS = ['before-transaction', 'after-event-insert', 'after-projection-write', 'after-commit'];
772
+ const ITERATIONS = 2;
773
+ for (const point of POINTS) {
774
+ for (let iteration = 0; iteration < ITERATIONS; iteration += 1) {
775
+ const stateRoot = realpathSync(mkdtempSync(join(workRoot, `crash-${point}-${iteration}-`)));
776
+ const markerPath = join(stateRoot, 'marker');
777
+ const run = await startChild(crashWorker, { STATE_LEDGER_URL, STATE_ROOT: stateRoot, MARKER_PATH: markerPath, CRASH_POINT: point }).done;
778
+ const tag = `B[${point}#${iteration}]`;
779
+ eq(run.timedOut, false, `${tag} crash worker finished inside its bound`);
780
+ eq(run.signal, 'SIGKILL', `${tag} the worker really died by SIGKILL (signal=${run.signal} exit=${run.exit} stderr=${run.stderr.slice(-200)})`);
781
+ ok(existsSync(markerPath), `${tag} the kill landed at the intended point`);
782
+ const dbPath = join(stateRoot, 'gotry-state', 'gotry-state.db');
783
+ const state = inspect(dbPath);
784
+ eq(state.integrity, 'ok', `${tag} integrity_check ok after SIGKILL`);
785
+ if (point === 'after-commit') {
786
+ eq(state.events, 1, `${tag} the committed event survives`);
787
+ eq(state.wishItems, 1, `${tag} the committed projection row survives`);
788
+ }
789
+ else {
790
+ eq(state.events, 0, `${tag} all-or-nothing: no uncommitted event survived`);
791
+ eq(state.wishItems, 0, `${tag} all-or-nothing: no uncommitted projection row survived`);
792
+ }
793
+ const bFold = foldMatchesDirectRead(dbPath, 'local');
794
+ eq(bFold.match, true, `${tag} fold rebuild equals the direct read after reopen`);
795
+ eq(bFold.events, state.events, `${tag} the fold copy saw the same ${state.events} events as the in-place read — the -wal sidecar really travelled with the copy, so this is not empty-vs-empty`);
796
+ if (point === 'after-commit' && iteration === 0) {
797
+ observe(`B[${point}] the fold copy of a SIGKILLed ledger carried ${bFold.events} committed event(s) — proof the WAL sidecar travelled with the copy, so fold==direct-read is a real comparison and not empty-vs-empty`);
798
+ }
799
+ }
800
+ }
801
+ // Timing-varied kills on the real product write path (deterministic seed).
802
+ let seed = 20261004;
803
+ const nextDelay = () => { seed = (seed * 1103515245 + 12345) % 2147483648; return 60 + (seed % 140); };
804
+ for (let iteration = 0; iteration < 3; iteration += 1) {
805
+ const stateRoot = realpathSync(mkdtempSync(join(workRoot, `product-crash-${iteration}-`)));
806
+ const readyPath = join(stateRoot, 'ready');
807
+ const started = startChild(productWorker, { STATE_LEDGER_URL, STATE_ROOT: stateRoot, READY_PATH: readyPath });
808
+ const tag = `B-product#${iteration}`;
809
+ try {
810
+ ok(await waitForReady(readyPath, 9_000, [started.child]), `${tag} the writer reached its loop`);
811
+ await sleep(nextDelay());
812
+ started.child.kill('SIGKILL');
813
+ const run = await started.done;
814
+ eq(run.signal, 'SIGKILL', `${tag} the writer died by SIGKILL mid-flight (exit=${run.exit})`);
815
+ }
816
+ finally {
817
+ started.reap();
818
+ }
819
+ // The worker is a direct child and is now reaped, so the file is stable.
820
+ const dbPath = join(stateRoot, 'gotry-state', 'gotry-state.db');
821
+ eq(inspect(dbPath).integrity, 'ok', `${tag} integrity_check ok after an unannounced kill`);
822
+ const ledger = openDb(stateRoot, 'local');
823
+ let directWishes = 0;
824
+ let addedEvents = 0;
825
+ try {
826
+ directWishes = ledger.readWishPool().length;
827
+ addedEvents = ledger.readEvents('wish.added', 10_000).length;
828
+ }
829
+ finally {
830
+ ledger.close();
831
+ }
832
+ eq(directWishes, addedEvents, `${tag} every committed wish.added has exactly one projection row (${directWishes} vs ${addedEvents})`);
833
+ ok(addedEvents > 0, `${tag} the writer committed real work before the kill (${addedEvents} wishes) — a zero here would make the rest of this arm vacuous`);
834
+ const pFold = foldMatchesDirectRead(dbPath, 'local');
835
+ eq(pFold.match, true, `${tag} fold rebuild equals the direct read (no torn write survived)`);
836
+ eq(pFold.events, inspect(dbPath).events, `${tag} the fold copy saw the same event count as the in-place read (not a vacuous comparison)`);
837
+ observe(`${tag} ${addedEvents} wishes were committed before the kill; reopen found no partial write`);
838
+ }
839
+ }
840
+ // ---- C: stale-claim rejection with fencing under N-process contention ---
841
+ console.log('-- C stale-claim rejection: N dispatchers, fencing token, forward-only status');
842
+ {
843
+ const stateRoot = realpathSync(mkdtempSync(join(workRoot, 'claim-')));
844
+ const idemKey = 'drill:claim:intent';
845
+ const fields = {
846
+ tenantId: 'local',
847
+ actorRef: 'actor:drill-host',
848
+ principalRef: 'principal:drill-traveler',
849
+ seam: 'hotelbyte-hotel-book-confirm',
850
+ supplier: 'fixture-supplier@drill',
851
+ productRef: 'offer:drill',
852
+ travelTerms: 'drill:2nights:2guests',
853
+ amountTotal: 12_345,
854
+ currency: 'CNY',
855
+ commissionDisclosure: 'none',
856
+ validUntil: new Date(Date.now() + 600_000).toISOString(),
857
+ presentationKey: 'card:drill:v1',
858
+ };
859
+ {
860
+ const seed = openDb(stateRoot, 'local');
861
+ try {
862
+ const gate = new WriteGate(seed);
863
+ seed.requestPendingWrite({ idemKey, seam: fields.seam, payload: {} });
864
+ const prepared = gate.preparePresentation({ idemKey, fields, expiresAt: new Date(Date.now() + 600_000).toISOString() });
865
+ ok(prepared.ok, 'C0 the drill seeded a prepared challenge');
866
+ if (!prepared.ok)
867
+ throw new Error(`prepare failed: ${prepared.reason}`);
868
+ const confirmed = gate.trustedHostConfirm({ challengeId: prepared.card.challenge_id, deliveryNonce: prepared.trustedDelivery.deliveryNonce, actorRef: fields.actorRef, seam: fields.seam });
869
+ ok(confirmed.ok, 'C0 the drill seeded a trusted-host confirmation');
870
+ if (!confirmed.ok)
871
+ throw new Error(`confirm failed: ${confirmed.reason}`);
872
+ const enqueued = gate.consumeApprovalAndEnqueue({ receiptId: confirmed.receiptId, idemKey, fields, effectName: 'drill-hotel-book' });
873
+ ok(enqueued.ok, `C0 the drill seeded exactly one queued outbox intent (${enqueued.ok ? 'ok' : enqueued.reason})`);
874
+ }
875
+ finally {
876
+ seed.close();
877
+ }
878
+ }
879
+ const WORKERS = 4;
880
+ const goPath = join(stateRoot, 'go');
881
+ const started = [];
882
+ try {
883
+ const readyPaths = [];
884
+ for (let w = 0; w < WORKERS; w += 1) {
885
+ const readyPath = join(stateRoot, `ready-${w}`);
886
+ readyPaths.push(readyPath);
887
+ started.push(startChild(claimWorker, {
888
+ STATE_LEDGER_URL,
889
+ WRITE_GATE_URL,
890
+ STATE_ROOT: stateRoot,
891
+ IDEM_KEY: idemKey,
892
+ WORKER_ID: String(w),
893
+ LEASE_UNTIL: new Date(Date.now() + 60_000).toISOString(),
894
+ READY_PATH: readyPath,
895
+ GO_PATH: goPath,
896
+ }, 'tsx'));
897
+ }
898
+ let allReady = true;
899
+ for (const readyPath of readyPaths)
900
+ allReady = (await waitForReady(readyPath, 9_000, started.map((s) => s.child))) && allReady;
901
+ ok(allReady, `C1 all ${WORKERS} dispatcher processes reached the claim callsite`);
902
+ writeFileSync(goPath, 'go');
903
+ const runs = await Promise.all(started.map((s) => s.done));
904
+ const verdicts = runs.map((run) => lastJsonLine(run));
905
+ ok(verdicts.every((v) => v !== null), `C2 every dispatcher reported a verdict (stderr=${runs.map((r) => r.stderr.slice(-160)).join('|')})`);
906
+ const winners = verdicts.filter((v) => v?.verdict.ok === true);
907
+ const losers = verdicts.filter((v) => v?.verdict.ok === false);
908
+ eq(winners.length, 1, `C3 exactly one of ${WORKERS} dispatcher processes wins the claim (losers=${JSON.stringify(losers.map((l) => l?.verdict.reason))})`);
909
+ eq(losers.length, WORKERS - 1, 'C4 every loser is refused');
910
+ // A loser may also surface a raw SQLite contention code: `claimForDispatch`
911
+ // uses an IMMEDIATE transaction, so a busy-timeout exhaustion throws rather
912
+ // than returning a verdict. That is a liveness outcome, not a correctness
913
+ // one — what must hold is that no loser claimed anything, which C8..C10
914
+ // assert against the database. So accept the throw here and report it.
915
+ const loserReasons = losers.map((l) => String(l?.verdict.reason));
916
+ const verdictReasons = ['lost-race', 'not-claimable', 'missing-intent'];
917
+ ok(loserReasons.every((r) => verdictReasons.includes(r) || /^threw:SQLITE_(BUSY|BUSY_SNAPSHOT|CANTOPEN|PROTOCOL)/.test(r)), `C5 every loser either got a closed-set verdict or threw a known SQLite contention code — never a partial claim (${JSON.stringify(loserReasons)})`);
918
+ const threwLosers = loserReasons.filter((r) => r.startsWith('threw:'));
919
+ if (threwLosers.length > 0) {
920
+ observe(`C LIVENESS ${threwLosers.length} of ${WORKERS - 1} losers surfaced a raw SQLite code instead of a typed verdict ${JSON.stringify(threwLosers)} — the IMMEDIATE claim transaction exhausted its busy timeout. Correctness held (exactly one claim row and one claim event below). claimForDispatch already takes the write lock up front, so this is pure busy-timeout exhaustion in write-gate.ts, not the D15-2 read-then-upgrade shape that was fixed in the ledger.`);
921
+ }
922
+ eq(winners[0]?.verdict.fencing_token, 1, 'C6 the winner persisted a fencing token before any outbound call');
923
+ ok(typeof winners[0]?.verdict.attempt_id === 'string', 'C7 the winner persisted an immutable attempt_id');
924
+ const verify = openDb(stateRoot, 'local');
925
+ try {
926
+ const intents = verify.db.prepare('SELECT dispatch_status, attempt_id, fencing_token FROM write_effect_intents WHERE tenant_id = ? AND idem_key = ?').all('local', idemKey);
927
+ eq(intents.length, 1, 'C8 exactly one outbox row exists for the contested intent');
928
+ eq(intents[0]?.dispatch_status, 'dispatching', 'C9 the single row is dispatching');
929
+ const claimEvents = verify.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'writegate.dispatch_claimed' AND tenant_id = ?").get('local');
930
+ eq(claimEvents.n, 1, 'C10 exactly one claim event landed (the losers wrote nothing)');
931
+ const retryGate = new WriteGate(verify);
932
+ const stale = retryGate.claimForDispatch({ idemKey, claimedBy: 'drill-stale-worker', leaseUntil: new Date(Date.now() + 60_000).toISOString() });
933
+ ok(stale.ok === false && stale.reason === 'not-claimable', `C11 a stale dispatcher retrying after the winner is refused (${stale.ok ? 'claimed!' : stale.reason})`);
934
+ verify.db.prepare('UPDATE write_effect_intents SET lease_until = ? WHERE tenant_id = ? AND idem_key = ?').run(new Date(Date.now() - 60_000).toISOString(), 'local', idemKey);
935
+ const afterExpiry = retryGate.claimForDispatch({ idemKey, claimedBy: 'drill-lease-expired', leaseUntil: new Date(Date.now() + 60_000).toISOString() });
936
+ ok(afterExpiry.ok === false && afterExpiry.reason === 'not-claimable', `C12 an expired lease does not make the intent claimable again (${afterExpiry.ok ? 'claimed!' : afterExpiry.reason})`);
937
+ let reversalBlocked = false;
938
+ try {
939
+ verify.db.prepare("UPDATE write_effect_intents SET dispatch_status = 'queued' WHERE tenant_id = ? AND idem_key = ?").run('local', idemKey);
940
+ }
941
+ catch (error) {
942
+ reversalBlocked = /forward-only/.test(error instanceof Error ? error.message : String(error));
943
+ }
944
+ eq(reversalBlocked, true, 'C13 a direct SQL reset to queued is refused by the storage-level trigger, not by an application if');
945
+ const tenantB = openDb(stateRoot, 'tenant-b');
946
+ try {
947
+ const gateB = new WriteGate(tenantB);
948
+ const crossTenant = gateB.claimForDispatch({ idemKey, claimedBy: 'drill-tenant-b', leaseUntil: new Date(Date.now() + 60_000).toISOString() });
949
+ ok(crossTenant.ok === false && crossTenant.reason === 'missing-intent', `C14 tenant-b claiming the same idem_key sees no intent: cross-tenant affected rows = 0 (${crossTenant.ok ? 'claimed!' : crossTenant.reason})`);
950
+ }
951
+ finally {
952
+ tenantB.close();
953
+ }
954
+ observe('C fencing_token is monotonic only vacuously: the forward-only trigger makes a second claim impossible, so no token above 1 is observable today. A real multi-machine lease handoff would be the first thing to exercise it.');
955
+ }
956
+ finally {
957
+ verify.close();
958
+ }
959
+ eq(inspect(join(stateRoot, 'gotry-state', 'gotry-state.db')).integrity, 'ok', 'C15 integrity_check ok after the claim race');
960
+ }
961
+ finally {
962
+ for (const s of started)
963
+ s.reap();
964
+ }
965
+ }
966
+ // ---- D: online backup under write load -> restore -> fold ---------------
967
+ console.log('-- D SQLite online backup taken under live write load, then restored');
968
+ {
969
+ const stateRoot = realpathSync(mkdtempSync(join(workRoot, 'backup-')));
970
+ seedLedgerFile(stateRoot, ['local']);
971
+ const readyPath = join(stateRoot, 'ready');
972
+ const dbPath = join(stateRoot, 'gotry-state', 'gotry-state.db');
973
+ const backupPath = join(workRoot, 'online-backup.db');
974
+ const started = startChild(loadWorker, { STATE_LEDGER_URL, STATE_ROOT: stateRoot, READY_PATH: readyPath });
975
+ let backupResult = null;
976
+ try {
977
+ ok(await waitForReady(readyPath, 9_000, [started.child]), 'D1 the load writer reached its loop');
978
+ await sleep(150);
979
+ const reader = new Database(dbPath);
980
+ try {
981
+ reader.pragma('busy_timeout = 8000');
982
+ backupResult = await reader.backup(backupPath);
983
+ }
984
+ finally {
985
+ reader.close();
986
+ }
987
+ ok(backupResult !== null && backupResult.remainingPages === 0, `D2 the online backup completed while writes were in flight (${JSON.stringify(backupResult)})`);
988
+ }
989
+ finally {
990
+ started.reap();
991
+ await started.done;
992
+ }
993
+ // Restore = copy + checksum verification, mirroring the state-repair discipline.
994
+ const restoreRoot = realpathSync(mkdtempSync(join(workRoot, 'restore-')));
995
+ mkdirSync(join(restoreRoot, 'gotry-state'), { recursive: true });
996
+ const restoredPath = join(restoreRoot, 'gotry-state', 'gotry-state.db');
997
+ const sha256Of = (path) => createHash('sha256').update(readFileSync(path)).digest('hex');
998
+ // The checksum must be taken BEFORE the restore, or comparing a file to its
999
+ // own copy is tautological. The meaningful facts are: the transfer preserved
1000
+ // the snapshot, the restore did NOT come from the still-advancing live file,
1001
+ // and a tampered snapshot is detectable.
1002
+ const backupSha = sha256Of(backupPath);
1003
+ const liveSha = sha256Of(dbPath);
1004
+ copyFileSync(backupPath, restoredPath);
1005
+ eq(sha256Of(restoredPath), backupSha, 'D3a the restore preserved the snapshot byte for byte (transfer integrity)');
1006
+ ok(sha256Of(restoredPath) !== liveSha, 'D3b the restored file is the SNAPSHOT, not the live source that kept being written after the backup');
1007
+ const tamperedPath = join(restoreRoot, 'tampered-snapshot.db');
1008
+ const tampered = readFileSync(backupPath);
1009
+ tampered[Math.floor(tampered.length / 2)] ^= 0xff;
1010
+ writeFileSync(tamperedPath, tampered);
1011
+ ok(sha256Of(tamperedPath) !== backupSha, 'D3c a single flipped byte changes the checksum, so the restore gate can detect a tampered snapshot');
1012
+ const source = inspect(dbPath);
1013
+ const restored = inspect(restoredPath);
1014
+ eq(restored.integrity, 'ok', 'D4 the restored snapshot passes integrity_check');
1015
+ eq(source.integrity, 'ok', 'D5 the live source still passes integrity_check after being backed up under load');
1016
+ ok(restored.events > 0, `D6 the snapshot captured real rows (${restored.events})`);
1017
+ ok(restored.events <= source.events, `D7 the snapshot is not ahead of its source (${restored.events} <= ${source.events})`);
1018
+ eq(restored.duplicateIdem, 0, 'D8 no duplicated idem key survived into the snapshot');
1019
+ eq(restored.seqs.every((seq, i) => source.seqs[i] === seq), true, 'D9 the snapshot event sequence is a prefix of the source: a consistent point in the log');
1020
+ const dFold = foldMatchesDirectRead(restoredPath, 'local');
1021
+ eq(dFold.match, true, 'D10 the restored fold equals the restored direct read (the snapshot point is self-consistent)');
1022
+ eq(dFold.events, restored.events, `D10b the fold copy saw all ${restored.events} restored events (not a vacuous comparison)`);
1023
+ observe(`D online backup under load: ${restored.events} of ${source.events} events captured at the snapshot point, pages=${JSON.stringify(backupResult)}`);
1024
+ }
1025
+ // ---- E: the replication dependency decision stays open ------------------
1026
+ console.log('-- E replication dependencies: absence pinned, not assumed');
1027
+ {
1028
+ for (const dependency of ['litestream', 'cr-sqlite', '@vlcn.io/crsqlite']) {
1029
+ let resolved = true;
1030
+ try {
1031
+ await import(dependency);
1032
+ }
1033
+ catch {
1034
+ resolved = false;
1035
+ }
1036
+ eq(resolved, false, `E ${dependency} is NOT installed — recorded as "needs real trigger + dependency decision", never as proven`);
1037
+ }
1038
+ observe('E Litestream (streaming backup) and cr-sqlite (multi-writer CRDT replication) remain unevaluated: they need the real D-15 trigger plus a founder dependency decision. The drill proved only the stock better-sqlite3 online-backup path.');
1039
+ }
1040
+ }
1041
+ finally {
1042
+ clearTimeout(suiteDeadline);
1043
+ reapAll();
1044
+ rmSync(workRoot, { recursive: true, force: true });
1045
+ }
1046
+ // Leak discipline: checked against the pid set, which is never cleared, so the
1047
+ // assertion cannot be satisfied by an emptied collection. Two of the workers
1048
+ // here loop forever by design (product-write and load), so a missed reap is a
1049
+ // real runaway process, not a cosmetic issue.
1050
+ {
1051
+ const survivors = [...trackedPids].filter((pid) => {
1052
+ try {
1053
+ process.kill(pid, 0);
1054
+ return true;
1055
+ }
1056
+ catch {
1057
+ return false;
1058
+ }
1059
+ });
1060
+ eq(survivors.length, 0, `Z no process this drill spawned survives (survivors=${JSON.stringify(survivors)})`);
1061
+ observe(`Z tracked ${trackedPids.size} spawned pids across all arms; ${survivors.length} survived the final reap`);
1062
+ }
1063
+ console.log(`\nDRILL #275 MULTI-WRITER (${DRILL_LABEL}): ${pass} ok, ${failures.length} fail`);
1064
+ console.log('OBSERVATIONS (facts, not acceptance):');
1065
+ for (const line of observations)
1066
+ console.log(` - ${line}`);
1067
+ console.log('EVIDENCE BOUNDARY: mkdtemp fixtures and synthetic workers only. No second real user,');
1068
+ console.log('no multi-machine deployment, no AaaS initiative. Tracking issue #275 was closed on');
1069
+ console.log('2026-10-05 as deferred, not completed; no replication or multi-writer path is enabled by');
1070
+ console.log('this drill. The D15-1/D15-2 ledger defects it found ARE fixed and are gated above.');
1071
+ if (failures.length > 0) {
1072
+ console.error(`\n${failures.length} FAILURE(S):`);
1073
+ for (const failure of failures)
1074
+ console.error(` - ${failure}`);
1075
+ process.exit(1);
1076
+ }