@open-agent-toolkit/cli 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/assets/docs/cli-utilities/configuration.md +40 -0
  2. package/assets/docs/cli-utilities/tool-packs.md +31 -0
  3. package/assets/docs/workflows/projects/artifacts.md +27 -0
  4. package/assets/docs/workflows/projects/implementation-execution.md +30 -5
  5. package/assets/docs/workflows/skills/explainer-kit.md +125 -0
  6. package/assets/docs/workflows/skills/index.md +4 -0
  7. package/assets/migration/pjm-restructure.md +12 -0
  8. package/assets/public-package-versions.json +4 -4
  9. package/assets/skills/explainer-kit/SKILL.md +95 -0
  10. package/assets/skills/explainer-kit/examples/project-explainer/content.md +31 -0
  11. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +22 -0
  12. package/assets/skills/explainer-kit/examples/project-recap/content.md +34 -0
  13. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +22 -0
  14. package/assets/skills/explainer-kit/examples/theme-bundle.json +87 -0
  15. package/assets/skills/explainer-kit/palettes/ember.json +37 -0
  16. package/assets/skills/explainer-kit/palettes/forest.json +37 -0
  17. package/assets/skills/explainer-kit/palettes/neutral.json +37 -0
  18. package/assets/skills/explainer-kit/palettes/ocean.json +37 -0
  19. package/assets/skills/explainer-kit/palettes/violet.json +37 -0
  20. package/assets/skills/explainer-kit/profiles/clean.json +42 -0
  21. package/assets/skills/explainer-kit/profiles/editorial.json +42 -0
  22. package/assets/skills/explainer-kit/profiles/technical.json +39 -0
  23. package/assets/skills/explainer-kit/recipes/engineer-tour.json +33 -0
  24. package/assets/skills/explainer-kit/recipes/program-recap.json +34 -0
  25. package/assets/skills/explainer-kit/recipes/project-explainer.json +33 -0
  26. package/assets/skills/explainer-kit/recipes/project-recap.json +34 -0
  27. package/assets/skills/explainer-kit/references/contracts.md +101 -0
  28. package/assets/skills/explainer-kit/references/destination-contract.md +94 -0
  29. package/assets/skills/explainer-kit/references/extension-contract.md +71 -0
  30. package/assets/skills/explainer-kit/references/fact-base-contract.md +119 -0
  31. package/assets/skills/explainer-kit/schemas/build-record.schema.json +92 -0
  32. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +53 -0
  33. package/assets/skills/explainer-kit/schemas/fact-base.schema.json +129 -0
  34. package/assets/skills/explainer-kit/schemas/manifest.schema.json +193 -0
  35. package/assets/skills/explainer-kit/schemas/publish-receipt.schema.json +97 -0
  36. package/assets/skills/explainer-kit/schemas/publish-request.schema.json +32 -0
  37. package/assets/skills/explainer-kit/schemas/run-request.schema.json +132 -0
  38. package/assets/skills/explainer-kit/schemas/theme.schema.json +217 -0
  39. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +180 -0
  40. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +680 -0
  41. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +538 -0
  42. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +494 -0
  43. package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +218 -0
  44. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +512 -0
  45. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +289 -0
  46. package/assets/skills/explainer-kit/scripts/lib/records.mjs +231 -0
  47. package/assets/skills/explainer-kit/scripts/lib/render.mjs +485 -0
  48. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +587 -0
  49. package/assets/skills/explainer-kit/scripts/lib/safe-paths.mjs +96 -0
  50. package/assets/skills/explainer-kit/scripts/lib/theme.mjs +332 -0
  51. package/assets/skills/explainer-kit/scripts/publish.mjs +76 -0
  52. package/assets/skills/explainer-kit/scripts/record-durability.mjs +64 -0
  53. package/assets/skills/explainer-kit/scripts/render-qa.mjs +273 -0
  54. package/assets/skills/explainer-kit/scripts/run.mjs +778 -0
  55. package/assets/skills/explainer-kit/scripts/validate.mjs +59 -0
  56. package/assets/skills/explainer-kit/templates/deck-shell.html +233 -0
  57. package/assets/skills/explainer-kit/templates/diagram-shell.html +219 -0
  58. package/assets/skills/explainer-kit/templates/engineer-tour.html +273 -0
  59. package/assets/skills/explainer-kit/templates/house-style.html +218 -0
  60. package/assets/skills/oat-explainer-kit/SKILL.md +92 -0
  61. package/assets/skills/oat-explainer-kit/references/config-contract.md +67 -0
  62. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +143 -0
  63. package/assets/skills/oat-explainer-kit/references/migration.md +143 -0
  64. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +103 -0
  65. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +127 -0
  66. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +392 -0
  67. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +130 -0
  68. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +293 -0
  69. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +209 -0
  70. package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +166 -0
  71. package/assets/skills/oat-explainer-kit/scripts/run.mjs +284 -0
  72. package/assets/skills/oat-project-autonomous/SKILL.md +22 -1
  73. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +5 -1
  74. package/assets/skills/oat-project-complete/SKILL.md +166 -28
  75. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +5 -1
  76. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  77. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +84 -9
  78. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +5 -1
  79. package/assets/skills/oat-project-next/SKILL.md +31 -11
  80. package/assets/skills/oat-project-plan/SKILL.md +30 -1
  81. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +5 -1
  82. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +5 -1
  83. package/assets/skills/oat-project-summary/SKILL.md +20 -1
  84. package/assets/templates/state.md +4 -1
  85. package/dist/commands/config/index.d.ts.map +1 -1
  86. package/dist/commands/config/index.js +232 -1
  87. package/dist/commands/init/tools/shared/skill-manifest.d.ts +2 -2
  88. package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
  89. package/dist/commands/init/tools/shared/skill-manifest.js +2 -0
  90. package/dist/commands/project/archive/archive-utils.d.ts +12 -0
  91. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  92. package/dist/commands/project/archive/archive-utils.js +320 -8
  93. package/dist/commands/project/archive/index.d.ts.map +1 -1
  94. package/dist/commands/project/archive/index.js +1 -0
  95. package/dist/commands/project/archive/push-runner.d.ts +1 -0
  96. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  97. package/dist/commands/project/archive/push-runner.js +9 -2
  98. package/dist/config/oat-config.d.ts +26 -0
  99. package/dist/config/oat-config.d.ts.map +1 -1
  100. package/dist/config/oat-config.js +88 -0
  101. package/dist/config/resolve.d.ts.map +1 -1
  102. package/dist/config/resolve.js +18 -0
  103. package/dist/validation/project-state.d.ts +3 -0
  104. package/dist/validation/project-state.d.ts.map +1 -1
  105. package/dist/validation/project-state.js +91 -0
  106. package/package.json +2 -2
