@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1744 -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 +135 -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 +546 -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 +509 -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/resilience.js +191 -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-tests.js +116 -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 +123 -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 +1479 -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/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 +564 -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-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 +131 -0
- package/dist/scripts/g5-guard.js +189 -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/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 +161 -0
- package/dist/scripts/nightly-evidence.js +286 -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/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 +20 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +52 -82
- 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-zones-tests.js +358 -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 +241 -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 +550 -199
- 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 +210 -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 +23 -19
- 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 +41 -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/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +97 -111
- 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 +2227 -882
- 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 +319 -160
- 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/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-zones.js +507 -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 +435 -516
- 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 +535 -359
- 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 +255 -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 +568 -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 +13 -3
- package/ts/src/dsh-llm.ts +44 -11
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +1948 -198
- 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 +223 -25
- 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 +912 -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 +354 -88
- package/ts/src/wish-pool.ts +79 -0
- package/dist/scripts/session-attach-wait.js +0 -41
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Offline registration contract for the eight public FlyAI search kinds. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { apply } from '../src/index.js';
|
|
7
|
+
const stateRoot = await mkdtemp(join(tmpdir(), 'gotry-flyai-registration-'));
|
|
8
|
+
const calls = [];
|
|
9
|
+
const tools = [];
|
|
10
|
+
const context = {
|
|
11
|
+
tools: { register: (tool) => tools.push(tool) },
|
|
12
|
+
systemPrompt: { variable: () => { } },
|
|
13
|
+
on: () => () => { },
|
|
14
|
+
get: () => undefined,
|
|
15
|
+
};
|
|
16
|
+
const config = { stateRoot, timeoutMs: 5_000, hbcliBin: 'unavailable', sessionAccess: 'off' };
|
|
17
|
+
const effect = async (input) => {
|
|
18
|
+
calls.push(input);
|
|
19
|
+
return {
|
|
20
|
+
result: {
|
|
21
|
+
ok: false,
|
|
22
|
+
via: 'flyai',
|
|
23
|
+
kind: input.params.kind,
|
|
24
|
+
verdict: 'miss',
|
|
25
|
+
evidence: '[contract-test:flyai]',
|
|
26
|
+
},
|
|
27
|
+
trace: {},
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
apply(context, config, { effect: effect });
|
|
31
|
+
const search = tools.find(tool => tool.name === 'gotry_flyai_search');
|
|
32
|
+
assert.ok(search, 'apply must register gotry_flyai_search');
|
|
33
|
+
assert.ok(tools.some(tool => tool.name === 'gotry_flyai_setup'), 'apply must register gotry_flyai_setup');
|
|
34
|
+
const publicSchema = JSON.stringify(search.parameters);
|
|
35
|
+
for (const forbidden of ['cliBin', 'cliPrefixArgs', 'credential', 'apiKey', 'endpoint', 'signal']) {
|
|
36
|
+
assert.equal(publicSchema.includes(forbidden), false, `search schema must not expose ${forbidden}`);
|
|
37
|
+
}
|
|
38
|
+
assert.deepEqual(search.parameters.properties.kind.enum, [
|
|
39
|
+
'flight', 'train', 'hotel', 'poi', 'keyword', 'ai', 'marriott-hotel', 'marriott-package',
|
|
40
|
+
]);
|
|
41
|
+
const signal = new AbortController().signal;
|
|
42
|
+
const queries = [
|
|
43
|
+
{ kind: 'flight', from: '上海' },
|
|
44
|
+
{ kind: 'train', origin: '上海', destination: '大理', depDateStart: '2099-10-01', depDateEnd: '2099-10-03' },
|
|
45
|
+
{ kind: 'hotel', to: '大理' },
|
|
46
|
+
{ kind: 'poi', cityName: '大理', category: '自然风光' },
|
|
47
|
+
{ kind: 'keyword', query: '大理古城' },
|
|
48
|
+
{ kind: 'ai', query: '大理周末' },
|
|
49
|
+
{ kind: 'marriott-hotel', destName: '大理' },
|
|
50
|
+
{ kind: 'marriott-package', keyword: '早餐' },
|
|
51
|
+
];
|
|
52
|
+
for (const query of queries) {
|
|
53
|
+
const result = await search.execute(query, { signal });
|
|
54
|
+
assert.equal(result.verdict, 'miss', `registered ${String(query.kind)} should execute`);
|
|
55
|
+
const call = calls.at(-1);
|
|
56
|
+
assert.ok(call, `${String(query.kind)} should reach the effect seam`);
|
|
57
|
+
assert.equal(call?.effect, 'FLYAI_SEARCH');
|
|
58
|
+
assert.equal(call?.params.kind, query.kind);
|
|
59
|
+
assert.equal(call?.params.signal, signal, `${String(query.kind)} must receive params.signal`);
|
|
60
|
+
assert.equal(call?.signal, signal, `${String(query.kind)} must preserve outer cancellation`);
|
|
61
|
+
assert.equal('cliBin' in (call?.params ?? {}), false);
|
|
62
|
+
assert.equal('credential' in (call?.params ?? {}), false);
|
|
63
|
+
assert.equal('endpoint' in (call?.params ?? {}), false);
|
|
64
|
+
}
|
|
65
|
+
const callsBeforeUnknown = calls.length;
|
|
66
|
+
const unknown = await search.execute({ kind: 'spaceship', from: '上海', to: '月球', date: '2099-10-01' }, { signal });
|
|
67
|
+
assert.equal(calls.length, callsBeforeUnknown, 'unknown kind must be rejected before the effect');
|
|
68
|
+
assert.equal(unknown.ok, false);
|
|
69
|
+
for (const invalid of [
|
|
70
|
+
{ kind: 'flight', to: '丽江' },
|
|
71
|
+
{ kind: 'flight', from: '上海', date: '2099-02-29' },
|
|
72
|
+
{ kind: 'train', from: '上海', dateStart: '2099-10-03', dateEnd: '2099-10-01' },
|
|
73
|
+
{ kind: 'flight', from: '上海', sortType: '9' },
|
|
74
|
+
{ kind: 'marriott-hotel', destName: '大理', hotelStars: '5' },
|
|
75
|
+
{ kind: 'poi', cityName: '' },
|
|
76
|
+
{ kind: 'keyword' },
|
|
77
|
+
{ kind: 'marriott-package' },
|
|
78
|
+
]) {
|
|
79
|
+
const before = calls.length;
|
|
80
|
+
const rejected = await search.execute(invalid, { signal });
|
|
81
|
+
assert.equal(calls.length, before, `${String(invalid.kind)} missing required input must not reach the effect`);
|
|
82
|
+
assert.equal(rejected.ok, false);
|
|
83
|
+
}
|
|
84
|
+
const marriott = await search.execute({ kind: 'marriott-hotel', destName: '大理', hotelBrands: '万豪', hotelName: '大理酒店' }, { signal });
|
|
85
|
+
assert.equal(marriott.verdict, 'miss');
|
|
86
|
+
const marriottCall = calls.at(-1);
|
|
87
|
+
assert.equal(marriottCall?.params.keyWords, '万豪 大理酒店', 'Marriott name/brand must map to official --key-words');
|
|
88
|
+
assert.equal('hotelBrands' in (marriottCall?.params ?? {}), false);
|
|
89
|
+
assert.equal('hotelName' in (marriottCall?.params ?? {}), false);
|
|
90
|
+
await rm(stateRoot, { recursive: true, force: true });
|
|
91
|
+
console.log('flyai registration contract OK');
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money/FX fact 合同 v1 测试(issue #344 契约切片;全离线、零网络、零活体汇率源):
|
|
3
|
+
*
|
|
4
|
+
* A. 币种封闭集 + 原币金额解析(未知币种/小写/精度超位/千分位/指数记法 fail-closed;负值与零结构可载)
|
|
5
|
+
* B. FxFact 构建(全字段必填/fact_id 幂等/rate 归一;缺源·来源不明·缺时刻·日期无时刻·as_of 晚于抓取·
|
|
6
|
+
* 零/负/伪精度汇率·同币种自兑·provenance 空 各负例;默认注册表为空 → 生产路径任何 provider 均拒)
|
|
7
|
+
* C. 单腿换算(精确 bigint·half-away-from-zero 落位·JPY 0 位指数·原币展示保留;
|
|
8
|
+
* 供应商币种变化/方向拿反 显式拒绝)
|
|
9
|
+
* D. 同一估值时点比较(同 as_of 不同拼写可比/预算判定/聚合;混时点·午夜切换·旧 rate 钉时点·
|
|
10
|
+
* 混币种·空聚合 各负例且错误带证据)
|
|
11
|
+
* E. 交叉汇率(两腿同 as_of 显式合成;混时点两腿拒绝)
|
|
12
|
+
* F. 契约 seam(注册表冻结为空/触发闸冻结 false;触发前 consultFxSources 结构化拒绝且
|
|
13
|
+
* fetch spy 零调用=零浮动汇率源被咨询;触发后 primary→fallback 顺位与 miss/error/stale 不伪装)
|
|
14
|
+
*
|
|
15
|
+
* 运行(在 ts/ 下):npx tsx scripts/fx-contract-tests.ts
|
|
16
|
+
*/
|
|
17
|
+
import { FX_FACT_SCHEMA, FX_TRIGGER_FIRED, LIVE_FX_PROVIDER_DESCRIPTORS, FxContractError, buildFxFact, compareMoney, consultFxSources, convertMoney, convertMoneyViaPivot, formatMoney, parseCurrency, parseMoney, pinNativeMoney, sumMoney, withinBudget, } from '../capabilities/fx-contract.js';
|
|
18
|
+
let passed = 0;
|
|
19
|
+
function check(cond, msg) {
|
|
20
|
+
if (cond) {
|
|
21
|
+
passed++;
|
|
22
|
+
console.log(` ok - ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.error(` FAIL - ${msg}`);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** 负向断言:必须抛 FxContractError,且消息携带证据片段「needle」。 */
|
|
30
|
+
function expectReject(fn, needle, msg) {
|
|
31
|
+
try {
|
|
32
|
+
fn();
|
|
33
|
+
check(false, `${msg}(应拒绝但未拒绝)`);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
const text = e instanceof Error ? e.message : String(e);
|
|
37
|
+
check(e instanceof FxContractError && text.includes(needle), `${msg}(拒绝且证据含「${needle}」)`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** 测试专用离线 registry(仅存在于测试文件;生产注册表冻结为空)。 */
|
|
41
|
+
const TEST_REGISTRY = [
|
|
42
|
+
{ id: 'fixture-offline-table', tier: 'primary', legal_basis: 'test-only offline fixture table (no live source, issue #344 contract slice)' },
|
|
43
|
+
{ id: 'fixture-offline-fallback', tier: 'fallback', legal_basis: 'test-only offline fixture table (no live source, issue #344 contract slice)' },
|
|
44
|
+
];
|
|
45
|
+
const T1 = '2026-10-02T00:00:00.000Z';
|
|
46
|
+
const T1_ALT_SPELLING = '2026-10-02T08:00:00+08:00'; // 与 T1 同一时刻的另一种拼写(+08:00 偏移)
|
|
47
|
+
const T2 = '2026-10-02T12:00:00.000Z';
|
|
48
|
+
const T_MIDNIGHT_BEFORE = '2026-10-01T23:59:59.000Z';
|
|
49
|
+
const T_MIDNIGHT_AFTER = '2026-10-02T00:00:01.000Z';
|
|
50
|
+
function fixtureFact(base, quote, rate, provider, asOf) {
|
|
51
|
+
return buildFxFact({ base, quote, rate, provider, provenance: `[汇率:fixture@契约测试:${provider}]`, as_of: asOf, fetched_at: '2026-10-02T12:00:01.000Z' }, TEST_REGISTRY);
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------- A. 币种与金额
|
|
54
|
+
console.log('A. 币种封闭集与原币金额解析');
|
|
55
|
+
check(parseCurrency('CNY') === 'CNY', '已知币种准入(CNY)');
|
|
56
|
+
expectReject(() => parseCurrency('usd'), 'uppercase 3-letter', '未知形态币种(小写 usd)拒绝');
|
|
57
|
+
expectReject(() => parseCurrency('XXY'), 'unknown currency code XXY', '未知币种(XXY 不在封闭集)拒绝');
|
|
58
|
+
expectReject(() => parseCurrency('US'), 'uppercase 3-letter', '两位币种码拒绝');
|
|
59
|
+
expectReject(() => parseCurrency(42), 'uppercase 3-letter', '非字符串币种拒绝');
|
|
60
|
+
check(parseMoney('CNY', '1234.56').amountMinor === 123456n, '金额解析:1234.56 CNY = 123456 最小单位');
|
|
61
|
+
check(formatMoney(parseMoney('CNY', '1234.56')) === '1234.56 CNY', '原币展示保留(1234.56 CNY)');
|
|
62
|
+
check(parseMoney('JPY', '1000').amountMinor === 1000n, 'JPY(0 位最小单位)整数金额解析');
|
|
63
|
+
check(parseMoney('CNY', '-50.00').amountMinor === -5000n, '负值金额结构可载(退款/抹零是真实数据)');
|
|
64
|
+
check(parseMoney('CNY', '0.00').amountMinor === 0n, '零金额结构可载');
|
|
65
|
+
check(formatMoney(parseMoney('CNY', '-50.00')) === '-50.00 CNY', '负值原币展示');
|
|
66
|
+
expectReject(() => parseMoney('JPY', '1000.5'), 'exceeds its 0-digit minor unit', 'JPY 带小数(亚最小单位精度)拒绝');
|
|
67
|
+
expectReject(() => parseMoney('CNY', '1,234.56'), 'no separators', '千分位分隔符拒绝');
|
|
68
|
+
expectReject(() => parseMoney('CNY', '1.234'), 'exceeds its 2-digit minor unit', 'CNY 三位小数拒绝');
|
|
69
|
+
expectReject(() => parseMoney('CNY', '1e3'), 'plain decimal', '指数记法金额拒绝');
|
|
70
|
+
expectReject(() => parseMoney('CNY', ' 12.34'), 'plain decimal', '带空白金额拒绝');
|
|
71
|
+
// ---------------------------------------------------------------- B. FxFact 构建
|
|
72
|
+
console.log('B. FxFact 构建(全字段必填 + fail-closed 负例)');
|
|
73
|
+
const usdCnyAtT1 = fixtureFact('USD', 'CNY', '7.20', 'fixture-offline-table', T1);
|
|
74
|
+
check(usdCnyAtT1.schema === FX_FACT_SCHEMA && FX_FACT_SCHEMA === 'gotry_fx_fact.v1', 'schema 版本化标识 gotry_fx_fact.v1');
|
|
75
|
+
check(usdCnyAtT1.rate === '7.2', 'rate 归一化(去尾零:7.20 → 7.2)');
|
|
76
|
+
check(usdCnyAtT1.as_of === T1 && usdCnyAtT1.fetched_at === '2026-10-02T12:00:01.000Z', 'as_of/fetched_at 规范化保留');
|
|
77
|
+
check(usdCnyAtT1.rounding === 'minor-unit-half-away-from-zero', '舍入规则冻结在事实内(可审计)');
|
|
78
|
+
const usdCnyAtT1Again = fixtureFact('USD', 'CNY', '7.2', 'fixture-offline-table', T1);
|
|
79
|
+
check(usdCnyAtT1.fact_id === usdCnyAtT1Again.fact_id, 'fact_id 幂等(rate 拼写差异不影响)');
|
|
80
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: 'mystery-rate-api', provenance: 'p', as_of: T1, fetched_at: T2 }, TEST_REGISTRY), 'not admitted in the registry: "mystery-rate-api"', '来源不明(未注册 provider)拒绝');
|
|
81
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: '', provenance: 'p', as_of: T1, fetched_at: T2 }, TEST_REGISTRY), 'non-empty source identifier', '缺源(provider 空串)拒绝');
|
|
82
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: undefined, provenance: 'p', as_of: T1, fetched_at: T2 }, TEST_REGISTRY), 'non-empty source identifier', '缺源(provider 缺失)拒绝');
|
|
83
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: 'fixture-offline-table', provenance: 'p', as_of: T1, fetched_at: T2 }), 'not admitted in the registry', '默认注册表(生产路径,冻结为空)任何 provider 均拒——零活体源');
|
|
84
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: 'no-basis-source', provenance: 'p', as_of: T1, fetched_at: T2 }, [{ id: 'no-basis-source', tier: 'primary', legal_basis: '' }]), 'carries no legal_basis', 'registry 条目缺 legal_basis(缺陷注册表)拒绝');
|
|
85
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: 'fixture-offline-table', provenance: 'p', as_of: '', fetched_at: T2 }, TEST_REGISTRY), 'non-empty ISO-8601 UTC instant', '缺估值时刻(as_of 空串)拒绝');
|
|
86
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: 'fixture-offline-table', provenance: 'p', as_of: undefined, fetched_at: T2 }, TEST_REGISTRY), 'non-empty ISO-8601 UTC instant', '缺估值时刻(as_of 缺失)拒绝');
|
|
87
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: 'fixture-offline-table', provenance: 'p', as_of: '2026-10-02', fetched_at: T2 }, TEST_REGISTRY), 'date-only', '日期无时刻(date-only as_of)拒绝——非估值时点');
|
|
88
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: 'fixture-offline-table', provenance: 'p', as_of: 'not-a-time', fetched_at: T2 }, TEST_REGISTRY), 'not a parseable ISO-8601 instant', '垃圾估值时刻拒绝');
|
|
89
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: 'fixture-offline-table', provenance: 'p', as_of: T1, fetched_at: '' }, TEST_REGISTRY), 'non-empty ISO-8601 UTC instant', '缺抓取时刻(fetched_at 空串)拒绝');
|
|
90
|
+
expectReject(() => fixtureFact('USD', 'CNY', '7.2', 'fixture-offline-table', '2026-10-02T12:00:02.000Z'), 'impossible provenance', 'as_of 晚于 fetched_at(不可能的 provenance)拒绝');
|
|
91
|
+
expectReject(() => buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: 'fixture-offline-table', provenance: '', as_of: T1, fetched_at: T2 }, TEST_REGISTRY), 'non-empty evidence-chain description', 'provenance 空串拒绝');
|
|
92
|
+
expectReject(() => fixtureFact('USD', 'USD', '1', 'fixture-offline-table', T1), 'base and quote must differ', '同币种自兑(恒等汇率绕过证据)拒绝');
|
|
93
|
+
expectReject(() => fixtureFact('USD', 'CNY', '0', 'fixture-offline-table', T1), 'strictly positive', '零汇率拒绝');
|
|
94
|
+
expectReject(() => fixtureFact('USD', 'CNY', '-7.2', 'fixture-offline-table', T1), 'plain positive decimal', '负汇率拒绝(记法即拒)');
|
|
95
|
+
expectReject(() => fixtureFact('USD', 'CNY', '1e5', 'fixture-offline-table', T1), 'plain positive decimal', '指数记法汇率拒绝');
|
|
96
|
+
expectReject(() => fixtureFact('USD', 'CNY', '7.1234567890123', 'fixture-offline-table', T1), 'exceeds 12 fractional digits', '伪精度汇率(13 位小数)拒绝');
|
|
97
|
+
expectReject(() => fixtureFact('USD', 'CNY', 7.2, 'fixture-offline-table', T1), 'plain positive decimal', '非字符串汇率(浮点)拒绝');
|
|
98
|
+
expectReject(() => fixtureFact('XXY', 'CNY', '7.2', 'fixture-offline-table', T1), 'unknown currency code XXY', '事实构建侧未知币种拒绝');
|
|
99
|
+
// ---------------------------------------------------------------- C. 换算
|
|
100
|
+
console.log('C. 单腿换算(精确 bigint + 冻结舍入 + 原币保留)');
|
|
101
|
+
const usd100 = parseMoney('USD', '100.00');
|
|
102
|
+
const cny720 = convertMoney(usd100, 'CNY', usdCnyAtT1);
|
|
103
|
+
check(cny720.amountMinor === 72000n, 'USD 100.00 × 7.2 = CNY 720.00(72000 最小单位)');
|
|
104
|
+
check(cny720.valuationAsOf === T1, '换算值绑定估值时点 = fact.as_of');
|
|
105
|
+
check(formatMoney(cny720.original) === '100.00 USD', '原币金额与供应商金额原样保留');
|
|
106
|
+
check(cny720.original === usd100, 'original 即传入原币对象(零改写)');
|
|
107
|
+
check(cny720.fx.fact_id === usdCnyAtT1.fact_id && cny720.fx.provider === 'fixture-offline-table' && cny720.fx.rate === '7.2', '汇率证据引用完整(fact_id/provider/rate)');
|
|
108
|
+
const halfUp = convertMoney(parseMoney('USD', '0.01'), 'CNY', fixtureFact('USD', 'CNY', '7.5', 'fixture-offline-table', T1));
|
|
109
|
+
check(halfUp.amountMinor === 8n, 'half-away-from-zero 上取整:USD 0.01 × 7.5 = ¥0.075 → 0.08');
|
|
110
|
+
const halfDownNegative = convertMoney(parseMoney('USD', '-0.01'), 'CNY', fixtureFact('USD', 'CNY', '7.5', 'fixture-offline-table', T1));
|
|
111
|
+
check(halfDownNegative.amountMinor === -8n, '负值对称远离零:-¥0.075 → -0.08');
|
|
112
|
+
const jpyTarget = convertMoney(usd100, 'JPY', fixtureFact('USD', 'JPY', '150.5', 'fixture-offline-table', T1));
|
|
113
|
+
check(jpyTarget.amountMinor === 15050n && jpyTarget.currency === 'JPY', 'JPY 目标币(0 位最小单位):USD 100 × 150.5 = 15050 円');
|
|
114
|
+
const zeroMoney = convertMoney(parseMoney('USD', '0.00'), 'CNY', usdCnyAtT1);
|
|
115
|
+
check(zeroMoney.amountMinor === 0n, '零金额换算确定性(0 × rate = 0)');
|
|
116
|
+
expectReject(() => convertMoney(parseMoney('EUR', '100.00'), 'CNY', usdCnyAtT1), 'does not cover the requested conversion EUR→CNY', '供应商币种变化(fact 币种对不覆盖)显式拒绝');
|
|
117
|
+
expectReject(() => convertMoney(usd100, 'CNY', fixtureFact('CNY', 'USD', '0.1389', 'fixture-offline-table', T1)), 'does not cover the requested conversion USD→CNY', '方向拿反(CNY→USD 腿不能反向用)拒绝');
|
|
118
|
+
// ---------------------------------------------------------------- D. 同一估值时点比较
|
|
119
|
+
console.log('D. 同一估值时点比较(混时点/午夜切换/旧 rate fail-closed)');
|
|
120
|
+
const usdCnyAtT1AltSpelling = fixtureFact('USD', 'CNY', '7.2', 'fixture-offline-table', T1_ALT_SPELLING);
|
|
121
|
+
check(usdCnyAtT1AltSpelling.as_of === T1, '时刻规范化:+08:00 偏移拼写归一到同一 UTC 毫秒时点');
|
|
122
|
+
const cny720Alt = convertMoney(usd100, 'CNY', usdCnyAtT1AltSpelling);
|
|
123
|
+
check(compareMoney(cny720, cny720Alt) === 0, '同一估值时点(不同拼写归一后)可比');
|
|
124
|
+
const budget1000 = pinNativeMoney(parseMoney('CNY', '1000.00'), T1);
|
|
125
|
+
check(compareMoney(cny720, budget1000) === -1 && withinBudget(cny720, budget1000) === true, '预算判定:¥720 ≤ ¥1000(FX 规范化值 vs 原生钉时点,同 as_of)');
|
|
126
|
+
const cny600 = pinNativeMoney(parseMoney('CNY', '600.00'), T1);
|
|
127
|
+
check(withinBudget(budget1000, cny600) === false, '预算判定方向语义:¥1000 ≤ ¥600 为假');
|
|
128
|
+
const summed = sumMoney([cny720, pinNativeMoney(parseMoney('CNY', '280.00'), T1)]);
|
|
129
|
+
check(summed.amountMinor === 100000n && summed.valuationAsOf === T1, '聚合求和(同币种同 as_of:720+280=1000)');
|
|
130
|
+
const usdCnyAtT2 = fixtureFact('USD', 'CNY', '7.2', 'fixture-offline-fallback', T2);
|
|
131
|
+
const cny720AtT2 = convertMoney(usd100, 'CNY', usdCnyAtT2);
|
|
132
|
+
expectReject(() => compareMoney(cny720, cny720AtT2), '同一估值时点', '混时点比较显式拒绝(错误提示含规则名)');
|
|
133
|
+
expectReject(() => compareMoney(cny720, cny720AtT2), usdCnyAtT1.fact_id, '混时点错误携带 side A 证据(fact_id)');
|
|
134
|
+
expectReject(() => compareMoney(cny720, cny720AtT2), usdCnyAtT2.fact_id, '混时点错误携带 side B 证据(fact_id)');
|
|
135
|
+
expectReject(() => compareMoney(cny720, cny720AtT2), 'as_of 2026-10-02T00:00:00.000Z', '混时点错误携带两侧估值时点');
|
|
136
|
+
expectReject(() => withinBudget(cny720, cny720AtT2), '同一估值时点', '混时点预算判定拒绝');
|
|
137
|
+
expectReject(() => sumMoney([cny720, cny720AtT2]), 'money aggregation', '混时点聚合拒绝');
|
|
138
|
+
const midnightFact = fixtureFact('USD', 'CNY', '7.2', 'fixture-offline-table', T_MIDNIGHT_BEFORE);
|
|
139
|
+
const midnightOther = fixtureFact('USD', 'CNY', '7.2', 'fixture-offline-fallback', T_MIDNIGHT_AFTER);
|
|
140
|
+
expectReject(() => compareMoney(convertMoney(usd100, 'CNY', midnightFact), convertMoney(usd100, 'CNY', midnightOther)), 'as_of 2026-10-01T23:59:59.000Z', '午夜切换:跨日 2 秒的相邻时点仍判混时点');
|
|
141
|
+
expectReject(() => convertMoney(usd100, 'CNY', usdCnyAtT1, T2), 'does not match the pinned valuation instant', '旧 rate(钉住 T2 但只有 T1 事实)拒绝——stale 不伪装为当前准确换算');
|
|
142
|
+
expectReject(() => convertMoney(usd100, 'CNY', usdCnyAtT1, '2026-10-02'), 'date-only', '钉住时点为日期无时刻也拒绝');
|
|
143
|
+
check(convertMoney(usd100, 'CNY', usdCnyAtT1, T1_ALT_SPELLING).amountMinor === 72000n, '钉住时点拼写不同但同时刻可换算');
|
|
144
|
+
const jpyCnyAtT1 = convertMoney(parseMoney('JPY', '20000'), 'CNY', fixtureFact('JPY', 'CNY', '0.048', 'fixture-offline-table', T1));
|
|
145
|
+
check(compareMoney(cny720, jpyCnyAtT1) === -1, '两条外币报价(USD/JPY)经各自 FX 事实规范化到 CNY 后同 as_of 可比(720 < 960)');
|
|
146
|
+
check(pinNativeMoney(parseMoney('CNY', '960.00'), T1).amountMinor === 96000n, '原生恒等钉时点保留金额(960.00 CNY)');
|
|
147
|
+
expectReject(() => pinNativeMoney(parseMoney('CNY', '1.00'), 'not-a-time'), 'not a parseable ISO-8601 instant', '原生钉时点同样要求合法时刻');
|
|
148
|
+
expectReject(() => sumMoney([]), 'at least one normalized amount', '空聚合拒绝(空聚合不是事实)');
|
|
149
|
+
// ---------------------------------------------------------------- E. 交叉汇率
|
|
150
|
+
console.log('E. 交叉汇率(两腿显式合成,同 as_of)');
|
|
151
|
+
const cnyThbAtT1 = fixtureFact('CNY', 'THB', '4.85', 'fixture-offline-table', T1);
|
|
152
|
+
const usdThb = convertMoneyViaPivot(usd100, 'CNY', 'THB', usdCnyAtT1, cnyThbAtT1);
|
|
153
|
+
check(usdThb.currency === 'THB' && usdThb.amountMinor === 349200n, 'USD 100 → CNY 720 → THB 3492.00(两腿同 as_of)');
|
|
154
|
+
expectReject(() => compareMoney(cny720, usdThb), 'same normalized currency', '未归一到同币种的比较拒绝(CNY vs THB,提示先规范化)');
|
|
155
|
+
check(usdThb.valuationAsOf === T1, '交叉汇率合成值绑定同一估值时点');
|
|
156
|
+
const cnyThbAtT2 = fixtureFact('CNY', 'THB', '4.85', 'fixture-offline-fallback', T2);
|
|
157
|
+
expectReject(() => convertMoneyViaPivot(usd100, 'CNY', 'THB', usdCnyAtT1, cnyThbAtT2), 'must share the same valuation instant', '交叉汇率混时点两腿拒绝(错误含两腿 fact_id)');
|
|
158
|
+
expectReject(() => convertMoneyViaPivot(usd100, 'CNY', 'THB', usdCnyAtT1, cnyThbAtT2), cnyThbAtT2.fact_id, '交叉汇率混时点错误携带第二腿证据');
|
|
159
|
+
// ---------------------------------------------------------------- F. 契约 seam(零活体源咨询)
|
|
160
|
+
console.log('F. 契约 seam:触发前零浮动汇率源被咨询(D-26 边界不变)');
|
|
161
|
+
check(LIVE_FX_PROVIDER_DESCRIPTORS.length === 0, '运行时源注册表冻结为空');
|
|
162
|
+
check(FX_TRIGGER_FIRED === false, '触发闸冻结为 false(#344 触发后置)');
|
|
163
|
+
const deferred = await consultFxSources({ base: parseCurrency('USD'), quote: parseCurrency('CNY') });
|
|
164
|
+
check(deferred.ok === false && deferred.code === 'trigger-deferred', '触发未开:consultFxSources 结构化拒绝(trigger-deferred)');
|
|
165
|
+
check(deferred.ok === false && deferred.detail.includes('zero floating-rate sources consulted'), '拒绝理由明示「零浮动汇率源被咨询」与 D-26 边界');
|
|
166
|
+
let spyCalls = 0;
|
|
167
|
+
const spySource = {
|
|
168
|
+
descriptor: TEST_REGISTRY[0],
|
|
169
|
+
fetchRate: async () => {
|
|
170
|
+
spyCalls++;
|
|
171
|
+
return { ok: true, fact: usdCnyAtT1 };
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
const refused = await consultFxSources({ base: parseCurrency('USD'), quote: parseCurrency('CNY') }, { sources: [spySource], triggerFired: false });
|
|
175
|
+
check(refused.ok === false && refused.code === 'trigger-deferred', '即使传入源列表,触发未开仍在读源之前拒绝');
|
|
176
|
+
check(spyCalls === 0, 'fetch spy 零调用 = 零浮动汇率源被咨询(W2A 式 inert 合同)');
|
|
177
|
+
const noSource = await consultFxSources({ base: parseCurrency('USD'), quote: parseCurrency('CNY') }, { sources: [], triggerFired: true });
|
|
178
|
+
check(noSource.ok === false && noSource.code === 'no-source-admitted', '触发已开但注册表为空:no-source-admitted(不猜源)');
|
|
179
|
+
const viaSpy = await consultFxSources({ base: parseCurrency('USD'), quote: parseCurrency('CNY') }, { sources: [spySource], triggerFired: true });
|
|
180
|
+
check(viaSpy.ok === true && spyCalls === 1 && viaSpy.fact.fact_id === usdCnyAtT1.fact_id, 'seam 数据形状可用:触发后单源返回完整证据事实');
|
|
181
|
+
let primaryCalls = 0;
|
|
182
|
+
let fallbackCalls = 0;
|
|
183
|
+
const missPrimary = {
|
|
184
|
+
descriptor: { id: 'miss-primary', tier: 'primary', legal_basis: 'test-only' },
|
|
185
|
+
fetchRate: async () => {
|
|
186
|
+
primaryCalls++;
|
|
187
|
+
return { ok: false, code: 'miss', detail: 'no quote for pair at instant' };
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
const fallbackSource = {
|
|
191
|
+
descriptor: { id: 'fallback-hit', tier: 'fallback', legal_basis: 'test-only' },
|
|
192
|
+
fetchRate: async () => {
|
|
193
|
+
fallbackCalls++;
|
|
194
|
+
return { ok: true, fact: usdCnyAtT1 };
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
const degraded = await consultFxSources({ base: parseCurrency('USD'), quote: parseCurrency('CNY') }, { sources: [missPrimary, fallbackSource], triggerFired: true });
|
|
198
|
+
check(degraded.ok === true && primaryCalls === 1 && fallbackCalls === 1, '降级顺位形状:primary miss → fallback 命中(各一次)');
|
|
199
|
+
const allMiss = await consultFxSources({ base: parseCurrency('USD'), quote: parseCurrency('CNY') }, { sources: [missPrimary], triggerFired: true });
|
|
200
|
+
check(allMiss.ok === false && allMiss.code === 'miss', 'miss 不伪装为当前准确换算(结构化 miss 直传)');
|
|
201
|
+
console.log(`\nFX CONTRACT TESTS: ${passed} pass${process.exitCode ? ', FAIL' : ' (全绿)'}`);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G5 机械闸自测(issue #348;全离线,零网络,零共享 state 写入)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* 1. (17) pattern 命中/不命中双向——真桥接形状(G5_BRIDGE/g5Bridge/
|
|
6
|
+
* capability-travel-mcp/t-system bridge/G5桥)必须命中;承运人代码
|
|
7
|
+
* G5/G52667、persona-bench 案例 G5、base64 噪声、「提到 G5 门」的
|
|
8
|
+
* 门定义句必须不命中(误报会把 tracker 自身打红);
|
|
9
|
+
* 2. (7) 台账解析——GRANT 缺 issue/决策链接 → 拒收(fail closed);
|
|
10
|
+
* code fence 内模板行不计入;glob 语义(** 跨段 / * 单段);覆盖判定;
|
|
11
|
+
* 3. (2) 命名面豁免存在(master-outline 对 + 台账/guard 对);
|
|
12
|
+
* 4. (4) CLI 端到端(临时 git fixture,--root 注入):未授权引用 → exit 1
|
|
13
|
+
* 且输出可操作指引;GRANT 覆盖后 → exit 0;干净仓 → exit 0;
|
|
14
|
+
* 台账缺失 → exit 2(闸自身不可用,而非放行)。
|
|
15
|
+
*
|
|
16
|
+
* 运行:cd ts && npx tsx scripts/g5-guard-tests.ts
|
|
17
|
+
*/
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { tmpdir } from 'node:os';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { globToRegExp, grantCovers, parseGrants, scanContent, NAMING_SURFACES } from './g5-guard.js';
|
|
24
|
+
let assertions = 0;
|
|
25
|
+
function ok(value, message) {
|
|
26
|
+
assert.ok(value, message);
|
|
27
|
+
assertions += 1;
|
|
28
|
+
}
|
|
29
|
+
// ── 1. pattern 命中/不命中双向 ──────────────────────────────────────────────
|
|
30
|
+
const mustHit = [
|
|
31
|
+
['env key', 'G5_BRIDGE_ENDPOINT=https://internal.example'],
|
|
32
|
+
['camelCase identifier', 'const g5Bridge = createBridge()'],
|
|
33
|
+
['hyphen path', 'see ts/src/adapters/g5-bridge/client.ts'],
|
|
34
|
+
['internal MCP plugin id', 'registerPlugin("capability-travel-mcp")'],
|
|
35
|
+
['t-system bridge', 't-system bridge adapter for flights'],
|
|
36
|
+
['bridge adjacent t-system', 'BridgeTSystem flights'],
|
|
37
|
+
['chinese adjacency', '启用 G5 桥'],
|
|
38
|
+
['chinese bridge g5', '桥 G5 工具'],
|
|
39
|
+
['chinese t-system prefix', '桥接 T 系统 的机票工具'],
|
|
40
|
+
['chinese t-system suffix', 'T 系统适配层'],
|
|
41
|
+
];
|
|
42
|
+
for (const [label, text] of mustHit) {
|
|
43
|
+
ok(scanContent('fixture.ts', text).length === 1, `pattern 应命中: ${label} (${text})`);
|
|
44
|
+
}
|
|
45
|
+
const mustNotHit = [
|
|
46
|
+
['carrier code', '{ "carrier_codes": ["CA", "CZ", "G5", "GS"] }'],
|
|
47
|
+
['flight number', '"known_flights": ["G52667", "CZ5817"]'],
|
|
48
|
+
['persona-bench case id', '| G5 | 万xx 指代 | = 甲米 |'],
|
|
49
|
+
['gate mention without bridge adjacency', 'constrained by G5); ② reference'],
|
|
50
|
+
['gate row phrase', 'G5 (internal travel bridge approval)'],
|
|
51
|
+
['base64-ish noise', '"integrity": "sha512-mQ+/0Fo3LTIX+4k3m+P4y9e9IA6/BeNHrWW19U+G5x0Z8BlybHDELZIB=="'],
|
|
52
|
+
['zh gate row phrase', '内部差旅工具桥(T 系统侧,脱敏)的内部审批'],
|
|
53
|
+
];
|
|
54
|
+
for (const [label, text] of mustNotHit) {
|
|
55
|
+
ok(scanContent('fixture.ts', text).length === 0, `pattern 不应命中(误报): ${label} (${text})`);
|
|
56
|
+
}
|
|
57
|
+
// ── 2. 台账解析 + glob 语义 ────────────────────────────────────────────────
|
|
58
|
+
const empty = parseGrants('# G5 Authorization Ledger\n\n(empty)\n');
|
|
59
|
+
ok(empty.grants.length === 0 && empty.malformed === 0, '空台账应解析为 0 授权');
|
|
60
|
+
const noLink = parseGrants('- GRANT: src/** — some bridge\n');
|
|
61
|
+
ok(noLink.grants.length === 0 && noLink.malformed === 1, '缺 issue/决策链接的 GRANT 应被拒收(fail closed)');
|
|
62
|
+
const ledger = parseGrants([
|
|
63
|
+
'# ledger',
|
|
64
|
+
'- GRANT: src/adapters/** — masked flight reads, tenant X — issue: #348',
|
|
65
|
+
'- GRANT: docs/notes.md — demo — decision: https://example/decision',
|
|
66
|
+
].join('\n'));
|
|
67
|
+
ok(ledger.grants.length === 2 && ledger.malformed === 0, '带 issue/决策链接的 GRANT 应成立');
|
|
68
|
+
ok(grantCovers(ledger.grants[0], 'src/adapters/flight/client.ts'), '`**` 应跨段覆盖');
|
|
69
|
+
ok(!grantCovers(ledger.grants[0], 'docs/notes.md'), '不匹配的路径不应被覆盖');
|
|
70
|
+
const withTemplate = parseGrants([
|
|
71
|
+
'# ledger',
|
|
72
|
+
'```',
|
|
73
|
+
'- GRANT: <path-glob> — system/purpose/scope one-liner — issue/decision: #<n> or URL',
|
|
74
|
+
'```',
|
|
75
|
+
'- GRANT: src/** — real entry — issue: #348',
|
|
76
|
+
].join('\n'));
|
|
77
|
+
ok(withTemplate.grants.length === 1 && withTemplate.malformed === 0, 'code fence 内的模板行不应计入 GRANT/畸形');
|
|
78
|
+
const single = globToRegExp('docs/*.md');
|
|
79
|
+
ok(single.test('docs/a.md') && !single.test('docs/sub/b.md'), '`*` 应限单段');
|
|
80
|
+
// ── 3. 命名面豁免 ──────────────────────────────────────────────────────────
|
|
81
|
+
ok(NAMING_SURFACES.has('docs/gotry-master-outline.md') && NAMING_SURFACES.has('docs/gotry-master-outline.zh-CN.md'), '门定义对必须在命名面');
|
|
82
|
+
ok(NAMING_SURFACES.has('docs/g5-authorization-ledger.md') && NAMING_SURFACES.has('ts/scripts/g5-guard.ts'), '台账与 guard 自身必须在命名面');
|
|
83
|
+
// ── 4. CLI 端到端(临时 git fixture) ───────────────────────────────────────
|
|
84
|
+
const tsRoot = join(import.meta.dirname, '..');
|
|
85
|
+
const guardScript = join(tsRoot, 'scripts', 'g5-guard.ts');
|
|
86
|
+
const tsxBin = join(tsRoot, 'node_modules', '.bin', 'tsx');
|
|
87
|
+
function makeFixture(files) {
|
|
88
|
+
const root = mkdtempSync(join(tmpdir(), 'g5-guard-fixture-'));
|
|
89
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
90
|
+
const abs = join(root, rel);
|
|
91
|
+
mkdirSync(join(abs, '..'), { recursive: true });
|
|
92
|
+
writeFileSync(abs, content);
|
|
93
|
+
}
|
|
94
|
+
spawnSync('git', ['init', '-q'], { cwd: root });
|
|
95
|
+
spawnSync('git', ['add', '-A'], { cwd: root });
|
|
96
|
+
return root;
|
|
97
|
+
}
|
|
98
|
+
function runGuard(root) {
|
|
99
|
+
const result = spawnSync(tsxBin, [guardScript, '--root', root], { encoding: 'utf8' });
|
|
100
|
+
return { status: result.status ?? -1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
101
|
+
}
|
|
102
|
+
const LEDGER_HEADER = [
|
|
103
|
+
'# G5 Authorization Ledger',
|
|
104
|
+
'',
|
|
105
|
+
'## Entries',
|
|
106
|
+
'',
|
|
107
|
+
'(empty)',
|
|
108
|
+
'',
|
|
109
|
+
].join('\n');
|
|
110
|
+
// 红:未授权桥接引用 → exit 1 + 可操作指引
|
|
111
|
+
const redRoot = makeFixture({
|
|
112
|
+
'src/adapter.ts': 'export const endpoint = process.env.G5_BRIDGE_ENDPOINT\n',
|
|
113
|
+
'docs/README.md': 'docs index\n',
|
|
114
|
+
'docs/g5-authorization-ledger.md': LEDGER_HEADER,
|
|
115
|
+
});
|
|
116
|
+
const red = runGuard(redRoot);
|
|
117
|
+
ok(red.status === 1 && red.stderr.includes('src/adapter.ts') && red.stderr.includes('issues/348'), `未授权引用应 exit 1 且给出路径与 #348 指引(status=${red.status}, stderr=${red.stderr.slice(0, 200)})`);
|
|
118
|
+
// 绿:创始人侧 GRANT 覆盖后 → exit 0(fixture 内的 GRANT 仅演示解析,非真实授权)
|
|
119
|
+
writeFileSync(join(redRoot, 'docs', 'g5-authorization-ledger.md'), LEDGER_HEADER + '- GRANT: src/** — fixture demo only, not a real grant — issue: #348\n');
|
|
120
|
+
const green = runGuard(redRoot);
|
|
121
|
+
ok(green.status === 0, `GRANT 覆盖后应 exit 0(status=${green.status}, stderr=${green.stderr.slice(0, 200)})`);
|
|
122
|
+
rmSync(redRoot, { recursive: true, force: true });
|
|
123
|
+
// 绿:干净仓(无任何引用)→ exit 0
|
|
124
|
+
const cleanRoot = makeFixture({ 'src/adapter.ts': 'export const endpoint = "https://api.hotelbyte.example"\n', 'docs/g5-authorization-ledger.md': LEDGER_HEADER });
|
|
125
|
+
ok(runGuard(cleanRoot).status === 0, '干净仓应 exit 0');
|
|
126
|
+
rmSync(cleanRoot, { recursive: true, force: true });
|
|
127
|
+
// exit 2:台账缺失 → 闸不可用,而不是放行
|
|
128
|
+
const noLedgerRoot = makeFixture({ 'src/adapter.ts': 'export const x = 1\n' });
|
|
129
|
+
ok(runGuard(noLedgerRoot).status === 2, '台账缺失应 exit 2(闸配置错误),不得放行');
|
|
130
|
+
rmSync(noLedgerRoot, { recursive: true, force: true });
|
|
131
|
+
console.log(`G5 GUARD TESTS: ${assertions}/${assertions} OK(命中/不命中双向 / 台账 fail-closed+glob / 命名面 / CLI 红绿+exit2)`);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G5 内部差旅桥机械闸(issue #348;governance/复用矩阵纪律的机械化,不是新政策)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:G5 = 「内部差旅工具桥(T 系统侧,脱敏)」的创始人审批门,当前关闭
|
|
5
|
+
* (docs/gotry-master-outline.md 决策门表 + #348)。复用矩阵纪律:内部资产只能
|
|
6
|
+
* bridge/reference,桥接前必须有创始人显式授权。本脚本把该约束落成机械检查:
|
|
7
|
+
* 1. 扫描全部 git-tracked 文件(排除第三方/生成物/创始人私有 state)中
|
|
8
|
+
* 「G5 桥接引用」形状的标识符/配置/文案;
|
|
9
|
+
* 2. 命中时要求 docs/g5-authorization-ledger.md 存在覆盖该路径的 GRANT 条目
|
|
10
|
+
* (含 issue/决策链接);没有 → exit 1,输出可操作指引。
|
|
11
|
+
*
|
|
12
|
+
* 边界(诚实声明):本闸只证明「无授权记录 → 桥接引用不落地」;
|
|
13
|
+
* GRANT 条目只能由创始人侧维护,agent 自助授权不被本闸承认(作者身份靠
|
|
14
|
+
* git review,机械不可证)。真实 G5 授权本身仍是创始人决策(#348)。
|
|
15
|
+
*
|
|
16
|
+
* 命名面豁免(NAMING_SURFACES):门定义/授权台账/guard 自身必须能「提到」
|
|
17
|
+
* 这条关闭中的 lane,否则 tracker 无法描述自己的门。除此之外一律要求 GRANT。
|
|
18
|
+
*
|
|
19
|
+
* 用法:cd ts && npx tsx scripts/g5-guard.ts [--root <dir>]
|
|
20
|
+
* 退出码:0 = 通过;1 = 存在未授权引用;2 = 闸自身无法运行(缺台账/git 失败)。
|
|
21
|
+
* 自测:cd ts && npx tsx scripts/g5-guard-tests.ts(全离线,临时 fixture)。
|
|
22
|
+
*/
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
25
|
+
import { dirname, join, resolve } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
export const LEDGER_REL = 'docs/g5-authorization-ledger.md';
|
|
28
|
+
export const ISSUE_348_URL = 'https://github.com/Danceiny/gotry/issues/348';
|
|
29
|
+
/**
|
|
30
|
+
* 「G5 桥接引用」的机械形状:标识符/配置键/插件名/中英文邻接短语。
|
|
31
|
+
* 边界用显式 lookaround 而非 `\b`:下划线是 word char,`\b` 会漏掉
|
|
32
|
+
* snake_case(G5_BRIDGE_*)与 camelCase(G5Bridge)相邻形状。
|
|
33
|
+
*/
|
|
34
|
+
export const G5_BRIDGE_PATTERNS = [
|
|
35
|
+
{ name: 'g5-adjacent-bridge-identifier', regex: /(?<![a-z0-9])g5[\s_-]{0,2}(bridge|bridged|bridging|adapter|adaptor|mcp)/i },
|
|
36
|
+
{ name: 'bridge-adjacent-g5-identifier', regex: /(?<![a-z0-9])(bridge|bridged|bridging|adapter|adaptor)[\s_-]{0,2}g5(?![0-9])/i },
|
|
37
|
+
{ name: 'internal-travel-mcp-plugin-id', regex: /\bcapability-travel-mcp\b/i },
|
|
38
|
+
{ name: 't-system-bridge-identifier', regex: /(?<![a-z0-9])t[-_ ]?system[\s_-]{0,2}(bridge|bridged|bridging|adapter|adaptor|mcp)/i },
|
|
39
|
+
{ name: 'bridge-adjacent-t-system', regex: /(?<![a-z0-9])(bridge|bridged|bridging|adapter|adaptor)[\s_-]{0,2}t[-_ ]?system/i },
|
|
40
|
+
{ name: 'chinese-g5-bridge-adjacency', regex: /g5\s*桥|桥\s*g5/i },
|
|
41
|
+
{ name: 'chinese-t-system-bridge-adjacency', regex: /T\s*系统\s*(桥|适配|接入)|(桥接|接入)\s*T\s*系统/ },
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* 命名面豁免:这些文件被允许「提到」G5 lane(定义它/授权它/检测它),
|
|
45
|
+
* 它们不是授权——真实桥接落在其它任何文件都仍需台账 GRANT。
|
|
46
|
+
* - master-outline 对:G5 门定义 + 复用矩阵 T 系统行(tracker 本体);
|
|
47
|
+
* - 授权台账对:授权面自身;
|
|
48
|
+
* - guard + 自测:检测器必须含有自己的检测串。
|
|
49
|
+
*/
|
|
50
|
+
export const NAMING_SURFACES = new Set([
|
|
51
|
+
'docs/gotry-master-outline.md',
|
|
52
|
+
'docs/gotry-master-outline.zh-CN.md',
|
|
53
|
+
'docs/g5-authorization-ledger.md',
|
|
54
|
+
'docs/g5-authorization-ledger.zh-CN.md',
|
|
55
|
+
'ts/scripts/g5-guard.ts',
|
|
56
|
+
'ts/scripts/g5-guard-tests.ts',
|
|
57
|
+
]);
|
|
58
|
+
/** 排除段:第三方闭包/生成物/创始人私有产品数据(只读红线目录也不扫)。 */
|
|
59
|
+
const EXCLUDED_SEGMENTS = ['node_modules', 'vendor', 'dist', '.git', 'dsh-runtime', 'gotry-state'];
|
|
60
|
+
const LOCKFILE_BASENAMES = new Set(['package-lock.json', 'pnpm-lock.yaml', 'uv.lock', 'bun.lock', 'bun.lockb', 'npm-shrinkwrap.json']);
|
|
61
|
+
const BINARY_EXTENSIONS = new Set([
|
|
62
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
63
|
+
'.mp4', '.mov', '.zip', '.gz', '.tgz', '.bz2', '.7z', '.pdf', '.wasm', '.node', '.dylib', '.so', '.bin', '.icns',
|
|
64
|
+
]);
|
|
65
|
+
export function isExcludedPath(relPath) {
|
|
66
|
+
const segments = relPath.split('/');
|
|
67
|
+
if (segments.some((s) => EXCLUDED_SEGMENTS.includes(s)))
|
|
68
|
+
return true;
|
|
69
|
+
const base = segments[segments.length - 1];
|
|
70
|
+
if (LOCKFILE_BASENAMES.has(base))
|
|
71
|
+
return true;
|
|
72
|
+
const dot = base.lastIndexOf('.');
|
|
73
|
+
if (dot >= 0 && BINARY_EXTENSIONS.has(base.slice(dot).toLowerCase()))
|
|
74
|
+
return true;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
/** 单文件内容扫描:返回全部命中行(文件级,供 collectHits 逐文件调用)。 */
|
|
78
|
+
export function scanContent(relPath, content) {
|
|
79
|
+
const hits = [];
|
|
80
|
+
const lines = content.split('\n');
|
|
81
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
82
|
+
const line = lines[i];
|
|
83
|
+
for (const { name, regex } of G5_BRIDGE_PATTERNS) {
|
|
84
|
+
if (regex.test(line)) {
|
|
85
|
+
hits.push({ path: relPath, line: i + 1, pattern: name, text: line.trim().slice(0, 160) });
|
|
86
|
+
break; // 一行一次足矣,避免同 pattern 行内重复刷屏
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return hits;
|
|
91
|
+
}
|
|
92
|
+
/** 台账 GRANT 行:`- GRANT: <glob> — <说明,必须含 issue/决策链接>`;code fence 内的行(模板/示例)不算数。 */
|
|
93
|
+
export function parseGrants(ledgerText) {
|
|
94
|
+
const grants = [];
|
|
95
|
+
let malformed = 0;
|
|
96
|
+
let inFence = false;
|
|
97
|
+
for (const rawLine of ledgerText.split('\n')) {
|
|
98
|
+
if (/^`{3,}/.test(rawLine.trim())) {
|
|
99
|
+
inFence = !inFence;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (inFence)
|
|
103
|
+
continue;
|
|
104
|
+
const match = rawLine.match(/^-\s*GRANT:\s*(\S+)\s+—\s+(.+)$/);
|
|
105
|
+
if (!match)
|
|
106
|
+
continue;
|
|
107
|
+
// 无 issue/决策链接的 GRANT 视为不成立(fail closed),单独计数以便报错指引
|
|
108
|
+
if (!/#\d+|https?:\/\//.test(match[2])) {
|
|
109
|
+
malformed += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
grants.push({ glob: match[1], description: match[2].trim() });
|
|
113
|
+
}
|
|
114
|
+
return { grants, malformed };
|
|
115
|
+
}
|
|
116
|
+
/** glob → 路径正则:`**` 跨段,`*` 单段内,`?` 单字符。 */
|
|
117
|
+
export function globToRegExp(glob) {
|
|
118
|
+
const escaped = glob
|
|
119
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
120
|
+
.replace(/\*\*/g, '\u0000')
|
|
121
|
+
.replace(/\*/g, '[^/]*')
|
|
122
|
+
.replace(/\?/g, '[^/]')
|
|
123
|
+
.replace(/\u0000/g, '.*');
|
|
124
|
+
return new RegExp(`^${escaped}$`);
|
|
125
|
+
}
|
|
126
|
+
export function grantCovers(grant, relPath) {
|
|
127
|
+
return globToRegExp(grant.glob).test(relPath);
|
|
128
|
+
}
|
|
129
|
+
function listTrackedFiles(root) {
|
|
130
|
+
const result = spawnSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'buffer' });
|
|
131
|
+
if (result.error || result.status !== 0) {
|
|
132
|
+
console.error(`G5 GUARD: 无法枚举 git-tracked 文件(需要 git 仓库): ${result.error?.message ?? result.stderr?.toString().trim()}`);
|
|
133
|
+
process.exit(2);
|
|
134
|
+
}
|
|
135
|
+
return result.stdout.toString().split('\0').filter(Boolean);
|
|
136
|
+
}
|
|
137
|
+
function main() {
|
|
138
|
+
const args = process.argv.slice(2);
|
|
139
|
+
const rootIndex = args.indexOf('--root');
|
|
140
|
+
const root = rootIndex >= 0 ? resolve(args[rootIndex + 1]) : resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
141
|
+
const ledgerPath = join(root, LEDGER_REL);
|
|
142
|
+
if (!existsSync(ledgerPath)) {
|
|
143
|
+
console.error(`G5 GUARD: 授权台账不存在: ${LEDGER_REL}(闸配置错误,exit 2;台账由创始人侧维护)`);
|
|
144
|
+
process.exit(2);
|
|
145
|
+
}
|
|
146
|
+
const ledger = parseGrants(readFileSync(ledgerPath, 'utf8'));
|
|
147
|
+
const hits = [];
|
|
148
|
+
let scanned = 0;
|
|
149
|
+
for (const relPath of listTrackedFiles(root)) {
|
|
150
|
+
if (isExcludedPath(relPath))
|
|
151
|
+
continue;
|
|
152
|
+
if (NAMING_SURFACES.has(relPath))
|
|
153
|
+
continue;
|
|
154
|
+
const abs = join(root, relPath);
|
|
155
|
+
let content;
|
|
156
|
+
try {
|
|
157
|
+
content = readFileSync(abs, 'utf8');
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
continue; // 不可读(权限/符号链接悬空):跳过,不属于闸的裁决面
|
|
161
|
+
}
|
|
162
|
+
if (content.includes('\0'))
|
|
163
|
+
continue; // 二进制内容:跳过
|
|
164
|
+
scanned += 1;
|
|
165
|
+
hits.push(...scanContent(relPath, content));
|
|
166
|
+
}
|
|
167
|
+
if (hits.length === 0) {
|
|
168
|
+
console.log(`G5 GUARD: OK — ${scanned} 个 tracked 文件中无未授权 G5 桥引用;台账 GRANT: ${ledger.grants.length}${ledger.malformed ? `(另有 ${ledger.malformed} 条缺 issue/决策链接的 GRANT 被拒)` : ''}`);
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
const uncovered = hits.filter((hit) => !ledger.grants.some((grant) => grantCovers(grant, hit.path)));
|
|
172
|
+
for (const hit of hits) {
|
|
173
|
+
const covered = ledger.grants.some((grant) => grantCovers(grant, hit.path));
|
|
174
|
+
console.error(` ${hit.path}:${hit.line} [${hit.pattern}]${covered ? ' (已授权)' : ''} ${hit.text}`);
|
|
175
|
+
}
|
|
176
|
+
if (uncovered.length === 0) {
|
|
177
|
+
console.log(`G5 GUARD: OK — ${hits.length} 处 G5 桥引用均被台账 GRANT 覆盖`);
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
console.error(`G5 GUARD: FAIL — ${uncovered.length} 处未授权的 G5 内部差旅桥引用(共扫描 ${scanned} 个文件):`);
|
|
181
|
+
console.error('G5 门当前关闭:桥接内部旅行系统(T 系统)前必须获得创始人显式授权,可执行其一:');
|
|
182
|
+
console.error(` 1. 由创始人在 ${LEDGER_REL} 增加覆盖上述路径的 GRANT 条目(含 issue/决策链接;仅创始人侧维护,agent 不得自助授权);`);
|
|
183
|
+
console.error(' 2. 或移除引用,保持外部供应商路径(公共 MIT hotelbyte-cli 进程桥)。');
|
|
184
|
+
console.error(`Tracker: ${ISSUE_348_URL}(复用矩阵:内部资产只能 bridge/reference,禁止代码/schema 搬运)`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const isDirectRun = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
188
|
+
if (isDirectRun)
|
|
189
|
+
main();
|