@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,14 +1,35 @@
1
+ /**
2
+ * 时间感评测(时间锚点层 + travel_slot_extraction.v1 槽位抽取):
3
+ *
4
+ * 确定性部分(进 CI,run-all-tests.sh 挂载):
5
+ * 1. 锚点卡:固定 now=2026-08-27(周四)断言关键换算(下周一/大后天/下下周四/下个月中旬/季度/节日)
6
+ * 2. 过期校验 flagExpiredSlots:绝对月日早于锚点今天判过期;相对表达永不判;幂等
7
+ * 3. 评分器 scoreExtraction 自测(通过/语言/域/槽位值/missing_slots/多键 warning)
8
+ * 4. mock 回放管道:25 题 golden 经 mock 回吐,评分管道应 25/25(管道自测,与模型质量解耦)
9
+ * 7. 春节锚点生成面(issue #274):到期即红守卫(最晚锚点年份 ≥ 当前年+3)+ 双 oracle
10
+ * 交叉验证(旧表 2026-2031 六条 + 港天文台 2032-2040 九条)+ 耗尽显式告警
11
+ *
12
+ * 真模型巡检(只读报告,不进 CI 红线;ADR-11 质量层定位):
13
+ * cd ts && npx tsx scripts/time-eval-tests.ts --real
14
+ * 需 LLM_API_KEY(或 DEEPSEEK_API_KEY;仓根 .env 亦可)。锚点固定 anchor_date=2026-08-27
15
+ * (golden 的过期判定依赖它,见数据文件 note)。只读:不碰 gotry-state(巡检状态纪律)。
16
+ */
1
17
  import assert from 'node:assert/strict';
2
18
  import { readFileSync } from 'node:fs';
3
19
  import { join } from 'node:path';
4
- import { buildTimeAnchor, parseAbsoluteDate } from '../src/time-anchor.js';
20
+ import { SPRING_FESTIVAL, SPRING_FESTIVAL_HORIZON_MIN_YEARS, buildTimeAnchor, parseAbsoluteDate, springFestivalHorizonOk, springFestivalHorizonYears, } from '../src/time-anchor.js';
5
21
  import { detectLanguage, flagExpiredSlots, scoreExtraction } from '../src/travel-slots.js';
6
22
  import { resolveSlotDate, resolveSlots, specDateMismatches } from '../src/slot-spec.js';
7
23
  import { createMockLlm } from '../src/mock-llm.js';
8
24
  import { createOpenAICompatLlm } from '../src/dsh-llm.js';
25
+ import { newState, runTurn } from '../src/loop.js';
26
+ import { parseRequest } from '../src/model.js';
27
+ import { solveChoiceSegment } from '../src/unified.js';
28
+ import { parsePlanningWindow } from '../src/time-anchor.js';
9
29
  const dataset = JSON.parse(readFileSync(join('..', 'data', 'time-slot-eval.json'), 'utf-8'));
10
30
  const [ay, am, ad] = dataset.anchor_date.split('-').map(Number);
