{"record":{"id":"65d64366c1745e68","repo":"CherryHQ/cherry-studio","slug":"theme-contract-declaration-name-is-defined-tw","errorCode":null,"errorMessage":"[theme-contract] ${declaration.name} is defined twice in ${selector}: ${existing.source} and ${sourceName}","messagePattern":"\\[theme-contract\\] (.+?) is defined twice in (.+?): (.+?) and (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/ui/scripts/validate-theme-contract.ts","lineNumber":145,"sourceCode":"  }\n}\n\nfunction assertExactImports(label: string, source: string, expected: readonly string[]): void {\n  const actual = extractImports(source)\n\n  if (actual.length !== expected.length || actual.some((entry, index) => entry !== expected[index])) {\n    throw new Error(`[theme-contract] ${label} imports must be exactly: ${expected.join(' -> ')}`)\n  }\n}\n\nfunction buildDeclarationMap(entries: SourceEntry[], selector: ':root' | '.dark'): Map<string, Declaration> {\n  const declarations = new Map<string, Declaration>()\n\n  for (const [sourceName, source] of entries) {\n    for (const declaration of extractModeDeclarations(source, sourceName, selector)) {\n      const existing = declarations.get(declaration.name)\n      if (existing) {\n        throw new Error(\n          `[theme-contract] ${declaration.name} is defined twice in ${selector}: ${existing.source} and ${sourceName}`\n        )\n      }\n      declarations.set(declaration.name, declaration)\n    }\n  }\n\n  return declarations\n}\n\nfunction assertRequiredDeclarations(\n  label: string,\n  declarations: Map<string, Declaration>,\n  variableNames: readonly string[],\n  prefix: string\n): void {\n  const missing = variableNames.map((name) => `${prefix}${name}`).filter((name) => !declarations.has(name))\n","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/packages/ui/scripts/validate-theme-contract.ts#L127-L163","documentation":"Thrown by buildDeclarationMap while aggregating CSS custom properties from the ordered source list (foundation, theme-input, shadcn, product) for a single mode block (:root or .dark). The contract requires each variable name to be owned by exactly one source file within a mode; declaring the same name in two files is an ambiguous ownership violation. The message names both conflicting sources so you can see which two files claim the variable.","triggerScenarios":"Adding `:root { --cs-background: hotpink; }` to product.css when --cs-background is already declared in primitive.css; copying a declaration from one token file into another instead of moving it; appending a stray `:root` block to the wrong layer file.","commonSituations":"Refactoring token files and leaving the original declaration behind; introducing a new product variable that collides with an existing foundation --cs-* name; merging branches that both touched the same selector.","solutions":["Read both source files named in the message and delete the duplicate declaration from the layer that should NOT own it (ownership follows the layering: --cs-* in foundation, --cs-theme-* in theme-input, official roles in shadcn, product roles in product).","If both declarations are intentional and distinct, rename one to a unique name that matches the contract prefix for its layer.","Re-run `pnpm --filter @cherrystudio/ui theme:check` to confirm the duplicate is gone."],"exampleFix":"// before (product.css)\n:root { --cs-background: hotpink; }\n// after — remove it; --cs-background is owned by tokens/colors/primitive.css","handlingStrategy":"validation","validationCode":"// Before calling validateThemeContractSources, scan each mode block for duplicate names across sources.\nimport { loadThemeContractSources } from './validate-theme-contract'\n\nfunction findDuplicateDeclarations(entries: ReadonlyArray<readonly [string, string]>, selector: string) {\n  const seen = new Map<string, string>()\n  const dupes: string[] = []\n  const block = new RegExp(`${selector}\\\\s*\\\\{([\\\\s\\\\S]*?)\\\\}`, 'g')\n  for (const [sourceName, source] of entries) {\n    for (const blockMatch of source.matchAll(block)) {\n      for (const [, name] of blockMatch[1].matchAll(/(?:^|[;{])\\s*(--[a-z0-9-]+)\\s*:/g)) {\n        if (seen.has(name[1])) dupes.push(`${name[1]} in ${seen.get(name[1])} and ${sourceName}`)\n        else seen.set(name[1], sourceName)\n      }\n    }\n  }\n  return dupes\n}","typeGuard":null,"tryCatchPattern":"try {\n  validateThemeContractSources(sources)\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith('[theme-contract]')) {\n    console.error(`Theme contract check failed: ${error.message}`)\n    process.exitCode = 1\n    return\n  }\n  throw error\n}","preventionTips":["Run `pnpm --filter @cherrystudio/ui theme:check` (or the repo's pnpm build:check / lint gate) before every commit touching packages/ui/src/styles.","When moving a declaration between token files, delete the original in the same commit.","Keep one owner per layer prefix: --cs-* in foundation, --cs-theme-* in theme-input, official roles in shadcn, product roles in product."],"tags":["theme-contract","css","build","validation"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}