cbrowser 18.78.0 → 18.80.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 (235) hide show
  1. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  2. package/dist/analysis/accessibility-empathy.js +395 -66
  3. package/dist/analysis/accessibility-empathy.js.map +1 -1
  4. package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
  5. package/dist/analysis/agent-ready-audit.js +78 -0
  6. package/dist/analysis/agent-ready-audit.js.map +1 -1
  7. package/dist/analysis/page-understanding.d.ts +139 -1
  8. package/dist/analysis/page-understanding.d.ts.map +1 -1
  9. package/dist/analysis/page-understanding.js +179 -12
  10. package/dist/analysis/page-understanding.js.map +1 -1
  11. package/dist/artifact-store.d.ts +19 -5
  12. package/dist/artifact-store.d.ts.map +1 -1
  13. package/dist/artifact-store.js +41 -11
  14. package/dist/artifact-store.js.map +1 -1
  15. package/dist/browser.d.ts +9 -0
  16. package/dist/browser.d.ts.map +1 -1
  17. package/dist/browser.js +81 -5
  18. package/dist/browser.js.map +1 -1
  19. package/dist/cli.js +55 -10
  20. package/dist/cli.js.map +1 -1
  21. package/dist/cognitive/index.d.ts.map +1 -1
  22. package/dist/cognitive/index.js +3 -2
  23. package/dist/cognitive/index.js.map +1 -1
  24. package/dist/config.d.ts +57 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +234 -7
  27. package/dist/config.js.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/mcp-server-remote.d.ts.map +1 -1
  31. package/dist/mcp-server-remote.js +108 -4
  32. package/dist/mcp-server-remote.js.map +1 -1
  33. package/dist/mcp-server.d.ts.map +1 -1
  34. package/dist/mcp-server.js +62 -938
  35. package/dist/mcp-server.js.map +1 -1
  36. package/dist/mcp-tools/account-personas.d.ts +41 -0
  37. package/dist/mcp-tools/account-personas.d.ts.map +1 -0
  38. package/dist/mcp-tools/account-personas.js +98 -0
  39. package/dist/mcp-tools/account-personas.js.map +1 -0
  40. package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
  41. package/dist/mcp-tools/base/analysis-tools.js +12 -2
  42. package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
  43. package/dist/mcp-tools/base/artifact-tools.d.ts +3 -0
  44. package/dist/mcp-tools/base/artifact-tools.d.ts.map +1 -0
  45. package/dist/mcp-tools/base/artifact-tools.js +78 -0
  46. package/dist/mcp-tools/base/artifact-tools.js.map +1 -0
  47. package/dist/mcp-tools/base/audit-tools.d.ts +11 -0
  48. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  49. package/dist/mcp-tools/base/audit-tools.js +595 -340
  50. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  51. package/dist/mcp-tools/base/browser-management-tools.d.ts.map +1 -1
  52. package/dist/mcp-tools/base/browser-management-tools.js +18 -0
  53. package/dist/mcp-tools/base/browser-management-tools.js.map +1 -1
  54. package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
  55. package/dist/mcp-tools/base/bug-analysis-tools.js +59 -3
  56. package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
  57. package/dist/mcp-tools/base/capture-tools.d.ts +37 -0
  58. package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
  59. package/dist/mcp-tools/base/capture-tools.js +267 -6
  60. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  61. package/dist/mcp-tools/base/cognitive-tools.d.ts +12 -0
  62. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
  63. package/dist/mcp-tools/base/cognitive-tools.js +63 -15
  64. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
  65. package/dist/mcp-tools/base/extraction-tools.d.ts.map +1 -1
  66. package/dist/mcp-tools/base/extraction-tools.js +33 -2
  67. package/dist/mcp-tools/base/extraction-tools.js.map +1 -1
  68. package/dist/mcp-tools/base/index.d.ts +2 -0
  69. package/dist/mcp-tools/base/index.d.ts.map +1 -1
  70. package/dist/mcp-tools/base/index.js +9 -0
  71. package/dist/mcp-tools/base/index.js.map +1 -1
  72. package/dist/mcp-tools/base/marketing-tools.js +3 -3
  73. package/dist/mcp-tools/base/marketing-tools.js.map +1 -1
  74. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
  75. package/dist/mcp-tools/base/persona-comparison-tools.js +529 -40
  76. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
  77. package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
  78. package/dist/mcp-tools/base/site-knowledge-tools.js +36 -3
  79. package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
  80. package/dist/mcp-tools/base/values-tools.d.ts +113 -0
  81. package/dist/mcp-tools/base/values-tools.d.ts.map +1 -1
  82. package/dist/mcp-tools/base/values-tools.js +868 -78
  83. package/dist/mcp-tools/base/values-tools.js.map +1 -1
  84. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  85. package/dist/mcp-tools/base/visual-testing-tools.js +130 -10
  86. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  87. package/dist/mcp-tools/index.d.ts +3 -1
  88. package/dist/mcp-tools/index.d.ts.map +1 -1
  89. package/dist/mcp-tools/index.js +3 -28
  90. package/dist/mcp-tools/index.js.map +1 -1
  91. package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -1
  92. package/dist/mcp-tools/persona-creation-tools.js +363 -20
  93. package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
  94. package/dist/mcp-tools/persona-lifecycle-tools.d.ts +3 -0
  95. package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -0
  96. package/dist/mcp-tools/persona-lifecycle-tools.js +406 -0
  97. package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -0
  98. package/dist/mcp-tools/screenshot-utils.d.ts.map +1 -1
  99. package/dist/mcp-tools/screenshot-utils.js +56 -10
  100. package/dist/mcp-tools/screenshot-utils.js.map +1 -1
  101. package/dist/mcp-tools/tier-gate.js +2 -2
  102. package/dist/mcp-tools/tier-gate.js.map +1 -1
  103. package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
  104. package/dist/mcp-tools/tool-categories.js +4 -1
  105. package/dist/mcp-tools/tool-categories.js.map +1 -1
  106. package/dist/mcp-tools/ui-resources.d.ts +167 -0
  107. package/dist/mcp-tools/ui-resources.d.ts.map +1 -0
  108. package/dist/mcp-tools/ui-resources.js +707 -0
  109. package/dist/mcp-tools/ui-resources.js.map +1 -0
  110. package/dist/mcp-tools/widget-kit.d.ts +299 -0
  111. package/dist/mcp-tools/widget-kit.d.ts.map +1 -0
  112. package/dist/mcp-tools/widget-kit.js +2034 -0
  113. package/dist/mcp-tools/widget-kit.js.map +1 -0
  114. package/dist/mcp-ui-resources.d.ts.map +1 -1
  115. package/dist/mcp-ui-resources.js +14 -4
  116. package/dist/mcp-ui-resources.js.map +1 -1
  117. package/dist/persona-questionnaire.d.ts +69 -1
  118. package/dist/persona-questionnaire.d.ts.map +1 -1
  119. package/dist/persona-questionnaire.js +177 -76
  120. package/dist/persona-questionnaire.js.map +1 -1
  121. package/dist/persona-scope.d.ts +20 -0
  122. package/dist/persona-scope.d.ts.map +1 -0
  123. package/dist/persona-scope.js +59 -0
  124. package/dist/persona-scope.js.map +1 -0
  125. package/dist/personas.d.ts +51 -17
  126. package/dist/personas.d.ts.map +1 -1
  127. package/dist/personas.js +947 -286
  128. package/dist/personas.js.map +1 -1
  129. package/dist/recording/capture-player.d.ts +68 -0
  130. package/dist/recording/capture-player.d.ts.map +1 -0
  131. package/dist/recording/capture-player.js +298 -0
  132. package/dist/recording/capture-player.js.map +1 -0
  133. package/dist/recording/engine.d.ts +52 -0
  134. package/dist/recording/engine.d.ts.map +1 -1
  135. package/dist/recording/engine.js +241 -3
  136. package/dist/recording/engine.js.map +1 -1
  137. package/dist/recording/inline-player.d.ts +39 -0
  138. package/dist/recording/inline-player.d.ts.map +1 -0
  139. package/dist/recording/inline-player.js +272 -0
  140. package/dist/recording/inline-player.js.map +1 -0
  141. package/dist/recording/saliency-overlay.d.ts +133 -0
  142. package/dist/recording/saliency-overlay.d.ts.map +1 -0
  143. package/dist/recording/saliency-overlay.js +360 -0
  144. package/dist/recording/saliency-overlay.js.map +1 -0
  145. package/dist/recording/types.d.ts +32 -0
  146. package/dist/recording/types.d.ts.map +1 -1
  147. package/dist/recording/types.js +46 -0
  148. package/dist/recording/types.js.map +1 -1
  149. package/dist/site-model/manager.d.ts +20 -0
  150. package/dist/site-model/manager.d.ts.map +1 -1
  151. package/dist/site-model/manager.js +52 -0
  152. package/dist/site-model/manager.js.map +1 -1
  153. package/dist/trait-reference.d.ts +26 -2
  154. package/dist/trait-reference.d.ts.map +1 -1
  155. package/dist/trait-reference.js +129 -19
  156. package/dist/trait-reference.js.map +1 -1
  157. package/dist/types.d.ts +105 -18
  158. package/dist/types.d.ts.map +1 -1
  159. package/dist/types.js.map +1 -1
  160. package/dist/url-resolution.d.ts +58 -0
  161. package/dist/url-resolution.d.ts.map +1 -0
  162. package/dist/url-resolution.js +143 -0
  163. package/dist/url-resolution.js.map +1 -0
  164. package/dist/values/big-five-inference.d.ts +121 -0
  165. package/dist/values/big-five-inference.d.ts.map +1 -0
  166. package/dist/values/big-five-inference.js +241 -0
  167. package/dist/values/big-five-inference.js.map +1 -0
  168. package/dist/values/big-five-values.d.ts +76 -0
  169. package/dist/values/big-five-values.d.ts.map +1 -0
  170. package/dist/values/big-five-values.js +152 -0
  171. package/dist/values/big-five-values.js.map +1 -0
  172. package/dist/values/index.d.ts +1 -1
  173. package/dist/values/index.d.ts.map +1 -1
  174. package/dist/values/index.js +1 -1
  175. package/dist/values/index.js.map +1 -1
  176. package/dist/values/persona-completeness.d.ts +91 -0
  177. package/dist/values/persona-completeness.d.ts.map +1 -0
  178. package/dist/values/persona-completeness.js +114 -0
  179. package/dist/values/persona-completeness.js.map +1 -0
  180. package/dist/values/persona-diff.d.ts +50 -0
  181. package/dist/values/persona-diff.d.ts.map +1 -0
  182. package/dist/values/persona-diff.js +126 -0
  183. package/dist/values/persona-diff.js.map +1 -0
  184. package/dist/values/persona-values.d.ts +16 -0
  185. package/dist/values/persona-values.d.ts.map +1 -1
  186. package/dist/values/persona-values.js +33 -0
  187. package/dist/values/persona-values.js.map +1 -1
  188. package/dist/values/schwartz-values.d.ts +9 -1
  189. package/dist/values/schwartz-values.d.ts.map +1 -1
  190. package/dist/values/schwartz-values.js +10 -2
  191. package/dist/values/schwartz-values.js.map +1 -1
  192. package/dist/values/value-mappings.d.ts +41 -2
  193. package/dist/values/value-mappings.d.ts.map +1 -1
  194. package/dist/values/value-mappings.js +65 -8
  195. package/dist/values/value-mappings.js.map +1 -1
  196. package/dist/visual/attention-quality.d.ts +27 -0
  197. package/dist/visual/attention-quality.d.ts.map +1 -1
  198. package/dist/visual/attention-quality.js +164 -2
  199. package/dist/visual/attention-quality.js.map +1 -1
  200. package/dist/visual/attention-transport.d.ts +41 -2
  201. package/dist/visual/attention-transport.d.ts.map +1 -1
  202. package/dist/visual/attention-transport.js +98 -26
  203. package/dist/visual/attention-transport.js.map +1 -1
  204. package/dist/visual/barrier-overlay-html.d.ts +10 -0
  205. package/dist/visual/barrier-overlay-html.d.ts.map +1 -1
  206. package/dist/visual/barrier-overlay-html.js.map +1 -1
  207. package/dist/visual/cognitive-models.d.ts +109 -13
  208. package/dist/visual/cognitive-models.d.ts.map +1 -1
  209. package/dist/visual/cognitive-models.js +166 -3
  210. package/dist/visual/cognitive-models.js.map +1 -1
  211. package/dist/visual/cognitive-transport-chain.d.ts +149 -5
  212. package/dist/visual/cognitive-transport-chain.d.ts.map +1 -1
  213. package/dist/visual/cognitive-transport-chain.js +558 -30
  214. package/dist/visual/cognitive-transport-chain.js.map +1 -1
  215. package/dist/visual/cognitive-transport.d.ts +45 -3
  216. package/dist/visual/cognitive-transport.d.ts.map +1 -1
  217. package/dist/visual/cognitive-transport.js +126 -12
  218. package/dist/visual/cognitive-transport.js.map +1 -1
  219. package/dist/visual/fixation-corpus.d.ts +52 -0
  220. package/dist/visual/fixation-corpus.d.ts.map +1 -1
  221. package/dist/visual/fixation-corpus.js +95 -0
  222. package/dist/visual/fixation-corpus.js.map +1 -1
  223. package/dist/visual/llm-relevance.d.ts +193 -0
  224. package/dist/visual/llm-relevance.d.ts.map +1 -0
  225. package/dist/visual/llm-relevance.js +480 -0
  226. package/dist/visual/llm-relevance.js.map +1 -0
  227. package/dist/visual/perceptual-transport.d.ts +60 -0
  228. package/dist/visual/perceptual-transport.d.ts.map +1 -1
  229. package/dist/visual/perceptual-transport.js +228 -19
  230. package/dist/visual/perceptual-transport.js.map +1 -1
  231. package/dist/visual/persona-context.d.ts +23 -0
  232. package/dist/visual/persona-context.d.ts.map +1 -0
  233. package/dist/visual/persona-context.js +44 -0
  234. package/dist/visual/persona-context.js.map +1 -0
  235. package/package.json +2 -1