@@ -0,0 +1,512 @@
1
+ const VOID_ELEMENTS = new Set([
2
+ 'area',
3
+ 'base',
4
+ 'br',
5
+ 'col',
6
+ 'embed',
7
+ 'hr',
8
+ 'img',
9
+ 'input',
10
+ 'link',
11
+ 'meta',
12
+ 'param',
13
+ 'source',
14
+ 'track',
15
+ 'wbr',
16
+ ]);
17
+ const EXTERNAL_ASSET_PATTERN =
18
+ /<(?:script|iframe|source|img|audio|video|object|embed)\b[^>]*(?:src|data)\s*=\s*["'](?!data:)[^"']+/i;
19
+ const INLINE_ASSET_VIOLATION_PATTERN =
20
+ /<link\b|@import\b|url\(\s*["']?(?!data:|#)/i;
21
+ const TOKEN_PATTERN = /{{\s*[A-Z][A-Z0-9_]*\s*}}/g;
22
+ const ARROW_KEYS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
23
+
24
+ export const REPRESENTATIVE_WIDTHS = Object.freeze([320, 768, 1440]);
25
+
26
+ export const BROWSER_PROBE_EVALUATE = `(() => {
27
+ const root = document.documentElement;
28
+ const clippedX = [...document.querySelectorAll('body *')]
29
+ .filter((element) => {
30
+ const style = getComputedStyle(element);
31
+ return element.scrollWidth > element.clientWidth + 2 &&
32
+ ['hidden', 'clip'].includes(style.overflowX);
33
+ })
34
+ .map((element) => ({
35
+ selector: element.id ? '#' + CSS.escape(element.id) :
36
+ element.classList.length ? '.' + [...element.classList].map(CSS.escape).join('.') :
37
+ element.tagName.toLowerCase(),
38
+ overflowX: getComputedStyle(element).overflowX,
39
+ clientWidth: element.clientWidth,
40
+ scrollWidth: element.scrollWidth
41
+ }))
42
+ .slice(0, 20);
43
+ return {
44
+ pageOverflowX: root.scrollWidth > root.clientWidth + 2,
45
+ clippedX,
46
+ reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
47
+ deckLayout: document.querySelector('.deck') ? {
48
+ flow: getComputedStyle(document.querySelector('.deck')).display === 'block' ?
49
+ 'vertical' : 'horizontal',
50
+ overflowX: getComputedStyle(document.querySelector('.slide__content')).overflowX
51
+ } : null,
52
+ themeToggle: document.querySelector('[data-theme-toggle]') ? {
53
+ present: true,
54
+ initialMode: root.dataset.themeMode
55
+ } : null
56
+ };
57
+ })()`;
58
+
59
+ export function checkHtmlStructure({
60
+ id = 'artifact',
61
+ type,
62
+ html,
63
+ denylist = [],
64
+ }) {
65
+ if (typeof html !== 'string') {
66
+ throw new TypeError(`QA artifact ${id} must provide HTML as a string.`);
67
+ }
68
+ if (
69
+ !Array.isArray(denylist) ||
70
+ denylist.some((item) => typeof item !== 'string')
71
+ ) {
72
+ throw new TypeError('QA denylist must be an array of strings.');
73
+ }
74
+
75
+ const issues = [];
76
+ const add = (code, message, details) => {
77
+ issues.push({ artifactId: id, code, message, ...(details && { details }) });
78
+ };
79
+
80
+ if (TOKEN_PATTERN.test(html)) {
81
+ add('unresolved-token', 'Artifact contains an unresolved template token.');
82
+ }
83
+ TOKEN_PATTERN.lastIndex = 0;
84
+
85
+ for (const denied of denylist.filter(Boolean)) {
86
+ if (html.toLocaleLowerCase().includes(denied.toLocaleLowerCase())) {
87
+ add(
88
+ 'denylisted-string',
89
+ 'Artifact contains a configured denylisted string.',
90
+ {
91
+ value: denied,
92
+ },
93
+ );
94
+ }
95
+ }
96
+
97
+ if (
98
+ EXTERNAL_ASSET_PATTERN.test(html) ||
99
+ INLINE_ASSET_VIOLATION_PATTERN.test(html)
100
+ ) {
101
+ add(
102
+ 'external-asset',
103
+ 'Final HTML assets must be inline and self-contained.',
104
+ );
105
+ }
106
+
107
+ for (const imbalance of findTagImbalances(html)) {
108
+ add('tag-balance', imbalance);
109
+ }
110
+
111
+ checkHeadings(html, add);
112
+ checkLinks(html, add);
113
+
114
+ const hasMotion =
115
+ /\b(?:animation(?:-name)?|transition|scroll-behavior)\s*:/i.test(html);
116
+ if (
117
+ hasMotion &&
118
+ !/@media\s*\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)/i.test(html)
119
+ ) {
120
+ add(
121
+ 'reduced-motion',
122
+ 'Artifact must define a prefers-reduced-motion reduction.',
123
+ );
124
+ }
125
+
126
+ const isDeck =
127
+ type === 'deck' || /class\s*=\s*["'][^"']*\bdeck\b/i.test(html);
128
+ if (isDeck) {
129
+ const missing = ARROW_KEYS.filter(
130
+ (key) => !new RegExp(`['"]${key}['"]`).test(html),
131
+ );
132
+ if (missing.length > 0) {
133
+ add(
134
+ 'deck-keyboard',
135
+ 'Deck keyboard navigation must support both horizontal and vertical arrow pairs.',
136
+ { missing },
137
+ );
138
+ }
139
+ }
140
+
141
+ return { valid: issues.length === 0, issues };
142
+ }
143
+
144
+ export function checkArtifactCohesion(artifacts) {
145
+ if (!Array.isArray(artifacts)) {
146
+ throw new TypeError('Artifact cohesion input must be an array.');
147
+ }
148
+ const issues = [];
149
+ const groups = ['terminology', 'numericClaims', 'statuses'];
150
+
151
+ for (const group of groups) {
152
+ const claims = new Map();
153
+ for (const artifact of artifacts) {
154
+ const values = artifact?.cohesion?.[group] ?? {};
155
+ if (!isPlainObject(values)) {
156
+ throw new TypeError(
157
+ `Artifact ${String(artifact?.id)} cohesion.${group} must be an object.`,
158
+ );
159
+ }
160
+ for (const [claim, value] of Object.entries(values)) {
161
+ const normalized = normalizeClaim(value);
162
+ const prior = claims.get(claim);
163
+ if (!prior) {
164
+ claims.set(claim, {
165
+ normalized,
166
+ value,
167
+ artifactId: artifact.id,
168
+ });
169
+ } else if (prior.normalized !== normalized) {
170
+ issues.push({
171
+ code: `cohesion-${group}`,
172
+ message: `Artifact set disagrees on ${group}.${claim}.`,
173
+ claim,
174
+ values: [
175
+ { artifactId: prior.artifactId, value: prior.value },
176
+ { artifactId: artifact.id, value },
177
+ ],
178
+ });
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ return { valid: issues.length === 0, issues };
185
+ }
186
+
187
+ export async function runBrowserProbes({
188
+ artifacts,
189
+ probe,
190
+ widths = REPRESENTATIVE_WIDTHS,
191
+ }) {
192
+ if (!Array.isArray(artifacts) || artifacts.length === 0) {
193
+ throw new TypeError('Browser QA requires at least one artifact.');
194
+ }
195
+ if (typeof probe !== 'function') {
196
+ throw new TypeError('Browser QA requires a probe callback.');
197
+ }
198
+ if (
199
+ !Array.isArray(widths) ||
200
+ widths.length === 0 ||
201
+ widths.some((width) => !Number.isInteger(width) || width < 240)
202
+ ) {
203
+ throw new TypeError('Browser QA widths must be viewport-sized integers.');
204
+ }
205
+
206
+ const issues = [];
207
+ let probes = 0;
208
+ for (const artifact of artifacts) {
209
+ for (const width of widths) {
210
+ for (const scenario of browserScenarios(artifact)) {
211
+ const request = {
212
+ artifact,
213
+ scenario,
214
+ viewport: { width, height: representativeHeight(width) },
215
+ reducedMotion: 'reduce',
216
+ evaluate: BROWSER_PROBE_EVALUATE,
217
+ keyboard:
218
+ artifact.type === 'deck' && scenario === 'default'
219
+ ? { tab: true, arrows: [...ARROW_KEYS] }
220
+ : { tab: true },
221
+ ...(scenario === 'no-js' && { javascriptEnabled: false }),
222
+ ...(scenario === 'print' && { media: 'print' }),
223
+ ...(artifact.type === 'deck' &&
224
+ scenario !== 'default' && {
225
+ wideContent: {
226
+ containerSelector: '.slide__content',
227
+ width: 2048,
228
+ },
229
+ }),
230
+ ...(artifact.html.includes(
231
+ 'data-render-strategy="user-switchable"',
232
+ ) &&
233
+ scenario === 'default' && {
234
+ themeToggle: {
235
+ selector: '[data-theme-toggle]',
236
+ activate: 'keyboard',
237
+ expectPersistence: true,
238
+ },
239
+ }),
240
+ };
241
+ const result = await probe(request);
242
+ probes += 1;
243
+ validateProbeResult(result, artifact.id, width, request);
244
+
245
+ const context = { artifactId: artifact.id, width, scenario };
246
+ if (result.pageOverflowX) {
247
+ issues.push({
248
+ ...context,
249
+ code: 'viewport-overflow',
250
+ message: 'Page exceeds the representative viewport width.',
251
+ });
252
+ }
253
+ if (result.clippedX.length > 0) {
254
+ issues.push({
255
+ ...context,
256
+ code: 'inner-x-overflow',
257
+ message: 'An inner container clips content on the x axis.',
258
+ details: result.clippedX,
259
+ });
260
+ }
261
+ if (!result.reducedMotion) {
262
+ issues.push({
263
+ ...context,
264
+ code: 'reduced-motion',
265
+ message: 'Browser did not observe reduced-motion mode.',
266
+ });
267
+ }
268
+ if (
269
+ !keyboardPassed(
270
+ artifact.type,
271
+ result.keyboard,
272
+ scenario === 'default',
273
+ )
274
+ ) {
275
+ issues.push({
276
+ ...context,
277
+ code: 'keyboard-navigation',
278
+ message: 'Browser keyboard navigation probe failed.',
279
+ });
280
+ }
281
+ if (
282
+ request.themeToggle &&
283
+ (!result.themeToggle.present ||
284
+ !result.themeToggle.keyboardOperable ||
285
+ result.themeToggle.initialMode === result.themeToggle.toggledMode ||
286
+ !result.themeToggle.persisted)
287
+ ) {
288
+ issues.push({
289
+ ...context,
290
+ code: 'theme-toggle',
291
+ message:
292
+ 'Switchable theme control must operate by keyboard and persist the alternate mode.',
293
+ });
294
+ }
295
+ if (
296
+ scenario === 'no-js' &&
297
+ (result.deckLayout.flow !== 'vertical' ||
298
+ result.deckLayout.overflowX !== 'auto')
299
+ ) {
300
+ issues.push({
301
+ ...context,
302
+ code: 'deck-no-js-layout',
303
+ message:
304
+ 'No-JS deck must use vertical flow with x-axis auto containment.',
305
+ });
306
+ }
307
+ if (
308
+ scenario === 'print' &&
309
+ (result.deckLayout.flow !== 'vertical' ||
310
+ result.deckLayout.overflowX !== 'visible')
311
+ ) {
312
+ issues.push({
313
+ ...context,
314
+ code: 'deck-print-layout',
315
+ message: 'Print deck must use its separate vertical print cascade.',
316
+ });
317
+ }
318
+ }
319
+ }
320
+ }
321
+
322
+ return { valid: issues.length === 0, issues, probes };
323
+ }
324
+
325
+ export async function auditArtifactSet({
326
+ artifacts,
327
+ denylist = [],
328
+ browserProbe,
329
+ widths,
330
+ }) {
331
+ if (!Array.isArray(artifacts) || artifacts.length === 0) {
332
+ throw new TypeError('Render QA requires at least one artifact.');
333
+ }
334
+
335
+ const structural = artifacts.map((artifact) => ({
336
+ id: artifact.id,
337
+ ...checkHtmlStructure({ ...artifact, denylist }),
338
+ }));
339
+ const cohesion = checkArtifactCohesion(artifacts);
340
+ const browser = browserProbe
341
+ ? await runBrowserProbes({
342
+ artifacts,
343
+ probe: browserProbe,
344
+ ...(widths && { widths }),
345
+ })
346
+ : null;
347
+ const issues = [
348
+ ...structural.flatMap((artifact) => artifact.issues),
349
+ ...cohesion.issues,
350
+ ...(browser?.issues ?? []),
351
+ ];
352
+
353
+ return {
354
+ valid: issues.length === 0,
355
+ issues,
356
+ artifacts: structural,
357
+ cohesion,
358
+ browser,
359
+ };
360
+ }
361
+
362
+ function checkHeadings(html, add) {
363
+ const headings = [
364
+ ...html.matchAll(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1\s*>/gi),
365
+ ].map((match) => ({
366
+ level: Number(match[1]),
367
+ text: visibleText(match[2]),
368
+ }));
369
+
370
+ if (
371
+ headings.length === 0 ||
372
+ headings.filter(({ level }) => level === 1).length > 1
373
+ ) {
374
+ add(
375
+ 'heading-text',
376
+ 'Artifact must contain readable headings and no more than one h1.',
377
+ );
378
+ }
379
+ if (headings.some(({ text }) => text.length === 0)) {
380
+ add('heading-text', 'Heading text must not be empty.');
381
+ }
382
+ for (let index = 1; index < headings.length; index += 1) {
383
+ if (headings[index].level > headings[index - 1].level + 1) {
384
+ add('heading-order', 'Heading levels must not skip hierarchy levels.');
385
+ break;
386
+ }
387
+ }
388
+ }
389
+
390
+ function checkLinks(html, add) {
391
+ const ids = new Set(
392
+ [...html.matchAll(/\bid\s*=\s*["']([^"']+)["']/gi)].map(
393
+ (match) => match[1],
394
+ ),
395
+ );
396
+ for (const match of html.matchAll(/<a\b([^>]*)>/gi)) {
397
+ const href = match[1].match(/\bhref\s*=\s*["']([^"']*)["']/i)?.[1];
398
+ const invalid =
399
+ !href ||
400
+ href.startsWith('/') ||
401
+ href.startsWith('//') ||
402
+ /^(?:javascript|data|file):/i.test(href) ||
403
+ (href.startsWith('#') && !ids.has(href.slice(1)));
404
+ if (invalid) {
405
+ add(
406
+ 'link-form',
407
+ 'Links require a safe absolute HTTPS, relative, or valid fragment href.',
408
+ { href: href ?? null },
409
+ );
410
+ }
411
+ }
412
+ }
413
+
414
+ function findTagImbalances(html) {
415
+ const normalized = html
416
+ .replace(/<!--[\s\S]*?-->/g, '')
417
+ .replace(/<(script|style)\b([^>]*)>[\s\S]*?<\/\1\s*>/gi, '<$1$2></$1>');
418
+ const stack = [];
419
+ const issues = [];
420
+ for (const match of normalized.matchAll(/<\/?([a-z][a-z0-9-]*)\b[^>]*>/gi)) {
421
+ const full = match[0];
422
+ const tag = match[1].toLowerCase();
423
+ if (
424
+ full.startsWith('<!') ||
425
+ VOID_ELEMENTS.has(tag) ||
426
+ /\/\s*>$/.test(full)
427
+ ) {
428
+ continue;
429
+ }
430
+ if (!full.startsWith('</')) {
431
+ stack.push(tag);
432
+ continue;
433
+ }
434
+ const expected = stack.pop();
435
+ if (expected !== tag) {
436
+ issues.push(
437
+ `Closing </${tag}> does not match ${expected ? `<${expected}>` : 'an open tag'}.`,
438
+ );
439
+ }
440
+ }
441
+ for (const tag of stack.reverse()) {
442
+ issues.push(`Tag <${tag}> is not closed.`);
443
+ }
444
+ return issues;
445
+ }
446
+
447
+ function visibleText(value) {
448
+ return value
449
+ .replace(/<[^>]+>/g, '')
450
+ .replace(/&(?:nbsp|#160);/gi, ' ')
451
+ .trim();
452
+ }
453
+
454
+ function normalizeClaim(value) {
455
+ if (typeof value === 'number') return `number:${value}`;
456
+ if (typeof value === 'string') {
457
+ const compact = value.trim().replace(/\s+/g, ' ').toLocaleLowerCase();
458
+ if (/^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(compact)) {
459
+ return `number:${Number(compact)}`;
460
+ }
461
+ return `string:${compact}`;
462
+ }
463
+ if (typeof value === 'boolean') return `boolean:${value}`;
464
+ throw new TypeError('Cohesion claims must be strings, numbers, or booleans.');
465
+ }
466
+
467
+ function validateProbeResult(result, artifactId, width, request) {
468
+ if (
469
+ !isPlainObject(result) ||
470
+ typeof result.pageOverflowX !== 'boolean' ||
471
+ !Array.isArray(result.clippedX) ||
472
+ typeof result.reducedMotion !== 'boolean' ||
473
+ !isPlainObject(result.keyboard)
474
+ ) {
475
+ throw new TypeError(
476
+ `Browser probe for ${artifactId} at ${width}px returned an invalid result.`,
477
+ );
478
+ }
479
+ if (request.themeToggle && !isPlainObject(result.themeToggle)) {
480
+ throw new TypeError(
481
+ `Browser theme probe for ${artifactId} at ${width}px returned an invalid result.`,
482
+ );
483
+ }
484
+ if (request.scenario !== 'default' && !isPlainObject(result.deckLayout)) {
485
+ throw new TypeError(
486
+ `Browser deck probe for ${artifactId} at ${width}px returned an invalid result.`,
487
+ );
488
+ }
489
+ }
490
+
491
+ function keyboardPassed(type, keyboard, requireDeckArrows) {
492
+ if (keyboard.tab !== true) return false;
493
+ if (type !== 'deck' || !requireDeckArrows) return true;
494
+ return (
495
+ isPlainObject(keyboard.arrows) &&
496
+ ARROW_KEYS.every((key) => keyboard.arrows[key] === true)
497
+ );
498
+ }
499
+
500
+ function browserScenarios(artifact) {
501
+ return artifact.type === 'deck' ? ['default', 'no-js', 'print'] : ['default'];
502
+ }
503
+
504
+ function representativeHeight(width) {
505
+ if (width <= 480) return 640;
506
+ if (width <= 900) return 1024;
507
+ return 900;
508
+ }
509
+
510
+ function isPlainObject(value) {
511
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
512
+ }