@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
@@ -6,1350 +6,246 @@ import { ensureLedger } from '../src/state-ledger.js';
6
6
  import { BookingCopilotTaskRuntime } from '../src/booking-surface/runtime.js';
7
7
  import { createAvailabilityPolicy, digest, MAX_HOTELS_PER_TASK, MAX_OFFER_CHECKS_PER_HOTEL, MAX_OFFER_QUERIES_PER_HOTEL, recordObservedOffersQuery, recordOfferCheckIssued, recordOfferCheckReceipt, recordOffersGeneration, recordOffersQueryIssued, canIssueOfferCheck } from '../src/booking-surface/availability-policy.js';
8
8
  import { bookingSurfaceAllowedActions } from '../src/booking-surface/contracts.js';
9
- const versionFor = (offerRef, suffix = 'v1')=>`${offerRef}:${suffix}`;
10
- const verifiedCapability = (offerRef, offerVersionRef = versionFor(offerRef), expiresAt = '2026-09-01T10:30:00.000Z')=>({
11
- offerRef,
12
- offerVersionRef,
13
- verifiedOfferRef: `verified-${offerRef}`,
14
- expiresAt
15
- });
16
- const ws = (revision, hotels, offers, shortlistedOfferRefs = [], verifiedOffer)=>({
17
- schemaVersion: 'booking.surface',
18
- contextRef: 'ctx-availability',
19
- surface: 'tenant',
20
- revision,
21
- locale: 'en-US',
22
- currency: 'AED',
23
- searchDraft: {},
24
- results: {
25
- status: 'idle'
26
- },
27
- visibleHotels: hotels.map((h)=>({
28
- hotelRef: h,
29
- name: h,
30
- factRefs: []
31
- })),
32
- loadedOffers: offers.map(([offerRef, hotelRef, offerVersionRef = versionFor(offerRef)])=>({
33
- offerRef,
34
- offerVersionRef,
35
- hotelRef,
36
- evidenceLevel: 'rate_loaded',
37
- factRefs: []
38
- })),
39
- shortlistedOfferRefs,
40
- capabilities: {
41
- surface: 'tenant',
42
- allowedActions: [
43
- 'offers.query',
44
- 'offer.check'
45
- ]
46
- },
47
- ...verifiedOffer ? {
48
- verifiedOffer
49
- } : {}
50
- });
51
- const receipt = (actionId, revision, offerRef, status = 'unavailable', available = false, checkedOfferVersionRef = versionFor(offerRef), currentOfferVersionRef)=>({
52
- schemaVersion: 'booking.surface',
53
- kind: 'action.receipt',
54
- actionId,
55
- contextRef: 'ctx-availability',
56
- status,
57
- revision,
58
- observation: {
59
- kind: 'offer.availability',
60
- offerRef,
61
- checkedOfferVersionRef,
62
- currentOfferVersionRef: available ? currentOfferVersionRef ?? checkedOfferVersionRef : undefined,
63
- available,
64
- verifiedOfferRef: available ? 'verified-' + offerRef : undefined,
65
- changedFactRefs: [],
66
- gapCodes: []
67
- },
68
- resultContract: {
69
- outcome: available ? 'complete' : 'empty',
70
- hardCriteriaMet: available,
71
- factRefs: [],
72
- gapCodes: [],
73
- blockers: [],
74
- relaxationsApplied: []
75
- }
76
- });
77
- const foldQuery = (state, hotelRefs, workspace, actionId, queryReceipt, criteriaDigest = '')=>recordOffersGeneration(state, hotelRefs, workspace, actionId, digest(queryReceipt), queryReceipt, criteriaDigest);
9
+ const versionFor = (offerRef, suffix = 'v1') => `${offerRef}:${suffix}`;
10
+ const verifiedCapability = (offerRef, offerVersionRef = versionFor(offerRef), expiresAt = '2026-09-01T10:30:00.000Z') => ({ offerRef, offerVersionRef, verifiedOfferRef: `verified-${offerRef}`, expiresAt });
11
+ const ws = (revision, hotels, offers, shortlistedOfferRefs = [], verifiedOffer) => ({ schemaVersion: 'booking.surface', contextRef: 'ctx-availability', surface: 'tenant', revision, locale: 'en-US', currency: 'AED', searchDraft: {}, results: { status: 'idle' }, visibleHotels: hotels.map(h => ({ hotelRef: h, name: h, factRefs: [] })), loadedOffers: offers.map(([offerRef, hotelRef, offerVersionRef = versionFor(offerRef)]) => ({ offerRef, offerVersionRef, hotelRef, evidenceLevel: 'rate_loaded', factRefs: [] })), shortlistedOfferRefs, capabilities: { surface: 'tenant', allowedActions: ['offers.query', 'offer.check'] }, ...(verifiedOffer ? { verifiedOffer } : {}) });
12
+ const receipt = (actionId, revision, offerRef, status = 'unavailable', available = false, checkedOfferVersionRef = versionFor(offerRef), currentOfferVersionRef) => ({ schemaVersion: 'booking.surface', kind: 'action.receipt', actionId, contextRef: 'ctx-availability', status, revision, observation: { kind: 'offer.availability', offerRef, checkedOfferVersionRef, currentOfferVersionRef: available ? (currentOfferVersionRef ?? checkedOfferVersionRef) : undefined, available, verifiedOfferRef: available ? 'verified-' + offerRef : undefined, changedFactRefs: [], gapCodes: [] }, resultContract: { outcome: available ? 'complete' : 'empty', hardCriteriaMet: available, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } });
13
+ const foldQuery = (state, hotelRefs, workspace, actionId, queryReceipt, criteriaDigest = '') => recordOffersGeneration(state, hotelRefs, workspace, actionId, digest(queryReceipt), queryReceipt, criteriaDigest);
78
14
  const criteria = {};
