prizmkit 1.1.53 → 1.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/bin/create-prizmkit.js +2 -0
  2. package/bundled/VERSION.json +3 -3
  3. package/bundled/rules/_rules-metadata.json +6 -1
  4. package/bundled/rules/general/cohesive-modeling.md +27 -0
  5. package/bundled/skills/_metadata.json +1 -1
  6. package/bundled/skills/app-planner/SKILL.md +114 -4
  7. package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +609 -0
  8. package/bundled/skills/app-planner/references/rules/backend/fixed-rules.md +285 -0
  9. package/bundled/skills/app-planner/references/rules/backend/question-bank.md +249 -0
  10. package/bundled/skills/app-planner/references/rules/backend/template.md +173 -0
  11. package/bundled/skills/app-planner/references/rules/database/derivation-rules.md +373 -0
  12. package/bundled/skills/app-planner/references/rules/database/fixed-rules.md +211 -0
  13. package/bundled/skills/app-planner/references/rules/database/question-bank.md +184 -0
  14. package/bundled/skills/app-planner/references/rules/database/template.md +158 -0
  15. package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +810 -0
  16. package/bundled/skills/app-planner/references/rules/frontend/fixed-rules.md +188 -0
  17. package/bundled/skills/app-planner/references/rules/frontend/question-bank.md +302 -0
  18. package/bundled/skills/app-planner/references/rules/frontend/template.md +320 -0
  19. package/bundled/skills/app-planner/references/rules/mobile/derivation-rules.md +639 -0
  20. package/bundled/skills/app-planner/references/rules/mobile/fixed-rules.md +290 -0
  21. package/bundled/skills/app-planner/references/rules/mobile/question-bank.md +232 -0
  22. package/bundled/skills/app-planner/references/rules/mobile/template.md +175 -0
  23. package/bundled/skills/prizm-kit/SKILL.md +1 -1
  24. package/bundled/skills/prizmkit-init/SKILL.md +47 -6
  25. package/bundled/skills/prizmkit-init/references/config-schema.md +7 -3
  26. package/bundled/skills/prizmkit-init/references/rules/layer-detection.md +41 -0
  27. package/package.json +1 -1
  28. package/src/index.js +10 -0
  29. package/src/scaffold.js +124 -7
