cbrowser 18.63.2 → 18.64.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/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +31 -16
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/cif-score.d.ts +141 -0
- package/dist/cif-score.d.ts.map +1 -0
- package/dist/cif-score.js +634 -0
- package/dist/cif-score.js.map +1 -0
- package/dist/ideal-profiles-gap.d.ts +92 -0
- package/dist/ideal-profiles-gap.d.ts.map +1 -0
- package/dist/ideal-profiles-gap.js +325 -0
- package/dist/ideal-profiles-gap.js.map +1 -0
- package/dist/ideal-profiles.d.ts +70 -0
- package/dist/ideal-profiles.d.ts.map +1 -0
- package/dist/ideal-profiles.js +404 -0
- package/dist/ideal-profiles.js.map +1 -0
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +40 -16
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -1
- package/dist/mcp-tools/persona-creation-tools.js +119 -15
- package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
- package/dist/visual/attention-transport.d.ts +24 -4
- package/dist/visual/attention-transport.d.ts.map +1 -1
- package/dist/visual/attention-transport.js +253 -9
- package/dist/visual/attention-transport.js.map +1 -1
- package/dist/visual/cognitive-transport-chain.d.ts.map +1 -1
- package/dist/visual/cognitive-transport-chain.js +6 -1
- package/dist/visual/cognitive-transport-chain.js.map +1 -1
- package/dist/visual/cognitive-transport.d.ts.map +1 -1
- package/dist/visual/cognitive-transport.js +18 -4
- package/dist/visual/cognitive-transport.js.map +1 -1
- package/dist/visual/perceptual-transport.d.ts +15 -3
- package/dist/visual/perceptual-transport.d.ts.map +1 -1
- package/dist/visual/perceptual-transport.js +92 -6
- package/dist/visual/perceptual-transport.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CIF -- Cognitive Interface Fitness
|
|
3
|
+
*
|
|
4
|
+
* 5-category scoring system measuring whether a website actually works
|
|
5
|
+
* for people with specific disabilities. Scores 0-100 per category.
|
|
6
|
+
*
|
|
7
|
+
* Categories:
|
|
8
|
+
* 1. Motor Fitness (20%) -- can they physically interact?
|
|
9
|
+
* 2. Visual Fitness (20%) -- can they see and distinguish?
|
|
10
|
+
* 3. Cognitive Fitness (25%) -- can they understand and decide?
|
|
11
|
+
* 4. Sensory Fitness (15%) -- can they process without overload?
|
|
12
|
+
* 5. Navigation Fitness (20%) -- can they get around?
|
|
13
|
+
*
|
|
14
|
+
* @copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com https://cbrowser.ai
|
|
15
|
+
* @license MIT
|
|
16
|
+
* @since v19.0.0
|
|
17
|
+
*/
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Utility functions
|
|
20
|
+
// ============================================================================
|
|
21
|
+
/** Clamp value between 0 and 100 */
|
|
22
|
+
function clamp(val) {
|
|
23
|
+
return Math.max(0, Math.min(100, val));
|
|
24
|
+
}
|
|
25
|
+
/** Sigmoid function centered at 0, scaled to [0, 1] */
|
|
26
|
+
function sigmoid(x) {
|
|
27
|
+
return 1 / (1 + Math.exp(-x));
|
|
28
|
+
}
|
|
29
|
+
/** Weighted average of component scores */
|
|
30
|
+
function weightedAvg(components) {
|
|
31
|
+
let totalWeight = 0;
|
|
32
|
+
let totalScore = 0;
|
|
33
|
+
for (const c of components) {
|
|
34
|
+
totalScore += c.score * c.weight;
|
|
35
|
+
totalWeight += c.weight;
|
|
36
|
+
}
|
|
37
|
+
return totalWeight > 0 ? totalScore / totalWeight : 50;
|
|
38
|
+
}
|
|
39
|
+
/** Safe number that defaults if input is null/undefined/NaN */
|
|
40
|
+
function safe(val, fallback) {
|
|
41
|
+
if (val == null || isNaN(val))
|
|
42
|
+
return fallback;
|
|
43
|
+
return val;
|
|
44
|
+
}
|
|
45
|
+
/** Count barriers of a given type */
|
|
46
|
+
function countBarriersByType(barriers, type) {
|
|
47
|
+
if (!barriers)
|
|
48
|
+
return 0;
|
|
49
|
+
return barriers.filter((b) => b.type === type).length;
|
|
50
|
+
}
|
|
51
|
+
/** Count barriers matching a substring in type or description */
|
|
52
|
+
function countBarriersMatching(barriers, pattern) {
|
|
53
|
+
if (!barriers)
|
|
54
|
+
return 0;
|
|
55
|
+
const p = pattern.toLowerCase();
|
|
56
|
+
return barriers.filter((b) => b.type.toLowerCase().includes(p) ||
|
|
57
|
+
(b.description || "").toLowerCase().includes(p)).length;
|
|
58
|
+
}
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Category Scorers
|
|
61
|
+
// ============================================================================
|
|
62
|
+
function computeMotorFitness(data) {
|
|
63
|
+
// --- touchTargetCompliance (30%) ---
|
|
64
|
+
let touchTargetCompliance;
|
|
65
|
+
if (data.touchTargets && data.touchTargets.length > 0) {
|
|
66
|
+
const nonExempt = data.touchTargets.filter((t) => !t.exempt);
|
|
67
|
+
if (nonExempt.length === 0) {
|
|
68
|
+
touchTargetCompliance = { score: 100, details: "All interactive elements are exempt from size requirements" };
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const compliant = nonExempt.filter((t) => Math.min(t.width, t.height) >= 44).length;
|
|
72
|
+
const pct = (compliant / nonExempt.length) * 100;
|
|
73
|
+
touchTargetCompliance = {
|
|
74
|
+
score: clamp(Math.round(pct)),
|
|
75
|
+
details: `${compliant}/${nonExempt.length} interactive elements meet 44px minimum (${Math.round(pct)}%)`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Fall back to barrier count as proxy
|
|
81
|
+
const touchBarriers = countBarriersByType(data.barriers, "touch_target");
|
|
82
|
+
touchTargetCompliance = {
|
|
83
|
+
score: clamp(Math.max(30, 100 - touchBarriers * 15)),
|
|
84
|
+
details: touchBarriers > 0
|
|
85
|
+
? `${touchBarriers} touch target barriers detected (no size data available)`
|
|
86
|
+
: "No touch target data available -- estimated from barriers",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// --- hitProbability (25%) ---
|
|
90
|
+
let hitProbability;
|
|
91
|
+
if (data.touchTargets && data.touchTargets.length > 0) {
|
|
92
|
+
const nonExempt = data.touchTargets.filter((t) => !t.exempt);
|
|
93
|
+
if (nonExempt.length === 0) {
|
|
94
|
+
hitProbability = { score: 100, details: "No non-exempt interactive elements" };
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const avgMinDim = nonExempt.reduce((sum, t) => sum + Math.min(t.width, t.height), 0) /
|
|
98
|
+
nonExempt.length;
|
|
99
|
+
const score = sigmoid((avgMinDim - 20) / 10) * 100;
|
|
100
|
+
hitProbability = {
|
|
101
|
+
score: clamp(Math.round(score)),
|
|
102
|
+
details: `Average minimum dimension: ${Math.round(avgMinDim)}px (sigmoid-scored)`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
hitProbability = {
|
|
108
|
+
score: 100,
|
|
109
|
+
details: "No touch target barriers detected",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// --- interactionCount (20%) ---
|
|
113
|
+
// Under 20 interactive elements is fine; penalize progressively above that
|
|
114
|
+
const interactiveCount = safe(data.interactiveCount, 15);
|
|
115
|
+
const interactionCountScore = interactiveCount <= 20 ? 100
|
|
116
|
+
: interactiveCount <= 40 ? clamp(Math.round(100 - (interactiveCount - 20) * 2.5))
|
|
117
|
+
: clamp(Math.round(50 - (interactiveCount - 40) * 1));
|
|
118
|
+
const interactionCount = {
|
|
119
|
+
score: interactionCountScore,
|
|
120
|
+
details: interactiveCount <= 20
|
|
121
|
+
? `${interactiveCount} interactive elements — manageable for motor impairments`
|
|
122
|
+
: `${interactiveCount} interactive elements on page (fewer is easier for motor impairments)`,
|
|
123
|
+
};
|
|
124
|
+
// --- spacingAdequacy (15%) ---
|
|
125
|
+
const touchTargetBarriers = countBarriersByType(data.barriers, "touch_target");
|
|
126
|
+
const spacingScore = clamp(Math.round(100 - touchTargetBarriers * 15));
|
|
127
|
+
const spacingAdequacy = {
|
|
128
|
+
score: spacingScore,
|
|
129
|
+
details: touchTargetBarriers > 0
|
|
130
|
+
? `${touchTargetBarriers} touch target/spacing barriers detected`
|
|
131
|
+
: "No spacing barriers detected",
|
|
132
|
+
};
|
|
133
|
+
// --- dragFreeAlternatives (10%) ---
|
|
134
|
+
const dragBarriers = countBarriersMatching(data.barriers, "drag");
|
|
135
|
+
const dragScore = clamp(100 - dragBarriers * 25);
|
|
136
|
+
const dragFreeAlternatives = {
|
|
137
|
+
score: dragScore,
|
|
138
|
+
details: dragBarriers > 0
|
|
139
|
+
? `${dragBarriers} drag-dependent interactions found without alternatives`
|
|
140
|
+
: "No drag-dependent interactions detected",
|
|
141
|
+
};
|
|
142
|
+
const components = {
|
|
143
|
+
touchTargetCompliance,
|
|
144
|
+
hitProbability,
|
|
145
|
+
interactionCount,
|
|
146
|
+
spacingAdequacy,
|
|
147
|
+
dragFreeAlternatives,
|
|
148
|
+
};
|
|
149
|
+
const score = clamp(Math.round(weightedAvg([
|
|
150
|
+
{ score: touchTargetCompliance.score, weight: 0.30 },
|
|
151
|
+
{ score: hitProbability.score, weight: 0.25 },
|
|
152
|
+
{ score: interactionCount.score, weight: 0.20 },
|
|
153
|
+
{ score: spacingAdequacy.score, weight: 0.15 },
|
|
154
|
+
{ score: dragFreeAlternatives.score, weight: 0.10 },
|
|
155
|
+
])));
|
|
156
|
+
return { score, components };
|
|
157
|
+
}
|
|
158
|
+
function computeVisualFitness(data) {
|
|
159
|
+
// --- contrastCompliance (30%) ---
|
|
160
|
+
let contrastCompliance;
|
|
161
|
+
if (data.contrastIssues && data.contrastIssues.length > 0) {
|
|
162
|
+
// Use element count as proxy for total text elements
|
|
163
|
+
const totalTextEstimate = Math.max(safe(data.elementCount, 50), data.contrastIssues.length);
|
|
164
|
+
const failRatio = data.contrastIssues.length / totalTextEstimate;
|
|
165
|
+
const score = clamp(Math.round(100 - failRatio * 100));
|
|
166
|
+
const failingBelow45 = data.contrastIssues.filter((c) => !c.isLargeText && c.ratio < 4.5).length;
|
|
167
|
+
const failingBelow3 = data.contrastIssues.filter((c) => c.isLargeText && c.ratio < 3).length;
|
|
168
|
+
contrastCompliance = {
|
|
169
|
+
score,
|
|
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
|
+
};
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Use agent_ready accessibility subscore as proxy
|
|
184
|
+
const accessibilityProxy = safe(data.agentReadyScore, 60);
|
|
185
|
+
contrastCompliance = {
|
|
186
|
+
score: clamp(Math.round(accessibilityProxy * 0.8 + 20)),
|
|
187
|
+
details: "No contrast data -- estimated from agent-ready accessibility score",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// --- textScalability (20%) ---
|
|
192
|
+
// Assume text scales correctly unless density is very high (which breaks at zoom)
|
|
193
|
+
const density = safe(data.informationDensity, 0.5);
|
|
194
|
+
const scalabilityDeduction = density > 0.6 ? (density - 0.6) * 80 : 0;
|
|
195
|
+
const textScalability = {
|
|
196
|
+
score: clamp(Math.round(100 - scalabilityDeduction)),
|
|
197
|
+
details: scalabilityDeduction > 0
|
|
198
|
+
? `High density (${(density * 100).toFixed(0)}%) may cause overlap at 200% zoom (-${Math.round(scalabilityDeduction)})`
|
|
199
|
+
: "Text content can scale to 200% without loss of functionality",
|
|
200
|
+
};
|
|
201
|
+
// --- colorIndependence (20%) ---
|
|
202
|
+
const colorOnlyBarriers = countBarriersMatching(data.barriers, "color");
|
|
203
|
+
const colorScore = clamp(100 - colorOnlyBarriers * 20);
|
|
204
|
+
const colorIndependence = {
|
|
205
|
+
score: colorScore,
|
|
206
|
+
details: colorOnlyBarriers > 0
|
|
207
|
+
? `${colorOnlyBarriers} color-only information barriers detected`
|
|
208
|
+
: "No color-only information barriers detected",
|
|
209
|
+
};
|
|
210
|
+
// --- focusVisibility (15%) ---
|
|
211
|
+
const semanticProxy = safe(data.semanticScore, safe(data.agentReadyScore, 50));
|
|
212
|
+
const focusScore = clamp(Math.round((semanticProxy / 100) * 100));
|
|
213
|
+
const focusVisibility = {
|
|
214
|
+
score: focusScore,
|
|
215
|
+
details: `Estimated from semantic score (${Math.round(semanticProxy)}/100)`,
|
|
216
|
+
};
|
|
217
|
+
// --- spacingDensity (15%) ---
|
|
218
|
+
const spacingDensityScore = clamp(Math.round(100 - density * 100));
|
|
219
|
+
const spacingDensity = {
|
|
220
|
+
score: spacingDensityScore,
|
|
221
|
+
details: `Information density: ${(density * 100).toFixed(0)}% (lower is better for visual processing)`,
|
|
222
|
+
};
|
|
223
|
+
const components = {
|
|
224
|
+
contrastCompliance,
|
|
225
|
+
textScalability,
|
|
226
|
+
colorIndependence,
|
|
227
|
+
focusVisibility,
|
|
228
|
+
spacingDensity,
|
|
229
|
+
};
|
|
230
|
+
const score = clamp(Math.round(weightedAvg([
|
|
231
|
+
{ score: contrastCompliance.score, weight: 0.30 },
|
|
232
|
+
{ score: textScalability.score, weight: 0.20 },
|
|
233
|
+
{ score: colorIndependence.score, weight: 0.20 },
|
|
234
|
+
{ score: focusVisibility.score, weight: 0.15 },
|
|
235
|
+
{ score: spacingDensity.score, weight: 0.15 },
|
|
236
|
+
])));
|
|
237
|
+
return { score, components };
|
|
238
|
+
}
|
|
239
|
+
function computeCognitiveFitness(data) {
|
|
240
|
+
// --- decisionComplexity (25%) ---
|
|
241
|
+
let decisionCost = 0;
|
|
242
|
+
if (data.layers && data.layers.length > 0) {
|
|
243
|
+
const decisionLayer = data.layers.find((l) => l.name.toLowerCase().includes("decision") || l.name.toLowerCase().includes("choice"));
|
|
244
|
+
decisionCost = decisionLayer ? decisionLayer.cost : 0;
|
|
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
|
+
};
|
|
253
|
+
// --- readability (20%) ---
|
|
254
|
+
const flesch = data.fleschScore;
|
|
255
|
+
const readabilityScore = flesch != null
|
|
256
|
+
? clamp(Math.round(Math.min(100, flesch * 1.2)))
|
|
257
|
+
: 85; // No data = assume reasonable readability
|
|
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 85",
|
|
263
|
+
};
|
|
264
|
+
// --- informationDensity (20%) ---
|
|
265
|
+
// A well-structured page can have many elements without being overwhelming.
|
|
266
|
+
// Only penalize when element count is extremely high relative to viewport.
|
|
267
|
+
// Threshold: 200 elements is fine, 500+ starts getting dense, 2000+ is overwhelming.
|
|
268
|
+
const elemCount = safe(data.elementCount, 100);
|
|
269
|
+
const infoScore = elemCount <= 200 ? 100
|
|
270
|
+
: elemCount <= 500 ? clamp(Math.round(100 - (elemCount - 200) / 6))
|
|
271
|
+
: clamp(Math.round(50 - (elemCount - 500) / 30));
|
|
272
|
+
const informationDensity = {
|
|
273
|
+
score: infoScore,
|
|
274
|
+
details: elemCount <= 200
|
|
275
|
+
? `${elemCount} elements — well within cognitive limits`
|
|
276
|
+
: `${elemCount} elements on page (progressive penalty above 200)`,
|
|
277
|
+
};
|
|
278
|
+
// --- attentionClarity (20%) ---
|
|
279
|
+
const captureRate = data.ctaCaptureRate;
|
|
280
|
+
const attentionScore = captureRate != null
|
|
281
|
+
? clamp(Math.round(Math.min(100, captureRate * 200)))
|
|
282
|
+
: 80; // No data = assume reasonable CTA clarity
|
|
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 80",
|
|
288
|
+
};
|
|
289
|
+
// --- predictability (15%) ---
|
|
290
|
+
const navItems = safe(data.navItemCount, 5);
|
|
291
|
+
let predictScore;
|
|
292
|
+
if (navItems <= 7) {
|
|
293
|
+
predictScore = 100;
|
|
294
|
+
}
|
|
295
|
+
else if (navItems <= 12) {
|
|
296
|
+
predictScore = clamp(Math.round(100 - (navItems - 7) * 5));
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
predictScore = clamp(Math.round(75 - (navItems - 12) * 5));
|
|
300
|
+
}
|
|
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
|
+
const components = {
|
|
308
|
+
decisionComplexity,
|
|
309
|
+
readability,
|
|
310
|
+
informationDensity,
|
|
311
|
+
attentionClarity,
|
|
312
|
+
predictability,
|
|
313
|
+
};
|
|
314
|
+
const score = clamp(Math.round(weightedAvg([
|
|
315
|
+
{ score: decisionComplexity.score, weight: 0.25 },
|
|
316
|
+
{ score: readability.score, weight: 0.20 },
|
|
317
|
+
{ score: informationDensity.score, weight: 0.20 },
|
|
318
|
+
{ score: attentionClarity.score, weight: 0.20 },
|
|
319
|
+
{ score: predictability.score, weight: 0.15 },
|
|
320
|
+
])));
|
|
321
|
+
return { score, components };
|
|
322
|
+
}
|
|
323
|
+
function computeSensoryFitness(data) {
|
|
324
|
+
// --- animationControl (30%) ---
|
|
325
|
+
const animCount = safe(data.animationCount, 0);
|
|
326
|
+
const hasMotionSupport = !!data.hasReducedMotionSupport;
|
|
327
|
+
// If prefers-reduced-motion is supported, animations are user-controllable → score 95 (not 100, small tax for having them at all)
|
|
328
|
+
// Without support, each animation is a hard penalty
|
|
329
|
+
const animScore = hasMotionSupport
|
|
330
|
+
? clamp(Math.max(90, 100 - animCount)) // 90-100 range — minor tax for quantity
|
|
331
|
+
: animCount === 0 ? 100 : clamp(Math.round(100 - animCount * 20));
|
|
332
|
+
const animationControl = {
|
|
333
|
+
score: animScore,
|
|
334
|
+
details: animCount === 0
|
|
335
|
+
? "No animations detected"
|
|
336
|
+
: hasMotionSupport
|
|
337
|
+
? `${animCount} animations detected — prefers-reduced-motion supported (user can disable)`
|
|
338
|
+
: `${animCount} animations detected without motion preference support (each deducts 20 points)`,
|
|
339
|
+
};
|
|
340
|
+
// --- sensoryIndependence (25%) ---
|
|
341
|
+
const captionBarriers = countBarriersMatching(data.barriers, "caption");
|
|
342
|
+
const audioBarriers = countBarriersMatching(data.barriers, "audio");
|
|
343
|
+
const sensoryDeductions = (captionBarriers + audioBarriers) * 15;
|
|
344
|
+
const sensoryScore = clamp(100 - sensoryDeductions);
|
|
345
|
+
const sensoryIndependence = {
|
|
346
|
+
score: sensoryScore,
|
|
347
|
+
details: sensoryDeductions > 0
|
|
348
|
+
? `${captionBarriers + audioBarriers} audio/caption barriers (each deducts 15)`
|
|
349
|
+
: "No audio/caption accessibility barriers detected",
|
|
350
|
+
};
|
|
351
|
+
// --- visualCalm (20%) ---
|
|
352
|
+
const density = safe(data.informationDensity, 0.5);
|
|
353
|
+
// If animations are user-controllable, they don't contribute to visual noise
|
|
354
|
+
const animCalmPenalty = hasMotionSupport ? 0 : animCount * 15;
|
|
355
|
+
const calmScore = clamp(Math.round(100 - density * 50 - animCalmPenalty));
|
|
356
|
+
const visualCalm = {
|
|
357
|
+
score: calmScore,
|
|
358
|
+
details: hasMotionSupport
|
|
359
|
+
? `Information density: ${(density * 100).toFixed(0)}% (animations controllable via prefers-reduced-motion)`
|
|
360
|
+
: `Information density: ${(density * 100).toFixed(0)}%, ${animCount} uncontrolled animations`,
|
|
361
|
+
};
|
|
362
|
+
// --- layoutConsistency (15%) ---
|
|
363
|
+
const agentReady = safe(data.agentReadyScore, 70);
|
|
364
|
+
const consistencyScore = clamp(Math.round(agentReady));
|
|
365
|
+
const layoutConsistency = {
|
|
366
|
+
score: consistencyScore,
|
|
367
|
+
details: `Layout consistency derived from structural score (${Math.round(agentReady)}/100)`,
|
|
368
|
+
};
|
|
369
|
+
// --- overloadThreshold (10%) ---
|
|
370
|
+
// CTC ≤ 0.3 is "easy" per our scale, 0.3-0.6 moderate, 0.6-1.0 hard, >1.0 very hard
|
|
371
|
+
const totalCTC = safe(data.totalCTC, 0.3);
|
|
372
|
+
const overloadScore = totalCTC <= 0.3 ? 100
|
|
373
|
+
: totalCTC <= 0.6 ? clamp(Math.round(100 - (totalCTC - 0.3) * 130))
|
|
374
|
+
: clamp(Math.round(60 - (totalCTC - 0.6) * 100));
|
|
375
|
+
const overloadThreshold = {
|
|
376
|
+
score: overloadScore,
|
|
377
|
+
details: totalCTC <= 0.3 ? `Total CTC: ${totalCTC.toFixed(3)} — within easy range`
|
|
378
|
+
: `Total CTC: ${totalCTC.toFixed(3)} (lower = less cognitive overload risk)`,
|
|
379
|
+
};
|
|
380
|
+
const components = {
|
|
381
|
+
animationControl,
|
|
382
|
+
sensoryIndependence,
|
|
383
|
+
visualCalm,
|
|
384
|
+
layoutConsistency,
|
|
385
|
+
overloadThreshold,
|
|
386
|
+
};
|
|
387
|
+
const score = clamp(Math.round(weightedAvg([
|
|
388
|
+
{ score: animationControl.score, weight: 0.30 },
|
|
389
|
+
{ score: sensoryIndependence.score, weight: 0.25 },
|
|
390
|
+
{ score: visualCalm.score, weight: 0.20 },
|
|
391
|
+
{ score: layoutConsistency.score, weight: 0.15 },
|
|
392
|
+
{ score: overloadThreshold.score, weight: 0.10 },
|
|
393
|
+
])));
|
|
394
|
+
return { score, components };
|
|
395
|
+
}
|
|
396
|
+
function computeNavigationFitness(data) {
|
|
397
|
+
// --- semanticStructure (25%) ---
|
|
398
|
+
const subScores = [];
|
|
399
|
+
if (data.semanticScore != null)
|
|
400
|
+
subScores.push(data.semanticScore);
|
|
401
|
+
if (data.headingScore != null)
|
|
402
|
+
subScores.push(data.headingScore);
|
|
403
|
+
if (data.landmarkScore != null)
|
|
404
|
+
subScores.push(data.landmarkScore);
|
|
405
|
+
const semanticAvg = subScores.length > 0
|
|
406
|
+
? subScores.reduce((a, b) => a + b, 0) / subScores.length
|
|
407
|
+
: safe(data.agentReadyScore, 50);
|
|
408
|
+
const semanticStructure = {
|
|
409
|
+
score: clamp(Math.round(semanticAvg)),
|
|
410
|
+
details: subScores.length > 0
|
|
411
|
+
? `Average of ${subScores.length} semantic sub-scores: ${Math.round(semanticAvg)}/100`
|
|
412
|
+
: `Estimated from agent-ready score: ${Math.round(semanticAvg)}/100`,
|
|
413
|
+
};
|
|
414
|
+
// --- keyboardOperability (25%) ---
|
|
415
|
+
const keyboardBarriers = countBarriersMatching(data.barriers, "keyboard");
|
|
416
|
+
const motorPrecision = countBarriersByType(data.barriers, "motor_precision");
|
|
417
|
+
const kbDeductions = (keyboardBarriers + motorPrecision) * 15;
|
|
418
|
+
const kbScore = clamp(100 - kbDeductions);
|
|
419
|
+
const keyboardOperability = {
|
|
420
|
+
score: kbScore,
|
|
421
|
+
details: kbDeductions > 0
|
|
422
|
+
? `${keyboardBarriers + motorPrecision} keyboard/motor barriers detected`
|
|
423
|
+
: "No keyboard accessibility barriers detected",
|
|
424
|
+
};
|
|
425
|
+
// --- skipMechanisms (15%) ---
|
|
426
|
+
const skipBarriers = countBarriersMatching(data.barriers, "skip");
|
|
427
|
+
const skipScore = clamp(100 - skipBarriers * 25);
|
|
428
|
+
const skipMechanisms = {
|
|
429
|
+
score: skipScore,
|
|
430
|
+
details: skipBarriers > 0
|
|
431
|
+
? `${skipBarriers} skip mechanism issues detected`
|
|
432
|
+
: "No skip navigation issues detected",
|
|
433
|
+
};
|
|
434
|
+
// --- linkButtonClarity (15%) ---
|
|
435
|
+
const ariaProxy = safe(data.ariaScore, 50);
|
|
436
|
+
const linkButtonClarity = {
|
|
437
|
+
score: clamp(Math.round(ariaProxy)),
|
|
438
|
+
details: `ARIA label score: ${Math.round(ariaProxy)}/100`,
|
|
439
|
+
};
|
|
440
|
+
// --- formAccessibility (20%) ---
|
|
441
|
+
let formScore = safe(data.formLabelScore, 60);
|
|
442
|
+
const formBarriers = countBarriersMatching(data.barriers, "form");
|
|
443
|
+
formScore = clamp(formScore - formBarriers * 10);
|
|
444
|
+
const formAccessibility = {
|
|
445
|
+
score: clamp(Math.round(formScore)),
|
|
446
|
+
details: formBarriers > 0
|
|
447
|
+
? `Form label score: ${Math.round(safe(data.formLabelScore, 60))}/100, ${formBarriers} form barriers`
|
|
448
|
+
: `Form label score: ${Math.round(safe(data.formLabelScore, 60))}/100`,
|
|
449
|
+
};
|
|
450
|
+
const components = {
|
|
451
|
+
semanticStructure,
|
|
452
|
+
keyboardOperability,
|
|
453
|
+
skipMechanisms,
|
|
454
|
+
linkButtonClarity,
|
|
455
|
+
formAccessibility,
|
|
456
|
+
};
|
|
457
|
+
const score = clamp(Math.round(weightedAvg([
|
|
458
|
+
{ score: semanticStructure.score, weight: 0.25 },
|
|
459
|
+
{ score: keyboardOperability.score, weight: 0.25 },
|
|
460
|
+
{ score: skipMechanisms.score, weight: 0.15 },
|
|
461
|
+
{ score: linkButtonClarity.score, weight: 0.15 },
|
|
462
|
+
{ score: formAccessibility.score, weight: 0.20 },
|
|
463
|
+
])));
|
|
464
|
+
return { score, components };
|
|
465
|
+
}
|
|
466
|
+
// ============================================================================
|
|
467
|
+
// Grading & Certification
|
|
468
|
+
// ============================================================================
|
|
469
|
+
function computeGrade(composite) {
|
|
470
|
+
if (composite >= 95)
|
|
471
|
+
return "A+";
|
|
472
|
+
if (composite >= 85)
|
|
473
|
+
return "A";
|
|
474
|
+
if (composite >= 70)
|
|
475
|
+
return "B";
|
|
476
|
+
if (composite >= 50)
|
|
477
|
+
return "C";
|
|
478
|
+
if (composite >= 30)
|
|
479
|
+
return "D";
|
|
480
|
+
return "F";
|
|
481
|
+
}
|
|
482
|
+
function computeCertification(motor, visual, cognitive, sensory, navigation) {
|
|
483
|
+
const all = [motor, visual, cognitive, sensory, navigation];
|
|
484
|
+
const minScore = Math.min(...all);
|
|
485
|
+
if (minScore >= 95)
|
|
486
|
+
return "platinum";
|
|
487
|
+
if (minScore >= 85)
|
|
488
|
+
return "gold";
|
|
489
|
+
if (minScore >= 70)
|
|
490
|
+
return "silver";
|
|
491
|
+
if (minScore >= 50)
|
|
492
|
+
return "bronze";
|
|
493
|
+
return "none";
|
|
494
|
+
}
|
|
495
|
+
// ============================================================================
|
|
496
|
+
// Fix Recommendations
|
|
497
|
+
// ============================================================================
|
|
498
|
+
function generateFixRecommendation(category, component, score, data) {
|
|
499
|
+
const key = `${category}.${component}`;
|
|
500
|
+
switch (key) {
|
|
501
|
+
case "motor.touchTargetCompliance": {
|
|
502
|
+
const count = data.touchTargets
|
|
503
|
+
? data.touchTargets.filter((t) => !t.exempt && Math.min(t.width, t.height) < 44).length
|
|
504
|
+
: countBarriersByType(data.barriers, "touch_target");
|
|
505
|
+
return `Increase ${count || "undersized"} interactive element sizes to at least 44x44px (WCAG 2.5.5 AAA)`;
|
|
506
|
+
}
|
|
507
|
+
case "motor.hitProbability":
|
|
508
|
+
return "Increase average interactive element dimensions -- target 48px minimum for reliable motor access";
|
|
509
|
+
case "motor.interactionCount":
|
|
510
|
+
return `Reduce interactive elements from ${safe(data.interactiveCount, 0)} to under 30 on a single view`;
|
|
511
|
+
case "motor.spacingAdequacy":
|
|
512
|
+
return "Add at least 8px spacing between adjacent interactive elements to prevent mis-taps";
|
|
513
|
+
case "motor.dragFreeAlternatives":
|
|
514
|
+
return "Provide keyboard/click alternatives for all drag-and-drop interactions";
|
|
515
|
+
case "visual.contrastCompliance": {
|
|
516
|
+
const count = data.contrastIssues?.length || countBarriersByType(data.barriers, "contrast");
|
|
517
|
+
return `Fix ${count} elements with contrast ratios below 4.5:1 for normal text or 3:1 for large text`;
|
|
518
|
+
}
|
|
519
|
+
case "visual.textScalability":
|
|
520
|
+
return "Ensure text scales to 200% without loss of content or functionality (WCAG 1.4.4)";
|
|
521
|
+
case "visual.colorIndependence":
|
|
522
|
+
return "Add non-color indicators (icons, patterns, text labels) wherever color conveys meaning";
|
|
523
|
+
case "visual.focusVisibility":
|
|
524
|
+
return "Ensure all interactive elements have visible focus indicators with at least 3:1 contrast";
|
|
525
|
+
case "visual.spacingDensity":
|
|
526
|
+
return "Increase whitespace between content blocks -- reduce information density below 60%";
|
|
527
|
+
case "cognitive.decisionComplexity": {
|
|
528
|
+
const choices = safe(data.interactiveCount, 0);
|
|
529
|
+
if (choices > 10) {
|
|
530
|
+
return `Reduce choices from ${choices} to 5-7 options per decision point (Hick's Law)`;
|
|
531
|
+
}
|
|
532
|
+
return "Simplify decision points -- group related options, provide defaults, reduce choices per step";
|
|
533
|
+
}
|
|
534
|
+
case "cognitive.readability":
|
|
535
|
+
return `Simplify text to 8th-grade reading level (current Flesch: ${Math.round(safe(data.fleschScore, 0))})`;
|
|
536
|
+
case "cognitive.informationDensity":
|
|
537
|
+
return `Reduce visible elements from ${safe(data.elementCount, 0)} -- use progressive disclosure to reveal content incrementally`;
|
|
538
|
+
case "cognitive.attentionClarity":
|
|
539
|
+
return "Make primary CTA visually dominant -- increase size, contrast, and whitespace around conversion elements";
|
|
540
|
+
case "cognitive.predictability":
|
|
541
|
+
return `Reduce navigation items from ${safe(data.navItemCount, 0)} to 7 or fewer -- group secondary items in submenus`;
|
|
542
|
+
case "sensory.animationControl":
|
|
543
|
+
return `Add prefers-reduced-motion support for ${safe(data.animationCount, 0)} animations and provide pause controls`;
|
|
544
|
+
case "sensory.sensoryIndependence":
|
|
545
|
+
return "Add captions/transcripts for all audio and video content";
|
|
546
|
+
case "sensory.visualCalm":
|
|
547
|
+
return "Reduce visual noise -- fewer animations, lower information density, more whitespace";
|
|
548
|
+
case "sensory.layoutConsistency":
|
|
549
|
+
return "Standardize layout patterns -- consistent nav placement, predictable content regions";
|
|
550
|
+
case "sensory.overloadThreshold":
|
|
551
|
+
return "Reduce total cognitive load -- simplify page structure, reduce simultaneous information streams";
|
|
552
|
+
case "navigation.semanticStructure":
|
|
553
|
+
return "Add proper heading hierarchy (h1-h6), landmark roles, and semantic HTML elements";
|
|
554
|
+
case "navigation.keyboardOperability":
|
|
555
|
+
return "Ensure all interactive elements are keyboard-accessible with logical tab order";
|
|
556
|
+
case "navigation.skipMechanisms":
|
|
557
|
+
return "Add skip-to-content links at the top of the page for keyboard/screen reader users";
|
|
558
|
+
case "navigation.linkButtonClarity":
|
|
559
|
+
return "Add descriptive ARIA labels to all links and buttons -- avoid 'click here' or icon-only controls";
|
|
560
|
+
case "navigation.formAccessibility":
|
|
561
|
+
return "Associate every form field with a visible label using <label for> or aria-labelledby";
|
|
562
|
+
default:
|
|
563
|
+
return `Improve ${component} in ${category} category (current score: ${score}/100)`;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// ============================================================================
|
|
567
|
+
// Top Issues
|
|
568
|
+
// ============================================================================
|
|
569
|
+
function buildTopIssues(motor, visual, cognitive, sensory, navigation, data) {
|
|
570
|
+
const allComponents = [];
|
|
571
|
+
for (const [name, val] of Object.entries(motor.components)) {
|
|
572
|
+
allComponents.push({ category: "motor", component: name, score: val.score });
|
|
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
|
+
}
|
|
586
|
+
// Sort by score ascending (worst first)
|
|
587
|
+
allComponents.sort((a, b) => a.score - b.score);
|
|
588
|
+
// Return top 5
|
|
589
|
+
return allComponents.slice(0, 5).map((c) => ({
|
|
590
|
+
category: c.category,
|
|
591
|
+
component: c.component,
|
|
592
|
+
score: c.score,
|
|
593
|
+
fix: generateFixRecommendation(c.category, c.component, c.score, data),
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
596
|
+
// ============================================================================
|
|
597
|
+
// Main Entry Point
|
|
598
|
+
// ============================================================================
|
|
599
|
+
/**
|
|
600
|
+
* Compute the CIF (Cognitive Interface Fitness) score from raw tool data.
|
|
601
|
+
*
|
|
602
|
+
* Accepts data from empathy_audit, agent_ready_audit, cognitive_effort CTC,
|
|
603
|
+
* page_understand, and attention_analysis. All fields are optional --
|
|
604
|
+
* missing data produces conservative estimates with lower confidence.
|
|
605
|
+
*/
|
|
606
|
+
export function computeCIFScore(data) {
|
|
607
|
+
const motor = computeMotorFitness(data);
|
|
608
|
+
const visual = computeVisualFitness(data);
|
|
609
|
+
const cognitive = computeCognitiveFitness(data);
|
|
610
|
+
const sensory = computeSensoryFitness(data);
|
|
611
|
+
const navigation = computeNavigationFitness(data);
|
|
612
|
+
const composite = clamp(Math.round(0.20 * motor.score +
|
|
613
|
+
0.20 * visual.score +
|
|
614
|
+
0.25 * cognitive.score +
|
|
615
|
+
0.15 * sensory.score +
|
|
616
|
+
0.20 * navigation.score));
|
|
617
|
+
const grade = computeGrade(composite);
|
|
618
|
+
const certification = computeCertification(motor.score, visual.score, cognitive.score, sensory.score, navigation.score);
|
|
619
|
+
const topIssues = buildTopIssues(motor, visual, cognitive, sensory, navigation, data);
|
|
620
|
+
return {
|
|
621
|
+
composite,
|
|
622
|
+
grade,
|
|
623
|
+
certification,
|
|
624
|
+
motor,
|
|
625
|
+
visual,
|
|
626
|
+
cognitive,
|
|
627
|
+
sensory,
|
|
628
|
+
navigation,
|
|
629
|
+
topIssues,
|
|
630
|
+
url: data.url,
|
|
631
|
+
timestamp: new Date().toISOString(),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
//# sourceMappingURL=cif-score.js.map
|