@usefragments/core 1.5.1 → 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-AOG4FTV6.js → chunk-BAHCOAVG.js} +26 -2
- package/dist/chunk-BAHCOAVG.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-B88uR3Zq.d.ts → governance-pKrfh517.d.ts} +46 -3
- package/dist/index.d.ts +46 -3
- package/dist/index.js +236 -7
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/codes/__tests__/codes.test.ts +1 -1
- package/src/codes/codes.ts +20 -0
- package/src/config.ts +20 -0
- package/src/facts/fact-index.ts +8 -1
- package/src/facts/facts.test.ts +14 -18
- package/src/governance-integrity.test.ts +105 -0
- package/src/governance-integrity.ts +316 -0
- package/src/governance.test.ts +20 -1
- package/src/governance.ts +45 -0
- package/src/index.ts +7 -1
- package/src/rules/styles-no-raw-spacing.test.ts +48 -0
- package/src/rules/styles-no-raw-spacing.ts +5 -5
- package/src/types.ts +21 -3
- package/dist/chunk-AOG4FTV6.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";
|
|
@@ -280,6 +583,17 @@ export function evaluateGovernanceIntegrity(
|
|
|
280
583
|
.filter((remediation): remediation is string => remediation !== undefined)
|
|
281
584
|
);
|
|
282
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
|
+
};
|
|
283
597
|
|
|
284
598
|
return {
|
|
285
599
|
status,
|
|
@@ -290,5 +604,7 @@ export function evaluateGovernanceIntegrity(
|
|
|
290
604
|
armed,
|
|
291
605
|
summary,
|
|
292
606
|
remediations,
|
|
607
|
+
configDiagnostics,
|
|
608
|
+
roster,
|
|
293
609
|
};
|
|
294
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"],
|
package/src/governance.ts
CHANGED
|
@@ -355,18 +355,55 @@ export type CanonicalSource = z.infer<typeof canonicalSourceSchema>;
|
|
|
355
355
|
export type CanonicalBridgeV1 = z.infer<typeof canonicalBridgeV1Schema>;
|
|
356
356
|
|
|
357
357
|
export interface GovernanceConfig {
|
|
358
|
+
/** Shared governance config modules to extend before applying this file's declarations. */
|
|
358
359
|
extends?: string[];
|
|
360
|
+
|
|
361
|
+
/** Default severity for governance rules that do not declare their own severity. */
|
|
359
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
|
+
*/
|
|
360
368
|
rules?: Record<string, unknown>;
|
|
369
|
+
|
|
370
|
+
/** Agent-id keyed rule overrides for supported agent-specific governance policies. */
|
|
361
371
|
agents?: Record<string, { rules?: Record<string, unknown> }>;
|
|
372
|
+
|
|
373
|
+
/** Reserved audit compatibility object; undeclared child keys are reported as inert. */
|
|
362
374
|
audit?: Record<string, unknown>;
|
|
375
|
+
|
|
376
|
+
/** Reserved runner compatibility map; undeclared child keys are reported as inert. */
|
|
363
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
|
+
*/
|
|
364
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
|
+
*/
|
|
365
389
|
canonicalBridges?: CanonicalBridgeV1[];
|
|
390
|
+
|
|
391
|
+
/** Versioned governance presets to resolve before applying local rule overrides. */
|
|
366
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
|
+
*/
|
|
367
398
|
scales?: Record<string, ScaleGovernanceRecord>;
|
|
399
|
+
|
|
400
|
+
/** Legacy typed style-policy records, normalized into the active rule policy. */
|
|
368
401
|
styles?: GlobalStyleGovernanceRecord[];
|
|
402
|
+
|
|
403
|
+
/** Legacy typed JSX-policy records, normalized into the active rule policy. */
|
|
369
404
|
jsx?: GlobalJsxGovernanceRecord[];
|
|
405
|
+
|
|
406
|
+
/** Tailwind palette allow/deny policy used by Tailwind governance rules. */
|
|
370
407
|
tailwind?: {
|
|
371
408
|
palette?: {
|
|
372
409
|
allow?: string[];
|
|
@@ -374,12 +411,20 @@ export interface GovernanceConfig {
|
|
|
374
411
|
};
|
|
375
412
|
[key: string]: unknown;
|
|
376
413
|
};
|
|
414
|
+
|
|
415
|
+
/** Agent repair-order guidance consumed when presenting deterministic fixes. */
|
|
377
416
|
agent?: {
|
|
378
417
|
repairOrder?: string[];
|
|
379
418
|
[key: string]: unknown;
|
|
380
419
|
};
|
|
420
|
+
|
|
421
|
+
/** Component-keyed governance records for canonical component metadata and prop policy. */
|
|
381
422
|
components?: Record<string, ComponentPolicyRecord>;
|
|
423
|
+
|
|
424
|
+
/** Ordered component-policy overrides selected by component identity fields. */
|
|
382
425
|
overrides?: ComponentPolicyOverride[];
|
|
426
|
+
|
|
427
|
+
/** Governance CI rendering options, including whether warnings fail the CI verdict. */
|
|
383
428
|
ci?: {
|
|
384
429
|
failOnWarnings?: boolean;
|
|
385
430
|
[key: string]: unknown;
|
package/src/index.ts
CHANGED
|
@@ -425,7 +425,7 @@ export {
|
|
|
425
425
|
} from "./schema.js";
|
|
426
426
|
|
|
427
427
|
// Main API
|
|
428
|
-
export { defineConfig } from "./config.js";
|
|
428
|
+
export { configDeclarationForDiagnostics, defineConfig } from "./config.js";
|
|
429
429
|
export {
|
|
430
430
|
defineFragment,
|
|
431
431
|
compileFragment,
|
|
@@ -713,6 +713,8 @@ export { BLOCKING_RULE_ALLOWLIST, gatesCi, isDenyEligible } from "./rules/index.
|
|
|
713
713
|
// Governance integrity — armed-vs-declared verdict over a fully-resolved policy.
|
|
714
714
|
// Encodes "enabled ≠ armed": a rule with no vocabulary enforces nothing.
|
|
715
715
|
export {
|
|
716
|
+
detectOrphanGovernanceScales,
|
|
717
|
+
detectUnconsumedConfigKeys,
|
|
716
718
|
evaluateGovernanceIntegrity,
|
|
717
719
|
hasEffectiveComponentVocabulary,
|
|
718
720
|
isEffectiveCanonicalSource,
|
|
@@ -721,8 +723,12 @@ export type {
|
|
|
721
723
|
GovernanceIntegrityFamily,
|
|
722
724
|
GovernanceIntegrityFamilyId,
|
|
723
725
|
GovernanceIntegrityInput,
|
|
726
|
+
GovernanceIntegrityRoster,
|
|
724
727
|
GovernanceIntegrityStatus,
|
|
725
728
|
GovernanceIntegrityVerdict,
|
|
729
|
+
InertConfigDiagnostic,
|
|
730
|
+
InertConfigDiagnosticCode,
|
|
731
|
+
InertConfigDiagnosticKind,
|
|
726
732
|
} from "./governance-integrity.js";
|
|
727
733
|
|
|
728
734
|
export {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
compileGlobalGovernanceFacts,
|
|
5
|
+
FactIndex,
|
|
6
|
+
g,
|
|
7
|
+
makeStyleDeclarationFact,
|
|
8
|
+
ruleStylesNoRawSpacing,
|
|
9
|
+
} from "../index.js";
|
|
10
|
+
|
|
11
|
+
describe("styles/no-raw-spacing fork copy", () => {
|
|
12
|
+
it("names the active scale and its config lever without changing machine attributes", () => {
|
|
13
|
+
const ix = new FactIndex();
|
|
14
|
+
ix.addMany(
|
|
15
|
+
compileGlobalGovernanceFacts({
|
|
16
|
+
scales: {
|
|
17
|
+
space: g.scale.px([0, 4, 8, 12, 16, 20, 24]),
|
|
18
|
+
},
|
|
19
|
+
styles: [
|
|
20
|
+
g.styles.rawSpacing().mustMatchScale("space", {
|
|
21
|
+
appliesTo: ["margin-top"],
|
|
22
|
+
severity: "warn",
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
ix.add(
|
|
28
|
+
makeStyleDeclarationFact({
|
|
29
|
+
file: "src/Card.css",
|
|
30
|
+
selector: ".card",
|
|
31
|
+
declarationPath: "0",
|
|
32
|
+
property: "margin-top",
|
|
33
|
+
value: "10px",
|
|
34
|
+
location: { file: "src/Card.css", line: 2, column: 3 },
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const [finding] = ruleStylesNoRawSpacing(ix);
|
|
39
|
+
|
|
40
|
+
expect(finding?.message).toContain("not on the `space` scale");
|
|
41
|
+
expect(finding?.message).toContain("Configure `govern.scales.space` to change it");
|
|
42
|
+
expect(finding?.attributes).toMatchObject({
|
|
43
|
+
scale: "space",
|
|
44
|
+
rawValue: "10px",
|
|
45
|
+
source: "css",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -117,7 +117,7 @@ function checkDeclaration(
|
|
|
117
117
|
ruleId: RULE_ID,
|
|
118
118
|
ruleVersion: RULE_VERSION,
|
|
119
119
|
severity: policy.severity,
|
|
120
|
-
message: spacingMessage(decl.property, decl.value, allowed, scale
|
|
120
|
+
message: spacingMessage(decl.property, decl.value, allowed, scale, checked),
|
|
121
121
|
location: decl.location,
|
|
122
122
|
evidence: ix.evidence([decl.id, policy.id, scale.id]),
|
|
123
123
|
fingerprintIdentity: {
|
|
@@ -185,7 +185,7 @@ function checkInlineStyle(
|
|
|
185
185
|
ruleId: RULE_ID,
|
|
186
186
|
ruleVersion: RULE_VERSION,
|
|
187
187
|
severity: policy.severity,
|
|
188
|
-
message: spacingMessage(inline.property, inline.value, allowed, scale
|
|
188
|
+
message: spacingMessage(inline.property, inline.value, allowed, scale, checked),
|
|
189
189
|
location: node.location,
|
|
190
190
|
evidence: ix.evidence(evidenceIds),
|
|
191
191
|
fingerprintIdentity: {
|
|
@@ -245,7 +245,7 @@ function spacingMessage(
|
|
|
245
245
|
property: string,
|
|
246
246
|
value: string,
|
|
247
247
|
allowed: number[],
|
|
248
|
-
|
|
248
|
+
scale: ScaleFact,
|
|
249
249
|
checked: CheckedSpacingValue
|
|
250
250
|
): string {
|
|
251
251
|
if (checked.reason === "token-equivalent" && checked.matchedToken) {
|
|
@@ -255,7 +255,7 @@ function spacingMessage(
|
|
|
255
255
|
.slice()
|
|
256
256
|
.sort((a, b) => a - b)
|
|
257
257
|
.slice(0, 6);
|
|
258
|
-
const suffix = `${sample.join(unit + ", ")}${unit}`;
|
|
258
|
+
const suffix = `${sample.join(scale.unit + ", ")}${scale.unit}`;
|
|
259
259
|
const ellipsis = allowed.length > sample.length ? ", …" : "";
|
|
260
|
-
return `\`${property}: ${value}\` is not on the
|
|
260
|
+
return `\`${property}: ${value}\` is not on the \`${scale.name}\` scale. Allowed: ${suffix}${ellipsis}. Configure \`govern.scales.${scale.name}\` to change it.`;
|
|
261
261
|
}
|
package/src/types.ts
CHANGED
|
@@ -567,7 +567,10 @@ export interface TokenSourceConfig {
|
|
|
567
567
|
/** Repo-root-relative token file or glob. */
|
|
568
568
|
path: RepoRelativePath;
|
|
569
569
|
|
|
570
|
-
/**
|
|
570
|
+
/**
|
|
571
|
+
* Explicit token format, or "auto" to infer from extension/content. Use
|
|
572
|
+
* "auto" for statically analyzable TypeScript and JavaScript token modules.
|
|
573
|
+
*/
|
|
571
574
|
format?: TokenSourceFormat;
|
|
572
575
|
}
|
|
573
576
|
|
|
@@ -578,7 +581,10 @@ export interface TokenConfig {
|
|
|
578
581
|
*/
|
|
579
582
|
include?: string[];
|
|
580
583
|
|
|
581
|
-
/**
|
|
584
|
+
/**
|
|
585
|
+
* Repo-root-relative token source files/globs for monorepos and Cloud setup.
|
|
586
|
+
* Set each source to format "auto" for TypeScript or JavaScript token modules.
|
|
587
|
+
*/
|
|
582
588
|
sources?: TokenSourceConfig[];
|
|
583
589
|
|
|
584
590
|
/**
|
|
@@ -613,7 +619,10 @@ export interface TokenConfig {
|
|
|
613
619
|
/** Enable token comparison in style diffs (default: true) */
|
|
614
620
|
enabled?: boolean;
|
|
615
621
|
|
|
616
|
-
/**
|
|
622
|
+
/**
|
|
623
|
+
* Token source format detection. "auto" infers supported formats from the
|
|
624
|
+
* file extension, including statically analyzable TypeScript/JavaScript modules.
|
|
625
|
+
*/
|
|
617
626
|
format?: TokenSourceFormat;
|
|
618
627
|
|
|
619
628
|
/** Vendor namespace for Fragments extensions in DTCG files (default: 'com.usefragments') */
|
|
@@ -813,12 +822,21 @@ export interface FragmentsConfig {
|
|
|
813
822
|
|
|
814
823
|
/** Local component-identity decisions used when Cloud does not own the key. */
|
|
815
824
|
identity?: {
|
|
825
|
+
/** Authored sanctions, rejections, and dismissals keyed by portable component identity. */
|
|
816
826
|
decisions?: Array<{
|
|
817
827
|
/** Portable component key (`file#exportName`). */
|
|
818
828
|
component: string;
|
|
829
|
+
|
|
830
|
+
/** The identity disposition recorded for this component. */
|
|
819
831
|
kind: "sanction" | "reject" | "dismiss";
|
|
832
|
+
|
|
833
|
+
/** Optional canonical target (`moduleSpecifier#exportName`) for sanctions. */
|
|
820
834
|
canonicalTarget?: string;
|
|
835
|
+
|
|
836
|
+
/** Human-readable rationale stored with the decision. */
|
|
821
837
|
reason?: string;
|
|
838
|
+
|
|
839
|
+
/** Stable external identifier used to reconcile an existing decision. */
|
|
822
840
|
decisionId?: string;
|
|
823
841
|
}>;
|
|
824
842
|
};
|