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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -1,123 +1,48 @@
1
1
  import { createHash } from 'node:crypto';
2
+ /** Typed CheckAvail budget. This module never parses prose or retries a supplier. */
2
3
  export const MAX_HOTELS_PER_TASK = 5;
3
4
  export const MAX_OFFERS_PER_HOTEL_GENERATION = 3;
4
5
  export const MAX_OFFER_CHECKS_PER_HOTEL = 2;
5
6
  export const MAX_OFFER_QUERIES_PER_HOTEL = 2;
6
- function unique(values) {
7
- return [
8
- ...new Set(values)
9
- ];
10
- }
7
+ function unique(values) { return [...new Set(values)]; }
11
8
  export function assertWorkspaceLoadedOfferRefsUnique(workspace) {
12
- if (workspace.loadedOffers.length !== new Set(workspace.loadedOffers.map((offer)=>offer.offerRef)).size) throw new Error('availability_duplicate_offer_ref');
13
- if (workspace.loadedOffers.length !== new Set(workspace.loadedOffers.map((offer)=>offer.offerVersionRef)).size) throw new Error('availability_duplicate_offer_version_ref');
14
- }
15
- function stable(value) {
16
- if (Array.isArray(value)) return value.map(stable);
17
- if (!value || typeof value !== 'object') return value;
18
- return Object.fromEntries(Object.keys(value).sort().map((key)=>[
19
- key,
20
- stable(value[key])
21
- ]));
22
- }
23
- export function digest(value) {
24
- return createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
25
- }
26
- function workspaceDigest(workspace) {
27
- const { contextRef: _context, ...bound } = workspace;
28
- return digest(bound);
29
- }
9
+ if (workspace.loadedOffers.length !== new Set(workspace.loadedOffers.map((offer) => offer.offerRef)).size)
10
+ throw new Error('availability_duplicate_offer_ref');
11
+ if (workspace.loadedOffers.length !== new Set(workspace.loadedOffers.map((offer) => offer.offerVersionRef)).size)
12
+ throw new Error('availability_duplicate_offer_version_ref');
13
+ }
14
+ function stable(value) { if (Array.isArray(value))
15
+ return value.map(stable); if (!value || typeof value !== 'object')
16
+ return value; return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])); }
17
+ export function digest(value) { return createHash('sha256').update(JSON.stringify(stable(value))).digest('hex'); }
18
+ function workspaceDigest(workspace) { const { contextRef: _context, ...bound } = workspace; return digest(bound); }
30
19
  function offerRefsForHotel(workspace, hotelRef, selectedOfferRef) {
31
- const refs = unique(workspace.loadedOffers.filter((offer)=>offer.hotelRef === hotelRef).map((offer)=>offer.offerRef));
32
- if (selectedOfferRef) return unique([
33
- selectedOfferRef,
34
- ...refs
35
- ]).slice(0, MAX_OFFERS_PER_HOTEL_GENERATION);
36
- if (refs.length > MAX_OFFERS_PER_HOTEL_GENERATION) throw new Error('availability_offer_limit_exceeded');
20
+ const refs = unique(workspace.loadedOffers.filter((offer) => offer.hotelRef === hotelRef).map((offer) => offer.offerRef));
21
+ if (selectedOfferRef)
22
+ return unique([selectedOfferRef, ...refs]).slice(0, MAX_OFFERS_PER_HOTEL_GENERATION);
23
+ if (refs.length > MAX_OFFERS_PER_HOTEL_GENERATION)
24
+ throw new Error('availability_offer_limit_exceeded');
37
25
  return refs;
38
26
  }
39
- function loadedOfferVersion(workspace, offerRef, offerVersionRef) {
40
- return workspace.loadedOffers.find((offer)=>offer.offerRef === offerRef && offer.offerVersionRef === offerVersionRef);
41
- }
27
+ function loadedOfferVersion(workspace, offerRef, offerVersionRef) { return workspace.loadedOffers.find((offer) => offer.offerRef === offerRef && offer.offerVersionRef === offerVersionRef); }
42
28
  function legalOfferRefsForHotel(workspace, hotel, refs) {
43
- const legal = refs.filter((ref)=>{
44
- if (hotel.tombstonedOfferRefs.includes(ref)) return false;
45
- const loaded = workspace.loadedOffers.find((offer)=>offer.offerRef === ref && offer.hotelRef === hotel.hotelRef);
29
+ const legal = refs.filter((ref) => {
30
+ if (hotel.tombstonedOfferRefs.includes(ref))
31
+ return false;
32
+ const loaded = workspace.loadedOffers.find((offer) => offer.offerRef === ref && offer.hotelRef === hotel.hotelRef);
46
33
  return Boolean(loaded && !hotel.tombstonedOfferVersionRefs.includes(loaded.offerVersionRef));
47
34
  });
48
- if (legal.length > MAX_OFFERS_PER_HOTEL_GENERATION) throw new Error('availability_offer_limit_exceeded');
35
+ if (legal.length > MAX_OFFERS_PER_HOTEL_GENERATION)
36
+ throw new Error('availability_offer_limit_exceeded');
49
37
  return unique(legal);
50
38
  }
51
39
  function copyHotel(hotel) {
52
- return {
53
- ...hotel,
54
- currentOfferRefs: [
55
- ...hotel.currentOfferRefs
56
- ],
57
- invalidatedOfferRefs: [
58
- ...hotel.invalidatedOfferRefs
59
- ],
60
- tombstonedOfferRefs: [
61
- ...hotel.tombstonedOfferRefs
62
- ],
63
- tombstonedOfferVersionRefs: [
64
- ...hotel.tombstonedOfferVersionRefs
65
- ],
66
- ...hotel.currentGeneration ? {
67
- currentGeneration: {
68
- ...hotel.currentGeneration,
69
- orderedOfferRefs: [
70
- ...hotel.currentGeneration.orderedOfferRefs
71
- ]
72
- }
73
- } : {}
74
- };
40
+ return { ...hotel, currentOfferRefs: [...hotel.currentOfferRefs], invalidatedOfferRefs: [...hotel.invalidatedOfferRefs], tombstonedOfferRefs: [...hotel.tombstonedOfferRefs], tombstonedOfferVersionRefs: [...hotel.tombstonedOfferVersionRefs], ...(hotel.currentGeneration ? { currentGeneration: { ...hotel.currentGeneration, orderedOfferRefs: [...hotel.currentGeneration.orderedOfferRefs] } } : {}) };
75
41
  }
76
42
  function copyState(state) {
77
- return {
78
- ...state,
79
- ...state.criteria ? {
80
- criteria: structuredClone(state.criteria)
81
- } : {},
82
- hotelRefs: [
83
- ...state.hotelRefs
84
- ],
85
- hotels: Object.fromEntries(Object.entries(state.hotels).map(([ref, hotel])=>[
86
- ref,
87
- copyHotel(hotel)
88
- ])),
89
- attempts: state.attempts.map((attempt)=>({
90
- ...attempt
91
- })),
92
- queryReservations: state.queryReservations.map((query)=>({
93
- ...query,
94
- hotelRefs: [
95
- ...query.hotelRefs
96
- ]
97
- })),
98
- ...state.lastQueryHotelRefs ? {
99
- lastQueryHotelRefs: [
100
- ...state.lastQueryHotelRefs
101
- ]
102
- } : {},
103
- ...state.lastQueryOfferHotels ? {
104
- lastQueryOfferHotels: {
105
- ...state.lastQueryOfferHotels
106
- }
107
- } : {},
108
- ...state.terminal ? {
109
- terminal: {
110
- ...state.terminal,
111
- hotelRefs: [
112
- ...state.terminal.hotelRefs
113
- ]
114
- }
115
- } : {}
116
- };
117
- }
118
- function isPlainRecord(value) {
119
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
43
+ 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] } } : {}) };
120
44
  }
45
+ function isPlainRecord(value) { return Boolean(value && typeof value === 'object' && !Array.isArray(value)); }
121
46
  function isCriterion(value, valueCheck) {
122
47
  return isPlainRecord(value) && Object.keys(value).length === 2 && (value.strength === 'must' || value.strength === 'prefer') && valueCheck(value.value);
123
48
  }
@@ -125,249 +50,242 @@ function isMoney(value) {
125
50
  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);
126
51
  }
127
52
  function isOfferCriteria(value) {
128
- if (!isPlainRecord(value)) return false;
129
- const allowed = [
130
- 'roomType',
131
- 'bedType',
132
- 'meals',
133
- 'freeCancellation',
134
- 'freeCancellationUntil',
135
- 'totalPriceMax',
136
- 'roomsAvailableMin',
137
- 'payAtProperty',
138
- 'mobileRate',
139
- 'targetCount',
140
- 'sort'
141
- ];
142
- if (Object.keys(value).some((key)=>!allowed.includes(key))) return false;
143
- const strings = (candidate)=>Array.isArray(candidate) && candidate.every((item)=>typeof item === 'string' && Boolean(item));
144
- if (value.roomType !== undefined && !isCriterion(value.roomType, strings)) return false;
145
- if (value.bedType !== undefined && !isCriterion(value.bedType, strings)) return false;
146
- if (value.meals !== undefined && !isCriterion(value.meals, strings)) return false;
147
- if (value.freeCancellation !== undefined && !isCriterion(value.freeCancellation, (candidate)=>typeof candidate === 'boolean')) return false;
148
- if (value.freeCancellationUntil !== undefined && !isCriterion(value.freeCancellationUntil, (candidate)=>typeof candidate === 'string' && Boolean(candidate))) return false;
149
- if (value.totalPriceMax !== undefined && !isCriterion(value.totalPriceMax, isMoney)) return false;
150
- if (value.roomsAvailableMin !== undefined && !isCriterion(value.roomsAvailableMin, (candidate)=>Number.isSafeInteger(candidate) && candidate >= 0)) return false;
151
- if (value.payAtProperty !== undefined && !isCriterion(value.payAtProperty, (candidate)=>typeof candidate === 'boolean')) return false;
152
- if (value.mobileRate !== undefined && !isCriterion(value.mobileRate, (candidate)=>typeof candidate === 'boolean')) return false;
153
- if (value.targetCount !== undefined && (!Number.isSafeInteger(value.targetCount) || value.targetCount < 0)) return false;
154
- if (value.sort !== undefined && ![
155
- 'best_match',
156
- 'total_price_asc',
157
- 'cancellation_latest'
158
- ].includes(value.sort)) return false;
53
+ if (!isPlainRecord(value))
54
+ return false;
55
+ const allowed = ['roomType', 'bedType', 'meals', 'freeCancellation', 'freeCancellationUntil', 'totalPriceMax', 'roomsAvailableMin', 'payAtProperty', 'mobileRate', 'targetCount', 'sort'];
56
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
57
+ return false;
58
+ const strings = (candidate) => Array.isArray(candidate) && candidate.every((item) => typeof item === 'string' && Boolean(item));
59
+ if (value.roomType !== undefined && !isCriterion(value.roomType, strings))
60
+ return false;
61
+ if (value.bedType !== undefined && !isCriterion(value.bedType, strings))
62
+ return false;
63
+ if (value.meals !== undefined && !isCriterion(value.meals, strings))
64
+ return false;
65
+ if (value.freeCancellation !== undefined && !isCriterion(value.freeCancellation, (candidate) => typeof candidate === 'boolean'))
66
+ return false;
67
+ if (value.freeCancellationUntil !== undefined && !isCriterion(value.freeCancellationUntil, (candidate) => typeof candidate === 'string' && Boolean(candidate)))
68
+ return false;
69
+ if (value.totalPriceMax !== undefined && !isCriterion(value.totalPriceMax, isMoney))
70
+ return false;
71
+ if (value.roomsAvailableMin !== undefined && !isCriterion(value.roomsAvailableMin, (candidate) => Number.isSafeInteger(candidate) && candidate >= 0))
72
+ return false;
73
+ if (value.payAtProperty !== undefined && !isCriterion(value.payAtProperty, (candidate) => typeof candidate === 'boolean'))
74
+ return false;
75
+ if (value.mobileRate !== undefined && !isCriterion(value.mobileRate, (candidate) => typeof candidate === 'boolean'))
76
+ return false;
77
+ if (value.targetCount !== undefined && (!Number.isSafeInteger(value.targetCount) || value.targetCount < 0))
78
+ return false;
79
+ if (value.sort !== undefined && !['best_match', 'total_price_asc', 'cancellation_latest'].includes(value.sort))
80
+ return false;
159
81
  return true;
160
82
  }
