@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,108 +1,377 @@
1
+ /**
2
+ * M4 memory value evidence scorer (GitHub Issue #20/#223).
3
+ *
4
+ * The scorer is deliberately read-only. It accepts a public fixture or a
5
+ * private observed cohort manifest and emits one deterministic JSON report.
6
+ * Synthetic fixtures can prove the contract and calculations, but can never
7
+ * satisfy M4 Exit. Observed-private inputs only become exit-eligible when a
8
+ * human source-review attestation contract is present; the scorer validates
9
+ * that contract shape but does not inspect private raw material.
10
+ *
11
+ * Usage:
12
+ * npx tsx scripts/memory-value-report.ts data/memory-value-fixture.json
13
+ */
14
+ import { createHash } from 'node:crypto';
1
15
  import { readFileSync } from 'node:fs';
2
16
  import { resolve } from 'node:path';
3
17
  import { pathToFileURL } from 'node:url';
18
+ const M4_ACCEPTANCE = {
19
+ minimumPairCountForExit: 5,
20
+ targetMedianReductionRatio: 0.5,
21
+ };
22
+ const HMAC_REF = /^hmac-sha256:[0-9a-f]{64}$/;
23
+ const SHA256 = /^[0-9a-f]{64}$/;
24
+ const ISO_UTC_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
4
25
  function isObject(value) {
5
26
  return typeof value === 'object' && value !== null && !Array.isArray(value);
6
27
  }
7
28
  function nonEmptyString(value) {
8
29
  return typeof value === 'string' && value.trim().length > 0;
9
30
  }
10
- function finiteNumber(value) {
11
- return typeof value === 'number' && Number.isFinite(value);
12
- }
13
31
  function round(value) {
14
32
  return Math.round((value + Number.EPSILON) * 1_000_000) / 1_000_000;
15
33
  }
