@esoteric-logic/praxis-harness 2.13.0 → 2.15.0

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 (46) hide show
  1. package/base/CLAUDE.md +7 -1
  2. package/base/configs/registry.json +4 -2
  3. package/base/hooks/context7-remind.sh +81 -0
  4. package/base/hooks/settings-hooks.json +9 -0
  5. package/base/rules/coding.md +7 -0
  6. package/base/skills/px-prompt/SKILL.md +373 -0
  7. package/base/skills/px-research/SKILL.md +13 -0
  8. package/bin/praxis.js +7 -0
  9. package/bin/prompt-blocks.js +145 -0
  10. package/bin/prompt-compile.js +313 -0
  11. package/kits/web-designer/rules/web-design.md +13 -2
  12. package/lib/assemblers.js +249 -0
  13. package/lib/loader.js +148 -0
  14. package/package.json +10 -3
  15. package/prompts/blocks/behaviors/flag-confidence.md +13 -0
  16. package/prompts/blocks/behaviors/handle-uncertainty.md +13 -0
  17. package/prompts/blocks/behaviors/no-flattery.md +15 -0
  18. package/prompts/blocks/behaviors/recommend-with-reasons.md +13 -0
  19. package/prompts/blocks/behaviors/verify-before-reporting.md +13 -0
  20. package/prompts/blocks/context/mcp-servers.md +12 -0
  21. package/prompts/blocks/context/official-docs-first.md +16 -0
  22. package/prompts/blocks/context/praxis-workflow.md +20 -0
  23. package/prompts/blocks/context/vault-integration.md +13 -0
  24. package/prompts/blocks/domains/cloud-infrastructure.md +13 -0
  25. package/prompts/blocks/domains/govcon.md +13 -0
  26. package/prompts/blocks/domains/web-development.md +13 -0
  27. package/prompts/blocks/formats/concise-responses.md +13 -0
  28. package/prompts/blocks/formats/what-so-what-now-what.md +16 -0
  29. package/prompts/blocks/identity/research-partner.md +10 -0
  30. package/prompts/blocks/identity/senior-engineer.md +15 -0
  31. package/prompts/blocks/identity/solutions-architect.md +13 -0
  32. package/prompts/profiles/_base.yaml +15 -0
  33. package/prompts/profiles/federal-cloud.yaml +18 -0
  34. package/prompts/profiles/praxis.yaml +13 -0
  35. package/prompts/projects/_template/prompt-config.yaml +34 -0
  36. package/prompts/projects/maximus/prompt-config.yaml +13 -0
  37. package/prompts/projects/maximus/references/maturity-questions.md +634 -0
  38. package/prompts/projects/maximus/references/phase-maturity-matrix.md +188 -0
  39. package/prompts/projects/maximus/references/proposal-writing-standards.md +367 -0
  40. package/prompts/projects/maximus/space-instructions.md +67 -0
  41. package/prompts/projects/maximus/system-prompt.md +641 -0
  42. package/prompts/projects/praxis/CLAUDE.md +84 -0
  43. package/prompts/projects/praxis/project-instructions.md +24 -0
  44. package/prompts/projects/praxis/prompt-config.yaml +40 -0
  45. package/prompts/projects/praxis/space-instructions.md +28 -0
  46. package/scripts/lint-harness.sh +42 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const yaml = require('js-yaml');