161
83
  function bindCriteria(next, criteriaDigest, criteria, required = false) {
162
84
  if (criteria !== undefined) {
163
- if (!isOfferCriteria(criteria) || digest(criteria) !== criteriaDigest) throw new Error('availability_criteria_invalid');
164
- if (next.criteriaDigest !== undefined && next.criteriaDigest !== criteriaDigest) throw new Error('availability_criteria_changed');
165
- if (next.criteria && digest(next.criteria) !== criteriaDigest) throw new Error('availability_criteria_changed');
85
+ if (!isOfferCriteria(criteria) || digest(criteria) !== criteriaDigest)
86
+ throw new Error('availability_criteria_invalid');
87
+ if (next.criteriaDigest !== undefined && next.criteriaDigest !== criteriaDigest)
88
+ throw new Error('availability_criteria_changed');
89
+ if (next.criteria && digest(next.criteria) !== criteriaDigest)
90
+ throw new Error('availability_criteria_changed');
166
91
  next.criteria = structuredClone(criteria);
167
92
  next.criteriaDigest = criteriaDigest;
168
- } else if (required && !next.criteria) throw new Error('availability_criteria_required');
169
- else if (next.criteria && next.criteriaDigest !== digest(next.criteria)) throw new Error('availability_criteria_mismatch');
93
+ }
94
+ else if (required && !next.criteria)
95
+ throw new Error('availability_criteria_required');
96
+ else if (next.criteria && next.criteriaDigest !== digest(next.criteria))
97
+ throw new Error('availability_criteria_mismatch');
170
98
  else if (!next.criteria && criteriaDigest) {
171
- if (next.criteriaDigest !== undefined && next.criteriaDigest !== criteriaDigest) throw new Error('availability_criteria_changed');
99
+ if (next.criteriaDigest !== undefined && next.criteriaDigest !== criteriaDigest)
100
+ throw new Error('availability_criteria_changed');
172
101
  next.criteriaDigest = criteriaDigest;
173
102
  }
174
- if (next.criteriaDigest !== undefined && next.criteriaDigest !== criteriaDigest && criteriaDigest) throw new Error('availability_criteria_changed');
103
+ if (next.criteriaDigest !== undefined && next.criteriaDigest !== criteriaDigest && criteriaDigest)
104
+ throw new Error('availability_criteria_changed');
175
105
  }
176
106
  function terminalFor(state) {
177
- if (!state.recoveryStarted || !state.hotelRefs.length) return undefined;
178
- const hotels = state.hotelRefs.map((ref)=>state.hotels[ref]);
179
- if (hotels.some((hotel)=>hotel.status === 'confirmed')) return {
180
- code: 'availability_confirmed',
181
- hotelRefs: [
182
- ...state.hotelRefs
183
- ],
184
- reason: 'confirmed',
185
- evidence: 'conclusive'
186
- };
187
- if (hotels.some((hotel)=>hotel.checksIssued < MAX_OFFER_CHECKS_PER_HOTEL && (hotel.status === 'active' || hotel.status === 'unvisited' || hotel.freshOffersRequired && hotel.offerQueriesIssued < MAX_OFFER_QUERIES_PER_HOTEL))) return undefined;
188
- const conclusive = hotels.every((hotel)=>hotel.status === 'negative');
189
- return {
190
- code: conclusive ? 'availability_exhausted_complete' : 'availability_exhausted_inconclusive',
191
- hotelRefs: [
192
- ...state.hotelRefs
193
- ],
194
- reason: hotels.every((hotel)=>!hotel.currentOfferRefs.length) ? 'no_current_offers' : 'check_limit_reached',
195
- evidence: conclusive ? 'conclusive' : 'inconclusive'
196
- };
197
- }
198
- function withTerminal(state) {
199
- const terminal = terminalFor(state);
200
- return terminal ? {
201
- ...state,
202
- terminal,
203
- availabilityPhase: 'terminal'
204
- } : state;
205
- }
206
- function newHotel(hotelRef) {
207
- return {
208
- hotelRef,
209
- status: 'unvisited',
210
- checksIssued: 0,
211
- offerQueriesIssued: 0,
212
- generationNo: 0,
213
- generation: 0,
214
- currentOfferRefs: [],
215
- invalidatedOfferRefs: [],
216
- checkCount: 0,
217
- freshOffersRequired: false,
218
- tombstonedOfferRefs: [],
219
- tombstonedOfferVersionRefs: [],
220
- lastEvidence: 'none'
221
- };
222
- }
107
+ if (!state.recoveryStarted || !state.hotelRefs.length)
108
+ return undefined;
109
+ const hotels = state.hotelRefs.map((ref) => state.hotels[ref]);
110
+ if (hotels.some((hotel) => hotel.status === 'confirmed'))
111
+ return { code: 'availability_confirmed', hotelRefs: [...state.hotelRefs], reason: 'confirmed', evidence: 'conclusive' };
112
+ // A negative result is conclusive only when it has no legal refresh left.
113
+ // The first manual check starts with a workspace generation (zero recovery
114
+ // queries), so it must leave a fresh-query transition available.
115
+ if (hotels.some((hotel) => hotel.checksIssued < MAX_OFFER_CHECKS_PER_HOTEL
116
+ && (hotel.status === 'active'
117
+ || hotel.status === 'unvisited'
118
+ || (hotel.freshOffersRequired && hotel.offerQueriesIssued < MAX_OFFER_QUERIES_PER_HOTEL))))
119
+ return undefined;
120
+ const conclusive = hotels.every((hotel) => hotel.status === 'negative');
121
+ 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' };
122
+ }
123
+ function withTerminal(state) { const terminal = terminalFor(state); return terminal ? { ...state, terminal, availabilityPhase: 'terminal' } : state; }
124
+ function newHotel(hotelRef) { return { hotelRef, status: 'unvisited', checksIssued: 0, offerQueriesIssued: 0, generationNo: 0, generation: 0, currentOfferRefs: [], invalidatedOfferRefs: [], checkCount: 0, freshOffersRequired: false, tombstonedOfferRefs: [], tombstonedOfferVersionRefs: [], lastEvidence: 'none' }; }
223
125
  function phaseForActiveHotel(state) {
224
126
  const active = state.hotels[state.hotelRefs[state.activeHotelOrdinal] ?? ''];
225
- return active?.status === 'active' && active.currentGeneration?.valid && active.currentOfferRefs.length > 0 && !active.freshOffersRequired ? 'need_check' : 'need_offers';
226
- }
227
- export function createAvailabilityPolicy(workspace) {
228
- assertWorkspaceLoadedOfferRefsUnique(workspace);
229
- return {
230
- initialized: false,
231
- recoveryStarted: false,
232
- availabilityPhase: 'need_offers',
233
- activeHotelOrdinal: 0,
234
- hotelRefs: [],
235
- hotels: {},
236
- attempts: [],
237
- queryReservations: []
238
- };
239
- }
240
- export function recordVisibleHotels(state, _workspace) {
241
- return copyState(state);
242
- }
127
+ return active?.status === 'active' && active.currentGeneration?.valid && active.currentOfferRefs.length > 0 && !active.freshOffersRequired
128
+ ? 'need_check'
129
+ : 'need_offers';
130
+ }
131
+ /** Initial workspace is only a hint; the bounded recovery starts at typed offers.query. */
132
+ export function createAvailabilityPolicy(workspace) { assertWorkspaceLoadedOfferRefsUnique(workspace); return { initialized: false, recoveryStarted: false, availabilityPhase: 'need_offers', activeHotelOrdinal: 0, hotelRefs: [], hotels: {}, attempts: [], queryReservations: [] }; }
133
+ /** Ordinary search results never get silently truncated into recovery candidates. */
134
+ export function recordVisibleHotels(state, _workspace) { return copyState(state); }
135
+ /** Observe a normal offers query without opening an availability recovery. */
243
136
  export function recordObservedOffersQuery(state, hotelRefs, criteriaDigest, workspace, receipt, sourceActionId, receiptDigest, criteria) {
244
- if (workspace) assertWorkspaceLoadedOfferRefsUnique(workspace);
245
- if (state.recoveryStarted) return copyState(state);
137
+ if (workspace)
138
+ assertWorkspaceLoadedOfferRefsUnique(workspace);
139
+ if (state.recoveryStarted)
140
+ return copyState(state);
246
141
  const next = copyState(state);
247
- if (!workspace || !receipt) return next;
142
+ if (!workspace || !receipt)
143
+ return next;
248
144
  delete next.lastQueryHotelRefs;
249
145
  delete next.lastQueryCriteriaDigest;
250
146
  delete next.lastQueryOfferHotels;
251
147
  delete next.lastQuerySourceActionId;
252
148
  delete next.lastQuerySourceReceiptDigest;
253
149
  delete next.lastQueryWorkspaceRevision;
254
- const completeReceipt = receipt?.status === 'applied' && receipt.resultContract.outcome === 'complete' && receipt.resultContract.hardCriteriaMet && !receipt.resultContract.gapCodes.length && !receipt.resultContract.blockers.length && receipt.observation.kind === 'offers.state' && !receipt.observation.gapCodes?.length;
150
+ const completeReceipt = receipt?.status === 'applied'
151
+ && receipt.resultContract.outcome === 'complete'
152
+ && receipt.resultContract.hardCriteriaMet
153
+ && !receipt.resultContract.gapCodes.length
154
+ && !receipt.resultContract.blockers.length
155
+ && receipt.observation.kind === 'offers.state'
156
+ && !receipt.observation.gapCodes?.length;
255
157
  if (completeReceipt && receipt.observation.kind === 'offers.state') {
256
- if (!criteria) throw new Error('availability_criteria_required');
158
+ if (!criteria)
159
+ 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.
257
162
  delete next.criteria;
258
163
  delete next.criteriaDigest;
259
164
  bindCriteria(next, criteriaDigest, criteria);
260
- if (!sourceActionId || sourceActionId !== receipt.actionId || !receiptDigest || receiptDigest !== digest(receipt)) throw new Error('availability_query_provenance_mismatch');
261
- next.lastQueryHotelRefs = [
262
- ...new Set(hotelRefs)
263
- ];
165
+ if (!sourceActionId || sourceActionId !== receipt.actionId || !receiptDigest || receiptDigest !== digest(receipt))
166
+ throw new Error('availability_query_provenance_mismatch');
167
+ next.lastQueryHotelRefs = [...new Set(hotelRefs)];
264
168
  next.lastQueryCriteriaDigest = criteriaDigest || undefined;
265
169
  next.lastQueryOfferHotels = validateOffersReceiptWorkspace(receipt, workspace, hotelRefs);
266
170
  next.lastQuerySourceActionId = sourceActionId;
267
171
  next.lastQuerySourceReceiptDigest = receiptDigest;
268
172
  next.lastQueryWorkspaceRevision = workspace.revision;
269
- } else {
173
+ }
174
+ else {
175
+ // The latest ordinary query is the only candidate provenance. A failed
176
+ // or partial query must not leave an older criteria object usable for a
177
+ // later manual recovery.
270
178
  delete next.criteria;
271
179
  delete next.criteriaDigest;
272
180
  }
273
181
  return next;
274
182
  }
