@qijenchen/design-system 0.1.0-beta.32 → 0.1.0-beta.34
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/CLAUDE.md +5 -7
- package/README.md +2 -2
- package/ds-canonical/hooks/auto_regen_ds_barrel.sh +58 -0
- package/ds-canonical/hooks/check_addon_subdir_ship.sh +76 -0
- package/ds-canonical/hooks/check_chrome_header_avatar_canonical.sh +84 -0
- package/ds-canonical/hooks/check_codex_brief_invariants.sh +40 -8
- package/ds-canonical/hooks/check_consumer_app_story_title.sh +81 -0
- package/ds-canonical/hooks/check_consumer_ds_primitive_misuse.sh +106 -0
- package/ds-canonical/hooks/check_consumer_no_ds_catalog.sh +98 -0
- package/ds-canonical/hooks/check_consumer_story_baseline.sh +76 -0
- package/ds-canonical/hooks/check_data_table_size_num_to_meta_width.sh +68 -0
- package/ds-canonical/hooks/check_ds_anchor_preflight.sh +132 -0
- package/ds-canonical/hooks/check_escape_marker_abuse.sh +86 -0
- package/ds-canonical/hooks/check_fork_user_plugin_install.sh +75 -0
- package/ds-canonical/hooks/check_full_story_visual_interaction_sweep.sh +71 -0
- package/ds-canonical/hooks/check_item_list_gap.sh +54 -0
- package/ds-canonical/hooks/check_layout_space_magic_numbers.sh +84 -0
- package/ds-canonical/hooks/check_orphan_ds_css.sh +66 -0
- package/ds-canonical/hooks/check_overlay_open_focus_escape_probe.sh +75 -0
- package/ds-canonical/hooks/check_plugin_freshness.sh +68 -0
- package/ds-canonical/hooks/check_post_main_ssot_propagate.sh +88 -0
- package/ds-canonical/hooks/check_propose_cite_required.sh +88 -0
- package/ds-canonical/hooks/check_propose_without_benchmark.sh +63 -0
- package/ds-canonical/hooks/check_sidebar_menu_button_implicit_wrap.sh +84 -0
- package/ds-canonical/hooks/check_storybook_addon_preset_cjs.sh +82 -0
- package/ds-canonical/hooks/check_substantive_edit_approval_preflight.sh +7 -2
- package/ds-canonical/hooks/check_tailwind_wildcard_in_docs.sh +68 -0
- package/ds-canonical/hooks/chrome_header_dispatcher.sh +36 -0
- package/ds-canonical/hooks/inject_deploy_url_after_push.sh +178 -0
- package/ds-canonical/hooks/inject_pending_self_audit.sh +45 -1
- package/ds-canonical/hooks/{check_app_shell_primary_header_consistency.sh → lib/_app_shell_primary_header_consistency.sh} +1 -1
- package/ds-canonical/hooks/lib/_approval_re.sh +1 -1
- package/ds-canonical/hooks/{check_chrome_header_handcraft.sh → lib/_chrome_header_handcraft.sh} +1 -1
- package/ds-canonical/hooks/{check_header_with_tabs_border.sh → lib/_header_with_tabs_border.sh} +1 -1
- package/ds-canonical/hooks/{check_tab_lg_chrome_header_equal.sh → lib/_tab_lg_chrome_header_equal.sh} +1 -1
- package/ds-canonical/hooks/session_start_governance_check.sh +16 -3
- package/ds-canonical/hooks/stop_self_audit.sh +28 -3
- package/ds-canonical/hooks/tests/test_check_app_shell_primary_header_consistency.sh +1 -1
- package/ds-canonical/hooks/tests/test_check_chrome_header_handcraft.sh +1 -1
- package/ds-canonical/hooks/tests/test_check_data_table_size_num_to_meta_width.sh +52 -0
- package/ds-canonical/hooks/tests/test_check_ds_anchor_preflight.sh +104 -0
- package/ds-canonical/hooks/tests/test_check_fork_user_plugin_install.sh +45 -0
- package/ds-canonical/hooks/tests/test_check_header_with_tabs_border.sh +1 -1
- package/ds-canonical/hooks/tests/test_check_item_list_gap.sh +44 -0
- package/ds-canonical/hooks/tests/test_check_propose_without_benchmark.sh +55 -0
- package/ds-canonical/hooks/tests/test_check_tab_lg_chrome_header_equal.sh +1 -1
- package/ds-canonical/hooks/tests/test_session_start_governance_check.sh +3 -3
- package/ds-canonical/references/composition-fidelity.md +101 -0
- package/ds-canonical/references/scenario-definition.md +146 -0
- package/ds-canonical/rules/meta-patterns.md +1 -1
- package/ds-canonical/rules/story-rules.md +11 -2
- package/ds-canonical/skills/codex-collab/references/brief-template.md +1 -1
- package/ds-canonical/skills/deep-audit-cross-codex/SKILL.md +61 -6
- package/ds-canonical/skills/design-system-audit/SKILL.md +28 -1
- package/ds-canonical/skills/knowledge-prune/SKILL.md +12 -0
- package/ds-story-manifest.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_consumer_story_baseline.sh — P0 BLOCKER
|
|
3
|
+
#
|
|
4
|
+
# Consumer storybook files wrapping high-risk DS primitives MUST declare
|
|
5
|
+
# `// @story-baseline: <DS-story-path>#<exportName>` marker
|
|
6
|
+
# (per M31 codex synthesis 2026-05-27).
|
|
7
|
+
#
|
|
8
|
+
# Anchor 2026-05-27 codex M31 v1:「Consumer 若 wrap 高風險 DS primitive,
|
|
9
|
+
# 檔頭必須有 @story-baseline:,並由 CI 對 DS canonical story 做 visual diff」.
|
|
10
|
+
#
|
|
11
|
+
# High-risk DS primitives(per codex list):
|
|
12
|
+
# DataTable / Dialog / Sheet / Popover / DropdownMenu / Tooltip / HoverCard /
|
|
13
|
+
# LinkInput / RadioGroup / CircularProgress / AppShell / Sidebar
|
|
14
|
+
#
|
|
15
|
+
# Triggers on consumer apps/**/*.stories.tsx edit. Blocks if file uses any of
|
|
16
|
+
# these primitives but lacks @story-baseline: marker.
|
|
17
|
+
#
|
|
18
|
+
# Escape:`// @story-baseline-allow: <rationale>` for legitimate exceptions
|
|
19
|
+
# (eg. behavior-only test stories, exception per ds-story-manifest.json).
|
|
20
|
+
|
|
21
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
26
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
27
|
+
|
|
28
|
+
case "${TOOL:-}" in
|
|
29
|
+
Edit|Write|MultiEdit) ;;
|
|
30
|
+
*) exit 0 ;;
|
|
31
|
+
esac
|
|
32
|
+
|
|
33
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
34
|
+
# Only check consumer storybook files
|
|
35
|
+
if ! echo "$FILE" | grep -qE '/(apps|consumer)/.*\.stories\.tsx$'; then exit 0; fi
|
|
36
|
+
if echo "$FILE" | grep -qE 'packages/design-system/src/'; then exit 0; fi
|
|
37
|
+
|
|
38
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
|
|
39
|
+
[ -z "$CONTENT" ] && exit 0
|
|
40
|
+
|
|
41
|
+
# Escape clauses
|
|
42
|
+
if echo "$CONTENT" | grep -qE '@story-baseline-allow:|@consumer-catalog-allow:'; then exit 0; fi
|
|
43
|
+
|
|
44
|
+
# High-risk DS primitives requiring baseline marker
|
|
45
|
+
HIGH_RISK_PRIMITIVES='DataTable|Dialog|Sheet|Popover|DropdownMenu|Tooltip|HoverCard|LinkInput|RadioGroup|CircularProgress|AppShell|Sidebar'
|
|
46
|
+
|
|
47
|
+
# Detect usage
|
|
48
|
+
USED=$(echo "$CONTENT" | grep -oE "<DS\.($HIGH_RISK_PRIMITIVES)\\b" | sort -u | head -10)
|
|
49
|
+
|
|
50
|
+
if [ -z "$USED" ]; then exit 0; fi
|
|
51
|
+
|
|
52
|
+
# Check for @story-baseline: marker
|
|
53
|
+
if echo "$CONTENT" | grep -qE '@story-baseline:[[:space:]]*\S'; then exit 0; fi
|
|
54
|
+
|
|
55
|
+
cat >&2 << EOF
|
|
56
|
+
🚨 CONSUMER-STORY-BASELINE BLOCKER(P0,2026-05-27 M31 codex synthesis)
|
|
57
|
+
|
|
58
|
+
Consumer file $FILE 用高風險 DS primitive 但無 \`// @story-baseline:\` marker:
|
|
59
|
+
$(echo "$USED" | sed 's/^/ /')
|
|
60
|
+
|
|
61
|
+
per M31 codex synthesis SSOT:「Consumer wrap 高風險 DS primitive 必 @story-baseline:
|
|
62
|
+
marker,由 CI 對 DS canonical story 做 visual diff」.
|
|
63
|
+
|
|
64
|
+
High-risk list:DataTable / Dialog / Sheet / Popover / DropdownMenu / Tooltip /
|
|
65
|
+
HoverCard / LinkInput / RadioGroup / CircularProgress / AppShell / Sidebar.
|
|
66
|
+
|
|
67
|
+
修法 2 選 1:
|
|
68
|
+
(a) 加 marker(檔頭或 story body):
|
|
69
|
+
// @story-baseline: @qijenchen/design-system/components/<Name>/<name>.stories.tsx#<ExportName>
|
|
70
|
+
例:// @story-baseline: @qijenchen/design-system/components/Sidebar/sidebar.stories.tsx#IconCollapse
|
|
71
|
+
(b) Escape:\`// @story-baseline-allow: <rationale>\` 顯式 documented exception
|
|
72
|
+
(eg. pure behavior test / per ds-story-manifest.json exception list)
|
|
73
|
+
|
|
74
|
+
完整 mapping → packages/design-system/ds-story-manifest.json(DS package ship)
|
|
75
|
+
EOF
|
|
76
|
+
exit 2
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_data_table_size_num_to_meta_width.sh — M23(c) mechanical enforcement(2026-05-26 backfill).
|
|
3
|
+
#
|
|
4
|
+
# Purpose:PreToolUse Edit/Write — 偵測 DataTable column 定義使用 TanStack `size: <number>`
|
|
5
|
+
# (px width)而非 DS canonical `meta: { width: 'sm'|'md'|'lg' }` density-aware token。
|
|
6
|
+
# 違反 M23(c) framework prop name namespace conflict(TanStack `size` 是 px 數字,
|
|
7
|
+
# DS `size` 是 density variant)→ soft warn 要求 wrap-and-rename。
|
|
8
|
+
#
|
|
9
|
+
# 對齊 meta-patterns.md M23(c) + data-table.spec.md L?「Column width canonical」段。
|
|
10
|
+
|
|
11
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
15
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
16
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
17
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
|
|
18
|
+
NEW=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""' 2>/dev/null)
|
|
19
|
+
|
|
20
|
+
[ "$EVENT" != "PreToolUse" ] && exit 0
|
|
21
|
+
case "$TOOL" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac
|
|
22
|
+
case "$FILE_PATH" in *.tsx|*.ts) ;; *) exit 0 ;; esac
|
|
23
|
+
case "$FILE_PATH" in *.stories.tsx|*.test.*) exit 0 ;; esac
|
|
24
|
+
|
|
25
|
+
# DataTable 上下文:column 定義 OR DataTable import OR ColumnDef 型別
|
|
26
|
+
HAS_DATATABLE_CTX=0
|
|
27
|
+
if echo "$NEW" | grep -qE '(ColumnDef|columns:|DataTable|@tanstack/react-table)'; then
|
|
28
|
+
HAS_DATATABLE_CTX=1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
[ "$HAS_DATATABLE_CTX" = "0" ] && exit 0
|
|
32
|
+
|
|
33
|
+
# Detect raw size: <number> in column def(TanStack 用法)
|
|
34
|
+
RAW_SIZE_HITS=$(echo "$NEW" | grep -cE '\bsize:\s*[0-9]+' 2>/dev/null)
|
|
35
|
+
RAW_SIZE_HITS=${RAW_SIZE_HITS:-0}
|
|
36
|
+
|
|
37
|
+
[ "$RAW_SIZE_HITS" -eq 0 ] && exit 0
|
|
38
|
+
|
|
39
|
+
# 若同時用 meta: { width 也 OK(混用允許 transition)
|
|
40
|
+
if echo "$NEW" | grep -qE 'meta:\s*\{[^}]*width'; then
|
|
41
|
+
# mixed — let it pass(transition state)
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
REL=${FILE_PATH#*/my-project/}
|
|
46
|
+
|
|
47
|
+
cat >&2 <<EOF
|
|
48
|
+
⚠️ M23(c) framework prop namespace conflict — DataTable column \`size: <number>\`
|
|
49
|
+
|
|
50
|
+
📁 File: $REL
|
|
51
|
+
🔍 偵測:$RAW_SIZE_HITS 個 \`size: <number>\` (TanStack px API)在 ColumnDef 上下文,但無 \`meta: { width: 'sm'|'md'|'lg' }\` DS density-aware variant。
|
|
52
|
+
|
|
53
|
+
M23(c)要求:
|
|
54
|
+
- TanStack \`size\` prop = px 數字(framework spec)
|
|
55
|
+
- DS \`size\` prop = density variant 'sm'|'md'|'lg'(DS spec)
|
|
56
|
+
- 同名不同義 → wrap-and-rename:DataTable column 用 \`meta.width: 'sm'|'md'|'lg'\`,DS internal 再 map 成 TanStack \`size\` px
|
|
57
|
+
|
|
58
|
+
修法:
|
|
59
|
+
// ❌ Avoid
|
|
60
|
+
{ accessorKey: 'name', size: 280 }
|
|
61
|
+
|
|
62
|
+
// ✅ Use DS density-aware
|
|
63
|
+
{ accessorKey: 'name', meta: { width: 'md' } }
|
|
64
|
+
|
|
65
|
+
對應 canonical:meta-patterns.md M23(c) + components/DataTable/data-table.spec.md「Column width SSOT」。
|
|
66
|
+
EOF
|
|
67
|
+
|
|
68
|
+
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
|
+
# - packages/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 `packages/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
|
+
*/packages/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 'packages/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#*/my-project/}
|
|
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 對 \`packages/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
|
+
- \`packages/design-system/src/<Component>/*.spec.md\`(找 owner SSOT)
|
|
118
|
+
- \`packages/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,86 @@
|
|
|
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
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
32
|
+
|
|
33
|
+
set -uo pipefail
|
|
34
|
+
|
|
35
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
36
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
37
|
+
|
|
38
|
+
case "${TOOL:-}" in
|
|
39
|
+
Edit|Write|MultiEdit) ;;
|
|
40
|
+
*) exit 0 ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
44
|
+
# Only check consumer apps tsx (not DS source — DS has legitimate exceptions per spec)
|
|
45
|
+
if ! echo "$FILE" | grep -qE '/(apps|consumer)/.*\.(tsx|ts)$'; then exit 0; fi
|
|
46
|
+
if echo "$FILE" | grep -qE 'packages/design-system/src/'; then exit 0; fi
|
|
47
|
+
|
|
48
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
|
|
49
|
+
[ -z "$CONTENT" ] && exit 0
|
|
50
|
+
|
|
51
|
+
# Global escape — meta-skip(env override OR explicit comment)
|
|
52
|
+
if [ "${CLAUDE_BYPASS_ESCAPE_MARKER_AUDIT:-0}" = "1" ]; then exit 0; fi
|
|
53
|
+
|
|
54
|
+
# Count distinct markers + total occurrences
|
|
55
|
+
MARKER_RE='@(ds-misuse-allow|story-baseline-allow|consumer-catalog-allow|overlay-open-skip|template-customized|layout-space-magic-ok|story-trait-allow|story-trait-rationale|story-split-rationale|story-name-canonical-allow|propose-cite-skip|anatomy-exempt|anatomy-exempt-next|benchmark-unverified|benchmark-citation-allow|benchmark-unverified-blanket)'
|
|
56
|
+
MARKERS_FOUND=$(echo "$CONTENT" | grep -oE "$MARKER_RE" | sort -u)
|
|
57
|
+
DISTINCT_COUNT=$(echo "$MARKERS_FOUND" | grep -c . || echo 0)
|
|
58
|
+
TOTAL_COUNT=$(echo "$CONTENT" | grep -oE "$MARKER_RE" | wc -l | tr -d ' ')
|
|
59
|
+
|
|
60
|
+
# Threshold: ≥3 distinct types OR ≥5 total
|
|
61
|
+
if [ "$DISTINCT_COUNT" -ge 3 ] || [ "$TOTAL_COUNT" -ge 5 ]; then
|
|
62
|
+
cat >&2 << EOF
|
|
63
|
+
🚨 ESCAPE-MARKER-ABUSE BLOCKER(P0,user 2026-05-27 verbatim「不亂加 escape markers — 加就跳 enforcement」)
|
|
64
|
+
|
|
65
|
+
File $FILE:
|
|
66
|
+
Distinct escape markers: $DISTINCT_COUNT(threshold ≥3)
|
|
67
|
+
Total occurrences: $TOTAL_COUNT(threshold ≥5)
|
|
68
|
+
|
|
69
|
+
Markers detected:
|
|
70
|
+
$(echo "$MARKERS_FOUND" | sed 's/^/ /')
|
|
71
|
+
|
|
72
|
+
Escape markers 設計為「rare per-line documented exception」,不該 routine 加。
|
|
73
|
+
Fork user file 大量加 marker = 違反 SSOT 哲學 — 應該:
|
|
74
|
+
|
|
75
|
+
修法 3 選 1:
|
|
76
|
+
(a) **重構 code** 走 DS canonical pattern(消除根因,不繞)
|
|
77
|
+
(b) **拆 file**:1 個 escape 對應 1 個 specific case,分散到不同 file
|
|
78
|
+
(c) **Override env**(極罕見,documented in commit msg):
|
|
79
|
+
CLAUDE_BYPASS_ESCAPE_MARKER_AUDIT=1 git commit -m "<rationale>"
|
|
80
|
+
|
|
81
|
+
per check_consumer_*.sh hooks SSOT — escape 是 emergency exit,不是 daily tool.
|
|
82
|
+
EOF
|
|
83
|
+
exit 2
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
exit 0
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_fork_user_plugin_install.sh — SessionStart enforce plugin install on fork-user repos.
|
|
3
|
+
#
|
|
4
|
+
# 2026-05-26 backfill per user verbatim「我們做那麼多 plugin 不就是要避免這件事?結果還避不了?」
|
|
5
|
+
#
|
|
6
|
+
# Purpose:當 session 跑在「consumer fork-user repo」(non-DS repo,但 package.json
|
|
7
|
+
# 含 @qijenchen/design-system dep)+ 偵測未安裝 design-system plugin → context inject
|
|
8
|
+
# 強制提示用 `/plugin marketplace add github:ajenchen/design-system → /plugin install`。
|
|
9
|
+
#
|
|
10
|
+
# Detection logic:
|
|
11
|
+
# (a) cwd 不是 DS repo(無 packages/design-system/ folder)
|
|
12
|
+
# (b) package.json 含 `"@qijenchen/design-system"` dep
|
|
13
|
+
# (c) `.claude/plugins/design-system@qijenchen-ds/` OR plugin marketplace symlink 不存在
|
|
14
|
+
# → 三題全 YES = consumer fork-user 沒裝 plugin → 強制提示
|
|
15
|
+
|
|
16
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
20
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null)
|
|
21
|
+
|
|
22
|
+
[ "$EVENT" != "SessionStart" ] && exit 0
|
|
23
|
+
|
|
24
|
+
CWD=$(pwd)
|
|
25
|
+
|
|
26
|
+
# (a) 不是 DS repo:無 packages/design-system/src
|
|
27
|
+
[ -d "$CWD/packages/design-system/src" ] && exit 0
|
|
28
|
+
|
|
29
|
+
# (b) package.json 含 DS dep?
|
|
30
|
+
[ ! -f "$CWD/package.json" ] && exit 0
|
|
31
|
+
if ! grep -q '"@qijenchen/design-system"' "$CWD/package.json" 2>/dev/null; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# (c) Plugin 已安裝?Anthropic plugin install 可能使用 <plugin> 或
|
|
36
|
+
# <plugin>@<marketplace> directory name;accept both.
|
|
37
|
+
PLUGIN_INSTALLED=0
|
|
38
|
+
[ -d "$HOME/.claude/plugins/design-system" ] && PLUGIN_INSTALLED=1
|
|
39
|
+
[ -d "$HOME/.claude/plugins/design-system@qijenchen-ds" ] && PLUGIN_INSTALLED=1
|
|
40
|
+
[ -d "$CWD/.claude/plugins/design-system" ] && PLUGIN_INSTALLED=1
|
|
41
|
+
[ -d "$CWD/.claude/plugins/design-system@qijenchen-ds" ] && PLUGIN_INSTALLED=1
|
|
42
|
+
# 也接受 plugin marketplace cache(claude code 自動產生)
|
|
43
|
+
[ -d "$HOME/.claude/marketplaces"/*/design-system/ ] 2>/dev/null && PLUGIN_INSTALLED=1
|
|
44
|
+
[ -d "$HOME/.claude/marketplaces"/*/design-system@qijenchen-ds/ ] 2>/dev/null && PLUGIN_INSTALLED=1
|
|
45
|
+
|
|
46
|
+
if [ "$PLUGIN_INSTALLED" = "1" ]; then
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# All YES → fork-user 沒裝 plugin → context inject(SessionStart additional context)
|
|
51
|
+
cat <<EOF
|
|
52
|
+
🚨 Fork-user plugin not installed — DS governance hooks 不會 fire,憑記憶寫 mock 不會被攔。
|
|
53
|
+
|
|
54
|
+
偵測:
|
|
55
|
+
cwd = $CWD
|
|
56
|
+
package.json 含 @qijenchen/design-system dep ✅
|
|
57
|
+
~/.claude/plugins/design-system@qijenchen-ds/ 或 .claude/plugins/design-system@qijenchen-ds/ 不存在 ❌
|
|
58
|
+
|
|
59
|
+
→ 後果(2026-05-26 anchor event):憑記憶寫 App.tsx mock(漏 SidebarTrigger / collapsible / startIcon /
|
|
60
|
+
tooltip / SidebarFooter)= production-grade fork-user 跑版 anti-pattern。
|
|
61
|
+
|
|
62
|
+
修法:**session 開始第一件事必跑**
|
|
63
|
+
1. \`/plugin marketplace add github:ajenchen/design-system\`
|
|
64
|
+
2. \`/plugin install design-system@qijenchen-ds\`(or 對應 plugin name per marketplace.json)
|
|
65
|
+
|
|
66
|
+
跑完 plugin install 後:
|
|
67
|
+
- 59 個 DS governance hooks 自動 fire(M29 anchor preflight / approval-preflight / story_invariants / inject_deploy_url_after_push 等;count snapshot 2026-05-29)
|
|
68
|
+
- DS canonical / rules / skills 從 ~/.claude/plugins/ 可 cross-load
|
|
69
|
+
- Fork-user 寫 App.tsx 時憑記憶寫 mock 會被 mechanical BLOCKER 攔
|
|
70
|
+
|
|
71
|
+
對應 canonical:
|
|
72
|
+
- product-workspace CLAUDE.md「Fork-and-go onboarding」step 2-3
|
|
73
|
+
- .claude-plugin/marketplace.json
|
|
74
|
+
EOF
|
|
75
|
+
exit 0
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_full_story_visual_interaction_sweep.sh — P0 BLOCKER
|
|
3
|
+
#
|
|
4
|
+
# Codex M31 P0 finding 2026-05-27: ds-story-manifest.json 是 62-component / 916-story
|
|
5
|
+
# SSOT,但無 hook 攔 audit reports 漏 manifest story IDs / screenshots / metrics / probe.
|
|
6
|
+
# User 2026-05-27 verbatim「不准抽樣 全 visual + interaction 全跑」.
|
|
7
|
+
#
|
|
8
|
+
# Triggers when audit report JSON is edited/written:
|
|
9
|
+
# - /tmp/codex-*-sweep/audit-report.json
|
|
10
|
+
# - /tmp/claude-*-sweep/audit-report.json
|
|
11
|
+
# - .claude/snapshots/*audit-report.json
|
|
12
|
+
# Verifies storyResults.length === manifest.totalStories (916) — block if sample.
|
|
13
|
+
#
|
|
14
|
+
# Escape: report frontmatter `"_sampling_allowed": "<rationale>"`(極罕見).
|
|
15
|
+
|
|
16
|
+
source "$(dirname "$0")/_log-fire.sh" 2>/dev/null && log_hook_fire
|
|
17
|
+
|
|
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
|
+
Write|Edit|MultiEdit) ;;
|
|
25
|
+
*) exit 0 ;;
|
|
26
|
+
esac
|
|
27
|
+
|
|
28
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
29
|
+
if ! echo "$FILE" | grep -qE '(sweep|audit-report)\.json$'; then exit 0; fi
|
|
30
|
+
|
|
31
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // ""' 2>/dev/null)
|
|
32
|
+
[ -z "$CONTENT" ] && exit 0
|
|
33
|
+
|
|
34
|
+
# Check escape
|
|
35
|
+
if echo "$CONTENT" | grep -q '_sampling_allowed'; then exit 0; fi
|
|
36
|
+
|
|
37
|
+
# Parse story count vs manifest (multi-field fallback)
|
|
38
|
+
STORY_COUNT=$(echo "$CONTENT" | jq -r '.storyResults // .results // [] | length' 2>/dev/null)
|
|
39
|
+
if [ -z "$STORY_COUNT" ] || [ "$STORY_COUNT" = "0" ] || [ "$STORY_COUNT" = "null" ]; then
|
|
40
|
+
STORY_COUNT=$(echo "$CONTENT" | jq -r '.storySummary.total // .summary.total // .total // 0' 2>/dev/null)
|
|
41
|
+
fi
|
|
42
|
+
STORY_COUNT=${STORY_COUNT:-0}
|
|
43
|
+
|
|
44
|
+
MANIFEST_PATH="${CLAUDE_PROJECT_DIR:-.}/packages/design-system/ds-story-manifest.json"
|
|
45
|
+
if [ ! -f "$MANIFEST_PATH" ]; then exit 0; fi
|
|
46
|
+
MANIFEST_TOTAL=$(jq -r '.totalStories // 0' "$MANIFEST_PATH" 2>/dev/null)
|
|
47
|
+
[ -z "$MANIFEST_TOTAL" ] || [ "$MANIFEST_TOTAL" = "0" ] && exit 0
|
|
48
|
+
[ "$STORY_COUNT" = "0" ] && exit 0
|
|
49
|
+
# numeric compare
|
|
50
|
+
if ! [[ "$STORY_COUNT" =~ ^[0-9]+$ ]]; then exit 0; fi
|
|
51
|
+
|
|
52
|
+
if [ "$STORY_COUNT" -lt "$MANIFEST_TOTAL" ]; then
|
|
53
|
+
cat >&2 << EOF
|
|
54
|
+
🚨 FULL-STORY-SWEEP BLOCKER(P0,codex M31 finding 2026-05-27 + user「不准抽樣」)
|
|
55
|
+
|
|
56
|
+
Audit report $FILE:
|
|
57
|
+
storyResults: $STORY_COUNT
|
|
58
|
+
manifest.totalStories: $MANIFEST_TOTAL
|
|
59
|
+
缺失: $((MANIFEST_TOTAL - STORY_COUNT)) stories
|
|
60
|
+
|
|
61
|
+
per ds-story-manifest.json SSOT(scripts/gen-ds-story-manifest.mjs:59)+ user 2026-05-27
|
|
62
|
+
「請你不要給我抽樣,因為你一抽樣就是會有東西壞掉,務必要全部檢查」directive.
|
|
63
|
+
|
|
64
|
+
修法 2 選 1:
|
|
65
|
+
(a) Re-run sweep covering all $MANIFEST_TOTAL stories
|
|
66
|
+
(b) Escape:report 含 \`"_sampling_allowed": "<rationale>"\`(極罕見)
|
|
67
|
+
EOF
|
|
68
|
+
exit 2
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
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
|
|
@@ -0,0 +1,84 @@
|
|
|
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 preceding line
|
|
53
|
+
UNJUSTIFIED=""
|
|
54
|
+
while IFS= read -r line; do
|
|
55
|
+
if echo "$line" | grep -qF "$ESCAPE_MARKER"; then continue; fi
|
|
56
|
+
UNJUSTIFIED="${UNJUSTIFIED}${line}\n"
|
|
57
|
+
done <<< "$MAGIC_LINES"
|
|
58
|
+
|
|
59
|
+
if [ -z "$UNJUSTIFIED" ]; then
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
cat >&2 << EOF
|
|
64
|
+
🚨 LAYOUT-SPACE-MAGIC-NUMBER BLOCKER(P0,2026-05-27 user verbatim「機械無強制就不會做?
|
|
65
|
+
為何不全部 ssot 都要強制吻合?」永久 codify)
|
|
66
|
+
|
|
67
|
+
Detected Tailwind spacing magic numbers in $FILE without escape:
|
|
68
|
+
$(echo -e "$UNJUSTIFIED" | sed 's/^/ /' | head -10)
|
|
69
|
+
|
|
70
|
+
per app-shell.spec.md L205-219 + layoutSpace.spec.md SSOT:consumer content 必遵循
|
|
71
|
+
layoutSpace 6 條規則 + 親疏 3 級,**禁** 硬寫 Tailwind magic numbers。改用:
|
|
72
|
+
p-[var(--layout-space-loose)] /* 16px 規則 1A/1B chrome / wrap */
|
|
73
|
+
p-[var(--layout-space-tight)] /* 12px 規則 3 親 gap */
|
|
74
|
+
gap-[var(--layout-space-distant)] /* 24px 規則 3 疏 gap */
|
|
75
|
+
space-y-[var(--layout-space-distant)]
|
|
76
|
+
|
|
77
|
+
修法 2 選 1:
|
|
78
|
+
(a) 改 token:換成 var(--layout-space-N) N∈{loose,tight} family per 6 規則 + 親疏 3 級
|
|
79
|
+
(b) Escape:在該 line 加 \`// @layout-space-magic-ok: <rationale>\` 顯式 documented
|
|
80
|
+
(eg.「\`gap-1\` 是 4px stack icon — non-spacing context,not consumer layout」)
|
|
81
|
+
|
|
82
|
+
完整 6 條規則 → packages/design-system/src/tokens/layoutSpace/layoutSpace.spec.md
|
|
83
|
+
EOF
|
|
84
|
+
exit 2
|