From 3c32baf0053293662587a7a11f5fb6597fb6e215 Mon Sep 17 00:00:00 2001 From: kirozeng Date: Wed, 2 Sep 2026 14:45:32 +0800 Subject: [PATCH 01/15] =?UTF-8?q?feat(design-system):=20DS-3=20=E6=9C=80?= =?UTF-8?q?=E5=B0=8F=E8=AF=AD=E4=B9=89=20Token=20=E5=BD=B1=E5=AD=90?= =?UTF-8?q?=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 packages/design-tokens 建立 reference → semantic 两层 DTCG 影子字典。 数据源仅为 DS-2b 冻结快照,零运行时接线;产品代码不改。 Signed-off-by: kirozeng --- docs/design-rules/cindy-design-system.md | 1 + docs/design-rules/design-governance.md | 7 +- packages/design-tokens/README.md | 55 + packages/design-tokens/package.json | 19 + .../src/__tests__/classification.test.ts | 64 + .../fixtures/alias-from-reference.json | 7 + .../__tests__/fixtures/invalid-syntax.json | 7 + .../__tests__/fixtures/missing-dark-mode.json | 11 + .../src/__tests__/structure.test.ts | 54 + .../src/__tests__/value-consistency.test.ts | 37 + .../src/__tests__/zero-wiring.test.ts | 33 + packages/design-tokens/src/build-layers.ts | 109 + .../design-tokens/src/classification.json | 5096 +++++++++++++++++ packages/design-tokens/src/classify.ts | 226 + packages/design-tokens/src/dtcg.ts | 105 + packages/design-tokens/src/generate.ts | 55 + packages/design-tokens/src/guards.ts | 328 ++ packages/design-tokens/src/paths.ts | 44 + .../design-tokens/src/reference/color.json | 179 + packages/design-tokens/src/semantic-roles.ts | 66 + .../design-tokens/src/semantic/color.json | 473 ++ packages/design-tokens/src/snapshot.ts | 25 + packages/design-tokens/tsconfig.json | 14 + pnpm-lock.yaml | 12 + scripts/test-workspaces.config.mjs | 1 + 25 files changed, 7025 insertions(+), 3 deletions(-) create mode 100644 packages/design-tokens/README.md create mode 100644 packages/design-tokens/package.json create mode 100644 packages/design-tokens/src/__tests__/classification.test.ts create mode 100644 packages/design-tokens/src/__tests__/fixtures/alias-from-reference.json create mode 100644 packages/design-tokens/src/__tests__/fixtures/invalid-syntax.json create mode 100644 packages/design-tokens/src/__tests__/fixtures/missing-dark-mode.json create mode 100644 packages/design-tokens/src/__tests__/structure.test.ts create mode 100644 packages/design-tokens/src/__tests__/value-consistency.test.ts create mode 100644 packages/design-tokens/src/__tests__/zero-wiring.test.ts create mode 100644 packages/design-tokens/src/build-layers.ts create mode 100644 packages/design-tokens/src/classification.json create mode 100644 packages/design-tokens/src/classify.ts create mode 100644 packages/design-tokens/src/dtcg.ts create mode 100644 packages/design-tokens/src/generate.ts create mode 100644 packages/design-tokens/src/guards.ts create mode 100644 packages/design-tokens/src/paths.ts create mode 100644 packages/design-tokens/src/reference/color.json create mode 100644 packages/design-tokens/src/semantic-roles.ts create mode 100644 packages/design-tokens/src/semantic/color.json create mode 100644 packages/design-tokens/src/snapshot.ts create mode 100644 packages/design-tokens/tsconfig.json diff --git a/docs/design-rules/cindy-design-system.md b/docs/design-rules/cindy-design-system.md index 45d3129bba2..d0d8e5067bc 100644 --- a/docs/design-rules/cindy-design-system.md +++ b/docs/design-rules/cindy-design-system.md @@ -23,6 +23,7 @@ ## 版本记录 +- **2026-09-02(DS-3 最小语义 Token 影子层)**:新建 `packages/design-tokens`(标准 DTCG JSON,reference → semantic 两层)。数据源为 DS-2b 冻结快照,零运行时接线;弃坑复查日期 2026-11-01。不改台账、不改产品代码。路线图 DS-2b 回填为 #3700。 - **2026-08-31(Switch / 通用手柄交稿包入仓)**:登记 `gamepads/nintendo-switch-pro/`、`gamepads/switch-joy-con/`、`gamepads/ultimate-c1/` 三组同事线稿,设置页 Nintendo 默认 Switch Pro、接上 Joy-Con 时换 Joy-Con 图,通用手柄用 Ultimate C1。零视觉规范改写。 - **2026-08-30(治理合同修订:管道/记账、已知边界、路线图勘误;同日按 review 收口)**:[`design-governance.md`](./design-governance.md) 新增 §1.1「管道与记账」(守卫红灯不是禁令——管道规则不许绕、记账值走「同 PR 更新快照/台账 + 设计师批」的合法路径改,消灭的是「没人决定过的变化」;**保护值例外**:CINDY 皮肤族 / U2 二级信息色 / `annotation-accent` 不适用通用路径,须按 `DESIGN.md` 各自的用户裁决或冻结条款;正式豁免登记是合法路径,只禁未经裁决为消红灯加豁免)与 §13「已知边界」(正则扫描边界——内联样式字面量会被 `hardcoded-color-audit` 发现、真正扫不到的是动态值与 canvas/xterm 自绘;复用道路唯一靠 review 不靠机器;**新代码默认走语义层**、保留 §3.3/§3.4 既有准入、仅存量渐进),两节自 mivo-canvas-plugin 仓 4/7 张 PR 实战沉淀移植。勘误三处:§2 旧编号「PR-8」→「DS-8」;§12 依赖行 DS-6 前置由已关闭的圆角裁决改为「Permission 迁移余项」并补 DS-7 受 `radius` 覆盖裁决约束;§12 路线图回填 DS-1 = #3609(合入日期修正为 2026-08-30)。零视觉,非 DS 序号。 diff --git a/docs/design-rules/design-governance.md b/docs/design-rules/design-governance.md index fb619424073..8793514ba48 100644 --- a/docs/design-rules/design-governance.md +++ b/docs/design-rules/design-governance.md @@ -86,7 +86,7 @@ protected 标签、目标道路、下一动作 ### 3.1 目标层级(DTCG) -`packages/design-tokens`(由后续 Token PR 建立)采用标准 DTCG JSON,三层: +`packages/design-tokens`(DS-3 已建影子层,零运行时接线;DS-8 才生产生成切换)采用标准 DTCG JSON,三层: ```text reference 原始值:色阶、字号、字重、间距、圆角、动效时长 @@ -179,7 +179,8 @@ Primitive 与 Pattern 默认只绑定 semantic 角色。只有品牌表达、兼 | CI 门禁调整 | 新增/升级检查、required 名单变动 | 先报告后阻断 + 管理员人工审核(§8) | 每张 PR 必须写明独立回退方式;任一阶段结束时仓库必须不劣于开始状态。影子 Token 包 -在约定复查期内没有真实消费者时应删除,不长期并存。已登记缺口不得描述为已完成能力; +在约定复查期内没有真实消费者时应删除,不长期并存(DS-3 弃坑复查日期 **2026-11-01**, +详见 `packages/design-tokens/README.md`)。已登记缺口不得描述为已完成能力; 缺口未修复前,对应验收矩阵格不得记为通过。 ## 8. 治理接线纪律 @@ -253,7 +254,7 @@ ls apps/desktop/src/renderer/themes/builtin/*.ts | wc -l | --- | --- | --- | --- | | DS-1 | `docs(design-system): DS-1 建立治理合同与存量门禁处置表` | 零视觉(纯文档) | ✅ #3609(2026-08-30 合入) | | DS-2a | `test(design-system): DS-2a 生产 UI 台账` | 零视觉 | ✅ #3648(2026-08-31 合入) | -| DS-2b | `ci(design-system): DS-2b 主题兼容冻结守卫`(新增阻断需 §8 管理员审核) | CI 门禁 | — | +| DS-2b | `ci(design-system): DS-2b 主题兼容冻结守卫`(新增阻断需 §8 管理员审核) | CI 门禁 | ✅ #3700(2026-09-02 合入) | | DS-3 | `feat(design-system): DS-3 最小语义 Token 影子层` | 零视觉 | — | | DS-4 | `feat(design-system): DS-4 Button 与 Input 标准组件`(落入既有 `components/ui/`) | 有意可见 | — | | DS-5 | `refactor(design-system): DS-5 AI 对话区 Pattern 迁移`(Tool Call / Reasoning / Message / Attachment) | 有意可见 | — | diff --git a/packages/design-tokens/README.md b/packages/design-tokens/README.md new file mode 100644 index 00000000000..f6312e9c101 --- /dev/null +++ b/packages/design-tokens/README.md @@ -0,0 +1,55 @@ +# @cindy/design-tokens + +Cindy 设计 token 的 **DTCG 影子层**(reference → semantic)。 + +本包只是字典,**零运行时接线**:Desktop / Mobile / 任何产品 package 都不得依赖它。生产生成切换在路线图 **DS-8**;在那之前,Desktop 颜色数值权威仍是 `apps/desktop/src/renderer/themes/colors.ts`,本包的取值必须与 DS-2b 冻结快照逐值一致。 + +## 弃坑条款 + +治理合同 §7:影子包在约定复查期内没有真实消费者时应删除。 + +- **复查日期:2026-11-01** +- 届时若 DS-4 仍未开工,整包删除(revert 本 PR 即可,零运行时残留) +- 等真做 DS-4 时再重建,不让没人消费的字典变成第三份漂移真相 + +## 数据源 + +分类与建层的唯一数据源是 DS-2b 冻结快照: + +`apps/desktop/src/renderer/themes/__tests__/fixtures/desktop-color-defaults.json` + +不重新解析 `colors.ts`。重新生成:在本包目录执行 `pnpm generate`(脚本 `src/generate.ts`)。连续两次生成必须字节一致。守卫测试会核对。 + +## 两层 + +| 层 | 路径 | 内容 | +| --- | --- | --- | +| reference | `src/reference/color.json` | 第一批 semantic 角色实际引用的原始色值(不铺全色板) | +| semantic | `src/semantic/color.json` | DESIGN.md §10 Tier-1 的 surface / border / text / accent 四族 + status 语义;每个角色 light/dark = 冻结快照现值 | + +依赖单向:semantic → reference。不建 component 层(DS-4)。不装 Terrazzo(DS-8)。 + +U2 二级信息色(`text-secondary` / `text-secondary-cross`)、`annotation-accent`、CINDY 皮肤族品牌红(`login-brand-accent` / `login-brand-accent-pressed`)按治理合同 §1.1 标记 **protected**,只登记、不进 semantic 映射。皮肤族其余值在 cindy-light/dark 主题 override 里,不在本快照默认值中。 + +## 多入口投放合同(只写合同,DS-8 才接线) + +每个消费者届时只消费自己的子集,由同一份 DTCG 生成,不得再手写第二份数值: + +| 消费者 | DS-8 起消费什么 | +| --- | --- | +| Desktop ColorRegistry | semantic + 后续 component;`registerColor` 的 id / 默认 light/dark 由生成物提供 | +| CSS Variables(`:root` / `theme-vars`) | 同一套 semantic id 的 kebab-case CSS 变量 | +| Tailwind 映射 | 现有 `tailwind.config.ts` 色名继续指向上述 CSS 变量,不另造色板 | +| Mobile TS Token | `apps/mobile/src/theme/tokens.ts` 改为引用生成子集;须另立高风险 PR(冷更边界) | +| DESIGN.md 机器摘要 | §10 Tier-1 表与 §16.1 登录表由本包生成摘要替换人工维护 | + +本张不产出任何被产品消费的生成物。 + +## 分类登记 + +`src/classification.json` 覆盖冻结快照全部 id,四类互斥完备: + +1. **literal** — 直接数值,reference 层候选 +2. **alias** — `var(--…)` / `hsl(var(--…))`,semantic/component 候选 +3. **hsl-triplet** — `-hsl` 后缀族,与对应 hex 必须指同一颜色(DS-8 起由生成器保证) +4. **runtime-derived-or-protected** — 运行期计算值、非颜色、双模式不全、加严保护值;只登记存在、负责人与去向,不建模 diff --git a/packages/design-tokens/package.json b/packages/design-tokens/package.json new file mode 100644 index 00000000000..4693702f734 --- /dev/null +++ b/packages/design-tokens/package.json @@ -0,0 +1,19 @@ +{ + "name": "@cindy/design-tokens", + "version": "0.0.0", + "private": true, + "description": "Cindy design-token shadow layer (DTCG reference + semantic). Zero runtime consumers until DS-8.", + "type": "module", + "scripts": { + "generate": "node --experimental-strip-types src/generate.ts", + "build": "tsc --noEmit", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "*", + "typescript": "^5.7.0", + "vitest": "^3.2.4" + } +} diff --git a/packages/design-tokens/src/__tests__/classification.test.ts b/packages/design-tokens/src/__tests__/classification.test.ts new file mode 100644 index 00000000000..956157437d0 --- /dev/null +++ b/packages/design-tokens/src/__tests__/classification.test.ts @@ -0,0 +1,64 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +import { + classifySnapshot, + PROTECTED_IDS, + stableStringify, +} from '../classify.ts'; +import { buildShadowLayerFiles } from '../generate.ts'; +import { + assertClassificationCoversSnapshot, + assertProtectedNotSemantic, +} from '../guards.ts'; +import { + classificationPath, + findRepoRoot, + referencePath, + semanticPath, +} from '../paths.ts'; +import { readSnapshot } from '../snapshot.ts'; + +describe('DS-3 · 分类登记', () => { + const repoRoot = findRepoRoot(); + const snapshot = readSnapshot(repoRoot); + const generated = classifySnapshot(snapshot.colors, snapshot.source); + const onDisk = JSON.parse(readFileSync(classificationPath(repoRoot), 'utf8')); + + it('覆盖冻结快照全部 id,计数等于 fixture.count,四类互斥完备', () => { + expect(snapshot.colors.length).toBe(snapshot.count); + assertClassificationCoversSnapshot(generated, snapshot); + expect(generated.snapshotCount).toBe(snapshot.count); + const sum = Object.values(generated.categories).reduce((a, b) => a + b, 0); + expect(sum).toBe(snapshot.count); + }); + + it('两次生成字节一致', () => { + const first = stableStringify(classifySnapshot(snapshot.colors, snapshot.source)); + const second = stableStringify(classifySnapshot(snapshot.colors, snapshot.source)); + expect(first).toBe(second); + expect(stableStringify(onDisk)).toBe(first); + }); + + it('磁盘上的影子层与内存生成字节一致', () => { + const built = buildShadowLayerFiles(repoRoot); + expect(built.files).toEqual([ + { path: classificationPath(repoRoot), body: stableStringify(generated) }, + { path: referencePath(repoRoot), body: stableStringify(built.layers.reference) }, + { path: semanticPath(repoRoot), body: stableStringify(built.layers.semantic) }, + ]); + for (const file of built.files) { + expect(readFileSync(file.path, 'utf8')).toBe(file.body); + } + }); + + it('加严保护值标记 protected,未进 semantic 映射', () => { + assertProtectedNotSemantic(generated); + for (const id of Object.keys(PROTECTED_IDS)) { + const entry = generated.entries.find((item) => item.id === id); + expect(entry?.category).toBe('runtime-derived-or-protected'); + expect(entry?.modeledAsSemantic).toBe(false); + expect(entry?.protected).toBeTruthy(); + } + }); +}); diff --git a/packages/design-tokens/src/__tests__/fixtures/alias-from-reference.json b/packages/design-tokens/src/__tests__/fixtures/alias-from-reference.json new file mode 100644 index 00000000000..b646d7c8b12 --- /dev/null +++ b/packages/design-tokens/src/__tests__/fixtures/alias-from-reference.json @@ -0,0 +1,7 @@ +{ + "$description": "error fixture: reference aliases a semantic token (wrong direction)", + "hex-ffffff": { + "$type": "color", + "$value": "{surface.surface.light}" + } +} diff --git a/packages/design-tokens/src/__tests__/fixtures/invalid-syntax.json b/packages/design-tokens/src/__tests__/fixtures/invalid-syntax.json new file mode 100644 index 00000000000..1be223b5eff --- /dev/null +++ b/packages/design-tokens/src/__tests__/fixtures/invalid-syntax.json @@ -0,0 +1,7 @@ +{ + "$description": "error fixture: illegal $type", + "broken": { + "$type": "not-a-dtcg-type", + "$value": "#ffffff" + } +} diff --git a/packages/design-tokens/src/__tests__/fixtures/missing-dark-mode.json b/packages/design-tokens/src/__tests__/fixtures/missing-dark-mode.json new file mode 100644 index 00000000000..9a561801b7d --- /dev/null +++ b/packages/design-tokens/src/__tests__/fixtures/missing-dark-mode.json @@ -0,0 +1,11 @@ +{ + "$description": "error fixture: semantic role missing dark", + "surface": { + "surface": { + "light": { + "$type": "color", + "$value": "{hex-f8f8f6}" + } + } + } +} diff --git a/packages/design-tokens/src/__tests__/structure.test.ts b/packages/design-tokens/src/__tests__/structure.test.ts new file mode 100644 index 00000000000..6ac82dbb1bd --- /dev/null +++ b/packages/design-tokens/src/__tests__/structure.test.ts @@ -0,0 +1,54 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +import { buildLayers } from '../build-layers.ts'; +import type { DtcgFile } from '../dtcg.ts'; +import { + validateAliasDirection, + validateDtcgSyntax, + validateDualModes, + validateStructure, +} from '../guards.ts'; +import { findRepoRoot, referencePath, semanticPath } from '../paths.ts'; +import { readSnapshot, snapshotById } from '../snapshot.ts'; + +const fixturesDir = join(dirname(fileURLToPath(import.meta.url)), 'fixtures'); + +function readFixture(name: string): DtcgFile { + return JSON.parse(readFileSync(join(fixturesDir, name), 'utf8')) as DtcgFile; +} + +describe('DS-3 · 结构守卫', () => { + const repoRoot = findRepoRoot(); + const layers = buildLayers(snapshotById(readSnapshot(repoRoot))); + + it('正式影子层 DTCG 语法合法、alias 单向、双模式齐全', () => { + expect(validateStructure(layers)).toEqual([]); + expect(JSON.parse(readFileSync(referencePath(repoRoot), 'utf8'))).toEqual( + layers.reference, + ); + expect(JSON.parse(readFileSync(semanticPath(repoRoot), 'utf8'))).toEqual( + layers.semantic, + ); + }); + + it('错误 fixture:非法 DTCG 语法被命中', () => { + const issues = validateDtcgSyntax(readFixture('invalid-syntax.json'), 'invalid-syntax'); + expect(issues.some((issue) => issue.code === 'invalid-syntax')).toBe(true); + }); + + it('错误 fixture:缺少 dark 模式被命中', () => { + const issues = validateDualModes(readFixture('missing-dark-mode.json')); + expect(issues.some((issue) => issue.code === 'missing-mode')).toBe(true); + }); + + it('错误 fixture:reference 反向 alias 被命中', () => { + const issues = validateAliasDirection({ + reference: readFixture('alias-from-reference.json'), + semantic: layers.semantic, + }); + expect(issues.some((issue) => issue.code === 'alias-direction')).toBe(true); + }); +}); diff --git a/packages/design-tokens/src/__tests__/value-consistency.test.ts b/packages/design-tokens/src/__tests__/value-consistency.test.ts new file mode 100644 index 00000000000..9236c73fa9c --- /dev/null +++ b/packages/design-tokens/src/__tests__/value-consistency.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; + +import { buildLayers, resolvedSemanticValues } from '../build-layers.ts'; +import { SEMANTIC_ROLES } from '../semantic-roles.ts'; +import { assertSemanticMatchesSnapshot, snapshotMismatch } from '../guards.ts'; +import { findRepoRoot } from '../paths.ts'; +import { readSnapshot, snapshotById } from '../snapshot.ts'; + +describe('DS-3 · 逐值一致守卫', () => { + const snapshot = readSnapshot(findRepoRoot()); + const layers = buildLayers(snapshotById(snapshot)); + + it('每个 semantic 角色 light/dark 等于 DS-2b 冻结快照对应 id', () => { + assertSemanticMatchesSnapshot(layers, snapshot); + const resolved = resolvedSemanticValues(layers); + expect(resolved.size).toBe(SEMANTIC_ROLES.length); + for (const role of SEMANTIC_ROLES) { + const frozen = snapshot.colors.find((entry) => entry.id === role.id); + expect(frozen, snapshotMismatch(`快照缺少 ${role.id}`)).toBeTruthy(); + expect(resolved.get(role.id)).toEqual({ + light: frozen?.light, + dark: frozen?.dark, + }); + } + }); + + it('自证伪:模拟改一个 semantic 值后守卫必然红', () => { + const mutated = structuredClone(layers); + const firstRef = Object.keys(mutated.reference).find((key) => !key.startsWith('$')); + expect(firstRef).toBeTruthy(); + const leaf = mutated.reference[firstRef!] as { $value: string }; + leaf.$value = '#not-the-frozen-snapshot-value'; + expect(() => assertSemanticMatchesSnapshot(mutated, snapshot)).toThrow( + /与 DS-2b 冻结快照不一致/, + ); + }); +}); diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts new file mode 100644 index 00000000000..c6661f3761f --- /dev/null +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -0,0 +1,33 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +import { findRuntimeImportsOfDesignTokens } from '../guards.ts'; +import { findRepoRoot } from '../paths.ts'; + +describe('DS-3 · 零接线守卫', () => { + const repoRoot = findRepoRoot(); + + it('不被 desktop / mobile / 任何运行时 package import', () => { + expect(findRuntimeImportsOfDesignTokens(repoRoot)).toEqual([]); + }); + + it('本包 package.json 不被任何 workspace 声明为依赖,且不装 token 工具', () => { + const pkg = JSON.parse( + readFileSync(new URL('../../package.json', import.meta.url), 'utf8'), + ) as { + name: string; + dependencies?: Record; + devDependencies?: Record; + }; + expect(pkg.name).toBe('@cindy/design-tokens'); + expect(pkg.dependencies ?? {}).toEqual({}); + const deps = { + ...(pkg.dependencies ?? {}), + ...(pkg.devDependencies ?? {}), + }; + expect(Object.keys(deps).sort()).toEqual(['@types/node', 'typescript', 'vitest']); + expect(Object.keys(deps).some((name) => /terrazzo|style-dictionary/i.test(name))).toBe( + false, + ); + }); +}); diff --git a/packages/design-tokens/src/build-layers.ts b/packages/design-tokens/src/build-layers.ts new file mode 100644 index 00000000000..7d58758b864 --- /dev/null +++ b/packages/design-tokens/src/build-layers.ts @@ -0,0 +1,109 @@ +import { aliasValue, assertTokenName, type DtcgFile, type DtcgType } from './dtcg.ts'; +import { SEMANTIC_ROLES } from './semantic-roles.ts'; +import type { SnapshotColor } from './snapshot.ts'; + +export interface BuiltLayers { + reference: DtcgFile; + semantic: DtcgFile; +} + +function dtcgTypeFor(value: string): DtcgType { + return value.includes('%') && !value.startsWith('#') && !value.startsWith('rgb') + ? 'other' + : 'color'; +} + +function refTokenName(value: string, used: Map): string { + const existing = used.get(value); + if (existing) return existing; + const slugRaw = value + .replace(/^#/, 'hex-') + .replace(/%/g, 'pct') + .replace(/[^A-Za-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .toLowerCase() || 'value'; + const slug = /^[A-Za-z]/.test(slugRaw) ? slugRaw : `hsl-${slugRaw}`; + let name = slug; + let n = 2; + const taken = new Set(used.values()); + while (taken.has(name)) { + name = `${slug}-${n}`; + n += 1; + } + used.set(value, name); + assertTokenName(name); + return name; +} + +export function buildLayers(byId: Map): BuiltLayers { + const used = new Map(); + const referenceTokens: DtcgFile = { + $description: + 'DS-3 reference 层:仅收录第一批 semantic 角色实际引用的冻结色值。', + }; + const semantic: DtcgFile = { + $description: + 'DS-3 semantic 层:DESIGN.md §10 Tier-1 surface/border/text/accent + status。取值来自 DS-2b 冻结快照。', + }; + + for (const role of SEMANTIC_ROLES) { + const color = byId.get(role.id); + if (!color) { + throw new Error(`快照缺少 semantic 角色 ${role.id}`); + } + if (color.light == null || color.dark == null) { + throw new Error(`semantic 角色 ${role.id} 缺少 light/dark 双模式值`); + } + const lightName = refTokenName(color.light, used); + const darkName = refTokenName(color.dark, used); + if (!referenceTokens[lightName]) { + referenceTokens[lightName] = { + $type: dtcgTypeFor(color.light), + $value: color.light, + }; + } + if (!referenceTokens[darkName]) { + referenceTokens[darkName] = { + $type: dtcgTypeFor(color.dark), + $value: color.dark, + }; + } + if (!semantic[role.group]) { + semantic[role.group] = {}; + } + const group = semantic[role.group] as DtcgFile; + group[role.id] = { + light: { + $type: dtcgTypeFor(color.light), + $value: aliasValue([lightName]), + }, + dark: { + $type: dtcgTypeFor(color.dark), + $value: aliasValue([darkName]), + }, + }; + } + + return { reference: referenceTokens, semantic }; +} + +export function resolvedSemanticValues( + layers: BuiltLayers, +): Map { + const resolved = new Map(); + const reference = layers.reference; + for (const [groupName, group] of Object.entries(layers.semantic)) { + if (groupName.startsWith('$') || !group || typeof group === 'string') continue; + for (const [roleId, modes] of Object.entries(group as DtcgFile)) { + if (roleId.startsWith('$') || !modes || typeof modes === 'string') continue; + const lightAlias = (modes as DtcgFile).light as { $value: string }; + const darkAlias = (modes as DtcgFile).dark as { $value: string }; + const lightPath = lightAlias.$value.slice(1, -1); + const darkPath = darkAlias.$value.slice(1, -1); + const lightLeaf = reference[lightPath] as { $value: string }; + const darkLeaf = reference[darkPath] as { $value: string }; + resolved.set(roleId, { light: lightLeaf.$value, dark: darkLeaf.$value }); + } + } + return resolved; +} diff --git a/packages/design-tokens/src/classification.json b/packages/design-tokens/src/classification.json new file mode 100644 index 00000000000..78861eafe94 --- /dev/null +++ b/packages/design-tokens/src/classification.json @@ -0,0 +1,5096 @@ +{ + "source": "apps/desktop/src/renderer/themes/colors.ts", + "generatedBy": "packages/design-tokens/src/classify.ts", + "snapshotCount": 514, + "categories": { + "literal": 246, + "alias": 235, + "hsl-triplet": 6, + "runtime-derived-or-protected": 27 + }, + "entries": [ + { + "id": "surface", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "surface-hsl", + "category": "hsl-triplet", + "destination": "hsl-pair", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "hslPairOf": "surface", + "modeledAsSemantic": true + }, + { + "id": "surface-elevated", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "surface-elevated-soft", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "surface-card-ivory", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "surface-chip", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "surface-chip-alt", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "surface-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "surface-hover-soft", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "surface-hover-hsl", + "category": "hsl-triplet", + "destination": "hsl-pair", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "hslPairOf": "surface-hover", + "modeledAsSemantic": true + }, + { + "id": "surface-on-card", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "panel-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface", + "dark": "surface" + }, + "modeledAsSemantic": false + }, + { + "id": "md-table-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "md-h1-fg", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "css-keyword", + "darkKind": "css-keyword", + "modeledAsSemantic": false + }, + { + "id": "md-h2-fg", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "css-keyword", + "darkKind": "css-keyword", + "modeledAsSemantic": false + }, + { + "id": "md-h3-fg", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "css-keyword", + "darkKind": "css-keyword", + "modeledAsSemantic": false + }, + { + "id": "md-h4-fg", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "css-keyword", + "darkKind": "css-keyword", + "modeledAsSemantic": false + }, + { + "id": "md-h5-fg", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "css-keyword", + "darkKind": "css-keyword", + "modeledAsSemantic": false + }, + { + "id": "md-h6-fg", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "css-keyword", + "darkKind": "css-keyword", + "modeledAsSemantic": false + }, + { + "id": "md-strong-fg", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "css-keyword", + "darkKind": "css-keyword", + "modeledAsSemantic": false + }, + { + "id": "border-default", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "border-default-hsl", + "category": "hsl-triplet", + "destination": "hsl-pair", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "hslPairOf": "border-default", + "modeledAsSemantic": true + }, + { + "id": "border-shadcn-hsl", + "category": "hsl-triplet", + "destination": "hsl-pair", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "hslPairOf": "border-shadcn", + "modeledAsSemantic": true + }, + { + "id": "border-transparent-mixed", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "transparent", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "board", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "text-primary", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-primary-on-dark", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-primary-emphasis", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-primary-inv", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-primary-body-strong", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-primary-hsl", + "category": "hsl-triplet", + "destination": "hsl-pair", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "hslPairOf": "text-primary", + "modeledAsSemantic": true + }, + { + "id": "text-secondary", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "hex", + "darkKind": "hex", + "protected": { + "family": "u2-secondary-info", + "owner": "DESIGN.md §15.5", + "rule": "U2 二级信息色,never darken unilaterally" + }, + "modeledAsSemantic": false + }, + { + "id": "text-secondary-cross", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "hex", + "darkKind": "hex", + "protected": { + "family": "u2-secondary-info", + "owner": "DESIGN.md §15.5", + "rule": "U2 二级信息色,never darken unilaterally" + }, + "modeledAsSemantic": false + }, + { + "id": "text-secondary-mid", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-tertiary", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-tertiary-stone", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-tertiary-mid", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-tertiary-hsl", + "category": "hsl-triplet", + "destination": "hsl-pair", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "hslPairOf": "text-tertiary", + "modeledAsSemantic": true + }, + { + "id": "text-disabled", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-disabled-tertiary", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "text-placeholder", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "accent-cta-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "accent-cta-bg-pure", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "accent-emphasis", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "accent-soft", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "accent-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "accent-pure-cta-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "error-flat", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "warning-accent", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "process-agent-task-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "process-agent-service-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "process-main-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "process-renderer-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "process-gpu-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "process-utility-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "shadow-soft-panel", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "shadow", + "darkKind": "shadow", + "modeledAsSemantic": false + }, + { + "id": "background", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "foreground", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-hsl", + "dark": "text-primary-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "muted", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "muted-foreground", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-shadcn-hsl", + "dark": "border-shadcn-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "input", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-shadcn-hsl", + "dark": "border-shadcn-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "switch-track-off", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "switch-thumb-off", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-on-card", + "dark": "surface-on-card" + }, + "modeledAsSemantic": false + }, + { + "id": "switch-disabled-opacity", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "non-color", + "darkKind": "non-color", + "modeledAsSemantic": false + }, + { + "id": "switch-disabled-thumb-opacity", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "non-color", + "darkKind": "non-color", + "modeledAsSemantic": false + }, + { + "id": "switch-track-on", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "primary", + "dark": "primary" + }, + "modeledAsSemantic": false + }, + { + "id": "ring", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-hsl", + "dark": "text-primary-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "primary", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-hsl", + "dark": "text-primary-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "primary-foreground", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "secondary", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "secondary-foreground", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-hsl", + "dark": "text-primary-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "accent", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "accent-foreground", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-hsl", + "dark": "text-primary-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "popover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "popover-foreground", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-hsl", + "dark": "text-primary-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "radius", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "non-color", + "darkKind": "null", + "modeledAsSemantic": false + }, + { + "id": "titlebar", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hsl", + "dark": "surface-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "titlebar-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default-hsl", + "dark": "border-default-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "titlebar-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "titlebar-button-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hover-hsl", + "dark": "surface-hover-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "titlebar-control-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hover-hsl", + "dark": "surface-hover-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "splash-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "splash-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "splash-text-muted", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "splash-text-destructive", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "splash-fade-duration", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "non-color", + "darkKind": "null", + "modeledAsSemantic": false + }, + { + "id": "splash-fade-easing", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "non-color", + "darkKind": "null", + "modeledAsSemantic": false + }, + { + "id": "destructive", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": true + }, + { + "id": "confirm-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "confirm-shadow", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "shadow-soft-panel", + "dark": "shadow-soft-panel" + }, + "modeledAsSemantic": false + }, + { + "id": "confirm-title", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "confirm-desc", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-stone", + "dark": "text-tertiary-stone" + }, + "modeledAsSemantic": false + }, + { + "id": "confirm-btn-primary-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "confirm-btn-primary-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "confirm-btn-primary-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-hover", + "dark": "accent-hover" + }, + "modeledAsSemantic": false + }, + { + "id": "confirm-btn-secondary-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "confirm-btn-secondary-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "confirm-btn-secondary-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "sidebar", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hsl", + "dark": "surface-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "sidebar-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default-hsl", + "dark": "border-default-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "sidebar-item-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hover-hsl", + "dark": "surface-hover-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "sidebar-item-active", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "sidebar-item-active-foreground", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "foreground", + "dark": "foreground" + }, + "modeledAsSemantic": false + }, + { + "id": "sidebar-item-active-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "sidebar-item-active", + "dark": "sidebar-item-active" + }, + "modeledAsSemantic": false + }, + { + "id": "sidebar-search-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hsl", + "dark": "surface-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "sidebar-muted", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-hsl", + "dark": "text-tertiary-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "sidebar-action-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "search-match-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "search-match-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hsl-triplet", + "darkKind": "hsl-triplet", + "modeledAsSemantic": false + }, + { + "id": "update-btn-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "update-btn-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "update-btn-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "update-btn-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "transparent", + "darkKind": "transparent", + "modeledAsSemantic": false + }, + { + "id": "content-area", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hsl", + "dark": "surface-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "welcome-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-hsl", + "dark": "text-tertiary-hsl" + }, + "modeledAsSemantic": false + }, + { + "id": "login-bg-base", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-bg-gradient-radial", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "css-keyword", + "darkKind": "css-keyword", + "modeledAsSemantic": false + }, + { + "id": "login-bg-gradient-linear", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "css-keyword", + "darkKind": "css-keyword", + "modeledAsSemantic": false + }, + { + "id": "login-panel-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-brand-accent", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "hex", + "darkKind": "hex", + "protected": { + "family": "cindy-skin-family", + "owner": "DESIGN.md §15.1", + "rule": "CINDY 皮肤族品牌红,实现期零裁量" + }, + "modeledAsSemantic": false + }, + { + "id": "login-brand-accent-pressed", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "hex", + "darkKind": "hex", + "protected": { + "family": "cindy-skin-family", + "owner": "DESIGN.md §15.1", + "rule": "CINDY 皮肤族品牌红(pressed),实现期零裁量" + }, + "modeledAsSemantic": false + }, + { + "id": "login-link-pressed", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-callback-card-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-callback-card-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-callback-title", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-callback-body", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-callback-cta-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-callback-cta-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-callback-cta-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-panel-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-control-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-action-control-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-back-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-control-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-control-border-active", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-control-border-disabled", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-control-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-control-placeholder", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-title-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-secondary-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-primary-button-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-primary-button-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-primary-button-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-disabled-button-overlay", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-disabled-button-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-disabled-button-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-inverted-button-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-link-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-link-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-error-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-deletion-bubble-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-deletion-bubble-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-splash-progress-track", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-splash-progress-fill", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-overlay-button-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-overlay-button-pressed", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-overlay-back-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-overlay-back-pressed", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-overlay-row-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-overlay-row-pressed", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-overlay-input-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-loading-ring-track", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-consent-radio-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-consent-radio-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-consent-radio-checked-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-consent-radio-check", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-consent-overlay", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-secondary-button-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-secondary-button-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-secondary-button-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "login-overlay-secondary-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-overlay-secondary-pressed", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "login-apple-circle-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "lightbox-cta-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-cta-bg-pure", + "dark": "accent-cta-bg-pure" + }, + "modeledAsSemantic": false + }, + { + "id": "lightbox-cta-fg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-pure-cta-fg", + "dark": "accent-pure-cta-fg" + }, + "modeledAsSemantic": false + }, + { + "id": "lightbox-cta-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-hover", + "dark": "accent-hover" + }, + "modeledAsSemantic": false + }, + { + "id": "chat-input-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "chat-input-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "chat-input-border-focus", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "chat-input-placeholder-subtle", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "color-mix", + "darkKind": "color-mix", + "modeledAsSemantic": false + }, + { + "id": "chat-input-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "chat-input-placeholder", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-placeholder", + "dark": "text-placeholder" + }, + "modeledAsSemantic": false + }, + { + "id": "file-chip-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "drop-overlay-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "drop-overlay-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "tooltip-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "tooltip-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "file-remove-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "file-badge-pdf", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "file-badge-doc", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "file-badge-sheet", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "file-badge-slide", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "file-badge-code", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "file-badge-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "chat-input-chip-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-chip", + "dark": "surface-chip" + }, + "modeledAsSemantic": false + }, + { + "id": "chat-input-chip-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "chat-input-chip-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "chat-input-chip-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "cmd-palette-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "cmd-palette-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "cmd-palette-shadow", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "shadow-soft-panel", + "dark": "shadow-soft-panel" + }, + "modeledAsSemantic": false + }, + { + "id": "cmd-palette-item-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hover", + "dark": "surface-hover" + }, + "modeledAsSemantic": false + }, + { + "id": "cmd-palette-item-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "cmd-palette-item-meta", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "cmd-palette-item-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "cmd-palette-empty", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "cmd-palette-tooltip-body", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "send-btn-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-cta-bg", + "dark": "accent-cta-bg" + }, + "modeledAsSemantic": false + }, + { + "id": "send-btn-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-on-card", + "dark": "surface-on-card" + }, + "modeledAsSemantic": false + }, + { + "id": "send-btn-disabled-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated-soft", + "dark": "surface-elevated-soft" + }, + "modeledAsSemantic": false + }, + { + "id": "send-btn-disabled-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-disabled-tertiary", + "dark": "text-disabled-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "send-btn-hover-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "send-btn-bg", + "dark": "send-btn-bg" + }, + "modeledAsSemantic": false + }, + { + "id": "send-btn-pressed-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "send-btn-bg", + "dark": "send-btn-bg" + }, + "modeledAsSemantic": false + }, + { + "id": "perm-code-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "perm-code-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "perm-allow-btn-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "perm-allow-btn-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "perm-allow-kbd-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "perm-allow-kbd-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "model-trigger-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "model-trigger-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "model-trigger-meta", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "model-trigger-arrow", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary-cross", + "dark": "text-secondary-cross" + }, + "modeledAsSemantic": false + }, + { + "id": "thinking-body-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "model-dropdown-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "model-dropdown-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "model-item-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hover", + "dark": "surface-hover" + }, + "modeledAsSemantic": false + }, + { + "id": "model-item-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "model-item-check", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-cta-bg-pure", + "dark": "accent-cta-bg-pure" + }, + "modeledAsSemantic": false + }, + { + "id": "model-item-desc", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "model-section-label", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "favorite-star", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "effort-tier-minimal", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "effort-tier-low", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "effort-tier-medium", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "effort-tier-high", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "effort-tier-xhigh", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "effort-tier-max", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "effort-tier-ultra", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "price-tier-t1", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "price-tier-t2", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "price-tier-t3", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "fast-accent", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "engine-badge-cc", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "engine-badge-codex", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "engine-badge-pi", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "perm-item-selected-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "perm-auto-selected-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "perm-bypass-selected-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "warning-accent", + "dark": "warning-accent" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-picker-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-picker-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-item-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hover", + "dark": "surface-hover" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-item-name", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-item-path", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-item-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-label", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-btn-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "chat-input-bg", + "dark": "chat-input-bg" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-btn-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-btn-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-soft", + "dark": "accent-soft" + }, + "modeledAsSemantic": false + }, + { + "id": "folder-btn-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-soft", + "dark": "accent-soft" + }, + "modeledAsSemantic": false + }, + { + "id": "workingdir-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "workingdir-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "fast-toggle-off", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "fast-toggle-track", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-disabled", + "dark": "text-disabled" + }, + "modeledAsSemantic": false + }, + { + "id": "chat-placeholder-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-user-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-user-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-user-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-assistant-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-tool-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary-mid", + "dark": "text-secondary-mid" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-code-block-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-code-block-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-code-inline-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-chip", + "dark": "surface-chip" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-md-inline-code-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "msg-table-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-table-header-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface", + "dark": "surface" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-blockquote-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "agent-actions-rail", + "dark": "agent-actions-rail" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-blockquote-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-hr-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-scrollbar", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-scrollbar-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "msg-cursor", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-link", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "msg-tool-card-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-tool-card-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-tool-card-chevron", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary-mid", + "dark": "text-secondary-mid" + }, + "modeledAsSemantic": false + }, + { + "id": "msg-tool-card-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "todo-bar-track", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-chip", + "dark": "surface-chip" + }, + "modeledAsSemantic": false + }, + { + "id": "diff-del-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "diff-del-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "diff-del-emphasis", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "diff-add-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "pr-open-on-light", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "pr-open-on-dark", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "diff-add-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "diff-add-emphasis", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "diff-line-num", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-stone", + "dark": "text-tertiary-stone" + }, + "modeledAsSemantic": false + }, + { + "id": "info-700", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "agent-actions-rail", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "annotation-accent", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "hex", + "darkKind": "hex", + "protected": { + "family": "annotation-accent", + "owner": "DESIGN.md §15.4", + "rule": "图片标注烧录墨色,exempt, do not change" + }, + "modeledAsSemantic": false + }, + { + "id": "quota-bar-fill", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "quota-bar-warn", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "warning-fg", + "dark": "warning-fg" + }, + "modeledAsSemantic": false + }, + { + "id": "quota-bar-crit", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "error-flat", + "dark": "error-flat" + }, + "modeledAsSemantic": false + }, + { + "id": "quota-bar-track", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-chip", + "dark": "surface-chip" + }, + "modeledAsSemantic": false + }, + { + "id": "status-bar-accent", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "warning-accent", + "dark": "warning-accent" + }, + "modeledAsSemantic": false + }, + { + "id": "status-badge-fg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-pure-cta-fg", + "dark": "accent-pure-cta-fg" + }, + "modeledAsSemantic": false + }, + { + "id": "surface-translucent-sidebar", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface", + "dark": "surface" + }, + "modeledAsSemantic": false + }, + { + "id": "surface-translucent-main", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "surface-translucent-overlay", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "sidebar-search-input-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "composer-pill-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "composer-pill-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "status-bar-meta", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface", + "dark": "surface" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-divider", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-back-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-mid", + "dark": "text-tertiary-mid" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-back-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-back-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-menu-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-menu-text-selected", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-menu-bg-selected", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "settings-menu-border-selected", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-transparent-mixed", + "dark": "border-transparent-mixed" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-menu-bg-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "settings-section-title", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-section-desc", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-mid", + "dark": "text-tertiary-mid" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-section-sublabel", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-mid", + "dark": "text-tertiary-mid" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-profile-card-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-card-ivory", + "dark": "surface-card-ivory" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-profile-card-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-profile-avatar-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-chip-alt", + "dark": "surface-chip-alt" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-profile-avatar-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-profile-name", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-input-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-card-ivory", + "dark": "surface-card-ivory" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-input-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-input-border-focus", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-stone", + "dark": "text-tertiary-stone" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-input-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-input-placeholder", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-placeholder", + "dark": "text-placeholder" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-eye-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-stone", + "dark": "text-tertiary-stone" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-eye-icon-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-trash-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-trash-icon-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-source-meta", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary-cross", + "dark": "text-secondary-cross" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-source-link", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "settings-error-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "error-flat", + "dark": "error-flat" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-badge-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-card-ivory", + "dark": "surface-card-ivory" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-badge-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-badge-needs-config", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-badge-saved", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-badge-connected", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "card-status-done", + "dark": "card-status-done" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-badge-connected-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-badge-error", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "error-flat", + "dark": "error-flat" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-btn-primary-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-emphasis", + "dark": "accent-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-btn-primary-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "settings-btn-primary-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-emphasis", + "dark": "accent-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-btn-primary-hover-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-hover", + "dark": "accent-hover" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-btn-secondary-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-chip-alt", + "dark": "surface-chip-alt" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-btn-secondary-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-btn-secondary-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-btn-secondary-hover-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "settings-theme-card-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-card-ivory", + "dark": "surface-card-ivory" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-theme-card-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-social-card-pressed-bg", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "color-mix", + "darkKind": "color-mix", + "modeledAsSemantic": false + }, + { + "id": "settings-theme-preview-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface", + "dark": "surface" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-theme-preview-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-theme-preview-border-active", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-emphasis", + "dark": "accent-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-theme-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-stone", + "dark": "text-tertiary-stone" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-theme-icon-active", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-emphasis", + "dark": "accent-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-theme-label", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-stone", + "dark": "text-tertiary-stone" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-theme-label-active", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-emphasis", + "dark": "accent-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-theme-auto-light", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "settings-theme-auto-dark", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "settings-logout-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "settings-logout-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-logout-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-logout-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-logout-hover-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hover-soft", + "dark": "surface-hover-soft" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-integration-avatar-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "settings-integration-avatar-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "settings-integration-avatar-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-integration-subtitle", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "settings-integration-warning", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "warning-accent", + "dark": "warning-accent" + }, + "modeledAsSemantic": false + }, + { + "id": "remote-status-ready", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "remote-status-progress", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "remote-status-failed", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "card-status-awaiting", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "sidebar-draft-indicator", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "hex", + "darkKind": "alias", + "modeledAsSemantic": false + }, + { + "id": "card-status-error", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "card-status-done", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "completion-badge-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "remote-status-disconnected", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-card-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-card-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-header-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-page-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-option-label", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-option-desc", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-stone", + "dark": "text-tertiary-stone" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-option-custom", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-option-divider", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-option-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-badge-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-chip", + "dark": "surface-chip" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-badge-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-header-chip-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-chip", + "dark": "surface-chip" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-option-list-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-input-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-input-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-input-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-input-placeholder", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-placeholder", + "dark": "text-placeholder" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-send-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-cta-bg", + "dark": "accent-cta-bg" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-send-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-on-card", + "dark": "surface-on-card" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-send-disabled-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated-soft", + "dark": "surface-elevated-soft" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-send-disabled-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-disabled-tertiary", + "dark": "text-disabled-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-answered-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-expired-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-option-hover", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hover-soft", + "dark": "surface-hover-soft" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-checkbox-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "ask-checkbox-checked-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-cta-bg", + "dark": "accent-cta-bg" + }, + "modeledAsSemantic": false + }, + { + "id": "ask-checkbox-checked-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-on-card", + "dark": "surface-on-card" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-card-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-card-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-header-title", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-emphasis", + "dark": "text-primary-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-header-hint", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-header-divider", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-toolbar-btn-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-stone", + "dark": "text-tertiary-stone" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-toolbar-btn-hover-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "plan-outline-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-outline-border", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-outline-label", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-stone", + "dark": "text-tertiary-stone" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-outline-item-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary-stone", + "dark": "text-tertiary-stone" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-outline-active-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "plan-outline-active-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-emphasis", + "dark": "text-primary-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-content-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-elevated", + "dark": "surface-elevated" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-content-section", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-emphasis", + "dark": "text-primary-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-content-body", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-body-strong", + "dark": "text-primary-body-strong" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-content-divider", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-edit-body", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-body-strong", + "dark": "text-primary-body-strong" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-action-approve-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-emphasis", + "dark": "text-primary-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-action-approve-enter", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary-cross", + "dark": "text-secondary-cross" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-action-row-divider", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "border-default", + "dark": "border-default" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-action-fb-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary-cross", + "dark": "text-secondary-cross" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-action-fb-placeholder", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-placeholder", + "dark": "text-placeholder" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-action-fb-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-emphasis", + "dark": "text-primary-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-action-row-hover-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-hover-soft", + "dark": "surface-hover-soft" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-action-approve-icon-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "warning-accent", + "dark": "warning-accent" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-action-approve-icon-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "plan-min-title", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-emphasis", + "dark": "text-primary-emphasis" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-min-icon", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary-cross", + "dark": "text-secondary-cross" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-bubble-badge-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "surface-chip", + "dark": "surface-chip" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-bubble-badge-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary", + "dark": "text-primary" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-bubble-body-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-primary-body-strong", + "dark": "text-primary-body-strong" + }, + "modeledAsSemantic": false + }, + { + "id": "plan-bubble-summary-text", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-secondary", + "dark": "text-secondary" + }, + "modeledAsSemantic": false + }, + { + "id": "color-primary", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "color-neutral-300", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-disabled", + "dark": "text-disabled" + }, + "modeledAsSemantic": false + }, + { + "id": "color-neutral-400", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "color-error-600", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "error-flat", + "dark": "error-flat" + }, + "modeledAsSemantic": false + }, + { + "id": "color-error-700", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "focus-ring", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "focus-ring-soft", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": true + }, + { + "id": "text-selection-bg", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "focus-ring-soft", + "dark": "focus-ring-soft" + }, + "modeledAsSemantic": false + }, + { + "id": "shadow-chip-raised", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "shadow", + "darkKind": "shadow", + "modeledAsSemantic": false + }, + { + "id": "shadow-menu", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "shadow", + "darkKind": "shadow", + "modeledAsSemantic": false + }, + { + "id": "overlay-modal", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "overlay-lightbox", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "lightbox-toolbar-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "lightbox-toolbar-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "lightbox-toolbar-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "lightbox-toolbar-fg-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "lightbox-toolbar-hover-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "error-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "error-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "error-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "error-fg-strong", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "warning-bg-soft", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": true + }, + { + "id": "warning-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": true + }, + { + "id": "upgrade-banner-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "upgrade-banner-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "upgrade-banner-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "skillhub-review-pending-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "skillhub-review-pending-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "skillhub-review-pending-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "skillhub-review-quarantine-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "skillhub-review-quarantine-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "skillhub-review-quarantine-fg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-control-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-control-bg-hover", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "alias", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-control-bg-pressed", + "category": "runtime-derived-or-protected", + "destination": "register-only", + "lightKind": "alias", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-control-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-control-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-control-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-segment-track-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-segment-inactive-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-send-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-send-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-send-bg-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-send-bg-pressed", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-send-disabled-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-send-disabled-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-focus-ring", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "text-tertiary", + "dark": "text-tertiary" + }, + "modeledAsSemantic": false + }, + { + "id": "create-agent-quick-card-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-quick-card-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-quick-card-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-quick-card-icon-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-quick-card-icon", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-quick-card-bg-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "create-agent-avatar-ring", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "create-agent-avatar-glass-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "create-agent-avatar-inner-ring-start", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "create-agent-avatar-inner-ring-end", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "sidebar-nav-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "sidebar-list-muted", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "sidebar-user-card-bg", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "sidebar-user-card-bg-hover", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "sidebar-user-card-border", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "rgb", + "darkKind": "rgb", + "modeledAsSemantic": false + }, + { + "id": "sidebar-user-card-text", + "category": "literal", + "destination": "reference-candidate", + "lightKind": "hex", + "darkKind": "hex", + "modeledAsSemantic": false + }, + { + "id": "caret-accent", + "category": "alias", + "destination": "semantic-or-component-candidate", + "lightKind": "alias", + "darkKind": "alias", + "aliasOf": { + "light": "accent-cta-bg", + "dark": "accent-cta-bg" + }, + "modeledAsSemantic": false + } + ] +} diff --git a/packages/design-tokens/src/classify.ts b/packages/design-tokens/src/classify.ts new file mode 100644 index 00000000000..ba85bf3aa49 --- /dev/null +++ b/packages/design-tokens/src/classify.ts @@ -0,0 +1,226 @@ +import type { SnapshotColor } from './snapshot.ts'; +import { SEMANTIC_ROLE_IDS } from './semantic-roles.ts'; + +export const CLASSIFICATION_CATEGORIES = [ + 'literal', + 'alias', + 'hsl-triplet', + 'runtime-derived-or-protected', +] as const; + +export type ClassificationCategory = (typeof CLASSIFICATION_CATEGORIES)[number]; + +export type ClassificationDestination = + | 'reference-candidate' + | 'semantic-or-component-candidate' + | 'hsl-pair' + | 'register-only'; + +export interface ProtectedRule { + family: 'cindy-skin-family' | 'u2-secondary-info' | 'annotation-accent'; + owner: string; + rule: string; +} + +export interface ClassificationEntry { + id: string; + category: ClassificationCategory; + destination: ClassificationDestination; + lightKind: ValueKind; + darkKind: ValueKind; + aliasOf?: { light: string | null; dark: string | null }; + hslPairOf?: string; + protected?: ProtectedRule; + modeledAsSemantic: boolean; +} + +export interface ClassificationDocument { + source: string; + generatedBy: 'packages/design-tokens/src/classify.ts'; + snapshotCount: number; + categories: Record; + entries: ClassificationEntry[]; +} + +export type ValueKind = + | 'null' + | 'alias' + | 'hex' + | 'rgb' + | 'hsl-function' + | 'hsl-triplet' + | 'transparent' + | 'css-keyword' + | 'color-mix' + | 'shadow' + | 'non-color' + | 'mixed'; + +const VAR_RE = /^(?:hsl\()?var\(--([a-z0-9-]+)\)\)?$/; +const HEX_RE = /^#([0-9a-fA-F]{3,8})$/; +const RGB_RE = /^rgba?\(/; +const HSL_FN_RE = /^hsla?\(/; +const TRIPLET_RE = /^-?\d+(?:\.\d+)?\s+\d+(?:\.\d+)?%\s+\d+(?:\.\d+)?%$/; +const LENGTH_RE = /^\d+(?:\.\d+)?(?:px|rem|ms|em)$/; +const NUMBER_RE = /^\d+(?:\.\d+)?$/; + +/** + * 加严保护值:治理合同 §1.1 / DESIGN.md §15。 + * 只登记、不进 semantic 映射。CINDY 皮肤族在默认 ColorRegistry 里 + * 只有登录品牌红两项(其余皮肤值在 cindy-light/dark 主题 override,不在本快照)。 + */ +export const PROTECTED_IDS: Readonly> = { + 'annotation-accent': { + family: 'annotation-accent', + owner: 'DESIGN.md §15.4', + rule: '图片标注烧录墨色,exempt, do not change', + }, + 'text-secondary': { + family: 'u2-secondary-info', + owner: 'DESIGN.md §15.5', + rule: 'U2 二级信息色,never darken unilaterally', + }, + 'text-secondary-cross': { + family: 'u2-secondary-info', + owner: 'DESIGN.md §15.5', + rule: 'U2 二级信息色,never darken unilaterally', + }, + 'login-brand-accent': { + family: 'cindy-skin-family', + owner: 'DESIGN.md §15.1', + rule: 'CINDY 皮肤族品牌红,实现期零裁量', + }, + 'login-brand-accent-pressed': { + family: 'cindy-skin-family', + owner: 'DESIGN.md §15.1', + rule: 'CINDY 皮肤族品牌红(pressed),实现期零裁量', + }, +}; + +export function parseAliasTarget(value: string | null): string | null { + if (value == null) return null; + const match = VAR_RE.exec(value.trim()); + return match ? match[1] : null; +} + +export function classifyValue(value: string | null): ValueKind { + if (value == null) return 'null'; + const text = value.trim(); + if (text.startsWith('color-mix(')) return 'color-mix'; + if (parseAliasTarget(text)) return 'alias'; + if (text === 'transparent') return 'transparent'; + if (text === 'inherit' || text === 'none' || text === 'currentColor') { + return 'css-keyword'; + } + if (HEX_RE.test(text)) return 'hex'; + if (RGB_RE.test(text)) return 'rgb'; + if (HSL_FN_RE.test(text)) return 'hsl-function'; + if (TRIPLET_RE.test(text)) return 'hsl-triplet'; + if (LENGTH_RE.test(text) || NUMBER_RE.test(text) || text.startsWith('cubic-bezier')) { + return 'non-color'; + } + if (/\brgba?\(/.test(text)) return 'shadow'; + return 'mixed'; +} + +export function isLiteralKind(kind: ValueKind): boolean { + return ( + kind === 'hex' || + kind === 'rgb' || + kind === 'hsl-function' || + kind === 'hsl-triplet' || + kind === 'transparent' + ); +} + +export function classifyColor(entry: SnapshotColor): ClassificationEntry { + const lightKind = classifyValue(entry.light); + const darkKind = classifyValue(entry.dark); + const protectedRule = PROTECTED_IDS[entry.id]; + + if (protectedRule) { + return { + id: entry.id, + category: 'runtime-derived-or-protected', + destination: 'register-only', + lightKind, + darkKind, + protected: protectedRule, + modeledAsSemantic: false, + }; + } + + if (entry.id.endsWith('-hsl')) { + return { + id: entry.id, + category: 'hsl-triplet', + destination: 'hsl-pair', + lightKind, + darkKind, + hslPairOf: entry.id.slice(0, -'-hsl'.length), + modeledAsSemantic: SEMANTIC_ROLE_IDS.has(entry.id), + }; + } + + if (lightKind === 'alias' && darkKind === 'alias') { + return { + id: entry.id, + category: 'alias', + destination: 'semantic-or-component-candidate', + lightKind, + darkKind, + aliasOf: { + light: parseAliasTarget(entry.light), + dark: parseAliasTarget(entry.dark), + }, + modeledAsSemantic: SEMANTIC_ROLE_IDS.has(entry.id), + }; + } + + if (isLiteralKind(lightKind) && isLiteralKind(darkKind)) { + return { + id: entry.id, + category: 'literal', + destination: 'reference-candidate', + lightKind, + darkKind, + modeledAsSemantic: SEMANTIC_ROLE_IDS.has(entry.id), + }; + } + + return { + id: entry.id, + category: 'runtime-derived-or-protected', + destination: 'register-only', + lightKind, + darkKind, + modeledAsSemantic: false, + }; +} + +export function classifySnapshot( + colors: readonly SnapshotColor[], + source: string, +): ClassificationDocument { + const entries = colors.map(classifyColor); + const categories = { + literal: 0, + alias: 0, + 'hsl-triplet': 0, + 'runtime-derived-or-protected': 0, + } satisfies Record; + for (const entry of entries) { + categories[entry.category] += 1; + } + return { + source, + generatedBy: 'packages/design-tokens/src/classify.ts', + snapshotCount: colors.length, + categories, + entries, + }; +} + +export function stableStringify(value: unknown): string { + return `${JSON.stringify(value, null, 2)}\n`; +} diff --git a/packages/design-tokens/src/dtcg.ts b/packages/design-tokens/src/dtcg.ts new file mode 100644 index 00000000000..9af68514e68 --- /dev/null +++ b/packages/design-tokens/src/dtcg.ts @@ -0,0 +1,105 @@ +export type DtcgType = 'color' | 'other'; + +export interface DtcgValueNode { + $type: DtcgType; + $value: string; +} + +export interface DtcgAliasNode { + $type: DtcgType; + $value: `{${string}}`; +} + +export type DtcgLeaf = DtcgValueNode | DtcgAliasNode; + +export interface DtcgGroup { + [key: string]: DtcgNode; +} + +export type DtcgNode = DtcgLeaf | DtcgGroup; + +export interface DtcgFile { + $description?: string; + [key: string]: DtcgNode | string | undefined; +} + +export const TOKEN_NAME_RE = /^[A-Za-z][A-Za-z0-9_-]*$/; +const ALIAS_RE = /^\{([A-Za-z0-9][A-Za-z0-9._-]*)\}$/; + +export function isDtcgLeaf(node: unknown): node is DtcgLeaf { + if (!node || typeof node !== 'object' || Array.isArray(node)) return false; + const record = node as Record; + return typeof record.$value === 'string' && typeof record.$type === 'string'; +} + +export function parseAliasPath(value: string): string[] | null { + const match = ALIAS_RE.exec(value); + if (!match) return null; + return match[1].split('.'); +} + +export function aliasValue(path: string[]): `{${string}}` { + return `{${path.join('.')}}`; +} + +export function walkLeaves( + node: DtcgNode, + visit: (path: string[], leaf: DtcgLeaf) => void, + path: string[] = [], +): void { + if (isDtcgLeaf(node)) { + visit(path, node); + return; + } + for (const [key, child] of Object.entries(node)) { + if (key.startsWith('$')) continue; + walkLeaves(child, visit, [...path, key]); + } +} + +export function lookupPath(root: DtcgGroup, path: string[]): DtcgNode | undefined { + let current: DtcgNode | undefined = root; + for (const segment of path) { + if (!current || isDtcgLeaf(current) || typeof current !== 'object') return undefined; + current = current[segment]; + } + return current; +} + +export function assertTokenName(name: string): void { + if (!TOKEN_NAME_RE.test(name)) { + throw new Error(`非法 DTCG token 名: ${name}`); + } +} + +export function collectLeaves(file: DtcgFile): Array<{ path: string[]; leaf: DtcgLeaf }> { + const leaves: Array<{ path: string[]; leaf: DtcgLeaf }> = []; + for (const [key, value] of Object.entries(file)) { + if (key.startsWith('$') || value == null || typeof value === 'string') continue; + walkLeaves(value, (path, leaf) => leaves.push({ path: [key, ...path], leaf })); + } + return leaves; +} + +export function resolveAlias( + files: Record, + fromFile: string, + value: string, +): { file: string; path: string[]; leaf: DtcgLeaf } | null { + const path = parseAliasPath(value); + if (!path) return null; + const file = files[fromFile]; + if (!file) return null; + const local = lookupPath(file as DtcgGroup, path); + if (isDtcgLeaf(local)) { + return { file: fromFile, path, leaf: local }; + } + for (const [name, other] of Object.entries(files)) { + if (name === fromFile) continue; + const found = lookupPath(other as DtcgGroup, path); + if (isDtcgLeaf(found)) { + return { file: name, path, leaf: found }; + } + } + return null; +} diff --git a/packages/design-tokens/src/generate.ts b/packages/design-tokens/src/generate.ts new file mode 100644 index 00000000000..53a1362f28c --- /dev/null +++ b/packages/design-tokens/src/generate.ts @@ -0,0 +1,55 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; + +import { buildLayers } from './build-layers.ts'; +import { classifySnapshot, stableStringify } from './classify.ts'; +import { + classificationPath, + findRepoRoot, + referencePath, + semanticPath, +} from './paths.ts'; +import { readSnapshot, snapshotById } from './snapshot.ts'; + +export function buildShadowLayerFiles(repoRoot = findRepoRoot()) { + const snapshot = readSnapshot(repoRoot); + if (snapshot.colors.length !== snapshot.count) { + throw new Error( + `快照 count=${snapshot.count} 与 colors.length=${snapshot.colors.length} 不一致`, + ); + } + const classification = classifySnapshot(snapshot.colors, snapshot.source); + const layers = buildLayers(snapshotById(snapshot)); + return { + classification, + layers, + files: [ + { path: classificationPath(repoRoot), body: stableStringify(classification) }, + { path: referencePath(repoRoot), body: stableStringify(layers.reference) }, + { path: semanticPath(repoRoot), body: stableStringify(layers.semantic) }, + ], + }; +} + +export function writeShadowLayerFiles( + files: Array<{ path: string; body: string }>, +): void { + for (const file of files) { + mkdirSync(dirname(file.path), { recursive: true }); + writeFileSync(file.path, file.body, 'utf8'); + } +} + +export function generateShadowLayer(repoRoot = findRepoRoot()) { + const built = buildShadowLayerFiles(repoRoot); + writeShadowLayerFiles(built.files); + return built; +} + +const isMain = + process.argv[1] && + (process.argv[1].endsWith('generate.ts') || process.argv[1].endsWith('generate.js')); + +if (isMain) { + generateShadowLayer(); +} diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts new file mode 100644 index 00000000000..8ed382df91c --- /dev/null +++ b/packages/design-tokens/src/guards.ts @@ -0,0 +1,328 @@ +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +import { resolvedSemanticValues, type BuiltLayers } from './build-layers.ts'; +import { + CLASSIFICATION_CATEGORIES, + type ClassificationCategory, + type ClassificationDocument, + PROTECTED_IDS, +} from './classify.ts'; +import { + collectLeaves, + isDtcgLeaf, + parseAliasPath, + resolveAlias, + TOKEN_NAME_RE, + type DtcgFile, +} from './dtcg.ts'; +import { + CLASSIFICATION_RELATIVE_PATH, + REFERENCE_RELATIVE_PATH, + SEMANTIC_RELATIVE_PATH, + SNAPSHOT_RELATIVE_PATH, +} from './paths.ts'; +import { SEMANTIC_ROLE_IDS } from './semantic-roles.ts'; +import type { ColorDefaultsSnapshot } from './snapshot.ts'; + +export function snapshotMismatch(what: string): string { + return [ + `${what} 与 DS-2b 冻结快照不一致。`, + '红灯不是禁令。有意改值的合法路径 = 同一 PR 更新本快照 + 同步影子层(packages/design-tokens 的 generate 脚本)+ 按治理合同 §6 交证据 + 设计师批准。', + `更新方式:把实时提取结果写回 ${SNAPSHOT_RELATIVE_PATH},并重新生成 ${CLASSIFICATION_RELATIVE_PATH} / ${REFERENCE_RELATIVE_PATH} / ${SEMANTIC_RELATIVE_PATH}。`, + '保护值(CINDY 皮肤族 DESIGN.md §15、U2 二级信息色、annotation-accent)另有比「改快照 + 设计师批准」更严的门槛,不能只更新本文件。', + '禁止为绿灯加豁免或绕加载路径。', + ].join('\n'); +} + +export function assertClassificationCoversSnapshot( + classification: ClassificationDocument, + snapshot: ColorDefaultsSnapshot, +): void { + if (classification.entries.length !== snapshot.count) { + throw new Error( + snapshotMismatch( + `classification.entries.length=${classification.entries.length} vs snapshot.count=${snapshot.count}`, + ), + ); + } + if (classification.entries.length !== snapshot.colors.length) { + throw new Error(snapshotMismatch('classification 条目数与快照 colors.length')); + } + + const snapshotIds = snapshot.colors.map((entry) => entry.id); + const classIds = classification.entries.map((entry) => entry.id); + if (JSON.stringify(classIds) !== JSON.stringify(snapshotIds)) { + throw new Error(snapshotMismatch('classification 的 id 顺序/集合')); + } + + const seen = new Set(); + const tallies: Record = { + literal: 0, + alias: 0, + 'hsl-triplet': 0, + 'runtime-derived-or-protected': 0, + }; + for (const entry of classification.entries) { + if (seen.has(entry.id)) { + throw new Error(`分类登记重复 id: ${entry.id}`); + } + seen.add(entry.id); + if (!CLASSIFICATION_CATEGORIES.includes(entry.category)) { + throw new Error(`未知分类 ${entry.category}(id=${entry.id})`); + } + tallies[entry.category] += 1; + } + for (const category of CLASSIFICATION_CATEGORIES) { + if (tallies[category] !== classification.categories[category]) { + throw new Error( + `分类计数不一致:${category} tally=${tallies[category]} header=${classification.categories[category]}`, + ); + } + } + const sum = CLASSIFICATION_CATEGORIES.reduce( + (total, category) => total + classification.categories[category], + 0, + ); + if (sum !== snapshot.count) { + throw new Error( + `四类计数之和 ${sum} ≠ snapshot.count ${snapshot.count}(分类必须互斥完备)`, + ); + } +} + +export function assertProtectedNotSemantic( + classification: ClassificationDocument, +): void { + for (const [id, rule] of Object.entries(PROTECTED_IDS)) { + const entry = classification.entries.find((item) => item.id === id); + if (!entry) { + throw new Error(`保护值 ${id} 未出现在分类登记`); + } + if (entry.category !== 'runtime-derived-or-protected') { + throw new Error(`保护值 ${id} 分类应为 runtime-derived-or-protected,实际 ${entry.category}`); + } + if (entry.modeledAsSemantic) { + throw new Error(`保护值 ${id} 不得进入 semantic 映射`); + } + if (entry.protected?.family !== rule.family) { + throw new Error(`保护值 ${id} 的 family 标记错误`); + } + if (SEMANTIC_ROLE_IDS.has(id)) { + throw new Error(`保护值 ${id} 出现在 SEMANTIC_ROLES`); + } + } +} + +export function assertSemanticMatchesSnapshot( + layers: BuiltLayers, + snapshot: ColorDefaultsSnapshot, +): void { + const byId = new Map(snapshot.colors.map((entry) => [entry.id, entry])); + const resolved = resolvedSemanticValues(layers); + if (resolved.size !== SEMANTIC_ROLE_IDS.size) { + throw new Error( + `semantic 角色数 ${resolved.size} ≠ SEMANTIC_ROLES ${SEMANTIC_ROLE_IDS.size}`, + ); + } + for (const [id, value] of resolved) { + const frozen = byId.get(id); + if (!frozen) { + throw new Error(snapshotMismatch(`semantic 角色 ${id} 不在冻结快照中`)); + } + if (value.light !== frozen.light || value.dark !== frozen.dark) { + throw new Error( + snapshotMismatch( + `semantic.${id} light/dark(影子层 ${value.light}/${value.dark} vs 快照 ${frozen.light}/${frozen.dark})`, + ), + ); + } + } +} + +export interface StructureIssue { + code: + | 'invalid-syntax' + | 'missing-mode' + | 'alias-cycle' + | 'alias-unresolved' + | 'alias-direction'; + message: string; +} + +function fileLeaves(file: DtcgFile) { + return collectLeaves(file); +} + +export function validateDtcgSyntax(file: DtcgFile, fileName: string): StructureIssue[] { + const issues: StructureIssue[] = []; + try { + const leaves = fileLeaves(file); + if (leaves.length === 0) { + issues.push({ + code: 'invalid-syntax', + message: `${fileName} 没有任何 $value 叶子`, + }); + } + for (const { path, leaf } of leaves) { + if (path.some((segment) => !TOKEN_NAME_RE.test(segment))) { + issues.push({ + code: 'invalid-syntax', + message: `${fileName} 路径 ${path.join('.')} 含非法 token 名`, + }); + } + if (leaf.$type !== 'color' && leaf.$type !== 'other') { + issues.push({ + code: 'invalid-syntax', + message: `${fileName} ${path.join('.')} 的 $type 非法: ${leaf.$type}`, + }); + } + if (typeof leaf.$value !== 'string' || leaf.$value.length === 0) { + issues.push({ + code: 'invalid-syntax', + message: `${fileName} ${path.join('.')} 缺少 $value`, + }); + } + } + } catch (error) { + issues.push({ + code: 'invalid-syntax', + message: `${fileName} DTCG 语法无法遍历: ${error instanceof Error ? error.message : String(error)}`, + }); + } + return issues; +} + +export function validateDualModes(semantic: DtcgFile): StructureIssue[] { + const issues: StructureIssue[] = []; + for (const [groupName, group] of Object.entries(semantic)) { + if (groupName.startsWith('$') || !group || typeof group === 'string' || isDtcgLeaf(group)) { + continue; + } + for (const [roleId, modes] of Object.entries(group as DtcgFile)) { + if (roleId.startsWith('$') || !modes || typeof modes === 'string') continue; + if (isDtcgLeaf(modes)) { + issues.push({ + code: 'missing-mode', + message: `semantic.${groupName}.${roleId} 不是 light/dark 分组`, + }); + continue; + } + const record = modes as DtcgFile; + for (const mode of ['light', 'dark'] as const) { + if (!isDtcgLeaf(record[mode])) { + issues.push({ + code: 'missing-mode', + message: `semantic.${groupName}.${roleId} 缺少 ${mode}`, + }); + } + } + } + } + return issues; +} + +export function validateAliasDirection(layers: BuiltLayers): StructureIssue[] { + const issues: StructureIssue[] = []; + const files = { + reference: layers.reference, + semantic: layers.semantic, + }; + + for (const { path, leaf } of fileLeaves(layers.reference)) { + if (parseAliasPath(leaf.$value)) { + issues.push({ + code: 'alias-direction', + message: `reference.${path.join('.')} 不得使用 alias(reference 只能持有字面量)`, + }); + } + } + + for (const { path, leaf } of fileLeaves(layers.semantic)) { + const alias = parseAliasPath(leaf.$value); + if (!alias) { + issues.push({ + code: 'alias-direction', + message: `semantic.${path.join('.')} 必须 alias 到 reference,不能写字面量`, + }); + continue; + } + const resolved = resolveAlias(files, 'semantic', leaf.$value); + if (!resolved) { + issues.push({ + code: 'alias-unresolved', + message: `semantic.${path.join('.')} 无法解析 ${leaf.$value}`, + }); + continue; + } + if (resolved.file === 'semantic') { + issues.push({ + code: 'alias-direction', + message: `semantic.${path.join('.')} 解析回 semantic,违反单向依赖`, + }); + } + if (parseAliasPath(resolved.leaf.$value)) { + issues.push({ + code: 'alias-cycle', + message: `semantic.${path.join('.')} 指向的 reference 仍是 alias`, + }); + } + } + return issues; +} + +export function validateStructure(layers: BuiltLayers): StructureIssue[] { + return [ + ...validateDtcgSyntax(layers.reference, 'reference'), + ...validateDtcgSyntax(layers.semantic, 'semantic'), + ...validateDualModes(layers.semantic), + ...validateAliasDirection(layers), + ]; +} + +const RUNTIME_PACKAGE_DIRS = ['apps/desktop', 'apps/mobile', 'packages']; + +function dependencyHits(pkgJson: Record, rel: string): string[] { + const hits: string[] = []; + for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) { + const deps = pkgJson[field]; + if (deps && typeof deps === 'object' && '@cindy/design-tokens' in deps) { + hits.push(`${rel}#${field}`); + } + } + return hits; +} + +export function findRuntimeImportsOfDesignTokens(repoRoot: string): string[] { + const hits: string[] = []; + const skip = new Set(['node_modules', 'dist', 'build', 'coverage', '.git']); + const importRe = + /from\s+['"]@cindy\/design-tokens|require\(['"]@cindy\/design-tokens|packages\/design-tokens\/src\//; + + const visit = (abs: string, rel: string) => { + const entries = readdirSync(abs, { withFileTypes: true }); + for (const entry of entries) { + if (skip.has(entry.name)) continue; + const childAbs = join(abs, entry.name); + const childRel = rel ? `${rel}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + if (childRel === 'packages/design-tokens') continue; + visit(childAbs, childRel); + continue; + } + if (entry.name === 'package.json') { + const pkg = JSON.parse(readFileSync(childAbs, 'utf8')) as Record; + hits.push(...dependencyHits(pkg, childRel)); + continue; + } + if (!/\.(ts|tsx|js|mjs|cjs)$/.test(entry.name)) continue; + const text = readFileSync(childAbs, 'utf8'); + if (importRe.test(text)) hits.push(childRel); + } + }; + + for (const dir of RUNTIME_PACKAGE_DIRS) { + visit(join(repoRoot, dir), dir); + } + return hits; +} diff --git a/packages/design-tokens/src/paths.ts b/packages/design-tokens/src/paths.ts new file mode 100644 index 00000000000..2c2a742adb8 --- /dev/null +++ b/packages/design-tokens/src/paths.ts @@ -0,0 +1,44 @@ +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** DS-2b 冻结快照。DS-3 只读这份 fixture,不解析 colors.ts。 */ +export const SNAPSHOT_RELATIVE_PATH = + 'apps/desktop/src/renderer/themes/__tests__/fixtures/desktop-color-defaults.json'; + +export const CLASSIFICATION_RELATIVE_PATH = + 'packages/design-tokens/src/classification.json'; + +export const REFERENCE_RELATIVE_PATH = + 'packages/design-tokens/src/reference/color.json'; + +export const SEMANTIC_RELATIVE_PATH = + 'packages/design-tokens/src/semantic/color.json'; + +export function findRepoRoot(startDir = dirname(fileURLToPath(import.meta.url))): string { + let dir = startDir; + while (true) { + if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir; + const parent = dirname(dir); + if (parent === dir) { + throw new Error('找不到仓库根(缺少 pnpm-workspace.yaml)'); + } + dir = parent; + } +} + +export function snapshotPath(repoRoot = findRepoRoot()): string { + return join(repoRoot, SNAPSHOT_RELATIVE_PATH); +} + +export function classificationPath(repoRoot = findRepoRoot()): string { + return join(repoRoot, CLASSIFICATION_RELATIVE_PATH); +} + +export function referencePath(repoRoot = findRepoRoot()): string { + return join(repoRoot, REFERENCE_RELATIVE_PATH); +} + +export function semanticPath(repoRoot = findRepoRoot()): string { + return join(repoRoot, SEMANTIC_RELATIVE_PATH); +} diff --git a/packages/design-tokens/src/reference/color.json b/packages/design-tokens/src/reference/color.json new file mode 100644 index 00000000000..c4b2fc912dc --- /dev/null +++ b/packages/design-tokens/src/reference/color.json @@ -0,0 +1,179 @@ +{ + "$description": "DS-3 reference 层:仅收录第一批 semantic 角色实际引用的冻结色值。", + "hex-f8f8f6": { + "$type": "color", + "$value": "#f8f8f6" + }, + "hex-1f1f1e": { + "$type": "color", + "$value": "#1f1f1e" + }, + "hsl-60-12-5pct-97pct": { + "$type": "other", + "$value": "60 12.5% 97%" + }, + "hsl-60-2pct-12pct": { + "$type": "other", + "$value": "60 2% 12%" + }, + "hex-ffffff": { + "$type": "color", + "$value": "#ffffff" + }, + "hex-2c2c2a": { + "$type": "color", + "$value": "#2c2c2a" + }, + "hex-e5e5e5": { + "$type": "color", + "$value": "#e5e5e5" + }, + "hex-faf9f5": { + "$type": "color", + "$value": "#faf9f5" + }, + "hex-3c3c3a": { + "$type": "color", + "$value": "#3c3c3a" + }, + "hsl-0-0pct-90pct": { + "$type": "other", + "$value": "0 0% 90%" + }, + "hsl-60-2pct-17pct": { + "$type": "other", + "$value": "60 2% 17%" + }, + "hex-d7d7d4": { + "$type": "color", + "$value": "#d7d7d4" + }, + "hsl-60-3pct-84pct": { + "$type": "other", + "$value": "60 3% 84%" + }, + "hsl-60-2pct-23pct": { + "$type": "other", + "$value": "60 2% 23%" + }, + "hsl-30-4pct-28pct": { + "$type": "other", + "$value": "30 4% 28%" + }, + "transparent": { + "$type": "color", + "$value": "transparent" + }, + "hex-262626": { + "$type": "color", + "$value": "#262626" + }, + "hex-d4d4d4": { + "$type": "color", + "$value": "#d4d4d4" + }, + "hsl-0-0pct-9pct": { + "$type": "other", + "$value": "0 0% 9%" + }, + "hsl-0-0pct-83pct": { + "$type": "other", + "$value": "0 0% 83%" + }, + "hex-1a1a1a": { + "$type": "color", + "$value": "#1a1a1a" + }, + "hex-525252": { + "$type": "color", + "$value": "#525252" + }, + "hex-a3a3a3": { + "$type": "color", + "$value": "#a3a3a3" + }, + "hex-737373": { + "$type": "color", + "$value": "#737373" + }, + "hsl-0-0pct-45pct": { + "$type": "other", + "$value": "0 0% 45%" + }, + "hex-c4c4c4": { + "$type": "color", + "$value": "#c4c4c4" + }, + "hex-000000": { + "$type": "color", + "$value": "#000000" + }, + "hex-417cdd": { + "$type": "color", + "$value": "#417CDD" + }, + "rgba-65-124-221-0-5": { + "$type": "color", + "$value": "rgba(65, 124, 221, 0.5)" + }, + "hsl-0-84pct-60pct": { + "$type": "other", + "$value": "0 84% 60%" + }, + "hsl-0-72pct-63pct": { + "$type": "other", + "$value": "0 72% 63%" + }, + "hex-ef4444": { + "$type": "color", + "$value": "#ef4444" + }, + "hex-fef2f2": { + "$type": "color", + "$value": "#fef2f2" + }, + "hex-3a2222": { + "$type": "color", + "$value": "#3a2222" + }, + "rgba-220-38-38-0-4": { + "$type": "color", + "$value": "rgba(220, 38, 38, 0.4)" + }, + "hex-7f1d1d": { + "$type": "color", + "$value": "#7f1d1d" + }, + "hex-dc2626": { + "$type": "color", + "$value": "#dc2626" + }, + "hex-f87171": { + "$type": "color", + "$value": "#f87171" + }, + "hex-991b1b": { + "$type": "color", + "$value": "#991b1b" + }, + "hex-fca5a5": { + "$type": "color", + "$value": "#fca5a5" + }, + "hex-ea6b17": { + "$type": "color", + "$value": "#EA6B17" + }, + "hex-f3a115": { + "$type": "color", + "$value": "#F3A115" + }, + "rgba-234-107-23-0-12": { + "$type": "color", + "$value": "rgba(234, 107, 23, 0.12)" + }, + "rgba-234-107-23-0-18": { + "$type": "color", + "$value": "rgba(234, 107, 23, 0.18)" + } +} diff --git a/packages/design-tokens/src/semantic-roles.ts b/packages/design-tokens/src/semantic-roles.ts new file mode 100644 index 00000000000..acdc243798d --- /dev/null +++ b/packages/design-tokens/src/semantic-roles.ts @@ -0,0 +1,66 @@ +/** + * DS-3 第一批 semantic 角色:DESIGN.md §10 Tier-1 既有名称 + * (surface / border / text / accent)+ status 语义。 + * 不改名、不发明新语义。U2 与 annotation-accent 等保护值不在此列。 + */ +export type SemanticGroup = 'surface' | 'border' | 'text' | 'accent' | 'status'; + +export interface SemanticRole { + id: string; + group: SemanticGroup; +} + +export const SEMANTIC_ROLES: readonly SemanticRole[] = [ + { id: 'surface', group: 'surface' }, + { id: 'surface-hsl', group: 'surface' }, + { id: 'surface-elevated', group: 'surface' }, + { id: 'surface-elevated-soft', group: 'surface' }, + { id: 'surface-card-ivory', group: 'surface' }, + { id: 'surface-chip', group: 'surface' }, + { id: 'surface-chip-alt', group: 'surface' }, + { id: 'surface-hover', group: 'surface' }, + { id: 'surface-hover-soft', group: 'surface' }, + { id: 'surface-hover-hsl', group: 'surface' }, + { id: 'surface-on-card', group: 'surface' }, + + { id: 'border-default', group: 'border' }, + { id: 'border-default-hsl', group: 'border' }, + { id: 'border-shadcn-hsl', group: 'border' }, + { id: 'border-transparent-mixed', group: 'border' }, + + { id: 'text-primary', group: 'text' }, + { id: 'text-primary-hsl', group: 'text' }, + { id: 'text-primary-on-dark', group: 'text' }, + { id: 'text-primary-emphasis', group: 'text' }, + { id: 'text-primary-inv', group: 'text' }, + { id: 'text-primary-body-strong', group: 'text' }, + { id: 'text-secondary-mid', group: 'text' }, + { id: 'text-tertiary', group: 'text' }, + { id: 'text-tertiary-stone', group: 'text' }, + { id: 'text-tertiary-mid', group: 'text' }, + { id: 'text-tertiary-hsl', group: 'text' }, + { id: 'text-disabled', group: 'text' }, + { id: 'text-disabled-tertiary', group: 'text' }, + { id: 'text-placeholder', group: 'text' }, + + { id: 'accent-cta-bg', group: 'accent' }, + { id: 'accent-cta-bg-pure', group: 'accent' }, + { id: 'accent-emphasis', group: 'accent' }, + { id: 'accent-soft', group: 'accent' }, + { id: 'accent-hover', group: 'accent' }, + { id: 'accent-pure-cta-fg', group: 'accent' }, + { id: 'focus-ring', group: 'accent' }, + { id: 'focus-ring-soft', group: 'accent' }, + + { id: 'destructive', group: 'status' }, + { id: 'error-flat', group: 'status' }, + { id: 'error-bg', group: 'status' }, + { id: 'error-border', group: 'status' }, + { id: 'error-fg', group: 'status' }, + { id: 'error-fg-strong', group: 'status' }, + { id: 'warning-accent', group: 'status' }, + { id: 'warning-fg', group: 'status' }, + { id: 'warning-bg-soft', group: 'status' }, +] as const; + +export const SEMANTIC_ROLE_IDS = new Set(SEMANTIC_ROLES.map((role) => role.id)); diff --git a/packages/design-tokens/src/semantic/color.json b/packages/design-tokens/src/semantic/color.json new file mode 100644 index 00000000000..fe93ad60ea6 --- /dev/null +++ b/packages/design-tokens/src/semantic/color.json @@ -0,0 +1,473 @@ +{ + "$description": "DS-3 semantic 层:DESIGN.md §10 Tier-1 surface/border/text/accent + status。取值来自 DS-2b 冻结快照。", + "surface": { + "surface": { + "light": { + "$type": "color", + "$value": "{hex-f8f8f6}" + }, + "dark": { + "$type": "color", + "$value": "{hex-1f1f1e}" + } + }, + "surface-hsl": { + "light": { + "$type": "other", + "$value": "{hsl-60-12-5pct-97pct}" + }, + "dark": { + "$type": "other", + "$value": "{hsl-60-2pct-12pct}" + } + }, + "surface-elevated": { + "light": { + "$type": "color", + "$value": "{hex-ffffff}" + }, + "dark": { + "$type": "color", + "$value": "{hex-2c2c2a}" + } + }, + "surface-elevated-soft": { + "light": { + "$type": "color", + "$value": "{hex-e5e5e5}" + }, + "dark": { + "$type": "color", + "$value": "{hex-2c2c2a}" + } + }, + "surface-card-ivory": { + "light": { + "$type": "color", + "$value": "{hex-faf9f5}" + }, + "dark": { + "$type": "color", + "$value": "{hex-2c2c2a}" + } + }, + "surface-chip": { + "light": { + "$type": "color", + "$value": "{hex-e5e5e5}" + }, + "dark": { + "$type": "color", + "$value": "{hex-3c3c3a}" + } + }, + "surface-chip-alt": { + "light": { + "$type": "color", + "$value": "{hex-e5e5e5}" + }, + "dark": { + "$type": "color", + "$value": "{hex-2c2c2a}" + } + }, + "surface-hover": { + "light": { + "$type": "color", + "$value": "{hex-e5e5e5}" + }, + "dark": { + "$type": "color", + "$value": "{hex-3c3c3a}" + } + }, + "surface-hover-soft": { + "light": { + "$type": "color", + "$value": "{hex-f8f8f6}" + }, + "dark": { + "$type": "color", + "$value": "{hex-3c3c3a}" + } + }, + "surface-hover-hsl": { + "light": { + "$type": "other", + "$value": "{hsl-0-0pct-90pct}" + }, + "dark": { + "$type": "other", + "$value": "{hsl-60-2pct-17pct}" + } + }, + "surface-on-card": { + "light": { + "$type": "color", + "$value": "{hex-ffffff}" + }, + "dark": { + "$type": "color", + "$value": "{hex-1f1f1e}" + } + } + }, + "border": { + "border-default": { + "light": { + "$type": "color", + "$value": "{hex-d7d7d4}" + }, + "dark": { + "$type": "color", + "$value": "{hex-3c3c3a}" + } + }, + "border-default-hsl": { + "light": { + "$type": "other", + "$value": "{hsl-60-3pct-84pct}" + }, + "dark": { + "$type": "other", + "$value": "{hsl-60-2pct-23pct}" + } + }, + "border-shadcn-hsl": { + "light": { + "$type": "other", + "$value": "{hsl-0-0pct-90pct}" + }, + "dark": { + "$type": "other", + "$value": "{hsl-30-4pct-28pct}" + } + }, + "border-transparent-mixed": { + "light": { + "$type": "color", + "$value": "{transparent}" + }, + "dark": { + "$type": "color", + "$value": "{hex-3c3c3a}" + } + } + }, + "text": { + "text-primary": { + "light": { + "$type": "color", + "$value": "{hex-262626}" + }, + "dark": { + "$type": "color", + "$value": "{hex-d4d4d4}" + } + }, + "text-primary-hsl": { + "light": { + "$type": "other", + "$value": "{hsl-0-0pct-9pct}" + }, + "dark": { + "$type": "other", + "$value": "{hsl-0-0pct-83pct}" + } + }, + "text-primary-on-dark": { + "light": { + "$type": "color", + "$value": "{hex-262626}" + }, + "dark": { + "$type": "color", + "$value": "{hex-ffffff}" + } + }, + "text-primary-emphasis": { + "light": { + "$type": "color", + "$value": "{hex-1a1a1a}" + }, + "dark": { + "$type": "color", + "$value": "{hex-d4d4d4}" + } + }, + "text-primary-inv": { + "light": { + "$type": "color", + "$value": "{hex-1a1a1a}" + }, + "dark": { + "$type": "color", + "$value": "{hex-ffffff}" + } + }, + "text-primary-body-strong": { + "light": { + "$type": "color", + "$value": "{hex-525252}" + }, + "dark": { + "$type": "color", + "$value": "{hex-d4d4d4}" + } + }, + "text-secondary-mid": { + "light": { + "$type": "color", + "$value": "{hex-525252}" + }, + "dark": { + "$type": "color", + "$value": "{hex-a3a3a3}" + } + }, + "text-tertiary": { + "light": { + "$type": "color", + "$value": "{hex-a3a3a3}" + }, + "dark": { + "$type": "color", + "$value": "{hex-737373}" + } + }, + "text-tertiary-stone": { + "light": { + "$type": "color", + "$value": "{hex-737373}" + }, + "dark": { + "$type": "color", + "$value": "{hex-737373}" + } + }, + "text-tertiary-mid": { + "light": { + "$type": "color", + "$value": "{hex-525252}" + }, + "dark": { + "$type": "color", + "$value": "{hex-737373}" + } + }, + "text-tertiary-hsl": { + "light": { + "$type": "other", + "$value": "{hsl-0-0pct-45pct}" + }, + "dark": { + "$type": "other", + "$value": "{hsl-0-0pct-45pct}" + } + }, + "text-disabled": { + "light": { + "$type": "color", + "$value": "{hex-d4d4d4}" + }, + "dark": { + "$type": "color", + "$value": "{hex-525252}" + } + }, + "text-disabled-tertiary": { + "light": { + "$type": "color", + "$value": "{hex-a3a3a3}" + }, + "dark": { + "$type": "color", + "$value": "{hex-737373}" + } + }, + "text-placeholder": { + "light": { + "$type": "color", + "$value": "{hex-c4c4c4}" + }, + "dark": { + "$type": "color", + "$value": "{hex-525252}" + } + } + }, + "accent": { + "accent-cta-bg": { + "light": { + "$type": "color", + "$value": "{hex-262626}" + }, + "dark": { + "$type": "color", + "$value": "{hex-ffffff}" + } + }, + "accent-cta-bg-pure": { + "light": { + "$type": "color", + "$value": "{hex-000000}" + }, + "dark": { + "$type": "color", + "$value": "{hex-ffffff}" + } + }, + "accent-emphasis": { + "light": { + "$type": "color", + "$value": "{hex-262626}" + }, + "dark": { + "$type": "color", + "$value": "{hex-d4d4d4}" + } + }, + "accent-soft": { + "light": { + "$type": "color", + "$value": "{hex-262626}" + }, + "dark": { + "$type": "color", + "$value": "{hex-ffffff}" + } + }, + "accent-hover": { + "light": { + "$type": "color", + "$value": "{hex-262626}" + }, + "dark": { + "$type": "color", + "$value": "{hex-e5e5e5}" + } + }, + "accent-pure-cta-fg": { + "light": { + "$type": "color", + "$value": "{hex-ffffff}" + }, + "dark": { + "$type": "color", + "$value": "{hex-000000}" + } + }, + "focus-ring": { + "light": { + "$type": "color", + "$value": "{hex-417cdd}" + }, + "dark": { + "$type": "color", + "$value": "{hex-417cdd}" + } + }, + "focus-ring-soft": { + "light": { + "$type": "color", + "$value": "{rgba-65-124-221-0-5}" + }, + "dark": { + "$type": "color", + "$value": "{rgba-65-124-221-0-5}" + } + } + }, + "status": { + "destructive": { + "light": { + "$type": "other", + "$value": "{hsl-0-84pct-60pct}" + }, + "dark": { + "$type": "other", + "$value": "{hsl-0-72pct-63pct}" + } + }, + "error-flat": { + "light": { + "$type": "color", + "$value": "{hex-ef4444}" + }, + "dark": { + "$type": "color", + "$value": "{hex-ef4444}" + } + }, + "error-bg": { + "light": { + "$type": "color", + "$value": "{hex-fef2f2}" + }, + "dark": { + "$type": "color", + "$value": "{hex-3a2222}" + } + }, + "error-border": { + "light": { + "$type": "color", + "$value": "{rgba-220-38-38-0-4}" + }, + "dark": { + "$type": "color", + "$value": "{hex-7f1d1d}" + } + }, + "error-fg": { + "light": { + "$type": "color", + "$value": "{hex-dc2626}" + }, + "dark": { + "$type": "color", + "$value": "{hex-f87171}" + } + }, + "error-fg-strong": { + "light": { + "$type": "color", + "$value": "{hex-991b1b}" + }, + "dark": { + "$type": "color", + "$value": "{hex-fca5a5}" + } + }, + "warning-accent": { + "light": { + "$type": "color", + "$value": "{hex-ea6b17}" + }, + "dark": { + "$type": "color", + "$value": "{hex-ea6b17}" + } + }, + "warning-fg": { + "light": { + "$type": "color", + "$value": "{hex-f3a115}" + }, + "dark": { + "$type": "color", + "$value": "{hex-f3a115}" + } + }, + "warning-bg-soft": { + "light": { + "$type": "color", + "$value": "{rgba-234-107-23-0-12}" + }, + "dark": { + "$type": "color", + "$value": "{rgba-234-107-23-0-18}" + } + } + } +} diff --git a/packages/design-tokens/src/snapshot.ts b/packages/design-tokens/src/snapshot.ts new file mode 100644 index 00000000000..b13d9aacdc6 --- /dev/null +++ b/packages/design-tokens/src/snapshot.ts @@ -0,0 +1,25 @@ +import { readFileSync } from 'node:fs'; + +import { snapshotPath } from './paths.ts'; + +export interface SnapshotColor { + id: string; + light: string | null; + dark: string | null; +} + +export interface ColorDefaultsSnapshot { + source: string; + count: number; + colors: SnapshotColor[]; +} + +export function readSnapshot(repoRoot?: string): ColorDefaultsSnapshot { + return JSON.parse(readFileSync(snapshotPath(repoRoot), 'utf8')) as ColorDefaultsSnapshot; +} + +export function snapshotById( + snapshot: ColorDefaultsSnapshot, +): Map { + return new Map(snapshot.colors.map((entry) => [entry.id, entry])); +} diff --git a/packages/design-tokens/tsconfig.json b/packages/design-tokens/tsconfig.json new file mode 100644 index 00000000000..c11d1c2c8f2 --- /dev/null +++ b/packages/design-tokens/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 511b773b168..260e29340ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -898,6 +898,18 @@ importers: specifier: ^3.2.4 version: 3.2.7(@types/debug@4.1.13)(@types/node@25.9.5)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + packages/design-tokens: + devDependencies: + '@types/node': + specifier: '*' + version: 25.9.5 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/debug@4.1.13)(@types/node@25.9.5)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + packages/device-link: dependencies: '@cindy/device-link-protocol': diff --git a/scripts/test-workspaces.config.mjs b/scripts/test-workspaces.config.mjs index 1fb944a3ecd..e4fe360e6d5 100644 --- a/scripts/test-workspaces.config.mjs +++ b/scripts/test-workspaces.config.mjs @@ -255,5 +255,6 @@ export default { noCollectableWorkspace('@cindy/device-link-protocol', 'packages/device-link-protocol'), requiredUnitWorkspace('@cindy/plugin-protocol', 'packages/plugin-protocol'), requiredUnitWorkspace('@cindy/slack-hook-protocol', 'packages/slack-hook-protocol'), + requiredUnitWorkspace('@cindy/design-tokens', 'packages/design-tokens'), ], }; From 9d5d2b5694bb69edd52421fbdc9fe3f2780bf505 Mon Sep 17 00:00:00 2001 From: kirozeng Date: Wed, 2 Sep 2026 16:53:11 +0800 Subject: [PATCH 02/15] fix-run ds3-r1b wave1 squash group g1 tip 793c462b68839fc857844d55b4e9d37031153c6b Signed-off-by: kirozeng --- .../src/__tests__/classification.test.ts | 88 +++++++++++++++++++ .../src/__tests__/zero-wiring.test.ts | 39 +++++++- packages/design-tokens/src/classify.ts | 9 +- packages/design-tokens/src/guards.ts | 35 +++++++- 4 files changed, 165 insertions(+), 6 deletions(-) diff --git a/packages/design-tokens/src/__tests__/classification.test.ts b/packages/design-tokens/src/__tests__/classification.test.ts index 956157437d0..03a13083019 100644 --- a/packages/design-tokens/src/__tests__/classification.test.ts +++ b/packages/design-tokens/src/__tests__/classification.test.ts @@ -2,9 +2,11 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { + classifyColor, classifySnapshot, PROTECTED_IDS, stableStringify, + type ClassificationCategory, } from '../classify.ts'; import { buildShadowLayerFiles } from '../generate.ts'; import { @@ -61,4 +63,90 @@ describe('DS-3 · 分类登记', () => { expect(entry?.protected).toBeTruthy(); } }); + + it('独立 oracle:分类类别必须与快照值实际形态语义一致,不能只看 id 后缀', () => { + // 独立于被测实现复刻分类语义口径(刻意不复用 classify.ts 的 + // classifyValue / isLiteralKind,改用自己的正则判定,分类规则变更时 + // 必须与本测试同步更新): + // protected 条目 → runtime-derived-or-protected(只看登记表); + // -hsl 后缀且双模式都是 hsl-triplet → hsl-triplet; + // 双 alias → alias;双字面量 → literal;其余一律 runtime-derived。 + const TRIPLET_VALUE_RE = /^-?\d+(?:\.\d+)?\s+\d+(?:\.\d+)?%\s+\d+(?:\.\d+)?%$/; + const ALIAS_VALUE_RE = /^(?:hsl\()?var\(--[a-z0-9-]+\)\)?$/; + const HEX_VALUE_RE = /^#([0-9a-fA-F]{3,8})$/; + const RGB_VALUE_RE = /^rgba?\(/; + const HSL_FN_VALUE_RE = /^hsla?\(/; + + const isAlias = (value: string | null) => + value != null && ALIAS_VALUE_RE.test(value.trim()); + const isTriplet = (value: string | null) => + value != null && TRIPLET_VALUE_RE.test(value.trim()); + const isLiteral = (value: string | null) => { + if (value == null) return false; + const text = value.trim(); + return ( + HEX_VALUE_RE.test(text) || + RGB_VALUE_RE.test(text) || + HSL_FN_VALUE_RE.test(text) || + text === 'transparent' || + isTriplet(value) + ); + }; + + const expected = new Map(); + for (const color of snapshot.colors) { + let category: ClassificationCategory; + if (color.id in PROTECTED_IDS) { + category = 'runtime-derived-or-protected'; + } else if ( + color.id.endsWith('-hsl') && + isTriplet(color.light) && + isTriplet(color.dark) + ) { + category = 'hsl-triplet'; + } else if (isAlias(color.light) && isAlias(color.dark)) { + category = 'alias'; + } else if (isLiteral(color.light) && isLiteral(color.dark)) { + category = 'literal'; + } else { + category = 'runtime-derived-or-protected'; + } + expected.set(color.id, category); + } + for (const entry of generated.entries) { + expect( + entry.category, + `id=${entry.id} category=${entry.category} 与独立判定 ${expected.get(entry.id)} 不一致`, + ).toBe(expected.get(entry.id)); + } + }); + + it('反证:-hsl 后缀但值非 triplet 的合成条目不许被分进 hsl-triplet', () => { + // 旧实现只看 -hsl 后缀就归 hsl-triplet;现在必须按双模式实际值判定。 + const hslSuffixedButLiteral = classifyColor({ + id: 'something-hsl', + light: '#000000', + dark: '#ffffff', + }); + expect(hslSuffixedButLiteral.category).toBe('literal'); + const hslSuffixedButAlias = classifyColor({ + id: 'something-hsl', + light: 'var(--surface)', + dark: 'var(--surface)', + }); + expect(hslSuffixedButAlias.category).toBe('alias'); + const hslSuffixedButSingleMode = classifyColor({ + id: 'something-hsl', + light: '60 12.5% 97%', + dark: '#ffffff', + }); + expect(hslSuffixedButSingleMode.category).toBe('literal'); + // 真实快照中全部 -hsl id 确实双模式都是 triplet(登记与现状一致) + const realHslIds = snapshot.colors.filter((color) => color.id.endsWith('-hsl')); + expect(realHslIds.length).toBeGreaterThan(0); + for (const color of realHslIds) { + expect(color.light).toMatch(/^-?\d+(?:\.\d+)?\s+\d+(?:\.\d+)?%\s+\d+(?:\.\d+)?%$/); + expect(color.dark).toMatch(/^-?\d+(?:\.\d+)?\s+\d+(?:\.\d+)?%\s+\d+(?:\.\d+)?%$/); + } + }); }); diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index c6661f3761f..6bef173b8b2 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -1,7 +1,10 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -import { findRuntimeImportsOfDesignTokens } from '../guards.ts'; +import { + containsRuntimeImportOfDesignTokens, + findRuntimeImportsOfDesignTokens, +} from '../guards.ts'; import { findRepoRoot } from '../paths.ts'; describe('DS-3 · 零接线守卫', () => { @@ -30,4 +33,38 @@ describe('DS-3 · 零接线守卫', () => { false, ); }); + + it('自证伪:每一种非法接线形态都必须被探测器命中', () => { + // 预测红集 = 除旧三形态(from / require / src 路径)外,本清单全部为新形态。 + // 逐个点名,避免 .some(...) 弱断言互相掩盖。 + const illegal: Array<[string, string]> = [ + ['静态具名 import', "import { surface } from '@cindy/design-tokens';"], + ['静态默认 import(双引号)', 'import surface from "@cindy/design-tokens";'], + ['命名空间 import', "import * as dt from '@cindy/design-tokens';"], + ['type-only import', "import type { surface } from '@cindy/design-tokens';"], + ['副作用 import', "import '@cindy/design-tokens';"], + ['动态 import()', "const layer = await import('@cindy/design-tokens');"], + ['动态 import() 跨行', "const layer = await import(\n '@cindy/design-tokens'\n);"], + ['TS import-equals', "import dt = require('@cindy/design-tokens');"], + ['CJS require', "const dt = require('@cindy/design-tokens');"], + ['require 子路径', "const pkg = require('@cindy/design-tokens/package.json');"], + ['import.meta.resolve', "const url = import.meta.resolve('@cindy/design-tokens');"], + ['re-export', "export { surface } from '@cindy/design-tokens';"], + ['相对路径直读包内源码', "import { x } from '../../../packages/design-tokens/src/snapshot.ts';"], + ]; + for (const [name, source] of illegal) { + expect(containsRuntimeImportOfDesignTokens(source), name).toBe(true); + } + }); + + it('自证伪:非导入语境(注释 / 普通字符串)不误报', () => { + const legal = [ + '// TODO(DS-8): wire "@cindy/design-tokens" here when consumers open up.', + "const moduleId = '@cindy/design-tokens';", + "console.log('migrating @cindy/design-tokens later');", + ]; + for (const source of legal) { + expect(containsRuntimeImportOfDesignTokens(source)).toBe(false); + } + }); }); diff --git a/packages/design-tokens/src/classify.ts b/packages/design-tokens/src/classify.ts index ba85bf3aa49..4266eea540b 100644 --- a/packages/design-tokens/src/classify.ts +++ b/packages/design-tokens/src/classify.ts @@ -150,7 +150,14 @@ export function classifyColor(entry: SnapshotColor): ClassificationEntry { }; } - if (entry.id.endsWith('-hsl')) { + // -hsl 后缀只是命名约定,不能单独作为判据:双模式必须真的都是 hsl-triplet + // 值(形如 `60 12.5% 97%`)。后缀命中但值不是 triplet 的条目按实际值形态 + // 落入 alias / literal / runtime-derived 分支,不许冒充 hsl-pair。 + if ( + entry.id.endsWith('-hsl') && + lightKind === 'hsl-triplet' && + darkKind === 'hsl-triplet' + ) { return { id: entry.id, category: 'hsl-triplet', diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index 8ed382df91c..770648e44b3 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -282,22 +282,49 @@ export function validateStructure(layers: BuiltLayers): StructureIssue[] { const RUNTIME_PACKAGE_DIRS = ['apps/desktop', 'apps/mobile', 'packages']; +export const DESIGN_TOKENS_MODULE_ID = '@cindy/design-tokens'; + function dependencyHits(pkgJson: Record, rel: string): string[] { const hits: string[] = []; for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) { const deps = pkgJson[field]; - if (deps && typeof deps === 'object' && '@cindy/design-tokens' in deps) { + if (deps && typeof deps === 'object' && DESIGN_TOKENS_MODULE_ID in deps) { hits.push(`${rel}#${field}`); } } return hits; } +/** + * 零运行时接线守卫必须拒绝所有合法的包导入与依赖入口,而不是只识别少数 + * 静态文本形态。逐一列出每种形态(模块 id 后允许子路径,如 + * '@cindy/design-tokens/package.json' 这类 subpath 消费同样算接线): + * - from 子句:静态具名 / 默认 / 命名空间 / type-only import 与 re-export + * (`\s` 覆盖跨行变体,不要求以 import 开头,避免漏掉 export…from); + * - import '' 副作用导入; + * - import('') 动态导入(可跨行); + * - require('') 与 TS import-equals(import x = require('')); + * - import.meta.resolve('') 运行期解析入口; + * - 绕过包 id、以相对路径直读包内源码 / 产物。 + * `(? pattern.test(text)); +} + export function findRuntimeImportsOfDesignTokens(repoRoot: string): string[] { const hits: string[] = []; const skip = new Set(['node_modules', 'dist', 'build', 'coverage', '.git']); - const importRe = - /from\s+['"]@cindy\/design-tokens|require\(['"]@cindy\/design-tokens|packages\/design-tokens\/src\//; const visit = (abs: string, rel: string) => { const entries = readdirSync(abs, { withFileTypes: true }); @@ -317,7 +344,7 @@ export function findRuntimeImportsOfDesignTokens(repoRoot: string): string[] { } if (!/\.(ts|tsx|js|mjs|cjs)$/.test(entry.name)) continue; const text = readFileSync(childAbs, 'utf8'); - if (importRe.test(text)) hits.push(childRel); + if (containsRuntimeImportOfDesignTokens(text)) hits.push(childRel); } }; From 77db609e2853d6ae3d249cb4ca509c8eee1d5459 Mon Sep 17 00:00:00 2001 From: Kirozeng Date: Wed, 2 Sep 2026 18:39:14 +0800 Subject: [PATCH 03/15] =?UTF-8?q?fix(design-tokens):=20HSL=20=E8=89=B2?= =?UTF-8?q?=E5=80=BC=E6=94=B9=E7=94=A8=E6=A0=87=E5=87=86=20DTCG=20?= =?UTF-8?q?=E9=A2=9C=E8=89=B2=E5=AF=B9=E8=B1=A1=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20Windows=20CRLF=20=E5=81=87=E7=BA=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 指出 12 个 HSL triplet reference 节点是自造的 $type: "other"—— 它不是标准 DTCG token 类型,且校验器此前显式放行,测试全绿也发现不了。 对治理合同锁定的 Terrazzo 2.7.1 实测:other 类型 token 被 CSS 插件静默 丢弃(不产出任何 CSS 变量),alias 到它同样无输出;裸 triplet 字符串在 $type: color 下被解析成黑色。DS-8 接线时影子层无法直接承担生成切换。 - dtcg.ts 新增 toDtcgColorObject / dtcgColorObjectToString:HSL triplet → {colorSpace:"hsl",components:[h,s,l]};hex/rgba/transparent → srgb 分量 (0-1) + 可选 alpha。$type 收紧为仅 color。 - build-layers.ts 删除 dtcgTypeFor 启发式,reference/semantic 全部节点 生成标准颜色对象;重生成两层 JSON(12 个 other reference + 14 个 other semantic alias 全部消除)。 - guards.ts:$type 校验不再放行 other;reference 只许颜色对象字面量, semantic 只许 alias;逐值一致守卫改在颜色对象层比较(hex 大小写不构成 色值差异)。 - .gitattributes 给 packages/design-tokens/src/**/*.json 钉 eol=lf(drizzle migration .sql 同款先例):Windows autocrlf 检出把生成物转 CRLF 导致 「磁盘 = 内存生成」字节一致守卫假红(本轮 CI Windows unit tests 红), 并补 git check-attr 回归测试钉住该契约。 - value-consistency.test.ts 补往返还原与 other 拒收的回归测试。 验证:pnpm --filter @cindy/design-tokens typecheck / vitest 19 测试全绿; pnpm test:unit:related 全量绿;生成器两次运行字节一致;最终生成物过 Terrazzo 2.7.1 tz build 零 lint 错误,136 token 全部产出 CSS 变量。 Signed-off-by: Kirozeng --- .gitattributes | 3 + packages/design-tokens/README.md | 6 + .../src/__tests__/classification.test.ts | 23 + .../src/__tests__/value-consistency.test.ts | 59 ++- packages/design-tokens/src/build-layers.ts | 38 +- packages/design-tokens/src/dtcg.ts | 140 +++++- packages/design-tokens/src/guards.ts | 110 ++++- .../design-tokens/src/reference/color.json | 425 +++++++++++++++--- .../design-tokens/src/semantic/color.json | 28 +- 9 files changed, 725 insertions(+), 107 deletions(-) diff --git a/.gitattributes b/.gitattributes index 5960a042bba..ac5be2e6b9f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,9 @@ .githooks/** text eol=lf # SQLite migration 内容参与用户数据库 drift hash;跨平台统一 LF。 apps/desktop/drizzle/**/*.sql text eol=lf +# design-tokens 影子层生成物参与「磁盘 = 内存生成」字节一致守卫;Windows +# autocrlf 检出转 CRLF 会让该守卫假红(classification.test.ts 实测),固定 LF。 +packages/design-tokens/src/**/*.json text eol=lf # sqlite-vec 扩展二进制(平台特定 .dylib / .dll)—— 走 Git LFS apps/desktop/native/sqlite-vec/**/*.dylib filter=lfs diff=lfs merge=lfs -text apps/desktop/native/sqlite-vec/**/*.dll filter=lfs diff=lfs merge=lfs -text diff --git a/packages/design-tokens/README.md b/packages/design-tokens/README.md index f6312e9c101..0dd904ecac7 100644 --- a/packages/design-tokens/README.md +++ b/packages/design-tokens/README.md @@ -29,6 +29,12 @@ Cindy 设计 token 的 **DTCG 影子层**(reference → semantic)。 依赖单向:semantic → reference。不建 component 层(DS-4)。不装 Terrazzo(DS-8)。 +色值一律用标准 DTCG 颜色对象(`$type: "color"` + `{colorSpace, components[, alpha]}`): +HSL triplet(`60 12.5% 97%`)→ `{"colorSpace":"hsl","components":[60,12.5,97]}`; +hex / rgba / transparent → srgb 分量(0–1)+ 可选 alpha。不用自定义 `$type` +(`"other"` 不是标准 DTCG 类型——Terrazzo 2.7.1 实测会静默丢弃这类 token, +DS-8 接线时无法生成 CSS 变量;裸 triplet 字符串也会被解析成黑色)。 + U2 二级信息色(`text-secondary` / `text-secondary-cross`)、`annotation-accent`、CINDY 皮肤族品牌红(`login-brand-accent` / `login-brand-accent-pressed`)按治理合同 §1.1 标记 **protected**,只登记、不进 semantic 映射。皮肤族其余值在 cindy-light/dark 主题 override 里,不在本快照默认值中。 ## 多入口投放合同(只写合同,DS-8 才接线) diff --git a/packages/design-tokens/src/__tests__/classification.test.ts b/packages/design-tokens/src/__tests__/classification.test.ts index 03a13083019..02c4d8bceec 100644 --- a/packages/design-tokens/src/__tests__/classification.test.ts +++ b/packages/design-tokens/src/__tests__/classification.test.ts @@ -1,3 +1,4 @@ +import { execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; @@ -54,6 +55,28 @@ describe('DS-3 · 分类登记', () => { } }); + it('生成物 JSON 检出行尾固定 LF(.gitattributes 已钉 eol=lf,Windows autocrlf 不会转 CRLF)', () => { + // CI 实锤(2026-09-02 Windows unit tests 红):core.autocrlf=true 的检出把 + // 生成物 JSON 转成 CRLF 后,上一条「磁盘 = 内存生成」字节一致守卫假红。 + // 修复 = .gitattributes 给 packages/design-tokens/src/**/*.json 钉 eol=lf + // (drizzle migration .sql 同款先例)。本测试钉住该契约:一旦有人删掉 + // .gitattributes 规则,这里用 git check-attr 直接红灯,不再等 Windows CI。 + for (const relPath of [ + 'packages/design-tokens/src/classification.json', + 'packages/design-tokens/src/reference/color.json', + 'packages/design-tokens/src/semantic/color.json', + ]) { + const attrs = execFileSync( + 'git', + ['check-attr', 'eol', '--', relPath], + { cwd: repoRoot, encoding: 'utf8' }, + ).trim(); + expect(attrs, `${relPath} 应被 .gitattributes 钉 eol=lf,实际: ${attrs}`).toBe( + `${relPath}: eol: lf`, + ); + } + }); + it('加严保护值标记 protected,未进 semantic 映射', () => { assertProtectedNotSemantic(generated); for (const id of Object.keys(PROTECTED_IDS)) { diff --git a/packages/design-tokens/src/__tests__/value-consistency.test.ts b/packages/design-tokens/src/__tests__/value-consistency.test.ts index 9236c73fa9c..2aa94d3304a 100644 --- a/packages/design-tokens/src/__tests__/value-consistency.test.ts +++ b/packages/design-tokens/src/__tests__/value-consistency.test.ts @@ -1,8 +1,13 @@ import { describe, expect, it } from 'vitest'; import { buildLayers, resolvedSemanticValues } from '../build-layers.ts'; +import { dtcgColorObjectToString, toDtcgColorObject, type DtcgFile } from '../dtcg.ts'; import { SEMANTIC_ROLES } from '../semantic-roles.ts'; -import { assertSemanticMatchesSnapshot, snapshotMismatch } from '../guards.ts'; +import { + assertSemanticMatchesSnapshot, + snapshotMismatch, + validateDtcgSyntax, +} from '../guards.ts'; import { findRepoRoot } from '../paths.ts'; import { readSnapshot, snapshotById } from '../snapshot.ts'; @@ -17,10 +22,14 @@ describe('DS-3 · 逐值一致守卫', () => { for (const role of SEMANTIC_ROLES) { const frozen = snapshot.colors.find((entry) => entry.id === role.id); expect(frozen, snapshotMismatch(`快照缺少 ${role.id}`)).toBeTruthy(); - expect(resolved.get(role.id)).toEqual({ - light: frozen?.light, - dark: frozen?.dark, - }); + // resolved 还原成字符串形态后与快照语义一致(hex 大小写不敏感)。 + const value = resolved.get(role.id)!; + expect(toDtcgColorObject(value.light)).toEqual( + toDtcgColorObject(frozen!.light!), + ); + expect(toDtcgColorObject(value.dark)).toEqual( + toDtcgColorObject(frozen!.dark!), + ); } }); @@ -28,10 +37,46 @@ describe('DS-3 · 逐值一致守卫', () => { const mutated = structuredClone(layers); const firstRef = Object.keys(mutated.reference).find((key) => !key.startsWith('$')); expect(firstRef).toBeTruthy(); - const leaf = mutated.reference[firstRef!] as { $value: string }; - leaf.$value = '#not-the-frozen-snapshot-value'; + const leaf = mutated.reference[firstRef!] as { $value: { colorSpace: string; components: number[] } }; + // $value 现在是标准 DTCG 颜色对象;模拟「改值不更新快照」= 换一组错误分量。 + leaf.$value = { colorSpace: 'srgb', components: [0.123, 0.123, 0.123] }; expect(() => assertSemanticMatchesSnapshot(mutated, snapshot)).toThrow( /与 DS-2b 冻结快照不一致/, ); }); + + it('标准颜色对象与快照原始字符串互逆(往返还原)', () => { + // HSL triplet、hex、rgba、transparent 四种快照形态都必须能 + // toDtcgColorObject → dtcgColorObjectToString 还原;hex 大小写在快照里 + // 两种都有(#f8f8f6 / #EA6B17),往返按不区分大小写校验。 + const samples = [ + '60 12.5% 97%', + '0 0% 45%', + '#f8f8f6', + '#EA6B17', + 'rgba(220, 38, 38, 0.4)', + 'transparent', + ]; + for (const original of samples) { + const roundTrip = dtcgColorObjectToString(toDtcgColorObject(original)); + expect(roundTrip.toLowerCase()).toBe(original.toLowerCase()); + } + }); + + it('自证伪:$type: "other" 不再被结构守卫放行', () => { + // 旧实现把 12 个 HSL triplet 生成 $type: "other" 且校验器显式放行; + // 现在自定义 $type 必须被 validateDtcgSyntax 命中(Terrazzo 会静默丢弃 + // other 类型 token,影子层不能靠非标准类型携带色值)。 + const badLayer: DtcgFile = { + $description: 'error fixture: legacy $type other must be rejected', + 'hsl-triplet': { + $type: 'other' as unknown as 'color', + $value: '60 12.5% 97%', + }, + }; + const issues = validateDtcgSyntax(badLayer, 'legacy-other'); + expect( + issues.some((issue) => issue.code === 'invalid-syntax'), + ).toBe(true); + }); }); diff --git a/packages/design-tokens/src/build-layers.ts b/packages/design-tokens/src/build-layers.ts index 7d58758b864..f71d142e6d1 100644 --- a/packages/design-tokens/src/build-layers.ts +++ b/packages/design-tokens/src/build-layers.ts @@ -1,4 +1,11 @@ -import { aliasValue, assertTokenName, type DtcgFile, type DtcgType } from './dtcg.ts'; +import { + aliasValue, + assertTokenName, + dtcgColorObjectToString, + toDtcgColorObject, + type DtcgColorObject, + type DtcgFile, +} from './dtcg.ts'; import { SEMANTIC_ROLES } from './semantic-roles.ts'; import type { SnapshotColor } from './snapshot.ts'; @@ -7,12 +14,6 @@ export interface BuiltLayers { semantic: DtcgFile; } -function dtcgTypeFor(value: string): DtcgType { - return value.includes('%') && !value.startsWith('#') && !value.startsWith('rgb') - ? 'other' - : 'color'; -} - function refTokenName(value: string, used: Map): string { const existing = used.get(value); if (existing) return existing; @@ -54,18 +55,20 @@ export function buildLayers(byId: Map): BuiltLayers { if (color.light == null || color.dark == null) { throw new Error(`semantic 角色 ${role.id} 缺少 light/dark 双模式值`); } + const lightValue = toDtcgColorObject(color.light); + const darkValue = toDtcgColorObject(color.dark); const lightName = refTokenName(color.light, used); const darkName = refTokenName(color.dark, used); if (!referenceTokens[lightName]) { referenceTokens[lightName] = { - $type: dtcgTypeFor(color.light), - $value: color.light, + $type: 'color', + $value: lightValue, }; } if (!referenceTokens[darkName]) { referenceTokens[darkName] = { - $type: dtcgTypeFor(color.dark), - $value: color.dark, + $type: 'color', + $value: darkValue, }; } if (!semantic[role.group]) { @@ -74,11 +77,11 @@ export function buildLayers(byId: Map): BuiltLayers { const group = semantic[role.group] as DtcgFile; group[role.id] = { light: { - $type: dtcgTypeFor(color.light), + $type: 'color', $value: aliasValue([lightName]), }, dark: { - $type: dtcgTypeFor(color.dark), + $type: 'color', $value: aliasValue([darkName]), }, }; @@ -100,9 +103,12 @@ export function resolvedSemanticValues( const darkAlias = (modes as DtcgFile).dark as { $value: string }; const lightPath = lightAlias.$value.slice(1, -1); const darkPath = darkAlias.$value.slice(1, -1); - const lightLeaf = reference[lightPath] as { $value: string }; - const darkLeaf = reference[darkPath] as { $value: string }; - resolved.set(roleId, { light: lightLeaf.$value, dark: darkLeaf.$value }); + const lightLeaf = reference[lightPath] as { $value: DtcgColorObject }; + const darkLeaf = reference[darkPath] as { $value: DtcgColorObject }; + resolved.set(roleId, { + light: dtcgColorObjectToString(lightLeaf.$value), + dark: dtcgColorObjectToString(darkLeaf.$value), + }); } } return resolved; diff --git a/packages/design-tokens/src/dtcg.ts b/packages/design-tokens/src/dtcg.ts index 9af68514e68..8ffe165103e 100644 --- a/packages/design-tokens/src/dtcg.ts +++ b/packages/design-tokens/src/dtcg.ts @@ -1,8 +1,21 @@ -export type DtcgType = 'color' | 'other'; +export type DtcgType = 'color'; + +/** + * 标准 DTCG 颜色对象值(W3C DTCG 规范 / Terrazzo 2.7.1 均支持)。 + * HSL triplet(如 `60 12.5% 97%`)表示为 `{"colorSpace":"hsl","components":[60,12.5,97]}`; + * hex / rgba 表示为 srgb 分量 + 可选 alpha。不再是自造的 `$type: "other"` 字符串。 + */ +export interface DtcgColorObject { + colorSpace: 'srgb' | 'hsl'; + components: number[]; + alpha?: number; +} + +export type DtcgColorValue = string | DtcgColorObject; export interface DtcgValueNode { $type: DtcgType; - $value: string; + $value: DtcgColorValue; } export interface DtcgAliasNode { @@ -29,7 +42,14 @@ const ALIAS_RE = /^\{([A-Za-z0-9][A-Za-z0-9._-]*)\}$/; export function isDtcgLeaf(node: unknown): node is DtcgLeaf { if (!node || typeof node !== 'object' || Array.isArray(node)) return false; const record = node as Record; - return typeof record.$value === 'string' && typeof record.$type === 'string'; + const valueIsString = typeof record.$value === 'string'; + const valueIsColorObject = + !!record.$value && + typeof record.$value === 'object' && + !Array.isArray(record.$value); + return ( + (valueIsString || valueIsColorObject) && typeof record.$type === 'string' + ); } export function parseAliasPath(value: string): string[] | null { @@ -72,6 +92,120 @@ export function assertTokenName(name: string): void { } } +const TRIPLET_COLOR_RE = + /^(-?\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/; +const HEX6_RE = /^#([0-9a-fA-F]{6})$/; +const HEX8_RE = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})$/; +const RGBA_RE = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/; +const RGB_RE = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/; + +function roundComponent(value: number): number { + // 6 位精度:覆盖快照里 63.92%(0.6392)等 2 位小数百分比且往返无损。 + return Math.round(value * 1e6) / 1e6; +} + +/** + * 把 DS-2b 冻结快照的原始色值字符串转成标准 DTCG 颜色对象。 + * + * HSL triplet(`60 12.5% 97%`)→ `{"colorSpace":"hsl","components":[60,12.5,97]}`; + * hex / rgba / transparent → `{"colorSpace":"srgb","components":[r,g,b](,"alpha")}`。 + * 通道归一到 [0,1](srgb),HSL 分量保持人读数值(h∈[0,360),s/l∈[0,100])。 + * 这是对 Terrazzo 2.7.1(治理合同 §5 锁定版本)实测过的表示: + * 裸 triplet 字符串会被解析成黑色,`$type: "other"` 会被 CSS 插件静默丢弃。 + */ +export function toDtcgColorObject(value: string): DtcgColorObject { + const text = value.trim(); + + const triplet = TRIPLET_COLOR_RE.exec(text); + if (triplet) { + return { + colorSpace: 'hsl', + components: [Number(triplet[1]), Number(triplet[2]), Number(triplet[3])], + }; + } + + if (text === 'transparent') { + return { colorSpace: 'srgb', components: [0, 0, 0], alpha: 0 }; + } + + const hex6 = HEX6_RE.exec(text); + if (hex6) { + const hex = hex6[1]; + return { + colorSpace: 'srgb', + components: [ + roundComponent(Number.parseInt(hex.slice(0, 2), 16) / 255), + roundComponent(Number.parseInt(hex.slice(2, 4), 16) / 255), + roundComponent(Number.parseInt(hex.slice(4, 6), 16) / 255), + ], + }; + } + + const hex8 = HEX8_RE.exec(text); + if (hex8) { + const hex = hex8[1]; + return { + colorSpace: 'srgb', + components: [ + roundComponent(Number.parseInt(hex.slice(0, 2), 16) / 255), + roundComponent(Number.parseInt(hex.slice(2, 4), 16) / 255), + roundComponent(Number.parseInt(hex.slice(4, 6), 16) / 255), + ], + alpha: roundComponent(Number.parseInt(hex8[2], 16) / 255), + }; + } + + const rgba = RGBA_RE.exec(text); + if (rgba) { + const components = [ + roundComponent(Number(rgba[1]) / 255), + roundComponent(Number(rgba[2]) / 255), + roundComponent(Number(rgba[3]) / 255), + ]; + const alpha = rgba[4] != null ? Number(rgba[4]) : undefined; + return alpha != null + ? { colorSpace: 'srgb', components, alpha } + : { colorSpace: 'srgb', components }; + } + + const rgb = RGB_RE.exec(text); + if (rgb) { + return { + colorSpace: 'srgb', + components: [ + roundComponent(Number(rgb[1]) / 255), + roundComponent(Number(rgb[2]) / 255), + roundComponent(Number(rgb[3]) / 255), + ], + }; + } + + throw new Error(`无法把快照色值转成标准 DTCG 颜色对象: ${value}`); +} + +/** + * 把 DTCG 颜色对象还原成快照原始字符串形态(逐值一致守卫用)。 + * 与 `toDtcgColorObject` 互逆;hsl 分量按快照的整数/小数原样还原。 + */ +export function dtcgColorObjectToString(color: DtcgColorObject): string { + if (color.colorSpace === 'hsl') { + const [h, s, l] = color.components; + return `${h} ${s}% ${l}%`; + } + const [r, g, b] = color.components; + const to255 = (v: number) => Math.round(v * 255); + const alpha = color.alpha; + if (alpha === 0) { + // 快照里全透明写作 CSS 关键字 transparent,不是 rgba(0,0,0,0)。 + return 'transparent'; + } + if (alpha != null && alpha < 1) { + return `rgba(${to255(r)}, ${to255(g)}, ${to255(b)}, ${alpha})`; + } + const hex = (v: number) => to255(v).toString(16).padStart(2, '0'); + return `#${hex(r)}${hex(g)}${hex(b)}`; +} + export function collectLeaves(file: DtcgFile): Array<{ path: string[]; leaf: DtcgLeaf }> { const leaves: Array<{ path: string[]; leaf: DtcgLeaf }> = []; for (const [key, value] of Object.entries(file)) { diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index 770648e44b3..6fb2b98d1f3 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -13,7 +13,9 @@ import { isDtcgLeaf, parseAliasPath, resolveAlias, + toDtcgColorObject, TOKEN_NAME_RE, + type DtcgColorObject, type DtcgFile, } from './dtcg.ts'; import { @@ -125,17 +127,53 @@ export function assertSemanticMatchesSnapshot( `semantic 角色数 ${resolved.size} ≠ SEMANTIC_ROLES ${SEMANTIC_ROLE_IDS.size}`, ); } + // 逐值一致在标准 DTCG 颜色对象层比较(语义等价),不在原始字符串层比较: + // 影子层 $value 是对象(如 {colorSpace:"hsl",components:[60,12.5,97]}), + // 快照是字符串("60 12.5% 97%");两侧都过 toDtcgColorObject 后必须深度相等。 + // hex 大小写(#417CDD vs #417cdd)不构成色值差异。 for (const [id, value] of resolved) { const frozen = byId.get(id); if (!frozen) { throw new Error(snapshotMismatch(`semantic 角色 ${id} 不在冻结快照中`)); } - if (value.light !== frozen.light || value.dark !== frozen.dark) { - throw new Error( - snapshotMismatch( - `semantic.${id} light/dark(影子层 ${value.light}/${value.dark} vs 快照 ${frozen.light}/${frozen.dark})`, - ), - ); + for (const mode of ['light', 'dark'] as const) { + const shadowValue = value[mode]; + const frozenValue = frozen[mode]; + if (shadowValue == null || frozenValue == null) { + throw new Error( + snapshotMismatch(`semantic.${id} ${mode} 缺值(影子层/快照)`), + ); + } + let shadowObj: DtcgColorObject; + let frozenObj: DtcgColorObject; + try { + shadowObj = + typeof shadowValue === 'string' + ? toDtcgColorObject(shadowValue) + : shadowValue; + } catch { + throw new Error( + snapshotMismatch(`semantic.${id} ${mode} 影子层值无法解析: ${String(shadowValue)}`), + ); + } + try { + frozenObj = toDtcgColorObject(frozenValue); + } catch { + throw new Error( + snapshotMismatch(`semantic.${id} ${mode} 快照值无法解析: ${frozenValue}`), + ); + } + if ( + shadowObj.colorSpace !== frozenObj.colorSpace || + JSON.stringify(shadowObj.components) !== JSON.stringify(frozenObj.components) || + shadowObj.alpha !== frozenObj.alpha + ) { + throw new Error( + snapshotMismatch( + `semantic.${id} ${mode}(影子层 ${JSON.stringify(shadowObj)} vs 快照 ${frozenValue})`, + ), + ); + } } } } @@ -150,6 +188,29 @@ export interface StructureIssue { message: string; } +const COLOR_SPACES = ['srgb', 'hsl'] as const; + +function isValidDtcgColorObject(value: unknown): value is DtcgColorObject { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const record = value as Record; + if (!COLOR_SPACES.includes(record.colorSpace as (typeof COLOR_SPACES)[number])) { + return false; + } + if (!Array.isArray(record.components) || record.components.length !== 3) { + return false; + } + if (!record.components.every((c) => typeof c === 'number' && Number.isFinite(c))) { + return false; + } + if ( + record.alpha != null && + (typeof record.alpha !== 'number' || record.alpha < 0 || record.alpha > 1) + ) { + return false; + } + return true; +} + function fileLeaves(file: DtcgFile) { return collectLeaves(file); } @@ -171,16 +232,32 @@ export function validateDtcgSyntax(file: DtcgFile, fileName: string): StructureI message: `${fileName} 路径 ${path.join('.')} 含非法 token 名`, }); } - if (leaf.$type !== 'color' && leaf.$type !== 'other') { + // $type 只允许标准 DTCG 颜色类型。此前这里放行自定义的 "other", + // 导致 12 个 HSL triplet 节点以非法类型落盘(Terrazzo 会静默丢弃, + // 见 build-layers.ts / dtcg.ts 的标准颜色对象迁移)。 + if (leaf.$type !== 'color') { issues.push({ code: 'invalid-syntax', message: `${fileName} ${path.join('.')} 的 $type 非法: ${leaf.$type}`, }); } - if (typeof leaf.$value !== 'string' || leaf.$value.length === 0) { + if (typeof leaf.$value === 'string') { + if (leaf.$value.length === 0) { + issues.push({ + code: 'invalid-syntax', + message: `${fileName} ${path.join('.')} 缺少 $value`, + }); + } + if (!parseAliasPath(leaf.$value)) { + issues.push({ + code: 'invalid-syntax', + message: `${fileName} ${path.join('.')} 的 $value 必须是 alias 或标准颜色对象,不能是裸字符串色值`, + }); + } + } else if (!isValidDtcgColorObject(leaf.$value)) { issues.push({ code: 'invalid-syntax', - message: `${fileName} ${path.join('.')} 缺少 $value`, + message: `${fileName} ${path.join('.')} 的 $value 不是合法标准 DTCG 颜色对象`, }); } } @@ -230,7 +307,8 @@ export function validateAliasDirection(layers: BuiltLayers): StructureIssue[] { }; for (const { path, leaf } of fileLeaves(layers.reference)) { - if (parseAliasPath(leaf.$value)) { + // reference 只能持有标准颜色对象字面量;alias 形态(`{…}` 字符串)违规。 + if (typeof leaf.$value === 'string' && parseAliasPath(leaf.$value)) { issues.push({ code: 'alias-direction', message: `reference.${path.join('.')} 不得使用 alias(reference 只能持有字面量)`, @@ -239,6 +317,13 @@ export function validateAliasDirection(layers: BuiltLayers): StructureIssue[] { } for (const { path, leaf } of fileLeaves(layers.semantic)) { + if (typeof leaf.$value !== 'string') { + issues.push({ + code: 'alias-direction', + message: `semantic.${path.join('.')} 必须 alias 到 reference,不能写字面量`, + }); + continue; + } const alias = parseAliasPath(leaf.$value); if (!alias) { issues.push({ @@ -261,7 +346,10 @@ export function validateAliasDirection(layers: BuiltLayers): StructureIssue[] { message: `semantic.${path.join('.')} 解析回 semantic,违反单向依赖`, }); } - if (parseAliasPath(resolved.leaf.$value)) { + if ( + typeof resolved.leaf.$value === 'string' && + parseAliasPath(resolved.leaf.$value) + ) { issues.push({ code: 'alias-cycle', message: `semantic.${path.join('.')} 指向的 reference 仍是 alias`, diff --git a/packages/design-tokens/src/reference/color.json b/packages/design-tokens/src/reference/color.json index c4b2fc912dc..67ce4b907a3 100644 --- a/packages/design-tokens/src/reference/color.json +++ b/packages/design-tokens/src/reference/color.json @@ -2,178 +2,491 @@ "$description": "DS-3 reference 层:仅收录第一批 semantic 角色实际引用的冻结色值。", "hex-f8f8f6": { "$type": "color", - "$value": "#f8f8f6" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.972549, + 0.972549, + 0.964706 + ] + } }, "hex-1f1f1e": { "$type": "color", - "$value": "#1f1f1e" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.121569, + 0.121569, + 0.117647 + ] + } }, "hsl-60-12-5pct-97pct": { - "$type": "other", - "$value": "60 12.5% 97%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 60, + 12.5, + 97 + ] + } }, "hsl-60-2pct-12pct": { - "$type": "other", - "$value": "60 2% 12%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 60, + 2, + 12 + ] + } }, "hex-ffffff": { "$type": "color", - "$value": "#ffffff" + "$value": { + "colorSpace": "srgb", + "components": [ + 1, + 1, + 1 + ] + } }, "hex-2c2c2a": { "$type": "color", - "$value": "#2c2c2a" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.172549, + 0.172549, + 0.164706 + ] + } }, "hex-e5e5e5": { "$type": "color", - "$value": "#e5e5e5" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.898039, + 0.898039, + 0.898039 + ] + } }, "hex-faf9f5": { "$type": "color", - "$value": "#faf9f5" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.980392, + 0.976471, + 0.960784 + ] + } }, "hex-3c3c3a": { "$type": "color", - "$value": "#3c3c3a" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.235294, + 0.235294, + 0.227451 + ] + } }, "hsl-0-0pct-90pct": { - "$type": "other", - "$value": "0 0% 90%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 0, + 0, + 90 + ] + } }, "hsl-60-2pct-17pct": { - "$type": "other", - "$value": "60 2% 17%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 60, + 2, + 17 + ] + } }, "hex-d7d7d4": { "$type": "color", - "$value": "#d7d7d4" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.843137, + 0.843137, + 0.831373 + ] + } }, "hsl-60-3pct-84pct": { - "$type": "other", - "$value": "60 3% 84%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 60, + 3, + 84 + ] + } }, "hsl-60-2pct-23pct": { - "$type": "other", - "$value": "60 2% 23%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 60, + 2, + 23 + ] + } }, "hsl-30-4pct-28pct": { - "$type": "other", - "$value": "30 4% 28%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 30, + 4, + 28 + ] + } }, "transparent": { "$type": "color", - "$value": "transparent" + "$value": { + "colorSpace": "srgb", + "components": [ + 0, + 0, + 0 + ], + "alpha": 0 + } }, "hex-262626": { "$type": "color", - "$value": "#262626" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.14902, + 0.14902, + 0.14902 + ] + } }, "hex-d4d4d4": { "$type": "color", - "$value": "#d4d4d4" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.831373, + 0.831373, + 0.831373 + ] + } }, "hsl-0-0pct-9pct": { - "$type": "other", - "$value": "0 0% 9%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 0, + 0, + 9 + ] + } }, "hsl-0-0pct-83pct": { - "$type": "other", - "$value": "0 0% 83%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 0, + 0, + 83 + ] + } }, "hex-1a1a1a": { "$type": "color", - "$value": "#1a1a1a" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.101961, + 0.101961, + 0.101961 + ] + } }, "hex-525252": { "$type": "color", - "$value": "#525252" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.321569, + 0.321569, + 0.321569 + ] + } }, "hex-a3a3a3": { "$type": "color", - "$value": "#a3a3a3" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.639216, + 0.639216, + 0.639216 + ] + } }, "hex-737373": { "$type": "color", - "$value": "#737373" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.45098, + 0.45098, + 0.45098 + ] + } }, "hsl-0-0pct-45pct": { - "$type": "other", - "$value": "0 0% 45%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 0, + 0, + 45 + ] + } }, "hex-c4c4c4": { "$type": "color", - "$value": "#c4c4c4" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.768627, + 0.768627, + 0.768627 + ] + } }, "hex-000000": { "$type": "color", - "$value": "#000000" + "$value": { + "colorSpace": "srgb", + "components": [ + 0, + 0, + 0 + ] + } }, "hex-417cdd": { "$type": "color", - "$value": "#417CDD" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.254902, + 0.486275, + 0.866667 + ] + } }, "rgba-65-124-221-0-5": { "$type": "color", - "$value": "rgba(65, 124, 221, 0.5)" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.254902, + 0.486275, + 0.866667 + ], + "alpha": 0.5 + } }, "hsl-0-84pct-60pct": { - "$type": "other", - "$value": "0 84% 60%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 0, + 84, + 60 + ] + } }, "hsl-0-72pct-63pct": { - "$type": "other", - "$value": "0 72% 63%" + "$type": "color", + "$value": { + "colorSpace": "hsl", + "components": [ + 0, + 72, + 63 + ] + } }, "hex-ef4444": { "$type": "color", - "$value": "#ef4444" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.937255, + 0.266667, + 0.266667 + ] + } }, "hex-fef2f2": { "$type": "color", - "$value": "#fef2f2" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.996078, + 0.94902, + 0.94902 + ] + } }, "hex-3a2222": { "$type": "color", - "$value": "#3a2222" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.227451, + 0.133333, + 0.133333 + ] + } }, "rgba-220-38-38-0-4": { "$type": "color", - "$value": "rgba(220, 38, 38, 0.4)" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.862745, + 0.14902, + 0.14902 + ], + "alpha": 0.4 + } }, "hex-7f1d1d": { "$type": "color", - "$value": "#7f1d1d" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.498039, + 0.113725, + 0.113725 + ] + } }, "hex-dc2626": { "$type": "color", - "$value": "#dc2626" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.862745, + 0.14902, + 0.14902 + ] + } }, "hex-f87171": { "$type": "color", - "$value": "#f87171" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.972549, + 0.443137, + 0.443137 + ] + } }, "hex-991b1b": { "$type": "color", - "$value": "#991b1b" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.6, + 0.105882, + 0.105882 + ] + } }, "hex-fca5a5": { "$type": "color", - "$value": "#fca5a5" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.988235, + 0.647059, + 0.647059 + ] + } }, "hex-ea6b17": { "$type": "color", - "$value": "#EA6B17" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.917647, + 0.419608, + 0.090196 + ] + } }, "hex-f3a115": { "$type": "color", - "$value": "#F3A115" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.952941, + 0.631373, + 0.082353 + ] + } }, "rgba-234-107-23-0-12": { "$type": "color", - "$value": "rgba(234, 107, 23, 0.12)" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.917647, + 0.419608, + 0.090196 + ], + "alpha": 0.12 + } }, "rgba-234-107-23-0-18": { "$type": "color", - "$value": "rgba(234, 107, 23, 0.18)" + "$value": { + "colorSpace": "srgb", + "components": [ + 0.917647, + 0.419608, + 0.090196 + ], + "alpha": 0.18 + } } } diff --git a/packages/design-tokens/src/semantic/color.json b/packages/design-tokens/src/semantic/color.json index fe93ad60ea6..8b36910b843 100644 --- a/packages/design-tokens/src/semantic/color.json +++ b/packages/design-tokens/src/semantic/color.json @@ -13,11 +13,11 @@ }, "surface-hsl": { "light": { - "$type": "other", + "$type": "color", "$value": "{hsl-60-12-5pct-97pct}" }, "dark": { - "$type": "other", + "$type": "color", "$value": "{hsl-60-2pct-12pct}" } }, @@ -93,11 +93,11 @@ }, "surface-hover-hsl": { "light": { - "$type": "other", + "$type": "color", "$value": "{hsl-0-0pct-90pct}" }, "dark": { - "$type": "other", + "$type": "color", "$value": "{hsl-60-2pct-17pct}" } }, @@ -125,21 +125,21 @@ }, "border-default-hsl": { "light": { - "$type": "other", + "$type": "color", "$value": "{hsl-60-3pct-84pct}" }, "dark": { - "$type": "other", + "$type": "color", "$value": "{hsl-60-2pct-23pct}" } }, "border-shadcn-hsl": { "light": { - "$type": "other", + "$type": "color", "$value": "{hsl-0-0pct-90pct}" }, "dark": { - "$type": "other", + "$type": "color", "$value": "{hsl-30-4pct-28pct}" } }, @@ -167,11 +167,11 @@ }, "text-primary-hsl": { "light": { - "$type": "other", + "$type": "color", "$value": "{hsl-0-0pct-9pct}" }, "dark": { - "$type": "other", + "$type": "color", "$value": "{hsl-0-0pct-83pct}" } }, @@ -257,11 +257,11 @@ }, "text-tertiary-hsl": { "light": { - "$type": "other", + "$type": "color", "$value": "{hsl-0-0pct-45pct}" }, "dark": { - "$type": "other", + "$type": "color", "$value": "{hsl-0-0pct-45pct}" } }, @@ -381,11 +381,11 @@ "status": { "destructive": { "light": { - "$type": "other", + "$type": "color", "$value": "{hsl-0-84pct-60pct}" }, "dark": { - "$type": "other", + "$type": "color", "$value": "{hsl-0-72pct-63pct}" } }, From a1c4b8cc9fe61605b76a4b22780e3ef8d4b1f4ca Mon Sep 17 00:00:00 2001 From: Kirozeng Date: Wed, 2 Sep 2026 18:55:13 +0800 Subject: [PATCH 04/15] =?UTF-8?q?fix(design-tokens):=20=E9=9B=B6=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=E5=AE=88=E5=8D=AB=E6=8C=89=E6=96=87=E4=BB=B6=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E8=A7=A3=E6=9E=90=E7=9B=B8=E5=AF=B9=20import=EF=BC=8C?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E5=99=A8=E6=A3=80=E6=9F=A5=E5=88=86=E9=87=8F?= =?UTF-8?q?=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex 新一轮 review 两条 P1 的跟进: ① 相对路径接线漏检(成立):兄弟 workspace(packages/foo/src/a.ts)写 `../../design-tokens/src/…` 时说明符不含 packages/ 段,旧正则 `packages\/design-tokens\/(?:src|dist|build)\//` 漏检,零运行时接线守卫 会放行真实消费者。修法:新增 relativeSpecifierHitsDesignTokens——提取 from/import()/require()/import.meta.resolve() 语境下的相对说明符,按被 扫描文件的 repo 相对位置 resolve,落进 packages/design-tokens/ 即命中; 任意 ../ 深度、兄弟包与 apps 侧路径全覆盖。findRuntimeImportsOfDesignTokens 双通道检测,并补正反两组自证伪用例(全仓扫描零误报保持)。 ② HSL 百分比分量应为 0–1(不成立):DTCG Color 模块 Color Space 表原文 是 hsl hue [0,360) / saturation / lightness [0,100]——s/l 用 0–100 不是 0–1,规范自己的白色示例就是 components: ["none", 0, 100]。Terrazzo 2.7.1 实测:[60, 12.5, 97] 产出 hsl(60 12.5% 97%)(正确),0–1 写法产出 hsl(60 0.125% 0.97%)(几乎全黑)。生成物不改。采纳意见的合理内核: 校验器现在按色彩空间检查分量合法范围(srgb 通道 [0,1]、hsl hue [0,360) 360 非法、s/l [0,100]、alpha [0,1]),把正确范围锁进守卫防止反向犯错, 并补越界反证用例。 验证:pnpm --filter @cindy/design-tokens typecheck 通过;22 个单测全绿 (新增 3 个);生成物未变(本轮零改动 rebase 在 77db609e2 之上)。 Signed-off-by: Kirozeng --- .../src/__tests__/value-consistency.test.ts | 54 +++++++++++++ .../src/__tests__/zero-wiring.test.ts | 72 +++++++++++++++++ packages/design-tokens/src/guards.ts | 77 ++++++++++++++++++- 3 files changed, 199 insertions(+), 4 deletions(-) diff --git a/packages/design-tokens/src/__tests__/value-consistency.test.ts b/packages/design-tokens/src/__tests__/value-consistency.test.ts index 2aa94d3304a..3f09bd6d465 100644 --- a/packages/design-tokens/src/__tests__/value-consistency.test.ts +++ b/packages/design-tokens/src/__tests__/value-consistency.test.ts @@ -79,4 +79,58 @@ describe('DS-3 · 逐值一致守卫', () => { issues.some((issue) => issue.code === 'invalid-syntax'), ).toBe(true); }); + + it('自证伪:分量超出色彩空间合法范围被拒(hsl s/l 是 0–100,srgb 是 0–1)', () => { + // DTCG Color 模块的 Color Space 表:hsl hue [0,360)、saturation/lightness + // [0,100];srgb 三通道 [0,1];alpha [0,1]。曾有意见把 hsl 误读成 0–1 + // (会把 12.5% 写成 0.125——Terrazzo 实测渲染成 hsl(60 0.125% 0.97%), + // 几乎全黑)。0.125 在 0–100 内语法合法但颜色错误,机器分不出语义, + // 这里锁的是真正的越界形态: + const srgbOverflow: DtcgFile = { + $description: 'error fixture: srgb channel overflow', + bad: { + $type: 'color', + $value: { colorSpace: 'srgb', components: [1.2, 0, 0] }, + }, + }; + expect( + validateDtcgSyntax(srgbOverflow, 'srgb-overflow').some( + (issue) => issue.code === 'invalid-syntax', + ), + ).toBe(true); + + const hue360: DtcgFile = { + $description: 'error fixture: hue must be [0,360) — 360 itself is illegal', + bad: { + $type: 'color', + $value: { colorSpace: 'hsl', components: [360, 0, 0] }, + }, + }; + expect( + validateDtcgSyntax(hue360, 'hue-360').some( + (issue) => issue.code === 'invalid-syntax', + ), + ).toBe(true); + + const lightnessOver100: DtcgFile = { + $description: 'error fixture: hsl lightness beyond 100', + bad: { + $type: 'color', + $value: { colorSpace: 'hsl', components: [60, 12.5, 100.5] }, + }, + }; + expect( + validateDtcgSyntax(lightnessOver100, 'lightness-over-100').some( + (issue) => issue.code === 'invalid-syntax', + ), + ).toBe(true); + + // 对照:合法范围(0–100 的 hsl 分量、0–1 的 srgb 通道)必须放行。 + const legal: DtcgFile = { + $description: 'legal ranges pass', + okHsl: { $type: 'color', $value: { colorSpace: 'hsl', components: [60, 12.5, 97] } }, + okSrgb: { $type: 'color', $value: { colorSpace: 'srgb', components: [0.973, 0.973, 0.965] } }, + }; + expect(validateDtcgSyntax(legal, 'legal-ranges')).toEqual([]); + }); }); diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index 6bef173b8b2..85a2f310551 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'; import { containsRuntimeImportOfDesignTokens, findRuntimeImportsOfDesignTokens, + relativeSpecifierHitsDesignTokens, } from '../guards.ts'; import { findRepoRoot } from '../paths.ts'; @@ -57,6 +58,77 @@ describe('DS-3 · 零接线守卫', () => { } }); + it('自证伪:兄弟 workspace 的相对路径直读必须被命中(review P1 补洞)', () => { + // 旧实现只有 `packages/design-tokens/…` 字面量正则;兄弟包 + // packages/foo/src/a.ts 写 `../../design-tokens/src/…` 时说明符不含 + // `packages/` 段,漏检。现在按被扫描文件位置 resolve 相对说明符判定。 + const cases: Array<[string, string, string]> = [ + [ + '兄弟包 src 下两级上行', + "import { x } from '../../design-tokens/src/snapshot.ts';", + 'packages/foo/src/a.ts', + ], + [ + '兄弟包包根一级上行', + "import { x } from '../design-tokens/src/snapshot.ts';", + 'packages/foo/index.ts', + ], + [ + 'apps 侧四层上行', + "import { x } from '../../../../packages/design-tokens/src/snapshot.ts';", + 'apps/desktop/src/renderer/a.tsx', + ], + [ + 'require 相对路径', + "const dt = require('../../design-tokens/src/snapshot.ts');", + 'packages/foo/src/a.ts', + ], + [ + '动态 import 相对路径', + "const layer = await import('../../design-tokens/src/snapshot.ts');", + 'packages/foo/src/a.ts', + ], + [ + 'export-from 相对路径', + "export { x } from '../../design-tokens/src/snapshot.ts';", + 'packages/foo/src/a.ts', + ], + ]; + for (const [name, source, fileRel] of cases) { + expect(relativeSpecifierHitsDesignTokens(source, fileRel), name).toBe(true); + } + }); + + it('自证伪:指向其它包的相对 import 不误报', () => { + // 注:design-tokens 包自身目录被 findRuntimeImportsOfDesignTokens 整体 + // 跳过,这里用兄弟包互相引用的形态验证不误报。 + const legal: Array<[string, string, string]> = [ + [ + '兄弟包指向另一个兄弟', + "import { x } from '../maker-shared/src/util.ts';", + 'packages/foo/src/a.ts', + ], + [ + '兄弟包 src 内部相对导入', + "import { y } from './classify.ts';", + 'packages/foo/src/a.ts', + ], + [ + 'apps 内部相对导入', + "import { z } from '../themes/colors';", + 'apps/desktop/src/renderer/a.tsx', + ], + [ + '普通字符串不是说明符', + "const hint = '../../design-tokens/src/snapshot.ts';", + 'packages/foo/src/a.ts', + ], + ]; + for (const [name, source, fileRel] of legal) { + expect(relativeSpecifierHitsDesignTokens(source, fileRel), name).toBe(false); + } + }); + it('自证伪:非导入语境(注释 / 普通字符串)不误报', () => { const legal = [ '// TODO(DS-8): wire "@cindy/design-tokens" here when consumers open up.', diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index 6fb2b98d1f3..04efc48ff14 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -1,5 +1,8 @@ import { readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; +import { posix } from 'node:path'; + +const { dirname: posixDirname, join: posixJoin, normalize: posixNormalize } = posix; import { resolvedSemanticValues, type BuiltLayers } from './build-layers.ts'; import { @@ -190,6 +193,28 @@ export interface StructureIssue { const COLOR_SPACES = ['srgb', 'hsl'] as const; +/** + * 各色彩空间分量的 DTCG 合法范围(W3C DTCG Color 模块的 Color Space 表): + * srgb 三通道 [0,1];hsl 的 hue [0,360)(闭开区间——360 非法)、 + * saturation / lightness [0,100]。注意 hsl 不是 0–1(与 srgb 不同), + * alpha 恒为 [0,1]。 + */ +const COLOR_SPACE_RANGES: Record< + (typeof COLOR_SPACES)[number], + Array<{ min: number; max: number; maxInclusive: boolean }> +> = { + srgb: [ + { min: 0, max: 1, maxInclusive: true }, + { min: 0, max: 1, maxInclusive: true }, + { min: 0, max: 1, maxInclusive: true }, + ], + hsl: [ + { min: 0, max: 360, maxInclusive: false }, + { min: 0, max: 100, maxInclusive: true }, + { min: 0, max: 100, maxInclusive: true }, + ], +}; + function isValidDtcgColorObject(value: unknown): value is DtcgColorObject { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; const record = value as Record; @@ -199,9 +224,13 @@ function isValidDtcgColorObject(value: unknown): value is DtcgColorObject { if (!Array.isArray(record.components) || record.components.length !== 3) { return false; } - if (!record.components.every((c) => typeof c === 'number' && Number.isFinite(c))) { - return false; - } + const space = record.colorSpace as (typeof COLOR_SPACES)[number]; + const inRange = record.components.every((c, i) => { + if (typeof c !== 'number' || !Number.isFinite(c)) return false; + const { min, max, maxInclusive } = COLOR_SPACE_RANGES[space][i]; + return c >= min && (maxInclusive ? c <= max : c < max); + }); + if (!inRange) return false; if ( record.alpha != null && (typeof record.alpha !== 'number' || record.alpha < 0 || record.alpha > 1) @@ -405,6 +434,39 @@ const IMPORT_ENTRY_PATTERNS: readonly RegExp[] = [ /packages\/design-tokens\/(?:src|dist|build)\//, ]; +/** + * 从源码文本中提取全部模块说明符('…' / "…" 形式的 import / require 目标), + * 供按被扫描文件位置解析相对路径。只取 import-from / import() / require() + * / import.meta.resolve() 语境下的说明符,不匹配普通字符串字面量。 + */ +const SPECIFIER_CONTEXT_RE = + /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|\bimport\.meta\.resolve\s*\(\s*)['"]([^'"]+)['"]/g; + +/** + * 相对路径直读检测:按被扫描文件的 repo 相对位置解析每个相对说明符, + * 归一化后落在 packages/design-tokens/ 之内即命中。 + * + * 为什么不能只靠 IMPORT_ENTRY_PATTERNS 的 `packages/design-tokens/…` 字面量: + * 兄弟 workspace(如 packages/foo/src/a.ts 写 `../../design-tokens/src/…`) + * 的相对说明符不含 `packages/` 段,正则漏检(review P1 实锤);按文件位置 + * resolve 后无论 `../` 深度、兄弟包还是 apps 侧路径都能正确判定。 + */ +export function relativeSpecifierHitsDesignTokens( + text: string, + fileRel: string, +): boolean { + const dir = posixDirname(fileRel); + for (const match of text.matchAll(SPECIFIER_CONTEXT_RE)) { + const spec = match[1]; + if (!spec.startsWith('.')) continue; + const resolved = posixNormalize(posixJoin(dir, spec)); + if (resolved === 'packages/design-tokens' || resolved.startsWith('packages/design-tokens/')) { + return true; + } + } + return false; +} + /** 一段源码文本是否含任何合法形态的包导入 / 依赖入口(供扫描与自证伪测试共用)。 */ export function containsRuntimeImportOfDesignTokens(text: string): boolean { return IMPORT_ENTRY_PATTERNS.some((pattern) => pattern.test(text)); @@ -432,7 +494,14 @@ export function findRuntimeImportsOfDesignTokens(repoRoot: string): string[] { } if (!/\.(ts|tsx|js|mjs|cjs)$/.test(entry.name)) continue; const text = readFileSync(childAbs, 'utf8'); - if (containsRuntimeImportOfDesignTokens(text)) hits.push(childRel); + // 双通道检测:包 id / 带前缀路径的静态形态 + 按本文件位置解析的相对 + // 说明符(兄弟 workspace 的 `../../design-tokens/src/…` 只有后者能抓到)。 + if ( + containsRuntimeImportOfDesignTokens(text) || + relativeSpecifierHitsDesignTokens(text, childRel) + ) { + hits.push(childRel); + } } }; From fe371617ea85b58e4b293ec945f271c8b5596c54 Mon Sep 17 00:00:00 2001 From: Kirozeng Date: Wed, 2 Sep 2026 19:05:11 +0800 Subject: [PATCH 05/15] =?UTF-8?q?fix(design-tokens):=20=E7=9B=B8=E5=AF=B9?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E7=AC=A6=E6=8F=90=E5=8F=96=E8=A1=A5=E5=89=AF?= =?UTF-8?q?=E4=BD=9C=E7=94=A8=20import=20=E8=AF=AD=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review P2(3913379862)跟进:兄弟 workspace 写 `import '../../design-tokens/src/generate.ts'`(副作用导入,无 from 子句 无括号)时,SPECIFIER_CONTEXT_RE 四种语境都命不中,相对路径解析通道 漏检,零接线守卫会放行。 - 补 `import '…'` 分支:用 `(?=['"])` 前瞻定位引号(分支自身不吞引号, 引号由共享后继统一消费,`\s*` 覆盖换行形态),实测单行 / 换行 / 双引号 形态全部命中。 - 顺带修既有误报:import() / require() 分支此前无 `(? --- .../src/__tests__/zero-wiring.test.ts | 20 +++++++++++++++++++ packages/design-tokens/src/guards.ts | 10 +++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index 85a2f310551..95a800f48c5 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -93,6 +93,16 @@ describe('DS-3 · 零接线守卫', () => { "export { x } from '../../design-tokens/src/snapshot.ts';", 'packages/foo/src/a.ts', ], + [ + '副作用 import 相对路径(review P2 补洞)', + "import '../../design-tokens/src/generate.ts';", + 'packages/foo/src/a.ts', + ], + [ + '副作用 import 相对路径换行', + "import\n '../../design-tokens/src/generate.ts';", + 'packages/foo/src/a.ts', + ], ]; for (const [name, source, fileRel] of cases) { expect(relativeSpecifierHitsDesignTokens(source, fileRel), name).toBe(true); @@ -123,6 +133,16 @@ describe('DS-3 · 零接线守卫', () => { "const hint = '../../design-tokens/src/snapshot.ts';", 'packages/foo/src/a.ts', ], + [ + '成员调用 foo.import 不是 import 语句', + "foo.import('../../design-tokens/src/snapshot.ts');", + 'packages/foo/src/a.ts', + ], + [ + '成员调用 foo.require 不是 require', + "foo.require('../../design-tokens/src/snapshot.ts');", + 'packages/foo/src/a.ts', + ], ]; for (const [name, source, fileRel] of legal) { expect(relativeSpecifierHitsDesignTokens(source, fileRel), name).toBe(false); diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index 04efc48ff14..ec3704ca179 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -436,11 +436,15 @@ const IMPORT_ENTRY_PATTERNS: readonly RegExp[] = [ /** * 从源码文本中提取全部模块说明符('…' / "…" 形式的 import / require 目标), - * 供按被扫描文件位置解析相对路径。只取 import-from / import() / require() - * / import.meta.resolve() 语境下的说明符,不匹配普通字符串字面量。 + * 供按被扫描文件位置解析相对路径。覆盖五种语境:import-from / import() / + * require() / import.meta.resolve() / 裸 `import '…'` 副作用导入(后者无 + * from 子句无括号,分支自身用 `(?=['"])` 前瞻定位引号、不吞引号,引号由 + * 共享后继 `['"]…['"]` 统一消费,`\s*` 覆盖换行形态)。 + * `(? Date: Wed, 2 Sep 2026 19:24:45 +0800 Subject: [PATCH 06/15] =?UTF-8?q?fix(design-tokens):=20=E9=9B=B6=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=E5=AE=88=E5=8D=AB=E5=89=A5=E7=A6=BB=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E4=B8=8E=E5=AD=97=E7=AC=A6=E4=B8=B2=E9=87=8C=E7=9A=84=E4=BC=AA?= =?UTF-8?q?=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review P1(3913459489)跟进:守卫把源码全文直接喂正则,注释或错误 消息里出现完整 import 语句(如 `// import '@cindy/design-tokens';`)会被 当真实接线,required unit workspace 假红,无运行时接线的改动被 CI 阻断。 实测确认属实(此前『非导入语境不误报』用例能过只是因为缺完整 import 语法)。 - 新增 stripCommentsAndDataStrings:轻量状态机剥除行注释 / 块注释(替换 为等长空白,行列结构不变);字符串字面量按语境二分——开引号前以 from/import(/require(/import.meta.resolve(/import 语境结尾的是模块 说明符,保留原文;否则是数据字符串,内容替换为空白(引号保留)。 语境判定只看输出末尾 64 字符,避免全文件正则回溯。 - containsRuntimeImportOfDesignTokens / relativeSpecifierHitsDesignTokens 改两段式:先跑廉价正则,零命中(绝大多数文件)不付剥除成本;命中后 剥除复核,命中若来自注释/文案则判非真实接线。全仓扫描 3.8s,超时余量 提到 60s(Windows 慢盘)。 - 正则字面量不识别:只在『漏放』方向偏差(正则内容被清空不命中),真实 import 语句不会写在正则里,可接受——如实注释声明。 - 反证用例:注释(单行/块/JSDoc)/ 数据字符串 / 报错文案里的完整 import 语句全部不误报(两个通道都验);剥除后真实 import 全形态(含前面有 注释或数据字符串的)仍然命中;stripper 语义四组直接断言。 验证:pnpm --filter @cindy/design-tokens typecheck 通过;25 单测全绿 (zero-wiring 新增 3 个用例 17 个断言形态);全仓扫描仍零命中。 Signed-off-by: Kirozeng --- .../src/__tests__/zero-wiring.test.ts | 54 +++++++++++- packages/design-tokens/src/guards.ts | 88 ++++++++++++++++++- 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index 95a800f48c5..a68ae642248 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -5,6 +5,7 @@ import { containsRuntimeImportOfDesignTokens, findRuntimeImportsOfDesignTokens, relativeSpecifierHitsDesignTokens, + stripCommentsAndDataStrings, } from '../guards.ts'; import { findRepoRoot } from '../paths.ts'; @@ -13,7 +14,7 @@ describe('DS-3 · 零接线守卫', () => { it('不被 desktop / mobile / 任何运行时 package import', () => { expect(findRuntimeImportsOfDesignTokens(repoRoot)).toEqual([]); - }); + }, 60_000); // 全仓扫描(数千文件 × 剥除层复核),Windows CI 慢盘需余量 it('本包 package.json 不被任何 workspace 声明为依赖,且不装 token 工具', () => { const pkg = JSON.parse( @@ -159,4 +160,55 @@ describe('DS-3 · 零接线守卫', () => { expect(containsRuntimeImportOfDesignTokens(source)).toBe(false); } }); + + it('自证伪:注释 / 错误消息里的完整 import 语句不误报(review P1 补洞)', () => { + // 旧实现把源码全文直接喂给正则,注释掉的完整 import 语句(语法齐全、 + // 只是语境是注释)会被当真实接线,无运行时接线的改动被 CI 阻断。 + const legal = [ + "// import '@cindy/design-tokens';", + "/* import '@cindy/design-tokens' */", + "/**\n * import '@cindy/design-tokens'\n */", + "// const l = await import('@cindy/design-tokens');", + "// import { x } from '@cindy/design-tokens';", + "const s = \"import '@cindy/design-tokens';\";", + "throw new Error(\"do not import '@cindy/design-tokens' here\");", + "// import '../../design-tokens/src/snapshot.ts';", + ]; + for (const source of legal) { + expect(containsRuntimeImportOfDesignTokens(source), source).toBe(false); + expect( + relativeSpecifierHitsDesignTokens(source, 'packages/foo/src/a.ts'), + source, + ).toBe(false); + } + }); + + it('自证伪:注释剥除后真实 import 语句仍然命中', () => { + // 剥除层不能反向吞掉真实接线:代码区(非注释非字符串)的 import 全形态 + // 必须照常命中。 + const illegal = [ + "import '@cindy/design-tokens';", + "import { x } from '@cindy/design-tokens';", + "const l = await import('@cindy/design-tokens');", + "const x = require('@cindy/design-tokens');", + "// 前面有注释没关系\nimport { x } from '@cindy/design-tokens';", + "const note = 'a data string';\nimport { x } from '@cindy/design-tokens';", + ]; + for (const source of illegal) { + expect(containsRuntimeImportOfDesignTokens(source), source).toBe(true); + } + }); + + it('自证伪:stripCommentsAndDataStrings 保留说明符、剥除数据内容', () => { + expect(stripCommentsAndDataStrings("import { x } from '../../a.ts';")).toBe( + "import { x } from '../../a.ts';", + ); + expect(stripCommentsAndDataStrings("const s = 'some data';")).toBe( + "const s = ' ';", + ); + expect(stripCommentsAndDataStrings("// a comment")).toBe(" "); + expect( + stripCommentsAndDataStrings("const s = 'x'; import { y } from './b';"), + ).toBe("const s = ' '; import { y } from './b';"); + }); }); diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index ec3704ca179..11ac94f3cf9 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -446,6 +446,75 @@ const IMPORT_ENTRY_PATTERNS: readonly RegExp[] = [ const SPECIFIER_CONTEXT_RE = /(?:\bfrom\s*|(? pattern.test(text)); + // 两段式:先跑廉价正则,零命中的文件(绝大多数)完全不付剥除成本; + // 命中后再剥注释与数据字符串复核——命中若是注释/报错文案里的完整 + // import 语句(语法齐全但语境非法),剥除后不再命中,不算真实接线。 + if (!IMPORT_ENTRY_PATTERNS.some((pattern) => pattern.test(text))) { + return false; + } + const codeOnly = stripCommentsAndDataStrings(text); + return IMPORT_ENTRY_PATTERNS.some((pattern) => pattern.test(codeOnly)); } export function findRuntimeImportsOfDesignTokens(repoRoot: string): string[] { From 8f481c0ae826dee0a066f900dd3e7aeeb5d65170 Mon Sep 17 00:00:00 2001 From: Kirozeng Date: Wed, 2 Sep 2026 19:37:27 +0800 Subject: [PATCH 07/15] =?UTF-8?q?fix(design-tokens):=20=E9=9B=B6=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=E5=AE=88=E5=8D=AB=E5=89=A5=E9=99=A4=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=E5=89=8D=E7=BD=AE=EF=BC=8C=E4=BF=AE=E5=A4=8D=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E9=9A=94=E6=96=AD=E7=9A=84=E5=90=88=E6=B3=95=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E6=BC=8F=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review P2(3913603623)跟进:`import(/* webpackChunkName: "tokens" */ '../../design-tokens/src/snapshot.ts')` 这类说明符前带合法注释的导入,两个 检测通道都漏检。根因是上一轮两段式的顺序缺陷:廉价正则预扫跑在原始文本 上,注释隔断了关键字→引号衔接,预扫零命中提前返回,剥除层(本可剥掉注释 恢复衔接)反而执行不到——防误报的层顺序自己制造了漏放。 - 剥除前置:containsRuntimeImportOfDesignTokens 与 relativeSpecifierHitsDesignTokens 都改为先剥注释/数据字符串、再跑正则。 注释的两个方向(伪命中 / 遮蔽真命中)都由剥除层统一解决。 - 性能:stripCommentsAndDataStrings 重写为 chunked 状态机——普通文本按 区间整段拷贝(不再逐字符拼接),语境判定从后往前拼接 chunk 凑 64 字符 窗口;7.7MB 真实包源码 59ms(125MB/s,较逐字符版 5.9MB/s 提速 21 倍)。 另加 import/require/from 关键字 includes 预检,无语法痕迹的文件零成本。 全仓扫描 1.24s(较上轮 3.8s 基线更快)。 - 反证用例:动态 import / require / 副作用 import 带注释的相对路径走 rel 通道、包 id 带注释走 id 通道,各自命中;注释/数据字符串误报形态 回归保持全绿。 验证:pnpm --filter @cindy/design-tokens typecheck 通过;26 单测全绿; 全仓扫描仍零命中零误报。 Signed-off-by: Kirozeng --- .../src/__tests__/zero-wiring.test.ts | 44 +++++++++++++ packages/design-tokens/src/guards.ts | 66 +++++++++++++------ 2 files changed, 90 insertions(+), 20 deletions(-) diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index a68ae642248..563a72bfb54 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -211,4 +211,48 @@ describe('DS-3 · 零接线守卫', () => { stripCommentsAndDataStrings("const s = 'x'; import { y } from './b';"), ).toBe("const s = ' '; import { y } from './b';"); }); + + it('自证伪:说明符前带注释的合法导入必须被命中(review P2 补洞)', () => { + // 旧的两段式先在原始文本上预扫:`import(/* c */ '…')` 的注释隔断了 + // 关键字→引号衔接,预扫零命中提前返回,剥除层反而执行不到——漏放。 + // 现在剥除在前(注释→空白替换恢复衔接)。相对说明符走 rel 通道, + // 包 id 说明符走 id 通道,各按各的管辖断言。 + const relativeCases: Array<[string, string]> = [ + [ + '动态 import 带注释(相对路径)', + "const l = await import(/* webpackChunkName: 'tokens' */ '../../design-tokens/src/snapshot.ts');", + ], + [ + 'require 带注释(相对路径)', + "const x = require(/* chunk */ '../../design-tokens/src/snapshot.ts');", + ], + [ + '副作用 import 关键字后带注释(相对路径)', + "import /* c */ '../../design-tokens/src/generate.ts';", + ], + ]; + for (const [name, source] of relativeCases) { + expect( + relativeSpecifierHitsDesignTokens(source, 'packages/foo/src/a.ts'), + name, + ).toBe(true); + } + const idCases: Array<[string, string]> = [ + [ + '包 id 动态 import 带注释', + "const l = await import(/* x */ '@cindy/design-tokens');", + ], + [ + '包 id 静态 import 带注释', + "import { x } /* load tokens */ from '@cindy/design-tokens';", + ], + [ + '包 id require 带注释', + "const x = require(/* tokens */ '@cindy/design-tokens');", + ], + ]; + for (const [name, source] of idCases) { + expect(containsRuntimeImportOfDesignTokens(source), name).toBe(true); + } + }); }); diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index 11ac94f3cf9..ca88e4e1325 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -472,27 +472,36 @@ const SPECIFIER_PREFIX_RE = * 写在正则字面量里,可接受。 */ export function stripCommentsAndDataStrings(source: string): string { - let out = ''; + const chunks: string[] = []; let i = 0; const n = source.length; + let plainStart = 0; + const flushPlain = (end: number) => { + if (end > plainStart) chunks.push(source.slice(plainStart, end)); + }; while (i < n) { const c = source[i]; const c2 = source[i + 1]; if (c === '/' && c2 === '/') { + flushPlain(i); const start = i; while (i < n && source[i] !== '\n') i++; - out += ' '.repeat(i - start); + chunks.push(' '.repeat(i - start)); + plainStart = i; continue; } if (c === '/' && c2 === '*') { + flushPlain(i); const start = i; i += 2; while (i < n && !(source[i] === '*' && source[i + 1] === '/')) i++; i = Math.min(i + 2, n); - out += ' '.repeat(i - start); + chunks.push(' '.repeat(i - start)); + plainStart = i; continue; } if (c === "'" || c === '"' || c === '`') { + flushPlain(i); const quote = c; const open = i; i++; @@ -502,17 +511,26 @@ export function stripCommentsAndDataStrings(source: string): string { } i = Math.min(i + 1, n); const str = source.slice(open, i); - if (SPECIFIER_PREFIX_RE.test(out.slice(-64))) { - out += str; + // 语境判定看已输出内容的末尾(可能跨多个 chunk:如 import( + 剥除的 + // 注释空白),从后往前拼接 chunk 凑 64 字符窗口。 + let tail = ''; + for (let k = chunks.length - 1; k >= 0 && tail.length < 64; k--) { + const need = 64 - tail.length; + const c = chunks[k]; + tail = (c.length > need ? c.slice(c.length - need) : c) + tail; + } + if (SPECIFIER_PREFIX_RE.test(tail)) { + chunks.push(str); } else { - out += quote + ' '.repeat(Math.max(0, str.length - 2)) + quote; + chunks.push(quote + ' '.repeat(Math.max(0, str.length - 2)) + quote); } + plainStart = i; continue; } - out += c; i++; } - return out; + flushPlain(n); + return chunks.join(''); } /** @@ -529,15 +547,17 @@ export function relativeSpecifierHitsDesignTokens( fileRel: string, ): boolean { const dir = posixDirname(fileRel); - // 同 containsRuntimeImportOfDesignTokens 的两段式:零命中时不付剥除成本。 - let anyRelative = false; - for (const match of text.matchAll(SPECIFIER_CONTEXT_RE)) { - if (match[1].startsWith('.')) { - anyRelative = true; - break; - } + // 剥除必须在提取之前:`import(/* c */ '…')` 这类合法注释隔断会让 + // 关键字→引号的正则衔接在原始文本上断开(review P2 实锤),预扫直接 + // 零命中、剥除层反而执行不到。剥除层按注释→空白替换,衔接恢复。 + // 关键字 includes 预检同 containsRuntimeImportOfDesignTokens。 + if ( + !text.includes('import') && + !text.includes('require') && + !text.includes('from') + ) { + return false; } - if (!anyRelative) return false; const codeOnly = stripCommentsAndDataStrings(text); for (const match of codeOnly.matchAll(SPECIFIER_CONTEXT_RE)) { const spec = match[1]; @@ -552,10 +572,16 @@ export function relativeSpecifierHitsDesignTokens( /** 一段源码文本是否含任何合法形态的包导入 / 依赖入口(供扫描与自证伪测试共用)。 */ export function containsRuntimeImportOfDesignTokens(text: string): boolean { - // 两段式:先跑廉价正则,零命中的文件(绝大多数)完全不付剥除成本; - // 命中后再剥注释与数据字符串复核——命中若是注释/报错文案里的完整 - // import 语句(语法齐全但语境非法),剥除后不再命中,不算真实接线。 - if (!IMPORT_ENTRY_PATTERNS.some((pattern) => pattern.test(text))) { + // 剥除必须在检测之前(同 relativeSpecifierHitsDesignTokens 的理由): + // 注释既会制造伪命中(注释掉的 import 语句),也会遮蔽真命中 + // (import(/* c */ '…') 的注释隔断)——两个方向都由剥除层统一解决。 + // 关键字 includes 预检让不含 import 语法痕迹的文件(多数资源型源码) + // 完全跳过剥除成本;剥除层只在确有 import/require/from 字样时启动。 + if ( + !text.includes('import') && + !text.includes('require') && + !text.includes('from') + ) { return false; } const codeOnly = stripCommentsAndDataStrings(text); From d8fbb5336815644d6cfb7efa1b42d7ac4071b906 Mon Sep 17 00:00:00 2001 From: Kirozeng Date: Wed, 2 Sep 2026 19:49:27 +0800 Subject: [PATCH 08/15] =?UTF-8?q?fix(design-tokens):=20=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E8=AF=AD=E5=A2=83=E5=88=A4=E5=AE=9A=E5=8E=BB=E9=95=BF=E5=BA=A6?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=EF=BC=8Cmember-require=20=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E8=A2=AB=E5=90=8E=E9=A1=BE=E6=8E=92=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review 两条 P1 跟进: ① 固定 64 字符窗口判语境(3913687085,成立):>64 字符的块注释剥成 空白后把 import( 挤出 tail 窗口,说明符被当数据字符串清空,两通道漏放。 修法采纳「状态机直接保留导入语境」:维护 lastCodeContext(无空白压缩的 最近输出,注释与空白不推进),判定时 trimEnd 后匹配——任意长度注释 都不再影响语境,无长度上限。实测 100/300 字符注释形态全命中。 ② module.require 被负向后顾排除(3913687096,成立):`module.require()` 是 Node 真实模块加载 API,`(? --- .../src/__tests__/zero-wiring.test.ts | 58 ++++++++++++++--- packages/design-tokens/src/guards.ts | 64 +++++++++++-------- 2 files changed, 84 insertions(+), 38 deletions(-) diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index 563a72bfb54..d4b728a72b0 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -134,16 +134,9 @@ describe('DS-3 · 零接线守卫', () => { "const hint = '../../design-tokens/src/snapshot.ts';", 'packages/foo/src/a.ts', ], - [ - '成员调用 foo.import 不是 import 语句', - "foo.import('../../design-tokens/src/snapshot.ts');", - 'packages/foo/src/a.ts', - ], - [ - '成员调用 foo.require 不是 require', - "foo.require('../../design-tokens/src/snapshot.ts');", - 'packages/foo/src/a.ts', - ], + // 注:`.` 前缀的成员调用(foo.import / foo.require / module.require) + // 自 2320ce02 轮起**有意命中**——module.require 是 Node 真实加载 API, + // 自定义成员调用宁误报不漏放(见 module.require 用例的说明)。 ]; for (const [name, source, fileRel] of legal) { expect(relativeSpecifierHitsDesignTokens(source, fileRel), name).toBe(false); @@ -212,6 +205,51 @@ describe('DS-3 · 零接线守卫', () => { ).toBe("const s = ' '; import { y } from './b';"); }); + it('自证伪:超长注释不挤掉导入语境(review P1 补洞)', () => { + // 旧实现用固定 64 字符 tail 窗口判语境:>64 字符的块注释被剥成空白后 + // 把 import( 挤出窗口,说明符被当数据字符串清空——两个通道都漏放。 + // 现在状态机维护「无空白压缩的最近输出」,注释不推进语境,任意长度 + // 注释都不影响判定。 + const c100 = 'x'.repeat(100); + const c300 = '很长的注释内容。'.repeat(30); + expect( + containsRuntimeImportOfDesignTokens( + `import(/* ${c100} */ '@cindy/design-tokens')`, + ), + ).toBe(true); + expect( + relativeSpecifierHitsDesignTokens( + `const l = await import(/* ${c300} */ '../../design-tokens/src/snapshot.ts');`, + 'packages/foo/src/a.ts', + ), + ).toBe(true); + }); + + it('自证伪:module.require 是真实加载入口,必须被命中(review P1 补洞)', () => { + // 旧实现的 `(? { // 旧的两段式先在原始文本上预扫:`import(/* c */ '…')` 的注释隔断了 // 关键字→引号衔接,预扫零命中提前返回,剥除层反而执行不到——漏放。 diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index ca88e4e1325..d083775053f 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -420,35 +420,41 @@ function dependencyHits(pkgJson: Record, rel: string): string[] * (`\s` 覆盖跨行变体,不要求以 import 开头,避免漏掉 export…from); * - import '' 副作用导入; * - import('') 动态导入(可跨行); - * - require('') 与 TS import-equals(import x = require('')); + * - require('') 与 TS import-equals(import x = require(''))—— + * 含 `module.require('')` 成员链形态(Node 真实加载 API); * - import.meta.resolve('') 运行期解析入口; * - 绕过包 id、以相对路径直读包内源码 / 产物。 - * `(?64 字符的 webpack 注释)都不会把 import( + * 挤出语境(review P1 实锤:固定 64 字符窗口会让长注释后的说明符被 + * 误当数据清空)。 * - 模板字符串内的 `${…}` 不再细分——数据语境整体剥除,说明符语境 * (不会出现插值)整体保留,两个方向都不产生误报。 * - 正则字面量(`/…/`)不识别:其内容若含引号会被当字符串处理。这只在 @@ -476,8 +486,14 @@ export function stripCommentsAndDataStrings(source: string): string { let i = 0; const n = source.length; let plainStart = 0; + let lastCodeContext = ''; const flushPlain = (end: number) => { - if (end > plainStart) chunks.push(source.slice(plainStart, end)); + if (end > plainStart) { + const text = source.slice(plainStart, end); + chunks.push(text); + // 压缩空白后并入语境:任意长空白折叠为一个空格,语境不设上限。 + lastCodeContext = (lastCodeContext + text.replace(/\s+/g, ' ')).slice(-256); + } }; while (i < n) { const c = source[i]; @@ -511,15 +527,7 @@ export function stripCommentsAndDataStrings(source: string): string { } i = Math.min(i + 1, n); const str = source.slice(open, i); - // 语境判定看已输出内容的末尾(可能跨多个 chunk:如 import( + 剥除的 - // 注释空白),从后往前拼接 chunk 凑 64 字符窗口。 - let tail = ''; - for (let k = chunks.length - 1; k >= 0 && tail.length < 64; k--) { - const need = 64 - tail.length; - const c = chunks[k]; - tail = (c.length > need ? c.slice(c.length - need) : c) + tail; - } - if (SPECIFIER_PREFIX_RE.test(tail)) { + if (SPECIFIER_PREFIX_RE.test(lastCodeContext.trimEnd())) { chunks.push(str); } else { chunks.push(quote + ' '.repeat(Math.max(0, str.length - 2)) + quote); From ae0e6855cad64e6db91876d5dc60e38ea306424f Mon Sep 17 00:00:00 2001 From: Kirozeng Date: Wed, 2 Sep 2026 20:02:34 +0800 Subject: [PATCH 09/15] =?UTF-8?q?fix(design-tokens):=20=E9=9B=B6=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=E6=89=AB=E6=8F=8F=E7=BA=B3=E5=85=A5=20require.resolve?= =?UTF-8?q?=20=E5=8A=A0=E8=BD=BD=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review P2(3913799524)跟进:产品包通过 `readFileSync(require.resolve('../../design-tokens/src/semantic/color.json'), 'utf8')` 消费影子层时是真实运行时消费者,但剥离层把 require.resolve 的参数当普通 数据字符串清空,包 id 与相对路径两个通道都返回 false——漏放。实测确认 (含 module.require.resolve 成员链)。 - SPECIFIER_CONTEXT_RE / SPECIFIER_PREFIX_RE 补 require.resolve 语境 (`require\s*\.\s*resolve\s*\(` 容忍空格形态);IMPORT_ENTRY_PATTERNS 补 require.resolve( 模块 id 形态。 - 反证用例:readFileSync(require.resolve(…))(包 id / 相对路径)、裸 require.resolve、module.require.resolve 成员链全部命中;注释/数据字符串/ myResolve 标识符回归不误报。 验证:pnpm --filter @cindy/design-tokens typecheck 通过;29 单测全绿; 全仓扫描 1.9s 零命中零误报。 Signed-off-by: Kirozeng --- .../src/__tests__/zero-wiring.test.ts | 40 +++++++++++++++++++ packages/design-tokens/src/guards.ts | 17 +++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index d4b728a72b0..fd4199212c2 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -250,6 +250,46 @@ describe('DS-3 · 零接线守卫', () => { ).toBe(false); }); + it('自证伪:require.resolve 是运行期加载入口,必须被命中(review P2 补洞)', () => { + // `readFileSync(require.resolve('../../design-tokens/…'))` 是消费影子层的 + // 真实路径:旧实现的剥离层把 require.resolve 的参数当普通数据字符串 + // 清空,两个通道都漏放。现在 require.resolve 与 import.meta.resolve + // 同列说明符语境(含 module.require.resolve 成员链与带空格形态)。 + const relCases: Array<[string, string]> = [ + [ + 'readFileSync(require.resolve(相对路径))', + "readFileSync(require.resolve('../../design-tokens/src/semantic/color.json'), 'utf8')", + ], + [ + '裸 require.resolve(相对路径)', + "const p = require.resolve('../../design-tokens/src/snapshot.ts');", + ], + [ + 'module.require.resolve(相对路径)', + "const p = module.require.resolve('../../design-tokens/src/snapshot.ts');", + ], + ]; + for (const [name, source] of relCases) { + expect( + relativeSpecifierHitsDesignTokens(source, 'packages/foo/src/a.ts'), + name, + ).toBe(true); + } + const idCases: Array<[string, string]> = [ + [ + 'readFileSync(require.resolve(包 id))', + "readFileSync(require.resolve('@cindy/design-tokens/semantic/color.json'), 'utf8')", + ], + [ + '裸 require.resolve(包 id)', + "const p = require.resolve('@cindy/design-tokens');", + ], + ]; + for (const [name, source] of idCases) { + expect(containsRuntimeImportOfDesignTokens(source), name).toBe(true); + } + }); + it('自证伪:说明符前带注释的合法导入必须被命中(review P2 补洞)', () => { // 旧的两段式先在原始文本上预扫:`import(/* c */ '…')` 的注释隔断了 // 关键字→引号衔接,预扫零命中提前返回,剥除层反而执行不到——漏放。 diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index d083775053f..ade05f59225 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -422,6 +422,8 @@ function dependencyHits(pkgJson: Record, rel: string): string[] * - import('') 动态导入(可跨行); * - require('') 与 TS import-equals(import x = require(''))—— * 含 `module.require('')` 成员链形态(Node 真实加载 API); + * - require.resolve('') 运行期路径解析(readFileSync(require.resolve(…)) + * 是消费影子层的真实路径); * - import.meta.resolve('') 运行期解析入口; * - 绕过包 id、以相对路径直读包内源码 / 产物。 * `(? Date: Wed, 2 Sep 2026 20:18:09 +0800 Subject: [PATCH 10/15] =?UTF-8?q?fix(design-tokens):=20=E9=9B=B6=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=E6=89=AB=E6=8F=8F=E7=BA=B3=E5=85=A5=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=AD=97=E9=9D=A2=E9=87=8F=E5=BD=A2=E5=BC=8F=E7=9A=84=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E8=AF=B4=E6=98=8E=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review P1(3913883249)跟进:`import(`../../design-tokens/src/snapshot.ts`)`、 `require(`@cindy/design-tokens/src/snapshot.ts`)` 这类无插值模板字面量是 有效运行时加载,但提取正则只认单双引号——包 id 与相对路径两个通道都漏放。 实测确认。 - SPECIFIER_CONTEXT_RE / IMPORT_ENTRY_PATTERNS 的引号字符类从 ['"] 扩到 ['"`],全部六种语境(from / import() / require() / require.resolve / import.meta.resolve / 裸副作用 import)同步。 - 带 ${…} 插值的模板**同样命中**:路径在运行期拼装恰是零接线阶段不该 出现的动态消费形态,按宁误报不漏放处理(与 member-require 同一威胁 模型,如实声明)。 - 数据语境的普通模板不误报:剥离层在非说明符位置把模板内容剥成空白, 既有机制无需改动。 - 反证用例:import/require/require.resolve/from/副作用 import 的模板形态 (包 id + 相对路径 + 插值)全部命中;数据模板回归不误报。 验证:pnpm --filter @cindy/design-tokens typecheck 通过;30 单测全绿; 全仓扫描 2.4s 零命中零误报。 Signed-off-by: Kirozeng --- .../src/__tests__/zero-wiring.test.ts | 33 ++++++++++++++++++ packages/design-tokens/src/guards.ts | 34 +++++++++++-------- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index fd4199212c2..277b6ab014c 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -250,6 +250,39 @@ describe('DS-3 · 零接线守卫', () => { ).toBe(false); }); + it('自证伪:模板字面量形式的说明符必须被命中(review P1 补洞)', () => { + // `import(\`../../design-tokens/src/snapshot.ts\`)` 这类无插值模板字面量 + // 是有效运行时加载形态,旧提取正则只认单双引号——漏放。带 `${…}` 插值 + // 的模板同样命中:路径在运行期拼装恰是零接线阶段不该出现的动态消费。 + const relCases: Array<[string, string]> = [ + ['import(无插值模板,相对路径)', 'import(`../../design-tokens/src/snapshot.ts`)'], + ['动态 import 模板(相对路径)', 'const l = await import(`../../design-tokens/src/snapshot.ts`);'], + ['require.resolve 模板(相对路径)', 'require.resolve(`../../design-tokens/src/snapshot.ts`)'], + ['import(插值模板,相对路径)', 'import(`../../design-tokens/src/${name}.ts`)'], + ]; + for (const [name, source] of relCases) { + expect( + relativeSpecifierHitsDesignTokens(source, 'packages/foo/src/a.ts'), + name, + ).toBe(true); + } + const idCases: Array<[string, string]> = [ + ['import(无插值模板,包 id)', 'import(`@cindy/design-tokens`)'], + ['require(无插值模板,包 id)', 'require(`@cindy/design-tokens/src/snapshot.ts`)'], + ['from 模板(包 id)', 'import { x } from `@cindy/design-tokens`;'], + ['副作用 import 模板(包 id)', 'import `@cindy/design-tokens`;'], + ]; + for (const [name, source] of idCases) { + expect(containsRuntimeImportOfDesignTokens(source), name).toBe(true); + } + // 数据语境的普通模板(非说明符位置)不误报——剥离层已剥内容。 + expect( + containsRuntimeImportOfDesignTokens( + 'const t = `plain template mentioning @cindy/design-tokens`;', + ), + ).toBe(false); + }); + it('自证伪:require.resolve 是运行期加载入口,必须被命中(review P2 补洞)', () => { // `readFileSync(require.resolve('../../design-tokens/…'))` 是消费影子层的 // 真实路径:旧实现的剥离层把 require.resolve 的参数当普通数据字符串 diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index ade05f59225..85a05058e60 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -431,31 +431,35 @@ function dependencyHits(pkgJson: Record, rel: string): string[] * 成员调用宁可误报(守卫拦截的是真接线,member-require 是合法接线形态)。 */ const IMPORT_ENTRY_PATTERNS: readonly RegExp[] = [ - /from\s*['"]@cindy\/design-tokens(?:\/[^'"\s]*)?['"]/, - /(? Date: Wed, 2 Sep 2026 21:22:50 +0800 Subject: [PATCH 11/15] =?UTF-8?q?fix(design-tokens):=20=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E8=B1=81=E5=85=8D=E8=89=B2=E7=99=BB=E8=AE=B0=20exemption=20?= =?UTF-8?q?=E5=85=83=E6=95=B0=E6=8D=AE=EF=BC=8Cfs=20=E8=A3=B8=E7=9B=B8?= =?UTF-8?q?=E5=AF=B9=E8=B7=AF=E5=BE=84=E7=9B=B4=E8=AF=BB=E7=BA=B3=E5=85=A5?= =?UTF-8?q?=E9=9B=B6=E6=8E=A5=E7=BA=BF=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review 两条 P2 跟进: ① 豁免色缺 protected 登记(3914411235,成立):destructive / error-* / warning-* / focus-ring* 已进 semantic 建模,但 DESIGN.md §10 列其为 theme-invariant 语义豁免色、治理合同 §3.2 要求 Tier-3 豁免色作为 protected 角色迁移——分类里却是无元数据的普通 literal,DS-8 生成主题入口时无法区分 可覆写 semantic 与必须保留原值的豁免族。修法:新增 SEMANTIC_EXEMPTION_IDS 与 exemption 元数据(与 PROTECTED_IDS 并列、语义不同——豁免色**照常建模** 只带标记,protected 是「只登记不建模」);ClassificationEntry 加 exemption 字段,11 个已建模豁免色重新生成登记;新守卫 assertSemanticExemptionsRegistered 校验已建模豁免色必须带标记且不与 protected 混用。 ② fs 裸相对路径直读漏检(3914411249,成立): readFileSync('../../design-tokens/src/semantic/color.json') 是最直接的 消费方式,两通道都漏。修法:fs API 路径参数成为第八类说明符语境 (readFileSync/readFile/writeFileSync/existsSync/statSync 等 13 个 API 的 第一参数,含成员链与模板形态),按调用文件位置 resolve。误报核查:第一方 代码既有 fs 读取全部走 resolve(__dirname…) / new URL(…) 包装(54 处实测), 裸相对形态为零——纳入扫描零误报,全仓扫描实证。 验证:pnpm --filter @cindy/design-tokens typecheck 通过;33 单测全绿 (新增豁免色守卫 + fs 检测两组用例);全仓扫描 3.0s 零命中零误报; 生成器两次运行字节一致。 Signed-off-by: Kirozeng --- packages/design-tokens/README.md | 2 + .../src/__tests__/classification.test.ts | 21 +++++ .../src/__tests__/zero-wiring.test.ts | 31 +++++++ .../design-tokens/src/classification.json | 55 +++++++++++++ packages/design-tokens/src/classify.ts | 82 +++++++++++++++++++ packages/design-tokens/src/guards.ts | 68 +++++++++++++-- 6 files changed, 254 insertions(+), 5 deletions(-) diff --git a/packages/design-tokens/README.md b/packages/design-tokens/README.md index 0dd904ecac7..e5f348d8451 100644 --- a/packages/design-tokens/README.md +++ b/packages/design-tokens/README.md @@ -37,6 +37,8 @@ DS-8 接线时无法生成 CSS 变量;裸 triplet 字符串也会被解析成 U2 二级信息色(`text-secondary` / `text-secondary-cross`)、`annotation-accent`、CINDY 皮肤族品牌红(`login-brand-accent` / `login-brand-accent-pressed`)按治理合同 §1.1 标记 **protected**,只登记、不进 semantic 映射。皮肤族其余值在 cindy-light/dark 主题 override 里,不在本快照默认值中。 +语义豁免色(DESIGN.md §10 theme-invariant 族:`destructive` / `error-*` / `warning-*` / `focus-ring*`)与 protected 不同:**照常 semantic 建模**,但在 `classification.json` 携带 `exemption` 元数据(外部主题不可覆盖、跨主题恒定)。DS-8 生成主题入口时据此区分可覆写 semantic 与必须保留原值的豁免族;治理合同 §3.2 要求 Tier-3 豁免色按此迁移。DESIGN.md §10 豁免表其余未建模项(`diff-*` / `login-error-fg` 等)进 shadow 层时再登记。 + ## 多入口投放合同(只写合同,DS-8 才接线) 每个消费者届时只消费自己的子集,由同一份 DTCG 生成,不得再手写第二份数值: diff --git a/packages/design-tokens/src/__tests__/classification.test.ts b/packages/design-tokens/src/__tests__/classification.test.ts index 02c4d8bceec..caf92bf6015 100644 --- a/packages/design-tokens/src/__tests__/classification.test.ts +++ b/packages/design-tokens/src/__tests__/classification.test.ts @@ -6,6 +6,7 @@ import { classifyColor, classifySnapshot, PROTECTED_IDS, + SEMANTIC_EXEMPTION_IDS, stableStringify, type ClassificationCategory, } from '../classify.ts'; @@ -13,6 +14,7 @@ import { buildShadowLayerFiles } from '../generate.ts'; import { assertClassificationCoversSnapshot, assertProtectedNotSemantic, + assertSemanticExemptionsRegistered, } from '../guards.ts'; import { classificationPath, @@ -87,6 +89,25 @@ describe('DS-3 · 分类登记', () => { } }); + it('语义豁免色照常建模并携带 exemption 元数据(review P2 补洞)', () => { + // DESIGN.md §10 theme-invariant 豁免族(destructive / error-* / warning-* / + // focus-ring*)与 PROTECTED_IDS 不同:照常 semantic 建模,但外部主题 + // 不可覆盖。DS-8 生成主题入口时靠 exemption 元数据区分可覆写 semantic + // 与必须保留原值的豁免族——缺标记时生成端无法区分(review P2 实锤)。 + assertSemanticExemptionsRegistered(generated); + for (const id of Object.keys(SEMANTIC_EXEMPTION_IDS)) { + const entry = generated.entries.find((item) => item.id === id); + expect(entry?.modeledAsSemantic, `${id} 应保持 semantic 建模`).toBe(true); + expect(entry?.exemption?.family, `${id} 应带 exemption 元数据`).toBe( + 'semantic-exemption', + ); + expect(entry?.protected, `${id} 不应同时是 protected(那是「只登记不建模」)`).toBeFalsy(); + } + // 反证:普通可覆写 semantic(如 surface)不带豁免标记。 + const surface = generated.entries.find((item) => item.id === 'surface'); + expect(surface?.exemption).toBeUndefined(); + }); + it('独立 oracle:分类类别必须与快照值实际形态语义一致,不能只看 id 后缀', () => { // 独立于被测实现复刻分类语义口径(刻意不复用 classify.ts 的 // classifyValue / isLiteralKind,改用自己的正则判定,分类规则变更时 diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index a3b7dd22c36..d9d80d5d0d2 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -325,6 +325,37 @@ describe('DS-3 · 零接线守卫', () => { ).toBe(false); }); + it('自证伪:fs API 裸相对路径直读必须被命中(review P2 补洞)', () => { + // `readFileSync('../../design-tokens/src/semantic/color.json')` 是最直接的 + // 文件消费方式——旧实现两个通道都漏(说明符语境只有 import/require/ + // resolve/new URL)。第一方代码既有 fs 读取全部走 resolve(__dirname…) + // / new URL(…) 包装(54 处实测),裸相对形态为零——纳入扫描零误报。 + const hitCases: Array<[string, string]> = [ + ['readFileSync 裸相对路径', "readFileSync('../../design-tokens/src/semantic/color.json', 'utf8')"], + ['readFile 异步形态', "readFile('../../design-tokens/src/snapshot.ts', cb)"], + ['existsSync', "existsSync('../../design-tokens/src/semantic/color.json')"], + ['readFileSync 模板形态', "readFileSync(`../../design-tokens/src/semantic/color.json`, 'utf8')"], + ['fs.readFileSync 成员链', "fs.readFileSync('../../design-tokens/src/semantic/color.json', 'utf8')"], + ]; + for (const [name, source] of hitCases) { + expect( + relativeSpecifierHitsDesignTokens(source, 'packages/foo/src/a.ts'), + name, + ).toBe(true); + } + // 绝对路径与裸文件名(cwd 相对)不命中:resolve 后不落进 design-tokens。 + const nonHits: Array<[string, string]> = [ + ['绝对路径', "readFileSync('/tmp/whatever.md', 'utf8')"], + ['裸文件名', "readFileSync('package.json', 'utf8')"], + ]; + for (const [name, source] of nonHits) { + expect( + relativeSpecifierHitsDesignTokens(source, 'packages/foo/src/a.ts'), + name, + ).toBe(false); + } + }); + it('自证伪:require.resolve 是运行期加载入口,必须被命中(review P2 补洞)', () => { // `readFileSync(require.resolve('../../design-tokens/…'))` 是消费影子层的 // 真实路径:旧实现的剥离层把 require.resolve 的参数当普通数据字符串 diff --git a/packages/design-tokens/src/classification.json b/packages/design-tokens/src/classification.json index 78861eafe94..2d1a18e19cf 100644 --- a/packages/design-tokens/src/classification.json +++ b/packages/design-tokens/src/classification.json @@ -415,6 +415,11 @@ "destination": "reference-candidate", "lightKind": "hex", "darkKind": "hex", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { @@ -423,6 +428,11 @@ "destination": "reference-candidate", "lightKind": "hex", "darkKind": "hex", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { @@ -803,6 +813,11 @@ "destination": "reference-candidate", "lightKind": "hsl-triplet", "darkKind": "hsl-triplet", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { @@ -4614,6 +4629,11 @@ "destination": "reference-candidate", "lightKind": "hex", "darkKind": "hex", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { @@ -4622,6 +4642,11 @@ "destination": "reference-candidate", "lightKind": "rgb", "darkKind": "rgb", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { @@ -4714,6 +4739,11 @@ "destination": "reference-candidate", "lightKind": "hex", "darkKind": "hex", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { @@ -4722,6 +4752,11 @@ "destination": "reference-candidate", "lightKind": "rgb", "darkKind": "hex", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { @@ -4730,6 +4765,11 @@ "destination": "reference-candidate", "lightKind": "hex", "darkKind": "hex", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { @@ -4738,6 +4778,11 @@ "destination": "reference-candidate", "lightKind": "hex", "darkKind": "hex", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { @@ -4746,6 +4791,11 @@ "destination": "reference-candidate", "lightKind": "rgb", "darkKind": "rgb", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { @@ -4754,6 +4804,11 @@ "destination": "reference-candidate", "lightKind": "hex", "darkKind": "hex", + "exemption": { + "family": "semantic-exemption", + "owner": "DESIGN.md §10", + "rule": "theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖" + }, "modeledAsSemantic": true }, { diff --git a/packages/design-tokens/src/classify.ts b/packages/design-tokens/src/classify.ts index 4266eea540b..50dc2bdfdb0 100644 --- a/packages/design-tokens/src/classify.ts +++ b/packages/design-tokens/src/classify.ts @@ -22,6 +22,12 @@ export interface ProtectedRule { rule: string; } +export interface ExemptionRule { + family: 'semantic-exemption'; + owner: string; + rule: string; +} + export interface ClassificationEntry { id: string; category: ClassificationCategory; @@ -31,6 +37,8 @@ export interface ClassificationEntry { aliasOf?: { light: string | null; dark: string | null }; hslPairOf?: string; protected?: ProtectedRule; + /** 语义豁免色(DESIGN.md §10 theme-invariant 族):照常 semantic 建模,但带豁免元数据——外部主题不可覆盖,DS-8 生成主题入口时据此与可覆写 semantic 区分。 */ + exemption?: ExemptionRule; modeledAsSemantic: boolean; } @@ -97,6 +105,75 @@ export const PROTECTED_IDS: Readonly> = { }, }; +/** + * 语义豁免色(DESIGN.md §10 Semantic Exemption Colors,theme-invariant): + * 与 PROTECTED_IDS 的区别——豁免色**照常进 semantic 建模**(消费方按语义 + * 角色引用),但外部主题不可覆盖、跨主题恒定。治理合同 §3.2 要求 Tier-3 + * 豁免色作为 semantic 中的 protected 角色迁移;这里用独立的 exemption + * 标记承载(不塞 PROTECTED_IDS——那个的语义是「只登记不建模」,会被 + * assertProtectedNotSemantic 拒进 semantic)。DS-8 生成主题入口时据此 + * 区分「可覆写 semantic」与「必须保留原值的豁免族」。 + * 只登记已进首批 semantic 角色(SEMANTIC_ROLES)的豁免色;DESIGN.md §10 + * 豁免表其余未建模项(diff-*、login-error-fg 等)进 shadow 层时再登记。 + */ +export const SEMANTIC_EXEMPTION_IDS: Readonly> = { + destructive: { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, + 'error-flat': { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, + 'error-bg': { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, + 'error-border': { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, + 'error-fg': { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, + 'error-fg-strong': { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, + 'warning-accent': { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, + 'warning-fg': { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, + 'warning-bg-soft': { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, + 'focus-ring': { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, + 'focus-ring-soft': { + family: 'semantic-exemption', + owner: 'DESIGN.md §10', + rule: 'theme-invariant 语义豁免色(Tier-3 singleton),外部主题不覆盖', + }, +}; + export function parseAliasTarget(value: string | null): string | null { if (value == null) return null; const match = VAR_RE.exec(value.trim()); @@ -137,6 +214,8 @@ export function classifyColor(entry: SnapshotColor): ClassificationEntry { const lightKind = classifyValue(entry.light); const darkKind = classifyValue(entry.dark); const protectedRule = PROTECTED_IDS[entry.id]; + // 豁免色照常建模(category / destination 走正常分支),只附加豁免元数据。 + const exemptionRule = SEMANTIC_EXEMPTION_IDS[entry.id]; if (protectedRule) { return { @@ -165,6 +244,7 @@ export function classifyColor(entry: SnapshotColor): ClassificationEntry { lightKind, darkKind, hslPairOf: entry.id.slice(0, -'-hsl'.length), + ...(exemptionRule ? { exemption: exemptionRule } : {}), modeledAsSemantic: SEMANTIC_ROLE_IDS.has(entry.id), }; } @@ -180,6 +260,7 @@ export function classifyColor(entry: SnapshotColor): ClassificationEntry { light: parseAliasTarget(entry.light), dark: parseAliasTarget(entry.dark), }, + ...(exemptionRule ? { exemption: exemptionRule } : {}), modeledAsSemantic: SEMANTIC_ROLE_IDS.has(entry.id), }; } @@ -191,6 +272,7 @@ export function classifyColor(entry: SnapshotColor): ClassificationEntry { destination: 'reference-candidate', lightKind, darkKind, + ...(exemptionRule ? { exemption: exemptionRule } : {}), modeledAsSemantic: SEMANTIC_ROLE_IDS.has(entry.id), }; } diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index a50f42b56c5..690097e91c5 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -10,6 +10,7 @@ import { type ClassificationCategory, type ClassificationDocument, PROTECTED_IDS, + SEMANTIC_EXEMPTION_IDS, } from './classify.ts'; import { collectLeaves, @@ -119,6 +120,44 @@ export function assertProtectedNotSemantic( } } +/** + * 语义豁免色登记守卫(DESIGN.md §10 theme-invariant 族,治理合同 §3.2): + * 已进 semantic 建模的豁免色必须携带 exemption 元数据——DS-8 生成主题入口 + * 时据此区分「可覆写 semantic」与「必须保留原值的豁免族」。豁免色与 + * PROTECTED_IDS 不同:照常建模,只是带标记。 + */ +export function assertSemanticExemptionsRegistered( + classification: ClassificationDocument, +): void { + for (const [id, rule] of Object.entries(SEMANTIC_EXEMPTION_IDS)) { + const entry = classification.entries.find((item) => item.id === id); + if (!entry) { + throw new Error(`豁免色 ${id} 未出现在分类登记`); + } + if (!SEMANTIC_ROLE_IDS.has(id)) { + throw new Error( + `豁免色 ${id} 未进 semantic 建模——豁免登记的前提是已建模(否则等建模后再登记)`, + ); + } + if (entry.modeledAsSemantic !== true) { + throw new Error(`豁免色 ${id} modeledAsSemantic 应为 true,实际 false`); + } + if (!entry.exemption) { + throw new Error( + `豁免色 ${id} 已建模但分类登记缺少 exemption 元数据(外部主题不覆盖的语义豁免族)`, + ); + } + if (entry.exemption.family !== rule.family) { + throw new Error(`豁免色 ${id} 的 family 标记错误`); + } + if (entry.protected) { + throw new Error( + `豁免色 ${id} 同时带 protected 与 exemption——豁免色照常建模,不属加严保护值(只登记不建模)`, + ); + } + } +} + export function assertSemanticMatchesSnapshot( layers: BuiltLayers, snapshot: ColorDefaultsSnapshot, @@ -464,9 +503,19 @@ const IMPORT_ENTRY_PATTERNS: readonly RegExp[] = [ const SPECIFIER_CONTEXT_RE = /(?:\bfrom\s*|(? Date: Wed, 2 Sep 2026 21:33:37 +0800 Subject: [PATCH 12/15] =?UTF-8?q?fix(design-tokens):=20=E9=9B=B6=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=E6=89=AB=E6=8F=8F=E7=BA=B3=E5=85=A5=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=9E=84=E9=80=A0=E5=99=A8=E4=B8=8E=20Windows=20=E5=8F=8D?= =?UTF-8?q?=E6=96=9C=E6=9D=A0=E8=AF=B4=E6=98=8E=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review 两条 P2 跟进: ① 路径构造器包装漏检(3914597287,成立): `readFileSync(resolve(__dirname, '../../design-tokens/…'))` / `join(…)` 是仓内既有 fs 读取的主流写法(54 处实测),旧实现只认 fs API 第一参数 直接以引号开始——包装形态两通道都漏。修法:resolve/join 调用内的静态 字符串片段(任意参数位置)成为第九类说明符语境,按调用文件位置 resolve (__dirname ≈ 文件所在目录)。剥离层语境判定同步(resolve(/join( 开括号 后未闭括号内的字符串保留)。误报核查:既有 54 处用法全部不指向 design-tokens,Array.join 分隔符字符串不以 '.' 开头天然过滤——全仓扫描 零误报实证。 ② Windows 反斜杠说明符漏检(3914597300,成立): `require('..\\..\\design-tokens\\src\\x')` 在 Windows CJS 合法, 但说明符直接交给 path.posix、反斜杠不被识别为分隔符——resolve 结果不 落进 design-tokens,漏放。修法:说明符统一反斜杠归一为 / 再 resolve (源码字符串里 \\ 是转义单反斜杠;仓内无含反斜杠字符的 POSIX 文件名, 替换是安全近似)。 验证:pnpm --filter @cindy/design-tokens typecheck 通过;35 单测全绿 (新增两组 11 个断言形态);全仓扫描 3.1s 零命中零误报。 Signed-off-by: Kirozeng --- .../src/__tests__/zero-wiring.test.ts | 50 +++++++++++++++++++ packages/design-tokens/src/guards.ts | 40 +++++++++++---- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index d9d80d5d0d2..7e7f46fe055 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -356,6 +356,56 @@ describe('DS-3 · 零接线守卫', () => { } }); + it('自证伪:路径构造器包装形态必须被命中(review P2 补洞)', () => { + // `readFileSync(resolve(__dirname, '../../design-tokens/…'))` 是仓内既有 + // fs 读取的主流写法——旧实现只认 fs API 第一参数直接以引号开始,包装 + // 形态两通道都漏。现在 resolve/join 调用内的静态字符串片段按调用文件 + // 位置 resolve(__dirname ≈ 文件所在目录)。 + const hitCases: Array<[string, string]> = [ + ['resolve(__dirname, rel)', "readFileSync(resolve(__dirname, '../../design-tokens/src/semantic/color.json'), 'utf8')"], + ['join(__dirname, rel)', "readFileSync(join(__dirname, '../../design-tokens/src/snapshot.ts'), 'utf8')"], + ['resolve(变量基座, rel)', "readFileSync(resolve(here, '../../design-tokens/src/x.json'))"], + ]; + for (const [name, source] of hitCases) { + expect( + relativeSpecifierHitsDesignTokens(source, 'packages/foo/src/a.ts'), + name, + ).toBe(true); + } + // 既有 resolve/join 用法(读兄弟 tsx / 组件 / 变量基座)不指向 + // design-tokens,必须零误报;Array.join 的分隔符字符串不以 `.` 开头, + // 天然过滤。 + const nonHits: Array<[string, string]> = [ + ['既有用法:resolve 读兄弟 tsx', "readFileSync(resolve(__dirname, '../CCAgentSessionView.tsx'), 'utf8')"], + ['既有用法:join 读组件', "readFileSync(join(here, '../../PublishDialog.tsx'), 'utf8')"], + ['既有用法:resolve 变量基座', "readFileSync(resolve(skillhubDir, '../../router.tsx'), 'utf8')"], + ['Array.join 分隔符', "arr.join('、')"], + ]; + for (const [name, source] of nonHits) { + expect( + relativeSpecifierHitsDesignTokens(source, 'packages/foo/src/a.ts'), + name, + ).toBe(false); + } + }); + + it('自证伪:Windows 反斜杠说明符必须被命中(review P2 补洞)', () => { + // Windows CJS 允许 require('..\\..\\design-tokens\\src\\x')(源码字符串里 + // `\\` 是转义的单反斜杠)——旧实现直接交给 path.posix,反斜杠不被识别 + // 为分隔符,resolve 结果不落进 design-tokens——漏放。现在说明符统一 + // 反斜杠归一为 / 再 resolve。 + const hitCases: Array<[string, string]> = [ + ['require 反斜杠', "require('..\\\\..\\\\design-tokens\\\\src\\\\snapshot.ts')"], + ['readFileSync 反斜杠', "readFileSync('..\\\\..\\\\design-tokens\\\\src\\\\semantic\\\\color.json', 'utf8')"], + ]; + for (const [name, source] of hitCases) { + expect( + relativeSpecifierHitsDesignTokens(source, 'packages/foo/src/a.ts'), + name, + ).toBe(true); + } + }); + it('自证伪:require.resolve 是运行期加载入口,必须被命中(review P2 补洞)', () => { // `readFileSync(require.resolve('../../design-tokens/…'))` 是消费影子层的 // 真实路径:旧实现的剥离层把 require.resolve 的参数当普通数据字符串 diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index 690097e91c5..87dd5b76ce6 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -506,16 +506,26 @@ const SPECIFIER_CONTEXT_RE = /** * fs API 路径参数语境(第八类):`readFileSync('…'` / `readFile('…'` 等 * 直接文件读取的**第一参数**。相对路径说明符按调用文件位置 resolve 后 - * 落进 packages/design-tokens/ 即命中。第一方代码的既有 fs 读取全部走 - * `resolve(__dirname…)` / `new URL(…)` 包装(实测 54 处),裸相对路径 - * 形态为零——纳入扫描零误报(review P2 实锤:这是最直接的绕过方式)。 + * 落进 packages/design-tokens/ 即命中(review P2 实锤:裸相对路径是最 + * 直接的绕过方式)。 */ const FS_API_PATH_CONTEXT_RE = /(? Date: Wed, 2 Sep 2026 21:50:53 +0800 Subject: [PATCH 13/15] =?UTF-8?q?fix(design-tokens):=20=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=9E=84=E9=80=A0=E5=99=A8=E9=87=8D=E5=BB=BA=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E9=9D=99=E6=80=81=E5=8F=82=E6=95=B0=EF=BC=8CTier-1=20=E5=8F=97?= =?UTF-8?q?=E4=BF=9D=E6=8A=A4=20slot=20=E7=85=A7=E5=B8=B8=E8=AF=AD?= =?UTF-8?q?=E4=B9=89=E5=BB=BA=E6=A8=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review 两条 P2 跟进: ① 路径构造器只捕获第一个字符串参数(3914703193,成立): `join(__dirname, '..', '..', 'design-tokens/…')` 分段拼接形态实际读取 design-tokens,但正则只看第一个 '..' 就返回 false——常见分段写法绕过 守卫。修法:重建调用内**全部**静态字符串参数——PATH_CONSTRUCTOR_CALL_RE 捕获完整调用实参区,按序提取字符串字面量拼成候选路径再按文件位置 resolve(非字符串参数天然缺席,等价于从文件目录起算的相对段)。反证: 分段 join / 多段 resolve / 模板段全命中;既有 54 处用法与 Array.join 分隔符回归零误报。 ② 受保护 Tier-1 文本角色被移出 semantic(3914703199,成立): text-secondary / text-secondary-cross 是 DESIGN.md §10 Tier-1 slot, 治理合同 §3.2 明确「Tier-1 semantic slots → semantic,名称与用途延续」; 却被 PROTECTED_IDS 统一 register-only 分支标成只登记、从 semantic JSON 消失——DS-8 接管生成时无法从新真相源生成这两个核心文本变量。U2 保护 限制的是改值须裁决(never darken unilaterally),不等于禁止迁移。 修法:ProtectedRule 加 mode 字段——Tier-1 slot(text-secondary / text-secondary-cross)semantic-modeled:照常值形态分类 + 强制进 semantic + 保留 protected 元数据;Tier-3 singleton(annotation-accent、 login-brand-*)维持 register-only(治理合同「保留原位,逐项裁决,默认 不动」,本 PR 未裁决它们进 semantic)。守卫 assertProtectedNotSemantic 按 mode 分支校验;SEMANTIC_ROLES 补登记两个 Tier-1 slot。 验证:pnpm --filter @cindy/design-tokens typecheck 通过;35 单测全绿 (oracle 与保护值用例同步 mode 语义;value-consistency 验证 text-secondary 双模式值与冻结快照一致);全仓扫描零命中零误报; 生成器两次运行字节一致。 Signed-off-by: Kirozeng --- packages/design-tokens/README.md | 2 +- .../src/__tests__/classification.test.ts | 25 ++++++-- .../src/__tests__/zero-wiring.test.ts | 12 ++++ .../design-tokens/src/classification.json | 31 ++++++---- packages/design-tokens/src/classify.ts | 55 +++++++++++++++-- packages/design-tokens/src/guards.ts | 61 ++++++++++++++----- .../design-tokens/src/reference/color.json | 16 ++--- packages/design-tokens/src/semantic-roles.ts | 7 ++- .../design-tokens/src/semantic/color.json | 20 ++++++ 9 files changed, 180 insertions(+), 49 deletions(-) diff --git a/packages/design-tokens/README.md b/packages/design-tokens/README.md index e5f348d8451..a2d9d535e68 100644 --- a/packages/design-tokens/README.md +++ b/packages/design-tokens/README.md @@ -35,7 +35,7 @@ hex / rgba / transparent → srgb 分量(0–1)+ 可选 alpha。不用自定 (`"other"` 不是标准 DTCG 类型——Terrazzo 2.7.1 实测会静默丢弃这类 token, DS-8 接线时无法生成 CSS 变量;裸 triplet 字符串也会被解析成黑色)。 -U2 二级信息色(`text-secondary` / `text-secondary-cross`)、`annotation-accent`、CINDY 皮肤族品牌红(`login-brand-accent` / `login-brand-accent-pressed`)按治理合同 §1.1 标记 **protected**,只登记、不进 semantic 映射。皮肤族其余值在 cindy-light/dark 主题 override 里,不在本快照默认值中。 +加严保护值按治理合同 §1.1 标记 **protected**,分两种 mode:Tier-1 slot(U2 二级信息色 `text-secondary` / `text-secondary-cross`)按 §3.2「名称与用途延续」**照常 semantic 建模** + protected 元数据——保护限制的是改值须经裁决,不是禁止迁移;Tier-3 singleton(`annotation-accent`、CINDY 皮肤族品牌红 `login-brand-accent` / `login-brand-accent-pressed`)按「保留原位,逐项裁决,默认不动」只登记、不建模。皮肤族其余值在 cindy-light/dark 主题 override 里,不在本快照默认值中。 语义豁免色(DESIGN.md §10 theme-invariant 族:`destructive` / `error-*` / `warning-*` / `focus-ring*`)与 protected 不同:**照常 semantic 建模**,但在 `classification.json` 携带 `exemption` 元数据(外部主题不可覆盖、跨主题恒定)。DS-8 生成主题入口时据此区分可覆写 semantic 与必须保留原值的豁免族;治理合同 §3.2 要求 Tier-3 豁免色按此迁移。DESIGN.md §10 豁免表其余未建模项(`diff-*` / `login-error-fg` 等)进 shadow 层时再登记。 diff --git a/packages/design-tokens/src/__tests__/classification.test.ts b/packages/design-tokens/src/__tests__/classification.test.ts index caf92bf6015..47b4922ba96 100644 --- a/packages/design-tokens/src/__tests__/classification.test.ts +++ b/packages/design-tokens/src/__tests__/classification.test.ts @@ -79,13 +79,23 @@ describe('DS-3 · 分类登记', () => { } }); - it('加严保护值标记 protected,未进 semantic 映射', () => { + it('加严保护值标记 protected,Tier-1 照常建模、Tier-3 只登记', () => { assertProtectedNotSemantic(generated); for (const id of Object.keys(PROTECTED_IDS)) { const entry = generated.entries.find((item) => item.id === id); - expect(entry?.category).toBe('runtime-derived-or-protected'); - expect(entry?.modeledAsSemantic).toBe(false); expect(entry?.protected).toBeTruthy(); + if (PROTECTED_IDS[id].mode === 'register-only') { + // Tier-3 singleton:只登记、不建模(治理合同 §3.2「保留原位」)。 + expect(entry?.category).toBe('runtime-derived-or-protected'); + expect(entry?.modeledAsSemantic).toBe(false); + } else { + // Tier-1 slot:照常 semantic 建模 + protected 元数据(治理合同 + // §3.2「名称与用途延续」;保护限制改值,不禁止迁移——review P2 实锤: + // 旧统一 register-only 分支让 text-secondary 从 semantic 消失, + // DS-8 无法从新真相源生成它)。 + expect(entry?.modeledAsSemantic).toBe(true); + expect(entry?.category).not.toBe('runtime-derived-or-protected'); + } } }); @@ -112,7 +122,8 @@ describe('DS-3 · 分类登记', () => { // 独立于被测实现复刻分类语义口径(刻意不复用 classify.ts 的 // classifyValue / isLiteralKind,改用自己的正则判定,分类规则变更时 // 必须与本测试同步更新): - // protected 条目 → runtime-derived-or-protected(只看登记表); + // protected 且 register-only(Tier-3)→ runtime-derived-or-protected; + // protected 且 semantic-modeled(Tier-1)→ 按值形态正常分类; // -hsl 后缀且双模式都是 hsl-triplet → hsl-triplet; // 双 alias → alias;双字面量 → literal;其余一律 runtime-derived。 const TRIPLET_VALUE_RE = /^-?\d+(?:\.\d+)?\s+\d+(?:\.\d+)?%\s+\d+(?:\.\d+)?%$/; @@ -140,7 +151,9 @@ describe('DS-3 · 分类登记', () => { const expected = new Map(); for (const color of snapshot.colors) { let category: ClassificationCategory; - if (color.id in PROTECTED_IDS) { + const protectedRule = PROTECTED_IDS[color.id]; + if (protectedRule && protectedRule.mode === 'register-only') { + // Tier-3 singleton:只登记(治理合同 §3.2「保留原位」)。 category = 'runtime-derived-or-protected'; } else if ( color.id.endsWith('-hsl') && @@ -151,6 +164,8 @@ describe('DS-3 · 分类登记', () => { } else if (isAlias(color.light) && isAlias(color.dark)) { category = 'alias'; } else if (isLiteral(color.light) && isLiteral(color.dark)) { + // Tier-1 semantic-modeled 的保护值(text-secondary 等)按值形态 + // 正常分类(literal),不因 protected 标记改变 category。 category = 'literal'; } else { category = 'runtime-derived-or-protected'; diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index 7e7f46fe055..5e872acd0a2 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -365,6 +365,18 @@ describe('DS-3 · 零接线守卫', () => { ['resolve(__dirname, rel)', "readFileSync(resolve(__dirname, '../../design-tokens/src/semantic/color.json'), 'utf8')"], ['join(__dirname, rel)', "readFileSync(join(__dirname, '../../design-tokens/src/snapshot.ts'), 'utf8')"], ['resolve(变量基座, rel)', "readFileSync(resolve(here, '../../design-tokens/src/x.json'))"], + [ + '分段 join(review P2 补洞:重建全部静态参数)', + "readFileSync(join(__dirname, '..', '..', 'design-tokens/src/semantic/color.json'))", + ], + [ + '多段 resolve(每段一个目录)', + "readFileSync(resolve(__dirname, '..', '..', 'design-tokens', 'src', 'semantic', 'color.json'))", + ], + [ + '分段 join 模板段', + "readFileSync(join(here, '..', '..', `design-tokens/src/x.json`))", + ], ]; for (const [name, source] of hitCases) { expect( diff --git a/packages/design-tokens/src/classification.json b/packages/design-tokens/src/classification.json index 2d1a18e19cf..618c223afe8 100644 --- a/packages/design-tokens/src/classification.json +++ b/packages/design-tokens/src/classification.json @@ -3,10 +3,10 @@ "generatedBy": "packages/design-tokens/src/classify.ts", "snapshotCount": 514, "categories": { - "literal": 246, + "literal": 248, "alias": 235, "hsl-triplet": 6, - "runtime-derived-or-protected": 27 + "runtime-derived-or-protected": 25 }, "entries": [ { @@ -272,29 +272,31 @@ }, { "id": "text-secondary", - "category": "runtime-derived-or-protected", - "destination": "register-only", + "category": "literal", + "destination": "reference-candidate", "lightKind": "hex", "darkKind": "hex", "protected": { "family": "u2-secondary-info", "owner": "DESIGN.md §15.5", - "rule": "U2 二级信息色,never darken unilaterally" + "rule": "U2 二级信息色,never darken unilaterally", + "mode": "semantic-modeled" }, - "modeledAsSemantic": false + "modeledAsSemantic": true }, { "id": "text-secondary-cross", - "category": "runtime-derived-or-protected", - "destination": "register-only", + "category": "literal", + "destination": "reference-candidate", "lightKind": "hex", "darkKind": "hex", "protected": { "family": "u2-secondary-info", "owner": "DESIGN.md §15.5", - "rule": "U2 二级信息色,never darken unilaterally" + "rule": "U2 二级信息色,never darken unilaterally", + "mode": "semantic-modeled" }, - "modeledAsSemantic": false + "modeledAsSemantic": true }, { "id": "text-secondary-mid", @@ -1125,7 +1127,8 @@ "protected": { "family": "cindy-skin-family", "owner": "DESIGN.md §15.1", - "rule": "CINDY 皮肤族品牌红,实现期零裁量" + "rule": "CINDY 皮肤族品牌红,实现期零裁量", + "mode": "register-only" }, "modeledAsSemantic": false }, @@ -1138,7 +1141,8 @@ "protected": { "family": "cindy-skin-family", "owner": "DESIGN.md §15.1", - "rule": "CINDY 皮肤族品牌红(pressed),实现期零裁量" + "rule": "CINDY 皮肤族品牌红(pressed),实现期零裁量", + "mode": "register-only" }, "modeledAsSemantic": false }, @@ -2879,7 +2883,8 @@ "protected": { "family": "annotation-accent", "owner": "DESIGN.md §15.4", - "rule": "图片标注烧录墨色,exempt, do not change" + "rule": "图片标注烧录墨色,exempt, do not change", + "mode": "register-only" }, "modeledAsSemantic": false }, diff --git a/packages/design-tokens/src/classify.ts b/packages/design-tokens/src/classify.ts index 50dc2bdfdb0..6de7e329bc6 100644 --- a/packages/design-tokens/src/classify.ts +++ b/packages/design-tokens/src/classify.ts @@ -20,6 +20,15 @@ export interface ProtectedRule { family: 'cindy-skin-family' | 'u2-secondary-info' | 'annotation-accent'; owner: string; rule: string; + /** + * semantic-modeled:Tier-1 semantic slot(治理合同 §3.2「名称与用途延续」) + * ——照常 semantic 建模 + 保留 protected 元数据;保护限制的是改值须经 + * 裁决,不是禁止迁移(review P2 实锤:U2 的 text-secondary 被统一 + * register-only 分支移出 semantic,DS-8 将无法从新真相源生成它)。 + * register-only:Tier-3 singleton(治理合同 §3.2「protected 角色或保留 + * 原位,逐项裁决,默认不动」)——本 PR 未裁决进 semantic,只登记。 + */ + mode: 'semantic-modeled' | 'register-only'; } export interface ExemptionRule { @@ -74,34 +83,45 @@ const NUMBER_RE = /^\d+(?:\.\d+)?$/; /** * 加严保护值:治理合同 §1.1 / DESIGN.md §15。 - * 只登记、不进 semantic 映射。CINDY 皮肤族在默认 ColorRegistry 里 - * 只有登录品牌红两项(其余皮肤值在 cindy-light/dark 主题 override,不在本快照)。 + * 两种 mode(治理合同 §3.2 的 Tier-1/Tier-3 分野): + * - semantic-modeled——Tier-1 slot(text-secondary / text-secondary-cross): + * 照常 semantic 建模 + protected 元数据。U2 保护限制的是改值须裁决, + * 不是禁止迁移;「名称与用途延续」要求 DS-8 能从新真相源生成它们。 + * - register-only——Tier-3 singleton(annotation-accent / login-brand-*): + * 只登记、不建模(治理合同「保留原位,逐项裁决,默认不动」)。 + * CINDY 皮肤族在默认 ColorRegistry 里只有登录品牌红两项(其余皮肤值在 + * cindy-light/dark 主题 override,不在本快照)。 */ export const PROTECTED_IDS: Readonly> = { 'annotation-accent': { family: 'annotation-accent', owner: 'DESIGN.md §15.4', rule: '图片标注烧录墨色,exempt, do not change', + mode: 'register-only', }, 'text-secondary': { family: 'u2-secondary-info', owner: 'DESIGN.md §15.5', rule: 'U2 二级信息色,never darken unilaterally', + mode: 'semantic-modeled', }, 'text-secondary-cross': { family: 'u2-secondary-info', owner: 'DESIGN.md §15.5', rule: 'U2 二级信息色,never darken unilaterally', + mode: 'semantic-modeled', }, 'login-brand-accent': { family: 'cindy-skin-family', owner: 'DESIGN.md §15.1', rule: 'CINDY 皮肤族品牌红,实现期零裁量', + mode: 'register-only', }, 'login-brand-accent-pressed': { family: 'cindy-skin-family', owner: 'DESIGN.md §15.1', rule: 'CINDY 皮肤族品牌红(pressed),实现期零裁量', + mode: 'register-only', }, }; @@ -218,14 +238,39 @@ export function classifyColor(entry: SnapshotColor): ClassificationEntry { const exemptionRule = SEMANTIC_EXEMPTION_IDS[entry.id]; if (protectedRule) { + // register-only(Tier-3 singleton):只登记、不建模,维持既有行为。 + if (protectedRule.mode === 'register-only') { + return { + id: entry.id, + category: 'runtime-derived-or-protected', + destination: 'register-only', + lightKind, + darkKind, + protected: protectedRule, + modeledAsSemantic: false, + }; + } + // semantic-modeled(Tier-1 slot):照常走值形态分类(命名与用途延续), + // 保留 protected 元数据、强制进 semantic——保护限制改值,不禁止迁移。 return { id: entry.id, - category: 'runtime-derived-or-protected', - destination: 'register-only', + category: isLiteralKind(lightKind) && isLiteralKind(darkKind) + ? 'literal' + : lightKind === 'alias' && darkKind === 'alias' + ? 'alias' + : 'runtime-derived-or-protected', + destination: isLiteralKind(lightKind) && isLiteralKind(darkKind) + ? 'reference-candidate' + : lightKind === 'alias' && darkKind === 'alias' + ? 'semantic-or-component-candidate' + : 'register-only', lightKind, darkKind, + aliasOf: lightKind === 'alias' && darkKind === 'alias' + ? { light: parseAliasTarget(entry.light), dark: parseAliasTarget(entry.dark) } + : undefined, protected: protectedRule, - modeledAsSemantic: false, + modeledAsSemantic: true, }; } diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index 87dd5b76ce6..5d6effe5029 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -105,17 +105,35 @@ export function assertProtectedNotSemantic( if (!entry) { throw new Error(`保护值 ${id} 未出现在分类登记`); } - if (entry.category !== 'runtime-derived-or-protected') { - throw new Error(`保护值 ${id} 分类应为 runtime-derived-or-protected,实际 ${entry.category}`); - } - if (entry.modeledAsSemantic) { - throw new Error(`保护值 ${id} 不得进入 semantic 映射`); - } if (entry.protected?.family !== rule.family) { throw new Error(`保护值 ${id} 的 family 标记错误`); } - if (SEMANTIC_ROLE_IDS.has(id)) { - throw new Error(`保护值 ${id} 出现在 SEMANTIC_ROLES`); + if (rule.mode === 'register-only') { + // Tier-3 singleton:只登记、不建模(治理合同 §3.2「保留原位」)。 + if (entry.category !== 'runtime-derived-or-protected') { + throw new Error( + `保护值 ${id} 分类应为 runtime-derived-or-protected,实际 ${entry.category}`, + ); + } + if (entry.modeledAsSemantic) { + throw new Error(`保护值 ${id}(register-only)不得进入 semantic 映射`); + } + if (SEMANTIC_ROLE_IDS.has(id)) { + throw new Error(`保护值 ${id}(register-only)出现在 SEMANTIC_ROLES`); + } + } else { + // Tier-1 slot(semantic-modeled):照常建模 + 保留 protected 元数据 + // (治理合同 §3.2「名称与用途延续」;保护限制改值,不禁止迁移)。 + if (!entry.modeledAsSemantic) { + throw new Error( + `保护值 ${id}(semantic-modeled)必须进入 semantic 映射——DS-8 依赖新真相源生成它`, + ); + } + if (!SEMANTIC_ROLE_IDS.has(id)) { + throw new Error( + `保护值 ${id}(semantic-modeled)未出现在 SEMANTIC_ROLES`, + ); + } } } } @@ -514,14 +532,17 @@ const FS_API_PATH_CONTEXT_RE = /** * 路径构造器语境(第九类):`resolve(__dirname, '…')` / `join(here, '…')` - * 等包装形态——仓内既有 fs 读取的主流写法(54 处实测),静态字符串片段 - * 按调用文件位置 resolve(`__dirname` ≈ 文件所在目录,与文件位置基准 - * 一致)。覆盖调用内的**任意位置**字符串参数(`resolve(a, '…', b)` 的 - * 中段也提取)。非相对片段(`'、'` 这类 join 分隔符)不以 `.` 开头, + * 等包装形态——仓内既有 fs 读取的主流写法(54 处实测)。 + * 重建**全部静态参数**(review P2 实锤:`join(__dirname, '..', '..', + * 'design-tokens/…')` 分段拼接时只看第一个 `'..'` 会漏)——把调用内 + * 按序出现的字符串字面量拼成一个候选路径再按文件位置 resolve;非字符串 + * 参数(`__dirname` / 变量)在拼接中天然缺席(等价于从文件目录起算的 + * 相对段)。非相对拼接结果(如 `'、'` 这类 join 分隔符)不以 `.` 开头, * 天然过滤。 */ -const PATH_CONSTRUCTOR_CONTEXT_RE = - /\b(?:resolve|join)\s*\((?:[^()]*?)['"`]([^'"`]+)['"`]/g; +const PATH_CONSTRUCTOR_CALL_RE = + /\b(?:resolve|join)\s*\(([^()]*)\)/g; +const STATIC_STRING_ARG_RE = /['"`]([^'"`]+)['"`]/g; /** 判断「无空白压缩后的最近输出」是否以 import / fs API / 路径构造器语境结尾。 */ const SPECIFIER_PREFIX_RE = @@ -650,8 +671,16 @@ export function relativeSpecifierHitsDesignTokens( for (const match of codeOnly.matchAll(FS_API_PATH_CONTEXT_RE)) { specs.push(match[1]); } - for (const match of codeOnly.matchAll(PATH_CONSTRUCTOR_CONTEXT_RE)) { - specs.push(match[1]); + // 路径构造器:重建调用内全部静态字符串参数(按序拼接为候选路径)。 + for (const call of codeOnly.matchAll(PATH_CONSTRUCTOR_CALL_RE)) { + const args = call[1]; + const statics: string[] = []; + for (const arg of args.matchAll(STATIC_STRING_ARG_RE)) { + statics.push(arg[1]); + } + if (statics.length > 0) { + specs.push(statics.join('/')); + } } for (const spec of specs) { // 说明符分隔符归一(review P2 实锤):源码字符串里的 `\\` 是转义的 diff --git a/packages/design-tokens/src/reference/color.json b/packages/design-tokens/src/reference/color.json index 67ce4b907a3..273c9c442b0 100644 --- a/packages/design-tokens/src/reference/color.json +++ b/packages/design-tokens/src/reference/color.json @@ -243,25 +243,25 @@ ] } }, - "hex-a3a3a3": { + "hex-737373": { "$type": "color", "$value": { "colorSpace": "srgb", "components": [ - 0.639216, - 0.639216, - 0.639216 + 0.45098, + 0.45098, + 0.45098 ] } }, - "hex-737373": { + "hex-a3a3a3": { "$type": "color", "$value": { "colorSpace": "srgb", "components": [ - 0.45098, - 0.45098, - 0.45098 + 0.639216, + 0.639216, + 0.639216 ] } }, diff --git a/packages/design-tokens/src/semantic-roles.ts b/packages/design-tokens/src/semantic-roles.ts index acdc243798d..0933512c54d 100644 --- a/packages/design-tokens/src/semantic-roles.ts +++ b/packages/design-tokens/src/semantic-roles.ts @@ -1,7 +1,10 @@ /** * DS-3 第一批 semantic 角色:DESIGN.md §10 Tier-1 既有名称 * (surface / border / text / accent)+ status 语义。 - * 不改名、不发明新语义。U2 与 annotation-accent 等保护值不在此列。 + * 不改名、不发明新语义。Tier-1 受保护 slot(U2 二级信息色 text-secondary / + * text-secondary-cross)按治理合同 §3.2「名称与用途延续」照常登记—— + * 保护限制的是改值须经裁决,不是禁止迁移;Tier-3 singleton(annotation-accent、 + * login-brand-*)不在首批,保留原位(分类登记仍标 protected)。 */ export type SemanticGroup = 'surface' | 'border' | 'text' | 'accent' | 'status'; @@ -34,6 +37,8 @@ export const SEMANTIC_ROLES: readonly SemanticRole[] = [ { id: 'text-primary-emphasis', group: 'text' }, { id: 'text-primary-inv', group: 'text' }, { id: 'text-primary-body-strong', group: 'text' }, + { id: 'text-secondary', group: 'text' }, + { id: 'text-secondary-cross', group: 'text' }, { id: 'text-secondary-mid', group: 'text' }, { id: 'text-tertiary', group: 'text' }, { id: 'text-tertiary-stone', group: 'text' }, diff --git a/packages/design-tokens/src/semantic/color.json b/packages/design-tokens/src/semantic/color.json index 8b36910b843..b78528db25a 100644 --- a/packages/design-tokens/src/semantic/color.json +++ b/packages/design-tokens/src/semantic/color.json @@ -215,6 +215,26 @@ "$value": "{hex-d4d4d4}" } }, + "text-secondary": { + "light": { + "$type": "color", + "$value": "{hex-737373}" + }, + "dark": { + "$type": "color", + "$value": "{hex-a3a3a3}" + } + }, + "text-secondary-cross": { + "light": { + "$type": "color", + "$value": "{hex-a3a3a3}" + }, + "dark": { + "$type": "color", + "$value": "{hex-a3a3a3}" + } + }, "text-secondary-mid": { "light": { "$type": "color", From f26072ca3b78e0b34a17f9dc7450e78682054dd1 Mon Sep 17 00:00:00 2001 From: Kirozeng Date: Wed, 2 Sep 2026 22:03:08 +0800 Subject: [PATCH 14/15] =?UTF-8?q?fix(design-tokens):=20=E9=9B=B6=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=E6=89=AB=E6=8F=8F=E7=BA=B3=E5=85=A5=20import.meta.glo?= =?UTF-8?q?b=EF=BC=8C=E8=AF=AD=E5=A2=83=E5=88=A4=E5=AE=9A=E9=94=9A?= =?UTF-8?q?=E5=AE=9A=E5=BC=80=E5=BC=95=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review 两条 P2 跟进: ① import.meta.glob 漏检(3914869131,成立):Vite 的 `import.meta.glob('../../design-tokens/src/**/*.json')` 在构建期把影子层 内容打进产品(仓内 packages/lizi-mcps/src/browser/recipe-loader.ts 有 实际用例),但说明符语境没有 import.meta.glob 分支——两通道漏放。修法: SPECIFIER_CONTEXT_RE / SPECIFIER_PREFIX_RE 补 import.meta.glob( 分支, 相对 glob 模式按文件位置 resolve(glob 元字符不影响前缀段判定)。 ② 语境残留误报(3914869140,成立):`import x from './x'; const hint = 'packages/design-tokens/src/x';` 被误报——SPECIFIER_PREFIX_RE 的 \\bfrom / \\bimport 分支在 2320ce02 轮重写时丢失了 \\$ 锚定,匹配的是 「语境里任何位置有 import 字样」而非「语境以 import 结尾」,数据字符串被 剥除层错误保留、裸路径模式随后误报接线。修法:所有分支共享末尾 \\$ 锚定 ((?:…)$ 整组包裹),保证匹配的是紧邻当前开引号的调用语境。 反证用例:glob(含模板形态与既有 recipes 用法不误报)、残留语境三种形态 不误报;此前全部九类语境 + 注释/数据字符串回归(23 形态)全绿。 验证:pnpm --filter @cindy/design-tokens typecheck 通过;37 单测全绿; 全仓扫描零命中零误报。 Signed-off-by: Kirozeng --- .../src/__tests__/zero-wiring.test.ts | 41 +++++++++++++++++++ packages/design-tokens/src/guards.ts | 13 ++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index 5e872acd0a2..51dde24dd87 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -418,6 +418,47 @@ describe('DS-3 · 零接线守卫', () => { } }); + it('自证伪:import.meta.glob 是构建期加载入口,必须被命中(review P2 补洞)', () => { + // Vite 的 import.meta.glob 在构建期把匹配文件打进产品——仓内 + // packages/lizi-mcps/src/browser/recipe-loader.ts 有实际用例。相对 + // glob 模式按文件位置 resolve(glob 元字符 ** /* 不影响前缀段判定)。 + expect( + relativeSpecifierHitsDesignTokens( + "import.meta.glob('../../design-tokens/src/**/*.json')", + 'packages/foo/src/a.ts', + ), + ).toBe(true); + expect( + relativeSpecifierHitsDesignTokens( + 'const mods = import.meta.glob(`../../design-tokens/src/**`)', + 'packages/foo/src/a.ts', + ), + ).toBe(true); + // 既有用法(glob 不指向 design-tokens)不误报。 + expect( + relativeSpecifierHitsDesignTokens( + "import.meta.glob('../recipes/*.json')", + 'packages/foo/src/a.ts', + ), + ).toBe(false); + }); + + it('自证伪:语境残留不产生误报(review P2 补洞)', () => { + // SPECIFIER_PREFIX_RE 的 \bfrom / \bimport 分支曾无 $ 锚定:源码先出现 + // 正常导入、再写 `const hint = 'packages/design-tokens/src/x'` 时,残留 + // 语境里的 import 字样让数据字符串被剥除层错误保留,裸路径模式随后 + // 误报接线、阻断 required unit workspace。$ 锚定保证只匹配紧邻当前 + // 开引号的调用语境。 + const falsePositiveCases = [ + "import x from './x'; const hint = 'packages/design-tokens/src/x';", + "import x from './x'; log('packages/design-tokens/src/x')", + "const a = 'x'; const hint = 'packages/design-tokens/src/x';", + ]; + for (const source of falsePositiveCases) { + expect(containsRuntimeImportOfDesignTokens(source), source).toBe(false); + } + }); + it('自证伪:require.resolve 是运行期加载入口,必须被命中(review P2 补洞)', () => { // `readFileSync(require.resolve('../../design-tokens/…'))` 是消费影子层的 // 真实路径:旧实现的剥离层把 require.resolve 的参数当普通数据字符串 diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index 5d6effe5029..130764646a9 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -519,7 +519,7 @@ const IMPORT_ENTRY_PATTERNS: readonly RegExp[] = [ * 「有人真接线」,member-require 恰是真实接线的合法形态。 */ const SPECIFIER_CONTEXT_RE = - /(?:\bfrom\s*|(? Date: Wed, 2 Sep 2026 22:20:48 +0800 Subject: [PATCH 15/15] =?UTF-8?q?fix(design-tokens):=20=E9=9B=B6=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=E6=89=AB=E6=8F=8F=E7=BA=B3=E5=85=A5=20fs/promises.ope?= =?UTF-8?q?n=20=E4=B8=8E=20.mts/.cts=20=E6=BA=90=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review 两条 P2 跟进: ① fs/promises.open 漏检(3915016839,成立): `const file = await open('../../design-tokens/src/semantic/color.json'); await file.readFile()` 真实消费影子层,但 open 不在 fs API 列表(openSync 在、异步 open 不在)——路径字符串被剥除,两通道漏放。修法:open 加入 FS_API_PATH_CONTEXT_RE 与 SPECIFIER_PREFIX_RE,预检补 open 字样(裸 open('…') 不含 File/Sync,此前连剥除层都进不去)。误报核查:window.open 的 URL 参数不以 '.' 开头天然过滤;仓内以 '.' 开头字符串参数的 open( 调用 实测为零。 ② .mts/.cts 扫描缺口(3915016844,成立):扫描器扩展名正则只认 ts/tsx/js/mjs/cjs,.mts/.cts(有效 Node/TS 模块扩展,仓内 scripts/ 与 tools/ 已使用)的文件在读取内容前被跳过——相对路径消费且不留 package.json 依赖,两通道都看不见。修法:扩展名正则补 mts|cts,并用 临时 fixture(.mts 与 .cjs 消费者各一)直接验证扫描器行为。 验证:pnpm --filter @cindy/design-tokens typecheck 通过;38 单测全绿; 全仓扫描零命中零误报。 Signed-off-by: Kirozeng --- .../src/__tests__/zero-wiring.test.ts | 53 +++++++++++++++++++ packages/design-tokens/src/guards.ts | 19 ++++--- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/design-tokens/src/__tests__/zero-wiring.test.ts b/packages/design-tokens/src/__tests__/zero-wiring.test.ts index 51dde24dd87..4df372f2d15 100644 --- a/packages/design-tokens/src/__tests__/zero-wiring.test.ts +++ b/packages/design-tokens/src/__tests__/zero-wiring.test.ts @@ -459,6 +459,59 @@ describe('DS-3 · 零接线守卫', () => { } }); + it('自证伪:fs/promises.open 句柄入口与 .mts/.cts 扫描必须被命中(review P2 补洞)', async () => { + // node:fs/promises 的异步 open 返回 FileHandle 再 readFile——路径参数 + // 同样消费影子层;open 不在 fs API 列表时该形态漏放。window.open 的 + // URL 参数不以 `.` 开头,天然过滤(仓内实测零误报)。 + expect( + relativeSpecifierHitsDesignTokens( + "const file = await open('../../design-tokens/src/semantic/color.json'); await file.readFile();", + 'packages/foo/src/a.ts', + ), + ).toBe(true); + expect( + relativeSpecifierHitsDesignTokens( + "const h = await open(`../../design-tokens/src/x.json`, 'r');", + 'packages/foo/src/a.ts', + ), + ).toBe(true); + expect( + relativeSpecifierHitsDesignTokens( + "window.open('https://example.com/path');", + 'packages/foo/src/a.ts', + ), + ).toBe(false); + + // .mts/.cts 是有效 Node/TS 模块扩展(仓内 scripts/ 与 tools/ 已使用), + // 扫描器不认它们时文件在读取内容前被跳过。用临时目录里的 .mts 消费者 + // 直接验证扫描器行为。 + const { mkdtempSync, writeFileSync, rmSync, mkdirSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + const { join: joinPath } = await import('node:path'); + const tmpRoot = mkdtempSync(joinPath(tmpdir(), 'dt-zero-wiring-')); + try { + // 扫描器固定访问 apps/desktop、apps/mobile、packages 三个根——空目录 + // 也要建齐,readdirSync 不容忍缺失。 + for (const root of ['apps/desktop', 'apps/mobile', 'packages/foo/src']) { + mkdirSync(joinPath(tmpRoot, root), { recursive: true }); + } + const pkgDir = joinPath(tmpRoot, 'packages', 'foo', 'src'); + writeFileSync( + joinPath(pkgDir, 'consumer.mts'), + "import { x } from '../../design-tokens/src/snapshot.ts';\n", + ); + writeFileSync( + joinPath(pkgDir, 'consumer.cjs'), + "const x = require('../../design-tokens/src/snapshot.ts');\n", + ); + const hits = findRuntimeImportsOfDesignTokens(tmpRoot); + expect(hits).toContain('packages/foo/src/consumer.mts'); + expect(hits).toContain('packages/foo/src/consumer.cjs'); + } finally { + rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + it('自证伪:require.resolve 是运行期加载入口,必须被命中(review P2 补洞)', () => { // `readFileSync(require.resolve('../../design-tokens/…'))` 是消费影子层的 // 真实路径:旧实现的剥离层把 require.resolve 的参数当普通数据字符串 diff --git a/packages/design-tokens/src/guards.ts b/packages/design-tokens/src/guards.ts index 130764646a9..40e49085f9b 100644 --- a/packages/design-tokens/src/guards.ts +++ b/packages/design-tokens/src/guards.ts @@ -525,10 +525,12 @@ const SPECIFIER_CONTEXT_RE = * fs API 路径参数语境(第八类):`readFileSync('…'` / `readFile('…'` 等 * 直接文件读取的**第一参数**。相对路径说明符按调用文件位置 resolve 后 * 落进 packages/design-tokens/ 即命中(review P2 实锤:裸相对路径是最 - * 直接的绕过方式)。 + * 直接的绕过方式)。含 `node:fs/promises` 的异步 `open('…')`(返回 + * FileHandle 再 readFile——review P2 实锤);`window.open('https://…')` + * 这类 URL 参数不以 `.` 开头,天然过滤,仓内实测零误报。 */ const FS_API_PATH_CONTEXT_RE = - /(?