#!/usr/bin/env sh set -e echo "▶ pre-commit: vue-tsc --noEmit" pnpm exec vue-tsc --noEmit echo "▶ pre-commit: pnpm test (vitest)" pnpm test echo "▶ pre-commit: check no figma-data/ sync-output writes" # Block only pure sync extract output (raw figma data + tokenize/normalize/icon # generator output + MCP cache). These are auto-regenerated by sync pipeline; # human edits get silently overwritten. Allows: figma-to-code-mapping.json # (human-maintained), audit-allowlist/, audit reports (*.audit.json), # docs-figma-members/ (derivative data modules), figma-styles.json (post- # EXTRACT-006 pipeline output, downstream-consumed by translation layer — # legitimate to commit since src/tokens/variables.css + token-aliases.ts need it). FIGMA_DATA_BLOCK_PATTERN='^figma-data/(raw/|published/|normalized/(figma-mcp-cache/|components-tokenized|variables\.json))' # Sync-commit whitelist (INFRA-F35 ⑥ + P2 #6): legitimate sync output commits # skip the block when a sync signal is present. Two signals (either suffices): # A. staged sync report file (cleanup-unpublished-report / cleanup-dup- # candidates / figma-sync-report-YYYY-MM-DD) # B. today's figma-sync-report-YYYY-MM-DD.md exists in workspace (tracked # or untracked) — means today's sync ran; pre-commit shouldn't force # user to also git-add the reports just to pass hook. # (We don't check commit message prefix because pre-commit runs before git # -m's message lands in COMMIT_EDITMSG — see 2026-05-27 retro.) SYNC_COMMIT_SIGNAL="" if git diff --cached --name-only --diff-filter=AM | grep -qE 'docs/internal/(cleanup-unpublished-report|figma-sync-report-[0-9-]+)\.md'; then SYNC_COMMIT_SIGNAL="staged sync report file" fi if [ -z "$SYNC_COMMIT_SIGNAL" ]; then TODAY=$(date +%Y-%m-%d) if [ -f "docs/internal/figma-sync-report-${TODAY}.md" ]; then SYNC_COMMIT_SIGNAL="today's sync report present in workspace (figma-sync-report-${TODAY}.md)" fi fi if git diff --cached --name-only --diff-filter=AM | grep -qE "$FIGMA_DATA_BLOCK_PATTERN"; then if [ -n "$SYNC_COMMIT_SIGNAL" ]; then echo " ✓ sync-commit whitelist allowed (signal: $SYNC_COMMIT_SIGNAL)" else echo "" echo "❌ figma-data/ sync-output 写入被 pre-commit hook 阻断" echo " (AGENTS.md 硬规则 #1:figma 真源镜像,sync pipeline 单向写入)" echo " 被阻断的路径模式:" echo " - figma-data/raw/ (raw figma extracts)" echo " - figma-data/published/ (export-icons output)" echo " - figma-data/normalized/figma-mcp-cache/ (MCP cache)" echo " - figma-data/normalized/components-tokenized* (tokenize output)" echo " - figma-data/normalized/variables.json (normalize output)" echo "" echo " 允许(human-maintained / audit reports / downstream-consumed):" echo " - figma-data/figma-to-code-mapping.json (mapping config)" echo " - figma-data/audit-allowlist/ (audit allowlists)" echo " - figma-data/normalized/figma-styles.json (EXTRACT-006 pipeline output; translation/token-aliases.ts 引用)" echo " - figma-data/normalized/*.audit.json (audit reports)" echo " - figma-data/normalized/tokenized-diff-report.json" echo " - figma-data/normalized/docs-figma-members/ (data modules)" echo "" echo " 合法 sync commit 自动放行的两个信号(INFRA-F35 ⑥ + P2 #6):" echo " A. staged 含 docs/internal/cleanup-unpublished-report.md" echo " 或 figma-sync-report-YYYY-MM-DD.md" echo " B. workspace 含今日 figma-sync-report-${TODAY:-YYYY-MM-DD}.md" echo " (今日跑过 sync 即可,无需手动 git-add reports)" echo "" echo " 如确实要绕过(非 sync 的手改),用 git commit --no-verify" exit 1 fi fi echo "▶ pre-commit: visual/design commit approval check" if git diff --cached --name-only --diff-filter=AM | grep -qE '\.(html|css|svg|vue|jsx|tsx)$'; then if [ -z "$VISUAL_COMMIT_APPROVED" ]; then echo "" echo "❌ Visual/design 文件 staged (.html / .css / .svg / .vue / .jsx / .tsx)" echo " 未设置 VISUAL_COMMIT_APPROVED 环境变量" echo "" echo " Why: AI auto-commits 视觉改动 without user 审 = 反复绕弯反模式" echo " (2026-05-13 retrospection: v6/v7 logo 多轮 push back)" echo "" echo " 要 commit 时显式声明(用户已 explicit 看过 + approve 视觉效果):" echo " VISUAL_COMMIT_APPROVED=1 git commit ..." exit 1 fi echo " ✓ VISUAL_COMMIT_APPROVED set, visual commit allowed" fi echo "▶ pre-commit: component-affordances SoT drift check" # Run when the affordance SoT/view/scripts change, or when a canonical # component / its export barrel changes (a prop rename there can stale the SoT). if git diff --cached --name-only --diff-filter=AM | grep -qE '(docs/internal/component-affordances\.(json|md)|scripts/(generate|audit)-component-affordances\.mjs|src/canonical/.*\.vue|src/canonical/index\.ts)'; then node scripts/audit-component-affordances.mjs else echo " ✓ no component-affordances-affecting files staged, skipped" fi echo "▶ pre-commit: rule-load-map coverage (scoped-load 路由表零漏 rule)" # scoped-load 安全网:conventions 文件的 §AI 读取指引 路由表必须覆盖每条 rule, # 否则 skill step 2 的 scoped 读会静默漏掉未路由的 rule(2026-06-12 激活 scoped-load # 的前置 gate)。Run when a conventions file (rules or its routing table) or a # consuming skill's 起手协议 or the audit itself changes. if git diff --cached --name-only --diff-filter=AM | grep -qE '(docs/internal/(mockup|code)-conventions\.md|skills/tvu-design-(mockup|code)/SKILL\.md|scripts/audit-rule-load-map\.mjs)'; then node scripts/audit-rule-load-map.mjs else echo " ✓ no rule-load-map-relevant files staged, skipped" fi echo "▶ pre-commit: stale-anchors (规则真源引用的锚点零孤儿)" # 过期锚点轻校验(authoring-gate spec Part 5 决策 1):规则文档引用的文件/脚本/env/§交叉引用/ # heading slug 一旦其目标被删/改名/合并就成孤儿。动 heading 文本会漂移 fragment slug,故 DEF # 文件(含 design-process / figma-technical-reference,被 SCAN 文件 fragment 指向)任一变更即跑。 if git diff --cached --name-only --diff-filter=AM | grep -qE '(docs/(internal/(mockup-conventions|code-conventions|design-process|figma-technical-reference)|meta-rules)\.md|scripts/audit-stale-anchors\.mjs)'; then node scripts/audit-stale-anchors.mjs else echo " ✓ no stale-anchors-relevant files staged, skipped" fi echo "▶ pre-commit: hard-rule compliance (no-side-by-side-theme / figma-fills / inline-svg)" # Run when a docs page, canonical/component, or figma-sync/scripts file changes — # these are what H1/H2/H3 scan. Catches the no-side-by-side-theme class mechanically # (the SelectPage dual-theme dropdown matrix that slipped past the manual review). if git diff --cached --name-only --diff-filter=AM | grep -qE '(playground/docs/pages/.*\.vue|src/(canonical|components)/.*\.vue|figma-sync/.*\.(mjs|ts|js)|scripts/.*\.(mjs|ts|js))'; then node scripts/audit-hard-rule-compliance.mjs else echo " ✓ no hard-rule-relevant files staged, skipped" fi echo "▶ pre-commit: export-coverage (有组件 ≠ 接进产物)" # Run when the public entry, a component/canonical .vue, or the audit itself # changes — a new component or a dropped export can orphan a component (the Logo # gap, 2026-06-03). Catches "built but never wired into src/index.ts". if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/index\.ts|src/(canonical|components)/.*\.vue|scripts/audit-export-coverage\.mjs)'; then node scripts/audit-export-coverage.mjs else echo " ✓ no export-coverage-relevant files staged, skipped" fi echo "▶ pre-commit: sprint self-audit D/F/G (canonical-compliance / render-verification-coverage / figma-library-vs-canonical)" # AGENTS.md §Sprint 收尾 Self-Audit promises D/E/F/G a pre-commit backstop, but # until 2026-06-10 only E (hard-rule) was wired here — D/F/G ran on AI discipline # alone (system-review-2026-06-10 P1-01). Same conditional pattern as E/H/I: run # when a canonical/component .vue, the mapping registry, the shared exempt # allowlist, or one of the three audits changes. Deliberately NOT the whole # audit:self-audit-phase2 chain — E and I already have their own blocks below/above. if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/(canonical|components)/.*\.vue|figma-data/figma-to-code-mapping\.json|figma-data/audit-allowlist/canonical-exempt\.json|scripts/audit-(canonical-compliance|render-verification-coverage|figma-library-vs-canonical)\.mjs)'; then node scripts/audit-canonical-compliance.mjs node scripts/audit-render-verification-coverage.mjs node scripts/audit-figma-library-vs-canonical.mjs else echo " ✓ no canonical-audit-relevant files staged, skipped" fi echo "▶ pre-commit: canonical slot-guard (CE-safe useHasSlot, not \$slots — INFRA-F54)" # Canonical components become CE via defineCustomElement, where $slots/useSlots() # are always empty — a v-if="$slots.x" guard silently drops the slot in the React / # web-component build while the Vue SFC build stays green (shipped broken twice # before pixel screenshots caught it). Run when a canonical .vue or the audit # itself changes; blocks $slots/useSlots in src/canonical/*.vue (use useHasSlot). if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/canonical/.*\.vue|scripts/audit-canonical-slot-guard\.mjs)'; then node scripts/audit-canonical-slot-guard.mjs else echo " ✓ no canonical-slot-guard-relevant files staged, skipped" fi echo "▶ pre-commit: token-contract (variables.css primitive tier vs Figma — INFO-04)" # prepublishOnly + sync Step 8.5 already gate this, but Owner-direct-master means # a hand-edited primitive in variables.css would otherwise sit undetected until # release (system-review-2026-06-10 INFO-04 defense-in-depth). if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/tokens/variables\.css|figma-data/normalized/variables\.json|scripts/audit-token-contract\.mjs)'; then pnpm run audit:token-contract else echo " ✓ no token-contract-relevant files staged, skipped" fi echo "▶ pre-commit: doc-de-mirror (耐久文档不镜像版本 — 真源单一)" # 耐久文档(PROJECT_GOAL / README / GETTING_STARTED / AGENTS / PLUGIN)不得写死版本号镜像 # current-state(真源在 STATUS/tracker/backlog;plugin 版本真源在 PLUGIN 自身)。 # 2026-07-08 实证:PROJECT_GOAL 两处 + PLUGIN stale v0.6.1 漂移 → 升机械闸防复漂(INFRA-F58 扩 5 份)。 if git diff --cached --name-only --diff-filter=AM | grep -qE '(docs/PROJECT_GOAL\.md|README\.md|docs/GETTING_STARTED\.md|AGENTS\.md|docs/PLUGIN\.md|scripts/audit-doc-de-mirror\.mjs)'; then pnpm run audit:doc-de-mirror else echo " ✓ no doc-de-mirror-relevant files staged, skipped" fi echo "▶ pre-commit: doc-sync (跨文件 staleness — 路径完整 + 事实一致, INFRA-F58)" # doc-sync-map 依赖边路径存在 + 跨文件事实(manifest entry 计数等)一致。 # 2026-07-08 实证:manifest 908 vs 930 跨 3 份文档漂移 → 升机械闸。 if git diff --cached --name-only --diff-filter=AM | grep -qE '(docs/internal/doc-sync-map\.json|scripts/audit-doc-sync\.mjs|figma-data/render-verification-manifest\.json|docs/FIGMA_AS_SOURCE_OF_TRUTH\.md|docs/PROJECT_GOAL\.md|docs/STATUS\.md)'; then pnpm run audit:doc-sync else echo " ✓ no doc-sync-relevant files staged, skipped" fi echo "▶ pre-commit: sort-tokens (variables.css 分类分组不拆散/无游离 — INFRA-F48)" # variables.css 按语义分类分组展示(meta-rules §1)。此闸机械守护 I1 同名分组标题不重复 # + I2 无游离 token,仅当 variables.css 或脚本自身 staged 才跑。与 token-contract 不重叠 # (那管值 vs Figma,这管分组结构)。 if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/tokens/variables\.css|scripts/audit-sort-tokens\.mjs)'; then node scripts/audit-sort-tokens.mjs src/tokens/variables.css else echo " ✓ no sort-tokens-relevant files staged, skipped" fi echo "▶ pre-commit: eslint dogfood (own plugin on src/canonical|components — P2-05)" # The plugin's rule logic must run on our own src, not only at consumers # (system-review-2026-06-10 P2-05: untested-path risk). Consumer-only rules # (no-base-component-import / no-native-element) are deliberately absent from # eslint.config.js — see its header. if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/(canonical|components)/.*\.vue|eslint\.config\.js|eslint-plugin/)'; then pnpm run lint:ds else echo " ✓ no eslint-dogfood-relevant files staged, skipped" fi echo "▶ pre-commit: icon-canonical-names (use canonical , not short alias)" # Run when a canonical/component .vue (where refs live), the generated icon # registry, or the audit itself changes. Blocks short-alias drift (e.g. name="close" # instead of "action/close") — generalizes the 2026-06-06 close/clear normalization. if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/(canonical|components)/.*\.vue|src/icons/generated/.*\.ts|scripts/audit-icon-canonical-names\.mjs)'; then node scripts/audit-icon-canonical-names.mjs else echo " ✓ no icon-name-relevant files staged, skipped" fi echo "▶ pre-commit: icon naming rules (category-enum / leaf-kebab — Q4)" # Run when the generated icon registry or the audit script itself changes. if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/icons/generated/.*\.ts|scripts/audit-icon-naming-rules\.mjs)'; then node scripts/audit-icon-naming-rules.mjs category-enum && node scripts/audit-icon-naming-rules.mjs leaf-kebab else echo " ✓ no icon-registry-relevant files staged, skipped" fi echo "▶ pre-commit: binding-config-parity (components.config.ts ⟷ canonical SFC defineProps)" # Run when the component config (source of truth for the generator) OR any # canonical SFC changes — a prop added/renamed in either side without updating # the other would silently drift the generated React types. if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/web-components/components\.config\.ts|src/canonical/.*\.vue|scripts/audit-binding-config-parity\.mjs)'; then pnpm audit:binding-config-parity else echo " ✓ no binding-config-parity-relevant files staged, skipped" fi echo "▶ pre-commit: prop-naming (appearance-variant 命名统一 + 黑名单 — INFRA-F59)" if git diff --cached --name-only --diff-filter=AM | grep -qE '(src/web-components/components\.config\.ts|src/canonical/.*\.vue|src/design-system/translation/prop-naming-conventions\.json|scripts/audit-prop-naming\.mjs)'; then node scripts/audit-prop-naming.mjs else echo " ✓ no prop-naming-relevant files staged, skipped" fi # artifact-routing gate — 改了制品真源/路由/唤醒词时,校验三件齐全 staged_routing=$(git diff --cached --name-only --diff-filter=ACM | grep -E 'docs/internal/(mockup-conventions|design-process)\.md|docs/WAKE-WORDS\.md' || true) if [ -n "$staged_routing" ]; then echo "▶ pre-commit: artifact-routing (canonical 制品三件齐全)" node scripts/audit-artifact-routing.mjs || exit 1 fi # mockup handoff evidence gate — staged 的 handoff .md 必须含脚本证据块 staged_md=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.md$' || true) if [ -n "$staged_md" ]; then echo "▶ pre-commit: mockup handoff evidence" node scripts/audit-mockup-handoff-evidence.mjs $staged_md || exit 1 fi echo "▶ pre-commit: demo slot+boolean coverage (INFRA-F45 硬规则 #8)" # Run when a demo surface, the in-scope SoT, or the audit itself changes. Blocks any # in-scope component whose Vue OR React demo drops slot projection or a boolean # live-toggle (the two chains most prone to silent CE breakage — INFRA-F43). if git diff --cached --name-only --diff-filter=AM | grep -qE '(react-pilot/src/App\.tsx|playground/docs/pages/.*\.vue|src/web-components/components\.config\.ts|scripts/audit-demo-slot-boolean-coverage\.mjs)'; then node scripts/audit-demo-slot-boolean-coverage.mjs else echo " ✓ no demo-slot-boolean-relevant files staged, skipped" fi echo "▶ pre-commit: demo framework parity (INFRA-F46 full-parity 防漂移闸)" # Run when a pilot Vue page, a React demo module, or the parity audit itself changes. # Blocks any pilot component whose Vue demo-region section titles ≠ its React demo # module section titles (full-parity drift between the two hand-written demo sources). if git diff --cached --name-only --diff-filter=AM | grep -qE '(react-pilot/src/demos/.*\.tsx|playground/docs/pages/.*\.vue|scripts/audit-demo-framework-parity\.mjs)'; then node scripts/audit-demo-framework-parity.mjs else echo " ✓ no demo-framework-parity-relevant files staged, skipped" fi echo "✅ pre-commit gates passed"