@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.
Files changed (533) hide show
  1. package/README.md +172 -219
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1788 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -56
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +161 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +553 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +947 -110
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +676 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/provider-detail-sanitize.js +64 -0
  41. package/dist/capabilities/resilience.js +191 -0
  42. package/dist/capabilities/route-provider-conformance.js +341 -0
  43. package/dist/capabilities/session/action-cache.js +50 -44
  44. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  45. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  46. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  47. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  48. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  49. package/dist/capabilities/session/benchmark.js +189 -0
  50. package/dist/capabilities/session/bridge-ledger.js +224 -0
  51. package/dist/capabilities/session/extension-bridge.js +636 -0
  52. package/dist/capabilities/session/extension-channel.js +109 -0
  53. package/dist/capabilities/session/extension-distribution.js +230 -0
  54. package/dist/capabilities/session/extract.js +31 -21
  55. package/dist/capabilities/session/golden-score.js +114 -0
  56. package/dist/capabilities/session/health-watch.js +215 -0
  57. package/dist/capabilities/session/read-guard.js +75 -24
  58. package/dist/capabilities/session/static-flight-golden.js +150 -0
  59. package/dist/capabilities/session/transport.js +97 -83
  60. package/dist/capabilities/session/wizard.js +117 -0
  61. package/dist/capabilities/session-consent.js +178 -0
  62. package/dist/capabilities/session-login.js +170 -0
  63. package/dist/capabilities/session-search.js +635 -44
  64. package/dist/capabilities/spawn-bounded.js +203 -0
  65. package/dist/capabilities/sponsor-plugin.js +341 -0
  66. package/dist/capabilities/visa-policy.js +140 -0
  67. package/dist/capabilities/weather.js +243 -138
  68. package/dist/data/session-golden-20.json +25 -0
  69. package/dist/data/sf-golden-manifest.json +103 -0
  70. package/dist/data/sf-static-routes.json +91 -0
  71. package/dist/scripts/action-cache-tests.js +24 -59
  72. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  73. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  74. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  75. package/dist/scripts/agent-reach-error-tests.js +354 -0
  76. package/dist/scripts/agent-reach-tests.js +26 -18
  77. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  78. package/dist/scripts/anything-path-measure-tests.js +133 -0
  79. package/dist/scripts/anything-path-measure.js +150 -0
  80. package/dist/scripts/anything-tests.js +250 -60
  81. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  82. package/dist/scripts/artifacts-capability-tests.js +727 -0
  83. package/dist/scripts/async-collect.js +50 -11
  84. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  85. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  86. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  87. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  88. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  89. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  90. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  91. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  92. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  93. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  94. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  95. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
  96. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  97. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  98. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  99. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  100. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  101. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  102. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  103. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  104. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  105. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  106. package/dist/scripts/booking-executor-tests.js +80 -0
  107. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  108. package/dist/scripts/booking-saga-tests.js +125 -0
  109. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  110. package/dist/scripts/bootstrap-tests.js +1549 -0
  111. package/dist/scripts/bridge-events-tests.js +239 -0
  112. package/dist/scripts/bridge-ledger-tests.js +428 -0
  113. package/dist/scripts/build-changelog.js +238 -0
  114. package/dist/scripts/build-metrics-report.js +331 -0
  115. package/dist/scripts/changelog-tests.js +118 -0
  116. package/dist/scripts/channel-probe-tests.js +103 -0
  117. package/dist/scripts/channel-probe.js +169 -0
  118. package/dist/scripts/channel-registry-tests.js +208 -0
  119. package/dist/scripts/city-scenario-tier-tests.js +234 -0
  120. package/dist/scripts/companion-tests.js +25 -70
  121. package/dist/scripts/dida-cookie-probe.js +87 -0
  122. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  123. package/dist/scripts/dida-sw-probe.js +114 -0
  124. package/dist/scripts/diff-test.js +17 -12
  125. package/dist/scripts/doctor-tests.js +600 -0
  126. package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
  127. package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
  128. package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
  129. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  130. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  131. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  132. package/dist/scripts/dsh-llm-tests.js +437 -0
  133. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  134. package/dist/scripts/dsh-runtime-closure.js +162 -0
  135. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  136. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  137. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  138. package/dist/scripts/effect-tests.js +539 -0
  139. package/dist/scripts/engine-run.js +1 -3
  140. package/dist/scripts/engine-tests.js +16 -10
  141. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  142. package/dist/scripts/evaluation-contract-tests.js +268 -0
  143. package/dist/scripts/extension-distribution-cli.js +39 -0
  144. package/dist/scripts/extension-distribution-tests.js +443 -0
  145. package/dist/scripts/extension-tests.js +988 -0
  146. package/dist/scripts/external-event-tests.js +350 -0
  147. package/dist/scripts/fact-gate-tests.js +869 -0
  148. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  149. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  150. package/dist/scripts/flight-malformed-tests.js +159 -0
  151. package/dist/scripts/flyai-setup-tests.js +228 -0
  152. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  153. package/dist/scripts/flyai-tests.js +692 -0
  154. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  155. package/dist/scripts/fx-contract-tests.js +201 -0
  156. package/dist/scripts/g5-guard-tests.js +150 -0
  157. package/dist/scripts/g5-guard.js +193 -0
  158. package/dist/scripts/gen-lunar-anchors.js +106 -0
  159. package/dist/scripts/geo-atlas-tests.js +283 -0
  160. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  161. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  162. package/dist/scripts/gotry-try-demo.js +49 -0
  163. package/dist/scripts/ground-transfer-tests.js +956 -0
  164. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  165. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  166. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  167. package/dist/scripts/hbcli-tests.js +121 -69
  168. package/dist/scripts/health-watch-cli.js +73 -0
  169. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  170. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  171. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  172. package/dist/scripts/i18n-tests.js +41 -0
  173. package/dist/scripts/incident-tests.js +281 -87
  174. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  175. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  176. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  177. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  178. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  179. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  180. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  181. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  182. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  183. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  184. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  185. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  186. package/dist/scripts/itinerary-deck-tests.js +435 -0
  187. package/dist/scripts/itinerary-html-tests.js +426 -0
  188. package/dist/scripts/journey-tests.js +22 -27
  189. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  190. package/dist/scripts/kernel-manifest-gate.js +126 -0
  191. package/dist/scripts/kernel-manifest-tests.js +212 -0
  192. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  193. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  194. package/dist/scripts/kernel-manifest.js +369 -0
  195. package/dist/scripts/lavish-local-tests.js +1226 -0
  196. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  197. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  198. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  199. package/dist/scripts/ledger-tests.js +382 -246
  200. package/dist/scripts/ledger-workflow-crash.js +10 -20
  201. package/dist/scripts/m3-cohort-tests.js +714 -0
  202. package/dist/scripts/m3-cohort.js +310 -0
  203. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  204. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  205. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  206. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  207. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  208. package/dist/scripts/memory-capture-tests.js +242 -61
  209. package/dist/scripts/memory-decay-tests.js +18 -52
  210. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  211. package/dist/scripts/memory-lifecycle.js +196 -0
  212. package/dist/scripts/memory-metrics.js +24 -12
  213. package/dist/scripts/memory-value-report.js +631 -0
  214. package/dist/scripts/memory-value-tests.js +328 -0
  215. package/dist/scripts/metrics-report-tests.js +151 -0
  216. package/dist/scripts/model-override-e2e.js +137 -0
  217. package/dist/scripts/nightly-evidence-tests.js +203 -0
  218. package/dist/scripts/nightly-evidence.js +303 -0
  219. package/dist/scripts/nudge-digest.js +39 -28
  220. package/dist/scripts/observability-tests.js +381 -0
  221. package/dist/scripts/onboarding-tests.js +161 -0
  222. package/dist/scripts/opensky-check.js +18 -13
  223. package/dist/scripts/opensky-tests.js +28 -28
  224. package/dist/scripts/outcome-projection-tests.js +427 -0
  225. package/dist/scripts/persona-sim-tests.js +603 -0
  226. package/dist/scripts/persona-sim.js +1081 -0
  227. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  228. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  229. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  230. package/dist/scripts/price-drift-tests.js +340 -0
  231. package/dist/scripts/price-drift-watch.js +447 -0
  232. package/dist/scripts/probe-poi-tests.js +53 -45
  233. package/dist/scripts/product-metrics.js +483 -0
  234. package/dist/scripts/publish-preverify.js +110 -0
  235. package/dist/scripts/realtime-pricing-tests.js +76 -0
  236. package/dist/scripts/recall-tests.js +513 -0
  237. package/dist/scripts/replay-async.js +22 -18
  238. package/dist/scripts/replay-real.js +18 -18
  239. package/dist/scripts/replay.js +54 -82
  240. package/dist/scripts/route-provider-conformance-tests.js +391 -0
  241. package/dist/scripts/session-attach-diagnose.js +20 -20
  242. package/dist/scripts/session-attach-poc.js +34 -37
  243. package/dist/scripts/session-benchmark.js +180 -0
  244. package/dist/scripts/session-dida-live-e2e.js +199 -0
  245. package/dist/scripts/session-extract-tests.js +14 -35
  246. package/dist/scripts/session-link-tests.js +478 -0
  247. package/dist/scripts/session-login.js +27 -46
  248. package/dist/scripts/session-tests.js +796 -96
  249. package/dist/scripts/session-zone-crash.js +49 -0
  250. package/dist/scripts/session-zone-ledger-tests.js +432 -0
  251. package/dist/scripts/session-zone-observation-tests.js +492 -0
  252. package/dist/scripts/session-zone-observe.js +135 -0
  253. package/dist/scripts/session-zone-wiring-tests.js +566 -0
  254. package/dist/scripts/session-zones-tests.js +374 -0
  255. package/dist/scripts/sf-live-benchmark.js +367 -0
  256. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  257. package/dist/scripts/sf-live-cli-tests.js +92 -0
  258. package/dist/scripts/sf-live-options.js +46 -0
  259. package/dist/scripts/sf-manifest-tests.js +30 -0
  260. package/dist/scripts/sf-manifest.js +33 -0
  261. package/dist/scripts/sf-soft-score-tests.js +67 -0
  262. package/dist/scripts/sf-summary-tests.js +324 -0
  263. package/dist/scripts/sf-summary.js +525 -0
  264. package/dist/scripts/share-tests.js +432 -0
  265. package/dist/scripts/skeleton-check.js +16 -14
  266. package/dist/scripts/skeleton-integration-test.js +28 -11
  267. package/dist/scripts/skills-contract-tests.js +51 -46
  268. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  269. package/dist/scripts/smoke.js +690 -246
  270. package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
  271. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  272. package/dist/scripts/state-cli-tests.js +226 -101
  273. package/dist/scripts/state-cli.js +565 -198
  274. package/dist/scripts/state-repair-cli.js +66 -0
  275. package/dist/scripts/state-repair-tests.js +140 -0
  276. package/dist/scripts/static-golden-tests.js +165 -0
  277. package/dist/scripts/time-eval-tests.js +212 -192
  278. package/dist/scripts/travel-timeline-tests.js +29 -93
  279. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  280. package/dist/scripts/turn-handoff-collect.js +161 -0
  281. package/dist/scripts/turn-policy-tests.js +55 -0
  282. package/dist/scripts/typed-contract-canary.js +176 -0
  283. package/dist/scripts/tz-resolver-sanity.js +50 -0
  284. package/dist/scripts/tz-resolver-tests.js +584 -0
  285. package/dist/scripts/unified-tests.js +60 -20
  286. package/dist/scripts/visa-policy-tests.js +62 -0
  287. package/dist/scripts/weather-tests.js +276 -41
  288. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  289. package/dist/scripts/write-gate-tests.js +522 -0
  290. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  291. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  292. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  293. package/dist/scripts/z3-race-tests.js +46 -0
  294. package/dist/src/artifact-gate.js +829 -0
  295. package/dist/src/backend/kernel.js +91 -0
  296. package/dist/src/backend/modules/booking-copilot.js +92 -0
  297. package/dist/src/backend/modules/booking-executor.js +124 -0
  298. package/dist/src/backend/modules/session-search.js +551 -0
  299. package/dist/src/benchmark-agent-conformance.js +578 -0
  300. package/dist/src/benchmark-environment-bridge.js +515 -0
  301. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  302. package/dist/src/benchmark-tool-isolation.js +151 -0
  303. package/dist/src/bookable-facts.js +533 -0
  304. package/dist/src/booking-saga.js +97 -0
  305. package/dist/src/booking-surface/availability-policy.js +697 -0
  306. package/dist/src/booking-surface/booking-intent.js +43 -0
  307. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  308. package/dist/src/booking-surface/canonical-schema.js +116 -0
  309. package/dist/src/booking-surface/contracts.js +75 -0
  310. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  311. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  312. package/dist/src/booking-surface/dsh-worker.js +98 -0
  313. package/dist/src/booking-surface/error-codes.js +231 -0
  314. package/dist/src/booking-surface/index.js +9 -0
  315. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  316. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  317. package/dist/src/booking-surface/profile.js +24 -0
  318. package/dist/src/booking-surface/recovery-chain.js +367 -0
  319. package/dist/src/booking-surface/runtime.js +1968 -0
  320. package/dist/src/booking-surface/server.js +674 -0
  321. package/dist/src/booking-surface/startup.js +168 -0
  322. package/dist/src/booking-surface/validation.js +261 -0
  323. package/dist/src/bridge.js +20 -13
  324. package/dist/src/city-scenario-tier.js +378 -0
  325. package/dist/src/companions.js +48 -74
  326. package/dist/src/contracts.js +21 -43
  327. package/dist/src/dsh-llm.js +171 -115
  328. package/dist/src/engine.js +276 -193
  329. package/dist/src/evaluation-cadence.js +97 -0
  330. package/dist/src/evaluation-contracts.js +283 -0
  331. package/dist/src/flight-pack-adapter.js +54 -0
  332. package/dist/src/flight-pack-contract.js +2 -0
  333. package/dist/src/flyai-setup-tool.js +70 -0
  334. package/dist/src/gotry-backend.js +125 -0
  335. package/dist/src/hotel-date-gate.js +118 -0
  336. package/dist/src/i18n.js +88 -0
  337. package/dist/src/index.js +2385 -881
  338. package/dist/src/itinerary-deck.js +139 -0
  339. package/dist/src/itinerary-doc-shared.js +661 -0
  340. package/dist/src/itinerary-html.js +131 -0
  341. package/dist/src/journey.js +155 -108
  342. package/dist/src/lavish-tools.js +400 -0
  343. package/dist/src/ledger-repair-apply.js +538 -0
  344. package/dist/src/ledger-repair-plan.js +463 -0
  345. package/dist/src/loop.js +331 -161
  346. package/dist/src/m3-cohort.js +1577 -0
  347. package/dist/src/memory-capture.js +180 -32
  348. package/dist/src/memory-decay.js +30 -14
  349. package/dist/src/memory-lifecycle.js +1310 -0
  350. package/dist/src/memory-utility.js +27 -40
  351. package/dist/src/mock-llm.js +36 -61
  352. package/dist/src/model.js +81 -36
  353. package/dist/src/outcome-projection.js +746 -0
  354. package/dist/src/realtime-pricing.js +103 -0
  355. package/dist/src/recall/card.js +74 -0
  356. package/dist/src/recall/evaluator.js +140 -0
  357. package/dist/src/recall/integration.js +17 -0
  358. package/dist/src/recall/tick.js +166 -0
  359. package/dist/src/session-link/plan-it-action.js +48 -0
  360. package/dist/src/session-link/session-link.js +248 -0
  361. package/dist/src/session-zone-ledger.js +312 -0
  362. package/dist/src/session-zone-observation.js +354 -0
  363. package/dist/src/session-zone-wiring.js +658 -0
  364. package/dist/src/session-zones.js +510 -0
  365. package/dist/src/share/adapters.js +51 -0
  366. package/dist/src/share/share-consent.js +76 -0
  367. package/dist/src/share/share-deck.js +58 -0
  368. package/dist/src/share/share-token.js +146 -0
  369. package/dist/src/slot-spec.js +84 -106
  370. package/dist/src/state-ledger.js +696 -524
  371. package/dist/src/state-repair.js +299 -0
  372. package/dist/src/subagent-job-id-guard.js +58 -0
  373. package/dist/src/time-anchor.js +231 -33
  374. package/dist/src/tool-packet.js +20 -8
  375. package/dist/src/travel-slots.js +58 -35
  376. package/dist/src/travel-timeline.js +44 -63
  377. package/dist/src/turn-deadline.js +287 -0
  378. package/dist/src/turn-policy.js +118 -0
  379. package/dist/src/tz-resolver.js +143 -0
  380. package/dist/src/unified.js +600 -360
  381. package/dist/src/wish-pool.js +42 -12
  382. package/dist/src/write-gate.js +495 -0
  383. package/dist/src/z3-shared.js +284 -0
  384. package/extension/README.md +21 -0
  385. package/extension/README.zh-CN.md +21 -0
  386. package/extension/background.js +463 -0
  387. package/extension/content-bridge.js +270 -0
  388. package/extension/content-main.js +111 -0
  389. package/extension/manifest.json +60 -0
  390. package/package.json +407 -14
  391. package/schemas/booking.intent.schema.json +193 -0
  392. package/schemas/booking.surface.schema.json +2622 -0
  393. package/ts/capabilities/agent-reach-deep.ts +114 -0
  394. package/ts/capabilities/agent-reach.ts +198 -0
  395. package/ts/capabilities/anything.ts +282 -0
  396. package/ts/capabilities/artifacts.ts +428 -0
  397. package/ts/capabilities/fact-log.ts +54 -0
  398. package/ts/capabilities/flyai-config.ts +388 -0
  399. package/ts/capabilities/flyai.ts +1274 -0
  400. package/ts/capabilities/hbcli.ts +253 -71
  401. package/ts/capabilities/incident-log.ts +88 -59
  402. package/ts/capabilities/itinerary-artifact.ts +214 -0
  403. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  404. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  405. package/ts/capabilities/opensky.ts +121 -0
  406. package/ts/capabilities/session/action-cache.ts +148 -0
  407. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  408. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  409. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  410. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  411. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  412. package/ts/capabilities/session/benchmark.ts +276 -0
  413. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  414. package/ts/capabilities/session/extension-bridge.ts +794 -0
  415. package/ts/capabilities/session/extension-channel.ts +144 -0
  416. package/ts/capabilities/session/extension-distribution.ts +266 -0
  417. package/ts/capabilities/session/extract.ts +54 -0
  418. package/ts/capabilities/session/golden-score.ts +139 -0
  419. package/ts/capabilities/session/health-watch.ts +264 -0
  420. package/ts/capabilities/session/read-guard.ts +141 -0
  421. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  422. package/ts/capabilities/session/transport.ts +146 -0
  423. package/ts/capabilities/session/wizard.ts +140 -0
  424. package/ts/capabilities/session-consent.ts +211 -0
  425. package/ts/capabilities/session-login.ts +208 -0
  426. package/ts/capabilities/session-search.ts +893 -0
  427. package/ts/capabilities/weather.ts +330 -0
  428. package/ts/dsh-runtime/vendor/README.md +68 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  440. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  441. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  442. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  443. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  444. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  445. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  446. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  447. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  448. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  449. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  450. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  451. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  452. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  453. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  454. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  455. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  456. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  457. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  458. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  459. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  460. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  461. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  462. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  463. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  464. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  465. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  466. package/ts/package.json +293 -3
  467. package/ts/scripts/async-collect.ts +91 -0
  468. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  469. package/ts/scripts/skeleton-check.ts +1 -1
  470. package/ts/scripts/skeleton-integration-test.ts +23 -1
  471. package/ts/scripts/state-cli.ts +586 -0
  472. package/ts/scripts/turn-handoff-collect.ts +178 -0
  473. package/ts/src/artifact-gate.ts +991 -0
  474. package/ts/src/benchmark-agent-conformance.ts +619 -0
  475. package/ts/src/benchmark-environment-bridge.ts +538 -0
  476. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  477. package/ts/src/benchmark-tool-isolation.ts +166 -0
  478. package/ts/src/bookable-facts.ts +837 -0
  479. package/ts/src/booking-saga.ts +133 -0
  480. package/ts/src/booking-surface/availability-policy.ts +522 -0
  481. package/ts/src/booking-surface/booking-intent.ts +86 -0
  482. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  483. package/ts/src/booking-surface/canonical-schema.js +116 -0
  484. package/ts/src/booking-surface/contracts.ts +475 -0
  485. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  486. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  487. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  488. package/ts/src/booking-surface/error-codes.ts +241 -0
  489. package/ts/src/booking-surface/index.ts +9 -0
  490. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  491. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  492. package/ts/src/booking-surface/profile.ts +42 -0
  493. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  494. package/ts/src/booking-surface/runtime.ts +1793 -0
  495. package/ts/src/booking-surface/server.ts +733 -0
  496. package/ts/src/booking-surface/startup.ts +201 -0
  497. package/ts/src/booking-surface/validation.ts +217 -0
  498. package/ts/src/companions.ts +113 -0
  499. package/ts/src/contracts.ts +17 -3
  500. package/ts/src/dsh-llm.ts +152 -17
  501. package/ts/src/flyai-setup-tool.ts +77 -0
  502. package/ts/src/index.ts +2123 -200
  503. package/ts/src/itinerary-deck.ts +189 -0
  504. package/ts/src/itinerary-doc-shared.ts +741 -0
  505. package/ts/src/itinerary-html.ts +171 -0
  506. package/ts/src/loop.ts +234 -26
  507. package/ts/src/memory-capture.ts +283 -0
  508. package/ts/src/memory-decay.ts +55 -0
  509. package/ts/src/memory-utility.ts +70 -0
  510. package/ts/src/mock-llm.ts +4 -8
  511. package/ts/src/model.ts +88 -2
  512. package/ts/src/recall/card.ts +106 -0
  513. package/ts/src/recall/evaluator.ts +170 -0
  514. package/ts/src/recall/integration.ts +35 -0
  515. package/ts/src/recall/tick.ts +213 -0
  516. package/ts/src/session-link/plan-it-action.ts +78 -0
  517. package/ts/src/session-link/session-link.ts +275 -0
  518. package/ts/src/share/adapters.ts +93 -0
  519. package/ts/src/share/share-consent.ts +105 -0
  520. package/ts/src/share/share-deck.ts +85 -0
  521. package/ts/src/share/share-token.ts +166 -0
  522. package/ts/src/slot-spec.ts +172 -0
  523. package/ts/src/state-ledger.ts +1166 -0
  524. package/ts/src/subagent-job-id-guard.ts +72 -0
  525. package/ts/src/time-anchor.ts +338 -0
  526. package/ts/src/tool-packet.ts +32 -0
  527. package/ts/src/travel-slots.ts +219 -0
  528. package/ts/src/travel-timeline.ts +83 -0
  529. package/ts/src/turn-deadline.ts +366 -0
  530. package/ts/src/turn-policy.ts +154 -0
  531. package/ts/src/unified.ts +444 -105
  532. package/ts/src/wish-pool.ts +79 -0
  533. package/dist/scripts/session-attach-wait.js +0 -41
