cbrowser 20.2.2 → 21.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-questionnaire.js +1 -1
- package/dist/persona-questionnaire.js.map +1 -1
- package/dist/personas.d.ts.map +1 -1
- package/dist/personas.js +133 -82
- 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 +334 -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/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 +112 -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
package/dist/cif-score.js
CHANGED
|
@@ -11,16 +11,46 @@
|
|
|
11
11
|
* 4. Sensory Fitness (15%) -- can they process without overload?
|
|
12
12
|
* 5. Navigation Fitness (20%) -- can they get around?
|
|
13
13
|
*
|
|
14
|
+
* Every number the score computes with is read through param() from
|
|
15
|
+
* visual/provenance/cif.ts, which says where it comes from: five are WCAG's,
|
|
16
|
+
* four are units or display choices, and the rest (the weights above among
|
|
17
|
+
* them) were set by hand. tests/cif-no-literals.test.ts keeps it that way.
|
|
18
|
+
*
|
|
19
|
+
* MISSING INPUTS follow one rule, stated with its rationale at cif.noData.score
|
|
20
|
+
* in visual/provenance/cif.ts: a component scores only from inputs the caller
|
|
21
|
+
* supplied, no assumed value ever stands in for a missing one, and a component
|
|
22
|
+
* with none of its inputs supplied is marked `measured: false`, scores
|
|
23
|
+
* cif.noData.score, says "Not measured" in its details, is listed in
|
|
24
|
+
* `notMeasured` and is never a top issue. `barriers` counts as supplied when it
|
|
25
|
+
* is an array, even an empty one (an audit ran and found nothing); undefined
|
|
26
|
+
* means no audit ran.
|
|
27
|
+
*
|
|
28
|
+
* CERTIFICATION needs every component measured. The grade is read from the
|
|
29
|
+
* composite as always, but platinum, gold, silver and bronze are issued only
|
|
30
|
+
* when all 25 components were measured; otherwise the certification is "none"
|
|
31
|
+
* and `certificationNote` says how many were not. An unmeasured component scores
|
|
32
|
+
* cif.noData.score, so without this a page with nothing measured certified
|
|
33
|
+
* bronze. (Alexa, 2026-10-24.)
|
|
34
|
+
*
|
|
14
35
|
* @copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com https://cbrowser.ai
|
|
15
36
|
* @license MIT
|
|
16
37
|
* @since v19.0.0
|
|
17
38
|
*/
|
|
39
|
+
import { param } from "./visual/model-provenance.js";
|
|
40
|
+
import { bandSentence, isPercentile, rankStatement } from "./visual/ctc-bands.js";
|
|
18
41
|
// ============================================================================
|
|
19
42
|
// Utility functions
|
|
20
43
|
// ============================================================================
|
|
21
|
-
/**
|
|
44
|
+
/** Read every id in `ids` through param(); one object per call so the reads are recorded. */
|
|
45
|
+
function readParams(ids) {
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const k in ids)
|
|
48
|
+
out[k] = param(ids[k]);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** Clamp value between 0 and the top of the scale */
|
|
22
52
|
function clamp(val) {
|
|
23
|
-
return Math.max(0, Math.min(
|
|
53
|
+
return Math.max(0, Math.min(param("cif.scale.max"), val));
|
|
24
54
|
}
|
|
25
55
|
/** Sigmoid function centered at 0, scaled to [0, 1] */
|
|
26
56
|
function sigmoid(x) {
|
|
@@ -28,19 +58,36 @@ function sigmoid(x) {
|
|
|
28
58
|
}
|
|
29
59
|
/** Weighted average of component scores */
|
|
30
60
|
function weightedAvg(components) {
|
|
61
|
+
const empty = param("cif.emptyAverage");
|
|
31
62
|
let totalWeight = 0;
|
|
32
63
|
let totalScore = 0;
|
|
33
64
|
for (const c of components) {
|
|
34
65
|
totalScore += c.score * c.weight;
|
|
35
66
|
totalWeight += c.weight;
|
|
36
67
|
}
|
|
37
|
-
return totalWeight > 0 ? totalScore / totalWeight :
|
|
68
|
+
return totalWeight > 0 ? totalScore / totalWeight : empty;
|
|
38
69
|
}
|
|
39
|
-
/**
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
/** True when the caller supplied a usable number (not null, undefined or NaN). */
|
|
71
|
+
function supplied(val) {
|
|
72
|
+
return val != null && !Number.isNaN(val);
|
|
73
|
+
}
|
|
74
|
+
/** A measured component score. */
|
|
75
|
+
function measured(score, details) {
|
|
76
|
+
return { score, details, measured: true };
|
|
77
|
+
}
|
|
78
|
+
/** A component none of whose inputs was supplied; `missing` names them. See cif.noData.score. */
|
|
79
|
+
function notMeasured(missing) {
|
|
80
|
+
return { score: param("cif.noData.score"), details: `Not measured: no ${missing} supplied`, measured: false };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Score at the knee of a two-slope piecewise component: where the slope from
|
|
84
|
+
* the free point reaches it. Derived rather than registered, so the branch
|
|
85
|
+
* above the knee always starts where the branch below ends. overloadThreshold
|
|
86
|
+
* stepped from 61 to 60 at its knee because its knee score was a separate
|
|
87
|
+
* constant (21.0).
|
|
88
|
+
*/
|
|
89
|
+
function kneeScore(top, free, knee, slopeBelow) {
|
|
90
|
+
return top - (knee - free) * slopeBelow;
|
|
44
91
|
}
|
|
45
92
|
/** Count barriers of a given type */
|
|
46
93
|
function countBarriersByType(barriers, type) {
|
|
@@ -48,97 +95,137 @@ function countBarriersByType(barriers, type) {
|
|
|
48
95
|
return 0;
|
|
49
96
|
return barriers.filter((b) => b.type === type).length;
|
|
50
97
|
}
|
|
51
|
-
/**
|
|
52
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Does a barrier's type or description name `word`? As a whole word or its
|
|
100
|
+
* plural, with "_" and "-" as separators: "form_unlabeled", "long-form",
|
|
101
|
+
* "Form input" and "captions" match; "Information", "platform", "format",
|
|
102
|
+
* "performance", "conform" and "skipped" do not. A substring match counted
|
|
103
|
+
* empathy_audit's colour-only barrier ("Information conveyed by color alone")
|
|
104
|
+
* as a form barrier too (21.0).
|
|
105
|
+
*/
|
|
106
|
+
function matchesBarrier(b, word) {
|
|
107
|
+
const named = new RegExp(`(^|[^\\p{L}\\p{N}])${word.toLowerCase()}s?([^\\p{L}\\p{N}]|$)`, "u");
|
|
108
|
+
return named.test(b.type.toLowerCase()) || named.test((b.description || "").toLowerCase());
|
|
109
|
+
}
|
|
110
|
+
/** Count barriers whose type or description names `word` (matchesBarrier). */
|
|
111
|
+
function countBarriersMatching(barriers, word) {
|
|
53
112
|
if (!barriers)
|
|
54
113
|
return 0;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
114
|
+
return barriers.filter((b) => matchesBarrier(b, word)).length;
|
|
115
|
+
}
|
|
116
|
+
/** empathy_audit's close-spacing barrier, which it weights as target_spacing. */
|
|
117
|
+
function isCloseSpacing(b) {
|
|
118
|
+
return b.weightKey === "target_spacing";
|
|
119
|
+
}
|
|
120
|
+
function countCloseSpacingBarriers(barriers) {
|
|
121
|
+
return barriers ? barriers.filter(isCloseSpacing).length : 0;
|
|
58
122
|
}
|
|
59
123
|
// ============================================================================
|
|
60
124
|
// Category Scorers
|
|
61
125
|
// ============================================================================
|
|
126
|
+
const MOTOR_IDS = {
|
|
127
|
+
scale: "cif.scale.max",
|
|
128
|
+
targetPx: "cif.wcag.targetSizeEnhancedPx",
|
|
129
|
+
touchBarrierPenalty: "cif.motor.touchTarget.barrierPenalty",
|
|
130
|
+
touchBarrierFloor: "cif.motor.touchTarget.barrierFloor",
|
|
131
|
+
hitMidpointPx: "cif.motor.hit.midpointPx",
|
|
132
|
+
hitScalePx: "cif.motor.hit.scalePx",
|
|
133
|
+
interactiveFree: "cif.motor.interactive.freeCount",
|
|
134
|
+
interactiveKnee: "cif.motor.interactive.kneeCount",
|
|
135
|
+
interactiveSlopeBelow: "cif.motor.interactive.slopeBelowKnee",
|
|
136
|
+
interactiveSlopeAbove: "cif.motor.interactive.slopeAboveKnee",
|
|
137
|
+
spacingBarrierPenalty: "cif.motor.spacing.barrierPenalty",
|
|
138
|
+
spacingGapPx: "cif.motor.spacing.minGapPx",
|
|
139
|
+
dragBarrierPenalty: "cif.motor.drag.barrierPenalty",
|
|
140
|
+
wTouchTarget: "cif.motor.weight.touchTargetCompliance",
|
|
141
|
+
wHit: "cif.motor.weight.hitProbability",
|
|
142
|
+
wInteraction: "cif.motor.weight.interactionCount",
|
|
143
|
+
wSpacing: "cif.motor.weight.spacingAdequacy",
|
|
144
|
+
wDrag: "cif.motor.weight.dragFreeAlternatives",
|
|
145
|
+
};
|
|
62
146
|
function computeMotorFitness(data) {
|
|
147
|
+
const P = readParams(MOTOR_IDS);
|
|
148
|
+
const targets = data.touchTargets && data.touchTargets.length > 0 ? data.touchTargets : null;
|
|
63
149
|
// --- touchTargetCompliance (30%) ---
|
|
64
150
|
let touchTargetCompliance;
|
|
65
|
-
if (
|
|
66
|
-
const nonExempt =
|
|
151
|
+
if (targets) {
|
|
152
|
+
const nonExempt = targets.filter((t) => !t.exempt);
|
|
67
153
|
if (nonExempt.length === 0) {
|
|
68
|
-
touchTargetCompliance =
|
|
154
|
+
touchTargetCompliance = measured(P.scale, "All interactive elements are exempt from size requirements");
|
|
69
155
|
}
|
|
70
156
|
else {
|
|
71
|
-
const compliant = nonExempt.filter((t) => Math.min(t.width, t.height) >=
|
|
72
|
-
const pct = (compliant / nonExempt.length) *
|
|
73
|
-
touchTargetCompliance = {
|
|
74
|
-
score: clamp(Math.round(pct)),
|
|
75
|
-
details: `${compliant}/${nonExempt.length} interactive elements meet 44px minimum (${Math.round(pct)}%)`,
|
|
76
|
-
};
|
|
157
|
+
const compliant = nonExempt.filter((t) => Math.min(t.width, t.height) >= P.targetPx).length;
|
|
158
|
+
const pct = (compliant / nonExempt.length) * P.scale;
|
|
159
|
+
touchTargetCompliance = measured(clamp(Math.round(pct)), `${compliant}/${nonExempt.length} interactive elements meet ${P.targetPx}px minimum (${Math.round(pct)}%)`);
|
|
77
160
|
}
|
|
78
161
|
}
|
|
79
|
-
else {
|
|
80
|
-
//
|
|
162
|
+
else if (data.barriers) {
|
|
163
|
+
// No sizes: fall back to the audit's touch_target barriers
|
|
81
164
|
const touchBarriers = countBarriersByType(data.barriers, "touch_target");
|
|
82
|
-
touchTargetCompliance =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
165
|
+
touchTargetCompliance = measured(clamp(Math.max(P.touchBarrierFloor, P.scale - touchBarriers * P.touchBarrierPenalty)), touchBarriers > 0
|
|
166
|
+
? `${touchBarriers} touch target barriers detected (no size data; scored from barriers)`
|
|
167
|
+
: "No touch target barriers detected (no size data; scored from barriers)");
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
touchTargetCompliance = notMeasured("touch target sizes or audit barriers");
|
|
88
171
|
}
|
|
89
172
|
// --- hitProbability (25%) ---
|
|
90
173
|
let hitProbability;
|
|
91
|
-
if (
|
|
92
|
-
const nonExempt =
|
|
174
|
+
if (targets) {
|
|
175
|
+
const nonExempt = targets.filter((t) => !t.exempt);
|
|
93
176
|
if (nonExempt.length === 0) {
|
|
94
|
-
hitProbability =
|
|
177
|
+
hitProbability = measured(P.scale, "No non-exempt interactive elements");
|
|
95
178
|
}
|
|
96
179
|
else {
|
|
97
180
|
const avgMinDim = nonExempt.reduce((sum, t) => sum + Math.min(t.width, t.height), 0) /
|
|
98
181
|
nonExempt.length;
|
|
99
|
-
const score = sigmoid((avgMinDim -
|
|
100
|
-
hitProbability = {
|
|
101
|
-
score: clamp(Math.round(score)),
|
|
102
|
-
details: `Average minimum dimension: ${Math.round(avgMinDim)}px (sigmoid-scored)`,
|
|
103
|
-
};
|
|
182
|
+
const score = sigmoid((avgMinDim - P.hitMidpointPx) / P.hitScalePx) * P.scale;
|
|
183
|
+
hitProbability = measured(clamp(Math.round(score)), `Average minimum dimension: ${Math.round(avgMinDim)}px (sigmoid-scored)`);
|
|
104
184
|
}
|
|
105
185
|
}
|
|
106
186
|
else {
|
|
107
|
-
hitProbability =
|
|
108
|
-
score: 100,
|
|
109
|
-
details: "No touch target barriers detected",
|
|
110
|
-
};
|
|
187
|
+
hitProbability = notMeasured("touch target sizes");
|
|
111
188
|
}
|
|
112
189
|
// --- interactionCount (20%) ---
|
|
113
|
-
// Under
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
190
|
+
// Under the free count interactive elements are fine; penalize progressively above that
|
|
191
|
+
let interactionCount;
|
|
192
|
+
if (supplied(data.interactiveCount)) {
|
|
193
|
+
const interactiveCount = data.interactiveCount;
|
|
194
|
+
const interactionCountScore = interactiveCount <= P.interactiveFree ? P.scale
|
|
195
|
+
: interactiveCount <= P.interactiveKnee ? clamp(Math.round(P.scale - (interactiveCount - P.interactiveFree) * P.interactiveSlopeBelow))
|
|
196
|
+
: clamp(Math.round(kneeScore(P.scale, P.interactiveFree, P.interactiveKnee, P.interactiveSlopeBelow) - (interactiveCount - P.interactiveKnee) * P.interactiveSlopeAbove));
|
|
197
|
+
interactionCount = measured(interactionCountScore, interactiveCount <= P.interactiveFree
|
|
121
198
|
? `${interactiveCount} interactive elements — manageable for motor impairments`
|
|
122
|
-
: `${interactiveCount} interactive elements on page (fewer is easier for motor impairments)
|
|
123
|
-
}
|
|
199
|
+
: `${interactiveCount} interactive elements on page (fewer is easier for motor impairments)`);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
interactionCount = notMeasured("interactive element count");
|
|
203
|
+
}
|
|
124
204
|
// --- spacingAdequacy (15%) ---
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
205
|
+
// Reads only the audit's close-spacing barrier. touch_target barriers are SIZE
|
|
206
|
+
// findings and belong to touchTargetCompliance; counting them here too
|
|
207
|
+
// penalised a page twice for the same barrier.
|
|
208
|
+
let spacingAdequacy;
|
|
209
|
+
if (data.barriers) {
|
|
210
|
+
const closeSpacing = countCloseSpacingBarriers(data.barriers);
|
|
211
|
+
spacingAdequacy = measured(clamp(Math.round(P.scale - closeSpacing * P.spacingBarrierPenalty)), closeSpacing > 0
|
|
212
|
+
? `${closeSpacing} close-spacing barriers detected (interactive elements under ${P.spacingGapPx}px apart)`
|
|
213
|
+
: "No close-spacing barriers detected");
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
spacingAdequacy = notMeasured("audit barriers");
|
|
217
|
+
}
|
|
133
218
|
// --- dragFreeAlternatives (10%) ---
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
details: dragBarriers > 0
|
|
219
|
+
let dragFreeAlternatives;
|
|
220
|
+
if (data.barriers) {
|
|
221
|
+
const dragBarriers = countBarriersMatching(data.barriers, "drag");
|
|
222
|
+
dragFreeAlternatives = measured(clamp(P.scale - dragBarriers * P.dragBarrierPenalty), dragBarriers > 0
|
|
139
223
|
? `${dragBarriers} drag-dependent interactions found without alternatives`
|
|
140
|
-
: "No drag-dependent interactions detected"
|
|
141
|
-
}
|
|
224
|
+
: "No drag-dependent interactions detected");
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
dragFreeAlternatives = notMeasured("audit barriers");
|
|
228
|
+
}
|
|
142
229
|
const components = {
|
|
143
230
|
touchTargetCompliance,
|
|
144
231
|
hitProbability,
|
|
@@ -147,79 +234,101 @@ function computeMotorFitness(data) {
|
|
|
147
234
|
dragFreeAlternatives,
|
|
148
235
|
};
|
|
149
236
|
const score = clamp(Math.round(weightedAvg([
|
|
150
|
-
{ score: touchTargetCompliance.score, weight:
|
|
151
|
-
{ score: hitProbability.score, weight:
|
|
152
|
-
{ score: interactionCount.score, weight:
|
|
153
|
-
{ score: spacingAdequacy.score, weight:
|
|
154
|
-
{ score: dragFreeAlternatives.score, weight:
|
|
237
|
+
{ score: touchTargetCompliance.score, weight: P.wTouchTarget },
|
|
238
|
+
{ score: hitProbability.score, weight: P.wHit },
|
|
239
|
+
{ score: interactionCount.score, weight: P.wInteraction },
|
|
240
|
+
{ score: spacingAdequacy.score, weight: P.wSpacing },
|
|
241
|
+
{ score: dragFreeAlternatives.score, weight: P.wDrag },
|
|
155
242
|
])));
|
|
156
243
|
return { score, components };
|
|
157
244
|
}
|
|
245
|
+
const VISUAL_IDS = {
|
|
246
|
+
scale: "cif.scale.max",
|
|
247
|
+
normalText: "cif.wcag.contrastNormalText",
|
|
248
|
+
largeText: "cif.wcag.contrastLargeText",
|
|
249
|
+
contrastBarrierPenalty: "cif.visual.contrast.barrierPenalty",
|
|
250
|
+
proxySlope: "cif.visual.contrast.proxySlope",
|
|
251
|
+
proxyIntercept: "cif.visual.contrast.proxyIntercept",
|
|
252
|
+
densityKnee: "cif.visual.textScalability.densityKnee",
|
|
253
|
+
scalabilitySlope: "cif.visual.textScalability.slope",
|
|
254
|
+
resizePct: "cif.wcag.resizeTextPercent",
|
|
255
|
+
colorBarrierPenalty: "cif.visual.color.barrierPenalty",
|
|
256
|
+
wContrast: "cif.visual.weight.contrastCompliance",
|
|
257
|
+
wScalability: "cif.visual.weight.textScalability",
|
|
258
|
+
wColor: "cif.visual.weight.colorIndependence",
|
|
259
|
+
wFocus: "cif.visual.weight.focusVisibility",
|
|
260
|
+
wSpacing: "cif.visual.weight.spacingDensity",
|
|
261
|
+
};
|
|
158
262
|
function computeVisualFitness(data) {
|
|
263
|
+
const P = readParams(VISUAL_IDS);
|
|
159
264
|
// --- contrastCompliance (30%) ---
|
|
160
265
|
let contrastCompliance;
|
|
266
|
+
const contrastBarriers = countBarriersByType(data.barriers, "contrast");
|
|
161
267
|
if (data.contrastIssues && data.contrastIssues.length > 0) {
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
details: `${data.contrastIssues.length} contrast issues (${failingBelow45} normal text below 4.5:1, ${failingBelow3} large text below 3:1)`,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
// Fall back to contrast barriers
|
|
175
|
-
const contrastBarriers = countBarriersByType(data.barriers, "contrast");
|
|
176
|
-
if (contrastBarriers > 0) {
|
|
177
|
-
contrastCompliance = {
|
|
178
|
-
score: clamp(100 - contrastBarriers * 10),
|
|
179
|
-
details: `${contrastBarriers} contrast barriers detected`,
|
|
180
|
-
};
|
|
268
|
+
const issues = data.contrastIssues.length;
|
|
269
|
+
const failingNormalText = data.contrastIssues.filter((c) => !c.isLargeText && c.ratio < P.normalText).length;
|
|
270
|
+
const failingLargeText = data.contrastIssues.filter((c) => c.isLargeText && c.ratio < P.largeText).length;
|
|
271
|
+
const breakdown = `${failingNormalText} normal text below ${P.normalText}:1, ${failingLargeText} large text below ${P.largeText}:1`;
|
|
272
|
+
if (supplied(data.elementCount)) {
|
|
273
|
+
// Share of elements with an issue, the element count standing in for the text elements checked
|
|
274
|
+
const failRatio = issues / Math.max(data.elementCount, issues);
|
|
275
|
+
contrastCompliance = measured(clamp(Math.round(P.scale - failRatio * P.scale)), `${issues} contrast issues among ${Math.max(data.elementCount, issues)} elements (${breakdown})`);
|
|
181
276
|
}
|
|
182
277
|
else {
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
contrastCompliance = {
|
|
186
|
-
score: clamp(Math.round(accessibilityProxy * 0.8 + 20)),
|
|
187
|
-
details: "No contrast data -- estimated from agent-ready accessibility score",
|
|
188
|
-
};
|
|
278
|
+
// No element count, so no share: scored per issue, like contrast barriers
|
|
279
|
+
contrastCompliance = measured(clamp(P.scale - issues * P.contrastBarrierPenalty), `${issues} contrast issues (${breakdown}); no element count, so scored per issue`);
|
|
189
280
|
}
|
|
190
281
|
}
|
|
282
|
+
else if (contrastBarriers > 0) {
|
|
283
|
+
contrastCompliance = measured(clamp(P.scale - contrastBarriers * P.contrastBarrierPenalty), `${contrastBarriers} contrast barriers detected`);
|
|
284
|
+
}
|
|
285
|
+
else if (supplied(data.agentReadyScore)) {
|
|
286
|
+
// Use agent_ready accessibility subscore as proxy
|
|
287
|
+
contrastCompliance = measured(clamp(Math.round(data.agentReadyScore * P.proxySlope + P.proxyIntercept)), "No contrast data -- estimated from agent-ready accessibility score");
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
contrastCompliance = notMeasured("contrast issues, contrast barriers or agent-ready score");
|
|
291
|
+
}
|
|
191
292
|
// --- textScalability (20%) ---
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
293
|
+
// A proxy: nothing is zoomed. Penalised only when density is high enough that
|
|
294
|
+
// zooming would plausibly break the layout; the details say it was not checked.
|
|
295
|
+
let textScalability;
|
|
296
|
+
if (supplied(data.informationDensity)) {
|
|
297
|
+
const density = data.informationDensity;
|
|
298
|
+
const scalabilityDeduction = density > P.densityKnee ? (density - P.densityKnee) * P.scalabilitySlope : 0;
|
|
299
|
+
textScalability = measured(clamp(Math.round(P.scale - scalabilityDeduction)), scalabilityDeduction > 0
|
|
300
|
+
? `High density (${(density * P.scale).toFixed(0)}%) may cause overlap at ${P.resizePct}% zoom (-${Math.round(scalabilityDeduction)}); estimated from density, the page was not zoomed`
|
|
301
|
+
: `Information density ${(density * P.scale).toFixed(0)}% is at or below ${(P.densityKnee * P.scale).toFixed(0)}%, so no deduction; resizing text to ${P.resizePct}% (WCAG 1.4.4) was not checked`);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
textScalability = notMeasured("information density");
|
|
305
|
+
}
|
|
201
306
|
// --- colorIndependence (20%) ---
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
details: colorOnlyBarriers > 0
|
|
307
|
+
let colorIndependence;
|
|
308
|
+
if (data.barriers) {
|
|
309
|
+
const colorOnlyBarriers = countBarriersMatching(data.barriers, "color");
|
|
310
|
+
colorIndependence = measured(clamp(P.scale - colorOnlyBarriers * P.colorBarrierPenalty), colorOnlyBarriers > 0
|
|
207
311
|
? `${colorOnlyBarriers} color-only information barriers detected`
|
|
208
|
-
: "No color-only information barriers detected"
|
|
209
|
-
}
|
|
312
|
+
: "No color-only information barriers detected");
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
colorIndependence = notMeasured("audit barriers");
|
|
316
|
+
}
|
|
210
317
|
// --- focusVisibility (15%) ---
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
318
|
+
let focusVisibility;
|
|
319
|
+
if (supplied(data.semanticScore)) {
|
|
320
|
+
focusVisibility = measured(clamp(Math.round(data.semanticScore)), `Estimated from semantic score (${Math.round(data.semanticScore)}/${P.scale})`);
|
|
321
|
+
}
|
|
322
|
+
else if (supplied(data.agentReadyScore)) {
|
|
323
|
+
focusVisibility = measured(clamp(Math.round(data.agentReadyScore)), `Estimated from agent-ready score (${Math.round(data.agentReadyScore)}/${P.scale})`);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
focusVisibility = notMeasured("semantic or agent-ready score");
|
|
327
|
+
}
|
|
217
328
|
// --- spacingDensity (15%) ---
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
details: `Information density: ${(density * 100).toFixed(0)}% (lower is better for visual processing)`,
|
|
222
|
-
};
|
|
329
|
+
const spacingDensity = supplied(data.informationDensity)
|
|
330
|
+
? measured(clamp(Math.round(P.scale - data.informationDensity * P.scale)), `Information density: ${(data.informationDensity * P.scale).toFixed(0)}% (lower is better for visual processing)`)
|
|
331
|
+
: notMeasured("information density");
|
|
223
332
|
const components = {
|
|
224
333
|
contrastCompliance,
|
|
225
334
|
textScalability,
|
|
@@ -228,82 +337,77 @@ function computeVisualFitness(data) {
|
|
|
228
337
|
spacingDensity,
|
|
229
338
|
};
|
|
230
339
|
const score = clamp(Math.round(weightedAvg([
|
|
231
|
-
{ score: contrastCompliance.score, weight:
|
|
232
|
-
{ score: textScalability.score, weight:
|
|
233
|
-
{ score: colorIndependence.score, weight:
|
|
234
|
-
{ score: focusVisibility.score, weight:
|
|
235
|
-
{ score: spacingDensity.score, weight:
|
|
340
|
+
{ score: contrastCompliance.score, weight: P.wContrast },
|
|
341
|
+
{ score: textScalability.score, weight: P.wScalability },
|
|
342
|
+
{ score: colorIndependence.score, weight: P.wColor },
|
|
343
|
+
{ score: focusVisibility.score, weight: P.wFocus },
|
|
344
|
+
{ score: spacingDensity.score, weight: P.wSpacing },
|
|
236
345
|
])));
|
|
237
346
|
return { score, components };
|
|
238
347
|
}
|
|
348
|
+
const COGNITIVE_IDS = {
|
|
349
|
+
scale: "cif.scale.max",
|
|
350
|
+
costDecimals: "cif.display.costDecimals",
|
|
351
|
+
decisionCostSlope: "cif.cognitive.decision.costSlope",
|
|
352
|
+
fleschGain: "cif.cognitive.readability.fleschGain",
|
|
353
|
+
densityFree: "cif.cognitive.density.freeCount",
|
|
354
|
+
densityKnee: "cif.cognitive.density.kneeCount",
|
|
355
|
+
densityDivisorBelow: "cif.cognitive.density.divisorBelowKnee",
|
|
356
|
+
densityDivisorAbove: "cif.cognitive.density.divisorAboveKnee",
|
|
357
|
+
captureGain: "cif.cognitive.attention.captureGain",
|
|
358
|
+
navFree: "cif.cognitive.predictability.freeCount",
|
|
359
|
+
navSlope: "cif.cognitive.predictability.slope",
|
|
360
|
+
wDecision: "cif.cognitive.weight.decisionComplexity",
|
|
361
|
+
wReadability: "cif.cognitive.weight.readability",
|
|
362
|
+
wDensity: "cif.cognitive.weight.informationDensity",
|
|
363
|
+
wAttention: "cif.cognitive.weight.attentionClarity",
|
|
364
|
+
wPredictability: "cif.cognitive.weight.predictability",
|
|
365
|
+
};
|
|
239
366
|
function computeCognitiveFitness(data) {
|
|
367
|
+
const P = readParams(COGNITIVE_IDS);
|
|
240
368
|
// --- decisionComplexity (25%) ---
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
const decisionScore = clamp(Math.round(100 - decisionCost * 200));
|
|
247
|
-
const decisionComplexity = {
|
|
248
|
-
score: decisionScore,
|
|
249
|
-
details: decisionCost > 0
|
|
250
|
-
? `Decision layer cost: ${decisionCost.toFixed(3)} (lower is better)`
|
|
251
|
-
: "No CTC decision layer data available",
|
|
252
|
-
};
|
|
369
|
+
const decisionLayer = data.layers?.find((l) => l.name.toLowerCase().includes("decision") || l.name.toLowerCase().includes("choice"));
|
|
370
|
+
const decisionComplexity = decisionLayer
|
|
371
|
+
? measured(clamp(Math.round(P.scale - decisionLayer.cost * P.decisionCostSlope)), `Decision layer cost: ${decisionLayer.cost.toFixed(P.costDecimals)} (lower is better)`)
|
|
372
|
+
: notMeasured(data.layers && data.layers.length > 0 ? "decision or choice layer among the CTC layers" : "CTC layers");
|
|
253
373
|
// --- readability (20%) ---
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
: 60; // No data = assume mediocre readability (not optimistic)
|
|
258
|
-
const readability = {
|
|
259
|
-
score: readabilityScore,
|
|
260
|
-
details: flesch != null
|
|
261
|
-
? `Flesch readability score: ${Math.round(flesch)} (higher = more readable)`
|
|
262
|
-
: "No readability data available — estimated 60 (conservative)",
|
|
263
|
-
};
|
|
374
|
+
const readability = supplied(data.fleschScore)
|
|
375
|
+
? measured(clamp(Math.round(Math.min(P.scale, data.fleschScore * P.fleschGain))), `Flesch readability score: ${Math.round(data.fleschScore)} (higher = more readable)`)
|
|
376
|
+
: notMeasured("Flesch reading-ease score");
|
|
264
377
|
// --- informationDensity (20%) ---
|
|
265
378
|
// A well-structured page can have many elements without being overwhelming.
|
|
266
379
|
// Only penalize when element count is extremely high relative to viewport.
|
|
267
|
-
// Threshold:
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
380
|
+
// Threshold: the free count is fine, the knee starts getting dense, 2000+ is overwhelming.
|
|
381
|
+
let informationDensity;
|
|
382
|
+
if (supplied(data.elementCount)) {
|
|
383
|
+
const elemCount = data.elementCount;
|
|
384
|
+
const infoScore = elemCount <= P.densityFree ? P.scale
|
|
385
|
+
: elemCount <= P.densityKnee ? clamp(Math.round(P.scale - (elemCount - P.densityFree) / P.densityDivisorBelow))
|
|
386
|
+
: clamp(Math.round(kneeScore(P.scale, P.densityFree, P.densityKnee, 1 / P.densityDivisorBelow) - (elemCount - P.densityKnee) / P.densityDivisorAbove));
|
|
387
|
+
informationDensity = measured(infoScore, elemCount <= P.densityFree
|
|
275
388
|
? `${elemCount} elements — well within cognitive limits`
|
|
276
|
-
: `${elemCount} elements on page (progressive penalty above
|
|
277
|
-
}
|
|
389
|
+
: `${elemCount} elements on page (progressive penalty above ${P.densityFree})`);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
informationDensity = notMeasured("element count");
|
|
393
|
+
}
|
|
278
394
|
// --- attentionClarity (20%) ---
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
: 60; // No data = assume mediocre CTA clarity (not optimistic)
|
|
283
|
-
const attentionClarity = {
|
|
284
|
-
score: attentionScore,
|
|
285
|
-
details: captureRate != null
|
|
286
|
-
? `CTA capture rate: ${(captureRate * 100).toFixed(1)}% (higher = clearer attention guidance)`
|
|
287
|
-
: "No attention data available — estimated 60 (conservative)",
|
|
288
|
-
};
|
|
395
|
+
const attentionClarity = supplied(data.ctaCaptureRate)
|
|
396
|
+
? measured(clamp(Math.round(Math.min(P.scale, data.ctaCaptureRate * P.captureGain))), `CTA capture rate: ${(data.ctaCaptureRate * P.scale).toFixed(1)}% (higher = clearer attention guidance)`)
|
|
397
|
+
: notMeasured("CTA capture rate");
|
|
289
398
|
// --- predictability (15%) ---
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
399
|
+
let predictability;
|
|
400
|
+
if (supplied(data.navItemCount)) {
|
|
401
|
+
const navItems = data.navItemCount;
|
|
402
|
+
// One slope above the free count. The knee at 12 it replaced joined two equal
|
|
403
|
+
// slopes, so it changed no score (21.0).
|
|
404
|
+
predictability = measured(navItems <= P.navFree ? P.scale : clamp(Math.round(P.scale - (navItems - P.navFree) * P.navSlope)), navItems <= P.navFree
|
|
405
|
+
? `${navItems} navigation items — clear, predictable structure`
|
|
406
|
+
: `${navItems} navigation items (${P.navFree} or fewer is optimal for cognitive predictability)`);
|
|
297
407
|
}
|
|
298
408
|
else {
|
|
299
|
-
|
|
409
|
+
predictability = notMeasured("navigation item count");
|
|
300
410
|
}
|
|
301
|
-
const predictability = {
|
|
302
|
-
score: predictScore,
|
|
303
|
-
details: navItems <= 7
|
|
304
|
-
? `${navItems} navigation items — clear, predictable structure`
|
|
305
|
-
: `${navItems} navigation items (7 or fewer is optimal for cognitive predictability)`,
|
|
306
|
-
};
|
|
307
411
|
const components = {
|
|
308
412
|
decisionComplexity,
|
|
309
413
|
readability,
|
|
@@ -312,71 +416,123 @@ function computeCognitiveFitness(data) {
|
|
|
312
416
|
predictability,
|
|
313
417
|
};
|
|
314
418
|
const score = clamp(Math.round(weightedAvg([
|
|
315
|
-
{ score: decisionComplexity.score, weight:
|
|
316
|
-
{ score: readability.score, weight:
|
|
317
|
-
{ score: informationDensity.score, weight:
|
|
318
|
-
{ score: attentionClarity.score, weight:
|
|
319
|
-
{ score: predictability.score, weight:
|
|
419
|
+
{ score: decisionComplexity.score, weight: P.wDecision },
|
|
420
|
+
{ score: readability.score, weight: P.wReadability },
|
|
421
|
+
{ score: informationDensity.score, weight: P.wDensity },
|
|
422
|
+
{ score: attentionClarity.score, weight: P.wAttention },
|
|
423
|
+
{ score: predictability.score, weight: P.wPredictability },
|
|
320
424
|
])));
|
|
321
425
|
return { score, components };
|
|
322
426
|
}
|
|
427
|
+
const SENSORY_IDS = {
|
|
428
|
+
scale: "cif.scale.max",
|
|
429
|
+
costDecimals: "cif.display.costDecimals",
|
|
430
|
+
controlledFloor: "cif.sensory.animation.controlledFloor",
|
|
431
|
+
controlledPenalty: "cif.sensory.animation.controlledPenalty",
|
|
432
|
+
uncontrolledPenalty: "cif.sensory.animation.uncontrolledPenalty",
|
|
433
|
+
audioBarrierPenalty: "cif.sensory.audio.barrierPenalty",
|
|
434
|
+
calmDensityWeight: "cif.sensory.calm.densityWeight",
|
|
435
|
+
calmAnimationPenalty: "cif.sensory.calm.animationPenalty",
|
|
436
|
+
// The verdict bands' cut points (provenance/ctc-reference.ts), not CIF's own:
|
|
437
|
+
// totalCTC is a percentile of real pages since 21.x, and CIF's 0.3 and 0.6
|
|
438
|
+
// were cuts on the logistic it used to be.
|
|
439
|
+
easyCTC: "ctcBands.upperHalf",
|
|
440
|
+
kneeCTC: "ctcBands.high",
|
|
441
|
+
overloadSlopeBelow: "cif.sensory.overload.slopeBelowKnee",
|
|
442
|
+
overloadSlopeAbove: "cif.sensory.overload.slopeAboveKnee",
|
|
443
|
+
wAnimation: "cif.sensory.weight.animationControl",
|
|
444
|
+
wIndependence: "cif.sensory.weight.sensoryIndependence",
|
|
445
|
+
wCalm: "cif.sensory.weight.visualCalm",
|
|
446
|
+
wLayout: "cif.sensory.weight.layoutConsistency",
|
|
447
|
+
wOverload: "cif.sensory.weight.overloadThreshold",
|
|
448
|
+
};
|
|
323
449
|
function computeSensoryFitness(data) {
|
|
450
|
+
const P = readParams(SENSORY_IDS);
|
|
324
451
|
// --- animationControl (30%) ---
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
452
|
+
// With prefers-reduced-motion support, animations are user-controllable → a small tax per animation, down to the floor.
|
|
453
|
+
// Without support, each animation is a hard penalty. With animations and no support flag, the score is not measured.
|
|
454
|
+
const motionSupport = data.hasReducedMotionSupport;
|
|
455
|
+
let animationControl;
|
|
456
|
+
if (!supplied(data.animationCount)) {
|
|
457
|
+
animationControl = notMeasured("animation count");
|
|
458
|
+
}
|
|
459
|
+
else if (data.animationCount === 0) {
|
|
460
|
+
animationControl = measured(P.scale, "No animations detected");
|
|
461
|
+
}
|
|
462
|
+
else if (motionSupport === true) {
|
|
463
|
+
animationControl = measured(clamp(Math.max(P.controlledFloor, P.scale - data.animationCount * P.controlledPenalty)), // floor-to-top range — minor tax for quantity
|
|
464
|
+
`${data.animationCount} animations detected — prefers-reduced-motion supported (user can disable)`);
|
|
465
|
+
}
|
|
466
|
+
else if (motionSupport === false) {
|
|
467
|
+
animationControl = measured(clamp(Math.round(P.scale - data.animationCount * P.uncontrolledPenalty)), `${data.animationCount} animations detected without motion preference support (each deducts ${P.uncontrolledPenalty} points)`);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
animationControl = notMeasured(`prefers-reduced-motion support flag for the ${data.animationCount} animations detected`);
|
|
471
|
+
}
|
|
340
472
|
// --- sensoryIndependence (25%) ---
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
473
|
+
let sensoryIndependence;
|
|
474
|
+
if (data.barriers) {
|
|
475
|
+
// Each barrier once, even when it mentions both captions and audio
|
|
476
|
+
const sensoryBarriers = data.barriers.filter((b) => matchesBarrier(b, "caption") || matchesBarrier(b, "audio")).length;
|
|
477
|
+
sensoryIndependence = measured(clamp(P.scale - sensoryBarriers * P.audioBarrierPenalty), sensoryBarriers > 0
|
|
478
|
+
? `${sensoryBarriers} audio/caption barriers (each deducts ${P.audioBarrierPenalty})`
|
|
479
|
+
: "No audio/caption accessibility barriers detected");
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
sensoryIndependence = notMeasured("audit barriers");
|
|
483
|
+
}
|
|
351
484
|
// --- visualCalm (20%) ---
|
|
352
|
-
|
|
353
|
-
// If animations are user-controllable, they don't contribute to visual noise
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
485
|
+
// Density is the input; animations adjust it only when their count and, if any, their motion support are known.
|
|
486
|
+
// If animations are user-controllable, they don't contribute to visual noise.
|
|
487
|
+
let visualCalm;
|
|
488
|
+
if (supplied(data.informationDensity)) {
|
|
489
|
+
const density = data.informationDensity;
|
|
490
|
+
const densityText = `Information density: ${(density * P.scale).toFixed(0)}%`;
|
|
491
|
+
let animCalmPenalty = 0;
|
|
492
|
+
let animText;
|
|
493
|
+
if (!supplied(data.animationCount)) {
|
|
494
|
+
animText = ", animations not measured";
|
|
495
|
+
}
|
|
496
|
+
else if (motionSupport === true) {
|
|
497
|
+
animText = " (animations controllable via prefers-reduced-motion)";
|
|
498
|
+
}
|
|
499
|
+
else if (motionSupport === false || data.animationCount === 0) {
|
|
500
|
+
animCalmPenalty = data.animationCount * P.calmAnimationPenalty;
|
|
501
|
+
animText = `, ${data.animationCount} uncontrolled animations`;
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
animText = `, ${data.animationCount} animations (prefers-reduced-motion support not measured)`;
|
|
505
|
+
}
|
|
506
|
+
visualCalm = measured(clamp(Math.round(P.scale - density * P.calmDensityWeight - animCalmPenalty)), `${densityText}${animText}`);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
visualCalm = notMeasured("information density");
|
|
510
|
+
}
|
|
362
511
|
// --- layoutConsistency (15%) ---
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
score: consistencyScore,
|
|
367
|
-
details: `Layout consistency derived from structural score (${Math.round(agentReady)}/100)`,
|
|
368
|
-
};
|
|
512
|
+
const layoutConsistency = supplied(data.agentReadyScore)
|
|
513
|
+
? measured(clamp(Math.round(data.agentReadyScore)), `Layout consistency derived from structural score (${Math.round(data.agentReadyScore)}/${P.scale})`)
|
|
514
|
+
: notMeasured("agent-ready score");
|
|
369
515
|
// --- overloadThreshold (10%) ---
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
516
|
+
// totalCTC is a percentile of real pages. Full marks through the lower half
|
|
517
|
+
// (ctcBands.upperHalf), sloping to the knee at the high band (ctcBands.high),
|
|
518
|
+
// then steeper. The details state the rank; the CTC is untested against
|
|
519
|
+
// human data, so they say nothing about how easy the page is.
|
|
520
|
+
let overloadThreshold;
|
|
521
|
+
if (supplied(data.totalCTC) && !isPercentile(data.totalCTC)) {
|
|
522
|
+
// A value outside zero to one is from another scale (a raw cost, or a
|
|
523
|
+
// number from before 21.x): it has no rank to score.
|
|
524
|
+
overloadThreshold = notMeasured(`percentile total CTC (${data.totalCTC} is not a percentile, so it is from another scale and has no rank among the real pages we measured)`);
|
|
525
|
+
}
|
|
526
|
+
else if (supplied(data.totalCTC)) {
|
|
527
|
+
const totalCTC = data.totalCTC;
|
|
528
|
+
const overloadScore = totalCTC <= P.easyCTC ? P.scale
|
|
529
|
+
: totalCTC <= P.kneeCTC ? clamp(Math.round(P.scale - (totalCTC - P.easyCTC) * P.overloadSlopeBelow))
|
|
530
|
+
: clamp(Math.round(kneeScore(P.scale, P.easyCTC, P.kneeCTC, P.overloadSlopeBelow) - (totalCTC - P.kneeCTC) * P.overloadSlopeAbove));
|
|
531
|
+
overloadThreshold = measured(overloadScore, `Total CTC ${totalCTC.toFixed(P.costDecimals)}: ${rankStatement(totalCTC)}. ${bandSentence(totalCTC)}`);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
overloadThreshold = notMeasured("total CTC");
|
|
535
|
+
}
|
|
380
536
|
const components = {
|
|
381
537
|
animationControl,
|
|
382
538
|
sensoryIndependence,
|
|
@@ -385,15 +541,27 @@ function computeSensoryFitness(data) {
|
|
|
385
541
|
overloadThreshold,
|
|
386
542
|
};
|
|
387
543
|
const score = clamp(Math.round(weightedAvg([
|
|
388
|
-
{ score: animationControl.score, weight:
|
|
389
|
-
{ score: sensoryIndependence.score, weight:
|
|
390
|
-
{ score: visualCalm.score, weight:
|
|
391
|
-
{ score: layoutConsistency.score, weight:
|
|
392
|
-
{ score: overloadThreshold.score, weight:
|
|
544
|
+
{ score: animationControl.score, weight: P.wAnimation },
|
|
545
|
+
{ score: sensoryIndependence.score, weight: P.wIndependence },
|
|
546
|
+
{ score: visualCalm.score, weight: P.wCalm },
|
|
547
|
+
{ score: layoutConsistency.score, weight: P.wLayout },
|
|
548
|
+
{ score: overloadThreshold.score, weight: P.wOverload },
|
|
393
549
|
])));
|
|
394
550
|
return { score, components };
|
|
395
551
|
}
|
|
552
|
+
const NAVIGATION_IDS = {
|
|
553
|
+
scale: "cif.scale.max",
|
|
554
|
+
keyboardBarrierPenalty: "cif.navigation.keyboard.barrierPenalty",
|
|
555
|
+
skipBarrierPenalty: "cif.navigation.skip.barrierPenalty",
|
|
556
|
+
formBarrierPenalty: "cif.navigation.form.barrierPenalty",
|
|
557
|
+
wSemantic: "cif.navigation.weight.semanticStructure",
|
|
558
|
+
wKeyboard: "cif.navigation.weight.keyboardOperability",
|
|
559
|
+
wSkip: "cif.navigation.weight.skipMechanisms",
|
|
560
|
+
wLinkButton: "cif.navigation.weight.linkButtonClarity",
|
|
561
|
+
wForm: "cif.navigation.weight.formAccessibility",
|
|
562
|
+
};
|
|
396
563
|
function computeNavigationFitness(data) {
|
|
564
|
+
const P = readParams(NAVIGATION_IDS);
|
|
397
565
|
// --- semanticStructure (25%) ---
|
|
398
566
|
const subScores = [];
|
|
399
567
|
if (data.semanticScore != null)
|
|
@@ -402,51 +570,71 @@ function computeNavigationFitness(data) {
|
|
|
402
570
|
subScores.push(data.headingScore);
|
|
403
571
|
if (data.landmarkScore != null)
|
|
404
572
|
subScores.push(data.landmarkScore);
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
573
|
+
let semanticStructure;
|
|
574
|
+
if (subScores.length > 0) {
|
|
575
|
+
const semanticAvg = subScores.reduce((a, b) => a + b, 0) / subScores.length;
|
|
576
|
+
semanticStructure = measured(clamp(Math.round(semanticAvg)), `Average of ${subScores.length} semantic sub-scores: ${Math.round(semanticAvg)}/${P.scale}`);
|
|
577
|
+
}
|
|
578
|
+
else if (supplied(data.agentReadyScore)) {
|
|
579
|
+
semanticStructure = measured(clamp(Math.round(data.agentReadyScore)), `Estimated from agent-ready score: ${Math.round(data.agentReadyScore)}/${P.scale}`);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
semanticStructure = notMeasured("semantic, heading or landmark score, or agent-ready score");
|
|
583
|
+
}
|
|
414
584
|
// --- keyboardOperability (25%) ---
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
details
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
585
|
+
let keyboardOperability;
|
|
586
|
+
if (data.barriers) {
|
|
587
|
+
// Each barrier once: a keyboard finding, or a motor_precision one other than
|
|
588
|
+
// the close-spacing barrier (spacingAdequacy counts that)
|
|
589
|
+
const kbBarriers = data.barriers.filter((b) => matchesBarrier(b, "keyboard") || (b.type === "motor_precision" && !isCloseSpacing(b))).length;
|
|
590
|
+
// empathy_audit's keyboard findings are hover-only and drag-without-keyboard
|
|
591
|
+
// interactions; the details say what it looks for and what it does not. The
|
|
592
|
+
// scope is the audit's, stated as such: a caller's list may hold other
|
|
593
|
+
// keyboard findings (a trap, an unreachable control), counted as found.
|
|
594
|
+
const auditScope = "keyboard reachability, traps or focus order";
|
|
595
|
+
keyboardOperability = measured(clamp(P.scale - kbBarriers * P.keyboardBarrierPenalty), kbBarriers > 0
|
|
596
|
+
? `${kbBarriers} keyboard barriers detected; empathy_audit looks for hover-only and drag-without-keyboard interactions, not ${auditScope}`
|
|
597
|
+
: `No hover-only or drag-without-keyboard barriers detected; empathy_audit does not check ${auditScope}`);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
keyboardOperability = notMeasured("audit barriers");
|
|
601
|
+
}
|
|
425
602
|
// --- skipMechanisms (15%) ---
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
603
|
+
// Measured only when a skip-link check is known to have run: the caller says
|
|
604
|
+
// so (skipLinksChecked), or the list holds a skip finding. empathy_audit has
|
|
605
|
+
// no skip-link detector, so its empty list said nothing about skip links, and
|
|
606
|
+
// this read a clean 100 on every page (21.0).
|
|
607
|
+
let skipMechanisms;
|
|
608
|
+
const skipBarriers = data.barriers ? countBarriersMatching(data.barriers, "skip") : 0;
|
|
609
|
+
if (!data.barriers) {
|
|
610
|
+
skipMechanisms = notMeasured("audit barriers");
|
|
611
|
+
}
|
|
612
|
+
else if (data.skipLinksChecked !== true && skipBarriers === 0) {
|
|
613
|
+
skipMechanisms = notMeasured("skip-link check");
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
skipMechanisms = measured(clamp(P.scale - skipBarriers * P.skipBarrierPenalty), skipBarriers > 0
|
|
431
617
|
? `${skipBarriers} skip mechanism issues detected`
|
|
432
|
-
: "No skip navigation issues detected"
|
|
433
|
-
}
|
|
618
|
+
: "No skip navigation issues detected");
|
|
619
|
+
}
|
|
434
620
|
// --- linkButtonClarity (15%) ---
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
details: `ARIA label score: ${Math.round(ariaProxy)}/100`,
|
|
439
|
-
};
|
|
621
|
+
const linkButtonClarity = supplied(data.ariaScore)
|
|
622
|
+
? measured(clamp(Math.round(data.ariaScore)), `ARIA label score: ${Math.round(data.ariaScore)}/${P.scale}`)
|
|
623
|
+
: notMeasured("ARIA score");
|
|
440
624
|
// --- formAccessibility (20%) ---
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
:
|
|
449
|
-
|
|
625
|
+
// The label score is the base; the audit's form barriers deduct from it. Either alone is a measurement.
|
|
626
|
+
let formAccessibility;
|
|
627
|
+
if (supplied(data.formLabelScore) || data.barriers) {
|
|
628
|
+
const labelScore = supplied(data.formLabelScore) ? data.formLabelScore : null;
|
|
629
|
+
const formBarriers = data.barriers ? countBarriersMatching(data.barriers, "form") : 0;
|
|
630
|
+
const labelText = labelScore !== null ? `Form label score: ${Math.round(labelScore)}/${P.scale}` : "Form label score not measured";
|
|
631
|
+
const barrierText = !data.barriers ? ", form barriers not measured"
|
|
632
|
+
: formBarriers > 0 ? `, ${formBarriers} form barriers` : "";
|
|
633
|
+
formAccessibility = measured(clamp(Math.round((labelScore ?? P.scale) - formBarriers * P.formBarrierPenalty)), `${labelText}${barrierText}`);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
formAccessibility = notMeasured("form label score or audit barriers");
|
|
637
|
+
}
|
|
450
638
|
const components = {
|
|
451
639
|
semanticStructure,
|
|
452
640
|
keyboardOperability,
|
|
@@ -455,92 +643,134 @@ function computeNavigationFitness(data) {
|
|
|
455
643
|
formAccessibility,
|
|
456
644
|
};
|
|
457
645
|
const score = clamp(Math.round(weightedAvg([
|
|
458
|
-
{ score: semanticStructure.score, weight:
|
|
459
|
-
{ score: keyboardOperability.score, weight:
|
|
460
|
-
{ score: skipMechanisms.score, weight:
|
|
461
|
-
{ score: linkButtonClarity.score, weight:
|
|
462
|
-
{ score: formAccessibility.score, weight:
|
|
646
|
+
{ score: semanticStructure.score, weight: P.wSemantic },
|
|
647
|
+
{ score: keyboardOperability.score, weight: P.wKeyboard },
|
|
648
|
+
{ score: skipMechanisms.score, weight: P.wSkip },
|
|
649
|
+
{ score: linkButtonClarity.score, weight: P.wLinkButton },
|
|
650
|
+
{ score: formAccessibility.score, weight: P.wForm },
|
|
463
651
|
])));
|
|
464
652
|
return { score, components };
|
|
465
653
|
}
|
|
466
654
|
// ============================================================================
|
|
467
655
|
// Grading & Certification
|
|
468
656
|
// ============================================================================
|
|
469
|
-
|
|
470
|
-
|
|
657
|
+
const GRADE_IDS = {
|
|
658
|
+
aPlus: "cif.grade.aPlus",
|
|
659
|
+
a: "cif.grade.a",
|
|
660
|
+
b: "cif.grade.b",
|
|
661
|
+
c: "cif.grade.c",
|
|
662
|
+
d: "cif.grade.d",
|
|
663
|
+
};
|
|
664
|
+
/**
|
|
665
|
+
* The grade for a composite, from the registered cif.grade.* bands. Exported so
|
|
666
|
+
* a caller that grades an average of composites (the dashboard) uses these bands
|
|
667
|
+
* rather than a copy of their numbers.
|
|
668
|
+
*/
|
|
669
|
+
export function computeGrade(composite) {
|
|
670
|
+
const G = readParams(GRADE_IDS);
|
|
671
|
+
if (composite >= G.aPlus)
|
|
471
672
|
return "A+";
|
|
472
|
-
if (composite >=
|
|
673
|
+
if (composite >= G.a)
|
|
473
674
|
return "A";
|
|
474
|
-
if (composite >=
|
|
675
|
+
if (composite >= G.b)
|
|
475
676
|
return "B";
|
|
476
|
-
if (composite >=
|
|
677
|
+
if (composite >= G.c)
|
|
477
678
|
return "C";
|
|
478
|
-
if (composite >=
|
|
679
|
+
if (composite >= G.d)
|
|
479
680
|
return "D";
|
|
480
681
|
return "F";
|
|
481
682
|
}
|
|
683
|
+
const CERTIFICATION_IDS = {
|
|
684
|
+
platinum: "cif.certification.platinum",
|
|
685
|
+
gold: "cif.certification.gold",
|
|
686
|
+
silver: "cif.certification.silver",
|
|
687
|
+
bronze: "cif.certification.bronze",
|
|
688
|
+
};
|
|
482
689
|
function computeCertification(motor, visual, cognitive, sensory, navigation) {
|
|
690
|
+
const C = readParams(CERTIFICATION_IDS);
|
|
483
691
|
const all = [motor, visual, cognitive, sensory, navigation];
|
|
484
692
|
const minScore = Math.min(...all);
|
|
485
|
-
if (minScore >=
|
|
693
|
+
if (minScore >= C.platinum)
|
|
486
694
|
return "platinum";
|
|
487
|
-
if (minScore >=
|
|
695
|
+
if (minScore >= C.gold)
|
|
488
696
|
return "gold";
|
|
489
|
-
if (minScore >=
|
|
697
|
+
if (minScore >= C.silver)
|
|
490
698
|
return "silver";
|
|
491
|
-
if (minScore >=
|
|
699
|
+
if (minScore >= C.bronze)
|
|
492
700
|
return "bronze";
|
|
493
701
|
return "none";
|
|
494
702
|
}
|
|
495
703
|
// ============================================================================
|
|
496
704
|
// Fix Recommendations
|
|
497
705
|
// ============================================================================
|
|
498
|
-
|
|
706
|
+
// Every number a fix quotes is one of these, so the advice cannot drift from
|
|
707
|
+
// the scorer (tests/cif-fix-text.test.ts parses this file to hold it there).
|
|
708
|
+
const FIX_IDS = {
|
|
709
|
+
scale: "cif.scale.max",
|
|
710
|
+
targetPx: "cif.wcag.targetSizeEnhancedPx",
|
|
711
|
+
normalText: "cif.wcag.contrastNormalText",
|
|
712
|
+
largeText: "cif.wcag.contrastLargeText",
|
|
713
|
+
nonTextContrast: "cif.wcag.nonTextContrast",
|
|
714
|
+
resizePct: "cif.wcag.resizeTextPercent",
|
|
715
|
+
interactiveFree: "cif.motor.interactive.freeCount",
|
|
716
|
+
spacingGapPx: "cif.motor.spacing.minGapPx",
|
|
717
|
+
densityKnee: "cif.visual.textScalability.densityKnee",
|
|
718
|
+
choiceThreshold: "cif.fix.decision.choiceThreshold",
|
|
719
|
+
fleschGain: "cif.cognitive.readability.fleschGain",
|
|
720
|
+
elementsFree: "cif.cognitive.density.freeCount",
|
|
721
|
+
navFree: "cif.cognitive.predictability.freeCount",
|
|
722
|
+
};
|
|
723
|
+
/** Fix text for a measured component. Exported for tests; topIssues is the only caller. */
|
|
724
|
+
export function generateFixRecommendation(category, component, score, data) {
|
|
725
|
+
const F = readParams(FIX_IDS);
|
|
499
726
|
const key = `${category}.${component}`;
|
|
500
727
|
switch (key) {
|
|
501
728
|
case "motor.touchTargetCompliance": {
|
|
502
729
|
const count = data.touchTargets
|
|
503
|
-
? data.touchTargets.filter((t) => !t.exempt && Math.min(t.width, t.height) <
|
|
730
|
+
? data.touchTargets.filter((t) => !t.exempt && Math.min(t.width, t.height) < F.targetPx).length
|
|
504
731
|
: countBarriersByType(data.barriers, "touch_target");
|
|
505
|
-
return `Increase ${count || "undersized"} interactive element sizes to at least
|
|
732
|
+
return `Increase ${count || "undersized"} interactive element sizes to at least ${F.targetPx}x${F.targetPx}px (WCAG 2.5.5 AAA)`;
|
|
733
|
+
}
|
|
734
|
+
case "motor.hitProbability": {
|
|
735
|
+
const sizes = (data.touchTargets ?? []).filter((t) => !t.exempt).map((t) => Math.min(t.width, t.height));
|
|
736
|
+
const avg = sizes.length > 0 ? Math.round(sizes.reduce((a, b) => a + b, 0) / sizes.length) : null;
|
|
737
|
+
return `Increase interactive element sizes${avg !== null ? ` (smaller side ${avg}px on average)` : ""} toward ${F.targetPx}x${F.targetPx}px (WCAG 2.5.5 AAA) for reliable motor access`;
|
|
506
738
|
}
|
|
507
|
-
case "motor.hitProbability":
|
|
508
|
-
return "Increase average interactive element dimensions -- target 48px minimum for reliable motor access";
|
|
509
739
|
case "motor.interactionCount":
|
|
510
|
-
return `Reduce interactive elements from ${
|
|
740
|
+
return `Reduce interactive elements on a single view from ${data.interactiveCount} to ${F.interactiveFree} or fewer`;
|
|
511
741
|
case "motor.spacingAdequacy":
|
|
512
|
-
return
|
|
742
|
+
return `Space adjacent interactive elements at least ${F.spacingGapPx}px apart (the gap below which the close-spacing barrier is reported) to prevent mis-taps`;
|
|
513
743
|
case "motor.dragFreeAlternatives":
|
|
514
744
|
return "Provide keyboard/click alternatives for all drag-and-drop interactions";
|
|
515
745
|
case "visual.contrastCompliance": {
|
|
516
746
|
const count = data.contrastIssues?.length || countBarriersByType(data.barriers, "contrast");
|
|
517
|
-
return `Fix ${count} elements with contrast ratios below
|
|
747
|
+
return `Fix ${count} elements with contrast ratios below ${F.normalText}:1 for normal text or ${F.largeText}:1 for large text`;
|
|
518
748
|
}
|
|
519
749
|
case "visual.textScalability":
|
|
520
|
-
return
|
|
750
|
+
return `Ensure text resizes to ${F.resizePct}% without loss of content or functionality (WCAG 1.4.4); this score is estimated from information density and the page was not zoomed`;
|
|
521
751
|
case "visual.colorIndependence":
|
|
522
752
|
return "Add non-color indicators (icons, patterns, text labels) wherever color conveys meaning";
|
|
523
753
|
case "visual.focusVisibility":
|
|
524
|
-
return
|
|
754
|
+
return `Ensure every interactive element has a visible focus indicator (WCAG 2.4.7) with at least ${F.nonTextContrast}:1 contrast against adjacent colors (WCAG 1.4.11)`;
|
|
525
755
|
case "visual.spacingDensity":
|
|
526
|
-
return
|
|
756
|
+
return `Increase whitespace between content blocks to lower information density${supplied(data.informationDensity) ? ` (now ${(data.informationDensity * F.scale).toFixed(0)}%)` : ""}; above ${(F.densityKnee * F.scale).toFixed(0)}% textScalability is penalised too`;
|
|
527
757
|
case "cognitive.decisionComplexity": {
|
|
528
|
-
const choices =
|
|
529
|
-
if (choices >
|
|
530
|
-
return `Reduce choices from ${choices} to
|
|
758
|
+
const choices = data.interactiveCount;
|
|
759
|
+
if (supplied(choices) && choices > F.choiceThreshold) {
|
|
760
|
+
return `Reduce choices from ${choices} to ${F.choiceThreshold} or fewer per decision point (decision time grows with the number of options: Hick-Hyman law)`;
|
|
531
761
|
}
|
|
532
762
|
return "Simplify decision points -- group related options, provide defaults, reduce choices per step";
|
|
533
763
|
}
|
|
534
764
|
case "cognitive.readability":
|
|
535
|
-
return `Simplify text to
|
|
765
|
+
return `Simplify text to raise Flesch reading ease from ${Math.round(data.fleschScore)} to ${Math.ceil(F.scale / F.fleschGain)} or more, where readability scores full marks`;
|
|
536
766
|
case "cognitive.informationDensity":
|
|
537
|
-
return `Reduce visible elements from ${
|
|
767
|
+
return `Reduce visible elements from ${data.elementCount} toward ${F.elementsFree} -- use progressive disclosure to reveal content incrementally`;
|
|
538
768
|
case "cognitive.attentionClarity":
|
|
539
769
|
return "Make primary CTA visually dominant -- increase size, contrast, and whitespace around conversion elements";
|
|
540
770
|
case "cognitive.predictability":
|
|
541
|
-
return `Reduce navigation items from ${
|
|
771
|
+
return `Reduce navigation items from ${data.navItemCount} to ${F.navFree} or fewer -- group secondary items in submenus`;
|
|
542
772
|
case "sensory.animationControl":
|
|
543
|
-
return `Add prefers-reduced-motion support for ${
|
|
773
|
+
return `Add prefers-reduced-motion support for ${data.animationCount} animations and provide pause controls`;
|
|
544
774
|
case "sensory.sensoryIndependence":
|
|
545
775
|
return "Add captions/transcripts for all audio and video content";
|
|
546
776
|
case "sensory.visualCalm":
|
|
@@ -560,33 +790,27 @@ function generateFixRecommendation(category, component, score, data) {
|
|
|
560
790
|
case "navigation.formAccessibility":
|
|
561
791
|
return "Associate every form field with a visible label using <label for> or aria-labelledby";
|
|
562
792
|
default:
|
|
563
|
-
return `Improve ${component} in ${category} category (current score: ${score}
|
|
793
|
+
return `Improve ${component} in ${category} category (current score: ${score}/${F.scale})`;
|
|
564
794
|
}
|
|
565
795
|
}
|
|
566
796
|
// ============================================================================
|
|
567
797
|
// Top Issues
|
|
568
798
|
// ============================================================================
|
|
799
|
+
/** Every component of the five categories, in category order. */
|
|
800
|
+
function everyComponent(...categories) {
|
|
801
|
+
const names = ["motor", "visual", "cognitive", "sensory", "navigation"];
|
|
802
|
+
return categories.flatMap((cat, i) => Object.entries(cat.components).map(([component, val]) => ({
|
|
803
|
+
category: names[i], component, score: val.score, measured: val.measured,
|
|
804
|
+
})));
|
|
805
|
+
}
|
|
569
806
|
function buildTopIssues(motor, visual, cognitive, sensory, navigation, data) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
for (const [name, val] of Object.entries(visual.components)) {
|
|
575
|
-
allComponents.push({ category: "visual", component: name, score: val.score });
|
|
576
|
-
}
|
|
577
|
-
for (const [name, val] of Object.entries(cognitive.components)) {
|
|
578
|
-
allComponents.push({ category: "cognitive", component: name, score: val.score });
|
|
579
|
-
}
|
|
580
|
-
for (const [name, val] of Object.entries(sensory.components)) {
|
|
581
|
-
allComponents.push({ category: "sensory", component: name, score: val.score });
|
|
582
|
-
}
|
|
583
|
-
for (const [name, val] of Object.entries(navigation.components)) {
|
|
584
|
-
allComponents.push({ category: "navigation", component: name, score: val.score });
|
|
585
|
-
}
|
|
807
|
+
// A component that was not measured is not an issue found, so it is never listed
|
|
808
|
+
const allComponents = everyComponent(motor, visual, cognitive, sensory, navigation)
|
|
809
|
+
.filter((c) => c.measured);
|
|
586
810
|
// Sort by score ascending (worst first)
|
|
587
811
|
allComponents.sort((a, b) => a.score - b.score);
|
|
588
|
-
// Return top
|
|
589
|
-
return allComponents.slice(0,
|
|
812
|
+
// Return the top few
|
|
813
|
+
return allComponents.slice(0, param("cif.topIssues.count")).map((c) => ({
|
|
590
814
|
category: c.category,
|
|
591
815
|
component: c.component,
|
|
592
816
|
score: c.score,
|
|
@@ -600,33 +824,58 @@ function buildTopIssues(motor, visual, cognitive, sensory, navigation, data) {
|
|
|
600
824
|
* Compute the CIF (Cognitive Interface Fitness) score from raw tool data.
|
|
601
825
|
*
|
|
602
826
|
* Accepts data from empathy_audit, agent_ready_audit, cognitive_effort CTC,
|
|
603
|
-
* page_understand, and attention_analysis. All fields are optional
|
|
604
|
-
* missing
|
|
827
|
+
* page_understand, and attention_analysis. All fields are optional; a
|
|
828
|
+
* component whose inputs are all missing is reported as not measured (see
|
|
829
|
+
* MISSING INPUTS at the top of this file), never estimated.
|
|
605
830
|
*/
|
|
831
|
+
const COMPOSITE_IDS = {
|
|
832
|
+
motor: "cif.weight.motor",
|
|
833
|
+
visual: "cif.weight.visual",
|
|
834
|
+
cognitive: "cif.weight.cognitive",
|
|
835
|
+
sensory: "cif.weight.sensory",
|
|
836
|
+
navigation: "cif.weight.navigation",
|
|
837
|
+
};
|
|
606
838
|
export function computeCIFScore(data) {
|
|
839
|
+
const W = readParams(COMPOSITE_IDS);
|
|
607
840
|
const motor = computeMotorFitness(data);
|
|
608
841
|
const visual = computeVisualFitness(data);
|
|
609
842
|
const cognitive = computeCognitiveFitness(data);
|
|
610
843
|
const sensory = computeSensoryFitness(data);
|
|
611
844
|
const navigation = computeNavigationFitness(data);
|
|
612
|
-
const composite = clamp(Math.round(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
845
|
+
const composite = clamp(Math.round(W.motor * motor.score +
|
|
846
|
+
W.visual * visual.score +
|
|
847
|
+
W.cognitive * cognitive.score +
|
|
848
|
+
W.sensory * sensory.score +
|
|
849
|
+
W.navigation * navigation.score));
|
|
617
850
|
const grade = computeGrade(composite);
|
|
618
|
-
const certification = computeCertification(motor.score, visual.score, cognitive.score, sensory.score, navigation.score);
|
|
619
851
|
const topIssues = buildTopIssues(motor, visual, cognitive, sensory, navigation, data);
|
|
852
|
+
const components = everyComponent(motor, visual, cognitive, sensory, navigation);
|
|
853
|
+
const notMeasuredComponents = components
|
|
854
|
+
.filter((c) => !c.measured)
|
|
855
|
+
.map((c) => `${c.category}.${c.component}`);
|
|
856
|
+
// A certification speaks for the whole page, so it needs every component
|
|
857
|
+
// measured. The band is still computed from the category scores; it is only
|
|
858
|
+
// issued when nothing was left unmeasured.
|
|
859
|
+
const allMeasured = notMeasuredComponents.length === 0;
|
|
860
|
+
const certification = allMeasured
|
|
861
|
+
? computeCertification(motor.score, visual.score, cognitive.score, sensory.score, navigation.score)
|
|
862
|
+
: "none";
|
|
863
|
+
const certificationNote = allMeasured
|
|
864
|
+
? null
|
|
865
|
+
: `Not certified: ${notMeasuredComponents.length} of ${components.length} components not measured.`;
|
|
620
866
|
return {
|
|
621
867
|
composite,
|
|
622
868
|
grade,
|
|
623
869
|
certification,
|
|
870
|
+
notMeasuredCount: notMeasuredComponents.length,
|
|
871
|
+
certificationNote,
|
|
624
872
|
motor,
|
|
625
873
|
visual,
|
|
626
874
|
cognitive,
|
|
627
875
|
sensory,
|
|
628
876
|
navigation,
|
|
629
877
|
topIssues,
|
|
878
|
+
notMeasured: notMeasuredComponents,
|
|
630
879
|
url: data.url,
|
|
631
880
|
timestamp: new Date().toISOString(),
|
|
632
881
|
};
|