@danceiny/gotry 0.0.1-rc.16 → 0.0.1-rc.18

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 (229) hide show
  1. package/README.md +161 -116
  2. package/README.zh-CN.md +173 -144
  3. package/bin/gotry-booking-copilot.js +53 -0
  4. package/bin/gotry-bootstrap.js +113 -66
  5. package/bin/gotry-inner.js +436 -70
  6. package/bin/gotry-runtime-resolution.d.ts +27 -0
  7. package/bin/gotry-runtime-resolution.js +50 -0
  8. package/bin/gotry.js +1 -1
  9. package/cordis.gotry-patch.yml +5 -1
  10. package/dist/capabilities/agent-reach-deep.js +1 -1
  11. package/dist/capabilities/agent-reach.js +1 -1
  12. package/dist/capabilities/anything.js +1 -1
  13. package/dist/capabilities/artifacts.js +1 -1
  14. package/dist/capabilities/effect.js +1 -1
  15. package/dist/capabilities/fact-log.js +1 -1
  16. package/dist/capabilities/flyai.js +20 -6
  17. package/dist/capabilities/hbcli.js +1 -1
  18. package/dist/capabilities/incident-log.js +1 -1
  19. package/dist/capabilities/model-override.js +18 -0
  20. package/dist/capabilities/opensky.js +1 -1
  21. package/dist/capabilities/resilience.js +1 -1
  22. package/dist/capabilities/session/action-cache.js +1 -1
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  24. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  25. package/dist/capabilities/session/benchmark.js +1 -1
  26. package/dist/capabilities/session/extension-bridge.js +19 -6
  27. package/dist/capabilities/session/extension-channel.js +3 -3
  28. package/dist/capabilities/session/extension-distribution.js +234 -0
  29. package/dist/capabilities/session/extract.js +1 -1
  30. package/dist/capabilities/session/golden-score.js +92 -0
  31. package/dist/capabilities/session/health-watch.js +1 -1
  32. package/dist/capabilities/session/read-guard.js +1 -1
  33. package/dist/capabilities/session/static-flight-golden.js +137 -0
  34. package/dist/capabilities/session/transport.js +1 -1
  35. package/dist/capabilities/session/wizard.js +21 -251
  36. package/dist/capabilities/session-consent.js +1 -1
  37. package/dist/capabilities/session-login.js +11 -5
  38. package/dist/capabilities/session-search.js +46 -4
  39. package/dist/capabilities/weather.js +168 -46
  40. package/dist/data/session-golden-20.json +25 -0
  41. package/dist/data/sf-golden-manifest.json +102 -0
  42. package/dist/data/sf-static-routes.json +91 -0
  43. package/dist/scripts/action-cache-tests.js +1 -1
  44. package/dist/scripts/agent-planning-budget-e2e.js +227 -0
  45. package/dist/scripts/agent-planning-budget-tests.js +173 -0
  46. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  47. package/dist/scripts/agent-reach-tests.js +1 -1
  48. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  49. package/dist/scripts/anything-tests.js +1 -1
  50. package/dist/scripts/async-collect.js +1 -1
  51. package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
  52. package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
  53. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
  54. package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
  55. package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
  56. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
  57. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
  58. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
  59. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
  60. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
  61. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
  62. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
  63. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
  64. package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
  65. package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
  66. package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
  67. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
  68. package/dist/scripts/booking-saga-tests.js +1 -1
  69. package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
  70. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
  71. package/dist/scripts/bootstrap-tests.js +45 -39
  72. package/dist/scripts/build-changelog.js +1 -1
  73. package/dist/scripts/changelog-tests.js +1 -1
  74. package/dist/scripts/companion-tests.js +1 -1
  75. package/dist/scripts/diff-test.js +1 -1
  76. package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
  77. package/dist/scripts/dsh-runtime-closure.js +150 -0
  78. package/dist/scripts/effect-tests.js +1 -1
  79. package/dist/scripts/engine-run.js +1 -1
  80. package/dist/scripts/engine-tests.js +1 -1
  81. package/dist/scripts/evaluation-cadence-tests.js +347 -0
  82. package/dist/scripts/evaluation-contract-tests.js +574 -0
  83. package/dist/scripts/extension-distribution-cli.js +35 -0
  84. package/dist/scripts/extension-distribution-tests.js +341 -0
  85. package/dist/scripts/extension-tests.js +128 -23
  86. package/dist/scripts/fact-gate-tests.js +1 -1
  87. package/dist/scripts/flyai-tests.js +1 -1
  88. package/dist/scripts/hbcli-e2e-tests.js +1 -1
  89. package/dist/scripts/hbcli-tests.js +1 -1
  90. package/dist/scripts/health-watch-cli.js +1 -1
  91. package/dist/scripts/i18n-tests.js +1 -1
  92. package/dist/scripts/incident-tests.js +1 -1
  93. package/dist/scripts/journey-tests.js +1 -1
  94. package/dist/scripts/ledger-tests.js +1 -1
  95. package/dist/scripts/ledger-workflow-crash.js +1 -1
  96. package/dist/scripts/memory-capture-tests.js +1 -1
  97. package/dist/scripts/memory-decay-tests.js +1 -1
  98. package/dist/scripts/memory-metrics.js +1 -1
  99. package/dist/scripts/memory-value-report.js +1 -1
  100. package/dist/scripts/model-override-e2e.js +176 -0
  101. package/dist/scripts/nightly-evidence-tests.js +1 -1
  102. package/dist/scripts/nightly-evidence.js +1 -1
  103. package/dist/scripts/nudge-digest.js +1 -1
  104. package/dist/scripts/onboarding-tests.js +21 -53
  105. package/dist/scripts/opensky-check.js +1 -1
  106. package/dist/scripts/opensky-tests.js +1 -1
  107. package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
  108. package/dist/scripts/price-drift-tests.js +1 -1
  109. package/dist/scripts/price-drift-watch.js +1 -1
  110. package/dist/scripts/probe-poi-tests.js +1 -1
  111. package/dist/scripts/product-metrics.js +1 -1
  112. package/dist/scripts/publish-preverify.js +40 -4
  113. package/dist/scripts/realtime-pricing-tests.js +1 -1
  114. package/dist/scripts/replay-async.js +1 -1
  115. package/dist/scripts/replay-real.js +1 -1
  116. package/dist/scripts/replay.js +1 -1
  117. package/dist/scripts/session-attach-diagnose.js +1 -1
  118. package/dist/scripts/session-attach-poc.js +1 -1
  119. package/dist/scripts/session-benchmark.js +1 -1
  120. package/dist/scripts/session-extract-tests.js +1 -1
  121. package/dist/scripts/session-login.js +1 -1
  122. package/dist/scripts/session-tests.js +70 -20
  123. package/dist/scripts/sf-live-benchmark.js +338 -0
  124. package/dist/scripts/sf-live-cli-tests.js +21 -0
  125. package/dist/scripts/sf-soft-score-tests.js +108 -0
  126. package/dist/scripts/sf-summary.js +93 -0
  127. package/dist/scripts/skeleton-check.js +1 -1
  128. package/dist/scripts/skeleton-integration-test.js +1 -1
  129. package/dist/scripts/skills-contract-tests.js +1 -1
  130. package/dist/scripts/smoke-session-gate-tests.js +29 -0
  131. package/dist/scripts/smoke.js +79 -33
  132. package/dist/scripts/state-cli-tests.js +1 -1
  133. package/dist/scripts/state-cli.js +1 -1
  134. package/dist/scripts/static-golden-tests.js +299 -0
  135. package/dist/scripts/time-eval-tests.js +1 -1
  136. package/dist/scripts/travel-timeline-tests.js +1 -1
  137. package/dist/scripts/unified-tests.js +1 -1
  138. package/dist/scripts/weather-tests.js +694 -44
  139. package/dist/scripts/z3-race-tests.js +1 -1
  140. package/dist/src/artifact-gate.js +1 -1
  141. package/dist/src/benchmark-agent-conformance.js +370 -0
  142. package/dist/src/benchmark-environment-bridge.js +384 -0
  143. package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
  144. package/dist/src/benchmark-tool-isolation.js +124 -0
  145. package/dist/src/bookable-facts.js +1 -1
  146. package/dist/src/booking-saga.js +1 -1
  147. package/dist/src/booking-surface/availability-policy-v2.js +830 -0
  148. package/dist/src/booking-surface/canonical-schema.js +113 -0
  149. package/dist/src/booking-surface/contracts-v2.js +89 -0
  150. package/dist/src/booking-surface/contracts.js +46 -0
  151. package/dist/src/booking-surface/dsh-planner.js +453 -0
  152. package/dist/src/booking-surface/dsh-plugin.js +93 -0
  153. package/dist/src/booking-surface/error-codes.js +94 -0
  154. package/dist/src/booking-surface/index.js +15 -0
  155. package/dist/src/booking-surface/profile.js +68 -0
  156. package/dist/src/booking-surface/runtime-v2.js +1771 -0
  157. package/dist/src/booking-surface/runtime.js +351 -0
  158. package/dist/src/booking-surface/server-v2.js +334 -0
  159. package/dist/src/booking-surface/server.js +302 -0
  160. package/dist/src/booking-surface/startup.js +159 -0
  161. package/dist/src/booking-surface/validation-v2.js +319 -0
  162. package/dist/src/booking-surface/validation.js +809 -0
  163. package/dist/src/bridge.js +1 -1
  164. package/dist/src/companions.js +1 -1
  165. package/dist/src/contracts.js +1 -1
  166. package/dist/src/dsh-llm.js +1 -1
  167. package/dist/src/engine.js +1 -1
  168. package/dist/src/evaluation-cadence.js +234 -0
  169. package/dist/src/evaluation-contracts.js +906 -0
  170. package/dist/src/i18n.js +1 -1
  171. package/dist/src/index.js +50 -19
  172. package/dist/src/journey.js +1 -1
  173. package/dist/src/loop.js +1 -1
  174. package/dist/src/memory-capture.js +1 -1
  175. package/dist/src/memory-decay.js +1 -1
  176. package/dist/src/memory-utility.js +1 -1
  177. package/dist/src/mock-llm.js +1 -1
  178. package/dist/src/model.js +1 -1
  179. package/dist/src/realtime-pricing.js +1 -1
  180. package/dist/src/slot-spec.js +1 -1
  181. package/dist/src/state-ledger.js +2 -1
  182. package/dist/src/time-anchor.js +1 -1
  183. package/dist/src/tool-budget.js +136 -0
  184. package/dist/src/tool-packet.js +1 -1
  185. package/dist/src/travel-slots.js +1 -1
  186. package/dist/src/travel-timeline.js +1 -1
  187. package/dist/src/unified.js +1 -1
  188. package/dist/src/wish-pool.js +1 -1
  189. package/dist/src/z3-shared.js +1 -1
  190. package/extension/README.md +31 -7
  191. package/package.json +286 -11
  192. package/schemas/booking.surface.v1.schema.json +927 -0
  193. package/schemas/booking.surface.v2.schema.json +61 -0
  194. package/ts/capabilities/flyai.ts +16 -3
  195. package/ts/capabilities/session/extension-bridge.ts +37 -14
  196. package/ts/capabilities/session/extension-channel.ts +6 -3
  197. package/ts/capabilities/session/extension-distribution.ts +264 -0
  198. package/ts/capabilities/session/golden-score.ts +139 -0
  199. package/ts/capabilities/session/health-watch.ts +1 -1
  200. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  201. package/ts/capabilities/session/wizard.ts +34 -176
  202. package/ts/capabilities/session-login.ts +15 -5
  203. package/ts/capabilities/session-search.ts +40 -3
  204. package/ts/capabilities/weather.ts +141 -52
  205. package/ts/package.json +3 -3
  206. package/ts/src/benchmark-agent-conformance.ts +448 -0
  207. package/ts/src/benchmark-environment-bridge.ts +348 -0
  208. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  209. package/ts/src/benchmark-tool-isolation.ts +166 -0
  210. package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
  211. package/ts/src/booking-surface/canonical-schema.js +113 -0
  212. package/ts/src/booking-surface/contracts-v2.ts +118 -0
  213. package/ts/src/booking-surface/contracts.ts +380 -0
  214. package/ts/src/booking-surface/dsh-planner.ts +452 -0
  215. package/ts/src/booking-surface/dsh-plugin.js +93 -0
  216. package/ts/src/booking-surface/error-codes.ts +101 -0
  217. package/ts/src/booking-surface/index.ts +12 -0
  218. package/ts/src/booking-surface/profile.ts +42 -0
  219. package/ts/src/booking-surface/runtime-v2.ts +1466 -0
  220. package/ts/src/booking-surface/runtime.ts +483 -0
  221. package/ts/src/booking-surface/server-v2.ts +247 -0
  222. package/ts/src/booking-surface/server.ts +324 -0
  223. package/ts/src/booking-surface/startup.ts +196 -0
  224. package/ts/src/booking-surface/validation-v2.ts +205 -0
  225. package/ts/src/booking-surface/validation.ts +453 -0
  226. package/ts/src/index.ts +64 -11
  227. package/ts/src/state-ledger.ts +1 -0
  228. package/ts/src/tool-budget.ts +165 -0
  229. package/dist/scripts/wizard-bootstrap.js +0 -32