@@ -0,0 +1,169 @@
1
+ /**
2
+ * 通道探针 tick(外部事件接缝落地第 1 段,docs/design/external-event-seam.md §6①;
3
+ * issue #82 兼容方向)——外部事件作为健康面第二个生产者的本地形态。
4
+ *
5
+ * 设计边界(设计文档 §4,边界即信任):
6
+ * - 只读探测:每通道一个无副作用最小调用(whoami/免费公开 GET),不检索、不下单;
7
+ * - 本地生产者免鉴权(§3.2 本地可信):异常写 `down` 事件进
8
+ * <stateRoot>/gotry-state/channel-health.jsonl,routingAdvice/doctor 零改动受益;
9
+ * - 恢复走事件:探测通过且该通道此前处于 down → 写 'ok' 恢复事件
10
+ * (readLatestChannelEvents latest-wins 超越 down);此前健康则零写放大;
11
+ * - session:* 通道(需用户 Chrome 会话)无头不可探,显式 skip 不误报;
12
+ * - flyai 默认不探(匿名共享额度,探针烧额度弊大于利)——`--only flyai` 显式选入;
13
+ * - 不做常驻监听:tick 由 loopx/cron 驱动,一次运行一轮探测后退出。
14
+ *
15
+ * 用法(loopx/cron 驱动):
16
+ * npx tsx ts/scripts/channel-probe.ts --state-root <root> # 探测+落账
17
+ * npx tsx ts/scripts/channel-probe.ts --dry-run # 只打印不落账
18
+ * npx tsx ts/scripts/channel-probe.ts --only hbcli-hotel,open-meteo # 选通道
19
+ * npx tsx ts/scripts/channel-probe.ts --list # 列探测面
20
+ *
21
+ * @module scripts/channel-probe
22
+ */
23
+ import { recordChannelEvent, readLatestChannelEvents } from '../capabilities/channel-health.js';
24
+ function timeout(ms) {
25
+ return AbortSignal.timeout(ms);
26
+ }
27
+ async function fetchOk(url, what, ms = 8000) {
28
+ const t0 = Date.now();
29
+ try {
30
+ const res = await fetch(url, { signal: timeout(ms), headers: { Accept: 'application/json' } });
31
+ if (res.ok)
32
+ return { ok: true, ms: Date.now() - t0 };
33
+ return { ok: false, reason: `${what}-http-${res.status}`, ms: Date.now() - t0 };
34
+ }
35
+ catch (err) {
36
+ const msg = err instanceof Error ? err.name : String(err);
37
+ return { ok: false, reason: msg === 'TimeoutError' ? 'timeout' : 'unreachable', ms: Date.now() - t0 };
38
+ }
39
+ }
40
+ function spawnOk(cmd, args, what, ms = 10000) {
41
+ return async () => {
42
+ const t0 = Date.now();
43
+ try {
44
+ const { spawn } = await import('node:child_process');
45
+ const child = spawn(cmd, args, { stdio: ['ignore', 'ignore', 'ignore'] });
46
+ const code = await new Promise((resolve, reject) => {
47
+ const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('timeout')); }, ms);
48
+ child.on('exit', (c) => { clearTimeout(timer); resolve(c ?? 1); });
49
+ child.on('error', (e) => { clearTimeout(timer); reject(e); });
50
+ });
51
+ return code === 0 ? { ok: true, ms: Date.now() - t0 } : { ok: false, reason: `${what}-exit-${code}`, ms: Date.now() - t0 };
52
+ }
53
+ catch (err) {
54
+ const msg = err instanceof Error ? err.message : String(err);
55
+ return { ok: false, reason: msg === 'timeout' ? 'timeout' : 'spawn-failed', ms: Date.now() - t0 };
56
+ }
57
+ };
58
+ }
59
+ /** 默认探测面(设计文档 §6①「关键通道」的无头子集;session 系与 flyai 显式 skip) */
60
+ export function defaultProbes() {
61
+ return [
62
+ {
63
+ channel: 'hbcli-hotel',
64
+ probeable: true,
65
+ run: spawnOk('hbcli', ['auth', 'whoami'], 'auth-denied'),
66
+ },
67
+ {
68
+ channel: 'open-meteo',
69
+ probeable: true,
70
+ run: () => fetchOk('https://api.open-meteo.com/v1/forecast?latitude=25.2048&longitude=55.2708&current=temperature_2m', 'weather'),
71
+ },
72
+ {
73
+ channel: 'opensky',
74
+ probeable: true,
75
+ run: () => fetchOk('https://opensky-network.org/api/states/all?lamin=24.0&lomin=53.0&lamax=26.5&lomax=56.5', 'flights', 12000),
76
+ },
77
+ { channel: 'flyai', probeable: false, skipReason: 'anonymous-shared-quota——探针烧额度弊大于利;--only flyai 显式选入', run: async () => ({ ok: true, ms: 0 }) },
78
+ { channel: 'session:ctrip-flight', probeable: false, skipReason: '需用户 Chrome 会话,无头不可探', run: async () => ({ ok: true, ms: 0 }) },
79
+ { channel: 'session:ctrip-hotel', probeable: false, skipReason: '需用户 Chrome 会话,无头不可探', run: async () => ({ ok: true, ms: 0 }) },
80
+ { channel: 'session:12306-train', probeable: false, skipReason: '需用户 Chrome 会话,无头不可探', run: async () => ({ ok: true, ms: 0 }) },
81
+ { channel: 'anything-geo', probeable: false, skipReason: '静态包通道,无远端可探', run: async () => ({ ok: true, ms: 0 }) },
82
+ { channel: 'static-hotel', probeable: false, skipReason: '静态包通道,无远端可探', run: async () => ({ ok: true, ms: 0 }) },
83
+ { channel: 'web-read', probeable: false, skipReason: '依赖 agent-reach 装配态,探测意义低;--only 显式选入可探', run: async () => ({ ok: true, ms: 0 }) },
84
+ ];
85
+ }
86
+ /**
87
+ * 探测结果 → 事件序列(纯函数,测试锚点):
88
+ * - fail → down 事件(永远落,哪怕已 down——重申事实,消费方幂等);
89
+ * - ok 且此前处于 down → 'ok' 恢复事件(latest-wins 超越);
90
+ * - ok 且此前健康 → 不落(零写放大);
91
+ * - skip(probeable=false)→ 不产生事件。
92
+ */
93
+ export function evaluateProbeResults(results, priorDown, now = new Date()) {
94
+ const events = [];
95
+ for (const { probe, result } of results) {
96
+ if (!probe.probeable)
97
+ continue;
98
+ if (result.ok) {
99
+ if (priorDown.has(probe.channel)) {
100
+ events.push({ channel: probe.channel, state: 'ok', at: now.toISOString() });
101
+ }
102
+ continue;
103
+ }
104
+ events.push({ channel: probe.channel, state: 'down', reason: result.reason, at: now.toISOString() });
105
+ }
106
+ return events;
107
+ }
108
+ export function parseArgs(argv) {
109
+ let stateRoot;
110
+ let only;
111
+ let dryRun = false;
112
+ let list = false;
113
+ for (let i = 0; i < argv.length; i += 1) {
114
+ const a = argv[i];
115
+ if (a === '--state-root')
116
+ stateRoot = argv[++i];
117
+ else if (a === '--only')
118
+ only = (argv[++i] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
119
+ else if (a === '--dry-run')
120
+ dryRun = true;
121
+ else if (a === '--list')
122
+ list = true;
123
+ }
124
+ return { stateRoot, only, dryRun, list };
125
+ }
126
+ export async function main(argv) {
127
+ const args = parseArgs(argv);
128
+ const all = defaultProbes();
129
+ if (args.list) {
130
+ for (const p of all) {
131
+ console.log(`${p.probeable ? 'probe' : 'skip '} ${p.channel}${p.skipReason ? ` # ${p.skipReason}` : ''}`);
132
+ }
133
+ return 0;
134
+ }
135
+ const selected = args.only?.length ? all.filter((p) => args.only.includes(p.channel)) : all.filter((p) => p.probeable);
136
+ if (selected.length === 0) {
137
+ console.error('[channel-probe] --only 未匹配任何通道;--list 查看探测面');
138
+ return 2;
139
+ }
140
+ const results = [];
141
+ for (const probe of selected) {
142
+ const result = await probe.run();
143
+ results.push({ probe, result });
144
+ console.log(`[channel-probe] ${result.ok ? 'ok ' : 'FAIL'} ${probe.channel} (${result.ms}ms)${result.ok ? '' : ` reason=${result.reason}`}`);
145
+ }
146
+ const failures = results.filter((r) => !r.result.ok).length;
147
+ if (args.dryRun || !args.stateRoot) {
148
+ if (!args.stateRoot)
149
+ console.log('[channel-probe] 未给 --state-root:只探测不落账(等价 --dry-run)');
150
+ return failures > 0 ? 1 : 0;
151
+ }
152
+ let priorDown = new Set();
153
+ try {
154
+ const latest = await readLatestChannelEvents(args.stateRoot);
155
+ priorDown = new Set([...latest.entries()].filter(([, ev]) => ev.state === 'down' || ev.state === 'cooldown').map(([ch]) => ch));
156
+ }
157
+ catch { /* 读不到按全健康处理 */ }
158
+ for (const ev of evaluateProbeResults(results, priorDown)) {
159
+ await recordChannelEvent(args.stateRoot, ev);
160
+ console.log(`[channel-probe] 记录 ${ev.state} ${ev.channel}${ev.reason ? `(${ev.reason})` : ''}`);
161
+ }
162
+ return failures > 0 ? 1 : 0;
163
+ }
164
+ if (import.meta.url === `file://${process.argv[1]}`) {
165
+ main(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
166
+ console.error('[channel-probe] fatal:', err instanceof Error ? err.message : err);
167
+ process.exit(1);
168
+ });
169
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * 通道注册表 + 通道健康面测试(issue #106/#107/#108 编排设计落地,
3
+ * docs/design/tool-orchestration-design.md §2/§3;全离线,隔离 tmp stateRoot):
4
+ * 1. 注册表封闭性:id 唯一/quotaClass·tier·intent 闭集/静态包不可路由
5
+ * 2. 意图顺位:证据级降序(官方API > 会话 > 网页兜底),同 tier 按效率
6
+ * 3. routingAdvice:down 通道被排除/发起通道被排除/limit 截断;hit 即恢复;
7
+ * Dida down 时推进到下一个健康酒店通道
8
+ * 4. noteChannelVerdict 映射:needs-setup→down/hit→清除/miss·error→不动/cooldown 过期
9
+ * 5. persona 路由卡:确定性渲染,含改道规则与意图顺位行
10
+ * 6. JSONL 持久面:record/readLatest 往返 + 坏行容忍 + limitDays 过滤
11
+ * 7. doctor 集成:flyai 最近达限时间可见;calendar 三态(默认不挂载/opt-in 未配置/opt-in 已配置);
12
+ * patch 宿主插件两态照亮(map-tools/ask-user,issue #113 L1 残量)
13
+ * 8. apply 接线:channel_routing_card 变量已注册;参数级拒绝不带 routing 字段;
14
+ * 工具描述首行生成(检索工具前置意图顺位卡,非检索工具原样,#113)
15
+ * 9. 持久健康面投影(#436):persistedDownChannels 纯投影(有效 down 才排除;
16
+ * ok/cooldown/缺时间戳/坏时间戳/未来时间戳一律不排除)+ routingAdvice
17
+ * 与会话态的并集语义(会话失败永不被持久面解除)
18
+ *
19
+ * 运行: cd ts && npx tsx scripts/channel-registry-tests.ts
20
+ */
21
+ import assert from 'node:assert/strict';
22
+ import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
23
+ import { tmpdir } from 'node:os';
24
+ import { join } from 'node:path';
25
+ import { CHANNELS, channelsForIntent, routingAdvice, renderRoutingCard, toolRoutingHeadline, INTENT_LABELS, persistedDownChannels, } from '../capabilities/channel-registry.js';
26
+ import { noteChannelVerdict, channelState, clearChannel, resetChannelHealth, recordChannelEvent, readLatestChannelEvents, } from '../capabilities/channel-health.js';
27
+ import { runDoctorChecks } from '../capabilities/doctor.js';
28
+ import { apply } from '../src/index.js';
29
+ const NOW = 1_700_000_000_000;
30
+ // 1. 注册表封闭性
31
+ const QUOTA_CLASSES = ['user-session', 'user-key', 'anonymous-trial', 'free-public', 'static'];
32
+ const TIERS = ['realtime-api', 'session', 'best-effort', 'static'];
33
+ const INTENTS = Object.keys(INTENT_LABELS);
34
+ const ids = new Set();
35
+ for (const c of CHANNELS) {
36
+ assert.ok(!ids.has(c.id), `通道 id 重复:${c.id}`);
37
+ ids.add(c.id);
38
+ assert.ok(QUOTA_CLASSES.includes(c.quotaClass), `${c.id} quotaClass 越界`);
39
+ assert.ok(TIERS.includes(c.tier), `${c.id} tier 越界`);
40
+ assert.ok(c.intents.length > 0 && c.intents.every(i => INTENTS.includes(i)), `${c.id} intents 越界`);
41
+ assert.ok(c.tool.startsWith('gotry_'), `${c.id} 工具名形态`);
42
+ if (c.quotaClass === 'static')
43
+ assert.equal(c.routable, false, '静态包不进模型可选路由');
44
+ }
45
+ assert.ok(CHANNELS.length >= 9, '注册表覆盖面(≥9 通道)');
46
+ console.log('1. 注册表封闭性 OK');
47
+ // 2. 意图顺位(证据级降序)
48
+ resetChannelHealth();
49
+ assert.deepEqual(channelsForIntent('search-flight').map(c => c.id), ['flyai', 'session:ctrip-flight', 'web-read'], '机票顺位');
50
+ assert.deepEqual(channelsForIntent('search-train').map(c => c.id), ['flyai', 'session:12306-train', 'web-read'], '火车顺位');
51
+ assert.deepEqual(channelsForIntent('search-hotel').map(c => c.id), ['flyai', 'hbcli-hotel', 'session:ctrip-hotel', 'session:dida-portal', 'web-read'], '酒店完整顺位(Dida 登记后仍钉全序)');
52
+ assert.deepEqual(channelsForIntent('weather').map(c => c.id), ['open-meteo'], '单通道意图');
53
+ console.log('2. 意图顺位(证据级 > 效率)OK');
54
+ // 3. routingAdvice:健康面驱动
55
+ const base = routingAdvice('search-flight');
56
+ assert.equal(base.alternatives[0].channel, 'flyai', '健康时 flyai 首荐(零 setup 摩擦)');
57
+ assert.ok(base.alternatives[0].why.length > 4 && (base.alternatives[0].setup?.length ?? 0) > 0, 'why/setup 齐备');
58
+ // #559 B 步:flyai setup 行必须显式提到 Trial limit reached 后果,给匿名试用用户
59
+ // 一眼看见「达限后会改道 gotry_session_search」(避免装好的人替没装的人以为整盘不可用)
60
+ assert.match(base.alternatives[0].setup ?? '', /Trial limit reached/, 'flyai setup 行必显 Trial limit reached');
61
+ assert.match(base.alternatives[0].setup ?? '', /gotry_session_search/, 'flyai setup 行必显 fallback 路径');
62
+ noteChannelVerdict('flyai', 'needs-setup', { now: NOW });
63
+ const afterDown = routingAdvice('search-flight', { now: NOW });
64
+ assert.ok(!afterDown.alternatives.some(a => a.channel === 'flyai'), 'flyai 达限后从建议表排除(#108 病灶)');
65
+ assert.equal(afterDown.alternatives[0].channel, 'session:ctrip-flight', 'session 升首荐(优先级=健康面投影)');
66
+ const excl = routingAdvice('search-flight', { excludeChannel: 'session:ctrip-flight', now: NOW });
67
+ assert.ok(!excl.alternatives.some(a => a.channel === 'session:ctrip-flight'), '发起通道被排除');
68
+ assert.equal(routingAdvice('search-flight', { limit: 1, now: NOW }).alternatives.length, 1, 'limit 截断');
69
+ noteChannelVerdict('flyai', 'hit', { now: NOW });
70
+ assert.equal(routingAdvice('search-flight', { now: NOW }).alternatives[0].channel, 'flyai', 'hit 即恢复(#107:补 key 不被陈旧状态锁死)');
71
+ // Dida 已是酒店 session 通道的一员:#297 后先让更高顺位通道不可用,证明 Dida
72
+ // 成为当前首选;再把 Dida 标 down,必须推进到注册表中的后继 web-read。
73
+ resetChannelHealth();
74
+ for (const channel of ['flyai', 'hbcli-hotel', 'session:ctrip-hotel']) {
75
+ noteChannelVerdict(channel, 'needs-setup', { now: NOW });
76
+ }
77
+ assert.equal(routingAdvice('search-hotel', { now: NOW }).alternatives[0].channel, 'session:dida-portal', '前三个酒店通道 down 后 Dida 升为首荐');
78
+ noteChannelVerdict('session:dida-portal', 'needs-login', { now: NOW });
79
+ assert.equal(routingAdvice('search-hotel', { now: NOW }).alternatives[0].channel, 'web-read', 'Dida down 后推进到后继健康酒店通道');
80
+ console.log('3. routingAdvice(down 排除/发起排除/limit/hit 恢复 + Dida down→后继)OK');
81
+ // 4. noteChannelVerdict 映射闭集
82
+ resetChannelHealth();
83
+ assert.ok(noteChannelVerdict('x', 'needs-setup', { now: NOW }), 'needs-setup → down 事件');
84
+ assert.equal(channelState('x', NOW)?.state, 'down');
85
+ assert.equal(noteChannelVerdict('x', 'challenged', { now: NOW })?.reason, 'challenged', 'challenged → down');
86
+ assert.equal(noteChannelVerdict('x', 'needs-login', { now: NOW })?.reason, 'needs-login', 'needs-login → down');
87
+ assert.equal(noteChannelVerdict('x', 'needs-extension', { now: NOW })?.reason, 'needs-extension', 'needs-extension → down');
88
+ assert.equal(noteChannelVerdict('x', 'miss', { now: NOW }), undefined, '业务 miss ≠ 通道故障');
89
+ assert.equal(noteChannelVerdict('x', 'error', { now: NOW }), undefined, '业务 error ≠ 通道故障');
90
+ clearChannel('x');
91
+ assert.equal(channelState('x', NOW), undefined, '清除后无状态');
92
+ const cd = noteChannelVerdict('y', 'cooldown', { now: NOW, cooldownMs: 30_000 });
93
+ assert.equal(cd?.state, 'cooldown', 'cooldown → cooldown 事件');
94
+ assert.equal(channelState('y', NOW + 29_999)?.state, 'cooldown', '冷却未满仍在');
95
+ assert.equal(channelState('y', NOW + 30_001), undefined, '冷却满惰性过期');
96
+ console.log('4. noteChannelVerdict 映射闭集 OK');
97
+ // 5. persona 路由卡
98
+ const card1 = renderRoutingCard(new Date(NOW));
99
+ const card2 = renderRoutingCard(new Date(NOW));
100
+ assert.equal(card1, card2, '同刻渲染确定');
101
+ assert.match(card1, /检索通道顺位/, '卡头');
102
+ assert.match(card1, /verdict≠hit/, '改道规则在卡内');
103
+ assert.match(card1, /- 机票: .+gotry_flyai_search.+→.+gotry_session_search/, '机票顺位行');
104
+ assert.match(card1, /- 火车: .+12306 公开查询面.+kind=train/, '火车行带 kind 消歧');
105
+ assert.match(card1, /匿名试用=首次体验导流/, '额度口径脚注(D-7)');
106
+ assert.ok(!card1.includes('- 天气'), '单通道意图不上卡');
107
+ console.log('5. persona 路由卡 OK');
108
+ // 6. JSONL 持久面往返(事件时间用真实时钟——limitDays 窗口以读方时钟为基准)
109
+ const stateRoot = await mkdtemp(join(tmpdir(), 'gotry-channel-test-'));
110
+ const realNow = Date.now();
111
+ await recordChannelEvent(stateRoot, { channel: 'flyai', state: 'down', reason: 'needs-setup', at: new Date(realNow - 86_400_000).toISOString() });
112
+ await recordChannelEvent(stateRoot, { channel: 'flyai', state: 'down', reason: 'needs-setup', at: new Date(realNow).toISOString() });
113
+ await recordChannelEvent(stateRoot, { channel: 'session:ctrip-flight', state: 'cooldown', at: new Date(realNow).toISOString() });
114
+ const latest = await readLatestChannelEvents(stateRoot);
115
+ assert.equal(latest.get('flyai')?.at, new Date(realNow).toISOString(), '每通道取最新');
116
+ assert.equal(latest.get('session:ctrip-flight')?.state, 'cooldown');
117
+ await recordChannelEvent(stateRoot, { channel: 'legacy', state: 'down', at: new Date(realNow - 40 * 86_400_000).toISOString() });
118
+ assert.ok(!((await readLatestChannelEvents(stateRoot)).has('legacy')), '40 天前事件被 limitDays 过滤');
119
+ const raw = `${stateRoot}/gotry-state/channel-health.jsonl`;
120
+ await writeFile(raw, `${await readFile(raw, 'utf-8')}not-json\n{"half":true}\n`, 'utf-8');
121
+ assert.equal((await readLatestChannelEvents(stateRoot)).size, 2, '坏行容忍');
122
+ console.log('6. JSONL 持久面 OK');
123
+ // 7. doctor 集成:flyai 配额可见 + calendar 三态(setup 状态面,非 env)
124
+ const emptyRepo = join(stateRoot, 'repo');
125
+ const emptyHome = join(stateRoot, 'home');
126
+ await mkdir(emptyRepo, { recursive: true });
127
+ await mkdir(join(emptyHome, '.gotry'), { recursive: true });
128
+ const calStatePath = join(emptyHome, '.gotry', 'calendar.json');
129
+ const d1 = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: emptyHome, env: {}, stateRoot });
130
+ assert.match(d1.items.find(i => i.id === 'flyai').detail, /最近一次试用达限/, '最近达限时间进 doctor(#107 可见性)');
131
+ assert.equal(d1.items.find(i => i.id === 'calendar').status, 'ok', 'calendar 默认不挂载=ok(D-9)');
132
+ assert.match(d1.items.find(i => i.id === 'calendar').detail, /默认未挂载/, '默认态人话');
133
+ await writeFile(calStatePath, JSON.stringify({ enabled: true }), 'utf-8');
134
+ const d2 = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: emptyHome, env: {} });
135
+ assert.equal(d2.items.find(i => i.id === 'calendar').status, 'degraded', 'setup 开启但未配置 username=degraded');
136
+ assert.match(d2.items.find(i => i.id === 'calendar').fix ?? '', /npx @danceiny\/gotry setup calendar/, 'fix 指向 setup 状态面');
137
+ const profileDir = join(emptyHome, '.dsh/profiles/web');
138
+ await mkdir(profileDir, { recursive: true });
139
+ await writeFile(join(profileDir, 'cordis.patch.yml'), '- id: dsh-calendar\n config:\n username: someone\n', 'utf-8');
140
+ const d3 = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: emptyHome, env: {} });
141
+ assert.equal(d3.items.find(i => i.id === 'calendar').status, 'ok', 'setup 开启且已配置=ok');
142
+ const d4 = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: emptyHome, env: {} });
143
+ assert.ok(!/最近一次试用达限/.test(d4.items.find(i => i.id === 'flyai').detail), '无 stateRoot 不读持久面(向后兼容)');
144
+ // patch 宿主插件两态照亮(issue #113):隔离环境两者皆 missing——静默剔除面进 doctor 可见
145
+ const mapItem = d4.items.find(i => i.id === 'map-tools');
146
+ const askItem = d4.items.find(i => i.id === 'ask-user');
147
+ assert.equal(mapItem.status, 'missing', '隔离环境 map-tools=missing(静默剔除面可见)');
148
+ assert.match(mapItem.detail, /静默剔除/, 'map-tools detail 说明静默剔除语义');
149
+ assert.equal(askItem.status, 'missing', '隔离环境 ask-user=missing');
150
+ assert.match(askItem.detail, /散文追问/, 'ask-user detail 说明退化面');
151
+ console.log('7. doctor 集成(flyai 配额可见 + calendar setup 状态面三态 + 宿主插件两态)OK');
152
+ // 8. apply 接线:变量注册 + 参数级拒绝不带 routing
153
+ resetChannelHealth();
154
+ const variables = {};
155
+ const registered = [];
156
+ const ctx = {
157
+ tools: { register: (t) => registered.push(t) },
158
+ systemPrompt: { variable: (name, provider) => { variables[name] = provider; } },
159
+ on: () => () => { },
160
+ };
161
+ apply(ctx, { stateRoot, timeoutMs: 30_000, hbcliBin: 'hbcli-not-on-path', sessionAccess: 'ask' });
162
+ assert.equal(typeof variables.channel_routing_card, 'function', 'channel_routing_card 变量已注册');
163
+ assert.match(variables.channel_routing_card(), /检索通道顺位/, '卡片可渲染');
164
+ const pastDate = await registered.find(t => t.name === 'gotry_flyai_search').execute({ kind: 'flight', from: '深圳', to: '普吉', date: '2026-01-01' }, null);
165
+ assert.ok(!('routing' in pastDate), '参数级预校验拒绝不带 routing(未发起检索,无改道语义)');
166
+ // D-30 迁移锁(issue #112):typed 平铺契约下 legacy blob 包裹在宿主权校验即拒绝,
167
+ // 拒绝形状保持 ADR-13 ToolFailure(guardToolExecute 兜底:ok:false + evidence)且不带 routing
168
+ const legacyBlob = await registered.find(t => t.name === 'gotry_flyai_search').execute({ query: { kind: 'flight', from: '深圳', to: '普吉', date: '2026-01-01' } }, null);
169
+ assert.ok(legacyBlob.ok === false && !('routing' in legacyBlob) && !!legacyBlob.evidence, 'legacy blob → 结构化拒绝(ToolFailure,无 routing)');
170
+ // 工具描述首行生成(issue #113):检索工具前置注册表生成的意图顺位卡;非检索工具原样
171
+ const flyaiTool = registered.find(t => t.name === 'gotry_flyai_search');
172
+ const doctorToolDesc = registered.find(t => t.name === 'gotry_doctor');
173
+ assert.ok(flyaiTool, 'flyai 工具已注册');
174
+ assert.match(toolRoutingHeadline('gotry_flyai_search'), /机票: 飞猪官方 API\(gotry_flyai_search\) → 携程·用户登录态/, 'flyai 首行卡:机票顺位 flyai 在前(证据级)');
175
+ assert.match(toolRoutingHeadline('gotry_session_search'), /火车: .*12306 公开查询面\(gotry_session_search kind=train\)/, 'session 首行卡带 kind 消歧');
176
+ assert.equal(toolRoutingHeadline('gotry_doctor'), '', '非检索工具无首行卡(原样描述)');
177
+ assert.ok(flyaiTool.description?.startsWith('本工具服务的检索意图与通道顺位'), 'flyai 描述已前置首行卡(接线生效)');
178
+ assert.ok(!doctorToolDesc.description?.startsWith('本工具服务的检索意图'), 'doctor 描述无首行卡(接线不误伤)');
179
+ console.log('8. apply 接线 + 描述首行生成 OK');
180
+ // 9. 持久健康面投影 + 并集语义(issue #436)
181
+ resetChannelHealth();
182
+ const latestEvents = new Map([
183
+ ['down-valid', { state: 'down', at: new Date(NOW - 60_000).toISOString() }],
184
+ ['down-now', { state: 'down', at: new Date(NOW).toISOString() }],
185
+ ['ok-latest', { state: 'ok', at: new Date(NOW).toISOString() }],
186
+ ['cooldown-latest', { state: 'cooldown', at: new Date(NOW).toISOString() }],
187
+ ['down-no-at', { state: 'down' }],
188
+ ['down-bad-at', { state: 'down', at: 'not-a-date' }],
189
+ ['down-future', { state: 'down', at: new Date(NOW + 86_400_000).toISOString() }],
190
+ ]);
191
+ assert.deepEqual([...persistedDownChannels(latestEvents, NOW)].sort(), ['down-now', 'down-valid'], '投影:只有带有效时间戳且不在未来的 down 才排除(cooldown 是节律、ok 是恢复)');
192
+ assert.ok(persistedDownChannels(new Map(), NOW).size === 0, '空事件表=零排除');
193
+ // 并集语义:持久集合只加排除,不能解除会话态;顺位表本身不变
194
+ const noPersisted = routingAdvice('search-flight', { now: NOW });
195
+ assert.ok(noPersisted.alternatives.some(a => a.channel === 'session:ctrip-flight'), '无持久集合=原会话态口径');
196
+ const persistedExcl = routingAdvice('search-flight', { now: NOW, persistedDownChannels: new Set(['session:ctrip-flight']) });
197
+ assert.ok(!persistedExcl.alternatives.some(a => a.channel === 'session:ctrip-flight'), '持久 down 通道被排除');
198
+ assert.equal(persistedExcl.alternatives[0].channel, 'flyai', '顺位不变,只过滤');
199
+ const persistedLimit = routingAdvice('search-hotel', { now: NOW, persistedDownChannels: new Set(['flyai', 'hbcli-hotel']), limit: 1 });
200
+ assert.deepEqual(persistedLimit.alternatives.map(a => a.channel), ['session:ctrip-hotel'], '持久排除叠加后 limit 仍截断');
201
+ noteChannelVerdict('session:ctrip-flight', 'challenged', { now: NOW });
202
+ assert.ok(!routingAdvice('search-flight', { now: NOW, persistedDownChannels: new Set() }).alternatives.some(a => a.channel === 'session:ctrip-flight'), '会话 down 在空持久集合(含持久 ok 恢复)下仍排除——持久面不解锁会话失败');
203
+ assert.equal(channelState('session:ctrip-flight', NOW)?.state, 'down', '会话态未被持久面改写');
204
+ clearChannel('session:ctrip-flight');
205
+ assert.ok(routingAdvice('search-flight', { now: NOW, persistedDownChannels: new Set() }).alternatives.some(a => a.channel === 'session:ctrip-flight'), '会话清除 + 持久面无 down → 通道恢复(恢复只对没有会话 down 的通道生效)');
206
+ console.log('9. 持久健康面投影 + 并集语义(#436)OK');
207
+ await rm(stateRoot, { recursive: true, force: true });
208
+ console.log('channel-registry-tests: 全部通过');
@@ -0,0 +1,234 @@
1
+ /**
2
+ * City × scenario tiering MECHANISM tests (issue #339 contract slice,
3
+ * SIMULATED-TRIGGER DRILL — fully offline, zero network, zero state).
4
+ *
5
+ * ⚠ Evidence label: `simulated_trigger_drill` / `fixture_contract`. #339's real
6
+ * trigger (reviewable real usage samples + an explicit scenario vocabulary and
7
+ * success metric) has NOT fired. Every registry used below is a TEST-ONLY shape
8
+ * probe constructed with `{ triggerFired: true }`; its city/scenario strings are
9
+ * deliberately meaningless placeholders (`city-a`, `scenario-x`) so that nothing
10
+ * here can be mistaken for a taxonomy. The product registry stays frozen EMPTY —
11
+ * #339 forbids fabricating a profile from fixtures.
12
+ *
13
+ * Sections:
14
+ * A. Default-off proof: trigger frozen false, registry frozen empty in SOURCE,
15
+ * admission refused before any validation, every resolution neutral.
16
+ * B. Admission shape: mandatory provenance (evidence grade / real sample source /
17
+ * ≥3 sample refs / reviewer / freeze instant) and retirement (condition +
18
+ * review deadline postdating the freeze); each omission fail-closed.
19
+ * C. Bounded modifier: integer ppm inside declared bounds; 0, negative,
20
+ * out-of-range, float and non-numeric refused — no tier can annihilate a score.
21
+ * D. Neutral fallback: unknown city, unknown scenario, conflicting evidence,
22
+ * empty registry; a conflict elects no winner.
23
+ * E. Ranking-only falsification: a modifier reorders but can NEVER remove a
24
+ * candidate (incl. lower bound × zero semantic), the result exposes no filter
25
+ * surface, and the mechanical no-hard-filter check passes.
26
+ * F. Determinism + versioning: identical inputs → byte-identical output; ties keep
27
+ * declaration order; a defective registry entry is refused, never clamped.
28
+ *
29
+ * Run (from ts/): npx tsx scripts/city-scenario-tier-tests.ts
30
+ */
31
+ import { readFileSync } from 'node:fs';
32
+ import { join } from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+ import { CITY_SCENARIO_TIER_REGISTRY, CITY_SCENARIO_TIER_SCHEMA, CITY_SCENARIO_TIER_TRIGGER_FIRED, TIER_APPLICATION_SURFACES, TIER_EVIDENCE_GRADES, TIER_KEY_MAX_CHARS, TIER_MIN_SAMPLE_REFS, TIER_MODIFIER_PPM_MAX, TIER_MODIFIER_PPM_MIN, TIER_MODIFIER_PPM_NEUTRAL, TIER_NEUTRAL_REASONS, TIER_SAMPLE_SOURCES, admitTierEntry, applyTierRanking, normalizeTierKey, resolveTier, tierRankingFilterViolation, } from '../src/city-scenario-tier.js';
35
+ let passed = 0;
36
+ function check(cond, msg) {
37
+ if (cond) {
38
+ passed++;
39
+ console.log(` ok - ${msg}`);
40
+ }
41
+ else {
42
+ console.error(` FAIL - ${msg}`);
43
+ process.exitCode = 1;
44
+ }
45
+ }
46
+ function expectRefusal(result, code, needle, msg) {
47
+ if (result.ok) {
48
+ check(false, `${msg} (expected refusal ${code}, got ok)`);
49
+ return;
50
+ }
51
+ check(result.code === code && result.detail.includes(needle), `${msg} (refused ${result.code}${result.code === code ? '' : ` ≠ ${code}`}, evidence contains "${needle}": ${result.detail.includes(needle)})`);
52
+ }
53
+ const FROZEN_AT = '2026-10-04T00:00:00.000Z';
54
+ const REVIEW_BY = '2027-04-04T00:00:00.000Z';
55
+ /** TEST-ONLY shape probe. The keys are meaningless placeholders, not a taxonomy. */
56
+ function probeEntryInput(overrides = {}) {
57
+ return {
58
+ tier_id: 'probe-tier-1',
59
+ city_key: 'city-a',
60
+ scenario_key: 'scenario-x',
61
+ modifier_ppm: 1_100_000,
62
+ applies_to: 'ranking',
63
+ provenance: {
64
+ evidence_grade: 'controlled_replay',
65
+ sample_source: 'real_usage_sample',
66
+ sample_refs: ['sample-ref-1', 'sample-ref-2', 'sample-ref-3'],
67
+ reviewer: 'drill-reviewer',
68
+ frozen_at: FROZEN_AT,
69
+ },
70
+ retirement: { condition: 'withdraw if the controlled replay no longer reproduces the ordering', review_by: REVIEW_BY },
71
+ taxonomy_version: 'probe.v0',
72
+ ...overrides,
73
+ };
74
+ }
75
+ function admittedProbe(overrides = {}, registry = []) {
76
+ const r = admitTierEntry(probeEntryInput(overrides), { triggerFired: true, registry });
77
+ if (!r.ok)
78
+ throw new Error(`probe admission failed unexpectedly: ${r.code} ${r.detail}`);
79
+ return r.value;
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // A. Default-off proof
83
+ // ---------------------------------------------------------------------------
84
+ console.log('A. default-off: trigger frozen false, registry frozen EMPTY, nothing admitted');
85
+ check(CITY_SCENARIO_TIER_TRIGGER_FIRED === false, '#339 trigger gate frozen false');
86
+ check(CITY_SCENARIO_TIER_REGISTRY.length === 0, 'the runtime taxonomy registry is empty');
87
+ check(CITY_SCENARIO_TIER_SCHEMA === 'gotry_city_scenario_tier.v1', 'the taxonomy schema id is versioned');
88
+ const MODULE_SOURCE = readFileSync(join(fileURLToPath(new URL('.', import.meta.url)), '..', 'src', 'city-scenario-tier.ts'), 'utf8');
89
+ check(MODULE_SOURCE.includes('export const CITY_SCENARIO_TIER_REGISTRY: readonly CityScenarioTierEntry[] = []'), 'the registry declaration is literally `= []` in source (no taxonomy content in code)');
90
+ check(MODULE_SOURCE.includes('export const CITY_SCENARIO_TIER_TRIGGER_FIRED = false'), 'the trigger declaration is literally `= false` in source');
91
+ expectRefusal(admitTierEntry(probeEntryInput()), 'trigger_not_fired', '不凭 fixture 造画像', 'with the default trigger, admission is refused before any validation');
92
+ expectRefusal(admitTierEntry({ tier_id: '', city_key: '', scenario_key: '', modifier_ppm: -1, taxonomy_version: '' }), 'trigger_not_fired', '#339 trigger has not fired', 'even a wholly invalid entry is refused by the trigger gate first (pre-trigger refusal precedes validation)');
93
+ const defaultResolution = resolveTier({ cityKey: 'any-city', scenarioKey: 'any-scenario' });
94
+ check(defaultResolution.ok && defaultResolution.value.status === 'neutral' && defaultResolution.value.modifier_ppm === TIER_MODIFIER_PPM_NEUTRAL, 'with the default trigger every pair resolves to the neutral modifier ×1.0');
95
+ check(defaultResolution.ok && defaultResolution.value.status === 'neutral' && defaultResolution.value.reason === 'trigger_not_fired', 'the neutral reason is trigger_not_fired, stated rather than implied');
96
+ check(TIER_MODIFIER_PPM_NEUTRAL === 1_000_000, 'the neutral modifier is exactly ×1.0');
97
+ check(TIER_NEUTRAL_REASONS.length === 5 && TIER_NEUTRAL_REASONS.includes('conflicting_evidence'), 'the neutral-reason set is closed and names conflicting evidence');
98
+ // ---------------------------------------------------------------------------
99
+ // B. Admission shape (mandatory provenance + retirement)
100
+ // ---------------------------------------------------------------------------
101
+ console.log('B. admission shape: provenance and retirement are mandatory per tier');
102
+ const admitted = admitTierEntry(probeEntryInput(), { triggerFired: true, registry: [] });
103
+ check(admitted.ok && admitted.value.schema === CITY_SCENARIO_TIER_SCHEMA && admitted.value.taxonomy_version === 'probe.v0', 'a complete probe entry is admitted and carries its schema + taxonomy version');
104
+ check(admitted.ok && admitted.value.applies_to === 'ranking' && TIER_APPLICATION_SURFACES.length === 1, 'the only application surface is `ranking` (there is no hard-filter surface)');
105
+ check(admitted.ok && admitted.value.provenance.sample_refs.length === 3 && admitted.value.retirement.review_by === REVIEW_BY, 'provenance sample refs and the retirement deadline are preserved');
106
+ expectRefusal(admitTierEntry(probeEntryInput({ provenance: undefined }), { triggerFired: true }), 'provenance_required', 'a tier without a stated evidence source is not evidence', 'missing provenance refused');
107
+ expectRefusal(admitTierEntry(probeEntryInput({ retirement: undefined }), { triggerFired: true }), 'retirement_required', 'a tier that cannot expire is not evidence', 'missing retirement refused');
108
+ expectRefusal(admitTierEntry(probeEntryInput({ provenance: { ...probeEntryInput().provenance, sample_source: 'fixture' } }), { triggerFired: true }), 'closed_set', '不凭 fixture 造画像', 'a fixture-sourced sample is refused by name (#339 red line)');
109
+ check(TIER_SAMPLE_SOURCES.length === 1 && TIER_SAMPLE_SOURCES[0] === 'real_usage_sample', 'the admitted sample-source set has exactly one member: real_usage_sample');
110
+ expectRefusal(admitTierEntry(probeEntryInput({ provenance: { ...probeEntryInput().provenance, evidence_grade: 'vibes' } }), { triggerFired: true }), 'closed_set', TIER_EVIDENCE_GRADES[0], 'an evidence grade outside the RFC S2 ordering is refused');
111
+ expectRefusal(admitTierEntry(probeEntryInput({ provenance: { ...probeEntryInput().provenance, sample_refs: ['only-one'] } }), { triggerFired: true }), 'provenance_required', 'a single anecdote is not a tier', `fewer than ${TIER_MIN_SAMPLE_REFS} sample refs refused`);
112
+ expectRefusal(admitTierEntry(probeEntryInput({ provenance: { ...probeEntryInput().provenance, reviewer: '' } }), { triggerFired: true }), 'provenance_required', 'reviewer', 'a tier with no accountable reviewer refused');
113
+ expectRefusal(admitTierEntry(probeEntryInput({ provenance: { ...probeEntryInput().provenance, frozen_at: '2026-10-04' } }), { triggerFired: true }), 'bad_ts', 'date-only', 'a date-only freeze instant refused');
114
+ expectRefusal(admitTierEntry(probeEntryInput({ retirement: { condition: '', review_by: REVIEW_BY } }), { triggerFired: true }), 'retirement_required', 'retirement.condition', 'an empty retirement condition refused');
115
+ expectRefusal(admitTierEntry(probeEntryInput({ retirement: { condition: 'c', review_by: FROZEN_AT } }), { triggerFired: true }), 'retirement_required', 'must postdate', 'a review deadline that does not postdate the freeze refused');
116
+ expectRefusal(admitTierEntry(probeEntryInput({ applies_to: 'hard_filter' }), { triggerFired: true }), 'closed_set', 'memory never enters a hard filter', 'applies_to = hard_filter refused by name');
117
+ expectRefusal(admitTierEntry(probeEntryInput({ taxonomy_version: '' }), { triggerFired: true }), 'binding_incomplete', 'taxonomy_version', 'an unversioned tier refused');
118
+ expectRefusal(admitTierEntry(probeEntryInput({ tier_id: ' ' }), { triggerFired: true }), 'binding_incomplete', 'tier_id', 'a blank tier id refused');
119
+ expectRefusal(admitTierEntry(probeEntryInput({ city_key: '' }), { triggerFired: true }), 'bad_key', 'must not be blank', 'a blank city key refused (blank ≠ unknown)');
120
+ expectRefusal(admitTierEntry(probeEntryInput({ scenario_key: 'x'.repeat(TIER_KEY_MAX_CHARS + 1) }), { triggerFired: true }), 'bad_key', 'never prose', 'an over-long scenario key refused');
121
+ const oneEntry = [admittedProbe()];
122
+ expectRefusal(admitTierEntry(probeEntryInput({ tier_id: 'probe-tier-2' }), { triggerFired: true, registry: oneEntry }), 'duplicate_tier', 'conflicting evidence, not an additional tier', 'a second tier for the same (city, scenario) pair is refused at admission');
123
+ expectRefusal(admitTierEntry(probeEntryInput({ city_key: 'city-b' }), { triggerFired: true, registry: oneEntry }), 'duplicate_tier', 'already exists', 'a duplicate tier_id refused');
124
+ // ---------------------------------------------------------------------------
125
+ // C. Bounded modifier
126
+ // ---------------------------------------------------------------------------
127
+ console.log('C. bounded modifier: declared [min,max], lower bound > 0 by design');
128
+ check(TIER_MODIFIER_PPM_MIN > 0 && TIER_MODIFIER_PPM_MIN < TIER_MODIFIER_PPM_NEUTRAL && TIER_MODIFIER_PPM_MAX > TIER_MODIFIER_PPM_NEUTRAL, 'bounds straddle neutral and the lower bound is strictly positive');
129
+ check(admitTierEntry(probeEntryInput({ modifier_ppm: TIER_MODIFIER_PPM_MIN }), { triggerFired: true }).ok, 'the lower bound itself is admissible (inclusive)');
130
+ check(admitTierEntry(probeEntryInput({ modifier_ppm: TIER_MODIFIER_PPM_MAX }), { triggerFired: true }).ok, 'the upper bound itself is admissible (inclusive)');
131
+ expectRefusal(admitTierEntry(probeEntryInput({ modifier_ppm: 0 }), { triggerFired: true }), 'modifier_bounds', 'never annihilate a candidate', 'a zero modifier is refused (it would annihilate a candidate score)');
132
+ expectRefusal(admitTierEntry(probeEntryInput({ modifier_ppm: -1_000_000 }), { triggerFired: true }), 'modifier_bounds', 'outside the declared bounds', 'a negative modifier refused');
133
+ expectRefusal(admitTierEntry(probeEntryInput({ modifier_ppm: TIER_MODIFIER_PPM_MIN - 1 }), { triggerFired: true }), 'modifier_bounds', 'outside the declared bounds', 'one ppm below the lower bound refused');
134
+ expectRefusal(admitTierEntry(probeEntryInput({ modifier_ppm: TIER_MODIFIER_PPM_MAX + 1 }), { triggerFired: true }), 'modifier_bounds', 'outside the declared bounds', 'one ppm above the upper bound refused');
135
+ expectRefusal(admitTierEntry(probeEntryInput({ modifier_ppm: 1_100_000.5 }), { triggerFired: true }), 'modifier_bounds', 'ppm integers are exact', 'a fractional ppm refused');
136
+ expectRefusal(admitTierEntry(probeEntryInput({ modifier_ppm: '1100000' }), { triggerFired: true }), 'modifier_bounds', 'must be an integer ppm', 'a string modifier refused');
137
+ expectRefusal(admitTierEntry(probeEntryInput({ modifier_ppm: Number.NaN }), { triggerFired: true }), 'modifier_bounds', 'must be an integer ppm', 'NaN modifier refused');
138
+ // ---------------------------------------------------------------------------
139
+ // D. Neutral fallback, never a guess
140
+ // ---------------------------------------------------------------------------
141
+ console.log('D. neutral fallback: unknown city / unknown scenario / conflicting evidence');
142
+ const probeRegistry = [admittedProbe()];
143
+ const hit = resolveTier({ cityKey: 'city-a', scenarioKey: 'scenario-x' }, { registry: probeRegistry, triggerFired: true });
144
+ check(hit.ok && hit.value.status === 'hit' && hit.value.modifier_ppm === 1_100_000, 'a known (city, scenario) pair resolves to its bounded modifier');
145
+ check(hit.ok && hit.value.status === 'hit' && hit.value.explanation.includes('retire when:'), 'a hit explains itself and surfaces its retirement condition');
146
+ check(hit.ok && hit.value.status === 'hit' && hit.value.evidence_grade === 'controlled_replay' && hit.value.taxonomy_version === 'probe.v0', 'a hit carries the evidence grade and taxonomy version');
147
+ const unknownCity = resolveTier({ cityKey: 'city-zz', scenarioKey: 'scenario-x' }, { registry: probeRegistry, triggerFired: true });
148
+ check(unknownCity.ok && unknownCity.value.status === 'neutral' && unknownCity.value.reason === 'unknown_city' && unknownCity.value.modifier_ppm === TIER_MODIFIER_PPM_NEUTRAL, 'unknown city → neutral ×1.0 with reason unknown_city');
149
+ check(unknownCity.ok && unknownCity.value.status === 'neutral' && unknownCity.value.explanation.includes('never guessed from a neighbour'), 'the unknown-city fallback states that no neighbour is substituted');
150
+ const unknownScenario = resolveTier({ cityKey: 'city-a', scenarioKey: 'scenario-zz' }, { registry: probeRegistry, triggerFired: true });
151
+ check(unknownScenario.ok && unknownScenario.value.status === 'neutral' && unknownScenario.value.reason === 'unknown_scenario', 'unknown scenario → neutral ×1.0 with reason unknown_scenario');
152
+ const emptyRegistry = resolveTier({ cityKey: 'city-a', scenarioKey: 'scenario-x' }, { registry: [], triggerFired: true });
153
+ check(emptyRegistry.ok && emptyRegistry.value.status === 'neutral' && emptyRegistry.value.reason === 'empty_registry', 'empty registry → neutral ×1.0, not a zero weight');
154
+ /** Conflicting registry built by bypassing the admission duplicate guard (a defective registry). */
155
+ const conflicting = [
156
+ admittedProbe({ tier_id: 'probe-conflict-a', modifier_ppm: 1_100_000 }),
157
+ admittedProbe({ tier_id: 'probe-conflict-b', modifier_ppm: 900_000 }),
158
+ ];
159
+ const conflict = resolveTier({ cityKey: 'city-a', scenarioKey: 'scenario-x' }, { registry: conflicting, triggerFired: true });
160
+ check(conflict.ok && conflict.value.status === 'neutral' && conflict.value.reason === 'conflicting_evidence' && conflict.value.modifier_ppm === TIER_MODIFIER_PPM_NEUTRAL, 'conflicting evidence → neutral ×1.0, no winner elected');
161
+ check(conflict.ok && conflict.value.status === 'neutral' && conflict.value.conflicting_tier_ids.join(',') === 'probe-conflict-a,probe-conflict-b', 'the conflict surfaces both tier ids for diagnosis (without choosing)');
162
+ check(conflict.ok && conflict.value.tier_id === null, 'a neutral resolution never reports a tier id');
163
+ check(normalizeTierKey(' City-A ', 'k').ok, 'keys normalize: trim + lowercase');
164
+ const n1 = normalizeTierKey(' City A ', 'k');
165
+ const n2 = normalizeTierKey('city a', 'k');
166
+ check(n1.ok && n2.ok && n1.value === n2.value, 'whitespace collapse + case folding are deterministic and agree');
167
+ const caseHit = resolveTier({ cityKey: 'CITY-A', scenarioKey: ' Scenario-X ' }, { registry: probeRegistry, triggerFired: true });
168
+ check(caseHit.ok && caseHit.value.status === 'hit', 'a case/whitespace variant of a known key still hits (normalization, not fuzzy matching)');
169
+ expectRefusal(resolveTier({ cityKey: ' ', scenarioKey: 'scenario-x' }, { registry: probeRegistry, triggerFired: true }), 'bad_key', 'must not be blank', 'a blank city key is a caller error, not a silent neutral');
170
+ expectRefusal(resolveTier({ cityKey: 42, scenarioKey: 'scenario-x' }, { registry: probeRegistry, triggerFired: true }), 'bad_key', 'must be a string', 'a non-string key is a caller error');
171
+ // ---------------------------------------------------------------------------
172
+ // E. Ranking-only falsification
173
+ // ---------------------------------------------------------------------------
174
+ console.log('E. FALSIFICATION: a modifier may reorder, it can NEVER remove a candidate');
175
+ const candidates = [
176
+ { id: 'cand-boosted', cityKey: 'city-a', semantic: 0.50 },
177
+ { id: 'cand-plain-1', cityKey: 'city-b', semantic: 0.54 },
178
+ { id: 'cand-plain-2', cityKey: 'city-c', semantic: 0.10 },
179
+ { id: 'cand-zero', cityKey: 'city-a', semantic: 0 },
180
+ ];
181
+ const neutralRank = applyTierRanking(candidates, { scenarioKey: 'scenario-x' });
182
+ check(neutralRank.ok && neutralRank.value.length === candidates.length, 'default (trigger off): all candidates survive');
183
+ check(neutralRank.ok && neutralRank.value.map(r => r.id).join(',') === 'cand-plain-1,cand-boosted,cand-plain-2,cand-zero', 'default ordering is pure semantic order (every modifier neutral)');
184
+ check(neutralRank.ok && neutralRank.value.every(r => r.modifier_ppm === TIER_MODIFIER_PPM_NEUTRAL && r.tier_id === null), 'default: every row carries the neutral modifier and no tier id');
185
+ check(neutralRank.ok && tierRankingFilterViolation(candidates, neutralRank.value) === null, 'the mechanical no-hard-filter check passes for the default ranking');
186
+ const boostedRank = applyTierRanking(candidates, { scenarioKey: 'scenario-x' }, { registry: probeRegistry, triggerFired: true });
187
+ check(boostedRank.ok && boostedRank.value.map(r => r.id).join(',') === 'cand-boosted,cand-plain-1,cand-plain-2,cand-zero', 'a ×1.1 tier REORDERS: 0.50×1.1 = 0.55 overtakes the plain 0.54');
188
+ check(boostedRank.ok && boostedRank.value.length === candidates.length, 'reordering preserves the candidate count exactly');
189
+ check(boostedRank.ok && tierRankingFilterViolation(candidates, boostedRank.value) === null, 'the mechanical no-hard-filter check passes for the reordered ranking');
190
+ check(boostedRank.ok && boostedRank.value.every(r => candidates.some(c => c.id === r.id && c.semantic === r.semantic)), 'every row preserves its original semantic score (the unmodified ranking is always recoverable)');
191
+ check(boostedRank.ok && (boostedRank.value.find(r => r.id === 'cand-boosted')?.explanation ?? '').includes('probe-tier-1'), 'the applied bias is disclosed per candidate (ranking-disclosure precedent)');
192
+ /** The most hostile admissible tier: the lower bound applied to a zero semantic score. */
193
+ const minTier = [admittedProbe({ tier_id: 'probe-min', modifier_ppm: TIER_MODIFIER_PPM_MIN })];
194
+ const minRank = applyTierRanking(candidates, { scenarioKey: 'scenario-x' }, { registry: minTier, triggerFired: true });
195
+ check(minRank.ok && minRank.value.length === candidates.length, 'FALSIFICATION: even the lower-bound modifier removes nobody (count unchanged)');
196
+ check(minRank.ok && minRank.value.some(r => r.id === 'cand-zero' && r.score === 0), 'FALSIFICATION: a zero-semantic candidate scores 0 and is still PRESENT (score 0 ≠ removal)');
197
+ check(minRank.ok && tierRankingFilterViolation(candidates, minRank.value) === null, 'FALSIFICATION: the no-hard-filter check passes at the hostile bound');
198
+ check(minRank.ok && !Object.keys(minRank.value[0]).some(k => /exclud|filter|drop|reject|threshold/i.test(k)), 'the ranked row shape exposes no exclude/filter/drop/threshold field (no filter surface exists)');
199
+ let everPresent = true;
200
+ for (let semantic = 0; semantic <= 10; semantic++) {
201
+ for (const registry of [[], probeRegistry, minTier]) {
202
+ const probe = applyTierRanking([{ id: 'solo', cityKey: 'city-a', semantic: semantic / 10 }], { scenarioKey: 'scenario-x' }, { registry, triggerFired: true });
203
+ if (!probe.ok || probe.value.length !== 1 || probe.value[0].id !== 'solo')
204
+ everPresent = false;
205
+ }
206
+ }
207
+ check(everPresent, 'FALSIFICATION battery: across 11 semantic scores × 3 registries the candidate is never dropped');
208
+ expectRefusal(applyTierRanking([{ id: '', cityKey: 'city-a', semantic: 1 }], { scenarioKey: 'scenario-x' }), 'bad_candidate', 'non-empty id', 'a blank candidate id refused');
209
+ expectRefusal(applyTierRanking([{ id: 'a', cityKey: 'city-a', semantic: Number.NaN }], { scenarioKey: 'scenario-x' }), 'bad_candidate', 'must be finite', 'a non-finite semantic score refused');
210
+ expectRefusal(applyTierRanking([{ id: 'a', cityKey: 'city-a', semantic: -0.5 }], { scenarioKey: 'scenario-x' }), 'bad_candidate', 'would invert every modifier', 'a NEGATIVE semantic score is refused: the contract is multiplicative, so a boost would demote it');
211
+ expectRefusal(applyTierRanking([{ id: 'ok', cityKey: 'city-b', semantic: 0.5 }, { id: 'neg', cityKey: 'city-a', semantic: -1 }], { scenarioKey: 'scenario-x' }, { registry: probeRegistry, triggerFired: true }), 'bad_candidate', 'must be non-negative', 'one negative candidate refuses the whole ranking (no partial ordering over inverted semantics)');
212
+ check(applyTierRanking([{ id: 'zero', cityKey: 'city-a', semantic: 0 }], { scenarioKey: 'scenario-x' }, { registry: probeRegistry, triggerFired: true }).ok, 'zero is admissible (it is the boundary, not a negative)');
213
+ check(applyTierRanking([{ id: 'negzero', cityKey: 'city-a', semantic: -0 }], { scenarioKey: 'scenario-x' }).ok, 'negative zero is admissible (it equals zero, not below it)');
214
+ expectRefusal(applyTierRanking([{ id: 'a', cityKey: 'city-a', semantic: 1 }, { id: 'a', cityKey: 'city-b', semantic: 2 }], { scenarioKey: 'scenario-x' }), 'bad_candidate', 'duplicate candidate id', 'duplicate candidate ids refused (ranking inputs are a set)');
215
+ check(tierRankingFilterViolation(candidates, []) !== null, 'the mechanical check CATCHES a dropped-candidate violation (the check itself is falsifiable)');
216
+ check(tierRankingFilterViolation(candidates, (neutralRank.ok ? neutralRank.value : []).slice(0, 3)) !== null, 'the mechanical check catches a partial drop');
217
+ // ---------------------------------------------------------------------------
218
+ // F. Determinism + versioning
219
+ // ---------------------------------------------------------------------------
220
+ console.log('F. determinism, stable ties, defective registry refused');
221
+ const runA = applyTierRanking(candidates, { scenarioKey: 'scenario-x' }, { registry: probeRegistry, triggerFired: true });
222
+ const runB = applyTierRanking(candidates, { scenarioKey: 'scenario-x' }, { registry: probeRegistry, triggerFired: true });
223
+ check(runA.ok && runB.ok && JSON.stringify(runA.value) === JSON.stringify(runB.value), 'identical inputs produce byte-identical output (deterministic)');
224
+ const ties = [
225
+ { id: 'tie-1', cityKey: 'city-b', semantic: 0.4 },
226
+ { id: 'tie-2', cityKey: 'city-c', semantic: 0.4 },
227
+ { id: 'tie-3', cityKey: 'city-d', semantic: 0.4 },
228
+ ];
229
+ const tieRank = applyTierRanking(ties, { scenarioKey: 'scenario-x' }, { registry: probeRegistry, triggerFired: true });
230
+ check(tieRank.ok && tieRank.value.map(r => r.id).join(',') === 'tie-1,tie-2,tie-3', 'ties keep declaration order (stable sort)');
231
+ const defective = [{ ...admittedProbe(), modifier_ppm: 5_000_000 }];
232
+ expectRefusal(resolveTier({ cityKey: 'city-a', scenarioKey: 'scenario-x' }, { registry: defective, triggerFired: true }), 'modifier_bounds', 'never clamped silently', 'a defective registry entry is refused at read time, never clamped silently');
233
+ expectRefusal(applyTierRanking(candidates, { scenarioKey: 'scenario-x' }, { registry: defective, triggerFired: true }), 'modifier_bounds', 'defective registry entry', 'ranking propagates the defective-registry refusal instead of degrading');
234
+ console.log(`\nCITY-SCENARIO TIER MECHANISM TESTS (#339, simulated_trigger_drill / fixture_contract; registry stays EMPTY): ${passed} pass${process.exitCode ? ', FAIL' : ' (all green)'}`);