183
+ /**
184
+ * Reconcile an offers receipt against the post-action workspace before its
185
+ * provenance can influence recovery. Complete receipts must describe exactly
186
+ * the authoritative loaded offers; partial/gap receipts may only describe an
187
+ * evidenced subset. An unreported offer is never silently admitted.
188
+ */
275
189
  export function validateOffersReceiptWorkspace(receipt, workspace, hotelRefs) {
276
190
  assertWorkspaceLoadedOfferRefsUnique(workspace);
277
- if (receipt.observation.kind !== 'offers.state') throw new Error('availability_offers_observation_required');
191
+ if (receipt.observation.kind !== 'offers.state')
192
+ throw new Error('availability_offers_observation_required');
278
193
  const requested = unique(hotelRefs);
279
- if (!sameSet(receipt.observation.hotelRefs, requested)) throw new Error('availability_offers_hotel_mismatch');
280
- const authoritative = workspace.loadedOffers.filter((offer)=>requested.includes(offer.hotelRef));
281
- const authoritativeRefs = unique(authoritative.map((offer)=>offer.offerRef));
194
+ if (!sameSet(receipt.observation.hotelRefs, requested))
195
+ throw new Error('availability_offers_hotel_mismatch');
196
+ const authoritative = workspace.loadedOffers.filter((offer) => requested.includes(offer.hotelRef));
197
+ const authoritativeRefs = unique(authoritative.map((offer) => offer.offerRef));
282
198
  const observedRefs = unique(receipt.observation.offerRefs);
283
- if (observedRefs.some((offerRef)=>!authoritativeRefs.includes(offerRef))) throw new Error('availability_offers_unreported');
284
- const authoritativeHotels = new Set(authoritative.map((offer)=>offer.hotelRef)).size;
285
- const observedOfferHotels = Object.fromEntries(observedRefs.map((offerRef)=>[
286
- offerRef,
287
- authoritative.find((offer)=>offer.offerRef === offerRef).hotelRef
288
- ]));
199
+ if (observedRefs.some((offerRef) => !authoritativeRefs.includes(offerRef)))
200
+ throw new Error('availability_offers_unreported');
201
+ const authoritativeHotels = new Set(authoritative.map((offer) => offer.hotelRef)).size;
202
+ const observedOfferHotels = Object.fromEntries(observedRefs.map((offerRef) => [offerRef, authoritative.find((offer) => offer.offerRef === offerRef).hotelRef]));
289
203
  const observedHotels = new Set(Object.values(observedOfferHotels)).size;
290
204
  const clean = !receipt.resultContract.gapCodes.length && !receipt.resultContract.blockers.length && !receipt.observation.gapCodes?.length;
291
- 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');
205
+ if ((receipt.status === 'no_match' || receipt.status === 'unavailable') && (receipt.resultContract.outcome !== 'empty' || observedRefs.length !== 0 || receipt.observation.loadedHotelCount !== 0))
206
+ throw new Error('availability_offers_receipt_incoherent');
292
207
  const complete = receipt.status === 'applied' && receipt.resultContract.outcome === 'complete' && receipt.resultContract.hardCriteriaMet && clean;
293
- const exactEmpty = clean && receipt.resultContract.outcome === 'empty' && ![
294
- 'changed',
295
- 'partial',
296
- 'stale',
297
- 'failed',
298
- 'unsupported'
299
- ].includes(receipt.status);
300
- if ((complete || exactEmpty) && (!sameSet(observedRefs, authoritativeRefs) || receipt.observation.loadedHotelCount !== authoritativeHotels)) throw new Error('availability_offers_workspace_mismatch');
301
- if (receipt.observation.loadedHotelCount !== observedHotels) throw new Error('availability_offers_count_mismatch');
208
+ const exactEmpty = clean && receipt.resultContract.outcome === 'empty' && !['changed', 'partial', 'stale', 'failed', 'unsupported'].includes(receipt.status);
209
+ if ((complete || exactEmpty) && (!sameSet(observedRefs, authoritativeRefs) || receipt.observation.loadedHotelCount !== authoritativeHotels))
210
+ throw new Error('availability_offers_workspace_mismatch');
211
+ if (receipt.observation.loadedHotelCount !== observedHotels)
212
+ throw new Error('availability_offers_count_mismatch');
302
213
  return observedOfferHotels;
303
214
  }
