@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.
Files changed (57) hide show
  1. package/CLAUDE.md +5 -7
  2. package/README.md +2 -2
  3. package/ds-canonical/hooks/auto_regen_ds_barrel.sh +58 -0
  4. package/ds-canonical/hooks/check_addon_subdir_ship.sh +76 -0
  5. package/ds-canonical/hooks/check_chrome_header_avatar_canonical.sh +84 -0
  6. package/ds-canonical/hooks/check_codex_brief_invariants.sh +40 -8
  7. package/ds-canonical/hooks/check_consumer_app_story_title.sh +81 -0
  8. package/ds-canonical/hooks/check_consumer_ds_primitive_misuse.sh +106 -0
  9. package/ds-canonical/hooks/check_consumer_no_ds_catalog.sh +98 -0
  10. package/ds-canonical/hooks/check_consumer_story_baseline.sh +76 -0
  11. package/ds-canonical/hooks/check_data_table_size_num_to_meta_width.sh +68 -0
  12. package/ds-canonical/hooks/check_ds_anchor_preflight.sh +132 -0
  13. package/ds-canonical/hooks/check_escape_marker_abuse.sh +86 -0
  14. package/ds-canonical/hooks/check_fork_user_plugin_install.sh +75 -0
  15. package/ds-canonical/hooks/check_full_story_visual_interaction_sweep.sh +71 -0
  16. package/ds-canonical/hooks/check_item_list_gap.sh +54 -0
  17. package/ds-canonical/hooks/check_layout_space_magic_numbers.sh +84 -0
  18. package/ds-canonical/hooks/check_orphan_ds_css.sh +66 -0
  19. package/ds-canonical/hooks/check_overlay_open_focus_escape_probe.sh +75 -0
  20. package/ds-canonical/hooks/check_plugin_freshness.sh +68 -0
  21. package/ds-canonical/hooks/check_post_main_ssot_propagate.sh +88 -0
  22. package/ds-canonical/hooks/check_propose_cite_required.sh +88 -0
  23. package/ds-canonical/hooks/check_propose_without_benchmark.sh +63 -0
  24. package/ds-canonical/hooks/check_sidebar_menu_button_implicit_wrap.sh +84 -0
  25. package/ds-canonical/hooks/check_storybook_addon_preset_cjs.sh +82 -0
  26. package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +7 -2
  27. package/ds-canonical/hooks/check_tailwind_wildcard_in_docs.sh +68 -0
  28. package/ds-canonical/hooks/chrome_header_dispatcher.sh +36 -0
  29. package/ds-canonical/hooks/inject_deploy_url_after_push.sh +178 -0
  30. package/ds-canonical/hooks/inject_pending_self_audit.sh +45 -1
  31. package/ds-canonical/hooks/{check_app_shell_primary_header_consistency.sh → lib/_app_shell_primary_header_consistency.sh} +1 -1
  32. package/ds-canonical/hooks/lib/_approval_re.sh +1 -1
  33. package/ds-canonical/hooks/{check_chrome_header_handcraft.sh → lib/_chrome_header_handcraft.sh} +1 -1
  34. package/ds-canonical/hooks/{check_header_with_tabs_border.sh → lib/_header_with_tabs_border.sh} +1 -1
  35. package/ds-canonical/hooks/{check_tab_lg_chrome_header_equal.sh → lib/_tab_lg_chrome_header_equal.sh} +1 -1
  36. package/ds-canonical/hooks/session_start_governance_check.sh +16 -3
  37. package/ds-canonical/hooks/stop_self_audit.sh +28 -3
  38. package/ds-canonical/hooks/tests/test_check_app_shell_primary_header_consistency.sh +1 -1
  39. package/ds-canonical/hooks/tests/test_check_chrome_header_handcraft.sh +1 -1
  40. package/ds-canonical/hooks/tests/test_check_data_table_size_num_to_meta_width.sh +52 -0
  41. package/ds-canonical/hooks/tests/test_check_ds_anchor_preflight.sh +104 -0
  42. package/ds-canonical/hooks/tests/test_check_fork_user_plugin_install.sh +45 -0
  43. package/ds-canonical/hooks/tests/test_check_header_with_tabs_border.sh +1 -1
  44. package/ds-canonical/hooks/tests/test_check_item_list_gap.sh +44 -0
  45. package/ds-canonical/hooks/tests/test_check_propose_without_benchmark.sh +55 -0
  46. package/ds-canonical/hooks/tests/test_check_tab_lg_chrome_header_equal.sh +1 -1
  47. package/ds-canonical/hooks/tests/test_session_start_governance_check.sh +3 -3
  48. package/ds-canonical/references/composition-fidelity.md +101 -0
  49. package/ds-canonical/references/scenario-definition.md +146 -0
  50. package/ds-canonical/rules/meta-patterns.md +1 -1
  51. package/ds-canonical/rules/story-rules.md +11 -2
  52. package/ds-canonical/skills/codex-collab/references/brief-template.md +1 -1
  53. package/ds-canonical/skills/deep-audit-cross-codex/SKILL.md +61 -6
  54. package/ds-canonical/skills/design-system-audit/SKILL.md +28 -1
  55. package/ds-canonical/skills/knowledge-prune/SKILL.md +12 -0
  56. package/ds-story-manifest.json +1 -1
  57. 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 / 40 hard**(SSOT = `session_start_governance_check.sh:173`)。動態值見 `scripts/sync-governance-counters.mjs` 跑出為準(2026-05-23:**31 M-rules / 56 audit dims / 37 hooks** — 不寫死避 drift,per 2026-05-22 prune codify)。
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
- # 元件完成 checklist
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
- **只用 top barrel import**(對齊 Material UI canonical)。`./components/<Name>` subpath 目前未支援 v1.0.0 stable 才會開啟( cross-component export name collision rename 完成)。
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
- if ! echo "$CMD" | grep -qE 'codex[[:space:]]+(exec|review)|node_modules/.bin/codex'; then
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
- # Extract brief content — handles `cat /tmp/file | codex exec` OR `codex exec "inline"` patterns
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
- # Inline prompt pattern fallback — grab everything after `codex exec` quotes
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