@@ -41,9 +41,37 @@ export const DEMAND_DIMENSIONS = [
41
41
  // Trust
42
42
  'trustCalibration',
43
43
  // Additional extended traits
44
- 'mentalModelRigidity', 'curiosity',
44
+ 'mentalModelFlexibility', 'curiosity',
45
45
  // Processing
46
46
  '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',
60
+ // Capacity, bridged from the formal reading and pointing models.
61
+ //
62
+ // These layers previously read DISPOSITION traits -- readingTendency for
63
+ // readability, patience and proceduralFluency for motor -- while the modules
64
+ // that actually model decoding and Fitts pointing sat beside them, computing
65
+ // WPM, visual span, phonological penalties, movement times and hit
66
+ // probabilities that no layer ever consumed. A dyslexic persona therefore
67
+ // scored LOWER readability cost than an ADHD persona on a text-dense page.
68
+ //
69
+ // `sustainedAttention` is the attentional half of reading, and it was missing
70
+ // from the model entirely. Taking `readingTendency` out of the readability
71
+ // layer left decoding and nothing else, so an ADHD reader -- whose decoding is
72
+ // typically intact and whose difficulty is holding the line -- read a wall of
73
+ // text at no attentional cost at all.
74
+ 'readingCapacity', 'motorCapacity', 'sustainedAttention',
47
75
  ];