304
- function sameSet(a, b) {
305
- return a.length === b.length && unique(a).sort().every((value, index)=>value === unique(b).sort()[index]);
306
- }
215
+ function sameSet(a, b) { return a.length === b.length && unique(a).sort().every((value, index) => value === unique(b).sort()[index]); }
216
+ /** A typed offers.query receipt starts/advances a bounded candidate recovery. */
307
217
  export function recordOffersQueryIssued(state, hotelRefs, workspace, actionId, criteriaDigest = '', criteria) {
308
218
  assertWorkspaceLoadedOfferRefsUnique(workspace);
309
219
  const next = copyState(state);
310
220
  const requested = unique(hotelRefs);
311
- if (!requested.length || requested.length > MAX_HOTELS_PER_TASK) throw new Error('availability_hotel_limit_exceeded');
312
- if (!state.recoveryStarted) throw new Error('availability_recovery_not_started');
221
+ if (!requested.length || requested.length > MAX_HOTELS_PER_TASK)
222
+ throw new Error('availability_hotel_limit_exceeded');
223
+ if (!state.recoveryStarted)
224
+ throw new Error('availability_recovery_not_started');
313
225
  if (state.terminal) {
314
- if (requested.some((ref)=>state.hotels[ref]?.offerQueriesIssued >= MAX_OFFER_QUERIES_PER_HOTEL)) throw new Error('availability_offer_query_limit_reached');
226
+ if (requested.some((ref) => state.hotels[ref]?.offerQueriesIssued >= MAX_OFFER_QUERIES_PER_HOTEL))
227
+ throw new Error('availability_offer_query_limit_reached');
315
228
  throw new Error('availability_terminal');
316
229
  }
317
- if (requested.some((ref)=>!next.hotels[ref])) throw new Error('availability_hotel_unknown');
318
- 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');
230
+ if (requested.some((ref) => !next.hotels[ref]))
231
+ throw new Error('availability_hotel_unknown');
232
+ else if ((requested.length !== 1 || next.hotelRefs[next.activeHotelOrdinal] !== requested[0]) && (next.attempts.length > 0 || Object.values(next.hotels).some((hotel) => hotel.generationNo > 0)))
233
+ throw new Error('availability_hotel_not_active');
319
234
  bindCriteria(next, criteriaDigest, criteria);
320
- const existingReservation = next.queryReservations.find((reservation)=>reservation.actionId === actionId);
235
+ const existingReservation = next.queryReservations.find((reservation) => reservation.actionId === actionId);
321
236
  if (existingReservation) {
322
- if (!sameSet(existingReservation.hotelRefs, requested) || existingReservation.workspaceRevision !== workspace.revision) throw new Error('availability_query_action_conflict');
237
+ if (!sameSet(existingReservation.hotelRefs, requested) || existingReservation.workspaceRevision !== workspace.revision)
238
+ throw new Error('availability_query_action_conflict');
323
239
  return next;
324
240
  }
325
- for (const hotelRef of requested){
241
+ for (const hotelRef of requested) {
326
242
  const hotel = next.hotels[hotelRef];
327
- if (hotel.offerQueriesIssued >= MAX_OFFER_QUERIES_PER_HOTEL) throw new Error('availability_offer_query_limit_reached');
243
+ if (hotel.offerQueriesIssued >= MAX_OFFER_QUERIES_PER_HOTEL)
244
+ throw new Error('availability_offer_query_limit_reached');
328
245
  hotel.offerQueriesIssued += 1;
329
246
  }
330
- next.queryReservations.push({
331
- actionId,
332
- hotelRefs: [
333
- ...requested
334
- ],
335
- workspaceRevision: workspace.revision
336
- });
247
+ next.queryReservations.push({ actionId, hotelRefs: [...requested], workspaceRevision: workspace.revision });
337
248
  next.availabilityPhase = 'waiting_offers';
338
249
  next.initialized = true;
339
250
  return next;
340
251
  }
341
252
  export function recordOffersGeneration(state, hotelRefs, workspace, actionId, receiptDigest, receipt, criteriaDigest = '', criteria) {
342
253
  assertWorkspaceLoadedOfferRefsUnique(workspace);
343
- if (!state.recoveryStarted) return copyState(state);
344
- if (state.terminal) throw new Error('availability_terminal');
254
+ if (!state.recoveryStarted)
255
+ return copyState(state);
256
+ if (state.terminal)
257
+ throw new Error('availability_terminal');
345
258
  const next = copyState(state);
346
259
  const requested = unique(hotelRefs);
347
- if (!requested.length || requested.length > MAX_HOTELS_PER_TASK) throw new Error('availability_hotel_limit_exceeded');
348
- const reservation = next.queryReservations.find((query)=>query.actionId === actionId);
349
- if (!reservation || !sameSet(reservation.hotelRefs, requested)) throw new Error('availability_query_not_reserved');
350
- if (receipt.actionId !== actionId) throw new Error('availability_query_receipt_mismatch');
351
- if (receiptDigest !== digest(receipt)) throw new Error('availability_receipt_digest_mismatch');
352
- if (requested.some((ref)=>!next.hotels[ref])) throw new Error('availability_hotel_unknown');
353
- 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');
260
+ if (!requested.length || requested.length > MAX_HOTELS_PER_TASK)
261
+ throw new Error('availability_hotel_limit_exceeded');
262
+ const reservation = next.queryReservations.find((query) => query.actionId === actionId);
263
+ if (!reservation || !sameSet(reservation.hotelRefs, requested))
264
+ throw new Error('availability_query_not_reserved');
265
+ if (receipt.actionId !== actionId)
266
+ throw new Error('availability_query_receipt_mismatch');
267
+ if (receiptDigest !== digest(receipt))
268
+ throw new Error('availability_receipt_digest_mismatch');
269
+ if (requested.some((ref) => !next.hotels[ref]))
270
+ throw new Error('availability_hotel_unknown');
271
+ else if ((requested.length !== 1 || next.hotelRefs[next.activeHotelOrdinal] !== requested[0]) && (next.attempts.length > 0 || Object.values(next.hotels).some((hotel) => hotel.generationNo > 0)))
272
+ throw new Error('availability_hotel_not_active');
354
273
  bindCriteria(next, criteriaDigest, criteria);
355
- if (receipt.observation.kind === 'offers.state') validateOffersReceiptWorkspace(receipt, workspace, requested);
356
- for (const hotelRef of requested){
274
+ if (receipt.observation.kind === 'offers.state')
275
+ validateOffersReceiptWorkspace(receipt, workspace, requested);
276
+ for (const hotelRef of requested) {
357
277
  const hotel = next.hotels[hotelRef];
358
- 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);
359
- const candidateRefs = partial ? receipt.observation.kind === 'offers.state' ? receipt.observation.offerRefs.filter((ref)=>workspace.loadedOffers.some((offer)=>offer.offerRef === ref && offer.hotelRef === hotelRef)) : [] : offerRefsForHotel(workspace, hotelRef);
278
+ 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));
279
+ const candidateRefs = partial
280
+ ? receipt.observation.kind === 'offers.state'
281
+ ? receipt.observation.offerRefs.filter((ref) => workspace.loadedOffers.some((offer) => offer.offerRef === ref && offer.hotelRef === hotelRef))
282
+ : []
283
+ : offerRefsForHotel(workspace, hotelRef);
360
284
  const refs = legalOfferRefsForHotel(workspace, hotel, candidateRefs);
361
- const unusable = [
362
- 'stale',
363
- 'failed',
364
- 'unsupported'
365
- ].includes(receipt.status);
285
+ const unusable = ['stale', 'failed', 'unsupported'].includes(receipt.status);
366
286
  if (unusable) {
367
287
  hotel.currentOfferRefs = [];
368
- hotel.invalidatedOfferRefs = [
369
- ...hotel.currentGeneration?.orderedOfferRefs ?? []
370
- ];
288
+ hotel.invalidatedOfferRefs = [...(hotel.currentGeneration?.orderedOfferRefs ?? [])];
371
289
  hotel.freshOffersRequired = hotel.checksIssued < MAX_OFFER_CHECKS_PER_HOTEL && hotel.offerQueriesIssued < MAX_OFFER_QUERIES_PER_HOTEL;
372
290
  hotel.status = 'inconclusive';
373
291
  hotel.lastEvidence = 'inconclusive';
@@ -382,31 +300,18 @@ export function recordOffersGeneration(state, hotelRefs, workspace, actionId, re
382
300
  hotel.freshOffersRequired = (partial || noLegalRefsNeedsRefresh) && hotel.checksIssued < MAX_OFFER_CHECKS_PER_HOTEL && hotel.offerQueriesIssued < MAX_OFFER_QUERIES_PER_HOTEL;
383
301
  hotel.status = partial || noLegalRefsNeedsRefresh ? 'inconclusive' : refs.length ? 'active' : 'negative';
384
302
  hotel.lastEvidence = partial || noLegalRefsNeedsRefresh ? 'inconclusive' : refs.length ? 'none' : 'unavailable';
385
- hotel.currentGeneration = {
386
- generationId: `${hotelRef}:generation:${hotel.generationNo}`,
387
- source: {
388
- kind: 'query_receipt',
389
- actionId,
390
- receiptDigest,
391
- workspaceRevision: workspace.revision
392
- },
393
- offerSetDigest: digest(refs),
394
- orderedOfferRefs: refs,
395
- evidence: partial ? 'partial' : 'complete',
396
- valid: refs.length > 0 && !partial
397
- };
303
+ 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 };
398
304
  }
399
305
  const firstRequested = next.hotelRefs.indexOf(requested[0]);
400
- if (firstRequested >= 0) next.activeHotelOrdinal = firstRequested;
306
+ if (firstRequested >= 0)
307
+ next.activeHotelOrdinal = firstRequested;
401
308
  const queried = next.hotels[requested[0]];
402
309
  if (!queried.currentOfferRefs.length && !queried.freshOffersRequired) {
403
- const fallback = next.hotelRefs.findIndex((ref)=>[
404
- 'active',
405
- 'unvisited'
406
- ].includes(next.hotels[ref].status));
407
- if (fallback >= 0) next.activeHotelOrdinal = fallback;
310
+ const fallback = next.hotelRefs.findIndex((ref) => ['active', 'unvisited'].includes(next.hotels[ref].status));
311
+ if (fallback >= 0)
312
+ next.activeHotelOrdinal = fallback;
408
313
  }
409
- next.queryReservations = next.queryReservations.filter((query)=>query.actionId !== actionId);
314
+ next.queryReservations = next.queryReservations.filter((query) => query.actionId !== actionId);
410
315
  next.initialized = true;
411
316
  next.availabilityPhase = phaseForActiveHotel(next);
412
317
  delete next.terminal;
@@ -414,195 +319,153 @@ export function recordOffersGeneration(state, hotelRefs, workspace, actionId, re
414
319
  }
415
320
  export function canIssueOfferCheck(state, workspace, offerRef, offerVersionRef) {
416
321
  assertWorkspaceLoadedOfferRefsUnique(workspace);
322
+ // Availability recovery is opt-in at the typed offers.query seam. Existing
323
+ // v2 read flows retain their pre-recovery offer.check behavior.
417
324
  if (!state.recoveryStarted) {
418
325
  const loaded = loadedOfferVersion(workspace, offerRef, offerVersionRef);
419
- return loaded ? {
420
- ok: true,
421
- hotelRef: loaded.hotelRef,
422
- offerRef,
423
- offerVersionRef,
424
- checkCount: 0,
425
- generation: 0
426
- } : {
427
- ok: false,
428
- code: 'offer_not_loaded'
429
- };
326
+ return loaded
327
+ ? { ok: true, hotelRef: loaded.hotelRef, offerRef, offerVersionRef, checkCount: 0, generation: 0 }
328
+ : { ok: false, code: 'offer_not_loaded' };
430
329
  }
431
- if (state.terminal) return {
432
- ok: false,
433
- code: 'availability_exhausted'
434
- };
330
+ if (state.terminal)
331
+ return { ok: false, code: 'availability_exhausted' };
435
332
  const loaded = loadedOfferVersion(workspace, offerRef, offerVersionRef);
436
- if (!loaded) return {
437
- ok: false,
438
- code: 'offer_not_loaded'
439
- };
333
+ if (!loaded)
334
+ return { ok: false, code: 'offer_not_loaded' };
440
335
  const hotel = state.hotels[loaded.hotelRef];
441
- if (!hotel) return {
442
- ok: false,
443
- code: 'hotel_unknown'
444
- };
445
- if (state.hotelRefs[state.activeHotelOrdinal] !== loaded.hotelRef) return {
446
- ok: false,
447
- code: 'hotel_not_active'
448
- };
449
- if (hotel.freshOffersRequired) return {
450
- ok: false,
451
- code: 'offers_refresh_required'
452
- };
453
- if (hotel.tombstonedOfferRefs.includes(offerRef) || hotel.tombstonedOfferVersionRefs.includes(offerVersionRef)) return {
454
- ok: false,
455
- code: 'generation_invalid'
456
- };
457
- if (!hotel.currentGeneration?.valid || !hotel.currentGeneration.orderedOfferRefs.includes(offerRef) || hotel.invalidatedOfferRefs.includes(offerRef)) return {
458
- ok: false,
459
- code: 'generation_invalid'
460
- };
461
- if (hotel.checksIssued >= MAX_OFFER_CHECKS_PER_HOTEL) return {
462
- ok: false,
463
- code: 'hotel_check_limit_reached'
464
- };
465
- return {
466
- ok: true,
467
- hotelRef: loaded.hotelRef,
468
- offerRef,
469
- offerVersionRef,
470
- checkCount: hotel.checksIssued + 1,
471
- generation: hotel.generationNo
472
- };
473
- }
336
+ if (!hotel)
337
+ return { ok: false, code: 'hotel_unknown' };
338
+ if (state.hotelRefs[state.activeHotelOrdinal] !== loaded.hotelRef)
339
+ return { ok: false, code: 'hotel_not_active' };
340
+ if (hotel.freshOffersRequired)
341
+ return { ok: false, code: 'offers_refresh_required' };
342
+ if (hotel.tombstonedOfferRefs.includes(offerRef) || hotel.tombstonedOfferVersionRefs.includes(offerVersionRef))
343
+ return { ok: false, code: 'generation_invalid' };
344
+ if (!hotel.currentGeneration?.valid || !hotel.currentGeneration.orderedOfferRefs.includes(offerRef) || hotel.invalidatedOfferRefs.includes(offerRef))
345
+ return { ok: false, code: 'generation_invalid' };
346
+ if (hotel.checksIssued >= MAX_OFFER_CHECKS_PER_HOTEL)
347
+ return { ok: false, code: 'hotel_check_limit_reached' };
348
+ return { ok: true, hotelRef: loaded.hotelRef, offerRef, offerVersionRef, checkCount: hotel.checksIssued + 1, generation: hotel.generationNo };
349
+ }
350
+ /** Start the bounded recovery epoch at the first typed offer.check. */
474
351
  function startRecoveryAtOffer(state, workspace, offerRef, actionId) {
475
352
  assertWorkspaceLoadedOfferRefsUnique(workspace);
476
- const loaded = workspace.loadedOffers.find((offer)=>offer.offerRef === offerRef);
477
- if (!loaded) return copyState(state);
478
- const shortlistHotels = workspace.shortlistedOfferRefs.map((ref)=>workspace.loadedOffers.find((offer)=>offer.offerRef === ref)?.hotelRef ?? state.lastQueryOfferHotels?.[ref]).map((ref, index)=>{
479
- if (!ref) throw new Error(`availability_shortlist_unmapped:${workspace.shortlistedOfferRefs[index]}`);
353
+ const loaded = workspace.loadedOffers.find((offer) => offer.offerRef === offerRef);
354
+ if (!loaded)
355
+ return copyState(state);
356
+ const shortlistHotels = workspace.shortlistedOfferRefs.map((ref) => workspace.loadedOffers.find((offer) => offer.offerRef === ref)?.hotelRef ?? state.lastQueryOfferHotels?.[ref]).map((ref, index) => {
357
+ if (!ref)
358
+ throw new Error(`availability_shortlist_unmapped:${workspace.shortlistedOfferRefs[index]}`);
480
359
  return ref;
481
360
  });
482
361
  const fallbackHotels = shortlistHotels.length ? shortlistHotels : [];
483
- const candidates = unique([
484
- loaded.hotelRef,
485
- ...fallbackHotels
486
- ]).slice(0, MAX_HOTELS_PER_TASK);
362
+ const candidates = unique([loaded.hotelRef, ...fallbackHotels]).slice(0, MAX_HOTELS_PER_TASK);
487
363
  const next = copyState(state);
488
364
  next.recoveryStarted = true;
489
365
  next.recoveryId = `recovery:${actionId}`;
490
366
  next.hotelRefs = candidates;
491
367
  next.candidateSetDigest = digest(candidates);
492
- next.hotels = Object.fromEntries(candidates.map((ref)=>[
493
- ref,
494
- newHotel(ref)
495
- ]));
368
+ next.hotels = Object.fromEntries(candidates.map((ref) => [ref, newHotel(ref)]));
496
369
  const nextHotel = next.hotels[loaded.hotelRef];
497
- const mappedRefs = state.lastQueryOfferHotels ? Object.entries(state.lastQueryOfferHotels).filter(([, hotel])=>hotel === loaded.hotelRef).map(([ref])=>ref) : [];
498
- const currentRefs = unique(workspace.loadedOffers.filter((offer)=>offer.hotelRef === loaded.hotelRef).map((offer)=>offer.offerRef));
499
- const mapped = state.lastQueryOfferHotels?.[offerRef] === loaded.hotelRef && state.lastQueryWorkspaceRevision === workspace.revision && Boolean(state.lastQuerySourceActionId && state.lastQuerySourceReceiptDigest) && sameSet(mappedRefs, currentRefs);
370
+ const mappedRefs = state.lastQueryOfferHotels ? Object.entries(state.lastQueryOfferHotels).filter(([, hotel]) => hotel === loaded.hotelRef).map(([ref]) => ref) : [];
371
+ const currentRefs = unique(workspace.loadedOffers.filter((offer) => offer.hotelRef === loaded.hotelRef).map((offer) => offer.offerRef));
372
+ const mapped = state.lastQueryOfferHotels?.[offerRef] === loaded.hotelRef
373
+ && state.lastQueryWorkspaceRevision === workspace.revision
374
+ && Boolean(state.lastQuerySourceActionId && state.lastQuerySourceReceiptDigest)
375
+ && sameSet(mappedRefs, currentRefs);
500
376
  if (mapped && state.criteria) {
501
377
  next.criteria = structuredClone(state.criteria);
502
378
  next.criteriaDigest = digest(next.criteria);
503
379
  }
504
- const refs = mapped && state.lastQueryOfferHotels ? unique([
505
- offerRef,
506
- ...mappedRefs
507
- ]).slice(0, MAX_OFFERS_PER_HOTEL_GENERATION) : offerRefsForHotel(workspace, loaded.hotelRef, offerRef);
508
- const source = mapped ? {
509
- kind: 'query_receipt',
510
- actionId: state.lastQuerySourceActionId,
511
- receiptDigest: state.lastQuerySourceReceiptDigest,
512
- workspaceRevision: state.lastQueryWorkspaceRevision
513
- } : {
514
- kind: 'workspace_snapshot',
515
- workspaceDigest: workspaceDigest(workspace),
516
- workspaceRevision: workspace.revision
517
- };
380
+ const refs = mapped && state.lastQueryOfferHotels
381
+ ? unique([offerRef, ...mappedRefs]).slice(0, MAX_OFFERS_PER_HOTEL_GENERATION)
382
+ : offerRefsForHotel(workspace, loaded.hotelRef, offerRef);
383
+ const source = mapped
384
+ ? { kind: 'query_receipt', actionId: state.lastQuerySourceActionId, receiptDigest: state.lastQuerySourceReceiptDigest, workspaceRevision: state.lastQueryWorkspaceRevision }
385
+ : { kind: 'workspace_snapshot', workspaceDigest: workspaceDigest(workspace), workspaceRevision: workspace.revision };
518
386
  nextHotel.generationNo = 1;
519
387
  nextHotel.generation = 1;
520
388
  nextHotel.currentOfferRefs = refs;
521
389
  nextHotel.status = refs.length ? 'active' : 'inconclusive';
522
- nextHotel.currentGeneration = {
523
- generationId: `${loaded.hotelRef}:generation:1`,
524
- source,
525
- offerSetDigest: digest(refs),
526
- orderedOfferRefs: refs,
527
- evidence: 'complete',
528
- valid: refs.length > 0
529
- };
390
+ nextHotel.currentGeneration = { generationId: `${loaded.hotelRef}:generation:1`, source, offerSetDigest: digest(refs), orderedOfferRefs: refs, evidence: 'complete', valid: refs.length > 0 };
530
391
  next.initialized = true;
531
392
  next.availabilityPhase = 'need_check';
532
393
  return next;
533
394
  }
534
395
  export function recordOfferCheckIssued(state, workspace, offerRef, offerVersionRef, actionId = `offer-check-${offerRef}-${state.attempts.length + 1}`) {
535
396
  assertWorkspaceLoadedOfferRefsUnique(workspace);
536
- if (state.terminal) throw new Error('availability_terminal');
397
+ if (state.terminal)
398
+ throw new Error('availability_terminal');
537
399
  if (!state.recoveryStarted) {
538
- if (!loadedOfferVersion(workspace, offerRef, offerVersionRef)) throw new Error('availability_offer_not_loaded');
400
+ if (!loadedOfferVersion(workspace, offerRef, offerVersionRef))
401
+ throw new Error('availability_offer_not_loaded');
539
402
  const started = startRecoveryAtOffer(state, workspace, offerRef, actionId);
540
403
  return recordOfferCheckIssued(started, workspace, offerRef, offerVersionRef, actionId);
541
404
  }
542
405
  const decision = canIssueOfferCheck(state, workspace, offerRef, offerVersionRef);
543
- if (!decision.ok) throw new Error(`availability_${decision.code}`);
406
+ if (!decision.ok)
407
+ throw new Error(`availability_${decision.code}`);
544
408
  const next = copyState(state);
545
409
  const hotel = next.hotels[decision.hotelRef];
546
410
  hotel.checksIssued += 1;
547
411
  hotel.checkCount = hotel.checksIssued;
548
- next.attempts.push({
549
- actionId,
550
- hotelRef: decision.hotelRef,
551
- offerRef,
552
- offerVersionRef,
553
- generation: hotel.generationNo,
554
- ordinal: hotel.checksIssued,
555
- workspaceRevision: workspace.revision
556
- });
412
+ next.attempts.push({ actionId, hotelRef: decision.hotelRef, offerRef, offerVersionRef, generation: hotel.generationNo, ordinal: hotel.checksIssued, workspaceRevision: workspace.revision });
557
413
  hotel.currentGeneration.valid = false;
558
414
  next.availabilityPhase = 'waiting_check';
559
415
  return next;
560
416
  }
417
+ /**
418
+ * Fold the availability portion of an issued action. Keeping this reducer
419
+ * next to the receipt reducers makes ledger replay use precisely the same
420
+ * transition as the write path.
421
+ */
561
422
  export function reduceAvailabilityAction(state, workspace, action) {
562
423
  if (action.kind === 'offers.query') {
563
- if (!isOfferCriteria(action.input.criteria) && (!state.criteria || !isOfferCriteria(state.criteria))) throw new Error('availability_criteria_required');
564
- if (!isOfferCriteria(action.input.criteria) && state.recoveryStarted) throw new Error('availability_criteria_required');
565
- if (!isOfferCriteria(action.input.criteria)) throw new Error('availability_criteria_required');
424
+ if (!isOfferCriteria(action.input.criteria) && (!state.criteria || !isOfferCriteria(state.criteria)))
425
+ throw new Error('availability_criteria_required');
426
+ if (!isOfferCriteria(action.input.criteria) && state.recoveryStarted)
427
+ throw new Error('availability_criteria_required');
428
+ if (!isOfferCriteria(action.input.criteria))
429
+ throw new Error('availability_criteria_required');
566
430
  const criteriaDigest = digest(action.input.criteria);
567
431
  let next = recordObservedOffersQuery(state, action.input.hotelRefs, criteriaDigest);
568
- if (state.recoveryStarted) next = recordOffersQueryIssued(next, action.input.hotelRefs, workspace, action.actionId, criteriaDigest, action.input.criteria);
432
+ if (state.recoveryStarted)
433
+ next = recordOffersQueryIssued(next, action.input.hotelRefs, workspace, action.actionId, criteriaDigest, action.input.criteria);
569
434
  return next;
570
435
  }
571
- if (action.kind === 'offer.check') return recordOfferCheckIssued(state, workspace, action.input.offerRef, action.input.offerVersionRef, action.actionId);
436
+ if (action.kind === 'offer.check')
437
+ return recordOfferCheckIssued(state, workspace, action.input.offerRef, action.input.offerVersionRef, action.actionId);
572
438
  return copyState(state);
573
439
  }
440
+ /** Fold a receipt against the exact durable attempt. Non-confirming results invalidate the whole generation. */
574
441
  export function recordOfferCheckReceipt(state, workspace, receipt, actionId, offerRef, offerVersionRef, expectedRevision) {
575
442
  assertWorkspaceLoadedOfferRefsUnique(workspace);
576
- if (!state.recoveryStarted) return copyState(state);
577
- const attempt = state.attempts.find((candidate)=>candidate.actionId === actionId);
578
- if (!attempt) throw new Error('availability_attempt_unknown');
579
- if (receipt.actionId !== actionId) throw new Error('availability_attempt_mismatch');
580
- if (receipt.revision !== workspace.revision || receipt.revision < expectedRevision) throw new Error('availability_stale_receipt');
443
+ if (!state.recoveryStarted)
444
+ return copyState(state);
445
+ const attempt = state.attempts.find((candidate) => candidate.actionId === actionId);
446
+ if (!attempt)
447
+ throw new Error('availability_attempt_unknown');
448
+ if (receipt.actionId !== actionId)
449
+ throw new Error('availability_attempt_mismatch');
450
+ if (receipt.revision !== workspace.revision || receipt.revision < expectedRevision)
451
+ throw new Error('availability_stale_receipt');
581
452
  const targetOfferRef = receipt.observation.kind === 'offer.availability' ? receipt.observation.offerRef : offerRef;
582
453
  const targetVersionRef = receipt.observation.kind === 'offer.availability' ? receipt.observation.checkedOfferVersionRef : offerVersionRef;
583
- if (attempt.offerRef !== targetOfferRef || attempt.offerVersionRef !== targetVersionRef || offerRef !== targetOfferRef || offerVersionRef !== targetVersionRef || attempt.workspaceRevision !== expectedRevision) throw new Error('availability_attempt_mismatch');
454
+ if (attempt.offerRef !== targetOfferRef || attempt.offerVersionRef !== targetVersionRef || offerRef !== targetOfferRef || offerVersionRef !== targetVersionRef || attempt.workspaceRevision !== expectedRevision)
455
+ throw new Error('availability_attempt_mismatch');
584
456
  const next = copyState(state);
585
457
  const hotel = next.hotels[attempt.hotelRef];
586
- if (!hotel || !hotel.currentGeneration || attempt.generation !== hotel.generationNo) throw new Error('availability_generation_mismatch');
458
+ if (!hotel || !hotel.currentGeneration || attempt.generation !== hotel.generationNo)
459
+ throw new Error('availability_generation_mismatch');
587
460
  const observation = receipt.observation.kind === 'offer.availability' ? receipt.observation : undefined;
588
- if ([
589
- 'applied',
590
- 'changed',
591
- 'unavailable',
592
- 'no_match'
593
- ].includes(receipt.status) && !observation) throw new Error('availability_observation_required');
594
- if (observation?.available && (receipt.resultContract.outcome === 'empty' || [
595
- 'unavailable',
596
- 'no_match',
597
- 'failed',
598
- 'stale',
599
- 'unsupported'
600
- ].includes(receipt.status))) throw new Error('availability_receipt_incoherent');
601
- if (observation && !observation.available && (observation.verifiedOfferRef || observation.currentOfferVersionRef)) throw new Error('availability_receipt_incoherent');
602
- if ([
603
- 'unavailable',
604
- 'no_match'
605
- ].includes(receipt.status) && receipt.resultContract.outcome !== 'empty') throw new Error('availability_receipt_incoherent');
461
+ if (['applied', 'changed', 'unavailable', 'no_match'].includes(receipt.status) && !observation)
462
+ throw new Error('availability_observation_required');
463
+ if (observation?.available && (receipt.resultContract.outcome === 'empty' || ['unavailable', 'no_match', 'failed', 'stale', 'unsupported'].includes(receipt.status)))
464
+ throw new Error('availability_receipt_incoherent');
465
+ if (observation && !observation.available && (observation.verifiedOfferRef || observation.currentOfferVersionRef))
466
+ throw new Error('availability_receipt_incoherent');
467
+ if (['unavailable', 'no_match'].includes(receipt.status) && receipt.resultContract.outcome !== 'empty')
468
+ throw new Error('availability_receipt_incoherent');
606
469
  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;
607
470
  if (confirmed) {
608
471
  hotel.status = 'confirmed';
@@ -611,15 +474,10 @@ export function recordOfferCheckReceipt(state, workspace, receipt, actionId, off
611
474
  return withTerminal(next);
612
475
  }
613
476
  if (receipt.status === 'changed' && observation?.available && observation.currentOfferVersionRef && observation.currentOfferVersionRef !== offerVersionRef && observation.changedFactRefs.length && !observation.verifiedOfferRef && loadedOfferVersion(workspace, offerRef, observation.currentOfferVersionRef)) {
614
- hotel.tombstonedOfferVersionRefs = unique([
615
- ...hotel.tombstonedOfferVersionRefs,
616
- offerVersionRef
617
- ]);
477
+ hotel.tombstonedOfferVersionRefs = unique([...hotel.tombstonedOfferVersionRefs, offerVersionRef]);
618
478
  hotel.currentGeneration.valid = true;
619
479
  hotel.invalidatedOfferRefs = [];
620
- hotel.currentOfferRefs = [
621
- offerRef
622
- ];
480
+ hotel.currentOfferRefs = [offerRef];
623
481
  hotel.freshOffersRequired = false;
624
482
  hotel.status = 'active';
625
483
  hotel.lastEvidence = 'none';
@@ -628,203 +486,212 @@ export function recordOfferCheckReceipt(state, workspace, receipt, actionId, off
628
486
  }
629
487
  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;
630
488
  if (completeNegative) {
631
- hotel.tombstonedOfferVersionRefs = unique([
632
- ...hotel.tombstonedOfferVersionRefs,
633
- offerVersionRef
634
- ]);
635
- hotel.tombstonedOfferRefs = unique([
636
- ...hotel.tombstonedOfferRefs,
637
- offerRef
638
- ]);
489
+ hotel.tombstonedOfferVersionRefs = unique([...hotel.tombstonedOfferVersionRefs, offerVersionRef]);
490
+ hotel.tombstonedOfferRefs = unique([...hotel.tombstonedOfferRefs, offerRef]);
639
491
  }
640
492
  hotel.currentGeneration.valid = false;
641
- hotel.invalidatedOfferRefs = [
642
- ...hotel.currentGeneration.orderedOfferRefs
643
- ];
493
+ hotel.invalidatedOfferRefs = [...hotel.currentGeneration.orderedOfferRefs];
644
494
  hotel.currentOfferRefs = [];
645
495
  hotel.freshOffersRequired = hotel.checksIssued < MAX_OFFER_CHECKS_PER_HOTEL && hotel.offerQueriesIssued < MAX_OFFER_QUERIES_PER_HOTEL;
646
496
  hotel.status = completeNegative ? 'negative' : 'inconclusive';
647
497
  hotel.lastEvidence = completeNegative ? 'unavailable' : 'inconclusive';
648
498
  const currentOrdinal = next.hotelRefs.indexOf(attempt.hotelRef);
649
- const nextOrdinal = hotel.freshOffersRequired ? currentOrdinal : next.hotelRefs.findIndex((ref)=>[
650
- 'active',
651
- 'unvisited'
652
- ].includes(next.hotels[ref].status));
653
- if (nextOrdinal >= 0) next.activeHotelOrdinal = nextOrdinal;
499
+ const nextOrdinal = hotel.freshOffersRequired ? currentOrdinal : next.hotelRefs.findIndex((ref) => ['active', 'unvisited'].includes(next.hotels[ref].status));
500
+ if (nextOrdinal >= 0)
501
+ next.activeHotelOrdinal = nextOrdinal;
654
502
  next.availabilityPhase = phaseForActiveHotel(next);
655
503
  return withTerminal(next);
656
504
  }
505
+ /** Fold the availability portion of a receipt using the same pure reducer as
506
+ * continueWithReceipt and resumeTask. */
657
507
  export function reduceAvailabilityReceipt(state, workspace, receipt, action) {
658
508
  let next = recordVisibleHotels(state, workspace);
659
509
  if (action.kind === 'offers.query' && !state.recoveryStarted) {
660
- if (!isOfferCriteria(action.input.criteria)) throw new Error('availability_criteria_required');
510
+ if (!isOfferCriteria(action.input.criteria))
511
+ throw new Error('availability_criteria_required');
661
512
  next = recordObservedOffersQuery(next, action.input.hotelRefs, digest(action.input.criteria), workspace, receipt, action.actionId, digest(receipt), action.input.criteria);
662
- } else if (action.kind === 'offers.query' && state.recoveryStarted) {
663
- if (!isOfferCriteria(action.input.criteria)) throw new Error('availability_criteria_required');
513
+ }
514
+ else if (action.kind === 'offers.query' && state.recoveryStarted) {
515
+ if (!isOfferCriteria(action.input.criteria))
516
+ throw new Error('availability_criteria_required');
664
517
  next = recordOffersGeneration(next, action.input.hotelRefs, workspace, action.actionId, digest(receipt), receipt, state.criteriaDigest ?? '', action.input.criteria);
665
- } else if (action.kind === 'offer.check') {
666
- if (action.expectedRevision === undefined) throw new Error('availability_expected_revision_missing');
518
+ }
519
+ else if (action.kind === 'offer.check') {
520
+ if (action.expectedRevision === undefined)
521
+ throw new Error('availability_expected_revision_missing');
667
522
  next = recordOfferCheckReceipt(next, workspace, receipt, action.actionId, action.input.offerRef, action.input.offerVersionRef, action.expectedRevision);
668
523
  }
669
524
  return next;
670
525
  }
671
- export function availabilityPolicyIsTerminal(state) {
672
- return Boolean(state.terminal);
673
- }
674
- export function availabilityPolicyResult(state) {
675
- return state.terminal ? {
676
- ...state.terminal,
677
- hotelRefs: [
678
- ...state.terminal.hotelRefs
679
- ]
680
- } : undefined;
681
- }
526
+ export function availabilityPolicyIsTerminal(state) { return Boolean(state.terminal); }
527
+ export function availabilityPolicyResult(state) { return state.terminal ? { ...state.terminal, hotelRefs: [...state.terminal.hotelRefs] } : undefined; }
682
528
  export function validateAvailabilityPolicy(state) {
683
529
  try {
684
- if (!state || typeof state.initialized !== 'boolean' || typeof state.recoveryStarted !== 'boolean') return false;
685
- if (state.recoveryId !== undefined && (typeof state.recoveryId !== 'string' || !state.recoveryId)) return false;
686
- if (state.criteriaDigest !== undefined && (typeof state.criteriaDigest !== 'string' || !/^[0-9a-f]{64}$/.test(state.criteriaDigest))) return false;
687
- if (state.candidateSetDigest !== undefined && (typeof state.candidateSetDigest !== 'string' || !/^[0-9a-f]{64}$/.test(state.candidateSetDigest))) return false;
688
- if (![
689
- 'need_offers',
690
- 'waiting_offers',
691
- 'need_check',
692
- 'waiting_check',
693
- 'terminal'
694
- ].includes(state.availabilityPhase)) return false;
695
- if (!Number.isSafeInteger(state.activeHotelOrdinal) || !Array.isArray(state.hotelRefs) || state.hotelRefs.length > MAX_HOTELS_PER_TASK) return false;
696
- if (state.hotelRefs.some((ref)=>typeof ref !== 'string' || !ref) || new Set(state.hotelRefs).size !== state.hotelRefs.length) return false;
697
- if (state.hotelRefs.length && (state.activeHotelOrdinal < 0 || state.activeHotelOrdinal >= state.hotelRefs.length) || !state.hotelRefs.length && state.activeHotelOrdinal !== 0) return false;
698
- if (!state.hotels || typeof state.hotels !== 'object' || Object.keys(state.hotels).sort().join('\0') !== [
699
- ...state.hotelRefs
700
- ].sort().join('\0')) return false;
701
- if (!Array.isArray(state.attempts) || !Array.isArray(state.queryReservations)) return false;
530
+ if (!state || typeof state.initialized !== 'boolean' || typeof state.recoveryStarted !== 'boolean')
531
+ return false;
532
+ if (state.recoveryId !== undefined && (typeof state.recoveryId !== 'string' || !state.recoveryId))
533
+ return false;
534
+ if (state.criteriaDigest !== undefined && (typeof state.criteriaDigest !== 'string' || !/^[0-9a-f]{64}$/.test(state.criteriaDigest)))
535
+ return false;
536
+ if (state.candidateSetDigest !== undefined && (typeof state.candidateSetDigest !== 'string' || !/^[0-9a-f]{64}$/.test(state.candidateSetDigest)))
537
+ return false;
538
+ if (!['need_offers', 'waiting_offers', 'need_check', 'waiting_check', 'terminal'].includes(state.availabilityPhase))
539
+ return false;
540
+ if (!Number.isSafeInteger(state.activeHotelOrdinal) || !Array.isArray(state.hotelRefs) || state.hotelRefs.length > MAX_HOTELS_PER_TASK)
541
+ return false;
542
+ if (state.hotelRefs.some((ref) => typeof ref !== 'string' || !ref) || new Set(state.hotelRefs).size !== state.hotelRefs.length)
543
+ return false;
544
+ if ((state.hotelRefs.length && (state.activeHotelOrdinal < 0 || state.activeHotelOrdinal >= state.hotelRefs.length)) || (!state.hotelRefs.length && state.activeHotelOrdinal !== 0))
545
+ return false;
546
+ if (!state.hotels || typeof state.hotels !== 'object' || Object.keys(state.hotels).sort().join('\0') !== [...state.hotelRefs].sort().join('\0'))
547
+ return false;
548
+ if (!Array.isArray(state.attempts) || !Array.isArray(state.queryReservations))
549
+ return false;
702
550
  if (!state.recoveryStarted) {
703
- 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;
551
+ if (state.recoveryId !== undefined || state.candidateSetDigest !== undefined || state.hotelRefs.length || Object.keys(state.hotels).length || state.attempts.length || state.queryReservations.length || state.terminal !== undefined)
552
+ return false;
704
553
  if (state.criteria !== undefined || state.criteriaDigest !== undefined) {
705
- if (!isOfferCriteria(state.criteria) || !state.criteriaDigest || state.criteriaDigest !== digest(state.criteria)) return false;
554
+ if (!isOfferCriteria(state.criteria) || !state.criteriaDigest || state.criteriaDigest !== digest(state.criteria))
555
+ return false;
706
556
  }
707
- if (state.lastQueryCriteriaDigest !== undefined && state.lastQueryCriteriaDigest !== state.criteriaDigest) return false;
708
- 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;
709
- 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;
710
- 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;
711
- if (state.lastQuerySourceReceiptDigest !== undefined && state.lastQuerySourceActionId === undefined) return false;
557
+ if (state.lastQueryCriteriaDigest !== undefined && state.lastQueryCriteriaDigest !== state.criteriaDigest)
558
+ return false;
559
+ if (state.lastQueryHotelRefs !== undefined && (!Array.isArray(state.lastQueryHotelRefs) || new Set(state.lastQueryHotelRefs).size !== state.lastQueryHotelRefs.length || state.lastQueryHotelRefs.some((ref) => typeof ref !== 'string' || !ref)))
560
+ return false;
561
+ 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)))
562
+ return false;
563
+ 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))
564
+ return false;
565
+ if (state.lastQuerySourceReceiptDigest !== undefined && state.lastQuerySourceActionId === undefined)
566
+ return false;
712
567
  return state.initialized === false && state.availabilityPhase === 'need_offers';
713
568
  }
714
- if (!state.initialized || !state.recoveryId || !state.candidateSetDigest || state.candidateSetDigest !== digest(state.hotelRefs)) return false;
569
+ if (!state.initialized || !state.recoveryId || !state.candidateSetDigest || state.candidateSetDigest !== digest(state.hotelRefs))
570
+ return false;
715
571
  const criteriaBound = isOfferCriteria(state.criteria) && Boolean(state.criteriaDigest) && state.criteriaDigest === digest(state.criteria);
716
- const manualCheckWorkspaceSnapshot = state.attempts.length > 0 && state.attempts.every((attempt)=>state.hotels[attempt.hotelRef]?.currentGeneration?.source.kind === 'workspace_snapshot');
717
- const manualCheckMayBindCriteria = !state.criteria && !state.criteriaDigest && state.queryReservations.length === 0 && manualCheckWorkspaceSnapshot && ([
718
- 'waiting_check',
719
- 'need_offers'
720
- ].includes(state.availabilityPhase) || state.availabilityPhase === 'need_check' || state.availabilityPhase === 'terminal' && state.terminal?.code === 'availability_confirmed');
721
- if (!criteriaBound && !manualCheckMayBindCriteria) return false;
722
- if (state.lastQueryCriteriaDigest !== undefined && state.lastQueryCriteriaDigest !== state.criteriaDigest) return false;
723
- 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;
724
- 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;
725
- 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;
726
- if (state.lastQuerySourceReceiptDigest !== undefined && state.lastQuerySourceActionId === undefined) return false;
727
- if (state.hotelRefs.length === 0) return false;
728
- const validStatus = new Set([
729
- 'unvisited',
730
- 'active',
731
- 'negative',
732
- 'confirmed',
733
- 'inconclusive'
734
- ]);
735
- const validEvidence = new Set([
736
- 'none',
737
- 'confirmed',
738
- 'unavailable',
739
- 'inconclusive'
740
- ]);
741
- for (const ref of state.hotelRefs){
572
+ const manualCheckWorkspaceSnapshot = state.attempts.length > 0 && state.attempts.every((attempt) => state.hotels[attempt.hotelRef]?.currentGeneration?.source.kind === 'workspace_snapshot');
573
+ const manualCheckMayBindCriteria = !state.criteria && !state.criteriaDigest && state.queryReservations.length === 0 && manualCheckWorkspaceSnapshot && (['waiting_check', 'need_offers'].includes(state.availabilityPhase)
574
+ || state.availabilityPhase === 'need_check'
575
+ || (state.availabilityPhase === 'terminal' && state.terminal?.code === 'availability_confirmed'));
576
+ if (!criteriaBound && !manualCheckMayBindCriteria)
577
+ return false;
578
+ if (state.lastQueryCriteriaDigest !== undefined && state.lastQueryCriteriaDigest !== state.criteriaDigest)
579
+ return false;
580
+ if (state.lastQueryHotelRefs !== undefined && (!Array.isArray(state.lastQueryHotelRefs) || new Set(state.lastQueryHotelRefs).size !== state.lastQueryHotelRefs.length || state.lastQueryHotelRefs.some((ref) => typeof ref !== 'string' || !ref)))
581
+ return false;
582
+ 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)))
583
+ return false;
584
+ 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))
585
+ return false;
586
+ if (state.lastQuerySourceReceiptDigest !== undefined && state.lastQuerySourceActionId === undefined)
587
+ return false;
588
+ if (state.hotelRefs.length === 0)
589
+ return false;
590
+ const validStatus = new Set(['unvisited', 'active', 'negative', 'confirmed', 'inconclusive']);
591
+ const validEvidence = new Set(['none', 'confirmed', 'unavailable', 'inconclusive']);
592
+ for (const ref of state.hotelRefs) {
742
593
  const hotel = state.hotels[ref];
743
- if (!hotel || hotel.hotelRef !== ref || !validStatus.has(hotel.status) || !validEvidence.has(hotel.lastEvidence)) return false;
744
- if (![
745
- hotel.checksIssued,
746
- hotel.offerQueriesIssued,
747
- hotel.generationNo,
748
- hotel.generation,
749
- hotel.checkCount
750
- ].every(Number.isSafeInteger)) return false;
751
- 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;
752
- if (typeof hotel.freshOffersRequired !== 'boolean' || !Array.isArray(hotel.currentOfferRefs) || !Array.isArray(hotel.invalidatedOfferRefs) || !Array.isArray(hotel.tombstonedOfferRefs) || !Array.isArray(hotel.tombstonedOfferVersionRefs)) return false;
753
- 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;
754
- 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;
594
+ if (!hotel || hotel.hotelRef !== ref || !validStatus.has(hotel.status) || !validEvidence.has(hotel.lastEvidence))
595
+ return false;
596
+ if (![hotel.checksIssued, hotel.offerQueriesIssued, hotel.generationNo, hotel.generation, hotel.checkCount].every(Number.isSafeInteger))
597
+ return false;
598
+ 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)
599
+ return false;
600
+ if (typeof hotel.freshOffersRequired !== 'boolean' || !Array.isArray(hotel.currentOfferRefs) || !Array.isArray(hotel.invalidatedOfferRefs) || !Array.isArray(hotel.tombstonedOfferRefs) || !Array.isArray(hotel.tombstonedOfferVersionRefs))
601
+ return false;
602
+ 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))
603
+ return false;
604
+ 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)))
605
+ return false;
755
606
  const generation = hotel.currentGeneration;
