CherryHQ/cherry-studio · error · Error

[theme-contract] ${label} has an invalid or duplicate surfac

Error message

[theme-contract] ${label} has an invalid or duplicate surface pair for ${surface}

What it means

Thrown by the theme contract validator when a surface-pair list (SHADCN_SURFACE_PAIRS or CHERRY_PRODUCT_SURFACE_PAIRS) has an invalid entry: either the surface and its foreground are the same token (surface === foreground), or the same surface token appears in more than one pair. Surface pairs define matched background/foreground combos (e.g., ['card', 'card-foreground']) and each surface must be unique and distinct from its foreground.

Source

Thrown at packages/ui/scripts/validate-theme-contract.ts:121

  })
}

function assertUnique(label: string, values: readonly string[]): void {
  if (new Set(values).size !== values.length) {
    throw new Error(`[theme-contract] ${label} contains duplicate names`)
  }
}

function assertSurfacePairs(
  label: string,
  pairs: ReadonlyArray<readonly [surface: string, foreground: string]>,
  variableNames: Set<string>
): void {
  const surfaces = new Set<string>()

  for (const [surface, foreground] of pairs) {
    if (surface === foreground || surfaces.has(surface)) {
      throw new Error(`[theme-contract] ${label} has an invalid or duplicate surface pair for ${surface}`)
    }
    if (!variableNames.has(surface) || !variableNames.has(foreground)) {
      throw new Error(`[theme-contract] ${label} pair ${surface} / ${foreground} is outside its public contract`)
    }
    surfaces.add(surface)
  }
}

function assertExactImports(label: string, source: string, expected: readonly string[]): void {
  const actual = extractImports(source)

  if (actual.length !== expected.length || actual.some((entry, index) => entry !== expected[index])) {
    throw new Error(`[theme-contract] ${label} imports must be exactly: ${expected.join(' -> ')}`)
  }
}

function buildDeclarationMap(entries: SourceEntry[], selector: ':root' | '.dark'): Map<string, Declaration> {
  const declarations = new Map<string, Declaration>()

View on GitHub (pinned to 726446b54c)

Solutions

  1. Inspect the surface pair array identified by the label (e.g., 'Shadcn contract', 'product contract') in packages/ui/scripts/theme-contract.ts.
  2. If surface === foreground, correct the foreground to the intended distinct token.
  3. If a surface is duplicated across pairs, remove or rename the duplicate so each surface appears exactly once.
  4. Run the validator to confirm: npx tsx packages/ui/scripts/validate-theme-contract.ts.

Example fix

// before
export const SHADCN_SURFACE_PAIRS = [
  ['card', 'card-foreground'],
  ['card', 'card-muted-foreground'],  // ← duplicate surface 'card'
  ['popover', 'popover']              // ← surface === foreground
] as const

// after — distinct, unique surfaces
export const SHADCN_SURFACE_PAIRS = [
  ['card', 'card-foreground'],
  ['popover', 'popover-foreground']
] as const
Defensive patterns

Strategy: validation

Validate before calling

// Check surface pairs for duplicates and self-references before committing
import { SHADCN_SURFACE_PAIRS, CHERRY_PRODUCT_SURFACE_PAIRS } from './theme-contract'

function checkSurfacePairs(label: string, pairs: ReadonlyArray<readonly [string, string]>): void {
  const surfaces = new Set<string>()
  for (const [surface, foreground] of pairs) {
    if (surface === foreground) throw new Error(`${label}: ${surface} pairs with itself`)
    if (surfaces.has(surface)) throw new Error(`${label}: duplicate surface ${surface}`)
    surfaces.add(surface)
  }
}

checkSurfacePairs('Shadcn', SHADCN_SURFACE_PAIRS)
checkSurfacePairs('product', CHERRY_PRODUCT_SURFACE_PAIRS)

Prevention

When it happens

Trigger: A surface pair array contains a self-referential pair like ['card', 'card'] (surface equals foreground), or the same surface name appears in two different pairs (e.g., ['card', 'card-foreground'] and ['card', 'card-muted'] both list 'card'). The assertSurfacePairs function iterates all pairs and tracks seen surfaces in a Set.

Common situations: A developer adds a new surface pair but reuses an existing surface name. A copy-paste error duplicates a pair entry. A typo where the foreground was set to the same value as the surface. A refactoring that merged or split pairs incorrectly.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/4696263c8612a783. Report an issue: GitHub.