@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,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money/FX fact 合同 v1(issue #344 契约切片,创始人 2026-10-02 授权提前启动,默认关闭、零活体汇率源)。
|
|
3
|
+
*
|
|
4
|
+
* 形态(对齐 `ts/data/llm-price-table.json` 的 fail-closed 纪律与 gotry_bookable_fact 证据链形态):
|
|
5
|
+
* - 类型化 Money(原币金额 = 币种 + 最小单位整数,杜绝浮点漂移);
|
|
6
|
+
* - 类型化 FxFact(base/quote 汇率事实:rate + provider + provenance + as_of 估值时点 + fetched_at 抓取时刻,
|
|
7
|
+
* 全字段必填,unknown/missing 一律 fail-closed,禁止猜测汇率——与「未知模型不猜价」同一条纪律);
|
|
8
|
+
* - 主源+降级只做契约 seam(FxProviderDescriptor/FxRateSource 接口与数据形状):
|
|
9
|
+
* 运行时注册表冻结为空(LIVE_FX_PROVIDER_DESCRIPTORS = []),FX_TRIGGER_FIRED = false;
|
|
10
|
+
* 触发前零浮动汇率源被咨询(consultFxSources 在读任何源之前直接结构化拒绝,docs/architecture.md §10 D-26 边界不变);
|
|
11
|
+
* - 同一估值时点比较:跨币种比较/聚合必须经同一 as_of 的规范化值(NormalizedAmount),
|
|
12
|
+
* 混时点比较显式拒绝(fail-closed 并给出带证据的错误);无宽限期数值——估值时点绑定即口径,
|
|
13
|
+
* stale/miss/error 不得伪装为当前准确换算(不发明容差)。
|
|
14
|
+
*
|
|
15
|
+
* 冻结的舍入规则:仅在「换算落位到目标币种最小单位」这一步舍入,half-away-from-zero;
|
|
16
|
+
* 交叉汇率(经中转币两腿)每腿落位一次(中间腿按中转币最小单位舍入),无中间无理数携带。
|
|
17
|
+
*
|
|
18
|
+
* 边界(D-26 / #344 触发后置保持):
|
|
19
|
+
* - 本模块纯函数、零 IO、零网络、零依赖新增;
|
|
20
|
+
* - 不选定任何主源/降级源(那是触发后 founders 决策);注册表非空 + FX_TRIGGER_FIRED 置真
|
|
21
|
+
* 都需要真实非 CNY 供应商报价、用户预算或目的地需求触发后另行准入;
|
|
22
|
+
* - CNY-only 结算路径不受影响(现有 budgetCny/priceCny 算术不动,ts/src/model.ts 内核钉住未改)。
|
|
23
|
+
*
|
|
24
|
+
* @module capabilities/fx-contract
|
|
25
|
+
*/
|
|
26
|
+
import { makeFactId } from '../src/bookable-facts.js';
|
|
27
|
+
/** 合同 schema 标识(版本化,读取端校验)。 */
|
|
28
|
+
export const FX_FACT_SCHEMA = 'gotry_fx_fact.v1';
|
|
29
|
+
/** 币种封闭集:ISO 4217 字母码 → 最小单位指数(客观公共标准数据,类比 IANA 时区名的运行时权威口径;
|
|
30
|
+
* 未知币种不在集内即 fail-closed,不做任何猜测映射)。 */
|
|
31
|
+
const ISO_4217_MINOR_UNIT_EXPONENTS = {
|
|
32
|
+
CNY: 2, USD: 2, EUR: 2, GBP: 2, HKD: 2, SGD: 2, THB: 2, MYR: 2,
|
|
33
|
+
JPY: 0, KRW: 0,
|
|
34
|
+
};
|
|
35
|
+
/** 合同违规(fail-closed):消息必须带证据(两侧估值时点/fact_id/provider 等)。 */
|
|
36
|
+
export class FxContractError extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(`fx contract: ${message} (fail-closed, no guessed rate)`);
|
|
39
|
+
this.name = 'FxContractError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** 解析并准入币种:必须是大写三字母且在封闭集内;未知/小写/变体一律拒绝。 */
|
|
43
|
+
export function parseCurrency(code) {
|
|
44
|
+
if (typeof code !== 'string' || !/^[A-Z]{3}$/.test(code)) {
|
|
45
|
+
throw new FxContractError(`currency code must be an uppercase 3-letter ISO 4217 code, got: ${JSON.stringify(code)}`);
|
|
46
|
+
}
|
|
47
|
+
const exponent = ISO_4217_MINOR_UNIT_EXPONENTS[code];
|
|
48
|
+
if (exponent === undefined) {
|
|
49
|
+
throw new FxContractError(`unknown currency code ${code} not in the frozen closed set [${Object.keys(ISO_4217_MINOR_UNIT_EXPONENTS).join('/')}] — extend the set via PR`);
|
|
50
|
+
}
|
|
51
|
+
return code;
|
|
52
|
+
}
|
|
53
|
+
/** 币种最小单位指数(ISO 4217)。 */
|
|
54
|
+
export function currencyExponent(currency) {
|
|
55
|
+
const exponent = ISO_4217_MINOR_UNIT_EXPONENTS[currency];
|
|
56
|
+
if (exponent === undefined)
|
|
57
|
+
throw new FxContractError(`unknown currency ${currency}`);
|
|
58
|
+
return exponent;
|
|
59
|
+
}
|
|
60
|
+
/** 严格十进制金额解析:`-?(0|[1-9][0-9]*)(\.[0-9]+)?`——无千分位、无指数记法、
|
|
61
|
+
* 小数位不得超过币种最小单位指数(JPY「0.5」即拒)。 */
|
|
62
|
+
export function parseMoney(currency, decimal) {
|
|
63
|
+
const cur = parseCurrency(currency);
|
|
64
|
+
if (typeof decimal !== 'string' || !/^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(decimal)) {
|
|
65
|
+
throw new FxContractError(`money decimal for ${cur} must be a plain decimal like '1234.56' (no separators/exponent/plus), got: ${JSON.stringify(decimal)}`);
|
|
66
|
+
}
|
|
67
|
+
const negative = decimal.startsWith('-');
|
|
68
|
+
const unsigned = negative ? decimal.slice(1) : decimal;
|
|
69
|
+
const [intPart, fracPart = ''] = unsigned.split('.');
|
|
70
|
+
const exponent = currencyExponent(cur);
|
|
71
|
+
if (fracPart.length > exponent) {
|
|
72
|
+
throw new FxContractError(`money decimal ${decimal} for ${cur} exceeds its ${exponent}-digit minor unit (sub-minor precision rejected, no silent rounding)`);
|
|
73
|
+
}
|
|
74
|
+
const padded = (fracPart + '0'.repeat(exponent)).slice(0, exponent);
|
|
75
|
+
const magnitude = BigInt(intPart + padded);
|
|
76
|
+
return { currency: cur, amountMinor: negative ? -magnitude : magnitude };
|
|
77
|
+
}
|
|
78
|
+
/** 原币展示(验收 3:保留原币展示与供应商金额)。 */
|
|
79
|
+
export function formatMoney(money) {
|
|
80
|
+
const exponent = currencyExponent(money.currency);
|
|
81
|
+
const negative = money.amountMinor < 0n;
|
|
82
|
+
const magnitude = negative ? -money.amountMinor : money.amountMinor;
|
|
83
|
+
const digits = magnitude.toString().padStart(exponent + 1, '0');
|
|
84
|
+
const body = exponent === 0 ? digits : `${digits.slice(0, -exponent)}.${digits.slice(-exponent)}`;
|
|
85
|
+
return `${negative ? '-' : ''}${body} ${money.currency}`;
|
|
86
|
+
}
|
|
87
|
+
/** UTC 时刻解析与规范化:接受完整 ISO-8601 时刻(含 Z/偏移),规范化为毫秒精度 UTC ISO 串;
|
|
88
|
+
* 仅日期(无时刻)、垃圾串、缺值一律拒绝(缺时刻 fail-closed)。 */
|
|
89
|
+
export function parseUtcInstant(value, label) {
|
|
90
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
91
|
+
throw new FxContractError(`${label} must be a non-empty ISO-8601 UTC instant string, got: ${JSON.stringify(value)}`);
|
|
92
|
+
}
|
|
93
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value.trim())) {
|
|
94
|
+
throw new FxContractError(`${label} must carry a full time-of-day (date-only '${value}' is not a valuation instant — mixed-date comparison would be ambiguous)`);
|
|
95
|
+
}
|
|
96
|
+
const ms = Date.parse(value);
|
|
97
|
+
if (!Number.isFinite(ms)) {
|
|
98
|
+
throw new FxContractError(`${label} is not a parseable ISO-8601 instant: ${JSON.stringify(value)}`);
|
|
99
|
+
}
|
|
100
|
+
return new Date(ms).toISOString();
|
|
101
|
+
}
|
|
102
|
+
/** 汇率严格十进制解析:无符号、无指数记法、必须严格大于 0(零/负汇率不是事实)、小数位 ≤ 12。
|
|
103
|
+
* 返回归一化字符串(去尾零)+ 分子 + 小数位,供精确 bigint 换算与 fact_id 幂等。 */
|
|
104
|
+
function parseRateDecimal(rate) {
|
|
105
|
+
if (typeof rate !== 'string' || !/^(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(rate)) {
|
|
106
|
+
throw new FxContractError(`fx rate must be a plain positive decimal like '7.25' (no sign/exponent/separators), got: ${JSON.stringify(rate)}`);
|
|
107
|
+
}
|
|
108
|
+
const [intPart, fracPart = ''] = rate.split('.');
|
|
109
|
+
if (fracPart.length > 12) {
|
|
110
|
+
throw new FxContractError(`fx rate ${rate} exceeds 12 fractional digits (absurd precision is not evidence)`);
|
|
111
|
+
}
|
|
112
|
+
const trimmedFrac = fracPart.replace(/0+$/, '');
|
|
113
|
+
const normalized = trimmedFrac.length === 0 ? intPart : `${intPart}.${trimmedFrac}`;
|
|
114
|
+
const numerator = BigInt(intPart + trimmedFrac);
|
|
115
|
+
if (numerator <= 0n) {
|
|
116
|
+
throw new FxContractError(`fx rate must be strictly positive, got: ${rate} (zero/negative rate is never a valid conversion fact)`);
|
|
117
|
+
}
|
|
118
|
+
return { normalized, numerator, scale: trimmedFrac.length };
|
|
119
|
+
}
|
|
120
|
+
/** 精确除法 + half-away-from-zero 落位(冻结舍入规则的实现核心)。 */
|
|
121
|
+
function roundDivHalfAwayFromZero(n, d) {
|
|
122
|
+
const sign = n < 0n ? -1n : 1n;
|
|
123
|
+
const magnitude = n < 0n ? -n : n;
|
|
124
|
+
const q = magnitude / d;
|
|
125
|
+
const rem = magnitude % d;
|
|
126
|
+
return sign * (rem * 2n >= d ? q + 1n : q);
|
|
127
|
+
}
|
|
128
|
+
/** 运行时源注册表:冻结为空(触发后置,D-26 边界不变)。任何非空变更都是创始人准入决策。 */
|
|
129
|
+
export const LIVE_FX_PROVIDER_DESCRIPTORS = [];
|
|
130
|
+
/** FX 实现触发闸:冻结为 false。置真 = 首条真实非 CNY 供应商报价、用户预算或目的地需求已出现(#344 触发条件)。 */
|
|
131
|
+
export const FX_TRIGGER_FIRED = false;
|
|
132
|
+
/** 咨询活体汇率源的唯一入口。触发未开(FX_TRIGGER_FIRED=false)时在读任何源之前直接结构化拒绝
|
|
133
|
+
* (零浮动汇率源被咨询);触发后按 primary→fallback 顺位,且每条结果仍必须过 buildFxFact 全字段校验。 */
|
|
134
|
+
export async function consultFxSources(query, options) {
|
|
135
|
+
const triggerFired = options?.triggerFired ?? FX_TRIGGER_FIRED;
|
|
136
|
+
const sources = options?.sources ?? [];
|
|
137
|
+
if (!triggerFired) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
code: 'trigger-deferred',
|
|
141
|
+
detail: 'FX implementation trigger has not fired (#344: first real non-CNY supplier quote, user budget, or destination demand); zero floating-rate sources consulted (D-26 boundary unchanged)',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (sources.length === 0) {
|
|
145
|
+
return { ok: false, code: 'no-source-admitted', detail: 'no FX provider admitted in the registry; admitting one is a founder decision with legal_basis' };
|
|
146
|
+
}
|
|
147
|
+
let lastMiss;
|
|
148
|
+
for (const source of sources) {
|
|
149
|
+
const out = await source.fetchRate(query);
|
|
150
|
+
if (out.ok)
|
|
151
|
+
return { ok: true, fact: out.fact };
|
|
152
|
+
lastMiss = out;
|
|
153
|
+
}
|
|
154
|
+
return lastMiss ?? { ok: false, code: 'miss', detail: 'no FX source produced a rate' };
|
|
155
|
+
}
|
|
156
|
+
/** 构建汇率事实(全字段必填校验入口,对齐价格表「未知模型即拒」的纪律)。
|
|
157
|
+
* registry 默认 = 冻结空的运行时注册表 → 生产路径任何 provider 都被拒(来源不明);
|
|
158
|
+
* 测试/未来适配器以显式注入的 registry 构造离线事实。 */
|
|
159
|
+
export function buildFxFact(input, registry = LIVE_FX_PROVIDER_DESCRIPTORS) {
|
|
160
|
+
const base = parseCurrency(input.base);
|
|
161
|
+
const quote = parseCurrency(input.quote);
|
|
162
|
+
if (base === quote) {
|
|
163
|
+
throw new FxContractError(`base and quote must differ (${base}→${quote}); an identity rate would bypass conversion evidence`);
|
|
164
|
+
}
|
|
165
|
+
const rate = parseRateDecimal(input.rate);
|
|
166
|
+
if (typeof input.provider !== 'string' || input.provider.length === 0) {
|
|
167
|
+
throw new FxContractError(`fx fact provider must be a non-empty source identifier, got: ${JSON.stringify(input.provider)}`);
|
|
168
|
+
}
|
|
169
|
+
const descriptor = registry.find(d => d.id === input.provider);
|
|
170
|
+
if (!descriptor) {
|
|
171
|
+
throw new FxContractError(`fx fact provider not admitted in the registry: ${JSON.stringify(input.provider)} (来源不明 fail-closed; admitted: [${registry.map(d => d.id).join('/') || 'none'}])`);
|
|
172
|
+
}
|
|
173
|
+
if (typeof descriptor.legal_basis !== 'string' || descriptor.legal_basis.length === 0) {
|
|
174
|
+
throw new FxContractError(`admitted provider ${descriptor.id} carries no legal_basis — registry entry is defective`);
|
|
175
|
+
}
|
|
176
|
+
if (typeof input.provenance !== 'string' || input.provenance.length === 0) {
|
|
177
|
+
throw new FxContractError(`fx fact provenance must be a non-empty evidence-chain description, got: ${JSON.stringify(input.provenance)}`);
|
|
178
|
+
}
|
|
179
|
+
const asOf = parseUtcInstant(input.as_of, 'fx fact as_of (估值时点)');
|
|
180
|
+
const fetchedAt = parseUtcInstant(input.fetched_at, 'fx fact fetched_at (抓取时刻)');
|
|
181
|
+
if (asOf > fetchedAt) {
|
|
182
|
+
throw new FxContractError(`fx fact as_of ${asOf} is later than fetched_at ${fetchedAt} — impossible provenance (valuation instant must not postdate observation)`);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
schema: FX_FACT_SCHEMA,
|
|
186
|
+
fact_id: makeFactId(['fx', base, quote, descriptor.id, asOf, rate.normalized]),
|
|
187
|
+
base,
|
|
188
|
+
quote,
|
|
189
|
+
rate: rate.normalized,
|
|
190
|
+
provider: descriptor.id,
|
|
191
|
+
provenance: input.provenance,
|
|
192
|
+
as_of: asOf,
|
|
193
|
+
fetched_at: fetchedAt,
|
|
194
|
+
rounding: 'minor-unit-half-away-from-zero',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** 原币即比较币时的恒等钉时点:金额不经任何汇率(同币种 rate=1 是恒等式,不是汇率猜测),
|
|
198
|
+
* 只把估值时点绑定到规范化值上,使原生币种金额能与 FX 规范化金额在同一口径下比较/聚合。 */
|
|
199
|
+
export function pinNativeMoney(money, valuationAsOf) {
|
|
200
|
+
const pinned = parseUtcInstant(valuationAsOf, 'native pin valuation as_of');
|
|
201
|
+
return {
|
|
202
|
+
currency: money.currency,
|
|
203
|
+
amountMinor: money.amountMinor,
|
|
204
|
+
valuationAsOf: pinned,
|
|
205
|
+
fx: { fact_id: `native:${money.currency}`, provider: 'native-identity', base: money.currency, quote: money.currency, rate: '1' },
|
|
206
|
+
original: money,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** 校验 FxFact 覆盖给定换算对(供应商币种变化/拿错腿在此显式拒绝)。 */
|
|
210
|
+
function assertFactPair(fact, from, to) {
|
|
211
|
+
if (fact.base !== from || fact.quote !== to) {
|
|
212
|
+
throw new FxContractError(`fx fact ${fact.fact_id} pair ${fact.base}→${fact.quote} does not cover the requested conversion ${from}→${to} (supplier currency changed or wrong leg — no cross-pair substitution)`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** 断言 fact 的估值时点等于调用方钉住的估值时点(旧 rate 不得混入新时点比较;无宽限数值,精确绑定)。 */
|
|
216
|
+
function assertPinnedValuation(fact, pinnedAsOf) {
|
|
217
|
+
if (pinnedAsOf === undefined)
|
|
218
|
+
return;
|
|
219
|
+
const pinned = parseUtcInstant(pinnedAsOf, 'pinned valuation as_of');
|
|
220
|
+
if (fact.as_of !== pinned) {
|
|
221
|
+
throw new FxContractError(`fx fact ${fact.fact_id} (${fact.base}→${fact.quote}, provider ${fact.provider}) as_of ${fact.as_of} does not match the pinned valuation instant ${pinned} — stale/未对齐 rate 不得当作该时点的准确换算`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function fxRef(fact) {
|
|
225
|
+
return { fact_id: fact.fact_id, provider: fact.provider, base: fact.base, quote: fact.quote, rate: fact.rate };
|
|
226
|
+
}
|
|
227
|
+
/** 单腿换算:money × rate,仅在目标币最小单位落位时舍入(half-away-from-zero)。
|
|
228
|
+
* valuationAsOf 可选钉住时点:给了就必须与 fact.as_of 完全一致(混时点 fail-closed)。 */
|
|
229
|
+
export function convertMoney(money, target, fact, valuationAsOf) {
|
|
230
|
+
const targetCurrency = parseCurrency(target);
|
|
231
|
+
assertFactPair(fact, money.currency, targetCurrency);
|
|
232
|
+
assertPinnedValuation(fact, valuationAsOf);
|
|
233
|
+
const fromExponent = currencyExponent(money.currency);
|
|
234
|
+
const toExponent = currencyExponent(targetCurrency);
|
|
235
|
+
const rate = parseRateDecimal(fact.rate);
|
|
236
|
+
// amountMinor × rateNumerator × 10^toExponent / 10^(fromExponent + rateScale)
|
|
237
|
+
const numerator = money.amountMinor * rate.numerator * 10n ** BigInt(toExponent);
|
|
238
|
+
const denominator = 10n ** BigInt(fromExponent + rate.scale);
|
|
239
|
+
const amountMinor = roundDivHalfAwayFromZero(numerator, denominator);
|
|
240
|
+
return {
|
|
241
|
+
currency: targetCurrency,
|
|
242
|
+
amountMinor,
|
|
243
|
+
valuationAsOf: fact.as_of,
|
|
244
|
+
fx: fxRef(fact),
|
|
245
|
+
original: money,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/** 交叉汇率(两腿显式合成):两腿估值时点必须完全一致(混时点显式拒绝,带两侧证据);
|
|
249
|
+
* 中间腿按中转币最小单位落位一次(冻结规则,无隐藏精度携带)。 */
|
|
250
|
+
export function convertMoneyViaPivot(money, pivot, target, legBaseToPivot, legPivotToTarget, valuationAsOf) {
|
|
251
|
+
if (legBaseToPivot.as_of !== legPivotToTarget.as_of) {
|
|
252
|
+
throw new FxContractError(`cross-rate legs must share the same valuation instant: leg1 ${legBaseToPivot.base}→${legBaseToPivot.quote} (${legBaseToPivot.provider}, ${legBaseToPivot.fact_id}) as_of ${legBaseToPivot.as_of} vs leg2 ${legPivotToTarget.base}→${legPivotToTarget.quote} (${legPivotToTarget.provider}, ${legPivotToTarget.fact_id}) as_of ${legPivotToTarget.as_of} — 混时点合成拒绝`);
|
|
253
|
+
}
|
|
254
|
+
const pivotCurrency = parseCurrency(pivot);
|
|
255
|
+
const intermediate = convertMoney(money, pivotCurrency, legBaseToPivot, valuationAsOf);
|
|
256
|
+
const pivotMoney = { currency: pivotCurrency, amountMinor: intermediate.amountMinor };
|
|
257
|
+
return convertMoney(pivotMoney, target, legPivotToTarget, valuationAsOf);
|
|
258
|
+
}
|
|
259
|
+
/** 混时点/混币种比较的带证据拒绝。 */
|
|
260
|
+
function assertSameComparisonBasis(a, b, op) {
|
|
261
|
+
if (a.currency !== b.currency) {
|
|
262
|
+
throw new FxContractError(`${op} requires the same normalized currency: ${a.currency} (fx ${a.fx.provider} ${a.fx.fact_id}) vs ${b.currency} (fx ${b.fx.provider} ${b.fx.fact_id}) — normalize both sides first`);
|
|
263
|
+
}
|
|
264
|
+
if (a.valuationAsOf !== b.valuationAsOf) {
|
|
265
|
+
throw new FxContractError(`${op} requires the same valuation instant (同一估值时点): side A ${a.currency} as_of ${a.valuationAsOf} (fx ${a.fx.provider} ${a.fx.fact_id} ${a.fx.base}→${a.fx.quote} @${a.fx.rate}) vs side B ${b.currency} as_of ${b.valuationAsOf} (fx ${b.fx.provider} ${b.fx.fact_id} ${b.fx.base}→${b.fx.quote} @${b.fx.rate}) — 混时点比较显式拒绝`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** 比较(预算比较的唯一口径):两侧必须同为规范化值、同币种、同一估值时点。返回 -1/0/1。 */
|
|
269
|
+
export function compareMoney(a, b) {
|
|
270
|
+
assertSameComparisonBasis(a, b, 'money comparison');
|
|
271
|
+
if (a.amountMinor < b.amountMinor)
|
|
272
|
+
return -1;
|
|
273
|
+
if (a.amountMinor > b.amountMinor)
|
|
274
|
+
return 1;
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
/** 预算判定(验收 3 的比较口径):total ≤ budget,同一估值时点。 */
|
|
278
|
+
export function withinBudget(total, budget) {
|
|
279
|
+
return compareMoney(total, budget) <= 0;
|
|
280
|
+
}
|
|
281
|
+
/** 聚合(求和):非空、同币种、同一估值时点;返回保留该估值时点的规范化金额。 */
|
|
282
|
+
export function sumMoney(items) {
|
|
283
|
+
if (items.length === 0) {
|
|
284
|
+
throw new FxContractError('sumMoney requires at least one normalized amount (empty aggregate is not a fact)');
|
|
285
|
+
}
|
|
286
|
+
const [first, ...rest] = items;
|
|
287
|
+
for (const item of rest)
|
|
288
|
+
assertSameComparisonBasis(first, item, 'money aggregation');
|
|
289
|
+
let total = 0n;
|
|
290
|
+
for (const item of items)
|
|
291
|
+
total += item.amountMinor;
|
|
292
|
+
return { currency: first.currency, amountMinor: total, valuationAsOf: first.valuationAsOf, fx: first.fx };
|
|
293
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline administrative-division atlas loader (issue #342, mechanism only —
|
|
3
|
+
* default OFF, zero callers).
|
|
4
|
+
*
|
|
5
|
+
* Reads the script-built snapshot under ts/data/geo-atlas/<version>/ (built by
|
|
6
|
+
* scripts/build-geo-atlas.mjs from Natural Earth, public domain) and answers
|
|
7
|
+
* by-name / by-id boundary lookups with typed provenance. Read-only: this
|
|
8
|
+
* module performs zero network access (node:fs + node:crypto only) and is not
|
|
9
|
+
* imported anywhere in the product path; activation stays gated on the
|
|
10
|
+
* offline-first trigger per docs/data-sources.md §2 / §7.
|
|
11
|
+
*
|
|
12
|
+
* Semantics (fail-closed, three-valued — mirrors the data-sources L4 contract):
|
|
13
|
+
* - Same-name places → `ambiguous` with candidates; the loader never picks a
|
|
14
|
+
* winner (clarification belongs to the caller).
|
|
15
|
+
* - Boundary changes → the snapshot is frozen as-of `fetchedAt`/`version`;
|
|
16
|
+
* every result carries that provenance so no boundary is presented as live.
|
|
17
|
+
* - Unknown region → `unknown`: a miss inside the snapshot is not proof of
|
|
18
|
+
* nonexistence (admin-1 coverage is partial by design — 50m set).
|
|
19
|
+
* - Multilingual names → `nameZh` is surfaced when the source carries it;
|
|
20
|
+
* when absent only the English name is returned, never a fabricated
|
|
21
|
+
* translation (ADR-10: the LLM fabricates nothing, and neither do we).
|
|
22
|
+
* - Missing / tampered / corrupt / shape-drifted snapshots → load `error`
|
|
23
|
+
* (SHA-256 verified against the manifest; nothing degrades silently).
|
|
24
|
+
*/
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
26
|
+
import { readFile } from 'node:fs/promises';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
export const GEO_ATLAS_MANIFEST_SCHEMA = 'gotry_geo_atlas_manifest_v1';
|
|
29
|
+
function sha256(buffer) {
|
|
30
|
+
return createHash('sha256').update(buffer).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
/** NFKC + casefold + whitespace collapse so " cHina " ≡ "China". */
|
|
33
|
+
export function normalizeAtlasName(value) {
|
|
34
|
+
return value.normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
35
|
+
}
|
|
36
|
+
function isGeoAtlasLayer(value) {
|
|
37
|
+
return value === 'admin-0' || value === 'admin-1';
|
|
38
|
+
}
|
|
39
|
+
function parseManifest(text) {
|
|
40
|
+
const manifest = JSON.parse(text);
|
|
41
|
+
if (manifest.schemaVersion !== GEO_ATLAS_MANIFEST_SCHEMA) {
|
|
42
|
+
throw new Error(`schemaVersion ${JSON.stringify(manifest.schemaVersion)} ≠ ${GEO_ATLAS_MANIFEST_SCHEMA}`);
|
|
43
|
+
}
|
|
44
|
+
for (const key of ['dataset', 'version']) {
|
|
45
|
+
if (typeof manifest[key] !== 'string' || manifest[key] === '')
|
|
46
|
+
throw new Error(`field "${key}" missing`);
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length === 0) {
|
|
49
|
+
throw new Error('artifacts empty');
|
|
50
|
+
}
|
|
51
|
+
for (const artifact of manifest.artifacts) {
|
|
52
|
+
if (!isGeoAtlasLayer(String(artifact.layer)))
|
|
53
|
+
throw new Error(`artifact layer "${artifact.layer}" unknown`);
|
|
54
|
+
for (const key of ['file', 'upstreamUrl', 'sha256']) {
|
|
55
|
+
if (typeof artifact[key] !== 'string' || artifact[key] === '')
|
|
56
|
+
throw new Error(`artifact field "${key}" missing`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (typeof manifest.upstream?.repository !== 'string' || typeof manifest.license?.name !== 'string') {
|
|
60
|
+
throw new Error('upstream/license block missing');
|
|
61
|
+
}
|
|
62
|
+
return manifest;
|
|
63
|
+
}
|
|
64
|
+
function toRecord(feature, layer, layerProvenance) {
|
|
65
|
+
const properties = feature.properties ?? {};
|
|
66
|
+
const id = properties.id;
|
|
67
|
+
const name = properties.name;
|
|
68
|
+
if (typeof id !== 'string' || id === '')
|
|
69
|
+
throw new Error('feature without string "id"');
|
|
70
|
+
if (typeof name !== 'string' || name === '')
|
|
71
|
+
throw new Error(`feature "${id}" without string "name"`);
|
|
72
|
+
if (feature.geometry?.type !== 'MultiPolygon' || !Array.isArray(feature.geometry.coordinates)) {
|
|
73
|
+
throw new Error(`feature "${id}" geometry is not MultiPolygon`);
|
|
74
|
+
}
|
|
75
|
+
const coordinates = feature.geometry.coordinates;
|
|
76
|
+
if (coordinates.length === 0 || coordinates.some((polygon) => !Array.isArray(polygon) || polygon.length === 0)) {
|
|
77
|
+
throw new Error(`feature "${id}" geometry has no polygons`);
|
|
78
|
+
}
|
|
79
|
+
const optional = (key) => {
|
|
80
|
+
const value = properties[key];
|
|
81
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
name,
|
|
86
|
+
nameZh: optional('name_zh'),
|
|
87
|
+
admin: optional('admin'),
|
|
88
|
+
isoA2: optional('iso_a2'),
|
|
89
|
+
isoA3: optional('iso_a3'),
|
|
90
|
+
adm0A3: optional('adm0_a3'),
|
|
91
|
+
typeEn: optional('type_en'),
|
|
92
|
+
geometry: { type: 'MultiPolygon', coordinates },
|
|
93
|
+
provenance: layerProvenance,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function atlasError(reason, details) {
|
|
97
|
+
return { status: 'error', reason, details };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Load and verify an atlas snapshot. `root` is the version directory that
|
|
101
|
+
* contains manifest.json plus the artifact files it lists.
|
|
102
|
+
* Offline by construction: filesystem reads only, every artifact byte-verified
|
|
103
|
+
* against its manifest SHA-256 before indexing.
|
|
104
|
+
*/
|
|
105
|
+
export async function loadGeoAtlas(root) {
|
|
106
|
+
let manifestText;
|
|
107
|
+
try {
|
|
108
|
+
manifestText = await readFile(join(root, 'manifest.json'), 'utf8');
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const code = error.code;
|
|
112
|
+
return code === 'ENOENT'
|
|
113
|
+
? atlasError('manifest-missing', `no manifest.json under ${root}`)
|
|
114
|
+
: atlasError('manifest-unreadable', String(error.message));
|
|
115
|
+
}
|
|
116
|
+
let manifest;
|
|
117
|
+
try {
|
|
118
|
+
manifest = parseManifest(manifestText);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return error instanceof SyntaxError
|
|
122
|
+
? atlasError('manifest-corrupt', `manifest.json is not valid JSON: ${error.message}`)
|
|
123
|
+
: atlasError('manifest-invalid', error.message);
|
|
124
|
+
}
|
|
125
|
+
const snapshot = {
|
|
126
|
+
dataset: manifest.dataset,
|
|
127
|
+
version: manifest.version,
|
|
128
|
+
license: manifest.license.name,
|
|
129
|
+
upstream: manifest.upstream,
|
|
130
|
+
fetchedAt: manifest.build.fetchedAt,
|
|
131
|
+
};
|
|
132
|
+
const byId = new Map();
|
|
133
|
+
const byName = new Map();
|
|
134
|
+
const counts = new Map();
|
|
135
|
+
for (const artifact of manifest.artifacts) {
|
|
136
|
+
const layer = artifact.layer;
|
|
137
|
+
let bytes;
|
|
138
|
+
try {
|
|
139
|
+
bytes = await readFile(join(root, artifact.file));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return atlasError('artifact-missing', `${artifact.file} listed in manifest but absent`);
|
|
143
|
+
}
|
|
144
|
+
const digest = sha256(bytes);
|
|
145
|
+
if (digest !== artifact.sha256) {
|
|
146
|
+
return atlasError('artifact-sha-mismatch', `${artifact.file}: manifest ${artifact.sha256} ≠ actual ${digest}`);
|
|
147
|
+
}
|
|
148
|
+
let collection;
|
|
149
|
+
try {
|
|
150
|
+
collection = JSON.parse(bytes.toString('utf8'));
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return atlasError('artifact-corrupt', `${artifact.file} is not valid JSON: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
if (collection.type !== 'FeatureCollection' || !Array.isArray(collection.features)) {
|
|
156
|
+
return atlasError('artifact-invalid', `${artifact.file} is not a FeatureCollection`);
|
|
157
|
+
}
|
|
158
|
+
const layerProvenance = {
|
|
159
|
+
layer,
|
|
160
|
+
upstreamUrl: artifact.upstreamUrl,
|
|
161
|
+
upstreamSha256: artifact.upstreamSha256,
|
|
162
|
+
snapshotFile: artifact.file,
|
|
163
|
+
snapshotSha256: digest,
|
|
164
|
+
};
|
|
165
|
+
counts.set(layer, collection.features.length);
|
|
166
|
+
for (const feature of collection.features) {
|
|
167
|
+
let record;
|
|
168
|
+
try {
|
|
169
|
+
record = toRecord(feature, layer, layerProvenance);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return atlasError('artifact-invalid', `${artifact.file}: ${error.message}`);
|
|
173
|
+
}
|
|
174
|
+
const normalizedId = normalizeAtlasName(record.id);
|
|
175
|
+
if (byId.has(normalizedId)) {
|
|
176
|
+
return atlasError('duplicate-id', `"${record.id}" appears twice across the snapshot; id is not a stable key`);
|
|
177
|
+
}
|
|
178
|
+
const entry = {
|
|
179
|
+
layer,
|
|
180
|
+
id: record.id,
|
|
181
|
+
normalizedName: normalizeAtlasName(record.name),
|
|
182
|
+
normalizedZhName: record.nameZh ? normalizeAtlasName(record.nameZh) : null,
|
|
183
|
+
record,
|
|
184
|
+
};
|
|
185
|
+
byId.set(normalizedId, entry);
|
|
186
|
+
for (const key of [entry.normalizedName, entry.normalizedZhName]) {
|
|
187
|
+
if (key == null || key === '')
|
|
188
|
+
continue;
|
|
189
|
+
const bucket = byName.get(key);
|
|
190
|
+
if (bucket)
|
|
191
|
+
bucket.push(entry);
|
|
192
|
+
else
|
|
193
|
+
byName.set(key, [entry]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const atlas = {
|
|
198
|
+
version: manifest.version,
|
|
199
|
+
snapshot,
|
|
200
|
+
byId,
|
|
201
|
+
byName,
|
|
202
|
+
recordCount: (layer) => (layer ? (counts.get(layer) ?? 0) : byId.size),
|
|
203
|
+
};
|
|
204
|
+
return { status: 'ok', atlas };
|
|
205
|
+
}
|
|
206
|
+
const UNKNOWN_NOTE = 'not found in this snapshot; a miss here is not proof the place does not exist (admin-1 coverage is partial by design)';
|
|
207
|
+
function unknown(snapshot) {
|
|
208
|
+
return { status: 'unknown', snapshot, note: UNKNOWN_NOTE };
|
|
209
|
+
}
|
|
210
|
+
/** Exact id lookup (e.g. "CHN" admin-0, "CN-YN" admin-1). Offline, read-only. */
|
|
211
|
+
export function lookupById(atlas, id) {
|
|
212
|
+
const loaded = atlas;
|
|
213
|
+
const entry = loaded.byId.get(normalizeAtlasName(id));
|
|
214
|
+
if (!entry)
|
|
215
|
+
return unknown(loaded.snapshot);
|
|
216
|
+
return { status: 'hit', record: entry.record, snapshot: loaded.snapshot };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Name lookup against English and zh names; `options.layer` narrows to one
|
|
220
|
+
* layer before the hit/ambiguous decision. One match → hit; several distinct
|
|
221
|
+
* ids → ambiguous with candidates (the loader never picks a winner); none →
|
|
222
|
+
* unknown (three-valued).
|
|
223
|
+
*/
|
|
224
|
+
export function lookupByName(atlas, name, options) {
|
|
225
|
+
const loaded = atlas;
|
|
226
|
+
const key = normalizeAtlasName(name);
|
|
227
|
+
if (key === '')
|
|
228
|
+
return unknown(loaded.snapshot);
|
|
229
|
+
const matched = (loaded.byName.get(key) ?? []).filter((entry) => options?.layer ? entry.layer === options.layer : true);
|
|
230
|
+
if (matched.length === 0)
|
|
231
|
+
return unknown(loaded.snapshot);
|
|
232
|
+
const distinct = matched.filter((entry, index) => matched.findIndex((other) => other.record.id === entry.record.id) === index);
|
|
233
|
+
if (distinct.length === 1)
|
|
234
|
+
return { status: 'hit', record: distinct[0].record, snapshot: loaded.snapshot };
|
|
235
|
+
return {
|
|
236
|
+
status: 'ambiguous',
|
|
237
|
+
candidates: distinct.map((entry) => ({
|
|
238
|
+
layer: entry.layer,
|
|
239
|
+
id: entry.record.id,
|
|
240
|
+
name: entry.record.name,
|
|
241
|
+
admin: entry.record.admin,
|
|
242
|
+
})),
|
|
243
|
+
snapshot: loaded.snapshot,
|
|
244
|
+
note: 'same-name match in more than one place; clarify (country/parent or id) before use',
|
|
245
|
+
};
|
|
246
|
+
}
|