cbrowser 20.2.2 → 21.0.1

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 (311) hide show
  1. package/README.md +2 -2
  2. package/dist/analysis/accessibility-empathy.d.ts +29 -1
  3. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  4. package/dist/analysis/accessibility-empathy.js +69 -35
  5. package/dist/analysis/accessibility-empathy.js.map +1 -1
  6. package/dist/browser.d.ts.map +1 -1
  7. package/dist/browser.js +11 -7
  8. package/dist/browser.js.map +1 -1
  9. package/dist/cif-score.d.ts +56 -5
  10. package/dist/cif-score.d.ts.map +1 -1
  11. package/dist/cif-score.js +595 -346
  12. package/dist/cif-score.js.map +1 -1
  13. package/dist/cli.js +10 -8
  14. package/dist/cli.js.map +1 -1
  15. package/dist/cognitive/default-traits.d.ts +14 -0
  16. package/dist/cognitive/default-traits.d.ts.map +1 -0
  17. package/dist/cognitive/default-traits.js +36 -0
  18. package/dist/cognitive/default-traits.js.map +1 -0
  19. package/dist/cognitive/emotions.d.ts.map +1 -1
  20. package/dist/cognitive/emotions.js +104 -174
  21. package/dist/cognitive/emotions.js.map +1 -1
  22. package/dist/cognitive/index.d.ts +28 -0
  23. package/dist/cognitive/index.d.ts.map +1 -1
  24. package/dist/cognitive/index.js +353 -287
  25. package/dist/cognitive/index.js.map +1 -1
  26. package/dist/cognitive/temporal.d.ts +0 -24
  27. package/dist/cognitive/temporal.d.ts.map +1 -1
  28. package/dist/cognitive/temporal.js +45 -35
  29. package/dist/cognitive/temporal.js.map +1 -1
  30. package/dist/data/ctc-reference-v2-2026-10-24.d.ts +16 -0
  31. package/dist/data/ctc-reference-v2-2026-10-24.d.ts.map +1 -0
  32. package/dist/data/ctc-reference-v2-2026-10-24.js +1004 -0
  33. package/dist/data/ctc-reference-v2-2026-10-24.js.map +1 -0
  34. package/dist/ideal-profiles-gap.d.ts +1 -0
  35. package/dist/ideal-profiles-gap.d.ts.map +1 -1
  36. package/dist/ideal-profiles-gap.js +9 -1
  37. package/dist/ideal-profiles-gap.js.map +1 -1
  38. package/dist/ideal-profiles.d.ts +8 -0
  39. package/dist/ideal-profiles.d.ts.map +1 -1
  40. package/dist/ideal-profiles.js +28 -24
  41. package/dist/ideal-profiles.js.map +1 -1
  42. package/dist/mcp-server-remote.d.ts.map +1 -1
  43. package/dist/mcp-server-remote.js +7 -8
  44. package/dist/mcp-server-remote.js.map +1 -1
  45. package/dist/mcp-tools/account-personas.d.ts.map +1 -1
  46. package/dist/mcp-tools/account-personas.js +3 -1
  47. package/dist/mcp-tools/account-personas.js.map +1 -1
  48. package/dist/mcp-tools/base/answer-provenance.d.ts +28 -0
  49. package/dist/mcp-tools/base/answer-provenance.d.ts.map +1 -0
  50. package/dist/mcp-tools/base/answer-provenance.js +32 -0
  51. package/dist/mcp-tools/base/answer-provenance.js.map +1 -0
  52. package/dist/mcp-tools/base/audit-tools.d.ts +0 -3
  53. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  54. package/dist/mcp-tools/base/audit-tools.js +152 -84
  55. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  56. package/dist/mcp-tools/base/capture-tools.js +1 -1
  57. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  58. package/dist/mcp-tools/base/chain-values.d.ts +38 -0
  59. package/dist/mcp-tools/base/chain-values.d.ts.map +1 -0
  60. package/dist/mcp-tools/base/chain-values.js +85 -0
  61. package/dist/mcp-tools/base/chain-values.js.map +1 -0
  62. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
  63. package/dist/mcp-tools/base/cognitive-tools.js +126 -120
  64. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
  65. package/dist/mcp-tools/base/gif-tools.d.ts.map +1 -1
  66. package/dist/mcp-tools/base/gif-tools.js +23 -5
  67. package/dist/mcp-tools/base/gif-tools.js.map +1 -1
  68. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
  69. package/dist/mcp-tools/base/persona-comparison-tools.js +229 -172
  70. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
  71. package/dist/mcp-tools/base/site-familiarity.d.ts +31 -0
  72. package/dist/mcp-tools/base/site-familiarity.d.ts.map +1 -0
  73. package/dist/mcp-tools/base/site-familiarity.js +30 -0
  74. package/dist/mcp-tools/base/site-familiarity.js.map +1 -0
  75. package/dist/mcp-tools/base/site-knowledge-tools.js +1 -1
  76. package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
  77. package/dist/mcp-tools/base/story-narrative.d.ts +50 -0
  78. package/dist/mcp-tools/base/story-narrative.d.ts.map +1 -0
  79. package/dist/mcp-tools/base/story-narrative.js +88 -0
  80. package/dist/mcp-tools/base/story-narrative.js.map +1 -0
  81. package/dist/mcp-tools/base/values-tools.d.ts.map +1 -1
  82. package/dist/mcp-tools/base/values-tools.js +19 -5
  83. package/dist/mcp-tools/base/values-tools.js.map +1 -1
  84. package/dist/mcp-tools/base/visual-testing-tools.d.ts +5 -5
  85. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  86. package/dist/mcp-tools/base/visual-testing-tools.js +38 -11
  87. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  88. package/dist/mcp-tools/persona-creation-tools.js +6 -6
  89. package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
  90. package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -1
  91. package/dist/mcp-tools/persona-lifecycle-tools.js +5 -1
  92. package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -1
  93. package/dist/mcp-tools/tier-gate.js +1 -1
  94. package/dist/mcp-tools/tool-result-saver.d.ts +7 -0
  95. package/dist/mcp-tools/tool-result-saver.d.ts.map +1 -1
  96. package/dist/mcp-tools/tool-result-saver.js +11 -0
  97. package/dist/mcp-tools/tool-result-saver.js.map +1 -1
  98. package/dist/mcp-tools/trait-provenance.d.ts +109 -0
  99. package/dist/mcp-tools/trait-provenance.d.ts.map +1 -0
  100. package/dist/mcp-tools/trait-provenance.js +187 -0
  101. package/dist/mcp-tools/trait-provenance.js.map +1 -0
  102. package/dist/mcp-tools/ui-resources.d.ts +6 -7
  103. package/dist/mcp-tools/ui-resources.d.ts.map +1 -1
  104. package/dist/mcp-tools/ui-resources.js +162 -12
  105. package/dist/mcp-tools/ui-resources.js.map +1 -1
  106. package/dist/mcp-tools/widget-kit.d.ts +129 -3
  107. package/dist/mcp-tools/widget-kit.d.ts.map +1 -1
  108. package/dist/mcp-tools/widget-kit.js +507 -28
  109. package/dist/mcp-tools/widget-kit.js.map +1 -1
  110. package/dist/performance/metrics.d.ts.map +1 -1
  111. package/dist/performance/metrics.js +4 -1
  112. package/dist/performance/metrics.js.map +1 -1
  113. package/dist/persona-age.d.ts +59 -0
  114. package/dist/persona-age.d.ts.map +1 -0
  115. package/dist/persona-age.js +84 -0
  116. package/dist/persona-age.js.map +1 -0
  117. package/dist/persona-questionnaire.js +1 -1
  118. package/dist/persona-questionnaire.js.map +1 -1
  119. package/dist/personas.d.ts +3 -1
  120. package/dist/personas.d.ts.map +1 -1
  121. package/dist/personas.js +154 -119
  122. package/dist/personas.js.map +1 -1
  123. package/dist/recording/engine.d.ts.map +1 -1
  124. package/dist/recording/engine.js +5 -2
  125. package/dist/recording/engine.js.map +1 -1
  126. package/dist/recording/saliency-overlay.d.ts +8 -3
  127. package/dist/recording/saliency-overlay.d.ts.map +1 -1
  128. package/dist/recording/saliency-overlay.js +14 -5
  129. package/dist/recording/saliency-overlay.js.map +1 -1
  130. package/dist/recording/types.js +1 -1
  131. package/dist/testing/nl-test-suite.d.ts.map +1 -1
  132. package/dist/testing/nl-test-suite.js +2 -1
  133. package/dist/testing/nl-test-suite.js.map +1 -1
  134. package/dist/trait-reference.d.ts +19 -2
  135. package/dist/trait-reference.d.ts.map +1 -1
  136. package/dist/trait-reference.js +19 -2
  137. package/dist/trait-reference.js.map +1 -1
  138. package/dist/types.d.ts +32 -21
  139. package/dist/types.d.ts.map +1 -1
  140. package/dist/types.js +62 -52
  141. package/dist/types.js.map +1 -1
  142. package/dist/values/persona-completeness.js +1 -1
  143. package/dist/values/persona-completeness.js.map +1 -1
  144. package/dist/visual/ab-comparison.d.ts.map +1 -1
  145. package/dist/visual/ab-comparison.js +4 -1
  146. package/dist/visual/ab-comparison.js.map +1 -1
  147. package/dist/visual/attention-quality.d.ts +1 -1
  148. package/dist/visual/attention-quality.js +1 -1
  149. package/dist/visual/attention-transport.d.ts +74 -10
  150. package/dist/visual/attention-transport.d.ts.map +1 -1
  151. package/dist/visual/attention-transport.js +353 -193
  152. package/dist/visual/attention-transport.js.map +1 -1
  153. package/dist/visual/cognitive-models.d.ts +77 -55
  154. package/dist/visual/cognitive-models.d.ts.map +1 -1
  155. package/dist/visual/cognitive-models.js +231 -104
  156. package/dist/visual/cognitive-models.js.map +1 -1
  157. package/dist/visual/cognitive-transport-chain.d.ts +328 -30
  158. package/dist/visual/cognitive-transport-chain.d.ts.map +1 -1
  159. package/dist/visual/cognitive-transport-chain.js +576 -353
  160. package/dist/visual/cognitive-transport-chain.js.map +1 -1
  161. package/dist/visual/cognitive-transport.d.ts +446 -13
  162. package/dist/visual/cognitive-transport.d.ts.map +1 -1
  163. package/dist/visual/cognitive-transport.js +1085 -245
  164. package/dist/visual/cognitive-transport.js.map +1 -1
  165. package/dist/visual/cross-browser.d.ts.map +1 -1
  166. package/dist/visual/cross-browser.js +8 -6
  167. package/dist/visual/cross-browser.js.map +1 -1
  168. package/dist/visual/ctc-bands.d.ts +91 -0
  169. package/dist/visual/ctc-bands.d.ts.map +1 -0
  170. package/dist/visual/ctc-bands.js +117 -0
  171. package/dist/visual/ctc-bands.js.map +1 -0
  172. package/dist/visual/ctc-reference.d.ts +287 -0
  173. package/dist/visual/ctc-reference.d.ts.map +1 -0
  174. package/dist/visual/ctc-reference.js +376 -0
  175. package/dist/visual/ctc-reference.js.map +1 -0
  176. package/dist/visual/demand-laws.d.ts +145 -0
  177. package/dist/visual/demand-laws.d.ts.map +1 -0
  178. package/dist/visual/demand-laws.js +220 -0
  179. package/dist/visual/demand-laws.js.map +1 -0
  180. package/dist/visual/distance-metrics.d.ts +23 -1
  181. package/dist/visual/distance-metrics.d.ts.map +1 -1
  182. package/dist/visual/distance-metrics.js +49 -18
  183. package/dist/visual/distance-metrics.js.map +1 -1
  184. package/dist/visual/dro-bound.d.ts +220 -0
  185. package/dist/visual/dro-bound.d.ts.map +1 -0
  186. package/dist/visual/dro-bound.js +341 -0
  187. package/dist/visual/dro-bound.js.map +1 -0
  188. package/dist/visual/exact-w1.d.ts +60 -0
  189. package/dist/visual/exact-w1.d.ts.map +1 -0
  190. package/dist/visual/exact-w1.js +247 -0
  191. package/dist/visual/exact-w1.js.map +1 -0
  192. package/dist/visual/human-input.d.ts.map +1 -1
  193. package/dist/visual/human-input.js +45 -41
  194. package/dist/visual/human-input.js.map +1 -1
  195. package/dist/visual/index.d.ts +2 -0
  196. package/dist/visual/index.d.ts.map +1 -1
  197. package/dist/visual/index.js +2 -0
  198. package/dist/visual/index.js.map +1 -1
  199. package/dist/visual/law-times-report.d.ts +49 -0
  200. package/dist/visual/law-times-report.d.ts.map +1 -0
  201. package/dist/visual/law-times-report.js +38 -0
  202. package/dist/visual/law-times-report.js.map +1 -0
  203. package/dist/visual/model-provenance.d.ts +151 -0
  204. package/dist/visual/model-provenance.d.ts.map +1 -0
  205. package/dist/visual/model-provenance.js +335 -0
  206. package/dist/visual/model-provenance.js.map +1 -0
  207. package/dist/visual/normal-cdf.d.ts +14 -0
  208. package/dist/visual/normal-cdf.d.ts.map +1 -0
  209. package/dist/visual/normal-cdf.js +26 -0
  210. package/dist/visual/normal-cdf.js.map +1 -0
  211. package/dist/visual/perceptual-transport.d.ts +28 -4
  212. package/dist/visual/perceptual-transport.d.ts.map +1 -1
  213. package/dist/visual/perceptual-transport.js +66 -32
  214. package/dist/visual/perceptual-transport.js.map +1 -1
  215. package/dist/visual/provenance/age.d.ts +30 -0
  216. package/dist/visual/provenance/age.d.ts.map +1 -0
  217. package/dist/visual/provenance/age.js +28 -0
  218. package/dist/visual/provenance/age.js.map +1 -0
  219. package/dist/visual/provenance/attention.d.ts +55 -0
  220. package/dist/visual/provenance/attention.d.ts.map +1 -0
  221. package/dist/visual/provenance/attention.js +188 -0
  222. package/dist/visual/provenance/attention.js.map +1 -0
  223. package/dist/visual/provenance/chain.d.ts +41 -0
  224. package/dist/visual/provenance/chain.d.ts.map +1 -0
  225. package/dist/visual/provenance/chain.js +66 -0
  226. package/dist/visual/provenance/chain.js.map +1 -0
  227. package/dist/visual/provenance/cif.d.ts +42 -0
  228. package/dist/visual/provenance/cif.d.ts.map +1 -0
  229. package/dist/visual/provenance/cif.js +116 -0
  230. package/dist/visual/provenance/cif.js.map +1 -0
  231. package/dist/visual/provenance/ctc-reference.d.ts +36 -0
  232. package/dist/visual/provenance/ctc-reference.d.ts.map +1 -0
  233. package/dist/visual/provenance/ctc-reference.js +96 -0
  234. package/dist/visual/provenance/ctc-reference.js.map +1 -0
  235. package/dist/visual/provenance/distance.d.ts +10 -0
  236. package/dist/visual/provenance/distance.d.ts.map +1 -0
  237. package/dist/visual/provenance/distance.js +72 -0
  238. package/dist/visual/provenance/distance.js.map +1 -0
  239. package/dist/visual/provenance/dro.d.ts +10 -0
  240. package/dist/visual/provenance/dro.d.ts.map +1 -0
  241. package/dist/visual/provenance/dro.js +2 -0
  242. package/dist/visual/provenance/dro.js.map +1 -0
  243. package/dist/visual/provenance/emotion.d.ts +26 -0
  244. package/dist/visual/provenance/emotion.d.ts.map +1 -0
  245. package/dist/visual/provenance/emotion.js +99 -0
  246. package/dist/visual/provenance/emotion.js.map +1 -0
  247. package/dist/visual/provenance/human-input.d.ts +19 -0
  248. package/dist/visual/provenance/human-input.d.ts.map +1 -0
  249. package/dist/visual/provenance/human-input.js +84 -0
  250. package/dist/visual/provenance/human-input.js.map +1 -0
  251. package/dist/visual/provenance/journey-group.d.ts +15 -0
  252. package/dist/visual/provenance/journey-group.d.ts.map +1 -0
  253. package/dist/visual/provenance/journey-group.js +15 -0
  254. package/dist/visual/provenance/journey-group.js.map +1 -0
  255. package/dist/visual/provenance/journey-mcp.d.ts +24 -0
  256. package/dist/visual/provenance/journey-mcp.d.ts.map +1 -0
  257. package/dist/visual/provenance/journey-mcp.js +54 -0
  258. package/dist/visual/provenance/journey-mcp.js.map +1 -0
  259. package/dist/visual/provenance/journey.d.ts +34 -0
  260. package/dist/visual/provenance/journey.d.ts.map +1 -0
  261. package/dist/visual/provenance/journey.js +184 -0
  262. package/dist/visual/provenance/journey.js.map +1 -0
  263. package/dist/visual/provenance/laws.d.ts +38 -0
  264. package/dist/visual/provenance/laws.d.ts.map +1 -0
  265. package/dist/visual/provenance/laws.js +40 -0
  266. package/dist/visual/provenance/laws.js.map +1 -0
  267. package/dist/visual/provenance/page-metrics.d.ts +42 -0
  268. package/dist/visual/provenance/page-metrics.d.ts.map +1 -0
  269. package/dist/visual/provenance/page-metrics.js +79 -0
  270. package/dist/visual/provenance/page-metrics.js.map +1 -0
  271. package/dist/visual/provenance/perceptual.d.ts +63 -0
  272. package/dist/visual/provenance/perceptual.d.ts.map +1 -0
  273. package/dist/visual/provenance/perceptual.js +115 -0
  274. package/dist/visual/provenance/perceptual.js.map +1 -0
  275. package/dist/visual/provenance/persona-rates.d.ts +84 -0
  276. package/dist/visual/provenance/persona-rates.d.ts.map +1 -0
  277. package/dist/visual/provenance/persona-rates.js +108 -0
  278. package/dist/visual/provenance/persona-rates.js.map +1 -0
  279. package/dist/visual/provenance/personas.d.ts +122 -0
  280. package/dist/visual/provenance/personas.d.ts.map +1 -0
  281. package/dist/visual/provenance/personas.js +298 -0
  282. package/dist/visual/provenance/personas.js.map +1 -0
  283. package/dist/visual/provenance/profiles.d.ts +39 -0
  284. package/dist/visual/provenance/profiles.d.ts.map +1 -0
  285. package/dist/visual/provenance/profiles.js +37 -0
  286. package/dist/visual/provenance/profiles.js.map +1 -0
  287. package/dist/visual/provenance/temporal.d.ts +21 -0
  288. package/dist/visual/provenance/temporal.d.ts.map +1 -0
  289. package/dist/visual/provenance/temporal.js +56 -0
  290. package/dist/visual/provenance/temporal.js.map +1 -0
  291. package/dist/visual/provenance/visual-distance.d.ts +22 -0
  292. package/dist/visual/provenance/visual-distance.d.ts.map +1 -0
  293. package/dist/visual/provenance/visual-distance.js +30 -0
  294. package/dist/visual/provenance/visual-distance.js.map +1 -0
  295. package/dist/visual/regression.d.ts.map +1 -1
  296. package/dist/visual/regression.js +14 -9
  297. package/dist/visual/regression.js.map +1 -1
  298. package/dist/visual/responsive.d.ts.map +1 -1
  299. package/dist/visual/responsive.js +4 -1
  300. package/dist/visual/responsive.js.map +1 -1
  301. package/dist/visual/scope-prose.js +2 -2
  302. package/dist/visual/scope-prose.js.map +1 -1
  303. package/dist/visual/seeded-random.d.ts +16 -0
  304. package/dist/visual/seeded-random.d.ts.map +1 -0
  305. package/dist/visual/seeded-random.js +25 -0
  306. package/dist/visual/seeded-random.js.map +1 -0
  307. package/dist/visual/stored-accessibility-traits.d.ts +27 -0
  308. package/dist/visual/stored-accessibility-traits.d.ts.map +1 -0
  309. package/dist/visual/stored-accessibility-traits.js +35 -0
  310. package/dist/visual/stored-accessibility-traits.js.map +1 -0
  311. package/package.json +1 -1
