| name | design-system-governance |
| description | Detect and track design token drift between Figma design systems and code implementations - report-only skill that identifies inconsistencies and creates wrangler issues for resolution |
Design System Governance
Skill Usage Announcement
MANDATORY: When using this skill, announce it at the start with:
🔧 Using Skill: design-system-governance | [brief purpose based on context]
Example:
🔧 Using Skill: design-system-governance | Auditing design token drift between Figma and codebase
This creates an audit trail showing which skills were applied during the session.
Overview
Use this skill to detect and track design token drift between Figma design systems and code implementations. This is a REPORT-ONLY skill that identifies inconsistencies and creates wrangler issues for resolution, but does not auto-fix tokens.
When to Use This Skill
Use this skill when:
- Periodic governance checks - Regular audits (weekly/monthly) to ensure design-code alignment
- After Figma updates - Design team publishes new token values in Figma
- Before releases - Pre-deployment verification of design token consistency
- Onboarding new projects - Initial baseline assessment of design system health
- Design system migrations - Tracking progress during token standardization efforts
- Post-implementation review - Verifying developers used correct design tokens
Do NOT use this skill for:
- One-off component styling (use regular development workflow)
- Custom/bespoke designs that intentionally deviate from the system
- Projects without established design systems
Prerequisites
Before running governance checks, ensure:
- Figma design system exists with published styles/variables
- Code tokens are exported in at least one format (CSS variables, Tailwind config, design tokens JSON)
- Figma MCP server is configured with access to the design file
- Token naming conventions are documented (if custom normalization needed)
- Wrangler MCP is initialized for issue tracking
Workflow
1. Extract Design Tokens from Figma
↓
2. Parse Code Token Sources
↓
3. Normalize Token Names
↓
4. Compare & Detect Drift
↓
5. Classify Drift Severity
↓
6. Generate Reconciliation Recommendations
↓
7. Create Wrangler Issues (one per drift category)
↓
8. Update Governance Metadata
Phase 1: Extract Design Tokens from Figma
Use Figma MCP tools to extract tokens from the design file:
Color Tokens
const colorStyles = await mcp__plugin_figma_figma_mcp__get_styles({
fileKey: "ABC123",
styleType: "FILL"
});
const figmaColorTokens = colorStyles.styles.map(style => ({
name: normalizeTokenName(style.name),
category: "color",
value: rgbaToHex(style.fills[0].color),
source: "figma",
rawName: style.name,
nodeId: style.styleId
}));
Typography Tokens
const textStyles = await mcp__plugin_figma_figma_mcp__get_styles({
fileKey: "ABC123",
styleType: "TEXT"
});
const figmaTypographyTokens = textStyles.styles.map(style => ({
name: normalizeTokenName(style.name),
category: "typography",
value: {
fontFamily: style.fontFamily,
fontSize: style.fontSize,
fontWeight: style.fontWeight,
lineHeight: style.lineHeight,
letterSpacing: style.letterSpacing
},
source: "figma",
rawName: style.name,
nodeId: style.styleId
}));
Spacing/Layout Tokens
const layoutGrids = await mcp__plugin_figma_figma_mcp__get_styles({
fileKey: "ABC123",
styleType: "GRID"
});
const spacingFrame = await mcp__plugin_figma_figma_mcp__get_file_nodes({
fileKey: "ABC123",
nodeIds: ["spacing-frame-id"]
});
const figmaSpacingTokens = parseSpacingFromFrame(spacingFrame);
Border Radius Tokens
const borderRadiusFrame = await mcp__plugin_figma_figma_mcp__get_file_nodes({
fileKey: "ABC123",
nodeIds: ["border-radius-frame-id"]
});
const figmaBorderRadiusTokens = parseBorderRadiusFromFrame(borderRadiusFrame);
Shadow/Effect Tokens
const effectStyles = await mcp__plugin_figma_figma_mcp__get_styles({
fileKey: "ABC123",
styleType: "EFFECT"
});
const figmaShadowTokens = effectStyles.styles
.filter(style => style.effects.some(e => e.type === "DROP_SHADOW"))
.map(style => ({
name: normalizeTokenName(style.name),
category: "shadow",
value: style.effects.find(e => e.type === "DROP_SHADOW"),
source: "figma",
rawName: style.name,
nodeId: style.styleId
}));
Phase 2: Parse Code Token Sources
Support multiple token formats commonly used in codebases:
CSS Variables
async function parseCSSVariables(filePath: string): Promise<Token[]> {
const content = await fs.readFile(filePath, 'utf-8');
const tokens: Token[] = [];
const cssVarRegex = /--([a-zA-Z0-9-_]+):\s*([^;]+);/g;
let match;
while ((match = cssVarRegex.exec(content)) !== null) {
const [, name, value] = match;
tokens.push({
name: normalizeTokenName(name),
category: inferCategory(name),
value: value.trim(),
source: "css",
rawName: name,
filePath
});
}
return tokens;
}
Tailwind Config
async function parseTailwindConfig(filePath: string): Promise<Token[]> {
const config = await import(filePath);
const tokens: Token[] = [];
if (config.theme?.extend?.colors || config.theme?.colors) {
const colors = config.theme?.extend?.colors || config.theme?.colors;
flattenObject(colors, 'color').forEach(({ path, value }) => {
tokens.push({
name: normalizeTokenName(path),
category: 'color',
value: value,
source: 'tailwind',
rawName: path,
filePath
});
});
}
if (config.theme?.extend?.spacing || config.theme?.spacing) {
const spacing = config.theme?.extend?.spacing || config.theme?.spacing;
flattenObject(spacing, 'spacing').forEach(({ path, value }) => {
tokens.push({
name: normalizeTokenName(path),
category: 'spacing',
value: value,
source: 'tailwind',
rawName: path,
filePath
});
});
}
if (config.theme?.extend?.fontSize || config.theme?.fontSize) {
const fontSize = config.theme?.extend?.fontSize || config.theme?.fontSize;
flattenObject(fontSize, 'fontSize').forEach(({ path, value }) => {
tokens.push({
name: normalizeTokenName(path),
category: 'typography',
value: value,
source: 'tailwind',
rawName: path,
filePath
});
});
}
if (config.theme?.extend?.borderRadius || config.theme?.borderRadius) {
const borderRadius = config.theme?.extend?.borderRadius || config.theme?.borderRadius;
flattenObject(borderRadius, 'borderRadius').forEach(({ path, value }) => {
tokens.push({
name: normalizeTokenName(path),
category: 'borderRadius',
value: value,
source: 'tailwind',
rawName: path,
filePath
});
});
}
if (config.theme?.extend?.boxShadow || config.theme?.boxShadow) {
const boxShadow = config.theme?.extend?.boxShadow || config.theme?.boxShadow;
flattenObject(boxShadow, 'boxShadow').forEach(({ path, value }) => {
tokens.push({
name: normalizeTokenName(path),
category: 'shadow',
value: value,
source: 'tailwind',
rawName: path,
filePath
});
});
}
return tokens;
}
function flattenObject(obj: any, prefix: string): Array<{ path: string, value: any }> {
const result: Array<{ path: string, value: any }> = [];
function recurse(current: any, path: string[]) {
if (typeof current === 'object' && current !== null && !Array.isArray(current)) {
Object.keys(current).forEach(key => {
recurse(current[key], [...path, key]);
});
} else {
result.push({
path: path.join('.'),
value: current
});
}
}
recurse(obj, [prefix]);
return result;
}
Design Tokens JSON (W3C Format)
async function parseDesignTokensJSON(filePath: string): Promise<Token[]> {
const content = await fs.readFile(filePath, 'utf-8');
const tokensData = JSON.parse(content);
const tokens: Token[] = [];
function traverse(obj: any, path: string[] = []) {
for (const [key, value] of Object.entries(obj)) {
if (value && typeof value === 'object') {
if ('$value' in value || 'value' in value) {
const tokenValue = value.$value || value.value;
const tokenType = value.$type || value.type || inferCategory(key);
tokens.push({
name: normalizeTokenName([...path, key].join('.')),
category: tokenType,
value: tokenValue,
source: 'design-tokens-json',
rawName: [...path, key].join('.'),
filePath,
metadata: {
description: value.$description || value.description,
deprecated: value.$deprecated || value.deprecated
}
});
} else {
traverse(value, [...path, key]);
}
}
}
}
traverse(tokensData);
return tokens;
}
Multi-Source Parsing Strategy
async function parseAllCodeTokens(config: {
cssFiles?: string[];
tailwindConfig?: string;
designTokensJSON?: string;
}): Promise<Token[]> {
const allTokens: Token[] = [];
if (config.cssFiles) {
for (const file of config.cssFiles) {
const tokens = await parseCSSVariables(file);
allTokens.push(...tokens);
}
}
if (config.tailwindConfig) {
const tokens = await parseTailwindConfig(config.tailwindConfig);
allTokens.push(...tokens);
}
if (config.designTokensJSON) {
const tokens = await parseDesignTokensJSON(config.designTokensJSON);
allTokens.push(...tokens);
}
return allTokens;
}
Phase 3: Normalize Token Names
Normalize token names from different sources for accurate comparison:
function normalizeTokenName(name: string): string {
return name
.toLowerCase()
.replace(/[_\/.]/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
Fuzzy Matching for Renamed Tokens
function calculateSimilarity(name1: string, name2: string): number {
const distance = levenshteinDistance(name1, name2);
const maxLength = Math.max(name1.length, name2.length);
return 1 - (distance / maxLength);
}
function findPotentialRenames(
figmaToken: Token,
codeTokens: Token[],
threshold: number = 0.75
): Token[] {
return codeTokens
.filter(codeToken => {
const similarity = calculateSimilarity(figmaToken.name, codeToken.name);
return similarity >= threshold && similarity < 1.0;
})
.sort((a, b) => {
const simA = calculateSimilarity(figmaToken.name, a.name);
const simB = calculateSimilarity(figmaToken.name, b.name);
return simB - simA;
});
}
function levenshteinDistance(str1: string, str2: string): number {
const matrix: number[][] = [];
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j] + 1
);
}
}
}
return matrix[str2.length][str1.length];
}
Phase 4: Compare & Detect Drift
Identify all types of drift between Figma and code:
interface DriftReport {
missingInCode: Token[];
missingInFigma: Token[];
valueMismatches: TokenMismatch[];
potentialRenames: TokenRename[];
}
interface TokenMismatch {
name: string;
figmaValue: any;
codeValue: any;
figmaSource: Token;
codeSource: Token;
}
interface TokenRename {
figmaToken: Token;
possibleCodeTokens: Array<{ token: Token; similarity: number }>;
}
async function detectDrift(
figmaTokens: Token[],
codeTokens: Token[]
): Promise<DriftReport> {
const report: DriftReport = {
missingInCode: [],
missingInFigma: [],
valueMismatches: [],
potentialRenames: []
};
const figmaMap = new Map(figmaTokens.map(t => [t.name, t]));
const codeMap = new Map(codeTokens.map(t => [t.name, t]));
for (const figmaToken of figmaTokens) {
if (!codeMap.has(figmaToken.name)) {
report.missingInCode.push(figmaToken);
const potentialMatches = findPotentialRenames(figmaToken, codeTokens);
if (potentialMatches.length > 0) {
report.potentialRenames.push({
figmaToken,
possibleCodeTokens: potentialMatches.map(token => ({
token,
similarity: calculateSimilarity(figmaToken.name, token.name)
}))
});
}
}
}
for (const codeToken of codeTokens) {
if (!figmaMap.has(codeToken.name)) {
report.missingInFigma.push(codeToken);
}
}
for (const [name, figmaToken] of figmaMap) {
const codeToken = codeMap.get(name);
if (codeToken) {
const valuesMatch = compareTokenValues(figmaToken, codeToken);
if (!valuesMatch) {
report.valueMismatches.push({
name,
figmaValue: figmaToken.value,
codeValue: codeToken.value,
figmaSource: figmaToken,
codeSource: codeToken
});
}
}
}
return report;
}
function compareTokenValues(token1: Token, token2: Token): boolean {
const val1 = normalizeTokenValue(token1.value, token1.category);
const val2 = normalizeTokenValue(token2.value, token2.category);
return JSON.stringify(val1) === JSON.stringify(val2);
}
function normalizeTokenValue(value: any, category: string): any {
switch (category) {
case 'color':
return normalizeColor(value);
case 'spacing':
case 'borderRadius':
return normalizeLength(value);
case 'typography':
if (typeof value === 'object') {
return {
fontFamily: value.fontFamily,
fontSize: normalizeLength(value.fontSize),
fontWeight: parseInt(value.fontWeight),
lineHeight: normalizeLength(value.lineHeight),
letterSpacing: normalizeLength(value.letterSpacing)
};
}
return value;
case 'shadow':
return normalizeShadow(value);
default:
return value;
}
}
function normalizeColor(color: string | object): string {
if (typeof color === 'string') {
if (color.startsWith('rgb')) {
return rgbToHex(color);
}
return color.toLowerCase();
}
if (typeof color === 'object' && 'r' in color) {
return rgbaToHex(color);
}
return String(color);
}
function normalizeLength(value: string | number): string {
if (typeof value === 'number') {
return `${value}px`;
}
if (value.endsWith('rem')) {
const rem = parseFloat(value);
return `${rem * 16}px`;
}
return value;
}
function normalizeShadow(shadow: any): string {
if (typeof shadow === 'string') {
return shadow;
}
if (shadow.offset) {
const { x, y } = shadow.offset;
const blur = shadow.radius || 0;
const spread = shadow.spread || 0;
const color = normalizeColor(shadow.color);
return `${x}px ${y}px ${blur}px ${spread}px ${color}`;
}
return String(shadow);
}
Phase 5: Classify Drift Severity
Classify each drift item by severity and impact:
type DriftSeverity = 'critical' | 'high' | 'medium' | 'low';
interface ClassifiedDrift {
severity: DriftSeverity;
category: string;
items: Array<{
type: 'missing-in-code' | 'missing-in-figma' | 'value-mismatch' | 'potential-rename';
token: Token | TokenMismatch | TokenRename;
impact: string;
recommendation: string;
}>;
}
function classifyDrift(report: DriftReport): ClassifiedDrift[] {
const classified: ClassifiedDrift[] = [];
const missingInCodeByCategory = groupBy(report.missingInCode, t => t.category);
for (const [category, tokens] of Object.entries(missingInCodeByCategory)) {
const severity = getSeverityForMissingTokens(category, tokens);
classified.push({
severity,
category: `missing-in-code-${category}`,
items: tokens.map(token => ({
type: 'missing-in-code',
token,
impact: getImpactForMissingInCode(token),
recommendation: getRecommendationForMissingInCode(token)
}))
});
}
const missingInFigmaByCategory = groupBy(report.missingInFigma, t => t.category);
for (const [category, tokens] of Object.entries(missingInFigmaByCategory)) {
const severity = 'low';
classified.push({
severity,
category: `missing-in-figma-${category}`,
items: tokens.map(token => ({
type: 'missing-in-figma',
token,
impact: 'Code uses tokens not documented in design system',
recommendation: getRecommendationForMissingInFigma(token)
}))
});
}
const mismatchesByCategory = groupBy(
report.valueMismatches,
m => m.figmaSource.category
);
for (const [category, mismatches] of Object.entries(mismatchesByCategory)) {
const severity = getSeverityForMismatches(category, mismatches);
classified.push({
severity,
category: `value-mismatch-${category}`,
items: mismatches.map(mismatch => ({
type: 'value-mismatch',
token: mismatch,
impact: getImpactForMismatch(mismatch),
recommendation: getRecommendationForMismatch(mismatch)
}))
});
}
if (report.potentialRenames.length > 0) {
classified.push({
severity: 'medium',
category: 'potential-renames',
items: report.potentialRenames.map(rename => ({
type: 'potential-rename',
token: rename,
impact: 'Token may have been renamed, causing confusion',
recommendation: getRecommendationForRename(rename)
}))
});
}
return classified;
}
function getSeverityForMissingTokens(category: string, tokens: Token[]): DriftSeverity {
if (category === 'color' && tokens.some(t => t.name.includes('primary') || t.name.includes('brand'))) {
return 'critical';
}
if (tokens.length >= 10) {
return 'high';
}
if (category === 'typography' || category === 'spacing') {
return 'high';
}
if (tokens.length >= 3) {
return 'medium';
}
return 'low';
}
function getSeverityForMismatches(category: string, mismatches: TokenMismatch[]): DriftSeverity {
if (category === 'color' && mismatches.some(m =>
m.name.includes('primary') || m.name.includes('brand')
)) {
return 'critical';
}
if (mismatches.length >= 10) {
return 'high';
}
if (category === 'typography') {
return 'high';
}
if (mismatches.length >= 3) {
return 'medium';
}
return 'low';
}
function getImpactForMissingInCode(token: Token): string {
const impacts = {
color: 'Developers may use hardcoded values instead of design system colors',
typography: 'Text styling may be inconsistent across components',
spacing: 'Layout spacing may not follow design system',
borderRadius: 'Component roundness may be inconsistent',
shadow: 'Elevation/depth may not match designs'
};
return impacts[token.category] || 'Design token not available in code';
}
function getRecommendationForMissingInCode(token: Token): string {
const source = token.source === 'figma' ? 'Figma' : 'code';
return `Add ${token.rawName} to code tokens (value: ${JSON.stringify(token.value)})`;
}
function getRecommendationForMissingInFigma(token: Token): string {
return `Document ${token.rawName} in Figma design system or remove from code if deprecated`;
}
function getImpactForMismatch(mismatch: TokenMismatch): string {
return `Implemented value (${JSON.stringify(mismatch.codeValue)}) differs from design (${JSON.stringify(mismatch.figmaValue)})`;
}
function getRecommendationForMismatch(mismatch: TokenMismatch): string {
return `Update code token ${mismatch.name} to match Figma value: ${JSON.stringify(mismatch.figmaValue)}`;
}
function getRecommendationForRename(rename: TokenRename): string {
const topMatch = rename.possibleCodeTokens[0];
return `Verify if ${rename.figmaToken.rawName} was renamed to ${topMatch.token.rawName} (${Math.round(topMatch.similarity * 100)}% similar)`;
}
function groupBy<T>(array: T[], keyFn: (item: T) => string): Record<string, T[]> {
return array.reduce((acc, item) => {
const key = keyFn(item);
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {} as Record<string, T[]>);
}
Phase 6: Generate Reconciliation Recommendations
Provide actionable recommendations for each sync direction:
interface ReconciliationPlan {
direction: 'figma-to-code' | 'code-to-figma';
actions: ReconciliationAction[];
}
interface ReconciliationAction {
type: 'add' | 'update' | 'remove' | 'verify';
target: 'code' | 'figma';
tokenName: string;
currentValue?: any;
newValue?: any;
reason: string;
priority: DriftSeverity;
}
function generateReconciliationPlan(
classified: ClassifiedDrift[],
direction: 'figma-to-code' | 'code-to-figma'
): ReconciliationPlan {
const actions: ReconciliationAction[] = [];
if (direction === 'figma-to-code') {
for (const drift of classified) {
for (const item of drift.items) {
if (item.type === 'missing-in-code') {
const token = item.token as Token;
actions.push({
type: 'add',
target: 'code',
tokenName: token.rawName,
newValue: token.value,
reason: `Add missing design token from Figma`,
priority: drift.severity
});
} else if (item.type === 'value-mismatch') {
const mismatch = item.token as TokenMismatch;
actions.push({
type: 'update',
target: 'code',
tokenName: mismatch.name,
currentValue: mismatch.codeValue,
newValue: mismatch.figmaValue,
reason: `Update code value to match Figma`,
priority: drift.severity
});
} else if (item.type === 'missing-in-figma') {
const token = item.token as Token;
actions.push({
type: 'verify',
target: 'code',
tokenName: token.rawName,
currentValue: token.value,
reason: `Verify if this code token is deprecated or should be added to Figma`,
priority: 'low'
});
}
}
}
} else {
for (const drift of classified) {
for (const item of drift.items) {
if (item.type === 'missing-in-figma') {
const token = item.token as Token;
actions.push({
type: 'add',
target: 'figma',
tokenName: token.rawName,
newValue: token.value,
reason: `Add missing design token to Figma`,
priority: drift.severity
});
} else if (item.type === 'value-mismatch') {
const mismatch = item.token as TokenMismatch;
actions.push({
type: 'update',
target: 'figma',
tokenName: mismatch.name,
currentValue: mismatch.figmaValue,
newValue: mismatch.codeValue,
reason: `Update Figma value to match code`,
priority: drift.severity
});
} else if (item.type === 'missing-in-code') {
const token = item.token as Token;
actions.push({
type: 'verify',
target: 'figma',
tokenName: token.rawName,
currentValue: token.value,
reason: `Verify if this Figma token is deprecated or should be added to code`,
priority: 'low'
});
}
}
}
}
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
actions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
return { direction, actions };
}
Phase 7: Create Wrangler Issues
Create one issue per drift category with detailed recommendations:
async function createDriftIssues(
classified: ClassifiedDrift[],
reconciliationPlan: ReconciliationPlan
): Promise<void> {
for (const drift of classified) {
if (drift.items.length === 0) continue;
const issueTitle = `Design Token Drift: ${drift.category} (${drift.severity})`;
const issueDescription = formatDriftIssueDescription(drift, reconciliationPlan);
await mcp__plugin_wrangler_wrangler_mcp__issues_create({
title: issueTitle,
description: issueDescription,
type: 'issue',
status: 'open',
priority: drift.severity,
labels: ['design-system', 'token-drift', drift.category, `sync-${reconciliationPlan.direction}`],
project: 'Design System Governance',
wranglerContext: {
agentId: 'design-governance',
estimatedEffort: estimateEffort(drift.items.length)
}
});
}
}
function formatDriftIssueDescription(
drift: ClassifiedDrift,
reconciliationPlan: ReconciliationPlan
): string {
const sections: string[] = [];
sections.push(`## Summary\n`);
sections.push(`Detected ${drift.items.length} design token drift issue(s) in category: ${drift.category}\n`);
sections.push(`**Severity**: ${drift.severity}\n`);
sections.push(`**Sync Direction**: ${reconciliationPlan.direction === 'figma-to-code' ? 'Figma → Code' : 'Code → Figma'}\n`);
sections.push(`## Drift Details\n`);
for (const item of drift.items) {
sections.push(`### ${item.type.replace(/-/g, ' ').toUpperCase()}\n`);
if (item.type === 'missing-in-code') {
const token = item.token as Token;
sections.push(`**Token**: \`${token.rawName}\`\n`);
sections.push(`**Figma Value**: \`${JSON.stringify(token.value)}\`\n`);
sections.push(`**Impact**: ${item.impact}\n`);
} else if (item.type === 'missing-in-figma') {
const token = item.token as Token;
sections.push(`**Token**: \`${token.rawName}\`\n`);
sections.push(`**Code Value**: \`${JSON.stringify(token.value)}\`\n`);
sections.push(`**Source**: \`${token.filePath || token.source}\`\n`);
sections.push(`**Impact**: ${item.impact}\n`);
} else if (item.type === 'value-mismatch') {
const mismatch = item.token as TokenMismatch;
sections.push(`**Token**: \`${mismatch.name}\`\n`);
sections.push(`**Figma Value**: \`${JSON.stringify(mismatch.figmaValue)}\`\n`);
sections.push(`**Code Value**: \`${JSON.stringify(mismatch.codeValue)}\`\n`);
sections.push(`**Impact**: ${item.impact}\n`);
} else if (item.type === 'potential-rename') {
const rename = item.token as TokenRename;
sections.push(`**Figma Token**: \`${rename.figmaToken.rawName}\`\n`);
sections.push(`**Possible Code Matches**:\n`);
for (const match of rename.possibleCodeTokens) {
sections.push(`- \`${match.token.rawName}\` (${Math.round(match.similarity * 100)}% similar)\n`);
}
sections.push(`**Impact**: ${item.impact}\n`);
}
sections.push(`\n`);
}
sections.push(`## Reconciliation Recommendations\n`);
const relevantActions = reconciliationPlan.actions.filter(action =>
drift.items.some(item => {
if (item.type === 'missing-in-code') {
return action.tokenName === (item.token as Token).rawName;
} else if (item.type === 'missing-in-figma') {
return action.tokenName === (item.token as Token).rawName;
} else if (item.type === 'value-mismatch') {
return action.tokenName === (item.token as TokenMismatch).name;
}
return false;
})
);
for (const action of relevantActions) {
sections.push(`### ${action.type.toUpperCase()} in ${action.target}\n`);
sections.push(`**Token**: \`${action.tokenName}\`\n`);
if (action.currentValue) {
sections.push(`**Current Value**: \`${JSON.stringify(action.currentValue)}\`\n`);
}
if (action.newValue) {
sections.push(`**New Value**: \`${JSON.stringify(action.newValue)}\`\n`);
}
sections.push(`**Reason**: ${action.reason}\n`);
sections.push(`**Priority**: ${action.priority}\n`);
sections.push(`\n`);
}
sections.push(`## Implementation Steps\n`);
sections.push(`1. Review drift details above\n`);
sections.push(`2. Verify recommendations align with design system strategy\n`);
sections.push(`3. Update ${reconciliationPlan.direction === 'figma-to-code' ? 'code tokens' : 'Figma styles'} as recommended\n`);
sections.push(`4. Run governance check again to verify resolution\n`);
sections.push(`5. Mark this issue as complete\n`);
return sections.join('');
}
function estimateEffort(itemCount: number): string {
if (itemCount <= 3) return '30 minutes';
if (itemCount <= 10) return '1-2 hours';
if (itemCount <= 30) return '2-4 hours';
return '1 day';
}
Phase 8: Update Governance Metadata
Track governance check history:
interface GovernanceMetadata {
lastCheckTimestamp: string;
lastCheckBy: string;
figmaFileKey: string;
figmaVersion?: string;
totalDriftItems: number;
criticalDriftItems: number;
highDriftItems: number;
mediumDriftItems: number;
lowDriftItems: number;
syncDirection: 'figma-to-code' | 'code-to-figma';
issuesCreated: string[];
}
async function updateGovernanceMetadata(
classified: ClassifiedDrift[],
issueIds: string[],
config: {
figmaFileKey: string;
syncDirection: 'figma-to-code' | 'code-to-figma';
}
): Promise<void> {
const metadata: GovernanceMetadata = {
lastCheckTimestamp: new Date().toISOString(),
lastCheckBy: 'design-governance-skill',
figmaFileKey: config.figmaFileKey,
totalDriftItems: classified.reduce((sum, c) => sum + c.items.length, 0),
criticalDriftItems: classified.filter(c => c.severity === 'critical').reduce((sum, c) => sum + c.items.length, 0),
highDriftItems: classified.filter(c => c.severity === 'high').reduce((sum, c) => sum + c.items.length, 0),
mediumDriftItems: classified.filter(c => c.severity === 'medium').reduce((sum, c) => sum + c.items.length, 0),
lowDriftItems: classified.filter(c => c.severity === 'low').reduce((sum, c) => sum + c.items.length, 0),
syncDirection: config.syncDirection,
issuesCreated: issueIds
};
const metadataPath = path.join(
process.env.WRANGLER_WORKSPACE_ROOT || process.cwd(),
'.wrangler',
'cache',
'governance',
`design-token-drift-${Date.now()}.json`
);
await fs.ensureDir(path.dirname(metadataPath));
await fs.writeJSON(metadataPath, metadata, { spaces: 2 });
console.log(`Governance metadata saved to: ${metadataPath}`);
}
async function getLastGovernanceCheck(): Promise<GovernanceMetadata | null> {
const governanceDir = path.join(
process.env.WRANGLER_WORKSPACE_ROOT || process.cwd(),
'.wrangler',
'cache',
'governance'
);
if (!await fs.pathExists(governanceDir)) {
return null;
}
const files = await fs.readdir(governanceDir);
const metadataFiles = files
.filter(f => f.startsWith('design-token-drift-') && f.endsWith('.json'))
.sort()
.reverse();
if (metadataFiles.length === 0) {
return null;
}
const latestFile = path.join(governanceDir, metadataFiles[0]);
return await fs.readJSON(latestFile);
}
Complete Workflow Implementation
async function runDesignSystemGovernance(config: {
figmaFileKey: string;
figmaAccessToken?: string;
codeTokenSources: {
cssFiles?: string[];
tailwindConfig?: string;
designTokensJSON?: string;
};
syncDirection: 'figma-to-code' | 'code-to-figma';
createIssues?: boolean; // Default: true
}): Promise<void> {
console.log('Starting design system governance check...\n');
console.log('Phase 1: Extracting design tokens from Figma...');
const figmaTokens = await extractFigmaTokens(config.figmaFileKey);
console.log(`Extracted ${figmaTokens.length} tokens from Figma\n`);
console.log('Phase 2: Parsing code token sources...');
const codeTokens = await parseAllCodeTokens(config.codeTokenSources);
console.log(`Parsed ${codeTokens.length} tokens from code\n`);
console.log('Phase 3: Normalizing token names...');
console.log('Token names normalized during parsing\n');
console.log('Phase 4: Detecting drift...');
const driftReport = await detectDrift(figmaTokens, codeTokens);
console.log(`Found ${driftReport.missingInCode.length} tokens missing in code`);
console.log(`Found ${driftReport.missingInFigma.length} tokens missing in Figma`);
console.log(`Found ${driftReport.valueMismatches.length} value mismatches`);
console.log(`Found ${driftReport.potentialRenames.length} potential renames\n`);
console.log('Phase 5: Classifying drift severity...');
const classified = classifyDrift(driftReport);
console.log(`Classified into ${classified.length} drift categories\n`);
console.log('Phase 6: Generating reconciliation recommendations...');
const reconciliationPlan = generateReconciliationPlan(classified, config.syncDirection);
console.log(`Generated ${reconciliationPlan.actions.length} reconciliation actions\n`);
if (config.createIssues !== false) {
console.log('Phase 7: Creating wrangler issues...');
await createDriftIssues(classified, reconciliationPlan);
console.log(`Created ${classified.length} drift tracking issues\n`);
}
console.log('Phase 8: Updating governance metadata...');
const issueIds = classified.map(c => c.category);
await updateGovernanceMetadata(classified, issueIds, {
figmaFileKey: config.figmaFileKey,
syncDirection: config.syncDirection
});
console.log('Governance metadata updated\n');
console.log('=== GOVERNANCE CHECK COMPLETE ===\n');
console.log(`Total drift items: ${classified.reduce((sum, c) => sum + c.items.length, 0)}`);
console.log(`Critical: ${classified.filter(c => c.severity === 'critical').reduce((sum, c) => sum + c.items.length, 0)}`);
console.log(`High: ${classified.filter(c => c.severity === 'high').reduce((sum, c) => sum + c.items.length, 0)}`);
console.log(`Medium: ${classified.filter(c => c.severity === 'medium').reduce((sum, c) => sum + c.items.length, 0)}`);
console.log(`Low: ${classified.filter(c => c.severity === 'low').reduce((sum, c) => sum + c.items.length, 0)}`);
console.log(`\nSync direction: ${config.syncDirection === 'figma-to-code' ? 'Figma → Code' : 'Code → Figma'}`);
console.log(`Issues created: ${config.createIssues !== false ? 'Yes' : 'No'}`);
}
Usage Examples
Example 1: Figma → Code Sync (Design is source of truth)
await runDesignSystemGovernance({
figmaFileKey: 'ABC123XYZ',
codeTokenSources: {
cssFiles: [
'src/styles/design-tokens.css',
'src/styles/variables.css'
],
tailwindConfig: 'tailwind.config.js'
},
syncDirection: 'figma-to-code',
createIssues: true
});
Example 2: Code → Figma Sync (Code is source of truth)
await runDesignSystemGovernance({
figmaFileKey: 'ABC123XYZ',
codeTokenSources: {
designTokensJSON: 'design-tokens/tokens.json'
},
syncDirection: 'code-to-figma',
createIssues: true
});
Example 3: Dry Run (Report without creating issues)
await runDesignSystemGovernance({
figmaFileKey: 'ABC123XYZ',
codeTokenSources: {
cssFiles: ['src/styles/tokens.css'],
tailwindConfig: 'tailwind.config.js',
designTokensJSON: 'tokens/design-tokens.json'
},
syncDirection: 'figma-to-code',
createIssues: false
});
Example 4: Scheduled Governance (Weekly Check)
0 9 * * 1 /path/to/run-governance-check.sh
cd /path/to/project
claude-code --execute "
import { runDesignSystemGovernance } from './governance';
await runDesignSystemGovernance({
figmaFileKey: process.env.FIGMA_FILE_KEY,
codeTokenSources: {
cssFiles: ['src/styles/tokens.css'],
tailwindConfig: 'tailwind.config.js'
},
syncDirection: 'figma-to-code',
createIssues: true
});
"
Best Practices
1. Choose the Right Sync Direction
Figma → Code when:
- Design team is authoritative for design tokens
- Code is implementation layer
- Designers update tokens in Figma first
- Common in design-led organizations
Code → Figma when:
- Engineering maintains design tokens
- Figma is used for visual documentation
- Tokens are generated/computed programmatically
- Common in engineering-led organizations
2. Run Governance Checks Regularly
- Weekly: Catch drift early
- Before releases: Ensure consistency
- After design updates: Verify implementation
- During audits: Comprehensive health check
3. Prioritize Critical Drift First
- Critical: Brand colors, primary typography
- High: Systematic issues (10+ tokens)
- Medium: Secondary tokens, spacing
- Low: Edge cases, deprecated tokens
4. Document Token Naming Conventions
- Use consistent separators (hyphens recommended)
- Document prefix/suffix conventions
- Maintain naming glossary for custom normalization
5. Archive Historical Checks
- Keep governance metadata for trend analysis
- Track improvement over time
- Identify recurring drift patterns
6. Coordinate with Design Team
- Communicate governance findings
- Align on source of truth
- Establish token update workflows
- Review potential renames together
Troubleshooting
Issue: False Positives in Value Mismatches
Cause: Different value representations (e.g., #3B82F6 vs rgb(59, 130, 246))
Solution: Enhance normalization in compareTokenValues():
function normalizeColor(color: string): string {
if (color.startsWith('rgb')) {
return rgbToHex(color).toLowerCase();
}
if (color.startsWith('#')) {
return color.toLowerCase();
}
return color;
}
Issue: Tokens Missing Due to Naming Differences
Cause: Inconsistent naming conventions between Figma and code
Solution: Customize normalizeTokenName() for your conventions:
function normalizeTokenName(name: string): string {
return name
.toLowerCase()
.replace(/primary color/g, 'primary')
.replace(/[_\/.]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
Issue: Too Many Potential Rename Matches
Cause: Fuzzy matching threshold too low
Solution: Increase similarity threshold:
const potentialMatches = findPotentialRenames(figmaToken, codeTokens, 0.85);
Issue: Governance Check Takes Too Long
Cause: Large number of tokens or code files
Solution:
- Cache parsed code tokens
- Use parallel processing for file parsing
- Filter to specific token categories
Issue: Can't Parse Tailwind Config
Cause: Dynamic config or unsupported format
Solution: Export static tokens:
const tokens = require('./design-tokens.json');
module.exports = {
theme: {
extend: tokens
}
};
Integration with Other Skills
- design-system-setup: Initialize design system before governance
- figma-design-workflow: Extract designs after governance finds drift
- systematic-debugging: Investigate complex drift patterns
- writing-plans: Create implementation plans for large-scale token updates
Success Metrics
Track governance health over time:
- Drift reduction rate: Decrease in total drift items week-over-week
- Time to resolution: Average time from drift detection to issue closure
- Critical drift count: Should trend toward zero
- Token coverage: Percentage of Figma tokens in code (or vice versa)
- Governance frequency: Regular checks maintained (e.g., weekly)
Limitations
- Report-only: Does not auto-fix tokens (intentional safety measure)
- Manual sync: Requires human review of recommendations
- Single file key: Checks one Figma file at a time
- No component usage tracking: Doesn't verify which components use drifted tokens
- Naming conventions: Assumes consistent naming (or requires custom normalization)
Future Enhancements
- Component impact analysis (which components use drifted tokens)
- Auto-fix mode with approval workflow
- Multi-file governance (check multiple Figma files)
- Historical drift trending dashboard
- Integration with CI/CD for automated checks
- Token usage statistics (which tokens are most used)