@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 +21 -0
- package/README.md +121 -0
- package/package.json +43 -0
- package/src/component-intelligence.ts +477 -0
- package/src/fill-signature.ts +217 -0
- package/src/import-pending.ts +210 -0
- package/src/index.ts +5 -0
- package/src/types.ts +651 -0
- package/src/version.ts +7 -0
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
|
+
}
|