48
76
  /**
49
77
  * Layer definitions for the sequential transport chain.
@@ -56,7 +84,16 @@ const LAYER_DEFINITIONS = [
56
84
  },
57
85
  {
58
86
  name: 'cognitiveLoad',
59
- traits: ['comprehension', 'workingMemory', 'informationForaging'],
87
+ // siteFamiliarity added 2026-08-02. It was in COGNITIVE_TRAITS and in no
88
+ // layer, so it reached the profile, got a capacity, and then contributed to
89
+ // nothing -- the third and final reason the parameter was inert. Knowing a
90
+ // site is a substitute for holding its layout in working memory, which is
91
+ // exactly this layer, and the demand term for it comes from the same
92
+ // navigationDepth signal that already feeds workingMemory here.
93
+ // `transferLearning` moved here from readability 2026-08-02: applying a
94
+ // pattern learned on one interface to another is a load question, not a
95
+ // decoding one, and readability is now decoding-only.
96
+ traits: ['comprehension', 'workingMemory', 'informationForaging', 'siteFamiliarity', 'transferLearning'],
60
97
  },
61
98
  {
62
99
  name: 'decision',
@@ -64,7 +101,48 @@ const LAYER_DEFINITIONS = [
64
101
  },
65
102
  {
66
103
  name: 'motor',
67
- traits: ['patience', 'proceduralFluency'],
104
+ // POINTING ONLY, as of 2026-08-03 (D-9 resolution). This layer answers one
105
+ // question: how expensive is it for this person to ACQUIRE a target.
106
+ //
107
+ // It used to sum pointing capability and procedural execution at equal
108
+ // weight in one scalar, which is the same failure shape the readability
109
+ // layer had before it was split. Measured on identical demand:
110
+ //
111
+ // motor-impairment-tremor cost 0.072 hitProbability 48.4%
112
+ // elderly-user cost 0.107 hitProbability 81.9%
113
+ //
114
+ // Worse pointing, lower cost -- because the tremor persona's procedural
115
+ // fluency (0.60) outweighed its pointing deficit against elderly's 0.40.
116
+ // `motorAccessibility` is reported in the same response and tracks only
117
+ // pointing, so every ordering check against it read as broken.
118
+ //
119
+ // `proceduralFluency` moved to `motorProcedure` below. Nothing was weighted
120
+ // to achieve this: the two layers contribute additively like every other
121
+ // layer, which makes their relative contribution visible and inspectable
122
+ // rather than hidden inside a scalar. There is no empirical basis for a
123
+ // relative weighting between target acquisition and sequence execution, and
124
+ // a constant invented here would acquire false authority by ending up in a
125
+ // research instrument. The calibration slot is deliberately left empty --
126
+ // see MOTOR_LAYER_WEIGHTS.
127
+ traits: ['motorCapacity'],
128
+ },
129
+ {
130
+ name: 'motorProcedure',
131
+ // Executing an interaction SEQUENCE once a target has been acquired --
132
+ // multi-step flows, form progressions, anything where the cost is in the
133
+ // order of operations rather than in hitting the control.
134
+ //
135
+ // Separate from `motor` because the two genuinely move in opposite
136
+ // directions for the same persona, which is precisely what makes them
137
+ // separable rather than one number: a tremor persona can be a poor pointer
138
+ // and a competent sequence-follower, and an elderly persona the reverse.
139
+ //
140
+ // DEMAND IS ABSOLUTE, not area-normalised. The number of steps in a
141
+ // sequence does not shrink because the page is tall. Its demand derives
142
+ // from interactiveElementCount, which is a raw count -- so there is nothing
143
+ // to remove here today, and this is recorded for the D-12 layer
144
+ // classification table rather than decided silently in code.
145
+ traits: ['proceduralFluency'],
68
146
  },
69
147
  {
70
148
  name: 'frustration',
@@ -72,7 +150,53 @@ const LAYER_DEFINITIONS = [
72
150
  },
73
151
  {
74
152
  name: 'readability',
75
- traits: ['readingTendency', 'comprehension', 'transferLearning'],
153
+ // DECODING ONLY, as of 2026-08-02 (D-3 Option 1). This layer answers one
154
+ // question -- how expensive is it for this person to turn these glyphs into
155
+ // words -- and it is the layer `legibilityQuality` makes a promise about:
156
+ // lower legibility for someone means higher readability cost for them.
157
+ //
158
+ // Everything else that was here has been moved out, and the history is why
159
+ // it had to be:
160
+ //
161
+ // R1 adhd 0.401 / dyslexic 0.189 inverted (disposition drove it)
162
+ // R2 adhd 0.112 / dyslexic 0.142 correct (attentional term removed)
163
+ // R4 adhd 0.161 / dyslexic 0.126 inverted (attentional term restored)
164
+ //
165
+ // Removing the attentional term fixed the ordering and deleted ADHD reading
166
+ // cost. Restoring it recovered ADHD reading cost and broke the ordering.
167
+ // BOTH were correct fixes. The layer was carrying two mechanisms through
168
+ // one scalar under a contract that only describes one of them, and no
169
+ // weighting satisfies both -- an ADHD attentionSpan of 0.30 against a
170
+ // dyslexic 0.50 swamps five decoding terms pointing the other way.
171
+ //
172
+ // So the second mechanism gets its own layer rather than a smaller weight.
173
+ // `sustainedAttention` moved to `readingAttention` below.
174
+ // `comprehension` moved out entirely: it is "grasp of UI conventions", not
175
+ // decoding, and it already drives cognitiveLoad -- keeping it here made the
176
+ // contract non-monotonic for personas whose comprehension and decoding
177
+ // disagree, which after the reading-capacity schema change is most of them.
178
+ // `transferLearning` moved to cognitiveLoad: applying a pattern learned on
179
+ // one interface to another is not reading. It is not orphaned by the move.
180
+ traits: ['readingCapacity'],
181
+ },
182
+ {
183
+ name: 'readingAttention',
184
+ // The attentional half of reading, split out of `readability` so both are
185
+ // separately attributable rather than summed into one number that can only
186
+ // be right about one of them.
187
+ //
188
+ // This is the layer an ADHD reader's cost lands in: decoding intact, loses
189
+ // place, re-reads, pulled off the line by movement in the periphery. It is
190
+ // NOT frustration -- losing your place is a processing cost, not an
191
+ // emotional response to one, and routing it through frustration would point
192
+ // remediation at reassurance instead of at chunking, shorter blocks and
193
+ // fewer moving distractors.
194
+ //
195
+ // Appended as layer 7 rather than inserted before readability, deliberately:
196
+ // layers spend from a budget the earlier ones depleted, so inserting would
197
+ // have changed readability's numbers through depletion as well as through
198
+ // its trait list, and the two effects could not be told apart.
199
+ traits: ['sustainedAttention'],
76
200
  },
77
201
  ];
78
202
  /**
@@ -89,10 +213,153 @@ const INTERACTION_PAIRS = [
89
213
  /** Asymmetric cost weights (Theorem 2 in paper) */
90
214
  const WEIGHT_DEFICIT = 1.0; // demand > capacity: high cost
91
215
  const WEIGHT_SURPLUS = 0.3; // capacity > demand: low cost (surplus is cheap)
