@open-agent-toolkit/cli 0.2.20 → 0.2.21
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/assets/docs/workflows/projects/artifacts.md +1 -1
- package/assets/docs/workflows/skills/explainer-kit.md +188 -23
- package/assets/docs/workflows/skills/index.md +1 -1
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +87 -25
- package/assets/skills/explainer-kit/briefs/deep-dive.md +35 -0
- package/assets/skills/explainer-kit/briefs/engineer-tour.md +45 -0
- package/assets/skills/explainer-kit/briefs/program-recap.md +41 -0
- package/assets/skills/explainer-kit/briefs/project-explainer.md +38 -0
- package/assets/skills/explainer-kit/briefs/project-page.md +38 -0
- package/assets/skills/explainer-kit/briefs/project-recap.md +47 -0
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +28 -0
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +35 -0
- package/assets/skills/explainer-kit/examples/project-recap/content.md +57 -14
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +104 -0
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +26 -4
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +27 -10
- package/assets/skills/explainer-kit/recipes/program-recap.json +35 -11
- package/assets/skills/explainer-kit/recipes/project-explainer.json +27 -10
- package/assets/skills/explainer-kit/recipes/project-recap.json +43 -11
- package/assets/skills/explainer-kit/references/contracts.md +45 -19
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +41 -0
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +52 -0
- package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +442 -0
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +223 -10
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +28 -43
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +237 -0
- package/assets/skills/explainer-kit/scripts/lib/html-safety.mjs +680 -0
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +414 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +309 -10
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +313 -41
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +61 -0
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +166 -12
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +147 -2
- package/assets/skills/explainer-kit/scripts/run.mjs +796 -272
- package/assets/skills/explainer-kit/templates/deck-shell.html +25 -5
- package/assets/skills/explainer-kit/templates/diagram-shell.html +29 -7
- package/assets/skills/explainer-kit/templates/engineer-tour.html +133 -9
- package/assets/skills/explainer-kit/templates/house-style.html +82 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +16 -9
- package/assets/skills/oat-explainer-kit/references/author-callback.md +51 -0
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +10 -8
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +14 -0
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +7 -11
- package/assets/skills/oat-project-complete/SKILL.md +18 -2
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +7 -1
- package/assets/skills/oat-wave-execute/SKILL.md +12 -19
- package/assets/skills/oat-wave-program/SKILL.md +12 -13
- package/package.json +2 -2
- package/assets/skills/explainer-kit/schemas/author-request.schema.json +0 -85
- package/assets/skills/explainer-kit/schemas/author-result.schema.json +0 -65
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { findUnpinnedResourceRefs } from './html-safety.mjs';
|
|
2
|
+
import { recipeFloor, recipeRequiredNarrative } from './recipes.mjs';
|
|
3
|
+
|
|
1
4
|
const VOID_ELEMENTS = new Set([
|
|
2
5
|
'area',
|
|
3
6
|
'base',
|
|
@@ -14,14 +17,148 @@ const VOID_ELEMENTS = new Set([
|
|
|
14
17
|
'track',
|
|
15
18
|
'wbr',
|
|
16
19
|
]);
|
|
17
|
-
const EXTERNAL_ASSET_PATTERN =
|
|
18
|
-
/<(?:script|iframe|source|img|audio|video|object|embed)\b[^>]*(?:src|data)\s*=\s*["'](?!data:)[^"']+/i;
|
|
19
20
|
const INLINE_ASSET_VIOLATION_PATTERN =
|
|
20
21
|
/<link\b|@import\b|url\(\s*["']?(?!data:|#)/i;
|
|
21
22
|
const TOKEN_PATTERN = /{{\s*[A-Z][A-Z0-9_]*\s*}}/g;
|
|
22
23
|
const ARROW_KEYS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
|
24
|
+
const INLINE_DIAGRAM_PATTERN =
|
|
25
|
+
/<svg\b(?=[^>]*(?:\bclass\s*=\s*["'][^"']*\bdiagram\b|\baria-label\s*=\s*["'][^"']*(?:architecture|diagram)))[^>]*>/i;
|
|
26
|
+
const STRUCTURED_BLOCK_PATTERNS = [
|
|
27
|
+
/<table\b/i,
|
|
28
|
+
/<(?:ul|ol)\b/i,
|
|
29
|
+
/<aside\b[^>]*\bclass\s*=\s*["'][^"']*\bcallout\b/i,
|
|
30
|
+
/<blockquote\b/i,
|
|
31
|
+
/<figure\b/i,
|
|
32
|
+
];
|
|
23
33
|
|
|
24
34
|
export const REPRESENTATIVE_WIDTHS = Object.freeze([320, 768, 1440]);
|
|
35
|
+
export const GUIDELINE_WARNING_IDS = Object.freeze({
|
|
36
|
+
narrativeCoverage: 'guideline-narrative-coverage-missing',
|
|
37
|
+
architectureDiagram: 'guideline-architecture-diagram-missing',
|
|
38
|
+
structuredDepth: 'guideline-structured-depth-missing',
|
|
39
|
+
expansionProfileLimit: 'expansion-profile-limit-exceeded',
|
|
40
|
+
expansionArtifactLimit: 'expansion-artifact-limit-exceeded',
|
|
41
|
+
expansionTypeLimit: 'expansion-type-limit-exceeded',
|
|
42
|
+
});
|
|
43
|
+
const EXPANSION_WARNING_BY_REJECTION_REASON = new Map([
|
|
44
|
+
['profile-limit', GUIDELINE_WARNING_IDS.expansionProfileLimit],
|
|
45
|
+
['recipe-limit', GUIDELINE_WARNING_IDS.expansionArtifactLimit],
|
|
46
|
+
['type-limit', GUIDELINE_WARNING_IDS.expansionTypeLimit],
|
|
47
|
+
]);
|
|
48
|
+
export const RENDER_WARNING_IDS = Object.freeze({
|
|
49
|
+
unsupportedDiagram: 'render-unsupported-diagram',
|
|
50
|
+
headingDepthJump: 'render-heading-depth-jump',
|
|
51
|
+
timelineEntryShape: 'render-timeline-entry-shape',
|
|
52
|
+
legacyRawHtmlEscaped: 'render-legacy-raw-html-escaped',
|
|
53
|
+
});
|
|
54
|
+
export const RENDER_QA_WARNING_IDS = Object.freeze({
|
|
55
|
+
documentOverflow: 'render-qa-document-overflow',
|
|
56
|
+
innerContainerOverflow: 'render-qa-inner-container-overflow',
|
|
57
|
+
viewportClipping: 'render-qa-viewport-clipping',
|
|
58
|
+
headingReadability: 'render-qa-heading-unreadable',
|
|
59
|
+
animationsEnabled: 'render-qa-animations-enabled',
|
|
60
|
+
reducedMotion: 'render-qa-reduced-motion',
|
|
61
|
+
keyboardNavigation: 'render-qa-keyboard-navigation',
|
|
62
|
+
themeToggle: 'render-qa-theme-toggle',
|
|
63
|
+
deckNoJsLayout: 'render-qa-deck-no-js-layout',
|
|
64
|
+
deckPrintLayout: 'render-qa-deck-print-layout',
|
|
65
|
+
skippedNoProbe: 'render-qa-skipped-no-probe',
|
|
66
|
+
});
|
|
67
|
+
const RENDER_QA_WARNING_BY_CODE = new Map([
|
|
68
|
+
['viewport-overflow', RENDER_QA_WARNING_IDS.documentOverflow],
|
|
69
|
+
['inner-x-overflow', RENDER_QA_WARNING_IDS.innerContainerOverflow],
|
|
70
|
+
['viewport-clipping', RENDER_QA_WARNING_IDS.viewportClipping],
|
|
71
|
+
['heading-readability', RENDER_QA_WARNING_IDS.headingReadability],
|
|
72
|
+
['animations-enabled', RENDER_QA_WARNING_IDS.animationsEnabled],
|
|
73
|
+
['reduced-motion', RENDER_QA_WARNING_IDS.reducedMotion],
|
|
74
|
+
['keyboard-navigation', RENDER_QA_WARNING_IDS.keyboardNavigation],
|
|
75
|
+
['theme-toggle', RENDER_QA_WARNING_IDS.themeToggle],
|
|
76
|
+
['deck-no-js-layout', RENDER_QA_WARNING_IDS.deckNoJsLayout],
|
|
77
|
+
['deck-print-layout', RENDER_QA_WARNING_IDS.deckPrintLayout],
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
export function renderQaWarningIds(issues) {
|
|
81
|
+
if (!Array.isArray(issues)) {
|
|
82
|
+
throw new TypeError('Render QA issues must be an array.');
|
|
83
|
+
}
|
|
84
|
+
return [
|
|
85
|
+
...new Set(
|
|
86
|
+
issues
|
|
87
|
+
.map(({ code }) => RENDER_QA_WARNING_BY_CODE.get(code))
|
|
88
|
+
.filter(Boolean),
|
|
89
|
+
),
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Degradation findings are prefixed rather than looked up so a newly added
|
|
94
|
+
// render warning code surfaces instead of being silently dropped.
|
|
95
|
+
export function renderWarningIds(warnings) {
|
|
96
|
+
if (!Array.isArray(warnings)) {
|
|
97
|
+
throw new TypeError('Render warnings must be an array.');
|
|
98
|
+
}
|
|
99
|
+
if (warnings.some(({ code } = {}) => typeof code !== 'string' || !code)) {
|
|
100
|
+
throw new TypeError('Every render warning requires a string code.');
|
|
101
|
+
}
|
|
102
|
+
return [...new Set(warnings.map(({ code }) => `render-${code}`))];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function checkGuidelines({ recipe, artifacts, expansion } = {}) {
|
|
106
|
+
if (!Array.isArray(artifacts)) {
|
|
107
|
+
throw new TypeError('Guideline checker artifacts must be an array.');
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
artifacts.some(
|
|
111
|
+
(artifact) =>
|
|
112
|
+
!isPlainObject(artifact) ||
|
|
113
|
+
typeof artifactId(artifact) !== 'string' ||
|
|
114
|
+
typeof artifact.type !== 'string' ||
|
|
115
|
+
typeof artifact.html !== 'string',
|
|
116
|
+
)
|
|
117
|
+
) {
|
|
118
|
+
throw new TypeError(
|
|
119
|
+
'Guideline checker artifacts require id, type, and HTML.',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const floor = recipeFloor(recipe);
|
|
124
|
+
const builtById = new Map(
|
|
125
|
+
artifacts.map((artifact) => [artifactId(artifact), artifact]),
|
|
126
|
+
);
|
|
127
|
+
const narrativeFloor = floor.filter(
|
|
128
|
+
(artifact) => recipeRequiredNarrative(recipe, artifact.id).length > 0,
|
|
129
|
+
);
|
|
130
|
+
const warnings = new Set();
|
|
131
|
+
|
|
132
|
+
const missesNarrative = narrativeFloor.some((floorArtifact) => {
|
|
133
|
+
const built = builtById.get(floorArtifact.id);
|
|
134
|
+
const required = recipeRequiredNarrative(recipe, floorArtifact.id);
|
|
135
|
+
return (
|
|
136
|
+
!built ||
|
|
137
|
+
required.some((sectionId) => !hasElementId(built.html, sectionId))
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
if (missesNarrative) {
|
|
141
|
+
warnings.add(GUIDELINE_WARNING_IDS.narrativeCoverage);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const hasDiagram =
|
|
145
|
+
artifacts.some(({ type }) => type === 'diagram') ||
|
|
146
|
+
artifacts.some(({ html }) => INLINE_DIAGRAM_PATTERN.test(html));
|
|
147
|
+
if (!hasDiagram) {
|
|
148
|
+
warnings.add(GUIDELINE_WARNING_IDS.architectureDiagram);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hasStructuredDepth = narrativeFloor.some((floorArtifact) => {
|
|
152
|
+
const html = builtById.get(floorArtifact.id)?.html ?? '';
|
|
153
|
+
return STRUCTURED_BLOCK_PATTERNS.some((pattern) => pattern.test(html));
|
|
154
|
+
});
|
|
155
|
+
if (narrativeFloor.length > 0 && !hasStructuredDepth) {
|
|
156
|
+
warnings.add(GUIDELINE_WARNING_IDS.structuredDepth);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
addExpansionWarnings(warnings, expansion);
|
|
160
|
+
return { valid: true, warnings: [...warnings] };
|
|
161
|
+
}
|
|
25
162
|
|
|
26
163
|
export function checkSourceDumping({
|
|
27
164
|
authoredText,
|
|
@@ -81,24 +218,111 @@ function shingles(value, size) {
|
|
|
81
218
|
|
|
82
219
|
export const BROWSER_PROBE_EVALUATE = `(() => {
|
|
83
220
|
const root = document.documentElement;
|
|
84
|
-
const
|
|
221
|
+
const elements = [...document.querySelectorAll('body *')];
|
|
222
|
+
const selector = (element) => element.id ? '#' + CSS.escape(element.id) :
|
|
223
|
+
element.classList.length ? '.' + [...element.classList].map(CSS.escape).join('.') :
|
|
224
|
+
element.tagName.toLowerCase();
|
|
225
|
+
const clippedX = elements
|
|
85
226
|
.filter((element) => {
|
|
86
227
|
const style = getComputedStyle(element);
|
|
87
228
|
return element.scrollWidth > element.clientWidth + 2 &&
|
|
88
229
|
['hidden', 'clip'].includes(style.overflowX);
|
|
89
230
|
})
|
|
90
231
|
.map((element) => ({
|
|
91
|
-
selector:
|
|
92
|
-
element.classList.length ? '.' + [...element.classList].map(CSS.escape).join('.') :
|
|
93
|
-
element.tagName.toLowerCase(),
|
|
232
|
+
selector: selector(element),
|
|
94
233
|
overflowX: getComputedStyle(element).overflowX,
|
|
95
234
|
clientWidth: element.clientWidth,
|
|
96
235
|
scrollWidth: element.scrollWidth
|
|
97
236
|
}))
|
|
98
237
|
.slice(0, 20);
|
|
238
|
+
// Ancestry inside a horizontal scroller is not reachability: scrollLeft only
|
|
239
|
+
// ranges over 0..scrollWidth-clientWidth, so content sitting at a negative
|
|
240
|
+
// content offset or past the scrollable extent can never be scrolled into
|
|
241
|
+
// view and stays a genuine clipping defect.
|
|
242
|
+
const TOLERANCE = 2;
|
|
243
|
+
const scrollReachable = (element) => {
|
|
244
|
+
const rect = element.getBoundingClientRect();
|
|
245
|
+
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
246
|
+
const style = getComputedStyle(node);
|
|
247
|
+
if (!['auto', 'scroll'].includes(style.overflowX)) continue;
|
|
248
|
+
if (node.scrollWidth - node.clientWidth <= TOLERANCE) continue;
|
|
249
|
+
const nodeRect = node.getBoundingClientRect();
|
|
250
|
+
const contentLeft =
|
|
251
|
+
rect.left - (nodeRect.left + node.clientLeft) + node.scrollLeft;
|
|
252
|
+
if (contentLeft >= -TOLERANCE &&
|
|
253
|
+
contentLeft + rect.width <= node.scrollWidth + TOLERANCE) return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
};
|
|
257
|
+
const viewportClipped = elements
|
|
258
|
+
.filter((element) => {
|
|
259
|
+
const rect = element.getBoundingClientRect();
|
|
260
|
+
return rect.width > 0 && rect.height > 0 &&
|
|
261
|
+
(rect.left < -TOLERANCE || rect.right > innerWidth + TOLERANCE) &&
|
|
262
|
+
!scrollReachable(element);
|
|
263
|
+
})
|
|
264
|
+
.map((element) => {
|
|
265
|
+
const rect = element.getBoundingClientRect();
|
|
266
|
+
return {
|
|
267
|
+
selector: selector(element),
|
|
268
|
+
left: rect.left,
|
|
269
|
+
right: rect.right,
|
|
270
|
+
viewportWidth: innerWidth
|
|
271
|
+
};
|
|
272
|
+
})
|
|
273
|
+
.slice(0, 20);
|
|
274
|
+
// A heading in a collapsed panel or an aria-hidden subtree is deliberately
|
|
275
|
+
// not presented, so it is out of scope rather than unreadable. Visually
|
|
276
|
+
// hidden accessibility text still renders a box and stays in scope.
|
|
277
|
+
const presented = (element) => {
|
|
278
|
+
const rendered = typeof element.checkVisibility === 'function'
|
|
279
|
+
? element.checkVisibility({
|
|
280
|
+
visibilityProperty: true,
|
|
281
|
+
contentVisibilityAuto: true
|
|
282
|
+
})
|
|
283
|
+
: element.getClientRects().length > 0;
|
|
284
|
+
return rendered && element.closest('[aria-hidden="true"]') === null;
|
|
285
|
+
};
|
|
286
|
+
const unreadableHeadings = [...document.querySelectorAll('h1,h2,h3,h4,h5,h6')]
|
|
287
|
+
.filter(presented)
|
|
288
|
+
.filter((heading) => {
|
|
289
|
+
const style = getComputedStyle(heading);
|
|
290
|
+
const rect = heading.getBoundingClientRect();
|
|
291
|
+
const fontSize = Number.parseFloat(style.fontSize);
|
|
292
|
+
return !heading.textContent.trim() || rect.width <= 0 || rect.height <= 0 ||
|
|
293
|
+
!Number.isFinite(fontSize) || fontSize < 12;
|
|
294
|
+
})
|
|
295
|
+
.map((heading) => ({
|
|
296
|
+
selector: selector(heading),
|
|
297
|
+
text: heading.textContent.trim(),
|
|
298
|
+
fontSize: Number.parseFloat(getComputedStyle(heading).fontSize)
|
|
299
|
+
}))
|
|
300
|
+
.slice(0, 20);
|
|
301
|
+
// Reduced-motion styling conventionally collapses transitions to a token
|
|
302
|
+
// 0.01ms rather than 0s so transitionend still fires. That is suppressed
|
|
303
|
+
// motion, not active motion, so anything under a millisecond counts as
|
|
304
|
+
// disabled while a perceptible duration still reports.
|
|
305
|
+
// Motion is just as visible on a generated ::before/::after box as on the
|
|
306
|
+
// element itself, so all three are inspected. A pseudo-element that
|
|
307
|
+
// generates no content cannot animate and is skipped.
|
|
308
|
+
const PERCEPTIBLE_SECONDS = 0.001;
|
|
309
|
+
const motionless = (element, pseudo) => {
|
|
310
|
+
const style = getComputedStyle(element, pseudo);
|
|
311
|
+
if (pseudo && ['none', 'normal'].includes(style.content)) return true;
|
|
312
|
+
const durations = (style.animationDuration + ',' + style.transitionDuration)
|
|
313
|
+
.split(',')
|
|
314
|
+
.map((value) => Number.parseFloat(value) || 0);
|
|
315
|
+
return style.animationName === 'none' &&
|
|
316
|
+
durations.every((value) => value < PERCEPTIBLE_SECONDS);
|
|
317
|
+
};
|
|
318
|
+
const animationsDisabled = elements.every((element) =>
|
|
319
|
+
[null, '::before', '::after'].every((pseudo) => motionless(element, pseudo)));
|
|
99
320
|
return {
|
|
100
321
|
pageOverflowX: root.scrollWidth > root.clientWidth + 2,
|
|
101
322
|
clippedX,
|
|
323
|
+
viewportClipped,
|
|
324
|
+
unreadableHeadings,
|
|
325
|
+
animationsDisabled,
|
|
102
326
|
reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
103
327
|
deckLayout: document.querySelector('.deck') ? {
|
|
104
328
|
flow: getComputedStyle(document.querySelector('.deck')).display === 'block' ?
|
|
@@ -150,13 +374,12 @@ export function checkHtmlStructure({
|
|
|
150
374
|
}
|
|
151
375
|
}
|
|
152
376
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
INLINE_ASSET_VIOLATION_PATTERN.test(html)
|
|
156
|
-
) {
|
|
377
|
+
const unpinnedRefs = findUnpinnedResourceRefs(html);
|
|
378
|
+
if (unpinnedRefs.length > 0 || INLINE_ASSET_VIOLATION_PATTERN.test(html)) {
|
|
157
379
|
add(
|
|
158
380
|
'external-asset',
|
|
159
381
|
'Final HTML assets must be inline and self-contained.',
|
|
382
|
+
...(unpinnedRefs.length > 0 ? [{ refs: unpinnedRefs.slice(0, 10) }] : []),
|
|
160
383
|
);
|
|
161
384
|
}
|
|
162
385
|
|
|
@@ -269,6 +492,9 @@ export async function runBrowserProbes({
|
|
|
269
492
|
scenario,
|
|
270
493
|
viewport: { width, height: representativeHeight(width) },
|
|
271
494
|
reducedMotion: 'reduce',
|
|
495
|
+
disableAnimations: true,
|
|
496
|
+
injectedCss:
|
|
497
|
+
'*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}',
|
|
272
498
|
evaluate: BROWSER_PROBE_EVALUATE,
|
|
273
499
|
keyboard:
|
|
274
500
|
artifact.type === 'deck' && scenario === 'default'
|
|
@@ -314,6 +540,30 @@ export async function runBrowserProbes({
|
|
|
314
540
|
details: result.clippedX,
|
|
315
541
|
});
|
|
316
542
|
}
|
|
543
|
+
if ((result.viewportClipped ?? []).length > 0) {
|
|
544
|
+
issues.push({
|
|
545
|
+
...context,
|
|
546
|
+
code: 'viewport-clipping',
|
|
547
|
+
message: 'Rendered content is clipped outside the viewport.',
|
|
548
|
+
details: result.viewportClipped,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if ((result.unreadableHeadings ?? []).length > 0) {
|
|
552
|
+
issues.push({
|
|
553
|
+
...context,
|
|
554
|
+
code: 'heading-readability',
|
|
555
|
+
message: 'A heading is not visibly readable.',
|
|
556
|
+
details: result.unreadableHeadings,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if (result.animationsDisabled === false) {
|
|
560
|
+
issues.push({
|
|
561
|
+
...context,
|
|
562
|
+
code: 'animations-enabled',
|
|
563
|
+
message:
|
|
564
|
+
'Render QA observed active animation or transition timing.',
|
|
565
|
+
});
|
|
566
|
+
}
|
|
317
567
|
if (!result.reducedMotion) {
|
|
318
568
|
issues.push({
|
|
319
569
|
...context,
|
|
@@ -532,6 +782,18 @@ function validateProbeResult(result, artifactId, width, request) {
|
|
|
532
782
|
`Browser probe for ${artifactId} at ${width}px returned an invalid result.`,
|
|
533
783
|
);
|
|
534
784
|
}
|
|
785
|
+
if (
|
|
786
|
+
(result.viewportClipped !== undefined &&
|
|
787
|
+
!Array.isArray(result.viewportClipped)) ||
|
|
788
|
+
(result.unreadableHeadings !== undefined &&
|
|
789
|
+
!Array.isArray(result.unreadableHeadings)) ||
|
|
790
|
+
(result.animationsDisabled !== undefined &&
|
|
791
|
+
typeof result.animationsDisabled !== 'boolean')
|
|
792
|
+
) {
|
|
793
|
+
throw new TypeError(
|
|
794
|
+
`Browser layout probe for ${artifactId} at ${width}px returned an invalid result.`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
535
797
|
if (request.themeToggle && !isPlainObject(result.themeToggle)) {
|
|
536
798
|
throw new TypeError(
|
|
537
799
|
`Browser theme probe for ${artifactId} at ${width}px returned an invalid result.`,
|
|
@@ -563,6 +825,43 @@ function representativeHeight(width) {
|
|
|
563
825
|
return 900;
|
|
564
826
|
}
|
|
565
827
|
|
|
828
|
+
function artifactId(artifact) {
|
|
829
|
+
return artifact?.id ?? artifact?.artifactId;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function hasElementId(html, id) {
|
|
833
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
834
|
+
return new RegExp(`\\bid\\s*=\\s*["']${escaped}["']`, 'i').test(html);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function addExpansionWarnings(warnings, expansion) {
|
|
838
|
+
if (expansion === undefined) return;
|
|
839
|
+
if (
|
|
840
|
+
!isPlainObject(expansion) ||
|
|
841
|
+
!Array.isArray(expansion.errors) ||
|
|
842
|
+
!Array.isArray(expansion.rejected) ||
|
|
843
|
+
!Array.isArray(expansion.warnings)
|
|
844
|
+
) {
|
|
845
|
+
throw new TypeError(
|
|
846
|
+
'Guideline checker expansion must be an evaluated proposal result.',
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
if (expansion.valid !== true || expansion.errors.length > 0) {
|
|
850
|
+
throw new TypeError(
|
|
851
|
+
'Guideline checker cannot convert expansion proposal errors into warnings.',
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const knownWarnings = new Set(EXPANSION_WARNING_BY_REJECTION_REASON.values());
|
|
856
|
+
for (const warning of expansion.warnings) {
|
|
857
|
+
if (knownWarnings.has(warning)) warnings.add(warning);
|
|
858
|
+
}
|
|
859
|
+
for (const rejected of expansion.rejected) {
|
|
860
|
+
const warning = EXPANSION_WARNING_BY_REJECTION_REASON.get(rejected?.reason);
|
|
861
|
+
if (warning) warnings.add(warning);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
566
865
|
function isPlainObject(value) {
|
|
567
866
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
568
867
|
}
|