diff --git a/README.md b/README.md index 1e65e36f..885e6f53 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,7 @@ The linter runs seven rules against a parsed DESIGN.md. Each rule produces findi | `orphaned-tokens` | warning | Color tokens defined but never referenced by any component | | `token-summary` | info | Summary of how many tokens are defined in each section | | `missing-sections` | info | Optional sections (spacing, rounded) absent when other tokens exist | +| `missing-typography` | warning | Colors are defined but no typography tokens exist — agents will use default fonts | | `section-order` | warning | Sections appear out of the canonical order defined by the spec | ### Programmatic API diff --git a/packages/cli/src/commands/spec.test.ts b/packages/cli/src/commands/spec.test.ts index 370660f9..e1f5d555 100644 --- a/packages/cli/src/commands/spec.test.ts +++ b/packages/cli/src/commands/spec.test.ts @@ -75,6 +75,6 @@ describe('spec command', () => { const output = JSON.parse(outputStr); expect(output.spec).toBeDefined(); expect(output.rules).toBeDefined(); - expect(output.rules.length).toBe(7); + expect(output.rules.length).toBe(8); }); }); diff --git a/packages/cli/src/linter/index.ts b/packages/cli/src/linter/index.ts index 43365a57..0c70cc00 100644 --- a/packages/cli/src/linter/index.ts +++ b/packages/cli/src/linter/index.ts @@ -28,6 +28,7 @@ export { orphanedTokens, tokenSummary, missingSections, + missingTypography, } from './linter/rules/index.js'; export { contrastRatio } from './model/handler.js'; export { TailwindEmitterHandler } from './tailwind/handler.js'; diff --git a/packages/cli/src/linter/linter/rules/index.ts b/packages/cli/src/linter/linter/rules/index.ts index 3c4e8435..eb7dbfef 100644 --- a/packages/cli/src/linter/linter/rules/index.ts +++ b/packages/cli/src/linter/linter/rules/index.ts @@ -8,6 +8,7 @@ import { orphanedTokensRule } from './orphaned-tokens.js'; import { tokenSummaryRule } from './token-summary.js'; import { missingSectionsRule } from './missing-sections.js'; import { sectionOrderRule } from './section-order.js'; +import { missingTypographyRule } from './missing-typography.js'; /** The default set of lint rule descriptors, in order. */ export const DEFAULT_RULE_DESCRIPTORS: RuleDescriptor[] = [ @@ -17,6 +18,7 @@ export const DEFAULT_RULE_DESCRIPTORS: RuleDescriptor[] = [ orphanedTokensRule, tokenSummaryRule, missingSectionsRule, + missingTypographyRule, sectionOrderRule, ]; @@ -40,5 +42,6 @@ export { contrastCheck } from './contrast-ratio.js'; export { orphanedTokens } from './orphaned-tokens.js'; export { tokenSummary } from './token-summary.js'; export { missingSections } from './missing-sections.js'; +export { missingTypography } from './missing-typography.js'; export { sectionOrder } from './section-order.js'; export type { LintRule } from './types.js'; diff --git a/packages/cli/src/linter/linter/rules/missing-typography.test.ts b/packages/cli/src/linter/linter/rules/missing-typography.test.ts new file mode 100644 index 00000000..3a7f9492 --- /dev/null +++ b/packages/cli/src/linter/linter/rules/missing-typography.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'bun:test'; +import { missingTypography } from './missing-typography.js'; +import { buildState } from './test-helpers.js'; + +describe('missingTypography', () => { + it('emits warning when colors exist but no typography defined', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + // no typography + }); + const findings = missingTypography(state); + expect(findings.length).toBe(1); + expect(findings[0]!.path).toBe('typography'); + expect(findings[0]!.message).toMatch(/typography/i); + }); + + it('returns empty when typography IS defined', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + typography: { + 'body-md': { + fontFamily: 'Inter', + fontSize: '16px', + }, + }, + }); + expect(missingTypography(state)).toEqual([]); + }); + + it('returns empty when no colors defined (nothing to compare against)', () => { + const state = buildState({}); + expect(missingTypography(state)).toEqual([]); + }); +}); diff --git a/packages/cli/src/linter/linter/rules/missing-typography.ts b/packages/cli/src/linter/linter/rules/missing-typography.ts new file mode 100644 index 00000000..0d8378c0 --- /dev/null +++ b/packages/cli/src/linter/linter/rules/missing-typography.ts @@ -0,0 +1,24 @@ +import type { DesignSystemState } from '../../model/spec.js'; +import type { RuleDescriptor, RuleFinding } from './types.js'; + +/** + * Missing typography — warns when colors are defined but no typography tokens exist. + * Without typography tokens, agents will fall back to their own font choices, + * reducing the author's control over the design system's typographic identity. + */ +export function missingTypography(state: DesignSystemState): RuleFinding[] { + if (state.typography.size === 0 && state.colors.size > 0) { + return [{ + path: 'typography', + message: "No typography tokens defined. Agents will use default font choices, reducing your control over the design system's typographic identity.", + }]; + } + return []; +} + +export const missingTypographyRule: RuleDescriptor = { + name: 'missing-typography', + severity: 'warning', + description: "Missing typography — warns when colors are defined but no typography tokens exist.", + run: missingTypography, +}; diff --git a/packages/cli/src/linter/linter/rules/types.test.ts b/packages/cli/src/linter/linter/rules/types.test.ts index 360820dd..634c5ecc 100644 --- a/packages/cli/src/linter/linter/rules/types.test.ts +++ b/packages/cli/src/linter/linter/rules/types.test.ts @@ -26,7 +26,7 @@ describe('LintRule type', () => { }); it('has all rules in DEFAULT_RULE_DESCRIPTORS', () => { - expect(DEFAULT_RULE_DESCRIPTORS.length).toBe(7); + expect(DEFAULT_RULE_DESCRIPTORS.length).toBe(8); DEFAULT_RULE_DESCRIPTORS.forEach((rule: RuleDescriptor) => { expect(rule.name).toBeTruthy(); expect(rule.severity).toBeTruthy();