@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (533) hide show
  1. package/README.md +172 -219
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1788 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -56
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +161 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +553 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +947 -110
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +676 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/provider-detail-sanitize.js +64 -0
  41. package/dist/capabilities/resilience.js +191 -0
  42. package/dist/capabilities/route-provider-conformance.js +341 -0
  43. package/dist/capabilities/session/action-cache.js +50 -44
  44. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  45. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  46. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  47. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  48. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  49. package/dist/capabilities/session/benchmark.js +189 -0
  50. package/dist/capabilities/session/bridge-ledger.js +224 -0
  51. package/dist/capabilities/session/extension-bridge.js +636 -0
  52. package/dist/capabilities/session/extension-channel.js +109 -0
  53. package/dist/capabilities/session/extension-distribution.js +230 -0
  54. package/dist/capabilities/session/extract.js +31 -21
  55. package/dist/capabilities/session/golden-score.js +114 -0
  56. package/dist/capabilities/session/health-watch.js +215 -0
  57. package/dist/capabilities/session/read-guard.js +75 -24
  58. package/dist/capabilities/session/static-flight-golden.js +150 -0
  59. package/dist/capabilities/session/transport.js +97 -83
  60. package/dist/capabilities/session/wizard.js +117 -0
  61. package/dist/capabilities/session-consent.js +178 -0
  62. package/dist/capabilities/session-login.js +170 -0
  63. package/dist/capabilities/session-search.js +635 -44
  64. package/dist/capabilities/spawn-bounded.js +203 -0
  65. package/dist/capabilities/sponsor-plugin.js +341 -0
  66. package/dist/capabilities/visa-policy.js +140 -0
  67. package/dist/capabilities/weather.js +243 -138
  68. package/dist/data/session-golden-20.json +25 -0
  69. package/dist/data/sf-golden-manifest.json +103 -0
  70. package/dist/data/sf-static-routes.json +91 -0
  71. package/dist/scripts/action-cache-tests.js +24 -59
  72. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  73. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  74. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  75. package/dist/scripts/agent-reach-error-tests.js +354 -0
  76. package/dist/scripts/agent-reach-tests.js +26 -18
  77. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  78. package/dist/scripts/anything-path-measure-tests.js +133 -0
  79. package/dist/scripts/anything-path-measure.js +150 -0
  80. package/dist/scripts/anything-tests.js +250 -60
  81. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  82. package/dist/scripts/artifacts-capability-tests.js +727 -0
  83. package/dist/scripts/async-collect.js +50 -11
  84. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  85. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  86. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  87. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  88. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  89. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  90. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  91. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  92. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  93. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  94. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  95. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
  96. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  97. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  98. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  99. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  100. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  101. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  102. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  103. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  104. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  105. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  106. package/dist/scripts/booking-executor-tests.js +80 -0
  107. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  108. package/dist/scripts/booking-saga-tests.js +125 -0
  109. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  110. package/dist/scripts/bootstrap-tests.js +1549 -0
  111. package/dist/scripts/bridge-events-tests.js +239 -0
  112. package/dist/scripts/bridge-ledger-tests.js +428 -0
  113. package/dist/scripts/build-changelog.js +238 -0
  114. package/dist/scripts/build-metrics-report.js +331 -0
  115. package/dist/scripts/changelog-tests.js +118 -0
  116. package/dist/scripts/channel-probe-tests.js +103 -0
  117. package/dist/scripts/channel-probe.js +169 -0
  118. package/dist/scripts/channel-registry-tests.js +208 -0
  119. package/dist/scripts/city-scenario-tier-tests.js +234 -0
  120. package/dist/scripts/companion-tests.js +25 -70
  121. package/dist/scripts/dida-cookie-probe.js +87 -0
  122. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  123. package/dist/scripts/dida-sw-probe.js +114 -0
  124. package/dist/scripts/diff-test.js +17 -12
  125. package/dist/scripts/doctor-tests.js +600 -0
  126. package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
  127. package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
  128. package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
  129. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  130. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  131. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  132. package/dist/scripts/dsh-llm-tests.js +437 -0
  133. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  134. package/dist/scripts/dsh-runtime-closure.js +162 -0
  135. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  136. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  137. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  138. package/dist/scripts/effect-tests.js +539 -0
  139. package/dist/scripts/engine-run.js +1 -3
  140. package/dist/scripts/engine-tests.js +16 -10
  141. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  142. package/dist/scripts/evaluation-contract-tests.js +268 -0
  143. package/dist/scripts/extension-distribution-cli.js +39 -0
  144. package/dist/scripts/extension-distribution-tests.js +443 -0
  145. package/dist/scripts/extension-tests.js +988 -0
  146. package/dist/scripts/external-event-tests.js +350 -0
  147. package/dist/scripts/fact-gate-tests.js +869 -0
  148. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  149. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  150. package/dist/scripts/flight-malformed-tests.js +159 -0
  151. package/dist/scripts/flyai-setup-tests.js +228 -0
  152. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  153. package/dist/scripts/flyai-tests.js +692 -0
  154. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  155. package/dist/scripts/fx-contract-tests.js +201 -0
  156. package/dist/scripts/g5-guard-tests.js +150 -0
  157. package/dist/scripts/g5-guard.js +193 -0
  158. package/dist/scripts/gen-lunar-anchors.js +106 -0
  159. package/dist/scripts/geo-atlas-tests.js +283 -0
  160. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  161. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  162. package/dist/scripts/gotry-try-demo.js +49 -0
  163. package/dist/scripts/ground-transfer-tests.js +956 -0
  164. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  165. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  166. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  167. package/dist/scripts/hbcli-tests.js +121 -69
  168. package/dist/scripts/health-watch-cli.js +73 -0
  169. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  170. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  171. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  172. package/dist/scripts/i18n-tests.js +41 -0
  173. package/dist/scripts/incident-tests.js +281 -87
  174. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  175. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  176. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  177. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  178. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  179. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  180. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  181. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  182. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  183. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  184. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  185. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  186. package/dist/scripts/itinerary-deck-tests.js +435 -0
  187. package/dist/scripts/itinerary-html-tests.js +426 -0
  188. package/dist/scripts/journey-tests.js +22 -27
  189. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  190. package/dist/scripts/kernel-manifest-gate.js +126 -0
  191. package/dist/scripts/kernel-manifest-tests.js +212 -0
  192. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  193. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  194. package/dist/scripts/kernel-manifest.js +369 -0
  195. package/dist/scripts/lavish-local-tests.js +1226 -0
  196. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  197. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  198. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  199. package/dist/scripts/ledger-tests.js +382 -246
  200. package/dist/scripts/ledger-workflow-crash.js +10 -20
  201. package/dist/scripts/m3-cohort-tests.js +714 -0
  202. package/dist/scripts/m3-cohort.js +310 -0
  203. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  204. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  205. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  206. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  207. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  208. package/dist/scripts/memory-capture-tests.js +242 -61
  209. package/dist/scripts/memory-decay-tests.js +18 -52
  210. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  211. package/dist/scripts/memory-lifecycle.js +196 -0
  212. package/dist/scripts/memory-metrics.js +24 -12
  213. package/dist/scripts/memory-value-report.js +631 -0
  214. package/dist/scripts/memory-value-tests.js +328 -0
  215. package/dist/scripts/metrics-report-tests.js +151 -0
  216. package/dist/scripts/model-override-e2e.js +137 -0
  217. package/dist/scripts/nightly-evidence-tests.js +203 -0
  218. package/dist/scripts/nightly-evidence.js +303 -0
  219. package/dist/scripts/nudge-digest.js +39 -28
  220. package/dist/scripts/observability-tests.js +381 -0
  221. package/dist/scripts/onboarding-tests.js +161 -0
  222. package/dist/scripts/opensky-check.js +18 -13
  223. package/dist/scripts/opensky-tests.js +28 -28
  224. package/dist/scripts/outcome-projection-tests.js +427 -0
  225. package/dist/scripts/persona-sim-tests.js +603 -0
  226. package/dist/scripts/persona-sim.js +1081 -0
  227. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  228. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  229. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  230. package/dist/scripts/price-drift-tests.js +340 -0
  231. package/dist/scripts/price-drift-watch.js +447 -0
  232. package/dist/scripts/probe-poi-tests.js +53 -45
  233. package/dist/scripts/product-metrics.js +483 -0
  234. package/dist/scripts/publish-preverify.js +110 -0
  235. package/dist/scripts/realtime-pricing-tests.js +76 -0
  236. package/dist/scripts/recall-tests.js +513 -0
  237. package/dist/scripts/replay-async.js +22 -18
  238. package/dist/scripts/replay-real.js +18 -18
  239. package/dist/scripts/replay.js +54 -82
  240. package/dist/scripts/route-provider-conformance-tests.js +391 -0
  241. package/dist/scripts/session-attach-diagnose.js +20 -20
  242. package/dist/scripts/session-attach-poc.js +34 -37
  243. package/dist/scripts/session-benchmark.js +180 -0
  244. package/dist/scripts/session-dida-live-e2e.js +199 -0
  245. package/dist/scripts/session-extract-tests.js +14 -35
  246. package/dist/scripts/session-link-tests.js +478 -0
  247. package/dist/scripts/session-login.js +27 -46
  248. package/dist/scripts/session-tests.js +796 -96
  249. package/dist/scripts/session-zone-crash.js +49 -0
  250. package/dist/scripts/session-zone-ledger-tests.js +432 -0
  251. package/dist/scripts/session-zone-observation-tests.js +492 -0
  252. package/dist/scripts/session-zone-observe.js +135 -0
  253. package/dist/scripts/session-zone-wiring-tests.js +566 -0
  254. package/dist/scripts/session-zones-tests.js +374 -0
  255. package/dist/scripts/sf-live-benchmark.js +367 -0
  256. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  257. package/dist/scripts/sf-live-cli-tests.js +92 -0
  258. package/dist/scripts/sf-live-options.js +46 -0
  259. package/dist/scripts/sf-manifest-tests.js +30 -0
  260. package/dist/scripts/sf-manifest.js +33 -0
  261. package/dist/scripts/sf-soft-score-tests.js +67 -0
  262. package/dist/scripts/sf-summary-tests.js +324 -0
  263. package/dist/scripts/sf-summary.js +525 -0
  264. package/dist/scripts/share-tests.js +432 -0
  265. package/dist/scripts/skeleton-check.js +16 -14
  266. package/dist/scripts/skeleton-integration-test.js +28 -11
  267. package/dist/scripts/skills-contract-tests.js +51 -46
  268. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  269. package/dist/scripts/smoke.js +690 -246
  270. package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
  271. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  272. package/dist/scripts/state-cli-tests.js +226 -101
  273. package/dist/scripts/state-cli.js +565 -198
  274. package/dist/scripts/state-repair-cli.js +66 -0
  275. package/dist/scripts/state-repair-tests.js +140 -0
  276. package/dist/scripts/static-golden-tests.js +165 -0
  277. package/dist/scripts/time-eval-tests.js +212 -192
  278. package/dist/scripts/travel-timeline-tests.js +29 -93
  279. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  280. package/dist/scripts/turn-handoff-collect.js +161 -0
  281. package/dist/scripts/turn-policy-tests.js +55 -0
  282. package/dist/scripts/typed-contract-canary.js +176 -0
  283. package/dist/scripts/tz-resolver-sanity.js +50 -0
  284. package/dist/scripts/tz-resolver-tests.js +584 -0
  285. package/dist/scripts/unified-tests.js +60 -20
  286. package/dist/scripts/visa-policy-tests.js +62 -0
  287. package/dist/scripts/weather-tests.js +276 -41
  288. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  289. package/dist/scripts/write-gate-tests.js +522 -0
  290. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  291. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  292. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  293. package/dist/scripts/z3-race-tests.js +46 -0
  294. package/dist/src/artifact-gate.js +829 -0
  295. package/dist/src/backend/kernel.js +91 -0
  296. package/dist/src/backend/modules/booking-copilot.js +92 -0
  297. package/dist/src/backend/modules/booking-executor.js +124 -0
  298. package/dist/src/backend/modules/session-search.js +551 -0
  299. package/dist/src/benchmark-agent-conformance.js +578 -0
  300. package/dist/src/benchmark-environment-bridge.js +515 -0
  301. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  302. package/dist/src/benchmark-tool-isolation.js +151 -0
  303. package/dist/src/bookable-facts.js +533 -0
  304. package/dist/src/booking-saga.js +97 -0
  305. package/dist/src/booking-surface/availability-policy.js +697 -0
  306. package/dist/src/booking-surface/booking-intent.js +43 -0
  307. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  308. package/dist/src/booking-surface/canonical-schema.js +116 -0
  309. package/dist/src/booking-surface/contracts.js +75 -0
  310. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  311. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  312. package/dist/src/booking-surface/dsh-worker.js +98 -0
  313. package/dist/src/booking-surface/error-codes.js +231 -0
  314. package/dist/src/booking-surface/index.js +9 -0
  315. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  316. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  317. package/dist/src/booking-surface/profile.js +24 -0
  318. package/dist/src/booking-surface/recovery-chain.js +367 -0
  319. package/dist/src/booking-surface/runtime.js +1968 -0
  320. package/dist/src/booking-surface/server.js +674 -0
  321. package/dist/src/booking-surface/startup.js +168 -0
  322. package/dist/src/booking-surface/validation.js +261 -0
  323. package/dist/src/bridge.js +20 -13
  324. package/dist/src/city-scenario-tier.js +378 -0
  325. package/dist/src/companions.js +48 -74
  326. package/dist/src/contracts.js +21 -43
  327. package/dist/src/dsh-llm.js +171 -115
  328. package/dist/src/engine.js +276 -193
  329. package/dist/src/evaluation-cadence.js +97 -0
  330. package/dist/src/evaluation-contracts.js +283 -0
  331. package/dist/src/flight-pack-adapter.js +54 -0
  332. package/dist/src/flight-pack-contract.js +2 -0
  333. package/dist/src/flyai-setup-tool.js +70 -0
  334. package/dist/src/gotry-backend.js +125 -0
  335. package/dist/src/hotel-date-gate.js +118 -0
  336. package/dist/src/i18n.js +88 -0
  337. package/dist/src/index.js +2385 -881
  338. package/dist/src/itinerary-deck.js +139 -0
  339. package/dist/src/itinerary-doc-shared.js +661 -0
  340. package/dist/src/itinerary-html.js +131 -0
  341. package/dist/src/journey.js +155 -108
  342. package/dist/src/lavish-tools.js +400 -0
  343. package/dist/src/ledger-repair-apply.js +538 -0
  344. package/dist/src/ledger-repair-plan.js +463 -0
  345. package/dist/src/loop.js +331 -161
  346. package/dist/src/m3-cohort.js +1577 -0
  347. package/dist/src/memory-capture.js +180 -32
  348. package/dist/src/memory-decay.js +30 -14
  349. package/dist/src/memory-lifecycle.js +1310 -0
  350. package/dist/src/memory-utility.js +27 -40
  351. package/dist/src/mock-llm.js +36 -61
  352. package/dist/src/model.js +81 -36
  353. package/dist/src/outcome-projection.js +746 -0
  354. package/dist/src/realtime-pricing.js +103 -0
  355. package/dist/src/recall/card.js +74 -0
  356. package/dist/src/recall/evaluator.js +140 -0
  357. package/dist/src/recall/integration.js +17 -0
  358. package/dist/src/recall/tick.js +166 -0
  359. package/dist/src/session-link/plan-it-action.js +48 -0
  360. package/dist/src/session-link/session-link.js +248 -0
  361. package/dist/src/session-zone-ledger.js +312 -0
  362. package/dist/src/session-zone-observation.js +354 -0
  363. package/dist/src/session-zone-wiring.js +658 -0
  364. package/dist/src/session-zones.js +510 -0
  365. package/dist/src/share/adapters.js +51 -0
  366. package/dist/src/share/share-consent.js +76 -0
  367. package/dist/src/share/share-deck.js +58 -0
  368. package/dist/src/share/share-token.js +146 -0
  369. package/dist/src/slot-spec.js +84 -106
  370. package/dist/src/state-ledger.js +696 -524
  371. package/dist/src/state-repair.js +299 -0
  372. package/dist/src/subagent-job-id-guard.js +58 -0
  373. package/dist/src/time-anchor.js +231 -33
  374. package/dist/src/tool-packet.js +20 -8
  375. package/dist/src/travel-slots.js +58 -35
  376. package/dist/src/travel-timeline.js +44 -63
  377. package/dist/src/turn-deadline.js +287 -0
  378. package/dist/src/turn-policy.js +118 -0
  379. package/dist/src/tz-resolver.js +143 -0
  380. package/dist/src/unified.js +600 -360
  381. package/dist/src/wish-pool.js +42 -12
  382. package/dist/src/write-gate.js +495 -0
  383. package/dist/src/z3-shared.js +284 -0
  384. package/extension/README.md +21 -0
  385. package/extension/README.zh-CN.md +21 -0
  386. package/extension/background.js +463 -0
  387. package/extension/content-bridge.js +270 -0
  388. package/extension/content-main.js +111 -0
  389. package/extension/manifest.json +60 -0
  390. package/package.json +407 -14
  391. package/schemas/booking.intent.schema.json +193 -0
  392. package/schemas/booking.surface.schema.json +2622 -0
  393. package/ts/capabilities/agent-reach-deep.ts +114 -0
  394. package/ts/capabilities/agent-reach.ts +198 -0
  395. package/ts/capabilities/anything.ts +282 -0
  396. package/ts/capabilities/artifacts.ts +428 -0
  397. package/ts/capabilities/fact-log.ts +54 -0
  398. package/ts/capabilities/flyai-config.ts +388 -0
  399. package/ts/capabilities/flyai.ts +1274 -0
  400. package/ts/capabilities/hbcli.ts +253 -71
  401. package/ts/capabilities/incident-log.ts +88 -59
  402. package/ts/capabilities/itinerary-artifact.ts +214 -0
  403. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  404. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  405. package/ts/capabilities/opensky.ts +121 -0
  406. package/ts/capabilities/session/action-cache.ts +148 -0
  407. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  408. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  409. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  410. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  411. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  412. package/ts/capabilities/session/benchmark.ts +276 -0
  413. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  414. package/ts/capabilities/session/extension-bridge.ts +794 -0
  415. package/ts/capabilities/session/extension-channel.ts +144 -0
  416. package/ts/capabilities/session/extension-distribution.ts +266 -0
  417. package/ts/capabilities/session/extract.ts +54 -0
  418. package/ts/capabilities/session/golden-score.ts +139 -0
  419. package/ts/capabilities/session/health-watch.ts +264 -0
  420. package/ts/capabilities/session/read-guard.ts +141 -0
  421. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  422. package/ts/capabilities/session/transport.ts +146 -0
  423. package/ts/capabilities/session/wizard.ts +140 -0
  424. package/ts/capabilities/session-consent.ts +211 -0
  425. package/ts/capabilities/session-login.ts +208 -0
  426. package/ts/capabilities/session-search.ts +893 -0
  427. package/ts/capabilities/weather.ts +330 -0
  428. package/ts/dsh-runtime/vendor/README.md +68 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  440. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  441. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  442. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  443. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  444. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  445. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  446. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  447. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  448. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  449. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  450. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  451. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  452. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  453. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  454. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  455. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  456. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  457. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  458. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  459. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  460. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  461. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  462. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  463. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  464. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  465. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  466. package/ts/package.json +293 -3
  467. package/ts/scripts/async-collect.ts +91 -0
  468. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  469. package/ts/scripts/skeleton-check.ts +1 -1
  470. package/ts/scripts/skeleton-integration-test.ts +23 -1
  471. package/ts/scripts/state-cli.ts +586 -0
  472. package/ts/scripts/turn-handoff-collect.ts +178 -0
  473. package/ts/src/artifact-gate.ts +991 -0
  474. package/ts/src/benchmark-agent-conformance.ts +619 -0
  475. package/ts/src/benchmark-environment-bridge.ts +538 -0
  476. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  477. package/ts/src/benchmark-tool-isolation.ts +166 -0
  478. package/ts/src/bookable-facts.ts +837 -0
  479. package/ts/src/booking-saga.ts +133 -0
  480. package/ts/src/booking-surface/availability-policy.ts +522 -0
  481. package/ts/src/booking-surface/booking-intent.ts +86 -0
  482. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  483. package/ts/src/booking-surface/canonical-schema.js +116 -0
  484. package/ts/src/booking-surface/contracts.ts +475 -0
  485. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  486. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  487. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  488. package/ts/src/booking-surface/error-codes.ts +241 -0
  489. package/ts/src/booking-surface/index.ts +9 -0
  490. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  491. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  492. package/ts/src/booking-surface/profile.ts +42 -0
  493. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  494. package/ts/src/booking-surface/runtime.ts +1793 -0
  495. package/ts/src/booking-surface/server.ts +733 -0
  496. package/ts/src/booking-surface/startup.ts +201 -0
  497. package/ts/src/booking-surface/validation.ts +217 -0
  498. package/ts/src/companions.ts +113 -0
  499. package/ts/src/contracts.ts +17 -3
  500. package/ts/src/dsh-llm.ts +152 -17
  501. package/ts/src/flyai-setup-tool.ts +77 -0
  502. package/ts/src/index.ts +2123 -200
  503. package/ts/src/itinerary-deck.ts +189 -0
  504. package/ts/src/itinerary-doc-shared.ts +741 -0
  505. package/ts/src/itinerary-html.ts +171 -0
  506. package/ts/src/loop.ts +234 -26
  507. package/ts/src/memory-capture.ts +283 -0
  508. package/ts/src/memory-decay.ts +55 -0
  509. package/ts/src/memory-utility.ts +70 -0
  510. package/ts/src/mock-llm.ts +4 -8
  511. package/ts/src/model.ts +88 -2
  512. package/ts/src/recall/card.ts +106 -0
  513. package/ts/src/recall/evaluator.ts +170 -0
  514. package/ts/src/recall/integration.ts +35 -0
  515. package/ts/src/recall/tick.ts +213 -0
  516. package/ts/src/session-link/plan-it-action.ts +78 -0
  517. package/ts/src/session-link/session-link.ts +275 -0
  518. package/ts/src/share/adapters.ts +93 -0
  519. package/ts/src/share/share-consent.ts +105 -0
  520. package/ts/src/share/share-deck.ts +85 -0
  521. package/ts/src/share/share-token.ts +166 -0
  522. package/ts/src/slot-spec.ts +172 -0
  523. package/ts/src/state-ledger.ts +1166 -0
  524. package/ts/src/subagent-job-id-guard.ts +72 -0
  525. package/ts/src/time-anchor.ts +338 -0
  526. package/ts/src/tool-packet.ts +32 -0
  527. package/ts/src/travel-slots.ts +219 -0
  528. package/ts/src/travel-timeline.ts +83 -0
  529. package/ts/src/turn-deadline.ts +366 -0
  530. package/ts/src/turn-policy.ts +154 -0
  531. package/ts/src/unified.ts +444 -105
  532. package/ts/src/wish-pool.ts +79 -0
  533. package/dist/scripts/session-attach-wait.js +0 -41
