@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
@@ -29,10 +29,11 @@ import { appendTrip as gateTrip, type TimelineEvent } from './travel-timeline.ts
29
29
  import { upsertCompanion, type CompanionProfile, type CompanionConstraints } from './companions.ts'
30
30
  import type { WishPoolEntry } from './wish-pool.ts'
31
31
 
32
- const SCHEMA_VERSION = '1'
32
+ const SCHEMA_VERSION = '2'
33
33
 
34
34
  export interface LedgerEventRow {
35
35
  seq: number
36
+ tenant_id: string
36
37
  ts: string
37
38
  actor: string
38
39
  kind: string
@@ -180,11 +181,12 @@ export class StateLedger {
180
181
  ts?: string
181
182
  }): number | null {
182
183
  const stmt = this.db.prepare(
183
- `INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key, run_id)
184
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
184
+ `INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
185
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
185
186
  )
186
187
  try {
187
188
  const info = stmt.run(
189
+ this.tenant,
188
190
  ev.ts ?? new Date().toISOString(),
189
191
  ev.actor,
190
192
  ev.kind,
@@ -201,12 +203,16 @@ export class StateLedger {
201
203
  }
202
204
 
203
205
  readEvents(kind?: string, limit = 100): LedgerEventRow[] {
204
- const where = kind ? 'WHERE kind = ?' : ''
205
- const rows = this.db.prepare(
206
- `SELECT seq, ts, actor, kind, subject_id, payload, idem_key, run_id
207
- FROM events ${where} ORDER BY seq DESC LIMIT ?`,
208
- ).all(...(kind ? [kind, limit] : [limit])) as LedgerEventRow[]
209
- return rows
206
+ const rows = kind
207
+ ? this.db.prepare(
208
+ `SELECT seq, tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id
209
+ FROM events WHERE tenant_id = ? AND kind = ? ORDER BY seq DESC LIMIT ?`,
210
+ ).all(this.tenant, kind, limit)
211
+ : this.db.prepare(
212
+ `SELECT seq, tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id
213
+ FROM events WHERE tenant_id = ? ORDER BY seq DESC LIMIT ?`,
214
+ ).all(this.tenant, limit)
215
+ return rows as LedgerEventRow[]
210
216
  }
211
217
 
212
218
  countEvents(): number {
@@ -261,10 +267,12 @@ export class StateLedger {
261
267
  saved: boolean
262
268
  profile: MergedProfile & { updated_at?: string }
263
269
  } {
270
+ // 事件边界 ISO:事件行的 ts 就是 homeCityPreference.updated_at 的源头,
271
+ // fold 路径会读同一字段回放——rebuildProjections 必须能复现该值。
264
272
  const ts = new Date().toISOString()
265
273
  const run = this.db.transaction((): { saved: boolean; profile: MergedProfile & { updated_at?: string } } => {
266
274
  const current = this.readMotivation()
267
- const merged = mergeProfile(current, patch)
275
+ const merged = mergeProfile(current, patch, ts)
268
276
  if (!merged) {
269
277
  const unchanged = { ...(current ?? { weights: {}, evidence: [], hard: {} }), updated_at: current?.updated_at ?? ts } as MergedProfile & { updated_at?: string }
270
278
  return { saved: false, profile: unchanged }
@@ -462,7 +470,8 @@ export class StateLedger {
462
470
  }
463
471
  case 'motivation.patch': {
464
472
  const current = this.readMotivation()
465
- const merged = mergeProfile(current, p['patch'] as ProfilePatch)
473
+ // 重建路径:时间戳 = 事件行 row.ts(账本事实);不在 fold 里再读时钟
474
+ const merged = mergeProfile(current, p['patch'] as ProfilePatch, row.ts)
466
475
  if (merged) {
467
476
  const doc = { ...merged, updated_at: row.ts }
468
477
  this.db.prepare(
@@ -481,7 +490,7 @@ export class StateLedger {
481
490
  case 'wish.updated': {
482
491
  const id = String(p['wish_id'])
483
492
  const patch = (p['patch'] ?? {}) as Record<string, unknown>
484
- const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND item_id = ?`).get(id) as { doc: string } | undefined
493
+ const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND item_id = ? AND tenant_id = ?`).get(id, this.tenant) as { doc: string } | undefined
485
494
  if (rows) {
486
495
  const doc = { ...JSON.parse(rows.doc), ...patch } as WishPoolEntry
487
496
  this.upsertItem('wish_pool', id, doc)
@@ -668,7 +677,7 @@ export class StateLedger {
668
677
  for (const s of subjects) {
669
678
  const placeholders = s.kinds.map(() => '?').join(',')
670
679
  const info = this.db.prepare(
671
- `DELETE FROM events WHERE subject_id = ? AND tenant_id = ? AND kind IN (${placeholders})`,
680
+ 'DELETE FROM events WHERE subject_id = ? AND tenant_id = ? AND kind IN (' + placeholders + ')',
672
681
  ).run(s.subjectId, this.tenant, ...s.kinds)
673
682
  deleted += info.changes
674
683
  }
@@ -735,11 +744,11 @@ export function openDb(stateRoot: string, tenant = 'local'): StateLedger {
735
744
  }
736
745
  db.exec(SCHEMA)
737
746
  db.pragma('user_version = 2')
738
- db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version', '2')
747
+ db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version', SCHEMA_VERSION)
739
748
  })()
740
- // v1 升级后:从 events 重建投影(v1 投影表已 DROP)
749
+ // v1 升级后:旧单用户事件只可安全归入 local;即使调用者打开非 local 租户,也只重建 local 投影。
741
750
  if (isV1) {
742
- const ledger = new StateLedger(db, stateRoot, tenant)
751
+ const ledger = new StateLedger(db, stateRoot, 'local')
743
752
  ledger.rebuildProjections()
744
753
  }
745
754
  } else {
@@ -782,9 +791,13 @@ export function ensureLedger(stateRoot: string, tenant = 'local'): StateLedger {
782
791
  if (existsSync(join(dir, f))) copyFileSync(join(dir, f), join(backupDir, f))
783
792
  }
784
793
  }
785
- const ledger = openDb(stateRoot, tenant)
786
- if (hasLegacy) importLegacyInto(ledger, dir)
787
- return ledger
794
+ if (hasLegacy) {
795
+ // JSON/JSONL 是单用户本地状态,没有可审计租户归属;迁移边界=只归入 local,绝不按调用 tenant 猜测或搬移。
796
+ const localLedger = openDb(stateRoot, 'local')
797
+ importLegacyInto(localLedger, dir)
798
+ return tenant === 'local' ? localLedger : openDb(stateRoot, tenant)
799
+ }
800
+ return openDb(stateRoot, tenant)
788
801
  }
789
802
 
790
803
  function importLegacyInto(ledger: StateLedger, dir: string): MigrationReport['imported'] {
@@ -863,32 +876,37 @@ function readLegacyJsonl<T>(stateRoot: string, name: string): T[] {
863
876
  }
864
877
  }
865
878
 
866
- export function readMotivationWithFallback(stateRoot: string): (MergedProfile & { updated_at?: string }) | null {
867
- const ledger = openLedgerIfExists(stateRoot)
879
+ export function readMotivationWithFallback(stateRoot: string, tenant = 'local'): (MergedProfile & { updated_at?: string }) | null {
880
+ const ledger = openLedgerIfExists(stateRoot, tenant)
868
881
  if (ledger) return ledger.readMotivation()
882
+ if (tenant !== 'local') return null
869
883
  return readLegacyJson<(MergedProfile & { updated_at?: string }) | null>(stateRoot, 'motivation-profile.json', null)
870
884
  }
871
885
 
872
- export function readWishPoolWithFallback(stateRoot: string): WishPoolEntry[] {
873
- const ledger = openLedgerIfExists(stateRoot)
886
+ export function readWishPoolWithFallback(stateRoot: string, tenant = 'local'): WishPoolEntry[] {
887
+ const ledger = openLedgerIfExists(stateRoot, tenant)
874
888
  if (ledger) return ledger.readWishPool()
889
+ if (tenant !== 'local') return []
875
890
  return readLegacyJson<WishPoolEntry[]>(stateRoot, 'wish-pool.json', [])
876
891
  }
877
892
 
878
- export function readUtilityEventsWithFallback(stateRoot: string): MemoryUtilityEvent[] {
879
- const ledger = openLedgerIfExists(stateRoot)
893
+ export function readUtilityEventsWithFallback(stateRoot: string, tenant = 'local'): MemoryUtilityEvent[] {
894
+ const ledger = openLedgerIfExists(stateRoot, tenant)
880
895
  if (ledger) return ledger.readUtilityEvents()
896
+ if (tenant !== 'local') return []
881
897
  return readLegacyJsonl<MemoryUtilityEvent>(stateRoot, 'memory-utility.jsonl')
882
898
  }
883
899
 
884
- export function readTripsWithFallback(stateRoot: string): TimelineEvent[] {
885
- const ledger = openLedgerIfExists(stateRoot)
900
+ export function readTripsWithFallback(stateRoot: string, tenant = 'local'): TimelineEvent[] {
901
+ const ledger = openLedgerIfExists(stateRoot, tenant)
886
902
  if (ledger) return ledger.readTrips()
903
+ if (tenant !== 'local') return []
887
904
  return readLegacyJsonl<TimelineEvent>(stateRoot, 'trips.jsonl')
888
905
  }
889
906
 
890
- export function readCompanionsWithFallback(stateRoot: string): CompanionProfile[] {
891
- const ledger = openLedgerIfExists(stateRoot)
907
+ export function readCompanionsWithFallback(stateRoot: string, tenant = 'local'): CompanionProfile[] {
908
+ const ledger = openLedgerIfExists(stateRoot, tenant)
892
909
  if (ledger) return ledger.readCompanions()
910
+ if (tenant !== 'local') return []
893
911
  return readLegacyJson<CompanionProfile[]>(stateRoot, 'companions.json', [])
894
912
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Prevent a continuable subagent durable id from entering the jobs tool family.
3
+ *
4
+ * dsh deliberately keeps continuable children out of the jobs registry. The
5
+ * jobs tools therefore cannot distinguish a mistaken durable child id from any
6
+ * other unknown id once their body runs. GoTry owns the surrounding plugin and
7
+ * can use dsh's typed pre-execute waterfall to give the model a recoverable,
8
+ * actionable error before the jobs registry is consulted.
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis'
11
+ import type { Agent } from '@deepseek-ai/dsh-agent'
12
+ import type { SessionId } from '@deepseek-ai/dsh-session'
13
+ import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
14
+ import type { SubagentListEntry, SubagentRuntime } from '@deepseek-ai/dsh-subagent'
15
+
16
+ const JOB_TOOLS = new Set(['job_output', 'job_kill'])
17
+
18
+ function subagentsOf(ctx: Context): SubagentRuntime | undefined {
19
+ try {
20
+ return (ctx as Context & { subagents?: SubagentRuntime }).subagents
21
+ } catch {
22
+ // Minimal hosts and benchmark contexts may not mount the optional service.
23
+ return undefined
24
+ }
25
+ }
26
+
27
+ function jobIdOf(exec: ToolExecution): string | undefined {
28
+ if (!exec.arguments || typeof exec.arguments !== 'object') return undefined
29
+ const jobId = (exec.arguments as Record<string, unknown>).job_id
30
+ return typeof jobId === 'string' && jobId.length > 0 ? jobId : undefined
31
+ }
32
+
33
+ function isContinuableChild(entry: SubagentListEntry, jobId: string): boolean {
34
+ return entry.kind === 'child' && entry.mode === 'continuable' && entry.id === jobId
35
+ }
36
+
37
+ function recoveryReason(toolName: string, jobId: string): string {
38
+ const stopHint = toolName === 'job_kill'
39
+ ? '; use interrupt_agent to stop its current turn.'
40
+ : '.'
41
+ return [
42
+ `SUBAGENT_ID_IS_NOT_JOB_ID: ${JSON.stringify(jobId)} is a continuable subagent durable id, not a background job id.`,
43
+ 'Its completion arrives as a completion notice; use list_agents to inspect it and send_message to continue it',
44
+ stopHint,
45
+ ].join(' ')
46
+ }
47
+
48
+ /** Install the GoTry-owned safety net around dsh's jobs tools. */
49
+ export function installSubagentJobIdGuard(ctx: Context): void {
50
+ const ctxOn = (ctx as unknown as { on?: typeof ctx.on }).on
51
+ if (typeof ctxOn !== 'function') return
52
+ ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
53
+ if (!JOB_TOOLS.has(exec.name)) return next()
54
+ const jobId = jobIdOf(exec)
55
+ const agent = exec.agent as Agent | undefined
56
+ const subagents = subagentsOf(ctx)
57
+ if (!jobId || !agent || !subagents) return next()
58
+
59
+ let children: SubagentListEntry[]
60
+ try {
61
+ children = await subagents.listChildren(agent.id as SessionId, exec.signal)
62
+ } catch (error) {
63
+ // The guard is diagnostic, not an alternate ownership or availability
64
+ // path. Preserve cancellation; otherwise retain dsh's native job error.
65
+ if (exec.signal.aborted) throw error
66
+ return next()
67
+ }
68
+
69
+ if (!children.some(entry => isContinuableChild(entry, jobId))) return next()
70
+ return { kind: 'deny', reason: recoveryReason(exec.name, jobId) }
71
+ }, { prepend: true })
72
+ }
@@ -7,8 +7,10 @@
7
7
  * - legacy 抽取路径:dsh-llm.ts 注入 FACTS/SKELETON/槽位抽取 prompt;
8
8
  * - 评测:time-eval-tests.ts 用固定 now(2026-08-27)保证确定性。
9
9
  *
10
- * 债务(architecture §10 登记):节日锚点表 SPRING_FESTIVAL 硬编码(2026-08-28 扩至 2031);
11
- * 2031 前必须再扩表,否则春节锚点静默缺失。
10
+ * 节日锚点表 SPRING_FESTIVAL:由构建期生成器机械生成(issue #274 清偿 D-9 手抄债务,覆盖 2026-2099);
11
+ * 扩表/换库 = 重跑 ts/scripts/gen-lunar-anchors.ts(--check 为漂移闸,run-all §58)。
12
+ * 到期即红守卫 springFestivalHorizonOk:最晚锚点年份 < 当前年+3 时测试红;表耗尽后锚点卡
13
+ * 显式告警(不再静默省略)——「换个日子的同款债务」的静默耗尽模式已消灭。
12
14
  */
13
15
 
14
16
  export interface TimeAnchor {
@@ -16,12 +18,30 @@ export interface TimeAnchor {
16
18
  today: string
17
19
  /** 今天星期几,如「周四」 */
18
20
  todayWeekdayZh: string
19
- /** 时区标注,如 UTC+8 / UTC+5:30 */
21
+ /** 时区标注,固定为 UTC±HH:MM,如 UTC+08:00 / UTC+05:30 */
20
22
  tzLabel: string
21
23
  /** 注入 prompt 的多行锚点卡文本 */
22
24
  card: string
23
25
  }
24
26
 
27
+ export type PlanningDateIntent = 'future' | 'historical'
28
+
29
+ /**
30
+ * Named-year planning context. It is derived per turn from the current
31
+ * TimeAnchor; it is deliberately not part of TripState so a later turn cannot
32
+ * inherit a stale planning reference date.
33
+ */
34
+ export interface PlanningWindow {
35
+ referenceDate: string
36
+ requestedYear: number
37
+ intent: PlanningDateIntent
38
+ }
39
+
40
+ export interface PlanningWindowBounds {
41
+ start: string
42
+ end: string
43
+ }
44
+
25
45
  const WEEKDAYS_ZH = ['日', '一', '二', '三', '四', '五', '六'] as const
26
46
 
27
47
  export function ymd(d: Date): string {
@@ -37,23 +57,116 @@ function weekMonday(d: Date): Date {
37
57
  return addDays(d, -((d.getDay() + 6) % 7))
38
58
  }
39
59
 
40
- /** 春节锚点表(仅此节日漂移大,硬编码;元旦/国庆固定月日,按「下一次发生」算)。D-9:2026-08-28 扩至 2031 */
41
- const SPRING_FESTIVAL: Record<number, string> = {
60
+ /**
61
+ * 春节锚点表(正月初一;仅此节日漂移大,故表驱动;元旦/国庆固定月日按「下一次发生」算)。
62
+ * 本块由构建期生成器机械生成,禁止手改(issue #274:手抄常量表=换个日子的同款债务,已否决)。
63
+ * provenance:lunar-typescript@1.8.6(MIT,零传递依赖,devDependency 不进 runtime)· 生成时间 2026-09-11T17:19:45.752Z
64
+ * 命令:cd ts && npx tsx scripts/gen-lunar-anchors.ts(扩表/换库=重跑;--check=漂移闸,run-all §58)
65
+ * 交叉验证:2026-2031 旧表 6 条 + 港天文台 2032-2040 核实 9 条逐条一致(time-eval §7 双 oracle)。
66
+ * 到期守卫:springFestivalHorizonOk——最晚锚点年份 < 当前年+3 测试即红(临近耗尽先红于静默缺失)。
67
+ */
68
+ // [gen:lunar-anchors:begin] 生成块起点——手改会被下次生成覆盖
69
+ export const SPRING_FESTIVAL: Record<number, string> = {
42
70
  2026: '2026-02-17',
43
71
  2027: '2027-02-06',
44
72
  2028: '2028-01-26',
45
73
  2029: '2029-02-13',
46
74
  2030: '2030-02-03',
47
75
  2031: '2031-01-23',
76
+ 2032: '2032-02-11',
77
+ 2033: '2033-01-31',
78
+ 2034: '2034-02-19',
79
+ 2035: '2035-02-08',
80
+ 2036: '2036-01-28',
81
+ 2037: '2037-02-15',
82
+ 2038: '2038-02-04',
83
+ 2039: '2039-01-24',
84
+ 2040: '2040-02-12',
85
+ 2041: '2041-02-01',
86
+ 2042: '2042-01-22',
87
+ 2043: '2043-02-10',
88
+ 2044: '2044-01-30',
89
+ 2045: '2045-02-17',
90
+ 2046: '2046-02-06',
91
+ 2047: '2047-01-26',
92
+ 2048: '2048-02-14',
93
+ 2049: '2049-02-02',
94
+ 2050: '2050-01-23',
95
+ 2051: '2051-02-11',
96
+ 2052: '2052-02-01',
97
+ 2053: '2053-02-19',
98
+ 2054: '2054-02-08',
99
+ 2055: '2055-01-28',
100
+ 2056: '2056-02-15',
101
+ 2057: '2057-02-04',
102
+ 2058: '2058-01-24',
103
+ 2059: '2059-02-12',
104
+ 2060: '2060-02-02',
105
+ 2061: '2061-01-21',
106
+ 2062: '2062-02-09',
107
+ 2063: '2063-01-29',
108
+ 2064: '2064-02-17',
109
+ 2065: '2065-02-05',
110
+ 2066: '2066-01-26',
111
+ 2067: '2067-02-14',
112
+ 2068: '2068-02-03',
113
+ 2069: '2069-01-23',
114
+ 2070: '2070-02-11',
115
+ 2071: '2071-01-31',
116
+ 2072: '2072-02-19',
117
+ 2073: '2073-02-07',
118
+ 2074: '2074-01-27',
119
+ 2075: '2075-02-15',
120
+ 2076: '2076-02-05',
121
+ 2077: '2077-01-24',
122
+ 2078: '2078-02-12',
123
+ 2079: '2079-02-02',
124
+ 2080: '2080-01-22',
125
+ 2081: '2081-02-09',
126
+ 2082: '2082-01-29',
127
+ 2083: '2083-02-17',
128
+ 2084: '2084-02-06',
129
+ 2085: '2085-01-26',
130
+ 2086: '2086-02-14',
131
+ 2087: '2087-02-03',
132
+ 2088: '2088-01-24',
133
+ 2089: '2089-02-10',
134
+ 2090: '2090-01-30',
135
+ 2091: '2091-02-18',
136
+ 2092: '2092-02-07',
137
+ 2093: '2093-01-27',
138
+ 2094: '2094-02-15',
139
+ 2095: '2095-02-05',
140
+ 2096: '2096-01-25',
141
+ 2097: '2097-02-12',
142
+ 2098: '2098-02-01',
143
+ 2099: '2099-01-21',
48
144
  }
145
+ // [gen:lunar-anchors:end]
49
146
 
50
- function tzLabelOf(d: Date): string {
51
- const offsetMin = -d.getTimezoneOffset()
52
- const sign = offsetMin >= 0 ? '+' : '-'
53
- const abs = Math.abs(offsetMin)
147
+ /** 到期即红守卫阈值(issue #274):最晚春节锚点年份必须 当前年+3——临近耗尽先红于静默缺失。 */
148
+ export const SPRING_FESTIVAL_HORIZON_MIN_YEARS = 3
149
+
150
+ /** 表内最晚锚点年份距 now 的年数(守卫与 time-eval §7 共用;旧表「牙齿证明」传表参)。 */
151
+ export function springFestivalHorizonYears(table: Record<number, string>, now: Date = new Date()): number {
152
+ return Math.max(...Object.keys(table).map(Number)) - now.getFullYear()
153
+ }
154
+
155
+ /** D-9 到期即红守卫:最晚锚点年份 < 当前年+3 → 测试红(而非春节锚点静默缺失)。 */
156
+ export function springFestivalHorizonOk(now: Date = new Date()): boolean {
157
+ return springFestivalHorizonYears(SPRING_FESTIVAL, now) >= SPRING_FESTIVAL_HORIZON_MIN_YEARS
158
+ }
159
+
160
+ export function formatUtcOffsetLabel(utcOffsetMinutes: number): string {
161
+ const sign = utcOffsetMinutes >= 0 ? '+' : '-'
162
+ const abs = Math.abs(utcOffsetMinutes)
54
163
  const h = Math.floor(abs / 60)
55
164
  const m = abs % 60
56
- return `UTC${sign}${m === 0 ? h : `${h}:${String(m).padStart(2, '0')}`}`
165
+ return `UTC${sign}${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
166
+ }
167
+
168
+ function tzLabelOf(d: Date): string {
169
+ return formatUtcOffsetLabel(-d.getTimezoneOffset())
57
170
  }
58
171
 
59
172
  function weekdayOf(d: Date): string {
@@ -101,6 +214,7 @@ export function buildTimeAnchor(now: Date = new Date()): TimeAnchor {
101
214
  ]
102
215
  const spring = Object.values(SPRING_FESTIVAL).filter(d => d >= today).sort()[0]
103
216
  if (spring) festivals.push(`春节 ${spring}`)
217
+ else festivals.push('春节(锚点表已到期:重跑 ts/scripts/gen-lunar-anchors.ts 扩表,勿静默省略)')
104
218
 
105
219
  const card = [
106
220
  `今天 ${today} ${weekdayOf(now)}(时区 ${tzLabelOf(now)})`,
@@ -116,19 +230,109 @@ export function buildTimeAnchor(now: Date = new Date()): TimeAnchor {
116
230
  return { today, todayWeekdayZh: weekdayOf(now), tzLabel: tzLabelOf(now), card }
117
231
  }
118
232
 
233
+ /**
234
+ * Extract the smallest named-year intent needed by the planning guard.
235
+ * Relative future years (今年/明年/后年/年内) resolve against the current
236
+ * TimeAnchor, so a future-tense phrase without a four-digit year still yields
237
+ * a named-year window. Historical bypass is intentionally narrow: a negated
238
+ * warning about past dates is not historical lookup, and multiple eligible
239
+ * years are ambiguous. Same-year restatements (「今年年内」「2026 年内」) are
240
+ * one window, not a conflict.
241
+ */
242
+ export function parsePlanningWindow(text: string, anchor: TimeAnchor): PlanningWindow | null {
243
+ const historical = /(?:回测|复盘|backtest|historical)|(?:历史|过去).{0,8}(?:查询|查|检索|回看|回测|复盘)|(?:查询|查|检索|回看).{0,8}(?:历史|过去)/i.test(text)
244
+ const anchorYear = Number(anchor.today.slice(0, 4))
245
+ const relativeYear = (token: string): number =>
246
+ token === '今年' ? anchorYear : token === '明年' ? anchorYear + 1 : anchorYear + 2
247
+ const matches: Array<{ year: number; start: number; end: number }> = []
248
+ for (const m of text.matchAll(/(?<!\d)(\d{4})\s*年/g)) {
249
+ const start = m.index ?? 0
250
+ matches.push({ year: Number(m[1]), start, end: start + m[0].length })
251
+ }
252
+ for (const m of text.matchAll(/\b(?:in|within|by)\s+(\d{4})\b/gi)) {
253
+ const start = m.index ?? 0
254
+ matches.push({ year: Number(m[1]), start, end: start + m[0].length })
255
+ }
256
+ for (const m of text.matchAll(/今年|明年|后年/g)) {
257
+ const start = m.index ?? 0
258
+ matches.push({ year: relativeYear(m[0]), start, end: start + m[0].length })
259
+ }
260
+ for (const m of text.matchAll(/年内/g)) {
261
+ const start = m.index ?? 0
262
+ // 「明年年内」「2025 年内」继承紧邻前缀的年份(「年」可已被年份 token 消费);裸「年内」= 锚点当前年
263
+ const adjacent = text.slice(0, start).match(/(今年|明年|后年|\d{4})\s*年?\s*$/)
264
+ matches.push({ year: adjacent ? (/^\d{4}$/.test(adjacent[1]!) ? Number(adjacent[1]) : relativeYear(adjacent[1]!)) : anchorYear, start, end: start + m[0].length })
265
+ }
266
+ if (matches.some(m => !Number.isInteger(m.year) || m.year < 1_000 || m.year > 9_999)) return null
267
+ const distinct = [...new Map(matches.map(m => [m.year, m])).values()]
268
+ if (historical && distinct.length !== 1) return null
269
+
270
+ const planningCue = /计划|规划|安排|打算|希望|想在|要在|完成|出行|旅行|预订|预定/i
271
+ const eligibleYears = new Set(
272
+ matches
273
+ .filter(m => {
274
+ const before = text.slice(Math.max(0, m.start - 24), m.start)
275
+ const after = text.slice(m.end, m.end + 24)
276
+ return planningCue.test(before) || /^\s*(?:内|以内|完成|出行|旅行|预订|预定)/i.test(after)
277
+ })
278
+ .map(m => m.year),
279
+ )
280
+ const selectedYear = eligibleYears.size === 1 ? [...eligibleYears][0]!
281
+ : (historical && distinct.length === 1 ? distinct[0]!.year : null)
282
+ if (selectedYear === null) return null
283
+ return { referenceDate: anchor.today, requestedYear: selectedYear, intent: historical ? 'historical' : 'future' }
284
+ }
285
+
286
+ export function planningWindowBounds(window: PlanningWindow, anchor: TimeAnchor): PlanningWindowBounds | null {
287
+ if (window.intent === 'historical') return null
288
+ const currentYear = Number(anchor.today.slice(0, 4))
289
+ if (window.requestedYear < currentYear) return null
290
+ const year = String(window.requestedYear).padStart(4, '0')
291
+ return {
292
+ start: window.requestedYear === currentYear ? anchor.today : `${year}-01-01`,
293
+ end: `${year}-12-31`,
294
+ }
295
+ }
296
+
297
+ /** 严格 ISO 校验:YYYY-MM-DD 必须落在真实日历(2026-02-30/2026-13-01 一律拒)。
298
+ * 复用 Date 月末自动进位:同年/月/day 还原回同一串 = 真日历;否则越界。
299
+ * 同时拒掉非有限值(NaN/Infinity)与越界月日(1-12/1-31)。
300
+ * 供 parseAbsoluteDate 词表口径 + hotel-date-gate 终消费层 复用。 */
301
+ export function isRealIsoDate(y: number, m: number, d: number): boolean {
302
+ if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return false
303
+ if (m < 1 || m > 12) return false
304
+ if (d < 1 || d > 31) return false
305
+ const dt = new Date(y, m - 1, d)
306
+ return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d
307
+ }
308
+
119
309
  /**
120
310
  * 绝对月日表达 → YYYY-MM-DD(年缺省时按锚点年)。只识别**绝对**表达:
121
311
  * ISO(2026-08-01)、数字点分(8.1 / 9.10)、中文(8月5日 / 8月5号)。
312
+ * 严格校验真日历存在性:2026-02-30 / 2026-13-01 / 2025-02-29 等一律返回 null——
313
+ * 词表外不一致日期不冒充合法,闸会归 unresolved 拒绝发供应商查询(issue #283 红线)。
122
314
  * 相对/模糊表达(下周一/明天/本周三/下个月中旬/近期/Aug-5 英文月名)一律返回 null——
123
315
  * 逐字保留给下游,本层不做换算也不做过期判定(与评测 golden 对齐:英文月日不判过期)。
124
316
  */
125
317
  export function parseAbsoluteDate(expr: string, anchorYear: number): string | null {
126
318
  const t = expr.trim()
127
319
  let m = t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/)
128
- if (m) return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}`
320
+ if (m) {
321
+ const y = Number(m[1]); const mo = Number(m[2]); const d = Number(m[3])
322
+ if (!isRealIsoDate(y, mo, d)) return null
323
+ return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}`
324
+ }
129
325
  m = t.match(/^(\d{1,2})[./](\d{1,2})(?:日|号)?$/)
130
- if (m) return `${anchorYear}-${m[1].padStart(2, '0')}-${m[2].padStart(2, '0')}`
326
+ if (m) {
327
+ const mo = Number(m[1]); const d = Number(m[2])
328
+ if (!isRealIsoDate(anchorYear, mo, d)) return null
329
+ return `${anchorYear}-${m[1].padStart(2, '0')}-${m[2].padStart(2, '0')}`
330
+ }
131
331
  m = t.match(/^(\d{1,2})月(\d{1,2})[日号]?$/)
132
- if (m) return `${anchorYear}-${m[1].padStart(2, '0')}-${m[2].padStart(2, '0')}`
332
+ if (m) {
333
+ const mo = Number(m[1]); const d = Number(m[2])
334
+ if (!isRealIsoDate(anchorYear, mo, d)) return null
335
+ return `${anchorYear}-${m[1].padStart(2, '0')}-${m[2].padStart(2, '0')}`
336
+ }
133
337
  return null
134
338
  }