Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions docs/architecture/theme-token-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ registry。
| compatibility alias family 直接使用次数 | 0 | generated widget frame 同时暴露 canonical 与 legacy family,内部 shell 读取 canonical family |
| stale compatibility alias family contracts | 0 | 防止动态 family 或 canonical family 失配 |
| missing compatibility alias family canonicals | 0 | 防止新增 `--radius-x` / `--spacing-x` 但缺少对应 canonical key |
| surface token rename contracts | 9 | 显式记录已迁移的 surface-local 旧 key、canonical 目标、owner 和命名边界 |
| active surface token rename key | 0 | 防止 `--primary-color`、`--operation-color`、`--delay`、`--um-*` 等旧局部 key 回流 |
| active surface token rename occurrences | 0 | 防止旧 key 在 SCSS、CSS 或 TSX inline style 中被重新定义或读取 |
| surface token rename missing canonical | 0 | 防止 rename registry 指向不存在的 canonical key |
| generated widget payload key | 326 | widget iframe 对外主题变量 allowlist,作为外部边界单独预算,不计入内部 alias 读取 |
| generated widget payload compatibility alias | 64 | payload 仍暴露已登记 legacy alias,防止已生成或第三方内容读取失败 |
| generated widget payload compatibility family key | 17 | payload 仍暴露 `--radius-*`、`--spacing-*` 具体 key,同时暴露 canonical family |
Expand All @@ -130,7 +134,7 @@ registry。
| Mermaid | 139 | 95 | Mermaid 专用渲染域 |
| Theme runtime | 54 | 45 | `ThemeService.ts` 运行时注入 |
| Language identity | 52 | 50 | 语言身份色,已集中到 identity registry |
| Terminal | 38 | 30 | terminal/ANSI 专用域 |
| Terminal | 37 | 29 | terminal/ANSI 专用域;工具命令空状态已复用 `--tool-command-empty-rgb`,不再保留独立 raw 色 |
| Boundary fallback | 22 | 22 | iframe/miniapp/截图兜底值,不作为普通 app token |
| Visual effects | 0 | 0 | StreamText/TextStroke raw literal 已迁出普通组件层 |
| UI exception registry | 38 | 34 | 已归档的 UI 例外色,包含 review team、agent capability、template context、insights 和 inspector 等固定身份色 |
Expand Down Expand Up @@ -161,7 +165,7 @@ fallback 收敛决策表:
| `--char-index` | 上移到组件根 | StreamText 根提供 `0`,每字符 inline style 仍可覆盖 | 移除 3 处 keyframe fallback |
| `--gallery-grid-min` | 上移到根 token 默认值 | `tokens.scss` 提供 `320px`,祖先变量和 props inline style 仍可覆盖 | 移除 grid sizing fallback |
| `--gallery-skeleton-height` | 上移到根 token 默认值 | `tokens.scss` 提供 `140px`,祖先变量和 props inline style 仍可覆盖 | 移除 skeleton height fallback |
| `--primary-color` | 改为 Markdown 组件 token | `--primary-color` tool card 局部兼容入口,不提升为全局 app token;BaseToolCard 映射到 `--markdown-primary-color` | Markdown 内部不再读取旧 key fallback |
| `--primary-color` | 改为明确的 tool-card accent token | `--primary-color` 是历史 tool card 局部入口,不提升为全局 app token;BaseToolCard 现在通过 `--base-tool-card-accent-color` 映射到 `--markdown-primary-color` | 产品代码不再定义或读取旧 key,回流由 `surfaceTokenRenames` 拦截 |
| `--scene-viewport-border-width` | 上移默认值 | 静态 token 提供 `1px`,ThemeService 继续按主题 layout 覆盖为 `1px` 或 `0` | 移除 viewport border fallback |