@@ -5,7 +5,6 @@
5
5
  * @license MIT
6
6
  */
7
7
  import { z } from "zod";
8
- import { resolveValuesForPersona } from "../../values/persona-values.js";
9
8
  import { writeArtifact } from "../../artifact-store.js";
10
9
  import { buildContentWithPreviews } from "../inline-image.js";
11
10
  import { makeTempDir } from "../../temp-files.js";
@@ -14,8 +13,14 @@ import { refuseHostedUrl } from "../hosted-inputs.js";
14
13
  import { comparePersonas } from "../../analysis/index.js";
15
14
  import { isApiKeyConfigured } from "../../cognitive/index.js";
16
15
  import { getAnyPersona, resolvePersonaOrThrow, getCognitiveProfile, createCognitivePersona, isAgentPersonaObject, BUILTIN_PERSONAS, ACCESSIBILITY_PERSONAS, EMOTIONAL_PERSONAS, } from "../../personas.js";
17
- import { buildOTCognitiveProfile as buildOTProfile, cognitiveDistance, cognitiveBarycenter, cognitiveGeodesic, cognitiveDistanceMatrix, selectMaxCoveragePersonas, estimateCognitiveLoad, COGNITIVE_TRAITS, } from "../../visual/cognitive-transport.js";
18
- import { bridgeCapacityTraits } from "../../visual/cognitive-models.js";
16
+ import { buildOTCognitiveProfile as buildOTProfile, cognitiveDistance, cognitiveBarycenter, cognitiveGeodesic, cognitiveDistanceMatrix, selectMaxCoveragePersonas, estimateCognitiveLoad, COGNITIVE_TRAITS, OVERALL_LEVEL_KEY, } from "../../visual/cognitive-transport.js";
17
+ import { bridgeCapacityTraits, personaRateContext } from "../../visual/cognitive-models.js";
18
+ import { lawTimesReport } from "../../visual/law-times-report.js";
19
+ import { activeProvenance, collectPersonaReads, isolatedProvenanceAsync, personaRateProvenance, ATTENTION_PROVENANCE_TEXT } from "../../visual/model-provenance.js";
20
+ import { ctcBandsDescription, ctcRank } from "../../visual/ctc-bands.js";
21
+ import { resolveSiteFamiliarity } from "./site-familiarity.js";
22
+ import { resolveChainValues, valueModulationReport } from "./chain-values.js";
23
+ import { recordedAnswer } from "./answer-provenance.js";
19
24
  /**
20
25
  * Most base64 characters cognitive_effort spends on its inline motor-overlay
21
26
  * preview. The overlay is supporting evidence here, not the finding, and the
@@ -381,7 +386,7 @@ Begin with the first persona: ${personas[0]}
381
386
  // ── Cognitive Transport Tools (v18.27.0) ──
382
387
  server.registerTool("cognitive_distance", {
383
388
  title: "Cognitive Distance Between Personas",
384
- description: "Compute the Wasserstein cognitive distance between two personas. Returns W₁ distance (true cognitive distance), per-trait contributions, and Bures-Wasserstein W₂ distance. Based on optimal transport theory — transport cost = cognitive processing cost.",
389
+ description: "Compute the cognitive distance between two personas as a generalized (unbalanced) Wasserstein-1 distance over their trait vectors: it counts both the shape of the profile and its overall level, so a persona with every trait at 0.2 is far from one with every trait at 0.8. Moving trait mass between traits costs a hand-set ground metric (0.2 same domain, 0.5 related, 0.8 unrelated); creating or destroying it costs 0.4 per unit, also hand-set. Both are hypotheses, untested against human data. Returns w1 with per-trait contributions (plus overallLevel, the share due to a difference in level), the shape-only Bures-Wasserstein W2 and sliced distances, and each metric's percentile among built-in persona pairs.",
385
390
  inputSchema: {
386
391
  personaA: z.string().describe("First persona name"),
387
392
  personaB: z.string().describe("Second persona name"),
@@ -404,8 +409,11 @@ Begin with the first persona: ${personas[0]}
404
409
  const otA = buildOTProfile(personaA, profileA.traits || {});
405
410
  const otB = buildOTProfile(personaB, profileB.traits || {});
406
411
  const dist = cognitiveDistance(otA, otB);
407
- // Top 5 trait contributions
412
+ // Top 5 trait contributions. The generalized W1's level term is reported
413
+ // on its own: it is not a trait, and listing it among traits read as one.
414
+ const overallLevelContribution = dist.traitContributions[OVERALL_LEVEL_KEY] ?? 0;
408
415
  const topTraits = Object.entries(dist.traitContributions)
416
+ .filter(([trait]) => trait !== OVERALL_LEVEL_KEY)
409
417
  .sort(([, a], [, b]) => b - a)
410
418
  .slice(0, 5)
411
419
  .map(([trait, contrib]) => ({ trait, contribution: Math.round(contrib * 10000) / 10000 }));
@@ -421,6 +429,9 @@ Begin with the first persona: ${personas[0]}
421
429
  w1Distance: Math.round(dist.w1 * 10000) / 10000,
422
430
  w2Distance: Math.round(dist.w2 * 10000) / 10000,
423
431
  slicedWasserstein: Math.round(dist.sliced * 10000) / 10000,
432
+ // The part of w1 forced by the difference in mean trait level; the
433
+ // rest is shape. topDifferentiatingTraits lists traits only.
434
+ overallLevelContribution: Math.round(overallLevelContribution * 10000) / 10000,
424
435
  // The comparable view: where this pair sits within all built-in
425
436
  // persona pairs, per metric. 0 = closest pair seen, 100 = farthest.
426
437
  metrics: {
@@ -435,11 +446,19 @@ Begin with the first persona: ${personas[0]}
435
446
  scaleNote:
436
447
  // The count is read from the list: "26" went stale when the three bridge
437
448
  // traits arrived (v10 B42).
438
- `w1 (L1 sum over ${COGNITIVE_TRAITS.length} traits), w2 (L2 norm) and sliced (mean random projection) ` +
439
- "are different quantities on different scales — w1 typically runs ~20x larger " +
440
- "than sliced. Compare the percentiles, not the raw values.",
449
+ `w1 (exact generalized Wasserstein-1 distance over ${COGNITIVE_TRAITS.length} traits), ` +
450
+ "w2 (L2 norm) and sliced (mean absolute projection over uniform directions, computed exactly) are different quantities on different " +
451
+ "scales. Compare the percentiles, not the raw values. w1 sees both the shape of a profile " +
452
+ "and its overall level: each trait t carries mass t/" + COGNITIVE_TRAITS.length + ", moving mass " +
453
+ "between traits costs the trait ground metric (0.2 / 0.5 / 0.8) and creating or destroying it " +
454
+ "costs 0.4 per unit (a hand-set hypothesis), so w1 ranges from 0 to 0.4 and all traits 0.2 " +
455
+ "versus all traits 0.8 is 0.24. overallLevelContribution, reported apart from the traits, is " +
456
+ "half the cost the difference in mean trait level forces, 0.5 x 0.4 x |mean A - " +
457
+ "mean B| (the other half is charged to the traits where mass is created or destroyed). w2 and " +
458
+ "sliced are computed on profiles normalised to the simplex, so they measure shape only: " +
459
+ "proportional trait vectors are at distance 0 on both.",
441
460
  // Verdict is a percentile band, not an absolute threshold. The former
442
- // absolute bands put 209 of 210 built-in pairs in one bucket.
461
+ // absolute bands put 209 of the then 210 built-in pairs in one bucket.
443
462
  interpretation: w1Pct < 10 ? "Nearly identical cognitive profiles (closer than 90% of persona pairs)"
444
463
  : w1Pct < 30 ? "Similar cognitive profiles with minor differences"
445
464
  : w1Pct < 70 ? "Moderately different cognitive profiles"
@@ -453,7 +472,7 @@ Begin with the first persona: ${personas[0]}
453
472
  });
454
473
  server.registerTool("cognitive_coverage", {
455
474
  title: "Maximum Coverage Persona Selection",
456
- description: "Select the N most cognitively different personas from a list for maximum test coverage. Uses greedy farthest-point sampling in Wasserstein space.",
475
+ description: "Select the N most cognitively different personas from a list for maximum test coverage. Uses greedy farthest-point sampling under the generalized Wasserstein-1 distance of cognitive_distance, which counts differences in overall trait level as well as in profile shape.",
457
476
  inputSchema: {
458
477
  personas: z.array(z.string()).describe("List of persona names to select from"),
459
478
  count: z.number().describe("Number of personas to select"),
@@ -555,10 +574,10 @@ Begin with the first persona: ${personas[0]}
555
574
  });
556
575
  server.registerTool("cognitive_load_estimate", {
557
576
  title: "Cognitive Load Estimate",
558
- description: "Estimate cognitive load for a specific persona on page metrics. Returns per-dimension breakdown (information, visual, attention, decision, motor, text, memory, patience) and identifies the bottleneck dimension. Attention, motor and text use the persona's modelled capacities (sustained attention, Fitts pointing, reading). Visual is persona-invariant: no trait has a researched mapping onto visual-complexity tolerance, so it measures the page only and is listed under personaInvariant.",
577
+ description: "Estimate cognitive load for a specific persona on page metrics. Returns per-dimension breakdown (visual, attention, decision, motor, text, memory, patience) and identifies the bottleneck dimension. informationDensity is accepted and not read: through 20.x it was charged against the persona's comprehension, which is grasp of UI conventions, not reading; text load is the text dimension. Attention, motor and text use the persona's modelled capacities (sustained attention, Fitts pointing, reading). Visual is persona-invariant: no trait has a researched mapping onto visual-complexity tolerance, so it measures the page only and is listed under personaInvariant.",
559
578
  inputSchema: {
560
579
  persona: z.string().describe("Persona name"),
561
- informationDensity: z.number().min(0).max(1).describe("Content density (0=sparse, 1=dense)"),
580
+ informationDensity: z.number().min(0).max(1).describe("Content density (0=sparse, 1=dense). Accepted for compatibility and not read since 21.0"),
562
581
  visualComplexity: z.number().min(0).max(1).describe("Visual complexity (0=simple, 1=complex)"),
563
582
  interactiveElements: z.number().describe("Number of interactive elements"),
564
583
  textDensity: z.number().min(0).max(1).describe("Text heaviness (0=minimal, 1=wall of text)"),
@@ -584,7 +603,7 @@ Begin with the first persona: ${personas[0]}
584
603
  // bridged capacities. Unbridged, they sat at the 0.5 fallback and every
585
604
  // persona scored identically on all three (B29, 2026-10-10). Same helper
586
605
  // as cognitive_effort, site_cognitive_assessment and the story tool.
587
- bridgeCapacityTraits(persona, traits, p.accessibilityTraits);
606
+ bridgeCapacityTraits(persona, traits, p.accessibilityTraits, personaRateContext(p));
588
607
  const otProfile = buildOTProfile(persona, traits);
589
608
  const load = estimateCognitiveLoad(otProfile, {
590
609
  informationDensity, visualComplexity, interactiveElementCount: interactiveElements,
@@ -612,21 +631,21 @@ Begin with the first persona: ${personas[0]}
612
631
  server.registerTool("cognitive_effort", {
613
632
  _meta: { ui: { resourceUri: "ui://cbrowser/effort" } },
614
633
  title: "Cognitive Effort Analysis",
615
- description: "Compute total cognitive effort for a persona to use a page. MEASURES THE VIEWPORT BY DEFAULT (above the fold); pass scope='full_page' to measure the whole page. Uses the 8-layer Sequential Transport Chain: saliency → cognitive load → decision → motor (target acquisition) → motor procedure (executing a sequence) → frustration → readability (decoding) → reading attention (holding the line, re-reading). IMPORTANT: High-familiarity personas (power-user, confident-user) require site knowledge. If the user asks for these personas, first check if site knowledge exists by running site_model_status. If no site knowledge exists, either (1) ask the user if they want to build it first by navigating the site, or (2) warn them that results will treat the persona as a first-time visitor. The tool returns a familiarityWarning when site knowledge is missing.",
634
+ description: "Compute total cognitive effort for a persona to use a page. MEASURES THE VIEWPORT BY DEFAULT (above the fold); pass scope='full_page' to measure the whole page. Uses the 8-layer Sequential Transport Chain: visual complexity (a count of visual elements and motion, not the attention model) → cognitive load → decision → motor (target acquisition) → motor procedure (executing a sequence) → frustration → readability (decoding) → reading attention (holding the line, re-reading). Site familiarity is per run: pass siteFamiliarity (0 first visit, 1 daily user) or every persona is scored as a first visit. No persona carries a familiarity of its own, and site knowledge from crawling is reported but does not change the score. The headline number, cognitiveTransportCost.total, is a percentile: the page's raw cost ranked among the real pages of the shipped reference set, for the built-in personas, at the same scope (0.8 = costlier than 80% of them). The verdict states that rank and its band; the cost is untested against human data, so it says nothing about how a person feels, and abandonmentRisk is a separate quantity.",
616
635
  inputSchema: {
617
636
  url: z.string().url().describe("URL to analyze"),
618
637
  // Per RUN, not per persona. A persona is familiar with one site and new
619
638
  // to another, so this only has an answer once a URL is named. The
620
639
  // persona's stored value, where one exists, is the fallback. (2026-08-01)
621
640
  siteFamiliarity: z.number().min(0).max(1).optional()
622
- .describe("How familiar this persona is with THIS url: 0 first visit, 1 daily user. Set it per run — familiarity is a persona-site pair, not a disposition. Supplying it also skips the site-knowledge downgrade, which exists only to stop a stored value asserting experience of a site nobody has crawled."),
623
- persona: z.string().describe("Persona name. WORKFLOW: For power-user, confident-user, or any persona the user describes as 'experienced' — check site_model_status first. If site knowledge exists, ask the user: 'Site knowledge exists for this domain. Should I use it to simulate an experienced user, or test as a first-time visitor?' If no site knowledge exists, warn: 'No site knowledge for this domain. power-user will be tested as a first-time visitor. Run page_understand first to build site knowledge.'"),
641
+ .describe("How familiar this persona is with THIS url: 0 first visit, 1 daily user. Omitted, the run scores a first visit (0). Familiarity is a persona-site pair, not a disposition: no persona carries one, and site knowledge does not supply one."),
642
+ persona: z.string().describe("Persona name (e.g. first-timer, power-user, cognitive-adhd). To simulate someone who already knows this site, pass siteFamiliarity; the persona name does not imply it."),
624
643
  _browserToken: z.string().optional().describe("Browser session token"),
625
644
  userLocation: z.string().optional().describe("User's approximate location (e.g., 'Denver, Colorado, US')"),
626
645
  userTimezone: z.string().optional().describe("User's timezone (e.g., 'America/Denver')"),
627
646
  userLanguage: z.string().optional().describe("User's expected language (e.g., 'en-US')"),
628
647
  proxy: z.string().optional().describe("Proxy server URL for geo-accurate testing"),
629
- useValues: z.boolean().optional().default(false).describe("Enable motivational value modulation (Schwartz values). When true, persona values modulate saliency maps, decision costs, and frustration costs. Default: false (trait-only mode)."),
648
+ useValues: z.boolean().optional().default(false).describe("Apply the persona's Schwartz values (opt-in). When true, the values modulate the page's demand inside the chain, through its ten registered coefficients (chain.valueModulation.*, hand-set hypotheses, counted in modelProvenance): stimulation and security on changeBlindness; achievement on satisficing and riskTolerance; conformity on socialProofSensitivity and anchoringBias; selfDirection on socialProofSensitivity; power on fearOfMissingOut; stimulation on patience; benevolence on emotionalContagion. Each adds coefficient x score x max(chain.valueModulation.demandFloor, demand) to that trait's demand, clamped to [0, 1]; hedonism, tradition and universalism change nothing. Applies to the aggregate score and, at scope 'sequential', to every screen. Scores come from the persona registry, the persona's own schwartzValues, its Big Five, or an account persona in the CMS; `valueModulation` in the response says whether any were found. Default: false (trait-only mode)."),
630
649
  waitAfterLoad: z.number().optional().describe("Extra ms to wait after page loads (e.g., 3000 for sites with client-side translation)"),
631
650
  waitForSelector: z.string().optional().describe("CSS selector to wait for after load (e.g., '[data-translated]')"),
632
651
  scope: z.enum(["viewport", "full_page", "sequential"]).optional().default("viewport").describe("What to measure: 'viewport' (first impression, above-the-fold — default), 'full_page' (scroll the whole page, count every element), or 'sequential' (score each screen on its own content, carrying capacity forward, and report where people stop). Viewport and full_page match empathy_audit's parameter. 'sequential' answers a different question from either: not 'what would the whole page cost' but 'how far do they get'. NOTE: every published CTC number predates this parameter and is a viewport measurement."),
@@ -638,7 +657,9 @@ Begin with the first persona: ${personas[0]}
638
657
  idempotentHint: true,
639
658
  openWorldHint: true,
640
659
  },
641
- }, async ({ url, persona: personaName, siteFamiliarity, _browserToken, userLocation, userTimezone, userLanguage, proxy, useValues, waitAfterLoad, waitForSelector, scope }) => {
660
+ }, recordedAnswer(async ({ url, persona: personaName, siteFamiliarity, _browserToken, userLocation, userTimezone, userLanguage, proxy, useValues, waitAfterLoad, waitForSelector, scope }) => {
661
+ // recordedAnswer: one provenance recording per call, across every await,
662
+ // reported as modelProvenance at the end (answer-provenance.ts).
642
663
  // Hosted: http(s) only (sec-paths, 2026-10-10; see hosted-inputs.ts).
643
664
  const hostedRefusal = refuseHostedUrl("cognitive_effort", { url });
644
665
  if (hostedRefusal)
@@ -734,12 +755,10 @@ Begin with the first persona: ${personas[0]}
734
755
  const personaObj = isAgentPersonaObject(existingPersona)
735
756
  ? createCognitivePersona(personaName, personaName, {})
736
757
  : existingPersona;
737
- // v18.61.0: siteFamiliarity is a binary gate based on site knowledge
738
- // Has site knowledge → persona keeps its familiarity (maxed to 1.0 for experts)
739
- // No site knowledge → forced to 0.0 (first visit)
758
+ // siteFamiliarity: the caller's value for this run, else 0.0 (first
759
+ // visit). Site knowledge is looked up only to report it; it does not
760
+ // change the number (site-familiarity.ts has the 20.x history).
740
761
  const traits = { ...(personaObj.cognitiveTraits || {}) };
741
- const requestedFamiliarity = traits.siteFamiliarity ?? 0.5;
742
- let familiarityWarning;
743
762
  let hasSiteKnowledge = false;
744
763
  try {
745
764
  const pageUrl = await page.url();
@@ -750,26 +769,10 @@ Begin with the first persona: ${personas[0]}
750
769
  hasSiteKnowledge = !!(stats && stats.navigationNodes > 0);
751
770
  }
752
771
  catch { }
753
- // A caller who states familiarity for THIS run has answered the question
754
- // the downgrade exists to answer. The gate guards against a value STORED
755
- // on a persona claiming experience of a site nobody has crawled -- it was
756
- // never meant to overrule someone naming the pair directly.
757
- const familiarityFromCaller = typeof siteFamiliarity === "number";
758
- if (familiarityFromCaller) {
759
- traits.siteFamiliarity = siteFamiliarity;
760
- }
761
- else if (hasSiteKnowledge) {
762
- // Site knowledge exists — high-familiarity personas get max familiarity
763
- if (requestedFamiliarity > 0.5)
764
- traits.siteFamiliarity = 1.0;
765
- }
766
- else {
767
- // No site knowledge — everyone is a first-time visitor
768
- traits.siteFamiliarity = 0.0;
769
- if (requestedFamiliarity > 0.5) {
770
- familiarityWarning = `"${personaName}" has siteFamiliarity=${requestedFamiliarity.toFixed(1)} stored on the persona but no site knowledge exists for this domain. Downgraded to 0.0 (first visit). Familiarity is a persona-site pair: pass siteFamiliarity on this call to state it for this run, or build knowledge by navigating the site (page_understand goes deeper).`;
771
- }
772
- }
772
+ // The visit, not the persona: Cockburn et al.'s expertise e on the
773
+ // choosing time, handed to the demand below (Stage C). No trait carries it.
774
+ const familiarity = resolveSiteFamiliarity(siteFamiliarity, hasSiteKnowledge);
775
+ const demandOptions = { siteFamiliarity: familiarity.value };
773
776
  // Bridge the formal models into the trait vector BEFORE the chain runs.
774
777
  //
775
778
  // getReadingProfile and getPointingProfile already model this persona's
@@ -785,6 +788,9 @@ Begin with the first persona: ${personas[0]}
785
788
  // the persona's profile, so storing them would create a second copy that
786
789
  // can drift from the model that produces it. (2026-08-02)
787
790
  let pointingProfileForResponse;
791
+ // The ids the bridge read for THIS persona, reported as personaRateProvenance:
792
+ // collected from the call, so no cache state can change them.
793
+ let personaRateIds = new Set();
788
794
  try {
789
795
  const { bridgeCapacityTraits } = await import("../../visual/cognitive-models.js");
790
796
  // accessibilityTraits passed so an author's STATED reading capacity is
@@ -794,14 +800,14 @@ Begin with the first persona: ${personas[0]}
794
800
  // inline here and NOWHERE ELSE, which is why the two tools disagreed by
795
801
  // 2.6x on the motor layer: the other path scored against the missing-trait
796
802
  // fallback of 0.5 and called it a pointing ability.
797
- pointingProfileForResponse = bridgeCapacityTraits(personaName, traits, accTraits);
803
+ ({ result: pointingProfileForResponse, ids: personaRateIds } = collectPersonaReads(() => bridgeCapacityTraits(personaName, traits, accTraits, personaRateContext(personaObj))));
798
804
  }
799
805
  catch (e) {
800
806
  // Left absent rather than defaulted: a missing capacity is visible as a
801
807
  // gap, a defaulted one silently reads as average.
802
808
  console.debug(`[cognitive_effort] capacity bridge unavailable: ${e.message}`);
803
809
  }
804
- // Build OT profile (with potentially adjusted siteFamiliarity)
810
+ // Build OT profile
805
811
  const otProfile = buildOTProfile(personaName, traits);
806
812
  // Extract page metrics
807
813
  const { extractPageMetrics } = await import("../../visual/cognitive-transport.js");
@@ -833,111 +839,110 @@ Begin with the first persona: ${personas[0]}
833
839
  // full_page since that is the comparable whole-document number.
834
840
  const pageMetrics = await extractPageMetrics(page, measureScope === "sequential" ? "full_page" : measureScope, sharedGeometry);
835
841
  // Compute full COT
836
- const { computeDemandDistribution, computeSequentialCTC } = await import("../../visual/cognitive-transport-chain.js");
837
- const demand = computeDemandDistribution(pageMetrics);
838
- // SEQUENTIAL PASS. Runs before the aggregate so a failure here degrades to
839
- // the normal answer rather than losing the whole call.
840
- let sequentialResult;
842
+ const { computeDemandDistribution, computeSequentialCTC, computeSequentialScrollCTC } = await import("../../visual/cognitive-transport-chain.js");
843
+ // Motivational values (opt-in, default off). Resolved here and handed to
844
+ // the chain, which applies them through its own registered modulation
845
+ // (VALUE_DEMAND_MODULATION, chain.valueModulation.*) and clamps the result
846
+ // to [0, 1].
847
+ //
848
+ // Through 21.0 this tool had a second path of its own: a
849
+ // conservation/openness formula with unregistered 0.3 and 0.5 constants
850
+ // that scaled five decision demands and emotionalContagion in place,
851
+ // unclamped (up to 1.3), while the chain's modulation was reached from no
852
+ // tool at all. Two hand-set models of one effect, and the one that ran was
853
+ // the one the provenance block could not see. (2026-10-22)
854
+ // One resolver, shared with visual_cognitive_story (chain-values.ts).
855
+ const schwartzValues = useValues ? await resolveChainValues(personaName) : undefined;
856
+ const chainOptions = { asymmetric: true, interactions: true, ...(schwartzValues ? { schwartzValues } : {}) };
857
+ // SEQUENTIAL PASS, measuring half. The per-screen metrics are read here and
858
+ // the curve is scored in the block below with everything else, so its
859
+ // constants are counted too. A failure here degrades to the normal answer
860
+ // rather than losing the whole call.
861
+ let perScreen;
862
+ let sequentialError;
841
863
  if (measureScope === "sequential") {
842
864
  try {
843
865
  const { extractPerScreenMetrics } = await import("../../visual/cognitive-transport.js");
844
- const { computeSequentialScrollCTC } = await import("../../visual/cognitive-transport-chain.js");
845
866
  // The SAME measurement the caption came from, so the curve length and the
846
867
  // caption cannot disagree.
847
- const perScreen = await extractPerScreenMetrics(page, 20, sharedGeometry);
848
- const seqProfile = buildOTProfile(personaName, traits);
849
- const seq = computeSequentialScrollCTC(seqProfile, perScreen.map((m) => computeDemandDistribution(m)), { asymmetric: true, interactions: true });
850
- sequentialResult = {
851
- screensMeasured: seq.screens.length,
852
- // The INPUTS the screen count came from, published so a differing curve is
853
- // diagnosable instead of guessable. An external client measured 5.8 screens
854
- // on a session's first navigation and 6.7 on every later one; with only
855
- // `screensMeasured: 7` in the payload a 6-screen curve is internally
856
- // consistent and unmarked, which is the shape of every defect this tool has
857
- // had. An invisible variance is indistinguishable from no variance.
858
- geometry: {
859
- documentHeight: sharedGeometry.documentHeight,
860
- viewportHeight: sharedGeometry.viewportHeight,
861
- pageScreens: sharedGeometry.pageScreens,
862
- screenCount: sharedGeometry.screenCount,
863
- note: "One measurement, shared by the curve length and by the pageScreens caption, so the two cannot disagree. Page height can change between navigations; if two runs of the same URL differ, compare documentHeight before anything else.",
864
- },
865
- totalAbandonment: `${Math.round(seq.totalAbandonment * 100)}%`,
866
- expectedDepthScreens: seq.expectedDepthScreens,
867
- dropOff: seq.dropOff
868
- ? { screen: seq.dropOff.screen, layer: seq.dropOff.layer, shareOfSessions: `${Math.round(seq.dropOff.share * 100)}%` }
869
- : null,
870
- reachWeightedCTC: seq.reachWeightedCTC,
871
- curve: seq.screens.map((x) => ({
872
- screen: x.screen,
873
- reach: `${Math.round(x.reachProbability * 100)}%`,
874
- stopHere: `${Math.round(x.stopHazard * 100)}%`,
875
- cumulativeAbandonment: `${Math.round(x.cumulativeAbandonment * 100)}%`,
876
- screenCTC: x.screenCTC,
877
- bottleneck: x.bottleneckLayer,
878
- })),
879
- note: "Each screen is measured on ITS OWN content, and capacity carries forward -- screen 7 is scored against what screens 1-6 left, not against a fresh persona. Count-based demands (interactive elements, choices) charge only elements not seen on an earlier screen, so a sticky nav is one decision rather than one per screen; text and visual density are NOT deduplicated, because re-encountering text is re-reading it.",
880
- differsFrom: "The aggregate `abandonmentRisk` above asks what the whole document would cost if it were all processed. This asks how far they get. They are different questions and will not agree.",
881
- uncalibrated: "The per-screen hazard uses the same ABANDONMENT_HILL_EXPONENT as the aggregate, which nothing empirical set. At 2 the curve is superlinear, so this model inherits a thumb on the scale.",
882
- readThisFirst: "TRUST THE SHAPE, NOT THE TOTAL. `totalAbandonment` compounds a per-screen hazard across every screen, so on a long page it saturates toward 100% almost regardless of the page -- measured, elderly-user reached 100% on a 7-screen university homepage. That is the uncalibrated exponent compounding, not a finding about that page. `expectedDepthScreens` and `dropOff` are the numbers to act on: they say how far people get and what stops them, and both depend on the RANKING of per-screen hazards rather than their absolute level, which is the part the exponent least disturbs. `dropOff` is persona-dependent by design and that is not noise: an impatient reader drops on screen one, a patient one survives to whichever screen is genuinely hardest, and the two answers are both correct for their reader.",
883
- };
868
+ perScreen = await extractPerScreenMetrics(page, 20, sharedGeometry);
884
869
  }
885
870
  catch (e) {
886
- sequentialResult = { error: `sequential pass failed: ${e.message}`, note: "The aggregate figures below are unaffected." };
871
+ sequentialError = e.message;
887
872
  }
888
873
  }
889
- // Modulate demand by motivational values (opt-in, default off)
890
- if (useValues)
891
- try {
892
- // regPV2 writes the caller's account store, not the process-wide registry. (tenant, 2026-10-10)
893
- const { getPersonaValues, registerAccountPersonaValues: regPV2, createPersonaValues: createPV2 } = await import("../../values/index.js");
894
- let pValues = resolveValuesForPersona(personaName);
895
- // Custom persona CMS fallback
896
- if (!pValues) {
897
- try {
898
- const { getSessionApiKey } = await import("./cognitive-tools.js");
899
- const _sessionApiKey = getSessionApiKey();
900
- if (_sessionApiKey) {
901
- const cmsUrl = process.env.CMS_URL || "http://localhost:3200";
902
- const res = await fetch(`${cmsUrl}/api/personas`, { headers: { "Authorization": `Bearer ${_sessionApiKey}` } });
903
- if (res.ok) {
904
- const data = await res.json();
905
- const match = data.personas.find((p) => p.slug === personaName || p.name.toLowerCase() === personaName.toLowerCase());
906
- if (match?.schwartz_values) {
907
- const sv = typeof match.schwartz_values === "string" ? JSON.parse(match.schwartz_values) : match.schwartz_values;
908
- const pv = createPV2({ selfDirection: sv.selfDirection ?? 0.5, stimulation: sv.stimulation ?? 0.5, hedonism: sv.hedonism ?? 0.5, achievement: sv.achievement ?? 0.5, power: sv.power ?? 0.5, security: sv.security ?? 0.5, conformity: sv.conformity ?? 0.5, tradition: sv.tradition ?? 0.5, benevolence: sv.benevolence ?? 0.5, universalism: sv.universalism ?? 0.5 }, { autonomyNeed: sv.autonomyNeed ?? 0.5, competenceNeed: sv.competenceNeed ?? 0.5, relatednessNeed: sv.relatednessNeed ?? 0.5 }, "esteem");
909
- regPV2([{ personaName, values: pv, rationale: "Custom persona from CMS" }]);
910
- pValues = pv;
911
- }
912
- }
913
- }
914
- }
915
- catch { /* CMS fallback failed */ }
874
+ // The demand curves, the chain, the value modulation when useValues
875
+ // applied it, and the sequential curve when scope asks for it. These used
876
+ // to run under their own synchronous withProvenance, the only part of the
877
+ // answer the recorder could see while it was module state; the whole
878
+ // handler is one recording now (recordedAnswer), so this is just the
879
+ // computation, and the block reported below counts it with everything
880
+ // else the answer read. (2026-10-22)
881
+ const scored = (() => {
882
+ let seq;
883
+ let seqError = sequentialError;
884
+ if (perScreen) {
885
+ try {
886
+ seq = computeSequentialScrollCTC(otProfile, perScreen.map((m) => computeDemandDistribution(m, demandOptions)), chainOptions);
916
887
  }
917
- if (pValues && demand.demands) {
918
- // Conservation (security+conformity+tradition)/3 amplifies decision layer
919
- const conservation = (pValues.security + pValues.conformity + pValues.tradition) / 3;
920
- // Openness (selfDirection+stimulation)/2 reduces decision cost (comfortable with choices)
921
- const openness = (pValues.selfDirection + pValues.stimulation) / 2;
922
- // Achievement amplifies frustration layer (impatience)
923
- const achMod = 1 + (pValues.achievement - 0.5) * 0.3;
924
- // Scale decision-related demands: satisficing, anchoringBias, riskTolerance, fearOfMissingOut
925
- const decisionScale = 1 + (conservation - openness) * 0.3; // -0.15 to +0.15 range
926
- for (const dim of ["satisficing", "anchoringBias", "riskTolerance", "fearOfMissingOut", "socialProofSensitivity"]) {
927
- if (demand.demands[dim] !== undefined) {
928
- demand.demands[dim] *= decisionScale;
929
- }
930
- }
931
- // Scale frustration-related demands
932
- for (const dim of ["resilience", "selfEfficacy", "emotionalContagion"]) {
933
- if (demand.demands[dim] !== undefined) {
934
- demand.demands[dim] *= achMod;
935
- }
936
- }
888
+ catch (e) {
889
+ seqError = e.message;
937
890
  }
938
891
  }
939
- catch { /* values not available — proceed without */ }
940
- const result = computeSequentialCTC(otProfile, demand, { asymmetric: true, interactions: true });
892
+ const demand = computeDemandDistribution(pageMetrics, demandOptions);
893
+ return { aggregate: computeSequentialCTC(otProfile, demand, chainOptions), seq, seqError };
894
+ })();
895
+ const result = scored.aggregate;
896
+ let sequentialResult;
897
+ if (scored.seq) {
898
+ const seq = scored.seq;
899
+ sequentialResult = {
900
+ screensMeasured: seq.screens.length,
901
+ // The INPUTS the screen count came from, published so a differing curve is
902
+ // diagnosable instead of guessable. An external client measured 5.8 screens
903
+ // on a session's first navigation and 6.7 on every later one; with only
904
+ // `screensMeasured: 7` in the payload a 6-screen curve is internally
905
+ // consistent and unmarked, which is the shape of every defect this tool has
906
+ // had. An invisible variance is indistinguishable from no variance.
907
+ geometry: {
908
+ documentHeight: sharedGeometry.documentHeight,
909
+ viewportHeight: sharedGeometry.viewportHeight,
910
+ pageScreens: sharedGeometry.pageScreens,
911
+ screenCount: sharedGeometry.screenCount,
912
+ note: "One measurement, shared by the curve length and by the pageScreens caption, so the two cannot disagree. Page height can change between navigations; if two runs of the same URL differ, compare documentHeight before anything else.",
913
+ },
914
+ totalAbandonment: `${Math.round(seq.totalAbandonment * 100)}%`,
915
+ expectedDepthScreens: seq.expectedDepthScreens,
916
+ dropOff: seq.dropOff
917
+ ? { screen: seq.dropOff.screen, layer: seq.dropOff.layer, shareOfSessions: `${Math.round(seq.dropOff.share * 100)}%` }
918
+ : null,
919
+ // The reach-weighted MEAN of the screens' percentiles: where the typical
920
+ // screen this persona actually sees ranks. Replaces reachWeightedCTC,
921
+ // the reach-weighted SUM, which grew with every screen reached and so
922
+ // ranked a long page costlier for being long; a sum of percentiles is
923
+ // not a percentile of anything. The chain still returns the sum.
924
+ reachWeightedMeanScreenCTC: (() => {
925
+ const reach = seq.screens.reduce((a, x) => a + x.reachProbability, 0);
926
+ return reach > 0 ? Math.round((seq.screens.reduce((a, x) => a + x.reachProbability * x.screenCTC, 0) / reach) * 1000) / 1000 : null;
927
+ })(),
928
+ curve: seq.screens.map((x) => ({
929
+ screen: x.screen,
930
+ reach: `${Math.round(x.reachProbability * 100)}%`,
931
+ stopHere: `${Math.round(x.stopHazard * 100)}%`,
932
+ cumulativeAbandonment: `${Math.round(x.cumulativeAbandonment * 100)}%`,
933
+ screenCTC: x.screenCTC,
934
+ bottleneck: x.bottleneckLayer,
935
+ })),
936
+ note: "Each screen is measured on ITS OWN content, and capacity carries forward -- screen 7 is scored against what screens 1-6 left, not against a fresh persona. Count-based demands (interactive elements, choices) charge only elements not seen on an earlier screen, so a sticky nav is one decision rather than one per screen; text and visual density are NOT deduplicated, because re-encountering text is re-reading it.",
937
+ screenCTCNote: "screenCTC is a percentile, like the aggregate total: the screen's raw cost ranked among the first screens of the real reference pages, scored for the built-in personas arriving fresh. A later screen is scored against the capacity earlier screens left, so its rank reflects that depletion as well as its own content. reachWeightedMeanScreenCTC averages the screens' percentiles, each weighted by the chance of reaching it: the rank of the typical screen this persona sees. It replaces reachWeightedCTC, an unnormalised sum that grew with the number of screens reached.",
938
+ differsFrom: "The aggregate `abandonmentRisk` above asks what the whole document would cost if it were all processed. This asks how far they get. They are different questions and will not agree.",
939
+ uncalibrated: "The per-screen hazard uses the same ABANDONMENT_HILL_EXPONENT as the aggregate, which nothing empirical set. At 2 the curve is superlinear, so this model inherits a thumb on the scale.",
940
+ readThisFirst: "TRUST THE SHAPE, NOT THE TOTAL. `totalAbandonment` compounds a per-screen hazard across every screen, so on a long page it saturates toward 100% almost regardless of the page -- measured, elderly-user reached 100% on a 7-screen university homepage. That is the uncalibrated exponent compounding, not a finding about that page. `expectedDepthScreens` and `dropOff` are the numbers to act on: they say how far people get and what stops them, and both depend on the RANKING of per-screen hazards rather than their absolute level, which is the part the exponent least disturbs. `dropOff` is persona-dependent by design and that is not noise: an impatient reader drops on screen one, a patient one survives to whichever screen is genuinely hardest, and the two answers are both correct for their reader.",
941
+ };
942
+ }
943
+ else if (scored.seqError !== undefined) {
944
+ sequentialResult = { error: `sequential pass failed: ${scored.seqError}`, note: "The aggregate figures below are unaffected." };
945
+ }
941
946
  // Also compute motor and readability from formal models
942
947
  let motorResult = null;
943
948
  let readabilityResult = null;
@@ -1106,6 +1111,9 @@ Begin with the first persona: ${personas[0]}
1106
1111
  return "";
1107
1112
  } };
