@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.27

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 (507) hide show
  1. package/README.md +172 -219
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1744 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -56
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +135 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +546 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +947 -110
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +509 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/resilience.js +191 -0
  41. package/dist/capabilities/session/action-cache.js +50 -44
  42. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  43. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  44. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  45. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  46. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  47. package/dist/capabilities/session/benchmark.js +189 -0
  48. package/dist/capabilities/session/bridge-ledger.js +224 -0
  49. package/dist/capabilities/session/extension-bridge.js +636 -0
  50. package/dist/capabilities/session/extension-channel.js +109 -0
  51. package/dist/capabilities/session/extension-distribution.js +230 -0
  52. package/dist/capabilities/session/extract.js +31 -21
  53. package/dist/capabilities/session/golden-score.js +114 -0
  54. package/dist/capabilities/session/health-watch.js +215 -0
  55. package/dist/capabilities/session/read-guard.js +75 -24
  56. package/dist/capabilities/session/static-flight-golden.js +150 -0
  57. package/dist/capabilities/session/transport.js +97 -83
  58. package/dist/capabilities/session/wizard.js +117 -0
  59. package/dist/capabilities/session-consent.js +178 -0
  60. package/dist/capabilities/session-login.js +170 -0
  61. package/dist/capabilities/session-search.js +635 -44
  62. package/dist/capabilities/spawn-bounded.js +203 -0
  63. package/dist/capabilities/sponsor-plugin.js +341 -0
  64. package/dist/capabilities/visa-policy.js +140 -0
  65. package/dist/capabilities/weather.js +243 -138
  66. package/dist/data/session-golden-20.json +25 -0
  67. package/dist/data/sf-golden-manifest.json +103 -0
  68. package/dist/data/sf-static-routes.json +91 -0
  69. package/dist/scripts/action-cache-tests.js +24 -59
  70. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  71. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  72. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  73. package/dist/scripts/agent-reach-error-tests.js +354 -0
  74. package/dist/scripts/agent-reach-tests.js +26 -18
  75. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  76. package/dist/scripts/anything-tests.js +116 -60
  77. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  78. package/dist/scripts/artifacts-capability-tests.js +727 -0
  79. package/dist/scripts/async-collect.js +50 -11
  80. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  81. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  82. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  83. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  84. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  85. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  86. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  87. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  88. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  89. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  90. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  91. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
  92. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  93. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  94. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  95. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  96. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  97. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  98. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  99. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  100. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  101. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  102. package/dist/scripts/booking-executor-tests.js +80 -0
  103. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  104. package/dist/scripts/booking-saga-tests.js +125 -0
  105. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  106. package/dist/scripts/bootstrap-tests.js +1479 -0
  107. package/dist/scripts/bridge-events-tests.js +239 -0
  108. package/dist/scripts/bridge-ledger-tests.js +428 -0
  109. package/dist/scripts/build-changelog.js +238 -0
  110. package/dist/scripts/build-metrics-report.js +331 -0
  111. package/dist/scripts/changelog-tests.js +118 -0
  112. package/dist/scripts/channel-probe-tests.js +103 -0
  113. package/dist/scripts/channel-probe.js +169 -0
  114. package/dist/scripts/channel-registry-tests.js +208 -0
  115. package/dist/scripts/companion-tests.js +25 -70
  116. package/dist/scripts/dida-cookie-probe.js +87 -0
  117. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  118. package/dist/scripts/dida-sw-probe.js +114 -0
  119. package/dist/scripts/diff-test.js +17 -12
  120. package/dist/scripts/doctor-tests.js +564 -0
  121. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  122. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  123. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  124. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  125. package/dist/scripts/dsh-runtime-closure.js +162 -0
  126. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  127. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  128. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  129. package/dist/scripts/effect-tests.js +539 -0
  130. package/dist/scripts/engine-run.js +1 -3
  131. package/dist/scripts/engine-tests.js +16 -10
  132. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  133. package/dist/scripts/evaluation-contract-tests.js +268 -0
  134. package/dist/scripts/extension-distribution-cli.js +39 -0
  135. package/dist/scripts/extension-distribution-tests.js +443 -0
  136. package/dist/scripts/extension-tests.js +988 -0
  137. package/dist/scripts/external-event-tests.js +350 -0
  138. package/dist/scripts/fact-gate-tests.js +869 -0
  139. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  140. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  141. package/dist/scripts/flight-malformed-tests.js +159 -0
  142. package/dist/scripts/flyai-setup-tests.js +228 -0
  143. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  144. package/dist/scripts/flyai-tests.js +692 -0
  145. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  146. package/dist/scripts/fx-contract-tests.js +201 -0
  147. package/dist/scripts/g5-guard-tests.js +131 -0
  148. package/dist/scripts/g5-guard.js +189 -0
  149. package/dist/scripts/gen-lunar-anchors.js +106 -0
  150. package/dist/scripts/geo-atlas-tests.js +283 -0
  151. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  152. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  153. package/dist/scripts/gotry-try-demo.js +49 -0
  154. package/dist/scripts/ground-transfer-tests.js +956 -0
  155. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  156. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  157. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  158. package/dist/scripts/hbcli-tests.js +121 -69
  159. package/dist/scripts/health-watch-cli.js +73 -0
  160. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  161. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  162. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  163. package/dist/scripts/i18n-tests.js +41 -0
  164. package/dist/scripts/incident-tests.js +281 -87
  165. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  166. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  167. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  168. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  169. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  170. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  171. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  172. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  173. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  174. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  175. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  176. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  177. package/dist/scripts/itinerary-deck-tests.js +435 -0
  178. package/dist/scripts/itinerary-html-tests.js +426 -0
  179. package/dist/scripts/journey-tests.js +22 -27
  180. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  181. package/dist/scripts/kernel-manifest-gate.js +126 -0
  182. package/dist/scripts/kernel-manifest-tests.js +212 -0
  183. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  184. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  185. package/dist/scripts/kernel-manifest.js +369 -0
  186. package/dist/scripts/lavish-local-tests.js +1226 -0
  187. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  188. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  189. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  190. package/dist/scripts/ledger-tests.js +382 -246
  191. package/dist/scripts/ledger-workflow-crash.js +10 -20
  192. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  193. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  194. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  195. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  196. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  197. package/dist/scripts/memory-capture-tests.js +242 -61
  198. package/dist/scripts/memory-decay-tests.js +18 -52
  199. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  200. package/dist/scripts/memory-lifecycle.js +196 -0
  201. package/dist/scripts/memory-metrics.js +24 -12
  202. package/dist/scripts/memory-value-report.js +631 -0
  203. package/dist/scripts/memory-value-tests.js +328 -0
  204. package/dist/scripts/metrics-report-tests.js +151 -0
  205. package/dist/scripts/model-override-e2e.js +137 -0
  206. package/dist/scripts/nightly-evidence-tests.js +161 -0
  207. package/dist/scripts/nightly-evidence.js +286 -0
  208. package/dist/scripts/nudge-digest.js +39 -28
  209. package/dist/scripts/observability-tests.js +381 -0
  210. package/dist/scripts/onboarding-tests.js +161 -0
  211. package/dist/scripts/opensky-check.js +18 -13
  212. package/dist/scripts/opensky-tests.js +28 -28
  213. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  214. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  215. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  216. package/dist/scripts/price-drift-tests.js +340 -0
  217. package/dist/scripts/price-drift-watch.js +447 -0
  218. package/dist/scripts/probe-poi-tests.js +53 -45
  219. package/dist/scripts/product-metrics.js +483 -0
  220. package/dist/scripts/publish-preverify.js +110 -0
  221. package/dist/scripts/realtime-pricing-tests.js +76 -0
  222. package/dist/scripts/recall-tests.js +513 -0
  223. package/dist/scripts/replay-async.js +20 -18
  224. package/dist/scripts/replay-real.js +18 -18
  225. package/dist/scripts/replay.js +52 -82
  226. package/dist/scripts/session-attach-diagnose.js +20 -20
  227. package/dist/scripts/session-attach-poc.js +34 -37
  228. package/dist/scripts/session-benchmark.js +180 -0
  229. package/dist/scripts/session-dida-live-e2e.js +199 -0
  230. package/dist/scripts/session-extract-tests.js +14 -35
  231. package/dist/scripts/session-link-tests.js +478 -0
  232. package/dist/scripts/session-login.js +27 -46
  233. package/dist/scripts/session-tests.js +796 -96
  234. package/dist/scripts/session-zones-tests.js +358 -0
  235. package/dist/scripts/sf-live-benchmark.js +367 -0
  236. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  237. package/dist/scripts/sf-live-cli-tests.js +92 -0
  238. package/dist/scripts/sf-live-options.js +46 -0
  239. package/dist/scripts/sf-manifest-tests.js +30 -0
  240. package/dist/scripts/sf-manifest.js +33 -0
  241. package/dist/scripts/sf-soft-score-tests.js +67 -0
  242. package/dist/scripts/sf-summary-tests.js +324 -0
  243. package/dist/scripts/sf-summary.js +525 -0
  244. package/dist/scripts/share-tests.js +432 -0
  245. package/dist/scripts/skeleton-check.js +16 -14
  246. package/dist/scripts/skeleton-integration-test.js +28 -11
  247. package/dist/scripts/skills-contract-tests.js +51 -46
  248. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  249. package/dist/scripts/smoke.js +690 -246
  250. package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
  251. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  252. package/dist/scripts/state-cli-tests.js +226 -101
  253. package/dist/scripts/state-cli.js +550 -199
  254. package/dist/scripts/state-repair-cli.js +66 -0
  255. package/dist/scripts/state-repair-tests.js +140 -0
  256. package/dist/scripts/static-golden-tests.js +165 -0
  257. package/dist/scripts/time-eval-tests.js +210 -192
  258. package/dist/scripts/travel-timeline-tests.js +29 -93
  259. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  260. package/dist/scripts/turn-handoff-collect.js +161 -0
  261. package/dist/scripts/turn-policy-tests.js +55 -0
  262. package/dist/scripts/typed-contract-canary.js +176 -0
  263. package/dist/scripts/tz-resolver-sanity.js +50 -0
  264. package/dist/scripts/tz-resolver-tests.js +584 -0
  265. package/dist/scripts/unified-tests.js +23 -19
  266. package/dist/scripts/visa-policy-tests.js +62 -0
  267. package/dist/scripts/weather-tests.js +276 -41
  268. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  269. package/dist/scripts/write-gate-tests.js +522 -0
  270. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  271. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  272. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  273. package/dist/scripts/z3-race-tests.js +41 -0
  274. package/dist/src/artifact-gate.js +829 -0
  275. package/dist/src/backend/kernel.js +91 -0
  276. package/dist/src/backend/modules/booking-copilot.js +92 -0
  277. package/dist/src/backend/modules/booking-executor.js +124 -0
  278. package/dist/src/backend/modules/session-search.js +551 -0
  279. package/dist/src/benchmark-agent-conformance.js +578 -0
  280. package/dist/src/benchmark-environment-bridge.js +515 -0
  281. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  282. package/dist/src/benchmark-tool-isolation.js +151 -0
  283. package/dist/src/bookable-facts.js +533 -0
  284. package/dist/src/booking-saga.js +97 -0
  285. package/dist/src/booking-surface/availability-policy.js +697 -0
  286. package/dist/src/booking-surface/booking-intent.js +43 -0
  287. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  288. package/dist/src/booking-surface/canonical-schema.js +116 -0
  289. package/dist/src/booking-surface/contracts.js +75 -0
  290. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  291. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  292. package/dist/src/booking-surface/dsh-worker.js +98 -0
  293. package/dist/src/booking-surface/error-codes.js +231 -0
  294. package/dist/src/booking-surface/index.js +9 -0
  295. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  296. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  297. package/dist/src/booking-surface/profile.js +24 -0
  298. package/dist/src/booking-surface/recovery-chain.js +367 -0
  299. package/dist/src/booking-surface/runtime.js +1968 -0
  300. package/dist/src/booking-surface/server.js +674 -0
  301. package/dist/src/booking-surface/startup.js +168 -0
  302. package/dist/src/booking-surface/validation.js +261 -0
  303. package/dist/src/bridge.js +20 -13
  304. package/dist/src/companions.js +48 -74
  305. package/dist/src/contracts.js +21 -43
  306. package/dist/src/dsh-llm.js +97 -111
  307. package/dist/src/engine.js +276 -193
  308. package/dist/src/evaluation-cadence.js +97 -0
  309. package/dist/src/evaluation-contracts.js +283 -0
  310. package/dist/src/flight-pack-adapter.js +54 -0
  311. package/dist/src/flight-pack-contract.js +2 -0
  312. package/dist/src/flyai-setup-tool.js +70 -0
  313. package/dist/src/gotry-backend.js +125 -0
  314. package/dist/src/hotel-date-gate.js +118 -0
  315. package/dist/src/i18n.js +88 -0
  316. package/dist/src/index.js +2227 -882
  317. package/dist/src/itinerary-deck.js +139 -0
  318. package/dist/src/itinerary-doc-shared.js +661 -0
  319. package/dist/src/itinerary-html.js +131 -0
  320. package/dist/src/journey.js +155 -108
  321. package/dist/src/lavish-tools.js +400 -0
  322. package/dist/src/ledger-repair-apply.js +538 -0
  323. package/dist/src/ledger-repair-plan.js +463 -0
  324. package/dist/src/loop.js +319 -160
  325. package/dist/src/memory-capture.js +180 -32
  326. package/dist/src/memory-decay.js +30 -14
  327. package/dist/src/memory-lifecycle.js +1310 -0
  328. package/dist/src/memory-utility.js +27 -40
  329. package/dist/src/mock-llm.js +36 -61
  330. package/dist/src/model.js +81 -36
  331. package/dist/src/realtime-pricing.js +103 -0
  332. package/dist/src/recall/card.js +74 -0
  333. package/dist/src/recall/evaluator.js +140 -0
  334. package/dist/src/recall/integration.js +17 -0
  335. package/dist/src/recall/tick.js +166 -0
  336. package/dist/src/session-link/plan-it-action.js +48 -0
  337. package/dist/src/session-link/session-link.js +248 -0
  338. package/dist/src/session-zones.js +507 -0
  339. package/dist/src/share/adapters.js +51 -0
  340. package/dist/src/share/share-consent.js +76 -0
  341. package/dist/src/share/share-deck.js +58 -0
  342. package/dist/src/share/share-token.js +146 -0
  343. package/dist/src/slot-spec.js +84 -106
  344. package/dist/src/state-ledger.js +435 -516
  345. package/dist/src/state-repair.js +299 -0
  346. package/dist/src/subagent-job-id-guard.js +58 -0
  347. package/dist/src/time-anchor.js +231 -33
  348. package/dist/src/tool-packet.js +20 -8
  349. package/dist/src/travel-slots.js +58 -35
  350. package/dist/src/travel-timeline.js +44 -63
  351. package/dist/src/turn-deadline.js +287 -0
  352. package/dist/src/turn-policy.js +118 -0
  353. package/dist/src/tz-resolver.js +143 -0
  354. package/dist/src/unified.js +535 -359
  355. package/dist/src/wish-pool.js +42 -12
  356. package/dist/src/write-gate.js +495 -0
  357. package/dist/src/z3-shared.js +284 -0
  358. package/extension/README.md +21 -0
  359. package/extension/README.zh-CN.md +21 -0
  360. package/extension/background.js +463 -0
  361. package/extension/content-bridge.js +270 -0
  362. package/extension/content-main.js +111 -0
  363. package/extension/manifest.json +60 -0
  364. package/package.json +407 -14
  365. package/schemas/booking.intent.schema.json +193 -0
  366. package/schemas/booking.surface.schema.json +2622 -0
  367. package/ts/capabilities/agent-reach-deep.ts +114 -0
  368. package/ts/capabilities/agent-reach.ts +198 -0
  369. package/ts/capabilities/anything.ts +255 -0
  370. package/ts/capabilities/artifacts.ts +428 -0
  371. package/ts/capabilities/fact-log.ts +54 -0
  372. package/ts/capabilities/flyai-config.ts +388 -0
  373. package/ts/capabilities/flyai.ts +1274 -0
  374. package/ts/capabilities/hbcli.ts +253 -71
  375. package/ts/capabilities/incident-log.ts +88 -59
  376. package/ts/capabilities/itinerary-artifact.ts +214 -0
  377. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  378. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  379. package/ts/capabilities/opensky.ts +121 -0
  380. package/ts/capabilities/session/action-cache.ts +148 -0
  381. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  382. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  383. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  384. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  385. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  386. package/ts/capabilities/session/benchmark.ts +276 -0
  387. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  388. package/ts/capabilities/session/extension-bridge.ts +794 -0
  389. package/ts/capabilities/session/extension-channel.ts +144 -0
  390. package/ts/capabilities/session/extension-distribution.ts +266 -0
  391. package/ts/capabilities/session/extract.ts +54 -0
  392. package/ts/capabilities/session/golden-score.ts +139 -0
  393. package/ts/capabilities/session/health-watch.ts +264 -0
  394. package/ts/capabilities/session/read-guard.ts +141 -0
  395. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  396. package/ts/capabilities/session/transport.ts +146 -0
  397. package/ts/capabilities/session/wizard.ts +140 -0
  398. package/ts/capabilities/session-consent.ts +211 -0
  399. package/ts/capabilities/session-login.ts +208 -0
  400. package/ts/capabilities/session-search.ts +893 -0
  401. package/ts/capabilities/weather.ts +330 -0
  402. package/ts/dsh-runtime/vendor/README.md +68 -0
  403. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  404. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  405. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  406. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  407. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  408. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  409. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  410. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  411. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  412. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  413. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  414. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  415. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  416. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  417. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  418. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  419. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  420. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  421. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  422. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  423. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  424. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  425. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  426. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  427. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  428. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  440. package/ts/package.json +293 -3
  441. package/ts/scripts/async-collect.ts +91 -0
  442. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  443. package/ts/scripts/skeleton-check.ts +1 -1
  444. package/ts/scripts/skeleton-integration-test.ts +23 -1
  445. package/ts/scripts/state-cli.ts +568 -0
  446. package/ts/scripts/turn-handoff-collect.ts +178 -0
  447. package/ts/src/artifact-gate.ts +991 -0
  448. package/ts/src/benchmark-agent-conformance.ts +619 -0
  449. package/ts/src/benchmark-environment-bridge.ts +538 -0
  450. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  451. package/ts/src/benchmark-tool-isolation.ts +166 -0
  452. package/ts/src/bookable-facts.ts +837 -0
  453. package/ts/src/booking-saga.ts +133 -0
  454. package/ts/src/booking-surface/availability-policy.ts +522 -0
  455. package/ts/src/booking-surface/booking-intent.ts +86 -0
  456. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  457. package/ts/src/booking-surface/canonical-schema.js +116 -0
  458. package/ts/src/booking-surface/contracts.ts +475 -0
  459. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  460. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  461. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  462. package/ts/src/booking-surface/error-codes.ts +241 -0
  463. package/ts/src/booking-surface/index.ts +9 -0
  464. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  465. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  466. package/ts/src/booking-surface/profile.ts +42 -0
  467. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  468. package/ts/src/booking-surface/runtime.ts +1793 -0
  469. package/ts/src/booking-surface/server.ts +733 -0
  470. package/ts/src/booking-surface/startup.ts +201 -0
  471. package/ts/src/booking-surface/validation.ts +217 -0
  472. package/ts/src/companions.ts +113 -0
  473. package/ts/src/contracts.ts +13 -3
  474. package/ts/src/dsh-llm.ts +44 -11
  475. package/ts/src/flyai-setup-tool.ts +77 -0
  476. package/ts/src/index.ts +1948 -198
  477. package/ts/src/itinerary-deck.ts +189 -0
  478. package/ts/src/itinerary-doc-shared.ts +741 -0
  479. package/ts/src/itinerary-html.ts +171 -0
  480. package/ts/src/loop.ts +223 -25
  481. package/ts/src/memory-capture.ts +283 -0
  482. package/ts/src/memory-decay.ts +55 -0
  483. package/ts/src/memory-utility.ts +70 -0
  484. package/ts/src/mock-llm.ts +4 -8
  485. package/ts/src/model.ts +88 -2
  486. package/ts/src/recall/card.ts +106 -0
  487. package/ts/src/recall/evaluator.ts +170 -0
  488. package/ts/src/recall/integration.ts +35 -0
  489. package/ts/src/recall/tick.ts +213 -0
  490. package/ts/src/session-link/plan-it-action.ts +78 -0
  491. package/ts/src/session-link/session-link.ts +275 -0
  492. package/ts/src/share/adapters.ts +93 -0
  493. package/ts/src/share/share-consent.ts +105 -0
  494. package/ts/src/share/share-deck.ts +85 -0
  495. package/ts/src/share/share-token.ts +166 -0
  496. package/ts/src/slot-spec.ts +172 -0
  497. package/ts/src/state-ledger.ts +912 -0
  498. package/ts/src/subagent-job-id-guard.ts +72 -0
  499. package/ts/src/time-anchor.ts +338 -0
  500. package/ts/src/tool-packet.ts +32 -0
  501. package/ts/src/travel-slots.ts +219 -0
  502. package/ts/src/travel-timeline.ts +83 -0
  503. package/ts/src/turn-deadline.ts +366 -0
  504. package/ts/src/turn-policy.ts +154 -0
  505. package/ts/src/unified.ts +354 -88
  506. package/ts/src/wish-pool.ts +79 -0
  507. package/dist/scripts/session-attach-wait.js +0 -41
