@open-agent-toolkit/cli 0.2.19 → 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.
Files changed (66) hide show
  1. package/assets/config/dispatch-matrix-recommendation.json +15 -5
  2. package/assets/docs/cli-utilities/configuration.md +10 -5
  3. package/assets/docs/contributing/index.md +1 -0
  4. package/assets/docs/contributing/verifying-cursor-pins.md +282 -0
  5. package/assets/docs/workflows/projects/artifacts.md +1 -1
  6. package/assets/docs/workflows/projects/dispatch-ceiling.md +50 -4
  7. package/assets/docs/workflows/skills/explainer-kit.md +188 -23
  8. package/assets/docs/workflows/skills/index.md +1 -1
  9. package/assets/public-package-versions.json +4 -4
  10. package/assets/skills/explainer-kit/SKILL.md +87 -25
  11. package/assets/skills/explainer-kit/briefs/deep-dive.md +35 -0
  12. package/assets/skills/explainer-kit/briefs/engineer-tour.md +45 -0
  13. package/assets/skills/explainer-kit/briefs/program-recap.md +41 -0
  14. package/assets/skills/explainer-kit/briefs/project-explainer.md +38 -0
  15. package/assets/skills/explainer-kit/briefs/project-page.md +38 -0
  16. package/assets/skills/explainer-kit/briefs/project-recap.md +47 -0
  17. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +28 -0
  18. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +35 -0
  19. package/assets/skills/explainer-kit/examples/project-recap/content.md +57 -14
  20. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +104 -0
  21. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +26 -4
  22. package/assets/skills/explainer-kit/recipes/engineer-tour.json +27 -10
  23. package/assets/skills/explainer-kit/recipes/program-recap.json +35 -11
  24. package/assets/skills/explainer-kit/recipes/project-explainer.json +27 -10
  25. package/assets/skills/explainer-kit/recipes/project-recap.json +43 -11
  26. package/assets/skills/explainer-kit/references/contracts.md +45 -19
  27. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +41 -0
  28. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +52 -0
  29. package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +442 -0
  30. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +223 -10
  31. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +28 -43
  32. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +237 -0
  33. package/assets/skills/explainer-kit/scripts/lib/html-safety.mjs +680 -0
  34. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +414 -0
  35. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +309 -10
  36. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +313 -41
  37. package/assets/skills/explainer-kit/scripts/lib/records.mjs +61 -0
  38. package/assets/skills/explainer-kit/scripts/lib/render.mjs +166 -12
  39. package/assets/skills/explainer-kit/scripts/render-qa.mjs +147 -2
  40. package/assets/skills/explainer-kit/scripts/run.mjs +796 -272
  41. package/assets/skills/explainer-kit/templates/deck-shell.html +25 -5
  42. package/assets/skills/explainer-kit/templates/diagram-shell.html +29 -7
  43. package/assets/skills/explainer-kit/templates/engineer-tour.html +133 -9
  44. package/assets/skills/explainer-kit/templates/house-style.html +82 -0
  45. package/assets/skills/oat-explainer-kit/SKILL.md +16 -9
  46. package/assets/skills/oat-explainer-kit/references/author-callback.md +51 -0
  47. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +10 -8
  48. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +14 -0
  49. package/assets/skills/oat-explainer-kit/scripts/run.mjs +7 -11
  50. package/assets/skills/oat-project-complete/SKILL.md +18 -2
  51. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  52. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +7 -1
  53. package/assets/skills/oat-wave-execute/SKILL.md +12 -19
  54. package/assets/skills/oat-wave-program/SKILL.md +12 -13
  55. package/assets/skills/subagent-orchestration/SKILL.md +1 -1
  56. package/assets/skills/subagent-orchestration/references/evidence-and-refresh.md +153 -15
  57. package/assets/skills/subagent-orchestration/references/model-selection-principles.md +75 -3
  58. package/assets/skills/subagent-orchestration/references/provider-claude.md +95 -35
  59. package/assets/skills/subagent-orchestration/references/provider-codex.md +36 -10
  60. package/assets/skills/subagent-orchestration/references/provider-cursor.md +91 -15
  61. package/dist/providers/cursor/codec/catalog.d.ts +19 -1
  62. package/dist/providers/cursor/codec/catalog.d.ts.map +1 -1
  63. package/dist/providers/cursor/codec/catalog.js +54 -0
  64. package/package.json +2 -2
  65. package/assets/skills/explainer-kit/schemas/author-request.schema.json +0 -85
  66. package/assets/skills/explainer-kit/schemas/author-result.schema.json +0 -65
