@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,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
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Field family unified invariant hook(2026-05-08 cluster A consolidation)
|
|
3
|
+
#
|
|
4
|
+
# Merges PreToolUse hooks(原各檔已 retire,合併入此)— 共 5 條 sub-rules:
|
|
5
|
+
# A.1 naked row-mode propagation(原 check_naked_row_mode_propagation,P0 BLOCKER)
|
|
6
|
+
# A.2 FieldControlGroup wrapper direct child(原 check_field_control_group_direct_child,P1 WARN)
|
|
7
|
+
# A.3 Field state ring SSOT(原 check_field_state_token_consume 3 sub-rules,P0 BLOCKER)
|
|
8
|
+
# A.4 disabled placeholder color(原 check_disabled_placeholder_color,P1 stderr only)
|
|
9
|
+
# A.5 _Group child fieldCtx.id 隔離(2026-05-31 折入,M4 AR34 regression detector,P0 BLOCKER)
|
|
10
|
+
#
|
|
11
|
+
# Why merge:皆 Field 家族 invariant,共用 INPUT parsing + Edit/Write filter pattern,
|
|
12
|
+
# 分散在 4 個 hook 是「散裝 SSOT」(M17 + Anthropic ≤ 15 hook best practice 違反)。
|
|
13
|
+
#
|
|
14
|
+
# Exit code precedence:BLOCK(2)> WARN(1)> INFO(0)。每 rule 可獨立觸發,worst 勝。
|
|
15
|
+
#
|
|
16
|
+
# Per-rule allowlist(各自獨立):
|
|
17
|
+
# A.1: `// @naked-row-mode-allow: <reason>`
|
|
18
|
+
# A.2: `// @fcg-wrapper-allow: <reason>` 或檔頭
|
|
19
|
+
# A.3: `// @field-state-ring-allow: <reason>`
|
|
20
|
+
# A.4: `// @disabled-color-allow: <reason>`
|
|
21
|
+
# A.5: `// @group-fieldctx-allow: <reason>`
|
|
22
|
+
|
|
23
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
24
|
+
|
|
25
|
+
set -uo pipefail
|
|
26
|
+
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
29
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
30
|
+
|
|
31
|
+
# Tool filter — 只 Edit/Write/MultiEdit 跑
|
|
32
|
+
case "$TOOL" in
|
|
33
|
+
Edit|Write|MultiEdit) ;;
|
|
34
|
+
*) exit 0 ;;
|
|
35
|
+
esac
|
|
36
|
+
|
|
37
|
+
# 不是 .tsx / .ts 直接過(各 rule 內部還會再 narrow)
|
|
38
|
+
case "$FILE_PATH" in
|
|
39
|
+
*.tsx|*.ts) ;;
|
|
40
|
+
*) exit 0 ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
# 讀 merged content(舊檔 + 新 edit 拼起)— A.1 / A.3 需要整檔判 naked variant 存在性
|
|
44
|
+
FILE_CONTENT=""
|
|
45
|
+
if [ -f "$FILE_PATH" ]; then
|
|
46
|
+
FILE_CONTENT=$(cat "$FILE_PATH")
|
|
47
|
+
fi
|
|
48
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '
|
|
49
|
+
(.tool_input.content // "") + "\n" +
|
|
50
|
+
(.tool_input.new_string // "") + "\n" +
|
|
51
|
+
([.tool_input.edits[]? | .new_string] | join("\n"))
|
|
52
|
+
' 2>/dev/null || echo "")
|
|
53
|
+
|
|
54
|
+
# A.2 / A.4 只看 NEW_CONTENT(diff-level signal),A.1 / A.3 看 MERGED
|
|
55
|
+
MERGED_CONTENT="${FILE_CONTENT}
|
|
56
|
+
${NEW_CONTENT}"
|
|
57
|
+
|
|
58
|
+
[ -z "${MERGED_CONTENT//[[:space:]]/}" ] && exit 0
|
|
59
|
+
|
|
60
|
+
WORST=0
|
|
61
|
+
record_worst() { local lvl=$1; [ "$lvl" -gt "$WORST" ] && WORST=$lvl; }
|
|
62
|
+
|
|
63
|
+
# ── A.1 naked row-mode propagation(P0 BLOCKER)─────────────────────────────────
|
|
64
|
+
case "$FILE_PATH" in
|
|
65
|
+
*components/*.tsx)
|
|
66
|
+
case "$FILE_PATH" in
|
|
67
|
+
*/field-wrapper.tsx|*/textarea.tsx) ;; # SSOT host skip
|
|
68
|
+
*)
|
|
69
|
+
if ! echo "$MERGED_CONTENT" | grep -q '@naked-row-mode-allow' \
|
|
70
|
+
&& echo "$MERGED_CONTENT" | grep -E "variant:\s*['\"]naked['\"]|variant=\{?['\"]naked['\"]" >/dev/null \
|
|
71
|
+
&& echo "$MERGED_CONTENT" | tr '\n' ' ' | grep -E "(inline-flex|flex)[^\"'\`]*items-center" >/dev/null \
|
|
72
|
+
&& ! echo "$MERGED_CONTENT" | grep -q "nakedCellRowModeAlign"; then
|
|
73
|
+
cat >&2 <<EOF
|
|
74
|
+
|
|
75
|
+
┄┄┄ A.1 check_field_family_invariants — naked row-mode propagation BLOCKER ┄┄┄
|
|
76
|
+
|
|
77
|
+
[P0] ${FILE_PATH}
|
|
78
|
+
偵測到此檔消費 \`variant="naked"\` + 內部 wrapper hardcode \`items-center\`,
|
|
79
|
+
但**未** import / apply \`nakedCellRowModeAlign\` SSOT。
|
|
80
|
+
|
|
81
|
+
⚠️ M19 canonical:naked variant 元件所有內部 wrapper 必 propagate host cell
|
|
82
|
+
\`data-row-mode\`(autoRow→items-start / fixed→items-center)。
|
|
83
|
+
|
|
84
|
+
修法:
|
|
85
|
+
1. import { nakedCellRowModeAlign } from '@/design-system/components/Field/field-wrapper'
|
|
86
|
+
2. wrapper className 加上 SSOT(eg \`cn('flex-1 min-w-0 inline-flex items-center', nakedCellRowModeAlign)\`)
|
|
87
|
+
3. 例外:行尾 \`// @naked-row-mode-allow: <reason>\`
|
|
88
|
+
|
|
89
|
+
EOF
|
|
90
|
+
record_worst 2
|
|
91
|
+
fi
|
|
92
|
+
;;
|
|
93
|
+
esac
|
|
94
|
+
;;
|
|
95
|
+
esac
|
|
96
|
+
|
|
97
|
+
# ── A.2 FieldControlGroup wrapper direct child(P1 WARN)────────────────────────
|
|
98
|
+
case "$FILE_PATH" in
|
|
99
|
+
*.tsx)
|
|
100
|
+
if echo "$NEW_CONTENT" | grep -q '<FieldControlGroup' \
|
|
101
|
+
&& ! echo "$NEW_CONTENT" | grep -q '@fcg-wrapper-allow'; then
|
|
102
|
+
SUSPECT=$(printf '%s' "$NEW_CONTENT" | awk '
|
|
103
|
+
/<FieldControlGroup/ { inFCG=1; next }
|
|
104
|
+
/<\/FieldControlGroup>/ { inFCG=0; next }
|
|
105
|
+
inFCG && /^[[:space:]]*<(div|span)[[:space:]>]/ {
|
|
106
|
+
if ($0 !~ /display:[[:space:]]*contents/ && $0 !~ /@fcg-wrapper-allow/) print NR ":" $0
|
|
107
|
+
}
|
|
108
|
+
')
|
|
109
|
+
if [ -n "$SUSPECT" ]; then
|
|
110
|
+
cat >&2 <<EOF
|
|
111
|
+
|
|
112
|
+
┄┄┄ A.2 check_field_family_invariants — FieldControlGroup wrapper WARN ┄┄┄
|
|
113
|
+
|
|
114
|
+
[P1] ${FILE_PATH}
|
|
115
|
+
偵測到 FieldControlGroup 內有 \`<div>\` / \`<span>\` wrapper(可能破壞 CSS \`[&>*]\` variants):
|
|
116
|
+
${SUSPECT}
|
|
117
|
+
|
|
118
|
+
修法 3 擇 1:
|
|
119
|
+
1. 移除 wrapper,Field control 直接是 FieldControlGroup direct child
|
|
120
|
+
2. 透過 prop forward className(eg \`<FilterValuePicker className="flex-1 min-w-0">\`)
|
|
121
|
+
3. wrapper 用 \`display:contents\` / 加 \`// @fcg-wrapper-allow: <reason>\`
|
|
122
|
+
|
|
123
|
+
EOF
|
|
124
|
+
record_worst 1
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
;;
|
|
128
|
+
esac
|
|
129
|
+
|
|
130
|
+
# ── A.3 Field state ring SSOT(P0 BLOCKER,3 sub-rules)─────────────────────────
|
|
131
|
+
case "$FILE_PATH" in
|
|
132
|
+
*components/*.tsx)
|
|
133
|
+
case "$FILE_PATH" in
|
|
134
|
+
*/field-wrapper.tsx|*/textarea.tsx|*.stories.tsx|*.test.*|*.spec.tsx) ;; # SSOT/test skip
|
|
135
|
+
*)
|
|
136
|
+
if ! echo "$NEW_CONTENT" | grep -q '@field-state-ring-allow'; then
|
|
137
|
+
# A.3.1 舊 box-shadow inset
|
|
138
|
+
if echo "$NEW_CONTENT" | grep -E "(hover|focus-within|data-\[state=open\]):shadow-\[inset" >/dev/null; then
|
|
139
|
+
cat >&2 <<'EOF'
|
|
140
|
+
|
|
141
|
+
┄┄┄ A.3.1 check_field_family_invariants — Field state ring shadow inset BLOCKER ┄┄┄
|
|
142
|
+
|
|
143
|
+
[P0] naked variant state ring 用 `box-shadow inset` — v9 retire pattern。
|
|
144
|
+
修法:讓 Field default state machine 自動繼承(border-based);cell hover 用 `nakedCellEditableDisplayHover` const。
|
|
145
|
+
|
|
146
|
+
EOF
|
|
147
|
+
record_worst 2
|
|
148
|
+
fi
|
|
149
|
+
# A.3.2 自寫 outline state ring
|
|
150
|
+
if echo "$NEW_CONTENT" | grep -E "(hover|focus-within|focus-visible|data-\[state=open\]):outline-(border|primary)" >/dev/null; then
|
|
151
|
+
cat >&2 <<'EOF'
|
|
152
|
+
|
|
153
|
+
┄┄┄ A.3.2 check_field_family_invariants — Field state ring outline BLOCKER ┄┄┄
|
|
154
|
+
|
|
155
|
+
[P0] 自寫 `hover:outline-border` / `focus-within:outline-primary` — v13 canonical 禁。
|
|
156
|
+
修法:Field default 自動繼承 / cell 用 `nakedCellEditableDisplayHover` const。
|
|
157
|
+
|
|
158
|
+
EOF
|
|
159
|
+
record_worst 2
|
|
160
|
+
fi
|
|
161
|
+
# A.3.3 per-control open=blue override(v13.5)
|
|
162
|
+
# 2026-06-11 deep-audit R2(n=38)精準化(M7 broad-vs-narrow 3-column):
|
|
163
|
+
# Spec wording:v13.3 禁「open 時 per-control 把 border 轉 primary(藍)」。
|
|
164
|
+
# Hook regex(舊):substring 無 boundary + 不分註解行。
|
|
165
|
+
# Gap:誤命中 (a) `data-[state=open]:border-primary-hover` canonical(button.tsx:99,109
|
|
166
|
+
# overlay trigger 維持 hover 樣式,-hover/-active 是合法 token 後綴);(b) v13.3 retire
|
|
167
|
+
# 註解內引用舊 pattern 文字(combobox.tsx:725 / select.tsx:594)。
|
|
168
|
+
# 修:純註解行剝離 + border-primary 加字尾 boundary([^-a-zA-Z]|$)。
|
|
169
|
+
# DS-wide counter-example scan 2026-06-11:新 regex 全 components/ 0 hit(真違規 shape
|
|
170
|
+
# `open && 'border-primary'` / `data-[state=open]:border-primary` 結尾 仍 BLOCK,fixture 驗證)。
|
|
171
|
+
A33_CONTENT=$(echo "$NEW_CONTENT" | grep -vE '^[[:space:]]*(//|\*|/\*)')
|
|
172
|
+
if echo "$A33_CONTENT" | grep -E "(open|isOpen) +&& +.{0,40}('border-primary'|\"border-primary\")" >/dev/null \
|
|
173
|
+
|| echo "$A33_CONTENT" | grep -E "data-\[state=open\]:border-primary([^-a-zA-Z]|$)" >/dev/null; then
|
|
174
|
+
cat >&2 <<'EOF'
|
|
175
|
+
|
|
176
|
+
┄┄┄ A.3.3 check_field_family_invariants — per-control open=blue BLOCKER ┄┄┄
|
|
177
|
+
|
|
178
|
+
[P0] per-control `open && 'border-primary'` / `data-[state=open]:border-primary` — v13.3 canonical「focus dominates everything」禁。
|
|
179
|
+
修法:刪 override。Radix Popover open 時 trigger 通常 focused → focus-within fires → 藍(自然 Ant 風)。改 Field default SSOT 須 spec 補 rationale。
|
|
180
|
+
|
|
181
|
+
EOF
|
|
182
|
+
record_worst 2
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
;;
|
|
186
|
+
esac
|
|
187
|
+
;;
|
|
188
|
+
esac
|
|
189
|
+
|
|
190
|
+
# ── A.4 disabled placeholder color(P1 stderr,exit 0 不 block)──────────────────
|
|
191
|
+
if ! echo "$NEW_CONTENT" | grep -q '@disabled-color-allow'; then
|
|
192
|
+
SUSPECT_DP=""
|
|
193
|
+
if echo "$NEW_CONTENT" | grep -E "placeholder:text-fg-muted" >/dev/null \
|
|
194
|
+
&& ! echo "$NEW_CONTENT" | grep -E "(disabled:placeholder:text-fg-disabled|group-data-\[field-mode=disabled\].*placeholder:text-fg-disabled|resolvedMode\s*===\s*'disabled'.*text-fg-disabled)" >/dev/null; then
|
|
195
|
+
SUSPECT_DP="$SUSPECT_DP [placeholder:text-fg-muted 無 disabled override]"
|
|
196
|
+
fi
|
|
197
|
+
if echo "$NEW_CONTENT" | tr '\n' ' ' | grep -E '<span[^>]*"text-fg-muted"[^>]*>[[:space:]]*\{[^}]*placeholder' >/dev/null 2>&1 \
|
|
198
|
+
&& ! echo "$NEW_CONTENT" | grep -E "resolvedMode\s*===\s*'disabled'" >/dev/null; then
|
|
199
|
+
SUSPECT_DP="$SUSPECT_DP [<span text-fg-muted>{placeholder} 不分 mode]"
|
|
200
|
+
fi
|
|
201
|
+
if [ -n "$SUSPECT_DP" ]; then
|
|
202
|
+
cat >&2 <<EOF
|
|
203
|
+
|
|
204
|
+
┄┄┄ A.4 check_field_family_invariants — disabled placeholder color WARN ┄┄┄
|
|
205
|
+
|
|
206
|
+
[P1] ${FILE_PATH}
|
|
207
|
+
${SUSPECT_DP}
|
|
208
|
+
修法:disabled:placeholder:text-fg-disabled / group-data-[field-mode=disabled]/field:placeholder:text-fg-disabled / JSX 條件
|
|
209
|
+
詳:tokens/color/color.spec.md「Disabled state precedence canonical」/ M24
|
|
210
|
+
例外:行尾 \`// @disabled-color-allow: <reason>\`
|
|
211
|
+
|
|
212
|
+
EOF
|
|
213
|
+
# A.4 原 hook exit 0(stderr only),保持向後兼容不升 WORST
|
|
214
|
+
fi
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# ── A.5 _Group child fieldCtx.id 隔離(P0 BLOCKER,2026-05-31 折入,M4 AR34 regression detector)──
|
|
218
|
+
# M4:_Group 元件(CheckboxGroup/RadioGroup/SwitchGroup)的 child item 不可共用 fieldCtx.id —— 否則
|
|
219
|
+
# group 內所有 label 被抑制 + 點 label 只 toggle 第一個(AR34 root bug)。正解:item 在 group 內走自己
|
|
220
|
+
# 的 generatedId,不 fall back 到共用 fieldCtx.id。偵測 AR34 regression shape(MERGED 整檔,3-signal AND):
|
|
221
|
+
# (1) 消費 *GroupContext (2) bare `idProp ?? fieldCtx?.id ?? generatedId` fallback
|
|
222
|
+
# (3) 缺 `insideGroup ? generatedId` / `inGroup ? generatedId` group guard
|
|
223
|
+
case "$FILE_PATH" in
|
|
224
|
+
*components/*.tsx)
|
|
225
|
+
if ! echo "$MERGED_CONTENT" | grep -q '@group-fieldctx-allow' \
|
|
226
|
+
&& echo "$MERGED_CONTENT" | grep -qE 'useContext\([A-Za-z_]*GroupContext\)' \
|
|
227
|
+
&& echo "$MERGED_CONTENT" | grep -qE 'idProp[[:space:]]*\?\?[[:space:]]*fieldCtx\?\.id[[:space:]]*\?\?[[:space:]]*generatedId' \
|
|
228
|
+
&& ! echo "$MERGED_CONTENT" | grep -qE '(insideGroup|inGroup)[[:space:]]*\?[[:space:]]*generatedId'; then
|
|
229
|
+
cat >&2 <<EOF
|
|
230
|
+
|
|
231
|
+
┄┄┄ A.5 check_field_family_invariants — _Group child fieldCtx.id 隔離 BLOCKER ┄┄┄
|
|
232
|
+
|
|
233
|
+
[P0] ${FILE_PATH}
|
|
234
|
+
偵測到 _Group child item 消費 GroupContext + bare \`idProp ?? fieldCtx?.id ?? generatedId\` fallback,
|
|
235
|
+
但**缺** group guard(\`insideGroup ? generatedId\`)= M4 AR34 regression shape。
|
|
236
|
+
|
|
237
|
+
⚠️ M4 canonical:Group 內 item 不可共用 fieldCtx.id(否則所有 label 被抑制 + 點 label 只 toggle 第一個)。
|
|
238
|
+
|
|
239
|
+
修法:
|
|
240
|
+
inputId = idProp ?? (insideGroup ? generatedId : (fieldCtx?.id ?? generatedId))
|
|
241
|
+
即 group 內走自己的 generatedId,不 fall back 共用 fieldCtx.id。
|
|
242
|
+
例外:行尾 \`// @group-fieldctx-allow: <reason>\`
|
|
243
|
+
|
|
244
|
+
EOF
|
|
245
|
+
record_worst 2
|
|
246
|
+
fi
|
|
247
|
+
;;
|
|
248
|
+
esac
|
|
249
|
+
|
|
250
|
+
exit $WORST
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_fork_product_quality.sh — REPLACE for check_substantive_edit_approval_preflight.sh (fork-mode)
|
|
3
|
+
#
|
|
4
|
+
# WHY REPLACE: 原 approval-gate 在 fork hard-block(exit 2)所有 apps/** 產品 edit → 鎖死 fork
|
|
5
|
+
# 使用者(連寫產品 code 都不行)。fork 使用者「寫產品」是正常開發,不該被 DS-author 的「設計
|
|
6
|
+
# 改動需 approval」儀式擋住。本 hook 改成 quality-POSITIVE soft inject:exit 0、永不 brick,只在
|
|
7
|
+
# 編輯產品 code 時提醒「先消費 DS 元件 + 讀 DS spec」。真正的品質 enforcement 由其他 fork-mode
|
|
8
|
+
# 治理 hook(primitive-misuse / layout-space / opacity-token / ds-anchor)負責,不靠 approval 閘。
|
|
9
|
+
#
|
|
10
|
+
# 對齊 C-prime 共識:world-class 產出靠「SSOT/anchor/primitive 機械檢查 fire」保證,
|
|
11
|
+
# 不靠「把一般 edit 擋在 approval 關鍵字後面」。
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
15
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
16
|
+
case "${TOOL:-}" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac
|
|
17
|
+
|
|
18
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
19
|
+
# 只在 fork 產品 code 提示;DS 內部 / node_modules / 非 product 不管
|
|
20
|
+
echo "$FILE" | grep -qE '(^|/)apps/.+\.(tsx|ts|css)$' || exit 0
|
|
21
|
+
echo "$FILE" | grep -qE 'node_modules/|packages/design-system/' && exit 0
|
|
22
|
+
|
|
23
|
+
# 一個 session 只提示一次(避免每次 edit 都吵)
|
|
24
|
+
MARK="${TMPDIR:-/tmp}/.ds-fork-quality-hinted-${CLAUDE_SESSION_ID:-session}"
|
|
25
|
+
[ -f "$MARK" ] && exit 0
|
|
26
|
+
: > "$MARK" 2>/dev/null || true
|
|
27
|
+
|
|
28
|
+
cat >&2 << 'EOF'
|
|
29
|
+
💡 DS 產品開發提示(soft,不阻擋):你正在編輯產品 code(apps/**)。
|
|
30
|
+
為確保世界級且一致的產出,改動前請:
|
|
31
|
+
- 優先消費既有 DS 元件(import from "@qijenchen/design-system"),勿手刻 raw HTML / 重造已有元件
|
|
32
|
+
- 組裝某元件前,先讀它的 spec/story:node_modules/@qijenchen/design-system/src/**/<name>.spec.md
|
|
33
|
+
- 用 design token(間距/色值/字級/圓角/陰影),勿硬寫
|
|
34
|
+
(其餘 fork 治理 hook 會機械把關 primitive 誤用 / 硬寫間距 / opacity token 等)
|
|
35
|
+
EOF
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_item_list_gap.sh — M16 mechanical enforcement(2026-05-26 backfill).
|
|
3
|
+
#
|
|
4
|
+
# Purpose:PreToolUse Edit/Write — 偵測 production tsx 含 standalone card/pill 渲染
|
|
5
|
+
# pattern(`<FileItem>` / `<MenuItem rich>` / 卡片型 standalone)without gap-N or space-y-N
|
|
6
|
+
# parent → soft warn requires explicit gap canonical per item-anatomy.spec.md。
|
|
7
|
+
#
|
|
8
|
+
# 3 條公式(per M16):
|
|
9
|
+
# - 同類 standalone 卡片 → 必 gap
|
|
10
|
+
# - 同類 permanent flush(連續貼齊)→ 0 gap OK,但必 codify in spec
|
|
11
|
+
# - 混合語言 → 必取最保守 gap
|
|
12
|
+
|
|
13
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
14
|
+
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
17
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
18
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
19
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
|
|
20
|
+
NEW=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""' 2>/dev/null)
|
|
21
|
+
|
|
22
|
+
[ "$EVENT" != "PreToolUse" ] && exit 0
|
|
23
|
+
case "$TOOL" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac
|
|
24
|
+
case "$FILE_PATH" in *.tsx) ;; *) exit 0 ;; esac
|
|
25
|
+
case "$FILE_PATH" in *.stories.tsx|*.test.tsx) exit 0 ;; esac
|
|
26
|
+
|
|
27
|
+
# Heuristic:multiple <FileItem> / <MenuItem variant="rich"> / standalone card pattern + parent 無 gap-N
|
|
28
|
+
# 2026-05-26 fix:grep -c 計行數不計次,用 -o count 真實 occurrence
|
|
29
|
+
STANDALONE_RE='<(FileItem|MenuItem[^>]+variant=["'\'']rich["'\''])'
|
|
30
|
+
COUNT=$(echo "$NEW" | grep -oE "$STANDALONE_RE" 2>/dev/null | wc -l | tr -d ' ')
|
|
31
|
+
COUNT=${COUNT:-0}
|
|
32
|
+
|
|
33
|
+
[ "$COUNT" -lt 2 ] && exit 0
|
|
34
|
+
|
|
35
|
+
# Has gap-N or space-y-N near the standalone pattern?
|
|
36
|
+
if echo "$NEW" | grep -qE '(gap-[0-9]|space-y-[0-9])'; then
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
cat >&2 <<EOF
|
|
41
|
+
⚠️ M16 Item-list gap canonical
|
|
42
|
+
|
|
43
|
+
→ 偵測到 $COUNT 個 standalone item(FileItem / MenuItem rich)未見 gap-N / space-y-N parent class。
|
|
44
|
+
|
|
45
|
+
M16 要求(per item-anatomy.spec.md):
|
|
46
|
+
- 同類 standalone → 必 gap(常見 gap-2 / gap-3)
|
|
47
|
+
- 同類 permanent flush(連續貼齊)→ 0 gap OK,但必 spec codify
|
|
48
|
+
- 混合語言 → 必取最保守 gap
|
|
49
|
+
|
|
50
|
+
修法:parent container 加 \`gap-2\` / \`space-y-2\` 或 codify 連續貼齊 in 元件 spec。
|
|
51
|
+
對應 canonical:meta-patterns.md M16 + patterns/element-anatomy/item-anatomy.spec.md。
|
|
52
|
+
EOF
|
|
53
|
+
|
|
54
|
+
exit 0
|