@rafters/shared 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ezmode Games
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @rafters/shared
2
+
3
+ > The data model the rest of Rafters is built on: W3C DTCG token schemas, base types, and the utilities that read a component's own intelligence.
4
+
5
+ Everything in Rafters that describes a token, a color, or a component agrees on
6
+ one set of shapes. This package holds them. It is Zod-first, so every schema is
7
+ both a runtime validator and a TypeScript type. Parse external data through the
8
+ schema and you get a typed value or a clear error, never an unchecked `any`.
9
+
10
+ ## Install
11
+
12
+ Published as TypeScript source; consumers need a bundler or tsx/vite. Plain
13
+ `node` cannot import it.
14
+
15
+ ```bash
16
+ pnpm add @rafters/shared
17
+ ```
18
+
19
+ Import from another package:
20
+
21
+ ```ts
22
+ import {
23
+ OKLCHSchema,
24
+ DTCGColorTokenSchema,
25
+ oklchToDTCG,
26
+ parseJSDocIntelligence,
27
+ parseFillSignature,
28
+ ImportPendingSchema,
29
+ RAFTERS_VERSION,
30
+ } from '@rafters/shared';
31
+ ```
32
+
33
+ Types live at the same entry, plus a narrow `./types` subpath for the schema
34
+ layer on its own:
35
+
36
+ ```ts
37
+ import type { OKLCH, DTCGColorToken, ComponentIntelligence } from '@rafters/shared/types';
38
+ ```
39
+
40
+ ## What's inside
41
+
42
+ ### Token schemas (`types.ts`)
43
+
44
+ The W3C Design Token Community Group spec, expressed as Zod schemas Rafters can
45
+ validate against.
46
+
47
+ - `OKLCHSchema` / `OKLCH` — the color primitive. Every color in the system is
48
+ OKLCH before it is anything else.
49
+ - `DTCGTokenBaseSchema`, `DTCGColorTokenSchema`, `DTCGGroupSchema`,
50
+ `DTCGFileSchema` — a token, a color token, a group, and a whole `.tokens.json`
51
+ file, following the DTCG shape.
52
+ - `oklchToDTCG(oklch)` / `dtcgToOKLCH(dtcg)` — cross the line between the color
53
+ primitive and the on-disk token value.
54
+ - `ComponentIntelligenceSchema`, `ColorIntelligenceSchema`, `IntelligenceSchema`
55
+ and friends — the shapes the MCP and Studio read when they describe a
56
+ component or a color.
57
+ - `COMPUTED` — a sentinel symbol. Set a token to it and the system reverts that
58
+ token to its computed value instead of a hardcoded override.
59
+
60
+ ### Component intelligence (`component-intelligence.ts`)
61
+
62
+ A component carries its own design guidance in a JSDoc block. This module reads
63
+ it back out.
64
+
65
+ - `parseJSDocIntelligence(source)` — pull the six-tag intelligence block out of a
66
+ component's source into a structured object.
67
+ - `extractVariants`, `extractSizes`, `extractDependencies`,
68
+ `extractPrimitiveDependencies`, `extractJSDocDependencies` — targeted reads for
69
+ one facet at a time.
70
+ - `findComponentCategory(registry, name)` — look up a category by name in a
71
+ registry. `DEFAULT_COMPONENT_CATEGORIES` is the registry Rafters ships.
72
+ - `validateComponentIntelligence(...)` — check a parsed block and return warnings
73
+ for what's missing or malformed.
74
+ - `toDisplayName(name)` — turn a component id into its display name.
75
+
76
+ ### Fill signatures (`fill-signature.ts`)
77
+
78
+ `fill=` is a compact color vocabulary, not a styling grammar. `fill="muted/50"`
79
+ is the muted color at 50% alpha; `fill="barbie-pink-to-ken-brown"` is a two-stop
80
+ gradient. `to`, `via`, and `from` are reserved segments.
81
+
82
+ - `parseFillSignature(input)` — parse a fill string into stops and alpha.
83
+ - `validateFillSignature(signature, hasWord)` — check every stop word against a
84
+ vocabulary predicate; returns the first unresolvable word, or `null`.
85
+ - `expandFillSignature(signature, context, hasWord?)` — expand a signature into
86
+ the classes it sets, for the given context (`surface` or `text`).
87
+ - `foregroundWordFor(...)` — pick the readable foreground word for a surface fill.
88
+
89
+ ### Import pending (`import-pending.ts`)
90
+
91
+ The schema for tokens found during `rafters init` detection and left awaiting
92
+ review. Written to `.rafters/import-pending.json`, read by the CLI prompts and
93
+ the Studio review UI.
94
+
95
+ - `ImportPendingSchema` and its parts (`PendingTokenSchema`,
96
+ `PendingPaletteSchema`, `ImportOriginalSchema`, `ImportModificationsSchema`).
97
+ - `ImportDecisionSchema` — `pending`, `accepted`, `rejected`, or `modified`.
98
+
99
+ Each pending entry keeps the original source variable, the proposed Rafters
100
+ token, the user's decision, and the confidence and rationale behind the mapping.
101
+
102
+ ### Version (`version.ts`)
103
+
104
+ - `RAFTERS_VERSION` — the lockstep suite version, written by
105
+ `packages/cli/scripts/sync-version.ts` from the CLI's own version at build
106
+ time. Read by the API root endpoint and anywhere else that reports it.
107
+
108
+ ## Testing
109
+
110
+ ```bash
111
+ pnpm --filter @rafters/shared test
112
+ ```
113
+
114
+ ## Notes
115
+
116
+ - Everything external is parsed through a schema. That is the rule the whole
117
+ design system leans on, so this package is where it starts.
118
+ - Schemas and their inferred types share a name (`OKLCHSchema` validates,
119
+ `OKLCH` is the type). Import the schema to check data, the type to annotate it.
120
+ - Published as TypeScript source. It gains nothing from a build step, but a
121
+ consumer needs a bundler or tsx/vite to import it — plain `node` cannot.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@rafters/shared",
3
+ "version": "0.2.3",
4
+ "description": "Shared W3C DTCG schemas, base types, and utilities for Rafters design system",
5
+ "homepage": "https://rafters.studio",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/rafters-studio/rafters.git",
10
+ "directory": "packages/shared"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "LICENSE"
15
+ ],
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": "./src/index.ts",
20
+ "./types": "./src/types.ts"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "test": "vitest run",
27
+ "test:unit": "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:coverage": "vitest run --coverage",
30
+ "typecheck": "tsc --noEmit",
31
+ "clean": "rm -rf .turbo coverage"
32
+ },
33
+ "dependencies": {
34
+ "comment-parser": "catalog:",
35
+ "zod": "catalog:"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "catalog:",
39
+ "typescript": "catalog:",
40
+ "vitest": "catalog:",
41
+ "zocker": "catalog:"
42
+ }
43
+ }
@@ -0,0 +1,477 @@
1
+ /**
2
+ * Component Intelligence
3
+ * Types and utilities for parsing component intelligence metadata from JSDoc comments
4
+ *
5
+ * Intelligence metadata captures design decisions and cognitive load information
6
+ * embedded in component source files via JSDoc tags.
7
+ *
8
+ * ---- Canonical JSDoc Intelligence Format ----
9
+ *
10
+ * All Rafters components use 6 intelligence tags in their JSDoc block.
11
+ * The canonical format for usage patterns is DO:/NEVER: lines under @usage-patterns.
12
+ *
13
+ * Example (from button.tsx):
14
+ *
15
+ * /**
16
+ * * Interactive button component for user actions
17
+ * *
18
+ * * @cognitive-load 3/10 - Simple action trigger with clear visual hierarchy
19
+ * * @attention-economics Size hierarchy: sm=tertiary, default=secondary, lg=primary
20
+ * * @trust-building Destructive actions require confirmation patterns
21
+ * * @accessibility WCAG AAA compliant with 44px minimum touch targets
22
+ * * @semantic-meaning Variant mapping: default=main, destructive=irreversible
23
+ * *
24
+ * * @usage-patterns
25
+ * * DO: Primary: Main user goal, maximum 1 per section
26
+ * * DO: Secondary: Alternative paths, supporting actions
27
+ * * NEVER: Multiple primary buttons competing for attention
28
+ * * /
29
+ *
30
+ * Parsing paths:
31
+ * 1. Canonical: DO:/NEVER: lines within the @usage-patterns tag value
32
+ * 2. Legacy: Separate @do and @never tags (still supported)
33
+ *
34
+ * Both paths populate the same usagePatterns.dos[] and usagePatterns.nevers[] arrays.
35
+ * If both formats appear in the same block, results are merged (no deduplication).
36
+ *
37
+ * Required intelligence tags (all 6 should be present on every component):
38
+ * - @cognitive-load (number/10 format, e.g. "3/10 - Description")
39
+ * - @attention-economics (free text)
40
+ * - @trust-building (free text)
41
+ * - @accessibility (free text)
42
+ * - @semantic-meaning (free text)
43
+ * - @usage-patterns (with DO:/NEVER: lines)
44
+ */
45
+
46
+ import { parse, type Spec } from 'comment-parser';
47
+ import { z } from 'zod';
48
+
49
+ // ==================== Types ====================
50
+
51
+ /**
52
+ * Intelligence metadata extracted from component JSDoc comments
53
+ * Named JSDocIntelligence to avoid conflict with ComponentIntelligence in types.ts
54
+ */
55
+ export interface JSDocIntelligence {
56
+ /** Cognitive load score (0-10 scale) */
57
+ cognitiveLoad?: number;
58
+ /** Attention economics guidance */
59
+ attentionEconomics?: string;
60
+ /** Accessibility requirements and guidance */
61
+ accessibility?: string;
62
+ /** Trust-building patterns */
63
+ trustBuilding?: string;
64
+ /** Semantic meaning and purpose */
65
+ semanticMeaning?: string;
66
+ /** Usage patterns with dos and nevers */
67
+ usagePatterns?: {
68
+ dos: string[];
69
+ nevers: string[];
70
+ };
71
+ }
72
+
73
+ /**
74
+ * A component category is a label + description. The library ships a default
75
+ * registry covering the rafters component taxonomy; consumers may use, extend,
76
+ * or replace it. Built-in and user-defined categories are structurally identical.
77
+ */
78
+ export const ComponentCategorySchema = z.object({
79
+ name: z.string(),
80
+ description: z.string().optional(),
81
+ });
82
+ export type ComponentCategory = z.infer<typeof ComponentCategorySchema>;
83
+
84
+ export const DEFAULT_COMPONENT_CATEGORIES: ComponentCategory[] = [
85
+ { name: 'layout', description: 'Components for structuring and organizing content' },
86
+ { name: 'form', description: 'Input controls and form elements' },
87
+ { name: 'feedback', description: 'Status indicators and user feedback' },
88
+ { name: 'navigation', description: 'Wayfinding and navigation components' },
89
+ { name: 'overlay', description: 'Modals, dialogs, and overlay components' },
90
+ { name: 'data-display', description: 'Components for displaying data and content' },
91
+ { name: 'utility', description: 'Helper and utility components' },
92
+ ];
93
+
94
+ /** Look up a category by name in a registry. Returns undefined if not found. */
95
+ export const findComponentCategory = (
96
+ registry: readonly ComponentCategory[],
97
+ name: string,
98
+ ): ComponentCategory | undefined => registry.find((c) => c.name === name);
99
+
100
+ /**
101
+ * Structured dependency information extracted from JSDoc tags
102
+ */
103
+ export interface JSDocDependencies {
104
+ /** Runtime dependencies from @dependencies tag */
105
+ runtime: string[];
106
+ /** Dev dependencies from @devDependencies tag */
107
+ dev: string[];
108
+ /** Internal workspace dependencies from @internal-dependencies tag */
109
+ internal: string[];
110
+ }
111
+
112
+ /**
113
+ * Full component metadata including intelligence
114
+ */
115
+ export interface ComponentMetadata {
116
+ /** Component file name (without extension) */
117
+ name: string;
118
+ /** Human-readable display name */
119
+ displayName: string;
120
+ /** Brief description from JSDoc */
121
+ description?: string;
122
+ /** Category name -- reference into a ComponentCategory registry */
123
+ category: string;
124
+ /** Intelligence metadata */
125
+ intelligence?: JSDocIntelligence;
126
+ /** Available variants */
127
+ variants: string[];
128
+ /** Available sizes */
129
+ sizes: string[];
130
+ /** External dependencies */
131
+ dependencies: string[];
132
+ /** Primitive dependencies */
133
+ primitives: string[];
134
+ /** Structured dependency information from JSDoc tags */
135
+ jsDocDependencies?: JSDocDependencies;
136
+ /** Relative file path */
137
+ filePath: string;
138
+ }
139
+
140
+ // ==================== Parser ====================
141
+
142
+ /**
143
+ * Parse JSDoc comments from source to extract intelligence metadata
144
+ */
145
+ export function parseJSDocIntelligence(source: string): JSDocIntelligence | undefined {
146
+ const blocks = parse(source);
147
+ if (blocks.length === 0) return undefined;
148
+
149
+ const intelligence: JSDocIntelligence = {};
150
+ let hasAnyField = false;
151
+
152
+ // Process all JSDoc blocks
153
+ for (const block of blocks) {
154
+ for (const tag of block.tags) {
155
+ const tagName = tag.tag.toLowerCase();
156
+ const value = getTagValue(tag);
157
+
158
+ switch (tagName) {
159
+ case 'cognitive-load':
160
+ case 'cognitiveload': {
161
+ // Parse format: "6/10 - Description" or just "6"
162
+ const match = value.match(/^(\d+)(?:\/10)?/);
163
+ if (match?.[1]) {
164
+ const num = Number.parseInt(match[1], 10);
165
+ if (!Number.isNaN(num) && num >= 0 && num <= 10) {
166
+ intelligence.cognitiveLoad = num;
167
+ hasAnyField = true;
168
+ }
169
+ }
170
+ break;
171
+ }
172
+ case 'attention-economics':
173
+ case 'attentioneconomics':
174
+ intelligence.attentionEconomics = value;
175
+ hasAnyField = true;
176
+ break;
177
+ case 'accessibility':
178
+ intelligence.accessibility = value;
179
+ hasAnyField = true;
180
+ break;
181
+ case 'trust-building':
182
+ case 'trustbuilding':
183
+ intelligence.trustBuilding = value;
184
+ hasAnyField = true;
185
+ break;
186
+ case 'semantic-meaning':
187
+ case 'semanticmeaning':
188
+ intelligence.semanticMeaning = value;
189
+ hasAnyField = true;
190
+ break;
191
+ case 'usage-patterns':
192
+ case 'usagepatterns':
193
+ // Parse DO:/NEVER: patterns from value
194
+ parseUsagePatternsInline(value, intelligence);
195
+ hasAnyField = true;
196
+ break;
197
+ case 'do':
198
+ if (!intelligence.usagePatterns) {
199
+ intelligence.usagePatterns = { dos: [], nevers: [] };
200
+ }
201
+ intelligence.usagePatterns.dos.push(value);
202
+ hasAnyField = true;
203
+ break;
204
+ case 'never':
205
+ if (!intelligence.usagePatterns) {
206
+ intelligence.usagePatterns = { dos: [], nevers: [] };
207
+ }
208
+ intelligence.usagePatterns.nevers.push(value);
209
+ hasAnyField = true;
210
+ break;
211
+ }
212
+ }
213
+ }
214
+
215
+ return hasAnyField ? intelligence : undefined;
216
+ }
217
+
218
+ /**
219
+ * Parse usage patterns from inline format
220
+ * Format: "DO: foo NEVER: bar DO: baz"
221
+ */
222
+ function parseUsagePatternsInline(value: string, intelligence: JSDocIntelligence): void {
223
+ if (!intelligence.usagePatterns) {
224
+ intelligence.usagePatterns = { dos: [], nevers: [] };
225
+ }
226
+
227
+ // Match DO: patterns - use negative lookahead to match any chars except keywords
228
+ const doMatches = value.matchAll(/DO:\s*((?:(?!DO:|NEVER:).)+?)(?=(?:DO:|NEVER:|$))/gi);
229
+ for (const match of doMatches) {
230
+ const text = match[1]?.trim();
231
+ if (text) intelligence.usagePatterns.dos.push(text);
232
+ }
233
+
234
+ // Match NEVER: patterns - use negative lookahead to match any chars except keywords
235
+ const neverMatches = value.matchAll(/NEVER:\s*((?:(?!DO:|NEVER:).)+?)(?=(?:DO:|NEVER:|$))/gi);
236
+ for (const match of neverMatches) {
237
+ const text = match[1]?.trim();
238
+ if (text) intelligence.usagePatterns.nevers.push(text);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Extract the full value from a JSDoc tag (name + description)
244
+ */
245
+ function getTagValue(tag: Spec): string {
246
+ const parts: string[] = [];
247
+ if (tag.name) parts.push(tag.name);
248
+ if (tag.description) parts.push(tag.description);
249
+ return parts.join(' ').trim();
250
+ }
251
+
252
+ /**
253
+ * Parse description from JSDoc (first line before tags)
254
+ */
255
+ export function parseDescription(source: string): string | undefined {
256
+ const blocks = parse(source);
257
+ if (blocks.length === 0) return undefined;
258
+
259
+ const description = blocks[0]?.description;
260
+ return description ? description.trim() : undefined;
261
+ }
262
+
263
+ // ==================== Extraction Utilities ====================
264
+
265
+ /**
266
+ * Extract variants from component source
267
+ * Looks for variant prop type definitions
268
+ */
269
+ export function extractVariants(source: string): string[] {
270
+ // Match variant type like: variant?: 'default' | 'secondary' | 'destructive'
271
+ const match = source.match(/variant\??\s*:\s*(['"][^'"]+['"](?:\s*\|\s*['"][^'"]+['"])*)/);
272
+
273
+ if (!match?.[1]) return ['default'];
274
+
275
+ const values: string[] = [];
276
+ const allMatches = [...match[1].matchAll(/['"]([^'"]+)['"]/g)];
277
+
278
+ for (const m of allMatches) {
279
+ const val = m[1];
280
+ if (val) values.push(val);
281
+ }
282
+
283
+ return values.length > 0 ? values : ['default'];
284
+ }
285
+
286
+ /**
287
+ * Extract sizes from component source
288
+ */
289
+ export function extractSizes(source: string): string[] {
290
+ // Match size type like: size?: 'sm' | 'default' | 'lg'
291
+ const match = source.match(/size\??\s*:\s*(['"][^'"]+['"](?:\s*\|\s*['"][^'"]+[''])*)/);
292
+
293
+ if (!match?.[1]) return ['default'];
294
+
295
+ const values: string[] = [];
296
+ const allMatches = [...match[1].matchAll(/['"]([^'"]+)['"]/g)];
297
+
298
+ for (const m of allMatches) {
299
+ const val = m[1];
300
+ if (val) values.push(val);
301
+ }
302
+
303
+ return values.length > 0 ? values : ['default'];
304
+ }
305
+
306
+ /**
307
+ * Extract external dependencies from imports
308
+ */
309
+ export function extractDependencies(source: string): string[] {
310
+ const deps: string[] = [];
311
+ const allMatches = [...source.matchAll(/import\s+.*?from\s+['"]([^'"]+)['"]/g)];
312
+
313
+ for (const match of allMatches) {
314
+ const pkg = match[1];
315
+ // Only include external packages, not relative imports
316
+ if (pkg && !pkg.startsWith('.') && !pkg.startsWith('/')) {
317
+ if (!deps.includes(pkg)) {
318
+ deps.push(pkg);
319
+ }
320
+ }
321
+ }
322
+
323
+ return deps;
324
+ }
325
+
326
+ /**
327
+ * Extract primitive dependencies from source
328
+ */
329
+ export function extractPrimitiveDependencies(source: string): string[] {
330
+ const primitives: string[] = [];
331
+ const allMatches = [...source.matchAll(/import\s+.*?from\s+['"]([^'"]+)['"]/g)];
332
+
333
+ for (const match of allMatches) {
334
+ const pkg = match[1];
335
+ // Check if it's a primitive import
336
+ if (pkg && (pkg.includes('/primitives/') || pkg.includes('../primitives/'))) {
337
+ const primitiveName = pkg
338
+ .split('/')
339
+ .pop()
340
+ ?.replace(/\.(ts|tsx)$/, '');
341
+ if (primitiveName && !primitives.includes(primitiveName)) {
342
+ primitives.push(primitiveName);
343
+ }
344
+ }
345
+ }
346
+
347
+ return primitives;
348
+ }
349
+
350
+ /** Maps lowercase JSDoc tag names to their JSDocDependencies field */
351
+ const DEP_TAG_MAP: Record<string, keyof JSDocDependencies> = {
352
+ dependencies: 'runtime',
353
+ devdependencies: 'dev',
354
+ 'internal-dependencies': 'internal',
355
+ internaldependencies: 'internal',
356
+ };
357
+
358
+ /**
359
+ * Extract structured dependency information from JSDoc tags
360
+ *
361
+ * Parses three dependency tags from JSDoc comments:
362
+ * - @dependencies - runtime dependencies (e.g., nanostores@^0.11.0)
363
+ * - @devDependencies - dev dependencies
364
+ * - @internal-dependencies - internal workspace dependencies (e.g., @rafters/color-utils)
365
+ *
366
+ * Values are split on whitespace. Tags present but empty produce empty arrays.
367
+ * Missing tags also produce empty arrays.
368
+ */
369
+ export function extractJSDocDependencies(source: string): JSDocDependencies {
370
+ const result: JSDocDependencies = { runtime: [], dev: [], internal: [] };
371
+ const blocks = parse(source);
372
+ if (blocks.length === 0) return result;
373
+
374
+ for (const block of blocks) {
375
+ for (const tag of block.tags) {
376
+ const field = DEP_TAG_MAP[tag.tag.toLowerCase()];
377
+ if (!field) continue;
378
+
379
+ const value = getTagValue(tag).trim();
380
+ if (value) {
381
+ const tokens = value.split(/\s+/).filter(Boolean);
382
+ const validTokens: string[] = [];
383
+ for (const token of tokens) {
384
+ if (token.startsWith('(')) break;
385
+ validTokens.push(token);
386
+ }
387
+ result[field].push(...validTokens);
388
+ }
389
+ }
390
+ }
391
+
392
+ return {
393
+ runtime: [...new Set(result.runtime)],
394
+ dev: [...new Set(result.dev)],
395
+ internal: [...new Set(result.internal)],
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Convert kebab-case to Title Case
401
+ */
402
+ export function toDisplayName(name: string): string {
403
+ return name
404
+ .split('-')
405
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
406
+ .join(' ');
407
+ }
408
+
409
+ // ==================== Validation ====================
410
+
411
+ /** The 6 required intelligence tags for a complete component */
412
+ const REQUIRED_TAGS = [
413
+ 'cognitiveLoad',
414
+ 'attentionEconomics',
415
+ 'accessibility',
416
+ 'trustBuilding',
417
+ 'semanticMeaning',
418
+ 'usagePatterns',
419
+ ] as const;
420
+
421
+ /**
422
+ * A warning produced when component intelligence metadata is incomplete or suspect
423
+ */
424
+ export interface IntelligenceWarning {
425
+ /** Warning severity: "missing" for absent data, "empty" for present but hollow data */
426
+ level: 'missing' | 'empty';
427
+ /** Human-readable warning message */
428
+ message: string;
429
+ }
430
+
431
+ /**
432
+ * Validate component intelligence metadata for completeness.
433
+ *
434
+ * Detects two classes of problems:
435
+ * 1. Missing required intelligence tags (all 6 should be present)
436
+ * 2. usagePatterns present but with zero DO: or NEVER: lines extracted
437
+ * (suggests a parsing failure or malformed JSDoc)
438
+ *
439
+ * Returns an empty array when the intelligence is complete and well-formed.
440
+ */
441
+ export function validateComponentIntelligence(
442
+ componentName: string,
443
+ intelligence: JSDocIntelligence | undefined,
444
+ ): IntelligenceWarning[] {
445
+ const warnings: IntelligenceWarning[] = [];
446
+
447
+ if (!intelligence) {
448
+ warnings.push({
449
+ level: 'missing',
450
+ message: `${componentName}: No intelligence metadata found. All 6 tags are required.`,
451
+ });
452
+ return warnings;
453
+ }
454
+
455
+ for (const tag of REQUIRED_TAGS) {
456
+ const value = intelligence[tag];
457
+ if (value === undefined || value === null) {
458
+ warnings.push({
459
+ level: 'missing',
460
+ message: `${componentName}: Missing required intelligence tag "@${tag}".`,
461
+ });
462
+ }
463
+ }
464
+
465
+ // Check for hollow usagePatterns (tag present but no DO:/NEVER: extracted)
466
+ if (intelligence.usagePatterns) {
467
+ const { dos, nevers } = intelligence.usagePatterns;
468
+ if (dos.length === 0 && nevers.length === 0) {
469
+ warnings.push({
470
+ level: 'empty',
471
+ message: `${componentName}: @usage-patterns present but zero DO: or NEVER: lines extracted. Check JSDoc format.`,
472
+ });
473
+ }
474
+ }
475
+
476
+ return warnings;
477
+ }