universal-physics-tensor 0.47.1 → 1.0.2

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 (702) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +405 -383
  3. package/bin/upt.mjs +7 -1
  4. package/dist/atlas/benchmark/hybrid-retrieval.d.ts +108 -0
  5. package/dist/atlas/benchmark/hybrid-retrieval.d.ts.map +1 -0
  6. package/dist/atlas/benchmark/hybrid-retrieval.js +241 -0
  7. package/dist/atlas/benchmark/hybrid-retrieval.js.map +1 -0
  8. package/dist/atlas/catalog-formal-ref.d.ts +24 -0
  9. package/dist/atlas/catalog-formal-ref.d.ts.map +1 -0
  10. package/dist/atlas/catalog-formal-ref.js +36 -0
  11. package/dist/atlas/catalog-formal-ref.js.map +1 -0
  12. package/dist/atlas/chain-pipeline.d.ts +52 -0
  13. package/dist/atlas/chain-pipeline.d.ts.map +1 -0
  14. package/dist/atlas/chain-pipeline.js +176 -0
  15. package/dist/atlas/chain-pipeline.js.map +1 -0
  16. package/dist/atlas/composition-table.d.ts +11 -46
  17. package/dist/atlas/composition-table.d.ts.map +1 -1
  18. package/dist/atlas/composition-table.js +7 -124
  19. package/dist/atlas/composition-table.js.map +1 -1
  20. package/dist/atlas/conventions.d.ts +4 -58
  21. package/dist/atlas/conventions.d.ts.map +1 -1
  22. package/dist/atlas/conventions.js +3 -88
  23. package/dist/atlas/conventions.js.map +1 -1
  24. package/dist/atlas/derive-evidence.d.ts +50 -3
  25. package/dist/atlas/derive-evidence.d.ts.map +1 -1
  26. package/dist/atlas/derive-evidence.js +73 -4
  27. package/dist/atlas/derive-evidence.js.map +1 -1
  28. package/dist/atlas/diffusion/bridges-closure.d.ts.map +1 -1
  29. package/dist/atlas/diffusion/bridges-closure.js +5 -1
  30. package/dist/atlas/diffusion/bridges-closure.js.map +1 -1
  31. package/dist/atlas/index.d.ts +9 -7
  32. package/dist/atlas/index.d.ts.map +1 -1
  33. package/dist/atlas/index.js +6 -5
  34. package/dist/atlas/index.js.map +1 -1
  35. package/dist/atlas/oscillators/bridges-exact.d.ts.map +1 -1
  36. package/dist/atlas/oscillators/bridges-exact.js +6 -0
  37. package/dist/atlas/oscillators/bridges-exact.js.map +1 -1
  38. package/dist/atlas/oscillators/bridges-limits.d.ts +2 -0
  39. package/dist/atlas/oscillators/bridges-limits.d.ts.map +1 -1
  40. package/dist/atlas/oscillators/bridges-limits.js +11 -19
  41. package/dist/atlas/oscillators/bridges-limits.js.map +1 -1
  42. package/dist/atlas/oscillators/limit-witnesses.d.ts +93 -0
  43. package/dist/atlas/oscillators/limit-witnesses.d.ts.map +1 -0
  44. package/dist/atlas/oscillators/limit-witnesses.js +152 -0
  45. package/dist/atlas/oscillators/limit-witnesses.js.map +1 -0
  46. package/dist/atlas/oscillators/models.d.ts +2 -1
  47. package/dist/atlas/oscillators/models.d.ts.map +1 -1
  48. package/dist/atlas/oscillators/models.js +4 -3
  49. package/dist/atlas/oscillators/models.js.map +1 -1
  50. package/dist/atlas/oscillators/norm-transport-witness.d.ts +37 -0
  51. package/dist/atlas/oscillators/norm-transport-witness.d.ts.map +1 -0
  52. package/dist/atlas/oscillators/norm-transport-witness.js +58 -0
  53. package/dist/atlas/oscillators/norm-transport-witness.js.map +1 -0
  54. package/dist/atlas/oscillators/norm-transport.d.ts +44 -0
  55. package/dist/atlas/oscillators/norm-transport.d.ts.map +1 -0
  56. package/dist/atlas/oscillators/norm-transport.js +66 -0
  57. package/dist/atlas/oscillators/norm-transport.js.map +1 -0
  58. package/dist/atlas/oscillators/norms.d.ts +11 -0
  59. package/dist/atlas/oscillators/norms.d.ts.map +1 -0
  60. package/dist/atlas/oscillators/norms.js +11 -0
  61. package/dist/atlas/oscillators/norms.js.map +1 -0
  62. package/dist/atlas/oscillators/pendulum-motion.d.ts +38 -0
  63. package/dist/atlas/oscillators/pendulum-motion.d.ts.map +1 -0
  64. package/dist/atlas/oscillators/pendulum-motion.js +122 -0
  65. package/dist/atlas/oscillators/pendulum-motion.js.map +1 -0
  66. package/dist/atlas/oscillators/phase-carriage.d.ts +38 -0
  67. package/dist/atlas/oscillators/phase-carriage.d.ts.map +1 -0
  68. package/dist/atlas/oscillators/phase-carriage.js +96 -0
  69. package/dist/atlas/oscillators/phase-carriage.js.map +1 -0
  70. package/dist/atlas/oscillators/phase-translation.d.ts +46 -0
  71. package/dist/atlas/oscillators/phase-translation.d.ts.map +1 -0
  72. package/dist/atlas/oscillators/phase-translation.js +178 -0
  73. package/dist/atlas/oscillators/phase-translation.js.map +1 -0
  74. package/dist/atlas/oscillators/position-translation.d.ts +59 -0
  75. package/dist/atlas/oscillators/position-translation.d.ts.map +1 -0
  76. package/dist/atlas/oscillators/position-translation.js +254 -0
  77. package/dist/atlas/oscillators/position-translation.js.map +1 -0
  78. package/dist/atlas/path-bound.d.ts +126 -9
  79. package/dist/atlas/path-bound.d.ts.map +1 -1
  80. package/dist/atlas/path-bound.js +372 -23
  81. package/dist/atlas/path-bound.js.map +1 -1
  82. package/dist/atlas/physjs-ref.d.ts +141 -0
  83. package/dist/atlas/physjs-ref.d.ts.map +1 -0
  84. package/dist/atlas/physjs-ref.js +878 -0
  85. package/dist/atlas/physjs-ref.js.map +1 -0
  86. package/dist/atlas/proof-target.d.ts +32 -0
  87. package/dist/atlas/proof-target.d.ts.map +1 -0
  88. package/dist/atlas/proof-target.js +140 -0
  89. package/dist/atlas/proof-target.js.map +1 -0
  90. package/dist/atlas/regime.d.ts +12 -136
  91. package/dist/atlas/regime.d.ts.map +1 -1
  92. package/dist/atlas/regime.js +8 -254
  93. package/dist/atlas/regime.js.map +1 -1
  94. package/dist/atlas/serialize.d.ts.map +1 -1
  95. package/dist/atlas/serialize.js +25 -0
  96. package/dist/atlas/serialize.js.map +1 -1
  97. package/dist/atlas/translation-registry.d.ts +22 -0
  98. package/dist/atlas/translation-registry.d.ts.map +1 -0
  99. package/dist/atlas/translation-registry.js +31 -0
  100. package/dist/atlas/translation-registry.js.map +1 -0
  101. package/dist/atlas/translation.d.ts +116 -0
  102. package/dist/atlas/translation.d.ts.map +1 -0
  103. package/dist/atlas/translation.js +34 -0
  104. package/dist/atlas/translation.js.map +1 -0
  105. package/dist/atlas/types.d.ts +74 -240
  106. package/dist/atlas/types.d.ts.map +1 -1
  107. package/dist/atlas/types.js +5 -37
  108. package/dist/atlas/types.js.map +1 -1
  109. package/dist/atlas/waves/bridges-closure.d.ts.map +1 -1
  110. package/dist/atlas/waves/bridges-closure.js +4 -0
  111. package/dist/atlas/waves/bridges-closure.js.map +1 -1
  112. package/dist/atlas/waves/bridges.d.ts.map +1 -1
  113. package/dist/atlas/waves/bridges.js +3 -0
  114. package/dist/atlas/waves/bridges.js.map +1 -1
  115. package/dist/atlas/waves/models.js +1 -1
  116. package/dist/atlas/waves/models.js.map +1 -1
  117. package/dist/atlas/witness-dominance.d.ts +50 -0
  118. package/dist/atlas/witness-dominance.d.ts.map +1 -0
  119. package/dist/atlas/witness-dominance.js +86 -0
  120. package/dist/atlas/witness-dominance.js.map +1 -0
  121. package/dist/atlas/witness-specs.d.ts +30 -0
  122. package/dist/atlas/witness-specs.d.ts.map +1 -1
  123. package/dist/atlas/witness-specs.js +107 -0
  124. package/dist/atlas/witness-specs.js.map +1 -1
  125. package/dist/bridges/be53-yang-mills-confrontation.d.ts +81 -0
  126. package/dist/bridges/be53-yang-mills-confrontation.d.ts.map +1 -0
  127. package/dist/bridges/be53-yang-mills-confrontation.js +110 -0
  128. package/dist/bridges/be53-yang-mills-confrontation.js.map +1 -0
  129. package/dist/bridges/be63-chandrasekhar-mass.d.ts.map +1 -1
  130. package/dist/bridges/be63-chandrasekhar-mass.js +1 -3
  131. package/dist/bridges/be63-chandrasekhar-mass.js.map +1 -1
  132. package/dist/bridges/confrontation-coverage.d.ts +5 -61
  133. package/dist/bridges/confrontation-coverage.d.ts.map +1 -1
  134. package/dist/bridges/confrontation-coverage.js +5 -70
  135. package/dist/bridges/confrontation-coverage.js.map +1 -1
  136. package/dist/bridges/confrontations.d.ts +1 -1
  137. package/dist/bridges/confrontations.d.ts.map +1 -1
  138. package/dist/bridges/confrontations.js +291 -3
  139. package/dist/bridges/confrontations.js.map +1 -1
  140. package/dist/bridges/equations/be-13-einstein-trace.d.ts +2 -3
  141. package/dist/bridges/equations/be-13-einstein-trace.d.ts.map +1 -1
  142. package/dist/bridges/equations/be-13-einstein-trace.js +2 -3
  143. package/dist/bridges/equations/be-13-einstein-trace.js.map +1 -1
  144. package/dist/bridges/equations/be-38-mond.d.ts +7 -5
  145. package/dist/bridges/equations/be-38-mond.d.ts.map +1 -1
  146. package/dist/bridges/equations/be-38-mond.js +7 -5
  147. package/dist/bridges/equations/be-38-mond.js.map +1 -1
  148. package/dist/bridges/equations/be-42-hawking-temperature.d.ts +3 -0
  149. package/dist/bridges/equations/be-42-hawking-temperature.d.ts.map +1 -1
  150. package/dist/bridges/equations/be-42-hawking-temperature.js +8 -1
  151. package/dist/bridges/equations/be-42-hawking-temperature.js.map +1 -1
  152. package/dist/bridges/equations/be-53-yang-mills-beta.d.ts +21 -0
  153. package/dist/bridges/equations/be-53-yang-mills-beta.d.ts.map +1 -1
  154. package/dist/bridges/equations/be-53-yang-mills-beta.js +12 -0
  155. package/dist/bridges/equations/be-53-yang-mills-beta.js.map +1 -1
  156. package/dist/bridges/evaluator-inputs.d.ts +21 -0
  157. package/dist/bridges/evaluator-inputs.d.ts.map +1 -0
  158. package/dist/bridges/evaluator-inputs.js +75 -0
  159. package/dist/bridges/evaluator-inputs.js.map +1 -0
  160. package/dist/bridges/evaluators.d.ts +46 -0
  161. package/dist/bridges/evaluators.d.ts.map +1 -1
  162. package/dist/bridges/evaluators.js +57 -16
  163. package/dist/bridges/evaluators.js.map +1 -1
  164. package/dist/bridges/gravitational-lensing.d.ts +1 -1
  165. package/dist/bridges/gravitational-lensing.js +1 -1
  166. package/dist/bridges/index.d.ts +3 -2
  167. package/dist/bridges/index.d.ts.map +1 -1
  168. package/dist/bridges/index.js +15 -14
  169. package/dist/bridges/index.js.map +1 -1
  170. package/dist/bridges/observations/types.d.ts +110 -2
  171. package/dist/bridges/observations/types.d.ts.map +1 -1
  172. package/dist/bridges/observations/types.js +29 -0
  173. package/dist/bridges/observations/types.js.map +1 -1
  174. package/dist/bridges/perihelion-precession.d.ts +1 -1
  175. package/dist/bridges/perihelion-precession.js +1 -1
  176. package/dist/bridges/tensor-index.d.ts +39 -0
  177. package/dist/bridges/tensor-index.d.ts.map +1 -0
  178. package/dist/bridges/tensor-index.js +66 -0
  179. package/dist/bridges/tensor-index.js.map +1 -0
  180. package/dist/canonical/canonical-equation.d.ts +1 -1
  181. package/dist/canonical/canonical-equation.d.ts.map +1 -1
  182. package/dist/canonical/entries/atomic.d.ts.map +1 -1
  183. package/dist/canonical/entries/atomic.js +32 -10
  184. package/dist/canonical/entries/atomic.js.map +1 -1
  185. package/dist/canonical/entries/dimensional-classics.js +2 -2
  186. package/dist/canonical/entries/dimensional-classics.js.map +1 -1
  187. package/dist/canonical/entries/mechanics.d.ts.map +1 -1
  188. package/dist/canonical/entries/mechanics.js +10 -6
  189. package/dist/canonical/entries/mechanics.js.map +1 -1
  190. package/dist/canonical/entries/relativity.d.ts.map +1 -1
  191. package/dist/canonical/entries/relativity.js +41 -8
  192. package/dist/canonical/entries/relativity.js.map +1 -1
  193. package/dist/canonical/entries/thermo-nuclear-cosmo.js +5 -5
  194. package/dist/canonical/entries/thermo-nuclear-cosmo.js.map +1 -1
  195. package/dist/canonical/linkage.d.ts.map +1 -1
  196. package/dist/canonical/linkage.js +31 -28
  197. package/dist/canonical/linkage.js.map +1 -1
  198. package/dist/canonical/normal-form.d.ts +17 -0
  199. package/dist/canonical/normal-form.d.ts.map +1 -1
  200. package/dist/canonical/normal-form.js +21 -4
  201. package/dist/canonical/normal-form.js.map +1 -1
  202. package/dist/canonical/structural.d.ts +84 -0
  203. package/dist/canonical/structural.d.ts.map +1 -0
  204. package/dist/canonical/structural.js +107 -0
  205. package/dist/canonical/structural.js.map +1 -0
  206. package/dist/cases/brownian-sphere.d.ts +68 -0
  207. package/dist/cases/brownian-sphere.d.ts.map +1 -0
  208. package/dist/cases/brownian-sphere.js +353 -0
  209. package/dist/cases/brownian-sphere.js.map +1 -0
  210. package/dist/cases/damped-resonator.d.ts +23 -0
  211. package/dist/cases/damped-resonator.d.ts.map +1 -0
  212. package/dist/cases/damped-resonator.js +224 -0
  213. package/dist/cases/damped-resonator.js.map +1 -0
  214. package/dist/cases/index.d.ts +11 -0
  215. package/dist/cases/index.d.ts.map +1 -0
  216. package/dist/cases/index.js +29 -0
  217. package/dist/cases/index.js.map +1 -0
  218. package/dist/cases/kepler-rv.d.ts +13 -0
  219. package/dist/cases/kepler-rv.d.ts.map +1 -0
  220. package/dist/cases/kepler-rv.js +168 -0
  221. package/dist/cases/kepler-rv.js.map +1 -0
  222. package/dist/cases/lumped-cooling.d.ts +46 -0
  223. package/dist/cases/lumped-cooling.d.ts.map +1 -0
  224. package/dist/cases/lumped-cooling.js +261 -0
  225. package/dist/cases/lumped-cooling.js.map +1 -0
  226. package/dist/cases/quadrature.d.ts +14 -0
  227. package/dist/cases/quadrature.d.ts.map +1 -0
  228. package/dist/cases/quadrature.js +41 -0
  229. package/dist/cases/quadrature.js.map +1 -0
  230. package/dist/cases/resistor-noise.d.ts +14 -0
  231. package/dist/cases/resistor-noise.d.ts.map +1 -0
  232. package/dist/cases/resistor-noise.js +210 -0
  233. package/dist/cases/resistor-noise.js.map +1 -0
  234. package/dist/cases/skin-depth.d.ts +21 -0
  235. package/dist/cases/skin-depth.d.ts.map +1 -0
  236. package/dist/cases/skin-depth.js +174 -0
  237. package/dist/cases/skin-depth.js.map +1 -0
  238. package/dist/cases/types.d.ts +115 -0
  239. package/dist/cases/types.d.ts.map +1 -0
  240. package/dist/cases/types.js +20 -0
  241. package/dist/cases/types.js.map +1 -0
  242. package/dist/cli/commands/_atlas-map.d.ts +409 -0
  243. package/dist/cli/commands/_atlas-map.d.ts.map +1 -0
  244. package/dist/cli/commands/_atlas-map.js +1014 -0
  245. package/dist/cli/commands/_atlas-map.js.map +1 -0
  246. package/dist/cli/commands/_atlas-route.d.ts +111 -0
  247. package/dist/cli/commands/_atlas-route.d.ts.map +1 -0
  248. package/dist/cli/commands/_atlas-route.js +140 -0
  249. package/dist/cli/commands/_atlas-route.js.map +1 -0
  250. package/dist/cli/commands/_discovery-opts.d.ts.map +1 -1
  251. package/dist/cli/commands/_discovery-opts.js +10 -1
  252. package/dist/cli/commands/_discovery-opts.js.map +1 -1
  253. package/dist/cli/commands/atlas.d.ts +20 -0
  254. package/dist/cli/commands/atlas.d.ts.map +1 -1
  255. package/dist/cli/commands/atlas.js +333 -19
  256. package/dist/cli/commands/atlas.js.map +1 -1
  257. package/dist/cli/commands/audit.d.ts.map +1 -1
  258. package/dist/cli/commands/audit.js +12 -3
  259. package/dist/cli/commands/audit.js.map +1 -1
  260. package/dist/cli/commands/axes.js +4 -4
  261. package/dist/cli/commands/candidates.d.ts.map +1 -1
  262. package/dist/cli/commands/candidates.js +3 -2
  263. package/dist/cli/commands/candidates.js.map +1 -1
  264. package/dist/cli/commands/canonical.d.ts.map +1 -1
  265. package/dist/cli/commands/canonical.js +39 -10
  266. package/dist/cli/commands/canonical.js.map +1 -1
  267. package/dist/cli/commands/confront.d.ts.map +1 -1
  268. package/dist/cli/commands/confront.js +210 -28
  269. package/dist/cli/commands/confront.js.map +1 -1
  270. package/dist/cli/commands/connectors.d.ts.map +1 -1
  271. package/dist/cli/commands/connectors.js +5 -4
  272. package/dist/cli/commands/connectors.js.map +1 -1
  273. package/dist/cli/commands/coverage.js +3 -3
  274. package/dist/cli/commands/derive.d.ts.map +1 -1
  275. package/dist/cli/commands/derive.js +22 -9
  276. package/dist/cli/commands/derive.js.map +1 -1
  277. package/dist/cli/commands/discover.d.ts.map +1 -1
  278. package/dist/cli/commands/discover.js +149 -32
  279. package/dist/cli/commands/discover.js.map +1 -1
  280. package/dist/cli/commands/eval.d.ts.map +1 -1
  281. package/dist/cli/commands/eval.js +87 -17
  282. package/dist/cli/commands/eval.js.map +1 -1
  283. package/dist/cli/commands/evaluate.d.ts +30 -0
  284. package/dist/cli/commands/evaluate.d.ts.map +1 -1
  285. package/dist/cli/commands/evaluate.js +441 -25
  286. package/dist/cli/commands/evaluate.js.map +1 -1
  287. package/dist/cli/commands/explain.d.ts.map +1 -1
  288. package/dist/cli/commands/explain.js +38 -11
  289. package/dist/cli/commands/explain.js.map +1 -1
  290. package/dist/cli/commands/frontier.d.ts +3 -0
  291. package/dist/cli/commands/frontier.d.ts.map +1 -0
  292. package/dist/cli/commands/frontier.js +28 -0
  293. package/dist/cli/commands/frontier.js.map +1 -0
  294. package/dist/cli/commands/ground.d.ts.map +1 -1
  295. package/dist/cli/commands/ground.js +37 -12
  296. package/dist/cli/commands/ground.js.map +1 -1
  297. package/dist/cli/commands/index.d.ts +5 -0
  298. package/dist/cli/commands/index.d.ts.map +1 -1
  299. package/dist/cli/commands/index.js +5 -0
  300. package/dist/cli/commands/index.js.map +1 -1
  301. package/dist/cli/commands/map.d.ts +8 -0
  302. package/dist/cli/commands/map.d.ts.map +1 -1
  303. package/dist/cli/commands/map.js +385 -57
  304. package/dist/cli/commands/map.js.map +1 -1
  305. package/dist/cli/commands/metric.d.ts +3 -0
  306. package/dist/cli/commands/metric.d.ts.map +1 -0
  307. package/dist/cli/commands/metric.js +136 -0
  308. package/dist/cli/commands/metric.js.map +1 -0
  309. package/dist/cli/commands/path.d.ts +109 -1
  310. package/dist/cli/commands/path.d.ts.map +1 -1
  311. package/dist/cli/commands/path.js +1019 -109
  312. package/dist/cli/commands/path.js.map +1 -1
  313. package/dist/cli/commands/predict.d.ts.map +1 -1
  314. package/dist/cli/commands/predict.js +3 -2
  315. package/dist/cli/commands/predict.js.map +1 -1
  316. package/dist/cli/commands/priority.d.ts.map +1 -1
  317. package/dist/cli/commands/priority.js +3 -2
  318. package/dist/cli/commands/priority.js.map +1 -1
  319. package/dist/cli/commands/probe.d.ts +7 -0
  320. package/dist/cli/commands/probe.d.ts.map +1 -1
  321. package/dist/cli/commands/probe.js +234 -52
  322. package/dist/cli/commands/probe.js.map +1 -1
  323. package/dist/cli/commands/recover.d.ts.map +1 -1
  324. package/dist/cli/commands/recover.js +17 -6
  325. package/dist/cli/commands/recover.js.map +1 -1
  326. package/dist/cli/commands/regime.d.ts +1 -1
  327. package/dist/cli/commands/regime.d.ts.map +1 -1
  328. package/dist/cli/commands/regime.js +134 -33
  329. package/dist/cli/commands/regime.js.map +1 -1
  330. package/dist/cli/commands/retrieve.d.ts +3 -0
  331. package/dist/cli/commands/retrieve.d.ts.map +1 -0
  332. package/dist/cli/commands/retrieve.js +65 -0
  333. package/dist/cli/commands/retrieve.js.map +1 -0
  334. package/dist/cli/commands/search.d.ts +3 -0
  335. package/dist/cli/commands/search.d.ts.map +1 -0
  336. package/dist/cli/commands/search.js +70 -0
  337. package/dist/cli/commands/search.js.map +1 -0
  338. package/dist/cli/commands/symbolic.d.ts.map +1 -1
  339. package/dist/cli/commands/symbolic.js +107 -16
  340. package/dist/cli/commands/symbolic.js.map +1 -1
  341. package/dist/cli/commands/testplan.d.ts +3 -0
  342. package/dist/cli/commands/testplan.d.ts.map +1 -0
  343. package/dist/cli/commands/testplan.js +131 -0
  344. package/dist/cli/commands/testplan.js.map +1 -0
  345. package/dist/cli/conventions.d.ts +22 -0
  346. package/dist/cli/conventions.d.ts.map +1 -0
  347. package/dist/cli/conventions.js +48 -0
  348. package/dist/cli/conventions.js.map +1 -0
  349. package/dist/cli/euler-guard.d.ts +9 -0
  350. package/dist/cli/euler-guard.d.ts.map +1 -0
  351. package/dist/cli/euler-guard.js +11 -0
  352. package/dist/cli/euler-guard.js.map +1 -0
  353. package/dist/cli/eval-numbers.d.ts +26 -0
  354. package/dist/cli/eval-numbers.d.ts.map +1 -0
  355. package/dist/cli/eval-numbers.js +48 -0
  356. package/dist/cli/eval-numbers.js.map +1 -0
  357. package/dist/cli/expr-print.d.ts +25 -0
  358. package/dist/cli/expr-print.d.ts.map +1 -0
  359. package/dist/cli/expr-print.js +123 -0
  360. package/dist/cli/expr-print.js.map +1 -0
  361. package/dist/cli/graphs.d.ts +26 -0
  362. package/dist/cli/graphs.d.ts.map +1 -1
  363. package/dist/cli/graphs.js +19 -0
  364. package/dist/cli/graphs.js.map +1 -1
  365. package/dist/cli/main.d.ts +4 -10
  366. package/dist/cli/main.d.ts.map +1 -1
  367. package/dist/cli/main.js +370 -167
  368. package/dist/cli/main.js.map +1 -1
  369. package/dist/cli/map-evidence.d.ts +47 -0
  370. package/dist/cli/map-evidence.d.ts.map +1 -0
  371. package/dist/cli/map-evidence.js +53 -0
  372. package/dist/cli/map-evidence.js.map +1 -0
  373. package/dist/cli/output.d.ts +21 -1
  374. package/dist/cli/output.d.ts.map +1 -1
  375. package/dist/cli/output.js +5 -1
  376. package/dist/cli/output.js.map +1 -1
  377. package/dist/{composition → cli}/poster-source.d.ts +3 -2
  378. package/dist/cli/poster-source.d.ts.map +1 -0
  379. package/dist/{composition → cli}/poster-source.js +2 -1
  380. package/dist/cli/poster-source.js.map +1 -0
  381. package/dist/cli/record-reach.d.ts +44 -0
  382. package/dist/cli/record-reach.d.ts.map +1 -0
  383. package/dist/cli/record-reach.js +228 -0
  384. package/dist/cli/record-reach.js.map +1 -0
  385. package/dist/cli/record-tables.d.ts +38 -0
  386. package/dist/cli/record-tables.d.ts.map +1 -0
  387. package/dist/cli/record-tables.js +110 -0
  388. package/dist/cli/record-tables.js.map +1 -0
  389. package/dist/cli/record.d.ts +141 -0
  390. package/dist/cli/record.d.ts.map +1 -0
  391. package/dist/cli/record.js +612 -0
  392. package/dist/cli/record.js.map +1 -0
  393. package/dist/cli/search-index.d.ts +56 -0
  394. package/dist/cli/search-index.d.ts.map +1 -0
  395. package/dist/cli/search-index.js +190 -0
  396. package/dist/cli/search-index.js.map +1 -0
  397. package/dist/cli/statuses.d.ts +25 -0
  398. package/dist/cli/statuses.d.ts.map +1 -0
  399. package/dist/cli/statuses.js +276 -0
  400. package/dist/cli/statuses.js.map +1 -0
  401. package/dist/cli/version.d.ts +8 -0
  402. package/dist/cli/version.d.ts.map +1 -1
  403. package/dist/cli/version.js +32 -1
  404. package/dist/cli/version.js.map +1 -1
  405. package/dist/cli-api.d.ts +34 -16
  406. package/dist/cli-api.d.ts.map +1 -1
  407. package/dist/cli-api.js +32 -12
  408. package/dist/cli-api.js.map +1 -1
  409. package/dist/composition/audit-coverage.d.ts +70 -0
  410. package/dist/composition/audit-coverage.d.ts.map +1 -0
  411. package/dist/composition/audit-coverage.js +80 -0
  412. package/dist/composition/audit-coverage.js.map +1 -0
  413. package/dist/composition/bridge-analysis.d.ts.map +1 -1
  414. package/dist/composition/bridge-analysis.js +2 -1
  415. package/dist/composition/bridge-analysis.js.map +1 -1
  416. package/dist/composition/buckingham-filter.d.ts +34 -0
  417. package/dist/composition/buckingham-filter.d.ts.map +1 -0
  418. package/dist/composition/buckingham-filter.js +64 -0
  419. package/dist/composition/buckingham-filter.js.map +1 -0
  420. package/dist/composition/canonical-compare.d.ts +29 -16
  421. package/dist/composition/canonical-compare.d.ts.map +1 -1
  422. package/dist/composition/canonical-compare.js +355 -59
  423. package/dist/composition/canonical-compare.js.map +1 -1
  424. package/dist/composition/canonical-graph.d.ts +2 -2
  425. package/dist/composition/canonical-graph.js +3 -3
  426. package/dist/composition/canonical-graph.js.map +1 -1
  427. package/dist/composition/canonical-prefactors.d.ts +24 -1
  428. package/dist/composition/canonical-prefactors.d.ts.map +1 -1
  429. package/dist/composition/canonical-prefactors.js +97 -1
  430. package/dist/composition/canonical-prefactors.js.map +1 -1
  431. package/dist/composition/catalog-graph.d.ts +4 -4
  432. package/dist/composition/catalog-graph.d.ts.map +1 -1
  433. package/dist/composition/catalog-graph.js +6 -4
  434. package/dist/composition/catalog-graph.js.map +1 -1
  435. package/dist/composition/chain-candidate.d.ts +43 -0
  436. package/dist/composition/chain-candidate.d.ts.map +1 -0
  437. package/dist/composition/chain-candidate.js +56 -0
  438. package/dist/composition/chain-candidate.js.map +1 -0
  439. package/dist/composition/chain-match.d.ts +19 -0
  440. package/dist/composition/chain-match.d.ts.map +1 -0
  441. package/dist/composition/chain-match.js +10 -0
  442. package/dist/composition/chain-match.js.map +1 -0
  443. package/dist/composition/chain-regime.d.ts +34 -0
  444. package/dist/composition/chain-regime.d.ts.map +1 -0
  445. package/dist/composition/chain-regime.js +89 -0
  446. package/dist/composition/chain-regime.js.map +1 -0
  447. package/dist/composition/compose-symbolic.js +1 -1
  448. package/dist/composition/compose-symbolic.js.map +1 -1
  449. package/dist/composition/compose.d.ts +11 -0
  450. package/dist/composition/compose.d.ts.map +1 -1
  451. package/dist/composition/compose.js +30 -10
  452. package/dist/composition/compose.js.map +1 -1
  453. package/dist/composition/composition-recovery.d.ts +47 -0
  454. package/dist/composition/composition-recovery.d.ts.map +1 -0
  455. package/dist/composition/composition-recovery.js +77 -0
  456. package/dist/composition/composition-recovery.js.map +1 -0
  457. package/dist/composition/consequence.d.ts +39 -0
  458. package/dist/composition/consequence.d.ts.map +1 -1
  459. package/dist/composition/consequence.js +37 -0
  460. package/dist/composition/consequence.js.map +1 -1
  461. package/dist/{bridges → composition}/descriptor.d.ts +6 -3
  462. package/dist/composition/descriptor.d.ts.map +1 -0
  463. package/dist/{bridges → composition}/descriptor.js +7 -4
  464. package/dist/composition/descriptor.js.map +1 -0
  465. package/dist/composition/discovery.d.ts +20 -0
  466. package/dist/composition/discovery.d.ts.map +1 -1
  467. package/dist/composition/discovery.js +27 -5
  468. package/dist/composition/discovery.js.map +1 -1
  469. package/dist/composition/edge.d.ts +1 -1
  470. package/dist/composition/edge.d.ts.map +1 -1
  471. package/dist/composition/edge.js.map +1 -1
  472. package/dist/composition/edges/_catalog-helpers.d.ts +4 -0
  473. package/dist/composition/edges/_catalog-helpers.d.ts.map +1 -1
  474. package/dist/composition/edges/_catalog-helpers.js +15 -0
  475. package/dist/composition/edges/_catalog-helpers.js.map +1 -1
  476. package/dist/composition/edges/calibration.d.ts +0 -9
  477. package/dist/composition/edges/calibration.d.ts.map +1 -1
  478. package/dist/composition/edges/calibration.js +85 -0
  479. package/dist/composition/edges/calibration.js.map +1 -1
  480. package/dist/composition/edges/catalog-condensed-matter.d.ts +0 -9
  481. package/dist/composition/edges/catalog-condensed-matter.d.ts.map +1 -1
  482. package/dist/composition/edges/catalog-condensed-matter.js +32 -1
  483. package/dist/composition/edges/catalog-condensed-matter.js.map +1 -1
  484. package/dist/composition/edges/catalog-fields.d.ts.map +1 -1
  485. package/dist/composition/edges/catalog-fields.js +2 -1
  486. package/dist/composition/edges/catalog-fields.js.map +1 -1
  487. package/dist/composition/edges/catalog-full.d.ts +4 -2
  488. package/dist/composition/edges/catalog-full.d.ts.map +1 -1
  489. package/dist/composition/edges/catalog-full.js +4 -2
  490. package/dist/composition/edges/catalog-full.js.map +1 -1
  491. package/dist/composition/edges/catalog-gravitation-cosmology.d.ts +0 -10
  492. package/dist/composition/edges/catalog-gravitation-cosmology.d.ts.map +1 -1
  493. package/dist/composition/edges/catalog-gravitation-cosmology.js +31 -1
  494. package/dist/composition/edges/catalog-gravitation-cosmology.js.map +1 -1
  495. package/dist/composition/edges/catalog-quantum.d.ts +0 -10
  496. package/dist/composition/edges/catalog-quantum.d.ts.map +1 -1
  497. package/dist/composition/edges/catalog-quantum.js +53 -17
  498. package/dist/composition/edges/catalog-quantum.js.map +1 -1
  499. package/dist/composition/edges/catalog-tranche.d.ts +0 -15
  500. package/dist/composition/edges/catalog-tranche.d.ts.map +1 -1
  501. package/dist/composition/edges/catalog-tranche.js +70 -16
  502. package/dist/composition/edges/catalog-tranche.js.map +1 -1
  503. package/dist/composition/edges/proved-seeds.d.ts +39 -0
  504. package/dist/composition/edges/proved-seeds.d.ts.map +1 -0
  505. package/dist/composition/edges/proved-seeds.js +197 -0
  506. package/dist/composition/edges/proved-seeds.js.map +1 -0
  507. package/dist/composition/enumerate.d.ts +20 -0
  508. package/dist/composition/enumerate.d.ts.map +1 -1
  509. package/dist/composition/enumerate.js +23 -0
  510. package/dist/composition/enumerate.js.map +1 -1
  511. package/dist/composition/expr-eval.d.ts +5 -4
  512. package/dist/composition/expr-eval.d.ts.map +1 -1
  513. package/dist/composition/expr-eval.js +40 -6
  514. package/dist/composition/expr-eval.js.map +1 -1
  515. package/dist/composition/expr-simplify.js +1 -1
  516. package/dist/composition/expr-simplify.js.map +1 -1
  517. package/dist/composition/expr-subst.d.ts +2 -1
  518. package/dist/composition/expr-subst.d.ts.map +1 -1
  519. package/dist/composition/expr-subst.js +9 -2
  520. package/dist/composition/expr-subst.js.map +1 -1
  521. package/dist/composition/frontier-account.d.ts +78 -0
  522. package/dist/composition/frontier-account.d.ts.map +1 -0
  523. package/dist/composition/frontier-account.js +127 -0
  524. package/dist/composition/frontier-account.js.map +1 -0
  525. package/dist/composition/graph-viz.d.ts +7 -22
  526. package/dist/composition/graph-viz.d.ts.map +1 -1
  527. package/dist/composition/graph-viz.js +4 -27
  528. package/dist/composition/graph-viz.js.map +1 -1
  529. package/dist/composition/grounding.d.ts +49 -0
  530. package/dist/composition/grounding.d.ts.map +1 -1
  531. package/dist/composition/grounding.js +38 -0
  532. package/dist/composition/grounding.js.map +1 -1
  533. package/dist/composition/index.d.ts +0 -2
  534. package/dist/composition/index.d.ts.map +1 -1
  535. package/dist/composition/index.js +1 -1
  536. package/dist/composition/index.js.map +1 -1
  537. package/dist/composition/not-composable-seeds.d.ts +30 -0
  538. package/dist/composition/not-composable-seeds.d.ts.map +1 -0
  539. package/dist/composition/not-composable-seeds.js +90 -0
  540. package/dist/composition/not-composable-seeds.js.map +1 -0
  541. package/dist/composition/probe/expression-gaps.d.ts +27 -0
  542. package/dist/composition/probe/expression-gaps.d.ts.map +1 -0
  543. package/dist/composition/probe/expression-gaps.js +47 -0
  544. package/dist/composition/probe/expression-gaps.js.map +1 -0
  545. package/dist/composition/probe/frontier.d.ts +2 -1
  546. package/dist/composition/probe/frontier.d.ts.map +1 -1
  547. package/dist/composition/probe/frontier.js +3 -2
  548. package/dist/composition/probe/frontier.js.map +1 -1
  549. package/dist/composition/probe/index.d.ts +4 -1
  550. package/dist/composition/probe/index.d.ts.map +1 -1
  551. package/dist/composition/probe/index.js +3 -1
  552. package/dist/composition/probe/index.js.map +1 -1
  553. package/dist/composition/probe/pipeline.d.ts +2 -0
  554. package/dist/composition/probe/pipeline.d.ts.map +1 -1
  555. package/dist/composition/probe/pipeline.js +6 -2
  556. package/dist/composition/probe/pipeline.js.map +1 -1
  557. package/dist/composition/probe/problem.d.ts +7 -0
  558. package/dist/composition/probe/problem.d.ts.map +1 -1
  559. package/dist/composition/probe/problem.js +15 -5
  560. package/dist/composition/probe/problem.js.map +1 -1
  561. package/dist/composition/probe/search-budget.d.ts.map +1 -1
  562. package/dist/composition/probe/search-budget.js +6 -2
  563. package/dist/composition/probe/search-budget.js.map +1 -1
  564. package/dist/composition/probe/study.d.ts +340 -0
  565. package/dist/composition/probe/study.d.ts.map +1 -0
  566. package/dist/composition/probe/study.js +1519 -0
  567. package/dist/composition/probe/study.js.map +1 -0
  568. package/dist/composition/proposed-bridges.js +1 -1
  569. package/dist/composition/proposed-bridges.js.map +1 -1
  570. package/dist/composition/quantities/_dims.d.ts +6 -0
  571. package/dist/composition/quantities/_dims.d.ts.map +1 -1
  572. package/dist/composition/quantities/_dims.js +6 -0
  573. package/dist/composition/quantities/_dims.js.map +1 -1
  574. package/dist/composition/quantities/condensed-matter.d.ts +16 -0
  575. package/dist/composition/quantities/condensed-matter.d.ts.map +1 -1
  576. package/dist/composition/quantities/condensed-matter.js +43 -2
  577. package/dist/composition/quantities/condensed-matter.js.map +1 -1
  578. package/dist/composition/quantities/fields.d.ts +10 -0
  579. package/dist/composition/quantities/fields.d.ts.map +1 -1
  580. package/dist/composition/quantities/fields.js +37 -2
  581. package/dist/composition/quantities/fields.js.map +1 -1
  582. package/dist/composition/quantities/gravitation-cosmology.d.ts +4 -0
  583. package/dist/composition/quantities/gravitation-cosmology.d.ts.map +1 -1
  584. package/dist/composition/quantities/gravitation-cosmology.js +14 -0
  585. package/dist/composition/quantities/gravitation-cosmology.js.map +1 -1
  586. package/dist/composition/quantities/quantum.d.ts +2 -0
  587. package/dist/composition/quantities/quantum.d.ts.map +1 -1
  588. package/dist/composition/quantities/quantum.js +7 -0
  589. package/dist/composition/quantities/quantum.js.map +1 -1
  590. package/dist/composition/quantities.d.ts +4 -16
  591. package/dist/composition/quantities.d.ts.map +1 -1
  592. package/dist/composition/quantities.js +4 -16
  593. package/dist/composition/quantities.js.map +1 -1
  594. package/dist/composition/retrodiction.d.ts.map +1 -1
  595. package/dist/composition/retrodiction.js +6 -2
  596. package/dist/composition/retrodiction.js.map +1 -1
  597. package/dist/composition/uncertainty.d.ts +1 -1
  598. package/dist/composition/uncertainty.d.ts.map +1 -1
  599. package/dist/composition/user-equation.d.ts +79 -8
  600. package/dist/composition/user-equation.d.ts.map +1 -1
  601. package/dist/composition/user-equation.js +224 -32
  602. package/dist/composition/user-equation.js.map +1 -1
  603. package/dist/core/constants.d.ts +17 -5
  604. package/dist/core/constants.d.ts.map +1 -1
  605. package/dist/core/constants.js +17 -5
  606. package/dist/core/constants.js.map +1 -1
  607. package/dist/core/types.d.ts +1 -1
  608. package/dist/core/types.d.ts.map +1 -1
  609. package/dist/core/types.js +2 -1
  610. package/dist/core/types.js.map +1 -1
  611. package/dist/dimensional/curvature.d.ts +7 -56
  612. package/dist/dimensional/curvature.d.ts.map +1 -1
  613. package/dist/dimensional/curvature.js +7 -76
  614. package/dist/dimensional/curvature.js.map +1 -1
  615. package/dist/dimensional/dimension-spec.d.ts +7 -1
  616. package/dist/dimensional/dimension-spec.d.ts.map +1 -1
  617. package/dist/dimensional/dimension-spec.js +131 -3
  618. package/dist/dimensional/dimension-spec.js.map +1 -1
  619. package/dist/dimensional/formula-names.d.ts +30 -0
  620. package/dist/dimensional/formula-names.d.ts.map +1 -0
  621. package/dist/dimensional/formula-names.js +37 -0
  622. package/dist/dimensional/formula-names.js.map +1 -0
  623. package/dist/dimensional/natural-units.d.ts +33 -0
  624. package/dist/dimensional/natural-units.d.ts.map +1 -0
  625. package/dist/dimensional/natural-units.js +58 -0
  626. package/dist/dimensional/natural-units.js.map +1 -0
  627. package/dist/{composition → dimensional}/symbolic-constants.d.ts +21 -2
  628. package/dist/dimensional/symbolic-constants.d.ts.map +1 -0
  629. package/dist/dimensional/symbolic-constants.js +122 -0
  630. package/dist/dimensional/symbolic-constants.js.map +1 -0
  631. package/dist/dimensional/unit-convention.d.ts +30 -0
  632. package/dist/dimensional/unit-convention.d.ts.map +1 -0
  633. package/dist/dimensional/unit-convention.js +60 -0
  634. package/dist/dimensional/unit-convention.js.map +1 -0
  635. package/dist/dimensional/units.d.ts +43 -0
  636. package/dist/dimensional/units.d.ts.map +1 -0
  637. package/dist/dimensional/units.js +197 -0
  638. package/dist/dimensional/units.js.map +1 -0
  639. package/dist/index.d.ts +11 -5
  640. package/dist/index.d.ts.map +1 -1
  641. package/dist/index.js +10 -3
  642. package/dist/index.js.map +1 -1
  643. package/dist/numerical/bianchi-residual.d.ts +55 -0
  644. package/dist/numerical/bianchi-residual.d.ts.map +1 -0
  645. package/dist/numerical/bianchi-residual.js +79 -0
  646. package/dist/numerical/bianchi-residual.js.map +1 -0
  647. package/dist/numerical/binding-value.d.ts +67 -0
  648. package/dist/numerical/binding-value.d.ts.map +1 -0
  649. package/dist/numerical/binding-value.js +349 -0
  650. package/dist/numerical/binding-value.js.map +1 -0
  651. package/dist/numerical/connection-lowering-helpers.d.ts.map +1 -1
  652. package/dist/numerical/connection-lowering-helpers.js +13 -26
  653. package/dist/numerical/connection-lowering-helpers.js.map +1 -1
  654. package/dist/numerical/curvature-lowering-helpers.d.ts.map +1 -1
  655. package/dist/numerical/curvature-lowering-helpers.js +9 -0
  656. package/dist/numerical/curvature-lowering-helpers.js.map +1 -1
  657. package/dist/numerical/float64-engine.d.ts.map +1 -1
  658. package/dist/numerical/float64-engine.js +23 -16
  659. package/dist/numerical/float64-engine.js.map +1 -1
  660. package/dist/numerical/formula-dimension.d.ts +14 -0
  661. package/dist/numerical/formula-dimension.d.ts.map +1 -1
  662. package/dist/numerical/formula-dimension.js +56 -9
  663. package/dist/numerical/formula-dimension.js.map +1 -1
  664. package/dist/numerical/formula-mathts.d.ts.map +1 -1
  665. package/dist/numerical/formula-mathts.js +75 -6
  666. package/dist/numerical/formula-mathts.js.map +1 -1
  667. package/dist/numerical/formula.d.ts +33 -0
  668. package/dist/numerical/formula.d.ts.map +1 -1
  669. package/dist/numerical/formula.js +75 -6
  670. package/dist/numerical/formula.js.map +1 -1
  671. package/dist/numerical/spacetime-metrics.d.ts +243 -0
  672. package/dist/numerical/spacetime-metrics.d.ts.map +1 -0
  673. package/dist/numerical/spacetime-metrics.js +1056 -0
  674. package/dist/numerical/spacetime-metrics.js.map +1 -0
  675. package/dist/relations/category.d.ts +38 -0
  676. package/dist/relations/category.d.ts.map +1 -0
  677. package/dist/relations/category.js +23 -0
  678. package/dist/relations/category.js.map +1 -0
  679. package/dist/relations/composition-table.d.ts +61 -0
  680. package/dist/relations/composition-table.d.ts.map +1 -0
  681. package/dist/relations/composition-table.js +140 -0
  682. package/dist/relations/composition-table.js.map +1 -0
  683. package/dist/relations/conventions.d.ts +63 -0
  684. package/dist/relations/conventions.d.ts.map +1 -0
  685. package/dist/relations/conventions.js +93 -0
  686. package/dist/relations/conventions.js.map +1 -0
  687. package/dist/relations/regime.d.ts +159 -0
  688. package/dist/relations/regime.d.ts.map +1 -0
  689. package/dist/relations/regime.js +304 -0
  690. package/dist/relations/regime.js.map +1 -0
  691. package/dist/relations/types.d.ts +257 -0
  692. package/dist/relations/types.d.ts.map +1 -0
  693. package/dist/relations/types.js +35 -0
  694. package/dist/relations/types.js.map +1 -0
  695. package/package.json +166 -163
  696. package/dist/bridges/descriptor.d.ts.map +0 -1
  697. package/dist/bridges/descriptor.js.map +0 -1
  698. package/dist/composition/poster-source.d.ts.map +0 -1
  699. package/dist/composition/poster-source.js.map +0 -1
  700. package/dist/composition/symbolic-constants.d.ts.map +0 -1
  701. package/dist/composition/symbolic-constants.js +0 -65
  702. package/dist/composition/symbolic-constants.js.map +0 -1
