@danceiny/gotry 0.0.1-rc.8 → 0.2.0-rc.27

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 (506) hide show
  1. package/README.md +172 -217
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1744 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -49
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +135 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +546 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +968 -0
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +509 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/resilience.js +191 -0
  41. package/dist/capabilities/session/action-cache.js +126 -0
  42. package/dist/capabilities/session/adapters/ctrip-flight.js +141 -0
  43. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  44. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  45. package/dist/capabilities/session/adapters/meituan-local.js +54 -0
  46. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  47. package/dist/capabilities/session/benchmark.js +189 -0
  48. package/dist/capabilities/session/bridge-ledger.js +224 -0
  49. package/dist/capabilities/session/extension-bridge.js +636 -0
  50. package/dist/capabilities/session/extension-channel.js +109 -0
  51. package/dist/capabilities/session/extension-distribution.js +230 -0
  52. package/dist/capabilities/session/extract.js +50 -0
  53. package/dist/capabilities/session/golden-score.js +114 -0
  54. package/dist/capabilities/session/health-watch.js +215 -0
  55. package/dist/capabilities/session/read-guard.js +107 -0
  56. package/dist/capabilities/session/static-flight-golden.js +150 -0
  57. package/dist/capabilities/session/transport.js +109 -0
  58. package/dist/capabilities/session/wizard.js +117 -0
  59. package/dist/capabilities/session-consent.js +178 -0
  60. package/dist/capabilities/session-login.js +170 -0
  61. package/dist/capabilities/session-search.js +686 -0
  62. package/dist/capabilities/spawn-bounded.js +203 -0
  63. package/dist/capabilities/sponsor-plugin.js +341 -0
  64. package/dist/capabilities/visa-policy.js +140 -0
  65. package/dist/capabilities/weather.js +243 -138
  66. package/dist/data/session-golden-20.json +25 -0
  67. package/dist/data/sf-golden-manifest.json +103 -0
  68. package/dist/data/sf-static-routes.json +91 -0
  69. package/dist/scripts/action-cache-tests.js +72 -0
  70. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  71. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  72. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  73. package/dist/scripts/agent-reach-error-tests.js +354 -0
  74. package/dist/scripts/agent-reach-tests.js +26 -18
  75. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  76. package/dist/scripts/anything-tests.js +116 -60
  77. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  78. package/dist/scripts/artifacts-capability-tests.js +727 -0
  79. package/dist/scripts/async-collect.js +68 -10
  80. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  81. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  82. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  83. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  84. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  85. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  86. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  87. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  88. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  89. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  90. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  91. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
  92. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  93. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  94. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  95. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  96. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  97. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  98. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  99. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  100. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  101. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  102. package/dist/scripts/booking-executor-tests.js +80 -0
  103. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  104. package/dist/scripts/booking-saga-tests.js +125 -0
  105. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  106. package/dist/scripts/bootstrap-tests.js +1479 -0
  107. package/dist/scripts/bridge-events-tests.js +239 -0
  108. package/dist/scripts/bridge-ledger-tests.js +428 -0
  109. package/dist/scripts/build-changelog.js +238 -0
  110. package/dist/scripts/build-metrics-report.js +331 -0
  111. package/dist/scripts/changelog-tests.js +118 -0
  112. package/dist/scripts/channel-probe-tests.js +103 -0
  113. package/dist/scripts/channel-probe.js +169 -0
  114. package/dist/scripts/channel-registry-tests.js +208 -0
  115. package/dist/scripts/companion-tests.js +67 -0
  116. package/dist/scripts/dida-cookie-probe.js +87 -0
  117. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  118. package/dist/scripts/dida-sw-probe.js +114 -0
  119. package/dist/scripts/diff-test.js +17 -12
  120. package/dist/scripts/doctor-tests.js +564 -0
  121. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  122. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  123. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  124. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  125. package/dist/scripts/dsh-runtime-closure.js +162 -0
  126. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  127. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  128. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  129. package/dist/scripts/effect-tests.js +539 -0
  130. package/dist/scripts/engine-run.js +1 -3
  131. package/dist/scripts/engine-tests.js +16 -10
  132. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  133. package/dist/scripts/evaluation-contract-tests.js +268 -0
  134. package/dist/scripts/extension-distribution-cli.js +39 -0
  135. package/dist/scripts/extension-distribution-tests.js +443 -0
  136. package/dist/scripts/extension-tests.js +988 -0
  137. package/dist/scripts/external-event-tests.js +350 -0
  138. package/dist/scripts/fact-gate-tests.js +869 -0
  139. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  140. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  141. package/dist/scripts/flight-malformed-tests.js +159 -0
  142. package/dist/scripts/flyai-setup-tests.js +228 -0
  143. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  144. package/dist/scripts/flyai-tests.js +692 -0
  145. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  146. package/dist/scripts/fx-contract-tests.js +201 -0
  147. package/dist/scripts/g5-guard-tests.js +131 -0
  148. package/dist/scripts/g5-guard.js +189 -0
  149. package/dist/scripts/gen-lunar-anchors.js +106 -0
  150. package/dist/scripts/geo-atlas-tests.js +283 -0
  151. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  152. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  153. package/dist/scripts/gotry-try-demo.js +49 -0
  154. package/dist/scripts/ground-transfer-tests.js +956 -0
  155. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  156. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  157. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  158. package/dist/scripts/hbcli-tests.js +121 -69
  159. package/dist/scripts/health-watch-cli.js +73 -0
  160. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  161. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  162. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  163. package/dist/scripts/i18n-tests.js +41 -0
  164. package/dist/scripts/incident-tests.js +281 -87
  165. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  166. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  167. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  168. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  169. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  170. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  171. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  172. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  173. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  174. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  175. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  176. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  177. package/dist/scripts/itinerary-deck-tests.js +435 -0
  178. package/dist/scripts/itinerary-html-tests.js +426 -0
  179. package/dist/scripts/journey-tests.js +22 -27
  180. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  181. package/dist/scripts/kernel-manifest-gate.js +126 -0
  182. package/dist/scripts/kernel-manifest-tests.js +212 -0
  183. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  184. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  185. package/dist/scripts/kernel-manifest.js +369 -0
  186. package/dist/scripts/lavish-local-tests.js +1226 -0
  187. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  188. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  189. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  190. package/dist/scripts/ledger-tests.js +480 -0
  191. package/dist/scripts/ledger-workflow-crash.js +32 -0
  192. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  193. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  194. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  195. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  196. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  197. package/dist/scripts/memory-capture-tests.js +242 -61
  198. package/dist/scripts/memory-decay-tests.js +49 -0
  199. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  200. package/dist/scripts/memory-lifecycle.js +196 -0
  201. package/dist/scripts/memory-metrics.js +29 -31
  202. package/dist/scripts/memory-value-report.js +631 -0
  203. package/dist/scripts/memory-value-tests.js +328 -0
  204. package/dist/scripts/metrics-report-tests.js +151 -0
  205. package/dist/scripts/model-override-e2e.js +137 -0
  206. package/dist/scripts/nightly-evidence-tests.js +161 -0
  207. package/dist/scripts/nightly-evidence.js +286 -0
  208. package/dist/scripts/nudge-digest.js +43 -42
  209. package/dist/scripts/observability-tests.js +381 -0
  210. package/dist/scripts/onboarding-tests.js +161 -0
  211. package/dist/scripts/opensky-check.js +18 -13
  212. package/dist/scripts/opensky-tests.js +28 -28
  213. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  214. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  215. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  216. package/dist/scripts/price-drift-tests.js +340 -0
  217. package/dist/scripts/price-drift-watch.js +447 -0
  218. package/dist/scripts/probe-poi-tests.js +53 -45
  219. package/dist/scripts/product-metrics.js +483 -0
  220. package/dist/scripts/publish-preverify.js +110 -0
  221. package/dist/scripts/realtime-pricing-tests.js +76 -0
  222. package/dist/scripts/recall-tests.js +513 -0
  223. package/dist/scripts/replay-async.js +20 -18
  224. package/dist/scripts/replay-real.js +18 -18
  225. package/dist/scripts/replay.js +52 -82
  226. package/dist/scripts/session-attach-diagnose.js +31 -0
  227. package/dist/scripts/session-attach-poc.js +62 -0
  228. package/dist/scripts/session-benchmark.js +180 -0
  229. package/dist/scripts/session-dida-live-e2e.js +199 -0
  230. package/dist/scripts/session-extract-tests.js +60 -0
  231. package/dist/scripts/session-link-tests.js +478 -0
  232. package/dist/scripts/session-login.js +29 -0
  233. package/dist/scripts/session-tests.js +862 -0
  234. package/dist/scripts/session-zones-tests.js +358 -0
  235. package/dist/scripts/sf-live-benchmark.js +367 -0
  236. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  237. package/dist/scripts/sf-live-cli-tests.js +92 -0
  238. package/dist/scripts/sf-live-options.js +46 -0
  239. package/dist/scripts/sf-manifest-tests.js +30 -0
  240. package/dist/scripts/sf-manifest.js +33 -0
  241. package/dist/scripts/sf-soft-score-tests.js +67 -0
  242. package/dist/scripts/sf-summary-tests.js +324 -0
  243. package/dist/scripts/sf-summary.js +525 -0
  244. package/dist/scripts/share-tests.js +432 -0
  245. package/dist/scripts/skeleton-check.js +16 -14
  246. package/dist/scripts/skeleton-integration-test.js +28 -11
  247. package/dist/scripts/skills-contract-tests.js +51 -46
  248. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  249. package/dist/scripts/smoke.js +703 -219
  250. package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
  251. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  252. package/dist/scripts/state-cli-tests.js +261 -0
  253. package/dist/scripts/state-cli.js +585 -0
  254. package/dist/scripts/state-repair-cli.js +66 -0
  255. package/dist/scripts/state-repair-tests.js +140 -0
  256. package/dist/scripts/static-golden-tests.js +165 -0
  257. package/dist/scripts/time-eval-tests.js +210 -192
  258. package/dist/scripts/travel-timeline-tests.js +59 -0
  259. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  260. package/dist/scripts/turn-handoff-collect.js +161 -0
  261. package/dist/scripts/turn-policy-tests.js +55 -0
  262. package/dist/scripts/typed-contract-canary.js +176 -0
  263. package/dist/scripts/tz-resolver-sanity.js +50 -0
  264. package/dist/scripts/tz-resolver-tests.js +584 -0
  265. package/dist/scripts/unified-tests.js +23 -19
  266. package/dist/scripts/visa-policy-tests.js +62 -0
  267. package/dist/scripts/weather-tests.js +276 -41
  268. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  269. package/dist/scripts/write-gate-tests.js +522 -0
  270. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  271. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  272. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  273. package/dist/scripts/z3-race-tests.js +41 -0
  274. package/dist/src/artifact-gate.js +829 -0
  275. package/dist/src/backend/kernel.js +91 -0
  276. package/dist/src/backend/modules/booking-copilot.js +92 -0
  277. package/dist/src/backend/modules/booking-executor.js +124 -0
  278. package/dist/src/backend/modules/session-search.js +551 -0
  279. package/dist/src/benchmark-agent-conformance.js +578 -0
  280. package/dist/src/benchmark-environment-bridge.js +515 -0
  281. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  282. package/dist/src/benchmark-tool-isolation.js +151 -0
  283. package/dist/src/bookable-facts.js +533 -0
  284. package/dist/src/booking-saga.js +97 -0
  285. package/dist/src/booking-surface/availability-policy.js +697 -0
  286. package/dist/src/booking-surface/booking-intent.js +43 -0
  287. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  288. package/dist/src/booking-surface/canonical-schema.js +116 -0
  289. package/dist/src/booking-surface/contracts.js +75 -0
  290. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  291. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  292. package/dist/src/booking-surface/dsh-worker.js +98 -0
  293. package/dist/src/booking-surface/error-codes.js +231 -0
  294. package/dist/src/booking-surface/index.js +9 -0
  295. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  296. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  297. package/dist/src/booking-surface/profile.js +24 -0
  298. package/dist/src/booking-surface/recovery-chain.js +367 -0
  299. package/dist/src/booking-surface/runtime.js +1968 -0
  300. package/dist/src/booking-surface/server.js +674 -0
  301. package/dist/src/booking-surface/startup.js +168 -0
  302. package/dist/src/booking-surface/validation.js +261 -0
  303. package/dist/src/bridge.js +20 -13
  304. package/dist/src/companions.js +86 -0
  305. package/dist/src/contracts.js +21 -43
  306. package/dist/src/dsh-llm.js +97 -111
  307. package/dist/src/engine.js +276 -193
  308. package/dist/src/evaluation-cadence.js +97 -0
  309. package/dist/src/evaluation-contracts.js +283 -0
  310. package/dist/src/flight-pack-adapter.js +54 -0
  311. package/dist/src/flight-pack-contract.js +2 -0
  312. package/dist/src/flyai-setup-tool.js +70 -0
  313. package/dist/src/gotry-backend.js +125 -0
  314. package/dist/src/hotel-date-gate.js +118 -0
  315. package/dist/src/i18n.js +88 -0
  316. package/dist/src/index.js +2305 -731
  317. package/dist/src/itinerary-deck.js +139 -0
  318. package/dist/src/itinerary-doc-shared.js +661 -0
  319. package/dist/src/itinerary-html.js +131 -0
  320. package/dist/src/journey.js +155 -108
  321. package/dist/src/lavish-tools.js +400 -0
  322. package/dist/src/ledger-repair-apply.js +538 -0
  323. package/dist/src/ledger-repair-plan.js +463 -0
  324. package/dist/src/loop.js +358 -162
  325. package/dist/src/memory-capture.js +180 -32
  326. package/dist/src/memory-decay.js +47 -0
  327. package/dist/src/memory-lifecycle.js +1310 -0
  328. package/dist/src/memory-utility.js +27 -40
  329. package/dist/src/mock-llm.js +36 -61
  330. package/dist/src/model.js +81 -36
  331. package/dist/src/realtime-pricing.js +103 -0
  332. package/dist/src/recall/card.js +74 -0
  333. package/dist/src/recall/evaluator.js +140 -0
  334. package/dist/src/recall/integration.js +17 -0
  335. package/dist/src/recall/tick.js +166 -0
  336. package/dist/src/session-link/plan-it-action.js +48 -0
  337. package/dist/src/session-link/session-link.js +248 -0
  338. package/dist/src/session-zones.js +507 -0
  339. package/dist/src/share/adapters.js +51 -0
  340. package/dist/src/share/share-consent.js +76 -0
  341. package/dist/src/share/share-deck.js +58 -0
  342. package/dist/src/share/share-token.js +146 -0
  343. package/dist/src/slot-spec.js +84 -106
  344. package/dist/src/state-ledger.js +767 -0
  345. package/dist/src/state-repair.js +299 -0
  346. package/dist/src/subagent-job-id-guard.js +58 -0
  347. package/dist/src/time-anchor.js +231 -33
  348. package/dist/src/tool-packet.js +20 -8
  349. package/dist/src/travel-slots.js +58 -35
  350. package/dist/src/travel-timeline.js +59 -0
  351. package/dist/src/turn-deadline.js +287 -0
  352. package/dist/src/turn-policy.js +118 -0
  353. package/dist/src/tz-resolver.js +143 -0
  354. package/dist/src/unified.js +538 -360
  355. package/dist/src/wish-pool.js +42 -12
  356. package/dist/src/write-gate.js +495 -0
  357. package/dist/src/z3-shared.js +284 -0
  358. package/extension/README.md +21 -0
  359. package/extension/README.zh-CN.md +21 -0
  360. package/extension/background.js +463 -0
  361. package/extension/content-bridge.js +270 -0
  362. package/extension/content-main.js +111 -0
  363. package/extension/manifest.json +60 -0
  364. package/package.json +407 -14
  365. package/schemas/booking.intent.schema.json +193 -0
  366. package/schemas/booking.surface.schema.json +2622 -0
  367. package/ts/capabilities/agent-reach-deep.ts +114 -0
  368. package/ts/capabilities/agent-reach.ts +198 -0
  369. package/ts/capabilities/anything.ts +255 -0
  370. package/ts/capabilities/artifacts.ts +428 -0
  371. package/ts/capabilities/fact-log.ts +54 -0
  372. package/ts/capabilities/flyai-config.ts +388 -0
  373. package/ts/capabilities/flyai.ts +1274 -0
  374. package/ts/capabilities/hbcli.ts +253 -71
  375. package/ts/capabilities/incident-log.ts +88 -59
  376. package/ts/capabilities/itinerary-artifact.ts +214 -0
  377. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  378. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  379. package/ts/capabilities/opensky.ts +121 -0
  380. package/ts/capabilities/session/action-cache.ts +148 -0
  381. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  382. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  383. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  384. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  385. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  386. package/ts/capabilities/session/benchmark.ts +276 -0
  387. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  388. package/ts/capabilities/session/extension-bridge.ts +794 -0
  389. package/ts/capabilities/session/extension-channel.ts +144 -0
  390. package/ts/capabilities/session/extension-distribution.ts +266 -0
  391. package/ts/capabilities/session/extract.ts +54 -0
  392. package/ts/capabilities/session/golden-score.ts +139 -0
  393. package/ts/capabilities/session/health-watch.ts +264 -0
  394. package/ts/capabilities/session/read-guard.ts +141 -0
  395. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  396. package/ts/capabilities/session/transport.ts +146 -0
  397. package/ts/capabilities/session/wizard.ts +140 -0
  398. package/ts/capabilities/session-consent.ts +211 -0
  399. package/ts/capabilities/session-login.ts +208 -0
  400. package/ts/capabilities/session-search.ts +893 -0
  401. package/ts/capabilities/weather.ts +330 -0
  402. package/ts/dsh-runtime/vendor/README.md +68 -0
  403. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  404. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  405. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  406. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  407. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  408. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  409. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  410. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  411. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  412. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  413. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  414. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  415. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  416. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  417. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  418. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  419. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  420. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  421. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  422. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  423. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  424. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  425. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  426. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  427. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  428. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  440. package/ts/package.json +296 -3
  441. package/ts/scripts/async-collect.ts +91 -0
  442. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  443. package/ts/scripts/skeleton-check.ts +1 -1
  444. package/ts/scripts/skeleton-integration-test.ts +23 -1
  445. package/ts/scripts/state-cli.ts +568 -0
  446. package/ts/scripts/turn-handoff-collect.ts +178 -0
  447. package/ts/src/artifact-gate.ts +991 -0
  448. package/ts/src/benchmark-agent-conformance.ts +619 -0
  449. package/ts/src/benchmark-environment-bridge.ts +538 -0
  450. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  451. package/ts/src/benchmark-tool-isolation.ts +166 -0
  452. package/ts/src/bookable-facts.ts +837 -0
  453. package/ts/src/booking-saga.ts +133 -0
  454. package/ts/src/booking-surface/availability-policy.ts +522 -0
  455. package/ts/src/booking-surface/booking-intent.ts +86 -0
  456. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  457. package/ts/src/booking-surface/canonical-schema.js +116 -0
  458. package/ts/src/booking-surface/contracts.ts +475 -0
  459. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  460. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  461. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  462. package/ts/src/booking-surface/error-codes.ts +241 -0
  463. package/ts/src/booking-surface/index.ts +9 -0
  464. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  465. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  466. package/ts/src/booking-surface/profile.ts +42 -0
  467. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  468. package/ts/src/booking-surface/runtime.ts +1793 -0
  469. package/ts/src/booking-surface/server.ts +733 -0
  470. package/ts/src/booking-surface/startup.ts +201 -0
  471. package/ts/src/booking-surface/validation.ts +217 -0
  472. package/ts/src/companions.ts +113 -0
  473. package/ts/src/contracts.ts +13 -3
  474. package/ts/src/dsh-llm.ts +44 -11
  475. package/ts/src/flyai-setup-tool.ts +77 -0
  476. package/ts/src/index.ts +2107 -224
  477. package/ts/src/itinerary-deck.ts +189 -0
  478. package/ts/src/itinerary-doc-shared.ts +741 -0
  479. package/ts/src/itinerary-html.ts +171 -0
  480. package/ts/src/loop.ts +278 -40
  481. package/ts/src/memory-capture.ts +283 -0
  482. package/ts/src/memory-decay.ts +55 -0
  483. package/ts/src/memory-utility.ts +70 -0
  484. package/ts/src/mock-llm.ts +4 -8
  485. package/ts/src/model.ts +88 -2
  486. package/ts/src/recall/card.ts +106 -0
  487. package/ts/src/recall/evaluator.ts +170 -0
  488. package/ts/src/recall/integration.ts +35 -0
  489. package/ts/src/recall/tick.ts +213 -0
  490. package/ts/src/session-link/plan-it-action.ts +78 -0
  491. package/ts/src/session-link/session-link.ts +275 -0
  492. package/ts/src/share/adapters.ts +93 -0
  493. package/ts/src/share/share-consent.ts +105 -0
  494. package/ts/src/share/share-deck.ts +85 -0
  495. package/ts/src/share/share-token.ts +166 -0
  496. package/ts/src/slot-spec.ts +172 -0
  497. package/ts/src/state-ledger.ts +912 -0
  498. package/ts/src/subagent-job-id-guard.ts +72 -0
  499. package/ts/src/time-anchor.ts +338 -0
  500. package/ts/src/tool-packet.ts +32 -0
  501. package/ts/src/travel-slots.ts +219 -0
  502. package/ts/src/travel-timeline.ts +83 -0
  503. package/ts/src/turn-deadline.ts +366 -0
  504. package/ts/src/turn-policy.ts +154 -0
  505. package/ts/src/unified.ts +360 -89
  506. package/ts/src/wish-pool.ts +79 -0