756
607
  if (hotel.generationNo === 0) {
757
- 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;
608
+ 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)
609
+ return false;
758
610
  continue;
759
611
  }
760
- 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 || ![
761
- 'complete',
762
- 'partial'
763
- ].includes(generation.evidence) || typeof generation.valid !== 'boolean') return false;
764
- if (generation.offerSetDigest !== digest(generation.orderedOfferRefs) || hotel.currentOfferRefs.some((offer)=>!generation.orderedOfferRefs.includes(offer)) || hotel.invalidatedOfferRefs.some((offer)=>!generation.orderedOfferRefs.includes(offer))) return false;
612
+ 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')
613
+ return false;
614
+ if (generation.offerSetDigest !== digest(generation.orderedOfferRefs) || hotel.currentOfferRefs.some((offer) => !generation.orderedOfferRefs.includes(offer)) || hotel.invalidatedOfferRefs.some((offer) => !generation.orderedOfferRefs.includes(offer)))
615
+ return false;
765
616
  const source = generation.source;
766
- if (!source || ![
767
- 'query_receipt',
768
- 'workspace_snapshot'
769
- ].includes(source.kind) || !Number.isSafeInteger(source.workspaceRevision) || source.workspaceRevision < 0) return false;
770
- if (source.kind === 'query_receipt' && (typeof source.actionId !== 'string' || !source.actionId || !/^[0-9a-f]{64}$/.test(source.receiptDigest))) return false;
771
- if (source.kind === 'workspace_snapshot' && (typeof source.workspaceDigest !== 'string' || !/^[0-9a-f]{64}$/.test(source.workspaceDigest))) return false;
772
- if (source.kind === 'query_receipt' && state.lastQuerySourceActionId === source.actionId && (state.lastQuerySourceReceiptDigest !== source.receiptDigest || state.lastQueryWorkspaceRevision !== source.workspaceRevision)) return false;
773
- if (hotel.freshOffersRequired && (hotel.currentOfferRefs.length !== 0 || hotel.checksIssued >= MAX_OFFER_CHECKS_PER_HOTEL || hotel.offerQueriesIssued >= MAX_OFFER_QUERIES_PER_HOTEL)) return false;
617
+ if (!source || !['query_receipt', 'workspace_snapshot'].includes(source.kind) || !Number.isSafeInteger(source.workspaceRevision) || source.workspaceRevision < 0)
618
+ return false;
619
+ if (source.kind === 'query_receipt' && (typeof source.actionId !== 'string' || !source.actionId || !/^[0-9a-f]{64}$/.test(source.receiptDigest)))
620
+ return false;
621
+ if (source.kind === 'workspace_snapshot' && (typeof source.workspaceDigest !== 'string' || !/^[0-9a-f]{64}$/.test(source.workspaceDigest)))
622
+ return false;
623
+ if (source.kind === 'query_receipt' && state.lastQuerySourceActionId === source.actionId && (state.lastQuerySourceReceiptDigest !== source.receiptDigest || state.lastQueryWorkspaceRevision !== source.workspaceRevision))
624
+ return false;
625
+ if (hotel.freshOffersRequired && (hotel.currentOfferRefs.length !== 0 || hotel.checksIssued >= MAX_OFFER_CHECKS_PER_HOTEL || hotel.offerQueriesIssued >= MAX_OFFER_QUERIES_PER_HOTEL))
626
+ return false;
774
627
  if (hotel.status === 'unvisited' || hotel.status === 'negative') {
775
- if (hotel.status === 'unvisited' || hotel.lastEvidence !== 'unavailable' || hotel.currentOfferRefs.length || generation.valid) return false;
628
+ if (hotel.status === 'unvisited' || hotel.lastEvidence !== 'unavailable' || hotel.currentOfferRefs.length || generation.valid)
629
+ return false;
776
630
  }