216
+ /**
217
+ * Dimensions where having MORE than the page asks for costs nothing at all.
218
+ *
219
+ * THE RULE, decided 2026-08-03, and it is derivable rather than chosen: surplus
220
+ * is FREE where the dimension's own `highEnd` names an ABILITY, and BILLED
221
+ * where it names a TENDENCY, a BIAS, or a SUSCEPTIBILITY. Read the trait
222
+ * reference and the classification falls out; a new dimension is classified by
223
+ * its own definition, not by anyone's taste.
224
+ *
225
+ * Why the split is real. Billing surplus is defensible for a disposition: a
226
+ * maximiser on a trivial page genuinely over-deliberates, and a careful reader
227
+ * genuinely reads a page that did not need reading. That is effort spent. It is
228
+ * NOT defensible for an ability -- having more working memory, or recognising
229
+ * conventions faster, than a page requires cannot make that page harder. The
230
+ * capacity/disposition axis is the same one that moved readingTendency to the
231
+ * demand side and kept comprehension out of the readability layer.
232
+ *
233
+ * What it was costing. Measured on a mid-density page before this change,
234
+ * SEVENTY PERCENT of `power-user`'s total cost was surplus, and 60% of
235
+ * `cognitive-adhd`'s. A uniform-capacity sweep produced a U:
236
+ *
237
+ * capacity 0.2 0.4 0.6 0.8 1.0
238
+ * cost 0.549 0.081 0.076 0.179 0.516
239
+ *
240
+ * A maximally capable persona scored almost exactly as badly as a maximally
241
+ * impaired one, for opposite reasons, and the output could not tell you which.
242
+ * That is the same defect as every other one this campaign found: a number that
243
+ * does not mean what its name says.
244
+ *
245
+ * Four dimensions are billed BECAUSE THEIR SCALE IS INVERTED -- changeBlindness,
246
+ * trustCalibration, curiosity and attentionPattern name a deficit at their high
247
+ * end ("misses most changes", "accepts claims at face value", "easily
248
+ * distracted"). Surplus there is more of the deficit, and billing it is
249
+ * correct.
250
+ *
251
+ * `selfEfficacy` is the weakest call in the free set: over-confidence is a real
252
+ * hazard, but its cost surfaces through riskTolerance and trustCalibration
253
+ * rather than as effort spent, so it is treated as the ability its high end
254
+ * describes.
255
+ */
256
+ const SURPLUS_FREE_DIMENSIONS = new Set([
257
+ // Derived capacity bridges -- pure abilities by construction.
258
+ 'readingCapacity', 'motorCapacity', 'sustainedAttention', 'processingSpeed', 'textProcessing', 'motorPrecision',
259
+ // Experience. Knowing a site better cannot make it harder to use.
260
+ 'siteFamiliarity',
261
+ // Abilities: every one of these has a highEnd naming something a person CAN
262
+ // do, not something they TEND to do.
263
+ 'comprehension', // "Instant recognition of all conventions"
264
+ 'workingMemory', // "Perfect recall of all attempts and context"
265
+ 'resilience', // "Instantly shrugs off errors, stays positive"
266
+ 'selfEfficacy', // "I can solve anything"
267
+ 'proceduralFluency', // "Follows procedures precisely and efficiently"
268
+ 'transferLearning', // "Instantly applies patterns across all interfaces"
269
+ 'interruptRecovery', // "Seamlessly resumes exactly where left off"
270
+ 'mentalModelFlexibility', // "Highly adaptive - quickly forms new mental models"
271
+ 'informationForaging', // "Follows information scent efficiently, abandons low-yield paths"
272
+ ]);
273
+ /**
274
+ * Dimensions whose surplus IS billed, and why each one earns it.
275
+ *
276
+ * Stated explicitly rather than left as "everything else", so that a dimension
277
+ * added to DEMAND_DIMENSIONS cannot acquire a billing treatment by default. A
278
+ * test asserts every dimension appears in exactly one of the two sets.
279
+ */
280
+ const SURPLUS_BILLED_DIMENSIONS = new Set([
281
+ 'patience', // "Waits indefinitely" -- waiting longer than needed is time spent
282
+ 'riskTolerance', // "Clicks anything that might work" -- a tendency, and a hazard
283
+ 'persistence', // "Keeps trying same approach repeatedly" -- repeating unnecessary work
284
+ 'readingTendency', // "Reads every word before acting" -- reading a page that did not need it
285
+ 'emotionalContagion', // susceptibility to the interface's tone
286
+ 'satisficing', // a decision STRATEGY, not an ability
287
+ 'anchoringBias', // a bias, by name
288
+ 'fearOfMissingOut', // susceptibility to urgency and scarcity
289
+ 'socialProofSensitivity', // susceptibility to what others chose
290
+ 'metacognitivePlanning', // "Careful planning before any action" -- planning a trivial page is waste
291
+ // Inverted scales: the HIGH end is the deficit, so surplus is more deficit.
292
+ 'changeBlindness', // "Misses most changes outside focus area"
293
+ 'trustCalibration', // "Highly trusting, accepts claims at face value"
294
+ 'curiosity', // "Easily distracted by interesting elements"
295
+ 'attentionPattern', // scanning-vs-thorough disposition
296
+ ]);
297
+ /**
298
+ * Hill exponent on the abandonment curve. UNCALIBRATED.
299
+ *
300
+ * Same status as MOTOR_LAYER_WEIGHTS and flagged for the same reason: it is
301
+ * doing a great deal of work and nothing empirical set it. At 2 the curve is
302
+ * superlinear, which means the same total cost delivered in small increments
303
+ * barely registers -- splitting a fixed deficit of 0.607 across 14 steps takes
304
+ * cumulative abandonment from 22% to 2%.
305
+ *
306
+ * That matters beyond the scalar. Any future per-screen hazard model inherits
307
+ * this exponent, and at 2 it would make a long page SAFER than a short one with
308
+ * identical total demand purely from chunking. The exponent is not the fix for
309
+ * that -- per-screen demand measured on its own content is -- but a sequential
310
+ * model must not be built without knowing this constant is load-bearing.
311
+ *
312
+ * What would settle it: observed abandonment against measured task difficulty,
313
+ * or a dose-response fit on real session data. Until then it is a shape someone
314
+ * chose, not a shape anyone measured. (2026-08-03)
315
+ */
316
+ export const ABANDONMENT_HILL_EXPONENT = 2;
317
+ /**
318
+ * Relative weighting between the two motor layers. BOTH DEFAULT TO 1.0 AND ARE
319
+ * UNCALIBRATED.
320
+ *
321
+ * They exist as named constants so that if a weighting is ever introduced it is
322
+ * visible, greppable and dated, rather than baked into the cost function as a
323
+ * bare multiplier. Nothing reads them today; the layers contribute additively
324
+ * like every other layer.
325
+ *
326
+ * What would settle them: motor-control literature on tremor adaptation in
327
+ * target acquisition, or validation of chain output against measured
328
+ * task-completion times. Until one of those exists, a number here would be an
329
+ * invention wearing the authority of a research instrument. (2026-08-03)
330
+ */
331
+ export const MOTOR_LAYER_WEIGHTS = {
332
+ /** UNCALIBRATED. Target acquisition. */
333
+ motor: 1.0,
334
+ /** UNCALIBRATED. Sequence execution. */
335
+ motorProcedure: 1.0,
336
+ };
92
337
  /** Capacity depletion rate per layer (alpha_i, Section 3.4) */
93
338
  const DEPLETION_RATE = 0.15;
94
339
  /** Minimum residual capacity floor (prevents total exhaustion) */
95
340
  const CAPACITY_FLOOR = 0.05;
