@usefragments/core 1.9.1 → 1.10.0
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/dist/{chunk-DGHZQTLH.js → chunk-XN3LSDPY.js} +3 -2
- package/dist/chunk-XN3LSDPY.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-eEzCyfes.d.ts → governance-B5WWcmFY.d.ts} +19 -3
- package/dist/index.d.ts +143 -135
- package/dist/index.js +110 -53
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +18 -18
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/governance-integrity.test.ts +93 -2
- package/src/governance-integrity.ts +46 -2
- package/src/governance.ts +15 -3
- package/src/rules/a11y-required-accessible-name.ts +45 -2
- package/src/rules/index.ts +4 -0
- package/src/rules/rules.test.ts +72 -0
- package/dist/chunk-DGHZQTLH.js.map +0 -1
- package/dist/{index-_sxhUNqx.d.ts → index-DtHxs0Pf.d.ts} +12 -12
package/dist/react-types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ComponentType, ReactNode, JSX } from 'react';
|
|
2
|
-
export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-
|
|
2
|
+
export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-B5WWcmFY.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
import './topology/index.js';
|
|
5
5
|
import './types-xJ2xyp_G.js';
|
package/dist/registry.d.ts
CHANGED
|
@@ -215,7 +215,7 @@ declare const registryComponentSchema: z.ZodObject<{
|
|
|
215
215
|
componentId: string;
|
|
216
216
|
name: string;
|
|
217
217
|
publicRef: string;
|
|
218
|
-
tier: "
|
|
218
|
+
tier: "core" | "composition" | "custom" | "utility";
|
|
219
219
|
provenance: Record<string, unknown>;
|
|
220
220
|
examples: unknown[];
|
|
221
221
|
files: {
|
|
@@ -263,7 +263,7 @@ declare const registryComponentSchema: z.ZodObject<{
|
|
|
263
263
|
status?: string | undefined;
|
|
264
264
|
category?: string | undefined;
|
|
265
265
|
contract?: unknown;
|
|
266
|
-
tier?: "
|
|
266
|
+
tier?: "core" | "composition" | "custom" | "utility" | undefined;
|
|
267
267
|
provenance?: Record<string, unknown> | undefined;
|
|
268
268
|
examples?: unknown[] | undefined;
|
|
269
269
|
styleFiles?: string[] | undefined;
|
|
@@ -497,7 +497,7 @@ declare const registryManifestDraftSchema: z.ZodObject<{
|
|
|
497
497
|
componentId: string;
|
|
498
498
|
name: string;
|
|
499
499
|
publicRef: string;
|
|
500
|
-
tier: "
|
|
500
|
+
tier: "core" | "composition" | "custom" | "utility";
|
|
501
501
|
provenance: Record<string, unknown>;
|
|
502
502
|
examples: unknown[];
|
|
503
503
|
files: {
|
|
@@ -545,7 +545,7 @@ declare const registryManifestDraftSchema: z.ZodObject<{
|
|
|
545
545
|
status?: string | undefined;
|
|
546
546
|
category?: string | undefined;
|
|
547
547
|
contract?: unknown;
|
|
548
|
-
tier?: "
|
|
548
|
+
tier?: "core" | "composition" | "custom" | "utility" | undefined;
|
|
549
549
|
provenance?: Record<string, unknown> | undefined;
|
|
550
550
|
examples?: unknown[] | undefined;
|
|
551
551
|
styleFiles?: string[] | undefined;
|
|
@@ -610,7 +610,7 @@ declare const registryManifestDraftSchema: z.ZodObject<{
|
|
|
610
610
|
componentId: string;
|
|
611
611
|
name: string;
|
|
612
612
|
publicRef: string;
|
|
613
|
-
tier: "
|
|
613
|
+
tier: "core" | "composition" | "custom" | "utility";
|
|
614
614
|
provenance: Record<string, unknown>;
|
|
615
615
|
examples: unknown[];
|
|
616
616
|
files: {
|
|
@@ -712,7 +712,7 @@ declare const registryManifestDraftSchema: z.ZodObject<{
|
|
|
712
712
|
status?: string | undefined;
|
|
713
713
|
category?: string | undefined;
|
|
714
714
|
contract?: unknown;
|
|
715
|
-
tier?: "
|
|
715
|
+
tier?: "core" | "composition" | "custom" | "utility" | undefined;
|
|
716
716
|
provenance?: Record<string, unknown> | undefined;
|
|
717
717
|
examples?: unknown[] | undefined;
|
|
718
718
|
styleFiles?: string[] | undefined;
|
|
@@ -970,7 +970,7 @@ declare const registryManifestSchema: z.ZodObject<{
|
|
|
970
970
|
componentId: string;
|
|
971
971
|
name: string;
|
|
972
972
|
publicRef: string;
|
|
973
|
-
tier: "
|
|
973
|
+
tier: "core" | "composition" | "custom" | "utility";
|
|
974
974
|
provenance: Record<string, unknown>;
|
|
975
975
|
examples: unknown[];
|
|
976
976
|
files: {
|
|
@@ -1018,7 +1018,7 @@ declare const registryManifestSchema: z.ZodObject<{
|
|
|
1018
1018
|
status?: string | undefined;
|
|
1019
1019
|
category?: string | undefined;
|
|
1020
1020
|
contract?: unknown;
|
|
1021
|
-
tier?: "
|
|
1021
|
+
tier?: "core" | "composition" | "custom" | "utility" | undefined;
|
|
1022
1022
|
provenance?: Record<string, unknown> | undefined;
|
|
1023
1023
|
examples?: unknown[] | undefined;
|
|
1024
1024
|
styleFiles?: string[] | undefined;
|
|
@@ -1085,7 +1085,7 @@ declare const registryManifestSchema: z.ZodObject<{
|
|
|
1085
1085
|
componentId: string;
|
|
1086
1086
|
name: string;
|
|
1087
1087
|
publicRef: string;
|
|
1088
|
-
tier: "
|
|
1088
|
+
tier: "core" | "composition" | "custom" | "utility";
|
|
1089
1089
|
provenance: Record<string, unknown>;
|
|
1090
1090
|
examples: unknown[];
|
|
1091
1091
|
files: {
|
|
@@ -1188,7 +1188,7 @@ declare const registryManifestSchema: z.ZodObject<{
|
|
|
1188
1188
|
status?: string | undefined;
|
|
1189
1189
|
category?: string | undefined;
|
|
1190
1190
|
contract?: unknown;
|
|
1191
|
-
tier?: "
|
|
1191
|
+
tier?: "core" | "composition" | "custom" | "utility" | undefined;
|
|
1192
1192
|
provenance?: Record<string, unknown> | undefined;
|
|
1193
1193
|
examples?: unknown[] | undefined;
|
|
1194
1194
|
styleFiles?: string[] | undefined;
|
|
@@ -1449,7 +1449,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1449
1449
|
componentId: string;
|
|
1450
1450
|
name: string;
|
|
1451
1451
|
publicRef: string;
|
|
1452
|
-
tier: "
|
|
1452
|
+
tier: "core" | "composition" | "custom" | "utility";
|
|
1453
1453
|
provenance: Record<string, unknown>;
|
|
1454
1454
|
examples: unknown[];
|
|
1455
1455
|
files: {
|
|
@@ -1497,7 +1497,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1497
1497
|
status?: string | undefined;
|
|
1498
1498
|
category?: string | undefined;
|
|
1499
1499
|
contract?: unknown;
|
|
1500
|
-
tier?: "
|
|
1500
|
+
tier?: "core" | "composition" | "custom" | "utility" | undefined;
|
|
1501
1501
|
provenance?: Record<string, unknown> | undefined;
|
|
1502
1502
|
examples?: unknown[] | undefined;
|
|
1503
1503
|
styleFiles?: string[] | undefined;
|
|
@@ -1564,7 +1564,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1564
1564
|
componentId: string;
|
|
1565
1565
|
name: string;
|
|
1566
1566
|
publicRef: string;
|
|
1567
|
-
tier: "
|
|
1567
|
+
tier: "core" | "composition" | "custom" | "utility";
|
|
1568
1568
|
provenance: Record<string, unknown>;
|
|
1569
1569
|
examples: unknown[];
|
|
1570
1570
|
files: {
|
|
@@ -1667,7 +1667,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1667
1667
|
status?: string | undefined;
|
|
1668
1668
|
category?: string | undefined;
|
|
1669
1669
|
contract?: unknown;
|
|
1670
|
-
tier?: "
|
|
1670
|
+
tier?: "core" | "composition" | "custom" | "utility" | undefined;
|
|
1671
1671
|
provenance?: Record<string, unknown> | undefined;
|
|
1672
1672
|
examples?: unknown[] | undefined;
|
|
1673
1673
|
styleFiles?: string[] | undefined;
|
|
@@ -1772,7 +1772,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1772
1772
|
componentId: string;
|
|
1773
1773
|
name: string;
|
|
1774
1774
|
publicRef: string;
|
|
1775
|
-
tier: "
|
|
1775
|
+
tier: "core" | "composition" | "custom" | "utility";
|
|
1776
1776
|
provenance: Record<string, unknown>;
|
|
1777
1777
|
examples: unknown[];
|
|
1778
1778
|
files: {
|
|
@@ -1885,7 +1885,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1885
1885
|
status?: string | undefined;
|
|
1886
1886
|
category?: string | undefined;
|
|
1887
1887
|
contract?: unknown;
|
|
1888
|
-
tier?: "
|
|
1888
|
+
tier?: "core" | "composition" | "custom" | "utility" | undefined;
|
|
1889
1889
|
provenance?: Record<string, unknown> | undefined;
|
|
1890
1890
|
examples?: unknown[] | undefined;
|
|
1891
1891
|
styleFiles?: string[] | undefined;
|
|
@@ -1978,7 +1978,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1978
1978
|
componentId: string;
|
|
1979
1979
|
name: string;
|
|
1980
1980
|
publicRef: string;
|
|
1981
|
-
tier: "
|
|
1981
|
+
tier: "core" | "composition" | "custom" | "utility";
|
|
1982
1982
|
provenance: Record<string, unknown>;
|
|
1983
1983
|
examples: unknown[];
|
|
1984
1984
|
files: {
|
|
@@ -2091,7 +2091,7 @@ declare const registryArtifactSchema: z.ZodEffects<z.ZodObject<{
|
|
|
2091
2091
|
status?: string | undefined;
|
|
2092
2092
|
category?: string | undefined;
|
|
2093
2093
|
contract?: unknown;
|
|
2094
|
-
tier?: "
|
|
2094
|
+
tier?: "core" | "composition" | "custom" | "utility" | undefined;
|
|
2095
2095
|
provenance?: Record<string, unknown> | undefined;
|
|
2096
2096
|
examples?: unknown[] | undefined;
|
|
2097
2097
|
styleFiles?: string[] | undefined;
|
package/dist/schemas/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import 'zod';
|
|
2
2
|
export { LegacySeverityLevel, Severity, SeverityLevel, legacySeverityLevelSchema, severityLevelSchema, severitySchema } from '../severity.js';
|
|
3
|
-
export { b as AGENT_FORMAT_SCHEMA_VERSION, c as AgentErrorEnvelope, A as AgentFormat, d as AgentOutput, Q as FactEvidence, R as FactLocation, F as Finding, a as FindingFix, f as FindingReplaceClassTokenFix, g as FindingReplaceComponentFix, h as FindingReplaceImportFix, i as FindingReplacePropValueFix, j as FindingReplaceStyleValueFix, k as Fix, G as GovernanceVerdict, l as GovernanceVerdictMetadata, S as SuppressionDirective, V as ValidatorResult, n as Violation, o as agentErrorEnvelopeSchema, T as agentFindingSchema, p as agentFormatSchema, q as agentIntegritySchema, r as agentOutputSchema, U as agentPlanSchema, t as factEvidenceSchema, u as factLocationSchema, v as findingFixSchema, w as findingReplaceClassTokenFixSchema, x as findingReplaceComponentFixSchema, y as findingReplaceImportFixSchema, z as findingReplacePropValueFixSchema, B as findingReplaceStyleValueFixSchema, C as findingSchema, D as fixSchema, H as governanceVerdictMetadataSchema, J as governanceVerdictSchema, K as normalizeFinding, L as normalizeSeverity, M as normalizeViolation, N as suppressionDirectiveSchema, O as validatorResultSchema, P as violationSchema } from '../index-
|
|
3
|
+
export { b as AGENT_FORMAT_SCHEMA_VERSION, c as AgentErrorEnvelope, A as AgentFormat, d as AgentOutput, Q as FactEvidence, R as FactLocation, F as Finding, a as FindingFix, f as FindingReplaceClassTokenFix, g as FindingReplaceComponentFix, h as FindingReplaceImportFix, i as FindingReplacePropValueFix, j as FindingReplaceStyleValueFix, k as Fix, G as GovernanceVerdict, l as GovernanceVerdictMetadata, S as SuppressionDirective, V as ValidatorResult, n as Violation, o as agentErrorEnvelopeSchema, T as agentFindingSchema, p as agentFormatSchema, q as agentIntegritySchema, r as agentOutputSchema, U as agentPlanSchema, t as factEvidenceSchema, u as factLocationSchema, v as findingFixSchema, w as findingReplaceClassTokenFixSchema, x as findingReplaceComponentFixSchema, y as findingReplaceImportFixSchema, z as findingReplacePropValueFixSchema, B as findingReplaceStyleValueFixSchema, C as findingSchema, D as fixSchema, H as governanceVerdictMetadataSchema, J as governanceVerdictSchema, K as normalizeFinding, L as normalizeSeverity, M as normalizeViolation, N as suppressionDirectiveSchema, O as validatorResultSchema, P as violationSchema } from '../index-DtHxs0Pf.js';
|
package/dist/schemas/index.js
CHANGED
package/dist/test-utils.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -118,6 +118,90 @@ describe("inert config diagnostics", () => {
|
|
|
118
118
|
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([]);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
it("accepts valid options declared by a registered rule schema", () => {
|
|
122
|
+
const options = { ignoreParents: ["FormControlLabel"], ignoreSpread: true };
|
|
123
|
+
const authored = {
|
|
124
|
+
govern: {
|
|
125
|
+
rules: {
|
|
126
|
+
"a11y/required-accessible-name": { enabled: true, options },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
const parsed = fragmentsConfigSchema.parse(authored);
|
|
131
|
+
|
|
132
|
+
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([]);
|
|
133
|
+
expect(
|
|
134
|
+
(
|
|
135
|
+
parsed.govern?.rules?.["a11y/required-accessible-name"] as {
|
|
136
|
+
options?: unknown;
|
|
137
|
+
}
|
|
138
|
+
).options
|
|
139
|
+
).toEqual(options);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("diagnoses invalid options against the registered rule schema", () => {
|
|
143
|
+
const authored = {
|
|
144
|
+
govern: {
|
|
145
|
+
rules: {
|
|
146
|
+
"a11y/required-accessible-name": {
|
|
147
|
+
enabled: true,
|
|
148
|
+
options: { ignoreSpread: "yes" },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const parsed = fragmentsConfigSchema.parse(authored);
|
|
154
|
+
|
|
155
|
+
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([
|
|
156
|
+
expect.objectContaining({
|
|
157
|
+
code: "FUI9004",
|
|
158
|
+
path: "govern.rules.a11y/required-accessible-name.options",
|
|
159
|
+
message: expect.stringMatching(
|
|
160
|
+
/a11y\/required-accessible-name.*ignoreSpread.*Expected boolean/
|
|
161
|
+
),
|
|
162
|
+
}),
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("diagnoses a typo'd option key instead of letting it silently no-op", () => {
|
|
167
|
+
const authored = {
|
|
168
|
+
govern: {
|
|
169
|
+
rules: {
|
|
170
|
+
"a11y/required-accessible-name": {
|
|
171
|
+
enabled: true,
|
|
172
|
+
options: { ignoredParents: ["FormControlLabel"] },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
const parsed = fragmentsConfigSchema.parse(authored);
|
|
178
|
+
|
|
179
|
+
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
code: "FUI9004",
|
|
182
|
+
path: "govern.rules.a11y/required-accessible-name.options",
|
|
183
|
+
message: expect.stringMatching(/ignoredParents/),
|
|
184
|
+
}),
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("leaves options on rules without a schema untouched", () => {
|
|
189
|
+
const options = { projectSpecificSetting: "unchanged" };
|
|
190
|
+
const authored = {
|
|
191
|
+
govern: {
|
|
192
|
+
rules: {
|
|
193
|
+
"styles/no-raw-color": { enabled: true, options },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
const parsed = fragmentsConfigSchema.parse(authored);
|
|
198
|
+
|
|
199
|
+
expect(detectUnconsumedConfigKeys(authored, parsed)).toEqual([]);
|
|
200
|
+
expect(
|
|
201
|
+
(parsed.govern?.rules?.["styles/no-raw-color"] as { options?: unknown }).options
|
|
202
|
+
).toEqual(options);
|
|
203
|
+
});
|
|
204
|
+
|
|
121
205
|
it("reports an authored spacing scale that no effective property policy references", () => {
|
|
122
206
|
const declared: GovernanceConfig = {
|
|
123
207
|
scales: {
|
|
@@ -167,8 +251,15 @@ describe("inert config diagnostics", () => {
|
|
|
167
251
|
expect(detectOrphanGovernanceScales(policy, policy)).toEqual([]);
|
|
168
252
|
});
|
|
169
253
|
|
|
170
|
-
it("accepts
|
|
171
|
-
const authored = {
|
|
254
|
+
it("accepts governance CI gates as consumed keys", () => {
|
|
255
|
+
const authored = {
|
|
256
|
+
govern: {
|
|
257
|
+
ci: {
|
|
258
|
+
failOnInert: true,
|
|
259
|
+
failOnAdoptionRegression: true,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
};
|
|
172
263
|
|
|
173
264
|
expect(detectUnconsumedConfigKeys(authored, fragmentsConfigSchema.parse(authored))).toEqual([]);
|
|
174
265
|
});
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* calling in.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { ZodObject } from "zod";
|
|
21
|
+
|
|
20
22
|
import type { CanonicalSource, GovernanceConfig, GovernanceSeverity } from "./governance.js";
|
|
21
23
|
import {
|
|
22
24
|
normalizePolicyExcludes,
|
|
@@ -25,7 +27,8 @@ import {
|
|
|
25
27
|
} from "./policy-exclude.js";
|
|
26
28
|
import { compileGlobalGovernanceFacts } from "./facts/index.js";
|
|
27
29
|
import type { FactConflict } from "./facts/index.js";
|
|
28
|
-
import {
|
|
30
|
+
import { RULES } from "./rules/index.js";
|
|
31
|
+
import { RULE_FAMILY_IDS, ruleFamilyMembers } from "./rules/families.js";
|
|
29
32
|
import { FRAGMENTS_INTERNAL_RULE_IDS, RULE_TIER } from "./rules/tiers.js";
|
|
30
33
|
import { BLOCKING_RULE_ALLOWLIST } from "./rules/emit-gate.js";
|
|
31
34
|
|
|
@@ -175,6 +178,23 @@ function dedupe(values: string[]): string[] {
|
|
|
175
178
|
|
|
176
179
|
const CONSUMED_RULE_IDS = new Set(Object.keys(RULE_TIER));
|
|
177
180
|
const RECOGNIZED_RULE_IDS = new Set([...CONSUMED_RULE_IDS, ...RULE_FAMILY_IDS]);
|
|
181
|
+
// Reporter-strict, executor-lenient: unknown option keys are diagnosed here as
|
|
182
|
+
// the inert-config class FUI9004 exists to name, while rules keep their own
|
|
183
|
+
// tolerant parse (unknown keys stripped, valid keys still honored).
|
|
184
|
+
const RULE_OPTIONS_SCHEMAS = new Map(
|
|
185
|
+
RULES.flatMap((rule) =>
|
|
186
|
+
rule.optionsSchema
|
|
187
|
+
? [
|
|
188
|
+
[
|
|
189
|
+
rule.id,
|
|
190
|
+
rule.optionsSchema instanceof ZodObject
|
|
191
|
+
? rule.optionsSchema.strict()
|
|
192
|
+
: rule.optionsSchema,
|
|
193
|
+
] as const,
|
|
194
|
+
]
|
|
195
|
+
: []
|
|
196
|
+
)
|
|
197
|
+
);
|
|
178
198
|
// `exclude` is consumed by the scan's finding-override pass (it scopes the rule off
|
|
179
199
|
// the matching paths and reports each drop in the ignored accounting). It was absent
|
|
180
200
|
// from this allow-set, so a working key was diagnosed as inert — the diagnostic was
|
|
@@ -219,7 +239,7 @@ const PASSTHROUGH_KEYS = [
|
|
|
219
239
|
},
|
|
220
240
|
{
|
|
221
241
|
path: ["govern", "ci"],
|
|
222
|
-
keys: ["failOnWarnings", "failOnInert"],
|
|
242
|
+
keys: ["failOnWarnings", "failOnInert", "failOnAdoptionRegression"],
|
|
223
243
|
},
|
|
224
244
|
] as const;
|
|
225
245
|
|
|
@@ -546,6 +566,30 @@ function collectRuleConfigKeys(
|
|
|
546
566
|
diagnostics.push(unconsumedKeyDiagnostic([...path, ruleId, key]));
|
|
547
567
|
}
|
|
548
568
|
}
|
|
569
|
+
if (config["options"] === undefined) continue;
|
|
570
|
+
|
|
571
|
+
const invalidOptions = ruleFamilyMembers(ruleId).flatMap((memberRuleId) => {
|
|
572
|
+
const schema = RULE_OPTIONS_SCHEMAS.get(memberRuleId);
|
|
573
|
+
if (!schema) return [];
|
|
574
|
+
const parsed = schema.safeParse(config["options"]);
|
|
575
|
+
if (parsed.success) return [];
|
|
576
|
+
const problem = parsed.error.errors
|
|
577
|
+
.map((issue) => `${issue.path.join(".") || "options"}: ${issue.message}`)
|
|
578
|
+
.join("; ");
|
|
579
|
+
return [memberRuleId === ruleId ? problem : `${memberRuleId}: ${problem}`];
|
|
580
|
+
});
|
|
581
|
+
if (invalidOptions.length === 0) continue;
|
|
582
|
+
|
|
583
|
+
const diagnosticPath = configPath([...path, ruleId, "options"]);
|
|
584
|
+
diagnostics.push({
|
|
585
|
+
code: "FUI9004",
|
|
586
|
+
kind: "unconsumed-key",
|
|
587
|
+
severity: "warn",
|
|
588
|
+
path: diagnosticPath,
|
|
589
|
+
message:
|
|
590
|
+
`${diagnosticPath} is invalid for rule \`${ruleId}\`: ${invalidOptions.join("; ")}. ` +
|
|
591
|
+
"Fix the option values or remove the options block.",
|
|
592
|
+
});
|
|
549
593
|
}
|
|
550
594
|
}
|
|
551
595
|
|
package/src/governance.ts
CHANGED
|
@@ -355,6 +355,7 @@ export const governanceConfigSchema = z
|
|
|
355
355
|
.object({
|
|
356
356
|
failOnWarnings: z.boolean().optional(),
|
|
357
357
|
failOnInert: z.boolean().optional(),
|
|
358
|
+
failOnAdoptionRegression: z.boolean().optional(),
|
|
358
359
|
})
|
|
359
360
|
.passthrough()
|
|
360
361
|
.optional(),
|
|
@@ -443,9 +444,13 @@ export interface GovernanceConfig {
|
|
|
443
444
|
overrides?: ComponentPolicyOverride[];
|
|
444
445
|
|
|
445
446
|
/**
|
|
446
|
-
* Governance CI rendering options
|
|
447
|
-
*
|
|
448
|
-
*
|
|
447
|
+
* Governance CI rendering options. Under `--ci`, `failOnWarnings` defaults to `true`
|
|
448
|
+
* when absent; set it to `false` to keep warning findings reported but advisory.
|
|
449
|
+
* `failOnInert` is opt-in and makes inert-config diagnostics (FUI9004-FUI9008) fail
|
|
450
|
+
* the verdict; `--allow-inert` bypasses the inert gates.
|
|
451
|
+
* `failOnAdoptionRegression` is opt-in and makes `check --ci` compare the
|
|
452
|
+
* current component-identity adoption percentage with the committed
|
|
453
|
+
* `.fragments/adoption-baseline.json` floor.
|
|
449
454
|
*/
|
|
450
455
|
ci?: {
|
|
451
456
|
failOnWarnings?: boolean;
|
|
@@ -456,6 +461,13 @@ export interface GovernanceConfig {
|
|
|
456
461
|
* bypasses it, like the governance-inert gate.
|
|
457
462
|
*/
|
|
458
463
|
failOnInert?: boolean;
|
|
464
|
+
/**
|
|
465
|
+
* Fail `check --ci` when measurable component-identity adoption falls below
|
|
466
|
+
* the committed local floor. Update the floor through `check
|
|
467
|
+
* --update-baseline`; accepting a lower floor additionally requires
|
|
468
|
+
* `--allow-regression`.
|
|
469
|
+
*/
|
|
470
|
+
failOnAdoptionRegression?: boolean;
|
|
459
471
|
[key: string]: unknown;
|
|
460
472
|
};
|
|
461
473
|
[key: string]: unknown;
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
UsageNodeFact,
|
|
25
25
|
UsagePropResolvedFact,
|
|
26
26
|
} from "../facts/index.js";
|
|
27
|
+
import { z } from "zod";
|
|
27
28
|
|
|
28
29
|
import { isAriaHidden } from "./a11y-utils.js";
|
|
29
30
|
import { makeFinding } from "./finding.js";
|
|
@@ -31,7 +32,16 @@ import type { Finding } from "./types.js";
|
|
|
31
32
|
import { indexPropsByNodeId, indexTextChildrenByNodeId } from "./utils.js";
|
|
32
33
|
|
|
33
34
|
export const RULE_ID = "a11y/required-accessible-name";
|
|
34
|
-
export const RULE_VERSION = "
|
|
35
|
+
export const RULE_VERSION = "3";
|
|
36
|
+
|
|
37
|
+
export const a11yRequiredAccessibleNameOptionsSchema = z.object({
|
|
38
|
+
ignoreParents: z.array(z.string().min(1)).optional(),
|
|
39
|
+
ignoreSpread: z.boolean().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export type A11yRequiredAccessibleNameOptions = z.infer<
|
|
43
|
+
typeof a11yRequiredAccessibleNameOptionsSchema
|
|
44
|
+
>;
|
|
35
45
|
|
|
36
46
|
const NAME_SOURCE_PROPS = new Set(["aria-label", "aria-labelledby", "title"]);
|
|
37
47
|
const COMPONENT_NAME_SOURCE_PROPS = new Set([...NAME_SOURCE_PROPS, "label"]);
|
|
@@ -57,7 +67,12 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
57
67
|
ix.byKind("usage_child_content").map((content) => [content.nodeId, content])
|
|
58
68
|
);
|
|
59
69
|
const globalPolicy = ix.policy.ruleConfig(RULE_ID);
|
|
70
|
+
const parsedOptions = a11yRequiredAccessibleNameOptionsSchema.safeParse(globalPolicy?.options);
|
|
71
|
+
const options: A11yRequiredAccessibleNameOptions = parsedOptions.success
|
|
72
|
+
? parsedOptions.data
|
|
73
|
+
: {};
|
|
60
74
|
const usageByNode = new Map(ix.byKind("usage_component").map((usage) => [usage.nodeId, usage]));
|
|
75
|
+
const usageNodes = ix.byKind("usage_node");
|
|
61
76
|
const definitions = new Map(
|
|
62
77
|
ix.byKind("component_definition").map((definition) => [definition.componentKey, definition])
|
|
63
78
|
);
|
|
@@ -73,6 +88,7 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
73
88
|
globalPolicy?.enabled === true &&
|
|
74
89
|
requiresAccessibleName(node, usageComponent, definitions, props);
|
|
75
90
|
if (!componentPolicy && !genericControl) continue;
|
|
91
|
+
if (isInsideIgnoredAttributeParent(node, usageNodes, options.ignoreParents)) continue;
|
|
76
92
|
|
|
77
93
|
if (
|
|
78
94
|
hasAccessibleName(
|
|
@@ -86,11 +102,16 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
86
102
|
continue;
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
const hasSpreadProps = props.some((prop) => prop.resolution === "spread");
|
|
106
|
+
if (hasSpreadProps && options.ignoreSpread === true) continue;
|
|
107
|
+
|
|
89
108
|
findings.push(
|
|
90
109
|
makeFinding({
|
|
91
110
|
ruleId: RULE_ID,
|
|
92
111
|
ruleVersion: RULE_VERSION,
|
|
93
|
-
severity:
|
|
112
|
+
severity: hasSpreadProps
|
|
113
|
+
? "minor"
|
|
114
|
+
: (componentPolicy?.severity ?? globalPolicy?.severity ?? "warn"),
|
|
94
115
|
message: `<${node.element}> has no accessible name. ${
|
|
95
116
|
componentPolicy?.because ??
|
|
96
117
|
"Add visible text, aria-label, aria-labelledby, or a component label."
|
|
@@ -119,6 +140,28 @@ export function ruleA11yRequiredAccessibleName(ix: FactIndex): Finding[] {
|
|
|
119
140
|
return findings;
|
|
120
141
|
}
|
|
121
142
|
|
|
143
|
+
function isInsideIgnoredAttributeParent(
|
|
144
|
+
node: UsageNodeFact,
|
|
145
|
+
usageNodes: readonly UsageNodeFact[],
|
|
146
|
+
ignoreParents: readonly string[] | undefined
|
|
147
|
+
): boolean {
|
|
148
|
+
if (!ignoreParents?.length || !node.nodePath.includes("/attr:")) return false;
|
|
149
|
+
const ignored = new Set(ignoreParents);
|
|
150
|
+
const segments = node.nodePath.split("/");
|
|
151
|
+
const ancestorPaths = new Set<string>();
|
|
152
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
153
|
+
if (segments[index].startsWith("attr:")) {
|
|
154
|
+
ancestorPaths.add(segments.slice(0, index).join("/"));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return usageNodes.some(
|
|
158
|
+
(candidate) =>
|
|
159
|
+
candidate.file === node.file &&
|
|
160
|
+
ancestorPaths.has(candidate.nodePath) &&
|
|
161
|
+
ignored.has(candidate.element)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
122
165
|
function hasAccessibleName(
|
|
123
166
|
props: UsagePropResolvedFact[],
|
|
124
167
|
textChildren: ReadonlyArray<{ text: string }>,
|
package/src/rules/index.ts
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { FactIndex } from "../facts/index.js";
|
|
11
|
+
import type { ZodTypeAny } from "zod";
|
|
11
12
|
|
|
12
13
|
import {
|
|
14
|
+
a11yRequiredAccessibleNameOptionsSchema,
|
|
13
15
|
RULE_VERSION as A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION,
|
|
14
16
|
ruleA11yRequiredAccessibleName,
|
|
15
17
|
} from "./a11y-required-accessible-name.js";
|
|
@@ -44,6 +46,7 @@ export interface Rule {
|
|
|
44
46
|
id: string;
|
|
45
47
|
version: string;
|
|
46
48
|
run: RuleFn;
|
|
49
|
+
optionsSchema?: ZodTypeAny;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export const RULES: readonly Rule[] = [
|
|
@@ -156,6 +159,7 @@ export const RULES: readonly Rule[] = [
|
|
|
156
159
|
id: "a11y/required-accessible-name",
|
|
157
160
|
version: A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION,
|
|
158
161
|
run: ruleA11yRequiredAccessibleName,
|
|
162
|
+
optionsSchema: a11yRequiredAccessibleNameOptionsSchema,
|
|
159
163
|
},
|
|
160
164
|
{
|
|
161
165
|
id: "a11y/standard",
|
package/src/rules/rules.test.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
runRules,
|
|
37
37
|
} from "../index.js";
|
|
38
38
|
import type { CanonicalBridgeV1, Fact, FactId } from "../index.js";
|
|
39
|
+
import { makeGovernanceRuleConfigFact } from "../facts/index.js";
|
|
39
40
|
import { RULE_VERSION as A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION } from "./a11y-required-accessible-name.js";
|
|
40
41
|
import { makeFinding } from "./finding.js";
|
|
41
42
|
|
|
@@ -1425,6 +1426,77 @@ describe("a11y/required-accessible-name", () => {
|
|
|
1425
1426
|
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1426
1427
|
});
|
|
1427
1428
|
|
|
1429
|
+
it("ignores controls rendered through an ignored same-file attribute parent", () => {
|
|
1430
|
+
const ix = emptyIndexWithGovernance();
|
|
1431
|
+
ix.add(
|
|
1432
|
+
makeGovernanceRuleConfigFact({
|
|
1433
|
+
ruleId: "a11y/required-accessible-name",
|
|
1434
|
+
enabled: true,
|
|
1435
|
+
options: { ignoreParents: ["FormControlLabel"] },
|
|
1436
|
+
})
|
|
1437
|
+
);
|
|
1438
|
+
ix.add(
|
|
1439
|
+
makeUsageNodeFact({
|
|
1440
|
+
file: "apps/x.tsx",
|
|
1441
|
+
nodePath: "0:0",
|
|
1442
|
+
element: "FormControlLabel",
|
|
1443
|
+
location: { file: "apps/x.tsx", line: 1, column: 0 },
|
|
1444
|
+
})
|
|
1445
|
+
);
|
|
1446
|
+
addButtonUsage(ix, {
|
|
1447
|
+
file: "apps/x.tsx",
|
|
1448
|
+
nodePath: "0:0/attr:control/0:0",
|
|
1449
|
+
line: 1,
|
|
1450
|
+
column: 28,
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
it("demotes an unnamed component with spread props to minor", () => {
|
|
1457
|
+
const ix = emptyIndexWithGovernance();
|
|
1458
|
+
addButtonUsage(ix, {
|
|
1459
|
+
file: "apps/x.tsx",
|
|
1460
|
+
nodePath: "0:0",
|
|
1461
|
+
line: 1,
|
|
1462
|
+
column: 0,
|
|
1463
|
+
props: [{ prop: "...props", resolution: "spread" }],
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toMatchObject([{ severity: "minor" }]);
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
it("skips an unnamed component with spread props when ignoreSpread is enabled", () => {
|
|
1470
|
+
const ix = emptyIndexWithGovernance();
|
|
1471
|
+
ix.add(
|
|
1472
|
+
makeGovernanceRuleConfigFact({
|
|
1473
|
+
ruleId: "a11y/required-accessible-name",
|
|
1474
|
+
enabled: true,
|
|
1475
|
+
options: { ignoreSpread: true },
|
|
1476
|
+
})
|
|
1477
|
+
);
|
|
1478
|
+
addButtonUsage(ix, {
|
|
1479
|
+
file: "apps/x.tsx",
|
|
1480
|
+
nodePath: "0:0",
|
|
1481
|
+
line: 1,
|
|
1482
|
+
column: 0,
|
|
1483
|
+
props: [{ prop: "...props", resolution: "spread" }],
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
expect(ruleA11yRequiredAccessibleName(ix)).toHaveLength(0);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
it("registers ignoreParents and ignoreSpread as supported rule options", () => {
|
|
1490
|
+
const registered = RULES.find((rule) => rule.id === "a11y/required-accessible-name");
|
|
1491
|
+
|
|
1492
|
+
expect(
|
|
1493
|
+
registered?.optionsSchema?.safeParse({
|
|
1494
|
+
ignoreParents: ["FormControlLabel"],
|
|
1495
|
+
ignoreSpread: true,
|
|
1496
|
+
}).success
|
|
1497
|
+
).toBe(true);
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1428
1500
|
it("keeps the registry version in lockstep with the emitted finding version", () => {
|
|
1429
1501
|
const registered = RULES.find((rule) => rule.id === "a11y/required-accessible-name");
|
|
1430
1502
|
expect(registered?.version).toBe(A11Y_REQUIRED_ACCESSIBLE_NAME_VERSION);
|