@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24

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 (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -1,104 +1,104 @@
1
+ /**
2
+ * 统一行程模型 TS 版(与 py/gotry_feasibility/unified.py 对齐;Python 为 oracle)。
3
+ * 行程 = Segment 序列;选择单元是 SegmentOption(目的地或具体班次)。
4
+ * 本文件实现:类型 + 双旧输入适配 + 航班链形态求解(Z3)。
5
+ * 候选形态 TS 求解(枚举过滤)与 engine 等价对账在下一迁移段。
6
+ */
1
7
  import { checkConnectivity } from '../scripts/skeleton-check.js';
2
- import { evaluateChoice, minToHhmm, hhmmToMin, requiredUsableHours, trueCostToDict, LATEST_ARRIVE_STAY_MIN } from './model.js';
8
+ import { evaluateChoice, minToHhmm, hhmmToMin, requiredUsableHours, trueCostToDict, LATEST_ARRIVE_STAY_MIN, doorToDoorFromMove, NonpositiveDurationError } from './model.js';
3
9
  import { withZ3 } from './z3-shared.js';
4
10
  import { t as i18nT } from './i18n.js';
11
+ import { resolveOffsetForLocalDate, describeResolveFailure, isKnownZoneLoose, readWallParts, wallPartsToIso } from './tz-resolver.js';
12
+ import { FLIGHT_PACK_VERSION } from './flight-pack-contract.js';
13
+ // Z3 运行时收敛到 z3-shared(单一 WASM 实例 + 单一 Context + 会话级互斥)——
14
+ // 此前模块级 z3Promise 三份并存是 WASM race/OOM 的根因(见 z3-shared.ts 头注)。
15
+ /* eslint-disable @typescript-eslint/no-explicit-any */
16
+ /** 航班数据包(data/flights_2026.json 形态)→ 段链 */
5
17
  const SEGMENT_ROUTES = {
6
- f1: 'HKG->HKT',
7
- f2: 'HKT->BKK',
8
- f3: 'BKK->KMG',
9
- f4: 'KMG->SZX',
10
- f5: 'SZX->DXB',
11
- yn1: 'SZX->LJG',
12
- yn2: 'LJG->DLU',
13
- yn3: 'DLU->LJG',
14
- yn4: 'LJG->SZX'
18
+ f1: 'HKG->HKT', f2: 'HKT->BKK', f3: 'BKK->KMG', f4: 'KMG->SZX', f5: 'SZX->DXB',
19
+ yn1: 'SZX->LJG', yn2: 'LJG->DLU', yn3: 'DLU->LJG', yn4: 'LJG->SZX',
15
20
  };
16
21
  function routeHint(id) {
17
22
  return SEGMENT_ROUTES[id];
18
23
  }
24
+ // ---- 适配器:旧候选用例(洱海形态:request + candidates)→ 单 choice 段 -------------
25
+ // 与 py unified.py segments_from_candidate 逐行对齐
19
26
  export function segmentsFromCandidate(req, candidates) {
20
27
  const motHard = req.motivation;
21
28
  const wakeFloor = motHard.wakeFloorMin;
22
- const options = candidates.map((c)=>({
23
- id: c.id,
24
- label: c.name,
25
- score: c.imageryMatch,
26
- bestMonths: c.bestMonths,
27
- minDays: c.minDaysForPurpose,
28
- move: {
29
- hub: c.hub,
30
- services: [
31
- ...c.servicesOut
32
- ],
33
- retServices: [
34
- ...c.servicesRet
35
- ],
36
- transfers: [
37
- ...c.destTransfers
38
- ],
39
- bufferMin: c.bufferOutMin,
40
- bufferRetMin: c.bufferRetMin,
41
- originTransferMin: 0,
42
- destTransferMin: 0
43
- },
44
- stay: {
45
- nights: req.windowDays - 1,
46
- stayCnyPerNight: c.stayCnyPerNight,
47
- localDailyCny: c.localDailyCny
48
- }
49
- }));
29
+ const options = candidates.map(c => ({
30
+ id: c.id,
31
+ label: c.name,
32
+ date: c.date,
33
+ score: c.imageryMatch,
34
+ bestMonths: c.bestMonths,
35
+ minDays: c.minDaysForPurpose,
36
+ move: {
37
+ hub: c.hub,
38
+ services: [...c.servicesOut],
39
+ retServices: [...c.servicesRet],
40
+ transfers: [...c.destTransfers],
41
+ bufferMin: c.bufferOutMin,
42
+ bufferRetMin: c.bufferRetMin,
43
+ originTransferMin: 0, // 候选形态无独立接驳(并入 destTransfers)
44
+ destTransferMin: 0,
45
+ },
46
+ stay: {
47
+ nights: req.windowDays - 1,
48
+ stayCnyPerNight: c.stayCnyPerNight,
49
+ localDailyCny: c.localDailyCny,
50
+ },
51
+ }));
50
52
  const escape = req.motivation.weights['escape_rest'] ?? 0;
51
53
  return {
52
54
  note: req.note,
53
- segments: [
54
- {
55
+ segments: [{
55
56
  id: 'dest',
56
57
  role: 'choice',
57
58
  note: '目的地选择',
58
- anchors: {
59
- wakeFloorMin: wakeFloor
60
- },
61
- options
62
- }
63
- ],
59
+ anchors: { wakeFloorMin: wakeFloor },
60
+ options,
61
+ }],
64
62
  budgetCny: req.budgetCny,
65
- defaultWakeFloorMin: wakeFloor
63
+ defaultWakeFloorMin: wakeFloor,
66
64
  };
67
65
  }
68
66
  export function parseFlightPackToSpec(pack) {
69
- const legs = pack['legs'].map((l)=>({
70
- id: String(l['id']),
71
- role: l['services'].length === 1 ? 'fixed' : 'choice',
72
- note: l['note'] ? String(l['note']) : undefined,
73
- date: l['date'] ? String(l['date']) : undefined,
74
- anchors: {
75
- arriveByMin: l['arrive_by'] ? hhmmToMin(String(l['arrive_by'])) : undefined
67
+ // Issue #343 v2:IANA path;v1(无 version 字段或 version===1)走原数值口径。
68
+ const version = pack['version'] === undefined ? 1 : Number(pack['version']);
69
+ if (version === 2)
70
+ return parseFlightPackToSpecV2(pack);
71
+ if (version !== 1) {
72
+ throw Object.assign(new Error(`parseFlightPackToSpec: unsupported pack version ${version}`), { code: 'unsupported_pack_version' });
73
+ }
74
+ return parseFlightPackToSpecV1(pack);
75
+ }
76
+ /** v1 数值口径(path 保留为 export,便于 §7-1/§6 直接复用与差分) */
77
+ export function parseFlightPackToSpecV1(pack) {
78
+ const legs = pack['legs'].map(l => ({
79
+ id: String(l['id']),
80
+ role: (l['services'].length === 1 ? 'fixed' : 'choice'),
81
+ note: l['note'] ? String(l['note']) : undefined,
82
+ date: l['date'] ? String(l['date']) : undefined,
83
+ anchors: { arriveByMin: l['arrive_by'] ? hhmmToMin(String(l['arrive_by'])) : undefined },
84
+ route: routeHint(l['id']),
85
+ options: l['services'].map(sv => ({
86
+ id: String(sv['id']),
87
+ label: `${String(sv['id'])} ${String(l['note'] ?? '').slice(0, 18)}`,
88
+ move: {
89
+ hub: String(l['hub'] ?? ''),
90
+ services: [{ id: String(sv['id']), depMin: hhmmToMin(String(sv['dep'])), arrMin: hhmmToMin(String(sv['arr'])), priceCny: Number(sv['price_cny']) }],
91
+ bufferMin: Number(l['buffer_min']),
92
+ originTransferMin: Number(l['origin_transfer_min']),
93
+ destTransferMin: Number(l['dest_transfer_min']),
94
+ redEye: Boolean(l['red_eye']),
95
+ redEyeDurationMin: Number(l['red_eye_duration_min'] ?? 0),
96
+ tzOffsetMin: Number(l['tz_offset_min'] ?? 0),
97
+ originTzOffsetMin: Number(l['origin_tz_offset_min'] ?? 480),
76
98
  },
77
- route: routeHint(l['id']),
78
- options: l['services'].map((sv)=>({
79
- id: String(sv['id']),
80
- label: `${String(sv['id'])} ${String(l['note'] ?? '').slice(0, 18)}`,
81
- move: {
82
- hub: String(l['hub'] ?? ''),
83
- services: [
84
- {
85
- id: String(sv['id']),
86
- depMin: hhmmToMin(String(sv['dep'])),
87
- arrMin: hhmmToMin(String(sv['arr'])),
88
- priceCny: Number(sv['price_cny'])
89
- }
90
- ],
91
- bufferMin: Number(l['buffer_min']),
92
- originTransferMin: Number(l['origin_transfer_min']),
93
- destTransferMin: Number(l['dest_transfer_min']),
94
- redEye: Boolean(l['red_eye']),
95
- redEyeDurationMin: Number(l['red_eye_duration_min'] ?? 0),
96
- tzOffsetMin: Number(l['tz_offset_min'] ?? 0),
97
- originTzOffsetMin: Number(l['origin_tz_offset_min'] ?? 480)
98
- }
99
- }))
100
- }));
101
- const meta = pack['meta'] ?? {};
99
+ })),
100
+ }));
101
+ const meta = (pack['meta'] ?? {});
102
102
  const ww = meta['work_window'];
103
103
  const spec = {
104
104
  segments: legs,
@@ -106,87 +106,262 @@ export function parseFlightPackToSpec(pack) {
106
106
  homeTzOffsetMin: Number(ww['home_tz_offset_min']),
107
107
  startMin: Number(ww['start_min']),
108
108
  endMin: Number(ww['end_min']),
109
- workdays: ww['workdays'] ?? [
110
- 0,
111
- 1,
112
- 2,
113
- 3,
114
- 4
115
- ]
116
- } : undefined
109
+ workdays: ww['workdays'] ?? [0, 1, 2, 3, 4],
110
+ } : undefined,
111
+ [FLIGHT_PACK_VERSION]: 1,
117
112
  };
118
- for (const l of pack['legs']){
119
- const seg = spec.segments.find((s)=>s.id === l['id']);
120
- for (const svc of l['services']){
113
+ // M-1:班次的星期标注挂到 Option(缺省=不受工作窗口约束)
114
+ for (const l of pack['legs']) {
115
+ const seg = spec.segments.find(s => s.id === l['id']);
116
+ for (const svc of l['services']) {
121
117
  if (svc['weekday']) {
122
- const opt = seg.options.find((o)=>o.id === svc['id']);
123
- if (opt) opt.depWeekday = String(svc['weekday']);
118
+ const opt = seg.options.find(o => o.id === svc['id']);
119
+ if (opt)
120
+ opt.depWeekday = String(svc['weekday']);
124
121
  }
125
122
  }
126
123
  }
127
124
  return spec;
128
125
  }
129
- const WEEKDAY_IDX = {
130
- mon: 0,
131
- tue: 1,
132
- wed: 2,
133
- thu: 3,
134
- fri: 4,
135
- sat: 5,
136
- sun: 6
137
- };
138
- const WEEKDAY_CN = {
139
- mon: '一',
140
- tue: '',
141
- wed: '',
142
- thu: '四',
143
- fri: '',
144
- sat: '六',
145
- sun: ''
146
- };
126
+ /** Issue #343 v2:IANA zone + 当地日期解析;数值 tz_* 字段不可用(混合形态解析时拒收)。 */
127
+ export function parseFlightPackToSpecV2(pack) {
128
+ const legsRaw = pack['legs'];
129
+ if (!Array.isArray(legsRaw) || legsRaw.length === 0) {
130
+ throw Object.assign(new Error('parseFlightPackToSpecV2: legs missing/empty'), { code: 'v2_legs_missing' });
131
+ }
132
+ const meta = (pack['meta'] ?? {});
133
+ const ww = meta['work_window'];
134
+ const legs = [];
135
+ for (const l of legsRaw) {
136
+ // v2 不允许混合数值 tz_offset_min/origin_tz_offset_min(早 fail)
137
+ if ('tz_offset_min' in l || 'origin_tz_offset_min' in l) {
138
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: leg ${l['id']} has legacy tz_offset_min/origin_tz_offset_min;v2 pack must use iana_*_zone + *_local_date`), { code: 'v2_legacy_offset_mixed' });
139
+ }
140
+ const legDate = l['date'] ? String(l['date']) : '';
141
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(legDate)) {
142
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: leg ${l['id']} requires ISO date`), { code: 'v2_leg_date_missing' });
143
+ }
144
+ const ianaDepZone = String(l['iana_dep_zone'] ?? '');
145
+ const ianaArrZone = String(l['iana_arr_zone'] ?? '');
146
+ if (!ianaDepZone || !ianaArrZone) {
147
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: leg ${l['id']} needs iana_dep_zone and iana_arr_zone`), { code: 'v2_zone_missing' });
148
+ }
149
+ const options = l['services'].map(sv => {
150
+ const svcDepDate = sv['dep_local_date'] ? String(sv['dep_local_date']) : legDate;
151
+ const svcArrDate = sv['arr_local_date'] ? String(sv['arr_local_date']) : legDate;
152
+ const depZone = sv['iana_dep_zone'] ? String(sv['iana_dep_zone']) : ianaDepZone;
153
+ const arrZone = sv['iana_arr_zone'] ? String(sv['iana_arr_zone']) : ianaArrZone;
154
+ const depR = resolveOffsetForLocalDate(depZone, svcDepDate, String(sv['dep']));
155
+ if (!depR.ok) {
156
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: dep zone/date failure: ${describeResolveFailure(depR)}`), { code: 'v2_dep_resolve_failed' });
157
+ }
158
+ const arrR = resolveOffsetForLocalDate(arrZone, svcArrDate, String(sv['arr']));
159
+ if (!arrR.ok) {
160
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: arr zone/date failure: ${describeResolveFailure(arrR)}`), { code: 'v2_arr_resolve_failed' });
161
+ }
162
+ // Issue #343 root 复核:d2d canonical 输入必须在 parse 边界校验严格 chronologically after,
163
+ // 否则把病态数据喂给 model.doorToDoorFromMove → throw NonpositiveDurationError → 被
164
+ // solveUnified WASM guard 误报为 wasm_runtime_error。
165
+ if (arrR.utcMs <= depR.utcMs) {
166
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: service ${String(sv['id'])} arrival instant (${new Date(arrR.utcMs).toISOString()}) must be strictly after departure instant (${new Date(depR.utcMs).toISOString()})`), { code: 'v2_arr_not_after_dep' });
167
+ }
168
+ // tzOffsetMin = arr - dep(positive = 抵达地更靠东)
169
+ const tzOffsetMin = arrR.offsetMin - depR.offsetMin;
170
+ return {
171
+ id: String(sv['id']),
172
+ label: `${String(sv['id'])} ${String(l['note'] ?? '').slice(0, 18)}`,
173
+ date: svcDepDate,
174
+ move: {
175
+ hub: String(l['hub'] ?? ''),
176
+ services: [{
177
+ id: String(sv['id']),
178
+ depMin: hhmmToMin(String(sv['dep'])),
179
+ arrMin: hhmmToMin(String(sv['arr'])),
180
+ priceCny: Number(sv['price_cny']),
181
+ // Issue #343 v2:把"墙上时刻"投影成真实 UTC instant 留给 evaluate / workWindow 用;
182
+ // d2d 公式走 (arrUtcMs - depUtcMs)/60000,跨日 / DST / 反向日界线都正确。
183
+ depUtcMs: depR.utcMs,
184
+ arrUtcMs: arrR.utcMs,
185
+ }],
186
+ bufferMin: Number(l['buffer_min']),
187
+ originTransferMin: Number(l['origin_transfer_min']),
188
+ destTransferMin: Number(l['dest_transfer_min']),
189
+ redEye: Boolean(l['red_eye']),
190
+ redEyeDurationMin: Number(sv['red_eye_duration_min'] ?? l['red_eye_duration_min'] ?? 0),
191
+ groundRecoveryMin: l['ground_recovery_min'] ? Number(l['ground_recovery_min']) : undefined,
192
+ tzOffsetMin,
193
+ originTzOffsetMin: depR.offsetMin,
194
+ ianaDepZone: depZone,
195
+ ianaArrZone: arrZone,
196
+ },
197
+ depWeekday: weekdayFromYmd(svcDepDate),
198
+ };
199
+ });
200
+ const role = (l['services'].length === 1 ? 'fixed' : 'choice');
201
+ legs.push({
202
+ id: String(l['id']),
203
+ role,
204
+ note: l['note'] ? String(l['note']) : undefined,
205
+ date: legDate,
206
+ anchors: { arriveByMin: l['arrive_by'] ? hhmmToMin(String(l['arrive_by'])) : undefined },
207
+ route: routeHint(l['id']),
208
+ options,
209
+ });
210
+ }
211
+ // work_window:home_zone 必需(v2 的 home 偏移走 IANA,数值字段不再权威);同时给两值报 reject。
212
+ let workWindow;
213
+ if (ww) {
214
+ const hasHomeZone = Boolean(ww['home_zone']);
215
+ const hasHomeNum = ww['home_tz_offset_min'] !== undefined;
216
+ if (hasHomeZone && hasHomeNum) {
217
+ throw Object.assign(new Error('parseFlightPackToSpecV2: work_window cannot combine home_zone and home_tz_offset_min'), { code: 'v2_ww_conflict' });
218
+ }
219
+ if (!hasHomeZone) {
220
+ throw Object.assign(new Error('parseFlightPackToSpecV2: work_window.home_zone is required for v2'), { code: 'v2_ww_home_zone_required' });
221
+ }
222
+ // 在 v2 parse 边界即时验证 home_zone 是已知 IANA zone(root counterexample #3:不许 fail-open)。
223
+ // 用 tz-resolver 的 isKnownZoneLoose:接受 Intl.DateTimeFormat 能识别的合法 alias(US/Pacific / ROK / Etc/GMT+8)。
224
+ const homeZone = String(ww['home_zone']);
225
+ if (!isKnownZoneLoose(homeZone)) {
226
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: work_window.home_zone '${homeZone}' is not a known IANA zone`), { code: 'v2_home_zone_unknown' });
227
+ }
228
+ const startHHmm = ww['start'] ? String(ww['start']) : undefined;
229
+ const endHHmm = ww['end'] ? String(ww['end']) : undefined;
230
+ const startMin = ww['start_min'] !== undefined ? Number(ww['start_min']) : (startHHmm ? hhmmToMin(startHHmm) : hhmmToMin('10:00'));
231
+ const endMin = ww['end_min'] !== undefined ? Number(ww['end_min']) : (endHHmm ? hhmmToMin(endHHmm) : hhmmToMin('19:00'));
232
+ // numeric homeTzOffsetMin 仅占位:在 v2 工作窗口过滤时按 home_zone + dep_date 重算;预填 leg-date 占位 0
233
+ workWindow = {
234
+ homeTzOffsetMin: 0,
235
+ startMin,
236
+ endMin,
237
+ workdays: ww['workdays'] ?? [0, 1, 2, 3, 4],
238
+ homeZone,
239
+ startHHmm,
240
+ endHHmm,
241
+ };
242
+ }
243
+ return { segments: legs, workWindow, [FLIGHT_PACK_VERSION]: 2 };
244
+ }
245
+ const WEEKDAY_IDX = { mon: 0, tue: 1, wed: 2, thu: 3, fri: 4, sat: 5, sun: 6 };
246
+ const WEEKDAY_CN = { mon: '一', tue: '二', wed: '三', thu: '四', fri: '五', sat: '六', sun: '日' };
247
+ /** YYYY-MM-DD → mon/tue/wed/thu/fri/sat/sun;v2 路径给 option 标 depWeekday 用。 */
248
+ export function weekdayFromYmd(ymd) {
249
+ const m = ymd.match(/^(\d{4})-(\d{2})-(\d{2})$/);
250
+ if (!m)
251
+ throw new Error(`weekdayFromYmd: bad ymd ${ymd}`);
252
+ const dow = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))).getUTCDay();
253
+ return ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][dow];
254
+ }
255
+ /** M-1:工作日的工作窗口内起飞 → 排除理由;否则 null(与 py _work_window_blocks 对齐)。
256
+ *
257
+ * Issue #343 v2:home zone 存在时,必须把"出发真实 UTC instant"投影到 home zone
258
+ * 取该 zone 的 date/weekday/HH:MM(root counterexample #2:Tokyo 06:30 ≈ LA 14:30 prev day);
259
+ * 不再用 dep-local date 直接当 home date。
260
+ *
261
+ * root 评审复核:`depUtcMs` 是已知确定的 instant,直接 `readWallParts(homeZone, depUtcMs)`
262
+ * 取墙时分量 — 不要把 projected wall time 再喂给 `resolveOffsetForLocalDate` 重做 ±14h 扫描,
263
+ * 否则秋回退日 01:30 会被错误判为 overlap_ambiguous 而 fail-open(已知 instant 永远唯一)。
264
+ *
265
+ * dep/arr 的"墙上本地输入"(leg-level `dep`/`arr` 字段)由 parseFlightPackToSpecV2 在
266
+ * 解析时已用 `resolveOffsetForLocalDate` 拒过 gap/overlap,所以 leg 进 solver 时
267
+ * 两个 instant 一定 strict-after 且偏移已知。
268
+ *
269
+ * home zone 未知时由 v2 parse 边界直接抛 v2_home_zone_unknown 拒绝;
270
+ * 这里兜底只在 v1 legacy 路径出现。
271
+ */
147
272
  function workWindowBlocks(spec, option) {
148
273
  const ww = spec.workWindow;
149
- if (!ww || !option.move || !option.depWeekday) return null;
150
- if (!(ww.workdays ?? [
151
- 0,
152
- 1,
153
- 2,
154
- 3,
155
- 4
156
- ]).includes(WEEKDAY_IDX[option.depWeekday])) return null;
157
- const dep = option.move.services[0].depMin;
158
- const shift = (option.move.originTzOffsetMin ?? 480) - ww.homeTzOffsetMin;
159
- const start = ww.startMin + shift, end = ww.endMin + shift;
160
- if (start <= dep && dep <= end) {
161
- const hhmm = (m)=>`${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
274
+ if (!ww || !option.move || !option.depWeekday)
275
+ return null;
276
+ const svc = option.move.services[0];
277
+ const dep = svc.depMin;
278
+ const homeZone = ww.homeZone;
279
+ let homeOffsetMin = ww.homeTzOffsetMin;
280
+ let homeDepMin;
281
+ let homeDepWeekday;
282
+ if (homeZone) {
283
+ if (typeof svc.depUtcMs !== 'number') {
284
+ // v2 路径必然填了 depUtcMs;没填 = 数据病,按 red flag 排除该 option。
285
+ return i18nT('un.workwindow_unresolvable_reason', {
286
+ reason: `home zone ${homeZone} requires UTC dep instant (v2 path bug)`,
287
+ });
288
+ }
289
+ // 已知 instant 直接投影到 home zone 的墙时 — 无歧义,不必也不应重走 ±14h 扫描。
290
+ const homeParts = readWallParts(homeZone, svc.depUtcMs);
291
+ const homeIso = wallPartsToIso(homeParts);
292
+ homeDepMin = hhmmToMin(homeIso.hhmm);
293
+ homeDepWeekday = weekdayFromYmd(homeIso.ymd);
294
+ }
295
+ const effectiveDep = homeDepMin ?? dep;
296
+ const effectiveWeekday = homeDepWeekday ?? option.depWeekday;
297
+ if (!((ww.workdays ?? [0, 1, 2, 3, 4]).includes(WEEKDAY_IDX[effectiveWeekday])))
298
+ return null;
299
+ // v2:startMin/endMin 与 homeDepMin 都是 home-zone 墙时分量(已 IANA 归一),
300
+ // 直接比较;不再做 originTzOffsetMin 偏移(root 复核该 shift 是 v1 数值残留,会让窗口错位)。
301
+ // v1 路径(无 homeZone, 走数值 homeTzOffsetMin)保留 shift 兼容旧 spec。
302
+ let start, end;
303
+ if (homeZone) {
304
+ start = ww.startMin;
305
+ end = ww.endMin;
306
+ }
307
+ else {
308
+ const shift = (option.move.originTzOffsetMin ?? 480) - homeOffsetMin;
309
+ start = ww.startMin + shift;
310
+ end = ww.endMin + shift;
311
+ }
312
+ if (start <= effectiveDep && effectiveDep <= end) {
313
+ const hhmm = (m) => `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
162
314
  return i18nT('un.workwindow_reason', {
163
- wd: WEEKDAY_CN[option.depWeekday] ?? '',
164
- dep: hhmm(dep),
165
- start: hhmm(start),
166
- end: hhmm(end)
315
+ wd: WEEKDAY_CN[effectiveWeekday] ?? '', dep: hhmm(effectiveDep), start: hhmm(start), end: hhmm(end),
167
316
  });
168
317
  }
169
318
  return null;
170
319
  }
320
+ /** D-5:时区感知的段核算(与 py unified._evaluate_option_move 对齐)。
321
+ *
322
+ * Issue #343 d2d 算术唯一权威入口 = model.ts 的 `doorToDoorFromMove`。
323
+ * v2 路径会传 Service.depUtcMs/arrUtcMs(由 parseFlightPackToSpecV2 用 tz-resolver 投出),
324
+ * 此时 d2d 用真实 UTC instant 差 — 跨日 / 反向日界线 / DST 切换都正确;v2 arr<=dep 抛
325
+ * NonpositiveDurationError → 该 option 当作不可行(由 solveUnifiedInner 转 unsat_core)。
326
+ * v1 数值口径无 UTC 字段,走老 (arrMin - depMin) - tzOffsetMin;字节不变。
327
+ */
171
328
  function evaluateOptionMove(segId, mv) {
172
329
  const svc = mv.services[0];
330
+ let d2dMin;
331
+ try {
332
+ d2dMin = doorToDoorFromMove(svc, mv).doorToDoorMin;
333
+ }
334
+ catch (e) {
335
+ if (e instanceof NonpositiveDurationError) {
336
+ // d2d 病态输入 → 把这条 option 抛红 flag;这里走结构化 reject 而非吞错。
337
+ throw Object.assign(new Error(`evaluateOptionMove ${segId}/${svc.id}: ${e.message}`), {
338
+ code: e.code,
339
+ kind: e.kind,
340
+ });
341
+ }
342
+ throw e;
343
+ }
173
344
  const wake = svc.depMin - mv.bufferMin - mv.originTransferMin;
174
- const trueFlight = svc.arrMin - svc.depMin - (mv.tzOffsetMin ?? 0);
175
- const d2d = mv.bufferMin + mv.originTransferMin + trueFlight + mv.destTransferMin;
176
345
  const wakeDisplay = wake < 0 ? `${minToHhmm(wake + 1440)}(前一日)` : minToHhmm(wake);
177
346
  const arriveStay = svc.arrMin + mv.destTransferMin;
178
347
  let energy;
179
348
  if (mv.redEye && (mv.redEyeDurationMin ?? 0) > 0) {
180
349
  const sleepH = ((mv.redEyeDurationMin ?? 0) - 60) / 60;
350
+ // D-6 校准:机上睡眠上限 75;落地接驳(destTransferMin)乘车补眠回血(1h≈+5%,上限 80)
181
351
  const groundH = (mv.groundRecoveryMin ?? mv.destTransferMin ?? 0) / 60;
182
352
  const recovery = Math.min(5, 5 * groundH);
183
353
  energy = Math.max(30, Math.min(75 + recovery, 80, 30 + 8 * sleepH + recovery));
184
- } else {
354
+ }
355
+ else {
185
356
  energy = 100 - 2 * 8;
186
- if (wake < 5 * 60) energy -= 30;
187
- else if (wake < 6 * 60) energy -= 25;
188
- if (arriveStay > 21 * 60) energy -= 10;
189
- if (d2d > 6 * 60) energy -= 10;
357
+ if (wake < 5 * 60)
358
+ energy -= 30;
359
+ else if (wake < 6 * 60)
360
+ energy -= 25;
361
+ if (arriveStay > 21 * 60)
362
+ energy -= 10;
363
+ if (d2dMin > 6 * 60)
364
+ energy -= 10;
190
365
  energy = Math.max(0, energy);
191
366
  }
192
367
  return {
@@ -195,157 +370,180 @@ function evaluateOptionMove(segId, mv) {
195
370
  wake: wakeDisplay,
196
371
  wakeMin: wake,
197
372
  arrive_stay: minToHhmm(arriveStay),
198
- door_to_door: `${Math.floor(d2d / 60)}h${String(d2d % 60).padStart(2, '0')}m`,
199
- d2d_min: d2d,
373
+ door_to_door: `${Math.floor(d2dMin / 60)}h${String(d2dMin % 60).padStart(2, '0')}m`,
374
+ d2d_min: d2dMin,
200
375
  energy_pct: Math.round(energy),
201
- price_cny: svc.priceCny
376
+ price_cny: svc.priceCny,
202
377
  };
203
378
  }
379
+ function releaseZ3(resource) {
380
+ if (typeof resource?.release === 'function')
381
+ resource.release();
382
+ }
383
+ /** 航班链形态求解:按 Option 选择,锚点命名约束,core 剥竖线(D-2 修复) */
204
384
  export async function solveUnified(spec) {
385
+ // WASM 防护:如果 z3-solver 加载或求解触发 memory access 错误,不让异常穿透到进程层把 dsh 杀掉。
386
+ // 候选形态走 solveChoiceSegment 不经过这里——这里是显式航班链路径,用户量较少。
205
387
  try {
206
388
  return await solveUnifiedInner(spec);
207
- } catch (e) {
389
+ }
390
+ catch (e) {
208
391
  console.error('[gotry] solveUnified failed (likely wasm thread race):', e.message?.slice(0, 200));
209
- return {
210
- feasible: false,
211
- unsat_core: [
212
- 'wasm_runtime_error'
213
- ],
214
- red_flags: [
215
- 'WASM 求解器异常,建议下次用候选形态(枚举)重试'
216
- ]
217
- };
392
+ return { feasible: false, unsat_core: ['wasm_runtime_error'], red_flags: ['WASM 求解器异常,建议下次用候选形态(枚举)重试'] };
218
393
  }
219
394
  }
220
395
  async function solveUnifiedInner(spec) {
396
+ // M-1:求解前的工作窗口确定性预过滤(与 py 对齐),排除理由入记录
397
+ // 骨架层(§7-1):三值语义标注——枢纽间否定只降权不排除(骨架滞后会错杀 EK329)
221
398
  const skeletonNotes = [];
222
399
  const exclusions = [];
223
- for (const seg of spec.segments){
400
+ for (const seg of spec.segments) {
224
401
  if (spec.skeletonHub) {
402
+ // 段级骨架查询:route 提示(如 "HKG->HKT")优先,否则跳过
225
403
  const route = seg.route;
226
404
  if (route) {
227
- const [a, b] = route.split('->').map((s)=>s.trim());
405
+ const [a, b] = route.split('->').map(s => s.trim());
228
406
  const verdict = await checkConnectivity(a, b);
229
407
  skeletonNotes.push(`${seg.id}: ${verdict.evidence}`);
230
408
  }
231
409
  }
232
- const kept = seg.options.filter((o)=>{
410
+ const kept = seg.options.filter(o => {
233
411
  const reason = workWindowBlocks(spec, o);
234
- if (reason) exclusions.push({
235
- segment: seg.id,
236
- option: o.id,
237
- reason
238
- });
412
+ if (reason)
413
+ exclusions.push({ segment: seg.id, option: o.id, reason });
239
414
  return !reason;
240
415
  });
241
416
  seg.options = kept;
242
417
  if (kept.length === 0) {
243
- return {
244
- feasible: false,
245
- unsat_core: [
246
- `${seg.id}:work_window`
247
- ],
248
- work_window_exclusions: exclusions
249
- };
418
+ return { feasible: false, unsat_core: [`${seg.id}:work_window`], work_window_exclusions: exclusions };
250
419
  }
251
420
  }
252
- return withZ3('unified.solveUnifiedInner', async (z3)=>{
421
+ // Z3 会话进互斥门(骨架/预过滤等网络段留在门外):solveUnifiedInner 的判定段
422
+ // 从 here 到 return 全部独占共享实例。
423
+ return withZ3('unified.solveUnifiedInner', async (z3) => {
253
424
  const { Bool, If, Int, Solver, Sum } = z3;
254
425
  const wakeFloor = spec.defaultWakeFloorMin ?? hhmmToMin('06:00');
255
426
  const allSels = {};
256
427
  const assertions = {};
257
428
  const svcOf = {};
258
- for (const seg of spec.segments){
259
- const sels = seg.options.map((_, i)=>Bool.const(`${seg.id}_o${i}`));
429
+ for (const seg of spec.segments) {
430
+ const sels = seg.options.map((_, i) => Bool.const(`${seg.id}_o${i}`));
260
431
  allSels[seg.id] = sels;
261
- const svcs = seg.options.map((o)=>o.move.services[0]);
432
+ const svcs = seg.options.map(o => o.move.services[0]);
262
433
  svcOf[seg.id] = svcs;
263
- const pick = (attr)=>Sum(...sels.map((s, i)=>If(s, Int.val(svcs[i][attr]), Int.val(0))));
434
+ const pick = (attr) => Sum(...sels.map((s, i) => If(s, Int.val(svcs[i][attr]), Int.val(0))));
264
435
  const dep = pick('depMin'), arr = pick('arrMin');
265
436
  const o0 = seg.options[0].move;
266
437
  const wake = dep.sub(Int.val(o0.bufferMin + o0.originTransferMin));
267
438
  const arriveStay = arr.add(Int.val(o0.destTransferMin));
268
- if (seg.anchors?.arriveByMin !== undefined) assertions[`${seg.id}:arrive_by`] = arriveStay.le(Int.val(seg.anchors.arriveByMin));
269
- if (seg.anchors?.departAfterMin !== undefined) assertions[`${seg.id}:depart_after`] = dep.ge(Int.val(seg.anchors.departAfterMin));
270
- if (!seg.options.some((o)=>o.move?.redEye)) assertions[`${seg.id}:wake_floor`] = wake.ge(Int.val(wakeFloor));
439
+ if (seg.anchors?.arriveByMin !== undefined)
440
+ assertions[`${seg.id}:arrive_by`] = arriveStay.le(Int.val(seg.anchors.arriveByMin));
441
+ if (seg.anchors?.departAfterMin !== undefined)
442
+ assertions[`${seg.id}:depart_after`] = dep.ge(Int.val(seg.anchors.departAfterMin));
443
+ if (!seg.options.some(o => o.move?.redEye))
444
+ assertions[`${seg.id}:wake_floor`] = wake.ge(Int.val(wakeFloor));
271
445
  }
272
- const total = Sum(...spec.segments.flatMap((seg)=>allSels[seg.id].map((s, i)=>If(s, Int.val(svcOf[seg.id][i].priceCny), Int.val(0)))));
273
- if (spec.budgetCny !== undefined) assertions['total:budget'] = total.le(Int.val(spec.budgetCny));
274
- const exactlyOne = (sels)=>Sum(...sels.map((s)=>If(s, Int.val(1), Int.val(0)))).eq(Int.val(1));
275
- const coreOf = (s)=>{
446
+ const total = Sum(...spec.segments.flatMap(seg => allSels[seg.id].map((s, i) => If(s, Int.val(svcOf[seg.id][i].priceCny), Int.val(0)))));
447
+ if (spec.budgetCny !== undefined)
448
+ assertions['total:budget'] = total.le(Int.val(spec.budgetCny));
449
+ const exactlyOne = (sels) => Sum(...sels.map((s) => If(s, Int.val(1), Int.val(0)))).eq(Int.val(1));
450
+ const coreOf = (s) => {
276
451
  const v = s.unsatCore();
277
452
  const out = [];
278
- for(let i = 0; i < v.length(); i++)out.push(String(v.get(i)).replace(/^\|/, '').replace(/\|$/, ''));
453
+ for (let i = 0; i < v.length(); i++)
454
+ out.push(String(v.get(i)).replace(/^\|/, '').replace(/\|$/, ''));
279
455
  return out;
280
456
  };
281
457
  const s = new Solver();
282
- for (const sel of Object.values(allSels))s.add(exactlyOne(sel));
283
- for (const [name, expr] of Object.entries(assertions))s.addAndTrack(expr, name);
284
- const report = async (model)=>{
285
- const out = [];
286
- for (const seg of spec.segments){
287
- for(let i = 0; i < allSels[seg.id].length; i++){
288
- const v = await maybeAwait(model.eval(allSels[seg.id][i], true));
289
- if (String(v) !== 'true') continue;
290
- const o = seg.options[i];
291
- out.push({
292
- leg: seg.id,
293
- ...evaluateOptionMove(seg.id, o.move)
294
- });
295
- break;
296
- }
458
+ let sOpen = true;
459
+ const closeS = () => {
460
+ if (sOpen) {
461
+ releaseZ3(s);
462
+ sOpen = false;
297
463
  }
298
- return out;
299
464
  };
300
- async function maybeAwait(v) {
301
- return v instanceof Promise ? await v : v;
302
- }
303
- if (String(await s.check()) !== 'unsat') {
304
- const legs = await report(s.model());
305
- const money = legs.reduce((a, l)=>a + l.price_cny, 0);
306
- const redFlags = legs.filter((l)=>spec.segments.find((sg)=>sg.id === l.leg)?.options.some((o)=>o.move?.redEye) && l.energy_pct < 50).map((l)=>i18nT('un.redflag_redeye', {
307
- leg: l.leg,
308
- pct: l.energy_pct
309
- }));
310
- return {
311
- feasible: true,
312
- money_cny: money,
313
- legs,
314
- red_flags: redFlags,
315
- work_window_exclusions: exclusions,
316
- skeleton_notes: skeletonNotes.length ? skeletonNotes : undefined
465
+ try {
466
+ for (const sel of Object.values(allSels))
467
+ s.add(exactlyOne(sel));
468
+ for (const [name, expr] of Object.entries(assertions))
469
+ s.addAndTrack(expr, name);
470
+ const report = async (model) => {
471
+ const out = [];
472
+ for (const seg of spec.segments) {
473
+ for (let i = 0; i < allSels[seg.id].length; i++) {
474
+ const v = await maybeAwait(model.eval(allSels[seg.id][i], true));
475
+ if (String(v) !== 'true')
476
+ continue;
477
+ const o = seg.options[i];
478
+ out.push({ leg: seg.id, ...evaluateOptionMove(seg.id, o.move) });
479
+ break;
480
+ }
481
+ }
482
+ return out;
317
483
  };
318
- }
319
- const core = coreOf(s).sort();
320
- const suggestions = [];
321
- for (const name of core){
322
- const s2 = new Solver();
323
- for (const sel of Object.values(allSels))s2.add(exactlyOne(sel));
324
- for (const [n2, expr] of Object.entries(assertions)){
325
- if (n2 !== name) s2.addAndTrack(expr, n2);
484
+ async function maybeAwait(v) {
485
+ return v instanceof Promise ? await v : v;
326
486
  }
327
- if (String(await s2.check()) !== 'unsat') {
328
- const legs = await report(s2.model());
329
- suggestions.push({
330
- relax: name,
331
- money_cny: legs.reduce((a, l)=>a + l.price_cny, 0)
332
- });
487
+ if (String(await s.check()) !== 'unsat') {
488
+ const model = s.model();
489
+ try {
490
+ const legs = await report(model);
491
+ const money = legs.reduce((a, l) => a + l.price_cny, 0);
492
+ const redFlags = legs
493
+ .filter(l => spec.segments.find(sg => sg.id === l.leg)?.options.some(o => o.move?.redEye) && l.energy_pct < 50)
494
+ .map(l => i18nT('un.redflag_redeye', { leg: l.leg, pct: l.energy_pct }));
495
+ return { feasible: true, money_cny: money, legs, red_flags: redFlags, work_window_exclusions: exclusions, skeleton_notes: skeletonNotes.length ? skeletonNotes : undefined };
496
+ }
497
+ finally {
498
+ releaseZ3(model);
499
+ }
333
500
  }
501
+ const core = coreOf(s).sort();
502
+ const suggestions = [];
503
+ closeS();
504
+ for (const name of core) {
505
+ const s2 = new Solver();
506
+ try {
507
+ for (const sel of Object.values(allSels))
508
+ s2.add(exactlyOne(sel));
509
+ for (const [n2, expr] of Object.entries(assertions)) {
510
+ if (n2 !== name)
511
+ s2.addAndTrack(expr, n2);
512
+ }
513
+ if (String(await s2.check()) !== 'unsat') {
514
+ const model = s2.model();
515
+ try {
516
+ const legs = await report(model);
517
+ suggestions.push({ relax: name, money_cny: legs.reduce((a, l) => a + l.price_cny, 0) });
518
+ }
519
+ finally {
520
+ releaseZ3(model);
521
+ }
522
+ }
523
+ }
524
+ finally {
525
+ releaseZ3(s2);
526
+ }
527
+ }
528
+ return { feasible: false, unsat_core: core, suggestions };
529
+ }
530
+ finally {
531
+ closeS();
334
532
  }
335
- return {
336
- feasible: false,
337
- unsat_core: core,
338
- suggestions
339
- };
340
533
  });
341
534
  }
342
535
  function checkTrueCost(t, spec) {
343
536
  const fails = [];
344
- if (t.wakeMin < spec.wakeFloorMin) fails.push('wake_floor');
345
- if (t.energyArrivalPct < spec.minArrivalEnergyPct) fails.push('energy_floor');
346
- if (t.usableHours < spec.requiredUsableHours) fails.push('usable_hours');
347
- if (spec.budgetCny !== undefined && t.moneyCny > spec.budgetCny) fails.push('budget');
348
- if (t.arriveStayMin > spec.latestArriveStayMin) fails.push('arrival_before_evening');
537
+ if (t.wakeMin < spec.wakeFloorMin)
538
+ fails.push('wake_floor');
539
+ if (t.energyArrivalPct < spec.minArrivalEnergyPct)
540
+ fails.push('energy_floor');
541
+ if (t.usableHours < spec.requiredUsableHours)
542
+ fails.push('usable_hours');
543
+ if (spec.budgetCny !== undefined && t.moneyCny > spec.budgetCny)
544
+ fails.push('budget');
545
+ if (t.arriveStayMin > spec.latestArriveStayMin)
546
+ fails.push('arrival_before_evening');
349
547
  return fails;
350
548
  }
351
549
  function enumerateCombos(opt, cand, req, days) {
@@ -356,25 +554,15 @@ function enumerateCombos(opt, cand, req, days) {
356
554
  minArrivalEnergyPct: req.motivation.minArrivalEnergyPct,
357
555
  requiredUsableHours: requiredUsableHours(req.motivation),
358
556
  budgetCny: req.budgetCny,
359
- latestArriveStayMin: LATEST_ARRIVE_STAY_MIN
557
+ latestArriveStayMin: LATEST_ARRIVE_STAY_MIN,
360
558
  };
361
- for (const o of mv.services){
362
- for (const tr of mv.transfers ?? []){
363
- for (const r of mv.retServices ?? mv.services){
364
- for (const trr of mv.transfers ?? []){
365
- const ch = {
366
- outService: o,
367
- outTransfer: tr,
368
- retService: r,
369
- retTransfer: trr,
370
- days
371
- };
559
+ for (const o of mv.services) {
560
+ for (const tr of mv.transfers ?? []) {
561
+ for (const r of (mv.retServices ?? mv.services)) {
562
+ for (const trr of mv.transfers ?? []) {
563
+ const ch = { outService: o, outTransfer: tr, retService: r, retTransfer: trr, days };
372
564
  const t = evaluateChoice(cand, req, ch);
373
- out.push({
374
- choice: ch,
375
- cost: t,
376
- fails: checkTrueCost(t, spec)
377
- });
565
+ out.push({ choice: ch, cost: t, fails: checkTrueCost(t, spec) });
378
566
  }
379
567
  }
380
568
  }
@@ -384,208 +572,143 @@ function enumerateCombos(opt, cand, req, days) {
384
572
  function optionToCandidate(opt) {
385
573
  const mv = opt.move, st = opt.stay;
386
574
  return {
387
- id: opt.id,
388
- name: opt.label,
389
- hub: mv.hub,
390
- bufferOutMin: mv.bufferMin,
391
- bufferRetMin: mv.bufferRetMin ?? mv.bufferMin,
392
- servicesOut: [
393
- ...mv.services
394
- ],
395
- servicesRet: [
396
- ...mv.retServices ?? mv.services
397
- ],
398
- destTransfers: [
399
- ...mv.transfers ?? []
400
- ],
401
- stayCnyPerNight: st.stayCnyPerNight ?? 0,
402
- localDailyCny: st.localDailyCny ?? 0,
575
+ id: opt.id, name: opt.label, hub: mv.hub,
576
+ bufferOutMin: mv.bufferMin, bufferRetMin: mv.bufferRetMin ?? mv.bufferMin,
577
+ servicesOut: [...mv.services], servicesRet: [...(mv.retServices ?? mv.services)],
578
+ destTransfers: [...(mv.transfers ?? [])],
579
+ stayCnyPerNight: st.stayCnyPerNight ?? 0, localDailyCny: st.localDailyCny ?? 0,
403
580
  minDaysForPurpose: opt.minDays ?? 1,
404
- imageryMatch: opt.score ?? 0,
405
- bestMonths: opt.bestMonths ?? []
581
+ imageryMatch: opt.score ?? 0, bestMonths: opt.bestMonths ?? [],
406
582
  };
407
583
  }
584
+ /** 单 choice 段(目的地选项):逐 Option 枚举过滤,按 score 排序。与 py solve_choice_segment 等价。 */
408
585
  export function solveChoiceSegment(spec, req) {
409
- const seg = spec.segments.find((s)=>s.role === 'choice' && s.options.length > 0 && s.options[0].stay);
410
- if (!seg) throw new Error('solveChoiceSegment: no choice segment with stay found');
586
+ const seg = spec.segments.find(s => s.role === 'choice' && s.options.length > 0 && s.options[0].stay);
587
+ if (!seg)
588
+ throw new Error('solveChoiceSegment: no choice segment with stay found');
411
589
  const windowDays = req.windowDays;
412
590
  const verdicts = [];
413
- for (const opt of seg.options){
591
+ for (const opt of seg.options) {
414
592
  const cand = optionToCandidate(opt);
415
593
  const minDays = cand.minDaysForPurpose;
416
594
  const durationOk = windowDays >= minDays;
417
595
  const combos = durationOk ? enumerateCombos(opt, cand, req, windowDays) : [];
418
- const good = combos.filter((c)=>c.fails.length === 0);
596
+ const good = combos.filter(c => c.fails.length === 0);
419
597
  if (good.length > 0) {
420
- const best = good.reduce((a, b)=>a.cost.moneyCny <= b.cost.moneyCny ? a : b);
598
+ const best = good.reduce((a, b) => a.cost.moneyCny <= b.cost.moneyCny ? a : b);
421
599
  verdicts.push({
422
- candidate_id: opt.id,
423
- name: opt.label,
424
- feasible: true,
425
- imagery_match: opt.score ?? 0,
600
+ candidate_id: opt.id, name: opt.label, feasible: true, imagery_match: opt.score ?? 0,
426
601
  chosen: {
427
- out_service: best.choice.outService.id,
428
- out_transfer: best.choice.outTransfer.mode,
429
- ret_service: best.choice.retService.id,
430
- ret_transfer: best.choice.retTransfer.mode,
431
- days: best.choice.days
602
+ out_service: best.choice.outService.id, out_transfer: best.choice.outTransfer.mode,
603
+ ret_service: best.choice.retService.id, ret_transfer: best.choice.retTransfer.mode,
604
+ days: best.choice.days,
432
605
  },
433
- true_cost: trueCostToDict(best.cost)
606
+ true_cost: trueCostToDict(best.cost),
434
607
  });
435
608
  continue;
436
609
  }
437
- const blocked = durationOk ? Array.from(new Set(combos.flatMap((c)=>c.fails))).sort() : [
438
- 'duration'
439
- ];
610
+ // 不可行:归因 + duration 换长口径的 wish pool
611
+ const blocked = durationOk
612
+ ? Array.from(new Set(combos.flatMap(c => c.fails))).sort()
613
+ : ['duration'];
440
614
  const suggestions = [];
441
- for (const name of blocked){
442
- const opened = combos.filter((c)=>!c.fails.includes(name));
615
+ for (const name of blocked) {
616
+ const opened = combos.filter(c => !c.fails.includes(name));
443
617
  if (opened.length > 0) {
444
- const cheapest = opened.reduce((a, b)=>a.cost.moneyCny <= b.cost.moneyCny ? a : b);
445
- suggestions.push({
446
- relax: name,
447
- resulting_money_cny: cheapest.cost.moneyCny
448
- });
618
+ const cheapest = opened.reduce((a, b) => a.cost.moneyCny <= b.cost.moneyCny ? a : b);
619
+ suggestions.push({ relax: name, resulting_money_cny: cheapest.cost.moneyCny });
449
620
  }
450
621
  }
451
622
  let wish = null;
452
623
  if (!durationOk) {
453
624
  const longCombos = enumerateCombos(opt, cand, req, minDays);
454
- const budgetDropped = longCombos.filter((c)=>c.fails.length === 0 || c.fails.length === 1 && c.fails[0] === 'budget');
455
- const conds = {
456
- days: minDays
457
- };
625
+ const budgetDropped = longCombos.filter(c => c.fails.length === 0 || (c.fails.length === 1 && c.fails[0] === 'budget'));
626
+ const conds = { days: minDays };
458
627
  if (budgetDropped.length > 0) {
459
- conds['budget_cny'] = Math.min(...budgetDropped.map((c)=>c.cost.moneyCny));
628
+ conds['budget_cny'] = Math.min(...budgetDropped.map(c => c.cost.moneyCny));
460
629
  }
461
- if (opt.bestMonths?.length) conds['best_months'] = opt.bestMonths;
462
- wish = {
463
- name: opt.label,
464
- conditions: conds,
465
- reason: i18nT('sg.wish_reason_window', {
466
- days: windowDays,
467
- minDays
468
- })
469
- };
630
+ if (opt.bestMonths?.length)
631
+ conds['best_months'] = opt.bestMonths;
632
+ wish = { name: opt.label, conditions: conds, reason: i18nT('sg.wish_reason_window', { days: windowDays, minDays }) };
470
633
  }
471
634
  verdicts.push({
472
- candidate_id: opt.id,
473
- name: opt.label,
474
- feasible: false,
475
- imagery_match: opt.score ?? 0,
476
- unsat_core: blocked,
477
- suggestions,
478
- wish_pool: wish
635
+ candidate_id: opt.id, name: opt.label, feasible: false, imagery_match: opt.score ?? 0,
636
+ unsat_core: blocked, suggestions, wish_pool: wish,
479
637
  });
480
638
  }
481
- const feasible = verdicts.filter((v)=>v.feasible).sort((a, b)=>b.imagery_match - a.imagery_match);
639
+ const feasible = verdicts.filter(v => v.feasible).sort((a, b) => b.imagery_match - a.imagery_match);
482
640
  return {
483
641
  verdicts,
484
642
  recommended: feasible.length > 0 ? feasible[0]['candidate_id'] : null,
485
- answer_md: renderCandidateMarkdown(req, verdicts, feasible.length > 0 ? String(feasible[0]['candidate_id']) : null)
643
+ answer_md: renderCandidateMarkdown(req, verdicts, feasible.length > 0 ? String(feasible[0]['candidate_id']) : null),
486
644
  };
487
645
  }
488
646
  function renderCandidateMarkdown(req, verdicts, recommended) {
489
- const feasible = verdicts.filter((v)=>v['feasible']).sort((a, b)=>b['imagery_match'] - a['imagery_match']);
490
- const parked = verdicts.filter((v)=>!v['feasible']);
647
+ const feasible = verdicts.filter(v => v['feasible']).sort((a, b) => b['imagery_match'] - a['imagery_match']);
648
+ const parked = verdicts.filter(v => !v['feasible']);
491
649
  const lines = [];
492
- lines.push(i18nT('md.header', {
493
- note: req.note
494
- }));
650
+ lines.push(i18nT('md.header', { note: req.note }));
495
651
  lines.push(i18nT('md.constraints', {
496
- days: req.windowDays,
497
- budget: req.budgetCny,
498
- hours: requiredUsableHours(req.motivation).toFixed(1)
652
+ days: req.windowDays, budget: req.budgetCny,
653
+ hours: requiredUsableHours(req.motivation).toFixed(1),
499
654
  }));
500
655
  lines.push('');
501
- for (const v of parked){
656
+ for (const v of parked) {
502
657
  const core = v['unsat_core'] ?? [];
503
- lines.push(i18nT('md.infeasible', {
504
- name: String(v['name']),
505
- core: core.join('、')
506
- }));
658
+ lines.push(i18nT('md.infeasible', { name: String(v['name']), core: core.join('、') }));
507
659
  const sug = (v['suggestions'] ?? [])[0];
508
- if (sug) lines.push(i18nT('md.relax_one', {
509
- relax: String(sug['relax']),
510
- money: String(sug['resulting_money_cny'] ?? '')
511
- }));
660
+ if (sug)
661
+ lines.push(i18nT('md.relax_one', { relax: String(sug['relax']), money: String(sug['resulting_money_cny'] ?? '') }));
512
662
  const wish = v['wish_pool'];
513
663
  if (wish) {
514
664
  const conds = wish['conditions'];
515
- const budgetNote = conds['budget_cny'] ? i18nT('md.wish_budget', {
516
- cny: String(conds['budget_cny'])
517
- }) : '';
665
+ const budgetNote = conds['budget_cny'] ? i18nT('md.wish_budget', { cny: String(conds['budget_cny']) }) : '';
518
666
  const months = conds['best_months'];
519
- const season = months ? i18nT('md.wish_season', {
520
- months: String(months)
521
- }) : '';
522
- lines.push(i18nT('md.wish_pool', {
523
- days: String(conds['days']),
524
- budgetNote,
525
- season
526
- }));
667
+ const season = months ? i18nT('md.wish_season', { months: String(months) }) : '';
668
+ lines.push(i18nT('md.wish_pool', { days: String(conds['days']), budgetNote, season }));
527
669
  }
528
670
  }
529
671
  lines.push('');
530
- for (const v of feasible){
672
+ for (const v of feasible) {
531
673
  const ch = v['chosen'];
532
674
  const tc = v['true_cost'];
533
675
  lines.push(i18nT('md.feasible_unified', {
534
- name: String(v['name']),
535
- svc: String(ch['out_service']),
536
- mode: String(ch['out_transfer']),
537
- wake: String(tc['wake']),
538
- energy: String(tc['energy_arrival_pct']),
539
- d2d: String(tc['door_to_door_out']),
540
- hours: String(tc['usable_hours']),
541
- money: String(tc['money_cny'])
676
+ name: String(v['name']), svc: String(ch['out_service']), mode: String(ch['out_transfer']),
677
+ wake: String(tc['wake']), energy: String(tc['energy_arrival_pct']), d2d: String(tc['door_to_door_out']),
678
+ hours: String(tc['usable_hours']), money: String(tc['money_cny']),
542
679
  }));
543
680
  }
544
681
  if (feasible.length) {
545
682
  lines.push('');
546
683
  const best = feasible[0];
547
684
  const alt = feasible[1];
548
- lines.push(i18nT('md.recommend', {
549
- best: String(best['name']),
550
- match: (best['imagery_match'] * 100).toFixed(0)
551
- }) + (alt ? i18nT('md.alt', {
552
- name: String(alt['name']),
553
- money: String(alt['true_cost']['money_cny']),
554
- match: (alt['imagery_match'] * 100).toFixed(0)
555
- }) : ''));
685
+ lines.push(i18nT('md.recommend', { best: String(best['name']), match: (best['imagery_match'] * 100).toFixed(0) })
686
+ + (alt ? i18nT('md.alt', {
687
+ name: String(alt['name']), money: String(alt['true_cost']['money_cny']),
688
+ match: (alt['imagery_match'] * 100).toFixed(0),
689
+ }) : ''));
556
690
  }
557
691
  lines.push('');
558
692
  lines.push(i18nT('md.decide_two'));
559
693
  if (feasible.length >= 2) {
560
- lines.push(i18nT('md.q_choice', {
561
- a: String(feasible[0]['name']),
562
- b: String(feasible[1]['name'])
563
- }));
564
- } else if (feasible.length === 1) {
565
- lines.push(i18nT('md.q_single', {
566
- name: String(feasible[0]['name'])
567
- }));
568
- } else {
694
+ lines.push(i18nT('md.q_choice', { a: String(feasible[0]['name']), b: String(feasible[1]['name']) }));
695
+ }
696
+ else if (feasible.length === 1) {
697
+ lines.push(i18nT('md.q_single', { name: String(feasible[0]['name']) }));
698
+ }
699
+ else {
569
700
  lines.push(i18nT('md.q_none'));
570
701
  }
571
702
  const p0 = parked[0];
572
703
  const p0wish = p0?.['wish_pool'];
573
704
  if (p0wish) {
574
705
  const conds = p0wish['conditions'];
575
- lines.push(i18nT('md.q_wish', {
576
- name: String(p0['name']),
577
- days: String(conds['days'])
578
- }));
579
- } else if (feasible.length) {
706
+ lines.push(i18nT('md.q_wish', { name: String(p0['name']), days: String(conds['days']) }));
707
+ }
708
+ else if (feasible.length) {
580
709
  const ch = feasible[0]['chosen'];
581
- lines.push(i18nT('md.q_depart', {
582
- id: String(ch['out_service']),
583
- dep: ''
584
- }));
710
+ lines.push(i18nT('md.q_depart', { id: String(ch['out_service']), dep: '' }));
585
711
  }
586
712
  return lines.join('\n');
587
713
  }
588
714
  export { minToHhmm };
589
-
590
-
591
- //# sourceURL=ts/src/unified.ts