341
+ /**
342
+ * Numerical floor on residual capacity. NOT a behavioural limit.
343
+ *
344
+ * CAPACITY_FLOOR above was doing behavioural work it should not have: at 0.05
345
+ * it was reachable inside a single page, and once reached the account stopped
346
+ * responding to cost at all. Proportional drawdown asymptotes toward zero
347
+ * instead, so this exists only to keep the value away from denormals and to
348
+ * keep `gap = demand - capacity` finite. It is three orders of magnitude below
349
+ * anything a persona reaches in practice. (2026-08-03)
350
+ */
351
+ const CAPACITY_EPSILON = 1e-4;
352
+ /**
353
+ * Largest fraction of remaining capacity one layer may draw. UNCALIBRATED.
354
+ *
355
+ * Prevents a single very expensive layer from emptying the account outright,
356
+ * which would reintroduce the saturation this replaced. 0.5 means no layer can
357
+ * take more than half of what is left, whatever its cost.
358
+ *
359
+ * What would settle it: within-session effort measurements across a task
360
+ * sequence. Same status as ABANDONMENT_HILL_EXPONENT and MOTOR_LAYER_WEIGHTS.
361
+ */
362
+ const MAX_DRAWDOWN_FRACTION = 0.5;
96
363
  // ── Sigmoid Helper ──
