@adia-ai/adia-ui-forge 0.8.59 → 0.8.61

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 (26) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +68 -0
  4. package/package.json +1 -1
  5. package/plugin.yaml +1 -1
  6. package/references/contracts/a2ui-mcp-surface.md +1 -1
  7. package/scripts/lint-rules.generated.mjs +2418 -359
  8. package/skills/a2ui-maintenance/references/eval-diagnostics.md +2 -2
  9. package/skills/a2ui-maintenance/references/pipeline-overview.md +26 -0
  10. package/skills/a2ui-maintenance/references/strategy-engines.md +12 -0
  11. package/skills/demo-audit/references/admin-shell-anatomy.md +18 -12
  12. package/skills/gen-ui-review/scripts/gen-review-decompose.mjs +27 -34
  13. package/skills/gen-ui-review/scripts/overflow-detect.generated.mjs +101 -0
  14. package/skills/package-release/references/cut-procedure.md +49 -5
  15. package/skills/package-release/references/recovery-paths.md +20 -0
  16. package/skills/package-release/scripts/bump.mjs +36 -6
  17. package/skills/package-release/scripts/dispatch-publish.mjs +8 -0
  18. package/skills/package-release/scripts/gate-roster.mjs +4 -4
  19. package/skills/package-release/scripts/release-pack.mjs +485 -68
  20. package/skills/package-release/scripts/stale-copy-warning.mjs +215 -0
  21. package/skills/package-release/scripts/tag-lockstep.mjs +8 -0
  22. package/skills/primitive-authoring/references/INDEX.md +1 -0
  23. package/skills/primitive-authoring/references/examples-structure.md +52 -0
  24. package/skills/primitive-authoring/references/shell-patterns.md +12 -11
  25. package/skills/primitive-authoring/references/token-contract.md +2 -2
  26. package/skills/primitive-authoring/references/yaml-contract.md +106 -9
@@ -1,5 +1,5 @@
1
- // GENERATED do not hand-edit. Sources:
2
- // scripts/lint/engine/primitives.mjs, scripts/lint/engine/run.mjs
1
+ // GENERATED, do not hand-edit. Sources:
2
+ // scripts/lint/engine/primitives.mjs, scripts/lint/engine/run.mjs, scripts/lint/engine/baseline.mjs
3
3
  // scripts/lint/rules/consumer/genui-doc.mjs
4
4
  // scripts/lint/rules/consumer/hardcoded-open.mjs
5
5
  // scripts/lint/rules/consumer/llm-key-in-client.mjs
@@ -13,11 +13,16 @@
13
13
  // scripts/lint/rules/generated/composition/action-list.mjs
14
14
  // scripts/lint/rules/generated/composition/admin-command.mjs
15
15
  // scripts/lint/rules/generated/composition/admin-content.mjs
16
- // scripts/lint/rules/generated/composition/admin-scroll.mjs
16
+ // scripts/lint/rules/generated/composition/admin-entity-item.mjs
17
+ // scripts/lint/rules/generated/composition/admin-roster.mjs
18
+ // scripts/lint/rules/generated/composition/admin-settings.mjs
17
19
  // scripts/lint/rules/generated/composition/admin-shell.mjs
18
20
  // scripts/lint/rules/generated/composition/admin-sidebar.mjs
19
21
  // scripts/lint/rules/generated/composition/admin-statusbar.mjs
20
22
  // scripts/lint/rules/generated/composition/admin-topbar.mjs
23
+ // scripts/lint/rules/generated/composition/agent-admin.mjs
24
+ // scripts/lint/rules/generated/composition/avatar-group.mjs
25
+ // scripts/lint/rules/generated/composition/chat-composer.mjs
21
26
  // scripts/lint/rules/generated/composition/chat-empty.mjs
22
27
  // scripts/lint/rules/generated/composition/chat-header.mjs
23
28
  // scripts/lint/rules/generated/composition/chat-shell.mjs
@@ -34,18 +39,30 @@
34
39
  // scripts/lint/rules/generated/composition/editor-sidebar.mjs
35
40
  // scripts/lint/rules/generated/composition/editor-statusbar.mjs
36
41
  // scripts/lint/rules/generated/composition/editor-toolbar.mjs
42
+ // scripts/lint/rules/generated/composition/embed-shell.mjs
37
43
  // scripts/lint/rules/generated/composition/feed-item.mjs
38
44
  // scripts/lint/rules/generated/composition/feed.mjs
45
+ // scripts/lint/rules/generated/composition/fields.mjs
46
+ // scripts/lint/rules/generated/composition/footer.mjs
47
+ // scripts/lint/rules/generated/composition/form-popover.mjs
48
+ // scripts/lint/rules/generated/composition/header.mjs
39
49
  // scripts/lint/rules/generated/composition/list-item.mjs
40
50
  // scripts/lint/rules/generated/composition/menu-divider.mjs
41
51
  // scripts/lint/rules/generated/composition/menu-item.mjs
42
52
  // scripts/lint/rules/generated/composition/menu-label.mjs
53
+ // scripts/lint/rules/generated/composition/menu.mjs
54
+ // scripts/lint/rules/generated/composition/nav-group.mjs
55
+ // scripts/lint/rules/generated/composition/nav-item.mjs
43
56
  // scripts/lint/rules/generated/composition/page-scroll.mjs
57
+ // scripts/lint/rules/generated/composition/radio-group.mjs
58
+ // scripts/lint/rules/generated/composition/search.mjs
59
+ // scripts/lint/rules/generated/composition/section.mjs
44
60
  // scripts/lint/rules/generated/composition/segment.mjs
45
61
  // scripts/lint/rules/generated/composition/segmented.mjs
46
62
  // scripts/lint/rules/generated/composition/simple-content.mjs
47
63
  // scripts/lint/rules/generated/composition/simple-hero.mjs
48
64
  // scripts/lint/rules/generated/composition/simple-shell.mjs
65
+ // scripts/lint/rules/generated/composition/skip-nav.mjs
49
66
  // scripts/lint/rules/generated/composition/stepper-item.mjs
50
67
  // scripts/lint/rules/generated/composition/stepper.mjs
51
68
  // scripts/lint/rules/generated/composition/tab.mjs
@@ -61,6 +78,7 @@
61
78
  // scripts/lint/rules/generated/lab-posture/internal-tier.mjs
62
79
  // scripts/lint/rules/generated/lab-posture/required-attr.mjs
63
80
  // scripts/lint/rules/generated/lab-posture/unknown-attr.mjs
81
+ // scripts/lint/rules/generated/lab-posture/unknown-slot.mjs
64
82
  // scripts/lint/rules/generated/lab-posture/unknown-tag.mjs
65
83
  // scripts/lint/rules/genui-output/anti-patterns.mjs
66
84
  // scripts/lint/rules/shared/attr-typo.mjs
@@ -69,8 +87,10 @@
69
87
  // scripts/lint/rules/shared/dead-font-token.mjs
70
88
  // scripts/lint/rules/shared/example-id-grammar.mjs
71
89
  // scripts/lint/rules/shared/example-prop-contract.mjs
90
+ // scripts/lint/rules/shared/img-dimension-reserve.mjs
72
91
  // scripts/lint/rules/shared/legacy-shell.mjs
73
92
  // scripts/lint/rules/shared/native-primitive.mjs
93
+ // scripts/lint/rules/shared/no-local-escape-html.mjs
74
94
  // scripts/lint/rules/shared/raw-color.mjs
75
95
  // scripts/lint/rules/shared/raw-px.mjs
76
96
  // scripts/lint/rules/shared/scope-extent.mjs
@@ -198,8 +218,24 @@ function lineOf(text, offset) {
198
218
  return n;
199
219
  }
200
220
 
201
- /** Suppression comment recognized on the line itself or the line before a finding. */
202
- const SUPPRESS_RE = /\/\*\s*adia-lint-disable\s+([A-Z0-9-]+)(?:\s*—\s*(.*?))?\s*\*\//;
221
+ /**
222
+ * Suppression comment recognized on the line itself or the line before a finding.
223
+ *
224
+ * gh#3715: the reason separator accepts an em dash, a colon, or a
225
+ * space-padded hyphen. It used to accept ONLY the em dash, which collided
226
+ * head-on with `check:em-dash-added-lines` (gh#3767): that gate forbids an
227
+ * em dash on any added line, so obeying it made a reasoned suppression
228
+ * impossible to write. Worse, the failure was silent rather than loud. A
229
+ * hyphen did not merely lose the reason, it made the WHOLE directive fail
230
+ * to match, so the suppression quietly did nothing and the finding stayed
231
+ * live with no diagnostic anywhere.
232
+ *
233
+ * The hyphen form requires whitespace on both sides. Rule ids contain
234
+ * hyphens themselves (SHELL-RESIZE), and the id class is greedy, so a bare
235
+ * `-` with no padding would be ambiguous with the id it follows.
236
+ */
237
+ const SUPPRESS_RE =
238
+ /\/\*\s*adia-lint-disable\s+([A-Z0-9-]+)(?:(?:\s*[\u2014:]\s*|\s+-\s+)(.*?))?\s*\*\//;
203
239
 
204
240
  /** Given all raw lines and a finding's line number, is it suppressed, and was a reason given? */
