/** * Rules — pure functions over `FactIndex` that return `Finding[]`. * * Phase 5 ships five generic rules. They are content-addressed: scanning the * same source twice produces the same fingerprints, so Cloud can dedupe and * preserve event history. Rules never reach into raw fact arrays — every * lookup goes through `FactIndex`. */ import type { FactIndex } from "../facts/index.js"; import { ruleA11yRequiredAccessibleName } from "./a11y-required-accessible-name.js"; import { ruleComponentsPreferLibrary } from "./components-prefer-library.js"; import { ruleComponentsForbiddenPropValue } from "./components-forbidden-prop-value.js"; import { ruleComponentsUnknownProp } from "./components-unknown-prop.js"; import { ruleCompositionCardinality, ruleCompositionCoOccurrence } from "./composition-pattern.js"; import { ruleJsxPreferredComponent } from "./jsx-preferred-component.js"; import { ruleJsxPreferredImportPath } from "./jsx-preferred-import-path.js"; import { rulePropsInvalidValue } from "./props-invalid-value.js"; import { ruleStylesNoRawColor } from "./styles-no-raw-color.js"; import { ruleStylesNoRawDimensions } from "./styles-no-raw-dimensions.js"; import { ruleStylesNoRawSpacing } from "./styles-no-raw-spacing.js"; import { ruleStylesNoRawTypography } from "./styles-no-raw-typography.js"; import { ruleThemeNoThemeCoupledLiteral } from "./theme-no-theme-coupled-literal.js"; import { ruleTailwindArbitraryColor } from "./tailwind-arbitrary-color.js"; import { ruleTailwindArbitrarySpacing } from "./tailwind-arbitrary-spacing.js"; import { ruleTailwindForbiddenPalette } from "./tailwind-forbidden-palette.js"; import { ruleTailwindOffScaleSpacingToken } from "./tailwind-off-scale-spacing-token.js"; import { ruleTailwindRawColorViaToken } from "./tailwind-raw-color-via-token.js"; import { ruleTailwindUnknownClass } from "./tailwind-unknown-class.js"; import { ruleTokensRequireDualFallback } from "./tokens-require-dual-fallback.js"; import { ruleTokensCssVarsMustBeDefined } from "./tokens-css-vars-must-be-defined.js"; import type { Finding } from "./types.js"; export type RuleFn = (ix: FactIndex) => Finding[]; export interface Rule { id: string; version: string; run: RuleFn; } export const RULES: readonly Rule[] = [ { id: "components/unknown-prop", version: "1", run: ruleComponentsUnknownProp, }, { id: "components/forbidden-prop-value", version: "1", run: ruleComponentsForbiddenPropValue, }, { id: "props/invalid-value", version: "1", run: rulePropsInvalidValue, }, { id: "imports/preferred-path", version: "1", run: ruleJsxPreferredImportPath, }, { id: "components/preferred-component", version: "1", run: ruleJsxPreferredComponent, }, { id: "components/prefer-library", version: "1", run: ruleComponentsPreferLibrary, }, { id: "styles/no-raw-color", version: "1", run: ruleStylesNoRawColor, }, { id: "styles/no-raw-dimensions", version: "1", run: ruleStylesNoRawDimensions, }, { id: "styles/no-raw-spacing", version: "1", run: ruleStylesNoRawSpacing, }, { id: "styles/no-raw-typography", version: "1", run: ruleStylesNoRawTypography, }, { id: "tailwind/arbitrary-color", version: "1", run: ruleTailwindArbitraryColor, }, { id: "tailwind/arbitrary-spacing", version: "1", run: ruleTailwindArbitrarySpacing, }, { id: "tailwind/forbidden-palette", version: "1", run: ruleTailwindForbiddenPalette, }, { id: "tailwind/off-scale-spacing-token", version: "1", run: ruleTailwindOffScaleSpacingToken, }, { id: "tailwind/raw-color-via-token", version: "1", run: ruleTailwindRawColorViaToken, }, { id: "tailwind/unknown-class", version: "1", run: ruleTailwindUnknownClass, }, { id: "tokens/require-dual-fallback", version: "1", run: ruleTokensRequireDualFallback, }, { id: "tokens/css-vars-must-be-defined", version: "1", run: ruleTokensCssVarsMustBeDefined, }, { id: "theme/no-theme-coupled-literal", version: "1", run: ruleThemeNoThemeCoupledLiteral, }, { id: "a11y/required-accessible-name", version: "1", run: ruleA11yRequiredAccessibleName, }, { id: "composition/cardinality", version: "1", run: ruleCompositionCardinality, }, { id: "composition/co-occurrence", version: "1", run: ruleCompositionCoOccurrence, }, ]; /** * Run every Phase 5 rule and return findings sorted by `(file, line, column, * fingerprint)` so output is stable across runs. */ export function runRules(ix: FactIndex): Finding[] { const findings: Finding[] = []; for (const rule of RULES) findings.push(...rule.run(ix)); return sortFindings(findings); } function sortFindings(findings: Finding[]): Finding[] { return findings.slice().sort((a, b) => { const fileCmp = a.location.file.localeCompare(b.location.file); if (fileCmp !== 0) return fileCmp; if (a.location.line !== b.location.line) return a.location.line - b.location.line; if (a.location.column !== b.location.column) return a.location.column - b.location.column; return a.fingerprint.localeCompare(b.fingerprint); }); } export type { Finding, FindingFix, FindingReplaceComponentFix, FindingReplaceImportFix, FindingReplacePropValueFix, FindingReplaceStyleValueFix, FindingReplaceClassTokenFix, } from "./types.js"; export { makeFinding } from "./finding.js"; // Blocking deny-set (Contract Mode Phase 0): the shared "may this finding // hard-deny a write" criteria, used by the CLI pre-write hook and the emit gate. export { BLOCKING_RULE_ALLOWLIST, gatesCi, isDenyEligible } from "./emit-gate.js"; export { ruleComponentsUnknownProp } from "./components-unknown-prop.js"; export { ruleComponentsForbiddenPropValue } from "./components-forbidden-prop-value.js"; export { ruleComponentsPreferLibrary } from "./components-prefer-library.js"; export { rulePropsInvalidValue } from "./props-invalid-value.js"; export { ruleJsxPreferredImportPath } from "./jsx-preferred-import-path.js"; export { ruleJsxPreferredComponent } from "./jsx-preferred-component.js"; export { ruleStylesNoRawColor } from "./styles-no-raw-color.js"; export { ruleStylesNoRawDimensions } from "./styles-no-raw-dimensions.js"; export { ruleStylesNoRawSpacing } from "./styles-no-raw-spacing.js"; export { ruleStylesNoRawTypography } from "./styles-no-raw-typography.js"; export { ruleTailwindArbitraryColor } from "./tailwind-arbitrary-color.js"; export { ruleTailwindArbitrarySpacing } from "./tailwind-arbitrary-spacing.js"; export { ruleTailwindForbiddenPalette } from "./tailwind-forbidden-palette.js"; export { ruleTailwindOffScaleSpacingToken } from "./tailwind-off-scale-spacing-token.js"; export { ruleTailwindRawColorViaToken } from "./tailwind-raw-color-via-token.js"; export { ruleTailwindUnknownClass } from "./tailwind-unknown-class.js"; export { ruleTokensRequireDualFallback } from "./tokens-require-dual-fallback.js"; export { ruleTokensCssVarsMustBeDefined } from "./tokens-css-vars-must-be-defined.js"; export { ruleThemeNoThemeCoupledLiteral } from "./theme-no-theme-coupled-literal.js"; export { ruleA11yRequiredAccessibleName } from "./a11y-required-accessible-name.js"; export { ruleCompositionCardinality, ruleCompositionCoOccurrence } from "./composition-pattern.js"; export { RULE_TIER, FRAGMENTS_INTERNAL_RULE_IDS, tierFor, isContractTierRule, contractTierRuleIds, hygieneTierRuleIds, vocabularyRank, compareByVocabularyRank, } from "./tiers.js"; export type { RuleTier } from "./tiers.js"; export { customerDefaultRuleStates, fragmentsPresetRuleStates } from "./presets.js"; export type { PresetRuleState } from "./presets.js"; export { compositionPatternSchema, compositionAuthoringEntrySchema, isCompositionPattern, lowerCompositionContract, } from "./composition-pattern-schema.js"; export type { CompositionPattern, CompositionRegionSelector, CompositionChildSelector, CompositionConstraint, CompositionAuthoringEntry, } from "./composition-pattern-schema.js";