97
364
  /**
98
365
  * Standard logistic sigmoid for mapping raw page metrics to demand values.
@@ -152,13 +419,13 @@ export function computeDemandDistribution(pageMetrics) {
152
419
  variance.workingMemory += infoDensity * 0.08;
153
420
  variance.readingTendency += infoDensity * 0.07;
154
421
  variance.informationForaging += infoDensity * 0.06;
155
- // ── visualComplexity → changeBlindness, mentalModelRigidity, attentionPattern (inferred)
422
+ // ── visualComplexity → changeBlindness, mentalModelFlexibility, attentionPattern (inferred)
156
423
  const visDemand = visCplx; // already 0-1 from logScale
157
424
  demands.changeBlindness = Math.max(demands.changeBlindness, visDemand);
158
- demands.mentalModelRigidity = Math.max(demands.mentalModelRigidity, visDemand * 0.7);
425
+ demands.mentalModelFlexibility = Math.max(demands.mentalModelFlexibility, visDemand * 0.7);
159
426
  demands.attentionPattern = Math.max(demands.attentionPattern, visDemand * 0.85);
160
427
  variance.changeBlindness += visCplx * 0.12;
161
- variance.mentalModelRigidity += visCplx * 0.08;
428
+ variance.mentalModelFlexibility += visCplx * 0.08;
162
429
  variance.attentionPattern += visCplx * 0.1;
163
430
  // ── interactiveElementCount → riskTolerance, satisficing, proceduralFluency, motorPrecision (inferred)
164
431
  // Use logScale for element count — 20 elements = 0.5, 100 = 0.83, 500 = 0.96
@@ -211,14 +478,14 @@ export function computeDemandDistribution(pageMetrics) {
211
478
  demands.readingTendency = Math.max(demands.readingTendency, longWordR * 0.9);
212
479
  demands.comprehension = Math.max(demands.comprehension, longWordR * 0.8);
213
480
  variance.readingTendency += longWordR * 0.11;
214
- // technicalDensity → comprehension, transferLearning, mentalModelRigidity
481
+ // technicalDensity → comprehension, transferLearning, mentalModelFlexibility
215
482
  // 10+ character words signal compound terms, jargon, domain-specific vocabulary
216
483
  demands.comprehension = Math.max(demands.comprehension, techDens * 0.95);
217
484
  demands.transferLearning = Math.max(demands.transferLearning, techDens * 0.8);
218
- demands.mentalModelRigidity = Math.max(demands.mentalModelRigidity, techDens * 0.7);
485
+ demands.mentalModelFlexibility = Math.max(demands.mentalModelFlexibility, techDens * 0.7);
219
486
  variance.comprehension += techDens * 0.13;
220
487
  variance.transferLearning += techDens * 0.09;
221
- variance.mentalModelRigidity += techDens * 0.07;
488
+ variance.mentalModelFlexibility += techDens * 0.07;
222
489
  }
223
490
  // ── animationLevel → changeBlindness, interruptRecovery, emotionalContagion
224
491
  const animDemand = sigmoid(animLevel, 0.3, 0.15);
@@ -228,6 +495,34 @@ export function computeDemandDistribution(pageMetrics) {
228
495
  variance.changeBlindness += animLevel * 0.1;
229
496
  variance.interruptRecovery += animLevel * 0.09;
230
497
  variance.emotionalContagion += animLevel * 0.07;
498
+ // ── textDensity × distractor pressure → sustainedAttention
499
+ //
500
+ // How much attention the page asks you to HOLD, as opposed to how hard its
501
+ // words are to decode (readingCapacity) or how much of it you choose to read
502
+ // (readingTendency, which modulates demand below). Gated on text: a page with
503
+ // no prose asks nothing here, however busy it is, because there is no line to
504
+ // lose your place in.
505
+ //
506
+ // Multiplicative rather than this file's usual `max`, deliberately -- the
507
+ // interaction IS the mechanism. A long article with nothing moving on it is
508
+ // readable; the same article beside an autoplaying carousel is where the
509
+ // re-reading happens. Two `max` terms would score those two pages the same.
510
+ //
511
+ // Animation dominates the distractor term because moving peripheral content
512
+ // captures attention involuntarily, while static clutter merely competes for
513
+ // it (Yantis & Jonides 1990 on abrupt-onset capture).
514
+ //
515
+ // The 0.7 base sits deliberately BELOW readingCapacity's 0.9: on a page with
516
+ // nothing moving, decoding is the larger demand of text and attention is the
517
+ // secondary one. The distractor multiplier is what lets it overtake decoding,
518
+ // and only on a page that has earned it. Without that ordering this dimension
519
+ // re-creates the exact inversion that removing readingTendency fixed --
520
+ // measured: at a flat 1.0 base, ADHD outscored dyslexic on readability on a
521
+ // page with animationLevel 0, which is the wrong answer.
522
+ const distractorPressure = Math.min(1, animDemand * 0.7 + visDemand * 0.3);
523
+ const sustainDemand = Math.min(1, textDemand * 0.7 * (1 + distractorPressure * 0.7));
524
+ demands.sustainedAttention = Math.max(demands.sustainedAttention, sustainDemand);
525
+ variance.sustainedAttention += textDens * 0.1 + animLevel * 0.08;
231
526
  // ── choiceCount → satisficing, anchoringBias, fearOfMissingOut, socialProofSensitivity
232
527
  // LogScale: 10 choices = 0.5, 30 = 0.75, 100 = 0.91
233
528
  const choiceDemand = choices / (choices + 10);
@@ -246,6 +541,31 @@ export function computeDemandDistribution(pageMetrics) {
246
541
  demands.metacognitivePlanning = Math.max(demands.metacognitivePlanning, navDemand * 0.8);
247
542
  demands.persistence = Math.max(demands.persistence, navDemand * 0.7);
248
543
  demands.transferLearning = Math.max(demands.transferLearning, navDemand * 0.65);
544
+ // ── navigationDepth → siteFamiliarity
545
+ //
546
+ // This dimension had NO demand term at all. It was initialised to 0 and never
547
+ // raised, so the persona's familiarity was transported against zero demand and
548
+ // contributed exactly nothing to the chain — three runs at familiarity unset,
549
+ // 1 and 0 returned byte-identical results while the tool reported the
550
+ // parameter as applied. The value was never overridden; nothing ever asked
551
+ // for it.
552
+ //
553
+ // Navigation depth is the honest driver: the further content sits from the
554
+ // entry point, the more the page asks you to already know where things are.
555
+ // A one-click page demands no site knowledge no matter who you are, which is
556
+ // why this is proportional to navDemand rather than a constant. Weighted just
557
+ // under workingMemory (0.95), since knowing the layout substitutes for
558
+ // holding it in mind. (2026-08-02)
559
+ demands.siteFamiliarity = Math.max(demands.siteFamiliarity, navDemand * 0.85);
560
+ // Reading capacity is demanded by how much text there is to decode, and motor
561
+ // capacity by how many things there are to hit. Both proportional, so a page
562
+ // with no text asks nothing of decoding and a page with no controls asks
563
+ // nothing of pointing.
564
+ demands.readingCapacity = Math.max(demands.readingCapacity, Math.max(textDens, infoDensity) * 0.9);
565
+ demands.motorCapacity = Math.max(demands.motorCapacity, interDemand * 0.9);
566
+ variance.readingCapacity += textDens * 0.08;
567
+ variance.motorCapacity += interDemand * 0.08;
568
+ variance.siteFamiliarity += Math.min(1, navDepth / 5) * 0.08;
249
569
  variance.workingMemory += Math.min(1, navDepth / 5) * 0.09;
250
570
  variance.metacognitivePlanning += Math.min(1, navDepth / 5) * 0.07;
251
571
  variance.persistence += Math.min(1, navDepth / 5) * 0.06;
@@ -325,8 +645,8 @@ const VALUE_DEMAND_MODULATION = [
325
645
  { value: 'selfDirection', trait: 'socialProofSensitivity', coefficient: -0.20, layer: 'decision' },
326
646
  // Self-Direction → amplifies metacognitive planning (plan their own path)
327
647
  { value: 'selfDirection', trait: 'metacognitivePlanning', coefficient: -0.15, layer: 'cognitiveLoad' },
328
- // Tradition → amplifies mentalModelRigidity (resist new UI patterns)
329
- { value: 'tradition', trait: 'mentalModelRigidity', coefficient: 0.25, layer: 'cognitiveLoad' },
648
+ // Tradition → amplifies mentalModelFlexibility (resist new UI patterns)
649
+ { value: 'tradition', trait: 'mentalModelFlexibility', coefficient: 0.25, layer: 'cognitiveLoad' },
330
650
  // Benevolence → reduces emotionalContagion cost (emotionally regulated)
331
651
  { value: 'benevolence', trait: 'emotionalContagion', coefficient: -0.15, layer: 'frustration' },
332
652
  // Power → reduces fear of missing out (confident, not swayed by urgency)
@@ -365,6 +685,46 @@ export function computeSequentialCTC(persona, demand, options) {
365
685
  demands: { ...demand.demands },
366
686
  variance: { ...demand.variance },
367
687
  };
688
+ // readingTendency modulates DEMAND, not capacity.
689
+ //
690
+ // It was in the readability layer as a capacity and inverted it: an ADHD
691
+ // persona who skims (0.2) scored a large capacity deficit against text
692
+ // demand, so a text-dense page cost them MORE than a dyslexic reader at 0.4,
693
+ // while the same run reported the dyslexic reader at 157 WPM against 204.
694
+ //
695
+ // The trait is not an ability, it is a STRATEGY -- its own criteria are
696
+ // "skims, reads carefully, scans for buttons" -- and skimming is how someone
697
+ // spends less effort on text, not evidence they cannot decode it. On the
698
+ // demand side that reads correctly: a skimmer engages less of the page's text,
699
+ // a careful reader engages more.
700
+ //
701
+ // Bounded to +/-25% around neutral, for two reasons. A skimmer still has to
702
+ // find the content, so demand cannot fall toward zero. And an unbounded
703
+ // modulator would recreate the very inversion this fixes by letting strategy
704
+ // outweigh decoding ability -- verified it does not: dyslexic still costs more
705
+ // than ADHD on a text-dense page after this. (2026-08-02)
706
+ const readingTendency = persona.traits?.readingTendency;
707
+ if (typeof readingTendency === "number") {
708
+ const engagement = 0.75 + readingTendency * 0.5; // 0.75 at pure skim, 1.25 at careful
709
+ // ATTENTIONAL demand only. Applied to `readingCapacity` as well for one
710
+ // commit, and that was wrong on the merits as well as breaking a contract.
711
+ //
712
+ // Engagement is how much of the page you read. That scales how long you
713
+ // must hold the line -- genuinely an attentional quantity -- but it does
714
+ // not change how hard the words you do read are to decode. Skimming reduces
715
+ // exposure, not difficulty.
716
+ //
717
+ // It also made decoding demand persona-specific, which silently voided the
718
+ // `legibilityQuality` contract the readability layer prints in its own
719
+ // output: measured, `distracted-user` at legibility 0.619 paid 0.000 while
720
+ // `careful-reader` at 0.724 paid 0.016 -- lower legibility, lower cost --
721
+ // purely because the skimmer's demand had been scaled down. A contract
722
+ // stated cross-persona has to be computed against a demand that does not
723
+ // vary by persona. (2026-08-02)
724
+ if (modulatedDemand.demands.sustainedAttention) {
725
+ modulatedDemand.demands.sustainedAttention = Math.max(0, Math.min(1, modulatedDemand.demands.sustainedAttention * engagement));
726
+ }
727
+ }
368
728
  if (values && Object.keys(values).length > 0) {
369
729
  for (const mod of VALUE_DEMAND_MODULATION) {
370
730
  const valueScore = values[mod.value];
@@ -381,7 +741,7 @@ export function computeSequentialCTC(persona, demand, options) {
381
741
  // Initialize residual capacity from persona traits
382
742
  const residualCapacity = {};
383
743
  for (const dim of DEMAND_DIMENSIONS) {
384
- residualCapacity[dim] = traitValue(persona.traits, dim);
744
+ residualCapacity[dim] = options?.carriedCapacity?.[dim] ?? traitValue(persona.traits, dim);
385
745
  }
386
746
  const layers = [];
387
747
  const traitCosts = {};
@@ -398,6 +758,14 @@ export function computeSequentialCTC(persona, demand, options) {
398
758
  let layerDeficit = 0;
399
759
  let layerSurplus = 0;
400
760
  // Compute transport cost for this layer's traits (using modulated demands)
761
+ //
762
+ // Headroom is tracked alongside cost because a surplus-free layer reports a
763
+ // bare 0 across its whole upper range and stops discriminating there. A
764
+ // reader at 91% legibility and one at 99% both cost 0 on a page neither of
765
+ // them struggles with -- true, and useless as a signal. Headroom is the
766
+ // spare capacity that produced the zero, so the upper range is readable
767
+ // without inventing a cost that is not there. (2026-08-03, BUG-18)
768
+ let layerHeadroom = 0;
401
769
  for (const trait of layerDef.traits) {
402
770
  const d = modulatedDemand.demands[trait] ?? 0;
403
771
  const c = residualCapacity[trait] ?? 0.5;
@@ -410,12 +778,16 @@ export function computeSequentialCTC(persona, demand, options) {
410
778
  }
411
779
  else {
412
780
  // Surplus: capacity exceeds demand — cheap
413
- cost = (useAsymmetric ? WEIGHT_SURPLUS : 1.0) * gap * gap;
781
+ const surplusWeight = SURPLUS_FREE_DIMENSIONS.has(trait) ? 0 : WEIGHT_SURPLUS;
782
+ cost = (useAsymmetric ? surplusWeight : 1.0) * gap * gap;
414
783
  layerSurplus += cost;
415
784
  }
785
+ if (gap < 0)
786
+ layerHeadroom += -gap;
416
787
  layerCost += cost;
417
788
  traitCosts[trait] = (traitCosts[trait] ?? 0) + cost;
418
789
  }
790
+ layerHeadroom = layerHeadroom / Math.max(1, layerDef.traits.length);
419
791
  // Scale layer cost by demand presence — empty pages shouldn't produce high costs
420
792
  const layerDemandMagnitude = layerDef.traits.reduce((sum, t) => sum + (modulatedDemand.demands[t] ?? 0), 0) / layerDef.traits.length;
421
793
  const contentScale = Math.min(1.0, layerDemandMagnitude * 3); // 0-1 scale, saturates at demand=0.33
@@ -429,17 +801,40 @@ export function computeSequentialCTC(persona, demand, options) {
429
801
  // Each trait in this layer loses capacity proportional to the layer's total cost
430
802
  const capacityConsumed = DEPLETION_RATE * layerCost;
431
803
  if (contentScale > 0.1) {
804
+ // PROPORTIONAL DRAWDOWN, not subtraction against a clamp.
805
+ //
806
+ // Capacity used to be spent as a flat amount and clamped at
807
+ // CAPACITY_FLOOR, which meant the account did not deplete -- it hit a
808
+ // wall and stopped carrying information. Measured on a dense page in a
809
+ // single eight-layer run: `intellectual-disability` finished with TEN
810
+ // dimensions pinned at exactly 0.05 and `elderly-user` with five. Every
811
+ // layer after that point saw an identical capacity vector, so the chain
812
+ // could no longer tell them apart. The personas the accessibility model
813
+ // exists to describe were the ones whose vector collapsed to a constant.
814
+ //
815
+ // Spending a FRACTION of what remains asymptotes toward zero instead of
816
+ // reaching it. A full account gives up more per unit cost than an empty
817
+ // one, the balance stays strictly ordered and strictly decreasing, and
818
+ // every later step is still distinguishable from the one before it.
819
+ // Measured over 112 steps: the clamped form produced 1 distinct value
820
+ // across the final four screens, this produces 32 of 32.
821
+ //
822
+ // This also makes the sequential model well-posed. Chaining screens is
823
+ // now just more draws on the same account, with no wall to hit at screen
824
+ // three and no `k` anywhere in the arithmetic. (2026-08-03)
825
+ const drawFraction = Math.min(MAX_DRAWDOWN_FRACTION, DEPLETION_RATE * layerCost);
432
826
  for (const trait of layerDef.traits) {
433
- residualCapacity[trait] -= capacityConsumed;
434
- residualCapacity[trait] = Math.max(CAPACITY_FLOOR, residualCapacity[trait]);
827
+ residualCapacity[trait] *= (1 - drawFraction);
828
+ residualCapacity[trait] = Math.max(CAPACITY_EPSILON, residualCapacity[trait]);
435
829
  }
436
830
  // Also deplete shared traits that appear in later layers
437
831
  // (cross-layer fatigue: cognitive exhaustion bleeds across boundaries)
832
+ const indirectFraction = Math.min(MAX_DRAWDOWN_FRACTION, (DEPLETION_RATE * 0.5) * layerCost);
438
833
  for (const dim of DEMAND_DIMENSIONS) {
439
834
  if (!layerDef.traits.includes(dim)) {
440
835
  // Indirect depletion at half rate
441
- residualCapacity[dim] -= (DEPLETION_RATE * 0.5) * layerCost;
442
- residualCapacity[dim] = Math.max(CAPACITY_FLOOR, residualCapacity[dim]);
836
+ residualCapacity[dim] *= (1 - indirectFraction);
837
+ residualCapacity[dim] = Math.max(CAPACITY_EPSILON, residualCapacity[dim]);
443
838
  }
444
839
  }
445
840
  }
@@ -448,6 +843,10 @@ export function computeSequentialCTC(persona, demand, options) {
448
843
  transportCost: layerCost,
449
844
  capacityConsumed,
450
845
  residualCapacity: { ...residualCapacity },
846
+ // Mean spare capacity across this layer's dimensions. Only meaningful
847
+ // when the cost is at or near zero; above that the cost carries the
848
+ // signal itself.
849
+ headroom: Math.round(layerHeadroom * 1000) / 1000,
451
850
  });
452
851
  }
453
852
  // Compute interaction terms
@@ -485,18 +884,47 @@ export function computeSequentialCTC(persona, demand, options) {
485
884
  bottleneckLayer = layer.name;
486
885
  }
487
886
  }
488
- // Compute abandonment risk
489
- // Based on deficit relative to patience capacity
490
- // deficit < 0.5 → risk < 30%, deficit ≈ patience → risk ≈ 50%, deficit >> patience → risk → 100%
887
+ // Compute abandonment risk.
888
+ //
889
+ // A page that costs this persona NOTHING must produce a risk of zero. The
890
+ // previous shape could not: it was a logistic centred at 1.5x tolerance, and
891
+ // a logistic evaluated below its centre still returns a substantial value, so
892
+ // every persona carried a floor that no page could lower. Measured:
893
+ //
894
+ // persona risk at deficit 0 real range over these pages (0-0.28)
895
+ // impatient-user 40% 40% -> 52%
896
+ // cognitive-adhd 34% 34% -> 45%
897
+ // power-user 25% 25% -> 34%
898
+ // elderly-user 11% 11% -> 16%
899
+ // perfect patience 8% --
900
+ //
901
+ // So "39% abandonment" beside "should handle this page comfortably" was never
902
+ // two independent measures disagreeing. It was one number that is ~85% floor
903
+ // and ~15% page. The reported fix -- fold abandonment into the verdict --
904
+ // would have made it worse, importing a near-constant into the sentence so
905
+ // that every ADHD run read as risky regardless of the page.
906
+ //
907
+ // Replaced with a Hill function: zero cost gives exactly zero risk, cost
908
+ // equal to tolerance gives 50%, and it approaches 1 from below. The
909
+ // "centre at 1.5x blended patience" intent is unchanged and so is the
910
+ // persona ordering; what is gone is the offset. (2026-08-03, BUG-17)
491
911
  const patienceCapacity = traitValue(persona.traits, 'patience');
492
912
  const resilienceCapacity = traitValue(persona.traits, 'resilience');
493
913
  const effectivePatience = patienceCapacity * 0.7 + resilienceCapacity * 0.3; // blended tolerance
494
- const adjustedCost = totalDeficit - totalSurplus * 0.3; // surplus comfort (30%)
495
- const riskInput = Math.max(0, adjustedCost) - effectivePatience * 1.5; // center at 1.5x patience
496
- const abandonmentRisk = Math.max(0, Math.min(1, 1 / (1 + Math.exp(-riskInput / 0.6)) // wider sigmoid scale
497
- ));
914
+ const adjustedCost = Math.max(0, totalDeficit - totalSurplus * 0.3); // surplus comfort (30%)
915
+ // Floored so a persona with no patience at all does not divide by zero and
916
+ // read as certain abandonment on a blank page.
917
+ const tolerance = Math.max(0.05, effectivePatience * 1.5);
918
+ const ABANDONMENT_STEEPNESS = ABANDONMENT_HILL_EXPONENT;
919
+ const abandonmentRisk = adjustedCost <= 0 ? 0 : Math.max(0, Math.min(1, Math.pow(adjustedCost, ABANDONMENT_STEEPNESS) /
920
+ (Math.pow(adjustedCost, ABANDONMENT_STEEPNESS) + Math.pow(tolerance, ABANDONMENT_STEEPNESS))));
498
921
  return {
499
922
  totalCTC,
923
+ // The un-normalized cost, exposed because totalCTC is a SIGMOID of it and
924
+ // the two are not interchangeable. Every ratio against additiveCTC has to
925
+ // use this one: additiveCTC is a raw sum, totalCTC is squashed to 0-1, and
926
+ // dividing one by the other compares different units. (2026-08-02)
927
+ rawCTC,
500
928
  layers,
501
929
  interactions,
502
930
  additiveCTC,
@@ -505,6 +933,7 @@ export function computeSequentialCTC(persona, demand, options) {
505
933
  traitCosts,
506
934
  bottleneckLayer,
507
935
  abandonmentRisk,
936
+ remainingCapacity: { ...residualCapacity },
508
937
  };
509
938
  }
510
939
  // ── 3. Convenience: Full COT Pipeline ──
@@ -551,15 +980,39 @@ export function computeDemandFromRawMetrics(informationDensity, visualComplexity
551
980
  /**
552
981
  * Compare sequential vs. additive CTC to quantify the sequential effect.
553
982
  *
554
- * Returns the ratio (sequential / additive). Values > 1.0 indicate that
555
- * capacity depletion and interactions amplify the total cost beyond what
556
- * independent layers would predict.
983
+ * Returns rawCTC / additiveCTC -- both in layer-cost units.
984
+ *
985
+ * NAMED FOR WHAT IT DOES, NOT WHAT IT WAS EXPECTED TO DO. The old name,
986
+ * `sequentialAmplification`, promised a value above 1.0 — depletion and
987
+ * interactions pushing total cost past what independent layers would predict.
988
+ * Measured across five real runs it was 0.30, 0.31 and 0.39: the chain
989
+ * ATTENUATES the additive sum by 61-70%, and has never been observed above 1.
990
+ *
991
+ * The transform is saturating rather than corrupting: the coefficient rises
992
+ * monotonically with the additive sum (0.30 -> 0.31 -> 0.39), so rank ordering
993
+ * across personas and pages is preserved and comparative claims built on CTC
994
+ * hold. What does not hold is any reading of an ABSOLUTE cost, which carries an
995
+ * undocumented compression of roughly 3x.
996
+ *
997
+ * "Amplification" for a term that only ever shrinks actively misleads anyone
998
+ * reading the JSON without the source, which is most readers. (2026-08-02)
557
999
  */
