cbrowser 20.2.2 → 21.0.0

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 (302) hide show
  1. package/README.md +2 -2
  2. package/dist/analysis/accessibility-empathy.d.ts +29 -1
  3. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  4. package/dist/analysis/accessibility-empathy.js +69 -35
  5. package/dist/analysis/accessibility-empathy.js.map +1 -1
  6. package/dist/browser.d.ts.map +1 -1
  7. package/dist/browser.js +11 -7
  8. package/dist/browser.js.map +1 -1
  9. package/dist/cif-score.d.ts +56 -5
  10. package/dist/cif-score.d.ts.map +1 -1
  11. package/dist/cif-score.js +595 -346
  12. package/dist/cif-score.js.map +1 -1
  13. package/dist/cli.js +10 -8
  14. package/dist/cli.js.map +1 -1
  15. package/dist/cognitive/default-traits.d.ts +14 -0
  16. package/dist/cognitive/default-traits.d.ts.map +1 -0
  17. package/dist/cognitive/default-traits.js +36 -0
  18. package/dist/cognitive/default-traits.js.map +1 -0
  19. package/dist/cognitive/emotions.d.ts.map +1 -1
  20. package/dist/cognitive/emotions.js +104 -174
  21. package/dist/cognitive/emotions.js.map +1 -1
  22. package/dist/cognitive/index.d.ts +28 -0
  23. package/dist/cognitive/index.d.ts.map +1 -1
  24. package/dist/cognitive/index.js +353 -287
  25. package/dist/cognitive/index.js.map +1 -1
  26. package/dist/cognitive/temporal.d.ts +0 -24
  27. package/dist/cognitive/temporal.d.ts.map +1 -1
  28. package/dist/cognitive/temporal.js +45 -35
  29. package/dist/cognitive/temporal.js.map +1 -1
  30. package/dist/data/ctc-reference-v2-2026-10-24.d.ts +16 -0
  31. package/dist/data/ctc-reference-v2-2026-10-24.d.ts.map +1 -0
  32. package/dist/data/ctc-reference-v2-2026-10-24.js +1004 -0
  33. package/dist/data/ctc-reference-v2-2026-10-24.js.map +1 -0
  34. package/dist/ideal-profiles-gap.d.ts +1 -0
  35. package/dist/ideal-profiles-gap.d.ts.map +1 -1
  36. package/dist/ideal-profiles-gap.js +9 -1
  37. package/dist/ideal-profiles-gap.js.map +1 -1
  38. package/dist/ideal-profiles.d.ts +8 -0
  39. package/dist/ideal-profiles.d.ts.map +1 -1
  40. package/dist/ideal-profiles.js +28 -24
  41. package/dist/ideal-profiles.js.map +1 -1
  42. package/dist/mcp-server-remote.d.ts.map +1 -1
  43. package/dist/mcp-server-remote.js +7 -8
  44. package/dist/mcp-server-remote.js.map +1 -1
  45. package/dist/mcp-tools/account-personas.d.ts.map +1 -1
  46. package/dist/mcp-tools/account-personas.js +3 -1
  47. package/dist/mcp-tools/account-personas.js.map +1 -1
  48. package/dist/mcp-tools/base/answer-provenance.d.ts +28 -0
  49. package/dist/mcp-tools/base/answer-provenance.d.ts.map +1 -0
  50. package/dist/mcp-tools/base/answer-provenance.js +32 -0
  51. package/dist/mcp-tools/base/answer-provenance.js.map +1 -0
  52. package/dist/mcp-tools/base/audit-tools.d.ts +0 -3
  53. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  54. package/dist/mcp-tools/base/audit-tools.js +152 -84
  55. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  56. package/dist/mcp-tools/base/capture-tools.js +1 -1
  57. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  58. package/dist/mcp-tools/base/chain-values.d.ts +38 -0
  59. package/dist/mcp-tools/base/chain-values.d.ts.map +1 -0
  60. package/dist/mcp-tools/base/chain-values.js +85 -0
  61. package/dist/mcp-tools/base/chain-values.js.map +1 -0
  62. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
  63. package/dist/mcp-tools/base/cognitive-tools.js +126 -120
  64. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
  65. package/dist/mcp-tools/base/gif-tools.d.ts.map +1 -1
  66. package/dist/mcp-tools/base/gif-tools.js +23 -5
  67. package/dist/mcp-tools/base/gif-tools.js.map +1 -1
  68. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
  69. package/dist/mcp-tools/base/persona-comparison-tools.js +229 -172
  70. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
  71. package/dist/mcp-tools/base/site-familiarity.d.ts +31 -0
  72. package/dist/mcp-tools/base/site-familiarity.d.ts.map +1 -0
  73. package/dist/mcp-tools/base/site-familiarity.js +30 -0
  74. package/dist/mcp-tools/base/site-familiarity.js.map +1 -0
  75. package/dist/mcp-tools/base/site-knowledge-tools.js +1 -1
  76. package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
  77. package/dist/mcp-tools/base/story-narrative.d.ts +50 -0
  78. package/dist/mcp-tools/base/story-narrative.d.ts.map +1 -0
  79. package/dist/mcp-tools/base/story-narrative.js +88 -0
  80. package/dist/mcp-tools/base/story-narrative.js.map +1 -0
  81. package/dist/mcp-tools/base/values-tools.d.ts.map +1 -1
  82. package/dist/mcp-tools/base/values-tools.js +19 -5
  83. package/dist/mcp-tools/base/values-tools.js.map +1 -1
  84. package/dist/mcp-tools/base/visual-testing-tools.d.ts +5 -5
  85. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  86. package/dist/mcp-tools/base/visual-testing-tools.js +38 -11
  87. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  88. package/dist/mcp-tools/persona-creation-tools.js +6 -6
  89. package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
  90. package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -1
  91. package/dist/mcp-tools/persona-lifecycle-tools.js +5 -1
  92. package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -1
  93. package/dist/mcp-tools/tier-gate.js +1 -1
  94. package/dist/mcp-tools/tool-result-saver.d.ts +7 -0
  95. package/dist/mcp-tools/tool-result-saver.d.ts.map +1 -1
  96. package/dist/mcp-tools/tool-result-saver.js +11 -0
  97. package/dist/mcp-tools/tool-result-saver.js.map +1 -1
  98. package/dist/mcp-tools/trait-provenance.d.ts +109 -0
  99. package/dist/mcp-tools/trait-provenance.d.ts.map +1 -0
  100. package/dist/mcp-tools/trait-provenance.js +187 -0
  101. package/dist/mcp-tools/trait-provenance.js.map +1 -0
  102. package/dist/mcp-tools/ui-resources.d.ts +6 -7
  103. package/dist/mcp-tools/ui-resources.d.ts.map +1 -1
  104. package/dist/mcp-tools/ui-resources.js +162 -12
  105. package/dist/mcp-tools/ui-resources.js.map +1 -1
  106. package/dist/mcp-tools/widget-kit.d.ts +129 -3
  107. package/dist/mcp-tools/widget-kit.d.ts.map +1 -1
  108. package/dist/mcp-tools/widget-kit.js +507 -28
  109. package/dist/mcp-tools/widget-kit.js.map +1 -1
  110. package/dist/performance/metrics.d.ts.map +1 -1
  111. package/dist/performance/metrics.js +4 -1
  112. package/dist/performance/metrics.js.map +1 -1
  113. package/dist/persona-questionnaire.js +1 -1
  114. package/dist/persona-questionnaire.js.map +1 -1
  115. package/dist/personas.d.ts.map +1 -1
  116. package/dist/personas.js +133 -82
  117. package/dist/personas.js.map +1 -1
  118. package/dist/recording/engine.d.ts.map +1 -1
  119. package/dist/recording/engine.js +5 -2
  120. package/dist/recording/engine.js.map +1 -1
  121. package/dist/recording/saliency-overlay.d.ts +8 -3
  122. package/dist/recording/saliency-overlay.d.ts.map +1 -1
  123. package/dist/recording/saliency-overlay.js +14 -5
  124. package/dist/recording/saliency-overlay.js.map +1 -1
  125. package/dist/recording/types.js +1 -1
  126. package/dist/testing/nl-test-suite.d.ts.map +1 -1
  127. package/dist/testing/nl-test-suite.js +2 -1
  128. package/dist/testing/nl-test-suite.js.map +1 -1
  129. package/dist/trait-reference.d.ts +19 -2
  130. package/dist/trait-reference.d.ts.map +1 -1
  131. package/dist/trait-reference.js +19 -2
  132. package/dist/trait-reference.js.map +1 -1
  133. package/dist/types.d.ts +32 -21
  134. package/dist/types.d.ts.map +1 -1
  135. package/dist/types.js +62 -52
  136. package/dist/types.js.map +1 -1
  137. package/dist/values/persona-completeness.js +1 -1
  138. package/dist/values/persona-completeness.js.map +1 -1
  139. package/dist/visual/ab-comparison.d.ts.map +1 -1
  140. package/dist/visual/ab-comparison.js +4 -1
  141. package/dist/visual/ab-comparison.js.map +1 -1
  142. package/dist/visual/attention-quality.d.ts +1 -1
  143. package/dist/visual/attention-quality.js +1 -1
  144. package/dist/visual/attention-transport.d.ts +74 -10
  145. package/dist/visual/attention-transport.d.ts.map +1 -1
  146. package/dist/visual/attention-transport.js +353 -193
  147. package/dist/visual/attention-transport.js.map +1 -1
  148. package/dist/visual/cognitive-models.d.ts +77 -55
  149. package/dist/visual/cognitive-models.d.ts.map +1 -1
  150. package/dist/visual/cognitive-models.js +231 -104
  151. package/dist/visual/cognitive-models.js.map +1 -1
  152. package/dist/visual/cognitive-transport-chain.d.ts +328 -30
  153. package/dist/visual/cognitive-transport-chain.d.ts.map +1 -1
  154. package/dist/visual/cognitive-transport-chain.js +576 -353
  155. package/dist/visual/cognitive-transport-chain.js.map +1 -1
  156. package/dist/visual/cognitive-transport.d.ts +446 -13
  157. package/dist/visual/cognitive-transport.d.ts.map +1 -1
  158. package/dist/visual/cognitive-transport.js +1085 -245
  159. package/dist/visual/cognitive-transport.js.map +1 -1
  160. package/dist/visual/cross-browser.d.ts.map +1 -1
  161. package/dist/visual/cross-browser.js +8 -6
  162. package/dist/visual/cross-browser.js.map +1 -1
  163. package/dist/visual/ctc-bands.d.ts +91 -0
  164. package/dist/visual/ctc-bands.d.ts.map +1 -0
  165. package/dist/visual/ctc-bands.js +117 -0
  166. package/dist/visual/ctc-bands.js.map +1 -0
  167. package/dist/visual/ctc-reference.d.ts +287 -0
  168. package/dist/visual/ctc-reference.d.ts.map +1 -0
  169. package/dist/visual/ctc-reference.js +376 -0
  170. package/dist/visual/ctc-reference.js.map +1 -0
  171. package/dist/visual/demand-laws.d.ts +145 -0
  172. package/dist/visual/demand-laws.d.ts.map +1 -0
  173. package/dist/visual/demand-laws.js +220 -0
  174. package/dist/visual/demand-laws.js.map +1 -0
  175. package/dist/visual/distance-metrics.d.ts +23 -1
  176. package/dist/visual/distance-metrics.d.ts.map +1 -1
  177. package/dist/visual/distance-metrics.js +49 -18
  178. package/dist/visual/distance-metrics.js.map +1 -1
  179. package/dist/visual/dro-bound.d.ts +220 -0
  180. package/dist/visual/dro-bound.d.ts.map +1 -0
  181. package/dist/visual/dro-bound.js +341 -0
  182. package/dist/visual/dro-bound.js.map +1 -0
  183. package/dist/visual/exact-w1.d.ts +60 -0
  184. package/dist/visual/exact-w1.d.ts.map +1 -0
  185. package/dist/visual/exact-w1.js +247 -0
  186. package/dist/visual/exact-w1.js.map +1 -0
  187. package/dist/visual/human-input.d.ts.map +1 -1
  188. package/dist/visual/human-input.js +45 -41
  189. package/dist/visual/human-input.js.map +1 -1
  190. package/dist/visual/index.d.ts +2 -0
  191. package/dist/visual/index.d.ts.map +1 -1
  192. package/dist/visual/index.js +2 -0
  193. package/dist/visual/index.js.map +1 -1
  194. package/dist/visual/law-times-report.d.ts +49 -0
  195. package/dist/visual/law-times-report.d.ts.map +1 -0
  196. package/dist/visual/law-times-report.js +38 -0
  197. package/dist/visual/law-times-report.js.map +1 -0
  198. package/dist/visual/model-provenance.d.ts +151 -0
  199. package/dist/visual/model-provenance.d.ts.map +1 -0
  200. package/dist/visual/model-provenance.js +334 -0
  201. package/dist/visual/model-provenance.js.map +1 -0
  202. package/dist/visual/normal-cdf.d.ts +14 -0
  203. package/dist/visual/normal-cdf.d.ts.map +1 -0
  204. package/dist/visual/normal-cdf.js +26 -0
  205. package/dist/visual/normal-cdf.js.map +1 -0
  206. package/dist/visual/perceptual-transport.d.ts +28 -4
  207. package/dist/visual/perceptual-transport.d.ts.map +1 -1
  208. package/dist/visual/perceptual-transport.js +66 -32
  209. package/dist/visual/perceptual-transport.js.map +1 -1
  210. package/dist/visual/provenance/attention.d.ts +55 -0
  211. package/dist/visual/provenance/attention.d.ts.map +1 -0
  212. package/dist/visual/provenance/attention.js +188 -0
  213. package/dist/visual/provenance/attention.js.map +1 -0
  214. package/dist/visual/provenance/chain.d.ts +41 -0
  215. package/dist/visual/provenance/chain.d.ts.map +1 -0
  216. package/dist/visual/provenance/chain.js +66 -0
  217. package/dist/visual/provenance/chain.js.map +1 -0
  218. package/dist/visual/provenance/cif.d.ts +42 -0
  219. package/dist/visual/provenance/cif.d.ts.map +1 -0
  220. package/dist/visual/provenance/cif.js +116 -0
  221. package/dist/visual/provenance/cif.js.map +1 -0
  222. package/dist/visual/provenance/ctc-reference.d.ts +36 -0
  223. package/dist/visual/provenance/ctc-reference.d.ts.map +1 -0
  224. package/dist/visual/provenance/ctc-reference.js +96 -0
  225. package/dist/visual/provenance/ctc-reference.js.map +1 -0
  226. package/dist/visual/provenance/distance.d.ts +10 -0
  227. package/dist/visual/provenance/distance.d.ts.map +1 -0
  228. package/dist/visual/provenance/distance.js +72 -0
  229. package/dist/visual/provenance/distance.js.map +1 -0
  230. package/dist/visual/provenance/dro.d.ts +10 -0
  231. package/dist/visual/provenance/dro.d.ts.map +1 -0
  232. package/dist/visual/provenance/dro.js +2 -0
  233. package/dist/visual/provenance/dro.js.map +1 -0
  234. package/dist/visual/provenance/emotion.d.ts +26 -0
  235. package/dist/visual/provenance/emotion.d.ts.map +1 -0
  236. package/dist/visual/provenance/emotion.js +99 -0
  237. package/dist/visual/provenance/emotion.js.map +1 -0
  238. package/dist/visual/provenance/human-input.d.ts +19 -0
  239. package/dist/visual/provenance/human-input.d.ts.map +1 -0
  240. package/dist/visual/provenance/human-input.js +84 -0
  241. package/dist/visual/provenance/human-input.js.map +1 -0
  242. package/dist/visual/provenance/journey-group.d.ts +15 -0
  243. package/dist/visual/provenance/journey-group.d.ts.map +1 -0
  244. package/dist/visual/provenance/journey-group.js +15 -0
  245. package/dist/visual/provenance/journey-group.js.map +1 -0
  246. package/dist/visual/provenance/journey-mcp.d.ts +24 -0
  247. package/dist/visual/provenance/journey-mcp.d.ts.map +1 -0
  248. package/dist/visual/provenance/journey-mcp.js +54 -0
  249. package/dist/visual/provenance/journey-mcp.js.map +1 -0
  250. package/dist/visual/provenance/journey.d.ts +34 -0
  251. package/dist/visual/provenance/journey.d.ts.map +1 -0
  252. package/dist/visual/provenance/journey.js +184 -0
  253. package/dist/visual/provenance/journey.js.map +1 -0
  254. package/dist/visual/provenance/laws.d.ts +38 -0
  255. package/dist/visual/provenance/laws.d.ts.map +1 -0
  256. package/dist/visual/provenance/laws.js +40 -0
  257. package/dist/visual/provenance/laws.js.map +1 -0
  258. package/dist/visual/provenance/page-metrics.d.ts +42 -0
  259. package/dist/visual/provenance/page-metrics.d.ts.map +1 -0
  260. package/dist/visual/provenance/page-metrics.js +79 -0
  261. package/dist/visual/provenance/page-metrics.js.map +1 -0
  262. package/dist/visual/provenance/perceptual.d.ts +63 -0
  263. package/dist/visual/provenance/perceptual.d.ts.map +1 -0
  264. package/dist/visual/provenance/perceptual.js +115 -0
  265. package/dist/visual/provenance/perceptual.js.map +1 -0
  266. package/dist/visual/provenance/persona-rates.d.ts +84 -0
  267. package/dist/visual/provenance/persona-rates.d.ts.map +1 -0
  268. package/dist/visual/provenance/persona-rates.js +112 -0
  269. package/dist/visual/provenance/persona-rates.js.map +1 -0
  270. package/dist/visual/provenance/personas.d.ts +122 -0
  271. package/dist/visual/provenance/personas.d.ts.map +1 -0
  272. package/dist/visual/provenance/personas.js +298 -0
  273. package/dist/visual/provenance/personas.js.map +1 -0
  274. package/dist/visual/provenance/profiles.d.ts +39 -0
  275. package/dist/visual/provenance/profiles.d.ts.map +1 -0
  276. package/dist/visual/provenance/profiles.js +37 -0
  277. package/dist/visual/provenance/profiles.js.map +1 -0
  278. package/dist/visual/provenance/temporal.d.ts +21 -0
  279. package/dist/visual/provenance/temporal.d.ts.map +1 -0
  280. package/dist/visual/provenance/temporal.js +56 -0
  281. package/dist/visual/provenance/temporal.js.map +1 -0
  282. package/dist/visual/provenance/visual-distance.d.ts +22 -0
  283. package/dist/visual/provenance/visual-distance.d.ts.map +1 -0
  284. package/dist/visual/provenance/visual-distance.js +30 -0
  285. package/dist/visual/provenance/visual-distance.js.map +1 -0
  286. package/dist/visual/regression.d.ts.map +1 -1
  287. package/dist/visual/regression.js +14 -9
  288. package/dist/visual/regression.js.map +1 -1
  289. package/dist/visual/responsive.d.ts.map +1 -1
  290. package/dist/visual/responsive.js +4 -1
  291. package/dist/visual/responsive.js.map +1 -1
  292. package/dist/visual/scope-prose.js +2 -2
  293. package/dist/visual/scope-prose.js.map +1 -1
  294. package/dist/visual/seeded-random.d.ts +16 -0
  295. package/dist/visual/seeded-random.d.ts.map +1 -0
  296. package/dist/visual/seeded-random.js +25 -0
  297. package/dist/visual/seeded-random.js.map +1 -0
  298. package/dist/visual/stored-accessibility-traits.d.ts +27 -0
  299. package/dist/visual/stored-accessibility-traits.d.ts.map +1 -0
  300. package/dist/visual/stored-accessibility-traits.js +35 -0
  301. package/dist/visual/stored-accessibility-traits.js.map +1 -0
  302. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
  * Cognitive Optimal Transport — Sequential Transport Chain