@@ -0,0 +1,741 @@
1
+ /**
2
+ * 行程文档共享契约层(issue #564,由 itinerary-html.ts 原样抽取;渲染契约见
3
+ * docs/design/itinerary-html-renderer.md 与 docs/design/itinerary-deck-renderer.md)。
4
+ *
5
+ * 单一事实源:输入校验、事实归一化、证据卡渲染与双面文案只在此实现一次,
6
+ * itinerary-html(单页文档)与 itinerary-deck(幻灯文档)两个投影共用同一套——
7
+ * 任何第二份实现都会静默漂移,这是渲染器设计文档 §3 已确立的反面教训。
8
+ *
9
+ * 本模块保持纯:无 IO、无时钟、无子进程、无网络、无夜数/预算/时间运算;
10
+ * 两条永不合流的证据面(计划面/证据面)与渲染闸锚点纪律同 itinerary-html。
11
+ */
12
+
13
+ import {
14
+ renderFlightFact,
15
+ renderHotelFact,
16
+ renderPolicyFact,
17
+ type Bookability,
18
+ type BookableFact,
19
+ type EvidenceTier,
20
+ type FlightFact,
21
+ type HotelFact,
22
+ type PolicyFact,
23
+ } from './bookable-facts.ts'
24
+
25
+ export interface ItineraryDocInput {
26
+ title: string
27
+ itinerary: {
28
+ trip_start: string
29
+ trip_end: string
30
+ stays: Array<{ place: string; check_in: string; check_out: string }>
31
+ od_segments: Array<{ from: string; to: string; date: string; mode: 'flight' | 'rail' | 'car'; legs: number }>
32
+ }
33
+ /** 调用方已从注册表选出的事实(渲染器不查询上游) */
34
+ facts: BookableFact[]
35
+ }
36
+
37
+ export type ItineraryDocResult = { ok: true; html: string } | { ok: false; errors: string[] }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // 容量与取值上限(超出即 ok:false,绝不静默截断核心行程)
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export const ITINERARY_DOC_LIMITS = {
44
+ docBytes: 2 * 1024 * 1024,
45
+ titleChars: 300,
46
+ stays: 200,
47
+ odSegments: 200,
48
+ facts: 200,
49
+ placeChars: 200,
50
+ fromToChars: 200,
51
+ flightNoChars: 64,
52
+ subjectChars: 64,
53
+ codeChars: 64,
54
+ sourceChars: 200,
55
+ queryIdChars: 200,
56
+ asOfChars: 64,
57
+ factIdChars: 96,
58
+ carrierChars: 96,
59
+ currencyChars: 8,
60
+ statementChars: 2000,
61
+ policyRuntimeFields: 24,
62
+ errors: 32,
63
+ legsMax: 20,
64
+ priceMax: 1e9,
65
+ optionsMax: 1_000_000,
66
+ yearMin: 2000,
67
+ yearMax: 2100,
68
+ } as const
69
+
70
+ const MODES = ['flight', 'rail', 'car'] as const
71
+ const BOOKABILITIES: readonly Bookability[] = ['bookable_exact_date', 'unavailable_exact_date', 'unverified', 'conflict']
72
+ const TIERS: readonly EvidenceTier[] = ['live_inventory', 'route_exists', 'historical_schedule', 'benchmark_price']
73
+
74
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
75
+ const HM_RE = /^([01]\d|2[0-3]):[0-5]\d$/
76
+ const ISO_INSTANT_RE = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(?:Z|[+-]\d{2}:?\d{2})$/
77
+ /** 输出大小上界:1 字符最多 4 字节 UTF-8,先用长度×4 做廉价门槛,再精确计数 */
78
+ const BYTES_PER_CHAR_MAX = 4
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // 校验原语
82
+ // ---------------------------------------------------------------------------
83
+
84
+ function isRecord(value: unknown): value is Record<string, unknown> {
85
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
86
+ }
87
+
88
+ /** 真实日历日期:格式 + 月长 + 闰年(year 2000..2100,故无需处理 0..99 映射歧义) */
89
+ function isRealDate(value: string): boolean {
90
+ if (!DATE_RE.test(value)) return false
91
+ const year = Number(value.slice(0, 4))
92
+ const month = Number(value.slice(5, 7))
93
+ const day = Number(value.slice(8, 10))
94
+ if (year < ITINERARY_DOC_LIMITS.yearMin || year > ITINERARY_DOC_LIMITS.yearMax) return false
95
+ if (month < 1 || month > 12 || day < 1) return false
96
+ return day <= new Date(Date.UTC(year, month, 0)).getUTCDate()
97
+ }
98
+
99
+ function isIsoInstant(value: string): boolean {
100
+ const m = ISO_INSTANT_RE.exec(value)
101
+ if (!m) return false
102
+ return isRealDate(`${m[1]}-${m[2]}-${m[3]}`) && Number(m[4]) <= 23 && Number(m[5]) <= 59 && Number(m[6]) <= 60
103
+ }
104
+
105
+ interface Ctx {
106
+ errors: string[]
107
+ truncated: boolean
108
+ }
109
+
110
+ function pushError(ctx: Ctx, message: string): void {
111
+ if (ctx.errors.length >= ITINERARY_DOC_LIMITS.errors) {
112
+ ctx.truncated = true
113
+ return
114
+ }
115
+ ctx.errors.push(message)
116
+ }
117
+
118
+ /** 必填字符串:非空、不超上限(错误里不回显输入内容,避免把恶意串回灌到日志/终端) */
119
+ function takeString(ctx: Ctx, value: unknown, path: string, maxChars: number): string | undefined {
120
+ if (typeof value !== 'string') {
121
+ pushError(ctx, `${path} 必须是字符串`)
122
+ return undefined
123
+ }
124
+ if (value.length === 0) {
125
+ pushError(ctx, `${path} 不能为空`)
126
+ return undefined
127
+ }
128
+ if (value.length > maxChars) {
129
+ pushError(ctx, `${path} 超出长度上限(${maxChars} 字符,实测 ${value.length})`)
130
+ return undefined
131
+ }
132
+ return value
133
+ }
134
+
135
+ /** 可选字符串:缺省即 undefined(上游未提供不是错误),一旦给出必须合法 */
136
+ function takeOptionalString(ctx: Ctx, value: unknown, path: string, maxChars: number): string | undefined {
137
+ if (value === undefined || value === null) return undefined
138
+ return takeString(ctx, value, path, maxChars)
139
+ }
140
+
141
+ function takeOptionalBoolean(ctx: Ctx, value: unknown, path: string): boolean | undefined {
142
+ if (value === undefined || value === null) return undefined
143
+ if (typeof value !== 'boolean') {
144
+ pushError(ctx, `${path} 必须是布尔值`)
145
+ return undefined
146
+ }
147
+ return value
148
+ }
149
+
150
+ function takeBoundedNumber(ctx: Ctx, value: unknown, path: string, min: number, max: number, integer: boolean): number | undefined {
151
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
152
+ pushError(ctx, `${path} 必须是有限数值`)
153
+ return undefined
154
+ }
155
+ if (value < min || value > max) {
156
+ pushError(ctx, `${path} 超出允许范围 [${min}, ${max}](实测 ${value})`)
157
+ return undefined
158
+ }
159
+ if (integer && !Number.isInteger(value)) {
160
+ pushError(ctx, `${path} 必须是整数`)
161
+ return undefined
162
+ }
163
+ return value
164
+ }
165
+
166
+ function takeOptionalNumber(ctx: Ctx, value: unknown, path: string, min: number, max: number, integer = false): number | undefined {
167
+ if (value === undefined || value === null) return undefined
168
+ return takeBoundedNumber(ctx, value, path, min, max, integer)
169
+ }
170
+
171
+ function takeRequiredNumber(ctx: Ctx, value: unknown, path: string, min: number, max: number, integer = false): number | undefined {
172
+ if (value === undefined || value === null) {
173
+ pushError(ctx, `${path} 缺失`)
174
+ return undefined
175
+ }
176
+ return takeBoundedNumber(ctx, value, path, min, max, integer)
177
+ }
178
+
179
+ function takeArray(ctx: Ctx, value: unknown, path: string, max: number): unknown[] | undefined {
180
+ if (!Array.isArray(value)) {
181
+ pushError(ctx, `${path} 必须是数组`)
182
+ return undefined
183
+ }
184
+ if (value.length > max) {
185
+ pushError(ctx, `${path} 超出容量上限(${max} 条,实测 ${value.length} 条)`)
186
+ return undefined
187
+ }
188
+ return value
189
+ }
190
+
191
+ function takeEnum<T extends string>(ctx: Ctx, value: unknown, path: string, allowed: readonly T[]): T | undefined {
192
+ if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {
193
+ pushError(ctx, `${path} 不是受支持取值(允许:${allowed.join(' / ')})`)
194
+ return undefined
195
+ }
196
+ return value as T
197
+ }
198
+
199
+ function takeDate(ctx: Ctx, value: unknown, path: string): string | undefined {
200
+ if (typeof value !== 'string') {
201
+ pushError(ctx, `${path} 必须是 YYYY-MM-DD 字符串`)
202
+ return undefined
203
+ }
204
+ if (!isRealDate(value)) {
205
+ pushError(ctx, `${path} 不是真实日历日期(要求 YYYY-MM-DD,范围 2000-01-01..2100-12-31)`)
206
+ return undefined
207
+ }
208
+ return value
209
+ }
210
+
211
+ function takeInstant(ctx: Ctx, value: unknown, path: string): string | undefined {
212
+ if (typeof value !== 'string' || !isIsoInstant(value)) {
213
+ pushError(ctx, `${path} 必须是可解析的 ISO 时间戳(含日期与时分秒)`)
214
+ return undefined
215
+ }
216
+ return value
217
+ }
218
+
219
+ function takeAsOf(ctx: Ctx, value: unknown, path: string): string | undefined {
220
+ if (value === undefined || value === null) return undefined
221
+ const asOf = takeString(ctx, value, path, ITINERARY_DOC_LIMITS.asOfChars)
222
+ if (asOf !== undefined && !isRealDate(asOf)) {
223
+ pushError(ctx, `${path} 必须是真实日历日期 YYYY-MM-DD`)
224
+ return undefined
225
+ }
226
+ return asOf
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // 事实校验(BookableFact 输入侧运行时收窄;未知 schema/kind 显式失败)
231
+ // ---------------------------------------------------------------------------
232
+
233
+ /** 上游给不出可判定的 plan:`runtime.is_plan` 只作可选展示,渲染器不据它升级任何结论 */
234
+ export interface PolicyRuntime {
235
+ is_plan?: boolean
236
+ }
237
+
238
+ function takePolicyRuntime(ctx: Ctx, value: unknown, path: string): PolicyRuntime {
239
+ if (value === undefined || value === null) return {}
240
+ if (!isRecord(value)) {
241
+ pushError(ctx, `${path} 必须是对象`)
242
+ return {}
243
+ }
244
+ if (Object.keys(value).length > ITINERARY_DOC_LIMITS.policyRuntimeFields) {
245
+ pushError(ctx, `${path} 字段数超出上限(${ITINERARY_DOC_LIMITS.policyRuntimeFields})`)
246
+ return {}
247
+ }
248
+ const out: PolicyRuntime = {}
249
+ const isPlan = takeOptionalBoolean(ctx, value.is_plan, `${path}.is_plan`)
250
+ if (isPlan !== undefined) out.is_plan = isPlan
251
+ return out
252
+ }
253
+
254
+ export interface NormalizedFlight { kind: 'flight' | 'train'; fact: FlightFact }
255
+ export interface NormalizedHotel { kind: 'hotel'; fact: HotelFact }
256
+ export interface NormalizedPolicy { kind: 'policy'; fact: PolicyFact; runtime: PolicyRuntime }
257
+ export type NormalizedFact = NormalizedFlight | NormalizedHotel | NormalizedPolicy
258
+
259
+ /** 占位值只用于「校验失败时仍把已知字段填进对象」,任何返回 ok:true 的路径下 errors 必为空 */
260
+ const UNKNOWN_TIER: EvidenceTier = 'route_exists'
261
+
262
+ function normalizeFlight(ctx: Ctx, rec: Record<string, unknown>, kind: 'flight' | 'train', path: string): NormalizedFlight {
263
+ const L = ITINERARY_DOC_LIMITS
264
+ const route = isRecord(rec.route) ? rec.route : undefined
265
+ if (!route) pushError(ctx, `${path}.route 必须是对象 {origin, destination}`)
266
+ const factId = takeString(ctx, rec.fact_id, `${path}.fact_id`, L.factIdChars)
267
+ const origin = takeString(ctx, route?.origin, `${path}.route.origin`, L.fromToChars)
268
+ const destination = takeString(ctx, route?.destination, `${path}.route.destination`, L.fromToChars)
269
+ const date = takeDate(ctx, rec.date, `${path}.date`)
270
+ const bookability = takeEnum(ctx, rec.bookability, `${path}.bookability`, BOOKABILITIES)
271
+ const tier = takeEnum(ctx, rec.tier, `${path}.tier`, TIERS)
272
+ const source = takeString(ctx, rec.source, `${path}.source`, L.sourceChars)
273
+ const queryId = takeString(ctx, rec.query_id, `${path}.query_id`, L.queryIdChars)
274
+ const fetchedAt = takeInstant(ctx, rec.fetched_at, `${path}.fetched_at`)
275
+ const asOf = takeAsOf(ctx, rec.as_of, `${path}.as_of`)
276
+ // 负事实(该档期 0 条)允许空班次号;其余可下单性必须给出班次号
277
+ const flightNo = typeof rec.flight_no === 'string' && rec.flight_no.length === 0
278
+ ? ''
279
+ : takeString(ctx, rec.flight_no, `${path}.flight_no`, L.flightNoChars)
280
+ if (bookability !== undefined && bookability !== 'unavailable_exact_date' && flightNo === '') {
281
+ pushError(ctx, `${path}.flight_no 缺失:非「该档期无结果」的事实必须给出班次号`)
282
+ }
283
+ const depLocal = takeOptionalString(ctx, rec.dep_local, `${path}.dep_local`, L.codeChars)
284
+ const arrLocal = takeOptionalString(ctx, rec.arr_local, `${path}.arr_local`, L.codeChars)
285
+ if (depLocal !== undefined && !HM_RE.test(depLocal)) pushError(ctx, `${path}.dep_local 必须是 HH:MM(24 小时制)`)
286
+ if (arrLocal !== undefined && !HM_RE.test(arrLocal)) pushError(ctx, `${path}.arr_local 必须是 HH:MM(24 小时制)`)
287
+ const reviewBy = takeAsOf(ctx, rec.review_by, `${path}.review_by`)
288
+ const fact: FlightFact = {
289
+ schema: 'gotry_bookable_fact.v1',
290
+ fact_id: factId ?? '',
291
+ kind,
292
+ route: { origin: origin ?? '', destination: destination ?? '', origin_airport: takeOptionalString(ctx, route?.origin_airport, `${path}.route.origin_airport`, L.codeChars), dest_airport: takeOptionalString(ctx, route?.dest_airport, `${path}.route.dest_airport`, L.codeChars) },
293
+ date: date ?? '',
294
+ flight_no: flightNo ?? '',
295
+ marketing_carrier: takeOptionalString(ctx, rec.marketing_carrier, `${path}.marketing_carrier`, L.carrierChars),
296
+ operating_carrier: takeOptionalString(ctx, rec.operating_carrier, `${path}.operating_carrier`, L.carrierChars),
297
+ dep_local: depLocal,
298
+ arr_local: arrLocal,
299
+ nonstop: takeOptionalBoolean(ctx, rec.nonstop, `${path}.nonstop`),
300
+ price: takeOptionalNumber(ctx, rec.price, `${path}.price`, 0, L.priceMax),
301
+ currency: takeOptionalString(ctx, rec.currency, `${path}.currency`, L.currencyChars),
302
+ baggage_included: takeOptionalBoolean(ctx, rec.baggage_included, `${path}.baggage_included`),
303
+ protected_connection: takeOptionalBoolean(ctx, rec.protected_connection, `${path}.protected_connection`),
304
+ tier: tier ?? UNKNOWN_TIER,
305
+ bookability: bookability ?? 'unverified',
306
+ source: source ?? '',
307
+ query_id: queryId ?? '',
308
+ fetched_at: fetchedAt ?? '',
309
+ as_of: asOf ?? '',
310
+ review_by: reviewBy,
311
+ }
312
+ return { kind, fact }
313
+ }
314
+
315
+ function normalizeHotel(ctx: Ctx, rec: Record<string, unknown>, path: string): NormalizedHotel {
316
+ const L = ITINERARY_DOC_LIMITS
317
+ const fact: HotelFact = {
318
+ schema: 'gotry_bookable_fact.v1',
319
+ fact_id: takeString(ctx, rec.fact_id, `${path}.fact_id`, L.factIdChars) ?? '',
320
+ kind: 'hotel',
321
+ destination: takeString(ctx, rec.destination, `${path}.destination`, L.placeChars) ?? '',
322
+ check_in: rec.check_in === undefined || rec.check_in === null ? undefined : takeDate(ctx, rec.check_in, `${path}.check_in`),
323
+ check_out: rec.check_out === undefined || rec.check_out === null ? undefined : takeDate(ctx, rec.check_out, `${path}.check_out`),
324
+ verdict: takeEnum(ctx, rec.verdict, `${path}.verdict`, ['hit', 'miss'] as const) ?? 'miss',
325
+ bookability: takeEnum(ctx, rec.bookability, `${path}.bookability`, BOOKABILITIES) ?? 'unverified',
326
+ options_masked: takeRequiredNumber(ctx, rec.options_masked, `${path}.options_masked`, 0, L.optionsMax, true) ?? 0,
327
+ source: takeString(ctx, rec.source, `${path}.source`, L.sourceChars) ?? '',
328
+ query_id: takeString(ctx, rec.query_id, `${path}.query_id`, L.queryIdChars) ?? '',
329
+ fetched_at: takeInstant(ctx, rec.fetched_at, `${path}.fetched_at`) ?? '',
330
+ as_of: takeAsOf(ctx, rec.as_of, `${path}.as_of`) ?? '',
331
+ tier: takeEnum(ctx, rec.tier, `${path}.tier`, TIERS) ?? UNKNOWN_TIER,
332
+ }
333
+ return { kind: 'hotel', fact }
334
+ }
335
+
336
+ function normalizePolicy(ctx: Ctx, rec: Record<string, unknown>, path: string): NormalizedPolicy {
337
+ const L = ITINERARY_DOC_LIMITS
338
+ const fact: PolicyFact = {
339
+ schema: 'gotry_bookable_fact.v1',
340
+ fact_id: takeString(ctx, rec.fact_id, `${path}.fact_id`, L.factIdChars) ?? '',
341
+ kind: 'policy',
342
+ subject: takeString(ctx, rec.subject, `${path}.subject`, L.subjectChars) ?? '',
343
+ statement: takeString(ctx, rec.statement, `${path}.statement`, L.statementChars) ?? '',
344
+ source: takeString(ctx, rec.source, `${path}.source`, L.sourceChars) ?? '',
345
+ query_id: takeString(ctx, rec.query_id, `${path}.query_id`, L.queryIdChars) ?? '',
346
+ fetched_at: takeInstant(ctx, rec.fetched_at, `${path}.fetched_at`) ?? '',
347
+ as_of: takeAsOf(ctx, rec.as_of, `${path}.as_of`) ?? '',
348
+ review_by: takeAsOf(ctx, rec.review_by, `${path}.review_by`),
349
+ }
350
+ return { kind: 'policy', fact, runtime: takePolicyRuntime(ctx, rec.runtime, `${path}.runtime`) }
351
+ }
352
+
353
+ function normalizeFact(ctx: Ctx, value: unknown, index: number): NormalizedFact | undefined {
354
+ const path = `facts[${index}]`
355
+ if (!isRecord(value)) {
356
+ pushError(ctx, `${path} 必须是对象`)
357
+ return undefined
358
+ }
359
+ if (value.schema !== 'gotry_bookable_fact.v1') {
360
+ pushError(ctx, `${path}.schema 不是受支持的事实 schema(gotry_bookable_fact.v1)`)
361
+ return undefined
362
+ }
363
+ if (value.kind === 'flight' || value.kind === 'train') return normalizeFlight(ctx, value, value.kind, path)
364
+ if (value.kind === 'hotel') return normalizeHotel(ctx, value, path)
365
+ if (value.kind === 'policy') return normalizePolicy(ctx, value, path)
366
+ pushError(ctx, `${path}.kind 不是受支持的事实类型(允许:flight / train / hotel / policy)`)
367
+ return undefined
368
+ }
369
+
370
+ // ---------------------------------------------------------------------------
371
+ // 行程结构校验
372
+ // ---------------------------------------------------------------------------
373
+
374
+ export interface NormalizedStay { place: string; check_in: string; check_out: string }
375
+ export interface NormalizedSegment { from: string; to: string; date: string; mode: 'flight' | 'rail' | 'car'; legs: number }
376
+ export interface NormalizedItinerary {
377
+ trip_start: string
378
+ trip_end: string
379
+ stays: NormalizedStay[]
380
+ od_segments: NormalizedSegment[]
381
+ }
382
+
383
+ function normalizeStay(ctx: Ctx, value: unknown, index: number): NormalizedStay | undefined {
384
+ const path = `itinerary.stays[${index}]`
385
+ if (!isRecord(value)) {
386
+ pushError(ctx, `${path} 必须是对象`)
387
+ return undefined
388
+ }
389
+ const L = ITINERARY_DOC_LIMITS
390
+ const place = takeString(ctx, value.place, `${path}.place`, L.placeChars)
391
+ const checkIn = takeDate(ctx, value.check_in, `${path}.check_in`)
392
+ const checkOut = takeDate(ctx, value.check_out, `${path}.check_out`)
393
+ if (place === undefined || checkIn === undefined || checkOut === undefined) return undefined
394
+ if (checkOut <= checkIn) {
395
+ pushError(ctx, `${path} 退房日期不晚于入住日期(${checkIn}→${checkOut}):不做夜数推算,显式失败`)
396
+ return undefined
397
+ }
398
+ return { place, check_in: checkIn, check_out: checkOut }
399
+ }
400
+
401
+ function normalizeSegment(ctx: Ctx, value: unknown, index: number): NormalizedSegment | undefined {
402
+ const path = `itinerary.od_segments[${index}]`
403
+ if (!isRecord(value)) {
404
+ pushError(ctx, `${path} 必须是对象`)
405
+ return undefined
406
+ }
407
+ const L = ITINERARY_DOC_LIMITS
408
+ const from = takeString(ctx, value.from, `${path}.from`, L.fromToChars)
409
+ const to = takeString(ctx, value.to, `${path}.to`, L.fromToChars)
410
+ const date = takeDate(ctx, value.date, `${path}.date`)
411
+ const mode = takeEnum(ctx, value.mode, `${path}.mode`, MODES)
412
+ const legs = takeRequiredNumber(ctx, value.legs, `${path}.legs`, 1, L.legsMax, true)
413
+ if (from === undefined || to === undefined || date === undefined || mode === undefined || legs === undefined) return undefined
414
+ return { from, to, date, mode, legs }
415
+ }
416
+
417
+ function normalizeItinerary(ctx: Ctx, value: unknown): NormalizedItinerary | undefined {
418
+ if (!isRecord(value)) {
419
+ pushError(ctx, 'itinerary 必须是对象 {trip_start, trip_end, stays, od_segments}')
420
+ return undefined
421
+ }
422
+ const L = ITINERARY_DOC_LIMITS
423
+ const tripStart = takeDate(ctx, value.trip_start, 'itinerary.trip_start')
424
+ const tripEnd = takeDate(ctx, value.trip_end, 'itinerary.trip_end')
425
+ const staysRaw = takeArray(ctx, value.stays, 'itinerary.stays', L.stays)
426
+ const segsRaw = takeArray(ctx, value.od_segments, 'itinerary.od_segments', L.odSegments)
427
+ const stays: NormalizedStay[] = []
428
+ for (const [i, raw] of (staysRaw ?? []).entries()) {
429
+ const stay = normalizeStay(ctx, raw, i)
430
+ if (stay) stays.push(stay)
431
+ }
432
+ const odSegments: NormalizedSegment[] = []
433
+ for (const [i, raw] of (segsRaw ?? []).entries()) {
434
+ const segment = normalizeSegment(ctx, raw, i)
435
+ if (segment) odSegments.push(segment)
436
+ }
437
+ if (tripStart === undefined || tripEnd === undefined) return undefined
438
+ if (tripEnd < tripStart) {
439
+ pushError(ctx, `itinerary.trip_end 早于 trip_start(${tripStart}→${tripEnd}):不做日期改写,显式失败`)
440
+ return undefined
441
+ }
442
+ return { trip_start: tripStart, trip_end: tripEnd, stays, od_segments: odSegments }
443
+ }
444
+
445
+ /** 归一化入口:两个文档投影共用的同一套输入校验与拒绝语义(反漂移的单一事实源) */
446
+ export function normalizeDocInput(input: unknown): { ok: true; title: string; itinerary: NormalizedItinerary; facts: NormalizedFact[] } | { ok: false; errors: string[] } {
447
+ const ctx: Ctx = { errors: [], truncated: false }
448
+ if (!isRecord(input)) return { ok: false, errors: ['input 必须是对象 {title, itinerary, facts}'] }
449
+ const title = takeString(ctx, input.title, 'title', ITINERARY_DOC_LIMITS.titleChars)
450
+ const itinerary = normalizeItinerary(ctx, input.itinerary)
451
+ const factsRaw = takeArray(ctx, input.facts, 'facts', ITINERARY_DOC_LIMITS.facts)
452
+ const facts: NormalizedFact[] = []
453
+ for (const [i, raw] of (factsRaw ?? []).entries()) {
454
+ const fact = normalizeFact(ctx, raw, i)
455
+ if (fact) facts.push(fact)
456
+ }
457
+ if (title === undefined || itinerary === undefined || factsRaw === undefined || ctx.errors.length > 0) {
458
+ const errors = ctx.errors.length > 0 ? [...ctx.errors] : ['输入无法渲染']
459
+ if (ctx.truncated) errors.push(`错误条目已达上限 ${ITINERARY_DOC_LIMITS.errors} 条,其余未展示`)
460
+ return { ok: false, errors }
461
+ }
462
+ return { ok: true, title, itinerary, facts }
463
+ }
464
+
465
+ /** 输出大小硬上限:超限即整体失败,绝不静默截断核心行程 */
466
+ export function docBytesError(html: string): string[] | undefined {
467
+ if (html.length * BYTES_PER_CHAR_MAX > ITINERARY_DOC_LIMITS.docBytes || Buffer.byteLength(html, 'utf8') > ITINERARY_DOC_LIMITS.docBytes) {
468
+ return [`渲染文档超出大小上限 ${ITINERARY_DOC_LIMITS.docBytes} 字节:拒绝对核心行程做静默截断`]
469
+ }
470
+ return undefined
471
+ }
472
+
473
+ // ---------------------------------------------------------------------------
474
+ // 渲染原语(两个投影共用的转义、标签与证据卡)
475
+ // ---------------------------------------------------------------------------
476
+
477
+ export function escapeHtml(value: string): string {
478
+ let out = ''
479
+ for (const ch of value) {
480
+ const code = ch.codePointAt(0) ?? 0
481
+ // 控制字符(含 NUL/ESC)一律丢弃:既不进正文也不进属性
482
+ if (code < 0x20 || code === 0x7f) continue
483
+ switch (ch) {
484
+ case '&': out += '&amp;'; break
485
+ case '<': out += '&lt;'; break
486
+ case '>': out += '&gt;'; break
487
+ case '"': out += '&quot;'; break
488
+ case "'": out += '&#39;'; break
489
+ default: out += ch
490
+ }
491
+ }
492
+ return out
493
+ }
494
+
495
+ export const MODE_LABEL: Record<NormalizedSegment['mode'], string> = { flight: '航班', rail: '铁路', car: '地面' }
496
+
497
+ export const TIER_LABEL: Record<EvidenceTier, string> = {
498
+ live_inventory: 'exact-date 实时在架',
499
+ route_exists: '仅航线存在',
500
+ historical_schedule: '历史/旧航季班期',
501
+ benchmark_price: '历史促销价 benchmark',
502
+ }
503
+
504
+ export const BOOKABILITY_LABEL: Record<Bookability, string> = {
505
+ bookable_exact_date: '该档期可下单',
506
+ unavailable_exact_date: '该档期无结果',
507
+ unverified: '未核验',
508
+ conflict: '两源冲突',
509
+ }
510
+
511
+ /** 证据边界声明:单页文档与 deck 封面共用同一句话(双语文案单一事实源) */
512
+ export const EVIDENCE_BOUNDARY_NOTICE = '<div class="notice"><strong>证据边界:</strong>本文档只投影调用方给出的显式行程与已注册事实;不做夜数/预算/时间运算,不查询上游,不生成预订断言。缺失数据一律标注为缺失,不补默认值。</div>'
513
+
514
+ /** 双面声明:计划面与证据面分开陈述,无整体「已验证」结论 */
515
+ export const TWO_PLANE_STATEMENT = '<p class="muted">计划面(明示日期与段落)与证据面(已注册事实)分开陈述。除逐条事实自带的可下单性标注外,本文档不给出整体「已验证」结论。</p>'
516
+
517
+ export function fieldRow(label: string, value: string | undefined, missingNote = '未提供'): string {
518
+ return value === undefined
519
+ ? `<dt>${escapeHtml(label)}</dt><dd class="missing">${escapeHtml(missingNote)}</dd>`
520
+ : `<dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd>`
521
+ }
522
+
523
+ export function timeEl(date: string): string {
524
+ return `<time datetime="${escapeHtml(date)}">${escapeHtml(date)}</time>`
525
+ }
526
+
527
+ export function factIdSlug(id: string): string {
528
+ return id.replace(/[^0-9a-zA-Z_-]/g, '').slice(0, 64) || 'x'
529
+ }
530
+
531
+ export function factBadges(f: NormalizedFact): string {
532
+ if (f.kind === 'policy') return `<span class="badge warn">政策事实(非可下单)</span>`
533
+ const bookability = f.fact.bookability
534
+ const cls = bookability === 'bookable_exact_date' ? 'ok' : bookability === 'unverified' ? 'bad' : 'warn'
535
+ return `<span class="badge ${cls}">${escapeHtml(BOOKABILITY_LABEL[bookability] ?? bookability)}</span>`
536
+ + `<span class="badge">证据层:${escapeHtml(TIER_LABEL[f.fact.tier] ?? f.fact.tier)}</span>`
537
+ }
538
+
539
+ export function evidenceFields(f: NormalizedFact): string {
540
+ const fact = f.fact
541
+ const rows: string[] = [
542
+ fieldRow('事实 id', fact.fact_id),
543
+ fieldRow('来源 source', `[${fact.source}]`),
544
+ fieldRow('查询 id', fact.query_id),
545
+ fieldRow('取数时刻 fetched_at', fact.fetched_at),
546
+ fieldRow('数据快照日 as_of', fact.as_of),
547
+ ]
548
+ if (f.kind !== 'policy') {
549
+ rows.push(fieldRow('证据层 tier', `${f.fact.tier}(${TIER_LABEL[f.fact.tier] ?? '未知层'})`))
550
+ rows.push(fieldRow('可下单性 bookability', `${f.fact.bookability}(${BOOKABILITY_LABEL[f.fact.bookability] ?? '未知态'})`))
551
+ }
552
+ const reviewBy = isRecord(fact) && typeof fact.review_by === 'string' ? fact.review_by : undefined
553
+ if (reviewBy !== undefined) rows.push(fieldRow('复核截止 review_by', reviewBy))
554
+ return `<dl class="fields">${rows.join('')}</dl>`
555
+ }
556
+
557
+ function canonicalLine(f: NormalizedFact, tripStart: string): string {
558
+ if (f.kind === 'hotel') return renderHotelFact(f.fact)
559
+ // 政策事实复用既有权威:缺 review_by 时由 renderPolicyFact 按 tripStart 派生远期复核日
560
+ if (f.kind === 'policy') return renderPolicyFact(f.fact, tripStart)
561
+ return renderFlightFact(f.fact)
562
+ }
563
+
564
+ export function factTitle(f: NormalizedFact): string {
565
+ if (f.kind === 'hotel') return `酒店库存事实:${f.fact.destination} ${f.fact.check_in ?? '未提供'}→${f.fact.check_out ?? '未提供'}`
566
+ if (f.kind === 'policy') return `政策事实:${f.fact.subject}`
567
+ const route = `${f.fact.route.origin}→${f.fact.route.destination}`
568
+ const no = f.fact.flight_no ? ` ${f.fact.flight_no}` : ''
569
+ return `${f.kind === 'train' ? '车次' : '航班'}事实:${route}${no} ${f.fact.date}`
570
+ }
571
+
572
+ export function renderFact(f: NormalizedFact, id: string, tripStart: string): string {
573
+ let canonical = canonicalLine(f, tripStart)
574
+ if (f.kind === 'policy' && f.runtime.is_plan === true) {
575
+ canonical += '\n注:该政策条目被标注为「计划」意图,不是查询结论,不得当作现行政策执行。'
576
+ }
577
+ // 目的地级档期库存 ≠ 某家酒店的可售房型/报价:命中事实必须显式降级陈述
578
+ const destinationScopeNote = f.kind === 'hotel'
579
+ ? `<p class="muted">本条是<strong>目的地 + 档期级</strong>库存结论(「${escapeHtml(f.fact.destination)}」该档期有多少家在架),不指明也不代表任何具体酒店有房或可订;价格上游打码时保持原样。</p>`
580
+ : ''
581
+ return `<details class="fact" id="${escapeHtml(id)}"><summary>${escapeHtml(factTitle(f))}</summary>`
582
+ + `<div class="body"><div class="row">${factBadges(f)}</div>`
583
+ + evidenceFields(f)
584
+ + `<pre class="canonical">${escapeHtml(canonical)}</pre>`
585
+ + destinationScopeNote
586
+ + `</div></details>`
587
+ }
588
+
589
+ /**
590
+ * 计划住宿卡:只陈述排期意图与指向独立证据的指针。
591
+ * 酒店事实一律不在此卡内展开——避免「目的地级库存」被读成「这家住宿可订」。
592
+ */
593
+ export function renderPlannedStay(stay: NormalizedStay, id: string, related: NormalizedHotel[]): string {
594
+ const pointer = related.length === 0
595
+ ? `<p class="muted missing">该计划住宿没有对应的酒店档期事实:未核验,不得视为可订。</p>`
596
+ : `<p class="muted">该目的地 + 该档期另有 ${related.length} 条<strong>独立记录</strong>的酒店库存事实(见下方「独立记录:目的地级酒店事实」)。那是对「目的地 + 档期」的检索结论,不是本条目这家酒店的房型/报价,也不代表它可订。</p>`
597
+ + `<p class="muted">对应事实 id:${related.map(f => `<code>${escapeHtml(f.fact.fact_id)}</code>`).join('、')}</p>`
598
+ return `<details id="${escapeHtml(id)}" class="planned"><summary>计划住宿:${escapeHtml(stay.place)} ${escapeHtml(stay.check_in)}→${escapeHtml(stay.check_out)}</summary><div class="body">`
599
+ + `<div class="row"><span class="badge">计划(用户排期意图)</span><span class="badge warn">非预订、非可订声明</span></div>`
600
+ + `<dl class="fields">${fieldRow('住宿地 place', stay.place)}${fieldRow('入住 check_in', stay.check_in)}${fieldRow('退房 check_out', stay.check_out)}</dl>`
601
+ + pointer
602
+ + `</div></details>`
603
+ }
604
+
605
+ /** 计划住宿只与「同目的地且入住/退房日期完全一致」的酒店事实关联;其它档期的证据留在独立区 */
606
+ export function hotelFactsForStay(hotelFacts: NormalizedHotel[], stay: NormalizedStay): NormalizedHotel[] {
607
+ return hotelFacts.filter(f =>
608
+ f.fact.destination === stay.place
609
+ && f.fact.check_in === stay.check_in
610
+ && f.fact.check_out === stay.check_out)
611
+ }
612
+
613
+ export function renderSegment(seg: NormalizedSegment, id: string, related: NormalizedFact[], tripStart: string): string {
614
+ const evidence = related.length === 0
615
+ ? `<p class="muted missing">该段没有匹配的已注册事实:未核验,不得视为可下单方案。</p>`
616
+ : `<h4>该段可回溯的事实(独立于计划段)</h4>${related.map(f => renderFact(f, `${id}-fact-${factIdSlug(f.fact.fact_id)}`, tripStart)).join('')}`
617
+ return `<details id="${escapeHtml(id)}"><summary>${escapeHtml(MODE_LABEL[seg.mode])} ${escapeHtml(seg.from)} → ${escapeHtml(seg.to)} ${escapeHtml(seg.date)}</summary><div class="body">`
618
+ + `<div class="row"><span class="badge">计划(用户排期意图)</span><span class="badge warn">非可订声明</span></div>`
619
+ + `<dl class="fields">${fieldRow('出发 from', seg.from)}${fieldRow('到达 to', seg.to)}${fieldRow('出发日期 date', seg.date)}`
620
+ + `${fieldRow('运输方式 mode', `${seg.mode}(${MODE_LABEL[seg.mode]})`)}${fieldRow('航段/乘次 legs', String(seg.legs))}</dl>`
621
+ + evidence
622
+ + `</div></details>`
623
+ }
624
+
625
+ /** route+date 完全匹配的事实(负事实必须保留,否则用户看不到「该档期 0 条」) */
626
+ export function routeFactsFor(facts: NormalizedFact[], seg: NormalizedSegment): NormalizedFact[] {
627
+ return facts.filter(f => {
628
+ if (f.kind === 'policy' || f.kind === 'hotel') return false
629
+ return f.fact.date === seg.date && f.fact.route.origin === seg.from && f.fact.route.destination === seg.to
630
+ })
631
+ }
632
+
633
+ /** 独立证据区:目的地级酒店事实与任何计划住宿分卡、分标题陈述 */
634
+ export function renderHotelEvidenceSection(hotelFacts: NormalizedHotel[], tripStart: string): string {
635
+ if (hotelFacts.length === 0) return '<p class="muted missing">本次输入没有酒店事实。</p>'
636
+ return `<h3 id="hotel-evidence">独立记录:目的地级酒店事实</h3>`
637
+ + `<p class="muted">以下事实由上游按「目的地 + 档期」检索落账,与上方任何一条计划住宿都不是同一件事;它们不指明具体酒店,也不构成「某家酒店有房/可订/报价」的声明。</p>`
638
+ + hotelFacts.map(f => renderFact(f, `hotelfact-${factIdSlug(f.fact.fact_id)}`, tripStart)).join('')
639
+ }
640
+
641
+ // ---------------------------------------------------------------------------
642
+ // 组合原语:日期索引、未匹配事实与条件性注脚(两个投影同一语义)
643
+ // ---------------------------------------------------------------------------
644
+
645
+ export interface DateIndexItem { date: string; label: string; anchor: string }
646
+
647
+ /** 按日期排序的显式条目索引:住宿按入住日、交通段按出发日,不合成日历日 */
648
+ export function buildDateIndex(itinerary: NormalizedItinerary): DateIndexItem[] {
649
+ return [
650
+ ...itinerary.stays.map((s, i) => ({ date: s.check_in, label: `入住 ${s.place}`, anchor: `#stay-${i + 1}` })),
651
+ ...itinerary.od_segments.map((s, i) => ({ date: s.date, label: `${MODE_LABEL[s.mode]} ${s.from}→${s.to}`, anchor: `#seg-${i + 1}` })),
652
+ ].sort((a, b) => a.date.localeCompare(b.date) || a.label.localeCompare(b.label))
653
+ }
654
+
655
+ export function renderDateList(items: DateIndexItem[]): string {
656
+ return items.length === 0
657
+ ? `<p class="muted missing">没有显式日期条目:行程为空,不合成日历日。</p>`
658
+ : `<ul>${items.map(item => `<li>${timeEl(item.date)} — <a href="${escapeHtml(item.anchor)}">${escapeHtml(item.label)}</a></li>`).join('')}</ul>`
659
+ }
660
+
661
+ /** 未被任何计划条目挂接的事实(计划面用不到的独立证据,原样列出,不升级) */
662
+ export function leftoverFacts(facts: NormalizedFact[], itinerary: NormalizedItinerary): NormalizedFact[] {
663
+ const used = new Set<string>()
664
+ const hotelFacts = facts.filter((f): f is NormalizedHotel => f.kind === 'hotel')
665
+ for (const stay of itinerary.stays) {
666
+ for (const f of hotelFactsForStay(hotelFacts, stay)) used.add(f.fact.fact_id)
667
+ }
668
+ for (const seg of itinerary.od_segments) {
669
+ for (const f of routeFactsFor(facts, seg)) used.add(f.fact.fact_id)
670
+ }
671
+ return facts.filter(f => !used.has(f.fact.fact_id))
672
+ }
673
+
674
+ export function renderLeftoverBlock(leftovers: NormalizedFact[], tripStart: string): string {
675
+ return leftovers.length === 0
676
+ ? `<p class="muted">没有未被计划条目匹配的额外事实。</p>`
677
+ : `<h3>未被计划条目匹配的事实</h3><p class="muted">这些事实来自注册表,但计划里没有对应的显式段/住宿地。列出不代表计划采用,也不升级为可下单。</p>`
678
+ + leftovers.map(f => renderFact(f, `unmatched-${factIdSlug(f.fact.fact_id)}`, tripStart)).join('')
679
+ }
680
+
681
+ export function renderHotelNote(hotelFacts: NormalizedHotel[]): string {
682
+ return hotelFacts.length > 0
683
+ ? `<p class="muted">酒店的授权结论只落在下方「独立记录」区;来源只记录在架家数,价格上游打码。计划住宿卡片不承载任何酒店可订结论。</p>`
684
+ : ''
685
+ }
686
+
687
+ export function renderPolicyNote(policyFacts: NormalizedPolicy[]): string {
688
+ return policyFacts.length > 0
689
+ ? `<p class="muted">政策事实恒为「截至 as_of 的现行政策」表述,永不带无条件确认;远期出行须按 review_by 复核。${policyFacts.some(f => f.runtime.is_plan === true) ? '其中标注「计划」的条目只是意图,不是查询结论。' : ''}</p>`
690
+ : ''
691
+ }
692
+
693
+ /** 事实计数行:总数与三类分解,缺失面由计数如实呈现 */
694
+ export function renderFactCountsLine(facts: NormalizedFact[]): string {
695
+ const hotelCount = facts.filter(f => f.kind === 'hotel').length
696
+ const policyCount = facts.filter(f => f.kind === 'policy').length
697
+ const flightCount = facts.length - hotelCount - policyCount
698
+ return `<p class="muted">共 ${facts.length} 条事实(航班/车次 ${flightCount},酒店 ${hotelCount},政策 ${policyCount})。逐条保留来源、查询 id、取数时点与数据快照日;缺失项标注缺失。</p>`
699
+ }
700
+
701
+ // ---------------------------------------------------------------------------
702
+ // 组件样式(两个投影共用的视觉 tokens 与内容组件;排版壳由各形态自带)
703
+ // ---------------------------------------------------------------------------
704
+
705
+ export const DOC_BASE_CSS = `:root{color-scheme:light dark;--bg:#fff;--fg:#16181d;--muted:#5b6470;--line:#d8dee6;--card:#f7f9fc;--accent:#11487d;--warn:#8a4b00;--bad:#8a1f1f;--ok:#14532d}
706
+ *{box-sizing:border-box}
707
+ body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans CJK SC","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;word-break:break-word}
708
+ a{color:var(--accent)}
709
+ .skip{position:absolute;left:-9999px;top:0;background:var(--bg);padding:.5rem 1rem;border:1px solid var(--line)}
710
+ .skip:focus{left:.5rem;z-index:9}
711
+ h1{font-size:1.5rem;margin:.25rem 0}
712
+ h2{font-size:1.15rem;margin:0 0 .5rem}
713
+ h3{font-size:1rem;margin:.9rem 0 .4rem}
714
+ h4{font-size:.95rem;margin:.6rem 0 .3rem}
715
+ .muted{color:var(--muted);font-size:.9rem}
716
+ .notice{background:var(--card);border:1px solid var(--line);border-left:4px solid var(--warn);padding:.6rem .8rem;margin:.75rem 0;border-radius:4px}
717
+ details{border:1px solid var(--line);border-radius:6px;background:var(--card);margin:.5rem 0}
718
+ details[open]{background:var(--bg)}
719
+ summary{cursor:pointer;padding:.55rem .75rem;font-weight:600}
720
+ summary:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
721
+ .body{padding:0 .75rem .75rem;border-top:1px dashed var(--line)}
722
+ dl.fields{margin:.5rem 0 0;display:grid;grid-template-columns:auto 1fr;gap:.15rem .6rem}
723
+ dl.fields dt{color:var(--muted);font-size:.85rem}
724
+ dl.fields dd{margin:0;font-variant-numeric:tabular-nums}
725
+ .row{display:flex;flex-wrap:wrap;gap:.3rem .4rem;align-items:center;margin:.35rem 0}
726
+ .badge{display:inline-block;border:1px solid var(--line);border-radius:999px;padding:.05rem .5rem;font-size:.78rem;background:var(--bg)}
727
+ .badge.ok{border-color:var(--ok);color:var(--ok)}
728
+ .badge.warn{border-color:var(--warn);color:var(--warn)}
729
+ .badge.bad{border-color:var(--bad);color:var(--bad)}
730
+ pre.canonical{white-space:pre-wrap;word-break:break-word;background:var(--card);border:1px solid var(--line);border-radius:4px;padding:.5rem .6rem;margin:.5rem 0;overflow-x:auto}
731
+ .fact{border:1px solid var(--line);border-radius:6px;padding:.6rem .7rem;margin:.5rem 0}
732
+ .planned{border-left:4px solid var(--accent)}
733
+ .missing{color:var(--warn)}`
734
+
735
+ export const DOC_NARROW_CSS = `@media (max-width:480px){
736
+ body{font-size:15px}
737
+ dl.fields{grid-template-columns:1fr;gap:0}
738
+ dl.fields dt{margin-top:.35rem}
739
+ summary{padding:.6rem}
740
+ pre.canonical{font-size:.82rem}
741
+ }`