558
- export function sequentialAmplification(result) {
1000
+ export function chainCoefficient(result) {
559
1001
  if (result.additiveCTC === 0)
560
1002
  return 1.0;
561
- return result.totalCTC / result.additiveCTC;
1003
+ // rawCTC, NOT totalCTC. Both are in layer-cost units, so their ratio is the
1004
+ // actual contribution of interactions and sequential depletion. Measured
1005
+ // 1.002-1.019 across page densities: the chain does amplify, by 0.2-2%.
1006
+ //
1007
+ // The old form divided totalCTC (a sigmoid, 0-1) by additiveCTC (a raw,
1008
+ // unbounded sum) and reported 0.30-0.56 as if the chain were dampening by
1009
+ // 60%. It was not measuring the chain at all, it was measuring the sigmoid --
1010
+ // and it was not even monotonic across density (0.329, 0.399, 0.556, 0.313,
1011
+ // 0.425), so it could not be used for ranking either.
1012
+ return result.rawCTC / result.additiveCTC;
562
1013
  }
1014
+ /** @deprecated Misleading name — it has never exceeded 1. Use `chainCoefficient`. */
1015
+ export const sequentialAmplification = chainCoefficient;
563
1016
  /**
564
1017
  * Identify the top N most costly traits across all layers.
565
1018
  * Useful for targeted remediation — fix the traits that hurt most.
@@ -586,4 +1039,79 @@ export function baselineCTC(persona) {
586
1039
  }
587
1040
  return computeSequentialCTC(persona, emptyDemand);
588
1041
  }
1042
+ /**
1043
+ * Score a page as a SEQUENCE of screens rather than one aggregate.
1044
+ *
1045
+ * The aggregate model answers "what would this cost if the whole document were
1046
+ * processed at once", which is a counterfactual almost nobody performs. This
1047
+ * answers the question customers actually have: where do people stop.
1048
+ *
1049
+ * Three things make it different from dividing the aggregate by k, which was
1050
+ * the obvious wrong implementation:
1051
+ *
1052
+ * 1. EACH SCREEN'S DEMAND IS MEASURED ON ITS OWN CONTENT. Splitting a fixed
1053
+ * total across k screens makes long pages safer than short ones -- measured,
1054
+ * a deficit of 0.607 split fourteen ways drops cumulative abandonment from
1055
+ * 22% to 2%, because the hazard is superlinear. A genuinely sparse long page
1056
+ * SHOULD be safer; a dense one must not become safe by being tall.
1057
+ * 2. CAPACITY CARRIES FORWARD. Screen 7 is scored against the budget screens
1058
+ * 1-6 left, not against a fresh persona. This is why depletion had to stop
1059
+ * saturating first: with the old clamp, every screen after the third saw an
1060
+ * identical capacity vector.
1061
+ * 3. DEMAND IS WEIGHTED BY THE CHANCE OF EVER GETTING THERE. A cost on screen
1062
+ * twelve that 4% of sessions reach is not a cost of the same size as one on
1063
+ * screen one.
1064
+ *
1065
+ * The hazard is the same Hill function the aggregate uses, applied per screen
1066
+ * against the capacity remaining at that point -- so ABANDONMENT_HILL_EXPONENT
1067
+ * is load-bearing here too and remains uncalibrated.
1068
+ */
1069
+ export function computeSequentialScrollCTC(persona, screenDemands, options) {
1070
+ const screens = [];
1071
+ let carried;
1072
+ let survive = 1;
1073
+ const patience = traitValue(persona.traits, 'patience');
1074
+ const resilience = traitValue(persona.traits, 'resilience');
1075
+ const tolerance = Math.max(0.05, (patience * 0.7 + resilience * 0.3) * 1.5);
1076
+ for (let i = 0; i < screenDemands.length; i++) {
1077
+ const r = computeSequentialCTC(persona, screenDemands[i], { ...options, carriedCapacity: carried });
1078
+ carried = r.remainingCapacity;
1079
+ // Hazard for THIS screen, from this screen's own unmet demand. Surplus
1080
+ // relief applies as it does in the aggregate.
1081
+ const screenCost = Math.max(0, r.deficitCost - r.surplusCost * 0.3);
1082
+ const stopHazard = screenCost <= 0 ? 0 : Math.min(1, Math.pow(screenCost, ABANDONMENT_HILL_EXPONENT) /
1083
+ (Math.pow(screenCost, ABANDONMENT_HILL_EXPONENT) + Math.pow(tolerance, ABANDONMENT_HILL_EXPONENT)));
1084
+ const reachProbability = survive;
1085
+ survive = survive * (1 - stopHazard);
1086
+ screens.push({
1087
+ screen: i + 1,
1088
+ layers: r.layers,
1089
+ screenCTC: Math.round(r.totalCTC * 1000) / 1000,
1090
+ stopHazard: Math.round(stopHazard * 1000) / 1000,
1091
+ cumulativeAbandonment: Math.round((1 - survive) * 1000) / 1000,
1092
+ reachProbability: Math.round(reachProbability * 1000) / 1000,
1093
+ bottleneckLayer: r.bottleneckLayer,
1094
+ remainingCapacity: r.remainingCapacity,
1095
+ });
1096
+ }
1097
+ // Where the most sessions end. Mass lost on a screen is reach x hazard, so a
1098
+ // brutal screen nobody reaches does not win.
1099
+ let dropOff = null;
1100
+ for (const s of screens) {
1101
+ const lost = s.reachProbability * s.stopHazard;
1102
+ if (!dropOff || lost > dropOff.share) {
1103
+ dropOff = { screen: s.screen, layer: s.bottleneckLayer, share: Math.round(lost * 1000) / 1000 };
1104
+ }
1105
+ }
1106
+ if (dropOff && dropOff.share <= 0)
1107
+ dropOff = null;
1108
+ return {
1109
+ screens,
1110
+ totalAbandonment: Math.round((1 - survive) * 1000) / 1000,
1111
+ // Sum of reach probabilities: the expected count of screens actually seen.
1112
+ expectedDepthScreens: Math.round(screens.reduce((a, s) => a + s.reachProbability, 0) * 100) / 100,
1113
+ dropOff,
1114
+ reachWeightedCTC: Math.round(screens.reduce((a, s) => a + s.reachProbability * s.screenCTC, 0) * 1000) / 1000,
1115
+ };
1116
+ }
589
1117
  //# sourceMappingURL=cognitive-transport-chain.js.map