@qijenchen/design-system 0.1.0-beta.69 → 0.1.0-beta.71

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 (28) hide show
  1. package/ds-canonical/fork/governance.lock +112 -0
  2. package/ds-canonical/fork/hooks/block_prototype_imports.py +111 -0
  3. package/ds-canonical/fork/hooks/check_chrome_header_avatar_canonical.sh +95 -0
  4. package/ds-canonical/fork/hooks/check_consumer_app_invariants.sh +349 -0
  5. package/ds-canonical/fork/hooks/check_ds_anchor_preflight.sh +132 -0
  6. package/ds-canonical/fork/hooks/check_escape_marker_abuse.sh +140 -0
  7. package/ds-canonical/fork/hooks/check_field_family_invariants.sh +250 -0
  8. package/ds-canonical/fork/hooks/check_fork_product_quality.sh +36 -0
  9. package/ds-canonical/fork/hooks/check_item_list_gap.sh +54 -0
  10. package/ds-canonical/fork/hooks/check_layout_space_magic_numbers.sh +96 -0
  11. package/ds-canonical/fork/hooks/check_opacity_token_usage.sh +149 -0
  12. package/ds-canonical/fork/hooks/check_pattern_invariants.sh +196 -0
  13. package/ds-canonical/fork/hooks/check_sidebar_menu_button_implicit_wrap.sh +86 -0
  14. package/ds-canonical/fork/hooks/check_tailwind_wildcard_in_docs.sh +79 -0
  15. package/ds-canonical/fork/hooks/inject_deploy_url_after_push.sh +238 -0
  16. package/ds-canonical/fork/launchers/fork-governance-dispatcher.sh +44 -0
  17. package/ds-canonical/fork/launchers/inject_fork_governance_preamble.sh +46 -0
  18. package/ds-canonical/fork/launchers/settings-hooks.json +58 -0
  19. package/ds-canonical/fork/manifest.json +79 -0
  20. package/ds-canonical/fork/preamble.md +495 -0
  21. package/ds-canonical/hooks/check_consumer_app_invariants.sh +19 -0
  22. package/ds-canonical/references/scenario-definition.md +4 -4
  23. package/ds-canonical/skills/design-system-audit/SKILL.md +1 -1
  24. package/llms-full.txt +1 -1
  25. package/llms.txt +1 -1
  26. package/package.json +5 -1
  27. package/src/story-governance/category-matrix.json +132 -0
  28. package/src/tokens/utility-registry.json +124 -0
