/** * Compile facts from the existing IR. * * Two entry points: * - `compileGlobalGovernanceFacts(govern)` turns a `GovernanceConfig` into * scale + scale_value + style_* + jsx_* policy facts. * - `compileComponentFacts(componentId, fragment)` turns a governed * fragment definition (or a compiled fragment) into component metadata, * prop metadata, capability, and component-scoped policy facts. * * Both functions are pure and produce arrays sorted only by source order; the * caller is free to add them to a `FactIndex` in any order. */ import type { ComponentGovernanceRecord, GovernanceConfig, GovernedFragmentDefinition, ResolvedGovernedFragmentDefinition, } from "../governance.js"; import { resolveComponentGovernance } from "../governance.js"; import type { CompiledFragment, PropDefinition } from "../compiled-types/index.js"; import { asComponentId } from "./ids.js"; import { makeA11yNameRequiredFact, makeComponentCapabilityFact, makeComponentMetadataFact, makeGovernanceRuleConfigFact, makeJsxComponentPreferredFact, makeJsxInlineStyleForbiddenRawFact, makeJsxImportPathPreferredFact, makeJsxUnknownPropsForbiddenFact, makePropMetadataFact, makePropValueAvoidedFact, makePropValueForbiddenFact, makeScaleFact, makeScaleValueFact, makeStyleCssVarsMustBeDefinedFact, makeStyleFontSizeScaleFact, makeStylePropertyScaleFact, makeStyleRawColorForbiddenFact, makeStyleRawDimensionForbiddenFact, makeTailwindPaletteAllowFact, makeTailwindPaletteDenyFact, makeTailwindUnknownClassEnabledFact, } from "./builders.js"; import type { ComponentId, Fact, PolicyFact } from "./types.js"; // --------------------------------------------------------------------------- // Global governance → facts // --------------------------------------------------------------------------- export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefined): PolicyFact[] { if (!govern) return []; const out: PolicyFact[] = []; if (govern.scales) { for (const [name, scale] of Object.entries(govern.scales)) { out.push( makeScaleFact({ name, unit: scale.unit, rootFontSizePx: scale.rootFontSizePx, emBasePx: scale.emBasePx, }) ); for (const value of scale.values) { out.push(makeScaleValueFact({ scale: name, value })); } } } if (govern.styles) { for (const style of govern.styles) { switch (style.kind) { case "style.rawColors.forbid": out.push( makeStyleRawColorForbiddenFact({ except: style.except, prefer: style.prefer, severity: style.severity, }) ); break; case "style.rawDimensions.forbid": out.push( makeStyleRawDimensionForbiddenFact({ appliesTo: style.appliesTo, prefer: style.prefer, severity: style.severity, }) ); break; case "style.rawSpacing.mustMatchScale": for (const property of style.appliesTo) { out.push( makeStylePropertyScaleFact({ property, scale: style.scale, severity: style.severity, }) ); } break; case "style.fontSize.mustMatchScale": out.push(makeStyleFontSizeScaleFact({ scale: style.scale, severity: style.severity })); break; case "style.cssVars.mustBeDefined": out.push(makeStyleCssVarsMustBeDefinedFact({ severity: style.severity })); break; } } } if (govern.jsx) { for (const jsx of govern.jsx) { switch (jsx.kind) { case "jsx.unknownProps.forbid": out.push(makeJsxUnknownPropsForbiddenFact({ severity: jsx.severity })); break; case "jsx.inlineStyle.forbidRaw": for (const property of jsx.properties) { out.push( makeJsxInlineStyleForbiddenRawFact({ property, severity: jsx.severity, }) ); } break; case "jsx.importPath.prefer": out.push( makeJsxImportPathPreferredFact({ from: jsx.from, to: jsx.to, imported: jsx.imported, because: jsx.because, severity: jsx.severity, }) ); break; case "jsx.component.prefer": out.push( makeJsxComponentPreferredFact({ from: asComponentId(jsx.from) as ComponentId, to: asComponentId(jsx.to) as ComponentId, because: jsx.because, severity: jsx.severity, }) ); break; } } } const tailwindPalette = govern.tailwind?.palette; const forbiddenPaletteSeverity = ruleSeverity( govern.rules?.["tailwind/forbidden-palette"], govern.severity ?? "warn" ); if (tailwindPalette?.allow?.length) { out.push( makeTailwindPaletteAllowFact({ patterns: tailwindPalette.allow, severity: forbiddenPaletteSeverity, }) ); } if (tailwindPalette?.deny?.length) { out.push( makeTailwindPaletteDenyFact({ patterns: tailwindPalette.deny, severity: forbiddenPaletteSeverity, }) ); } const unknownClassSeverity = enabledRuleSeverity( govern.rules?.["tailwind/unknown-class"], "info" ); if (unknownClassSeverity) { out.push(makeTailwindUnknownClassEnabledFact({ severity: unknownClassSeverity })); } out.push(...compileRuleConfigFacts(govern)); return out; } const RULE_FAMILY_MEMBERS: Record = { "tokens/hardcoded-values": [ "styles/no-raw-color", "styles/no-raw-spacing", "tokens/require-dual-fallback", "theme/no-theme-coupled-literal", ], "components/usage": [ "components/forbidden-prop-value", "components/preferred-component", "components/unknown-prop", "props/invalid-value", ], "a11y/wcag": ["a11y/required-accessible-name"], }; function compileRuleConfigFacts(govern: GovernanceConfig): PolicyFact[] { const rules = govern.rules; if (!rules && !govern.canonicalSources?.length) return []; const configs = new Map>(); for (const [ruleId, value] of Object.entries(rules ?? {})) { const family = RULE_FAMILY_MEMBERS[ruleId]; if (!family) continue; const config = ruleConfigFromValue(value, govern.severity ?? "warn"); for (const memberRuleId of family) configs.set(memberRuleId, config); } for (const [ruleId, value] of Object.entries(rules ?? {})) { configs.set(ruleId, ruleConfigFromValue(value, govern.severity ?? "warn")); } if ( govern.canonicalSources?.length && configs.get("components/prefer-library")?.enabled !== false ) { const existing = configs.get("components/prefer-library"); configs.set("components/prefer-library", { enabled: true, severity: existing?.severity ?? govern.severity ?? "warn", options: { ...existing?.options, canonicalSources: existing?.options?.canonicalSources ?? govern.canonicalSources, }, }); } return [...configs.entries()].map(([ruleId, config]) => makeGovernanceRuleConfigFact({ ruleId, enabled: config.enabled, severity: config.severity, options: config.options, }) ); } function ruleConfigFromValue( value: unknown, fallbackSeverity: "error" | "warn" | "info" ): { enabled: boolean; severity?: "error" | "warn" | "info"; options?: Record } { if (value === false) return { enabled: false }; if (value === true) return { enabled: true, severity: fallbackSeverity }; if (!value || typeof value !== "object" || Array.isArray(value)) { return { enabled: true, severity: fallbackSeverity }; } const record = value as { enabled?: unknown; severity?: unknown; options?: unknown; }; const options = record.options && typeof record.options === "object" && !Array.isArray(record.options) ? (record.options as Record) : undefined; return { enabled: record.enabled !== false, severity: parseSeverity(record.severity) ?? fallbackSeverity, options, }; } function enabledRuleSeverity( value: unknown, fallback: "error" | "warn" | "info" ): "error" | "warn" | "info" | null { if (value === true) return fallback; if (!value || typeof value !== "object") return null; const record = value as { enabled?: unknown; severity?: unknown }; if (record.enabled === false) return null; if (record.enabled !== true) return null; return parseSeverity(record.severity) ?? fallback; } function ruleSeverity( value: unknown, fallback: "error" | "warn" | "info" ): "error" | "warn" | "info" { if (!value || typeof value !== "object") return fallback; return parseSeverity((value as { severity?: unknown }).severity) ?? fallback; } function parseSeverity(value: unknown): "error" | "warn" | "info" | undefined { if (value === "warning") return "warn"; return value === "error" || value === "warn" || value === "info" ? value : undefined; } // --------------------------------------------------------------------------- // Component → facts // --------------------------------------------------------------------------- interface ComponentFactSource { meta: { name: string; description?: string; category?: string }; guidance?: { when?: string[]; whenNot?: string[] }; props?: Record; governance?: ComponentGovernanceRecord[]; filePath?: string; } function isResolvedGovernedDefinition(value: unknown): value is ResolvedGovernedFragmentDefinition { if (!value || typeof value !== "object") return false; return "governance" in value && Array.isArray((value as { governance: unknown }).governance); } function isGovernedDefinition(value: unknown): value is GovernedFragmentDefinition { if (!value || typeof value !== "object") return false; return "govern" in value || "governance" in value; } function toComponentFactSource(input: ComponentFactInput): ComponentFactSource { if (isResolvedGovernedDefinition(input)) { return { meta: input.meta, guidance: input.guidance, props: (input.props ?? {}) as Record, governance: input.governance, }; } if (isGovernedDefinition(input)) { return { meta: input.meta, guidance: input.guidance, props: (input.props ?? {}) as Record, governance: resolveComponentGovernance(input), }; } // CompiledFragment return { meta: input.meta, guidance: input.guidance ?? input.usage, props: input.props, governance: input.governance, filePath: input.filePath, }; } export type ComponentFactInput = | CompiledFragment | GovernedFragmentDefinition | ResolvedGovernedFragmentDefinition; export function compileComponentFacts(componentId: ComponentId, input: ComponentFactInput): Fact[] { const source = toComponentFactSource(input); const facts: Fact[] = []; facts.push( makeComponentMetadataFact({ componentId, name: source.meta.name, description: source.meta.description, category: source.meta.category, filePath: source.filePath, }) ); for (const [propName, prop] of Object.entries(source.props ?? {})) { facts.push( makePropMetadataFact({ componentId, prop: propName, type: prop.type, values: prop.values ? [...prop.values] : undefined, required: prop.required ?? false, default: prop.default, description: prop.description, }) ); } for (const record of source.governance ?? []) { const fact = compileComponentGovernanceRecord(componentId, record); if (fact) facts.push(fact); } return facts; } function compileComponentGovernanceRecord( componentId: ComponentId, record: ComponentGovernanceRecord ): Fact | undefined { switch (record.kind) { case "capability": return makeComponentCapabilityFact({ componentId, capability: record.capability }); case "prop.value.avoid": return makePropValueAvoidedFact({ componentId, prop: record.prop, value: record.value, because: record.because, suggest: record.suggest, severity: record.severity, }); case "prop.value.forbid": return makePropValueForbiddenFact({ componentId, prop: record.prop, value: record.value, pathPattern: record.when?.path, because: record.because, replaceWith: record.fix?.replaceWith, severity: record.severity, }); case "a11y.requireName": return makeA11yNameRequiredFact({ componentId, because: record.because, severity: record.severity, }); } }