@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,96 @@
1
+ #!/bin/bash
2
+ # check_layout_space_magic_numbers.sh — P0 BLOCKER
3
+ #
4
+ # 偵測 consumer / DS app code 用 Tailwind spacing magic numbers
5
+ # (`p-4` / `px-6` / `py-2` / `gap-3` 等)而非 layoutSpace token
6
+ # (`p-[var(--layout-space-N) N∈{loose,tight}]` / `gap-[var(--layout-space-N) N∈{loose,tight}]`)。
7
+ # 2026-05-27 user verbatim「機械無強制就不會做?那為何不全部 ssot 都要強制吻合?」
8
+ # 永久 codify — SSOT canonical 必 P0 BLOCKER,不分級。
9
+ #
10
+ # Anchor:user 質疑「content 自動繼承 layoutSpace SSOT 嗎?」
11
+ # - app-shell.spec.md:205 明文 `<main>` landmark padding=0 (intentional)
12
+ # - app-shell.spec.md:207-212 consumer 必遵循 layoutSpace.spec.md 6 條規則 + 親疏 3 級
13
+ #
14
+ # PostToolUse Edit/Write detect magic Tailwind spacing → P0 BLOCKER exit 2
15
+ # 強制改 token OR 加 `// @layout-space-magic-ok: <rationale>` escape comment(per-line)。
16
+
17
+ source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
18
+
19
+ set -uo pipefail
20
+
21
+ INPUT=$(cat 2>/dev/null || echo "{}")
22
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
23
+
24
+ case "${TOOL:-}" in
25
+ Edit|Write|MultiEdit) ;;
26
+ *) exit 0 ;;
27
+ esac
28
+
29
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.notebook_path // ""' 2>/dev/null)
30
+
31
+ # Only check .tsx / .ts in app code
32
+ if ! echo "$FILE" | grep -qE '\.(tsx|ts)$'; then exit 0; fi
33
+ # Skip DS source (DS components have their own spacing logic via cva)
34
+ if echo "$FILE" | grep -qE 'packages/design-system/src/|node_modules/'; then exit 0; fi
35
+
36
+ # Get new content
37
+ NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
38
+ [ -z "$NEW_CONTENT" ] && exit 0
39
+
40
+ # Escape clause:single line escape comment per line of magic number usage
41
+ # `// @layout-space-magic-ok: <rationale>` immediately above the line OR on same line
42
+ ESCAPE_MARKER='@layout-space-magic-ok:'
43
+
44
+ # Detect magic spacing classes (Tailwind class strings only — NOT JSX props like size={24})
45
+ # Match per line so we can check per-line escape comments
46
+ MAGIC_LINES=$(echo "$NEW_CONTENT" | grep -nE '\b(p|px|py|pt|pb|pl|pr|gap|space-x|space-y|m|mx|my|mt|mb|ml|mr)-(0\.5|[1-9][0-9]?(\.[0-9])?)\b')
47
+
48
+ if [ -z "$MAGIC_LINES" ]; then
49
+ exit 0
50
+ fi
51
+
52
+ # Filter out lines with escape marker on same line OR immediately preceding line
53
+ # 2026-06-03 修(doc-vs-code bug,M32):L41 文件宣稱支援「preceding line OR same line」,
54
+ # 但原 code 只檢查同行 → JSX className 行無法放同行 `//` comment(會破壞 JSX)→ escape 對 JSX
55
+ # 實質失效。補實作前一行檢查(grep -n 行號 → sed 取前一行),對齊文件 + 解 JSX 必需。
56
+ UNJUSTIFIED=""
57
+ while IFS= read -r line; do
58
+ # same-line marker
59
+ if echo "$line" | grep -qF "$ESCAPE_MARKER"; then continue; fi
60
+ # preceding-line marker(JSX `{/* @layout-space-magic-ok: ... */}` 在上一行)。
61
+ # 對齊 ESLint disable-next-line 慣例:前一行 marker 僅在該行是「註解專用行」(trimmed 開頭
62
+ # //、{/*、/*、*)時生效 — 否則上一行 code 的「同行 escape」會誤串到下一行(P8 conflict)。
63
+ lineno="${line%%:*}"
64
+ if [ "$lineno" -gt 1 ] 2>/dev/null; then
65
+ prev=$(echo "$NEW_CONTENT" | sed -n "$((lineno-1))p")
66
+ if echo "$prev" | grep -qF "$ESCAPE_MARKER" && echo "$prev" | grep -qE '^[[:space:]]*(//|\{?/\*|\*)'; then continue; fi
67
+ fi
68
+ UNJUSTIFIED="${UNJUSTIFIED}${line}\n"
69
+ done <<< "$MAGIC_LINES"
70
+
71
+ if [ -z "$UNJUSTIFIED" ]; then
72
+ exit 0
73
+ fi
74
+
75
+ cat >&2 << EOF
76
+ 🚨 LAYOUT-SPACE-MAGIC-NUMBER BLOCKER(P0,2026-05-27 user verbatim「機械無強制就不會做?
77
+ 為何不全部 ssot 都要強制吻合?」永久 codify)
78
+
79
+ Detected Tailwind spacing magic numbers in $FILE without escape:
80
+ $(echo -e "$UNJUSTIFIED" | sed 's/^/ /' | head -10)
81
+
82
+ per app-shell.spec.md L205-219 + layoutSpace.spec.md SSOT:consumer content 必遵循
83
+ layoutSpace 6 條規則 + 親疏 3 級,**禁** 硬寫 Tailwind magic numbers。改用:
84
+ p-[var(--layout-space-loose)] /* 16px 規則 1A/1B chrome / wrap */
85
+ p-[var(--layout-space-tight)] /* 12px 規則 3 親 gap */
86
+ gap-[var(--layout-space-distant)] /* 24px 規則 3 疏 gap */
87
+ space-y-[var(--layout-space-distant)]
88
+
89
+ 修法 2 選 1:
90
+ (a) 改 token:換成 var(--layout-space-N) N∈{loose,tight} family per 6 規則 + 親疏 3 級
91
+ (b) Escape:在該 line 加 \`// @layout-space-magic-ok: <rationale>\` 顯式 documented
92
+ (eg.「\`gap-1\` 是 4px stack icon — non-spacing context,not consumer layout」)
93
+
94
+ 完整 6 條規則 → packages/design-system/src/tokens/layoutSpace/layoutSpace.spec.md
95
+ EOF
96
+ exit 2
@@ -0,0 +1,149 @@
1
+ #!/bin/bash
2
+ set -uo pipefail
3
+ # Tailwind token registry enforcement hook(2026-05-18 升級 per audit Dim 47 + codex P1-5):
4
+ # 原 hook 名 `check_opacity_token_usage.sh`(keep filename for settings.json registration),
5
+ # 實際 logic 已升級成 full Tailwind utility registry compliance(per Dim 47 SSOT)。
6
+ #
7
+ # SSOT source:`node_modules/@qijenchen/design-system/src/tokens/utility-registry.json`(block list per category)。
8
+ # 對齊 Atlassian @atlaskit/tokens / Carbon @carbon/themes / Ant ConfigProvider / Polaris polaris-tokens
9
+ # token-first lint enforcement。
10
+ #
11
+ # 檢的 block category:
12
+ # - opacity:opacity-{5..95} numeric tier(原 logic,保留)
13
+ # - typography:text-{xs..9xl} / font-{thin|light|semibold|black} / leading-{N} numeric / tracking-{wide..widest|tighter|tight}
14
+ # - radius:rounded-{xl|2xl|3xl} / rounded(unscoped)
15
+ # - elevation:shadow-{sm|md|lg|xl|2xl|inner} / shadow(unscoped)
16
+ # - shadcn alias:bg-popover / text-muted-foreground / bg-accent 等
17
+ # - primitive 色名:bg-neutral-N / text-blue-N 等(越過 semantic 層)
18
+ #
19
+ # 例外:utility-registry.json `_meta.exceptions` 段定義的 anatomy stories / principles code blocks 豁免
20
+ # (本 hook 已 skip *.anatomy.stories.tsx / *.principles.stories.tsx 不檢)
21
+ #
22
+ # 修法:reuse semantic utility / token reference / var() bracket(詳 utility-registry.json `rationale_path`)。
23
+
24
+ source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
25
+
26
+ INPUT=$(cat)
27
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
28
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
29
+
30
+ case "$TOOL" in
31
+ Edit|Write|MultiEdit) ;;
32
+ *) exit 0 ;;
33
+ esac
34
+
35
+ # Scope:.tsx + .css(DS source);skip stories / tests / token spec 自家 / anatomy/principles per registry exception
36
+ case "$FILE_PATH" in
37
+ *.tsx|*.css) ;;
38
+ *) exit 0 ;;
39
+ esac
40
+ case "$FILE_PATH" in
41
+ # 註解:anatomy + principles stories per utility-registry.json `_meta.exceptions` 豁免
42
+ *.anatomy.stories.tsx|*.principles.stories.tsx) exit 0 ;;
43
+ *.stories.tsx|*.test.*|*.spec.tsx) exit 0 ;;
44
+ *tokens/opacity/*|*tokens/typography/*|*tokens/radius/*|*tokens/elevation/*|*tokens/color/*) exit 0 ;;
45
+ esac
46
+
47
+ NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""')
48
+ [ -z "$NEW_CONTENT" ] && exit 0
49
+
50
+ # Resolve registry path(absolute via $CLAUDE_PROJECT_DIR or relative fallback)
51
+ REGISTRY="${CLAUDE_PROJECT_DIR:-}/node_modules/@qijenchen/design-system/src/tokens/utility-registry.json"
52
+ if [ ! -f "$REGISTRY" ]; then
53
+ # Fallback: try relative from hook dir
54
+ REGISTRY="$(dirname "$0")/../../node_modules/@qijenchen/design-system/src/tokens/utility-registry.json"
55
+ fi
56
+ if [ ! -f "$REGISTRY" ]; then
57
+ # 沒 registry → fall back 到原 opacity-only logic(避免 hook 啞掉)
58
+ HITS=$(echo "$NEW_CONTENT" | grep -oE "opacity-[0-9]+" | grep -vE "^opacity-(0|100)$" | sort -u || true)
59
+ if [ -n "$HITS" ]; then
60
+ echo "" >&2
61
+ echo "⚠️ M23 violation(opacity tier;registry 不可用,fallback mode):" >&2
62
+ echo "$HITS" | sed 's/^/ /' >&2
63
+ echo "" >&2
64
+ echo " reuse opacity-disabled / alpha 色階。詳 tokens/opacity/opacity.spec.md" >&2
65
+ fi
66
+ exit 0
67
+ fi
68
+
69
+ # Aggregate violations across all categories
70
+ VIOLATIONS=""
71
+
72
+ # 1) Opacity numeric tier(opacity-5..95,除 0/100/disabled)
73
+ HITS_OPACITY=$(echo "$NEW_CONTENT" | grep -oE "\bopacity-[0-9]+\b" | grep -vE "^opacity-(0|100)$" | sort -u || true)
74
+ if [ -n "$HITS_OPACITY" ]; then
75
+ VIOLATIONS="${VIOLATIONS}\n [opacity] $(echo "$HITS_OPACITY" | tr '\n' ' ')"
76
+ fi
77
+
78
+ # 2) Typography raw size(text-xs..9xl)— skip semantic text-h*/text-body/text-caption/text-helper/text-label
79
+ HITS_TEXT=$(echo "$NEW_CONTENT" | grep -oE "\btext-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)\b" | sort -u || true)
80
+ if [ -n "$HITS_TEXT" ]; then
81
+ VIOLATIONS="${VIOLATIONS}\n [typography size] $(echo "$HITS_TEXT" | tr '\n' ' ')"
82
+ fi
83
+
84
+ # 3) Typography raw weight
85
+ HITS_FONT=$(echo "$NEW_CONTENT" | grep -oE "\bfont-(thin|extralight|light|semibold|extrabold|black)\b" | sort -u || true)
86
+ if [ -n "$HITS_FONT" ]; then
87
+ VIOLATIONS="${VIOLATIONS}\n [typography weight] $(echo "$HITS_FONT" | tr '\n' ' ')"
88
+ fi
89
+
90
+ # 4) Leading numeric(leading-N where N is digit;skip leading-compact/normal/none/tight/snug/relaxed/loose)
91
+ HITS_LEADING=$(echo "$NEW_CONTENT" | grep -oE "\bleading-[0-9]+\b" | sort -u || true)
92
+ if [ -n "$HITS_LEADING" ]; then
93
+ VIOLATIONS="${VIOLATIONS}\n [leading numeric] $(echo "$HITS_LEADING" | tr '\n' ' ')"
94
+ fi
95
+
96
+ # 5) Tracking raw(skip tracking-shortcut canonical token)
97
+ HITS_TRACKING=$(echo "$NEW_CONTENT" | grep -oE "\btracking-(tighter|tight|wide|wider|widest)\b" | sort -u || true)
98
+ if [ -n "$HITS_TRACKING" ]; then
99
+ VIOLATIONS="${VIOLATIONS}\n [tracking raw] $(echo "$HITS_TRACKING" | tr '\n' ' ')"
100
+ fi
101
+
102
+ # 6) Radius out-of-range
103
+ HITS_RADIUS=$(echo "$NEW_CONTENT" | grep -oE "\brounded-(xl|2xl|3xl)\b" | sort -u || true)
104
+ if [ -n "$HITS_RADIUS" ]; then
105
+ VIOLATIONS="${VIOLATIONS}\n [radius out-of-range] $(echo "$HITS_RADIUS" | tr '\n' ' ')"
106
+ fi
107
+
108
+ # 7) Shadow Tailwind size(shadow-sm/md/lg/xl/2xl/inner — DS 用 shadow-[var(--elevation-N)] N∈{100,200})
109
+ HITS_SHADOW=$(echo "$NEW_CONTENT" | grep -oE "\bshadow-(sm|md|lg|xl|2xl|inner)\b" | sort -u || true)
110
+ if [ -n "$HITS_SHADOW" ]; then
111
+ VIOLATIONS="${VIOLATIONS}\n [elevation size] $(echo "$HITS_SHADOW" | tr '\n' ' ')"
112
+ fi
113
+
114
+ # 8) Shadcn compat alias(bg-popover / text-muted-foreground 等)
115
+ HITS_SHADCN=$(echo "$NEW_CONTENT" | grep -oE "\b(bg-popover|text-popover-foreground|text-muted-foreground|bg-accent|text-accent-foreground|bg-destructive|bg-background|text-background|border-input)\b" | sort -u || true)
116
+ if [ -n "$HITS_SHADCN" ]; then
117
+ VIOLATIONS="${VIOLATIONS}\n [shadcn alias] $(echo "$HITS_SHADCN" | tr '\n' ' ')"
118
+ fi
119
+
120
+ # 9) Primitive 色名作 utility(bg-neutral-N / text-blue-N 等)
121
+ HITS_PRIMITIVE=$(echo "$NEW_CONTENT" | grep -oE "\b(bg|text|border)-(neutral|blue|red|green|yellow|orange|purple|pink|cyan|teal|deep-orange|deep-purple|light-blue|light-green|lime|amber|indigo|brown|gray)-[0-9]+\b" | sort -u || true)
122
+ if [ -n "$HITS_PRIMITIVE" ]; then
123
+ VIOLATIONS="${VIOLATIONS}\n [primitive color as utility] $(echo "$HITS_PRIMITIVE" | tr '\n' ' ')"
124
+ fi
125
+
126
+ if [ -n "$VIOLATIONS" ]; then
127
+ cat >&2 <<'EOF_HEAD'
128
+
129
+ ⚠️ M23 / Dim 47 violation:Tailwind utility 繞 SSOT registry。
130
+ EOF_HEAD
131
+ echo -e "$VIOLATIONS" >&2
132
+ cat >&2 <<'EOF_BODY'
133
+
134
+ SSOT:node_modules/@qijenchen/design-system/src/tokens/utility-registry.json(block list per category)
135
+ 修法 quick map:
136
+ text-xs..9xl → text-h1..h6 / text-body / text-caption / text-helper / text-label
137
+ font-thin/...black → font-normal / font-medium / font-bold
138
+ leading-{N} → leading-compact / leading-normal
139
+ tracking-* → tracking-shortcut(or codify role-specific semantic utility)
140
+ rounded-xl.. → rounded-xs..rounded-lg / rounded-full
141
+ shadow-sm.. → shadow-[var(--elevation-100)] / shadow-[var(--elevation-200)]
142
+ bg-popover .. → direct semantic token(--surface-raised / --fg-muted / --error 等)
143
+ bg-neutral-N → semantic utility(bg-surface 等)或 bg-[var(--color-neutral-N)]
144
+
145
+ 詳 utility-registry.json `_meta.spec_sources` + M23「DS 內既有 canonical 優先」。
146
+ EOF_BODY
147
+ fi
148
+
149
+ exit 0
@@ -0,0 +1,196 @@
1
+ #!/bin/bash
2
+ # Pattern invariants unified hook(2026-05-08 cluster C consolidation)
3
+ #
4
+ # Merges 4 PreToolUse hooks(原各檔已 retire,合併入此):
5
+ # C.1 overlay panel scroll chain(原 check_overlay_panel_scroll_chain,P1 WARN stderr)
6
+ # C.2 inline-action canonical gap(原 check_inline_action_canonical_gap,P1 WARN stderr)
7
+ # C.3 primitive wrapper padding(原 check_primitive_wrapper_padding,P0 BLOCK exit 2)
8
+ # C.4 row slot handcraft(原 check_row_slot_handcraft,P0 BLOCK exit 2)
9
+ #
10
+ # Why merge:皆 element-anatomy / overlay-surface SSOT 消費紀律 invariant,共用 INPUT
11
+ # parsing + tsx filter,散裝是 M17 + Anthropic ≤ 15 hook best-practice 偏離。
12
+ #
13
+ # Exit precedence:BLOCK(2)> WARN-stderr(0)。每 rule 獨立 fire,worst 勝。
14
+ #
15
+ # Per-rule allowlist:
16
+ # C.1: `// @scroll-chain-allow: <reason>`(any line)
17
+ # C.2: `// @inline-action-gap-allow: <reason>`(any line)
18
+ # C.3: `// @primitive-padding-allow: <reason>`(檔案前 5 行)
19
+ # C.4: `// @row-slot-handcraft-allow: <reason>`(any line)
20
+
21
+ source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
22
+
23
+ set -uo pipefail
24
+
25
+ INPUT=$(cat)
26
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
27
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
28
+
29
+ case "$TOOL" in
30
+ Edit|Write|MultiEdit) ;;
31
+ *) exit 0 ;;
32
+ esac
33
+
34
+ case "$FILE_PATH" in
35
+ *.tsx) ;;
36
+ *) exit 0 ;;
37
+ esac
38
+
39
+ NEW_CONTENT=$(echo "$INPUT" | jq -r '
40
+ (.tool_input.content // "") + "\n" +
41
+ (.tool_input.new_string // "") + "\n" +
42
+ ([.tool_input.edits[]? | .new_string] | join("\n"))
43
+ ' 2>/dev/null || echo "")
44
+
45
+ [ -z "${NEW_CONTENT//[[:space:]]/}" ] && exit 0
46
+
47
+ WORST=0
48
+ record_worst() { local lvl=$1; [ "$lvl" -gt "$WORST" ] && WORST=$lvl; }
49
+
50
+ # ── C.1 overlay panel scroll chain(P1 WARN stderr only)──────────────────────
51
+ if ! echo "$NEW_CONTENT" | grep -q '@scroll-chain-allow' \
52
+ && echo "$NEW_CONTENT" | grep -q '<SurfaceBody'; then
53
+ SUSPECT_C1=$(printf '%s' "$NEW_CONTENT" | awk '
54
+ /^[[:space:]]*<div[[:space:]]/ {
55
+ line = $0
56
+ while (index(line, ">") == 0 && (getline next_line) > 0) line = line " " next_line
57
+ if (line ~ /w-\[/ && (line !~ /flex[^"]*flex-col/ || line !~ /h-full/ || line !~ /min-h-0/)) {
58
+ for (k = 0; k < 30 && (getline next_line) > 0; k++) {
59
+ if (next_line ~ /<SurfaceBody/) {
60
+ print "[panel root w-[...] 缺 flex flex-col h-full min-h-0]: " substr(line, 1, 100)
61
+ break
62
+ }
63
+ if (next_line ~ /<\/div>/) break
64
+ }
65
+ }
66
+ }
67
+ ')
68
+ if [ -n "$SUSPECT_C1" ]; then
69
+ cat >&2 <<EOF
70
+
71
+ ┄┄┄ C.1 check_pattern_invariants — overlay scroll chain WARN ┄┄┄
72
+
73
+ [P1] ${FILE_PATH}
74
+ ${SUSPECT_C1}
75
+
76
+ ⚠️ M25 canonical:Popover/HoverCard/Dialog/Sheet viewport-aware scroll 要求 root → SurfaceBody
77
+ 所有中間 wrapper forward \`flex flex-col h-full\`。斷鏈 → SurfaceBody flex-1 失效。
78
+
79
+ 修法:wrapper className 加 \`flex flex-col h-full\`(最小組合 \`flex flex-col h-full min-h-0\`)
80
+ 詳 patterns/overlay-surface/overlay-surface.spec.md「Viewport-aware scroll chain invariant」/ M25
81
+ 例外:行尾 \`// @scroll-chain-allow: <reason>\`
82
+
83
+ EOF
84
+ fi
85
+ fi
86
+
87
+ # ── C.2 inline-action canonical gap(P1 WARN stderr only)─────────────────────
88
+ case "$FILE_PATH" in
89
+ *components/*.tsx|*patterns/*.tsx)
90
+ case "$FILE_PATH" in
91
+ */item-anatomy.tsx|*.stories.tsx|*.test.*) ;; # SSOT/test skip
92
+ *)
93
+ if ! echo "$NEW_CONTENT" | grep -q '@inline-action-gap-allow' \
94
+ && echo "$NEW_CONTENT" | grep -qE '<ItemInlineAction(Button)?\b|DropdownMenuTrigger.*ItemInlineAction'; then
95
+ WRONG_GAP=$(echo "$NEW_CONTENT" | grep -nE 'className=.*\bgap-(1|3|4|5|6|8|10|12)\b' | head -3 || true)
96
+ if [ -n "$WRONG_GAP" ]; then
97
+ cat >&2 <<EOF
98
+
99
+ ┄┄┄ C.2 check_pattern_invariants — inline-action gap WARN ┄┄┄
100
+
101
+ [P1] ${FILE_PATH}
102
+ ItemInlineAction* consumer 用非 \`gap-2\` 的 gap class:
103
+ ${WRONG_GAP}
104
+
105
+ ⚠️ inline-action.spec.md:80:inline-action 跟 sibling gap 必 \`gap-2\`(8px)。
106
+ 12px = --table-cell-px(cell L/R padding,不是 inline-action gap)。
107
+
108
+ 修法:className gap → gap-2 / 加 \`// @inline-action-gap-allow: <reason>\` 行尾豁免
109
+
110
+ EOF
111
+ fi
112
+ fi
113
+ ;;
114
+ esac
115
+ ;;
116
+ esac
117
+
118
+ # ── C.3 primitive wrapper padding(P0 BLOCK exit 2)───────────────────────────
119
+ # File-level allowlist:檔頭前 5 行
120
+ ALLOW_C3=0
121
+ FIRST_LINES_NEW=$(printf '%s\n' "$NEW_CONTENT" | sed -n '1,5p')
122
+ echo "$FIRST_LINES_NEW" | grep -qE '//[[:space:]]*@primitive-padding-allow:' && ALLOW_C3=1
123
+ if [ -f "$FILE_PATH" ] && [ "$ALLOW_C3" = "0" ]; then
124
+ ON_DISK_FIRST=$(sed -n '1,5p' "$FILE_PATH" 2>/dev/null || true)
125
+ echo "$ON_DISK_FIRST" | grep -qE '//[[:space:]]*@primitive-padding-allow:' && ALLOW_C3=1
126
+ fi
127
+ if [ "$ALLOW_C3" = "0" ]; then
128
+ PRIMITIVES_REGEX='DateGrid|Calendar|Surface|SurfaceHeader|SurfaceBody|SurfaceFooter'
129
+ VIOLATIONS_C3=$(printf '%s' "$NEW_CONTENT" | perl -0777 -ne '
130
+ while (/<div\b[^>]*className\s*=\s*["\x27`][^"\x27`]*\bp-\d+[^"\x27`]*["\x27`][^>]*>(?:[^<]*<(?!\/div\b)){0,8}\s*<('"$PRIMITIVES_REGEX"')\b/gs) {
131
+ my $primitive = $1;
132
+ my $offset = $-[0];
133
+ my $before = substr($_, 0, $offset);
134
+ my $line = ($before =~ tr/\n//) + 1;
135
+ print "line $line: <div className=\"...p-X...\"> wrapping <$primitive>\n";
136
+ }
137
+ ')
138
+ if [ -n "$VIOLATIONS_C3" ]; then
139
+ cat >&2 <<EOF
140
+
141
+ ┄┄┄ C.3 check_pattern_invariants — primitive wrapper padding BLOCKER ┄┄┄
142
+
143
+ [P0] ${FILE_PATH}
144
+ ${VIOLATIONS_C3}
145
+
146
+ SSOT primitive 自帶 outer padding,**consumer 不得另加 padding wrapper**:
147
+ - DateGrid / Calendar:root 自帶 p-3
148
+ - Surface{Header,Body,Footer}:各 segment 自帶 padding
149
+
150
+ 修法:直接放 primitive,不包 padding div。
151
+ ❌ <div className="p-2"><DateGrid /></div>
152
+ ✅ <DateGrid />
153
+
154
+ 整檔豁免:檔頭前 5 行加 \`// @primitive-padding-allow: <reason>\`(需 spec rationale)。
155
+ 詳 mindset #2 + M1 SSOT 消費。
156
+
157
+ EOF
158
+ record_worst 2
159
+ fi
160
+ fi
161
+
162
+ # ── C.4 row slot handcraft(P0 BLOCK exit 2)──────────────────────────────────
163
+ case "$FILE_PATH" in
164
+ *components/*.tsx|*patterns/*.tsx)
165
+ case "$FILE_PATH" in
166
+ */item-anatomy.tsx|*/field-wrapper.tsx|*.stories.tsx|*.test.*|*.spec.tsx) ;; # SSOT/test skip
167
+ *)
168
+ # 2026-05-30(dim 39 M7/M34 fix):order-INDEPENDENT — extract className attrs,require ALL 4 tokens
169
+ # present(natural Tailwind 序 `flex items-center gap-2 shrink-0 h-[1lh]` 之前漏抓)。BSD/GNU grep 通用。
170
+ if ! echo "$NEW_CONTENT" | grep -q '@row-slot-handcraft-allow' \
171
+ && echo "$NEW_CONTENT" | grep -oE 'class(Name)?="[^"]*"' | grep -F 'h-[1lh]' | grep -F 'shrink-0' | grep -F 'flex' | grep -F 'items-center' >/dev/null 2>&1; then
172
+ cat >&2 <<EOF
173
+
174
+ ┄┄┄ C.4 check_pattern_invariants — row slot handcraft BLOCKER ┄┄┄
175
+
176
+ [P0] ${FILE_PATH}
177
+ 偵測自刻 row-layout slot:\`<span class="h-[1lh] shrink-0 flex items-center...">\`
178
+ M1+M17 違反 — 該消費 L1 primitive。
179
+
180
+ ⚠️ M19 canonical(2026-05-05 v8):row prefix/suffix slot 必走 patterns/element-anatomy:
181
+ import { ItemPrefix, ItemSuffix } from '@/design-system/patterns/element-anatomy/item-anatomy'
182
+ <ItemPrefix><StartIcon /></ItemPrefix>
183
+ <ItemSuffix>{chevron}</ItemSuffix>
184
+
185
+ \`<ItemPrefix>\` / \`<ItemSuffix>\` 永遠 \`h-[1lh] shrink-0 flex items-center\`(item-anatomy.spec.md:175+190)。
186
+ 例外:行尾 \`// @row-slot-handcraft-allow: <reason>\`
187
+
188
+ EOF
189
+ record_worst 2
190
+ fi
191
+ ;;
192
+ esac
193
+ ;;
194
+ esac
195
+
196
+ exit $WORST
@@ -0,0 +1,86 @@
1
+ #!/bin/bash
2
+ # check_sidebar_menu_button_implicit_wrap.sh — PreToolUse Edit/Write 攔 SidebarMenuButton 沒 asChild + children 含 ItemAvatar/Avatar/Icon 致隱式 wrap 垂直 stack(2026-05-27)
3
+ #
4
+ # Per user 2026-05-27 抓 UserFooter Avatar 垂直 stack drift:
5
+ # SidebarMenuButton(sidebar.tsx L1036-1043)在沒 asChild 時把所有 children 塞進 <ItemLabel> 單一 span,
6
+ # Avatar + text-span 都在同 span 內 → 強迫垂直堆疊。
7
+ #
8
+ # Canonical:含 Avatar/ItemAvatar/icon prefix 的 SidebarMenuButton 必用 asChild + <div> wrap,
9
+ # per sidebar.tsx:1025-1027 docblock:「asChild 的 consumer 自行放 icon + label」+ DS canonical
10
+ # sidebar.stories.tsx#UserFooter 範例 L76-104。
11
+ #
12
+ # Detection:
13
+ # `<SidebarMenuButton ...>` (no asChild) ... `<ItemAvatar` or `<Avatar` inside → BLOCKER
14
+ # ALLOW:`<SidebarMenuButton startIcon={X}>label</SidebarMenuButton>`(用 startIcon prop 不 wrap)
15
+ # ALLOW:`<SidebarMenuButton asChild>...</SidebarMenuButton>`(consumer 自管 layout)
16
+
17
+ source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
18
+
19
+ set -uo pipefail
20
+ INPUT=$(cat 2>/dev/null || echo "{}")
21
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
22
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
23
+ EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
24
+ NEW=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""' 2>/dev/null)
25
+
26
+ [ "$EVENT" != "PreToolUse" ] && exit 0
27
+ case "$TOOL" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac
28
+ case "$FILE_PATH" in *.tsx) ;; *) exit 0 ;; esac
29
+ case "$FILE_PATH" in *.test.tsx) exit 0 ;; esac
30
+
31
+ # Python multiline regex:`<SidebarMenuButton` 不含 `asChild` 的 block + 內含 Avatar/ItemAvatar prefix
32
+ HAS_DRIFT=$(printf '%s' "$NEW" | python3 -c '
33
+ import sys, re
34
+ content = sys.stdin.read()
35
+ # Find SidebarMenuButton blocks(opening tag → closing tag,non-greedy multiline)
36
+ # match opening `<SidebarMenuButton ...>` capture full open tag
37
+ for m in re.finditer(r"<SidebarMenuButton\b([^>]*)>(.*?)</SidebarMenuButton>", content, re.DOTALL):
38
+ open_tag, body = m.group(1), m.group(2)
39
+ # Skip if asChild present
40
+ if re.search(r"\basChild\b", open_tag):
41
+ continue
42
+ # Drift = body 含 <ItemAvatar 或 <Avatar(both = avatar prefix)
43
+ if re.search(r"<(ItemAvatar|Avatar)\b", body):
44
+ print("DRIFT")
45
+ sys.exit(0)
46
+ ' 2>/dev/null)
47
+
48
+ [ "$HAS_DRIFT" != "DRIFT" ] && exit 0
49
+
50
+ # Override env(audit-logged)
51
+ if [ "${CLAUDE_BYPASS_SIDEBAR_MENU_BUTTON_WRAP:-0}" = "1" ]; then
52
+ mkdir -p "$(dirname "$0")/../logs" 2>/dev/null
53
+ printf '{"ts":"%s","event":"sidebar-menu-button-wrap-bypass","file":"%s"}\n' \
54
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$FILE_PATH" >> "$(dirname "$0")/../logs/governance-bypass.jsonl" 2>/dev/null
55
+ exit 0
56
+ fi
57
+
58
+ REL=${FILE_PATH#"$CLAUDE_PROJECT_DIR"/}
59
+
60
+ cat >&2 <<'EOF'
61
+ 🚨 SidebarMenuButton implicit-wrap canonical violation(per user 2026-05-27 UserFooter 垂直 stack 事件):
62
+
63
+ 🔍 偵測:SidebarMenuButton 內含 ItemAvatar 或 Avatar 但 **無 asChild**
64
+
65
+ ⚠️ 後果(per sidebar.tsx:1036-1043 source code):
66
+ SidebarMenuButton 沒 asChild → children 全塞進 ItemLabel(單 span)
67
+ → Avatar + text 在同 span 內 → 強迫垂直堆疊(user 抓的 bug)
68
+
69
+ 修法(per DS canonical sidebar.stories.tsx#UserFooter):
70
+
71
+ ❌ SidebarMenuButton id="..." 直接放 ItemAvatar + span 為 children
72
+
73
+ ✅ SidebarMenuButton asChild 包 div role="group" 內含 ItemAvatar + span data-sidebar="menu-label" min-w-0 flex-1 truncate
74
+
75
+ Or use startIcon prop(自動 layout,不 wrap):
76
+ ✅ SidebarMenuButton startIcon={SomeLucideIcon} 直接 children 為純文字
77
+
78
+ Bypass(極罕見):CLAUDE_BYPASS_SIDEBAR_MENU_BUTTON_WRAP=1 env var(audit-logged)。
79
+
80
+ Citation:
81
+ - packages/design-system/src/components/Sidebar/sidebar.tsx:1025-1043(asChild docblock)
82
+ - packages/design-system/src/components/Sidebar/sidebar.stories.tsx UserFooter L76-104(canonical)
83
+ EOF
84
+ # 2026-05-31:exit 0 → exit 2(folded-hook-audit:原宣稱 BLOCKER 但 exit 0 = 假 enforcement;
85
+ # SidebarMenuButton implicit-wrap 是 SSOT canonical,verified clean on 現有 sidebar code + 有 env escape)。
86
+ exit 2
@@ -0,0 +1,79 @@
1
+ #!/bin/bash
2
+ # check_tailwind_wildcard_in_docs.sh — P0 BLOCKER
3
+ #
4
+ # Per 2026-05-28 beta.27 release fail anchor(6+ CI iterations 燒):
5
+ # Tailwind v4 vite plugin scans .md / .spec.md / docs by default。
6
+ # 文件範例 like `shadow-[var(--elevation-*)]` 或 `var(--field-height-*)` /
7
+ # `var(--elevation-100/200/300)` 是給 dev 看的 shorthand notation(`*` / `/` 代表
8
+ # enumeration placeholder),但 Tailwind 不知 → 當 literal class string 抓 → 產
9
+ # invalid CSS `var(--X-*)` `var(--X-A/B/C)` → Storybook FULL smoke 死。
10
+ #
11
+ # 機械強制 PreToolUse Edit/Write:阻止寫入新檔含此 anti-pattern。
12
+ # 改用 math notation:`var(--X-N) N∈{a,b,c}` Tailwind 不會誤判。
13
+ #
14
+ # Scope:所有 .md / .spec.md / .sh / .ts / .tsx 寫入時 grep new_string。
15
+ # Exit 2 BLOCKER + cite this anchor。Escape:`@tailwind-wildcard-allow:` comment。
16
+
17
+ source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
18
+
19
+ set -uo pipefail
20
+
21
+ INPUT=$(cat 2>/dev/null || echo "{}")
22
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
23
+
24
+ case "${TOOL:-}" in
25
+ Edit|Write|MultiEdit) ;;
26
+ *) exit 0 ;;
27
+ esac
28
+
29
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
30
+ # Self-exemption:本 hook help-text 合法含 anti-pattern literal 作為文件範例(且 .sh 不被 Tailwind
31
+ # vite plugin 掃,無 build 風險)→ 不掃自己,避免 self-trigger false-positive(2026-05-30 test-surfaced)。
32
+ case "$FILE" in */check_tailwind_wildcard_in_docs.sh) exit 0 ;; esac
33
+ # 2026-06-11 R2 held-item #13:.claude/{tmp,logs} 歷史 artifact(codex brief / reply、audit findings
34
+ # json、preflight log)非 Tailwind 掃描對象 — src/globals.css 用 explicit positive @source(只掃 src/ +
35
+ # packages/{design-system,storybook-config} + .storybook/)且 `@source not "**/.claude/**"`;盤上 12 檔
36
+ # 已含 antiPattern 而 build 連續綠(至 beta.61)= 零 build 風險實證。掃描對象路徑(src/packages/docs)
37
+ # 保護不變。
38
+ case "$FILE" in */.claude/tmp/*|*/.claude/logs/*|.claude/tmp/*|.claude/logs/*) exit 0 ;; esac
39
+ # Only check files Tailwind v4 might scan
40
+ if ! echo "$FILE" | grep -qE '\.(md|spec\.md|sh|ts|tsx|css|json)$'; then exit 0; fi
41
+
42
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
43
+ [ -z "$CONTENT" ] && exit 0
44
+
45
+ # Escape clause
46
+ if echo "$CONTENT" | grep -qE '@tailwind-wildcard-allow:'; then exit 0; fi
47
+
48
+ # Detect anti-patterns(class form with wildcard / slash enumeration in CSS var)
49
+ # 2026-05-30 fix(test-surfaced M34 over-narrow):slash-segment 改 repeatable,
50
+ # 否則漏多段斜線列舉形式(beta.27 anchor 的 N-段 enum,hook header 列為必擋 anti-pattern)。
51
+ ANTI_PATTERNS=$(echo "$CONTENT" | grep -oE 'var\(--[a-z][a-z0-9-]*([\*/]+[a-z0-9-]*)+\)' | sort -u)
52
+
53
+ if [ -n "$ANTI_PATTERNS" ]; then
54
+ cat >&2 << EOF
55
+ 🚨 TAILWIND v4 WILDCARD-IN-DOCS BLOCKER(P0,2026-05-28 beta.27 anchor)
56
+
57
+ File: $FILE
58
+ Detected anti-pattern(s):
59
+ $(echo "$ANTI_PATTERNS" | sed 's/^/ /')
60
+
61
+ Why blocked:
62
+ Tailwind v4 vite plugin scans .md/.ts/etc → 把 \`var(--X-*)\` / \`var(--X-A/B/C)\`
63
+ 當 literal class string 抓 → 產 invalid CSS(CSS 變數名禁 \`*\` / \`/\`)→ Storybook
64
+ build 死 → release CI fail。本 anchor:beta.27 6+ CI iteration 燒此問題。
65
+
66
+ 改用 math notation(Tailwind 不誤判):
67
+ var(--elevation-*) → var(--elevation-N) N∈{100,200}
68
+ var(--elevation-100/200) → var(--elevation-N) N∈{100,200}
69
+ var(--field-height-*) → var(--field-height-N) N∈{sm,md,lg}
70
+ var(--layout-space-*) → var(--layout-space-N) N∈{loose,tight}
71
+ var(--radix-*-available-height) → var(--radix-{popover|hover-card|dialog}-content-available-height)
72
+
73
+ Escape(極罕見,本 file 真需要 wildcard token literal):
74
+ add \`// @tailwind-wildcard-allow: <rationale>\` to file content
75
+ EOF
76
+ exit 2
77
+ fi
78
+
79
+ exit 0