777
- if (hotel.status === 'active' && (!hotel.currentOfferRefs.length || hotel.lastEvidence !== 'none' || hotel.freshOffersRequired)) return false;
778
- if (hotel.status === 'confirmed' && (hotel.lastEvidence !== 'confirmed' || !hotel.currentOfferRefs.length || generation.valid)) return false;
779
- if (hotel.status === 'inconclusive' && (hotel.lastEvidence !== 'inconclusive' || hotel.currentOfferRefs.length || generation.valid)) return false;
631
+ if (hotel.status === 'active' && (!hotel.currentOfferRefs.length || hotel.lastEvidence !== 'none' || hotel.freshOffersRequired))
632
+ return false;
633
+ if (hotel.status === 'confirmed' && (hotel.lastEvidence !== 'confirmed' || !hotel.currentOfferRefs.length || generation.valid))
634
+ return false;
635
+ if (hotel.status === 'inconclusive' && (hotel.lastEvidence !== 'inconclusive' || hotel.currentOfferRefs.length || generation.valid))
636
+ return false;
780
637
  }
781
638
  const attempts = state.attempts;
782
- if (new Set(attempts.map((attempt)=>attempt.actionId)).size !== attempts.length) return false;
639
+ if (new Set(attempts.map((attempt) => attempt.actionId)).size !== attempts.length)
640
+ return false;
783
641
  const attemptsByHotel = new Map();
