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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -1,24 +1,43 @@
1
+ /**
2
+ * M3 真实种子 cohort 指标评分器(issue #22)。
3
+ *
4
+ * 私有证据默认位于 gotry-state/evidence/m3/:
5
+ * manifest.json 冻结样本窗口、纳排、分母、归因与阈值
6
+ * cohort.jsonl 脱敏 plan 记录与 nightly real-LLM 运行记录
7
+ * summary.json 可复跑的聚合结果(仅显式 --write-summary 时写入)
8
+ *
9
+ * 运行:
10
+ * npx tsx scripts/product-metrics.ts --evidence-root gotry-state/evidence/m3 --format json
11
+ * npx tsx scripts/product-metrics.ts --evidence-root gotry-state/evidence/m3 --write-summary
12
+ * npx tsx scripts/product-metrics.ts --fixture data/product-metrics-fixture.json --format json
13
+ *
14
+ * 纪律:
15
+ * - fixture 只验证 schema 与公式,永远不能得到 business_pass=true。
16
+ * - participant/plan/run 只接受 HMAC-SHA256 假名键;未知字段 fail-closed,
17
+ * 防止姓名、邮箱、电话、原始会话等进入证据面。
18
+ * - 分母为 0 时指标 unavailable(pass=false),不得把空样本算作 0% 幻觉。
19
+ */
1
20
  import { createHash } from 'node:crypto';
2
21
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
22
  import { join } from 'node:path';
4
23
  const FORMULAS = {
5
24
  finalization: 'finalized_eligible_delivered_plans / eligible_delivered_plans',
6
25
  nps: '100 * (promoters_9_10 - detractors_0_6) / valid_responses',
7
- poi: 'audited_invalid_poi_claims / locked_audited_poi_claims'
26
+ poi: 'audited_invalid_poi_claims / locked_audited_poi_claims',
8
27
  };
9
28
  const EXCLUSION_CODES = [
10
29
  'outside_locked_window',
11
30
  'not_invited',
12
31
  'consent_missing_or_withdrawn',
13
32
  'test_or_staff',
14
- 'attribution_not_allowed'
33
+ 'attribution_not_allowed',
15
34
  ];
16
35
  const M3_ACCEPTANCE = {
17
36
  sampleMinimum: 50,
18
37
  sampleMaximum: 200,
19
38
  finalizationMinimum: 0.4,
20
39
  npsMinimum: 40,
21
- poiExclusiveMaximum: 0.01
40
+ poiExclusiveMaximum: 0.01,
22
41
  };
23
42
  const HMAC_KEY = /^hmac-sha256:[0-9a-f]{64}$/;
24
43
  const SHA256 = /^[0-9a-f]{64}$/;
@@ -30,155 +49,131 @@ function object(value, label) {
30
49
  }
31
50
  function exactKeys(value, label, keys) {
32
51
  const expected = new Set(keys);
33
- const unknown = Object.keys(value).filter((key)=>!expected.has(key));
34
- const missing = keys.filter((key)=>!(key in value));
35
- if (unknown.length > 0) throw new Error(`${label} contains undeclared fields: ${unknown.join(', ')}`);
36
- if (missing.length > 0) throw new Error(`${label} missing fields: ${missing.join(', ')}`);
52
+ const unknown = Object.keys(value).filter(key => !expected.has(key));
53
+ const missing = keys.filter(key => !(key in value));
54
+ if (unknown.length > 0)
55
+ throw new Error(`${label} contains undeclared fields: ${unknown.join(', ')}`);
56
+ if (missing.length > 0)
57
+ throw new Error(`${label} missing fields: ${missing.join(', ')}`);
37
58
  }
38
59
  function string(value, label) {
39
- if (typeof value !== 'string' || value.length === 0) throw new Error(`${label} must be a non-empty string`);
60
+ if (typeof value !== 'string' || value.length === 0)
61
+ throw new Error(`${label} must be a non-empty string`);
40
62
  return value;
41
63
  }
42
64
  function boolean(value, label) {
43
- if (typeof value !== 'boolean') throw new Error(`${label} must be boolean`);
65
+ if (typeof value !== 'boolean')
66
+ throw new Error(`${label} must be boolean`);
44
67
  return value;
45
68
  }
46
69
  function finiteNumber(value, label) {
47
- if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`${label} must be a finite number`);
70
+ if (typeof value !== 'number' || !Number.isFinite(value))
71
+ throw new Error(`${label} must be a finite number`);
48
72
  return value;
49
73
  }
50
74
  function integer(value, label) {
51
75
  const result = finiteNumber(value, label);
52
- if (!Number.isInteger(result)) throw new Error(`${label} must be an integer`);
76
+ if (!Number.isInteger(result))
77
+ throw new Error(`${label} must be an integer`);
53
78
  return result;
54
79
  }
55
80
  function timestamp(value, label) {
56
81
  const result = string(value, label);
57
- if (!Number.isFinite(Date.parse(result))) throw new Error(`${label} must be an ISO timestamp`);
82
+ if (!Number.isFinite(Date.parse(result)))
83
+ throw new Error(`${label} must be an ISO timestamp`);
58
84
  return result;
59
85
  }
60
86
  function nullableTimestamp(value, label) {
61
- if (value === null) return null;
87
+ if (value === null)
88
+ return null;
62
89
  return timestamp(value, label);
63
90
  }
