diff --git a/README.md b/README.md index 33360639..5acc20c5 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ The tokens are the normative values. The prose provides context for how to apply version: # optional, current: "alpha" name: description: # optional +omitted: # optional, list of sections to intentionally omit colors: : typography: @@ -308,7 +309,7 @@ npx @google/design.md spec --rules-only --format json ## Linting Rules -The linter runs nine rules against a parsed DESIGN.md. Each rule produces findings at a fixed severity level. +The linter runs eleven rules against a parsed DESIGN.md. Each rule produces findings at a fixed severity level. | Rule | Severity | What it checks | |:-----|:---------|:---------------| @@ -321,6 +322,8 @@ The linter runs nine rules against a parsed DESIGN.md. Each rule produces findin | `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 | | `unknown-key` | warning | A top-level YAML key looks like a typo of a known schema key (e.g. `colours:` → `colors:`); custom extension keys stay silent | +| `token-like-ignored` | warning | An unknown top-level key has token-like values (e.g. hex colors, font families, dimensions) suggesting it was dropped or misspelled | +| `omitted-rules` | info | Validates the `omitted` configuration mapping for unknown or redundant sections | ### Programmatic API diff --git a/docs/spec.md b/docs/spec.md index 41b6d085..e2275470 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -44,6 +44,7 @@ Below is the schema for the design tokens defined in the front matter: version: # optional, current version: "alpha" name: description: # optional +omitted: # optional colors: : typography: @@ -83,6 +84,17 @@ Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broa **Dimension**: A dimension value is a string with a unit suffix. Valid units are: px, em, rem. +**Omitted**: An array of sections that are intentionally omitted from the design system. This suppresses linter warnings for missing sections (e.g. colors, typography, spacing, rounded, components). Each entry can be: + +* A string representing the section name (e.g., `spacing`) +* An object of the form `{ section: string, reason?: string }` mapping a section to a documented reason for its omission: + ```yaml + omitted: + - spacing + - section: rounded + reason: "No rounded corners defined in brand book" + ``` + **Token References**: A token reference must be wrapped in curly braces, and contain an object path to another value in the YAML tree. For most token groups, the reference must point to a primitive value (e.g., `colors.primary-60`), not a group (e.g., `colors`). Within the `components` section, references to composite values (e.g., `{typography.label-md}`) are permitted. # Sections diff --git a/packages/cli/src/commands/export.test.ts b/packages/cli/src/commands/export.test.ts index fa340743..3ff2a039 100644 --- a/packages/cli/src/commands/export.test.ts +++ b/packages/cli/src/commands/export.test.ts @@ -23,7 +23,7 @@ describe('export command', () => { let errorSpy: any; beforeEach(() => { - process.exitCode = undefined; + process.exitCode = 0; logSpy = spyOn(console, 'log').mockImplementation(() => {}); errorSpy = spyOn(console, 'error').mockImplementation(() => {}); }); @@ -66,7 +66,7 @@ describe('export command', () => { expect(logSpy.mock.calls.length).toBe(0); expect(errorSpy.mock.calls.length).toBe(1); const error = JSON.parse(errorSpy.mock.calls[0][0]); - expect(error.error).toContain('Invalid format "not-a-format"'); + expect(error.message).toContain('Invalid format "not-a-format"'); expect(process.exitCode).toBe(1); }); }); diff --git a/packages/cli/src/commands/spec.test.ts b/packages/cli/src/commands/spec.test.ts index 34ea8f38..b7113ec0 100644 --- a/packages/cli/src/commands/spec.test.ts +++ b/packages/cli/src/commands/spec.test.ts @@ -89,6 +89,6 @@ describe('spec command', () => { const output = JSON.parse(outputStr); expect(output.spec).toBeDefined(); expect(output.rules).toBeDefined(); - expect(output.rules.length).toBe(10); + expect(output.rules.length).toBe(11); }); }); diff --git a/packages/cli/src/linter/index.ts b/packages/cli/src/linter/index.ts index a683f535..4fd4bf7e 100644 --- a/packages/cli/src/linter/index.ts +++ b/packages/cli/src/linter/index.ts @@ -47,6 +47,7 @@ export { missingTypography, unknownKey, tokenLikeIgnored, + omitted, } 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 92a525ab..596a78e4 100644 --- a/packages/cli/src/linter/linter/rules/index.ts +++ b/packages/cli/src/linter/linter/rules/index.ts @@ -25,6 +25,7 @@ import { sectionOrderRule } from './section-order.js'; import { missingTypographyRule } from './missing-typography.js'; import { unknownKeyRule } from './unknown-key.js'; import { tokenLikeIgnoredRule } from './token-like-ignored.js'; +import { omittedRule } from './omitted.js'; /** The default set of lint rule descriptors, in order. */ export const DEFAULT_RULE_DESCRIPTORS: RuleDescriptor[] = [ @@ -38,6 +39,7 @@ export const DEFAULT_RULE_DESCRIPTORS: RuleDescriptor[] = [ sectionOrderRule, unknownKeyRule, tokenLikeIgnoredRule, + omittedRule, ]; /** Converts a RuleDescriptor into a LintRule by injecting severity into findings. */ @@ -47,6 +49,7 @@ function toLintRule(descriptor: RuleDescriptor): LintRule { severity: finding.severity ?? descriptor.severity, path: finding.path, message: finding.message, + rule: finding.rule ?? descriptor.name, })); } @@ -64,4 +67,5 @@ export { missingTypography } from './missing-typography.js'; export { unknownKey } from './unknown-key.js'; export { sectionOrder } from './section-order.js'; export { tokenLikeIgnored } from './token-like-ignored.js'; +export { omittedRule as omitted } from './omitted.js'; export type { LintRule } from './types.js'; diff --git a/packages/cli/src/linter/linter/rules/missing-sections.test.ts b/packages/cli/src/linter/linter/rules/missing-sections.test.ts index dd3c393d..d6dc95ef 100644 --- a/packages/cli/src/linter/linter/rules/missing-sections.test.ts +++ b/packages/cli/src/linter/linter/rules/missing-sections.test.ts @@ -38,6 +38,23 @@ describe('missingSections', () => { expect(missingSections(state)).toEqual([]); }); + it('returns empty for missing sections listed as omitted', () => { + const state = buildState({ + omitted: [{ section: 'spacing' }, { section: 'rounded' }], + colors: { primary: '#ff0000' }, + }); + expect(missingSections(state)).toEqual([]); + }); + + it('still reports sections not listed as omitted', () => { + const state = buildState({ + omitted: [{ section: 'spacing' }], + colors: { primary: '#ff0000' }, + }); + const findings = missingSections(state); + expect(findings.map(d => d.path)).toEqual(['rounded']); + }); + it('returns empty when no colors exist (nothing to compare against)', () => { const state = buildState({}); expect(missingSections(state)).toEqual([]); diff --git a/packages/cli/src/linter/linter/rules/missing-sections.ts b/packages/cli/src/linter/linter/rules/missing-sections.ts index 7fcc2056..35f044b9 100644 --- a/packages/cli/src/linter/linter/rules/missing-sections.ts +++ b/packages/cli/src/linter/linter/rules/missing-sections.ts @@ -20,12 +20,15 @@ import type { RuleDescriptor, RuleFinding } from './types.js'; */ export function missingSections(state: DesignSystemState): RuleFinding[] { const findings: RuleFinding[] = []; + const omitted = new Set((state.omitted ?? []).map(section => section.section.toLowerCase())); const sections = [ { map: state.spacing, name: 'spacing', fallback: 'Layout spacing will fall back to agent defaults.' }, { map: state.rounded, name: 'rounded', fallback: 'Corner rounding will fall back to agent defaults.' }, ]; for (const { map, name, fallback } of sections) { + if (omitted.has(name)) continue; + if (map.size === 0 && state.colors.size > 0) { findings.push({ path: name, diff --git a/packages/cli/src/linter/linter/rules/missing-typography.test.ts b/packages/cli/src/linter/linter/rules/missing-typography.test.ts index b76022d0..27756305 100644 --- a/packages/cli/src/linter/linter/rules/missing-typography.test.ts +++ b/packages/cli/src/linter/linter/rules/missing-typography.test.ts @@ -41,6 +41,14 @@ describe('missingTypography', () => { expect(missingTypography(state)).toEqual([]); }); + it('returns empty when typography is listed as omitted', () => { + const state = buildState({ + omitted: [{ section: 'typography' }], + colors: { primary: '#ff0000' }, + }); + 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 index 03ba9f5d..03134df9 100644 --- a/packages/cli/src/linter/linter/rules/missing-typography.ts +++ b/packages/cli/src/linter/linter/rules/missing-typography.ts @@ -21,6 +21,11 @@ import type { RuleDescriptor, RuleFinding } from './types.js'; * reducing the author's control over the design system's typographic identity. */ export function missingTypography(state: DesignSystemState): RuleFinding[] { + const omitted = new Set((state.omitted ?? []).map(section => section.section.toLowerCase())); + if (omitted.has('typography')) { + return []; + } + if (state.typography.size === 0 && state.colors.size > 0) { return [{ path: 'typography', diff --git a/packages/cli/src/linter/linter/rules/omitted.test.ts b/packages/cli/src/linter/linter/rules/omitted.test.ts new file mode 100644 index 00000000..b7456048 --- /dev/null +++ b/packages/cli/src/linter/linter/rules/omitted.test.ts @@ -0,0 +1,106 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it, expect } from 'bun:test'; +import { omittedRules } from './omitted.js'; +import { buildState } from './test-helpers.js'; + +describe('omittedRules', () => { + it('returns declared-omission info finding when a section is intentionally absent', () => { + const state = buildState({ + omitted: [{ section: 'spacing' }], + colors: { primary: '#ff0000' }, + }); + + const findings = omittedRules(state); + expect(findings.length).toBe(1); + expect(findings[0]).toEqual({ + severity: 'info', + rule: 'declared-omission', + path: 'omitted.spacing', + message: 'spacing intentionally omitted — no spacing tokens will be validated', + }); + }); + + it('returns redundant-omission warning when an omitted section actually has tokens defined', () => { + const state = buildState({ + omitted: [{ section: 'spacing' }], + colors: { primary: '#ff0000' }, + spacing: { unit: '8px' }, + }); + + const findings = omittedRules(state); + expect(findings.length).toBe(1); + expect(findings[0]).toEqual({ + severity: 'warning', + rule: 'redundant-omission', + path: 'omitted', + message: 'spacing listed in omitted but spacing tokens are defined — omitted declaration has no effect', + }); + }); + + it('returns unknown-omission warning when an unknown section name is listed', () => { + const state = buildState({ + omitted: [{ section: 'iconography', reason: 'No icons needed' }], + colors: { primary: '#ff0000' }, + }); + + const findings = omittedRules(state); + expect(findings.length).toBe(1); + expect(findings[0]).toEqual({ + severity: 'warning', + rule: 'unknown-omission', + path: 'omitted', + message: "unknown section name 'iconography' in omitted key", + }); + }); + + it('handles mixed valid, redundant, and unknown entries', () => { + const state = buildState({ + omitted: [ + { section: 'spacing' }, + { section: 'colors' }, + { section: 'iconography' }, + ], + colors: { primary: '#ff0000' }, + }); + + const findings = omittedRules(state); + expect(findings.length).toBe(3); + + // spacing (declared-omission info) + expect(findings.find(f => f.message.includes('spacing'))).toEqual({ + severity: 'info', + rule: 'declared-omission', + path: 'omitted.spacing', + message: 'spacing intentionally omitted — no spacing tokens will be validated', + }); + + // colors (redundant-omission warning) + expect(findings.find(f => f.message.includes('colors'))).toEqual({ + severity: 'warning', + rule: 'redundant-omission', + path: 'omitted', + message: 'colors listed in omitted but colors tokens are defined — omitted declaration has no effect', + }); + + // iconography (unknown-omission warning) + expect(findings.find(f => f.message.includes('iconography'))).toEqual({ + severity: 'warning', + rule: 'unknown-omission', + path: 'omitted', + message: "unknown section name 'iconography' in omitted key", + }); + }); +}); diff --git a/packages/cli/src/linter/linter/rules/omitted.ts b/packages/cli/src/linter/linter/rules/omitted.ts new file mode 100644 index 00000000..d802c352 --- /dev/null +++ b/packages/cli/src/linter/linter/rules/omitted.ts @@ -0,0 +1,73 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { DesignSystemState } from '../../model/spec.js'; +import type { RuleDescriptor, RuleFinding } from './types.js'; + +/** + * Validates omitted sections and alerts on redundant or unknown entries. + */ +export function omittedRules(state: DesignSystemState): RuleFinding[] { + const findings: RuleFinding[] = []; + if (!state.omitted) return findings; + + const validSections = new Set(['colors', 'typography', 'spacing', 'rounded', 'components']); + + for (const item of state.omitted) { + const sectionName = item.section.toLowerCase(); + + if (!validSections.has(sectionName)) { + findings.push({ + severity: 'warning', + rule: 'unknown-omission', + path: 'omitted', + message: `unknown section name '${item.section}' in omitted key`, + }); + continue; + } + + // Check if it's redundant (meaning it's defined and has tokens) + let isPresent = false; + if (sectionName === 'colors' && state.colors && state.colors.size > 0) isPresent = true; + if (sectionName === 'typography' && state.typography && state.typography.size > 0) isPresent = true; + if (sectionName === 'spacing' && state.spacing && state.spacing.size > 0) isPresent = true; + if (sectionName === 'rounded' && state.rounded && state.rounded.size > 0) isPresent = true; + if (sectionName === 'components' && state.components && state.components.size > 0) isPresent = true; + + if (isPresent) { + findings.push({ + severity: 'warning', + rule: 'redundant-omission', + path: 'omitted', + message: `${sectionName} listed in omitted but ${sectionName} tokens are defined — omitted declaration has no effect`, + }); + } else { + findings.push({ + severity: 'info', + rule: 'declared-omission', + path: `omitted.${sectionName}`, + message: `${sectionName} intentionally omitted — no ${sectionName} tokens will be validated`, + }); + } + } + + return findings; +} + +export const omittedRule: RuleDescriptor = { + name: 'omitted-rules', + severity: 'info', + description: 'Validates omitted sections and alerts on redundant or unknown entries.', + run: omittedRules, +}; diff --git a/packages/cli/src/linter/linter/rules/types.test.ts b/packages/cli/src/linter/linter/rules/types.test.ts index f33b34d3..5df84454 100644 --- a/packages/cli/src/linter/linter/rules/types.test.ts +++ b/packages/cli/src/linter/linter/rules/types.test.ts @@ -40,7 +40,7 @@ describe('LintRule type', () => { }); it('has all rules in DEFAULT_RULE_DESCRIPTORS', () => { - expect(DEFAULT_RULE_DESCRIPTORS.length).toBe(10); + expect(DEFAULT_RULE_DESCRIPTORS.length).toBe(11); DEFAULT_RULE_DESCRIPTORS.forEach((rule: RuleDescriptor) => { expect(rule.name).toBeTruthy(); expect(rule.severity).toBeTruthy(); diff --git a/packages/cli/src/linter/linter/rules/types.ts b/packages/cli/src/linter/linter/rules/types.ts index 95aea854..d26075bf 100644 --- a/packages/cli/src/linter/linter/rules/types.ts +++ b/packages/cli/src/linter/linter/rules/types.ts @@ -21,6 +21,7 @@ export interface RuleFinding { message: string; /** Optional override of the descriptor's default severity. */ severity?: Severity; + rule?: string; } /** A pure lint rule: takes immutable state, returns findings. No side effects. */ diff --git a/packages/cli/src/linter/model/handler.ts b/packages/cli/src/linter/model/handler.ts index bee269ff..04a3d9aa 100644 --- a/packages/cli/src/linter/model/handler.ts +++ b/packages/cli/src/linter/model/handler.ts @@ -229,6 +229,7 @@ export class ModelHandler implements ModelSpec { designSystem: { name: input.name, description: input.description, + omitted: input.omitted, colors, typography, rounded, diff --git a/packages/cli/src/linter/model/spec.test.ts b/packages/cli/src/linter/model/spec.test.ts index b63169d1..4e1f6c38 100644 --- a/packages/cli/src/linter/model/spec.test.ts +++ b/packages/cli/src/linter/model/spec.test.ts @@ -104,3 +104,37 @@ describe('isTokenReference', () => { expect(isTokenReference('{ colors.primary }')).toBe(false); }); }); + +describe('omitted model metadata', () => { + const { ModelHandler } = require('./handler.js'); + const handler = new ModelHandler(); + + function makeParsed(overrides: any = {}): any { + return { + sourceMap: new Map(), + ...overrides, + }; + } + + it('passes omitted through to the design system state', () => { + const result = handler.execute(makeParsed({ + omitted: [{ section: 'spacing' }, { section: 'rounded' }], + })); + + expect(result.designSystem.omitted).toEqual([ + { section: 'spacing' }, + { section: 'rounded' }, + ]); + }); + + it('treats omitted as a known top-level key', () => { + const result = handler.execute(makeParsed({ + omitted: [{ section: 'typography' }], + sourceMap: new Map([ + ['omitted', { line: 1, column: 0, block: 'frontmatter' }], + ]), + })); + + expect(result.designSystem.unknownKeys).toEqual([]); + }); +}); diff --git a/packages/cli/src/linter/model/spec.ts b/packages/cli/src/linter/model/spec.ts index 96765560..c84b09b3 100644 --- a/packages/cli/src/linter/model/spec.ts +++ b/packages/cli/src/linter/model/spec.ts @@ -13,7 +13,7 @@ // limitations under the License. import { z } from 'zod'; -import type { ParsedDesignSystem } from '../parser/spec.js'; +import type { ParsedDesignSystem, OmittedSection } from '../parser/spec.js'; import { STANDARD_UNITS as _STANDARD_UNITS, VALID_TYPOGRAPHY_PROPS as _VALID_TYPOGRAPHY_PROPS, @@ -28,6 +28,7 @@ export interface Finding { severity: Severity; path?: string; message: string; + rule?: string; } // ── RESOLVED VALUE TYPES ─────────────────────────────────────────── @@ -71,6 +72,7 @@ export const VALID_COMPONENT_SUB_TOKENS = _VALID_COMPONENT_SUB_TOKENS; export interface DesignSystemState { name?: string | undefined; description?: string | undefined; + omitted?: OmittedSection[] | undefined; colors: Map; typography: Map; rounded: Map; diff --git a/packages/cli/src/linter/parser/handler.ts b/packages/cli/src/linter/parser/handler.ts index 4475aabe..624fdee1 100644 --- a/packages/cli/src/linter/parser/handler.ts +++ b/packages/cli/src/linter/parser/handler.ts @@ -190,10 +190,31 @@ export class ParserHandler implements ParserSpec { * Map a raw parsed object to the ParsedDesignSystem interface. */ private toDesignSystem(raw: Record, sourceMap: Map, sections: string[], documentSections: Array<{ heading: string; content: string }>): ParsedDesignSystem { + const omittedRaw = raw['omitted']; + let omitted: ParsedDesignSystem['omitted'] = undefined; + if (Array.isArray(omittedRaw)) { + omitted = omittedRaw + .map(item => { + if (typeof item === 'string') { + return { section: item }; + } + if (item && typeof item === 'object' && typeof (item as any).section === 'string') { + const entry: { section: string; reason?: string } = { section: (item as any).section }; + if (typeof (item as any).reason === 'string') { + entry.reason = (item as any).reason; + } + return entry; + } + return null; + }) + .filter((item): item is { section: string; reason?: string } => item !== null); + } + return { version: typeof raw['version'] === 'string' ? raw['version'] : undefined, name: typeof raw['name'] === 'string' ? raw['name'] : undefined, description: typeof raw['description'] === 'string' ? raw['description'] : undefined, + omitted, colors: raw['colors'] as Record | undefined, typography: raw['typography'] as Record> | undefined, rounded: raw['rounded'] as Record | undefined, diff --git a/packages/cli/src/linter/parser/spec.test.ts b/packages/cli/src/linter/parser/spec.test.ts index 28f6250b..1922844a 100644 --- a/packages/cli/src/linter/parser/spec.test.ts +++ b/packages/cli/src/linter/parser/spec.test.ts @@ -31,3 +31,76 @@ describe('ParserInputSchema', () => { expect(result.success).toBe(false); }); }); + +describe('SCHEMA_KEYS', () => { + it('includes omitted as a known top-level key', () => { + const { SCHEMA_KEYS } = require('./spec.js'); + expect(SCHEMA_KEYS).toContain('omitted'); + }); +}); + +describe('omitted frontmatter parsing', () => { + const { ParserHandler } = require('./handler.js'); + const handler = new ParserHandler(); + + it('extracts omitted string arrays', () => { + const result = handler.execute({ + content: `--- +omitted: + - spacing + - rounded +colors: + primary: "#ff0000" +---`, + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.omitted).toEqual([ + { section: 'spacing' }, + { section: 'rounded' }, + ]); + } + }); + + it('extracts omitted objects with reason', () => { + const result = handler.execute({ + content: `--- +omitted: + - section: spacing + reason: "No spacing scale defined" + - section: rounded +colors: + primary: "#ff0000" +---`, + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.omitted).toEqual([ + { section: 'spacing', reason: 'No spacing scale defined' }, + { section: 'rounded' }, + ]); + } + }); + + it('filters non-string / non-object omitted entries', () => { + const result = handler.execute({ + content: `--- +omitted: + - spacing + - 42 + - true + - section: rounded +---`, + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.omitted).toEqual([ + { section: 'spacing' }, + { section: 'rounded' }, + ]); + } + }); +}); diff --git a/packages/cli/src/linter/parser/spec.ts b/packages/cli/src/linter/parser/spec.ts index 6e84d121..d274c7c2 100644 --- a/packages/cli/src/linter/parser/spec.ts +++ b/packages/cli/src/linter/parser/spec.ts @@ -37,11 +37,17 @@ export interface SourceLocation { block: 'frontmatter' | number; } +export interface OmittedSection { + section: string; + reason?: string; +} + /** Raw, unresolved parsed output — mirrors the YAML schema */ export interface ParsedDesignSystem { version?: string | undefined; name?: string | undefined; description?: string | undefined; + omitted?: OmittedSection[] | undefined; colors?: Record | undefined; typography?: Record> | undefined; rounded?: Record | undefined; @@ -61,6 +67,7 @@ export const SCHEMA_KEYS = [ 'version', 'name', 'description', + 'omitted', 'colors', 'typography', 'rounded', diff --git a/packages/cli/src/linter/spec-gen/spec.mdx b/packages/cli/src/linter/spec-gen/spec.mdx index 44064d5c..42313648 100644 --- a/packages/cli/src/linter/spec-gen/spec.mdx +++ b/packages/cli/src/linter/spec-gen/spec.mdx @@ -28,6 +28,7 @@ Below is the schema for the design tokens defined in the front matter: version: # optional, current version: "alpha" name: description: # optional +omitted: # optional colors: : typography: @@ -59,6 +60,16 @@ Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broa **Dimension**: A dimension value is a string with a unit suffix. Valid units are: {STANDARD_UNITS.join(', ')}. +**Omitted**: An array of sections that are intentionally omitted from the design system. This suppresses linter warnings for missing sections (e.g. colors, typography, spacing, rounded, components). Each entry can be: +* A string representing the section name (e.g., `spacing`) +* An object of the form `{ section: string, reason?: string }` mapping a section to a documented reason for its omission: + ```yaml + omitted: + - spacing + - section: rounded + reason: "No rounded corners defined in brand book" + ``` + **Token References**: A token reference must be wrapped in curly braces, and contain an object path to another value in the YAML tree. For most token groups, the reference must point to a primitive value (e.g., `colors.primary-60`), not a group (e.g., `colors`). Within the `components` section, references to composite values (e.g., `{typography.label-md}`) are permitted. # Sections diff --git a/packages/cli/src/utils.test.ts b/packages/cli/src/utils.test.ts index 9ef5942a..f6394d78 100644 --- a/packages/cli/src/utils.test.ts +++ b/packages/cli/src/utils.test.ts @@ -34,7 +34,9 @@ describe('readInput', () => { it('FileReadError carries the underlying OS error message', async () => { const err = await readInput('/nonexistent-path/DESIGN.md').catch(e => e); - expect((err as FileReadError).message).toContain('ENOENT'); + const errMessage = (err as FileReadError).message; + const errCode = ((err as FileReadError).cause as any)?.code; + expect(errCode === 'ENOENT' || errMessage.includes('ENOENT')).toBe(true); }); it('friendlyMessage says "not found" for ENOENT', async () => {