@@ -0,0 +1,1519 @@
1
+ /**
2
+ * Falsification-oriented study over calibrated observations (audit §14 I19).
3
+ *
4
+ * A study file carries observations with units and σ, each with a role:
5
+ * `exploratory` rows are the only rows the search and every fit see;
6
+ * `holdout` rows are a withheld test (typically a change of regime);
7
+ * `replication` rows are an independent acquisition, tested but never fit.
8
+ * The file also declares its provenance (`synthetic` is required, never
9
+ * inferred) and optional competing baseline formulas.
10
+ *
11
+ * Every test is a χ² test with the declared σ at a stated α. A searched
12
+ * candidate is credible only if it passes on the exploratory rows AND a
13
+ * constant (null) model is rejected there; otherwise the result is
14
+ * `no-credible-candidate`. Only the selected credible candidate is then
15
+ * scored on the holdout: `survives-holdout` or `refuted-on-holdout`.
16
+ * Replication is reported separately and never folded into the verdict.
17
+ *
18
+ * A study is JSON or CSV (the CSV compiles to the same object, so both are
19
+ * refused for the same reasons). Inputs may carry σ, propagated by the
20
+ * effective-variance method. A file may declare correction families in one
21
+ * or more dimensionless inputs; their terms are admitted only by an F test on
22
+ * the exploratory rows. Replication rows may come from a separate file with
23
+ * its own provenance; replication rows at a study row's inputs that agree
24
+ * with it more closely than independent measurements would are reported as
25
+ * `too-close`, not as a replication.
26
+ *
27
+ * @internal
28
+ */
29
+ import { readFileSync } from 'node:fs';
30
+ import { DIMENSIONLESS } from '../../dimensional/types.js';
31
+ import { equals, format } from '../../dimensional/algebra.js';
32
+ import { convertValue, parseUnit, UnitError } from '../../dimensional/units.js';
33
+ import { parseFormula } from '../../numerical/formula.js';
34
+ import { builtinFormulaDimensionChecker } from '../../numerical/formula-dimension.js';
35
+ import { evalExpr } from '../expr-eval.js';
36
+ import { SCHEMA_VERSION } from './types.js';
37
+ import { datasetFromRows } from './dataset.js';
38
+ import { problemFromResidualGap } from './frontier.js';
39
+ import { isGapKind, makeResidualGap } from './problem.js';
40
+ import { NO_HOLDOUT_WORDING, runProbeSearch } from './pipeline.js';
41
+ import { bodyExpression } from './fingerprint.js';
42
+ import { canonicalJson, hashCanonical } from './serialize.js';
43
+ /** The study file was refused: bad units, missing σ, leakage, undeclared keys. @internal */
44
+ export class StudyRefusal extends Error {
45
+ constructor(message) {
46
+ super(message);
47
+ this.name = 'StudyRefusal';
48
+ }
49
+ }
50
+ const DEFAULT_ALPHA = 0.001;
51
+ const TOP_KEYS = new Set([
52
+ 'description', 'gap', 'provenance', 'target', 'governing', 'observations', 'baselines', 'criterion', 'design',
53
+ 'correction',
54
+ ]);
55
+ const ROW_KEYS = new Set(['id', 'role', 'source', 'values', 'observed', 'sigma', 'inputSigma', 'note']);
56
+ /** The keys a separate replication file may carry; the rest belong in the study file. */
57
+ const REPLICATION_KEYS = new Set(['description', 'provenance', 'target', 'governing', 'observations']);
58
+ const MAX_CORRECTION_TERMS = 6;
59
+ function refuse(where, why) {
60
+ throw new StudyRefusal(`study refused (${where}): ${why}`);
61
+ }
62
+ function isObject(v) {
63
+ return !!v && typeof v === 'object' && !Array.isArray(v);
64
+ }
65
+ function nonEmptyString(v) {
66
+ return typeof v === 'string' && v.trim().length > 0;
67
+ }
68
+ function quantity(raw, where) {
69
+ if (!isObject(raw) || !nonEmptyString(raw.name) || typeof raw.unit !== 'string') {
70
+ refuse(where, 'needs {"name", "unit"} ("unit": "" for a dimensionless quantity)');
71
+ }
72
+ let parsed;
73
+ try {
74
+ parsed = parseUnit(raw.unit);
75
+ }
76
+ catch (e) {
77
+ refuse(where, e.message);
78
+ }
79
+ if (parsed.affine !== undefined)
80
+ refuse(where, `declare an absolute unit (K), not '${raw.unit}'`);
81
+ return { name: raw.name, unit: raw.unit, dim: parsed.dim, scale: parsed.scale };
82
+ }
83
+ /** A number is read in the declared unit; a string may carry its own unit. */
84
+ function toSi(raw, q, where, reading) {
85
+ if (typeof raw !== 'number' && typeof raw !== 'string')
86
+ refuse(where, 'is not a number or a "value unit" string');
87
+ let v;
88
+ try {
89
+ v = convertValue(String(raw), q.unit, reading).value;
90
+ }
91
+ catch (e) {
92
+ if (e instanceof UnitError)
93
+ refuse(where, e.message);
94
+ throw e;
95
+ }
96
+ if (!Number.isFinite(v))
97
+ refuse(where, 'is not finite');
98
+ return v * q.scale;
99
+ }
100
+ function positiveSigma(raw, q, where) {
101
+ const s = toSi(raw, q, where, 'difference');
102
+ if (!(s > 0))
103
+ refuse(where, 'must be positive');
104
+ return s;
105
+ }
106
+ function governingQuantity(raw, where) {
107
+ const q = quantity(raw, where);
108
+ const sigma = raw.sigma;
109
+ return sigma === undefined ? q : { ...q, sigma: positiveSigma(sigma, q, `${where}.sigma`) };
110
+ }
111
+ function parseFamily(raw, governing, where) {
112
+ if (!isObject(raw) || !nonEmptyString(raw.input) || !Array.isArray(raw.powers)) {
113
+ refuse(where, 'needs {"input": <dimensionless governing input>, "powers": [p, ...]}');
114
+ }
115
+ for (const k of Object.keys(raw)) {
116
+ if (k !== 'input' && k !== 'powers')
117
+ refuse(where, `undeclared key '${k}'`);
118
+ }
119
+ const q = governing.find((g) => g.name === raw.input);
120
+ if (!q)
121
+ refuse(`${where}.input`, `'${raw.input}' is not a declared governing input`);
122
+ if (!equals(q.dim, DIMENSIONLESS)) {
123
+ refuse(`${where}.input`, `'${q.name}' is ${format(q.dim)}; a correction family needs a dimensionless input`);
124
+ }
125
+ const powers = raw.powers;
126
+ if (powers.length === 0 || powers.length > MAX_CORRECTION_TERMS) {
127
+ refuse(`${where}.powers`, `must list 1 to ${MAX_CORRECTION_TERMS} powers`);
128
+ }
129
+ if (!powers.every((p) => typeof p === 'number' && Number.isFinite(p) && p > 0)) {
130
+ refuse(`${where}.powers`, 'must be positive finite numbers');
131
+ }
132
+ if (new Set(powers).size !== powers.length)
133
+ refuse(`${where}.powers`, 'must be distinct');
134
+ return { input: q.name, powers };
135
+ }
136
+ /** One family `{input, powers}`, or a list of them, each in a different dimensionless input. */
137
+ function parseCorrection(raw, governing) {
138
+ if (raw === undefined)
139
+ return undefined;
140
+ if (!Array.isArray(raw))
141
+ return [parseFamily(raw, governing, 'correction')];
142
+ if (raw.length === 0)
143
+ refuse('correction', 'must list at least one family');
144
+ const families = raw.map((f, i) => parseFamily(f, governing, `correction[${i}]`));
145
+ const seen = new Set();
146
+ for (const [i, f] of families.entries()) {
147
+ if (seen.has(f.input))
148
+ refuse(`correction[${i}].input`, `'${f.input}' already has a family; give each input one family`);
149
+ seen.add(f.input);
150
+ }
151
+ const total = families.reduce((s, f) => s + f.powers.length, 0);
152
+ if (total > MAX_CORRECTION_TERMS) {
153
+ refuse('correction', `lists ${total} powers across its families; at most ${MAX_CORRECTION_TERMS}`);
154
+ }
155
+ return families;
156
+ }
157
+ /** Provenance text compared without case, spacing or punctuation: "Lab A" and "lab-a" are one source. */
158
+ const provenanceKey = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
159
+ function parseProvenance(raw) {
160
+ if (!isObject(raw))
161
+ refuse('provenance', 'is required: {"synthetic": true|false, "source": "..."}');
162
+ if (typeof raw.synthetic !== 'boolean') {
163
+ refuse('provenance.synthetic', 'must be declared true or false; it is never inferred');
164
+ }
165
+ if (!nonEmptyString(raw.source))
166
+ refuse('provenance.source', 'must name where the observations came from');
167
+ return {
168
+ synthetic: raw.synthetic,
169
+ source: raw.source,
170
+ ...(nonEmptyString(raw.acquisition) ? { acquisition: raw.acquisition } : {}),
171
+ ...(nonEmptyString(raw.calibration) ? { calibration: raw.calibration } : {}),
172
+ };
173
+ }
174
+ function parseBaselines(raw, target, governing) {
175
+ if (raw === undefined)
176
+ return [];
177
+ if (!Array.isArray(raw))
178
+ refuse('baselines', 'must be an array');
179
+ const dims = Object.fromEntries(governing.map((g) => [g.name, g.dim]));
180
+ const checker = builtinFormulaDimensionChecker();
181
+ return raw.map((b, i) => {
182
+ const where = `baselines[${i}]`;
183
+ if (!isObject(b) || !nonEmptyString(b.name) || !nonEmptyString(b.formula)) {
184
+ refuse(where, 'needs {"name", "formula"}');
185
+ }
186
+ let vars;
187
+ try {
188
+ vars = parseFormula(b.formula).variables;
189
+ }
190
+ catch (e) {
191
+ refuse(where, e.message);
192
+ }
193
+ const unknown = vars.filter((v) => !(v in dims));
194
+ if (unknown.length > 0)
195
+ refuse(where, `formula reads undeclared input(s) ${unknown.join(', ')}`);
196
+ const check = checker.check(b.formula, dims);
197
+ if (!check.ok || !check.dim)
198
+ refuse(where, check.error ?? 'not dimensionally homogeneous');
199
+ if (!equals(check.dim, target.dim)) {
200
+ refuse(where, `formula is ${format(check.dim)}, but the target '${target.name}' is ${format(target.dim)}`);
201
+ }
202
+ if (b.fitPrefactor !== undefined && typeof b.fitPrefactor !== 'boolean') {
203
+ refuse(`${where}.fitPrefactor`, 'must be true or false');
204
+ }
205
+ return { name: b.name, formula: b.formula, fitPrefactor: b.fitPrefactor === true };
206
+ });
207
+ }
208
+ function parseDesign(raw, governing) {
209
+ if (raw === undefined)
210
+ return undefined;
211
+ if (!isObject(raw) || !isObject(raw.variables))
212
+ refuse('design', 'needs {"variables": {name: {"min", "max"}}}');
213
+ const byName = new Map(governing.map((g) => [g.name, g]));
214
+ const out = {};
215
+ for (const [name, spec] of Object.entries(raw.variables)) {
216
+ const q = byName.get(name);
217
+ const where = `design.variables.${name}`;
218
+ if (!q)
219
+ refuse(where, 'is not a declared governing input');
220
+ if (!isObject(spec))
221
+ refuse(where, 'needs {"min", "max"}');
222
+ const min = toSi(spec.min, q, `${where}.min`, 'absolute');
223
+ const max = toSi(spec.max, q, `${where}.max`, 'absolute');
224
+ if (min > max)
225
+ refuse(where, 'has min > max');
226
+ const steps = spec.steps === undefined ? 8 : Number(spec.steps);
227
+ if (!Number.isInteger(steps) || steps < 1)
228
+ refuse(`${where}.steps`, 'must be a positive integer');
229
+ out[name] = { min, max, steps };
230
+ }
231
+ return out;
232
+ }
233
+ function parseObservations(raw, provenance, target, governing, defaultSigma) {
234
+ if (!Array.isArray(raw) || raw.length === 0)
235
+ refuse('observations', 'must be a non-empty array');
236
+ const ids = new Set();
237
+ const rows = raw.map((row, i) => {
238
+ const id = isObject(row) && nonEmptyString(row.id) ? row.id : `row-${i + 1}`;
239
+ const where = `observation ${id}`;
240
+ if (!isObject(row))
241
+ refuse(where, 'is not an object');
242
+ for (const k of Object.keys(row)) {
243
+ if (!ROW_KEYS.has(k))
244
+ refuse(where, `undeclared key '${k}'`);
245
+ }
246
+ if (ids.has(id))
247
+ refuse(where, 'duplicate id');
248
+ ids.add(id);
249
+ if (row.role !== 'exploratory' && row.role !== 'holdout' && row.role !== 'replication') {
250
+ refuse(where, `role must be exploratory, holdout or replication (got '${String(row.role)}')`);
251
+ }
252
+ if (!isObject(row.values))
253
+ refuse(where, 'needs "values": {input: value}');
254
+ for (const k of Object.keys(row.values)) {
255
+ if (!governing.some((g) => g.name === k))
256
+ refuse(`${where} values.${k}`, 'is not a declared governing input');
257
+ }
258
+ const inputs = {};
259
+ for (const g of governing) {
260
+ if (!(g.name in row.values))
261
+ refuse(where, `is missing input '${g.name}'`);
262
+ inputs[g.name] = toSi(row.values[g.name], g, `${where} values.${g.name}`, 'absolute');
263
+ }
264
+ if (row.observed === undefined)
265
+ refuse(where, `is missing "observed" (${target.name})`);
266
+ const observed = toSi(row.observed, target, `${where} observed`, 'absolute');
267
+ const sigmaRaw = row.sigma ?? defaultSigma;
268
+ if (sigmaRaw === undefined) {
269
+ refuse(where, 'has no uncertainty: give "sigma" on the row or on the target');
270
+ }
271
+ const sigma = positiveSigma(sigmaRaw, target, `${where} sigma`);
272
+ if (row.inputSigma !== undefined && !isObject(row.inputSigma))
273
+ refuse(where, '"inputSigma" must be {input: σ}');
274
+ const rowInputSigma = (row.inputSigma ?? {});
275
+ for (const k of Object.keys(rowInputSigma)) {
276
+ if (!governing.some((g) => g.name === k))
277
+ refuse(`${where} inputSigma.${k}`, 'is not a declared governing input');
278
+ }
279
+ const inputSigma = {};
280
+ for (const g of governing) {
281
+ const s = rowInputSigma[g.name] === undefined
282
+ ? g.sigma
283
+ : positiveSigma(rowInputSigma[g.name], g, `${where} inputSigma.${g.name}`);
284
+ if (s !== undefined)
285
+ inputSigma[g.name] = s;
286
+ }
287
+ const source = row.source === undefined ? provenance.source : row.source;
288
+ if (!nonEmptyString(source))
289
+ refuse(`${where} source`, 'must be a non-empty string');
290
+ return { id, role: row.role, source, inputs, observed, sigma, inputSigma };
291
+ });
292
+ guardLeakage(rows);
293
+ return rows;
294
+ }
295
+ /** The data of a row, without its id, role or source: equal keys are the same measurement. */
296
+ const dataKey = (r) => canonicalJson({ inputs: r.inputs, observed: r.observed, sigma: r.sigma, inputSigma: r.inputSigma });
297
+ /** Withheld rows must not repeat fit rows, and replication must be an independent acquisition. */
298
+ function guardLeakage(rows) {
299
+ const fitKeys = new Map();
300
+ const fitSources = new Set();
301
+ const studyData = new Map();
302
+ for (const r of rows) {
303
+ if (r.role !== 'replication')
304
+ studyData.set(dataKey(r), r);
305
+ if (r.role !== 'exploratory')
306
+ continue;
307
+ fitKeys.set(canonicalJson(r.inputs), r.id);
308
+ fitSources.add(provenanceKey(r.source));
309
+ }
310
+ for (const r of rows) {
311
+ if (r.role === 'exploratory')
312
+ continue;
313
+ const copy = r.role === 'replication' ? studyData.get(dataKey(r)) : undefined;
314
+ if (copy !== undefined) {
315
+ refuse(`observation ${r.id}`, `replication row is identical (inputs, observed, σ) to ${copy.role} row ${copy.id} of source '${copy.source}'; ` +
316
+ 'the same data under another source name is not an independent acquisition');
317
+ }
318
+ const twin = fitKeys.get(canonicalJson(r.inputs));
319
+ if (twin !== undefined) {
320
+ refuse(`observation ${r.id}`, `${r.role} row repeats the inputs of exploratory row ${twin}; withheld data must not be fit data`);
321
+ }
322
+ if (r.role === 'replication' && fitSources.has(provenanceKey(r.source))) {
323
+ refuse(`observation ${r.id}`, `replication row has source '${r.source}', which also supplied exploratory rows; ` +
324
+ 'replication must come from an independent acquisition (give it its own "source")');
325
+ }
326
+ }
327
+ }
328
+ /** Parse and validate a study file. Throws {@link StudyRefusal}. @internal */
329
+ export function parseStudy(raw, source = 'inline') {
330
+ if (!isObject(raw))
331
+ refuse(source, 'is not a JSON object');
332
+ for (const k of Object.keys(raw)) {
333
+ if (!TOP_KEYS.has(k))
334
+ refuse(source, `undeclared top-level key '${k}'`);
335
+ }
336
+ const provenance = parseProvenance(raw.provenance);
337
+ const target = quantity(raw.target, 'target');
338
+ if (!Array.isArray(raw.governing) || raw.governing.length === 0) {
339
+ refuse('governing', 'must be a non-empty array of {"name", "unit"}');
340
+ }
341
+ const governing = raw.governing.map((g, i) => governingQuantity(g, `governing[${i}]`));
342
+ const names = [target.name, ...governing.map((g) => g.name)];
343
+ if (new Set(names).size !== names.length)
344
+ refuse('governing', 'names must be distinct from each other and from the target');
345
+ const gapRaw = isObject(raw.gap) ? raw.gap : {};
346
+ const gap = {
347
+ id: nonEmptyString(gapRaw.id) ? gapRaw.id : 'fg-study',
348
+ kind: nonEmptyString(gapRaw.kind) ? gapRaw.kind : 'unexplained-observation',
349
+ summary: nonEmptyString(gapRaw.summary) ? gapRaw.summary : `study from ${source}`,
350
+ };
351
+ if (!isGapKind(gap.kind))
352
+ refuse('gap.kind', `unknown gap kind '${gap.kind}'`);
353
+ let alpha = DEFAULT_ALPHA;
354
+ if (raw.criterion !== undefined) {
355
+ if (!isObject(raw.criterion) || typeof raw.criterion.alpha !== 'number') {
356
+ refuse('criterion', 'needs {"alpha": number}');
357
+ }
358
+ alpha = raw.criterion.alpha;
359
+ if (!(alpha > 0 && alpha < 1))
360
+ refuse('criterion.alpha', 'must be in (0, 1)');
361
+ }
362
+ const targetRaw = raw.target;
363
+ const observations = parseObservations(raw.observations, provenance, target, governing, targetRaw.sigma);
364
+ const baselines = parseBaselines(raw.baselines, target, governing);
365
+ const design = parseDesign(raw.design, governing);
366
+ const correction = parseCorrection(raw.correction, governing);
367
+ return {
368
+ source,
369
+ gap,
370
+ provenance,
371
+ target,
372
+ governing,
373
+ observations,
374
+ baselines,
375
+ alpha,
376
+ ...(design ? { design } : {}),
377
+ ...(correction ? { correction } : {}),
378
+ };
379
+ }
380
+ /** Load a study JSON file. @internal */
381
+ export function loadStudyFromJson(path) {
382
+ return parseStudy(JSON.parse(readFileSync(path, 'utf8')), path);
383
+ }
384
+ function readStudyRaw(path) {
385
+ const text = readFileSync(path, 'utf8');
386
+ if (/\.csv$/i.test(path))
387
+ return studyCsvToRaw(text, path);
388
+ try {
389
+ return JSON.parse(text);
390
+ }
391
+ catch (e) {
392
+ throw e instanceof SyntaxError ? new SyntaxError(`${path}: ${e.message}`) : e;
393
+ }
394
+ }
395
+ /**
396
+ * Load a study file (`.csv`, else JSON) and, optionally, a separate
397
+ * replication file with its own provenance. Throws {@link StudyRefusal}.
398
+ *
399
+ * @internal
400
+ */
401
+ export function loadStudyFile(path, replicationPath) {
402
+ const study = parseStudy(readStudyRaw(path), path);
403
+ return replicationPath === undefined ? study : attachReplication(study, readStudyRaw(replicationPath), replicationPath);
404
+ }
405
+ const sameDim = (a, b) => a.name === b.name && equals(a.dim, b.dim);
406
+ /**
407
+ * Add the rows of a separate replication file to a study. The file must
408
+ * declare its own provenance, the study's target and inputs (units may
409
+ * differ), and replication rows only. Its source and acquisition must differ
410
+ * from the study's, and no row may repeat a study row's data.
411
+ *
412
+ * @internal
413
+ */
414
+ export function attachReplication(study, raw, source) {
415
+ if (!isObject(raw))
416
+ refuse(source, 'is not a JSON object');
417
+ for (const k of Object.keys(raw)) {
418
+ if (!REPLICATION_KEYS.has(k)) {
419
+ refuse(source, `'${k}' belongs in the study file; a replication file carries only ${[...REPLICATION_KEYS].join(', ')}`);
420
+ }
421
+ }
422
+ if (study.observations.some((o) => o.role === 'replication')) {
423
+ refuse(source, `the study ${study.source} already has replication rows; give replication in one place`);
424
+ }
425
+ const rep = parseStudy(raw, source);
426
+ const mine = rep.observations.find((o) => o.role !== 'replication');
427
+ if (mine)
428
+ refuse(`${source} observation ${mine.id}`, `a replication file holds replication rows only (got '${mine.role}')`);
429
+ if (!sameDim(rep.target, study.target)) {
430
+ refuse(`${source} target`, `must be '${study.target.name}' ${format(study.target.dim)}, as in the study`);
431
+ }
432
+ const names = (qs) => qs.map((q) => q.name).sort().join(', ');
433
+ if (rep.governing.length !== study.governing.length ||
434
+ !study.governing.every((g) => rep.governing.some((r) => sameDim(r, g)))) {
435
+ refuse(`${source} governing`, `must declare the study's inputs {${names(study.governing)}} with the same dimensions (got {${names(rep.governing)}})`);
436
+ }
437
+ if (provenanceKey(rep.provenance.source) === provenanceKey(study.provenance.source)) {
438
+ refuse(`${source} provenance.source`, `is '${rep.provenance.source}', the study's own source; replication must be an independent acquisition`);
439
+ }
440
+ if (rep.provenance.acquisition !== undefined &&
441
+ study.provenance.acquisition !== undefined &&
442
+ provenanceKey(rep.provenance.acquisition) === provenanceKey(study.provenance.acquisition)) {
443
+ refuse(`${source} provenance.acquisition`, 'is the study\'s own acquisition; replication must be an independent acquisition');
444
+ }
445
+ const ids = new Set(study.observations.map((o) => o.id));
446
+ const clash = rep.observations.find((o) => ids.has(o.id));
447
+ if (clash)
448
+ refuse(`${source} observation ${clash.id}`, `id is also used in the study ${study.source}`);
449
+ const observations = [...study.observations, ...rep.observations];
450
+ guardLeakage(observations);
451
+ return { ...study, observations, replication: { source, provenance: rep.provenance } };
452
+ }
453
+ function sameInputs(a, b) {
454
+ const keys = Object.keys(a.inputs);
455
+ if (keys.length !== Object.keys(b.inputs).length)
456
+ return false;
457
+ return keys.every((k) => {
458
+ const x = a.inputs[k];
459
+ const y = b.inputs[k];
460
+ if (x === undefined || y === undefined)
461
+ return false;
462
+ const sa = a.inputSigma[k] ?? 0;
463
+ const sb = b.inputSigma[k] ?? 0;
464
+ const tol = sa > 0 || sb > 0 ? 3 * Math.hypot(sa, sb) : 1e-9 * Math.max(Math.abs(x), Math.abs(y));
465
+ return Math.abs(x - y) <= tol;
466
+ });
467
+ }
468
+ function closeness(chi2, dof, alpha) {
469
+ const pLower = chiSquareCdf(chi2, dof);
470
+ return { chi2, dof, pLower, flagged: pLower < alpha / 2 };
471
+ }
472
+ /** Weighted straight line y = a + b·x with the effective variance σ_y² + b²σ_x², iterated from b = 1. */
473
+ function affineFit(pts) {
474
+ let b = 1;
475
+ let a = 0;
476
+ for (let it = 0; it < 6; it++) {
477
+ const w = pts.map((p) => 1 / (p.sy ** 2 + b * b * p.sx ** 2));
478
+ const W = w.reduce((s, v) => s + v, 0);
479
+ const xm = pts.reduce((s, p, i) => s + w[i] * p.x, 0) / W;
480
+ const ym = pts.reduce((s, p, i) => s + w[i] * p.y, 0) / W;
481
+ const sxx = pts.reduce((s, p, i) => s + w[i] * (p.x - xm) ** 2, 0);
482
+ const sxy = pts.reduce((s, p, i) => s + w[i] * (p.x - xm) * (p.y - ym), 0);
483
+ if (!(sxx > 0) || !Number.isFinite(sxy))
484
+ return null;
485
+ b = sxy / sxx;
486
+ a = ym - b * xm;
487
+ }
488
+ const chi2 = pts.reduce((s, p) => s + (p.y - a - b * p.x) ** 2 / (p.sy ** 2 + b * b * p.sx ** 2), 0);
489
+ return Number.isFinite(chi2) ? { a, b, chi2 } : null;
490
+ }
491
+ /**
492
+ * Fisher's test of agreement too good to be true. Each replication row is
493
+ * paired with the first unused study row at the same inputs (within 3·√(σ_x²
494
+ * + σ_x'²) when an input σ is declared, else to 1e-9 relative). Independent
495
+ * acquisitions differ by about their σ, so a χ² far in the lower tail means a
496
+ * copy, or an overstated σ. Two tests, each at α/2: identity (y_rep = y_study)
497
+ * and affine (y_rep = a + b·y_study, which catches a rescaled or shifted copy).
498
+ * Only paired rows are tested: a copy moved to other inputs is not caught.
499
+ *
500
+ * @internal
501
+ */
502
+ export function replicationIndependence(observations, alpha) {
503
+ const reps = observations.filter((o) => o.role === 'replication');
504
+ if (reps.length === 0) {
505
+ return { pairs: 0, identity: null, affine: null, tooClose: false, reason: 'no replication rows; the closeness test did not run' };
506
+ }
507
+ const used = new Set();
508
+ const pts = [];
509
+ for (const r of reps) {
510
+ const s = observations.find((o) => o.role !== 'replication' && !used.has(o) && sameInputs(r, o));
511
+ if (!s)
512
+ continue;
513
+ used.add(s);
514
+ pts.push({ x: s.observed, sx: s.sigma, y: r.observed, sy: r.sigma });
515
+ }
516
+ const n = pts.length;
517
+ if (n === 0) {
518
+ return {
519
+ pairs: 0,
520
+ identity: null,
521
+ affine: null,
522
+ tooClose: false,
523
+ reason: 'no replication row shares its inputs with a study row; the closeness test did not run',
524
+ };
525
+ }
526
+ const identity = closeness(pts.reduce((s, p) => s + (p.y - p.x) ** 2 / (p.sy ** 2 + p.sx ** 2), 0), n, alpha);
527
+ const fit = n >= 3 ? affineFit(pts) : null;
528
+ const affine = fit ? { ...closeness(fit.chi2, n - 2, alpha), slope: fit.b, offset: fit.a } : null;
529
+ const tooClose = identity.flagged || (affine?.flagged ?? false);
530
+ const cmp = (t) => `lower-tail p = ${fmtP(t.pLower)} ${t.flagged ? '<' : '≥'} α/2 = ${alpha / 2}`;
531
+ const parts = [
532
+ n === 1 ? '1 replication row shares its inputs with a study row' : `${n} replication rows share their inputs with study rows`,
533
+ ...(n < reps.length ? [`${reps.length - n} other(s) are at inputs no study row has and are not tested`] : []),
534
+ `identity: Σz² = ${identity.chi2.toPrecision(3)} on ν = ${n}, ${cmp(identity)}`,
535
+ affine
536
+ ? `affine: y_rep = ${affine.offset.toPrecision(3)} + ${affine.slope.toPrecision(6)} · y_study, χ² = ${affine.chi2.toPrecision(3)} on ν = ${n - 2}, ${cmp(affine)}`
537
+ : n < 3
538
+ ? 'affine: not run (needs 3 pairs)'
539
+ : 'affine: not run (the paired study values do not vary)',
540
+ ];
541
+ const verdict = tooClose
542
+ ? 'closer than independent measurements with the declared σ allow — not independent, or σ is overstated'
543
+ : 'no closer than independent measurements with the declared σ would be';
544
+ return { pairs: n, identity, affine, tooClose, reason: `${parts.join('; ')}: ${verdict}` };
545
+ }
546
+ // --- CSV study files -------------------------------------------------------
547
+ const CSV_ROW_COLUMNS = new Set(['id', 'role', 'source', 'sigma', 'note']);
548
+ function csvCells(line, where) {
549
+ const cells = [];
550
+ let i = 0;
551
+ for (;;) {
552
+ let cell = '';
553
+ if (line[i] === '"') {
554
+ i++;
555
+ for (;;) {
556
+ if (i >= line.length)
557
+ refuse(where, 'has an unterminated quoted cell');
558
+ if (line[i] === '"') {
559
+ if (line[i + 1] === '"') {
560
+ cell += '"';
561
+ i += 2;
562
+ continue;
563
+ }
564
+ i++;
565
+ break;
566
+ }
567
+ cell += line[i++];
568
+ }
569
+ if (i < line.length && line[i] !== ',')
570
+ refuse(where, 'has text after a closing quote');
571
+ }
572
+ else {
573
+ const end = line.indexOf(',', i);
574
+ cell = line.slice(i, end < 0 ? line.length : end);
575
+ i = end < 0 ? line.length : end;
576
+ }
577
+ cells.push(cell.trim());
578
+ if (i >= line.length)
579
+ return cells;
580
+ i++;
581
+ }
582
+ }
583
+ /** A declaration value: JSON when it parses as JSON, else the text itself. */
584
+ function declValue(text) {
585
+ try {
586
+ return JSON.parse(text);
587
+ }
588
+ catch {
589
+ return text;
590
+ }
591
+ }
592
+ function setPath(obj, path, value, where) {
593
+ let at = obj;
594
+ for (const k of path.slice(0, -1)) {
595
+ if (at[k] === undefined)
596
+ at[k] = {};
597
+ if (!isObject(at[k]))
598
+ refuse(where, `'${k}' is already set to a value`);
599
+ at = at[k];
600
+ }
601
+ const last = path[path.length - 1];
602
+ if (at[last] !== undefined)
603
+ refuse(where, `'${path.join('.')}' is declared twice`);
604
+ at[last] = value;
605
+ }
606
+ /**
607
+ * Compile a CSV study into the object {@link parseStudy} reads, so a CSV is
608
+ * refused for exactly the reasons its JSON twin would be.
609
+ *
610
+ * Lines `# key: value` above the header declare the study: `target` names
611
+ * the target column; `target.sigma`, `governing.<input>.sigma` and any other
612
+ * dotted key (`provenance.synthetic`, `provenance.source`, `criterion.alpha`,
613
+ * `baselines`, `design`, `correction`, ...) set that key. A value is JSON
614
+ * when it parses as JSON, else text. The header names `name[unit]` quantity
615
+ * columns (`name[]` when dimensionless) and the reserved columns `id`,
616
+ * `role`, `source`, `sigma` (target σ), `sigma(<input>)` and `note`. An
617
+ * empty cell is an absent value.
618
+ *
619
+ * @internal
620
+ */
621
+ export function studyCsvToRaw(text, source = 'inline.csv') {
622
+ const lines = text.split(/\r?\n/).map((l, i) => ({ l, n: i + 1 })).filter(({ l }) => l.trim() !== '');
623
+ const raw = {};
624
+ let targetName;
625
+ const governingSigma = {};
626
+ let h = 0;
627
+ for (; h < lines.length && lines[h].l.trimStart().startsWith('#'); h++) {
628
+ const { l, n } = lines[h];
629
+ const where = `${source} line ${n}`;
630
+ const m = /^\s*#\s*([A-Za-z_][\w.()-]*)\s*:\s?(.*)$/.exec(l);
631
+ if (!m)
632
+ refuse(where, "a declaration is '# key: value'");
633
+ const [, key, valueText] = m;
634
+ const value = declValue(valueText.trim());
635
+ const path = key.split('.');
636
+ if (key === 'target') {
637
+ if (targetName !== undefined)
638
+ refuse(where, "'target' is declared twice");
639
+ if (!nonEmptyString(value))
640
+ refuse(where, "'# target:' names the target column");
641
+ targetName = value;
642
+ }
643
+ else if (path[0] === 'governing') {
644
+ if (path.length !== 3 || path[2] !== 'sigma')
645
+ refuse(where, "the only governing declaration is '# governing.<input>.sigma: σ'");
646
+ if (path[1] in governingSigma)
647
+ refuse(where, `'${key}' is declared twice`);
648
+ governingSigma[path[1]] = value;
649
+ }
650
+ else if (key === 'observations') {
651
+ refuse(where, 'observations are the rows below the header');
652
+ }
653
+ else {
654
+ setPath(raw, path, value, where);
655
+ }
656
+ }
657
+ if (h >= lines.length)
658
+ refuse(source, 'has no header row');
659
+ const header = csvCells(lines[h].l, `${source} header`);
660
+ if (targetName === undefined)
661
+ refuse(source, "needs a '# target: <column>' declaration");
662
+ const seen = new Set();
663
+ const columns = header.map((cell) => {
664
+ const where = `${source} header column '${cell}'`;
665
+ let col;
666
+ let name;
667
+ const q = /^([A-Za-z_]\w*)\[(.*)\]$/.exec(cell);
668
+ const s = /^sigma\(([A-Za-z_]\w*)\)$/.exec(cell);
669
+ if (CSV_ROW_COLUMNS.has(cell)) {
670
+ col = { kind: 'row', key: cell };
671
+ name = cell;
672
+ }
673
+ else if (s) {
674
+ col = { kind: 'inputSigma', input: s[1] };
675
+ name = cell;
676
+ }
677
+ else if (q) {
678
+ col = { kind: 'quantity', name: q[1], unit: q[2].trim() };
679
+ name = q[1];
680
+ }
681
+ else {
682
+ refuse(where, `is not ${[...CSV_ROW_COLUMNS].join(', ')}, sigma(<input>) or a quantity 'name[unit]' ('name[]' when dimensionless)`);
683
+ }
684
+ if (seen.has(name))
685
+ refuse(where, 'is a duplicate column');
686
+ seen.add(name);
687
+ return col;
688
+ });
689
+ const quantities = columns.filter((c) => c.kind === 'quantity');
690
+ const targetCol = quantities.find((c) => c.name === targetName);
691
+ if (!targetCol)
692
+ refuse(source, `'# target: ${targetName}' names no 'name[unit]' column`);
693
+ const governing = quantities.filter((c) => c !== targetCol);
694
+ for (const c of columns) {
695
+ if (c.kind === 'inputSigma' && !governing.some((g) => g.name === c.input)) {
696
+ refuse(`${source} header column 'sigma(${c.input})'`, `'${c.input}' is not a governing input column`);
697
+ }
698
+ }
699
+ for (const k of Object.keys(governingSigma)) {
700
+ if (!governing.some((g) => g.name === k))
701
+ refuse(`${source} governing.${k}.sigma`, `'${k}' is not a governing input column`);
702
+ }
703
+ if (isObject(raw.target)) {
704
+ for (const k of Object.keys(raw.target)) {
705
+ if (k !== 'sigma')
706
+ refuse(`${source} target.${k}`, "the target's name and unit come from its column; only 'target.sigma' is declared");
707
+ }
708
+ }
709
+ raw.target = { name: targetCol.name, unit: targetCol.unit, ...(isObject(raw.target) ? raw.target : {}) };
710
+ raw.governing = governing.map((g) => ({
711
+ name: g.name,
712
+ unit: g.unit,
713
+ ...(g.name in governingSigma ? { sigma: governingSigma[g.name] } : {}),
714
+ }));
715
+ raw.observations = lines.slice(h + 1).map(({ l, n }) => {
716
+ const where = `${source} line ${n}`;
717
+ if (l.trimStart().startsWith('#'))
718
+ refuse(where, 'declarations go above the header row');
719
+ const cells = csvCells(l, where);
720
+ if (cells.length !== columns.length)
721
+ refuse(where, `has ${cells.length} cells; the header has ${columns.length}`);
722
+ const row = {};
723
+ const values = {};
724
+ const inputSigma = {};
725
+ columns.forEach((c, i) => {
726
+ const cell = cells[i];
727
+ if (cell === '')
728
+ return;
729
+ if (c.kind === 'row')
730
+ row[c.key] = cell;
731
+ else if (c.kind === 'inputSigma')
732
+ inputSigma[c.input] = cell;
733
+ else if (c === targetCol)
734
+ row.observed = cell;
735
+ else
736
+ values[c.name] = cell;
737
+ });
738
+ row.values = values;
739
+ if (Object.keys(inputSigma).length > 0)
740
+ row.inputSigma = inputSigma;
741
+ return row;
742
+ });
743
+ return raw;
744
+ }
745
+ // --- χ² survival function -------------------------------------------------
746
+ const LANCZOS = [
747
+ 0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313, -176.61502916214059,
748
+ 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7,
749
+ ];
750
+ function lnGamma(z) {
751
+ if (z < 0.5)
752
+ return Math.log(Math.PI / Math.sin(Math.PI * z)) - lnGamma(1 - z);
753
+ const zz = z - 1;
754
+ let x = LANCZOS[0];
755
+ for (let i = 1; i < LANCZOS.length; i++)
756
+ x += LANCZOS[i] / (zz + i);
757
+ const t = zz + 7.5;
758
+ return 0.5 * Math.log(2 * Math.PI) + (zz + 0.5) * Math.log(t) - t + Math.log(x);
759
+ }
760
+ /** The series for the regularized lower incomplete gamma P(a, x); converges fast for x < a + 1. */
761
+ function gammaSeries(a, x) {
762
+ const front = Math.exp(-x + a * Math.log(x) - lnGamma(a));
763
+ let ap = a;
764
+ let del = 1 / a;
765
+ let sum = del;
766
+ for (let n = 0; n < 1000; n++) {
767
+ ap += 1;
768
+ del *= x / ap;
769
+ sum += del;
770
+ if (Math.abs(del) < Math.abs(sum) * 1e-16)
771
+ break;
772
+ }
773
+ return sum * front;
774
+ }
775
+ /** Regularized upper incomplete gamma Q(a, x). */
776
+ function gammaQ(a, x) {
777
+ if (x <= 0)
778
+ return 1;
779
+ if (x < a + 1)
780
+ return Math.max(0, 1 - gammaSeries(a, x));
781
+ const front = Math.exp(-x + a * Math.log(x) - lnGamma(a));
782
+ const tiny = 1e-300;
783
+ let b = x + 1 - a;
784
+ let c = 1 / tiny;
785
+ let d = 1 / b;
786
+ let h = d;
787
+ for (let i = 1; i < 1000; i++) {
788
+ const an = -i * (i - a);
789
+ b += 2;
790
+ d = an * d + b;
791
+ if (Math.abs(d) < tiny)
792
+ d = tiny;
793
+ c = b + an / c;
794
+ if (Math.abs(c) < tiny)
795
+ c = tiny;
796
+ d = 1 / d;
797
+ const del = d * c;
798
+ h *= del;
799
+ if (Math.abs(del - 1) < 1e-16)
800
+ break;
801
+ }
802
+ return front * h;
803
+ }
804
+ /** P(χ²_ν ≥ chi2): the probability of a χ² at least this large if the model and σ are right. @internal */
805
+ export function chiSquareSurvival(chi2, dof) {
806
+ if (!(dof > 0))
807
+ throw new RangeError('chiSquareSurvival: dof must be positive');
808
+ if (!(chi2 > 0))
809
+ return 1;
810
+ return gammaQ(dof / 2, chi2 / 2);
811
+ }
812
+ /**
813
+ * P(χ²_ν ≤ chi2), computed directly in the lower tail, where 1 − Q would
814
+ * round a small probability away. @internal
815
+ */
816
+ export function chiSquareCdf(chi2, dof) {
817
+ if (!(dof > 0))
818
+ throw new RangeError('chiSquareCdf: dof must be positive');
819
+ if (!(chi2 > 0))
820
+ return 0;
821
+ const a = dof / 2;
822
+ const x = chi2 / 2;
823
+ return x < a + 1 ? Math.min(1, gammaSeries(a, x)) : 1 - gammaQ(a, x);
824
+ }
825
+ /** Continued fraction for the incomplete beta function (modified Lentz). */
826
+ function betaCf(a, b, x) {
827
+ const tiny = 1e-300;
828
+ let c = 1;
829
+ let d = 1 - ((a + b) * x) / (a + 1);
830
+ if (Math.abs(d) < tiny)
831
+ d = tiny;
832
+ d = 1 / d;
833
+ let h = d;
834
+ for (let m = 1; m < 1000; m++) {
835
+ const m2 = 2 * m;
836
+ let aa = (m * (b - m) * x) / ((a + m2 - 1) * (a + m2));
837
+ d = 1 + aa * d;
838
+ if (Math.abs(d) < tiny)
839
+ d = tiny;
840
+ c = 1 + aa / c;
841
+ if (Math.abs(c) < tiny)
842
+ c = tiny;
843
+ d = 1 / d;
844
+ h *= d * c;
845
+ aa = (-(a + m) * (a + b + m) * x) / ((a + m2) * (a + m2 + 1));
846
+ d = 1 + aa * d;
847
+ if (Math.abs(d) < tiny)
848
+ d = tiny;
849
+ c = 1 + aa / c;
850
+ if (Math.abs(c) < tiny)
851
+ c = tiny;
852
+ d = 1 / d;
853
+ const del = d * c;
854
+ h *= del;
855
+ if (Math.abs(del - 1) < 1e-16)
856
+ break;
857
+ }
858
+ return h;
859
+ }
860
+ /** Regularized incomplete beta I_x(a, b). */
861
+ function betaI(a, b, x) {
862
+ if (x <= 0)
863
+ return 0;
864
+ if (x >= 1)
865
+ return 1;
866
+ const front = Math.exp(lnGamma(a + b) - lnGamma(a) - lnGamma(b) + a * Math.log(x) + b * Math.log(1 - x));
867
+ return x < (a + 1) / (a + b + 2) ? (front * betaCf(a, b, x)) / a : 1 - (front * betaCf(b, a, 1 - x)) / b;
868
+ }
869
+ /** P(F_{d1,d2} ≥ f): the probability of an F ratio at least this large if the extra terms explain nothing. @internal */
870
+ export function fSurvival(f, d1, d2) {
871
+ if (!(d1 > 0 && d2 > 0))
872
+ throw new RangeError('fSurvival: degrees of freedom must be positive');
873
+ if (!(f > 0))
874
+ return 1;
875
+ if (f === Infinity)
876
+ return 0;
877
+ return betaI(d2 / 2, d1 / 2, d2 / (d2 + d1 * f));
878
+ }
879
+ /**
880
+ * The effective σ of one row under a model f: σ_eff² = σ_y² + Σ_i (∂f/∂x_i · σ_xi)²
881
+ * over the inputs with a σ, each derivative by a central difference at the
882
+ * recorded inputs. First order in σ_x: exact for f linear in x_i.
883
+ *
884
+ * @internal
885
+ */
886
+ export function effectiveSigma(r, predict) {
887
+ let v = r.sigma * r.sigma;
888
+ for (const [name, sx] of Object.entries(r.inputSigma)) {
889
+ const x = r.inputs[name];
890
+ const h = Math.max(Math.abs(x) * 1e-6, sx * 1e-3);
891
+ const d = (predict({ ...r.inputs, [name]: x + h }) - predict({ ...r.inputs, [name]: x - h })) / (2 * h);
892
+ if (!Number.isFinite(d))
893
+ throw new RangeError(`the model's slope in ${name} is not finite on row ${r.id}`);
894
+ v += (d * sx) ** 2;
895
+ }
896
+ return Math.sqrt(v);
897
+ }
898
+ const hasInputSigma = (rows) => rows.some((r) => Object.keys(r.inputSigma).length > 0);
899
+ function testRows(rows, predict, fitted, alpha) {
900
+ let chi2 = 0;
901
+ let maxAbsZ = 0;
902
+ let worstRow = null;
903
+ for (const r of rows) {
904
+ const z = (r.observed - predict(r.inputs)) / effectiveSigma(r, predict);
905
+ if (!Number.isFinite(z))
906
+ throw new RangeError(`prediction is not finite on row ${r.id}`);
907
+ chi2 += z * z;
908
+ if (Math.abs(z) > maxAbsZ) {
909
+ maxAbsZ = Math.abs(z);
910
+ worstRow = r.id;
911
+ }
912
+ }
913
+ const dof = rows.length - fitted;
914
+ const p = dof >= 1 ? chiSquareSurvival(chi2, dof) : null;
915
+ return { n: rows.length, chi2, dof, p, maxAbsZ, worstRow, pass: p === null ? null : p >= alpha };
916
+ }
917
+ const combine = (basis, coef) => (x) => basis.reduce((s, b, j) => s + coef[j] * b(x), 0);
918
+ /** Solve the weighted normal equations for y ≈ Σ a_j·φ_j; null when they are singular. */
919
+ function solveWeighted(phi, y, sigma) {
920
+ const k = phi[0]?.length ?? 0;
921
+ const A = Array.from({ length: k }, () => new Array(k + 1).fill(0));
922
+ phi.forEach((f, i) => {
923
+ const w = 1 / (sigma[i] * sigma[i]);
924
+ for (let a = 0; a < k; a++) {
925
+ for (let b = 0; b < k; b++)
926
+ A[a][b] += w * f[a] * f[b];
927
+ A[a][k] += w * f[a] * y[i];
928
+ }
929
+ });
930
+ if (k === 1)
931
+ return A[0][0] > 0 ? [A[0][1] / A[0][0]] : null;
932
+ // Equilibrate so the pivot test is scale-free, then eliminate with partial pivoting.
933
+ const s = A.map((row, a) => Math.sqrt(row[a]));
934
+ if (!s.every((v) => v > 0))
935
+ return null;
936
+ for (let a = 0; a < k; a++) {
937
+ for (let b = 0; b < k; b++)
938
+ A[a][b] /= s[a] * s[b];
939
+ A[a][k] /= s[a];
940
+ }
941
+ for (let c = 0; c < k; c++) {
942
+ let p = c;
943
+ for (let r = c + 1; r < k; r++)
944
+ if (Math.abs(A[r][c]) > Math.abs(A[p][c]))
945
+ p = r;
946
+ if (Math.abs(A[p][c]) < 1e-13)
947
+ return null;
948
+ [A[c], A[p]] = [A[p], A[c]];
949
+ for (let r = 0; r < k; r++) {
950
+ if (r === c)
951
+ continue;
952
+ const m = A[r][c] / A[c][c];
953
+ for (let q = c; q <= k; q++)
954
+ A[r][q] -= m * A[c][q];
955
+ }
956
+ }
957
+ return A.map((row, a) => row[k] / row[a] / s[a]);
958
+ }
959
+ const EV_ROUNDS = 50;
960
+ /**
961
+ * Weighted least squares y ≈ Σ a_j·φ_j(x), from the exploratory rows only.
962
+ * With input σ the weights are 1/σ_eff² of the current fit, iterated to a
963
+ * fixed point (the effective-variance method).
964
+ */
965
+ function fitLinear(rows, basis) {
966
+ const phi = rows.map((r) => basis.map((b) => {
967
+ const f = b(r.inputs);
968
+ if (!Number.isFinite(f))
969
+ throw new RangeError(`model is not finite on exploratory row ${r.id}`);
970
+ return f;
971
+ }));
972
+ const y = rows.map((r) => r.observed);
973
+ let coef = solveWeighted(phi, y, rows.map((r) => r.sigma));
974
+ if (!hasInputSigma(rows))
975
+ return coef;
976
+ for (let round = 0; coef !== null && round < EV_ROUNDS; round++) {
977
+ const predict = combine(basis, coef);
978
+ const next = solveWeighted(phi, y, rows.map((r) => effectiveSigma(r, predict)));
979
+ if (next === null)
980
+ return null;
981
+ const settled = next.every((a, j) => Math.abs(a - coef[j]) <= 1e-13 * Math.max(Math.abs(a), 1e-300));
982
+ coef = next;
983
+ if (settled)
984
+ break;
985
+ }
986
+ return coef;
987
+ }
988
+ function split(study) {
989
+ return {
990
+ exploratory: study.observations.filter((o) => o.role === 'exploratory'),
991
+ holdout: study.observations.filter((o) => o.role === 'holdout'),
992
+ replication: study.observations.filter((o) => o.role === 'replication'),
993
+ };
994
+ }
995
+ /**
996
+ * Fit a model's coefficients on the exploratory rows, then (only if asked)
997
+ * score it on the withheld rows with them frozen.
998
+ */
999
+ function scoreModel(spec, sets, alpha, withheld) {
1000
+ const { id, kind, label } = spec;
1001
+ const k = spec.fit ? spec.basis.length : 0;
1002
+ const failed = (error) => ({
1003
+ test: { id, kind, label, fittedParameters: k, prefactor: null, exploratory: null, holdout: null, replication: null, error },
1004
+ predict: null,
1005
+ coef: null,
1006
+ });
1007
+ try {
1008
+ const coef = spec.fit ? fitLinear(sets.exploratory, spec.basis) : [1];
1009
+ if (coef === null) {
1010
+ return failed(k === 1
1011
+ ? 'model is zero on every exploratory row; no scale can be fit'
1012
+ : 'the model\'s terms are degenerate on the exploratory rows; they cannot be fit');
1013
+ }
1014
+ const predict = combine(spec.basis, coef);
1015
+ const on = (rows) => (withheld && rows.length > 0 ? testRows(rows, predict, 0, alpha) : null);
1016
+ return {
1017
+ test: {
1018
+ id,
1019
+ kind,
1020
+ label,
1021
+ fittedParameters: k,
1022
+ prefactor: coef[0],
1023
+ exploratory: sets.exploratory.length > 0 ? testRows(sets.exploratory, predict, k, alpha) : null,
1024
+ holdout: on(sets.holdout),
1025
+ replication: on(sets.replication),
1026
+ },
1027
+ predict,
1028
+ coef,
1029
+ };
1030
+ }
1031
+ catch (e) {
1032
+ return failed(e.message);
1033
+ }
1034
+ }
1035
+ const SUBSCRIPT = '₀₁₂₃₄₅₆₇₈₉';
1036
+ const sub = (n) => [...String(n)].map((d) => SUBSCRIPT[Number(d)]).join('');
1037
+ function correctionBasis(shape, terms) {
1038
+ return [shape, ...terms.map((t) => (x) => shape(x) * x[t.input] ** t.power)];
1039
+ }
1040
+ /**
1041
+ * Admit the declared terms one at a time, family by family in the declared
1042
+ * order and each family's powers in its declared order, while each lowers the
1043
+ * exploratory χ² significantly given the terms already admitted: F on
1044
+ * (1, ν_k) with p < α. The first term a family fails ends that family; the
1045
+ * next family is then tried. The F ratio divides by the residual χ²/ν, so an
1046
+ * understated σ does not inflate it.
1047
+ */
1048
+ function admitCorrection(shape, families, rows, alpha) {
1049
+ const chi2With = (terms) => {
1050
+ const basis = correctionBasis(shape, terms);
1051
+ const coef = fitLinear(rows, basis);
1052
+ return coef === null ? null : testRows(rows, combine(basis, coef), terms.length + 1, alpha).chi2;
1053
+ };
1054
+ const steps = [];
1055
+ const admitted = [];
1056
+ let prev = chi2With([]);
1057
+ families: for (const family of families) {
1058
+ for (const power of family.powers) {
1059
+ if (prev === null)
1060
+ break families;
1061
+ const term = { input: family.input, power };
1062
+ const trial = [...admitted, term];
1063
+ const dof = rows.length - (trial.length + 1);
1064
+ if (dof < 1) {
1065
+ steps.push({ ...term, F: null, dof, p: null, admitted: false, reason: 'ν < 1: too few exploratory rows to test another term' });
1066
+ break families;
1067
+ }
1068
+ const cur = chi2With(trial);
1069
+ if (cur === null) {
1070
+ steps.push({ ...term, F: null, dof, p: null, admitted: false, reason: 'degenerate with the terms before it on the exploratory rows' });
1071
+ break;
1072
+ }
1073
+ const F = cur > 0 ? Math.max(0, prev - cur) / (cur / dof) : Infinity;
1074
+ const p = fSurvival(F, 1, dof);
1075
+ const ok = p < alpha;
1076
+ steps.push({
1077
+ ...term,
1078
+ F,
1079
+ dof,
1080
+ p,
1081
+ admitted: ok,
1082
+ reason: ok ? `admitted: F = ${F.toPrecision(3)} on (1, ${dof}), p = ${fmtP(p)} < α` : `not admitted: F = ${F.toPrecision(3)} on (1, ${dof}), p = ${fmtP(p)} ≥ α`,
1083
+ });
1084
+ if (!ok)
1085
+ break;
1086
+ admitted.push(term);
1087
+ prev = cur;
1088
+ }
1089
+ }
1090
+ return { terms: admitted, steps };
1091
+ }
1092
+ /** Infix rendering of a scalar ExprNode for reports. */
1093
+ export function exprToInfix(e) {
1094
+ if (e.kind === 'symbol')
1095
+ return e.name;
1096
+ if (e.kind === 'op') {
1097
+ const parts = e.args.map((a) => (a.kind === 'op' && a.op !== '^' ? `(${exprToInfix(a)})` : exprToInfix(a)));
1098
+ return e.op === '^' ? `${parts[0]}^${parts[1]}` : parts.join(` ${e.op === '*' ? '·' : e.op} `);
1099
+ }
1100
+ return JSON.stringify(e);
1101
+ }
1102
+ function fmtP(p) {
1103
+ if (p === null)
1104
+ return 'n/a';
1105
+ return p === 0 ? '0 (underflow)' : p < 1e-3 ? p.toExponential(1) : p.toPrecision(2);
1106
+ }
1107
+ function credibility(t, nullTest, alpha) {
1108
+ if (t.error)
1109
+ return { credible: false, credibility: t.error };
1110
+ const ex = t.exploratory;
1111
+ if (!ex || ex.pass === null) {
1112
+ return { credible: false, credibility: 'too few exploratory rows to test a fitted model (ν < 1)' };
1113
+ }
1114
+ if (!ex.pass) {
1115
+ return { credible: false, credibility: `fails on the exploratory rows (p = ${fmtP(ex.p)} < α = ${alpha})` };
1116
+ }
1117
+ if (nullTest.exploratory?.pass !== false) {
1118
+ return {
1119
+ credible: false,
1120
+ credibility: 'fits, but so does a constant: the exploratory data show no structure for it to explain',
1121
+ };
1122
+ }
1123
+ return { credible: true, credibility: `passes on the exploratory rows (p = ${fmtP(ex.p)} ≥ α = ${alpha}) and the constant model is rejected there` };
1124
+ }
1125
+ function designAgainst(study, sets, candidate, baselines) {
1126
+ if (!candidate.predict)
1127
+ return { abstained: true, reason: 'no credible candidate to discriminate' };
1128
+ const live = baselines
1129
+ .filter((b) => b.predict && b.test.exploratory?.pass === true)
1130
+ .sort((a, b) => (b.test.exploratory.p ?? 0) - (a.test.exploratory.p ?? 0));
1131
+ const best = live[0];
1132
+ if (!best) {
1133
+ return {
1134
+ abstained: true,
1135
+ reason: baselines.length === 0
1136
+ ? 'no baseline declared; nothing to discriminate the candidate against'
1137
+ : 'no declared baseline passes on the exploratory rows; none is a live competitor',
1138
+ };
1139
+ }
1140
+ const sigmas = sets.exploratory.map((r) => r.sigma).sort((a, b) => a - b);
1141
+ const sigma = sigmas[Math.floor(sigmas.length / 2)];
1142
+ const grids = study.governing.map((g) => {
1143
+ const spec = study.design?.[g.name] ?? (() => {
1144
+ const vs = sets.exploratory.map((r) => r.inputs[g.name]);
1145
+ return { min: Math.min(...vs), max: Math.max(...vs), steps: 8 };
1146
+ })();
1147
+ const steps = spec.min === spec.max ? 1 : Math.max(2, spec.steps);
1148
+ return Array.from({ length: steps }, (_, i) => (steps === 1 ? spec.min : spec.min + (i / (steps - 1)) * (spec.max - spec.min)));
1149
+ });
1150
+ let bestPoint = null;
1151
+ let bestDisc = -1;
1152
+ const idx = new Array(grids.length).fill(0);
1153
+ const visit = (d) => {
1154
+ if (d === grids.length) {
1155
+ const point = {};
1156
+ study.governing.forEach((g, i) => (point[g.name] = grids[i][idx[i]]));
1157
+ let disc;
1158
+ try {
1159
+ disc = Math.abs(candidate.predict(point) - best.predict(point)) / sigma;
1160
+ }
1161
+ catch {
1162
+ return;
1163
+ }
1164
+ if (Number.isFinite(disc) && disc > bestDisc) {
1165
+ bestDisc = disc;
1166
+ bestPoint = point;
1167
+ }
1168
+ return;
1169
+ }
1170
+ for (let i = 0; i < grids[d].length; i++) {
1171
+ idx[d] = i;
1172
+ visit(d + 1);
1173
+ }
1174
+ };
1175
+ visit(0);
1176
+ if (bestPoint === null)
1177
+ return { abstained: true, reason: 'neither model is finite anywhere in the design bounds' };
1178
+ const chosen = bestPoint;
1179
+ const maxEx = Math.max(...sets.exploratory.map((r) => Math.abs(candidate.predict(r.inputs) - best.predict(r.inputs)) / r.sigma));
1180
+ return {
1181
+ abstained: false,
1182
+ against: best.test.label,
1183
+ point: Object.fromEntries(study.governing.map((g) => [g.name, chosen[g.name] / g.scale])),
1184
+ discrimination: bestDisc,
1185
+ sigma: sigma / study.target.scale,
1186
+ maxExploratoryDiscrimination: maxEx,
1187
+ bounds: study.design ? 'declared' : 'exploratory-range',
1188
+ };
1189
+ }
1190
+ /**
1191
+ * Run a study: search and fit on exploratory rows only, then test on withheld
1192
+ * rows. Never mutates the study.
1193
+ *
1194
+ * @internal
1195
+ */
1196
+ export async function runProbeStudy(study, opts = {}) {
1197
+ const alpha = opts.alpha ?? study.alpha;
1198
+ const sets = split(study);
1199
+ const inputsWithSigma = study.governing
1200
+ .map((g) => g.name)
1201
+ .filter((n) => study.observations.some((o) => n in o.inputSigma));
1202
+ // The id hashes the data, not the file paths: the same study as CSV or JSON has one id.
1203
+ const { source: _path, replication: repFile, ...content } = study;
1204
+ const studyId = `ps-${hashCanonical({ ...content, replicationProvenance: repFile?.provenance ?? null, alpha }).slice(0, 12)}`;
1205
+ // The search sees the exploratory rows and nothing else.
1206
+ const exploratoryRows = sets.exploratory.map((r) => ({ ...r.inputs, [study.target.name]: r.observed }));
1207
+ const gap = makeResidualGap(study.gap.id, study.gap.summary, study.gap.kind);
1208
+ const problem = problemFromResidualGap(gap, { name: study.target.name, dim: study.target.dim }, study.governing.map((g) => ({ name: g.name, dim: g.dim })), datasetFromRows(exploratoryRows, study.target.name, 'exploratory-fit', `${studyId}#exploratory`), undefined);
1209
+ const search = await runProbeSearch(problem, {
1210
+ budget: opts.budget,
1211
+ runId: `dr-${studyId.slice(3)}`,
1212
+ now: '1970-01-01T00:00:00.000Z',
1213
+ });
1214
+ const nullModel = scoreModel({ id: 'null', kind: 'null', label: 'constant', basis: [() => 1], fit: true }, sets, alpha, false).test;
1215
+ const fitted = search.candidates.filter((c) => c.id in search.fits);
1216
+ const corr = study.correction;
1217
+ const perCandidate = [];
1218
+ const specs = fitted.flatMap((c) => {
1219
+ const expr = bodyExpression(c.body);
1220
+ const shape = (x) => evalExpr(expr, x);
1221
+ const label = `ĉ · ${exprToInfix(expr)}`;
1222
+ const base = { spec: { id: c.id, kind: 'candidate', label, basis: [shape], fit: true }, complexity: c.complexity.astNodes };
1223
+ if (!corr)
1224
+ return [base];
1225
+ let admission;
1226
+ try {
1227
+ admission = admitCorrection(shape, corr, sets.exploratory, alpha);
1228
+ }
1229
+ catch (e) {
1230
+ perCandidate.push({ base: c.id, steps: [], error: e.message });
1231
+ return [base];
1232
+ }
1233
+ perCandidate.push({ base: c.id, steps: admission.steps });
1234
+ const admitted = admission.terms;
1235
+ if (admitted.length === 0)
1236
+ return [base];
1237
+ const terms = admitted.map((t, j) => `c${sub(j + 1)}·${t.input}^${t.power}`).join(' + ');
1238
+ return [
1239
+ base,
1240
+ {
1241
+ spec: {
1242
+ id: `${c.id}×(1+${admitted.map((t) => `${t.input}^${t.power}`).join('+')})`,
1243
+ kind: 'candidate',
1244
+ label: `${label} · (1 + ${terms})`,
1245
+ basis: correctionBasis(shape, admitted),
1246
+ fit: true,
1247
+ },
1248
+ complexity: c.complexity.astNodes + 4 * admitted.length,
1249
+ correction: { base: c.id, terms: admitted },
1250
+ },
1251
+ ];
1252
+ });
1253
+ const correctionOf = (s, coef) => 'correction' in s && s.correction && coef
1254
+ ? {
1255
+ correction: {
1256
+ base: s.correction.base,
1257
+ terms: s.correction.terms.map((t, j) => ({ input: t.input, power: t.power, coefficient: coef[j + 1] / coef[0] })),
1258
+ },
1259
+ }
1260
+ : {};
1261
+ const scored = specs.map((s) => {
1262
+ const m = scoreModel(s.spec, sets, alpha, false);
1263
+ return { model: m, spec: s.spec, complexity: s.complexity, extra: correctionOf(s, m.coef), ...credibility(m.test, nullModel, alpha) };
1264
+ });
1265
+ const credibleOnes = scored
1266
+ .filter((s) => s.credible)
1267
+ .sort((a, b) => (b.model.test.exploratory.p - a.model.test.exploratory.p) || a.complexity - b.complexity);
1268
+ const chosen = credibleOnes[0];
1269
+ const baselines = study.baselines.map((b) => {
1270
+ const f = parseFormula(b.formula);
1271
+ return scoreModel({ id: `baseline:${b.name}`, kind: 'baseline', label: b.name, basis: [(x) => f.evaluate({ ...x })], fit: b.fitPrefactor }, sets, alpha, true);
1272
+ });
1273
+ // Design is computed from exploratory fits only, before any withheld row is read.
1274
+ const design = chosen
1275
+ ? designAgainst(study, sets, chosen.model, baselines)
1276
+ : { abstained: true, reason: 'no credible candidate to discriminate' };
1277
+ const verdictReasons = [];
1278
+ let verdict;
1279
+ let replication = sets.replication.length === 0 ? 'no-replication-data' : 'not-tested';
1280
+ let selected = null;
1281
+ const candidates = scored.map((s) => ({ ...s.model.test, credible: s.credible, credibility: s.credibility, ...s.extra }));
1282
+ if (!chosen) {
1283
+ verdict = 'no-credible-candidate';
1284
+ if (fitted.length === 0) {
1285
+ verdictReasons.push(`the search produced no candidate that could be fit (search stop: ${search.stopReason})`);
1286
+ }
1287
+ else {
1288
+ for (const s of scored)
1289
+ verdictReasons.push(`${s.model.test.id}: ${s.credibility}`);
1290
+ }
1291
+ verdictReasons.push('the withheld rows were not consulted for any candidate');
1292
+ }
1293
+ else {
1294
+ const withheld = scoreModel(chosen.spec, sets, alpha, true).test;
1295
+ selected = { ...withheld, credible: true, credibility: chosen.credibility, ...chosen.extra };
1296
+ const i = candidates.findIndex((c) => c.id === selected.id);
1297
+ candidates[i] = selected;
1298
+ const h = selected.holdout;
1299
+ if (!h) {
1300
+ verdict = 'untested-on-holdout';
1301
+ verdictReasons.push('the file has no holdout rows; the candidate is credible on exploratory data only');
1302
+ }
1303
+ else if (h.pass) {
1304
+ verdict = 'survives-holdout';
1305
+ const frozen = selected.fittedParameters > 1 ? 'coefficients' : 'prefactor';
1306
+ verdictReasons.push(`holdout χ² = ${h.chi2.toPrecision(3)} on ν = ${h.dof} (p = ${fmtP(h.p)} ≥ α = ${alpha}) with the exploratory ${frozen} frozen`);
1307
+ }
1308
+ else {
1309
+ verdict = 'refuted-on-holdout';
1310
+ verdictReasons.push(`holdout χ² = ${h.chi2.toPrecision(3)} on ν = ${h.dof} (p = ${fmtP(h.p)} < α = ${alpha}); ` +
1311
+ `worst row ${h.worstRow} at ${h.maxAbsZ.toPrecision(3)}σ`);
1312
+ }
1313
+ const rep = selected.replication;
1314
+ if (rep)
1315
+ replication = rep.pass ? 'survives-replication' : 'refuted-on-replication';
1316
+ }
1317
+ // Rows closer to the study's than independent data can be are not a replication, whatever they agree with.
1318
+ const independence = replicationIndependence(study.observations, alpha);
1319
+ if (independence.tooClose)
1320
+ replication = 'too-close';
1321
+ const corrInputs = corr?.map((c) => `'${c.input}'`).join(', ') ?? '';
1322
+ const sharedCalibration = repFile?.provenance.calibration !== undefined &&
1323
+ study.provenance.calibration !== undefined &&
1324
+ provenanceKey(repFile.provenance.calibration) === provenanceKey(study.provenance.calibration);
1325
+ const caveats = [
1326
+ 'A fit is not a mechanism: a candidate that survives a holdout was not refuted by these rows; ' +
1327
+ 'that does not make it the causal explanation.',
1328
+ 'Every test assumes independent Gaussian errors with the declared σ; a mis-stated σ changes every verdict.',
1329
+ ...(study.baselines.length > 0
1330
+ ? ['The baselines are the file author\'s; UPT cannot verify they were chosen before the holdout was seen.']
1331
+ : []),
1332
+ ...(study.provenance.synthetic
1333
+ ? ['SYNTHETIC DATA: these rows were generated, not measured. A result on them is a control of the method, not evidence about nature.']
1334
+ : ['The file declares these rows measured; UPT has not verified that provenance.']),
1335
+ ...(study.provenance.synthetic || repFile?.provenance.synthetic
1336
+ ? ['A synthetic control whose generating law was chosen knowing the answer shows only that the method recovers ' +
1337
+ 'that law; it is not a blind test of finding an unknown one. UPT ships no blind control.']
1338
+ : []),
1339
+ ...(repFile
1340
+ ? [`The replication rows come from ${repFile.source}, whose provenance is its author's declaration. UPT checked only ` +
1341
+ 'that its source and acquisition differ from the study\'s (ignoring case, spacing and punctuation), that no row ' +
1342
+ 'repeats a study row\'s data exactly, and that rows at a study row\'s inputs are not closer to it than their σ ' +
1343
+ 'allows, as an identical or an affine (rescaled or shifted) copy, each tested at α/2. A copy at altered inputs, ' +
1344
+ 'or of rows the study does not contain, is not caught.']
1345
+ : sets.replication.length > 0
1346
+ ? ['The replication rows are in the study file and share its provenance block (calibration included); only their ' +
1347
+ 'row source differs from the fit rows\'.']
1348
+ : []),
1349
+ ...(sharedCalibration
1350
+ ? [`The replication file declares the study's own calibration ('${study.provenance.calibration}'): whatever that ` +
1351
+ 'calibration gets wrong, both share, so the replication does not test it.']
1352
+ : []),
1353
+ ...(inputsWithSigma.length > 0
1354
+ ? ['Input σ is propagated to first order (effective variance): exact for a model linear in the input, ' +
1355
+ 'approximate when the model curves appreciably over ±σ_x.']
1356
+ : []),
1357
+ ...(corr && corr.length === 1
1358
+ ? [`The correction family in ${corrInputs} was declared by the file; only that family was searched, and UPT ` +
1359
+ 'cannot verify it was declared before the data were seen.']
1360
+ : corr
1361
+ ? [`The correction families in ${corrInputs} were declared by the file; only those families were searched, and UPT ` +
1362
+ 'cannot verify they were declared before the data were seen. The families are additive: a cross term (u·v) ' +
1363
+ 'is not in them, and the order they were declared in decides which terms are tried first.']
1364
+ : []),
1365
+ ];
1366
+ return {
1367
+ studyId,
1368
+ source: study.source,
1369
+ provenance: study.provenance,
1370
+ alpha,
1371
+ target: { name: study.target.name, unit: study.target.unit },
1372
+ governing: study.governing.map((g) => ({ name: g.name, unit: g.unit })),
1373
+ counts: { exploratory: sets.exploratory.length, holdout: sets.holdout.length, replication: sets.replication.length },
1374
+ ...(repFile ? { replicationFile: repFile } : {}),
1375
+ uncertainty: { method: inputsWithSigma.length > 0 ? 'effective-variance' : 'output-only', inputs: inputsWithSigma },
1376
+ correction: corr
1377
+ ? {
1378
+ families: corr,
1379
+ criterion: `m(x)·(1 + ${corr.map((c) => `Σ c_k·${c.input}^p_k`).join(' + ')}) for each searched monomial m, fit on the ` +
1380
+ 'exploratory rows only; terms admitted in the declared order' +
1381
+ (corr.length > 1 ? ', family by family (the first term a family fails ends that family),' : '') +
1382
+ ` while each passes an extra-sum-of-squares F test at p < α = ${alpha}`,
1383
+ perCandidate,
1384
+ }
1385
+ : null,
1386
+ search: {
1387
+ stopReason: search.stopReason,
1388
+ generated: search.candidates.length,
1389
+ fitted: fitted.length,
1390
+ rejected: search.rejections.length,
1391
+ // The search is given no withheld rows by design; the study tests them itself.
1392
+ notes: [...new Set(search.wording)].filter((w) => w !== NO_HOLDOUT_WORDING),
1393
+ },
1394
+ nullModel,
1395
+ candidates,
1396
+ selected: selected?.id ?? null,
1397
+ verdict,
1398
+ verdictReasons,
1399
+ replication,
1400
+ replicationIndependence: independence,
1401
+ baselines: baselines.map((b) => b.test),
1402
+ design,
1403
+ caveats,
1404
+ schemaVersion: SCHEMA_VERSION,
1405
+ };
1406
+ }
1407
+ // --- report ----------------------------------------------------------------
1408
+ function setLine(name, t) {
1409
+ if (!t)
1410
+ return `${name}: no rows`;
1411
+ const verdict = t.pass === null ? 'undecidable (ν < 1)' : t.pass ? 'pass' : 'FAIL';
1412
+ const nu = t.dof >= 1 ? `χ²/ν = ${(t.chi2 / t.dof).toPrecision(3)} (ν = ${t.dof})` : `χ² = ${t.chi2.toPrecision(3)}`;
1413
+ return `${name}: ${nu}, p = ${fmtP(t.p)}, max |z| = ${t.maxAbsZ.toPrecision(3)}${t.worstRow ? ` at ${t.worstRow}` : ''} — ${verdict}`;
1414
+ }
1415
+ function fmtPoint(point, governing) {
1416
+ return governing.map((g) => `${g.name}=${Number(point[g.name].toPrecision(4))}${g.unit ? ` ${g.unit}` : ''}`).join(', ');
1417
+ }
1418
+ /** Scientist-facing study report. @internal */
1419
+ export function formatProbeStudy(r) {
1420
+ const L = [];
1421
+ L.push(`upt probe study — fit on exploratory rows, test on withheld rows [study ${r.studyId}]`);
1422
+ L.push('⚠ experimental Product B. Not a discovery claim. `upt discover` is the identification funnel.');
1423
+ if (r.provenance.synthetic) {
1424
+ L.push(`⚠ SYNTHETIC DATA — generated, not measured. source: ${r.provenance.source}`);
1425
+ }
1426
+ else {
1427
+ L.push(` data declared measured (not verified by UPT). source: ${r.provenance.source}`);
1428
+ }
1429
+ if (r.provenance.acquisition)
1430
+ L.push(` acquisition: ${r.provenance.acquisition}`);
1431
+ if (r.provenance.calibration)
1432
+ L.push(` calibration: ${r.provenance.calibration}`);
1433
+ L.push(` rows: ${r.counts.exploratory} exploratory (searched and fit) · ${r.counts.holdout} holdout (withheld) · ` +
1434
+ `${r.counts.replication} replication (withheld, independent source)`);
1435
+ const rf = r.replicationFile;
1436
+ if (rf) {
1437
+ L.push(rf.provenance.synthetic
1438
+ ? ` replication file: ${rf.source} — ⚠ SYNTHETIC DATA, generated, not measured. source: ${rf.provenance.source}`
1439
+ : ` replication file: ${rf.source} — declared measured (not verified by UPT). source: ${rf.provenance.source}`);
1440
+ if (rf.provenance.acquisition)
1441
+ L.push(` acquisition: ${rf.provenance.acquisition}`);
1442
+ }
1443
+ L.push(` criterion: χ² with the declared σ at α = ${r.alpha}. A candidate is credible only if it passes on the ` +
1444
+ 'exploratory rows and a constant model is rejected there.');
1445
+ L.push(r.uncertainty.method === 'effective-variance'
1446
+ ? ` uncertainty: σ_eff² = σ_y² + Σ_i (∂f/∂x_i · σ_x_i)² over {${r.uncertainty.inputs.join(', ')}} (effective variance; ` +
1447
+ 'each model\'s own slopes at the recorded inputs, fits reweighted to a fixed point; first order in σ_x)'
1448
+ : ' uncertainty: σ on the target only; the inputs are taken as exact');
1449
+ L.push('');
1450
+ L.push(` search (exploratory rows only): ${r.search.generated} candidate(s) generated, ${r.search.fitted} fit, ` +
1451
+ `${r.search.rejected} rejected; stop: ${r.search.stopReason}`);
1452
+ for (const n of r.search.notes.slice(0, 6))
1453
+ L.push(` ${n}`);
1454
+ if (r.correction) {
1455
+ L.push(` declared correction ${r.correction.families.length > 1 ? 'families' : 'family'}: ${r.correction.criterion}`);
1456
+ for (const pc of r.correction.perCandidate) {
1457
+ const steps = pc.error ?? (pc.steps.map((s) => `${s.input}^${s.power} ${s.reason}`).join('; ') || 'no term tested');
1458
+ L.push(` on ${pc.base}: ${steps}`);
1459
+ }
1460
+ }
1461
+ L.push(` null model (constant ${r.target.name}) — ${setLine('exploratory', r.nullModel.exploratory)}`);
1462
+ L.push('');
1463
+ for (const c of r.candidates) {
1464
+ const pre = c.prefactor === null ? '' : `, ĉ = ${c.prefactor.toPrecision(5)} (SI)`;
1465
+ L.push(` candidate ${c.id}: ${c.label}${pre}`);
1466
+ if (c.correction) {
1467
+ L.push(` ${c.correction.terms.map((t, j) => `c${sub(j + 1)} = ${t.coefficient.toPrecision(4)}`).join(', ')} (fit on exploratory rows)`);
1468
+ }
1469
+ L.push(` ${setLine('exploratory', c.exploratory)}`);
1470
+ L.push(` ${c.credible ? 'credible' : 'not credible'}: ${c.credibility}`);
1471
+ if (c.id === r.selected) {
1472
+ L.push(` ${setLine('holdout', c.holdout)}`);
1473
+ L.push(` ${setLine('replication', c.replication)}`);
1474
+ }
1475
+ }
1476
+ if (r.candidates.length === 0)
1477
+ L.push(' no candidate could be fit.');
1478
+ if (r.baselines.length > 0) {
1479
+ L.push('');
1480
+ L.push(' baselines (declared by the file; same rows, same criterion):');
1481
+ for (const b of r.baselines) {
1482
+ const how = b.fittedParameters === 0 ? 'fixed, no fitted parameter' : `prefactor fit on exploratory rows = ${b.prefactor?.toPrecision(5)}`;
1483
+ L.push(` ${b.label} [${how}]`);
1484
+ if (b.error) {
1485
+ L.push(` not evaluable: ${b.error}`);
1486
+ continue;
1487
+ }
1488
+ for (const [name, t] of [['exploratory', b.exploratory], ['holdout', b.holdout], ['replication', b.replication]]) {
1489
+ L.push(` ${setLine(name, t)}`);
1490
+ }
1491
+ }
1492
+ }
1493
+ L.push('');
1494
+ const d = r.design;
1495
+ L.push(' discriminating measurement (a suggestion, not evidence):');
1496
+ if (d.abstained) {
1497
+ L.push(` abstained: ${d.reason}`);
1498
+ }
1499
+ else {
1500
+ L.push(` the candidate and '${d.against}' differ most at ${fmtPoint(d.point, r.governing)}: ` +
1501
+ `${d.discrimination.toPrecision(3)}σ (σ = ${Number(d.sigma.toPrecision(3))} ${r.target.unit}, the median exploratory ` +
1502
+ `${r.uncertainty.method === 'effective-variance' ? 'target σ, input σ not included' : 'σ'}; ` +
1503
+ `${d.bounds === 'declared' ? 'declared design bounds' : 'bounds = exploratory range; declare "design" to look beyond it'})`);
1504
+ L.push(` on the exploratory rows they differ by at most ${d.maxExploratoryDiscrimination.toPrecision(2)}σ` +
1505
+ (d.maxExploratoryDiscrimination < 1 ? ': those rows cannot tell them apart' : ''));
1506
+ }
1507
+ L.push('');
1508
+ L.push(` verdict: ${r.verdict}`);
1509
+ for (const why of r.verdictReasons)
1510
+ L.push(` ${why}`);
1511
+ L.push(` replication: ${r.replication}`);
1512
+ if (r.counts.replication > 0)
1513
+ L.push(` closeness: ${r.replicationIndependence.reason}`);
1514
+ L.push('');
1515
+ for (const c of r.caveats)
1516
+ L.push(` ⚠ ${c}`);
1517
+ return L.join('\n');
1518
+ }
1519
+ //# sourceMappingURL=study.js.map