@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.
- package/ds-canonical/fork/governance.lock +112 -0
- package/ds-canonical/fork/hooks/block_prototype_imports.py +111 -0
- package/ds-canonical/fork/hooks/check_chrome_header_avatar_canonical.sh +95 -0
- package/ds-canonical/fork/hooks/check_consumer_app_invariants.sh +349 -0
- package/ds-canonical/fork/hooks/check_ds_anchor_preflight.sh +132 -0
- package/ds-canonical/fork/hooks/check_escape_marker_abuse.sh +140 -0
- package/ds-canonical/fork/hooks/check_field_family_invariants.sh +250 -0
- package/ds-canonical/fork/hooks/check_fork_product_quality.sh +36 -0
- package/ds-canonical/fork/hooks/check_item_list_gap.sh +54 -0
- package/ds-canonical/fork/hooks/check_layout_space_magic_numbers.sh +96 -0
- package/ds-canonical/fork/hooks/check_opacity_token_usage.sh +149 -0
- package/ds-canonical/fork/hooks/check_pattern_invariants.sh +196 -0
- package/ds-canonical/fork/hooks/check_sidebar_menu_button_implicit_wrap.sh +86 -0
- package/ds-canonical/fork/hooks/check_tailwind_wildcard_in_docs.sh +79 -0
- package/ds-canonical/fork/hooks/inject_deploy_url_after_push.sh +238 -0
- package/ds-canonical/fork/launchers/fork-governance-dispatcher.sh +44 -0
- package/ds-canonical/fork/launchers/inject_fork_governance_preamble.sh +46 -0
- package/ds-canonical/fork/launchers/settings-hooks.json +58 -0
- package/ds-canonical/fork/manifest.json +79 -0
- package/ds-canonical/fork/preamble.md +495 -0
- package/ds-canonical/hooks/check_consumer_app_invariants.sh +19 -0
- package/ds-canonical/references/scenario-definition.md +4 -4
- package/ds-canonical/skills/design-system-audit/SKILL.md +1 -1
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +5 -1
- package/src/story-governance/category-matrix.json +132 -0
- 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
|