7
+
8
+ const { BLOCKS_DIR, PROFILES_DIR, parseFrontmatter } = require('../lib/loader');
9
+
10
+ /** Collect all blocks from the blocks directory. */
11
+ function loadAllBlocks() {
12
+ const blocks = [];
13
+ if (!fs.existsSync(BLOCKS_DIR)) return blocks;
14
+
15
+ for (const category of fs.readdirSync(BLOCKS_DIR)) {
16
+ const catDir = path.join(BLOCKS_DIR, category);
17
+ if (!fs.statSync(catDir).isDirectory()) continue;
18
+
19
+ for (const file of fs.readdirSync(catDir)) {
20
+ if (!file.endsWith('.md')) continue;
21
+ const content = fs.readFileSync(path.join(catDir, file), 'utf8');
22
+ const { meta, body } = parseFrontmatter(content);
23
+ const hasCondensed = body.includes('<!-- CONDENSED -->');
24
+ blocks.push({
25
+ id: meta.id || file.replace('.md', ''),
26
+ category,
27
+ platforms: meta.platforms || [],
28
+ charEstimate: meta.char_estimate || null,
29
+ description: meta.description || '',
30
+ tags: meta.tags || [],
31
+ hasCondensed,
32
+ actualChars: body.length,
33
+ file: `prompts/blocks/${category}/${file}`,
34
+ });
35
+ }
36
+ }
37
+ return blocks;
38
+ }
39
+
40
+ /** Collect all profiles and their block references. */
41
+ function loadAllProfiles() {
42
+ const profiles = {};
43
+ if (!fs.existsSync(PROFILES_DIR)) return profiles;
44
+
45
+ for (const file of fs.readdirSync(PROFILES_DIR)) {
46
+ if (!file.endsWith('.yaml')) continue;
47
+ const name = file.replace('.yaml', '');
48
+ const content = yaml.load(fs.readFileSync(path.join(PROFILES_DIR, file), 'utf8'));
49
+ const blockIds = content.blocks
50
+ ? Object.values(content.blocks).filter(Array.isArray).flat()
51
+ : [];
52
+ profiles[name] = { ...content, blockIds };
53
+ }
54
+ return profiles;
55
+ }
56
+
57
+ // ── CLI ──────────────────────────────────────────────────────
58
+
59
+ function main() {
60
+ const args = process.argv.slice(2);
61
+
62
+ if (args.includes('--help')) {
63
+ console.log('Usage: prompt-blocks [options]');
64
+ console.log('Options:');
65
+ console.log(' --category <cat> Filter by category (identity, behaviors, domains, formats, context)');
66
+ console.log(' --profile <name> Show blocks used by a specific profile');
67
+ console.log(' --unused Show blocks not referenced by any profile');
68
+ console.log(' --tags Group output by tags');
69
+ process.exit(0);
70
+ }
71
+
72
+ const blocks = loadAllBlocks();
73
+ const profiles = loadAllProfiles();
74
+
75
+ // Resolve profile inheritance to get full block lists
76
+ for (const [name, profile] of Object.entries(profiles)) {
77
+ if (profile.extends && profiles[profile.extends]) {
78
+ const baseIds = profiles[profile.extends].blockIds || [];
79
+ profile.blockIds = [...new Set([...baseIds, ...profile.blockIds])];
80
+ }
81
+ }
82
+
83
+ // All block IDs referenced by any profile
84
+ const allProfileBlockIds = new Set();
85
+ for (const profile of Object.values(profiles)) {
86
+ for (const id of profile.blockIds) allProfileBlockIds.add(id);
87
+ }
88
+
89
+ // Filter by category
90
+ const catIdx = args.indexOf('--category');
91
+ let filtered = blocks;
92
+ if (catIdx !== -1 && args[catIdx + 1]) {
93
+ const cat = args[catIdx + 1];
94
+ filtered = blocks.filter((b) => b.category === cat);
95
+ }
96
+
97
+ // Filter by profile
98
+ const profIdx = args.indexOf('--profile');
99
+ if (profIdx !== -1 && args[profIdx + 1]) {
100
+ const profName = args[profIdx + 1];
101
+ const profile = profiles[profName];
102
+ if (!profile) {
103
+ console.error(`Profile not found: ${profName}`);
104
+ process.exit(1);
105
+ }
106
+ const profileIds = new Set(profile.blockIds);
107
+ filtered = blocks.filter((b) => profileIds.has(b.id));
108
+ }
109
+
110
+ // Unused mode
111
+ if (args.includes('--unused')) {
112
+ filtered = blocks.filter((b) => !allProfileBlockIds.has(b.id));
113
+ if (filtered.length === 0) {
114
+ console.log('All blocks are referenced by at least one profile.');
115
+ return;
116
+ }
117
+ console.log(`${filtered.length} unused block(s):\n`);
118
+ }
119
+
120
+ // Display
121
+ console.log(`${'ID'.padEnd(28)} ${'Category'.padEnd(12)} ${'Platforms'.padEnd(35)} ${'Chars'.padEnd(8)} Condensed`);
122
+ console.log('-'.repeat(95));
123
+
124
+ for (const block of filtered) {
125
+ const platforms = block.platforms.join(', ') || 'all';
126
+ const chars = block.charEstimate ? String(block.charEstimate) : '—';
127
+ const condensed = block.hasCondensed ? 'yes' : '—';
128
+ console.log(
129
+ `${block.id.padEnd(28)} ${block.category.padEnd(12)} ${platforms.padEnd(35)} ${chars.padEnd(8)} ${condensed}`
130
+ );
131
+ }
132
+
133
+ console.log(`\nTotal: ${filtered.length} blocks`);
134
+
135
+ // Show profile usage summary
136
+ if (!args.includes('--unused') && profIdx === -1) {
137
+ console.log('\nProfile usage:');
138
+ for (const [name, profile] of Object.entries(profiles)) {
139
+ if (name === '_base') continue;
140
+ console.log(` ${name}: ${profile.blockIds.length} blocks`);
141
+ }
142
+ }
143
+ }
144
+
145
+ main();
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const yaml = require('js-yaml');
7
+
8
+ const {
9
+ TARGETS,
10
+ PROMPTS_DIR,
11
+ loadPraxisConfig,
12
+ loadProfile,
13
+ mergeProfiles,
14
+ loadBlocks,
15
+ applyOverrides,
16
+ } = require('../lib/loader');
17
+
18
+ const {
19
+ interpolate,
20
+ findUnresolved,
21
+ assembleClaudeCode,
22
+ assembleClaudeProject,
23
+ assemblePerplexitySpace,
24
+ } = require('../lib/assemblers');
25
+
26
+ const PROJECTS_DIR = path.join(PROMPTS_DIR, 'projects');
27
+
28
+ const CHAR_BUDGETS = {
29
+ 'claude-code': Infinity,
30
+ 'claude-project': 2500,
31
+ 'perplexity-space': 4000,
32
+ };
33
+
34
+ // Global flags set by CLI parser
35
+ let PREVIEW_MODE = false;
36
+ let DIFF_MODE = false;
37
+ let STRICT_MODE = false;
38
+
39
+ // ── Helpers ──────────────────────────────────────────────────
40
+
41
+ function fail(msg) {
42
+ console.error(`\x1b[31mERROR:\x1b[0m ${msg}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ function warn(msg) {
47
+ console.error(`\x1b[33mWARN:\x1b[0m ${msg}`);
48
+ }
49
+
50
+ function ok(msg) {
51
+ console.log(`\x1b[32m✓\x1b[0m ${msg}`);
52
+ }
53
+
54
+ // ── Main ─────────────────────────────────────────────────────
55
+
56
+ /** Validate a standalone project — check files exist, report char budgets. */
57
+ function validateStandalone(projectName, projectDir, projectConfig) {
58
+ console.log(`\nValidating standalone: ${projectName}`);
59
+
60
+ const inventory = [
61
+ { file: 'system-prompt.md', budget: Infinity, required: true, label: 'System Prompt (Claude Projects)' },
62
+ { file: 'CLAUDE.md', budget: Infinity, required: false, label: 'Claude Code' },
63
+ { file: 'space-instructions.md', budget: CHAR_BUDGETS['perplexity-space'], required: false, label: 'Perplexity Space' },
64
+ { file: 'project-instructions.md', budget: CHAR_BUDGETS['claude-project'], required: false, label: 'Claude Project' },
65
+ ];
66
+
67
+ let missingGenerable = [];
68
+
69
+ for (const item of inventory) {
70
+ const filePath = path.join(projectDir, item.file);
71
+ if (fs.existsSync(filePath)) {
72
+ const content = fs.readFileSync(filePath, 'utf8');
73
+ const charCount = content.length;
74
+ const lineCount = content.split('\n').length;
75
+ const sizeInfo = item.budget < Infinity
76
+ ? `${charCount} chars (budget: ${item.budget})`
77
+ : `${charCount} chars, ${lineCount} lines`;
78
+
79
+ if (charCount > item.budget) {
80
+ warn(`${item.file} exceeds budget: ${charCount} chars (limit: ${item.budget})`);
81
+ } else {
82
+ ok(`${item.file} — ${sizeInfo}`);
83
+ }
84
+ } else if (item.required) {
85
+ warn(`${item.file} MISSING — standalone projects require a system prompt`);
86
+ } else {
87
+ missingGenerable.push(item.file);
88
+ }
89
+ }
90
+
91
+ if (missingGenerable.length > 0) {
92
+ console.log(`\n Missing platform outputs: ${missingGenerable.join(', ')}`);
93
+ console.log(' Run /px-prompt ' + projectName + ' to auto-generate from system-prompt.md');
94
+ }
95
+
96
+ // Version consistency check
97
+ const systemPromptPath = path.join(projectDir, 'system-prompt.md');
98
+ if (fs.existsSync(systemPromptPath) && projectConfig.version) {
99
+ const spContent = fs.readFileSync(systemPromptPath, 'utf8');
100
+ const versionMatch = spContent.match(/^version:\s*["']?([^"'\n]+)/m);
101
+ if (versionMatch && versionMatch[1].trim() !== String(projectConfig.version).trim()) {
102
+ warn(`Version mismatch: prompt-config.yaml says "${projectConfig.version}" but system-prompt.md says "${versionMatch[1].trim()}"`);
103
+ }
104
+ }
105
+
106
+ // Check for reference files
107
+ const refsDir = path.join(projectDir, 'references');
108
+ if (fs.existsSync(refsDir)) {
109
+ const refs = fs.readdirSync(refsDir).filter((f) => f.endsWith('.md'));
110
+ if (refs.length > 0) {
111
+ ok(`${refs.length} reference file(s): ${refs.join(', ')}`);
112
+ }
113
+ }
114
+ }
115
+
116
+ function compileProject(projectName, targets) {
117
+ const projectDir = path.join(PROJECTS_DIR, projectName);
118
+ const configPath = path.join(projectDir, 'prompt-config.yaml');
119
+
120
+ if (!fs.existsSync(configPath)) {
121
+ fail(`Project config not found: ${configPath}\nRun /px-prompt <project-name> to create one.`);
122
+ }
123
+
124
+ const projectConfig = yaml.load(fs.readFileSync(configPath, 'utf8'));
125
+
126
+ // Standalone mode: validate files, report budgets, skip compilation
127
+ if (projectConfig.mode === 'standalone') {
128
+ validateStandalone(projectName, projectDir, projectConfig);
129
+ return;
130
+ }
131
+
132
+ const praxisConfig = loadPraxisConfig();
133
+
134
+ // Build vars map: project vars + praxis config + project name
135
+ const vars = {
136
+ ...praxisConfig,
137
+ ...(projectConfig.vars || {}),
138
+ project: projectConfig.project || projectName,
139
+ };
140
+
141
+ // Load profile: from named profile, project-local blocks, or _base fallback
142
+ let profile;
143
+ if (projectConfig.profile) {
144
+ profile = loadProfile(projectConfig.profile, fail);
145
+ } else if (projectConfig.blocks) {
146
+ const base = loadProfile('_base', fail);
147
+ profile = mergeProfiles(base, { blocks: projectConfig.blocks });
148
+ } else {
149
+ profile = loadProfile('_base', fail);
150
+ }
151
+ profile = applyOverrides(profile, projectConfig.overrides);
152
+
153
+ const profileName = projectConfig.profile || 'project-local';
154
+ console.log(`\nCompiling: ${projectName} (profile: ${profileName})`);
155
+
156
+ const assemblers = {
157
+ 'claude-code': assembleClaudeCode,
158
+ 'claude-project': assembleClaudeProject,
159
+ 'perplexity-space': assemblePerplexitySpace,
160
+ };
161
+
162
+ const outputNames = {
163
+ 'claude-code': 'CLAUDE.md',
164
+ 'claude-project': 'project-instructions.md',
165
+ 'perplexity-space': 'space-instructions.md',
166
+ };
167
+
168
+ for (const target of targets) {
169
+ const blocks = loadBlocks(profile, target, warn);
170
+ let output = assemblers[target](blocks, projectConfig, vars);
171
+
172
+ // Interpolate variables
173
+ output = interpolate(output, vars);
174
+
175
+ // Validate no unresolved placeholders
176
+ const unresolved = findUnresolved(output);
177
+ if (unresolved.length > 0) {
178
+ if (STRICT_MODE) {
179
+ fail(`[strict] Unresolved placeholders in ${target}: ${unresolved.join(', ')}`);
180
+ }
181
+ warn(`Unresolved placeholders in ${target}: ${unresolved.join(', ')}`);
182
+ }
183
+
184
+ // Check character budget
185
+ const budget = CHAR_BUDGETS[target];
186
+ if (output.length > budget) {
187
+ if (STRICT_MODE) {
188
+ fail(`[strict] ${target} exceeds budget: ${output.length} chars (limit: ${budget})`);
189
+ }
190
+ warn(`${target} output exceeds budget: ${output.length} chars (limit: ${budget})`);
191
+ }
192
+
193
+ const outputPath = path.join(projectDir, outputNames[target]);
194
+
195
+ // Preview mode: print to stdout instead of writing
196
+ if (PREVIEW_MODE) {
197
+ console.log(`\n--- ${outputNames[target]} (${output.length} chars) ---`);
198
+ console.log(output);
199
+ continue;
200
+ }
201
+
202
+ // Diff mode: show diff against existing file before writing
203
+ if (DIFF_MODE && fs.existsSync(outputPath)) {
204
+ const existing = fs.readFileSync(outputPath, 'utf8');
205
+ if (existing === output) {
206
+ ok(`${outputNames[target]} — unchanged (${output.length} chars)`);
207
+ continue;
208
+ }
209
+ console.log(`\n--- ${outputNames[target]} changed ---`);
210
+ const existingLines = existing.split('\n');
211
+ const outputLines = output.split('\n');
212
+ const addedCount = outputLines.filter((l) => !existingLines.includes(l)).length;
213
+ const removedCount = existingLines.filter((l) => !outputLines.includes(l)).length;
214
+ console.log(` +${addedCount} lines added, -${removedCount} lines removed`);
215
+ }
216
+
217
+ fs.writeFileSync(outputPath, output, 'utf8');
218
+ ok(`${outputNames[target]} — ${output.length} chars → ${outputPath}`);
219
+ }
220
+ }
221
+
222
+ // ── CLI ──────────────────────────────────────────────────────
223
+
224
+ function main() {
225
+ const args = process.argv.slice(2);
226
+
227
+ if (args.length === 0 || args.includes('--help')) {
228
+ console.log('Usage: prompt-compile <project-name|--all> [options]');
229
+ console.log('Options:');
230
+ console.log(' --target <target> claude-code|claude-project|perplexity-space|all');
231
+ console.log(' --preview Print output to stdout without writing files');
232
+ console.log(' --diff Show what changed before writing');
233
+ console.log(' --strict Exit with error on budget overruns or unresolved vars');
234
+ console.log(' --list List all projects with mode and file status');
235
+ process.exit(0);
236
+ }
237
+
238
+ // --list mode: show all projects
239
+ if (args.includes('--list')) {
240
+ const projectDirs = fs.readdirSync(PROJECTS_DIR)
241
+ .filter((d) => d !== '_template' && fs.statSync(path.join(PROJECTS_DIR, d)).isDirectory());
242
+ if (projectDirs.length === 0) {
243
+ console.log('No projects found.');
244
+ process.exit(0);
245
+ }
246
+ console.log(`${'Project'.padEnd(20)} ${'Mode'.padEnd(12)} ${'System Prompt'.padEnd(15)} ${'Claude Proj'.padEnd(15)} ${'Perplexity'.padEnd(15)} ${'CLAUDE.md'.padEnd(12)} Refs`);
247
+ console.log('-'.repeat(95));
248
+ for (const name of projectDirs) {
249
+ const dir = path.join(PROJECTS_DIR, name);
250
+ const cfgPath = path.join(dir, 'prompt-config.yaml');
251
+ const cfg = fs.existsSync(cfgPath) ? yaml.load(fs.readFileSync(cfgPath, 'utf8')) : {};
252
+ const mode = cfg.mode || 'compiled';
253
+ const fileStatus = (f) => {
254
+ const p = path.join(dir, f);
255
+ if (!fs.existsSync(p)) return '—';
256
+ return `${fs.readFileSync(p, 'utf8').length} chars`;
257
+ };
258
+ const refsDir = path.join(dir, 'references');
259
+ const refCount = fs.existsSync(refsDir)
260
+ ? fs.readdirSync(refsDir).filter((f) => f.endsWith('.md')).length
261
+ : 0;
262
+ console.log(
263
+ `${name.padEnd(20)} ${mode.padEnd(12)} ${fileStatus('system-prompt.md').padEnd(15)} ${fileStatus('project-instructions.md').padEnd(15)} ${fileStatus('space-instructions.md').padEnd(15)} ${fileStatus('CLAUDE.md').padEnd(12)} ${refCount}`
264
+ );
265
+ }
266
+ process.exit(0);
267
+ }
268
+
269
+ // Parse global flags
270
+ PREVIEW_MODE = args.includes('--preview');
271
+ DIFF_MODE = args.includes('--diff');
272
+ STRICT_MODE = args.includes('--strict');
273
+
274
+ // Parse --target flag
275
+ const targetIdx = args.indexOf('--target');
276
+ let targets = TARGETS;
277
+ if (targetIdx !== -1 && args[targetIdx + 1]) {
278
+ const targetArg = args[targetIdx + 1];
279
+ if (TARGETS.includes(targetArg)) {
280
+ targets = [targetArg];
281
+ } else if (targetArg !== 'all') {
282
+ fail(`Unknown target: ${targetArg}. Use: ${TARGETS.join(', ')}, all`);
283
+ }
284
+ }
285
+
286
+ // Determine project(s) to compile
287
+ const flagValues = new Set();
288
+ if (targetIdx !== -1 && args[targetIdx + 1]) {
289
+ flagValues.add(args[targetIdx + 1]);
290
+ }
291
+ const projectArg = args.find((a) => !a.startsWith('--') && !flagValues.has(a));
292
+
293
+ if (args.includes('--all')) {
294
+ const projectDirs = fs.readdirSync(PROJECTS_DIR)
295
+ .filter((d) => d !== '_template' && fs.statSync(path.join(PROJECTS_DIR, d)).isDirectory());
296
+
297
+ if (projectDirs.length === 0) {
298
+ fail('No projects found in prompts/projects/');
299
+ }
300
+
301
+ for (const projectName of projectDirs) {
302
+ compileProject(projectName, targets);
303
+ }
304
+ } else if (projectArg) {
305
+ compileProject(projectArg, targets);
306
+ } else {
307
+ fail('Specify a project name or use --all');
308
+ }
309
+
310
+ console.log('\nDone.');
311
+ }
312
+
313
+ main();
@@ -9,25 +9,30 @@ paths:
9
9
  - "design-system/**"
10
10
  - "styles/**"
11
11
  ---
12
+
12
13
  # Web Design — Rules
13
- # Scope: Frontend component and design system files
14
- # Part of: web-designer AI-Kit
14
+
15
+ Scope: Frontend component and design system files.
16
+ Part of: web-designer AI-Kit.
15
17
 
16
18
  ## Invariants (BLOCK on violation)
17
19
 
18
20
  ### Design Tokens
21
+
19
22
  - No inline styles in component files — use design tokens or Tailwind utilities.
20
23
  - No hardcoded color values (`#fff`, `rgb(...)`) — must reference token variables
21
24
  (`var(--color-primary)`, Tailwind classes, or theme values).
22
25
  - Check: `grep -rn "color:" src/components/ | grep -v "var(--" | grep -v "tailwind"`
23
26
 
24
27
  ### Semantic HTML
28
+
25
29
  - No `<div>` click handlers — use `<button>` for actions, `<a>` for navigation.
26
30
  - Check: `grep -rn 'onClick.*<div' src/components/`
27
31
  - Every interactive element must have explicit keyboard handling (`onKeyDown` or
28
32
  native keyboard support via semantic elements).
29
33
 
30
34
  ### Accessibility
35
+
31
36
  - Every `<img>` must have an `alt` attribute (empty `alt=""` for decorative images).
32
37
  - Form inputs must have associated `<label>` elements or `aria-label`.
33
38
  - Color contrast must meet WCAG AA minimum (4.5:1 for text, 3:1 for large text).
@@ -35,6 +40,7 @@ paths:
35
40
  ## Conventions (WARN on violation)
36
41
 
37
42
  ### Component Structure
43
+
38
44
  - Component files follow `ComponentName/index.tsx` + `ComponentName.module.css`
39
45
  (or `ComponentName.tsx` with Tailwind — pick one pattern per project, don't mix).
40
46
  - Design tokens live in `design-system/tokens/` or equivalent.
@@ -42,21 +48,25 @@ paths:
42
48
  live in `src/components/[page-name]/`.
43
49
 
44
50
  ### Animation and Motion
51
+
45
52
  - Animation durations use token values, not magic numbers.
46
53
  - Respect `prefers-reduced-motion` — wrap animations in media query.
47
54
  - CSS transitions preferred over JS animation libraries for simple effects.
48
55
 
49
56
  ### Performance
57
+
50
58
  - Images have explicit `width` and `height` to prevent CLS (Cumulative Layout Shift).
51
59
  - Lazy-load images below the fold (`loading="lazy"`).
52
60
  - No layout-triggering CSS in animation loops (`top`, `left`, `width`, `height` — use `transform` and `opacity`).
53
61
 
54
62
  ### Design System Hygiene
63
+
55
64
  - New components must use existing tokens before creating new ones.
56
65
  - If a new token is needed: add to the token file, not inline.
57
66
  - Component variants use a consistent API pattern (props, not className overrides).
58
67
 
59
68
  ## Verification Commands
69
+
60
70
  ```bash
61
71
  # Token compliance — find hardcoded colors
62
72
  grep -rn "color:" src/components/ | grep -v "var(--" | grep -v "token" | grep -v "tailwind"
@@ -75,5 +85,6 @@ npx axe-core src/ --exit
75
85
  ```
76
86
 
77
87
  ## Removal Condition
88
+
78
89
  Remove when the project's design system is mature with >90% component coverage
79
90
  and design review is handled by a dedicated design tool or CI pipeline.