@qijenchen/design-system 0.1.0-beta.30 → 0.1.0-beta.33
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/CLAUDE.md +5 -7
- package/README.md +2 -2
- package/ds-canonical/hooks/auto_regen_ds_barrel.sh +58 -0
- package/ds-canonical/hooks/check_addon_subdir_ship.sh +76 -0
- package/ds-canonical/hooks/check_chrome_header_avatar_canonical.sh +84 -0
- package/ds-canonical/hooks/check_codex_brief_invariants.sh +40 -8
- package/ds-canonical/hooks/check_consumer_app_story_title.sh +81 -0
- package/ds-canonical/hooks/check_consumer_ds_primitive_misuse.sh +106 -0
- package/ds-canonical/hooks/check_consumer_no_ds_catalog.sh +98 -0
- package/ds-canonical/hooks/check_consumer_story_baseline.sh +76 -0
- package/ds-canonical/hooks/check_data_table_size_num_to_meta_width.sh +68 -0
- package/ds-canonical/hooks/check_ds_anchor_preflight.sh +132 -0
- package/ds-canonical/hooks/check_escape_marker_abuse.sh +86 -0
- package/ds-canonical/hooks/check_fork_user_plugin_install.sh +75 -0
- package/ds-canonical/hooks/check_full_story_visual_interaction_sweep.sh +71 -0
- package/ds-canonical/hooks/check_item_list_gap.sh +54 -0
- package/ds-canonical/hooks/check_layout_space_magic_numbers.sh +84 -0
- package/ds-canonical/hooks/check_orphan_ds_css.sh +66 -0
- package/ds-canonical/hooks/check_overlay_open_focus_escape_probe.sh +75 -0
- package/ds-canonical/hooks/check_plugin_freshness.sh +68 -0
- package/ds-canonical/hooks/check_post_main_ssot_propagate.sh +88 -0
- package/ds-canonical/hooks/check_propose_cite_required.sh +88 -0
- package/ds-canonical/hooks/check_propose_without_benchmark.sh +63 -0
- package/ds-canonical/hooks/check_sidebar_menu_button_implicit_wrap.sh +84 -0
- package/ds-canonical/hooks/check_storybook_addon_preset_cjs.sh +82 -0
- package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +7 -2
- package/ds-canonical/hooks/check_tailwind_wildcard_in_docs.sh +68 -0
- package/ds-canonical/hooks/chrome_header_dispatcher.sh +36 -0
- package/ds-canonical/hooks/inject_deploy_url_after_push.sh +178 -0
- package/ds-canonical/hooks/inject_pending_self_audit.sh +45 -1
- package/ds-canonical/hooks/{check_app_shell_primary_header_consistency.sh → lib/_app_shell_primary_header_consistency.sh} +1 -1
- package/ds-canonical/hooks/lib/_approval_re.sh +1 -1
- package/ds-canonical/hooks/{check_chrome_header_handcraft.sh → lib/_chrome_header_handcraft.sh} +1 -1
- package/ds-canonical/hooks/{check_header_with_tabs_border.sh → lib/_header_with_tabs_border.sh} +1 -1
- package/ds-canonical/hooks/{check_tab_lg_chrome_header_equal.sh → lib/_tab_lg_chrome_header_equal.sh} +1 -1
- package/ds-canonical/hooks/session_start_governance_check.sh +16 -3
- package/ds-canonical/hooks/stop_self_audit.sh +28 -3
- package/ds-canonical/hooks/tests/test_check_app_shell_primary_header_consistency.sh +1 -1
- package/ds-canonical/hooks/tests/test_check_chrome_header_handcraft.sh +1 -1
- package/ds-canonical/hooks/tests/test_check_data_table_size_num_to_meta_width.sh +52 -0
- package/ds-canonical/hooks/tests/test_check_ds_anchor_preflight.sh +104 -0
- package/ds-canonical/hooks/tests/test_check_fork_user_plugin_install.sh +45 -0
- package/ds-canonical/hooks/tests/test_check_header_with_tabs_border.sh +1 -1
- package/ds-canonical/hooks/tests/test_check_item_list_gap.sh +44 -0
- package/ds-canonical/hooks/tests/test_check_propose_without_benchmark.sh +55 -0
- package/ds-canonical/hooks/tests/test_check_tab_lg_chrome_header_equal.sh +1 -1
- package/ds-canonical/hooks/tests/test_session_start_governance_check.sh +3 -3
- package/ds-canonical/references/composition-fidelity.md +101 -0
- package/ds-canonical/references/scenario-definition.md +146 -0
- package/ds-canonical/rules/meta-patterns.md +1 -1
- package/ds-canonical/rules/story-rules.md +11 -2
- package/ds-canonical/skills/codex-collab/references/brief-template.md +1 -1
- package/ds-canonical/skills/deep-audit-cross-codex/SKILL.md +61 -6
- package/ds-canonical/skills/design-system-audit/SKILL.md +28 -1
- package/ds-canonical/skills/knowledge-prune/SKILL.md +12 -0
- package/ds-story-manifest.json +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
## 行數預算(Anthropic 對齊)
|
|
34
34
|
|
|
35
|
-
CLAUDE.md target ≤ 200(Anthropic best-practice)/ transition ≤ 400 / hard cap 800。SKILL ≤ 250 / spec ≤ 300(foundational SSOT 例外 ≤ 800-1200)/ memory **per-file ≤ 100 lines** + **MEMORY.md index ≤ 20 entries**(soft 18 / hard 20,session-start hook 攔)。Hooks **26 soft /
|
|
35
|
+
CLAUDE.md target ≤ 200(Anthropic best-practice)/ transition ≤ 400 / hard cap 800。SKILL ≤ 250 / spec ≤ 300(foundational SSOT 例外 ≤ 800-1200)/ memory **per-file ≤ 100 lines** + **MEMORY.md index ≤ 20 entries**(soft 18 / hard 20,session-start hook 攔)。Hooks **26 soft / 60 hard**(SSOT = `session_start_governance_check.sh:186`,2026-05-27 升 50→55→60 per codex M31 P0 hooks + baseline + primitive-misuse 3 new hooks)。動態值見 `scripts/sync-governance-counters.mjs` 跑出為準(snapshot 2026-05-28:**31 M-rules / 82 audit dims / 59 hooks** — 數字僅供 sanity check,真值以 script 輸出為準避 drift)。
|
|
36
36
|
|
|
37
37
|
## Anti-bloat L1-L3
|
|
38
38
|
|
|
@@ -85,6 +85,7 @@ CLAUDE.md target ≤ 200(Anthropic best-practice)/ transition ≤ 400 / hard cap
|
|
|
85
85
|
| **寫 story / 視覺 code** | `/story-writing` + `# SSOT 消費 canonical` |
|
|
86
86
|
| **命名新檔 / 變數 / prop** | `# 命名與語言一致性` + `.claude/rules/ui-development.md`「元件 Props 命名」 |
|
|
87
87
|
| **新元件 layout** | `# 4-Family Layout Model` |
|
|
88
|
+
| **建產品 / 開新 product app** | `npm run create-app <name>` → `apps/<name>/`;**2-scenario architecture SSOT** → `.claude/references/scenario-definition.md`(Scenario A / Scenario B 定義 + 20 test case + mirror chain + verify checkpoints,**之後增刪改 reference 此 SSOT**)|
|
|
88
89
|
| **新 skill / hook / command** | `.claude/{home}/README.md` charter |
|
|
89
90
|
| **無前例設計決策** | `# 遇不確定時的協議` |
|
|
90
91
|
| **Tailwind 出怪事** | `.claude/rules/ui-development.md`「Tailwind 5 條核心」+ `# 失敗記憶索引` |
|
|
@@ -106,6 +107,7 @@ CLAUDE.md target ≤ 200(Anthropic best-practice)/ transition ≤ 400 / hard cap
|
|
|
106
107
|
| 3 告訴 user 主要 change(or preview URL)| 讓 user 知道看什麼 |
|
|
107
108
|
| 4 等 user trigger | **「push / OK / 好 / 合 main」** → step 5;**「改 X / 不對 / 等等」** → 繼續 step 1 |
|
|
108
109
|
| 5 Squash merge to main | 不開 PR(可 GitHub API squash-merge OR fast-forward)|
|
|
110
|
+
| 5.5 **SSOT propagation gate**(2026-05-26 加 per user verbatim「push main 後所有 repo 都能獲得更新」)| Hook `check_post_main_ssot_propagate.sh` 偵測 `git push origin main` + diff HEAD~..HEAD 含 SSOT-affecting paths(`packages/{design-system,storybook-config}/src` / `.claude/{rules,hooks,skills,commands,references}` / `.claude-plugin/*.json` / `hooks/hooks.json` / `CLAUDE.md`)→ inject context 提議 bump npm `0.1.0-beta.<N+1>` + tag。AI 跑 bump + push tag → Release workflow auto-fire → npm publish → ds-product-template + fork repos Dependabot daily auto-PR(整鏈 1 trigger 涵蓋 /knowledge-prune / /deep-audit-cross-codex / 一般 dev / 任何 SSOT-affecting 來源,不需 skill-specific Phase Z)|
|
|
109
111
|
| 6 砍 remote branch | `git push origin --delete <branch>` ;sandbox HTTP 403 → 提醒 user GitHub UI 手動 |
|
|
110
112
|
| 7 Local 對齊 | `git checkout main && git fetch && git reset --hard origin/main && git branch -d <branch>` |
|
|
111
113
|
|
|
@@ -192,10 +194,6 @@ Vite + React + TypeScript + Tailwind v4 + shadcn/ui + Storybook + 自訂 Design
|
|
|
192
194
|
- `.claude/rules/ui-development.md` — paths: `**/*.tsx` + `**/*.ts`(含 Tailwind / Token / Props 命名 / shadcn)
|
|
193
195
|
- `.claude/rules/story-rules.md` — paths: `**/*.stories.tsx`(三層定位 + Title + 範例最高準則)
|
|
194
196
|
|
|
195
|
-
# 元件完成
|
|
197
|
+
# 元件完成 + Exploration
|
|
196
198
|
|
|
197
|
-
merge 前 invoke `/component-quality-gate`(45 項 + visual + clean-code 三層)
|
|
198
|
-
|
|
199
|
-
# Exploration & Prototype
|
|
200
|
-
|
|
201
|
-
正式 `packages/design-system/src/`(Phase 1 後 npm workspace 內化)vs 比稿 `src/explorations/`(hook `block_prototype_imports.py` 強制隔離)。比稿 `*.v1.stories.tsx` + `notes.md`,定案升級 patterns/ 或 components/。Skills:`/prototype` / `/component-quality-gate` / `/delivery-handoff`。
|
|
199
|
+
merge 前 invoke `/component-quality-gate`(45 項 + visual + clean-code 三層)。正式 `packages/design-system/src/`(Phase 1 後 npm workspace 內化)vs 比稿 `src/explorations/`(hook `block_prototype_imports.py` 強制隔離);比稿 `*.v1.stories.tsx` + `notes.md`,定案升級 patterns/ 或 components/。Skills:`/prototype` / `/component-quality-gate` / `/delivery-handoff`。
|
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ import { Button, Avatar, Dialog } from '@qijenchen/design-system'
|
|
|
76
76
|
<Dialog>...</Dialog>
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
**預設用 top barrel import**(對齊 Material UI canonical)。需要更窄的 import surface 時,也支援 `@qijenchen/design-system/components/<Name>` / `patterns/<Name>` / `hooks/<name>` subpath(見 package `exports`)。
|
|
80
80
|
|
|
81
81
|
Tree-shake 透過 `sideEffects: ["**/*.css"]` 配置自動 work,unused JS 被 bundler 剝除。
|
|
82
82
|
|
|
@@ -106,7 +106,7 @@ Token `--font-sans` stack = `Roboto, -apple-system, BlinkMacSystemFont, "Segoe U
|
|
|
106
106
|
|
|
107
107
|
## Storybook(consumer 看 DS 元件用法)
|
|
108
108
|
|
|
109
|
-
- Production:**https://ajenchen.github.io/design-system
|
|
109
|
+
- Production:**https://ajenchen-design-system.netlify.app/**(GH Pages mirror: <https://ajenchen.github.io/design-system/>)
|
|
110
110
|
- 看每個元件的「展示」/「設計規格」/「設計原則」3 層 stories
|
|
111
111
|
|
|
112
112
|
---
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# auto_regen_ds_barrel.sh — PostToolUse 自動 regen barrel + per-component index
|
|
3
|
+
#
|
|
4
|
+
# 2026-05-25 SSOT auto-sync invariant(user 永久 directive 2026-05-23):
|
|
5
|
+
# DS infra 增刪改 → package 該同步的都該自動,不需耳提面命。
|
|
6
|
+
# 解決 gap:新增 / 刪除 packages/design-system/src/{components,patterns}/<Dir>/ 後
|
|
7
|
+
# barrel `src/index.ts` 沒 auto-regen → consumer subpath import 壞 / new component 漏 export。
|
|
8
|
+
#
|
|
9
|
+
# Fire condition(PostToolUse Write|Edit|MultiEdit):
|
|
10
|
+
# - Write/Edit 創建新 `packages/design-system/src/components/<Dir>/<kebab>.tsx`
|
|
11
|
+
# OR 改動 component primary tsx(可能 export 名 change)
|
|
12
|
+
# - Write/Edit 創建 / 改動 `index.ts` in components/<Dir>/ or patterns/<dir>/
|
|
13
|
+
# - 新增 / 移除 hooks/*.ts or lib/*.ts(barrel 也 include 這些)
|
|
14
|
+
#
|
|
15
|
+
# Action:silent fire `node scripts/gen-component-indexes.mjs` + `node scripts/gen-design-system-barrel.mjs`
|
|
16
|
+
# 不 BLOCKER — auto fix-up,不打斷 workflow。emit message 告訴 AI「已 auto regen barrel」
|
|
17
|
+
# 對齊 sync-version-to-all-manifests.mjs pattern(post-action auto fix-up)
|
|
18
|
+
|
|
19
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
20
|
+
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
24
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
|
|
25
|
+
|
|
26
|
+
if [ -z "$FILE_PATH" ]; then exit 0; fi
|
|
27
|
+
|
|
28
|
+
# Scope filter:必 packages/design-system/src/{components,patterns,hooks,lib}/**
|
|
29
|
+
if ! echo "$FILE_PATH" | grep -qE 'packages/design-system/src/(components|patterns|hooks|lib)/'; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Skip stories / spec / test files(not barrel-included)
|
|
34
|
+
if echo "$FILE_PATH" | grep -qE '\.(stories|anatomy\.stories|principles\.stories|spec|test|spec\.md)\.(tsx?|md)$'; then
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
39
|
+
cd "$PROJECT_DIR" 2>/dev/null || exit 0
|
|
40
|
+
|
|
41
|
+
# Fire 2 gen scripts silently(stderr captured for diagnostics)
|
|
42
|
+
INDEX_OUT=$(node scripts/gen-component-indexes.mjs 2>&1 || true)
|
|
43
|
+
BARREL_OUT=$(node scripts/gen-design-system-barrel.mjs 2>&1 || true)
|
|
44
|
+
|
|
45
|
+
# Only emit additionalContext if barrel content changed(detected by checking if grep finds count delta)
|
|
46
|
+
# Simple proxy:grep "generated" in output means script ran successfully
|
|
47
|
+
if echo "$BARREL_OUT" | grep -qE 'generated|with [0-9]+ components'; then
|
|
48
|
+
cat <<EOF
|
|
49
|
+
{
|
|
50
|
+
"hookSpecificOutput": {
|
|
51
|
+
"hookEventName": "PostToolUse",
|
|
52
|
+
"additionalContext": "🔄 auto-regen DS barrel(SSOT auto-sync per 2026-05-23 user directive)— $FILE_PATH 改動觸發 \`gen-component-indexes.mjs\` + \`gen-design-system-barrel.mjs\` re-run。Barrel src/index.ts + per-component index.ts 已對齊。若 commit 此 turn 改動,記得 stage barrel 變化。"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
EOF
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
exit 0
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_addon_subdir_ship.sh — P0 BLOCKER
|
|
3
|
+
#
|
|
4
|
+
# Per 2026-05-28 beta.27 release fail anchor:
|
|
5
|
+
# ds-devmode addon 2026-05-27 從 .storybook/addons/ 搬到 packages/storybook-config/
|
|
6
|
+
# addons/ 時主檔(constants/manager/Panel/preset/preview)有搬,但 preview.ts 依
|
|
7
|
+
# 賴的 utils/ 子資料夾(6 files:dom-geometry/computed-style/token-reverse-lookup/
|
|
8
|
+
# overlay/geometry-diagnostic/token-drift-detector)沒搬 → npm package build 時
|
|
9
|
+
# Rollup「Could not resolve "./utils/dom-geometry"」→ CSF parse error cascade。
|
|
10
|
+
#
|
|
11
|
+
# 機械強制 PreToolUse Edit/Write 對 addon 主檔(preview.ts / manager.tsx 等):
|
|
12
|
+
# 偵測 import 從 `./utils/*` / `./components/*` 等 relative subdir,但對應 dir 不
|
|
13
|
+
# 存在於 file_path 同層 → BLOCK + cite missing files。
|
|
14
|
+
#
|
|
15
|
+
# Scope:packages/storybook-config/addons/ + .storybook/addons/ 內主檔。
|
|
16
|
+
# Escape:`@addon-subdir-skip:` comment。
|
|
17
|
+
|
|
18
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
19
|
+
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
23
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
24
|
+
|
|
25
|
+
case "${TOOL:-}" in
|
|
26
|
+
Edit|Write|MultiEdit) ;;
|
|
27
|
+
*) exit 0 ;;
|
|
28
|
+
esac
|
|
29
|
+
|
|
30
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
31
|
+
# Only check addon main files
|
|
32
|
+
if ! echo "$FILE" | grep -qE '/(\.storybook|storybook-config)/addons/[^/]+/[a-zA-Z]+\.(ts|tsx)$'; then exit 0; fi
|
|
33
|
+
|
|
34
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
|
|
35
|
+
[ -z "$CONTENT" ] && exit 0
|
|
36
|
+
|
|
37
|
+
# Escape clause
|
|
38
|
+
if echo "$CONTENT" | grep -qE '@addon-subdir-skip:'; then exit 0; fi
|
|
39
|
+
|
|
40
|
+
# Find relative subdir imports `./<dir>/*`
|
|
41
|
+
RELATIVE_IMPORTS=$(echo "$CONTENT" | grep -oE "from\s+['\"]\\./[a-zA-Z][a-zA-Z0-9_-]+/[a-zA-Z][a-zA-Z0-9_./-]*['\"]" | sed -E "s|^from\s+['\"]\./([^/]+)/.*['\"]$|\\1|" | sort -u)
|
|
42
|
+
|
|
43
|
+
[ -z "$RELATIVE_IMPORTS" ] && exit 0
|
|
44
|
+
|
|
45
|
+
ADDON_DIR=$(dirname "$FILE")
|
|
46
|
+
MISSING=""
|
|
47
|
+
for subdir in $RELATIVE_IMPORTS; do
|
|
48
|
+
if [ ! -d "$ADDON_DIR/$subdir" ]; then
|
|
49
|
+
MISSING="${MISSING} - ./${subdir}/ (referenced but dir absent)\n"
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
|
|
53
|
+
if [ -n "$MISSING" ]; then
|
|
54
|
+
cat >&2 << EOF
|
|
55
|
+
🚨 ADDON SUBDIR SHIP BLOCKER(P0,2026-05-28 beta.27 anchor)
|
|
56
|
+
|
|
57
|
+
File: $FILE
|
|
58
|
+
Imports from subdirectories that don't exist alongside this file:
|
|
59
|
+
$(printf '%b' "$MISSING")
|
|
60
|
+
|
|
61
|
+
Why blocked:
|
|
62
|
+
Addon main 檔 import 子資料夾 helper,但子 dir 沒一起 ship → npm build /
|
|
63
|
+
Rollup「Could not resolve」→ Storybook build 死。本 anchor:beta.27 第 7 次 CI
|
|
64
|
+
才抓到 ds-devmode 搬家漏帶 utils/ 6 files(燒 6 prior iterations debug)。
|
|
65
|
+
|
|
66
|
+
修方向:
|
|
67
|
+
1. Copy 缺漏 dir 過來:`cp -r <source>/<dir> $ADDON_DIR/<dir>`
|
|
68
|
+
2. 跑 npm run build-storybook local 過再 commit
|
|
69
|
+
3. 或:改 import 路徑 to existing location
|
|
70
|
+
|
|
71
|
+
Escape(極罕見): add \`// @addon-subdir-skip: <rationale>\` to file content
|
|
72
|
+
EOF
|
|
73
|
+
exit 2
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
exit 0
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_chrome_header_avatar_canonical.sh — PreToolUse Edit/Write 攔 chrome header descendant 用 ItemAvatar(2026-05-27)
|
|
3
|
+
#
|
|
4
|
+
# Per user 2026-05-27 抓 UserFooter vertical stack drift + codex collab Step 4 cite battle:
|
|
5
|
+
# header-canonical.spec.md:57-72 + sidebar.spec.md:241-247 + item-anatomy.spec.md:513-537 明文:
|
|
6
|
+
# 「Chrome header 不是 row context → 必 raw <Avatar size={24}>,禁 <ItemAvatar>(會誤啟動 row anatomy lookup)」
|
|
7
|
+
#
|
|
8
|
+
# Detection:
|
|
9
|
+
# PreToolUse Edit/Write content 偵測 `<SidebarHeader>` block 內含 `<ItemAvatar` → soft BLOCKER inject
|
|
10
|
+
# (允許 SidebarFooter 內 ItemAvatar — footer 是 SidebarMenu row context)
|
|
11
|
+
#
|
|
12
|
+
# Scope:
|
|
13
|
+
# - packages/design-system/src/components/Sidebar/**.tsx + **.stories.tsx
|
|
14
|
+
# - apps/**.tsx (consumer)
|
|
15
|
+
# - node_modules/@qijenchen/design-system/**(禁改 — block_prototype_imports 已攔)
|
|
16
|
+
#
|
|
17
|
+
# 對應 audit dim 預留 — TBD 升 audit dim 65
|
|
18
|
+
|
|
19
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
20
|
+
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
23
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
24
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
25
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
|
|
26
|
+
NEW=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""' 2>/dev/null)
|
|
27
|
+
|
|
28
|
+
[ "$EVENT" != "PreToolUse" ] && exit 0
|
|
29
|
+
case "$TOOL" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac
|
|
30
|
+
case "$FILE_PATH" in *.tsx) ;; *) exit 0 ;; esac
|
|
31
|
+
case "$FILE_PATH" in *.test.tsx|*.spec.md) exit 0 ;; esac
|
|
32
|
+
|
|
33
|
+
# Multi-line detection:`<SidebarHeader>...<ItemAvatar...>...</SidebarHeader>` block
|
|
34
|
+
# Use python for proper multiline match
|
|
35
|
+
HAS_DRIFT=$(printf '%s' "$NEW" | python3 -c '
|
|
36
|
+
import sys, re
|
|
37
|
+
content = sys.stdin.read()
|
|
38
|
+
# Find SidebarHeader blocks(opening tag → closing tag,non-greedy)
|
|
39
|
+
blocks = re.findall(r"<SidebarHeader[^>]*>.*?</SidebarHeader>", content, re.DOTALL)
|
|
40
|
+
for block in blocks:
|
|
41
|
+
if re.search(r"<ItemAvatar\b", block):
|
|
42
|
+
print("DRIFT")
|
|
43
|
+
sys.exit(0)
|
|
44
|
+
' 2>/dev/null)
|
|
45
|
+
|
|
46
|
+
[ "$HAS_DRIFT" != "DRIFT" ] && exit 0
|
|
47
|
+
|
|
48
|
+
# Override env var
|
|
49
|
+
if [ "${CLAUDE_BYPASS_CHROME_HEADER_AVATAR:-0}" = "1" ]; then
|
|
50
|
+
mkdir -p "$(dirname "$0")/../logs" 2>/dev/null
|
|
51
|
+
printf '{"ts":"%s","event":"chrome-header-avatar-bypass","file":"%s"}\n' \
|
|
52
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$FILE_PATH" >> "$(dirname "$0")/../logs/governance-bypass.jsonl" 2>/dev/null
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
REL=${FILE_PATH#*/my-project/}
|
|
57
|
+
|
|
58
|
+
cat >&2 <<EOF
|
|
59
|
+
🚨 Chrome header avatar canonical violation(per user 2026-05-27 抓 + codex collab cite battle):
|
|
60
|
+
|
|
61
|
+
📁 File: $REL
|
|
62
|
+
🔍 偵測:<SidebarHeader> block 內含 <ItemAvatar>(禁用)
|
|
63
|
+
|
|
64
|
+
Canonical citation:
|
|
65
|
+
- header-canonical.spec.md:57-72:「Chrome header 不是 row context → 必用 raw <Avatar size={24}>,禁用 <ItemAvatar>(會誤啟動 row anatomy lookup)」
|
|
66
|
+
- sidebar.spec.md:241-247:「consumer 用 raw <Avatar size={24}>(chrome header 不是 row context → 不該用 <ItemAvatar>)」
|
|
67
|
+
- item-anatomy.spec.md:513-537:「ItemAvatar scope = row context only;Chrome header 有自己的 canonical = raw <Avatar size={24}>」
|
|
68
|
+
|
|
69
|
+
修法(per DS canonical):
|
|
70
|
+
❌ <SidebarHeader>
|
|
71
|
+
<ItemAvatar alt="..." shape="square" color="..." solid />
|
|
72
|
+
<span>brand</span>
|
|
73
|
+
</SidebarHeader>
|
|
74
|
+
|
|
75
|
+
✅ <SidebarHeader>
|
|
76
|
+
<Avatar size={24} shape="square" color="..." solid alt="..." />
|
|
77
|
+
<span className="text-body-lg font-medium truncate">brand</span>
|
|
78
|
+
</SidebarHeader>
|
|
79
|
+
|
|
80
|
+
注意:SidebarFooter 內 ItemAvatar OK(footer 是 SidebarMenu row context)。本 hook 只攔 SidebarHeader。
|
|
81
|
+
|
|
82
|
+
Bypass(極罕見):CLAUDE_BYPASS_CHROME_HEADER_AVATAR=1 env var(audit-logged)。
|
|
83
|
+
EOF
|
|
84
|
+
exit 0
|
|
@@ -24,21 +24,46 @@ esac
|
|
|
24
24
|
|
|
25
25
|
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
|
|
26
26
|
|
|
27
|
-
# Only fire on codex CLI invocations
|
|
28
|
-
|
|
27
|
+
# Only fire on codex CLI invocations actually executing a brief
|
|
28
|
+
# (must be followed by `exec` or `review` subcommand; bare path mention like
|
|
29
|
+
# `ls node_modules/.bin/codex` or `which codex` is discovery, not a brief)
|
|
30
|
+
if ! echo "$CMD" | grep -qE '(^|[[:space:]/])codex[[:space:]]+(exec|review)\b'; then
|
|
29
31
|
exit 0
|
|
30
32
|
fi
|
|
31
33
|
|
|
32
|
-
#
|
|
34
|
+
# Discovery / introspection flags are not briefs — skip
|
|
35
|
+
if echo "$CMD" | grep -qE '(^|[[:space:]])-{1,2}(help|h|version|V)\b'; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Extract brief content — handles multiple invocation patterns:
|
|
40
|
+
# 1. `cat /tmp/file | codex exec` (cat-pipe → file)
|
|
41
|
+
# 2. `codex exec "$(cat /tmp/file)"` (arg-substitution → file)
|
|
42
|
+
# 3. `codex exec < /tmp/file` (stdin redirect → file)
|
|
43
|
+
# 4. `codex exec "inline brief..."` (inline arg → CMD itself)
|
|
33
44
|
BRIEF_CONTENT=""
|
|
45
|
+
BRIEF_FILE=""
|
|
46
|
+
|
|
47
|
+
# Pattern 1: cat-pipe
|
|
34
48
|
if echo "$CMD" | grep -qE 'cat[[:space:]]+[^|]+\|[[:space:]]*[^|]*codex'; then
|
|
35
|
-
# cat-pipe pattern — try reading the cat'd file
|
|
36
49
|
BRIEF_FILE=$(echo "$CMD" | grep -oE 'cat[[:space:]]+[^[:space:]|]+' | head -1 | sed 's/^cat[[:space:]]*//')
|
|
37
|
-
if [ -n "$BRIEF_FILE" ] && [ -f "$BRIEF_FILE" ]; then
|
|
38
|
-
BRIEF_CONTENT=$(cat "$BRIEF_FILE" 2>/dev/null)
|
|
39
|
-
fi
|
|
40
50
|
fi
|
|
41
|
-
|
|
51
|
+
|
|
52
|
+
# Pattern 2: arg-substitution `"$(cat /path)"` or `$(cat /path)`
|
|
53
|
+
if [ -z "$BRIEF_FILE" ] && echo "$CMD" | grep -qE '\$\([[:space:]]*cat[[:space:]]+[^)]+\)'; then
|
|
54
|
+
BRIEF_FILE=$(echo "$CMD" | grep -oE '\$\([[:space:]]*cat[[:space:]]+[^)]+\)' | head -1 | sed -E 's/^\$\([[:space:]]*cat[[:space:]]+//; s/[[:space:]]*\)$//')
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Pattern 3: stdin redirect `< /path`
|
|
58
|
+
if [ -z "$BRIEF_FILE" ] && echo "$CMD" | grep -qE 'codex[[:space:]]+exec[[:space:]].*<[[:space:]]*[^[:space:]<>|&]+'; then
|
|
59
|
+
BRIEF_FILE=$(echo "$CMD" | grep -oE '<[[:space:]]*[^[:space:]<>|&]+' | head -1 | sed -E 's/^<[[:space:]]*//')
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [ -n "$BRIEF_FILE" ] && [ -f "$BRIEF_FILE" ]; then
|
|
63
|
+
BRIEF_CONTENT=$(cat "$BRIEF_FILE" 2>/dev/null)
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Pattern 4 fallback — inline prompt (or unparseable cmd): scan CMD itself
|
|
42
67
|
if [ -z "$BRIEF_CONTENT" ]; then
|
|
43
68
|
BRIEF_CONTENT="$CMD"
|
|
44
69
|
fi
|
|
@@ -66,6 +91,13 @@ if ! echo "$BRIEF_CONTENT" | grep -qiE '禁抽樣|禁 sample|NO-SAMPLE|不抽樣
|
|
|
66
91
|
MISSING="${MISSING} • 3️⃣ 禁抽樣 invariant 缺(「禁抽樣」/「NO-SAMPLE」/「sample = reject」keyword)\n"
|
|
67
92
|
fi
|
|
68
93
|
|
|
94
|
+
# 4. 禁列檔 invariant(2026-05-27 codify per codex v1/v2 token-burn anchor)
|
|
95
|
+
# Codex CLI 2 次連續 invocation 都跑 `rg --files` / `find` 列 1300+ files 燒光 reasoning。
|
|
96
|
+
# Brief 必含 directive 限制 codex 探索範圍 — 「只讀 N 列檔 + 禁列檔 / 禁 rg --files / 禁 find 全 repo」
|
|
97
|
+
if ! echo "$BRIEF_CONTENT" | grep -qiE '禁列檔|禁 rg --files|禁 find 全|只讀.*[0-9]+.*file|限定.*file|targeted rg|不需報告探索|直接出'; then
|
|
98
|
+
MISSING="${MISSING} • 4️⃣ 禁列檔 invariant 缺(「禁列檔」/「禁 rg --files」/「只讀 N file」/「直接出 verdict」keyword)— per 2026-05-27 codex token-burn 2× anchor\n"
|
|
99
|
+
fi
|
|
100
|
+
|
|
69
101
|
if [ -n "$MISSING" ]; then
|
|
70
102
|
printf '🚨 CODEX BRIEF MISSING INVARIANTS BLOCKER(2026-05-23 user verbatim:「codex 會跑的稽核流程理應要跟你跑的深度稽核流程是一模一樣 SSOT 的不能偏移」):\n' >&2
|
|
71
103
|
printf '\n Brief 缺以下 invariant:\n' >&2
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_consumer_app_story_title.sh — P0 BLOCKER
|
|
3
|
+
#
|
|
4
|
+
# 2026-05-28 codify per template create-app duplicate-id bug anchor:
|
|
5
|
+
# Consumer apps(template / fork repos)裡 `apps/<name>/**/*.stories.tsx` 的
|
|
6
|
+
# `title:` 必開頭 `Apps/<kebab-app-name>/...` — 否則 Storybook glob 撈到後撞 id。
|
|
7
|
+
#
|
|
8
|
+
# SSOT:.claude/rules/story-rules.md「Title 命名 2-namespace canonical」
|
|
9
|
+
#
|
|
10
|
+
# Mechanical 強制 PreToolUse Edit/Write 對 `apps/<name>/**/*.stories.tsx`:
|
|
11
|
+
# - 偵測 `title:` field
|
|
12
|
+
# - 若 title 開頭非 `Apps/<dir-name>/...` → BLOCK
|
|
13
|
+
# - 自動推導 dir-name 從 file path(`apps/order-dashboard/src/App.stories.tsx`
|
|
14
|
+
# → expected prefix `Apps/order-dashboard/`)
|
|
15
|
+
#
|
|
16
|
+
# Scope:`/apps/<name>/**/*.stories.@(tsx|ts|mdx)`(consumer repos only)。
|
|
17
|
+
# DS-internal stories(`packages/design-system/**`)走另條 canonical(`Design System/...`),
|
|
18
|
+
# 不在本 hook scope。
|
|
19
|
+
#
|
|
20
|
+
# Escape:`@app-story-title-skip:` comment(極罕見)。
|
|
21
|
+
|
|
22
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
23
|
+
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
27
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
case "${TOOL:-}" in
|
|
30
|
+
Edit|Write|MultiEdit) ;;
|
|
31
|
+
*) exit 0 ;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
35
|
+
# Scope:apps/<name>/**/*.stories.(tsx|ts|mdx)
|
|
36
|
+
if ! echo "$FILE" | grep -qE '/apps/[^/]+/.+\.stories\.(tsx|ts|mdx)$'; then exit 0; fi
|
|
37
|
+
|
|
38
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
|
|
39
|
+
[ -z "$CONTENT" ] && exit 0
|
|
40
|
+
|
|
41
|
+
# Escape clause
|
|
42
|
+
if echo "$CONTENT" | grep -qE '@app-story-title-skip:'; then exit 0; fi
|
|
43
|
+
|
|
44
|
+
# Extract expected app name from file path
|
|
45
|
+
APP_NAME=$(echo "$FILE" | sed -E 's|.*/apps/([^/]+)/.*|\1|')
|
|
46
|
+
[ -z "$APP_NAME" ] && exit 0
|
|
47
|
+
|
|
48
|
+
# Find title field(支援 single/double/backtick quote)
|
|
49
|
+
TITLE_LINE=$(echo "$CONTENT" | grep -oE "title:\s*['\"\`][^'\"\`]+['\"\`]" | head -1)
|
|
50
|
+
|
|
51
|
+
# 若無 title field,skip(no-op stories OK)
|
|
52
|
+
[ -z "$TITLE_LINE" ] && exit 0
|
|
53
|
+
|
|
54
|
+
EXPECTED_PREFIX="Apps/${APP_NAME}/"
|
|
55
|
+
|
|
56
|
+
# Check title 是否開頭 `Apps/<app-name>/`
|
|
57
|
+
if ! echo "$TITLE_LINE" | grep -qE "title:\s*['\"\`]Apps/${APP_NAME}/"; then
|
|
58
|
+
cat >&2 << EOF
|
|
59
|
+
🚨 CONSUMER APP STORY TITLE BLOCKER(P0,2026-05-28 codify per create-app duplicate-id anchor)
|
|
60
|
+
|
|
61
|
+
File: $FILE
|
|
62
|
+
Detected title: $TITLE_LINE
|
|
63
|
+
Expected prefix: \`title: 'Apps/${APP_NAME}/...'\`
|
|
64
|
+
|
|
65
|
+
Why blocked:
|
|
66
|
+
Consumer apps 內 stories 必用 \`Apps/<app-name>/<page-purpose>\` 開頭 namespace
|
|
67
|
+
(per .claude/rules/story-rules.md「Title 命名 2-namespace canonical」)。
|
|
68
|
+
錯 prefix → Storybook glob 撈到後與 template/其他 app 撞 id → build duplicate
|
|
69
|
+
warning + 只顯第一個 → 新 app 在 sidebar 不可見。
|
|
70
|
+
|
|
71
|
+
Anchor:2026-05-28 npm run create-app 不改 story title 導致 e2e 抓 4 個 collisions。
|
|
72
|
+
|
|
73
|
+
Fix:
|
|
74
|
+
title: 'Apps/${APP_NAME}/<Your Page Purpose>' // ex: 'Apps/${APP_NAME}/Dashboard'
|
|
75
|
+
|
|
76
|
+
Escape(極罕見):add \`// @app-story-title-skip: <rationale>\`
|
|
77
|
+
EOF
|
|
78
|
+
exit 2
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
exit 0
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_consumer_ds_primitive_misuse.sh — P0 BLOCKER
|
|
3
|
+
#
|
|
4
|
+
# 偵測 consumer production code(`.tsx`)用 DS primitive 但走 anti-pattern API 用法
|
|
5
|
+
# (per user 2026-05-27 verbatim「做產品真的能使用跟 ds repo 一模一樣的元件做產品嗎?」
|
|
6
|
+
# +「眼不見為淨欸,重點是你他媽做產品真的要能使用跟 ds repo 一模一樣的元件才是目標」)
|
|
7
|
+
#
|
|
8
|
+
# Anchor 2026-05-27 — 7 bugs caught by user 在 storybook 但同樣的 anti-pattern 會出現在
|
|
9
|
+
# production App.tsx / page components:
|
|
10
|
+
# 1. <CircularProgress size={N}> 任意 number 而非 default 24
|
|
11
|
+
# 2. <RadioGroupItem> 沒 wrap <SelectionItem control={...}>(per selection-item.spec.md:23)
|
|
12
|
+
# 3. <DataTable columns={[<2 cols]}> minimal mock 看起來破
|
|
13
|
+
# 4. <LinkInput placeholder=...> 沒 value prop = placeholder-only mode 抹平 link/edit pattern
|
|
14
|
+
# 5. <Empty title=...> 無 icon AND 無 description = 違反 Empty.tsx:11「預設只需 description」
|
|
15
|
+
# 6. <Tooltip|Popover|Dialog|Sheet|DropdownMenu> trigger 後 user 無路徑看 content
|
|
16
|
+
# 7. <DS.X> 內 free-form ad-hoc layout 跟 DS canonical 結構差異
|
|
17
|
+
#
|
|
18
|
+
# Triggers on consumer apps/**/*.tsx + .stories.tsx edit. Blocks anti-patterns above.
|
|
19
|
+
#
|
|
20
|
+
# Escape:per-line `// @ds-misuse-allow: <rationale>` documented exception.
|
|
21
|
+
|
|
22
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
23
|
+
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
27
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
case "${TOOL:-}" in
|
|
30
|
+
Edit|Write|MultiEdit) ;;
|
|
31
|
+
*) exit 0 ;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
35
|
+
# Cover BOTH stories AND production .tsx in consumer apps
|
|
36
|
+
if ! echo "$FILE" | grep -qE '/(apps|consumer)/.*\.(tsx|ts)$'; then exit 0; fi
|
|
37
|
+
if echo "$FILE" | grep -qE 'packages/design-system/src/|node_modules/'; then exit 0; fi
|
|
38
|
+
|
|
39
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
|
|
40
|
+
[ -z "$CONTENT" ] && exit 0
|
|
41
|
+
|
|
42
|
+
# Global escape — file-wide allowlist
|
|
43
|
+
if echo "$CONTENT" | grep -q '@ds-misuse-allow:'; then exit 0; fi
|
|
44
|
+
|
|
45
|
+
VIOLATIONS=""
|
|
46
|
+
|
|
47
|
+
# Pattern 1: <CircularProgress size={N}> with literal number (override default 24)
|
|
48
|
+
if echo "$CONTENT" | grep -qE '<DS\.CircularProgress[^>]+size=\{[0-9]+\}'; then
|
|
49
|
+
VIOLATIONS="${VIOLATIONS} - <CircularProgress size={N}> hardcoded number override default 24 (per circular-progress.spec.md:101)\n"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Pattern 2: <RadioGroupItem> NOT wrapped in <SelectionItem control={...}>
|
|
53
|
+
# Approximation: file uses RadioGroupItem but doesn't reference SelectionItem
|
|
54
|
+
if echo "$CONTENT" | grep -qE '<DS\.RadioGroupItem\b' && ! echo "$CONTENT" | grep -qE 'SelectionItem|RadioGroupItem.*label='; then
|
|
55
|
+
VIOLATIONS="${VIOLATIONS} - <RadioGroupItem> 沒 wrap <SelectionItem control={<RadioGroupItem>}> (per selection-item.spec.md:23 SSOT spacing/padding)\n"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Pattern 3: <DataTable columns={[…]}> with literal single column
|
|
59
|
+
if echo "$CONTENT" | grep -qE '<DS\.DataTable[^>]+columns=\{\[\s*\{[^}]+\}\s*\]\}' && ! echo "$CONTENT" | grep -qE 'columns=\{[^}]*\},\s*\{'; then
|
|
60
|
+
VIOLATIONS="${VIOLATIONS} - <DataTable columns={[single-col]}> minimal one-column = 違反 data-table.spec.md canonical(min 2 cols for meaningful render)\n"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Pattern 4: <LinkInput placeholder=...> without value prop
|
|
64
|
+
if echo "$CONTENT" | grep -qE '<DS\.LinkInput[^>]+placeholder=' && ! echo "$CONTENT" | grep -qE '<DS\.LinkInput[^>]+(value|defaultValue)='; then
|
|
65
|
+
VIOLATIONS="${VIOLATIONS} - <LinkInput placeholder=...> 沒 value prop = placeholder-only mode 抹平 link/edit canonical (per link-input.spec.md:18,48-58)\n"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Pattern 5: <Empty title=...> without icon and without description
|
|
69
|
+
if echo "$CONTENT" | grep -qE '<DS\.Empty[^>]+title=' && \
|
|
70
|
+
! echo "$CONTENT" | grep -qE '<DS\.Empty[^>]+icon=' && \
|
|
71
|
+
! echo "$CONTENT" | grep -qE '<DS\.Empty[^>]+description='; then
|
|
72
|
+
VIOLATIONS="${VIOLATIONS} - <Empty title=...> 無 icon 無 description = 違反 Empty.tsx:11「預設只需 description」minimal mock looks weird\n"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Pattern 6: Overlay trigger without defaultOpen state for visual demo
|
|
76
|
+
# (Skip in production .tsx; only enforce in .stories.tsx where visual snapshot matters)
|
|
77
|
+
if echo "$FILE" | grep -qE '\.stories\.tsx$'; then
|
|
78
|
+
for overlay in Tooltip Popover Dialog Sheet DropdownMenu; do
|
|
79
|
+
if echo "$CONTENT" | grep -qE "<DS\.${overlay}\b" && \
|
|
80
|
+
! echo "$CONTENT" | grep -qE "(defaultOpen|open=\{(true|isOpen)\})"; then
|
|
81
|
+
VIOLATIONS="${VIOLATIONS} - Story uses <${overlay}> without defaultOpen — visual audit can't see overlay content\n"
|
|
82
|
+
fi
|
|
83
|
+
done
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
if [ -n "$VIOLATIONS" ]; then
|
|
87
|
+
cat >&2 << EOF
|
|
88
|
+
🚨 CONSUMER-DS-PRIMITIVE-MISUSE BLOCKER(P0,2026-05-27 user verbatim「做產品真的要能使用跟 ds repo 一模一樣的元件」)
|
|
89
|
+
|
|
90
|
+
File $FILE detected anti-pattern DS API usage:
|
|
91
|
+
$(echo -e "$VIOLATIONS")
|
|
92
|
+
per M31 codex synthesis SSOT + DS spec.md citations(file:line 在每條 violation).
|
|
93
|
+
|
|
94
|
+
Anchor:user 2026-05-27 抓 7 個 visual bug 全 root cause = consumer minimal-mock 抹平
|
|
95
|
+
DS canonical 設計意圖。本 hook 攔 production 重犯同 pattern。
|
|
96
|
+
|
|
97
|
+
修法 2 選 1:
|
|
98
|
+
(a) 改用 DS canonical pattern(per file:line cited spec).
|
|
99
|
+
(b) Escape:加 \`// @ds-misuse-allow: <rationale>\` 顯式 documented per file OR per line.
|
|
100
|
+
|
|
101
|
+
Per-bug fix paths → /tmp/codex-ssot-output.txt(M31 codex synthesis 2026-05-27)
|
|
102
|
+
EOF
|
|
103
|
+
exit 2
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
exit 0
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_consumer_no_ds_catalog.sh — P0 BLOCKER
|
|
3
|
+
#
|
|
4
|
+
# Block consumer apps from authoring per-component DS catalog stories
|
|
5
|
+
# (per M31 codex synthesis 2026-05-27: PW only owns composition stories,
|
|
6
|
+
# DS canonical Storybook is the sole per-component visual SSOT).
|
|
7
|
+
#
|
|
8
|
+
# Anchor 2026-05-27 user verbatim:
|
|
9
|
+
# 「確保跟 ds repo 一模一樣」+「全盤避免 minimal mock 抹平」
|
|
10
|
+
# 7 bugs caught by user(CircularProgress size=32 / RadioGroup raw / DataTable one-col /
|
|
11
|
+
# LinkInput placeholder / Empty 缺 icon / Overlay no-open / Tooltip)— all root cause:
|
|
12
|
+
# consumer hand-mock minimal-prop render ≠ DS canonical → 必 drift.
|
|
13
|
+
#
|
|
14
|
+
# Triggers on consumer apps/**/*.stories.tsx edit. Blocks:
|
|
15
|
+
# - File named EveryDsComponent / AllDsComponents (catalog naming pattern)
|
|
16
|
+
# - Story title containing「所有 DS 元件」/「Every DS Component」/「per-component default render」
|
|
17
|
+
# - `import * as DS from '@qijenchen/design-system'` + `Object.keys(DS).map`(iterate-render pattern)
|
|
18
|
+
# - `<DS.X minimal-props>` repeated mass render(detect: ≥5 different DS components rendered in same story)
|
|
19
|
+
#
|
|
20
|
+
# Escape:`// @consumer-catalog-allow: <rationale>` per-file marker(極罕見;eg. portal proxy file).
|
|
21
|
+
|
|
22
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
23
|
+
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
27
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
case "${TOOL:-}" in
|
|
30
|
+
Edit|Write|MultiEdit) ;;
|
|
31
|
+
*) exit 0 ;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
35
|
+
# Only check consumer storybook files
|
|
36
|
+
if ! echo "$FILE" | grep -qE '/(apps|consumer)/.*\.stories\.tsx$'; then exit 0; fi
|
|
37
|
+
# Skip DS source
|
|
38
|
+
if echo "$FILE" | grep -qE 'packages/design-system/src/'; then exit 0; fi
|
|
39
|
+
|
|
40
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
|
|
41
|
+
[ -z "$CONTENT" ] && exit 0
|
|
42
|
+
|
|
43
|
+
# Escape clause
|
|
44
|
+
if echo "$CONTENT" | grep -q '@consumer-catalog-allow:'; then exit 0; fi
|
|
45
|
+
|
|
46
|
+
VIOLATIONS=""
|
|
47
|
+
|
|
48
|
+
# Pattern 1: file basename forbidden
|
|
49
|
+
basename=$(basename "$FILE" .stories.tsx)
|
|
50
|
+
if echo "$basename" | grep -qE '^(EveryDsComponent|AllDsComponents|AllComponents|DsCatalog|EveryComponent)$'; then
|
|
51
|
+
# AllDsComponents allowed IF it's only portal proxy (check title)
|
|
52
|
+
if [ "$basename" = "AllDsComponents" ] && echo "$CONTENT" | grep -qE 'DsCanonicalPortal|iframe.*design-system|@consumer-catalog-allow'; then
|
|
53
|
+
: # portal proxy OK
|
|
54
|
+
else
|
|
55
|
+
VIOLATIONS="${VIOLATIONS} - File basename '$basename' = catalog pattern. PW 不該重寫 DS catalog.\n"
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Pattern 2: title claims per-component default
|
|
60
|
+
if echo "$CONTENT" | grep -qE "title:.*['\"](所有 DS 元件|Every DS Component|All DS Components.*render|每元件 default)"; then
|
|
61
|
+
VIOLATIONS="${VIOLATIONS} - Story title claims per-component default render. PW catalog 只可 import smoke + DS portal proxy.\n"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Pattern 3: iterate-render anti-pattern
|
|
65
|
+
if echo "$CONTENT" | grep -qE 'Object\.keys\(DS\)\.(map|forEach)' || \
|
|
66
|
+
echo "$CONTENT" | grep -qE 'Object\.entries\(DS\)\.(map|forEach)'; then
|
|
67
|
+
VIOLATIONS="${VIOLATIONS} - Detected Object.keys/entries(DS).map iterate-render pattern. 禁 iterate render DS exports.\n"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Pattern 4: mass hand-mock(≥5 different <DS.X> tags in same file)
|
|
71
|
+
DS_TAG_COUNT=$(echo "$CONTENT" | grep -oE '<DS\.[A-Z][a-zA-Z]+' | sort -u | wc -l | tr -d ' ')
|
|
72
|
+
if [ "$DS_TAG_COUNT" -ge 5 ]; then
|
|
73
|
+
VIOLATIONS="${VIOLATIONS} - Detected ${DS_TAG_COUNT} distinct <DS.X> renders in single file. 大量 hand-mock = drift risk(per 2026-05-27 7-bug 錨例). 重構成 single composition demo.\n"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if [ -n "$VIOLATIONS" ]; then
|
|
77
|
+
cat >&2 << EOF
|
|
78
|
+
🚨 CONSUMER-NO-DS-CATALOG BLOCKER(P0,2026-05-27 user 永久 directive「確保跟 ds repo 一模一樣」+ M31 codex synthesis)
|
|
79
|
+
|
|
80
|
+
Consumer file $FILE 違反:
|
|
81
|
+
$(echo -e "$VIOLATIONS")
|
|
82
|
+
per M31 codex synthesis SSOT:
|
|
83
|
+
- DS owns per-component canonical pixels(62/62 components ×3 tiers stories in DS Storybook)
|
|
84
|
+
- PW(consumer)owns 真實業務 composition demos(AppShell Dashboard etc.)
|
|
85
|
+
- Catalog → DS canonical Storybook iframe/link proxy,**禁** PW 重寫 <DS.X minimal mock>
|
|
86
|
+
|
|
87
|
+
歷史錨點 2026-05-27 7 bugs:CircularProgress size=32 hardcode / RadioGroup raw item 沒 SelectionItem / DataTable one-col / LinkInput placeholder mock / Empty 缺 icon / Overlay trigger-only / Tooltip context — ALL 從 PW hand-mock minimal-prop drift.
|
|
88
|
+
|
|
89
|
+
修法 2 選 1:
|
|
90
|
+
(a) 改用 DS canonical Storybook iframe portal(per template AllDsComponents.stories.tsx#DsCanonicalPortal pattern)
|
|
91
|
+
(b) Escape:加 \`// @consumer-catalog-allow: <rationale>\` 顯式 documented
|
|
92
|
+
|
|
93
|
+
完整 SSOT → DS package ds-story-manifest.json + codex M31 synthesis output /tmp/codex-ssot-output.txt
|
|
94
|
+
EOF
|
|
95
|
+
exit 2
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
exit 0
|