@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
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* the sole executor after the parent adapter validates the canonical action.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import Ajv2020 from 'ajv/dist/2020.js'
|
|
11
|
+
import { canonicalBookingSurfaceSchema, dshBookingActionSchemaForKind } from './canonical-schema.js'
|
|
11
12
|
|
|
12
13
|
export const name = 'gotry-embedded-booking'
|
|
13
14
|
export const inject = ['tools']
|
|
@@ -50,6 +51,12 @@ function toolDefinition(toolName, capabilityId, actionKinds) {
|
|
|
50
51
|
kind: { type: 'string', const: 'operation' },
|
|
51
52
|
action: { oneOf: actionKinds.map(actionSchema) },
|
|
52
53
|
})
|
|
54
|
+
const parameters = closedObject({
|
|
55
|
+
decision: {
|
|
56
|
+
oneOf: [operationDecision, questionDecision, explanationDecision, terminalDecision, errorDecision],
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
const validateArgs = new Ajv2020({ allErrors: true, strict: false }).compile(parameters)
|
|
53
60
|
return Object.freeze({
|
|
54
61
|
name: toolName,
|
|
55
62
|
description: `Emit exactly one typed ${capabilityId} decision for the existing Booking workspace. This capability never books, pays, edits holder/guest data, or calls a supplier.`,
|
|
@@ -69,6 +76,15 @@ function toolDefinition(toolName, capabilityId, actionKinds) {
|
|
|
69
76
|
},
|
|
70
77
|
},
|
|
71
78
|
async execute(args) {
|
|
79
|
+
// Native self-correction boundary: when the model's tool call violates
|
|
80
|
+
// the canonical decision schema, throw with the exact Ajv errors. The
|
|
81
|
+
// dsh agent loop feeds tool failures back to the model as tool results,
|
|
82
|
+
// so the model repairs its own shape inside the same turn — no schema
|
|
83
|
+
// knowledge duplicated outside this plugin.
|
|
84
|
+
if (!validateArgs(args)) {
|
|
85
|
+
const errors = (validateArgs.errors ?? []).map((e) => `${e.instancePath || '/'}: ${e.message}`).join('; ')
|
|
86
|
+
throw new Error(`decision_schema_violation: ${errors}`)
|
|
87
|
+
}
|
|
72
88
|
const decision = args.decision
|
|
73
89
|
return {
|
|
74
90
|
accepted: true,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** Closed, non-sensitive error vocabulary for planner/runtime boundaries. */
|
|
1
2
|
export const BOOKING_TYPED_ERROR_CODES = [
|
|
2
3
|
'PAYLOAD_TOO_LARGE',
|
|
3
4
|
'TASK_NOT_FOUND',
|
|
@@ -21,8 +22,77 @@ export const BOOKING_TYPED_ERROR_CODES = [
|
|
|
21
22
|
'INVALID_INGRESS_BINDING',
|
|
22
23
|
'BOOKING_SURFACE_SCHEMA_MISMATCH',
|
|
23
24
|
'UNAUTHORIZED',
|
|
24
|
-
'NOT_FOUND'
|
|
25
|
+
'NOT_FOUND',
|
|
25
26
|
];
|
|
27
|
+
/** Exact, closed reasons safe to expose in dispatch diagnostics. */
|
|
28
|
+
export const BOOKING_DISPATCH_REJECTION_REASONS = [
|
|
29
|
+
'approval_expired',
|
|
30
|
+
'approval_mismatch',
|
|
31
|
+
'approval_not_awaiting',
|
|
32
|
+
'approval_not_presented',
|
|
33
|
+
'approval_replayed',
|
|
34
|
+
'approval_required',
|
|
35
|
+
'availability_attempt_mismatch',
|
|
36
|
+
'availability_attempt_unknown',
|
|
37
|
+
'availability_criteria_changed',
|
|
38
|
+
'availability_criteria_invalid',
|
|
39
|
+
'availability_criteria_mismatch',
|
|
40
|
+
'availability_criteria_required',
|
|
41
|
+
'availability_duplicate_offer_ref',
|
|
42
|
+
'availability_duplicate_offer_version_ref',
|
|
43
|
+
'availability_expected_revision_missing',
|
|
44
|
+
'availability_generation_mismatch',
|
|
45
|
+
'availability_hotel_limit_exceeded',
|
|
46
|
+
'availability_hotel_not_active',
|
|
47
|
+
'availability_hotel_unknown',
|
|
48
|
+
'availability_observation_required',
|
|
49
|
+
'availability_offer_limit_exceeded',
|
|
50
|
+
'availability_offers_count_mismatch',
|
|
51
|
+
'availability_offers_hotel_mismatch',
|
|
52
|
+
'availability_offers_observation_required',
|
|
53
|
+
'availability_offers_receipt_incoherent',
|
|
54
|
+
'availability_offers_unreported',
|
|
55
|
+
'availability_offers_workspace_mismatch',
|
|
56
|
+
'availability_query_not_reserved',
|
|
57
|
+
'availability_query_provenance_mismatch',
|
|
58
|
+
'availability_query_receipt_mismatch',
|
|
59
|
+
'availability_receipt_digest_mismatch',
|
|
60
|
+
'availability_receipt_incoherent',
|
|
61
|
+
'availability_stale_receipt',
|
|
62
|
+
'availability_terminal',
|
|
63
|
+
'booking_malformed_workspace',
|
|
64
|
+
'context_mismatch',
|
|
65
|
+
'invalid_task_id',
|
|
66
|
+
'receipt_action_mismatch',
|
|
67
|
+
'receipt_conflict',
|
|
68
|
+
'receipt_observation_action_mismatch',
|
|
69
|
+
'receipt_relaxation_unauthorized',
|
|
70
|
+
'receipt_required',
|
|
71
|
+
'receipt_source_action_mismatch',
|
|
72
|
+
'receipt_source_digest_mismatch',
|
|
73
|
+
'receipt_target_mismatch',
|
|
74
|
+
'receipt_verified_offer_expired',
|
|
75
|
+
'reanchor_turn_required',
|
|
76
|
+
'request_conflict',
|
|
77
|
+
'request_key_invalidated',
|
|
78
|
+
'revision_mismatch',
|
|
79
|
+
'revision_regression',
|
|
80
|
+
'task_not_found',
|
|
81
|
+
'task_terminal',
|
|
82
|
+
'turn_conflict',
|
|
83
|
+
'unexpected_receipt',
|
|
84
|
+
'unsafe_opaque_ref',
|
|
85
|
+
'workspace_mismatch',
|
|
86
|
+
];
|
|
87
|
+
export const BOOKING_DISPATCH_UNKNOWN_REASON = 'UNCLASSIFIED';
|
|
88
|
+
const DISPATCH_REJECTION_REASON_SET = new Set(BOOKING_DISPATCH_REJECTION_REASONS);
|
|
89
|
+
/** Returns a reason only when the complete boundary error is one known token. */
|
|
90
|
+
export function extractBookingDispatchReason(error) {
|
|
91
|
+
const message = typeof error === 'string' ? error : error instanceof Error ? error.message : undefined;
|
|
92
|
+
return message !== undefined && DISPATCH_REJECTION_REASON_SET.has(message)
|
|
93
|
+
? message
|
|
94
|
+
: BOOKING_DISPATCH_UNKNOWN_REASON;
|
|
95
|
+
}
|
|
26
96
|
const ALIASES = {
|
|
27
97
|
payload_too_large: 'PAYLOAD_TOO_LARGE',
|
|
28
98
|
task_not_found: 'TASK_NOT_FOUND',
|
|
@@ -46,7 +116,7 @@ const ALIASES = {
|
|
|
46
116
|
invalid_ingress_binding: 'INVALID_INGRESS_BINDING',
|
|
47
117
|
booking_surface_schema_mismatch: 'BOOKING_SURFACE_SCHEMA_MISMATCH',
|
|
48
118
|
unauthorized: 'UNAUTHORIZED',
|
|
49
|
-
not_found: 'NOT_FOUND'
|
|
119
|
+
not_found: 'NOT_FOUND',
|
|
50
120
|
};
|
|
51
121
|
const SAFE_MESSAGES = {
|
|
52
122
|
PAYLOAD_TOO_LARGE: 'Request payload exceeds the configured limit.',
|
|
@@ -71,24 +141,26 @@ const SAFE_MESSAGES = {
|
|
|
71
141
|
INVALID_INGRESS_BINDING: 'The trusted ingress binding is invalid.',
|
|
72
142
|
BOOKING_SURFACE_SCHEMA_MISMATCH: 'The booking surface schema is not supported.',
|
|
73
143
|
UNAUTHORIZED: 'The request is not authorized.',
|
|
74
|
-
NOT_FOUND: 'The requested resource was not found.'
|
|
144
|
+
NOT_FOUND: 'The requested resource was not found.',
|
|
75
145
|
};
|
|
76
146
|
function rawCode(error) {
|
|
77
|
-
if (typeof error === 'string')
|
|
78
|
-
|
|
147
|
+
if (typeof error === 'string')
|
|
148
|
+
return error.split(':', 1)[0];
|
|
149
|
+
if (error instanceof Error && error.message)
|
|
150
|
+
return error.message.split(':', 1)[0];
|
|
79
151
|
return 'planner_failed';
|
|
80
152
|
}
|
|
81
153
|
export function normalizeBookingErrorCode(error) {
|
|
82
154
|
const raw = rawCode(error);
|
|
83
155
|
const normalized = raw.trim().toLowerCase();
|
|
84
156
|
const aliased = ALIASES[normalized];
|
|
85
|
-
if (aliased)
|
|
157
|
+
if (aliased)
|
|
158
|
+
return aliased;
|
|
86
159
|
const uppercase = raw.trim().replace(/[^A-Za-z0-9]+/g, '_').toUpperCase();
|
|
87
|
-
return BOOKING_TYPED_ERROR_CODES.includes(uppercase)
|
|
160
|
+
return BOOKING_TYPED_ERROR_CODES.includes(uppercase)
|
|
161
|
+
? uppercase
|
|
162
|
+
: 'PLANNER_FAILED';
|
|
88
163
|
}
|
|
89
164
|
export function safeBookingErrorMessage(code) {
|
|
90
165
|
return SAFE_MESSAGES[BOOKING_TYPED_ERROR_CODES.includes(code) ? code : 'PLANNER_FAILED'];
|
|
91
166
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//# sourceURL=ts/src/booking-surface/error-codes.ts
|
|
@@ -4,65 +4,21 @@ export const EMBEDDED_BOOKING_CAPABILITY_IDS = [
|
|
|
4
4
|
'find-room-offers',
|
|
5
5
|
'compare-offers',
|
|
6
6
|
'prepare-booking',
|
|
7
|
-
'observe-booking'
|
|
7
|
+
'observe-booking',
|
|
8
8
|
];
|
|
9
|
+
/** Closed registry. No booking/payment/write capability exists in this profile. */
|
|
9
10
|
export const embeddedBookingProfile = Object.freeze({
|
|
10
11
|
id: 'embedded-booking',
|
|
11
12
|
schemaVersion: 'booking.surface',
|
|
12
13
|
capabilities: Object.freeze([
|
|
13
|
-
Object.freeze({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}),
|
|
21
|
-
Object.freeze({
|
|
22
|
-
id: 'refine-results',
|
|
23
|
-
effect: 'read',
|
|
24
|
-
actions: Object.freeze([
|
|
25
|
-
'results.view.patch',
|
|
26
|
-
'hotel.focus',
|
|
27
|
-
'hotel.select'
|
|
28
|
-
])
|
|
29
|
-
}),
|
|
30
|
-
Object.freeze({
|
|
31
|
-
id: 'find-room-offers',
|
|
32
|
-
effect: 'read',
|
|
33
|
-
actions: Object.freeze([
|
|
34
|
-
'offers.query',
|
|
35
|
-
'offers.view.patch'
|
|
36
|
-
])
|
|
37
|
-
}),
|
|
38
|
-
Object.freeze({
|
|
39
|
-
id: 'compare-offers',
|
|
40
|
-
effect: 'read',
|
|
41
|
-
actions: Object.freeze([
|
|
42
|
-
'offers.compare',
|
|
43
|
-
'offer.select'
|
|
44
|
-
])
|
|
45
|
-
}),
|
|
46
|
-
Object.freeze({
|
|
47
|
-
id: 'prepare-booking',
|
|
48
|
-
effect: 'read',
|
|
49
|
-
actions: Object.freeze([
|
|
50
|
-
'offer.check',
|
|
51
|
-
'checkout.prepare'
|
|
52
|
-
])
|
|
53
|
-
}),
|
|
54
|
-
Object.freeze({
|
|
55
|
-
id: 'observe-booking',
|
|
56
|
-
effect: 'read',
|
|
57
|
-
actions: Object.freeze([
|
|
58
|
-
'order.observe'
|
|
59
|
-
])
|
|
60
|
-
})
|
|
61
|
-
])
|
|
14
|
+
Object.freeze({ id: 'search-hotels', effect: 'read', actions: Object.freeze(['search.patch', 'search.run']) }),
|
|
15
|
+
Object.freeze({ id: 'refine-results', effect: 'read', actions: Object.freeze(['results.view.patch', 'hotel.focus', 'hotel.select']) }),
|
|
16
|
+
Object.freeze({ id: 'find-room-offers', effect: 'read', actions: Object.freeze(['offers.query', 'offers.view.patch']) }),
|
|
17
|
+
Object.freeze({ id: 'compare-offers', effect: 'read', actions: Object.freeze(['offers.compare', 'offer.select']) }),
|
|
18
|
+
Object.freeze({ id: 'prepare-booking', effect: 'read', actions: Object.freeze(['offer.check', 'checkout.prepare']) }),
|
|
19
|
+
Object.freeze({ id: 'observe-booking', effect: 'read', actions: Object.freeze(['order.observe']) }),
|
|
20
|
+
]),
|
|
62
21
|
});
|
|
63
22
|
export function actionsForEmbeddedCapability(id) {
|
|
64
|
-
return embeddedBookingProfile.capabilities.find((capability)=>capability.id === id)?.actions ?? [];
|
|
23
|
+
return embeddedBookingProfile.capabilities.find((capability) => capability.id === id)?.actions ?? [];
|
|
65
24
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
//# sourceURL=ts/src/booking-surface/profile.ts
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booking Copilot unavailable/changed 恢复链契约层(issue #142 非门控切片)。
|
|
3
|
+
*
|
|
4
|
+
* ⚠ 非运行时激活:纯函数契约层,不发起供应商调用、不读凭据、不接线 runtime seam——
|
|
5
|
+
* #142 合并 gate 中「四 surface 真实库存」与「真实 Checkout/QueryOrders 证据」仍是
|
|
6
|
+
* no-spend 门控项(等真实供应商/UAT)。本层只把恢复链的**结构化处置**固化为可离线
|
|
7
|
+
* 证明的契约:检测(识别分类)→ 恢复(受控重试)→ 用户沟通(显式披露)→ 审计(可对账)。
|
|
8
|
+
* 检测/恢复的合法性判定仍归 availability reducer(ADR-23 availability-policy.ts)所有;
|
|
9
|
+
* 本层是它的用户沟通与审计投影,不替代、不绕过。
|
|
10
|
+
*
|
|
11
|
+
* 词汇复用(不发明新通道):
|
|
12
|
+
* - 分类闭集复用 ActionReceipt.status 闭集(changed/unavailable/no_match/applied/partial/
|
|
13
|
+
* stale/failed/needs_input/unsupported,contracts.ts)与 offer.availability/gap 观测形态;
|
|
14
|
+
* - 每酒店有界恢复预算复用 availability-policy 的 MAX_OFFER_QUERIES_PER_HOTEL /
|
|
15
|
+
* MAX_OFFER_CHECKS_PER_HOTEL(RECOVERY_REUSED_BUDGETS 冻结引用,防两处漂移);
|
|
16
|
+
* - 恢复轮次的模型调用语义对齐 planner 三次调用预算(dsh-planner attempt < 3):
|
|
17
|
+
* 每个触发源最多 3 次计入预算的恢复轮,第 4 次结构性拒绝、必须降级;
|
|
18
|
+
* - 审计事件命名沿用账本 booking.copilot.* 命名空间(runtime.ts 事件 kind 惯例);
|
|
19
|
+
* 链校验形态对齐 booking-saga 的 sagaTraceViolations(开立→推进→终态吸收)。
|
|
20
|
+
*
|
|
21
|
+
* 红线(全部结构性编码):
|
|
22
|
+
* - changed/unavailable 必须显式分类——不静默当确认,也不静默丢弃;
|
|
23
|
+
* - 不可确认证据只产生 inconclusive,不得宣称「市场无房」(与 availability reducer 同红线);
|
|
24
|
+
* - 恢复命中与用户原选择不同酒店/报价 = 替换,必须先出显式披露——静默换房换航结构性拒绝;
|
|
25
|
+
* - 恢复耗尽只能显式降级并向用户披露,不得静默替换或假称确认;
|
|
26
|
+
* - 版本位移(v1→v2)的解决必须携带非空变化说明——不静默换版;
|
|
27
|
+
* - 每条审计事件带 receiptDigest 可回溯;resolved/degraded 为吸收终态,其后不得再有事件;
|
|
28
|
+
* - 纯函数:每次推进返回新状态,原状态不可变。
|
|
29
|
+
*/
|
|
30
|
+
import { createHash } from 'node:crypto';
|
|
31
|
+
import { MAX_OFFER_CHECKS_PER_HOTEL, MAX_OFFER_QUERIES_PER_HOTEL } from './availability-policy.js';
|
|
32
|
+
export const RECOVERY_CHAIN_SCHEMA = 'booking_recovery_chain.v1';
|
|
33
|
+
/** 每个触发源最多 3 次计入预算的恢复轮——对齐 planner 三次调用预算的语义(dsh-planner attempt < 3)。 */
|
|
34
|
+
export const MAX_RECOVERY_MODEL_TURNS_PER_TRIGGER = 3;
|
|
35
|
+
export const MAX_RECOVERY_BUDGET_NOTE = '3 counted model turns per trigger, aligned with the planner three-call attempt budget (dsh-planner); ' +
|
|
36
|
+
'per-hotel query/check budgets stay policy-owned (availability-policy MAX_OFFER_QUERIES_PER_HOTEL / MAX_OFFER_CHECKS_PER_HOTEL)';
|
|
37
|
+
/** 恢复重试计入的既有预算(冻结引用 availability-policy 常量,防两处漂移)。 */
|
|
38
|
+
export const RECOVERY_REUSED_BUDGETS = {
|
|
39
|
+
offerQueriesPerHotel: MAX_OFFER_QUERIES_PER_HOTEL,
|
|
40
|
+
offerChecksPerHotel: MAX_OFFER_CHECKS_PER_HOTEL,
|
|
41
|
+
modelTurnsPerTrigger: MAX_RECOVERY_MODEL_TURNS_PER_TRIGGER,
|
|
42
|
+
};
|
|
43
|
+
const RECOVERY_APPROACHES = new Set(['recheck_version', 'requery_same_hotel', 'alternative_candidate']);
|
|
44
|
+
function isNonEmpty(value) { return typeof value === 'string' && value.trim().length > 0; }
|
|
45
|
+
function requireBinding(key, value) {
|
|
46
|
+
if (!isNonEmpty(value))
|
|
47
|
+
throw new Error(`recovery_binding_missing:${key}`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function stable(value) {
|
|
51
|
+
if (Array.isArray(value))
|
|
52
|
+
return value.map(stable);
|
|
53
|
+
if (!value || typeof value !== 'object')
|
|
54
|
+
return value;
|
|
55
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
56
|
+
}
|
|
57
|
+
/** 分类输入 receipt 的规范摘要(键排序后 sha256;证据链锚点)。 */
|
|
58
|
+
export function receiptDigestOf(receipt) {
|
|
59
|
+
return createHash('sha256').update(JSON.stringify(stable(receipt))).digest('hex');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 检测(识别分类):把一张 action receipt 分类到恢复链闭集。
|
|
63
|
+
* - offer.availability 观测:完整分类 + 连贯性守卫(与 availability reducer 同一套不变式,
|
|
64
|
+
* 违例抛 availability_receipt_incoherent / availability_observation_required 同名错误);
|
|
65
|
+
* - offers.state 观测:query seam,generation 折叠归 policy 所有 → unrelated;
|
|
66
|
+
* - gap 观测:不可确认 → inconclusive(不得宣称市场无房);
|
|
67
|
+
* - 其余观测上的 changed/unavailable/no_match 是错配凭证 → fail-closed 抛错;
|
|
68
|
+
* 其余状态(applied/partial/…)落在其他 seam → unrelated。
|
|
69
|
+
*/
|
|
70
|
+
export function classifyRecoveryTrigger(receipt) {
|
|
71
|
+
const observation = receipt.observation;
|
|
72
|
+
if (observation.kind === 'offers.state')
|
|
73
|
+
return { kind: 'unrelated', observationKind: observation.kind, reason: 'query seam: generation folding is availability-policy owned' };
|
|
74
|
+
if (observation.kind === 'gap')
|
|
75
|
+
return { kind: 'inconclusive', reason: `gap observation (${observation.code}): unconfirmable evidence — not_claim_market_empty` };
|
|
76
|
+
if (observation.kind !== 'offer.availability') {
|
|
77
|
+
if (['changed', 'unavailable', 'no_match'].includes(receipt.status))
|
|
78
|
+
throw new Error('availability_observation_required');
|
|
79
|
+
return { kind: 'unrelated', observationKind: observation.kind, reason: `receipt status ${receipt.status} rides a non-offer observation` };
|
|
80
|
+
}
|
|
81
|
+
const { offerRef, checkedOfferVersionRef, available, currentOfferVersionRef, verifiedOfferRef, changedFactRefs } = observation;
|
|
82
|
+
// 连贯性守卫(镜像 recordOfferCheckReceipt 的 incoherent 判定,fail-closed)
|
|
83
|
+
if (available && (receipt.resultContract.outcome === 'empty' || ['unavailable', 'no_match', 'failed', 'stale', 'unsupported'].includes(receipt.status)))
|
|
84
|
+
throw new Error('availability_receipt_incoherent');
|
|
85
|
+
if (!available && (verifiedOfferRef || currentOfferVersionRef))
|
|
86
|
+
throw new Error('availability_receipt_incoherent');
|
|
87
|
+
if (['unavailable', 'no_match'].includes(receipt.status) && receipt.resultContract.outcome !== 'empty')
|
|
88
|
+
throw new Error('availability_receipt_incoherent');
|
|
89
|
+
if (receipt.status === 'applied' && available && currentOfferVersionRef === checkedOfferVersionRef && verifiedOfferRef && changedFactRefs.length === 0) {
|
|
90
|
+
return { kind: 'confirmed', offerRef, offerVersionRef: checkedOfferVersionRef, verifiedOfferRef };
|
|
91
|
+
}
|
|
92
|
+
if (receipt.status === 'changed' && available && currentOfferVersionRef && currentOfferVersionRef !== checkedOfferVersionRef) {
|
|
93
|
+
return { kind: 'changed', offerRef, checkedOfferVersionRef, currentOfferVersionRef, changedFactRefs, reason: `supplier superseded ${checkedOfferVersionRef} with ${currentOfferVersionRef} (${changedFactRefs.join(',') || 'unspecified facts'})` };
|
|
94
|
+
}
|
|
95
|
+
if (['unavailable', 'no_match'].includes(receipt.status) && !available) {
|
|
96
|
+
return { kind: 'unavailable', offerRef, offerVersionRef: checkedOfferVersionRef, reason: `complete negative evidence (${receipt.status}, empty outcome)` };
|
|
97
|
+
}
|
|
98
|
+
return { kind: 'inconclusive', reason: `status ${receipt.status}: unconfirmable evidence — partial/stale/failed/gap cannot claim market empty (not_claim_market_empty)` };
|
|
99
|
+
}
|
|
100
|
+
function isSelection(value) {
|
|
101
|
+
const candidate = value;
|
|
102
|
+
return Boolean(candidate && isNonEmpty(candidate.hotelRef) && isNonEmpty(candidate.offerRef) && isNonEmpty(candidate.offerVersionRef));
|
|
103
|
+
}
|
|
104
|
+
function sameOfferTarget(a, b) {
|
|
105
|
+
return a.hotelRef === b.hotelRef && a.offerRef === b.offerRef;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 开立恢复链:检测出 changed/unavailable/inconclusive 触发后建链(fail-closed 绑定键 +
|
|
109
|
+
* 首条 recovery.detected 审计事件)。confirmed 无需恢复链、unrelated 无触发,均拒绝开链。
|
|
110
|
+
*/
|
|
111
|
+
export function openRecoveryChain(seed) {
|
|
112
|
+
const taskId = requireBinding('taskId', seed.taskId);
|
|
113
|
+
const contextRef = requireBinding('contextRef', seed.contextRef);
|
|
114
|
+
if (!isNonEmpty(seed.at))
|
|
115
|
+
throw new Error('recovery_binding_missing:at');
|
|
116
|
+
const originalSelection = seed.originalSelection;
|
|
117
|
+
if (!isSelection(originalSelection))
|
|
118
|
+
throw new Error('recovery_binding_missing:originalSelection');
|
|
119
|
+
const classification = classifyRecoveryTrigger(seed.receipt);
|
|
120
|
+
if (classification.kind === 'confirmed')
|
|
121
|
+
throw new Error('recovery_chain_not_required');
|
|
122
|
+
if (classification.kind === 'unrelated')
|
|
123
|
+
throw new Error('recovery_trigger_absent');
|
|
124
|
+
return {
|
|
125
|
+
schemaVersion: RECOVERY_CHAIN_SCHEMA,
|
|
126
|
+
taskId,
|
|
127
|
+
contextRef,
|
|
128
|
+
trigger: classification.kind,
|
|
129
|
+
triggerReason: classification.reason,
|
|
130
|
+
originalSelection: { ...originalSelection },
|
|
131
|
+
countedModelTurns: 0,
|
|
132
|
+
phase: 'detected',
|
|
133
|
+
substitutionDisclosed: false,
|
|
134
|
+
history: [{
|
|
135
|
+
at: seed.at,
|
|
136
|
+
kind: 'recovery.detected',
|
|
137
|
+
from: 'detected',
|
|
138
|
+
to: 'detected',
|
|
139
|
+
reason: classification.reason,
|
|
140
|
+
receiptDigest: receiptDigestOf(seed.receipt),
|
|
141
|
+
target: { ...originalSelection },
|
|
142
|
+
}],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 受控恢复推进:一步一凭证(重查/换版复查/替代候选的检查结果)。
|
|
147
|
+
* - confirmed → resolved;目标与原选择不同酒店/报价且未先披露 = 静默替换,结构性拒绝;
|
|
148
|
+
* - 其余分类 → 计入预算的恢复轮(countedModelTurns + 1,含确认轮);超 3 次拒绝、必须降级;
|
|
149
|
+
* - resolved/degraded 为吸收终态,其后不得再有 attempt。
|
|
150
|
+
*/
|
|
151
|
+
export function attemptRecoveryStep(state, attempt, at) {
|
|
152
|
+
if (state.phase === 'resolved' || state.phase === 'degraded')
|
|
153
|
+
throw new Error('recovery_chain_terminal');
|
|
154
|
+
if (!RECOVERY_APPROACHES.has(attempt.approach))
|
|
155
|
+
throw new Error('recovery_approach_invalid');
|
|
156
|
+
if (!isNonEmpty(at))
|
|
157
|
+
throw new Error('recovery_binding_missing:at');
|
|
158
|
+
if (!isSelection(attempt.target))
|
|
159
|
+
throw new Error('recovery_binding_missing:target');
|
|
160
|
+
const classification = classifyRecoveryTrigger(attempt.receipt);
|
|
161
|
+
if (classification.kind === 'unrelated')
|
|
162
|
+
throw new Error('recovery_trigger_absent');
|
|
163
|
+
if (classification.kind === 'confirmed') {
|
|
164
|
+
if (!sameOfferTarget(attempt.target, state.originalSelection) && !state.substitutionDisclosed) {
|
|
165
|
+
throw new Error('recovery_silent_swap_forbidden');
|
|
166
|
+
}
|
|
167
|
+
const versionShifted = attempt.target.offerVersionRef !== state.originalSelection.offerVersionRef;
|
|
168
|
+
return {
|
|
169
|
+
...state,
|
|
170
|
+
phase: 'resolved',
|
|
171
|
+
resolvedTo: { ...attempt.target },
|
|
172
|
+
history: [...state.history, {
|
|
173
|
+
at,
|
|
174
|
+
kind: 'recovery.resolved',
|
|
175
|
+
from: state.phase,
|
|
176
|
+
to: 'resolved',
|
|
177
|
+
reason: `confirmed via ${attempt.approach}${versionShifted ? ' on superseded version' : ''}`,
|
|
178
|
+
receiptDigest: receiptDigestOf(attempt.receipt),
|
|
179
|
+
target: { ...attempt.target },
|
|
180
|
+
substitution: !sameOfferTarget(attempt.target, state.originalSelection),
|
|
181
|
+
}],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (state.countedModelTurns + 1 > MAX_RECOVERY_MODEL_TURNS_PER_TRIGGER) {
|
|
185
|
+
throw new Error(`recovery_model_turn_budget_exhausted (${MAX_RECOVERY_MODEL_TURNS_PER_TRIGGER} counted turns per trigger; degrade explicitly instead of retrying)`);
|
|
186
|
+
}
|
|
187
|
+
const reason = classification.reason;
|
|
188
|
+
return {
|
|
189
|
+
...state,
|
|
190
|
+
phase: 'recovering',
|
|
191
|
+
countedModelTurns: state.countedModelTurns + 1,
|
|
192
|
+
history: [...state.history, {
|
|
193
|
+
at,
|
|
194
|
+
kind: 'recovery.attempt',
|
|
195
|
+
from: state.phase,
|
|
196
|
+
to: 'recovering',
|
|
197
|
+
reason: `${attempt.approach}: ${classification.kind}: ${reason}`,
|
|
198
|
+
receiptDigest: receiptDigestOf(attempt.receipt),
|
|
199
|
+
target: { ...attempt.target },
|
|
200
|
+
}],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 替换披露:恢复将落在与用户原选择不同的酒店/报价上时,必须在确认前显式披露。
|
|
205
|
+
* 空披露文本拒绝——「显式说明变化,不静默换房换航」的结构化落点。
|
|
206
|
+
*/
|
|
207
|
+
export function discloseSubstitution(state, disclosure) {
|
|
208
|
+
if (state.phase === 'resolved' || state.phase === 'degraded')
|
|
209
|
+
throw new Error('recovery_chain_terminal');
|
|
210
|
+
if (!isNonEmpty(disclosure.disclosureText))
|
|
211
|
+
throw new Error('recovery_disclosure_required');
|
|
212
|
+
if (!isSelection(disclosure.target))
|
|
213
|
+
throw new Error('recovery_binding_missing:target');
|
|
214
|
+
if (!isNonEmpty(disclosure.at))
|
|
215
|
+
throw new Error('recovery_binding_missing:at');
|
|
216
|
+
return {
|
|
217
|
+
...state,
|
|
218
|
+
substitutionDisclosed: true,
|
|
219
|
+
history: [...state.history, {
|
|
220
|
+
at: disclosure.at,
|
|
221
|
+
kind: 'recovery.disclosed',
|
|
222
|
+
from: state.phase,
|
|
223
|
+
to: state.phase,
|
|
224
|
+
reason: disclosure.disclosureText.slice(0, 500),
|
|
225
|
+
target: { ...disclosure.target },
|
|
226
|
+
substitution: true,
|
|
227
|
+
}],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 显式降级:恢复预算耗尽(或有界候选全负/不可确认)时,只能携非空用户披露降级结束——
|
|
232
|
+
* 静默降级拒绝;降级后链吸收,不得再推进。
|
|
233
|
+
*/
|
|
234
|
+
export function degradeRecoveryChain(state, degrade) {
|
|
235
|
+
if (state.phase === 'resolved' || state.phase === 'degraded')
|
|
236
|
+
throw new Error('recovery_chain_terminal');
|
|
237
|
+
if (!isNonEmpty(degrade.reason))
|
|
238
|
+
throw new Error('recovery_binding_missing:reason');
|
|
239
|
+
if (!isNonEmpty(degrade.disclosureText))
|
|
240
|
+
throw new Error('recovery_degrade_requires_disclosure');
|
|
241
|
+
if (!isNonEmpty(degrade.at))
|
|
242
|
+
throw new Error('recovery_binding_missing:at');
|
|
243
|
+
return {
|
|
244
|
+
...state,
|
|
245
|
+
phase: 'degraded',
|
|
246
|
+
history: [...state.history,
|
|
247
|
+
{
|
|
248
|
+
at: degrade.at,
|
|
249
|
+
kind: 'recovery.disclosed',
|
|
250
|
+
from: state.phase,
|
|
251
|
+
to: state.phase,
|
|
252
|
+
reason: degrade.disclosureText.slice(0, 500),
|
|
253
|
+
substitution: false,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
at: degrade.at,
|
|
257
|
+
kind: 'recovery.degraded',
|
|
258
|
+
from: state.phase,
|
|
259
|
+
to: 'degraded',
|
|
260
|
+
reason: degrade.reason.slice(0, 500),
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** 用户沟通投影:结构化披露(substitution/versionShifted/确定性说明文本)。 */
|
|
266
|
+
export function buildUserDisclosure(state) {
|
|
267
|
+
if (state.phase === 'resolved' && state.resolvedTo) {
|
|
268
|
+
const substitution = !sameOfferTarget(state.resolvedTo, state.originalSelection);
|
|
269
|
+
const versionShifted = state.resolvedTo.offerVersionRef !== state.originalSelection.offerVersionRef;
|
|
270
|
+
if (substitution) {
|
|
271
|
+
return {
|
|
272
|
+
substitution,
|
|
273
|
+
versionShifted,
|
|
274
|
+
text: `Availability changed: original ${state.originalSelection.hotelRef}/${state.originalSelection.offerRef} could not be confirmed (${state.triggerReason}). The substitute ${state.resolvedTo.hotelRef}/${state.resolvedTo.offerRef} is disclosed explicitly for your approval — no silent swap.`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (versionShifted) {
|
|
278
|
+
return {
|
|
279
|
+
substitution,
|
|
280
|
+
versionShifted,
|
|
281
|
+
text: `Availability changed: ${state.originalSelection.offerRef} moved from version ${state.originalSelection.offerVersionRef} to ${state.resolvedTo.offerVersionRef} at the supplier; re-confirmed on the new version.`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return { substitution, versionShifted, text: 'Original selection confirmed after recovery.' };
|
|
285
|
+
}
|
|
286
|
+
if (state.phase === 'degraded') {
|
|
287
|
+
return { substitution: false, versionShifted: false, text: `Recovery exhausted for ${state.originalSelection.hotelRef}/${state.originalSelection.offerRef}: ${state.triggerReason}. Task ended explicitly with no substitute booked.` };
|
|
288
|
+
}
|
|
289
|
+
return { substitution: false, versionShifted: false, text: `Recovery in progress: supplier reported ${state.trigger} — ${state.triggerReason}` };
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* 审计链校验(形态对齐 booking-saga 的 sagaTraceViolations;空数组 = 链合法):
|
|
293
|
+
* - 必须以恰好一条 recovery.detected 开链;
|
|
294
|
+
* - resolved/degraded 至多各一次且为吸收终态——其后不得有任何事件;
|
|
295
|
+
* - attempt/disclosed 只许出现在终态前;
|
|
296
|
+
* - resolved 目标与 detected 原目标不同酒店/报价时,之前必须有 substitution=true 的披露;
|
|
297
|
+
* - degraded 之前必须有披露事件(静默降级违例)。
|
|
298
|
+
*/
|
|
299
|
+
export function recoveryAuditViolations(events) {
|
|
300
|
+
const violations = [];
|
|
301
|
+
let opened = false;
|
|
302
|
+
let detectedTarget;
|
|
303
|
+
let terminal;
|
|
304
|
+
let disclosedSubstitution = false;
|
|
305
|
+
let disclosedSeen = false;
|
|
306
|
+
events.forEach((event, index) => {
|
|
307
|
+
const at = `#${index} ${event.kind}`;
|
|
308
|
+
if (terminal) {
|
|
309
|
+
violations.push(`${at}: 终态(${terminal})后出现事件(吸收态)`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
switch (event.kind) {
|
|
313
|
+
case 'recovery.detected': {
|
|
314
|
+
if (opened) {
|
|
315
|
+
violations.push(`${at}: 重复开链`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
opened = true;
|
|
319
|
+
detectedTarget = event.target;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
case 'recovery.attempt': {
|
|
323
|
+
if (!opened) {
|
|
324
|
+
violations.push(`${at}: 开链前的 attempt`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
case 'recovery.disclosed': {
|
|
330
|
+
if (!opened) {
|
|
331
|
+
violations.push(`${at}: 开链前的披露`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
disclosedSeen = true;
|
|
335
|
+
if (event.substitution)
|
|
336
|
+
disclosedSubstitution = true;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
case 'recovery.resolved': {
|
|
340
|
+
if (!opened) {
|
|
341
|
+
violations.push(`${at}: 开链前的 resolved`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (detectedTarget && event.target && !sameOfferTarget(event.target, detectedTarget) && !disclosedSubstitution) {
|
|
345
|
+
violations.push(`${at}: 静默替换(resolved 目标偏离原选择且无披露)`);
|
|
346
|
+
}
|
|
347
|
+
terminal = 'resolved';
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
case 'recovery.degraded': {
|
|
351
|
+
if (!opened) {
|
|
352
|
+
violations.push(`${at}: 开链前的 degraded`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (!disclosedSeen)
|
|
356
|
+
violations.push(`${at}: 静默降级(degraded 前无披露)`);
|
|
357
|
+
terminal = 'degraded';
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
default:
|
|
361
|
+
violations.push(`${at}: 未知事件 kind`);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
if (!opened)
|
|
365
|
+
violations.push('链未以 recovery.detected 开立');
|
|
366
|
+
return violations;
|
|
367
|
+
}
|