@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -219
- package/README.zh-CN.md +222 -0
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +1788 -0
- package/bin/gotry-inner.js +733 -83
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +49 -0
- package/bin/gotry.js +1 -1
- package/client/client.js +203 -0
- package/cordis.gotry-patch.yml +80 -56
- package/data/airline-airports.json +39 -0
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +101 -146
- package/dist/capabilities/anything.js +161 -94
- package/dist/capabilities/artifacts.js +370 -0
- package/dist/capabilities/channel-health.js +131 -0
- package/dist/capabilities/channel-registry.js +256 -0
- package/dist/capabilities/doctor.js +553 -0
- package/dist/capabilities/effect.js +394 -0
- package/dist/capabilities/external-event.js +149 -0
- package/dist/capabilities/fact-log.js +55 -0
- package/dist/capabilities/flyai-config.js +337 -0
- package/dist/capabilities/flyai.js +947 -110
- package/dist/capabilities/fx-contract.js +293 -0
- package/dist/capabilities/geo-atlas.js +246 -0
- package/dist/capabilities/ground-transfer.js +676 -0
- package/dist/capabilities/hbcli.js +260 -123
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/itinerary-artifact.js +190 -0
- package/dist/capabilities/itinerary-deck-artifact.js +194 -0
- package/dist/capabilities/itinerary-deck-export.js +339 -0
- package/dist/capabilities/lavish-local.js +924 -0
- package/dist/capabilities/model-override.js +38 -0
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/provider-detail-sanitize.js +64 -0
- package/dist/capabilities/resilience.js +191 -0
- package/dist/capabilities/route-provider-conformance.js +341 -0
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
- package/dist/capabilities/session/adapters/dida-portal.js +274 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +342 -0
- package/dist/capabilities/session/benchmark.js +189 -0
- package/dist/capabilities/session/bridge-ledger.js +224 -0
- package/dist/capabilities/session/extension-bridge.js +636 -0
- package/dist/capabilities/session/extension-channel.js +109 -0
- package/dist/capabilities/session/extension-distribution.js +230 -0
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +114 -0
- package/dist/capabilities/session/health-watch.js +215 -0
- package/dist/capabilities/session/read-guard.js +75 -24
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +97 -83
- package/dist/capabilities/session/wizard.js +117 -0
- package/dist/capabilities/session-consent.js +178 -0
- package/dist/capabilities/session-login.js +170 -0
- package/dist/capabilities/session-search.js +635 -44
- package/dist/capabilities/spawn-bounded.js +203 -0
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +140 -0
- package/dist/capabilities/weather.js +243 -138
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +103 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-error-tests.js +354 -0
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
- package/dist/scripts/anything-path-measure-tests.js +133 -0
- package/dist/scripts/anything-path-measure.js +150 -0
- package/dist/scripts/anything-tests.js +250 -60
- package/dist/scripts/artifact-client-contract-tests.js +220 -0
- package/dist/scripts/artifacts-capability-tests.js +727 -0
- package/dist/scripts/async-collect.js +50 -11
- package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
- package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
- package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
- package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
- package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
- package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +125 -0
- package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
- package/dist/scripts/bootstrap-tests.js +1549 -0
- package/dist/scripts/bridge-events-tests.js +239 -0
- package/dist/scripts/bridge-ledger-tests.js +428 -0
- package/dist/scripts/build-changelog.js +238 -0
- package/dist/scripts/build-metrics-report.js +331 -0
- package/dist/scripts/changelog-tests.js +118 -0
- package/dist/scripts/channel-probe-tests.js +103 -0
- package/dist/scripts/channel-probe.js +169 -0
- package/dist/scripts/channel-registry-tests.js +208 -0
- package/dist/scripts/city-scenario-tier-tests.js +234 -0
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-runner-stop-tests.js +311 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +600 -0
- package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
- package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
- package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
- package/dist/scripts/dsh-artifact-e2e.js +279 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-llm-tests.js +437 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
- package/dist/scripts/dsh-runtime-closure.js +162 -0
- package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +539 -0
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +174 -0
- package/dist/scripts/evaluation-contract-tests.js +268 -0
- package/dist/scripts/extension-distribution-cli.js +39 -0
- package/dist/scripts/extension-distribution-tests.js +443 -0
- package/dist/scripts/extension-tests.js +988 -0
- package/dist/scripts/external-event-tests.js +350 -0
- package/dist/scripts/fact-gate-tests.js +869 -0
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-setup-tests.js +228 -0
- package/dist/scripts/flyai-setup-tool-tests.js +54 -0
- package/dist/scripts/flyai-tests.js +692 -0
- package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
- package/dist/scripts/fx-contract-tests.js +201 -0
- package/dist/scripts/g5-guard-tests.js +150 -0
- package/dist/scripts/g5-guard.js +193 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/geo-atlas-tests.js +283 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
- package/dist/scripts/gotry-try-demo-tests.js +79 -0
- package/dist/scripts/gotry-try-demo.js +49 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +159 -0
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +121 -69
- package/dist/scripts/health-watch-cli.js +73 -0
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +41 -0
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +569 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
- package/dist/scripts/itinerary-artifact-tests.js +368 -0
- package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
- package/dist/scripts/itinerary-deck-export-tests.js +552 -0
- package/dist/scripts/itinerary-deck-tests.js +435 -0
- package/dist/scripts/itinerary-html-tests.js +426 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/lavish-local-tests.js +1226 -0
- package/dist/scripts/lavish-product-tools-tests.js +537 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +382 -246
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/m3-cohort-tests.js +714 -0
- package/dist/scripts/m3-cohort.js +310 -0
- package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
- package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
- package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
- package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
- package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +631 -0
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +151 -0
- package/dist/scripts/model-override-e2e.js +137 -0
- package/dist/scripts/nightly-evidence-tests.js +203 -0
- package/dist/scripts/nightly-evidence.js +303 -0
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/observability-tests.js +381 -0
- package/dist/scripts/onboarding-tests.js +161 -0
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/outcome-projection-tests.js +427 -0
- package/dist/scripts/persona-sim-tests.js +603 -0
- package/dist/scripts/persona-sim.js +1081 -0
- package/dist/scripts/persona-surface-guard-tests.js +49 -0
- package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +340 -0
- package/dist/scripts/price-drift-watch.js +447 -0
- package/dist/scripts/probe-poi-tests.js +53 -45
- package/dist/scripts/product-metrics.js +483 -0
- package/dist/scripts/publish-preverify.js +110 -0
- package/dist/scripts/realtime-pricing-tests.js +76 -0
- package/dist/scripts/recall-tests.js +513 -0
- package/dist/scripts/replay-async.js +22 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +54 -82
- package/dist/scripts/route-provider-conformance-tests.js +391 -0
- package/dist/scripts/session-attach-diagnose.js +20 -20
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +180 -0
- package/dist/scripts/session-dida-live-e2e.js +199 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +27 -46
- package/dist/scripts/session-tests.js +796 -96
- package/dist/scripts/session-zone-crash.js +49 -0
- package/dist/scripts/session-zone-ledger-tests.js +432 -0
- package/dist/scripts/session-zone-observation-tests.js +492 -0
- package/dist/scripts/session-zone-observe.js +135 -0
- package/dist/scripts/session-zone-wiring-tests.js +566 -0
- package/dist/scripts/session-zones-tests.js +374 -0
- package/dist/scripts/sf-live-benchmark.js +367 -0
- package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
- package/dist/scripts/sf-live-cli-tests.js +92 -0
- package/dist/scripts/sf-live-options.js +46 -0
- package/dist/scripts/sf-manifest-tests.js +30 -0
- package/dist/scripts/sf-manifest.js +33 -0
- package/dist/scripts/sf-soft-score-tests.js +67 -0
- package/dist/scripts/sf-summary-tests.js +324 -0
- package/dist/scripts/sf-summary.js +525 -0
- package/dist/scripts/share-tests.js +432 -0
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +28 -11
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +21 -0
- package/dist/scripts/smoke.js +690 -246
- package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +565 -198
- package/dist/scripts/state-repair-cli.js +66 -0
- package/dist/scripts/state-repair-tests.js +140 -0
- package/dist/scripts/static-golden-tests.js +165 -0
- package/dist/scripts/time-eval-tests.js +212 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +169 -0
- package/dist/scripts/turn-handoff-collect.js +161 -0
- package/dist/scripts/turn-policy-tests.js +55 -0
- package/dist/scripts/typed-contract-canary.js +176 -0
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +60 -20
- package/dist/scripts/visa-policy-tests.js +62 -0
- package/dist/scripts/weather-tests.js +276 -41
- package/dist/scripts/wish-channel-gate-tests.js +54 -0
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +46 -0
- package/dist/src/artifact-gate.js +829 -0
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +92 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +551 -0
- package/dist/src/benchmark-agent-conformance.js +578 -0
- package/dist/src/benchmark-environment-bridge.js +515 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
- package/dist/src/benchmark-tool-isolation.js +151 -0
- package/dist/src/bookable-facts.js +533 -0
- package/dist/src/booking-saga.js +97 -0
- package/dist/src/booking-surface/availability-policy.js +697 -0
- package/dist/src/booking-surface/booking-intent.js +43 -0
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/canonical-schema.js +116 -0
- package/dist/src/booking-surface/contracts.js +75 -0
- package/dist/src/booking-surface/dsh-planner.js +1854 -0
- package/dist/src/booking-surface/dsh-plugin.js +378 -0
- package/dist/src/booking-surface/dsh-worker.js +98 -0
- package/dist/src/booking-surface/error-codes.js +231 -0
- package/dist/src/booking-surface/index.js +9 -0
- package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
- package/dist/src/booking-surface/profile.js +24 -0
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +1968 -0
- package/dist/src/booking-surface/server.js +674 -0
- package/dist/src/booking-surface/startup.js +168 -0
- package/dist/src/booking-surface/validation.js +261 -0
- package/dist/src/bridge.js +20 -13
- package/dist/src/city-scenario-tier.js +378 -0
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +171 -115
- package/dist/src/engine.js +276 -193
- package/dist/src/evaluation-cadence.js +97 -0
- package/dist/src/evaluation-contracts.js +283 -0
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/flyai-setup-tool.js +70 -0
- package/dist/src/gotry-backend.js +125 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +88 -0
- package/dist/src/index.js +2385 -881
- package/dist/src/itinerary-deck.js +139 -0
- package/dist/src/itinerary-doc-shared.js +661 -0
- package/dist/src/itinerary-html.js +131 -0
- package/dist/src/journey.js +155 -108
- package/dist/src/lavish-tools.js +400 -0
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +331 -161
- package/dist/src/m3-cohort.js +1577 -0
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/outcome-projection.js +746 -0
- package/dist/src/realtime-pricing.js +103 -0
- package/dist/src/recall/card.js +74 -0
- package/dist/src/recall/evaluator.js +140 -0
- package/dist/src/recall/integration.js +17 -0
- package/dist/src/recall/tick.js +166 -0
- package/dist/src/session-link/plan-it-action.js +48 -0
- package/dist/src/session-link/session-link.js +248 -0
- package/dist/src/session-zone-ledger.js +312 -0
- package/dist/src/session-zone-observation.js +354 -0
- package/dist/src/session-zone-wiring.js +658 -0
- package/dist/src/session-zones.js +510 -0
- package/dist/src/share/adapters.js +51 -0
- package/dist/src/share/share-consent.js +76 -0
- package/dist/src/share/share-deck.js +58 -0
- package/dist/src/share/share-token.js +146 -0
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +696 -524
- package/dist/src/state-repair.js +299 -0
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +287 -0
- package/dist/src/turn-policy.js +118 -0
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +600 -360
- package/dist/src/wish-pool.js +42 -12
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +284 -0
- package/extension/README.md +21 -0
- package/extension/README.zh-CN.md +21 -0
- package/extension/background.js +463 -0
- package/extension/content-bridge.js +270 -0
- package/extension/content-main.js +111 -0
- package/extension/manifest.json +60 -0
- package/package.json +407 -14
- package/schemas/booking.intent.schema.json +193 -0
- package/schemas/booking.surface.schema.json +2622 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +198 -0
- package/ts/capabilities/anything.ts +282 -0
- package/ts/capabilities/artifacts.ts +428 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai-config.ts +388 -0
- package/ts/capabilities/flyai.ts +1274 -0
- package/ts/capabilities/hbcli.ts +253 -71
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/itinerary-artifact.ts +214 -0
- package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
- package/ts/capabilities/itinerary-deck-export.ts +378 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
- package/ts/capabilities/session/benchmark.ts +276 -0
- package/ts/capabilities/session/bridge-ledger.ts +333 -0
- package/ts/capabilities/session/extension-bridge.ts +794 -0
- package/ts/capabilities/session/extension-channel.ts +144 -0
- package/ts/capabilities/session/extension-distribution.ts +266 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +264 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/transport.ts +146 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-consent.ts +211 -0
- package/ts/capabilities/session-login.ts +208 -0
- package/ts/capabilities/session-search.ts +893 -0
- package/ts/capabilities/weather.ts +330 -0
- package/ts/dsh-runtime/vendor/README.md +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
- package/ts/package.json +293 -3
- package/ts/scripts/async-collect.ts +91 -0
- package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
- package/ts/scripts/skeleton-check.ts +1 -1
- package/ts/scripts/skeleton-integration-test.ts +23 -1
- package/ts/scripts/state-cli.ts +586 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +991 -0
- package/ts/src/benchmark-agent-conformance.ts +619 -0
- package/ts/src/benchmark-environment-bridge.ts +538 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +837 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/booking-surface/availability-policy.ts +522 -0
- package/ts/src/booking-surface/booking-intent.ts +86 -0
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/canonical-schema.js +116 -0
- package/ts/src/booking-surface/contracts.ts +475 -0
- package/ts/src/booking-surface/dsh-planner.ts +1949 -0
- package/ts/src/booking-surface/dsh-plugin.js +378 -0
- package/ts/src/booking-surface/dsh-worker.ts +110 -0
- package/ts/src/booking-surface/error-codes.ts +241 -0
- package/ts/src/booking-surface/index.ts +9 -0
- package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/runtime.ts +1793 -0
- package/ts/src/booking-surface/server.ts +733 -0
- package/ts/src/booking-surface/startup.ts +201 -0
- package/ts/src/booking-surface/validation.ts +217 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +17 -3
- package/ts/src/dsh-llm.ts +152 -17
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +2123 -200
- package/ts/src/itinerary-deck.ts +189 -0
- package/ts/src/itinerary-doc-shared.ts +741 -0
- package/ts/src/itinerary-html.ts +171 -0
- package/ts/src/loop.ts +234 -26
- package/ts/src/memory-capture.ts +283 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/recall/card.ts +106 -0
- package/ts/src/recall/evaluator.ts +170 -0
- package/ts/src/recall/integration.ts +35 -0
- package/ts/src/recall/tick.ts +213 -0
- package/ts/src/session-link/plan-it-action.ts +78 -0
- package/ts/src/session-link/session-link.ts +275 -0
- package/ts/src/share/adapters.ts +93 -0
- package/ts/src/share/share-consent.ts +105 -0
- package/ts/src/share/share-deck.ts +85 -0
- package/ts/src/share/share-token.ts +166 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +1166 -0
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +338 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/turn-deadline.ts +366 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/unified.ts +444 -105
- package/ts/src/wish-pool.ts +79 -0
- package/dist/scripts/session-attach-wait.js +0 -41
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 可下单事实模型 gotry_bookable_fact.v1(issue #46,ADR-19)。
|
|
3
|
+
*
|
|
4
|
+
* 问题(2026-08-29 实测会话):exact-date 工具全部 miss 后,LLM 用航线页当日班期
|
|
5
|
+
* (2026 快照)填充 2027 行程,航班号/时刻/机场/价格以「✓/推荐」确定性语气手写进
|
|
6
|
+
* 产物——「错误但看起来已验证」比 unknown 更危险。
|
|
7
|
+
*
|
|
8
|
+
* 本层职责(纯函数,零 I/O——持久化见 capabilities/fact-log.ts):
|
|
9
|
+
* 1. 结构化事实 schema:route/exact local date/时刻/航班号/营销与实际承运(分开)/
|
|
10
|
+
* 机场/直飞或中转/价格/币种/source/query_id/fetched_at/as_of/confidence/bookability;
|
|
11
|
+
* 2. 证据分层永不合并:live_inventory(exact-date 命中)/ route_exists(只验证航线)/
|
|
12
|
+
* historical_schedule(旧航季)/ benchmark_price(历史促销价)——miss 落「负事实」,
|
|
13
|
+
* exact-date 无库存 ≠ 可用历史填充;
|
|
14
|
+
* 3. 判定原语:航班 claim 可述性(fail closed)/ 同日衔接硬约束 / 行程统计不变量
|
|
15
|
+
* (酒店夜+机上夜/O&D 段数/flight legs/预算自洽)/ 回头路检测;
|
|
16
|
+
* 4. 确定性渲染:航班行/政策行只从结构化对象生成,政策恒带 as_of + 复核日期,
|
|
17
|
+
* 分票永不称「联程」(protected_connection=true 才允许)。
|
|
18
|
+
*
|
|
19
|
+
* 渲染闸(从 markdown 反向核验)在 artifact-gate.ts。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createHash } from 'node:crypto'
|
|
23
|
+
import { hasRecognizedAvailableSeat, validateTrainQueryResponseUrl, type SessionTrainOption } from '../capabilities/session/adapters/rail-12306.ts'
|
|
24
|
+
|
|
25
|
+
export const BOOKABLE_FACT_SCHEMA = 'gotry_bookable_fact.v1' as const
|
|
26
|
+
|
|
27
|
+
/** 证据分层:四层永不合并(issue #46 根因②——skeleton/historical/live 被混写) */
|
|
28
|
+
export type EvidenceTier =
|
|
29
|
+
| 'live_inventory' // exact-date 实时库存命中(唯一可下单层)
|
|
30
|
+
| 'route_exists' // 只验证了「航线存在」(骨架/航线页),不得升级为「该日期有该航班」
|
|
31
|
+
| 'historical_schedule' // 历史/旧航季班期,只作参考叙述
|
|
32
|
+
| 'benchmark_price' // 历史促销价,只作 benchmark,不进预算下限
|
|
33
|
+
|
|
34
|
+
/** 可下单状态:fail-closed 四态 */
|
|
35
|
+
export type Bookability =
|
|
36
|
+
| 'bookable_exact_date' // exact-date 源返回,可下单
|
|
37
|
+
| 'unavailable_exact_date' // exact-date 源正常返回 0 条(负事实)
|
|
38
|
+
| 'unverified' // 未经 exact-date 核验
|
|
39
|
+
| 'conflict' // 两源冲突,不得给 ✓
|
|
40
|
+
|
|
41
|
+
export interface FlightFact {
|
|
42
|
+
schema: typeof BOOKABLE_FACT_SCHEMA
|
|
43
|
+
/** 语义派生 id:sha(route|date|flight_no|query_id)——同查询重放幂等 */
|
|
44
|
+
fact_id: string
|
|
45
|
+
kind: 'flight' | 'train'
|
|
46
|
+
route: { origin: string; destination: string; origin_airport?: string; dest_airport?: string }
|
|
47
|
+
/** 出发地当地日期 YYYY-MM-DD(exact local date) */
|
|
48
|
+
date: string
|
|
49
|
+
/** 营销航班号(如 CZ8582);负事实为空串 */
|
|
50
|
+
flight_no: string
|
|
51
|
+
/** 营销承运与实际承运分开保存(codeshare 必须两者同输出) */
|
|
52
|
+
marketing_carrier?: string
|
|
53
|
+
operating_carrier?: string
|
|
54
|
+
dep_local?: string // HH:MM
|
|
55
|
+
arr_local?: string // HH:MM
|
|
56
|
+
nonstop?: boolean
|
|
57
|
+
price?: number
|
|
58
|
+
currency?: string
|
|
59
|
+
baggage_included?: boolean
|
|
60
|
+
/** 仅当同票联程(同一票号/行李直挂/误机保护)为 true;两段接得上 ≠ 联程 */
|
|
61
|
+
protected_connection?: boolean
|
|
62
|
+
tier: EvidenceTier
|
|
63
|
+
bookability: Bookability
|
|
64
|
+
/** 数据源标识(flyai / session:ctrip-flight / …) */
|
|
65
|
+
source: string
|
|
66
|
+
/** 可重放查询 id(如 flyai:flight:HKG-HKT:2027-07-17) */
|
|
67
|
+
query_id: string
|
|
68
|
+
/** 取数时刻 ISO */
|
|
69
|
+
fetched_at: string
|
|
70
|
+
/** 数据快照日(证据 as_of) */
|
|
71
|
+
as_of: string
|
|
72
|
+
/** 下次复核截止(远期班期/政策必带) */
|
|
73
|
+
review_by?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 动态政策事实:签证/免签/入境申报只允许「截至 as_of 的现行政策」表述 */
|
|
77
|
+
export interface PolicyFact {
|
|
78
|
+
schema: typeof BOOKABLE_FACT_SCHEMA
|
|
79
|
+
fact_id: string
|
|
80
|
+
kind: 'policy'
|
|
81
|
+
/** 政策对象(如 泰国免签 / 迪拜入境) */
|
|
82
|
+
subject: string
|
|
83
|
+
/** 政策内容陈述(不带时间无条件断言) */
|
|
84
|
+
statement: string
|
|
85
|
+
source: string
|
|
86
|
+
query_id: string
|
|
87
|
+
fetched_at: string
|
|
88
|
+
as_of: string
|
|
89
|
+
/** 复核 gate(通常 D-30/D-7) */
|
|
90
|
+
review_by?: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type BookableFact = FlightFact | PolicyFact | HotelFact
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 酒店事实(D-26,issue #118):exact-date 酒店检索(hit/miss)的唯一事实形态。
|
|
97
|
+
* 纪律与机/火同源:①未定档期的摸底检索不落账(无 exact-date 语义);
|
|
98
|
+
* ②needs-setup/error 是传输失败,永不落负事实;③价格上游打码(¥7xx)——
|
|
99
|
+
* 本事实**不落数字价**,只记在架家数,真实价以落地页为准(打码价保真纪律)。
|
|
100
|
+
*/
|
|
101
|
+
export interface HotelFact {
|
|
102
|
+
schema: typeof BOOKABLE_FACT_SCHEMA
|
|
103
|
+
/** 语义派生 id:sha(destination|checkIn|checkOut|query_id)——同查询重放幂等 */
|
|
104
|
+
fact_id: string
|
|
105
|
+
kind: 'hotel'
|
|
106
|
+
/** 目的地中文(flyai/session 的 to;闸侧 claim 匹配键) */
|
|
107
|
+
destination: string
|
|
108
|
+
check_in?: string // YYYY-MM-DD
|
|
109
|
+
check_out?: string // YYYY-MM-DD
|
|
110
|
+
verdict: 'hit' | 'miss'
|
|
111
|
+
bookability: Bookability
|
|
112
|
+
/** 在架条数(hit 时;价格打码不落数字价) */
|
|
113
|
+
options_masked: number
|
|
114
|
+
source: string // flyai-hotel / session:ctrip-hotel
|
|
115
|
+
/** 可重放查询 id(如 flyai:hotel:大理:2026-10-01_2026-10-03) */
|
|
116
|
+
query_id: string
|
|
117
|
+
fetched_at: string
|
|
118
|
+
as_of: string
|
|
119
|
+
tier: EvidenceTier
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// id 与查询词(可重放:query_id 即重放凭证)
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
function sha16(text: string): string {
|
|
127
|
+
return createHash('sha256').update(text).digest('hex').slice(0, 16)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function makeQueryId(source: string, kind: string, origin: string, destination: string, date: string): string {
|
|
131
|
+
return `${source}:${kind}:${origin}-${destination}:${date}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function makeFactId(parts: Array<string | undefined>): string {
|
|
135
|
+
return sha16(parts.map(p => p ?? '').join('|'))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 城市→IATA 词表(data/airline-airports.json city_alias);转换时把 route 归一到 IATA 空间 */
|
|
139
|
+
export type CityAlias = Record<string, string>
|
|
140
|
+
|
|
141
|
+
function iataOf(alias: CityAlias | undefined, city: string): string {
|
|
142
|
+
return alias?.[city] ?? city
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// 工具结果 → 事实(转换器):hit 落正事实,miss 落负事实,error 不落(无结论)
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
/** flyai 机/火结果形状(capabilities/flyai.ts FlyaiOption 的输入侧最小面) */
|
|
150
|
+
export interface FlyaiLikeOption {
|
|
151
|
+
no?: string
|
|
152
|
+
name?: string
|
|
153
|
+
depDateTime?: string
|
|
154
|
+
arrDateTime?: string
|
|
155
|
+
depStation?: string
|
|
156
|
+
arrStation?: string
|
|
157
|
+
price?: number
|
|
158
|
+
priceRaw?: string
|
|
159
|
+
/** 直达/中转;缺省视为直达(单段条目),中转条目必须显式 false */
|
|
160
|
+
nonstop?: boolean
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface FlyaiLikeResult {
|
|
164
|
+
/** needs-setup/auth-error/forbidden/rate-limited/timeout 同 error:无结论不是证据,不落事实 */
|
|
165
|
+
verdict: 'hit' | 'miss' | 'error' | 'needs-setup' | 'auth-error' | 'forbidden' | 'rate-limited' | 'timeout' | 'cancelled'
|
|
166
|
+
options?: FlyaiLikeOption[]
|
|
167
|
+
evidence?: string
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 会话检索(携程)结果形状(session-search.ts 最小面) */
|
|
171
|
+
export interface SessionLikeOption {
|
|
172
|
+
flightNo?: string
|
|
173
|
+
airline?: string
|
|
174
|
+
depDateTime?: string
|
|
175
|
+
arrDateTime?: string
|
|
176
|
+
depAirport?: string
|
|
177
|
+
arrAirport?: string
|
|
178
|
+
price?: number
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface SessionLikeResult {
|
|
182
|
+
verdict: string
|
|
183
|
+
options?: SessionLikeOption[]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 12306 exact-date fact freshness rule: capture must be non-future and <=15 minutes old. */
|
|
187
|
+
export const TRAIN_FACT_FRESHNESS_RULE = 'captured_at_non_future_and_max_age_15m' as const
|
|
188
|
+
export const TRAIN_FACT_MAX_AGE_MS = 15 * 60 * 1000
|
|
189
|
+
|
|
190
|
+
export interface SessionTrainFactResult {
|
|
191
|
+
outcome: {
|
|
192
|
+
kind: 'recognized-nonempty' | 'recognized-empty' | 'malformed' | 'transport-runtime-error'
|
|
193
|
+
trains?: SessionTrainOption[]
|
|
194
|
+
}
|
|
195
|
+
collection?: {
|
|
196
|
+
requested: { from: string; to: string; date: string }
|
|
197
|
+
request: { fromStationTelecode: string; toStationTelecode: string; date: string }
|
|
198
|
+
response: { url: string; fromStationTelecode: string; toStationTelecode: string; date: string }
|
|
199
|
+
batchId: string
|
|
200
|
+
queryId: string
|
|
201
|
+
fetchedAt: string
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const YMD = /^\d{4}-\d{2}-\d{2}$/
|
|
206
|
+
|
|
207
|
+
function hmOf(dateTime: string | undefined): string | undefined {
|
|
208
|
+
const m = dateTime?.match(/T?(\d{2}:\d{2})/)
|
|
209
|
+
return m?.[1]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function dateOf(dateTime: string | undefined, fallback: string): string {
|
|
213
|
+
const m = dateTime?.match(/^(\d{4}-\d{2}-\d{2})/)
|
|
214
|
+
return m?.[1] ?? fallback
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** exact-date miss → 负事实(route+date 级「当前不可售/无结果」记录,fail-closed 的另一半) */
|
|
218
|
+
export function negativeFact(queryId: string, kind: 'flight' | 'train', origin: string, destination: string, date: string, source: string, fetchedAt: string, alias?: CityAlias): FlightFact {
|
|
219
|
+
return {
|
|
220
|
+
schema: BOOKABLE_FACT_SCHEMA,
|
|
221
|
+
fact_id: makeFactId(['neg', kind, iataOf(alias, origin), iataOf(alias, destination), date, queryId]),
|
|
222
|
+
kind,
|
|
223
|
+
route: { origin: iataOf(alias, origin), destination: iataOf(alias, destination) },
|
|
224
|
+
date,
|
|
225
|
+
flight_no: '',
|
|
226
|
+
tier: 'live_inventory',
|
|
227
|
+
bookability: 'unavailable_exact_date',
|
|
228
|
+
source,
|
|
229
|
+
query_id: queryId,
|
|
230
|
+
fetched_at: fetchedAt,
|
|
231
|
+
as_of: fetchedAt.slice(0, 10),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** flyai hit/miss → 事实列(error 不落事实——无结论不是证据) */
|
|
236
|
+
export function factsFromFlyai(q: { kind: 'flight' | 'train'; origin: string; destination: string; date: string }, r: FlyaiLikeResult, fetchedAt: string, alias?: CityAlias): FlightFact[] {
|
|
237
|
+
const source = 'flyai'
|
|
238
|
+
const queryId = makeQueryId(source, q.kind, q.origin, q.destination, q.date)
|
|
239
|
+
const origin = iataOf(alias, q.origin)
|
|
240
|
+
const destination = iataOf(alias, q.destination)
|
|
241
|
+
if (r.verdict !== 'hit' && r.verdict !== 'miss') return []
|
|
242
|
+
if (r.verdict === 'miss') return [negativeFact(queryId, q.kind, q.origin, q.destination, q.date, source, fetchedAt, alias)]
|
|
243
|
+
const facts: FlightFact[] = []
|
|
244
|
+
for (const o of r.options ?? []) {
|
|
245
|
+
if (!o.no || !o.depDateTime) continue
|
|
246
|
+
const numeric = typeof o.price === 'number' && Number.isFinite(o.price) && o.price > 0 ? o.price : undefined
|
|
247
|
+
facts.push({
|
|
248
|
+
schema: BOOKABLE_FACT_SCHEMA,
|
|
249
|
+
fact_id: makeFactId([q.kind, origin, destination, q.date, o.no, queryId]),
|
|
250
|
+
kind: q.kind,
|
|
251
|
+
route: { origin, destination, origin_airport: o.depStation, dest_airport: o.arrStation },
|
|
252
|
+
date: dateOf(o.depDateTime, q.date),
|
|
253
|
+
flight_no: o.no,
|
|
254
|
+
marketing_carrier: o.name || undefined,
|
|
255
|
+
operating_carrier: undefined, // flyai 上游只给营销承运;实际承运未知 ≠ 相同,留空不猜
|
|
256
|
+
dep_local: hmOf(o.depDateTime),
|
|
257
|
+
arr_local: hmOf(o.arrDateTime),
|
|
258
|
+
nonstop: o.nonstop !== false,
|
|
259
|
+
price: numeric,
|
|
260
|
+
currency: numeric !== undefined ? 'CNY' : undefined,
|
|
261
|
+
tier: 'live_inventory',
|
|
262
|
+
bookability: 'bookable_exact_date',
|
|
263
|
+
source,
|
|
264
|
+
query_id: queryId,
|
|
265
|
+
fetched_at: fetchedAt,
|
|
266
|
+
as_of: fetchedAt.slice(0, 10),
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
// hit 但解析后零有效条目 = 与 miss 同语义(无可用事实,落负事实)
|
|
270
|
+
return facts.length > 0 ? facts : [negativeFact(queryId, q.kind, q.origin, q.destination, q.date, source, fetchedAt, alias)]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 会话检索 hit → 事实列(用户本人登录态来源,与 flyai 并列独立 source) */
|
|
274
|
+
export function factsFromSession(q: { origin: string; destination: string; date: string }, r: SessionLikeResult, fetchedAt: string, alias?: CityAlias): FlightFact[] {
|
|
275
|
+
const source = 'session:ctrip-flight'
|
|
276
|
+
const queryId = makeQueryId(source, 'flight', q.origin, q.destination, q.date)
|
|
277
|
+
const origin = iataOf(alias, q.origin)
|
|
278
|
+
const destination = iataOf(alias, q.destination)
|
|
279
|
+
if (r.verdict !== 'hit') {
|
|
280
|
+
// miss/cooldown/challenged/needs-login:仅「正常返回 0 条」是负事实;通道不可用不落事实
|
|
281
|
+
return r.verdict === 'miss' ? [negativeFact(queryId, 'flight', q.origin, q.destination, q.date, source, fetchedAt, alias)] : []
|
|
282
|
+
}
|
|
283
|
+
const facts: FlightFact[] = []
|
|
284
|
+
for (const o of r.options ?? []) {
|
|
285
|
+
if (!o.flightNo || !o.depDateTime) continue
|
|
286
|
+
const numeric = typeof o.price === 'number' && Number.isFinite(o.price) && o.price > 0 ? o.price : undefined
|
|
287
|
+
facts.push({
|
|
288
|
+
schema: BOOKABLE_FACT_SCHEMA,
|
|
289
|
+
fact_id: makeFactId(['flight', origin, destination, q.date, o.flightNo, queryId]),
|
|
290
|
+
kind: 'flight',
|
|
291
|
+
route: { origin, destination, origin_airport: o.depAirport, dest_airport: o.arrAirport },
|
|
292
|
+
date: dateOf(o.depDateTime, q.date),
|
|
293
|
+
flight_no: o.flightNo,
|
|
294
|
+
marketing_carrier: o.airline || undefined,
|
|
295
|
+
operating_carrier: undefined,
|
|
296
|
+
dep_local: hmOf(o.depDateTime),
|
|
297
|
+
arr_local: hmOf(o.arrDateTime),
|
|
298
|
+
nonstop: true,
|
|
299
|
+
price: numeric,
|
|
300
|
+
currency: numeric !== undefined ? 'CNY' : undefined,
|
|
301
|
+
tier: 'live_inventory',
|
|
302
|
+
bookability: 'bookable_exact_date',
|
|
303
|
+
source,
|
|
304
|
+
query_id: queryId,
|
|
305
|
+
fetched_at: fetchedAt,
|
|
306
|
+
as_of: fetchedAt.slice(0, 10),
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
return facts
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function trainCollectionIsFresh(
|
|
313
|
+
q: { from: string; to: string; date: string },
|
|
314
|
+
r: SessionTrainFactResult,
|
|
315
|
+
now: Date,
|
|
316
|
+
): r is SessionTrainFactResult & { collection: NonNullable<SessionTrainFactResult['collection']> } {
|
|
317
|
+
const c = r.collection
|
|
318
|
+
if (!c || !c.batchId.trim() || !c.queryId.trim() || c.queryId !== `session:12306-train:${c.batchId}`) return false
|
|
319
|
+
if (c.requested.from !== q.from || c.requested.to !== q.to || c.requested.date !== q.date) return false
|
|
320
|
+
if (!c.request || !c.response
|
|
321
|
+
|| c.request.date !== c.requested.date
|
|
322
|
+
|| c.response.fromStationTelecode !== c.request.fromStationTelecode
|
|
323
|
+
|| c.response.toStationTelecode !== c.request.toStationTelecode
|
|
324
|
+
|| c.response.date !== c.request.date
|
|
325
|
+
|| !validateTrainQueryResponseUrl(c.response.url, c.request).ok) return false
|
|
326
|
+
const fetchedMs = Date.parse(c.fetchedAt)
|
|
327
|
+
const nowMs = now.getTime()
|
|
328
|
+
if (!Number.isFinite(fetchedMs) || !Number.isFinite(nowMs)) return false
|
|
329
|
+
return fetchedMs <= nowMs && nowMs - fetchedMs <= TRAIN_FACT_MAX_AGE_MS
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 12306 typed outcome → exact-date train facts.
|
|
333
|
+
* The converter never invents collection identity or timestamps: both must come
|
|
334
|
+
* from the session invocation binding. Only recognized-empty can create a
|
|
335
|
+
* negative fact. A recognized row needs Y plus a closed-set available seat token.
|
|
336
|
+
*/
|
|
337
|
+
export function factsFromSessionTrain(
|
|
338
|
+
q: { from: string; to: string; date: string },
|
|
339
|
+
r: SessionTrainFactResult,
|
|
340
|
+
now: Date = new Date(),
|
|
341
|
+
): FlightFact[] {
|
|
342
|
+
if (!trainCollectionIsFresh(q, r, now)) return []
|
|
343
|
+
const c = r.collection
|
|
344
|
+
const source = 'session:12306-train'
|
|
345
|
+
const fetchedAt = c.fetchedAt
|
|
346
|
+
if (r.outcome.kind === 'recognized-empty') {
|
|
347
|
+
return [negativeFact(c.queryId, 'train', q.from, q.to, q.date, source, fetchedAt)]
|
|
348
|
+
}
|
|
349
|
+
if (r.outcome.kind !== 'recognized-nonempty') return []
|
|
350
|
+
const facts: FlightFact[] = []
|
|
351
|
+
for (const o of r.outcome.trains ?? []) {
|
|
352
|
+
if (o.canWebBuy !== 'Y' || !hasRecognizedAvailableSeat(o)) continue
|
|
353
|
+
facts.push({
|
|
354
|
+
schema: BOOKABLE_FACT_SCHEMA,
|
|
355
|
+
fact_id: makeFactId(['train', q.from, q.to, q.date, o.trainCode, c.queryId]),
|
|
356
|
+
kind: 'train',
|
|
357
|
+
route: { origin: q.from, destination: q.to },
|
|
358
|
+
date: q.date,
|
|
359
|
+
flight_no: o.trainCode,
|
|
360
|
+
dep_local: o.depTime,
|
|
361
|
+
arr_local: o.arrTime,
|
|
362
|
+
tier: 'live_inventory',
|
|
363
|
+
bookability: 'bookable_exact_date',
|
|
364
|
+
source,
|
|
365
|
+
query_id: c.queryId,
|
|
366
|
+
fetched_at: fetchedAt,
|
|
367
|
+
as_of: fetchedAt.slice(0, 10),
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
return facts
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// 注册表(纯函数视图;持久化在 capabilities/fact-log.ts)
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
/** 同 fact_id 去重,保留 fetched_at 最新(重查覆盖旧快照,历史不丢——append-only 由持久层保证) */
|
|
378
|
+
export function dedupeFacts<T extends BookableFact>(facts: T[]): T[] {
|
|
379
|
+
const byId = new Map<string, T>()
|
|
380
|
+
for (const f of facts) {
|
|
381
|
+
const prev = byId.get(f.fact_id)
|
|
382
|
+
if (!prev || prev.fetched_at <= f.fetched_at) byId.set(f.fact_id, f)
|
|
383
|
+
}
|
|
384
|
+
return [...byId.values()].sort((a, b) => a.fetched_at.localeCompare(b.fetched_at) || a.fact_id.localeCompare(b.fact_id))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** route+date 的最新查询结论(同日多次查询:最新一次为准,避免「昨天 miss 今天 hit」陈旧否定) */
|
|
388
|
+
export function latestFactsForRouteDate(
|
|
389
|
+
facts: BookableFact[], origin: string, destination: string, date: string,
|
|
390
|
+
kind?: 'flight' | 'train',
|
|
391
|
+
): FlightFact[] {
|
|
392
|
+
const hits = facts.filter((f): f is FlightFact =>
|
|
393
|
+
(f.kind === 'flight' || f.kind === 'train')
|
|
394
|
+
&& (!kind || f.kind === kind)
|
|
395
|
+
&& f.route.origin === origin && f.route.destination === destination && f.date === date)
|
|
396
|
+
const latestQueryTs = new Map<string, string>()
|
|
397
|
+
for (const f of hits) {
|
|
398
|
+
const prev = latestQueryTs.get(f.query_id)
|
|
399
|
+
if (!prev || prev < f.fetched_at) latestQueryTs.set(f.query_id, f.fetched_at)
|
|
400
|
+
}
|
|
401
|
+
const latestTs = [...latestQueryTs.values()].sort().pop()
|
|
402
|
+
if (!latestTs) return []
|
|
403
|
+
return hits.filter(f => f.fetched_at === latestTs)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// 判定原语①:航班 claim 可述性(fail closed,flight-only)
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
export type FlightClaimVerdict =
|
|
411
|
+
| 'traceable' // 注册表内有 bookable_exact_date 的该航班事实
|
|
412
|
+
| 'not_in_source' // 该 route+date 查过 exact-date,结果里没有它——最强违例(填充历史/相邻日期)
|
|
413
|
+
| 'route_unqueried' // 该 route+date 从未查过 exact-date——无证据断言
|
|
414
|
+
| 'contradicted' // 注册表内有同号航班但事实字段冲突(时刻/机场)
|
|
415
|
+
|
|
416
|
+
export interface FlightClaim {
|
|
417
|
+
flight_no: string
|
|
418
|
+
origin?: string
|
|
419
|
+
destination?: string
|
|
420
|
+
date?: string
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* 航班 claim 裁决(flight-only):注册表只接受 `kind:'flight'` 的同号事实
|
|
425
|
+
* ——train 事实走 `railClaimVerdict` 独立裁决,不得混入航班 not_in_source
|
|
426
|
+
* 路径(issue #299)。中文高铁/动车/城际/直达代码(G/D/C/Z + 3–4 位)在
|
|
427
|
+
* 闸侧 `extractClaims` 已分流到 `trains` claim,本函数无需再做扩展。
|
|
428
|
+
*/
|
|
429
|
+
export function flightClaimVerdict(facts: BookableFact[], claim: FlightClaim): { verdict: FlightClaimVerdict; fact?: FlightFact; reason: string } {
|
|
430
|
+
const no = claim.flight_no.toUpperCase().replace(/\s+/g, '')
|
|
431
|
+
const sameNo = facts.filter((f): f is FlightFact =>
|
|
432
|
+
f.kind === 'flight' && f.flight_no.toUpperCase() === no && f.bookability === 'bookable_exact_date')
|
|
433
|
+
const routeMatched = sameNo.filter(f =>
|
|
434
|
+
(!claim.origin || f.route.origin === claim.origin || f.route.origin_airport === claim.origin)
|
|
435
|
+
&& (!claim.destination || f.route.destination === claim.destination || f.route.dest_airport === claim.destination)
|
|
436
|
+
&& (!claim.date || f.date === claim.date))
|
|
437
|
+
if (routeMatched.length > 0) return { verdict: 'traceable', fact: routeMatched[0], reason: `回溯 ${routeMatched[0]!.query_id}` }
|
|
438
|
+
// 无同号可下单事实:该 route+date 是否查过?
|
|
439
|
+
if (claim.origin && claim.destination && claim.date) {
|
|
440
|
+
const scoped = latestFactsForRouteDate(facts, claim.origin, claim.destination, claim.date, 'flight')
|
|
441
|
+
if (scoped.length > 0) {
|
|
442
|
+
return {
|
|
443
|
+
verdict: 'not_in_source',
|
|
444
|
+
reason: scoped.some(f => f.bookability === 'unavailable_exact_date')
|
|
445
|
+
? `${claim.origin}→${claim.destination} ${claim.date} exact-date 源返回 0 条(${scoped[0]!.query_id})——不得用历史班期/相邻日期填充`
|
|
446
|
+
: `${claim.origin}→${claim.destination} ${claim.date} exact-date 源在架 ${scoped.filter(f => f.bookability === 'bookable_exact_date').map(f => f.flight_no).join('/')}——无 ${no}`,
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return { verdict: 'route_unqueried', reason: `${claim.origin}→${claim.destination} ${claim.date} 从未经 exact-date 源核验` }
|
|
450
|
+
}
|
|
451
|
+
return { verdict: 'route_unqueried', reason: `航班 ${no} 无可回溯的 exact-date 事实(缺 route/date 上下文)` }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// 判定原语①b:车次 claim 可述性(rail-only,fail closed;issue #299)
|
|
456
|
+
// 闸侧 `extractClaims` 已把 G/D/C/Z + 3–4 位 token 收集为 train claim;
|
|
457
|
+
// 本裁决只在 train 事实里查同号 bookable_exact_date,缺事实/缺上下文/
|
|
458
|
+
// 路线未查均 fail-closed。支持的事实生产端是 `factsFromFlyai kind:'train'`
|
|
459
|
+
// 与 `factsFromSessionTrain`,两者均经真实工具路径追加到事实日志; session
|
|
460
|
+
// 结果必须先通过 typed parser outcome + seat availability/freshness contract。
|
|
461
|
+
// 不得用历史班期或 static-schedule 凑合格回溯。
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
export type RailClaimVerdict =
|
|
465
|
+
| 'traceable' // 注册表内有 bookable_exact_date 的该 train 事实(同号+同 route+同 date)
|
|
466
|
+
| 'not_in_source' // 该 route+date 查过 exact-date,结果里没有它——最强违例
|
|
467
|
+
| 'route_unqueried' // 该 route+date 从未查过 exact-date——无证据断言
|
|
468
|
+
| 'rail_claim_unverified' // 缺 route/date 上下文,或缺同号事实且 route/date 无 exact-date 证据
|
|
469
|
+
|
|
470
|
+
export interface RailClaim {
|
|
471
|
+
flight_no: string
|
|
472
|
+
origin?: string
|
|
473
|
+
destination?: string
|
|
474
|
+
date?: string
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const TRAIN_FACT_SOURCES = new Set(['flyai', 'session:12306-train'])
|
|
478
|
+
|
|
479
|
+
function isStructuredTrainFact(f: BookableFact): f is FlightFact {
|
|
480
|
+
return f.kind === 'train'
|
|
481
|
+
&& f.schema === BOOKABLE_FACT_SCHEMA
|
|
482
|
+
&& TRAIN_FACT_SOURCES.has(f.source)
|
|
483
|
+
&& f.query_id.trim().length > 0
|
|
484
|
+
&& f.tier === 'live_inventory'
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function railClaimVerdict(facts: BookableFact[], claim: RailClaim): { verdict: RailClaimVerdict; fact?: FlightFact; reason: string } {
|
|
488
|
+
const no = claim.flight_no.toUpperCase().replace(/\s+/g, '')
|
|
489
|
+
if (!claim.origin || !claim.destination || !claim.date) {
|
|
490
|
+
return { verdict: 'rail_claim_unverified', reason: `车次 ${no} 无可回溯的 exact-date 事实(缺完整 route/date 上下文)` }
|
|
491
|
+
}
|
|
492
|
+
// 先按 train kind 选择该 route+date 的最新查询批次,再在该批次匹配车次。
|
|
493
|
+
// 这样旧 hit 不会越过更新的 miss/unavailable,flight 批次也不能遮蔽 train 批次。
|
|
494
|
+
const scoped = latestFactsForRouteDate(facts, claim.origin, claim.destination, claim.date, 'train')
|
|
495
|
+
.filter(isStructuredTrainFact)
|
|
496
|
+
const sameNo = scoped.filter(f =>
|
|
497
|
+
f.flight_no.toUpperCase() === no
|
|
498
|
+
&& f.bookability === 'bookable_exact_date')
|
|
499
|
+
if (sameNo.length > 0) return { verdict: 'traceable', fact: sameNo[0], reason: `回溯 ${sameNo[0]!.query_id}` }
|
|
500
|
+
if (scoped.length > 0) {
|
|
501
|
+
return {
|
|
502
|
+
verdict: 'not_in_source',
|
|
503
|
+
reason: scoped.some(f => f.bookability === 'unavailable_exact_date')
|
|
504
|
+
? `${claim.origin}→${claim.destination} ${claim.date} train exact-date 源返回 0 条(${scoped[0]!.query_id})——不得用历史班期/相邻日期填充`
|
|
505
|
+
: `${claim.origin}→${claim.destination} ${claim.date} train exact-date 源在架 ${scoped.filter(f => f.bookability === 'bookable_exact_date').map(f => f.flight_no).join('/')}——无 ${no}`,
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return { verdict: 'route_unqueried', reason: `${claim.origin}→${claim.destination} ${claim.date} train 路线从未经 exact-date 源核验` }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
// 判定原语②:同日衔接硬约束(办事结束 → 门到机场 → 值机截止 → 起飞)
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
export interface SameDayDepartureConstraint {
|
|
516
|
+
/** 当日地面事务最早结束(当地 HH:MM,如银行办事) */
|
|
517
|
+
errand_end_local: string
|
|
518
|
+
/** 事务地点→机场门到门分钟(含步行/等车) */
|
|
519
|
+
airport_transit_min: number
|
|
520
|
+
/** 起飞前值机/安检截止分钟(国际建议 120) */
|
|
521
|
+
checkin_deadline_min: number
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export interface SameDayDepartureVerdict {
|
|
525
|
+
feasible: boolean
|
|
526
|
+
/** 事务约束下的最早可接起飞时刻(当地 HH:MM) */
|
|
527
|
+
earliest_departure_local: string
|
|
528
|
+
/** exact-date 在架且满足硬约束的直飞 */
|
|
529
|
+
feasible_flights: FlightFact[]
|
|
530
|
+
/** 被约束排除的在架直飞(带排除原因) */
|
|
531
|
+
excluded: Array<{ flight_no: string; reason: string }>
|
|
532
|
+
/** 不可行时的结构化最小改动建议 */
|
|
533
|
+
suggestions: string[]
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function hmToMin(hm: string): number {
|
|
537
|
+
const [h, m] = hm.split(':').map(Number)
|
|
538
|
+
return (h ?? 0) * 60 + (m ?? 0)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function minToHm(min: number): string {
|
|
542
|
+
return `${String(Math.floor(min / 60)).padStart(2, '0')}:${String(min % 60).padStart(2, '0')}`
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* 「上午办事 + 当天下午直飞」类核心衔接判定:
|
|
547
|
+
* 最早起飞 = errand_end + 门到机场 + 值机截止;exact-date 在架直飞逐班过闸。
|
|
548
|
+
* 在架为空的失败语义与「有班但接不上」分开陈述(前者是库存问题,后者是约束问题)。
|
|
549
|
+
*/
|
|
550
|
+
export function checkSameDayDeparture(
|
|
551
|
+
facts: BookableFact[],
|
|
552
|
+
q: { origin: string; destination: string; date: string },
|
|
553
|
+
c: SameDayDepartureConstraint,
|
|
554
|
+
): SameDayDepartureVerdict {
|
|
555
|
+
const earliest = hmToMin(c.errand_end_local) + c.airport_transit_min + c.checkin_deadline_min
|
|
556
|
+
const scoped = latestFactsForRouteDate(facts, q.origin, q.destination, q.date, 'flight')
|
|
557
|
+
const nonstopBookable = scoped.filter(f => f.bookability === 'bookable_exact_date' && f.nonstop !== false)
|
|
558
|
+
const feasible: FlightFact[] = []
|
|
559
|
+
const excluded: Array<{ flight_no: string; reason: string }> = []
|
|
560
|
+
for (const f of nonstopBookable) {
|
|
561
|
+
const dep = f.dep_local ? hmToMin(f.dep_local) : null
|
|
562
|
+
if (dep === null) {
|
|
563
|
+
excluded.push({ flight_no: f.flight_no, reason: '缺起飞时刻,不参与衔接判定' })
|
|
564
|
+
} else if (dep >= earliest) {
|
|
565
|
+
feasible.push(f)
|
|
566
|
+
} else {
|
|
567
|
+
excluded.push({ flight_no: f.flight_no, reason: `${f.dep_local} 起飞早于最早可接时刻 ${minToHm(earliest)}(${c.errand_end_local} 结束事务 + ${c.airport_transit_min}m 到机场 + ${c.checkin_deadline_min}m 值机)` })
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const ok = feasible.length > 0
|
|
571
|
+
const suggestions: string[] = []
|
|
572
|
+
if (!ok) {
|
|
573
|
+
const inStock = nonstopBookable.map(f => `${f.flight_no} ${f.dep_local ?? '?'}`).join('、')
|
|
574
|
+
suggestions.push(`增加 1 个${q.origin}住宿夜:改乘次日早班${inStock ? `(当日在架:${inStock})` : ''}`)
|
|
575
|
+
suggestions.push('或提前一天完成地面事务/提前一天出发,把同日衔接改为隔夜衔接')
|
|
576
|
+
}
|
|
577
|
+
return { feasible: ok, earliest_departure_local: minToHm(earliest), feasible_flights: feasible, excluded, suggestions }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
// 判定原语③:行程统计不变量(机器断言,不许自然语言总结反算)
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
|
|
584
|
+
export interface ItineraryFacts {
|
|
585
|
+
trip_start: string // YYYY-MM-DD
|
|
586
|
+
trip_end: string // YYYY-MM-DD(回到家的当地日期)
|
|
587
|
+
stays: Array<{ place: string; check_in: string; check_out: string }>
|
|
588
|
+
/** 机上夜(红眼跨夜)数 */
|
|
589
|
+
onboard_nights: number
|
|
590
|
+
/** O&D 段(用户视角段);flight 段含 legs 数(中转一段多 leg) */
|
|
591
|
+
od_segments: Array<{ from: string; to: string; date: string; mode: 'flight' | 'rail' | 'car'; legs: number }>
|
|
592
|
+
budget_items: Array<{ label: string; min_cny: number; max_cny: number }>
|
|
593
|
+
/** 产物里的预算下限话术(「可压到 ¥N」) */
|
|
594
|
+
claimed_floor_cny?: number
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export interface InvariantViolation {
|
|
598
|
+
kind: 'nights_inconsistent' | 'legs_inconsistent' | 'budget_floor_inconsistent' | 'date_order'
|
|
599
|
+
detail: string
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function daysBetween(a: string, b: string): number {
|
|
603
|
+
return Math.round((Date.parse(`${b}T00:00:00Z`) - Date.parse(`${a}T00:00:00Z`)) / 86_400_000)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function itineraryInvariants(it: ItineraryFacts): InvariantViolation[] {
|
|
607
|
+
const violations: InvariantViolation[] = []
|
|
608
|
+
const totalNights = daysBetween(it.trip_start, it.trip_end)
|
|
609
|
+
let hotelNights = 0
|
|
610
|
+
for (const s of it.stays) {
|
|
611
|
+
const n = daysBetween(s.check_in, s.check_out)
|
|
612
|
+
if (n <= 0) violations.push({ kind: 'date_order', detail: `${s.place} 住宿 ${s.check_in}→${s.check_out} 晚数 ≤0` })
|
|
613
|
+
else hotelNights += n
|
|
614
|
+
}
|
|
615
|
+
if (hotelNights + it.onboard_nights !== totalNights) {
|
|
616
|
+
violations.push({
|
|
617
|
+
kind: 'nights_inconsistent',
|
|
618
|
+
detail: `酒店夜 ${hotelNights} + 机上夜 ${it.onboard_nights} = ${hotelNights + it.onboard_nights} ≠ 行程总夜数 ${totalNights}(${it.trip_start}→${it.trip_end})——夜数口径必须由结构化住宿反算,不许自然语言估算`,
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
for (const seg of it.od_segments) {
|
|
622
|
+
if (seg.mode === 'flight' && seg.legs < 1) {
|
|
623
|
+
violations.push({ kind: 'legs_inconsistent', detail: `${seg.from}→${seg.to}(${seg.date})flight O&D 段 legs=${seg.legs}——每段至少 1 leg` })
|
|
624
|
+
}
|
|
625
|
+
if (seg.date < it.trip_start || seg.date > it.trip_end) {
|
|
626
|
+
violations.push({ kind: 'date_order', detail: `${seg.from}→${seg.to} 日期 ${seg.date} 落在行程窗 ${it.trip_start}→${it.trip_end} 之外` })
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const minSum = it.budget_items.reduce((acc, b) => acc + b.min_cny, 0)
|
|
630
|
+
if (it.claimed_floor_cny !== undefined && it.claimed_floor_cny < minSum) {
|
|
631
|
+
violations.push({
|
|
632
|
+
kind: 'budget_floor_inconsistent',
|
|
633
|
+
detail: `预算分项最低合计 ¥${minSum.toLocaleString()} > 产物声称可压到 ¥${it.claimed_floor_cny.toLocaleString()}——下限话术不得低于分项最低合计`,
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
return violations
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
// 判定原语④:回头路检测(A→B→A;存在 B→下一站直飞时给最小改动建议)
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
|
|
643
|
+
export interface BacktrackFinding {
|
|
644
|
+
kind: 'backtrack'
|
|
645
|
+
/** 回头路径,如 ['HKT','KBV','HKT'] */
|
|
646
|
+
path: string[]
|
|
647
|
+
/** 最小改动建议(存在 B→C 直飞边时) */
|
|
648
|
+
suggestion?: string
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function detectBacktrack(stops: string[], nonstopEdges: ReadonlySet<string>): BacktrackFinding[] {
|
|
652
|
+
const findings: BacktrackFinding[] = []
|
|
653
|
+
for (let i = 0; i + 2 < stops.length; i++) {
|
|
654
|
+
const a = stops[i]!, b = stops[i + 1]!, c = stops[i + 2]!
|
|
655
|
+
if (a !== c) continue
|
|
656
|
+
// A→B→A 回头;若 B→D(下一站)有直飞边,建议「去程后直接 B→D」
|
|
657
|
+
const d = stops[i + 3]
|
|
658
|
+
const suggestion = d && nonstopEdges.has(`${b}-${d}`)
|
|
659
|
+
? `${b}→${d} 有直飞:改为 ${a}→${b} 后直接 ${b}→${d},消除 ${a}→${b}→${a} 回头路`
|
|
660
|
+
: undefined
|
|
661
|
+
findings.push({ kind: 'backtrack', path: [a, b, c], suggestion })
|
|
662
|
+
}
|
|
663
|
+
return findings
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
// 确定性渲染:航班/政策行只从结构化对象生成(issue #46 期望①:禁止凭记忆抄写)
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
|
|
670
|
+
/** 远期复核默认 gate:出发前 30 天 */
|
|
671
|
+
export function defaultReviewBy(tripDate: string): string {
|
|
672
|
+
const d = new Date(Date.parse(`${tripDate}T00:00:00Z`) - 30 * 86_400_000)
|
|
673
|
+
return d.toISOString().slice(0, 10)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* 航班/车次事实行。bookable_exact_date 才允许完整班次行;其余一律 fail-closed 措辞。
|
|
678
|
+
* train 复用此 canonical renderer 但只输出车次语义,不输出直飞或价格;永不输出无条件 ✓;
|
|
679
|
+
* 证据链(source/query_id/fetched_at)随行必达。
|
|
680
|
+
*/
|
|
681
|
+
export function renderFlightFact(f: FlightFact): string {
|
|
682
|
+
return renderFlightFactInner(f) + ` <!-- fact:${f.fact_id} -->`
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function renderFlightFactInner(f: FlightFact): string {
|
|
686
|
+
// 单向生成锚点(D-26):每条事实行内嵌 fact_id,闸侧锚点匹配 = 确定性回溯
|
|
687
|
+
const anchor = ` <!-- fact:${f.fact_id} -->`
|
|
688
|
+
const route = `${f.route.origin}→${f.route.destination}`
|
|
689
|
+
const evidence = `[${f.source}@${f.fetched_at} #${f.query_id}]`
|
|
690
|
+
if (f.kind === 'train' && f.bookability === 'unavailable_exact_date') {
|
|
691
|
+
const review = f.review_by ?? defaultReviewBy(f.date)
|
|
692
|
+
return `- ${route} ${f.date} 车次${f.flight_no ? ` ${f.flight_no}` : ''}:**未确认/当前不可售**——exact-date 源 ${f.source} 于 ${f.fetched_at.slice(0, 10)} 返回 0 条;到 ${review} 复核,不得用历史班期/相邻日期填充 ${evidence}`
|
|
693
|
+
}
|
|
694
|
+
if (f.kind === 'train' && f.bookability !== 'bookable_exact_date') {
|
|
695
|
+
return `- ${route} ${f.date} 车次${f.flight_no ? ` ${f.flight_no}` : ''}:**未核验**(${f.bookability})——仅作参考,不得作为可下单方案呈现 ${evidence}`
|
|
696
|
+
}
|
|
697
|
+
if (f.bookability === 'unavailable_exact_date') {
|
|
698
|
+
const review = f.review_by ?? defaultReviewBy(f.date)
|
|
699
|
+
return `- ${route} ${f.date}:**未确认/当前不可售**——exact-date 源 ${f.source} 于 ${f.fetched_at.slice(0, 10)} 返回 0 条;到 ${review} 复核,不得用历史班期/相邻日期填充 ${evidence}`
|
|
700
|
+
}
|
|
701
|
+
if (f.bookability !== 'bookable_exact_date') {
|
|
702
|
+
return `- ${route} ${f.date} ${f.flight_no || ''}:**未核验**(${f.bookability})——仅作参考,不得作为可下单方案呈现 ${evidence}`
|
|
703
|
+
}
|
|
704
|
+
if (f.kind === 'train') {
|
|
705
|
+
const times = f.dep_local && f.arr_local ? ` ${f.dep_local}→${f.arr_local}` : ''
|
|
706
|
+
return `- ${route} ${f.date} 车次 ${f.flight_no}${times} ${evidence}`
|
|
707
|
+
}
|
|
708
|
+
const carrier = f.operating_carrier && f.operating_carrier !== f.marketing_carrier
|
|
709
|
+
? `营销 ${f.marketing_carrier ?? '?'} / 实际承运 ${f.operating_carrier}`
|
|
710
|
+
: (f.marketing_carrier ?? '')
|
|
711
|
+
const times = f.dep_local && f.arr_local ? ` ${f.dep_local}→${f.arr_local}` : ''
|
|
712
|
+
const nonstop = f.nonstop === false ? '(中转)' : ' 直飞'
|
|
713
|
+
// 双机场城市防混淆:落点机场码与城市码不同时显式标注(FD 落 DMK / VZ 落 BKK 不互换)
|
|
714
|
+
const airportNote = f.route.dest_airport && f.route.dest_airport !== f.route.destination
|
|
715
|
+
? `(落 ${f.route.dest_airport})`
|
|
716
|
+
: ''
|
|
717
|
+
const price = f.price !== undefined ? ` ¥${f.price}` : ' 价待询'
|
|
718
|
+
const baggage = f.baggage_included === false ? ',不含托运行李' : ''
|
|
719
|
+
return `- ${route} ${f.date} ${f.flight_no}${carrier ? `(${carrier})` : ''}${times}${nonstop}${airportNote}${price}${baggage} ${evidence}`
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* 联程/分票连接行:只有同票保护(protected_connection=true)才允许称「联程」;
|
|
724
|
+
* 分票必须显式标红自助转机风险(无行李直挂/误机无保护)。
|
|
725
|
+
*/
|
|
726
|
+
export function renderConnection(legA: FlightFact, legB: FlightFact): string {
|
|
727
|
+
const through = legA.protected_connection === true && legB.protected_connection === true
|
|
728
|
+
const head = `${legA.route.origin}→${legA.route.destination} ${legA.flight_no} + ${legB.route.origin}→${legB.route.destination} ${legB.flight_no}`
|
|
729
|
+
return through
|
|
730
|
+
? `- ${head}:联程(同一票号/行李直挂/误机保护)`
|
|
731
|
+
: `- ${head}:⚠️ **分票/自助转机**——两段接得上 ≠ 联程;无同一票号、行李需自取重挂、误机无保护,缓冲与风险自担`
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* 政策事实行:恒为「截至 as_of 的现行政策」表述 + 复核 gate,永不用无条件 ✓。
|
|
736
|
+
* 远期(出发日晚于 as_of)必须附 review_by(默认 D-30)。
|
|
737
|
+
*
|
|
738
|
+
* 行尾内嵌 `<!-- fact:<fact_id> -->` 锚点(issue #273,ADR-19 typed-anchor 单向生成):
|
|
739
|
+
* 产物经渲染原语生成即自带溯源锚,闸侧锚点确定性回溯;手改/伪造锚点 = fact_anchor_unknown。
|
|
740
|
+
* 与 renderFlightFact / renderHotelFact 同源:启发式 POLICY_WORD 仅覆盖手写/历史产物。
|
|
741
|
+
*/
|
|
742
|
+
export function renderPolicyFact(p: PolicyFact, tripStart?: string): string {
|
|
743
|
+
return `${policyCanonicalBody(p, tripStart)} <!-- fact:${p.fact_id} -->`
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* 政策行 canonical body(不含锚点;issue #359,D-26 残余收口):
|
|
748
|
+
* 闸侧锚点回溯用同一原文,subject/statement/source/fetched_at/query_id 任意一项
|
|
749
|
+
* 改动 → 行文本不再等于 canonical body → fact_anchor_unknown fail-closed。
|
|
750
|
+
* 不走宽松 substring/subset 匹配,避免锚点行被改写 subject/statement 后仍 pass。
|
|
751
|
+
* 复核提醒(review_by 或 tripStart 派生的 defaultReviewBy)是合法的可选段——
|
|
752
|
+
* `tripStart` 与 renderPolicyFact 同一传入,产物与闸两侧语义一致。
|
|
753
|
+
*/
|
|
754
|
+
export function policyCanonicalBody(p: PolicyFact, tripStart?: string): string {
|
|
755
|
+
const review = p.review_by ?? (tripStart ? defaultReviewBy(tripStart) : undefined)
|
|
756
|
+
const reviewNote = review ? `;远期政策须复核——到 ${review} 再核验一次` : ''
|
|
757
|
+
return `- ${p.subject}:截至 ${p.as_of} 的现行政策——${p.statement}${reviewNote} [${p.source}@${p.fetched_at} #${p.query_id}]`
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** 从结构化行程渲染夜数口径行(机器反算,唯一口径) */
|
|
761
|
+
export function renderNightsLine(it: ItineraryFacts): string {
|
|
762
|
+
const totalNights = daysBetween(it.trip_start, it.trip_end)
|
|
763
|
+
const hotelNights = it.stays.reduce((acc, s) => acc + Math.max(0, daysBetween(s.check_in, s.check_out)), 0)
|
|
764
|
+
const flightOd = it.od_segments.filter(s => s.mode === 'flight').length
|
|
765
|
+
const flightLegs = it.od_segments.filter(s => s.mode === 'flight').reduce((acc, s) => acc + s.legs, 0)
|
|
766
|
+
return `共 ${daysBetween(it.trip_start, it.trip_end) + 1} 天 ${totalNights} 晚 = ${hotelNights} 个酒店夜 + ${it.onboard_nights} 个机上夜;航班按 O&D ${flightOd} 段(共 ${flightLegs} 个 flight legs)`
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ---------------------------------------------------------------------------
|
|
770
|
+
// 酒店事实(D-26,issue #118):exact-date 酒店检索 hit/miss 落账
|
|
771
|
+
// 纪律:摸底(未定档期)不落账(无 exact-date 语义);needs-setup/error 传输失败永不落负事实
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
|
|
774
|
+
export function factsFromHotel(input: {
|
|
775
|
+
source: string
|
|
776
|
+
destination: string
|
|
777
|
+
checkIn?: string
|
|
778
|
+
checkOut?: string
|
|
779
|
+
verdict: string
|
|
780
|
+
options: number
|
|
781
|
+
evidence: string
|
|
782
|
+
fetchedAt: string
|
|
783
|
+
}): HotelFact[] {
|
|
784
|
+
if (!input.checkIn || !input.checkOut) return []
|
|
785
|
+
if (input.verdict !== 'hit' && input.verdict !== 'miss') return []
|
|
786
|
+
const queryId = `hotel:${input.source}:${input.destination}:${input.checkIn}_${input.checkOut}`
|
|
787
|
+
return [{
|
|
788
|
+
schema: BOOKABLE_FACT_SCHEMA,
|
|
789
|
+
kind: 'hotel',
|
|
790
|
+
fact_id: makeFactId([input.destination, input.checkIn, input.checkOut, queryId]),
|
|
791
|
+
destination: input.destination,
|
|
792
|
+
check_in: input.checkIn,
|
|
793
|
+
check_out: input.checkOut,
|
|
794
|
+
verdict: input.verdict,
|
|
795
|
+
bookability: input.verdict === 'hit' ? 'bookable_exact_date' : 'unavailable_exact_date',
|
|
796
|
+
options_masked: input.options,
|
|
797
|
+
source: input.source,
|
|
798
|
+
query_id: queryId,
|
|
799
|
+
fetched_at: input.fetchedAt,
|
|
800
|
+
as_of: input.fetchedAt.slice(0, 10),
|
|
801
|
+
tier: 'live_inventory',
|
|
802
|
+
}]
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** 酒店 claim 回溯:同目的地+同档期的 exact-date 事实裁决(无事实=unverified,fail-closed) */
|
|
806
|
+
export function hotelClaimVerdict(
|
|
807
|
+
facts: BookableFact[],
|
|
808
|
+
claim: { destination: string; check_in?: string; check_out?: string },
|
|
809
|
+
): { verdict: 'traceable' | 'not_in_source' | 'unverified'; fact?: HotelFact; reason: string } {
|
|
810
|
+
const scoped = facts.filter((f): f is HotelFact =>
|
|
811
|
+
f.kind === 'hotel' && f.destination === claim.destination
|
|
812
|
+
&& (!claim.check_in || f.check_in === claim.check_in)
|
|
813
|
+
&& (!claim.check_out || f.check_out === claim.check_out))
|
|
814
|
+
if (scoped.length === 0) {
|
|
815
|
+
return {
|
|
816
|
+
verdict: 'unverified',
|
|
817
|
+
reason: `酒店 claim(${claim.destination}${claim.check_in ? ` ${claim.check_in}→${claim.check_out ?? '?'}` : ''})未经 exact-date 源核验——先检索再写结论`,
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const hit = scoped.find(f => f.bookability === 'bookable_exact_date')
|
|
821
|
+
if (hit) return { verdict: 'traceable', fact: hit, reason: 'exact-date 命中' }
|
|
822
|
+
const latest = scoped[scoped.length - 1]!
|
|
823
|
+
return { verdict: 'not_in_source', fact: latest, reason: `该档期 exact-date 源正常返回 0 条(${latest.fetched_at})——不得写有房/可订` }
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
// 渲染原语(单向生成,issue #118):事实 → 行,锚点内嵌 fact_id;
|
|
828
|
+
// 产物经原语渲染即自带溯源锚,闸侧锚点匹配 = 确定性回溯,不靠行文猜测
|
|
829
|
+
// ---------------------------------------------------------------------------
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
export function renderHotelFact(f: HotelFact): string {
|
|
833
|
+
if (f.verdict !== 'hit') {
|
|
834
|
+
return `- ${f.destination} ${f.check_in}→${f.check_out}:exact-date 源 0 条在架(不可订) <!-- fact:${f.fact_id} -->`
|
|
835
|
+
}
|
|
836
|
+
return `- ${f.destination} ${f.check_in}→${f.check_out}:住宿已核验(在架 ${f.options_masked} 家;价格上游打码,真实价以落地页为准) <!-- fact:${f.fact_id} -->`
|
|
837
|
+
}
|