64
91
  function nullableNps(value, label) {
65
- if (value === null) return null;
92
+ if (value === null)
93
+ return null;
66
94
  const result = integer(value, label);
67
- if (result < 0 || result > 10) throw new Error(`${label} must be an integer from 0 to 10`);
95
+ if (result < 0 || result > 10)
96
+ throw new Error(`${label} must be an integer from 0 to 10`);
68
97
  return result;
69
98
  }
70
99
  function hmacKey(value, label) {
71
100
  const result = string(value, label);
72
- if (!HMAC_KEY.test(result)) throw new Error(`${label} must be an hmac-sha256 pseudonymous key`);
101
+ if (!HMAC_KEY.test(result))
102
+ throw new Error(`${label} must be an hmac-sha256 pseudonymous key`);
73
103
  return result;
74
104
  }
75
105
  function sha256(value, label) {
76
106
  const result = string(value, label);
77
- if (!SHA256.test(result)) throw new Error(`${label} must be a lowercase SHA-256 digest`);
107
+ if (!SHA256.test(result))
108
+ throw new Error(`${label} must be a lowercase SHA-256 digest`);
78
109
  return result;
79
110
  }
80
111
  function literal(value, label, allowed) {
81
112
  const result = string(value, label);
82
- if (!allowed.includes(result)) throw new Error(`${label} must be one of ${allowed.join(', ')}`);
113
+ if (!allowed.includes(result))
114
+ throw new Error(`${label} must be one of ${allowed.join(', ')}`);
83
115
  return result;
84
116
  }
85
117
  function literalArray(value, label, allowed) {
86
- if (!Array.isArray(value) || value.length === 0) throw new Error(`${label} must be a non-empty array`);
87
- const result = value.map((item, index)=>literal(item, `${label}[${index}]`, allowed));
88
- if (new Set(result).size !== result.length) throw new Error(`${label} must not contain duplicates`);
118
+ if (!Array.isArray(value) || value.length === 0)
119
+ throw new Error(`${label} must be a non-empty array`);
120
+ const result = value.map((item, index) => literal(item, `${label}[${index}]`, allowed));
121
+ if (new Set(result).size !== result.length)
122
+ throw new Error(`${label} must not contain duplicates`);
89
123
  return result;
90
124
  }
91
125
  function exactStringArray(value, label, expected) {
92
- if (!Array.isArray(value) || value.some((item)=>typeof item !== 'string')) {
126
+ if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
93
127
  throw new Error(`${label} must be a string array`);
94
128
  }
95
- const actual = [
96
- ...value
97
- ].sort();
98
- const wanted = [
99
- ...expected
100
- ].sort();
129
+ const actual = [...value].sort();
130
+ const wanted = [...expected].sort();
101
131
  if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
102
132
  throw new Error(`${label} must freeze exactly: ${wanted.join(', ')}`);
103
133
  }
104
134
  return value;
105
135
  }
106
136
  function requiredTrue(value, label) {
107
- if (value !== true) throw new Error(`${label} must be true in schema v1`);
137
+ if (value !== true)
138
+ throw new Error(`${label} must be true in schema v1`);
108
139
  return true;
109
140
  }
