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