@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.
@@ -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
  }
@@ -1,5 +1,12 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { compileFragment, defineConfig, defineFragment, g } from "./index.js";
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.unit, checked),
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.unit, checked),
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
- unit: "px" | "rem",
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 spacing scale. Allowed: ${suffix}${ellipsis}.`;
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
- /** Explicit token format, or "auto" to infer from extension/content. */
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
- /** Repo-root-relative token source files/globs for monorepos and Cloud setup. */
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
- /** Token source format detection ('auto' detects from file extension) */
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
  };