1108
1113
  const redirected = !!landedUrl && hostOf(landedUrl) !== "" && hostOf(landedUrl) !== hostOf(url);
1114
+ // Where the cost ranks among the real reference pages (ctc-bands.ts): the
1115
+ // band, the rank sentence and the verdict all read this one value.
1116
+ const rank = ctcRank(result);
1109
1117
  const response = {
1110
1118
  url,
1111
1119
  ...(redirected ? {
@@ -1113,27 +1121,61 @@ Begin with the first persona: ${personas[0]}
1113
1121
  redirectNote: `Requested ${hostOf(url)} and landed on ${hostOf(landedUrl)}. Every number below describes ${hostOf(landedUrl)}.`,
1114
1122
  } : { resolvedUrl: landedUrl }),
1115
1123
  persona: personaName,
1124
+ // Status counts of every registry constant the score read, and the ids
1125
+ // of the hand-set ones. A placeholder here so the field keeps its place
1126
+ // in the JSON; filled as the last step before serialising, so the block
1127
+ // covers the whole score: page measurement, demand, chain, values,
1128
+ // sequential curve and verdict. The saliency overlay, drawn as context,
1129
+ // carries its own block on its layerOverlays entry.
1130
+ modelProvenance: undefined,
1131
+ // The scored persona's motor and reading rates, counted apart from the
1132
+ // score's constants (they describe the persona).
1133
+ personaRateProvenance: personaRateProvenance(personaRateIds),
1134
+ // Stage B: the four law times the motor, decision, procedure and reading
1135
+ // demands come from, beside the demand each makes against the budget.
1136
+ lawTimes: lawTimesReport(pageMetrics, familiarity.value),
1137
+ // Only when useValues asked for it, so a persona with no values is
1138
+ // visibly scored without them rather than silently.
1139
+ ...(useValues ? { valueModulation: valueModulationReport(schwartzValues) } : {}),
1116
1140
  cognitiveTransportCost: {
1141
+ // A percentile (21.x): raw ranked among the real reference pages for
1142
+ // the built-in personas at the same scope. 0.8 is costlier than 80%.
1117
1143
  total: Math.round(result.totalCTC * 1000) / 1000,
1144
+ // The 95% interval of that percentile across bootstrap resamples of
1145
+ // the reference pages: how firmly the reference fixes the rank.
1146
+ totalCTCInterval: result.totalCTCInterval.map((x) => Math.round(x * 1000) / 1000),
1147
+ band: rank.band,
1148
+ referenceScope: result.referenceScope,
1149
+ rank: rank.sentence,
1150
+ // The whole percents that sentence states, for a view to say the
1151
+ // same numbers. total and totalCTCInterval are rounded to three
1152
+ // places, and recomputed from them the rank can differ by a point
1153
+ // (cbrowser.ai: 0.8098 is stated 80 and shipped as 0.81). end names
1154
+ // the two ends, where the sentence states a place, not a percent.
1155
+ rankFigures: {
1156
+ percent: rank.percent,
1157
+ interval: rank.interval,
1158
+ end: result.totalCTC === 0 ? "cheapest" : result.totalCTC === 1 ? "costliest" : null,
1159
+ },
1118
1160
  additive: Math.round(result.additiveCTC * 1000) / 1000,
1119
- // The un-normalized cost. `total` is a sigmoid of this, so the two
1120
- // are not interchangeable and any ratio must use `raw`.
1161
+ // The un-normalized cost. `total` is its percentile among real pages,
1162
+ // so the two are not interchangeable and any ratio must use `raw`.
1121
1163
  raw: Math.round(result.rawCTC * 1000) / 1000,
1122
1164
  // raw / additive — both in layer-cost units, so this is the genuine
1123
1165
  // contribution of interactions and sequential depletion.
1124
1166
  chainCoefficient: Math.round((result.rawCTC / Math.max(0.001, result.additiveCTC)) * 1000) / 1000,
1125
1167
  /**
1126
1168
  * @deprecated Use chainCoefficient. NOTE ITS VALUE CHANGED: this used
1127
- * to divide `total` (a sigmoid, 0-1) by `additive` (a raw sum) and
1169
+ * to divide `total` (then a logistic, 0-1) by `additive` (a raw sum) and
1128
1170
  * reported ~0.3-0.5, which read as the chain dampening by 60%. That
1129
1171
  * was a unit error, not a measurement — it was reporting the sigmoid.
1130
1172
  * Preserving the old number bit-for-bit would preserve a meaningless
1131
1173
  * one, so this now carries the corrected value.
1132
1174
  */
1133
1175
  sequentialAmplification: Math.round((result.rawCTC / Math.max(0.001, result.additiveCTC)) * 1000) / 1000,
1134
- chainNote: "chainCoefficient = raw / additive, both in layer-cost units. Above 1 means interactions and sequential depletion added cost. Re-measured 1.002-1.036 at EIGHT layers, after `motor` was split into target acquisition and sequence execution (2026-08-03). It was 1.003-1.054 at seven and 1.002-1.019 at six. The range narrowed rather than widening this time: splitting one layer into two halves each layer's per-step cost, so the same total is reached through smaller increments and less of it compounds. Real, and small. Do NOT compute total/additive — total is a sigmoid of raw and the ratio compares different units.",
1176
+ chainNote: "chainCoefficient = raw / additive, both in layer-cost units. Above 1 means interactions and sequential depletion added cost. Re-measured 1.002-1.036 at EIGHT layers, after `motor` was split into target acquisition and sequence execution (2026-08-03). It was 1.003-1.054 at seven and 1.002-1.019 at six. The range narrowed rather than widening this time: splitting one layer into two halves each layer's per-step cost, so the same total is reached through smaller increments and less of it compounds. Real, and small. Do NOT compute total/additive — total is the percentile of raw among the real reference pages, not a cost, and the ratio compares different units.",
1135
1177
  interpretationBasis: "total",
1136
- interpretationBands: "total < 0.3 comfortable, < 0.6 moderate, < 0.8 struggling, else likely abandon",
1178
+ interpretationBands: `total is a percentile of the real reference pages, banded ${ctcBandsDescription()}. The cut points are registered definitions (ctcBands.*), chosen as conventions and not fitted. A band is a rank among real pages: the CTC is untested against human data, so it says nothing about how a person experiences the page, and it is not an abandonment prediction (that is abandonmentRisk, a separate quantity). totalCTCInterval is the 95% interval of the percentile across bootstrap resamples of the reference pages: how firmly the reference pages fix the rank, not whether the cost is right.`,
1137
1179
  },
1138
1180
  layers: result.layers.map(l => ({
1139
1181
  name: l.name,
@@ -1171,10 +1213,11 @@ Begin with the first persona: ${personas[0]}
1171
1213
  abandonmentRisk: Math.round(result.abandonmentRisk * 100) + "%",
1172
1214
  // Says out loud that these two are different constructs.
1173
1215
  //
1174
- // A run can read "easy" on CTC and 53% on abandonment and both be
1175
- // correct, which looks like a contradiction and is not one:
1176
- // abandonmentRisk is a sigmoid of (deficit - 0.3*surplus) against this
1177
- // persona's blended patience and resilience, and never touches
1216
+ // A run can rank in the lower half on CTC and read 53% on abandonment
1217
+ // and both be correct, which looks like a contradiction and is not one:
1218
+ // abandonmentRisk is a Hill hazard of the deficit (unmet demand; surplus
1219
+ // no longer relieves it, 21.0) against the blended patience and
1220
+ // resilience this persona has LEFT after the page (21.0), and never touches
1178
1221
  // totalCTC. CTC asks how expensive the page is; abandonment asks
1179
1222
  // whether THIS persona's tolerance runs out first. A page can be cheap
1180
1223
  // in absolute terms and still exceed someone with little patience.
@@ -1219,7 +1262,7 @@ Begin with the first persona: ${personas[0]}
1219
1262
  pointingInputs: {
1220
1263
  fittsThroughput: Math.round(pp.throughput * 100) / 100,
1221
1264
  endpointDispersionPx: Math.round(((pp.sigmaX + pp.sigmaY) / 2) * 10) / 10,
1222
- note: "throughput drives movementTimeMs; dispersion drives hitProbability. motorCapacity weights throughput at 0.75, so the `motor` layer and `hitProbability` legitimately rank personas differently.",
1265
+ note: "throughput drives movementTimeMs; dispersion drives hitProbability. motorCapacity is throughput / (throughput + 4.9 bits/s, the reference mouse throughput) and does not read dispersion, so the `motor` layer and `hitProbability` legitimately rank personas differently.",
1223
1266
  },
1224
1267
  note: "These two are additive and unweighted against each other. There is no empirical basis for a relative weighting between acquiring a target and executing a sequence, so none is applied - see MOTOR_LAYER_WEIGHTS, both 1.0 and marked uncalibrated.",
1225
1268
  };
@@ -1277,7 +1320,7 @@ Begin with the first persona: ${personas[0]}
1277
1320
  ...(readabilityResult.blocks.length > 0 ? {
1278
1321
  hardestBlockPenaltyMs: readabilityResult.blocks
1279
1322
  .reduce((worst, b) => b.difficulty > worst.difficulty ? b : worst).penaltyBreakdown,
1280
- penaltyNote: "hardestBlockPenaltyMs carries every component, including those that came to 0. A 0 means the component was evaluated and does not apply to this text -- crowding, for one, only begins below 16px (Pelli et al. 2004) -- not that it is unmodelled. crowdingSensitivity additionally carries a 0.15 weight into readingCapacity, which reaches the readability layer at every font size.",
1323
+ penaltyNote: "hardestBlockPenaltyMs carries every component, including those that came to 0. A 0 means the component was evaluated and does not apply to this text -- crowding, for one, only begins below 16px (Pelli et al. 2004) -- not that it is unmodelled. Since Stage B crowdingSensitivity does not enter readingCapacity: that is a rate ratio, from the persona's registered reading rate or, for a custom persona that states a reading profile, the fixation model's text-independent terms (base, phonological, visual span). Crowding is priced only here, in this audit's per-block time below 16px.",
1281
1324
  } : {}),
1282
1325
  },
1283
1326
  } : {}),