784
- for (const attempt of attempts){
785
- 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;
642
+ for (const attempt of attempts) {
643
+ 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)
644
+ return false;
786
645
  const hotel = state.hotels[attempt.hotelRef];
787
646
  const generation = hotel.currentGeneration;
788
- 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;
647
+ 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)))
648
+ return false;
789
649
  const list = attemptsByHotel.get(attempt.hotelRef) ?? [];
790
650
  list.push(attempt);
791
651
  attemptsByHotel.set(attempt.hotelRef, list);
792
652
  }
793
- for (const [ref, attemptsForHotel] of attemptsByHotel){
794
- const ordinals = attemptsForHotel.map((attempt)=>attempt.ordinal);
795
- 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;
653
+ for (const [ref, attemptsForHotel] of attemptsByHotel) {
654
+ const ordinals = attemptsForHotel.map((attempt) => attempt.ordinal);
655
+ 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)
656
+ return false;
796
657
  }
797
- for (const ref of state.hotelRefs)if ((attemptsByHotel.get(ref)?.length ?? 0) !== state.hotels[ref].checksIssued) return false;
798
- if (new Set(state.queryReservations.map((reservation)=>reservation.actionId)).size !== state.queryReservations.length) return false;
658
+ for (const ref of state.hotelRefs)
659
+ if ((attemptsByHotel.get(ref)?.length ?? 0) !== state.hotels[ref].checksIssued)
660
+ return false;
661
+ if (new Set(state.queryReservations.map((reservation) => reservation.actionId)).size !== state.queryReservations.length)
662
+ return false;
799
663
  const reservedHotels = new Set();