3
3
  *
4
4
  * Implements the CHI 2027 paper's core contribution:
5
- * - 26-dimensional demand distribution computed from page features
5
+ * - 29-dimensional demand distribution computed from page features
6
6
  * - Sequential transport chain where each layer depletes capacity
7
7
  * - Asymmetric deficit-surplus cost
8
8
  * - Layer interaction terms
@@ -14,13 +14,24 @@
14
14
  * @see Section 3.4 of "Cognitive Optimal Transport: A Unified Framework"
15
15
  * @since v18.39.0
16
16
  */
17
- import { extractPageMetrics, } from './cognitive-transport.js';
17
+ import { deriveMetrics, extractPageMetrics, } from './cognitive-transport.js';
18
+ import { activeOverrides, param, readsOf } from './model-provenance.js';
19
+ import { timeDemand } from './demand-laws.js';
20
+ import { bootstrapPercentileInterval, intervalAt, percentileAt, percentileCurve, referencePopulation, referenceRaw, } from './ctc-reference.js';
18
21
  // ── Constants ──
19
22
  /**
20
- * The 26 demand dimensions used by the sequential transport chain.
21
- * Superset of the base 25 OT cognitive traits — includes extended traits
22
- * from CognitiveTraits that capture decision biases, learning transfer,
23
- * and emotional regulation.
23
+ * The 29 demand dimensions used by the sequential transport chain, covering
24
+ * the core capacities plus decision biases, learning transfer, emotional
25
+ * regulation and the bridged reading/motor/attention capacities.
26
+ *
27
+ * NOT the same space as COGNITIVE_TRAITS (29). Four dimensions exist only here
28
+ * (attentionPattern, motorPrecision, processingSpeed, textProcessing: no persona
29
+ * in the tool path carries them, so they read the missing-trait fallback, and
30
+ * none is in a layer). Four traits exist only there (attributionStyle,
31
+ * timeHorizon, authoritySensitivity: no demand term; siteFamiliarity: a visit
32
+ * state the demand reads, DemandOptions, since 21.0). Both differences are
33
+ * pinned in tests/transport-dimension-alignment.test.ts, so a new one fails a
34
+ * test; nothing asserts the two sets are equal.
24
35
  */
