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