@@ -1301,13 +1344,13 @@ Begin with the first persona: ${personas[0]}
1301
1344
  const screens = pageMetrics.pageScreens > 1.2
1302
1345
  ? `${wholeDocument ? " Measured across" : `${where}; the page is`} ${pageMetrics.pageScreens} screens${wholeDocument ? "." : ", so most of it was not measured — pass scope='full_page' for the whole thing.)"}${seqTail}`
1303
1346
  : where ? `${where}.)` : seqTail;
1304
- const verdict = result.totalCTC < 0.3
1305
- ? `${personaName} should handle ${subject} comfortably.`
1306
- : result.totalCTC < 0.6
1307
- ? `${personaName} will experience moderate cognitive effort on ${subject}. Bottleneck: ${result.bottleneckLayer}.`
1308
- : result.totalCTC < 0.8
1309
- ? `${personaName} will struggle significantly with ${subject}. ${result.bottleneckLayer} is the primary barrier. Consider simplifying.`
1310
- : `${personaName} is likely to abandon ${subject}. Cognitive transport cost is ${Math.round(result.totalCTC * 100)}%. Immediate remediation needed on ${result.bottleneckLayer}.`;
1347
+ // The rank, not a comfort claim. Through 21.0 this said "should handle
1348
+ // comfortably", "will struggle significantly" and "is likely to
1349
+ // abandon" off fixed points on a logistic nobody fitted; the CTC is
1350
+ // untested against human data, so a sentence on it can say where the
1351
+ // page ranks among real pages and nothing about how a person feels.
1352
+ // Abandonment stays in abandonmentRisk, its own quantity.
1353
+ const verdict = `For ${personaName}, ${subject} is ${rank.statement}. ${rank.bandSentence} Bottleneck: ${result.bottleneckLayer}.`;
1311
1354
  return verdict + screens;