@@ -0,0 +1,1355 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { ensureLedger } from '../src/state-ledger.js';
6
+ import { BookingCopilotTaskRuntimeV2 } from '../src/booking-surface/runtime-v2.js';
7
+ import { createAvailabilityPolicyV2, digestV2, MAX_HOTELS_PER_TASK_V2, MAX_OFFER_CHECKS_PER_HOTEL_V2, MAX_OFFER_QUERIES_PER_HOTEL_V2, recordObservedOffersQueryV2, recordOfferCheckIssuedV2, recordOfferCheckReceiptV2, recordOffersGenerationV2, recordOffersQueryIssuedV2, canIssueOfferCheckV2 } from '../src/booking-surface/availability-policy-v2.js';
8
+ import { bookingSurfaceAllowedActionsV2 } from '../src/booking-surface/contracts-v2.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.v2',
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.v2',
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 = '')=>recordOffersGenerationV2(state, hotelRefs, workspace, actionId, digestV2(queryReceipt), queryReceipt, criteriaDigest);
78
+ const criteria = {};
79
+ const offersReceipt = (actionId, revision, hotelRefs, offerRefs, loadedHotelCount, status = 'applied', outcome = 'complete', hardCriteriaMet = true)=>({
80
+ schemaVersion: 'booking.surface.v2',
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.v2',
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(bookingSurfaceAllowedActionsV2('storefront'), [
126
+ 'search.patch',
127
+ 'search.run',
128
+ 'results.view.patch',
129
+ 'hotel.focus'
130
+ ]);
131
+ assert.deepEqual(bookingSurfaceAllowedActionsV2('payment_link'), [
132
+ 'search.patch',
133
+ 'search.run',
134
+ 'results.view.patch',
135
+ 'hotel.focus',
136
+ 'hotel.select'
137
+ ]);
138
+ {
139
+ assert.equal(MAX_HOTELS_PER_TASK_V2, 5);
140
+ assert.equal(MAX_OFFER_CHECKS_PER_HOTEL_V2, 2);
141
+ assert.equal(MAX_OFFER_QUERIES_PER_HOTEL_V2, 2);
142
+ const unavailableWorkspace = ws(0, [
143
+ 'budget-hotel'
144
+ ], [
145
+ [
146
+ 'budget-hotel:ratepkg-1',
147
+ 'budget-hotel'
148
+ ]
149
+ ]);
150
+ let exhausted = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'budget-check-1');
151
+ exhausted = recordOfferCheckReceiptV2(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);
159
+ assert.equal(exhausted.hotels['budget-hotel']?.freshOffersRequired, true, 'first unavailable opens exactly one fresh-generation transition');
160
+ exhausted = recordOffersQueryIssuedV2(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 = recordOffersGenerationV2(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', digestV2(reusedReceipt), reusedReceipt);
185
+ assert.deepEqual(reused.hotels['budget-hotel']?.currentOfferRefs, [], 'fresh HotelRates cannot re-admit the checked stale OfferRef');
186
+ 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 = recordOffersGenerationV2(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', digestV2(refreshedReceipt), refreshedReceipt);
202
+ exhausted = recordOfferCheckIssuedV2(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 = recordOfferCheckReceiptV2(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);
218
+ assert.equal(exhausted.hotels['budget-hotel']?.checksIssued, 2, 'second unavailable consumes the lifetime CheckAvail budget');
219
+ assert.equal(exhausted.terminal?.code, 'availability_exhausted_complete', 'second unavailable reaches conclusive exhaustion');
220
+ assert.throws(()=>recordOfferCheckIssuedV2(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
+ });
251
+ let inconclusive = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'gap-check-1');
252
+ inconclusive = recordOfferCheckReceiptV2(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);
260
+ assert.equal(inconclusive.hotels['budget-hotel']?.status, 'inconclusive', 'generic gap is not accepted as a typed negative');
261
+ assert.equal(inconclusive.hotels['budget-hotel']?.tombstonedOfferRefs.includes('budget-hotel:ratepkg-1'), false, 'generic gap cannot tombstone logical offer');
262
+ assert.equal(inconclusive.hotels['budget-hotel']?.tombstonedOfferVersionRefs.includes(versionFor('budget-hotel:ratepkg-1')), false, 'generic gap cannot tombstone offer version');
263
+ inconclusive = recordOffersQueryIssuedV2(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 = recordOffersGenerationV2(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', digestV2(gapRefreshedReceipt), gapRefreshedReceipt);
288
+ inconclusive = recordOfferCheckIssuedV2(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 = recordOfferCheckReceiptV2(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);
314
+ 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
+ ]){
319
+ const gapAction = `generic-gap-${status}`;
320
+ const gapState = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(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 = recordOfferCheckReceiptV2(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);
352
+ assert.equal(afterGap.hotels['budget-hotel']?.status, 'inconclusive', `${status} with generic gap remains inconclusive`);
353
+ assert.equal(afterGap.hotels['budget-hotel']?.tombstonedOfferRefs.includes('budget-hotel:ratepkg-1'), false, `${status} with generic gap cannot tombstone logical offer`);
354
+ assert.equal(afterGap.hotels['budget-hotel']?.tombstonedOfferVersionRefs.includes(versionFor('budget-hotel:ratepkg-1')), false, `${status} with generic gap cannot tombstone version`);
355
+ }
356
+ let queryBudget = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'query-budget-check');
357
+ queryBudget = recordOfferCheckReceiptV2(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
+ ]){
375
+ const offerRef = `budget-hotel:query-ratepkg-${revision}`;
376
+ queryBudget = recordOffersQueryIssuedV2(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 = recordOffersGenerationV2(queryBudget, [
392
+ 'budget-hotel'
393
+ ], ws(revision, [
394
+ 'budget-hotel'
395
+ ], [
396
+ [
397
+ offerRef,
398
+ 'budget-hotel'
399
+ ]
400
+ ]), actionId, digestV2(queryReceipt), queryReceipt);
401
+ }
402
+ assert.equal(queryBudget.hotels['budget-hotel']?.offerQueriesIssued, 2, 'two refresh queries are retained as lifetime usage');
403
+ assert.throws(()=>recordOffersQueryIssuedV2(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
+ ]);
432
+ const candidates = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(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');
440
+ 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(()=>createAvailabilityPolicyV2(duplicateWorkspace), /availability_duplicate_offer_ref/, 'loaded OfferRef must be globally unique before any .find-based authority lookup');
455
+ assert.throws(()=>canIssueOfferCheckV2(createAvailabilityPolicyV2(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
+ ]);
481
+ const selected = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(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
+ ]);
487
+ 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
+ ]);
496
+ const completeStarted = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(complete), complete, 'complete-1', versionFor('complete-1'), 'complete-check');
497
+ const completeQuery = recordOffersQueryIssuedV2(completeStarted, [
498
+ 'complete-hotel'
499
+ ], complete, 'complete-query', digestV2(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(()=>recordOffersGenerationV2(completeQuery, [
521
+ 'complete-hotel'
522
+ ], fourOfferWorkspace, 'complete-query', digestV2(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), digestV2(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
+ ]);
554
+ const partialStarted = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(partialWorkspace), partialWorkspace, 'partial-1', versionFor('partial-1'), 'partial-check');
555
+ const partialQuery = recordOffersQueryIssuedV2(partialStarted, [
556
+ 'partial-hotel'
557
+ ], partialWorkspace, 'partial-query', digestV2(criteria), criteria);
558
+ const partialReceipt = offersReceipt('partial-query', 1, [
559
+ 'partial-hotel'
560
+ ], [
561
+ 'partial-2'
562
+ ], 1, 'partial', 'partial', false);
563
+ const partial = recordOffersGenerationV2(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', digestV2(partialReceipt), partialReceipt, digestV2(criteria), criteria);
581
+ assert.equal(partial.hotels['partial-hotel']?.currentGeneration?.evidence, 'partial');
582
+ assert.equal(partial.hotels['partial-hotel']?.currentOfferRefs.length, 0);
583
+ assert.equal(partial.hotels['partial-hotel']?.freshOffersRequired, true);
584
+ assert.throws(()=>recordOffersGenerationV2(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', digestV2(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), digestV2(criteria), criteria), /availability_offers_count_mismatch/);
606
+ const emptyWorkspace = ws(0, [
607
+ 'empty-hotel'
608
+ ], [
609
+ [
610
+ 'empty-1',
611
+ 'empty-hotel'
612
+ ]
613
+ ]);
614
+ const emptyStarted = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(emptyWorkspace), emptyWorkspace, 'empty-1', versionFor('empty-1'), 'empty-check');
615
+ const emptyQuery = recordOffersQueryIssuedV2(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(()=>recordOffersGenerationV2(emptyQuery, [
622
+ 'empty-hotel'
623
+ ], emptyWorkspace, 'empty-query', digestV2(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 = recordObservedOffersQueryV2(createAvailabilityPolicyV2(seedWorkspace), [
639
+ 'seed-hotel'
640
+ ], digestV2(criteria), seedWorkspace, seed, 'seed-query', digestV2(seed), criteria);
641
+ assert.equal(seeded.criteriaDigest, digestV2(criteria));
642
+ const cleared = recordObservedOffersQueryV2(seeded, [
643
+ 'seed-hotel'
644
+ ], digestV2(criteria), ws(2, [
645
+ 'seed-hotel'
646
+ ], []), gapReceipt('failed-query', 2), 'failed-query', digestV2(gapReceipt('failed-query', 2)), criteria);
647
+ assert.equal(cleared.criteria, undefined);
648
+ assert.equal(cleared.criteriaDigest, undefined);
649
+ assert.equal(cleared.lastQuerySourceActionId, undefined);
650
+ const verificationWorkspace = ws(0, [
651
+ 'verify-hotel'
652
+ ], [
653
+ [
654
+ 'verify-1',
655
+ 'verify-hotel'
656
+ ]
657
+ ]);
658
+ const verificationStarted = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(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
+ };
664
+ const wrong = recordOfferCheckReceiptV2(verificationStarted, wrongVerified, receipt('verify-check', 1, 'verify-1', 'applied', true), 'verify-check', 'verify-1', versionFor('verify-1'), 0);
665
+ assert.notEqual(wrong.terminal?.code, 'availability_confirmed');
666
+ assert.equal(wrong.hotels['verify-hotel']?.status, 'inconclusive');
667
+ const matchingStarted = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(verificationWorkspace), verificationWorkspace, 'verify-1', versionFor('verify-1'), 'verify-match');
668
+ const matchingWorkspace = {
669
+ ...verificationWorkspace,
670
+ revision: 1,
671
+ verifiedOffer: verifiedCapability('verify-1')
672
+ };
673
+ const matching = recordOfferCheckReceiptV2(matchingStarted, matchingWorkspace, receipt('verify-match', 1, 'verify-1', 'applied', true), 'verify-match', 'verify-1', versionFor('verify-1'), 0);
674
+ 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 = recordObservedOffersQueryV2(createAvailabilityPolicyV2(verificationWorkspace), [
681
+ 'verify-hotel'
682
+ ], digestV2(criteria), verificationWorkspace, verificationSeedReceipt, 'verify-seed', digestV2(verificationSeedReceipt), criteria);
683
+ const changedStarted = recordOfferCheckIssuedV2(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 = recordOfferCheckReceiptV2(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);
719
+ assert.equal(changed.hotels['verify-hotel']?.status, 'active');
720
+ assert.deepEqual(changed.hotels['verify-hotel']?.currentOfferRefs, [
721
+ 'verify-1'
722
+ ]);
723
+ assert.equal(changed.hotels['verify-hotel']?.tombstonedOfferVersionRefs.includes(versionFor('verify-1')), true);
724
+ 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(()=>recordOffersQueryIssuedV2(createAvailabilityPolicyV2(initial), [
745
+ 'h1'
746
+ ], initial, 'pre-q'), /recovery_not_started/);
747
+ let state = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(initial), initial, 'o1', versionFor('o1'), 'check-1');
748
+ assert.deepEqual(state.hotelRefs, [
749
+ 'h1',
750
+ 'h2'
751
+ ]);
752
+ assert.equal(state.hotels.h1?.offerQueriesIssued, 0);
753
+ state = recordOfferCheckReceiptV2(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);
770
+ assert.equal(state.hotels.h1?.freshOffersRequired, true);
771
+ assert.equal(canIssueOfferCheckV2(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 = recordOffersQueryIssuedV2(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(()=>recordOffersQueryIssuedV2(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');
828
+ assert.equal(q.hotels.h1?.generationNo, 1);
829
+ 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 = recordOffersQueryIssuedV2(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 = recordOfferCheckIssuedV2(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 = recordOfferCheckReceiptV2(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
+ ]);
905
+ assert.equal(q.hotels.h1?.freshOffersRequired, false);
906
+ assert.equal(q.terminal, undefined);
907
+ assert.throws(()=>recordOffersQueryIssuedV2(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
+ ]);
933
+ let fallback = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(fw), fw, 'a:ratepkg-1', versionFor('a:ratepkg-1'), 'fa');
934
+ fallback = recordOfferCheckReceiptV2(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);
949
+ assert.equal(fallback.hotels.a?.freshOffersRequired, true);
950
+ fallback = recordOffersQueryIssuedV2(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);
983
+ assert.equal(fallback.activeHotelOrdinal, 1);
984
+ assert.equal(canIssueOfferCheckV2(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 = recordOffersQueryIssuedV2(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(canIssueOfferCheckV2(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 = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(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 = recordOfferCheckReceiptV2(exhausted, ws(1, [
1053
+ 'x'
1054
+ ], [
1055
+ [
1056
+ 'x1',
1057
+ 'x'
1058
+ ]
1059
+ ]), receipt('ex1', 1, 'x1'), 'ex1', 'x1', versionFor('x1'), 0);
1060
+ assert.equal(exhausted.terminal, undefined);
1061
+ assert.equal(exhausted.hotels.x?.freshOffersRequired, true);
1062
+ exhausted = recordOffersQueryIssuedV2(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);
1088
+ assert.equal(exhausted.hotels.x?.generationNo, 2);
1089
+ assert.equal(exhausted.terminal?.code, 'availability_exhausted_complete');
1090
+ assert.throws(()=>recordOfferCheckIssuedV2(exhausted, ws(2, [
1091
+ 'x'
1092
+ ], []), 'x1', versionFor('x1'), 'after-terminal'), /availability_terminal/);
1093
+ {
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
+ ]);
1109
+ let candidateState = recordOfferCheckIssuedV2(createAvailabilityPolicyV2(candidateWorkspace), candidateWorkspace, 'candidate-a', versionFor('candidate-a'), 'candidate-a-check');
1110
+ candidateState = recordOfferCheckReceiptV2(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 = recordOffersQueryIssuedV2(candidateState, [
1147
+ 'candidate-hotel'
1148
+ ], queryWorkspace, 'candidate-query');
1149
+ candidateState = recordOffersGenerationV2(candidateState, [
1150
+ 'candidate-hotel'
1151
+ ], queryWorkspace, 'candidate-query', digestV2(candidateQueryReceipt), candidateQueryReceipt);
1152
+ 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');
1156
+ 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
+ ]);
1167
+ assert.equal(canIssueOfferCheckV2(candidateState, bWorkspace, 'candidate-b', versionFor('candidate-b')).ok, true, 'B@v1 is recheckable after A@v1 unavailable');
1168
+ candidateState = recordOfferCheckIssuedV2(candidateState, bWorkspace, 'candidate-b', versionFor('candidate-b'), 'candidate-b-check');
1169
+ const bVerified = verifiedCapability('candidate-b', versionFor('candidate-b'));
1170
+ candidateState = recordOfferCheckReceiptV2(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);
1180
+ assert.equal(candidateState.terminal?.code, 'availability_confirmed', 'recheck of B@v1 can confirm after A@v1 unavailable');
1181
+ }console.log('BOOKING COPILOT AVAILABILITY POLICY V2: admission, criteria CAS, stale no-generation, invalidation, cursor fallback, exhaustion, absorbing terminal OK');
1182
+ const ledgerRoot = mkdtempSync(join(tmpdir(), 'gotry-availability-proof-'));
1183
+ const ledger = ensureLedger(ledgerRoot);
1184
+ const runtime = new BookingCopilotTaskRuntimeV2(ledger, {
1185
+ contextRefFactory: ()=>'ctx-availability'
1186
+ });
1187
+ const runtimeWorkspace = ws(0, [
1188
+ 'rh'
1189
+ ], []);
1190
+ runtime.startTask({
1191
+ schemaVersion: 'booking.surface.v2',
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.v2',
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
+ };
1215
+ runtime.issueOperation('runtime-availability', query);
1216
+ runtime.continueWithReceipt({
1217
+ schemaVersion: 'booking.surface.v2',
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.v2',
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.v2',
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
+ };
1268
+ runtime.issueOperation('runtime-availability', check);
1269
+ runtime.issueOperation('runtime-availability', check);
1270
+ assert.equal(runtime.resumeTask('runtime-availability')?.availability.attempts.length, 1);
1271
+ const restarted = new BookingCopilotTaskRuntimeV2(ensureLedger(ledgerRoot), {
1272
+ contextRefFactory: ()=>'ctx-availability'
1273
+ });
1274
+ assert.equal(restarted.resumeTask('runtime-availability')?.availability.attempts[0]?.actionId, 'rc-1');
1275
+ runtime.continueWithReceipt({
1276
+ schemaVersion: 'booking.surface.v2',
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
+ };
1297
+ runtime.issueOperation('runtime-availability', query2);
1298
+ runtime.continueWithReceipt({
1299
+ schemaVersion: 'booking.surface.v2',
1300
+ kind: 'action.receipt.continuation',
1301
+ taskId: 'runtime-availability',
1302
+ workspace: ws(3, [
1303
+ 'rh'
1304
+ ], []),
1305
+ receipt: {
1306
+ schemaVersion: 'booking.surface.v2',
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
+ });
1330
+ assert.equal(runtime.resumeTask('runtime-availability')?.phase, 'terminal');
1331
+ const eventCount = ledger.countEvents();
1332
+ const terminalBatchPayload = ledger.db.prepare("SELECT payload FROM events WHERE kind = 'booking.copilot.v2.decision.batch' ORDER BY seq DESC LIMIT 1").get();
1333
+ const terminalBatch = JSON.parse(terminalBatchPayload.payload);
1334
+ assert.deepEqual(runtime.terminalDecisionBatch('runtime-availability', terminalBatch.requestKey), terminalBatch.events);
1335
+ 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/);
1346
+ assert.equal(ledger.countEvents(), eventCount);
1347
+ ledger.close();
1348
+ restarted['ledger'].close();
1349
+ rmSync(ledgerRoot, {
1350
+ recursive: true,
1351
+ force: true
1352
+ });
1353
+
1354
+
1355
+ //# sourceURL=ts/scripts/booking-copilot-availability-policy-v2-tests.ts