@@ -0,0 +1,1577 @@
1
+ /**
2
+ * Explicit opt-in M3 seed-cohort capture core (issue #22).
3
+ *
4
+ * `scripts/product-metrics.ts` is the M3 scorer; before this module nothing in the
5
+ * repository produced the `gotry_m3_cohort_record_v1` / `gotry_m3_evidence_manifest_v1`
6
+ * records it consumes. This module owns the deterministic capture contract behind
7
+ * `scripts/m3-cohort.ts`:
8
+ * - every participant/plan/cohort identifier is HMAC-SHA256 pseudonymous; raw labels
9
+ * (the only place a human handle could appear) are never persisted;
10
+ * - the sample window, eligibility, exclusion-code vocabulary and acceptance thresholds
11
+ * are frozen at `init` and must equal the scorer's own frozen values;
12
+ * - funnel events are append-only and idempotent by HMAC event ref;
13
+ * - `evidence_kind` is frozen at `init` and decides who may be captured:
14
+ * `real_seed_cohort` → simulated participants are refused outright;
15
+ * `synthetic_fixture` → every participant MUST carry a simulation provenance block.
16
+ * The two can therefore never be mixed in one store or one evidence root.
17
+ * - export is all-or-nothing and writes exactly the scorer's v1 shapes plus two sidecars
18
+ * the scorer ignores (`provenance.jsonl`, `export-attestation.json`); the attestation
19
+ * carries a keyed MAC over its own body, so an export cannot be relabelled or
20
+ * re-digested without the capture key. That proves the four files are the unmodified
21
+ * output of a capture store holding the key — it does NOT prove the participants were
22
+ * real people, which is a claim about collection that no digest can establish.
23
+ *
24
+ * Path/lock/atomic-write discipline deliberately mirrors `memory-lifecycle.ts` rather than
25
+ * importing it: the two collectors keep independent error-code domains and independent
26
+ * store roots, and a shared private helper surface would couple their fail-closed codes.
27
+ *
28
+ * Arithmetic discipline: this module computes no metric. It records funnel facts and lets
29
+ * `scripts/product-metrics.ts` do every division.
30
+ */
31
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
32
+ import { chmodSync, closeSync, constants, existsSync, ftruncateSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, statSync, unlinkSync, writeSync, } from 'node:fs';
33
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
34
+ export const M3_COHORT_STORE_SCHEMA = 'gotry_m3_cohort_capture.v1';
35
+ export const M3_COHORT_MANIFEST_SCHEMA = 'gotry_m3_cohort_capture_manifest.v1';
36
+ export const M3_COHORT_EVENT_SCHEMA = 'gotry_m3_cohort_capture_event.v1';
37
+ export const M3_COHORT_RECORD_SCHEMA = 'gotry_m3_cohort_record_v1';
38
+ export const M3_EVIDENCE_MANIFEST_SCHEMA = 'gotry_m3_evidence_manifest_v1';
39
+ export const M3_SIMULATION_PROVENANCE_SCHEMA = 'gotry_m3_simulation_provenance_v1';
40
+ export const M3_EXPORT_ATTESTATION_SCHEMA = 'gotry_m3_export_attestation_v1';
41
+ export const HMAC_REF_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
42
+ export const SHA256_PATTERN = /^[0-9a-f]{64}$/;
43
+ export const PERSONA_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
44
+ export const MODEL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:\-/]{0,95}$/;
45
+ /** Frozen copies of the scorer's own acceptance thresholds (`scripts/product-metrics.ts`).
46
+ * A drift here is caught by `m3-cohort-tests.ts`, which asserts the scorer accepts the
47
+ * manifest this module writes. */
48
+ export const M3_ACCEPTANCE = {
49
+ sampleMinimum: 50,
50
+ sampleMaximum: 200,
51
+ finalizationMinimum: 0.4,
52
+ npsMinimum: 40,
53
+ poiExclusiveMaximum: 0.01,
54
+ };
55
+ export const M3_FORMULAS = {
56
+ finalization: 'finalized_eligible_delivered_plans / eligible_delivered_plans',
57
+ nps: '100 * (promoters_9_10 - detractors_0_6) / valid_responses',
58
+ poi: 'audited_invalid_poi_claims / locked_audited_poi_claims',
59
+ };
60
+ export const M3_EXCLUSION_CODES = [
61
+ 'outside_locked_window',
62
+ 'not_invited',
63
+ 'consent_missing_or_withdrawn',
64
+ 'test_or_staff',
65
+ 'attribution_not_allowed',
66
+ ];
67
+ export class M3CohortError extends Error {
68
+ code;
69
+ constructor(code) {
70
+ super(code);
71
+ this.code = code;
72
+ }
73
+ }
74
+ function fail(code) {
75
+ throw new M3CohortError(code);
76
+ }
77
+ export const systemClock = { now: () => new Date() };
78
+ const ATTESTATION_BODY_KEYS = [
79
+ 'schema_version', 'cohort_id', 'evidence_kind', 'exported_at', 'record_count',
80
+ 'simulated_record_count', 'capture_manifest_digest_sha256', 'manifest_sha256',
81
+ 'cohort_records_sha256', 'provenance_jsonl_sha256',
82
+ ];
83
+ function attestationMac(hmacKey, body) {
84
+ return pseudonymousRef(hmacKey, 'export-attestation', [canonicalJson(body)]);
85
+ }
86
+ /** The cohort-record subset of a `cohort.jsonl`, in file order, as the digest sees it. */
87
+ export function cohortRecordLines(cohortJsonl) {
88
+ const lines = [];
89
+ for (const line of cohortJsonl.split('\n')) {
90
+ if (line.trim() === '')
91
+ continue;
92
+ let parsed;
93
+ try {
94
+ parsed = JSON.parse(line);
95
+ }
96
+ catch {
97
+ fail('export_validation_failed');
98
+ }
99
+ if (!isObject(parsed))
100
+ fail('export_validation_failed');
101
+ if (parsed['schema_version'] === M3_COHORT_RECORD_SCHEMA)
102
+ lines.push(line);
103
+ }
104
+ return lines;
105
+ }
106
+ function cohortRecordsDigest(lines) {
107
+ return sha256Hex(lines.join('\n') + (lines.length > 0 ? '\n' : ''));
108
+ }
109
+ // ---------------------------------------------------------------------------
110
+ // Validation primitives
111
+ // ---------------------------------------------------------------------------
112
+ function isObject(value) {
113
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
114
+ }
115
+ function nonEmptyString(value) {
116
+ return typeof value === 'string' && value.trim().length > 0;
117
+ }
118
+ function assertRawLabel(value, limit = 512) {
119
+ if (!nonEmptyString(value))
120
+ fail('invalid_input');
121
+ const text = value;
122
+ if (text.length > limit)
123
+ fail('invalid_input');
124
+ if (/[\u0000-\u001f\u007f]/u.test(text))
125
+ fail('invalid_input');
126
+ return text;
127
+ }
128
+ function assertIsoDate(date) {
129
+ const ms = date.getTime();
130
+ if (!Number.isFinite(ms))
131
+ fail('invalid_input');
132
+ return date.toISOString();
133
+ }
134
+ function parseIsoMs(value) {
135
+ const ms = Date.parse(value);
136
+ if (!Number.isFinite(ms))
137
+ fail('invalid_store');
138
+ if (new Date(ms).toISOString() !== value)
139
+ fail('invalid_store');
140
+ return ms;
141
+ }
142
+ function assertTimestampInput(value) {
143
+ const text = assertRawLabel(value, 64);
144
+ const ms = Date.parse(text);
145
+ if (!Number.isFinite(ms))
146
+ fail('invalid_input');
147
+ if (new Date(ms).toISOString() !== text)
148
+ fail('invalid_input');
149
+ return text;
150
+ }
151
+ function normalizeHmacKey(key) {
152
+ if (!nonEmptyString(key))
153
+ fail('missing_hmac_key');
154
+ const trimmed = key.trim();
155
+ if (trimmed.length < 32)
156
+ fail('invalid_hmac_key');
157
+ if (/[\u0000-\u001f\u007f]/u.test(trimmed))
158
+ fail('invalid_hmac_key');
159
+ return trimmed;
160
+ }
161
+ function assertEvidenceKind(value) {
162
+ if (value !== 'real_seed_cohort' && value !== 'synthetic_fixture')
163
+ fail('invalid_input');
164
+ return value;
165
+ }
166
+ function assertAttribution(value) {
167
+ if (value !== 'gotry_primary' && value !== 'gotry_assisted')
168
+ fail('invalid_input');
169
+ return value;
170
+ }
171
+ function assertNonNegativeInteger(value) {
172
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0)
173
+ fail('invalid_input');
174
+ return value;
175
+ }
176
+ function assertNpsScore(value) {
177
+ const score = assertNonNegativeInteger(value);
178
+ if (score > 10)
179
+ fail('invalid_input');
180
+ return score;
181
+ }
182
+ function assertHmacRef(value) {
183
+ if (!nonEmptyString(value) || !HMAC_REF_PATTERN.test(value))
184
+ fail('invalid_store');
185
+ return value;
186
+ }
187
+ export function canonicalJson(value) {
188
+ if (Array.isArray(value))
189
+ return `[${value.map(canonicalJson).join(',')}]`;
190
+ if (isObject(value)) {
191
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
192
+ }
193
+ return JSON.stringify(value);
194
+ }
195
+ export function sha256Hex(value) {
196
+ return createHash('sha256').update(value).digest('hex');
197
+ }
198
+ /** Domain-separated pseudonym. The `evidence_kind` is part of the domain: a record captured
199
+ * as synthetic cannot be relabelled real and still recompute to the same key. */
200
+ export function pseudonymousRef(hmacKey, kind, parts) {
201
+ const key = normalizeHmacKey(hmacKey);
202
+ const hmac = createHmac('sha256', key);
203
+ hmac.update('gotry-m3-cohort.v1');
204
+ hmac.update('\0');
205
+ hmac.update(assertRawLabel(kind, 64));
206
+ for (const part of parts) {
207
+ hmac.update('\0');
208
+ hmac.update(assertRawLabel(part, 2048));
209
+ }
210
+ return `hmac-sha256:${hmac.digest('hex')}`;
211
+ }
212
+ export function participantKeyFor(hmacKey, evidenceKind, participant) {
213
+ return pseudonymousRef(hmacKey, `participant:${assertEvidenceKind(evidenceKind)}`, [assertRawLabel(participant)]);
214
+ }
215
+ export function planKeyFor(hmacKey, evidenceKind, participant, plan) {
216
+ return pseudonymousRef(hmacKey, `plan:${assertEvidenceKind(evidenceKind)}`, [
217
+ participantKeyFor(hmacKey, evidenceKind, participant),
218
+ assertRawLabel(plan),
219
+ ]);
220
+ }
221
+ function cohortRefFor(hmacKey, evidenceKind, cohort) {
222
+ return pseudonymousRef(hmacKey, `cohort:${assertEvidenceKind(evidenceKind)}`, [assertRawLabel(cohort)]);
223
+ }
224
+ function cohortKeyVerifier(hmacKey, evidenceKind) {
225
+ return pseudonymousRef(hmacKey, `cohort-key-verifier:${assertEvidenceKind(evidenceKind)}`, [M3_COHORT_STORE_SCHEMA]);
226
+ }
227
+ function eventRef(hmacKey, kind, parts) {
228
+ return pseudonymousRef(hmacKey, `event:${kind}`, parts);
229
+ }
230
+ export function assertSimulationProvenance(value) {
231
+ if (!isObject(value))
232
+ fail('invalid_input');
233
+ const expected = new Set(['schema_version', 'kind', 'persona_id', 'product_model', 'persona_model', 'prompt_digest']);
234
+ for (const key of Object.keys(value))
235
+ if (!expected.has(key))
236
+ fail('invalid_input');
237
+ if (value['schema_version'] !== M3_SIMULATION_PROVENANCE_SCHEMA)
238
+ fail('invalid_input');
239
+ if (value['kind'] !== 'llm_persona')
240
+ fail('invalid_input');
241
+ const personaId = assertRawLabel(value['persona_id'], 64);
242
+ if (!PERSONA_ID_PATTERN.test(personaId))
243
+ fail('invalid_input');
244
+ const productModel = assertRawLabel(value['product_model'], 96);
245
+ const personaModel = assertRawLabel(value['persona_model'], 96);
246
+ if (!MODEL_NAME_PATTERN.test(productModel) || !MODEL_NAME_PATTERN.test(personaModel))
247
+ fail('invalid_input');
248
+ const digest = assertRawLabel(value['prompt_digest'], 64);
249
+ if (!SHA256_PATTERN.test(digest))
250
+ fail('invalid_input');
251
+ return {
252
+ schema_version: M3_SIMULATION_PROVENANCE_SCHEMA,
253
+ kind: 'llm_persona',
254
+ persona_id: personaId,
255
+ product_model: productModel,
256
+ persona_model: personaModel,
257
+ prompt_digest: digest,
258
+ };
259
+ }
260
+ // ---------------------------------------------------------------------------
261
+ // Path safety / atomic private IO
262
+ // ---------------------------------------------------------------------------
263
+ function normalizePolicyPath(path) {
264
+ return resolve(path).replace(/\\/g, '/');
265
+ }
266
+ function assertSafePolicyPath(path) {
267
+ const normalized = normalizePolicyPath(path);
268
+ if (normalized === '/' || normalized.endsWith('/.git') || normalized.includes('/.git/'))
269
+ fail('unsafe_state_root');
270
+ if (normalized.endsWith('/ts/dsh-runtime') || normalized.includes('/ts/dsh-runtime/'))
271
+ fail('unsafe_state_root');
272
+ if (normalized.endsWith('/.dsh') || normalized.includes('/.dsh/'))
273
+ fail('unsafe_state_root');
274
+ if (normalized.endsWith('/.gotry') || normalized.includes('/.gotry/'))
275
+ fail('unsafe_state_root');
276
+ }
277
+ function nearestExistingAncestor(path) {
278
+ let current = resolve(path);
279
+ for (;;) {
280
+ if (existsSync(current))
281
+ return current;
282
+ const parent = dirname(current);
283
+ if (parent === current)
284
+ return current;
285
+ current = parent;
286
+ }
287
+ }
288
+ function realpathOrUnsafe(path) {
289
+ try {
290
+ return realpathSync.native(path);
291
+ }
292
+ catch {
293
+ fail('unsafe_state_root');
294
+ }
295
+ }
296
+ function sameOrChildPath(parent, child) {
297
+ const distance = relative(parent, child);
298
+ return distance === '' || (!distance.startsWith('..') && !isAbsolute(distance));
299
+ }
300
+ /** Policy gate on a caller-supplied state root. Exported so a harness can refuse an unsafe
301
+ * root BEFORE it creates any directory inside it. */
302
+ export function assertSafeStateRoot(stateRoot) {
303
+ if (!nonEmptyString(stateRoot))
304
+ fail('missing_state_root');
305
+ const resolved = resolve(stateRoot);
306
+ assertSafePolicyPath(resolved);
307
+ assertSafePolicyPath(realpathOrUnsafe(nearestExistingAncestor(resolved)));
308
+ return resolved;
309
+ }
310
+ export function storeRootForStateRoot(stateRoot) {
311
+ return join(assertSafeStateRoot(stateRoot), 'gotry-state', 'm3-cohort');
312
+ }
313
+ function assertManagedStoreIsolation(storeRoot) {
314
+ const resolvedStoreRoot = resolve(storeRoot);
315
+ const stateRoot = resolve(resolvedStoreRoot, '..', '..');
316
+ assertSafePolicyPath(stateRoot);
317
+ assertSafePolicyPath(resolvedStoreRoot);
318
+ assertSafePolicyPath(realpathOrUnsafe(nearestExistingAncestor(stateRoot)));
319
+ if (!existsSync(stateRoot))
320
+ return;
321
+ const stateRootReal = realpathOrUnsafe(stateRoot);
322
+ assertSafePolicyPath(stateRootReal);
323
+ for (const managedPath of [join(stateRoot, 'gotry-state'), resolvedStoreRoot]) {
324
+ const existingReal = realpathOrUnsafe(nearestExistingAncestor(managedPath));
325
+ assertSafePolicyPath(existingReal);
326
+ if (!sameOrChildPath(stateRootReal, existingReal))
327
+ fail('unsafe_state_root');
328
+ }
329
+ }
330
+ const OPEN_NOFOLLOW = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
331
+ let tempFileCounter = 0;
332
+ function isNodeError(error, code) {
333
+ return isObject(error) && error['code'] === code;
334
+ }
335
+ function ensurePrivateDir(path) {
336
+ mkdirSync(path, { recursive: true, mode: 0o700 });
337
+ try {
338
+ chmodSync(path, 0o700);
339
+ }
340
+ catch { /* best effort on non-POSIX filesystems */ }
341
+ }
342
+ function pathExistsByLstat(path) {
343
+ try {
344
+ lstatSync(path);
345
+ return true;
346
+ }
347
+ catch (error) {
348
+ if (isNodeError(error, 'ENOENT'))
349
+ return false;
350
+ fail('unsafe_state_root');
351
+ }
352
+ }
353
+ function assertNoSymlinkLeaf(path) {
354
+ try {
355
+ if (lstatSync(path).isSymbolicLink())
356
+ fail('unsafe_state_root');
357
+ }
358
+ catch (error) {
359
+ if (isNodeError(error, 'ENOENT'))
360
+ return;
361
+ if (error instanceof M3CohortError)
362
+ throw error;
363
+ fail('unsafe_state_root');
364
+ }
365
+ }
366
+ function assertManagedFilePath(storeRoot, path) {
367
+ const resolvedStoreRoot = resolve(storeRoot);
368
+ const resolvedPath = resolve(path);
369
+ if (!sameOrChildPath(resolvedStoreRoot, resolvedPath))
370
+ fail('unsafe_state_root');
371
+ assertSafePolicyPath(resolvedPath);
372
+ assertManagedStoreIsolation(resolvedStoreRoot);
373
+ const stateRootReal = realpathOrUnsafe(resolve(resolvedStoreRoot, '..', '..'));
374
+ const parentReal = realpathOrUnsafe(nearestExistingAncestor(dirname(resolvedPath)));
375
+ assertSafePolicyPath(parentReal);
376
+ if (!sameOrChildPath(stateRootReal, parentReal))
377
+ fail('unsafe_state_root');
378
+ assertNoSymlinkLeaf(resolvedPath);
379
+ }
380
+ function assertOutputPath(path) {
381
+ const resolvedPath = resolve(path);
382
+ assertSafePolicyPath(resolvedPath);
383
+ const parent = dirname(resolvedPath);
384
+ assertSafePolicyPath(parent);
385
+ assertSafePolicyPath(realpathOrUnsafe(nearestExistingAncestor(parent)));
386
+ if (existsSync(parent))
387
+ assertSafePolicyPath(realpathOrUnsafe(parent));
388
+ assertNoSymlinkLeaf(resolvedPath);
389
+ return resolvedPath;
390
+ }
391
+ function privateTempPath(path) {
392
+ tempFileCounter += 1;
393
+ return join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.${tempFileCounter}.${randomBytes(6).toString('hex')}.tmp`);
394
+ }
395
+ function writeAllSync(fd, content) {
396
+ const bytes = Buffer.from(content);
397
+ let offset = 0;
398
+ while (offset < bytes.length) {
399
+ const written = writeSync(fd, bytes, offset, bytes.length - offset);
400
+ if (!Number.isInteger(written) || written <= 0)
401
+ fail('internal_error');
402
+ offset += written;
403
+ }
404
+ }
405
+ function writePrivateTempFile(path, content) {
406
+ let fd = -1;
407
+ let created = false;
408
+ let caught;
409
+ try {
410
+ fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | OPEN_NOFOLLOW, 0o600);
411
+ created = true;
412
+ writeAllSync(fd, content);
413
+ }
414
+ catch (error) {
415
+ caught = error;
416
+ }
417
+ finally {
418
+ if (fd >= 0) {
419
+ try {
420
+ closeSync(fd);
421
+ }
422
+ catch (error) {
423
+ caught ??= error;
424
+ }
425
+ }
426
+ if (caught !== undefined && created) {
427
+ try {
428
+ unlinkSync(path);
429
+ }
430
+ catch { /* best effort temp cleanup */ }
431
+ }
432
+ }
433
+ if (caught !== undefined) {
434
+ if (caught instanceof M3CohortError)
435
+ throw caught;
436
+ if (isNodeError(caught, 'ELOOP'))
437
+ fail('unsafe_state_root');
438
+ fail('internal_error');
439
+ }
440
+ try {
441
+ chmodSync(path, 0o600);
442
+ }
443
+ catch { /* best effort */ }
444
+ }
445
+ function publishTempNoOverwrite(tempPath, finalPath, existsCode) {
446
+ try {
447
+ linkSync(tempPath, finalPath);
448
+ }
449
+ catch (error) {
450
+ if (isNodeError(error, 'EEXIST'))
451
+ fail(existsCode);
452
+ fail('internal_error');
453
+ }
454
+ }
455
+ function writeManagedFileNoOverwrite(storeRoot, path, content, existsCode) {
456
+ assertManagedFilePath(storeRoot, path);
457
+ ensurePrivateDir(dirname(path));
458
+ assertManagedFilePath(storeRoot, path);
459
+ if (pathExistsByLstat(path))
460
+ fail(existsCode);
461
+ const tempPath = privateTempPath(path);
462
+ let tempCreated = false;
463
+ try {
464
+ assertManagedFilePath(storeRoot, tempPath);
465
+ writePrivateTempFile(tempPath, content);
466
+ tempCreated = true;
467
+ assertManagedFilePath(storeRoot, path);
468
+ publishTempNoOverwrite(tempPath, path, existsCode);
469
+ }
470
+ finally {
471
+ if (tempCreated) {
472
+ try {
473
+ unlinkSync(tempPath);
474
+ }
475
+ catch { /* best effort temp cleanup */ }
476
+ }
477
+ }
478
+ try {
479
+ chmodSync(path, 0o600);
480
+ }
481
+ catch { /* best effort */ }
482
+ }
483
+ function writeOutputFileNoOverwrite(path, content) {
484
+ const finalPath = assertOutputPath(path);
485
+ ensurePrivateDir(dirname(finalPath));
486
+ assertOutputPath(finalPath);
487
+ if (pathExistsByLstat(finalPath))
488
+ fail('output_exists');
489
+ const tempPath = privateTempPath(finalPath);
490
+ let tempCreated = false;
491
+ try {
492
+ assertOutputPath(tempPath);
493
+ writePrivateTempFile(tempPath, content);
494
+ tempCreated = true;
495
+ assertOutputPath(finalPath);
496
+ if (pathExistsByLstat(finalPath))
497
+ fail('output_exists');
498
+ publishTempNoOverwrite(tempPath, finalPath, 'output_exists');
499
+ }
500
+ finally {
501
+ if (tempCreated) {
502
+ try {
503
+ unlinkSync(tempPath);
504
+ }
505
+ catch { /* best effort temp cleanup */ }
506
+ }
507
+ }
508
+ try {
509
+ chmodSync(finalPath, 0o600);
510
+ }
511
+ catch { /* best effort */ }
512
+ }
513
+ function appendPrivateLine(storeRoot, path, line) {
514
+ assertManagedFilePath(storeRoot, path);
515
+ ensurePrivateDir(dirname(path));
516
+ assertManagedFilePath(storeRoot, path);
517
+ const existedBefore = pathExistsByLstat(path);
518
+ const originalSize = existedBefore ? statSync(path).size : 0;
519
+ let fd = -1;
520
+ let caught;
521
+ try {
522
+ fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | OPEN_NOFOLLOW, 0o600);
523
+ writeAllSync(fd, `${line}\n`);
524
+ }
525
+ catch (error) {
526
+ caught = error;
527
+ if (fd >= 0) {
528
+ try {
529
+ ftruncateSync(fd, originalSize);
530
+ }
531
+ catch { /* keep original error */ }
532
+ }
533
+ }
534
+ finally {
535
+ if (fd >= 0) {
536
+ try {
537
+ closeSync(fd);
538
+ }
539
+ catch { /* close is non-semantic after append/truncate */ }
540
+ }
541
+ if (caught !== undefined && !existedBefore) {
542
+ try {
543
+ unlinkSync(path);
544
+ }
545
+ catch { /* best effort rollback */ }
546
+ }
547
+ }
548
+ if (caught !== undefined) {
549
+ if (caught instanceof M3CohortError)
550
+ throw caught;
551
+ if (isNodeError(caught, 'ELOOP'))
552
+ fail('unsafe_state_root');
553
+ fail('internal_error');
554
+ }
555
+ try {
556
+ chmodSync(path, 0o600);
557
+ }
558
+ catch { /* best effort */ }
559
+ }
560
+ export const M3_COHORT_LOCK_SCHEMA = 'gotry_m3_cohort_lock.v1';
561
+ /** `true` when the pid is gone, `false` when it exists (EPERM means it exists under another
562
+ * uid), `null` when the lock does not name a usable pid. */
563
+ function pidIsDead(pid) {
564
+ if (pid === null)
565
+ return null;
566
+ if (pid === process.pid)
567
+ return false;
568
+ try {
569
+ process.kill(pid, 0);
570
+ return false;
571
+ }
572
+ catch (error) {
573
+ if (isNodeError(error, 'ESRCH'))
574
+ return true;
575
+ return false;
576
+ }
577
+ }
578
+ function readLockPid(lockPath) {
579
+ try {
580
+ const parsed = JSON.parse(readFileSync(lockPath, 'utf8'));
581
+ if (!isObject(parsed) || parsed['schema_version'] !== M3_COHORT_LOCK_SCHEMA)
582
+ return null;
583
+ const pid = parsed['pid'];
584
+ return typeof pid === 'number' && Number.isInteger(pid) && pid > 0 ? pid : null;
585
+ }
586
+ catch {
587
+ return null;
588
+ }
589
+ }
590
+ /** Read-only lock inspection: tells an operator whether `lock_busy` is a live writer or a
591
+ * leftover from a killed one. Never writes. */
592
+ export function inspectStoreLock(stateRoot) {
593
+ const storeRoot = storeRootForStateRoot(stateRoot);
594
+ const lockPath = join(storeRoot, '.writer.lock');
595
+ if (!existsSync(storeRoot) || !pathExistsByLstat(lockPath)) {
596
+ return { path: lockPath, held: false, pid: null, pid_alive: null, reason: 'absent' };
597
+ }
598
+ const pid = readLockPid(lockPath);
599
+ const dead = pidIsDead(pid);
600
+ if (pid === null)
601
+ return { path: lockPath, held: true, pid: null, pid_alive: null, reason: 'unreadable' };
602
+ return {
603
+ path: lockPath,
604
+ held: true,
605
+ pid,
606
+ pid_alive: dead === null ? null : !dead,
607
+ reason: dead === true ? 'stale_writer' : 'live_writer',
608
+ };
609
+ }
610
+ /**
611
+ * Clear a writer lock whose owning process is gone.
612
+ *
613
+ * A Ctrl-C during a long real capture used to wedge the store at `lock_busy` forever. The
614
+ * safety check is the recorded pid: a live owner (or a lock that does not name a readable
615
+ * pid) is refused, so this can never steal the lock from a running writer.
616
+ */
617
+ export function unlockStore(stateRoot) {
618
+ const state = inspectStoreLock(stateRoot);
619
+ if (!state.held)
620
+ return { schema: 'gotry_m3_cohort_unlock.v1', ok: true, status: 'unchanged', lock: state };
621
+ if (state.reason !== 'stale_writer')
622
+ fail('lock_busy');
623
+ const storeRoot = storeRootForStateRoot(stateRoot);
624
+ assertManagedFilePath(storeRoot, state.path);
625
+ try {
626
+ unlinkSync(state.path);
627
+ }
628
+ catch (error) {
629
+ if (isNodeError(error, 'ENOENT'))
630
+ return { schema: 'gotry_m3_cohort_unlock.v1', ok: true, status: 'unchanged', lock: state };
631
+ fail('internal_error');
632
+ }
633
+ return { schema: 'gotry_m3_cohort_unlock.v1', ok: true, status: 'recorded', lock: state };
634
+ }
635
+ function withStoreLock(storeRoot, createStore, body) {
636
+ assertManagedStoreIsolation(storeRoot);
637
+ if (createStore)
638
+ ensurePrivateDir(storeRoot);
639
+ else if (!existsSync(storeRoot))
640
+ fail('cohort_not_initialized');
641
+ assertManagedStoreIsolation(storeRoot);
642
+ const lockPath = join(storeRoot, '.writer.lock');
643
+ assertManagedFilePath(storeRoot, lockPath);
644
+ const acquire = () => {
645
+ let fd = -1;
646
+ let createdLock = false;
647
+ try {
648
+ fd = openSync(lockPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | OPEN_NOFOLLOW, 0o600);
649
+ createdLock = true;
650
+ writeAllSync(fd, JSON.stringify({ schema_version: M3_COHORT_LOCK_SCHEMA, pid: process.pid }));
651
+ }
652
+ catch (error) {
653
+ if (fd >= 0) {
654
+ try {
655
+ closeSync(fd);
656
+ }
657
+ catch { /* preserve original error */ }
658
+ fd = -1;
659
+ }
660
+ if (createdLock) {
661
+ try {
662
+ unlinkSync(lockPath);
663
+ }
664
+ catch { /* best effort for this writer's failed lock */ }
665
+ }
666
+ if (isNodeError(error, 'EEXIST'))
667
+ return 'busy';
668
+ fail('internal_error');
669
+ }
670
+ finally {
671
+ if (fd >= 0)
672
+ closeSync(fd);
673
+ }
674
+ return 'acquired';
675
+ };
676
+ if (acquire() === 'busy') {
677
+ // A lock whose writer is gone (Ctrl-C, OOM kill) must not wedge the store forever. Only
678
+ // a provably dead pid is reclaimed; a live one — or a lock we cannot read a pid from —
679
+ // still returns lock_busy, so a running writer is never robbed.
680
+ if (pidIsDead(readLockPid(lockPath)) !== true)
681
+ fail('lock_busy');
682
+ try {
683
+ unlinkSync(lockPath);
684
+ }
685
+ catch { /* another writer may have cleared it first */ }
686
+ if (acquire() === 'busy')
687
+ fail('lock_busy');
688
+ }
689
+ try {
690
+ assertManagedStoreIsolation(storeRoot);
691
+ return body();
692
+ }
693
+ finally {
694
+ try {
695
+ unlinkSync(lockPath);
696
+ }
697
+ catch { /* lock cleanup is best effort */ }
698
+ }
699
+ }
700
+ // ---------------------------------------------------------------------------
701
+ // Store read/write
702
+ // ---------------------------------------------------------------------------
703
+ function manifestPath(storeRoot) {
704
+ return join(storeRoot, 'manifest.json');
705
+ }
706
+ function eventsPath(storeRoot) {
707
+ return join(storeRoot, 'events.jsonl');
708
+ }
709
+ function prepareCommon(options) {
710
+ const stateRoot = assertSafeStateRoot(options.stateRoot);
711
+ if (!nonEmptyString(options.consent))
712
+ fail('missing_consent');
713
+ const hmacKey = normalizeHmacKey(options.hmacKey);
714
+ const consent = assertRawLabel(options.consent, 512);
715
+ return {
716
+ consentRef: pseudonymousRef(hmacKey, 'consent', [consent]),
717
+ hmacKey,
718
+ stateRoot,
719
+ storeRoot: join(stateRoot, 'gotry-state', 'm3-cohort'),
720
+ };
721
+ }
722
+ function exactKeys(value, keys) {
723
+ const expected = new Set(keys);
724
+ for (const key of Object.keys(value))
725
+ if (!expected.has(key))
726
+ fail('invalid_store');
727
+ for (const key of keys)
728
+ if (!(key in value))
729
+ fail('invalid_store');
730
+ }
731
+ function assertEvidenceManifest(value) {
732
+ if (!isObject(value))
733
+ fail('invalid_store');
734
+ exactKeys(value, ['schema_version', 'evidence_kind', 'cohort_id', 'locked_at', 'window', 'eligibility', 'metrics', 'nightly']);
735
+ if (value['schema_version'] !== M3_EVIDENCE_MANIFEST_SCHEMA)
736
+ fail('invalid_store');
737
+ const window = value['window'];
738
+ const eligibility = value['eligibility'];
739
+ const metrics = value['metrics'];
740
+ const nightly = value['nightly'];
741
+ if (!isObject(window) || !isObject(eligibility) || !isObject(metrics) || !isObject(nightly))
742
+ fail('invalid_store');
743
+ exactKeys(window, ['start_at', 'end_at', 'timezone']);
744
+ exactKeys(eligibility, ['sample_unit', 'requires_invitation', 'requires_consent', 'allowed_attribution', 'exclusion_codes']);
745
+ exactKeys(metrics, ['sample_size', 'finalization_rate', 'nps', 'poi_hallucination_rate']);
746
+ exactKeys(nightly, ['requires_real_llm', 'requires_prompt_set_sha256', 'requires_output_sha256', 'requires_cost_usd']);
747
+ const kind = value['evidence_kind'];
748
+ if (kind !== 'real_seed_cohort' && kind !== 'synthetic_fixture')
749
+ fail('invalid_store');
750
+ if (!nonEmptyString(value['cohort_id']))
751
+ fail('invalid_store');
752
+ parseIsoMs(String(value['locked_at']));
753
+ const startAt = String(window['start_at']);
754
+ const endAt = String(window['end_at']);
755
+ if (parseIsoMs(startAt) > parseIsoMs(endAt))
756
+ fail('invalid_store');
757
+ if (eligibility['sample_unit'] !== 'unique_participant_with_eligible_delivered_plan')
758
+ fail('invalid_store');
759
+ if (eligibility['requires_invitation'] !== true || eligibility['requires_consent'] !== true)
760
+ fail('invalid_store');
761
+ const attribution = eligibility['allowed_attribution'];
762
+ if (!Array.isArray(attribution) || attribution.length === 0)
763
+ fail('invalid_store');
764
+ const allowed = attribution.map(item => {
765
+ if (item !== 'gotry_primary' && item !== 'gotry_assisted')
766
+ fail('invalid_store');
767
+ return item;
768
+ });
769
+ if (new Set(allowed).size !== allowed.length)
770
+ fail('invalid_store');
771
+ const exclusions = eligibility['exclusion_codes'];
772
+ if (!Array.isArray(exclusions))
773
+ fail('invalid_store');
774
+ if (canonicalJson([...exclusions].sort()) !== canonicalJson([...M3_EXCLUSION_CODES].sort()))
775
+ fail('invalid_store');
776
+ const sampleSize = metrics['sample_size'];
777
+ const finalization = metrics['finalization_rate'];
778
+ const nps = metrics['nps'];
779
+ const poi = metrics['poi_hallucination_rate'];
780
+ if (!isObject(sampleSize) || !isObject(finalization) || !isObject(nps) || !isObject(poi))
781
+ fail('invalid_store');
782
+ exactKeys(sampleSize, ['minimum', 'maximum']);
783
+ exactKeys(finalization, ['formula', 'minimum']);
784
+ exactKeys(nps, ['formula', 'minimum']);
785
+ exactKeys(poi, ['formula', 'exclusive_maximum']);
786
+ if (sampleSize['minimum'] !== M3_ACCEPTANCE.sampleMinimum || sampleSize['maximum'] !== M3_ACCEPTANCE.sampleMaximum)
787
+ fail('invalid_store');
788
+ if (finalization['formula'] !== M3_FORMULAS.finalization || finalization['minimum'] !== M3_ACCEPTANCE.finalizationMinimum)
789
+ fail('invalid_store');
790
+ if (nps['formula'] !== M3_FORMULAS.nps || nps['minimum'] !== M3_ACCEPTANCE.npsMinimum)
791
+ fail('invalid_store');
792
+ if (poi['formula'] !== M3_FORMULAS.poi || poi['exclusive_maximum'] !== M3_ACCEPTANCE.poiExclusiveMaximum)
793
+ fail('invalid_store');
794
+ if (nightly['requires_real_llm'] !== true || nightly['requires_prompt_set_sha256'] !== true)
795
+ fail('invalid_store');
796
+ if (nightly['requires_output_sha256'] !== true || nightly['requires_cost_usd'] !== true)
797
+ fail('invalid_store');
798
+ return {
799
+ schema_version: M3_EVIDENCE_MANIFEST_SCHEMA,
800
+ evidence_kind: kind,
801
+ cohort_id: String(value['cohort_id']),
802
+ locked_at: String(value['locked_at']),
803
+ window: { start_at: startAt, end_at: endAt, timezone: String(window['timezone']) },
804
+ eligibility: {
805
+ sample_unit: 'unique_participant_with_eligible_delivered_plan',
806
+ requires_invitation: true,
807
+ requires_consent: true,
808
+ allowed_attribution: allowed,
809
+ exclusion_codes: [...M3_EXCLUSION_CODES],
810
+ },
811
+ metrics: {
812
+ sample_size: { minimum: M3_ACCEPTANCE.sampleMinimum, maximum: M3_ACCEPTANCE.sampleMaximum },
813
+ finalization_rate: { formula: M3_FORMULAS.finalization, minimum: M3_ACCEPTANCE.finalizationMinimum },
814
+ nps: { formula: M3_FORMULAS.nps, minimum: M3_ACCEPTANCE.npsMinimum },
815
+ poi_hallucination_rate: { formula: M3_FORMULAS.poi, exclusive_maximum: M3_ACCEPTANCE.poiExclusiveMaximum },
816
+ },
817
+ nightly: {
818
+ requires_real_llm: true,
819
+ requires_prompt_set_sha256: true,
820
+ requires_output_sha256: true,
821
+ requires_cost_usd: true,
822
+ },
823
+ };
824
+ }
825
+ function assertCaptureManifest(value) {
826
+ if (!isObject(value))
827
+ fail('invalid_store');
828
+ exactKeys(value, ['schema_version', 'capture_schema', 'cohort_ref', 'cohort_key_verifier', 'created_at', 'consent_ref', 'evidence_manifest']);
829
+ if (value['schema_version'] !== M3_COHORT_MANIFEST_SCHEMA)
830
+ fail('invalid_store');
831
+ if (value['capture_schema'] !== M3_COHORT_STORE_SCHEMA)
832
+ fail('invalid_store');
833
+ const cohortRef = assertHmacRef(value['cohort_ref']);
834
+ const verifier = assertHmacRef(value['cohort_key_verifier']);
835
+ const consentRef = assertHmacRef(value['consent_ref']);
836
+ parseIsoMs(String(value['created_at']));
837
+ return {
838
+ schema_version: M3_COHORT_MANIFEST_SCHEMA,
839
+ capture_schema: M3_COHORT_STORE_SCHEMA,
840
+ cohort_ref: cohortRef,
841
+ cohort_key_verifier: verifier,
842
+ created_at: String(value['created_at']),
843
+ consent_ref: consentRef,
844
+ evidence_manifest: assertEvidenceManifest(value['evidence_manifest']),
845
+ };
846
+ }
847
+ function readManifest(storeRoot) {
848
+ const path = manifestPath(storeRoot);
849
+ assertManagedFilePath(storeRoot, path);
850
+ if (!existsSync(path))
851
+ fail('cohort_not_initialized');
852
+ let parsed;
853
+ try {
854
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
855
+ }
856
+ catch {
857
+ fail('invalid_store');
858
+ }
859
+ return assertCaptureManifest(parsed);
860
+ }
861
+ function assertCaptureEvent(value) {
862
+ if (!isObject(value))
863
+ fail('invalid_store');
864
+ if (value['schema_version'] !== M3_COHORT_EVENT_SCHEMA)
865
+ fail('invalid_store');
866
+ const base = {
867
+ schema_version: M3_COHORT_EVENT_SCHEMA,
868
+ event_ref: assertHmacRef(value['event_ref']),
869
+ occurred_at: (() => { parseIsoMs(String(value['occurred_at'])); return String(value['occurred_at']); })(),
870
+ consent_ref: assertHmacRef(value['consent_ref']),
871
+ };
872
+ switch (value['kind']) {
873
+ case 'participant_enrolled': {
874
+ exactKeys(value, ['schema_version', 'event_ref', 'kind', 'occurred_at', 'consent_ref', 'participant_key', 'invited', 'consent', 'test_or_staff', 'simulation']);
875
+ for (const flag of ['invited', 'consent', 'test_or_staff']) {
876
+ if (typeof value[flag] !== 'boolean')
877
+ fail('invalid_store');
878
+ }
879
+ const simulation = value['simulation'];
880
+ let parsedSimulation = null;
881
+ if (simulation !== null) {
882
+ try {
883
+ parsedSimulation = assertSimulationProvenance(simulation);
884
+ }
885
+ catch {
886
+ fail('invalid_store');
887
+ }
888
+ }
889
+ return {
890
+ ...base,
891
+ kind: 'participant_enrolled',
892
+ participant_key: assertHmacRef(value['participant_key']),
893
+ invited: value['invited'],
894
+ consent: value['consent'],
895
+ test_or_staff: value['test_or_staff'],
896
+ simulation: parsedSimulation,
897
+ };
898
+ }
899
+ case 'plan_delivered': {
900
+ exactKeys(value, ['schema_version', 'event_ref', 'kind', 'occurred_at', 'consent_ref', 'participant_key', 'plan_key', 'attribution']);
901
+ const attribution = value['attribution'];
902
+ if (attribution !== 'gotry_primary' && attribution !== 'gotry_assisted')
903
+ fail('invalid_store');
904
+ return {
905
+ ...base,
906
+ kind: 'plan_delivered',
907
+ participant_key: assertHmacRef(value['participant_key']),
908
+ plan_key: assertHmacRef(value['plan_key']),
909
+ attribution,
910
+ };
911
+ }
912
+ case 'plan_finalized': {
913
+ exactKeys(value, ['schema_version', 'event_ref', 'kind', 'occurred_at', 'consent_ref', 'participant_key', 'plan_key']);
914
+ return {
915
+ ...base,
916
+ kind: 'plan_finalized',
917
+ participant_key: assertHmacRef(value['participant_key']),
918
+ plan_key: assertHmacRef(value['plan_key']),
919
+ };
920
+ }
921
+ case 'nps_recorded': {
922
+ exactKeys(value, ['schema_version', 'event_ref', 'kind', 'occurred_at', 'consent_ref', 'participant_key', 'plan_key', 'nps_score']);
923
+ const score = value['nps_score'];
924
+ if (typeof score !== 'number' || !Number.isInteger(score) || score < 0 || score > 10)
925
+ fail('invalid_store');
926
+ return {
927
+ ...base,
928
+ kind: 'nps_recorded',
929
+ participant_key: assertHmacRef(value['participant_key']),
930
+ plan_key: assertHmacRef(value['plan_key']),
931
+ nps_score: score,
932
+ };
933
+ }
934
+ case 'poi_audit_locked': {
935
+ exactKeys(value, ['schema_version', 'event_ref', 'kind', 'occurred_at', 'consent_ref', 'participant_key', 'plan_key', 'locked_claims', 'invalid_claims']);
936
+ const locked = value['locked_claims'];
937
+ const invalid = value['invalid_claims'];
938
+ if (typeof locked !== 'number' || !Number.isInteger(locked) || locked < 0)
939
+ fail('invalid_store');
940
+ if (typeof invalid !== 'number' || !Number.isInteger(invalid) || invalid < 0 || invalid > locked)
941
+ fail('invalid_store');
942
+ return {
943
+ ...base,
944
+ kind: 'poi_audit_locked',
945
+ participant_key: assertHmacRef(value['participant_key']),
946
+ plan_key: assertHmacRef(value['plan_key']),
947
+ locked_claims: locked,
948
+ invalid_claims: invalid,
949
+ };
950
+ }
951
+ default:
952
+ fail('invalid_store');
953
+ }
954
+ }
955
+ function readEvents(storeRoot) {
956
+ const path = eventsPath(storeRoot);
957
+ assertManagedFilePath(storeRoot, path);
958
+ if (!existsSync(path))
959
+ return [];
960
+ let raw;
961
+ try {
962
+ raw = readFileSync(path, 'utf8');
963
+ }
964
+ catch {
965
+ fail('invalid_store');
966
+ }
967
+ const events = [];
968
+ const seen = new Map();
969
+ for (const line of raw.split('\n')) {
970
+ if (line.trim() === '')
971
+ continue;
972
+ let parsed;
973
+ try {
974
+ parsed = JSON.parse(line);
975
+ }
976
+ catch {
977
+ fail('invalid_store');
978
+ }
979
+ const event = assertCaptureEvent(parsed);
980
+ const canonical = canonicalJson(event);
981
+ const prior = seen.get(event.event_ref);
982
+ if (prior !== undefined) {
983
+ if (prior !== canonical)
984
+ fail('invalid_store');
985
+ continue;
986
+ }
987
+ seen.set(event.event_ref, canonical);
988
+ events.push(event);
989
+ }
990
+ return events;
991
+ }
992
+ export function projectCaptureEvents(events) {
993
+ const participants = new Map();
994
+ const plans = new Map();
995
+ const npsByParticipant = new Set();
996
+ for (const event of events) {
997
+ switch (event.kind) {
998
+ case 'participant_enrolled': {
999
+ if (participants.has(event.participant_key))
1000
+ fail('invalid_store');
1001
+ participants.set(event.participant_key, {
1002
+ participantKey: event.participant_key,
1003
+ invited: event.invited,
1004
+ consent: event.consent,
1005
+ testOrStaff: event.test_or_staff,
1006
+ simulation: event.simulation,
1007
+ enrolledAt: event.occurred_at,
1008
+ });
1009
+ break;
1010
+ }
1011
+ case 'plan_delivered': {
1012
+ const participant = participants.get(event.participant_key);
1013
+ if (!participant)
1014
+ fail('invalid_store');
1015
+ if (plans.has(event.plan_key))
1016
+ fail('invalid_store');
1017
+ if (parseIsoMs(event.occurred_at) < parseIsoMs(participant.enrolledAt))
1018
+ fail('invalid_store');
1019
+ plans.set(event.plan_key, {
1020
+ participantKey: event.participant_key,
1021
+ planKey: event.plan_key,
1022
+ attribution: event.attribution,
1023
+ deliveredAt: event.occurred_at,
1024
+ });
1025
+ break;
1026
+ }
1027
+ case 'plan_finalized': {
1028
+ const plan = plans.get(event.plan_key);
1029
+ if (!plan || plan.participantKey !== event.participant_key || plan.finalizedAt !== undefined)
1030
+ fail('invalid_store');
1031
+ if (parseIsoMs(event.occurred_at) < parseIsoMs(plan.deliveredAt))
1032
+ fail('invalid_store');
1033
+ plan.finalizedAt = event.occurred_at;
1034
+ break;
1035
+ }
1036
+ case 'nps_recorded': {
1037
+ const plan = plans.get(event.plan_key);
1038
+ if (!plan || plan.participantKey !== event.participant_key || plan.npsScore !== undefined)
1039
+ fail('invalid_store');
1040
+ if (npsByParticipant.has(event.participant_key))
1041
+ fail('invalid_store');
1042
+ if (parseIsoMs(event.occurred_at) < parseIsoMs(plan.deliveredAt))
1043
+ fail('invalid_store');
1044
+ npsByParticipant.add(event.participant_key);
1045
+ plan.npsScore = event.nps_score;
1046
+ plan.npsRecordedAt = event.occurred_at;
1047
+ break;
1048
+ }
1049
+ case 'poi_audit_locked': {
1050
+ const plan = plans.get(event.plan_key);
1051
+ if (!plan || plan.participantKey !== event.participant_key || plan.poiLockedAt !== undefined)
1052
+ fail('invalid_store');
1053
+ if (parseIsoMs(event.occurred_at) < parseIsoMs(plan.deliveredAt))
1054
+ fail('invalid_store');
1055
+ plan.poiLockedAt = event.occurred_at;
1056
+ plan.lockedClaims = event.locked_claims;
1057
+ plan.invalidClaims = event.invalid_claims;
1058
+ break;
1059
+ }
1060
+ }
1061
+ }
1062
+ return { participants, plans };
1063
+ }
1064
+ function assertCandidateProjection(events, event) {
1065
+ try {
1066
+ projectCaptureEvents([...events, event]);
1067
+ }
1068
+ catch (error) {
1069
+ if (error instanceof M3CohortError && error.code === 'invalid_store')
1070
+ fail('invalid_transition');
1071
+ throw error;
1072
+ }
1073
+ }
1074
+ function appendEvent(storeRoot, events, event) {
1075
+ assertCandidateProjection(events, event);
1076
+ assertManagedStoreIsolation(storeRoot);
1077
+ appendPrivateLine(storeRoot, eventsPath(storeRoot), JSON.stringify(event));
1078
+ }
1079
+ function eventExists(events, ref) {
1080
+ return events.some(event => event.event_ref === ref);
1081
+ }
1082
+ /** The verifier is domain-separated by the manifest's own `evidence_kind`, so a store whose
1083
+ * kind was edited on disk no longer matches its key. */
1084
+ function assertCohortKeyMatches(manifest, hmacKey) {
1085
+ if (manifest.cohort_key_verifier !== cohortKeyVerifier(hmacKey, manifest.evidence_manifest.evidence_kind))
1086
+ fail('cohort_key_mismatch');
1087
+ }
1088
+ function loadLockedStore(common) {
1089
+ const manifest = readManifest(common.storeRoot);
1090
+ assertCohortKeyMatches(manifest, common.hmacKey);
1091
+ if (manifest.consent_ref !== common.consentRef)
1092
+ fail('cohort_consent_mismatch');
1093
+ const events = readEvents(common.storeRoot);
1094
+ if (events.some(event => event.consent_ref !== manifest.consent_ref))
1095
+ fail('invalid_store');
1096
+ return { manifest, events, projection: projectCaptureEvents(events) };
1097
+ }
1098
+ function buildEvidenceManifest(options, cohortRef, lockedAt) {
1099
+ const kind = assertEvidenceKind(options.evidenceKind);
1100
+ const startAt = assertTimestampInput(options.windowStartAt);
1101
+ const endAt = assertTimestampInput(options.windowEndAt);
1102
+ if (Date.parse(startAt) > Date.parse(endAt))
1103
+ fail('invalid_input');
1104
+ const timezone = assertRawLabel(options.timezone, 64);
1105
+ if (!Array.isArray(options.allowedAttribution) || options.allowedAttribution.length === 0)
1106
+ fail('invalid_input');
1107
+ const allowed = options.allowedAttribution.map(assertAttribution);
1108
+ if (new Set(allowed).size !== allowed.length)
1109
+ fail('invalid_input');
1110
+ return {
1111
+ schema_version: M3_EVIDENCE_MANIFEST_SCHEMA,
1112
+ evidence_kind: kind,
1113
+ // The cohort id published into the evidence root is itself the pseudonym: an operator
1114
+ // label never crosses into the evidence face.
1115
+ cohort_id: cohortRef,
1116
+ locked_at: lockedAt,
1117
+ window: { start_at: startAt, end_at: endAt, timezone },
1118
+ eligibility: {
1119
+ sample_unit: 'unique_participant_with_eligible_delivered_plan',
1120
+ requires_invitation: true,
1121
+ requires_consent: true,
1122
+ allowed_attribution: allowed,
1123
+ exclusion_codes: [...M3_EXCLUSION_CODES],
1124
+ },
1125
+ metrics: {
1126
+ sample_size: { minimum: M3_ACCEPTANCE.sampleMinimum, maximum: M3_ACCEPTANCE.sampleMaximum },
1127
+ finalization_rate: { formula: M3_FORMULAS.finalization, minimum: M3_ACCEPTANCE.finalizationMinimum },
1128
+ nps: { formula: M3_FORMULAS.nps, minimum: M3_ACCEPTANCE.npsMinimum },
1129
+ poi_hallucination_rate: { formula: M3_FORMULAS.poi, exclusive_maximum: M3_ACCEPTANCE.poiExclusiveMaximum },
1130
+ },
1131
+ nightly: {
1132
+ requires_real_llm: true,
1133
+ requires_prompt_set_sha256: true,
1134
+ requires_output_sha256: true,
1135
+ requires_cost_usd: true,
1136
+ },
1137
+ };
1138
+ }
1139
+ function sameFrozenCohort(existing, next) {
1140
+ return existing.cohort_ref === next.cohort_ref
1141
+ && existing.cohort_key_verifier === next.cohort_key_verifier
1142
+ && existing.consent_ref === next.consent_ref
1143
+ && canonicalJson({ ...existing.evidence_manifest, locked_at: '' }) === canonicalJson({ ...next.evidence_manifest, locked_at: '' });
1144
+ }
1145
+ export function initM3Cohort(options, clock = systemClock) {
1146
+ const common = prepareCommon(options);
1147
+ const evidenceKind = assertEvidenceKind(options.evidenceKind);
1148
+ const cohortRef = cohortRefFor(common.hmacKey, evidenceKind, options.cohort);
1149
+ const next = {
1150
+ schema_version: M3_COHORT_MANIFEST_SCHEMA,
1151
+ capture_schema: M3_COHORT_STORE_SCHEMA,
1152
+ cohort_ref: cohortRef,
1153
+ cohort_key_verifier: cohortKeyVerifier(common.hmacKey, evidenceKind),
1154
+ created_at: assertIsoDate(clock.now()),
1155
+ consent_ref: common.consentRef,
1156
+ evidence_manifest: buildEvidenceManifest(options, cohortRef, assertIsoDate(clock.now())),
1157
+ };
1158
+ return withStoreLock(common.storeRoot, true, () => {
1159
+ const path = manifestPath(common.storeRoot);
1160
+ if (existsSync(path)) {
1161
+ const existing = readManifest(common.storeRoot);
1162
+ assertCohortKeyMatches(existing, common.hmacKey);
1163
+ if (existing.consent_ref !== next.consent_ref)
1164
+ fail('cohort_consent_mismatch');
1165
+ if (existing.evidence_manifest.evidence_kind !== next.evidence_manifest.evidence_kind)
1166
+ fail('evidence_kind_mismatch');
1167
+ if (!sameFrozenCohort(existing, next))
1168
+ fail('cohort_already_initialized');
1169
+ return {
1170
+ schema: 'gotry_m3_cohort_cli_result.v1',
1171
+ ok: true,
1172
+ command: 'init',
1173
+ status: 'unchanged',
1174
+ cohort_ref: existing.cohort_ref,
1175
+ evidence_kind: existing.evidence_manifest.evidence_kind,
1176
+ };
1177
+ }
1178
+ writeManagedFileNoOverwrite(common.storeRoot, path, `${JSON.stringify(next, null, 2)}\n`, 'cohort_already_initialized');
1179
+ return {
1180
+ schema: 'gotry_m3_cohort_cli_result.v1',
1181
+ ok: true,
1182
+ command: 'init',
1183
+ status: 'recorded',
1184
+ cohort_ref: next.cohort_ref,
1185
+ evidence_kind: next.evidence_manifest.evidence_kind,
1186
+ };
1187
+ });
1188
+ }
1189
+ export function enrollParticipant(options, clock = systemClock) {
1190
+ const common = prepareCommon(options);
1191
+ const participantLabel = assertRawLabel(options.participant);
1192
+ for (const flag of [options.invited, options.participantConsent, options.testOrStaff]) {
1193
+ if (typeof flag !== 'boolean')
1194
+ fail('invalid_input');
1195
+ }
1196
+ const simulation = options.simulation === undefined ? null : assertSimulationProvenance(options.simulation);
1197
+ return withStoreLock(common.storeRoot, false, () => {
1198
+ const { manifest, events } = loadLockedStore(common);
1199
+ const kind = manifest.evidence_manifest.evidence_kind;
1200
+ // The two evidence kinds are mutually exclusive by construction: a simulated
1201
+ // participant can never enter a real cohort, and an unlabelled participant can never
1202
+ // enter a synthetic one.
1203
+ if (kind === 'real_seed_cohort' && simulation !== null)
1204
+ fail('simulation_forbidden_in_real_cohort');
1205
+ if (kind === 'synthetic_fixture' && simulation === null)
1206
+ fail('simulation_label_required');
1207
+ const participantKey = participantKeyFor(common.hmacKey, kind, participantLabel);
1208
+ const ref = eventRef(common.hmacKey, 'participant_enrolled', [
1209
+ participantKey,
1210
+ String(options.invited),
1211
+ String(options.participantConsent),
1212
+ String(options.testOrStaff),
1213
+ simulation === null ? 'none' : canonicalJson(simulation),
1214
+ ]);
1215
+ if (eventExists(events, ref)) {
1216
+ return { schema: 'gotry_m3_cohort_cli_result.v1', ok: true, command: 'enroll', status: 'unchanged', event_ref: ref, participant_key: participantKey };
1217
+ }
1218
+ const event = {
1219
+ schema_version: M3_COHORT_EVENT_SCHEMA,
1220
+ event_ref: ref,
1221
+ kind: 'participant_enrolled',
1222
+ occurred_at: assertIsoDate(clock.now()),
1223
+ consent_ref: common.consentRef,
1224
+ participant_key: participantKey,
1225
+ invited: options.invited,
1226
+ consent: options.participantConsent,
1227
+ test_or_staff: options.testOrStaff,
1228
+ simulation,
1229
+ };
1230
+ appendEvent(common.storeRoot, events, event);
1231
+ return { schema: 'gotry_m3_cohort_cli_result.v1', ok: true, command: 'enroll', status: 'recorded', event_ref: ref, participant_key: participantKey };
1232
+ });
1233
+ }
1234
+ function withPlanContext(options, body) {
1235
+ const common = prepareCommon(options);
1236
+ const participantLabel = assertRawLabel(options.participant);
1237
+ const planLabel = assertRawLabel(options.plan);
1238
+ return withStoreLock(common.storeRoot, false, () => {
1239
+ const { manifest, events } = loadLockedStore(common);
1240
+ const kind = manifest.evidence_manifest.evidence_kind;
1241
+ return body({
1242
+ common,
1243
+ manifest,
1244
+ events,
1245
+ participantKey: participantKeyFor(common.hmacKey, kind, participantLabel),
1246
+ planKey: planKeyFor(common.hmacKey, kind, participantLabel, planLabel),
1247
+ });
1248
+ });
1249
+ }
1250
+ export function recordPlanDelivered(options, clock = systemClock) {
1251
+ const attribution = assertAttribution(options.attribution);
1252
+ return withPlanContext(options, ({ common, events, participantKey, planKey }) => {
1253
+ const ref = eventRef(common.hmacKey, 'plan_delivered', [participantKey, planKey, attribution]);
1254
+ if (eventExists(events, ref)) {
1255
+ return { schema: 'gotry_m3_cohort_cli_result.v1', ok: true, command: 'deliver', status: 'unchanged', event_ref: ref, plan_key: planKey };
1256
+ }
1257
+ const event = {
1258
+ schema_version: M3_COHORT_EVENT_SCHEMA,
1259
+ event_ref: ref,
1260
+ kind: 'plan_delivered',
1261
+ occurred_at: assertIsoDate(clock.now()),
1262
+ consent_ref: common.consentRef,
1263
+ participant_key: participantKey,
1264
+ plan_key: planKey,
1265
+ attribution,
1266
+ };
1267
+ appendEvent(common.storeRoot, events, event);
1268
+ return { schema: 'gotry_m3_cohort_cli_result.v1', ok: true, command: 'deliver', status: 'recorded', event_ref: ref, plan_key: planKey };
1269
+ });
1270
+ }
1271
+ export function recordPlanFinalized(options, clock = systemClock) {
1272
+ return withPlanContext(options, ({ common, events, participantKey, planKey }) => {
1273
+ const ref = eventRef(common.hmacKey, 'plan_finalized', [participantKey, planKey]);
1274
+ if (eventExists(events, ref)) {
1275
+ return { schema: 'gotry_m3_cohort_cli_result.v1', ok: true, command: 'finalize', status: 'unchanged', event_ref: ref, plan_key: planKey };
1276
+ }
1277
+ const event = {
1278
+ schema_version: M3_COHORT_EVENT_SCHEMA,
1279
+ event_ref: ref,
1280
+ kind: 'plan_finalized',
1281
+ occurred_at: assertIsoDate(clock.now()),
1282
+ consent_ref: common.consentRef,
1283
+ participant_key: participantKey,
1284
+ plan_key: planKey,
1285
+ };
1286
+ appendEvent(common.storeRoot, events, event);
1287
+ return { schema: 'gotry_m3_cohort_cli_result.v1', ok: true, command: 'finalize', status: 'recorded', event_ref: ref, plan_key: planKey };
1288
+ });
1289
+ }
1290
+ export function recordNps(options, clock = systemClock) {
1291
+ const score = assertNpsScore(options.score);
1292
+ return withPlanContext(options, ({ common, events, participantKey, planKey }) => {
1293
+ const ref = eventRef(common.hmacKey, 'nps_recorded', [participantKey, planKey, String(score)]);
1294
+ if (eventExists(events, ref)) {
1295
+ return { schema: 'gotry_m3_cohort_cli_result.v1', ok: true, command: 'nps', status: 'unchanged', event_ref: ref, plan_key: planKey };
1296
+ }
1297
+ const event = {
1298
+ schema_version: M3_COHORT_EVENT_SCHEMA,
1299
+ event_ref: ref,
1300
+ kind: 'nps_recorded',
1301
+ occurred_at: assertIsoDate(clock.now()),
1302
+ consent_ref: common.consentRef,
1303
+ participant_key: participantKey,
1304
+ plan_key: planKey,
1305
+ nps_score: score,
1306
+ };
1307
+ appendEvent(common.storeRoot, events, event);
1308
+ return { schema: 'gotry_m3_cohort_cli_result.v1', ok: true, command: 'nps', status: 'recorded', event_ref: ref, plan_key: planKey };
1309
+ });
1310
+ }
1311
+ export function recordPoiLock(options, clock = systemClock) {
1312
+ const locked = assertNonNegativeInteger(options.lockedClaims);
1313
+ const invalid = assertNonNegativeInteger(options.invalidClaims);
1314
+ if (invalid > locked)
1315
+ fail('invalid_input');
1316
+ return withPlanContext(options, ({ common, events, participantKey, planKey }) => {
1317
+ const ref = eventRef(common.hmacKey, 'poi_audit_locked', [participantKey, planKey, String(locked), String(invalid)]);
1318
+ if (eventExists(events, ref)) {
1319
+ return { schema: 'gotry_m3_cohort_cli_result.v1', ok: true, command: 'poi-lock', status: 'unchanged', event_ref: ref, plan_key: planKey };
1320
+ }
1321
+ const event = {
1322
+ schema_version: M3_COHORT_EVENT_SCHEMA,
1323
+ event_ref: ref,
1324
+ kind: 'poi_audit_locked',
1325
+ occurred_at: assertIsoDate(clock.now()),
1326
+ consent_ref: common.consentRef,
1327
+ participant_key: participantKey,
1328
+ plan_key: planKey,
1329
+ locked_claims: locked,
1330
+ invalid_claims: invalid,
1331
+ };
1332
+ appendEvent(common.storeRoot, events, event);
1333
+ return { schema: 'gotry_m3_cohort_cli_result.v1', ok: true, command: 'poi-lock', status: 'recorded', event_ref: ref, plan_key: planKey };
1334
+ });
1335
+ }
1336
+ /** Build the evidence payload from the capture store. A plan without a locked POI audit is
1337
+ * not exportable: the scorer requires `poi_audit`, and inventing one would be fabrication. */
1338
+ export function buildExportPayload(manifest, events, exportedAt, hmacKey) {
1339
+ const projection = projectCaptureEvents(events);
1340
+ const evidenceKind = manifest.evidence_manifest.evidence_kind;
1341
+ const cohort = [];
1342
+ const provenance = [];
1343
+ const plans = [...projection.plans.values()].sort((a, b) => a.planKey.localeCompare(b.planKey));
1344
+ for (const plan of plans) {
1345
+ const participant = projection.participants.get(plan.participantKey);
1346
+ if (!participant)
1347
+ fail('invalid_store');
1348
+ if (plan.poiLockedAt === undefined || plan.lockedClaims === undefined || plan.invalidClaims === undefined)
1349
+ continue;
1350
+ // The capture-side kind gate is re-asserted at export: nothing may slip through a
1351
+ // store that was mutated between commands.
1352
+ if (evidenceKind === 'real_seed_cohort' && participant.simulation !== null)
1353
+ fail('simulation_forbidden_in_real_cohort');
1354
+ if (evidenceKind === 'synthetic_fixture' && participant.simulation === null)
1355
+ fail('simulation_label_required');
1356
+ cohort.push({
1357
+ schema_version: M3_COHORT_RECORD_SCHEMA,
1358
+ participant_key: plan.participantKey,
1359
+ plan_key: plan.planKey,
1360
+ invited: participant.invited,
1361
+ consent: participant.consent,
1362
+ test_or_staff: participant.testOrStaff,
1363
+ attribution: plan.attribution,
1364
+ delivered_at: plan.deliveredAt,
1365
+ finalized_at: plan.finalizedAt ?? null,
1366
+ nps_score: plan.npsScore ?? null,
1367
+ nps_recorded_at: plan.npsRecordedAt ?? null,
1368
+ poi_audit: { locked_at: plan.poiLockedAt, locked_claims: plan.lockedClaims, invalid_claims: plan.invalidClaims },
1369
+ });
1370
+ if (participant.simulation !== null) {
1371
+ provenance.push({ ...participant.simulation, participant_key: plan.participantKey, plan_key: plan.planKey });
1372
+ }
1373
+ }
1374
+ const manifestBytes = `${JSON.stringify(manifest.evidence_manifest, null, 2)}\n`;
1375
+ const recordLines = cohort.map(record => JSON.stringify(record));
1376
+ const provenanceBytes = provenance.map(row => JSON.stringify(row)).join('\n') + (provenance.length > 0 ? '\n' : '');
1377
+ const body = {
1378
+ schema_version: M3_EXPORT_ATTESTATION_SCHEMA,
1379
+ cohort_id: manifest.evidence_manifest.cohort_id,
1380
+ evidence_kind: evidenceKind,
1381
+ exported_at: exportedAt,
1382
+ record_count: cohort.length,
1383
+ simulated_record_count: provenance.length,
1384
+ capture_manifest_digest_sha256: sha256Hex(canonicalJson(manifest)),
1385
+ manifest_sha256: sha256Hex(manifestBytes),
1386
+ cohort_records_sha256: cohortRecordsDigest(recordLines),
1387
+ provenance_jsonl_sha256: sha256Hex(provenanceBytes),
1388
+ };
1389
+ const attestation = { ...body, attestation_hmac: attestationMac(hmacKey, body) };
1390
+ return { manifest: manifest.evidence_manifest, cohort, provenance, attestation };
1391
+ }
1392
+ export function exportM3Cohort(options, clock = systemClock) {
1393
+ const common = prepareCommon(options);
1394
+ const evidenceRoot = assertOutputPath(assertRawLabel(options.evidenceRoot, 4096));
1395
+ return withStoreLock(common.storeRoot, false, () => {
1396
+ const { manifest, events } = loadLockedStore(common);
1397
+ const payload = buildExportPayload(manifest, events, assertIsoDate(clock.now()), common.hmacKey);
1398
+ const attestationPath = join(evidenceRoot, 'export-attestation.json');
1399
+ const targets = [join(evidenceRoot, 'manifest.json'), join(evidenceRoot, 'cohort.jsonl'), join(evidenceRoot, 'provenance.jsonl'), attestationPath];
1400
+ // An evidence root that already carries an attestation of a different kind is never
1401
+ // appended to: real and simulated evidence never share a directory.
1402
+ if (pathExistsByLstat(attestationPath)) {
1403
+ try {
1404
+ const existing = JSON.parse(readFileSync(attestationPath, 'utf8'));
1405
+ if (existing['evidence_kind'] !== payload.attestation.evidence_kind)
1406
+ fail('evidence_root_conflict');
1407
+ }
1408
+ catch (error) {
1409
+ if (error instanceof M3CohortError)
1410
+ throw error;
1411
+ fail('evidence_root_conflict');
1412
+ }
1413
+ fail('output_exists');
1414
+ }
1415
+ // Every target is checked before the first byte is written. Checking only the
1416
+ // attestation would let a root that already holds somebody else's cohort.jsonl take
1417
+ // this export's manifest.json and then fail — leaving a manifest of one kind sitting
1418
+ // over records of another, which the scorer would read without ever consulting verify.
1419
+ for (const target of targets) {
1420
+ if (pathExistsByLstat(target))
1421
+ fail('output_exists');
1422
+ }
1423
+ if (options.validate) {
1424
+ try {
1425
+ options.validate(payload);
1426
+ }
1427
+ catch (error) {
1428
+ if (error instanceof M3CohortError)
1429
+ throw error;
1430
+ fail('export_validation_failed');
1431
+ }
1432
+ }
1433
+ const manifestBytes = `${JSON.stringify(payload.manifest, null, 2)}\n`;
1434
+ const recordLines = payload.cohort.map(record => JSON.stringify(record));
1435
+ const cohortBytes = recordLines.join('\n') + (recordLines.length > 0 ? '\n' : '');
1436
+ const provenanceBytes = payload.provenance.map(row => JSON.stringify(row)).join('\n') + (payload.provenance.length > 0 ? '\n' : '');
1437
+ const bytesByTarget = [manifestBytes, cohortBytes, provenanceBytes, `${JSON.stringify(payload.attestation, null, 2)}\n`];
1438
+ // All-or-nothing: a partial export is rolled back, so no reader can ever meet a
1439
+ // manifest without the records and attestation that belong to it.
1440
+ const written = [];
1441
+ try {
1442
+ for (const [index, target] of targets.entries()) {
1443
+ writeOutputFileNoOverwrite(target, bytesByTarget[index]);
1444
+ written.push(target);
1445
+ }
1446
+ }
1447
+ catch (error) {
1448
+ for (const target of written.reverse()) {
1449
+ try {
1450
+ unlinkSync(target);
1451
+ }
1452
+ catch { /* best effort rollback */ }
1453
+ }
1454
+ throw error;
1455
+ }
1456
+ return {
1457
+ payload,
1458
+ result: {
1459
+ schema: 'gotry_m3_cohort_cli_result.v1',
1460
+ ok: true,
1461
+ command: 'export',
1462
+ status: 'recorded',
1463
+ cohort_ref: manifest.cohort_ref,
1464
+ evidence_kind: payload.attestation.evidence_kind,
1465
+ record_count: payload.attestation.record_count,
1466
+ simulated_record_count: payload.attestation.simulated_record_count,
1467
+ output_written: true,
1468
+ },
1469
+ };
1470
+ });
1471
+ }
1472
+ /**
1473
+ * Re-derive every digest in `export-attestation.json` from the bytes on disk AND check the
1474
+ * keyed MAC over the attestation body.
1475
+ *
1476
+ * What this proves: the four files are exactly the ones a capture store holding this HMAC
1477
+ * key exported together, and none of them has changed since. Flipping
1478
+ * `manifest.json.evidence_kind` from `synthetic_fixture` to `real_seed_cohort` leaves the
1479
+ * scorer happy — it trusts the manifest it is handed — but breaks `manifest_sha256`; and a
1480
+ * forger who recomputes all three digests and edits `evidence_kind` plus
1481
+ * `simulated_record_count` in the attestation still cannot produce `attestation_hmac`,
1482
+ * which is why this function requires the key rather than being a pure byte check.
1483
+ *
1484
+ * What this does not prove: that the participants behind the records were real people. That
1485
+ * is a claim about collection, not about bytes, and no digest can establish it.
1486
+ */
1487
+ export function verifyExportedEvidence(evidenceRoot, hmacKey) {
1488
+ const key = normalizeHmacKey(hmacKey);
1489
+ const root = assertOutputPath(assertRawLabel(evidenceRoot, 4096));
1490
+ const read = (name) => {
1491
+ const path = join(root, name);
1492
+ assertNoSymlinkLeaf(path);
1493
+ if (!existsSync(path))
1494
+ fail('export_validation_failed');
1495
+ try {
1496
+ return readFileSync(path, 'utf8');
1497
+ }
1498
+ catch {
1499
+ fail('export_validation_failed');
1500
+ }
1501
+ };
1502
+ const manifestBytes = read('manifest.json');
1503
+ const cohortBytes = read('cohort.jsonl');
1504
+ const provenanceBytes = read('provenance.jsonl');
1505
+ let attestation;
1506
+ try {
1507
+ attestation = JSON.parse(read('export-attestation.json'));
1508
+ }
1509
+ catch {
1510
+ fail('export_validation_failed');
1511
+ }
1512
+ if (!isObject(attestation))
1513
+ fail('export_validation_failed');
1514
+ exactKeys(attestation, [...ATTESTATION_BODY_KEYS, 'attestation_hmac']);
1515
+ if (attestation['schema_version'] !== M3_EXPORT_ATTESTATION_SCHEMA)
1516
+ fail('export_validation_failed');
1517
+ const evidenceKind = assertEvidenceKind(attestation['evidence_kind']);
1518
+ // Keyed first: without the MAC every other check below is recomputable by a forger.
1519
+ const presentedMac = attestation['attestation_hmac'];
1520
+ if (!nonEmptyString(presentedMac) || !HMAC_REF_PATTERN.test(presentedMac))
1521
+ fail('export_validation_failed');
1522
+ const body = Object.fromEntries(ATTESTATION_BODY_KEYS.map(bodyKey => [bodyKey, attestation[bodyKey]]));
1523
+ const expectedMac = pseudonymousRef(key, 'export-attestation', [canonicalJson(body)]);
1524
+ if (presentedMac.length !== expectedMac.length || !timingSafeEqual(Buffer.from(presentedMac), Buffer.from(expectedMac))) {
1525
+ fail('export_validation_failed');
1526
+ }
1527
+ const recordLines = cohortRecordLines(cohortBytes);
1528
+ if (sha256Hex(manifestBytes) !== attestation['manifest_sha256'])
1529
+ fail('export_validation_failed');
1530
+ if (cohortRecordsDigest(recordLines) !== attestation['cohort_records_sha256'])
1531
+ fail('export_validation_failed');
1532
+ if (sha256Hex(provenanceBytes) !== attestation['provenance_jsonl_sha256'])
1533
+ fail('export_validation_failed');
1534
+ const manifest = assertEvidenceManifest(JSON.parse(manifestBytes));
1535
+ if (manifest.evidence_kind !== evidenceKind)
1536
+ fail('export_validation_failed');
1537
+ if (manifest.cohort_id !== attestation['cohort_id'])
1538
+ fail('export_validation_failed');
1539
+ const records = recordLines.map(line => JSON.parse(line));
1540
+ if (records.length !== attestation['record_count'])
1541
+ fail('export_validation_failed');
1542
+ const provenanceRows = provenanceBytes.split('\n').filter(line => line.trim() !== '').map(line => JSON.parse(line));
1543
+ if (provenanceRows.length !== attestation['simulated_record_count'])
1544
+ fail('export_validation_failed');
1545
+ // A synthetic export must label every single record; a real export must label none.
1546
+ const planKeys = new Set(records.map(record => String(record['plan_key'])));
1547
+ const labelled = new Set(provenanceRows.map(row => String(row['plan_key'])));
1548
+ if (evidenceKind === 'synthetic_fixture') {
1549
+ if (labelled.size !== planKeys.size)
1550
+ fail('export_validation_failed');
1551
+ for (const key of planKeys)
1552
+ if (!labelled.has(key))
1553
+ fail('export_validation_failed');
1554
+ }
1555
+ else if (labelled.size !== 0) {
1556
+ fail('export_validation_failed');
1557
+ }
1558
+ for (const row of provenanceRows) {
1559
+ const { participant_key: _participant, plan_key: _plan, ...rest } = row;
1560
+ assertSimulationProvenance(rest);
1561
+ }
1562
+ return {
1563
+ schema: 'gotry_m3_export_verification.v1',
1564
+ ok: true,
1565
+ evidence_kind: evidenceKind,
1566
+ record_count: records.length,
1567
+ simulated_record_count: provenanceRows.length,
1568
+ cohort_id: manifest.cohort_id,
1569
+ };
1570
+ }
1571
+ export function readStoreForTests(stateRoot) {
1572
+ const storeRoot = storeRootForStateRoot(stateRoot);
1573
+ return { manifest: readManifest(storeRoot), events: readEvents(storeRoot) };
1574
+ }
1575
+ export function fileMode(path) {
1576
+ return statSync(path).mode & 0o777;
1577
+ }