@@ -0,0 +1,810 @@
1
+ # Derivation Rules — Frontend Auto-Derivation Rule Mapping
2
+
3
+ > This file defines the mapping "Phase 2 answers → rule blocks auto-injected into frontend-rules.md". The AI reads this file in Phase 3 — **do not ask the user again**.
4
+ >
5
+ > Usage: Each rule block has a unique ID (e.g., `D-TAILWIND-01`). After matching the user's answer, inject the "rule body" verbatim into the corresponding `{{ ... }}` placeholder in [frontend-rules.template.md](../templates/frontend-rules.template.md).
6
+
7
+ ---
8
+
9
+ ## Phase 2 Answer Direct-Fill Table (no derivation needed; copy the user's answer text directly into the placeholder)
10
+
11
+ > These placeholders **don't need rule blocks**. In Phase 4, the AI directly writes the answers collected in Phase 2 into them.
12
+
13
+ | Template Placeholder | Source | Example Fill |
14
+ |---------------------|--------|-------------|
15
+ | `{{ project_name }}` | Phase 0 auto-read from `package.json` `name` field; if unavailable, ask the user | `robot-svc` |
16
+ | `{{ generated_at }}` | Phase 4, AI writes current ISO date | `2026-05-18` |
17
+ | `{{ project_scope }}` | Phase 0 ask user ("Which directories/subprojects does this spec apply to?") | `apps/web, apps/admin` |
18
+ | `{{ design_source }}` | Q8 answer | `Existing HTML prototype, path temp/` |
19
+ | `{{ framework }}` | Q1 answer | `React 18+` |
20
+ | `{{ meta_framework }}` | Q2 answer | `Next.js (App Router)` |
21
+ | `{{ package_manager }}` | Q3 answer | `pnpm` |
22
+ | `{{ style_solution }}` | Q4 answer | `Tailwind CSS` |
23
+ | `{{ state_lib }}` | Q5 answer | `Zustand` |
24
+ | `{{ server_state_lib }}` | Q6 answer | `TanStack Query` |
25
+ | `{{ api_type_source }}` | Q7 answer | `OpenAPI auto-generated` |
26
+ | `{{ token_layering }}` | Q9 answer | `Three-layer (primitive / semantic / component)` |
27
+ | `{{ dark_mode }}` | Q10 answer | `Supported, CSS variable switching` |
28
+ | `{{ responsive_strategy }}` | Q11 answer | `Mobile-first` |
29
+ | `{{ breakpoint_scheme }}` | Q12 answer | `Tailwind defaults` |
30
+ | `{{ i18n }}` | Q13 answer | `Chinese only` |
31
+ | `{{ a11y_level }}` | Q14 answer | `WCAG 2.1 AA` |
32
+ | `{{ test_coverage }}` | Q15 answer | `Shared components + critical path E2E` |
33
+ | `{{ unit_test_framework }}` | Q16 answer | `Vitest` |
34
+ | `{{ e2e_framework }}` | Q17 answer | `Playwright` |
35
+ | `{{ ai_max_lines }}` | Q22 answer (number) | `300` |
36
+ | `{{ lcp_target }}` | Q23 answer | `<2.5s` |
37
+ | `{{ font_strategy }}` | Q4b answer | Direct fill |
38
+ | `{{ bundle_size }}` | Q24 answer | `<300KB (<100KB gzipped)` |
39
+
40
+ Appendix placeholders are auto-generated by AI in Phase 4:
41
+
42
+ | Template Placeholder | Generation Method |
43
+ |---------------------|-------------------|
44
+ | `{{ deny_list_summary }}` | Extract all "❌ Forbid" entries from §1.5 / §3 / §7.4 / §9 |
45
+ | `{{ recommended_libs }}` | Recommend ecosystem based on `framework` / `style_solution` / `state_lib` combinations |
46
+
47
+ ---
48
+
49
+ ## Trigger Map
50
+
51
+ | Trigger (Phase 2 Answer) | Inject Rule Block ID | Inject Into Placeholder |
52
+ |--------------------------|---------------------|------------------------|
53
+ | Q1 = React | `D-REACT-01`, `D-REACT-02` | `{{ tech_stack_rules }}`, `{{ style_specific_rules }}` |
54
+ | Q1 = Vue 3 | `D-VUE-01`, `D-VUE-02` | `{{ tech_stack_rules }}` |
55
+ | Q1 = Svelte | `D-SVELTE-01` | `{{ tech_stack_rules }}` |
56
+ | Q1 = Solid | `D-SOLID-01` | `{{ tech_stack_rules }}` |
57
+ | Q1 = Custom | `D-CUSTOM-FW-01` | `{{ tech_stack_rules }}` |
58
+ | Q2 = Next.js | `D-NEXT-01`, `D-NEXT-02` | `{{ tech_stack_rules }}` |
59
+ | Q2 = Nuxt | `D-NUXT-01` | `{{ tech_stack_rules }}` |
60
+ | Q2 = Remix | `D-REMIX-01` | `{{ tech_stack_rules }}` |
61
+ | Q2 = Plain SPA (Vite) | `D-SPA-01` | `{{ tech_stack_rules }}` |
62
+ | Q2 = Custom | `D-CUSTOM-META-01` | `{{ tech_stack_rules }}` |
63
+ | Q3 = pnpm | `D-PNPM-01` | `{{ tech_stack_rules }}` |
64
+ | Q3 = npm | `D-NPM-01` | `{{ tech_stack_rules }}` |
65
+ | Q3 = yarn | `D-YARN-01` | `{{ tech_stack_rules }}` |
66
+ | Q3 = bun | `D-BUN-01` | `{{ tech_stack_rules }}` |
67
+ | Q4 = Tailwind | `D-TAILWIND-01`, `D-TAILWIND-02` | `{{ style_specific_rules }}` |
68
+ | Q4 = CSS Modules | `D-CSSMOD-01` | `{{ style_specific_rules }}` |
69
+ | Q4 = CSS-in-JS | `D-CSSINJS-01` | `{{ style_specific_rules }}` |
70
+ | Q4 = UnoCSS | `D-UNO-01` | `{{ style_specific_rules }}` |
71
+ | Q4 = Plain CSS + BEM | `D-BEM-01` | `{{ style_specific_rules }}` |
72
+ | Q4b = Self-hosted, font-display: swap | `D-FONT-SELF-01` | `{{ performance_rules }}` |
73
+ | Q4b = CDN, font-display: swap | `D-FONT-CDN-01` | `{{ performance_rules }}` |
74
+ | Q4b = System font stack only | `D-FONT-SYSTEM-01` | `{{ performance_rules }}` |
75
+ | Q4b = Not decided yet | `D-FONT-UNDECIDED-01` | `{{ performance_rules }}` |
76
+ | Q5 = Zustand | `D-ZUSTAND-01` | `{{ state_rules }}` |
77
+ | Q5 = Pinia | `D-PINIA-01` | `{{ state_rules }}` |
78
+ | Q5 = Redux Toolkit | `D-RTK-01` | `{{ state_rules }}` |
79
+ | Q5 = Jotai/Valtio | `D-JOTAI-01` | `{{ state_rules }}` |
80
+ | Q5 = Context only | `D-CTX-01` | `{{ state_rules }}` |
81
+ | Q6 = TanStack Query | `D-TANSTACK-01`, `D-TANSTACK-02` | `{{ server_state_rules }}` |
82
+ | Q6 = SWR | `D-SWR-01` | `{{ server_state_rules }}` |
83
+ | Q6 = RTK Query | `D-RTKQ-01` | `{{ server_state_rules }}` |
84
+ | Q6 = Custom fetch hook wrapper | `D-CUSTOM-FETCH-01` | `{{ server_state_rules }}` |
85
+ | Q7 = OpenAPI auto-gen | `D-OPENAPI-01` | `{{ server_state_rules }}` |
86
+ | Q7 = Protobuf | `D-PROTO-01` | `{{ server_state_rules }}` |
87
+ | Q7 = Hand-written .d.ts | `D-TYPES-MANUAL-01` | `{{ server_state_rules }}` |
88
+ | Q7 = Shared monorepo type package | `D-TYPES-MONOREPO-01` | `{{ server_state_rules }}` |
89
+ | Q9 = Three-layer Token | `D-TOKEN-3L-01` | `{{ token_layering_rules }}` |
90
+ | Q9 = Two-layer Token | `D-TOKEN-2L-01` | `{{ token_layering_rules }}` |
91
+ | Q9 = Single-layer Token | `D-TOKEN-1L-01` | `{{ token_layering_rules }}` |
92
+ | Q10 = CSS variable dark | `D-DARK-CSSVAR-01` | `{{ dark_mode_rules }}` |
93
+ | Q10 = class-based dark | `D-DARK-CLASS-01` | `{{ dark_mode_rules }}` |
94
+ | Q10 = No dark mode | `D-DARK-NONE-01` | `{{ dark_mode_rules }}` |
95
+ | Q11 = Mobile-first | `D-RESP-MF-01` | `{{ breakpoint_rules }}` |
96
+ | Q11 = Desktop-first | `D-RESP-DF-01` | `{{ breakpoint_rules }}` |
97
+ | Q11 = Desktop only | `D-RESP-DESKTOP-ONLY-01` | `{{ breakpoint_rules }}` |
98
+ | Q11 = Mobile only | `D-RESP-MOBILE-ONLY-01` | `{{ breakpoint_rules }}` |
99
+ | Q12 = Tailwind defaults | `D-BP-TW-01` | `{{ breakpoint_rules }}` |
100
+ | Q12 = MD breakpoints | `D-BP-MD-01` | `{{ breakpoint_rules }}` |
101
+ | Q12 = Custom breakpoints | `D-BP-CUSTOM-01` | `{{ breakpoint_rules }}` |
102
+ | Q13 = Multi-language | `D-I18N-01` | `{{ i18n_rules }}` |
103
+ | Q13 = Single-language, centralized | `D-I18N-NONE-01` | `{{ i18n_rules }}` |
104
+ | Q13 = Single-language, hardcoding | `D-I18N-HARDCODE-01` | `{{ i18n_rules }}` |
105
+ | Q14 = WCAG 2.1 AA | D-A11Y-AA-01 | `{{ a11y_extra_rules }}` |
106
+ | Q14 = WCAG 2.1 AAA | `D-A11Y-AAA-01` | `{{ a11y_extra_rules }}` |
107
+ | Q14 = Basic keyboard reachability only | `D-A11Y-BASIC-01` | `{{ a11y_extra_rules }}` |
108
+ | Q15 + Q16/Q17 | `D-TEST-01` (dynamically assembled) | `{{ test_rules }}` |
109
+ | Q18 = Enforce index sync | `D-AI-INDEX-STRICT-01` | `{{ ai_index_rule }}` |
110
+ | Q18 = Not required | `D-AI-INDEX-LOOSE-01` | `{{ ai_index_rule }}` |
111
+ | Q19 = No, forbid self-initiated | `D-AI-DEP-STRICT-01` | `{{ ai_dependency_rule }}` |
112
+ | Q19 = Yes, with declaration | `D-AI-DEP-ANNOUNCE-01` | `{{ ai_dependency_rule }}` |
113
+ | Q19 = No limit | `D-AI-DEP-FREE-01` | `{{ ai_dependency_rule }}` |
114
+ | Q20 = Yes, must list callers | `D-AI-BREAK-STRICT-01` | `{{ ai_breaking_change_rule }}` |
115
+ | Q20 = No | `D-AI-BREAK-LOOSE-01` | `{{ ai_breaking_change_rule }}` |
116
+ | Q21 = Forbid; config files must be manually edited | `D-AI-CONFIG-FRONTEND-STRICT-01` | `{{ ai_config_rule }}` |
117
+ | Q21 = Allowed, must explain each change | `D-AI-CONFIG-FRONTEND-LOOSE-01` | `{{ ai_config_rule }}` |
118
+ | Q22 = Any value N | `D-AI-LINES-01` (replace `{{ ai_max_lines }}`) | `{{ ai_max_lines }}` |
119
+ | Q23 = LCP <2.5s | `D-PERF-25-01` | `{{ performance_rules }}` |
120
+ | Q23 = LCP <1.5s | `D-PERF-15-01` | `{{ performance_rules }}` |
121
+ | Q23 = Not required yet | `D-PERF-NONE-01` | `{{ performance_rules }}` |
122
+ | Q24 = chunk <300KB | `D-CHUNK-300-01` | `{{ performance_rules }}` |
123
+ | Q24 = chunk <500KB | `D-CHUNK-500-01` | `{{ performance_rules }}` |
124
+ | Q24 = No limit | `D-CHUNK-NONE-01` | `{{ performance_rules }}` |
125
+
126
+ ---
127
+
128
+ ## Rule Block Definitions
129
+
130
+ ### D-REACT-01
131
+ **Trigger**: Q1 = React | **Inject into**: `{{ tech_stack_rules }}`
132
+
133
+ - Framework: React 18+, use function components + Hooks. Forbid Class components.
134
+ - Hooks rules:
135
+ - Custom Hook filenames start with `use` (`useUserProfile.ts`).
136
+ - Strictly forbid calling Hooks inside loops, conditions, or nested functions.
137
+ - `useEffect` dependency arrays must be complete. Forbid `// eslint-disable-next-line react-hooks/exhaustive-deps`.
138
+ - Side effect cleanup functions must be explicitly returned.
139
+ - Performance:
140
+ - List rendering must provide a stable `key`. Forbid using `index` as key (unless the list is read-only and never reordered).
141
+ - Reusable callbacks use `useCallback`. Reusable objects use `useMemo`. But **prefer restructuring the component** over abusing memo.
142
+ - Large lists use virtualization (`react-window` / `react-virtuoso`).
143
+ - Context:
144
+ - One Context carries only one concern.
145
+ - Frequently updated state must not go in Context. Use a dedicated store.
146
+
147
+ ### D-REACT-02
148
+ **Trigger**: Q1 = React | **Inject into**: `{{ style_specific_rules }}` (only when Q4 is not CSS-in-JS)
149
+
150
+ - React components use `clsx` / `classnames` to concatenate className. Forbid string concatenation.
151
+ - Conditional styles must be expressed in `clsx`. Avoid ternary operator stacking.
152
+
153
+ ### D-VUE-01
154
+ **Trigger**: Q1 = Vue 3 | **Inject into**: `{{ tech_stack_rules }}`
155
+
156
+ - Framework: Vue 3 + `<script setup lang="ts">`. Forbid Options API (unless legacy code).
157
+ - Composition API:
158
+ - `ref` / `reactive` selection rule: primitives use `ref`, objects/arrays use `reactive`.
159
+ - Cross-component reusable logic must be encapsulated as `useXxx` composables, named with the `use` prefix.
160
+ - `watch` must explicitly specify the data source. `watchEffect` only for pure reactive tracking.
161
+ - Templates:
162
+ - `v-for` must include `:key`, and the `key` must be unique and stable.
163
+ - Strictly forbid `v-if` and `v-for` on the same element simultaneously.
164
+ - `v-html` only for trusted content. Must have a comment explaining the source.
165
+ - Single-file component structure order: `<script setup>` → `<template>` → `<style scoped>`.
166
+
167
+ ### D-VUE-02
168
+ **Trigger**: Q1 = Vue 3 | **Inject into**: `{{ tech_stack_rules }}`
169
+
170
+ - Props/Emits must use `defineProps<T>()` / `defineEmits<T>()` TypeScript generic form.
171
+ - Component Props must have default values (`withDefaults`).
172
+ - Cross-component communication priority: props/emits > provide/inject > global store.
173
+
174
+ ### D-SVELTE-01
175
+ **Trigger**: Q1 = Svelte | **Inject into**: `{{ tech_stack_rules }}`
176
+
177
+ - Use Svelte 5 Runes (`$state`, `$derived`, `$effect`). Forbid mixing old `$:` reactive syntax.
178
+ - Component filenames PascalCase. `<script lang="ts">` must enable TypeScript.
179
+
180
+ ### D-SOLID-01
181
+ **Trigger**: Q1 = Solid | **Inject into**: `{{ tech_stack_rules }}`
182
+
183
+ - Strictly forbid destructuring props (breaks reactivity). Must access as `props.foo`.
184
+ - Side effects use `createEffect`. Derived state uses `createMemo`.
185
+
186
+ ### D-CUSTOM-FW-01
187
+ **Trigger**: Q1 = Custom | **Inject into**: `{{ tech_stack_rules }}`
188
+
189
+ - Record user's custom framework and version verbatim. Platform-specific rules deferred to user input.
190
+
191
+ ### D-CUSTOM-META-01
192
+ **Trigger**: Q2 = Custom | **Inject into**: `{{ tech_stack_rules }}`
193
+
194
+ - User selected a custom meta-framework. Record the user's exact description of the meta-framework and its conventions. No pre-authored rules exist for this option.
195
+
196
+ ### D-NEXT-01
197
+ **Trigger**: Q2 = Next.js | **Inject into**: `{{ tech_stack_rules }}`
198
+
199
+ - Use App Router (`app/` directory). Forbid adding new Pages Router pages.
200
+ - Default to Server Components. Explicitly mark `'use client'` when interactivity is needed, and push it down to leaf nodes.
201
+ - Server Action function files start with `'use server'`. Forbid exposing sensitive logic in Client Components.
202
+ - Data fetching:
203
+ - Server Components directly `await fetch`, using Next.js caching strategies (`cache`, `revalidate`).
204
+ - Client Components use TanStack Query / SWR.
205
+ - Route file naming follows Next.js conventions: `page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`.
206
+
207
+ ### D-NEXT-02
208
+ **Trigger**: Q2 = Next.js | **Inject into**: `{{ tech_stack_rules }}`
209
+
210
+ - Images must use `next/image`. Forbid `<img>` (unless SVG inline).
211
+ - Fonts must use `next/font`. Forbid external CDN font links.
212
+ - Links must use `next/link`. Forbid using `<a>` for internal navigation.
213
+
214
+ ### D-NUXT-01
215
+ **Trigger**: Q2 = Nuxt | **Inject into**: `{{ tech_stack_rules }}`
216
+
217
+ - Use Nuxt 3+. Directory conventions: `pages/`, `components/`, `composables/`, `server/`.
218
+ - Server-side data fetching uses `useFetch` / `useAsyncData`. Forbid direct `await fetch` in setup.
219
+ - `definePageMeta` is used to declare route metadata (auth, layout).
220
+
221
+ ### D-REMIX-01
222
+ **Trigger**: Q2 = Remix | **Inject into**: `{{ tech_stack_rules }}`
223
+
224
+ - Data loading uses `loader`. Data writing uses `action`. Forbid writing fetch in components.
225
+ - Prefer `<Form>` for forms. Follow Remix's progressive enhancement philosophy.
226
+ - Error handling uses route-level `ErrorBoundary` exports.
227
+
228
+ ### D-PNPM-01
229
+ **Trigger**: Q3 = pnpm | **Inject into**: `{{ tech_stack_rules }}`
230
+
231
+ - Package manager locked to pnpm. Commit `pnpm-lock.yaml` to the repository root. Forbid committing `package-lock.json` / `yarn.lock`.
232
+ - `package.json` declares `"packageManager": "pnpm@x.y.z"`. CI validates.
233
+ - Monorepo uses `pnpm workspace`. Forbid global dependencies.
234
+
235
+ ### D-TAILWIND-01
236
+ **Trigger**: Q4 = Tailwind | **Inject into**: `{{ style_specific_rules }}`
237
+
238
+ - Use Tailwind CSS as the sole styling solution. Forbid mixing with CSS Modules / CSS-in-JS.
239
+ - Colors, spacing, and font sizes must use tokens extended in `tailwind.config.{ts,js}`. Forbid arbitrary values (e.g., `bg-[#1a73e8]`, `p-[13px]`).
240
+ - Exception: only when a one-off non-token value is truly needed (e.g., integrating a third-party widget), and must have a comment explaining why.
241
+ - Utility class stacks ≤ 5 write directly in JSX. Beyond 5, must:
242
+ - Extract as `@apply` component class (only for generic semantic components), or
243
+ - Split into sub-components.
244
+ - State variant order fixed: `base → hover → focus → active → disabled → dark`.
245
+ - Forbid writing complex conditional class string concatenation in templates. Must use `clsx` or `tailwind-variants` / `cva`.
246
+ - Recommend installing and enabling `prettier-plugin-tailwindcss` for automatic utility class sorting.
247
+
248
+ ### D-TAILWIND-02
249
+ **Trigger**: Q4 = Tailwind | **Inject into**: `{{ style_specific_rules }}`
250
+
251
+ - Design tokens configured in `tailwind.config`'s `theme.extend` in layers:
252
+ - Must not write hex colors directly in `theme.colors`. Must reference CSS variables (e.g., `var(--color-primary-500)`).
253
+ - This naturally makes dark mode / multi-theme switching compatible.
254
+ - Custom plugins centralized in `tailwind/plugins/` directory. One plugin per file, single responsibility.
255
+
256
+ ### D-CSSMOD-01
257
+ **Trigger**: Q4 = CSS Modules | **Inject into**: `{{ style_specific_rules }}`
258
+
259
+ - Each component gets a matching `Component.module.css`. Class naming uses simplified BEM: `block__element--modifier`.
260
+ - Forbid using `:global` to cross-contaminate component styles (except global reset/typography files).
261
+ - Reusable styles use `composes:`. Forbid copy-pasting style blocks.
262
+ - Colors and spacing referenced via CSS variables. Forbid hardcoding.
263
+ - Class names must be camelCase (auto-converted to kebab-case on export by the build tool).
264
+
265
+ ### D-CSSINJS-01
266
+ **Trigger**: Q4 = CSS-in-JS (styled-components / emotion) | **Inject into**: `{{ style_specific_rules }}`
267
+
268
+ - Theme object centralized in `src/theme/index.ts`. All colors/spacing/font sizes must be read from the theme.
269
+ - Forbid hardcoding hex colors in styled template literals.
270
+ - Dynamic styles must go through props (`${({ $primary }) => ...}`). Forbid using className switching.
271
+ - Watch out for runtime performance: styled components reused in lists must be hoisted to the module level. Forbid defining inside render.
272
+ - Recommend using `babel-plugin-styled-components` / `@emotion/babel-plugin` to optimize bundle size.
273
+
274
+ ### D-UNO-01
275
+ **Trigger**: Q4 = UnoCSS | **Inject into**: `{{ style_specific_rules }}`
276
+
277
+ - Use UnoCSS preset-uno + preset-attributify (on demand).
278
+ - Shortcuts centralized in `uno.config.ts`'s `shortcuts` field. Named semantically.
279
+ - Forbid writing more than 5 attributes in attributify mode. Beyond that, extract a shortcut.
280
+
281
+ ### D-ZUSTAND-01
282
+ **Trigger**: Q5 = Zustand | **Inject into**: `{{ state_rules }}`
283
+
284
+ - Global state uses Zustand. One store per file, path `src/stores/<domain>Store.ts`.
285
+ - Stores must export selector functions. Forbid `useStore(state => state)` full subscription in components.
286
+ - Async actions implemented in the store. Components only call actions, not process async directly.
287
+ - Persistence uses `zustand/middleware persist`. Explicitly set `partialize` fields. Forbid full persistence of sensitive data.
288
+ - Derived state uses `useShallow` or selector composition to avoid unnecessary re-renders.
289
+
290
+ ### D-PINIA-01
291
+ **Trigger**: Q5 = Pinia | **Inject into**: `{{ state_rules }}`
292
+
293
+ - Use Pinia setup store style (functional). Do not use options store style.
294
+ - One store per file, path `src/stores/<domain>.ts`. Named `useXxxStore`.
295
+ - Async action logic placed inside the store. Getters used only for derived computation. Forbid side effects.
296
+ - Persistence uses `pinia-plugin-persistedstate`. Declare fields as needed.
297
+
298
+ ### D-RTK-01
299
+ **Trigger**: Q5 = Redux Toolkit | **Inject into**: `{{ state_rules }}`
300
+
301
+ - Use Redux Toolkit only. Forbid bare Redux + hand-written reducers.
302
+ - Use `createSlice` to define reducers. Use `createAsyncThunk` for async.
303
+ - Selectors must use `createSelector` (reselect) for memoization.
304
+ - File structure: `src/features/<domain>/<domain>Slice.ts`.
305
+
306
+ ### D-JOTAI-01
307
+ **Trigger**: Q5 = Jotai / Valtio | **Inject into**: `{{ state_rules }}`
308
+
309
+ - Atom naming ends with `Atom` (`userAtom`, `themeAtom`).
310
+ - Derived atoms use `atom((get) => ...)`. Forbid recalculating in components.
311
+ - Async atoms must pair with loadable / Suspense handling.
312
+
313
+ ### D-CTX-01
314
+ **Trigger**: Q5 = Context only | **Inject into**: `{{ state_rules }}`
315
+
316
+ - No global state library used. State kept close to where it's used (component-local `useState`).
317
+ - Cross-component sharing uses React Context + `useReducer`.
318
+ - Single Context carries only one concern. Frequently updated state split into separate Contexts.
319
+ - Provide selector hooks (e.g., `useAuthUser`) wrapping `useContext` to avoid components directly consuming Context.
320
+
321
+ ### D-TANSTACK-01
322
+ **Trigger**: Q6 = TanStack Query | **Inject into**: `{{ server_state_rules }}`
323
+
324
+ - Server state uniformly uses TanStack Query. Forbid the `useEffect + fetch` pattern in components.
325
+ - queryKey naming convention: `[domain, resource, params?]`. Must be a stable, serializable array.
326
+ - Example: `['user', 'profile', { userId }]`, `['order', 'list', { page, status }]`.
327
+ - Default config (set centrally in `QueryClient`):
328
+ - `staleTime: 60_000` (default 1 minute)
329
+ - `gcTime: 5 * 60_000` (default 5 minutes)
330
+ - `retry: 1` (write operations `retry: 0`)
331
+ - `refetchOnWindowFocus: false` (enable per business need)
332
+ - Mutations must handle `onSuccess` to invalidate related queries (`queryClient.invalidateQueries`).
333
+ - Error handling: use the global `QueryCache` `onError` hook for unified reporting. UI layer renders error state via the `error` field.
334
+
335
+ ### D-TANSTACK-02
336
+ **Trigger**: Q6 = TanStack Query | **Inject into**: `{{ server_state_rules }}`
337
+
338
+ - Each domain provides a `queryKeys` object for centralized key management. Avoid scattered strings:
339
+ ```ts
340
+ export const userKeys = {
341
+ all: ['user'] as const,
342
+ profile: (id: string) => [...userKeys.all, 'profile', id] as const,
343
+ };
344
+ ```
345
+ - Custom hook wrapping: `useUserProfile(id)`, not `useQuery` directly in components.
346
+
347
+ ### D-SWR-01
348
+ **Trigger**: Q6 = SWR | **Inject into**: `{{ server_state_rules }}`
349
+
350
+ - Use SWR. Key naming convention: `/api/<resource>?<params>` or tuple `[resource, params]`.
351
+ - Global config provided at the top level via `<SWRConfig>`, including fetcher, error handling, and refresh strategy.
352
+ - Mutations use `useSWRMutation`. After write operations, call `mutate(key)` to invalidate cache.
353
+
354
+ ### D-RTKQ-01
355
+ **Trigger**: Q6 = RTK Query | **Inject into**: `{{ server_state_rules }}`
356
+
357
+ - Use RTK Query's `createApi` to centrally define endpoints. Split API slices by domain.
358
+ - `tagTypes` must be explicitly declared. Mutations use `invalidatesTags` to invalidate related queries.
359
+ - When paired with OpenAPI, use `@rtk-query/codegen-openapi` for auto-generation.
360
+
361
+ ### D-OPENAPI-01
362
+ **Trigger**: Q7 = OpenAPI auto-gen | **Inject into**: `{{ server_state_rules }}`
363
+
364
+ - API types auto-generated from backend OpenAPI/Swagger. Recommended tools: `openapi-typescript` or `orval`.
365
+ - Generated type files placed in `src/api/generated/`. **Forbid manual modification**. File header adds `// AUTO-GENERATED, DO NOT EDIT`.
366
+ - Generation command named `gen:api` in `package.json`. CI validates generated output matches the committed version.
367
+ - Business code references generated types using type-only import: `import type { User } from '@/api/generated'`.
368
+
369
+ ### D-PROTO-01
370
+ **Trigger**: Q7 = Protobuf | **Inject into**: `{{ server_state_rules }}`
371
+
372
+ - Use protobuf-ts / ts-proto to generate TypeScript types and client.
373
+ - Generated output placed in `src/api/proto/`. Forbid manual modification.
374
+
375
+ ### D-TOKEN-3L-01
376
+ **Trigger**: Q9 = Three-layer Token | **Inject into**: `{{ token_layering_rules }}`
377
+
378
+ - Tokens are divided into three layers (referencing Material Design / Tailwind design philosophy):
379
+ 1. **Primitive layer**: pure color values/numbers, no semantics attached.
380
+ - `--color-blue-500: #1a73e8`
381
+ - `--space-4: 16px`
382
+ 2. **Semantic layer**: business semantics, referencing Primitive.
383
+ - `--color-primary: var(--color-blue-500)`
384
+ - `--color-text-default: var(--color-gray-900)`
385
+ 3. **Component layer**: component-specific, referencing Semantic.
386
+ - `--button-primary-bg: var(--color-primary)`
387
+ - `--card-padding: var(--space-4)`
388
+ - Business code / component code **may only reference Semantic and Component layers**. Strictly forbid directly referencing Primitive.
389
+ - Dark mode only switches Semantic layer values. Primitive layer remains unchanged.
390
+
391
+ ### D-TOKEN-2L-01
392
+ **Trigger**: Q9 = Two-layer Token | **Inject into**: `{{ token_layering_rules }}`
393
+
394
+ - Tokens divided into two layers:
395
+ 1. **Base layer**: raw color values/spacing
396
+ 2. **Semantic layer**: business semantics, referencing Base
397
+ - Business code may only reference Semantic layer.
398
+ - Dark mode switches Semantic layer values.
399
+
400
+ ### D-TOKEN-1L-01
401
+ **Trigger**: Q9 = Single-layer Token | **Inject into**: `{{ token_layering_rules }}`
402
+
403
+ - Tokens only have a semantic layer (e.g., `--color-primary`, `--color-bg-default`). No primitive layer.
404
+ - Suitable for small projects. If dark mode or multi-theme support is added in the future, must upgrade to a two-layer structure.
405
+
406
+ ### D-DARK-CSSVAR-01
407
+ **Trigger**: Q10 = CSS variable dark | **Inject into**: `{{ dark_mode_rules }}`
408
+
409
+ - Dark mode uses CSS variable switching: `:root` defines light, `:root[data-theme="dark"]` defines dark.
410
+ - Switch via `<html data-theme="dark">` attribute. To avoid FOUC (Flash of Unstyled Content), an inline script must set it early in `<head>`.
411
+ - **Every Semantic Token must have both light and dark values**. CI validates for omissions.
412
+ - User preference persisted to `localStorage` and supports following the system setting (`prefers-color-scheme`).
413
+
414
+ ### D-DARK-CLASS-01
415
+ **Trigger**: Q10 = class-based dark | **Inject into**: `{{ dark_mode_rules }}`
416
+
417
+ - Dark mode uses class switching (Tailwind `darkMode: 'class'`). Root element toggles `.dark`.
418
+ - Components use `dark:` prefix utilities, which must appear in pairs with base classes.
419
+ - Example: `bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100`.
420
+ - Before toggling, an inline script in `<head>` reads localStorage to set the initial class to avoid FOUC.
421
+ - Supports following system theme (`prefers-color-scheme`).
422
+
423
+ ### D-DARK-NONE-01
424
+ **Trigger**: Q10 = No dark mode | **Inject into**: `{{ dark_mode_rules }}`
425
+
426
+ - Dark mode not supported at the current stage.
427
+ - But Semantic Tokens must be defined using CSS variables to reserve expansion space for future switching.
428
+ - Forbid hardcoding colors in components. All colors must go through Semantic Tokens.
429
+
430
+ ### D-RESP-MF-01
431
+ **Trigger**: Q11 = Mobile-first | **Inject into**: `{{ breakpoint_rules }}`
432
+
433
+ - Mobile-first: default styles target mobile. Use `min-width` media queries to expand upward.
434
+ - Forbid using `max-width` media queries (unless a desktop-to-mobile downgrade scenario is truly needed).
435
+ - Minimum touch target 44×44 CSS pixels.
436
+ - Must consider iOS safe areas (`env(safe-area-inset-*)`).
437
+
438
+ ### D-RESP-DF-01
439
+ **Trigger**: Q11 = Desktop-first | **Inject into**: `{{ breakpoint_rules }}`
440
+
441
+ - Desktop-first: default styles target desktop. Use `max-width` media queries to adapt downward.
442
+ - Mobile must be validated for landscape/portrait switching, keyboard popup, and scrolling scenarios.
443
+ - Documentation must declare that this project only provides a full experience at ≥ XXX width.
444
+
445
+ ### D-BP-TW-01
446
+ **Trigger**: Q12 = Tailwind defaults | **Inject into**: `{{ breakpoint_rules }}`
447
+
448
+ - Breakpoints use Tailwind defaults:
449
+ - `sm` 640px
450
+ - `md` 768px
451
+ - `lg` 1024px
452
+ - `xl` 1280px
453
+ - `2xl` 1536px
454
+ - Forbid using arbitrary non-breakpoint values in media queries.
455
+
456
+ ### D-BP-MD-01
457
+ **Trigger**: Q12 = Material Design breakpoints | **Inject into**: `{{ breakpoint_rules }}`
458
+
459
+ - Breakpoints use Material Design standards:
460
+ - `xs` 0
461
+ - `sm` 600px
462
+ - `md` 905px
463
+ - `lg` 1240px
464
+ - `xl` 1440px
465
+
466
+ ### D-I18N-01
467
+ **Trigger**: Q13 = Multi-language | **Inject into**: `{{ i18n_rules }}`
468
+
469
+ - i18n solution: React projects use `react-i18next` or `next-intl`. Vue projects use `vue-i18n`.
470
+ - **Forbid hardcoding any user-visible text in JSX/templates**. Must use i18n keys.
471
+ - Key naming convention: `<page>.<section>.<element>` three-segment format.
472
+ - Example: `login.form.submitButton`, `order.detail.priceLabel`.
473
+ - Translation files split by language: `locales/zh-CN.json`, `locales/en-US.json`.
474
+ - Plurals, dates, and currency use ICU MessageFormat. Forbid string concatenation.
475
+ - Missing keys must cause CI errors. Translation files sorted by key for easy diffing.
476
+
477
+ ### D-I18N-NONE-01
478
+ **Trigger**: Q13 = Single-language, centralized | **Inject into**: `{{ i18n_rules }}`
479
+
480
+ - The current project does not need multi-language support, but user-visible text must still be **centrally managed** in `src/i18n/<lang>.ts`.
481
+ - Forbid hardcoding text in JSX/templates to prepare for future i18n adoption.
482
+
483
+ ### D-I18N-HARDCODE-01
484
+ **Trigger**: Q13 = Single-language, hardcoding | **Inject into**: `{{ i18n_rules }}`
485
+
486
+ - User-visible text may be hardcoded. Recommend centralizing text before expanding to multi-language. No i18n infrastructure required at this stage.
487
+ - Forbid mixing hardcoded text with partial i18n keys (all-or-nothing: either fully hardcoded or fully centralized).
488
+
489
+ ### D-A11Y-AAA-01
490
+ **Trigger**: Q14 = WCAG 2.1 AAA | **Inject into**: `{{ a11y_extra_rules }}`
491
+
492
+ - On top of the fixed-rules a11y baseline, additionally require:
493
+ - Text contrast ratio ≥ 7:1 (normal text) / 4.5:1 (large text).
494
+ - Provide text alternatives (audio captions, video sign language).
495
+ - Key workflows must provide "contextual help".
496
+ - Form errors must provide specific remediation suggestions.
497
+
498
+ ### D-TEST-01 (Dynamically Assembled)
499
+ **Trigger**: Q15 + Q16 + Q17 combination | **Inject into**: `{{ test_rules }}`
500
+ **Generation rule**: Dynamically assemble based on user selection. Omit the corresponding framework line when not asked (Q16 is only asked for Q15=A/B; Q17 is only asked for Q15=A/C).
501
+
502
+ - Testing strategy: {{ Q15 description }}
503
+ - Unit test framework: {{ Q16 choice }} (only if Q15=A or B; if Vitest: supplement with "use `vitest` + `@testing-library/{{ framework }}` + `@testing-library/jest-dom`")
504
+ - E2E framework: {{ Q17 choice }} (only if Q15=A or C; if Playwright: supplement with "E2E cases in `e2e/` directory, named `*.spec.ts`, run headless in CI, `--ui` for local debugging")
505
+ - Coverage targets: shared components ≥ 80%, business components ≥ 50%, utility functions ≥ 90%.
506
+ - Unit test principles:
507
+ - Test behavior, not implementation.
508
+ - Prefer `getByRole` / `getByLabelText`, then `getByText`. Avoid `getByTestId` (unless no semantics available).
509
+ - Async assertions use `findBy*` or `waitFor`. Forbid `setTimeout`.
510
+ - E2E principles (if enabled):
511
+ - Cases cover critical business paths (login, checkout, payment, etc.).
512
+ - Data isolation. Each case has independent setup/teardown.
513
+ - Screenshot comparison only for core visual components.
514
+
515
+ ### D-AI-DEP-STRICT-01
516
+ **Trigger**: Q19 = No, forbid self-initiated | **Inject into**: `{{ ai_dependency_rule }}`
517
+
518
+ - AI **must not modify** `dependencies` / `devDependencies` in `package.json` on its own.
519
+ - If a new dependency is truly needed, AI must:
520
+ 1. List in the PR description: package name, version, purpose, alternative comparison.
521
+ 2. Await human review approval.
522
+ 3. After approval, human executes `pnpm add`.
523
+ - Exception: lockfile updates (`pnpm-lock.yaml`) may be modified by AI.
524
+
525
+ ### D-AI-DEP-ANNOUNCE-01
526
+ **Trigger**: Q19 = Yes, with declaration | **Inject into**: `{{ ai_dependency_rule }}`
527
+
528
+ - AI may introduce new dependencies, but must explicitly declare in the PR description:
529
+ - Package name / version / reason for introduction / bundle size impact.
530
+ - Avoid introducing libraries with overlapping functionality to existing dependencies (check during review).
531
+
532
+ ### D-AI-DEP-FREE-01
533
+ **Trigger**: Q19 = No limit | **Inject into**: `{{ ai_dependency_rule }}`
534
+
535
+ - AI may freely introduce dependencies, but should prioritize:
536
+ - Actively maintained (commits in the last 6 months).
537
+ - Reasonable bundle size (< 50KB gzipped preferred).
538
+ - Complete TypeScript types.
539
+
540
+ ### D-AI-BREAK-STRICT-01
541
+ **Trigger**: Q20 = Yes, must list callers | **Inject into**: `{{ ai_breaking_change_rule }}`
542
+
543
+ - Before modifying any shared component under `src/components/`, `src/hooks/`, or `src/utils/`, AI must:
544
+ 1. Full-text search for callers in the repository (grep / IDE reference finder).
545
+ 2. List **all affected files** in the change description.
546
+ 3. Assess whether existing usage is broken (API signature, behavior).
547
+ - If it is a breaking change, must:
548
+ - Provide a codemod or migration guide.
549
+ - Prefix the PR title with `[BREAKING]`.
550
+ - Bump the major version number.
551
+
552
+ ### D-AI-BREAK-LOOSE-01
553
+ **Trigger**: Q20 = No | **Inject into**: `{{ ai_breaking_change_rule }}`
554
+
555
+ - AI does not need to force-list all callers when modifying shared modules, but should describe the change intent and potential impact in the PR description.
556
+
557
+ ### D-AI-INDEX-STRICT-01
558
+ **Trigger**: Q18 = Enforce index sync | **Inject into**: `{{ ai_index_rule }}`
559
+
560
+ - After adding any new shared component, it must be added to `src/components/index.ts` in the same commit.
561
+ - Index file maintained in alphabetical order. New entries must include a JSDoc comment (one-line description of the component's purpose).
562
+ - When deleting a component, it must be removed from the index synchronously, and all callers must be searched.
563
+ - Before creating a new component, AI **must** first read `components/index.ts` to confirm there is no reusable component already.
564
+ - CI should have a script to validate: all `*.tsx` files under `components/` must appear in `index.ts` exports (orphan files treated as build failure).
565
+
566
+ ### D-AI-INDEX-LOOSE-01
567
+ **Trigger**: Q18 = Not required | **Inject into**: `{{ ai_index_rule }}`
568
+
569
+ - New components may temporarily not be written to `components/index.ts`, but it is recommended to list new component additions in the PR description.
570
+ - Before creating a new component, AI should still **search the `src/components/` directory** to avoid duplicating work.
571
+ - The team should periodically (recommended: each milestone) organize `components/index.ts` and archive stable components.
572
+
573
+ ### D-AI-LINES-01
574
+ **Trigger**: Q22 = N | **Inject into**: `{{ ai_max_lines }}`
575
+ **Generation rule**: Directly replace the placeholder with the user's chosen value (200 / 300 / 500 / no limit).
576
+
577
+ ### D-PERF-25-01
578
+ **Trigger**: Q23 = LCP < 2.5s | **Inject into**: `{{ performance_rules }}`
579
+
580
+ - First screen LCP (Largest Contentful Paint) target < 2.5s (real 4G / mid-range device).
581
+ - INP (Interaction to Next Paint) target < 200ms.
582
+ - CLS (Cumulative Layout Shift) target < 0.1.
583
+ - Critical path optimization:
584
+ - Critical CSS inlined. Non-critical CSS loaded asynchronously.
585
+ - First screen images `loading="eager"`. Others `loading="lazy"`.
586
+ - Fonts use `font-display: swap`. Preload critical fonts (`<link rel="preload">`).
587
+ - CI integrates Lighthouse validation. PRs blocked if performance regresses by > 10%.
588
+
589
+ ### D-PERF-15-01
590
+ **Trigger**: Q23 = LCP < 1.5s | **Inject into**: `{{ performance_rules }}`
591
+
592
+ - Extreme performance requirements. On top of D-PERF-25-01, additionally:
593
+ - LCP < 1.5s.
594
+ - First screen JS bundle < 100KB gzipped.
595
+ - Must use SSR / SSG pre-rendering.
596
+ - Images must use AVIF/WebP and have LQIP placeholders.
597
+ - CDN enables HTTP/3, Brotli compression.
598
+
599
+ ### D-CHUNK-300-01
600
+ **Trigger**: Q24 = chunk < 300KB | **Inject into**: `{{ performance_rules }}`
601
+
602
+ - Single chunk gzipped size < 300KB. Beyond this, must code-split (`React.lazy` / dynamic `import()`).
603
+ - Route-level lazy loading is the default requirement.
604
+ - CI integrates `rollup-plugin-visualizer` / `vite-bundle-visualizer` for bundle size analysis.
605
+ - Large dependencies (chart libraries, rich text editors, map SDKs) must be loaded on demand.
606
+
607
+ ### D-CHUNK-500-01
608
+ **Trigger**: Q24 = chunk < 500KB | **Inject into**: `{{ performance_rules }}`
609
+
610
+ - Single chunk gzipped size < 500KB.
611
+ - Route-level lazy loading recommended.
612
+ - CI integrates bundle size analysis.
613
+
614
+ ### D-BEM-01
615
+ **Trigger**: Q4 = Plain CSS + BEM | **Inject into**: `{{ style_specific_rules }}`
616
+
617
+ - Use BEM naming: Block__Element--Modifier.
618
+ - Style files co-located with components.
619
+ - Global styles in `src/styles/`.
620
+ - Forbid `!important`.
621
+ - Colors use CSS variables defined in `:root`.
622
+
623
+ ### D-FONT-SELF-01
624
+ **Trigger**: Q4b = Self-hosted, font-display: swap | **Inject into**: `{{ performance_rules }}`
625
+
626
+ - Fonts self-hosted in `src/assets/fonts/`. Use `font-display: swap`. Preload critical fonts in `<head>`. Subset to reduce file size. Formats: woff2 (primary) + woff (fallback).
627
+
628
+ ### D-FONT-CDN-01
629
+ **Trigger**: Q4b = CDN, font-display: swap | **Inject into**: `{{ performance_rules }}`
630
+
631
+ - Fonts loaded from CDN (Google Fonts or similar). Use `font-display: swap`. Preconnect to font CDN origin. Forbid blocking render on font load. Fallback font stack must match metrics closely.
632
+
633
+ ### D-FONT-SYSTEM-01
634
+ **Trigger**: Q4b = System font stack only | **Inject into**: `{{ performance_rules }}`
635
+
636
+ - System font stack only. No custom fonts loaded. Example: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`. Zero font download overhead.
637
+
638
+ ### D-FONT-UNDECIDED-01
639
+ **Trigger**: Q4b = Not decided yet | **Inject into**: `{{ performance_rules }}`
640
+
641
+ - Font strategy not yet decided. No font-specific rules injected. Revisit before production.
642
+
643
+ ### D-CUSTOM-FETCH-01
644
+ **Trigger**: Q6 = Custom fetch hook wrapper | **Inject into**: `{{ server_state_rules }}`
645
+
646
+ - Custom fetch hooks must be in `src/hooks/api/`.
647
+ - Each hook must return `{ data, error, loading, refetch }`.
648
+ - Forbid fetch calls directly in components.
649
+ - Request cancellation via AbortController on unmount.
650
+
651
+ ### D-TYPES-MANUAL-01
652
+ **Trigger**: Q7 = Hand-written .d.ts | **Inject into**: `{{ server_state_rules }}`
653
+
654
+ - API types hand-written in `src/types/api/`.
655
+ - File naming matches endpoint: `getUser.ts` -> `GetUserResponse`, `GetUserParams`.
656
+ - Changes to API types must sync with backend team.
657
+
658
+ ### D-TYPES-MONOREPO-01
659
+ **Trigger**: Q7 = Shared monorepo type package | **Inject into**: `{{ server_state_rules }}`
660
+
661
+ - Types imported from shared monorepo package `@project/shared-types`.
662
+ - Forbid local re-declaration of shared types.
663
+ - Type package version bumps must be coordinated.
664
+
665
+ ### D-RESP-DESKTOP-ONLY-01
666
+ **Trigger**: Q11 = Desktop only | **Inject into**: `{{ breakpoint_rules }}`
667
+
668
+ - Target desktop only (>=1024px).
669
+ - Forbid horizontal scroll on viewport < 1024px.
670
+ - Min touch target waived.
671
+
672
+ ### D-RESP-MOBILE-ONLY-01
673
+ **Trigger**: Q11 = Mobile only | **Inject into**: `{{ breakpoint_rules }}`
674
+
675
+ - Target mobile only (<=428px).
676
+ - Desktop shows centered mobile layout with `max-width: 428px`.
677
+
678
+ ### D-BP-CUSTOM-01
679
+ **Trigger**: Q12 = Custom breakpoints | **Inject into**: `{{ breakpoint_rules }}`
680
+
681
+ - Custom breakpoints defined in `src/styles/breakpoints.ts` as a constant object.
682
+ - All media queries reference these constants.
683
+ - Forbid hardcoded pixel values in media queries.
684
+
685
+ ### D-PERF-NONE-01
686
+ **Trigger**: Q23 = Not required yet | **Inject into**: `{{ performance_rules }}`
687
+
688
+ - Performance targets not yet defined.
689
+ - Recommend setting LCP and CLS targets before production launch.
690
+ - Current: no CI performance gates.
691
+
692
+ ### D-CHUNK-NONE-01
693
+ **Trigger**: Q24 = No limit | **Inject into**: `{{ performance_rules }}`
694
+
695
+ - No chunk size limit enforced.
696
+ - Recommend setting a limit before production to prevent accidentally large bundles.
697
+
698
+ ### D-SPA-01
699
+ **Trigger**: Q2 = Plain SPA (Vite) | **Inject into**: `{{ tech_stack_rules }}`
700
+
701
+ - Plain SPA with Vite. No SSR/SSG.
702
+ - Routing via react-router-dom or equivalent.
703
+ - All rendering client-side.
704
+
705
+ ### D-NPM-01
706
+ **Trigger**: Q3 = npm | **Inject into**: `{{ tech_stack_rules }}`
707
+
708
+ - Package manager locked to npm. Commit `package-lock.json` to the repository root. Forbid committing `yarn.lock` / `pnpm-lock.yaml`.
709
+ - `package.json` declares `"packageManager": "npm@x.y.z"`. CI validates.
710
+
711
+ ### D-YARN-01
712
+ **Trigger**: Q3 = yarn | **Inject into**: `{{ tech_stack_rules }}`
713
+
714
+ - Package manager locked to yarn. Commit `yarn.lock` to the repository root. Forbid committing `package-lock.json` / `pnpm-lock.yaml`.
715
+ - `package.json` declares `"packageManager": "yarn@x.y.z"`. CI validates.
716
+
717
+ ### D-BUN-01
718
+ **Trigger**: Q3 = bun | **Inject into**: `{{ tech_stack_rules }}`
719
+
720
+ - Package manager locked to bun. Commit `bun.lockb` to the repository root. Forbid committing `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml`.
721
+ - `package.json` declares `"packageManager": "bun@x.y.z"`. CI validates.
722
+
723
+ ### D-A11Y-AA-01
724
+ **Trigger**: Q14 = WCAG 2.1 AA | **Inject into**: `{{ a11y_extra_rules }}`
725
+
726
+ - Text contrast ratio >= 4.5:1 (normal text) / 3:1 (large text >= 18px bold or >= 24px).
727
+ - Focus indicators must be visible on all interactive elements (>= 2px outline or equivalent).
728
+ - Heading hierarchy must be correct (h1 -> h2 -> h3, no skipping levels).
729
+ - Landmark roles must be used (header, main, nav, footer).
730
+ - Form errors must be announced to screen readers via aria-live or equivalent.
731
+
732
+ ### D-A11Y-BASIC-01
733
+ **Trigger**: Q14 = Basic keyboard reachability only | **Inject into**: `{{ a11y_extra_rules }}`
734
+
735
+ - No additional WCAG rules beyond the a11y baseline. Focus on keyboard reachability and focus management only.
736
+
737
+ ### D-AI-CONFIG-FRONTEND-STRICT-01
738
+ **Trigger**: Q21 = Forbid; config files must be manually edited | **Inject into**: `{{ ai_config_rule }}`
739
+
740
+ - AI must not modify config files (`tsconfig.json`, `vite.config.*`, `eslint.config.*`, `tailwind.config.*`) on its own.
741
+ - Config changes require human review and explicit approval.
742
+ - If a config change is truly needed, AI must explain each proposed change and await approval before applying.
743
+
744
+ ### D-AI-CONFIG-FRONTEND-LOOSE-01
745
+ **Trigger**: Q21 = Allowed, must explain each change | **Inject into**: `{{ ai_config_rule }}`
746
+
747
+ - AI may modify config files, but must explain each change in the PR description.
748
+ - Each config change must include: which file, what was changed, why the change is needed, and potential impact on other developers.
749
+
750
+
751
+ ## Template Placeholder Coverage Self-Check Table (Mandatory Check Before Phase 4 Rendering)
752
+
753
+ > This table lists **all** placeholders from [frontend-rules.template.md](../templates/frontend-rules.template.md) with their fill sources. The AI must check each item before Phase 4 rendering: every placeholder must hit at least one source. Otherwise, it's treated as an alignment failure.
754
+
755
+ | Placeholder | Fill Source | Source Type |
756
+ |-------------|------------|-------------|
757
+ | `{{ project_name }}` | Phase 0 auto-read | Metadata |
758
+ | `{{ generated_at }}` | Phase 4 writes current date | Metadata |
759
+ | `{{ project_scope }}` | Phase 0 ask | Metadata |
760
+ | `{{ design_source }}` | Q8 answer | Direct fill |
761
+ | `{{ framework }}` | Q1 answer | Direct fill |
762
+ | `{{ meta_framework }}` | Q2 answer | Direct fill |
763
+ | `{{ package_manager }}` | Q3 answer | Direct fill |
764
+ | `{{ style_solution }}` | Q4 answer | Direct fill |
765
+ | `{{ font_strategy }}` | Q4b answer | Direct fill |
766
+ | `{{ state_lib }}` | Q5 answer | Direct fill |
767
+ | `{{ server_state_lib }}` | Q6 answer | Direct fill |
768
+ | `{{ api_type_source }}` | Q7 answer | Direct fill |
769
+ | `{{ token_layering }}` | Q9 answer | Direct fill |
770
+ | `{{ dark_mode }}` | Q10 answer | Direct fill |
771
+ | `{{ responsive_strategy }}` | Q11 answer | Direct fill |
772
+ | `{{ breakpoint_scheme }}` | Q12 answer | Direct fill |
773
+ | `{{ i18n }}` | Q13 answer | Direct fill |
774
+ | `{{ a11y_level }}` | Q14 answer | Direct fill |
775
+ | `{{ test_coverage }}` | Q15 answer | Direct fill |
776
+ | `{{ unit_test_framework }}` | Q16 answer | Direct fill |
777
+ | `{{ e2e_framework }}` | Q17 answer | Direct fill |
778
+ | `{{ ai_max_lines }}` | Q22 answer | Direct fill (D-AI-LINES-01) |
779
+ | `{{ lcp_target }}` | Q23 answer | Direct fill |
780
+ | `{{ bundle_size }}` | Q24 answer | Direct fill |
781
+ | `{{ token_layering_rules }}` | D-TOKEN-{1L/2L/3L}-01 | Derivation |
782
+ | `{{ dark_mode_rules }}` | D-DARK-{CSSVAR/CLASS/NONE}-01 | Derivation |
783
+ | `{{ breakpoint_rules }}` | D-RESP-{MF/DF/DESKTOP-ONLY/MOBILE-ONLY}-01 + D-BP-{TW/MD/CUSTOM}-01 | Derivation |
784
+ | `{{ tech_stack_rules }}` | D-{REACT/VUE/SVELTE/SOLID}-01 + D-{NEXT/NUXT/REMIX}-01 + D-PNPM-01 | Derivation |
785
+ | `{{ style_specific_rules }}` | D-{TAILWIND/CSSMOD/CSSINJS/UNO}-01 (+ D-REACT-02 optional) | Derivation |
786
+ | `{{ state_rules }}` | D-{ZUSTAND/PINIA/RTK/JOTAI/CTX}-01 | Derivation |
787
+ | `{{ server_state_rules }}` | D-{TANSTACK/SWR/RTKQ}-01 + D-{OPENAPI/PROTO}-01 | Derivation |
788
+ | `{{ a11y_extra_rules }}` | Q14=AA injects D-A11Y-AA-01; Q14=AAA injects D-A11Y-AAA-01; Q14=Basic keyboard reachability only injects D-A11Y-BASIC-01 | Derivation |
789
+ | `{{ i18n_rules }}` | D-I18N-01 / D-I18N-NONE-01 / D-I18N-HARDCODE-01 | Derivation |
790
+ | `{{ performance_rules }}` | D-PERF-{25/15}-01 + D-CHUNK-{300/500}-01 + D-FONT-{SELF/CDN/SYSTEM}-01 | Derivation |
791
+ | `{{ test_rules }}` | D-TEST-01 (dynamically assembled from Q15+Q16+Q17) | Derivation |
792
+ | `{{ ai_dependency_rule }}` | D-AI-DEP-{STRICT/ANNOUNCE/FREE}-01 | Derivation |
793
+ | `{{ ai_breaking_change_rule }}` | D-AI-BREAK-{STRICT/LOOSE}-01 | Derivation |
794
+ | `{{ ai_config_rule }}` | D-AI-CONFIG-FRONTEND-{STRICT/LOOSE}-01 | Derivation |
795
+ | `{{ ai_index_rule }}` | D-AI-INDEX-{STRICT/LOOSE}-01 | Derivation |
796
+ | `{{ FIXED_RULES_COMPONENT_CONTRACT }}` | fixed-rules.md F3.1 | Fixed injection |
797
+ | `{{ FIXED_RULES_I18N_BASELINE }}` | fixed-rules.md F4 | Fixed injection |
798
+ | `{{ FIXED_RULES_ERROR_HANDLING }}` | fixed-rules.md F5 | Fixed injection |
799
+ | `{{ FIXED_RULES_DENY_LIST }}` | fixed-rules.md F2 full body | Fixed injection |
800
+ | `{{ FIXED_RULES_TYPESCRIPT }}` | fixed-rules.md F1.1 full body | Fixed injection |
801
+ | `{{ FIXED_RULES_NAMING }}` | fixed-rules.md F1.3 full body | Fixed injection |
802
+ | `{{ FIXED_RULES_A11Y }}` | fixed-rules.md F6 full body | Fixed injection |
803
+ | `{{ FIXED_RULES_PERFORMANCE }}` | fixed-rules.md F10 full body | Fixed injection |
804
+ | `{{ FIXED_RULES_AI_BASE }}` | fixed-rules.md F9 full body | Fixed injection |
805
+ | `{{ FIXED_RULES_GIT }}` | fixed-rules.md F7 full body | Fixed injection |
806
+ | `{{ FIXED_RULES_SECURITY }}` | fixed-rules.md F8 full body | Fixed injection |
807
+ | `{{ deny_list_summary }}` | Phase 4 auto-extract all ❌ entries from §1.5/§3/§7.4/§9 | Auto-generated |
808
+ | `{{ recommended_libs }}` | Phase 4 recommend based on framework + style_solution + state_lib | Auto-generated |
809
+
810
+ **Self-check rule**: Before rendering, scan the output file. If any `{{ ... }}` string still remains, it's an alignment failure — must go back to Phase 2/3 to trace the issue.