@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,238 @@
1
+ #!/bin/bash
2
+ # inject_deploy_url_after_push.sh — UserPromptSubmit + PostToolUse: 偵測 git push 後自動 inject deploy URL
3
+ #
4
+ # Per user verbatim 2026-05-26:「完成部署之後都應該自動回吐部署的連結,每次必定自動回,不論是現在這個 session 還是其他的」
5
+ # Per user verbatim 2026-05-27:「不管在任何 repo,只要有部署東西到 netlify 上不管是否是 production 都應該要提供連結」
6
+ #
7
+ # Mechanism(2026-05-27 v2 expand scope per user complaint — DS GH Pages also auto-provide URL):
8
+ # PostToolUse Bash:tool_input.command 含 `git push origin <branch>` → 偵測 → multi-target URL detection:
9
+ # 1. Netlify(scripts/deploy-url.mjs + .netlify/state.json)— PW + fork
10
+ # 2. Netlify dashboard-link(netlify.toml exists,no state.json)— PW with Netlify auto-build
11
+ # 3. GitHub Pages(.github/workflows/*.yml 含 pages action)— DS repo
12
+ # → output URL list inject into AI context(下個 reply 必看到)
13
+ #
14
+ # 為何走 Hook(per CLAUDE.md governance 8-home L7 Hook 自動化):
15
+ # - 不靠 AI 記得「每次推完都要 echo URL」(會忘記 — 本 session user 抓「你他媽到底做得怎樣」)
16
+ # - 不靠 user 每次問「部署到哪?」(無聊重複)
17
+ # - Hook 機械保證每 push 必觸發,跨 session / 跨 fork user 自動受惠
18
+ #
19
+ # Scope expanded(2026-05-27):
20
+ # - Netlify CLI-linked (.netlify/state.json + scripts/deploy-url.mjs) → 直接 script 抓 URL
21
+ # - Netlify dashboard-linked (netlify.toml + branch deploys) → 用 git remote 推導 site name
22
+ # - GitHub Pages (.github/workflows/*.yml 含 pages.yml OR ci.yml deploy-pages) → 推導 GH Pages URL
23
+ #
24
+ # 對齊:.claude/skills/codex-collab/SKILL.md PostToolUse pattern + check_fork_user_plugin_install.sh detection pattern
25
+
26
+ source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
27
+
28
+ set -uo pipefail
29
+ INPUT=$(cat 2>/dev/null || echo "{}")
30
+ EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
31
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
32
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
33
+
34
+ # Scope:PostToolUse Bash 且 cmd 含 git push to remote(main / branch)
35
+ [ "$EVENT" != "PostToolUse" ] && exit 0
36
+ [ "$TOOL" != "Bash" ] && exit 0
37
+
38
+ # Heuristic:detect `git push origin <branch>` patterns
39
+ # 2026-06-06 fix:要求 `git push` 在「命令邊界」(行首 / ; / && / || / $( )擋字串提及(`P='git push origin'`)。
40
+ # 2026-06-07 fix(對抗 sweep):容忍 env-prefix(`GIT_SSH_COMMAND=… git push`)/ `git -C <dir>` /
41
+ # push 前後任意 flag(`--force-with-lease` / `-f` / `--set-upstream` / `--no-verify` 等)。原本只認 `-u`
42
+ # → 漏 fire 在這些「真實 push」上 = 該吐 URL 卻沒吐,defeat hook purpose。
43
+ _ENVP='([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+[[:space:]]+)*'
44
+ _GITPRE='git[[:space:]]+(-C[[:space:]]+[^[:space:]]+[[:space:]]+|-[A-Za-z][^[:space:]]*[[:space:]]+)*push'
45
+ _PUSHFLAGS='([[:space:]]+-{1,2}[A-Za-z][^[:space:]]*)*'
46
+ if ! echo "$CMD" | grep -qE "(^|[;&|(])[[:space:]]*${_ENVP}${_GITPRE}${_PUSHFLAGS}[[:space:]]+origin\b"; then
47
+ exit 0
48
+ fi
49
+
50
+ # Skip if push --delete (branch cleanup, not deploy)
51
+ if echo "$CMD" | grep -qE 'push\s+origin\s+--delete'; then
52
+ exit 0
53
+ fi
54
+
55
+ CWD=$(pwd)
56
+ # 2026-06-07 fix:偵測「真實 working dir」作 deploy-target 偵測根,跨 repo push 不吐「錯 repo」URL
57
+ # (anchor:syncing ds-product-template 時吐成 DS 站台 URL)。優先 `git -C <dir>`;否則命令鏈中
58
+ # 「最後一個 cd」(= push 時 effective cwd,**非第一個**;`echo x && cd /other && git push` 必用 /other)。
59
+ _GITC=$(echo "$CMD" | grep -oE 'git[[:space:]]+-C[[:space:]]+[^[:space:]]+' | head -1 | sed -E 's/.*-C[[:space:]]+//; s/^"//; s/"$//')
60
+ _LASTCD=$(echo "$CMD" | grep -oE '(^|[;&|(])[[:space:]]*cd[[:space:]]+[^;&|]+' | tail -1 | sed -E 's/.*cd[[:space:]]+//; s/[[:space:]]+$//; s/^"//; s/"$//')
61
+ for _d in "$_GITC" "$_LASTCD"; do
62
+ if [ -n "$_d" ] && [ -d "$_d" ] && git -C "$_d" rev-parse --git-dir >/dev/null 2>&1; then CWD="$_d"; break; fi
63
+ done
64
+ URLS_FOUND=""
65
+ # 2026-06-07 ROOT fix:只從「git push ... origin <ref>」這一段擷取 branch,不是整條 compound command 的
66
+ # 第一個 `origin X`。否則 `git fetch origin --quiet && git push origin main` 會誤抓 fetch 段的 `--quiet`
67
+ # 當 branch → 推導 `--quiet--site` 404(此前 5 道 guard 都沒擋到,因 `--quiet` 全是合法 git ref 字元)。
68
+ # 隔出 push 段(到下個 command 分隔 ; & | 為止)後,取 origin 後第一個 token。
69
+ # PUSH_SEG 用同一 env/-C/flag-tolerant prefix 隔出 push 段(到下個分隔 ; & | 為止),再取 origin 後第一 token
70
+ PUSH_SEG=$(echo "$CMD" | grep -oE "(^|[;&|(])[[:space:]]*${_ENVP}${_GITPRE}[^;&|]*" | head -1)
71
+ BRANCH=$(echo "$PUSH_SEG" | grep -oE 'origin[[:space:]]+[^[:space:]]+' | head -1 | awk '{print $2}')
72
+ # origin 後第一個 token 是 flag(`git push origin --force` = 無顯式 branch)→ 清空走 current-branch fallback
73
+ case "$BRANCH" in -*) BRANCH="" ;; esac
74
+ # 2026-06-06 fix:refspec `src:dst` → 取 dst(`HEAD:main` → `main`),否則推導出 `HEAD:main--site` 404 URL
75
+ case "$BRANCH" in *:*) BRANCH="${BRANCH##*:}" ;; esac
76
+ # 2026-06-07 fix:full-ref dst — `HEAD:refs/heads/main` → `main`(否則 `refs/heads/main--site` 誤判 preview);
77
+ # `refs/tags/...` → tag push,skip
78
+ case "$BRANCH" in refs/tags/*) exit 0 ;; esac
79
+ BRANCH="${BRANCH#refs/heads/}"
80
+ # 2026-06-06 fix:`git push origin HEAD`(或 `@`)= symbolic ref 指向當前 branch → 解析成真 branch 名。
81
+ # 2026-06-07 fix:detached HEAD 時 show-current 回「空字串 + exit 0」→ BRANCH 留空,交給下方 value-based fallback
82
+ # (原 `|| echo ""` 沒用,因 git 不是失敗而是成功回空)。
83
+ if [ "$BRANCH" = "HEAD" ] || [ "$BRANCH" = "@" ]; then
84
+ BRANCH=$(git -C "$CWD" branch --show-current 2>/dev/null)
85
+ fi
86
+ # 2026-06-06 fix:BRANCH 含非法 git ref 字元(" ' 空白 \ 等)→ 此命令只是「字串裡含 git push origin」
87
+ # (測試迴圈 / 文件 / echo),非真推送 → skip,避免把 `main"` 等垃圾推導成 404 URL 注入 context。
88
+ # git ref 合法字元集 ⊂ [A-Za-z0-9._/-];非此集 = 必為解析噪音(root guard,涵蓋 refspec/tag 之外的雜訊)。
89
+ if [ -n "$BRANCH" ] && ! echo "$BRANCH" | grep -qE '^[A-Za-z0-9._/-]+$'; then exit 0; fi
90
+ # 2026-06-06 fix:tag push(`v1.2.3` 等)不產 branch-preview / production deploy → skip,
91
+ # 否則把 tag 名當 branch 推導出 `v0.1.0-beta.56--site` 404 URL(release tag push 每次都誤吐)
92
+ if echo "$BRANCH" | grep -qE '^v[0-9]'; then exit 0; fi
93
+ if [ -n "$BRANCH" ] && git -C "$CWD" rev-parse --verify --quiet "refs/tags/$BRANCH" >/dev/null 2>&1; then exit 0; fi
94
+ # 2026-06-07 fix:仍空(bare `git push origin` / detached HEAD / 解析不出)→ 取 current branch(value-based);
95
+ # 仍空 → 無法產生合法 URL(會變 `https://--site` malformed)→ skip,不亂猜 main。
96
+ [ -z "$BRANCH" ] && BRANCH=$(git -C "$CWD" branch --show-current 2>/dev/null)
97
+ [ -z "$BRANCH" ] && exit 0
98
+
99
+ # v3 2026-05-27:curl HEAD verify URL before reporting(per user「你確定有做到」complaint)
100
+ # v4 2026-05-27:add content sniff(防 squat URLs 200 但 unrelated content)
101
+ verify_url() {
102
+ local url="$1"
103
+ local code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 5 -I "$url" 2>/dev/null)
104
+ case "$code" in
105
+ 200|301|302) echo "OK" ;;
106
+ *) echo "FAIL:$code" ;;
107
+ esac
108
+ }
109
+
110
+ # v4 content sniff:check 200 URL 是 Storybook real deploy(not squat)
111
+ # 用 Storybook hallmark patterns(sb-manager / sb-addons / @storybook/core title)
112
+ is_storybook_deploy() {
113
+ local url="$1"
114
+ curl -s --max-time 5 -L "$url" 2>/dev/null | grep -qE "sb-manager|sb-addons|@storybook/core|storybook-static"
115
+ }
116
+
117
+ # Detection 1:Netlify CLI-linked(.netlify/state.json + scripts/deploy-url.mjs)
118
+ DEPLOY_SCRIPT="$CWD/scripts/deploy-url.mjs"
119
+ if [ -f "$DEPLOY_SCRIPT" ] && [ -f "$CWD/.netlify/state.json" ]; then
120
+ URL_INFO=$(node "$DEPLOY_SCRIPT" --json 2>/dev/null)
121
+ if [ -n "$URL_INFO" ]; then
122
+ URL=$(echo "$URL_INFO" | jq -r '.url // ""' 2>/dev/null)
123
+ IS_PROD=$(echo "$URL_INFO" | jq -r '.isProd // false' 2>/dev/null)
124
+ if [ -n "$URL" ]; then
125
+ if [ "$IS_PROD" = "true" ]; then
126
+ URLS_FOUND="${URLS_FOUND}🚀 Netlify PRODUCTION(${BRANCH}): ${URL}\n"
127
+ else
128
+ URLS_FOUND="${URLS_FOUND}🔍 Netlify PREVIEW(${BRANCH}): ${URL}\n"
129
+ fi
130
+ fi
131
+ fi
132
+ fi
133
+
134
+ # Detection 2:Netlify dashboard-linked(netlify.toml + no state.json)
135
+ # v4:try multiple naming conventions + content sniff to filter squat URLs
136
+ if [ -z "$URLS_FOUND" ] && [ -f "$CWD/netlify.toml" ]; then
137
+ GH_REMOTE=$(git -C "$CWD" remote get-url origin 2>/dev/null)
138
+ REPO_NAME=$(echo "$GH_REMOTE" | sed -E 's|.*/([^/.]+)(\.git)?$|\1|')
139
+ OWNER_REPO=$(echo "$GH_REMOTE" | sed -E 's|.*github\.com[:/]([^/]+/[^/.]+)(\.git)?$|\1|')
140
+ OWNER=$(echo "$OWNER_REPO" | cut -d/ -f1)
141
+
142
+ # v5 multi-candidate strategy(Netlify naming conventions in order of likelihood):
143
+ # 1. ~/.claude/local/deploy-targets.json overrides(per-user known URLs)— win all
144
+ # 2. <owner>-<package.json.name>.netlify.app(setup-netlify script convention,per scripts/setup-netlify-access.mjs `${ghUser}-${repoName}` formula)
145
+ # 3. <owner>-<repo-from-remote>.netlify.app(Netlify Import default,fork users without setup script)
146
+ # 4. <repo-from-remote>.netlify.app(simple,rare)
147
+ USER_OVERRIDE=""
148
+ if [ -f "$HOME/.claude/local/deploy-targets.json" ]; then
149
+ USER_OVERRIDE=$(jq -r --arg key "$OWNER_REPO" '.[$key] // ""' "$HOME/.claude/local/deploy-targets.json" 2>/dev/null)
150
+ fi
151
+ # Read package.json.name for setup-script convention
152
+ PKG_NAME=""
153
+ if [ -f "$CWD/package.json" ]; then
154
+ PKG_NAME=$(jq -r '.name // ""' "$CWD/package.json" 2>/dev/null | sed -E 's|^@[^/]+/||') # strip npm scope
155
+ fi
156
+ CANDIDATES=""
157
+ if [ -n "$USER_OVERRIDE" ]; then
158
+ CANDIDATES="$USER_OVERRIDE"
159
+ else
160
+ # Setup-script convention candidate(highest match rate for fork users following docs)
161
+ if [ -n "$OWNER" ] && [ -n "$PKG_NAME" ]; then CANDIDATES="$CANDIDATES https://${OWNER}-${PKG_NAME}.netlify.app"; fi
162
+ # Netlify Import default(no setup script, manual dashboard import)
163
+ if [ -n "$OWNER" ] && [ -n "$REPO_NAME" ] && [ "$REPO_NAME" != "$PKG_NAME" ]; then
164
+ CANDIDATES="$CANDIDATES https://${OWNER}-${REPO_NAME}.netlify.app"
165
+ fi
166
+ # Simple fallback
167
+ if [ -n "$REPO_NAME" ]; then CANDIDATES="$CANDIDATES https://${REPO_NAME}.netlify.app"; fi
168
+ fi
169
+
170
+ REAL_URL=""; REAL_NOTE=""
171
+ if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
172
+ # 2026-06-07 fix:優先「200 + Storybook content」;沒有則退「200(內容非 Storybook)」,
173
+ # **不丟棄**已驗 200 的 candidate。anchor:apps/template 是真 product app(非 Storybook)→ 原
174
+ # is_storybook_deploy 把活著的 200 deploy 誤當 squat 報「全 404」,違反 user「不管是否 production 都給連結」。
175
+ FALLBACK_URL=""
176
+ for candidate in $CANDIDATES; do
177
+ if [ "$(verify_url "$candidate")" = "OK" ]; then
178
+ if is_storybook_deploy "$candidate"; then
179
+ REAL_URL="$candidate"; REAL_NOTE="✅ verified 200 + Storybook content"; break
180
+ elif [ -z "$FALLBACK_URL" ]; then
181
+ FALLBACK_URL="$candidate"
182
+ fi
183
+ fi
184
+ done
185
+ if [ -z "$REAL_URL" ] && [ -n "$FALLBACK_URL" ]; then
186
+ REAL_URL="$FALLBACK_URL"; REAL_NOTE="✅ verified 200(內容非 Storybook — 可能 product app deploy)"
187
+ fi
188
+ if [ -n "$REAL_URL" ]; then
189
+ URLS_FOUND="${URLS_FOUND}🚀 Netlify PRODUCTION(${BRANCH}): ${REAL_URL} ${REAL_NOTE}\n"
190
+ else
191
+ URLS_FOUND="${URLS_FOUND}🚀 Netlify PRODUCTION URL 未驗 — tried: ${CANDIDATES// /, }\n ⚠️ 全 404。需要 user 手動 share dashboard URL,OR 創 \$HOME/.claude/local/deploy-targets.json:\n {\"${OWNER_REPO}\": \"https://<actual-site>.netlify.app\"}\n"
192
+ fi
193
+ else
194
+ # Branch preview:always use `<branch>--<sitename>` pattern,but sitename unknown unless USER_OVERRIDE
195
+ if [ -n "$USER_OVERRIDE" ]; then
196
+ SITENAME=$(echo "$USER_OVERRIDE" | sed -E 's|https?://([^.]+)\.netlify\.app.*|\1|')
197
+ CANDIDATE="https://${BRANCH}--${SITENAME}.netlify.app"
198
+ if [ "$(verify_url "$CANDIDATE")" = "OK" ]; then
199
+ URLS_FOUND="${URLS_FOUND}🔍 Netlify PREVIEW(${BRANCH}): ${CANDIDATE} ✅ verified 200\n"
200
+ else
201
+ URLS_FOUND="${URLS_FOUND}🔍 Netlify PREVIEW 推導: ${CANDIDATE} ⚠️ 404(preview 未啟 OR build pending — Netlify build 2-3 min)\n"
202
+ fi
203
+ else
204
+ URLS_FOUND="${URLS_FOUND}🔍 Netlify PREVIEW(${BRANCH}) — sitename 未知;設 \$HOME/.claude/local/deploy-targets.json 後 hook 可推 preview URL\n"
205
+ fi
206
+ fi
207
+ fi
208
+
209
+ # Detection 3:GitHub Pages(.github/workflows/*.yml deploys to gh-pages OR uses actions/deploy-pages)
210
+ if ls "$CWD/.github/workflows/"*.yml >/dev/null 2>&1; then
211
+ if grep -l "actions/deploy-pages\|gh-pages\|github.io" "$CWD/.github/workflows/"*.yml >/dev/null 2>&1; then
212
+ GH_REMOTE=$(git -C "$CWD" remote get-url origin 2>/dev/null)
213
+ OWNER_REPO=$(echo "$GH_REMOTE" | sed -E 's|.*github\.com[:/]([^/]+/[^/.]+)(\.git)?$|\1|')
214
+ OWNER=$(echo "$OWNER_REPO" | cut -d/ -f1)
215
+ REPO=$(echo "$OWNER_REPO" | cut -d/ -f2)
216
+ if [ -n "$OWNER" ] && [ -n "$REPO" ] && { [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; }; then
217
+ CANDIDATE="https://${OWNER}.github.io/${REPO}/"
218
+ VERIFY=$(verify_url "$CANDIDATE")
219
+ if [ "$VERIFY" = "OK" ]; then
220
+ URLS_FOUND="${URLS_FOUND}📄 GitHub Pages(${BRANCH}): ${CANDIDATE} ✅ verified 200\n"
221
+ else
222
+ URLS_FOUND="${URLS_FOUND}📄 GitHub Pages 推導 URL: ${CANDIDATE} ⚠️ ${VERIFY}(build ~3-5 min;若仍 404 check Actions tab build status)\n"
223
+ fi
224
+ fi
225
+ fi
226
+ fi
227
+
228
+ # No deploy target detected → silent skip
229
+ [ -z "$URLS_FOUND" ] && exit 0
230
+
231
+ # Inject into AI context
232
+ # 2026-05-29 ROOT-CAUSE FIX:PostToolUse hook 的純 stdout **不會**注入 AI context(只進 transcript)→
233
+ # 原 `printf` 輸出讓 AI 看不到 URL → AI 每次 push 都沒 relay 給 user(user verbatim「部署完都沒給我 url」)。
234
+ # 必須輸出 JSON `hookSpecificOutput.additionalContext` 才會真注入 AI context。
235
+ MSG=$(printf '%b' "🚀 Deploy URLs auto-detected — RELAY 給 user(per user 2026-05-26「完成部署都自動回吐連結」+ 2026-05-27「不管 repo」):\n${URLS_FOUND}\n(AI:必須把上面 URL 貼給 user,不可省略)")
236
+ jq -n --arg ctx "$MSG" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$ctx}}'
237
+
238
+ exit 0
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # fork-governance-dispatcher.sh — C-prime 治理派發器(committed in fork,極穩定、幾乎永不改)
3
+ #
4
+ # 這是 fork repo 唯一 commit 的「啟動器」:讀 npm 套件 ship 的 fork 治理 manifest,跑當下這個
5
+ # event 的所有官方 fork hook。DS 未來治理增刪改 → 重生 npm 套件的 manifest → fork `npm install`
6
+ # 後本派發器自動跑新的那套(committed 設定不用動)= 完全同步。
7
+ #
8
+ # 設計鐵則:
9
+ # - 本體在 node_modules(npm install 覆蓋 = 官方控管、使用者改不動)
10
+ # - manifest 缺(還沒 npm install)→ exit 0 永不 brick(bootstrap 另外發 notice)
11
+ # - 任一官方 fork hook exit 2(BLOCKER)→ 派發器 exit 2(轉發攔截)
12
+ # - 其餘 exit code 不阻擋(soft)
13
+
14
+ set -uo pipefail
15
+ INPUT=$(cat 2>/dev/null || echo "{}")
16
+ EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
17
+
18
+ DS_FORK="${CLAUDE_PROJECT_DIR:-.}/node_modules/@qijenchen/design-system/ds-canonical/fork"
19
+ MANIFEST="$DS_FORK/manifest.json"
20
+
21
+ # 治理本體還沒就位(fork 第一個 session 還沒 npm install)→ 不阻擋(bootstrap 會發 notice)
22
+ [ -f "$MANIFEST" ] || exit 0
23
+
24
+ # 取這個 event 註冊的官方 fork hook 清單
25
+ HOOK_FILES=$(jq -r --arg ev "$EVENT" '.hooks[$ev][]?.file // empty' "$MANIFEST" 2>/dev/null)
26
+ [ -z "$HOOK_FILES" ] && exit 0
27
+
28
+ RC=0
29
+ while IFS= read -r hf; do
30
+ [ -z "$hf" ] && continue
31
+ body="$DS_FORK/$hf"
32
+ [ -f "$body" ] || continue # 個別 body 缺 → skip,不阻擋(派發器穩健)
33
+ # 依副檔名選 interpreter:.py 用 python3、其餘 bash。
34
+ # 鐵則:用 bash 跑 .py → syntax error → exit 2 → 被下面誤判成 BLOCKER → 擋掉所有編輯(致命)。
35
+ # python3 缺(罕見)→ exit 127(非 2)→ 不轉發 = fail-open,不 brick。
36
+ case "$body" in
37
+ *.py) echo "$INPUT" | python3 "$body" ;;
38
+ *) echo "$INPUT" | bash "$body" ;;
39
+ esac
40
+ hc=$?
41
+ [ "$hc" -eq 2 ] && RC=2 # 官方 hook 攔截 → 轉發攔截
42
+ done <<< "$HOOK_FILES"
43
+
44
+ exit $RC
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # inject_fork_governance_preamble.sh — C-prime SessionStart 治理 init(install-if-missing + 事前指引注入)
3
+ #
4
+ # 2026-06-17 合併原 check_governance_bootstrap 的 install(adversarial run-4 MAJOR — cloud-flow race):
5
+ # 原本 SessionStart 有 3 個 hook(bootstrap 裝 / inject 注入 / dispatcher),Claude Code 同組 hook「並行」跑。
6
+ # 雲端 fresh-clone 時 bootstrap 還在前景 `npm install`(冷裝 30s–4min),inject 並行跑完一次 `[ -f preamble ]`
7
+ # 檢查 → 檔案還沒到 → 靜默 exit 0 → 什麼都沒注入 = proactive 指引在雲端首次 session 失效(且每 session 重演)。
8
+ # 修:把 install 與 inject 放「同一 hook process 內 sequential」→ 消除並行 race + 避免兩個並發 npm install 衝突。
9
+ # dispatcher 不再掛 SessionStart(manifest 無 SessionStart body = 本來就 no-op);它留在 Pre/Post/UserPromptSubmit。
10
+ #
11
+ # fail-open:install 失敗 / preamble 缺 → notice + exit 0,永不 brick。
12
+ # 對齊既有用 additionalContext 的 hook 輸出契約。
13
+
14
+ set -uo pipefail
15
+ PD="${CLAUDE_PROJECT_DIR:-.}"
16
+ FORK="$PD/node_modules/@qijenchen/design-system/ds-canonical/fork"
17
+ PREAMBLE="$FORK/preamble.md"
18
+
19
+ # (1) 治理本體缺(雲端 fresh-clone / 還沒裝)+ 有 package.json → 前景裝 @beta。
20
+ # 這裡 block 到裝完才往下讀 → sequential,後面的讀取保證在 install 之後(消除 race)。
21
+ # codex risk 2:plain `npm install` 會被 lockfile 重現舊樹 → 明確 @beta 拿最新治理。
22
+ if [ ! -f "$PREAMBLE" ] && [ -f "$PD/package.json" ]; then
23
+ echo "💡 DS 治理本體缺(可能雲端 fresh-clone session)→ 自動裝最新治理(@beta)…" >&2
24
+ ( cd "$PD" && npm install @qijenchen/design-system@beta @qijenchen/storybook-config@beta --legacy-peer-deps --no-audit --no-fund >/dev/null 2>&1 ) || true
25
+ fi
26
+
27
+ # (2) 本體在(本機持久 / 剛裝完)→ 讀 + 經 additionalContext 注入設計紀律
28
+ if [ -f "$PREAMBLE" ]; then
29
+ CONTENT=$(cat "$PREAMBLE" 2>/dev/null)
30
+ if [ -n "$CONTENT" ]; then
31
+ jq -n --arg ctx "$CONTENT" '{
32
+ hookSpecificOutput: {
33
+ hookEventName: "SessionStart",
34
+ additionalContext: ("# DS 設計治理(npm-current,事前主動遵循)\n\n" + $ctx)
35
+ }
36
+ }'
37
+ exit 0
38
+ fi
39
+ fi
40
+
41
+ # (3) 仍缺(install 失敗 / 無 package.json)→ notice,不阻擋(fail-open)
42
+ cat >&2 << 'EOF'
43
+ 💡 DS 治理尚未就位(npm install 未完成或失敗)。請手動執行 → npm install(或 npm run sync-all)。
44
+ 注:現在「不阻擋」你工作,只是提醒;治理本體就位後設計治理 hook 才會機械把關。
45
+ EOF
46
+ exit 0
@@ -0,0 +1,58 @@
1
+ {
2
+ "_generated": "build-fork-governance.mjs",
3
+ "_source": "template/ds-product-template/.claude/settings.json(hooks + permissions 區塊)",
4
+ "_merge": "sync-all/refresh-fork-launchers.mjs:strip 舊 launcher 註冊 + append canonical hooks + union permissions.allow",
5
+ "hooks": {
6
+ "SessionStart": [
7
+ {
8
+ "hooks": [
9
+ {
10
+ "type": "command",
11
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject_fork_governance_preamble.sh\""
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "PreToolUse": [
17
+ {
18
+ "matcher": "Edit|Write|MultiEdit",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/fork-governance-dispatcher.sh\""
23
+ }
24
+ ]
25
+ }
26
+ ],
27
+ "PostToolUse": [
28
+ {
29
+ "matcher": "Edit|Write|MultiEdit|Bash",
30
+ "hooks": [
31
+ {
32
+ "type": "command",
33
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/fork-governance-dispatcher.sh\""
34
+ }
35
+ ]
36
+ }
37
+ ],
38
+ "UserPromptSubmit": [
39
+ {
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/fork-governance-dispatcher.sh\""
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ },
49
+ "permissions": {
50
+ "allow": [
51
+ "Bash(npm install)",
52
+ "Bash(npm ci)",
53
+ "Bash(npm run *)",
54
+ "Bash(npx *)",
55
+ "Bash(git *)"
56
+ ]
57
+ }
58
+ }
@@ -0,0 +1,79 @@
1
+ {
2
+ "_generated": "build-fork-governance.mjs",
3
+ "hooks": {
4
+ "PostToolUse": [
5
+ {
6
+ "file": "hooks/check_consumer_app_invariants.sh",
7
+ "sourceHook": "check_consumer_app_invariants.sh",
8
+ "bucket": "SHIP_AS_IS"
9
+ },
10
+ {
11
+ "file": "hooks/block_prototype_imports.py",
12
+ "sourceHook": "block_prototype_imports.py",
13
+ "bucket": "SHIP_AS_IS"
14
+ },
15
+ {
16
+ "file": "hooks/check_layout_space_magic_numbers.sh",
17
+ "sourceHook": "check_layout_space_magic_numbers.sh",
18
+ "bucket": "SHIP_AS_IS"
19
+ },
20
+ {
21
+ "file": "hooks/check_tailwind_wildcard_in_docs.sh",
22
+ "sourceHook": "check_tailwind_wildcard_in_docs.sh",
23
+ "bucket": "SHIP_AS_IS"
24
+ },
25
+ {
26
+ "file": "hooks/check_escape_marker_abuse.sh",
27
+ "sourceHook": "check_escape_marker_abuse.sh",
28
+ "bucket": "SHIP_REWRITTEN"
29
+ },
30
+ {
31
+ "file": "hooks/inject_deploy_url_after_push.sh",
32
+ "sourceHook": "inject_deploy_url_after_push.sh",
33
+ "bucket": "SHIP_REWRITTEN"
34
+ }
35
+ ],
36
+ "PreToolUse": [
37
+ {
38
+ "file": "hooks/check_item_list_gap.sh",
39
+ "sourceHook": "check_item_list_gap.sh",
40
+ "bucket": "SHIP_AS_IS"
41
+ },
42
+ {
43
+ "file": "hooks/check_pattern_invariants.sh",
44
+ "sourceHook": "check_pattern_invariants.sh",
45
+ "bucket": "SHIP_AS_IS"
46
+ },
47
+ {
48
+ "file": "hooks/check_field_family_invariants.sh",
49
+ "sourceHook": "check_field_family_invariants.sh",
50
+ "bucket": "SHIP_AS_IS"
51
+ },
52
+ {
53
+ "file": "hooks/check_sidebar_menu_button_implicit_wrap.sh",
54
+ "sourceHook": "check_sidebar_menu_button_implicit_wrap.sh",
55
+ "bucket": "SHIP_AS_IS"
56
+ },
57
+ {
58
+ "file": "hooks/check_chrome_header_avatar_canonical.sh",
59
+ "sourceHook": "check_chrome_header_avatar_canonical.sh",
60
+ "bucket": "SHIP_AS_IS"
61
+ },
62
+ {
63
+ "file": "hooks/check_opacity_token_usage.sh",
64
+ "sourceHook": "check_opacity_token_usage.sh",
65
+ "bucket": "SHIP_REWRITTEN"
66
+ },
67
+ {
68
+ "file": "hooks/check_ds_anchor_preflight.sh",
69
+ "sourceHook": "check_ds_anchor_preflight.sh",
70
+ "bucket": "SHIP_REWRITTEN"
71
+ },
72
+ {
73
+ "file": "hooks/check_fork_product_quality.sh",
74
+ "sourceHook": "check_substantive_edit_approval_preflight.sh",
75
+ "bucket": "REPLACE"
76
+ }
77
+ ]
78
+ }
79
+ }