11
- const ANCHOR_NOW = new Date(ay, am - 1, ad, 12);
31
+ const ANCHOR_NOW = new Date(ay, am - 1, ad, 12); // 锚点日中午,避免午夜边界
32
+ // ---- 1. 锚点卡(固定 now 断言) ------------------------------------------------
12
33
  {
13
34
  const a = buildTimeAnchor(ANCHOR_NOW);
14
35
  assert.equal(a.today, '2026-08-27', 'today');
@@ -26,124 +47,58 @@ const ANCHOR_NOW = new Date(ay, am - 1, ad, 12);
26
47
  assert.ok(c.includes('元旦 2027-01-01'), '元旦取下一次发生');
27
48
  assert.ok(c.includes('春节 2027-02-06'), '春节锚点(2026 春节已过,取 2027)');
28
49
  console.log('1. 锚点卡断言 OK(固定 2026-08-27:下周一/大后天/下下周四/月分段/季度/节日)');
50
+ // D-9 扩表回归:2030 年中锚点 → 春节 2031-01-23(表外年份静默缺失的回归闸)
29
51
  const a2030 = buildTimeAnchor(new Date(2030, 5, 1, 12));
30
52
  assert.ok(a2030.card.includes('春节 2031-01-23'), 'D-9 扩表:2030 年视视角春节取 2031-01-23');
31
53
  console.log('1b. D-9 扩表断言 OK(2030 锚点 → 春节 2031-01-23,表覆盖至 2031)');
32
- }{
54
+ }
55
+ // ---- 2. 过期校验(确定性,对齐 golden 约定) ------------------------------------
56
+ {
33
57
  const anchor = buildTimeAnchor(ANCHOR_NOW);
34
58
  const base = {
35
- schema_version: 'travel_slot_extraction.v1',
36
- language: 'zh',
37
- domains: [
38
- 'requisition'
39
- ],
40
- slots: {
41
- requisition: {
42
- start_date: '8.1',
43
- end_date: '8.10'
44
- }
45
- },
46
- missing_slots: []
59
+ schema_version: 'travel_slot_extraction.v1', language: 'zh',
60
+ domains: ['requisition'], slots: { requisition: { start_date: '8.1', end_date: '8.10' } }, missing_slots: [],
47
61
  };
48
62
  const flagged = flagExpiredSlots(base, anchor);
49
- assert.deepEqual(flagged.missing_slots, [
50
- 'requisition.start_date (date is expired)'
51
- ], '8.1 在 08-27 锚点下过期');
63
+ assert.deepEqual(flagged.missing_slots, ['requisition.start_date (date is expired)'], '8.1 在 08-27 锚点下过期');
52
64
  assert.equal(flagged.slots.requisition.start_date, '8.1', '槽位原值逐字保留');
53
65
  assert.equal(flagged.missing_slots.length, 1, 'end_date 不重复判(只判主日期字段,对齐 golden)');
54
66
  const again = flagExpiredSlots(flagged, anchor);
55
67
  assert.equal(again.missing_slots.length, 1, '幂等不重复加');
56
- const future = flagExpiredSlots({
57
- ...base,
58
- slots: {
59
- requisition: {
60
- start_date: '9.1'
61
- }
62
- }
63
- }, anchor);
68
+ const future = flagExpiredSlots({ ...base, slots: { requisition: { start_date: '9.1' } } }, anchor);
64
69
  assert.equal(future.missing_slots.length, 0, '9.1 不过期');
65
- const relative = flagExpiredSlots({
66
- ...base,
67
- slots: {
68
- requisition: {
69
- start_date: '本周三'
70
- }
71
- }
72
- }, anchor);
70
+ const relative = flagExpiredSlots({ ...base, slots: { requisition: { start_date: '本周三' } } }, anchor);
73
71
  assert.equal(relative.missing_slots.length, 0, '相对表达(本周三)永不判过期');
74
- const iso = flagExpiredSlots({
75
- ...base,
76
- slots: {
77
- requisition: {
78
- start_date: '2026-08-01'
79
- }
80
- }
81
- }, anchor);
72
+ const iso = flagExpiredSlots({ ...base, slots: { requisition: { start_date: '2026-08-01' } } }, anchor);
82
73
  assert.equal(iso.missing_slots.length, 1, 'ISO 绝对日期判过期');
83
- const zhDate = flagExpiredSlots({
84
- ...base,
85
- slots: {
86
- requisition: {
87
- start_date: '8月5日'
88
- }
89
- }
90
- }, anchor);
74
+ const zhDate = flagExpiredSlots({ ...base, slots: { requisition: { start_date: '8月5日' } } }, anchor);
91
75
  assert.equal(zhDate.missing_slots.length, 1, '中文月日判过期');
92
- const enDate = flagExpiredSlots({
93
- ...base,
94
- slots: {
95
- requisition: {
96
- start_date: 'Aug-5'
97
- }
98
- }
99
- }, anchor);
76
+ const enDate = flagExpiredSlots({ ...base, slots: { requisition: { start_date: 'Aug-5' } } }, anchor);
100
77
  assert.equal(enDate.missing_slots.length, 0, '英文月名不判过期(逐字传递,对齐 golden time_english_date_range)');
101
- const flight = flagExpiredSlots({
102
- ...base,
103
- domains: [
104
- 'flight'
105
- ],
106
- slots: {
107
- flight: {
108
- departure_date: '8.1'
109
- }
110
- }
111
- }, anchor);
112
- assert.deepEqual(flight.missing_slots, [
113
- 'flight.departure_date (date is expired)'
114
- ], 'flight 主日期字段');
78
+ const flight = flagExpiredSlots({ ...base, domains: ['flight'], slots: { flight: { departure_date: '8.1' } } }, anchor);
79
+ assert.deepEqual(flight.missing_slots, ['flight.departure_date (date is expired)'], 'flight 主日期字段');
115
80
  assert.equal(parseAbsoluteDate('下周一', 2026), null, 'parseAbsoluteDate 不碰相对表达');
116
- for (const c of dataset.cases){
81
+ // language 判定归代码层(detectLanguage):全中文 zh / 全英文 en / 混排 mixed
82
+ for (const c of dataset.cases) {
117
83
  assert.equal(detectLanguage(c.utterance), c.expected.language, `detectLanguage ${c.id}`);
118
84
  }
119
85
  console.log('2. 过期校验断言 OK(8.1 过期/9.1 不/本周三不/ISO+中文月日判/英文不判/幂等)+ detectLanguage 全题对齐');
120
- }{
86
+ }
87
+ // ---- 3. 评分器自测 ------------------------------------------------------------
88
+ {
121
89
  const exp = dataset.cases[0].expected;
122
90
  const identical = structuredClone(exp);
123
91
  assert.equal(scoreExtraction(exp, identical).pass, true, '完全一致应 pass');
124
- const wrongLang = {
125
- ...structuredClone(exp),
126
- language: 'en'
127
- };
92
+ const wrongLang = { ...structuredClone(exp), language: 'en' };
128
93
  assert.equal(scoreExtraction(exp, wrongLang).pass, false, 'language 不一致应 fail');
129
- const wrongDomain = {
130
- ...structuredClone(exp),
131
- domains: [
132
- 'flight'
133
- ]
134
- };
94
+ const wrongDomain = { ...structuredClone(exp), domains: ['flight'] };
135
95
  assert.equal(scoreExtraction(exp, wrongDomain).pass, false, 'domains 不一致应 fail');
136
96
  const wrongSlot = structuredClone(exp);
137
97
  wrongSlot.slots.requisition.destination = '北京';
138
98
  const r = scoreExtraction(exp, wrongSlot);
139
99
  assert.equal(r.pass, false, '槽位值不一致应 fail');
140
- assert.ok(r.diffs.some((d)=>d.includes('slots.requisition.destination')), 'diff 带路径');
141
- const wrongMissing = {
142
- ...structuredClone(exp),
143
- missing_slots: [
144
- 'requisition.start_date (date is expired)'
145
- ]
146
- };
100
+ assert.ok(r.diffs.some(d => d.includes('slots.requisition.destination')), 'diff 带路径');
101
+ const wrongMissing = { ...structuredClone(exp), missing_slots: ['requisition.start_date (date is expired)'] };
147
102
  assert.equal(scoreExtraction(exp, wrongMissing).pass, false, 'missing_slots 不一致应 fail');
148
103
  const extraKey = structuredClone(exp);
149
104
  extraKey.slots.requisition['adults'] = 2;
@@ -151,149 +106,212 @@ const ANCHOR_NOW = new Date(ay, am - 1, ad, 12);
151
106
  assert.equal(rk.pass, true, '多出键不 fail(非致命)');
152
107
  assert.equal(rk.warnings.length, 1, '多出键记 warning');
153
108
  console.log('3. 评分器自测 OK(pass/language/domains/槽位值/missing_slots/extra-key warning)');
154
- }{
155
- const script = dataset.cases.map((c)=>({
156
- when: c.utterance,
157
- extraction: c.expected
158
- }));
109
+ }
110
+ // ---- 4. mock 回放管道(25/25 管道自测) ------------------------------------------
111
+ {
112
+ const script = dataset.cases.map(c => ({ when: c.utterance, extraction: c.expected }));
159
113
  const llm = createMockLlm(undefined, script);
160
114
  let pass = 0;
161
- for (const c of dataset.cases){
162
- const ext = await llm.extractSlots([
163
- {
164
- role: 'user',
165
- text: c.utterance
166
- }
167
- ]);
115
+ for (const c of dataset.cases) {
116
+ const ext = await llm.extractSlots([{ role: 'user', text: c.utterance }]);
168
117
  assert.ok(ext, `mock 应命中 ${c.id}`);
169
118
  const r = scoreExtraction(c.expected, ext);
170
119
  assert.ok(r.pass, `mock 回放 ${c.id} 应 pass:${r.diffs.join(';')}`);
171
120
  pass++;
172
121
  }
173
122
  console.log(`4. mock 回放管道 OK(${pass}/${dataset.cases.length}——评分管道自身正确)`);
174
- }{
175
- const anchor = buildTimeAnchor(ANCHOR_NOW);
176
- const r = (expr)=>resolveSlotDate(expr, anchor);
123
+ }
124
+ // ---- 5. 槽位→日期解析层(D-10 切片 A,slot-spec.ts) ------------------------------
125
+ {
126
+ const anchor = buildTimeAnchor(ANCHOR_NOW); // 2026-08-27 周四
127
+ const r = (expr) => resolveSlotDate(expr, anchor);
128
+ // 绝对表达:ISO/点分/中文月日(kind=absolute)
177
129
  assert.equal(r('2026-09-05').date, '2026-09-05', 'ISO');
178
130
  assert.equal(r('8.20').date, '2026-08-20', '点分');
179
131
  assert.equal(r('9月5日').date, '2026-09-05', '中文月日');
132
+ // 锚点卡词表:相对周/近邻日/月分段(kind=card)
180
133
  assert.equal(r('下周一').date, '2026-08-31', '下周一');
181
134
  assert.equal(r('本周六').date, '2026-08-29', '本周六');
182
135
  assert.equal(r('下下周四').date, '2026-09-10', '下下周四');
183
136
  assert.equal(r('明天').date, '2026-08-28', '明天');
184
137
  assert.equal(r('下个月中旬').date, '2026-09-11', '下个月中旬取首日');
138
+ // 「+N」后缀(槽位规则 5/6 拼接约定)
185
139
  assert.equal(r('下周五+3').date, '2026-09-07', '下周五+3');
186
140
  assert.equal(r('8.20+2天').date, '2026-08-22', '8.20+2天');
141
+ // 词表外:unresolved 逐字保留(不做开放式解析,ADR-12 边界)
187
142
  assert.equal(r('近期').kind, 'unresolved', '模糊词');
188
143
  assert.equal(r('这阵子').date, null, '模糊词不猜');
189
144
  assert.equal(r('next Monday').kind, 'unresolved', '英文相对不解析');
190
145
  assert.equal(r('next Monday').raw, 'next Monday', 'unresolved 保留原话');
146
+ // 整张抽取解析:hotel+flight 双域 + unresolved 清单
191
147
  const ext = {
192
148
  schema_version: 'travel_slot_extraction.v1',
193
149
  language: 'zh',
194
- domains: [
195
- 'hotel',
196
- 'flight'
197
- ],
150
+ domains: ['hotel', 'flight'],
198
151
  slots: {
199
- hotel: {
200
- action: 'search',
201
- city: '上海',
202
- check_in_date: '下周五',
203
- check_out_date: '下周五+3'
204
- },
205
- flight: {
206
- action: 'search',
207
- origin: '上海',
208
- destination: '杭州',
209
- departure_date: '这阵子',
210
- trip_type: 'one_way'
211
- }
152
+ hotel: { action: 'search', city: '上海', check_in_date: '下周五', check_out_date: '下周五+3' },
153
+ flight: { action: 'search', origin: '上海', destination: '杭州', departure_date: '这阵子', trip_type: 'one_way' },
212
154
  },
213
- missing_slots: []
155
+ missing_slots: [],
214
156
  };
215
157
  const resolved = resolveSlots(ext, anchor);
216
158
  assert.equal(resolved.hotel?.check_in_date?.date, '2026-09-04', 'hotel 入住=下周五');
217
159
  assert.equal(resolved.hotel?.check_out_date?.date, '2026-09-07', 'hotel 退房=下周五+3');
218
- assert.deepEqual(resolved.unresolved, [
219
- {
220
- field: 'flight.departure_date',
221
- raw: '这阵子'
222
- }
223
- ], 'unresolved 清单');
224
- assert.equal(specDateMismatches({
225
- segments: [
226
- {
227
- date: '2026-09-04'
228
- }
229
- ]
230
- }, resolved).length, 0, '一致则零 mismatch');
231
- const mm = specDateMismatches({
232
- segments: [
233
- {
234
- date: '2026-09-05'
235
- }
236
- ]
237
- }, resolved);
160
+ assert.deepEqual(resolved.unresolved, [{ field: 'flight.departure_date', raw: '这阵子' }], 'unresolved 清单');
161
+ // spec 一致性闸:单日期段分歧必须暴露;多段/无日期/unresolved 不判(不造假阳性)
162
+ assert.equal(specDateMismatches({ segments: [{ date: '2026-09-04' }] }, resolved).length, 0, '一致则零 mismatch');
163
+ const mm = specDateMismatches({ segments: [{ date: '2026-09-05' }] }, resolved);
238
164
  assert.equal(mm.length, 1, '单日期段分歧暴露');
239
165
  assert.equal(mm[0]?.specDate, '2026-09-05');
240
166
  assert.equal(mm[0]?.slotDate, '2026-09-04');
241
- assert.equal(specDateMismatches({
242
- segments: [
243
- {
244
- date: '2026-09-04'
245
- },
246
- {
247
- date: '2026-09-05'
248
- }
249
- ]
250
- }, resolved).length, 0, '多段无逐段真值不判(巡检修正)');
251
- const unresolvedOnly = {
252
- unresolved: [
253
- {
254
- field: 'flight.departure_date',
255
- raw: '近期'
256
- }
257
- ]
258
- };
259
- assert.equal(specDateMismatches({
260
- segments: [
261
- {
262
- date: '2026-09-05'
263
- }
264
- ]
265
- }, unresolvedOnly).length, 0, '全 unresolved 不参与比对');
266
- assert.equal(specDateMismatches({
267
- segments: []
268
- }, resolved).length, 0, 'spec 无日期不比对');
167
+ assert.equal(specDateMismatches({ segments: [{ date: '2026-09-04' }, { date: '2026-09-05' }] }, resolved).length, 0, '多段无逐段真值不判(巡检修正)');
168
+ const unresolvedOnly = { unresolved: [{ field: 'flight.departure_date', raw: '近期' }] };
169
+ assert.equal(specDateMismatches({ segments: [{ date: '2026-09-05' }] }, unresolvedOnly).length, 0, '全 unresolved 不参与比对');
170
+ assert.equal(specDateMismatches({ segments: [] }, resolved).length, 0, 'spec 无日期不比对');
269
171
  console.log('5. 槽位→日期解析 OK(绝对/词表/+N/unresolved 边界/整张解析/spec 一致性闸)');
270
- }console.log('\nTIME-EVAL TESTS: 5/5 OK(确定性部分,CI 口径)');
172
+ }
173
+ // ---- 6. Issue #2 未来年度窗口(真实 runTurn→validateSpec→solve 路径) -----------
174
+ {
175
+ const request = parseRequest({
176
+ note: 'issue-2-fixture',
177
+ home: { hubs: { H: { to_hub_min: 0 } } },
178
+ motivation: { weights: {}, hard: { wake_not_before: '06:00', min_arrival_energy_pct: 0 } },
179
+ window_days: 2,
180
+ budget_cny: 1000,
181
+ });
182
+ const option = (id, date, score) => ({
183
+ id, label: id, date, score, minDays: 1,
184
+ move: {
185
+ hub: 'H',
186
+ services: [{ id: `out-${id}`, depMin: 600, arrMin: 700, priceCny: 10 }],
187
+ retServices: [{ id: `ret-${id}`, depMin: 1000, arrMin: 1100, priceCny: 10 }],
188
+ transfers: [{ mode: 'walk', minutes: 10, priceCny: 0 }],
189
+ bufferMin: 30, bufferRetMin: 30, originTransferMin: 0, destTransferMin: 0,
190
+ },
191
+ stay: { nights: 1, stayCnyPerNight: 10, localDailyCny: 10 },
192
+ });
193
+ const spec = () => ({
194
+ segments: [{ id: 'dest', role: 'choice', options: [
195
+ option('past', '2026-09-01', 1), option('valid', '2026-10-01', 0.5), option('next-year', '2027-01-01', 0.9),
196
+ ] }],
197
+ });
198
+ const llmFor = (planned) => ({
199
+ extractFacts: async () => ({
200
+ assumptions: [],
201
+ profile: { workWindow: { homeTzOffsetMin: 0, startMin: 600, endMin: 1080, workdays: [0, 1, 2, 3, 4], evidence: 'fixture' }, bookedResources: [] },
202
+ }),
203
+ extractSpec: async () => structuredClone(planned),
204
+ extractSlots: async () => null,
205
+ polishQuestion: async (q) => q.text,
206
+ });
207
+ const run = (message, now) => runTurn(newState(2026), message, llmFor(spec()), [], async (candidate) => solveChoiceSegment(candidate, request), now);
208
+ const anchor = buildTimeAnchor(new Date(2026, 8, 10, 12));
209
+ const parsed = parsePlanningWindow('计划在2026年内完成一次旅行', anchor);
210
+ assert.deepEqual(parsed, { referenceDate: '2026-09-10', requestedYear: 2026, intent: 'future' }, 'named-year future context');
211
+ assert.equal(parsePlanningWindow('不要再推荐过去的时段,计划2026年内完成旅行', anchor)?.intent, 'future', 'negated past recommendation is not historical intent');
212
+ assert.equal(parsePlanningWindow('2025年没去成,计划2026年内完成旅行', anchor)?.requestedYear, 2026, 'historical context does not override requested future year');
213
+ assert.equal(parsePlanningWindow('计划在2025年或2026年内完成旅行', anchor), null, 'conflicting future years remain ambiguous');
214
+ assert.equal(parsePlanningWindow('历史回测2025年,计划2026年内完成旅行', anchor), null, 'historical and future years remain ambiguous');
215
+ // 相对年份(今年/明年/后年/年内)按本轮锚点换算为命名年窗口——未来时态不落过去(issue #2)
216
+ assert.deepEqual(parsePlanningWindow('今年年内完成一次旅行', anchor), { referenceDate: '2026-09-10', requestedYear: 2026, intent: 'future' }, 'relative this-year future context resolves against the anchor');
217
+ assert.equal(parsePlanningWindow('计划明年出行一次', anchor)?.requestedYear, 2027, 'relative next-year future context');
218
+ assert.equal(parsePlanningWindow('计划在明年或后年完成旅行', anchor), null, 'conflicting relative years remain ambiguous');
219
+ assert.equal(parsePlanningWindow('今年的计划很多', anchor), null, 'non-planning mention of this year is not a window');
220
+ assert.equal(parsePlanningWindow('历史回测今年内的方案', anchor)?.intent, 'historical', 'relative-year historical lookup stays historical');
221
+ assert.equal(parsePlanningWindow('2025年没去成,今年年内完成旅行', anchor)?.requestedYear, 2026, 'past failure does not override relative future year');
222
+ const future = await run('计划在2026年内完成一次旅行', new Date(2026, 8, 10, 12));
223
+ assert.deepEqual(future.state.spec?.segments[0]?.options.map(o => o.date), ['2026-10-01'], 'past option removed before solve');
224
+ assert.equal(future.state.solve?.recommended, 'valid', 'solver accepts only same-year future option');
225
+ assert.match(future.reply, /已排除规划窗口外的候选/, 'reply records deterministic rejection');
226
+ const expired = await run('计划在2025年内完成一次旅行', new Date(2026, 8, 10, 12));
227
+ assert.equal(expired.state.solve, undefined, 'expired requested year does not solve');
228
+ assert.match(expired.reply, /2025 年已结束/, 'expired requested year is explicit');
229
+ assert.doesNotMatch(expired.reply, /2027/, 'expired year is not silently rolled over');
230
+ const historical = await run('历史回测2026年内的方案', new Date(2026, 8, 10, 12));
231
+ assert.deepEqual(historical.state.spec?.segments[0]?.options.map(o => o.date), ['2026-09-01', '2026-10-01', '2027-01-01'], 'historical lookup bypass preserved');
232
+ assert.equal(historical.state.solve?.recommended, 'past', 'historical solver may select historical option');
233
+ // 相对年份未来意图同样不得把过去候选送进求解(issue #2 剩余面,固定时钟反例)
234
+ const relativeFuture = await run('今年年内完成一次旅行', new Date(2026, 8, 10, 12));
235
+ assert.deepEqual(relativeFuture.state.spec?.segments[0]?.options.map(o => o.date), ['2026-10-01'], 'relative this-year intent removes past option before solve');
236
+ assert.equal(relativeFuture.state.solve?.recommended, 'valid', 'relative this-year intent does not recommend a past slot');
237
+ assert.match(relativeFuture.reply, /已排除规划窗口外的候选/, 'relative this-year intent records deterministic rejection');
238
+ const relativeNextYear = await run('计划明年出行一次', new Date(2026, 8, 10, 12));
239
+ assert.deepEqual(relativeNextYear.state.spec?.segments[0]?.options.map(o => o.date), ['2027-01-01'], 'relative next-year intent keeps only in-window options');
240
+ assert.equal(relativeNextYear.state.solve?.recommended, 'next-year', 'relative next-year intent solves within the requested year');
241
+ const relativeHistorical = await run('历史回测今年内的方案', new Date(2026, 8, 10, 12));
242
+ assert.deepEqual(relativeHistorical.state.spec?.segments[0]?.options.map(o => o.date), ['2026-09-01', '2026-10-01', '2027-01-01'], 'explicit relative-year historical lookup keeps past options');
243
+ assert.equal(relativeHistorical.state.solve?.recommended, 'past', 'relative-year historical lookup may select past option');
244
+ const refreshedState = newState(2026);
245
+ await runTurn(refreshedState, '计划在2026年内完成一次旅行', llmFor(spec()), [], async (candidate) => solveChoiceSegment(candidate, request), new Date(2026, 8, 10, 12));
246
+ const refreshed = await runTurn(refreshedState, '计划在2026年内完成一次旅行', llmFor(spec()), [], async (candidate) => solveChoiceSegment(candidate, request), new Date(2026, 9, 2, 12));
247
+ assert.equal(refreshed.state.solve, undefined, 'later eligible turn refreshes reference date');
248
+ assert.match(refreshed.reply, /2026-10-02 至 2026-12-31/, 'later reference date is used');
249
+ assert.equal('planningWindow' in refreshed.state, false, 'planning reference is not persisted in TripState');
250
+ console.log('6. Issue #2 未来年度窗口 OK(runTurn→validateSpec→solve:排除过去候选/拒绝过期年/保留历史回测/刷新参考日)');
251
+ }
252
+ // ---- 7. Issue #274 春节锚点生成面:到期即红守卫 + 双 oracle 交叉验证 + 耗尽显式告警 ----
253
+ {
254
+ // 守卫(真实时钟):最晚锚点年份 ≥ 当前年+3——现在过,临近耗尽自动红(2099 表则 2097 年起红)
255
+ assert.ok(springFestivalHorizonOk(), `春节锚点地平线不足 ${SPRING_FESTIVAL_HORIZON_MIN_YEARS} 年(到期即红守卫)`);
256
+ // 牙齿证明:被否决的旧 2031 手抄表,进入预警窗(2029-01-01 时钟)守卫必红——而非 2031 静默耗尽
257
+ const LEGACY_HAND_2031 = {
258
+ 2026: '2026-02-17', 2027: '2027-02-06', 2028: '2028-01-26',
259
+ 2029: '2029-02-13', 2030: '2030-02-03', 2031: '2031-01-23',
260
+ };
261
+ assert.ok(springFestivalHorizonYears(LEGACY_HAND_2031, new Date(2029, 0, 1)) < SPRING_FESTIVAL_HORIZON_MIN_YEARS, '旧 2031 手抄表在 2029 时钟下守卫必须红(牙齿证明)');
262
+ // 守卫行程自证(对当前表,表扩展后无需改测试):耗尽前 3 年绿、前 2 年红
263
+ const latestYear = Math.max(...Object.keys(SPRING_FESTIVAL).map(Number));
264
+ assert.equal(springFestivalHorizonOk(new Date(latestYear - 3, 0, 1)), true, '耗尽前 3 年守卫绿');
265
+ assert.equal(springFestivalHorizonOk(new Date(latestYear - 2, 0, 1)), false, '耗尽前 2 年守卫红');
266
+ // oracle A:仓库旧表 2026-2031 逐条比对——库生成不许漂移现状行为
267
+ for (const [year, date] of Object.entries(LEGACY_HAND_2031)) {
268
+ assert.equal(SPRING_FESTIVAL[Number(year)], date, `oracle A 旧表 ${year}`);
269
+ }
270
+ // oracle B:#384 港天文台核实 2032-2040(gts/time/calendar T2032e–T2040e 正月初一行,2026-09-11 取)
271
+ const HKO_2032_2040 = {
272
+ 2032: '2032-02-11', 2033: '2033-01-31', 2034: '2034-02-19',
273
+ 2035: '2035-02-08', 2036: '2036-01-28', 2037: '2037-02-15',
274
+ 2038: '2038-02-04', 2039: '2039-01-24', 2040: '2040-02-12',
275
+ };
276
+ for (const [year, date] of Object.entries(HKO_2032_2040)) {
277
+ assert.equal(SPRING_FESTIVAL[Number(year)], date, `oracle B 港天文台 ${year}`);
278
+ }
279
+ // 生成面贯通:旧表外年份(2038)经 buildTimeAnchor 真实出现在锚点卡
280
+ const a2038 = buildTimeAnchor(new Date(2038, 0, 10, 12));
281
+ assert.ok(a2038.card.includes('春节 2038-02-04'), '2038 视角春节取 2038-02-04(生成表贯通锚点卡)');
282
+ // 耗尽显式告警:表耗尽后锚点卡显式标注,不再静默省略;未耗尽则不告警
283
+ const exhausted = buildTimeAnchor(new Date(latestYear + 1, 5, 1, 12));
284
+ assert.ok(exhausted.card.includes('春节(锚点表已到期'), '耗尽后锚点卡显式告警(非静默省略)');
285
+ assert.ok(!buildTimeAnchor(ANCHOR_NOW).card.includes('锚点表已到期'), '未耗尽不告警');
286
+ console.log('7. 春节锚点生成面 OK(到期守卫/旧 2031 表牙齿/行程自证/双 oracle 15 条/生成贯通/耗尽告警)');
287
+ }
288
+ console.log('\nTIME-EVAL TESTS: 7/7 OK(确定性部分,CI 口径)');
289
+ // ---- 真模型巡检(--real,只读报告,不进 CI 红线) ----------------------------------
271
290
  if (process.argv.includes('--real')) {
272
291
  try {
273
- for (const line of readFileSync(join('..', '.env'), 'utf-8').split('\n')){
292
+ for (const line of readFileSync(join('..', '.env'), 'utf-8').split('\n')) {
274
293
  const m = line.match(/^([A-Z_]+)=(.*)$/);
275
- if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
294
+ if (m && !process.env[m[1]])
295
+ process.env[m[1]] = m[2].trim();
276
296
  }
277
- } catch {}
297
+ }
298
+ catch { /* .env 可选 */ }
278
299
  const hasKey = Boolean(process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY']);
279
300
  if (!hasKey) {
280
301
  console.log('\n--real:无 LLM_API_KEY,跳过真模型巡检(ADR-8 回退原则)');
281
- } else {
282
- const clock = ()=>ANCHOR_NOW;
302
+ }
303
+ else {
304
+ const clock = () => ANCHOR_NOW; // 锚点固定:golden 过期判定依赖 anchor_date
283
305
  const llm = createOpenAICompatLlm(undefined, clock);
284
306
  console.log(`\n=== 真模型巡检(${process.env['LLM_MODEL'] ?? 'MiniMax-M2'},锚点 ${dataset.anchor_date})===`);
285
307
  let pass = 0;
286
308
  let p0Pass = 0;
287
309
  let p0Total = 0;
288
- for (const c of dataset.cases){
289
- if (c.priority === 'P0') p0Total++;
310
+ for (const c of dataset.cases) {
311
+ if (c.priority === 'P0')
312
+ p0Total++;
290
313
  try {
291
- const ext = await llm.extractSlots([
292
- {
293
- role: 'user',
294
- text: c.utterance
295
- }
296
- ]);
314
+ const ext = await llm.extractSlots([{ role: 'user', text: c.utterance }]);
297
315
  if (!ext) {
298
316
  console.log(`FAIL ${c.id}(${c.priority}):模型未产出合法 v1 JSON`);
299
317
  continue;
@@ -301,18 +319,18 @@ if (process.argv.includes('--real')) {
301
319
  const r = scoreExtraction(c.expected, ext);
302
320
  if (r.pass) {
303
321
  pass++;
304
- if (c.priority === 'P0') p0Pass++;
322
+ if (c.priority === 'P0')
323
+ p0Pass++;
305
324
  console.log(`PASS ${c.id}${r.warnings.length ? `(warning: ${r.warnings.join(';')})` : ''}`);
306
- } else {
325
+ }
326
+ else {
307
327
  console.log(`FAIL ${c.id}(${c.priority}):\n ${r.diffs.join('\n ')}`);
308
328
  }
309
- } catch (e) {
329
+ }
330
+ catch (e) {
310
331
  console.log(`ERROR ${c.id}(${c.priority}):${e.message.slice(0, 120)}`);
311
332
  }
312
333
  }
313
334
  console.log(`\nREAL ACCURACY: ${pass}/${dataset.cases.length}(P0: ${p0Pass}/${p0Total})——巡检报告口径,不设红线`);
314
335
  }
315
336
  }
316
-
317
-
318
- //# sourceURL=ts/scripts/time-eval-tests.ts