@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,522 @@
1
+ import type { ActionReceipt, BookingReadAction, BookingWorkspaceSnapshot, OfferCriteria } from './contracts.ts'
2
+ import { createHash } from 'node:crypto'
3
+
4
+ /** Typed CheckAvail budget. This module never parses prose or retries a supplier. */
5
+ export const MAX_HOTELS_PER_TASK = 5
6
+ export const MAX_OFFERS_PER_HOTEL_GENERATION = 3
7
+ export const MAX_OFFER_CHECKS_PER_HOTEL = 2
8
+ export const MAX_OFFER_QUERIES_PER_HOTEL = 2
9
+
10
+ export type AvailabilityHotelStatus = 'unvisited' | 'active' | 'negative' | 'confirmed' | 'inconclusive'
11
+ export type AvailabilityGenerationSource =
12
+ | { kind: 'query_receipt'; actionId: string; receiptDigest: string; workspaceRevision: number }
13
+ | { kind: 'workspace_snapshot'; workspaceDigest: string; workspaceRevision: number }
14
+ export interface AvailabilityGeneration { generationId: string; source: AvailabilityGenerationSource; offerSetDigest: string; orderedOfferRefs: string[]; evidence: 'complete' | 'partial'; valid: boolean }
15
+ export interface AvailabilityHotelState {
16
+ hotelRef: string; status: AvailabilityHotelStatus; checksIssued: number; offerQueriesIssued: number; generationNo: number
17
+ currentGeneration?: AvailabilityGeneration
18
+ /** Compatibility projection for callers needing current refs only. */
19
+ generation: number; currentOfferRefs: string[]; invalidatedOfferRefs: string[]; checkCount: number; freshOffersRequired: boolean
20
+ tombstonedOfferRefs: string[]; tombstonedOfferVersionRefs: string[]
21
+ lastEvidence: 'none' | 'confirmed' | 'unavailable' | 'inconclusive'
22
+ }
23
+ export interface AvailabilityAttempt { actionId: string; hotelRef: string; offerRef: string; offerVersionRef: string; generation: number; ordinal: number; workspaceRevision: number }
24
+ export interface AvailabilityQueryReservation { actionId: string; hotelRefs: string[]; workspaceRevision: number }
25
+ export interface AvailabilityExhaustion {
26
+ code: 'availability_confirmed' | 'availability_exhausted_complete' | 'availability_exhausted_inconclusive'
27
+ hotelRefs: string[]; reason: 'no_current_offers' | 'check_limit_reached' | 'confirmed'; evidence: 'conclusive' | 'inconclusive'
28
+ }
29
+ export interface AvailabilityPolicyState {
30
+ initialized: boolean; recoveryStarted: boolean
31
+ availabilityPhase: 'need_offers' | 'waiting_offers' | 'need_check' | 'waiting_check' | 'terminal'
32
+ activeHotelOrdinal: number; hotelRefs: string[]; hotels: Record<string, AvailabilityHotelState>; attempts: AvailabilityAttempt[]; queryReservations: AvailabilityQueryReservation[]; terminal?: AvailabilityExhaustion
33
+ recoveryId?: string; criteria?: OfferCriteria; criteriaDigest?: string; candidateSetDigest?: string; lastQueryHotelRefs?: string[]; lastQueryCriteriaDigest?: string
34
+ /** Receipt-authoritative mapping used when a shortlist ref is no longer loaded. */
35
+ lastQueryOfferHotels?: Record<string, string>; lastQuerySourceActionId?: string; lastQuerySourceReceiptDigest?: string; lastQueryWorkspaceRevision?: number
36
+ }
37
+ export type AvailabilityPolicyDecision =
38
+ | { ok: true; hotelRef: string; offerRef: string; offerVersionRef: string; checkCount: number; generation: number }
39
+ | { ok: false; code: 'hotel_unknown' | 'offer_not_loaded' | 'offers_refresh_required' | 'hotel_check_limit_reached' | 'availability_exhausted' | 'generation_invalid' | 'hotel_not_active' }
40
+
41
+ type AvailabilityActionLike = { kind: BookingReadAction['kind']; actionId: string; input: Record<string, any>; expectedRevision?: number }
42
+
43
+ function unique(values: readonly string[]): string[] { return [...new Set(values)] }
44
+ export function assertWorkspaceLoadedOfferRefsUnique(workspace: BookingWorkspaceSnapshot): void {
45
+ if (workspace.loadedOffers.length !== new Set(workspace.loadedOffers.map((offer) => offer.offerRef)).size) throw new Error('availability_duplicate_offer_ref')
46
+ if (workspace.loadedOffers.length !== new Set(workspace.loadedOffers.map((offer) => offer.offerVersionRef)).size) throw new Error('availability_duplicate_offer_version_ref')
47
+ }
48
+ function stable(value: unknown): unknown { if (Array.isArray(value)) return value.map(stable); if (!value || typeof value !== 'object') return value; return Object.fromEntries(Object.keys(value as Record<string, unknown>).sort().map((key) => [key, stable((value as Record<string, unknown>)[key])])); }
49
+ export function digest(value: unknown): string { return createHash('sha256').update(JSON.stringify(stable(value))).digest('hex') }
50
+ function workspaceDigest(workspace: BookingWorkspaceSnapshot): string { const { contextRef: _context, ...bound } = workspace as unknown as Record<string, unknown>; return digest(bound) }
51
+ function offerRefsForHotel(workspace: BookingWorkspaceSnapshot, hotelRef: string, selectedOfferRef?: string): string[] {
52
+ const refs = unique(workspace.loadedOffers.filter((offer) => offer.hotelRef === hotelRef).map((offer) => offer.offerRef))
53
+ if (selectedOfferRef) return unique([selectedOfferRef, ...refs]).slice(0, MAX_OFFERS_PER_HOTEL_GENERATION)
54
+ if (refs.length > MAX_OFFERS_PER_HOTEL_GENERATION) throw new Error('availability_offer_limit_exceeded')
55
+ return refs
56
+ }
57
+ function loadedOfferVersion(workspace: BookingWorkspaceSnapshot, offerRef: string, offerVersionRef: string) { return workspace.loadedOffers.find((offer) => offer.offerRef === offerRef && offer.offerVersionRef === offerVersionRef) }
58
+ function legalOfferRefsForHotel(workspace: BookingWorkspaceSnapshot, hotel: AvailabilityHotelState, refs: readonly string[]): string[] {
59
+ const legal = refs.filter((ref) => {
60
+ if (hotel.tombstonedOfferRefs.includes(ref)) return false
61
+ const loaded = workspace.loadedOffers.find((offer) => offer.offerRef === ref && offer.hotelRef === hotel.hotelRef)
62
+ return Boolean(loaded && !hotel.tombstonedOfferVersionRefs.includes(loaded.offerVersionRef))
63
+ })
64
+ if (legal.length > MAX_OFFERS_PER_HOTEL_GENERATION) throw new Error('availability_offer_limit_exceeded')
65
+ return unique(legal)
66
+ }
67
+ function copyHotel(hotel: AvailabilityHotelState): AvailabilityHotelState {
68
+ return { ...hotel, currentOfferRefs: [...hotel.currentOfferRefs], invalidatedOfferRefs: [...hotel.invalidatedOfferRefs], tombstonedOfferRefs: [...hotel.tombstonedOfferRefs], tombstonedOfferVersionRefs: [...hotel.tombstonedOfferVersionRefs], ...(hotel.currentGeneration ? { currentGeneration: { ...hotel.currentGeneration, orderedOfferRefs: [...hotel.currentGeneration.orderedOfferRefs] } } : {}) }
69
+ }
70
+ function copyState(state: AvailabilityPolicyState): AvailabilityPolicyState {
71
+ return { ...state, ...(state.criteria ? { criteria: structuredClone(state.criteria) } : {}), hotelRefs: [...state.hotelRefs], hotels: Object.fromEntries(Object.entries(state.hotels).map(([ref, hotel]) => [ref, copyHotel(hotel)])), attempts: state.attempts.map((attempt) => ({ ...attempt })), queryReservations: state.queryReservations.map((query) => ({ ...query, hotelRefs: [...query.hotelRefs] })), ...(state.lastQueryHotelRefs ? { lastQueryHotelRefs: [...state.lastQueryHotelRefs] } : {}), ...(state.lastQueryOfferHotels ? { lastQueryOfferHotels: { ...state.lastQueryOfferHotels } } : {}), ...(state.terminal ? { terminal: { ...state.terminal, hotelRefs: [...state.terminal.hotelRefs] } } : {}) }
72
+ }
73
+
74
+ function isPlainRecord(value: unknown): value is Record<string, unknown> { return Boolean(value && typeof value === 'object' && !Array.isArray(value)) }
75
+ function isCriterion(value: unknown, valueCheck: (candidate: unknown) => boolean): boolean {
76
+ return isPlainRecord(value) && Object.keys(value).length === 2 && (value.strength === 'must' || value.strength === 'prefer') && valueCheck(value.value)
77
+ }
78
+ function isMoney(value: unknown): boolean {
79
+ return isPlainRecord(value) && Object.keys(value).length === 3 && typeof value.amount === 'string' && Boolean(value.amount) && typeof value.currency === 'string' && Boolean(value.currency) && typeof value.sourceFactRef === 'string' && Boolean(value.sourceFactRef)
80
+ }
81
+ function isOfferCriteria(value: unknown): value is OfferCriteria {
82
+ if (!isPlainRecord(value)) return false
83
+ const allowed = ['roomType', 'bedType', 'meals', 'freeCancellation', 'freeCancellationUntil', 'totalPriceMax', 'roomsAvailableMin', 'payAtProperty', 'mobileRate', 'targetCount', 'sort']
84
+ if (Object.keys(value).some((key) => !allowed.includes(key))) return false
85
+ const strings = (candidate: unknown): boolean => Array.isArray(candidate) && candidate.every((item) => typeof item === 'string' && Boolean(item))
86
+ if (value.roomType !== undefined && !isCriterion(value.roomType, strings)) return false
87
+ if (value.bedType !== undefined && !isCriterion(value.bedType, strings)) return false
88
+ if (value.meals !== undefined && !isCriterion(value.meals, strings)) return false
89
+ if (value.freeCancellation !== undefined && !isCriterion(value.freeCancellation, (candidate) => typeof candidate === 'boolean')) return false
90
+ if (value.freeCancellationUntil !== undefined && !isCriterion(value.freeCancellationUntil, (candidate) => typeof candidate === 'string' && Boolean(candidate))) return false
91
+ if (value.totalPriceMax !== undefined && !isCriterion(value.totalPriceMax, isMoney)) return false
92
+ if (value.roomsAvailableMin !== undefined && !isCriterion(value.roomsAvailableMin, (candidate) => Number.isSafeInteger(candidate) && (candidate as number) >= 0)) return false
93
+ if (value.payAtProperty !== undefined && !isCriterion(value.payAtProperty, (candidate) => typeof candidate === 'boolean')) return false
94
+ if (value.mobileRate !== undefined && !isCriterion(value.mobileRate, (candidate) => typeof candidate === 'boolean')) return false
95
+ if (value.targetCount !== undefined && (!Number.isSafeInteger(value.targetCount) || (value.targetCount as number) < 0)) return false
96
+ if (value.sort !== undefined && !['best_match', 'total_price_asc', 'cancellation_latest'].includes(value.sort as string)) return false
97
+ return true
98
+ }
99
+ function bindCriteria(next: AvailabilityPolicyState, criteriaDigest: string, criteria?: OfferCriteria, required = false): void {
100
+ if (criteria !== undefined) {
101
+ if (!isOfferCriteria(criteria) || digest(criteria) !== criteriaDigest) throw new Error('availability_criteria_invalid')
102
+ if (next.criteriaDigest !== undefined && next.criteriaDigest !== criteriaDigest) throw new Error('availability_criteria_changed')
103
+ if (next.criteria && digest(next.criteria) !== criteriaDigest) throw new Error('availability_criteria_changed')
104
+ next.criteria = structuredClone(criteria); next.criteriaDigest = criteriaDigest
105
+ } else if (required && !next.criteria) throw new Error('availability_criteria_required')
106
+ else if (next.criteria && next.criteriaDigest !== digest(next.criteria)) throw new Error('availability_criteria_mismatch')
107
+ else if (!next.criteria && criteriaDigest) {
108
+ if (next.criteriaDigest !== undefined && next.criteriaDigest !== criteriaDigest) throw new Error('availability_criteria_changed')
109
+ next.criteriaDigest = criteriaDigest
110
+ }
111
+ if (next.criteriaDigest !== undefined && next.criteriaDigest !== criteriaDigest && criteriaDigest) throw new Error('availability_criteria_changed')
112
+ }
113
+ function terminalFor(state: AvailabilityPolicyState): AvailabilityExhaustion | undefined {
114
+ if (!state.recoveryStarted || !state.hotelRefs.length) return undefined
115
+ const hotels = state.hotelRefs.map((ref) => state.hotels[ref]!)
116
+ if (hotels.some((hotel) => hotel.status === 'confirmed')) return { code: 'availability_confirmed', hotelRefs: [...state.hotelRefs], reason: 'confirmed', evidence: 'conclusive' }
117
+ // A negative result is conclusive only when it has no legal refresh left.
118
+ // The first manual check starts with a workspace generation (zero recovery
119
+ // queries), so it must leave a fresh-query transition available.
120
+ if (hotels.some((hotel) =>
121
+ hotel.checksIssued < MAX_OFFER_CHECKS_PER_HOTEL
122
+ && (
123
+ hotel.status === 'active'
124
+ || hotel.status === 'unvisited'
125
+ || (hotel.freshOffersRequired && hotel.offerQueriesIssued < MAX_OFFER_QUERIES_PER_HOTEL)
126
+ )
127
+ )) return undefined
128
+ const conclusive = hotels.every((hotel) => hotel.status === 'negative')
129
+ return { code: conclusive ? 'availability_exhausted_complete' : 'availability_exhausted_inconclusive', hotelRefs: [...state.hotelRefs], reason: hotels.every((hotel) => !hotel.currentOfferRefs.length) ? 'no_current_offers' : 'check_limit_reached', evidence: conclusive ? 'conclusive' : 'inconclusive' }
130
+ }
131
+ function withTerminal(state: AvailabilityPolicyState): AvailabilityPolicyState { const terminal = terminalFor(state); return terminal ? { ...state, terminal, availabilityPhase: 'terminal' } : state }
132
+ function newHotel(hotelRef: string): AvailabilityHotelState { return { hotelRef, status: 'unvisited', checksIssued: 0, offerQueriesIssued: 0, generationNo: 0, generation: 0, currentOfferRefs: [], invalidatedOfferRefs: [], checkCount: 0, freshOffersRequired: false, tombstonedOfferRefs: [], tombstonedOfferVersionRefs: [], lastEvidence: 'none' } }
133
+ function phaseForActiveHotel(state: AvailabilityPolicyState): 'need_offers' | 'need_check' {
134
+ const active = state.hotels[state.hotelRefs[state.activeHotelOrdinal] ?? '']
135
+ return active?.status === 'active' && active.currentGeneration?.valid && active.currentOfferRefs.length > 0 && !active.freshOffersRequired
136
+ ? 'need_check'
137
+ : 'need_offers'
138
+ }
139
+
140
+ /** Initial workspace is only a hint; the bounded recovery starts at typed offers.query. */
141
+ export function createAvailabilityPolicy(workspace: BookingWorkspaceSnapshot): AvailabilityPolicyState { assertWorkspaceLoadedOfferRefsUnique(workspace); return { initialized: false, recoveryStarted: false, availabilityPhase: 'need_offers', activeHotelOrdinal: 0, hotelRefs: [], hotels: {}, attempts: [], queryReservations: [] } }
142
+ /** Ordinary search results never get silently truncated into recovery candidates. */
143
+ export function recordVisibleHotels(state: AvailabilityPolicyState, _workspace: BookingWorkspaceSnapshot): AvailabilityPolicyState { return copyState(state) }
144
+ /** Observe a normal offers query without opening an availability recovery. */
145
+ export function recordObservedOffersQuery(state: AvailabilityPolicyState, hotelRefs: readonly string[], criteriaDigest: string, workspace?: BookingWorkspaceSnapshot, receipt?: ActionReceipt, sourceActionId?: string, receiptDigest?: string, criteria?: OfferCriteria): AvailabilityPolicyState {
146
+ if (workspace) assertWorkspaceLoadedOfferRefsUnique(workspace)
147
+ if (state.recoveryStarted) return copyState(state)
148
+ const next = copyState(state)
149
+ if (!workspace || !receipt) return next
150
+ delete next.lastQueryHotelRefs; delete next.lastQueryCriteriaDigest; delete next.lastQueryOfferHotels; delete next.lastQuerySourceActionId; delete next.lastQuerySourceReceiptDigest; delete next.lastQueryWorkspaceRevision
151
+ const completeReceipt = receipt?.status === 'applied'
152
+ && receipt.resultContract.outcome === 'complete'
153
+ && receipt.resultContract.hardCriteriaMet
154
+ && !receipt.resultContract.gapCodes.length
155
+ && !receipt.resultContract.blockers.length
156
+ && receipt.observation.kind === 'offers.state'
157
+ && !receipt.observation.gapCodes?.length
158
+ if (completeReceipt && receipt.observation.kind === 'offers.state') {
159
+ if (!criteria) throw new Error('availability_criteria_required')
160
+ // Ordinary queries establish the latest recovery seed. Before recovery
161
+ // starts, a newer complete query is allowed to replace an older seed.
162
+ delete next.criteria
163
+ delete next.criteriaDigest
164
+ bindCriteria(next, criteriaDigest, criteria)
165
+ if (!sourceActionId || sourceActionId !== receipt.actionId || !receiptDigest || receiptDigest !== digest(receipt)) throw new Error('availability_query_provenance_mismatch')
166
+ next.lastQueryHotelRefs = [...new Set(hotelRefs)]
167
+ next.lastQueryCriteriaDigest = criteriaDigest || undefined
168
+ next.lastQueryOfferHotels = validateOffersReceiptWorkspace(receipt, workspace, hotelRefs)
169
+ next.lastQuerySourceActionId = sourceActionId
170
+ next.lastQuerySourceReceiptDigest = receiptDigest
171
+ next.lastQueryWorkspaceRevision = workspace.revision
172
+ } else {
173
+ // The latest ordinary query is the only candidate provenance. A failed
174
+ // or partial query must not leave an older criteria object usable for a
175
+ // later manual recovery.
176
+ delete next.criteria
177
+ delete next.criteriaDigest
178
+ }
179
+ return next
180
+ }
181
+
182
+ /**
183
+ * Reconcile an offers receipt against the post-action workspace before its
184
+ * provenance can influence recovery. Complete receipts must describe exactly
185
+ * the authoritative loaded offers; partial/gap receipts may only describe an
186
+ * evidenced subset. An unreported offer is never silently admitted.
187
+ */
188
+ export function validateOffersReceiptWorkspace(receipt: ActionReceipt, workspace: BookingWorkspaceSnapshot, hotelRefs: readonly string[]): Record<string, string> {
189
+ assertWorkspaceLoadedOfferRefsUnique(workspace)
190
+ if (receipt.observation.kind !== 'offers.state') throw new Error('availability_offers_observation_required')
191
+ const requested = unique(hotelRefs)
192
+ if (!sameSet(receipt.observation.hotelRefs, requested)) throw new Error('availability_offers_hotel_mismatch')
193
+ const authoritative = workspace.loadedOffers.filter((offer) => requested.includes(offer.hotelRef))
194
+ const authoritativeRefs = unique(authoritative.map((offer) => offer.offerRef))
195
+ const observedRefs = unique(receipt.observation.offerRefs)
196
+ if (observedRefs.some((offerRef) => !authoritativeRefs.includes(offerRef))) throw new Error('availability_offers_unreported')
197
+ const authoritativeHotels = new Set(authoritative.map((offer) => offer.hotelRef)).size
198
+ const observedOfferHotels = Object.fromEntries(observedRefs.map((offerRef) => [offerRef, authoritative.find((offer) => offer.offerRef === offerRef)!.hotelRef]))
199
+ const observedHotels = new Set(Object.values(observedOfferHotels)).size
200
+ const clean = !receipt.resultContract.gapCodes.length && !receipt.resultContract.blockers.length && !receipt.observation.gapCodes?.length
201
+ if ((receipt.status === 'no_match' || receipt.status === 'unavailable') && (receipt.resultContract.outcome !== 'empty' || observedRefs.length !== 0 || receipt.observation.loadedHotelCount !== 0)) throw new Error('availability_offers_receipt_incoherent')
202
+ const complete = receipt.status === 'applied' && receipt.resultContract.outcome === 'complete' && receipt.resultContract.hardCriteriaMet && clean
203
+ const exactEmpty = clean && receipt.resultContract.outcome === 'empty' && !['changed', 'partial', 'stale', 'failed', 'unsupported'].includes(receipt.status)
204
+ if ((complete || exactEmpty) && (!sameSet(observedRefs, authoritativeRefs) || receipt.observation.loadedHotelCount !== authoritativeHotels)) throw new Error('availability_offers_workspace_mismatch')
205
+ if (receipt.observation.loadedHotelCount !== observedHotels) throw new Error('availability_offers_count_mismatch')
206
+ return observedOfferHotels
207
+ }
208
+
209
+ function sameSet(a: readonly string[], b: readonly string[]): boolean { return a.length === b.length && unique(a).sort().every((value, index) => value === unique(b).sort()[index]) }
210
+
211
+ /** A typed offers.query receipt starts/advances a bounded candidate recovery. */
212
+ export function recordOffersQueryIssued(state: AvailabilityPolicyState, hotelRefs: readonly string[], workspace: BookingWorkspaceSnapshot, actionId: string, criteriaDigest = '', criteria?: OfferCriteria): AvailabilityPolicyState {
213
+ assertWorkspaceLoadedOfferRefsUnique(workspace)
214
+ const next = copyState(state); const requested = unique(hotelRefs)
215
+ if (!requested.length || requested.length > MAX_HOTELS_PER_TASK) throw new Error('availability_hotel_limit_exceeded')
216
+ if (!state.recoveryStarted) throw new Error('availability_recovery_not_started')
217
+ if (state.terminal) {
218
+ if (requested.some((ref) => state.hotels[ref]?.offerQueriesIssued >= MAX_OFFER_QUERIES_PER_HOTEL)) throw new Error('availability_offer_query_limit_reached')
219
+ throw new Error('availability_terminal')
220
+ }
221
+ if (requested.some((ref) => !next.hotels[ref])) throw new Error('availability_hotel_unknown')
222
+ else if ((requested.length !== 1 || next.hotelRefs[next.activeHotelOrdinal] !== requested[0]) && (next.attempts.length > 0 || Object.values(next.hotels).some((hotel) => hotel.generationNo > 0))) throw new Error('availability_hotel_not_active')
223
+ bindCriteria(next, criteriaDigest, criteria)
224
+ const existingReservation = next.queryReservations.find((reservation) => reservation.actionId === actionId)
225
+ if (existingReservation) {
226
+ if (!sameSet(existingReservation.hotelRefs, requested) || existingReservation.workspaceRevision !== workspace.revision) throw new Error('availability_query_action_conflict')
227
+ return next
228
+ }
229
+ for (const hotelRef of requested) { const hotel = next.hotels[hotelRef]!; if (hotel.offerQueriesIssued >= MAX_OFFER_QUERIES_PER_HOTEL) throw new Error('availability_offer_query_limit_reached'); hotel.offerQueriesIssued += 1 }
230
+ next.queryReservations.push({ actionId, hotelRefs: [...requested], workspaceRevision: workspace.revision }); next.availabilityPhase = 'waiting_offers'; next.initialized = true; return next
231
+ }
232
+ export function recordOffersGeneration(state: AvailabilityPolicyState, hotelRefs: readonly string[], workspace: BookingWorkspaceSnapshot, actionId: string, receiptDigest: string, receipt: ActionReceipt, criteriaDigest = '', criteria?: OfferCriteria): AvailabilityPolicyState {
233
+ assertWorkspaceLoadedOfferRefsUnique(workspace)
234
+ if (!state.recoveryStarted) return copyState(state)
235
+ if (state.terminal) throw new Error('availability_terminal')
236
+ const next = copyState(state); const requested = unique(hotelRefs)
237
+ if (!requested.length || requested.length > MAX_HOTELS_PER_TASK) throw new Error('availability_hotel_limit_exceeded')
238
+ const reservation = next.queryReservations.find((query) => query.actionId === actionId)
239
+ if (!reservation || !sameSet(reservation.hotelRefs, requested)) throw new Error('availability_query_not_reserved')
240
+ if (receipt.actionId !== actionId) throw new Error('availability_query_receipt_mismatch')
241
+ if (receiptDigest !== digest(receipt)) throw new Error('availability_receipt_digest_mismatch')
242
+ if (requested.some((ref) => !next.hotels[ref])) throw new Error('availability_hotel_unknown')
243
+ else if ((requested.length !== 1 || next.hotelRefs[next.activeHotelOrdinal] !== requested[0]) && (next.attempts.length > 0 || Object.values(next.hotels).some((hotel) => hotel.generationNo > 0))) throw new Error('availability_hotel_not_active')
244
+ bindCriteria(next, criteriaDigest, criteria)
245
+ if (receipt.observation.kind === 'offers.state') validateOffersReceiptWorkspace(receipt, workspace, requested)
246
+ for (const hotelRef of requested) {
247
+ const hotel = next.hotels[hotelRef]!
248
+ const partial = receipt.status === 'changed' || receipt.status === 'partial' || receipt.status === 'failed' || receipt.status === 'stale' || receipt.resultContract.outcome === 'partial' || (receipt.resultContract.outcome === 'complete' && !receipt.resultContract.hardCriteriaMet) || Boolean(receipt.resultContract.blockers.length) || Boolean(receipt.resultContract.gapCodes.length) || Boolean(receipt.observation.kind === 'gap' || (receipt.observation.kind === 'offers.state' && receipt.observation.gapCodes?.length))
249
+ const candidateRefs = partial
250
+ ? receipt.observation.kind === 'offers.state'
251
+ ? receipt.observation.offerRefs.filter((ref) => workspace.loadedOffers.some((offer) => offer.offerRef === ref && offer.hotelRef === hotelRef))
252
+ : []
253
+ : offerRefsForHotel(workspace, hotelRef)
254
+ const refs = legalOfferRefsForHotel(workspace, hotel, candidateRefs)
255
+ const unusable = ['stale', 'failed', 'unsupported'].includes(receipt.status)
256
+ if (unusable) { hotel.currentOfferRefs = []; hotel.invalidatedOfferRefs = [...(hotel.currentGeneration?.orderedOfferRefs ?? [])]; hotel.freshOffersRequired = hotel.checksIssued < MAX_OFFER_CHECKS_PER_HOTEL && hotel.offerQueriesIssued < MAX_OFFER_QUERIES_PER_HOTEL; hotel.status = 'inconclusive'; hotel.lastEvidence = 'inconclusive'; continue }
257
+ const filteredAllCandidates = candidateRefs.length > 0 && refs.length === 0
258
+ const noLegalRefsNeedsRefresh = filteredAllCandidates && hotel.offerQueriesIssued < MAX_OFFER_QUERIES_PER_HOTEL
259
+ hotel.generationNo += 1; hotel.generation = hotel.generationNo; hotel.currentOfferRefs = partial || noLegalRefsNeedsRefresh ? [] : refs; hotel.invalidatedOfferRefs = partial || noLegalRefsNeedsRefresh ? candidateRefs : []; hotel.freshOffersRequired = (partial || noLegalRefsNeedsRefresh) && hotel.checksIssued < MAX_OFFER_CHECKS_PER_HOTEL && hotel.offerQueriesIssued < MAX_OFFER_QUERIES_PER_HOTEL; hotel.status = partial || noLegalRefsNeedsRefresh ? 'inconclusive' : refs.length ? 'active' : 'negative'; hotel.lastEvidence = partial || noLegalRefsNeedsRefresh ? 'inconclusive' : refs.length ? 'none' : 'unavailable'
260
+ hotel.currentGeneration = { generationId: `${hotelRef}:generation:${hotel.generationNo}`, source: { kind: 'query_receipt', actionId, receiptDigest, workspaceRevision: workspace.revision }, offerSetDigest: digest(refs), orderedOfferRefs: refs, evidence: partial ? 'partial' : 'complete', valid: refs.length > 0 && !partial }
261
+ }
262
+ const firstRequested = next.hotelRefs.indexOf(requested[0]!)
263
+ if (firstRequested >= 0) next.activeHotelOrdinal = firstRequested
264
+ const queried = next.hotels[requested[0]!]!
265
+ if (!queried.currentOfferRefs.length && !queried.freshOffersRequired) {
266
+ const fallback = next.hotelRefs.findIndex((ref) => ['active', 'unvisited'].includes(next.hotels[ref]!.status))
267
+ if (fallback >= 0) next.activeHotelOrdinal = fallback
268
+ }
269
+ next.queryReservations = next.queryReservations.filter((query) => query.actionId !== actionId)
270
+ next.initialized = true; next.availabilityPhase = phaseForActiveHotel(next); delete next.terminal; return withTerminal(next)
271
+ }
272
+
273
+ export function canIssueOfferCheck(state: AvailabilityPolicyState, workspace: BookingWorkspaceSnapshot, offerRef: string, offerVersionRef: string): AvailabilityPolicyDecision {
274
+ assertWorkspaceLoadedOfferRefsUnique(workspace)
275
+ // Availability recovery is opt-in at the typed offers.query seam. Existing
276
+ // v2 read flows retain their pre-recovery offer.check behavior.
277
+ if (!state.recoveryStarted) {
278
+ const loaded = loadedOfferVersion(workspace, offerRef, offerVersionRef)
279
+ return loaded
280
+ ? { ok: true, hotelRef: loaded.hotelRef, offerRef, offerVersionRef, checkCount: 0, generation: 0 }
281
+ : { ok: false, code: 'offer_not_loaded' }
282
+ }
283
+ if (state.terminal) return { ok: false, code: 'availability_exhausted' }
284
+ const loaded = loadedOfferVersion(workspace, offerRef, offerVersionRef); if (!loaded) return { ok: false, code: 'offer_not_loaded' }
285
+ const hotel = state.hotels[loaded.hotelRef]; if (!hotel) return { ok: false, code: 'hotel_unknown' }
286
+ if (state.hotelRefs[state.activeHotelOrdinal] !== loaded.hotelRef) return { ok: false, code: 'hotel_not_active' }
287
+ if (hotel.freshOffersRequired) return { ok: false, code: 'offers_refresh_required' }
288
+ if (hotel.tombstonedOfferRefs.includes(offerRef) || hotel.tombstonedOfferVersionRefs.includes(offerVersionRef)) return { ok: false, code: 'generation_invalid' }
289
+ if (!hotel.currentGeneration?.valid || !hotel.currentGeneration.orderedOfferRefs.includes(offerRef) || hotel.invalidatedOfferRefs.includes(offerRef)) return { ok: false, code: 'generation_invalid' }
290
+ if (hotel.checksIssued >= MAX_OFFER_CHECKS_PER_HOTEL) return { ok: false, code: 'hotel_check_limit_reached' }
291
+ return { ok: true, hotelRef: loaded.hotelRef, offerRef, offerVersionRef, checkCount: hotel.checksIssued + 1, generation: hotel.generationNo }
292
+ }
293
+ /** Start the bounded recovery epoch at the first typed offer.check. */
294
+ function startRecoveryAtOffer(state: AvailabilityPolicyState, workspace: BookingWorkspaceSnapshot, offerRef: string, actionId: string): AvailabilityPolicyState {
295
+ assertWorkspaceLoadedOfferRefsUnique(workspace)
296
+ const loaded = workspace.loadedOffers.find((offer) => offer.offerRef === offerRef)
297
+ if (!loaded) return copyState(state)
298
+ const shortlistHotels = workspace.shortlistedOfferRefs.map((ref) => workspace.loadedOffers.find((offer) => offer.offerRef === ref)?.hotelRef ?? state.lastQueryOfferHotels?.[ref]).map((ref, index) => {
299
+ if (!ref) throw new Error(`availability_shortlist_unmapped:${workspace.shortlistedOfferRefs[index]}`)
300
+ return ref
301
+ })
302
+ const fallbackHotels = shortlistHotels.length ? shortlistHotels : []
303
+ const candidates = unique([loaded.hotelRef, ...fallbackHotels]).slice(0, MAX_HOTELS_PER_TASK)
304
+ const next = copyState(state); next.recoveryStarted = true; next.recoveryId = `recovery:${actionId}`; next.hotelRefs = candidates; next.candidateSetDigest = digest(candidates); next.hotels = Object.fromEntries(candidates.map((ref) => [ref, newHotel(ref)]))
305
+ const nextHotel = next.hotels[loaded.hotelRef]!
306
+ const mappedRefs = state.lastQueryOfferHotels ? Object.entries(state.lastQueryOfferHotels).filter(([, hotel]) => hotel === loaded.hotelRef).map(([ref]) => ref) : []
307
+ const currentRefs = unique(workspace.loadedOffers.filter((offer) => offer.hotelRef === loaded.hotelRef).map((offer) => offer.offerRef))
308
+ const mapped = state.lastQueryOfferHotels?.[offerRef] === loaded.hotelRef
309
+ && state.lastQueryWorkspaceRevision === workspace.revision
310
+ && Boolean(state.lastQuerySourceActionId && state.lastQuerySourceReceiptDigest)
311
+ && sameSet(mappedRefs, currentRefs)
312
+ if (mapped && state.criteria) { next.criteria = structuredClone(state.criteria); next.criteriaDigest = digest(next.criteria) }
313
+ const refs = mapped && state.lastQueryOfferHotels
314
+ ? unique([offerRef, ...mappedRefs]).slice(0, MAX_OFFERS_PER_HOTEL_GENERATION)
315
+ : offerRefsForHotel(workspace, loaded.hotelRef, offerRef)
316
+ const source = mapped
317
+ ? { kind: 'query_receipt' as const, actionId: state.lastQuerySourceActionId!, receiptDigest: state.lastQuerySourceReceiptDigest!, workspaceRevision: state.lastQueryWorkspaceRevision! }
318
+ : { kind: 'workspace_snapshot' as const, workspaceDigest: workspaceDigest(workspace), workspaceRevision: workspace.revision }
319
+ nextHotel.generationNo = 1; nextHotel.generation = 1; nextHotel.currentOfferRefs = refs; nextHotel.status = refs.length ? 'active' : 'inconclusive'; nextHotel.currentGeneration = { generationId: `${loaded.hotelRef}:generation:1`, source, offerSetDigest: digest(refs), orderedOfferRefs: refs, evidence: 'complete', valid: refs.length > 0 }; next.initialized = true; next.availabilityPhase = 'need_check'; return next
320
+ }
321
+ export function recordOfferCheckIssued(state: AvailabilityPolicyState, workspace: BookingWorkspaceSnapshot, offerRef: string, offerVersionRef: string, actionId = `offer-check-${offerRef}-${state.attempts.length + 1}`): AvailabilityPolicyState {
322
+ assertWorkspaceLoadedOfferRefsUnique(workspace)
323
+ if (state.terminal) throw new Error('availability_terminal')
324
+ if (!state.recoveryStarted) {
325
+ if (!loadedOfferVersion(workspace, offerRef, offerVersionRef)) throw new Error('availability_offer_not_loaded')
326
+ const started = startRecoveryAtOffer(state, workspace, offerRef, actionId)
327
+ return recordOfferCheckIssued(started, workspace, offerRef, offerVersionRef, actionId)
328
+ }
329
+ const decision = canIssueOfferCheck(state, workspace, offerRef, offerVersionRef); if (!decision.ok) throw new Error(`availability_${decision.code}`)
330
+ const next = copyState(state); const hotel = next.hotels[decision.hotelRef]!; hotel.checksIssued += 1; hotel.checkCount = hotel.checksIssued; next.attempts.push({ actionId, hotelRef: decision.hotelRef, offerRef, offerVersionRef, generation: hotel.generationNo, ordinal: hotel.checksIssued, workspaceRevision: workspace.revision }); hotel.currentGeneration!.valid = false; next.availabilityPhase = 'waiting_check'; return next
331
+ }
332
+
333
+ /**
334
+ * Fold the availability portion of an issued action. Keeping this reducer
335
+ * next to the receipt reducers makes ledger replay use precisely the same
336
+ * transition as the write path.
337
+ */
338
+ export function reduceAvailabilityAction(
339
+ state: AvailabilityPolicyState,
340
+ workspace: BookingWorkspaceSnapshot,
341
+ action: AvailabilityActionLike,
342
+ ): AvailabilityPolicyState {
343
+ if (action.kind === 'offers.query') {
344
+ if (!isOfferCriteria(action.input.criteria) && (!state.criteria || !isOfferCriteria(state.criteria))) throw new Error('availability_criteria_required')
345
+ if (!isOfferCriteria(action.input.criteria) && state.recoveryStarted) throw new Error('availability_criteria_required')
346
+ if (!isOfferCriteria(action.input.criteria)) throw new Error('availability_criteria_required')
347
+ const criteriaDigest = digest(action.input.criteria)
348
+ let next = recordObservedOffersQuery(state, action.input.hotelRefs, criteriaDigest)
349
+ if (state.recoveryStarted) next = recordOffersQueryIssued(next, action.input.hotelRefs, workspace, action.actionId, criteriaDigest, action.input.criteria)
350
+ return next
351
+ }
352
+ if (action.kind === 'offer.check') return recordOfferCheckIssued(state, workspace, action.input.offerRef, action.input.offerVersionRef, action.actionId)
353
+ return copyState(state)
354
+ }
355
+
356
+ /** Fold a receipt against the exact durable attempt. Non-confirming results invalidate the whole generation. */
357
+ export function recordOfferCheckReceipt(state: AvailabilityPolicyState, workspace: BookingWorkspaceSnapshot, receipt: ActionReceipt, actionId: string, offerRef: string, offerVersionRef: string, expectedRevision: number): AvailabilityPolicyState {
358
+ assertWorkspaceLoadedOfferRefsUnique(workspace)
359
+ if (!state.recoveryStarted) return copyState(state)
360
+ const attempt = state.attempts.find((candidate) => candidate.actionId === actionId); if (!attempt) throw new Error('availability_attempt_unknown')
361
+ if (receipt.actionId !== actionId) throw new Error('availability_attempt_mismatch')
362
+ if (receipt.revision !== workspace.revision || receipt.revision < expectedRevision) throw new Error('availability_stale_receipt')
363
+ const targetOfferRef = receipt.observation.kind === 'offer.availability' ? receipt.observation.offerRef : offerRef
364
+ const targetVersionRef = receipt.observation.kind === 'offer.availability' ? receipt.observation.checkedOfferVersionRef : offerVersionRef
365
+ if (attempt.offerRef !== targetOfferRef || attempt.offerVersionRef !== targetVersionRef || offerRef !== targetOfferRef || offerVersionRef !== targetVersionRef || attempt.workspaceRevision !== expectedRevision) throw new Error('availability_attempt_mismatch')
366
+ const next = copyState(state); const hotel = next.hotels[attempt.hotelRef]; if (!hotel || !hotel.currentGeneration || attempt.generation !== hotel.generationNo) throw new Error('availability_generation_mismatch')
367
+ const observation = receipt.observation.kind === 'offer.availability' ? receipt.observation : undefined
368
+ if (['applied', 'changed', 'unavailable', 'no_match'].includes(receipt.status) && !observation) throw new Error('availability_observation_required')
369
+ if (observation?.available && (receipt.resultContract.outcome === 'empty' || ['unavailable', 'no_match', 'failed', 'stale', 'unsupported'].includes(receipt.status))) throw new Error('availability_receipt_incoherent')
370
+ if (observation && !observation.available && (observation.verifiedOfferRef || observation.currentOfferVersionRef)) throw new Error('availability_receipt_incoherent')
371
+ if (['unavailable', 'no_match'].includes(receipt.status) && receipt.resultContract.outcome !== 'empty') throw new Error('availability_receipt_incoherent')
372
+ const confirmed = receipt.status === 'applied' && Boolean(observation?.available && observation.currentOfferVersionRef === offerVersionRef && observation.verifiedOfferRef && workspace.verifiedOffer?.offerRef === offerRef && workspace.verifiedOffer.offerVersionRef === offerVersionRef && workspace.verifiedOffer.verifiedOfferRef === observation.verifiedOfferRef && loadedOfferVersion(workspace, offerRef, offerVersionRef)) && !observation?.changedFactRefs.length && !observation?.gapCodes?.length && receipt.resultContract.outcome === 'complete' && receipt.resultContract.hardCriteriaMet && !receipt.resultContract.blockers.length && !receipt.resultContract.gapCodes.length
373
+ if (confirmed) { hotel.status = 'confirmed'; hotel.lastEvidence = 'confirmed'; hotel.currentGeneration.valid = false; return withTerminal(next) }
374
+ if (receipt.status === 'changed' && observation?.available && observation.currentOfferVersionRef && observation.currentOfferVersionRef !== offerVersionRef && observation.changedFactRefs.length && !observation.verifiedOfferRef && loadedOfferVersion(workspace, offerRef, observation.currentOfferVersionRef)) {
375
+ hotel.tombstonedOfferVersionRefs = unique([...hotel.tombstonedOfferVersionRefs, offerVersionRef])
376
+ hotel.currentGeneration.valid = true; hotel.invalidatedOfferRefs = []; hotel.currentOfferRefs = [offerRef]; hotel.freshOffersRequired = false; hotel.status = 'active'; hotel.lastEvidence = 'none'; next.availabilityPhase = 'need_check'; return withTerminal(next)
377
+ }
378
+ const completeNegative = (receipt.status === 'unavailable' || receipt.status === 'no_match') && receipt.resultContract.outcome === 'empty' && observation?.available === false && receipt.resultContract.gapCodes.length === 0 && !receipt.resultContract.blockers.length && !observation.gapCodes?.length
379
+ if (completeNegative) {
380
+ hotel.tombstonedOfferVersionRefs = unique([...hotel.tombstonedOfferVersionRefs, offerVersionRef])
381
+ hotel.tombstonedOfferRefs = unique([...hotel.tombstonedOfferRefs, offerRef])
382
+ }
383
+ hotel.currentGeneration.valid = false; hotel.invalidatedOfferRefs = [...hotel.currentGeneration.orderedOfferRefs]; hotel.currentOfferRefs = []; hotel.freshOffersRequired = hotel.checksIssued < MAX_OFFER_CHECKS_PER_HOTEL && hotel.offerQueriesIssued < MAX_OFFER_QUERIES_PER_HOTEL
384
+ hotel.status = completeNegative ? 'negative' : 'inconclusive'; hotel.lastEvidence = completeNegative ? 'unavailable' : 'inconclusive'
385
+ const currentOrdinal = next.hotelRefs.indexOf(attempt.hotelRef)
386
+ const nextOrdinal = hotel.freshOffersRequired ? currentOrdinal : next.hotelRefs.findIndex((ref) => ['active', 'unvisited'].includes(next.hotels[ref]!.status))
387
+ if (nextOrdinal >= 0) next.activeHotelOrdinal = nextOrdinal
388
+ next.availabilityPhase = phaseForActiveHotel(next); return withTerminal(next)
389
+ }
390
+
391
+ /** Fold the availability portion of a receipt using the same pure reducer as
392
+ * continueWithReceipt and resumeTask. */
393
+ export function reduceAvailabilityReceipt(
394
+ state: AvailabilityPolicyState,
395
+ workspace: BookingWorkspaceSnapshot,
396
+ receipt: ActionReceipt,
397
+ action: AvailabilityActionLike,
398
+ ): AvailabilityPolicyState {
399
+ let next = recordVisibleHotels(state, workspace)
400
+ if (action.kind === 'offers.query' && !state.recoveryStarted) {
401
+ if (!isOfferCriteria(action.input.criteria)) throw new Error('availability_criteria_required')
402
+ next = recordObservedOffersQuery(next, action.input.hotelRefs, digest(action.input.criteria), workspace, receipt, action.actionId, digest(receipt), action.input.criteria)
403
+ } else if (action.kind === 'offers.query' && state.recoveryStarted) {
404
+ if (!isOfferCriteria(action.input.criteria)) throw new Error('availability_criteria_required')
405
+ next = recordOffersGeneration(next, action.input.hotelRefs, workspace, action.actionId, digest(receipt), receipt, state.criteriaDigest ?? '', action.input.criteria)
406
+ } else if (action.kind === 'offer.check') {
407
+ if (action.expectedRevision === undefined) throw new Error('availability_expected_revision_missing')
408
+ next = recordOfferCheckReceipt(next, workspace, receipt, action.actionId, action.input.offerRef, action.input.offerVersionRef, action.expectedRevision)
409
+ }
410
+ return next
411
+ }
412
+ export function availabilityPolicyIsTerminal(state: AvailabilityPolicyState): boolean { return Boolean(state.terminal) }
413
+ export function availabilityPolicyResult(state: AvailabilityPolicyState): AvailabilityExhaustion | undefined { return state.terminal ? { ...state.terminal, hotelRefs: [...state.terminal.hotelRefs] } : undefined }
414
+ export function validateAvailabilityPolicy(state: AvailabilityPolicyState): boolean {
415
+ try {
416
+ if (!state || typeof state.initialized !== 'boolean' || typeof state.recoveryStarted !== 'boolean') return false
417
+ if (state.recoveryId !== undefined && (typeof state.recoveryId !== 'string' || !state.recoveryId)) return false
418
+ if (state.criteriaDigest !== undefined && (typeof state.criteriaDigest !== 'string' || !/^[0-9a-f]{64}$/.test(state.criteriaDigest))) return false
419
+ if (state.candidateSetDigest !== undefined && (typeof state.candidateSetDigest !== 'string' || !/^[0-9a-f]{64}$/.test(state.candidateSetDigest))) return false
420
+ if (!['need_offers', 'waiting_offers', 'need_check', 'waiting_check', 'terminal'].includes(state.availabilityPhase)) return false
421
+ if (!Number.isSafeInteger(state.activeHotelOrdinal) || !Array.isArray(state.hotelRefs) || state.hotelRefs.length > MAX_HOTELS_PER_TASK) return false
422
+ if (state.hotelRefs.some((ref) => typeof ref !== 'string' || !ref) || new Set(state.hotelRefs).size !== state.hotelRefs.length) return false
423
+ if ((state.hotelRefs.length && (state.activeHotelOrdinal < 0 || state.activeHotelOrdinal >= state.hotelRefs.length)) || (!state.hotelRefs.length && state.activeHotelOrdinal !== 0)) return false
424
+ if (!state.hotels || typeof state.hotels !== 'object' || Object.keys(state.hotels).sort().join('\0') !== [...state.hotelRefs].sort().join('\0')) return false
425
+ if (!Array.isArray(state.attempts) || !Array.isArray(state.queryReservations)) return false
426
+ if (!state.recoveryStarted) {
427
+ if (state.recoveryId !== undefined || state.candidateSetDigest !== undefined || state.hotelRefs.length || Object.keys(state.hotels).length || state.attempts.length || state.queryReservations.length || state.terminal !== undefined) return false
428
+ if (state.criteria !== undefined || state.criteriaDigest !== undefined) {
429
+ if (!isOfferCriteria(state.criteria) || !state.criteriaDigest || state.criteriaDigest !== digest(state.criteria)) return false
430
+ }
431
+ if (state.lastQueryCriteriaDigest !== undefined && state.lastQueryCriteriaDigest !== state.criteriaDigest) return false
432
+ if (state.lastQueryHotelRefs !== undefined && (!Array.isArray(state.lastQueryHotelRefs) || new Set(state.lastQueryHotelRefs).size !== state.lastQueryHotelRefs.length || state.lastQueryHotelRefs.some((ref) => typeof ref !== 'string' || !ref))) return false
433
+ if (state.lastQueryOfferHotels !== undefined && (!state.lastQueryHotelRefs || Object.values(state.lastQueryOfferHotels).some((ref) => typeof ref !== 'string' || !state.lastQueryHotelRefs!.includes(ref)) || Object.keys(state.lastQueryOfferHotels).some((ref) => typeof ref !== 'string' || !ref))) return false
434
+ if (state.lastQuerySourceActionId !== undefined && (typeof state.lastQuerySourceActionId !== 'string' || !state.lastQuerySourceActionId || typeof state.lastQuerySourceReceiptDigest !== 'string' || !/^[0-9a-f]{64}$/.test(state.lastQuerySourceReceiptDigest) || !Number.isSafeInteger(state.lastQueryWorkspaceRevision) || state.lastQueryWorkspaceRevision! < 0)) return false
435
+ if (state.lastQuerySourceReceiptDigest !== undefined && state.lastQuerySourceActionId === undefined) return false
436
+ return state.initialized === false && state.availabilityPhase === 'need_offers'
437
+ }
438
+ if (!state.initialized || !state.recoveryId || !state.candidateSetDigest || state.candidateSetDigest !== digest(state.hotelRefs)) return false
439
+ const criteriaBound = isOfferCriteria(state.criteria) && Boolean(state.criteriaDigest) && state.criteriaDigest === digest(state.criteria)
440
+ const manualCheckWorkspaceSnapshot = state.attempts.length > 0 && state.attempts.every((attempt) => state.hotels[attempt.hotelRef]?.currentGeneration?.source.kind === 'workspace_snapshot')
441
+ const manualCheckMayBindCriteria = !state.criteria && !state.criteriaDigest && state.queryReservations.length === 0 && manualCheckWorkspaceSnapshot && (
442
+ ['waiting_check', 'need_offers'].includes(state.availabilityPhase)
443
+ || state.availabilityPhase === 'need_check'
444
+ || (state.availabilityPhase === 'terminal' && state.terminal?.code === 'availability_confirmed')
445
+ )
446
+ if (!criteriaBound && !manualCheckMayBindCriteria) return false
447
+ if (state.lastQueryCriteriaDigest !== undefined && state.lastQueryCriteriaDigest !== state.criteriaDigest) return false
448
+ if (state.lastQueryHotelRefs !== undefined && (!Array.isArray(state.lastQueryHotelRefs) || new Set(state.lastQueryHotelRefs).size !== state.lastQueryHotelRefs.length || state.lastQueryHotelRefs.some((ref) => typeof ref !== 'string' || !ref))) return false
449
+ if (state.lastQueryOfferHotels !== undefined && (!state.lastQueryHotelRefs || Object.values(state.lastQueryOfferHotels).some((ref) => typeof ref !== 'string' || !state.lastQueryHotelRefs!.includes(ref)) || Object.keys(state.lastQueryOfferHotels).some((ref) => typeof ref !== 'string' || !ref))) return false
450
+ if (state.lastQuerySourceActionId !== undefined && (typeof state.lastQuerySourceActionId !== 'string' || !state.lastQuerySourceActionId || typeof state.lastQuerySourceReceiptDigest !== 'string' || !/^[0-9a-f]{64}$/.test(state.lastQuerySourceReceiptDigest) || !Number.isSafeInteger(state.lastQueryWorkspaceRevision) || state.lastQueryWorkspaceRevision! < 0)) return false
451
+ if (state.lastQuerySourceReceiptDigest !== undefined && state.lastQuerySourceActionId === undefined) return false
452
+ if (state.hotelRefs.length === 0) return false
453
+ const validStatus = new Set<AvailabilityHotelStatus>(['unvisited', 'active', 'negative', 'confirmed', 'inconclusive'])
454
+ const validEvidence = new Set<AvailabilityHotelState['lastEvidence']>(['none', 'confirmed', 'unavailable', 'inconclusive'])
455
+ for (const ref of state.hotelRefs) {
456
+ const hotel = state.hotels[ref]
457
+ if (!hotel || hotel.hotelRef !== ref || !validStatus.has(hotel.status) || !validEvidence.has(hotel.lastEvidence)) return false
458
+ if (![hotel.checksIssued, hotel.offerQueriesIssued, hotel.generationNo, hotel.generation, hotel.checkCount].every(Number.isSafeInteger)) return false
459
+ if (hotel.checksIssued < 0 || hotel.checksIssued > MAX_OFFER_CHECKS_PER_HOTEL || hotel.offerQueriesIssued < 0 || hotel.offerQueriesIssued > MAX_OFFER_QUERIES_PER_HOTEL || hotel.generationNo < 0 || hotel.generation !== hotel.generationNo || hotel.checkCount !== hotel.checksIssued) return false
460
+ if (typeof hotel.freshOffersRequired !== 'boolean' || !Array.isArray(hotel.currentOfferRefs) || !Array.isArray(hotel.invalidatedOfferRefs) || !Array.isArray(hotel.tombstonedOfferRefs) || !Array.isArray(hotel.tombstonedOfferVersionRefs)) return false
461
+ if (hotel.currentOfferRefs.some((offer) => typeof offer !== 'string' || !offer) || hotel.invalidatedOfferRefs.some((offer) => typeof offer !== 'string' || !offer) || hotel.tombstonedOfferRefs.some((offer) => typeof offer !== 'string' || !offer) || hotel.tombstonedOfferVersionRefs.some((offer) => typeof offer !== 'string' || !offer)) return false
462
+ if (new Set(hotel.currentOfferRefs).size !== hotel.currentOfferRefs.length || new Set(hotel.invalidatedOfferRefs).size !== hotel.invalidatedOfferRefs.length || new Set(hotel.tombstonedOfferRefs).size !== hotel.tombstonedOfferRefs.length || new Set(hotel.tombstonedOfferVersionRefs).size !== hotel.tombstonedOfferVersionRefs.length || hotel.currentOfferRefs.length > MAX_OFFERS_PER_HOTEL_GENERATION || hotel.invalidatedOfferRefs.length > MAX_OFFERS_PER_HOTEL_GENERATION || hotel.currentOfferRefs.some((offer) => hotel.invalidatedOfferRefs.includes(offer) || hotel.tombstonedOfferRefs.includes(offer))) return false
463
+ const generation = hotel.currentGeneration
464
+ if (hotel.generationNo === 0) {
465
+ if (generation !== undefined || hotel.currentOfferRefs.length || hotel.invalidatedOfferRefs.length || hotel.tombstonedOfferRefs.length || hotel.tombstonedOfferVersionRefs.length || hotel.status !== 'unvisited' || hotel.lastEvidence !== 'none' || hotel.checksIssued !== 0 || hotel.offerQueriesIssued !== 0 || hotel.freshOffersRequired) return false
466
+ continue
467
+ }
468
+ if (!generation || generation.generationId !== `${ref}:generation:${hotel.generationNo}` || !/^[0-9a-f]{64}$/.test(generation.offerSetDigest) || generation.offerSetDigest !== digest(generation.orderedOfferRefs) || !Array.isArray(generation.orderedOfferRefs) || generation.orderedOfferRefs.length > MAX_OFFERS_PER_HOTEL_GENERATION || generation.orderedOfferRefs.some((offer) => typeof offer !== 'string' || !offer) || new Set(generation.orderedOfferRefs).size !== generation.orderedOfferRefs.length || !['complete', 'partial'].includes(generation.evidence) || typeof generation.valid !== 'boolean') return false
469
+ if (generation.offerSetDigest !== digest(generation.orderedOfferRefs) || hotel.currentOfferRefs.some((offer) => !generation.orderedOfferRefs.includes(offer)) || hotel.invalidatedOfferRefs.some((offer) => !generation.orderedOfferRefs.includes(offer))) return false
470
+ const source = generation.source
471
+ if (!source || !['query_receipt', 'workspace_snapshot'].includes(source.kind) || !Number.isSafeInteger(source.workspaceRevision) || source.workspaceRevision < 0) return false
472
+ if (source.kind === 'query_receipt' && (typeof source.actionId !== 'string' || !source.actionId || !/^[0-9a-f]{64}$/.test(source.receiptDigest))) return false
473
+ if (source.kind === 'workspace_snapshot' && (typeof source.workspaceDigest !== 'string' || !/^[0-9a-f]{64}$/.test(source.workspaceDigest))) return false
474
+ if (source.kind === 'query_receipt' && state.lastQuerySourceActionId === source.actionId && (state.lastQuerySourceReceiptDigest !== source.receiptDigest || state.lastQueryWorkspaceRevision !== source.workspaceRevision)) return false
475
+ if (hotel.freshOffersRequired && (hotel.currentOfferRefs.length !== 0 || hotel.checksIssued >= MAX_OFFER_CHECKS_PER_HOTEL || hotel.offerQueriesIssued >= MAX_OFFER_QUERIES_PER_HOTEL)) return false
476
+ if (hotel.status === 'unvisited' || hotel.status === 'negative') {
477
+ if (hotel.status === 'unvisited' || hotel.lastEvidence !== 'unavailable' || hotel.currentOfferRefs.length || generation.valid) return false
478
+ }
479
+ if (hotel.status === 'active' && (!hotel.currentOfferRefs.length || hotel.lastEvidence !== 'none' || hotel.freshOffersRequired)) return false
480
+ if (hotel.status === 'confirmed' && (hotel.lastEvidence !== 'confirmed' || !hotel.currentOfferRefs.length || generation.valid)) return false
481
+ if (hotel.status === 'inconclusive' && (hotel.lastEvidence !== 'inconclusive' || hotel.currentOfferRefs.length || generation.valid)) return false
482
+ }
483
+ const attempts = state.attempts
484
+ if (new Set(attempts.map((attempt) => attempt.actionId)).size !== attempts.length) return false
485
+ const attemptsByHotel = new Map<string, AvailabilityAttempt[]>()
486
+ for (const attempt of attempts) {
487
+ if (typeof attempt.actionId !== 'string' || !attempt.actionId || !state.hotels[attempt.hotelRef] || typeof attempt.offerRef !== 'string' || !attempt.offerRef || typeof attempt.offerVersionRef !== 'string' || !attempt.offerVersionRef || !Number.isSafeInteger(attempt.generation) || !Number.isSafeInteger(attempt.ordinal) || !Number.isSafeInteger(attempt.workspaceRevision) || attempt.workspaceRevision < 0) return false
488
+ const hotel = state.hotels[attempt.hotelRef]!
489
+ const generation = hotel.currentGeneration
490
+ if (!generation || attempt.generation < 1 || attempt.generation > hotel.generationNo || attempt.ordinal < 1 || attempt.ordinal > hotel.checksIssued || (attempt.generation === hotel.generationNo && !generation.orderedOfferRefs.includes(attempt.offerRef))) return false
491
+ const list = attemptsByHotel.get(attempt.hotelRef) ?? []; list.push(attempt); attemptsByHotel.set(attempt.hotelRef, list)
492
+ }
493
+ for (const [ref, attemptsForHotel] of attemptsByHotel) {
494
+ const ordinals = attemptsForHotel.map((attempt) => attempt.ordinal)
495
+ if (new Set(ordinals).size !== ordinals.length || attemptsForHotel.length !== state.hotels[ref]!.checksIssued || new Set(attemptsForHotel.map((attempt) => `${attempt.generation}:${attempt.offerRef}:${attempt.offerVersionRef}`)).size !== attemptsForHotel.length) return false
496
+ }
497
+ for (const ref of state.hotelRefs) if ((attemptsByHotel.get(ref)?.length ?? 0) !== state.hotels[ref]!.checksIssued) return false
498
+ if (new Set(state.queryReservations.map((reservation) => reservation.actionId)).size !== state.queryReservations.length) return false
499
+ const reservedHotels = new Set<string>()
500
+ for (const reservation of state.queryReservations) {
501
+ if (typeof reservation.actionId !== 'string' || !reservation.actionId || !Array.isArray(reservation.hotelRefs) || !reservation.hotelRefs.length || reservation.hotelRefs.length > MAX_HOTELS_PER_TASK || new Set(reservation.hotelRefs).size !== reservation.hotelRefs.length || reservation.hotelRefs.some((ref) => !state.hotels[ref]) || !Number.isSafeInteger(reservation.workspaceRevision) || reservation.workspaceRevision < 0 || reservation.hotelRefs.some((ref) => reservedHotels.has(ref))) return false
502
+ reservation.hotelRefs.forEach((ref) => reservedHotels.add(ref))
503
+ }
504
+ if (state.availabilityPhase === 'waiting_offers' && state.queryReservations.length === 0) return false
505
+ if (state.availabilityPhase !== 'waiting_offers' && state.queryReservations.length > 0) return false
506
+ if (state.availabilityPhase === 'waiting_check' && !attempts.length) return false
507
+ if (state.availabilityPhase === 'need_check') {
508
+ const active = state.hotels[state.hotelRefs[state.activeHotelOrdinal]!]!
509
+ if (active.status !== 'active' || !active.currentGeneration?.valid || !active.currentOfferRefs.length || active.freshOffersRequired) return false
510
+ }
511
+ if (state.availabilityPhase === 'terminal' && !state.terminal) return false
512
+ if (state.availabilityPhase !== 'terminal' && state.terminal) return false
513
+ const recomputedTerminal = terminalFor(state)
514
+ if (state.terminal) {
515
+ if (!recomputedTerminal || digest(state.terminal) !== digest(recomputedTerminal)) return false
516
+ } else if (recomputedTerminal && !['waiting_offers', 'waiting_check'].includes(state.availabilityPhase)) return false
517
+ return true
518
+ } catch {
519
+ return false
520
+ }
521
+ }
522
+ export function availabilityPolicyActionIsRelevant(action: BookingReadAction): boolean { return action.kind === 'offers.query' || action.kind === 'offer.check' }