@qijenchen/design-system 0.1.0-beta.68 → 0.1.0-beta.70
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/dist/components/Steps/steps.d.ts.map +1 -1
- package/dist/components/Steps/steps.js +11 -3
- package/dist/components/Steps/steps.js.map +1 -1
- package/ds-canonical/fork/governance.lock +112 -0
- package/ds-canonical/fork/hooks/__pycache__/block_prototype_imports.cpython-312.pyc +0 -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 +3 -1
- package/src/components/Steps/steps.tsx +11 -3
- package/src/story-governance/category-matrix.json +132 -0
- package/src/tokens/utility-registry.json +124 -0
|
@@ -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
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_ds_anchor_preflight.sh — M29 mechanical enforcement(2026-05-26 backfill per user verbatim
|
|
3
|
+
# 「該程式化的都沒程式化,導致你他媽那麼容易便宜」+「未來其他人 fork 用其他元件也偏移」)。
|
|
4
|
+
#
|
|
5
|
+
# Purpose: PreToolUse Edit/Write/MultiEdit 偵測 production tsx wrap DS primitive
|
|
6
|
+
# (`<Sidebar>` / `<AppShell>` / `<DataTable>` / `<Dialog>` / `<Field>` 等)
|
|
7
|
+
# 沒同 turn 跑過 Grep / Read DS canonical baseline → 軟 BLOCKER 提醒 propose 前必出 3-column owner table。
|
|
8
|
+
#
|
|
9
|
+
# Scope:同 check_substantive_edit_approval_preflight.sh extended scope
|
|
10
|
+
# - node_modules/@qijenchen/design-system/src/**.tsx (DS internal)
|
|
11
|
+
# - apps/**.tsx (consumer fork-user code) ← M29 gap absorption(2026-05-26)
|
|
12
|
+
# - node_modules/@qijenchen/design-system/** (禁改)
|
|
13
|
+
#
|
|
14
|
+
# Excluded: *.stories.tsx (story_invariants R7/R8 already cover) / *.test.tsx / *.spec.md.
|
|
15
|
+
#
|
|
16
|
+
# Why mechanical:M29 anchor preflight 在 meta-patterns.md 喊很久,5 個文件 reference but file 0,
|
|
17
|
+
# 結果 2026-05-26 product-workspace App.tsx mock-drift 沒被攔 → user 抓「肌肉沒長出來」。
|
|
18
|
+
# 本 hook 將「propose / write 視覺結構 前必 grep DS spec.md / canonical story」從 mindset 升 mechanical。
|
|
19
|
+
#
|
|
20
|
+
# Detection:scan transcript 過去 ~30 turns:
|
|
21
|
+
# (a) 有 Grep / Read tool call hit `node_modules/@qijenchen/design-system/src/**/*.spec.md` OR
|
|
22
|
+
# `**/*.stories.tsx`(canonical baseline) → PASS
|
|
23
|
+
# (b) 無 → soft BLOCKER inject context 提醒 grep canonical 出 3-column owner table。
|
|
24
|
+
#
|
|
25
|
+
# 對齊 meta-patterns.md M29 「視覺/結構 propose 前必 grep DS spec.md 找 owner SSOT(M29)— 出 3-column 表」+
|
|
26
|
+
# .claude/rules/self-verify.md Pre-edit phase「M29 3-column owner table」。
|
|
27
|
+
|
|
28
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
29
|
+
|
|
30
|
+
set -uo pipefail
|
|
31
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
32
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
33
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
34
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null)
|
|
35
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
|
|
36
|
+
|
|
37
|
+
[ "$EVENT" != "PreToolUse" ] && exit 0
|
|
38
|
+
case "$TOOL" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac
|
|
39
|
+
|
|
40
|
+
# Scope:DS production code + consumer fork-user app code(不含 stories / spec / test)
|
|
41
|
+
case "$FILE_PATH" in
|
|
42
|
+
*/node_modules/@qijenchen/design-system/src/*.tsx) ;;
|
|
43
|
+
*/apps/*.tsx) ;;
|
|
44
|
+
*/node_modules/@qijenchen/design-system/*.tsx) ;;
|
|
45
|
+
*) exit 0 ;;
|
|
46
|
+
esac
|
|
47
|
+
|
|
48
|
+
case "$FILE_PATH" in
|
|
49
|
+
*.stories.tsx|*.test.tsx|*.spec.md|*.spec.ts) exit 0 ;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
52
|
+
# Extract content being written/edited
|
|
53
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""' 2>/dev/null)
|
|
54
|
+
|
|
55
|
+
# DS primitive 名單(wrap 這些就觸發 anchor preflight)
|
|
56
|
+
DS_PRIMITIVES_RE='<(Sidebar|AppShell|DataTable|Dialog|Sheet|Popover|DropdownMenu|Field|FieldControlGroup|MenuItem|ItemAvatar|ItemLabel|ItemIcon|SegmentedControl|Tabs|TabsList|TabsTrigger|Combobox|Select|DatePicker|TimePicker|TreeView|Tooltip|Coachmark|FileViewer|ScrollArea|Avatar|Badge|Button|ChromeHeader|SurfaceHeader|SurfaceBody|SurfaceFooter|OverlaySurface|NameCard|Toast|FileUpload|DescriptionList|Chart|BulkActionBar|ActionBar|Carousel|Breadcrumb)\b'
|
|
57
|
+
|
|
58
|
+
# 沒 wrap DS primitive → 跳過(可能是純 utility / hook code)
|
|
59
|
+
if ! echo "$NEW_CONTENT" | grep -qE "$DS_PRIMITIVES_RE"; then
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
[ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ] && exit 0
|
|
64
|
+
|
|
65
|
+
# Scan last ~30 turns(~600 lines transcript)— 找 Grep/Read 對 DS spec.md / canonical story 的 trace
|
|
66
|
+
RECENT_TRANSCRIPT=$(tail -600 "$TRANSCRIPT_PATH" 2>/dev/null)
|
|
67
|
+
|
|
68
|
+
# Pass condition(a):有 Grep tool call pattern hit spec.md / stories.tsx
|
|
69
|
+
HAS_CANONICAL_READ=$(echo "$RECENT_TRANSCRIPT" | \
|
|
70
|
+
jq -r 'select(.message.content != null) |
|
|
71
|
+
.message.content // empty |
|
|
72
|
+
if type == "array" then
|
|
73
|
+
(.[]? | select(.type == "tool_use") |
|
|
74
|
+
select(.name == "Grep" or .name == "Read" or .name == "Glob") |
|
|
75
|
+
.input | tostring)
|
|
76
|
+
else empty
|
|
77
|
+
end' 2>/dev/null | \
|
|
78
|
+
grep -cE 'node_modules/@qijenchen/design-system/src/.*\.(spec\.md|stories\.tsx)|stories\.tsx#' 2>/dev/null)
|
|
79
|
+
HAS_CANONICAL_READ=${HAS_CANONICAL_READ:-0}
|
|
80
|
+
|
|
81
|
+
# Pass condition(b):AI 已 written 3-column owner table OR cite `@story-baseline:` marker
|
|
82
|
+
HAS_3COL_OR_MARKER=$(echo "$RECENT_TRANSCRIPT" | \
|
|
83
|
+
grep -cE '@story-baseline:|owner spec|canonical sentence|3-column' 2>/dev/null)
|
|
84
|
+
HAS_3COL_OR_MARKER=${HAS_3COL_OR_MARKER:-0}
|
|
85
|
+
|
|
86
|
+
# Pass: 有任一 canonical read OR 3-column marker → silent
|
|
87
|
+
if [ "$HAS_CANONICAL_READ" -gt 0 ] || [ "$HAS_3COL_OR_MARKER" -gt 0 ]; then
|
|
88
|
+
exit 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Override env var(audit-logged)
|
|
92
|
+
if [ "${CLAUDE_BYPASS_DS_ANCHOR:-0}" = "1" ]; then
|
|
93
|
+
mkdir -p "$(dirname "$0")/../logs" 2>/dev/null
|
|
94
|
+
printf '{"ts":"%s","event":"ds-anchor-bypass","file":"%s","tool":"%s"}\n' \
|
|
95
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$FILE_PATH" "$TOOL" >> "$(dirname "$0")/../logs/governance-bypass.jsonl" 2>/dev/null
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
REL_PATH=${FILE_PATH#"$CLAUDE_PROJECT_DIR"/}
|
|
100
|
+
|
|
101
|
+
# Soft BLOCKER(對齊 check_substantive_edit_approval_preflight.sh hybrid pattern)
|
|
102
|
+
cat >&2 <<EOF
|
|
103
|
+
🚨 M29 DS Anchor Preflight — visual/structural edit 偵測
|
|
104
|
+
|
|
105
|
+
📁 File: $REL_PATH
|
|
106
|
+
🔧 Tool: $TOOL
|
|
107
|
+
🧩 偵測到 DS primitive 在 new content:wrap pattern
|
|
108
|
+
|
|
109
|
+
⚠️ 過去 ~30 turns 無 Grep/Read 對 \`node_modules/@qijenchen/design-system/src/**/spec.md\` 或
|
|
110
|
+
\`*.stories.tsx\` baseline 的 trace,且未見 \`@story-baseline:\` marker 或 3-column owner table。
|
|
111
|
+
|
|
112
|
+
→ 這違反 M29 anchor preflight invariant + story-rules.md「Production-grade composition fidelity」。
|
|
113
|
+
→ 對 fork-user 後果:憑記憶寫 simplified mock(像 2026-05-26 App.tsx SidebarTrigger 漏 / collapsible 漏 / startIcon 漏)。
|
|
114
|
+
|
|
115
|
+
修法 — 2 選 1(對齊 check_substantive_edit_approval_preflight.sh hybrid):
|
|
116
|
+
(a) 先 Grep / Read DS canonical:
|
|
117
|
+
- \`node_modules/@qijenchen/design-system/src/<Component>/*.spec.md\`(找 owner SSOT)
|
|
118
|
+
- \`node_modules/@qijenchen/design-system/src/<Component>/*.stories.tsx\`(找完整佈局 baseline)
|
|
119
|
+
然後 inline 寫 3-column owner table(\`owner spec\` / \`canonical sentence\` / \`conflicting code\`)
|
|
120
|
+
或加 \`// @story-baseline: <path>#<StoryName>\` marker 在檔頭。
|
|
121
|
+
(b) Bypass:\`CLAUDE_BYPASS_DS_ANCHOR=1\` env var(audit-logged in governance-bypass.jsonl)
|
|
122
|
+
僅當你**真的**已 Read canonical 但 transcript scan miss 時用,not 規則繞道。
|
|
123
|
+
|
|
124
|
+
對應 canonical:
|
|
125
|
+
- \`.claude/rules/meta-patterns.md\` M29
|
|
126
|
+
- \`.claude/rules/self-verify.md\` Pre-edit phase
|
|
127
|
+
- \`.claude/references/ssot-index.md\` Step 0.1 high-risk interface owner mapping
|
|
128
|
+
EOF
|
|
129
|
+
|
|
130
|
+
# Soft warn (exit 0):inject context 讓 AI 自決,Stop hook backstop。
|
|
131
|
+
# 對齊 check_substantive_edit_approval_preflight.sh hybrid pattern(soft pre + hard stop)。
|
|
132
|
+
exit 0
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_escape_marker_abuse.sh — P0 BLOCKER (codify warning)
|
|
3
|
+
#
|
|
4
|
+
# 偵測 consumer code(.tsx/.stories.tsx)濫用 escape markers 跳 SSOT enforcement.
|
|
5
|
+
# Per user 2026-05-27 verbatim「不亂加 escape markers — 加就跳 enforcement」.
|
|
6
|
+
#
|
|
7
|
+
# Escape markers exist for真 exceptions(per-line documented rationale)。但 fork
|
|
8
|
+
# user 若大量加 escape markers (eg. ≥3 同一 file) = 違反 escape philosophy → BLOCK.
|
|
9
|
+
#
|
|
10
|
+
# Detected markers:
|
|
11
|
+
# - @ds-misuse-allow: (check_consumer_ds_primitive_misuse.sh escape)
|
|
12
|
+
# - @story-baseline-allow:(check_consumer_story_baseline.sh escape)
|
|
13
|
+
# - @consumer-catalog-allow:(check_consumer_no_ds_catalog.sh escape)
|
|
14
|
+
# - @overlay-open-skip: (check_overlay_open_focus_escape_probe.sh escape)
|
|
15
|
+
# - @template-customized (template canonical sync opt-out)
|
|
16
|
+
# - @layout-space-magic-ok:(check_layout_space_magic_numbers.sh escape)
|
|
17
|
+
# - @story-trait-allow: (story-baseline / catalog escape)
|
|
18
|
+
# - @story-trait-rationale:(check_story_invariants.sh R3 escape)
|
|
19
|
+
# - @story-split-rationale:(check_story_invariants.sh R2 escape)
|
|
20
|
+
# - @story-name-canonical-allow:(check_story_invariants.sh R4 escape)
|
|
21
|
+
# - @propose-cite-skip: (check_propose_cite_required.sh escape)
|
|
22
|
+
# - @anatomy-exempt: (story-rules escape)
|
|
23
|
+
# - @anatomy-exempt-next:(story-rules per-line escape)
|
|
24
|
+
# - @benchmark-unverified: (M22 cite)
|
|
25
|
+
# - @benchmark-citation-allow / @benchmark-unverified-blanket (M22 file-level cite escapes)
|
|
26
|
+
#
|
|
27
|
+
# Threshold:≥3 distinct markers OR ≥5 total occurrences in same file → BLOCK
|
|
28
|
+
# Forces fork user to either (a) fix root cause OR (b) refactor properly OR
|
|
29
|
+
# (c) explicitly cite reason in commit message via env override.
|
|
30
|
+
#
|
|
31
|
+
# 2026-05-31 升級(per 3-hardening-items 調查,折進本 hook 不新增檔):
|
|
32
|
+
# 1. Justification gate:escape marker 後空理由(@x-allow: 後純空白 / bare @benchmark-unverified)
|
|
33
|
+
# = 靜默繞過 SSOT → BLOCK(consumer + DS source 都跑)。對齊 ESLint require-description / Google NOLINT。
|
|
34
|
+
# 2. Scope 擴 DS source(node_modules/@qijenchen/design-system/src tsx/ts):原整段 skip 讓 DS 內 marker 不受 justification
|
|
35
|
+
# 約束;現 DS 跑 justification gate 但跳過 ≥3/≥5 數量 gate(DS 有大量 legit exception 避免誤殺)。
|
|
36
|
+
# 3. MARKER_RE 廣化(M7/M34 broad):@<x>-allow 家族用廣義 regex 自動納管,免 enum drift。
|
|
37
|
+
|
|
38
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
39
|
+
|
|
40
|
+
set -uo pipefail
|
|
41
|
+
|
|
42
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
43
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
44
|
+
|
|
45
|
+
case "${TOOL:-}" in
|
|
46
|
+
Edit|Write|MultiEdit) ;;
|
|
47
|
+
*) exit 0 ;;
|
|
48
|
+
esac
|
|
49
|
+
|
|
50
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
51
|
+
# Scope 分流(2026-05-31 加 DS source — 原整段 skip 讓 DS 內 143 個 marker 不受任何 justification 約束):
|
|
52
|
+
# IS_CONSUMER=apps/consumer tsx/ts → 跑 justification gate + 數量 gate(≥3/≥5)
|
|
53
|
+
# IS_DS=node_modules/@qijenchen/design-system/src tsx/ts → 只跑 justification gate(DS 有大量 legit exception,不套數量上限避免誤殺)
|
|
54
|
+
IS_CONSUMER=0; IS_DS=0
|
|
55
|
+
if echo "$FILE" | grep -qE '/(apps|consumer)/.*\.(tsx|ts)$'; then IS_CONSUMER=1; fi
|
|
56
|
+
if echo "$FILE" | grep -qE 'node_modules/@qijenchen/design-system/src/.*\.(tsx|ts)$'; then IS_DS=1; fi
|
|
57
|
+
if [ "$IS_CONSUMER" -eq 0 ] && [ "$IS_DS" -eq 0 ]; then exit 0; fi
|
|
58
|
+
|
|
59
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
|
|
60
|
+
[ -z "$CONTENT" ] && exit 0
|
|
61
|
+
|
|
62
|
+
# Global escape — meta-skip(env override OR explicit comment)
|
|
63
|
+
if [ "${CLAUDE_BYPASS_ESCAPE_MARKER_AUDIT:-0}" = "1" ]; then exit 0; fi
|
|
64
|
+
|
|
65
|
+
# ── Justification gate(2026-05-31,折進本 hook;核心 real gap)──────────────────
|
|
66
|
+
# Escape marker 必帶 per-line rationale。空理由 = 靜默繞過 SSOT enforcement。
|
|
67
|
+
# (a) @<x>-allow: 後純空白到行尾(有 marker 有冒號但無理由)
|
|
68
|
+
# (b) bare @benchmark-unverified 後純空白到行尾(非 -blanket / 非 ": 理由" / 非後接說明文字)
|
|
69
|
+
# 對齊 ESLint eslint-comments/require-description + Google NOLINT(category) 必帶說明。
|
|
70
|
+
EMPTY_RATIONALE=$(echo "$CONTENT" | grep -nE '@[a-z][a-z-]+-allow:[[:space:]]*$|@benchmark-unverified[[:space:]]*$' || true)
|
|
71
|
+
if [ -n "$EMPTY_RATIONALE" ]; then
|
|
72
|
+
cat >&2 << EOF
|
|
73
|
+
🚨 ESCAPE-MARKER-NO-RATIONALE BLOCKER(P0,2026-05-31 folded into check_escape_marker_abuse)
|
|
74
|
+
|
|
75
|
+
File $FILE — escape marker 後無 rationale(空理由 = 靜默繞過 SSOT):
|
|
76
|
+
$(echo "$EMPTY_RATIONALE" | sed 's/^/ /')
|
|
77
|
+
|
|
78
|
+
修法:marker 冒號後寫具體理由,eg.
|
|
79
|
+
// @ds-misuse-allow: Notion-style inline edit canonical(Field 在 cell 內編輯)
|
|
80
|
+
/* @benchmark-unverified: AG Grid pixel-snapshot 共識,待補 URL cite */
|
|
81
|
+
|
|
82
|
+
Escape directive 世界級共識(ESLint require-description / Google NOLINT)必帶 rationale。
|
|
83
|
+
EOF
|
|
84
|
+
exit 2
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# ── Sanctioned portal 檔 allowlist(2026-06-11 R2 held-item #10)──────────────
|
|
88
|
+
# AllDsComponents.stories.tsx 是 documented DS proxy portal(檔頭 @anatomy-exempt +
|
|
89
|
+
# @consumer-catalog-allow per 2026-05-27 M31 codex synthesis + @layout-space-magic-ok
|
|
90
|
+
# dev-artifact 豁免)— 3 個 marker 各帶 rationale 是它的 sanctioned 常態,數量 gate
|
|
91
|
+
# (≥3 distinct)對它必誤擋 = hook-vs-hook 張力。只豁免數量 gate;justification gate
|
|
92
|
+
# (空理由 BLOCK)上方已跑完照常生效,真保護不削弱。
|
|
93
|
+
IS_SANCTIONED_PORTAL=0
|
|
94
|
+
case "$FILE" in
|
|
95
|
+
*apps/*/src/AllDsComponents.stories.tsx) IS_SANCTIONED_PORTAL=1 ;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
# ── 數量 gate(≥3 distinct / ≥5 total)— 僅 consumer(DS source 有大量 legit exception,只走上方 justification gate)──
|
|
99
|
+
# 2026-06-11 R2 Phase B(codex b3 抓縫):portal 無限豁免會放走「第 4、5 個新增 marker」——
|
|
100
|
+
# 改為提高閾值(portal ≥6 / 一般 ≥3)保留 ceiling,justification gate 不變。
|
|
101
|
+
if [ "$IS_SANCTIONED_PORTAL" -eq 1 ]; then DISTINCT_CAP=6; else DISTINCT_CAP=3; fi
|
|
102
|
+
if [ "$IS_CONSUMER" -eq 1 ]; then
|
|
103
|
+
# 2026-05-31 廣化 MARKER_RE(M7/M34:spec wording broad「任何 escape marker」→ hook regex 不該 narrow 只 16 種):
|
|
104
|
+
# @<x>-allow 家族用廣義 [a-z][a-z-]*-allow 自動納管(免每次補 enum drift)+ 非 -allow markers 顯式列。
|
|
105
|
+
MARKER_RE='@([a-z][a-z-]*-allow|benchmark-unverified(-blanket)?|template-customized|anatomy-exempt(-next)?|overlay-open-skip|layout-space-magic-ok|propose-cite-skip|story-(trait|split)-rationale)'
|
|
106
|
+
MARKERS_FOUND=$(echo "$CONTENT" | grep -oE "$MARKER_RE" | sort -u)
|
|
107
|
+
# 2026-05-30 fix(test-surfaced):空 MARKERS_FOUND 時 grep -c 印 "0" 已 exit 1,原 `|| echo 0`
|
|
108
|
+
# 會再 append 一個 "0" → "0\n0" → 下方 `[ -ge "$DISTINCT_CAP" ]` integer-expression error。改 `|| true` 不重複。
|
|
109
|
+
DISTINCT_COUNT=$(echo "$MARKERS_FOUND" | grep -c . || true)
|
|
110
|
+
[ -z "$DISTINCT_COUNT" ] && DISTINCT_COUNT=0
|
|
111
|
+
TOTAL_COUNT=$(echo "$CONTENT" | grep -oE "$MARKER_RE" | wc -l | tr -d ' ')
|
|
112
|
+
|
|
113
|
+
# Threshold: ≥3 distinct types OR ≥5 total
|
|
114
|
+
if [ "$DISTINCT_COUNT" -ge 3 ] || [ "$TOTAL_COUNT" -ge 5 ]; then
|
|
115
|
+
cat >&2 << EOF
|
|
116
|
+
🚨 ESCAPE-MARKER-ABUSE BLOCKER(P0,user 2026-05-27 verbatim「不亂加 escape markers — 加就跳 enforcement」)
|
|
117
|
+
|
|
118
|
+
File $FILE:
|
|
119
|
+
Distinct escape markers: $DISTINCT_COUNT(threshold ≥3)
|
|
120
|
+
Total occurrences: $TOTAL_COUNT(threshold ≥5)
|
|
121
|
+
|
|
122
|
+
Markers detected:
|
|
123
|
+
$(echo "$MARKERS_FOUND" | sed 's/^/ /')
|
|
124
|
+
|
|
125
|
+
Escape markers 設計為「rare per-line documented exception」,不該 routine 加。
|
|
126
|
+
Fork user file 大量加 marker = 違反 SSOT 哲學 — 應該:
|
|
127
|
+
|
|
128
|
+
修法 3 選 1:
|
|
129
|
+
(a) **重構 code** 走 DS canonical pattern(消除根因,不繞)
|
|
130
|
+
(b) **拆 file**:1 個 escape 對應 1 個 specific case,分散到不同 file
|
|
131
|
+
(c) **Override env**(極罕見,documented in commit msg):
|
|
132
|
+
CLAUDE_BYPASS_ESCAPE_MARKER_AUDIT=1 git commit -m "<rationale>"
|
|
133
|
+
|
|
134
|
+
per check_consumer_*.sh hooks SSOT — escape 是 emergency exit,不是 daily tool.
|
|
135
|
+
EOF
|
|
136
|
+
exit 2
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
exit 0
|