16
- function nearestRank(values, percentile) {
17
- if (values.length === 0) return 0;
18
- const sorted = [
19
- ...values
20
- ].sort((a, b)=>a - b);
34
+ function nearestRankRaw(values, percentile) {
35
+ if (values.length === 0)
36
+ return 0;
37
+ const sorted = [...values].sort((a, b) => a - b);
21
38
  const index = Math.max(0, Math.ceil(percentile * sorted.length) - 1);
22
- return round(sorted[index]);
39
+ return sorted[index];
40
+ }
41
+ function nearestRank(values, percentile) {
42
+ return round(nearestRankRaw(values, percentile));
43
+ }
44
+ function canonical(value) {
45
+ if (Array.isArray(value))
46
+ return `[${value.map(canonical).join(',')}]`;
47
+ if (isObject(value)) {
48
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
49
+ }
50
+ return JSON.stringify(value);
51
+ }
52
+ export function memoryValueSummaryDigest(input) {
53
+ const payload = isObject(input)
54
+ ? Object.fromEntries(Object.entries(input).filter(([key]) => key !== 'source_review'))
55
+ : input;
56
+ return createHash('sha256').update(canonical(payload)).digest('hex');
57
+ }
58
+ function exactKeys(value, label, keys, errors) {
59
+ const expected = new Set(keys);
60
+ if (Object.keys(value).some(key => !expected.has(key))) {
61
+ errors.push(`${label} contains undeclared field(s)`);
62
+ }
63
+ if (keys.some(key => !(key in value))) {
64
+ errors.push(`${label} is missing required field(s)`);
65
+ }
66
+ }
67
+ function object(value, label, errors) {
68
+ if (!isObject(value)) {
69
+ errors.push(`${label} must be an object`);
70
+ return null;
71
+ }
72
+ return value;
73
+ }
74
+ function array(value, label, errors, options = {}) {
75
+ if (!Array.isArray(value)) {
76
+ errors.push(`${label} must be an array`);
77
+ return [];
78
+ }
79
+ if (options.nonEmpty && value.length === 0)
80
+ errors.push(`${label} must be a non-empty array`);
81
+ return value;
23
82
  }
24
- function parseTimestamp(value, path, errors) {
83
+ function string(value, label, errors) {
25
84
  if (!nonEmptyString(value)) {
26
- errors.push(`${path} must be a non-empty ISO timestamp`);
85
+ errors.push(`${label} must be a non-empty string`);
86
+ return null;
87
+ }
88
+ return value;
89
+ }
90
+ function nullableString(value, label, errors) {
91
+ if (value === null)
92
+ return null;
93
+ return string(value, label, errors);
94
+ }
95
+ function boolean(value, label, errors) {
96
+ if (typeof value !== 'boolean') {
97
+ errors.push(`${label} must be boolean`);
98
+ return null;
99
+ }
100
+ return value;
101
+ }
102
+ function finiteNumber(value, label, errors) {
103
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
104
+ errors.push(`${label} must be a finite number`);
27
105
  return null;
28
106
  }
29
- const parsed = Date.parse(value);
107
+ return value;
108
+ }
109
+ function integer(value, label, errors) {
110
+ const result = finiteNumber(value, label, errors);
111
+ if (result === null)
112
+ return null;
113
+ if (!Number.isInteger(result)) {
114
+ errors.push(`${label} must be an integer`);
115
+ return null;
116
+ }
117
+ return result;
118
+ }
119
+ function literal(value, label, allowed, errors) {
120
+ const result = string(value, label, errors);
121
+ if (result === null)
122
+ return null;
123
+ if (!allowed.includes(result)) {
124
+ errors.push(`${label} must be one of the declared enum values`);
125
+ return null;
126
+ }
127
+ return result;
128
+ }
129
+ function hmacRef(value, label, errors) {
130
+ const result = string(value, label, errors);
131
+ if (result === null)
132
+ return null;
133
+ if (!HMAC_REF.test(result)) {
134
+ errors.push(`${label} must be an hmac-sha256 pseudonymous reference`);
135
+ return null;
136
+ }
137
+ return result;
138
+ }
139
+ function nullableHmacRef(value, label, errors) {
140
+ if (value === null)
141
+ return null;
142
+ return hmacRef(value, label, errors);
143
+ }
144
+ function nullableSha256(value, label, errors) {
145
+ if (value === null)
146
+ return null;
147
+ const result = string(value, label, errors);
148
+ if (result === null)
149
+ return null;
150
+ if (!SHA256.test(result)) {
151
+ errors.push(`${label} must be a lowercase SHA-256 digest`);
152
+ return null;
153
+ }
154
+ return result;
155
+ }
156
+ function parseTimestamp(value, label, errors) {
157
+ const result = string(value, label, errors);
158
+ if (result === null)
159
+ return null;
160
+ if (!ISO_UTC_TIMESTAMP.test(result)) {
161
+ errors.push(`${label} must be a strict UTC ISO timestamp`);
162
+ return null;
163
+ }
164
+ const parsed = Date.parse(result);
30
165
  if (!Number.isFinite(parsed)) {
31
- errors.push(`${path} is not a valid ISO timestamp`);
166
+ errors.push(`${label} must be a valid UTC ISO timestamp`);
32
167
  return null;
33
168
  }
34
- return parsed;
169
+ const canonical = new Date(parsed).toISOString();
170
+ const normalized = result.includes('.') ? result : result.replace('Z', '.000Z');
171
+ if (canonical !== normalized) {
172
+ errors.push(`${label} must be a valid UTC ISO timestamp`);
173
+ return null;
174
+ }
175
+ return { text: result, ms: parsed };
35
176
  }
36
- function scoreFlow(value, path, expectedEligibleIndex, allowedWaitCodes, errors) {
37
- if (!isObject(value)) {
38
- errors.push(`${path} must be an object`);
177
+ function nullableTimestamp(value, label, errors) {
178
+ if (value === null)
179
+ return null;
180
+ return parseTimestamp(value, label, errors);
181
+ }
182
+ function assertUnique(value, seen, label, errors) {
183
+ if (value === null)
184
+ return;
185
+ if (seen.has(value))
186
+ errors.push(`${label} must be unique`);
187
+ else
188
+ seen.add(value);
189
+ }
190
+ function waitCode(value, label, allowedWaitCodes, errors) {
191
+ const code = string(value, label, errors);
192
+ if (code === null)
193
+ return null;
194
+ if (!/^[a-z][a-z0-9_]{0,31}$/.test(code) || !allowedWaitCodes.has(code)) {
195
+ errors.push(`${label} must be predeclared by measurement_policy`);
39
196
  return null;
40
197
  }
41
- const flowId = value.flow_id;
42
- if (!nonEmptyString(flowId)) errors.push(`${path}.flow_id must be non-empty`);
43
- if (value.eligible !== true) errors.push(`${path}.eligible must be true`);
44
- if (value.status !== 'completed') errors.push(`${path}.status must be completed`);
45
- if (value.eligible_planning_index !== expectedEligibleIndex) {
46
- errors.push(`${path}.eligible_planning_index must be ${expectedEligibleIndex}`);
47
- }
48
- const startedAtMs = parseTimestamp(value.started_at, `${path}.started_at`, errors);
49
- const completedAtMs = parseTimestamp(value.completed_at, `${path}.completed_at`, errors);
50
- if (startedAtMs === null || completedAtMs === null) return null;
51
- if (completedAtMs <= startedAtMs) {
52
- errors.push(`${path}.completed_at must be after started_at`);
198
+ return code;
199
+ }
200
+ function scoreFlow(value, path, expectedEligibleIndex, allowedWaitCodes, errors) {
201
+ const raw = object(value, path, errors);
202
+ if (raw === null)
53
203
  return null;
204
+ exactKeys(raw, path, ['flow_id', 'eligible_planning_index', 'eligible', 'status', 'started_at', 'completed_at', 'external_waits'], errors);
205
+ const flowId = hmacRef(raw.flow_id, `${path}.flow_id`, errors);
206
+ const eligible = boolean(raw.eligible, `${path}.eligible`, errors);
207
+ if (eligible !== true)
208
+ errors.push(`${path}.eligible must be true`);
209
+ literal(raw.status, `${path}.status`, ['completed'], errors);
210
+ const eligiblePlanningIndex = integer(raw.eligible_planning_index, `${path}.eligible_planning_index`, errors);
211
+ if (eligiblePlanningIndex !== null && eligiblePlanningIndex !== expectedEligibleIndex) {
212
+ errors.push(`${path}.eligible_planning_index must match the paired flow position`);
54
213
  }
55
- const rawWaits = value.external_waits;
56
- if (!Array.isArray(rawWaits)) {
57
- errors.push(`${path}.external_waits must be an array`);
214
+ const startedAt = parseTimestamp(raw.started_at, `${path}.started_at`, errors);
215
+ const completedAt = parseTimestamp(raw.completed_at, `${path}.completed_at`, errors);
216
+ if (startedAt === null || completedAt === null)
217
+ return null;
218
+ if (completedAt.ms <= startedAt.ms) {
219
+ errors.push(`${path}.completed_at must be after started_at`);
58
220
  return null;
59
221
  }
60
222
  const waits = [];
61
- for (const [index, rawWait] of rawWaits.entries()){
223
+ for (const [index, rawWait] of array(raw.external_waits, `${path}.external_waits`, errors).entries()) {
62
224
  const waitPath = `${path}.external_waits[${index}]`;
63
- if (!isObject(rawWait)) {
64
- errors.push(`${waitPath} must be an object`);
225
+ const wait = object(rawWait, waitPath, errors);
226
+ if (wait === null)
65
227
  continue;
66
- }
67
- const code = rawWait.code;
68
- if (!nonEmptyString(code) || !allowedWaitCodes.has(code)) {
69
- errors.push(`${waitPath}.code must be predeclared by measurement_policy`);
228
+ exactKeys(wait, waitPath, ['code', 'started_at', 'completed_at'], errors);
229
+ const code = waitCode(wait.code, `${waitPath}.code`, allowedWaitCodes, errors);
230
+ const waitStartedAt = parseTimestamp(wait.started_at, `${waitPath}.started_at`, errors);
231
+ const waitCompletedAt = parseTimestamp(wait.completed_at, `${waitPath}.completed_at`, errors);
232
+ if (code === null || waitStartedAt === null || waitCompletedAt === null)
70
233
  continue;
71
- }
72
- const waitStartedAtMs = parseTimestamp(rawWait.started_at, `${waitPath}.started_at`, errors);
73
- const waitCompletedAtMs = parseTimestamp(rawWait.completed_at, `${waitPath}.completed_at`, errors);
74
- if (waitStartedAtMs === null || waitCompletedAtMs === null) continue;
75
- if (waitCompletedAtMs <= waitStartedAtMs) {
234
+ if (waitCompletedAt.ms <= waitStartedAt.ms) {
76
235
  errors.push(`${waitPath}.completed_at must be after started_at`);
77
236
  continue;
78
237
  }
79
- if (waitStartedAtMs < startedAtMs || waitCompletedAtMs > completedAtMs) {
238
+ if (waitStartedAt.ms < startedAt.ms || waitCompletedAt.ms > completedAt.ms) {
80
239
  errors.push(`${waitPath} must stay inside the planning flow`);
81
240
  continue;
82
241
  }
83
- waits.push({
84
- code,
85
- startedAtMs: waitStartedAtMs,
86
- completedAtMs: waitCompletedAtMs
87
- });
88
- }
89
- waits.sort((a, b)=>a.startedAtMs - b.startedAtMs);
90
- for(let index = 1; index < waits.length; index += 1){
242
+ waits.push({ code, startedAtMs: waitStartedAt.ms, completedAtMs: waitCompletedAt.ms });
243
+ }
244
+ waits.sort((a, b) => a.startedAtMs - b.startedAtMs);
245
+ for (let index = 1; index < waits.length; index += 1) {
91
246
  if (waits[index].startedAtMs < waits[index - 1].completedAtMs) {
92
247
  errors.push(`${path}.external_waits must not overlap`);
93
248
  }
94
249
  }
95
- const externalWaitMs = waits.reduce((total, wait)=>total + wait.completedAtMs - wait.startedAtMs, 0);
96
- const activeSeconds = (completedAtMs - startedAtMs - externalWaitMs) / 1_000;
97
- if (activeSeconds <= 0) errors.push(`${path} must have positive active planning duration`);
98
- return nonEmptyString(flowId) ? {
99
- activeSeconds: round(activeSeconds),
100
- completedAtMs,
101
- flowId,
102
- startedAtMs
103
- } : null;
250
+ const externalWaitMs = waits.reduce((total, wait) => total + wait.completedAtMs - wait.startedAtMs, 0);
251
+ const activeSeconds = (completedAt.ms - startedAt.ms - externalWaitMs) / 1_000;
252
+ if (activeSeconds <= 0)
253
+ errors.push(`${path} must have positive active planning duration`);
254
+ return flowId === null
255
+ ? null
256
+ : { activeSeconds, completedAtMs: completedAt.ms, flowId, startedAtMs: startedAt.ms };
104
257
  }
105
- function invalidReport(errors, schema = '', evidenceKind = '') {
258
+ function sourceReviewReport(evidenceKind, state, contractMet, fields = {}) {
259
+ const shared = {
260
+ reviewed_at: fields.reviewedAt ?? null,
261
+ reviewer_ref: fields.reviewerRef ?? null,
262
+ attestation_ref: fields.attestationRef ?? null,
263
+ private_source_digest_sha256: fields.privateSourceDigest ?? null,
264
+ reviewed_summary_digest_sha256: fields.reviewedSummaryDigest ?? null,
265
+ current_summary_digest_sha256: fields.currentSummaryDigest ?? null,
266
+ };
267
+ if (evidenceKind === 'synthetic_fixture') {
268
+ return {
269
+ required_for_exit: false,
270
+ state,
271
+ contract_met: false,
272
+ provenance_verified_by_scorer: false,
273
+ ...shared,
274
+ reason: 'synthetic_fixture validates formulas only and is never business evidence',
275
+ };
276
+ }
277
+ if (contractMet) {
278
+ return {
279
+ required_for_exit: true,
280
+ state,
281
+ contract_met: true,
282
+ provenance_verified_by_scorer: false,
283
+ ...shared,
284
+ reason: 'manual source-review attestation contract is present for this scoring payload; raw private source review remains a human responsibility',
285
+ };
286
+ }
287
+ return {
288
+ required_for_exit: evidenceKind === 'observed_private',
289
+ state,
290
+ contract_met: false,
291
+ provenance_verified_by_scorer: false,
292
+ ...shared,
293
+ reason: evidenceKind === 'observed_private'
294
+ ? 'observed_private remains a candidate until the manual source-review attestation contract matches this scoring payload'
295
+ : 'source kind is not exit-eligible',
296
+ };
297
+ }
298
+ function scoreSourceReview(value, evidenceKind, currentSummaryDigest, errors) {
299
+ const raw = object(value, 'source_review', errors);
300
+ if (raw === null) {
301
+ return { report: sourceReviewReport(evidenceKind, 'unknown', false, { currentSummaryDigest }) };
302
+ }
303
+ exactKeys(raw, 'source_review', [
304
+ 'schema_version',
305
+ 'state',
306
+ 'reviewed_at',
307
+ 'reviewer_ref',
308
+ 'attestation_ref',
309
+ 'private_source_digest_sha256',
310
+ 'reviewed_summary_digest_sha256',
311
+ 'checks',
312
+ ], errors);
313
+ literal(raw.schema_version, 'source_review.schema_version', ['memory_value_source_review.v1'], errors);
314
+ const state = literal(raw.state, 'source_review.state', ['not_required_for_synthetic', 'candidate', 'manual_attested'], errors) ?? 'unknown';
315
+ const reviewedAt = nullableTimestamp(raw.reviewed_at, 'source_review.reviewed_at', errors);
316
+ const reviewerRef = nullableHmacRef(raw.reviewer_ref, 'source_review.reviewer_ref', errors);
317
+ const attestationRef = nullableHmacRef(raw.attestation_ref, 'source_review.attestation_ref', errors);
318
+ const privateSourceDigest = nullableSha256(raw.private_source_digest_sha256, 'source_review.private_source_digest_sha256', errors);
319
+ const reviewedSummaryDigest = nullableSha256(raw.reviewed_summary_digest_sha256, 'source_review.reviewed_summary_digest_sha256', errors);
320
+ if (reviewedSummaryDigest !== null && reviewedSummaryDigest !== currentSummaryDigest) {
321
+ errors.push('source_review.reviewed_summary_digest_sha256 must match the current scoring payload digest');
322
+ }
323
+ const checks = object(raw.checks, 'source_review.checks', errors);
324
+ const parsedChecks = {};
325
+ if (checks !== null) {
326
+ exactKeys(checks, 'source_review.checks', [
327
+ 'raw_private_material_excluded',
328
+ 'paired_cohort_source_reviewed',
329
+ 'summary_matches_private_source',
330
+ 'no_synthetic_or_test_subjects',
331
+ ], errors);
332
+ for (const key of [
333
+ 'raw_private_material_excluded',
334
+ 'paired_cohort_source_reviewed',
335
+ 'summary_matches_private_source',
336
+ 'no_synthetic_or_test_subjects',
337
+ ]) {
338
+ const parsed = boolean(checks[key], `source_review.checks.${key}`, errors);
339
+ if (parsed !== null)
340
+ parsedChecks[key] = parsed;
341
+ }
342
+ }
343
+ if (evidenceKind === 'synthetic_fixture' && state !== 'not_required_for_synthetic') {
344
+ errors.push('source_review.state must match synthetic evidence');
345
+ }
346
+ if (evidenceKind === 'observed_private' && state === 'not_required_for_synthetic') {
347
+ errors.push('source_review.state must require observed private review');
348
+ }
349
+ const allReviewChecksMet = [
350
+ parsedChecks.raw_private_material_excluded,
351
+ parsedChecks.paired_cohort_source_reviewed,
352
+ parsedChecks.summary_matches_private_source,
353
+ parsedChecks.no_synthetic_or_test_subjects,
354
+ ].every(Boolean);
355
+ const contractMet = evidenceKind === 'observed_private'
356
+ && state === 'manual_attested'
357
+ && reviewedAt !== null
358
+ && reviewerRef !== null
359
+ && attestationRef !== null
360
+ && privateSourceDigest !== null
361
+ && reviewedSummaryDigest === currentSummaryDigest
362
+ && allReviewChecksMet;
363
+ return {
364
+ report: sourceReviewReport(evidenceKind, state, contractMet, {
365
+ reviewedAt: reviewedAt?.text ?? null,
366
+ reviewerRef,
367
+ attestationRef,
368
+ privateSourceDigest,
369
+ reviewedSummaryDigest,
370
+ currentSummaryDigest,
371
+ }),
372
+ };
373
+ }
374
+ function invalidReport(errors, schema = '', evidenceKind = '', sourceReview = sourceReviewReport(evidenceKind || null, 'unknown', false)) {
106
375
  return {
107
376
  schema: 'memory_value_report.v1',
108
377
  contract_valid: false,
@@ -111,91 +380,75 @@ function invalidReport(errors, schema = '', evidenceKind = '') {
111
380
  fixture_schema: schema,
112
381
  evidence_kind: evidenceKind,
113
382
  quantile_method: 'nearest_rank',
114
- active_duration_rule: 'wall_clock_minus_non_overlapping_predeclared_external_waits'
383
+ active_duration_rule: 'wall_clock_minus_non_overlapping_predeclared_external_waits',
384
+ id_format: 'hmac-sha256:<64lowerhex>',
385
+ source_review: sourceReview,
115
386
  },
116
387
  cohort: {
117
388
  eligible_pair_count: 0,
118
- first_active_seconds: {
119
- p50: 0,
120
- p75: 0
121
- },
122
- returning_active_seconds: {
123
- p50: 0,
124
- p75: 0
125
- },
126
- paired_reduction_ratio: {
127
- p50: 0,
128
- p75: 0
129
- },
389
+ first_active_seconds: { p50: 0, p75: 0 },
390
+ returning_active_seconds: { p50: 0, p75: 0 },
391
+ paired_reduction_ratio: { p50: 0, p75: 0 },
130
392
  target_median_reduction_ratio: 0,
131
393
  target_met: false,
132
394
  minimum_pair_count_for_exit: 0,
133
- sample_size_met: false
395
+ sample_size_met: false,
134
396
  },
135
397
  experience_reflux: {
136
398
  recalled_experience_count: 0,
137
399
  verified_experience_count: 0,
138
400
  baseline: null,
139
401
  baseline_available: false,
140
- real_evidence: false
402
+ real_evidence: false,
141
403
  },
142
404
  preference_assertions: {
143
405
  total_count: 0,
144
406
  traceable_count: 0,
145
407
  traceable_ratio: 0,
146
408
  hard_filter_violation_count: 0,
147
- contract_met: false
148
- },
149
- p4: {
150
- state: 'unknown',
151
- trigger_observed: false,
152
- contract_met: false
409
+ contract_met: false,
153
410
  },
411
+ p4: { state: 'unknown', trigger_observed: false, contract_met: false },
154
412
  exit_evidence_eligible: false,
155
- exit_ready: false
413
+ exit_ready: false,
156
414
  };
157
415
  }
158
416
  export function scoreMemoryValue(input) {
159
417
  const errors = [];
160
- if (!isObject(input)) return invalidReport([
161
- 'root must be an object'
162
- ]);
163
- const schema = nonEmptyString(input.schema) ? input.schema : '';
164
- const evidenceKind = nonEmptyString(input.evidence_kind) ? input.evidence_kind : '';
165
- if (schema !== 'memory_value_fixture.v1') errors.push('schema must be memory_value_fixture.v1');
166
- if (![
167
- 'synthetic_fixture',
168
- 'observed_private'
169
- ].includes(evidenceKind)) {
170
- errors.push('evidence_kind must be synthetic_fixture or observed_private');
171
- }
172
- const policy = input.measurement_policy;
173
- if (!isObject(policy)) return invalidReport([
174
- ...errors,
175
- 'measurement_policy must be an object'
176
- ], schema, evidenceKind);
177
- if (policy.quantile_method !== 'nearest_rank') errors.push('measurement_policy.quantile_method must be nearest_rank');
178
- const rawWaitCodes = policy.predeclared_external_wait_codes;
418
+ if (!isObject(input))
419
+ return invalidReport(['root must be an object']);
420
+ exactKeys(input, 'root', ['schema', 'evidence_kind', 'source_review', 'measurement_policy', 'pairs', 'experience_reflux_events', 'preference_assertions', 'p4'], errors);
421
+ const currentSummaryDigest = memoryValueSummaryDigest(input);
422
+ const schema = literal(input.schema, 'schema', ['memory_value_fixture.v1'], errors);
423
+ const evidenceKind = literal(input.evidence_kind, 'evidence_kind', ['synthetic_fixture', 'observed_private'], errors);
424
+ const sourceReview = scoreSourceReview(input.source_review, evidenceKind, currentSummaryDigest, errors).report;
425
+ const policy = object(input.measurement_policy, 'measurement_policy', errors);
179
426
  const allowedWaitCodes = new Set();
180
- if (!Array.isArray(rawWaitCodes) || rawWaitCodes.length === 0) {
181
- errors.push('measurement_policy.predeclared_external_wait_codes must be a non-empty array');
182
- } else {
183
- for (const code of rawWaitCodes){
184
- if (nonEmptyString(code)) allowedWaitCodes.add(code);
185
- else errors.push('measurement_policy.predeclared_external_wait_codes must contain non-empty strings');
427
+ if (policy !== null) {
428
+ exactKeys(policy, 'measurement_policy', [
429
+ 'quantile_method',
430
+ 'predeclared_external_wait_codes',
431
+ 'minimum_pair_count_for_exit',
432
+ 'target_median_reduction_ratio',
433
+ ], errors);
434
+ literal(policy.quantile_method, 'measurement_policy.quantile_method', ['nearest_rank'], errors);
435
+ for (const code of array(policy.predeclared_external_wait_codes, 'measurement_policy.predeclared_external_wait_codes', errors, { nonEmpty: true })) {
436
+ if (!nonEmptyString(code) || !/^[a-z][a-z0-9_]{0,31}$/.test(code)) {
437
+ errors.push('measurement_policy.predeclared_external_wait_codes must contain declared wait-code strings');
438
+ continue;
439
+ }
440
+ if (allowedWaitCodes.has(code))
441
+ errors.push('measurement_policy.predeclared_external_wait_codes must not contain duplicates');
442
+ allowedWaitCodes.add(code);
443
+ }
444
+ const minimumPairCount = integer(policy.minimum_pair_count_for_exit, 'measurement_policy.minimum_pair_count_for_exit', errors);
445
+ if (minimumPairCount !== null && minimumPairCount !== M4_ACCEPTANCE.minimumPairCountForExit) {
446
+ errors.push(`measurement_policy.minimum_pair_count_for_exit is frozen at ${M4_ACCEPTANCE.minimumPairCountForExit}`);
447
+ }
448
+ const targetReduction = finiteNumber(policy.target_median_reduction_ratio, 'measurement_policy.target_median_reduction_ratio', errors);
449
+ if (targetReduction !== null && targetReduction !== M4_ACCEPTANCE.targetMedianReductionRatio) {
450
+ errors.push(`measurement_policy.target_median_reduction_ratio is frozen at ${M4_ACCEPTANCE.targetMedianReductionRatio}`);
186
451
  }
187
- }
188
- const minimumPairCount = policy.minimum_pair_count_for_exit;
189
- const targetReduction = policy.target_median_reduction_ratio;
190
- if (!Number.isInteger(minimumPairCount) || minimumPairCount <= 0) {
191
- errors.push('measurement_policy.minimum_pair_count_for_exit must be a positive integer');
192
- }
193
- if (!finiteNumber(targetReduction) || targetReduction < 0 || targetReduction > 1) {
194
- errors.push('measurement_policy.target_median_reduction_ratio must be between 0 and 1');
195
- }
196
- const rawPairs = input.pairs;
197
- if (!Array.isArray(rawPairs) || rawPairs.length === 0) {
198
- errors.push('pairs must be a non-empty array');
199
452
  }
200
453
  const firstDurations = [];
201
454
  const returningDurations = [];
@@ -203,103 +456,110 @@ export function scoreMemoryValue(input) {
203
456
  const pairIds = new Set();
204
457
  const subjectRefs = new Set();
205
458
  const flowIds = new Set();
206
- for (const [index, rawPair] of (Array.isArray(rawPairs) ? rawPairs : []).entries()){
459
+ for (const [index, rawPair] of array(input.pairs, 'pairs', errors, { nonEmpty: true }).entries()) {
207
460
  const pairPath = `pairs[${index}]`;
208
- if (!isObject(rawPair)) {
209
- errors.push(`${pairPath} must be an object`);
461
+ const pair = object(rawPair, pairPath, errors);
462
+ if (pair === null)
210
463
  continue;
464
+ exactKeys(pair, pairPath, ['pair_id', 'subject_ref', 'first', 'returning'], errors);
465
+ const pairId = hmacRef(pair.pair_id, `${pairPath}.pair_id`, errors);
466
+ const subjectRef = hmacRef(pair.subject_ref, `${pairPath}.subject_ref`, errors);
467
+ assertUnique(pairId, pairIds, `${pairPath}.pair_id`, errors);
468
+ assertUnique(subjectRef, subjectRefs, `${pairPath}.subject_ref`, errors);
469
+ const first = scoreFlow(pair.first, `${pairPath}.first`, 1, allowedWaitCodes, errors);
470
+ const returning = scoreFlow(pair.returning, `${pairPath}.returning`, 2, allowedWaitCodes, errors);
471
+ for (const flow of [first, returning]) {
472
+ if (!flow)
473
+ continue;
474
+ assertUnique(flow.flowId, flowIds, `${pairPath}.flow_id`, errors);
211
475
  }
212
- if (!nonEmptyString(rawPair.pair_id)) errors.push(`${pairPath}.pair_id must be non-empty`);
213
- else if (pairIds.has(rawPair.pair_id)) errors.push(`${pairPath}.pair_id must be unique`);
214
- else pairIds.add(rawPair.pair_id);
215
- if (!nonEmptyString(rawPair.subject_ref)) {
216
- errors.push(`${pairPath}.subject_ref must be a non-empty pseudonymous reference`);
217
- } else if (subjectRefs.has(rawPair.subject_ref)) {
218
- errors.push(`${pairPath}.subject_ref must be unique across the paired cohort`);
219
- } else {
220
- subjectRefs.add(rawPair.subject_ref);
221
- }
222
- const first = scoreFlow(rawPair.first, `${pairPath}.first`, 1, allowedWaitCodes, errors);
223
- const returning = scoreFlow(rawPair.returning, `${pairPath}.returning`, 2, allowedWaitCodes, errors);
224
- for (const flow of [
225
- first,
226
- returning
227
- ]){
228
- if (!flow) continue;
229
- if (flowIds.has(flow.flowId)) errors.push(`${pairPath} flow_id values must be globally unique`);
230
- flowIds.add(flow.flowId);
231
- }
232
- if (!first || !returning || first.activeSeconds <= 0 || returning.activeSeconds <= 0) continue;
476
+ if (!first || !returning || first.activeSeconds <= 0 || returning.activeSeconds <= 0)
477
+ continue;
233
478
  if (returning.startedAtMs <= first.completedAtMs) {
234
479
  errors.push(`${pairPath}.returning must start after the first completed flow`);
235
480
  }
236
481
  firstDurations.push(first.activeSeconds);
237
482
  returningDurations.push(returning.activeSeconds);
238
- reductions.push(round((first.activeSeconds - returning.activeSeconds) / first.activeSeconds));
483
+ reductions.push((first.activeSeconds - returning.activeSeconds) / first.activeSeconds);
239
484
  }
240
- const rawRefluxEvents = input.experience_reflux_events;
241
- if (!Array.isArray(rawRefluxEvents)) errors.push('experience_reflux_events must be an array');
242
485
  const recalled = new Set();
243
486
  const verified = new Set();
244
- for (const [index, rawEvent] of (Array.isArray(rawRefluxEvents) ? rawRefluxEvents : []).entries()){
487
+ const refluxEventKeys = new Set();
488
+ const refluxEvidenceRefs = new Set();
489
+ for (const [index, rawEvent] of array(input.experience_reflux_events, 'experience_reflux_events', errors).entries()) {
245
490
  const eventPath = `experience_reflux_events[${index}]`;
246
- if (!isObject(rawEvent)) {
247
- errors.push(`${eventPath} must be an object`);
491
+ const event = object(rawEvent, eventPath, errors);
492
+ if (event === null)
248
493
  continue;
249
- }
250
- if (!nonEmptyString(rawEvent.experience_id)) {
251
- errors.push(`${eventPath}.experience_id must be non-empty`);
494
+ exactKeys(event, eventPath, ['experience_id', 'kind', 'evidence_ref'], errors);
495
+ const experienceId = hmacRef(event.experience_id, `${eventPath}.experience_id`, errors);
496
+ const kind = literal(event.kind, `${eventPath}.kind`, ['recalled', 'verified_outcome'], errors);
497
+ const evidenceRef = hmacRef(event.evidence_ref, `${eventPath}.evidence_ref`, errors);
498
+ assertUnique(evidenceRef, refluxEvidenceRefs, `${eventPath}.evidence_ref`, errors);
499
+ if (experienceId === null || kind === null)
252
500
  continue;
253
- }
254
- if (!nonEmptyString(rawEvent.evidence_ref)) errors.push(`${eventPath}.evidence_ref must be non-empty`);
255
- if (rawEvent.kind === 'recalled') recalled.add(rawEvent.experience_id);
256
- else if (rawEvent.kind === 'verified_outcome') verified.add(rawEvent.experience_id);
257
- else errors.push(`${eventPath}.kind must be recalled or verified_outcome`);
501
+ assertUnique(`${kind}:${experienceId}`, refluxEventKeys, `${eventPath}.experience_id+kind`, errors);
502
+ if (kind === 'recalled')
503
+ recalled.add(experienceId);
504
+ else
505
+ verified.add(experienceId);
258
506
  }
259
- for (const experienceId of verified){
260
- if (!recalled.has(experienceId)) errors.push(`verified experience ${experienceId} must have a recalled event`);
507
+ for (const experienceId of verified) {
508
+ if (!recalled.has(experienceId))
509
+ errors.push('each verified_outcome experience must have a recalled event');
261
510
  }
262
- const verifiedRecalledCount = [
263
- ...verified
264
- ].filter((id)=>recalled.has(id)).length;
511
+ const verifiedRecalledCount = [...verified].filter(id => recalled.has(id)).length;
265
512
  const refluxBaseline = recalled.size > 0 ? round(verifiedRecalledCount / recalled.size) : null;
266
- const rawAssertions = input.preference_assertions;
267
- if (!Array.isArray(rawAssertions)) errors.push('preference_assertions must be an array');
513
+ const assertionsRaw = array(input.preference_assertions, 'preference_assertions', errors);
268
514
  let traceableCount = 0;
269
515
  let hardFilterViolationCount = 0;
270
- const assertions = Array.isArray(rawAssertions) ? rawAssertions : [];
271
- for (const [index, rawAssertion] of assertions.entries()){
516
+ const assertionIds = new Set();
517
+ for (const [index, rawAssertion] of assertionsRaw.entries()) {
272
518
  const assertionPath = `preference_assertions[${index}]`;
273
- if (!isObject(rawAssertion)) {
274
- errors.push(`${assertionPath} must be an object`);
519
+ const assertion = object(rawAssertion, assertionPath, errors);
520
+ if (assertion === null)
275
521
  continue;
276
- }
277
- if (!nonEmptyString(rawAssertion.assertion_id)) errors.push(`${assertionPath}.assertion_id must be non-empty`);
278
- if (nonEmptyString(rawAssertion.evidence_ref)) traceableCount += 1;
279
- if (rawAssertion.hard_filter === true || rawAssertion.consumer === 'hard_filter') hardFilterViolationCount += 1;
522
+ exactKeys(assertion, assertionPath, ['assertion_id', 'evidence_ref', 'consumer', 'hard_filter'], errors);
523
+ const assertionId = hmacRef(assertion.assertion_id, `${assertionPath}.assertion_id`, errors);
524
+ const evidenceRef = hmacRef(assertion.evidence_ref, `${assertionPath}.evidence_ref`, errors);
525
+ const consumer = literal(assertion.consumer, `${assertionPath}.consumer`, ['ranking', 'explanation', 'hard_filter'], errors);
526
+ const hardFilter = boolean(assertion.hard_filter, `${assertionPath}.hard_filter`, errors);
527
+ assertUnique(assertionId, assertionIds, `${assertionPath}.assertion_id`, errors);
528
+ if (evidenceRef !== null)
529
+ traceableCount += 1;
530
+ if (hardFilter === true || consumer === 'hard_filter')
531
+ hardFilterViolationCount += 1;
280
532
  }
281
- const traceableRatio = assertions.length > 0 ? round(traceableCount / assertions.length) : 0;
282
- const assertionContractMet = assertions.length > 0 && traceableRatio === 1 && hardFilterViolationCount === 0;
283
- const rawP4 = input.p4;
533
+ const traceableRatio = assertionsRaw.length > 0 ? round(traceableCount / assertionsRaw.length) : 0;
534
+ const assertionContractMet = assertionsRaw.length > 0 && traceableRatio === 1 && hardFilterViolationCount === 0;
284
535
  let p4State = 'unknown';
285
536
  let p4TriggerObserved = false;
286
- if (!isObject(rawP4) || !isObject(rawP4.triggers)) {
287
- errors.push('p4.state and p4.triggers must be declared');
288
- } else {
289
- if (nonEmptyString(rawP4.state)) p4State = rawP4.state;
290
- else errors.push('p4.state must be non-empty');
291
- p4TriggerObserved = rawP4.triggers.real_usage === true || rawP4.triggers.multi_user === true;
537
+ const rawP4 = object(input.p4, 'p4', errors);
538
+ if (rawP4 !== null) {
539
+ exactKeys(rawP4, 'p4', ['state', 'triggers'], errors);
540
+ p4State = literal(rawP4.state, 'p4.state', ['closed', 'open'], errors) ?? 'unknown';
541
+ const triggers = object(rawP4.triggers, 'p4.triggers', errors);
542
+ if (triggers !== null) {
543
+ exactKeys(triggers, 'p4.triggers', ['real_usage', 'multi_user'], errors);
544
+ const realUsage = boolean(triggers.real_usage, 'p4.triggers.real_usage', errors);
545
+ const multiUser = boolean(triggers.multi_user, 'p4.triggers.multi_user', errors);
546
+ p4TriggerObserved = realUsage === true || multiUser === true;
547
+ }
292
548
  }
293
549
  const p4ContractMet = p4TriggerObserved || p4State === 'closed';
294
- if (errors.length > 0) return invalidReport(errors, schema, evidenceKind);
295
- const medianReduction = nearestRank(reductions, 0.5);
296
- const numericMinimumPairCount = minimumPairCount;
297
- const numericTargetReduction = targetReduction;
298
- const sampleSizeMet = firstDurations.length >= numericMinimumPairCount;
299
- const targetMet = medianReduction >= numericTargetReduction;
300
- const exitEvidenceEligible = evidenceKind === 'observed_private';
550
+ if (errors.length > 0)
551
+ return invalidReport(errors, schema ?? '', evidenceKind ?? '', sourceReview);
552
+ const medianReduction = nearestRankRaw(reductions, 0.5);
553
+ const sampleSizeMet = firstDurations.length >= M4_ACCEPTANCE.minimumPairCountForExit;
554
+ const targetMet = medianReduction >= M4_ACCEPTANCE.targetMedianReductionRatio;
555
+ const sourceEvidenceEligible = evidenceKind === 'observed_private' && sourceReview.contract_met;
301
556
  const baselineAvailable = refluxBaseline !== null;
302
- const exitReady = exitEvidenceEligible && sampleSizeMet && targetMet && baselineAvailable && assertionContractMet && p4ContractMet;
557
+ const exitReady = sourceEvidenceEligible
558
+ && sampleSizeMet
559
+ && targetMet
560
+ && baselineAvailable
561
+ && assertionContractMet
562
+ && p4ContractMet;
303
563
  return {
304
564
  schema: 'memory_value_report.v1',
305
565
  contract_valid: true,
@@ -308,48 +568,41 @@ export function scoreMemoryValue(input) {
308
568
  fixture_schema: schema,
309
569
  evidence_kind: evidenceKind,
310
570
  quantile_method: 'nearest_rank',
311
- active_duration_rule: 'wall_clock_minus_non_overlapping_predeclared_external_waits'
571
+ active_duration_rule: 'wall_clock_minus_non_overlapping_predeclared_external_waits',
572
+ id_format: 'hmac-sha256:<64lowerhex>',
573
+ source_review: sourceReview,
312
574
  },
313
575
  cohort: {
314
576
  eligible_pair_count: firstDurations.length,
315
- first_active_seconds: {
316
- p50: nearestRank(firstDurations, 0.5),
317
- p75: nearestRank(firstDurations, 0.75)
318
- },
319
- returning_active_seconds: {
320
- p50: nearestRank(returningDurations, 0.5),
321
- p75: nearestRank(returningDurations, 0.75)
322
- },
323
- paired_reduction_ratio: {
324
- p50: medianReduction,
325
- p75: nearestRank(reductions, 0.75)
326
- },
327
- target_median_reduction_ratio: numericTargetReduction,
577
+ first_active_seconds: { p50: nearestRank(firstDurations, 0.5), p75: nearestRank(firstDurations, 0.75) },
578
+ returning_active_seconds: { p50: nearestRank(returningDurations, 0.5), p75: nearestRank(returningDurations, 0.75) },
579
+ paired_reduction_ratio: { p50: round(medianReduction), p75: nearestRank(reductions, 0.75) },
580
+ target_median_reduction_ratio: M4_ACCEPTANCE.targetMedianReductionRatio,
328
581
  target_met: targetMet,
329
- minimum_pair_count_for_exit: numericMinimumPairCount,
330
- sample_size_met: sampleSizeMet
582
+ minimum_pair_count_for_exit: M4_ACCEPTANCE.minimumPairCountForExit,
583
+ sample_size_met: sampleSizeMet,
331
584
  },
332
585
  experience_reflux: {
333
586
  recalled_experience_count: recalled.size,
334
587
  verified_experience_count: verifiedRecalledCount,
335
588
  baseline: refluxBaseline,
336
589
  baseline_available: baselineAvailable,
337
- real_evidence: exitEvidenceEligible
590
+ real_evidence: sourceEvidenceEligible,
338
591
  },
339
592
  preference_assertions: {
340
- total_count: assertions.length,
593
+ total_count: assertionsRaw.length,
341
594
  traceable_count: traceableCount,
342
595
  traceable_ratio: traceableRatio,
343
596
  hard_filter_violation_count: hardFilterViolationCount,
344
- contract_met: assertionContractMet
597
+ contract_met: assertionContractMet,
345
598
  },
346
599
  p4: {
347
600
  state: p4State,
348
601
  trigger_observed: p4TriggerObserved,
349
- contract_met: p4ContractMet
602
+ contract_met: p4ContractMet,
350
603
  },
351
- exit_evidence_eligible: exitEvidenceEligible,
352
- exit_ready: exitReady
604
+ exit_evidence_eligible: sourceEvidenceEligible,
605
+ exit_ready: exitReady,
353
606
  };
354
607
  }
355
608
  function main() {
@@ -362,18 +615,17 @@ function main() {
362
615
  let input;
363
616
  try {
364
617
  input = JSON.parse(readFileSync(resolve(inputPath), 'utf8'));
365
- } catch (error) {
366
- const message = error instanceof Error ? error.message : String(error);
367
- console.error(`cannot read memory value input: ${message}`);
618
+ }
619
+ catch {
620
+ console.error('MEMORY_VALUE_INPUT_INVALID: input file could not be read or parsed');
368
621
  process.exitCode = 2;
369
622
  return;
370
623
  }
371
624
  const report = scoreMemoryValue(input);
372
625
  console.log(JSON.stringify(report, null, 2));
373
- if (!report.contract_valid) process.exitCode = 2;
626
+ if (!report.contract_valid)
627
+ process.exitCode = 2;
374
628
  }
375
629
  const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : '';
376
- if (import.meta.url === invokedPath) main();
377
-
378
-
379
- //# sourceURL=ts/scripts/memory-value-report.ts
630
+ if (import.meta.url === invokedPath)
631
+ main();