@@ -0,0 +1,112 @@
1
+ {
2
+ "_purpose": "sha256 of every shipped fork governance body + manifest (tamper-detect baseline)",
3
+ "entries": [
4
+ {
5
+ "file": "hooks/block_prototype_imports.py",
6
+ "sha256": "0ab679ddb522f508c490891711559175a912be8b7f4db04d8a55cdac983fd482",
7
+ "sourceHook": "block_prototype_imports.py",
8
+ "bucket": "SHIP_AS_IS"
9
+ },
10
+ {
11
+ "file": "hooks/check_chrome_header_avatar_canonical.sh",
12
+ "sha256": "ffa91559c19d3f2aab50c69c4b247c15a29376d10b116f6223afb3289b4f91a2",
13
+ "sourceHook": "check_chrome_header_avatar_canonical.sh",
14
+ "bucket": "SHIP_AS_IS"
15
+ },
16
+ {
17
+ "file": "hooks/check_consumer_app_invariants.sh",
18
+ "sha256": "e2d63b142759bf791ca3c4462342d91623827aeb54c15398c94853485684900b",
19
+ "sourceHook": "check_consumer_app_invariants.sh",
20
+ "bucket": "SHIP_AS_IS"
21
+ },
22
+ {
23
+ "file": "hooks/check_ds_anchor_preflight.sh",
24
+ "sha256": "15fb44c8b35042efd03e1992f8b5b7b121a7633cebe556d4ca0a1a7f5483dc35",
25
+ "sourceHook": "check_ds_anchor_preflight.sh",
26
+ "bucket": "SHIP_REWRITTEN"
27
+ },
28
+ {
29
+ "file": "hooks/check_escape_marker_abuse.sh",
30
+ "sha256": "4f9d8dc91275145204f0bc760c4378dd53f7ad4389d89da737aef23f84b2502b",
31
+ "sourceHook": "check_escape_marker_abuse.sh",
32
+ "bucket": "SHIP_REWRITTEN"
33
+ },
34
+ {
35
+ "file": "hooks/check_field_family_invariants.sh",
36
+ "sha256": "1beeb2b347cacf5d8a912470bce0b9ab6b54c2595873155389456cdbe6125d80",
37
+ "sourceHook": "check_field_family_invariants.sh",
38
+ "bucket": "SHIP_AS_IS"
39
+ },
40
+ {
41
+ "file": "hooks/check_fork_product_quality.sh",
42
+ "sha256": "1985d7607de85ae71b07b4b302c27b3dbb51670bbe68421489e27c75a2b64ffa",
43
+ "sourceHook": "check_substantive_edit_approval_preflight.sh",
44
+ "bucket": "REPLACE"
45
+ },
46
+ {
47
+ "file": "hooks/check_item_list_gap.sh",
48
+ "sha256": "4e779e5894f16873f1bc322f64571a7ff869d3b75ed5383a5b0f662c7b533d99",
49
+ "sourceHook": "check_item_list_gap.sh",
50
+ "bucket": "SHIP_AS_IS"
51
+ },
52
+ {
53
+ "file": "hooks/check_layout_space_magic_numbers.sh",
54
+ "sha256": "09c74c67c61a366b14bb1cce2ac27d4b8d0541a8698f85d50e4afe9783ad7309",
55
+ "sourceHook": "check_layout_space_magic_numbers.sh",
56
+ "bucket": "SHIP_AS_IS"
57
+ },
58
+ {
59
+ "file": "hooks/check_opacity_token_usage.sh",
60
+ "sha256": "1772e83242520f569a9a44e96d08d74e121c119c940c019c8ed50989f14de646",
61
+ "sourceHook": "check_opacity_token_usage.sh",
62
+ "bucket": "SHIP_REWRITTEN"
63
+ },
64
+ {
65
+ "file": "hooks/check_pattern_invariants.sh",
66
+ "sha256": "c6cf3288cbc2bdb689091aef678073470f58b7f1bf98cdefb55afc9724ad277b",
67
+ "sourceHook": "check_pattern_invariants.sh",
68
+ "bucket": "SHIP_AS_IS"
69
+ },
70
+ {
71
+ "file": "hooks/check_sidebar_menu_button_implicit_wrap.sh",
72
+ "sha256": "240807737e9c75bd137a8abd9a7194ffcb4a92b1015f626f14539fd8dbf73ea8",
73
+ "sourceHook": "check_sidebar_menu_button_implicit_wrap.sh",
74
+ "bucket": "SHIP_AS_IS"
75
+ },
76
+ {
77
+ "file": "hooks/check_tailwind_wildcard_in_docs.sh",
78
+ "sha256": "51fcf04d3adc2a15b6c06312753e51412ed8ff88d088a5e8fe4ec0c784c7aa96",
79
+ "sourceHook": "check_tailwind_wildcard_in_docs.sh",
80
+ "bucket": "SHIP_AS_IS"
81
+ },
82
+ {
83
+ "file": "hooks/inject_deploy_url_after_push.sh",
84
+ "sha256": "f5cd8597574f1b061b132a289337731f7dabac0c6666f1ca611d8316504b401c",
85
+ "sourceHook": "inject_deploy_url_after_push.sh",
86
+ "bucket": "SHIP_REWRITTEN"
87
+ },
88
+ {
89
+ "file": "launchers/fork-governance-dispatcher.sh",
90
+ "sha256": "6a99ad2d7404a4d721a611a19e4537ebfe8cf5e2ddba99edc7f6b386d86e8b5d",
91
+ "source": "template/.claude/hooks"
92
+ },
93
+ {
94
+ "file": "launchers/inject_fork_governance_preamble.sh",
95
+ "sha256": "eccf9dbb5ccd3a2e0bea6e3e225de2eb0f1634a6c12c349aab89439cf6e1fb89",
96
+ "source": "template/.claude/hooks"
97
+ },
98
+ {
99
+ "file": "launchers/settings-hooks.json",
100
+ "sha256": "04f328ae97687e112995288bff2c1fa8c9feecf25335f4b63036964209e7b402",
101
+ "source": "template/.claude/settings.json"
102
+ },
103
+ {
104
+ "file": "manifest.json",
105
+ "sha256": "ee4f619b722c7d3a4f80dddb52dcee4bf900c7e6923945cd59d290d4a8236e5a"
106
+ },
107
+ {
108
+ "file": "preamble.md",
109
+ "sha256": "394299047d01f60bf1d9fd14a41e173b31d649068c62bd47352556beab9683e1"
110
+ }
111
+ ]
112
+ }
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+ import time
7
+
8
+ # Per-hook fire logging(enables /knowledge-prune D2 dead-hook detection)
9
+ # Resolve project root from this script's location(stable; cwd may be anywhere
10
+ # depending on how Claude Code invokes the hook)to avoid stray .claude/ trees.
11
+ try:
12
+ _project_root = os.environ.get('CLAUDE_PROJECT_DIR') or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+ _log_dir = os.path.join(_project_root, '.claude', 'logs')
14
+ os.makedirs(_log_dir, exist_ok=True)
15
+ with open(os.path.join(_log_dir, 'hook-fires-per-hook.jsonl'), 'a') as _f:
16
+ _f.write(json.dumps({'ts': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'hook': os.path.basename(__file__)}) + '\n')
17
+ except Exception:
18
+ pass
19
+
20
+ EXPLORATION_PREFIX = "src/explorations/"
21
+ SRC_PREFIX = "src/"
22
+
23
+ def load_event():
24
+ try:
25
+ return json.load(sys.stdin)
26
+ except Exception:
27
+ return None
28
+
29
+ def collect_file_paths(tool_input):
30
+ paths = []
31
+
32
+ if not isinstance(tool_input, dict):
33
+ return paths
34
+
35
+ file_path = tool_input.get("file_path")
36
+ if isinstance(file_path, str):
37
+ paths.append(file_path)
38
+
39
+ edits = tool_input.get("edits")
40
+ if isinstance(edits, list):
41
+ for item in edits:
42
+ if isinstance(item, dict):
43
+ p = item.get("file_path")
44
+ if isinstance(p, str):
45
+ paths.append(p)
46
+
47
+ files = tool_input.get("files")
48
+ if isinstance(files, list):
49
+ for item in files:
50
+ if isinstance(item, dict):
51
+ p = item.get("file_path")
52
+ if isinstance(p, str):
53
+ paths.append(p)
54
+
55
+ return list(dict.fromkeys(paths))
56
+
57
+ def should_check(path):
58
+ if not path.startswith(SRC_PREFIX):
59
+ return False
60
+ if path.startswith(EXPLORATION_PREFIX):
61
+ return False
62
+ if not (path.endswith(".ts") or path.endswith(".tsx")):
63
+ return False
64
+ return True
65
+
66
+ def contains_exploration_import(content):
67
+ patterns = [
68
+ r'from\s+[\'"].*src/explorations/.*[\'"]',
69
+ r'from\s+[\'"].*explorations/.*[\'"]',
70
+ r'import\s+[\'"].*src/explorations/.*[\'"]',
71
+ r'import\s+[\'"].*explorations/.*[\'"]'
72
+ ]
73
+ return any(re.search(p, content) for p in patterns)
74
+
75
+ def main():
76
+ event = load_event()
77
+ if not event:
78
+ sys.exit(0)
79
+
80
+ tool_input = event.get("tool_input", {})
81
+ touched_paths = collect_file_paths(tool_input)
82
+
83
+ offenders = []
84
+
85
+ for path in touched_paths:
86
+ if not should_check(path):
87
+ continue
88
+ if not os.path.exists(path):
89
+ continue
90
+
91
+ try:
92
+ with open(path, "r", encoding="utf-8") as f:
93
+ content = f.read()
94
+ except Exception:
95
+ continue
96
+
97
+ if contains_exploration_import(content):
98
+ offenders.append(path)
99
+
100
+ if offenders:
101
+ lines = [
102
+ "Blocked: 正式程式碼不得 import src/explorations/ 內的檔案。"
103
+ ]
104
+ lines.extend(f"- {p}" for p in offenders)
105
+ print("\n".join(lines))
106
+ sys.exit(2)
107
+
108
+ sys.exit(0)
109
+
110
+ if __name__ == "__main__":
111
+ main()
@@ -0,0 +1,95 @@
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
+ # 2026-06-11 R2 held-item #9:strip 註解再 match — apps/template/src/App.tsx:57 canonical citation
36
+ # 註解({/* ... 禁用 <ItemAvatar> ... */})在 SidebarHeader block 內被當真 JSX 誤發 P0(code 實際正確
37
+ # 用 raw <Avatar size={24}>)。Strip 順序:JSX comment {/* */} → block comment /* */ → 行首 // 整行
38
+ # (不 strip 行內 //,避免 mutilate https:// URL — 對齊 check_story_invariants.sh R9 idiom)。
39
+ HAS_DRIFT=$(printf '%s' "$NEW" | python3 -c '
40
+ import sys, re
41
+ content = sys.stdin.read()
42
+ # Strip comments(citation 註解含 <ItemAvatar> 字樣不是 drift)
43
+ content = re.sub(r"\{/\*.*?\*/\}", "", content, flags=re.DOTALL)
44
+ content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
45
+ content = re.sub(r"(?m)^[ \t]*//.*$", "", content)
46
+ # Find SidebarHeader blocks(opening tag → closing tag,non-greedy)
47
+ blocks = re.findall(r"<SidebarHeader[^>]*>.*?</SidebarHeader>", content, re.DOTALL)
48
+ for block in blocks:
49
+ if re.search(r"<ItemAvatar\b", block):
50
+ print("DRIFT")
51
+ sys.exit(0)
52
+ ' 2>/dev/null)
53
+
54
+ [ "$HAS_DRIFT" != "DRIFT" ] && exit 0
55
+
56
+ # Override env var
57
+ if [ "${CLAUDE_BYPASS_CHROME_HEADER_AVATAR:-0}" = "1" ]; then
58
+ mkdir -p "$(dirname "$0")/../logs" 2>/dev/null
59
+ printf '{"ts":"%s","event":"chrome-header-avatar-bypass","file":"%s"}\n' \
60
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$FILE_PATH" >> "$(dirname "$0")/../logs/governance-bypass.jsonl" 2>/dev/null
61
+ exit 0
62
+ fi
63
+
64
+ REL=${FILE_PATH#"$CLAUDE_PROJECT_DIR"/}
65
+
66
+ cat >&2 <<EOF
67
+ 🚨 Chrome header avatar canonical violation(per user 2026-05-27 抓 + codex collab cite battle):
68
+
69
+ 📁 File: $REL
70
+ 🔍 偵測:<SidebarHeader> block 內含 <ItemAvatar>(禁用)
71
+
72
+ Canonical citation:
73
+ - header-canonical.spec.md:57-72:「Chrome header 不是 row context → 必用 raw <Avatar size={24}>,禁用 <ItemAvatar>(會誤啟動 row anatomy lookup)」
74
+ - sidebar.spec.md:241-247:「consumer 用 raw <Avatar size={24}>(chrome header 不是 row context → 不該用 <ItemAvatar>)」
75
+ - item-anatomy.spec.md:513-537:「ItemAvatar scope = row context only;Chrome header 有自己的 canonical = raw <Avatar size={24}>」
76
+
77
+ 修法(per DS canonical):
78
+ ❌ <SidebarHeader>
79
+ <ItemAvatar alt="..." shape="square" color="..." solid />
80
+ <span>brand</span>
81
+ </SidebarHeader>
82
+
83
+ ✅ <SidebarHeader>
84
+ <Avatar size={24} shape="square" color="..." solid alt="..." />
85
+ <span className="text-body-lg font-medium truncate">brand</span>
86
+ </SidebarHeader>
87
+
88
+ 注意:SidebarFooter 內 ItemAvatar OK(footer 是 SidebarMenu row context)。本 hook 只攔 SidebarHeader。
89
+
90
+ Bypass(極罕見):CLAUDE_BYPASS_CHROME_HEADER_AVATAR=1 env var(audit-logged)。
91
+ EOF
92
+ # 2026-05-31:exit 0 → exit 2(folded-hook-audit:原宣稱 BLOCKER 但 exit 0 = 假 enforcement;
93
+ # chrome-header avatar 是 SSOT canonical [feedback_ssot_mechanical_p0_not_p1 = 必 P0 BLOCK],
94
+ # verified clean on 現有 canonical sidebar code + 有 env escape 兜 false-positive)。
95
+ exit 2
@@ -0,0 +1,349 @@
1
+ #!/bin/bash
2
+ # check_consumer_app_invariants.sh — P0 BLOCKER ×4 — consumer app DS 使用紀律(2026-05-27/28 user directive 家族)
3
+ #
4
+ # 2026-06-11 prune merge(user 拍板「照你建議做」;59→51 headroom):
5
+ # # r1_no_ds_catalog = 原 check_consumer_no_ds_catalog.sh(規則逐字搬入,BLOCKER 級別與 escape 標記不變)
6
+ # r2_story_baseline = 原 check_consumer_story_baseline.sh(規則逐字搬入,BLOCKER 級別與 escape 標記不變)
7
+ # r3_ds_primitive_misuse = 原 check_consumer_ds_primitive_misuse.sh(規則逐字搬入,BLOCKER 級別與 escape 標記不變)
8
+ # r4_app_story_title = 原 check_consumer_app_story_title.sh(規則逐字搬入,BLOCKER 級別與 escape 標記不變)
9
+ # 原檔 → .claude/hooks/retired/2026-06-11-prune-merge/
10
+ # 各規則跑在 pipeline 子 shell:規則內 exit 不中斷其他規則;任一 exit 2 → 整體 exit 2。
11
+
12
+ source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
13
+
14
+ set -uo pipefail
15
+ INPUT=$(cat 2>/dev/null || echo "{}")
16
+
17
+ r1_no_ds_catalog() {
18
+ set -uo pipefail
19
+
20
+ INPUT=$(cat 2>/dev/null || echo "{}")
21
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
22
+
23
+ case "${TOOL:-}" in
24
+ Edit|Write|MultiEdit) ;;
25
+ *) exit 0 ;;
26
+ esac
27
+
28
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
29
+ # Only check consumer storybook files
30
+ if ! echo "$FILE" | grep -qE '(^|/)(apps|consumer)/.*\.stories\.tsx$'; then exit 0; fi
31
+ # Skip DS source
32
+ if echo "$FILE" | grep -qE 'packages/design-system/src/'; 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 — 2026-06-03 修(同 R8 fragment-vs-file bug class):Edit 只送 new_string 片段,
38
+ # 但 @consumer-catalog-allow marker 在檔頭(不在每次 edit 的片段裡)→ 編輯有 marker 的 portal 檔
39
+ # 任一非 marker 行就被誤擋。本 hook 是 PostToolUse(檔已落 disk)→ 補查整檔 marker。
40
+ if echo "$CONTENT" | grep -q '@consumer-catalog-allow:'; then exit 0; fi
41
+ if [ -f "$FILE" ] && grep -q '@consumer-catalog-allow:' "$FILE" 2>/dev/null; then exit 0; fi
42
+
43
+ VIOLATIONS=""
44
+
45
+ # Pattern 1: file basename forbidden
46
+ basename=$(basename "$FILE" .stories.tsx)
47
+ if echo "$basename" | grep -qE '^(EveryDsComponent|AllDsComponents|AllComponents|DsCatalog|EveryComponent)$'; then
48
+ # AllDsComponents allowed IF it's only portal proxy (check title)
49
+ if [ "$basename" = "AllDsComponents" ] && echo "$CONTENT" | grep -qE 'DsCanonicalPortal|iframe.*design-system|@consumer-catalog-allow'; then
50
+ : # portal proxy OK
51
+ else
52
+ VIOLATIONS="${VIOLATIONS} - File basename '$basename' = catalog pattern. PW 不該重寫 DS catalog.\n"
53
+ fi
54
+ fi
55
+
56
+ # Pattern 2: title claims per-component default
57
+ if echo "$CONTENT" | grep -qE "title:.*['\"](所有 DS 元件|Every DS Component|All DS Components.*render|每元件 default)"; then
58
+ VIOLATIONS="${VIOLATIONS} - Story title claims per-component default render. PW catalog 只可 import smoke + DS portal proxy.\n"
59
+ fi
60
+
61
+ # Pattern 3: iterate-render anti-pattern
62
+ if echo "$CONTENT" | grep -qE 'Object\.keys\(DS\)\.(map|forEach)' || \
63
+ echo "$CONTENT" | grep -qE 'Object\.entries\(DS\)\.(map|forEach)'; then
64
+ VIOLATIONS="${VIOLATIONS} - Detected Object.keys/entries(DS).map iterate-render pattern. 禁 iterate render DS exports.\n"
65
+ fi
66
+
67
+ # Pattern 4: mass hand-mock(≥5 different <DS.X> tags in same file)
68
+ DS_TAG_COUNT=$(echo "$CONTENT" | grep -oE '<DS\.[A-Z][a-zA-Z]+' | sort -u | wc -l | tr -d ' ')
69
+ if [ "$DS_TAG_COUNT" -ge 5 ]; then
70
+ 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"
71
+ fi
72
+
73
+ if [ -n "$VIOLATIONS" ]; then
74
+ cat >&2 << EOF
75
+ 🚨 CONSUMER-NO-DS-CATALOG BLOCKER(P0,2026-05-27 user 永久 directive「確保跟 ds repo 一模一樣」+ M31 codex synthesis)
76
+
77
+ Consumer file $FILE 違反:
78
+ $(echo -e "$VIOLATIONS")
79
+ per M31 codex synthesis SSOT:
80
+ - DS owns per-component canonical pixels(62/62 components ×3 tiers stories in DS Storybook)
81
+ - PW(consumer)owns 真實業務 composition demos(AppShell Dashboard etc.)
82
+ - Catalog → DS canonical Storybook iframe/link proxy,**禁** PW 重寫 <DS.X minimal mock>
83
+
84
+ 歷史錨點 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.
85
+
86
+ 修法 2 選 1:
87
+ (a) 改用 DS canonical Storybook iframe portal(per template AllDsComponents.stories.tsx#DsCanonicalPortal pattern)
88
+ (b) Escape:加 \`// @consumer-catalog-allow: <rationale>\` 顯式 documented
89
+
90
+ 完整 SSOT → DS package ds-story-manifest.json + codex M31 synthesis output /tmp/codex-ssot-output.txt
91
+ EOF
92
+ exit 2
93
+ fi
94
+
95
+ exit 0
96
+ }
97
+
98
+ r2_story_baseline() {
99
+ set -uo pipefail
100
+
101
+ INPUT=$(cat 2>/dev/null || echo "{}")
102
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
103
+
104
+ case "${TOOL:-}" in
105
+ Edit|Write|MultiEdit) ;;
106
+ *) exit 0 ;;
107
+ esac
108
+
109
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
110
+ # Only check consumer storybook files
111
+ if ! echo "$FILE" | grep -qE '(^|/)(apps|consumer)/.*\.stories\.tsx$'; then exit 0; fi
112
+ if echo "$FILE" | grep -qE 'packages/design-system/src/'; then exit 0; fi
113
+
114
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
115
+ [ -z "$CONTENT" ] && exit 0
116
+
117
+ # Escape clauses
118
+ if echo "$CONTENT" | grep -qE '@story-baseline-allow:|@consumer-catalog-allow:'; then exit 0; fi
119
+
120
+ # High-risk DS primitives requiring baseline marker
121
+ HIGH_RISK_PRIMITIVES='DataTable|Dialog|Sheet|Popover|DropdownMenu|Tooltip|HoverCard|LinkInput|RadioGroup|CircularProgress|AppShell|Sidebar'
122
+
123
+ # Detect usage
124
+ USED=$(echo "$CONTENT" | grep -oE "<DS\.($HIGH_RISK_PRIMITIVES)\\b" | sort -u | head -10)
125
+
126
+ if [ -z "$USED" ]; then exit 0; fi
127
+
128
+ # Check for @story-baseline: marker
129
+ if echo "$CONTENT" | grep -qE '@story-baseline:[[:space:]]*\S'; then exit 0; fi
130
+
131
+ cat >&2 << EOF
132
+ 🚨 CONSUMER-STORY-BASELINE BLOCKER(P0,2026-05-27 M31 codex synthesis)
133
+
134
+ Consumer file $FILE 用高風險 DS primitive 但無 \`// @story-baseline:\` marker:
135
+ $(echo "$USED" | sed 's/^/ /')
136
+
137
+ per M31 codex synthesis SSOT:「Consumer wrap 高風險 DS primitive 必 @story-baseline:
138
+ marker,由 CI 對 DS canonical story 做 visual diff」.
139
+
140
+ High-risk list:DataTable / Dialog / Sheet / Popover / DropdownMenu / Tooltip /
141
+ HoverCard / LinkInput / RadioGroup / CircularProgress / AppShell / Sidebar.
142
+
143
+ 修法 2 選 1:
144
+ (a) 加 marker(檔頭或 story body):
145
+ // @story-baseline: @qijenchen/design-system/components/<Name>/<name>.stories.tsx#<ExportName>
146
+ 例:// @story-baseline: @qijenchen/design-system/components/Sidebar/sidebar.stories.tsx#IconCollapse
147
+ (b) Escape:\`// @story-baseline-allow: <rationale>\` 顯式 documented exception
148
+ (eg. pure behavior test / per ds-story-manifest.json exception list)
149
+
150
+ 完整 mapping → packages/design-system/ds-story-manifest.json(DS package ship)
151
+ EOF
152
+ exit 2
153
+ }
154
+
155
+ r3_ds_primitive_misuse() {
156
+ set -uo pipefail
157
+
158
+ INPUT=$(cat 2>/dev/null || echo "{}")
159
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
160
+
161
+ case "${TOOL:-}" in
162
+ Edit|Write|MultiEdit) ;;
163
+ *) exit 0 ;;
164
+ esac
165
+
166
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
167
+ # Cover BOTH stories AND production .tsx in consumer apps
168
+ if ! echo "$FILE" | grep -qE '(^|/)(apps|consumer)/.*\.(tsx|ts)$'; then exit 0; fi
169
+ if echo "$FILE" | grep -qE 'packages/design-system/src/|node_modules/'; then exit 0; fi
170
+
171
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
172
+ [ -z "$CONTENT" ] && exit 0
173
+
174
+ # 2026-06-03 修(同 R8 bug class):換行→空格 flatten。真實 JSX 屬性跨行(<DS.X\n size={N}\n/>),
175
+ # grep 逐行 + 各 pattern 用 [^>]+ 跨屬性匹配 → 不 flatten 的話多行 component 靜默繞過全部 anti-pattern 檢查
176
+ # (= BLOCKER false-negative,consumer DS misuse 沒被擋)。[^>]+ 自帶 tag 邊界(遇 > 停),flatten 後不會跨 component。
177
+ CONTENT=$(echo "$CONTENT" | tr '\n' ' ')
178
+
179
+ # Global escape — file-wide allowlist
180
+ if echo "$CONTENT" | grep -q '@ds-misuse-allow:'; then exit 0; fi
181
+
182
+ VIOLATIONS=""
183
+
184
+ # Pattern 1: <CircularProgress size={N}> with literal number (override default 24)
185
+ if echo "$CONTENT" | grep -qE '<DS\.CircularProgress[^>]+size=\{[0-9]+\}'; then
186
+ VIOLATIONS="${VIOLATIONS} - <CircularProgress size={N}> hardcoded number override default 24 (per circular-progress.spec.md:101)\n"
187
+ fi
188
+
189
+ # Pattern 2: <RadioGroupItem> NOT wrapped in <SelectionItem control={...}>
190
+ # Approximation: file uses RadioGroupItem but doesn't reference SelectionItem
191
+ if echo "$CONTENT" | grep -qE '<DS\.RadioGroupItem\b' && ! echo "$CONTENT" | grep -qE 'SelectionItem|<DS\.RadioGroupItem[^>]+label='; then
192
+ VIOLATIONS="${VIOLATIONS} - <RadioGroupItem> 沒 wrap <SelectionItem control={<RadioGroupItem>}> (per selection-item.spec.md:23 SSOT spacing/padding)\n"
193
+ fi
194
+
195
+ # Pattern 3: <DataTable columns={[…]}> with literal single column
196
+ if echo "$CONTENT" | grep -qE '<DS\.DataTable[^>]+columns=\{\[\s*\{[^}]+\}\s*\]\}' && ! echo "$CONTENT" | grep -qE 'columns=\{[^}]*\},\s*\{'; then
197
+ VIOLATIONS="${VIOLATIONS} - <DataTable columns={[single-col]}> minimal one-column = 違反 data-table.spec.md canonical(min 2 cols for meaningful render)\n"
198
+ fi
199
+
200
+ # Pattern 4: <LinkInput placeholder=...> without value prop
201
+ if echo "$CONTENT" | grep -qE '<DS\.LinkInput[^>]+placeholder=' && ! echo "$CONTENT" | grep -qE '<DS\.LinkInput[^>]+(value|defaultValue)='; then
202
+ VIOLATIONS="${VIOLATIONS} - <LinkInput placeholder=...> 沒 value prop = placeholder-only mode 抹平 link/edit canonical (per link-input.spec.md:18,48-58)\n"
203
+ fi
204
+
205
+ # Pattern 5: <Empty title=...> without icon and without description
206
+ if echo "$CONTENT" | grep -qE '<DS\.Empty[^>]+title=' && \
207
+ ! echo "$CONTENT" | grep -qE '<DS\.Empty[^>]+icon=' && \
208
+ ! echo "$CONTENT" | grep -qE '<DS\.Empty[^>]+description='; then
209
+ VIOLATIONS="${VIOLATIONS} - <Empty title=...> 無 icon 無 description = 違反 Empty.tsx:11「預設只需 description」minimal mock looks weird\n"
210
+ fi
211
+
212
+ # Pattern 8: 硬寫色值 / 字級 / shadow 繞過 DS token(2026-06-02 CF conformance-model 補主防線 —
213
+ # composition-fidelity 從 pixel-identity 收窄成 identity-opt-in 後,「consumer 用對 DS token」改由靜態
214
+ # conformance 防線保證,對齊 Polaris stylelint-polaris / Atlassian eslint-plugin / Carbon stylelint。
215
+ # 既有 check_layout_space_magic_numbers 守「間距」;此 pattern 補「色值/字級/shadow」缺口。
216
+ # 零誤判優先:只抓 hardcoded(`-[var(--...)]` token 用法不匹配)。
217
+ 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
218
+ 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"
219
+ fi
220
+
221
+ # Pattern 9(2026-06-12,user 抓 fork「四不像」G4 補洞):AppShell slot 餵 raw HTML element。
222
+ # app-shell.spec.md:296-299 明文禁 sidebar={<div>}/header={<header>},原註「靠 audit 把關」
223
+ # 無機械閘 → 兌現成 P0(per memory feedback_ssot_mechanical_p0_not_p1_warn)。
224
+ # 零誤判:雙條件 = 同檔有 <DS.AppShell> + slot 屬性直接餵 raw tag(div/header/nav/aside/section)。
225
+ if echo "$CONTENT" | grep -qE '<DS\.AppShell\b' && \
226
+ echo "$CONTENT" | grep -qE '\b(header|sidebar|aside)=\{ *<(div|header|nav|aside|section)\b'; then
227
+ VIOLATIONS="${VIOLATIONS} - <AppShell header/sidebar/aside={<raw element>}> — slot 必餵 DS 元件(ChromeHeader / Sidebar / AppShellAside),raw div/header 漏接 border/scroll/responsive canonical(per app-shell.spec.md:296-299)\n"
228
+ fi
229
+
230
+ # Pattern 10(2026-06-16,user 抓 fork prototype 手刻 table 不用 DataTable):RAW PRIMITIVE 手刻
231
+ # r3 既有 Pattern 1-9 只抓「DS 元件用錯」(<DS.X> 已在用但用法錯),漏掉**根本沒用 DS 元件、亂刻
232
+ # raw HTML** —— 即 mindset #2 +「# SSOT 消費 canonical」+ build-ui-canonicals.md:9「命中既有元件
233
+ # → 必消費,不 hand-craft raw HTML 繞過」這條**必定遵循大原則**的機械閘缺口。反 pattern 由
234
+ # build-ui-canonicals.md ❌→✅ 對照表(SSOT)驅動。零誤判:只抓高信心 raw-tag 訊號;<DS.X> 元件是
235
+ # PascalCase + DS. prefix 不匹配小寫 raw tag;node_modules 已於上方排除;有理由可 @ds-misuse-allow escape。
236
+ if echo "$CONTENT" | grep -qE '<table\b' && echo "$CONTENT" | grep -qE '<thead\b|<tbody\b|<th\b'; then
237
+ VIOLATIONS="${VIOLATIONS} - 手刻 raw <table><thead>/<tbody> 資料表 → 必用 <DataTable columns={...} data={...} />(build-ui-canonicals.md:18 ❌→✅ SSOT;這是「優先消費既有元件」大原則,無理由不得手刻)\n"
238
+ fi
239
+ if echo "$CONTENT" | grep -qE '<img\b[^>]*rounded-full'; then
240
+ VIOLATIONS="${VIOLATIONS} - 手刻 <img ... rounded-full> 頭像 → 必用 <Avatar>(build-ui-canonicals.md:25)\n"
241
+ fi
242
+ if echo "$CONTENT" | grep -qE '<select\b'; then
243
+ VIOLATIONS="${VIOLATIONS} - native <select> 手刻下拉 → 必用 <Select> / <DropdownMenu>(build-ui-canonicals.md:15)\n"
244
+ fi
245
+ if echo "$CONTENT" | grep -qE '<hr\b'; then
246
+ VIOLATIONS="${VIOLATIONS} - 手刻 <hr> 分隔線 → 用 <Separator>(或 separator.spec 允許的 CSS border)(build-ui-canonicals.md:23)\n"
247
+ fi
248
+
249
+ # Pattern 6: Overlay trigger without defaultOpen state for visual demo
250
+ # (Skip in production .tsx; only enforce in .stories.tsx where visual snapshot matters)
251
+ if echo "$FILE" | grep -qE '\.stories\.tsx$'; then
252
+ for overlay in Tooltip Popover Dialog Sheet DropdownMenu; do
253
+ if echo "$CONTENT" | grep -qE "<DS\.${overlay}\b" && \
254
+ ! echo "$CONTENT" | grep -qE "(defaultOpen|open=\{(true|isOpen)\})"; then
255
+ VIOLATIONS="${VIOLATIONS} - Story uses <${overlay}> without defaultOpen — visual audit can't see overlay content\n"
256
+ fi
257
+ done
258
+ fi
259
+
260
+ if [ -n "$VIOLATIONS" ]; then
261
+ cat >&2 << EOF
262
+ 🚨 CONSUMER-DS-PRIMITIVE-MISUSE BLOCKER(P0,2026-05-27 user verbatim「做產品真的要能使用跟 ds repo 一模一樣的元件」)
263
+
264
+ File $FILE detected anti-pattern DS API usage:
265
+ $(echo -e "$VIOLATIONS")
266
+ per M31 codex synthesis SSOT + DS spec.md citations(file:line 在每條 violation).
267
+
268
+ Anchor:user 2026-05-27 抓 7 個 visual bug 全 root cause = consumer minimal-mock 抹平
269
+ DS canonical 設計意圖。本 hook 攔 production 重犯同 pattern。
270
+
271
+ 修法 2 選 1:
272
+ (a) 改用 DS canonical pattern(per file:line cited spec).
273
+ (b) Escape:加 \`// @ds-misuse-allow: <rationale>\` 顯式 documented per file OR per line.
274
+
275
+ Per-bug fix paths → /tmp/codex-ssot-output.txt(M31 codex synthesis 2026-05-27)
276
+ EOF
277
+ exit 2
278
+ fi
279
+
280
+ exit 0
281
+ }
282
+
283
+ r4_app_story_title() {
284
+ set -uo pipefail
285
+
286
+ INPUT=$(cat 2>/dev/null || echo "{}")
287
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
288
+
289
+ case "${TOOL:-}" in
290
+ Edit|Write|MultiEdit) ;;
291
+ *) exit 0 ;;
292
+ esac
293
+
294
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
295
+ # Scope:apps/<name>/**/*.stories.(tsx|ts|mdx)
296
+ if ! echo "$FILE" | grep -qE '(^|/)apps/[^/]+/.+\.stories\.(tsx|ts|mdx)$'; then exit 0; fi
297
+
298
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
299
+ [ -z "$CONTENT" ] && exit 0
300
+
301
+ # Escape clause
302
+ if echo "$CONTENT" | grep -qE '@app-story-title-skip:'; then exit 0; fi
303
+
304
+ # Extract expected app name from file path
305
+ APP_NAME=$(echo "$FILE" | sed -E 's|.*/apps/([^/]+)/.*|\1|')
306
+ [ -z "$APP_NAME" ] && exit 0
307
+
308
+ # Find title field(支援 single/double/backtick quote)
309
+ TITLE_LINE=$(echo "$CONTENT" | grep -oE "title:\s*['\"\`][^'\"\`]+['\"\`]" | head -1)
310
+
311
+ # 若無 title field,skip(no-op stories OK)
312
+ [ -z "$TITLE_LINE" ] && exit 0
313
+
314
+ EXPECTED_PREFIX="Apps/${APP_NAME}/"
315
+
316
+ # Check title 是否開頭 `Apps/<app-name>/`
317
+ if ! echo "$TITLE_LINE" | grep -qE "title:\s*['\"\`]Apps/${APP_NAME}/"; then
318
+ cat >&2 << EOF
319
+ 🚨 CONSUMER APP STORY TITLE BLOCKER(P0,2026-05-28 codify per create-app duplicate-id anchor)
320
+
321
+ File: $FILE
322
+ Detected title: $TITLE_LINE
323
+ Expected prefix: \`title: 'Apps/${APP_NAME}/...'\`
324
+
325
+ Why blocked:
326
+ Consumer apps 內 stories 必用 \`Apps/<app-name>/<page-purpose>\` 開頭 namespace
327
+ (per .claude/rules/story-rules.md「Title 命名 2-namespace canonical」)。
328
+ 錯 prefix → Storybook glob 撈到後與 template/其他 app 撞 id → build duplicate
329
+ warning + 只顯第一個 → 新 app 在 sidebar 不可見。
330
+
331
+ Anchor:2026-05-28 npm run create-app 不改 story title 導致 e2e 抓 4 個 collisions。
332
+
333
+ Fix:
334
+ title: 'Apps/${APP_NAME}/<Your Page Purpose>' // ex: 'Apps/${APP_NAME}/Dashboard'
335
+
336
+ Escape(極罕見):add \`// @app-story-title-skip: <rationale>\`
337
+ EOF
338
+ exit 2
339
+ fi
340
+
341
+ exit 0
342
+ }
343
+
344
+ for _rule in r1_no_ds_catalog r2_story_baseline r3_ds_primitive_misuse r4_app_story_title; do
345
+ echo "$INPUT" | "$_rule"
346
+ _rc=$?
347
+ if [ "$_rc" -eq 2 ]; then exit 2; fi
348
+ done
349
+ exit 0