@@ -0,0 +1,680 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const ALLOWED_ELEMENTS = new Set([
4
+ 'a',
5
+ 'abbr',
6
+ 'address',
7
+ 'article',
8
+ 'aside',
9
+ 'audio',
10
+ 'b',
11
+ 'bdi',
12
+ 'bdo',
13
+ 'blockquote',
14
+ 'body',
15
+ 'br',
16
+ 'button',
17
+ 'caption',
18
+ 'cite',
19
+ 'code',
20
+ 'col',
21
+ 'colgroup',
22
+ 'data',
23
+ 'dd',
24
+ 'del',
25
+ 'details',
26
+ 'dfn',
27
+ 'dialog',
28
+ 'div',
29
+ 'dl',
30
+ 'dt',
31
+ 'em',
32
+ 'fieldset',
33
+ 'figcaption',
34
+ 'figure',
35
+ 'footer',
36
+ 'h1',
37
+ 'h2',
38
+ 'h3',
39
+ 'h4',
40
+ 'h5',
41
+ 'h6',
42
+ 'head',
43
+ 'header',
44
+ 'hgroup',
45
+ 'hr',
46
+ 'html',
47
+ 'i',
48
+ 'img',
49
+ 'input',
50
+ 'kbd',
51
+ 'label',
52
+ 'legend',
53
+ 'li',
54
+ 'main',
55
+ 'mark',
56
+ 'menu',
57
+ 'meta',
58
+ 'meter',
59
+ 'nav',
60
+ 'noscript',
61
+ 'ol',
62
+ 'optgroup',
63
+ 'option',
64
+ 'output',
65
+ 'p',
66
+ 'picture',
67
+ 'pre',
68
+ 'progress',
69
+ 'q',
70
+ 'rp',
71
+ 'rt',
72
+ 'ruby',
73
+ 's',
74
+ 'samp',
75
+ 'script',
76
+ 'search',
77
+ 'section',
78
+ 'select',
79
+ 'slot',
80
+ 'small',
81
+ 'source',
82
+ 'span',
83
+ 'strong',
84
+ 'style',
85
+ 'sub',
86
+ 'summary',
87
+ 'sup',
88
+ 'table',
89
+ 'tbody',
90
+ 'td',
91
+ 'template',
92
+ 'textarea',
93
+ 'tfoot',
94
+ 'th',
95
+ 'thead',
96
+ 'time',
97
+ 'title',
98
+ 'tr',
99
+ 'u',
100
+ 'ul',
101
+ 'var',
102
+ 'video',
103
+ 'wbr',
104
+ // Inline SVG used by both artistic paths.
105
+ 'animate',
106
+ 'animatemotion',
107
+ 'animatetransform',
108
+ 'circle',
109
+ 'clippath',
110
+ 'defs',
111
+ 'desc',
112
+ 'ellipse',
113
+ 'feblend',
114
+ 'fecolormatrix',
115
+ 'fecomponenttransfer',
116
+ 'fecomposite',
117
+ 'feconvolvematrix',
118
+ 'fediffuselighting',
119
+ 'fedisplacementmap',
120
+ 'fedistantlight',
121
+ 'fedropshadow',
122
+ 'feflood',
123
+ 'fefunca',
124
+ 'fefuncb',
125
+ 'fefuncg',
126
+ 'fefuncr',
127
+ 'fegaussianblur',
128
+ 'feimage',
129
+ 'femerge',
130
+ 'femergenode',
131
+ 'femorphology',
132
+ 'feoffset',
133
+ 'fepointlight',
134
+ 'fespecularlighting',
135
+ 'fespotlight',
136
+ 'fetile',
137
+ 'feturbulence',
138
+ 'filter',
139
+ 'foreignobject',
140
+ 'g',
141
+ 'image',
142
+ 'line',
143
+ 'lineargradient',
144
+ 'marker',
145
+ 'mask',
146
+ 'metadata',
147
+ 'mpath',
148
+ 'path',
149
+ 'pattern',
150
+ 'polygon',
151
+ 'polyline',
152
+ 'radialgradient',
153
+ 'rect',
154
+ 'set',
155
+ 'stop',
156
+ 'svg',
157
+ 'switch',
158
+ 'symbol',
159
+ 'text',
160
+ 'textpath',
161
+ 'tspan',
162
+ 'use',
163
+ 'view',
164
+ ]);
165
+
166
+ const BLOCKED_ACTIVE_ELEMENTS = new Set([
167
+ 'applet',
168
+ 'base',
169
+ 'embed',
170
+ 'frame',
171
+ 'frameset',
172
+ 'iframe',
173
+ 'object',
174
+ ]);
175
+ const URL_ATTRIBUTES = new Set([
176
+ 'action',
177
+ 'cite',
178
+ 'data',
179
+ 'formaction',
180
+ 'href',
181
+ 'ping',
182
+ 'poster',
183
+ 'src',
184
+ 'srcset',
185
+ 'xlink:href',
186
+ ]);
187
+ // Every element that can pull a subresource into the document, including the
188
+ // SVG reference elements that resolve `href`/`xlink:href` against another
189
+ // document.
190
+ const RESOURCE_ELEMENTS = new Set([
191
+ 'animate',
192
+ 'animatemotion',
193
+ 'animatetransform',
194
+ 'audio',
195
+ 'clippath',
196
+ 'embed',
197
+ 'feimage',
198
+ 'filter',
199
+ 'frame',
200
+ 'iframe',
201
+ 'image',
202
+ 'img',
203
+ 'lineargradient',
204
+ 'link',
205
+ 'marker',
206
+ 'mask',
207
+ 'mpath',
208
+ 'object',
209
+ 'pattern',
210
+ 'radialgradient',
211
+ 'script',
212
+ 'set',
213
+ 'source',
214
+ 'textpath',
215
+ 'track',
216
+ 'use',
217
+ 'video',
218
+ 'view',
219
+ ]);
220
+ // Submission targets never have a legitimate use in a self-contained artifact,
221
+ // so they are rejected under every scheme rather than only when external.
222
+ const SUBMISSION_ATTRIBUTES = new Set(['action', 'formaction', 'ping']);
223
+ const DANGEROUS_SCHEMES = [
224
+ 'javascript:',
225
+ 'vbscript:',
226
+ 'file:',
227
+ 'data:text/html',
228
+ 'data:image/svg+xml',
229
+ ];
230
+ const REQUIRED_THEME_TOKENS = [
231
+ '--canvas',
232
+ '--panel',
233
+ '--border',
234
+ '--ink',
235
+ '--muted',
236
+ '--accent',
237
+ '--sans',
238
+ '--mono',
239
+ ];
240
+ const REQUIRED_ANCHORS = {
241
+ 'deck-shell': [
242
+ ['class', 'deck'],
243
+ ['class', 'deck-controls'],
244
+ ['id', 'deck-counter'],
245
+ ['class', 'deck-progress'],
246
+ ['id', 'deck-progress'],
247
+ ['class', 'deck-progress__bar'],
248
+ ],
249
+ 'diagram-shell': [
250
+ ['id', 'diagram-title'],
251
+ ['id', 'diagram-description'],
252
+ ['class', 'diagram-shell'],
253
+ ['class', 'zoom-controls'],
254
+ ['class', 'diagram-viewport'],
255
+ ['class', 'diagram-canvas'],
256
+ ['class', 'legend'],
257
+ ],
258
+ 'engineer-tour': [
259
+ ['class', 'layout'],
260
+ ['class', 'toc'],
261
+ ['class', 'tour-body'],
262
+ ['class', 'diagram-rail'],
263
+ ['class', 'diagram-card'],
264
+ ],
265
+ };
266
+
267
+ export function validateHtmlSafety({ html, shell, shellName } = {}) {
268
+ if (typeof html !== 'string' || typeof shell !== 'string') {
269
+ return {
270
+ valid: false,
271
+ errors: ['invalid-html-safety-input'],
272
+ warnings: [],
273
+ };
274
+ }
275
+
276
+ const authored = tokenizeHtml(html);
277
+ const core = tokenizeHtml(shell);
278
+ const errors = new Set();
279
+
280
+ if (authored.errors.length > 0) errors.add('malformed-html');
281
+ if (core.errors.length > 0) errors.add('invalid-core-shell');
282
+
283
+ inspectElements(authored, errors);
284
+ compareCoreScripts(core.scripts, authored.scripts, errors);
285
+
286
+ const resolvedShellName =
287
+ normalizeShellName(shellName) ?? inferShellName(core);
288
+ const warnings = styleWarnings(authored, resolvedShellName);
289
+
290
+ return {
291
+ valid: errors.size === 0,
292
+ errors: [...errors],
293
+ warnings,
294
+ };
295
+ }
296
+
297
+ export function coreScriptHashes(shell) {
298
+ if (typeof shell !== 'string') return [];
299
+ return tokenizeHtml(shell).scripts.map(hash);
300
+ }
301
+
302
+ function inspectElements(document, errors) {
303
+ for (const element of document.elements) {
304
+ if (
305
+ BLOCKED_ACTIVE_ELEMENTS.has(element.name) ||
306
+ !ALLOWED_ELEMENTS.has(element.name)
307
+ ) {
308
+ errors.add(`disallowed-element:${element.name}`);
309
+ }
310
+
311
+ for (const attribute of element.attributes) {
312
+ if (attribute.name.startsWith('on')) {
313
+ errors.add('inline-event-handler');
314
+ }
315
+ if (attribute.name === 'srcdoc') {
316
+ errors.add('external-active-content');
317
+ }
318
+ if (attribute.name === 'style' && containsExternalCss(attribute.value)) {
319
+ errors.add('external-active-content');
320
+ }
321
+ if (
322
+ URL_ATTRIBUTES.has(attribute.name) &&
323
+ isUnsafeUrl(attribute.value, element.name, attribute.name)
324
+ ) {
325
+ errors.add('external-active-content');
326
+ }
327
+ }
328
+
329
+ if (
330
+ element.name === 'meta' &&
331
+ attributeValue(element, 'http-equiv')?.toLowerCase() === 'refresh'
332
+ ) {
333
+ errors.add('external-active-content');
334
+ }
335
+ }
336
+
337
+ for (const css of document.styles) {
338
+ if (containsExternalCss(css)) errors.add('external-active-content');
339
+ }
340
+ }
341
+
342
+ function compareCoreScripts(expected, actual, errors) {
343
+ if (expected.length !== actual.length) {
344
+ errors.add('core-script-count-mismatch');
345
+ }
346
+
347
+ const length = Math.min(expected.length, actual.length);
348
+ for (let index = 0; index < length; index += 1) {
349
+ if (hash(expected[index]) !== hash(actual[index])) {
350
+ errors.add(`core-script-hash-mismatch:${index}`);
351
+ }
352
+ }
353
+ }
354
+
355
+ function styleWarnings(document, shellName) {
356
+ const warnings = [];
357
+ const styleText = document.styles.join('\n');
358
+
359
+ for (const token of REQUIRED_THEME_TOKENS) {
360
+ if (!new RegExp(`${escapeRegExp(token)}\\s*:`).test(styleText)) {
361
+ warnings.push(`missing-theme-token:${token}`);
362
+ }
363
+ }
364
+
365
+ for (const [attribute, value] of REQUIRED_ANCHORS[shellName] ?? []) {
366
+ const present = document.elements.some((element) => {
367
+ const candidate = attributeValue(element, attribute);
368
+ return attribute === 'class'
369
+ ? candidate?.split(/\s+/).includes(value)
370
+ : candidate === value;
371
+ });
372
+ if (!present) warnings.push(`missing-required-anchor:${value}`);
373
+ }
374
+
375
+ return warnings;
376
+ }
377
+
378
+ function tokenizeHtml(html) {
379
+ const elements = [];
380
+ const errors = [];
381
+ const scripts = [];
382
+ const styles = [];
383
+ let cursor = 0;
384
+
385
+ while (cursor < html.length) {
386
+ const opening = html.indexOf('<', cursor);
387
+ if (opening === -1) break;
388
+
389
+ if (html.startsWith('<!--', opening)) {
390
+ const end = html.indexOf('-->', opening + 4);
391
+ if (end === -1) {
392
+ errors.push('unclosed-comment');
393
+ break;
394
+ }
395
+ cursor = end + 3;
396
+ continue;
397
+ }
398
+
399
+ if (html[opening + 1] === '!' || html[opening + 1] === '?') {
400
+ const end = findTagEnd(html, opening + 2);
401
+ if (end === -1) {
402
+ errors.push('unclosed-declaration');
403
+ break;
404
+ }
405
+ cursor = end + 1;
406
+ continue;
407
+ }
408
+
409
+ if (html[opening + 1] === '/') {
410
+ const end = findTagEnd(html, opening + 2);
411
+ if (end === -1) {
412
+ errors.push('unclosed-end-tag');
413
+ break;
414
+ }
415
+ cursor = end + 1;
416
+ continue;
417
+ }
418
+
419
+ const end = findTagEnd(html, opening + 1);
420
+ if (end === -1) {
421
+ errors.push('unclosed-start-tag');
422
+ break;
423
+ }
424
+
425
+ const rawTag = html.slice(opening, end + 1);
426
+ const match = rawTag.match(/^<\s*([^\s/>]+)/);
427
+ if (!match) {
428
+ errors.push('invalid-start-tag');
429
+ cursor = end + 1;
430
+ continue;
431
+ }
432
+
433
+ const name = match[1].toLowerCase();
434
+ const attributeSource = rawTag.slice(match[0].length, -1);
435
+ const parsedAttributes = parseAttributes(attributeSource);
436
+ if (parsedAttributes.error) errors.push(parsedAttributes.error);
437
+ elements.push({ name, attributes: parsedAttributes.attributes });
438
+
439
+ if ((name === 'script' || name === 'style') && !/\/\s*>$/.test(rawTag)) {
440
+ const closing = findRawTextClosingTag(html, end + 1, name);
441
+ if (!closing) {
442
+ errors.push(`unclosed-${name}`);
443
+ break;
444
+ }
445
+ const body = html.slice(end + 1, closing.start);
446
+ const rawElement = html.slice(opening, closing.end);
447
+ if (name === 'script') scripts.push(rawElement);
448
+ else styles.push(body);
449
+ cursor = closing.end;
450
+ continue;
451
+ }
452
+
453
+ cursor = end + 1;
454
+ }
455
+
456
+ return { elements, errors, scripts, styles };
457
+ }
458
+
459
+ function findTagEnd(html, start) {
460
+ let quote = null;
461
+ for (let index = start; index < html.length; index += 1) {
462
+ const character = html[index];
463
+ if (quote) {
464
+ if (character === quote) quote = null;
465
+ } else if (character === '"' || character === "'") {
466
+ quote = character;
467
+ } else if (character === '>') {
468
+ return index;
469
+ }
470
+ }
471
+ return -1;
472
+ }
473
+
474
+ function findRawTextClosingTag(html, start, name) {
475
+ const pattern = new RegExp(`<\\/\\s*${name}\\s*>`, 'gi');
476
+ pattern.lastIndex = start;
477
+ const match = pattern.exec(html);
478
+ return match ? { start: match.index, end: pattern.lastIndex } : null;
479
+ }
480
+
481
+ function parseAttributes(source) {
482
+ const attributes = [];
483
+ let cursor = 0;
484
+
485
+ while (cursor < source.length) {
486
+ while (/\s/.test(source[cursor])) cursor += 1;
487
+ if (cursor >= source.length) break;
488
+ if (source[cursor] === '/') {
489
+ cursor += 1;
490
+ continue;
491
+ }
492
+
493
+ const nameStart = cursor;
494
+ while (cursor < source.length && !/[\s=/>]/.test(source[cursor])) {
495
+ cursor += 1;
496
+ }
497
+ if (nameStart === cursor) {
498
+ return { attributes, error: 'invalid-attribute' };
499
+ }
500
+
501
+ const name = source.slice(nameStart, cursor).toLowerCase();
502
+ while (/\s/.test(source[cursor])) cursor += 1;
503
+ let value = '';
504
+
505
+ if (source[cursor] === '=') {
506
+ cursor += 1;
507
+ while (/\s/.test(source[cursor])) cursor += 1;
508
+ const quote = source[cursor];
509
+ if (quote === '"' || quote === "'") {
510
+ cursor += 1;
511
+ const valueStart = cursor;
512
+ while (cursor < source.length && source[cursor] !== quote) cursor += 1;
513
+ if (cursor >= source.length) {
514
+ return { attributes, error: 'unclosed-attribute-value' };
515
+ }
516
+ value = source.slice(valueStart, cursor);
517
+ cursor += 1;
518
+ } else {
519
+ const valueStart = cursor;
520
+ while (cursor < source.length && !/[\s>]/.test(source[cursor])) {
521
+ cursor += 1;
522
+ }
523
+ value = source.slice(valueStart, cursor);
524
+ }
525
+ }
526
+
527
+ attributes.push({ name, value: decodeHtmlReferences(value) });
528
+ }
529
+
530
+ return { attributes, error: null };
531
+ }
532
+
533
+ function isUnsafeUrl(value, elementName, attributeName) {
534
+ if (
535
+ DANGEROUS_SCHEMES.some((scheme) => compactUrl(value).startsWith(scheme))
536
+ ) {
537
+ return true;
538
+ }
539
+ if (SUBMISSION_ATTRIBUTES.has(attributeName)) return true;
540
+ return isUnpinnedResourceRef(value, elementName, attributeName);
541
+ }
542
+
543
+ // A resource element may only reference payloads that travel with the
544
+ // document: an allowed inline `data:` payload or a same-document fragment.
545
+ function isUnpinnedResourceRef(value, elementName, attributeName) {
546
+ if (
547
+ !RESOURCE_ELEMENTS.has(elementName) ||
548
+ !URL_ATTRIBUTES.has(attributeName)
549
+ ) {
550
+ return false;
551
+ }
552
+ const candidates =
553
+ attributeName === 'srcset' ? srcsetCandidates(value) : [value];
554
+ return candidates.length === 0 || !candidates.every(isSelfContainedRef);
555
+ }
556
+
557
+ function isSelfContainedRef(value) {
558
+ const compact = compactUrl(value);
559
+ if (compact.startsWith('#')) return true;
560
+ return (
561
+ compact.startsWith('data:') &&
562
+ !DANGEROUS_SCHEMES.some((scheme) => compact.startsWith(scheme))
563
+ );
564
+ }
565
+
566
+ // `srcset` candidates are whitespace-delimited URL/descriptor pairs separated
567
+ // by commas, and a `data:` URL may itself contain commas, so candidates are
568
+ // split on the trailing comma of a token rather than on every comma.
569
+ function srcsetCandidates(value) {
570
+ const urls = [];
571
+ let expectUrl = true;
572
+ for (const token of value.split(/\s+/).filter(Boolean)) {
573
+ const terminated = token.endsWith(',');
574
+ if (expectUrl) {
575
+ urls.push(terminated ? token.slice(0, -1) : token);
576
+ }
577
+ expectUrl = terminated;
578
+ }
579
+ return urls.filter(Boolean);
580
+ }
581
+
582
+ function compactUrl(value) {
583
+ return value.replace(/[\u0000-\u0020\u007f]+/g, '').toLowerCase();
584
+ }
585
+
586
+ // Shared with the render QA structural check so both call sites enforce one
587
+ // self-contained-resource policy.
588
+ export function findUnpinnedResourceRefs(html) {
589
+ if (typeof html !== 'string') return [];
590
+ const refs = [];
591
+ for (const element of tokenizeHtml(html).elements) {
592
+ for (const attribute of element.attributes) {
593
+ if (
594
+ isUnpinnedResourceRef(attribute.value, element.name, attribute.name)
595
+ ) {
596
+ refs.push({
597
+ element: element.name,
598
+ attribute: attribute.name,
599
+ value: attribute.value,
600
+ });
601
+ }
602
+ }
603
+ }
604
+ return refs;
605
+ }
606
+
607
+ function containsExternalCss(value) {
608
+ const normalized = value
609
+ .replace(/\/\*[\s\S]*?\*\//g, '')
610
+ .replace(/\\([\da-f]{1,6})\s?/gi, (_, digits) =>
611
+ decodeCodePoint(Number.parseInt(digits, 16)),
612
+ )
613
+ .replace(/\\([^\r\n\f])/g, '$1');
614
+ return (
615
+ /@import\b/i.test(normalized) ||
616
+ /url\(\s*(['"]?)(?:(?:https?:)?\/\/|data:text\/html|data:image\/svg\+xml)/i.test(
617
+ normalized,
618
+ )
619
+ );
620
+ }
621
+
622
+ function decodeHtmlReferences(value) {
623
+ const named = {
624
+ amp: '&',
625
+ colon: ':',
626
+ newline: '\n',
627
+ quot: '"',
628
+ tab: '\t',
629
+ };
630
+ return value.replace(
631
+ /&(?:#(\d+)|#x([\da-f]+)|([a-z]+));?/gi,
632
+ (reference, decimal, hexadecimal, name) => {
633
+ if (decimal) return decodeCodePoint(Number(decimal));
634
+ if (hexadecimal) return decodeCodePoint(Number.parseInt(hexadecimal, 16));
635
+ return named[name.toLowerCase()] ?? reference;
636
+ },
637
+ );
638
+ }
639
+
640
+ function decodeCodePoint(value) {
641
+ if (
642
+ !Number.isInteger(value) ||
643
+ value < 0 ||
644
+ value > 0x10ffff ||
645
+ (value >= 0xd800 && value <= 0xdfff)
646
+ ) {
647
+ return '\uFFFD';
648
+ }
649
+ return String.fromCodePoint(value);
650
+ }
651
+
652
+ function attributeValue(element, name) {
653
+ return element.attributes.find((attribute) => attribute.name === name)?.value;
654
+ }
655
+
656
+ function normalizeShellName(shellName) {
657
+ if (typeof shellName !== 'string') return null;
658
+ const normalized = shellName.replace(/\.html$/i, '');
659
+ return REQUIRED_ANCHORS[normalized] ? normalized : null;
660
+ }
661
+
662
+ function inferShellName(document) {
663
+ const classes = new Set(
664
+ document.elements.flatMap(
665
+ (element) => attributeValue(element, 'class')?.split(/\s+/) ?? [],
666
+ ),
667
+ );
668
+ if (classes.has('deck')) return 'deck-shell';
669
+ if (classes.has('diagram-shell')) return 'diagram-shell';
670
+ if (classes.has('layout') && classes.has('tour-body')) return 'engineer-tour';
671
+ return null;
672
+ }
673
+
674
+ function hash(value) {
675
+ return createHash('sha256').update(value).digest('hex');
676
+ }
677
+
678
+ function escapeRegExp(value) {
679
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
680
+ }