@markuplint/ml-core 5.0.0-rc.5 → 5.0.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,39 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-rc.7](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.6...v5.0.0-rc.7) (2026-08-31)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **ml-core:** fix named rule group disable propagation and validation ([139f466](https://github.com/markuplint/markuplint/commit/139f4661a12ac10b15ade305c30bbd8234b07b0f))
11
+
12
+ # [5.0.0-rc.6](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.5...v5.0.0-rc.6) (2026-08-30)
13
+
14
+ ### Features
15
+
16
+ - split rule-deprecation notices out of config-error ([#4013](https://github.com/markuplint/markuplint/issues/4013)) ([812e6f3](https://github.com/markuplint/markuplint/commit/812e6f356839af8f257cfd91e6b16cfdfdd7cf33))
17
+
18
+ ### BREAKING CHANGES
19
+
20
+ - violations for deprecated rule names now have
21
+ `ruleId: 'rule-deprecation'` instead of `ruleId: 'config-error'`. Any
22
+ consumer filtering `MLCore.verify()` output (or the markuplint CLI/API) by
23
+ `ruleId === 'config-error'` to catch deprecation messages must also check
24
+ for `rule-deprecation`.
25
+
26
+ - feat(markuplint): add --severity-deprecation CLI flag
27
+
28
+ Wires the new severity.deprecation config option (@markuplint/ml-config)
29
+ and the rule-deprecation ruleId (@markuplint/ml-core) through the CLI:
30
+
31
+ - --severity-deprecation flag, mirroring --severity-parse-error
32
+ - --show-config details now also surfaces ruleDeprecations
33
+ - per-run dedupe and failed-file counting generalized to cover both
34
+ config-level ruleIds (config-error and rule-deprecation), not just
35
+ config-error
36
+
37
+ * docs(website): document severity.deprecation (EN + JA)
38
+
6
39
  # [5.0.0-rc.5](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.4...v5.0.0-rc.5) (2026-08-28)
7
40
 
8
41
  ### Bug Fixes
package/lib/ml-core.d.ts CHANGED
@@ -2,6 +2,20 @@ import type { MLFabric } from './types.js';
2
2
  import type { PlainData, RuleConfigValue, TextEdit, Violation } from '@markuplint/ml-config';
3
3
  import { ParserError } from '@markuplint/parser-utils';
4
4
  import { Document } from './ml-dom/index.js';
5
+ /**
6
+ * `ruleId` of a genuinely broken config (unresolved rule reference, plugin
7
+ * resolution failure, ...). Exported so consumers that need to recognize
8
+ * config-level violations (e.g. the CLI's per-run dedupe and failed-file
9
+ * counting in `packages/markuplint/src/cli/command.ts`) reference the same
10
+ * literal `MLCore.verify()` emits, instead of duplicating the string.
11
+ */
12
+ export declare const CONFIG_ERROR_RULE_ID = "config-error";
13
+ /**
14
+ * `ruleId` of a deprecated-but-working rule name notice — see
15
+ * {@link CONFIG_ERROR_RULE_ID} for why this is exported rather than a
16
+ * private literal.
17
+ */
18
+ export declare const RULE_DEPRECATION_RULE_ID = "rule-deprecation";
5
19
  /**
6
20
  * Summary of the multi-pass fix process.
7
21
  */
@@ -74,7 +88,7 @@ export type MLCoreParams = {
74
88
  */
75
89
  export declare class MLCore {
76
90
  #private;
77
- constructor({ parser, sourceCode, ruleset, rules, locale, schemas, ruleCommonSettings, parserOptions, severity, pretenders, filename, debug, configErrors, }: MLCoreParams);
91
+ constructor({ parser, sourceCode, ruleset, rules, locale, schemas, ruleCommonSettings, parserOptions, severity, pretenders, filename, debug, configErrors, ruleDeprecations, }: MLCoreParams);
78
92
  /**
79
93
  * The parsed document, or a {@link ParserError} if parsing failed.
80
94
  */
@@ -91,7 +105,7 @@ export declare class MLCore {
91
105
  *
92
106
  * @param fabric - Partial fabric with the properties to update
93
107
  */
94
- update({ parser, ruleset, rules, locale, schemas, parserOptions, pretenders, configErrors }: Partial<MLFabric>): void;
108
+ update({ parser, ruleset, rules, locale, schemas, parserOptions, pretenders, configErrors, ruleDeprecations, }: Partial<MLFabric>): void;
95
109
  /**
96
110
  * Runs all configured rules against the parsed document and returns violations.
97
111
  *
package/lib/ml-core.js CHANGED
@@ -4,6 +4,20 @@ import { applyFixes } from './fix-applier.js';
4
4
  import { Document } from './ml-dom/index.js';
5
5
  import { expandNamedNodeRules, expandNamedRules } from './virtual-rule.js';
6
6
  const resultLog = log.extend('result');
7
+ /**
8
+ * `ruleId` of a genuinely broken config (unresolved rule reference, plugin
9
+ * resolution failure, ...). Exported so consumers that need to recognize
10
+ * config-level violations (e.g. the CLI's per-run dedupe and failed-file
11
+ * counting in `packages/markuplint/src/cli/command.ts`) reference the same
12
+ * literal `MLCore.verify()` emits, instead of duplicating the string.
13
+ */
14
+ export const CONFIG_ERROR_RULE_ID = 'config-error';
15
+ /**
16
+ * `ruleId` of a deprecated-but-working rule name notice — see
17
+ * {@link CONFIG_ERROR_RULE_ID} for why this is exported rather than a
18
+ * private literal.
19
+ */
20
+ export const RULE_DEPRECATION_RULE_ID = 'rule-deprecation';
7
21
  /**
8
22
  * The core linting engine for markuplint.
9
23
  *
@@ -29,6 +43,13 @@ export class MLCore {
29
43
  * Set once per construction/`update()` and never mutated by re-parsing.
30
44
  */
31
45
  #configErrors;
46
+ /**
47
+ * Deprecated-rule-name notices, kept structured and separate from
48
+ * `#configErrors` so `verify()` can report them under their own
49
+ * `rule-deprecation` ruleId. Set once per construction/`update()`, same
50
+ * lifecycle as `#configErrors`.
51
+ */
52
+ #ruleDeprecations;
32
53
  /**
33
54
  * Rule-mapping errors for the CURRENT document. Reset (not accumulated) on
34
55
  * every `#createDocument()` so repeated `setCode()` calls don't duplicate
@@ -44,7 +65,7 @@ export class MLCore {
44
65
  #originalNodeRules;
45
66
  #originalChildNodeRules;
46
67
  #disabledNamespaces;
47
- constructor({ parser, sourceCode, ruleset, rules, locale, schemas, ruleCommonSettings, parserOptions, severity, pretenders, filename, debug, configErrors, }) {
68
+ constructor({ parser, sourceCode, ruleset, rules, locale, schemas, ruleCommonSettings, parserOptions, severity, pretenders, filename, debug, configErrors, ruleDeprecations, }) {
48
69
  if (debug) {
49
70
  enableDebug();
50
71
  }
@@ -58,6 +79,7 @@ export class MLCore {
58
79
  this.#severity = severity;
59
80
  this.#pretenders = [...pretenders];
60
81
  this.#configErrors = [...(configErrors ?? [])];
82
+ this.#ruleDeprecations = [...(ruleDeprecations ?? [])];
61
83
  // Preserve pre-expansion nodeRules for hot-reload
62
84
  this.#originalNodeRules = ruleset.nodeRules ?? [];
63
85
  this.#originalChildNodeRules = ruleset.childNodeRules ?? [];
@@ -72,6 +94,11 @@ export class MLCore {
72
94
  nodeRules: nodeRuleResult.transformedNodeRules,
73
95
  childNodeRules: childNodeRuleResult.transformedNodeRules,
74
96
  baseRuleToVirtualNames: buildBaseRuleToVirtualNames(namedRulesResult.virtualRules),
97
+ baseRuleToScopedVirtualNames: buildBaseRuleToVirtualNames([
98
+ ...nodeRuleResult.virtualRules,
99
+ ...childNodeRuleResult.virtualRules,
100
+ ]),
101
+ knownNamedRuleGroupKeys: ruleset.knownNamedRuleGroupKeys ?? new Set(),
75
102
  mappingErrors: [],
76
103
  };
77
104
  this.#disabledNamespaces = extractDisabledNamespaces(resolvedRules);
@@ -101,12 +128,13 @@ export class MLCore {
101
128
  *
102
129
  * @param fabric - Partial fabric with the properties to update
103
130
  */
104
- update({ parser, ruleset, rules, locale, schemas, parserOptions, pretenders, configErrors }) {
131
+ update({ parser, ruleset, rules, locale, schemas, parserOptions, pretenders, configErrors, ruleDeprecations, }) {
105
132
  this.#parser = parser ?? this.#parser;
106
133
  this.#locale = locale ?? this.#locale;
107
134
  this.#schemas = schemas ?? this.#schemas;
108
135
  this.#pretenders = pretenders ? [...pretenders] : this.#pretenders;
109
136
  this.#configErrors = [...(configErrors ?? [])];
137
+ this.#ruleDeprecations = [...(ruleDeprecations ?? [])];
110
138
  const baseRules = rules ? [...rules] : this.#rules.filter(r => !r.baseRuleId);
111
139
  const incomingNodeRules = ruleset?.nodeRules ?? this.#originalNodeRules;
112
140
  const incomingChildNodeRules = ruleset?.childNodeRules ?? this.#originalChildNodeRules;
@@ -128,6 +156,11 @@ export class MLCore {
128
156
  nodeRules: nodeRuleResult.transformedNodeRules,
129
157
  childNodeRules: childNodeRuleResult.transformedNodeRules,
130
158
  baseRuleToVirtualNames: buildBaseRuleToVirtualNames(namedRulesResult.virtualRules),
159
+ baseRuleToScopedVirtualNames: buildBaseRuleToVirtualNames([
160
+ ...nodeRuleResult.virtualRules,
161
+ ...childNodeRuleResult.virtualRules,
162
+ ]),
163
+ knownNamedRuleGroupKeys: ruleset?.knownNamedRuleGroupKeys ?? this.#ruleset.knownNamedRuleGroupKeys,
131
164
  mappingErrors: [],
132
165
  };
133
166
  this.#disabledNamespaces = extractDisabledNamespaces(resolvedRules);
@@ -168,9 +201,18 @@ export class MLCore {
168
201
  if (setRuleName.endsWith('/*')) {
169
202
  continue;
170
203
  }
204
+ // Skip a named rule group disabled via top-level `rules` (e.g. "html-standard/foo": false).
205
+ // `expandNamedRules` intentionally doesn't instantiate a virtual rule when the merged value
206
+ // already resolved to `false` (there's nothing to run), so it never appears in `definedRuleName`.
207
+ // `knownNamedRuleGroupKeys` (threaded from config merge — see its JSDoc) tells a genuine
208
+ // named-group disable apart from a typo'd/nonexistent rule name that also happens to be
209
+ // set to `false` with a `/` in its key; only the former is exempt from this check.
210
+ if (this.#ruleset.rules[setRuleName] === false && this.#ruleset.knownNamedRuleGroupKeys.has(setRuleName)) {
211
+ continue;
212
+ }
171
213
  if (!definedRuleName.has(setRuleName)) {
172
214
  configViolations.push({
173
- ruleId: 'config-error',
215
+ ruleId: CONFIG_ERROR_RULE_ID,
174
216
  severity: 'warning',
175
217
  message: `Rule not found: ${setRuleName}`,
176
218
  col: 1,
@@ -181,7 +223,7 @@ export class MLCore {
181
223
  }
182
224
  for (const error of [...this.#configErrors, ...this.#mappingErrors]) {
183
225
  configViolations.push({
184
- ruleId: 'config-error',
226
+ ruleId: CONFIG_ERROR_RULE_ID,
185
227
  severity: 'warning',
186
228
  message: error.message,
187
229
  col: 1,
@@ -189,6 +231,19 @@ export class MLCore {
189
231
  raw: '',
190
232
  });
191
233
  }
234
+ const deprecationSeverity = this.#resolveDeprecationSeverity();
235
+ if (deprecationSeverity != null) {
236
+ for (const { deprecatedName, replacedBy } of this.#ruleDeprecations) {
237
+ configViolations.push({
238
+ ruleId: RULE_DEPRECATION_RULE_ID,
239
+ severity: deprecationSeverity,
240
+ message: `Rule "${deprecatedName}" is deprecated and will be removed in v6. Use ${replacedBy.join(', ')} instead.`,
241
+ col: 1,
242
+ line: 1,
243
+ raw: '',
244
+ });
245
+ }
246
+ }
192
247
  violations.push(...configViolations);
193
248
  const ruleViolations = await this.#runAllRules(fix);
194
249
  violations.push(...ruleViolations);
@@ -363,6 +418,22 @@ export class MLCore {
363
418
  }
364
419
  }
365
420
  }
421
+ /**
422
+ * Resolves `severity.deprecation`, honouring its single-value form only
423
+ * (unlike `severity.parseError`, there's no fixed enum of deprecated rule
424
+ * names to key a per-code `Record` on).
425
+ *
426
+ * Unlike `#createParseError`'s parse-error channel, this defaults to
427
+ * `'warning'` (not off/suppressed) when unset — the deprecation channel
428
+ * is being carved out of the always-on `config-error` channel, not
429
+ * introduced cold, so leaving the option unset must not silence a notice
430
+ * users already see today.
431
+ *
432
+ * @returns the resolved severity, or `null` if suppressed.
433
+ */
434
+ #resolveDeprecationSeverity() {
435
+ return resolveUniformSeverity(this.#severity.deprecation, 'warning');
436
+ }
366
437
  /**
367
438
  * Builds a `ruleId: 'parse-error'` violation, honouring
368
439
  * `severity.parseError`.
@@ -377,36 +448,18 @@ export class MLCore {
377
448
  */
378
449
  #createParseError(message, line, col, raw, code) {
379
450
  const cfg = this.#severity.parseError;
380
- if (cfg === false || cfg === 'off') {
451
+ // Fatal ParserErrors (no `code`) can't be targeted by the per-code
452
+ // Record form, so they fall back to `'error'` there; under the
453
+ // uniform form they default to `'error'` too, while non-fatal entries
454
+ // default to suppressed when unset — see `resolveUniformSeverity`.
455
+ const severity = typeof cfg === 'object'
456
+ ? code == null
457
+ ? 'error'
458
+ : resolveUniformSeverity(cfg[code], null)
459
+ : resolveUniformSeverity(cfg, code == null ? 'error' : null);
460
+ if (severity == null) {
381
461
  return null;
382
462
  }
383
- let severity;
384
- if (typeof cfg === 'object') {
385
- if (code == null) {
386
- // Fatal ParserError without a code; the Record form cannot target
387
- // it, so fall back to `'error'` (the channel is otherwise enabled).
388
- severity = 'error';
389
- }
390
- else {
391
- const perCode = cfg[code];
392
- if (perCode == null || perCode === false || perCode === 'off') {
393
- return null;
394
- }
395
- severity = perCode === true ? 'error' : perCode;
396
- }
397
- }
398
- else if (cfg == null) {
399
- // New default: non-fatal `parseErrors` entries are off; fatal
400
- // `ParserError`s (no code) still emit at `'error'`.
401
- if (code != null) {
402
- return null;
403
- }
404
- severity = 'error';
405
- }
406
- else {
407
- // Uniform string/boolean form (legacy).
408
- severity = cfg === true ? 'error' : cfg;
409
- }
410
463
  return {
411
464
  ruleId: 'parse-error',
412
465
  severity,
@@ -597,6 +650,24 @@ export class MLCore {
597
650
  }
598
651
  }
599
652
  }
653
+ /**
654
+ * Normalizes a single-value `Severity | 'off' | boolean | undefined` option
655
+ * (the shape `severity.deprecation` and `severity.parseError`'s uniform/
656
+ * per-code leaf values share) to a resolved severity or `null` (suppressed).
657
+ *
658
+ * Shared so the same three-way rule — `false`/`'off'` → suppressed, `true` →
659
+ * `'error'`, unset → `defaultWhenUnset` — isn't reimplemented at each call
660
+ * site as channels using this shape are added.
661
+ */
662
+ function resolveUniformSeverity(cfg, defaultWhenUnset) {
663
+ if (cfg === false || cfg === 'off') {
664
+ return null;
665
+ }
666
+ if (cfg == null) {
667
+ return defaultWhenUnset;
668
+ }
669
+ return cfg === true ? 'error' : cfg;
670
+ }
600
671
  function extractDisabledNamespaces(rules) {
601
672
  return Object.entries(rules)
602
673
  .filter(([key, value]) => key.endsWith('/*') && value === false)
@@ -2440,7 +2440,9 @@ export class MLDocument extends MLParentNode {
2440
2440
  specificity: matches.specificity,
2441
2441
  rule: mergedRule,
2442
2442
  });
2443
- // Propagate to virtual rules that wrap this base rule
2443
+ // Propagate to top-level-group virtual rules that wrap this base rule. These
2444
+ // wrappers have no selector scope of their own (they mirror the base rule
2445
+ // globally), so any matched value is safe to forward.
2444
2446
  const virtualNames = ruleset.baseRuleToVirtualNames.get(ruleName);
2445
2447
  if (virtualNames) {
2446
2448
  for (const vName of virtualNames) {
@@ -2453,6 +2455,26 @@ export class MLDocument extends MLParentNode {
2453
2455
  });
2454
2456
  }
2455
2457
  }
2458
+ // Propagate a disable to selector-scoped (nodeRules/childNodeRules-named) virtual
2459
+ // rules that wrap this base rule. Limited to `false`: unlike the top-level-group
2460
+ // case above, these wrappers DO have their own selector scope, so forwarding a
2461
+ // non-false value would apply their semantics (e.g. a required attribute name) to
2462
+ // every node this (unrelated) nodeRule matches, even where the virtual rule's own
2463
+ // selector never matched — see issue #4023's fix history for the regression this caused.
2464
+ if (convertedRule === false) {
2465
+ const scopedVirtualNames = ruleset.baseRuleToScopedVirtualNames.get(ruleName);
2466
+ if (scopedVirtualNames) {
2467
+ for (const vName of scopedVirtualNames) {
2468
+ // No merge needed: mergeRule(_, false) always returns false, so the
2469
+ // disable is unconditional regardless of any global config for vName.
2470
+ ruleMapper.set(node, vName, {
2471
+ from: 'nodeRules',
2472
+ specificity: matches.specificity,
2473
+ rule: false,
2474
+ });
2475
+ }
2476
+ }
2477
+ }
2456
2478
  }
2457
2479
  }
2458
2480
  // overwrite rule to child node
@@ -2518,7 +2540,8 @@ export class MLDocument extends MLParentNode {
2518
2540
  rule: mergedRule,
2519
2541
  });
2520
2542
  }
2521
- // Propagate to virtual rules that wrap this base rule
2543
+ // Propagate to top-level-group virtual rules see the matching nodeRules
2544
+ // propagation above for why any value is safe to forward here.
2522
2545
  const virtualNames = ruleset.baseRuleToVirtualNames.get(ruleName);
2523
2546
  if (virtualNames) {
2524
2547
  for (const vName of virtualNames) {
@@ -2533,6 +2556,25 @@ export class MLDocument extends MLParentNode {
2533
2556
  }
2534
2557
  }
2535
2558
  }
2559
+ // Propagate a disable to selector-scoped virtual rules. Limited to `false` —
2560
+ // see the matching nodeRules propagation above for why a non-false value must
2561
+ // not be forwarded to a virtual rule whose own selector didn't match.
2562
+ if (convertedRule === false) {
2563
+ const scopedVirtualNames = ruleset.baseRuleToScopedVirtualNames.get(ruleName);
2564
+ if (scopedVirtualNames) {
2565
+ for (const vName of scopedVirtualNames) {
2566
+ // No merge needed: mergeRule(_, false) always returns false, so the
2567
+ // disable is unconditional regardless of any global config for vName.
2568
+ for (const descendant of targetDescendants) {
2569
+ ruleMapper.set(descendant, vName, {
2570
+ from: 'childNodeRules',
2571
+ specificity: matches.specificity,
2572
+ rule: false,
2573
+ });
2574
+ }
2575
+ }
2576
+ }
2577
+ }
2536
2578
  }
2537
2579
  }
2538
2580
  }
@@ -5,14 +5,31 @@ import type { ChildNodeRule, Config, NodeRule, Rules } from '@markuplint/ml-conf
5
5
  */
6
6
  export declare class Ruleset {
7
7
  /**
8
- * Maps base rule names to their virtual rule names created by NamedRuleGroups.
9
- * For example, if `a11y/landmark-roles` wraps `no-nested-top-level-landmark`, this maps
10
- * `"no-nested-top-level-landmark"` `["a11y/landmark-roles"]`.
11
- * Used by nodeRules/childNodeRules to propagate settings to virtual rules.
8
+ * Maps base rule names to the virtual rule names created by top-level `rules`
9
+ * NamedRuleGroups (e.g. `"a11y/landmark-roles": { rules: { "no-nested-top-level-landmark": true } }`).
10
+ * These wrappers have no selector scope of their own — they mirror the base rule
11
+ * globally so nodeRules/childNodeRules may propagate *any* matched value to them,
12
+ * not just a disable.
12
13
  */
13
14
  readonly baseRuleToVirtualNames: ReadonlyMap<string, readonly string[]>;
15
+ /**
16
+ * Maps base rule names to the virtual rule names created by `nodeRules[].name` /
17
+ * `childNodeRules[].name` (selector-scoped named groups), as opposed to
18
+ * {@link baseRuleToVirtualNames}'s top-level `rules` groups. Because these wrappers
19
+ * DO have their own selector scope, only a disable (`false`) may be propagated to
20
+ * them from an unrelated nodeRules/childNodeRules entry — forwarding a non-false
21
+ * value would apply the wrapper's semantics to nodes its own selector never matched
22
+ * (see issue #4023's fix history).
23
+ */
24
+ readonly baseRuleToScopedVirtualNames: ReadonlyMap<string, readonly string[]>;
14
25
  /** Rule overrides that apply to child nodes matching specific selectors */
15
26
  readonly childNodeRules: readonly ChildNodeRule[];
27
+ /**
28
+ * `rules` keys that are a genuine NamedRuleGroup in some layer of the
29
+ * `extends`/override chain, carried over from {@link Config.knownNamedRuleGroupKeys}
30
+ * even where the final merged value collapsed to `false`. See that field's JSDoc.
31
+ */
32
+ readonly knownNamedRuleGroupKeys: ReadonlySet<string>;
16
33
  /**
17
34
  * Errors collected during rule mapping (e.g., invalid wildcard usage).
18
35
  * Consumed by MLCore and reported as config-error violations.
@@ -24,7 +41,8 @@ export declare class Ruleset {
24
41
  readonly rules: Rules;
25
42
  /**
26
43
  * @param config - The markuplint configuration to extract rules from
27
- * @param baseRuleToVirtualNames - Mapping from base rule names to virtual rule names
44
+ * @param baseRuleToVirtualNames - Mapping from base rule names to top-level-group virtual rule names
45
+ * @param baseRuleToScopedVirtualNames - Mapping from base rule names to selector-scoped (nodeRules/childNodeRules) virtual rule names
28
46
  */
29
- constructor(config: Config, baseRuleToVirtualNames?: ReadonlyMap<string, readonly string[]>);
47
+ constructor(config: Config, baseRuleToVirtualNames?: ReadonlyMap<string, readonly string[]>, baseRuleToScopedVirtualNames?: ReadonlyMap<string, readonly string[]>);
30
48
  }
@@ -4,14 +4,31 @@
4
4
  */
5
5
  export class Ruleset {
6
6
  /**
7
- * Maps base rule names to their virtual rule names created by NamedRuleGroups.
8
- * For example, if `a11y/landmark-roles` wraps `no-nested-top-level-landmark`, this maps
9
- * `"no-nested-top-level-landmark"` `["a11y/landmark-roles"]`.
10
- * Used by nodeRules/childNodeRules to propagate settings to virtual rules.
7
+ * Maps base rule names to the virtual rule names created by top-level `rules`
8
+ * NamedRuleGroups (e.g. `"a11y/landmark-roles": { rules: { "no-nested-top-level-landmark": true } }`).
9
+ * These wrappers have no selector scope of their own — they mirror the base rule
10
+ * globally so nodeRules/childNodeRules may propagate *any* matched value to them,
11
+ * not just a disable.
11
12
  */
12
13
  baseRuleToVirtualNames;
14
+ /**
15
+ * Maps base rule names to the virtual rule names created by `nodeRules[].name` /
16
+ * `childNodeRules[].name` (selector-scoped named groups), as opposed to
17
+ * {@link baseRuleToVirtualNames}'s top-level `rules` groups. Because these wrappers
18
+ * DO have their own selector scope, only a disable (`false`) may be propagated to
19
+ * them from an unrelated nodeRules/childNodeRules entry — forwarding a non-false
20
+ * value would apply the wrapper's semantics to nodes its own selector never matched
21
+ * (see issue #4023's fix history).
22
+ */
23
+ baseRuleToScopedVirtualNames;
13
24
  /** Rule overrides that apply to child nodes matching specific selectors */
14
25
  childNodeRules;
26
+ /**
27
+ * `rules` keys that are a genuine NamedRuleGroup in some layer of the
28
+ * `extends`/override chain, carried over from {@link Config.knownNamedRuleGroupKeys}
29
+ * even where the final merged value collapsed to `false`. See that field's JSDoc.
30
+ */
31
+ knownNamedRuleGroupKeys;
15
32
  /**
16
33
  * Errors collected during rule mapping (e.g., invalid wildcard usage).
17
34
  * Consumed by MLCore and reported as config-error violations.
@@ -23,12 +40,15 @@ export class Ruleset {
23
40
  rules;
24
41
  /**
25
42
  * @param config - The markuplint configuration to extract rules from
26
- * @param baseRuleToVirtualNames - Mapping from base rule names to virtual rule names
43
+ * @param baseRuleToVirtualNames - Mapping from base rule names to top-level-group virtual rule names
44
+ * @param baseRuleToScopedVirtualNames - Mapping from base rule names to selector-scoped (nodeRules/childNodeRules) virtual rule names
27
45
  */
28
- constructor(config, baseRuleToVirtualNames) {
46
+ constructor(config, baseRuleToVirtualNames, baseRuleToScopedVirtualNames) {
29
47
  this.rules = config.rules ?? {};
30
48
  this.nodeRules = config.nodeRules ?? [];
31
49
  this.childNodeRules = config.childNodeRules ?? [];
32
50
  this.baseRuleToVirtualNames = baseRuleToVirtualNames ?? new Map();
51
+ this.baseRuleToScopedVirtualNames = baseRuleToScopedVirtualNames ?? new Map();
52
+ this.knownNamedRuleGroupKeys = new Set(config.knownNamedRuleGroupKeys);
33
53
  }
34
54
  }
package/lib/types.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { AnyMLRule } from './ml-rule/index.js';
2
2
  import type { Ruleset } from './ruleset/index.js';
3
3
  import type { LocaleSet } from '@markuplint/i18n';
4
4
  import type { MLParser, ParserOptions } from '@markuplint/ml-ast';
5
- import type { Pretender, RuleCommonSettings, SeverityOptions } from '@markuplint/ml-config';
5
+ import type { Pretender, RuleAliasWarning, RuleCommonSettings, SeverityOptions } from '@markuplint/ml-config';
6
6
  import type { ExtendedSpec, MLMLSpec } from '@markuplint/ml-spec';
7
7
  /**
8
8
  * A tuple of the base HTML/ARIA specification and zero or more
@@ -24,4 +24,11 @@ export type MLFabric = {
24
24
  readonly severity: SeverityOptions;
25
25
  readonly pretenders: readonly Pretender[];
26
26
  readonly configErrors?: readonly Readonly<Error>[];
27
+ /**
28
+ * Deprecated-rule-name notices found while applying the rule-alias table
29
+ * (v5 rule-system redesign, #3989). Kept structured, separate from
30
+ * {@link configErrors}, so `MLCore.verify()` can report them under their
31
+ * own `rule-deprecation` ruleId instead of `config-error`.
32
+ */
33
+ readonly ruleDeprecations?: readonly RuleAliasWarning[];
27
34
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/ml-core",
3
- "version": "5.0.0-rc.5",
3
+ "version": "5.0.0-rc.7",
4
4
  "description": "The core module of markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,20 +34,20 @@
34
34
  "./lib/configs.js": "./lib/configs.browser.js"
35
35
  },
36
36
  "dependencies": {
37
- "@markuplint/config-presets": "5.0.0-rc.5",
38
- "@markuplint/html-parser": "5.0.0-rc.5",
39
- "@markuplint/html-spec": "5.0.0-rc.5",
40
- "@markuplint/i18n": "5.0.0-rc.5",
41
- "@markuplint/ml-ast": "5.0.0-rc.5",
42
- "@markuplint/ml-config": "5.0.0-rc.5",
43
- "@markuplint/ml-spec": "5.0.0-rc.5",
44
- "@markuplint/parser-utils": "5.0.0-rc.5",
45
- "@markuplint/selector": "5.0.0-rc.5",
46
- "@markuplint/shared": "5.0.0-rc.5",
37
+ "@markuplint/config-presets": "5.0.0-rc.7",
38
+ "@markuplint/html-parser": "5.0.0-rc.7",
39
+ "@markuplint/html-spec": "5.0.0-rc.7",
40
+ "@markuplint/i18n": "5.0.0-rc.7",
41
+ "@markuplint/ml-ast": "5.0.0-rc.7",
42
+ "@markuplint/ml-config": "5.0.0-rc.7",
43
+ "@markuplint/ml-spec": "5.0.0-rc.7",
44
+ "@markuplint/parser-utils": "5.0.0-rc.7",
45
+ "@markuplint/selector": "5.0.0-rc.7",
46
+ "@markuplint/shared": "5.0.0-rc.7",
47
47
  "@types/debug": "4.1.13",
48
48
  "debug": "4.4.3",
49
49
  "is-plain-object": "5.0.0",
50
50
  "type-fest": "5.6.0"
51
51
  },
52
- "gitHead": "8d87463af2ff3f1b83fb28da20f1819362cf3555"
52
+ "gitHead": "8d7a3b801e0f5f88438c003e7eee2bd45e6aedd0"
53
53
  }