800
- for (const reservation of state.queryReservations){
801
- 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;
802
- reservation.hotelRefs.forEach((ref)=>reservedHotels.add(ref));
664
+ for (const reservation of state.queryReservations) {
665
+ 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)))
666
+ return false;
667
+ reservation.hotelRefs.forEach((ref) => reservedHotels.add(ref));
803
668
  }
804
- if (state.availabilityPhase === 'waiting_offers' && state.queryReservations.length === 0) return false;
805
- if (state.availabilityPhase !== 'waiting_offers' && state.queryReservations.length > 0) return false;
806
- if (state.availabilityPhase === 'waiting_check' && !attempts.length) return false;
669
+ if (state.availabilityPhase === 'waiting_offers' && state.queryReservations.length === 0)
670
+ return false;
671
+ if (state.availabilityPhase !== 'waiting_offers' && state.queryReservations.length > 0)
672
+ return false;
673
+ if (state.availabilityPhase === 'waiting_check' && !attempts.length)
674
+ return false;
807
675
  if (state.availabilityPhase === 'need_check') {
808
676
  const active = state.hotels[state.hotelRefs[state.activeHotelOrdinal]];
809
- if (active.status !== 'active' || !active.currentGeneration?.valid || !active.currentOfferRefs.length || active.freshOffersRequired) return false;
677
+ if (active.status !== 'active' || !active.currentGeneration?.valid || !active.currentOfferRefs.length || active.freshOffersRequired)
678
+ return false;
810
679
  }
811
- if (state.availabilityPhase === 'terminal' && !state.terminal) return false;
812
- if (state.availabilityPhase !== 'terminal' && state.terminal) return false;
680
+ if (state.availabilityPhase === 'terminal' && !state.terminal)
681
+ return false;
682
+ if (state.availabilityPhase !== 'terminal' && state.terminal)
683
+ return false;
813
684
  const recomputedTerminal = terminalFor(state);
814
685
  if (state.terminal) {
815
- if (!recomputedTerminal || digest(state.terminal) !== digest(recomputedTerminal)) return false;
816
- } else if (recomputedTerminal && ![
817
- 'waiting_offers',
818
- 'waiting_check'
819
- ].includes(state.availabilityPhase)) return false;
686
+ if (!recomputedTerminal || digest(state.terminal) !== digest(recomputedTerminal))
687
+ return false;
688
+ }
689
+ else if (recomputedTerminal && !['waiting_offers', 'waiting_check'].includes(state.availabilityPhase))
690
+ return false;
820
691
  return true;
821
- } catch {
692
+ }
693
+ catch {
822
694
  return false;
823
695
  }
824
696
  }
825
- export function availabilityPolicyActionIsRelevant(action) {
826
- return action.kind === 'offers.query' || action.kind === 'offer.check';
827
- }
828
-
829
-
830
- //# sourceURL=ts/src/booking-surface/availability-policy.ts
697
+ export function availabilityPolicyActionIsRelevant(action) { return action.kind === 'offers.query' || action.kind === 'offer.check'; }