110
141
  export function parseManifest(raw) {
111
142
  const root = object(raw, 'manifest');
112
- exactKeys(root, 'manifest', [
113
- 'schema_version',
114
- 'evidence_kind',
115
- 'cohort_id',
116
- 'locked_at',
117
- 'window',
118
- 'eligibility',
119
- 'metrics',
120
- 'nightly'
121
- ]);
143
+ exactKeys(root, 'manifest', ['schema_version', 'evidence_kind', 'cohort_id', 'locked_at', 'window', 'eligibility', 'metrics', 'nightly']);
122
144
  const window = object(root.window, 'manifest.window');
123
- exactKeys(window, 'manifest.window', [
124
- 'start_at',
125
- 'end_at',
126
- 'timezone'
127
- ]);
145
+ exactKeys(window, 'manifest.window', ['start_at', 'end_at', 'timezone']);
128
146
  const startAt = timestamp(window.start_at, 'manifest.window.start_at');
129
147
  const endAt = timestamp(window.end_at, 'manifest.window.end_at');
130
- if (Date.parse(startAt) > Date.parse(endAt)) throw new Error('manifest.window start_at must not be after end_at');
148
+ if (Date.parse(startAt) > Date.parse(endAt))
149
+ throw new Error('manifest.window start_at must not be after end_at');
131
150
  const eligibility = object(root.eligibility, 'manifest.eligibility');
132
- exactKeys(eligibility, 'manifest.eligibility', [
133
- 'sample_unit',
134
- 'requires_invitation',
135
- 'requires_consent',
136
- 'allowed_attribution',
137
- 'exclusion_codes'
138
- ]);
151
+ exactKeys(eligibility, 'manifest.eligibility', ['sample_unit', 'requires_invitation', 'requires_consent', 'allowed_attribution', 'exclusion_codes']);
139
152
  const metrics = object(root.metrics, 'manifest.metrics');
140
- exactKeys(metrics, 'manifest.metrics', [
141
- 'sample_size',
142
- 'finalization_rate',
143
- 'nps',
144
- 'poi_hallucination_rate'
145
- ]);
153
+ exactKeys(metrics, 'manifest.metrics', ['sample_size', 'finalization_rate', 'nps', 'poi_hallucination_rate']);
146
154
  const sampleSize = object(metrics.sample_size, 'manifest.metrics.sample_size');
147
- exactKeys(sampleSize, 'manifest.metrics.sample_size', [
148
- 'minimum',
149
- 'maximum'
150
- ]);
155
+ exactKeys(sampleSize, 'manifest.metrics.sample_size', ['minimum', 'maximum']);
151
156
  const finalization = object(metrics.finalization_rate, 'manifest.metrics.finalization_rate');
152
- exactKeys(finalization, 'manifest.metrics.finalization_rate', [
153
- 'formula',
154
- 'minimum'
155
- ]);
157
+ exactKeys(finalization, 'manifest.metrics.finalization_rate', ['formula', 'minimum']);
156
158
  const nps = object(metrics.nps, 'manifest.metrics.nps');
157
- exactKeys(nps, 'manifest.metrics.nps', [
158
- 'formula',
159
- 'minimum'
160
- ]);
159
+ exactKeys(nps, 'manifest.metrics.nps', ['formula', 'minimum']);
161
160
  const poi = object(metrics.poi_hallucination_rate, 'manifest.metrics.poi_hallucination_rate');
162
- exactKeys(poi, 'manifest.metrics.poi_hallucination_rate', [
163
- 'formula',
164
- 'exclusive_maximum'
165
- ]);
161
+ exactKeys(poi, 'manifest.metrics.poi_hallucination_rate', ['formula', 'exclusive_maximum']);
166
162
  const nightly = object(root.nightly, 'manifest.nightly');
167
- exactKeys(nightly, 'manifest.nightly', [
168
- 'requires_real_llm',
169
- 'requires_prompt_set_sha256',
170
- 'requires_output_sha256',
171
- 'requires_cost_usd'
172
- ]);
163
+ exactKeys(nightly, 'manifest.nightly', ['requires_real_llm', 'requires_prompt_set_sha256', 'requires_output_sha256', 'requires_cost_usd']);
173
164
  const minimum = integer(sampleSize.minimum, 'manifest.metrics.sample_size.minimum');
174
165
  const maximum = integer(sampleSize.maximum, 'manifest.metrics.sample_size.maximum');
175
- if (minimum < 1 || maximum < minimum) throw new Error('manifest sample size bounds are invalid');
166
+ if (minimum < 1 || maximum < minimum)
167
+ throw new Error('manifest sample size bounds are invalid');
176
168
  const finalizationMinimum = finiteNumber(finalization.minimum, 'manifest.metrics.finalization_rate.minimum');
177
169
  const npsMinimum = finiteNumber(nps.minimum, 'manifest.metrics.nps.minimum');
178
170
  const poiMaximum = finiteNumber(poi.exclusive_maximum, 'manifest.metrics.poi_hallucination_rate.exclusive_maximum');
179
- if (finalizationMinimum < 0 || finalizationMinimum > 1) throw new Error('finalization minimum must be within 0..1');
180
- if (npsMinimum < -100 || npsMinimum > 100) throw new Error('NPS minimum must be within -100..100');
181
- if (poiMaximum <= 0 || poiMaximum > 1) throw new Error('POI exclusive maximum must be within 0..1');
171
+ if (finalizationMinimum < 0 || finalizationMinimum > 1)
172
+ throw new Error('finalization minimum must be within 0..1');
173
+ if (npsMinimum < -100 || npsMinimum > 100)
174
+ throw new Error('NPS minimum must be within -100..100');
175
+ if (poiMaximum <= 0 || poiMaximum > 1)
176
+ throw new Error('POI exclusive maximum must be within 0..1');
182
177
  if (minimum !== M3_ACCEPTANCE.sampleMinimum || maximum !== M3_ACCEPTANCE.sampleMaximum) {
183
178
  throw new Error(`M3 sample size thresholds are frozen at ${M3_ACCEPTANCE.sampleMinimum}..${M3_ACCEPTANCE.sampleMaximum}`);
184
179
  }
@@ -192,87 +187,47 @@ export function parseManifest(raw) {
192
187
  throw new Error(`M3 POI hallucination threshold is frozen at ${M3_ACCEPTANCE.poiExclusiveMaximum}`);
193
188
  }
194
189
  return {
195
- schema_version: literal(root.schema_version, 'manifest.schema_version', [
196
- 'gotry_m3_evidence_manifest_v1'
197
- ]),
198
- evidence_kind: literal(root.evidence_kind, 'manifest.evidence_kind', [
199
- 'real_seed_cohort',
200
- 'synthetic_fixture'
201
- ]),
190
+ schema_version: literal(root.schema_version, 'manifest.schema_version', ['gotry_m3_evidence_manifest_v1']),
191
+ evidence_kind: literal(root.evidence_kind, 'manifest.evidence_kind', ['real_seed_cohort', 'synthetic_fixture']),
202
192
  cohort_id: string(root.cohort_id, 'manifest.cohort_id'),
203
193
  locked_at: timestamp(root.locked_at, 'manifest.locked_at'),
204
- window: {
205
- start_at: startAt,
206
- end_at: endAt,
207
- timezone: string(window.timezone, 'manifest.window.timezone')
208
- },
194
+ window: { start_at: startAt, end_at: endAt, timezone: string(window.timezone, 'manifest.window.timezone') },
209
195
  eligibility: {
210
- sample_unit: literal(eligibility.sample_unit, 'manifest.eligibility.sample_unit', [
211
- 'unique_participant_with_eligible_delivered_plan'
212
- ]),
196
+ sample_unit: literal(eligibility.sample_unit, 'manifest.eligibility.sample_unit', ['unique_participant_with_eligible_delivered_plan']),
213
197
  requires_invitation: requiredTrue(eligibility.requires_invitation, 'manifest.eligibility.requires_invitation'),
214
198
  requires_consent: requiredTrue(eligibility.requires_consent, 'manifest.eligibility.requires_consent'),
215
- allowed_attribution: literalArray(eligibility.allowed_attribution, 'manifest.eligibility.allowed_attribution', [
216
- 'gotry_primary',
217
- 'gotry_assisted'
218
- ]),
219
- exclusion_codes: exactStringArray(eligibility.exclusion_codes, 'manifest.eligibility.exclusion_codes', EXCLUSION_CODES)
199
+ allowed_attribution: literalArray(eligibility.allowed_attribution, 'manifest.eligibility.allowed_attribution', ['gotry_primary', 'gotry_assisted']),
200
+ exclusion_codes: exactStringArray(eligibility.exclusion_codes, 'manifest.eligibility.exclusion_codes', EXCLUSION_CODES),
220
201
  },
221
202
  metrics: {
222
- sample_size: {
223
- minimum,
224
- maximum
225
- },
203
+ sample_size: { minimum, maximum },
226
204
  finalization_rate: {
227
- formula: literal(finalization.formula, 'manifest.metrics.finalization_rate.formula', [
228
- FORMULAS.finalization
229
- ]),
230
- minimum: finalizationMinimum
205
+ formula: literal(finalization.formula, 'manifest.metrics.finalization_rate.formula', [FORMULAS.finalization]),
206
+ minimum: finalizationMinimum,
231
207
  },
232
208
  nps: {
233
- formula: literal(nps.formula, 'manifest.metrics.nps.formula', [
234
- FORMULAS.nps
235
- ]),
236
- minimum: npsMinimum
209
+ formula: literal(nps.formula, 'manifest.metrics.nps.formula', [FORMULAS.nps]),
210
+ minimum: npsMinimum,
237
211
  },
238
212
  poi_hallucination_rate: {
239
- formula: literal(poi.formula, 'manifest.metrics.poi_hallucination_rate.formula', [
240
- FORMULAS.poi
241
- ]),
242
- exclusive_maximum: poiMaximum
243
- }
213
+ formula: literal(poi.formula, 'manifest.metrics.poi_hallucination_rate.formula', [FORMULAS.poi]),
214
+ exclusive_maximum: poiMaximum,
215
+ },
244
216
  },
245
217
  nightly: {
246
218
  requires_real_llm: requiredTrue(nightly.requires_real_llm, 'manifest.nightly.requires_real_llm'),
247
219
  requires_prompt_set_sha256: requiredTrue(nightly.requires_prompt_set_sha256, 'manifest.nightly.requires_prompt_set_sha256'),
248
220
  requires_output_sha256: requiredTrue(nightly.requires_output_sha256, 'manifest.nightly.requires_output_sha256'),
249
- requires_cost_usd: requiredTrue(nightly.requires_cost_usd, 'manifest.nightly.requires_cost_usd')
250
- }
221
+ requires_cost_usd: requiredTrue(nightly.requires_cost_usd, 'manifest.nightly.requires_cost_usd'),
222
+ },
251
223
  };
