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