阶段状态:
Expand All @@ -172,9 +176,9 @@ fallback 收敛决策表:
| Phase 1:canonical token 契约 | 已完成调用方迁移 | compatibility alias registry 已记录 64 个显式 alias 和 2 个 alias family;内部 `var()` 读取已清零;widget payload 的 64 个显式 alias 和 17 个 family key 作为外部兼容面单独预算,并补齐 `--size-radius-2xl` / `--size-radius-full` canonical 导出 |
| Phase 2:精确重复合并 | 已完成主体 | token-equivalent app literal 已清零;截图兜底、language identity 和 review/agent/insights 固定色已迁入显式 registry;`tokens.scss` 中同域同语义的 deep-review consent 精确重复已改为 alias |
| Phase 3:legacy fallback 迁移 | 已完成主体 | 组件级 `var(--token, fallback)` 已清零,baseline 降到 0;新增 fallback 会被审计报告和 baseline 拦截 |
| Phase 4:组件 token 抽取 | 已完成主体 | CodeEditor、StreamText、ChatInputPixelPet、ReferencesPanel、AgentCompanion、tool-card、editor 组件装饰色已抽为组件私有 RGB channel 或复用 contract token |
| Phase 4:组件 token 抽取 | 已完成主体 | CodeEditor、StreamText、ChatInputPixelPet、ReferencesPanel、AgentCompanion、tool-card、editor、generative-widget 组件装饰色已抽为组件 token 或复用 contract token;Flow Chat 局部动态 key 已改为 surface namespace |
| Phase 5:近似色合并 | 已完成主体 | 普通组件 near pair 已清零;极近似视觉色只在不相邻或不承担状态差异时合并,Monaco/terminal/Mermaid/syntax 专用 palette 不强行合并 |
| Phase 6:防回退约束 | 已强化 | baseline 已同步到 component/non-token=0、appUi=0、token-equivalent=0、nearPair=0、compatibility alias 读取=0、fallback=0,并保留 generated widget payload、domain contract 防回退指标 |
| Phase 6:防回退约束 | 已强化 | baseline 已同步到 component/non-token=0、appUi=0、token-equivalent=0、nearPair=0、compatibility alias 读取=0、fallback=0、surface rename debt=0,并保留 generated widget payload、domain contract 防回退指标 |

Phase 5 决策记录:

Expand Down Expand Up @@ -211,6 +215,9 @@ Phase 6 防回退约束:
| `compatibilityAliases.staleRegisteredUnique` | 0 | 0 | 防止兼容 alias registry 保留没有定义或 canonical 目标缺失的 key |
| `compatibilityAliases.staleRegisteredFamilyUnique` | 0 | 0 | 防止 `--radius-*`、`--spacing-*` 这类动态 family 与 canonical family 失配 |
| `compatibilityAliases.missingCanonicalUnique` | 0 | 0 | 防止 family alias 具体 key 缺失对应 canonical key |
| `surfaceTokenRenames.activeUnique` | 0 | 0 | 防止已迁移的 surface-local 旧 key 重新出现 |
| `surfaceTokenRenames.activeOccurrences` | 0 | 0 | 防止旧 key 在定义和读取两侧回流 |
| `surfaceTokenRenames.missingCanonicalUnique` | 0 | 0 | 防止 surface rename contract 指向不存在的 canonical key |
| `generatedWidgetPayload.varUnique` | 326 | 326 | 控制 widget 对外主题 payload allowlist 不继续膨胀 |
| `generatedWidgetPayload.compatibilityAliasUnique` | 64 | 64 | 控制 payload 中显式 legacy alias 数量,后续只能降低或经复审调整 |
| `generatedWidgetPayload.compatibilityAliasFamilyUnique` | 17 | 17 | 控制 payload 中 legacy size family 具体 key 数量 |
Expand All @@ -237,7 +244,7 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域
| tool-cards-review | tool card、review panel、expanded/collapsed/status | danger alias 保留 destructive 语义,不能和 error 无证据合并 |
| code-editor-diff | Monaco、diff、selection、added/deleted/conflict | editor/diff 色表达相邻状态,不能按数值相似直接合并 |
| terminal | ANSI normal/bright、selection、error | ANSI 语义独立于 app semantic color |
| markdown-mermaid | Markdown、Prism、Mermaid、diagram/error | Markdown accent 通过 `--markdown-primary-color` 表达;tool-card 的旧 `--primary-color` 只作为局部兼容输入映射,Mermaid 角色不等于 app status |
| markdown-mermaid | Markdown、Prism、Mermaid、diagram/error | Markdown accent 通过 `--markdown-primary-color` 表达;tool-card 历史 `--primary-color` 已退场,Mermaid 角色不等于 app status |
| generated-widget | iframe fallback、host payload、loading/error | widget payload 兼容是保留多组旧 alias 的主要原因 |
| theme-settings | theme switcher、system/custom theme preview | custom theme preview 可能比普通组件更早暴露 runtime alias 缺失 |
| mobile-web-shell | mobile-web、mobile/narrow、loading/error/navigation | mobile web 是独立构建目标,不能只依赖 desktop WebView 验证 |
Expand Down Expand Up @@ -840,7 +847,7 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值