252
224
  }
253
225
  export function parseCohortRecord(raw, index) {
254
226
  const label = `cohort[${index}]`;
255
227
  const root = object(raw, label);
256
- exactKeys(root, label, [
257
- 'schema_version',
258
- 'participant_key',
259
- 'plan_key',
260
- 'invited',
261
- 'consent',
262
- 'test_or_staff',
263
- 'attribution',
264
- 'delivered_at',
265
- 'finalized_at',
266
- 'nps_score',
267
- 'nps_recorded_at',
268
- 'poi_audit'
269
- ]);
228
+ exactKeys(root, label, ['schema_version', 'participant_key', 'plan_key', 'invited', 'consent', 'test_or_staff', 'attribution', 'delivered_at', 'finalized_at', 'nps_score', 'nps_recorded_at', 'poi_audit']);
270
229
  const poi = object(root.poi_audit, `${label}.poi_audit`);
271
- exactKeys(poi, `${label}.poi_audit`, [
272
- 'locked_at',
273
- 'locked_claims',
274
- 'invalid_claims'
275
- ]);
230
+ exactKeys(poi, `${label}.poi_audit`, ['locked_at', 'locked_claims', 'invalid_claims']);
276
231
  const lockedClaims = integer(poi.locked_claims, `${label}.poi_audit.locked_claims`);
277
232
  const invalidClaims = integer(poi.invalid_claims, `${label}.poi_audit.invalid_claims`);
278
233
  if (lockedClaims < 0 || invalidClaims < 0 || invalidClaims > lockedClaims) {
@@ -286,7 +241,7 @@ export function parseCohortRecord(raw, index) {
286
241
  if (finalizedAt !== null && Date.parse(finalizedAt) < Date.parse(deliveredAt)) {
287
242
  throw new Error(`${label}.finalized_at must not be before delivered_at`);
288
243
  }
289
- if (npsScore === null !== (npsRecordedAt === null)) {
244
+ if ((npsScore === null) !== (npsRecordedAt === null)) {
290
245
  throw new Error(`${label}.nps_score and nps_recorded_at must both be null or both be set`);
291
246
  }
292
247
  if (npsRecordedAt !== null && Date.parse(npsRecordedAt) < Date.parse(deliveredAt)) {
@@ -296,125 +251,112 @@ export function parseCohortRecord(raw, index) {
296
251
  throw new Error(`${label}.poi_audit.locked_at must not be before delivered_at`);
297
252
  }
298
253
  return {
299
- schema_version: literal(root.schema_version, `${label}.schema_version`, [
300
- 'gotry_m3_cohort_record_v1'
301
- ]),
254
+ schema_version: literal(root.schema_version, `${label}.schema_version`, ['gotry_m3_cohort_record_v1']),
302
255
  participant_key: hmacKey(root.participant_key, `${label}.participant_key`),
303
256
  plan_key: hmacKey(root.plan_key, `${label}.plan_key`),
304
257
  invited: boolean(root.invited, `${label}.invited`),
305
258
  consent: boolean(root.consent, `${label}.consent`),
306
259
  test_or_staff: boolean(root.test_or_staff, `${label}.test_or_staff`),
307
- attribution: literal(root.attribution, `${label}.attribution`, [
308
- 'gotry_primary',
309
- 'gotry_assisted'
310
- ]),
260
+ attribution: literal(root.attribution, `${label}.attribution`, ['gotry_primary', 'gotry_assisted']),
311
261
  delivered_at: deliveredAt,
312
262
  finalized_at: finalizedAt,
313
263
  nps_score: npsScore,
314
264
  nps_recorded_at: npsRecordedAt,
315
- poi_audit: {
316
- locked_at: poiLockedAt,
317
- locked_claims: lockedClaims,
318
- invalid_claims: invalidClaims
319
- }
265
+ poi_audit: { locked_at: poiLockedAt, locked_claims: lockedClaims, invalid_claims: invalidClaims },
320
266
  };
321
267
  }
322
268
  export function parseNightlyRun(raw, index) {
323
269
  const label = `nightly_runs[${index}]`;
324
270
  const root = object(raw, label);
325
- exactKeys(root, label, [
326
- 'schema_version',
327
- 'run_key',
328
- 'executed_at',
329
- 'real_llm',
330
- 'prompt_set_sha256',
331
- 'output_sha256',
332
- 'cost_usd'
333
- ]);
271
+ exactKeys(root, label, ['schema_version', 'run_key', 'executed_at', 'real_llm', 'prompt_set_sha256', 'output_sha256', 'cost_usd']);
334
272
  const costUsd = finiteNumber(root.cost_usd, `${label}.cost_usd`);
335
- if (costUsd < 0) throw new Error(`${label}.cost_usd must be non-negative`);
273
+ if (costUsd < 0)
274
+ throw new Error(`${label}.cost_usd must be non-negative`);
336
275
  return {
337
- schema_version: literal(root.schema_version, `${label}.schema_version`, [
338
- 'gotry_m3_nightly_run_v1'
339
- ]),
276
+ schema_version: literal(root.schema_version, `${label}.schema_version`, ['gotry_m3_nightly_run_v1']),
340
277
  run_key: hmacKey(root.run_key, `${label}.run_key`),
341
278
  executed_at: timestamp(root.executed_at, `${label}.executed_at`),
342
279
  real_llm: boolean(root.real_llm, `${label}.real_llm`),
343
280
  prompt_set_sha256: sha256(root.prompt_set_sha256, `${label}.prompt_set_sha256`),
344
281
  output_sha256: sha256(root.output_sha256, `${label}.output_sha256`),
345
- cost_usd: costUsd
282
+ cost_usd: costUsd,
346
283
  };
347
284
  }
348
285
  function round(value) {
349
286
  return Number(value.toFixed(6));
350
287
  }
351
288
  function canonical(value) {
352
- if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
289
+ if (Array.isArray(value))
290
+ return `[${value.map(canonical).join(',')}]`;
353
291
  if (typeof value === 'object' && value !== null) {
354
292
  const record = value;
355
- return `{${Object.keys(record).sort().map((key)=>`${JSON.stringify(key)}:${canonical(record[key])}`).join(',')}}`;
293
+ return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${canonical(record[key])}`).join(',')}}`;
356
294
  }
357
295
  return JSON.stringify(value);
358
296
  }
359
297
  function exclusionsFor(manifest, record) {
360
298
  const result = [];
361
299
  const delivered = Date.parse(record.delivered_at);
362
- if (delivered < Date.parse(manifest.window.start_at) || delivered > Date.parse(manifest.window.end_at)) result.push('outside_locked_window');
363
- if (!record.invited) result.push('not_invited');
364
- if (!record.consent) result.push('consent_missing_or_withdrawn');
365
- if (record.test_or_staff) result.push('test_or_staff');
366
- if (!manifest.eligibility.allowed_attribution.includes(record.attribution)) result.push('attribution_not_allowed');
300
+ if (delivered < Date.parse(manifest.window.start_at) || delivered > Date.parse(manifest.window.end_at))
301
+ result.push('outside_locked_window');
302
+ if (!record.invited)
303
+ result.push('not_invited');
304
+ if (!record.consent)
305
+ result.push('consent_missing_or_withdrawn');
306
+ if (record.test_or_staff)
307
+ result.push('test_or_staff');
308
+ if (!manifest.eligibility.allowed_attribution.includes(record.attribution))
309
+ result.push('attribution_not_allowed');
367
310
  return result;
368
311
  }
369
312
  export function scoreProductMetrics(manifest, cohort, nightlyRuns) {
370
313
  const planKeys = new Set();
371
314
  const runKeys = new Set();
372
- for (const record of cohort){
373
- if (planKeys.has(record.plan_key)) throw new Error(`duplicate plan_key: ${record.plan_key}`);
315
+ for (const record of cohort) {
316
+ if (planKeys.has(record.plan_key))
317
+ throw new Error(`duplicate plan_key: ${record.plan_key}`);
374
318
  planKeys.add(record.plan_key);
375
319
  }
376
- for (const run of nightlyRuns){
377
- if (runKeys.has(run.run_key)) throw new Error(`duplicate run_key: ${run.run_key}`);
320
+ for (const run of nightlyRuns) {
321
+ if (runKeys.has(run.run_key))
322
+ throw new Error(`duplicate run_key: ${run.run_key}`);
378
323
  runKeys.add(run.run_key);
379
324
  }
380
- const exclusionCounts = Object.fromEntries(EXCLUSION_CODES.map((code)=>[
381
- code,
382
- 0
383
- ]));
384
- const eligible = cohort.filter((record)=>{
325
+ const exclusionCounts = Object.fromEntries(EXCLUSION_CODES.map(code => [code, 0]));
326
+ const eligible = cohort.filter(record => {
385
327
  const exclusions = exclusionsFor(manifest, record);
386
- for (const code of exclusions)exclusionCounts[code] += 1;
328
+ for (const code of exclusions)
329
+ exclusionCounts[code] += 1;
387
330
  return exclusions.length === 0;
388
331
  });
389
- const participants = new Set(eligible.map((record)=>record.participant_key));
332
+ const participants = new Set(eligible.map(record => record.participant_key));
390
333
  const windowEnd = Date.parse(manifest.window.end_at);
391
- const finalized = eligible.filter((record)=>record.finalized_at !== null && Date.parse(record.finalized_at) <= windowEnd).length;
334
+ const finalized = eligible.filter(record => record.finalized_at !== null && Date.parse(record.finalized_at) <= windowEnd).length;
392
335
  const finalizationRate = eligible.length === 0 ? null : round(finalized / eligible.length);
393
336
  const npsByParticipant = new Map();
394
- for (const record of eligible){
395
- if (record.nps_score === null || record.nps_recorded_at === null || Date.parse(record.nps_recorded_at) > windowEnd) continue;
337
+ for (const record of eligible) {
338
+ if (record.nps_score === null || record.nps_recorded_at === null || Date.parse(record.nps_recorded_at) > windowEnd)
339
+ continue;
396
340
  if (npsByParticipant.has(record.participant_key)) {
397
341
  throw new Error(`participant has more than one NPS response in the locked window: ${record.participant_key}`);
398
342
  }
399
343
  npsByParticipant.set(record.participant_key, record.nps_score);
400
344
  }
401
- const scores = [
402
- ...npsByParticipant.values()
403
- ];
404
- const promoters = scores.filter((score)=>score >= 9).length;
405
- const detractors = scores.filter((score)=>score <= 6).length;
345
+ const scores = [...npsByParticipant.values()];
346
+ const promoters = scores.filter(score => score >= 9).length;
347
+ const detractors = scores.filter(score => score <= 6).length;
406
348
  const passives = scores.length - promoters - detractors;
407
349
  const npsScore = scores.length === 0 ? null : round(100 * (promoters - detractors) / scores.length);
408
- const lockedAudits = eligible.filter((record)=>Date.parse(record.poi_audit.locked_at) <= windowEnd);
409
- const lockedClaims = lockedAudits.reduce((sum, record)=>sum + record.poi_audit.locked_claims, 0);
410
- const invalidClaims = lockedAudits.reduce((sum, record)=>sum + record.poi_audit.invalid_claims, 0);
350
+ const lockedAudits = eligible.filter(record => Date.parse(record.poi_audit.locked_at) <= windowEnd);
351
+ const lockedClaims = lockedAudits.reduce((sum, record) => sum + record.poi_audit.locked_claims, 0);
352
+ const invalidClaims = lockedAudits.reduce((sum, record) => sum + record.poi_audit.invalid_claims, 0);
411
353
  const poiRate = lockedClaims === 0 ? null : round(invalidClaims / lockedClaims);
412
354
  const windowStart = Date.parse(manifest.window.start_at);
413
- const replayableRuns = nightlyRuns.filter((run)=>{
355
+ const replayableRuns = nightlyRuns.filter(run => {
414
356
  const executedAt = Date.parse(run.executed_at);
415
357
  return run.real_llm && executedAt >= windowStart && executedAt <= windowEnd;
416
358
  });
417
- const nightlyCost = round(replayableRuns.reduce((sum, run)=>sum + run.cost_usd, 0));
359
+ const nightlyCost = round(replayableRuns.reduce((sum, run) => sum + run.cost_usd, 0));
418
360
  const samplePass = participants.size >= manifest.metrics.sample_size.minimum && participants.size <= manifest.metrics.sample_size.maximum;
419
361
  const finalizationPass = finalizationRate !== null && finalizationRate >= manifest.metrics.finalization_rate.minimum;
420
362
  const npsPass = npsScore !== null && npsScore >= manifest.metrics.nps.minimum;
@@ -427,83 +369,57 @@ export function scoreProductMetrics(manifest, cohort, nightlyRuns) {
427
369
  !finalizationPass && 'finalization_rate',
428
370
  !npsPass && 'nps',
429
371
  !poiPass && 'poi_hallucination_rate',
430
- !nightlyPass && 'nightly_real_llm'
372
+ !nightlyPass && 'nightly_real_llm',
431
373
  ].filter(Boolean);
432
- const businessPassReason = manifest.evidence_kind === 'synthetic_fixture' ? 'evidence_kind=synthetic_fixture cannot prove business pass' : businessPass ? 'all locked M3 acceptance thresholds passed on real_seed_cohort evidence' : `real_seed_cohort acceptance gaps: ${failedChecks.join(', ')}`;
374
+ const businessPassReason = manifest.evidence_kind === 'synthetic_fixture'
375
+ ? 'evidence_kind=synthetic_fixture cannot prove business pass'
376
+ : businessPass
377
+ ? 'all locked M3 acceptance thresholds passed on real_seed_cohort evidence'
378
+ : `real_seed_cohort acceptance gaps: ${failedChecks.join(', ')}`;
433
379
  return {
434
380
  schema_version: 'gotry_m3_product_metrics_summary_v1',
435
381
  evidence_kind: manifest.evidence_kind,
436
382
  cohort_id: manifest.cohort_id,
437
- evidence_digest_sha256: createHash('sha256').update(canonical({
438
- manifest,
439
- cohort,
440
- nightlyRuns
441
- })).digest('hex'),
383
+ evidence_digest_sha256: createHash('sha256').update(canonical({ manifest, cohort, nightlyRuns })).digest('hex'),
442
384
  sample: {
443
385
  participants: participants.size,
444
386
  minimum: manifest.metrics.sample_size.minimum,
445
387
  maximum: manifest.metrics.sample_size.maximum,
446
- pass: samplePass
447
- },
448
- finalization: {
449
- numerator: finalized,
450
- denominator: eligible.length,
451
- rate: finalizationRate,
452
- pass: finalizationPass
453
- },
454
- nps: {
455
- promoters,
456
- passives,
457
- detractors,
458
- denominator: scores.length,
459
- score: npsScore,
460
- pass: npsPass
461
- },
462
- poi_hallucination: {
463
- invalid_claims: invalidClaims,
464
- locked_claims: lockedClaims,
465
- rate: poiRate,
466
- pass: poiPass
467
- },
468
- nightly: {
469
- replayable_real_llm_runs: replayableRuns.length,
470
- cost_usd: nightlyCost,
471
- pass: nightlyPass
388
+ pass: samplePass,
472
389
  },
390
+ finalization: { numerator: finalized, denominator: eligible.length, rate: finalizationRate, pass: finalizationPass },
391
+ nps: { promoters, passives, detractors, denominator: scores.length, score: npsScore, pass: npsPass },
392
+ poi_hallucination: { invalid_claims: invalidClaims, locked_claims: lockedClaims, rate: poiRate, pass: poiPass },
393
+ nightly: { replayable_real_llm_runs: replayableRuns.length, cost_usd: nightlyCost, pass: nightlyPass },
473
394
  exclusions: exclusionCounts,
474
395
  business_pass: businessPass,
475
- business_pass_reason: businessPassReason
396
+ business_pass_reason: businessPassReason,
476
397
  };
477
398
  }
478
399
  function parseJsonLines(contents) {
479
- return contents.split(/\r?\n/).flatMap((line, index)=>{
480
- if (line.trim() === '') return [];
400
+ return contents.split(/\r?\n/).flatMap((line, index) => {
401
+ if (line.trim() === '')
402
+ return [];
481
403
  try {
482
- return [
483
- JSON.parse(line)
484
- ];
485
- } catch (error) {
404
+ return [JSON.parse(line)];
405
+ }
406
+ catch (error) {
486
407
  throw new Error(`cohort.jsonl line ${index + 1} is invalid JSON: ${error.message}`);
487
408
  }
488
409
  });
489
410
  }
490
411
  function loadFixture(path) {
491
412
  const raw = object(JSON.parse(readFileSync(path, 'utf8')), 'fixture');
492
- exactKeys(raw, 'fixture', [
493
- 'schema_version',
494
- 'manifest',
495
- 'cohort',
496
- 'nightly_runs'
497
- ]);
498
- literal(raw.schema_version, 'fixture.schema_version', [
499
- 'gotry_m3_product_metrics_fixture_v1'
500
- ]);
501
- if (!Array.isArray(raw.cohort)) throw new Error('fixture.cohort must be an array');
502
- if (!Array.isArray(raw.nightly_runs)) throw new Error('fixture.nightly_runs must be an array');
413
+ exactKeys(raw, 'fixture', ['schema_version', 'manifest', 'cohort', 'nightly_runs']);
414
+ literal(raw.schema_version, 'fixture.schema_version', ['gotry_m3_product_metrics_fixture_v1']);
415
+ if (!Array.isArray(raw.cohort))
416
+ throw new Error('fixture.cohort must be an array');
417
+ if (!Array.isArray(raw.nightly_runs))
418
+ throw new Error('fixture.nightly_runs must be an array');
503
419
  return {
504
420
  manifest: parseManifest(raw.manifest),
505
421
  cohort: raw.cohort.map(parseCohortRecord),
506
- nightlyRuns: raw.nightly_runs.map(parseNightlyRun)
422
+ nightlyRuns: raw.nightly_runs.map(parseNightlyRun),
507
423
  };
508
424
  }
509
425
  function loadEvidenceRoot(root) {
@@ -511,24 +427,23 @@ function loadEvidenceRoot(root) {
511
427
  const records = parseJsonLines(readFileSync(join(root, 'cohort.jsonl'), 'utf8'));
512
428
  const cohort = [];
513
429
  const nightlyRuns = [];
514
- for (const record of records){
430
+ for (const record of records) {
515
431
  const schema = object(record, 'cohort.jsonl record').schema_version;
516
- if (schema === 'gotry_m3_cohort_record_v1') cohort.push(parseCohortRecord(record, cohort.length));
517
- else if (schema === 'gotry_m3_nightly_run_v1') nightlyRuns.push(parseNightlyRun(record, nightlyRuns.length));
518
- else throw new Error(`cohort.jsonl contains unknown schema_version: ${String(schema)}`);
432
+ if (schema === 'gotry_m3_cohort_record_v1')
433
+ cohort.push(parseCohortRecord(record, cohort.length));
434
+ else if (schema === 'gotry_m3_nightly_run_v1')
435
+ nightlyRuns.push(parseNightlyRun(record, nightlyRuns.length));
436
+ else
437
+ throw new Error(`cohort.jsonl contains unknown schema_version: ${String(schema)}`);
519
438
  }
520
- return {
521
- manifest,
522
- cohort,
523
- nightlyRuns
524
- };
439
+ return { manifest, cohort, nightlyRuns };
525
440
  }
526
441
  function arg(name) {
527
442
  const index = process.argv.indexOf(name);
528
443
  return index >= 0 ? process.argv[index + 1] : undefined;
529
444
  }
530
445
  function renderMarkdown(summary) {
531
- const percent = (value)=>value === null ? 'unavailable' : `${round(value * 100)}%`;
446
+ const percent = (value) => value === null ? 'unavailable' : `${round(value * 100)}%`;
532
447
  return [
533
448
  `# M3 cohort 指标摘要(${summary.evidence_kind})`,
534
449
  '',
@@ -538,7 +453,7 @@ function renderMarkdown(summary) {
538
453
  `- POI 幻觉率: ${summary.poi_hallucination.invalid_claims}/${summary.poi_hallucination.locked_claims} = ${percent(summary.poi_hallucination.rate)}(pass=${summary.poi_hallucination.pass})`,
539
454
  `- Nightly real-LLM: ${summary.nightly.replayable_real_llm_runs} runs, $${summary.nightly.cost_usd}(pass=${summary.nightly.pass})`,
540
455
  `- Business pass: ${summary.business_pass} — ${summary.business_pass_reason}`,
541
- `- Evidence digest: ${summary.evidence_digest_sha256}`
456
+ `- Evidence digest: ${summary.evidence_digest_sha256}`,
542
457
  ].join('\n');
543
458
  }
544
459
  function main() {
@@ -547,23 +462,22 @@ function main() {
547
462
  const loaded = fixturePath ? loadFixture(fixturePath) : loadEvidenceRoot(evidenceRoot);
548
463
  const summary = scoreProductMetrics(loaded.manifest, loaded.cohort, loaded.nightlyRuns);
549
464
  if (process.argv.includes('--write-summary')) {
550
- if (fixturePath) throw new Error('--write-summary is forbidden for synthetic fixtures');
551
- mkdirSync(evidenceRoot, {
552
- recursive: true
553
- });
465
+ if (fixturePath)
466
+ throw new Error('--write-summary is forbidden for synthetic fixtures');
467
+ mkdirSync(evidenceRoot, { recursive: true });
554
468
  writeFileSync(join(evidenceRoot, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
555
469
  }
556
- if ((arg('--format') ?? 'markdown') === 'json') console.log(JSON.stringify(summary));
557
- else console.log(renderMarkdown(summary));
470
+ if ((arg('--format') ?? 'markdown') === 'json')
471
+ console.log(JSON.stringify(summary));
472
+ else
473
+ console.log(renderMarkdown(summary));
558
474
  }
559
475
  if (process.argv[1]?.endsWith('product-metrics.ts')) {
560
476
  try {
561
477
  main();
562
- } catch (error) {
478
+ }
479
+ catch (error) {
563
480
  console.error(`M3 product metrics error: ${error.message}`);
564
481
  process.exitCode = 1;
565
482
  }
566
483
  }
567
-
568
-
569
- //# sourceURL=ts/scripts/product-metrics.ts