@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,1081 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* LLM-persona simulation harness for the M3 capture path (issue #22).
|
|
4
|
+
*
|
|
5
|
+
* EVIDENCE BOUNDARY — read this before reading the code.
|
|
6
|
+
* Everything this harness produces is SYNTHETIC. It drives a persona LLM against the real
|
|
7
|
+
* GoTry session logic and records the funnel with `src/m3-cohort.ts`, so it validates the
|
|
8
|
+
* capture path, the funnel mechanics, interview friction and system-side measurements
|
|
9
|
+
* (fact-gate claim adjudication). It proves NOTHING about real-traveller value and can
|
|
10
|
+
* never contribute to M3 or M4 exit evidence. Two record-level facts enforce that, and
|
|
11
|
+
* neither depends on the other:
|
|
12
|
+
* - every simulated participant is enrolled `test_or_staff=true`, so the scorer's own
|
|
13
|
+
* exclusion drops all of them and its eligible sample is 0 — true even if a manifest
|
|
14
|
+
* were relabelled, because the exclusion lives in the records;
|
|
15
|
+
* - the manifest carries `evidence_kind=synthetic_fixture`, which `product-metrics.ts`
|
|
16
|
+
* refuses to turn into `business_pass=true` regardless of the numbers.
|
|
17
|
+
* The export attestation is a third, weaker thing: it is keyed, so it detects tampering by
|
|
18
|
+
* anyone without the capture key — it says nothing about whether the participants were real.
|
|
19
|
+
* Because the scorer excludes every simulated participant, the funnel numbers a reader
|
|
20
|
+
* wants are computed here by `summarizeFunnel()`, not read off the scorer.
|
|
21
|
+
*
|
|
22
|
+
* What is real in a run and what is simulated:
|
|
23
|
+
* real — the deterministic interview (`src/loop.ts interviewNext`), the spec gate,
|
|
24
|
+
* the planning-window gate, the unified solver (`src/unified.ts`), the
|
|
25
|
+
* rendered plan, and the registered `gotry_fact_gate` tool reading the
|
|
26
|
+
* session's own bookable-fact registry from an isolated `stateRoot`;
|
|
27
|
+
* simulated — the traveller. Persona utterances, the finalize decision and the NPS score
|
|
28
|
+
* come from the persona LLM reading a persona card, not from a person.
|
|
29
|
+
*
|
|
30
|
+
* Harness shape (documented in docs/evaluation/persona-sim-report.md §2): the product side
|
|
31
|
+
* runs in-process through the same seam `scripts/nightly-evidence.ts` already uses —
|
|
32
|
+
* `createOpenAICompatLlm` + `newState`/`runTurn` + `solveUnified` — and the product's own
|
|
33
|
+
* registered tools are mounted with `src/index.ts apply()` against a per-persona
|
|
34
|
+
* `mkdtemp` stateRoot, exactly the way `scripts/smoke.ts` does it.
|
|
35
|
+
*
|
|
36
|
+
* Discipline:
|
|
37
|
+
* - no credentials and not `--dry-run` => exit 0 with state `waiting_external_evidence`,
|
|
38
|
+
* zero writes, zero spend (same stop rule as `scripts/nightly-evidence.ts`);
|
|
39
|
+
* - this script never reads a `.env` file: credentials come from the environment only;
|
|
40
|
+
* - every model must have a sealed price entry before any spend (fail-closed), and
|
|
41
|
+
* `responsesMissingUsage > 0` makes cost unprovable, which is also fail-closed;
|
|
42
|
+
* - `GOTRY_PERSONA_BUDGET_USD` (default 0.25) stops the batch and exits 3;
|
|
43
|
+
* - PATH/HOME are scrubbed for the duration of the batch so the session's POI probe
|
|
44
|
+
* (`src/loop.ts` -> `capabilities/anything.ts`) cannot reach the live `hbcli` backend,
|
|
45
|
+
* and `GOTRY_HBCLI_LIVE` defaults to `0` (the probe's explicit offline switch, issue #617;
|
|
46
|
+
* a value the operator set explicitly is not overridden);
|
|
47
|
+
* - one `mkdtemp` stateRoot per persona, bounded turns, bounded deadlines, concurrency
|
|
48
|
+
* <= 2, deterministic persona ordering, every temp dir and server closed in `finally`.
|
|
49
|
+
*
|
|
50
|
+
* Offline: npx tsx scripts/persona-sim.ts --dry-run --format json
|
|
51
|
+
* Real: see docs/evaluation/persona-sim-report.md §3 for the single command.
|
|
52
|
+
*/
|
|
53
|
+
import { createHash } from 'node:crypto';
|
|
54
|
+
import { createServer } from 'node:http';
|
|
55
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
56
|
+
import { tmpdir } from 'node:os';
|
|
57
|
+
import { dirname, join, resolve } from 'node:path';
|
|
58
|
+
import { fileURLToPath } from 'node:url';
|
|
59
|
+
import { apply } from '../src/index.js';
|
|
60
|
+
import { createOpenAICompatLlm } from '../src/dsh-llm.js';
|
|
61
|
+
import { newState, renderSolve, runTurn } from '../src/loop.js';
|
|
62
|
+
import { solveUnified } from '../src/unified.js';
|
|
63
|
+
import { realtimeSolvePort } from '../src/realtime-pricing.js';
|
|
64
|
+
import { assertSafeStateRoot, enrollParticipant, exportM3Cohort, initM3Cohort, M3CohortError, M3_SIMULATION_PROVENANCE_SCHEMA, recordNps, recordPlanDelivered, recordPlanFinalized, recordPoiLock, verifyExportedEvidence, } from '../src/m3-cohort.js';
|
|
65
|
+
import { loadPriceTable, priceRunCost } from './nightly-evidence.js';
|
|
66
|
+
import { parseCohortRecord, parseManifest, scoreProductMetrics } from './product-metrics.js';
|
|
67
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
68
|
+
const TS_ROOT = resolve(__dirname, '..');
|
|
69
|
+
const REPO_ROOT = resolve(TS_ROOT, '..');
|
|
70
|
+
export const PERSONA_DECK_SCHEMA = 'gotry_persona_sim_deck_v1';
|
|
71
|
+
export const DEFAULT_DECK_PATH = join(TS_ROOT, 'data', 'persona-sim', 'personas.json');
|
|
72
|
+
export const DEFAULT_BUDGET_USD = 0.25;
|
|
73
|
+
export const DEFAULT_MAX_TURNS = 8;
|
|
74
|
+
export const MAX_CONCURRENCY = 2;
|
|
75
|
+
export const PERSONA_SYSTEM_MARKER = 'GOTRY-PERSONA-SIM';
|
|
76
|
+
/** Non-secret fixture values used only by `--dry-run`; a real batch must supply its own. */
|
|
77
|
+
export const FIXTURE_HMAC_KEY = 'gotry-persona-sim-dry-run-fixture-key-0000';
|
|
78
|
+
export const FIXTURE_CONSENT = 'persona-sim dry run: synthetic personas only, no real participant';
|
|
79
|
+
export const FIXTURE_CLOCK_START = '2026-10-05T00:00:00.000Z';
|
|
80
|
+
export class PersonaSimError extends Error {
|
|
81
|
+
code;
|
|
82
|
+
constructor(code, detail) {
|
|
83
|
+
super(detail ? `${code}: ${detail}` : code);
|
|
84
|
+
this.code = code;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function fail(code, detail) {
|
|
88
|
+
throw new PersonaSimError(code, detail);
|
|
89
|
+
}
|
|
90
|
+
const PERSONA_ID = /^[a-z][a-z0-9-]{0,63}$/;
|
|
91
|
+
function isObject(value) {
|
|
92
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
function deckString(value, label, limit = 2048) {
|
|
95
|
+
if (typeof value !== 'string' || value.trim() === '' || value.length > limit)
|
|
96
|
+
fail('invalid_persona_deck', label);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function deckStringArray(value, label) {
|
|
100
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
101
|
+
fail('invalid_persona_deck', label);
|
|
102
|
+
return value.map((item, index) => deckString(item, `${label}[${index}]`));
|
|
103
|
+
}
|
|
104
|
+
function deckKeys(value, label, keys) {
|
|
105
|
+
const expected = new Set(keys);
|
|
106
|
+
for (const key of Object.keys(value))
|
|
107
|
+
if (!expected.has(key))
|
|
108
|
+
fail('invalid_persona_deck', `${label} has unknown field ${key}`);
|
|
109
|
+
for (const key of keys)
|
|
110
|
+
if (!(key in value))
|
|
111
|
+
fail('invalid_persona_deck', `${label} missing ${key}`);
|
|
112
|
+
}
|
|
113
|
+
function deckLiteral(value, label, allowed) {
|
|
114
|
+
if (typeof value !== 'string' || !allowed.includes(value))
|
|
115
|
+
fail('invalid_persona_deck', label);
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
function deckPositiveInt(value, label, max) {
|
|
119
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > max)
|
|
120
|
+
fail('invalid_persona_deck', label);
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
function parseScriptTurn(value, label) {
|
|
124
|
+
if (!isObject(value))
|
|
125
|
+
fail('invalid_persona_deck', label);
|
|
126
|
+
for (const key of Object.keys(value)) {
|
|
127
|
+
if (!['decision', 'message', 'nps'].includes(key))
|
|
128
|
+
fail('invalid_persona_deck', `${label} has unknown field ${key}`);
|
|
129
|
+
}
|
|
130
|
+
const decision = value['decision'];
|
|
131
|
+
if (decision !== 'continue' && decision !== 'finalize' && decision !== 'abandon')
|
|
132
|
+
fail('invalid_persona_deck', `${label}.decision`);
|
|
133
|
+
const turn = { decision };
|
|
134
|
+
if (value['message'] !== undefined)
|
|
135
|
+
turn.message = deckString(value['message'], `${label}.message`);
|
|
136
|
+
if (value['nps'] !== undefined) {
|
|
137
|
+
const nps = value['nps'];
|
|
138
|
+
if (typeof nps !== 'number' || !Number.isInteger(nps) || nps < 0 || nps > 10)
|
|
139
|
+
fail('invalid_persona_deck', `${label}.nps`);
|
|
140
|
+
turn.nps = nps;
|
|
141
|
+
}
|
|
142
|
+
if (decision === 'continue' && turn.message === undefined)
|
|
143
|
+
fail('invalid_persona_deck', `${label} continue needs a message`);
|
|
144
|
+
return turn;
|
|
145
|
+
}
|
|
146
|
+
export function parsePersonaDeck(raw) {
|
|
147
|
+
if (!isObject(raw))
|
|
148
|
+
fail('invalid_persona_deck', 'deck must be an object');
|
|
149
|
+
deckKeys(raw, 'deck', ['schema_version', 'description', 'personas']);
|
|
150
|
+
if (raw['schema_version'] !== PERSONA_DECK_SCHEMA)
|
|
151
|
+
fail('invalid_persona_deck', 'schema_version');
|
|
152
|
+
deckString(raw['description'], 'deck.description', 4096);
|
|
153
|
+
const personas = raw['personas'];
|
|
154
|
+
if (!Array.isArray(personas) || personas.length < 6)
|
|
155
|
+
fail('invalid_persona_deck', 'deck needs at least 6 personas');
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
const cards = personas.map((value, index) => {
|
|
158
|
+
const label = `personas[${index}]`;
|
|
159
|
+
if (!isObject(value))
|
|
160
|
+
fail('invalid_persona_deck', label);
|
|
161
|
+
deckKeys(value, label, [
|
|
162
|
+
'persona_id', 'title', 'grounding', 'goals', 'hard_constraints', 'patience',
|
|
163
|
+
'answering_style', 'finalize_rule', 'rating_rule', 'opening_message',
|
|
164
|
+
'expected_outcome', 'dry_run',
|
|
165
|
+
]);
|
|
166
|
+
const personaId = deckString(value['persona_id'], `${label}.persona_id`, 64);
|
|
167
|
+
if (!PERSONA_ID.test(personaId))
|
|
168
|
+
fail('invalid_persona_deck', `${label}.persona_id`);
|
|
169
|
+
if (seen.has(personaId))
|
|
170
|
+
fail('invalid_persona_deck', `duplicate persona_id ${personaId}`);
|
|
171
|
+
seen.add(personaId);
|
|
172
|
+
const patience = value['patience'];
|
|
173
|
+
if (!isObject(patience))
|
|
174
|
+
fail('invalid_persona_deck', `${label}.patience`);
|
|
175
|
+
deckKeys(patience, `${label}.patience`, ['max_user_turns', 'abandon_after_questions']);
|
|
176
|
+
const style = deckLiteral(value['answering_style'], `${label}.answering_style`, ['terse', 'verbose', 'evasive']);
|
|
177
|
+
const outcome = deckLiteral(value['expected_outcome'], `${label}.expected_outcome`, [
|
|
178
|
+
'plan_delivered_and_finalized', 'plan_delivered_not_finalized', 'interview_only',
|
|
179
|
+
]);
|
|
180
|
+
const dryRun = value['dry_run'];
|
|
181
|
+
if (!isObject(dryRun))
|
|
182
|
+
fail('invalid_persona_deck', `${label}.dry_run`);
|
|
183
|
+
deckKeys(dryRun, `${label}.dry_run`, ['product_facts', 'product_skeleton', 'product_slots', 'persona_turns']);
|
|
184
|
+
const facts = dryRun['product_facts'];
|
|
185
|
+
if (!Array.isArray(facts) || facts.length === 0 || !facts.every(isObject))
|
|
186
|
+
fail('invalid_persona_deck', `${label}.dry_run.product_facts`);
|
|
187
|
+
const skeleton = dryRun['product_skeleton'];
|
|
188
|
+
if (skeleton !== null && !isObject(skeleton))
|
|
189
|
+
fail('invalid_persona_deck', `${label}.dry_run.product_skeleton`);
|
|
190
|
+
const slots = dryRun['product_slots'];
|
|
191
|
+
if (slots !== null && !isObject(slots))
|
|
192
|
+
fail('invalid_persona_deck', `${label}.dry_run.product_slots`);
|
|
193
|
+
const turns = dryRun['persona_turns'];
|
|
194
|
+
if (!Array.isArray(turns) || turns.length === 0)
|
|
195
|
+
fail('invalid_persona_deck', `${label}.dry_run.persona_turns`);
|
|
196
|
+
return {
|
|
197
|
+
persona_id: personaId,
|
|
198
|
+
title: deckString(value['title'], `${label}.title`, 256),
|
|
199
|
+
grounding: deckString(value['grounding'], `${label}.grounding`),
|
|
200
|
+
goals: deckStringArray(value['goals'], `${label}.goals`),
|
|
201
|
+
hard_constraints: deckStringArray(value['hard_constraints'], `${label}.hard_constraints`),
|
|
202
|
+
patience: {
|
|
203
|
+
max_user_turns: deckPositiveInt(patience['max_user_turns'], `${label}.patience.max_user_turns`, DEFAULT_MAX_TURNS),
|
|
204
|
+
abandon_after_questions: deckPositiveInt(patience['abandon_after_questions'], `${label}.patience.abandon_after_questions`, DEFAULT_MAX_TURNS),
|
|
205
|
+
},
|
|
206
|
+
answering_style: style,
|
|
207
|
+
finalize_rule: deckString(value['finalize_rule'], `${label}.finalize_rule`),
|
|
208
|
+
rating_rule: deckString(value['rating_rule'], `${label}.rating_rule`),
|
|
209
|
+
opening_message: deckString(value['opening_message'], `${label}.opening_message`),
|
|
210
|
+
expected_outcome: outcome,
|
|
211
|
+
dry_run: {
|
|
212
|
+
product_facts: facts,
|
|
213
|
+
product_skeleton: skeleton,
|
|
214
|
+
product_slots: slots,
|
|
215
|
+
persona_turns: turns.map((turn, turnIndex) => parseScriptTurn(turn, `${label}.dry_run.persona_turns[${turnIndex}]`)),
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
// Deterministic ordering, independent of file order.
|
|
220
|
+
return cards.sort((a, b) => a.persona_id.localeCompare(b.persona_id));
|
|
221
|
+
}
|
|
222
|
+
export function loadPersonaDeck(path = DEFAULT_DECK_PATH) {
|
|
223
|
+
let raw;
|
|
224
|
+
try {
|
|
225
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
fail('invalid_persona_deck', `${path}: ${error.message}`);
|
|
229
|
+
}
|
|
230
|
+
return parsePersonaDeck(raw);
|
|
231
|
+
}
|
|
232
|
+
/** The persona system prompt. It is the whole simulated traveller: the provenance digest
|
|
233
|
+
* recorded with every synthetic record is the SHA-256 of exactly this text. */
|
|
234
|
+
export function personaSystemPrompt(card) {
|
|
235
|
+
return [
|
|
236
|
+
`${PERSONA_SYSTEM_MARKER} persona=${card.persona_id}`,
|
|
237
|
+
'You are role-playing a traveller talking to a travel planning assistant. You are a simulation, not a real traveller.',
|
|
238
|
+
`Persona: ${card.title}`,
|
|
239
|
+
`Goals:\n${card.goals.map(goal => `- ${goal}`).join('\n')}`,
|
|
240
|
+
`Hard constraints (never violate, never silently drop):\n${card.hard_constraints.map(item => `- ${item}`).join('\n')}`,
|
|
241
|
+
`Answering style: ${card.answering_style}. Patience: at most ${card.patience.max_user_turns} of your own messages; lose interest after ${card.patience.abandon_after_questions} questions with nothing concrete.`,
|
|
242
|
+
`Finalize rule: ${card.finalize_rule}`,
|
|
243
|
+
`Rating rule: ${card.rating_rule}`,
|
|
244
|
+
'Reply ONLY with a JSON object and nothing else:',
|
|
245
|
+
'{"decision":"continue"|"finalize"|"abandon","message":"your next message to the assistant","nps":<integer 0-10 or null>}',
|
|
246
|
+
'Rules: "continue" requires a non-empty message. "finalize" means you accept the delivered plan. "abandon" means you walk away.',
|
|
247
|
+
'Set "nps" to an integer 0-10 only when you finalize or abandon after seeing a plan; otherwise null. Never invent facts outside your constraints.',
|
|
248
|
+
].join('\n\n');
|
|
249
|
+
}
|
|
250
|
+
export function promptDigest(card) {
|
|
251
|
+
return createHash('sha256').update(personaSystemPrompt(card)).digest('hex');
|
|
252
|
+
}
|
|
253
|
+
const FIXTURE_USAGE = { prompt_tokens: 900, completion_tokens: 180, prompt_cache_hit_tokens: 0, prompt_cache_miss_tokens: 900, total_tokens: 1080 };
|
|
254
|
+
function fixtureBody(model, content, omitUsage) {
|
|
255
|
+
return JSON.stringify({
|
|
256
|
+
id: 'persona-sim-fixture',
|
|
257
|
+
object: 'chat.completion',
|
|
258
|
+
created: 0,
|
|
259
|
+
model,
|
|
260
|
+
choices: [{ index: 0, message: { role: 'assistant', content }, finish_reason: 'stop' }],
|
|
261
|
+
...(omitUsage ? {} : { usage: { ...FIXTURE_USAGE, total_tokens: FIXTURE_USAGE.prompt_tokens + FIXTURE_USAGE.completion_tokens } }),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* A local OpenAI-compatible endpoint serving scripted replies for BOTH models. Requests are
|
|
266
|
+
* classified by the caller's system prompt; an unclassified prompt is a hard 500 rather than
|
|
267
|
+
* a silent fallback, so a prompt change in `src/dsh-llm.ts` surfaces as a test failure.
|
|
268
|
+
*/
|
|
269
|
+
export async function startFixtureProvider(options = {}) {
|
|
270
|
+
const state = { card: null, factsIndex: 0, personaIndex: 0 };
|
|
271
|
+
const calls = { facts: 0, skeleton: 0, slots: 0, polish: 0, persona: 0, unclassified: 0 };
|
|
272
|
+
const omitUsage = options.omitUsage === true;
|
|
273
|
+
const server = createServer((request, response) => {
|
|
274
|
+
const chunks = [];
|
|
275
|
+
request.on('data', chunk => chunks.push(chunk));
|
|
276
|
+
request.on('end', () => {
|
|
277
|
+
let kind = 'unclassified';
|
|
278
|
+
try {
|
|
279
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
280
|
+
const model = String(body.model ?? 'fixture');
|
|
281
|
+
const system = (body.messages ?? []).filter(message => message.role === 'system').map(message => message.content).join('\n');
|
|
282
|
+
const card = state.card;
|
|
283
|
+
if (system.includes(PERSONA_SYSTEM_MARKER)) {
|
|
284
|
+
kind = 'persona';
|
|
285
|
+
if (!card)
|
|
286
|
+
throw new Error('fixture has no active persona');
|
|
287
|
+
const scripted = card.dry_run.persona_turns[Math.min(state.personaIndex, card.dry_run.persona_turns.length - 1)];
|
|
288
|
+
state.personaIndex += 1;
|
|
289
|
+
const payload = {
|
|
290
|
+
decision: scripted.decision,
|
|
291
|
+
message: scripted.message ?? '',
|
|
292
|
+
nps: scripted.nps ?? null,
|
|
293
|
+
};
|
|
294
|
+
calls[kind] = (calls[kind] ?? 0) + 1;
|
|
295
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
296
|
+
response.end(fixtureBody(model, JSON.stringify(payload), omitUsage));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!card)
|
|
300
|
+
throw new Error('fixture has no active persona');
|
|
301
|
+
let content;
|
|
302
|
+
if (system.includes('事实抽取器')) {
|
|
303
|
+
kind = 'facts';
|
|
304
|
+
const facts = card.dry_run.product_facts;
|
|
305
|
+
content = JSON.stringify(facts[Math.min(state.factsIndex, facts.length - 1)]);
|
|
306
|
+
state.factsIndex += 1;
|
|
307
|
+
}
|
|
308
|
+
else if (system.includes('行程骨架抽取器')) {
|
|
309
|
+
kind = 'skeleton';
|
|
310
|
+
content = JSON.stringify(card.dry_run.product_skeleton ?? { scenario: 'generic', segments: [] });
|
|
311
|
+
}
|
|
312
|
+
else if (system.includes('槽位抽取器')) {
|
|
313
|
+
kind = 'slots';
|
|
314
|
+
content = JSON.stringify(card.dry_run.product_slots ?? {});
|
|
315
|
+
}
|
|
316
|
+
else if (system.includes('润色')) {
|
|
317
|
+
kind = 'polish';
|
|
318
|
+
content = (body.messages ?? []).filter(message => message.role === 'user').map(message => message.content).join('\n');
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
calls['unclassified'] = (calls['unclassified'] ?? 0) + 1;
|
|
322
|
+
response.writeHead(500, { 'content-type': 'application/json' });
|
|
323
|
+
response.end(JSON.stringify({ error: 'fixture_unclassified_prompt', system: system.slice(0, 120) }));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
calls[kind] = (calls[kind] ?? 0) + 1;
|
|
327
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
328
|
+
response.end(fixtureBody(model, content, omitUsage));
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
calls['unclassified'] = (calls['unclassified'] ?? 0) + 1;
|
|
332
|
+
response.writeHead(500, { 'content-type': 'application/json' });
|
|
333
|
+
response.end(JSON.stringify({ error: 'fixture_error', detail: error.message.slice(0, 200) }));
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
338
|
+
server.once('error', rejectListen);
|
|
339
|
+
server.listen(0, '127.0.0.1', () => resolveListen());
|
|
340
|
+
});
|
|
341
|
+
const address = server.address();
|
|
342
|
+
if (!address || typeof address === 'string')
|
|
343
|
+
fail('internal_error', 'fixture provider did not bind a port');
|
|
344
|
+
return {
|
|
345
|
+
url: `http://127.0.0.1:${address.port}/v1`,
|
|
346
|
+
calls,
|
|
347
|
+
setPersona(card) {
|
|
348
|
+
state.card = card;
|
|
349
|
+
state.factsIndex = 0;
|
|
350
|
+
state.personaIndex = 0;
|
|
351
|
+
},
|
|
352
|
+
close: () => new Promise(resolveClose => server.close(() => resolveClose())),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function emptyUsage() {
|
|
356
|
+
return { calls: 0, inputTokens: 0, outputTokens: 0, inputCacheHitTokens: 0, inputCacheMissTokens: 0, responsesMissingUsage: 0 };
|
|
357
|
+
}
|
|
358
|
+
function parsePersonaDecision(raw, planDelivered) {
|
|
359
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
360
|
+
if (!match)
|
|
361
|
+
fail('persona_contract_violation', 'persona reply carried no JSON object');
|
|
362
|
+
let parsed;
|
|
363
|
+
try {
|
|
364
|
+
parsed = JSON.parse(match[0]);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
fail('persona_contract_violation', 'persona reply was not valid JSON');
|
|
368
|
+
}
|
|
369
|
+
if (!isObject(parsed))
|
|
370
|
+
fail('persona_contract_violation', 'persona reply was not an object');
|
|
371
|
+
for (const key of Object.keys(parsed)) {
|
|
372
|
+
if (!['decision', 'message', 'nps'].includes(key))
|
|
373
|
+
fail('persona_contract_violation', `persona reply has unknown field ${key}`);
|
|
374
|
+
}
|
|
375
|
+
const decision = parsed['decision'];
|
|
376
|
+
if (decision !== 'continue' && decision !== 'finalize' && decision !== 'abandon')
|
|
377
|
+
fail('persona_contract_violation', 'persona decision outside the closed set');
|
|
378
|
+
if (decision === 'finalize' && !planDelivered)
|
|
379
|
+
fail('persona_contract_violation', 'persona finalized before a plan was delivered');
|
|
380
|
+
const message = parsed['message'];
|
|
381
|
+
if (typeof message !== 'string' || (decision === 'continue' && message.trim() === ''))
|
|
382
|
+
fail('persona_contract_violation', 'persona continue needs a message');
|
|
383
|
+
if (message.length > 4096)
|
|
384
|
+
fail('persona_contract_violation', 'persona message too long');
|
|
385
|
+
const npsRaw = parsed['nps'];
|
|
386
|
+
let nps = null;
|
|
387
|
+
if (npsRaw !== null && npsRaw !== undefined) {
|
|
388
|
+
if (typeof npsRaw !== 'number' || !Number.isInteger(npsRaw) || npsRaw < 0 || npsRaw > 10)
|
|
389
|
+
fail('persona_contract_violation', 'persona nps outside 0..10');
|
|
390
|
+
nps = npsRaw;
|
|
391
|
+
}
|
|
392
|
+
if (nps !== null && !planDelivered)
|
|
393
|
+
fail('persona_contract_violation', 'persona rated a plan it never saw');
|
|
394
|
+
return { decision, message, nps };
|
|
395
|
+
}
|
|
396
|
+
async function personaDecide(endpoint, card, history, planDelivered, usage) {
|
|
397
|
+
const transcript = history.map(turn => `${turn.role === 'user' ? '你' : '助手'}: ${turn.text}`).join('\n');
|
|
398
|
+
const body = {
|
|
399
|
+
model: endpoint.model,
|
|
400
|
+
messages: [
|
|
401
|
+
{ role: 'system', content: personaSystemPrompt(card) },
|
|
402
|
+
{
|
|
403
|
+
role: 'user',
|
|
404
|
+
content: `${planDelivered ? '助手已经给出了方案。' : '助手还没有给出方案。'}\n\n对话记录:\n${transcript}\n\n只输出 JSON。`,
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
response_format: { type: 'json_object' },
|
|
408
|
+
temperature: 0,
|
|
409
|
+
};
|
|
410
|
+
let response;
|
|
411
|
+
try {
|
|
412
|
+
response = await fetch(`${endpoint.baseUrl.replace(/\/$/, '')}/chat/completions`, {
|
|
413
|
+
method: 'POST',
|
|
414
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${endpoint.apiKey}` },
|
|
415
|
+
body: JSON.stringify(body),
|
|
416
|
+
signal: AbortSignal.timeout(endpoint.timeoutMs),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
const name = error.name;
|
|
421
|
+
if (name === 'TimeoutError' || name === 'AbortError')
|
|
422
|
+
fail('persona_timeout', `persona model did not answer within ${endpoint.timeoutMs}ms`);
|
|
423
|
+
fail('provider_error', `persona model transport failure: ${name}`);
|
|
424
|
+
}
|
|
425
|
+
if (!response.ok)
|
|
426
|
+
fail('provider_error', `persona model returned ${response.status}`);
|
|
427
|
+
const data = await response.json();
|
|
428
|
+
usage.calls += 1;
|
|
429
|
+
const reported = data.usage;
|
|
430
|
+
if (reported && Number.isFinite(reported.prompt_tokens) && Number.isFinite(reported.completion_tokens)) {
|
|
431
|
+
usage.inputTokens += reported.prompt_tokens ?? 0;
|
|
432
|
+
usage.outputTokens += reported.completion_tokens ?? 0;
|
|
433
|
+
usage.inputCacheHitTokens += reported.prompt_cache_hit_tokens ?? 0;
|
|
434
|
+
usage.inputCacheMissTokens += reported.prompt_cache_miss_tokens ?? (reported.prompt_tokens ?? 0);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
usage.responsesMissingUsage += 1;
|
|
438
|
+
}
|
|
439
|
+
const content = data.choices?.[0]?.message?.content ?? '';
|
|
440
|
+
return parsePersonaDecision(content.replace(/<think>[\s\S]*?<\/think>/g, '').trim() || content, planDelivered);
|
|
441
|
+
}
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
// POI / bookable-claim audit, derived from the product's own fact gate
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
/**
|
|
446
|
+
* Gate violation kinds where the artifact asserts something the fact registry actively
|
|
447
|
+
* contradicts. These — and only these — are counted as invalid claims.
|
|
448
|
+
*
|
|
449
|
+
* `route_unqueried` / `*_unverified` are deliberately NOT here: an unverified claim is not a
|
|
450
|
+
* hallucination, and in a simulated run the registry is empty, so every claim is unverified.
|
|
451
|
+
* See docs/evaluation/persona-sim-report.md §4 for what this can and cannot mean.
|
|
452
|
+
*/
|
|
453
|
+
export const CONTRADICTION_KINDS = new Set([
|
|
454
|
+
'not_in_source',
|
|
455
|
+
'contradicted',
|
|
456
|
+
'airport_mapping_conflict',
|
|
457
|
+
'price_contradicted',
|
|
458
|
+
'fact_anchor_unknown',
|
|
459
|
+
'unconditional_check',
|
|
460
|
+
'self_transfer_called_through',
|
|
461
|
+
'nights_inconsistent',
|
|
462
|
+
'legs_inconsistent',
|
|
463
|
+
'budget_floor_inconsistent',
|
|
464
|
+
'date_order',
|
|
465
|
+
]);
|
|
466
|
+
/**
|
|
467
|
+
* `locked_claims` is the *audited* denominator, not the extracted claim count.
|
|
468
|
+
*
|
|
469
|
+
* Counting every extracted claim as audited was wrong: with an empty registry a plan whose
|
|
470
|
+
* claims are all `route_unqueried` would record 0/N and the scorer would print a 0% POI
|
|
471
|
+
* rate with `pass=true` — an unaudited plan scoring as a clean one. A claim is only part of
|
|
472
|
+
* the denominator when the registry could adjudicate it, i.e. it came back traceable or
|
|
473
|
+
* contradicted. An empty registry therefore yields `locked_claims=0`, which the scorer
|
|
474
|
+
* reports as `unavailable` with `pass=false`, which is the truth.
|
|
475
|
+
*/
|
|
476
|
+
export function poiAuditFromGateReport(report) {
|
|
477
|
+
const extracted = typeof report.claims_checked === 'number' ? report.claims_checked : 0;
|
|
478
|
+
const traceable = typeof report.traceable === 'number' ? report.traceable : 0;
|
|
479
|
+
const violations = Array.isArray(report.violations) ? report.violations : [];
|
|
480
|
+
const kinds = {};
|
|
481
|
+
let contradicted = 0;
|
|
482
|
+
for (const violation of violations) {
|
|
483
|
+
const kind = String(violation.kind ?? 'unknown');
|
|
484
|
+
kinds[kind] = (kinds[kind] ?? 0) + 1;
|
|
485
|
+
if (CONTRADICTION_KINDS.has(kind))
|
|
486
|
+
contradicted += 1;
|
|
487
|
+
}
|
|
488
|
+
const invalid = Math.min(contradicted, extracted);
|
|
489
|
+
const locked = Math.min(traceable + invalid, extracted);
|
|
490
|
+
return {
|
|
491
|
+
locked_claims: locked,
|
|
492
|
+
invalid_claims: Math.min(invalid, locked),
|
|
493
|
+
claims_extracted: extracted,
|
|
494
|
+
traceable_claims: traceable,
|
|
495
|
+
unverified_claims: Math.max(extracted - locked, 0),
|
|
496
|
+
gate_verdict: report.verdict === 'pass' ? 'pass' : 'blocked',
|
|
497
|
+
presentation: String(report.presentation ?? 'verified_label_forbidden'),
|
|
498
|
+
violation_kinds: kinds,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/** Mount the real product tools against an isolated stateRoot, the way smoke.ts does. */
|
|
502
|
+
function mountProductTools(stateRoot) {
|
|
503
|
+
const registered = new Map();
|
|
504
|
+
const ctx = {
|
|
505
|
+
tools: { register: (tool) => { const typed = tool; registered.set(typed.name, typed); } },
|
|
506
|
+
systemPrompt: { variable: () => { } },
|
|
507
|
+
get: () => undefined,
|
|
508
|
+
on: () => () => { },
|
|
509
|
+
};
|
|
510
|
+
apply(ctx, { stateRoot, timeoutMs: 15_000, hbcliBin: 'hbcli-not-on-path', sessionAccess: 'deny' });
|
|
511
|
+
return registered;
|
|
512
|
+
}
|
|
513
|
+
/** The delivered artifact: the candidate solver's own markdown when the scenario produced
|
|
514
|
+
* one, otherwise the deterministic plan renderer. */
|
|
515
|
+
export function deliveredPlanMarkdown(state) {
|
|
516
|
+
const solve = state.solve;
|
|
517
|
+
if (!solve)
|
|
518
|
+
return null;
|
|
519
|
+
if (typeof solve['answer_md'] === 'string' && solve['answer_md'].trim() !== '')
|
|
520
|
+
return solve['answer_md'];
|
|
521
|
+
const rendered = renderSolve(state);
|
|
522
|
+
return rendered.trim() === '' || rendered === '(无求解结果)' ? null : rendered;
|
|
523
|
+
}
|
|
524
|
+
function solverVerdict(state) {
|
|
525
|
+
const solve = state.solve;
|
|
526
|
+
if (!solve)
|
|
527
|
+
return { verdict: 'none', unsat: [], errorCode: null };
|
|
528
|
+
if (typeof solve['answer_md'] === 'string')
|
|
529
|
+
return { verdict: 'candidate_choice', unsat: [], errorCode: null };
|
|
530
|
+
// issue #620: a solver failure is classified before `feasible` is read at all. The solver
|
|
531
|
+
// crashing says nothing about the trip, so it must not be counted as an infeasibility.
|
|
532
|
+
const solverError = solve['solver_error'];
|
|
533
|
+
if (solverError && typeof solverError === 'object') {
|
|
534
|
+
return { verdict: 'solver_error', unsat: [], errorCode: String(solverError.code ?? 'unknown') };
|
|
535
|
+
}
|
|
536
|
+
const unsat = Array.isArray(solve['unsat_core']) ? solve['unsat_core'].map(String) : [];
|
|
537
|
+
return { verdict: solve['feasible'] === true ? 'feasible' : 'infeasible', unsat, errorCode: null };
|
|
538
|
+
}
|
|
539
|
+
async function withDeadline(promise, ms, code, detail) {
|
|
540
|
+
// A loser promise that rejects after the race would otherwise surface as an unhandled
|
|
541
|
+
// rejection, so it is defused before the race starts.
|
|
542
|
+
promise.catch(() => { });
|
|
543
|
+
let timer;
|
|
544
|
+
try {
|
|
545
|
+
return await Promise.race([
|
|
546
|
+
promise,
|
|
547
|
+
new Promise((_resolveRace, rejectRace) => {
|
|
548
|
+
timer = setTimeout(() => rejectRace(new PersonaSimError(code, detail)), ms);
|
|
549
|
+
}),
|
|
550
|
+
]);
|
|
551
|
+
}
|
|
552
|
+
finally {
|
|
553
|
+
if (timer)
|
|
554
|
+
clearTimeout(timer);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async function runPersonaSession(options) {
|
|
558
|
+
const { card, personaEndpoint, clock, productUsage, personaUsage } = options;
|
|
559
|
+
const outcome = {
|
|
560
|
+
persona_id: card.persona_id,
|
|
561
|
+
expected_outcome: card.expected_outcome,
|
|
562
|
+
user_turns: 0,
|
|
563
|
+
plan_delivered: false,
|
|
564
|
+
finalized: false,
|
|
565
|
+
abandoned: false,
|
|
566
|
+
nps: null,
|
|
567
|
+
solver_verdict: 'none',
|
|
568
|
+
unsat_core: [],
|
|
569
|
+
solver_error_code: null,
|
|
570
|
+
poi_audit: null,
|
|
571
|
+
plan_markdown_sha256: null,
|
|
572
|
+
product_usage: productUsage,
|
|
573
|
+
persona_usage: personaUsage,
|
|
574
|
+
error: null,
|
|
575
|
+
};
|
|
576
|
+
const tools = mountProductTools(options.stateRoot);
|
|
577
|
+
const factGate = tools.get('gotry_fact_gate');
|
|
578
|
+
if (!factGate)
|
|
579
|
+
fail('internal_error', 'gotry_fact_gate is not registered — the product tool surface changed');
|
|
580
|
+
const now = clock.now();
|
|
581
|
+
const port = createOpenAICompatLlm(options.packPath, () => now);
|
|
582
|
+
const syncProductUsage = () => {
|
|
583
|
+
const tracked = port.usage;
|
|
584
|
+
if (tracked)
|
|
585
|
+
Object.assign(productUsage, tracked);
|
|
586
|
+
};
|
|
587
|
+
const solvePort = realtimeSolvePort(solveUnified);
|
|
588
|
+
const state = newState();
|
|
589
|
+
const history = [];
|
|
590
|
+
const maxTurns = Math.min(options.maxTurns, card.patience.max_user_turns);
|
|
591
|
+
let message = card.opening_message;
|
|
592
|
+
for (let turn = 0; turn < maxTurns && message !== null; turn += 1) {
|
|
593
|
+
const userMessage = message;
|
|
594
|
+
try {
|
|
595
|
+
const { reply } = await runTurn(state, userMessage, port, [...history], solvePort, now);
|
|
596
|
+
syncProductUsage();
|
|
597
|
+
history.push({ role: 'user', text: userMessage }, { role: 'assistant', text: reply });
|
|
598
|
+
outcome.user_turns += 1;
|
|
599
|
+
const planMarkdown = deliveredPlanMarkdown(state);
|
|
600
|
+
if (planMarkdown !== null && !outcome.plan_delivered) {
|
|
601
|
+
outcome.plan_delivered = true;
|
|
602
|
+
outcome.plan_markdown_sha256 = createHash('sha256').update(planMarkdown).digest('hex');
|
|
603
|
+
const verdict = solverVerdict(state);
|
|
604
|
+
outcome.solver_verdict = verdict.verdict;
|
|
605
|
+
outcome.unsat_core = verdict.unsat;
|
|
606
|
+
outcome.solver_error_code = verdict.errorCode;
|
|
607
|
+
// The product's own registered fact gate, reading this session's isolated registry.
|
|
608
|
+
const report = await factGate.execute({ markdown: planMarkdown, tripYear: 2026 }, null);
|
|
609
|
+
outcome.poi_audit = poiAuditFromGateReport(report);
|
|
610
|
+
}
|
|
611
|
+
const decision = await personaDecide(personaEndpoint, card, history, outcome.plan_delivered, personaUsage);
|
|
612
|
+
if (decision.nps !== null)
|
|
613
|
+
outcome.nps = decision.nps;
|
|
614
|
+
if (decision.decision === 'finalize') {
|
|
615
|
+
outcome.finalized = true;
|
|
616
|
+
message = null;
|
|
617
|
+
}
|
|
618
|
+
else if (decision.decision === 'abandon') {
|
|
619
|
+
outcome.abandoned = true;
|
|
620
|
+
message = null;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
message = decision.message;
|
|
624
|
+
}
|
|
625
|
+
// Budget is enforced between turns, not only between sessions: two concurrent
|
|
626
|
+
// sessions could otherwise both run to completion past the limit.
|
|
627
|
+
options.checkBudget(options.sessionCost());
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
syncProductUsage();
|
|
631
|
+
// A broken persona contract or a provider failure ends this session without
|
|
632
|
+
// discarding what already happened: the partial funnel is the measurement.
|
|
633
|
+
outcome.error = {
|
|
634
|
+
code: error instanceof PersonaSimError ? error.code : 'internal_error',
|
|
635
|
+
detail: error.message.slice(0, 300),
|
|
636
|
+
};
|
|
637
|
+
message = null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
syncProductUsage();
|
|
641
|
+
return outcome;
|
|
642
|
+
}
|
|
643
|
+
export function summarizeFunnel(outcomes) {
|
|
644
|
+
const round = (value) => Number(value.toFixed(6));
|
|
645
|
+
const delivered = outcomes.filter(outcome => outcome.plan_delivered);
|
|
646
|
+
const finalized = delivered.filter(outcome => outcome.finalized);
|
|
647
|
+
const scores = delivered.map(outcome => outcome.nps).filter((score) => score !== null);
|
|
648
|
+
const promoters = scores.filter(score => score >= 9).length;
|
|
649
|
+
const detractors = scores.filter(score => score <= 6).length;
|
|
650
|
+
const claimsExtracted = delivered.reduce((sum, outcome) => sum + (outcome.poi_audit?.claims_extracted ?? 0), 0);
|
|
651
|
+
const locked = delivered.reduce((sum, outcome) => sum + (outcome.poi_audit?.locked_claims ?? 0), 0);
|
|
652
|
+
const invalid = delivered.reduce((sum, outcome) => sum + (outcome.poi_audit?.invalid_claims ?? 0), 0);
|
|
653
|
+
return {
|
|
654
|
+
schema: 'gotry_persona_sim_funnel.v1',
|
|
655
|
+
personas_run: outcomes.length,
|
|
656
|
+
errored: outcomes.filter(outcome => outcome.error !== null).length,
|
|
657
|
+
delivered: delivered.length,
|
|
658
|
+
finalized: finalized.length,
|
|
659
|
+
finalization_rate: delivered.length === 0 ? null : round(finalized.length / delivered.length),
|
|
660
|
+
nps_responses: scores.length,
|
|
661
|
+
promoters,
|
|
662
|
+
passives: scores.length - promoters - detractors,
|
|
663
|
+
detractors,
|
|
664
|
+
nps_score: scores.length === 0 ? null : round(100 * (promoters - detractors) / scores.length),
|
|
665
|
+
claims_extracted: claimsExtracted,
|
|
666
|
+
locked_claims: locked,
|
|
667
|
+
invalid_claims: invalid,
|
|
668
|
+
poi_rate: locked === 0 ? null : round(invalid / locked),
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function envValue(name) {
|
|
672
|
+
const value = process.env[name];
|
|
673
|
+
return value !== undefined && value.trim() !== '' ? value : undefined;
|
|
674
|
+
}
|
|
675
|
+
function steppingClock(startIso) {
|
|
676
|
+
let ms = Date.parse(startIso);
|
|
677
|
+
return { now: () => { ms += 1_000; return new Date(ms); } };
|
|
678
|
+
}
|
|
679
|
+
function assertModelPriced(model, table) {
|
|
680
|
+
try {
|
|
681
|
+
priceRunCost(model, emptyUsage(), table);
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
fail('missing_price_entry', `${model}: ${error.message}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function usageCost(model, usage, table) {
|
|
688
|
+
if (usage.calls === 0)
|
|
689
|
+
return 0;
|
|
690
|
+
if (usage.responsesMissingUsage > 0) {
|
|
691
|
+
fail('cost_unprovable', `${model} omitted usage on ${usage.responsesMissingUsage} response(s) — refusing to price`);
|
|
692
|
+
}
|
|
693
|
+
return priceRunCost(model, usage, table);
|
|
694
|
+
}
|
|
695
|
+
/** Scrub the environment the sessions run in: no live POI backend, no realtime pricing,
|
|
696
|
+
* no inherited provider endpoint. Restored by the returned function.
|
|
697
|
+
* GOTRY_HBCLI_LIVE (issue #617) is the explicit offline switch of the POI probe's `hbcli` channel:
|
|
698
|
+
* it defaults to `0` here, but a value the operator set explicitly is never overridden
|
|
699
|
+
* (PATH/HOME stay scrubbed either way, so even an explicit `1` finds no real `hbcli` unless one is planted under the sandbox HOME). */
|
|
700
|
+
export function scrubSessionEnv(patch, sandbox) {
|
|
701
|
+
const binDir = join(sandbox, 'empty-bin');
|
|
702
|
+
mkdirSync(binDir, { recursive: true });
|
|
703
|
+
const full = {
|
|
704
|
+
PATH: binDir,
|
|
705
|
+
HOME: sandbox,
|
|
706
|
+
GOTRY_REALTIME_PRICING: undefined,
|
|
707
|
+
GOTRY_SESSION_LIVE: '0',
|
|
708
|
+
GOTRY_HBCLI_LIVE: process.env['GOTRY_HBCLI_LIVE'] ?? '0',
|
|
709
|
+
...patch,
|
|
710
|
+
};
|
|
711
|
+
const saved = new Map();
|
|
712
|
+
for (const [key, value] of Object.entries(full)) {
|
|
713
|
+
saved.set(key, process.env[key]);
|
|
714
|
+
if (value === undefined)
|
|
715
|
+
delete process.env[key];
|
|
716
|
+
else
|
|
717
|
+
process.env[key] = value;
|
|
718
|
+
}
|
|
719
|
+
return () => {
|
|
720
|
+
for (const [key, value] of saved) {
|
|
721
|
+
if (value === undefined)
|
|
722
|
+
delete process.env[key];
|
|
723
|
+
else
|
|
724
|
+
process.env[key] = value;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
export async function runPersonaBatch(options) {
|
|
729
|
+
const deck = loadPersonaDeck(options.deckPath);
|
|
730
|
+
const selected = options.personaFilter && options.personaFilter.length > 0
|
|
731
|
+
? deck.filter(card => options.personaFilter.includes(card.persona_id))
|
|
732
|
+
: deck;
|
|
733
|
+
if (selected.length === 0)
|
|
734
|
+
fail('bad_args', 'persona filter matched no card');
|
|
735
|
+
const productKey = envValue('LLM_API_KEY') ?? envValue('DEEPSEEK_API_KEY');
|
|
736
|
+
const budgetUsd = options.budgetUsd ?? Number(envValue('GOTRY_PERSONA_BUDGET_USD') ?? DEFAULT_BUDGET_USD);
|
|
737
|
+
if (!Number.isFinite(budgetUsd) || budgetUsd <= 0)
|
|
738
|
+
fail('bad_args', 'GOTRY_PERSONA_BUDGET_USD must be a positive number');
|
|
739
|
+
if (!options.dryRun && !productKey) {
|
|
740
|
+
// Same stop rule as nightly-evidence.ts: wait, back off, spend nothing, write nothing.
|
|
741
|
+
return {
|
|
742
|
+
schema: 'gotry_persona_sim_result.v1',
|
|
743
|
+
state: 'waiting_external_evidence',
|
|
744
|
+
reason: 'no real LLM credential (LLM_API_KEY/DEEPSEEK_API_KEY) in the environment; waiting, no spend, no writes',
|
|
745
|
+
evidence_kind: 'synthetic_fixture',
|
|
746
|
+
product_model: null,
|
|
747
|
+
persona_model: null,
|
|
748
|
+
real_llm: false,
|
|
749
|
+
budget_usd: budgetUsd,
|
|
750
|
+
cost_usd: 0,
|
|
751
|
+
real_spend_usd: 0,
|
|
752
|
+
cost_over_budget: false,
|
|
753
|
+
personas: [],
|
|
754
|
+
funnel: summarizeFunnel([]),
|
|
755
|
+
evidence_root: null,
|
|
756
|
+
scorer_summary: null,
|
|
757
|
+
verification: null,
|
|
758
|
+
fixture_calls: null,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
const priceTable = loadPriceTable(join(TS_ROOT, 'data', 'llm-price-table.json'));
|
|
762
|
+
const productModel = envValue('LLM_MODEL') ?? envValue('DEEPSEEK_MODEL') ?? 'MiniMax-M2';
|
|
763
|
+
const personaModel = envValue('GOTRY_PERSONA_MODEL') ?? productModel;
|
|
764
|
+
// Fail-closed before any spend: an unpriced model makes cost unprovable.
|
|
765
|
+
assertModelPriced(productModel, priceTable);
|
|
766
|
+
assertModelPriced(personaModel, priceTable);
|
|
767
|
+
const consent = options.consent ?? (options.dryRun ? FIXTURE_CONSENT : envValue('GOTRY_PERSONA_CONSENT'));
|
|
768
|
+
if (!consent)
|
|
769
|
+
fail('missing_consent', 'a real batch needs --consent (or GOTRY_PERSONA_CONSENT)');
|
|
770
|
+
const hmacKey = options.hmacKey ?? envValue('GOTRY_M3_COHORT_HMAC_KEY') ?? (options.dryRun ? FIXTURE_HMAC_KEY : undefined);
|
|
771
|
+
if (!hmacKey)
|
|
772
|
+
fail('missing_hmac_key', 'a real batch needs GOTRY_M3_COHORT_HMAC_KEY');
|
|
773
|
+
// A real batch costs money, so its evidence must not land in a temp root this function
|
|
774
|
+
// then deletes: both destinations have to be chosen by the caller.
|
|
775
|
+
if (!options.dryRun && (options.stateRoot === undefined || options.evidenceRoot === undefined)) {
|
|
776
|
+
fail('bad_args', 'a real batch needs both --state-root and --evidence-root so the evidence survives the run');
|
|
777
|
+
}
|
|
778
|
+
const ownsRoot = options.stateRoot === undefined;
|
|
779
|
+
const batchRoot = options.stateRoot ?? mkdtempSync(join(tmpdir(), 'gotry-persona-sim-'));
|
|
780
|
+
// The capture core enforces the state-root policy, but only once it is asked to write —
|
|
781
|
+
// by then the harness would already have mkdir'd its sandbox and capture dir inside a
|
|
782
|
+
// forbidden root. Apply the same policy here, before any directory is created.
|
|
783
|
+
try {
|
|
784
|
+
assertSafeStateRoot(batchRoot);
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
if (error instanceof M3CohortError && error.code === 'unsafe_state_root')
|
|
788
|
+
fail('unsafe_state_root', batchRoot);
|
|
789
|
+
fail('bad_args', `unusable --state-root: ${error.message}`);
|
|
790
|
+
}
|
|
791
|
+
const captureRoot = join(batchRoot, 'capture');
|
|
792
|
+
const evidenceRoot = options.evidenceRoot ?? join(batchRoot, 'evidence');
|
|
793
|
+
const clock = options.clock ?? (options.dryRun ? steppingClock(FIXTURE_CLOCK_START) : { now: () => new Date() });
|
|
794
|
+
const maxTurns = Math.min(options.maxTurns ?? DEFAULT_MAX_TURNS, DEFAULT_MAX_TURNS);
|
|
795
|
+
// The fixture provider keeps one active persona at a time, so any fixture-backed batch
|
|
796
|
+
// is serialized regardless of the requested concurrency.
|
|
797
|
+
const concurrency = options.dryRun || options.fixture
|
|
798
|
+
? 1
|
|
799
|
+
: Math.max(1, Math.min(options.concurrency ?? 1, MAX_CONCURRENCY));
|
|
800
|
+
const sessionDeadlineMs = options.sessionDeadlineMs ?? 180_000;
|
|
801
|
+
let fixture = options.fixture ?? null;
|
|
802
|
+
let ownsFixture = false;
|
|
803
|
+
let restoreEnv = null;
|
|
804
|
+
const outcomes = [];
|
|
805
|
+
let costUsd = 0;
|
|
806
|
+
let overBudget = false;
|
|
807
|
+
try {
|
|
808
|
+
if (options.dryRun && !fixture) {
|
|
809
|
+
fixture = await startFixtureProvider({ omitUsage: options.omitFixtureUsage });
|
|
810
|
+
ownsFixture = true;
|
|
811
|
+
}
|
|
812
|
+
restoreEnv = scrubSessionEnv(fixture
|
|
813
|
+
? { LLM_BASE_URL: fixture.url, LLM_API_KEY: 'persona-sim-fixture-token', LLM_MODEL: productModel, DEEPSEEK_BASE_URL: undefined, DEEPSEEK_API_KEY: undefined, DEEPSEEK_MODEL: undefined }
|
|
814
|
+
: {}, batchRoot);
|
|
815
|
+
const personaEndpointBase = fixture?.url ?? envValue('GOTRY_PERSONA_BASE_URL') ?? envValue('LLM_BASE_URL') ?? 'https://api.minimax.io/v1';
|
|
816
|
+
const personaEndpoint = {
|
|
817
|
+
baseUrl: personaEndpointBase,
|
|
818
|
+
apiKey: fixture ? 'persona-sim-fixture-token' : (envValue('GOTRY_PERSONA_API_KEY') ?? productKey ?? ''),
|
|
819
|
+
model: personaModel,
|
|
820
|
+
timeoutMs: 60_000,
|
|
821
|
+
};
|
|
822
|
+
mkdirSync(captureRoot, { recursive: true });
|
|
823
|
+
const captureCommon = { stateRoot: captureRoot, consent, hmacKey };
|
|
824
|
+
const windowStart = new Date(Date.parse(clock.now().toISOString()) - 86_400_000).toISOString();
|
|
825
|
+
initM3Cohort({
|
|
826
|
+
...captureCommon,
|
|
827
|
+
// Bounded label: the deck can grow, the cohort handle must not.
|
|
828
|
+
cohort: `persona-sim:${createHash('sha256').update(selected.map(card => card.persona_id).join(',')).digest('hex').slice(0, 32)}`,
|
|
829
|
+
evidenceKind: 'synthetic_fixture',
|
|
830
|
+
windowStartAt: windowStart,
|
|
831
|
+
windowEndAt: new Date(Date.parse(windowStart) + 30 * 86_400_000).toISOString(),
|
|
832
|
+
timezone: 'UTC',
|
|
833
|
+
allowedAttribution: ['gotry_primary'],
|
|
834
|
+
}, clock);
|
|
835
|
+
// Capture writes are serialized: the capture store holds an exclusive writer lock, and
|
|
836
|
+
// a concurrent session must never turn into a `lock_busy` data loss.
|
|
837
|
+
let captureChain = Promise.resolve();
|
|
838
|
+
const capture = (body) => {
|
|
839
|
+
captureChain = captureChain.then(() => { body(); });
|
|
840
|
+
return captureChain;
|
|
841
|
+
};
|
|
842
|
+
const queue = [...selected];
|
|
843
|
+
const runNext = async () => {
|
|
844
|
+
for (;;) {
|
|
845
|
+
if (overBudget)
|
|
846
|
+
return;
|
|
847
|
+
const card = queue.shift();
|
|
848
|
+
if (!card)
|
|
849
|
+
return;
|
|
850
|
+
const sessionRoot = join(batchRoot, 'sessions', card.persona_id);
|
|
851
|
+
mkdirSync(sessionRoot, { recursive: true });
|
|
852
|
+
if (fixture)
|
|
853
|
+
fixture.setPersona(card);
|
|
854
|
+
// The trackers are owned here so a timed-out session still reports its spend.
|
|
855
|
+
const productUsage = emptyUsage();
|
|
856
|
+
const personaUsage = emptyUsage();
|
|
857
|
+
const sessionCost = () => usageCost(productModel, productUsage, priceTable) + usageCost(personaModel, personaUsage, priceTable);
|
|
858
|
+
let outcome;
|
|
859
|
+
try {
|
|
860
|
+
outcome = await withDeadline(runPersonaSession({
|
|
861
|
+
card,
|
|
862
|
+
stateRoot: sessionRoot,
|
|
863
|
+
personaEndpoint,
|
|
864
|
+
packPath: join(REPO_ROOT, 'data', 'flights_2026.json'),
|
|
865
|
+
clock,
|
|
866
|
+
maxTurns,
|
|
867
|
+
productUsage,
|
|
868
|
+
personaUsage,
|
|
869
|
+
sessionCost,
|
|
870
|
+
checkBudget: (thisSession) => {
|
|
871
|
+
if (Number((costUsd + thisSession).toFixed(6)) > budgetUsd) {
|
|
872
|
+
overBudget = true;
|
|
873
|
+
fail('budget_exceeded', `running total exceeds GOTRY_PERSONA_BUDGET_USD ${budgetUsd}`);
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
}), sessionDeadlineMs, 'persona_timeout', `persona ${card.persona_id} exceeded ${sessionDeadlineMs}ms`);
|
|
877
|
+
}
|
|
878
|
+
catch (error) {
|
|
879
|
+
const code = error instanceof PersonaSimError ? error.code : 'internal_error';
|
|
880
|
+
outcome = {
|
|
881
|
+
persona_id: card.persona_id,
|
|
882
|
+
expected_outcome: card.expected_outcome,
|
|
883
|
+
user_turns: 0,
|
|
884
|
+
plan_delivered: false,
|
|
885
|
+
finalized: false,
|
|
886
|
+
abandoned: false,
|
|
887
|
+
nps: null,
|
|
888
|
+
solver_verdict: 'none',
|
|
889
|
+
unsat_core: [],
|
|
890
|
+
solver_error_code: null,
|
|
891
|
+
poi_audit: null,
|
|
892
|
+
plan_markdown_sha256: null,
|
|
893
|
+
product_usage: productUsage,
|
|
894
|
+
persona_usage: personaUsage,
|
|
895
|
+
error: { code, detail: error.message.slice(0, 300) },
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
outcomes.push(outcome);
|
|
899
|
+
// Only a delivered plan with a locked claim audit becomes a cohort record; an
|
|
900
|
+
// interview-only or failed session contributes nothing, exactly as a real funnel
|
|
901
|
+
// would record it.
|
|
902
|
+
if (outcome.error === null && outcome.plan_delivered && outcome.poi_audit) {
|
|
903
|
+
const provenance = {
|
|
904
|
+
schema_version: M3_SIMULATION_PROVENANCE_SCHEMA,
|
|
905
|
+
kind: 'llm_persona',
|
|
906
|
+
persona_id: card.persona_id,
|
|
907
|
+
product_model: productModel,
|
|
908
|
+
persona_model: personaModel,
|
|
909
|
+
prompt_digest: promptDigest(card),
|
|
910
|
+
};
|
|
911
|
+
const participant = `persona:${card.persona_id}`;
|
|
912
|
+
const plan = `persona:${card.persona_id}:plan`;
|
|
913
|
+
// test_or_staff=true is deliberate and is the record-level half of the
|
|
914
|
+
// never-counts guarantee: the scorer's own `test_or_staff` exclusion drops every
|
|
915
|
+
// simulated participant, so even a relabelled manifest yields sample=0 and
|
|
916
|
+
// business_pass=false. The funnel numbers a reader wants are therefore computed
|
|
917
|
+
// by summarizeFunnel() over the harness's outcomes, not read off the scorer.
|
|
918
|
+
try {
|
|
919
|
+
await capture(() => {
|
|
920
|
+
enrollParticipant({ ...captureCommon, participant, invited: true, participantConsent: true, testOrStaff: true, simulation: provenance }, clock);
|
|
921
|
+
recordPlanDelivered({ ...captureCommon, participant, plan, attribution: 'gotry_primary' }, clock);
|
|
922
|
+
if (outcome.finalized)
|
|
923
|
+
recordPlanFinalized({ ...captureCommon, participant, plan }, clock);
|
|
924
|
+
if (outcome.nps !== null)
|
|
925
|
+
recordNps({ ...captureCommon, participant, plan, score: outcome.nps }, clock);
|
|
926
|
+
recordPoiLock({
|
|
927
|
+
...captureCommon,
|
|
928
|
+
participant,
|
|
929
|
+
plan,
|
|
930
|
+
lockedClaims: outcome.poi_audit.locked_claims,
|
|
931
|
+
invalidClaims: outcome.poi_audit.invalid_claims,
|
|
932
|
+
}, clock);
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
// Same granularity as a failed session: one unrecordable persona is reported on
|
|
937
|
+
// that persona and the paid batch still exports everything else.
|
|
938
|
+
captureChain = Promise.resolve();
|
|
939
|
+
outcome.error = { code: 'capture_failed', detail: error.message.slice(0, 300) };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
costUsd = Number((costUsd
|
|
943
|
+
+ usageCost(productModel, outcome.product_usage, priceTable)
|
|
944
|
+
+ usageCost(personaModel, outcome.persona_usage, priceTable)).toFixed(6));
|
|
945
|
+
if (costUsd > budgetUsd) {
|
|
946
|
+
overBudget = true;
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, runNext));
|
|
952
|
+
await captureChain.catch(() => { });
|
|
953
|
+
// An over-budget batch still exports: the spend already happened and the captured
|
|
954
|
+
// records are facts about it. The batch reports cost_over_budget and the CLI exits 3.
|
|
955
|
+
const { payload } = exportM3Cohort({ ...captureCommon, evidenceRoot }, clock);
|
|
956
|
+
const manifest = parseManifest(JSON.parse(JSON.stringify(payload.manifest)));
|
|
957
|
+
const cohort = payload.cohort.map((record, index) => parseCohortRecord(JSON.parse(JSON.stringify(record)), index));
|
|
958
|
+
const summary = scoreProductMetrics(manifest, cohort, []);
|
|
959
|
+
const verification = verifyExportedEvidence(evidenceRoot, hmacKey);
|
|
960
|
+
if (summary.business_pass)
|
|
961
|
+
fail('internal_error', 'a synthetic export must never reach business_pass');
|
|
962
|
+
if (summary.sample.participants !== 0) {
|
|
963
|
+
fail('internal_error', 'simulated participants must be excluded from the scorer eligible set');
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
schema: 'gotry_persona_sim_result.v1',
|
|
967
|
+
state: options.dryRun ? 'dry_run_complete' : 'evidence_written',
|
|
968
|
+
reason: overBudget ? `cost_usd ${costUsd} exceeds GOTRY_PERSONA_BUDGET_USD ${budgetUsd} — batch stopped early` : undefined,
|
|
969
|
+
evidence_kind: 'synthetic_fixture',
|
|
970
|
+
product_model: productModel,
|
|
971
|
+
persona_model: personaModel,
|
|
972
|
+
real_llm: !options.dryRun,
|
|
973
|
+
budget_usd: budgetUsd,
|
|
974
|
+
cost_usd: costUsd,
|
|
975
|
+
real_spend_usd: options.dryRun ? 0 : costUsd,
|
|
976
|
+
cost_over_budget: overBudget,
|
|
977
|
+
personas: outcomes.sort((a, b) => a.persona_id.localeCompare(b.persona_id)),
|
|
978
|
+
funnel: summarizeFunnel(outcomes),
|
|
979
|
+
evidence_root: evidenceRoot,
|
|
980
|
+
scorer_summary: summary,
|
|
981
|
+
verification: {
|
|
982
|
+
evidence_kind: verification.evidence_kind,
|
|
983
|
+
record_count: verification.record_count,
|
|
984
|
+
simulated_record_count: verification.simulated_record_count,
|
|
985
|
+
},
|
|
986
|
+
fixture_calls: fixture ? { ...fixture.calls } : null,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
finally {
|
|
990
|
+
if (restoreEnv)
|
|
991
|
+
restoreEnv();
|
|
992
|
+
if (ownsFixture && fixture)
|
|
993
|
+
await fixture.close();
|
|
994
|
+
if (ownsRoot && options.keep !== true)
|
|
995
|
+
rmSync(batchRoot, { recursive: true, force: true });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// ---------------------------------------------------------------------------
|
|
999
|
+
// CLI
|
|
1000
|
+
// ---------------------------------------------------------------------------
|
|
1001
|
+
function arg(name) {
|
|
1002
|
+
const index = process.argv.indexOf(name);
|
|
1003
|
+
if (index < 0)
|
|
1004
|
+
return undefined;
|
|
1005
|
+
const value = process.argv[index + 1];
|
|
1006
|
+
if (value === undefined || value.startsWith('--'))
|
|
1007
|
+
throw new PersonaSimError('bad_args', `${name} needs a value`);
|
|
1008
|
+
return value;
|
|
1009
|
+
}
|
|
1010
|
+
function renderMarkdown(result) {
|
|
1011
|
+
const lines = [
|
|
1012
|
+
`# persona-sim (${result.state}) — SYNTHETIC ONLY, never M3/M4 evidence`,
|
|
1013
|
+
'',
|
|
1014
|
+
`- models: product=${result.product_model ?? '-'} persona=${result.persona_model ?? '-'} real_llm=${result.real_llm}`,
|
|
1015
|
+
`- cost: computed $${result.cost_usd} / budget $${result.budget_usd} (real spend $${result.real_spend_usd}${result.cost_over_budget ? ', OVER BUDGET' : ''})`,
|
|
1016
|
+
`- evidence root: ${result.evidence_root ?? '(none written)'}`,
|
|
1017
|
+
];
|
|
1018
|
+
if (result.reason)
|
|
1019
|
+
lines.push(`- reason: ${result.reason}`);
|
|
1020
|
+
if (result.personas.length > 0) {
|
|
1021
|
+
lines.push('', '| persona | turns | delivered | finalized | nps | solver | extracted | audited | invalid | gate | error |', '|---|---|---|---|---|---|---|---|---|---|---|');
|
|
1022
|
+
for (const persona of result.personas) {
|
|
1023
|
+
lines.push(`| ${persona.persona_id} | ${persona.user_turns} | ${persona.plan_delivered} | ${persona.finalized} | ${persona.nps ?? '-'} `
|
|
1024
|
+
+ `| ${persona.solver_verdict}${persona.solver_error_code ? `(${persona.solver_error_code})` : persona.unsat_core.length ? `(${persona.unsat_core.join(',')})` : ''} `
|
|
1025
|
+
+ `| ${persona.poi_audit?.claims_extracted ?? '-'} | ${persona.poi_audit?.locked_claims ?? '-'} | ${persona.poi_audit?.invalid_claims ?? '-'} `
|
|
1026
|
+
+ `| ${persona.poi_audit?.gate_verdict ?? '-'} | ${persona.error ? persona.error.code : '-'} |`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const funnel = result.funnel;
|
|
1030
|
+
lines.push('', `- harness funnel (simulation, computed here): delivered=${funnel.delivered}/${funnel.personas_run} `
|
|
1031
|
+
+ `finalized=${funnel.finalized} finalization=${funnel.finalization_rate ?? 'unavailable'} `
|
|
1032
|
+
+ `nps=${funnel.nps_score ?? 'unavailable'}(n=${funnel.nps_responses}) `
|
|
1033
|
+
+ `claims extracted=${funnel.claims_extracted} audited=${funnel.locked_claims} invalid=${funnel.invalid_claims} `
|
|
1034
|
+
+ `poi=${funnel.poi_rate ?? 'unavailable'} errored=${funnel.errored}`);
|
|
1035
|
+
const summary = result.scorer_summary;
|
|
1036
|
+
if (summary) {
|
|
1037
|
+
lines.push(`- scorer (excludes every simulated participant via test_or_staff): participants=${summary.sample.participants} `
|
|
1038
|
+
+ `finalization=${summary.finalization.rate ?? 'unavailable'} nps=${summary.nps.score ?? 'unavailable'} `
|
|
1039
|
+
+ `poi=${summary.poi_hallucination.rate ?? 'unavailable'} test_or_staff_excluded=${summary.exclusions['test_or_staff'] ?? 0}`);
|
|
1040
|
+
lines.push(`- business_pass: ${summary.business_pass} — ${summary.business_pass_reason}`);
|
|
1041
|
+
}
|
|
1042
|
+
return lines.join('\n');
|
|
1043
|
+
}
|
|
1044
|
+
async function main() {
|
|
1045
|
+
// `--format` is read inside the try: a malformed flag must surface as bad_args, not as an
|
|
1046
|
+
// unhandled rejection before the error path exists.
|
|
1047
|
+
let asJson = process.argv.includes('--format');
|
|
1048
|
+
try {
|
|
1049
|
+
asJson = (arg('--format') ?? 'markdown') === 'json';
|
|
1050
|
+
const result = await runPersonaBatch({
|
|
1051
|
+
deckPath: arg('--deck'),
|
|
1052
|
+
dryRun: process.argv.includes('--dry-run'),
|
|
1053
|
+
stateRoot: arg('--state-root'),
|
|
1054
|
+
evidenceRoot: arg('--evidence-root'),
|
|
1055
|
+
budgetUsd: arg('--budget-usd') === undefined ? undefined : Number(arg('--budget-usd')),
|
|
1056
|
+
maxTurns: arg('--max-turns') === undefined ? undefined : Number(arg('--max-turns')),
|
|
1057
|
+
concurrency: arg('--concurrency') === undefined ? undefined : Number(arg('--concurrency')),
|
|
1058
|
+
personaFilter: arg('--persona') === undefined ? undefined : arg('--persona').split(','),
|
|
1059
|
+
consent: arg('--consent'),
|
|
1060
|
+
keep: process.argv.includes('--keep'),
|
|
1061
|
+
});
|
|
1062
|
+
if (asJson)
|
|
1063
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1064
|
+
else
|
|
1065
|
+
console.log(renderMarkdown(result));
|
|
1066
|
+
if (result.cost_over_budget)
|
|
1067
|
+
process.exitCode = 3;
|
|
1068
|
+
}
|
|
1069
|
+
catch (error) {
|
|
1070
|
+
const code = error instanceof PersonaSimError ? error.code : 'internal_error';
|
|
1071
|
+
const payload = { schema: 'gotry_persona_sim_result.v1', state: 'error', code, detail: error.message.slice(0, 400) };
|
|
1072
|
+
if (asJson)
|
|
1073
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1074
|
+
else
|
|
1075
|
+
console.log(`persona-sim error: ${code} — ${payload.detail}`);
|
|
1076
|
+
process.exitCode = 1;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (process.argv[1]?.endsWith('persona-sim.ts')) {
|
|
1080
|
+
await main();
|
|
1081
|
+
}
|