205
241
  function suppressionFor(rawLines, lineNo, ruleId) {
@@ -230,6 +266,47 @@ function allSuppressions(rawLines) {
230
266
  return out;
231
267
  }
232
268
 
269
+ /**
270
+ * Loose match for anything that READS as a suppression-directive comment:
271
+ * opens `/* adia-lint-disable`, closes on the same line, regardless of
272
+ * whether its body satisfies SUPPRESS_RE's own strict grammar. Deliberately
273
+ * permissive: the only anchor is the shared prefix both regexes start on,
274
+ * so any separator/id/reason shape SUPPRESS_RE would refuse still gets
275
+ * caught here.
276
+ */
277
+ const LOOSE_SUPPRESS_RE = /\/\*\s*adia-lint-disable\b.*?\*\//g;
278
+
279
+ /**
280
+ * gh#4161: SUPPRESS_RE is both matcher and detector, so a directive it
281
+ * refuses produces no suppression, no SUPPRESS-NO-REASON, and no
282
+ * diagnostic anywhere, silently invisible. gh#3868 fixed the hyphen-
283
+ * separator case and gh#3871 swept existing inert directives; neither
284
+ * closed the general class. This is the loose scan that closes it: every
285
+ * candidate suppression-directive comment (per LOOSE_SUPPRESS_RE above)
286
+ * that the STRICT regex refuses to parse, one entry per hit, naming the
287
+ * line and a trimmed snippet, so the engine (run.mjs) can turn each into a
288
+ * SUPPRESS-MALFORMED (warn) finding.
289
+ *
290
+ * `strictRe` is injectable (defaults to the live SUPPRESS_RE) so a test can
291
+ * prove this scan against a narrower, superseded grammar too, e.g.
292
+ * gh#3868's own pre-fix, em-dash-only regex, not only today's.
293
+ */
294
+ function malformedSuppressions(rawLines, strictRe = SUPPRESS_RE) {
295
+ const out = [];
296
+ rawLines.forEach((line, idx) => {
297
+ LOOSE_SUPPRESS_RE.lastIndex = 0;
298
+ let m;
299
+ while ((m = LOOSE_SUPPRESS_RE.exec(line))) {
300
+ const candidate = m[0];
301
+ const re = new RegExp(strictRe.source);
302
+ if (!re.test(candidate)) {
303
+ out.push({ lineNo: idx + 1, snippet: candidate.trim().slice(0, 90) });
304
+ }
305
+ }
306
+ });
307
+ return out;
308
+ }
309
+
233
310
  const RE = {
234
311
  HEXCOLOR: /#[0-9a-fA-F]{3,8}\b/,
235
312
  FUNCCOLOR: /\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\s*\(/,
@@ -238,12 +315,17 @@ const RE = {
238
315
  BOOL_TRUE: /\bdefault:\s*true\b/,
239
316
  ATTR_TYPO: /\battr:\s*['"]/,
240
317
  NATIVE_PRIMITIVE: /<(?:button|input|select|textarea|dialog)(?![\w-])/,
241
- LEGACY_SHELL: /data-chat-(?:messages|input|empty|name)|data-editor-body|data-canvas\b|data-sidebar=|data-pane-(?:side|grow)|<aside-ui\b|<dialog\s+data-command/,
318
+ LEGACY_SHELL: /^(?:data-chat-(?:messages|input|empty|name)|data-editor-body|data-canvas|data-sidebar|data-pane-(?:side|grow))$/,
242
319
  PX_GE: /(?<![\w.-])(\d+)px\b/g,
243
320
  SELECTOR_RULE: /^\s*[.#:[&\w][^{}\n]*\{/m,
244
321
  SSR_SIGNAL: /['"]use client['"]|\buseEffect\b|\bonMounted\b|\bonMount\b|from\s+['"](?:react|vue|svelte|next|nuxt|@sveltejs|astro)|getServerSideProps|defineNuxtComponent/i,
245
322
  TOPLEVEL_IMPORT: /^\s*import\s+['"]@adia-ai\/web-components['"]\s*;?\s*$/m,
246
323
  OVERLAY_OPEN: /<(?:modal|drawer)-ui\b[^>]*?(?<![:.\w])\bopen\b(?!\s*=\s*\{)/,
324
+ // gh#3888: a real @scope at-rule opening, never a bare substring match
325
+ // on the word "@scope" (which also matches prose in a comment, the
326
+ // exact loophole this fixes: MISSING-SCOPE's old `text.includes('@scope')`
327
+ // passed any file whose comments merely mentioned the word).
328
+ SCOPE_BLOCK: /@scope\s*\(/,
247
329
  };
248
330
 
249
331
  /** Per-declaration split on `;` — mirrors both Python scripts' `line.split(';')` loops. */
@@ -385,9 +467,18 @@ const VOID_TAGS = new Set([
385
467
  * are flushed as-is) so a small fixture snippet never throws.
386
468
  *
387
469
  * Returns one node per open (or self-closing/void) tag, in document
388
- * order: `{ tag, parentTag, line, childTags }` `parentTag` is `'ROOT'`
389
- * for a top-level occurrence (the A2UI "Surface" case), `childTags` is
390
- * the ordered list of direct-child tag names only (not descendants).
470
+ * order: `{ tag, parentTag, line, childTags }`. `parentTag` is the
471
+ * nearest CUSTOM-ELEMENT ancestor (gh#3310) — the stack is walked from
472
+ * the top down past any native, non-hyphenated wrapper tag (`section`,
473
+ * `div`, `td`, `tr`, `tbody`, ...; the Custom Elements naming
474
+ * requirement means no native HTML tag is ever hyphenated, so "contains
475
+ * a hyphen" is the discriminator, not an allowlist) to the first
476
+ * hyphenated frame, and is `'ROOT'` if the walk empties the stack
477
+ * without finding one (the A2UI "Surface" case). `childTags` is the
478
+ * ordered list of DIRECT children only (not descendants, and not
479
+ * ancestor-walked — `allowedChildren` matching stays direct-children-only),
480
+ * each a `{ tag, slot }` pair (`slot` is `null` for a default-slot child —
481
+ * gh#3308, compositionFindings' own slot-aware allowedChildren check).
391
482
  *
392
483
  * gh#2937: `makeLinter`'s `lintText` calls every active rule's `match()`
393
484
  * with the SAME `text` string for one file — and each of the ~48
@@ -401,16 +492,34 @@ const VOID_TAGS = new Set([
401
492
  * the next file is a single cache miss that recomputes and replaces
402
493
  * the slot — no manual invalidation, no unbounded growth.
403
494
  */
404
- let _tagTreeCache = { text: null, nodes: null };
405
- function tagTree(text) {
406
- if (_tagTreeCache.text === text) return _tagTreeCache.nodes;
495
+ // gh#3310: the Custom Elements spec requires a hyphen in every custom
496
+ // tag name, so "contains a hyphen" is a definitional discriminator for
497
+ // "is a catalog component" — no native HTML tag is ever hyphenated, and
498
+ // no allowlist of "transparent" wrapper tags needs maintaining.
499
+ function nearestCustomElementAncestor(stack) {
500
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
501
+ if (stack[i].tag.includes('-')) return stack[i].tag;
502
+ }
503
+ return 'ROOT';
504
+ }
505
+
506
+ let _tagTreeCache = { text: null, path: null, nodes: null };
507
+ // gh#3492: scans `markupScanSurface(text, path)`'s blanked output, not raw
508
+ // `text`. A `<!-- ... -->` comment naming a real tag in prose (a
509
+ // "Pattern source:" doc header) was previously read as live markup, and
510
+ // the comment's missing closing tag left every REAL element after it
511
+ // folded into a phantom nesting context. Blanking (not deleting) keeps
512
+ // line numbers aligned, same guarantee `tagOccurrences` already relies on.
513
+ function tagTree(text, path = '') {
514
+ if (_tagTreeCache.text === text && _tagTreeCache.path === path) return _tagTreeCache.nodes;
515
+ const surface = markupScanSurface(text, path);
407
516
  const nodes = [];
408
- const stack = []; // { tag, parentTag, line, children: string[] }
517
+ const stack = []; // { tag, parentTag, line, children: {tag, slot}[] }
409
518
  TAG_TOKEN_RE.lastIndex = 0;
410
519
  let m;
411
- while ((m = TAG_TOKEN_RE.exec(text))) {
412
- const [, closing, tag, , selfClose] = m;
413
- const line = lineOf(text, m.index);
520
+ while ((m = TAG_TOKEN_RE.exec(surface))) {
521
+ const [, closing, tag, attrString, selfClose] = m;
522
+ const line = lineOf(surface, m.index);
414
523
  if (closing) {
415
524
  let at = -1;
416
525
  for (let i = stack.length - 1; i >= 0; i -= 1) {
@@ -422,8 +531,8 @@ function tagTree(text) {
422
531
  stack.length = at; // pop this frame and any unclosed frames above it
423
532
  continue;
424
533
  }
425
- const parentTag = stack.length ? stack[stack.length - 1].tag : 'ROOT';
426
- if (stack.length) stack[stack.length - 1].children.push(tag);
534
+ const parentTag = nearestCustomElementAncestor(stack);
535
+ if (stack.length) stack[stack.length - 1].children.push({ tag, slot: parseAttrs(attrString).slot ?? null });
427
536
  if (selfClose || VOID_TAGS.has(tag.toLowerCase())) {
428
537
  nodes.push({ tag, parentTag, line, childTags: [] });
429
538
  } else {
@@ -434,7 +543,7 @@ function tagTree(text) {
434
543
  const frame = stack.pop();
435
544
  nodes.push({ tag: frame.tag, parentTag: frame.parentTag, line: frame.line, childTags: frame.children });
436
545
  }
437
- _tagTreeCache = { text, nodes };
546
+ _tagTreeCache = { text, path, nodes };
438
547
  return nodes;
439
548
  }
440
549
 
@@ -446,10 +555,26 @@ function tagTree(text) {
446
555
  * string/tree matcher with no registry lookup at match-time, same as
447
556
  * every other rule). The reserved catalog name `'Surface'` in
448
557
  * `allowedParents` matches a `parentTag === 'ROOT'` occurrence.
558
+ *
559
+ * gh#3308: `allowedChildren` applies to default-slot children only — a
560
+ * child carrying ANY `slot=` attribute (menu-ui's `slot=trigger`,
561
+ * context-menu-ui's wrap-mode target) is a sibling-level named slot whose
562
+ * content is deliberately unconstrained by the component's own source, not
563
+ * a default-slot member subject to the roster check. `allowedChildrenBySlot`
564
+ * is the opt-in per-slot roster for the rare case a named slot DOES want
565
+ * constraining — a slot absent from that map (or the map itself absent)
566
+ * stays exempt by design, same as an unconstrained default slot.
567
+ *
568
+ * gh#3492: `path` is optional and defaults to `''`. Every current caller
569
+ * (the generated `COMPOSITION-*` rules) has no path to give, and doesn't
570
+ * need one: `tagTree()`'s HTML-comment blanking runs unconditionally
571
+ * regardless of path, only its `.vue`-template scoping depends on it.
449
572
  */
450
- function compositionFindings(text, { tag, allowedParents, allowedChildren }) {
573
+ function compositionFindings(text, { tag, allowedParents, allowedChildren, allowedChildrenBySlot, path = '' }) {
451
574
  const out = [];
452
- for (const node of tagTree(text)) {
575
+ const hasChildRule = (allowedChildren && allowedChildren.length)
576
+ || (allowedChildrenBySlot && Object.keys(allowedChildrenBySlot).length);
577
+ for (const node of tagTree(text, path)) {
453
578
  if (node.tag !== tag) continue;
454
579
  if (allowedParents && allowedParents.length) {
455
580
  const parentOk = node.parentTag === 'ROOT'
@@ -464,13 +589,26 @@ function compositionFindings(text, { tag, allowedParents, allowedChildren }) {
464
589
  });
465
590
  }
466
591
  }
467
- if (allowedChildren && allowedChildren.length) {
592
+ if (hasChildRule) {
468
593
  for (const child of node.childTags) {
469
- if (!allowedChildren.includes(child)) {
594
+ if (child.slot) {
595
+ const slotAllowed = allowedChildrenBySlot && allowedChildrenBySlot[child.slot];
596
+ if (!slotAllowed) continue; // named slot left unconstrained by design
597
+ if (!slotAllowed.includes(child.tag)) {
598
+ out.push({
599
+ line: node.line,
600
+ snippet: `<${tag} slot="${child.slot}"> > <${child.tag}>`,
601
+ why: `<${tag}> declares a2ui.allowedChildrenBySlot.${child.slot}: [${slotAllowed.join(', ')}] — <${child.tag}> is not one of them`,
602
+ });
603
+ }
604
+ continue;
605
+ }
606
+ if (!allowedChildren || !allowedChildren.length) continue; // default slot unconstrained
607
+ if (!allowedChildren.includes(child.tag)) {
470
608
  out.push({
471
609
  line: node.line,
472
- snippet: `<${tag}> > <${child}>`,
473
- why: `<${tag}> declares a2ui.allowedChildren: [${allowedChildren.join(', ')}] — <${child}> is not one of them`,
610
+ snippet: `<${tag}> > <${child.tag}>`,
611
+ why: `<${tag}> declares a2ui.allowedChildren: [${allowedChildren.join(', ')}] — <${child.tag}> is not one of them`,
474
612
  });
475
613
  }
476
614
  }
@@ -660,6 +798,32 @@ function markupScanSurface(text, path = '') {
660
798
  return surface;
661
799
  }
662
800
 
801
+ const CSS_COMMENT_RE = /\/\*[\s\S]*?(?:\*\/|$)/g;
802
+
803
+ /**
804
+ * gh#4008: the text `RE.SCOPE_BLOCK` (MISSING-SCOPE) actually tests, BLANKING
805
+ * (not deleting: line numbers must survive) every `/* ... *\/` CSS comment
806
+ * range the same way `markupScanSurface`'s HTML_COMMENT_RE already blanks
807
+ * markup comments above. gh#3888 closed "`@scope` appearing only in a bare
808
+ * word inside a comment must not satisfy MISSING-SCOPE" by switching the
809
+ * rule's substring test to a real at-rule regex (`/@scope\s*\(/`), but never
810
+ * blanked the comment itself, so a comment that carries the FULL pattern,
811
+ * `/* @scope (admin-shell) *\/`, still matches the same regex the raw-text
812
+ * word never could. An unterminated comment blanks to end-of-file, mirroring
813
+ * `markupScanSurface`'s own behavior for an unterminated `<!--`.
814
+ *
815
+ * Only used where a real, live at-rule (or declaration) is what the caller
816
+ * means to detect, NOT a general-purpose "strip all CSS comments" helper.
817
+ * `isFoundationCss`'s `/* forge-lint: foundation *\/` opt-in tag and
818
+ * missing-scope.mjs's own `PREFIX_SCOPED_RE` are deliberately read straight
819
+ * out of raw text instead: those tags are AUTHORED inside a comment on
820
+ * purpose, so blanking comments before reading them would break the very
821
+ * mechanism they are.
822
+ */
823
+ function cssCommentBlankedSurface(text) {
824
+ return text.replace(CSS_COMMENT_RE, (m) => m.replace(/[^\n]/g, ' '));
825
+ }
826
+
663
827
  /**
664
828
  * ADR-0108 D5 / LLD-0019 C5: builds lookup tables from a manifest object
665
829
  * shaped like `packages/gen-ui/mcp/factory/manifest.js`'s `getManifest()`
@@ -950,6 +1114,46 @@ function requiredAttrFindings(text, manifest, path = '') {
950
1114
  return out;
951
1115
  }
952
1116
 
1117
+ /**
1118
+ * UNKNOWN-SLOT (gh#3222): a `slot="…"` value that is not a slot name ANY
1119
+ * component in the manifest declares. `manifest` here is the flat
1120
+ * `{ slotNames: [...] }` shape `trimForEmbed`'s own `'slot-names'` branch
1121
+ * produces (gen-lab-posture-rules.mjs) — not the per-tag `{ components }`
1122
+ * shape `buildManifestIndex` reads for the other five rules.
1123
+ *
1124
+ * Deliberately NOT a per-tag/nearest-ancestor check the way UNKNOWN-ATTR
1125
+ * validates an attribute against its own element's tag: Light DOM's
1126
+ * `slot=` is decorative metadata this kit's own component JS reads with
1127
+ * an unscoped selector, so the element carrying `slot="action"` is
1128
+ * routinely a `<header>`'s own descendant rather than a direct child of
1129
+ * the component that actually declares the `action` slot
1130
+ * (card.examples.html's `<header><span slot="heading">…` — `heading` is
1131
+ * `card-ui`'s slot, two levels down from `<card-ui>` itself, past a plain
1132
+ * `<header>` wrapper with no manifest entry of its own). Reliably finding
1133
+ * the "owning" tag would need a nearest-kit-ancestor tree walk that is
1134
+ * fragile against exactly this shape; checking the NAME against the
1135
+ * manifest-wide vocabulary instead still catches the real failure mode
1136
+ * (a generator inventing a slot name no component anywhere declares)
1137
+ * without that fragility — the same "name only, never placement" scope
1138
+ * this primitive's own callers document.
1139
+ */
1140
+ function slotNameFindings(text, manifest, path = '') {
1141
+ const known = new Set((manifest && manifest.slotNames) || []);
1142
+ const out = [];
1143
+ for (const occ of tagOccurrences(text, path)) {
1144
+ const value = occ.attrs.slot;
1145
+ if (value === undefined || value === null) continue;
1146
+ if (isDynamicAttr('slot', value)) continue;
1147
+ if (known.has(value)) continue;
1148
+ out.push({
1149
+ line: occ.line,
1150
+ snippet: `<${occ.tag} slot="${value}">`,
1151
+ why: `UNKNOWN-SLOT: slot="${value}" is not a slot name any component in the installed manifest declares`,
1152
+ });
1153
+ }
1154
+ return out;
1155
+ }
1156
+
953
1157
  /**
954
1158
  * Yaml-corpus primitive (LLD-0016 §C3): reads one component yaml's own
955
1159
  * top-level `component:` scalar and its `props:` mapping's first-level key
@@ -1004,6 +1208,18 @@ function extractYamlComponentPropKeys(text) {
1004
1208
  * @param {Array<{id:string, severity:string, scope:string, fileTypes:string[], match:Function}>} rules
1005
1209
  * @param {{scopes?: string[]}} [opts] which rule scopes to run — omit for all
1006
1210
  */
1211
+ // gh#3716: a `scope: 'forge-only'` rule is declared to check framework
1212
+ // source (packages/web-components, packages/web-modules), the two trees
1213
+ // that are actually authored as scoped components. `npm run lint` sweeps
1214
+ // the whole repo unscoped (the monorepo is both the framework source and
1215
+ // its own consumer), so without this restriction a forge-only rule also
1216
+ // fires against e.g. apps/* page CSS that was never authored to that
1217
+ // contract. `consumer-only`/`shared` rules carry no such corpus fence.
1218
+ const FORGE_ONLY_PATHS = ['packages/web-components/', 'packages/web-modules/'];
1219
+ function isForgeOnlyPath(path) {
1220
+ return FORGE_ONLY_PATHS.some((p) => path.startsWith(p));
1221
+ }
1222
+
1007
1223
  function makeLinter(rules, opts = {}) {
1008
1224
  const scopes = opts.scopes || null;
1009
1225
  const active = rules.filter((r) => !scopes || scopes.includes(r.scope));
@@ -1014,16 +1230,28 @@ function makeLinter(rules, opts = {}) {
1014
1230
  let findings = [];
1015
1231
  for (const rule of active) {
1016
1232
  if (rule.fileTypes && rule.fileTypes.length && !rule.fileTypes.includes(fileExt)) continue;
1233
+ if (rule.scope === 'forge-only' && !isForgeOnlyPath(path)) continue;
1017
1234
  const hits = rule.match(text, path, { scopes }) || [];
1018
1235
  for (const h of hits) {
1019
- findings.push({ name: rule.id, line: h.line, snippet: h.snippet, why: h.why });
1236
+ findings.push({
1237
+ name: rule.id,
1238
+ line: h.line,
1239
+ snippet: h.snippet,
1240
+ why: h.why,
1241
+ ...(h.severity ? { severity: h.severity } : {}),
1242
+ });
1020
1243
  }
1021
1244
  }
1022
1245
 
1023
1246
  // Suppression syntax (LLD-0016 §Interfaces):
1024
- // /* adia-lint-disable <rule-id> — <reason> */ on the finding's own line
1025
- // or the line before it drops that one finding. A bare disable with no
1026
- // <reason> is itself a finding (SUPPRESS-NO-REASON, warn) checked
1247
+ // /* adia-lint-disable RULE-ID: reason text */ on the finding's own line
1248
+ // or the line before it drops that one finding. The separator may be a
1249
+ // colon, a space-padded hyphen, or an em dash (SUPPRESS_RE in
1250
+ // primitives.mjs is the one source); the padding on the hyphen is
1251
+ // required because rule ids contain hyphens. Prefer the colon or the
1252
+ // padded hyphen when writing one, since check:em-dash-added-lines
1253
+ // (gh#3767) rejects an em dash on any added line. A bare disable with no
1254
+ // <reason> is itself a finding (SUPPRESS-NO-REASON, warn), checked
1027
1255
  // once per suppression comment in the file, independent of whether it
1028
1256
  // actually suppressed anything this run.
1029
1257
  findings = findings.filter((f) => !suppressionFor(rawLines, f.line, f.name).suppressed);
@@ -1038,6 +1266,24 @@ function makeLinter(rules, opts = {}) {
1038
1266
  }
1039
1267
  }
1040
1268
 
1269
+ // gh#4161: SUPPRESS_RE is both matcher and detector, so a directive it
1270
+ // refuses produces no suppression, no SUPPRESS-NO-REASON, and no
1271
+ // diagnostic at all, silently invisible (gh#3868/gh#3871 each closed
1272
+ // one specific inert shape, never the general class). This loose scan
1273
+ // closes it: every adia-lint-disable-shaped comment the strict grammar
1274
+ // refuses to parse becomes a warn-severity SUPPRESS-MALFORMED finding
1275
+ // naming the file (via the caller) and line, so a malformed directive
1276
+ // is loud instead of vanishing.
1277
+ for (const s of malformedSuppressions(rawLines)) {
1278
+ findings.push({
1279
+ name: 'SUPPRESS-MALFORMED',
1280
+ line: s.lineNo,
1281
+ snippet: s.snippet,
1282
+ why: 'a comment mentioning adia-lint-disable that the strict directive grammar (SUPPRESS_RE) refuses to parse; it suppresses nothing and raises no other diagnostic, fix the <id>/<separator>/<reason> shape or drop the comment',
1283
+ severity: 'warn',
1284
+ });
1285
+ }
1286
+
1041
1287
  findings.sort((a, b) => (a.line - b.line) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
1042
1288
  return findings;
1043
1289
  }
@@ -1055,22 +1301,161 @@ function render(path, findings) {
1055
1301
  }
1056
1302
 
1057
1303
 
1304
+ /**
1305
+ * scripts/lint/engine/baseline.mjs (gh#3718): the checked-in baseline of
1306
+ * known SCOPE-EXTENT sites, and the normalization that keys them.
1307
+ *
1308
+ * No static `node:fs`/`node:path`/`node:url` import, same as
1309
+ * `scripts/lint/engine/primitives.mjs` and `scripts/lint/engine/run.mjs`,
1310
+ * because this module is inlined into the shared generated bank (LLD-0016
1311
+ * §C5) and copied byte-for-byte to 4 destinations, one of which
1312
+ * (anti-patterns.generated.js) is browser-reachable; check:browser-safe
1313
+ * forbids a top-level `node:*` import there. `process.getBuiltinModule()`
1314
+ * reaches the same builtins with zero import statements, undefined in a
1315
+ * browser, so this fails soft to "excuse nothing" there instead of throwing.
1316
+ *
1317
+ * Loaded once per process. Four outcomes, all of them chosen rather than
1318
+ * incidental:
1319
+ *
1320
+ * 1. No Node fs/path/url builtins (browser context). Not an error: no site
1321
+ * is excused, so every finding reports at the rule's own severity.
1322
+ * 2. No baseline file on disk. Same as (1).
1323
+ * 3. An unusable baseline file: unparseable, OR parseable but not an object
1324
+ * with an object "sites" (JSON.parse succeeds on null, on a bare string, on
1325
+ * a number and on an array, so "it parsed" says nothing about the shape).
1326
+ * Same as (1), excuse nothing, but WARN loudly naming what was wrong. It
1327
+ * must not throw: this is one rule's data file, and an uncaught error here
1328
+ * takes down the whole lint run for every other rule, which is a far worse
1329
+ * failure than losing the downgrades.
1330
+ * 4. A readable baseline. Its sites are excused, nothing else is.
1331
+ *
1332
+ * What is never an outcome is "unreadable, therefore everything is excused",
1333
+ * which would disable the rule silently.
1334
+ */
1335
+ let cache = null;
1336
+
1337
+ const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
1338
+
1339
+ const describe = (v) => (v === null ? 'null' : Array.isArray(v) ? 'an array' : typeof v);
1340
+
1341
+ function nodeBuiltins() {
1342
+ if (typeof process === 'undefined' || typeof process.getBuiltinModule !== 'function') return null;
1343
+ return {
1344
+ fs: process.getBuiltinModule('node:fs'),
1345
+ path: process.getBuiltinModule('node:path'),
1346
+ url: process.getBuiltinModule('node:url'),
1347
+ };
1348
+ }
1349
+
1350
+ function load() {
1351
+ if (cache) return cache;
1352
+ const builtins = nodeBuiltins();
1353
+ if (!builtins) {
1354
+ cache = { sites: {} };
1355
+ return cache;
1356
+ }
1357
+ const here = builtins.path.dirname(builtins.url.fileURLToPath(import.meta.url));
1358
+ const baselinePath = builtins.path.join(here, '..', 'scope-extent-baseline.json');
1359
+ if (!builtins.fs.existsSync(baselinePath)) {
1360
+ cache = { sites: {} };
1361
+ return cache;
1362
+ }
1363
+ try {
1364
+ const parsed = JSON.parse(builtins.fs.readFileSync(baselinePath, 'utf8'));
1365
+ // Shape validation lives INSIDE the try, deliberately. JSON.parse succeeds
1366
+ // on `null`, on a bare string, on a number and on an array, so a parse that
1367
+ // did not throw proves nothing about the shape. Validating out here is how
1368
+ // a file containing the four characters `null` reached `cache.sites` and
1369
+ // threw past the catch, crashing every other rule's run.
1370
+ if (!isPlainObject(parsed)) {
1371
+ throw new TypeError(`expected a JSON object at the top level, got ${describe(parsed)}`);
1372
+ }
1373
+ // An absent `sites` is a legitimately empty baseline (excuses nothing) and
1374
+ // is NOT warned about; a present-but-wrong-typed `sites` is a real mistake
1375
+ // and is. Warning on the empty case would train readers to ignore the
1376
+ // warning, which is how a loud instrument stops being read.
1377
+ if (parsed.sites !== undefined && !isPlainObject(parsed.sites)) {
1378
+ throw new TypeError(`"sites" must be an object, got ${describe(parsed.sites)}`);
1379
+ }
1380
+ cache = { ...parsed, sites: parsed.sites || {} };
1381
+ } catch (err) {
1382
+ console.warn(
1383
+ `[SCOPE-EXTENT] baseline at ${baselinePath} is unusable, excusing nothing: ${err.message}`,
1384
+ );
1385
+ cache = { sites: {} };
1386
+ }
1387
+ return cache;
1388
+ }
1389
+
1390
+ /**
1391
+ * Normalize a matched block so that reformatting does not invalidate an entry:
1392
+ * strip CSS comments, collapse all whitespace, drop spaces around the
1393
+ * punctuation that separates declarations. Editing the selector or the
1394
+ * declaration itself still changes the key, which is intended, and that is a real
1395
+ * change to a known site.
1396
+ *
1397
+ * The matcher itself can stop mid-comment (a comment mentioning a property
1398
+ * name like "min-width:" satisfies the same width/height text test the real
1399
+ * rule looks for), so a raw match can end inside an unterminated `/* ... `
1400
+ * with no closing marker in the captured text yet. That trailing open
1401
+ * comment is stripped too, same as a closed one: it carries no declaration
1402
+ * text, and dropping it is what keeps a baseline key from carrying prose
1403
+ * (comment wording, including punctuation the rest of this codebase avoids)
1404
+ * that has nothing to do with the actual finding.
1405
+ */
1406
+ function normalizeMatch(raw) {
1407
+ return raw
1408
+ .replace(/\/\*[\s\S]*?\*\//g, '')
1409
+ .replace(/\/\*[\s\S]*$/g, '')
1410
+ .replace(/\s+/g, ' ')
1411
+ .replace(/\s*([{};:,])\s*/g, '$1')
1412
+ .trim();
1413
+ }
1414
+
1415
+ /** The baselined keys for one file, as a multiset (duplicates preserved). */
1416
+ function baselineFor(path) {
1417
+ const p = (path || '').replace(/\\/g, '/');
1418
+ return load().sites[p] || [];
1419
+ }
1420
+
1421
+ /** Total baselined entries. The acceptance compares this to a live count. */
1422
+ function baselineCount() {
1423
+ const { sites } = load();
1424
+ return Object.values(sites).reduce((n, list) => n + list.length, 0);
1425
+ }
1426
+
1427
+
1058
1428
  const RULES = [];
1059
1429
  {
1060
1430
 
1061
1431
 
1062
1432
  const APPLIES_EXT = ['.html', '.htm', '.vue', '.svelte', '.astro', '.tsx', '.jsx', '.js', '.mjs', '.ts'];
1063
1433
 
1434
+ // gh#3717: a whole-file `String.includes('validate_schema')` cleared the
1435
+ // finding on ANY occurrence of the name anywhere in the file, a prose
1436
+ // comment, a docstring, an unrelated identifier, even a substring baked
1437
+ // into a fixture string. site/pages/catalogs/catalogs.setup.js passed
1438
+ // purely because a comment mentions both names while `.doc =` is set with
1439
+ // no validation ever run. The fix asks two sharper questions instead:
1440
+ // (1) is the `.doc =` assignment itself real code, not commented out
1441
+ // (stripCommentLines, gh#976's own comment-stripping primitive, the same
1442
+ // one that keeps a retired-selector rule from firing on a docstring), and
1443
+ // (2) does the SAME module actually CALL validate_schema(...) or
1444
+ // check_anti_patterns(...), a real call expression, not a bare mention.
1445
+ const VALIDATE_CALL_RE = /\b(?:validate_schema|check_anti_patterns)\s*\(/;
1446
+
1064
1447
  const genuiUnvalidated = {
1065
1448
  id: 'GENUI-UNVALIDATED',
1066
1449
  severity: 'error',
1067
1450
  scope: 'consumer-only',
1068
1451
  fileTypes: APPLIES_EXT,
1069
- match(text) {
1070
- const m = /\.doc\s*=/.exec(text);
1071
- if (!m) return [];
1072
- const line = lineOf(text, m.index);
1073
- if (text.includes('validate_schema') || text.includes('check_anti_patterns')) return [];
1452
+ match(text, filePath) {
1453
+ if (!/\.doc\s*=/.test(text)) return [];
1454
+ const stripped = stripCommentLines(text, ext(filePath)).map((l) => l.stripped).join('\n');
1455
+ const m = /\.doc\s*=/.exec(stripped);
1456
+ if (!m) return []; // every `.doc =` in the file was inside a comment
1457
+ if (VALIDATE_CALL_RE.test(stripped)) return [];
1458
+ const line = lineOf(stripped, m.index);
1074
1459
  return [{
1075
1460
  line,
1076
1461
  snippet: '.doc = … with no validate in file',
@@ -1088,6 +1473,42 @@ const genuiUnvalidated = {
1088
1473
  // a declarative `src=`/`transport=` on an instance.
1089
1474
  const RUNTIME_HOME_PATH = /(?:^|[\\/])packages[\\/]web-modules[\\/]runtime[\\/](?:a2ui-root|gen-root)[\\/]/;
1090
1475
 
1476
+ // gh#3714: the old check matched ".doc =" and "src="/"transport=" anywhere
1477
+ // in the FILE, so a2ui-root.js's own docstring (documenting both usage
1478
+ // modes side by side in one comment block) tripped it, even with the
1479
+ // RUNTIME_HOME_PATH exemption above already carved out for that one file.
1480
+ // The real bug is broader than that single case: any consumer file wiring
1481
+ // TWO root elements, one fed declaratively and one fed programmatically,
1482
+ // was also a false positive, since the old match never asked WHICH
1483
+ // element each signal belonged to.
1484
+ //
1485
+ // This is a regex heuristic, not a JS/HTML parser, so it correlates by two
1486
+ // cheap signals rather than real scope analysis:
1487
+ // 1. Single-element files: if the file has exactly one <a2ui-root>/
1488
+ // <gen-root> tag, any ".doc =" assignment anywhere in the file is
1489
+ // presumed to target that one element (nothing else for it to mean).
1490
+ // 2. Multi-element files: a ".doc =" assignment correlates to a specific
1491
+ // element only when its target variable was bound via
1492
+ // document.getElementById('id') / document.querySelector('#id') to
1493
+ // an element carrying a matching id="..." attribute.
1494
+ // Documented misses (false negatives), by design rather than oversight:
1495
+ // - A file with 2+ root elements where NONE carry an id= attribute, or
1496
+ // where the ".doc =" target variable is bound some other way (a
1497
+ // framework ref, a destructured array, ".doc =" set via "this.el"),
1498
+ // cannot be correlated and is silently allowed. The old file-wide
1499
+ // check would have (over-)caught this, at the cost of the
1500
+ // a2ui-root.js false positive this ticket exists to fix.
1501
+ // Documented possible false positive: in a single-element file, a ".doc ="
1502
+ // assignment that provably targets a DIFFERENT, unrelated object which
1503
+ // merely has its own "doc" property (e.g. an unrelated "state.doc = ...")
1504
+ // cannot be told apart from a real root reference by rule 1 above.
1505
+ const ROOT_TAG = /<(a2ui-root|gen-root)\b([^>]*)>/g;
1506
+ const ID_ATTR = /\bid\s*=\s*["']([^"']+)["']/;
1507
+ const DECLARATIVE_ATTR = /\b(?:src|transport)\s*=/;
1508
+ const VAR_TO_ID =
1509
+ /(?:const|let|var)?\s*([A-Za-z_$][\w$]*)\s*=\s*document\.(?:getElementById\(\s*['"]([\w-]+)['"]\s*\)|querySelector\(\s*['"]#([\w-]+)['"]\s*\))/g;
1510
+ const DOC_ASSIGN = /([A-Za-z_$][\w$]*)\.doc\s*=/g;
1511
+
1091
1512
  const genuiDocConflict = {
1092
1513
  id: 'GENUI-DOC-CONFLICT',
1093
1514
  severity: 'error',
@@ -1095,17 +1516,34 @@ const genuiDocConflict = {
1095
1516
  fileTypes: APPLIES_EXT,
1096
1517
  match(text, filePath) {
1097
1518
  if (typeof filePath === 'string' && RUNTIME_HOME_PATH.test(filePath)) return [];
1098
- const m = /\.doc\s*=/.exec(text);
1099
- if (!m) return [];
1100
- const line = lineOf(text, m.index);
1101
- if (/<(?:a2ui-root|gen-root)[^>]*\b(?:src|transport)=/.test(text)) {
1102
- return [{
1103
- line,
1104
- snippet: '.doc = … alongside src=/transport=',
1519
+
1520
+ const elements = [];
1521
+ for (const m of text.matchAll(ROOT_TAG)) {
1522
+ const attrs = m[2];
1523
+ const idMatch = ID_ATTR.exec(attrs);
1524
+ elements.push({ id: idMatch ? idMatch[1] : null, declarative: DECLARATIVE_ATTR.test(attrs) });
1525
+ }
1526
+ if (elements.length === 0) return [];
1527
+
1528
+ const idsWithDeclarative = new Set(elements.filter((e) => e.declarative && e.id).map((e) => e.id));
1529
+ const varToId = new Map();
1530
+ for (const m of text.matchAll(VAR_TO_ID)) {
1531
+ varToId.set(m[1], m[2] || m[3]);
1532
+ }
1533
+ const soleElementIsDeclarative = elements.length === 1 && elements[0].declarative;
1534
+
1535
+ const findings = [];
1536
+ for (const m of text.matchAll(DOC_ASSIGN)) {
1537
+ const targetId = varToId.get(m[1]);
1538
+ const conflicts = targetId ? idsWithDeclarative.has(targetId) : soleElementIsDeclarative;
1539
+ if (!conflicts) continue;
1540
+ findings.push({
1541
+ line: lineOf(text, m.index),
1542
+ snippet: '.doc = … alongside src=/transport= on the same root',
1105
1543
  why: 'two feed paths fight — a root is fed EITHER declaratively (src/transport) OR programmatically (.doc), never both',
1106
- }];
1544
+ });
1107
1545
  }
1108
- return [];
1546
+ return findings;
1109
1547
  },
1110
1548
  };
1111
1549
 
@@ -1126,6 +1564,13 @@ RULES.push(genuiUnvalidated, genuiDocConflict);
1126
1564
  // gets the exemption; scope it to that tree, not the filename alone.
1127
1565
  const EXAMPLES_FILE = /\.examples\.html$/;
1128
1566
  const CATALOG_CONTENTS_FILE = /\.contents\.html$/;
1567
+ // packages/web-components/templates/ is the Class R npm mirror of those same
1568
+ // catalog/apps/playgrounds demo fragments (ADR-0084, built by
1569
+ // scripts/build/templates-mirror.mjs). The mirror step renames each file to
1570
+ // `pattern-library-<name>.html` and drops the `catalog/` prefix, so a
1571
+ // mirrored demo's default-open state no longer matches either pattern above
1572
+ // even though it is a byte-for-byte copy of already-exempt source markup.
1573
+ const TEMPLATES_MIRROR_DIR = /^packages\/web-components\/templates\//;
1129
1574
 
1130
1575
 
1131
1576
  RULES.push({
@@ -1137,6 +1582,7 @@ RULES.push({
1137
1582
  const p = (path || '').replace(/\\/g, '/');
1138
1583
  if (EXAMPLES_FILE.test(p)) return [];
1139
1584
  if (CATALOG_CONTENTS_FILE.test(p) && p.split('/')[0] === 'catalog') return [];
1585
+ if (TEMPLATES_MIRROR_DIR.test(p)) return [];
1140
1586
  const out = [];
1141
1587
  text.split('\n').forEach((line, i) => {
1142
1588
  if (RE.OVERLAY_OPEN.test(line)) {
@@ -1222,8 +1668,11 @@ RULES.push({
1222
1668
  const pages = (text.match(/<admin-page[\s/>]/g) || []).length;
1223
1669
  const scrolls = (text.match(/<admin-scroll[\s/>]/g) || []).length;
1224
1670
  if (scrolls && pages > scrolls) {
1671
+ // report the first <admin-page> occurrence, the line a developer would
1672
+ // actually attach an adia-lint-disable comment to for this finding.
1673
+ const firstIndex = text.search(/<admin-page[\s/>]/);
1225
1674
  out.push({
1226
- line: 1,
1675
+ line: lineOf(text, firstIndex),
1227
1676
  snippet: '<admin-page> × N inside <admin-scroll>',
1228
1677
  why: 'each <admin-scroll> hosts exactly one <admin-page> — multiple pages need multiple scroll regions',
1229
1678
  });
@@ -1243,6 +1692,8 @@ RULES.push({
1243
1692
  }
1244
1693
 
1245
1694
  {
1695
+
1696
+
1246
1697
  const APPLIES_EXT = ['.html', '.htm', '.vue', '.svelte', '.astro', '.tsx', '.jsx', '.js', '.mjs', '.ts'];
1247
1698
 
1248
1699
 
@@ -1252,9 +1703,10 @@ RULES.push({
1252
1703
  scope: 'consumer-only',
1253
1704
  fileTypes: APPLIES_EXT,
1254
1705
  match(text) {
1255
- if (/<admin-sidebar[^>]*\bresizable\b/.test(text) && !text.includes('data-sidebar-resize')) {
1706
+ const m = /<admin-sidebar[^>]*\bresizable\b/.exec(text);
1707
+ if (m && !text.includes('data-sidebar-resize')) {
1256
1708
  return [{
1257
- line: 1,
1709
+ line: lineOf(text, m.index),
1258
1710
  snippet: '<admin-sidebar resizable> without [data-sidebar-resize]',
1259
1711
  why: '[resizable] needs a child <div data-sidebar-resize> or there is no drag handle',
1260
1712
  }];
@@ -1322,6 +1774,29 @@ RULES.push(ssrDoubleRouter, ssrToplevelImport);
1322
1774
  {
1323
1775
 
1324
1776
 
1777
+ // gh#3888: a second, honestly-named opt-out alongside `forge-lint:
1778
+ // foundation`. Some CSS intentionally scopes via a repeated parent-tag
1779
+ // selector prefix (`admin-shell [data-x] { … }`) instead of a real
1780
+ // `@scope` at-rule. Labeling that shape `forge-lint: foundation` would
1781
+ // be a lie (neither file is a token sheet); this tag says what the
1782
+ // file actually does instead.
1783
+ //
1784
+ // ALLOWLISTED, not a self-serve bypass (gh#3888 critic finding): an
1785
+ // undocumented, unrestricted opt-out comment is the same shape as the
1786
+ // substring-loophole bug this rule just fixed, only deliberate. A
1787
+ // file adding this comment on its own must FAIL, naming the allowlist,
1788
+ // so escaping MISSING-SCOPE this way always needs a human to edit this
1789
+ // rule module, never just a CSS comment.
1790
+ //
1791
+ // gh#3922 closed the follow-up: the two files gh#3888 allowlisted
1792
+ // (admin-shell.helpers.css, admin-shell.mobile-nav.css) are now real
1793
+ // `@scope (admin-shell) { … }` blocks and admin-shell.test.js's prefix
1794
+ // assertions were rewritten to match. The allowlist is empty again;
1795
+ // `forge-lint: prefix-scoped` stays available as a mechanism, not a
1796
+ // standing exemption; a future file needs a human to add it here.
1797
+ const PREFIX_SCOPED_RE = /forge-lint:\s*prefix-scoped/i;
1798
+ const PREFIX_SCOPED_ALLOWLIST = [];
1799
+
1325
1800
 
1326
1801
  RULES.push({
1327
1802
  id: 'MISSING-SCOPE',
@@ -1331,19 +1806,34 @@ RULES.push({
1331
1806
  match(text, path) {
1332
1807
  const isTokenish = isFoundationCss(path, text, 'forge-lint');
1333
1808
  if (isTokenish) return [];
1334
- if (text.includes('@scope')) return [];
1809
+ // gh#4008: a comment carrying the full `@scope (...)` pattern (not just
1810
+ // the bare word gh#3888 already excluded) must not satisfy this test:
1811
+ // blank CSS comments first, mirroring markupScanSurface's HTML-comment
1812
+ // blanking, so only a REAL, live at-rule can match.
1813
+ if (RE.SCOPE_BLOCK.test(cssCommentBlankedSurface(text))) return [];
1814
+ const normalizedPath = (path || '').replace(/\\/g, '/');
1815
+ const claimsPrefixScoped = PREFIX_SCOPED_RE.test(text.slice(0, 1000));
1816
+ const isAllowlisted = PREFIX_SCOPED_ALLOWLIST.some((p) => normalizedPath.endsWith(p));
1817
+ if (claimsPrefixScoped && !isAllowlisted) {
1818
+ return [{
1819
+ line: 1,
1820
+ snippet: '(file has CSS rules but no @scope block)',
1821
+ why: `forge-lint: prefix-scoped is allowlisted to zero files (gh#3922 closed the last two, gh#3888). Adding this comment does not opt a file out; either wrap it in a real @scope block, or ask for the allowlist to be extended in scripts/lint/rules/forge/missing-scope.mjs itself.`,
1822
+ }];
1823
+ }
1824
+ if (claimsPrefixScoped && isAllowlisted) return [];
1335
1825
  if (!RE.SELECTOR_RULE.test(text)) return [];
1336
1826
  return [{
1337
1827
  line: 1,
1338
1828
  snippet: '(file has CSS rules but no @scope block)',
1339
- why: 'component CSS must be wrapped in `@scope (<tag>) { … }` so styles don\'t leak in light DOM (foundation/token sheets excepted; opt out with /* forge-lint: foundation */)',
1829
+ why: 'component CSS must be wrapped in `@scope (<tag>) { … }` so styles don\'t leak in light DOM (foundation/token sheets excepted with /* forge-lint: foundation */)',
1340
1830
  }];
1341
1831
  },
1342
1832
  });
1343
1833
  }
1344
1834
 
1345
1835
  {
1346
- // GENERATED do not hand-edit. Source: packages/web-components/components/accordion/accordion-item.yaml's a2ui.allowedParents/allowedChildren.
1836
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/accordion/accordion-item.yaml's a2ui.allowedParents/allowedChildren.
1347
1837
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1348
1838
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1349
1839
 
@@ -1351,15 +1841,16 @@ RULES.push({
1351
1841
  const TAG = "accordion-item-ui";
1352
1842
  const ALLOWED_PARENTS = ["accordion-ui"];
1353
1843
  const ALLOWED_CHILDREN = [];
1844
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1354
1845
 
1355
1846
 
1356
1847
  RULES.push({
1357
1848
  id: "COMPOSITION-ACCORDION-ITEM-UI",
1358
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
1849
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1359
1850
  scope: 'shared',
1360
1851
  fileTypes: MARKUP_EXT,
1361
1852
  match(text) {
1362
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
1853
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1363
1854
  },
1364
1855
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1365
1856
  fixtures: {
@@ -1374,7 +1865,7 @@ RULES.push({
1374
1865
  }
1375
1866
 
1376
1867
  {
1377
- // GENERATED do not hand-edit. Source: packages/web-components/components/accordion/accordion.yaml's a2ui.allowedParents/allowedChildren.
1868
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/accordion/accordion.yaml's a2ui.allowedParents/allowedChildren.
1378
1869
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1379
1870
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1380
1871
 
@@ -1382,15 +1873,16 @@ RULES.push({
1382
1873
  const TAG = "accordion-ui";
1383
1874
  const ALLOWED_PARENTS = [];
1384
1875
  const ALLOWED_CHILDREN = ["accordion-item-ui"];
1876
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1385
1877
 
1386
1878
 
1387
1879
  RULES.push({
1388
1880
  id: "COMPOSITION-ACCORDION-UI",
1389
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
1881
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1390
1882
  scope: 'shared',
1391
1883
  fileTypes: MARKUP_EXT,
1392
1884
  match(text) {
1393
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
1885
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1394
1886
  },
1395
1887
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1396
1888
  fixtures: {
@@ -1398,14 +1890,15 @@ RULES.push({
1398
1890
  "<accordion-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></accordion-ui>"
1399
1891
  ],
1400
1892
  "clean": [
1401
- "<accordion-ui><accordion-item-ui></accordion-item-ui></accordion-ui>"
1893
+ "<accordion-ui><accordion-item-ui></accordion-item-ui></accordion-ui>",
1894
+ "<accordion-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></accordion-ui>"
1402
1895
  ]
1403
1896
  },
1404
1897
  });
1405
1898
  }
1406
1899
 
1407
1900
  {
1408
- // GENERATED do not hand-edit. Source: packages/web-components/components/action-list/action-item.yaml's a2ui.allowedParents/allowedChildren.
1901
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/action-list/action-item.yaml's a2ui.allowedParents/allowedChildren.
1409
1902
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1410
1903
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1411
1904
 
@@ -1413,15 +1906,16 @@ RULES.push({
1413
1906
  const TAG = "action-item-ui";
1414
1907
  const ALLOWED_PARENTS = ["action-list-ui"];
1415
1908
  const ALLOWED_CHILDREN = [];
1909
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1416
1910
 
1417
1911
 
1418
1912
  RULES.push({
1419
1913
  id: "COMPOSITION-ACTION-ITEM-UI",
1420
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
1914
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1421
1915
  scope: 'shared',
1422
1916
  fileTypes: MARKUP_EXT,
1423
1917
  match(text) {
1424
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
1918
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1425
1919
  },
1426
1920
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1427
1921
  fixtures: {
@@ -1436,7 +1930,7 @@ RULES.push({
1436
1930
  }
1437
1931
 
1438
1932
  {
1439
- // GENERATED do not hand-edit. Source: packages/web-components/components/action-list/action-list.yaml's a2ui.allowedParents/allowedChildren.
1933
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/action-list/action-list.yaml's a2ui.allowedParents/allowedChildren.
1440
1934
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1441
1935
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1442
1936
 
@@ -1444,15 +1938,16 @@ RULES.push({
1444
1938
  const TAG = "action-list-ui";
1445
1939
  const ALLOWED_PARENTS = [];
1446
1940
  const ALLOWED_CHILDREN = ["action-item-ui"];
1941
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1447
1942
 
1448
1943
 
1449
1944
  RULES.push({
1450
1945
  id: "COMPOSITION-ACTION-LIST-UI",
1451
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
1946
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1452
1947
  scope: 'shared',
1453
1948
  fileTypes: MARKUP_EXT,
1454
1949
  match(text) {
1455
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
1950
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1456
1951
  },
1457
1952
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1458
1953
  fixtures: {
@@ -1460,14 +1955,15 @@ RULES.push({
1460
1955
  "<action-list-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></action-list-ui>"
1461
1956
  ],
1462
1957
  "clean": [
1463
- "<action-list-ui><action-item-ui></action-item-ui></action-list-ui>"
1958
+ "<action-list-ui><action-item-ui></action-item-ui></action-list-ui>",
1959
+ "<action-list-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></action-list-ui>"
1464
1960
  ]
1465
1961
  },
1466
1962
  });
1467
1963
  }
1468
1964
 
1469
1965
  {
1470
- // GENERATED do not hand-edit. Source: packages/web-modules/shell/admin-command/admin-command.yaml's a2ui.allowedParents/allowedChildren.
1966
+ // GENERATED: do not hand-edit. Source: packages/web-modules/shell/admin-command/admin-command.yaml's a2ui.allowedParents/allowedChildren.
1471
1967
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1472
1968
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1473
1969
 
@@ -1475,15 +1971,16 @@ RULES.push({
1475
1971
  const TAG = "admin-command";
1476
1972
  const ALLOWED_PARENTS = ["admin-shell"];
1477
1973
  const ALLOWED_CHILDREN = [];
1974
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1478
1975
 
1479
1976
 
1480
1977
  RULES.push({
1481
1978
  id: "COMPOSITION-ADMIN-COMMAND",
1482
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
1979
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1483
1980
  scope: 'shared',
1484
1981
  fileTypes: MARKUP_EXT,
1485
1982
  match(text) {
1486
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
1983
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1487
1984
  },
1488
1985
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1489
1986
  fixtures: {
@@ -1498,7 +1995,7 @@ RULES.push({
1498
1995
  }
1499
1996
 
1500
1997
  {
1501
- // GENERATED do not hand-edit. Source: packages/web-modules/shell/admin-content/admin-content.yaml's a2ui.allowedParents/allowedChildren.
1998
+ // GENERATED: do not hand-edit. Source: packages/web-modules/shell/admin-content/admin-content.yaml's a2ui.allowedParents/allowedChildren.
1502
1999
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1503
2000
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1504
2001
 
@@ -1506,15 +2003,16 @@ RULES.push({
1506
2003
  const TAG = "admin-content";
1507
2004
  const ALLOWED_PARENTS = ["admin-shell"];
1508
2005
  const ALLOWED_CHILDREN = [];
2006
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1509
2007
 
1510
2008
 
1511
2009
  RULES.push({
1512
2010
  id: "COMPOSITION-ADMIN-CONTENT",
1513
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2011
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1514
2012
  scope: 'shared',
1515
2013
  fileTypes: MARKUP_EXT,
1516
2014
  match(text) {
1517
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2015
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1518
2016
  },
1519
2017
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1520
2018
  fixtures: {
@@ -1529,38 +2027,103 @@ RULES.push({
1529
2027
  }
1530
2028
 
1531
2029
  {
1532
- // GENERATED do not hand-edit. Source: packages/web-modules/shell/admin-scroll/admin-scroll.yaml's a2ui.allowedParents/allowedChildren.
2030
+ // GENERATED: do not hand-edit. Source: packages/web-modules/shell/admin-entity-item/admin-entity-item.yaml's a2ui.allowedParents/allowedChildren.
1533
2031
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1534
2032
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1535
2033
 
1536
2034
 
1537
- const TAG = "admin-scroll";
1538
- const ALLOWED_PARENTS = ["admin-content"];
2035
+ const TAG = "admin-entity-item";
2036
+ const ALLOWED_PARENTS = ["admin-topbar","admin-statusbar"];
1539
2037
  const ALLOWED_CHILDREN = [];
2038
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1540
2039
 
1541
2040
 
1542
2041
  RULES.push({
1543
- id: "COMPOSITION-ADMIN-SCROLL",
1544
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2042
+ id: "COMPOSITION-ADMIN-ENTITY-ITEM",
2043
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1545
2044
  scope: 'shared',
1546
2045
  fileTypes: MARKUP_EXT,
1547
2046
  match(text) {
1548
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2047
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1549
2048
  },
1550
2049
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1551
2050
  fixtures: {
1552
2051
  "smelly": [
1553
- "<div><admin-scroll></admin-scroll></div>"
2052
+ "<div><admin-entity-item></admin-entity-item></div>"
1554
2053
  ],
1555
2054
  "clean": [
1556
- "<admin-content><admin-scroll></admin-scroll></admin-content>"
2055
+ "<admin-topbar><admin-entity-item></admin-entity-item></admin-topbar>"
1557
2056
  ]
1558
2057
  },
1559
2058
  });
1560
2059
  }
1561
2060
 
1562
2061
  {
1563
- // GENERATED do not hand-edit. Source: packages/web-modules/shell/admin-shell/admin-shell.yaml's a2ui.allowedParents/allowedChildren.
2062
+ // GENERATED: do not hand-edit. Source: packages/web-modules/agent-admin/admin-roster/admin-roster.yaml's a2ui.allowedParents/allowedChildren.
2063
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
2064
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2065
+
2066
+
2067
+ const TAG = "admin-roster-ui";
2068
+ const ALLOWED_PARENTS = ["agent-admin-ui"];
2069
+ const ALLOWED_CHILDREN = [];
2070
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2071
+
2072
+
2073
+ RULES.push({
2074
+ id: "COMPOSITION-ADMIN-ROSTER-UI",
2075
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2076
+ scope: 'shared',
2077
+ fileTypes: MARKUP_EXT,
2078
+ match(text) {
2079
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2080
+ },
2081
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2082
+ fixtures: {
2083
+ "smelly": [
2084
+ "<div><admin-roster-ui></admin-roster-ui></div>"
2085
+ ],
2086
+ "clean": [
2087
+ "<agent-admin-ui><admin-roster-ui></admin-roster-ui></agent-admin-ui>"
2088
+ ]
2089
+ },
2090
+ });
2091
+ }
2092
+
2093
+ {
2094
+ // GENERATED: do not hand-edit. Source: packages/web-modules/agent-admin/admin-settings/admin-settings.yaml's a2ui.allowedParents/allowedChildren.
2095
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
2096
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2097
+
2098
+
2099
+ const TAG = "admin-settings-ui";
2100
+ const ALLOWED_PARENTS = ["agent-admin-ui"];
2101
+ const ALLOWED_CHILDREN = [];
2102
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2103
+
2104
+
2105
+ RULES.push({
2106
+ id: "COMPOSITION-ADMIN-SETTINGS-UI",
2107
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2108
+ scope: 'shared',
2109
+ fileTypes: MARKUP_EXT,
2110
+ match(text) {
2111
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2112
+ },
2113
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2114
+ fixtures: {
2115
+ "smelly": [
2116
+ "<div><admin-settings-ui></admin-settings-ui></div>"
2117
+ ],
2118
+ "clean": [
2119
+ "<agent-admin-ui><admin-settings-ui></admin-settings-ui></agent-admin-ui>"
2120
+ ]
2121
+ },
2122
+ });
2123
+ }
2124
+
2125
+ {
2126
+ // GENERATED: do not hand-edit. Source: packages/web-modules/shell/admin-shell/admin-shell.yaml's a2ui.allowedParents/allowedChildren.
1564
2127
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1565
2128
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1566
2129
 
@@ -1568,15 +2131,16 @@ RULES.push({
1568
2131
  const TAG = "admin-shell";
1569
2132
  const ALLOWED_PARENTS = ["Surface"];
1570
2133
  const ALLOWED_CHILDREN = [];
2134
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1571
2135
 
1572
2136
 
1573
2137
  RULES.push({
1574
2138
  id: "COMPOSITION-ADMIN-SHELL",
1575
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2139
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1576
2140
  scope: 'shared',
1577
2141
  fileTypes: MARKUP_EXT,
1578
2142
  match(text) {
1579
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2143
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1580
2144
  },
1581
2145
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1582
2146
  fixtures: {
@@ -1591,7 +2155,7 @@ RULES.push({
1591
2155
  }
1592
2156
 
1593
2157
  {
1594
- // GENERATED do not hand-edit. Source: packages/web-modules/shell/admin-sidebar/admin-sidebar.yaml's a2ui.allowedParents/allowedChildren.
2158
+ // GENERATED: do not hand-edit. Source: packages/web-modules/shell/admin-sidebar/admin-sidebar.yaml's a2ui.allowedParents/allowedChildren.
1595
2159
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1596
2160
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1597
2161
 
@@ -1599,15 +2163,16 @@ RULES.push({
1599
2163
  const TAG = "admin-sidebar";
1600
2164
  const ALLOWED_PARENTS = ["admin-shell"];
1601
2165
  const ALLOWED_CHILDREN = [];
2166
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1602
2167
 
1603
2168
 
1604
2169
  RULES.push({
1605
2170
  id: "COMPOSITION-ADMIN-SIDEBAR",
1606
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2171
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1607
2172
  scope: 'shared',
1608
2173
  fileTypes: MARKUP_EXT,
1609
2174
  match(text) {
1610
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2175
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1611
2176
  },
1612
2177
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1613
2178
  fixtures: {
@@ -1622,7 +2187,7 @@ RULES.push({
1622
2187
  }
1623
2188
 
1624
2189
  {
1625
- // GENERATED do not hand-edit. Source: packages/web-modules/shell/admin-statusbar/admin-statusbar.yaml's a2ui.allowedParents/allowedChildren.
2190
+ // GENERATED: do not hand-edit. Source: packages/web-modules/shell/admin-statusbar/admin-statusbar.yaml's a2ui.allowedParents/allowedChildren.
1626
2191
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1627
2192
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1628
2193
 
@@ -1630,15 +2195,16 @@ RULES.push({
1630
2195
  const TAG = "admin-statusbar";
1631
2196
  const ALLOWED_PARENTS = ["admin-shell","admin-content","admin-sidebar"];
1632
2197
  const ALLOWED_CHILDREN = [];
2198
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1633
2199
 
1634
2200
 
1635
2201
  RULES.push({
1636
2202
  id: "COMPOSITION-ADMIN-STATUSBAR",
1637
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2203
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1638
2204
  scope: 'shared',
1639
2205
  fileTypes: MARKUP_EXT,
1640
2206
  match(text) {
1641
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2207
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1642
2208
  },
1643
2209
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1644
2210
  fixtures: {
@@ -1653,7 +2219,7 @@ RULES.push({
1653
2219
  }
1654
2220
 
1655
2221
  {
1656
- // GENERATED do not hand-edit. Source: packages/web-modules/shell/admin-topbar/admin-topbar.yaml's a2ui.allowedParents/allowedChildren.
2222
+ // GENERATED: do not hand-edit. Source: packages/web-modules/shell/admin-topbar/admin-topbar.yaml's a2ui.allowedParents/allowedChildren.
1657
2223
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1658
2224
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1659
2225
 
@@ -1661,15 +2227,16 @@ RULES.push({
1661
2227
  const TAG = "admin-topbar";
1662
2228
  const ALLOWED_PARENTS = ["admin-shell","admin-content","admin-sidebar"];
1663
2229
  const ALLOWED_CHILDREN = [];
2230
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1664
2231
 
1665
2232
 
1666
2233
  RULES.push({
1667
2234
  id: "COMPOSITION-ADMIN-TOPBAR",
1668
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2235
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1669
2236
  scope: 'shared',
1670
2237
  fileTypes: MARKUP_EXT,
1671
2238
  match(text) {
1672
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2239
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1673
2240
  },
1674
2241
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1675
2242
  fixtures: {
@@ -1684,7 +2251,105 @@ RULES.push({
1684
2251
  }
1685
2252
 
1686
2253
  {
1687
- // GENERATED do not hand-edit. Source: packages/web-modules/chat/chat-empty/chat-empty.yaml's a2ui.allowedParents/allowedChildren.
2254
+ // GENERATED: do not hand-edit. Source: packages/web-modules/agent-admin/agent-admin/agent-admin.yaml's a2ui.allowedParents/allowedChildren.
2255
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
2256
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2257
+
2258
+
2259
+ const TAG = "agent-admin-ui";
2260
+ const ALLOWED_PARENTS = [];
2261
+ const ALLOWED_CHILDREN = ["admin-roster-ui","admin-settings-ui","chat-shell"];
2262
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2263
+
2264
+
2265
+ RULES.push({
2266
+ id: "COMPOSITION-AGENT-ADMIN-UI",
2267
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2268
+ scope: 'shared',
2269
+ fileTypes: MARKUP_EXT,
2270
+ match(text) {
2271
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2272
+ },
2273
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2274
+ fixtures: {
2275
+ "smelly": [
2276
+ "<agent-admin-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></agent-admin-ui>"
2277
+ ],
2278
+ "clean": [
2279
+ "<agent-admin-ui><admin-roster-ui></admin-roster-ui></agent-admin-ui>",
2280
+ "<agent-admin-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></agent-admin-ui>"
2281
+ ]
2282
+ },
2283
+ });
2284
+ }
2285
+
2286
+ {
2287
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/avatar/avatar-group.yaml's a2ui.allowedParents/allowedChildren.
2288
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
2289
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2290
+
2291
+
2292
+ const TAG = "avatar-group-ui";
2293
+ const ALLOWED_PARENTS = [];
2294
+ const ALLOWED_CHILDREN = ["avatar-ui"];
2295
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2296
+
2297
+
2298
+ RULES.push({
2299
+ id: "COMPOSITION-AVATAR-GROUP-UI",
2300
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2301
+ scope: 'shared',
2302
+ fileTypes: MARKUP_EXT,
2303
+ match(text) {
2304
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2305
+ },
2306
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2307
+ fixtures: {
2308
+ "smelly": [
2309
+ "<avatar-group-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></avatar-group-ui>"
2310
+ ],
2311
+ "clean": [
2312
+ "<avatar-group-ui><avatar-ui></avatar-ui></avatar-group-ui>",
2313
+ "<avatar-group-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></avatar-group-ui>"
2314
+ ]
2315
+ },
2316
+ });
2317
+ }
2318
+
2319
+ {
2320
+ // GENERATED: do not hand-edit. Source: packages/web-modules/chat/chat-composer/chat-composer.yaml's a2ui.allowedParents/allowedChildren.
2321
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
2322
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2323
+
2324
+
2325
+ const TAG = "chat-composer";
2326
+ const ALLOWED_PARENTS = ["chat-shell"];
2327
+ const ALLOWED_CHILDREN = [];
2328
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2329
+
2330
+
2331
+ RULES.push({
2332
+ id: "COMPOSITION-CHAT-COMPOSER",
2333
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2334
+ scope: 'shared',
2335
+ fileTypes: MARKUP_EXT,
2336
+ match(text) {
2337
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2338
+ },
2339
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2340
+ fixtures: {
2341
+ "smelly": [
2342
+ "<div><chat-composer></chat-composer></div>"
2343
+ ],
2344
+ "clean": [
2345
+ "<chat-shell><chat-composer></chat-composer></chat-shell>"
2346
+ ]
2347
+ },
2348
+ });
2349
+ }
2350
+
2351
+ {
2352
+ // GENERATED: do not hand-edit. Source: packages/web-modules/chat/chat-empty/chat-empty.yaml's a2ui.allowedParents/allowedChildren.
1688
2353
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1689
2354
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1690
2355
 
@@ -1692,15 +2357,16 @@ RULES.push({
1692
2357
  const TAG = "chat-empty";
1693
2358
  const ALLOWED_PARENTS = ["chat-thread"];
1694
2359
  const ALLOWED_CHILDREN = [];
2360
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1695
2361
 
1696
2362
 
1697
2363
  RULES.push({
1698
2364
  id: "COMPOSITION-CHAT-EMPTY",
1699
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2365
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1700
2366
  scope: 'shared',
1701
2367
  fileTypes: MARKUP_EXT,
1702
2368
  match(text) {
1703
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2369
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1704
2370
  },
1705
2371
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1706
2372
  fixtures: {
@@ -1715,7 +2381,7 @@ RULES.push({
1715
2381
  }
1716
2382
 
1717
2383
  {
1718
- // GENERATED do not hand-edit. Source: packages/web-modules/chat/chat-header/chat-header.yaml's a2ui.allowedParents/allowedChildren.
2384
+ // GENERATED: do not hand-edit. Source: packages/web-modules/chat/chat-header/chat-header.yaml's a2ui.allowedParents/allowedChildren.
1719
2385
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1720
2386
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1721
2387
 
@@ -1723,15 +2389,16 @@ RULES.push({
1723
2389
  const TAG = "chat-header";
1724
2390
  const ALLOWED_PARENTS = ["chat-shell","chat-sidebar"];
1725
2391
  const ALLOWED_CHILDREN = [];
2392
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1726
2393
 
1727
2394
 
1728
2395
  RULES.push({
1729
2396
  id: "COMPOSITION-CHAT-HEADER",
1730
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2397
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1731
2398
  scope: 'shared',
1732
2399
  fileTypes: MARKUP_EXT,
1733
2400
  match(text) {
1734
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2401
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1735
2402
  },
1736
2403
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1737
2404
  fixtures: {
@@ -1746,23 +2413,24 @@ RULES.push({
1746
2413
  }
1747
2414
 
1748
2415
  {
1749
- // GENERATED do not hand-edit. Source: packages/web-modules/chat/chat-shell/chat-shell.yaml's a2ui.allowedParents/allowedChildren.
2416
+ // GENERATED: do not hand-edit. Source: packages/web-modules/chat/chat-shell/chat-shell.yaml's a2ui.allowedParents/allowedChildren.
1750
2417
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1751
2418
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1752
2419
 
1753
2420
 
1754
2421
  const TAG = "chat-shell";
1755
- const ALLOWED_PARENTS = ["Surface"];
2422
+ const ALLOWED_PARENTS = ["Surface","agent-admin-ui","pane-ui"];
1756
2423
  const ALLOWED_CHILDREN = [];
2424
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1757
2425
 
1758
2426
 
1759
2427
  RULES.push({
1760
2428
  id: "COMPOSITION-CHAT-SHELL",
1761
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2429
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1762
2430
  scope: 'shared',
1763
2431
  fileTypes: MARKUP_EXT,
1764
2432
  match(text) {
1765
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2433
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1766
2434
  },
1767
2435
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1768
2436
  fixtures: {
@@ -1777,7 +2445,7 @@ RULES.push({
1777
2445
  }
1778
2446
 
1779
2447
  {
1780
- // GENERATED do not hand-edit. Source: packages/web-modules/chat/chat-sidebar/chat-sidebar.yaml's a2ui.allowedParents/allowedChildren.
2448
+ // GENERATED: do not hand-edit. Source: packages/web-modules/chat/chat-sidebar/chat-sidebar.yaml's a2ui.allowedParents/allowedChildren.
1781
2449
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1782
2450
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1783
2451
 
@@ -1785,15 +2453,16 @@ RULES.push({
1785
2453
  const TAG = "chat-sidebar";
1786
2454
  const ALLOWED_PARENTS = ["chat-shell"];
1787
2455
  const ALLOWED_CHILDREN = [];
2456
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1788
2457
 
1789
2458
 
1790
2459
  RULES.push({
1791
2460
  id: "COMPOSITION-CHAT-SIDEBAR",
1792
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2461
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1793
2462
  scope: 'shared',
1794
2463
  fileTypes: MARKUP_EXT,
1795
2464
  match(text) {
1796
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2465
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1797
2466
  },
1798
2467
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1799
2468
  fixtures: {
@@ -1808,7 +2477,7 @@ RULES.push({
1808
2477
  }
1809
2478
 
1810
2479
  {
1811
- // GENERATED do not hand-edit. Source: packages/web-modules/chat/chat-status/chat-status.yaml's a2ui.allowedParents/allowedChildren.
2480
+ // GENERATED: do not hand-edit. Source: packages/web-modules/chat/chat-status/chat-status.yaml's a2ui.allowedParents/allowedChildren.
1812
2481
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1813
2482
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1814
2483
 
@@ -1816,15 +2485,16 @@ RULES.push({
1816
2485
  const TAG = "chat-status";
1817
2486
  const ALLOWED_PARENTS = ["chat-header"];
1818
2487
  const ALLOWED_CHILDREN = [];
2488
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1819
2489
 
1820
2490
 
1821
2491
  RULES.push({
1822
2492
  id: "COMPOSITION-CHAT-STATUS",
1823
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2493
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1824
2494
  scope: 'shared',
1825
2495
  fileTypes: MARKUP_EXT,
1826
2496
  match(text) {
1827
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2497
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1828
2498
  },
1829
2499
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1830
2500
  fixtures: {
@@ -1839,7 +2509,7 @@ RULES.push({
1839
2509
  }
1840
2510
 
1841
2511
  {
1842
- // GENERATED do not hand-edit. Source: packages/web-modules/chat/chat-thread/chat-thread.yaml's a2ui.allowedParents/allowedChildren.
2512
+ // GENERATED: do not hand-edit. Source: packages/web-modules/chat/chat-thread/chat-thread.yaml's a2ui.allowedParents/allowedChildren.
1843
2513
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1844
2514
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1845
2515
 
@@ -1847,15 +2517,16 @@ RULES.push({
1847
2517
  const TAG = "chat-thread";
1848
2518
  const ALLOWED_PARENTS = ["chat-shell"];
1849
2519
  const ALLOWED_CHILDREN = [];
2520
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1850
2521
 
1851
2522
 
1852
2523
  RULES.push({
1853
2524
  id: "COMPOSITION-CHAT-THREAD",
1854
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2525
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1855
2526
  scope: 'shared',
1856
2527
  fileTypes: MARKUP_EXT,
1857
2528
  match(text) {
1858
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2529
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1859
2530
  },
1860
2531
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1861
2532
  fixtures: {
@@ -1870,7 +2541,7 @@ RULES.push({
1870
2541
  }
1871
2542
 
1872
2543
  {
1873
- // GENERATED do not hand-edit. Source: packages/web-components/components/choice-card/choice-card.yaml's a2ui.allowedParents/allowedChildren.
2544
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/choice-card/choice-card.yaml's a2ui.allowedParents/allowedChildren.
1874
2545
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1875
2546
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1876
2547
 
@@ -1878,15 +2549,16 @@ RULES.push({
1878
2549
  const TAG = "choice-card-ui";
1879
2550
  const ALLOWED_PARENTS = [];
1880
2551
  const ALLOWED_CHILDREN = ["choice-ui"];
2552
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1881
2553
 
1882
2554
 
1883
2555
  RULES.push({
1884
2556
  id: "COMPOSITION-CHOICE-CARD-UI",
1885
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2557
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1886
2558
  scope: 'shared',
1887
2559
  fileTypes: MARKUP_EXT,
1888
2560
  match(text) {
1889
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2561
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1890
2562
  },
1891
2563
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1892
2564
  fixtures: {
@@ -1894,14 +2566,15 @@ RULES.push({
1894
2566
  "<choice-card-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></choice-card-ui>"
1895
2567
  ],
1896
2568
  "clean": [
1897
- "<choice-card-ui><choice-ui></choice-ui></choice-card-ui>"
2569
+ "<choice-card-ui><choice-ui></choice-ui></choice-card-ui>",
2570
+ "<choice-card-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></choice-card-ui>"
1898
2571
  ]
1899
2572
  },
1900
2573
  });
1901
2574
  }
1902
2575
 
1903
2576
  {
1904
- // GENERATED do not hand-edit. Source: packages/web-components/components/choice-card/choice.yaml's a2ui.allowedParents/allowedChildren.
2577
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/choice-card/choice.yaml's a2ui.allowedParents/allowedChildren.
1905
2578
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1906
2579
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1907
2580
 
@@ -1909,15 +2582,16 @@ RULES.push({
1909
2582
  const TAG = "choice-ui";
1910
2583
  const ALLOWED_PARENTS = ["choice-card-ui"];
1911
2584
  const ALLOWED_CHILDREN = [];
2585
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1912
2586
 
1913
2587
 
1914
2588
  RULES.push({
1915
2589
  id: "COMPOSITION-CHOICE-UI",
1916
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2590
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1917
2591
  scope: 'shared',
1918
2592
  fileTypes: MARKUP_EXT,
1919
2593
  match(text) {
1920
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2594
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1921
2595
  },
1922
2596
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1923
2597
  fixtures: {
@@ -1932,7 +2606,7 @@ RULES.push({
1932
2606
  }
1933
2607
 
1934
2608
  {
1935
- // GENERATED do not hand-edit. Source: packages/web-components/components/drilldown/drilldown.yaml's a2ui.allowedParents/allowedChildren.
2609
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/drilldown/drilldown.yaml's a2ui.allowedParents/allowedChildren.
1936
2610
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1937
2611
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1938
2612
 
@@ -1940,15 +2614,16 @@ RULES.push({
1940
2614
  const TAG = "drilldown-ui";
1941
2615
  const ALLOWED_PARENTS = [];
1942
2616
  const ALLOWED_CHILDREN = ["breadcrumb-ui"];
2617
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1943
2618
 
1944
2619
 
1945
2620
  RULES.push({
1946
2621
  id: "COMPOSITION-DRILLDOWN-UI",
1947
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2622
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1948
2623
  scope: 'shared',
1949
2624
  fileTypes: MARKUP_EXT,
1950
2625
  match(text) {
1951
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2626
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1952
2627
  },
1953
2628
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1954
2629
  fixtures: {
@@ -1956,14 +2631,15 @@ RULES.push({
1956
2631
  "<drilldown-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></drilldown-ui>"
1957
2632
  ],
1958
2633
  "clean": [
1959
- "<drilldown-ui><breadcrumb-ui></breadcrumb-ui></drilldown-ui>"
2634
+ "<drilldown-ui><breadcrumb-ui></breadcrumb-ui></drilldown-ui>",
2635
+ "<drilldown-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></drilldown-ui>"
1960
2636
  ]
1961
2637
  },
1962
2638
  });
1963
2639
  }
1964
2640
 
1965
2641
  {
1966
- // GENERATED do not hand-edit. Source: packages/web-modules/editor/editor-canvas-empty/editor-canvas-empty.yaml's a2ui.allowedParents/allowedChildren.
2642
+ // GENERATED: do not hand-edit. Source: packages/web-modules/editor/editor-canvas-empty/editor-canvas-empty.yaml's a2ui.allowedParents/allowedChildren.
1967
2643
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1968
2644
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1969
2645
 
@@ -1971,15 +2647,16 @@ RULES.push({
1971
2647
  const TAG = "editor-canvas-empty";
1972
2648
  const ALLOWED_PARENTS = ["editor-canvas"];
1973
2649
  const ALLOWED_CHILDREN = [];
2650
+ const ALLOWED_CHILDREN_BY_SLOT = null;
1974
2651
 
1975
2652
 
1976
2653
  RULES.push({
1977
2654
  id: "COMPOSITION-EDITOR-CANVAS-EMPTY",
1978
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2655
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
1979
2656
  scope: 'shared',
1980
2657
  fileTypes: MARKUP_EXT,
1981
2658
  match(text) {
1982
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2659
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1983
2660
  },
1984
2661
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1985
2662
  fixtures: {
@@ -1994,7 +2671,7 @@ RULES.push({
1994
2671
  }
1995
2672
 
1996
2673
  {
1997
- // GENERATED do not hand-edit. Source: packages/web-modules/editor/editor-canvas-toolbar/editor-canvas-toolbar.yaml's a2ui.allowedParents/allowedChildren.
2674
+ // GENERATED: do not hand-edit. Source: packages/web-modules/editor/editor-canvas-toolbar/editor-canvas-toolbar.yaml's a2ui.allowedParents/allowedChildren.
1998
2675
  // Rebuild: node scripts/build/gen-composition-rules.mjs
1999
2676
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2000
2677
 
@@ -2002,15 +2679,16 @@ RULES.push({
2002
2679
  const TAG = "editor-canvas-toolbar";
2003
2680
  const ALLOWED_PARENTS = ["editor-canvas"];
2004
2681
  const ALLOWED_CHILDREN = [];
2682
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2005
2683
 
2006
2684
 
2007
2685
  RULES.push({
2008
2686
  id: "COMPOSITION-EDITOR-CANVAS-TOOLBAR",
2009
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2687
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2010
2688
  scope: 'shared',
2011
2689
  fileTypes: MARKUP_EXT,
2012
2690
  match(text) {
2013
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2691
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2014
2692
  },
2015
2693
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2016
2694
  fixtures: {
@@ -2025,7 +2703,7 @@ RULES.push({
2025
2703
  }
2026
2704
 
2027
2705
  {
2028
- // GENERATED do not hand-edit. Source: packages/web-modules/editor/editor-canvas/editor-canvas.yaml's a2ui.allowedParents/allowedChildren.
2706
+ // GENERATED: do not hand-edit. Source: packages/web-modules/editor/editor-canvas/editor-canvas.yaml's a2ui.allowedParents/allowedChildren.
2029
2707
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2030
2708
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2031
2709
 
@@ -2033,15 +2711,16 @@ RULES.push({
2033
2711
  const TAG = "editor-canvas";
2034
2712
  const ALLOWED_PARENTS = ["editor-shell"];
2035
2713
  const ALLOWED_CHILDREN = [];
2714
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2036
2715
 
2037
2716
 
2038
2717
  RULES.push({
2039
2718
  id: "COMPOSITION-EDITOR-CANVAS",
2040
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2719
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2041
2720
  scope: 'shared',
2042
2721
  fileTypes: MARKUP_EXT,
2043
2722
  match(text) {
2044
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2723
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2045
2724
  },
2046
2725
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2047
2726
  fixtures: {
@@ -2056,7 +2735,7 @@ RULES.push({
2056
2735
  }
2057
2736
 
2058
2737
  {
2059
- // GENERATED do not hand-edit. Source: packages/web-modules/editor/editor-shell/editor-shell.yaml's a2ui.allowedParents/allowedChildren.
2738
+ // GENERATED: do not hand-edit. Source: packages/web-modules/editor/editor-shell/editor-shell.yaml's a2ui.allowedParents/allowedChildren.
2060
2739
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2061
2740
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2062
2741
 
@@ -2064,15 +2743,16 @@ RULES.push({
2064
2743
  const TAG = "editor-shell";
2065
2744
  const ALLOWED_PARENTS = ["Surface"];
2066
2745
  const ALLOWED_CHILDREN = [];
2746
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2067
2747
 
2068
2748
 
2069
2749
  RULES.push({
2070
2750
  id: "COMPOSITION-EDITOR-SHELL",
2071
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2751
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2072
2752
  scope: 'shared',
2073
2753
  fileTypes: MARKUP_EXT,
2074
2754
  match(text) {
2075
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2755
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2076
2756
  },
2077
2757
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2078
2758
  fixtures: {
@@ -2087,7 +2767,7 @@ RULES.push({
2087
2767
  }
2088
2768
 
2089
2769
  {
2090
- // GENERATED do not hand-edit. Source: packages/web-modules/editor/editor-sidebar/editor-sidebar.yaml's a2ui.allowedParents/allowedChildren.
2770
+ // GENERATED: do not hand-edit. Source: packages/web-modules/editor/editor-sidebar/editor-sidebar.yaml's a2ui.allowedParents/allowedChildren.
2091
2771
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2092
2772
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2093
2773
 
@@ -2095,15 +2775,16 @@ RULES.push({
2095
2775
  const TAG = "editor-sidebar";
2096
2776
  const ALLOWED_PARENTS = ["editor-shell"];
2097
2777
  const ALLOWED_CHILDREN = [];
2778
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2098
2779
 
2099
2780
 
2100
2781
  RULES.push({
2101
2782
  id: "COMPOSITION-EDITOR-SIDEBAR",
2102
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2783
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2103
2784
  scope: 'shared',
2104
2785
  fileTypes: MARKUP_EXT,
2105
2786
  match(text) {
2106
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2787
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2107
2788
  },
2108
2789
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2109
2790
  fixtures: {
@@ -2118,7 +2799,7 @@ RULES.push({
2118
2799
  }
2119
2800
 
2120
2801
  {
2121
- // GENERATED do not hand-edit. Source: packages/web-modules/editor/editor-statusbar/editor-statusbar.yaml's a2ui.allowedParents/allowedChildren.
2802
+ // GENERATED: do not hand-edit. Source: packages/web-modules/editor/editor-statusbar/editor-statusbar.yaml's a2ui.allowedParents/allowedChildren.
2122
2803
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2123
2804
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2124
2805
 
@@ -2126,15 +2807,16 @@ RULES.push({
2126
2807
  const TAG = "editor-statusbar";
2127
2808
  const ALLOWED_PARENTS = ["editor-shell"];
2128
2809
  const ALLOWED_CHILDREN = [];
2810
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2129
2811
 
2130
2812
 
2131
2813
  RULES.push({
2132
2814
  id: "COMPOSITION-EDITOR-STATUSBAR",
2133
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2815
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2134
2816
  scope: 'shared',
2135
2817
  fileTypes: MARKUP_EXT,
2136
2818
  match(text) {
2137
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2819
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2138
2820
  },
2139
2821
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2140
2822
  fixtures: {
@@ -2149,7 +2831,7 @@ RULES.push({
2149
2831
  }
2150
2832
 
2151
2833
  {
2152
- // GENERATED do not hand-edit. Source: packages/web-modules/editor/editor-toolbar/editor-toolbar.yaml's a2ui.allowedParents/allowedChildren.
2834
+ // GENERATED: do not hand-edit. Source: packages/web-modules/editor/editor-toolbar/editor-toolbar.yaml's a2ui.allowedParents/allowedChildren.
2153
2835
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2154
2836
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2155
2837
 
@@ -2157,15 +2839,16 @@ RULES.push({
2157
2839
  const TAG = "editor-toolbar";
2158
2840
  const ALLOWED_PARENTS = ["editor-shell"];
2159
2841
  const ALLOWED_CHILDREN = [];
2842
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2160
2843
 
2161
2844
 
2162
2845
  RULES.push({
2163
2846
  id: "COMPOSITION-EDITOR-TOOLBAR",
2164
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2847
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2165
2848
  scope: 'shared',
2166
2849
  fileTypes: MARKUP_EXT,
2167
2850
  match(text) {
2168
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2851
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2169
2852
  },
2170
2853
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2171
2854
  fixtures: {
@@ -2180,7 +2863,39 @@ RULES.push({
2180
2863
  }
2181
2864
 
2182
2865
  {
2183
- // GENERATED do not hand-edit. Source: packages/web-components/components/feed/feed-item.yaml's a2ui.allowedParents/allowedChildren.
2866
+ // GENERATED: do not hand-edit. Source: packages/web-modules/shell/embed-shell/embed-shell.yaml's a2ui.allowedParents/allowedChildren.
2867
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
2868
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2869
+
2870
+
2871
+ const TAG = "embed-shell";
2872
+ const ALLOWED_PARENTS = ["Surface","theme-provider"];
2873
+ const ALLOWED_CHILDREN = [];
2874
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2875
+
2876
+
2877
+ RULES.push({
2878
+ id: "COMPOSITION-EMBED-SHELL",
2879
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2880
+ scope: 'shared',
2881
+ fileTypes: MARKUP_EXT,
2882
+ match(text) {
2883
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2884
+ },
2885
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2886
+ fixtures: {
2887
+ "smelly": [
2888
+ "<col-ui><embed-shell></embed-shell></col-ui>"
2889
+ ],
2890
+ "clean": [
2891
+ "<embed-shell></embed-shell>"
2892
+ ]
2893
+ },
2894
+ });
2895
+ }
2896
+
2897
+ {
2898
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/feed/feed-item.yaml's a2ui.allowedParents/allowedChildren.
2184
2899
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2185
2900
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2186
2901
 
@@ -2188,15 +2903,16 @@ RULES.push({
2188
2903
  const TAG = "feed-item-ui";
2189
2904
  const ALLOWED_PARENTS = ["feed-ui"];
2190
2905
  const ALLOWED_CHILDREN = [];
2906
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2191
2907
 
2192
2908
 
2193
2909
  RULES.push({
2194
2910
  id: "COMPOSITION-FEED-ITEM-UI",
2195
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
2911
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2196
2912
  scope: 'shared',
2197
2913
  fileTypes: MARKUP_EXT,
2198
2914
  match(text) {
2199
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
2915
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2200
2916
  },
2201
2917
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2202
2918
  fixtures: {
@@ -2211,7 +2927,7 @@ RULES.push({
2211
2927
  }
2212
2928
 
2213
2929
  {
2214
- // GENERATED do not hand-edit. Source: packages/web-components/components/feed/feed.yaml's a2ui.allowedParents/allowedChildren.
2930
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/feed/feed.yaml's a2ui.allowedParents/allowedChildren.
2215
2931
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2216
2932
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2217
2933
 
@@ -2219,185 +2935,520 @@ RULES.push({
2219
2935
  const TAG = "feed-ui";
2220
2936
  const ALLOWED_PARENTS = [];
2221
2937
  const ALLOWED_CHILDREN = ["feed-item-ui"];
2938
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2939
+
2940
+
2941
+ RULES.push({
2942
+ id: "COMPOSITION-FEED-UI",
2943
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2944
+ scope: 'shared',
2945
+ fileTypes: MARKUP_EXT,
2946
+ match(text) {
2947
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2948
+ },
2949
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2950
+ fixtures: {
2951
+ "smelly": [
2952
+ "<feed-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></feed-ui>"
2953
+ ],
2954
+ "clean": [
2955
+ "<feed-ui><feed-item-ui></feed-item-ui></feed-ui>",
2956
+ "<feed-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></feed-ui>"
2957
+ ]
2958
+ },
2959
+ });
2960
+ }
2961
+
2962
+ {
2963
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/fields/fields.yaml's a2ui.allowedParents/allowedChildren.
2964
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
2965
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2966
+
2967
+
2968
+ const TAG = "fields-ui";
2969
+ const ALLOWED_PARENTS = [];
2970
+ const ALLOWED_CHILDREN = ["field-ui"];
2971
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2972
+
2973
+
2974
+ RULES.push({
2975
+ id: "COMPOSITION-FIELDS-UI",
2976
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2977
+ scope: 'shared',
2978
+ fileTypes: MARKUP_EXT,
2979
+ match(text) {
2980
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2981
+ },
2982
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2983
+ fixtures: {
2984
+ "smelly": [
2985
+ "<fields-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></fields-ui>"
2986
+ ],
2987
+ "clean": [
2988
+ "<fields-ui><field-ui></field-ui></fields-ui>",
2989
+ "<fields-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></fields-ui>"
2990
+ ]
2991
+ },
2992
+ });
2993
+ }
2994
+
2995
+ {
2996
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/footer/footer.yaml's a2ui.allowedParents/allowedChildren.
2997
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
2998
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2999
+
3000
+
3001
+ const TAG = "footer-ui";
3002
+ const ALLOWED_PARENTS = ["page-ui"];
3003
+ const ALLOWED_CHILDREN = [];
3004
+ const ALLOWED_CHILDREN_BY_SLOT = null;
3005
+
3006
+
3007
+ RULES.push({
3008
+ id: "COMPOSITION-FOOTER-UI",
3009
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
3010
+ scope: 'shared',
3011
+ fileTypes: MARKUP_EXT,
3012
+ match(text) {
3013
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
3014
+ },
3015
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
3016
+ fixtures: {
3017
+ "smelly": [
3018
+ "<div><footer-ui></footer-ui></div>"
3019
+ ],
3020
+ "clean": [
3021
+ "<page-ui><footer-ui></footer-ui></page-ui>"
3022
+ ]
3023
+ },
3024
+ });
3025
+ }
3026
+
3027
+ {
3028
+ // GENERATED: do not hand-edit. Source: packages/web-modules/form/form-popover/form-popover.yaml's a2ui.allowedParents/allowedChildren.
3029
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
3030
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
3031
+
3032
+
3033
+ const TAG = "form-popover-ui";
3034
+ const ALLOWED_PARENTS = [];
3035
+ const ALLOWED_CHILDREN = ["check-ui","radio-ui","input-ui","textarea-ui","divider-ui","field-ui"];
3036
+ const ALLOWED_CHILDREN_BY_SLOT = null;
3037
+
3038
+
3039
+ RULES.push({
3040
+ id: "COMPOSITION-FORM-POPOVER-UI",
3041
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
3042
+ scope: 'shared',
3043
+ fileTypes: MARKUP_EXT,
3044
+ match(text) {
3045
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
3046
+ },
3047
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
3048
+ fixtures: {
3049
+ "smelly": [
3050
+ "<form-popover-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></form-popover-ui>"
3051
+ ],
3052
+ "clean": [
3053
+ "<form-popover-ui><check-ui></check-ui></form-popover-ui>",
3054
+ "<form-popover-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></form-popover-ui>"
3055
+ ]
3056
+ },
3057
+ });
3058
+ }
3059
+
3060
+ {
3061
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/header/header.yaml's a2ui.allowedParents/allowedChildren.
3062
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
3063
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
3064
+
3065
+
3066
+ const TAG = "header-ui";
3067
+ const ALLOWED_PARENTS = ["page-ui"];
3068
+ const ALLOWED_CHILDREN = [];
3069
+ const ALLOWED_CHILDREN_BY_SLOT = null;
3070
+
3071
+
3072
+ RULES.push({
3073
+ id: "COMPOSITION-HEADER-UI",
3074
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
3075
+ scope: 'shared',
3076
+ fileTypes: MARKUP_EXT,
3077
+ match(text) {
3078
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
3079
+ },
3080
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
3081
+ fixtures: {
3082
+ "smelly": [
3083
+ "<div><header-ui></header-ui></div>"
3084
+ ],
3085
+ "clean": [
3086
+ "<page-ui><header-ui></header-ui></page-ui>"
3087
+ ]
3088
+ },
3089
+ });
3090
+ }
3091
+
3092
+ {
3093
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/list/list-item.yaml's a2ui.allowedParents/allowedChildren.
3094
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
3095
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
3096
+
3097
+
3098
+ const TAG = "list-item-ui";
3099
+ const ALLOWED_PARENTS = ["list-ui"];
3100
+ const ALLOWED_CHILDREN = [];
3101
+ const ALLOWED_CHILDREN_BY_SLOT = null;
3102
+
3103
+
3104
+ RULES.push({
3105
+ id: "COMPOSITION-LIST-ITEM-UI",
3106
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
3107
+ scope: 'shared',
3108
+ fileTypes: MARKUP_EXT,
3109
+ match(text) {
3110
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
3111
+ },
3112
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
3113
+ fixtures: {
3114
+ "smelly": [
3115
+ "<div><list-item-ui></list-item-ui></div>"
3116
+ ],
3117
+ "clean": [
3118
+ "<list-ui><list-item-ui></list-item-ui></list-ui>"
3119
+ ]
3120
+ },
3121
+ });
3122
+ }
3123
+
3124
+ {
3125
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/menu/menu-divider.yaml's a2ui.allowedParents/allowedChildren.
3126
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
3127
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
3128
+
3129
+
3130
+ const TAG = "menu-divider-ui";
3131
+ const ALLOWED_PARENTS = ["menu-ui","context-menu-ui"];
3132
+ const ALLOWED_CHILDREN = [];
3133
+ const ALLOWED_CHILDREN_BY_SLOT = null;
3134
+
3135
+
3136
+ RULES.push({
3137
+ id: "COMPOSITION-MENU-DIVIDER-UI",
3138
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
3139
+ scope: 'shared',
3140
+ fileTypes: MARKUP_EXT,
3141
+ match(text) {
3142
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
3143
+ },
3144
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
3145
+ fixtures: {
3146
+ "smelly": [
3147
+ "<div><menu-divider-ui></menu-divider-ui></div>"
3148
+ ],
3149
+ "clean": [
3150
+ "<menu-ui><menu-divider-ui></menu-divider-ui></menu-ui>"
3151
+ ]
3152
+ },
3153
+ });
3154
+ }
3155
+
3156
+ {
3157
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/menu/menu-item.yaml's a2ui.allowedParents/allowedChildren.
3158
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
3159
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
3160
+
3161
+
3162
+ const TAG = "menu-item-ui";
3163
+ const ALLOWED_PARENTS = ["menu-ui","context-menu-ui"];
3164
+ const ALLOWED_CHILDREN = [];
3165
+ const ALLOWED_CHILDREN_BY_SLOT = null;
3166
+
3167
+
3168
+ RULES.push({
3169
+ id: "COMPOSITION-MENU-ITEM-UI",
3170
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
3171
+ scope: 'shared',
3172
+ fileTypes: MARKUP_EXT,
3173
+ match(text) {
3174
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
3175
+ },
3176
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
3177
+ fixtures: {
3178
+ "smelly": [
3179
+ "<div><menu-item-ui></menu-item-ui></div>"
3180
+ ],
3181
+ "clean": [
3182
+ "<menu-ui><menu-item-ui></menu-item-ui></menu-ui>"
3183
+ ]
3184
+ },
3185
+ });
3186
+ }
3187
+
3188
+ {
3189
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/menu/menu-label.yaml's a2ui.allowedParents/allowedChildren.
3190
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
3191
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
3192
+
3193
+
3194
+ const TAG = "menu-label-ui";
3195
+ const ALLOWED_PARENTS = ["menu-ui"];
3196
+ const ALLOWED_CHILDREN = [];
3197
+ const ALLOWED_CHILDREN_BY_SLOT = null;
3198
+
3199
+
3200
+ RULES.push({
3201
+ id: "COMPOSITION-MENU-LABEL-UI",
3202
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
3203
+ scope: 'shared',
3204
+ fileTypes: MARKUP_EXT,
3205
+ match(text) {
3206
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
3207
+ },
3208
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
3209
+ fixtures: {
3210
+ "smelly": [
3211
+ "<div><menu-label-ui></menu-label-ui></div>"
3212
+ ],
3213
+ "clean": [
3214
+ "<menu-ui><menu-label-ui></menu-label-ui></menu-ui>"
3215
+ ]
3216
+ },
3217
+ });
3218
+ }
3219
+
3220
+ {
3221
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/menu/menu.yaml's a2ui.allowedParents/allowedChildren.
3222
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
3223
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
3224
+
3225
+
3226
+ const TAG = "menu-ui";
3227
+ const ALLOWED_PARENTS = [];
3228
+ const ALLOWED_CHILDREN = ["menu-item-ui","menu-divider-ui","menu-label-ui"];
3229
+ const ALLOWED_CHILDREN_BY_SLOT = null;
3230
+
3231
+
3232
+ RULES.push({
3233
+ id: "COMPOSITION-MENU-UI",
3234
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
3235
+ scope: 'shared',
3236
+ fileTypes: MARKUP_EXT,
3237
+ match(text) {
3238
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
3239
+ },
3240
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
3241
+ fixtures: {
3242
+ "smelly": [
3243
+ "<menu-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></menu-ui>"
3244
+ ],
3245
+ "clean": [
3246
+ "<menu-ui><menu-item-ui></menu-item-ui></menu-ui>",
3247
+ "<menu-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></menu-ui>"
3248
+ ]
3249
+ },
3250
+ });
3251
+ }
3252
+
3253
+ {
3254
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/nav-group/nav-group.yaml's a2ui.allowedParents/allowedChildren.
3255
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
3256
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
3257
+
3258
+
3259
+ const TAG = "nav-group-ui";
3260
+ const ALLOWED_PARENTS = ["nav-ui"];
3261
+ const ALLOWED_CHILDREN = ["nav-item-ui"];
3262
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2222
3263
 
2223
3264
 
2224
3265
  RULES.push({
2225
- id: "COMPOSITION-FEED-UI",
2226
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3266
+ id: "COMPOSITION-NAV-GROUP-UI",
3267
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2227
3268
  scope: 'shared',
2228
3269
  fileTypes: MARKUP_EXT,
2229
3270
  match(text) {
2230
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3271
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2231
3272
  },
2232
3273
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2233
3274
  fixtures: {
2234
3275
  "smelly": [
2235
- "<feed-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></feed-ui>"
3276
+ "<div><nav-group-ui></nav-group-ui></div>",
3277
+ "<nav-group-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></nav-group-ui>"
2236
3278
  ],
2237
3279
  "clean": [
2238
- "<feed-ui><feed-item-ui></feed-item-ui></feed-ui>"
3280
+ "<nav-ui><nav-group-ui></nav-group-ui></nav-ui>",
3281
+ "<nav-ui><nav-group-ui><nav-item-ui></nav-item-ui></nav-group-ui></nav-ui>",
3282
+ "<nav-ui><nav-group-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></nav-group-ui></nav-ui>"
2239
3283
  ]
2240
3284
  },
2241
3285
  });
2242
3286
  }
2243
3287
 
2244
3288
  {
2245
- // GENERATED do not hand-edit. Source: packages/web-components/components/list/list-item.yaml's a2ui.allowedParents/allowedChildren.
3289
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/nav-item/nav-item.yaml's a2ui.allowedParents/allowedChildren.
2246
3290
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2247
3291
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2248
3292
 
2249
3293
 
2250
- const TAG = "list-item-ui";
2251
- const ALLOWED_PARENTS = ["list-ui"];
3294
+ const TAG = "nav-item-ui";
3295
+ const ALLOWED_PARENTS = ["nav-ui","nav-group-ui"];
2252
3296
  const ALLOWED_CHILDREN = [];
3297
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2253
3298
 
2254
3299
 
2255
3300
  RULES.push({
2256
- id: "COMPOSITION-LIST-ITEM-UI",
2257
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3301
+ id: "COMPOSITION-NAV-ITEM-UI",
3302
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2258
3303
  scope: 'shared',
2259
3304
  fileTypes: MARKUP_EXT,
2260
3305
  match(text) {
2261
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3306
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2262
3307
  },
2263
3308
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2264
3309
  fixtures: {
2265
3310
  "smelly": [
2266
- "<div><list-item-ui></list-item-ui></div>"
3311
+ "<div><nav-item-ui></nav-item-ui></div>"
2267
3312
  ],
2268
3313
  "clean": [
2269
- "<list-ui><list-item-ui></list-item-ui></list-ui>"
3314
+ "<nav-ui><nav-item-ui></nav-item-ui></nav-ui>"
2270
3315
  ]
2271
3316
  },
2272
3317
  });
2273
3318
  }
2274
3319
 
2275
3320
  {
2276
- // GENERATED do not hand-edit. Source: packages/web-components/components/menu/menu-divider.yaml's a2ui.allowedParents/allowedChildren.
3321
+ // GENERATED: do not hand-edit. Source: packages/web-modules/shell/page-scroll/page-scroll.yaml's a2ui.allowedParents/allowedChildren.
2277
3322
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2278
3323
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2279
3324
 
2280
3325
 
2281
- const TAG = "menu-divider-ui";
2282
- const ALLOWED_PARENTS = ["menu-ui"];
3326
+ const TAG = "page-scroll";
3327
+ const ALLOWED_PARENTS = ["admin-content"];
2283
3328
  const ALLOWED_CHILDREN = [];
3329
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2284
3330
 
2285
3331
 
2286
3332
  RULES.push({
2287
- id: "COMPOSITION-MENU-DIVIDER-UI",
2288
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3333
+ id: "COMPOSITION-PAGE-SCROLL",
3334
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2289
3335
  scope: 'shared',
2290
3336
  fileTypes: MARKUP_EXT,
2291
3337
  match(text) {
2292
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3338
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2293
3339
  },
2294
3340
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2295
3341
  fixtures: {
2296
3342
  "smelly": [
2297
- "<div><menu-divider-ui></menu-divider-ui></div>"
3343
+ "<div><page-scroll></page-scroll></div>"
2298
3344
  ],
2299
3345
  "clean": [
2300
- "<menu-ui><menu-divider-ui></menu-divider-ui></menu-ui>"
3346
+ "<admin-content><page-scroll></page-scroll></admin-content>"
2301
3347
  ]
2302
3348
  },
2303
3349
  });
2304
3350
  }
2305
3351
 
2306
3352
  {
2307
- // GENERATED do not hand-edit. Source: packages/web-components/components/menu/menu-item.yaml's a2ui.allowedParents/allowedChildren.
3353
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/radio-group/radio-group.yaml's a2ui.allowedParents/allowedChildren.
2308
3354
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2309
3355
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2310
3356
 
2311
3357
 
2312
- const TAG = "menu-item-ui";
2313
- const ALLOWED_PARENTS = ["menu-ui"];
2314
- const ALLOWED_CHILDREN = [];
3358
+ const TAG = "radio-group-ui";
3359
+ const ALLOWED_PARENTS = [];
3360
+ const ALLOWED_CHILDREN = ["radio-ui"];
3361
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2315
3362
 
2316
3363
 
2317
3364
  RULES.push({
2318
- id: "COMPOSITION-MENU-ITEM-UI",
2319
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3365
+ id: "COMPOSITION-RADIO-GROUP-UI",
3366
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2320
3367
  scope: 'shared',
2321
3368
  fileTypes: MARKUP_EXT,
2322
3369
  match(text) {
2323
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3370
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2324
3371
  },
2325
3372
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2326
3373
  fixtures: {
2327
3374
  "smelly": [
2328
- "<div><menu-item-ui></menu-item-ui></div>"
3375
+ "<radio-group-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></radio-group-ui>"
2329
3376
  ],
2330
3377
  "clean": [
2331
- "<menu-ui><menu-item-ui></menu-item-ui></menu-ui>"
3378
+ "<radio-group-ui><radio-ui></radio-ui></radio-group-ui>",
3379
+ "<radio-group-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></radio-group-ui>"
2332
3380
  ]
2333
3381
  },
2334
3382
  });
2335
3383
  }
2336
3384
 
2337
3385
  {
2338
- // GENERATED do not hand-edit. Source: packages/web-components/components/menu/menu-label.yaml's a2ui.allowedParents/allowedChildren.
3386
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/search/search.yaml's a2ui.allowedParents/allowedChildren.
2339
3387
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2340
3388
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2341
3389
 
2342
3390
 
2343
- const TAG = "menu-label-ui";
2344
- const ALLOWED_PARENTS = ["menu-ui"];
2345
- const ALLOWED_CHILDREN = [];
3391
+ const TAG = "search-ui";
3392
+ const ALLOWED_PARENTS = [];
3393
+ const ALLOWED_CHILDREN = ["input-ui"];
3394
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2346
3395
 
2347
3396
 
2348
3397
  RULES.push({
2349
- id: "COMPOSITION-MENU-LABEL-UI",
2350
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3398
+ id: "COMPOSITION-SEARCH-UI",
3399
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2351
3400
  scope: 'shared',
2352
3401
  fileTypes: MARKUP_EXT,
2353
3402
  match(text) {
2354
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3403
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2355
3404
  },
2356
3405
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2357
3406
  fixtures: {
2358
3407
  "smelly": [
2359
- "<div><menu-label-ui></menu-label-ui></div>"
3408
+ "<search-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></search-ui>"
2360
3409
  ],
2361
3410
  "clean": [
2362
- "<menu-ui><menu-label-ui></menu-label-ui></menu-ui>"
3411
+ "<search-ui><input-ui></input-ui></search-ui>",
3412
+ "<search-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></search-ui>"
2363
3413
  ]
2364
3414
  },
2365
3415
  });
2366
3416
  }
2367
3417
 
2368
3418
  {
2369
- // GENERATED do not hand-edit. Source: packages/web-modules/shell/page-scroll/page-scroll.yaml's a2ui.allowedParents/allowedChildren.
3419
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/section/section.yaml's a2ui.allowedParents/allowedChildren.
2370
3420
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2371
3421
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2372
3422
 
2373
3423
 
2374
- const TAG = "page-scroll";
2375
- const ALLOWED_PARENTS = ["admin-content"];
3424
+ const TAG = "section-ui";
3425
+ const ALLOWED_PARENTS = ["page-ui","admin-sidebar"];
2376
3426
  const ALLOWED_CHILDREN = [];
3427
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2377
3428
 
2378
3429
 
2379
3430
  RULES.push({
2380
- id: "COMPOSITION-PAGE-SCROLL",
2381
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3431
+ id: "COMPOSITION-SECTION-UI",
3432
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2382
3433
  scope: 'shared',
2383
3434
  fileTypes: MARKUP_EXT,
2384
3435
  match(text) {
2385
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3436
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2386
3437
  },
2387
3438
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2388
3439
  fixtures: {
2389
3440
  "smelly": [
2390
- "<div><page-scroll></page-scroll></div>"
3441
+ "<div><section-ui></section-ui></div>"
2391
3442
  ],
2392
3443
  "clean": [
2393
- "<admin-content><page-scroll></page-scroll></admin-content>"
3444
+ "<page-ui><section-ui></section-ui></page-ui>"
2394
3445
  ]
2395
3446
  },
2396
3447
  });
2397
3448
  }
2398
3449
 
2399
3450
  {
2400
- // GENERATED do not hand-edit. Source: packages/web-components/components/segment/segment.yaml's a2ui.allowedParents/allowedChildren.
3451
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/segment/segment.yaml's a2ui.allowedParents/allowedChildren.
2401
3452
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2402
3453
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2403
3454
 
@@ -2405,15 +3456,16 @@ RULES.push({
2405
3456
  const TAG = "segment-ui";
2406
3457
  const ALLOWED_PARENTS = ["segmented-ui"];
2407
3458
  const ALLOWED_CHILDREN = [];
3459
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2408
3460
 
2409
3461
 
2410
3462
  RULES.push({
2411
3463
  id: "COMPOSITION-SEGMENT-UI",
2412
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3464
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2413
3465
  scope: 'shared',
2414
3466
  fileTypes: MARKUP_EXT,
2415
3467
  match(text) {
2416
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3468
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2417
3469
  },
2418
3470
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2419
3471
  fixtures: {
@@ -2428,7 +3480,7 @@ RULES.push({
2428
3480
  }
2429
3481
 
2430
3482
  {
2431
- // GENERATED do not hand-edit. Source: packages/web-components/components/segmented/segmented.yaml's a2ui.allowedParents/allowedChildren.
3483
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/segmented/segmented.yaml's a2ui.allowedParents/allowedChildren.
2432
3484
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2433
3485
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2434
3486
 
@@ -2436,15 +3488,16 @@ RULES.push({
2436
3488
  const TAG = "segmented-ui";
2437
3489
  const ALLOWED_PARENTS = [];
2438
3490
  const ALLOWED_CHILDREN = ["segment-ui"];
3491
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2439
3492
 
2440
3493
 
2441
3494
  RULES.push({
2442
3495
  id: "COMPOSITION-SEGMENTED-UI",
2443
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3496
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2444
3497
  scope: 'shared',
2445
3498
  fileTypes: MARKUP_EXT,
2446
3499
  match(text) {
2447
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3500
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2448
3501
  },
2449
3502
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2450
3503
  fixtures: {
@@ -2452,14 +3505,15 @@ RULES.push({
2452
3505
  "<segmented-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></segmented-ui>"
2453
3506
  ],
2454
3507
  "clean": [
2455
- "<segmented-ui><segment-ui></segment-ui></segmented-ui>"
3508
+ "<segmented-ui><segment-ui></segment-ui></segmented-ui>",
3509
+ "<segmented-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></segmented-ui>"
2456
3510
  ]
2457
3511
  },
2458
3512
  });
2459
3513
  }
2460
3514
 
2461
3515
  {
2462
- // GENERATED do not hand-edit. Source: packages/web-modules/simple/simple-content/simple-content.yaml's a2ui.allowedParents/allowedChildren.
3516
+ // GENERATED: do not hand-edit. Source: packages/web-modules/simple/simple-content/simple-content.yaml's a2ui.allowedParents/allowedChildren.
2463
3517
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2464
3518
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2465
3519
 
@@ -2467,15 +3521,16 @@ RULES.push({
2467
3521
  const TAG = "simple-content";
2468
3522
  const ALLOWED_PARENTS = ["simple-shell"];
2469
3523
  const ALLOWED_CHILDREN = [];
3524
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2470
3525
 
2471
3526
 
2472
3527
  RULES.push({
2473
3528
  id: "COMPOSITION-SIMPLE-CONTENT",
2474
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3529
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2475
3530
  scope: 'shared',
2476
3531
  fileTypes: MARKUP_EXT,
2477
3532
  match(text) {
2478
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3533
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2479
3534
  },
2480
3535
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2481
3536
  fixtures: {
@@ -2490,7 +3545,7 @@ RULES.push({
2490
3545
  }
2491
3546
 
2492
3547
  {
2493
- // GENERATED do not hand-edit. Source: packages/web-modules/simple/simple-hero/simple-hero.yaml's a2ui.allowedParents/allowedChildren.
3548
+ // GENERATED: do not hand-edit. Source: packages/web-modules/simple/simple-hero/simple-hero.yaml's a2ui.allowedParents/allowedChildren.
2494
3549
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2495
3550
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2496
3551
 
@@ -2498,15 +3553,16 @@ RULES.push({
2498
3553
  const TAG = "simple-hero";
2499
3554
  const ALLOWED_PARENTS = ["simple-shell"];
2500
3555
  const ALLOWED_CHILDREN = [];
3556
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2501
3557
 
2502
3558
 
2503
3559
  RULES.push({
2504
3560
  id: "COMPOSITION-SIMPLE-HERO",
2505
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3561
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2506
3562
  scope: 'shared',
2507
3563
  fileTypes: MARKUP_EXT,
2508
3564
  match(text) {
2509
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3565
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2510
3566
  },
2511
3567
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2512
3568
  fixtures: {
@@ -2521,7 +3577,7 @@ RULES.push({
2521
3577
  }
2522
3578
 
2523
3579
  {
2524
- // GENERATED do not hand-edit. Source: packages/web-modules/simple/simple-shell/simple-shell.yaml's a2ui.allowedParents/allowedChildren.
3580
+ // GENERATED: do not hand-edit. Source: packages/web-modules/simple/simple-shell/simple-shell.yaml's a2ui.allowedParents/allowedChildren.
2525
3581
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2526
3582
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2527
3583
 
@@ -2529,15 +3585,16 @@ RULES.push({
2529
3585
  const TAG = "simple-shell";
2530
3586
  const ALLOWED_PARENTS = ["Surface"];
2531
3587
  const ALLOWED_CHILDREN = [];
3588
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2532
3589
 
2533
3590
 
2534
3591
  RULES.push({
2535
3592
  id: "COMPOSITION-SIMPLE-SHELL",
2536
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3593
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2537
3594
  scope: 'shared',
2538
3595
  fileTypes: MARKUP_EXT,
2539
3596
  match(text) {
2540
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3597
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2541
3598
  },
2542
3599
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2543
3600
  fixtures: {
@@ -2552,7 +3609,39 @@ RULES.push({
2552
3609
  }
2553
3610
 
2554
3611
  {
2555
- // GENERATED do not hand-edit. Source: packages/web-components/components/stepper/stepper-item.yaml's a2ui.allowedParents/allowedChildren.
3612
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/skip-nav/skip-nav.yaml's a2ui.allowedParents/allowedChildren.
3613
+ // Rebuild: node scripts/build/gen-composition-rules.mjs
3614
+ // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
3615
+
3616
+
3617
+ const TAG = "skip-nav-ui";
3618
+ const ALLOWED_PARENTS = ["Surface"];
3619
+ const ALLOWED_CHILDREN = [];
3620
+ const ALLOWED_CHILDREN_BY_SLOT = null;
3621
+
3622
+
3623
+ RULES.push({
3624
+ id: "COMPOSITION-SKIP-NAV-UI",
3625
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
3626
+ scope: 'shared',
3627
+ fileTypes: MARKUP_EXT,
3628
+ match(text) {
3629
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
3630
+ },
3631
+ fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
3632
+ fixtures: {
3633
+ "smelly": [
3634
+ "<col-ui><skip-nav-ui></skip-nav-ui></col-ui>"
3635
+ ],
3636
+ "clean": [
3637
+ "<skip-nav-ui></skip-nav-ui>"
3638
+ ]
3639
+ },
3640
+ });
3641
+ }
3642
+
3643
+ {
3644
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/stepper/stepper-item.yaml's a2ui.allowedParents/allowedChildren.
2556
3645
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2557
3646
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2558
3647
 
@@ -2560,15 +3649,16 @@ RULES.push({
2560
3649
  const TAG = "stepper-item-ui";
2561
3650
  const ALLOWED_PARENTS = ["stepper-ui"];
2562
3651
  const ALLOWED_CHILDREN = [];
3652
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2563
3653
 
2564
3654
 
2565
3655
  RULES.push({
2566
3656
  id: "COMPOSITION-STEPPER-ITEM-UI",
2567
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3657
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2568
3658
  scope: 'shared',
2569
3659
  fileTypes: MARKUP_EXT,
2570
3660
  match(text) {
2571
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3661
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2572
3662
  },
2573
3663
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2574
3664
  fixtures: {
@@ -2583,7 +3673,7 @@ RULES.push({
2583
3673
  }
2584
3674
 
2585
3675
  {
2586
- // GENERATED do not hand-edit. Source: packages/web-components/components/stepper/stepper.yaml's a2ui.allowedParents/allowedChildren.
3676
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/stepper/stepper.yaml's a2ui.allowedParents/allowedChildren.
2587
3677
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2588
3678
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2589
3679
 
@@ -2591,15 +3681,16 @@ RULES.push({
2591
3681
  const TAG = "stepper-ui";
2592
3682
  const ALLOWED_PARENTS = [];
2593
3683
  const ALLOWED_CHILDREN = ["stepper-item-ui"];
3684
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2594
3685
 
2595
3686
 
2596
3687
  RULES.push({
2597
3688
  id: "COMPOSITION-STEPPER-UI",
2598
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3689
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2599
3690
  scope: 'shared',
2600
3691
  fileTypes: MARKUP_EXT,
2601
3692
  match(text) {
2602
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3693
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2603
3694
  },
2604
3695
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2605
3696
  fixtures: {
@@ -2607,14 +3698,15 @@ RULES.push({
2607
3698
  "<stepper-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></stepper-ui>"
2608
3699
  ],
2609
3700
  "clean": [
2610
- "<stepper-ui><stepper-item-ui></stepper-item-ui></stepper-ui>"
3701
+ "<stepper-ui><stepper-item-ui></stepper-item-ui></stepper-ui>",
3702
+ "<stepper-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></stepper-ui>"
2611
3703
  ]
2612
3704
  },
2613
3705
  });
2614
3706
  }
2615
3707
 
2616
3708
  {
2617
- // GENERATED do not hand-edit. Source: packages/web-components/components/tabs/tab.yaml's a2ui.allowedParents/allowedChildren.
3709
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/tabs/tab.yaml's a2ui.allowedParents/allowedChildren.
2618
3710
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2619
3711
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2620
3712
 
@@ -2622,15 +3714,16 @@ RULES.push({
2622
3714
  const TAG = "tab-ui";
2623
3715
  const ALLOWED_PARENTS = ["tabs-ui"];
2624
3716
  const ALLOWED_CHILDREN = [];
3717
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2625
3718
 
2626
3719
 
2627
3720
  RULES.push({
2628
3721
  id: "COMPOSITION-TAB-UI",
2629
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3722
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2630
3723
  scope: 'shared',
2631
3724
  fileTypes: MARKUP_EXT,
2632
3725
  match(text) {
2633
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3726
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2634
3727
  },
2635
3728
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2636
3729
  fixtures: {
@@ -2645,7 +3738,7 @@ RULES.push({
2645
3738
  }
2646
3739
 
2647
3740
  {
2648
- // GENERATED do not hand-edit. Source: packages/web-components/components/tabs/tabs.yaml's a2ui.allowedParents/allowedChildren.
3741
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/tabs/tabs.yaml's a2ui.allowedParents/allowedChildren.
2649
3742
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2650
3743
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2651
3744
 
@@ -2653,15 +3746,16 @@ RULES.push({
2653
3746
  const TAG = "tabs-ui";
2654
3747
  const ALLOWED_PARENTS = [];
2655
3748
  const ALLOWED_CHILDREN = ["tab-ui"];
3749
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2656
3750
 
2657
3751
 
2658
3752
  RULES.push({
2659
3753
  id: "COMPOSITION-TABS-UI",
2660
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3754
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2661
3755
  scope: 'shared',
2662
3756
  fileTypes: MARKUP_EXT,
2663
3757
  match(text) {
2664
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3758
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2665
3759
  },
2666
3760
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2667
3761
  fixtures: {
@@ -2669,14 +3763,15 @@ RULES.push({
2669
3763
  "<tabs-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></tabs-ui>"
2670
3764
  ],
2671
3765
  "clean": [
2672
- "<tabs-ui><tab-ui></tab-ui></tabs-ui>"
3766
+ "<tabs-ui><tab-ui></tab-ui></tabs-ui>",
3767
+ "<tabs-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></tabs-ui>"
2673
3768
  ]
2674
3769
  },
2675
3770
  });
2676
3771
  }
2677
3772
 
2678
3773
  {
2679
- // GENERATED do not hand-edit. Source: packages/web-components/components/timeline/timeline-item.yaml's a2ui.allowedParents/allowedChildren.
3774
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/timeline/timeline-item.yaml's a2ui.allowedParents/allowedChildren.
2680
3775
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2681
3776
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2682
3777
 
@@ -2684,15 +3779,16 @@ RULES.push({
2684
3779
  const TAG = "timeline-item-ui";
2685
3780
  const ALLOWED_PARENTS = ["timeline-ui"];
2686
3781
  const ALLOWED_CHILDREN = [];
3782
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2687
3783
 
2688
3784
 
2689
3785
  RULES.push({
2690
3786
  id: "COMPOSITION-TIMELINE-ITEM-UI",
2691
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3787
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2692
3788
  scope: 'shared',
2693
3789
  fileTypes: MARKUP_EXT,
2694
3790
  match(text) {
2695
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3791
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2696
3792
  },
2697
3793
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2698
3794
  fixtures: {
@@ -2707,7 +3803,7 @@ RULES.push({
2707
3803
  }
2708
3804
 
2709
3805
  {
2710
- // GENERATED do not hand-edit. Source: packages/web-components/components/timeline/timeline.yaml's a2ui.allowedParents/allowedChildren.
3806
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/timeline/timeline.yaml's a2ui.allowedParents/allowedChildren.
2711
3807
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2712
3808
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2713
3809
 
@@ -2715,15 +3811,16 @@ RULES.push({
2715
3811
  const TAG = "timeline-ui";
2716
3812
  const ALLOWED_PARENTS = [];
2717
3813
  const ALLOWED_CHILDREN = ["timeline-item-ui"];
3814
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2718
3815
 
2719
3816
 
2720
3817
  RULES.push({
2721
3818
  id: "COMPOSITION-TIMELINE-UI",
2722
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3819
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2723
3820
  scope: 'shared',
2724
3821
  fileTypes: MARKUP_EXT,
2725
3822
  match(text) {
2726
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3823
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2727
3824
  },
2728
3825
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2729
3826
  fixtures: {
@@ -2731,14 +3828,15 @@ RULES.push({
2731
3828
  "<timeline-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></timeline-ui>"
2732
3829
  ],
2733
3830
  "clean": [
2734
- "<timeline-ui><timeline-item-ui></timeline-item-ui></timeline-ui>"
3831
+ "<timeline-ui><timeline-item-ui></timeline-item-ui></timeline-ui>",
3832
+ "<timeline-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></timeline-ui>"
2735
3833
  ]
2736
3834
  },
2737
3835
  });
2738
3836
  }
2739
3837
 
2740
3838
  {
2741
- // GENERATED do not hand-edit. Source: packages/web-components/components/toolbar/toolbar-group.yaml's a2ui.allowedParents/allowedChildren.
3839
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/toolbar/toolbar-group.yaml's a2ui.allowedParents/allowedChildren.
2742
3840
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2743
3841
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2744
3842
 
@@ -2746,15 +3844,16 @@ RULES.push({
2746
3844
  const TAG = "toolbar-group-ui";
2747
3845
  const ALLOWED_PARENTS = ["toolbar-ui"];
2748
3846
  const ALLOWED_CHILDREN = [];
3847
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2749
3848
 
2750
3849
 
2751
3850
  RULES.push({
2752
3851
  id: "COMPOSITION-TOOLBAR-GROUP-UI",
2753
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3852
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2754
3853
  scope: 'shared',
2755
3854
  fileTypes: MARKUP_EXT,
2756
3855
  match(text) {
2757
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3856
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2758
3857
  },
2759
3858
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2760
3859
  fixtures: {
@@ -2769,7 +3868,7 @@ RULES.push({
2769
3868
  }
2770
3869
 
2771
3870
  {
2772
- // GENERATED do not hand-edit. Source: packages/web-components/components/tour/tour-step.yaml's a2ui.allowedParents/allowedChildren.
3871
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/tour/tour-step.yaml's a2ui.allowedParents/allowedChildren.
2773
3872
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2774
3873
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2775
3874
 
@@ -2777,15 +3876,16 @@ RULES.push({
2777
3876
  const TAG = "tour-step-ui";
2778
3877
  const ALLOWED_PARENTS = ["tour-ui"];
2779
3878
  const ALLOWED_CHILDREN = [];
3879
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2780
3880
 
2781
3881
 
2782
3882
  RULES.push({
2783
3883
  id: "COMPOSITION-TOUR-STEP-UI",
2784
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3884
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2785
3885
  scope: 'shared',
2786
3886
  fileTypes: MARKUP_EXT,
2787
3887
  match(text) {
2788
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3888
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2789
3889
  },
2790
3890
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2791
3891
  fixtures: {
@@ -2800,7 +3900,7 @@ RULES.push({
2800
3900
  }
2801
3901
 
2802
3902
  {
2803
- // GENERATED do not hand-edit. Source: packages/web-components/components/tour/tour.yaml's a2ui.allowedParents/allowedChildren.
3903
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/tour/tour.yaml's a2ui.allowedParents/allowedChildren.
2804
3904
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2805
3905
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2806
3906
 
@@ -2808,15 +3908,16 @@ RULES.push({
2808
3908
  const TAG = "tour-ui";
2809
3909
  const ALLOWED_PARENTS = [];
2810
3910
  const ALLOWED_CHILDREN = ["tour-step-ui"];
3911
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2811
3912
 
2812
3913
 
2813
3914
  RULES.push({
2814
3915
  id: "COMPOSITION-TOUR-UI",
2815
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3916
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2816
3917
  scope: 'shared',
2817
3918
  fileTypes: MARKUP_EXT,
2818
3919
  match(text) {
2819
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3920
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2820
3921
  },
2821
3922
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2822
3923
  fixtures: {
@@ -2824,14 +3925,15 @@ RULES.push({
2824
3925
  "<tour-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></tour-ui>"
2825
3926
  ],
2826
3927
  "clean": [
2827
- "<tour-ui><tour-step-ui></tour-step-ui></tour-ui>"
3928
+ "<tour-ui><tour-step-ui></tour-step-ui></tour-ui>",
3929
+ "<tour-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></tour-ui>"
2828
3930
  ]
2829
3931
  },
2830
3932
  });
2831
3933
  }
2832
3934
 
2833
3935
  {
2834
- // GENERATED do not hand-edit. Source: packages/web-components/components/tree/tree-item.yaml's a2ui.allowedParents/allowedChildren.
3936
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/tree/tree-item.yaml's a2ui.allowedParents/allowedChildren.
2835
3937
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2836
3938
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2837
3939
 
@@ -2839,15 +3941,16 @@ RULES.push({
2839
3941
  const TAG = "tree-item-ui";
2840
3942
  const ALLOWED_PARENTS = ["tree-ui","tree-item-ui"];
2841
3943
  const ALLOWED_CHILDREN = [];
3944
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2842
3945
 
2843
3946
 
2844
3947
  RULES.push({
2845
3948
  id: "COMPOSITION-TREE-ITEM-UI",
2846
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3949
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2847
3950
  scope: 'shared',
2848
3951
  fileTypes: MARKUP_EXT,
2849
3952
  match(text) {
2850
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3953
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2851
3954
  },
2852
3955
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2853
3956
  fixtures: {
@@ -2862,7 +3965,7 @@ RULES.push({
2862
3965
  }
2863
3966
 
2864
3967
  {
2865
- // GENERATED do not hand-edit. Source: packages/web-components/components/tree/tree.yaml's a2ui.allowedParents/allowedChildren.
3968
+ // GENERATED: do not hand-edit. Source: packages/web-components/components/tree/tree.yaml's a2ui.allowedParents/allowedChildren.
2866
3969
  // Rebuild: node scripts/build/gen-composition-rules.mjs
2867
3970
  // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
2868
3971
 
@@ -2870,15 +3973,16 @@ RULES.push({
2870
3973
  const TAG = "tree-ui";
2871
3974
  const ALLOWED_PARENTS = [];
2872
3975
  const ALLOWED_CHILDREN = ["tree-item-ui"];
3976
+ const ALLOWED_CHILDREN_BY_SLOT = null;
2873
3977
 
2874
3978
 
2875
3979
  RULES.push({
2876
3980
  id: "COMPOSITION-TREE-UI",
2877
- severity: 'warn', // LLD-0016 §C4: new, unproven-at-scale matcher (Risk R4) advisory until a corpus-wide rollout promotes it, same posture check-example-ids.mjs's Phase-1 rule started at.
3981
+ severity: 'error', // gh#3719: promoted from warn once the corpus-wide rollout (LLD-0016 §C4) cleared clean, every COMPOSITION-* rule's real corpus hits fixed, so this stays advisory no longer.
2878
3982
  scope: 'shared',
2879
3983
  fileTypes: MARKUP_EXT,
2880
3984
  match(text) {
2881
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN });
3985
+ return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
2882
3986
  },
2883
3987
  fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
2884
3988
  fixtures: {
@@ -2886,35 +3990,27 @@ RULES.push({
2886
3990
  "<tree-ui><not-an-allowed-child-ui></not-an-allowed-child-ui></tree-ui>"
2887
3991
  ],
2888
3992
  "clean": [
2889
- "<tree-ui><tree-item-ui></tree-item-ui></tree-ui>"
3993
+ "<tree-ui><tree-item-ui></tree-item-ui></tree-ui>",
3994
+ "<tree-ui><not-an-allowed-child-ui slot=\"arbitrary-slot\"></not-an-allowed-child-ui></tree-ui>"
2890
3995
  ]
2891
3996
  },
2892
3997
  });
2893
3998
  }
2894
3999
 
2895
4000
  {
2896
- // GENERATED do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
4001
+ // GENERATED, do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
2897
4002
  // packages/web-modules/**/*.a2ui.json + yaml (ADR-0108 D5 / LLD-0019 C5).
2898
4003
  // Rebuild: node scripts/build/gen-lab-posture-rules.mjs
2899
4004
  // Freshness gate: node scripts/verify/check-lab-posture-rules-fresh.mjs --verify
2900
4005
  //
2901
4006
  // ENUM-VALUE (ADR-0108 D5): a declared enum attribute set to a value outside its manifest-declared enum (default + opt-in + internal).
2902
4007
  //
2903
- // severity: 'info' here governs ONLY `npm run lint`'s bare-repo-shaped
4008
+ // severity: 'error' here governs ONLY `npm run lint`'s bare-repo-shaped
2904
4009
  // advisory bank (ADR-0040 unchanged). The lab-posture write-time hook
2905
4010
  // (packages/plugins/adia-ui-factory/scripts/lab-posture-guard.mjs,
2906
4011
  // LLD-0019 C4) does not read this field — a lab-posture write denies on
2907
4012
  // ANY finding from these same matcher functions regardless of severity
2908
4013
  // (ADR-0108 D5: "PreToolUse deny... error under the lab hook").
2909
- //
2910
- // Demoted warn -> info (gh#2939, Kim-ruled 2026-09-03): gh#2911's G2
2911
- // warn-triage sample found this rule's class overwhelmingly manifest
2912
- // drift, not real authoring bugs (INTERNAL-TIER 594/1413 findings, 42%
2913
- // of the corpus total — almost entirely shipped-and-sanctioned values
2914
- // like button-ui[variant="primary"] the tier manifest hasn't promoted
2915
- // to match reality). The matcher/detection logic is UNCHANGED — this is
2916
- // a severity relabel only. Fixing the underlying manifest drift itself
2917
- // is split out as gh#3044, not this ticket.
2918
4014
 
2919
4015
 
2920
4016
  const MANIFEST_SNAPSHOT = {
@@ -3133,10 +4229,6 @@ const MANIFEST_SNAPSHOT = {
3133
4229
  }
3134
4230
  ]
3135
4231
  },
3136
- {
3137
- "tag": "admin-scroll",
3138
- "attrs": []
3139
- },
3140
4232
  {
3141
4233
  "tag": "admin-settings-ui",
3142
4234
  "attrs": [
@@ -3252,6 +4344,9 @@ const MANIFEST_SNAPSHOT = {
3252
4344
  {
3253
4345
  "name": "multi"
3254
4346
  },
4347
+ {
4348
+ "name": "options"
4349
+ },
3255
4350
  {
3256
4351
  "name": "question"
3257
4352
  },
@@ -3315,6 +4410,9 @@ const MANIFEST_SNAPSHOT = {
3315
4410
  }
3316
4411
  ]
3317
4412
  },
4413
+ {
4414
+ "name": "suggestions"
4415
+ },
3318
4416
  {
3319
4417
  "name": "variant",
3320
4418
  "values": [
@@ -4048,13 +5146,16 @@ const MANIFEST_SNAPSHOT = {
4048
5146
  "name": "precision",
4049
5147
  "values": [
4050
5148
  {
4051
- "value": "date"
5149
+ "value": "date",
5150
+ "tier": "default"
4052
5151
  },
4053
5152
  {
4054
- "value": "minute"
5153
+ "value": "minute",
5154
+ "tier": "opt-in"
4055
5155
  },
4056
5156
  {
4057
- "value": "second"
5157
+ "value": "second",
5158
+ "tier": "opt-in"
4058
5159
  }
4059
5160
  ]
4060
5161
  },
@@ -4578,6 +5679,10 @@ const MANIFEST_SNAPSHOT = {
4578
5679
  },
4579
5680
  {
4580
5681
  "name": "thinking"
5682
+ },
5683
+ {
5684
+ "name": "thinkingBudget",
5685
+ "attribute": "thinking-budget"
4581
5686
  }
4582
5687
  ]
4583
5688
  },
@@ -5270,7 +6375,30 @@ const MANIFEST_SNAPSHOT = {
5270
6375
  "name": "to"
5271
6376
  },
5272
6377
  {
5273
- "name": "value"
6378
+ "name": "value",
6379
+ "values": [
6380
+ {
6381
+ "value": "today"
6382
+ },
6383
+ {
6384
+ "value": "7d"
6385
+ },
6386
+ {
6387
+ "value": "30d"
6388
+ },
6389
+ {
6390
+ "value": "90d"
6391
+ },
6392
+ {
6393
+ "value": "qtd"
6394
+ },
6395
+ {
6396
+ "value": "ytd"
6397
+ },
6398
+ {
6399
+ "value": "custom"
6400
+ }
6401
+ ]
5274
6402
  }
5275
6403
  ]
5276
6404
  },
@@ -5403,16 +6531,19 @@ const MANIFEST_SNAPSHOT = {
5403
6531
  "name": "side",
5404
6532
  "values": [
5405
6533
  {
5406
- "value": "left"
6534
+ "value": "left",
6535
+ "tier": "opt-in"
5407
6536
  },
5408
6537
  {
5409
- "value": "right"
6538
+ "value": "right",
6539
+ "tier": "default"
5410
6540
  },
5411
6541
  {
5412
6542
  "value": "top"
5413
6543
  },
5414
6544
  {
5415
- "value": "bottom"
6545
+ "value": "bottom",
6546
+ "tier": "opt-in"
5416
6547
  }
5417
6548
  ]
5418
6549
  },
@@ -5907,13 +7038,16 @@ const MANIFEST_SNAPSHOT = {
5907
7038
  "name": "scale",
5908
7039
  "values": [
5909
7040
  {
5910
- "value": "linear"
7041
+ "value": "linear",
7042
+ "tier": "default"
5911
7043
  },
5912
7044
  {
5913
- "value": "log"
7045
+ "value": "log",
7046
+ "tier": "opt-in"
5914
7047
  },
5915
7048
  {
5916
- "value": "quantile"
7049
+ "value": "quantile",
7050
+ "tier": "opt-in"
5917
7051
  }
5918
7052
  ]
5919
7053
  },
@@ -6134,7 +7268,29 @@ const MANIFEST_SNAPSHOT = {
6134
7268
  "name": "text"
6135
7269
  },
6136
7270
  {
6137
- "name": "variant"
7271
+ "name": "variant",
7272
+ "values": [
7273
+ {
7274
+ "value": "default",
7275
+ "tier": "default"
7276
+ },
7277
+ {
7278
+ "value": "info",
7279
+ "tier": "opt-in"
7280
+ },
7281
+ {
7282
+ "value": "success",
7283
+ "tier": "opt-in"
7284
+ },
7285
+ {
7286
+ "value": "warning",
7287
+ "tier": "opt-in"
7288
+ },
7289
+ {
7290
+ "value": "danger",
7291
+ "tier": "opt-in"
7292
+ }
7293
+ ]
6138
7294
  }
6139
7295
  ]
6140
7296
  },
@@ -6180,7 +7336,33 @@ const MANIFEST_SNAPSHOT = {
6180
7336
  "name": "error"
6181
7337
  },
6182
7338
  {
6183
- "name": "inputmode"
7339
+ "name": "inputmode",
7340
+ "values": [
7341
+ {
7342
+ "value": "text"
7343
+ },
7344
+ {
7345
+ "value": "decimal"
7346
+ },
7347
+ {
7348
+ "value": "numeric"
7349
+ },
7350
+ {
7351
+ "value": "tel"
7352
+ },
7353
+ {
7354
+ "value": "search"
7355
+ },
7356
+ {
7357
+ "value": "email"
7358
+ },
7359
+ {
7360
+ "value": "url"
7361
+ },
7362
+ {
7363
+ "value": "none"
7364
+ }
7365
+ ]
6184
7366
  },
6185
7367
  {
6186
7368
  "name": "label"
@@ -6281,7 +7463,24 @@ const MANIFEST_SNAPSHOT = {
6281
7463
  "required": true
6282
7464
  },
6283
7465
  {
6284
- "name": "status"
7466
+ "name": "status",
7467
+ "values": [
7468
+ {
7469
+ "value": "available"
7470
+ },
7471
+ {
7472
+ "value": "connected"
7473
+ },
7474
+ {
7475
+ "value": "error"
7476
+ },
7477
+ {
7478
+ "value": "pending"
7479
+ },
7480
+ {
7481
+ "value": "coming-soon"
7482
+ }
7483
+ ]
6285
7484
  }
6286
7485
  ]
6287
7486
  },
@@ -6313,7 +7512,24 @@ const MANIFEST_SNAPSHOT = {
6313
7512
  "name": "searchPlaceholder"
6314
7513
  },
6315
7514
  {
6316
- "name": "statusFilter"
7515
+ "name": "statusFilter",
7516
+ "values": [
7517
+ {
7518
+ "value": "all"
7519
+ },
7520
+ {
7521
+ "value": "available"
7522
+ },
7523
+ {
7524
+ "value": "connected"
7525
+ },
7526
+ {
7527
+ "value": "error"
7528
+ },
7529
+ {
7530
+ "value": "coming-soon"
7531
+ }
7532
+ ]
6317
7533
  }
6318
7534
  ]
6319
7535
  },
@@ -6355,7 +7571,24 @@ const MANIFEST_SNAPSHOT = {
6355
7571
  "name": "printable"
6356
7572
  },
6357
7573
  {
6358
- "name": "status"
7574
+ "name": "status",
7575
+ "values": [
7576
+ {
7577
+ "value": "draft"
7578
+ },
7579
+ {
7580
+ "value": "open"
7581
+ },
7582
+ {
7583
+ "value": "paid"
7584
+ },
7585
+ {
7586
+ "value": "past-due"
7587
+ },
7588
+ {
7589
+ "value": "void"
7590
+ }
7591
+ ]
6359
7592
  }
6360
7593
  ]
6361
7594
  },
@@ -6387,7 +7620,27 @@ const MANIFEST_SNAPSHOT = {
6387
7620
  "name": "exportable"
6388
7621
  },
6389
7622
  {
6390
- "name": "filter"
7623
+ "name": "filter",
7624
+ "values": [
7625
+ {
7626
+ "value": ""
7627
+ },
7628
+ {
7629
+ "value": "draft"
7630
+ },
7631
+ {
7632
+ "value": "open"
7633
+ },
7634
+ {
7635
+ "value": "paid"
7636
+ },
7637
+ {
7638
+ "value": "past-due"
7639
+ },
7640
+ {
7641
+ "value": "void"
7642
+ }
7643
+ ]
6391
7644
  },
6392
7645
  {
6393
7646
  "name": "hrefPattern",
@@ -6600,7 +7853,21 @@ const MANIFEST_SNAPSHOT = {
6600
7853
  "name": "label"
6601
7854
  },
6602
7855
  {
6603
- "name": "variant"
7856
+ "name": "variant",
7857
+ "values": [
7858
+ {
7859
+ "value": "default",
7860
+ "tier": "default"
7861
+ },
7862
+ {
7863
+ "value": "transparent",
7864
+ "tier": "opt-in"
7865
+ },
7866
+ {
7867
+ "value": "blur",
7868
+ "tier": "opt-in"
7869
+ }
7870
+ ]
6604
7871
  }
6605
7872
  ]
6606
7873
  },
@@ -7229,6 +8496,10 @@ const MANIFEST_SNAPSHOT = {
7229
8496
  {
7230
8497
  "tag": "pagination-ui",
7231
8498
  "attrs": [
8499
+ {
8500
+ "name": "noLandmark",
8501
+ "attribute": "no-landmark"
8502
+ },
7232
8503
  {
7233
8504
  "name": "page"
7234
8505
  },
@@ -7434,7 +8705,15 @@ const MANIFEST_SNAPSHOT = {
7434
8705
  "name": "current"
7435
8706
  },
7436
8707
  {
7437
- "name": "cycle"
8708
+ "name": "cycle",
8709
+ "values": [
8710
+ {
8711
+ "value": "monthly"
8712
+ },
8713
+ {
8714
+ "value": "annual"
8715
+ }
8716
+ ]
7438
8717
  },
7439
8718
  {
7440
8719
  "name": "cycleToggle",
@@ -8149,6 +9428,10 @@ const MANIFEST_SNAPSHOT = {
8149
9428
  {
8150
9429
  "value": "dot-grid",
8151
9430
  "tier": "default"
9431
+ },
9432
+ {
9433
+ "value": "noise",
9434
+ "tier": "alternative"
8152
9435
  }
8153
9436
  ]
8154
9437
  }
@@ -10082,7 +11365,7 @@ const MANIFEST_SNAPSHOT = {
10082
11365
 
10083
11366
  RULES.push({
10084
11367
  id: "ENUM-VALUE",
10085
- severity: "info",
11368
+ severity: "error",
10086
11369
  scope: 'shared',
10087
11370
  fileTypes: MARKUP_EXT,
10088
11371
  match(text, path = '') {
@@ -10103,12 +11386,12 @@ RULES.push({
10103
11386
  }
10104
11387
 
10105
11388
  {
10106
- // GENERATED do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
11389
+ // GENERATED, do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
10107
11390
  // packages/web-modules/**/*.a2ui.json + yaml (ADR-0108 D5 / LLD-0019 C5).
10108
11391
  // Rebuild: node scripts/build/gen-lab-posture-rules.mjs
10109
11392
  // Freshness gate: node scripts/verify/check-lab-posture-rules-fresh.mjs --verify
10110
11393
  //
10111
- // INTERNAL-TIER (ADR-0108 D3/D5): a declared enum value present in the manifest but tiered `internal` reachable in source, never the intended answer for a generator or author.
11394
+ // INTERNAL-TIER (ADR-0108 D3/D5): a declared enum value present in the manifest but tiered `internal`, reachable in source, never the intended answer for a generator or author.
10112
11395
  //
10113
11396
  // severity: 'info' here governs ONLY `npm run lint`'s bare-repo-shaped
10114
11397
  // advisory bank (ADR-0040 unchanged). The lab-posture write-time hook
@@ -10343,10 +11626,6 @@ const MANIFEST_SNAPSHOT = {
10343
11626
  }
10344
11627
  ]
10345
11628
  },
10346
- {
10347
- "tag": "admin-scroll",
10348
- "attrs": []
10349
- },
10350
11629
  {
10351
11630
  "tag": "admin-settings-ui",
10352
11631
  "attrs": [
@@ -10462,6 +11741,9 @@ const MANIFEST_SNAPSHOT = {
10462
11741
  {
10463
11742
  "name": "multi"
10464
11743
  },
11744
+ {
11745
+ "name": "options"
11746
+ },
10465
11747
  {
10466
11748
  "name": "question"
10467
11749
  },
@@ -10525,6 +11807,9 @@ const MANIFEST_SNAPSHOT = {
10525
11807
  }
10526
11808
  ]
10527
11809
  },
11810
+ {
11811
+ "name": "suggestions"
11812
+ },
10528
11813
  {
10529
11814
  "name": "variant",
10530
11815
  "values": [
@@ -11258,13 +12543,16 @@ const MANIFEST_SNAPSHOT = {
11258
12543
  "name": "precision",
11259
12544
  "values": [
11260
12545
  {
11261
- "value": "date"
12546
+ "value": "date",
12547
+ "tier": "default"
11262
12548
  },
11263
12549
  {
11264
- "value": "minute"
12550
+ "value": "minute",
12551
+ "tier": "opt-in"
11265
12552
  },
11266
12553
  {
11267
- "value": "second"
12554
+ "value": "second",
12555
+ "tier": "opt-in"
11268
12556
  }
11269
12557
  ]
11270
12558
  },
@@ -11788,6 +13076,10 @@ const MANIFEST_SNAPSHOT = {
11788
13076
  },
11789
13077
  {
11790
13078
  "name": "thinking"
13079
+ },
13080
+ {
13081
+ "name": "thinkingBudget",
13082
+ "attribute": "thinking-budget"
11791
13083
  }
11792
13084
  ]
11793
13085
  },
@@ -12480,7 +13772,30 @@ const MANIFEST_SNAPSHOT = {
12480
13772
  "name": "to"
12481
13773
  },
12482
13774
  {
12483
- "name": "value"
13775
+ "name": "value",
13776
+ "values": [
13777
+ {
13778
+ "value": "today"
13779
+ },
13780
+ {
13781
+ "value": "7d"
13782
+ },
13783
+ {
13784
+ "value": "30d"
13785
+ },
13786
+ {
13787
+ "value": "90d"
13788
+ },
13789
+ {
13790
+ "value": "qtd"
13791
+ },
13792
+ {
13793
+ "value": "ytd"
13794
+ },
13795
+ {
13796
+ "value": "custom"
13797
+ }
13798
+ ]
12484
13799
  }
12485
13800
  ]
12486
13801
  },
@@ -12613,16 +13928,19 @@ const MANIFEST_SNAPSHOT = {
12613
13928
  "name": "side",
12614
13929
  "values": [
12615
13930
  {
12616
- "value": "left"
13931
+ "value": "left",
13932
+ "tier": "opt-in"
12617
13933
  },
12618
13934
  {
12619
- "value": "right"
13935
+ "value": "right",
13936
+ "tier": "default"
12620
13937
  },
12621
13938
  {
12622
13939
  "value": "top"
12623
13940
  },
12624
13941
  {
12625
- "value": "bottom"
13942
+ "value": "bottom",
13943
+ "tier": "opt-in"
12626
13944
  }
12627
13945
  ]
12628
13946
  },
@@ -13117,13 +14435,16 @@ const MANIFEST_SNAPSHOT = {
13117
14435
  "name": "scale",
13118
14436
  "values": [
13119
14437
  {
13120
- "value": "linear"
14438
+ "value": "linear",
14439
+ "tier": "default"
13121
14440
  },
13122
14441
  {
13123
- "value": "log"
14442
+ "value": "log",
14443
+ "tier": "opt-in"
13124
14444
  },
13125
14445
  {
13126
- "value": "quantile"
14446
+ "value": "quantile",
14447
+ "tier": "opt-in"
13127
14448
  }
13128
14449
  ]
13129
14450
  },
@@ -13344,7 +14665,29 @@ const MANIFEST_SNAPSHOT = {
13344
14665
  "name": "text"
13345
14666
  },
13346
14667
  {
13347
- "name": "variant"
14668
+ "name": "variant",
14669
+ "values": [
14670
+ {
14671
+ "value": "default",
14672
+ "tier": "default"
14673
+ },
14674
+ {
14675
+ "value": "info",
14676
+ "tier": "opt-in"
14677
+ },
14678
+ {
14679
+ "value": "success",
14680
+ "tier": "opt-in"
14681
+ },
14682
+ {
14683
+ "value": "warning",
14684
+ "tier": "opt-in"
14685
+ },
14686
+ {
14687
+ "value": "danger",
14688
+ "tier": "opt-in"
14689
+ }
14690
+ ]
13348
14691
  }
13349
14692
  ]
13350
14693
  },
@@ -13390,7 +14733,33 @@ const MANIFEST_SNAPSHOT = {
13390
14733
  "name": "error"
13391
14734
  },
13392
14735
  {
13393
- "name": "inputmode"
14736
+ "name": "inputmode",
14737
+ "values": [
14738
+ {
14739
+ "value": "text"
14740
+ },
14741
+ {
14742
+ "value": "decimal"
14743
+ },
14744
+ {
14745
+ "value": "numeric"
14746
+ },
14747
+ {
14748
+ "value": "tel"
14749
+ },
14750
+ {
14751
+ "value": "search"
14752
+ },
14753
+ {
14754
+ "value": "email"
14755
+ },
14756
+ {
14757
+ "value": "url"
14758
+ },
14759
+ {
14760
+ "value": "none"
14761
+ }
14762
+ ]
13394
14763
  },
13395
14764
  {
13396
14765
  "name": "label"
@@ -13491,7 +14860,24 @@ const MANIFEST_SNAPSHOT = {
13491
14860
  "required": true
13492
14861
  },
13493
14862
  {
13494
- "name": "status"
14863
+ "name": "status",
14864
+ "values": [
14865
+ {
14866
+ "value": "available"
14867
+ },
14868
+ {
14869
+ "value": "connected"
14870
+ },
14871
+ {
14872
+ "value": "error"
14873
+ },
14874
+ {
14875
+ "value": "pending"
14876
+ },
14877
+ {
14878
+ "value": "coming-soon"
14879
+ }
14880
+ ]
13495
14881
  }
13496
14882
  ]
13497
14883
  },
@@ -13523,7 +14909,24 @@ const MANIFEST_SNAPSHOT = {
13523
14909
  "name": "searchPlaceholder"
13524
14910
  },
13525
14911
  {
13526
- "name": "statusFilter"
14912
+ "name": "statusFilter",
14913
+ "values": [
14914
+ {
14915
+ "value": "all"
14916
+ },
14917
+ {
14918
+ "value": "available"
14919
+ },
14920
+ {
14921
+ "value": "connected"
14922
+ },
14923
+ {
14924
+ "value": "error"
14925
+ },
14926
+ {
14927
+ "value": "coming-soon"
14928
+ }
14929
+ ]
13527
14930
  }
13528
14931
  ]
13529
14932
  },
@@ -13565,7 +14968,24 @@ const MANIFEST_SNAPSHOT = {
13565
14968
  "name": "printable"
13566
14969
  },
13567
14970
  {
13568
- "name": "status"
14971
+ "name": "status",
14972
+ "values": [
14973
+ {
14974
+ "value": "draft"
14975
+ },
14976
+ {
14977
+ "value": "open"
14978
+ },
14979
+ {
14980
+ "value": "paid"
14981
+ },
14982
+ {
14983
+ "value": "past-due"
14984
+ },
14985
+ {
14986
+ "value": "void"
14987
+ }
14988
+ ]
13569
14989
  }
13570
14990
  ]
13571
14991
  },
@@ -13597,7 +15017,27 @@ const MANIFEST_SNAPSHOT = {
13597
15017
  "name": "exportable"
13598
15018
  },
13599
15019
  {
13600
- "name": "filter"
15020
+ "name": "filter",
15021
+ "values": [
15022
+ {
15023
+ "value": ""
15024
+ },
15025
+ {
15026
+ "value": "draft"
15027
+ },
15028
+ {
15029
+ "value": "open"
15030
+ },
15031
+ {
15032
+ "value": "paid"
15033
+ },
15034
+ {
15035
+ "value": "past-due"
15036
+ },
15037
+ {
15038
+ "value": "void"
15039
+ }
15040
+ ]
13601
15041
  },
13602
15042
  {
13603
15043
  "name": "hrefPattern",
@@ -13810,7 +15250,21 @@ const MANIFEST_SNAPSHOT = {
13810
15250
  "name": "label"
13811
15251
  },
13812
15252
  {
13813
- "name": "variant"
15253
+ "name": "variant",
15254
+ "values": [
15255
+ {
15256
+ "value": "default",
15257
+ "tier": "default"
15258
+ },
15259
+ {
15260
+ "value": "transparent",
15261
+ "tier": "opt-in"
15262
+ },
15263
+ {
15264
+ "value": "blur",
15265
+ "tier": "opt-in"
15266
+ }
15267
+ ]
13814
15268
  }
13815
15269
  ]
13816
15270
  },
@@ -14439,6 +15893,10 @@ const MANIFEST_SNAPSHOT = {
14439
15893
  {
14440
15894
  "tag": "pagination-ui",
14441
15895
  "attrs": [
15896
+ {
15897
+ "name": "noLandmark",
15898
+ "attribute": "no-landmark"
15899
+ },
14442
15900
  {
14443
15901
  "name": "page"
14444
15902
  },
@@ -14644,7 +16102,15 @@ const MANIFEST_SNAPSHOT = {
14644
16102
  "name": "current"
14645
16103
  },
14646
16104
  {
14647
- "name": "cycle"
16105
+ "name": "cycle",
16106
+ "values": [
16107
+ {
16108
+ "value": "monthly"
16109
+ },
16110
+ {
16111
+ "value": "annual"
16112
+ }
16113
+ ]
14648
16114
  },
14649
16115
  {
14650
16116
  "name": "cycleToggle",
@@ -15359,6 +16825,10 @@ const MANIFEST_SNAPSHOT = {
15359
16825
  {
15360
16826
  "value": "dot-grid",
15361
16827
  "tier": "default"
16828
+ },
16829
+ {
16830
+ "value": "noise",
16831
+ "tier": "alternative"
15362
16832
  }
15363
16833
  ]
15364
16834
  }
@@ -17313,18 +18783,23 @@ RULES.push({
17313
18783
  }
17314
18784
 
17315
18785
  {
17316
- // GENERATED do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
18786
+ // GENERATED, do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
17317
18787
  // packages/web-modules/**/*.a2ui.json + yaml (ADR-0108 D5 / LLD-0019 C5).
17318
18788
  // Rebuild: node scripts/build/gen-lab-posture-rules.mjs
17319
18789
  // Freshness gate: node scripts/verify/check-lab-posture-rules-fresh.mjs --verify
17320
18790
  //
17321
18791
  // REQUIRED-ATTR (ADR-0108 D5 / gh#3221): a manifest-declared `required: true` prop with no matching attribute set on the occurrence at all.
17322
18792
  //
17323
- // severity: 'warn' here governs ONLY `npm run lint`'s bare-repo-shaped
17324
- // advisory bank (ADR-0040 unchanged). The lab-posture write-time hook
18793
+ // severity: 'warn' here governs `npm run lint`'s bare-repo-shaped
18794
+ // advisory bank (ADR-0040 unchanged) AND the lab-posture write-time hook
17325
18795
  // (packages/plugins/adia-ui-factory/scripts/lab-posture-guard.mjs,
17326
- // LLD-0019 C4) does not read this field a lab-posture write denies on
17327
- // ANY finding from these same matcher functions regardless of severity
18796
+ // LLD-0019 C4 amendment, gh#3236) unlike every sibling rule in this
18797
+ // family, this one IS wired into the hook's write-time pass but never
18798
+ // denies: a finding surfaces as a non-blocking `hookSpecificOutput.
18799
+ // additionalContext` on a "allow" decision instead (conductor ruling,
18800
+ // gh#3221, 2026-09-04: "a false-positive rule never blocks authoring at
18801
+ // the keystroke"). The other four rules in this family stay severity-
18802
+ // blind at write time — ANY of their findings still denies unconditionally
17328
18803
  // (ADR-0108 D5: "PreToolUse deny... error under the lab hook").
17329
18804
 
17330
18805
 
@@ -17544,10 +19019,6 @@ const MANIFEST_SNAPSHOT = {
17544
19019
  }
17545
19020
  ]
17546
19021
  },
17547
- {
17548
- "tag": "admin-scroll",
17549
- "attrs": []
17550
- },
17551
19022
  {
17552
19023
  "tag": "admin-settings-ui",
17553
19024
  "attrs": [
@@ -17663,6 +19134,9 @@ const MANIFEST_SNAPSHOT = {
17663
19134
  {
17664
19135
  "name": "multi"
17665
19136
  },
19137
+ {
19138
+ "name": "options"
19139
+ },
17666
19140
  {
17667
19141
  "name": "question"
17668
19142
  },
@@ -17726,6 +19200,9 @@ const MANIFEST_SNAPSHOT = {
17726
19200
  }
17727
19201
  ]
17728
19202
  },
19203
+ {
19204
+ "name": "suggestions"
19205
+ },
17729
19206
  {
17730
19207
  "name": "variant",
17731
19208
  "values": [
@@ -18459,13 +19936,16 @@ const MANIFEST_SNAPSHOT = {
18459
19936
  "name": "precision",
18460
19937
  "values": [
18461
19938
  {
18462
- "value": "date"
19939
+ "value": "date",
19940
+ "tier": "default"
18463
19941
  },
18464
19942
  {
18465
- "value": "minute"
19943
+ "value": "minute",
19944
+ "tier": "opt-in"
18466
19945
  },
18467
19946
  {
18468
- "value": "second"
19947
+ "value": "second",
19948
+ "tier": "opt-in"
18469
19949
  }
18470
19950
  ]
18471
19951
  },
@@ -18989,6 +20469,10 @@ const MANIFEST_SNAPSHOT = {
18989
20469
  },
18990
20470
  {
18991
20471
  "name": "thinking"
20472
+ },
20473
+ {
20474
+ "name": "thinkingBudget",
20475
+ "attribute": "thinking-budget"
18992
20476
  }
18993
20477
  ]
18994
20478
  },
@@ -19681,7 +21165,30 @@ const MANIFEST_SNAPSHOT = {
19681
21165
  "name": "to"
19682
21166
  },
19683
21167
  {
19684
- "name": "value"
21168
+ "name": "value",
21169
+ "values": [
21170
+ {
21171
+ "value": "today"
21172
+ },
21173
+ {
21174
+ "value": "7d"
21175
+ },
21176
+ {
21177
+ "value": "30d"
21178
+ },
21179
+ {
21180
+ "value": "90d"
21181
+ },
21182
+ {
21183
+ "value": "qtd"
21184
+ },
21185
+ {
21186
+ "value": "ytd"
21187
+ },
21188
+ {
21189
+ "value": "custom"
21190
+ }
21191
+ ]
19685
21192
  }
19686
21193
  ]
19687
21194
  },
@@ -19814,16 +21321,19 @@ const MANIFEST_SNAPSHOT = {
19814
21321
  "name": "side",
19815
21322
  "values": [
19816
21323
  {
19817
- "value": "left"
21324
+ "value": "left",
21325
+ "tier": "opt-in"
19818
21326
  },
19819
21327
  {
19820
- "value": "right"
21328
+ "value": "right",
21329
+ "tier": "default"
19821
21330
  },
19822
21331
  {
19823
21332
  "value": "top"
19824
21333
  },
19825
21334
  {
19826
- "value": "bottom"
21335
+ "value": "bottom",
21336
+ "tier": "opt-in"
19827
21337
  }
19828
21338
  ]
19829
21339
  },
@@ -20318,13 +21828,16 @@ const MANIFEST_SNAPSHOT = {
20318
21828
  "name": "scale",
20319
21829
  "values": [
20320
21830
  {
20321
- "value": "linear"
21831
+ "value": "linear",
21832
+ "tier": "default"
20322
21833
  },
20323
21834
  {
20324
- "value": "log"
21835
+ "value": "log",
21836
+ "tier": "opt-in"
20325
21837
  },
20326
21838
  {
20327
- "value": "quantile"
21839
+ "value": "quantile",
21840
+ "tier": "opt-in"
20328
21841
  }
20329
21842
  ]
20330
21843
  },
@@ -20534,18 +22047,40 @@ const MANIFEST_SNAPSHOT = {
20534
22047
  "value": "off"
20535
22048
  },
20536
22049
  {
20537
- "value": "polite"
22050
+ "value": "polite"
22051
+ },
22052
+ {
22053
+ "value": "assertive"
22054
+ }
22055
+ ]
22056
+ },
22057
+ {
22058
+ "name": "text"
22059
+ },
22060
+ {
22061
+ "name": "variant",
22062
+ "values": [
22063
+ {
22064
+ "value": "default",
22065
+ "tier": "default"
22066
+ },
22067
+ {
22068
+ "value": "info",
22069
+ "tier": "opt-in"
22070
+ },
22071
+ {
22072
+ "value": "success",
22073
+ "tier": "opt-in"
22074
+ },
22075
+ {
22076
+ "value": "warning",
22077
+ "tier": "opt-in"
20538
22078
  },
20539
22079
  {
20540
- "value": "assertive"
22080
+ "value": "danger",
22081
+ "tier": "opt-in"
20541
22082
  }
20542
22083
  ]
20543
- },
20544
- {
20545
- "name": "text"
20546
- },
20547
- {
20548
- "name": "variant"
20549
22084
  }
20550
22085
  ]
20551
22086
  },
@@ -20591,7 +22126,33 @@ const MANIFEST_SNAPSHOT = {
20591
22126
  "name": "error"
20592
22127
  },
20593
22128
  {
20594
- "name": "inputmode"
22129
+ "name": "inputmode",
22130
+ "values": [
22131
+ {
22132
+ "value": "text"
22133
+ },
22134
+ {
22135
+ "value": "decimal"
22136
+ },
22137
+ {
22138
+ "value": "numeric"
22139
+ },
22140
+ {
22141
+ "value": "tel"
22142
+ },
22143
+ {
22144
+ "value": "search"
22145
+ },
22146
+ {
22147
+ "value": "email"
22148
+ },
22149
+ {
22150
+ "value": "url"
22151
+ },
22152
+ {
22153
+ "value": "none"
22154
+ }
22155
+ ]
20595
22156
  },
20596
22157
  {
20597
22158
  "name": "label"
@@ -20692,7 +22253,24 @@ const MANIFEST_SNAPSHOT = {
20692
22253
  "required": true
20693
22254
  },
20694
22255
  {
20695
- "name": "status"
22256
+ "name": "status",
22257
+ "values": [
22258
+ {
22259
+ "value": "available"
22260
+ },
22261
+ {
22262
+ "value": "connected"
22263
+ },
22264
+ {
22265
+ "value": "error"
22266
+ },
22267
+ {
22268
+ "value": "pending"
22269
+ },
22270
+ {
22271
+ "value": "coming-soon"
22272
+ }
22273
+ ]
20696
22274
  }
20697
22275
  ]
20698
22276
  },
@@ -20724,7 +22302,24 @@ const MANIFEST_SNAPSHOT = {
20724
22302
  "name": "searchPlaceholder"
20725
22303
  },
20726
22304
  {
20727
- "name": "statusFilter"
22305
+ "name": "statusFilter",
22306
+ "values": [
22307
+ {
22308
+ "value": "all"
22309
+ },
22310
+ {
22311
+ "value": "available"
22312
+ },
22313
+ {
22314
+ "value": "connected"
22315
+ },
22316
+ {
22317
+ "value": "error"
22318
+ },
22319
+ {
22320
+ "value": "coming-soon"
22321
+ }
22322
+ ]
20728
22323
  }
20729
22324
  ]
20730
22325
  },
@@ -20766,7 +22361,24 @@ const MANIFEST_SNAPSHOT = {
20766
22361
  "name": "printable"
20767
22362
  },
20768
22363
  {
20769
- "name": "status"
22364
+ "name": "status",
22365
+ "values": [
22366
+ {
22367
+ "value": "draft"
22368
+ },
22369
+ {
22370
+ "value": "open"
22371
+ },
22372
+ {
22373
+ "value": "paid"
22374
+ },
22375
+ {
22376
+ "value": "past-due"
22377
+ },
22378
+ {
22379
+ "value": "void"
22380
+ }
22381
+ ]
20770
22382
  }
20771
22383
  ]
20772
22384
  },
@@ -20798,7 +22410,27 @@ const MANIFEST_SNAPSHOT = {
20798
22410
  "name": "exportable"
20799
22411
  },
20800
22412
  {
20801
- "name": "filter"
22413
+ "name": "filter",
22414
+ "values": [
22415
+ {
22416
+ "value": ""
22417
+ },
22418
+ {
22419
+ "value": "draft"
22420
+ },
22421
+ {
22422
+ "value": "open"
22423
+ },
22424
+ {
22425
+ "value": "paid"
22426
+ },
22427
+ {
22428
+ "value": "past-due"
22429
+ },
22430
+ {
22431
+ "value": "void"
22432
+ }
22433
+ ]
20802
22434
  },
20803
22435
  {
20804
22436
  "name": "hrefPattern",
@@ -21011,7 +22643,21 @@ const MANIFEST_SNAPSHOT = {
21011
22643
  "name": "label"
21012
22644
  },
21013
22645
  {
21014
- "name": "variant"
22646
+ "name": "variant",
22647
+ "values": [
22648
+ {
22649
+ "value": "default",
22650
+ "tier": "default"
22651
+ },
22652
+ {
22653
+ "value": "transparent",
22654
+ "tier": "opt-in"
22655
+ },
22656
+ {
22657
+ "value": "blur",
22658
+ "tier": "opt-in"
22659
+ }
22660
+ ]
21015
22661
  }
21016
22662
  ]
21017
22663
  },
@@ -21640,6 +23286,10 @@ const MANIFEST_SNAPSHOT = {
21640
23286
  {
21641
23287
  "tag": "pagination-ui",
21642
23288
  "attrs": [
23289
+ {
23290
+ "name": "noLandmark",
23291
+ "attribute": "no-landmark"
23292
+ },
21643
23293
  {
21644
23294
  "name": "page"
21645
23295
  },
@@ -21845,7 +23495,15 @@ const MANIFEST_SNAPSHOT = {
21845
23495
  "name": "current"
21846
23496
  },
21847
23497
  {
21848
- "name": "cycle"
23498
+ "name": "cycle",
23499
+ "values": [
23500
+ {
23501
+ "value": "monthly"
23502
+ },
23503
+ {
23504
+ "value": "annual"
23505
+ }
23506
+ ]
21849
23507
  },
21850
23508
  {
21851
23509
  "name": "cycleToggle",
@@ -22560,6 +24218,10 @@ const MANIFEST_SNAPSHOT = {
22560
24218
  {
22561
24219
  "value": "dot-grid",
22562
24220
  "tier": "default"
24221
+ },
24222
+ {
24223
+ "value": "noise",
24224
+ "tier": "alternative"
22563
24225
  }
22564
24226
  ]
22565
24227
  }
@@ -24513,7 +26175,7 @@ RULES.push({
24513
26175
  }
24514
26176
 
24515
26177
  {
24516
- // GENERATED do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
26178
+ // GENERATED, do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
24517
26179
  // packages/web-modules/**/*.a2ui.json + yaml (ADR-0108 D5 / LLD-0019 C5).
24518
26180
  // Rebuild: node scripts/build/gen-lab-posture-rules.mjs
24519
26181
  // Freshness gate: node scripts/verify/check-lab-posture-rules-fresh.mjs --verify
@@ -24672,10 +26334,6 @@ const MANIFEST_SNAPSHOT = {
24672
26334
  }
24673
26335
  ]
24674
26336
  },
24675
- {
24676
- "tag": "admin-scroll",
24677
- "attrs": []
24678
- },
24679
26337
  {
24680
26338
  "tag": "admin-settings-ui",
24681
26339
  "attrs": [
@@ -24788,6 +26446,9 @@ const MANIFEST_SNAPSHOT = {
24788
26446
  {
24789
26447
  "name": "multi"
24790
26448
  },
26449
+ {
26450
+ "name": "options"
26451
+ },
24791
26452
  {
24792
26453
  "name": "question"
24793
26454
  },
@@ -24827,6 +26488,9 @@ const MANIFEST_SNAPSHOT = {
24827
26488
  {
24828
26489
  "name": "size"
24829
26490
  },
26491
+ {
26492
+ "name": "suggestions"
26493
+ },
24830
26494
  {
24831
26495
  "name": "variant"
24832
26496
  }
@@ -25380,6 +27044,10 @@ const MANIFEST_SNAPSHOT = {
25380
27044
  },
25381
27045
  {
25382
27046
  "name": "thinking"
27047
+ },
27048
+ {
27049
+ "name": "thinkingBudget",
27050
+ "attribute": "thinking-budget"
25383
27051
  }
25384
27052
  ]
25385
27053
  },
@@ -27129,6 +28797,10 @@ const MANIFEST_SNAPSHOT = {
27129
28797
  {
27130
28798
  "tag": "pagination-ui",
27131
28799
  "attrs": [
28800
+ {
28801
+ "name": "noLandmark",
28802
+ "attribute": "no-landmark"
28803
+ },
27132
28804
  {
27133
28805
  "name": "page"
27134
28806
  },
@@ -28963,7 +30635,131 @@ RULES.push({
28963
30635
  }
28964
30636
 
28965
30637
  {
28966
- // GENERATED do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
30638
+ // GENERATED, do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
30639
+ // packages/web-modules/**/*.a2ui.json + yaml (ADR-0108 D5 / LLD-0019 C5).
30640
+ // Rebuild: node scripts/build/gen-lab-posture-rules.mjs
30641
+ // Freshness gate: node scripts/verify/check-lab-posture-rules-fresh.mjs --verify
30642
+ //
30643
+ // UNKNOWN-SLOT (gh#3222): a `slot="…"` value that is not a slot name any component in the installed manifest declares. Name only, never placement: see trimForEmbed's own `slot-names` branch for why there is no per-tag/nearest-ancestor check.
30644
+ //
30645
+ // severity: 'error' here governs ONLY `npm run lint`'s bare-repo-shaped
30646
+ // advisory bank (ADR-0040 unchanged). The lab-posture write-time hook
30647
+ // (packages/plugins/adia-ui-factory/scripts/lab-posture-guard.mjs,
30648
+ // LLD-0019 C4) does not read this field — a lab-posture write denies on
30649
+ // ANY finding from these same matcher functions regardless of severity
30650
+ // (ADR-0108 D5: "PreToolUse deny... error under the lab hook").
30651
+
30652
+
30653
+ const MANIFEST_SNAPSHOT = {
30654
+ "slotNames": [
30655
+ "action",
30656
+ "action-leading",
30657
+ "actions",
30658
+ "actions-leading",
30659
+ "add",
30660
+ "after",
30661
+ "after-invoice-history",
30662
+ "aside",
30663
+ "attach",
30664
+ "badge",
30665
+ "bar",
30666
+ "before",
30667
+ "before-plan-picker",
30668
+ "body",
30669
+ "breadcrumb",
30670
+ "canvas",
30671
+ "caption",
30672
+ "caret",
30673
+ "change",
30674
+ "chart",
30675
+ "charts",
30676
+ "chip",
30677
+ "chrome",
30678
+ "cite",
30679
+ "close",
30680
+ "complete",
30681
+ "content",
30682
+ "copy",
30683
+ "cursor",
30684
+ "customer-block",
30685
+ "default",
30686
+ "description",
30687
+ "digit",
30688
+ "empty",
30689
+ "error",
30690
+ "eyebrow",
30691
+ "footer",
30692
+ "header",
30693
+ "header-actions",
30694
+ "heading",
30695
+ "hint",
30696
+ "icon",
30697
+ "icon-trailing",
30698
+ "image",
30699
+ "kpis",
30700
+ "label",
30701
+ "leading",
30702
+ "lede",
30703
+ "legal",
30704
+ "loading",
30705
+ "media",
30706
+ "meta",
30707
+ "model",
30708
+ "name",
30709
+ "off",
30710
+ "on",
30711
+ "panel",
30712
+ "popover",
30713
+ "prefix",
30714
+ "presets",
30715
+ "primary",
30716
+ "remit-to-block",
30717
+ "scope",
30718
+ "secondary",
30719
+ "send",
30720
+ "skeleton",
30721
+ "status",
30722
+ "subtitle",
30723
+ "suffix",
30724
+ "summary",
30725
+ "table",
30726
+ "text",
30727
+ "time",
30728
+ "title",
30729
+ "toolbar",
30730
+ "track",
30731
+ "trailing",
30732
+ "trigger",
30733
+ "value",
30734
+ "zoom"
30735
+ ]
30736
+ };
30737
+
30738
+
30739
+ RULES.push({
30740
+ id: "UNKNOWN-SLOT",
30741
+ severity: "error",
30742
+ scope: 'shared',
30743
+ fileTypes: MARKUP_EXT,
30744
+ match(text, path = '') {
30745
+ return slotNameFindings(text, MANIFEST_SNAPSHOT, path).map((f) => ({ line: f.line, snippet: f.snippet, why: f.why }));
30746
+ },
30747
+ fix: null, // a manifest-drift violation names the correction in its own diagnostic; applying it is a judgment call the agent makes, not a mechanical rewrite
30748
+ fixtures: {
30749
+ "smelly": [
30750
+ "<span slot=\"not-a-real-slot-name\"></span>"
30751
+ ],
30752
+ "clean": [
30753
+ "<span slot=\"action\"></span>",
30754
+ "<span slot=\"${s.section}\"></span>",
30755
+ "<span slot=\"{s.section}\"></span>"
30756
+ ]
30757
+ },
30758
+ });
30759
+ }
30760
+
30761
+ {
30762
+ // GENERATED, do not hand-edit. Source: packages/web-components/**/*.a2ui.json + yaml,
28967
30763
  // packages/web-modules/**/*.a2ui.json + yaml (ADR-0108 D5 / LLD-0019 C5).
28968
30764
  // Rebuild: node scripts/build/gen-lab-posture-rules.mjs
28969
30765
  // Freshness gate: node scripts/verify/check-lab-posture-rules-fresh.mjs --verify
@@ -29024,10 +30820,6 @@ const MANIFEST_SNAPSHOT = {
29024
30820
  "tag": "admin-roster-ui",
29025
30821
  "attrs": []
29026
30822
  },
29027
- {
29028
- "tag": "admin-scroll",
29029
- "attrs": []
29030
- },
29031
30823
  {
29032
30824
  "tag": "admin-settings-ui",
29033
30825
  "attrs": []
@@ -30580,6 +32372,120 @@ RULES.push({
30580
32372
  {
30581
32373
 
30582
32374
 
32375
+ // gh#3627 (follow-up to gh#3604): select-ui's avatar `<img>` shipped with no
32376
+ // width/height HTML attributes, sizing purely dependent on select.css's own
32377
+ // rule loading before first paint. select.css already carried explicit
32378
+ // width/height on that selector (packages/web-components/components/select/
32379
+ // select.css's `img[data-option-avatar]` rule), nothing was wrong with the
32380
+ // CSS. The gap was the OTHER half of the contract: an `<img>` stamped by a
32381
+ // component's own render logic needs its own width/height (or an inline
32382
+ // aspect-ratio) so the box is reserved from HTML parse time, independent of
32383
+ // when that component's stylesheet actually applies (a per-route CSS bundle
32384
+ // can arrive after first paint, gh#3357). gh#3604's fix added exactly that
32385
+ // to the three template-literal `<img>` sites in select.class.js; this rule
32386
+ // keeps a fourth one from shipping the same way.
32387
+ //
32388
+ // Scope, deliberately narrow: ONLY a literal `<img ...>` tag authored
32389
+ // inside a JS template-literal/string in packages/web-components or
32390
+ // packages/web-modules, the shape that actually regressed. Three things
32391
+ // this rule does NOT catch, on purpose:
32392
+ // 1. `document.createElement('img')` call sites (avatar.class.js,
32393
+ // image.class.js, integration-card.class.js), each of those adopts
32394
+ // the img into a host element that already carries its own explicit
32395
+ // size (avatar-ui's :host sizing tokens, image-ui's own #applySize(),
32396
+ // a fixed-size logo slot), so the box is reserved by the PARENT
32397
+ // regardless of the img's own attributes. Flagging every
32398
+ // createElement('img') would nag on a legitimate, already-reserved
32399
+ // pattern this framework uses on purpose, exactly the "sized by a
32400
+ // parent's grid cell" false positive this rule is written to avoid.
32401
+ // 2. Demo/example/gallery markup (`.html`, `.yaml` examples, `.a2ui.json`),
32402
+ // illustrative content already exempt from render-timing concerns
32403
+ // (HARDCODED-OPEN's `.examples.html` precedent is the same reasoning).
32404
+ // 3. `apps/`, `playgrounds/`, `catalog/`, consumer/demo surfaces, not
32405
+ // framework source. NO-LOCAL-ESCAPE-HTML (gh#3641/gh#3678) is the
32406
+ // precedent for widening a framework-scoped rule to a consumer tree
32407
+ // only once a ticket finds and migrates a real instance there; no such
32408
+ // instance exists for this rule yet.
32409
+ // An author who has a real reason an img here doesn't need its own box
32410
+ // (e.g. deliberately sized by an ancestor CSS grid/flex cell) can still
32411
+ // suppress a specific finding with the standard engine-wide
32412
+ // adia-lint-disable comment (SUPPRESS_RE/suppressionFor, primitives.mjs).
32413
+ //
32414
+ // Known imprecision (documented, not fixed here): like every other
32415
+ // CODE_EXT rule in this bank, comment-stripping is line-based and doesn't
32416
+ // understand string literals, so a hardcoded `src="https://..."` URL on
32417
+ // the SAME source line as `width=`/`height=` would truncate the line at
32418
+ // `//` and could false-positive. Every real instance in this corpus today
32419
+ // interpolates `src` from a variable (`src="${escapeHTML(x)}"`), not a
32420
+ // literal URL, so this hasn't triggered in practice; a literal `//` URL
32421
+ // sharing a line with the dimension attributes is the one shape to watch
32422
+ // for in review.
32423
+ //
32424
+ // gh#3985 (a second consequence of the same string-literal-blind gap
32425
+ // above): a plain JS string mentioning markup IN PROSE, not authoring it,
32426
+ // still satisfies `<img\b[^>]*?>`, PR #3977's own vitest-setup error
32427
+ // message ("...a <link>, <script> or " + "<img> src), turn that loading
32428
+ // off...") matched a bare, zero-attribute `<img>` and read as an
32429
+ // error-severity finding on a file that emits no DOM at all. Every REAL
32430
+ // `<img>` this rule was written to catch (gh#3604's select.class.js
32431
+ // sites, this corpus's only non-comment instances) sets at least one real
32432
+ // attribute (`src=` at minimum: an img with none renders nothing, so
32433
+ // hand-authored markup never omits it); a bare `<img>`/`<img >` with zero
32434
+ // `name=value` pairs is never genuine output markup in this corpus, only
32435
+ // prose describing one, so HAS_ANY_ATTR below gates the match the same
32436
+ // way HAS_WIDTH/HAS_HEIGHT already gate the dimension check. This is a
32437
+ // narrow, corpus-verified discriminator, not a full JS/HTML parse (an
32438
+ // attribute-bearing `<img width=100>` still mentioned in a doc comment or
32439
+ // prose sentence would still false-positive; none exists in this corpus
32440
+ // today, watch for it in review the same way the paragraph above already
32441
+ // asks).
32442
+ const IMG_TAG_RE = /<img\b[^>]*?>/gis;
32443
+ const HAS_WIDTH = /\bwidth\s*=/i;
32444
+ const HAS_HEIGHT = /\bheight\s*=/i;
32445
+ const HAS_ASPECT_RATIO = /aspect-ratio\s*:/i;
32446
+ const HAS_ANY_ATTR = /\S=/;
32447
+ const CORPUS_PATH_RE = /packages[\\/](web-components|web-modules)[\\/]/;
32448
+
32449
+
32450
+ RULES.push({
32451
+ id: 'IMG-DIMENSION-RESERVE',
32452
+ severity: 'error',
32453
+ scope: 'shared',
32454
+ fileTypes: CODE_EXT,
32455
+ match(text, path) {
32456
+ const out = [];
32457
+ const normalized = (path || '').replace(/\\/g, '/');
32458
+ if (!CORPUS_PATH_RE.test(normalized)) return out;
32459
+ if (normalized.endsWith('.test.js')) return out;
32460
+
32461
+ const rawLines = text.split('\n');
32462
+ const blanked = stripCommentLines(text, ext(path))
32463
+ .map((l) => l.stripped)
32464
+ .join('\n');
32465
+
32466
+ IMG_TAG_RE.lastIndex = 0;
32467
+ let m;
32468
+ while ((m = IMG_TAG_RE.exec(blanked))) {
32469
+ const tag = m[0];
32470
+ if (!HAS_ANY_ATTR.test(tag)) continue; // gh#3985: no attribute at all, prose mentioning <img>, not authored markup
32471
+ if ((HAS_WIDTH.test(tag) && HAS_HEIGHT.test(tag)) || HAS_ASPECT_RATIO.test(tag)) continue;
32472
+ const lineNo = lineOf(blanked, m.index);
32473
+ out.push({
32474
+ line: lineNo,
32475
+ snippet: (rawLines[lineNo - 1] || tag).trim().slice(0, 90),
32476
+ why: 'an <img> stamped by component render logic needs its own width/height (or an inline aspect-ratio) so the box is reserved before CSS applies (gh#3604 CLS)',
32477
+ });
32478
+ }
32479
+ return out;
32480
+ },
32481
+ });
32482
+ }
32483
+
32484
+ {
32485
+
32486
+
32487
+ const SHELLS = new Set(['admin-shell', 'chat-shell', 'editor-shell', 'app-shell-ui', 'adia-chat-ui', 'adia-editor-ui']);
32488
+
30583
32489
 
30584
32490
  RULES.push({
30585
32491
  id: 'LEGACY-SHELL',
@@ -30587,18 +32493,50 @@ RULES.push({
30587
32493
  scope: 'shared',
30588
32494
  fileTypes: [],
30589
32495
  match(text, path) {
30590
- const out = [];
30591
- for (const { lineNo, raw, stripped } of stripCommentLines(text, ext(path))) {
30592
- if (!stripped.trim()) continue;
30593
- if (RE.LEGACY_SHELL.test(stripped)) {
30594
- out.push({
30595
- line: lineNo,
30596
- snippet: raw.trim().slice(0, 90),
30597
- why: 'retired shell shape (ADR-0024, v0.4.0) use the bespoke tag (chat-thread / chat-composer / chat-empty · admin-sidebar / admin-command · editor-canvas · pane-ui)',
30598
- });
32496
+ // Component yaml contains contract prose and A2UI JSON, not live HTML.
32497
+ // Match syntax in source rather than retirement examples or name prefixes.
32498
+ const fileExt = ext(path);
32499
+ if (YAML_EXT.includes(fileExt)) return [];
32500
+ const lines = stripCommentLines(text, fileExt);
32501
+ const source = markupScanSurface(lines.map(({ stripped }) => stripped).join('\n'));
32502
+ const found = new Set();
32503
+ for (const { attrs, tag, line } of tagOccurrences(source, path)) {
32504
+ if (Object.keys(attrs).some((name) => RE.LEGACY_SHELL.test(name)) ||
32505
+ (tag === 'dialog' && Object.hasOwn(attrs, 'data-command'))) found.add(line);
32506
+ }
32507
+ // aside-ui remains a supported in-page slot stub. Only its use as a
32508
+ // shell region is retired; native wrappers preserve that shell context.
32509
+ for (const { tag, parentTag, line } of tagTree(source, path)) {
32510
+ if (tag === 'aside-ui' && SHELLS.has(parentTag)) found.add(line);
32511
+ }
32512
+ const referenceSurfaces = [];
32513
+ if (CODE_EXT.includes(fileExt) || STYLE_EXT.includes(fileExt)) {
32514
+ referenceSurfaces.push({ text: source, line: 1 });
32515
+ } else {
32516
+ // Inline executable blocks need the same coverage as separate files.
32517
+ // Scan their bodies only, so selectors quoted in page prose stay clean.
32518
+ for (const block of source.matchAll(/<(script|style)\b[^>]*>([\s\S]*?)<\/\1\s*>/gi)) {
32519
+ const start = block.index + block[0].indexOf('>') + 1;
32520
+ const body = stripCommentLines(block[2], block[1].toLowerCase() === 'script' ? '.js' : '.css')
32521
+ .map(({ stripped }) => stripped).join('\n');
32522
+ referenceSurfaces.push({ text: body, line: lineOf(source, start) });
30599
32523
  }
30600
32524
  }
30601
- return out;
32525
+ // Keep selector reads and DOM attribute writes covered as well as
32526
+ // markup. Exact attribute names preserve unrelated namespaced hooks.
32527
+ const references = /\[\s*([\w-]+)(?=[\s\]=~|^$*])|\b(?:setAttribute|getAttribute|hasAttribute|removeAttribute|toggleAttribute)\s*\(\s*['"]([\w-]+)['"]/g;
32528
+ for (const surface of referenceSurfaces) {
32529
+ for (const match of surface.text.matchAll(references)) {
32530
+ if (RE.LEGACY_SHELL.test(match[1] || match[2])) {
32531
+ found.add(surface.line + lineOf(surface.text, match.index) - 1);
32532
+ }
32533
+ }
32534
+ }
32535
+ return [...found].sort((a, b) => a - b).map((line) => ({
32536
+ line,
32537
+ snippet: lines[line - 1].raw.trim().slice(0, 90),
32538
+ why: 'retired shell shape (ADR-0024, v0.4.0): use the bespoke tag (chat-thread / chat-composer / chat-empty · admin-sidebar / admin-command · editor-canvas · pane-ui)',
32539
+ }));
30602
32540
  },
30603
32541
  });
30604
32542
  }
@@ -30682,6 +32620,66 @@ RULES.push({
30682
32620
  {
30683
32621
 
30684
32622
 
32623
+ // gh#3641: escapeHTML was independently defined seven times across the
32624
+ // framework corpus (six components plus chat-shell, one of the seven
32625
+ // spelled `escapeHtml`, case varies, tour.class.js, three of the seven
32626
+ // leaving quotes unescaped (an attribute-breakout XSS vector). The
32627
+ // shared, quote-safe implementation now lives at
32628
+ // packages/web-components/core/html-escape.js; this rule refuses a new
32629
+ // local `function escapeHtml(`/`escapeHTML(` (any casing) inside the
32630
+ // framework corpus (packages/web-components, packages/web-modules) so
32631
+ // the fix cannot re-fragment the way it already did once (twice: the
32632
+ // original 5-variant fragmentation, then a 7th instance under a
32633
+ // different casing that a case-sensitive first version of this very
32634
+ // rule could not see). Case-insensitive on purpose: the defect class is
32635
+ // "a local escapeHTML-shaped helper", not one exact spelling.
32636
+ // gh#3678: five more local copies turned up in apps/ (construct-canvas,
32637
+ // genui), two of them quote-unsafe, the same attribute-breakout defect
32638
+ // class this rule was written for. This scope note previously said
32639
+ // consumer app code was exempt because it "owns its own copy of a value
32640
+ // this small", that call was reversed by conductor ruling (gh#3678
32641
+ // resolution 2: migrate, not wontfix), so CORPUS_PATH_RE now also
32642
+ // matches apps/. playgrounds/ and catalog/ remain out of scope until a
32643
+ // ticket similarly finds and migrates a local copy there.
32644
+ // `declare function` is a .d.ts ambient signature, not a re-implementation:
32645
+ // html-escape.d.ts's own `export declare function escapeHTML(...)` must
32646
+ // not flag itself. Only a real function body is the defect this rule
32647
+ // exists for. The lookbehind requires `\s+` (one or more), not `\s`
32648
+ // (exactly one), so `declare function` (extra space) or a line break
32649
+ // between the two keywords still exempts the ambient signature.
32650
+ const LOCAL_DEF_RE = /(?<!declare\s+)\bfunction\s+escapeHtml\s*\(/i;
32651
+ const OWNER_PATH_RE = /packages[\\/]web-components[\\/]core[\\/]html-escape\.(js|d\.ts)$/;
32652
+ const CORPUS_PATH_RE = /(?:packages[\\/](web-components|web-modules)|(?:^|[\\/])apps)[\\/]/;
32653
+
32654
+
32655
+ RULES.push({
32656
+ id: 'NO-LOCAL-ESCAPE-HTML',
32657
+ severity: 'error',
32658
+ scope: 'shared',
32659
+ fileTypes: CODE_EXT,
32660
+ match(text, path) {
32661
+ const out = [];
32662
+ const normalized = path.replace(/\\/g, '/');
32663
+ if (!CORPUS_PATH_RE.test(normalized)) return out;
32664
+ if (OWNER_PATH_RE.test(normalized)) return out;
32665
+ for (const { lineNo, raw, stripped } of stripCommentLines(text, ext(path))) {
32666
+ if (!stripped.trim()) continue;
32667
+ if (LOCAL_DEF_RE.test(stripped)) {
32668
+ out.push({
32669
+ line: lineNo,
32670
+ snippet: raw.trim().slice(0, 90),
32671
+ why: 'do not redefine escapeHTML locally, import { escapeHTML } from the core barrel (packages/web-components/core/html-escape.js), gh#3641',
32672
+ });
32673
+ }
32674
+ }
32675
+ return out;
32676
+ },
32677
+ });
32678
+ }
32679
+
32680
+ {
32681
+
32682
+
30685
32683
  // Parity with the two retired scripts: forge-lint recognized only
30686
32684
  // `forge-lint: foundation`, adia-lint only `adia-lint: foundation` — never
30687
32685
  // either tag under the other script. Since this rule is shared, the active
@@ -30694,6 +32692,39 @@ function optInTagsFor(scopes) {
30694
32692
  return ['forge-lint', 'adia-lint'];
30695
32693
  }
30696
32694
 
32695
+ // gh#3839: a `var(--token, <literal>)` fallback is still a shipped literal
32696
+ // whenever the named token is undefined: RAW-COLOR must see inside it.
32697
+ // `light-dark(<light>, <dark>)` is the one legitimate literal-pair form (both
32698
+ // arms are meant to be literal color values by design), so its call is
32699
+ // stripped out, balanced on parens, so a color function nested in either
32700
+ // arm (e.g. `light-dark(oklch(...), oklch(...))`) is stripped too, before
32701
+ // the rest of the declaration (var() fallbacks included) is scanned as
32702
+ // plain text. Custom-property names never match HEXCOLOR/FUNCCOLOR, so
32703
+ // leaving `var(` unstripped carries no risk of matching the reference
32704
+ // itself, only a literal actually sitting in its fallback.
32705
+ function stripBalancedCalls(text, fnName) {
32706
+ const token = `${fnName}(`;
32707
+ let result = '';
32708
+ let i = 0;
32709
+ while (i < text.length) {
32710
+ const idx = text.indexOf(token, i);
32711
+ if (idx === -1) {
32712
+ result += text.slice(i);
32713
+ break;
32714
+ }
32715
+ result += text.slice(i, idx);
32716
+ let depth = 1;
32717
+ let j = idx + token.length;
32718
+ while (j < text.length && depth > 0) {
32719
+ if (text[j] === '(') depth += 1;
32720
+ else if (text[j] === ')') depth -= 1;
32721
+ j += 1;
32722
+ }
32723
+ i = j;
32724
+ }
32725
+ return result;
32726
+ }
32727
+
30697
32728
 
30698
32729
  RULES.push({
30699
32730
  id: 'RAW-COLOR',
@@ -30709,8 +32740,8 @@ RULES.push({
30709
32740
  for (const decl of stripped.split(';')) {
30710
32741
  const d = decl.trim();
30711
32742
  if (!d || d.startsWith('//') || d.startsWith('/*') || d.startsWith('*')) continue;
30712
- if (decl.includes('var(') || decl.includes('light-dark(')) continue;
30713
- if (RE.HEXCOLOR.test(decl) || RE.FUNCCOLOR.test(decl)) {
32743
+ const scanText = decl.includes('light-dark(') ? stripBalancedCalls(decl, 'light-dark') : decl;
32744
+ if (RE.HEXCOLOR.test(scanText) || RE.FUNCCOLOR.test(scanText)) {
30714
32745
  out.push({
30715
32746
  line: lineNo,
30716
32747
  snippet: raw.trim().slice(0, 90),
@@ -30775,20 +32806,48 @@ RULES.push({
30775
32806
 
30776
32807
 
30777
32808
 
32809
+ /**
32810
+ * SCOPE-EXTENT (gh#3718): error severity, with the pre-existing corpus of
32811
+ * findings downgraded to warn via a checked-in baseline rather than by a
32812
+ * blanket severity.json entry.
32813
+ *
32814
+ * The blanket downgrade is what this replaces. It made every site invisible,
32815
+ * including sites added after the downgrade, so the rule could not do the one
32816
+ * job it was written for. The baseline inverts that: known sites stay warn,
32817
+ * anything not in the baseline errors.
32818
+ *
32819
+ * Sites are keyed on the normalized text of the matched block, per file, as a
32820
+ * MULTISET, not on line numbers. A line-keyed baseline goes stale whenever an
32821
+ * unrelated edit shifts a selector down, which reports a pre-existing finding
32822
+ * as new for a reason that has nothing to do with this rule. The multiset also
32823
+ * keeps duplicates, so a second identical violation in an already-baselined
32824
+ * file exceeds its entry and errors, which a per-file count would miss.
32825
+ */
32826
+
30778
32827
  RULES.push({
30779
32828
  id: 'SCOPE-EXTENT',
30780
32829
  severity: 'error',
30781
32830
  scope: 'shared',
30782
32831
  fileTypes: STYLE_EXT,
30783
- match(text) {
32832
+ match(text, path) {
30784
32833
  const out = [];
30785
32834
  const re = new RegExp(RE.SCOPE_EXTENT.source, 'gs');
32835
+ // A local copy, consumed as we go: each baselined entry excuses exactly
32836
+ // one finding, so the second identical violation in the same file finds
32837
+ // the entry already spent and reports at full severity.
32838
+ const remaining = [...baselineFor(path)];
30786
32839
  let m;
30787
32840
  while ((m = re.exec(text))) {
32841
+ const key = normalizeMatch(m[0]);
32842
+ const at = remaining.indexOf(key);
32843
+ const baselined = at !== -1;
32844
+ if (baselined) remaining.splice(at, 1);
30788
32845
  out.push({
30789
32846
  line: lineOf(text, m.index),
30790
32847
  snippet: ':scope { … width/height … }',
30791
32848
  why: "a primitive is size-agnostic — let the consumer own width/height; don't set extent on :scope",
32849
+ // Per-finding override; absent means the rule's own severity applies.
32850
+ ...(baselined ? { severity: 'warn' } : {}),
30792
32851
  });
30793
32852
  }
30794
32853
  return out;