import { z } from "zod"; import type { ComponentRelation, FragmentComponent, FragmentContract, FragmentExample, FragmentGuidance, FragmentMeta, FragmentProvenance, PropDefinition, } from "./types.js"; import { portableRepoPathError } from "./config-paths.js"; export const governanceSeveritySchema = z.enum(["error", "warn", "info"]); export type GovernanceSeverity = z.infer; export const scaleGovernanceRecordSchema = z.object({ kind: z.literal("scale").default("scale"), name: z.string().min(1).optional(), unit: z.enum(["px", "rem"]), values: z.array(z.number()), rootFontSizePx: z.number().positive().optional(), emBasePx: z.number().positive().optional(), }); const styleRawColorsForbidRecordSchema = z.object({ kind: z.literal("style.rawColors.forbid"), except: z.array(z.string()), prefer: z.enum(["token", "css-variable"]), severity: governanceSeveritySchema, }); const styleRawDimensionsForbidRecordSchema = z.object({ kind: z.literal("style.rawDimensions.forbid"), appliesTo: z.array(z.string().min(1)), prefer: z.enum(["token", "css-variable"]), severity: governanceSeveritySchema, }); const styleRawSpacingMustMatchScaleRecordSchema = z.object({ kind: z.literal("style.rawSpacing.mustMatchScale"), scale: z.string().min(1), appliesTo: z.array(z.string().min(1)), severity: governanceSeveritySchema, }); const styleFontSizeMustMatchScaleRecordSchema = z.object({ kind: z.literal("style.fontSize.mustMatchScale"), scale: z.string().min(1), severity: governanceSeveritySchema, }); const styleCssVarsMustBeDefinedRecordSchema = z.object({ kind: z.literal("style.cssVars.mustBeDefined"), severity: governanceSeveritySchema, }); export const globalStyleGovernanceRecordSchema = z.discriminatedUnion("kind", [ styleRawColorsForbidRecordSchema, styleRawDimensionsForbidRecordSchema, styleRawSpacingMustMatchScaleRecordSchema, styleFontSizeMustMatchScaleRecordSchema, styleCssVarsMustBeDefinedRecordSchema, ]); const jsxUnknownPropsForbidRecordSchema = z.object({ kind: z.literal("jsx.unknownProps.forbid"), severity: governanceSeveritySchema, }); const jsxInlineStyleForbidRawRecordSchema = z.object({ kind: z.literal("jsx.inlineStyle.forbidRaw"), properties: z.array(z.string().min(1)), severity: governanceSeveritySchema, }); const jsxImportPathPreferRecordSchema = z.object({ kind: z.literal("jsx.importPath.prefer"), from: z.string().min(1), to: z.string().min(1), imported: z.string().min(1).optional(), because: z.string().optional(), severity: governanceSeveritySchema, }); const jsxComponentPreferRecordSchema = z.object({ kind: z.literal("jsx.component.prefer"), from: z.string().min(1), to: z.string().min(1), because: z.string().optional(), severity: governanceSeveritySchema, }); export const globalJsxGovernanceRecordSchema = z.discriminatedUnion("kind", [ jsxUnknownPropsForbidRecordSchema, jsxInlineStyleForbidRawRecordSchema, jsxImportPathPreferRecordSchema, jsxComponentPreferRecordSchema, ]); export const globalGovernanceRecordSchema = z.discriminatedUnion("kind", [ scaleGovernanceRecordSchema, styleRawColorsForbidRecordSchema, styleRawDimensionsForbidRecordSchema, styleRawSpacingMustMatchScaleRecordSchema, styleFontSizeMustMatchScaleRecordSchema, styleCssVarsMustBeDefinedRecordSchema, jsxUnknownPropsForbidRecordSchema, jsxInlineStyleForbidRawRecordSchema, jsxImportPathPreferRecordSchema, jsxComponentPreferRecordSchema, ]); const componentCapabilityRecordSchema = z.object({ kind: z.literal("capability"), capability: z.string().min(1), }); const propValueAvoidRecordSchema = z.object({ kind: z.literal("prop.value.avoid"), prop: z.string().min(1), value: z.unknown(), because: z.string(), suggest: z.string().optional(), severity: governanceSeveritySchema, }); const propValueForbidRecordSchema = z.object({ kind: z.literal("prop.value.forbid"), prop: z.string().min(1), value: z.unknown(), when: z.object({ path: z.string().min(1).optional() }).optional(), because: z.string(), fix: z.object({ replaceWith: z.unknown() }).optional(), severity: governanceSeveritySchema, }); const a11yRequireNameRecordSchema = z.object({ kind: z.literal("a11y.requireName"), because: z.string(), severity: governanceSeveritySchema, }); export const componentGovernanceRecordSchema = z.discriminatedUnion("kind", [ componentCapabilityRecordSchema, propValueAvoidRecordSchema, propValueForbidRecordSchema, a11yRequireNameRecordSchema, ]); export const componentGovernanceRecordsSchema = z.array(componentGovernanceRecordSchema); const componentPolicyMatchSchema = z .object({ componentKey: z.string().min(1).optional(), componentId: z.string().min(1).optional(), publicRef: z.string().min(1).optional(), importPath: z.string().min(1).optional(), sourcePath: z.string().min(1).optional(), path: z.string().min(1).optional(), exportName: z.string().min(1).optional(), export: z.string().min(1).optional(), name: z.string().min(1).optional(), scope: z.enum(["one", "all"]).optional(), }) .strict(); const componentPolicyRecordSchema = z .object({ rules: componentGovernanceRecordsSchema, status: z.enum(["active", "draft", "disabled"]).optional(), }) .strict(); const componentPolicyOverrideSchema = z .object({ match: componentPolicyMatchSchema, rules: componentGovernanceRecordsSchema, status: z.enum(["active", "draft", "disabled"]).optional(), }) .strict(); const canonicalSourcePathSchema = z .string() .min(1) .superRefine((value, ctx) => { const error = portableRepoPathError(value); if (error) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: error }); } }); const canonicalSourceRegistryHashSchema = z.string().regex(/^[0-9a-f]{64}$/); const canonicalSourceSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("npm"), specifier: z.string().min(1), implementationPath: z.string().min(1).optional(), include: z.array(z.string().min(1)).optional(), exclude: z.array(z.string().min(1)).optional(), }), z.object({ kind: z.literal("directory"), path: z.string().min(1), include: z.array(z.string().min(1)).optional(), exclude: z.array(z.string().min(1)).optional(), }), z.object({ kind: z.literal("registry"), registryId: z.string().min(1), registryHash: canonicalSourceRegistryHashSchema.optional(), receiptPath: canonicalSourcePathSchema.default(".fragments/registry-lock.json"), installPath: canonicalSourcePathSchema, importPath: z.string().min(1).optional(), include: z.array(z.string().min(1)).optional(), exclude: z.array(z.string().min(1)).optional(), severity: governanceSeveritySchema.optional(), }), ]); export const governanceConfigSchema = z .object({ extends: z.array(z.string()).optional(), severity: governanceSeveritySchema.optional(), rules: z.record(z.string(), z.unknown()).optional(), agents: z .record( z.string(), z .object({ rules: z.record(z.string(), z.unknown()).optional(), }) .passthrough() ) .optional(), audit: z.object({}).passthrough().optional(), runners: z.record(z.string(), z.object({}).passthrough()).optional(), canonicalSources: z.array(canonicalSourceSchema).optional(), presets: z.array(z.string()).optional(), scales: z.record(z.string(), scaleGovernanceRecordSchema).optional(), styles: z.array(globalStyleGovernanceRecordSchema).optional(), jsx: z.array(globalJsxGovernanceRecordSchema).optional(), tailwind: z .object({ palette: z .object({ allow: z.array(z.string().min(1)).optional(), deny: z.array(z.string().min(1)).optional(), }) .optional(), }) .passthrough() .optional(), agent: z .object({ repairOrder: z.array(z.string().min(1)).optional(), }) .passthrough() .optional(), components: z.record(componentPolicyRecordSchema).optional(), overrides: z.array(componentPolicyOverrideSchema).optional(), ci: z .object({ failOnWarnings: z.boolean().optional(), }) .passthrough() .optional(), }) .strict(); export type ScaleGovernanceRecord = z.infer; export type GlobalStyleGovernanceRecord = z.infer; export type GlobalJsxGovernanceRecord = z.infer; export type GlobalGovernanceRecord = z.infer; export type ComponentGovernanceRecord = z.infer; export type ComponentPolicyMatch = z.infer; export type ComponentPolicyRecord = z.infer; export type ComponentPolicyOverride = z.infer; export type CanonicalSource = z.infer; export interface GovernanceConfig { extends?: string[]; severity?: GovernanceSeverity; rules?: Record; agents?: Record }>; audit?: Record; runners?: Record>; canonicalSources?: CanonicalSource[]; presets?: string[]; scales?: Record; styles?: GlobalStyleGovernanceRecord[]; jsx?: GlobalJsxGovernanceRecord[]; tailwind?: { palette?: { allow?: string[]; deny?: string[]; }; [key: string]: unknown; }; agent?: { repairOrder?: string[]; [key: string]: unknown; }; components?: Record; overrides?: ComponentPolicyOverride[]; ci?: { failOnWarnings?: boolean; [key: string]: unknown; }; [key: string]: unknown; } type SeverityOption = { severity?: GovernanceSeverity; }; type PropContext = { path?: string; }; type NonNullish = T extends null | undefined ? never : T; export type GovernancePropValue = NonNullish extends string ? string extends NonNullish ? string : NonNullish : NonNullish extends number ? number extends NonNullish ? number : NonNullish : NonNullish extends boolean ? boolean extends NonNullish ? boolean : NonNullish : NonNullish; export type PropKey = Extract; export interface PropGovernanceOptions extends SeverityOption { because?: string; suggest?: string; when?: PropContext; fix?: { replaceWith: GovernancePropValue; }; } export interface PropGovernanceBuilder { avoid( value: GovernancePropValue, options?: Omit, "fix" | "when"> ): ComponentGovernanceRecord; forbid( value: GovernancePropValue, options?: PropGovernanceOptions ): ComponentGovernanceRecord; } export interface AccessibilityGovernanceBuilder { requireName(options?: SeverityOption & { because?: string }): ComponentGovernanceRecord; } export interface ComponentGovernanceBuilder { capability(capability: string): ComponentGovernanceRecord; prop>(prop: TKey): PropGovernanceBuilder; accessibility(): AccessibilityGovernanceBuilder; } export interface GovernedFragmentDefinition< TProps = unknown, TComponent extends FragmentComponent = FragmentComponent, > { component: TComponent; meta: FragmentMeta; guidance: FragmentGuidance; props?: Record; relations?: ComponentRelation[]; examples?: FragmentExample[]; composition?: { pattern?: "compound" | "simple" | "controlled" | "wrapper"; subComponents?: string[]; requiredChildren?: string[]; commonPatterns?: string[]; }; contract?: FragmentContract; _provenance?: FragmentProvenance; govern?: (g: ComponentGovernanceBuilder) => ComponentGovernanceRecord[]; governance?: ComponentGovernanceRecord[]; } export type ResolvedGovernedFragmentDefinition< TProps = unknown, TComponent extends FragmentComponent = FragmentComponent, > = GovernedFragmentDefinition & { governance: ComponentGovernanceRecord[]; }; export function createComponentGovernanceBuilder(): ComponentGovernanceBuilder { return { capability(capability) { return { kind: "capability", capability }; }, prop(prop) { return { avoid(value, options = {}) { return { kind: "prop.value.avoid", prop, value, because: options.because ?? "", suggest: options.suggest, severity: options.severity ?? "warn", }; }, forbid(value, options = {}) { return { kind: "prop.value.forbid", prop, value, when: options.when, because: options.because ?? "", fix: options.fix, severity: options.severity ?? "error", }; }, }; }, accessibility() { return { requireName(options = {}) { return { kind: "a11y.requireName", because: options.because ?? "", severity: options.severity ?? "error", }; }, }; }, }; } export function resolveComponentGovernance( definition: GovernedFragmentDefinition ): ComponentGovernanceRecord[] { const builder = createComponentGovernanceBuilder(); const records = definition.govern ? definition.govern(builder) : (definition.governance ?? []); return componentGovernanceRecordsSchema.parse(records); } export function normalizeGovernanceConfig( config: TConfig ): TConfig { if (!config.govern?.scales) { return config; } const scales = Object.fromEntries( Object.entries(config.govern.scales).map(([name, scale]) => { if (!scale || typeof scale !== "object") { return [name, scale]; } return [name, { ...scale, name: scale.name ?? name }]; }) ); return { ...config, govern: { ...config.govern, scales, }, }; } export const g = { scale: { px(values: readonly number[]): ScaleGovernanceRecord { return { kind: "scale", unit: "px", values: [...values] }; }, rem(values: readonly number[]): ScaleGovernanceRecord { return { kind: "scale", unit: "rem", values: [...values] }; }, }, styles: { rawColors() { return { forbid( options: { except?: string[]; prefer?: "token" | "css-variable"; severity?: GovernanceSeverity; } = {} ): GlobalStyleGovernanceRecord { return { kind: "style.rawColors.forbid", except: options.except ?? [], prefer: options.prefer ?? "token", severity: options.severity ?? "error", }; }, }; }, rawSpacing() { return { mustMatchScale( scale: string, options: SeverityOption & { appliesTo?: string[] } = {} ): GlobalStyleGovernanceRecord { return { kind: "style.rawSpacing.mustMatchScale", scale, appliesTo: options.appliesTo ?? [], severity: options.severity ?? "error", }; }, }; }, fontSize() { return { mustMatchScale(scale: string, options: SeverityOption = {}): GlobalStyleGovernanceRecord { return { kind: "style.fontSize.mustMatchScale", scale, severity: options.severity ?? "warn", }; }, }; }, cssVars() { return { mustBeDefined(options: SeverityOption = {}): GlobalStyleGovernanceRecord { return { kind: "style.cssVars.mustBeDefined", severity: options.severity ?? "error", }; }, }; }, }, jsx: { unknownProps() { return { forbid(options: SeverityOption = {}): GlobalJsxGovernanceRecord { return { kind: "jsx.unknownProps.forbid", severity: options.severity ?? "error", }; }, }; }, inlineStyle() { return { forbidRaw( properties: readonly string[], options: SeverityOption = {} ): GlobalJsxGovernanceRecord { return { kind: "jsx.inlineStyle.forbidRaw", properties: [...properties], severity: options.severity ?? "error", }; }, }; }, importPath() { return { prefer( from: string, to: string, options: SeverityOption & { imported?: string; because?: string } = {} ): GlobalJsxGovernanceRecord { return { kind: "jsx.importPath.prefer", from, to, imported: options.imported, because: options.because, severity: options.severity ?? "error", }; }, }; }, component() { return { prefer( from: string, to: string, options: SeverityOption & { because?: string } = {} ): GlobalJsxGovernanceRecord { return { kind: "jsx.component.prefer", from, to, because: options.because, severity: options.severity ?? "error", }; }, }; }, }, component: createComponentGovernanceBuilder>(), };