建议按证据和 surface 拆分,避免一次性大迁移:

当前 PR 已覆盖前 5 项,并把 widget payload 外部兼容面纳入审计预算:
已完成的治理批次覆盖前 9 项,并把 widget payload 外部兼容面纳入审计预算:

1. 审计工具和 baseline report。
2. canonical token map 与 compatibility alias。
Expand Down
66 changes: 66 additions & 0 deletions scripts/audit-theme-colors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
REGISTERED_DYNAMIC_VAR_PREFIXES,
RUNTIME_CONTRACT_VAR_DEFINITION_PATH_PARTS,
STATIC_CONTRACT_VAR_DEFINITION_PATH_PARTS,
SURFACE_TOKEN_RENAME_CONTRACTS,
TOKEN_COMPATIBILITY_ALIAS_CONTRACTS,
TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS,
TOKEN_ALIAS_SOURCE_PATH_PARTS,
Expand Down Expand Up @@ -1115,6 +1116,35 @@ function audit(options) {
))
.map(([key, scope]) => ({ key, count: scope.occurrences }))
.sort((a, b) => a.key.localeCompare(b.key));
const surfaceTokenRenameEntries = SURFACE_TOKEN_RENAME_CONTRACTS
.map(contract => {
const usageCount = varUsageCounts.get(contract.key) ?? 0;
const definitionCount = varDefinitionCounts.get(contract.key) ?? 0;
const files = new Set([
...Array.from(varUsageFiles.get(contract.key) ?? []),
...Array.from(varDefinitionFiles.get(contract.key) ?? []),
]);
return {
key: contract.key,
canonical: contract.canonical,
usageCount,
definitionCount,
count: usageCount + definitionCount,
files: Array.from(files).sort().slice(0, 5),
};
})
.filter(entry => entry.count > 0)
.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
const missingSurfaceTokenRenameCanonicalEntries = checksFullThemeSourceRoot
? SURFACE_TOKEN_RENAME_CONTRACTS
.map(contract => ({
key: contract.key,
canonical: contract.canonical,
canonicalDefinitionKind: getDefinitionKind(contract.canonical),
}))
.filter(entry => !entry.canonicalDefinitionKind)
.sort((a, b) => a.key.localeCompare(b.key))
: [];

