@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,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 实时票价 overlay 回归(FlyAI 实时桥 + 静态降级,三值语义;纯离线——查询注入,零网):
|
|
3
|
+
* 1. hit:dated 段按航班号精确匹配 → 价格覆写 + `[实时API:flyai@ts]` 证据链 + 静态原价留档;
|
|
4
|
+
* 2. error/miss:段原样保留(静态价即降级),降级证据行进 notes;
|
|
5
|
+
* 3. 非 YYYY-MM-DD 日期段(如「2026-07-31 或 08-01」):不查询、不触碰;
|
|
6
|
+
* 4. 无匹配航班号/打码价(NaN):不覆写不抛错;
|
|
7
|
+
* 5. 求解集成:覆写后的 spec 进 solveUnified 判定可算、钱不低于降价前。
|
|
8
|
+
*
|
|
9
|
+
* 运行(在 ts/ 下,零网):npx tsx scripts/realtime-pricing-tests.ts
|
|
10
|
+
*/
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { overlayRealtimeFlightPrices } from '../src/realtime-pricing.js';
|
|
15
|
+
import { parseFlightPackToSpec, solveUnified } from '../src/unified.js';
|
|
16
|
+
const packJson = JSON.parse(await readFile(join('..', 'data', 'flights_2026.json'), 'utf-8'));
|
|
17
|
+
function hit(no, price) {
|
|
18
|
+
const opt = {
|
|
19
|
+
no, name: '测试航司',
|
|
20
|
+
depDateTime: '2026-07-18 14:45:00', arrDateTime: '2026-07-18 17:20:00',
|
|
21
|
+
depStation: 'HKG', arrStation: 'HKT', durationMin: 155, price,
|
|
22
|
+
};
|
|
23
|
+
return { ok: true, via: 'flyai', verdict: 'hit', kind: 'flight', evidence: '[实时API:flyai@2026-08-29T06:00:00.000Z] hit', latencyMs: 1, options: [opt] };
|
|
24
|
+
}
|
|
25
|
+
function err() {
|
|
26
|
+
return { ok: false, via: 'flyai-error', verdict: 'error', kind: 'flight', evidence: '[实时API:flyai@error@2026-08-29T06:00:00.000Z] synthetic-timeout', latencyMs: 1, error: 'synthetic-timeout' };
|
|
27
|
+
}
|
|
28
|
+
const queries = [];
|
|
29
|
+
const query = async (q) => {
|
|
30
|
+
queries.push(q);
|
|
31
|
+
// f1 香港→普吉:仅 CX773 有实时价(1200),HX741 无条目(无匹配分支)
|
|
32
|
+
return q.origin === '香港' ? hit('CX773', 1200) : err(); // f5 深圳→迪拜:error 降级分支
|
|
33
|
+
};
|
|
34
|
+
// 五段链 spec:f1(HKG->HKT, 2026-07-18)、f5(SZX->DXB, 2026-08-10)进查询;
|
|
35
|
+
// f2/f3 日期为「2026-07-31 或 08-01」非严格形 → 词表/日期闸跳过;f4 无 arrive_by 仍是 dated。
|
|
36
|
+
const overlay = await overlayRealtimeFlightPrices(parseFlightPackToSpec(packJson), { query });
|
|
37
|
+
// 1. hit 覆写:仅精确匹配的 CX773 被覆写,静态原价留档在证据链里
|
|
38
|
+
assert.equal(overlay.matched.length, 1, '仅 CX773 命中覆写(1 条)');
|
|
39
|
+
const m = overlay.matched[0];
|
|
40
|
+
assert.equal(m.segment, 'f1');
|
|
41
|
+
assert.equal(m.option, 'CX773');
|
|
42
|
+
assert.equal(m.staticCny, 2300);
|
|
43
|
+
assert.equal(m.realtimeCny, 1200);
|
|
44
|
+
assert.ok(m.evidence.includes('[实时API:flyai@2026-08-29T06:00:00.000Z]'), '证据链带时间戳');
|
|
45
|
+
assert.ok(m.evidence.includes('静态包原价 ¥2300'), '静态原价留档');
|
|
46
|
+
const f1svc = (id) => overlay.spec.segments.find(s => s.id === 'f1').options.find(o => o.id === id).move.services[0].priceCny;
|
|
47
|
+
assert.equal(f1svc('CX773'), 1200, '实时价覆写进 spec');
|
|
48
|
+
assert.equal(f1svc('HX741'), 1600, '无匹配条目原价保留');
|
|
49
|
+
// 2. error 降级:f5 段原样 + 降级证据行
|
|
50
|
+
const f5svc = overlay.spec.segments.find(s => s.id === 'f5').options[0].move.services[0];
|
|
51
|
+
assert.equal(f5svc.id, 'EK329');
|
|
52
|
+
assert.equal(f5svc.priceCny, 3400, 'error 段不覆写');
|
|
53
|
+
assert.ok(overlay.notes.some(n => n.startsWith('f5:') && n.includes('未取回(error')), 'error 降级证据行存在');
|
|
54
|
+
// 3. 查询面:仅 dated + route 词表内城市对的段被查询(f1/f4/f5;f2/f3 非严格日期不查)
|
|
55
|
+
assert.equal(queries.length, 3, '仅 f1/f4/f5 被查询(f2/f3 非严格日期)');
|
|
56
|
+
assert.deepEqual(queries.map(q => q.origin), ['香港', '深圳', '昆明']);
|
|
57
|
+
// f4(昆明→深圳)error 降级:两班原价不动
|
|
58
|
+
const f4prices = overlay.spec.segments.find(s => s.id === 'f4').options.map(o => o.move.services[0].priceCny);
|
|
59
|
+
assert.deepEqual(f4prices, [1150, 980, 700], 'f4 error 段原价保留');
|
|
60
|
+
// 4. 求解集成:覆写后 spec 判定可算(价格只降,结果钱 ≤ 基线)
|
|
61
|
+
const baseline = await solveUnified(parseFlightPackToSpec(packJson));
|
|
62
|
+
const overlaid = await solveUnified(overlay.spec);
|
|
63
|
+
assert.equal(overlaid.feasible, true, '覆写后 spec 求解可行');
|
|
64
|
+
assert.ok(baseline.money_cny !== undefined && overlaid.money_cny !== undefined, '两条路径都出总价');
|
|
65
|
+
assert.ok(overlaid.money_cny <= baseline.money_cny, '实时价不抬高求解总钱');
|
|
66
|
+
// 5. 无 dated 段 spec:零查询零改动(结构化直验)
|
|
67
|
+
const bare = await overlayRealtimeFlightPrices({
|
|
68
|
+
note: 'no-dates',
|
|
69
|
+
segments: [{
|
|
70
|
+
id: 'dest', role: 'choice',
|
|
71
|
+
options: [{ id: 'a', label: 'x', move: { hub: 'SZX', services: [{ id: 'EK329', depMin: 45, arrMin: 260, priceCny: 3400 }], bufferMin: 30, originTransferMin: 0, destTransferMin: 30 } }],
|
|
72
|
+
}],
|
|
73
|
+
});
|
|
74
|
+
assert.equal(bare.matched.length, 0, '无 dated 段 → 不查询');
|
|
75
|
+
assert.equal(bare.spec.segments[0].options[0].move.services[0].priceCny, 3400, '价格原样');
|
|
76
|
+
console.log('\nREALTIME PRICING TESTS: 5/5 OK(hit 覆写+证据/error 降级/日期词表闸/求解集成/无段零触碰)');
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall contract 验收套件(issue #577,Phase D):
|
|
3
|
+
* tick source(InMemory + PeriodicTickSource opt-in 纪律)
|
|
4
|
+
* + evaluator(5 类 RecallReason 闭集 + 广播/定向匹配)
|
|
5
|
+
* + why-now card(source tag 必现 + 行动建议)
|
|
6
|
+
* + integration(wish-pool 只读编排)
|
|
7
|
+
* + RecallTickScheduler 端到端(tick → 评估 → 卡 → sink)。
|
|
8
|
+
*
|
|
9
|
+
* 全离线合成数据,无网络、无 IO、无 stateRoot 写。产品代码零 setInterval 激活;
|
|
10
|
+
* 测试本身启动有界、用后即 stop 的 interval(验证 PeriodicTickSource 契约)。
|
|
11
|
+
*
|
|
12
|
+
* 运行(在 ts/ 下):npx tsx scripts/recall-tests.ts
|
|
13
|
+
*/
|
|
14
|
+
import { ArrayRecallSink, InMemoryTickSource, PeriodicTickSource, RecallTickScheduler, } from '../src/recall/tick.js';
|
|
15
|
+
import { RECALL_REASON_LABEL, RECALL_REASONS, evaluateRecallTriggers, } from '../src/recall/evaluator.js';
|
|
16
|
+
import { buildWhyNowCard, renderWhyNowCardLine } from '../src/recall/card.js';
|
|
17
|
+
import { evaluatePoolRecall } from '../src/recall/integration.js';
|
|
18
|
+
let pass = 0;
|
|
19
|
+
let fail = 0;
|
|
20
|
+
const failures = [];
|
|
21
|
+
function ok(cond, msg) {
|
|
22
|
+
if (cond) {
|
|
23
|
+
pass++;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
fail++;
|
|
27
|
+
failures.push(msg);
|
|
28
|
+
}
|
|
29
|
+
const NOW = new Date('2026-09-24T08:00:00.000Z');
|
|
30
|
+
const TICK = { at: NOW, source: 'test-tick' };
|
|
31
|
+
const POOL = [
|
|
32
|
+
{ wish_id: 'w-dali-erhai', name: '大理 · 洱海恢复之旅', conditions: { days: 5, budget_cny: 5000 }, added_at: '2026-07-01T00:00:00Z' },
|
|
33
|
+
{ wish_id: 'w-qiandao', name: '千岛湖周末', conditions: { days: 2, budget_cny: 2000 }, added_at: '2026-08-15T00:00:00Z' },
|
|
34
|
+
{ wish_id: 'w-nocond', name: '无条件的愿望(条件门不召回)', added_at: '2026-09-20T00:00:00Z' },
|
|
35
|
+
{ wish_id: 'w-muted', name: '静音的愿望', muted: true, conditions: { days: 2 }, added_at: '2026-06-01T00:00:00Z' },
|
|
36
|
+
{ wish_id: '', name: '无 id 的愿望(不召回)', conditions: { days: 2 }, added_at: '2026-06-01T00:00:00Z' },
|
|
37
|
+
];
|
|
38
|
+
/** 召回窗口上下文(§6/§7 条件资格门用)。days=7 使 POOL 全部带条件条目均命中
|
|
39
|
+
* (含 muted/无 id 的 days:2)——它们被排除只能归因于过滤,而非条件不命中。 */
|
|
40
|
+
const MATCH_CTX = { days: 7, budgetCny: 6000, month: 10 };
|
|
41
|
+
const SIGNAL_HOLIDAY = {
|
|
42
|
+
reason: 'holiday_proximity',
|
|
43
|
+
source: 'holiday-calendar',
|
|
44
|
+
current_value: '2026-10-01(距今 7 天)',
|
|
45
|
+
threshold: '距假期 ≤14 天',
|
|
46
|
+
};
|
|
47
|
+
const SIGNAL_PRICE = {
|
|
48
|
+
reason: 'price_drop',
|
|
49
|
+
source: 'price-monitor',
|
|
50
|
+
current_value: 'SZX→HKT ¥890',
|
|
51
|
+
threshold: '≤¥1000',
|
|
52
|
+
wish_ids: ['w-dali-erhai'],
|
|
53
|
+
};
|
|
54
|
+
const SIGNAL_WEATHER = {
|
|
55
|
+
reason: 'weather_window',
|
|
56
|
+
source: 'weather-forecast',
|
|
57
|
+
current_value: '大理 10/01-10/05 晴 18-24°C',
|
|
58
|
+
threshold: '连续 5 天适旅',
|
|
59
|
+
wish_ids: ['w-dali-erhai'],
|
|
60
|
+
};
|
|
61
|
+
const SIGNAL_ROUTE = {
|
|
62
|
+
reason: 'route_new',
|
|
63
|
+
source: 'schedule-update',
|
|
64
|
+
current_value: 'G7315 新增 10/01 班次',
|
|
65
|
+
threshold: '覆盖目标日期',
|
|
66
|
+
};
|
|
67
|
+
const SIGNAL_RECOVERED = {
|
|
68
|
+
reason: 'availability_recovered',
|
|
69
|
+
source: 'channel-health',
|
|
70
|
+
current_value: 'session:ctrip-flight 恢复 ok',
|
|
71
|
+
threshold: '此前 down 的通道恢复',
|
|
72
|
+
};
|
|
73
|
+
// ===========================================================================
|
|
74
|
+
// §1 tick source — InMemory 顺序 + PeriodicTickSource 默认关闭(opt-in 纪律)
|
|
75
|
+
// ===========================================================================
|
|
76
|
+
{
|
|
77
|
+
const src = new InMemoryTickSource([
|
|
78
|
+
{ at: NOW, source: 't1' },
|
|
79
|
+
{ at: new Date(NOW.getTime() + 60_000), source: 't2' },
|
|
80
|
+
]);
|
|
81
|
+
const a = await src.next();
|
|
82
|
+
const b = await src.next();
|
|
83
|
+
const c = await src.next();
|
|
84
|
+
ok(a?.source === 't1' && b?.source === 't2', '§1a InMemoryTickSource 按序返回');
|
|
85
|
+
ok(c === null, '§1b 耗尽后返回 null');
|
|
86
|
+
// PeriodicTickSource 默认关闭
|
|
87
|
+
const periodic = new PeriodicTickSource({ intervalMs: 1000 });
|
|
88
|
+
ok(periodic.enabled === false, '§1c PeriodicTickSource 默认 enabled=false(opt-in 纪律)');
|
|
89
|
+
ok(periodic.start() === 'disabled', "§1d enabled=false 时 start() 返回 'disabled'(不启动)");
|
|
90
|
+
// review2 #2:未运行(disabled/未 start)的 next() 返回 null(流终结,永不悬挂)
|
|
91
|
+
const disabledNext = await periodic.next();
|
|
92
|
+
ok(disabledNext === null, '§1d0 未运行源的 next() 返回 null(drain-loop 安全退出)');
|
|
93
|
+
periodic.stop(); // 幂等停止
|
|
94
|
+
// review #3:start() 三态——已启动的实例重复 start 返回 already-running(不混同 disabled)
|
|
95
|
+
const running = new PeriodicTickSource({ intervalMs: 10, enabled: true, source: 'rt' });
|
|
96
|
+
ok(running.start() === 'started', "§1d2 首次 start → 'started'");
|
|
97
|
+
ok(running.start() === 'already-running', "§1d3 重复 start → 'already-running'(与 disabled 不混同)");
|
|
98
|
+
// review #1:next() 阻塞语义——10ms 间隔,1s 超时护栏内必须等到真 tick
|
|
99
|
+
const gotTick = await Promise.race([
|
|
100
|
+
running.next(),
|
|
101
|
+
new Promise(resolve => setTimeout(() => resolve(null), 1000)),
|
|
102
|
+
]);
|
|
103
|
+
ok(gotTick !== null && gotTick.source === 'rt', '§1d4 next() 阻塞到下一个 tick(drain-loop 安全)');
|
|
104
|
+
running.stop();
|
|
105
|
+
// review #5:pending 有界——1ms 间隔 + maxPending=3,不消费,等 200ms,队列恰满 3(上限真到达)
|
|
106
|
+
const capped = new PeriodicTickSource({ intervalMs: 1, enabled: true, maxPending: 3, source: 'cap' });
|
|
107
|
+
capped.start();
|
|
108
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
109
|
+
ok(capped.pendingLength === 3, `§1d5 pending 恰满 maxPending=3(实测 ${capped.pendingLength};上限真到达,一侧不等式防 vacuous pass)`);
|
|
110
|
+
// review2 #3:stop() 清空 pending(重启不吐陈旧 tick)
|
|
111
|
+
capped.stop();
|
|
112
|
+
ok(capped.pendingLength === 0, `§1d5b stop() 清空 pending(实测 ${capped.pendingLength};重启不吐陈旧 tick)`);
|
|
113
|
+
// review2 #2:stop() 把等待者以 null 唤醒(消费协程不泄漏)
|
|
114
|
+
const flusher = new PeriodicTickSource({ intervalMs: 10_000, enabled: true, source: 'fl' });
|
|
115
|
+
flusher.start();
|
|
116
|
+
const hangingNext = flusher.next(); // 先挂起一个等待者(不 await)
|
|
117
|
+
await new Promise(resolve => setTimeout(resolve, 30)); // 确保 waiter 已注册
|
|
118
|
+
flusher.stop();
|
|
119
|
+
const flushed = await Promise.race([hangingNext, new Promise(r => setTimeout(() => r('timeout'), 500))]);
|
|
120
|
+
ok(flushed === null, `§1d5c stop() 把等待者以 null 唤醒(实测 ${String(flushed)};协程不泄漏)`);
|
|
121
|
+
// review #5b:maxPending 非法 → 构造抛错
|
|
122
|
+
let badMax = false;
|
|
123
|
+
try {
|
|
124
|
+
new PeriodicTickSource({ intervalMs: 100, enabled: true, maxPending: 0 });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
badMax = true;
|
|
128
|
+
}
|
|
129
|
+
ok(badMax, '§1d6 maxPending=0 → 构造抛错(fail-closed)');
|
|
130
|
+
// review #7:InMemoryTickSource 防御式深拷贝——改返回的 tick.at 不污染队列
|
|
131
|
+
const mem = new InMemoryTickSource([{ at: new Date(1000), source: 'dc' }, { at: new Date(2000), source: 'dc2' }]);
|
|
132
|
+
const first = await mem.next();
|
|
133
|
+
if (first)
|
|
134
|
+
first.at.setTime(999999); // 恶意改副本
|
|
135
|
+
const second = await mem.next();
|
|
136
|
+
ok(second !== null && second.at.getTime() === 2000, `§1d7 InMemory 深拷贝:改返回副本不污染队列(第二个 at=${second?.at.getTime()})`);
|
|
137
|
+
// intervalMs 非法 → 构造抛错
|
|
138
|
+
let badInterval = false;
|
|
139
|
+
try {
|
|
140
|
+
new PeriodicTickSource({ intervalMs: -1 });
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
badInterval = true;
|
|
144
|
+
}
|
|
145
|
+
ok(badInterval, '§1f intervalMs 非法 → 构造抛错(fail-closed)');
|
|
146
|
+
// ArrayRecallSink 收集
|
|
147
|
+
const sink = new ArrayRecallSink();
|
|
148
|
+
sink.deliver({ x: 1 });
|
|
149
|
+
sink.deliver({ x: 2 });
|
|
150
|
+
ok(sink.items.length === 2 && sink.items[0].x === 1, '§1g ArrayRecallSink 按序收集');
|
|
151
|
+
}
|
|
152
|
+
// ===========================================================================
|
|
153
|
+
// §2 evaluator — 闭集 + 广播/定向 + muted/无 id 过滤 + 无信号空返回
|
|
154
|
+
// ===========================================================================
|
|
155
|
+
{
|
|
156
|
+
ok(RECALL_REASONS.length === 5, `§2a RecallReason 闭集 5 类(实测 ${RECALL_REASONS.length})`);
|
|
157
|
+
ok(RECALL_REASON_LABEL.holiday_proximity === '假期临近', '§2b reason label 中文标签');
|
|
158
|
+
// 无信号 → 空触发
|
|
159
|
+
const empty = evaluateRecallTriggers(POOL, { now: NOW, match_context: MATCH_CTX, signals: [] });
|
|
160
|
+
ok(empty.length === 0, '§2c 无信号 → 空触发(不硬推)');
|
|
161
|
+
// 广播信号(holiday_proximity 无 wish_ids)→ 匹配全部未 muted 且有 id 的候选
|
|
162
|
+
const broadcast = evaluateRecallTriggers(POOL, { now: NOW, match_context: MATCH_CTX, signals: [SIGNAL_HOLIDAY] });
|
|
163
|
+
ok(broadcast.length === 2, `§2d 广播信号命中 2 个有效候选(实测 ${broadcast.length};muted + 无 id 被滤)`);
|
|
164
|
+
ok(broadcast.every(t => t.wish_id === 'w-dali-erhai' || t.wish_id === 'w-qiandao'), '§2e 广播信号命中的 id 都是有效候选(muted/无 id 不出现)');
|
|
165
|
+
ok(broadcast.every(t => t.signal.reason === 'holiday_proximity'), '§2f 触发携带原信号 reason');
|
|
166
|
+
ok(broadcast.every(t => t.evaluated_at === NOW.toISOString()), '§2g 触发携带评估时刻(ISO)');
|
|
167
|
+
// 定向信号(price_drop 带 wish_ids)→ 只命中指定 id
|
|
168
|
+
const targeted = evaluateRecallTriggers(POOL, { now: NOW, match_context: MATCH_CTX, signals: [SIGNAL_PRICE] });
|
|
169
|
+
ok(targeted.length === 1 && targeted[0].wish_id === 'w-dali-erhai', '§2h 定向信号只命中指定 wish_id(其它候选不出现)');
|
|
170
|
+
// 多信号 → 每个信号独立触发(一个 wish 可命中多个信号)
|
|
171
|
+
const multi = evaluateRecallTriggers(POOL, { now: NOW, match_context: MATCH_CTX, signals: [SIGNAL_PRICE, SIGNAL_WEATHER] });
|
|
172
|
+
ok(multi.length === 2, `§2i 两个定向信号(同 wish)→ 2 个触发(实测 ${multi.length})`);
|
|
173
|
+
ok(multi.every(t => t.wish_id === 'w-dali-erhai'), '§2j 同 wish 多信号 → 各自独立触发');
|
|
174
|
+
ok(multi[0].signal.reason !== multi[1].signal.reason, '§2k 触发按信号顺序(reason 不同)');
|
|
175
|
+
// 全 5 类信号可达
|
|
176
|
+
const allSignals = [SIGNAL_HOLIDAY, SIGNAL_PRICE, SIGNAL_WEATHER, SIGNAL_ROUTE, SIGNAL_RECOVERED];
|
|
177
|
+
const allFired = evaluateRecallTriggers(POOL, { now: NOW, match_context: MATCH_CTX, signals: allSignals });
|
|
178
|
+
const firedReasons = new Set(allFired.map(t => t.signal.reason));
|
|
179
|
+
ok(firedReasons.size === 5 && RECALL_REASONS.every(r => firedReasons.has(r)), `§2l 5 类 RecallReason 全部可达(实际触发 ${firedReasons.size} 类)`);
|
|
180
|
+
// review2 #1:wish_ids 为字符串(真值非数组)→ 整条畸形跳过,绝不降级广播
|
|
181
|
+
const stringIds = evaluateRecallTriggers(POOL, { now: NOW, match_context: MATCH_CTX, signals: [{ ...SIGNAL_PRICE, wish_ids: 'w-dali-erhai' }] });
|
|
182
|
+
ok(stringIds.length === 0, `§2m0 wish_ids 非数组 → 整条跳过(实测 ${stringIds.length} 触发;绝不广播扩权)`);
|
|
183
|
+
// review2 #14:muted 为 'false' 字符串(JSON 往返产物)→ 视为未静音(严格 true 才排除)
|
|
184
|
+
const stringMutedPool = [{ ...POOL[0], muted: 'false' }, { ...POOL[1], muted: 'false' }];
|
|
185
|
+
const stringMuted = evaluateRecallTriggers(stringMutedPool, { now: NOW, match_context: MATCH_CTX, signals: [SIGNAL_HOLIDAY] });
|
|
186
|
+
ok(stringMuted.length === 2, `§2m1 muted='false' 字符串 → 未静音照常召回(实测 ${stringMuted.length};严格 muted===true 才排除)`);
|
|
187
|
+
// review2 #6:容器/时钟守卫——坏时钟/非数组容器 → 空返回不抛
|
|
188
|
+
let clockThrew = false;
|
|
189
|
+
try {
|
|
190
|
+
evaluateRecallTriggers(POOL, { now: new Date('garbage'), match_context: MATCH_CTX, signals: [SIGNAL_HOLIDAY] });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
clockThrew = true;
|
|
194
|
+
}
|
|
195
|
+
ok(!clockThrew, '§2m2 ctx.now 非法 → 空返回不抛(toISOString 前置守卫)');
|
|
196
|
+
let containerThrew = false;
|
|
197
|
+
try {
|
|
198
|
+
evaluateRecallTriggers(POOL, { now: NOW, match_context: MATCH_CTX, signals: undefined });
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
containerThrew = true;
|
|
202
|
+
}
|
|
203
|
+
ok(!containerThrew, '§2m3 signals 非数组 → 空返回不抛(容器守卫)');
|
|
204
|
+
// 畸形信号(reason 不在闭集 / source 空 / 字段缺失)→ 静默跳过(不崩、不产触发)
|
|
205
|
+
const badSignals = [
|
|
206
|
+
{ reason: 'not_a_reason', source: 'x', current_value: 'y', threshold: 'z' },
|
|
207
|
+
{ reason: 'price_drop', source: '', current_value: 'y', threshold: 'z' },
|
|
208
|
+
{ reason: 'price_drop', source: 'x' },
|
|
209
|
+
null,
|
|
210
|
+
'not-an-object',
|
|
211
|
+
];
|
|
212
|
+
const badResult = evaluateRecallTriggers(POOL, { now: NOW, match_context: MATCH_CTX, signals: badSignals });
|
|
213
|
+
ok(badResult.length === 0, `§2m 畸形信号全部跳过(实测产 ${badResult.length} 触发;不崩不产)`);
|
|
214
|
+
// 空 pool → 任何信号都空
|
|
215
|
+
const emptyPool = evaluateRecallTriggers([], { now: NOW, match_context: MATCH_CTX, signals: [SIGNAL_HOLIDAY] });
|
|
216
|
+
ok(emptyPool.length === 0, '§2n 空 pool → 空触发');
|
|
217
|
+
}
|
|
218
|
+
// ===========================================================================
|
|
219
|
+
// §3 why-now card — source tag 必现 + 行动建议 + 标题封闭词汇
|
|
220
|
+
// ===========================================================================
|
|
221
|
+
{
|
|
222
|
+
const [trigger] = evaluateRecallTriggers(POOL, { now: NOW, match_context: MATCH_CTX, signals: [SIGNAL_PRICE] });
|
|
223
|
+
ok(trigger !== undefined, '§3a 前置:定向触发存在');
|
|
224
|
+
const card = buildWhyNowCard(trigger);
|
|
225
|
+
ok(card.title === '现在可以去了:大理 · 洱海恢复之旅', `§3b 标题封闭词汇(实测「${card.title}」)`);
|
|
226
|
+
ok(card.reason === 'price_drop' && card.reason_label === '价格降至阈值', '§3c reason + 中文标签');
|
|
227
|
+
ok(card.current_value === 'SZX→HKT ¥890' && card.threshold === '≤¥1000', '§3d 当前值 + 阈值保持');
|
|
228
|
+
ok(typeof card.action_hint === 'string' && card.action_hint.length > 0, `§3e 行动建议非空(${card.action_hint})`);
|
|
229
|
+
ok(card.source_tag === '[source:price-monitor]', `§3f source tag 必现(实测 ${card.source_tag})`);
|
|
230
|
+
ok(card.evaluated_at === NOW.toISOString(), '§3g evaluated_at 保持');
|
|
231
|
+
ok(card.evidence_boundary === true, '§3h evidence_boundary 恒 true');
|
|
232
|
+
// review2 #4:卡构造边界强制——source 空 / reason 出集 → 抛错(「无信源卡构造不可能」的字面兑现)
|
|
233
|
+
let badSourceThrew = false;
|
|
234
|
+
try {
|
|
235
|
+
buildWhyNowCard({ wish_id: 'x', wish_name: 'x', signal: { ...SIGNAL_PRICE, source: '' }, match_hits: [], evaluated_at: NOW.toISOString() });
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
badSourceThrew = true;
|
|
239
|
+
}
|
|
240
|
+
ok(badSourceThrew, "§3h2 buildWhyNowCard source='' → 抛错(边界强制 provenance)");
|
|
241
|
+
let badReasonThrew = false;
|
|
242
|
+
try {
|
|
243
|
+
buildWhyNowCard({ wish_id: 'x', wish_name: 'x', signal: { ...SIGNAL_PRICE, reason: 'bogus' }, match_hits: [], evaluated_at: NOW.toISOString() });
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
badReasonThrew = true;
|
|
247
|
+
}
|
|
248
|
+
ok(badReasonThrew, '§3h3 buildWhyNowCard reason 出闭集 → 抛错');
|
|
249
|
+
// review2 #11:5 类行动建议——评估器必须真触发(去 fallback,回归即红)
|
|
250
|
+
const reasonCards = [];
|
|
251
|
+
for (const s of [SIGNAL_HOLIDAY, SIGNAL_PRICE, SIGNAL_WEATHER, SIGNAL_ROUTE, SIGNAL_RECOVERED]) {
|
|
252
|
+
const fired = evaluateRecallTriggers(POOL, { now: NOW, match_context: MATCH_CTX, signals: [s] });
|
|
253
|
+
ok(fired.length >= 1, `§3i0 前置:信号 ${s.reason} 真触发(实测 ${fired.length};无 fallback 掩盖)`);
|
|
254
|
+
reasonCards.push(buildWhyNowCard(fired[0]));
|
|
255
|
+
}
|
|
256
|
+
ok(reasonCards.every(c => c.action_hint.length > 5), '§3i 5 类 reason 的行动建议全部非空');
|
|
257
|
+
// 单行渲染含 source tag
|
|
258
|
+
const line = renderWhyNowCardLine(card);
|
|
259
|
+
ok(line.includes(card.title) && line.includes(card.reason_label) && line.includes(card.source_tag), `§3j 单行渲染含标题/原因/source tag(${line.slice(0, 60)}...)`);
|
|
260
|
+
ok(line.includes('当前:') && line.includes('阈值:'), '§3k 单行渲染含当前值/阈值');
|
|
261
|
+
// review2 #8:渲染契约——单行渲染必须含证据边界标记
|
|
262
|
+
ok(line.includes('证据边界'), '§3k2 单行渲染含证据边界标记(渲染面必须展示 evidence_boundary)');
|
|
263
|
+
}
|
|
264
|
+
// ===========================================================================
|
|
265
|
+
// §4 integration — wish-pool 只读编排(tick + signals → triggers + cards)
|
|
266
|
+
// ===========================================================================
|
|
267
|
+
{
|
|
268
|
+
const result = evaluatePoolRecall({
|
|
269
|
+
pool: POOL,
|
|
270
|
+
tick: TICK,
|
|
271
|
+
match_context: MATCH_CTX,
|
|
272
|
+
signals: [SIGNAL_HOLIDAY, SIGNAL_PRICE],
|
|
273
|
+
});
|
|
274
|
+
ok(result.triggers.length === 3, `§4a 触发数 = 广播(2)+ 定向(1)= 3(实测 ${result.triggers.length})`);
|
|
275
|
+
ok(result.cards.length === result.triggers.length, '§4b 卡数与触发数一致(一卡一触发)');
|
|
276
|
+
ok(result.cards.every(c => c.source_tag.startsWith('[source:')), '§4c 每张卡都带 source tag');
|
|
277
|
+
// 无信号 → 全空
|
|
278
|
+
const emptyResult = evaluatePoolRecall({ pool: POOL, tick: TICK, match_context: MATCH_CTX, signals: [] });
|
|
279
|
+
ok(emptyResult.triggers.length === 0 && emptyResult.cards.length === 0, '§4d 无信号 → 空(不硬推)');
|
|
280
|
+
}
|
|
281
|
+
// ===========================================================================
|
|
282
|
+
// §5 RecallTickScheduler 端到端 — tick → 评估 → 卡 → sink
|
|
283
|
+
// ===========================================================================
|
|
284
|
+
{
|
|
285
|
+
const sink = new ArrayRecallSink();
|
|
286
|
+
const scheduler = new RecallTickScheduler({
|
|
287
|
+
evaluate: (tick) => tick.source === 'test-tick'
|
|
288
|
+
? evaluatePoolRecall({ pool: POOL, tick, match_context: MATCH_CTX, signals: [SIGNAL_PRICE] }).triggers
|
|
289
|
+
.map(t => ({ wish_id: t.wish_id, signal: t.signal, match_hits: t.match_hits }))
|
|
290
|
+
: [], // 非 test-tick 的 tick 无信号(§5e 用)
|
|
291
|
+
toCard: (evaluation, tick) => buildWhyNowCard({
|
|
292
|
+
wish_id: evaluation.wish_id,
|
|
293
|
+
wish_name: POOL.find(p => p.wish_id === evaluation.wish_id)?.name ?? evaluation.wish_id,
|
|
294
|
+
signal: evaluation.signal,
|
|
295
|
+
match_hits: evaluation.match_hits, // #577 收口:真命中证据贯穿到卡
|
|
296
|
+
evaluated_at: tick.at.toISOString(), // review #2:取 tick.at,不用外层 NOW
|
|
297
|
+
}),
|
|
298
|
+
sink,
|
|
299
|
+
});
|
|
300
|
+
const count = await scheduler.run(TICK);
|
|
301
|
+
ok(count.delivered === 1 && count.failed === 0, `§5a 一个 tick + 一个定向信号 → 1 张卡(实测 delivered=${count.delivered}/failed=${count.failed})`);
|
|
302
|
+
ok(sink.items.length === 1, '§5b sink 收到 1 张卡');
|
|
303
|
+
const delivered = sink.items[0];
|
|
304
|
+
ok(delivered.source_tag === '[source:price-monitor]', '§5c 卡片 source tag 正确');
|
|
305
|
+
ok(delivered.title.includes('大理'), `§5d 卡片标题含 wish name(${delivered.title})`);
|
|
306
|
+
// 第二个 tick + 无信号 → 0 卡
|
|
307
|
+
const count2 = await scheduler.run({ at: new Date(NOW.getTime() + 3600_000), source: 't2' });
|
|
308
|
+
ok(count2.delivered === 0 && sink.items.length === 1, '§5e 第二 tick 无信号 → 0 新卡(sink 不变)');
|
|
309
|
+
// review #4:sink.deliver 逐卡容错——一个抛错不弃余下,返部分计数
|
|
310
|
+
const flakySink = {
|
|
311
|
+
deliver: (card) => {
|
|
312
|
+
const c = card;
|
|
313
|
+
if (c.title.includes('千岛湖'))
|
|
314
|
+
throw new Error('sink exploded'); // 第二张卡炸
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
const flakyScheduler = new RecallTickScheduler({
|
|
318
|
+
evaluate: (tick) => evaluateRecallTriggers(POOL, { now: tick.at, match_context: MATCH_CTX, signals: [SIGNAL_HOLIDAY] })
|
|
319
|
+
.map(t => ({ wish_id: t.wish_id, signal: t.signal, match_hits: t.match_hits })),
|
|
320
|
+
toCard: (evaluation, tick) => buildWhyNowCard({
|
|
321
|
+
wish_id: evaluation.wish_id,
|
|
322
|
+
wish_name: POOL.find(p => p.wish_id === evaluation.wish_id)?.name ?? evaluation.wish_id,
|
|
323
|
+
signal: evaluation.signal,
|
|
324
|
+
match_hits: evaluation.match_hits,
|
|
325
|
+
evaluated_at: tick.at.toISOString(),
|
|
326
|
+
}),
|
|
327
|
+
sink: flakySink,
|
|
328
|
+
});
|
|
329
|
+
const flakyResult = await flakyScheduler.run(TICK);
|
|
330
|
+
ok(flakyResult.delivered === 1 && flakyResult.failed === 1, `§5e2 deliver 逐卡容错:1 成功 1 失败不外泄(实测 ${flakyResult.delivered}/${flakyResult.failed})`);
|
|
331
|
+
ok(Array.isArray(flakyResult.errors) && flakyResult.errors.length > 0 && flakyResult.errors[0].includes('deliver'), `§5e3 失败原因记入 errors 数组(实测 ${JSON.stringify(flakyResult.errors)})`);
|
|
332
|
+
// review2 #5:toCard 抛错 → 同样逐卡容错(failed 计数,不杀 run)
|
|
333
|
+
const badCardScheduler = new RecallTickScheduler({
|
|
334
|
+
evaluate: (tick) => evaluateRecallTriggers(POOL, { now: tick.at, match_context: MATCH_CTX, signals: [SIGNAL_PRICE] })
|
|
335
|
+
.map(tr => ({ wish_id: tr.wish_id, signal: tr.signal, match_hits: tr.match_hits })),
|
|
336
|
+
toCard: () => { throw new Error('card exploded'); },
|
|
337
|
+
sink: new ArrayRecallSink(),
|
|
338
|
+
});
|
|
339
|
+
const badCardResult = await badCardScheduler.run(TICK);
|
|
340
|
+
ok(badCardResult.delivered === 0 && badCardResult.failed === 1, `§5e4 toCard 抛错 → failed 计数不杀 run(实测 ${badCardResult.delivered}/${badCardResult.failed})`);
|
|
341
|
+
// review2 #5:evaluate 抛错 → 空结果 + error 记录(不杀 drain loop)
|
|
342
|
+
const badEvalScheduler = new RecallTickScheduler({
|
|
343
|
+
evaluate: () => { throw new Error('eval exploded'); },
|
|
344
|
+
toCard: (e) => e,
|
|
345
|
+
sink: new ArrayRecallSink(),
|
|
346
|
+
});
|
|
347
|
+
const badEvalResult = await badEvalScheduler.run(TICK);
|
|
348
|
+
ok(badEvalResult.delivered === 0 && badEvalResult.failed === 0 && badEvalResult.errors?.[0]?.includes('evaluate') === true, `§5e5 evaluate 抛错 → {0,0,errors} 不外泄(实测 ${JSON.stringify(badEvalResult)})`);
|
|
349
|
+
// InMemoryTickSource 驱动多 tick 端到端
|
|
350
|
+
const multiSink = new ArrayRecallSink();
|
|
351
|
+
const multiScheduler = new RecallTickScheduler({
|
|
352
|
+
evaluate: evaluatePoolRecallShim,
|
|
353
|
+
toCard: toCardShim,
|
|
354
|
+
sink: multiSink,
|
|
355
|
+
});
|
|
356
|
+
const src = new InMemoryTickSource([TICK, { at: new Date(NOW.getTime() + 60_000), source: 't2' }]);
|
|
357
|
+
let total = 0;
|
|
358
|
+
for (let t = await src.next(); t !== null; t = await src.next()) {
|
|
359
|
+
const r = await multiScheduler.run(t);
|
|
360
|
+
total += r.delivered;
|
|
361
|
+
}
|
|
362
|
+
ok(total === 2 && multiSink.items.length === 2, `§5f InMemory 多 tick 端到端:2 tick × 1 信号 → 2 卡(实测 ${total}/${multiSink.items.length})`);
|
|
363
|
+
// review #2:第二张卡的 evaluated_at 必须是第二 tick 的时刻(NOW+60s),不是外层 NOW
|
|
364
|
+
const secondCard = multiSink.items[1];
|
|
365
|
+
ok(secondCard.evaluated_at === new Date(NOW.getTime() + 60_000).toISOString(), `§5f2 第二张卡 evaluated_at = 第二 tick 时刻(实测 ${secondCard.evaluated_at})`);
|
|
366
|
+
function evaluatePoolRecallShim(tick) {
|
|
367
|
+
return evaluatePoolRecall({ pool: POOL, tick, match_context: MATCH_CTX, signals: [SIGNAL_PRICE] }).triggers
|
|
368
|
+
.map(t => ({ wish_id: t.wish_id, signal: t.signal, match_hits: t.match_hits }));
|
|
369
|
+
}
|
|
370
|
+
function toCardShim(evaluation, tick) {
|
|
371
|
+
return buildWhyNowCard({
|
|
372
|
+
wish_id: evaluation.wish_id,
|
|
373
|
+
wish_name: POOL.find(p => p.wish_id === evaluation.wish_id)?.name ?? evaluation.wish_id,
|
|
374
|
+
signal: evaluation.signal,
|
|
375
|
+
match_hits: evaluation.match_hits,
|
|
376
|
+
evaluated_at: tick.at.toISOString(),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// ===========================================================================
|
|
381
|
+
// §6 条件资格门(#577 收口):信号不能绕过 wish 条件——
|
|
382
|
+
// 资格 = scoreWishMatch(原样复用)非空(任一 days/budget/month 命中;
|
|
383
|
+
// 命名 down 通道否证)。match_context 缺省/畸形 → fail-closed 空返回,
|
|
384
|
+
// 绝不降级为「全部广播」。语义:任一条件命中即有召回资格,
|
|
385
|
+
// 不等于「全部出行条件已满足」。
|
|
386
|
+
// ===========================================================================
|
|
387
|
+
{
|
|
388
|
+
// 混合资格池:远超窗口 / 无条件 / 仅天数命中 / 依赖 down 通道 / 仅月份命中
|
|
389
|
+
const POOL_R = [
|
|
390
|
+
{ wish_id: 'w-far', name: '天数预算都差得远', conditions: { days: 30, budget_cny: 99000 }, added_at: '2026-09-01T00:00:00Z' },
|
|
391
|
+
{ wish_id: 'w-nocond', name: '无条件的愿望', added_at: '2026-09-02T00:00:00Z' },
|
|
392
|
+
{ wish_id: 'w-onehit', name: '只有天数达标', conditions: { days: 3, budget_cny: 99000, best_months: [1] }, added_at: '2026-09-03T00:00:00Z' },
|
|
393
|
+
{ wish_id: 'w-veto', name: '依赖已 down 通道', conditions: { days: 3, channels: ['hbcli-hotel'] }, added_at: '2026-09-04T00:00:00Z' },
|
|
394
|
+
{ wish_id: 'w-months', name: '只看月份', conditions: { best_months: [10, 11] }, added_at: '2026-09-05T00:00:00Z' },
|
|
395
|
+
];
|
|
396
|
+
const idsOf = (ts) => ts.map(t => t.wish_id).sort().join(',');
|
|
397
|
+
// §6a 有信号但条件全不命中(且无条件)→ 不触发
|
|
398
|
+
const unmatched = evaluateRecallTriggers([{ wish_id: 'w-far', name: '差得远', conditions: { days: 30, budget_cny: 99000 } }, POOL_R[1]], { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: MATCH_CTX });
|
|
399
|
+
ok(unmatched.length === 0, `§6a 有信号但条件全不命中/无条件 → 0 触发(实测 ${unmatched.length};不硬推)`);
|
|
400
|
+
// §6b 资格门广播:仅条件命中的候选触发(w-veto days 命中但未提供 down 面 → 不否证,
|
|
401
|
+
// 与 scoreWishMatch「通道健康或健康面缺席不否证」原语义一致;§6g 单独钉否证)
|
|
402
|
+
const gate = evaluateRecallTriggers(POOL_R, { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: MATCH_CTX });
|
|
403
|
+
ok(idsOf(gate) === 'w-months,w-onehit,w-veto', `§6b 广播只达条件命中的候选(实测 ${idsOf(gate)};w-far/w-nocond 不触发)`);
|
|
404
|
+
// §6c match_context 缺省 → fail-closed 空返回(绝不降级广播)
|
|
405
|
+
const noCtx = evaluateRecallTriggers(POOL, { now: NOW, signals: [SIGNAL_HOLIDAY] });
|
|
406
|
+
ok(noCtx.length === 0, `§6c 缺 match_context → 0 触发(实测 ${noCtx.length};fail-closed 不广播)`);
|
|
407
|
+
// §6d match_context 畸形(JSON 往返产物:数字变字符串 / Set 变数组 / NaN)→ fail-closed
|
|
408
|
+
const strDays = evaluateRecallTriggers(POOL, { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: { days: '7', budgetCny: 6000 } });
|
|
409
|
+
ok(strDays.length === 0, `§6d1 days='7' 字符串 → 0 触发(实测 ${strDays.length};畸形上下文 fail-closed)`);
|
|
410
|
+
const arrDown = evaluateRecallTriggers(POOL, { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: { days: 7, channelDown: ['hbcli-hotel'] } });
|
|
411
|
+
ok(arrDown.length === 0, `§6d2 channelDown 是数组非 Set → 0 触发(实测 ${arrDown.length})`);
|
|
412
|
+
const nanDays = evaluateRecallTriggers(POOL, { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: { days: Number.NaN } });
|
|
413
|
+
ok(nanDays.length === 0, `§6d3 days=NaN → 0 触发(实测 ${nanDays.length})`);
|
|
414
|
+
// §6e match_context 形状合法但全空({})→ 零证据零触发(不把空窗口当广播授权)
|
|
415
|
+
const emptyCtx = evaluateRecallTriggers(POOL, { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: {} });
|
|
416
|
+
ok(emptyCtx.length === 0, `§6e 空 match_context({})→ 0 触发(实测 ${emptyCtx.length};零证据不硬推)`);
|
|
417
|
+
// §6f 定向信号不能绕过条件门:指定 id 无条件命中 → 0;混合 id → 只达命中的那个
|
|
418
|
+
const bypass = evaluateRecallTriggers([POOL_R[0]], { now: NOW, signals: [{ ...SIGNAL_PRICE, wish_ids: ['w-far'] }], match_context: MATCH_CTX });
|
|
419
|
+
ok(bypass.length === 0, `§6f1 定向信号指向条件不命中的候选 → 0 触发(实测 ${bypass.length};定向不扩权)`);
|
|
420
|
+
const mixed = evaluateRecallTriggers(POOL_R, { now: NOW, signals: [{ ...SIGNAL_PRICE, wish_ids: ['w-onehit', 'w-far'] }], match_context: MATCH_CTX });
|
|
421
|
+
ok(idsOf(mixed) === 'w-onehit', `§6f2 定向混合 id → 只达条件命中的(实测 ${idsOf(mixed)})`);
|
|
422
|
+
// §6g 命名 down 通道否证:days 命中但依赖通道 down → 否证(定向/广播都不得触发);
|
|
423
|
+
// 通道健康或 down 的是别的通道 → 照常触发(scoreWishMatch 原语义)
|
|
424
|
+
const vetoPool = [POOL_R[3]];
|
|
425
|
+
const preVeto = evaluateRecallTriggers(vetoPool, { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: MATCH_CTX });
|
|
426
|
+
ok(preVeto.length === 1, `§6g0 无否证信息时 days 命中 → 有资格(实测 ${preVeto.length})`);
|
|
427
|
+
const vetoCtx = { ...MATCH_CTX, channelDown: new Set(['hbcli-hotel']) };
|
|
428
|
+
const vetoed = evaluateRecallTriggers(vetoPool, { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: vetoCtx });
|
|
429
|
+
ok(vetoed.length === 0, `§6g1 依赖通道 down → 广播否证(实测 ${vetoed.length})`);
|
|
430
|
+
const vetoTargeted = evaluateRecallTriggers(vetoPool, { now: NOW, signals: [{ ...SIGNAL_PRICE, wish_ids: ['w-veto'] }], match_context: vetoCtx });
|
|
431
|
+
ok(vetoTargeted.length === 0, `§6g2 依赖通道 down → 定向同样否证(实测 ${vetoTargeted.length};定向不绕过否证)`);
|
|
432
|
+
const otherDown = evaluateRecallTriggers(vetoPool, { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: { ...MATCH_CTX, channelDown: new Set(['other-channel']) } });
|
|
433
|
+
ok(otherDown.length === 1, `§6g3 down 的是无关通道 → 不误杀(实测 ${otherDown.length})`);
|
|
434
|
+
// §6h 一项命中即有资格(既有语义):仅 days 命中 / 仅 month 命中,且触发携带逐项命中
|
|
435
|
+
const one = evaluateRecallTriggers(POOL_R, { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: MATCH_CTX });
|
|
436
|
+
const onehit = one.find(t => t.wish_id === 'w-onehit');
|
|
437
|
+
const monthhit = one.find(t => t.wish_id === 'w-months');
|
|
438
|
+
ok(onehit !== undefined && Array.isArray(onehit.match_hits) && onehit.match_hits.join('+') === 'days≥3', `§6h1 仅 days 命中即有资格,match_hits=[days≥3](实测 ${String(onehit?.match_hits)})`);
|
|
439
|
+
ok(monthhit !== undefined && Array.isArray(monthhit.match_hits) && monthhit.match_hits.join('+') === 'month=10', `§6h2 仅 month 命中即有资格,match_hits=[month=10](实测 ${String(monthhit?.match_hits)})`);
|
|
440
|
+
// §6i 畸形 conditions/条目不崩,资格由 scoreWishMatch 原语义判定:
|
|
441
|
+
// conditions 为字符串/数字 → 无命中;best_months 为字符串 → 忽略但 days 照常命中;null 条目被滤
|
|
442
|
+
const POOL_MAL = [
|
|
443
|
+
{ wish_id: 'w-cond-str', name: 'conditions 是字符串', conditions: '5天', added_at: '2026-09-06T00:00:00Z' },
|
|
444
|
+
{ wish_id: 'w-cond-num', name: 'conditions 是数字', conditions: 42, added_at: '2026-09-07T00:00:00Z' },
|
|
445
|
+
{ wish_id: 'w-months-str', name: 'best_months 是字符串', conditions: { days: 1, best_months: '10,11' }, added_at: '2026-09-08T00:00:00Z' },
|
|
446
|
+
null,
|
|
447
|
+
];
|
|
448
|
+
let malThrew = false;
|
|
449
|
+
let mal = [];
|
|
450
|
+
try {
|
|
451
|
+
mal = evaluateRecallTriggers(POOL_MAL, { now: NOW, signals: [SIGNAL_HOLIDAY], match_context: MATCH_CTX });
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
malThrew = true;
|
|
455
|
+
}
|
|
456
|
+
ok(!malThrew, '§6i1 畸形 conditions/条目 → 不抛不崩');
|
|
457
|
+
ok(mal.length === 1 && mal[0]?.wish_id === 'w-months-str', `§6i2 畸形形状按原评分语义判定(实测 ${mal.map(t => t.wish_id).join(',')};字符串/数字 conditions 无命中,字符串 best_months 忽略)`);
|
|
458
|
+
// §6j 全 5 类原因在有效条件下可达,且每个触发都带非空逐项命中
|
|
459
|
+
const allR = evaluateRecallTriggers(POOL, { now: NOW, signals: [SIGNAL_HOLIDAY, SIGNAL_PRICE, SIGNAL_WEATHER, SIGNAL_ROUTE, SIGNAL_RECOVERED], match_context: MATCH_CTX });
|
|
460
|
+
const allReasons = new Set(allR.map(t => t.signal.reason));
|
|
461
|
+
ok(allReasons.size === 5 && RECALL_REASONS.every(r => allReasons.has(r)), `§6j1 有效条件下 5 类原因全部可达(实测 ${allReasons.size} 类)`);
|
|
462
|
+
ok(allR.every(t => Array.isArray(t.match_hits) && t.match_hits.length > 0), `§6j2 每个触发都携带非空 match_hits(实测 ${allR.filter(t => !Array.isArray(t.match_hits) || t.match_hits.length === 0).length} 个缺命中)`);
|
|
463
|
+
}
|
|
464
|
+
// ===========================================================================
|
|
465
|
+
// §7 命中证据贯穿(#577 收口):触发 → 卡 → 渲染边界——
|
|
466
|
+
// 卡只陈述实际命中的愿望条件,渲染面显式声明「非全部出行条件已满足」;
|
|
467
|
+
// 手工构造的卡不发明证据。
|
|
468
|
+
// ===========================================================================
|
|
469
|
+
{
|
|
470
|
+
// §7a 集成:真命中证据进卡与渲染(证据逐项 = scoreWishMatch 的 hits 原文)
|
|
471
|
+
const result = evaluatePoolRecall({ pool: POOL, tick: TICK, signals: [SIGNAL_PRICE], match_context: MATCH_CTX });
|
|
472
|
+
const priceCard = result.cards[0];
|
|
473
|
+
ok(priceCard !== undefined && Array.isArray(priceCard.match_evidence) && priceCard.match_evidence.join('+') === 'days≥5+budget≥5000', `§7a 卡携带真命中证据(实测 ${String(priceCard?.match_evidence)})`);
|
|
474
|
+
const evidenceLine = renderWhyNowCardLine(priceCard);
|
|
475
|
+
ok(evidenceLine.includes('命中条件:days≥5+budget≥5000'), `§7b 渲染行陈述命中条件(实测含「${evidenceLine.slice(0, 80)}…」)`);
|
|
476
|
+
ok(evidenceLine.includes('非全部出行条件已满足'), '§7c 渲染行显式声明不主张全部出行条件已满足');
|
|
477
|
+
ok(evidenceLine.includes('证据边界'), '§7c2 渲染行保留证据边界标记(渲染契约不变)');
|
|
478
|
+
// §7d 缺 match_context → 集成层同样 fail-closed(0 触发 0 卡)
|
|
479
|
+
const noCtx = evaluatePoolRecall({ pool: POOL, tick: TICK, signals: [SIGNAL_HOLIDAY] });
|
|
480
|
+
ok(noCtx.triggers.length === 0 && noCtx.cards.length === 0, `§7d 集成缺 match_context → 空(实测 ${noCtx.triggers.length}/${noCtx.cards.length})`);
|
|
481
|
+
// §7e 手工构造的卡不发明证据:match_hits 缺省/空数组/畸形 → 卡无证据、渲染无命中子句
|
|
482
|
+
const manual = buildWhyNowCard({ wish_id: 'w-x', wish_name: '手工卡', signal: SIGNAL_PRICE, evaluated_at: NOW.toISOString() });
|
|
483
|
+
ok(manual.match_evidence === undefined, `§7e1 手工卡无 match_hits → 卡无证据(实测 ${String(manual.match_evidence)})`);
|
|
484
|
+
const emptyHits = buildWhyNowCard({ wish_id: 'w-x', wish_name: '手工卡', signal: SIGNAL_PRICE, match_hits: [], evaluated_at: NOW.toISOString() });
|
|
485
|
+
ok(emptyHits.match_evidence === undefined, `§7e2 match_hits=[] → 卡无证据(空数组不发明;实测 ${String(emptyHits.match_evidence)})`);
|
|
486
|
+
let garbledThrew = false;
|
|
487
|
+
let garbled = null;
|
|
488
|
+
try {
|
|
489
|
+
garbled = buildWhyNowCard({ wish_id: 'w-x', wish_name: '手工卡', signal: SIGNAL_PRICE, match_hits: 'days≥5', evaluated_at: NOW.toISOString() });
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
garbledThrew = true;
|
|
493
|
+
}
|
|
494
|
+
ok(!garbledThrew && garbled !== null && garbled.match_evidence === undefined, `§7e3 畸形 match_hits → 不崩且不发明证据(实测 ${String(garbled?.match_evidence)})`);
|
|
495
|
+
ok(!renderWhyNowCardLine(manual).includes('命中条件'), '§7f 无证据卡的渲染不出现命中条件子句');
|
|
496
|
+
// §7g scheduler 端到端:真命中证据贯穿 evaluate → toCard → sink
|
|
497
|
+
const evidenceSink = new ArrayRecallSink();
|
|
498
|
+
const evidenceScheduler = new RecallTickScheduler({
|
|
499
|
+
evaluate: (tick) => evaluatePoolRecall({ pool: POOL, tick, signals: [SIGNAL_PRICE], match_context: MATCH_CTX }).triggers,
|
|
500
|
+
toCard: (evaluation, tick) => buildWhyNowCard({ ...evaluation, evaluated_at: tick.at.toISOString() }),
|
|
501
|
+
sink: evidenceSink,
|
|
502
|
+
});
|
|
503
|
+
await evidenceScheduler.run(TICK);
|
|
504
|
+
const evidenceCard = evidenceSink.items[0];
|
|
505
|
+
ok(Array.isArray(evidenceCard?.match_evidence) && evidenceCard.match_evidence.join('+') === 'days≥5+budget≥5000', `§7g scheduler 端到端:卡证据 = 真命中(实测 ${String(evidenceCard?.match_evidence)})`);
|
|
506
|
+
}
|
|
507
|
+
console.log(`\nRECALL TESTS: ${pass} pass, ${fail} fail`);
|
|
508
|
+
if (fail > 0) {
|
|
509
|
+
console.log('\nFAILURES:');
|
|
510
|
+
for (const f of failures)
|
|
511
|
+
console.log(' - ' + f);
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|