@qijenchen/design-system 0.1.0-beta.47 → 0.1.0-beta.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ds-canonical/hooks/check_consumer_ds_primitive_misuse.sh +9 -0
- package/ds-canonical/hooks/check_story_invariants.sh +2 -3
- package/ds-canonical/hooks/tests/test_check_story_invariants.sh +18 -3
- package/ds-canonical/references/composition-fidelity.md +27 -81
- package/ds-story-manifest.json +1 -1
- package/package.json +1 -1
- package/src/components/Sidebar/sidebar.stories.tsx +3 -2
|
@@ -72,6 +72,15 @@ if echo "$CONTENT" | grep -qE '<DS\.Empty[^>]+title=' && \
|
|
|
72
72
|
VIOLATIONS="${VIOLATIONS} - <Empty title=...> 無 icon 無 description = 違反 Empty.tsx:11「預設只需 description」minimal mock looks weird\n"
|
|
73
73
|
fi
|
|
74
74
|
|
|
75
|
+
# Pattern 8: 硬寫色值 / 字級 / shadow 繞過 DS token(2026-06-02 CF conformance-model 補主防線 —
|
|
76
|
+
# composition-fidelity 從 pixel-identity 收窄成 identity-opt-in 後,「consumer 用對 DS token」改由靜態
|
|
77
|
+
# conformance 防線保證,對齊 Polaris stylelint-polaris / Atlassian eslint-plugin / Carbon stylelint。
|
|
78
|
+
# 既有 check_layout_space_magic_numbers 守「間距」;此 pattern 補「色值/字級/shadow」缺口。
|
|
79
|
+
# 零誤判優先:只抓 hardcoded(`-[var(--...)]` token 用法不匹配)。
|
|
80
|
+
if echo "$CONTENT" | grep -qE '\b[a-z][a-z-]*-\[(#[0-9a-fA-F]{3,8}|rgb|rgba|hsl|hsla)[(]?|\btext-\[[0-9]|\bshadow-(sm|md|lg|xl|2xl)\b'; then
|
|
81
|
+
VIOLATIONS="${VIOLATIONS} - 硬寫色值/字級/shadow 繞過 DS token(bg-[#hex] / text-[14px] / shadow-md)→ 改 semantic color token / text-body 等 typography token / shadow-[var(--elevation-N)](per ui-development.md「Tailwind 5 條核心」rule 3)\n"
|
|
82
|
+
fi
|
|
83
|
+
|
|
75
84
|
# Pattern 6: Overlay trigger without defaultOpen state for visual demo
|
|
76
85
|
# (Skip in production .tsx; only enforce in .stories.tsx where visual snapshot matters)
|
|
77
86
|
if echo "$FILE" | grep -qE '\.stories\.tsx$'; then
|
|
@@ -534,7 +534,7 @@ rule_story_baseline_reference() {
|
|
|
534
534
|
# `.claude/references/story-baseline-registry.json` antiPatterns regex,逐條 scan 寫入內容。
|
|
535
535
|
# Severity=block → record_worst 2;severity=warn → stderr only。Allowlist
|
|
536
536
|
# `// @story-baseline-allow: <reason>` 整檔豁免。
|
|
537
|
-
#
|
|
537
|
+
# 2026-06-02 升 P0 BLOCKER(block-severity antiPattern):DS + consumer 全掃確認零現有違規後升級。
|
|
538
538
|
|
|
539
539
|
rule_story_archetype_registry() {
|
|
540
540
|
case "$TOOL" in
|
|
@@ -592,8 +592,7 @@ rule_story_archetype_registry() {
|
|
|
592
592
|
echo " 修法:Read baseline story + helpers,抄 production archetype。" >&2
|
|
593
593
|
echo " 或加 \`// @story-baseline-allow: <reason>\` 檔頭豁免(audit-logged)。" >&2
|
|
594
594
|
echo " 詳 .claude/rules/meta-patterns.md M35 + memory/feedback_nearest_same_purpose_canonical.md" >&2
|
|
595
|
-
#
|
|
596
|
-
# Future:zero existing violation → 改 record_worst 2 升 P0 BLOCKER
|
|
595
|
+
record_worst 2 # 2026-06-02 升 P0 BLOCKER:DS + consumer(ds-product-template)全掃確認零現有違規後升級(原 2026-05-20 ship-as-warn 的 TODO 條件達成);只 block-severity antiPattern(warn-severity 仍 stderr-only)
|
|
597
596
|
fi
|
|
598
597
|
done <<< "$PATTERNS"
|
|
599
598
|
done
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
# R5 name_jargon(PostToolUse;reads disk;L<n> layer / canonical / 中英夾雜 jargon)
|
|
11
11
|
# R6 description_jargon(PostToolUse;TS generic in description: → stderr warn)
|
|
12
12
|
# R7 story_baseline_reference(PreToolUse;wrap Sidebar/ChromeHeader/DataTable 無 baseline marker → stderr warn)
|
|
13
|
-
# R8 story_archetype_registry(PreToolUse;讀 .claude/references/story-baseline-registry.json
|
|
13
|
+
# R8 story_archetype_registry(PreToolUse;讀 .claude/references/story-baseline-registry.json;
|
|
14
|
+
# block-severity antiPattern → P0 record_worst 2,2026-06-02 升 P0)
|
|
14
15
|
#
|
|
15
16
|
# Test 重點:silent skip / 各 rule fire / allowlist marker escape。
|
|
16
|
-
# 不測 R3
|
|
17
|
+
# 不測 R3(需 spec.md frontmatter);R8 P0 block-severity 已測(#11,2026-06-02)。
|
|
17
18
|
|
|
18
19
|
set -u
|
|
19
20
|
|
|
@@ -194,12 +195,26 @@ STORIES_APP="/foo/my-project/packages/design-system/src/components/AppShell/app-
|
|
|
194
195
|
run_hook "PreToolUse" "Write" "$STORIES_APP" '
|
|
195
196
|
export const Default = () => (
|
|
196
197
|
<Sidebar>
|
|
197
|
-
<SidebarHeader><
|
|
198
|
+
<SidebarHeader><WorkspaceBrand /></SidebarHeader>
|
|
198
199
|
</Sidebar>
|
|
199
200
|
);
|
|
200
201
|
'
|
|
201
202
|
expect_warn "10. R7 wrap <Sidebar> no @story-baseline → stderr warn" "R7 story_baseline_reference"
|
|
202
203
|
|
|
204
|
+
# 11. R8 archetype registry P0(2026-06-02 升 P0;DS+consumer 零違規確認後升級):
|
|
205
|
+
# wrap <Sidebar> + simplified <SidebarHeader><span> mock(registry block-severity antiPattern)→ BLOCK
|
|
206
|
+
# 有 @story-baseline marker(R7 missing-marker 不 fire)→ exit 2 純由 R8 record_worst 2 造成
|
|
207
|
+
STORIES_R8="/foo/my-project/packages/design-system/src/components/AppShell/app-shell-r8.stories.tsx"
|
|
208
|
+
run_hook "PreToolUse" "Write" "$STORIES_R8" '
|
|
209
|
+
// @story-baseline: @qijenchen/design-system/components/Sidebar/sidebar.stories.tsx#IconCollapse
|
|
210
|
+
export const Default = () => (
|
|
211
|
+
<Sidebar>
|
|
212
|
+
<SidebarHeader><span>Acme</span></SidebarHeader>
|
|
213
|
+
</Sidebar>
|
|
214
|
+
);
|
|
215
|
+
'
|
|
216
|
+
expect_block "11. R8 simplified-mock <SidebarHeader><span> → P0 BLOCK" "R8 story_archetype_registry"
|
|
217
|
+
|
|
203
218
|
echo ""
|
|
204
219
|
echo "=== Summary ==="
|
|
205
220
|
echo "Passed: $PASS / $((PASS + FAIL))"
|
|
@@ -1,101 +1,47 @@
|
|
|
1
|
-
# Composition Fidelity
|
|
1
|
+
# Composition Fidelity(SSOT,2026-05-27 初版 / 2026-06-02 conformance-model 修正)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**目標**:驗證「DS components 在 consumer(ds-product-template / fork)被正確使用、沒違反設計原則 / SSOT token」。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**2026-06-02 模型修正(per CF research + world-class benchmark + 專案 2026-05-27 自身結論)**:
|
|
6
|
+
驗的是「**consumer 有沒有用對 DS(conformance)**」,**不是**「consumer 畫面跟 DS showcase 長得一模一樣(pixel-identity)」。後者對「內容刻意不同的產品範本」是 **false-positive 來源、世界級公認反 pattern**。
|
|
6
7
|
|
|
7
|
-
## 對齊世界級
|
|
8
|
+
## 對齊世界級(2026-06-02 WebFetch verified,修正初版未驗 benchmark)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
- **Material UI X** PR snapshot gate:`mui-x` package consumer apps 用 `@mui/x-data-grid` 必過 visual snapshot
|
|
11
|
-
- **Carbon Design System**(IBM):Percy visual diff cross DS storybook + consumer-app sample
|
|
12
|
-
- **Atlassian Design System**:VR(visual regression)CI on consumer repos pulling DS
|
|
10
|
+
初版宣稱「Polaris/MUI/Carbon/Atlassian 都做 cross DS+consumer pixel diff」= **未驗證 + 不實**(M22/M26 違反)。實證(URL):
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
- **Consumer 用對 DS = 一律靜態 lint,非截圖**:Polaris [`stylelint-polaris`](https://polaris-react.shopify.com/tools/stylelint-polaris)(40+ rule promote DS adoption in consuming apps;「Please use a Polaris color token」)/ Atlassian [`@atlaskit/eslint-plugin-design-system`](https://atlassian.design/components/eslint-plugin-design-system/)(`ensure-design-token-usage` / `prefer-primitives` / `use-tokens-space`,帶 auto-fix)/ Carbon [`stylelint-plugin-carbon-tokens`](https://github.com/carbon-design-system/stylelint-plugin-carbon-tokens)(「enforce Carbon tokens ... rather than hard-coded values」)/ MUI(TS theme 型別 + eslint)。
|
|
13
|
+
- **Visual regression = 同一個 story 跨 commit 比自己的 baseline**(抓 DS 改壞自己),非跨 repo 比不同畫面:[Chromatic](https://www.chromatic.com/docs/branching-and-baselines/)(「baseline = last known good state of the story ... from a previous commit on that same branch」)/ [Storybook](https://storybook.js.org/docs/writing-tests/visual-testing)(「compare rendered pixels of every story against known baselines」+ 測 component-in-isolation)。
|
|
14
|
+
- **VR deterministic 鐵律**:seed 靜態 fixture + mask 動態區。產品 app 內容本質跟 DS showcase 不同 → 拿來 pixel 比必全紅 = 100% false positive。
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
## 機制三層(conformance 主、pixel identity 窄)
|
|
17
|
+
|
|
18
|
+
| 層 | 驗什麼 | 落地 |
|
|
17
19
|
|---|---|---|
|
|
18
|
-
| 1
|
|
19
|
-
| 2
|
|
20
|
-
| 3
|
|
20
|
+
| **1 marker(conformance 意圖)** | consumer wrap 高風險 primitive 必標 `// @story-baseline: <DS-path>#<export>` cite canonical | `check_consumer_story_baseline.sh`(P0)+ `check_story_invariants.sh` R7 |
|
|
21
|
+
| **2 靜態 conformance(主防線)** | consumer 用對 token/primitive、沒 simplified mock、沒硬寫色值/字級/間距/shadow、沒 API-misuse | `check_consumer_ds_primitive_misuse.sh`(P0,含 2026-06-02 Pattern 8 硬寫 token)+ `check_layout_space_magic_numbers.sh`(P0 間距)+ `check_story_invariants.sh` R8(P0,registry antiPattern)+ `check_chrome_header_avatar_canonical.sh` / `check_sidebar_menu_button_implicit_wrap.sh` |
|
|
22
|
+
| **3 pixel/DOM identity(opt-in,窄用)** | 僅「忠實複製 replica」或「同 story 跨版本回歸」才比 render identity | `scripts/composition-fidelity-visual-diff.mjs`,**只比標了 `@composition-fidelity-mode` 的 mapping**;單獨 `@story-baseline` = conformance 不做 identity diff |
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
**層 3 為何 opt-in(2026-06-02)**:`@story-baseline` 單獨 = conformance 意圖(交層 2 驗)。pixel/DOM identity 只在該 consumer story **額外標** `@composition-fidelity-mode: pixel|shell-only|structural` 才跑(用於忠實複製 / same-story 回歸)。0 個 opt-in → script exit 0(conformance 由層 2 保證)。**禁** 拿產品範本(內容刻意不同)去 pixel 比 DS showcase。
|
|
23
25
|
|
|
24
|
-
## 層 3 用法
|
|
26
|
+
## 層 3 用法(opt-in 時)
|
|
25
27
|
|
|
26
28
|
```bash
|
|
27
|
-
|
|
28
|
-
npm run composition-fidelity -- \
|
|
29
|
-
--ds-url=http://localhost:9001 \
|
|
30
|
-
--consumer-url=http://localhost:9002 \
|
|
31
|
-
--consumer-root=/path/to/ds-product-template \
|
|
32
|
-
--out=.claude/snapshots/composition-fidelity \
|
|
33
|
-
--threshold-pct=2
|
|
34
|
-
|
|
35
|
-
# CI — against built storybook-static dirs:
|
|
36
|
-
npm run composition-fidelity -- \
|
|
29
|
+
node scripts/composition-fidelity-visual-diff.mjs \
|
|
37
30
|
--ds-static=storybook-static \
|
|
38
31
|
--consumer-static=/path/to/ds-product-template/storybook-static \
|
|
39
|
-
--consumer-root=/path/to/ds-product-template
|
|
40
|
-
--threshold-pct=0.5
|
|
32
|
+
--consumer-root=/path/to/ds-product-template --threshold-pct=0.5
|
|
41
33
|
```
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
**Exit codes**:0 = all within threshold / 1 = at least one exceeds / 2 = setup error。
|
|
46
|
-
|
|
47
|
-
## Threshold guidance
|
|
48
|
-
|
|
49
|
-
- `0.5%` — strict baseline change(intentional fix必 update baseline)
|
|
50
|
-
- `2%` — typical(allows brand text difference like "Acme Inc" vs "Acme Product")
|
|
51
|
-
- `5%` — initial bootstrap(consumer 多元化內容差異)
|
|
52
|
-
|
|
53
|
-
**Initial ds-product-template baseline 1.41%**(measured 2026-05-27):brand text + NAV labels content-level diff。Structural composition byte-equal。
|
|
54
|
-
|
|
55
|
-
## CI workflow(shipped)
|
|
56
|
-
|
|
57
|
-
Actual gate is centralized in DS repo at `.github/workflows/composition-fidelity.yml`:checkout DS + `ajenchen/ds-product-template`,build both Storybooks,and run the local DS script against the two `storybook-static` directories.
|
|
58
|
-
|
|
59
|
-
```yaml
|
|
60
|
-
# .github/workflows/composition-fidelity.yml(design-system)
|
|
61
|
-
on: [push, pull_request]
|
|
62
|
-
jobs:
|
|
63
|
-
composition-fidelity:
|
|
64
|
-
runs-on: ubuntu-latest
|
|
65
|
-
steps:
|
|
66
|
-
- uses: actions/checkout@v4
|
|
67
|
-
with: { path: design-system }
|
|
68
|
-
- uses: actions/checkout@v4
|
|
69
|
-
with: { repository: ajenchen/ds-product-template, path: ds-product-template }
|
|
70
|
-
- run: npm ci --legacy-peer-deps
|
|
71
|
-
working-directory: design-system
|
|
72
|
-
- run: npm ci --legacy-peer-deps
|
|
73
|
-
working-directory: ds-product-template
|
|
74
|
-
- run: npm run build-storybook
|
|
75
|
-
working-directory: design-system
|
|
76
|
-
- run: npm run build-storybook
|
|
77
|
-
working-directory: ds-product-template
|
|
78
|
-
- run: |
|
|
79
|
-
node scripts/composition-fidelity-visual-diff.mjs \
|
|
80
|
-
--ds-static=storybook-static \
|
|
81
|
-
--consumer-static=../ds-product-template/storybook-static \
|
|
82
|
-
--consumer-root=../ds-product-template \
|
|
83
|
-
--threshold-pct=0.5
|
|
84
|
-
working-directory: design-system
|
|
85
|
-
```
|
|
35
|
+
掛在 `.github/workflows/composition-fidelity.yml`(checkout DS + ds-product-template,build 兩邊 storybook 後比對;**build-storybook 前必先 `build:lib`** 否則 storybook-config tsc 找不到 `@qijenchen/design-system` 型別 = TS2307)。
|
|
86
36
|
|
|
87
37
|
## 不該做的事
|
|
88
38
|
|
|
89
|
-
- ❌
|
|
90
|
-
- ❌
|
|
91
|
-
- ❌
|
|
92
|
-
- ❌ 抽樣
|
|
93
|
-
|
|
94
|
-
## 反 pattern 錨例
|
|
39
|
+
- ❌ **拿產品範本 demo 當 pixel-identity baseline 對 DS showcase 比**(內容刻意不同 = false positive,world-class 公認反 pattern)— 2026-06-02 改 identity opt-in 修正
|
|
40
|
+
- ❌ baseline story 用 `Math.random()` 等非確定性產值(render 不可重現 → 任何 diff 都是 noise)— 2026-06-02 修 `sidebar.stories.tsx` PageContent
|
|
41
|
+
- ❌ 把 baseline screenshots commit 進 consumer repo(stale)— fetch from DS Pages live
|
|
42
|
+
- ❌ 抽樣 N stories(M-rule 不抽樣)
|
|
95
43
|
|
|
96
|
-
|
|
97
|
-
1. Source byte-equivalent(DS sidebar.stories.tsx WorkspaceBrand 跟 product-workspace App.tsx 同 pattern)
|
|
98
|
-
2. Stale build artifact:DS storybook-static built BEFORE commit 4e3256c1 fix → DS render 用 ItemAvatar wrapper / consumer 用 raw Avatar
|
|
99
|
-
3. User screenshot 從 stale deploy 看到「DS-rendered」vs「consumer-rendered」structural diff
|
|
44
|
+
## 歷史錨例
|
|
100
45
|
|
|
101
|
-
**Lesson
|
|
46
|
+
- **2026-05-27**:user 抓 AppShell Avatar+Label drift。Root cause = stale build artifact(DS storybook-static built BEFORE fix commit)。Lesson:byte-identity ≠ visual ≠ deployed identity。
|
|
47
|
+
- **2026-06-02**:CF check 從建立起 30 次全紅 = 把產品範本(App.tsx,Acme Product / 自訂 nav / dashboard 內容)硬比 DS `sidebar#IconCollapse`(Acme Inc / 不同 nav / 隨機亂數內容),pixel/DOM identity 物理上不可能。修法:層 3 改 opt-in、補層 2 靜態 conformance(Pattern 8 + R8 升 P0)、修 baseline 隨機亂數。對齊專案 2026-05-27 自身結論(memory `feedback_ai_ground_truth_unreliable_mechanical_primary`:render fidelity 由架構保障、template-vs-canonical pixel diff = noise 非 drift)。
|
package/ds-story-manifest.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"scripts/composition-fidelity-visual-diff.mjs",
|
|
8
8
|
"product-workspace apps/template/src/AllDsComponents.stories.tsx (DsCanonicalPortal)"
|
|
9
9
|
],
|
|
10
|
-
"generatedAt": "2026-06-02T14:
|
|
10
|
+
"generatedAt": "2026-06-02T14:55:19.698Z"
|
|
11
11
|
},
|
|
12
12
|
"components": {
|
|
13
13
|
"accordion": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qijenchen/design-system",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.48",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).",
|
|
6
6
|
"type": "module",
|
|
@@ -132,10 +132,11 @@ const PageContent = ({ title, description }: { title: string; description: React
|
|
|
132
132
|
<div className="max-w-2xl">
|
|
133
133
|
<p className="text-body text-fg-secondary mb-6">{description}</p>
|
|
134
134
|
<div className="grid grid-cols-2 gap-4">
|
|
135
|
-
{
|
|
135
|
+
{/* 固定值(非 Math.random)— story 是 visual baseline,必確定性,否則每次 build 像素不同 = composition-fidelity / visual regression 無法比對(2026-06-02 fix)*/}
|
|
136
|
+
{([['專案數量', 24], ['團隊成員', 8], ['本週提交', 47], ['待處理', 3]] as const).map(([t, v]) => (
|
|
136
137
|
<div key={t} className="rounded-lg border border-divider bg-surface p-4">
|
|
137
138
|
<p className="text-caption text-fg-muted">{t}</p>
|
|
138
|
-
<p className="text-h5 font-semibold mt-1">{
|
|
139
|
+
<p className="text-h5 font-semibold mt-1">{v}</p>
|
|
139
140
|
</div>
|
|
140
141
|
))}
|
|
141
142
|
</div>
|