@adia-ai/adia-ui-forge 0.8.60 → 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.
@@ -1,5 +1,5 @@
1
1
  // GENERATED, do not hand-edit. Sources:
2
- // scripts/lint/engine/primitives.mjs, scripts/lint/engine/run.mjs
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
@@ -15,7 +15,6 @@
15
15
  // scripts/lint/rules/generated/composition/admin-content.mjs
16
16
  // scripts/lint/rules/generated/composition/admin-entity-item.mjs
17
17
  // scripts/lint/rules/generated/composition/admin-roster.mjs
18
- // scripts/lint/rules/generated/composition/admin-scroll.mjs
19
18
  // scripts/lint/rules/generated/composition/admin-settings.mjs
20
19
  // scripts/lint/rules/generated/composition/admin-shell.mjs
21
20
  // scripts/lint/rules/generated/composition/admin-sidebar.mjs
@@ -219,8 +218,24 @@ function lineOf(text, offset) {
219
218
  return n;
220
219
  }
221
220
 
222
- /** Suppression comment recognized on the line itself or the line before a finding. */
223
- 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*\*\//;
224
239
 
225
240
  /** Given all raw lines and a finding's line number, is it suppressed, and was a reason given? */
226
241
  function suppressionFor(rawLines, lineNo, ruleId) {
@@ -251,6 +266,47 @@ function allSuppressions(rawLines) {
251
266
  return out;
252
267
  }
253
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
+
254
310
  const RE = {
255
311
  HEXCOLOR: /#[0-9a-fA-F]{3,8}\b/,
256
312
  FUNCCOLOR: /\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\s*\(/,
@@ -265,6 +321,11 @@ const RE = {
265
321
  SSR_SIGNAL: /['"]use client['"]|\buseEffect\b|\bonMounted\b|\bonMount\b|from\s+['"](?:react|vue|svelte|next|nuxt|@sveltejs|astro)|getServerSideProps|defineNuxtComponent/i,
266
322
  TOPLEVEL_IMPORT: /^\s*import\s+['"]@adia-ai\/web-components['"]\s*;?\s*$/m,
267
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*\(/,
268
329
  };
269
330
 
270
331
  /** Per-declaration split on `;` — mirrors both Python scripts' `line.split(';')` loops. */
@@ -737,6 +798,32 @@ function markupScanSurface(text, path = '') {
737
798
  return surface;
738
799
  }
739
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
+
740
827
  /**
741
828
  * ADR-0108 D5 / LLD-0019 C5: builds lookup tables from a manifest object
742
829
  * shaped like `packages/gen-ui/mcp/factory/manifest.js`'s `getManifest()`
@@ -1146,14 +1233,25 @@ function makeLinter(rules, opts = {}) {
1146
1233
  if (rule.scope === 'forge-only' && !isForgeOnlyPath(path)) continue;
1147
1234
  const hits = rule.match(text, path, { scopes }) || [];
1148
1235
  for (const h of hits) {
1149
- 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
+ });
1150
1243
  }
1151
1244
  }
1152
1245
 
1153
1246
  // Suppression syntax (LLD-0016 §Interfaces):
1154
- // /* adia-lint-disable <rule-id> — <reason> */ on the finding's own line
1155
- // or the line before it drops that one finding. A bare disable with no
1156
- // <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
1157
1255
  // once per suppression comment in the file, independent of whether it
1158
1256
  // actually suppressed anything this run.
1159
1257
  findings = findings.filter((f) => !suppressionFor(rawLines, f.line, f.name).suppressed);
@@ -1168,6 +1266,24 @@ function makeLinter(rules, opts = {}) {
1168
1266
  }
1169
1267
  }
1170
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
+
1171
1287
  findings.sort((a, b) => (a.line - b.line) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
1172
1288
  return findings;
1173
1289
  }
@@ -1185,6 +1301,130 @@ function render(path, findings) {
1185
1301
  }
1186
1302
 
1187
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
+
1188
1428
  const RULES = [];
1189
1429
  {
1190
1430
 
@@ -1428,8 +1668,11 @@ RULES.push({
1428
1668
  const pages = (text.match(/<admin-page[\s/>]/g) || []).length;
1429
1669
  const scrolls = (text.match(/<admin-scroll[\s/>]/g) || []).length;
1430
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/>]/);
1431
1674
  out.push({
1432
- line: 1,
1675
+ line: lineOf(text, firstIndex),
1433
1676
  snippet: '<admin-page> × N inside <admin-scroll>',
1434
1677
  why: 'each <admin-scroll> hosts exactly one <admin-page> — multiple pages need multiple scroll regions',
1435
1678
  });
@@ -1449,6 +1692,8 @@ RULES.push({
1449
1692
  }
1450
1693
 
1451
1694
  {
1695
+
1696
+
1452
1697
  const APPLIES_EXT = ['.html', '.htm', '.vue', '.svelte', '.astro', '.tsx', '.jsx', '.js', '.mjs', '.ts'];
1453
1698
 
1454
1699
 
@@ -1458,9 +1703,10 @@ RULES.push({
1458
1703
  scope: 'consumer-only',
1459
1704
  fileTypes: APPLIES_EXT,
1460
1705
  match(text) {
1461
- 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')) {
1462
1708
  return [{
1463
- line: 1,
1709
+ line: lineOf(text, m.index),
1464
1710
  snippet: '<admin-sidebar resizable> without [data-sidebar-resize]',
1465
1711
  why: '[resizable] needs a child <div data-sidebar-resize> or there is no drag handle',
1466
1712
  }];
@@ -1528,6 +1774,29 @@ RULES.push(ssrDoubleRouter, ssrToplevelImport);
1528
1774
  {
1529
1775
 
1530
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
+
1531
1800
 
1532
1801
  RULES.push({
1533
1802
  id: 'MISSING-SCOPE',
@@ -1537,12 +1806,27 @@ RULES.push({
1537
1806
  match(text, path) {
1538
1807
  const isTokenish = isFoundationCss(path, text, 'forge-lint');
1539
1808
  if (isTokenish) return [];
1540
- 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 [];
1541
1825
  if (!RE.SELECTOR_RULE.test(text)) return [];
1542
1826
  return [{
1543
1827
  line: 1,
1544
1828
  snippet: '(file has CSS rules but no @scope block)',
1545
- 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 */)',
1546
1830
  }];
1547
1831
  },
1548
1832
  });
@@ -1562,7 +1846,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1562
1846
 
1563
1847
  RULES.push({
1564
1848
  id: "COMPOSITION-ACCORDION-ITEM-UI",
1565
- 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.
1566
1850
  scope: 'shared',
1567
1851
  fileTypes: MARKUP_EXT,
1568
1852
  match(text) {
@@ -1594,7 +1878,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1594
1878
 
1595
1879
  RULES.push({
1596
1880
  id: "COMPOSITION-ACCORDION-UI",
1597
- 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.
1598
1882
  scope: 'shared',
1599
1883
  fileTypes: MARKUP_EXT,
1600
1884
  match(text) {
@@ -1627,7 +1911,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1627
1911
 
1628
1912
  RULES.push({
1629
1913
  id: "COMPOSITION-ACTION-ITEM-UI",
1630
- 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.
1631
1915
  scope: 'shared',
1632
1916
  fileTypes: MARKUP_EXT,
1633
1917
  match(text) {
@@ -1659,7 +1943,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1659
1943
 
1660
1944
  RULES.push({
1661
1945
  id: "COMPOSITION-ACTION-LIST-UI",
1662
- 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.
1663
1947
  scope: 'shared',
1664
1948
  fileTypes: MARKUP_EXT,
1665
1949
  match(text) {
@@ -1692,7 +1976,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1692
1976
 
1693
1977
  RULES.push({
1694
1978
  id: "COMPOSITION-ADMIN-COMMAND",
1695
- 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.
1696
1980
  scope: 'shared',
1697
1981
  fileTypes: MARKUP_EXT,
1698
1982
  match(text) {
@@ -1724,7 +2008,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1724
2008
 
1725
2009
  RULES.push({
1726
2010
  id: "COMPOSITION-ADMIN-CONTENT",
1727
- 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.
1728
2012
  scope: 'shared',
1729
2013
  fileTypes: MARKUP_EXT,
1730
2014
  match(text) {
@@ -1756,7 +2040,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1756
2040
 
1757
2041
  RULES.push({
1758
2042
  id: "COMPOSITION-ADMIN-ENTITY-ITEM",
1759
- 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.
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.
1760
2044
  scope: 'shared',
1761
2045
  fileTypes: MARKUP_EXT,
1762
2046
  match(text) {
@@ -1788,7 +2072,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1788
2072
 
1789
2073
  RULES.push({
1790
2074
  id: "COMPOSITION-ADMIN-ROSTER-UI",
1791
- 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.
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.
1792
2076
  scope: 'shared',
1793
2077
  fileTypes: MARKUP_EXT,
1794
2078
  match(text) {
@@ -1806,38 +2090,6 @@ RULES.push({
1806
2090
  });
1807
2091
  }
1808
2092
 
1809
- {
1810
- // GENERATED: do not hand-edit. Source: packages/web-modules/shell/admin-scroll/admin-scroll.yaml's a2ui.allowedParents/allowedChildren.
1811
- // Rebuild: node scripts/build/gen-composition-rules.mjs
1812
- // Freshness gate: node scripts/verify/check-composition-rules-fresh.mjs --verify
1813
-
1814
-
1815
- const TAG = "admin-scroll";
1816
- const ALLOWED_PARENTS = ["admin-content"];
1817
- const ALLOWED_CHILDREN = [];
1818
- const ALLOWED_CHILDREN_BY_SLOT = null;
1819
-
1820
-
1821
- RULES.push({
1822
- id: "COMPOSITION-ADMIN-SCROLL",
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.
1824
- scope: 'shared',
1825
- fileTypes: MARKUP_EXT,
1826
- match(text) {
1827
- return compositionFindings(text, { tag: TAG, allowedParents: ALLOWED_PARENTS, allowedChildren: ALLOWED_CHILDREN, allowedChildrenBySlot: ALLOWED_CHILDREN_BY_SLOT });
1828
- },
1829
- fix: null, // moving markup to satisfy a composition constraint is a judgment call, not a mechanical rename (LLD-0016 §Risks R4/R5)
1830
- fixtures: {
1831
- "smelly": [
1832
- "<div><admin-scroll></admin-scroll></div>"
1833
- ],
1834
- "clean": [
1835
- "<admin-content><admin-scroll></admin-scroll></admin-content>"
1836
- ]
1837
- },
1838
- });
1839
- }
1840
-
1841
2093
  {
1842
2094
  // GENERATED: do not hand-edit. Source: packages/web-modules/agent-admin/admin-settings/admin-settings.yaml's a2ui.allowedParents/allowedChildren.
1843
2095
  // Rebuild: node scripts/build/gen-composition-rules.mjs
@@ -1852,7 +2104,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1852
2104
 
1853
2105
  RULES.push({
1854
2106
  id: "COMPOSITION-ADMIN-SETTINGS-UI",
1855
- 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.
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.
1856
2108
  scope: 'shared',
1857
2109
  fileTypes: MARKUP_EXT,
1858
2110
  match(text) {
@@ -1884,7 +2136,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1884
2136
 
1885
2137
  RULES.push({
1886
2138
  id: "COMPOSITION-ADMIN-SHELL",
1887
- 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.
1888
2140
  scope: 'shared',
1889
2141
  fileTypes: MARKUP_EXT,
1890
2142
  match(text) {
@@ -1916,7 +2168,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1916
2168
 
1917
2169
  RULES.push({
1918
2170
  id: "COMPOSITION-ADMIN-SIDEBAR",
1919
- 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.
1920
2172
  scope: 'shared',
1921
2173
  fileTypes: MARKUP_EXT,
1922
2174
  match(text) {
@@ -1948,7 +2200,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1948
2200
 
1949
2201
  RULES.push({
1950
2202
  id: "COMPOSITION-ADMIN-STATUSBAR",
1951
- 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.
1952
2204
  scope: 'shared',
1953
2205
  fileTypes: MARKUP_EXT,
1954
2206
  match(text) {
@@ -1980,7 +2232,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
1980
2232
 
1981
2233
  RULES.push({
1982
2234
  id: "COMPOSITION-ADMIN-TOPBAR",
1983
- 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.
1984
2236
  scope: 'shared',
1985
2237
  fileTypes: MARKUP_EXT,
1986
2238
  match(text) {
@@ -2012,7 +2264,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2012
2264
 
2013
2265
  RULES.push({
2014
2266
  id: "COMPOSITION-AGENT-ADMIN-UI",
2015
- 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.
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.
2016
2268
  scope: 'shared',
2017
2269
  fileTypes: MARKUP_EXT,
2018
2270
  match(text) {
@@ -2045,7 +2297,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2045
2297
 
2046
2298
  RULES.push({
2047
2299
  id: "COMPOSITION-AVATAR-GROUP-UI",
2048
- 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.
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.
2049
2301
  scope: 'shared',
2050
2302
  fileTypes: MARKUP_EXT,
2051
2303
  match(text) {
@@ -2078,7 +2330,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2078
2330
 
2079
2331
  RULES.push({
2080
2332
  id: "COMPOSITION-CHAT-COMPOSER",
2081
- 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.
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.
2082
2334
  scope: 'shared',
2083
2335
  fileTypes: MARKUP_EXT,
2084
2336
  match(text) {
@@ -2110,7 +2362,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2110
2362
 
2111
2363
  RULES.push({
2112
2364
  id: "COMPOSITION-CHAT-EMPTY",
2113
- 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.
2114
2366
  scope: 'shared',
2115
2367
  fileTypes: MARKUP_EXT,
2116
2368
  match(text) {
@@ -2142,7 +2394,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2142
2394
 
2143
2395
  RULES.push({
2144
2396
  id: "COMPOSITION-CHAT-HEADER",
2145
- 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.
2146
2398
  scope: 'shared',
2147
2399
  fileTypes: MARKUP_EXT,
2148
2400
  match(text) {
@@ -2167,14 +2419,14 @@ RULES.push({
2167
2419
 
2168
2420
 
2169
2421
  const TAG = "chat-shell";
2170
- const ALLOWED_PARENTS = ["Surface","agent-admin-ui"];
2422
+ const ALLOWED_PARENTS = ["Surface","agent-admin-ui","pane-ui"];
2171
2423
  const ALLOWED_CHILDREN = [];
2172
2424
  const ALLOWED_CHILDREN_BY_SLOT = null;
2173
2425
 
2174
2426
 
2175
2427
  RULES.push({
2176
2428
  id: "COMPOSITION-CHAT-SHELL",
2177
- 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.
2178
2430
  scope: 'shared',
2179
2431
  fileTypes: MARKUP_EXT,
2180
2432
  match(text) {
@@ -2206,7 +2458,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2206
2458
 
2207
2459
  RULES.push({
2208
2460
  id: "COMPOSITION-CHAT-SIDEBAR",
2209
- 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.
2210
2462
  scope: 'shared',
2211
2463
  fileTypes: MARKUP_EXT,
2212
2464
  match(text) {
@@ -2238,7 +2490,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2238
2490
 
2239
2491
  RULES.push({
2240
2492
  id: "COMPOSITION-CHAT-STATUS",
2241
- 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.
2242
2494
  scope: 'shared',
2243
2495
  fileTypes: MARKUP_EXT,
2244
2496
  match(text) {
@@ -2270,7 +2522,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2270
2522
 
2271
2523
  RULES.push({
2272
2524
  id: "COMPOSITION-CHAT-THREAD",
2273
- 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.
2274
2526
  scope: 'shared',
2275
2527
  fileTypes: MARKUP_EXT,
2276
2528
  match(text) {
@@ -2302,7 +2554,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2302
2554
 
2303
2555
  RULES.push({
2304
2556
  id: "COMPOSITION-CHOICE-CARD-UI",
2305
- 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.
2306
2558
  scope: 'shared',
2307
2559
  fileTypes: MARKUP_EXT,
2308
2560
  match(text) {
@@ -2335,7 +2587,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2335
2587
 
2336
2588
  RULES.push({
2337
2589
  id: "COMPOSITION-CHOICE-UI",
2338
- 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.
2339
2591
  scope: 'shared',
2340
2592
  fileTypes: MARKUP_EXT,
2341
2593
  match(text) {
@@ -2367,7 +2619,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2367
2619
 
2368
2620
  RULES.push({
2369
2621
  id: "COMPOSITION-DRILLDOWN-UI",
2370
- 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.
2371
2623
  scope: 'shared',
2372
2624
  fileTypes: MARKUP_EXT,
2373
2625
  match(text) {
@@ -2400,7 +2652,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2400
2652
 
2401
2653
  RULES.push({
2402
2654
  id: "COMPOSITION-EDITOR-CANVAS-EMPTY",
2403
- 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.
2404
2656
  scope: 'shared',
2405
2657
  fileTypes: MARKUP_EXT,
2406
2658
  match(text) {
@@ -2432,7 +2684,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2432
2684
 
2433
2685
  RULES.push({
2434
2686
  id: "COMPOSITION-EDITOR-CANVAS-TOOLBAR",
2435
- 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.
2436
2688
  scope: 'shared',
2437
2689
  fileTypes: MARKUP_EXT,
2438
2690
  match(text) {
@@ -2464,7 +2716,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2464
2716
 
2465
2717
  RULES.push({
2466
2718
  id: "COMPOSITION-EDITOR-CANVAS",
2467
- 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.
2468
2720
  scope: 'shared',
2469
2721
  fileTypes: MARKUP_EXT,
2470
2722
  match(text) {
@@ -2496,7 +2748,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2496
2748
 
2497
2749
  RULES.push({
2498
2750
  id: "COMPOSITION-EDITOR-SHELL",
2499
- 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.
2500
2752
  scope: 'shared',
2501
2753
  fileTypes: MARKUP_EXT,
2502
2754
  match(text) {
@@ -2528,7 +2780,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2528
2780
 
2529
2781
  RULES.push({
2530
2782
  id: "COMPOSITION-EDITOR-SIDEBAR",
2531
- 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.
2532
2784
  scope: 'shared',
2533
2785
  fileTypes: MARKUP_EXT,
2534
2786
  match(text) {
@@ -2560,7 +2812,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2560
2812
 
2561
2813
  RULES.push({
2562
2814
  id: "COMPOSITION-EDITOR-STATUSBAR",
2563
- 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.
2564
2816
  scope: 'shared',
2565
2817
  fileTypes: MARKUP_EXT,
2566
2818
  match(text) {
@@ -2592,7 +2844,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2592
2844
 
2593
2845
  RULES.push({
2594
2846
  id: "COMPOSITION-EDITOR-TOOLBAR",
2595
- 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.
2596
2848
  scope: 'shared',
2597
2849
  fileTypes: MARKUP_EXT,
2598
2850
  match(text) {
@@ -2617,14 +2869,14 @@ RULES.push({
2617
2869
 
2618
2870
 
2619
2871
  const TAG = "embed-shell";
2620
- const ALLOWED_PARENTS = ["Surface"];
2872
+ const ALLOWED_PARENTS = ["Surface","theme-provider"];
2621
2873
  const ALLOWED_CHILDREN = [];
2622
2874
  const ALLOWED_CHILDREN_BY_SLOT = null;
2623
2875
 
2624
2876
 
2625
2877
  RULES.push({
2626
2878
  id: "COMPOSITION-EMBED-SHELL",
2627
- 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.
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.
2628
2880
  scope: 'shared',
2629
2881
  fileTypes: MARKUP_EXT,
2630
2882
  match(text) {
@@ -2656,7 +2908,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2656
2908
 
2657
2909
  RULES.push({
2658
2910
  id: "COMPOSITION-FEED-ITEM-UI",
2659
- 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.
2660
2912
  scope: 'shared',
2661
2913
  fileTypes: MARKUP_EXT,
2662
2914
  match(text) {
@@ -2688,7 +2940,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2688
2940
 
2689
2941
  RULES.push({
2690
2942
  id: "COMPOSITION-FEED-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.
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.
2692
2944
  scope: 'shared',
2693
2945
  fileTypes: MARKUP_EXT,
2694
2946
  match(text) {
@@ -2721,7 +2973,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2721
2973
 
2722
2974
  RULES.push({
2723
2975
  id: "COMPOSITION-FIELDS-UI",
2724
- 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.
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.
2725
2977
  scope: 'shared',
2726
2978
  fileTypes: MARKUP_EXT,
2727
2979
  match(text) {
@@ -2754,7 +3006,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2754
3006
 
2755
3007
  RULES.push({
2756
3008
  id: "COMPOSITION-FOOTER-UI",
2757
- 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.
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.
2758
3010
  scope: 'shared',
2759
3011
  fileTypes: MARKUP_EXT,
2760
3012
  match(text) {
@@ -2786,7 +3038,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2786
3038
 
2787
3039
  RULES.push({
2788
3040
  id: "COMPOSITION-FORM-POPOVER-UI",
2789
- 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.
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.
2790
3042
  scope: 'shared',
2791
3043
  fileTypes: MARKUP_EXT,
2792
3044
  match(text) {
@@ -2819,7 +3071,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2819
3071
 
2820
3072
  RULES.push({
2821
3073
  id: "COMPOSITION-HEADER-UI",
2822
- 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.
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.
2823
3075
  scope: 'shared',
2824
3076
  fileTypes: MARKUP_EXT,
2825
3077
  match(text) {
@@ -2851,7 +3103,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2851
3103
 
2852
3104
  RULES.push({
2853
3105
  id: "COMPOSITION-LIST-ITEM-UI",
2854
- 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.
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.
2855
3107
  scope: 'shared',
2856
3108
  fileTypes: MARKUP_EXT,
2857
3109
  match(text) {
@@ -2883,7 +3135,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2883
3135
 
2884
3136
  RULES.push({
2885
3137
  id: "COMPOSITION-MENU-DIVIDER-UI",
2886
- 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.
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.
2887
3139
  scope: 'shared',
2888
3140
  fileTypes: MARKUP_EXT,
2889
3141
  match(text) {
@@ -2915,7 +3167,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2915
3167
 
2916
3168
  RULES.push({
2917
3169
  id: "COMPOSITION-MENU-ITEM-UI",
2918
- 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.
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.
2919
3171
  scope: 'shared',
2920
3172
  fileTypes: MARKUP_EXT,
2921
3173
  match(text) {
@@ -2947,7 +3199,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2947
3199
 
2948
3200
  RULES.push({
2949
3201
  id: "COMPOSITION-MENU-LABEL-UI",
2950
- 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.
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.
2951
3203
  scope: 'shared',
2952
3204
  fileTypes: MARKUP_EXT,
2953
3205
  match(text) {
@@ -2979,7 +3231,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
2979
3231
 
2980
3232
  RULES.push({
2981
3233
  id: "COMPOSITION-MENU-UI",
2982
- 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.
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.
2983
3235
  scope: 'shared',
2984
3236
  fileTypes: MARKUP_EXT,
2985
3237
  match(text) {
@@ -3012,7 +3264,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3012
3264
 
3013
3265
  RULES.push({
3014
3266
  id: "COMPOSITION-NAV-GROUP-UI",
3015
- 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.
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.
3016
3268
  scope: 'shared',
3017
3269
  fileTypes: MARKUP_EXT,
3018
3270
  match(text) {
@@ -3047,7 +3299,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3047
3299
 
3048
3300
  RULES.push({
3049
3301
  id: "COMPOSITION-NAV-ITEM-UI",
3050
- 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.
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.
3051
3303
  scope: 'shared',
3052
3304
  fileTypes: MARKUP_EXT,
3053
3305
  match(text) {
@@ -3079,7 +3331,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3079
3331
 
3080
3332
  RULES.push({
3081
3333
  id: "COMPOSITION-PAGE-SCROLL",
3082
- 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.
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.
3083
3335
  scope: 'shared',
3084
3336
  fileTypes: MARKUP_EXT,
3085
3337
  match(text) {
@@ -3111,7 +3363,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3111
3363
 
3112
3364
  RULES.push({
3113
3365
  id: "COMPOSITION-RADIO-GROUP-UI",
3114
- 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.
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.
3115
3367
  scope: 'shared',
3116
3368
  fileTypes: MARKUP_EXT,
3117
3369
  match(text) {
@@ -3144,7 +3396,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3144
3396
 
3145
3397
  RULES.push({
3146
3398
  id: "COMPOSITION-SEARCH-UI",
3147
- 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.
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.
3148
3400
  scope: 'shared',
3149
3401
  fileTypes: MARKUP_EXT,
3150
3402
  match(text) {
@@ -3177,7 +3429,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3177
3429
 
3178
3430
  RULES.push({
3179
3431
  id: "COMPOSITION-SECTION-UI",
3180
- 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.
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.
3181
3433
  scope: 'shared',
3182
3434
  fileTypes: MARKUP_EXT,
3183
3435
  match(text) {
@@ -3209,7 +3461,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3209
3461
 
3210
3462
  RULES.push({
3211
3463
  id: "COMPOSITION-SEGMENT-UI",
3212
- 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.
3213
3465
  scope: 'shared',
3214
3466
  fileTypes: MARKUP_EXT,
3215
3467
  match(text) {
@@ -3241,7 +3493,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3241
3493
 
3242
3494
  RULES.push({
3243
3495
  id: "COMPOSITION-SEGMENTED-UI",
3244
- 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.
3245
3497
  scope: 'shared',
3246
3498
  fileTypes: MARKUP_EXT,
3247
3499
  match(text) {
@@ -3274,7 +3526,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3274
3526
 
3275
3527
  RULES.push({
3276
3528
  id: "COMPOSITION-SIMPLE-CONTENT",
3277
- 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.
3278
3530
  scope: 'shared',
3279
3531
  fileTypes: MARKUP_EXT,
3280
3532
  match(text) {
@@ -3306,7 +3558,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3306
3558
 
3307
3559
  RULES.push({
3308
3560
  id: "COMPOSITION-SIMPLE-HERO",
3309
- 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.
3310
3562
  scope: 'shared',
3311
3563
  fileTypes: MARKUP_EXT,
3312
3564
  match(text) {
@@ -3338,7 +3590,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3338
3590
 
3339
3591
  RULES.push({
3340
3592
  id: "COMPOSITION-SIMPLE-SHELL",
3341
- 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.
3342
3594
  scope: 'shared',
3343
3595
  fileTypes: MARKUP_EXT,
3344
3596
  match(text) {
@@ -3370,7 +3622,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3370
3622
 
3371
3623
  RULES.push({
3372
3624
  id: "COMPOSITION-SKIP-NAV-UI",
3373
- 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.
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.
3374
3626
  scope: 'shared',
3375
3627
  fileTypes: MARKUP_EXT,
3376
3628
  match(text) {
@@ -3402,7 +3654,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3402
3654
 
3403
3655
  RULES.push({
3404
3656
  id: "COMPOSITION-STEPPER-ITEM-UI",
3405
- 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.
3406
3658
  scope: 'shared',
3407
3659
  fileTypes: MARKUP_EXT,
3408
3660
  match(text) {
@@ -3434,7 +3686,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3434
3686
 
3435
3687
  RULES.push({
3436
3688
  id: "COMPOSITION-STEPPER-UI",
3437
- 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.
3438
3690
  scope: 'shared',
3439
3691
  fileTypes: MARKUP_EXT,
3440
3692
  match(text) {
@@ -3467,7 +3719,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3467
3719
 
3468
3720
  RULES.push({
3469
3721
  id: "COMPOSITION-TAB-UI",
3470
- 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.
3471
3723
  scope: 'shared',
3472
3724
  fileTypes: MARKUP_EXT,
3473
3725
  match(text) {
@@ -3499,7 +3751,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3499
3751
 
3500
3752
  RULES.push({
3501
3753
  id: "COMPOSITION-TABS-UI",
3502
- 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.
3503
3755
  scope: 'shared',
3504
3756
  fileTypes: MARKUP_EXT,
3505
3757
  match(text) {
@@ -3532,7 +3784,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3532
3784
 
3533
3785
  RULES.push({
3534
3786
  id: "COMPOSITION-TIMELINE-ITEM-UI",
3535
- 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.
3536
3788
  scope: 'shared',
3537
3789
  fileTypes: MARKUP_EXT,
3538
3790
  match(text) {
@@ -3564,7 +3816,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3564
3816
 
3565
3817
  RULES.push({
3566
3818
  id: "COMPOSITION-TIMELINE-UI",
3567
- 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.
3568
3820
  scope: 'shared',
3569
3821
  fileTypes: MARKUP_EXT,
3570
3822
  match(text) {
@@ -3597,7 +3849,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3597
3849
 
3598
3850
  RULES.push({
3599
3851
  id: "COMPOSITION-TOOLBAR-GROUP-UI",
3600
- 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.
3601
3853
  scope: 'shared',
3602
3854
  fileTypes: MARKUP_EXT,
3603
3855
  match(text) {
@@ -3629,7 +3881,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3629
3881
 
3630
3882
  RULES.push({
3631
3883
  id: "COMPOSITION-TOUR-STEP-UI",
3632
- 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.
3633
3885
  scope: 'shared',
3634
3886
  fileTypes: MARKUP_EXT,
3635
3887
  match(text) {
@@ -3661,7 +3913,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3661
3913
 
3662
3914
  RULES.push({
3663
3915
  id: "COMPOSITION-TOUR-UI",
3664
- 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.
3665
3917
  scope: 'shared',
3666
3918
  fileTypes: MARKUP_EXT,
3667
3919
  match(text) {
@@ -3694,7 +3946,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3694
3946
 
3695
3947
  RULES.push({
3696
3948
  id: "COMPOSITION-TREE-ITEM-UI",
3697
- 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.
3698
3950
  scope: 'shared',
3699
3951
  fileTypes: MARKUP_EXT,
3700
3952
  match(text) {
@@ -3726,7 +3978,7 @@ const ALLOWED_CHILDREN_BY_SLOT = null;
3726
3978
 
3727
3979
  RULES.push({
3728
3980
  id: "COMPOSITION-TREE-UI",
3729
- 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.
3730
3982
  scope: 'shared',
3731
3983
  fileTypes: MARKUP_EXT,
3732
3984
  match(text) {
@@ -3977,10 +4229,6 @@ const MANIFEST_SNAPSHOT = {
3977
4229
  }
3978
4230
  ]
3979
4231
  },
3980
- {
3981
- "tag": "admin-scroll",
3982
- "attrs": []
3983
- },
3984
4232
  {
3985
4233
  "tag": "admin-settings-ui",
3986
4234
  "attrs": [
@@ -9180,6 +9428,10 @@ const MANIFEST_SNAPSHOT = {
9180
9428
  {
9181
9429
  "value": "dot-grid",
9182
9430
  "tier": "default"
9431
+ },
9432
+ {
9433
+ "value": "noise",
9434
+ "tier": "alternative"
9183
9435
  }
9184
9436
  ]
9185
9437
  }
@@ -11374,10 +11626,6 @@ const MANIFEST_SNAPSHOT = {
11374
11626
  }
11375
11627
  ]
11376
11628
  },
11377
- {
11378
- "tag": "admin-scroll",
11379
- "attrs": []
11380
- },
11381
11629
  {
11382
11630
  "tag": "admin-settings-ui",
11383
11631
  "attrs": [
@@ -16577,6 +16825,10 @@ const MANIFEST_SNAPSHOT = {
16577
16825
  {
16578
16826
  "value": "dot-grid",
16579
16827
  "tier": "default"
16828
+ },
16829
+ {
16830
+ "value": "noise",
16831
+ "tier": "alternative"
16580
16832
  }
16581
16833
  ]
16582
16834
  }
@@ -18767,10 +19019,6 @@ const MANIFEST_SNAPSHOT = {
18767
19019
  }
18768
19020
  ]
18769
19021
  },
18770
- {
18771
- "tag": "admin-scroll",
18772
- "attrs": []
18773
- },
18774
19022
  {
18775
19023
  "tag": "admin-settings-ui",
18776
19024
  "attrs": [
@@ -23970,6 +24218,10 @@ const MANIFEST_SNAPSHOT = {
23970
24218
  {
23971
24219
  "value": "dot-grid",
23972
24220
  "tier": "default"
24221
+ },
24222
+ {
24223
+ "value": "noise",
24224
+ "tier": "alternative"
23973
24225
  }
23974
24226
  ]
23975
24227
  }
@@ -26082,10 +26334,6 @@ const MANIFEST_SNAPSHOT = {
26082
26334
  }
26083
26335
  ]
26084
26336
  },
26085
- {
26086
- "tag": "admin-scroll",
26087
- "attrs": []
26088
- },
26089
26337
  {
26090
26338
  "tag": "admin-settings-ui",
26091
26339
  "attrs": [
@@ -30572,10 +30820,6 @@ const MANIFEST_SNAPSHOT = {
30572
30820
  "tag": "admin-roster-ui",
30573
30821
  "attrs": []
30574
30822
  },
30575
- {
30576
- "tag": "admin-scroll",
30577
- "attrs": []
30578
- },
30579
30823
  {
30580
30824
  "tag": "admin-settings-ui",
30581
30825
  "attrs": []
@@ -32176,10 +32420,30 @@ RULES.push({
32176
32420
  // literal URL, so this hasn't triggered in practice; a literal `//` URL
32177
32421
  // sharing a line with the dimension attributes is the one shape to watch
32178
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).
32179
32442
  const IMG_TAG_RE = /<img\b[^>]*?>/gis;
32180
32443
  const HAS_WIDTH = /\bwidth\s*=/i;
32181
32444
  const HAS_HEIGHT = /\bheight\s*=/i;
32182
32445
  const HAS_ASPECT_RATIO = /aspect-ratio\s*:/i;
32446
+ const HAS_ANY_ATTR = /\S=/;
32183
32447
  const CORPUS_PATH_RE = /packages[\\/](web-components|web-modules)[\\/]/;
32184
32448
 
32185
32449
 
@@ -32203,6 +32467,7 @@ RULES.push({
32203
32467
  let m;
32204
32468
  while ((m = IMG_TAG_RE.exec(blanked))) {
32205
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
32206
32471
  if ((HAS_WIDTH.test(tag) && HAS_HEIGHT.test(tag)) || HAS_ASPECT_RATIO.test(tag)) continue;
32207
32472
  const lineNo = lineOf(blanked, m.index);
32208
32473
  out.push({
@@ -32427,6 +32692,39 @@ function optInTagsFor(scopes) {
32427
32692
  return ['forge-lint', 'adia-lint'];
32428
32693
  }
32429
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
+
32430
32728
 
32431
32729
  RULES.push({
32432
32730
  id: 'RAW-COLOR',
@@ -32442,8 +32740,8 @@ RULES.push({
32442
32740
  for (const decl of stripped.split(';')) {
32443
32741
  const d = decl.trim();
32444
32742
  if (!d || d.startsWith('//') || d.startsWith('/*') || d.startsWith('*')) continue;
32445
- if (decl.includes('var(') || decl.includes('light-dark(')) continue;
32446
- 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)) {
32447
32745
  out.push({
32448
32746
  line: lineNo,
32449
32747
  snippet: raw.trim().slice(0, 90),
@@ -32508,20 +32806,48 @@ RULES.push({
32508
32806
 
32509
32807
 
32510
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
+
32511
32827
  RULES.push({
32512
32828
  id: 'SCOPE-EXTENT',
32513
32829
  severity: 'error',
32514
32830
  scope: 'shared',
32515
32831
  fileTypes: STYLE_EXT,
32516
- match(text) {
32832
+ match(text, path) {
32517
32833
  const out = [];
32518
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)];
32519
32839
  let m;
32520
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);
32521
32845
  out.push({
32522
32846
  line: lineOf(text, m.index),
32523
32847
  snippet: ':scope { … width/height … }',
32524
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' } : {}),
32525
32851
  });
32526
32852
  }
32527
32853
  return out;