return {
root: normalizePath(path.relative(cwd, root)) || '.',
Expand Down Expand Up @@ -1213,6 +1243,14 @@ function audit(options) {
missingColorDomainContracts: missingColorDomainContractEntries,
staleColorDomainContracts: staleColorDomainContractEntries,
activeUncontractedColorDomains: activeUncontractedColorDomainEntries,
surfaceTokenRenames: {
registeredUnique: SURFACE_TOKEN_RENAME_CONTRACTS.length,
activeUnique: surfaceTokenRenameEntries.length,
activeOccurrences: surfaceTokenRenameEntries.reduce((total, entry) => total + entry.count, 0),
missingCanonicalUnique: missingSurfaceTokenRenameCanonicalEntries.length,
active: surfaceTokenRenameEntries.slice(0, REPORT_ROW_LIMIT),
missingCanonicals: missingSurfaceTokenRenameCanonicalEntries.slice(0, REPORT_ROW_LIMIT),
},
tokenAliasLiterals: {
occurrences: sumMapValues(tokenAliasLiteralCounts),
uniqueColors: tokenAliasLiteralCounts.size,
Expand Down Expand Up @@ -1301,6 +1339,12 @@ function printText(report) {
`stale=${report.colorDomainContracts.staleRegisteredUnique}, ` +
`activeUncontracted=${report.colorDomainContracts.activeUncontractedUnique}`
);
console.log(
`Surface token renames: registered=${report.surfaceTokenRenames.registeredUnique}, ` +
`active=${report.surfaceTokenRenames.activeUnique}, ` +
`occurrences=${report.surfaceTokenRenames.activeOccurrences}, ` +
`missingCanonicals=${report.surfaceTokenRenames.missingCanonicalUnique}`
);

console.log('\nTop colors:');
console.log(printRows(report.topColors));
Expand Down Expand Up @@ -1446,6 +1490,28 @@ function printText(report) {
];
console.log(printRows(colorDomainGapRows.slice(0, 10)));

console.log('\nSurface token rename debt:');
if (report.surfaceTokenRenames.active.length === 0) {
console.log(' none');
} else {
for (const row of report.surfaceTokenRenames.active.slice(0, 10)) {
console.log(
` ${row.key} -> ${row.canonical} ` +
`count=${row.count} definitions=${row.definitionCount} usages=${row.usageCount} ` +
`files=${row.files.join(', ')}`
);
}
}

console.log('\nSurface token rename missing canonicals:');
if (report.surfaceTokenRenames.missingCanonicals.length === 0) {
console.log(' none');
} else {
for (const row of report.surfaceTokenRenames.missingCanonicals.slice(0, 10)) {
console.log(` ${row.key} -> ${row.canonical}`);
}
}

console.log('\nTop token-equivalent app literals:');
if (report.tokenAliasLiterals.top.length === 0) {
console.log(' none');
Expand Down
64 changes: 64 additions & 0 deletions scripts/audit-theme-colors.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
COLOR_DOMAIN_RULES,
DYNAMIC_VAR_FAMILY_CONTRACTS,
FALLBACK_VAR_CONTRACTS,
SURFACE_TOKEN_RENAME_CONTRACTS,
TOKEN_COMPATIBILITY_ALIAS_CONTRACTS,
TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS,
} from './theme-css-var-contract.mjs';
Expand Down Expand Up @@ -122,6 +123,20 @@ test('theme CSS var contract registry is explicit and non-overlapping', () => {
assert.ok(contract.reason.trim().length >= 30, `${contract.key} must explain why fallback is intentional`);
assert.ok(contract.boundary.trim().length >= 10, `${contract.key} must classify the fallback boundary`);
}

const surfaceRenameKeys = new Set(SURFACE_TOKEN_RENAME_CONTRACTS.map(contract => contract.key));
assert.equal(
surfaceRenameKeys.size,
SURFACE_TOKEN_RENAME_CONTRACTS.length,
'surface token rename contracts must be unique',
);
for (const contract of SURFACE_TOKEN_RENAME_CONTRACTS) {
assert.match(contract.key, /^--[a-z0-9-]+$/);
assert.match(contract.canonical, /^--[a-z0-9-]+$/);
assert.notEqual(contract.key, contract.canonical, `${contract.key} must point to a different canonical token`);
assert.ok(contract.owner.includes('src/web-ui/src/'), `${contract.key} must name a source owner`);
assert.ok(contract.reason.trim().length >= 30, `${contract.key} must explain the rename boundary`);
}
});

test('repository dynamic CSS var families match the registered contract', () => {
Expand All @@ -146,6 +161,9 @@ test('repository dynamic CSS var families match the registered contract', () =>
assert.equal(report.colorDomainContracts.missingRegisteredUnique, 0);
assert.equal(report.colorDomainContracts.staleRegisteredUnique, 0);
assert.equal(report.colorDomainContracts.activeUncontractedUnique, 0);
assert.equal(report.surfaceTokenRenames.activeUnique, 0);
assert.equal(report.surfaceTokenRenames.activeOccurrences, 0);
assert.equal(report.surfaceTokenRenames.missingCanonicalUnique, 0);
});

test('theme color audit reports alias family usages whose exact canonical key is missing', (t) => {
Expand Down Expand Up @@ -219,6 +237,52 @@ test('theme color audit emits scoped machine-readable reports', (t) => {
assert.equal(report.summary.baseline.enforced, false);
});

test('theme color audit reports deprecated surface-local token names', (t) => {
const { dir, sourceRoot } = createFixture({
'component-library/styles/tokens.scss': [
':root {',
' --base-tool-card-accent-color: #60a5fa;',
' --snapshot-card-operation-color: #60a5fa;',
'}',
'',
].join('\n'),
'component-library/components/FlowChatCards/BaseToolCard/BaseToolCard.scss': [
'.base-tool-card {',
' --primary-color: var(--base-tool-card-accent-color);',
' color: var(--primary-color);',
'}',
'',
].join('\n'),
'component-library/components/FlowChatCards/SnapshotCard/SnapshotCard.tsx': [
"export const style = { '--operation-color': 'var(--snapshot-card-operation-color)' };",
'',
].join('\n'),
'tools/editor/meditor/components/TiptapEditor.scss': [
'.m-editor-tiptap {',
' --m-editor-highlight-rgb: var(--markdown-editor-highlight-rgb);',
' background: rgba(var(--m-editor-highlight-rgb), 0.15);',
'}',
'',
].join('\n'),
});
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));

const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']);
assert.equal(result.status, 0, result.stderr || result.stdout);

const report = JSON.parse(result.stdout);
assert.equal(report.surfaceTokenRenames.activeUnique, 3);
assert.equal(report.surfaceTokenRenames.activeOccurrences, 5);
assert.deepEqual(
report.surfaceTokenRenames.active.map(row => [row.key, row.canonical, row.definitionCount, row.usageCount]),
[
['--m-editor-highlight-rgb', '--markdown-editor-highlight-rgb', 1, 1],
['--primary-color', '--base-tool-card-accent-color', 1, 1],
['--operation-color', '--snapshot-card-operation-color', 1, 0],
],
);
});

test('theme color audit reports compatibility alias usage without treating it as raw color debt', (t) => {
const { dir, sourceRoot } = createFixture({
'component-library/styles/tokens.scss': [
Expand Down
16 changes: 14 additions & 2 deletions scripts/theme-color-governance-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@
"compatibilityAliases.missingCanonicalUnique": {
"max": 0
},
"surfaceTokenRenames.registeredUnique": {
"max": 9
},
"surfaceTokenRenames.activeUnique": {
"max": 0
},
"surfaceTokenRenames.activeOccurrences": {
"max": 0
},
"surfaceTokenRenames.missingCanonicalUnique": {
"max": 0
},
"generatedWidgetPayload.varUnique": {
"max": 326
},
Expand Down Expand Up @@ -90,7 +102,7 @@
"max": 697
},
"colorScopes.exception.uniqueColors": {
"max": 274
"max": 273
},
"cssVarDefinitions.unresolvedRequiredUnique": {
"max": 0
Expand Down Expand Up @@ -159,7 +171,7 @@
"max": 18
},
"colorDomainScopes.terminal.occurrences": {
"max": 38
"max": 37
},
"colorDomainScopes.debugOverlay.occurrences": {
"max": 0
Expand Down
Loading
Loading