@qijenchen/design-system 0.1.0-beta.52 → 0.1.0-beta.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Empty/empty.d.ts +6 -0
- package/dist/components/Empty/empty.d.ts.map +1 -1
- package/dist/components/Empty/empty.js +4 -4
- package/dist/components/Empty/empty.js.map +1 -1
- package/dist/components/FileItem/file-item.d.ts +10 -0
- package/dist/components/FileItem/file-item.d.ts.map +1 -1
- package/dist/components/FileItem/file-item.js +18 -4
- package/dist/components/FileItem/file-item.js.map +1 -1
- package/dist/components/FileUpload/file-upload.d.ts +21 -9
- package/dist/components/FileUpload/file-upload.d.ts.map +1 -1
- package/dist/components/FileUpload/file-upload.js +55 -15
- package/dist/components/FileUpload/file-upload.js.map +1 -1
- package/ds-canonical/hooks/check_consumer_ds_primitive_misuse.sh +6 -1
- package/ds-canonical/hooks/check_consumer_no_ds_catalog.sh +4 -1
- package/ds-canonical/hooks/check_field_family_invariants.sh +2 -2
- package/ds-canonical/hooks/check_layout_space_magic_numbers.sh +13 -1
- package/ds-canonical/hooks/check_story_invariants.sh +62 -5
- package/ds-canonical/hooks/lib/_chrome_header_handcraft.sh +4 -1
- package/ds-canonical/hooks/tests/test_check_chrome_header_handcraft.sh +11 -0
- package/ds-canonical/hooks/tests/test_check_consumer_ds_primitive_misuse.sh +10 -0
- package/ds-canonical/hooks/tests/test_check_consumer_no_ds_catalog.sh +10 -0
- package/ds-canonical/hooks/tests/test_check_field_family_invariants.sh +9 -0
- package/ds-canonical/hooks/tests/test_check_layout_space_magic_numbers.sh +11 -1
- package/ds-canonical/hooks/tests/test_check_story_invariants.sh +131 -1
- package/ds-canonical/references/story-baseline-registry.json +3 -3
- package/ds-canonical/rules/meta-patterns.md +1 -1
- package/ds-canonical/skills/deep-audit-cross-codex/SKILL.md +5 -1
- package/ds-canonical/skills/design-system-audit/SKILL.md +1 -1
- package/ds-story-manifest.json +8 -5
- package/package.json +1 -1
- package/src/components/Empty/empty.tsx +11 -4
- package/src/components/FileItem/file-item.anatomy.stories.tsx +4 -4
- package/src/components/FileItem/file-item.principles.stories.tsx +1 -1
- package/src/components/FileItem/file-item.spec.md +73 -43
- package/src/components/FileItem/file-item.stories.tsx +69 -8
- package/src/components/FileItem/file-item.tsx +35 -11
- package/src/components/FileUpload/file-upload.anatomy.stories.tsx +37 -50
- package/src/components/FileUpload/file-upload.spec.md +19 -8
- package/src/components/FileUpload/file-upload.stories.tsx +40 -7
- package/src/components/FileUpload/file-upload.tsx +70 -21
|
@@ -49,10 +49,22 @@ if [ -z "$MAGIC_LINES" ]; then
|
|
|
49
49
|
exit 0
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
|
-
# Filter out lines with escape marker on same line OR preceding line
|
|
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 必需。
|
|
53
56
|
UNJUSTIFIED=""
|
|
54
57
|
while IFS= read -r line; do
|
|
58
|
+
# same-line marker
|
|
55
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
|
|
56
68
|
UNJUSTIFIED="${UNJUSTIFIED}${line}\n"
|
|
57
69
|
done <<< "$MAGIC_LINES"
|
|
58
70
|
|
|
@@ -517,13 +517,17 @@ rule_story_baseline_reference() {
|
|
|
517
517
|
fi
|
|
518
518
|
|
|
519
519
|
# 偵測明顯 simplified mock anti-pattern
|
|
520
|
-
|
|
520
|
+
# 2026-06-03 修(同 R8 bug class):tr 換行→空格 flatten(grep 逐行無法跨行)+ tag anchor 加 [^>]*> 容忍屬性
|
|
521
|
+
# (真實 JSX 是 <ChromeHeader className=...> 多行,原 <ChromeHeader> 死匹配漏)。
|
|
522
|
+
local CONTENT_FLAT7
|
|
523
|
+
CONTENT_FLAT7=$(echo "$CONTENT" | tr '\n' ' ')
|
|
524
|
+
if echo "$CONTENT_FLAT7" | grep -qE '<SidebarHeader[^>]*>[[:space:]]*<span'; then
|
|
521
525
|
echo "❌ R7 anti-pattern:<SidebarHeader><span> — 應 wrap WorkspaceBrand-like ItemAvatar block(per sidebar.stories IconCollapse baseline)" >&2
|
|
522
526
|
fi
|
|
523
|
-
if echo "$
|
|
527
|
+
if echo "$CONTENT_FLAT7" | grep -qE '<SidebarMenuButton[^>]*>[^<]*<[A-Z][a-zA-Z]+[[:space:]]+className="size-'; then
|
|
524
528
|
echo "❌ R7 anti-pattern:<SidebarMenuButton><Icon className=\"size-N\"> — 應用 startIcon prop + tooltip prop(per sidebar.stories MAIN_NAV map)" >&2
|
|
525
529
|
fi
|
|
526
|
-
if echo "$
|
|
530
|
+
if echo "$CONTENT_FLAT7" | grep -qE '<ChromeHeader[^>]*>.{0,160}<span[[:space:]]+className="[^"]*flex-1'; then
|
|
527
531
|
echo "❌ R7 anti-pattern:<ChromeHeader><span flex-1> — 應 SidebarTrigger + h1 緊鄰 gap-2(per sidebar.stories PageContent baseline)" >&2
|
|
528
532
|
fi
|
|
529
533
|
fi
|
|
@@ -562,10 +566,15 @@ rule_story_archetype_registry() {
|
|
|
562
566
|
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // ""')
|
|
563
567
|
fi
|
|
564
568
|
|
|
565
|
-
# Allow override
|
|
569
|
+
# Allow override — 2026-06-03 修(同 fragment-vs-file bug class,對抗稽核抓到):Edit 只送 new_string
|
|
570
|
+
# 片段,@story-baseline-allow marker 在檔頭(不在每次 edit 片段)→ 編輯有 marker 的 story 任一非
|
|
571
|
+
# marker 行就被 R8 誤擋。補查整檔 disk head(對齊 R7 L499 disk-check 做法)。
|
|
566
572
|
if echo "$CONTENT" | head -10 | grep -qE '@story-baseline-allow:'; then
|
|
567
573
|
return 0
|
|
568
574
|
fi
|
|
575
|
+
if [ -f "$FILE_PATH" ] && head -10 "$FILE_PATH" 2>/dev/null | grep -qE '@story-baseline-allow:'; then
|
|
576
|
+
return 0
|
|
577
|
+
fi
|
|
569
578
|
|
|
570
579
|
# Read antiPatterns regex from registry(per component)
|
|
571
580
|
# Iterate Sidebar/DataTable/ChromeHeader components
|
|
@@ -581,9 +590,14 @@ rule_story_archetype_registry() {
|
|
|
581
590
|
PATTERNS=$(jq -r --arg c "$COMP" '.components[$c].antiPatterns[]? | select(.severity == "block") | .regex' "$REGISTRY" 2>/dev/null)
|
|
582
591
|
[ -z "$PATTERNS" ] && continue
|
|
583
592
|
|
|
593
|
+
# 2026-06-03 修:CONTENT 換行 → 空格再 grep。真實 JSX 是多行(<ChromeHeader> 與 <span> 分行),
|
|
594
|
+
# 而 grep -E 是 line-oriented + registry regex 用 [[:space:]] 可攜語法 → 不正規化的話多行 antiPattern
|
|
595
|
+
# 靜默漏 = 假 P0(對抗稽核抓到)。tr 後整段成單行,[[:space:]]* / .* 才能跨原換行匹配。
|
|
596
|
+
local CONTENT_FLAT
|
|
597
|
+
CONTENT_FLAT=$(echo "$CONTENT" | tr '\n' ' ')
|
|
584
598
|
while IFS= read -r PATTERN; do
|
|
585
599
|
[ -z "$PATTERN" ] && continue
|
|
586
|
-
if echo "$
|
|
600
|
+
if echo "$CONTENT_FLAT" | grep -qE "$PATTERN"; then
|
|
587
601
|
echo "⚠️ R8 story_archetype_registry violation:" >&2
|
|
588
602
|
echo " $FILE_PATH wrap <$COMP> matches anti-pattern:" >&2
|
|
589
603
|
echo " regex: $PATTERN" >&2
|
|
@@ -598,6 +612,48 @@ rule_story_archetype_registry() {
|
|
|
598
612
|
done
|
|
599
613
|
}
|
|
600
614
|
|
|
615
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
616
|
+
# R9 — hand-craft overlay / chrome header(2026-06-04 codify per upload-manager 面板 drift)
|
|
617
|
+
# Story 內手刻 `<div ... px-[var(--layout-space-loose)] ... border-b border-divider ...>`
|
|
618
|
+
# = 浮層 / chrome header signature → 必消費 SurfaceHeader / PopoverHeader / DialogHeader primitive。
|
|
619
|
+
# 零誤判簽名(DS-wide 量測:px-loose + border-b border-divider 同 div 僅手刻 overlay header 用 —
|
|
620
|
+
# doc-table row 用 px-4 硬寫;真 overlay header 寫 `<SurfaceHeader>`,className 不外露)。
|
|
621
|
+
# Anchor:upload-manager 面板原手刻 `<div px-loose py-2 border-b border-divider>`(py-2≠py-tight /
|
|
622
|
+
# 殼 rounded-md≠lg / border-divider≠border / bg-surface≠raised),既有 3 道網全漏:
|
|
623
|
+
# R1-C(要 div 有 absolute + 附近 onClose/dismiss)、_chrome_header_handcraft(skip stories +
|
|
624
|
+
# 只比 h-[chrome-header-height] signature)、R7/R8(只比已註冊 primitive 名)。adversarial workflow 確認。
|
|
625
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
626
|
+
rule_handcraft_overlay_header() {
|
|
627
|
+
# skip primitive / overlay 家(它們定義 / 示範 header primitive 本身)。
|
|
628
|
+
# 含全部 isOverlay 元件家(2026-06-04 adversarial workflow R9-SKIP-001:補 HoverCard/Tooltip/DropdownMenu/FileViewer)。
|
|
629
|
+
case "$FILE_PATH" in
|
|
630
|
+
*/overlay-surface/*|*/header-canonical/*|*/ChromeHeader/*|*/Dialog/*|*/Sheet/*|*/Popover/*|*/Coachmark/*|*/Tabs/*|*/HoverCard/*|*/Tooltip/*|*/DropdownMenu/*|*/FileViewer/*) return 0 ;;
|
|
631
|
+
esac
|
|
632
|
+
# allow escape(檔頭 OR 本次片段)
|
|
633
|
+
if echo "$NEW_CONTENT" | head -10 | grep -qE '@story-baseline-allow:'; then return 0; fi
|
|
634
|
+
if [ -f "$FILE_PATH" ] && head -10 "$FILE_PATH" 2>/dev/null | grep -qE '@story-baseline-allow:'; then return 0; fi
|
|
635
|
+
local FLAT
|
|
636
|
+
# 2026-06-04 adversarial workflow 抓 R9 regex 3 漏洞,robustify:
|
|
637
|
+
# (1) drop 純註解行(^//、^*、^/*、^{/*)再 flatten → 避免 commented-out / 描述用 JSX 含 pattern 誤判(FP-002);
|
|
638
|
+
# 不 strip 行內 // (避免 mutilate https:// URL 造成 FN)。
|
|
639
|
+
# (2) token 間用 [^">]*(非 [^>]*)→ px-loose 與 border-b 必在「同一 className 字串」內,不跨 " 屬性邊界
|
|
640
|
+
# (FP-001:data-style="border-b border-divider" 不再誤判)。
|
|
641
|
+
# (3) border-b[[:space:]]+border-divider(非單空格)→ 多行 className flatten 後多空格不漏(FN-001,同 R8 multiline bug class)。
|
|
642
|
+
FLAT=$(echo "$NEW_CONTENT" | grep -vE '^[[:space:]]*(//|\*|/\*|\{/\*)' | tr '\n' ' ')
|
|
643
|
+
if echo "$FLAT" | grep -qE '<div[^>]*px-\[var\(--layout-space-loose\)\][^">]*border-b[[:space:]]+border-divider|<div[^>]*border-b[[:space:]]+border-divider[^">]*px-\[var\(--layout-space-loose\)\]'; then
|
|
644
|
+
{
|
|
645
|
+
echo "❌ R9 hand-craft overlay / chrome header:${FILE_PATH}"
|
|
646
|
+
echo " 偵測到 <div ... px-[var(--layout-space-loose)] ... border-b border-divider> = 手刻浮層 / chrome header。"
|
|
647
|
+
echo " 必消費 <SurfaceHeader>(patterns/overlay-surface)/ <PopoverHeader> / <DialogHeader>;"
|
|
648
|
+
echo " 面板殼用 Popover 同款 chrome token(rounded-lg border-border bg-surface-raised elevation-200)。"
|
|
649
|
+
echo " 理由:px-loose + divider border 的 header chrome 是 overlay-surface SSOT(px-loose py-tight + border-b);手刻 = drift。"
|
|
650
|
+
echo " Anchor:2026-06-04 upload-manager 面板手刻 header(py-2≠py-tight / 殼 token 全偏),user 抓。"
|
|
651
|
+
echo " 豁免:檔頭加 // @story-baseline-allow: <reason>。"
|
|
652
|
+
} >&2
|
|
653
|
+
record_worst 2
|
|
654
|
+
fi
|
|
655
|
+
}
|
|
656
|
+
|
|
601
657
|
# ─── Run rules ───
|
|
602
658
|
rule_anatomy
|
|
603
659
|
rule_slot_split
|
|
@@ -607,5 +663,6 @@ rule_name_jargon
|
|
|
607
663
|
rule_description_jargon
|
|
608
664
|
rule_story_baseline_reference
|
|
609
665
|
rule_story_archetype_registry
|
|
666
|
+
rule_handcraft_overlay_header
|
|
610
667
|
|
|
611
668
|
exit $WORST
|
|
@@ -55,7 +55,10 @@ if echo "$NEW_CONTENT" | grep -qE '@chrome-header-handcraft-allow:'; then
|
|
|
55
55
|
fi
|
|
56
56
|
|
|
57
57
|
# Detect handcraft signature: h-[var(--chrome-header-height)] paired with border-b border-divider
|
|
58
|
-
|
|
58
|
+
# 2026-06-03 修(同 R8 multiline bug class,對抗稽核抓到):tr 換行→空格 flatten。真實 JSX className
|
|
59
|
+
# 跨行(h-[...] 與 border-b 分行)+ [^"]* 跨屬性匹配 → 不 flatten 的話多行靜默漏(現 warn-only,但
|
|
60
|
+
# Phase 3 升 P0 後會 false-negative)。tr 後整段成單行,[^"]* 才能跨原換行匹配。
|
|
61
|
+
HANDCRAFT_HIT=$(echo "$NEW_CONTENT" | tr '\n' ' ' | grep -cE 'h-\[var\(--chrome-header-height\)\][^"]*border-b[^"]*border-divider' || true)
|
|
59
62
|
|
|
60
63
|
if [ "$HANDCRAFT_HIT" -gt "0" ]; then
|
|
61
64
|
printf '⚠️ CHROME HEADER HANDCRAFT(soft P1,Phase 3 將升 P0):\n' >&2
|
|
@@ -101,6 +101,17 @@ export const Bar = () => (
|
|
|
101
101
|
'
|
|
102
102
|
expect_warn "5. handcraft signature → warn" "CHROME HEADER HANDCRAFT"
|
|
103
103
|
|
|
104
|
+
# 5b. 2026-06-03 回歸防護(同 R8 multiline bug class,對抗稽核抓到):h-[chrome-header-height] 與
|
|
105
|
+
# border-divider 跨多行 className(真實 JSX 格式)→ warn。修前 grep 逐行 + [^"]* 跨屬性 → 多行靜默漏。
|
|
106
|
+
run_hook_write "/repo/packages/design-system/src/components/Baz/baz.tsx" '
|
|
107
|
+
export const Baz = () => (
|
|
108
|
+
<div className="h-[var(--chrome-header-height)]
|
|
109
|
+
flex items-center px-loose
|
|
110
|
+
border-b border-divider" />
|
|
111
|
+
)
|
|
112
|
+
'
|
|
113
|
+
expect_warn "5b. handcraft 多行 className → warn(回歸防護)" "CHROME HEADER HANDCRAFT"
|
|
114
|
+
|
|
104
115
|
# 6. Escape allowlist → silent
|
|
105
116
|
run_hook_write "/repo/packages/design-system/src/components/Bar/bar.tsx" '// @chrome-header-handcraft-allow: tabs cva pattern
|
|
106
117
|
export const Bar = () => (
|
|
@@ -138,6 +138,16 @@ expect_block "8. P1 <CircularProgress size={24}> → BLOCK"
|
|
|
138
138
|
run_hook "$PROD_TSX" 'export const L = () => <DS.CircularProgress size="lg" />'
|
|
139
139
|
expect_silent "9. P1 nearmiss <CircularProgress size=\"lg\"> → silent"
|
|
140
140
|
|
|
141
|
+
# 9b. 2026-06-03 回歸防護(同 R8 bug class):屬性跨多行的真實 JSX → BLOCK。修前 grep 逐行 +
|
|
142
|
+
# [^>]+ 跨屬性匹配 → 多行 component 靜默繞過全部 anti-pattern(= BLOCKER false-negative,對抗稽核抓到)。
|
|
143
|
+
run_hook "$PROD_TSX" 'export const L = () => (
|
|
144
|
+
<DS.CircularProgress
|
|
145
|
+
variant="blue"
|
|
146
|
+
size={48}
|
|
147
|
+
/>
|
|
148
|
+
)'
|
|
149
|
+
expect_block "9b. P1 多行屬性 <CircularProgress size={48}> → BLOCK(回歸防護)"
|
|
150
|
+
|
|
141
151
|
# ── P2 RadioGroupItem(broad-vs-narrow symmetry)───────────────────────────────
|
|
142
152
|
|
|
143
153
|
# 10. POSITIVE:RadioGroupItem 裸用,無 SelectionItem 無 label → BLOCK
|
|
@@ -191,6 +191,16 @@ expect_pass_silent "17. legit business composition demo → silent"
|
|
|
191
191
|
run_hook "Edit" "$CONSUMER_FILE" ""
|
|
192
192
|
expect_pass_silent "18. empty content → silent"
|
|
193
193
|
|
|
194
|
+
# 19. 2026-06-03 回歸防護(fragment-vs-file bug class):Edit 只送 new_string 片段,
|
|
195
|
+
# 但 @consumer-catalog-allow marker 在檔頭(不在每次 edit 片段)→ 修前編輯有 marker 的 portal
|
|
196
|
+
# 檔任一非 marker 行就被誤擋(AllDsComponents basename = catalog pattern)。本 hook 是 PostToolUse
|
|
197
|
+
# (檔已落 disk)→ 補查整檔 marker。建真實 disk 檔(含 marker)+ Edit 無 marker 片段 → 必 silent。
|
|
198
|
+
DISK_PORTAL="$TMP_DIR/apps/template/src/AllDsComponents.stories.tsx"
|
|
199
|
+
mkdir -p "$(dirname "$DISK_PORTAL")"
|
|
200
|
+
printf '%s\n' "// @consumer-catalog-allow: documented proxy portal" "export const ImportSmoke = () => <div/>;" > "$DISK_PORTAL"
|
|
201
|
+
run_hook "Edit" "$DISK_PORTAL" " <p>some unrelated edited line without the marker</p>"
|
|
202
|
+
expect_pass_silent "19. Edit 片段無 marker + 檔頭 disk marker → silent(回歸防護)"
|
|
203
|
+
|
|
194
204
|
echo ""
|
|
195
205
|
echo "=== Summary ==="
|
|
196
206
|
echo "Passed: $PASS / $((PASS + FAIL))"
|
|
@@ -56,6 +56,15 @@ function F() { return <span variant="naked" className="inline-flex items-center"
|
|
|
56
56
|
'
|
|
57
57
|
expect_exit "A.1.2 naked + no SSOT → BLOCK" 2 "naked row-mode propagation"
|
|
58
58
|
|
|
59
|
+
# 2b. 2026-06-03 回歸防護(同 R8 bug class):naked + items-center 跨多行 className(真實 JSX 格式)
|
|
60
|
+
# + no SSOT → BLOCK。修前 grep 逐行 → 多行 className 靜默漏 = P0 false-negative(對抗稽核抓到)。
|
|
61
|
+
run_hook "/r/packages/design-system/src/components/BadMulti/bad-multi.tsx" '
|
|
62
|
+
function F() { return <span variant="naked" className="inline-flex
|
|
63
|
+
bg-white
|
|
64
|
+
items-center" /> }
|
|
65
|
+
'
|
|
66
|
+
expect_exit "A.1.2b naked + 多行 className → BLOCK(回歸防護)" 2 "naked row-mode propagation"
|
|
67
|
+
|
|
59
68
|
# 3. allowlist → pass
|
|
60
69
|
run_hook "/r/packages/design-system/src/components/Edge/edge.tsx" '
|
|
61
70
|
// @naked-row-mode-allow: popover content
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
# - BLOCKER(exit 2):content 任一行命中 Tailwind spacing magic-number regex
|
|
8
8
|
# \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
|
|
9
9
|
# 且該行 **無** escape marker `@layout-space-magic-ok:`
|
|
10
|
-
# - Escape
|
|
10
|
+
# - Escape:同行 OR 前一行加 `// @layout-space-magic-ok: <rationale>` → 該行被濾掉
|
|
11
|
+
# (前一行支援 2026-06-03 補實作 — JSX className 行無法放同行 //,必靠前一行 {/* */})
|
|
11
12
|
# - 非 Edit/Write/MultiEdit tool / 非 .tsx,.ts / DS src / 空 content → silent exit 0
|
|
12
13
|
#
|
|
13
14
|
# M34 broad-vs-narrow symmetry:
|
|
@@ -157,6 +158,15 @@ run_hook "Write" "$APP_TSX" "content" \
|
|
|
157
158
|
'const Stack = () => <div className="gap-1">x</div>; // @layout-space-magic-ok: 4px icon stack, non-consumer-layout'
|
|
158
159
|
expect_silent "N4. magic line with @layout-space-magic-ok escape → silent"
|
|
159
160
|
|
|
161
|
+
# N4b. 2026-06-03 回歸防護:escape marker 在「前一行」→ silent。JSX className 行無法放同行 //,
|
|
162
|
+
# 原 code 只查同行(doc 卻宣稱支援前一行)→ JSX escape 實質失效;修後前一行 marker 必被認。
|
|
163
|
+
run_hook "Write" "$APP_TSX" "content" \
|
|
164
|
+
'const C = () => (
|
|
165
|
+
{/* @layout-space-magic-ok: dev artifact wrapper, non-consumer-layout */}
|
|
166
|
+
<div className="p-4">x</div>
|
|
167
|
+
);'
|
|
168
|
+
expect_silent "N4b. magic line with escape on PRECEDING line → silent(回歸防護)"
|
|
169
|
+
|
|
160
170
|
# N5. DS source file is excluded from scope → silent (even WITH magic number)
|
|
161
171
|
run_hook "Write" "$DS_TSX" "content" \
|
|
162
172
|
'const Btn = () => <button className="px-4 py-2 gap-2">click</button>;'
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# block-severity antiPattern → P0 record_worst 2,2026-06-02 升 P0)
|
|
15
15
|
#
|
|
16
16
|
# Test 重點:silent skip / 各 rule fire / allowlist marker escape。
|
|
17
|
-
# 不測 R3(需 spec.md frontmatter);R8 P0 block-severity 已測(#11
|
|
17
|
+
# 不測 R3(需 spec.md frontmatter);R8 P0 block-severity 已測(#11 單行 / #12-14 多行回歸防護,2026-06-03)。
|
|
18
18
|
|
|
19
19
|
set -u
|
|
20
20
|
|
|
@@ -215,6 +215,136 @@ export const Default = () => (
|
|
|
215
215
|
'
|
|
216
216
|
expect_block "11. R8 simplified-mock <SidebarHeader><span> → P0 BLOCK" "R8 story_archetype_registry"
|
|
217
217
|
|
|
218
|
+
# 12. R8 MULTI-LINE Sidebar drift(2026-06-03 回歸防護;修前 grep -E line-oriented + \s 當字面 's' → 多行靜默漏 = 假 P0):
|
|
219
|
+
# <SidebarHeader> 與 <span> 分行(真實 JSX 縮排格式)→ 修後(hook tr 換行→空格 + regex [[:space:]])應 BLOCK
|
|
220
|
+
run_hook "PreToolUse" "Write" "$STORIES_R8" '
|
|
221
|
+
// @story-baseline: @qijenchen/design-system/components/Sidebar/sidebar.stories.tsx#IconCollapse
|
|
222
|
+
export const Default = () => (
|
|
223
|
+
<Sidebar>
|
|
224
|
+
<SidebarHeader>
|
|
225
|
+
<span>Acme</span>
|
|
226
|
+
</SidebarHeader>
|
|
227
|
+
</Sidebar>
|
|
228
|
+
);
|
|
229
|
+
'
|
|
230
|
+
expect_block "12. R8 多行 <SidebarHeader> 換行 <span> → P0 BLOCK(回歸防護)" "R8 story_archetype_registry"
|
|
231
|
+
|
|
232
|
+
# 13. R8 MULTI-LINE ChromeHeader drift:<ChromeHeader> 與 <span flex-1> 分行 → 修後應 BLOCK
|
|
233
|
+
# (修前 [\s\S]*? 在 BSD grep -E 是字元類非「任意字元」+ line-oriented → 雙重漏)
|
|
234
|
+
STORIES_CH="/foo/my-project/packages/design-system/src/components/AppShell/app-shell-ch.stories.tsx"
|
|
235
|
+
run_hook "PreToolUse" "Write" "$STORIES_CH" '
|
|
236
|
+
// @story-baseline: @qijenchen/design-system/components/Sidebar/sidebar.stories.tsx#IconCollapse
|
|
237
|
+
export const Default = () => (
|
|
238
|
+
<ChromeHeader>
|
|
239
|
+
<SidebarTrigger />
|
|
240
|
+
<span className="flex-1 text-body-lg">儀表板</span>
|
|
241
|
+
</ChromeHeader>
|
|
242
|
+
);
|
|
243
|
+
'
|
|
244
|
+
expect_block "13. R8 多行 <ChromeHeader> 換行 <span flex-1> → P0 BLOCK" "R8 story_archetype_registry"
|
|
245
|
+
|
|
246
|
+
# 14. R8 false-positive 防護:正確多行 ChromeHeader(h1 緊鄰、無 flex-1 span)+ 另一 story 遠處(距 >160 字)
|
|
247
|
+
# 有無關 flex-1 span → .{0,160} 長度界擋住跨 story 誤匹配(greedy .* boolean 等同 lazy,故必設界)→ NO block
|
|
248
|
+
run_hook "PreToolUse" "Write" "$STORIES_CH" '
|
|
249
|
+
// @story-baseline: @qijenchen/design-system/components/Sidebar/sidebar.stories.tsx#IconCollapse
|
|
250
|
+
export const Correct = () => (
|
|
251
|
+
<ChromeHeader>
|
|
252
|
+
<SidebarTrigger />
|
|
253
|
+
<h1 className="text-body-lg font-medium">儀表板</h1>
|
|
254
|
+
</ChromeHeader>
|
|
255
|
+
);
|
|
256
|
+
export const Unrelated = () => (
|
|
257
|
+
<div className="grid grid-cols-3 gap-4 rounded-lg border border-border bg-surface p-6 mt-4">
|
|
258
|
+
<div className="text-body-sm text-muted">統計面板區塊內容說明文字</div>
|
|
259
|
+
<span className="flex-1">無關的彈性間距元素</span>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
'
|
|
263
|
+
expect_pass_silent "14. R8 正確多行 ChromeHeader + 遠處無關 flex-1(>160 字)→ 不誤判"
|
|
264
|
+
|
|
265
|
+
# 15. R8 disk @story-baseline-allow 豁免(2026-06-03 回歸防護,fragment-vs-file bug class,對抗稽核抓到):
|
|
266
|
+
# 檔頭有 @story-baseline-allow 的 disk 檔 + Edit 只送無 marker 片段(含 antiPattern)→ R8 不擋
|
|
267
|
+
# (補查整檔 disk head;修前只查片段 → 編輯有 marker 的 story 任一非 marker 行就被 R8 誤擋)。
|
|
268
|
+
# 斷言 EXIT=0(R8 未 record_worst 2);R7 anti-pattern stderr 警告與否不影響此斷言。
|
|
269
|
+
DISK_STORY_R8="$TMP_DIR/r8-disk-allow.stories.tsx"
|
|
270
|
+
printf '%s\n' "// @story-baseline-allow: legacy migration RFC-123" "export const X = () => (<Sidebar><SidebarHeader><span>A</span></SidebarHeader></Sidebar>)" > "$DISK_STORY_R8"
|
|
271
|
+
run_hook "PreToolUse" "Edit" "$DISK_STORY_R8" ' <SidebarHeader><span>Acme</span></SidebarHeader>'
|
|
272
|
+
if [ "$EXIT" = "0" ]; then
|
|
273
|
+
echo " PASS 15. R8 disk @story-baseline-allow + Edit 無 marker 片段 → R8 不擋(回歸防護)"; PASS=$((PASS+1))
|
|
274
|
+
else
|
|
275
|
+
echo " FAIL 15. R8 disk marker exempt (expected exit 0, got exit=$EXIT)"
|
|
276
|
+
echo " --- stderr ---"; echo "$STDERR_TEXT" | sed 's/^/ /'; echo " --- end ---"
|
|
277
|
+
FAIL=$((FAIL+1)); FAILED_TESTS="${FAILED_TESTS}\n - 15. R8 disk marker"
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# 16. R9 hand-craft overlay/chrome header(2026-06-04 codify per upload-manager 面板 drift):
|
|
281
|
+
# story 內手刻 <div px-[var(--layout-space-loose)] ... border-b border-divider> → P0 BLOCK
|
|
282
|
+
run_hook "PreToolUse" "Write" "$TMP_DIR/r9-handcraft.stories.tsx" '
|
|
283
|
+
export const Panel = () => (
|
|
284
|
+
<div className="max-w-md rounded-lg border bg-surface shadow-[var(--elevation-200)]">
|
|
285
|
+
<div className="flex items-center justify-between px-[var(--layout-space-loose)] py-2 border-b border-divider">
|
|
286
|
+
<span className="text-body font-medium">正在上傳 3 個項目</span>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
'
|
|
291
|
+
expect_block "16. R9 手刻 <div px-loose border-b border-divider> overlay header → P0 BLOCK" "R9 hand-craft overlay"
|
|
292
|
+
|
|
293
|
+
# 17. R9 false-positive 防護:正確消費 SurfaceHeader + PopoverTitle → 不擋(silent)
|
|
294
|
+
run_hook "PreToolUse" "Write" "$TMP_DIR/r9-correct.stories.tsx" '
|
|
295
|
+
export const Panel = () => (
|
|
296
|
+
<div className="max-w-md flex flex-col rounded-lg border-border bg-surface-raised shadow-[var(--elevation-200)]">
|
|
297
|
+
<SurfaceHeader className="justify-between [--chrome-slot-h:1.25rem]">
|
|
298
|
+
<div className="flex-1 min-w-0"><PopoverTitle>正在上傳 3 個項目</PopoverTitle></div>
|
|
299
|
+
</SurfaceHeader>
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
'
|
|
303
|
+
expect_pass_silent "17. R9 正確消費 SurfaceHeader+PopoverTitle → 不誤判"
|
|
304
|
+
|
|
305
|
+
# 18. R9 false-positive 防護:doc-table row 用 px-4(非 loose token)+ border-b → 不擋
|
|
306
|
+
run_hook "PreToolUse" "Write" "$TMP_DIR/r9-doctable.stories.tsx" '
|
|
307
|
+
export const Tbl = () => (<div className="px-4 py-2.5 border-b border-divider bg-neutral-hover">表頭列</div>);
|
|
308
|
+
'
|
|
309
|
+
expect_pass_silent "18. R9 doc-table px-4 + border-b(非 loose token)→ 不誤判"
|
|
310
|
+
|
|
311
|
+
# 19. R9 escape:檔頭 @story-baseline-allow → 不擋
|
|
312
|
+
run_hook "PreToolUse" "Write" "$TMP_DIR/r9-allow.stories.tsx" '// @story-baseline-allow: legacy panel demo
|
|
313
|
+
export const P = () => (<div className="px-[var(--layout-space-loose)] py-2 border-b border-divider" />);
|
|
314
|
+
'
|
|
315
|
+
expect_pass_silent "19. R9 @story-baseline-allow 豁免 → 不擋"
|
|
316
|
+
|
|
317
|
+
# 20. R9 FN-001 回歸(2026-06-04 adversarial workflow):多行 className flatten 後多空格 → 仍 BLOCK
|
|
318
|
+
# (修前單空格 regex 漏;改 border-b[[:space:]]+border-divider)
|
|
319
|
+
run_hook "PreToolUse" "Write" "$TMP_DIR/r9-multispace.stories.tsx" 'export const P = () => (
|
|
320
|
+
<div
|
|
321
|
+
className="flex justify-between px-[var(--layout-space-loose)] py-2
|
|
322
|
+
border-b
|
|
323
|
+
border-divider"
|
|
324
|
+
>正在上傳</div>
|
|
325
|
+
);
|
|
326
|
+
'
|
|
327
|
+
expect_block "20. R9 多行/多空格 className 手刻 header → BLOCK(FN-001 修)" "R9 hand-craft overlay"
|
|
328
|
+
|
|
329
|
+
# 21. R9 FP-001 防護:border-b border-divider 在 data-* 屬性(非 className)→ 不擋
|
|
330
|
+
# (修前 [^>]* 跨屬性誤判;改 [^\">]* 限同 className 字串)
|
|
331
|
+
run_hook "PreToolUse" "Write" "$TMP_DIR/r9-crossattr.stories.tsx" '
|
|
332
|
+
export const P = () => (<div className="px-[var(--layout-space-loose)]" data-x="border-b border-divider">x</div>);
|
|
333
|
+
'
|
|
334
|
+
expect_pass_silent "21. R9 border-divider 在 data-* 屬性 → 不誤判(FP-001 修)"
|
|
335
|
+
|
|
336
|
+
# 22. R9 FP-002 防護:純註解行含 pattern → 不擋(drop comment-only line 後 flatten)
|
|
337
|
+
run_hook "PreToolUse" "Write" "$TMP_DIR/r9-comment.stories.tsx" '// 範例(禁): <div className="px-[var(--layout-space-loose)] border-b border-divider">
|
|
338
|
+
export const P = () => (<SurfaceHeader>x</SurfaceHeader>);
|
|
339
|
+
'
|
|
340
|
+
expect_pass_silent "22. R9 純註解行含 pattern → 不誤判(FP-002 修)"
|
|
341
|
+
|
|
342
|
+
# 23. R9 skip-list 補 isOverlay 家(FileViewer)→ 不擋(R9-SKIP-001)
|
|
343
|
+
run_hook "PreToolUse" "Write" "/foo/my-project/packages/design-system/src/components/FileViewer/fv.stories.tsx" '
|
|
344
|
+
export const P = () => (<div className="px-[var(--layout-space-loose)] border-b border-divider">x</div>);
|
|
345
|
+
'
|
|
346
|
+
expect_pass_silent "23. R9 skip FileViewer(isOverlay 家)→ 不檢查"
|
|
347
|
+
|
|
218
348
|
echo ""
|
|
219
349
|
echo "=== Summary ==="
|
|
220
350
|
echo "Passed: $PASS / $((PASS + FAIL))"
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
"antiPatterns": [
|
|
17
17
|
{
|
|
18
18
|
"id": "sidebar-header-simplified-span",
|
|
19
|
-
"regex": "<SidebarHeader
|
|
19
|
+
"regex": "<SidebarHeader[^>]*>[[:space:]]*<span",
|
|
20
20
|
"severity": "block",
|
|
21
21
|
"rationale": "SidebarHeader 必 wrap WorkspaceBrand-like ItemAvatar block(per sidebar.stories#IconCollapse baseline);simplified <span> mock = drift"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"id": "sidebar-menu-button-inline-icon",
|
|
25
|
-
"regex": "<SidebarMenuButton
|
|
25
|
+
"regex": "<SidebarMenuButton[^>]*>[^<]*<[A-Z][a-zA-Z]+[[:space:]]+className=\"size-",
|
|
26
26
|
"severity": "block",
|
|
27
27
|
"rationale": "SidebarMenuButton 必用 startIcon prop + tooltip prop;children inline icon = drift"
|
|
28
28
|
}
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"antiPatterns": [
|
|
70
70
|
{
|
|
71
71
|
"id": "chromeheader-span-flex-1",
|
|
72
|
-
"regex": "<ChromeHeader
|
|
72
|
+
"regex": "<ChromeHeader[^>]*>.{0,160}<span[[:space:]]+className=\"[^\"]*flex-1[^\"]*\">",
|
|
73
73
|
"severity": "block",
|
|
74
74
|
"rationale": "ChromeHeader title 該用 <h1 className=\"text-body-lg font-medium\"> 緊鄰 SidebarTrigger,不該用 <span flex-1> 撐空間造成 toggle + title 巨大間距"
|
|
75
75
|
}
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
| **M20** | **AI 自問 best-practice + 自動 self-improve**(不靠 user 提醒)。Stop hook `rule_infra_best_practice_score`(R5 in `stop_passive_logging.sh`,2026-05-13 folded from retired `stop_meta_self_audit.sh`)每 turn 跑 score → < 80 / regression ≥ 5 log to `score-history.jsonl`;next-turn UserPromptSubmit `inject_pending_self_audit.sh` 注入 BLOCKER prompt。**Threshold 數值 SSOT**:`session_start_governance_check.sh`(L19-22 + L173)+ CLAUDE.md `# 治理 canonical` 行數預算表;本 M-rule 不重述具體 cap,避免多 home 漂移(2026-05-22 prune codify per Rule-of-3 violation fix)。**+ Sub-rule M33 (folded 2026-05-22)**:Stop hook overfire → AI conservative-defer 反 pattern。同 stop hook fire ≥ 5 次 within session → AI 自動 hedge / 撤回 claim / 「下個 session」defer **可做工作**。真理由 vs 偽 prereq 區辨 — (a)真 prereq 缺(cross-frame test fixture 等)= 具體 deliverable need;(b)偽 prereq(「context budget」「省工」「下個 session」)= conditioned hedge,user 反覆催 = 真理由不存在。**Rule**:AI reply 含「下個 session」「context budget」「省工」keyword 且前 1 turn user 已拍板「全部做完」/「繼續」/「沒理由不做」→ BLOCKER。對齊 Atlassian「stop using vague defer」+ Linux kernel patch review。 | user 10 次問「best-practice 嗎」直到外部 benchmark 確認真 gap;2026-05-13 claim-verify-gap stop hook fire 12+ 次 → 我每 reply 自動 hedge → user 多次糾正「沒理由不做」 |
|
|
61
61
|
| **M21** | **新元件 / 新 sub-component 抽象前必過 prop variant test**。當新元件名 = `<Existing>+suffix`(Time / Range / Color / Light / Dark / Filled / Outline / Compact / Rich)→ 強烈 signal 應為 prop variant on `<Existing>`。3-test 通過才能分(全失敗 → prop):(1) `<Existing>` 加 prop 無法達成同 DOM/behavior?(2) ≥3 家 world-class DS 用分離元件而非 prop(必 cite source)?(3) value 結構或 contract 真的不同(如 Range = [start, end])?Hook check_premature_abstraction(retired/未實作;mindset enforcement)。 | DateTimePicker 從 DatePicker 拆 → 撤回合併 `<DatePicker showTime>`(2026-05-02);DataTableFilterPanel 5-file → 撤回 sub-file pattern |
|
|
62
62
|
| **M22** | **Benchmark claim 必附 inline source citation**(不可憑印象)。寫 spec / tsx 含「Ant / Material / Polaris / Atlassian / Carbon / shadcn / Radix」等 world-class DS claim,必同段附:(a) inline URL(domain 對應 DS 官網 / GitHub source);(b) GitHub source path + line ref `#L42`;(c) screenshot reference `snapshots/...`;(d) 顯式撤回 `@benchmark-unverified`。Hook `check_benchmark_citation.sh` 機械化警告(P1 soft)。每次 implement 前跑 WebFetch 取真 source,不憑印象解碼。 | claim「Ant showTime range = 2 calendars」憑印象,實證 source code `multiplePanel = false` = 1 calendar(2026-05-02 鬼話事件)|
|
|
63
|
-
| **M23** | **DS 內既有 canonical 優先於外部 benchmark**。寫 visual decision(color / size / spacing / typography / state)/ prop name namespace 前必先 grep DS 既有 codified token / variant / pattern / prop;命中 → 必對齊;沒命中 → 才引 world-class + 同步補 canonical。**禁止**:外部 benchmark(M22 cite OK)直接覆蓋 DS 內已有 canonical。Mindset #2「優先消費既有」的 visual layer。判斷流程:(a) grep 該屬性 / prop 既有 token/variant/usage;(b) 命中 canonical?有 → 用,沒 → 走 M22 cite 然後 codify;(c) **prop name conflict 子規則**(former M27,2026-05-15 collapsed):外部 framework prop name(TanStack `size` / Radix `disabled` / dnd-kit attrs 等)跟 DS 既有 prop 同字不同義 → wrap-and-rename(DS-internal naming + pre-process map);(d) **nearest same-purpose canonical wins 子規則**(former M35,2026-05-22 folded):寫 stories / UI composition wrap 既有 primitive 前必先 grep + Read primitive 的「完整佈局」production canonical story(eg. `sidebar.stories.tsx#IconCollapse`)抄 archetype 結構 / helper / variant — 不准憑記憶寫 simplified mock。**Cite 存在 ≠ consume 落實**(codex 2026-05-20 verdict)。最相近同目的 canonical 用法 > 泛用 component spec / 寬鬆 pattern wording。Registry SSOT `.claude/references/story-baseline-registry.json` + hook `check_story_invariants.sh R8 story_archetype_registry
|
|
63
|
+
| **M23** | **DS 內既有 canonical 優先於外部 benchmark**。寫 visual decision(color / size / spacing / typography / state)/ prop name namespace 前必先 grep DS 既有 codified token / variant / pattern / prop;命中 → 必對齊;沒命中 → 才引 world-class + 同步補 canonical。**禁止**:外部 benchmark(M22 cite OK)直接覆蓋 DS 內已有 canonical。Mindset #2「優先消費既有」的 visual layer。判斷流程:(a) grep 該屬性 / prop 既有 token/variant/usage;(b) 命中 canonical?有 → 用,沒 → 走 M22 cite 然後 codify;(c) **prop name conflict 子規則**(former M27,2026-05-15 collapsed):外部 framework prop name(TanStack `size` / Radix `disabled` / dnd-kit attrs 等)跟 DS 既有 prop 同字不同義 → wrap-and-rename(DS-internal naming + pre-process map);(d) **nearest same-purpose canonical wins 子規則**(former M35,2026-05-22 folded):寫 stories / UI composition wrap 既有 primitive 前必先 grep + Read primitive 的「完整佈局」production canonical story(eg. `sidebar.stories.tsx#IconCollapse`)抄 archetype 結構 / helper / variant — 不准憑記憶寫 simplified mock。**Cite 存在 ≠ consume 落實**(codex 2026-05-20 verdict)。最相近同目的 canonical 用法 > 泛用 component spec / 寬鬆 pattern wording。**手刻浮層 / chrome header 子規則(2026-06-04 加)**:做「像 popover / dialog 的浮層面板」前必先 grep + Read `patterns/overlay-surface`(SurfaceHeader/Body/Footer)+ `Popover`(chrome token + PopoverHeader/PopoverTitle)SSOT;**手刻 `<div px-loose border-b border-divider>` header 或 `<div rounded bg-surface shadow-elevation>` 殼 = drift**,必消費 primitive。Registry SSOT `.claude/references/story-baseline-registry.json` + hook `check_story_invariants.sh R8 story_archetype_registry` **+ R9 hand_craft_overlay_header(零誤判 px-loose+border-divider 簽名,BLOCKER)**。**forcing-function 教訓**:M1/M23(d) 是 mindset,但 mindset 沒有完整 hook 兜底 = 紙老虎(會憑印象手刻);新場景若既有 hook 無 antiPattern 覆蓋 → 補 hook rule 才算落地。 | 2026-05-03 chevron color:DS `text-foreground` (icon-only Button neutral-9 85%) vs 我憑「Ant 5 家 muted」覆蓋 → 自開新 tier 違反一致設計語言。2026-05-06 column width:DS 49+ 處 `size: 'sm'\|'md'\|'lg'` density vs TanStack `size: 280` px = 同 prop 不同義 → `meta.width` wrap。2026-05-20 AppShell-vs-Sidebar drift:simplified mock + jargon + wrong variant + 不消費 primitive props。**2026-06-04 upload-manager 面板手刻 header + 殼**(`py-2`≠`py-tight` / `rounded-md`≠`lg` / `border-divider`≠`border` / `bg-surface`≠`raised`),既有 3 道網全漏(R1-C 要 absolute+dismiss / `_chrome_header_handcraft` skip stories / R7-R8 只比已註冊 primitive 名)→ adversarial workflow 抓 + 加 R9 補洞。Hook `check_data_table_size_num_to_meta_width.sh` + `check_story_invariants.sh R8/R9` |
|
|
64
64
|
| **M24** | **State 顯著性 precedence:disabled > muted > emphasis**。元件在 disabled state 時,內部所有文字載體(label / value / placeholder / icon)統一切 disabled token(`text-fg-disabled` neutral-6),**不**繼續 muted token(`text-fg-muted` neutral-7)。muted 是裝飾,disabled 是語意 state — state 勝 emphasis。同理:error / warning state 內 placeholder 應對應 state token。Hook `check_field_family_invariants.sh` A.4(原 check_disabled_placeholder_color folded,live P1 stderr)。 | 2026-05-04 5+ violation:`bareInputStyles` 永遠 muted / `select.tsx` 3 處 plain&tag empty 不分 mode / `textarea.tsx` 同;Input.tsx 唯一做對。User 5+ 次糾正才 codify |
|
|
65
65
|
| **M25** | **Layered chain invariant 必整鏈 forward(viewport-aware overlay scroll 範例)**。Overlay surface(Popover / HoverCard / Dialog / Sheet)的 viewport-aware scroll 機制要求 root → SurfaceBody 之間**所有中間 wrapper 都 forward `flex flex-col h-full`**;任何中間 div 沒 forward → SurfaceBody flex-1 失效 → body 不 scroll。同類 chain pattern:density 透傳 / fieldCtx 鏈 / theme subtree(M3 portal 逃逸對偶)。Hook `check_pattern_invariants.sh` C.1(原 check_overlay_panel_scroll_chain folded,live P1 WARN)。 | 2026-05-04:Filter / Sort panel root `<div w-[640px]>` 無 flex-col → user 縮 viewport 時 body 不 scroll。NameCard 因自設 max-h flex-col 才繞過(無中間 wrapper) |
|
|
66
66
|
| **M26** | **Behavior / visual canonical decision 前必跑 WebFetch + WebSearch 取 ≥ 3 source,不可憑印象 propose**。M22 升級版 — M22 管「寫 spec / tsx 含 claim 必附 cite」(實作後),M26 管「propose / 決策前必先 fetch」(實作前)。Pipeline:(1) WebFetch 3 家世界級 source(Atlassian / Material / Polaris / Ant / Carbon / Apple HIG / shadcn / Radix);(2) 全 403 → WebSearch fallback 用 snippet,**明示「search-only confidence」**;(3) WebFetch + WebSearch 都失敗 → STOP propose,告知 user「無法 verify,要看 screenshot/實機」。Hook `check_propose_without_benchmark.sh`(2026-05-26 backfill — 前期 doc claim 但 file missing;UserPromptSubmit 偵測 user prompt 含 propose / visual / behavior decision keyword + 近 20 turn WebFetch/WebSearch < 2 → soft inject 提醒)。 | 2026-05-05:user 反覆糾「為什麼每次都沒 webfetch 只憑印象」— Jira drag handle / cell display 兩題我憑印象 propose 多輪;升級成硬規範 |
|
|
@@ -80,7 +80,7 @@ detect_mode() {
|
|
|
80
80
|
|
|
81
81
|
**PURE-JUDGMENT dim 真跑證據強制(2026-05-30 generalize,user 問「包括所有 infra 稽核?」)**:judgment dim(無 deterministic script / write-time hook 兜底者,含 infra 62/66/68/72 fork-onboarding/runtime/API-surface)report 必逐 dim show「DS-wide N files scanned + file:line findings / 或『0 after 全掃』」真跑證據,**禁只 mention dim 號**。report-validator `check_audit_post_report_validator.sh` Validator G 機械強制(evidence marker 數 < judgment dim 數 = BLOCKER)。DETERMINISTIC(22)+ HOOK(42)dim 由 CI / write-time hook 兜底(含 22/26 infra dim),不在此 risk。
|
|
82
82
|
|
|
83
|
-
### A.1b —
|
|
83
|
+
### A.1b — Claim-vs-code + docblock + spec-internal adversarial verification(MANDATORY,NO-SAMPLE,per-component)
|
|
84
84
|
|
|
85
85
|
**2026-05-30 anchor(user verbatim 質問「之前他媽都在偷懶?」)**:獨立 adversarial 再審抓到 **403 findings / 64 單元 / 0 全乾淨**,其中 **202 個 FALSE_CLAIM**(anatomy/a11y/principles/spec 系統性記載 code 根本沒有的行為:Calendar 宣稱方向鍵導覽 / Tooltip·HoverCard 宣稱 focus trap / Alert 記不存在的 `actions` prop / Select 宣稱「用原生 select」但桌機自建 cmdk / AspectRatio spec 說「無 wrapper」但 code wrap)。**根因**:前期 audit 把 story-content dim(12/24/25/30/43 等)當「散文層 looks-fine 掃」跑,**沒 adversarial 讀 .tsx(+ wrap 的 lib)逐句比對宣稱**。這是「偷懶」的具體 failure mode。
|
|
86
86
|
|
|
@@ -90,6 +90,10 @@ detect_mode() {
|
|
|
90
90
|
0. **機械 gate 先跑(deterministic,2026-06-02 加)**:`npm run typecheck:stories`(deterministic 抓 stories 的 `{var}`-undefined / prop 型別錯 —— **這是 SizeMatrix `{size}` crash 的真防線**;主 tsc -b exclude stories 故必跑此)+ `node scripts/storybook-smoke-test.mjs --full`(runtime crash render 掃)。先過才進 adversarial read。Anchor:2026-06-02 Field SizeMatrix `{size}` JSX-undefined crash 隨 beta.44 ship。**注**:smoke 全覆蓋 coverage-gate(防靜默-skip 假綠燈)attempted 但 CI server 規模化降級(~60 story 後 timeout 撞 20-min budget)→ **defer**(需 robust-server / browser-recycle + 可靠測試環境);故 typecheck:stories 是目前 deterministic 主防線。
|
|
91
91
|
1. **per-component(NO-SAMPLE,全 62 component + 全 pattern)** dispatch adversarial agent。
|
|
92
92
|
2. 每 agent 必 **Read 元件 .tsx + 其 wrap 的 lib(Radix/cmdk/react-day-picker/sonner 等)source**,對該元件**所有** anatomy / a11y / principles / spec 宣稱**逐句**比對:鍵盤 map / ARIA role / focus 行為 / prop 存在性 / 視覺 token / 預設值 / native-vs-custom。
|
|
93
|
+
**+ 2026-06-04 補兩 lens(Dim 15 cross-doc 明文涵蓋但前期被「散文 skim」漏的同 class failure;非抽樣、非無覆蓋,是「有 dim 淺跑」)**:
|
|
94
|
+
- **(a) 元件 `.tsx` 自己的 docblock / inline 註解 vs 同檔 code**:A.1b 原設計把 `.tsx` 當「真相來源」去驗別人(story/spec),**從不反驗它自己的註解** → docblock claim 的 padding / typography mode / hover / 行為 vs code 真實值,逐行比。
|
|
95
|
+
- **(b) spec 段落間描述性一致(spec-internal cross-section)**:A.1b 原驗 claim-vs-code,**不驗 spec-段-vs-段** → 同一 spec 內 Mode 表 typography 標籤 / padding 值 / gap 值 / surface 行為,跨 section 不可打架。
|
|
96
|
+
- **Anchor**:2026-06-04 user 抓「deep-audit 多次沒發現」—— tsx docblock(`閱讀模式 1.5` / `hover:bg-neutral-hover`,自 2026-04-23 stale)+ spec Mode 表 typography stale,全是 Dim 15 該抓、卻被當散文掃漏。納入此強制 adversarial(Validator F per-component 覆蓋率閘一併保證),不再可 skim。
|
|
93
97
|
3. **「自上次 audit 無 code 改動」≠ 可跳過** —— content 宣稱可在 code 沒變下就是假的(前期正是用此藉口跳過 = 違規)。
|
|
94
98
|
4. output per-component:`{component, claimsVerified: N, falseClaims: [{fileLine, 宣稱, 真實 code 行為}]}`。
|
|
95
99
|
5. findings 併入 A.2 triage(FALSE_CLAIM 對齊 doc-to-code = autonomous;substantive design-language tension = HOLD propose)。
|
|
@@ -75,7 +75,7 @@ Grouped by theme. Each runs as an independent subagent; many can parallelize.
|
|
|
75
75
|
| # | Audit | What it catches |
|
|
76
76
|
|---|-------|-----------------|
|
|
77
77
|
| 14 | **命名一致性** | PascalCase folder / kebab-case file / hook naming / spec chapter 中文 / identifier 英文 / single-file 語言統一 |
|
|
78
|
-
| 15 | **Cross-doc 一致性** | CLAUDE.md 自身 + cross-spec full dup(Rule-of-3)+ tsx docblock-spec drift + stale upgrade markers
|
|
78
|
+
| 15 | **Cross-doc 一致性** | CLAUDE.md 自身 + cross-spec full dup(Rule-of-3)+ tsx docblock-spec drift + stale upgrade markers + **(2026-06-04 加)docblock-vs-同檔-code + spec 段內描述性一致(Mode 表 / typography / padding / gap cross-section)**。詳 `audit-prompts.md` Dim 15。**Enforcement 升級**:本 dim 易被「散文 skim」淺跑漏(2026-06-04 anchor:tsx docblock `閱讀模式`/`hover-bg` 自 2026-04-23 stale 多次沒抓)→ 改由 `/deep-audit-cross-codex` A.1b **強制 adversarial per-component**(Validator F 覆蓋率閘),不再可 skim |
|
|
79
79
|
|
|
80
80
|
### Group F — Architecture compliance (P1 priority, session-learned)
|
|
81
81
|
|
package/ds-story-manifest.json
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"consumedBy": [
|
|
6
6
|
".claude/hooks/check_consumer_story_baseline.sh",
|
|
7
7
|
"scripts/composition-fidelity-visual-diff.mjs",
|
|
8
|
-
"product-workspace apps/template/src/AllDsComponents.stories.tsx (
|
|
8
|
+
"product-workspace apps/template/src/AllDsComponents.stories.tsx (ImportSmoke portal → DS Storybook link)"
|
|
9
9
|
],
|
|
10
|
-
"generatedAt": "2026-06-
|
|
10
|
+
"generatedAt": "2026-06-04T02:30:20.840Z"
|
|
11
11
|
},
|
|
12
12
|
"components": {
|
|
13
13
|
"accordion": {
|
|
@@ -770,7 +770,9 @@
|
|
|
770
770
|
"design-system-components-fileitem-展示--compact",
|
|
771
771
|
"design-system-components-fileitem-展示--hover-swap",
|
|
772
772
|
"design-system-components-fileitem-展示--clickable",
|
|
773
|
-
"design-system-components-fileitem-展示--compact-mixed"
|
|
773
|
+
"design-system-components-fileitem-展示--compact-mixed",
|
|
774
|
+
"design-system-components-fileitem-展示--upload-manager-surface",
|
|
775
|
+
"design-system-components-fileitem-展示--upload-manager-compact-surface"
|
|
774
776
|
],
|
|
775
777
|
"設計規格": [
|
|
776
778
|
"design-system-components-fileitem-設計規格--overview",
|
|
@@ -797,7 +799,8 @@
|
|
|
797
799
|
"design-system-components-fileupload-展示--resume-upload",
|
|
798
800
|
"design-system-components-fileupload-展示--bulk-image-upload",
|
|
799
801
|
"design-system-components-fileupload-展示--with-file-list",
|
|
800
|
-
"design-system-components-fileupload-展示--custom-children"
|
|
802
|
+
"design-system-components-fileupload-展示--custom-children",
|
|
803
|
+
"design-system-components-fileupload-展示--button-variant"
|
|
801
804
|
],
|
|
802
805
|
"設計規格": [
|
|
803
806
|
"design-system-components-fileupload-設計規格--overview",
|
|
@@ -1681,5 +1684,5 @@
|
|
|
1681
1684
|
}
|
|
1682
1685
|
},
|
|
1683
1686
|
"totalComponents": 62,
|
|
1684
|
-
"totalStories":
|
|
1687
|
+
"totalStories": 914
|
|
1685
1688
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qijenchen/design-system",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.53",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).",
|
|
6
6
|
"type": "module",
|
|
@@ -27,10 +27,16 @@ export interface EmptyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
27
27
|
description?: string
|
|
28
28
|
/** 行動按鈕(可選) */
|
|
29
29
|
action?: React.ReactNode
|
|
30
|
+
/**
|
|
31
|
+
* Disabled context(2026-06-03 加 — FileUpload disabled 等情境消費):title / description 轉
|
|
32
|
+
* `text-fg-disabled`(語意 disabled token,非 opacity)。icon glyph 也 → fg-disabled(icon 是文字一環);icon-circle bg 維持 muted。
|
|
33
|
+
* 預設 false,不影響既有 consumer。
|
|
34
|
+
*/
|
|
35
|
+
disabled?: boolean
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
const Empty = React.forwardRef<HTMLDivElement, EmptyProps>(
|
|
33
|
-
({ icon, title, description, action, className, ...props }, ref) => {
|
|
39
|
+
({ icon, title, description, action, disabled = false, className, ...props }, ref) => {
|
|
34
40
|
// 字體 tier:讀 RowSizeContext(menu 內自動對齊 menu items 的字體)
|
|
35
41
|
// 沒有 context(standalone)→ fallback 'md' → text-body (14px)
|
|
36
42
|
const rowSize = useRowSize('md')
|
|
@@ -45,7 +51,8 @@ const Empty = React.forwardRef<HTMLDivElement, EmptyProps>(
|
|
|
45
51
|
iconElement = icon
|
|
46
52
|
} else {
|
|
47
53
|
const Icon = icon as LucideIcon
|
|
48
|
-
|
|
54
|
+
// disabled:glyph → fg-disabled([&_svg]:!… 蓋過 Avatar 內聯 color;circle bg 維持 muted)。icon 是文字一環,隨 disabled 變淡。
|
|
55
|
+
iconElement = <Avatar icon={Icon} size={48} color="neutral" className={disabled ? '[&_svg]:!text-fg-disabled' : undefined} />
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -59,7 +66,7 @@ const Empty = React.forwardRef<HTMLDivElement, EmptyProps>(
|
|
|
59
66
|
<div className="mb-4">{iconElement}</div>
|
|
60
67
|
)}
|
|
61
68
|
{title && (
|
|
62
|
-
<span className=
|
|
69
|
+
<span className={cn('text-body-lg font-medium', disabled ? 'text-fg-disabled' : 'text-foreground')}>
|
|
63
70
|
{title}
|
|
64
71
|
</span>
|
|
65
72
|
)}
|
|
@@ -69,7 +76,7 @@ const Empty = React.forwardRef<HTMLDivElement, EmptyProps>(
|
|
|
69
76
|
// 字體跟 RowSizeContext 對齊:sm/md = text-body (14px),lg = text-body-lg (16px)
|
|
70
77
|
// 在 menu 內自動對齊 menu items;standalone 時 fallback text-body
|
|
71
78
|
descFont,
|
|
72
|
-
(title || action) ? 'text-fg-secondary' : 'text-fg-muted',
|
|
79
|
+
disabled ? 'text-fg-disabled' : (title || action) ? 'text-fg-secondary' : 'text-fg-muted',
|
|
73
80
|
// Empty title 永遠 body-lg(16)→ 用 reading-lg token(label tier 決定)
|
|
74
81
|
title && 'mt-[var(--item-gap-label-desc-reading-lg)]',
|
|
75
82
|
)}
|
|
@@ -101,7 +101,7 @@ export const ColorMatrix: Story = {
|
|
|
101
101
|
<H3>Status × 元素 色彩矩陣</H3>
|
|
102
102
|
<Desc>
|
|
103
103
|
FileItem 本身無色彩變體——text 走 item-anatomy row primitive 共用 token
|
|
104
|
-
(`--foreground` / `--fg-secondary`);background 依 mode 固定(rich = `--surface` + border / compact
|
|
104
|
+
(`--foreground` / `--fg-secondary`);background 依 mode 固定(rich = `--surface` + border / compact 無 status = `--secondary` / compact 有 status = transparent),**無 hover-bg**(見下方 Container background table)。
|
|
105
105
|
Status 才驅動色彩:progress bar 色(inProgress / success / error)+ status icon 色(check / X)+ description 色(error 時升階)。
|
|
106
106
|
</Desc>
|
|
107
107
|
<div className="overflow-x-auto mb-4">
|
|
@@ -162,7 +162,7 @@ export const ColorMatrix: Story = {
|
|
|
162
162
|
<div>
|
|
163
163
|
<H3>Container background(per mode,**無 hover-bg**)</H3>
|
|
164
164
|
<Desc>
|
|
165
|
-
FileItem 設計準則(2026-04-23):**永不顯示 hover-bg**。三種型態皆已 permanent-anchored(rich = border card / compact
|
|
165
|
+
FileItem 設計準則(2026-04-23):**永不顯示 hover-bg**。三種型態皆已 permanent-anchored(rich = border card / compact 無 status = bg-secondary / compact 有 status = 底部 progress bar),再加 hover-bg 是 double-emphasis 視覺雜。affordance 只靠 `cursor-pointer`(onClick 時)+ hover-swap icon fade。詳 spec「Hover 行為 canonical」。
|
|
166
166
|
</Desc>
|
|
167
167
|
<div className="overflow-x-auto">
|
|
168
168
|
<table className="text-caption border-collapse">
|
|
@@ -171,8 +171,8 @@ export const ColorMatrix: Story = {
|
|
|
171
171
|
</thead>
|
|
172
172
|
<tbody>
|
|
173
173
|
<tr><Td mono>rich(all status)</Td><Td><span className="inline-flex items-center gap-1.5"><Swatch value="--surface" size="sm" /><span className="font-mono">--surface</span> + border</span></Td><Td>永遠是 card(border + rounded + bg-surface)</Td></tr>
|
|
174
|
-
<tr><Td mono>compact
|
|
175
|
-
<tr><Td mono>compact
|
|
174
|
+
<tr><Td mono>compact 無 status</Td><Td><span className="inline-flex items-center gap-1.5"><Swatch value="--secondary" size="sm" /><span className="font-mono">--secondary</span>(= neutral-3)</span></Td><Td>靜態 pill,對齊 Badge low / ProgressBar track SSOT</Td></tr>
|
|
175
|
+
<tr><Td mono>compact 有 status</Td><Td><span className="font-mono">transparent</span></Td><Td>底部 progress bar 作 permanent affordance(分隔線型)</Td></tr>
|
|
176
176
|
<tr><Td mono>hover(任意 mode)</Td><Td><span className="font-mono">無變化</span></Td><Td>permanent-anchored → 不加 hover-bg。cursor-pointer 作 affordance(onClick 時)</Td></tr>
|
|
177
177
|
<tr><Td mono>error</Td><Td><span className="font-mono">容器不變</span></Td><Td>只升階 description / bar / icon,不染容器——避免整 row 轉紅蓋過其他 metadata</Td></tr>
|
|
178
178
|
</tbody>
|