1312
1355
  })(),
1313
1356
  // ONE AUTHORITATIVE FIELD PER READING DIMENSION, stated rather than
@@ -1337,7 +1380,6 @@ Begin with the first persona: ${personas[0]}
1337
1380
  scope: measureScope,
1338
1381
  pageScreens: pageMetrics.pageScreens,
1339
1382
  scopeNote: prose.scopeNote,
1340
- ...(familiarityWarning ? { familiarityWarning, siteFamiliarityAdjusted: true, originalFamiliarity: requestedFamiliarity, effectiveFamiliarity: traits.siteFamiliarity } : {}),
1341
1383
  // Reports what was USED, not what was passed.
1342
1384
  //
1343
1385
  // This previously said "supplied for this run" whenever the caller sent
@@ -1347,13 +1389,12 @@ Begin with the first persona: ${personas[0]}
1347
1389
  // asserting the parameter had been applied. An attestation nobody
1348
1390
  // verifies is worse than no attestation, because it makes an
1349
1391
  // output-level audit report the knob as working. (2026-08-02)
1350
- siteFamiliarity: traits.siteFamiliarity,
1351
- siteFamiliaritySource: familiarityFromCaller
1352
- ? "supplied for this run"
1353
- : hasSiteKnowledge
1354
- ? "derived from this install's crawl of the site"
1355
- : "defaulted to first visit — no site knowledge and none supplied",
1356
- siteFamiliarityEffect: `Consumed by the cognitiveLoad layer against a demand set by navigation depth. A page reachable in one click demands no site knowledge, so this changes nothing there.`,
1392
+ siteFamiliarity: familiarity.value,
1393
+ siteFamiliaritySource: familiarity.source,
1394
+ // Reported, not applied: crawling a site says nothing about whether
1395
+ // THIS persona has used it.
1396
+ siteKnowledgeAvailable: familiarity.siteKnowledgeAvailable,
1397
+ siteFamiliarityEffect: `Cockburn, Gutwin & Greenberg's (2007) expertise e on the choosing time: (1 - e) x novice visual search + e x the Hick-Hyman expert limit, so a familiar visitor is asked less search by every choice on the page. It moves the cognitiveLoad layer (convention knowledge, working memory, foraging, transfer), the same for every persona; the decision dispositions keep the first-visit time. It is not a persona trait. The score is still ranked against first visits (e = 0).`,
1357
1398
  ...(languageWarning ? { languageWarning } : {}),
