medesign 0.0.0-stage → 1.2.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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -2
  3. package/bin/medesign.js +200 -0
  4. package/integrations/claude-code/SKILL.md +20 -0
  5. package/integrations/cursor/medesign.mdc +26 -0
  6. package/medesign.config.js +46 -0
  7. package/package.json +62 -4
  8. package/presets/apple/design-tokens.css +115 -0
  9. package/presets/apple/guidelines.md +19 -0
  10. package/presets/apple/preset.json +53 -0
  11. package/presets/metha/design-tokens.css +127 -0
  12. package/presets/metha/guidelines.md +23 -0
  13. package/presets/metha/preset.json +53 -0
  14. package/presets/schema.json +102 -0
  15. package/presets/tesla/design-tokens.css +112 -0
  16. package/presets/tesla/guidelines.md +17 -0
  17. package/presets/tesla/preset.json +53 -0
  18. package/references/ai-tells.md +127 -0
  19. package/references/appendices.md +220 -0
  20. package/references/architecture.md +39 -0
  21. package/references/argon-design-archetypes.md +317 -0
  22. package/references/banner-sizes-and-styles.md +118 -0
  23. package/references/block-library.md +61 -0
  24. package/references/brand-design-tokens.md +42 -0
  25. package/references/catalog.json +108 -0
  26. package/references/cip-deliverable-guide.md +95 -0
  27. package/references/cip-design.md +121 -0
  28. package/references/cip-prompt-engineering.md +84 -0
  29. package/references/cip-style-guide.md +68 -0
  30. package/references/cli-architecture.md +146 -0
  31. package/references/components.md +240 -0
  32. package/references/dark-mode.md +23 -0
  33. package/references/design-core.md +42 -0
  34. package/references/design-md-protocol.md +75 -0
  35. package/references/design-routing.md +207 -0
  36. package/references/eng-imagery-content.md +71 -0
  37. package/references/eng-layout.md +28 -0
  38. package/references/eng-other.md +26 -0
  39. package/references/eng-states-theme.md +22 -0
  40. package/references/eng-type-color.md +44 -0
  41. package/references/icon-design.md +122 -0
  42. package/references/image-mockup-direction.md +53 -0
  43. package/references/image-to-code.md +46 -0
  44. package/references/impeccable-polish.md +85 -0
  45. package/references/intake.md +90 -0
  46. package/references/logo-color-psychology.md +101 -0
  47. package/references/logo-design.md +110 -0
  48. package/references/logo-prompt-engineering.md +158 -0
  49. package/references/logo-style-guide.md +109 -0
  50. package/references/mesurv-dna.md +104 -0
  51. package/references/micro-interactions-and-auto-animate.md +71 -0
  52. package/references/modern-craft-invariants.md +332 -0
  53. package/references/motion-tokens.md +99 -0
  54. package/references/preflight.md +110 -0
  55. package/references/proactive-motion.md +167 -0
  56. package/references/quality-gate.md +147 -0
  57. package/references/redesign-audit.md +38 -0
  58. package/references/redesign.md +52 -0
  59. package/references/slides-copywriting-formulas.md +84 -0
  60. package/references/slides-create.md +4 -0
  61. package/references/slides-html-template.md +295 -0
  62. package/references/slides-layout-patterns.md +137 -0
  63. package/references/slides-strategies.md +94 -0
  64. package/references/slides.md +42 -0
  65. package/references/social-photos-design.md +329 -0
  66. package/references/spatial-canvas-engine.md +160 -0
  67. package/references/stitch-system.md +184 -0
  68. package/references/style-extraction.md +17 -0
  69. package/references/styles/brutalist.md +88 -0
  70. package/references/styles/minimalist.md +81 -0
  71. package/references/styles/soft.md +94 -0
  72. package/references/threeui-spatial-engine.md +224 -0
  73. package/references/ui-styling-shadcn.md +336 -0
  74. package/references/ui-ux-vocabulary-vault.json +1223 -0
  75. package/references/ui-ux-vocabulary-vault.md +438 -0
  76. package/references/vocab-protocol.md +52 -0
  77. package/references/vocabulary.md +78 -0
  78. package/scripts/export-tokens.js +403 -0
  79. package/tests/browser-audit.js +442 -0
  80. package/tests/linter.js +412 -0
  81. package/tests/schemas/sarif-2.1.0.schema.json +2915 -0
  82. package/tests/visual-testbed.html +681 -0
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ME-DESIGN vNext — Verification Engine & Linter
5
+ * Static AST & Rule Verification Runner for HTML / CSS / JSX
6
+ * Usage: node tests/linter.js <filepath> [--preset <name>] [--format text|json|sarif] [--config <path>]
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const args = process.argv.slice(2);
13
+ let targetFile = null;
14
+ let presetName = 'default';
15
+ let format = 'text'; // 'text' | 'json' | 'sarif'
16
+ let configPath = null;
17
+
18
+ for (let i = 0; i < args.length; i++) {
19
+ if (args[i] === '--json') {
20
+ format = 'json';
21
+ } else if (args[i] === '--sarif') {
22
+ format = 'sarif';
23
+ } else if (args[i] === '--format' && args[i + 1]) {
24
+ format = args[++i].toLowerCase();
25
+ } else if (args[i] === '--preset' && args[i + 1]) {
26
+ presetName = args[++i];
27
+ } else if (args[i] === '--config' && args[i + 1]) {
28
+ configPath = args[++i];
29
+ } else if (!targetFile && !args[i].startsWith('--')) {
30
+ targetFile = args[i];
31
+ }
32
+ }
33
+
34
+ targetFile = targetFile || path.join(__dirname, 'visual-testbed.html');
35
+
36
+ if (!fs.existsSync(targetFile)) {
37
+ console.error(`Error: File not found: ${targetFile}`);
38
+ process.exit(2);
39
+ }
40
+
41
+ // Load medesign.config.js if available
42
+ let config = {
43
+ weights: { H: 0.30, I: 0.25, G: 0.20, R: 0.15, V: 0.10 },
44
+ minQScore: 75.0,
45
+ rules: {}
46
+ };
47
+
48
+ const candidateConfigs = [
49
+ configPath,
50
+ path.join(process.cwd(), 'medesign.config.js'),
51
+ path.join(__dirname, '..', 'medesign.config.js')
52
+ ].filter(Boolean);
53
+
54
+ for (const cfg of candidateConfigs) {
55
+ if (fs.existsSync(cfg)) {
56
+ try {
57
+ const loaded = require(path.resolve(cfg));
58
+ config = {
59
+ ...config,
60
+ ...loaded,
61
+ weights: { ...config.weights, ...(loaded.weights || {}) },
62
+ rules: { ...config.rules, ...(loaded.rules || {}) }
63
+ };
64
+ break;
65
+ } catch (e) {
66
+ // ignore config load errors
67
+ }
68
+ }
69
+ }
70
+
71
+ const content = fs.readFileSync(targetFile, 'utf8');
72
+
73
+ // Support inline suppression: /* medesign-disable-next-line RULE */ and block suppression
74
+ let preprocessedCode = content
75
+ .replace(/\/\*\s*medesign-disable-next-line\s+[A-Za-z0-9_]+\s*\*\/[\r\n]+[^\r\n]+/g, '/* medesign-suppressed */')
76
+ .replace(/\/\*\s*medesign-disable\s+[A-Za-z0-9_]+\s*\*\/[\s\S]*?\/\*\s*medesign-enable\s+[A-Za-z0-9_]+\s*\*\//g, '/* medesign-suppressed */');
77
+
78
+ // Strip remaining comments so commentary does not trigger false positives or bypasses
79
+ const cleanCode = preprocessedCode
80
+ .replace(/\/\*[\s\S]*?\*\//g, '')
81
+ .replace(/<!--[\s\S]*?-->/g, '');
82
+
83
+ const diagnostics = [];
84
+
85
+ // 1. RULE MD001: Focus Visibility (:focus-visible) - Hard Invariant
86
+ const hasFocusVisible = /:focus-visible/i.test(cleanCode) || /focus:outline/i.test(cleanCode) || /focus-visible:/i.test(cleanCode) || /outline:\s*2px/i.test(cleanCode);
87
+ if (!hasFocusVisible) {
88
+ diagnostics.push({
89
+ ruleId: 'MD001',
90
+ ruleName: 'FocusVisibility',
91
+ category: 'I',
92
+ severity: config.rules.MD001 || 'error',
93
+ hardInvariant: true,
94
+ message: 'Missing visible focus indicator (:focus-visible) for interactive controls.',
95
+ remediation: 'Define :focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }'
96
+ });
97
+ }
98
+
99
+ // 2. RULE MD002: Reduced Motion Compliance - Hard Invariant
100
+ const hasAnimations = /animation:|transition:/i.test(cleanCode) && !/transition:\s*none/i.test(cleanCode);
101
+ const hasReducedMotion = /prefers-reduced-motion/i.test(cleanCode);
102
+ if (hasAnimations && !hasReducedMotion) {
103
+ diagnostics.push({
104
+ ruleId: 'MD002',
105
+ ruleName: 'ReducedMotionCompliance',
106
+ category: 'R',
107
+ severity: config.rules.MD002 || 'error',
108
+ hardInvariant: true,
109
+ message: 'Animations/transitions detected without @media (prefers-reduced-motion) compliance gate.',
110
+ remediation: 'Add @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }'
111
+ });
112
+ }
113
+
114
+ // 3. RULE MD003: Uncontrolled Button Scale / Hit Target Crush - Hard Invariant
115
+ const hasUncontrolledScale = /active:scale-9[0-5]/i.test(cleanCode) || /:active\s*\{\s*transform:\s*scale\(0\.9[0-5]\)/i.test(cleanCode);
116
+ if (hasUncontrolledScale) {
117
+ diagnostics.push({
118
+ ruleId: 'MD003',
119
+ ruleName: 'IsolatedHitTarget',
120
+ category: 'G',
121
+ severity: config.rules.MD003 || 'error',
122
+ hardInvariant: true,
123
+ message: 'Detected raw scale(0.90-0.95) directly on interactive button container, collapsing touch hit-target.',
124
+ remediation: 'Isolate outer 44px hit-target container and scale only the inner .visual-surface using clamp(0.96, 1 - 2.5/max(W,H), 0.995).'
125
+ });
126
+ }
127
+
128
+ // 4. RULE MD004: Broken State Contract (Dead Buttons) - Hard Invariant
129
+ const hasButtons = /<button/i.test(cleanCode) || /class="[^"]*btn[^"]*"/i.test(cleanCode);
130
+ const hasInteractiveStates = /:hover/i.test(cleanCode) || /:active/i.test(cleanCode);
131
+ if (hasButtons && !hasInteractiveStates) {
132
+ diagnostics.push({
133
+ ruleId: 'MD004',
134
+ ruleName: 'InteractiveStateContract',
135
+ category: 'I',
136
+ severity: config.rules.MD004 || 'error',
137
+ hardInvariant: true,
138
+ message: 'Buttons declared without defined interactive states (:hover / :active).',
139
+ remediation: 'Provide meaningful visual feedback for hover and active/pressed states.'
140
+ });
141
+ }
142
+
143
+ // 5. RULE MD005: High Contrast Focus Ring Visibility (forced-colors: active) - Hard Invariant
144
+ // In Windows High Contrast Mode, box-shadow is completely stripped by the browser. Focus MUST use physical outline.
145
+ const focusBlocks = cleanCode.match(/(?:button|a|\.[\w-]+)?:focus(?:-visible)?\s*\{[^}]+\}/gi) || [];
146
+ let hasBoxShadowOnlyFocus = false;
147
+ for (const block of focusBlocks) {
148
+ const hasOutline = /outline\s*:\s*(?!none|0px?)([^;}]+)/i.test(block);
149
+ const hasBoxShadow = /box-shadow\s*:/i.test(block);
150
+ const stripsOutline = /outline\s*:\s*(?:none|0px?)\s*(?:;|\})/i.test(block);
151
+ if (stripsOutline || (hasBoxShadow && !hasOutline)) {
152
+ hasBoxShadowOnlyFocus = true;
153
+ break;
154
+ }
155
+ }
156
+ if (hasBoxShadowOnlyFocus) {
157
+ diagnostics.push({
158
+ ruleId: 'MD005',
159
+ ruleName: 'HighContrastFocusRing',
160
+ category: 'I',
161
+ severity: config.rules.MD005 || 'error',
162
+ hardInvariant: true,
163
+ message: 'Focus indicator relies on box-shadow or suppresses outline without replacement. In Windows High Contrast (forced-colors: active), box-shadow is stripped, causing total focus ring loss.',
164
+ remediation: 'Always specify a physical outline in focus styles: :focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }'
165
+ });
166
+ }
167
+
168
+ // 6. RULE MD101: Dense Grid on Data Dashboards - Soft Heuristic (Warning)
169
+ const hasDenseGrid = /grid-auto-flow:\s*dense/i.test(cleanCode);
170
+ if (hasDenseGrid) {
171
+ diagnostics.push({
172
+ ruleId: 'MD101',
173
+ ruleName: 'DenseGridTabSequence',
174
+ category: 'G',
175
+ severity: config.rules.MD101 || 'warning',
176
+ hardInvariant: false,
177
+ message: 'grid-auto-flow: dense may scramble DOM order away from keyboard tab order.',
178
+ remediation: 'Use grid-auto-flow: row with Named Grid Areas (grid-template-areas) for predictable data layouts.'
179
+ });
180
+ }
181
+
182
+ // 7. RULE MD102: Color Gamut Interpolation Space - Soft Heuristic (Warning)
183
+ const hasGradient = /linear-gradient\(/i.test(cleanCode);
184
+ const hasOklchOklab = /linear-gradient\(\s*in\s+(oklch|oklab)/i.test(cleanCode);
185
+ if (hasGradient && !hasOklchOklab) {
186
+ diagnostics.push({
187
+ ruleId: 'MD102',
188
+ ruleName: 'PerceptualColorInterpolation',
189
+ category: 'V',
190
+ severity: config.rules.MD102 || 'warning',
191
+ hardInvariant: false,
192
+ message: 'Gradients detected without modern perceptual interpolation space (in oklch / in oklab).',
193
+ remediation: 'Use linear-gradient(in oklch shorter hue ...) or linear-gradient(in oklab ...) to prevent middle-gray mud.'
194
+ });
195
+ }
196
+
197
+ // 8. RULE MD103: Concentric Radius Math - Soft Heuristic (Info)
198
+ const hasOuterRadius = /border-radius:\s*\d+px/i.test(cleanCode);
199
+ const hasInnerConcentric = /max\(\s*0px/i.test(cleanCode) || /calc\(infinity/i.test(cleanCode) || /--corner-r/i.test(cleanCode);
200
+ if (hasOuterRadius && !hasInnerConcentric) {
201
+ diagnostics.push({
202
+ ruleId: 'MD103',
203
+ ruleName: 'ConcentricRadiusMath',
204
+ category: 'G',
205
+ severity: config.rules.MD103 || 'info',
206
+ hardInvariant: false,
207
+ message: 'Consider applying Safe Concentric Radius formula max(0px, r_outer - border_w - pad) for nested cards.',
208
+ remediation: 'Ensure inner containers compute radius proportionally to avoid clipped corners.'
209
+ });
210
+ }
211
+
212
+ // 9. RULE MD104: Mobile Viewport Dynamic Units - Soft Heuristic (Warning)
213
+ const hasRaw100vh = /(?:height|min-height|max-height)\s*:\s*100vh/i.test(cleanCode);
214
+ const hasDvhSvh = /100[ds]vh/i.test(cleanCode);
215
+ if (hasRaw100vh && !hasDvhSvh) {
216
+ diagnostics.push({
217
+ ruleId: 'MD104',
218
+ ruleName: 'MobileViewportStability',
219
+ category: 'R',
220
+ severity: config.rules.MD104 || 'warning',
221
+ hardInvariant: false,
222
+ message: '100vh viewport unit used without dynamic viewport fallback (100dvh/100svh). On mobile browsers, toolbar expansion causes vertical jumping.',
223
+ remediation: 'Provide dynamic viewport unit: min-height: 100vh; min-height: 100dvh;'
224
+ });
225
+ }
226
+
227
+ // 10. RULE MD105: Visual/DOM Order Scrambling - Soft Heuristic (Warning)
228
+ const hasOrderReverse = /flex-direction\s*:\s*(row|column)-reverse/i.test(cleanCode) || /\border\s*:\s*(-?[1-9]\d*)/i.test(cleanCode);
229
+ if (hasOrderReverse) {
230
+ diagnostics.push({
231
+ ruleId: 'MD105',
232
+ ruleName: 'VisualDomSequenceScramble',
233
+ category: 'G',
234
+ severity: config.rules.MD105 || 'warning',
235
+ hardInvariant: false,
236
+ message: 'Reverse flex-direction or CSS order detected. Reordering visual sequence without updating DOM order violates WCAG 1.3.2 and disorients keyboard users.',
237
+ remediation: 'Reorder elements in HTML/DOM source directly rather than relying on reverse layout or CSS order.'
238
+ });
239
+ }
240
+
241
+ // 11. RULE MD201: Editorial Style: Em-Dash Punctuation - Soft Heuristic (Info)
242
+ const visibleTextMatch = content.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '');
243
+ const hasEmDash = /[—–]/.test(visibleTextMatch);
244
+ if (hasEmDash) {
245
+ diagnostics.push({
246
+ ruleId: 'MD201',
247
+ ruleName: 'EditorialPunctuationHyphen',
248
+ category: 'V',
249
+ severity: config.rules.MD201 || 'info',
250
+ hardInvariant: false,
251
+ message: 'Detected em-dash (—) or en-dash (–) in rendered copy.',
252
+ remediation: 'Consider replacing with hyphen (-) or colon for concise UI copywriting.'
253
+ });
254
+ }
255
+
256
+ // Calculate Scores
257
+ const hardFailures = diagnostics.filter(d => d.hardInvariant && d.severity === 'error');
258
+ const errors = diagnostics.filter(d => d.severity === 'error');
259
+ const warnings = diagnostics.filter(d => d.severity === 'warning');
260
+ const infos = diagnostics.filter(d => d.severity === 'info');
261
+
262
+ // Base 100 with category deductions
263
+ let H = 95.0;
264
+ let I = 95.0 - (diagnostics.filter(d => d.category === 'I').length * 15);
265
+ let G = 95.0 - (diagnostics.filter(d => d.category === 'G').length * 10);
266
+ let R = 95.0 - (diagnostics.filter(d => d.category === 'R').length * 15);
267
+ let V = 95.0 - (diagnostics.filter(d => d.category === 'V').length * 5);
268
+
269
+ H = Math.max(0, Math.min(100, H));
270
+ I = Math.max(0, Math.min(100, I));
271
+ G = Math.max(0, Math.min(100, G));
272
+ R = Math.max(0, Math.min(100, R));
273
+ V = Math.max(0, Math.min(100, V));
274
+
275
+ const w = config.weights;
276
+ const qScore = Math.round((w.H * H + w.I * I + w.G * G + w.R * R + w.V * V) * 10) / 10;
277
+ const isPassed = hardFailures.length === 0 && qScore >= config.minQScore;
278
+
279
+ // Helper: SARIF Level
280
+ function toSarifLevel(severity) {
281
+ if (severity === 'error') return 'error';
282
+ if (severity === 'warning') return 'warning';
283
+ return 'note';
284
+ }
285
+
286
+ // 1. Output: SARIF v2.1.0
287
+ if (format === 'sarif') {
288
+ const allRules = [
289
+ { id: 'MD001', name: 'FocusVisibility', desc: 'Keyboard focus ring visibility (:focus-visible)', level: 'error' },
290
+ { id: 'MD002', name: 'ReducedMotionCompliance', desc: 'Reduced motion compliance (@media prefers-reduced-motion)', level: 'error' },
291
+ { id: 'MD003', name: 'IsolatedHitTarget', desc: 'Isolated hit-target area (No raw scale-95 crushing)', level: 'error' },
292
+ { id: 'MD004', name: 'InteractiveStateContract', desc: 'Interactive state contract (No dead buttons)', level: 'error' },
293
+ { id: 'MD005', name: 'HighContrastFocusRing', desc: 'High contrast focus ring visibility (forced-colors: active)', level: 'error' },
294
+ { id: 'MD101', name: 'DenseGridTabSequence', desc: 'Dense grid tab sequence consistency', level: 'warning' },
295
+ { id: 'MD102', name: 'PerceptualColorInterpolation', desc: 'Perceptual color interpolation space (oklch/oklab)', level: 'warning' },
296
+ { id: 'MD103', name: 'ConcentricRadiusMath', desc: 'Concentric radius geometry', level: 'note' },
297
+ { id: 'MD104', name: 'MobileViewportStability', desc: 'Mobile viewport dynamic unit stability (100dvh)', level: 'warning' },
298
+ { id: 'MD105', name: 'VisualDomSequenceScramble', desc: 'Visual/DOM sequence synchronization (order / flex reverse)', level: 'warning' },
299
+ { id: 'MD201', name: 'EditorialPunctuationHyphen', desc: 'Editorial style punctuation (hyphen vs em-dash)', level: 'note' }
300
+ ];
301
+
302
+ const uri = path.relative(process.cwd(), targetFile).replace(/\\/g, '/');
303
+
304
+ const sarifLog = {
305
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
306
+ version: '2.1.0',
307
+ runs: [
308
+ {
309
+ tool: {
310
+ driver: {
311
+ name: 'medesign',
312
+ version: '1.2.0',
313
+ informationUri: 'https://github.com/metha-design/medesign',
314
+ rules: allRules.map(r => ({
315
+ id: r.id,
316
+ name: r.name,
317
+ shortDescription: { text: r.desc },
318
+ defaultConfiguration: { level: r.level }
319
+ }))
320
+ }
321
+ },
322
+ results: diagnostics.map(d => ({
323
+ ruleId: d.ruleId,
324
+ level: toSarifLevel(d.severity),
325
+ message: { text: d.message },
326
+ locations: [
327
+ {
328
+ physicalLocation: {
329
+ artifactLocation: {
330
+ uri: uri,
331
+ uriBaseId: '%SRCROOT%'
332
+ }
333
+ }
334
+ }
335
+ ]
336
+ }))
337
+ }
338
+ ]
339
+ };
340
+
341
+ console.log(JSON.stringify(sarifLog, null, 2));
342
+ process.exit(isPassed ? 0 : 1);
343
+ }
344
+
345
+ // 2. Output: JSON
346
+ if (format === 'json') {
347
+ console.log(JSON.stringify({
348
+ file: targetFile,
349
+ preset: presetName,
350
+ qScore,
351
+ passed: isPassed,
352
+ scores: { H, I, G, R, V },
353
+ hardFailures,
354
+ diagnostics
355
+ }, null, 2));
356
+ process.exit(isPassed ? 0 : 1);
357
+ }
358
+
359
+ // 3. Output: Terminal Summary Card
360
+ const ANSI_RESET = '\x1b[0m';
361
+ const ANSI_BOLD = '\x1b[1m';
362
+ const ANSI_CYAN = '\x1b[36m';
363
+ const ANSI_GREEN = '\x1b[32m';
364
+ const ANSI_YELLOW = '\x1b[33m';
365
+ const ANSI_RED = '\x1b[31m';
366
+ const ANSI_GRAY = '\x1b[90m';
367
+
368
+ console.log(`\n${ANSI_BOLD}ME-DESIGN vNext${ANSI_RESET}`);
369
+ console.log(`${ANSI_CYAN}Design Quality & Governance Audit${ANSI_RESET}`);
370
+ console.log(`${ANSI_GRAY}────────────────────────────────────────${ANSI_RESET}`);
371
+ console.log(`Target ${path.basename(targetFile)}`);
372
+ console.log(`Preset ${presetName.toUpperCase()}`);
373
+ console.log(`Analysis Static AST & Semantic Rules`);
374
+ console.log(`Ruleset medesign-standard@1.2.0\n`);
375
+
376
+ console.log(`${ANSI_BOLD}QUALITY SCORE (Q = ${w.H}H + ${w.I}I + ${w.G}G + ${w.R}R + ${w.V}V)${ANSI_RESET}`);
377
+ console.log(` Hierarchy (H) ${H.toFixed(1)} / 100`);
378
+ console.log(` Information (I) ${I.toFixed(1)} / 100`);
379
+ console.log(` Geometry (G) ${G.toFixed(1)} / 100`);
380
+ console.log(` Responsive (R) ${R.toFixed(1)} / 100`);
381
+ console.log(` Visual Craft (V) ${V.toFixed(1)} / 100\n`);
382
+
383
+ const qScoreColor = isPassed ? ANSI_GREEN : ANSI_RED;
384
+ console.log(` ${ANSI_BOLD}Q-SCORE ${qScoreColor}${qScore.toFixed(1)} / 100${ANSI_RESET} [${isPassed ? 'PASS' : 'FAIL'}]\n`);
385
+
386
+ console.log(`${ANSI_BOLD}HARD INVARIANTS CHECKLIST${ANSI_RESET}`);
387
+ console.log(` ${hasFocusVisible ? ANSI_GREEN + 'PASS' : ANSI_RED + 'FAIL'}${ANSI_RESET} MD001: Keyboard Focus Ring Visibility (:focus-visible)`);
388
+ console.log(` ${!hasAnimations || hasReducedMotion ? ANSI_GREEN + 'PASS' : ANSI_RED + 'FAIL'}${ANSI_RESET} MD002: Reduced Motion Compliance (@media prefers-reduced-motion)`);
389
+ console.log(` ${!hasUncontrolledScale ? ANSI_GREEN + 'PASS' : ANSI_RED + 'FAIL'}${ANSI_RESET} MD003: Isolated Hit-Target Area (No raw scale-95 crushing)`);
390
+ console.log(` ${!hasButtons || hasInteractiveStates ? ANSI_GREEN + 'PASS' : ANSI_RED + 'FAIL'}${ANSI_RESET} MD004: Interactive State Contract (No dead buttons)`);
391
+ console.log(` ${!hasBoxShadowOnlyFocus ? ANSI_GREEN + 'PASS' : ANSI_RED + 'FAIL'}${ANSI_RESET} MD005: High Contrast Focus Visibility (forced-colors: active outline)\n`);
392
+
393
+ if (diagnostics.length > 0) {
394
+ console.log(`${ANSI_BOLD}FINDINGS (${diagnostics.length})${ANSI_RESET}`);
395
+ diagnostics.forEach(d => {
396
+ const sevColor = d.severity === 'error' ? ANSI_RED : (d.severity === 'warning' ? ANSI_YELLOW : ANSI_CYAN);
397
+ console.log(` ${sevColor}${d.severity.padEnd(7)}${ANSI_RESET} ${d.ruleId} ${d.message}`);
398
+ if (d.remediation) {
399
+ console.log(` ${ANSI_GRAY}-> Remediation: ${d.remediation}${ANSI_RESET}`);
400
+ }
401
+ });
402
+ console.log('');
403
+ }
404
+
405
+ console.log(`${ANSI_GRAY}────────────────────────────────────────${ANSI_RESET}`);
406
+ if (isPassed) {
407
+ console.log(`${ANSI_GREEN}${ANSI_BOLD}RESULT: PASS (Exit Code: 0)${ANSI_RESET}`);
408
+ process.exit(0);
409
+ } else {
410
+ console.log(`${ANSI_RED}${ANSI_BOLD}RESULT: FAIL (${hardFailures.length} hard invariant failures, Exit Code: 1)${ANSI_RESET}`);
411
+ process.exit(1);
412
+ }