79
- const offersReceipt = (actionId, revision, hotelRefs, offerRefs, loadedHotelCount, status = 'applied', outcome = 'complete', hardCriteriaMet = true)=>({
80
- schemaVersion: 'booking.surface',
81
- kind: 'action.receipt',
82
- actionId,
83
- contextRef: 'ctx-availability',
84
- status,
85
- revision,
86
- observation: {
87
- kind: 'offers.state',
88
- hotelRefs,
89
- offerRefs,
90
- loadedHotelCount,
91
- gapCodes: []
92
- },
93
- resultContract: {
94
- outcome,
95
- hardCriteriaMet,
96
- factRefs: [],
97
- gapCodes: [],
98
- blockers: [],
99
- relaxationsApplied: []
100
- }
101
- });
102
- const gapReceipt = (actionId, revision, status = 'failed')=>({
103
- schemaVersion: 'booking.surface',
104
- kind: 'action.receipt',
105
- actionId,
106
- contextRef: 'ctx-availability',
107
- status,
108
- revision,
109
- observation: {
110
- kind: 'gap',
111
- code: 'hotel_rates_failed',
112
- factRefs: []
113
- },
114
- resultContract: {
115
- outcome: 'partial',
116
- hardCriteriaMet: false,
117
- factRefs: [],
118
- gapCodes: [
119
- 'hotel_rates_failed'
120
- ],
121
- blockers: [],
122
- relaxationsApplied: []
123
- }
124
- });
125
- assert.deepEqual(bookingSurfaceAllowedActions('storefront'), [
126
- 'search.patch',
127
- 'search.run',
128
- 'results.view.patch',
129
- 'hotel.focus'
130
- ]);
131
- assert.deepEqual(bookingSurfaceAllowedActions('payment_link'), [
132
- 'search.patch',
133
- 'search.run',
134
- 'results.view.patch',
135
- 'hotel.focus',
136
- 'hotel.select'
137
- ]);
15
+ const offersReceipt = (actionId, revision, hotelRefs, offerRefs, loadedHotelCount, status = 'applied', outcome = 'complete', hardCriteriaMet = true) => ({ schemaVersion: 'booking.surface', kind: 'action.receipt', actionId, contextRef: 'ctx-availability', status, revision, observation: { kind: 'offers.state', hotelRefs, offerRefs, loadedHotelCount, gapCodes: [] }, resultContract: { outcome, hardCriteriaMet, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } });
16
+ const gapReceipt = (actionId, revision, status = 'failed') => ({ schemaVersion: 'booking.surface', kind: 'action.receipt', actionId, contextRef: 'ctx-availability', status, revision, observation: { kind: 'gap', code: 'hotel_rates_failed', factRefs: [] }, resultContract: { outcome: 'partial', hardCriteriaMet: false, factRefs: [], gapCodes: ['hotel_rates_failed'], blockers: [], relaxationsApplied: [] } });
17
+ // UAT-facing product availability observation: these are exact least-privilege
18
+ // sets, and are intentionally separate from the full tenant action vocabulary.
19
+ assert.deepEqual(bookingSurfaceAllowedActions('storefront'), ['search.patch', 'search.run', 'results.view.patch', 'hotel.focus']);
20
+ assert.deepEqual(bookingSurfaceAllowedActions('payment_link'), ['search.patch', 'search.run', 'results.view.patch', 'hotel.focus', 'hotel.select']);
21
+ // Lifetime budget mutation guard: these assertions exercise the transitions,
22
+ // not just the exported constants, so changing a comparison or reset policy
23
+ // makes this proof fail.
138
24
  {
139
25
  assert.equal(MAX_HOTELS_PER_TASK, 5);
140
26
  assert.equal(MAX_OFFER_CHECKS_PER_HOTEL, 2);
141
27
  assert.equal(MAX_OFFER_QUERIES_PER_HOTEL, 2);
142
- const unavailableWorkspace = ws(0, [
143
- 'budget-hotel'
144
- ], [
145
- [
146
- 'budget-hotel:ratepkg-1',
147
- 'budget-hotel'
148
- ]
149
- ]);
28
+ const unavailableWorkspace = ws(0, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]);
150
29
  let exhausted = recordOfferCheckIssued(createAvailabilityPolicy(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'budget-check-1');
151
- exhausted = recordOfferCheckReceipt(exhausted, ws(1, [
152
- 'budget-hotel'
153
- ], [
154
- [
155
- 'budget-hotel:ratepkg-1',
156
- 'budget-hotel'
157
- ]
158
- ]), receipt('budget-check-1', 1, 'budget-hotel:ratepkg-1'), 'budget-check-1', 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
30
+ exhausted = recordOfferCheckReceipt(exhausted, ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), receipt('budget-check-1', 1, 'budget-hotel:ratepkg-1'), 'budget-check-1', 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
159
31
  assert.equal(exhausted.hotels['budget-hotel']?.freshOffersRequired, true, 'first unavailable opens exactly one fresh-generation transition');
160
- exhausted = recordOffersQueryIssued(exhausted, [
161
- 'budget-hotel'
162
- ], ws(1, [
163
- 'budget-hotel'
164
- ], [
165
- [
166
- 'budget-hotel:ratepkg-1',
167
- 'budget-hotel'
168
- ]
169
- ]), 'budget-query-1');
170
- const reusedReceipt = offersReceipt('budget-query-1', 2, [
171
- 'budget-hotel'
172
- ], [
173
- 'budget-hotel:ratepkg-1'
174
- ], 1);
175
- const reused = recordOffersGeneration(exhausted, [
176
- 'budget-hotel'
177
- ], ws(2, [
178
- 'budget-hotel'
179
- ], [
180
- [
181
- 'budget-hotel:ratepkg-1',
182
- 'budget-hotel'
183
- ]
184
- ]), 'budget-query-1', digest(reusedReceipt), reusedReceipt);
32
+ exhausted = recordOffersQueryIssued(exhausted, ['budget-hotel'], ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), 'budget-query-1');
33
+ const reusedReceipt = offersReceipt('budget-query-1', 2, ['budget-hotel'], ['budget-hotel:ratepkg-1'], 1);
34
+ const reused = recordOffersGeneration(exhausted, ['budget-hotel'], ws(2, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), 'budget-query-1', digest(reusedReceipt), reusedReceipt);
185
35
  assert.deepEqual(reused.hotels['budget-hotel']?.currentOfferRefs, [], 'fresh HotelRates cannot re-admit the checked stale OfferRef');
186
36
  assert.equal(reused.hotels['budget-hotel']?.freshOffersRequired, true, 'stale OfferRef reuse leaves an explicit refresh transition');
187
- const refreshedReceipt = offersReceipt('budget-query-1', 2, [
188
- 'budget-hotel'
189
- ], [
190
- 'budget-hotel:ratepkg-2'
191
- ], 1);
192
- exhausted = recordOffersGeneration(exhausted, [
193
- 'budget-hotel'
194
- ], ws(2, [
195
- 'budget-hotel'
196
- ], [
197
- [
198
- 'budget-hotel:ratepkg-2',
199
- 'budget-hotel'
200
- ]
201
- ]), 'budget-query-1', digest(refreshedReceipt), refreshedReceipt);
202
- exhausted = recordOfferCheckIssued(exhausted, ws(2, [
203
- 'budget-hotel'
204
- ], [
205
- [
206
- 'budget-hotel:ratepkg-2',
207
- 'budget-hotel'
208
- ]
209
- ]), 'budget-hotel:ratepkg-2', versionFor('budget-hotel:ratepkg-2'), 'budget-check-2');
210
- exhausted = recordOfferCheckReceipt(exhausted, ws(3, [
211
- 'budget-hotel'
212
- ], [
213
- [
214
- 'budget-hotel:ratepkg-2',
215
- 'budget-hotel'
216
- ]
217
- ]), receipt('budget-check-2', 3, 'budget-hotel:ratepkg-2'), 'budget-check-2', 'budget-hotel:ratepkg-2', versionFor('budget-hotel:ratepkg-2'), 2);
37
+ const refreshedReceipt = offersReceipt('budget-query-1', 2, ['budget-hotel'], ['budget-hotel:ratepkg-2'], 1);
38
+ exhausted = recordOffersGeneration(exhausted, ['budget-hotel'], ws(2, ['budget-hotel'], [['budget-hotel:ratepkg-2', 'budget-hotel']]), 'budget-query-1', digest(refreshedReceipt), refreshedReceipt);
39
+ exhausted = recordOfferCheckIssued(exhausted, ws(2, ['budget-hotel'], [['budget-hotel:ratepkg-2', 'budget-hotel']]), 'budget-hotel:ratepkg-2', versionFor('budget-hotel:ratepkg-2'), 'budget-check-2');
40
+ exhausted = recordOfferCheckReceipt(exhausted, ws(3, ['budget-hotel'], [['budget-hotel:ratepkg-2', 'budget-hotel']]), receipt('budget-check-2', 3, 'budget-hotel:ratepkg-2'), 'budget-check-2', 'budget-hotel:ratepkg-2', versionFor('budget-hotel:ratepkg-2'), 2);
218
41
  assert.equal(exhausted.hotels['budget-hotel']?.checksIssued, 2, 'second unavailable consumes the lifetime CheckAvail budget');
219
42
  assert.equal(exhausted.terminal?.code, 'availability_exhausted_complete', 'second unavailable reaches conclusive exhaustion');
220
- assert.throws(()=>recordOfferCheckIssued(exhausted, ws(3, [
221
- 'budget-hotel'
222
- ], [
223
- [
224
- 'budget-hotel:ratepkg-1',
225
- 'budget-hotel'
226
- ]
227
- ]), 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'budget-check-3'), /availability_terminal/, 'third CheckAvail cannot be issued after terminal exhaustion');
228
- const unavailableWithGap = (actionId, revision)=>({
229
- ...receipt(actionId, revision, 'budget-hotel:ratepkg-1'),
230
- observation: {
231
- kind: 'offer.availability',
232
- offerRef: 'budget-hotel:ratepkg-1',
233
- checkedOfferVersionRef: versionFor('budget-hotel:ratepkg-1'),
234
- available: false,
235
- changedFactRefs: [],
236
- gapCodes: [
237
- 'hotel_rates_failed'
238
- ]
239
- },
240
- resultContract: {
241
- outcome: 'empty',
242
- hardCriteriaMet: false,
243
- factRefs: [],
244
- gapCodes: [
245
- 'hotel_rates_failed'
246
- ],
247
- blockers: [],
248
- relaxationsApplied: []
249
- }
250
- });
43
+ assert.throws(() => recordOfferCheckIssued(exhausted, ws(3, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'budget-check-3'), /availability_terminal/, 'third CheckAvail cannot be issued after terminal exhaustion');
44
+ const unavailableWithGap = (actionId, revision) => ({
45
+ ...receipt(actionId, revision, 'budget-hotel:ratepkg-1'),
46
+ observation: { kind: 'offer.availability', offerRef: 'budget-hotel:ratepkg-1', checkedOfferVersionRef: versionFor('budget-hotel:ratepkg-1'), available: false, changedFactRefs: [], gapCodes: ['hotel_rates_failed'] },
47
+ resultContract: { outcome: 'empty', hardCriteriaMet: false, factRefs: [], gapCodes: ['hotel_rates_failed'], blockers: [], relaxationsApplied: [] },
48
+ });
251
49
  let inconclusive = recordOfferCheckIssued(createAvailabilityPolicy(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'gap-check-1');
252
- inconclusive = recordOfferCheckReceipt(inconclusive, ws(1, [
253
- 'budget-hotel'
254
- ], [
255
- [
256
- 'budget-hotel:ratepkg-1',
257
- 'budget-hotel'
258
- ]
259
- ]), unavailableWithGap('gap-check-1', 1), 'gap-check-1', 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
50
+ inconclusive = recordOfferCheckReceipt(inconclusive, ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), unavailableWithGap('gap-check-1', 1), 'gap-check-1', 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
260
51
  assert.equal(inconclusive.hotels['budget-hotel']?.status, 'inconclusive', 'generic gap is not accepted as a typed negative');
261
52
  assert.equal(inconclusive.hotels['budget-hotel']?.tombstonedOfferRefs.includes('budget-hotel:ratepkg-1'), false, 'generic gap cannot tombstone logical offer');
262
53
  assert.equal(inconclusive.hotels['budget-hotel']?.tombstonedOfferVersionRefs.includes(versionFor('budget-hotel:ratepkg-1')), false, 'generic gap cannot tombstone offer version');
263
- inconclusive = recordOffersQueryIssued(inconclusive, [
264
- 'budget-hotel'
265
- ], ws(1, [
266
- 'budget-hotel'
267
- ], [
268
- [
269
- 'budget-hotel:ratepkg-1',
270
- 'budget-hotel'
271
- ]
272
- ]), 'gap-query-1');
273
- const gapRefreshedReceipt = offersReceipt('gap-query-1', 2, [
274
- 'budget-hotel'
275
- ], [
276
- 'budget-hotel:ratepkg-3'
277
- ], 1);
278
- inconclusive = recordOffersGeneration(inconclusive, [
279
- 'budget-hotel'
280
- ], ws(2, [
281
- 'budget-hotel'
282
- ], [
283
- [
284
- 'budget-hotel:ratepkg-3',
285
- 'budget-hotel'
286
- ]
287
- ]), 'gap-query-1', digest(gapRefreshedReceipt), gapRefreshedReceipt);
288
- inconclusive = recordOfferCheckIssued(inconclusive, ws(2, [
289
- 'budget-hotel'
290
- ], [
291
- [
292
- 'budget-hotel:ratepkg-3',
293
- 'budget-hotel'
294
- ]
295
- ]), 'budget-hotel:ratepkg-3', versionFor('budget-hotel:ratepkg-3'), 'gap-check-2');
296
- inconclusive = recordOfferCheckReceipt(inconclusive, ws(3, [
297
- 'budget-hotel'
298
- ], [
299
- [
300
- 'budget-hotel:ratepkg-3',
301
- 'budget-hotel'
302
- ]
303
- ]), {
304
- ...unavailableWithGap('gap-check-2', 3),
305
- observation: {
306
- kind: 'offer.availability',
307
- offerRef: 'budget-hotel:ratepkg-3',
308
- checkedOfferVersionRef: versionFor('budget-hotel:ratepkg-3'),
309
- available: false,
310
- changedFactRefs: [],
311
- gapCodes: []
312
- }
313
- }, 'gap-check-2', 'budget-hotel:ratepkg-3', versionFor('budget-hotel:ratepkg-3'), 2);
54
+ inconclusive = recordOffersQueryIssued(inconclusive, ['budget-hotel'], ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), 'gap-query-1');
55
+ const gapRefreshedReceipt = offersReceipt('gap-query-1', 2, ['budget-hotel'], ['budget-hotel:ratepkg-3'], 1);
56
+ inconclusive = recordOffersGeneration(inconclusive, ['budget-hotel'], ws(2, ['budget-hotel'], [['budget-hotel:ratepkg-3', 'budget-hotel']]), 'gap-query-1', digest(gapRefreshedReceipt), gapRefreshedReceipt);
57
+ inconclusive = recordOfferCheckIssued(inconclusive, ws(2, ['budget-hotel'], [['budget-hotel:ratepkg-3', 'budget-hotel']]), 'budget-hotel:ratepkg-3', versionFor('budget-hotel:ratepkg-3'), 'gap-check-2');
58
+ inconclusive = recordOfferCheckReceipt(inconclusive, ws(3, ['budget-hotel'], [['budget-hotel:ratepkg-3', 'budget-hotel']]), { ...unavailableWithGap('gap-check-2', 3), observation: { kind: 'offer.availability', offerRef: 'budget-hotel:ratepkg-3', checkedOfferVersionRef: versionFor('budget-hotel:ratepkg-3'), available: false, changedFactRefs: [], gapCodes: [] } }, 'gap-check-2', 'budget-hotel:ratepkg-3', versionFor('budget-hotel:ratepkg-3'), 2);
314
59
  assert.equal(inconclusive.terminal?.code, 'availability_exhausted_inconclusive', 'an unavailable receipt with a gap never masquerades as conclusive exhaustion');
315
- for (const status of [
316
- 'unavailable',
317
- 'no_match'
318
- ]){
60
+ for (const status of ['unavailable', 'no_match']) {
319
61
  const gapAction = `generic-gap-${status}`;
320
62
  const gapState = recordOfferCheckIssued(createAvailabilityPolicy(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), gapAction);
321
- const gap = {
322
- ...receipt(gapAction, 1, 'budget-hotel:ratepkg-1', status),
323
- observation: {
324
- kind: 'offer.availability',
325
- offerRef: 'budget-hotel:ratepkg-1',
326
- checkedOfferVersionRef: versionFor('budget-hotel:ratepkg-1'),
327
- available: false,
328
- changedFactRefs: [],
329
- gapCodes: [
330
- 'hotel_rates_failed'
331
- ]
332
- },
333
- resultContract: {
334
- outcome: 'empty',
335
- hardCriteriaMet: false,
336
- factRefs: [],
337
- gapCodes: [
338
- 'hotel_rates_failed'
339
- ],
340
- blockers: [],
341
- relaxationsApplied: []
342
- }
343
- };
344
- const afterGap = recordOfferCheckReceipt(gapState, ws(1, [
345
- 'budget-hotel'
346
- ], [
347
- [
348
- 'budget-hotel:ratepkg-1',
349
- 'budget-hotel'
350
- ]
351
- ]), gap, gapAction, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
63
+ const gap = { ...receipt(gapAction, 1, 'budget-hotel:ratepkg-1', status), observation: { kind: 'offer.availability', offerRef: 'budget-hotel:ratepkg-1', checkedOfferVersionRef: versionFor('budget-hotel:ratepkg-1'), available: false, changedFactRefs: [], gapCodes: ['hotel_rates_failed'] }, resultContract: { outcome: 'empty', hardCriteriaMet: false, factRefs: [], gapCodes: ['hotel_rates_failed'], blockers: [], relaxationsApplied: [] } };
64
+ const afterGap = recordOfferCheckReceipt(gapState, ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), gap, gapAction, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
352
65
  assert.equal(afterGap.hotels['budget-hotel']?.status, 'inconclusive', `${status} with generic gap remains inconclusive`);
353
66
  assert.equal(afterGap.hotels['budget-hotel']?.tombstonedOfferRefs.includes('budget-hotel:ratepkg-1'), false, `${status} with generic gap cannot tombstone logical offer`);
354
67
  assert.equal(afterGap.hotels['budget-hotel']?.tombstonedOfferVersionRefs.includes(versionFor('budget-hotel:ratepkg-1')), false, `${status} with generic gap cannot tombstone version`);
355
68
  }
356
69
  let queryBudget = recordOfferCheckIssued(createAvailabilityPolicy(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'query-budget-check');
357
- queryBudget = recordOfferCheckReceipt(queryBudget, ws(1, [
358
- 'budget-hotel'
359
- ], [
360
- [
361
- 'budget-hotel:ratepkg-1',
362
- 'budget-hotel'
363
- ]
364
- ]), receipt('query-budget-check', 1, 'budget-hotel:ratepkg-1'), 'query-budget-check', 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
365
- for (const [actionId, revision] of [
366
- [
367
- 'query-budget-1',
368
- 2
369
- ],
370
- [
371
- 'query-budget-2',
372
- 3
373
- ]
374
- ]){
70
+ queryBudget = recordOfferCheckReceipt(queryBudget, ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), receipt('query-budget-check', 1, 'budget-hotel:ratepkg-1'), 'query-budget-check', 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
71
+ for (const [actionId, revision] of [['query-budget-1', 2], ['query-budget-2', 3]]) {
375
72
  const offerRef = `budget-hotel:query-ratepkg-${revision}`;
376
- queryBudget = recordOffersQueryIssued(queryBudget, [
377
- 'budget-hotel'
378
- ], ws(revision - 1, [
379
- 'budget-hotel'
380
- ], [
381
- [
382
- offerRef,
383
- 'budget-hotel'
384
- ]
385
- ]), actionId);
386
- const queryReceipt = offersReceipt(actionId, revision, [
387
- 'budget-hotel'
388
- ], [
389
- offerRef
390
- ], 1);
391
- queryBudget = recordOffersGeneration(queryBudget, [
392
- 'budget-hotel'
393
- ], ws(revision, [
394
- 'budget-hotel'
395
- ], [
396
- [
397
- offerRef,
398
- 'budget-hotel'
399
- ]
400
- ]), actionId, digest(queryReceipt), queryReceipt);
73
+ queryBudget = recordOffersQueryIssued(queryBudget, ['budget-hotel'], ws(revision - 1, ['budget-hotel'], [[offerRef, 'budget-hotel']]), actionId);
74
+ const queryReceipt = offersReceipt(actionId, revision, ['budget-hotel'], [offerRef], 1);
75
+ queryBudget = recordOffersGeneration(queryBudget, ['budget-hotel'], ws(revision, ['budget-hotel'], [[offerRef, 'budget-hotel']]), actionId, digest(queryReceipt), queryReceipt);
401
76
  }
402
77
  assert.equal(queryBudget.hotels['budget-hotel']?.offerQueriesIssued, 2, 'two refresh queries are retained as lifetime usage');
403
- assert.throws(()=>recordOffersQueryIssued(queryBudget, [
404
- 'budget-hotel'
405
- ], ws(3, [
406
- 'budget-hotel'
407
- ], [
408
- [
409
- 'budget-hotel:ratepkg-1',
410
- 'budget-hotel'
411
- ]
412
- ]), 'query-budget-3'), /availability_offer_query_limit_reached/, 'third offers.query is rejected');
413
- const hotels = [
414
- 'h1',
415
- 'h2',
416
- 'h3',
417
- 'h4',
418
- 'h5',
419
- 'h6'
420
- ];
421
- const candidateWorkspace = ws(0, hotels, hotels.map((hotelRef, index)=>[
422
- `candidate-${index + 1}`,
423
- hotelRef
424
- ]), [
425
- 'candidate-6',
426
- 'candidate-1',
427
- 'candidate-2',
428
- 'candidate-3',
429
- 'candidate-4',
430
- 'candidate-5'
431
- ]);
78
+ assert.throws(() => recordOffersQueryIssued(queryBudget, ['budget-hotel'], ws(3, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), 'query-budget-3'), /availability_offer_query_limit_reached/, 'third offers.query is rejected');
79
+ const hotels = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
80
+ const candidateWorkspace = ws(0, hotels, hotels.map((hotelRef, index) => [`candidate-${index + 1}`, hotelRef]), ['candidate-6', 'candidate-1', 'candidate-2', 'candidate-3', 'candidate-4', 'candidate-5']);
432
81
  const candidates = recordOfferCheckIssued(createAvailabilityPolicy(candidateWorkspace), candidateWorkspace, 'candidate-6', versionFor('candidate-6'), 'candidate-check');
433
- assert.deepEqual(candidates.hotelRefs, [
434
- 'h6',
435
- 'h1',
436
- 'h2',
437
- 'h3',
438
- 'h4'
439
- ], 'selected hotel is first and recovery freezes at five candidates');
82
+ assert.deepEqual(candidates.hotelRefs, ['h6', 'h1', 'h2', 'h3', 'h4'], 'selected hotel is first and recovery freezes at five candidates');
440
83
  assert.equal(candidates.hotelRefs.includes('h5'), false, 'sixth shortlisted hotel never enters the recovery epoch');
441
- const duplicateWorkspace = ws(0, [
442
- 'dup-a',
443
- 'dup-b'
444
- ], [
445
- [
446
- 'dup-offer',
447
- 'dup-a'
448
- ],
449
- [
450
- 'dup-offer',
451
- 'dup-b'
452
- ]
453
- ]);
454
- assert.throws(()=>createAvailabilityPolicy(duplicateWorkspace), /availability_duplicate_offer_ref/, 'loaded OfferRef must be globally unique before any .find-based authority lookup');
455
- assert.throws(()=>canIssueOfferCheck(createAvailabilityPolicy(ws(0, [
456
- 'dup-a'
457
- ], [])), duplicateWorkspace, 'dup-offer', versionFor('dup-offer')), /availability_duplicate_offer_ref/);
458
- }{
459
- const roomList = ws(0, [
460
- 'room-hotel'
461
- ], [
462
- [
463
- 'room-1',
464
- 'room-hotel'
465
- ],
466
- [
467
- 'room-2',
468
- 'room-hotel'
469
- ],
470
- [
471
- 'room-3',
472
- 'room-hotel'
473
- ],
474
- [
475
- 'room-4',
476
- 'room-hotel'
477
- ]
478
- ], [
479
- 'room-4'
480
- ]);
84
+ const duplicateWorkspace = ws(0, ['dup-a', 'dup-b'], [['dup-offer', 'dup-a'], ['dup-offer', 'dup-b']]);
85
+ assert.throws(() => createAvailabilityPolicy(duplicateWorkspace), /availability_duplicate_offer_ref/, 'loaded OfferRef must be globally unique before any .find-based authority lookup');
86
+ assert.throws(() => canIssueOfferCheck(createAvailabilityPolicy(ws(0, ['dup-a'], [])), duplicateWorkspace, 'dup-offer', versionFor('dup-offer')), /availability_duplicate_offer_ref/);
87
+ }
88
+ // Locked cap and source cases: a selected RoomList offer wins ordering, while
89
+ // a complete recovery generation never admits more than three offers.
90
+ {
91
+ const roomList = ws(0, ['room-hotel'], [['room-1', 'room-hotel'], ['room-2', 'room-hotel'], ['room-3', 'room-hotel'], ['room-4', 'room-hotel']], ['room-4']);
481
92
  const selected = recordOfferCheckIssued(createAvailabilityPolicy(roomList), roomList, 'room-4', versionFor('room-4'), 'room-check');
482
- assert.deepEqual(selected.hotels['room-hotel']?.currentOfferRefs, [
483
- 'room-4',
484
- 'room-1',
485
- 'room-2'
486
- ]);
93
+ assert.deepEqual(selected.hotels['room-hotel']?.currentOfferRefs, ['room-4', 'room-1', 'room-2']);
487
94
  assert.equal(selected.hotels['room-hotel']?.currentGeneration?.source.kind, 'workspace_snapshot');
488
- const complete = ws(0, [
489
- 'complete-hotel'
490
- ], [
491
- [
492
- 'complete-1',
493
- 'complete-hotel'
494
- ]
495
- ]);
95
+ const complete = ws(0, ['complete-hotel'], [['complete-1', 'complete-hotel']]);
496
96
  const completeStarted = recordOfferCheckIssued(createAvailabilityPolicy(complete), complete, 'complete-1', versionFor('complete-1'), 'complete-check');
497
- const completeQuery = recordOffersQueryIssued(completeStarted, [
498
- 'complete-hotel'
499
- ], complete, 'complete-query', digest(criteria), criteria);
500
- const fourOfferWorkspace = ws(1, [
501
- 'complete-hotel'
502
- ], [
503
- [
504
- 'complete-1',
505
- 'complete-hotel'
506
- ],
507
- [
508
- 'complete-2',
509
- 'complete-hotel'
510
- ],
511
- [
512
- 'complete-3',
513
- 'complete-hotel'
514
- ],
515
- [
516
- 'complete-4',
517
- 'complete-hotel'
518
- ]
519
- ]);
520
- assert.throws(()=>recordOffersGeneration(completeQuery, [
521
- 'complete-hotel'
522
- ], fourOfferWorkspace, 'complete-query', digest(offersReceipt('complete-query', 1, [
523
- 'complete-hotel'
524
- ], [
525
- 'complete-1',
526
- 'complete-2',
527
- 'complete-3',
528
- 'complete-4'
529
- ], 1)), offersReceipt('complete-query', 1, [
530
- 'complete-hotel'
531
- ], [
532
- 'complete-1',
533
- 'complete-2',
534
- 'complete-3',
535
- 'complete-4'
536
- ], 1), digest(criteria), criteria), /availability_offer_limit_exceeded/);
537
- }{
538
- const partialWorkspace = ws(0, [
539
- 'partial-hotel'
540
- ], [
541
- [
542
- 'partial-1',
543
- 'partial-hotel'
544
- ],
545
- [
546
- 'partial-2',
547
- 'partial-hotel'
548
- ],
549
- [
550
- 'partial-3',
551
- 'partial-hotel'
552
- ]
553
- ]);
97
+ const completeQuery = recordOffersQueryIssued(completeStarted, ['complete-hotel'], complete, 'complete-query', digest(criteria), criteria);
98
+ const fourOfferWorkspace = ws(1, ['complete-hotel'], [['complete-1', 'complete-hotel'], ['complete-2', 'complete-hotel'], ['complete-3', 'complete-hotel'], ['complete-4', 'complete-hotel']]);
99
+ assert.throws(() => recordOffersGeneration(completeQuery, ['complete-hotel'], fourOfferWorkspace, 'complete-query', digest(offersReceipt('complete-query', 1, ['complete-hotel'], ['complete-1', 'complete-2', 'complete-3', 'complete-4'], 1)), offersReceipt('complete-query', 1, ['complete-hotel'], ['complete-1', 'complete-2', 'complete-3', 'complete-4'], 1), digest(criteria), criteria), /availability_offer_limit_exceeded/);
100
+ }
101
+ // Locked partial/empty reconciliation: a partial receipt may only project an
102
+ // evidenced subset, and a clean empty receipt must agree with workspace truth.
103
+ {
104
+ const partialWorkspace = ws(0, ['partial-hotel'], [['partial-1', 'partial-hotel'], ['partial-2', 'partial-hotel'], ['partial-3', 'partial-hotel']]);
554
105
  const partialStarted = recordOfferCheckIssued(createAvailabilityPolicy(partialWorkspace), partialWorkspace, 'partial-1', versionFor('partial-1'), 'partial-check');
555
- const partialQuery = recordOffersQueryIssued(partialStarted, [
556
- 'partial-hotel'
557
- ], partialWorkspace, 'partial-query', digest(criteria), criteria);
558
- const partialReceipt = offersReceipt('partial-query', 1, [
559
- 'partial-hotel'
560
- ], [
561
- 'partial-2'
562
- ], 1, 'partial', 'partial', false);
563
- const partial = recordOffersGeneration(partialQuery, [
564
- 'partial-hotel'
565
- ], ws(1, [
566
- 'partial-hotel'
567
- ], [
568
- [
569
- 'partial-1',
570
- 'partial-hotel'
571
- ],
572
- [
573
- 'partial-2',
574
- 'partial-hotel'
575
- ],
576
- [
577
- 'partial-3',
578
- 'partial-hotel'
579
- ]
580
- ]), 'partial-query', digest(partialReceipt), partialReceipt, digest(criteria), criteria);
106
+ const partialQuery = recordOffersQueryIssued(partialStarted, ['partial-hotel'], partialWorkspace, 'partial-query', digest(criteria), criteria);
107
+ const partialReceipt = offersReceipt('partial-query', 1, ['partial-hotel'], ['partial-2'], 1, 'partial', 'partial', false);
108
+ const partial = recordOffersGeneration(partialQuery, ['partial-hotel'], ws(1, ['partial-hotel'], [['partial-1', 'partial-hotel'], ['partial-2', 'partial-hotel'], ['partial-3', 'partial-hotel']]), 'partial-query', digest(partialReceipt), partialReceipt, digest(criteria), criteria);
581
109
  assert.equal(partial.hotels['partial-hotel']?.currentGeneration?.evidence, 'partial');
582
110
  assert.equal(partial.hotels['partial-hotel']?.currentOfferRefs.length, 0);
583
111
  assert.equal(partial.hotels['partial-hotel']?.freshOffersRequired, true);
584
- assert.throws(()=>recordOffersGeneration(partialQuery, [
585
- 'partial-hotel'
586
- ], ws(1, [
587
- 'partial-hotel'
588
- ], [
589
- [
590
- 'partial-1',
591
- 'partial-hotel'
592
- ],
593
- [
594
- 'partial-2',
595
- 'partial-hotel'
596
- ]
597
- ]), 'partial-query', digest(offersReceipt('partial-query', 1, [
598
- 'partial-hotel'
599
- ], [
600
- 'partial-1'
601
- ], 2, 'partial', 'partial', false)), offersReceipt('partial-query', 1, [
602
- 'partial-hotel'
603
- ], [
604
- 'partial-1'
605
- ], 2, 'partial', 'partial', false), digest(criteria), criteria), /availability_offers_count_mismatch/);
606
- const emptyWorkspace = ws(0, [
607
- 'empty-hotel'
608
- ], [
609
- [
610
- 'empty-1',
611
- 'empty-hotel'
612
- ]
613
- ]);
112
+ assert.throws(() => recordOffersGeneration(partialQuery, ['partial-hotel'], ws(1, ['partial-hotel'], [['partial-1', 'partial-hotel'], ['partial-2', 'partial-hotel']]), 'partial-query', digest(offersReceipt('partial-query', 1, ['partial-hotel'], ['partial-1'], 2, 'partial', 'partial', false)), offersReceipt('partial-query', 1, ['partial-hotel'], ['partial-1'], 2, 'partial', 'partial', false), digest(criteria), criteria), /availability_offers_count_mismatch/);
113
+ const emptyWorkspace = ws(0, ['empty-hotel'], [['empty-1', 'empty-hotel']]);
614
114
  const emptyStarted = recordOfferCheckIssued(createAvailabilityPolicy(emptyWorkspace), emptyWorkspace, 'empty-1', versionFor('empty-1'), 'empty-check');
615
- const emptyQuery = recordOffersQueryIssued(emptyStarted, [
616
- 'empty-hotel'
617
- ], emptyWorkspace, 'empty-query');
618
- const empty = offersReceipt('empty-query', 1, [
619
- 'empty-hotel'
620
- ], [], 0, 'no_match', 'empty', false);
621
- assert.throws(()=>recordOffersGeneration(emptyQuery, [
622
- 'empty-hotel'
623
- ], emptyWorkspace, 'empty-query', digest(empty), empty), /availability_offers_workspace_mismatch/);
624
- }{
625
- const seedWorkspace = ws(0, [
626
- 'seed-hotel'
627
- ], [
628
- [
629
- 'seed-1',
630
- 'seed-hotel'
631
- ]
632
- ]);
633
- const seed = offersReceipt('seed-query', 1, [
634
- 'seed-hotel'
635
- ], [
636
- 'seed-1'
637
- ], 1);
638
- const seeded = recordObservedOffersQuery(createAvailabilityPolicy(seedWorkspace), [
639
- 'seed-hotel'
640
- ], digest(criteria), seedWorkspace, seed, 'seed-query', digest(seed), criteria);
115
+ const emptyQuery = recordOffersQueryIssued(emptyStarted, ['empty-hotel'], emptyWorkspace, 'empty-query');
116
+ const empty = offersReceipt('empty-query', 1, ['empty-hotel'], [], 0, 'no_match', 'empty', false);
117
+ assert.throws(() => recordOffersGeneration(emptyQuery, ['empty-hotel'], emptyWorkspace, 'empty-query', digest(empty), empty), /availability_offers_workspace_mismatch/);
118
+ }
119
+ // Locked provenance and verification cases: failed ordinary queries erase the
120
+ // seed, while a CheckAvail claim is confirmed only by the current workspace's
121
+ // verifiedOffer capability; changed is inconclusive and requires a fresh generation.
122
+ {
123
+ const seedWorkspace = ws(0, ['seed-hotel'], [['seed-1', 'seed-hotel']]);
124
+ const seed = offersReceipt('seed-query', 1, ['seed-hotel'], ['seed-1'], 1);
125
+ const seeded = recordObservedOffersQuery(createAvailabilityPolicy(seedWorkspace), ['seed-hotel'], digest(criteria), seedWorkspace, seed, 'seed-query', digest(seed), criteria);
641
126
  assert.equal(seeded.criteriaDigest, digest(criteria));
642
- const cleared = recordObservedOffersQuery(seeded, [
643
- 'seed-hotel'
644
- ], digest(criteria), ws(2, [
645
- 'seed-hotel'
646
- ], []), gapReceipt('failed-query', 2), 'failed-query', digest(gapReceipt('failed-query', 2)), criteria);
127
+ const cleared = recordObservedOffersQuery(seeded, ['seed-hotel'], digest(criteria), ws(2, ['seed-hotel'], []), gapReceipt('failed-query', 2), 'failed-query', digest(gapReceipt('failed-query', 2)), criteria);
647
128
  assert.equal(cleared.criteria, undefined);
648
129
  assert.equal(cleared.criteriaDigest, undefined);
649
130
  assert.equal(cleared.lastQuerySourceActionId, undefined);
650
- const verificationWorkspace = ws(0, [
651
- 'verify-hotel'
652
- ], [
653
- [
654
- 'verify-1',
655
- 'verify-hotel'
656
- ]
657
- ]);
131
+ const verificationWorkspace = ws(0, ['verify-hotel'], [['verify-1', 'verify-hotel']]);
658
132
  const verificationStarted = recordOfferCheckIssued(createAvailabilityPolicy(verificationWorkspace), verificationWorkspace, 'verify-1', versionFor('verify-1'), 'verify-check');
659
- const wrongVerified = {
660
- ...verificationWorkspace,
661
- revision: 1,
662
- verifiedOffer: verifiedCapability('other-offer', versionFor('other-offer'))
663
- };
133
+ const wrongVerified = { ...verificationWorkspace, revision: 1, verifiedOffer: verifiedCapability('other-offer', versionFor('other-offer')) };
664
134
  const wrong = recordOfferCheckReceipt(verificationStarted, wrongVerified, receipt('verify-check', 1, 'verify-1', 'applied', true), 'verify-check', 'verify-1', versionFor('verify-1'), 0);
665
135
  assert.notEqual(wrong.terminal?.code, 'availability_confirmed');
666
136
  assert.equal(wrong.hotels['verify-hotel']?.status, 'inconclusive');
667
137
  const matchingStarted = recordOfferCheckIssued(createAvailabilityPolicy(verificationWorkspace), verificationWorkspace, 'verify-1', versionFor('verify-1'), 'verify-match');
668
- const matchingWorkspace = {
669
- ...verificationWorkspace,
670
- revision: 1,
671
- verifiedOffer: verifiedCapability('verify-1')
672
- };
138
+ const matchingWorkspace = { ...verificationWorkspace, revision: 1, verifiedOffer: verifiedCapability('verify-1') };
673
139
  const matching = recordOfferCheckReceipt(matchingStarted, matchingWorkspace, receipt('verify-match', 1, 'verify-1', 'applied', true), 'verify-match', 'verify-1', versionFor('verify-1'), 0);
674
140
  assert.equal(matching.terminal?.code, 'availability_confirmed');
675
- const verificationSeedReceipt = offersReceipt('verify-seed', 0, [
676
- 'verify-hotel'
677
- ], [
678
- 'verify-1'
679
- ], 1);
680
- const verificationSeed = recordObservedOffersQuery(createAvailabilityPolicy(verificationWorkspace), [
681
- 'verify-hotel'
682
- ], digest(criteria), verificationWorkspace, verificationSeedReceipt, 'verify-seed', digest(verificationSeedReceipt), criteria);
141
+ const verificationSeedReceipt = offersReceipt('verify-seed', 0, ['verify-hotel'], ['verify-1'], 1);
142
+ const verificationSeed = recordObservedOffersQuery(createAvailabilityPolicy(verificationWorkspace), ['verify-hotel'], digest(criteria), verificationWorkspace, verificationSeedReceipt, 'verify-seed', digest(verificationSeedReceipt), criteria);
683
143
  const changedStarted = recordOfferCheckIssued(verificationSeed, verificationWorkspace, 'verify-1', versionFor('verify-1'), 'verify-changed');
684
- const changedReceipt = {
685
- ...receipt('verify-changed', 1, 'verify-1', 'changed', true, versionFor('verify-1'), versionFor('verify-1', 'v2')),
686
- observation: {
687
- kind: 'offer.availability',
688
- offerRef: 'verify-1',
689
- checkedOfferVersionRef: versionFor('verify-1'),
690
- currentOfferVersionRef: versionFor('verify-1', 'v2'),
691
- available: true,
692
- changedFactRefs: [
693
- 'price'
694
- ],
695
- gapCodes: []
696
- },
697
- resultContract: {
698
- outcome: 'partial',
699
- hardCriteriaMet: false,
700
- factRefs: [],
701
- gapCodes: [],
702
- blockers: [],
703
- relaxationsApplied: []
704
- }
705
- };
706
- const changed = recordOfferCheckReceipt(changedStarted, {
707
- ...verificationWorkspace,
708
- revision: 1,
709
- loadedOffers: [
710
- {
711
- offerRef: 'verify-1',
712
- offerVersionRef: versionFor('verify-1', 'v2'),
713
- hotelRef: 'verify-hotel',
714
- evidenceLevel: 'rate_loaded',
715
- factRefs: []
716
- }
717
- ]
718
- }, changedReceipt, 'verify-changed', 'verify-1', versionFor('verify-1'), 0);
144
+ const changedReceipt = { ...receipt('verify-changed', 1, 'verify-1', 'changed', true, versionFor('verify-1'), versionFor('verify-1', 'v2')), observation: { kind: 'offer.availability', offerRef: 'verify-1', checkedOfferVersionRef: versionFor('verify-1'), currentOfferVersionRef: versionFor('verify-1', 'v2'), available: true, changedFactRefs: ['price'], gapCodes: [] }, resultContract: { outcome: 'partial', hardCriteriaMet: false, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } };
145
+ const changed = recordOfferCheckReceipt(changedStarted, { ...verificationWorkspace, revision: 1, loadedOffers: [{ offerRef: 'verify-1', offerVersionRef: versionFor('verify-1', 'v2'), hotelRef: 'verify-hotel', evidenceLevel: 'rate_loaded', factRefs: [] }] }, changedReceipt, 'verify-changed', 'verify-1', versionFor('verify-1'), 0);
719
146
  assert.equal(changed.hotels['verify-hotel']?.status, 'active');
720
- assert.deepEqual(changed.hotels['verify-hotel']?.currentOfferRefs, [
721
- 'verify-1'
722
- ]);
147
+ assert.deepEqual(changed.hotels['verify-hotel']?.currentOfferRefs, ['verify-1']);
723
148
  assert.equal(changed.hotels['verify-hotel']?.tombstonedOfferVersionRefs.includes(versionFor('verify-1')), true);
724
149
  assert.equal(changed.hotels['verify-hotel']?.freshOffersRequired, false);
725
- }const initial = ws(0, [
726
- 'h1',
727
- 'h2'
728
- ], [
729
- [
730
- 'o1',
731
- 'h1'
732
- ],
733
- [
734
- 'o2',
735
- 'h1'
736
- ],
737
- [
738
- 'b1',
739
- 'h2'
740
- ]
741
- ], [
742
- 'b1'
743
- ]);
744
- assert.throws(()=>recordOffersQueryIssued(createAvailabilityPolicy(initial), [
745
- 'h1'
746
- ], initial, 'pre-q'), /recovery_not_started/);
150
+ }
151
+ const initial = ws(0, ['h1', 'h2'], [['o1', 'h1'], ['o2', 'h1'], ['b1', 'h2']], ['b1']);
152
+ assert.throws(() => recordOffersQueryIssued(createAvailabilityPolicy(initial), ['h1'], initial, 'pre-q'), /recovery_not_started/);
747
153
  let state = recordOfferCheckIssued(createAvailabilityPolicy(initial), initial, 'o1', versionFor('o1'), 'check-1');
748
- assert.deepEqual(state.hotelRefs, [
749
- 'h1',
750
- 'h2'
751
- ]);
154
+ assert.deepEqual(state.hotelRefs, ['h1', 'h2']);
752
155
  assert.equal(state.hotels.h1?.offerQueriesIssued, 0);
753
- state = recordOfferCheckReceipt(state, ws(1, [
754
- 'h1',
755
- 'h2'
756
- ], [
757
- [
758
- 'o1',
759
- 'h1'
760
- ],
761
- [
762
- 'o2',
763
- 'h1'
764
- ],
765
- [
766
- 'b1',
767
- 'h2'
768
- ]
769
- ]), receipt('check-1', 1, 'o1'), 'check-1', 'o1', versionFor('o1'), 0);
156
+ state = recordOfferCheckReceipt(state, ws(1, ['h1', 'h2'], [['o1', 'h1'], ['o2', 'h1'], ['b1', 'h2']]), receipt('check-1', 1, 'o1'), 'check-1', 'o1', versionFor('o1'), 0);
770
157
  assert.equal(state.hotels.h1?.freshOffersRequired, true);
771
- assert.equal(canIssueOfferCheck(state, ws(1, [
772
- 'h1',
773
- 'h2'
774
- ], [
775
- [
776
- 'o2',
777
- 'h1'
778
- ],
779
- [
780
- 'b1',
781
- 'h2'
782
- ]
783
- ]), 'b1', versionFor('b1')).ok, false);
784
- let q = recordOffersQueryIssued(state, [
785
- 'h1'
786
- ], ws(1, [
787
- 'h1',
788
- 'h2'
789
- ], [
790
- [
791
- 'o2',
792
- 'h1'
793
- ],
794
- [
795
- 'b1',
796
- 'h2'
797
- ]
798
- ]), 'q1', 'criteria-a');
799
- assert.throws(()=>recordOffersQueryIssued(q, [
800
- 'h1'
801
- ], ws(1, [
802
- 'h1',
803
- 'h2'
804
- ], []), 'q2', 'criteria-b'), /criteria_changed/);
805
- const staleQueryReceipt = {
806
- ...receipt('q1', 2, 'o2', 'stale'),
807
- observation: {
808
- kind: 'gap',
809
- code: 'stale_revision',
810
- factRefs: []
811
- }
812
- };
813
- q = foldQuery(q, [
814
- 'h1'
815
- ], ws(2, [
816
- 'h1',
817
- 'h2'
818
- ], [
819
- [
820
- 'o2',
821
- 'h1'
822
- ],
823
- [
824
- 'b1',
825
- 'h2'
826
- ]
827
- ]), 'q1', staleQueryReceipt, 'criteria-a');
158
+ assert.equal(canIssueOfferCheck(state, ws(1, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'b1', versionFor('b1')).ok, false);
159
+ let q = recordOffersQueryIssued(state, ['h1'], ws(1, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'q1', 'criteria-a');
160
+ assert.throws(() => recordOffersQueryIssued(q, ['h1'], ws(1, ['h1', 'h2'], []), 'q2', 'criteria-b'), /criteria_changed/);
161
+ const staleQueryReceipt = { ...receipt('q1', 2, 'o2', 'stale'), observation: { kind: 'gap', code: 'stale_revision', factRefs: [] } };
162
+ q = foldQuery(q, ['h1'], ws(2, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'q1', staleQueryReceipt, 'criteria-a');
828
163
  assert.equal(q.hotels.h1?.generationNo, 1);
829
164
  assert.equal(q.hotels.h1?.currentGeneration?.generationId, 'h1:generation:1');
830
- const completeQueryReceipt = {
831
- ...receipt('q2', 3, 'o2', 'applied'),
832
- observation: {
833
- kind: 'offers.state',
834
- hotelRefs: [
835
- 'h1'
836
- ],
837
- offerRefs: [
838
- 'o2'
839
- ],
840
- loadedHotelCount: 1
841
- }
842
- };
843
- q = recordOffersQueryIssued(q, [
844
- 'h1'
845
- ], ws(2, [
846
- 'h1',
847
- 'h2'
848
- ], [
849
- [
850
- 'o2',
851
- 'h1'
852
- ],
853
- [
854
- 'b1',
855
- 'h2'
856
- ]
857
- ]), 'q2', 'criteria-a');
858
- q = foldQuery(q, [
859
- 'h1'
860
- ], ws(3, [
861
- 'h1',
862
- 'h2'
863
- ], [
864
- [
865
- 'o2',
866
- 'h1'
867
- ],
868
- [
869
- 'b1',
870
- 'h2'
871
- ]
872
- ]), 'q2', completeQueryReceipt, 'criteria-a');
873
- assert.deepEqual(q.hotels.h1?.currentOfferRefs, [
874
- 'o2'
875
- ]);
876
- q = recordOfferCheckIssued(q, ws(3, [
877
- 'h1',
878
- 'h2'
879
- ], [
880
- [
881
- 'o2',
882
- 'h1'
883
- ],
884
- [
885
- 'b1',
886
- 'h2'
887
- ]
888
- ]), 'o2', versionFor('o2'), 'check-2');
889
- q = recordOfferCheckReceipt(q, ws(4, [
890
- 'h1',
891
- 'h2'
892
- ], [
893
- [
894
- 'o2',
895
- 'h1'
896
- ],
897
- [
898
- 'b1',
899
- 'h2'
900
- ]
901
- ]), receipt('check-2', 4, 'o2'), 'check-2', 'o2', versionFor('o2'), 3);
902
- assert.deepEqual(q.hotels.h1?.invalidatedOfferRefs, [
903
- 'o2'
904
- ]);
165
+ const completeQueryReceipt = { ...receipt('q2', 3, 'o2', 'applied'), observation: { kind: 'offers.state', hotelRefs: ['h1'], offerRefs: ['o2'], loadedHotelCount: 1 } };
166
+ q = recordOffersQueryIssued(q, ['h1'], ws(2, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'q2', 'criteria-a');
167
+ q = foldQuery(q, ['h1'], ws(3, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'q2', completeQueryReceipt, 'criteria-a');
168
+ assert.deepEqual(q.hotels.h1?.currentOfferRefs, ['o2']);
169
+ q = recordOfferCheckIssued(q, ws(3, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'o2', versionFor('o2'), 'check-2');
170
+ q = recordOfferCheckReceipt(q, ws(4, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), receipt('check-2', 4, 'o2'), 'check-2', 'o2', versionFor('o2'), 3);
171
+ assert.deepEqual(q.hotels.h1?.invalidatedOfferRefs, ['o2']);
905
172
  assert.equal(q.hotels.h1?.freshOffersRequired, false);
906
173
  assert.equal(q.terminal, undefined);
907
- assert.throws(()=>recordOffersQueryIssued(q, [
908
- 'h1'
909
- ], ws(4, [
910
- 'h1',
911
- 'h2'
912
- ], [
913
- [
914
- 'b1',
915
- 'h2'
916
- ]
917
- ]), 'q-after-two-checks', 'criteria-a'), /hotel_not_active/);
918
- const fw = ws(0, [
919
- 'a',
920
- 'b'
921
- ], [
922
- [
923
- 'a:ratepkg-1',
924
- 'a'
925
- ],
926
- [
927
- 'b:ratepkg-1',
928
- 'b'
929
- ]
930
- ], [
931
- 'b:ratepkg-1'
932
- ]);
174
+ assert.throws(() => recordOffersQueryIssued(q, ['h1'], ws(4, ['h1', 'h2'], [['b1', 'h2']]), 'q-after-two-checks', 'criteria-a'), /hotel_not_active/);
175
+ const fw = ws(0, ['a', 'b'], [['a:ratepkg-1', 'a'], ['b:ratepkg-1', 'b']], ['b:ratepkg-1']);
933
176
  let fallback = recordOfferCheckIssued(createAvailabilityPolicy(fw), fw, 'a:ratepkg-1', versionFor('a:ratepkg-1'), 'fa');
934
- fallback = recordOfferCheckReceipt(fallback, ws(1, [
935
- 'a',
936
- 'b'
937
- ], [
938
- [
939
- 'a:ratepkg-1',
940
- 'a'
941
- ],
942
- [
943
- 'b:ratepkg-1',
944
- 'b'
945
- ]
946
- ], [
947
- 'b:ratepkg-1'
948
- ]), receipt('fa', 1, 'a:ratepkg-1'), 'fa', 'a:ratepkg-1', versionFor('a:ratepkg-1'), 0);
177
+ fallback = recordOfferCheckReceipt(fallback, ws(1, ['a', 'b'], [['a:ratepkg-1', 'a'], ['b:ratepkg-1', 'b']], ['b:ratepkg-1']), receipt('fa', 1, 'a:ratepkg-1'), 'fa', 'a:ratepkg-1', versionFor('a:ratepkg-1'), 0);
949
178
  assert.equal(fallback.hotels.a?.freshOffersRequired, true);
950
- fallback = recordOffersQueryIssued(fallback, [
951
- 'a'
952
- ], ws(1, [
953
- 'a',
954
- 'b'
955
- ], [
956
- [
957
- 'b:ratepkg-1',
958
- 'b'
959
- ]
960
- ]), 'fq1');
961
- const fallbackEmpty = {
962
- ...receipt('fq1', 2, 'a:ratepkg-1', 'no_match'),
963
- observation: {
964
- kind: 'offers.state',
965
- hotelRefs: [
966
- 'a'
967
- ],
968
- offerRefs: [],
969
- loadedHotelCount: 0
970
- }
971
- };
972
- fallback = foldQuery(fallback, [
973
- 'a'
974
- ], ws(2, [
975
- 'a',
976
- 'b'
977
- ], [
978
- [
979
- 'b:ratepkg-1',
980
- 'b'
981
- ]
982
- ]), 'fq1', fallbackEmpty);
179
+ fallback = recordOffersQueryIssued(fallback, ['a'], ws(1, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'fq1');
180
+ const fallbackEmpty = { ...receipt('fq1', 2, 'a:ratepkg-1', 'no_match'), observation: { kind: 'offers.state', hotelRefs: ['a'], offerRefs: [], loadedHotelCount: 0 } };
181
+ fallback = foldQuery(fallback, ['a'], ws(2, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'fq1', fallbackEmpty);
983
182
  assert.equal(fallback.activeHotelOrdinal, 1);
984
- assert.equal(canIssueOfferCheck(fallback, ws(2, [
985
- 'a',
986
- 'b'
987
- ], [
988
- [
989
- 'b:ratepkg-1',
990
- 'b'
991
- ]
992
- ]), 'b:ratepkg-1', versionFor('b:ratepkg-1')).ok, false);
993
- fallback = recordOffersQueryIssued(fallback, [
994
- 'b'
995
- ], ws(2, [
996
- 'a',
997
- 'b'
998
- ], [
999
- [
1000
- 'b:ratepkg-1',
1001
- 'b'
1002
- ]
1003
- ]), 'fq2');
1004
- const fallbackOffers = {
1005
- ...receipt('fq2', 3, 'b:ratepkg-1', 'applied'),
1006
- observation: {
1007
- kind: 'offers.state',
1008
- hotelRefs: [
1009
- 'b'
1010
- ],
1011
- offerRefs: [
1012
- 'b:ratepkg-1'
1013
- ],
1014
- loadedHotelCount: 1
1015
- }
1016
- };
1017
- fallback = foldQuery(fallback, [
1018
- 'b'
1019
- ], ws(3, [
1020
- 'a',
1021
- 'b'
1022
- ], [
1023
- [
1024
- 'b:ratepkg-1',
1025
- 'b'
1026
- ]
1027
- ]), 'fq2', fallbackOffers);
1028
- assert.equal(canIssueOfferCheck(fallback, ws(3, [
1029
- 'a',
1030
- 'b'
1031
- ], [
1032
- [
1033
- 'b:ratepkg-1',
1034
- 'b'
1035
- ]
1036
- ]), 'b:ratepkg-1', versionFor('b:ratepkg-1')).ok, true);
1037
- let exhausted = recordOfferCheckIssued(createAvailabilityPolicy(ws(0, [
1038
- 'x'
1039
- ], [
1040
- [
1041
- 'x1',
1042
- 'x'
1043
- ]
1044
- ])), ws(0, [
1045
- 'x'
1046
- ], [
1047
- [
1048
- 'x1',
1049
- 'x'
1050
- ]
1051
- ]), 'x1', versionFor('x1'), 'ex1');
1052
- exhausted = recordOfferCheckReceipt(exhausted, ws(1, [
1053
- 'x'
1054
- ], [
1055
- [
1056
- 'x1',
1057
- 'x'
1058
- ]
1059
- ]), receipt('ex1', 1, 'x1'), 'ex1', 'x1', versionFor('x1'), 0);
183
+ assert.equal(canIssueOfferCheck(fallback, ws(2, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'b:ratepkg-1', versionFor('b:ratepkg-1')).ok, false);
184
+ fallback = recordOffersQueryIssued(fallback, ['b'], ws(2, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'fq2');
185
+ const fallbackOffers = { ...receipt('fq2', 3, 'b:ratepkg-1', 'applied'), observation: { kind: 'offers.state', hotelRefs: ['b'], offerRefs: ['b:ratepkg-1'], loadedHotelCount: 1 } };
186
+ fallback = foldQuery(fallback, ['b'], ws(3, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'fq2', fallbackOffers);
187
+ assert.equal(canIssueOfferCheck(fallback, ws(3, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'b:ratepkg-1', versionFor('b:ratepkg-1')).ok, true);
188
+ let exhausted = recordOfferCheckIssued(createAvailabilityPolicy(ws(0, ['x'], [['x1', 'x']])), ws(0, ['x'], [['x1', 'x']]), 'x1', versionFor('x1'), 'ex1');
189
+ exhausted = recordOfferCheckReceipt(exhausted, ws(1, ['x'], [['x1', 'x']]), receipt('ex1', 1, 'x1'), 'ex1', 'x1', versionFor('x1'), 0);
1060
190
  assert.equal(exhausted.terminal, undefined);
1061
191
  assert.equal(exhausted.hotels.x?.freshOffersRequired, true);
1062
- exhausted = recordOffersQueryIssued(exhausted, [
1063
- 'x'
1064
- ], ws(1, [
1065
- 'x'
1066
- ], [
1067
- [
1068
- 'x1',
1069
- 'x'
1070
- ]
1071
- ]), 'exq');
1072
- const exhaustedReceipt = {
1073
- ...receipt('exq', 2, 'x1', 'no_match'),
1074
- observation: {
1075
- kind: 'offers.state',
1076
- hotelRefs: [
1077
- 'x'
1078
- ],
1079
- offerRefs: [],
1080
- loadedHotelCount: 0
1081
- }
1082
- };
1083
- exhausted = foldQuery(exhausted, [
1084
- 'x'
1085
- ], ws(2, [
1086
- 'x'
1087
- ], []), 'exq', exhaustedReceipt);
192
+ exhausted = recordOffersQueryIssued(exhausted, ['x'], ws(1, ['x'], [['x1', 'x']]), 'exq');
193
+ const exhaustedReceipt = { ...receipt('exq', 2, 'x1', 'no_match'), observation: { kind: 'offers.state', hotelRefs: ['x'], offerRefs: [], loadedHotelCount: 0 } };
194
+ exhausted = foldQuery(exhausted, ['x'], ws(2, ['x'], []), 'exq', exhaustedReceipt);
1088
195
  assert.equal(exhausted.hotels.x?.generationNo, 2);
1089
196
  assert.equal(exhausted.terminal?.code, 'availability_exhausted_complete');
1090
- assert.throws(()=>recordOfferCheckIssued(exhausted, ws(2, [
1091
- 'x'
1092
- ], []), 'x1', versionFor('x1'), 'after-terminal'), /availability_terminal/);
197
+ assert.throws(() => recordOfferCheckIssued(exhausted, ws(2, ['x'], []), 'x1', versionFor('x1'), 'after-terminal'), /availability_terminal/);
198
+ // A failed A@v1 must not poison a same-hotel candidate query containing B@v1.
199
+ // The reducer filters A's tombstone inside the generation, admits only B, and
200
+ // issues the next CheckAvail without getting stuck on the old offer.
1093
201
  {
1094
- const candidateWorkspace = ws(0, [
1095
- 'candidate-hotel'
1096
- ], [
1097
- [
1098
- 'candidate-a',
1099
- 'candidate-hotel'
1100
- ],
1101
- [
1102
- 'candidate-b',
1103
- 'candidate-hotel'
1104
- ]
1105
- ], [
1106
- 'candidate-a',
1107
- 'candidate-b'
1108
- ]);
202
+ const candidateWorkspace = ws(0, ['candidate-hotel'], [['candidate-a', 'candidate-hotel'], ['candidate-b', 'candidate-hotel']], ['candidate-a', 'candidate-b']);
1109
203
  let candidateState = recordOfferCheckIssued(createAvailabilityPolicy(candidateWorkspace), candidateWorkspace, 'candidate-a', versionFor('candidate-a'), 'candidate-a-check');
1110
- candidateState = recordOfferCheckReceipt(candidateState, ws(1, [
1111
- 'candidate-hotel'
1112
- ], [
1113
- [
1114
- 'candidate-a',
1115
- 'candidate-hotel'
1116
- ],
1117
- [
1118
- 'candidate-b',
1119
- 'candidate-hotel'
1120
- ]
1121
- ], [
1122
- 'candidate-a',
1123
- 'candidate-b'
1124
- ]), receipt('candidate-a-check', 1, 'candidate-a'), 'candidate-a-check', 'candidate-a', versionFor('candidate-a'), 0);
1125
- const queryWorkspace = ws(1, [
1126
- 'candidate-hotel'
1127
- ], [
1128
- [
1129
- 'candidate-a',
1130
- 'candidate-hotel'
1131
- ],
1132
- [
1133
- 'candidate-b',
1134
- 'candidate-hotel'
1135
- ]
1136
- ], [
1137
- 'candidate-a',
1138
- 'candidate-b'
1139
- ]);
1140
- const candidateQueryReceipt = offersReceipt('candidate-query', 2, [
1141
- 'candidate-hotel'
1142
- ], [
1143
- 'candidate-a',
1144
- 'candidate-b'
1145
- ], 1);
1146
- candidateState = recordOffersQueryIssued(candidateState, [
1147
- 'candidate-hotel'
1148
- ], queryWorkspace, 'candidate-query');
1149
- candidateState = recordOffersGeneration(candidateState, [
1150
- 'candidate-hotel'
1151
- ], queryWorkspace, 'candidate-query', digest(candidateQueryReceipt), candidateQueryReceipt);
204
+ candidateState = recordOfferCheckReceipt(candidateState, ws(1, ['candidate-hotel'], [['candidate-a', 'candidate-hotel'], ['candidate-b', 'candidate-hotel']], ['candidate-a', 'candidate-b']), receipt('candidate-a-check', 1, 'candidate-a'), 'candidate-a-check', 'candidate-a', versionFor('candidate-a'), 0);
205
+ const queryWorkspace = ws(1, ['candidate-hotel'], [['candidate-a', 'candidate-hotel'], ['candidate-b', 'candidate-hotel']], ['candidate-a', 'candidate-b']);
206
+ const candidateQueryReceipt = offersReceipt('candidate-query', 2, ['candidate-hotel'], ['candidate-a', 'candidate-b'], 1);
207
+ candidateState = recordOffersQueryIssued(candidateState, ['candidate-hotel'], queryWorkspace, 'candidate-query');
208
+ candidateState = recordOffersGeneration(candidateState, ['candidate-hotel'], queryWorkspace, 'candidate-query', digest(candidateQueryReceipt), candidateQueryReceipt);
1152
209
  assert.equal(candidateState.hotels['candidate-hotel']?.currentOfferRefs.includes('candidate-a'), false, 'unavailable A@v1 is not re-admitted by a same-hotel candidate query');
1153
- assert.deepEqual(candidateState.hotels['candidate-hotel']?.currentOfferRefs, [
1154
- 'candidate-b'
1155
- ], 'same-hotel candidate query admits B@v1 only');
210
+ assert.deepEqual(candidateState.hotels['candidate-hotel']?.currentOfferRefs, ['candidate-b'], 'same-hotel candidate query admits B@v1 only');
1156
211
  assert.equal(candidateState.activeHotelOrdinal, 0, 'same-hotel recovery remains on the active hotel after filtering A');
1157
- const bWorkspace = ws(2, [
1158
- 'candidate-hotel'
1159
- ], [
1160
- [
1161
- 'candidate-b',
1162
- 'candidate-hotel'
1163
- ]
1164
- ], [
1165
- 'candidate-b'
1166
- ]);
212
+ const bWorkspace = ws(2, ['candidate-hotel'], [['candidate-b', 'candidate-hotel']], ['candidate-b']);
1167
213
  assert.equal(canIssueOfferCheck(candidateState, bWorkspace, 'candidate-b', versionFor('candidate-b')).ok, true, 'B@v1 is recheckable after A@v1 unavailable');
1168
214
  candidateState = recordOfferCheckIssued(candidateState, bWorkspace, 'candidate-b', versionFor('candidate-b'), 'candidate-b-check');
1169
215
  const bVerified = verifiedCapability('candidate-b', versionFor('candidate-b'));
1170
- candidateState = recordOfferCheckReceipt(candidateState, ws(3, [
1171
- 'candidate-hotel'
1172
- ], [
1173
- [
1174
- 'candidate-b',
1175
- 'candidate-hotel'
1176
- ]
1177
- ], [
1178
- 'candidate-b'
1179
- ], bVerified), receipt('candidate-b-check', 3, 'candidate-b', 'applied', true), 'candidate-b-check', 'candidate-b', versionFor('candidate-b'), 2);
216
+ candidateState = recordOfferCheckReceipt(candidateState, ws(3, ['candidate-hotel'], [['candidate-b', 'candidate-hotel']], ['candidate-b'], bVerified), receipt('candidate-b-check', 3, 'candidate-b', 'applied', true), 'candidate-b-check', 'candidate-b', versionFor('candidate-b'), 2);
1180
217
  assert.equal(candidateState.terminal?.code, 'availability_confirmed', 'recheck of B@v1 can confirm after A@v1 unavailable');
1181
- }console.log('BOOKING COPILOT AVAILABILITY POLICY: admission, criteria CAS, stale no-generation, invalidation, cursor fallback, exhaustion, absorbing terminal OK');
218
+ }
219
+ console.log('BOOKING COPILOT AVAILABILITY POLICY: admission, criteria CAS, stale no-generation, invalidation, cursor fallback, exhaustion, absorbing terminal OK');
220
+ // Runtime proof: action replay, restart restoration, terminal SSE batch and absorbing seams.
1182
221
  const ledgerRoot = mkdtempSync(join(tmpdir(), 'gotry-availability-proof-'));
1183
222
  const ledger = ensureLedger(ledgerRoot);
1184
- const runtime = new BookingCopilotTaskRuntime(ledger, {
1185
- contextRefFactory: ()=>'ctx-availability'
1186
- });
1187
- const runtimeWorkspace = ws(0, [
1188
- 'rh'
1189
- ], []);
1190
- runtime.startTask({
1191
- schemaVersion: 'booking.surface',
1192
- kind: 'user.turn',
1193
- taskId: 'runtime-availability',
1194
- turnId: 'turn-1',
1195
- workspace: runtimeWorkspace,
1196
- request: {
1197
- text: 'find rates'
1198
- }
1199
- });
1200
- const query = {
1201
- schemaVersion: 'booking.surface',
1202
- kind: 'offers.query',
1203
- actionId: 'rq-1',
1204
- contextRef: 'ctx-availability',
1205
- expectedRevision: 0,
1206
- reason: 'ordinary query',
1207
- factRefs: [],
1208
- input: {
1209
- hotelRefs: [
1210
- 'rh'
1211
- ],
1212
- criteria: {}
1213
- }
1214
- };
223
+ const runtime = new BookingCopilotTaskRuntime(ledger, { contextRefFactory: () => 'ctx-availability' });
224
+ const runtimeWorkspace = ws(0, ['rh'], []);
225
+ runtime.startTask({ schemaVersion: 'booking.surface', kind: 'user.turn', taskId: 'runtime-availability', turnId: 'turn-1', workspace: runtimeWorkspace, request: { text: 'find rates' } });
226
+ const query = { schemaVersion: 'booking.surface', kind: 'offers.query', actionId: 'rq-1', contextRef: 'ctx-availability', expectedRevision: 0, reason: 'ordinary query', factRefs: [], input: { hotelRefs: ['rh'], criteria: {} } };
1215
227
  runtime.issueOperation('runtime-availability', query);
1216
- runtime.continueWithReceipt({
1217
- schemaVersion: 'booking.surface',
1218
- kind: 'action.receipt.continuation',
1219
- taskId: 'runtime-availability',
1220
- workspace: ws(1, [
1221
- 'rh'
1222
- ], [
1223
- [
1224
- 'ro1',
1225
- 'rh'
1226
- ]
1227
- ]),
1228
- receipt: {
1229
- schemaVersion: 'booking.surface',
1230
- kind: 'action.receipt',
1231
- actionId: 'rq-1',
1232
- contextRef: 'ctx-availability',
1233
- status: 'applied',
1234
- revision: 1,
1235
- observation: {
1236
- kind: 'offers.state',
1237
- hotelRefs: [
1238
- 'rh'
1239
- ],
1240
- offerRefs: [
1241
- 'ro1'
1242
- ],
1243
- loadedHotelCount: 1
1244
- },
1245
- resultContract: {
1246
- outcome: 'complete',
1247
- hardCriteriaMet: true,
1248
- factRefs: [],
1249
- gapCodes: [],
1250
- blockers: [],
1251
- relaxationsApplied: []
1252
- }
1253
- }
1254
- });
1255
- const check = {
1256
- schemaVersion: 'booking.surface',
1257
- kind: 'offer.check',
1258
- actionId: 'rc-1',
1259
- contextRef: 'ctx-availability',
1260
- expectedRevision: 1,
1261
- reason: 'CheckAvail',
1262
- factRefs: [],
1263
- input: {
1264
- offerRef: 'ro1',
1265
- offerVersionRef: versionFor('ro1')
1266
- }
1267
- };
228
+ runtime.continueWithReceipt({ schemaVersion: 'booking.surface', kind: 'action.receipt.continuation', taskId: 'runtime-availability', workspace: ws(1, ['rh'], [['ro1', 'rh']]), receipt: { schemaVersion: 'booking.surface', kind: 'action.receipt', actionId: 'rq-1', contextRef: 'ctx-availability', status: 'applied', revision: 1, observation: { kind: 'offers.state', hotelRefs: ['rh'], offerRefs: ['ro1'], loadedHotelCount: 1 }, resultContract: { outcome: 'complete', hardCriteriaMet: true, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } } });
229
+ const check = { schemaVersion: 'booking.surface', kind: 'offer.check', actionId: 'rc-1', contextRef: 'ctx-availability', expectedRevision: 1, reason: 'CheckAvail', factRefs: [], input: { offerRef: 'ro1', offerVersionRef: versionFor('ro1') } };
1268
230
  runtime.issueOperation('runtime-availability', check);
1269
231
  runtime.issueOperation('runtime-availability', check);
1270
232
  assert.equal(runtime.resumeTask('runtime-availability')?.availability.attempts.length, 1);
1271
- const restarted = new BookingCopilotTaskRuntime(ensureLedger(ledgerRoot), {
1272
- contextRefFactory: ()=>'ctx-availability'
1273
- });
233
+ const restarted = new BookingCopilotTaskRuntime(ensureLedger(ledgerRoot), { contextRefFactory: () => 'ctx-availability' });
1274
234
  assert.equal(restarted.resumeTask('runtime-availability')?.availability.attempts[0]?.actionId, 'rc-1');
1275
- runtime.continueWithReceipt({
1276
- schemaVersion: 'booking.surface',
1277
- kind: 'action.receipt.continuation',
1278
- taskId: 'runtime-availability',
1279
- workspace: ws(2, [
1280
- 'rh'
1281
- ], [
1282
- [
1283
- 'ro1',
1284
- 'rh'
1285
- ]
1286
- ]),
1287
- receipt: {
1288
- ...receipt('rc-1', 2, 'ro1'),
1289
- status: 'unavailable'
1290
- }
1291
- });
1292
- const query2 = {
1293
- ...query,
1294
- actionId: 'rq-2',
1295
- expectedRevision: 2
1296
- };
235
+ runtime.continueWithReceipt({ schemaVersion: 'booking.surface', kind: 'action.receipt.continuation', taskId: 'runtime-availability', workspace: ws(2, ['rh'], [['ro1', 'rh']]), receipt: { ...receipt('rc-1', 2, 'ro1'), status: 'unavailable' } });
236
+ const query2 = { ...query, actionId: 'rq-2', expectedRevision: 2 };
1297
237
  runtime.issueOperation('runtime-availability', query2);
1298
- runtime.continueWithReceipt({
1299
- schemaVersion: 'booking.surface',
1300
- kind: 'action.receipt.continuation',
1301
- taskId: 'runtime-availability',
1302
- workspace: ws(3, [
1303
- 'rh'
1304
- ], []),
1305
- receipt: {
1306
- schemaVersion: 'booking.surface',
1307
- kind: 'action.receipt',
1308
- actionId: 'rq-2',
1309
- contextRef: 'ctx-availability',
1310
- status: 'no_match',
1311
- revision: 3,
1312
- observation: {
1313
- kind: 'offers.state',
1314
- hotelRefs: [
1315
- 'rh'
1316
- ],
1317
- offerRefs: [],
1318
- loadedHotelCount: 0
1319
- },
1320
- resultContract: {
1321
- outcome: 'empty',
1322
- hardCriteriaMet: false,
1323
- factRefs: [],
1324
- gapCodes: [],
1325
- blockers: [],
1326
- relaxationsApplied: []
1327
- }
1328
- }
1329
- });
238
+ runtime.continueWithReceipt({ schemaVersion: 'booking.surface', kind: 'action.receipt.continuation', taskId: 'runtime-availability', workspace: ws(3, ['rh'], []), receipt: { schemaVersion: 'booking.surface', kind: 'action.receipt', actionId: 'rq-2', contextRef: 'ctx-availability', status: 'no_match', revision: 3, observation: { kind: 'offers.state', hotelRefs: ['rh'], offerRefs: [], loadedHotelCount: 0 }, resultContract: { outcome: 'empty', hardCriteriaMet: false, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } } });
1330
239
  assert.equal(runtime.resumeTask('runtime-availability')?.phase, 'terminal');
1331
240
  const eventCount = ledger.countEvents();
1332
241
  const terminalBatchPayload = ledger.db.prepare("SELECT payload FROM events WHERE kind = 'booking.copilot.decision.batch' ORDER BY seq DESC LIMIT 1").get();
1333
242
  const terminalBatch = JSON.parse(terminalBatchPayload.payload);
1334
243
  assert.deepEqual(runtime.terminalDecisionBatch('runtime-availability', terminalBatch.requestKey), terminalBatch.events);
1335
244
  assert.deepEqual(runtime.terminalDecisionBatch('runtime-availability', terminalBatch.requestKey), terminalBatch.events, 'exact terminal batch replay is read-only');
1336
- assert.throws(()=>runtime.terminalDecisionBatch('runtime-availability', 'arbitrary-new-terminal-key'), /task_terminal/);
1337
- assert.throws(()=>runtime.issueOperation('runtime-availability', {
1338
- ...query,
1339
- actionId: 'late',
1340
- expectedRevision: 3
1341
- }), /task_terminal/);
1342
- assert.throws(()=>runtime.emitEvent('runtime-availability', {
1343
- kind: 'status',
1344
- status: 'working'
1345
- }), /task_terminal/);
245
+ assert.throws(() => runtime.terminalDecisionBatch('runtime-availability', 'arbitrary-new-terminal-key'), /task_terminal/);
246
+ assert.throws(() => runtime.issueOperation('runtime-availability', { ...query, actionId: 'late', expectedRevision: 3 }), /task_terminal/);
247
+ assert.throws(() => runtime.emitEvent('runtime-availability', { kind: 'status', status: 'working' }), /task_terminal/);
1346
248
  assert.equal(ledger.countEvents(), eventCount);
1347
249
  ledger.close();
1348
250
  restarted['ledger'].close();
1349
- rmSync(ledgerRoot, {
1350
- recursive: true,
1351
- force: true
1352
- });
1353
-
1354
-
1355
- //# sourceURL=ts/scripts/booking-copilot-availability-policy-tests.ts
251
+ rmSync(ledgerRoot, { recursive: true, force: true });