25
36
  export const DEMAND_DIMENSIONS = [
26
37
  // Core capacity traits
@@ -44,19 +55,11 @@ export const DEMAND_DIMENSIONS = [
44
55
  'mentalModelFlexibility', 'curiosity',
45
56
  // Processing
46
57
  'processingSpeed', 'textProcessing',
47
- // Experience
48
- //
49
- // Absent until 2026-08-02, which is why the siteFamiliarity parameter was
50
- // inert. It is a member of COGNITIVE_TRAITS, so a persona carried a capacity
51
- // on this dimension, but the demand vector had no slot for it -- so the
52
- // capacity was transported against a demand that did not exist and
53
- // contributed nothing. Runs at familiarity 0 and 1 came back byte-identical
54
- // while the tool attested the parameter had been applied.
55
- //
56
- // The two vectors are supposed to be the same space. A dimension present in
57
- // one and missing from the other is silently dropped, and there is now a test
58
- // asserting COGNITIVE_TRAITS and DEMAND_DIMENSIONS do not diverge.
59
- 'siteFamiliarity',
58
+ // siteFamiliarity left the vector in 21.0 (Stage C). It is the visit's
59
+ // state, not the person's: Cockburn, Gutwin & Greenberg's (2007) expertise e
60
+ // on the choosing time, supplied per run (DemandOptions) and the same for
61
+ // every persona in it. As a capacity it read a navigation-depth demand that
62
+ // was a constant on 92% of real pages, so every persona paid the same 0.056.
60
63
  // Capacity, bridged from the formal reading and pointing models.
61
64
  //
62
65
  // These layers previously read DISPOSITION traits -- readingTendency for
@@ -87,21 +90,38 @@ export const DEMAND_DIMENSIONS = [
87
90
  */
88
91
  export const LAYER_DEFINITIONS = [
89
92
  {
90
- name: 'saliency',
91
- traits: ['changeBlindness', 'attentionPattern'],
93
+ // Named `saliency` until 21.0, which claimed a mechanism this layer does not
94
+ // have. Its demand is pageMetrics.visualComplexity (a count of visual
95
+ // elements) and the animation level; the fitted attention model (UEyes,
96
+ // attention-transport.ts) never reaches it. The name now says what it reads.
97
+ //
98
+ // attentionPattern left this layer in 21.0. It is not a CognitiveTraits
99
+ // field, so as cognitive_effort builds a persona its capacity was the
100
+ // missing-trait fallback for everyone: a term that moved every score and
101
+ // told no two personas apart.
102
+ name: 'visualComplexity',
103
+ traits: ['changeBlindness'],
92
104
  },
93
105
  {
94
106
  name: 'cognitiveLoad',
95
- // siteFamiliarity added 2026-08-02. It was in COGNITIVE_TRAITS and in no
96
- // layer, so it reached the profile, got a capacity, and then contributed to
97
- // nothing -- the third and final reason the parameter was inert. Knowing a
98
- // site is a substitute for holding its layout in working memory, which is
99
- // exactly this layer, and the demand term for it comes from the same
100
- // navigationDepth signal that already feeds workingMemory here.
101
- // `transferLearning` moved here from readability 2026-08-02: applying a
102
- // pattern learned on one interface to another is a load question, not a
103
- // decoding one, and readability is now decoding-only.
104
- traits: ['comprehension', 'workingMemory', 'informationForaging', 'siteFamiliarity', 'transferLearning'],
107
+ // STAGE C (21.0): four abilities asked the choosing time. Each takes the
108
+ // decision demand, d = T_choose / (T_choose + tau), with no share, so a
109
+ // value reads as tolerated reference choosing time tau c / (1 - c): the
110
+ // claim is the persona value (a hypothesis, provenance/personas.ts), not a
111
+ // coefficient. Through 20.x the layer read text features (word and
112
+ // sentence length, a lexical diversity that is 1 on any page with 100
113
+ // distinct words, a nested text count) that the reading law now prices,
114
+ // against `comprehension`, which is grasp of UI conventions, not reading.
115
+ // The channel is where the evidence points (convention knowledge and web
116
+ // experience act on finding things: research/stage-c-design.md, 3.5); the
117
+ // magnitude is the persona's value. Same seconds, second layer: decision
118
+ // prices dispositions against them, this layer abilities, as reading
119
+ // seconds feed both readability and readingAttention.
120
+ //
121
+ // siteFamiliarity left this layer: it is the visit's e on the choosing
122
+ // time (DemandOptions), not a capacity. `transferLearning` moved here from
123
+ // readability 2026-08-02.
124
+ traits: ['comprehension', 'workingMemory', 'informationForaging', 'transferLearning'],
105
125
  },
106
126
  {
107
127
  name: 'decision',
@@ -154,7 +174,16 @@ export const LAYER_DEFINITIONS = [
154
174
  },
155
175
  {
156
176
  name: 'frustration',
157
- traits: ['resilience', 'selfEfficacy', 'patience', 'emotionalContagion'],
177
+ // Two traits, not four, as of 21.0. resilience and selfEfficacy sat here
178
+ // through 20.x, but no page feature writes demand to either, so in the
179
+ // asymmetric mode every tool runs they could only ever be surplus on
180
+ // surplus-free dimensions: cost 0, every time. All they did there was halve
181
+ // the content gate, which is the mean demand over this list, so frustration
182
+ // scored at roughly half its own demand. (In the symmetric ablation, where
183
+ // all surplus is billed, they instead charged their whole capacity squared
184
+ // against a page that asked nothing of them.) They remain dimensions --
185
+ // abandonment reads resilience -- and are no longer in a layer.
186
+ traits: ['patience', 'emotionalContagion'],
158
187
  },
159
188
  {
160
189
  name: 'readability',
@@ -208,19 +237,21 @@ export const LAYER_DEFINITIONS = [
208
237
  },
209
238
  ];
210
239
  /**
211
- * Pairwise interaction definitions — the top 5 cross-layer effects
212
- * identified in the CHI 2027 paper's empirical calibration (Table 3).
240
+ * Pairwise interaction definitions: five cross-layer couplings, each a
241
+ * hand-set hypothesis registered in provenance/chain.ts (no empirical
242
+ * calibration exists for them; an earlier comment claimed one). The weight
243
+ * is read through param() so a run records that it depended on it.
213
244
  */
214
- const INTERACTION_PAIRS = [
215
- { a: 'cognitiveLoad', b: 'decision', weight: 0.15 },
216
- { a: 'saliency', b: 'cognitiveLoad', weight: 0.15 },
217
- { a: 'frustration', b: 'readability', weight: 0.15 },
218
- { a: 'cognitiveLoad', b: 'motor', weight: 0.15 },
219
- { a: 'frustration', b: 'decision', weight: 0.15 },
245
+ export const INTERACTION_PAIRS = [
246
+ { a: 'cognitiveLoad', b: 'decision', weight: 'chain.interaction.cognitiveLoadXdecision' },
247
+ { a: 'visualComplexity', b: 'cognitiveLoad', weight: 'chain.interaction.visualComplexityXcognitiveLoad' },
248
+ { a: 'frustration', b: 'readability', weight: 'chain.interaction.frustrationXreadability' },
249
+ { a: 'cognitiveLoad', b: 'motor', weight: 'chain.interaction.cognitiveLoadXmotor' },
250
+ { a: 'frustration', b: 'decision', weight: 'chain.interaction.frustrationXdecision' },
220
251
  ];
221
- /** Asymmetric cost weights (Theorem 2 in paper) */
222
- const WEIGHT_DEFICIT = 1.0; // demand > capacity: high cost
223
- const WEIGHT_SURPLUS = 0.3; // capacity > demand: low cost (surplus is cheap)
252
+ // Asymmetric cost weights (chain.weightDeficit 1, chain.weightSurplus 0.3):
253
+ // hand-set hypotheses registered in provenance/chain.ts, not a theorem. Read
254
+ // into CHAIN_PARAMS at function entry.
224
255
  /**
225
256
  * Dimensions where having MORE than the page asks for costs nothing at all.
226
257
  *
@@ -250,22 +281,46 @@ const WEIGHT_SURPLUS = 0.3; // capacity > demand: low cost (surplus is cheap)
250
281
  * That is the same defect as every other one this campaign found: a number that
251
282
  * does not mean what its name says.
252
283
  *
253
- * Four dimensions are billed BECAUSE THEIR SCALE IS INVERTED -- changeBlindness,
254
- * trustCalibration, curiosity and attentionPattern name a deficit at their high
255
- * end ("misses most changes", "accepts claims at face value", "easily
256
- * distracted"). Surplus there is more of the deficit, and billing it is
257
- * correct.
284
+ * Three dimensions stay billed BECAUSE THEIR SCALE IS INVERTED -- trustCalibration,
285
+ * curiosity and attentionPattern name a deficit at their high end ("accepts
286
+ * claims at face value", "easily distracted"), and none is in a layer, so the
287
+ * billing moves no number. changeBlindness was the fourth until 2026-10-24
288
+ * (below).
258
289
  *
259
290
  * `selfEfficacy` is the weakest call in the free set: over-confidence is a real
260
291
  * hazard, but its cost surfaces through riskTolerance and trustCalibration
261
292
  * rather than as effort spent, so it is treated as the ability its high end
262
293
  * describes.
294
+ *
295
+ * A STIMULUS NEVER MAKES A PAGE CHEAPER (2026-10-24, option f of
296
+ * research/surplus-billing-design.md). A billed surplus costs
297
+ * weightSurplus (c - d)^2, which FALLS as the page's demand d rises toward the
298
+ * persona's value c. Measured on the 514 reference pages, one carousel on a
299
+ * static page lowered the typical user's viewport totalCTC by 0.085 and was
300
+ * cheaper on 92% of them, because emotionalContagion's only demand is motion
301
+ * (0 on a calm page) and its calm-page surplus was the largest bill on the
302
+ * page. So:
303
+ * - emotionalContagion is priced by EXPOSURE (EXPOSURE_DIMENSIONS):
304
+ * weightSurplus x c x d. Contagion is caught from an expression (Hatfield,
305
+ * Bensman, Thornton & Rapson 2014), so with no stimulus nothing is caught
306
+ * and with no susceptibility nothing is caught either. The product form,
307
+ * the weight and animation as the stimulus are hypotheses.
308
+ * - changeBlindness is surplus-free: missing more changes than a page makes
309
+ * costs nothing on a page that makes none (Simons & Levin 1997: change
310
+ * blindness is "the inability to detect changes"). Its deficit side still
311
+ * prices a good noticer on a complex page, a named limit.
312
+ * - patience is surplus-free: its value reads as tolerated reading time
313
+ * (Stage C), and tolerance left unused is not time spent; the page's time
314
+ * is priced by the reading and decision layers.
315
+ * The decision-layer susceptibilities (fearOfMissingOut,
316
+ * socialProofSensitivity, anchoringBias, riskTolerance) are DEFERRED, a named
317
+ * limit: they still get cheaper as choosing time grows, because the cue each
318
+ * responds to (urgency, reviews, a first option, uncertain controls) is not
319
+ * measured and a share of choosing time stands in for it.
263
320
  */
264
- const SURPLUS_FREE_DIMENSIONS = new Set([
321
+ export const SURPLUS_FREE_DIMENSIONS = new Set([
265
322
  // Derived capacity bridges -- pure abilities by construction.
266
323
  'readingCapacity', 'motorCapacity', 'sustainedAttention', 'processingSpeed', 'textProcessing', 'motorPrecision',
267
- // Experience. Knowing a site better cannot make it harder to use.
268
- 'siteFamiliarity',
269
324
  // Abilities: every one of these has a highEnd naming something a person CAN
270
325
  // do, not something they TEND to do.
271
326
  'comprehension', // "Instant recognition of all conventions"
@@ -277,27 +332,36 @@ const SURPLUS_FREE_DIMENSIONS = new Set([
277
332
  'interruptRecovery', // "Seamlessly resumes exactly where left off"
278
333
  'mentalModelFlexibility', // "Highly adaptive - quickly forms new mental models"
279
334
  'informationForaging', // "Follows information scent efficiently, abandons low-yield paths"
335
+ // Free since 2026-10-24 (see above): neither surplus is effort spent.
336
+ 'changeBlindness', // "Misses most changes outside focus area" -- nothing to miss where nothing changes
337
+ 'patience', // "Waits indefinitely" -- a tolerance; unused, it is not time spent
338
+ ]);
339
+ /**
340
+ * Dimensions priced by exposure rather than by a gap: weightSurplus x c x d,
341
+ * the persona's susceptibility times the stimulus, in asymmetric mode (the
342
+ * symmetric ablation keeps the gap). 0 when the page presents nothing, never
343
+ * lower for more stimulus or more susceptibility. See the note above.
344
+ */
345
+ export const EXPOSURE_DIMENSIONS = new Set([
346
+ 'emotionalContagion', // "Adopts UI's emotional tone completely" -- a susceptibility to the page's tone
280
347
  ]);
281
348
  /**
282
349
  * Dimensions whose surplus IS billed, and why each one earns it.
283
350
  *
284
351
  * Stated explicitly rather than left as "everything else", so that a dimension
285
352
  * added to DEMAND_DIMENSIONS cannot acquire a billing treatment by default. A
286
- * test asserts every dimension appears in exactly one of the two sets.
353
+ * test asserts every dimension appears in exactly one of the three sets.
287
354
  */
288
355
  const SURPLUS_BILLED_DIMENSIONS = new Set([
289
- 'patience', // "Waits indefinitely" -- waiting longer than needed is time spent
290
- 'riskTolerance', // "Clicks anything that might work" -- a tendency, and a hazard
356
+ 'riskTolerance', // "Clicks anything that might work" -- a tendency, and a hazard (deferred, see above)
291
357
  'persistence', // "Keeps trying same approach repeatedly" -- repeating unnecessary work
292
358
  'readingTendency', // "Reads every word before acting" -- reading a page that did not need it
293
- 'emotionalContagion', // susceptibility to the interface's tone
294
359
  'satisficing', // a decision STRATEGY, not an ability
295
- 'anchoringBias', // a bias, by name
296
- 'fearOfMissingOut', // susceptibility to urgency and scarcity
297
- 'socialProofSensitivity', // susceptibility to what others chose
360
+ 'anchoringBias', // a bias, by name (deferred, see above)
361
+ 'fearOfMissingOut', // susceptibility to urgency and scarcity (deferred, see above)
362
+ 'socialProofSensitivity', // susceptibility to what others chose (deferred, see above)
298
363
  'metacognitivePlanning', // "Careful planning before any action" -- planning a trivial page is waste
299
364
  // Inverted scales: the HIGH end is the deficit, so surplus is more deficit.
300
- 'changeBlindness', // "Misses most changes outside focus area"
301
365
  'trustCalibration', // "Highly trusting, accepts claims at face value"
302
366
  'curiosity', // "Easily distracted by interesting elements"
303
367
  'attentionPattern', // scanning-vs-thorough disposition
@@ -321,7 +385,7 @@ const SURPLUS_BILLED_DIMENSIONS = new Set([
321
385
  * or a dose-response fit on real session data. Until then it is a shape someone
322
386
  * chose, not a shape anyone measured. (2026-08-03)
323
387
  */
324
- export const ABANDONMENT_HILL_EXPONENT = 2;
388
+ export const ABANDONMENT_HILL_EXPONENT = param('chain.abandonmentHillExponent');
325
389
  /**
326
390
  * Relative weighting between the two motor layers. BOTH DEFAULT TO 1.0 AND ARE
327
391
  * UNCALIBRATED.
@@ -342,66 +406,104 @@ export const MOTOR_LAYER_WEIGHTS = {
342
406
  /** UNCALIBRATED. Sequence execution. */
343
407
  motorProcedure: 1.0,
344
408
  };
345
- /** Capacity depletion rate per layer (alpha_i, Section 3.4) */
346
- const DEPLETION_RATE = 0.15;
347
- /** Minimum residual capacity floor (prevents total exhaustion) */
348
- const CAPACITY_FLOOR = 0.05;
349
- /**
350
- * Numerical floor on residual capacity. NOT a behavioural limit.
351
- *
352
- * CAPACITY_FLOOR above was doing behavioural work it should not have: at 0.05
353
- * it was reachable inside a single page, and once reached the account stopped
354
- * responding to cost at all. Proportional drawdown asymptotes toward zero
355
- * instead, so this exists only to keep the value away from denormals and to
356
- * keep `gap = demand - capacity` finite. It is three orders of magnitude below
357
- * anything a persona reaches in practice. (2026-08-03)
358
- */
359
- const CAPACITY_EPSILON = 1e-4;
360
- /**
361
- * Largest fraction of remaining capacity one layer may draw. UNCALIBRATED.
362
- *
363
- * Prevents a single very expensive layer from emptying the account outright,
364
- * which would reintroduce the saturation this replaced. 0.5 means no layer can
365
- * take more than half of what is left, whatever its cost.
366
- *
367
- * What would settle it: within-session effort measurements across a task
368
- * sequence. Same status as ABANDONMENT_HILL_EXPONENT and MOTOR_LAYER_WEIGHTS.
369
- */
370
- const MAX_DRAWDOWN_FRACTION = 0.5;
409
+ // ── Registry reads ──
410
+ //
411
+ // Every hand-set number the chain uses lives in provenance/chain.ts and is
412
+ // read through param() once per call, at function entry. The code cannot
413
+ // drift from the registry (an unregistered id throws), and a run under
414
+ // withProvenance records exactly which constants its answer rested on.
415
+ //
416
+ // Depletion (chain.depletionRate 0.15, chain.maxDrawdownFraction 0.5,
417
+ // chain.indirectDepletionShare 0.5) and the numerical floor
418
+ // (chain.capacityEpsilon 1e-4) are registered there with their status and what
419
+ // would settle them. The floor binds only when a residual falls below it; through
420
+ // 20.x siteFamiliarity sat at 0 in the tool path and was lifted to 1e-4 on every
421
+ // run, and since it left the vector in 21.0 no built-in persona starts below 0.1.
422
+ // The old CAPACITY_FLOOR (0.05)
423
+ // was removed 2026-10-21: nothing read it since proportional drawdown replaced
424
+ // the clamp on 2026-08-03.
425
+ /** Read every id in `ids` through param(); one object per call so the reads are recorded. */
426
+ function readParams(ids) {
427
+ const out = {};
428
+ for (const k in ids)
429
+ out[k] = param(ids[k]);
430
+ return out;
431
+ }
432
+ const DEMAND_IDS = {
433
+ animationSigmoidThreshold: 'chain.demand.animation.sigmoidThreshold',
434
+ animationSigmoidScale: 'chain.demand.animation.sigmoidScale',
435
+ animationChangeBlindness: 'chain.demand.animation.changeBlindness',
436
+ animationEmotionalContagion: 'chain.demand.animation.emotionalContagion',
437
+ distractorAnimationShare: 'chain.demand.distractor.animationShare',
438
+ distractorVisualShare: 'chain.demand.distractor.visualShare',
439
+ sustainedBase: 'chain.demand.sustained.base',
440
+ sustainedDistractorGain: 'chain.demand.sustained.distractorGain',
441
+ choiceAnchoringBias: 'chain.demand.choice.anchoringBias',
442
+ choiceFearOfMissingOut: 'chain.demand.choice.fearOfMissingOut',
443
+ choiceSocialProofSensitivity: 'chain.demand.choice.socialProofSensitivity',
444
+ decisionRiskTolerance: 'chain.demand.decision.riskTolerance',
445
+ };
446
+ const CHAIN_IDS = {
447
+ missingTraitCapacity: 'chain.missingTraitCapacity',
448
+ engagementAtSkim: 'chain.engagement.atSkim',
449
+ engagementRange: 'chain.engagement.range',
450
+ weightDeficit: 'chain.weightDeficit',
451
+ weightSurplus: 'chain.weightSurplus',
452
+ contentScaleGain: 'chain.contentScaleGain',
453
+ depletionGate: 'chain.depletionGate',
454
+ depletionRate: 'chain.depletionRate',
455
+ indirectDepletionShare: 'chain.indirectDepletionShare',
456
+ maxDrawdownFraction: 'chain.maxDrawdownFraction',
457
+ capacityEpsilon: 'chain.capacityEpsilon',
458
+ roundingScale: 'chain.roundingScale',
459
+ patienceShare: 'chain.abandonment.patienceShare',
460
+ resilienceShare: 'chain.abandonment.resilienceShare',
461
+ toleranceGain: 'chain.abandonment.toleranceGain',
462
+ toleranceFloor: 'chain.abandonment.toleranceFloor',
463
+ abandonmentHillExponent: 'chain.abandonmentHillExponent',
464
+ };
371
465
  // ── Sigmoid Helper ──
372
466
  /**
373
467
  * Standard logistic sigmoid for mapping raw page metrics to demand values.
374
- * Calibrated thresholds ensure typical web pages produce demands in [0.1, 0.9].
468
+ * The threshold and scale are hand-set hypotheses (see provenance/chain.ts),
469
+ * chosen so typical web pages produce demands in [0.1, 0.9]; not calibrated.
375
470
  */
376
471
  function sigmoid(raw, threshold, scale) {
472
+ // No exponent clamp. One was registered (chain.sigmoidExponentClamp, 500)
473
+ // and could not change a value: x > 500 needs raw below zero, which the
474
+ // input sanitiser forbids, and for x < -500 the plain form already returns
475
+ // exactly 1. Math.exp overflows to Infinity, which this form maps to 0, not
476
+ // NaN. Removed in 21.0 when the dead-constant test found it.
377
477
  const x = -(raw - threshold) / scale;
378
- // Clamp exponent to prevent overflow
379
- if (x > 500)
380
- return 0;
381
- if (x < -500)
382
- return 1;
383
478
  return 1 / (1 + Math.exp(x));
384
479
  }
385
480
  // ── 1. Demand Distribution Computation ──
386
481
  /**
387
- * Compute the 26-dimensional demand distribution from page metrics.
482
+ * Compute the 29-dimensional demand distribution from page metrics.
388
483
  *
389
- * Each page feature maps to specific trait dimensions via calibrated
390
- * sigmoid functions. The mapping encodes HOW each page characteristic
391
- * creates cognitive demand on specific user capacities.
484
+ * Since Stage B and C the demand is law times against one time budget, d =
485
+ * T / (T + tau) (demand-laws.ts, law.timeBudget): pointing (Fitts), choosing
486
+ * (Cockburn et al.'s menu model), reading (Brysbaert), procedure (KLM). What
487
+ * remains hand-set are the shares of those demands some traits take and the
488
+ * motion and clutter curves, every one a hypothesis in provenance/chain.ts.
489
+ * The text features (informationDensity, textDensity, word and sentence
490
+ * length, lexical diversity, long and technical words) and navigationDepth
491
+ * are reported on PageMetrics and drive no demand: reading time prices text,
492
+ * and depth is a property of the path, not the page (research/stage-c-design.md).
392
493
  *
393
494
  * Traits not affected by any page feature receive demand = 0.0 (no demand).
394
495
  *
395
496
  * @param pageMetrics - Extracted page analysis metrics
396
- * @returns DemandDistribution with per-trait demand and variance
497
+ * @param options - The visit: siteFamiliarity, Cockburn et al.'s expertise e
498
+ * on the choosing time, the same for every persona scored in a run
499
+ * @returns DemandDistribution with per-trait demand
397
500
  */
398
- export function computeDemandDistribution(pageMetrics) {
501
+ export function computeDemandDistribution(pageMetrics, options) {
502
+ const D = readParams(DEMAND_IDS);
399
503
  const demands = {};
400
- const variance = {};
401
- // Initialize all 26 dimensions to zero demand
504
+ // Initialize every dimension to zero demand
402
505
  for (const dim of DEMAND_DIMENSIONS) {
403
506
  demands[dim] = 0;
404
- variance[dim] = 0;
405
507
  }
406
508
  // Sanitize inputs — guard against NaN, undefined, negative
407
509
  const safe = (v, fallback = 0) => {
@@ -409,100 +511,80 @@ export function computeDemandDistribution(pageMetrics) {
409
511
  return fallback;
410
512
  return Math.max(0, v);
411
513
  };
412
- const infoDensity = safe(pageMetrics.informationDensity);
413
514
  const visCplx = safe(pageMetrics.visualComplexity);
414
- const interactiveCount = safe(pageMetrics.interactiveElementCount);
415
- const textDens = safe(pageMetrics.textDensity);
416
515
  const animLevel = safe(pageMetrics.animationLevel);
417
- const choices = safe(pageMetrics.choiceCount);
418
- const navDepth = safe(pageMetrics.navigationDepth);
419
- // ── informationDensity → comprehension, workingMemory, readingTendency, informationForaging
420
- // Page metrics already use logScale (0-1) — pass through directly for differentiation
421
- const infoDemand = infoDensity;
422
- demands.comprehension = Math.max(demands.comprehension, infoDemand);
423
- demands.workingMemory = Math.max(demands.workingMemory, infoDemand * 0.9);
424
- demands.readingTendency = Math.max(demands.readingTendency, infoDemand * 0.85);
425
- demands.informationForaging = Math.max(demands.informationForaging, infoDemand * 0.8);
426
- variance.comprehension += infoDensity * 0.1;
427
- variance.workingMemory += infoDensity * 0.08;
428
- variance.readingTendency += infoDensity * 0.07;
429
- variance.informationForaging += infoDensity * 0.06;
430
- // ── visualComplexity → changeBlindness, mentalModelFlexibility, attentionPattern (inferred)
516
+ // ── The law times, each a demand against one budget ──
517
+ // d = T / (T + tau), tau = law.timeBudget: 0 at no work, 0.5 at the budget.
518
+ // No fallback: a PageMetrics without them was not derived from a measurement
519
+ // (deriveMetrics) and has no motor, decision, procedure or reading demand to
520
+ // give. interactiveElementCount and choiceCount stay on PageMetrics as
521
+ // reported counts; no demand reads them now.
522
+ const seconds = (k) => {
523
+ const v = pageMetrics[k];
524
+ if (typeof v !== 'number' || !Number.isFinite(v) || v < 0) {
525
+ throw new Error(`computeDemandDistribution: pageMetrics.${k} must be a finite number of seconds >= 0 (got ${String(v)}). Derive the metrics from a measurement (extractPageMetrics, deriveMetrics) or pass the law times to computeDemandFromRawMetrics.`);
526
+ }
527
+ return v;
528
+ };
529
+ // The visit. Cockburn, Gutwin & Greenberg (2007), Eq. 4: decision/search time
530
+ // interpolates from novice linear search to the Hick-Hyman expert limit by
531
+ // expertise e, because the expert's "visual search time decreases towards
532
+ // zero". A first visit (e = 0) is the default and what the reference curve is
533
+ // built at, and it reads the novice time alone, exactly as before.
534
+ const e = options?.siteFamiliarity ?? 0;
535
+ if (typeof e !== 'number' || !Number.isFinite(e) || e < 0 || e > 1) {
536
+ throw new Error(`computeDemandDistribution: siteFamiliarity must be a number in [0, 1], Cockburn et al.'s expertise e (got ${String(e)})`);
537
+ }
538
+ const novice = seconds('choosingSeconds');
539
+ const expert = seconds('choosingSecondsExpert');
540
+ const pointingDemand = timeDemand(seconds('pointingSeconds'));
541
+ // The decision dispositions keep the first-visit choosing time on every run
542
+ // (Stage B). Familiarity acts on the finding abilities below instead: the
543
+ // dispositions bill surplus, so a shorter choosing time would turn a small
544
+ // deficit into billed surplus, and measured on the reference pages a familiar
545
+ // visit then cost MORE than a first one on 72 of 513 for the typical user.
546
+ // Knowing a site cannot make it harder.
547
+ const decisionDemand = timeDemand(novice);
548
+ // What familiarity shortens: the search for where things are, the work the
549
+ // cognitiveLoad abilities are asked. At e = 0 this is 1 x novice + 0, the
550
+ // novice time to the last bit, so a first visit is unchanged.
551
+ const findingDemand = timeDemand((1 - e) * novice + e * expert);
552
+ const readingDemand = timeDemand(seconds('readingSeconds'));
553
+ const procedureDemand = timeDemand(seconds('procedureSeconds'));
554
+ // Demand is written only to dimensions some layer reads. Through 20.x these
555
+ // curves also wrote readingTendency, mentalModelFlexibility,
556
+ // interruptRecovery, metacognitivePlanning, persistence and motorPrecision,
557
+ // none of which is in a layer, so those terms (and eight registered weights)
558
+ // changed no number. Removed in 21.0; the dimensions themselves remain.
559
+ // ── visualComplexity → changeBlindness
431
560
  const visDemand = visCplx; // already 0-1 from logScale
432
561
  demands.changeBlindness = Math.max(demands.changeBlindness, visDemand);
433
- demands.mentalModelFlexibility = Math.max(demands.mentalModelFlexibility, visDemand * 0.7);
434
- demands.attentionPattern = Math.max(demands.attentionPattern, visDemand * 0.85);
435
- variance.changeBlindness += visCplx * 0.12;
436
- variance.mentalModelFlexibility += visCplx * 0.08;
437
- variance.attentionPattern += visCplx * 0.1;
438
- // ── interactiveElementCount → riskTolerance, satisficing, proceduralFluency, motorPrecision (inferred)
439
- // Use logScale for element count — 20 elements = 0.5, 100 = 0.83, 500 = 0.96
440
- const interDemand = interactiveCount / (interactiveCount + 20);
441
- demands.riskTolerance = Math.max(demands.riskTolerance, interDemand * 0.75);
442
- demands.satisficing = Math.max(demands.satisficing, interDemand * 0.8);
443
- demands.proceduralFluency = Math.max(demands.proceduralFluency, interDemand * 0.9);
444
- demands.motorPrecision = Math.max(demands.motorPrecision, interDemand);
445
- variance.riskTolerance += interDemand * 0.06;
446
- variance.satisficing += interDemand * 0.07;
447
- variance.proceduralFluency += interDemand * 0.08;
448
- variance.motorPrecision += interDemand * 0.1;
449
- // ── textDensity → readingTendency, patience, comprehension
450
- const textDemand = textDens; // already 0-1 from logScale
451
- demands.readingTendency = Math.max(demands.readingTendency, textDemand);
452
- demands.patience = Math.max(demands.patience, textDemand * 0.7);
453
- demands.comprehension = Math.max(demands.comprehension, textDemand * 0.85);
454
- variance.readingTendency += textDens * 0.09;
455
- variance.patience += textDens * 0.06;
456
- variance.comprehension += textDens * 0.08;
457
- // ── Text Readability Metrics (v18.56) ──
458
- // These measure HOW HARD the text is to read, not just how much there is.
459
- // Captures language effects (German compound words), technical jargon, prose complexity.
460
- const avgWordLen = safe(pageMetrics.avgWordLength);
461
- const avgSentLen = safe(pageMetrics.avgSentenceLength);
462
- const lexDiv = safe(pageMetrics.lexicalDiversity);
463
- const longWordR = safe(pageMetrics.longWordRatio);
464
- const techDens = safe(pageMetrics.technicalDensity);
465
- if (avgWordLen > 0 || avgSentLen > 0) {
466
- // avgWordLength → comprehension, readingTendency
467
- // Longer words = harder to parse (especially for dyslexic, ADHD, non-native speakers)
468
- demands.comprehension = Math.max(demands.comprehension, avgWordLen * 0.9);
469
- demands.readingTendency = Math.max(demands.readingTendency, avgWordLen * 0.85);
470
- variance.comprehension += avgWordLen * 0.12;
471
- variance.readingTendency += avgWordLen * 0.1;
472
- // avgSentenceLength → workingMemory, patience, comprehension
473
- // Longer sentences demand more working memory to hold clause structure
474
- demands.workingMemory = Math.max(demands.workingMemory, avgSentLen * 0.85);
475
- demands.patience = Math.max(demands.patience, avgSentLen * 0.7);
476
- demands.comprehension = Math.max(demands.comprehension, avgSentLen * 0.8);
477
- variance.workingMemory += avgSentLen * 0.1;
478
- variance.patience += avgSentLen * 0.07;
479
- // lexicalDiversity → comprehension, transferLearning
480
- // High vocabulary diversity = more cognitive effort to track meaning
481
- demands.comprehension = Math.max(demands.comprehension, lexDiv * 0.75);
482
- demands.transferLearning = Math.max(demands.transferLearning, lexDiv * 0.65);
483
- variance.comprehension += lexDiv * 0.08;
484
- // longWordRatio → readingTendency, comprehension
485
- // Pages with many 7+ character words are harder to skim
486
- demands.readingTendency = Math.max(demands.readingTendency, longWordR * 0.9);
487
- demands.comprehension = Math.max(demands.comprehension, longWordR * 0.8);
488
- variance.readingTendency += longWordR * 0.11;
489
- // technicalDensity → comprehension, transferLearning, mentalModelFlexibility
490
- // 10+ character words signal compound terms, jargon, domain-specific vocabulary
491
- demands.comprehension = Math.max(demands.comprehension, techDens * 0.95);
492
- demands.transferLearning = Math.max(demands.transferLearning, techDens * 0.8);
493
- demands.mentalModelFlexibility = Math.max(demands.mentalModelFlexibility, techDens * 0.7);
494
- variance.comprehension += techDens * 0.13;
495
- variance.transferLearning += techDens * 0.09;
496
- variance.mentalModelFlexibility += techDens * 0.07;
497
- }
498
- // ── animationLevel → changeBlindness, interruptRecovery, emotionalContagion
499
- const animDemand = sigmoid(animLevel, 0.3, 0.15);
500
- demands.changeBlindness = Math.max(demands.changeBlindness, animDemand * 0.9);
501
- demands.interruptRecovery = Math.max(demands.interruptRecovery, animDemand * 0.85);
502
- demands.emotionalContagion = Math.max(demands.emotionalContagion, animDemand * 0.7);
503
- variance.changeBlindness += animLevel * 0.1;
504
- variance.interruptRecovery += animLevel * 0.09;
505
- variance.emotionalContagion += animLevel * 0.07;
562
+ // ── procedure (KLM M per entry step) → proceduralFluency
563
+ demands.proceduralFluency = Math.max(demands.proceduralFluency, procedureDemand);
564
+ // ── reading → patience (Stage C)
565
+ //
566
+ // patience is "tolerance for delays, loading, friction before abandoning",
567
+ // and its registered measure is a time to abandonment. It takes the reading
568
+ // demand whole, so a value reads as tolerated reference reading time,
569
+ // tau c / (1 - c): 0.5 tolerates the budget itself, the median first-visit
570
+ // stay the budget is. Through 20.x it took 0.7 of textDensity (a nested text
571
+ // count per unit area) and 0.7 of sentence length, two hand-set shares of two
572
+ // text features the reading law now prices.
573
+ demands.patience = Math.max(demands.patience, readingDemand);
574
+ // ── animationLevel → changeBlindness, emotionalContagion
575
+ //
576
+ // ZERO AT REST, as of 21.0. The logistic is centred at the threshold (0.3),
577
+ // so at animationLevel 0 it already reads 0.119, and that offset was demand:
578
+ // a page with nothing moving asked ~0.107 of changeBlindness and ~0.083 of
579
+ // emotionalContagion and fed distractor pressure, so an empty page had a
580
+ // non-zero raw cost. The curve is now shifted and rescaled by its own value at
581
+ // 0: still monotone in animationLevel, 0 at rest, below 1 and approaching it.
582
+ // No new constant: the offset is the registered curve evaluated at 0.
583
+ const animRaw = sigmoid(animLevel, D.animationSigmoidThreshold, D.animationSigmoidScale);
584
+ const animAtRest = sigmoid(0, D.animationSigmoidThreshold, D.animationSigmoidScale);
585
+ const animDemand = animAtRest < 1 ? Math.max(0, (animRaw - animAtRest) / (1 - animAtRest)) : 0;
586
+ demands.changeBlindness = Math.max(demands.changeBlindness, animDemand * D.animationChangeBlindness);
587
+ demands.emotionalContagion = Math.max(demands.emotionalContagion, animDemand * D.animationEmotionalContagion);
506
588
  // ── textDensity × distractor pressure → sustainedAttention
507
589
  //
508
590
  // How much attention the page asks you to HOLD, as opposed to how hard its
@@ -527,7 +609,7 @@ export function computeDemandDistribution(pageMetrics) {
527
609
  // re-creates the exact inversion that removing readingTendency fixed --
528
610
  // measured: at a flat 1.0 base, ADHD outscored dyslexic on readability on a
529
611
  // page with animationLevel 0, which is the wrong answer.
530
- const distractorPressure = Math.min(1, animDemand * 0.7 + visDemand * 0.3);
612
+ const distractorPressure = Math.min(1, animDemand * D.distractorAnimationShare + visDemand * D.distractorVisualShare);
531
613
  // `max(textDens, infoDensity)`, not `textDens` alone -- the same base
532
614
  // readingCapacity uses below, and for the same reason.
533
615
  //
@@ -545,90 +627,82 @@ export function computeDemandDistribution(pageMetrics) {
545
627
  // Sharing the base makes the two halves of reading agree about how much there is
546
628
  // to read; the 0.7 vs 0.9 coefficients still order them.
547
629
  //
548
- // NOT applied to saliency, whose fall has a different cause: its inputs
549
- // (visualComplexity, animationLevel) are counts and RISE with page size. It falls
550
- // because its traits are surplus-BILLED, so rising demand shrinks the surplus gap
551
- // and the 0.3-weighted cost with it. Same symptom, unrelated mechanism, and it
552
- // needs its own decision rather than this one applied twice.
553
- const sustainBase = Math.max(textDemand, infoDensity);
554
- const sustainDemand = Math.min(1, sustainBase * 0.7 * (1 + distractorPressure * 0.7));
555
- demands.sustainedAttention = Math.max(demands.sustainedAttention, sustainDemand);
556
- variance.sustainedAttention += textDens * 0.1 + animLevel * 0.08;
557
- // ── choiceCount → satisficing, anchoringBias, fearOfMissingOut, socialProofSensitivity
558
- // LogScale: 10 choices = 0.5, 30 = 0.75, 100 = 0.91
559
- const choiceDemand = choices / (choices + 10);
560
- demands.satisficing = Math.max(demands.satisficing, choiceDemand);
561
- demands.anchoringBias = Math.max(demands.anchoringBias, choiceDemand * 0.85);
562
- demands.fearOfMissingOut = Math.max(demands.fearOfMissingOut, choiceDemand * 0.75);
563
- demands.socialProofSensitivity = Math.max(demands.socialProofSensitivity, choiceDemand * 0.7);
564
- variance.satisficing += Math.min(1, choices / 20) * 0.1;
565
- variance.anchoringBias += Math.min(1, choices / 20) * 0.08;
566
- variance.fearOfMissingOut += Math.min(1, choices / 20) * 0.07;
567
- variance.socialProofSensitivity += Math.min(1, choices / 20) * 0.06;
568
- // ── navigationDepth → workingMemory, metacognitivePlanning, persistence, transferLearning
569
- // LogScale: depth 2 = 0.4, depth 5 = 0.63, depth 10 = 0.77
570
- const navDemand = navDepth / (navDepth + 3);
571
- demands.workingMemory = Math.max(demands.workingMemory, navDemand * 0.95);
572
- demands.metacognitivePlanning = Math.max(demands.metacognitivePlanning, navDemand * 0.8);
573
- demands.persistence = Math.max(demands.persistence, navDemand * 0.7);
574
- demands.transferLearning = Math.max(demands.transferLearning, navDemand * 0.65);
575
- // ── navigationDepth → siteFamiliarity
630
+ // NOT applied to visualComplexity, whose fall had a different cause: its inputs
631
+ // (visualComplexity, animationLevel) are counts and RISE with page size. It fell
632
+ // because its trait was surplus-BILLED, so rising demand shrank the surplus gap
633
+ // and the 0.3-weighted cost with it. That got its own decision: changeBlindness
634
+ // is surplus-free since 2026-10-24 (see SURPLUS_FREE_DIMENSIONS).
576
635
  //
577
- // This dimension had NO demand term at all. It was initialised to 0 and never
578
- // raised, so the persona's familiarity was transported against zero demand and
579
- // contributed exactly nothing to the chain — three runs at familiarity unset,
580
- // 1 and 0 returned byte-identical results while the tool reported the
581
- // parameter as applied. The value was never overridden; nothing ever asked
582
- // for it.
636
+ // STAGE B: the base is the reading demand (reading seconds against the time
637
+ // budget), which is absolute in the amount of prose, the property the
638
+ // max(textDensity, informationDensity) base above was chosen for.
639
+ const sustainDemand = Math.min(1, readingDemand * D.sustainedBase * (1 + distractorPressure * D.sustainedDistractorGain));
640
+ demands.sustainedAttention = Math.max(demands.sustainedAttention, sustainDemand);
641
+ // ── choosing (novice linear search, Cockburn et al. 2007) → the decision traits
642
+ // satisficing takes the whole decision demand; the others hand-set shares of it.
643
+ demands.satisficing = Math.max(demands.satisficing, decisionDemand);
644
+ demands.anchoringBias = Math.max(demands.anchoringBias, decisionDemand * D.choiceAnchoringBias);
645
+ demands.fearOfMissingOut = Math.max(demands.fearOfMissingOut, decisionDemand * D.choiceFearOfMissingOut);
646
+ demands.socialProofSensitivity = Math.max(demands.socialProofSensitivity, decisionDemand * D.choiceSocialProofSensitivity);
647
+ demands.riskTolerance = Math.max(demands.riskTolerance, decisionDemand * D.decisionRiskTolerance);
648
+ // ── choosing → comprehension, workingMemory, informationForaging, transferLearning (Stage C)
583
649
  //
584
- // Navigation depth is the honest driver: the further content sits from the
585
- // entry point, the more the page asks you to already know where things are.
586
- // A one-click page demands no site knowledge no matter who you are, which is
587
- // why this is proportional to navDemand rather than a constant. Weighted just
588
- // under workingMemory (0.95), since knowing the layout substitutes for
589
- // holding it in mind. (2026-08-02)
590
- demands.siteFamiliarity = Math.max(demands.siteFamiliarity, navDemand * 0.85);
591
- // Reading capacity is demanded by how much text there is to decode, and motor
592
- // capacity by how many things there are to hit. Both proportional, so a page
593
- // with no text asks nothing of decoding and a page with no controls asks
594
- // nothing of pointing.
595
- demands.readingCapacity = Math.max(demands.readingCapacity, Math.max(textDens, infoDensity) * 0.9);
596
- demands.motorCapacity = Math.max(demands.motorCapacity, interDemand * 0.9);
597
- variance.readingCapacity += textDens * 0.08;
598
- variance.motorCapacity += interDemand * 0.08;
599
- variance.siteFamiliarity += Math.min(1, navDepth / 5) * 0.08;
600
- variance.workingMemory += Math.min(1, navDepth / 5) * 0.09;
601
- variance.metacognitivePlanning += Math.min(1, navDepth / 5) * 0.07;
602
- variance.persistence += Math.min(1, navDepth / 5) * 0.06;
603
- variance.transferLearning += Math.min(1, navDepth / 5) * 0.05;
604
- // Clamp all demands and variances to [0, 1]
650
+ // The four abilities of cognitiveLoad take the choosing demand whole, as
651
+ // satisficing does on a first visit, so each value reads as tolerated
652
+ // reference choosing time. On a familiar run they take it at the run's e.
653
+ // Through 20.x they read text features (and siteFamiliarity a
654
+ // navigation-depth constant); see LAYER_DEFINITIONS.
655
+ demands.comprehension = Math.max(demands.comprehension, findingDemand);
656
+ demands.workingMemory = Math.max(demands.workingMemory, findingDemand);
657
+ demands.informationForaging = Math.max(demands.informationForaging, findingDemand);
658
+ demands.transferLearning = Math.max(demands.transferLearning, findingDemand);
659
+ // Reading capacity is demanded by the time the prose takes to read, and motor
660
+ // capacity by the time its targets take to acquire, each against the same
661
+ // budget as the persona's rate-ratio capacity (cognitive-models.ts), so a
662
+ // deficit means the persona's own time exceeds the budget. A page with no
663
+ // prose asks nothing of decoding, one with no controls nothing of pointing.
664
+ demands.readingCapacity = Math.max(demands.readingCapacity, readingDemand);
665
+ demands.motorCapacity = Math.max(demands.motorCapacity, pointingDemand);
666
+ // Clamp all demands to [0, 1]
605
667
  for (const dim of DEMAND_DIMENSIONS) {
606
668
  demands[dim] = Math.max(0, Math.min(1, demands[dim]));
607
- variance[dim] = Math.max(0, Math.min(1, variance[dim]));
608
669
  }
609
- return { demands, variance };
670
+ // Per-dimension demand variance was computed here from 41 hand-set
671
+ // coefficients and returned, but nothing in the chain or any tool read it, so
672
+ // it changed no score. Removed in 21.0 rather than registered as 41 more
673
+ // hypotheses that do nothing. (2026-10-21)
674
+ //
675
+ // The scope travels with the demand so totalCTC can be ranked against
676
+ // reference pages measured the same way, without every caller having to
677
+ // repeat a scope it already gave extractPageMetrics.
678
+ return pageMetrics.scope ? { demands, scope: pageMetrics.scope } : { demands };
610
679
  }
611
680
  // ── 2. Sequential Transport Chain ──
612
681
  /**
613
682
  * Resolve a trait value from the persona's trait record.
614
- * Falls back to 0.5 (neutral midpoint) for missing traits.
683
+ * Falls back to chain.missingTraitCapacity (0.5, a hypothesis) for missing traits.
684
+ */
685
+ /**
686
+ * A persona's capacity vector over DEMAND_DIMENSIONS, exactly as the chain reads
687
+ * it at the start of a page: each trait through traitValue (clamped to [0, 1],
688
+ * missing or non-numeric traits at chain.missingTraitCapacity).
689
+ * The Wasserstein-DRO bound (dro-bound.ts) works on these vectors.
615
690
  */
616
- function traitValue(traits, trait) {
691
+ export function capacityVector(traits) {
692
+ const P = readParams(CHAIN_IDS);
693
+ const out = {};
694
+ for (const dim of DEMAND_DIMENSIONS)
695
+ out[dim] = traitValue(traits, dim, P);
696
+ return out;
697
+ }
698
+ function traitValue(traits, trait, P) {
617
699
  const v = traits?.[trait];
618
- // Handle string enum traits (e.g. attentionPattern: "targeted" | "f-pattern" | ...)
619
- if (typeof v === 'string') {
620
- const STRING_TRAIT_MAP = {
621
- 'targeted': 0.9,
622
- 'sequential': 0.8,
623
- 'f-pattern': 0.7,
624
- 'z-pattern': 0.6,
625
- 'exploratory': 0.3,
626
- };
627
- return STRING_TRAIT_MAP[v] ?? 0.5;
628
- }
700
+ // A string attentionPattern ("targeted", "f-pattern", ...) used to map to five
701
+ // hand-set levels. attentionPattern is in no layer since 21.0, so the levels
702
+ // moved nothing and were removed; a non-numeric trait reads as missing.
629
703
  if (typeof v === 'number' && !isNaN(v))
630
704
  return Math.max(0, Math.min(1, v));
631
- return 0.5;
705
+ return P.missingTraitCapacity;
632
706
  }
633
707
  /**
634
708
  * Schwartz Value → Trait Demand Modulation Coefficients
@@ -636,6 +710,9 @@ function traitValue(traits, trait) {
636
710
  * Each entry maps a Schwartz value to the cognitive traits it amplifies or
637
711
  * dampens in the demand distribution. The coefficient represents how much
638
712
  * a high value score (1.0) amplifies (+) or dampens (-) the demand on that trait.
713
+ * Every coefficient is a hand-set hypothesis registered in provenance/chain.ts
714
+ * (the citations below motivate a direction, not a number) and is read
715
+ * through param() by id.
639
716
  *
640
717
  * Theoretical basis:
641
718
  * - Security-driven personas demand MORE trustCalibration (Fogg 2003)
@@ -646,54 +723,51 @@ function traitValue(traits, trait) {
646
723
  * @see Schwartz (1992) "Universals in the content and structure of values"
647
724
  * @see Section 4.2 of "Cognitive Optimal Transport: A Unified Framework"
648
725
  * @since v18.54.0
726
+ *
727
+ * Exported so cognitive_effort's useValues description can be checked against
728
+ * it; this table is the only place values reach a transport score.
649
729
  */
650
- const VALUE_DEMAND_MODULATION = [
651
- // ── Saliency Layer Modulations ──
730
+ export const VALUE_DEMAND_MODULATION = [
731
+ // Seven entries were removed in 21.0 because their trait is in no layer, so
732
+ // the modulated demand was never read: stimulation and conformity on
733
+ // attentionPattern, security on trustCalibration, stimulation on curiosity,
734
+ // selfDirection on metacognitivePlanning, tradition on mentalModelFlexibility
735
+ // and universalism on readingTendency.
736
+ // ── Visual Complexity Layer Modulations ──
652
737
  // Stimulation → reduces changeBlindness threshold (novelty-seekers notice changes more)
653
- { value: 'stimulation', trait: 'changeBlindness', coefficient: -0.20, layer: 'saliency' },
654
- // Stimulation → amplifies attentionPattern demand (drawn to novel visual elements)
655
- { value: 'stimulation', trait: 'attentionPattern', coefficient: 0.20, layer: 'saliency' },
738
+ { value: 'stimulation', trait: 'changeBlindness', coefficient: 'chain.valueModulation.stimulation.changeBlindness', layer: 'visualComplexity' },
656
739
  // Security → amplifies changeBlindness demand (vigilant scanners notice more changes)
657
- { value: 'security', trait: 'changeBlindness', coefficient: -0.15, layer: 'saliency' },
658
- // Conformity → dampens attentionPattern (follows expected scan patterns, less exploratory)
659
- { value: 'conformity', trait: 'attentionPattern', coefficient: -0.15, layer: 'saliency' },
660
- // ── Frustration Layer ──
661
- // Security → amplifies trust demand (Fogg 2003: high-security users need more trust signals)
662
- { value: 'security', trait: 'trustCalibration', coefficient: 0.25, layer: 'frustration' },
740
+ { value: 'security', trait: 'changeBlindness', coefficient: 'chain.valueModulation.security.changeBlindness', layer: 'visualComplexity' },
741
+ // ── Decision Layer ──
663
742
  // Achievement → reduces satisficing demand (optimizers don't settle for "good enough")
664
- { value: 'achievement', trait: 'satisficing', coefficient: -0.20, layer: 'decision' },
743
+ { value: 'achievement', trait: 'satisficing', coefficient: 'chain.valueModulation.achievement.satisficing', layer: 'decision' },
665
744
  // Achievement → increases riskTolerance capacity (confident decision-makers)
666
- { value: 'achievement', trait: 'riskTolerance', coefficient: -0.15, layer: 'decision' },
745
+ { value: 'achievement', trait: 'riskTolerance', coefficient: 'chain.valueModulation.achievement.riskTolerance', layer: 'decision' },
667
746
  // Conformity → amplifies social proof demand (Cialdini 2001: need validation from others)
668
- { value: 'conformity', trait: 'socialProofSensitivity', coefficient: 0.30, layer: 'decision' },
747
+ { value: 'conformity', trait: 'socialProofSensitivity', coefficient: 'chain.valueModulation.conformity.socialProofSensitivity', layer: 'decision' },
669
748
  // Conformity → amplifies anchoring (defer to first/default option)
670
- { value: 'conformity', trait: 'anchoringBias', coefficient: 0.20, layer: 'decision' },
671
- // Stimulation → amplifies curiosity demand (explore more, bored faster)
672
- { value: 'stimulation', trait: 'curiosity', coefficient: 0.25, layer: 'cognitiveLoad' },
673
- // Stimulation → depletes patience (novelty-seekers abandon monotonous pages)
674
- { value: 'stimulation', trait: 'patience', coefficient: 0.20, layer: 'motor' },
749
+ { value: 'conformity', trait: 'anchoringBias', coefficient: 'chain.valueModulation.conformity.anchoringBias', layer: 'decision' },
675
750
  // Self-Direction → reduces conformity-related demand (independent thinkers)
676
- { value: 'selfDirection', trait: 'socialProofSensitivity', coefficient: -0.20, layer: 'decision' },
677
- // Self-Direction → amplifies metacognitive planning (plan their own path)
678
- { value: 'selfDirection', trait: 'metacognitivePlanning', coefficient: -0.15, layer: 'cognitiveLoad' },
679
- // Tradition → amplifies mentalModelFlexibility (resist new UI patterns)
680
- { value: 'tradition', trait: 'mentalModelFlexibility', coefficient: 0.25, layer: 'cognitiveLoad' },
681
- // Benevolence → reduces emotionalContagion cost (emotionally regulated)
682
- { value: 'benevolence', trait: 'emotionalContagion', coefficient: -0.15, layer: 'frustration' },
751
+ { value: 'selfDirection', trait: 'socialProofSensitivity', coefficient: 'chain.valueModulation.selfDirection.socialProofSensitivity', layer: 'decision' },
683
752
  // Power → reduces fear of missing out (confident, not swayed by urgency)
684
- { value: 'power', trait: 'fearOfMissingOut', coefficient: -0.20, layer: 'decision' },
685
- // Universalism → amplifies readingTendency (reads everything, values completeness)
686
- { value: 'universalism', trait: 'readingTendency', coefficient: 0.15, layer: 'readability' },
753
+ { value: 'power', trait: 'fearOfMissingOut', coefficient: 'chain.valueModulation.power.fearOfMissingOut', layer: 'decision' },
754
+ // ── Frustration Layer ──
755
+ // Stimulation → depletes patience (novelty-seekers abandon monotonous pages).
756
+ // Labelled `motor` through 20.x; patience is in frustration.
757
+ { value: 'stimulation', trait: 'patience', coefficient: 'chain.valueModulation.stimulation.patience', layer: 'frustration' },
758
+ // Benevolence → reduces emotionalContagion cost (emotionally regulated)
759
+ { value: 'benevolence', trait: 'emotionalContagion', coefficient: 'chain.valueModulation.benevolence.emotionalContagion', layer: 'frustration' },
687
760
  ];
688
761
  /**
689
762
  * Compute the Sequential Cognitive Transport Cost (CTC).
690
763
  *
691
764
  * Unlike additive cognitive load models, the sequential chain depletes
692
765
  * capacity at each processing layer. A user who spends cognitive resources
693
- * on saliency detection has LESS available for subsequent decision-making.
766
+ * on visual clutter and motion has LESS available for subsequent decision-making.
694
767
  *
695
- * The algorithm processes layers in temporal order:
696
- * Saliency -> Cognitive Load -> Decision -> Motor -> Frustration -> Readability
768
+ * The algorithm processes the eight layers of LAYER_DEFINITIONS in order:
769
+ * visualComplexity -> cognitiveLoad -> decision -> motor -> motorProcedure
770
+ * -> frustration -> readability -> readingAttention
697
771
  *
698
772
  * Each layer:
699
773
  * 1. Applies Schwartz value modulation to demand (v18.54.0)
@@ -702,11 +776,38 @@ const VALUE_DEMAND_MODULATION = [
702
776
  * 4. Records the remaining capacity for downstream layers
703
777
  *
704
778
  * @param persona - OTCognitiveProfile with trait values
705
- * @param demand - 26D demand distribution from page analysis
779
+ * @param demand - 29D demand distribution from page analysis
706
780
  * @param options - Enable/disable asymmetric costs, interaction terms, and value modulation
707
781
  * @returns Complete sequential transport result with per-layer breakdown
708
782
  */
709
783
  export function computeSequentialCTC(persona, demand, options) {
784
+ const cost = transportChain(persona, demand, options);
785
+ const referenceScope = demand.scope ?? 'viewport';
786
+ const reference = ctcReferenceCurve(referenceScope, options);
787
+ // totalCTC depends on every constant the curve was built from, the demand
788
+ // curves included. Re-read them here so a run under withProvenance records
789
+ // the same dependencies whether the curve was built in this call or cached.
790
+ for (const id of reference.reads)
791
+ param(id);
792
+ // Read on every call for the same reason, and checked: the registered page
793
+ // count must be the count the curve was built from, so a reference file
794
+ // swapped without its registry entry is refused rather than ranked against.
795
+ const registeredPages = param(`ctcReference.${referenceScope}`);
796
+ if (registeredPages !== reference.pages) {
797
+ throw new Error(`ctcReference.${referenceScope} registers ${registeredPages} pages; the shipped reference has ${reference.pages}`);
798
+ }
799
+ const totalCTC = percentileAt(reference.curve, cost.rawCTC);
800
+ return { ...cost, totalCTC, totalCTCInterval: intervalAt(reference.interval, cost.rawCTC, totalCTC), referenceScope };
801
+ }
802
+ /**
803
+ * The transport chain itself: demand against capacity, layer by layer, ending
804
+ * at rawCTC. computeSequentialCTC adds the percentile; the reference curve is
805
+ * built from this function directly, since ranking the reference against
806
+ * itself would need the curve it is building. Only `traits` is read from the
807
+ * persona.
808
+ */
809
+ function transportChain(persona, demand, options) {
810
+ const P = readParams(CHAIN_IDS);
710
811
  const useAsymmetric = options?.asymmetric !== false; // default true
711
812
  const useInteractions = options?.interactions !== false; // default true
712
813
  const values = options?.schwartzValues;
@@ -714,7 +815,6 @@ export function computeSequentialCTC(persona, demand, options) {
714
815
  // Values amplify or dampen demands on specific traits, changing what the page "asks" of the user
715
816
  const modulatedDemand = {
716
817
  demands: { ...demand.demands },
717
- variance: { ...demand.variance },
718
818
  };
719
819
  // readingTendency modulates DEMAND, not capacity.
720
820
  //
@@ -736,7 +836,7 @@ export function computeSequentialCTC(persona, demand, options) {
736
836
  // than ADHD on a text-dense page after this. (2026-08-02)
737
837
  const readingTendency = persona.traits?.readingTendency;
738
838
  if (typeof readingTendency === "number") {
739
- const engagement = 0.75 + readingTendency * 0.5; // 0.75 at pure skim, 1.25 at careful
839
+ const engagement = P.engagementAtSkim + readingTendency * P.engagementRange; // 0.75 at pure skim, 1.25 at careful
740
840
  // ATTENTIONAL demand only. Applied to `readingCapacity` as well for one
741
841
  // commit, and that was wrong on the merits as well as breaking a contract.
742
842
  //
@@ -757,22 +857,25 @@ export function computeSequentialCTC(persona, demand, options) {
757
857
  }
758
858
  }
759
859
  if (values && Object.keys(values).length > 0) {
860
+ const demandFloor = param('chain.valueModulation.demandFloor');
760
861
  for (const mod of VALUE_DEMAND_MODULATION) {
862
+ // Read before the skip so a run records every coefficient it could have used.
863
+ const coefficient = param(mod.coefficient);
761
864
  const valueScore = values[mod.value];
762
865
  if (valueScore === undefined || valueScore === null)
763
866
  continue;
764
867
  const currentDemand = modulatedDemand.demands[mod.trait] ?? 0;
765
868
  // Modulation: demand += coefficient * valueScore * currentDemand
766
869
  // High value score amplifies the modulation effect
767
- // Multiplicative on current demand — no effect on zero-demand traits
768
- const modulation = mod.coefficient * valueScore * Math.max(0.1, currentDemand);
870
+ // Multiplicative on current demand, floored so a zero-demand trait still moves a little
871
+ const modulation = coefficient * valueScore * Math.max(demandFloor, currentDemand);
769
872
  modulatedDemand.demands[mod.trait] = Math.max(0, Math.min(1, currentDemand + modulation));
770
873
  }
771
874
  }
772
875
  // Initialize residual capacity from persona traits
773
876
  const residualCapacity = {};
774
877
  for (const dim of DEMAND_DIMENSIONS) {
775
- residualCapacity[dim] = options?.carriedCapacity?.[dim] ?? traitValue(persona.traits, dim);
878
+ residualCapacity[dim] = options?.carriedCapacity?.[dim] ?? traitValue(persona.traits, dim, P);
776
879
  }
777
880
  const layers = [];
778
881
  const traitCosts = {};
@@ -799,17 +902,25 @@ export function computeSequentialCTC(persona, demand, options) {
799
902
  let layerHeadroom = 0;
800
903
  for (const trait of layerDef.traits) {
801
904
  const d = modulatedDemand.demands[trait] ?? 0;
802
- const c = residualCapacity[trait] ?? 0.5;
905
+ const c = residualCapacity[trait] ?? P.missingTraitCapacity;
803
906
  const gap = d - c;
804
907
  let cost;
805
- if (gap > 0) {
908
+ if (useAsymmetric && EXPOSURE_DIMENSIONS.has(trait)) {
909
+ // Exposure: the stimulus acting on a susceptibility, w c d (see
910
+ // EXPOSURE_DIMENSIONS). A cost the page imposes, so it is counted with
911
+ // the deficit, which abandonment reads. Depletion can only lower c, so
912
+ // on a page with no stimulus it stays 0.
913
+ cost = P.weightSurplus * c * d;
914
+ layerDeficit += cost;
915
+ }
916
+ else if (gap > 0) {
806
917
  // Deficit: demand exceeds capacity — expensive
807
- cost = (useAsymmetric ? WEIGHT_DEFICIT : 1.0) * gap * gap;
918
+ cost = (useAsymmetric ? P.weightDeficit : 1.0) * gap * gap;
808
919
  layerDeficit += cost;
809
920
  }
810
921
  else {
811
922
  // Surplus: capacity exceeds demand — cheap
812
- const surplusWeight = SURPLUS_FREE_DIMENSIONS.has(trait) ? 0 : WEIGHT_SURPLUS;
923
+ const surplusWeight = SURPLUS_FREE_DIMENSIONS.has(trait) ? 0 : P.weightSurplus;
813
924
  cost = (useAsymmetric ? surplusWeight : 1.0) * gap * gap;
814
925
  layerSurplus += cost;
815
926
  }
@@ -821,7 +932,7 @@ export function computeSequentialCTC(persona, demand, options) {
821
932
  layerHeadroom = layerHeadroom / Math.max(1, layerDef.traits.length);
822
933
  // Scale layer cost by demand presence — empty pages shouldn't produce high costs
823
934
  const layerDemandMagnitude = layerDef.traits.reduce((sum, t) => sum + (modulatedDemand.demands[t] ?? 0), 0) / layerDef.traits.length;
824
- const contentScale = Math.min(1.0, layerDemandMagnitude * 3); // 0-1 scale, saturates at demand=0.33
935
+ const contentScale = Math.min(1.0, layerDemandMagnitude * P.contentScaleGain); // 0-1 scale, saturates at demand=0.33
825
936
  layerCost *= contentScale;
826
937
  layerDeficit *= contentScale;
827
938
  layerSurplus *= contentScale;
@@ -830,8 +941,15 @@ export function computeSequentialCTC(persona, demand, options) {
830
941
  additiveCTC += layerCost;
831
942
  // Deplete capacity for next layers (only when page has meaningful content)
832
943
  // Each trait in this layer loses capacity proportional to the layer's total cost
833
- const capacityConsumed = DEPLETION_RATE * layerCost;
834
- if (contentScale > 0.1) {
944
+ const DEPLETION_RATE = P.depletionRate;
945
+ const MAX_DRAWDOWN_FRACTION = P.maxDrawdownFraction;
946
+ const INDIRECT_DEPLETION_SHARE = P.indirectDepletionShare;
947
+ const CAPACITY_EPSILON = P.capacityEpsilon;
948
+ // What this layer actually removed, summed over every dimension from the
949
+ // balances themselves (LayerResult.capacityConsumed). It stays 0 when the
950
+ // gate below keeps the layer from depleting.
951
+ let capacityConsumed = 0;
952
+ if (contentScale > P.depletionGate) {
835
953
  // PROPORTIONAL DRAWDOWN, not subtraction against a clamp.
836
954
  //
837
955
  // Capacity used to be spent as a flat amount and clamped at
@@ -855,17 +973,22 @@ export function computeSequentialCTC(persona, demand, options) {
855
973
  // three and no `k` anywhere in the arithmetic. (2026-08-03)
856
974
  const drawFraction = Math.min(MAX_DRAWDOWN_FRACTION, DEPLETION_RATE * layerCost);
857
975
  for (const trait of layerDef.traits) {
976
+ const before = residualCapacity[trait];
858
977
  residualCapacity[trait] *= (1 - drawFraction);
859
978
  residualCapacity[trait] = Math.max(CAPACITY_EPSILON, residualCapacity[trait]);
979
+ // A floor lift (a dimension below CAPACITY_EPSILON raised to it) is not a draw.
980
+ capacityConsumed += Math.max(0, before - residualCapacity[trait]);
860
981
  }
861
982
  // Also deplete shared traits that appear in later layers
862
983
  // (cross-layer fatigue: cognitive exhaustion bleeds across boundaries)
863
- const indirectFraction = Math.min(MAX_DRAWDOWN_FRACTION, (DEPLETION_RATE * 0.5) * layerCost);
984
+ const indirectFraction = Math.min(MAX_DRAWDOWN_FRACTION, (DEPLETION_RATE * INDIRECT_DEPLETION_SHARE) * layerCost);
864
985
  for (const dim of DEMAND_DIMENSIONS) {
865
986
  if (!layerDef.traits.includes(dim)) {
866
987
  // Indirect depletion at half rate
988
+ const before = residualCapacity[dim];
867
989
  residualCapacity[dim] *= (1 - indirectFraction);
868
990
  residualCapacity[dim] = Math.max(CAPACITY_EPSILON, residualCapacity[dim]);
991
+ capacityConsumed += Math.max(0, before - residualCapacity[dim]);
869
992
  }
870
993
  }
871
994
  }
@@ -877,7 +1000,7 @@ export function computeSequentialCTC(persona, demand, options) {
877
1000
  // Mean spare capacity across this layer's dimensions. Only meaningful
878
1001
  // when the cost is at or near zero; above that the cost carries the
879
1002
  // signal itself.
880
- headroom: Math.round(layerHeadroom * 1000) / 1000,
1003
+ headroom: Math.round(layerHeadroom * P.roundingScale) / P.roundingScale,
881
1004
  });
882
1005
  }
883
1006
  // Compute interaction terms
@@ -891,7 +1014,7 @@ export function computeSequentialCTC(persona, demand, options) {
891
1014
  for (const pair of INTERACTION_PAIRS) {
892
1015
  const costA = layerCostMap[pair.a] ?? 0;
893
1016
  const costB = layerCostMap[pair.b] ?? 0;
894
- const interaction = costA * costB * pair.weight;
1017
+ const interaction = costA * costB * param(pair.weight);
895
1018
  const key = `${pair.a}x${pair.b}`;
896
1019
  interactions[key] = interaction;
897
1020
  interactionTotal += interaction;
@@ -901,14 +1024,14 @@ export function computeSequentialCTC(persona, demand, options) {
901
1024
  // Note: additiveCTC is layers-only (no interactions, no sequential effects)
902
1025
  // The sequential effect is already embedded in layer costs via capacity depletion
903
1026
  const rawCTC = layers.reduce((sum, l) => sum + l.transportCost, 0) + interactionTotal;
904
- // Normalize CTC to a 0-1 scale that maps to human-interpretable difficulty
905
- // Calibration: raw CTC from the W₂ formula tends to be 0.3-3.0 for real pages.
906
- // We normalize using a sigmoid so: 0.5 raw → 0.2, 1.0 → 0.4, 2.0 → 0.7, 3.0 → 0.9
907
- // This ensures "easy" pages (simple layout, clear content) land below 0.3
908
- const totalCTC = 1 / (1 + Math.exp(-2.5 * (rawCTC - 1.2)));
1027
+ // No normalisation here. Through 21.0 totalCTC was a logistic of rawCTC,
1028
+ // slope 2.5 and centre 1.2 (chain.ctcSigmoidSlope / chain.ctcSigmoidCentre,
1029
+ // both hand-set), which nothing tied to where real pages fall: at viewport
1030
+ // scope it put half of them between 0.055 and 0.133. computeSequentialCTC
1031
+ // now ranks rawCTC against real pages instead.
909
1032
  // Find bottleneck layer
910
1033
  let maxLayerCost = 0;
911
- let bottleneckLayer = layers[0]?.name ?? 'saliency';
1034
+ let bottleneckLayer = layers[0]?.name ?? LAYER_DEFINITIONS[0].name;
912
1035
  for (const layer of layers) {
913
1036
  if (layer.transportCost > maxLayerCost) {
914
1037
  maxLayerCost = layer.transportCost;
@@ -939,21 +1062,37 @@ export function computeSequentialCTC(persona, demand, options) {
939
1062
  // equal to tolerance gives 50%, and it approaches 1 from below. The
940
1063
  // "centre at 1.5x blended patience" intent is unchanged and so is the
941
1064
  // persona ordering; what is gone is the offset. (2026-08-03, BUG-17)
942
- const patienceCapacity = traitValue(persona.traits, 'patience');
943
- const resilienceCapacity = traitValue(persona.traits, 'resilience');
944
- const effectivePatience = patienceCapacity * 0.7 + resilienceCapacity * 0.3; // blended tolerance
945
- const adjustedCost = Math.max(0, totalDeficit - totalSurplus * 0.3); // surplus comfort (30%)
1065
+ //
1066
+ // UNMET DEMAND ONLY, as of 21.0. Through 20.x the hazard's input was
1067
+ // deficit - 0.3 x surplus: billed surplus (a maximiser over-deliberating a
1068
+ // trivial page) was RELIEF here while the same surplus was COST in totalCTC.
1069
+ // One quantity cannot be both. A disposition surplus is effort spent, not
1070
+ // comfort, so it no longer offsets the deficit; chain.abandonment.surplusRelief
1071
+ // is gone. The per-screen hazard in computeSequentialScrollCTC reads this
1072
+ // value, so it changed with it.
1073
+ //
1074
+ // RESIDUAL TOLERANCE, as of 21.0. The page spends patience in the
1075
+ // frustration layer and drains resilience through cross-layer depletion;
1076
+ // judging abandonment against the persona as it ARRIVED treated that spending
1077
+ // as free. Tolerance now comes from what the page left (residualCapacity at
1078
+ // the end of the layer loop), so a later screen in a scroll is judged against
1079
+ // the smaller budget it actually has.
1080
+ const patienceCapacity = residualCapacity.patience;
1081
+ const resilienceCapacity = residualCapacity.resilience;
1082
+ const effectivePatience = patienceCapacity * P.patienceShare + resilienceCapacity * P.resilienceShare; // blended tolerance
1083
+ const unmetDemand = totalDeficit;
946
1084
  // Floored so a persona with no patience at all does not divide by zero and
947
1085
  // read as certain abandonment on a blank page.
948
- const tolerance = Math.max(0.05, effectivePatience * 1.5);
949
- const ABANDONMENT_STEEPNESS = ABANDONMENT_HILL_EXPONENT;
950
- const abandonmentRisk = adjustedCost <= 0 ? 0 : Math.max(0, Math.min(1, Math.pow(adjustedCost, ABANDONMENT_STEEPNESS) /
951
- (Math.pow(adjustedCost, ABANDONMENT_STEEPNESS) + Math.pow(tolerance, ABANDONMENT_STEEPNESS))));
1086
+ const tolerance = Math.max(P.toleranceFloor, effectivePatience * P.toleranceGain);
1087
+ // The same registry value ABANDONMENT_HILL_EXPONENT exports; read here so the
1088
+ // dependency is recorded per run.
1089
+ const ABANDONMENT_STEEPNESS = P.abandonmentHillExponent;
1090
+ const abandonmentRisk = unmetDemand <= 0 ? 0 : Math.max(0, Math.min(1, Math.pow(unmetDemand, ABANDONMENT_STEEPNESS) /
1091
+ (Math.pow(unmetDemand, ABANDONMENT_STEEPNESS) + Math.pow(tolerance, ABANDONMENT_STEEPNESS))));
952
1092
  return {
953
- totalCTC,
954
- // The un-normalized cost, exposed because totalCTC is a SIGMOID of it and
955
- // the two are not interchangeable. Every ratio against additiveCTC has to
956
- // use this one: additiveCTC is a raw sum, totalCTC is squashed to 0-1, and
1093
+ // The un-normalized cost, exposed because totalCTC is a PERCENTILE of it
1094
+ // and the two are not interchangeable. Every ratio against additiveCTC has
1095
+ // to use this one: additiveCTC is a raw sum, totalCTC is a rank in 0-1, and
957
1096
  // dividing one by the other compares different units. (2026-08-02)
958
1097
  rawCTC,
959
1098
  layers,
@@ -967,12 +1106,84 @@ export function computeSequentialCTC(persona, demand, options) {
967
1106
  remainingCapacity: { ...residualCapacity },
968
1107
  };
969
1108
  }
1109
+ const referenceCurves = new Map();
1110
+ const overriddenReferenceCurves = new WeakMap();
1111
+ /**
1112
+ * The percentile curve for one scope and option set: rawCTC of every graded
1113
+ * reference page (ctc-reference.ts) for every built-in persona, through this
1114
+ * chain, pooled. Each page's metrics are derived from its stored raw
1115
+ * measurement (deriveMetrics) inside the build, so the page-metric
1116
+ * normalisers move the curve exactly as the chain's constants do.
1117
+ *
1118
+ * WHICH OPTIONS SELECT A CURVE. `asymmetric` and `interactions` change the
1119
+ * cost function, so each pair has its own curve: a symmetric raw cost ranked
1120
+ * among asymmetric ones would compare two different quantities.
1121
+ * `schwartzValues` and `carriedCapacity` do not. Both describe the person
1122
+ * being scored (what they value, what earlier screens left them), not the
1123
+ * yardstick, and a curve that moved with them would cancel part of the effect
1124
+ * it is there to show. So a values-modulated score, and every screen after the
1125
+ * first in a scroll, are ranked against the reference population as it
1126
+ * arrives at a page. cognitive_effort's useValues edits demand before the chain
1127
+ * runs, which lands in the same place.
1128
+ *
1129
+ * COST AND CACHE. One build is one deriveMetrics per page, one chain run per
1130
+ * page per persona, and the page bootstrap (ctcReference.interval.*), under a
1131
+ * second for the whole reference; cached per scope and option set for the
1132
+ * life of the process, so every later score pays two binary searches. While a
1133
+ * test overrides registered constants (activeOverrides() is non-null) a curve
1134
+ * is cached against that override object instead: a change to any chain or
1135
+ * page-metric constant re-derives the curve rather than reusing one built
1136
+ * under other constants.
1137
+ */
1138
+ export function ctcReferenceCurve(scope, options) {
1139
+ const asymmetric = options?.asymmetric !== false;
1140
+ const interactions = options?.interactions !== false;
1141
+ const overrides = activeOverrides();
1142
+ let cache = referenceCurves;
1143
+ if (overrides) {
1144
+ cache = overriddenReferenceCurves.get(overrides) ?? new Map();
1145
+ overriddenReferenceCurves.set(overrides, cache);
1146
+ }
1147
+ const key = `${scope}|${asymmetric}|${interactions}`;
1148
+ const hit = cache.get(key);
1149
+ if (hit)
1150
+ return hit;
1151
+ const population = referencePopulation();
1152
+ const { result: { costs, pages, settings }, ids } = readsOf(() => {
1153
+ const raws = referenceRaw(scope);
1154
+ // Page-major, every persona of a page together: the bootstrap resamples pages.
1155
+ const out = new Float64Array(raws.length * population.length);
1156
+ let k = 0;
1157
+ for (const raw of raws) {
1158
+ const demand = computeDemandDistribution(deriveMetrics(raw));
1159
+ for (const persona of population)
1160
+ out[k++] = transportChain(persona, demand, { asymmetric, interactions }).rawCTC;
1161
+ }
1162
+ const settings = {
1163
+ replicates: param('ctcReference.interval.replicates'),
1164
+ seed: param('ctcReference.interval.seed'),
1165
+ level: param('ctcReference.interval.level'),
1166
+ grid: param('ctcReference.interval.grid'),
1167
+ };
1168
+ return { costs: out, pages: raws.length, settings };
1169
+ });
1170
+ const curve = percentileCurve(costs);
1171
+ const built = {
1172
+ scope, asymmetric, interactions,
1173
+ pages, personas: population.length,
1174
+ reads: [...ids].sort(),
1175
+ curve,
1176
+ interval: bootstrapPercentileInterval(costs, population.length, settings),
1177
+ };
1178
+ cache.set(key, built);
1179
+ return built;
1180
+ }
970
1181
  // ── 3. Convenience: Full COT Pipeline ──
971
1182
  /**
972
1183
  * End-to-end Cognitive Optimal Transport computation.
973
1184
  *
974
1185
  * Extracts page metrics from a live Playwright page, computes the
975
- * 26D demand distribution, and runs the full sequential transport chain.
1186
+ * 29D demand distribution, and runs the full sequential transport chain.
976
1187
  *
977
1188
  * This is the primary entry point for MCP tools that need a single
978
1189
  * CTC score with full breakdown.
@@ -984,7 +1195,7 @@ export function computeSequentialCTC(persona, demand, options) {
984
1195
  export async function computeFullCOT(persona, page) {
985
1196
  // Extract page metrics via Playwright's page.evaluate()
986
1197
  const metrics = await extractPageMetrics(page);
987
- // Compute 26D demand distribution
1198
+ // Compute 29D demand distribution
988
1199
  const demand = computeDemandDistribution(metrics);
989
1200
  // Run sequential transport chain with full options
990
1201
  return computeSequentialCTC(persona, demand, {
@@ -996,17 +1207,30 @@ export async function computeFullCOT(persona, page) {
996
1207
  /**
997
1208
  * Compute demand distribution from raw metric values (no page required).
998
1209
  * Useful for testing, simulation, and sensitivity analysis.
1210
+ *
1211
+ * informationDensity, textDensity and navigationDepth are carried onto the
1212
+ * PageMetrics as reported values; since Stage C no demand reads them.
1213
+ * choosingSecondsExpert (Stage C) is the choosing time's Hick-Hyman expert
1214
+ * limit. It is required like the other law times and moves a demand only when
1215
+ * options.siteFamiliarity is above 0.
999
1216
  */
1000
- export function computeDemandFromRawMetrics(informationDensity, visualComplexity, interactiveElementCount, textDensity, animationLevel, choiceCount, navigationDepth) {
1217
+ export function computeDemandFromRawMetrics(informationDensity, visualComplexity, textDensity, animationLevel, navigationDepth, pointingSeconds, choosingSeconds, readingSeconds, procedureSeconds, choosingSecondsExpert, options) {
1218
+ // interactiveElementCount and choiceCount are reported counts that no demand
1219
+ // reads since Stage B; the law times replace them.
1001
1220
  return computeDemandDistribution({
1002
1221
  informationDensity,
1003
1222
  visualComplexity,
1004
- interactiveElementCount,
1223
+ interactiveElementCount: 0,
1005
1224
  textDensity,
1006
1225
  animationLevel,
1007
- choiceCount,
1226
+ choiceCount: 0,
1008
1227
  navigationDepth,
1009
- });
1228
+ pointingSeconds,
1229
+ choosingSeconds,
1230
+ readingSeconds,
1231
+ procedureSeconds,
1232
+ choosingSecondsExpert,
1233
+ }, options);
1010
1234
  }
1011
1235
  /**
1012
1236
  * Compare sequential vs. additive CTC to quantify the sequential effect.
@@ -1062,11 +1286,9 @@ export function topCostlyTraits(result, n = 5) {
1062
1286
  export function baselineCTC(persona) {
1063
1287
  const emptyDemand = {
1064
1288
  demands: {},
1065
- variance: {},
1066
1289
  };
1067
1290
  for (const dim of DEMAND_DIMENSIONS) {
1068
1291
  emptyDemand.demands[dim] = 0;
1069
- emptyDemand.variance[dim] = 0;
1070
1292
  }
1071
1293
  return computeSequentialCTC(persona, emptyDemand);
1072
1294
  }
@@ -1098,29 +1320,30 @@ export function baselineCTC(persona) {
1098
1320
  * is load-bearing here too and remains uncalibrated.
1099
1321
  */
1100
1322
  export function computeSequentialScrollCTC(persona, screenDemands, options) {
1323
+ const P = readParams(CHAIN_IDS);
1324
+ const R = P.roundingScale;
1325
+ const depthR = param('chain.depthRoundingScale');
1101
1326
  const screens = [];
1102
1327
  let carried;
1103
1328
  let survive = 1;
1104
- const patience = traitValue(persona.traits, 'patience');
1105
- const resilience = traitValue(persona.traits, 'resilience');
1106
- const tolerance = Math.max(0.05, (patience * 0.7 + resilience * 0.3) * 1.5);
1107
1329
  for (let i = 0; i < screenDemands.length; i++) {
1108
1330
  const r = computeSequentialCTC(persona, screenDemands[i], { ...options, carriedCapacity: carried });
1109
1331
  carried = r.remainingCapacity;
1110
- // Hazard for THIS screen, from this screen's own unmet demand. Surplus
1111
- // relief applies as it does in the aggregate.
1112
- const screenCost = Math.max(0, r.deficitCost - r.surplusCost * 0.3);
1113
- const stopHazard = screenCost <= 0 ? 0 : Math.min(1, Math.pow(screenCost, ABANDONMENT_HILL_EXPONENT) /
1114
- (Math.pow(screenCost, ABANDONMENT_HILL_EXPONENT) + Math.pow(tolerance, ABANDONMENT_HILL_EXPONENT)));
1332
+ // Hazard for THIS screen: the screen's own abandonment risk, which is the
1333
+ // Hill hazard of its unmet demand. One formula, read from the chain run,
1334
+ // rather than a second copy here that could drift from it (the copy that
1335
+ // was here applied surplus relief in step with the aggregate; 21.0 removed
1336
+ // it from both by removing this copy).
1337
+ const stopHazard = r.abandonmentRisk;
1115
1338
  const reachProbability = survive;
1116
1339
  survive = survive * (1 - stopHazard);
1117
1340
  screens.push({
1118
1341
  screen: i + 1,
1119
1342
  layers: r.layers,
1120
- screenCTC: Math.round(r.totalCTC * 1000) / 1000,
1121
- stopHazard: Math.round(stopHazard * 1000) / 1000,
1122
- cumulativeAbandonment: Math.round((1 - survive) * 1000) / 1000,
1123
- reachProbability: Math.round(reachProbability * 1000) / 1000,
1343
+ screenCTC: Math.round(r.totalCTC * R) / R,
1344
+ stopHazard: Math.round(stopHazard * R) / R,
1345
+ cumulativeAbandonment: Math.round((1 - survive) * R) / R,
1346
+ reachProbability: Math.round(reachProbability * R) / R,
1124
1347
  bottleneckLayer: r.bottleneckLayer,
1125
1348
  remainingCapacity: r.remainingCapacity,
1126
1349
  });
@@ -1131,18 +1354,18 @@ export function computeSequentialScrollCTC(persona, screenDemands, options) {
1131
1354
  for (const s of screens) {
1132
1355
  const lost = s.reachProbability * s.stopHazard;
1133
1356
  if (!dropOff || lost > dropOff.share) {
1134
- dropOff = { screen: s.screen, layer: s.bottleneckLayer, share: Math.round(lost * 1000) / 1000 };
1357
+ dropOff = { screen: s.screen, layer: s.bottleneckLayer, share: Math.round(lost * R) / R };
1135
1358
  }
1136
1359
  }
1137
1360
  if (dropOff && dropOff.share <= 0)
1138
1361
  dropOff = null;
1139
1362
  return {
1140
1363
  screens,
1141
- totalAbandonment: Math.round((1 - survive) * 1000) / 1000,
1364
+ totalAbandonment: Math.round((1 - survive) * R) / R,
1142
1365
  // Sum of reach probabilities: the expected count of screens actually seen.
1143
- expectedDepthScreens: Math.round(screens.reduce((a, s) => a + s.reachProbability, 0) * 100) / 100,
1366
+ expectedDepthScreens: Math.round(screens.reduce((a, s) => a + s.reachProbability, 0) * depthR) / depthR,
1144
1367
  dropOff,
1145
- reachWeightedCTC: Math.round(screens.reduce((a, s) => a + s.reachProbability * s.screenCTC, 0) * 1000) / 1000,
1368
+ reachWeightedCTC: Math.round(screens.reduce((a, s) => a + s.reachProbability * s.screenCTC, 0) * R) / R,
1146
1369
  };
1147
1370
  }
1148
1371
  //# sourceMappingURL=cognitive-transport-chain.js.map