/** * Fact builders — factory functions that compute the content-addressed ID * from each fact's identity attributes. Use these everywhere instead of hand- * rolling fact objects so the identity contract stays consistent. * * The "identity" of a fact is the subset of attributes that determines its * ID. Other attributes (e.g., `because`, `description`) can change without * changing the ID — same logical fact, refined payload. */ import type { GovernanceSeverity } from "../governance.js"; import { factId } from "./ids.js"; import type { A11yNameRequiredFact, CanonicalCandidateFact, CanonicalMappingFact, CanonicalReplacementFact, ClassNameDynamicFact, ClassNameDynamicReason, ClassNameLiteralFact, ClassNameOrigin, ComponentCapabilityFact, ComponentId, ComponentMetadataFact, FactLocation, GovernanceRuleConfigFact, JsxComponentPreferredFact, JsxImportPathPreferredFact, JsxInlineStyleForbiddenRawFact, ContractTokenFact, JsxUnknownPropsForbiddenFact, PropMetadataFact, PropValueAvoidedFact, PropValueForbiddenFact, ScaleFact, ScaleValueFact, StructuralConfidenceFact, StyleCssVarsMustBeDefinedFact, StyleDeclarationFact, StyleFontSizeScaleFact, StylePropertyScaleFact, StyleRawColorForbiddenFact, StyleRawDimensionForbiddenFact, StyleUnsupportedFact, UnsupportedStyleReason, TailwindPaletteAllowFact, TailwindPaletteDenyFact, TailwindUnknownClassEnabledFact, TailwindClassFact, TailwindModifier, TailwindResolvedValue, TailwindTokenResolvedFact, TailwindValue, ThemeDeclarationFact, TokenDefinitionFact, UsageComponentFact, UsageImportFact, UsageInlineStyleFact, UsageNodeFact, UsagePropResolvedFact, UsageTextChildFact, FactId, } from "./types.js"; // --------------------------------------------------------------------------- // Component facts // --------------------------------------------------------------------------- export function makeComponentMetadataFact(input: { componentId: ComponentId; name: string; description?: string; category?: string; filePath?: string; }): ComponentMetadataFact { return { id: factId("component", { componentId: input.componentId }), kind: "component", componentId: input.componentId, name: input.name, description: input.description, category: input.category, filePath: input.filePath, }; } export function makeComponentCapabilityFact(input: { componentId: ComponentId; capability: string; }): ComponentCapabilityFact { return { id: factId("component_capability", { componentId: input.componentId, capability: input.capability, }), kind: "component_capability", componentId: input.componentId, capability: input.capability, }; } // --------------------------------------------------------------------------- // Classifier facts // --------------------------------------------------------------------------- export function makeCanonicalCandidateFact(input: { componentId: ComponentId; canonical: string; confidence: number; band: CanonicalCandidateFact["band"]; location?: FactLocation; classifierVersion: string; vocabVersion: string; evidence: unknown[]; userConfirmed?: boolean; }): CanonicalCandidateFact { return { id: factId("canonical_candidate", { componentId: input.componentId, canonical: input.canonical, classifierVersion: input.classifierVersion, vocabVersion: input.vocabVersion, }), kind: "canonical_candidate", componentId: input.componentId, canonical: input.canonical, confidence: input.confidence, band: input.band, location: input.location, classifierVersion: input.classifierVersion, vocabVersion: input.vocabVersion, evidence: input.evidence, userConfirmed: input.userConfirmed, }; } export function makeCanonicalMappingFact(input: { componentId: ComponentId; canonical: string; status: CanonicalMappingFact["status"]; confidence: number; location?: FactLocation; }): CanonicalMappingFact { return { id: factId("canonical_mapping", { componentId: input.componentId, canonical: input.canonical, }), kind: "canonical_mapping", componentId: input.componentId, canonical: input.canonical, status: input.status, confidence: input.confidence, location: input.location, }; } export function makeStructuralConfidenceFact(input: { componentId: ComponentId; canonical: string; confidence: number; signalCount: number; location?: FactLocation; }): StructuralConfidenceFact { return { id: factId("structural_confidence", { componentId: input.componentId, canonical: input.canonical, }), kind: "structural_confidence", componentId: input.componentId, canonical: input.canonical, confidence: input.confidence, signalCount: input.signalCount, location: input.location, }; } export function makeCanonicalReplacementFact(input: { componentId: ComponentId; canonical: string; importPath?: string; componentName?: string; confidence: number; location?: FactLocation; }): CanonicalReplacementFact { return { id: factId("canonical_replacement", { componentId: input.componentId, canonical: input.canonical, }), kind: "canonical_replacement", componentId: input.componentId, canonical: input.canonical, importPath: input.importPath, componentName: input.componentName, confidence: input.confidence, location: input.location, }; } export function makePropMetadataFact(input: { componentId: ComponentId; prop: string; type: string; values?: string[]; required: boolean; default?: unknown; description?: string; }): PropMetadataFact { return { id: factId("prop_metadata", { componentId: input.componentId, prop: input.prop, }), kind: "prop_metadata", componentId: input.componentId, prop: input.prop, type: input.type, values: input.values, required: input.required, default: input.default, description: input.description, }; } // --------------------------------------------------------------------------- // Component-scoped policy facts // --------------------------------------------------------------------------- export function makePropValueAvoidedFact(input: { componentId: ComponentId; prop: string; value: unknown; because: string; suggest?: string; severity: GovernanceSeverity; }): PropValueAvoidedFact { return { id: factId("prop_value_avoided", { componentId: input.componentId, prop: input.prop, value: input.value, }), kind: "prop_value_avoided", componentId: input.componentId, prop: input.prop, value: input.value, because: input.because, suggest: input.suggest, severity: input.severity, }; } export function makePropValueForbiddenFact(input: { componentId: ComponentId; prop: string; value: unknown; pathPattern?: string; because: string; replaceWith?: unknown; severity: GovernanceSeverity; }): PropValueForbiddenFact { return { id: factId("prop_value_forbidden", { componentId: input.componentId, prop: input.prop, value: input.value, pathPattern: input.pathPattern, }), kind: "prop_value_forbidden", componentId: input.componentId, prop: input.prop, value: input.value, pathPattern: input.pathPattern, because: input.because, replaceWith: input.replaceWith, severity: input.severity, }; } export function makeA11yNameRequiredFact(input: { componentId: ComponentId; because: string; severity: GovernanceSeverity; }): A11yNameRequiredFact { return { id: factId("a11y_name_required", { componentId: input.componentId }), kind: "a11y_name_required", componentId: input.componentId, because: input.because, severity: input.severity, }; } // --------------------------------------------------------------------------- // Global policy facts // --------------------------------------------------------------------------- export function makeScaleFact(input: { name: string; unit: "px" | "rem"; rootFontSizePx?: number; emBasePx?: number; }): ScaleFact { return { id: factId("scale", { name: input.name }), kind: "scale", name: input.name, unit: input.unit, ...(input.rootFontSizePx === undefined ? {} : { rootFontSizePx: input.rootFontSizePx }), ...(input.emBasePx === undefined ? {} : { emBasePx: input.emBasePx }), }; } export function makeScaleValueFact(input: { scale: string; value: number }): ScaleValueFact { return { id: factId("scale_value", { scale: input.scale, value: input.value }), kind: "scale_value", scale: input.scale, value: input.value, }; } export function makeStyleRawColorForbiddenFact(input: { except: string[]; prefer: "token" | "css-variable"; severity: GovernanceSeverity; }): StyleRawColorForbiddenFact { return { id: factId("style_raw_color_forbidden", {}), kind: "style_raw_color_forbidden", except: [...input.except], prefer: input.prefer, severity: input.severity, }; } export function makeStyleRawDimensionForbiddenFact(input: { appliesTo: string[]; prefer: "token" | "css-variable"; severity: GovernanceSeverity; }): StyleRawDimensionForbiddenFact { return { id: factId("style_raw_dimension_forbidden", {}), kind: "style_raw_dimension_forbidden", appliesTo: [...input.appliesTo], prefer: input.prefer, severity: input.severity, }; } export function makeStylePropertyScaleFact(input: { property: string; scale: string; severity: GovernanceSeverity; }): StylePropertyScaleFact { return { id: factId("style_property_scale", { property: input.property }), kind: "style_property_scale", property: input.property, scale: input.scale, severity: input.severity, }; } export function makeStyleFontSizeScaleFact(input: { scale: string; severity: GovernanceSeverity; }): StyleFontSizeScaleFact { return { id: factId("style_font_size_scale", {}), kind: "style_font_size_scale", scale: input.scale, severity: input.severity, }; } export function makeStyleCssVarsMustBeDefinedFact(input: { severity: GovernanceSeverity; }): StyleCssVarsMustBeDefinedFact { return { id: factId("style_css_vars_must_be_defined", {}), kind: "style_css_vars_must_be_defined", severity: input.severity, }; } export function makeContractTokenFact(input: { name: string }): ContractTokenFact { const name = input.name.startsWith("--") ? input.name : `--${input.name}`; return { id: factId("contract_token", { name }), kind: "contract_token", name, }; } export function makeJsxUnknownPropsForbiddenFact(input: { severity: GovernanceSeverity; }): JsxUnknownPropsForbiddenFact { return { id: factId("jsx_unknown_props_forbidden", {}), kind: "jsx_unknown_props_forbidden", severity: input.severity, }; } export function makeJsxInlineStyleForbiddenRawFact(input: { property: string; severity: GovernanceSeverity; }): JsxInlineStyleForbiddenRawFact { return { id: factId("jsx_inline_style_forbidden_raw", { property: input.property }), kind: "jsx_inline_style_forbidden_raw", property: input.property, severity: input.severity, }; } export function makeJsxImportPathPreferredFact(input: { from: string; to: string; imported?: string; because?: string; severity: GovernanceSeverity; }): JsxImportPathPreferredFact { return { id: factId("jsx_import_path_preferred", { from: input.from, to: input.to, imported: input.imported, }), kind: "jsx_import_path_preferred", from: input.from, to: input.to, imported: input.imported, because: input.because, severity: input.severity, }; } export function makeJsxComponentPreferredFact(input: { from: ComponentId; to: ComponentId; because?: string; severity: GovernanceSeverity; }): JsxComponentPreferredFact { return { id: factId("jsx_component_preferred", { from: input.from, to: input.to, }), kind: "jsx_component_preferred", from: input.from, to: input.to, because: input.because, severity: input.severity, }; } export function makeTokenDefinitionFact(input: { name: string; value: string; category?: TokenDefinitionFact["category"]; referenceFormat?: TokenDefinitionFact["referenceFormat"]; }): TokenDefinitionFact { return { id: factId("token_definition", { name: input.name }), kind: "token_definition", name: input.name, value: input.value, category: input.category, referenceFormat: input.referenceFormat, }; } export function makeTailwindPaletteAllowFact(input: { patterns: string[]; severity: GovernanceSeverity; }): TailwindPaletteAllowFact { return { id: factId("tailwind_palette_allow", {}), kind: "tailwind_palette_allow", patterns: [...input.patterns], severity: input.severity, }; } export function makeTailwindPaletteDenyFact(input: { patterns: string[]; severity: GovernanceSeverity; }): TailwindPaletteDenyFact { return { id: factId("tailwind_palette_deny", {}), kind: "tailwind_palette_deny", patterns: [...input.patterns], severity: input.severity, }; } export function makeTailwindUnknownClassEnabledFact(input: { severity: GovernanceSeverity; }): TailwindUnknownClassEnabledFact { return { id: factId("tailwind_unknown_class_enabled", {}), kind: "tailwind_unknown_class_enabled", severity: input.severity, }; } export function makeGovernanceRuleConfigFact(input: { ruleId: string; enabled: boolean; severity?: GovernanceSeverity; options?: Record; }): GovernanceRuleConfigFact { return { id: factId("governance_rule_config", { ruleId: input.ruleId }), kind: "governance_rule_config", ruleId: input.ruleId, enabled: input.enabled, severity: input.severity, options: input.options, }; } // --------------------------------------------------------------------------- // Usage facts // --------------------------------------------------------------------------- export function makeUsageNodeFact(input: { file: string; nodePath: string; element: string; role?: string; interactive?: boolean; location: FactLocation; }): UsageNodeFact { return { // Identity is structural (file + nodePath + element) only — the normalized // semantics (role/interactive) are evidence carried on the fact, never part // of its ID, so adding them never perturbs existing fact identities. id: factId("usage_node", { file: input.file, nodePath: input.nodePath, element: input.element, }), kind: "usage_node", file: input.file, nodePath: input.nodePath, element: input.element, ...(input.role !== undefined ? { role: input.role } : {}), ...(input.interactive ? { interactive: true } : {}), location: input.location, }; } export function makeUsageComponentFact(input: { nodeId: FactId; componentId: ComponentId; }): UsageComponentFact { return { id: factId("usage_component", { nodeId: input.nodeId, componentId: input.componentId, }), kind: "usage_component", nodeId: input.nodeId, componentId: input.componentId, }; } export function makeUsageImportFact(input: { file: string; local: string; imported: string; source: string; namespace?: boolean; location: FactLocation; }): UsageImportFact { return { id: factId("usage_import", { file: input.file, local: input.local, imported: input.imported, source: input.source, }), kind: "usage_import", file: input.file, local: input.local, imported: input.imported, source: input.source, namespace: input.namespace, location: input.location, }; } export function makeUsagePropResolvedFact(input: { nodeId: FactId; prop: string; resolution: "static" | "dynamic" | "spread" | "jsx"; value?: unknown; location?: FactLocation; }): UsagePropResolvedFact { return { id: factId("usage_prop_resolved", { nodeId: input.nodeId, prop: input.prop }), kind: "usage_prop_resolved", nodeId: input.nodeId, prop: input.prop, resolution: input.resolution, value: input.value, location: input.location, }; } export function makeUsageInlineStyleFact(input: { nodeId: FactId; property: string; valueKind: "static" | "number" | "css-variable"; value: string; }): UsageInlineStyleFact { return { id: factId("usage_inline_style", { nodeId: input.nodeId, property: input.property, }), kind: "usage_inline_style", nodeId: input.nodeId, property: input.property, valueKind: input.valueKind, value: input.value, }; } export function makeUsageTextChildFact(input: { nodeId: FactId; text: string; index: number; }): UsageTextChildFact { return { id: factId("usage_text_child", { nodeId: input.nodeId, index: input.index }), kind: "usage_text_child", nodeId: input.nodeId, text: input.text, index: input.index, }; } export function makeClassNameLiteralFact(input: { file: string; nodeId: FactId; attr: string; classes: string[]; origin: ClassNameOrigin; originPath: string; location: FactLocation; }): ClassNameLiteralFact { return { id: factId("classname_literal", { nodeId: input.nodeId, attr: input.attr, origin: input.origin, originPath: input.originPath, }), kind: "classname_literal", file: input.file, nodeId: input.nodeId, attr: input.attr, classes: [...input.classes], origin: input.origin, originPath: input.originPath, location: input.location, }; } export function makeClassNameDynamicFact(input: { file: string; nodeId: FactId; attr: string; reason: ClassNameDynamicReason; snippet: string; originPath: string; location: FactLocation; }): ClassNameDynamicFact { return { id: factId("classname_dynamic", { nodeId: input.nodeId, attr: input.attr, reason: input.reason, originPath: input.originPath, }), kind: "classname_dynamic", file: input.file, nodeId: input.nodeId, attr: input.attr, reason: input.reason, snippet: input.snippet, originPath: input.originPath, location: input.location, }; } export function makeTailwindClassFact(input: { file: string; nodeId: FactId; raw: string; originPath: string; prefix: string | null; modifiers: TailwindModifier[]; important: boolean; negative: boolean; utility: string; value: TailwindValue; location: FactLocation; }): TailwindClassFact { return { id: factId("tailwind_class", { nodeId: input.nodeId, originPath: input.originPath, }), kind: "tailwind_class", file: input.file, nodeId: input.nodeId, raw: input.raw, originPath: input.originPath, prefix: input.prefix, modifiers: input.modifiers.map((m) => ({ ...m })), important: input.important, negative: input.negative, utility: input.utility, value: { ...input.value }, location: input.location, }; } export function makeThemeDeclarationFact(input: { file: string; name: string; value: string; declarationPath: string; inline: boolean; flavor: ThemeDeclarationFact["flavor"]; location: FactLocation; }): ThemeDeclarationFact { return { id: factId("theme_declaration", { file: input.file, name: input.name, declarationPath: input.declarationPath, }), kind: "theme_declaration", file: input.file, name: input.name, value: input.value, declarationPath: input.declarationPath, inline: input.inline, flavor: input.flavor, location: input.location, }; } export function makeTailwindTokenResolvedFact(input: { utility: string; token: string; resolved: TailwindResolvedValue; }): TailwindTokenResolvedFact { return { id: factId("tailwind_token_resolved", { utility: input.utility, token: input.token, }), kind: "tailwind_token_resolved", utility: input.utility, token: input.token, resolved: { ...input.resolved }, }; } export function makeStyleDeclarationFact(input: { file: string; selector: string; declarationPath: string; property: string; value: string; location: FactLocation; declaredTokenSource?: boolean; }): StyleDeclarationFact { return { id: factId("style_declaration", { file: input.file, selector: input.selector, declarationPath: input.declarationPath, property: input.property, }), kind: "style_declaration", file: input.file, selector: input.selector, declarationPath: input.declarationPath, property: input.property, value: input.value, location: input.location, ...(input.declaredTokenSource ? { declaredTokenSource: true } : {}), }; } export function makeStyleUnsupportedFact(input: { file: string; source: "css-in-js"; syntax: string; selector: string; declarationPath: string; property?: string; reason: UnsupportedStyleReason; location: FactLocation; }): StyleUnsupportedFact { return { id: factId("style_unsupported", { file: input.file, source: input.source, syntax: input.syntax, selector: input.selector, declarationPath: input.declarationPath, property: input.property ?? "", }), kind: "style_unsupported", file: input.file, source: input.source, syntax: input.syntax, selector: input.selector, declarationPath: input.declarationPath, property: input.property, reason: input.reason, location: input.location, }; }