diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts index 1bb50d5b..87b1a0c8 100644 --- a/packages/cli/src/commands/export.ts +++ b/packages/cli/src/commands/export.ts @@ -62,7 +62,7 @@ export default defineCommand({ return; } - console.log(serializeTailwindV4(result.data.theme)); + process.stdout.write(serializeTailwindV4(result.data.theme)); } else if (format === 'json-tailwind' || format === 'tailwind') { const handler = new TailwindEmitterHandler(); const result = handler.execute(report.designSystem); diff --git a/packages/cli/src/commands/spec.test.ts b/packages/cli/src/commands/spec.test.ts index 34ea8f38..1d5ca972 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(13); }); }); diff --git a/packages/cli/src/linter/linter/rules/declared-omission.test.ts b/packages/cli/src/linter/linter/rules/declared-omission.test.ts new file mode 100644 index 00000000..a0dcc453 --- /dev/null +++ b/packages/cli/src/linter/linter/rules/declared-omission.test.ts @@ -0,0 +1,56 @@ +// 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 { declaredOmission } from './declared-omission.js'; +import { buildState } from './test-helpers.js'; + +describe('declaredOmission', () => { + it('emits info for each omitted section (bare string form)', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + omitted: ['spacing', 'rounded'], + }); + const findings = declaredOmission(state); + expect(findings.length).toBe(2); + expect(findings[0]!.path).toBe('omitted.spacing'); + expect(findings[0]!.severity).toBe('info'); + expect(findings[1]!.path).toBe('omitted.rounded'); + }); + + it('emits info with reason when object form is used', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + omitted: [ + { section: 'spacing', reason: 'No spacing scale defined in source material' }, + ], + }); + const findings = declaredOmission(state); + expect(findings.length).toBe(1); + expect(findings[0]!.path).toBe('omitted.spacing'); + expect(findings[0]!.message).toContain('No spacing scale defined'); + }); + + it('returns empty when no omitted key', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + }); + expect(declaredOmission(state)).toEqual([]); + }); + + it('returns empty for empty state', () => { + const state = buildState({}); + expect(declaredOmission(state)).toEqual([]); + }); +}); diff --git a/packages/cli/src/linter/linter/rules/declared-omission.ts b/packages/cli/src/linter/linter/rules/declared-omission.ts new file mode 100644 index 00000000..e05515bd --- /dev/null +++ b/packages/cli/src/linter/linter/rules/declared-omission.ts @@ -0,0 +1,43 @@ +// 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'; +import { getSectionName } from './omitted-utils.js'; + +/** + * Declared omission — acknowledges sections that are intentionally absent. + */ +export function declaredOmission(state: DesignSystemState): RuleFinding[] { + const findings: RuleFinding[] = []; + if (!state.omitted) return findings; + + for (const entry of state.omitted) { + const section = getSectionName(entry); + const reason = typeof entry === 'object' && entry.reason ? ` Reason: ${entry.reason}` : ''; + findings.push({ + path: `omitted.${section}`, + message: `'${section}' intentionally omitted — no ${section} tokens will be validated.${reason}`, + severity: 'info', + }); + } + return findings; +} + +export const declaredOmissionRule: RuleDescriptor = { + name: 'declared-omission', + severity: 'info', + description: 'Declared omission — acknowledges sections that are intentionally absent.', + run: declaredOmission, +}; diff --git a/packages/cli/src/linter/linter/rules/index.ts b/packages/cli/src/linter/linter/rules/index.ts index 92a525ab..e24f09c0 100644 --- a/packages/cli/src/linter/linter/rules/index.ts +++ b/packages/cli/src/linter/linter/rules/index.ts @@ -25,6 +25,9 @@ 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 { declaredOmissionRule } from './declared-omission.js'; +import { redundantOmissionRule } from './redundant-omission.js'; +import { unknownOmissionRule } from './unknown-omission.js'; /** The default set of lint rule descriptors, in order. */ export const DEFAULT_RULE_DESCRIPTORS: RuleDescriptor[] = [ @@ -38,6 +41,9 @@ export const DEFAULT_RULE_DESCRIPTORS: RuleDescriptor[] = [ sectionOrderRule, unknownKeyRule, tokenLikeIgnoredRule, + declaredOmissionRule, + redundantOmissionRule, + unknownOmissionRule, ]; /** Converts a RuleDescriptor into a LintRule by injecting severity into findings. */ @@ -64,4 +70,7 @@ 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 { declaredOmission } from './declared-omission.js'; +export { redundantOmission } from './redundant-omission.js'; +export { unknownOmission } from './unknown-omission.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..c553af09 100644 --- a/packages/cli/src/linter/linter/rules/missing-sections.test.ts +++ b/packages/cli/src/linter/linter/rules/missing-sections.test.ts @@ -42,4 +42,25 @@ describe('missingSections', () => { const state = buildState({}); expect(missingSections(state)).toEqual([]); }); + + it('suppresses spacing warning when spacing is declared omitted', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + rounded: { regular: '4px' }, + // no spacing — but declared omitted + omitted: ['spacing'], + }); + const findings = missingSections(state); + expect(findings.length).toBe(0); + }); + + it('suppresses rounded warning when rounded is declared omitted', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + spacing: { unit: '8px' }, + omitted: ['rounded'], + }); + const findings = missingSections(state); + expect(findings.length).toBe(0); + }); }); diff --git a/packages/cli/src/linter/linter/rules/missing-sections.ts b/packages/cli/src/linter/linter/rules/missing-sections.ts index 7fcc2056..27d2810d 100644 --- a/packages/cli/src/linter/linter/rules/missing-sections.ts +++ b/packages/cli/src/linter/linter/rules/missing-sections.ts @@ -14,19 +14,21 @@ import type { DesignSystemState } from '../../model/spec.js'; import type { RuleDescriptor, RuleFinding } from './types.js'; +import { getOmittedSections } from './omitted-utils.js'; /** * Missing sections — notes when optional sections (spacing, rounded) are absent. */ export function missingSections(state: DesignSystemState): RuleFinding[] { const findings: RuleFinding[] = []; + const omittedSections = getOmittedSections(state); 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 (map.size === 0 && state.colors.size > 0) { + if (map.size === 0 && state.colors.size > 0 && !omittedSections.has(name)) { findings.push({ path: name, message: `No '${name}' section defined. ${fallback}`, 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..6cd46479 100644 --- a/packages/cli/src/linter/linter/rules/missing-typography.test.ts +++ b/packages/cli/src/linter/linter/rules/missing-typography.test.ts @@ -45,4 +45,12 @@ describe('missingTypography', () => { const state = buildState({}); expect(missingTypography(state)).toEqual([]); }); + + it('suppresses warning when typography is declared omitted', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + omitted: ['typography'], + }); + 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..0e1c67f7 100644 --- a/packages/cli/src/linter/linter/rules/missing-typography.ts +++ b/packages/cli/src/linter/linter/rules/missing-typography.ts @@ -19,9 +19,13 @@ 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. + * Suppressed when typography is declared in the `omitted` key. */ export function missingTypography(state: DesignSystemState): RuleFinding[] { - if (state.typography.size === 0 && state.colors.size > 0) { + const typographyOmitted = state.omitted?.some( + e => (typeof e === 'string' ? e : e.section) === 'typography' + ); + if (state.typography.size === 0 && state.colors.size > 0 && !typographyOmitted) { return [{ path: 'typography', message: "No typography tokens defined. Agents will use default font choices, reducing your control over the design system's typographic identity.", diff --git a/packages/cli/src/linter/linter/rules/omitted-utils.ts b/packages/cli/src/linter/linter/rules/omitted-utils.ts new file mode 100644 index 00000000..0fa477f7 --- /dev/null +++ b/packages/cli/src/linter/linter/rules/omitted-utils.ts @@ -0,0 +1,53 @@ +// 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 { OmittedEntry } from '../../parser/spec.js'; + +/** Valid section names that can appear in the `omitted` key. */ +export const VALID_OMITTED_SECTIONS = new Set([ + 'colors', + 'typography', + 'spacing', + 'rounded', + 'components', +]); + +/** + * Extract the section name from an OmittedEntry. + */ +export function getSectionName(entry: OmittedEntry): string { + return typeof entry === 'string' ? entry : entry.section; +} + +/** + * Build a Set of section names declared as omitted. + */ +export function getOmittedSections(state: DesignSystemState): Set { + const omitted = new Set(); + if (state.omitted) { + for (const entry of state.omitted) { + omitted.add(getSectionName(entry)); + } + } + return omitted; +} + +/** + * Check if a specific section is declared as omitted. + */ +export function isOmitted(state: DesignSystemState, section: string): boolean { + if (!state.omitted) return false; + return state.omitted.some(e => getSectionName(e) === section); +} diff --git a/packages/cli/src/linter/linter/rules/redundant-omission.test.ts b/packages/cli/src/linter/linter/rules/redundant-omission.test.ts new file mode 100644 index 00000000..1562996b --- /dev/null +++ b/packages/cli/src/linter/linter/rules/redundant-omission.test.ts @@ -0,0 +1,67 @@ +// 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 { redundantOmission } from './redundant-omission.js'; +import { buildState } from './test-helpers.js'; + +describe('redundantOmission', () => { + it('emits warning when spacing is omitted but spacing tokens exist', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + spacing: { unit: '8px' }, + rounded: { regular: '4px' }, + omitted: ['spacing'], + }); + const findings = redundantOmission(state); + expect(findings.length).toBe(1); + expect(findings[0]!.path).toBe('omitted.spacing'); + expect(findings[0]!.severity).toBe('warning'); + expect(findings[0]!.message).toContain('has no effect'); + }); + + it('emits warning when typography is omitted but typography tokens exist', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + typography: { + 'body-md': { fontFamily: 'Inter', fontSize: '16px' }, + }, + omitted: ['typography'], + }); + const findings = redundantOmission(state); + expect(findings.length).toBe(1); + expect(findings[0]!.path).toBe('omitted.typography'); + }); + + it('returns empty when omitted sections genuinely have no tokens', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + omitted: ['spacing', 'rounded'], + }); + expect(redundantOmission(state)).toEqual([]); + }); + + it('returns empty when no omitted key', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + spacing: { unit: '8px' }, + }); + expect(redundantOmission(state)).toEqual([]); + }); + + it('returns empty for empty state', () => { + const state = buildState({}); + expect(redundantOmission(state)).toEqual([]); + }); +}); diff --git a/packages/cli/src/linter/linter/rules/redundant-omission.ts b/packages/cli/src/linter/linter/rules/redundant-omission.ts new file mode 100644 index 00000000..48cb34b8 --- /dev/null +++ b/packages/cli/src/linter/linter/rules/redundant-omission.ts @@ -0,0 +1,57 @@ +// 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'; +import { getSectionName, getOmittedSections } from './omitted-utils.js'; + +/** Maps omitted section names to their corresponding state map. */ +const SECTION_MAP: Record Map> = { + colors: s => s.colors as Map, + typography: s => s.typography as Map, + spacing: s => s.spacing as Map, + rounded: s => s.rounded as Map, + components: s => s.components as Map, +}; + +/** + * Redundant omission — warns when a section is listed in `omitted` but tokens are present. + */ +export function redundantOmission(state: DesignSystemState): RuleFinding[] { + const findings: RuleFinding[] = []; + if (!state.omitted) return findings; + + const omittedSections = getOmittedSections(state); + for (const section of omittedSections) { + const getMap = SECTION_MAP[section]; + if (getMap) { + const map = getMap(state); + if (map.size > 0) { + findings.push({ + path: `omitted.${section}`, + message: `'${section}' listed in omitted but ${section} tokens are defined — omitted declaration has no effect.`, + severity: 'warning', + }); + } + } + } + return findings; +} + +export const redundantOmissionRule: RuleDescriptor = { + name: 'redundant-omission', + severity: 'warning', + description: 'Redundant omission — warns when a section is listed in omitted but tokens are present.', + run: redundantOmission, +}; diff --git a/packages/cli/src/linter/linter/rules/types.test.ts b/packages/cli/src/linter/linter/rules/types.test.ts index f33b34d3..149ff6a1 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(13); DEFAULT_RULE_DESCRIPTORS.forEach((rule: RuleDescriptor) => { expect(rule.name).toBeTruthy(); expect(rule.severity).toBeTruthy(); diff --git a/packages/cli/src/linter/linter/rules/unknown-omission.test.ts b/packages/cli/src/linter/linter/rules/unknown-omission.test.ts new file mode 100644 index 00000000..8c85a743 --- /dev/null +++ b/packages/cli/src/linter/linter/rules/unknown-omission.test.ts @@ -0,0 +1,61 @@ +// 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 { unknownOmission } from './unknown-omission.js'; +import { buildState } from './test-helpers.js'; + +describe('unknownOmission', () => { + it('emits warning for unknown section in omitted (bare string)', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + omitted: ['iconography'], + }); + const findings = unknownOmission(state); + expect(findings.length).toBe(1); + expect(findings[0]!.path).toBe('omitted.iconography'); + expect(findings[0]!.severity).toBe('warning'); + expect(findings[0]!.message).toContain("Unknown section name 'iconography'"); + }); + + it('emits warning for unknown section in omitted (object form)', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + omitted: [{ section: 'bogus', reason: 'test' }], + }); + const findings = unknownOmission(state); + expect(findings.length).toBe(1); + expect(findings[0]!.path).toBe('omitted.bogus'); + }); + + it('returns empty when all omitted sections are valid', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + omitted: ['spacing', 'rounded', 'typography', 'colors', 'components'], + }); + expect(unknownOmission(state)).toEqual([]); + }); + + it('returns empty when no omitted key', () => { + const state = buildState({ + colors: { primary: '#ff0000' }, + }); + expect(unknownOmission(state)).toEqual([]); + }); + + it('returns empty for empty state', () => { + const state = buildState({}); + expect(unknownOmission(state)).toEqual([]); + }); +}); diff --git a/packages/cli/src/linter/linter/rules/unknown-omission.ts b/packages/cli/src/linter/linter/rules/unknown-omission.ts new file mode 100644 index 00000000..7a68337c --- /dev/null +++ b/packages/cli/src/linter/linter/rules/unknown-omission.ts @@ -0,0 +1,44 @@ +// 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'; +import { getSectionName, VALID_OMITTED_SECTIONS } from './omitted-utils.js'; + +/** + * Unknown omission — warns when an unrecognized section name appears in the `omitted` key. + */ +export function unknownOmission(state: DesignSystemState): RuleFinding[] { + const findings: RuleFinding[] = []; + if (!state.omitted) return findings; + + for (const entry of state.omitted) { + const section = getSectionName(entry); + if (!VALID_OMITTED_SECTIONS.has(section)) { + findings.push({ + path: `omitted.${section}`, + message: `Unknown section name '${section}' in omitted key. Valid sections are: ${[...VALID_OMITTED_SECTIONS].join(', ')}.`, + severity: 'warning', + }); + } + } + return findings; +} + +export const unknownOmissionRule: RuleDescriptor = { + name: 'unknown-omission', + severity: 'warning', + description: 'Unknown omission — warns when an unrecognized section name appears in the omitted key.', + run: unknownOmission, +}; diff --git a/packages/cli/src/linter/model/handler.ts b/packages/cli/src/linter/model/handler.ts index bee269ff..550b1ca3 100644 --- a/packages/cli/src/linter/model/handler.ts +++ b/packages/cli/src/linter/model/handler.ts @@ -238,6 +238,7 @@ export class ModelHandler implements ModelSpec { sections: input.sections, unknownKeys, unknownKeyValues, + omitted: input.omitted, }, findings, }; diff --git a/packages/cli/src/linter/model/spec.ts b/packages/cli/src/linter/model/spec.ts index 115ee47c..cb3e4449 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, OmittedEntry } from '../parser/spec.js'; import { STANDARD_UNITS as _STANDARD_UNITS, VALID_TYPOGRAPHY_PROPS as _VALID_TYPOGRAPHY_PROPS, @@ -84,6 +84,8 @@ export interface DesignSystemState { unknownKeys?: string[] | undefined; /** Raw YAML values for unknown top-level keys, keyed by the unknown key name */ unknownKeyValues?: Record | undefined; + /** Sections intentionally omitted — suppresses missing-section/typography warnings. */ + omitted?: OmittedEntry[] | undefined; } export interface ComponentDef { diff --git a/packages/cli/src/linter/parser/handler.ts b/packages/cli/src/linter/parser/handler.ts index 4475aabe..0ebd9b26 100644 --- a/packages/cli/src/linter/parser/handler.ts +++ b/packages/cli/src/linter/parser/handler.ts @@ -13,7 +13,7 @@ // limitations under the License. import YAML from 'yaml'; -import type { ParserSpec, ParserInput, ParserResult, ParsedDesignSystem, SourceLocation } from './spec.js'; +import type { ParserSpec, ParserInput, ParserResult, ParsedDesignSystem, SourceLocation, OmittedEntry } from './spec.js'; import { unified } from 'unified'; import remarkParse from 'remark-parse'; import remarkFrontmatter from 'remark-frontmatter'; @@ -199,6 +199,7 @@ export class ParserHandler implements ParserSpec { rounded: raw['rounded'] as Record | undefined, spacing: raw['spacing'] as Record | undefined, components: raw['components'] as Record> | undefined, + omitted: parseOmitted(raw['omitted']), sourceMap, sections, documentSections, @@ -213,3 +214,22 @@ export class ParserHandler implements ParserSpec { .trim(); } } + +/** + * Parse the raw `omitted` YAML value into an array of OmittedEntry. + * Accepts both bare strings and objects with `section` and optional `reason`. + * Returns undefined when the value is absent, empty, or not an array. + */ +function parseOmitted(raw: unknown): OmittedEntry[] | undefined { + if (!Array.isArray(raw)) return undefined; + const entries: OmittedEntry[] = []; + for (const item of raw) { + if (typeof item === 'string') { + entries.push(item); + } else if (typeof item === 'object' && item !== null && typeof (item as Record).section === 'string') { + entries.push(item as { section: string; reason?: string }); + } + // Silently skip invalid entries + } + return entries.length > 0 ? entries : undefined; +} diff --git a/packages/cli/src/linter/parser/spec.ts b/packages/cli/src/linter/parser/spec.ts index 6e84d121..af00bdb8 100644 --- a/packages/cli/src/linter/parser/spec.ts +++ b/packages/cli/src/linter/parser/spec.ts @@ -37,6 +37,9 @@ export interface SourceLocation { block: 'frontmatter' | number; } +/** A single entry in the `omitted` list — either a bare section name or an object with reason. */ +export type OmittedEntry = string | { section: string; reason?: string }; + /** Raw, unresolved parsed output — mirrors the YAML schema */ export interface ParsedDesignSystem { version?: string | undefined; @@ -47,6 +50,8 @@ export interface ParsedDesignSystem { rounded?: Record | undefined; spacing?: Record | undefined; components?: Record> | undefined; + /** Sections intentionally omitted (linter hint — suppresses missing-section warnings). */ + omitted?: OmittedEntry[] | undefined; sourceMap: Map; /** Markdown heading names found in the document (e.g., 'Colors', 'Typography') */ sections?: string[] | undefined;