@usefragments/core 1.5.0 → 1.5.2
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-HXGE3O2F.js → chunk-BAHCOAVG.js} +133 -7
- package/dist/chunk-BAHCOAVG.js.map +1 -0
- package/dist/{chunk-57QDBEHQ.js → chunk-ZHS52OT4.js} +3 -9
- package/dist/chunk-ZHS52OT4.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +2 -2
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-BLsyk56o.d.ts → governance-pKrfh517.d.ts} +264 -4
- package/dist/{index-h_yWj15D.d.ts → index-DbkPE46t.d.ts} +40 -0
- package/dist/index.d.ts +472 -28
- package/dist/index.js +561 -39
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- 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/agent-format.test.ts +1 -0
- package/src/canonical-bridge.ts +46 -0
- package/src/canonical-direction.ts +118 -0
- package/src/codes/__tests__/codes.test.ts +1 -1
- package/src/codes/codes.ts +29 -0
- package/src/config.ts +20 -0
- package/src/contract/preimage.test.ts +40 -0
- package/src/contract/preimage.ts +20 -0
- package/src/effective-governance-inputs.test.ts +52 -0
- package/src/effective-governance-inputs.ts +106 -0
- package/src/facts/builders.ts +25 -1
- package/src/facts/compile.ts +20 -2
- package/src/facts/fact-index.ts +12 -1
- package/src/facts/facts.test.ts +14 -18
- package/src/facts/types.ts +25 -6
- package/src/governance-integrity.test.ts +127 -0
- package/src/governance-integrity.ts +326 -4
- package/src/governance.test.ts +87 -1
- package/src/governance.ts +121 -0
- package/src/index.ts +34 -5
- package/src/rules/__tests__/fix-emission-invariant.test.ts +35 -4
- package/src/rules/components-prefer-library.test.ts +103 -0
- package/src/rules/components-prefer-library.ts +30 -4
- package/src/rules/fix-availability.ts +1 -0
- package/src/rules/index.ts +7 -0
- package/src/rules/jsx-preferred-import-path.ts +45 -9
- package/src/rules/rules.test.ts +202 -6
- package/src/rules/spacing-resolution.ts +2 -2
- package/src/rules/styles-no-raw-color.test.ts +6 -1
- package/src/rules/styles-no-raw-color.ts +3 -2
- package/src/rules/styles-no-raw-dimensions.ts +5 -7
- package/src/rules/styles-no-raw-spacing.test.ts +48 -0
- package/src/rules/styles-no-raw-spacing.ts +10 -7
- package/src/rules/styles-no-raw-typography.ts +7 -4
- package/src/rules/taxonomy.test.ts +3 -0
- package/src/rules/tiers.ts +2 -0
- package/src/rules/token-candidates.ts +36 -0
- package/src/rules/tokens-require-dual-fallback.ts +2 -1
- package/src/rules/tokens-upstream-drift.test.ts +111 -0
- package/src/rules/tokens-upstream-drift.ts +57 -0
- package/src/rules/utils.ts +22 -14
- package/src/schema.ts +13 -0
- package/src/schemas/index.ts +5 -8
- package/src/token-types.ts +71 -1
- package/src/types.ts +27 -3
- package/dist/chunk-57QDBEHQ.js.map +0 -1
- package/dist/chunk-HXGE3O2F.js.map +0 -1
|
@@ -24,6 +24,23 @@ import { BLOCKING_RULE_ALLOWLIST } from "./rules/emit-gate.js";
|
|
|
24
24
|
|
|
25
25
|
export type GovernanceIntegrityStatus = "healthy" | "degraded" | "inert";
|
|
26
26
|
|
|
27
|
+
export type InertConfigDiagnosticKind = "orphan-scale" | "unconsumed-key";
|
|
28
|
+
export type InertConfigDiagnosticCode = "FUI9004" | "FUI9005";
|
|
29
|
+
|
|
30
|
+
export interface InertConfigDiagnostic {
|
|
31
|
+
code: InertConfigDiagnosticCode;
|
|
32
|
+
kind: InertConfigDiagnosticKind;
|
|
33
|
+
severity: "warn";
|
|
34
|
+
path: string;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GovernanceIntegrityRoster {
|
|
39
|
+
configured: number;
|
|
40
|
+
active: number;
|
|
41
|
+
inert: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
export type GovernanceIntegrityFamilyId =
|
|
28
45
|
| "policy"
|
|
29
46
|
| "components"
|
|
@@ -53,6 +70,8 @@ export interface GovernanceIntegrityInput {
|
|
|
53
70
|
/** Whether `tokens/css-vars-must-be-defined` is activated (caller supplies). */
|
|
54
71
|
cssVarsActive?: boolean;
|
|
55
72
|
mode?: "scan" | "ci" | "hook" | "doctor" | "setup";
|
|
73
|
+
/** Named, verdict-neutral diagnostics derived from the authored config. */
|
|
74
|
+
configDiagnostics?: readonly InertConfigDiagnostic[];
|
|
56
75
|
}
|
|
57
76
|
|
|
58
77
|
export interface GovernanceIntegrityVerdict {
|
|
@@ -64,6 +83,8 @@ export interface GovernanceIntegrityVerdict {
|
|
|
64
83
|
armed: GovernanceIntegrityFamilyId[];
|
|
65
84
|
summary: string;
|
|
66
85
|
remediations: string[];
|
|
86
|
+
configDiagnostics?: InertConfigDiagnostic[];
|
|
87
|
+
roster?: GovernanceIntegrityRoster;
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
interface EffectiveRuleConfig {
|
|
@@ -131,6 +152,287 @@ function dedupe(values: string[]): string[] {
|
|
|
131
152
|
return [...new Set(values)];
|
|
132
153
|
}
|
|
133
154
|
|
|
155
|
+
const RULE_FAMILY_IDS = new Set(["tokens/hardcoded-values", "components/usage", "a11y/wcag"]);
|
|
156
|
+
const CONSUMED_RULE_IDS = new Set(Object.keys(RULE_TIER));
|
|
157
|
+
const RECOGNIZED_RULE_IDS = new Set([...CONSUMED_RULE_IDS, ...RULE_FAMILY_IDS]);
|
|
158
|
+
const RULE_CONFIG_KEYS = new Set(["enabled", "severity", "options"]);
|
|
159
|
+
|
|
160
|
+
const PASSTHROUGH_KEYS = [
|
|
161
|
+
{
|
|
162
|
+
path: ["tokens"],
|
|
163
|
+
keys: [
|
|
164
|
+
"include",
|
|
165
|
+
"sources",
|
|
166
|
+
"packages",
|
|
167
|
+
"aliases",
|
|
168
|
+
"upstream",
|
|
169
|
+
"exclude",
|
|
170
|
+
"themeSelectors",
|
|
171
|
+
"enabled",
|
|
172
|
+
"format",
|
|
173
|
+
"namespace",
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
path: ["screenshots"],
|
|
178
|
+
keys: ["viewport", "threshold", "delay", "outputDir", "themes"],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
path: ["service"],
|
|
182
|
+
keys: ["poolSize", "idleTimeout"],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
path: ["registry"],
|
|
186
|
+
keys: ["requireStory", "publicOnly", "categoryDepth", "includeProps", "embedFragments"],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
path: ["govern", "tailwind"],
|
|
190
|
+
keys: ["palette"],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
path: ["govern", "agent"],
|
|
194
|
+
keys: ["repairOrder"],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
path: ["govern", "ci"],
|
|
198
|
+
keys: ["failOnWarnings"],
|
|
199
|
+
},
|
|
200
|
+
] as const;
|
|
201
|
+
|
|
202
|
+
function objectRecord(value: unknown): Record<string, unknown> | undefined {
|
|
203
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
204
|
+
return value as Record<string, unknown>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function valueAtPath(value: unknown, path: readonly string[]): unknown {
|
|
208
|
+
let current = value;
|
|
209
|
+
for (const segment of path) {
|
|
210
|
+
const record = objectRecord(current);
|
|
211
|
+
if (!record) return undefined;
|
|
212
|
+
current = record[segment];
|
|
213
|
+
}
|
|
214
|
+
return current;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function configPath(path: readonly (string | number)[]): string {
|
|
218
|
+
return path.reduce<string>(
|
|
219
|
+
(output, segment) =>
|
|
220
|
+
typeof segment === "number"
|
|
221
|
+
? `${output}[${segment}]`
|
|
222
|
+
: output
|
|
223
|
+
? `${output}.${segment}`
|
|
224
|
+
: segment,
|
|
225
|
+
""
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function unconsumedKeyDiagnostic(path: readonly (string | number)[]): InertConfigDiagnostic {
|
|
230
|
+
const renderedPath = configPath(path);
|
|
231
|
+
return {
|
|
232
|
+
code: "FUI9004",
|
|
233
|
+
kind: "unconsumed-key",
|
|
234
|
+
severity: "warn",
|
|
235
|
+
path: renderedPath,
|
|
236
|
+
message: `${renderedPath} is not consumed by Fragments and has no effect. Remove it or use a supported config key.`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function collectStrippedKeys(
|
|
241
|
+
authored: unknown,
|
|
242
|
+
parsed: unknown,
|
|
243
|
+
path: readonly (string | number)[],
|
|
244
|
+
diagnostics: InertConfigDiagnostic[]
|
|
245
|
+
): void {
|
|
246
|
+
if (Array.isArray(authored)) {
|
|
247
|
+
if (!Array.isArray(parsed)) return;
|
|
248
|
+
authored.forEach((item, index) => {
|
|
249
|
+
collectStrippedKeys(item, parsed[index], [...path, index], diagnostics);
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const authoredRecord = objectRecord(authored);
|
|
255
|
+
const parsedRecord = objectRecord(parsed);
|
|
256
|
+
if (!authoredRecord || !parsedRecord) return;
|
|
257
|
+
|
|
258
|
+
for (const key of Object.keys(authoredRecord).sort()) {
|
|
259
|
+
if (!Object.prototype.hasOwnProperty.call(parsedRecord, key)) {
|
|
260
|
+
diagnostics.push(unconsumedKeyDiagnostic([...path, key]));
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
collectStrippedKeys(authoredRecord[key], parsedRecord[key], [...path, key], diagnostics);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function collectPassthroughKeys(
|
|
268
|
+
authored: unknown,
|
|
269
|
+
path: readonly string[],
|
|
270
|
+
allowed: ReadonlySet<string>,
|
|
271
|
+
diagnostics: InertConfigDiagnostic[]
|
|
272
|
+
): void {
|
|
273
|
+
const record = objectRecord(valueAtPath(authored, path));
|
|
274
|
+
if (!record) return;
|
|
275
|
+
for (const key of Object.keys(record).sort()) {
|
|
276
|
+
if (!allowed.has(key)) diagnostics.push(unconsumedKeyDiagnostic([...path, key]));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function collectRuleConfigKeys(
|
|
281
|
+
authored: unknown,
|
|
282
|
+
path: readonly string[],
|
|
283
|
+
diagnostics: InertConfigDiagnostic[]
|
|
284
|
+
): void {
|
|
285
|
+
const rules = objectRecord(valueAtPath(authored, path));
|
|
286
|
+
if (!rules) return;
|
|
287
|
+
for (const ruleId of Object.keys(rules).sort()) {
|
|
288
|
+
if (!RECOGNIZED_RULE_IDS.has(ruleId)) {
|
|
289
|
+
diagnostics.push(unconsumedKeyDiagnostic([...path, ruleId]));
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const config = objectRecord(rules[ruleId]);
|
|
293
|
+
if (!config) continue;
|
|
294
|
+
for (const key of Object.keys(config).sort()) {
|
|
295
|
+
if (!RULE_CONFIG_KEYS.has(key)) {
|
|
296
|
+
diagnostics.push(unconsumedKeyDiagnostic([...path, ruleId, key]));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function collectPassthroughRecordValues(
|
|
303
|
+
authored: unknown,
|
|
304
|
+
path: readonly string[],
|
|
305
|
+
allowed: ReadonlySet<string>,
|
|
306
|
+
diagnostics: InertConfigDiagnostic[]
|
|
307
|
+
): void {
|
|
308
|
+
const entries = objectRecord(valueAtPath(authored, path));
|
|
309
|
+
if (!entries) return;
|
|
310
|
+
for (const [entryName, value] of Object.entries(entries).sort(([left], [right]) =>
|
|
311
|
+
left.localeCompare(right)
|
|
312
|
+
)) {
|
|
313
|
+
const record = objectRecord(value);
|
|
314
|
+
if (!record) continue;
|
|
315
|
+
for (const key of Object.keys(record).sort()) {
|
|
316
|
+
if (!allowed.has(key)) {
|
|
317
|
+
diagnostics.push(unconsumedKeyDiagnostic([...path, entryName, key]));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function stableConfigDiagnostics(
|
|
324
|
+
diagnostics: readonly InertConfigDiagnostic[]
|
|
325
|
+
): InertConfigDiagnostic[] {
|
|
326
|
+
const unique = new Map<string, InertConfigDiagnostic>();
|
|
327
|
+
for (const diagnostic of diagnostics) {
|
|
328
|
+
unique.set(`${diagnostic.code}\0${diagnostic.path}`, diagnostic);
|
|
329
|
+
}
|
|
330
|
+
return [...unique.values()].sort(
|
|
331
|
+
(left, right) =>
|
|
332
|
+
left.path.localeCompare(right.path) ||
|
|
333
|
+
left.code.localeCompare(right.code) ||
|
|
334
|
+
left.message.localeCompare(right.message)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Diagnose config keys that the permissive validation boundary accepts but the
|
|
340
|
+
* runtime cannot consume. The parsed config is the source of truth for stripped
|
|
341
|
+
* keys; explicit allow-sets cover intentional `.passthrough()`/record seams.
|
|
342
|
+
*/
|
|
343
|
+
export function detectUnconsumedConfigKeys(
|
|
344
|
+
authoredConfig: unknown,
|
|
345
|
+
parsedConfig: unknown
|
|
346
|
+
): InertConfigDiagnostic[] {
|
|
347
|
+
const diagnostics: InertConfigDiagnostic[] = [];
|
|
348
|
+
collectStrippedKeys(authoredConfig, parsedConfig, [], diagnostics);
|
|
349
|
+
|
|
350
|
+
for (const boundary of PASSTHROUGH_KEYS) {
|
|
351
|
+
collectPassthroughKeys(
|
|
352
|
+
authoredConfig,
|
|
353
|
+
boundary.path,
|
|
354
|
+
new Set<string>(boundary.keys),
|
|
355
|
+
diagnostics
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
collectRuleConfigKeys(authoredConfig, ["govern", "rules"], diagnostics);
|
|
360
|
+
collectPassthroughRecordValues(
|
|
361
|
+
authoredConfig,
|
|
362
|
+
["govern", "agents"],
|
|
363
|
+
new Set(["rules"]),
|
|
364
|
+
diagnostics
|
|
365
|
+
);
|
|
366
|
+
collectPassthroughKeys(authoredConfig, ["govern", "audit"], new Set(), diagnostics);
|
|
367
|
+
collectPassthroughRecordValues(authoredConfig, ["govern", "runners"], new Set(), diagnostics);
|
|
368
|
+
|
|
369
|
+
return stableConfigDiagnostics(diagnostics);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
interface ScaleBinding {
|
|
373
|
+
label: string;
|
|
374
|
+
mechanism: string;
|
|
375
|
+
scale: string;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function effectiveScaleBindings(policy: GovernanceConfig | undefined): ScaleBinding[] {
|
|
379
|
+
const bindings: ScaleBinding[] = [];
|
|
380
|
+
for (const style of policy?.styles ?? []) {
|
|
381
|
+
if (style.kind === "style.rawSpacing.mustMatchScale") {
|
|
382
|
+
bindings.push({
|
|
383
|
+
label: "Spacing properties",
|
|
384
|
+
mechanism: "style.rawSpacing.mustMatchScale",
|
|
385
|
+
scale: style.scale,
|
|
386
|
+
});
|
|
387
|
+
} else if (style.kind === "style.fontSize.mustMatchScale") {
|
|
388
|
+
bindings.push({
|
|
389
|
+
label: "Font-size properties",
|
|
390
|
+
mechanism: "style.fontSize.mustMatchScale",
|
|
391
|
+
scale: style.scale,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return bindings;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Compare locally declared scales with the effective property-policy graph.
|
|
400
|
+
* Preset-only scales are not diagnosed because the user did not declare them.
|
|
401
|
+
*/
|
|
402
|
+
export function detectOrphanGovernanceScales(
|
|
403
|
+
declaredPolicy: GovernanceConfig | undefined,
|
|
404
|
+
effectivePolicy: GovernanceConfig | undefined
|
|
405
|
+
): InertConfigDiagnostic[] {
|
|
406
|
+
const bindings = effectiveScaleBindings(effectivePolicy);
|
|
407
|
+
const referenced = new Set(bindings.map((binding) => binding.scale));
|
|
408
|
+
const diagnostics: InertConfigDiagnostic[] = [];
|
|
409
|
+
|
|
410
|
+
for (const scale of Object.keys(declaredPolicy?.scales ?? {}).sort()) {
|
|
411
|
+
if (referenced.has(scale)) continue;
|
|
412
|
+
const path = `govern.scales.${scale}`;
|
|
413
|
+
const preferred =
|
|
414
|
+
(scale.toLowerCase().includes("spac")
|
|
415
|
+
? bindings.find((binding) => binding.mechanism === "style.rawSpacing.mustMatchScale")
|
|
416
|
+
: undefined) ??
|
|
417
|
+
(scale.toLowerCase().includes("font")
|
|
418
|
+
? bindings.find((binding) => binding.mechanism === "style.fontSize.mustMatchScale")
|
|
419
|
+
: undefined) ??
|
|
420
|
+
bindings[0];
|
|
421
|
+
const remediation = preferred
|
|
422
|
+
? `${preferred.label} are bound to the scale named "${preferred.scale}" — rename the key to "${preferred.scale}" or bind it via ${preferred.mechanism}.`
|
|
423
|
+
: `Bind it via style.rawSpacing.mustMatchScale or style.fontSize.mustMatchScale, or remove it.`;
|
|
424
|
+
diagnostics.push({
|
|
425
|
+
code: "FUI9005",
|
|
426
|
+
kind: "orphan-scale",
|
|
427
|
+
severity: "warn",
|
|
428
|
+
path,
|
|
429
|
+
message: `${path} is not referenced by any property policy. ${remediation}`,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return stableConfigDiagnostics(diagnostics);
|
|
434
|
+
}
|
|
435
|
+
|
|
134
436
|
function summarize(
|
|
135
437
|
status: GovernanceIntegrityStatus,
|
|
136
438
|
flags: { componentsArmed: boolean; tokensArmed: boolean; blockingArmed: boolean }
|
|
@@ -152,6 +454,7 @@ export function evaluateGovernanceIntegrity(
|
|
|
152
454
|
input: GovernanceIntegrityInput
|
|
153
455
|
): GovernanceIntegrityVerdict {
|
|
154
456
|
const configs = effectiveRuleConfigs(input.policy);
|
|
457
|
+
const configDiagnostics = stableConfigDiagnostics(input.configDiagnostics ?? []);
|
|
155
458
|
|
|
156
459
|
// --- policy family --------------------------------------------------------
|
|
157
460
|
const policyArmed = input.policy !== undefined && input.policySource !== "none";
|
|
@@ -166,23 +469,29 @@ export function evaluateGovernanceIntegrity(
|
|
|
166
469
|
|
|
167
470
|
// --- components family ----------------------------------------------------
|
|
168
471
|
const componentsConfig = configs.get("components/prefer-library");
|
|
472
|
+
const confirmedBridges = input.policy?.canonicalBridges ?? [];
|
|
169
473
|
const componentsArmed =
|
|
170
|
-
|
|
474
|
+
confirmedBridges.length > 0 ||
|
|
475
|
+
(componentsConfig?.enabled === true &&
|
|
476
|
+
hasEffectiveComponentVocabulary(componentsConfig.options));
|
|
171
477
|
const componentsFamily: GovernanceIntegrityFamily = {
|
|
172
478
|
id: "components",
|
|
173
479
|
armed: componentsArmed,
|
|
174
|
-
rules: [
|
|
480
|
+
rules: [
|
|
481
|
+
...(confirmedBridges.length > 0 ? ["imports/preferred-path"] : []),
|
|
482
|
+
...(componentsConfig?.enabled === true ? ["components/prefer-library"] : []),
|
|
483
|
+
],
|
|
175
484
|
};
|
|
176
485
|
if (!componentsArmed) {
|
|
177
486
|
if (componentsConfig?.enabled === true) {
|
|
178
487
|
componentsFamily.reason =
|
|
179
488
|
"components/prefer-library enabled but no effective canonical source";
|
|
180
489
|
componentsFamily.remediation =
|
|
181
|
-
"add govern.
|
|
490
|
+
"add govern.canonicalBridges for local wrappers, govern.canonicalSources, or designSystem.path/packageName";
|
|
182
491
|
} else {
|
|
183
492
|
componentsFamily.reason = "components/prefer-library not enabled";
|
|
184
493
|
componentsFamily.remediation =
|
|
185
|
-
"add govern.
|
|
494
|
+
"add govern.canonicalBridges for local wrappers, govern.canonicalSources, or designSystem.path/packageName";
|
|
186
495
|
}
|
|
187
496
|
}
|
|
188
497
|
|
|
@@ -274,6 +583,17 @@ export function evaluateGovernanceIntegrity(
|
|
|
274
583
|
.filter((remediation): remediation is string => remediation !== undefined)
|
|
275
584
|
);
|
|
276
585
|
const summary = summarize(status, { componentsArmed, tokensArmed, blockingArmed });
|
|
586
|
+
const configuredRuleCount = [...configs.keys()].filter((ruleId) =>
|
|
587
|
+
CONSUMED_RULE_IDS.has(ruleId)
|
|
588
|
+
).length;
|
|
589
|
+
const activeRuleCount = [...configs.entries()].filter(
|
|
590
|
+
([ruleId, config]) => CONSUMED_RULE_IDS.has(ruleId) && config.enabled
|
|
591
|
+
).length;
|
|
592
|
+
const roster = {
|
|
593
|
+
configured: configuredRuleCount + configDiagnostics.length,
|
|
594
|
+
active: activeRuleCount,
|
|
595
|
+
inert: configDiagnostics.length,
|
|
596
|
+
};
|
|
277
597
|
|
|
278
598
|
return {
|
|
279
599
|
status,
|
|
@@ -284,5 +604,7 @@ export function evaluateGovernanceIntegrity(
|
|
|
284
604
|
armed,
|
|
285
605
|
summary,
|
|
286
606
|
remediations,
|
|
607
|
+
configDiagnostics,
|
|
608
|
+
roster,
|
|
287
609
|
};
|
|
288
610
|
}
|
package/src/governance.test.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
compileFragment,
|
|
4
|
+
configDeclarationForDiagnostics,
|
|
5
|
+
defineConfig,
|
|
6
|
+
defineFragment,
|
|
7
|
+
g,
|
|
8
|
+
type FragmentsConfig,
|
|
9
|
+
} from "./index.js";
|
|
3
10
|
|
|
4
11
|
type ButtonProps = {
|
|
5
12
|
variant?: "primary" | "secondary" | "ghost" | "link";
|
|
@@ -12,6 +19,18 @@ function Button(_props: ButtonProps) {
|
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
describe("governance DSL", () => {
|
|
22
|
+
it("preserves the authored declaration for stripped-key diagnostics", () => {
|
|
23
|
+
const authored = {
|
|
24
|
+
include: ["src/**/*.fragment.ts"],
|
|
25
|
+
styles: { spacing: true },
|
|
26
|
+
};
|
|
27
|
+
const config = defineConfig(authored as FragmentsConfig);
|
|
28
|
+
|
|
29
|
+
expect(config).not.toHaveProperty("styles");
|
|
30
|
+
expect(configDeclarationForDiagnostics(config)).toBe(authored);
|
|
31
|
+
expect(Object.keys(config)).toEqual(["include"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
15
34
|
it("defineConfig accepts global governance records", () => {
|
|
16
35
|
const config = defineConfig({
|
|
17
36
|
include: ["src/**/*.fragment.ts"],
|
|
@@ -139,6 +158,73 @@ describe("governance DSL", () => {
|
|
|
139
158
|
});
|
|
140
159
|
});
|
|
141
160
|
|
|
161
|
+
it("accepts one confirmed package-to-local canonical bridge", () => {
|
|
162
|
+
const config = defineConfig({
|
|
163
|
+
govern: {
|
|
164
|
+
canonicalBridges: [
|
|
165
|
+
{
|
|
166
|
+
underlying: { packageName: "@mui/material", exportName: "Button" },
|
|
167
|
+
local: {
|
|
168
|
+
componentKey: "src/components/Button.tsx#Button",
|
|
169
|
+
moduleSpecifier: "@/components",
|
|
170
|
+
exportName: "Button",
|
|
171
|
+
implementationFiles: ["src/components/Button.tsx"],
|
|
172
|
+
},
|
|
173
|
+
decision: { state: "confirmed", source: "authored" },
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(config.govern?.canonicalBridges?.[0]).toMatchObject({
|
|
180
|
+
underlying: { packageName: "@mui/material", exportName: "Button" },
|
|
181
|
+
local: { moduleSpecifier: "@/components", exportName: "Button" },
|
|
182
|
+
decision: { state: "confirmed", source: "authored" },
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("rejects ambiguous or unsafe canonical bridge declarations", () => {
|
|
187
|
+
const bridge = {
|
|
188
|
+
underlying: { packageName: "@mui/material", exportName: "Button" },
|
|
189
|
+
local: {
|
|
190
|
+
componentKey: "src/components/Button.tsx#Button",
|
|
191
|
+
moduleSpecifier: "@/components",
|
|
192
|
+
exportName: "Button",
|
|
193
|
+
implementationFiles: ["src/components/Button.tsx"],
|
|
194
|
+
},
|
|
195
|
+
decision: { state: "confirmed" as const, source: "authored" as const },
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
expect(() => defineConfig({ govern: { canonicalBridges: [bridge, bridge] } })).toThrow(
|
|
199
|
+
"Invalid fragments config"
|
|
200
|
+
);
|
|
201
|
+
expect(() =>
|
|
202
|
+
defineConfig({
|
|
203
|
+
govern: {
|
|
204
|
+
canonicalBridges: [
|
|
205
|
+
bridge,
|
|
206
|
+
{
|
|
207
|
+
...bridge,
|
|
208
|
+
underlying: { packageName: "@radix-ui/react-slot", exportName: "Slot" },
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
).toThrow("Invalid fragments config");
|
|
214
|
+
expect(() =>
|
|
215
|
+
defineConfig({
|
|
216
|
+
govern: {
|
|
217
|
+
canonicalBridges: [
|
|
218
|
+
{
|
|
219
|
+
...bridge,
|
|
220
|
+
local: { ...bridge.local, moduleSpecifier: "../components" },
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
).toThrow("Invalid fragments config");
|
|
226
|
+
});
|
|
227
|
+
|
|
142
228
|
it("preserves top-level component identity decisions", () => {
|
|
143
229
|
const config = defineConfig({
|
|
144
230
|
identity: {
|
package/src/governance.ts
CHANGED
|
@@ -192,6 +192,79 @@ const canonicalSourcePathSchema = z
|
|
|
192
192
|
|
|
193
193
|
const canonicalSourceRegistryHashSchema = z.string().regex(/^[0-9a-f]{64}$/);
|
|
194
194
|
|
|
195
|
+
const canonicalBridgeModuleSpecifierSchema = z
|
|
196
|
+
.string()
|
|
197
|
+
.min(1)
|
|
198
|
+
.superRefine((value, ctx) => {
|
|
199
|
+
const normalized = value.replace(/\\/gu, "/");
|
|
200
|
+
if (normalized.startsWith("/") || /^[A-Za-z]:\//u.test(normalized)) {
|
|
201
|
+
ctx.addIssue({
|
|
202
|
+
code: z.ZodIssueCode.custom,
|
|
203
|
+
message: "Canonical bridge moduleSpecifier must be a portable import specifier",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (normalized.split("/").includes("..")) {
|
|
207
|
+
ctx.addIssue({
|
|
208
|
+
code: z.ZodIssueCode.custom,
|
|
209
|
+
message: "Canonical bridge moduleSpecifier must not traverse outside the project",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
export const canonicalBridgeV1Schema = z
|
|
215
|
+
.object({
|
|
216
|
+
underlying: z
|
|
217
|
+
.object({
|
|
218
|
+
packageName: z.string().min(1),
|
|
219
|
+
exportName: z.string().min(1),
|
|
220
|
+
})
|
|
221
|
+
.strict(),
|
|
222
|
+
local: z
|
|
223
|
+
.object({
|
|
224
|
+
componentKey: z.string().min(1),
|
|
225
|
+
moduleSpecifier: canonicalBridgeModuleSpecifierSchema,
|
|
226
|
+
exportName: z.string().min(1),
|
|
227
|
+
implementationFiles: z.array(canonicalSourcePathSchema).min(1),
|
|
228
|
+
})
|
|
229
|
+
.strict(),
|
|
230
|
+
decision: z
|
|
231
|
+
.object({
|
|
232
|
+
state: z.literal("confirmed"),
|
|
233
|
+
source: z.enum(["authored", "migration"]),
|
|
234
|
+
})
|
|
235
|
+
.strict(),
|
|
236
|
+
})
|
|
237
|
+
.strict();
|
|
238
|
+
|
|
239
|
+
const canonicalBridgesV1Schema = z.array(canonicalBridgeV1Schema).superRefine((bridges, ctx) => {
|
|
240
|
+
const underlying = new Map<string, number>();
|
|
241
|
+
const componentKeys = new Map<string, number>();
|
|
242
|
+
for (const [index, bridge] of bridges.entries()) {
|
|
243
|
+
const underlyingKey = `${bridge.underlying.packageName}\u0000${bridge.underlying.exportName}`;
|
|
244
|
+
const previousUnderlying = underlying.get(underlyingKey);
|
|
245
|
+
if (previousUnderlying !== undefined) {
|
|
246
|
+
ctx.addIssue({
|
|
247
|
+
code: z.ZodIssueCode.custom,
|
|
248
|
+
message: `Duplicate confirmed bridge target for ${bridge.underlying.packageName}#${bridge.underlying.exportName} (also at canonicalBridges.${previousUnderlying})`,
|
|
249
|
+
path: [index, "underlying"],
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
underlying.set(underlyingKey, index);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const previousComponent = componentKeys.get(bridge.local.componentKey);
|
|
256
|
+
if (previousComponent !== undefined) {
|
|
257
|
+
ctx.addIssue({
|
|
258
|
+
code: z.ZodIssueCode.custom,
|
|
259
|
+
message: `Local component key overlaps canonicalBridges.${previousComponent}`,
|
|
260
|
+
path: [index, "local", "componentKey"],
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
componentKeys.set(bridge.local.componentKey, index);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
195
268
|
const canonicalSourceSchema = z.discriminatedUnion("kind", [
|
|
196
269
|
z.object({
|
|
197
270
|
kind: z.literal("npm"),
|
|
@@ -237,6 +310,7 @@ export const governanceConfigSchema = z
|
|
|
237
310
|
audit: z.object({}).passthrough().optional(),
|
|
238
311
|
runners: z.record(z.string(), z.object({}).passthrough()).optional(),
|
|
239
312
|
canonicalSources: z.array(canonicalSourceSchema).optional(),
|
|
313
|
+
canonicalBridges: canonicalBridgesV1Schema.optional(),
|
|
240
314
|
presets: z.array(z.string()).optional(),
|
|
241
315
|
scales: z.record(z.string(), scaleGovernanceRecordSchema).optional(),
|
|
242
316
|
styles: z.array(globalStyleGovernanceRecordSchema).optional(),
|
|
@@ -278,19 +352,58 @@ export type ComponentPolicyMatch = z.infer<typeof componentPolicyMatchSchema>;
|
|
|
278
352
|
export type ComponentPolicyRecord = z.infer<typeof componentPolicyRecordSchema>;
|
|
279
353
|
export type ComponentPolicyOverride = z.infer<typeof componentPolicyOverrideSchema>;
|
|
280
354
|
export type CanonicalSource = z.infer<typeof canonicalSourceSchema>;
|
|
355
|
+
export type CanonicalBridgeV1 = z.infer<typeof canonicalBridgeV1Schema>;
|
|
281
356
|
|
|
282
357
|
export interface GovernanceConfig {
|
|
358
|
+
/** Shared governance config modules to extend before applying this file's declarations. */
|
|
283
359
|
extends?: string[];
|
|
360
|
+
|
|
361
|
+
/** Default severity for governance rules that do not declare their own severity. */
|
|
284
362
|
severity?: GovernanceSeverity;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Rule-id keyed enablement and severity overrides. Only fields consumed by the named
|
|
366
|
+
* rule are valid; unsupported fields are reported as inert config.
|
|
367
|
+
*/
|
|
285
368
|
rules?: Record<string, unknown>;
|
|
369
|
+
|
|
370
|
+
/** Agent-id keyed rule overrides for supported agent-specific governance policies. */
|
|
286
371
|
agents?: Record<string, { rules?: Record<string, unknown> }>;
|
|
372
|
+
|
|
373
|
+
/** Reserved audit compatibility object; undeclared child keys are reported as inert. */
|
|
287
374
|
audit?: Record<string, unknown>;
|
|
375
|
+
|
|
376
|
+
/** Reserved runner compatibility map; undeclared child keys are reported as inert. */
|
|
288
377
|
runners?: Record<string, Record<string, unknown>>;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Canonical component authorities: npm packages, repository directories, or registry
|
|
381
|
+
* receipts whose included exports arm canonical-component rules.
|
|
382
|
+
*/
|
|
289
383
|
canonicalSources?: CanonicalSource[];
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Confirmed mappings from an underlying library export to the approved local wrapper.
|
|
387
|
+
* The wrapper's implementationFiles scope permits its direct underlying import.
|
|
388
|
+
*/
|
|
389
|
+
canonicalBridges?: CanonicalBridgeV1[];
|
|
390
|
+
|
|
391
|
+
/** Versioned governance presets to resolve before applying local rule overrides. */
|
|
290
392
|
presets?: string[];
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Named numeric scales. Spacing rules bind through
|
|
396
|
+
* style.rawSpacing.mustMatchScale; the built-in spacing policy references `space`.
|
|
397
|
+
*/
|
|
291
398
|
scales?: Record<string, ScaleGovernanceRecord>;
|
|
399
|
+
|
|
400
|
+
/** Legacy typed style-policy records, normalized into the active rule policy. */
|
|
292
401
|
styles?: GlobalStyleGovernanceRecord[];
|
|
402
|
+
|
|
403
|
+
/** Legacy typed JSX-policy records, normalized into the active rule policy. */
|
|
293
404
|
jsx?: GlobalJsxGovernanceRecord[];
|
|
405
|
+
|
|
406
|
+
/** Tailwind palette allow/deny policy used by Tailwind governance rules. */
|
|
294
407
|
tailwind?: {
|
|
295
408
|
palette?: {
|
|
296
409
|
allow?: string[];
|
|
@@ -298,12 +411,20 @@ export interface GovernanceConfig {
|
|
|
298
411
|
};
|
|
299
412
|
[key: string]: unknown;
|
|
300
413
|
};
|
|
414
|
+
|
|
415
|
+
/** Agent repair-order guidance consumed when presenting deterministic fixes. */
|
|
301
416
|
agent?: {
|
|
302
417
|
repairOrder?: string[];
|
|
303
418
|
[key: string]: unknown;
|
|
304
419
|
};
|
|
420
|
+
|
|
421
|
+
/** Component-keyed governance records for canonical component metadata and prop policy. */
|
|
305
422
|
components?: Record<string, ComponentPolicyRecord>;
|
|
423
|
+
|
|
424
|
+
/** Ordered component-policy overrides selected by component identity fields. */
|
|
306
425
|
overrides?: ComponentPolicyOverride[];
|
|
426
|
+
|
|
427
|
+
/** Governance CI rendering options, including whether warnings fail the CI verdict. */
|
|
307
428
|
ci?: {
|
|
308
429
|
failOnWarnings?: boolean;
|
|
309
430
|
[key: string]: unknown;
|