@@ -0,0 +1,552 @@
1
+ /**
2
+ * 行程 deck 静态导出 bundle 注册工具 E2E(issue #568,Phase B 切片 3a;
3
+ * QR 真矩阵编码独立为 issue #569 切片 3b)。
4
+ *
5
+ * 真实执行面:在隔离临时 stateRoot 下 `apply(ctx, config)` 装载 gotry-tools 插件,
6
+ * 取**注册定义** `gotry_deck_export` 的 execute 直调,exec 上下文给出真实 target_dir。
7
+ * 事实经 `appendFacts` 写入隔离注册表,导出入口只能从那里取。
8
+ *
9
+ * 覆盖(对照切片 2 验收面 + bundle 三件齐落盘断言):
10
+ * 1. 空 fact_ids = 明确未核验 bundle:三件齐落盘,manifest 计数 0,无整体「已验证」徽章;
11
+ * 2. 选中 3 条事实:三件齐落盘;manifest.sha256 = 磁盘 deck sha256;bytes 与磁盘一致;
12
+ * source_tags/by_kind/evidence_chain 全部如实统计;
13
+ * 3. 未知 id / 重复 id / 超量 id 一律拒绝且零字节(target_dir 零新增文件);
14
+ * 4. 登记行畸形 → 渲染器拒绝,零字节;
15
+ * 5. 调用方自带 facts 不被接受;
16
+ * 6. bundle 已有任一文件(manifest.json / html / qr.svg)→ 拒绝覆盖,零字节;
17
+ * 7. 符号链接目标不被改写(target_dir 指向别处的链接即拒);
18
+ * 8. basename 路径分隔符/越级/超长全部拒绝,target_dir 外零文件;
19
+ * 9. target_dir 位于 .git/node_modules 段或不存在 → 拒绝;
20
+ * 10. 随机默认 bundle basename 符合契约且两次不同;确定性:同输入两次 bundle html 字节一致;
21
+ * 11. 畸形 title/itinerary/fact_ids → 拒绝且零字节;
22
+ * 12. target_dir 非法一律 fail closed(零字节,绝不回落 process cwd);
23
+ * 13. QR 真矩阵(issue #569 切片 3b 落地):`qr.svg` 是合法 SVG、含真实 QR 路径(path 元素 + viewBox + width/height);有 target_url 时编码它,无 target_url 时编码 local bundle 占位串;manifest.share_intent.qr = 'generated';
24
+ * 14. target_url 记录在 manifest;超长 URL 拒绝;
25
+ * 15. 真实链路:三件文件都被 `gotry_artifacts_list` 发现、`gotry_artifacts_read` 以 html
26
+ * 源码读回 deck html 与磁盘一致、sha256 与 manifest.deck.sha256 一致。
27
+ * 16. QR 独立解码验收(#569 T2):**实际落盘的 qr.svg** 经 sharp(libvips)光栅化 +
28
+ * jsqr(独立解码器,Apache-2.0,与编码侧 qrcode 库零共享代码)解码,逐字节还原
29
+ * payload——ASCII URL / UTF-8 URL / 字面本地占位串 × 128/240/480px 全矩阵;不同
30
+ * payload 经解码器还原后互不相等(负控制)。期望占位串在测试侧独立钉死为审批
31
+ * 字面量,§22r 把生产常量与之互锁(漂移即红)。
32
+ * 17. manifest.local_only 显式指示(#569 的 local_only 期望,以 v1 兼容附加字段落地):
33
+ * 无 target_url → true(绝不暗示线上目的地),有 target_url → false;与
34
+ * target_url 缺省、QR 本地占位串三态互锁。
35
+ * 全部合成事实,无用户真实行程;不启动 dsh 宿主、无网络、无供应商调用。
36
+ *
37
+ * 运行(在 ts/ 下):
38
+ * PATH=/opt/homebrew/opt/node@24/bin:$PATH npx tsx scripts/itinerary-deck-export-tests.ts
39
+ */
40
+ import assert from 'node:assert/strict';
41
+ import { createHash } from 'node:crypto';
42
+ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
43
+ import { tmpdir } from 'node:os';
44
+ import { join } from 'node:path';
45
+ import jsQRDefault from 'jsqr';
46
+ import sharp from 'sharp';
47
+ import { apply } from '../src/index.js';
48
+ import { BOOKABLE_FACT_SCHEMA, } from '../src/bookable-facts.js';
49
+ import { appendFacts, loadFactRegistry } from '../capabilities/fact-log.js';
50
+ import { generateItineraryDeckExport } from '../capabilities/itinerary-deck-export.js';
51
+ import { ITINERARY_DECK_EXPORT_BASENAME_RE, ITINERARY_DECK_EXPORT_HTML_SUFFIX, ITINERARY_DECK_EXPORT_LOCAL_MARKER, ITINERARY_DECK_EXPORT_MANIFEST_SUFFIX, ITINERARY_DECK_EXPORT_QR_SUFFIX, } from '../capabilities/itinerary-deck-export.js';
52
+ let pass = 0;
53
+ let fail = 0;
54
+ const failures = [];
55
+ // jsqr@1.4.0 的 d.ts 在本项目 skipLibCheck 下 default export 解析退化(default 被当成
56
+ // 模块命名空间);运行时(tsx/CJS interop)default 就是函数。这里显式钉住运行时契约。
57
+ const jsQR = jsQRDefault;
58
+ function ok(cond, msg) {
59
+ if (cond) {
60
+ pass++;
61
+ return;
62
+ }
63
+ fail++;
64
+ failures.push(msg);
65
+ throw new Error(`断言失败: ${msg}`);
66
+ }
67
+ const TITLE = '合成行程:深圳 → 普吉(切片 3a 夹具)';
68
+ const ITINERARY = {
69
+ trip_start: '2027-07-16',
70
+ trip_end: '2027-07-24',
71
+ stays: [
72
+ { place: '普吉岛', check_in: '2027-07-17', check_out: '2027-07-21' },
73
+ { place: '甲米', check_in: '2027-07-21', check_out: '2027-07-23' },
74
+ ],
75
+ od_segments: [
76
+ { from: '广州南', to: '深圳北', date: '2027-07-16', mode: 'rail', legs: 1 },
77
+ { from: 'SZX', to: 'HKT', date: '2027-07-17', mode: 'flight', legs: 1 },
78
+ { from: 'HKT', to: 'BKK', date: '2027-07-21', mode: 'flight', legs: 2 },
79
+ ],
80
+ };
81
+ const FLIGHT_ID = 'i568flight0001';
82
+ const HOTEL_ID = 'i568hotel0001';
83
+ const POLICY_ID = 'i568policy0001';
84
+ const FLIGHT = {
85
+ schema: BOOKABLE_FACT_SCHEMA,
86
+ fact_id: FLIGHT_ID,
87
+ kind: 'flight',
88
+ route: { origin: 'SZX', destination: 'HKT', origin_airport: 'SZX', dest_airport: 'HKT' },
89
+ date: '2027-07-17',
90
+ flight_no: 'CZ6061',
91
+ marketing_carrier: '南方航空',
92
+ dep_local: '09:20',
93
+ arr_local: '12:05',
94
+ nonstop: true,
95
+ price: 1680,
96
+ currency: 'CNY',
97
+ tier: 'live_inventory',
98
+ bookability: 'bookable_exact_date',
99
+ source: 'session:ctrip-flight',
100
+ query_id: 'session:ctrip-flight:flight:SZX-HKT:2027-07-17',
101
+ fetched_at: '2026-09-12T02:10:03.000Z',
102
+ as_of: '2026-09-12',
103
+ };
104
+ const HOTEL = {
105
+ schema: BOOKABLE_FACT_SCHEMA,
106
+ fact_id: HOTEL_ID,
107
+ kind: 'hotel',
108
+ destination: '普吉岛',
109
+ check_in: '2027-07-17',
110
+ check_out: '2027-07-21',
111
+ verdict: 'hit',
112
+ bookability: 'bookable_exact_date',
113
+ options_masked: 37,
114
+ source: 'flyai-hotel',
115
+ query_id: 'hotel:flyai-hotel:普吉岛:2027-07-17_2027-07-21',
116
+ fetched_at: '2026-09-12T02:20:00.000Z',
117
+ as_of: '2026-09-12',
118
+ tier: 'live_inventory',
119
+ };
120
+ const POLICY = {
121
+ schema: BOOKABLE_FACT_SCHEMA,
122
+ fact_id: POLICY_ID,
123
+ kind: 'policy',
124
+ subject: '泰国免签',
125
+ statement: '中国护照可免签入境停留不超过 30 天(以落地执行口径为准)',
126
+ source: 'flyai',
127
+ query_id: 'flyai:policy:泰国免签',
128
+ fetched_at: '2026-09-12T02:30:00.000Z',
129
+ as_of: '2026-09-12',
130
+ review_by: '2027-06-17',
131
+ };
132
+ function loadTools(stateRoot) {
133
+ assert.ok(stateRoot.startsWith(tmpdir()), 'stateRoot 必须是隔离临时根(绝不触真实 stateRoot)');
134
+ const registered = [];
135
+ const ctx = {
136
+ tools: { register: (t) => { registered.push(t); } },
137
+ systemPrompt: { variable: () => { } },
138
+ on: () => () => { },
139
+ };
140
+ apply(ctx, { stateRoot, timeoutMs: 30_000, hbcliBin: 'hbcli-not-on-path', sessionAccess: 'ask' });
141
+ return new Map(registered.map(t => [t.name, t]));
142
+ }
143
+ function bundleFiles(dir) {
144
+ return readdirSync(dir).sort();
145
+ }
146
+ function countOf(haystack, needle) {
147
+ return haystack.split(needle).length - 1;
148
+ }
149
+ /** 独立解码链(#569 T2):qr.svg 字节 → sharp(libvips)光栅化到 size×size → jsqr 解码。
150
+ * 解码器(jsqr)与编码器(qrcode 库)零共享代码;识别不出返回 null。
151
+ * 返回 data 为解码字符串,bytes 为原始 UTF-8 字节(QR byte 模式原样还原)。 */
152
+ async function decodeQrSvg(svgText, size) {
153
+ const raster = await sharp(Buffer.from(svgText, 'utf-8')).resize(size, size).raw().toBuffer({ resolveWithObject: true });
154
+ const pixels = new Uint8ClampedArray(raster.data);
155
+ const decoded = jsQR(pixels, raster.info.width, raster.info.height);
156
+ if (!decoded)
157
+ return null;
158
+ return { data: decoded.data, bytes: new Uint8Array(decoded.binaryData) };
159
+ }
160
+ async function main() {
161
+ const home = mkdtempSync(join(tmpdir(), 'gotry-568-export-'));
162
+ const stateRoot = join(home, 'state');
163
+ const cwd = join(home, 'workspace');
164
+ const outside = join(home, 'outside');
165
+ for (const d of [stateRoot, cwd, outside])
166
+ mkdirSync(d, { recursive: true });
167
+ const outsideFile = join(outside, 'target.html');
168
+ writeFileSync(outsideFile, 'DO NOT TOUCH', 'utf-8');
169
+ const tools = loadTools(stateRoot);
170
+ const tool = tools.get('gotry_deck_export');
171
+ ok(Boolean(tool), '§0a gotry_deck_export 必须已注册');
172
+ if (!tool) {
173
+ console.log('\nITINERARY DECK EXPORT TESTS: 0 pass, 1 fail');
174
+ console.log(' - 工具未注册');
175
+ process.exit(1);
176
+ }
177
+ const exec = { agent: { session: { header: { cwd } } } };
178
+ const run = async (args, execCtx = exec) => await tool.execute(args, execCtx);
179
+ // ---- §1 模型可见契约 ----
180
+ const params = tool.parameters;
181
+ ok(params.required?.includes('fact_ids') === true, '§1a fact_ids 必填');
182
+ ok(params.required?.includes('title') === true, '§1b title 必填');
183
+ ok(params.required?.includes('itinerary') === true, '§1c itinerary 必填');
184
+ ok(params.required?.includes('basename') !== true, '§1d basename 可选');
185
+ ok(params.required?.includes('target_url') !== true, '§1e target_url 可选');
186
+ ok(/registry/i.test(tool.description), '§1f 描述声明事实只从注册表取');
187
+ ok(/never overwrites/i.test(tool.description), '§1g 描述声明绝不覆盖');
188
+ ok(/manifest\.json/.test(tool.description), '§1h 描述声明产出 manifest.json');
189
+ ok(/qr\.svg/.test(tool.description), '§1i 描述声明产出 qr.svg');
190
+ ok(/real QR matrix/i.test(tool.description), '§1j 描述声明 qr 是真矩阵(#569)');
191
+ // ---- §2 事实登记进隔离注册表 ----
192
+ ok(await appendFacts(stateRoot, [FLIGHT, HOTEL, POLICY]), '§2a 合成事实写入隔离注册表');
193
+ const registry = await loadFactRegistry(stateRoot);
194
+ ok(registry.length === 3, `§2b 注册表读回 3 条(实测 ${registry.length})`);
195
+ ok(bundleFiles(cwd).length === 0, '§2c 导出前 target_dir 为空');
196
+ // ---- §3 空 fact_ids = 明确未核验 bundle ----
197
+ const empty = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [] });
198
+ ok(empty.ok === true, `§3a 空 fact_ids 合法(实际:${JSON.stringify(empty.error ?? '')})`);
199
+ ok(empty.overall_verified === false, '§3b 结果显式声明无整体「已验证」结论');
200
+ const emptyDir = String(empty.target_dir ?? '');
201
+ ok(realpathSync(emptyDir) === realpathSync(cwd), '§3c target_dir 解析到宿主给出的会话工作目录');
202
+ const emptyFiles = bundleFiles(emptyDir);
203
+ ok(emptyFiles.length === 3, `§3d bundle 三件齐落盘(实测 ${emptyFiles.length} 个文件: ${emptyFiles.join(',')})`);
204
+ const emptyManName = String(empty.basename) + ITINERARY_DECK_EXPORT_MANIFEST_SUFFIX;
205
+ const emptyQrName = String(empty.basename) + ITINERARY_DECK_EXPORT_QR_SUFFIX;
206
+ const emptyHtmlName = String(empty.basename) + ITINERARY_DECK_EXPORT_HTML_SUFFIX;
207
+ ok(emptyFiles.includes(emptyManName), `§3e 含 <basename>.manifest.json(${emptyManName})`);
208
+ ok(emptyFiles.includes(emptyHtmlName), `§3f 含 <basename>.html(${emptyHtmlName})`);
209
+ ok(emptyFiles.includes(emptyQrName), `§3g 含 <basename>.qr.svg(${emptyQrName})`);
210
+ // manifest schema + 字段如实
211
+ const emptyManifest = JSON.parse(readFileSync(String(empty.files?.manifest), 'utf-8'));
212
+ ok(emptyManifest.schema === 'gotry_deck_manifest.v1', '§3h manifest.schema = gotry_deck_manifest.v1');
213
+ ok(typeof emptyManifest.exported_at === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(emptyManifest.exported_at), '§3i manifest.exported_at 是 ISO 时间戳');
214
+ ok(emptyManifest.deck?.basename === emptyHtmlName, '§3j manifest.deck.basename 对得上落盘文件名');
215
+ ok(emptyManifest.deck?.sha256?.length === 64, '§3k manifest.deck.sha256 是 64 字符 hex');
216
+ const emptyHtmlDisk = readFileSync(String(empty.files?.html), 'utf-8');
217
+ const emptyHtmlSha = createHash('sha256').update(Buffer.from(emptyHtmlDisk, 'utf8')).digest('hex');
218
+ ok(emptyManifest.deck?.sha256 === emptyHtmlSha, '§3l manifest sha256 = 磁盘 deck sha256');
219
+ ok(emptyManifest.deck?.bytes === Buffer.byteLength(emptyHtmlDisk, 'utf8'), '§3m manifest bytes = 磁盘字节');
220
+ ok(emptyManifest.facts?.total === 0, '§3n facts.total = 0');
221
+ ok(emptyManifest.facts?.by_kind?.flight === 0 && emptyManifest.facts?.by_kind?.hotel === 0 && emptyManifest.facts?.by_kind?.policy === 0, '§3o facts.by_kind 全 0');
222
+ ok(emptyManifest.evidence_chain?.unverified === 0, '§3p evidence_chain.unverified = 0');
223
+ ok(emptyManifest.share_intent?.qr === 'generated', '§3q share_intent.qr = generated(#569 真矩阵)');
224
+ ok(emptyManifest.share_intent?.qr_path === emptyQrName, `§3r share_intent.qr_path = <basename>.qr.svg(${emptyQrName})`);
225
+ ok(emptyManifest.target_url === undefined, '§3s 未提供 target_url 时 manifest.target_url 缺省');
226
+ // ---- §4 选中 3 条事实:sha256 + 字节 + 分类计数 + source_tags ----
227
+ const selected = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [FLIGHT_ID, HOTEL_ID, POLICY_ID], basename: 'with-facts' });
228
+ ok(selected.ok === true, `§4a 选中 3 条成功(实际:${JSON.stringify(selected.error ?? '')})`);
229
+ const selDir = String(selected.target_dir ?? '');
230
+ // §3 已写入同 dir 的 bundle;§4 再写不会撞(不同 basename),dir 内文件叠加,只断言 §4 三件齐
231
+ ok(bundleFiles(selDir).includes(`${String(selected.basename)}${ITINERARY_DECK_EXPORT_HTML_SUFFIX}`)
232
+ && bundleFiles(selDir).includes(`${String(selected.basename)}${ITINERARY_DECK_EXPORT_MANIFEST_SUFFIX}`)
233
+ && bundleFiles(selDir).includes(`${String(selected.basename)}${ITINERARY_DECK_EXPORT_QR_SUFFIX}`), '§4b §4 bundle 三件齐 + 与 §3 不互撞(不同 basename)');
234
+ const selManifest = JSON.parse(readFileSync(String(selected.files?.manifest), 'utf-8'));
235
+ ok(selManifest.facts?.total === 3, `§4c facts.total = 3(实测 ${selManifest.facts?.total})`);
236
+ ok(selManifest.facts?.by_kind?.flight === 1 && selManifest.facts?.by_kind?.hotel === 1 && selManifest.facts?.by_kind?.policy === 1, '§4d facts.by_kind 各 1');
237
+ ok(selManifest.evidence_chain?.by_tier?.live_inventory === 2, '§4e evidence_chain.by_tier.live_inventory = 2(flight + hotel)');
238
+ ok(selManifest.evidence_chain?.by_bookability?.bookable_exact_date === 2, '§4f by_bookability.bookable_exact_date = 2');
239
+ const selHtmlDisk = readFileSync(String(selected.files?.html), 'utf-8');
240
+ const selHtmlSha = createHash('sha256').update(Buffer.from(selHtmlDisk, 'utf8')).digest('hex');
241
+ ok(selManifest.deck?.sha256 === selHtmlSha, '§4g sha256 与磁盘 deck 一致');
242
+ ok(selManifest.deck?.bytes === Buffer.byteLength(selHtmlDisk, 'utf8'), '§4h bytes 与磁盘字节一致');
243
+ ok(Array.isArray(selManifest.facts?.source_tags) && selManifest.facts.source_tags.includes('[session:ctrip-flight]') && selManifest.facts.source_tags.includes('[flyai-hotel]') && selManifest.facts.source_tags.includes('[flyai]'), '§4i source_tags 含三类源标识');
244
+ // ---- §5 target_url 记录在 manifest(可选) ----
245
+ const withUrl = await run({
246
+ title: TITLE,
247
+ itinerary: ITINERARY,
248
+ fact_ids: [],
249
+ basename: 'with-url',
250
+ target_url: 'https://example.com/decks/abc.html',
251
+ });
252
+ ok(withUrl.ok === true, '§5a 带 target_url 成功');
253
+ const urlManifest = JSON.parse(readFileSync(String(withUrl.files?.manifest), 'utf-8'));
254
+ ok(urlManifest.target_url === 'https://example.com/decks/abc.html', '§5b manifest.target_url 正确记录');
255
+ // ---- §6 未知 id 拒绝,零字节 ----
256
+ const before6 = bundleFiles(cwd).length;
257
+ const unknown = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: ['i568notthere'] });
258
+ ok(unknown.ok === false && String(unknown.error).includes('i568notthere'), '§6a 未知 id 显式拒绝并回显 id');
259
+ ok(bundleFiles(cwd).length === before6, '§6b 未知 id 零新增文件');
260
+ // ---- §7 重复 id 拒绝,零字节 ----
261
+ const dup = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [FLIGHT_ID, FLIGHT_ID] });
262
+ ok(dup.ok === false && /重复/.test(String(dup.error)), '§7a 重复 id 拒绝');
263
+ ok(bundleFiles(cwd).length === before6, '§7b 重复 id 零新增');
264
+ // ---- §8 超量 id 拒绝,零字节 ----
265
+ const many = Array.from({ length: 201 }, (_, i) => `i568bulk${i}`);
266
+ const over = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: many });
267
+ ok(over.ok === false && /容量上限/.test(String(over.error)), '§8a 超量 id 拒绝');
268
+ ok(bundleFiles(cwd).length === before6, '§8b 超量 id 零新增');
269
+ // ---- §9 登记行畸形 → 渲染器拒绝,零字节 ----
270
+ const malformed = { ...HOTEL, fact_id: 'i568malformed01', verdict: 'hit', options_masked: 'many' };
271
+ ok(await appendFacts(stateRoot, [malformed]), '§9a 畸形登记行写入注册表');
272
+ const malformedRun = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: ['i568malformed01'], basename: 'malformed' });
273
+ ok(malformedRun.ok === false, '§9b 畸形登记行不导出成功');
274
+ ok(Array.isArray(malformedRun.errors) && malformedRun.errors.length > 0, '§9c 畸形行给出结构化 errors');
275
+ ok(!bundleFiles(cwd).includes('malformed.html'), '§9d 畸形行零新增 bundle');
276
+ // ---- §10 调用方自带 facts 不被接受 ----
277
+ const forgedId = 'i568forged0001';
278
+ const callerFacts = await run({
279
+ title: TITLE,
280
+ itinerary: ITINERARY,
281
+ fact_ids: [],
282
+ facts: [{ schema: BOOKABLE_FACT_SCHEMA, fact_id: forgedId, kind: 'flight' }],
283
+ }, exec);
284
+ ok(callerFacts.ok === true, '§10a 额外的 facts 字段不影响空列表生成');
285
+ const cfHtml = readFileSync(String(callerFacts.files?.html), 'utf-8');
286
+ ok(!cfHtml.includes(forgedId), '§10b 调用方自带事实对象不被投影');
287
+ const forged = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [forgedId], basename: 'forged' });
288
+ ok(forged.ok === false, '§10c 伪造 id 按未知 id 拒绝');
289
+ ok(!bundleFiles(cwd).includes('forged.html'), '§10d 伪造 id 零新增 bundle');
290
+ // ---- §11 bundle 已有任一文件 → 拒绝覆盖,零字节 ----
291
+ const before11 = bundleFiles(cwd).length;
292
+ // 预先放一个 <basename>.manifest.json(模拟同 bundle 名残留)
293
+ const collisionDir = join(home, 'collision');
294
+ mkdirSync(collisionDir, { recursive: true });
295
+ const collisionBase = 'collide';
296
+ writeFileSync(join(collisionDir, `${collisionBase}${ITINERARY_DECK_EXPORT_MANIFEST_SUFFIX}`), 'preexisting', 'utf-8');
297
+ const collisionExec = { agent: { session: { header: { cwd: collisionDir } } } };
298
+ const collision = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: collisionBase }, collisionExec);
299
+ ok(collision.ok === false && /拒绝覆盖/.test(String(collision.error)), '§11a 已有 <basename>.manifest.json 拒绝覆盖');
300
+ ok(readFileSync(join(collisionDir, `${collisionBase}${ITINERARY_DECK_EXPORT_MANIFEST_SUFFIX}`), 'utf-8') === 'preexisting', '§11b 既有 manifest.json 字节未被改写');
301
+ ok(!bundleFiles(collisionDir).includes(`${collisionBase}${ITINERARY_DECK_EXPORT_HTML_SUFFIX}`), '§11c 拒绝时不补写 html');
302
+ // html 已存在
303
+ const collision2Dir = join(home, 'collision2');
304
+ mkdirSync(collision2Dir, { recursive: true });
305
+ const collision2Base = 'something';
306
+ writeFileSync(join(collision2Dir, `${collision2Base}${ITINERARY_DECK_EXPORT_HTML_SUFFIX}`), 'preexisting', 'utf-8');
307
+ const collision2Exec = { agent: { session: { header: { cwd: collision2Dir } } } };
308
+ const collision2 = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: collision2Base }, collision2Exec);
309
+ ok(collision2.ok === false && /拒绝覆盖/.test(String(collision2.error)), '§11d 已有 <basename>.html 拒绝覆盖');
310
+ ok(readFileSync(join(collision2Dir, `${collision2Base}${ITINERARY_DECK_EXPORT_HTML_SUFFIX}`), 'utf-8') === 'preexisting', '§11e 既有 html 字节未被改写');
311
+ // qr.svg 已存在
312
+ const collision3Dir = join(home, 'collision3');
313
+ mkdirSync(collision3Dir, { recursive: true });
314
+ const collision3Base = 'qr-base';
315
+ writeFileSync(join(collision3Dir, `${collision3Base}${ITINERARY_DECK_EXPORT_QR_SUFFIX}`), 'preexisting', 'utf-8');
316
+ const collision3Exec = { agent: { session: { header: { cwd: collision3Dir } } } };
317
+ const collision3 = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: collision3Base }, collision3Exec);
318
+ ok(collision3.ok === false && /拒绝覆盖/.test(String(collision3.error)), '§11f 已有 <basename>.qr.svg 拒绝覆盖');
319
+ ok(readFileSync(join(collision3Dir, `${collision3Base}${ITINERARY_DECK_EXPORT_QR_SUFFIX}`), 'utf-8') === 'preexisting', '§11g 既有 qr.svg 字节未被改写');
320
+ ok(bundleFiles(cwd).length === before11, '§11h 拒绝碰撞的主目录零新增');
321
+ // ---- §12 符号链接 target_dir 拒绝(指向别处) ----
322
+ const linkDir = join(home, 'linkdir');
323
+ symlinkSync(outside, linkDir);
324
+ const linkedExec = { agent: { session: { header: { cwd: linkDir } } } };
325
+ const linked = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: 'sym' }, linkedExec);
326
+ ok(linked.ok === false, '§12a 指向别处的符号链接 target_dir 拒绝');
327
+ ok(readFileSync(outsideFile, 'utf-8') === 'DO NOT TOUCH', '§12b 链接目标字节未被改写');
328
+ ok(bundleFiles(outside).length === 1, '§12c 仓外 bundle 目录零新增(只有预留的 target.html)');
329
+ // ---- §13 basename 路径与命名契约 ----
330
+ const before13 = bundleFiles(cwd).length;
331
+ const rejectNames = [
332
+ '../../evil',
333
+ 'sub/evil',
334
+ 'sub\\evil',
335
+ '../evil',
336
+ 'evil.html', // 含 . 不允许(basename 是 stem,后缀固定)
337
+ `prefix-${'a'.repeat(70)}`,
338
+ 'has space',
339
+ '',
340
+ ];
341
+ for (const name of rejectNames) {
342
+ const r = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: name });
343
+ ok(r.ok === false, `§13a basename 拒绝:${JSON.stringify(name)}`);
344
+ }
345
+ ok(bundleFiles(cwd).length === before13, `§13b 拒绝的 basename 未产生文件(实测 ${bundleFiles(cwd).length})`);
346
+ ok(!readdirSync(outside).includes('evil'), '§13c 仓外无越级文件');
347
+ ok(ITINERARY_DECK_EXPORT_BASENAME_RE.source.startsWith('^[A-Za-z0-9]'), '§13d 契约 regex 锁定 stem-only');
348
+ // ---- §14 target_dir 边界 ----
349
+ for (const bad of [join(stateRoot, 'node_modules'), join(home, '.git'), join(home, 'nope')]) {
350
+ mkdirSync(join(stateRoot, 'node_modules'), { recursive: true });
351
+ mkdirSync(join(home, '.git'), { recursive: true });
352
+ const r = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [] }, { agent: { session: { header: { cwd: bad } } } });
353
+ ok(r.ok === false, `§14a 受限/不存在的 target_dir 拒绝:${bad}`);
354
+ }
355
+ ok(readdirSync(join(home, '.git')).length === 0, '§14b .git 段零写入');
356
+ ok(readdirSync(join(stateRoot, 'node_modules')).length === 0, '§14c node_modules 段零写入');
357
+ // ---- §15 随机默认 bundle basename + 确定性 ----
358
+ const r1 = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: 'det-basename' });
359
+ const r2 = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: 'det-basename-2' });
360
+ ok(r1.ok === true && r2.ok === true, '§15a 显式 basename 导出成功');
361
+ ok(r1.basename !== r2.basename, '§15b 不同 basename 不互串');
362
+ const r3 = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [] });
363
+ ok(r3.ok === true, '§15c 默认随机 basename 导出成功');
364
+ ok(ITINERARY_DECK_EXPORT_BASENAME_RE.test(String(r3.basename)), '§15d 随机 basename 符合 stem 契约');
365
+ const r4 = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [] });
366
+ ok(r4.basename !== r3.basename, '§15e 连续两次随机 basename 不同');
367
+ // 确定性:同输入两次 html 字节一致
368
+ const detA = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [FLIGHT_ID, HOTEL_ID, POLICY_ID], basename: 'det-A' });
369
+ const detB = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [FLIGHT_ID, HOTEL_ID, POLICY_ID], basename: 'det-B' });
370
+ ok(detA.ok === true && detB.ok === true, '§15f 同输入连续导出成功');
371
+ ok(readFileSync(String(detA.files?.html), 'utf-8') === readFileSync(String(detB.files?.html), 'utf-8'), '§15g 同输入 deck html 字节一致');
372
+ // ---- §16 畸形输入拒绝,零字节 ----
373
+ const before16 = bundleFiles(cwd).length;
374
+ const invalid = [
375
+ { title: TITLE, itinerary: ITINERARY },
376
+ { title: TITLE, itinerary: ITINERARY, fact_ids: 'not-an-array' },
377
+ { title: TITLE, itinerary: ITINERARY, fact_ids: [42] },
378
+ { title: TITLE, itinerary: ITINERARY, fact_ids: [''] },
379
+ { itinerary: ITINERARY, fact_ids: [] },
380
+ { title: TITLE, fact_ids: [] },
381
+ { title: TITLE, itinerary: { ...ITINERARY, trip_start: '2027-02-30' }, fact_ids: [] },
382
+ { title: TITLE, itinerary: { ...ITINERARY, od_segments: [{ from: 'SZX', to: 'HKT', date: '2027-07-17', mode: 'teleport', legs: 1 }] }, fact_ids: [] },
383
+ ];
384
+ for (const [i, args] of invalid.entries()) {
385
+ const r = await run(args);
386
+ ok(r.ok === false, `§16a 畸形输入 ${i} 拒绝`);
387
+ }
388
+ ok(bundleFiles(cwd).length === before16, `§16b 畸形输入零新增(实测 ${bundleFiles(cwd).length})`);
389
+ // ---- §17 target_dir 非法一律 fail closed(零字节,绝不回落 process cwd) ----
390
+ const procCwdBefore = readdirSync(process.cwd()).sort().join(',');
391
+ const badExecs = [
392
+ ['缺 exec', undefined],
393
+ ['空对象 exec', {}],
394
+ ['无 session', { agent: {} }],
395
+ ['无 header', { agent: { session: {} } }],
396
+ ['header 无 cwd', { agent: { session: { header: {} } } }],
397
+ ['cwd 空串', { agent: { session: { header: { cwd: '' } } } }],
398
+ ['cwd 纯空白', { agent: { session: { header: { cwd: ' ' } } } }],
399
+ ['cwd 非字符串', { agent: { session: { header: { cwd: 42 } } } }],
400
+ ['cwd 相对路径', { agent: { session: { header: { cwd: 'relative/workspace' } } } }],
401
+ ];
402
+ for (const [label, badExec] of badExecs) {
403
+ const r = await tool.execute({ title: TITLE, itinerary: ITINERARY, fact_ids: [] }, badExec);
404
+ ok(r.ok === false, `§17a 非法 target_dir 拒绝:${label}`);
405
+ ok(/target_dir|会话工作目录/.test(String(r.error)), `§17b 拒绝原因是 target_dir/会话工作目录缺失/非法:${label}`);
406
+ }
407
+ ok(readdirSync(process.cwd()).sort().join(',') === procCwdBefore, '§17c 进程 cwd 零新增文件');
408
+ // ---- §17d 直接调用能力层同样 fail closed ----
409
+ for (const bad of ['', ' ', undefined]) {
410
+ const r = await generateItineraryDeckExport({ title: TITLE, itinerary: ITINERARY, fact_ids: [] }, { stateRoot, targetDir: bad });
411
+ ok(r.ok === false && /target_dir|会话工作目录/.test(String(r.error)), `§17d 能力层拒绝 target_dir=${JSON.stringify(bad)}`);
412
+ }
413
+ ok(readdirSync(process.cwd()).sort().join(',') === procCwdBefore, '§17e 能力层非法 target_dir 零写入进程 cwd');
414
+ // ---- §18 QR 真矩阵(issue #569 切片 3b):结构 + 编码内容校验 ----
415
+ const qrDisk = readFileSync(String(empty.files?.qr), 'utf-8');
416
+ ok(qrDisk.includes('<svg') && qrDisk.includes('</svg>'), '§18a qr.svg 是合法 SVG');
417
+ ok(qrDisk.includes('<path') || qrDisk.includes('<rect'), '§18b qr.svg 含 QR 模块元素(path 或 rect)');
418
+ ok(/viewBox=/.test(qrDisk), '§18c qr.svg 有 viewBox(浏览器/扫码 app 可缩放)');
419
+ ok(qrDisk.length > 600, `§18d qr.svg 是非平凡矩阵(占位 SVG 远短于此;实测 ${qrDisk.length} 字节)`);
420
+ // ---- §18e 有 target_url 时 QR 编码它;无时编码本地占位串(语义校验) ----
421
+ const withUrlQrDisk = readFileSync(String(withUrl.files?.qr), 'utf-8');
422
+ ok(withUrlQrDisk.length > 600, `§18e 有 target_url 的 bundle qr.svg 也是非平凡矩阵(${withUrlQrDisk.length} 字节)`);
423
+ // 语义校验(自检 review #3):不同 payload 必须产不同 QR——空 fact_ids→本地 marker vs
424
+ // 有 target_url→真 URL,字节必不同
425
+ ok(qrDisk !== withUrlQrDisk, `§18e2 不同 payload 的 QR 字节不同(${qrDisk.length} vs ${withUrlQrDisk.length};QR 必随编码内容变化)`);
426
+ // 同一个 target_url 两次生成 qr.svg 字节级一致(确定性)
427
+ const withUrl2 = await run({
428
+ title: TITLE,
429
+ itinerary: ITINERARY,
430
+ fact_ids: [],
431
+ basename: 'with-url-2',
432
+ target_url: 'https://example.com/decks/abc.html',
433
+ });
434
+ ok(withUrl2.ok === true, '§18f 同 target_url 重复导出成功');
435
+ const withUrl2QrDisk = readFileSync(String(withUrl2.files?.qr), 'utf-8');
436
+ ok(withUrl2QrDisk === withUrlQrDisk, '§18g 同 target_url 字节级一致(qrcode 确定性)');
437
+ // ---- §19 target_url 超长拒绝 ----
438
+ const longUrl = 'https://example.com/' + 'a'.repeat(2100);
439
+ const longUrlRun = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: 'long-url', target_url: longUrl });
440
+ ok(longUrlRun.ok === false && /target_url/.test(String(longUrlRun.error)), '§19a 超长 target_url 拒绝');
441
+ // 超出 QR 编码容量(但未超 target_url 长度上限)的输入:QR 先于任何写盘生成 → 零写入
442
+ // (#569 切片 3b 零写入保护回归:qrcode 库 reject 时三件一件都不落盘,重试不撞 EEXIST)
443
+ const overQrUrl = '普'.repeat(1000); // 1000 字符 ≤ 2048 上限;UTF-8 3000 字节 > QR v40-M 2331 上限
444
+ const overQr = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: 'over-qr', target_url: overQrUrl });
445
+ ok(overQr.ok === false && /QR 编码失败/.test(String(overQr.error)), '§19b 超出 QR v40-M 容量的 target_url 结构化拒绝');
446
+ ok(!bundleFiles(cwd).includes(`over-qr${ITINERARY_DECK_EXPORT_HTML_SUFFIX}`)
447
+ && !bundleFiles(cwd).includes(`over-qr${ITINERARY_DECK_EXPORT_MANIFEST_SUFFIX}`)
448
+ && !bundleFiles(cwd).includes(`over-qr${ITINERARY_DECK_EXPORT_QR_SUFFIX}`), '§19c QR 编码失败零写入(bundle 三件都未落盘)');
449
+ // ---- §22 QR 独立解码验收(#569 T2):光栅化后经独立解码器逐字节还原 payload ----
450
+ // 「不同字节 + 有 <path>」不构成解码证明;这里把**实际落盘的 qr.svg**交给独立解码链
451
+ // (sharp 光栅化 → jsqr 解码,与编码侧 qrcode 库零共享代码),断言还原结果与期望
452
+ // payload 逐字节一致。依赖说明:jsqr 是本次新增的 dev-only 依赖;sharp 早已在运行时
453
+ // 闭包内(@deepseek-ai/dsh-attachment-local 的传递依赖)——本次只在 ts/package.json
454
+ // 把既有版本显式声明为 devDependency 并新增测试 import,不新增任何 runtime 调用。
455
+ // 审批过的字面占位串在测试侧独立钉死(期望值不引用生产常量):生产常量被意外改动时
456
+ // §22b 必红;§22r 再把生产常量与这个字面量互锁(漂移即红)。
457
+ const marker = '<local bundle; not yet hosted>';
458
+ const emptyDecoded = await decodeQrSvg(qrDisk, 240);
459
+ ok(emptyDecoded !== null, '§22a 本地占位 QR 经独立解码器解码成功(非 null)');
460
+ ok(emptyDecoded?.data === marker, `§22b 无 target_url 时解码还原字面占位串(实测:${JSON.stringify(emptyDecoded?.data)})`);
461
+ ok(Buffer.compare(Buffer.from(marker, 'utf8'), Buffer.from(emptyDecoded?.bytes ?? [])) === 0, '§22c 占位串 UTF-8 字节逐字节一致');
462
+ const urlDecoded = await decodeQrSvg(withUrlQrDisk, 240);
463
+ const withUrlStr = 'https://example.com/decks/abc.html';
464
+ ok(urlDecoded !== null, '§22d target_url QR 经独立解码器解码成功(非 null)');
465
+ ok(urlDecoded?.data === withUrlStr, `§22e 解码还原精确 URL(实测:${JSON.stringify(urlDecoded?.data)})`);
466
+ ok(Buffer.compare(Buffer.from(withUrlStr, 'utf8'), Buffer.from(urlDecoded?.bytes ?? [])) === 0, '§22f URL UTF-8 字节逐字节一致');
467
+ // 负控制:两条 payload 经**解码器**还原后互不相等——证明比较非空洞(不是同码/常量误判)
468
+ ok(urlDecoded?.data !== marker && emptyDecoded?.data !== withUrlStr, '§22g 不同 payload 解码还原互不相等(负控制)');
469
+ // UTF-8 payload(非 ASCII 走 QR byte 模式 UTF-8 编码):解码必须逐字节还原
470
+ const utf8Url = 'https://example.com/decks/普吉-2027.html?from=深圳';
471
+ const utf8Run = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: 'utf8-url', target_url: utf8Url });
472
+ ok(utf8Run.ok === true, `§22h UTF-8 target_url 导出成功(实际:${JSON.stringify(utf8Run.error ?? '')})`);
473
+ const utf8QrDisk = readFileSync(String(utf8Run.files?.qr), 'utf-8');
474
+ const utf8Decoded = await decodeQrSvg(utf8QrDisk, 240);
475
+ ok(utf8Decoded !== null, '§22i UTF-8 QR 解码成功(非 null)');
476
+ ok(utf8Decoded?.data === utf8Url, `§22j UTF-8 URL 字符串精确还原(实测:${JSON.stringify(utf8Decoded?.data)})`);
477
+ ok(Buffer.compare(Buffer.from(utf8Url, 'utf8'), Buffer.from(utf8Decoded?.bytes ?? [])) === 0, '§22k UTF-8 URL 字节逐字节一致');
478
+ // 实用显示尺寸:128px(小图)与 240px(典型)都能被独立解码器还原——margin=2 静默区的证据
479
+ const smallDecoded = await decodeQrSvg(withUrlQrDisk, 128);
480
+ ok(smallDecoded !== null && smallDecoded.data === withUrlStr, '§22l 128px 实用显示尺寸解码还原精确 URL(margin=2 静默区可扫描)');
481
+ const largeDecoded = await decodeQrSvg(withUrlQrDisk, 480);
482
+ ok(largeDecoded !== null && largeDecoded.data === withUrlStr, '§22m 480px 放大尺寸解码还原精确 URL');
483
+ // 补全 payload × 尺寸矩阵:UTF-8 与本地占位串在 128/480px 同样解码还原精确 payload
484
+ const utf8Small = await decodeQrSvg(utf8QrDisk, 128);
485
+ ok(utf8Small !== null && utf8Small.data === utf8Url, '§22n 128px UTF-8 URL 解码还原精确');
486
+ const utf8Large = await decodeQrSvg(utf8QrDisk, 480);
487
+ ok(utf8Large !== null && utf8Large.data === utf8Url, '§22o 480px UTF-8 URL 解码还原精确');
488
+ const markerSmall = await decodeQrSvg(qrDisk, 128);
489
+ ok(markerSmall !== null && markerSmall.data === marker, '§22p 128px 本地占位串解码还原精确');
490
+ const markerLarge = await decodeQrSvg(qrDisk, 480);
491
+ ok(markerLarge !== null && markerLarge.data === marker, '§22q 480px 本地占位串解码还原精确');
492
+ // 漂移锁:生产常量必须仍等于上方测试侧独立钉死的审批字面占位串(期望值没有引用它)
493
+ ok(marker === ITINERARY_DECK_EXPORT_LOCAL_MARKER, '§22r ITINERARY_DECK_EXPORT_LOCAL_MARKER 与审批字面占位串一致(漂移即红)');
494
+ // ---- §23 manifest.local_only 显式指示(#569 的 local_only 期望 × v1 兼容附加字段) ----
495
+ ok(emptyManifest.local_only === true, '§23a 无 target_url 时 manifest.local_only = true');
496
+ ok(urlManifest.local_only === false, '§23b 有 target_url 时 manifest.local_only = false');
497
+ // 三态互锁:local_only=true ⟺ target_url 缺省 ⟺ QR 编码本地占位串(绝不暗示线上目的地)
498
+ ok(emptyManifest.target_url === undefined && emptyManifest.local_only === true && emptyDecoded?.data === marker, '§23c local_only=true 与 target_url 缺省与本地占位串三态互锁');
499
+ // ---- §20 真实链路:产物 list/read → deck html 与磁盘一致 + sha256 与 manifest 一致 ----
500
+ const listTool = tools.get('gotry_artifacts_list');
501
+ const readTool = tools.get('gotry_artifacts_read');
502
+ ok(Boolean(listTool && readTool), '§20a 产物 list/read 注册工具可用');
503
+ const chainBasename = 'e2e-chain';
504
+ const chain = await run({
505
+ title: TITLE,
506
+ itinerary: ITINERARY,
507
+ fact_ids: [FLIGHT_ID, HOTEL_ID, POLICY_ID],
508
+ basename: chainBasename,
509
+ target_url: 'https://example.com/decks/e2e.html',
510
+ });
511
+ ok(chain.ok === true, `§20b 导出成功(实际:${JSON.stringify(chain.error ?? '')})`);
512
+ const chainDir = String(chain.target_dir ?? '');
513
+ const listed = await listTool.execute({ limit: 50 }, exec);
514
+ // artifacts list 只白名单 .md/.html/.htm——bundle 的 .manifest.json / .qr.svg 不入册是预期(它们是 bundle 元数据,不是用户产物面)
515
+ const chainHtmlId = `${chainBasename}${ITINERARY_DECK_EXPORT_HTML_SUFFIX}`;
516
+ const htmlEntry = (listed.artifacts ?? []).find(a => a.id === chainHtmlId);
517
+ ok(Boolean(htmlEntry), '§20c list 在册发现 bundle html(manifest/qr 是 bundle 元数据,不入 artifact 列表)');
518
+ const chainHtmlDisk = readFileSync(String(chain.files?.html), 'utf-8');
519
+ const chainManDisk = readFileSync(String(chain.files?.manifest), 'utf-8');
520
+ const chainQrDisk = readFileSync(String(chain.files?.qr), 'utf-8');
521
+ ok(htmlEntry?.bytes === Buffer.byteLength(chainHtmlDisk, 'utf8'), '§20d html list 字节与磁盘一致');
522
+ const chainManifest = JSON.parse(chainManDisk);
523
+ const chainSha = createHash('sha256').update(Buffer.from(chainHtmlDisk, 'utf8')).digest('hex');
524
+ ok(chainManifest.deck?.sha256 === chainSha, '§20g manifest.deck.sha256 与磁盘 deck 一致');
525
+ ok(chainManifest.deck?.bytes === Buffer.byteLength(chainHtmlDisk, 'utf8'), '§20h manifest.deck.bytes 与磁盘一致');
526
+ const htmlRead = await readTool.execute({ path: String(chain.files?.html) }, exec);
527
+ ok(htmlRead.ok === true && htmlRead.lang === 'html', '§20i read html 成功并以 html 源码读出');
528
+ ok(htmlRead.content === chainHtmlDisk, '§20j read 内容与落盘字节一致');
529
+ ok(htmlRead.version === chainManifest.deck?.sha256?.slice(0, 12), '§20k read version = manifest.sha256 前 12 位');
530
+ // qr.svg 直接读盘验证(不在 artifact list 白名单内)
531
+ ok(qrDisk.startsWith('<svg'), '§20l qr.svg 是合法 SVG(真矩阵,#569)');
532
+ // ---- §21 bundle 路径保真(尾随空格) ----
533
+ const plainDir = join(home, 'session');
534
+ const spacedDir = join(home, 'session ');
535
+ mkdirSync(plainDir, { recursive: true });
536
+ mkdirSync(spacedDir, { recursive: true });
537
+ const spacedExec = { agent: { session: { header: { cwd: spacedDir } } } };
538
+ const viaTool = await run({ title: TITLE, itinerary: ITINERARY, fact_ids: [], basename: 'spaced' }, spacedExec);
539
+ ok(viaTool.ok === true, `§21a 带尾随空格 target_dir 导出成功(实际:${JSON.stringify(viaTool.error ?? '')})`);
540
+ ok(realpathSync(String(viaTool.target_dir)) === realpathSync(spacedDir), '§21b 落点 = 宿主给出的带空格目录');
541
+ ok(bundleFiles(spacedDir).length === 3, '§21c bundle 三件落在带空格目录内');
542
+ ok(bundleFiles(plainDir).length === 0, '§21d 同名兄弟目录零写入(未归一化)');
543
+ rmSync(home, { recursive: true, force: true });
544
+ console.log(`\nITINERARY DECK EXPORT TESTS: ${pass} pass, ${fail} fail`);
545
+ if (fail > 0) {
546
+ console.log('\nFAILURES:');
547
+ for (const f of failures)
548
+ console.log(` - ${f}`);
549
+ process.exit(1);
550
+ }
551
+ }
552
+ await main();