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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -7,7 +7,8 @@
7
7
  * the sole executor after the parent adapter validates the canonical action.
8
8
  */
9
9
 
10
- import { dshBookingActionSchemaForKind } from './canonical-schema.js'
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') return error.split(':', 1)[0];
78
- if (error instanceof Error && error.message) return error.message.split(':', 1)[0];
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) return 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) ? uppercase : 'PLANNER_FAILED';
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
@@ -6,6 +6,3 @@ export * from './server.js';
6
6
  export * from './startup.js';
7
7
  export * from './validation.js';
8
8
  export * from './availability-policy.js';
9
-
10
-
11
- //# sourceURL=ts/src/booking-surface/index.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
- id: 'search-hotels',
15
- effect: 'read',
16
- actions: Object.freeze([
17
- 'search.patch',
18
- 'search.run'
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
+ }