1358
1399
  ...(userLocation ? { userContext: { location: userLocation, timezone: userTimezone, language: userLanguage } } : {}),
1359
1400
  ...(token ? { _browserToken: token } : {}),
@@ -1367,6 +1408,7 @@ Begin with the first persona: ${personas[0]}
1367
1408
  let motorTemp;
1368
1409
  try {
1369
1410
  if (motorResult && motorResult.elements.length > 0) {
1411
+ const { writeFileSync, mkdirSync, existsSync } = await import("fs");
1370
1412
  motorTemp = await makeTempDir("cog-effort");
1371
1413
  const ssPath = motorTemp.file("page.png");
1372
1414
  await page.screenshot({ path: ssPath, fullPage: false });
@@ -1435,20 +1477,32 @@ Begin with the first persona: ${personas[0]}
1435
1477
  layersTemp = await makeTempDir("cog-effort-layers");
1436
1478
  const ssPath = layersTemp.file("page.png");
1437
1479
  await page.screenshot({ path: ssPath, fullPage: false });
1438
- // saliency -- keyless. Lab-space centre-surround over the screenshot,
1439
- // so this needs no API key and no model call.
1480
+ // visualComplexity -- keyless. Lab-space centre-surround over the
1481
+ // screenshot, so this needs no API key and no model call. It is shown as
1482
+ // visual context for the layer, NOT as its input: the layer's cost comes
1483
+ // from the visual-complexity count and the animation level, and this map
1484
+ // never reaches the chain. The legend says so, because the layer was
1485
+ // called "saliency" until 21.0 and this heatmap read as its evidence.
1440
1486
  try {
1441
1487
  const { computeLabSaliency } = await import("../../visual/attention-transport.js");
1442
1488
  const { generateHeatmapOverlay } = await import("../../visual/heatmap-overlay.js");
1443
- const sal = await computeLabSaliency(ssPath);
1444
- const b64 = await generateHeatmapOverlay(ssPath, sal.cells, sal.rows, sal.cols, `${personaName} — saliency`);
1489
+ // Recorded apart from the score. The map reads the attention model's
1490
+ // constants (published colour science, fitted gaze calibration), and
1491
+ // counted in modelProvenance they made the score look partly fitted
1492
+ // while the map is context, not an input. Its own block rides on
1493
+ // this overlay entry.
1494
+ const { result: b64, provenance: mapProvenance } = await isolatedProvenanceAsync(async () => {
1495
+ const sal = await computeLabSaliency(ssPath);
1496
+ return generateHeatmapOverlay(ssPath, sal.cells, sal.rows, sal.cols, `${personaName} — saliency`);
1497
+ }, ATTENTION_PROVENANCE_TEXT);
1445
1498
  const w = writeArtifact(Buffer.from(b64, "base64"), `saliency-${personaName}-${Date.now()}.png`);
1446
1499
  if (w)
1447
- layerOverlays.push({ layer: "saliency", file: w.url.split("/").pop(), available: true,
1448
- legend: "Hotter = more visually salient before any reading happens." });
1500
+ layerOverlays.push({ layer: "visualComplexity", file: w.url.split("/").pop(), available: true,
1501
+ legend: "Lab-space saliency of the screenshot, shown as context. Hotter = more visually salient. The layer's cost is not computed from this map: it comes from the page's visual-complexity count and animation level.",
1502
+ provenance: mapProvenance });
1449
1503
  }
1450
1504
  catch (e) {
1451
- layerOverlays.push({ layer: "saliency", available: false, reason: `saliency map failed: ${e.message}` });
1505
+ layerOverlays.push({ layer: "visualComplexity", available: false, reason: `saliency map failed: ${e.message}` });
1452
1506
  }
1453
1507
  // motor -- already drawn above; surfaced here so the chain has one list.
1454
1508
  if (response.motorOverlayUrl) {
@@ -1512,6 +1566,9 @@ Begin with the first persona: ${personas[0]}
1512
1566
  response.layerOverlays = layerOverlays;
1513
1567
  response.layerOverlayNote = "Each entry maps to a bar in the transport chain. Layers without an overlay say why.";
1514
1568
  }
1569
+ // Last, so it covers everything above. Serialising the previews reads no
1570
+ // registered constant.
1571
+ response.modelProvenance = activeProvenance();
1515
1572
  const content = motorPng
1516
1573
  ? await buildContentWithPreviews(response, [{ name: "motor", png: motorPng, fullResolution: motorFullResolution }], (p) => ({ motorOverlayPreview: p.motor }), { maxCharsPerImage: MOTOR_PREVIEW_MAX_CHARS })
1517
1574
  : [{ type: "text", text: JSON.stringify(response, null, 2) }];
@@ -1537,6 +1594,6 @@ Begin with the first persona: ${personas[0]}
1537
1594
  }],
1538
1595
  };
1539
1596
  }
1540
- });
1597
+ }));
1541
1598
  }
1542
1599
  //# sourceMappingURL=persona-comparison-tools.js.map